From 7dedaa0344a7c29523d3e5add541f7a6275b4043 Mon Sep 17 00:00:00 2001 From: Andreas Martens Date: Mon, 30 Jul 2018 14:42:02 +0100 Subject: [PATCH 0001/1131] add comments to the OVS service --- daemon/core/services/sdn.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/daemon/core/services/sdn.py b/daemon/core/services/sdn.py index 9d8a26ba..44a40f48 100644 --- a/daemon/core/services/sdn.py +++ b/daemon/core/services/sdn.py @@ -51,10 +51,15 @@ class OvsService(SdnService): cfg = "#!/bin/sh\n" cfg += "# auto-generated by OvsService (OvsService.py)\n" - cfg += "/etc/init.d/openvswitch-switch start < /dev/null\n" - cfg += "ovs-vsctl add-br ovsbr0\n" - cfg += "ifconfig ovsbr0 up\n" + cfg += "## First make sure that the ovs services are up and running\n" + cfg += "/etc/init.d/openvswitch-switch start < /dev/null\n\n" + cfg += "## create the switch itself, set the fail mode to secure, \n" + cfg += "## this stops it from routing traffic without defined flows.\n" + cfg += "## remove the -- and everything after if you want it to act as a regular switch\n" + cfg += "ovs-vsctl add-br ovsbr0 -- set Bridge ovsbr0 fail-mode=secure\n" + cfg += "\n## Now add all our interfaces as ports to the switch\n" + portnum = 1 for ifc in node.netifs(): if hasattr(ifc, 'control') and ifc.control is True: continue @@ -62,9 +67,10 @@ class OvsService(SdnService): ifnum = ifnumstr[0] # create virtual interfaces + cfg += "## Create a veth pair to send the data to\n" cfg += "ip link add rtr%s type veth peer name sw%s\n" % (ifnum, ifnum) - cfg += "ifconfig rtr%s up\n" % ifnum - cfg += "ifconfig sw%s up\n" % ifnum +# cfg += "ifconfig rtr%s up\n" % ifnum +# cfg += "ifconfig sw%s up\n" % ifnum # remove ip address of eths because quagga/zebra will assign same IPs to rtr interfaces # or assign them manually to rtr interfaces if zebra is not running @@ -81,17 +87,31 @@ class OvsService(SdnService): raise ValueError("invalid address: %s" % ifcaddr) # add interfaces to bridge - cfg += "ovs-vsctl add-port ovsbr0 eth%s\n" % ifnum - cfg += "ovs-vsctl add-port ovsbr0 sw%s\n" % ifnum + # Make port numbers explicit so they're easier to follow in reading the script + cfg += "## Add the CORE interface to the switch\n" + cfg += "ovs-vsctl add-port ovsbr0 eth%s -- set Interface eth%s ofport_request=%d\n" % (ifnum, ifnum, portnum) + cfg += "## And then add its sibling veth interface\n" + cfg += "ovs-vsctl add-port ovsbr0 sw%s -- set Interface sw%s ofport_request=%d\n" % (ifnum, ifnum, portnum+1) + cfg += "## start them up so we can send/receive data\n" + cfg += "ovs-ofctl mod-port ovsbr0 eth%s up\n" % ifnum + cfg += "ovs-ofctl mod-port ovsbr0 sw%s up\n" % ifnum + cfg += "## Bring up the lower part of the veth pair\n" + cfg += "ip link set dev rtr%s up\n" % ifnum + portnum += 2 # Add rule for default controller if there is one local (even if the controller is not local, it finds it) + cfg += "\n## We assume there will be an SDN controller on the other end of this, \n" + cfg += "## but it will still function if there's not\n" cfg += "ovs-vsctl set-controller ovsbr0 tcp:127.0.0.1:6633\n" + cfg += "\n## Now to create some default flows, \n" + cfg += "## if the above controller will be present then you probably want to delete them\n" # Setup default flows portnum = 1 for ifc in node.netifs(): if hasattr(ifc, 'control') and ifc.control is True: continue + cfg += "## Take the data from the CORE interface and put it on the veth and vice versa\n" cfg += "ovs-ofctl add-flow ovsbr0 priority=1000,in_port=%d,action=output:%d\n" % (portnum, portnum + 1) cfg += "ovs-ofctl add-flow ovsbr0 priority=1000,in_port=%d,action=output:%d\n" % (portnum + 1, portnum) portnum += 2 From 939921812358ab4502b17df847494977ce30c6ae Mon Sep 17 00:00:00 2001 From: Jeff Ahrenholz Date: Wed, 10 Oct 2018 09:58:18 -0700 Subject: [PATCH 0002/1131] enable OSPFv2 fast convergence, and fix router-id for IPv6-only nodes --- daemon/core/services/quagga.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/daemon/core/services/quagga.py b/daemon/core/services/quagga.py index de25ecda..a7b85ed8 100644 --- a/daemon/core/services/quagga.py +++ b/daemon/core/services/quagga.py @@ -242,7 +242,7 @@ class QuaggaService(CoreService): if a.find(".") >= 0: return a.split('/')[0] # raise ValueError, "no IPv4 address found for router ID" - return "0.0.0.0" + return "0.0.0.%d" % node.objid @staticmethod def rj45check(ifc): @@ -329,22 +329,18 @@ class Ospfv2(QuaggaService): return cfg @classmethod - def generatequaggaifcconfig(cls, node, ifc): - return cls.mtucheck(ifc) - # cfg = cls.mtucheck(ifc) + def generatequaggaifcconfig(cls, node, ifc): + cfg = cls.mtucheck(ifc) # external RJ45 connections will use default OSPF timers - # if cls.rj45check(ifc): - # return cfg - # cfg += cls.ptpcheck(ifc) - - # return cfg + """\ - - -# ip ospf hello-interval 2 -# ip ospf dead-interval 6 -# ip ospf retransmit-interval 5 -# """ + if cls.rj45check(ifc): + return cfg + cfg += cls.ptpcheck(ifc) + return cfg + """\ + ip ospf hello-interval 2 + ip ospf dead-interval 6 + ip ospf retransmit-interval 5 +""" class Ospfv3(QuaggaService): """ From 04076450616e467e8fbec9be9ad28dfdf96d08c3 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 5 Feb 2020 15:09:33 -0800 Subject: [PATCH 0003/1131] replace tkinter errormessage with custom create error dialog --- daemon/core/gui/coreclient.py | 27 +++++++++++-------- .../core/gui/dialogs/configserviceconfig.py | 12 ++++++--- daemon/core/gui/dialogs/emaneconfig.py | 10 ++++--- daemon/core/gui/dialogs/mobilityconfig.py | 6 +++-- daemon/core/gui/dialogs/mobilityplayer.py | 6 ++--- daemon/core/gui/dialogs/nodeconfigservice.py | 3 ++- daemon/core/gui/dialogs/nodeservice.py | 7 ++++- daemon/core/gui/dialogs/serviceconfig.py | 18 +++++++++---- daemon/core/gui/dialogs/sessionoptions.py | 9 ++++--- daemon/core/gui/dialogs/sessions.py | 9 ++++--- daemon/core/gui/dialogs/wlanconfig.py | 6 +++-- daemon/core/gui/errors.py | 26 +++++++++++++++--- daemon/core/gui/graph/node.py | 8 +++--- daemon/core/gui/menuaction.py | 23 +++++++++++----- daemon/core/gui/toolbar.py | 18 ++++++++----- 15 files changed, 132 insertions(+), 56 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 11030eda..bb5dd45a 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -335,7 +335,7 @@ class CoreClient: self.parse_metadata(response.config) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + self.app.after(0, show_grpc_error, e, self.app, self.app) # update ui to represent current state self.app.after(0, self.app.joined_session_update) @@ -423,16 +423,21 @@ class CoreClient: ) self.join_session(response.session_id, query_location=False) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + self.app.after(0, show_grpc_error, e, self.app, self.app) - def delete_session(self, session_id: int = None): + def delete_session(self, session_id: int = None, parent_frame=None): if session_id is None: session_id = self.session_id try: response = self.client.delete_session(session_id) logging.info("deleted session(%s), Result: %s", session_id, response) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + # use the right master widget so the error dialog displays right on top of it + master = self.app + if parent_frame: + master = parent_frame + print("stop session error") + self.app.after(0, show_grpc_error, e, master, self.app) def set_up(self): """ @@ -468,7 +473,7 @@ class CoreClient: x.node_type: set(x.services) for x in response.defaults } except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + self.app.after(0, show_grpc_error, e, self.app, self.app) self.app.close() def edit_node(self, core_node: core_pb2.Node): @@ -477,7 +482,7 @@ class CoreClient: self.session_id, core_node.id, core_node.position, source=GUI_SOURCE ) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + self.app.after(0, show_grpc_error, e, self.app, self.app) def start_session(self) -> core_pb2.StartSessionResponse: nodes = [x.core_node for x in self.canvas_nodes.values()] @@ -521,7 +526,7 @@ class CoreClient: if response.result: self.set_metadata() except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + self.app.after(0, show_grpc_error, e, self.app, self.app) return response def stop_session(self, session_id: int = None) -> core_pb2.StartSessionResponse: @@ -532,7 +537,7 @@ class CoreClient: response = self.client.stop_session(session_id) logging.info("stopped session(%s), result: %s", session_id, response) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + self.app.after(0, show_grpc_error, e, self.app, self.app) return response def show_mobility_players(self): @@ -577,7 +582,7 @@ class CoreClient: logging.info("launching terminal %s", cmd) os.system(cmd) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + self.app.after(0, show_grpc_error, e, self.app, self.app) def save_xml(self, file_path: str): """ @@ -590,7 +595,7 @@ class CoreClient: response = self.client.save_xml(self.session_id, file_path) logging.info("saved xml file %s, result: %s", file_path, response) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + self.app.after(0, show_grpc_error, e, self.app, self.app) def open_xml(self, file_path: str): """ @@ -601,7 +606,7 @@ class CoreClient: logging.info("open xml file %s, response: %s", file_path, response) self.join_session(response.session_id) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + self.app.after(0, show_grpc_error, e, self.app, self.app) def get_node_service( self, node_id: int, service_name: str diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py index f92d23bb..3aaac1a4 100644 --- a/daemon/core/gui/dialogs/configserviceconfig.py +++ b/daemon/core/gui/dialogs/configserviceconfig.py @@ -65,8 +65,13 @@ class ConfigServiceConfigDialog(Dialog): self.config_frame = None self.default_config = None self.config = None + + self.has_error = False + self.load() - self.draw() + + if not self.has_error: + self.draw() def load(self): try: @@ -106,7 +111,8 @@ class ConfigServiceConfigDialog(Dialog): self.modified_files.add(file) self.temp_service_files[file] = data except grpc.RpcError as e: - show_grpc_error(e) + self.has_error = True + show_grpc_error(e, self.app, self.app) def draw(self): self.top.columnconfigure(0, weight=1) @@ -327,7 +333,7 @@ class ConfigServiceConfigDialog(Dialog): all_current = current_listbox.get(0, tk.END) current_listbox.itemconfig(all_current.index(self.service_name), bg="green") except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.top, self.app) self.destroy() def handle_template_changed(self, event: tk.Event): diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index 1bcdd78b..66ffce01 100644 --- a/daemon/core/gui/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -65,14 +65,17 @@ class EmaneModelDialog(Dialog): self.model = f"emane_{model}" self.interface = interface self.config_frame = None + self.error = False try: self.config = self.app.core.get_emane_model_config( self.node.id, self.model, self.interface ) except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.app, self.app) + self.error = True self.destroy() - self.draw() + if not self.error: + self.draw() def draw(self): self.top.columnconfigure(0, weight=1) @@ -225,7 +228,8 @@ class EmaneConfigDialog(Dialog): dialog = EmaneModelDialog( self, self.app, self.canvas_node.core_node, model_name ) - dialog.show() + if not dialog.error: + dialog.show() def emane_model_change(self, event: tk.Event): """ diff --git a/daemon/core/gui/dialogs/mobilityconfig.py b/daemon/core/gui/dialogs/mobilityconfig.py index 18e62a17..f7192ca4 100644 --- a/daemon/core/gui/dialogs/mobilityconfig.py +++ b/daemon/core/gui/dialogs/mobilityconfig.py @@ -29,12 +29,14 @@ class MobilityConfigDialog(Dialog): self.canvas_node = canvas_node self.node = canvas_node.core_node self.config_frame = None + self.has_error = False try: self.config = self.app.core.get_mobility_config(self.node.id) + self.draw() except grpc.RpcError as e: - show_grpc_error(e) + self.has_error = True + show_grpc_error(e, self.app, self.app) self.destroy() - self.draw() def draw(self): self.top.columnconfigure(0, weight=1) diff --git a/daemon/core/gui/dialogs/mobilityplayer.py b/daemon/core/gui/dialogs/mobilityplayer.py index 873a2b37..dd2e9f9a 100644 --- a/daemon/core/gui/dialogs/mobilityplayer.py +++ b/daemon/core/gui/dialogs/mobilityplayer.py @@ -153,7 +153,7 @@ class MobilityPlayerDialog(Dialog): session_id, self.node.id, MobilityAction.START ) except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.top, self.app) def click_pause(self): self.set_pause() @@ -163,7 +163,7 @@ class MobilityPlayerDialog(Dialog): session_id, self.node.id, MobilityAction.PAUSE ) except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.top, self.app) def click_stop(self): self.set_stop() @@ -173,4 +173,4 @@ class MobilityPlayerDialog(Dialog): session_id, self.node.id, MobilityAction.STOP ) except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.top, self.app) diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeconfigservice.py index 1230cede..e41290ed 100644 --- a/daemon/core/gui/dialogs/nodeconfigservice.py +++ b/daemon/core/gui/dialogs/nodeconfigservice.py @@ -124,7 +124,8 @@ class NodeConfigServiceDialog(Dialog): service_name=self.current.listbox.get(current_selection[0]), node_id=self.node_id, ) - dialog.show() + if not dialog.has_error: + dialog.show() else: messagebox.showinfo( "Node service configuration", "Select a service to configure" diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index c61983f7..d1972859 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -140,7 +140,12 @@ class NodeServiceDialog(Dialog): service_name=self.current.listbox.get(current_selection[0]), node_id=self.node_id, ) - dialog.show() + + # if error occurred when creating ServiceConfigDialog, don't show the dialog + if not dialog.error: + dialog.show() + else: + dialog.destroy() else: messagebox.showinfo( "Node service configuration", "Select a service to configure" diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index 804e7e3f..07a68c0e 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -64,10 +64,16 @@ class ServiceConfigDialog(Dialog): self.original_service_files = {} self.temp_service_files = {} self.modified_files = set() - self.load() - self.draw() - def load(self): + self.error = True + + load_result = self.load() + if load_result: + self.draw() + self.error = False + + def load(self) -> bool: + result = False try: self.app.core.create_nodes_and_links() default_config = self.app.core.get_node_service( @@ -108,8 +114,10 @@ class ServiceConfigDialog(Dialog): ): for file, data in file_configs[self.node_id][self.service_name].items(): self.temp_service_files[file] = data + result = True except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.master, self.app) + return result def draw(self): self.top.columnconfigure(0, weight=1) @@ -444,7 +452,7 @@ class ServiceConfigDialog(Dialog): all_current = current_listbox.get(0, tk.END) current_listbox.itemconfig(all_current.index(self.service_name), bg="green") except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.top, self.app) self.destroy() def display_service_file_data(self, event: tk.Event): diff --git a/daemon/core/gui/dialogs/sessionoptions.py b/daemon/core/gui/dialogs/sessionoptions.py index ffd61340..a3f738a7 100644 --- a/daemon/core/gui/dialogs/sessionoptions.py +++ b/daemon/core/gui/dialogs/sessionoptions.py @@ -17,8 +17,10 @@ class SessionOptionsDialog(Dialog): def __init__(self, master: "Application", app: "Application"): super().__init__(master, app, "Session Options", modal=True) self.config_frame = None + self.has_error = False self.config = self.get_config() - self.draw() + if not self.has_error: + self.draw() def get_config(self): try: @@ -26,7 +28,8 @@ class SessionOptionsDialog(Dialog): response = self.app.core.client.get_session_options(session_id) return response.config except grpc.RpcError as e: - show_grpc_error(e) + self.has_error = True + show_grpc_error(e, self.app, self.app) self.destroy() def draw(self): @@ -53,5 +56,5 @@ class SessionOptionsDialog(Dialog): response = self.app.core.client.set_session_options(session_id, config) logging.info("saved session config: %s", response) except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.top, self.app) self.destroy() diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index 3af76c52..6f7d5a9a 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -25,8 +25,10 @@ class SessionsDialog(Dialog): self.selected = False self.selected_id = None self.tree = None + self.error = False self.sessions = self.get_sessions() - self.draw() + if not self.error: + self.draw() def get_sessions(self) -> Iterable[core_pb2.SessionSummary]: try: @@ -34,7 +36,8 @@ class SessionsDialog(Dialog): logging.info("sessions: %s", response) return response.sessions except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.app, self.app) + self.error = True self.destroy() def draw(self): @@ -211,7 +214,7 @@ class SessionsDialog(Dialog): item = self.tree.selection() if item: sid = int(self.tree.item(item, "text")) - self.app.core.delete_session(sid) + self.app.core.delete_session(sid, self.top) self.tree.delete(item[0]) if sid == self.app.core.session_id: self.click_new() diff --git a/daemon/core/gui/dialogs/wlanconfig.py b/daemon/core/gui/dialogs/wlanconfig.py index 264b9e2e..fb807f57 100644 --- a/daemon/core/gui/dialogs/wlanconfig.py +++ b/daemon/core/gui/dialogs/wlanconfig.py @@ -27,12 +27,14 @@ class WlanConfigDialog(Dialog): self.canvas_node = canvas_node self.node = canvas_node.core_node self.config_frame = None + self.error = False try: self.config = self.app.core.get_wlan_config(self.node.id) + self.draw() except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.app, self.app) + self.error = True self.destroy() - self.draw() def draw(self): self.top.columnconfigure(0, weight=1) diff --git a/daemon/core/gui/errors.py b/daemon/core/gui/errors.py index 18e025db..291002be 100644 --- a/daemon/core/gui/errors.py +++ b/daemon/core/gui/errors.py @@ -1,12 +1,32 @@ -from tkinter import messagebox from typing import TYPE_CHECKING +from core.gui.dialogs.dialog import Dialog +from core.gui.widgets import CodeText + if TYPE_CHECKING: import grpc + from core.gui.app import Application -def show_grpc_error(e: "grpc.RpcError"): +class ErrorDialog(Dialog): + def __init__(self, master, app: "Application", title: str, details: str): + super().__init__(master, app, title, modal=True) + self.error_message = None + self.details = details + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + self.error_message = CodeText(self.top) + self.error_message.text.insert("1.0", self.details) + self.error_message.text.config(state="disabled") + self.error_message.grid(row=0, column=0, sticky="nsew") + + +def show_grpc_error(e: "grpc.RpcError", master, app: "Application"): title = [x.capitalize() for x in e.code().name.lower().split("_")] title = " ".join(title) title = f"GRPC {title}" - messagebox.showerror(title, e.details()) + dialog = ErrorDialog(master, app, title, e.details()) + dialog.show() diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index de3a9204..5ce89b9d 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -165,7 +165,7 @@ class CanvasNode: output = self.app.core.run(self.core_node.id) self.tooltip.text.set(output) except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.app, self.app) def on_leave(self, event: tk.Event): self.tooltip.on_leave(event) @@ -240,12 +240,14 @@ class CanvasNode: def show_wlan_config(self): self.canvas.context = None dialog = WlanConfigDialog(self.app, self.app, self) - dialog.show() + if not dialog.error: + dialog.show() def show_mobility_config(self): self.canvas.context = None dialog = MobilityConfigDialog(self.app, self.app, self) - dialog.show() + if not dialog.has_error: + dialog.show() def show_mobility_player(self): self.canvas.context = None diff --git a/daemon/core/gui/menuaction.py b/daemon/core/gui/menuaction.py index 292415f2..145db0cf 100644 --- a/daemon/core/gui/menuaction.py +++ b/daemon/core/gui/menuaction.py @@ -9,6 +9,8 @@ import webbrowser from tkinter import filedialog, messagebox from typing import TYPE_CHECKING +import grpc + from core.gui.appconfig import XMLS_PATH from core.gui.dialogs.about import AboutDialog from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog @@ -32,12 +34,18 @@ class MenuAction: self.app = app self.canvas = app.canvas - def cleanup_old_session(self, session_id): - response = self.app.core.stop_session() - self.app.core.delete_session(session_id) - logging.info( - "Stop session(%s) and delete it, result: %s", session_id, response.result - ) + def cleanup_old_session(self, session_id: int): + try: + res = self.app.core.client.get_session(session_id) + logging.debug("retrieve session(%s), %s", session_id, res) + stop_response = self.app.core.stop_session() + logging.debug("stop session(%s), result: %s", session_id, stop_response) + delete_response = self.app.core.delete_session(session_id) + logging.debug( + "deleted session(%s), result: %s", session_id, delete_response + ) + except grpc.RpcError: + logging.debug("session is not alive") def prompt_save_running_session(self, quitapp: bool = False): """ @@ -121,7 +129,8 @@ class MenuAction: def session_options(self): logging.debug("Click options") dialog = SessionOptionsDialog(self.app, self.app) - dialog.show() + if not dialog.has_error: + dialog.show() def session_change_sessions(self): logging.debug("Click change sessions") diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 063c2e5b..c6cf1ce1 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -2,7 +2,7 @@ import logging import time import tkinter as tk from functools import partial -from tkinter import messagebox, ttk +from tkinter import ttk from tkinter.font import Font from typing import TYPE_CHECKING, Callable @@ -262,9 +262,13 @@ class Toolbar(ttk.Frame): self.set_runtime() self.app.core.set_metadata() self.app.core.show_mobility_players() - else: - message = "\n".join(response.exceptions) - messagebox.showerror("Start Error", message) + # else: + # message = "\n".join(response.exceptions) + # print("start error") + # print(response) + # dialog = ErrorDialog(self.app, self.app, "Start ERROR", message) + # dialog.show() + # messagebox.showerror("Start Error", message) def set_runtime(self): self.runtime_frame.tkraise() @@ -431,8 +435,10 @@ class Toolbar(ttk.Frame): message = f"Stopped in {total:.3f} seconds" self.app.statusbar.set_status(message) self.app.canvas.stopped_session() - if not response.result: - messagebox.showerror("Stop Error", "Errors stopping session") + # if not response.result: + # dialog = ErrorDialog(self.app, self.app, "Stop ERROR", "Error stopping session") + # dialog.show() + # # messagebox.showerror("Stop Error", "Errors stopping session") def update_annotation(self, image: "ImageTk.PhotoImage", shape_type: ShapeType): logging.debug("clicked annotation: ") From ee0c63e4a1f636e244f04c80ee6d0bc7cfaca333 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 5 Feb 2020 15:53:14 -0800 Subject: [PATCH 0004/1131] change some variable names to be more informative, add an image to error dialog --- daemon/core/gui/data/icons/error.png | Bin 0 -> 2258 bytes daemon/core/gui/dialogs/emaneconfig.py | 9 ++++----- daemon/core/gui/dialogs/nodeservice.py | 2 +- daemon/core/gui/dialogs/serviceconfig.py | 11 ++++------- daemon/core/gui/dialogs/sessions.py | 6 +++--- daemon/core/gui/dialogs/wlanconfig.py | 4 ++-- daemon/core/gui/errors.py | 8 +++++++- daemon/core/gui/graph/node.py | 2 +- daemon/core/gui/images.py | 1 + 9 files changed, 23 insertions(+), 20 deletions(-) create mode 100644 daemon/core/gui/data/icons/error.png diff --git a/daemon/core/gui/data/icons/error.png b/daemon/core/gui/data/icons/error.png new file mode 100644 index 0000000000000000000000000000000000000000..d73d1dd402887d094863eba351eb62b72dec7d22 GIT binary patch literal 2258 zcmV;@2rc)CP)zV$55q=N^QZh7OFB>fv~hB zkdR-SeaAntA)913d2inh!sz#}eed4$-v=)k~FgBl``0nTmMu*Qhl2`ml5Cjd?ubVoFm`hBSk zN;1H^Tent(d#-*D^oynPun1LU_rPHP6L)lU4EQDXt^C<|yxGD00(7}w+8M@Ms_w%L z?d^Z{L*&ZfukQaf7FV(IeuBe(V4ebYh>y^NBc)GB0;}($pSRA}mW|5k#Ep`BN zMBLQ*4T*<6>)@r+`FMP*AbS9(WYs1LuFm3oxf3aO8QxoqzFlF9H9%HWK3(75_QG@> zr=0=kHzXer65JkO~(BLYupw+_lNxQPJlwv?kV1tH)H0=UQ7Ai@3iAW?54W zFiZL{&HZC9W@m98ylhhhK$#ldmtMbV(UiIrYd9T?l^b#PVB6u9xj2dL$%?GJiYyqi zDhinEI@uTA-MY2t`YLLGit@_uBiv&D1=LV?gSH|f#`r~3E(FClp zDlM#-c{DVeVkQ2$gS3MgH>na@#fKoYX-=we$KWe^T6ez zo1Jex)5+kFq6)kByvK01z;rj@Z0-WFyA+JbXY)klppvvHMCvm*_ZE)nt_z*-Wv)H< z_PJc@F0vFB3}No+v#A=QY#jYnYOky%inFP!==RANC<~8I2r|RP|7EAOt8n&M zI_^G`Ox8fo0O4@>ek>-Trlk+LZR(g`RWV*%q-qNB<%-ifdH)aqPPyye54l0Rm}S;j z&(31jPp@pGYHWIh92xgq2xeIy@*xk2lvlVTgSS4q3B`OTtRBRAJXK{fZ7@#25=O! zxhwEqRdi&`qkltHAa1t|V^;gy$5%&6SOPhi#zAD+RbMm{G5V(m4}GIWO`p!G)EO`e zC*F-z2G0#f4N$T<&GM_r9llSbxO351nXYkunQwZjbTR8ncZdQ#Fct+$szXVITXKTZ za%TmRDnoU|VRcJsE7082i!_#&8>ozod35fjfd#%5*w77CrPcGRBM!%2XhKAoH@Czi z2LUJ9#qjQ1!S8BN(l8bUPWhI7+aTtyV2-G^Cd4h(!OmMGn!#CB(gZ#U*9;)OuAYda zI1dF2MXK<-V^=x!S`S$_SavtE?e=$1LNw0+2KTrx;*a+rb-|L6C%=1+Ejv#0nXjGU z(Xan25HCWCll13{6gdGo0}QDi20SfhnQJEr7(gOlv`ddT|c+YcP*g7Jyaf$6Bs zoqd=l|1rN1e)jF#*tV&LPp+TKzGoTIeGonP`AXu0lFGhDCG-aC6Q1so@Ixr9$_L3;N3Xg9neOvb#{*O^TiMsnpRz z4Ml^+z+nFq3UADuPS*`5GLhL&7Hga`{mLare*)%MSL=E}={C44qV4T}FVNY-u=n@UT}BU*`f1sZ@u_IxA4# z^sn8_Lg5XAyTZ@lp@f5x=%M}Z^bhv05_BgwMzZ7rKj;-!P3Q0mA=pP#t;rT++#g~4 za+PPuadBmP{mTblE15%36*LwLU8tyd9Oaw9!l3*-;vb+-)kh*fmwo#JMJIb7PR3%P z^sLHlD!LuG*B0LiI4bC_=)l0<8|K5wLb#YnG!3ghn!Am4H>H1Vk_FyS~(mHM55GK0%kMVX;87DzyT0G#5? bool: - result = False try: self.app.core.create_nodes_and_links() default_config = self.app.core.get_node_service( @@ -114,10 +112,9 @@ class ServiceConfigDialog(Dialog): ): for file, data in file_configs[self.node_id][self.service_name].items(): self.temp_service_files[file] = data - result = True except grpc.RpcError as e: + self.has_error = True show_grpc_error(e, self.master, self.app) - return result def draw(self): self.top.columnconfigure(0, weight=1) diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index 6f7d5a9a..e540a3ca 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -25,9 +25,9 @@ class SessionsDialog(Dialog): self.selected = False self.selected_id = None self.tree = None - self.error = False + self.has_error = False self.sessions = self.get_sessions() - if not self.error: + if not self.has_error: self.draw() def get_sessions(self) -> Iterable[core_pb2.SessionSummary]: @@ -37,7 +37,7 @@ class SessionsDialog(Dialog): return response.sessions except grpc.RpcError as e: show_grpc_error(e, self.app, self.app) - self.error = True + self.has_error = True self.destroy() def draw(self): diff --git a/daemon/core/gui/dialogs/wlanconfig.py b/daemon/core/gui/dialogs/wlanconfig.py index fb807f57..d6da667e 100644 --- a/daemon/core/gui/dialogs/wlanconfig.py +++ b/daemon/core/gui/dialogs/wlanconfig.py @@ -27,13 +27,13 @@ class WlanConfigDialog(Dialog): self.canvas_node = canvas_node self.node = canvas_node.core_node self.config_frame = None - self.error = False + self.has_error = False try: self.config = self.app.core.get_wlan_config(self.node.id) self.draw() except grpc.RpcError as e: show_grpc_error(e, self.app, self.app) - self.error = True + self.has_error = True self.destroy() def draw(self): diff --git a/daemon/core/gui/errors.py b/daemon/core/gui/errors.py index 291002be..b6152489 100644 --- a/daemon/core/gui/errors.py +++ b/daemon/core/gui/errors.py @@ -1,6 +1,8 @@ +from tkinter import ttk from typing import TYPE_CHECKING from core.gui.dialogs.dialog import Dialog +from core.gui.images import ImageEnum, Images from core.gui.widgets import CodeText if TYPE_CHECKING: @@ -18,10 +20,14 @@ class ErrorDialog(Dialog): def draw(self): self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) + image = Images.get(ImageEnum.ERROR, 36) + label = ttk.Label(self.top, image=image) + label.image = image + label.grid(row=0, column=0) self.error_message = CodeText(self.top) self.error_message.text.insert("1.0", self.details) self.error_message.text.config(state="disabled") - self.error_message.grid(row=0, column=0, sticky="nsew") + self.error_message.grid(row=1, column=0, sticky="nsew") def show_grpc_error(e: "grpc.RpcError", master, app: "Application"): diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 5ce89b9d..e6a84e72 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -240,7 +240,7 @@ class CanvasNode: def show_wlan_config(self): self.canvas.context = None dialog = WlanConfigDialog(self.app, self.app, self) - if not dialog.error: + if not dialog.has_error: dialog.show() def show_mobility_config(self): diff --git a/daemon/core/gui/images.py b/daemon/core/gui/images.py index 6bf23e24..cd472764 100644 --- a/daemon/core/gui/images.py +++ b/daemon/core/gui/images.py @@ -89,3 +89,4 @@ class ImageEnum(Enum): DELETE = "delete" SHUTDOWN = "shutdown" CANCEL = "cancel" + ERROR = "error" From 9b8caf813e0af53919584763d09b1f4f9d165f8f Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 7 Feb 2020 15:20:57 -0800 Subject: [PATCH 0005/1131] add link button to service configuration that links to github documentations, add link button to emane config, as well as emane model config that links to emane wiki page, sketch up the create IPsec tab to IPsec configuration --- daemon/core/gui/dialogs/dialog.py | 5 + daemon/core/gui/dialogs/emaneconfig.py | 47 ++++++- daemon/core/gui/dialogs/nodeservice.py | 3 +- daemon/core/gui/dialogs/serviceconfig.py | 149 ++++++++++++++++++++++- 4 files changed, 197 insertions(+), 7 deletions(-) diff --git a/daemon/core/gui/dialogs/dialog.py b/daemon/core/gui/dialogs/dialog.py index 00532793..21222f39 100644 --- a/daemon/core/gui/dialogs/dialog.py +++ b/daemon/core/gui/dialogs/dialog.py @@ -1,4 +1,5 @@ import tkinter as tk +import webbrowser from tkinter import ttk from typing import TYPE_CHECKING @@ -41,3 +42,7 @@ class Dialog(tk.Toplevel): frame.grid(row=row, sticky="nsew") frame.rowconfigure(0, weight=1) self.top.rowconfigure(frame.grid_info()["row"], weight=1) + + @classmethod + def navigate_link(cls, link: str): + webbrowser.open_new(link) diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index 1bcdd78b..c9760c00 100644 --- a/daemon/core/gui/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -3,6 +3,7 @@ emane configuration """ import tkinter as tk import webbrowser +from enum import Enum from tkinter import ttk from typing import TYPE_CHECKING, Any @@ -20,6 +21,18 @@ if TYPE_CHECKING: from core.gui.graph.node import CanvasNode +class EmaneModelEnum(Enum): + RFPIPE = "emane_rfpipe" + IEEE80211ABG = "emane_ieee80211abg" + COMMEFFECT = "emane_commeffect" + BYPASS = "emane_bypass" + TDMA = "emane_tdma" + + +def get_model(enum_class: EmaneModelEnum): + return enum_class.value + + class GlobalEmaneDialog(Dialog): def __init__(self, master: Any, app: "Application"): super().__init__(master, app, "EMANE Configuration", modal=True) @@ -38,7 +51,7 @@ class GlobalEmaneDialog(Dialog): def draw_buttons(self): frame = ttk.Frame(self.top) frame.grid(sticky="ew") - for i in range(2): + for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) button.grid(row=0, column=0, sticky="ew", padx=PADX) @@ -46,6 +59,15 @@ class GlobalEmaneDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") + button = ttk.Button( + frame, + text="Info Link", + command=lambda: self.navigate_link( + "https://github.com/adjacentlink/emane/wiki/Configuring-the-Emulator" + ), + ) + button.grid(row=0, column=2, sticky="ew") + def click_apply(self): self.config_frame.parse_config() self.destroy() @@ -86,7 +108,7 @@ class EmaneModelDialog(Dialog): def draw_buttons(self): frame = ttk.Frame(self.top) frame.grid(sticky="ew") - for i in range(2): + for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) button.grid(row=0, column=0, sticky="ew", padx=PADX) @@ -94,6 +116,9 @@ class EmaneModelDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") + button = ttk.Button(frame, text="Info Link ", command=self.wiki_link) + button.grid(row=0, column=2, sticky="ew") + def click_apply(self): self.config_frame.parse_config() self.app.core.set_emane_model_config( @@ -101,6 +126,24 @@ class EmaneModelDialog(Dialog): ) self.destroy() + def wiki_link(self): + if self.model == get_model(EmaneModelEnum.RFPIPE): + self.navigate_link( + "https://github.com/adjacentlink/emane/wiki/RF-Pipe-Model" + ) + elif self.model == get_model(EmaneModelEnum.COMMEFFECT): + self.navigate_link( + "https://github.com/adjacentlink/emane/wiki/Comm-Effect-Model" + ) + elif self.model == get_model(EmaneModelEnum.TDMA): + self.navigate_link("https://github.com/adjacentlink/emane/wiki/TDMA-Model") + elif self.model == get_model(EmaneModelEnum.IEEE80211ABG): + self.navigate_link( + "https://github.com/adjacentlink/emane/wiki/IEEE-802.11abg-Model" + ) + else: + return + class EmaneConfigDialog(Dialog): def __init__( diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index c61983f7..57f9bd20 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -133,11 +133,12 @@ class NodeServiceDialog(Dialog): def click_configure(self): current_selection = self.current.listbox.curselection() + service_name = self.current.listbox.get(current_selection[0]) if len(current_selection): dialog = ServiceConfigDialog( master=self, app=self.app, - service_name=self.current.listbox.get(current_selection[0]), + service_name=service_name, node_id=self.node_id, ) dialog.show() diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index 804e7e3f..3b728fe6 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -65,7 +65,7 @@ class ServiceConfigDialog(Dialog): self.temp_service_files = {} self.modified_files = set() self.load() - self.draw() + # self.draw() def load(self): try: @@ -108,8 +108,12 @@ class ServiceConfigDialog(Dialog): ): for file, data in file_configs[self.node_id][self.service_name].items(): self.temp_service_files[file] = data + self.draw() except grpc.RpcError as e: - show_grpc_error(e) + if not self.is_ipsec(): + show_grpc_error(e) + else: + self.draw() def draw(self): self.top.columnconfigure(0, weight=1) @@ -127,13 +131,139 @@ class ServiceConfigDialog(Dialog): # draw notebook self.notebook = ttk.Notebook(self.top) self.notebook.grid(sticky="nsew", pady=PADY) - self.draw_tab_files() + if not self.is_ipsec(): + self.draw_tab_files() + else: + self.draw_ipsec_tab() self.draw_tab_directories() self.draw_tab_startstop() self.draw_tab_configuration() self.draw_buttons() + def draw_ipsec_tab(self): + tab = ttk.Frame(self.notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew") + tab.columnconfigure(0, weight=1) + self.notebook.add(tab, text="IPsec") + + text = ( + "This IPsec service helper will assist with building an ipsec.sh file (located on the Files tab).\nThe IPsec service builds ESP tunnels between" + "the specified peers using the racoon IKEv2\nkeying daemon. You need to provide keys and the addresses of peers, along with the\nsubnet to tunnel." + ) + label = ttk.Label(tab, text=text) + label.grid(row=0, column=0, sticky="nsew") + + label_frame = ttk.LabelFrame(tab, text="Keys", padding=FRAME_PAD) + label_frame.grid(row=1, column=0, sticky="nsew") + label_frame.columnconfigure(0, weight=1) + for i in range(3): + label_frame.rowconfigure(i, weight=1) + + frame = ttk.Frame(label_frame, padding=FRAME_PAD) + frame.grid(row=0, column=0, sticky="nsew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=2) + frame.columnconfigure(2, weight=1) + + label = ttk.Label(frame, text="Key directory: ") + label.grid(row=0, column=0, sticky="ew") + entry = ttk.Entry(frame) + entry.grid(row=0, column=1, stick="ew") + button = ttk.Button(frame, text="...") + button.grid(row=0, column=2, sticky="ew") + + frame = ttk.Frame(label_frame, padding=FRAME_PAD) + frame.grid(row=1, column=0, sticky="nsew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=3) + label = ttk.Label(frame, text="Key base name: ") + label.grid(row=0, column=0, sticky="ew") + entry = ttk.Entry(frame) + entry.grid(row=0, column=1, sticky="ew") + + text = ( + "The (name).pem x509 certificate and (name).key RSA private key need to exist in the\n" + "specified directory. These can be generated using the openssl tool. Also, a ca-cert.pem\n" + "file should exist in the key directory for the CA that issue the certs." + ) + label = ttk.Label(label_frame, text=text, padding=FRAME_PAD) + label.grid(row=2, column=0, sticky="ew") + + label_frame = ttk.LabelFrame( + tab, text="IPsec Tunnel Endpoints", padding=FRAME_PAD + ) + label_frame.grid(row=2, column=0, sticky="nsew") + + i = 0 + text = ( + "(1) Define tunnel endpoints (select peer node using the button, then select" + "address from the list)" + ) + label = ttk.Label(label_frame, text=text, padding=FRAME_PAD) + label.grid(row=i, column=0, sticky="nsew") + i = i + 1 + + frame = ttk.Frame(label_frame, padding=FRAME_PAD) + frame.grid(row=i, column=0, sticky="nsew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=3) + i = i + 1 + label = ttk.Label(frame, text="Local: ") + label.grid(row=0, column=0, sticky="ew") + combobox = ttk.Combobox(frame) + combobox.grid(row=0, column=1, sticky="ew") + + frame = ttk.Frame(label_frame, padding=FRAME_PAD) + frame.grid(row=i, column=0, sticky="nsew") + i = i + 1 + label = ttk.Label(frame, text="Peer node: (none)") + label.grid(row=0, column=0) + + frame = ttk.Frame(label_frame, padding=FRAME_PAD) + frame.grid(row=i, column=0, sticky="nsew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=3) + i = i + 1 + label = ttk.Label(frame, text="Peer: ") + label.grid(row=0, column=0, sticky="ew") + combobox = ttk.Combobox(frame) + combobox.grid(row=0, column=1, sticky="ew") + + text = "(2) Select endpoints below and add the subnets to be encrypted" + label = ttk.Label(label_frame, text=text, padding=FRAME_PAD) + label.grid(row=i, column=0, sticky="ew") + i = i + i + + frame = ttk.Frame(label_frame, padding=FRAME_PAD) + frame.grid(row=i, column=0, sticky="nsew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=3) + i = i + 1 + label = ttk.Label(frame, text="Local subnet: ") + label.grid(row=0, column=0, sticky="ew") + combobox = ttk.Combobox(frame) + combobox.grid(row=0, column=1, sticky="ew") + + frame = ttk.Frame(label_frame, padding=FRAME_PAD) + frame.grid(row=i, column=0, sticky="nsew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=3) + i = i + 1 + label = ttk.Label(frame, text="Remote subnet: ") + label.grid(row=0, column=0, sticky="ew") + combobox = ttk.Combobox(frame) + combobox.grid(row=0, column=1, sticky="ew") + + frame = ttk.Frame(tab, padding=FRAME_PAD) + frame.grid(row=3, column=0, sticky="nsew") + for i in range(2): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Trash") + button.grid(row=0, column=0, sticky="ew") + button = ttk.Button(frame, text="Generate ipsec.sh") + button.grid(row=0, column=1, sticky="ew") + def draw_tab_files(self): tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab.grid(sticky="nsew") @@ -342,7 +472,7 @@ class ServiceConfigDialog(Dialog): def draw_buttons(self): frame = ttk.Frame(self.top) frame.grid(sticky="ew") - for i in range(4): + for i in range(5): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) button.grid(row=0, column=0, sticky="ew", padx=PADX) @@ -352,6 +482,14 @@ class ServiceConfigDialog(Dialog): button.grid(row=0, column=2, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=3, sticky="ew") + button = ttk.Button( + frame, + text="Info Link", + command=lambda: self.navigate_link( + "http://coreemu.github.io/core/services.html" + ), + ) + button.grid(row=0, column=4, sticky="ew") def add_filename(self, event: tk.Event): # not worry about it for now @@ -504,3 +642,6 @@ class ServiceConfigDialog(Dialog): for cmd in to_add: commands.append(cmd) listbox.insert(tk.END, cmd) + + def is_ipsec(self): + return self.service_name == "IPsec" From d2fe35279793a858ab88fe5324fed41edae20926 Mon Sep 17 00:00:00 2001 From: Gabriel Somlo Date: Sun, 9 Feb 2020 07:42:02 -0500 Subject: [PATCH 0006/1131] services/frr.py: frrboot.sh: start 'staticd' to support static routes Unlike Quagga, FRR requires 'staticd' to be running in order to support provisioning and use of static routes in the running configuration (e.g., 'ip route a.b.c.d/p nexthop'). Signed-off-by: Gabriel Somlo --- daemon/core/services/frr.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py index 95ffb0b5..51ab1e38 100644 --- a/daemon/core/services/frr.py +++ b/daemon/core/services/frr.py @@ -208,6 +208,9 @@ bootfrr() fi bootdaemon "zebra" + if grep -q "^ip route " $FRR_CONF; then + bootdaemon "staticd" + fi for r in rip ripng ospf6 ospf bgp babel; do if grep -q "^router \\<${r}\\>" $FRR_CONF; then bootdaemon "${r}d" From 8734b9f22f578c9887d5dec2e581f8632d63a116 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 10 Feb 2020 15:20:07 -0800 Subject: [PATCH 0007/1131] attempt adding scaling function to the gui --- daemon/core/gui/dialogs/preferences.py | 39 ++++++++ daemon/core/gui/graph/graph.py | 3 + daemon/core/gui/statusbar.py | 2 +- daemon/core/gui/toolbar.py | 133 +++++++++++++++++++++---- 4 files changed, 159 insertions(+), 18 deletions(-) diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index f60da652..9c3da76a 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -10,10 +10,14 @@ from core.gui.themes import FRAME_PAD, PADX, PADY if TYPE_CHECKING: from core.gui.app import Application +WIDTH = 1000 +HEIGHT = 800 + class PreferencesDialog(Dialog): def __init__(self, master: "Application", app: "Application"): super().__init__(master, app, "Preferences", modal=True) + self.gui_scale = tk.DoubleVar(value=self.app.canvas.app_scale) preferences = self.app.guiconfig["preferences"] self.editor = tk.StringVar(value=preferences["editor"]) self.theme = tk.StringVar(value=preferences["theme"]) @@ -64,6 +68,27 @@ class PreferencesDialog(Dialog): entry = ttk.Entry(frame, textvariable=self.gui3d) entry.grid(row=3, column=1, sticky="ew") + label = ttk.Label(frame, text="Scaling") + label.grid(row=4, column=0, pady=PADY, padx=PADX, sticky="w") + + scale_frame = ttk.Frame(frame) + scale_frame.grid(row=4, column=1, sticky="ew") + scale_frame.columnconfigure(0, weight=1) + scale = ttk.Scale( + scale_frame, + from_=0.5, + to=5, + value=1, + orient=tk.HORIZONTAL, + variable=self.gui_scale, + command=self.scale_adjust, + ) + scale.grid(row=0, column=0, sticky="ew") + entry = ttk.Entry( + scale_frame, textvariable=self.gui_scale, width=4, state="disabled" + ) + entry.grid(row=0, column=1) + def draw_buttons(self): frame = ttk.Frame(self.top) frame.grid(sticky="ew") @@ -89,3 +114,17 @@ class PreferencesDialog(Dialog): preferences["theme"] = self.theme.get() self.app.save_config() self.destroy() + + def scale_adjust(self, scale: str): + self.gui_scale.set(round(self.gui_scale.get(), 2)) + app_scale = self.gui_scale.get() + self.app.canvas.app_scale = app_scale + screen_width = self.app.master.winfo_screenwidth() + screen_height = self.app.master.winfo_screenheight() + scaled_width = WIDTH * app_scale + scaled_height = HEIGHT * app_scale + x = int(screen_width / 2 - scaled_width / 2) + y = int(screen_height / 2 - scaled_height / 2) + self.app.master.geometry(f"{int(scaled_width)}x{int(scaled_height)}+{x}+{y}") + + self.app.toolbar.scale(app_scale) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index a56f1423..da394503 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -53,6 +53,9 @@ class CanvasGraph(tk.Canvas): self.marker_tool = None self.to_copy = [] + # app's scale, different scale values to support higher resolution display + self.app_scale = 1.0 + # background related self.wallpaper_id = None self.wallpaper = None diff --git a/daemon/core/gui/statusbar.py b/daemon/core/gui/statusbar.py index 6de511c4..7524e318 100644 --- a/daemon/core/gui/statusbar.py +++ b/daemon/core/gui/statusbar.py @@ -29,7 +29,7 @@ class StatusBar(ttk.Frame): def draw(self): self.columnconfigure(0, weight=1) - self.columnconfigure(1, weight=7) + self.columnconfigure(1, weight=5) self.columnconfigure(2, weight=1) self.columnconfigure(3, weight=1) self.columnconfigure(4, weight=1) diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 063c2e5b..2b40cee7 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -1,6 +1,7 @@ import logging import time import tkinter as tk +from enum import Enum from functools import partial from tkinter import messagebox, ttk from tkinter.font import Font @@ -25,6 +26,12 @@ TOOLBAR_SIZE = 32 PICKER_SIZE = 24 +class NodeTypeEnum(Enum): + NODE = 0 + NETWORK = 1 + OTHER = 2 + + def icon(image_enum, width=TOOLBAR_SIZE): return Images.get(image_enum, width) @@ -47,6 +54,7 @@ class Toolbar(ttk.Frame): self.picker_font = Font(size=8) # design buttons + self.play_button = None self.select_button = None self.link_button = None self.node_button = None @@ -71,9 +79,21 @@ class Toolbar(ttk.Frame): # dialog self.marker_tool = None + # these variables help keep track of what images being drawn so that scaling is possible + # since ImageTk.PhotoImage does not have resize method + self.node_enum = None + self.network_enum = None + self.annotation_enum = None + # draw components self.draw() + def get_icon(self, image_enum, width=TOOLBAR_SIZE): + if not self.app.canvas: + return Images.get(image_enum, width) + else: + return Images.get(image_enum, int(width * self.app.canvas.app_scale)) + def draw(self): self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) @@ -85,20 +105,26 @@ class Toolbar(ttk.Frame): self.design_frame = ttk.Frame(self) self.design_frame.grid(row=0, column=0, sticky="nsew") self.design_frame.columnconfigure(0, weight=1) - self.create_button( + self.play_button = self.create_button( self.design_frame, - icon(ImageEnum.START), + # icon(ImageEnum.START), + self.get_icon(ImageEnum.START), self.click_start, "start the session", ) self.select_button = self.create_button( self.design_frame, - icon(ImageEnum.SELECT), + # icon(ImageEnum.SELECT), + self.get_icon(ImageEnum.SELECT), self.click_selection, "selection tool", ) self.link_button = self.create_button( - self.design_frame, icon(ImageEnum.LINK), self.click_link, "link tool" + self.design_frame, + # icon(ImageEnum.LINK), + self.get_icon(ImageEnum.LINK), + self.click_link, + "link tool", ) self.create_node_button() self.create_network_button() @@ -130,18 +156,24 @@ class Toolbar(ttk.Frame): self.stop_button = self.create_button( self.runtime_frame, - icon(ImageEnum.STOP), + # icon(ImageEnum.STOP), + self.get_icon(ImageEnum.STOP), self.click_stop, "stop the session", ) self.runtime_select_button = self.create_button( self.runtime_frame, - icon(ImageEnum.SELECT), + # icon(ImageEnum.SELECT), + self.get_icon(ImageEnum.SELECT), self.click_runtime_selection, "selection tool", ) self.plot_button = self.create_button( - self.runtime_frame, icon(ImageEnum.PLOT), self.click_plot_button, "plot" + self.runtime_frame, + # icon(ImageEnum.PLOT), + self.get_icon(ImageEnum.PLOT), + self.click_plot_button, + "plot", ) self.runtime_marker_button = self.create_button( self.runtime_frame, @@ -165,22 +197,40 @@ class Toolbar(ttk.Frame): # draw default nodes for node_draw in NodeUtils.NODES: toolbar_image = icon(node_draw.image_enum) - image = icon(node_draw.image_enum, PICKER_SIZE) + # image = icon(node_draw.image_enum, PICKER_SIZE) + image = self.get_icon( + node_draw.image_enum, PICKER_SIZE * self.app.canvas.app_scale + ) func = partial( - self.update_button, self.node_button, toolbar_image, node_draw + self.update_button, + self.node_button, + toolbar_image, + node_draw, + NodeTypeEnum.NODE, + node_draw.image_enum, ) self.create_picker_button(image, func, self.node_picker, node_draw.label) # draw custom nodes for name in sorted(self.app.core.custom_nodes): node_draw = self.app.core.custom_nodes[name] toolbar_image = Images.get_custom(node_draw.image_file, TOOLBAR_SIZE) - image = Images.get_custom(node_draw.image_file, PICKER_SIZE) + image = Images.get_custom( + node_draw.image_file, int(PICKER_SIZE * self.app.canvas.app_scale) + ) func = partial( - self.update_button, self.node_button, toolbar_image, node_draw + self.update_button, + self.node_button, + toolbar_image, + node_draw, + NodeTypeEnum, + node_draw.image_file, ) self.create_picker_button(image, func, self.node_picker, name) # draw edit node - image = icon(ImageEnum.EDITNODE, PICKER_SIZE) + # image = icon(ImageEnum.EDITNODE, PICKER_SIZE) + image = self.get_icon( + ImageEnum.EDITNODE, PICKER_SIZE * self.app.canvas.app_scale + ) self.create_picker_button( image, self.click_edit_node, self.node_picker, "Custom" ) @@ -284,13 +334,24 @@ class Toolbar(ttk.Frame): dialog = CustomNodesDialog(self.app, self.app) dialog.show() - def update_button(self, button: ttk.Button, image: "ImageTk", node_draw: NodeDraw): + def update_button( + self, + button: ttk.Button, + image: "ImageTk", + node_draw: NodeDraw, + type_enum, + image_enum, + ): logging.debug("update button(%s): %s", button, node_draw) self.hide_pickers() button.configure(image=image) button.image = image self.app.canvas.mode = GraphMode.NODE self.app.canvas.node_draw = node_draw + if type_enum == NodeTypeEnum.NODE: + self.node_enum = image_enum + elif type_enum == NodeTypeEnum.NETWORK: + self.network_enum = image_enum def hide_pickers(self): logging.debug("hiding pickers") @@ -308,13 +369,14 @@ class Toolbar(ttk.Frame): """ Create network layer button """ - image = icon(ImageEnum.ROUTER) + image = icon(ImageEnum.ROUTER, TOOLBAR_SIZE) self.node_button = ttk.Button( self.design_frame, image=image, command=self.draw_node_picker ) self.node_button.image = image self.node_button.grid(sticky="ew") Tooltip(self.node_button, "Network-layer virtual nodes") + self.node_enum = ImageEnum.ROUTER def draw_network_picker(self): """ @@ -328,7 +390,12 @@ class Toolbar(ttk.Frame): self.create_picker_button( image, partial( - self.update_button, self.network_button, toolbar_image, node_draw + self.update_button, + self.network_button, + toolbar_image, + node_draw, + NodeTypeEnum.NETWORK, + node_draw.image_enum, ), self.network_picker, node_draw.label, @@ -350,6 +417,7 @@ class Toolbar(ttk.Frame): self.network_button.image = image self.network_button.grid(sticky="ew") Tooltip(self.network_button, "link-layer nodes") + self.network_enum = ImageEnum.HUB def draw_annotation_picker(self): """ @@ -368,7 +436,7 @@ class Toolbar(ttk.Frame): image = icon(image_enum, PICKER_SIZE) self.create_picker_button( image, - partial(self.update_annotation, toolbar_image, shape_type), + partial(self.update_annotation, toolbar_image, shape_type, image_enum), self.annotation_picker, shape_type.value, ) @@ -388,6 +456,7 @@ class Toolbar(ttk.Frame): self.annotation_button.image = image self.annotation_button.grid(sticky="ew") Tooltip(self.annotation_button, "background annotation tools") + self.annotation_enum = ImageEnum.MARKER def create_observe_button(self): menu_button = ttk.Menubutton( @@ -434,13 +503,16 @@ class Toolbar(ttk.Frame): if not response.result: messagebox.showerror("Stop Error", "Errors stopping session") - def update_annotation(self, image: "ImageTk.PhotoImage", shape_type: ShapeType): + def update_annotation( + self, image: "ImageTk.PhotoImage", shape_type: ShapeType, image_enum + ): logging.debug("clicked annotation: ") self.hide_pickers() self.annotation_button.configure(image=image) self.annotation_button.image = image self.app.canvas.mode = GraphMode.ANNOTATION self.app.canvas.annotation_type = shape_type + self.annotation_enum = image_enum if is_marker(shape_type): if self.marker_tool: self.marker_tool.destroy() @@ -465,3 +537,30 @@ class Toolbar(ttk.Frame): def click_two_node_button(self): logging.debug("Click TWONODE button") + + @classmethod + def scale_button(cls, button, image_enum, scale): + image = icon(image_enum, int(TOOLBAR_SIZE * scale)) + button.config(image=image) + button.image = image + + def scale(self, scale): + self.scale_button(self.play_button, ImageEnum.START, scale) + self.scale_button(self.select_button, ImageEnum.SELECT, scale) + self.scale_button(self.link_button, ImageEnum.LINK, scale) + self.scale_button(self.node_button, self.node_enum, scale) + self.scale_button(self.network_button, self.network_enum, scale) + self.scale_button(self.annotation_button, self.annotation_enum, scale) + + self.scale_button(self.runtime_select_button, ImageEnum.SELECT, scale) + self.scale_button(self.stop_button, ImageEnum.STOP, scale) + self.scale_button(self.plot_button, ImageEnum.PLOT, scale) + self.scale_button(self.runtime_marker_button, ImageEnum.MARKER, scale) + self.scale_button(self.node_command_button, ImageEnum.TWONODE, scale) + self.scale_button(self.run_command_button, ImageEnum.RUN, scale) + + # self.stop_button = None + # self.plot_button = None + # self.runtime_marker_button = None + # self.node_command_button = None + # self.run_command_button = None From 7fbbfa8c63954928b199d51ab2f5120780a1f0c9 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 12 Feb 2020 08:35:14 -0800 Subject: [PATCH 0008/1131] scale font --- daemon/core/gui/app.py | 4 +++- daemon/core/gui/dialogs/preferences.py | 21 +++++++++++---------- daemon/core/gui/themes.py | 9 ++++++++- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index e70b9f52..c3ab4b6b 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -1,5 +1,5 @@ import tkinter as tk -from tkinter import ttk +from tkinter import font, ttk from core.gui import appconfig, themes from core.gui.coreclient import CoreClient @@ -22,6 +22,8 @@ class Application(tk.Frame): # load node icons NodeUtils.setup() + self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()} + # widgets self.menubar = None self.toolbar = None diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index 9c3da76a..a10e7e89 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING from core.gui import appconfig from core.gui.dialogs.dialog import Dialog -from core.gui.themes import FRAME_PAD, PADX, PADY +from core.gui.themes import FRAME_PAD, PADX, PADY, scale_fonts if TYPE_CHECKING: from core.gui.app import Application @@ -119,12 +119,13 @@ class PreferencesDialog(Dialog): self.gui_scale.set(round(self.gui_scale.get(), 2)) app_scale = self.gui_scale.get() self.app.canvas.app_scale = app_scale - screen_width = self.app.master.winfo_screenwidth() - screen_height = self.app.master.winfo_screenheight() - scaled_width = WIDTH * app_scale - scaled_height = HEIGHT * app_scale - x = int(screen_width / 2 - scaled_width / 2) - y = int(screen_height / 2 - scaled_height / 2) - self.app.master.geometry(f"{int(scaled_width)}x{int(scaled_height)}+{x}+{y}") - - self.app.toolbar.scale(app_scale) + scale_fonts(self.app.fonts_size, app_scale) + # screen_width = self.app.master.winfo_screenwidth() + # screen_height = self.app.master.winfo_screenheight() + # scaled_width = WIDTH * app_scale + # scaled_height = HEIGHT * app_scale + # x = int(screen_width / 2 - scaled_width / 2) + # y = int(screen_height / 2 - scaled_height / 2) + # self.app.master.geometry(f"{int(scaled_width)}x{int(scaled_height)}+{x}+{y}") + # + # self.app.toolbar.scale(app_scale) diff --git a/daemon/core/gui/themes.py b/daemon/core/gui/themes.py index 26ee5379..482c86d2 100644 --- a/daemon/core/gui/themes.py +++ b/daemon/core/gui/themes.py @@ -1,5 +1,5 @@ import tkinter as tk -from tkinter import ttk +from tkinter import font, ttk THEME_DARK = "black" PADX = (0, 5) @@ -198,3 +198,10 @@ def theme_change(event: tk.Event): relief=tk.NONE, font=("TkDefaultFont", 8, "normal"), ) + + +def scale_fonts(fonts_size, scale): + for name in font.names(): + f = font.nametofont(name) + if name in fonts_size: + f.config(size=int(fonts_size[name] * scale)) From b4bf3ee3919084690304f82f165fa1114995f533 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 12 Feb 2020 08:39:10 -0800 Subject: [PATCH 0009/1131] remove unecessary print statement and remove commented code --- daemon/core/gui/coreclient.py | 1 - daemon/core/gui/toolbar.py | 11 ----------- 2 files changed, 12 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index bb5dd45a..f99147be 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -436,7 +436,6 @@ class CoreClient: master = self.app if parent_frame: master = parent_frame - print("stop session error") self.app.after(0, show_grpc_error, e, master, self.app) def set_up(self): diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index c6cf1ce1..4ba07665 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -262,13 +262,6 @@ class Toolbar(ttk.Frame): self.set_runtime() self.app.core.set_metadata() self.app.core.show_mobility_players() - # else: - # message = "\n".join(response.exceptions) - # print("start error") - # print(response) - # dialog = ErrorDialog(self.app, self.app, "Start ERROR", message) - # dialog.show() - # messagebox.showerror("Start Error", message) def set_runtime(self): self.runtime_frame.tkraise() @@ -435,10 +428,6 @@ class Toolbar(ttk.Frame): message = f"Stopped in {total:.3f} seconds" self.app.statusbar.set_status(message) self.app.canvas.stopped_session() - # if not response.result: - # dialog = ErrorDialog(self.app, self.app, "Stop ERROR", "Error stopping session") - # dialog.show() - # # messagebox.showerror("Stop Error", "Errors stopping session") def update_annotation(self, image: "ImageTk.PhotoImage", shape_type: ShapeType): logging.debug("clicked annotation: ") From 3a466fd463e0d84587facd077d79a4e904389b99 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 12 Feb 2020 14:13:28 -0800 Subject: [PATCH 0010/1131] remove custom size for custom style so that text can scale, scale the remaining node icons from the node picker, scale node's name --- daemon/core/gui/app.py | 1 + daemon/core/gui/dialogs/preferences.py | 30 ++++++++----- daemon/core/gui/graph/node.py | 4 +- daemon/core/gui/themes.py | 13 +++--- daemon/core/gui/toolbar.py | 58 ++++++++++---------------- 5 files changed, 50 insertions(+), 56 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index c3ab4b6b..888c9c62 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -23,6 +23,7 @@ class Application(tk.Frame): NodeUtils.setup() self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()} + self.icon_text_font = font.Font(family="TkIconFont", size=12) # widgets self.menubar = None diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index a10e7e89..654f9e32 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -81,7 +81,6 @@ class PreferencesDialog(Dialog): value=1, orient=tk.HORIZONTAL, variable=self.gui_scale, - command=self.scale_adjust, ) scale.grid(row=0, column=0, sticky="ew") entry = ttk.Entry( @@ -113,19 +112,28 @@ class PreferencesDialog(Dialog): preferences["gui3d"] = self.gui3d.get() preferences["theme"] = self.theme.get() self.app.save_config() + self.scale_adjust() self.destroy() - def scale_adjust(self, scale: str): + def scale_adjust(self): self.gui_scale.set(round(self.gui_scale.get(), 2)) app_scale = self.gui_scale.get() self.app.canvas.app_scale = app_scale + + self.app.master.tk.call("tk", "scaling", app_scale) + + # scale fonts scale_fonts(self.app.fonts_size, app_scale) - # screen_width = self.app.master.winfo_screenwidth() - # screen_height = self.app.master.winfo_screenheight() - # scaled_width = WIDTH * app_scale - # scaled_height = HEIGHT * app_scale - # x = int(screen_width / 2 - scaled_width / 2) - # y = int(screen_height / 2 - scaled_height / 2) - # self.app.master.geometry(f"{int(scaled_width)}x{int(scaled_height)}+{x}+{y}") - # - # self.app.toolbar.scale(app_scale) + self.app.icon_text_font.config(size=int(12 * app_scale)) + + # scale application widow size + screen_width = self.app.master.winfo_screenwidth() + screen_height = self.app.master.winfo_screenheight() + scaled_width = WIDTH * app_scale + scaled_height = HEIGHT * app_scale + x = int(screen_width / 2 - scaled_width / 2) + y = int(screen_height / 2 - scaled_height / 2) + self.app.master.geometry(f"{int(scaled_width)}x{int(scaled_height)}+{x}+{y}") + + # scale toolbar icons and picker icons + self.app.toolbar.scale() diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index de3a9204..ff33d7e5 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -1,6 +1,5 @@ import logging import tkinter as tk -from tkinter import font from typing import TYPE_CHECKING import grpc @@ -42,14 +41,13 @@ class CanvasNode: self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE ) - text_font = font.Font(family="TkIconFont", size=12) label_y = self._get_label_y() self.text_id = self.canvas.create_text( x, label_y, text=self.core_node.name, tags=tags.NODE_NAME, - font=text_font, + font=self.app.icon_text_font, fill="#0000CD", ) self.tooltip = CanvasTooltip(self.canvas) diff --git a/daemon/core/gui/themes.py b/daemon/core/gui/themes.py index 482c86d2..7da0b1dd 100644 --- a/daemon/core/gui/themes.py +++ b/daemon/core/gui/themes.py @@ -176,27 +176,27 @@ def style_listbox(widget: tk.Widget): def theme_change(event: tk.Event): style = ttk.Style() - style.configure(Styles.picker_button, font=("TkDefaultFont", 8, "normal")) + style.configure(Styles.picker_button, font="TkSmallCaptionFont") style.configure( Styles.green_alert, background="green", padding=0, relief=tk.NONE, - font=("TkDefaultFont", 8, "normal"), + font="TkSmallCaptionFont", ) style.configure( Styles.yellow_alert, background="yellow", padding=0, relief=tk.NONE, - font=("TkDefaultFont", 8, "normal"), + font="TkSmallCaptionFont", ) style.configure( Styles.red_alert, background="red", padding=0, relief=tk.NONE, - font=("TkDefaultFont", 8, "normal"), + font="TkSmallCaptionFont", ) @@ -204,4 +204,7 @@ def scale_fonts(fonts_size, scale): for name in font.names(): f = font.nametofont(name) if name in fonts_size: - f.config(size=int(fonts_size[name] * scale)) + if name == "TkSmallCaptionFont": + f.config(size=int(fonts_size[name] * scale * 8 / 9)) + else: + f.config(size=int(fonts_size[name] * scale)) diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 2b40cee7..8298cafb 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -4,7 +4,6 @@ import tkinter as tk from enum import Enum from functools import partial from tkinter import messagebox, ttk -from tkinter.font import Font from typing import TYPE_CHECKING, Callable from core.api.grpc import core_pb2 @@ -50,9 +49,6 @@ class Toolbar(ttk.Frame): self.master = app.master self.time = None - # picker data - self.picker_font = Font(size=8) - # design buttons self.play_button = None self.select_button = None @@ -198,9 +194,7 @@ class Toolbar(ttk.Frame): for node_draw in NodeUtils.NODES: toolbar_image = icon(node_draw.image_enum) # image = icon(node_draw.image_enum, PICKER_SIZE) - image = self.get_icon( - node_draw.image_enum, PICKER_SIZE * self.app.canvas.app_scale - ) + image = self.get_icon(node_draw.image_enum, PICKER_SIZE) func = partial( self.update_button, self.node_button, @@ -214,9 +208,7 @@ class Toolbar(ttk.Frame): for name in sorted(self.app.core.custom_nodes): node_draw = self.app.core.custom_nodes[name] toolbar_image = Images.get_custom(node_draw.image_file, TOOLBAR_SIZE) - image = Images.get_custom( - node_draw.image_file, int(PICKER_SIZE * self.app.canvas.app_scale) - ) + image = Images.get_custom(node_draw.image_file, PICKER_SIZE) func = partial( self.update_button, self.node_button, @@ -228,9 +220,7 @@ class Toolbar(ttk.Frame): self.create_picker_button(image, func, self.node_picker, name) # draw edit node # image = icon(ImageEnum.EDITNODE, PICKER_SIZE) - image = self.get_icon( - ImageEnum.EDITNODE, PICKER_SIZE * self.app.canvas.app_scale - ) + image = self.get_icon(ImageEnum.EDITNODE, PICKER_SIZE) self.create_picker_button( image, self.click_edit_node, self.node_picker, "Custom" ) @@ -386,7 +376,7 @@ class Toolbar(ttk.Frame): self.network_picker = ttk.Frame(self.master) for node_draw in NodeUtils.NETWORK_NODES: toolbar_image = icon(node_draw.image_enum) - image = icon(node_draw.image_enum, PICKER_SIZE) + image = self.get_icon(node_draw.image_enum, PICKER_SIZE) self.create_picker_button( image, partial( @@ -433,7 +423,7 @@ class Toolbar(ttk.Frame): ] for image_enum, shape_type in nodes: toolbar_image = icon(image_enum) - image = icon(image_enum, PICKER_SIZE) + image = self.get_icon(image_enum, PICKER_SIZE) self.create_picker_button( image, partial(self.update_annotation, toolbar_image, shape_type, image_enum), @@ -538,29 +528,23 @@ class Toolbar(ttk.Frame): def click_two_node_button(self): logging.debug("Click TWONODE button") - @classmethod - def scale_button(cls, button, image_enum, scale): - image = icon(image_enum, int(TOOLBAR_SIZE * scale)) + # def scale_button(cls, button, image_enum, scale): + def scale_button(self, button, image_enum): + image = icon(image_enum, int(TOOLBAR_SIZE * self.app.canvas.app_scale)) button.config(image=image) button.image = image - def scale(self, scale): - self.scale_button(self.play_button, ImageEnum.START, scale) - self.scale_button(self.select_button, ImageEnum.SELECT, scale) - self.scale_button(self.link_button, ImageEnum.LINK, scale) - self.scale_button(self.node_button, self.node_enum, scale) - self.scale_button(self.network_button, self.network_enum, scale) - self.scale_button(self.annotation_button, self.annotation_enum, scale) + def scale(self): + self.scale_button(self.play_button, ImageEnum.START) + self.scale_button(self.select_button, ImageEnum.SELECT) + self.scale_button(self.link_button, ImageEnum.LINK) + self.scale_button(self.node_button, self.node_enum) + self.scale_button(self.network_button, self.network_enum) + self.scale_button(self.annotation_button, self.annotation_enum) - self.scale_button(self.runtime_select_button, ImageEnum.SELECT, scale) - self.scale_button(self.stop_button, ImageEnum.STOP, scale) - self.scale_button(self.plot_button, ImageEnum.PLOT, scale) - self.scale_button(self.runtime_marker_button, ImageEnum.MARKER, scale) - self.scale_button(self.node_command_button, ImageEnum.TWONODE, scale) - self.scale_button(self.run_command_button, ImageEnum.RUN, scale) - - # self.stop_button = None - # self.plot_button = None - # self.runtime_marker_button = None - # self.node_command_button = None - # self.run_command_button = None + self.scale_button(self.runtime_select_button, ImageEnum.SELECT) + self.scale_button(self.stop_button, ImageEnum.STOP) + self.scale_button(self.plot_button, ImageEnum.PLOT) + self.scale_button(self.runtime_marker_button, ImageEnum.MARKER) + self.scale_button(self.node_command_button, ImageEnum.TWONODE) + self.scale_button(self.run_command_button, ImageEnum.RUN) From 83e6bbee45797dad53bec031c8cd4f31f80aae39 Mon Sep 17 00:00:00 2001 From: "Daniel R. Kerr" Date: Thu, 13 Feb 2020 00:38:45 -0500 Subject: [PATCH 0011/1131] Update utility.py fix on radvd config file generation to support python3 --- daemon/core/services/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index fa2b2672..556fddf8 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -669,7 +669,7 @@ class RadvdService(UtilService): for ifc in node.netifs(): if hasattr(ifc, "control") and ifc.control is True: continue - prefixes = map(cls.subnetentry, ifc.addrlist) + prefixes = list(map(cls.subnetentry, ifc.addrlist)) if len(prefixes) < 1: continue cfg += ( From 55b6cbbd90331745c1144f04d6ac7da4bf34174c Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 13 Feb 2020 12:15:56 -0800 Subject: [PATCH 0012/1131] sacle toolbar button after choosing a node from node picker, scale canvas nodes and canvas node text --- daemon/core/gui/dialogs/preferences.py | 2 ++ daemon/core/gui/graph/graph.py | 13 ++++++++++++- daemon/core/gui/graph/node.py | 6 ++++++ daemon/core/gui/images.py | 21 +++++++++++++++++++++ daemon/core/gui/toolbar.py | 13 +++---------- 5 files changed, 44 insertions(+), 11 deletions(-) diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index 654f9e32..415fbe9a 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -137,3 +137,5 @@ class PreferencesDialog(Dialog): # scale toolbar icons and picker icons self.app.toolbar.scale() + + self.app.canvas.scale_graph() diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index da394503..a4ddd0b7 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -12,7 +12,7 @@ from core.gui.graph.enums import GraphMode, ScaleOption from core.gui.graph.node import CanvasNode from core.gui.graph.shape import Shape from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker -from core.gui.images import ImageEnum, Images +from core.gui.images import ImageEnum, Images, TypeToImage from core.gui.nodeutils import EdgeUtils, NodeUtils if TYPE_CHECKING: @@ -914,3 +914,14 @@ class CanvasGraph(tk.Canvas): width=self.itemcget(edge.id, "width"), fill=self.itemcget(edge.id, "fill"), ) + + def scale_graph(self): + for nid, canvas_node in self.nodes.items(): + image_enum = TypeToImage.get( + canvas_node.core_node.type, canvas_node.core_node.model + ) + img = Images.get(image_enum, int(ICON_SIZE * self.app_scale)) + self.itemconfig(nid, image=img) + canvas_node.image = img + + canvas_node.scale_text() diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index dbd8827b..b8faef13 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -106,6 +106,12 @@ class CanvasNode: image_box = self.canvas.bbox(self.id) return image_box[3] + NODE_TEXT_OFFSET + def scale_text(self): + text_bound = self.canvas.bbox(self.text_id) + prev_y = (text_bound[3] + text_bound[1]) / 2 + new_y = self._get_label_y() + self.canvas.move(self.text_id, 0, new_y - prev_y) + def move(self, x: int, y: int): x, y = self.canvas.get_scaled_coords(x, y) current_x, current_y = self.canvas.coords(self.id) diff --git a/daemon/core/gui/images.py b/daemon/core/gui/images.py index cd472764..f299c5a5 100644 --- a/daemon/core/gui/images.py +++ b/daemon/core/gui/images.py @@ -3,6 +3,7 @@ from tkinter import messagebox from PIL import Image, ImageTk +from core.api.grpc import core_pb2 from core.gui.appconfig import LOCAL_ICONS_PATH @@ -90,3 +91,23 @@ class ImageEnum(Enum): SHUTDOWN = "shutdown" CANCEL = "cancel" ERROR = "error" + + +class TypeToImage: + type_to_image = { + (core_pb2.NodeType.DEFAULT, "router"): ImageEnum.ROUTER, + (core_pb2.NodeType.DEFAULT, "PC"): ImageEnum.PC, + (core_pb2.NodeType.DEFAULT, "host"): ImageEnum.HOST, + (core_pb2.NodeType.DEFAULT, "mdr"): ImageEnum.MDR, + (core_pb2.NodeType.DEFAULT, "prouter"): ImageEnum.PROUTER, + (core_pb2.NodeType.HUB, ""): ImageEnum.HUB, + (core_pb2.NodeType.SWITCH, ""): ImageEnum.SWITCH, + (core_pb2.NodeType.WIRELESS_LAN, ""): ImageEnum.WLAN, + (core_pb2.NodeType.EMANE, ""): ImageEnum.EMANE, + (core_pb2.NodeType.RJ45, ""): ImageEnum.RJ45, + (core_pb2.NodeType.TUNNEL, ""): ImageEnum.TUNNEL, + } + + @classmethod + def get(cls, node_type, model): + return cls.type_to_image.get((node_type, model), None) diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 1df24993..10075b74 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -103,21 +103,18 @@ class Toolbar(ttk.Frame): self.design_frame.columnconfigure(0, weight=1) self.play_button = self.create_button( self.design_frame, - # icon(ImageEnum.START), self.get_icon(ImageEnum.START), self.click_start, "start the session", ) self.select_button = self.create_button( self.design_frame, - # icon(ImageEnum.SELECT), self.get_icon(ImageEnum.SELECT), self.click_selection, "selection tool", ) self.link_button = self.create_button( self.design_frame, - # icon(ImageEnum.LINK), self.get_icon(ImageEnum.LINK), self.click_link, "link tool", @@ -152,21 +149,18 @@ class Toolbar(ttk.Frame): self.stop_button = self.create_button( self.runtime_frame, - # icon(ImageEnum.STOP), self.get_icon(ImageEnum.STOP), self.click_stop, "stop the session", ) self.runtime_select_button = self.create_button( self.runtime_frame, - # icon(ImageEnum.SELECT), self.get_icon(ImageEnum.SELECT), self.click_runtime_selection, "selection tool", ) self.plot_button = self.create_button( self.runtime_frame, - # icon(ImageEnum.PLOT), self.get_icon(ImageEnum.PLOT), self.click_plot_button, "plot", @@ -192,8 +186,7 @@ class Toolbar(ttk.Frame): self.node_picker = ttk.Frame(self.master) # draw default nodes for node_draw in NodeUtils.NODES: - toolbar_image = icon(node_draw.image_enum) - # image = icon(node_draw.image_enum, PICKER_SIZE) + toolbar_image = self.get_icon(node_draw.image_enum, TOOLBAR_SIZE) image = self.get_icon(node_draw.image_enum, PICKER_SIZE) func = partial( self.update_button, @@ -372,7 +365,7 @@ class Toolbar(ttk.Frame): self.hide_pickers() self.network_picker = ttk.Frame(self.master) for node_draw in NodeUtils.NETWORK_NODES: - toolbar_image = icon(node_draw.image_enum) + toolbar_image = self.get_icon(node_draw.image_enum, TOOLBAR_SIZE) image = self.get_icon(node_draw.image_enum, PICKER_SIZE) self.create_picker_button( image, @@ -419,7 +412,7 @@ class Toolbar(ttk.Frame): (ImageEnum.TEXT, ShapeType.TEXT), ] for image_enum, shape_type in nodes: - toolbar_image = icon(image_enum) + toolbar_image = self.get_icon(image_enum, TOOLBAR_SIZE) image = self.get_icon(image_enum, PICKER_SIZE) self.create_picker_button( image, From 71aeb98bb9966e888074d95f0cf4ac6040ce38f6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 14 Feb 2020 13:18:05 -0800 Subject: [PATCH 0013/1131] updates to grpc add_link to return created interface data --- daemon/core/api/grpc/grpcutils.py | 40 +++++++++++++++++++++++++++ daemon/core/api/grpc/server.py | 26 ++++++++--------- daemon/core/emulator/session.py | 14 ++++++++-- daemon/core/nodes/base.py | 3 ++ daemon/proto/core/api/grpc/core.proto | 2 ++ daemon/tests/conftest.py | 10 +++++-- daemon/tests/test_distributed.py | 1 + 7 files changed, 77 insertions(+), 19 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 94a9b98c..c0ba3ceb 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -2,6 +2,8 @@ import logging import time from typing import Any, Dict, List, Tuple, Type +import netaddr + from core import utils from core.api.grpc import common_pb2, core_pb2 from core.config import ConfigurableOptions @@ -10,6 +12,7 @@ from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.enumerations import LinkTypes, NodeTypes from core.emulator.session import Session from core.nodes.base import CoreNetworkBase, NodeBase +from core.nodes.interface import CoreInterface from core.services.coreservices import CoreService WORKERS = 10 @@ -397,3 +400,40 @@ def get_service_configuration(service: Type[CoreService]) -> core_pb2.NodeServic shutdown=service.shutdown, meta=service.meta, ) + + +def interface_to_proto(interface: CoreInterface) -> core_pb2.Interface: + """ + Convenience for converting a core interface to the protobuf representation. + :param interface: interface to convert + :return: interface proto + """ + net_id = None + if interface.net: + net_id = interface.net.id + ip4 = None + ip4mask = None + ip6 = None + ip6mask = None + for addr in interface.addrlist: + network = netaddr.IPNetwork(addr) + mask = network.prefixlen + ip = str(network.ip) + if netaddr.valid_ipv4(ip) and not ip4: + ip4 = ip + ip4mask = mask + elif netaddr.valid_ipv6(ip) and not ip6: + ip6 = ip + ip6mask = mask + return core_pb2.Interface( + id=interface.netindex, + netid=net_id, + name=interface.name, + mac=str(interface.hwaddr), + mtu=interface.mtu, + flowid=interface.flow_id, + ip4=ip4, + ip4mask=ip4mask, + ip6=ip6, + ip6mask=ip6mask, + ) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 3a207ce4..3cc2020b 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -637,17 +637,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): interfaces = [] for interface_id in node._netif: interface = node._netif[interface_id] - net_id = None - if interface.net: - net_id = interface.net.id - interface_proto = core_pb2.Interface( - id=interface_id, - netid=net_id, - name=interface.name, - mac=str(interface.hwaddr), - mtu=interface.mtu, - flowid=interface.flow_id, - ) + interface_proto = grpcutils.interface_to_proto(interface) interfaces.append(interface_proto) emane_model = None @@ -795,10 +785,20 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): node_one_id = request.link.node_one_id node_two_id = request.link.node_two_id interface_one, interface_two, options = grpcutils.add_link_data(request.link) - session.add_link( + node_one_interface, node_two_interface = session.add_link( node_one_id, node_two_id, interface_one, interface_two, link_options=options ) - return core_pb2.AddLinkResponse(result=True) + interface_one_proto = None + interface_two_proto = None + if node_one_interface: + interface_one_proto = grpcutils.interface_to_proto(node_one_interface) + if node_two_interface: + interface_two_proto = grpcutils.interface_to_proto(node_two_interface) + return core_pb2.AddLinkResponse( + result=True, + interface_one=interface_one_proto, + interface_two=interface_two_proto, + ) def EditLink( self, request: core_pb2.EditLinkRequest, context: ServicerContext diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 329f5587..08793d2d 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -42,7 +42,7 @@ from core.location.event import EventLoop from core.location.mobility import BasicRangeModel, MobilityManager from core.nodes.base import CoreNetworkBase, CoreNode, CoreNodeBase, NodeBase from core.nodes.docker import DockerNode -from core.nodes.interface import GreTap +from core.nodes.interface import CoreInterface, GreTap from core.nodes.lxd import LxcNode from core.nodes.network import ( CtrlNet, @@ -301,7 +301,7 @@ class Session: interface_one: InterfaceData = None, interface_two: InterfaceData = None, link_options: LinkOptions = None, - ) -> None: + ) -> Tuple[CoreInterface, CoreInterface]: """ Add a link between nodes. @@ -313,7 +313,7 @@ class Session: data, defaults to none :param link_options: data for creating link, defaults to no options - :return: nothing + :return: tuple of created core interfaces, depending on link """ if not link_options: link_options = LinkOptions() @@ -328,6 +328,9 @@ class Session: if node_two: node_two.lock.acquire() + node_one_interface = None + node_two_interface = None + try: # wireless link if link_options.type == LinkTypes.WIRELESS: @@ -353,6 +356,7 @@ class Session: net_one.name, ) interface = create_interface(node_one, net_one, interface_one) + node_one_interface = interface link_config(net_one, interface, link_options) # network to node @@ -363,6 +367,7 @@ class Session: net_one.name, ) interface = create_interface(node_two, net_one, interface_two) + node_two_interface = interface if not link_options.unidirectional: link_config(net_one, interface, link_options) @@ -374,6 +379,7 @@ class Session: net_two.name, ) interface = net_one.linknet(net_two) + node_one_interface = interface link_config(net_one, interface, link_options) if not link_options.unidirectional: @@ -426,6 +432,8 @@ class Session: if node_two: node_two.lock.release() + return node_one_interface, node_two_interface + def delete_link( self, node_one_id: int, diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 4f9d1e37..9def7777 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -738,6 +738,9 @@ class CoreNode(CoreNodeBase): flow_id = self.node_net_client.get_ifindex(veth.name) veth.flow_id = int(flow_id) logging.debug("interface flow index: %s - %s", veth.name, veth.flow_id) + hwaddr = self.node_net_client.get_mac(veth.name) + logging.debug("interface mac: %s - %s", veth.name, hwaddr) + veth.sethwaddr(hwaddr) try: # add network interface to the node. If unsuccessful, destroy the diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index aa9bde24..975a0a87 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -454,6 +454,8 @@ message AddLinkRequest { message AddLinkResponse { bool result = 1; + Interface interface_one = 2; + Interface interface_two = 3; } message EditLinkRequest { diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index 2055820c..0c021b25 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -19,6 +19,7 @@ from core.emulator.emudata import IpPrefixes from core.emulator.enumerations import EventTypes from core.emulator.session import Session from core.nodes.base import CoreNode +from core.nodes.netclient import LinuxNetClient EMANE_SERVICES = "zebra|OSPFv3MDR|IPForward" @@ -27,8 +28,8 @@ class PatchManager: def __init__(self): self.patches = [] - def patch_obj(self, _cls, attribute): - p = mock.patch.object(_cls, attribute) + def patch_obj(self, _cls, attribute, return_value=None): + p = mock.patch.object(_cls, attribute, return_value=return_value) p.start() self.patches.append(p) @@ -51,11 +52,14 @@ class MockServer: @pytest.fixture(scope="session") def patcher(request): patch_manager = PatchManager() - patch_manager.patch_obj(DistributedServer, "remote_cmd") + patch_manager.patch_obj(DistributedServer, "remote_cmd", return_value="1") if request.config.getoption("mock"): patch_manager.patch("os.mkdir") patch_manager.patch("core.utils.cmd") patch_manager.patch("core.nodes.netclient.get_net_client") + patch_manager.patch_obj( + LinuxNetClient, "get_mac", return_value="00:00:00:00:00:00" + ) patch_manager.patch_obj(CoreNode, "nodefile") patch_manager.patch_obj(Session, "write_state") patch_manager.patch_obj(Session, "write_nodes") diff --git a/daemon/tests/test_distributed.py b/daemon/tests/test_distributed.py index d6b251e0..07f17ecb 100644 --- a/daemon/tests/test_distributed.py +++ b/daemon/tests/test_distributed.py @@ -13,6 +13,7 @@ class TestDistributed: options = NodeOptions() options.server = server_name node = session.add_node(options=options) + session.instantiate() # then assert node.server is not None From 0ea99ca809829c32ac891f4714e39731d9cc2bca Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 14 Feb 2020 13:34:00 -0800 Subject: [PATCH 0014/1131] scale edge text font (ipv4 and ipv6 address, scale edge, scale node when first drawn on canvas and when joining session --- daemon/core/gui/app.py | 1 + daemon/core/gui/dialogs/preferences.py | 1 + daemon/core/gui/graph/edges.py | 21 ++++++++++++++------- daemon/core/gui/graph/graph.py | 12 +++++++++--- daemon/core/gui/nodeutils.py | 25 ++++++++++++++----------- 5 files changed, 39 insertions(+), 21 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 888c9c62..3a19b9eb 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -24,6 +24,7 @@ class Application(tk.Frame): self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()} self.icon_text_font = font.Font(family="TkIconFont", size=12) + self.edge_font = font.Font(family="TkDefaultFont", size=8) # widgets self.menubar = None diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index 415fbe9a..45feef8c 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -125,6 +125,7 @@ class PreferencesDialog(Dialog): # scale fonts scale_fonts(self.app.fonts_size, app_scale) self.app.icon_text_font.config(size=int(12 * app_scale)) + self.app.edge_font.config(size=int(8 * app_scale)) # scale application widow size screen_width = self.app.master.winfo_screenwidth() diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 1259ffa9..413d94ac 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -1,6 +1,5 @@ import logging import tkinter as tk -from tkinter.font import Font from typing import TYPE_CHECKING, Any, Tuple from core.gui import themes @@ -31,7 +30,10 @@ class CanvasWirelessEdge: self.dst = dst self.canvas = canvas self.id = self.canvas.create_line( - *position, tags=tags.WIRELESS_EDGE, width=1.5, fill="#009933" + *position, + tags=tags.WIRELESS_EDGE, + width=1.5 * self.canvas.app_scale, + fill="#009933", ) def delete(self): @@ -61,13 +63,18 @@ class CanvasEdge: self.dst_interface = None self.canvas = canvas self.id = self.canvas.create_line( - x1, y1, x2, y2, tags=tags.EDGE, width=EDGE_WIDTH, fill=EDGE_COLOR + x1, + y1, + x2, + y2, + tags=tags.EDGE, + width=EDGE_WIDTH * self.canvas.app_scale, + fill=EDGE_COLOR, ) self.text_src = None self.text_dst = None self.text_middle = None self.token = None - self.font = Font(size=8) self.link = None self.asymmetric_link = None self.throughput = None @@ -117,7 +124,7 @@ class CanvasEdge: y1, text=label_one, justify=tk.CENTER, - font=self.font, + font=self.canvas.app.edge_font, tags=tags.LINK_INFO, ) self.text_dst = self.canvas.create_text( @@ -125,7 +132,7 @@ class CanvasEdge: y2, text=label_two, justify=tk.CENTER, - font=self.font, + font=self.canvas.app.edge_font, tags=tags.LINK_INFO, ) @@ -146,7 +153,7 @@ class CanvasEdge: if self.text_middle is None: x, y = self.get_midpoint() self.text_middle = self.canvas.create_text( - x, y, tags=tags.THROUGHPUT, font=self.font, text=value + x, y, tags=tags.THROUGHPUT, font=self.canvas.app.edge_font, text=value ) else: self.canvas.itemconfig(self.text_middle, text=value) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index a4ddd0b7..d65f8c1c 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -7,7 +7,7 @@ from PIL import Image, ImageTk from core.api.grpc import core_pb2 from core.gui.dialogs.shapemod import ShapeDialog from core.gui.graph import tags -from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge +from core.gui.graph.edges import EDGE_WIDTH, CanvasEdge, CanvasWirelessEdge from core.gui.graph.enums import GraphMode, ScaleOption from core.gui.graph.node import CanvasNode from core.gui.graph.shape import Shape @@ -223,10 +223,10 @@ class CanvasGraph(tk.Canvas): # peer to peer node is not drawn on the GUI if NodeUtils.is_ignore_node(core_node.type): continue - image = NodeUtils.node_image(core_node, self.app.guiconfig) + image = NodeUtils.node_image(core_node, self.app.guiconfig, self.app_scale) # if the gui can't find node's image, default to the "edit-node" image if not image: - image = Images.get(ImageEnum.EDITNODE, ICON_SIZE) + image = Images.get(ImageEnum.EDITNODE, int(ICON_SIZE * self.app_scale)) x = core_node.position.x y = core_node.position.y node = CanvasNode(self.master, x, y, core_node, image) @@ -666,6 +666,9 @@ class CanvasGraph(tk.Canvas): core_node = self.core.create_node( actual_x, actual_y, self.node_draw.node_type, self.node_draw.model ) + self.node_draw.image = Images.get( + self.node_draw.image_enum, int(ICON_SIZE * self.app_scale) + ) node = CanvasNode(self.master, x, y, core_node, self.node_draw.image) self.core.canvas_nodes[core_node.id] = node self.nodes[node.id] = node @@ -925,3 +928,6 @@ class CanvasGraph(tk.Canvas): canvas_node.image = img canvas_node.scale_text() + + for edge_id in self.find_withtag(tags.EDGE): + self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app_scale)) diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index c8ddb8fa..a9cee938 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -2,7 +2,7 @@ import logging from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union from core.api.grpc.core_pb2 import NodeType -from core.gui.images import ImageEnum, Images +from core.gui.images import ImageEnum, Images, TypeToImage if TYPE_CHECKING: from core.api.grpc import core_pb2 @@ -96,25 +96,28 @@ class NodeUtils: node_type: NodeType, model: str, gui_config: Dict[str, List[Dict[str, str]]], + scale=1.0, ) -> "ImageTk.PhotoImage": - if model == "": - model = None - try: - image = cls.NODE_ICONS[(node_type, model)] - return image - except KeyError: + + image_enum = TypeToImage.get(node_type, model) + if image_enum: + return Images.get(image_enum, int(ICON_SIZE * scale)) + else: image_stem = cls.get_image_file(gui_config, model) if image_stem: - return Images.get_with_image_file(image_stem, ICON_SIZE) + return Images.get_with_image_file(image_stem, int(ICON_SIZE * scale)) @classmethod def node_image( - cls, core_node: "core_pb2.Node", gui_config: Dict[str, List[Dict[str, str]]] + cls, + core_node: "core_pb2.Node", + gui_config: Dict[str, List[Dict[str, str]]], + scale=1.0, ) -> "ImageTk.PhotoImage": - image = cls.node_icon(core_node.type, core_node.model, gui_config) + image = cls.node_icon(core_node.type, core_node.model, gui_config, scale) if core_node.icon: try: - image = Images.create(core_node.icon, ICON_SIZE) + image = Images.create(core_node.icon, int(ICON_SIZE * scale)) except OSError: logging.error("invalid icon: %s", core_node.icon) return image From ebafa228ff625c79573ea2bbb991acfb0403795f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 14 Feb 2020 13:40:10 -0800 Subject: [PATCH 0015/1131] added files/directories to grpc set_node_service --- daemon/core/api/grpc/client.py | 12 +++++++++--- daemon/core/api/grpc/grpcutils.py | 13 ++++++++++--- daemon/core/gui/coreclient.py | 13 +++++++++---- daemon/core/gui/dialogs/serviceconfig.py | 6 +++--- daemon/proto/core/api/grpc/core.proto | 2 ++ daemon/tests/test_grpc.py | 2 +- 6 files changed, 34 insertions(+), 14 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 0f939921..9c6b15c6 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -834,9 +834,11 @@ class CoreGrpcClient: session_id: int, node_id: int, service: str, - startup: List[str], - validate: List[str], - shutdown: List[str], + files: List[str] = None, + directories: List[str] = None, + startup: List[str] = None, + validate: List[str] = None, + shutdown: List[str] = None, ) -> core_pb2.SetNodeServiceResponse: """ Set service data for a node. @@ -844,6 +846,8 @@ class CoreGrpcClient: :param session_id: session id :param node_id: node id :param service: service name + :param files: service files + :param directories: service directories :param startup: startup commands :param validate: validation commands :param shutdown: shutdown commands @@ -853,6 +857,8 @@ class CoreGrpcClient: config = core_pb2.ServiceConfig( node_id=node_id, service=service, + files=files, + directories=directories, startup=startup, validate=validate, shutdown=shutdown, diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index c0ba3ceb..b9cf33ef 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -376,9 +376,16 @@ def service_configuration(session: Session, config: core_pb2.ServiceConfig) -> N """ session.services.set_service(config.node_id, config.service) service = session.services.get_service(config.node_id, config.service) - service.startup = tuple(config.startup) - service.validate = tuple(config.validate) - service.shutdown = tuple(config.shutdown) + if config.files: + service.files = tuple(config.files) + if config.directories: + service.directories = tuple(config.directories) + if config.startup: + service.startup = tuple(config.startup) + if config.validate: + service.validate = tuple(config.validate) + if config.shutdown: + service.shutdown = tuple(config.shutdown) def get_service_configuration(service: Type[CoreService]) -> core_pb2.NodeServiceData: diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index f99147be..7fa31ee3 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -625,7 +625,12 @@ class CoreClient: shutdowns: List[str], ) -> core_pb2.NodeServiceData: response = self.client.set_node_service( - self.session_id, node_id, service_name, startups, validations, shutdowns + self.session_id, + node_id, + service_name, + startup=startups, + validate=validations, + shutdown=shutdowns, ) logging.info( "Set %s service for node(%s), Startup: %s, Validation: %s, Shutdown: %s, Result: %s", @@ -713,9 +718,9 @@ class CoreClient: self.session_id, config_proto.node_id, config_proto.service, - config_proto.startup, - config_proto.validate, - config_proto.shutdown, + startup=config_proto.startup, + validate=config_proto.validate, + shutdown=config_proto.shutdown, ) for config_proto in self.get_service_file_configs_proto(): self.client.set_node_service_file( diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index e8c13019..b528e32a 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -426,9 +426,9 @@ class ServiceConfigDialog(Dialog): config = self.core.set_node_service( self.node_id, self.service_name, - startup_commands, - validate_commands, - shutdown_commands, + startups=startup_commands, + validations=validate_commands, + shutdowns=shutdown_commands, ) if self.node_id not in self.service_configs: self.service_configs[self.node_id] = {} diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 975a0a87..fbf0367a 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -771,6 +771,8 @@ message ServiceConfig { repeated string startup = 3; repeated string validate = 4; repeated string shutdown = 5; + repeated string files = 6; + repeated string directories = 7; } message ServiceFileConfig { diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 2e2685ac..d26c46e4 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -935,7 +935,7 @@ class TestGrpc: # then with client.context_connect(): response = client.set_node_service( - session.id, node.id, service_name, [], validate, [] + session.id, node.id, service_name, validate=validate ) # then From 1375af51cb2c4d8e37ac44c0a80a42d6bad8bf36 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 14 Feb 2020 16:22:28 -0800 Subject: [PATCH 0016/1131] added grpc to get emane event channel being used --- daemon/core/api/grpc/client.py | 8 ++++++++ daemon/core/api/grpc/server.py | 15 +++++++++++++++ daemon/core/emane/emanemanager.py | 5 +++-- daemon/proto/core/api/grpc/core.proto | 12 ++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 9c6b15c6..73393004 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -26,6 +26,10 @@ from core.api.grpc.configservices_pb2 import ( SetNodeConfigServiceRequest, SetNodeConfigServiceResponse, ) +from core.api.grpc.core_pb2 import ( + GetEmaneEventChannelRequest, + GetEmaneEventChannelResponse, +) class InterfaceHelper: @@ -1139,6 +1143,10 @@ class CoreGrpcClient: ) return self.stub.SetNodeConfigService(request) + def get_emane_event_channel(self, session_id: int) -> GetEmaneEventChannelResponse: + request = GetEmaneEventChannelRequest(session_id=session_id) + return self.stub.GetEmaneEventChannel(request) + def connect(self) -> None: """ Open connection to server, must be closed manually. diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 3cc2020b..f155867d 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -32,6 +32,10 @@ from core.api.grpc.configservices_pb2 import ( SetNodeConfigServiceRequest, SetNodeConfigServiceResponse, ) +from core.api.grpc.core_pb2 import ( + GetEmaneEventChannelRequest, + GetEmaneEventChannelResponse, +) from core.api.grpc.events import EventStreamer from core.api.grpc.grpcutils import ( get_config_options, @@ -1630,3 +1634,14 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): grpc.StatusCode.NOT_FOUND, f"node {node.name} missing service {request.name}", ) + + def GetEmaneEventChannel( + self, request: GetEmaneEventChannelRequest, context: ServicerContext + ) -> GetEmaneEventChannelResponse: + session = self.get_session(request.session_id, context) + group = None + port = None + device = None + if session.emane.eventchannel: + group, port, device = session.emane.eventchannel + return GetEmaneEventChannelResponse(group=group, port=port, device=device) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 0c508284..9a7b3a0d 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -91,6 +91,7 @@ class EmaneManager(ModelManager): self.set_configs(self.emane_config.default_values()) self.service = None + self.eventchannel = None self.event_device = None self.emane_check() @@ -204,13 +205,13 @@ class EmaneManager(ModelManager): if eventnet is not None: # direct EMANE events towards control net bridge self.event_device = eventnet.brname - eventchannel = (group, int(port), self.event_device) + self.eventchannel = (group, int(port), self.event_device) # disabled otachannel for event service # only needed for e.g. antennaprofile events xmit by models logging.info("using %s for event service traffic", self.event_device) try: - self.service = EventService(eventchannel=eventchannel, otachannel=None) + self.service = EventService(eventchannel=self.eventchannel, otachannel=None) except EventServiceException: logging.exception("error instantiating emane EventService") diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index fbf0367a..e515ab2e 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -140,6 +140,8 @@ service CoreApi { } rpc GetEmaneModelConfigs (GetEmaneModelConfigsRequest) returns (GetEmaneModelConfigsResponse) { } + rpc GetEmaneEventChannel (GetEmaneEventChannelRequest) returns (GetEmaneEventChannelResponse) { + } // xml rpc rpc SaveXml (SaveXmlRequest) returns (SaveXmlResponse) { @@ -710,6 +712,16 @@ message GetEmaneModelConfigsResponse { repeated ModelConfig configs = 1; } +message GetEmaneEventChannelRequest { + int32 session_id = 1; +} + +message GetEmaneEventChannelResponse { + string group = 1; + int32 port = 2; + string device = 3; +} + message SaveXmlRequest { int32 session_id = 1; } From e8f6ccaa4ef16f61b77aa4b9eca77dd9e84ccd93 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 14 Feb 2020 16:25:05 -0800 Subject: [PATCH 0017/1131] fixed typing used for session.instantiate --- daemon/core/emulator/session.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 08793d2d..ebb74509 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -55,7 +55,7 @@ from core.nodes.network import ( ) from core.nodes.physical import PhysicalNode, Rj45Node from core.plugins.sdt import Sdt -from core.services.coreservices import CoreServices, ServiceBootError +from core.services.coreservices import CoreServices from core.xml import corexml, corexmldeployment from core.xml.corexml import CoreXmlReader, CoreXmlWriter @@ -1467,7 +1467,7 @@ class Session: ) self.broadcast_exception(exception_data) - def instantiate(self) -> List[ServiceBootError]: + def instantiate(self) -> List[Exception]: """ We have entered the instantiation state, invoke startup methods of various managers and boot the nodes. Validate nodes and check From 4fd1338cf15ccea09ba66d525020b125d8583313 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 17 Feb 2020 11:10:13 -0800 Subject: [PATCH 0018/1131] save application scale to gui configuration, and draw everything to the correct saved scale when starting the application --- daemon/core/gui/app.py | 29 +++++++++++++++++++++----- daemon/core/gui/appconfig.py | 1 + daemon/core/gui/dialogs/preferences.py | 10 +++++---- daemon/core/gui/graph/edges.py | 4 ++-- daemon/core/gui/graph/graph.py | 17 ++++++++------- daemon/core/gui/toolbar.py | 13 +++++------- 6 files changed, 47 insertions(+), 27 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 3a19b9eb..31651cfa 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -22,10 +22,6 @@ class Application(tk.Frame): # load node icons NodeUtils.setup() - self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()} - self.icon_text_font = font.Font(family="TkIconFont", size=12) - self.edge_font = font.Font(family="TkDefaultFont", size=8) - # widgets self.menubar = None self.toolbar = None @@ -33,8 +29,15 @@ class Application(tk.Frame): self.statusbar = None self.validation = None + # fonts + self.fonts_size = None + self.icon_text_font = None + self.edge_font = None + # setup self.guiconfig = appconfig.read() + self.app_scale = self.guiconfig["scale"] + self.setup_scaling() self.style = ttk.Style() self.setup_theme() self.core = CoreClient(self, proxy) @@ -42,6 +45,20 @@ class Application(tk.Frame): self.draw() self.core.set_up() + def setup_scaling(self): + self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()} + for name in font.names(): + f = font.nametofont(name) + if name in self.fonts_size: + if name == "TkSmallCaptionFont": + f.config(size=int(self.fonts_size[name] * self.app_scale * 8 / 9)) + else: + f.config(size=int(self.fonts_size[name] * self.app_scale)) + self.icon_text_font = font.Font( + family="TkIconFont", size=int(12 * self.app_scale) + ) + self.edge_font = font.Font(family="TkDefaultFont", size=int(8 * self.app_scale)) + def setup_theme(self): themes.load(self.style) self.master.bind_class("Menu", "<>", themes.theme_change_menu) @@ -62,7 +79,9 @@ class Application(tk.Frame): screen_height = self.master.winfo_screenheight() x = int((screen_width / 2) - (WIDTH / 2)) y = int((screen_height / 2) - (HEIGHT / 2)) - self.master.geometry(f"{WIDTH}x{HEIGHT}+{x}+{y}") + self.master.geometry( + f"{int(WIDTH * self.app_scale)}x{int(HEIGHT * self.app_scale)}+{x}+{y}" + ) def draw(self): self.master.option_add("*tearOff", tk.FALSE) diff --git a/daemon/core/gui/appconfig.py b/daemon/core/gui/appconfig.py index f73a842c..97c76065 100644 --- a/daemon/core/gui/appconfig.py +++ b/daemon/core/gui/appconfig.py @@ -96,6 +96,7 @@ def check_directory(): "nodes": [], "recentfiles": [], "observers": [{"name": "hello", "cmd": "echo hello"}], + "scale": 1.0, } save(config) diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index 45feef8c..440fab40 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -17,7 +17,7 @@ HEIGHT = 800 class PreferencesDialog(Dialog): def __init__(self, master: "Application", app: "Application"): super().__init__(master, app, "Preferences", modal=True) - self.gui_scale = tk.DoubleVar(value=self.app.canvas.app_scale) + self.gui_scale = tk.DoubleVar(value=self.app.app_scale) preferences = self.app.guiconfig["preferences"] self.editor = tk.StringVar(value=preferences["editor"]) self.theme = tk.StringVar(value=preferences["theme"]) @@ -111,15 +111,17 @@ class PreferencesDialog(Dialog): preferences["editor"] = self.editor.get() preferences["gui3d"] = self.gui3d.get() preferences["theme"] = self.theme.get() + self.gui_scale.set(round(self.gui_scale.get(), 2)) + app_scale = self.gui_scale.get() + self.app.guiconfig["scale"] = app_scale + self.app.save_config() self.scale_adjust() self.destroy() def scale_adjust(self): - self.gui_scale.set(round(self.gui_scale.get(), 2)) app_scale = self.gui_scale.get() - self.app.canvas.app_scale = app_scale - + self.app.app_scale = app_scale self.app.master.tk.call("tk", "scaling", app_scale) # scale fonts diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 413d94ac..9516de14 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -32,7 +32,7 @@ class CanvasWirelessEdge: self.id = self.canvas.create_line( *position, tags=tags.WIRELESS_EDGE, - width=1.5 * self.canvas.app_scale, + width=1.5 * self.canvas.app.app_scale, fill="#009933", ) @@ -68,7 +68,7 @@ class CanvasEdge: x2, y2, tags=tags.EDGE, - width=EDGE_WIDTH * self.canvas.app_scale, + width=EDGE_WIDTH * self.canvas.app.app_scale, fill=EDGE_COLOR, ) self.text_src = None diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index d65f8c1c..dd5025fd 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -53,9 +53,6 @@ class CanvasGraph(tk.Canvas): self.marker_tool = None self.to_copy = [] - # app's scale, different scale values to support higher resolution display - self.app_scale = 1.0 - # background related self.wallpaper_id = None self.wallpaper = None @@ -223,10 +220,14 @@ class CanvasGraph(tk.Canvas): # peer to peer node is not drawn on the GUI if NodeUtils.is_ignore_node(core_node.type): continue - image = NodeUtils.node_image(core_node, self.app.guiconfig, self.app_scale) + image = NodeUtils.node_image( + core_node, self.app.guiconfig, self.app.app_scale + ) # if the gui can't find node's image, default to the "edit-node" image if not image: - image = Images.get(ImageEnum.EDITNODE, int(ICON_SIZE * self.app_scale)) + image = Images.get( + ImageEnum.EDITNODE, int(ICON_SIZE * self.app.app_scale) + ) x = core_node.position.x y = core_node.position.y node = CanvasNode(self.master, x, y, core_node, image) @@ -667,7 +668,7 @@ class CanvasGraph(tk.Canvas): actual_x, actual_y, self.node_draw.node_type, self.node_draw.model ) self.node_draw.image = Images.get( - self.node_draw.image_enum, int(ICON_SIZE * self.app_scale) + self.node_draw.image_enum, int(ICON_SIZE * self.app.app_scale) ) node = CanvasNode(self.master, x, y, core_node, self.node_draw.image) self.core.canvas_nodes[core_node.id] = node @@ -923,11 +924,11 @@ class CanvasGraph(tk.Canvas): image_enum = TypeToImage.get( canvas_node.core_node.type, canvas_node.core_node.model ) - img = Images.get(image_enum, int(ICON_SIZE * self.app_scale)) + img = Images.get(image_enum, int(ICON_SIZE * self.app.app_scale)) self.itemconfig(nid, image=img) canvas_node.image = img canvas_node.scale_text() for edge_id in self.find_withtag(tags.EDGE): - self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app_scale)) + self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app.app_scale)) diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 10075b74..d0702386 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -85,10 +85,7 @@ class Toolbar(ttk.Frame): self.draw() def get_icon(self, image_enum, width=TOOLBAR_SIZE): - if not self.app.canvas: - return Images.get(image_enum, width) - else: - return Images.get(image_enum, int(width * self.app.canvas.app_scale)) + return Images.get(image_enum, int(width * self.app.app_scale)) def draw(self): self.columnconfigure(0, weight=1) @@ -349,7 +346,7 @@ class Toolbar(ttk.Frame): """ Create network layer button """ - image = icon(ImageEnum.ROUTER, TOOLBAR_SIZE) + image = self.get_icon(ImageEnum.ROUTER, TOOLBAR_SIZE) self.node_button = ttk.Button( self.design_frame, image=image, command=self.draw_node_picker ) @@ -390,7 +387,7 @@ class Toolbar(ttk.Frame): Create link-layer node button and the options that represent different link-layer node types. """ - image = icon(ImageEnum.HUB) + image = self.get_icon(ImageEnum.HUB, TOOLBAR_SIZE) self.network_button = ttk.Button( self.design_frame, image=image, command=self.draw_network_picker ) @@ -429,7 +426,7 @@ class Toolbar(ttk.Frame): """ Create marker button and options that represent different marker types """ - image = icon(ImageEnum.MARKER) + image = self.get_icon(ImageEnum.MARKER, TOOLBAR_SIZE) self.annotation_button = ttk.Button( self.design_frame, image=image, command=self.draw_annotation_picker ) @@ -518,7 +515,7 @@ class Toolbar(ttk.Frame): # def scale_button(cls, button, image_enum, scale): def scale_button(self, button, image_enum): - image = icon(image_enum, int(TOOLBAR_SIZE * self.app.canvas.app_scale)) + image = icon(image_enum, int(TOOLBAR_SIZE * self.app.app_scale)) button.config(image=image) button.image = image From 1d911a763f8352736acc92ae3a90a7abd4874474 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 17 Feb 2020 12:56:19 -0800 Subject: [PATCH 0019/1131] scale custom node icon and custom node drawn on canvas --- daemon/core/gui/graph/graph.py | 29 +++++++++++++++++++++-------- daemon/core/gui/toolbar.py | 8 ++++++-- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index dd5025fd..f705565f 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -667,9 +667,14 @@ class CanvasGraph(tk.Canvas): core_node = self.core.create_node( actual_x, actual_y, self.node_draw.node_type, self.node_draw.model ) - self.node_draw.image = Images.get( - self.node_draw.image_enum, int(ICON_SIZE * self.app.app_scale) - ) + try: + self.node_draw.image = Images.get( + self.node_draw.image_enum, int(ICON_SIZE * self.app.app_scale) + ) + except AttributeError: + self.node_draw.image = Images.get_custom( + self.node_draw.image_file, int(ICON_SIZE * self.app.app_scale) + ) node = CanvasNode(self.master, x, y, core_node, self.node_draw.image) self.core.canvas_nodes[core_node.id] = node self.nodes[node.id] = node @@ -921,13 +926,21 @@ class CanvasGraph(tk.Canvas): def scale_graph(self): for nid, canvas_node in self.nodes.items(): - image_enum = TypeToImage.get( - canvas_node.core_node.type, canvas_node.core_node.model - ) - img = Images.get(image_enum, int(ICON_SIZE * self.app.app_scale)) + img = None + if NodeUtils.is_custom(canvas_node.core_node.model): + for custom_node in self.app.guiconfig["nodes"]: + if custom_node["name"] == canvas_node.core_node.model: + img = Images.get_custom( + custom_node["image"], int(ICON_SIZE * self.app.app_scale) + ) + else: + image_enum = TypeToImage.get( + canvas_node.core_node.type, canvas_node.core_node.model + ) + img = Images.get(image_enum, int(ICON_SIZE * self.app.app_scale)) + self.itemconfig(nid, image=img) canvas_node.image = img - canvas_node.scale_text() for edge_id in self.find_withtag(tags.EDGE): diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index d0702386..eff37257 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -197,8 +197,12 @@ class Toolbar(ttk.Frame): # draw custom nodes for name in sorted(self.app.core.custom_nodes): node_draw = self.app.core.custom_nodes[name] - toolbar_image = Images.get_custom(node_draw.image_file, TOOLBAR_SIZE) - image = Images.get_custom(node_draw.image_file, PICKER_SIZE) + toolbar_image = Images.get_custom( + node_draw.image_file, int(TOOLBAR_SIZE * self.app.app_scale) + ) + image = Images.get_custom( + node_draw.image_file, int(PICKER_SIZE * self.app.app_scale) + ) func = partial( self.update_button, self.node_button, From 87c9492d320c8d0ae6095557eca0f0271bcfb4ea Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 17 Feb 2020 15:14:52 -0800 Subject: [PATCH 0020/1131] scale antenna and mobility player buttons --- daemon/core/gui/app.py | 12 ++----- daemon/core/gui/coreclient.py | 2 +- daemon/core/gui/dialogs/mobilityplayer.py | 6 ++-- daemon/core/gui/dialogs/nodeservice.py | 2 +- daemon/core/gui/dialogs/preferences.py | 13 ++----- daemon/core/gui/graph/edges.py | 6 ++-- daemon/core/gui/graph/graph.py | 5 ++- daemon/core/gui/graph/node.py | 42 +++++++++++++++++------ daemon/core/gui/nodeutils.py | 4 +-- 9 files changed, 52 insertions(+), 40 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 31651cfa..8b18beeb 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -47,13 +47,7 @@ class Application(tk.Frame): def setup_scaling(self): self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()} - for name in font.names(): - f = font.nametofont(name) - if name in self.fonts_size: - if name == "TkSmallCaptionFont": - f.config(size=int(self.fonts_size[name] * self.app_scale * 8 / 9)) - else: - f.config(size=int(self.fonts_size[name] * self.app_scale)) + themes.scale_fonts(self.fonts_size, self.app_scale) self.icon_text_font = font.Font( family="TkIconFont", size=int(12 * self.app_scale) ) @@ -77,8 +71,8 @@ class Application(tk.Frame): def center(self): screen_width = self.master.winfo_screenwidth() screen_height = self.master.winfo_screenheight() - x = int((screen_width / 2) - (WIDTH / 2)) - y = int((screen_height / 2) - (HEIGHT / 2)) + x = int((screen_width / 2) - (WIDTH * self.app_scale / 2)) + y = int((screen_height / 2) - (HEIGHT * self.app_scale / 2)) self.master.geometry( f"{int(WIDTH * self.app_scale)}x{int(HEIGHT * self.app_scale)}+{x}+{y}" ) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 7fa31ee3..7d8e832c 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -794,7 +794,7 @@ class CoreClient: image=image, emane=emane, ) - if NodeUtils.is_custom(model): + if NodeUtils.is_custom(node_type, model): services = NodeUtils.get_custom_node_services(self.app.guiconfig, model) node.services[:] = services logging.info( diff --git a/daemon/core/gui/dialogs/mobilityplayer.py b/daemon/core/gui/dialogs/mobilityplayer.py index dd2e9f9a..bb4d203c 100644 --- a/daemon/core/gui/dialogs/mobilityplayer.py +++ b/daemon/core/gui/dialogs/mobilityplayer.py @@ -100,17 +100,17 @@ class MobilityPlayerDialog(Dialog): for i in range(3): frame.columnconfigure(i, weight=1) - image = Images.get(ImageEnum.START, width=ICON_SIZE) + image = Images.get(ImageEnum.START, width=int(ICON_SIZE * self.app.app_scale)) self.play_button = ttk.Button(frame, image=image, command=self.click_play) self.play_button.image = image self.play_button.grid(row=0, column=0, sticky="ew", padx=PADX) - image = Images.get(ImageEnum.PAUSE, width=ICON_SIZE) + image = Images.get(ImageEnum.PAUSE, width=int(ICON_SIZE * self.app.app_scale)) self.pause_button = ttk.Button(frame, image=image, command=self.click_pause) self.pause_button.image = image self.pause_button.grid(row=0, column=1, sticky="ew", padx=PADX) - image = Images.get(ImageEnum.STOP, width=ICON_SIZE) + image = Images.get(ImageEnum.STOP, width=int(ICON_SIZE * self.app.app_scale)) self.stop_button = ttk.Button(frame, image=image, command=self.click_stop) self.stop_button.image = image self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX) diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index f2fe1db2..9a289aeb 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -38,7 +38,7 @@ class NodeServiceDialog(Dialog): if len(services) == 0: # not custom node type and node's services haven't been modified before if not NodeUtils.is_custom( - canvas_node.core_node.model + canvas_node.core_node.type, canvas_node.core_node.model ) and not self.app.core.service_been_modified(self.node_id): services = set(self.app.core.default_services[model]) # services of default type nodes were modified to be empty diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index 440fab40..dd1c1c04 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -129,16 +129,9 @@ class PreferencesDialog(Dialog): self.app.icon_text_font.config(size=int(12 * app_scale)) self.app.edge_font.config(size=int(8 * app_scale)) - # scale application widow size - screen_width = self.app.master.winfo_screenwidth() - screen_height = self.app.master.winfo_screenheight() - scaled_width = WIDTH * app_scale - scaled_height = HEIGHT * app_scale - x = int(screen_width / 2 - scaled_width / 2) - y = int(screen_height / 2 - scaled_height / 2) - self.app.master.geometry(f"{int(scaled_width)}x{int(scaled_height)}+{x}+{y}") + # scale application window + self.app.center() - # scale toolbar icons and picker icons + # scale toolbar and canvas items self.app.toolbar.scale() - self.app.canvas.scale_graph() diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 9516de14..e5d7b5db 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -13,6 +13,8 @@ if TYPE_CHECKING: TEXT_DISTANCE = 0.30 EDGE_WIDTH = 3 EDGE_COLOR = "#ff0000" +WIRELESS_WIDTH = 1.5 +WIRELESS_COLOR = "#009933" class CanvasWirelessEdge: @@ -32,8 +34,8 @@ class CanvasWirelessEdge: self.id = self.canvas.create_line( *position, tags=tags.WIRELESS_EDGE, - width=1.5 * self.canvas.app.app_scale, - fill="#009933", + width=WIRELESS_WIDTH * self.canvas.app.app_scale, + fill=WIRELESS_COLOR, ) def delete(self): diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index f705565f..0869a0d0 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -927,7 +927,9 @@ class CanvasGraph(tk.Canvas): def scale_graph(self): for nid, canvas_node in self.nodes.items(): img = None - if NodeUtils.is_custom(canvas_node.core_node.model): + if NodeUtils.is_custom( + canvas_node.core_node.type, canvas_node.core_node.model + ): for custom_node in self.app.guiconfig["nodes"]: if custom_node["name"] == canvas_node.core_node.model: img = Images.get_custom( @@ -942,6 +944,7 @@ class CanvasGraph(tk.Canvas): self.itemconfig(nid, image=img) canvas_node.image = img canvas_node.scale_text() + canvas_node.scale_antennas() for edge_id in self.find_withtag(tags.EDGE): self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app.app_scale)) diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index b8faef13..3ed5b1d9 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -16,7 +16,8 @@ from core.gui.dialogs.wlanconfig import WlanConfigDialog from core.gui.errors import show_grpc_error from core.gui.graph import tags from core.gui.graph.tooltip import CanvasTooltip -from core.gui.nodeutils import NodeUtils +from core.gui.images import ImageEnum, Images +from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils if TYPE_CHECKING: from core.gui.app import Application @@ -54,7 +55,8 @@ class CanvasNode: self.edges = set() self.interfaces = [] self.wireless_edges = set() - self.antennae = [] + self.antennas = [] + self.antenna_images = {} self.setup_bindings() def setup_bindings(self): @@ -70,33 +72,37 @@ class CanvasNode: def add_antenna(self): x, y = self.canvas.coords(self.id) - offset = len(self.antennae) * 8 + offset = len(self.antennas) * 8 * self.app.app_scale + img = Images.get(ImageEnum.ANTENNA, int(ANTENNA_SIZE * self.app.app_scale)) antenna_id = self.canvas.create_image( x - 16 + offset, - y - 23, + y - int(23 * self.app.app_scale), anchor=tk.CENTER, - image=NodeUtils.ANTENNA_ICON, + image=img, tags=tags.ANTENNA, ) - self.antennae.append(antenna_id) + self.antennas.append(antenna_id) + self.antenna_images[antenna_id] = img def delete_antenna(self): """ delete one antenna """ logging.debug("Delete an antenna on %s", self.core_node.name) - if self.antennae: - antenna_id = self.antennae.pop() + if self.antennas: + antenna_id = self.antennas.pop() self.canvas.delete(antenna_id) + self.antenna_images.pop(antenna_id, None) def delete_antennas(self): """ delete all antennas """ logging.debug("Remove all antennas for %s", self.core_node.name) - for antenna_id in self.antennae: + for antenna_id in self.antennas: self.canvas.delete(antenna_id) - self.antennae.clear() + self.antennas.clear() + self.antenna_images.clear() def redraw(self): self.canvas.itemconfig(self.id, image=self.image) @@ -135,7 +141,7 @@ class CanvasNode: self.canvas.move_selection(self.id, x_offset, y_offset) # move antennae - for antenna_id in self.antennae: + for antenna_id in self.antennas: self.canvas.move(antenna_id, x_offset, y_offset) # move edges @@ -299,3 +305,17 @@ class CanvasNode: if core_node.type == core_pb2.NodeType.DEFAULT and core_node.model == "mdr": self.canvas.create_edge(self, self.canvas.nodes[canvas_nid]) self.canvas.clear_selection() + + def scale_antennas(self): + for i in range(len(self.antennas)): + antenna_id = self.antennas[i] + image = Images.get( + ImageEnum.ANTENNA, int(ANTENNA_SIZE * self.app.app_scale) + ) + self.canvas.itemconfig(antenna_id, image=image) + self.antenna_images[antenna_id] = image + node_x, node_y = self.canvas.coords(self.id) + x, y = self.canvas.coords(antenna_id) + dx = node_x - 16 + (i * 8 * self.app.app_scale) - x + dy = node_y - int(23 * self.app.app_scale) - y + self.canvas.move(antenna_id, dx, dy) diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index a9cee938..81aa2cba 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -123,8 +123,8 @@ class NodeUtils: return image @classmethod - def is_custom(cls, model: str) -> bool: - return model not in cls.NODE_MODELS + def is_custom(cls, node_type: NodeType, model: str) -> bool: + return node_type == NodeType.DEFAULT and model not in cls.NODE_MODELS @classmethod def get_custom_node_services( From b3dabbfe05430946d6e6b76e9c3b2259e1c683d3 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 18 Feb 2020 10:33:49 -0800 Subject: [PATCH 0021/1131] delete wireless links on canvas during runtime --- daemon/core/gui/graph/node.py | 12 ++++++++++++ daemon/core/location/mobility.py | 5 +---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index e6a84e72..123eb2c3 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -66,6 +66,18 @@ class CanvasNode: def delete(self): logging.debug("Delete canvas node for %s", self.core_node) + # print(self.app.core.client.get_session(self.app.core.session_id)) + # response = self.app.core.client.delete_node(self.app.core.session_id, self.core_node.id) + # for wireless_edge in self.wireless_edges: + # token = wireless_edge.token + # other = token[0] + # if other == self.id: + # other = token[1] + # self.canvas.nodes[other].wireless_edges.discard(wireless_edge) + # wlan_edge = self.canvas.wireless_edges.pop(token, None) + # self.canvas.delete(wlan_edge.id) + + self.wireless_edges.clear() self.canvas.delete(self.id) self.canvas.delete(self.text_id) self.delete_antennas() diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 4689d217..2b6051a4 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -291,10 +291,7 @@ class BasicRangeModel(WirelessModel): label="transmission delay (usec)", ), Configuration( - _id="error", - _type=ConfigDataTypes.STRING, - default="0", - label="error rate (%)", + _id="error", _type=ConfigDataTypes.STRING, default="0", label="loss (%)" ), ] From 471f40a0bd1968c9e633a4501783c745658eb94b Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 18 Feb 2020 10:37:37 -0800 Subject: [PATCH 0022/1131] change wlan configuration's label name from error rate (%) to loss (%) to match the old gui --- daemon/core/location/mobility.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 4689d217..2b6051a4 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -291,10 +291,7 @@ class BasicRangeModel(WirelessModel): label="transmission delay (usec)", ), Configuration( - _id="error", - _type=ConfigDataTypes.STRING, - default="0", - label="error rate (%)", + _id="error", _type=ConfigDataTypes.STRING, default="0", label="loss (%)" ), ] From 08e652633f8ba1cd457e28aa9b2bd25bc2818476 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 18 Feb 2020 13:59:23 -0800 Subject: [PATCH 0023/1131] support wireless link deletion during runtime --- daemon/core/gui/graph/node.py | 74 +++++++++++++++++++++++++++++------ daemon/core/gui/nodeutils.py | 4 ++ 2 files changed, 65 insertions(+), 13 deletions(-) diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 123eb2c3..8210b4f2 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -17,7 +17,7 @@ from core.gui.dialogs.wlanconfig import WlanConfigDialog from core.gui.errors import show_grpc_error from core.gui.graph import tags from core.gui.graph.tooltip import CanvasTooltip -from core.gui.nodeutils import NodeUtils +from core.gui.nodeutils import EdgeUtils, NodeUtils if TYPE_CHECKING: from core.gui.app import Application @@ -66,21 +66,43 @@ class CanvasNode: def delete(self): logging.debug("Delete canvas node for %s", self.core_node) - # print(self.app.core.client.get_session(self.app.core.session_id)) - # response = self.app.core.client.delete_node(self.app.core.session_id, self.core_node.id) - # for wireless_edge in self.wireless_edges: - # token = wireless_edge.token - # other = token[0] - # if other == self.id: - # other = token[1] - # self.canvas.nodes[other].wireless_edges.discard(wireless_edge) - # wlan_edge = self.canvas.wireless_edges.pop(token, None) - # self.canvas.delete(wlan_edge.id) - self.wireless_edges.clear() + # if node is wlan, EMANE type, remove any existing wireless links between nodes connetect to this node + if NodeUtils.is_wireless_node(self.core_node.type): + nodes = [] + for edge in self.edges: + token = edge.token + if self.id == token[0]: + nodes.append(token[1]) + else: + nodes.append(token[0]) + for i in range(len(nodes)): + for j in range(i + 1, len(nodes)): + token = EdgeUtils.get_token(nodes[i], nodes[j]) + wireless_edge = self.canvas.wireless_edges.pop(token, None) + if wireless_edge: + + self.canvas.nodes[nodes[i]].wireless_edges.remove(wireless_edge) + self.canvas.nodes[nodes[j]].wireless_edges.remove(wireless_edge) + self.canvas.delete(wireless_edge.id) + else: + logging.debug("%s is not a wireless edge", token) + # if node is MDR, remove wireless links to other MDRs + elif NodeUtils.is_mdr_node(self.core_node.type, self.core_node.model): + for wireless_edge in self.wireless_edges: + token = wireless_edge.token + other = token[0] + if other == self.id: + other = token[1] + self.canvas.nodes[other].wireless_edges.discard(wireless_edge) + wlan_edge = self.canvas.wireless_edges.pop(token, None) + self.canvas.delete(wlan_edge.id) + self.delete_antennas() + + self.wireless_edges.clear() + self.canvas.delete(self.id) self.canvas.delete(self.text_id) - self.delete_antennas() def add_antenna(self): x, y = self.canvas.coords(self.id) @@ -307,3 +329,29 @@ class CanvasNode: if core_node.type == core_pb2.NodeType.DEFAULT and core_node.model == "mdr": self.canvas.create_edge(self, self.canvas.nodes[canvas_nid]) self.canvas.clear_selection() + + def remove_wireless_links(self): + """ + remove the wireless links between the nodes that are connected to this node, + if this node is a wireless network node (wlan or EMANE) + :return: + """ + if NodeUtils.is_wireless_node(self.core_node.type): + nodes = [] + for edge in self.edges: + token = edge.token + if self.id == token[0]: + nodes.append(token[1]) + else: + nodes.append(token[0]) + for i in range(len(nodes)): + for j in range(i + 1, len(nodes)): + token = EdgeUtils.get_token(nodes[i], nodes[j]) + wireless_edge = self.canvas.wireless_edges.pop(token, None) + if wireless_edge: + + self.canvas.nodes[nodes[i]].wireless_edges.remove(wireless_edge) + self.canvas.nodes[nodes[j]].wireless_edges.remove(wireless_edge) + self.canvas.delete(wireless_edge.id) + else: + logging.debug("%s is not a wireless edge", token) diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index c8ddb8fa..f0a3a35d 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -90,6 +90,10 @@ class NodeUtils: def is_rj45_node(cls, node_type: NodeType) -> bool: return node_type in cls.RJ45_NODES + @classmethod + def is_mdr_node(cls, node_type: NodeType, model: str) -> bool: + return cls.is_container_node(node_type) and model == "mdr" + @classmethod def node_icon( cls, From d8f586bd2be628c4d4bf1ee21529379ee3d0d09b Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 18 Feb 2020 15:58:18 -0800 Subject: [PATCH 0024/1131] add wireless network variable to CanvasGraph that maps a wireless/EMANE node to all MDRs connected to it --- daemon/core/gui/dialogs/wlanconfig.py | 10 ++++++++++ daemon/core/gui/graph/edges.py | 11 +++++++++++ daemon/core/gui/graph/graph.py | 4 ++++ 3 files changed, 25 insertions(+) diff --git a/daemon/core/gui/dialogs/wlanconfig.py b/daemon/core/gui/dialogs/wlanconfig.py index d6da667e..eb525f6d 100644 --- a/daemon/core/gui/dialogs/wlanconfig.py +++ b/daemon/core/gui/dialogs/wlanconfig.py @@ -27,6 +27,7 @@ class WlanConfigDialog(Dialog): self.canvas_node = canvas_node self.node = canvas_node.core_node self.config_frame = None + self.range_entry = None self.has_error = False try: self.config = self.app.core.get_wlan_config(self.node.id) @@ -53,6 +54,11 @@ class WlanConfigDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) + self.range_entry = self.config_frame.winfo_children()[0].frame.winfo_children()[ + -1 + ] + self.range_entry.bind("", self.update_range) + button = ttk.Button(frame, text="Apply", command=self.click_apply) button.grid(row=0, column=0, padx=PADX, sticky="ew") @@ -69,3 +75,7 @@ class WlanConfigDialog(Dialog): session_id = self.app.core.session_id self.app.core.client.set_wlan_config(session_id, self.node.id, config) self.destroy() + + def update_range(self, event): + if event.char.isdigit(): + print(self.range_entry.get() + event.char) diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 1259ffa9..c00fd2aa 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -177,6 +177,17 @@ class CanvasEdge: 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 def check_wireless(self): diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index a56f1423..77234064 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -42,6 +42,10 @@ class CanvasGraph(tk.Canvas): self.edges = {} self.shapes = {} self.wireless_edges = {} + + # map wireless/EMANE node to the set of MDRs connected to that node + self.wireless_network = {} + self.drawing_edge = None self.grid = None self.shape_drawing = False From 23aeb40f54f7ab5c955cc2d3e9c24dd1b90a2275 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 19 Feb 2020 13:22:52 -0800 Subject: [PATCH 0025/1131] display the range while configuring wlan node --- daemon/core/gui/dialogs/wlanconfig.py | 56 +++++++++++++++++++++++---- daemon/core/gui/graph/edges.py | 3 +- daemon/core/gui/graph/node.py | 2 +- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/daemon/core/gui/dialogs/wlanconfig.py b/daemon/core/gui/dialogs/wlanconfig.py index eb525f6d..c0c8c845 100644 --- a/daemon/core/gui/dialogs/wlanconfig.py +++ b/daemon/core/gui/dialogs/wlanconfig.py @@ -1,7 +1,3 @@ -""" -wlan configuration -""" - from tkinter import ttk from typing import TYPE_CHECKING @@ -16,6 +12,9 @@ if TYPE_CHECKING: from core.gui.app import Application from core.gui.graph.node import CanvasNode +RANGE_COLOR = "#009933" +RANGE_WIDTH = 3 + class WlanConfigDialog(Dialog): def __init__( @@ -29,14 +28,27 @@ class WlanConfigDialog(Dialog): self.config_frame = None self.range_entry = None self.has_error = False + self.canvas = app.canvas + self.ranges = {} + self.positive_int = self.app.master.register(self.validate_and_update) try: self.config = self.app.core.get_wlan_config(self.node.id) + self.init_draw_range() self.draw() except grpc.RpcError as e: show_grpc_error(e, self.app, self.app) self.has_error = True self.destroy() + def init_draw_range(self): + if self.canvas_node.id in self.canvas.wireless_network: + for cid in self.canvas.wireless_network[self.canvas_node.id]: + x, y = self.canvas.coords(cid) + range_id = self.canvas.create_oval( + x, y, x, y, width=RANGE_WIDTH, outline=RANGE_COLOR, tags="range" + ) + self.ranges[cid] = range_id + def draw(self): self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) @@ -44,6 +56,7 @@ class WlanConfigDialog(Dialog): self.config_frame.draw_config() self.config_frame.grid(sticky="nsew", pady=PADY) self.draw_apply_buttons() + self.top.bind("", self.remove_ranges) def draw_apply_buttons(self): """ @@ -57,7 +70,7 @@ class WlanConfigDialog(Dialog): self.range_entry = self.config_frame.winfo_children()[0].frame.winfo_children()[ -1 ] - self.range_entry.bind("", self.update_range) + self.range_entry.config(validatecommand=(self.positive_int, "%P")) button = ttk.Button(frame, text="Apply", command=self.click_apply) button.grid(row=0, column=0, padx=PADX, sticky="ew") @@ -74,8 +87,35 @@ class WlanConfigDialog(Dialog): if self.app.core.is_runtime(): session_id = self.app.core.session_id self.app.core.client.set_wlan_config(session_id, self.node.id, config) + self.remove_ranges() self.destroy() - def update_range(self, event): - if event.char.isdigit(): - print(self.range_entry.get() + event.char) + def remove_ranges(self, event=None): + for cid in self.canvas.find_withtag("range"): + self.canvas.delete(cid) + self.ranges.clear() + + def validate_and_update(self, s: str) -> bool: + """ + custom validation to also redraw the mdr ranges when the range value changes + """ + if len(s) == 0: + return True + try: + int_value = int(s) + if int_value >= 0: + net_range = int_value * self.canvas.ratio + if self.canvas_node.id in self.canvas.wireless_network: + for cid in self.canvas.wireless_network[self.canvas_node.id]: + x, y = self.canvas.coords(cid) + self.canvas.coords( + self.ranges[cid], + x - net_range, + y - net_range, + x + net_range, + y + net_range, + ) + return True + return False + except ValueError: + return False diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index c00fd2aa..0ca1c8e6 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -14,6 +14,7 @@ if TYPE_CHECKING: TEXT_DISTANCE = 0.30 EDGE_WIDTH = 3 EDGE_COLOR = "#ff0000" +WIRELESS_COLOR = "#009933" class CanvasWirelessEdge: @@ -31,7 +32,7 @@ class CanvasWirelessEdge: self.dst = dst self.canvas = canvas self.id = self.canvas.create_line( - *position, tags=tags.WIRELESS_EDGE, width=1.5, fill="#009933" + *position, tags=tags.WIRELESS_EDGE, width=EDGE_WIDTH, fill="#009933" ) def delete(self): diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 8210b4f2..ecd95d58 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -97,7 +97,7 @@ class CanvasNode: self.canvas.nodes[other].wireless_edges.discard(wireless_edge) wlan_edge = self.canvas.wireless_edges.pop(token, None) self.canvas.delete(wlan_edge.id) - self.delete_antennas() + self.delete_antennas() self.wireless_edges.clear() From 8572e153f4a9a8129db2526c3a56236d371ea3d6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 19 Feb 2020 21:21:21 -0800 Subject: [PATCH 0026/1131] fixed comparison logic for waypoints and added tests to help catch issue in the future --- daemon/core/location/mobility.py | 8 ++++---- daemon/tests/test_mobility.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 daemon/tests/test_mobility.py diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 2b6051a4..55af58d9 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -570,10 +570,10 @@ class WayPoint: return not self == other def __lt__(self, other: "WayPoint") -> bool: - result = self.time < other.time - if result: - result = self.nodenum < other.nodenum - return result + if self.time == other.time: + return self.nodenum < other.nodenum + else: + return self.time < other.time class WayPointMobility(WirelessModel): diff --git a/daemon/tests/test_mobility.py b/daemon/tests/test_mobility.py new file mode 100644 index 00000000..e2e8f90e --- /dev/null +++ b/daemon/tests/test_mobility.py @@ -0,0 +1,17 @@ +import pytest + +from core.location.mobility import WayPoint + + +class TestMobility: + @pytest.mark.parametrize( + "wp1, wp2, expected", + [ + (WayPoint(10.0, 1, [0, 0], 1.0), WayPoint(1.0, 2, [0, 0], 1.0), False), + (WayPoint(1.0, 1, [0, 0], 1.0), WayPoint(10.0, 2, [0, 0], 1.0), True), + (WayPoint(1.0, 1, [0, 0], 1.0), WayPoint(1.0, 2, [0, 0], 1.0), True), + (WayPoint(1.0, 2, [0, 0], 1.0), WayPoint(1.0, 1, [0, 0], 1.0), False), + ], + ) + def test_waypoint_lessthan(self, wp1, wp2, expected): + assert (wp1 < wp2) == expected From 2a6c6ac286b9934b2ecbca713642e92aa6687e4f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 20 Feb 2020 09:03:34 -0800 Subject: [PATCH 0027/1131] version bump for next release --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index c27d6f7f..905fbde0 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, 6.0.0) +AC_INIT(core, 6.1.0) # autoconf and automake initialization AC_CONFIG_SRCDIR([netns/version.h.in]) From d128f2425dd042885b14a6b0e5c816358c848d79 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 20 Feb 2020 09:32:37 -0800 Subject: [PATCH 0028/1131] updated changelog --- CHANGELOG.md | 56 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 986d8abf..35ee1ef4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,35 @@ +## 2020-02-20 CORE 6.1.0 +* New + * config services - these services leverage a proper template engine and have configurable parameters, given enough time may replace existing services + * core-imn-to-xml - IMN to XML utility script + * replaced internal code for determining ip/mac address with netaddr library +* Enhancements + * added distributed package for built packages + * made use of python type hinting for functions and their return values + * updated Quagga zebra service to remove deprecated warning +* Removed + * removed stale ns3 code +* CORETK GUI + * added logging + * improved error dialog + * properly use global ipv6 addresses for nodes + * disable proxy usage by default, flag available to enable +* gRPC API + * add_link - now returns created interface information + * set_node_service - can now set files and directories to properly replicate previous usage + * get_emane_event_channel - return information related to the currently used emane event channel +* Bugfixes + * fixed session SDT functionality back to working order, due to python3 changes + * avoid shutting down services for nodes that are not up + * EMANE bypass model options will now display properly in GUIs + * XML scenarios will now properly read in custom node icons + * \#372 - fixed mobility waypoint comparisons + * \#370 - fixed radvd service + * \#368 - updated frr services to properly start staticd when needed + * \#358 - fixed systemd service install path + * \#350 - fixed frr babel wireless configuration + * \#354 - updated frr to reset interfaces to properly take configurations + ## 2020-01-01 CORE 6.0.0 * New * beta release of the python based tk GUI, use **coretk-gui** to try it out, plan will be to eventually sunset the old GUI once this is good enough @@ -114,11 +146,11 @@ * Added EMANE prefix configuration when looking for emane model manifest files * requires configuring **emane_prefix** in /etc/core/core.conf * Cleanup - * Refactoring of the core python package structure, trying to help provide better organization and + * Refactoring of the core python package structure, trying to help provide better organization and logical groupings * Issues * \#246 - Fixed network to network link handling when reading xml files - * \#236 - Fixed storing/reading of link configuration values within xml files + * \#236 - Fixed storing/reading of link configuration values within xml files * \#170 - FRR Service * \#155 - EMANE path configuration * \#233 - Python 3 support @@ -152,16 +184,16 @@ ## 2018-05-22 CORE 5.1 * DAEMON: - * removed and cleared out code that is either legacy or no longer supported (Xen, BSD, Kernel patching, RPM/DEB + * removed and cleared out code that is either legacy or no longer supported (Xen, BSD, Kernel patching, RPM/DEB specific files) * default nodes are now set in the node map * moved ns3 and netns directories to the top of the repo * changes to make use of fpm as the tool for building packages * removed usage of logzero to avoid dependency issues for built packages * removed daemon addons directory - * added CoreEmu to core.emulator.coreemu to help begin serving as the basis for a more formal API for scripting + * added CoreEmu to core.emulator.coreemu to help begin serving as the basis for a more formal API for scripting and creating new external APIs out of - * cleaned up logging, moved more logging to DEBUG from INFO, tried to mold INFO message to be more simple and + * cleaned up logging, moved more logging to DEBUG from INFO, tried to mold INFO message to be more simple and informative * EMANE 1.0.1-1.21 supported * updates to leverage EMANE python bindings for dynamically parsing phy/mac manifest files @@ -175,7 +207,7 @@ * updated broken help links in GUI Help->About * Packaging: * fixed PYTHON_PATH to PYTHONPATH in sysv script - * added make command to leverage FPM as the tool for creating deb/rpm packages going forward, there is documentation + * added make command to leverage FPM as the tool for creating deb/rpm packages going forward, there is documentation within README.md to try it out * TEST: * fixed some broken tests @@ -184,7 +216,7 @@ * \#142 - duplication of custom services * \#136 - sphinx-apidoc command not found * \#137 - make command fails when using distclean - + ## 2017-09-01 CORE 5.0 * DEVELOPMENT: * support for editorconfig to help standardize development across IDEs, from the defined configuration file @@ -339,7 +371,7 @@ * added "--addons" startup mode to pass control to code included from addons dir * added "Locked" entry to View menu to prevent moving items * use currently selected node type when invoking a topology generator - * updated throughput plots with resizing, color picker, plot labels, locked scales, and save/load plot + * updated throughput plots with resizing, color picker, plot labels, locked scales, and save/load plot configuration with imn file * improved session dialog * EMANE: @@ -356,11 +388,11 @@ * XML import and export * renamed "cored.py" to "cored", "coresendmsg.py" to "coresendmsg" * code reorganization and clean-up - * updated XML export to write NetworkPlan, MotionPlan, and ServicePlan within a Scenario tag, added new + * updated XML export to write NetworkPlan, MotionPlan, and ServicePlan within a Scenario tag, added new "Save As XML..." File menu entry * added script_start/pause/stop options to Ns2ScriptedMobility * "python" source sub-directory renamed to "daemon" - * added "cored -e" option to execute a Python script, adding its session to the active sessions list, allowing for + * added "cored -e" option to execute a Python script, adding its session to the active sessions list, allowing for GUI connection * support comma-separated list for custom_services_dir in core.conf file * updated kernel patches for Linux kernel 3.5 @@ -369,7 +401,7 @@ * integrate ns-3 node location between CORE and ns-3 simulation * added ns-3 random walk mobility example * updated ns-3 Wifi example to allow GUI connection and moving of nodes -* fixed the following bugs: 54, 103, 111, 136, 145, 153, 157, 160, 161, 162, 164, 165, 168, 170, 171, 173, 174, 176, +* fixed the following bugs: 54, 103, 111, 136, 145, 153, 157, 160, 161, 162, 164, 165, 168, 170, 171, 173, 174, 176, 184, 190, 193 ## 2012-09-25 CORE 4.4 @@ -410,7 +442,7 @@ * support /etc/core/environment and ~/.core/environment files * added Ns2ScriptedMobility model to Python, removed from the GUI * namespace nodes mount a private /sys - * fixed the following bugs: 80, 81, 84, 99, 104, 109, 110, 122, 124, 131, 133, 134, 135, 137, 140, 143, 144, 146, + * fixed the following bugs: 80, 81, 84, 99, 104, 109, 110, 122, 124, 131, 133, 134, 135, 137, 140, 143, 144, 146, 147, 151, 154, 155 ## 2012-03-07 CORE 4.3 From 44bf4e020cc8ed1179b150a49d87d258918beab1 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 20 Feb 2020 09:46:25 -0800 Subject: [PATCH 0029/1131] updated config services frr to match standard frr service --- .../configservices/frrservices/templates/frrboot.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/daemon/core/configservices/frrservices/templates/frrboot.sh b/daemon/core/configservices/frrservices/templates/frrboot.sh index 5a6a0e3d..3c14cd1a 100644 --- a/daemon/core/configservices/frrservices/templates/frrboot.sh +++ b/daemon/core/configservices/frrservices/templates/frrboot.sh @@ -74,6 +74,9 @@ bootfrr() fi bootdaemon "zebra" + if grep -q "^ip route " $FRR_CONF; then + bootdaemon "staticd" + fi for r in rip ripng ospf6 ospf bgp babel; do if grep -q "^router \\<$${}{r}\\>" $FRR_CONF; then bootdaemon "$${}{r}d" @@ -93,3 +96,10 @@ if [ "$1" != "zebra" ]; then fi confcheck bootfrr + +# reset interfaces +% for ifc, _, _ , _ in interfaces: +ip link set dev ${ifc.name} down +sleep 1 +ip link set dev ${ifc.name} up +% endfor From 20be527add0e0a78b4eebacf572a9d099d8a93ba Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 20 Feb 2020 10:02:13 -0800 Subject: [PATCH 0030/1131] remove extra code --- daemon/core/gui/dialogs/preferences.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index dd1c1c04..83f50f07 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -10,9 +10,6 @@ from core.gui.themes import FRAME_PAD, PADX, PADY, scale_fonts if TYPE_CHECKING: from core.gui.app import Application -WIDTH = 1000 -HEIGHT = 800 - class PreferencesDialog(Dialog): def __init__(self, master: "Application", app: "Application"): From e90eff578e076bd5c8db05514f80b064e60d8420 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 20 Feb 2020 11:16:26 -0800 Subject: [PATCH 0031/1131] reset variable --- daemon/core/gui/graph/graph.py | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 811bca45..7905ed8c 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -117,6 +117,7 @@ class CanvasGraph(tk.Canvas): self.edges.clear() self.shapes.clear() self.wireless_edges.clear() + self.wireless_network.clear() self.drawing_edge = None self.draw_session(session) From 2a8f689ad52b4c7a93f87bfb4f4b65c6fa53c551 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 20 Feb 2020 11:26:48 -0800 Subject: [PATCH 0032/1131] remove extra code --- daemon/core/gui/graph/node.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 96003905..7b4ccf31 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -345,32 +345,6 @@ class CanvasNode: self.canvas.create_edge(self, self.canvas.nodes[canvas_nid]) self.canvas.clear_selection() - def remove_wireless_links(self): - """ - remove the wireless links between the nodes that are connected to this node, - if this node is a wireless network node (wlan or EMANE) - :return: - """ - if NodeUtils.is_wireless_node(self.core_node.type): - nodes = [] - for edge in self.edges: - token = edge.token - if self.id == token[0]: - nodes.append(token[1]) - else: - nodes.append(token[0]) - for i in range(len(nodes)): - for j in range(i + 1, len(nodes)): - token = EdgeUtils.get_token(nodes[i], nodes[j]) - wireless_edge = self.canvas.wireless_edges.pop(token, None) - if wireless_edge: - - self.canvas.nodes[nodes[i]].wireless_edges.remove(wireless_edge) - self.canvas.nodes[nodes[j]].wireless_edges.remove(wireless_edge) - self.canvas.delete(wireless_edge.id) - else: - logging.debug("%s is not a wireless edge", token) - def scale_antennas(self): for i in range(len(self.antennas)): antenna_id = self.antennas[i] From 3a2da0282fa76d592699a890d3f8fab618fc4ea8 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 20 Feb 2020 15:46:18 -0800 Subject: [PATCH 0033/1131] display error dialog when start session fails --- daemon/core/gui/errors.py | 9 +++++++++ daemon/core/gui/task.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/daemon/core/gui/errors.py b/daemon/core/gui/errors.py index b6152489..51c90e35 100644 --- a/daemon/core/gui/errors.py +++ b/daemon/core/gui/errors.py @@ -36,3 +36,12 @@ def show_grpc_error(e: "grpc.RpcError", master, app: "Application"): title = f"GRPC {title}" dialog = ErrorDialog(master, app, title, e.details()) dialog.show() + + +def show_grpc_response_exceptions(class_name, exceptions, master, app: "Application"): + title = f"Exceptions from {class_name}" + detail = "" + for e in exceptions: + detail = detail + f"{e}\n" + dialog = ErrorDialog(master, app, title, detail) + dialog.show() diff --git a/daemon/core/gui/task.py b/daemon/core/gui/task.py index bd7423ee..2863326f 100644 --- a/daemon/core/gui/task.py +++ b/daemon/core/gui/task.py @@ -2,6 +2,8 @@ import logging import threading from typing import Any, Callable +from core.gui.errors import show_grpc_response_exceptions + class BackgroundTask: def __init__(self, master: Any, task: Callable, callback: Callable = None, args=()): @@ -19,6 +21,19 @@ class BackgroundTask: def run(self): result = self.task(*self.args) logging.info("task completed") + # if start session fails, a response with Result: False and a list of exceptions is returned + if hasattr(result, "result") and not result.result: + if hasattr(result, "exceptions") and len(result.exceptions) > 0: + self.master.after( + 0, + show_grpc_response_exceptions, + *( + result.__class__.__name__, + result.exceptions, + self.master, + self.master, + ) + ) if self.callback: if result is None: args = () From 95c32ddd28b99c1b7b3962dfa4f2d649cbe82755 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 21 Feb 2020 15:54:55 -0800 Subject: [PATCH 0034/1131] initial geo location conversion using pyproj --- daemon/Pipfile | 1 + daemon/Pipfile.lock | 585 +++++++++++++++------------- daemon/core/api/tlv/corehandlers.py | 1 - daemon/core/emulator/session.py | 4 +- daemon/core/location/geo.py | 119 ++++++ daemon/setup.py.in | 1 + 6 files changed, 435 insertions(+), 276 deletions(-) create mode 100644 daemon/core/location/geo.py diff --git a/daemon/Pipfile b/daemon/Pipfile index d55b248f..3653f82f 100644 --- a/daemon/Pipfile +++ b/daemon/Pipfile @@ -21,3 +21,4 @@ mock = "*" [packages] core = {editable = true,path = "."} +pyproj = "*" diff --git a/daemon/Pipfile.lock b/daemon/Pipfile.lock index b3cacedc..4b720b93 100644 --- a/daemon/Pipfile.lock +++ b/daemon/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d702e6eed5a1362bf261543572bbffd2e8a87140b8d8cb07b99fb0d25220a2b5" + "sha256": "f737e90b72e5e8c1f92357272d0827e359848ac923d2e48fde9af1b4a67c855e" }, "pipfile-spec": 6, "requires": {}, @@ -39,41 +39,36 @@ }, "cffi": { "hashes": [ - "sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42", - "sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04", - "sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5", - "sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54", - "sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba", - "sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57", - "sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396", - "sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12", - "sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97", - "sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43", - "sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db", - "sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3", - "sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b", - "sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579", - "sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346", - "sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159", - "sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652", - "sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e", - "sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a", - "sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506", - "sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f", - "sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d", - "sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c", - "sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20", - "sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858", - "sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc", - "sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a", - "sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3", - "sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e", - "sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410", - "sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25", - "sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b", - "sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d" + "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff", + "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b", + "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac", + "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0", + "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384", + "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26", + "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6", + "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b", + "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e", + "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd", + "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2", + "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66", + "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc", + "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8", + "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55", + "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4", + "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5", + "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d", + "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78", + "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa", + "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793", + "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f", + "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a", + "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f", + "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30", + "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f", + "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3", + "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c" ], - "version": "==1.13.2" + "version": "==1.14.0" }, "core": { "editable": true, @@ -114,90 +109,91 @@ }, "grpcio": { "hashes": [ - "sha256:066630f6b62bffa291dacbee56994279a6a3682b8a11967e9ccaf3cc770fc11e", - "sha256:07e95762ca6b18afbeb3aa2793e827c841152d5e507089b1db0b18304edda105", - "sha256:0a0fb2f8e3a13537106bc77e4c63005bc60124a6203034304d9101921afa4e90", - "sha256:0c61b74dcfb302613926e785cb3542a0905b9a3a86e9410d8cf5d25e25e10104", - "sha256:13383bd70618da03684a8aafbdd9e3d9a6720bf8c07b85d0bc697afed599d8f0", - "sha256:1c6e0f6b9d091e3717e9a58d631c8bb4898be3b261c2a01fe46371fdc271052f", - "sha256:1cf710c04689daa5cc1e598efba00b028215700dcc1bf66fcb7b4f64f2ea5d5f", - "sha256:2da5cee9faf17bb8daf500cd0d28a17ae881ab5500f070a6aace457f4c08cac4", - "sha256:2f78ebf340eaf28fa09aba0f836a8b869af1716078dfe8f3b3f6ff785d8f2b0f", - "sha256:33a07a1a8e817d733588dbd18e567caad1a6fe0d440c165619866cd490c7911a", - "sha256:3d090c66af9c065b7228b07c3416f93173e9839b1d40bb0ce3dd2aa783645026", - "sha256:42b903a3596a10e2a3727bae2a76f8aefd324d498424b843cfa9606847faea7b", - "sha256:4fffbb58134c4f23e5a8312ac3412db6f5e39e961dc0eb5e3115ce5aa16bf927", - "sha256:57be5a6c509a406fe0ffa6f8b86904314c77b5e2791be8123368ad2ebccec874", - "sha256:5b0fa09efb33e2af4e8822b4eb8b2cbc201d562e3e185c439be7eaeee2e8b8aa", - "sha256:5ef42dfc18f9a63a06aca938770b69470bb322e4c137cf08cf21703d1ef4ae5c", - "sha256:6a43d2f2ff8250f200fdf7aa31fa191a997922aa9ea1182453acd705ad83ab72", - "sha256:6d8ab28559be98b02f8b3a154b53239df1aa5b0d28ff865ae5be4f30e7ed4d3f", - "sha256:6e47866b7dc14ca3a12d40c1d6082e7bea964670f1c5315ea0fb8b0550244d64", - "sha256:6edda1b96541187f73aab11800d25f18ee87e53d5f96bb74473873072bf28a0e", - "sha256:7109c8738a8a3c98cfb5dda1c45642a8d6d35dc00d257ab7a175099b2b4daecd", - "sha256:8d866aafb08657c456a18c4a31c8526ea62de42427c242b58210b9eae6c64559", - "sha256:9939727d9ae01690b24a2b159ac9dbca7b7e8e6edd5af6a6eb709243cae7b52b", - "sha256:99fd873699df17cb11c542553270ae2b32c169986e475df0d68a8629b8ef4df7", - "sha256:b6fda5674f990e15e1bcaacf026428cf50bce36e708ddcbd1de9673b14aab760", - "sha256:bdb2f3dcb664f0c39ef1312cd6acf6bc6375252e4420cf8f36fff4cb4fa55c71", - "sha256:bfd7d3130683a1a0a50c456273c21ec8a604f2d043b241a55235a78a0090ee06", - "sha256:c6c2db348ac73d73afe14e0833b18abbbe920969bf2c5c03c0922719f8020d06", - "sha256:cb7a4b41b5e2611f85c3402ac364f1d689f5d7ecbc24a55ef010eedcd6cf460f", - "sha256:cd3d3e328f20f7c807a862620c6ee748e8d57ba2a8fc960d48337ed71c6d9d32", - "sha256:d1a481777952e4f99b8a6956581f3ee866d7614100d70ae6d7e07327570b85ce", - "sha256:d1d49720ed636920bb3d74cedf549382caa9ad55aea89d1de99d817068d896b2", - "sha256:d42433f0086cccd192114343473d7dbd4aae9141794f939e2b7b83efc57543db", - "sha256:d44c34463a7c481e076f691d8fa25d080c3486978c2c41dca09a8dd75296c2d7", - "sha256:d7e5b7af1350e9c8c17a7baf99d575fbd2de69f7f0b0e6ebd47b57506de6493a", - "sha256:d9542366a0917b9b48bab1fee481ac01f56bdffc52437b598c09e7840148a6a9", - "sha256:df7cdfb40179acc9790a462c049e0b8e109481164dd7ad1a388dd67ff1528759", - "sha256:e1a9d9d2e7224d981aea8da79260c7f6932bf31ce1f99b7ccfa5eceeb30dc5d0", - "sha256:ed10e5fad105ecb0b12822f924e62d0deb07f46683a0b64416b17fd143daba1d", - "sha256:f0ec5371ce2363b03531ed522bfbe691ec940f51f0e111f0500fc0f44518c69d", - "sha256:f6580a8a4f5e701289b45fd62a8f6cb5ec41e4d77082424f8b676806dcd22564", - "sha256:f7b83e4b2842d44fce3cdc0d54db7a7e0d169a598751bf393601efaa401c83e0", - "sha256:ffec45b0db18a555fdfe0c6fa2d0a3fceb751b22b31e8fcd14ceed7bde05481e" + "sha256:02aef8ef1a5ac5f0836b543e462eb421df6048a7974211a906148053b8055ea6", + "sha256:07f82aefb4a56c7e1e52b78afb77d446847d27120a838a1a0489260182096045", + "sha256:1cff47297ee614e7ef66243dc34a776883ab6da9ca129ea114a802c5e58af5c1", + "sha256:1ec8fc865d8da6d0713e2092a27eee344cd54628b2c2065a0e77fff94df4ae00", + "sha256:1ef949b15a1f5f30651532a9b54edf3bd7c0b699a10931505fa2c80b2d395942", + "sha256:209927e65395feb449783943d62a3036982f871d7f4045fadb90b2d82b153ea8", + "sha256:25c77692ea8c0929d4ad400ea9c3dcbcc4936cee84e437e0ef80da58fa73d88a", + "sha256:28f27c64dd699b8b10f70da5f9320c1cffcaefca7dd76275b44571bd097f276c", + "sha256:355bd7d7ce5ff2917d217f0e8ddac568cb7403e1ce1639b35a924db7d13a39b6", + "sha256:4a0a33ada3f6f94f855f92460896ef08c798dcc5f17d9364d1735c5adc9d7e4a", + "sha256:4d3b6e66f32528bf43ca2297caca768280a8e068820b1c3dca0fcf9f03c7d6f1", + "sha256:5121fa96c79fc0ec81825091d0be5c16865f834f41b31da40b08ee60552f9961", + "sha256:57949756a3ce1f096fa2b00f812755f5ab2effeccedb19feeb7d0deafa3d1de7", + "sha256:586d931736912865c9790c60ca2db29e8dc4eace160d5a79fec3e58df79a9386", + "sha256:5ae532b93cf9ce5a2a549b74a2c35e3b690b171ece9358519b3039c7b84c887e", + "sha256:5dab393ab96b2ce4012823b2f2ed4ee907150424d2f02b97bd6f8dd8f17cc866", + "sha256:5ebc13451246de82f130e8ee7e723e8d7ae1827f14b7b0218867667b1b12c88d", + "sha256:68a149a0482d0bc697aac702ec6efb9d380e0afebf9484db5b7e634146528371", + "sha256:6db7ded10b82592c472eeeba34b9f12d7b0ab1e2dcad12f081b08ebdea78d7d6", + "sha256:6e545908bcc2ae28e5b190ce3170f92d0438cf26a82b269611390114de0106eb", + "sha256:6f328a3faaf81a2546a3022b3dfc137cc6d50d81082dbc0c94d1678943f05df3", + "sha256:706e2dea3de33b0d8884c4d35ecd5911b4ff04d0697c4138096666ce983671a6", + "sha256:80c3d1ce8820dd819d1c9d6b63b6f445148480a831173b572a9174a55e7abd47", + "sha256:8111b61eee12d7af5c58f82f2c97c2664677a05df9225ef5cbc2f25398c8c454", + "sha256:9713578f187fb1c4d00ac554fe1edcc6b3ddd62f5d4eb578b81261115802df8e", + "sha256:9c0669ba9aebad540fb05a33beb7e659ea6e5ca35833fc5229c20f057db760e8", + "sha256:9e9cfe55dc7ac2aa47e0fd3285ff829685f96803197042c9d2f0fb44e4b39b2c", + "sha256:a22daaf30037b8e59d6968c76fe0f7ff062c976c7a026e92fbefc4c4bf3fc5a4", + "sha256:a25b84e10018875a0f294a7649d07c43e8bc3e6a821714e39e5cd607a36386d7", + "sha256:a71138366d57901597bfcc52af7f076ab61c046f409c7b429011cd68de8f9fe6", + "sha256:b4efde5524579a9ce0459ca35a57a48ca878a4973514b8bb88cb80d7c9d34c85", + "sha256:b78af4d42985ab3143d9882d0006f48d12f1bc4ba88e78f23762777c3ee64571", + "sha256:bb2987eb3af9bcf46019be39b82c120c3d35639a95bc4ee2d08f36ecdf469345", + "sha256:c03ce53690fe492845e14f4ab7e67d5a429a06db99b226b5c7caa23081c1e2bb", + "sha256:c59b9280284b791377b3524c8e39ca7b74ae2881ba1a6c51b36f4f1bb94cee49", + "sha256:d18b4c8cacbb141979bb44355ee5813dd4d307e9d79b3a36d66eca7e0a203df8", + "sha256:d1e5563e3b7f844dbc48d709c9e4a75647e11d0387cc1fa0c861d3e9d34bc844", + "sha256:d22c897b65b1408509099f1c3334bd3704f5e4eb7c0486c57d0e212f71cb8f54", + "sha256:dbec0a3a154dbf2eb85b38abaddf24964fa1c059ee0a4ad55d6f39211b1a4bca", + "sha256:ed123037896a8db6709b8ad5acc0ed435453726ea0b63361d12de369624c2ab5", + "sha256:f3614dabd2cc8741850597b418bcf644d4f60e73615906c3acc407b78ff720b3", + "sha256:f9d632ce9fd485119c968ec6a7a343de698c5e014d17602ae2f110f1b05925ed", + "sha256:fb62996c61eeff56b59ab8abfcaa0859ec2223392c03d6085048b576b567459b" ], - "version": "==1.26.0" + "version": "==1.27.2" }, "invoke": { "hashes": [ - "sha256:4668a4a594a47f2da2f0672ec2f7b1566f809cebf10bcd95ce2de9ecf39b95d1", - "sha256:ae7b4513638bde9afcda0825e9535599637a3f65bd819a27098356027bb17c8a", - "sha256:e04faba8ea7cdf6f5c912be42dcafd5c1074b7f2f306998992c4bfb40a9a690b" + "sha256:87b3ef9d72a1667e104f89b159eaf8a514dbf2f3576885b2bbdefe74c3fb2132", + "sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134", + "sha256:de3f23bfe669e3db1085789fd859eb8ca8e0c5d9c20811e2407fa042e8a5e15d" ], - "version": "==1.4.0" + "version": "==1.4.1" }, "lxml": { "hashes": [ - "sha256:00ac0d64949fef6b3693813fe636a2d56d97a5a49b5bbb86e4cc4cc50ebc9ea2", - "sha256:0571e607558665ed42e450d7bf0e2941d542c18e117b1ebbf0ba72f287ad841c", - "sha256:0e3f04a7615fdac0be5e18b2406529521d6dbdb0167d2a690ee328bef7807487", - "sha256:13cf89be53348d1c17b453867da68704802966c433b2bb4fa1f970daadd2ef70", - "sha256:217262fcf6a4c2e1c7cb1efa08bd9ebc432502abc6c255c4abab611e8be0d14d", - "sha256:223e544828f1955daaf4cefbb4853bc416b2ec3fd56d4f4204a8b17007c21250", - "sha256:277cb61fede2f95b9c61912fefb3d43fbd5f18bf18a14fae4911b67984486f5d", - "sha256:3213f753e8ae86c396e0e066866e64c6b04618e85c723b32ecb0909885211f74", - "sha256:4690984a4dee1033da0af6df0b7a6bde83f74e1c0c870623797cec77964de34d", - "sha256:4fcc472ef87f45c429d3b923b925704aa581f875d65bac80f8ab0c3296a63f78", - "sha256:61409bd745a265a742f2693e4600e4dbd45cc1daebe1d5fad6fcb22912d44145", - "sha256:678f1963f755c5d9f5f6968dded7b245dd1ece8cf53c1aa9d80e6734a8c7f41d", - "sha256:6c6d03549d4e2734133badb9ab1c05d9f0ef4bcd31d83e5d2b4747c85cfa21da", - "sha256:6e74d5f4d6ecd6942375c52ffcd35f4318a61a02328f6f1bd79fcb4ffedf969e", - "sha256:7b4fc7b1ecc987ca7aaf3f4f0e71bbfbd81aaabf87002558f5bc95da3a865bcd", - "sha256:7ed386a40e172ddf44c061ad74881d8622f791d9af0b6f5be20023029129bc85", - "sha256:8f54f0924d12c47a382c600c880770b5ebfc96c9fd94cf6f6bdc21caf6163ea7", - "sha256:ad9b81351fdc236bda538efa6879315448411a81186c836d4b80d6ca8217cdb9", - "sha256:bbd00e21ea17f7bcc58dccd13869d68441b32899e89cf6cfa90d624a9198ce85", - "sha256:c3c289762cc09735e2a8f8a49571d0e8b4f57ea831ea11558247b5bdea0ac4db", - "sha256:cf4650942de5e5685ad308e22bcafbccfe37c54aa7c0e30cd620c2ee5c93d336", - "sha256:cfcbc33c9c59c93776aa41ab02e55c288a042211708b72fdb518221cc803abc8", - "sha256:e301055deadfedbd80cf94f2f65ff23126b232b0d1fea28f332ce58137bcdb18", - "sha256:ebbfe24df7f7b5c6c7620702496b6419f6a9aa2fd7f005eb731cc80d7b4692b9", - "sha256:eff69ddbf3ad86375c344339371168640951c302450c5d3e9936e98d6459db06", - "sha256:f6ed60a62c5f1c44e789d2cf14009423cb1646b44a43e40a9cf6a21f077678a1" + "sha256:06d4e0bbb1d62e38ae6118406d7cdb4693a3fa34ee3762238bcb96c9e36a93cd", + "sha256:0701f7965903a1c3f6f09328c1278ac0eee8f56f244e66af79cb224b7ef3801c", + "sha256:1f2c4ec372bf1c4a2c7e4bb20845e8bcf8050365189d86806bad1e3ae473d081", + "sha256:4235bc124fdcf611d02047d7034164897ade13046bda967768836629bc62784f", + "sha256:5828c7f3e615f3975d48f40d4fe66e8a7b25f16b5e5705ffe1d22e43fb1f6261", + "sha256:585c0869f75577ac7a8ff38d08f7aac9033da2c41c11352ebf86a04652758b7a", + "sha256:5d467ce9c5d35b3bcc7172c06320dddb275fea6ac2037f72f0a4d7472035cea9", + "sha256:63dbc21efd7e822c11d5ddbedbbb08cd11a41e0032e382a0fd59b0b08e405a3a", + "sha256:7bc1b221e7867f2e7ff1933165c0cec7153dce93d0cdba6554b42a8beb687bdb", + "sha256:8620ce80f50d023d414183bf90cc2576c2837b88e00bea3f33ad2630133bbb60", + "sha256:8a0ebda56ebca1a83eb2d1ac266649b80af8dd4b4a3502b2c1e09ac2f88fe128", + "sha256:90ed0e36455a81b25b7034038e40880189169c308a3df360861ad74da7b68c1a", + "sha256:95e67224815ef86924fbc2b71a9dbd1f7262384bca4bc4793645794ac4200717", + "sha256:afdb34b715daf814d1abea0317b6d672476b498472f1e5aacbadc34ebbc26e89", + "sha256:b4b2c63cc7963aedd08a5f5a454c9f67251b1ac9e22fd9d72836206c42dc2a72", + "sha256:d068f55bda3c2c3fcaec24bd083d9e2eede32c583faf084d6e4b9daaea77dde8", + "sha256:d5b3c4b7edd2e770375a01139be11307f04341ec709cf724e0f26ebb1eef12c3", + "sha256:deadf4df349d1dcd7b2853a2c8796593cc346600726eff680ed8ed11812382a7", + "sha256:df533af6f88080419c5a604d0d63b2c33b1c0c4409aba7d0cb6de305147ea8c8", + "sha256:e4aa948eb15018a657702fee0b9db47e908491c64d36b4a90f59a64741516e77", + "sha256:e5d842c73e4ef6ed8c1bd77806bf84a7cb535f9c0cf9b2c74d02ebda310070e1", + "sha256:ebec08091a22c2be870890913bdadd86fcd8e9f0f22bcb398abd3af914690c15", + "sha256:edc15fcfd77395e24543be48871c251f38132bb834d9fdfdad756adb6ea37679", + "sha256:f2b74784ed7e0bc2d02bd53e48ad6ba523c9b36c194260b7a5045071abbb1012", + "sha256:fa071559f14bd1e92077b1b5f6c22cf09756c6de7139370249eb372854ce51e6", + "sha256:fd52e796fee7171c4361d441796b64df1acfceb51f29e545e812f16d023c4bbc", + "sha256:fe976a0f1ef09b3638778024ab9fb8cde3118f203364212c198f71341c0715ca" ], - "version": "==4.4.2" + "version": "==4.5.0" }, "mako": { "hashes": [ @@ -211,13 +207,16 @@ "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", @@ -234,7 +233,9 @@ "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", + "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" ], "version": "==1.1.1" }, @@ -281,26 +282,26 @@ }, "protobuf": { "hashes": [ - "sha256:0329e86a397db2a83f9dcbe21d9be55a47f963cdabc893c3a24f4d3a8f117c37", - "sha256:0a7219254afec0d488211f3d482d8ed57e80ae735394e584a98d8f30a8c88a36", - "sha256:14d6ac53df9cb5bb87c4f91b677c1bc5cec9c0fd44327f367a3c9562de2877c4", - "sha256:180fc364b42907a1d2afa183ccbeffafe659378c236b1ec3daca524950bb918d", - "sha256:3d7a7d8d20b4e7a8f63f62de2d192cfd8b7a53c56caba7ece95367ca2b80c574", - "sha256:3f509f7e50d806a434fe4a5fbf602516002a0f092889209fff7db82060efffc0", - "sha256:4571da974019849201fc1ec6626b9cea54bd11b6bed140f8f737c0a33ea37de5", - "sha256:56bd1d84fbf4505c7b73f04de987eef5682e5752c811141b0186a3809bfb396f", - "sha256:680c668d00b5eff08b86aef9e5ba9a705e621ea05d39071cfea8e28cb2400946", - "sha256:6b5b947dc8b3f2aec0eaad65b0b5113fcd642c358c31357c647da6281ee31104", - "sha256:6e96dffaf4d0a9a329e528b353ba62fd9ef13599688723d96bc9c165d0b6871e", - "sha256:919f0d6f6addc836d08658eba3b52be2e92fd3e76da3ce00c325d8e9826d17c7", - "sha256:9c7b19c30cf0644afd0e4218b13f637ce54382fdcb1c8f75bf3e84e49a5f6d0a", - "sha256:a2e6f57114933882ec701807f217df2fb4588d47f71f227c0a163446b930d507", - "sha256:a6b970a2eccfcbabe1acf230fbf112face1c4700036c95e195f3554d7bcb04c1", - "sha256:bc45641cbcdea068b67438244c926f9fd3e5cbdd824448a4a64370610df7c593", - "sha256:d61b14a9090da77fe87e38ba4c6c43d3533dcbeb5d84f5474e7ac63c532dcc9c", - "sha256:d6faf5dbefb593e127463f58076b62fcfe0784187be8fe1aa9167388f24a22a1" + "sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab", + "sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f", + "sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a", + "sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0", + "sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4", + "sha256:7774bbbaac81d3ba86de646c39f154afc8156717972bf0450c9dbfa1dc8dbea2", + "sha256:82d7ac987715d8d1eb4068bf997f3053468e0ce0287e2729c30601feb6602fee", + "sha256:8eb9c93798b904f141d9de36a0ba9f9b73cc382869e67c9e642c0aba53b0fc07", + "sha256:adf0e4d57b33881d0c63bb11e7f9038f98ee0c3e334c221f0858f826e8fb0151", + "sha256:c40973a0aee65422d8cb4e7d7cbded95dfeee0199caab54d5ab25b63bce8135a", + "sha256:c77c974d1dadf246d789f6dad1c24426137c9091e930dbf50e0a29c1fcf00b1f", + "sha256:dd9aa4401c36785ea1b6fff0552c674bdd1b641319cb07ed1fe2392388e9b0d7", + "sha256:e11df1ac6905e81b815ab6fd518e79be0a58b5dc427a2cf7208980f30694b956", + "sha256:e2f8a75261c26b2f5f3442b0525d50fd79a71aeca04b5ec270fc123536188306", + "sha256:e512b7f3a4dd780f59f1bf22c302740e27b10b5c97e858a6061772668cd6f961", + "sha256:ef2c2e56aaf9ee914d3dccc3408d42661aaf7d9bb78eaa8f17b2e6282f214481", + "sha256:fac513a9dc2a74b99abd2e17109b53945e364649ca03d9f7a0b96aa8d1807d0a", + "sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80" ], - "version": "==3.11.2" + "version": "==3.11.3" }, "pycparser": { "hashes": [ @@ -334,6 +335,38 @@ ], "version": "==1.3.0" }, + "pyproj": { + "hashes": [ + "sha256:0608ac0aed84dcf57c859df87ac315b9acce18268f62bafc04071b7b1ff1c5a9", + "sha256:18265fb755e01df1d2248f1e837d81da4c9625e8f09481d64a9d6282c96f7467", + "sha256:190540946bb6fbfce285f46c08fcfd9d03e9331a0e952a3ef2047e6b8e8d8125", + "sha256:1da7f86d3b5e80ba3dabfd2c904a41bb6997ad9b55b47a934035492eaa0f331e", + "sha256:2ebbaee33e076664058effc3f6c943ed4c19a45df3989203ac081fca4a4722e3", + "sha256:32168c57450a1e6310b7ca331983d62d88393cc3e93b866fd6ea63dac30c7d3b", + "sha256:34b8ccf42032d89ebb8e0a839ae91e943ed222dab9bf3c1373f6fb972f8bcac4", + "sha256:432b4d28030635fac72713610aad2ed7424a7f07746fa1aa620c89761eb5e7a4", + "sha256:55103aa0adf25d207efd6f7f36d79dadee7706f22c1791955cc52033b40071e3", + "sha256:6bc74337edc1239f8c59d0d5b18a7996670b8fd523712d2dac599d5b792feae2", + "sha256:6d2838bec2d9ccd31dba68c76e8e7504bf819a4d4ace86adfca1e009d8f30f19", + "sha256:763ccac4398889cb798668824d34c4135f2e84a50681465a4199554aa1bd8611", + "sha256:8dbf1633ad2abdae6f73fe8989700c74a12dc82cb8597e66af28ff3d990d9c45", + "sha256:8ddffa4bcd9008c963840e8e79f2f3124f85f18d5987d4bbd9e7f38d9839a985", + "sha256:8f225c6186b0cd2cb07fe377786425a2ddc4183ae438fe63c60b4a879c91620f", + "sha256:97844a87cac739e389d1d0c69bc3b36c1d5c50c9f91443ef68bdef8fdf007f02", + "sha256:9d7a13def19a91836a2c84e5c7fcb6dd5e2c9bb205fb75ee102ffba24d80bf32", + "sha256:abd0784a017eedb3b03cd13f51b8852f4c68aa07affbee549bbd421f9b4268bb", + "sha256:acf150ca1506fcdaa52b0570f2903216413a2a4da78dfdf5ff7ee4eb92c2f8d5", + "sha256:b41522f8b77b64553280fb93823555bc8afb2469f77b8ce0e9aeed39abb50adc", + "sha256:c1058da6c02152d8637bb739dca940c6ab72683e59db6065fdcbe9102f66ca46", + "sha256:c70e713748c9c9d4a9d7bc42e1c71a17b1fc9b75b686b408a04eaf4909ead365", + "sha256:d47caa0a89dcb39ecd405e3899e07b69d8eaa6dbf267621087a4a5328da8492a", + "sha256:ed186edb4b610ed1e5589f3ba964d61da33d0bc54e89b8cbf8751da2e18555b3", + "sha256:f2dc8c2128f20ee9ed571783ce4730b181476083c403514714e15000b8b470cf", + "sha256:fba87f98344474da6df19bbfde4ca31c7d98a007069c8ef78cb27189f4bc7f04" + ], + "index": "pypi", + "version": "==2.4.2.post1" + }, "pyyaml": { "hashes": [ "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6", @@ -366,13 +399,6 @@ ], "version": "==1.4.3" }, - "aspy.yaml": { - "hashes": [ - "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc", - "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45" - ], - "version": "==1.3.0" - }, "attrs": { "hashes": [ "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", @@ -390,10 +416,10 @@ }, "cfgv": { "hashes": [ - "sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144", - "sha256:fbd93c9ab0a523bf7daec408f3be2ed99a980e20b2d19b50fc184ca6b820d289" + "sha256:04b093b14ddf9fd4d17c53ebfd55582d27b76ed30050193c14e560770c5360eb", + "sha256:f22b426ed59cd2ab2b54ff96608d846c33dfb8766a67f0b4a6ce130ce244414f" ], - "version": "==2.0.1" + "version": "==3.0.0" }, "click": { "hashes": [ @@ -402,6 +428,12 @@ ], "version": "==7.0" }, + "distlib": { + "hashes": [ + "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21" + ], + "version": "==0.3.0" + }, "entrypoints": { "hashes": [ "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", @@ -409,6 +441,13 @@ ], "version": "==0.3" }, + "filelock": { + "hashes": [ + "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", + "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836" + ], + "version": "==3.0.12" + }, "flake8": { "hashes": [ "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", @@ -419,115 +458,115 @@ }, "grpcio": { "hashes": [ - "sha256:066630f6b62bffa291dacbee56994279a6a3682b8a11967e9ccaf3cc770fc11e", - "sha256:07e95762ca6b18afbeb3aa2793e827c841152d5e507089b1db0b18304edda105", - "sha256:0a0fb2f8e3a13537106bc77e4c63005bc60124a6203034304d9101921afa4e90", - "sha256:0c61b74dcfb302613926e785cb3542a0905b9a3a86e9410d8cf5d25e25e10104", - "sha256:13383bd70618da03684a8aafbdd9e3d9a6720bf8c07b85d0bc697afed599d8f0", - "sha256:1c6e0f6b9d091e3717e9a58d631c8bb4898be3b261c2a01fe46371fdc271052f", - "sha256:1cf710c04689daa5cc1e598efba00b028215700dcc1bf66fcb7b4f64f2ea5d5f", - "sha256:2da5cee9faf17bb8daf500cd0d28a17ae881ab5500f070a6aace457f4c08cac4", - "sha256:2f78ebf340eaf28fa09aba0f836a8b869af1716078dfe8f3b3f6ff785d8f2b0f", - "sha256:33a07a1a8e817d733588dbd18e567caad1a6fe0d440c165619866cd490c7911a", - "sha256:3d090c66af9c065b7228b07c3416f93173e9839b1d40bb0ce3dd2aa783645026", - "sha256:42b903a3596a10e2a3727bae2a76f8aefd324d498424b843cfa9606847faea7b", - "sha256:4fffbb58134c4f23e5a8312ac3412db6f5e39e961dc0eb5e3115ce5aa16bf927", - "sha256:57be5a6c509a406fe0ffa6f8b86904314c77b5e2791be8123368ad2ebccec874", - "sha256:5b0fa09efb33e2af4e8822b4eb8b2cbc201d562e3e185c439be7eaeee2e8b8aa", - "sha256:5ef42dfc18f9a63a06aca938770b69470bb322e4c137cf08cf21703d1ef4ae5c", - "sha256:6a43d2f2ff8250f200fdf7aa31fa191a997922aa9ea1182453acd705ad83ab72", - "sha256:6d8ab28559be98b02f8b3a154b53239df1aa5b0d28ff865ae5be4f30e7ed4d3f", - "sha256:6e47866b7dc14ca3a12d40c1d6082e7bea964670f1c5315ea0fb8b0550244d64", - "sha256:6edda1b96541187f73aab11800d25f18ee87e53d5f96bb74473873072bf28a0e", - "sha256:7109c8738a8a3c98cfb5dda1c45642a8d6d35dc00d257ab7a175099b2b4daecd", - "sha256:8d866aafb08657c456a18c4a31c8526ea62de42427c242b58210b9eae6c64559", - "sha256:9939727d9ae01690b24a2b159ac9dbca7b7e8e6edd5af6a6eb709243cae7b52b", - "sha256:99fd873699df17cb11c542553270ae2b32c169986e475df0d68a8629b8ef4df7", - "sha256:b6fda5674f990e15e1bcaacf026428cf50bce36e708ddcbd1de9673b14aab760", - "sha256:bdb2f3dcb664f0c39ef1312cd6acf6bc6375252e4420cf8f36fff4cb4fa55c71", - "sha256:bfd7d3130683a1a0a50c456273c21ec8a604f2d043b241a55235a78a0090ee06", - "sha256:c6c2db348ac73d73afe14e0833b18abbbe920969bf2c5c03c0922719f8020d06", - "sha256:cb7a4b41b5e2611f85c3402ac364f1d689f5d7ecbc24a55ef010eedcd6cf460f", - "sha256:cd3d3e328f20f7c807a862620c6ee748e8d57ba2a8fc960d48337ed71c6d9d32", - "sha256:d1a481777952e4f99b8a6956581f3ee866d7614100d70ae6d7e07327570b85ce", - "sha256:d1d49720ed636920bb3d74cedf549382caa9ad55aea89d1de99d817068d896b2", - "sha256:d42433f0086cccd192114343473d7dbd4aae9141794f939e2b7b83efc57543db", - "sha256:d44c34463a7c481e076f691d8fa25d080c3486978c2c41dca09a8dd75296c2d7", - "sha256:d7e5b7af1350e9c8c17a7baf99d575fbd2de69f7f0b0e6ebd47b57506de6493a", - "sha256:d9542366a0917b9b48bab1fee481ac01f56bdffc52437b598c09e7840148a6a9", - "sha256:df7cdfb40179acc9790a462c049e0b8e109481164dd7ad1a388dd67ff1528759", - "sha256:e1a9d9d2e7224d981aea8da79260c7f6932bf31ce1f99b7ccfa5eceeb30dc5d0", - "sha256:ed10e5fad105ecb0b12822f924e62d0deb07f46683a0b64416b17fd143daba1d", - "sha256:f0ec5371ce2363b03531ed522bfbe691ec940f51f0e111f0500fc0f44518c69d", - "sha256:f6580a8a4f5e701289b45fd62a8f6cb5ec41e4d77082424f8b676806dcd22564", - "sha256:f7b83e4b2842d44fce3cdc0d54db7a7e0d169a598751bf393601efaa401c83e0", - "sha256:ffec45b0db18a555fdfe0c6fa2d0a3fceb751b22b31e8fcd14ceed7bde05481e" + "sha256:02aef8ef1a5ac5f0836b543e462eb421df6048a7974211a906148053b8055ea6", + "sha256:07f82aefb4a56c7e1e52b78afb77d446847d27120a838a1a0489260182096045", + "sha256:1cff47297ee614e7ef66243dc34a776883ab6da9ca129ea114a802c5e58af5c1", + "sha256:1ec8fc865d8da6d0713e2092a27eee344cd54628b2c2065a0e77fff94df4ae00", + "sha256:1ef949b15a1f5f30651532a9b54edf3bd7c0b699a10931505fa2c80b2d395942", + "sha256:209927e65395feb449783943d62a3036982f871d7f4045fadb90b2d82b153ea8", + "sha256:25c77692ea8c0929d4ad400ea9c3dcbcc4936cee84e437e0ef80da58fa73d88a", + "sha256:28f27c64dd699b8b10f70da5f9320c1cffcaefca7dd76275b44571bd097f276c", + "sha256:355bd7d7ce5ff2917d217f0e8ddac568cb7403e1ce1639b35a924db7d13a39b6", + "sha256:4a0a33ada3f6f94f855f92460896ef08c798dcc5f17d9364d1735c5adc9d7e4a", + "sha256:4d3b6e66f32528bf43ca2297caca768280a8e068820b1c3dca0fcf9f03c7d6f1", + "sha256:5121fa96c79fc0ec81825091d0be5c16865f834f41b31da40b08ee60552f9961", + "sha256:57949756a3ce1f096fa2b00f812755f5ab2effeccedb19feeb7d0deafa3d1de7", + "sha256:586d931736912865c9790c60ca2db29e8dc4eace160d5a79fec3e58df79a9386", + "sha256:5ae532b93cf9ce5a2a549b74a2c35e3b690b171ece9358519b3039c7b84c887e", + "sha256:5dab393ab96b2ce4012823b2f2ed4ee907150424d2f02b97bd6f8dd8f17cc866", + "sha256:5ebc13451246de82f130e8ee7e723e8d7ae1827f14b7b0218867667b1b12c88d", + "sha256:68a149a0482d0bc697aac702ec6efb9d380e0afebf9484db5b7e634146528371", + "sha256:6db7ded10b82592c472eeeba34b9f12d7b0ab1e2dcad12f081b08ebdea78d7d6", + "sha256:6e545908bcc2ae28e5b190ce3170f92d0438cf26a82b269611390114de0106eb", + "sha256:6f328a3faaf81a2546a3022b3dfc137cc6d50d81082dbc0c94d1678943f05df3", + "sha256:706e2dea3de33b0d8884c4d35ecd5911b4ff04d0697c4138096666ce983671a6", + "sha256:80c3d1ce8820dd819d1c9d6b63b6f445148480a831173b572a9174a55e7abd47", + "sha256:8111b61eee12d7af5c58f82f2c97c2664677a05df9225ef5cbc2f25398c8c454", + "sha256:9713578f187fb1c4d00ac554fe1edcc6b3ddd62f5d4eb578b81261115802df8e", + "sha256:9c0669ba9aebad540fb05a33beb7e659ea6e5ca35833fc5229c20f057db760e8", + "sha256:9e9cfe55dc7ac2aa47e0fd3285ff829685f96803197042c9d2f0fb44e4b39b2c", + "sha256:a22daaf30037b8e59d6968c76fe0f7ff062c976c7a026e92fbefc4c4bf3fc5a4", + "sha256:a25b84e10018875a0f294a7649d07c43e8bc3e6a821714e39e5cd607a36386d7", + "sha256:a71138366d57901597bfcc52af7f076ab61c046f409c7b429011cd68de8f9fe6", + "sha256:b4efde5524579a9ce0459ca35a57a48ca878a4973514b8bb88cb80d7c9d34c85", + "sha256:b78af4d42985ab3143d9882d0006f48d12f1bc4ba88e78f23762777c3ee64571", + "sha256:bb2987eb3af9bcf46019be39b82c120c3d35639a95bc4ee2d08f36ecdf469345", + "sha256:c03ce53690fe492845e14f4ab7e67d5a429a06db99b226b5c7caa23081c1e2bb", + "sha256:c59b9280284b791377b3524c8e39ca7b74ae2881ba1a6c51b36f4f1bb94cee49", + "sha256:d18b4c8cacbb141979bb44355ee5813dd4d307e9d79b3a36d66eca7e0a203df8", + "sha256:d1e5563e3b7f844dbc48d709c9e4a75647e11d0387cc1fa0c861d3e9d34bc844", + "sha256:d22c897b65b1408509099f1c3334bd3704f5e4eb7c0486c57d0e212f71cb8f54", + "sha256:dbec0a3a154dbf2eb85b38abaddf24964fa1c059ee0a4ad55d6f39211b1a4bca", + "sha256:ed123037896a8db6709b8ad5acc0ed435453726ea0b63361d12de369624c2ab5", + "sha256:f3614dabd2cc8741850597b418bcf644d4f60e73615906c3acc407b78ff720b3", + "sha256:f9d632ce9fd485119c968ec6a7a343de698c5e014d17602ae2f110f1b05925ed", + "sha256:fb62996c61eeff56b59ab8abfcaa0859ec2223392c03d6085048b576b567459b" ], - "version": "==1.26.0" + "version": "==1.27.2" }, "grpcio-tools": { "hashes": [ - "sha256:0286f704e55e3012fec3910400fe1a4ed11aeb66d3ec4b7f8041845af7fb7206", - "sha256:033a4e80dc78d9c11860800bd5a66b65ff385be8f669e96b02e795364c860597", - "sha256:0e3b5469912430f19407ebe14cfd1bece1b5a277c4d43e1b65dbff19d9475ccc", - "sha256:131aa8c3862a555819428856f872ab9e919e351d7cd60c98012e12d2fb6afc45", - "sha256:1783b8fa74f58a643e7780112fc4eb6110789672e852a691fad6af6b94a90c4a", - "sha256:1e80f74854bd1c7263942e836d69f95ffc66bb45bf14bf3e1ab61113271b5884", - "sha256:27ae784acff3d2fa04e3b4dc72f8d60a55d654f90e410adf08f46a4d2d673dd3", - "sha256:33c6bee5a02408018dc10a5737818d2159f14cbb0613df41cc93ba6cbaeea095", - "sha256:376a1840d1f5d25e9c3391557d6b3eeb3de17be697b0e55d8247d0262fcbaacf", - "sha256:3922dffd8160d54dc00c7d32b30776a974cc098086493c668faffac19e752087", - "sha256:4ba7e5afc93b413bbb5f3dd65ba583e078ff5895a5053d825ab793cf7720ae96", - "sha256:4e9a1276f8699d06518cec8caceb2c423fc7f971765cab7550d39f281795fd81", - "sha256:51ac9c4f8a542cd20c6776fde781c84c0acd8faba55ec14f121c6b4eb4245e89", - "sha256:5580b86cf49936c9c74f0def44d3582a7a1bb720eba8a14805c3a61efa790c70", - "sha256:58a879208bd84d6819a61c1b0618655574ef9df1d63a0e2f434fdcb5cfa1fb57", - "sha256:675918f83fa35bd54f4c29d95d8652c6215d5e95a13b6f14e626cdef6d0fce79", - "sha256:68259fd06188951d152665ffe44f9660edd715c102ae4bc4216eca4c4666dadf", - "sha256:6cea124cbd9081a587e1954b98e9a27c7cca6ae72babc3046ab6b439a5730679", - "sha256:6f356a445ba7afc634b1046d9f51d3ae37afbf4fe1a500285aca37677462a7b9", - "sha256:7f7430434bd997584f2136a675559ba0d4afdf7cb71d9bbc429b0cc831e6828c", - "sha256:809d60f15a32c21dc221ddb591aff8adfdde4e05095414eb8e015cdfef361615", - "sha256:826c19f26b41e99691e77823ad67f04dc0b69e514212907695e330c6f106415c", - "sha256:96c6f657b93f49243d083840d27a5a686a1fc26044a80ebf8585734d5152d4ee", - "sha256:9a2091371298f04ef350f776365945537d0befa95bad5623d80c4207bdff9d3a", - "sha256:9af72b764b41ba939e8e0a7ae9ec8a17d1c46a18797c6342cba6483f29e1790f", - "sha256:a209002e3d4787f0e90e29f15cddbe83dc9054238c0da7f539c913002a348cc1", - "sha256:a908d5af2f26673e970c7c03703437bf95d10e88dad3322e7e267467db44a04d", - "sha256:ab841c69581085b6f9aa54044a13db6ec31183513f7cce0862d29c9b7b4e3c64", - "sha256:b1bc78efefb8e085c072add2c02326fdecad9b8644b3be11e715ea4c6102ad87", - "sha256:b97e74ffe121dfa9ae7ec94393fce4e95e9e0a343827663e989dc4b7c918d1a5", - "sha256:bba8d3b61ec113bb94596599d2568217b22ddfc7baa46c00dec5106cfd4e914b", - "sha256:bfe0e33aea60da100b214c72c1746cc0194bb8da910004518c185041cc795543", - "sha256:c15f0718cbc3986e747d5b0734198dce0ac07d188ec5e063b1e9889ac947f86e", - "sha256:c56d0ac769bf1f01dbb6ec6b6492849e70cd35bdeeb660e206a70ab43917ae92", - "sha256:d396fdb7026986e6d3897bb207cc7d5bc536a82a2e50af806a24b3d254c73bc3", - "sha256:d62ab00dea7fa0813fc813a6c848da2eeda5cb71893b892a229d23949de0cecd", - "sha256:da75e33e185c8be17a82ec4a97f5c75ec05d57e85f8b285f86e2a22484849e4a", - "sha256:dcbd1fbb540638c9ad9c3a071b392b654f79666a2bc12808080b0e9f674b9a80", - "sha256:e7e90bad5466347a3648358e9f437e72d5f6d6025fe741171a88aca8b9d864df", - "sha256:eae371a663ceeef8f930323a120a9d11e13e1c49903a66ddb4ada4830d5bcb7d", - "sha256:f290cccc972533a288c2ebc55eb3c0fbe0c6a0d0a9775cb34ce6bfb11fe14a11", - "sha256:facb8c588cdd6adc51ae7545f59283565dae8d946c6163e578b70ab6bf161215", - "sha256:fb043e45f91634776acdfe4b8dfc96b636c53a458799179041ab633e15c3d833" + "sha256:00c5080cfb197ed20ecf0d0ff2d07f1fc9c42c724cad21c40ff2d048de5712b1", + "sha256:069826dd02ce1886444cf4519c4fe1b05ac9ef41491f26e97400640531db47f6", + "sha256:1266b577abe7c720fd16a83d0a4999a192e87c4a98fc9f97e0b99b106b3e155f", + "sha256:16dc3fad04fe18d50777c56af7b2d9b9984cd1cfc71184646eb431196d1645c6", + "sha256:1de5a273eaffeb3d126a63345e9e848ea7db740762f700eb8b5d84c5e3e7687d", + "sha256:2ca280af2cae1a014a238057bd3c0a254527569a6a9169a01c07f0590081d530", + "sha256:43a1573400527a23e4174d88604fde7a9d9a69bf9473c21936b7f409858f8ebb", + "sha256:4698c6b6a57f73b14d91a542c69ff33a2da8729691b7060a5d7f6383624d045e", + "sha256:520b7dafddd0f82cb7e4f6e9c6ba1049aa804d0e207870def9fe7f94d1e14090", + "sha256:57f8b9e2c7f55cd45f6dd930d6de61deb42d3eb7f9788137fbc7155cf724132a", + "sha256:59fbeb5bb9a7b94eb61642ac2cee1db5233b8094ca76fc56d4e0c6c20b5dd85f", + "sha256:5fd7efc2fd3370bd2c72dc58f31a407a5dff5498befa145da211b2e8c6a52c63", + "sha256:6016c07d6566e3109a3c032cf3861902d66501ecc08a5a84c47e43027302f367", + "sha256:627c91923df75091d8c4d244af38d5ab7ed8d786d480751d6c2b9267fbb92fe0", + "sha256:69c4a63919b9007e845d9f8980becd2f89d808a4a431ca32b9723ee37b521cb1", + "sha256:77e25c241e33b75612f2aa62985f746c6f6803ec4e452da508bb7f8d90a69db4", + "sha256:7a2d5fb558ac153a326e742ebfd7020eb781c43d3ffd920abd42b2e6c6fdfb37", + "sha256:7b54b283ec83190680903a9037376dc915e1f03852a2d574ba4d981b7a1fd3d0", + "sha256:845a51305af9fc7f9e2078edaec9a759153195f6cf1fbb12b1fa6f077e56b260", + "sha256:84724458c86ff9b14c29b49e321f34d80445b379f4cd4d0494c694b49b1d6f88", + "sha256:87e8ca2c2d2d3e09b2a2bed5d740d7b3e64028dafb7d6be543b77eec85590736", + "sha256:8e7738a4b93842bca1158cde81a3587c9b7111823e40a1ddf73292ca9d58e08b", + "sha256:915a695bc112517af48126ee0ecdb6aff05ed33f3eeef28f0d076f1f6b52ef5e", + "sha256:99961156a36aae4a402d6b14c1e7efde642794b3ddbf32c51db0cb3a199e8b11", + "sha256:9ba88c2d99bcaf7b9cb720925e3290d73b2367d238c5779363fd5598b2dc98c7", + "sha256:a140bf853edb2b5e8692fe94869e3e34077d7599170c113d07a58286c604f4fe", + "sha256:a14dc7a36c845991d908a7179502ca47bcba5ae1817c4426ce68cf2c97b20ad9", + "sha256:a3d2aec4b09c8e59fee8b0d1ed668d09e8c48b738f03f5d8401d7eb409111c47", + "sha256:a8f892378b0b02526635b806f59141abbb429d19bec56e869e04f396502c9651", + "sha256:aaa5ae26883c3d58d1a4323981f96b941fa09bb8f0f368d97c6225585280cf04", + "sha256:b56caecc16307b088a431a4038c3b3bb7d0e7f9988cbd0e9fa04ac937455ea38", + "sha256:bd7f59ff1252a3db8a143b13ea1c1e93d4b8cf4b852eb48b22ef1e6942f62a84", + "sha256:c1bb8f47d58e9f7c4825abfe01e6b85eda53c8b31d2267ca4cddf3c4d0829b80", + "sha256:d1a5e5fa47ba9557a7d3b31605631805adc66cdba9d95b5d10dfc52cca1fed53", + "sha256:dcbc06556f3713a9348c4fce02d05d91e678fc320fb2bcf0ddf8e4bb11d17867", + "sha256:e17b2e0936b04ced99769e26111e1e86ba81619d1b2691b1364f795e45560953", + "sha256:e6932518db389ede8bf06b4119bbd3e17f42d4626e72dec2b8955b20ec732cb6", + "sha256:ea4b3ad696d976d5eac74ec8df9a2c692113e455446ee38d5b3bd87f8e034fa6", + "sha256:ee50b0cf0d28748ef9f941894eb50fc464bd61b8e96aaf80c5056bea9b80d580", + "sha256:ef624b6134aef737b3daa4fb7e806cb8c5749efecd0b1fa9ce4f7e060c7a0221", + "sha256:f5450aa904e720f9c6407b59e96a8951ed6a95463f49444b6d2594b067d39588", + "sha256:f8514453411d72cc3cf7d481f2b6057e5b7436736d0cd39ee2b2f72088bbf497", + "sha256:fae91f30dc050a8d0b32d20dc700e6092f0bd2138d83e9570fff3f0372c1b27e" ], "index": "pypi", - "version": "==1.26.0" + "version": "==1.27.2" }, "identify": { "hashes": [ - "sha256:418f3b2313ac0b531139311a6b426854e9cbdfcfb6175447a5039aa6291d8b30", - "sha256:8ad99ed1f3a965612dcb881435bf58abcfbeb05e230bb8c352b51e8eac103360" + "sha256:1222b648251bdcb8deb240b294f450fbf704c7984e08baa92507e4ea10b436d5", + "sha256:d824ebe21f38325c771c41b08a95a761db1982f1fc0eee37c6c97df3f1636b96" ], - "version": "==1.4.10" + "version": "==1.4.11" }, "importlib-metadata": { "hashes": [ - "sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359", - "sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8" + "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302", + "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b" ], "markers": "python_version < '3.8'", - "version": "==1.4.0" + "version": "==1.5.0" }, "importlib-resources": { "hashes": [ @@ -554,24 +593,24 @@ }, "mock": { "hashes": [ - "sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3", - "sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8" + "sha256:2a572b715f09dd2f0a583d8aeb5bb67d7ed7a8fd31d193cf1227a99c16a67bc3", + "sha256:5e48d216809f6f393987ed56920305d8f3c647e6ed35407c1ff2ecb88a9e1151" ], "index": "pypi", - "version": "==3.0.5" + "version": "==4.0.1" }, "more-itertools": { "hashes": [ - "sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39", - "sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288" + "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", + "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507" ], - "version": "==8.1.0" + "version": "==8.2.0" }, "nodeenv": { "hashes": [ - "sha256:561057acd4ae3809e665a9aaaf214afff110bbb6a6d5c8a96121aea6878408b3" + "sha256:5b2438f2e42af54ca968dd1b374d14a1194848955187b0e5e4be1f73813a5212" ], - "version": "==1.3.4" + "version": "==1.3.5" }, "packaging": { "hashes": [ @@ -589,34 +628,34 @@ }, "pre-commit": { "hashes": [ - "sha256:8f48d8637bdae6fa70cc97db9c1dd5aa7c5c8bf71968932a380628c25978b850", - "sha256:f92a359477f3252452ae2e8d3029de77aec59415c16ae4189bcfba40b757e029" + "sha256:5295fb6d652a6c5e0b4636cd2c73183efdf253d45b657ce7367183134e806fe1", + "sha256:5387b53bb84ad9abc9b0845775dddd4e3243fd64cdcddaa6db28d3da6fbf06c2" ], "index": "pypi", - "version": "==1.21.0" + "version": "==2.1.0" }, "protobuf": { "hashes": [ - "sha256:0329e86a397db2a83f9dcbe21d9be55a47f963cdabc893c3a24f4d3a8f117c37", - "sha256:0a7219254afec0d488211f3d482d8ed57e80ae735394e584a98d8f30a8c88a36", - "sha256:14d6ac53df9cb5bb87c4f91b677c1bc5cec9c0fd44327f367a3c9562de2877c4", - "sha256:180fc364b42907a1d2afa183ccbeffafe659378c236b1ec3daca524950bb918d", - "sha256:3d7a7d8d20b4e7a8f63f62de2d192cfd8b7a53c56caba7ece95367ca2b80c574", - "sha256:3f509f7e50d806a434fe4a5fbf602516002a0f092889209fff7db82060efffc0", - "sha256:4571da974019849201fc1ec6626b9cea54bd11b6bed140f8f737c0a33ea37de5", - "sha256:56bd1d84fbf4505c7b73f04de987eef5682e5752c811141b0186a3809bfb396f", - "sha256:680c668d00b5eff08b86aef9e5ba9a705e621ea05d39071cfea8e28cb2400946", - "sha256:6b5b947dc8b3f2aec0eaad65b0b5113fcd642c358c31357c647da6281ee31104", - "sha256:6e96dffaf4d0a9a329e528b353ba62fd9ef13599688723d96bc9c165d0b6871e", - "sha256:919f0d6f6addc836d08658eba3b52be2e92fd3e76da3ce00c325d8e9826d17c7", - "sha256:9c7b19c30cf0644afd0e4218b13f637ce54382fdcb1c8f75bf3e84e49a5f6d0a", - "sha256:a2e6f57114933882ec701807f217df2fb4588d47f71f227c0a163446b930d507", - "sha256:a6b970a2eccfcbabe1acf230fbf112face1c4700036c95e195f3554d7bcb04c1", - "sha256:bc45641cbcdea068b67438244c926f9fd3e5cbdd824448a4a64370610df7c593", - "sha256:d61b14a9090da77fe87e38ba4c6c43d3533dcbeb5d84f5474e7ac63c532dcc9c", - "sha256:d6faf5dbefb593e127463f58076b62fcfe0784187be8fe1aa9167388f24a22a1" + "sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab", + "sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f", + "sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a", + "sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0", + "sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4", + "sha256:7774bbbaac81d3ba86de646c39f154afc8156717972bf0450c9dbfa1dc8dbea2", + "sha256:82d7ac987715d8d1eb4068bf997f3053468e0ce0287e2729c30601feb6602fee", + "sha256:8eb9c93798b904f141d9de36a0ba9f9b73cc382869e67c9e642c0aba53b0fc07", + "sha256:adf0e4d57b33881d0c63bb11e7f9038f98ee0c3e334c221f0858f826e8fb0151", + "sha256:c40973a0aee65422d8cb4e7d7cbded95dfeee0199caab54d5ab25b63bce8135a", + "sha256:c77c974d1dadf246d789f6dad1c24426137c9091e930dbf50e0a29c1fcf00b1f", + "sha256:dd9aa4401c36785ea1b6fff0552c674bdd1b641319cb07ed1fe2392388e9b0d7", + "sha256:e11df1ac6905e81b815ab6fd518e79be0a58b5dc427a2cf7208980f30694b956", + "sha256:e2f8a75261c26b2f5f3442b0525d50fd79a71aeca04b5ec270fc123536188306", + "sha256:e512b7f3a4dd780f59f1bf22c302740e27b10b5c97e858a6061772668cd6f961", + "sha256:ef2c2e56aaf9ee914d3dccc3408d42661aaf7d9bb78eaa8f17b2e6282f214481", + "sha256:fac513a9dc2a74b99abd2e17109b53945e364649ca03d9f7a0b96aa8d1807d0a", + "sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80" ], - "version": "==3.11.2" + "version": "==3.11.3" }, "py": { "hashes": [ @@ -648,11 +687,11 @@ }, "pytest": { "hashes": [ - "sha256:1d122e8be54d1a709e56f82e2d85dcba3018313d64647f38a91aec88c239b600", - "sha256:c13d1943c63e599b98cf118fcb9703e4d7bde7caa9a432567bcdcae4bf512d20" + "sha256:0d5fe9189a148acc3c3eb2ac8e1ac0742cb7618c084f3d228baaec0c254b318d", + "sha256:ff615c761e25eb25df19edddc0b970302d2a9091fbce0e7213298d85fb61fef6" ], "index": "pypi", - "version": "==5.3.4" + "version": "==5.3.5" }, "pyyaml": { "hashes": [ @@ -686,10 +725,10 @@ }, "virtualenv": { "hashes": [ - "sha256:0d62c70883c0342d59c11d0ddac0d954d0431321a41ab20851facf2b222598f3", - "sha256:55059a7a676e4e19498f1aad09b8313a38fcc0cdbe4fdddc0e9b06946d21b4bb" + "sha256:531b142e300d405bb9faedad4adbeb82b4098b918e35209af2adef3129274aae", + "sha256:5dd42a9f56307542bddc446cfd10ef6576f11910366a07609fe8d0d88fa8fb7e" ], - "version": "==16.7.9" + "version": "==20.0.5" }, "wcwidth": { "hashes": [ @@ -700,10 +739,10 @@ }, "zipp": { "hashes": [ - "sha256:b338014b9bc7102ca69e0fb96ed07215a8954d2989bc5d83658494ab2ba634af", - "sha256:e013e7800f60ec4dde789ebf4e9f7a54236e4bbf5df2a1a4e20ce9e1d9609d67" + "sha256:12248a63bbdf7548f89cb4c7cda4681e537031eda29c02ea29674bc6854460c2", + "sha256:7c0f8e91abc0dc07a5068f315c52cb30c66bfbc581e5b50704c8a2f6ebae794a" ], - "version": "==2.0.1" + "version": "==3.0.0" } } } diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index d28776a1..2da9acfa 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -1128,7 +1128,6 @@ class CoreHandler(socketserver.BaseRequestHandler): self.session.location.refgeo, self.session.location.refscale, ) - logging.info("location configured: UTM%s", self.session.location.refutm) def handle_config_metadata(self, message_type, config_data): replies = [] diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index ebb74509..2d91bab3 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -37,8 +37,8 @@ from core.emulator.emudata import ( from core.emulator.enumerations import EventTypes, ExceptionLevels, LinkTypes, NodeTypes from core.emulator.sessionconfig import SessionConfig from core.errors import CoreError -from core.location.corelocation import CoreLocation from core.location.event import EventLoop +from core.location.geo import GeoLocation from core.location.mobility import BasicRangeModel, MobilityManager from core.nodes.base import CoreNetworkBase, CoreNode, CoreNodeBase, NodeBase from core.nodes.docker import DockerNode @@ -146,7 +146,7 @@ class Session: self.distributed = DistributedController(self) # initialize session feature helpers - self.location = CoreLocation() + self.location = GeoLocation() self.mobility = MobilityManager(session=self) self.services = CoreServices(session=self) self.emane = EmaneManager(session=self) diff --git a/daemon/core/location/geo.py b/daemon/core/location/geo.py new file mode 100644 index 00000000..939858bc --- /dev/null +++ b/daemon/core/location/geo.py @@ -0,0 +1,119 @@ +""" +Provides conversions from x,y,z to lon,lat,alt. +""" + +import logging +from typing import Tuple + +import pyproj + +from core.emulator.enumerations import RegisterTlvs + +SCALE_FACTOR = 100.0 + + +class GeoLocation: + """ + Provides logic to convert x,y,z coordinates to lon,lat,alt using + defined projections. + """ + + name = "location" + config_type = RegisterTlvs.UTILITY.value + + def __init__(self) -> None: + """ + Creates a GeoLocation instance. + """ + self.projection = pyproj.Proj("epsg:3857") + self.refproj = (0.0, 0.0) + self.refgeo = (0.0, 0.0, 0.0) + self.refxyz = (0.0, 0.0, 0.0) + self.refscale = 1.0 + + def setrefgeo(self, lat: float, lon: float, alt: float) -> None: + """ + Set the geospatial reference point. + + :param lat: latitude reference + :param lon: longitude reference + :param alt: altitude reference + :return: nothing + """ + self.refgeo = (lat, lon, alt) + px, py = self.projection(lon, lat) + self.refproj = (px, py, alt) + + def reset(self) -> None: + """ + Reset reference data to default values. + + :return: nothing + """ + self.refxyz = (0.0, 0.0, 0.0) + self.refgeo = (0.0, 0.0, 0.0) + self.refscale = 1.0 + self.refproj = self.projection(self.refgeo[0], self.refgeo[1]) + + def pixels2meters(self, value: float) -> float: + """ + Provides conversion from pixels to meters. + + :param value: pixels value + :return: pixels value in meters + """ + return (value / SCALE_FACTOR) * self.refscale + + def meters2pixels(self, value: float) -> float: + """ + Provides conversion from meters to pixels. + + :param value: meters value + :return: meters value in pixels + """ + if self.refscale == 0.0: + return 0.0 + return SCALE_FACTOR * (value / self.refscale) + + def getxyz(self, lat: float, lon: float, alt: float) -> Tuple[float, float, float]: + """ + Convert provided lon,lat,alt to x,y,z. + + :param lat: latitude value + :param lon: longitude value + :param alt: altitude value + :return: x,y,z representation of provided values + """ + logging.debug("input lon,lat,alt(%s, %s, %s)", lon, lat, alt) + px, py = self.projection(lon, lat) + px -= self.refproj[0] + py -= self.refproj[1] + pz = alt - self.refproj[2] + x = self.meters2pixels(px) + self.refxyz[0] + y = -(self.meters2pixels(py) + self.refxyz[1]) + z = self.meters2pixels(pz) + self.refxyz[2] + logging.debug("result x,y,z(%s, %s, %s)", x, y, z) + return x, y, z + + def getgeo(self, x: float, y: float, z: float) -> Tuple[float, float, float]: + """ + Convert provided x,y,z to lon,lat,alt. + + :param x: x value + :param y: y value + :param z: z value + :return: lat,lon,alt representation of provided values + """ + logging.debug("input x,y(%s, %s)", x, y) + x -= self.refxyz[0] + y = -(y - self.refxyz[1]) + if z is None: + z = self.refxyz[2] + else: + z -= self.refxyz[2] + px = self.refproj[0] + self.pixels2meters(x) + py = self.refproj[1] + self.pixels2meters(y) + lon, lat = self.projection(px, py, inverse=True) + alt = self.refgeo[2] + self.pixels2meters(z) + logging.debug("result lon,lat(%s, %s)", lon, lat) + return lat, lon, alt diff --git a/daemon/setup.py.in b/daemon/setup.py.in index bdef71ab..c4d2ae56 100644 --- a/daemon/setup.py.in +++ b/daemon/setup.py.in @@ -42,6 +42,7 @@ setup( "mako", "pillow", "protobuf", + "pyproj", "pyyaml", ], tests_require=[ From a3c7ed8012daf4f4aea841c53d9118367214148f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 21 Feb 2020 16:42:23 -0800 Subject: [PATCH 0035/1131] update emaneevent logging to debug, fixed emaneevent thread stop logic, fixed node data conversion for lon,lat,alt values --- daemon/core/api/tlv/dataconversion.py | 6 +++--- daemon/core/emane/emanemanager.py | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/daemon/core/api/tlv/dataconversion.py b/daemon/core/api/tlv/dataconversion.py index 8228b536..8d47613d 100644 --- a/daemon/core/api/tlv/dataconversion.py +++ b/daemon/core/api/tlv/dataconversion.py @@ -31,9 +31,9 @@ def convert_node(node_data): (NodeTlvs.CANVAS, node_data.canvas), (NodeTlvs.NETWORK_ID, node_data.network_id), (NodeTlvs.SERVICES, node_data.services), - (NodeTlvs.LATITUDE, node_data.latitude), - (NodeTlvs.LONGITUDE, node_data.longitude), - (NodeTlvs.ALTITUDE, node_data.altitude), + (NodeTlvs.LATITUDE, str(node_data.latitude)), + (NodeTlvs.LONGITUDE, str(node_data.longitude)), + (NodeTlvs.ALTITUDE, str(node_data.altitude)), (NodeTlvs.ICON, node_data.icon), (NodeTlvs.OPAQUE, node_data.opaque), ], diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 9a7b3a0d..0eb3f9d4 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -698,8 +698,6 @@ class EmaneManager(ModelManager): self.initeventservice(shutdown=True) if self.eventmonthread is not None: - # TODO: fix this - self.eventmonthread._Thread__stop() self.eventmonthread.join() self.eventmonthread = None @@ -773,7 +771,7 @@ class EmaneManager(ModelManager): x = int(x) y = int(y) z = int(z) - logging.info( + logging.debug( "location event NEM %s (%s, %s, %s) -> (%s, %s, %s)", nemid, lat, From afb0fe8b464de6d355fc1098b59b2ff4c7e035e8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 21 Feb 2020 17:17:09 -0800 Subject: [PATCH 0036/1131] avoid sending sdt 2 updates for emane location event, avoid not using lon,lat,alt if any value is 0 --- daemon/core/plugins/sdt.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 0f605100..e5a5a545 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -100,16 +100,9 @@ class Sdt: lat = node_data.latitude lon = node_data.longitude alt = node_data.altitude - - if all([lat, lon, alt]): - self.updatenodegeo( - node_data.id, - node_data.latitude, - node_data.longitude, - node_data.altitude, - ) - - if node_data.message_type == 0: + if all([lat is not None, lon is not None, alt is not None]): + self.updatenodegeo(node_data.id, lat, lon, alt) + elif node_data.message_type == 0: # TODO: z is not currently supported by node messages self.updatenode(node_data.id, 0, x, y, 0) From ddaba7c477045cc47a690cede56da07e2cd9fd24 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 24 Feb 2020 10:58:01 -0800 Subject: [PATCH 0037/1131] remove code for deleting wireless links and nodes during runtime --- daemon/core/gui/graph/node.py | 43 ++--------------------------------- daemon/core/gui/nodeutils.py | 4 ---- 2 files changed, 2 insertions(+), 45 deletions(-) diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 7b4ccf31..3ed5b1d9 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -17,7 +17,7 @@ from core.gui.errors import show_grpc_error from core.gui.graph import tags from core.gui.graph.tooltip import CanvasTooltip from core.gui.images import ImageEnum, Images -from core.gui.nodeutils import ANTENNA_SIZE, EdgeUtils, NodeUtils +from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils if TYPE_CHECKING: from core.gui.app import Application @@ -66,48 +66,9 @@ class CanvasNode: def delete(self): logging.debug("Delete canvas node for %s", self.core_node) - - # if node is wlan, EMANE type, remove any existing wireless links between nodes connetect to this node - if NodeUtils.is_wireless_node(self.core_node.type): - nodes = [] - for edge in self.edges: - token = edge.token - if self.id == token[0]: - nodes.append(token[1]) - else: - nodes.append(token[0]) - for i in range(len(nodes)): - for j in range(i + 1, len(nodes)): - token = EdgeUtils.get_token(nodes[i], nodes[j]) - wireless_edge = self.canvas.wireless_edges.pop(token, None) - if wireless_edge: - - self.canvas.nodes[nodes[i]].wireless_edges.remove(wireless_edge) - self.canvas.nodes[nodes[j]].wireless_edges.remove(wireless_edge) - self.canvas.delete(wireless_edge.id) - else: - logging.debug("%s is not a wireless edge", token) - # if node is MDR, remove wireless links to other MDRs - elif NodeUtils.is_mdr_node(self.core_node.type, self.core_node.model): - for wireless_edge in self.wireless_edges: - token = wireless_edge.token - other = token[0] - if other == self.id: - other = token[1] - self.canvas.nodes[other].wireless_edges.discard(wireless_edge) - try: - wlan_edge = self.canvas.wireless_edges.pop(token) - self.canvas.delete(wlan_edge.id) - except KeyError: - logging.error( - "wireless link not found, potentially multiple wireless link issue" - ) - self.delete_antennas() - - self.wireless_edges.clear() - self.canvas.delete(self.id) self.canvas.delete(self.text_id) + self.delete_antennas() def add_antenna(self): x, y = self.canvas.coords(self.id) diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index 870ac8bf..81aa2cba 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -90,10 +90,6 @@ class NodeUtils: def is_rj45_node(cls, node_type: NodeType) -> bool: return node_type in cls.RJ45_NODES - @classmethod - def is_mdr_node(cls, node_type: NodeType, model: str) -> bool: - return cls.is_container_node(node_type) and model == "mdr" - @classmethod def node_icon( cls, From 1dca477e6df5b1af996c2e73e51e1e51ba5ab615 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 24 Feb 2020 11:17:06 -0800 Subject: [PATCH 0038/1131] disable delete, copy, paste during runtime --- daemon/core/gui/coreclient.py | 3 +++ daemon/core/gui/graph/graph.py | 9 ++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 7d8e832c..89ce7ba0 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -1064,3 +1064,6 @@ class CoreClient: def service_been_modified(self, node_id: int) -> bool: return node_id in self.modified_service_nodes + + def is_runtime_state(self): + return self.state == core_pb2.SessionState.RUNTIME diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 7905ed8c..d07039d4 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -656,8 +656,11 @@ class CanvasGraph(tk.Canvas): delete selected nodes and any data that relates to it """ logging.debug("press delete key") - nodes = self.delete_selection_objects() - self.core.delete_graph_nodes(nodes) + if not self.app.core.is_runtime_state(): + nodes = self.delete_selection_objects() + self.core.delete_graph_nodes(nodes) + else: + logging.debug("node deletion is disabled during runtime") def double_click(self, event: tk.Event): selected = self.get_selected(event) @@ -850,7 +853,7 @@ class CanvasGraph(tk.Canvas): self.core.create_link(edge, source, dest) def copy(self): - if self.selection: + if self.selection and not self.app.core.is_runtime_state(): logging.debug("to copy %s nodes", len(self.selection)) self.to_copy = self.selection.keys() From 7a50f6ac25981ef89f5e2b2345e985b8f9276f6a Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 24 Feb 2020 11:24:59 -0800 Subject: [PATCH 0039/1131] replace hasattr with getattr for cleaner code --- daemon/core/gui/task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daemon/core/gui/task.py b/daemon/core/gui/task.py index 2863326f..eb6655f8 100644 --- a/daemon/core/gui/task.py +++ b/daemon/core/gui/task.py @@ -22,8 +22,8 @@ class BackgroundTask: result = self.task(*self.args) logging.info("task completed") # if start session fails, a response with Result: False and a list of exceptions is returned - if hasattr(result, "result") and not result.result: - if hasattr(result, "exceptions") and len(result.exceptions) > 0: + if not getattr(result, "result", True): + if len(getattr(result, "exceptions", [])) > 0: self.master.after( 0, show_grpc_response_exceptions, From 8a0257d130c6e393aeca8c215bb68e1500c86b8d Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 24 Feb 2020 12:51:47 -0800 Subject: [PATCH 0040/1131] disable copy/paste/delete shortcuts as well as commands during runtime state --- daemon/core/gui/coreclient.py | 3 --- daemon/core/gui/graph/graph.py | 12 +++++++++--- daemon/core/gui/menubar.py | 14 ++++++++++++++ daemon/core/gui/toolbar.py | 2 ++ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 89ce7ba0..7d8e832c 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -1064,6 +1064,3 @@ class CoreClient: def service_been_modified(self, node_id: int) -> bool: return node_id in self.modified_service_nodes - - def is_runtime_state(self): - return self.state == core_pb2.SessionState.RUNTIME diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index d07039d4..5652fa40 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -656,11 +656,11 @@ class CanvasGraph(tk.Canvas): delete selected nodes and any data that relates to it """ logging.debug("press delete key") - if not self.app.core.is_runtime_state(): + if not self.app.core.is_runtime(): nodes = self.delete_selection_objects() self.core.delete_graph_nodes(nodes) else: - logging.debug("node deletion is disabled during runtime") + logging.info("node deletion is disabled during runtime state") def double_click(self, event: tk.Event): selected = self.get_selected(event) @@ -853,11 +853,17 @@ class CanvasGraph(tk.Canvas): self.core.create_link(edge, source, dest) def copy(self): - if self.selection and not self.app.core.is_runtime_state(): + if self.app.core.is_runtime(): + logging.info("copy is disabled during runtime state") + return + if self.selection: logging.debug("to copy %s nodes", len(self.selection)) self.to_copy = self.selection.keys() def paste(self): + if self.app.core.is_runtime(): + logging.info("paste is disabled during runtime state") + return # maps original node canvas id to copy node canvas id copy_map = {} # the edges that will be copy over diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 935e0b92..f4c12014 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -25,6 +25,7 @@ class Menubar(tk.Menu): self.app = app self.menuaction = action.MenuAction(app, master) self.recent_menu = None + self.edit_menu = None self.draw() def draw(self): @@ -110,6 +111,7 @@ class Menubar(tk.Menu): self.app.master.bind_all("", self.menuaction.copy) self.app.master.bind_all("", self.menuaction.paste) + self.edit_menu = menu def draw_canvas_menu(self): """ @@ -439,3 +441,15 @@ class Menubar(tk.Menu): self.app.core.save_xml(xml_file) else: self.menuaction.file_save_as_xml() + + def change_menubar_item_state(self, is_runtime: bool): + for i in range(self.edit_menu.index("end")): + try: + label_name = self.edit_menu.entrycget(i, "label") + if label_name in ["Copy", "Paste"]: + if is_runtime: + self.edit_menu.entryconfig(i, state="disabled") + else: + self.edit_menu.entryconfig(i, state="normal") + except tk.TclError: + logging.debug("Ignore separators") diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index eff37257..3b4828d0 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -280,6 +280,7 @@ class Toolbar(ttk.Frame): server. """ self.app.canvas.hide_context() + self.app.menubar.change_menubar_item_state(is_runtime=True) self.app.statusbar.progress_bar.start(5) self.app.canvas.mode = GraphMode.SELECT self.time = time.perf_counter() @@ -469,6 +470,7 @@ class Toolbar(ttk.Frame): """ logging.info("Click stop button") self.app.canvas.hide_context() + self.app.menubar.change_menubar_item_state(is_runtime=False) self.app.statusbar.progress_bar.start(5) self.time = time.perf_counter() task = BackgroundTask(self, self.app.core.stop_session, self.stop_callback) From 177f27372e0c5443b93ba9e52edefd9b32cd2348 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 25 Feb 2020 11:30:26 -0800 Subject: [PATCH 0041/1131] fixed wrong variable used for configuring service in grpcutils --- daemon/core/api/grpc/grpcutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index b9cf33ef..94cfce56 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -377,7 +377,7 @@ def service_configuration(session: Session, config: core_pb2.ServiceConfig) -> N session.services.set_service(config.node_id, config.service) service = session.services.get_service(config.node_id, config.service) if config.files: - service.files = tuple(config.files) + service.configs = tuple(config.files) if config.directories: service.directories = tuple(config.directories) if config.startup: From 014707580f9e8b08fbb3f58e756098a39b2f7859 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 25 Feb 2020 11:38:58 -0800 Subject: [PATCH 0042/1131] allow custom service file to be created --- daemon/core/gui/coreclient.py | 6 ++- daemon/core/gui/dialogs/serviceconfig.py | 47 +++++++++++++++--------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 7d8e832c..10e0820d 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -521,7 +521,6 @@ class CoreClient: logging.info( "start session(%s), result: %s", self.session_id, response.result ) - if response.result: self.set_metadata() except grpc.RpcError as e: @@ -620,6 +619,7 @@ class CoreClient: self, node_id: int, service_name: str, + files: List[str], startups: List[str], validations: List[str], shutdowns: List[str], @@ -628,14 +628,16 @@ class CoreClient: self.session_id, node_id, service_name, + files=files, startup=startups, validate=validations, shutdown=shutdowns, ) logging.info( - "Set %s service for node(%s), Startup: %s, Validation: %s, Shutdown: %s, Result: %s", + "Set %s service for node(%s), files: %s, Startup: %s, Validation: %s, Shutdown: %s, Result: %s", service_name, node_id, + files, startups, validations, shutdowns, diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index b528e32a..5a2fede3 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -1,6 +1,7 @@ """ Service configuration dialog """ +import logging import tkinter as tk from tkinter import ttk from typing import TYPE_CHECKING, Any, List @@ -155,15 +156,15 @@ class ServiceConfigDialog(Dialog): frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="File Name") label.grid(row=0, column=0, padx=PADX, sticky="w") - self.filename_combobox = ttk.Combobox( - frame, values=self.filenames, state="readonly" - ) + self.filename_combobox = ttk.Combobox(frame, values=self.filenames) self.filename_combobox.bind( "<>", self.display_service_file_data ) self.filename_combobox.grid(row=0, column=1, sticky="ew", padx=PADX) - button = ttk.Button(frame, image=self.documentnew_img, state="disabled") - button.bind("", self.add_filename) + button = ttk.Button( + frame, image=self.documentnew_img, command=self.add_filename + ) + # button.bind("", self.add_filename) button.grid(row=0, column=2, padx=PADX) button = ttk.Button(frame, image=self.editdelete_img, state="disabled") button.bind("", self.delete_filename) @@ -358,14 +359,16 @@ class ServiceConfigDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=3, sticky="ew") - def add_filename(self, event: tk.Event): - # not worry about it for now - return - frame_contains_button = event.widget.master - combobox = frame_contains_button.grid_slaves(row=0, column=1)[0] - filename = combobox.get() - if filename not in combobox["values"]: - combobox["values"] += (filename,) + def add_filename(self): + filename = self.filename_combobox.get() + if filename not in self.filename_combobox["values"]: + self.filename_combobox["values"] += (filename,) + self.filename_combobox.set(filename) + self.temp_service_files[filename] = self.service_file_data.text.get( + 1.0, "end" + ) + else: + logging.debug("file already existed") def delete_filename(self, event: tk.Event): # not worry about it for now @@ -411,7 +414,11 @@ class ServiceConfigDialog(Dialog): def click_apply(self): current_listbox = self.master.current.listbox - if not self.is_custom_service_config() and not self.is_custom_service_file(): + if ( + not self.is_custom_service_config() + and not self.is_custom_service_file() + and not self.has_new_files() + ): if self.node_id in self.service_configs: self.service_configs[self.node_id].pop(self.service_name, None) current_listbox.itemconfig(current_listbox.curselection()[0], bg="") @@ -419,13 +426,14 @@ class ServiceConfigDialog(Dialog): return try: - if self.is_custom_service_config(): + if self.is_custom_service_config() or self.has_new_files(): startup_commands = self.startup_commands_listbox.get(0, "end") shutdown_commands = self.shutdown_commands_listbox.get(0, "end") validate_commands = self.validate_commands_listbox.get(0, "end") config = self.core.set_node_service( self.node_id, self.service_name, + files=list(self.filename_combobox["values"]), startups=startup_commands, validations=validate_commands, shutdowns=shutdown_commands, @@ -433,7 +441,6 @@ class ServiceConfigDialog(Dialog): if self.node_id not in self.service_configs: self.service_configs[self.node_id] = {} self.service_configs[self.node_id][self.service_name] = config - for file in self.modified_files: if self.node_id not in self.file_configs: self.file_configs[self.node_id] = {} @@ -442,7 +449,6 @@ class ServiceConfigDialog(Dialog): self.file_configs[self.node_id][self.service_name][ file ] = self.temp_service_files[file] - self.app.core.set_node_service_file( self.node_id, self.service_name, file, self.temp_service_files[file] ) @@ -462,7 +468,9 @@ class ServiceConfigDialog(Dialog): scrolledtext = event.widget filename = self.filename_combobox.get() self.temp_service_files[filename] = scrolledtext.get(1.0, "end") - if self.temp_service_files[filename] != self.original_service_files[filename]: + if self.temp_service_files[filename] != self.original_service_files.get( + filename, "" + ): self.modified_files.add(filename) else: self.modified_files.discard(filename) @@ -477,6 +485,9 @@ class ServiceConfigDialog(Dialog): or set(self.default_shutdown) != set(shutdown_commands) ) + def has_new_files(self): + return set(self.filenames) != set(self.filename_combobox["values"]) + def is_custom_service_file(self): return len(self.modified_files) > 0 From 32efc75c64beafa0dffb81875d619f5917386837 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 25 Feb 2020 20:40:51 -0800 Subject: [PATCH 0043/1131] removed legacy location translation --- daemon/core/location/corelocation.py | 279 --------------------------- daemon/core/location/utm.py | 259 ------------------------- 2 files changed, 538 deletions(-) delete mode 100644 daemon/core/location/corelocation.py delete mode 100644 daemon/core/location/utm.py diff --git a/daemon/core/location/corelocation.py b/daemon/core/location/corelocation.py deleted file mode 100644 index 6eb7d16d..00000000 --- a/daemon/core/location/corelocation.py +++ /dev/null @@ -1,279 +0,0 @@ -""" -location.py: definition of CoreLocation class that is a member of the -Session object. Provides conversions between Cartesian and geographic coordinate -systems. Depends on utm contributed module, from -https://pypi.python.org/pypi/utm (version 0.3.0). -""" - -import logging -from typing import Optional, Tuple - -from core.emulator.enumerations import RegisterTlvs -from core.location import utm - - -class CoreLocation: - """ - Member of session class for handling global location data. This keeps - track of a latitude/longitude/altitude reference point and scale in - order to convert between X,Y and geo coordinates. - """ - - name = "location" - config_type = RegisterTlvs.UTILITY.value - - def __init__(self) -> None: - """ - Creates a MobilityManager instance. - - :return: nothing - """ - # ConfigurableManager.__init__(self) - self.reset() - self.zonemap = {} - self.refxyz = (0.0, 0.0, 0.0) - self.refscale = 1.0 - self.zoneshifts = {} - self.refgeo = (0.0, 0.0, 0.0) - for n, l in utm.ZONE_LETTERS: - self.zonemap[l] = n - - def reset(self) -> None: - """ - Reset to initial state. - """ - # (x, y, z) coordinates of the point given by self.refgeo - self.refxyz = (0.0, 0.0, 0.0) - # decimal latitude, longitude, and altitude at the point (x, y, z) - self.setrefgeo(0.0, 0.0, 0.0) - # 100 pixels equals this many meters - self.refscale = 1.0 - # cached distance to refpt in other zones - self.zoneshifts = {} - - def px2m(self, val: float) -> float: - """ - Convert the specified value in pixels to meters using the - configured scale. The scale is given as s, where - 100 pixels = s meters. - - :param val: value to use in converting to meters - :return: value converted to meters - """ - return (val / 100.0) * self.refscale - - def m2px(self, val: float) -> float: - """ - Convert the specified value in meters to pixels using the - configured scale. The scale is given as s, where - 100 pixels = s meters. - - :param val: value to convert to pixels - :return: value converted to pixels - """ - if self.refscale == 0.0: - return 0.0 - return 100.0 * (val / self.refscale) - - def setrefgeo(self, lat: float, lon: float, alt: float) -> None: - """ - Record the geographical reference point decimal (lat, lon, alt) - and convert and store its UTM equivalent for later use. - - :param lat: latitude - :param lon: longitude - :param alt: altitude - :return: nothing - """ - self.refgeo = (lat, lon, alt) - # easting, northing, zone - e, n, zonen, zonel = utm.from_latlon(lat, lon) - self.refutm = ((zonen, zonel), e, n, alt) - - def getgeo(self, x: float, y: float, z: float) -> Tuple[float, float, float]: - """ - Given (x, y, z) Cartesian coordinates, convert them to latitude, - longitude, and altitude based on the configured reference point - and scale. - - :param x: x value - :param y: y value - :param z: z value - :return: lat, lon, alt values for provided coordinates - """ - # shift (x,y,z) over to reference point (x,y,z) - x -= self.refxyz[0] - y = -(y - self.refxyz[1]) - if z is None: - z = self.refxyz[2] - else: - z -= self.refxyz[2] - # use UTM coordinates since unit is meters - zone = self.refutm[0] - if zone == "": - raise ValueError("reference point not configured") - e = self.refutm[1] + self.px2m(x) - n = self.refutm[2] + self.px2m(y) - alt = self.refutm[3] + self.px2m(z) - (e, n, zone) = self.getutmzoneshift(e, n) - try: - lat, lon = utm.to_latlon(e, n, zone[0], zone[1]) - except utm.OutOfRangeError: - logging.exception( - "UTM out of range error for n=%s zone=%s xyz=(%s,%s,%s)", - n, - zone, - x, - y, - z, - ) - lat, lon = self.refgeo[:2] - return lat, lon, alt - - def getxyz(self, lat: float, lon: float, alt: float) -> Tuple[float, float, float]: - """ - Given latitude, longitude, and altitude location data, convert them - to (x, y, z) Cartesian coordinates based on the configured - reference point and scale. Lat/lon is converted to UTM meter - coordinates, UTM zones are accounted for, and the scale turns - meters to pixels. - - :param lat: latitude - :param lon: longitude - :param alt: altitude - :return: converted x, y, z coordinates - """ - # convert lat/lon to UTM coordinates in meters - e, n, zonen, zonel = utm.from_latlon(lat, lon) - _rlat, _rlon, ralt = self.refgeo - xshift = self.geteastingshift(zonen, zonel) - if xshift is None: - xm = e - self.refutm[1] - else: - xm = e + xshift - yshift = self.getnorthingshift(zonen, zonel) - if yshift is None: - ym = n - self.refutm[2] - else: - ym = n + yshift - zm = alt - ralt - - # shift (x,y,z) over to reference point (x,y,z) - x = self.m2px(xm) + self.refxyz[0] - y = -(self.m2px(ym) + self.refxyz[1]) - z = self.m2px(zm) + self.refxyz[2] - return x, y, z - - def geteastingshift(self, zonen: float, zonel: float) -> Optional[float]: - """ - If the lat, lon coordinates being converted are located in a - different UTM zone than the canvas reference point, the UTM meters - may need to be shifted. - This picks a reference point in the same longitudinal band - (UTM zone number) as the provided zone, to calculate the shift in - meters for the x coordinate. - - :param zonen: zonen - :param zonel: zone1 - :return: the x shift value - """ - rzonen = int(self.refutm[0][0]) - # same zone number, no x shift required - if zonen == rzonen: - return None - z = (zonen, zonel) - # x shift already calculated, cached - if z in self.zoneshifts and self.zoneshifts[z][0] is not None: - return self.zoneshifts[z][0] - - rlat, rlon, _ralt = self.refgeo - # ea. zone is 6deg band - lon2 = rlon + 6 * (zonen - rzonen) - # ignore northing - e2, _n2, _zonen2, _zonel2 = utm.from_latlon(rlat, lon2) - # NOTE: great circle distance used here, not reference ellipsoid! - xshift = utm.haversine(rlon, rlat, lon2, rlat) - e2 - # cache the return value - yshift = None - if z in self.zoneshifts: - yshift = self.zoneshifts[z][1] - self.zoneshifts[z] = (xshift, yshift) - return xshift - - def getnorthingshift(self, zonen: float, zonel: float) -> Optional[float]: - """ - If the lat, lon coordinates being converted are located in a - different UTM zone than the canvas reference point, the UTM meters - may need to be shifted. - This picks a reference point in the same latitude band (UTM zone letter) - as the provided zone, to calculate the shift in meters for the - y coordinate. - - :param zonen: zonen - :param zonel: zone1 - :return: calculated y shift - """ - rzonel = self.refutm[0][1] - # same zone letter, no y shift required - if zonel == rzonel: - return None - z = (zonen, zonel) - # y shift already calculated, cached - if z in self.zoneshifts and self.zoneshifts[z][1] is not None: - return self.zoneshifts[z][1] - - rlat, rlon, _ralt = self.refgeo - # zonemap is used to calculate degrees difference between zone letters - latshift = self.zonemap[zonel] - self.zonemap[rzonel] - # ea. latitude band is 8deg high - lat2 = rlat + latshift - _e2, n2, _zonen2, _zonel2 = utm.from_latlon(lat2, rlon) - # NOTE: great circle distance used here, not reference ellipsoid - yshift = -(utm.haversine(rlon, rlat, rlon, lat2) + n2) - # cache the return value - xshift = None - if z in self.zoneshifts: - xshift = self.zoneshifts[z][0] - self.zoneshifts[z] = (xshift, yshift) - return yshift - - def getutmzoneshift( - self, e: float, n: float - ) -> Tuple[float, float, Tuple[float, str]]: - """ - Given UTM easting and northing values, check if they fall outside - the reference point's zone boundary. Return the UTM coordinates in a - different zone and the new zone if they do. Zone lettering is only - changed when the reference point is in the opposite hemisphere. - - :param e: easting value - :param n: northing value - :return: modified easting, northing, and zone values - """ - zone = self.refutm[0] - rlat, rlon, _ralt = self.refgeo - if e > 834000 or e < 166000: - num_zones = (int(e) - 166000) / (utm.R / 10) - # estimate number of zones to shift, E (positive) or W (negative) - rlon2 = self.refgeo[1] + (num_zones * 6) - _e2, _n2, zonen2, zonel2 = utm.from_latlon(rlat, rlon2) - xshift = utm.haversine(rlon, rlat, rlon2, rlat) - # after >3 zones away from refpt, the above estimate won't work - # (the above estimate could be improved) - if not 100000 <= (e - xshift) < 1000000: - # move one more zone away - num_zones = (abs(num_zones) + 1) * (abs(num_zones) / num_zones) - rlon2 = self.refgeo[1] + (num_zones * 6) - _e2, _n2, zonen2, zonel2 = utm.from_latlon(rlat, rlon2) - xshift = utm.haversine(rlon, rlat, rlon2, rlat) - e = e - xshift - zone = (zonen2, zonel2) - if n < 0: - # refpt in northern hemisphere and we crossed south of equator - n += 10000000 - zone = (zone[0], "M") - elif n > 10000000: - # refpt in southern hemisphere and we crossed north of equator - n -= 10000000 - zone = (zone[0], "N") - return e, n, zone diff --git a/daemon/core/location/utm.py b/daemon/core/location/utm.py deleted file mode 100644 index b80a7d6d..00000000 --- a/daemon/core/location/utm.py +++ /dev/null @@ -1,259 +0,0 @@ -""" -utm -=== - -.. image:: https://travis-ci.org/Turbo87/utm.png - -Bidirectional UTM-WGS84 converter for python - -Usage ------ - -:: - - import utm - -Convert a (latitude, longitude) tuple into an UTM coordinate:: - - utm.from_latlon(51.2, 7.5) - >>> (395201.3103811303, 5673135.241182375, 32, 'U') - -Convert an UTM coordinate into a (latitude, longitude) tuple:: - - utm.to_latlon(340000, 5710000, 32, 'U') - >>> (51.51852098408468, 6.693872395145327) - -Speed ------ - -The library has been compared to the more generic pyproj library by running the -unit test suite through pyproj instead of utm. These are the results: - -* with pyproj (without projection cache): 4.0 - 4.5 sec -* with pyproj (with projection cache): 0.9 - 1.0 sec -* with utm: 0.4 - 0.5 sec - -Authors -------- - -* Tobias Bieniek - -License -------- - -Copyright (C) 2012 Tobias Bieniek - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -import math - -__all__ = ['to_latlon', 'from_latlon'] - - -class OutOfRangeError(ValueError): - pass - - -K0 = 0.9996 - -E = 0.00669438 -E2 = E * E -E3 = E2 * E -E_P2 = E / (1.0 - E) - -SQRT_E = math.sqrt(1 - E) -_E = (1 - SQRT_E) / (1 + SQRT_E) -_E3 = _E * _E * _E -_E4 = _E3 * _E - -M1 = (1 - E / 4 - 3 * E2 / 64 - 5 * E3 / 256) -M2 = (3 * E / 8 + 3 * E2 / 32 + 45 * E3 / 1024) -M3 = (15 * E2 / 256 + 45 * E3 / 1024) -M4 = (35 * E3 / 3072) - -P2 = (3 * _E / 2 - 27 * _E3 / 32) -P3 = (21 * _E3 / 16 - 55 * _E4 / 32) -P4 = (151 * _E3 / 96) - -R = 6378137 - -ZONE_LETTERS = [ - (84, None), (72, 'X'), (64, 'W'), (56, 'V'), (48, 'U'), (40, 'T'), - (32, 'S'), (24, 'R'), (16, 'Q'), (8, 'P'), (0, 'N'), (-8, 'M'), (-16, 'L'), - (-24, 'K'), (-32, 'J'), (-40, 'H'), (-48, 'G'), (-56, 'F'), (-64, 'E'), - (-72, 'D'), (-80, 'C') -] - - -def to_latlon(easting, northing, zone_number, zone_letter): - zone_letter = zone_letter.upper() - - if not 100000 <= easting < 1000000: - raise OutOfRangeError('easting out of range (must be between 100.000 m and 999.999 m)') - if not 0 <= northing <= 10000000: - raise OutOfRangeError('northing out of range (must be between 0 m and 10.000.000 m)') - if not 1 <= zone_number <= 60: - raise OutOfRangeError('zone number out of range (must be between 1 and 60)') - if not 'C' <= zone_letter <= 'X' or zone_letter in ['I', 'O']: - raise OutOfRangeError('zone letter out of range (must be between C and X)') - - x = easting - 500000 - y = northing - - if zone_letter < 'N': - y -= 10000000 - - m = y / K0 - mu = m / (R * M1) - - p_rad = (mu + P2 * math.sin(2 * mu) + P3 * math.sin(4 * mu) + P4 * math.sin(6 * mu)) - - p_sin = math.sin(p_rad) - p_sin2 = p_sin * p_sin - - p_cos = math.cos(p_rad) - - p_tan = p_sin / p_cos - p_tan2 = p_tan * p_tan - p_tan4 = p_tan2 * p_tan2 - - ep_sin = 1 - E * p_sin2 - ep_sin_sqrt = math.sqrt(1 - E * p_sin2) - - n = R / ep_sin_sqrt - r = (1 - E) / ep_sin - - c = _E * p_cos ** 2 - c2 = c * c - - d = x / (n * K0) - d2 = d * d - d3 = d2 * d - d4 = d3 * d - d5 = d4 * d - d6 = d5 * d - - latitude = (p_rad - (p_tan / r) * - (d2 / 2 - - d4 / 24 * (5 + 3 * p_tan2 + 10 * c - 4 * c2 - 9 * E_P2)) + - d6 / 720 * (61 + 90 * p_tan2 + 298 * c + 45 * p_tan4 - 252 * E_P2 - 3 * c2)) - - longitude = (d - - d3 / 6 * (1 + 2 * p_tan2 + c) + - d5 / 120 * (5 - 2 * c + 28 * p_tan2 - 3 * c2 + 8 * E_P2 + 24 * p_tan4)) / p_cos - - return (math.degrees(latitude), - math.degrees(longitude) + zone_number_to_central_longitude(zone_number)) - - -def from_latlon(latitude, longitude): - if not -80.0 <= latitude <= 84.0: - raise OutOfRangeError('latitude out of range (must be between 80 deg S and 84 deg N)') - if not -180.0 <= longitude <= 180.0: - raise OutOfRangeError('northing out of range (must be between 180 deg W and 180 deg E)') - - lat_rad = math.radians(latitude) - lat_sin = math.sin(lat_rad) - lat_cos = math.cos(lat_rad) - - lat_tan = lat_sin / lat_cos - lat_tan2 = lat_tan * lat_tan - lat_tan4 = lat_tan2 * lat_tan2 - - lon_rad = math.radians(longitude) - - zone_number = latlon_to_zone_number(latitude, longitude) - central_lon = zone_number_to_central_longitude(zone_number) - central_lon_rad = math.radians(central_lon) - - zone_letter = latitude_to_zone_letter(latitude) - - n = R / math.sqrt(1 - E * lat_sin ** 2) - c = E_P2 * lat_cos ** 2 - - a = lat_cos * (lon_rad - central_lon_rad) - a2 = a * a - a3 = a2 * a - a4 = a3 * a - a5 = a4 * a - a6 = a5 * a - - m = R * (M1 * lat_rad - - M2 * math.sin(2 * lat_rad) + - M3 * math.sin(4 * lat_rad) - - M4 * math.sin(6 * lat_rad)) - - easting = K0 * n * (a + - a3 / 6 * (1 - lat_tan2 + c) + - a5 / 120 * (5 - 18 * lat_tan2 + lat_tan4 + 72 * c - 58 * E_P2)) + 500000 - - northing = K0 * (m + n * lat_tan * (a2 / 2 + - a4 / 24 * (5 - lat_tan2 + 9 * c + 4 * c ** 2) + - a6 / 720 * (61 - 58 * lat_tan2 + lat_tan4 + 600 * c - 330 * E_P2))) - - if latitude < 0: - northing += 10000000 - - return easting, northing, zone_number, zone_letter - - -def latitude_to_zone_letter(latitude): - for lat_min, zone_letter in ZONE_LETTERS: - if latitude >= lat_min: - return zone_letter - - return None - - -def latlon_to_zone_number(latitude, longitude): - if 56 <= latitude <= 64 and 3 <= longitude <= 12: - return 32 - - if 72 <= latitude <= 84 and longitude >= 0: - if longitude <= 9: - return 31 - elif longitude <= 21: - return 33 - elif longitude <= 33: - return 35 - elif longitude <= 42: - return 37 - - return int((longitude + 180) / 6) + 1 - - -def zone_number_to_central_longitude(zone_number): - return (zone_number - 1) * 6 - 180 + 3 - - -def haversine(lon1, lat1, lon2, lat2): - """ - Calculate the great circle distance between two points - on the earth (specified in decimal degrees) - """ - # convert decimal degrees to radians - lon1, lat1, lon2, lat2 = map(math.radians, [lon1, lat1, lon2, lat2]) - # haversine formula - dlon = lon2 - lon1 - dlat = lat2 - lat1 - a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2 - c = 2 * math.asin(math.sqrt(a)) - m = 6367000 * c - return m From b5b51794d8aa18a03ae757bd5e513a2fd6a5060b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 25 Feb 2020 21:26:41 -0800 Subject: [PATCH 0044/1131] update pyproj logic to use formal transformers, added altitude to conversion debug logging --- daemon/core/location/geo.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/daemon/core/location/geo.py b/daemon/core/location/geo.py index 939858bc..7d3aea22 100644 --- a/daemon/core/location/geo.py +++ b/daemon/core/location/geo.py @@ -10,6 +10,8 @@ import pyproj from core.emulator.enumerations import RegisterTlvs SCALE_FACTOR = 100.0 +CRS_WGS84 = 4326 +CRS_PROJ = 3857 class GeoLocation: @@ -25,7 +27,10 @@ class GeoLocation: """ Creates a GeoLocation instance. """ - self.projection = pyproj.Proj("epsg:3857") + self.to_pixels = pyproj.Transformer.from_crs( + CRS_WGS84, CRS_PROJ, always_xy=True + ) + self.to_geo = pyproj.Transformer.from_crs(CRS_PROJ, CRS_WGS84, always_xy=True) self.refproj = (0.0, 0.0) self.refgeo = (0.0, 0.0, 0.0) self.refxyz = (0.0, 0.0, 0.0) @@ -41,7 +46,7 @@ class GeoLocation: :return: nothing """ self.refgeo = (lat, lon, alt) - px, py = self.projection(lon, lat) + px, py = self.to_pixels.transform(lon, lat) self.refproj = (px, py, alt) def reset(self) -> None: @@ -53,7 +58,7 @@ class GeoLocation: self.refxyz = (0.0, 0.0, 0.0) self.refgeo = (0.0, 0.0, 0.0) self.refscale = 1.0 - self.refproj = self.projection(self.refgeo[0], self.refgeo[1]) + self.refproj = self.to_pixels.transform(self.refgeo[0], self.refgeo[1]) def pixels2meters(self, value: float) -> float: """ @@ -85,7 +90,7 @@ class GeoLocation: :return: x,y,z representation of provided values """ logging.debug("input lon,lat,alt(%s, %s, %s)", lon, lat, alt) - px, py = self.projection(lon, lat) + px, py = self.to_pixels.transform(lon, lat) px -= self.refproj[0] py -= self.refproj[1] pz = alt - self.refproj[2] @@ -113,7 +118,7 @@ class GeoLocation: z -= self.refxyz[2] px = self.refproj[0] + self.pixels2meters(x) py = self.refproj[1] + self.pixels2meters(y) - lon, lat = self.projection(px, py, inverse=True) + lon, lat = self.to_geo.transform(px, py) alt = self.refgeo[2] + self.pixels2meters(z) - logging.debug("result lon,lat(%s, %s)", lon, lat) + logging.debug("result lon,lat,alt(%s, %s, %s)", lon, lat, alt) return lat, lon, alt From 696fda00ea85fc3416bf2315faf9009aec927975 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 26 Feb 2020 08:31:28 -0800 Subject: [PATCH 0045/1131] add/delete custom service file to node --- daemon/core/gui/coreclient.py | 1 + daemon/core/gui/dialogs/serviceconfig.py | 73 +++++++++++++----------- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 10e0820d..bd4e4aad 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -935,6 +935,7 @@ class CoreClient: config_proto = core_pb2.ServiceConfig( node_id=node_id, service=name, + files=config.configs, startup=config.startup, validate=config.validate, shutdown=config.shutdown, diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index 5a2fede3..af5a9a37 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -49,8 +49,8 @@ class ServiceConfigDialog(Dialog): self.validation_mode = None self.validation_time = None self.validation_period = None - self.documentnew_img = Images.get(ImageEnum.DOCUMENTNEW, 16) - self.editdelete_img = Images.get(ImageEnum.EDITDELETE, 16) + self.documentnew_img = Images.get(ImageEnum.DOCUMENTNEW, 16 * app.app_scale) + self.editdelete_img = Images.get(ImageEnum.EDITDELETE, 16 * app.app_scale) self.notebook = None self.metadata_entry = None @@ -103,7 +103,7 @@ class ServiceConfigDialog(Dialog): x: self.app.core.get_node_service_file( self.node_id, self.service_name, x ) - for x in self.filenames + for x in default_config.configs } self.temp_service_files = dict(self.original_service_files) file_configs = self.file_configs @@ -164,10 +164,11 @@ class ServiceConfigDialog(Dialog): button = ttk.Button( frame, image=self.documentnew_img, command=self.add_filename ) - # button.bind("", self.add_filename) button.grid(row=0, column=2, padx=PADX) - button = ttk.Button(frame, image=self.editdelete_img, state="disabled") - button.bind("", self.delete_filename) + button = ttk.Button( + frame, image=self.editdelete_img, command=self.delete_filename + ) + # button.bind("", self.delete_filename) button.grid(row=0, column=3) frame = ttk.Frame(tab) @@ -370,17 +371,19 @@ class ServiceConfigDialog(Dialog): else: logging.debug("file already existed") - def delete_filename(self, event: tk.Event): - # not worry about it for now - return - frame_comntains_button = event.widget.master - combobox = frame_comntains_button.grid_slaves(row=0, column=1)[0] - filename = combobox.get() - if filename in combobox["values"]: - combobox["values"] = tuple([x for x in combobox["values"] if x != filename]) - combobox.set("") + def delete_filename(self): + cbb = self.filename_combobox + filename = cbb.get() + if filename in cbb["values"]: + cbb["values"] = tuple([x for x in cbb["values"] if x != filename]) + cbb.set("") + self.service_file_data.text.delete(1.0, "end") + self.temp_service_files.pop(filename, None) + if filename in self.modified_files: + self.modified_files.remove(filename) - def add_command(self, event: tk.Event): + @classmethod + def add_command(cls, event: tk.Event): frame_contains_button = event.widget.master listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox command_to_add = frame_contains_button.grid_slaves(row=0, column=0)[0].get() @@ -391,7 +394,8 @@ class ServiceConfigDialog(Dialog): return listbox.insert(tk.END, command_to_add) - def update_entry(self, event: tk.Event): + @classmethod + def update_entry(cls, event: tk.Event): listbox = event.widget current_selection = listbox.curselection() if len(current_selection) > 0: @@ -402,7 +406,8 @@ class ServiceConfigDialog(Dialog): entry.delete(0, "end") entry.insert(0, cmd) - def delete_command(self, event: tk.Event): + @classmethod + def delete_command(cls, event: tk.Event): button = event.widget frame_contains_button = button.master listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox @@ -415,7 +420,7 @@ class ServiceConfigDialog(Dialog): def click_apply(self): current_listbox = self.master.current.listbox if ( - not self.is_custom_service_config() + not self.is_custom_command() and not self.is_custom_service_file() and not self.has_new_files() ): @@ -426,17 +431,15 @@ class ServiceConfigDialog(Dialog): return try: - if self.is_custom_service_config() or self.has_new_files(): - startup_commands = self.startup_commands_listbox.get(0, "end") - shutdown_commands = self.shutdown_commands_listbox.get(0, "end") - validate_commands = self.validate_commands_listbox.get(0, "end") + if self.is_custom_command() or self.has_new_files(): + startup, validate, shutdown = self.get_commands() config = self.core.set_node_service( self.node_id, self.service_name, files=list(self.filename_combobox["values"]), - startups=startup_commands, - validations=validate_commands, - shutdowns=shutdown_commands, + startups=startup, + validations=validate, + shutdowns=shutdown, ) if self.node_id not in self.service_configs: self.service_configs[self.node_id] = {} @@ -475,14 +478,12 @@ class ServiceConfigDialog(Dialog): else: self.modified_files.discard(filename) - def is_custom_service_config(self): - startup_commands = self.startup_commands_listbox.get(0, "end") - shutdown_commands = self.shutdown_commands_listbox.get(0, "end") - validate_commands = self.validate_commands_listbox.get(0, "end") + def is_custom_command(self): + startup, validate, shutdown = self.get_commands() return ( - set(self.default_startup) != set(startup_commands) - or set(self.default_validate) != set(validate_commands) - or set(self.default_shutdown) != set(shutdown_commands) + set(self.default_startup) != set(startup) + or set(self.default_validate) != set(validate) + or set(self.default_shutdown) != set(shutdown) ) def has_new_files(self): @@ -520,3 +521,9 @@ class ServiceConfigDialog(Dialog): for cmd in to_add: commands.append(cmd) listbox.insert(tk.END, cmd) + + def get_commands(self): + startup = self.startup_commands_listbox.get(0, "end") + shutdown = self.shutdown_commands_listbox.get(0, "end") + validate = self.validate_commands_listbox.get(0, "end") + return startup, validate, shutdown From 764a61e89e8a61bc9017b29086eb5fc054f4dcb1 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 26 Feb 2020 10:43:01 -0800 Subject: [PATCH 0046/1131] create layout for service config - directory tab --- daemon/core/gui/dialogs/serviceconfig.py | 71 ++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index af5a9a37..7f0f12e1 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -2,8 +2,9 @@ Service configuration dialog """ import logging +import os import tkinter as tk -from tkinter import ttk +from tkinter import filedialog, ttk from typing import TYPE_CHECKING, Any, List import grpc @@ -49,12 +50,18 @@ class ServiceConfigDialog(Dialog): self.validation_mode = None self.validation_time = None self.validation_period = None - self.documentnew_img = Images.get(ImageEnum.DOCUMENTNEW, 16 * app.app_scale) - self.editdelete_img = Images.get(ImageEnum.EDITDELETE, 16 * app.app_scale) + self.directory_entry = None + self.default_directories = [] + self.temp_directories = [] + self.documentnew_img = Images.get( + ImageEnum.DOCUMENTNEW, int(16 * app.app_scale) + ) + self.editdelete_img = Images.get(ImageEnum.EDITDELETE, int(16 * app.app_scale)) self.notebook = None self.metadata_entry = None self.filename_combobox = None + self.dir_list = None self.startup_commands_listbox = None self.shutdown_commands_listbox = None self.validate_commands_listbox = None @@ -81,6 +88,7 @@ class ServiceConfigDialog(Dialog): self.default_startup = default_config.startup[:] self.default_validate = default_config.validate[:] self.default_shutdown = default_config.shutdown[:] + self.default_directories = default_config.dirs[:] custom_configs = self.service_configs if ( self.node_id in custom_configs @@ -99,6 +107,7 @@ class ServiceConfigDialog(Dialog): self.shutdown_commands = service_config.shutdown[:] self.validation_mode = service_config.validation_mode self.validation_time = service_config.validation_timer + self.temp_directories = service_config.dirs[:] self.original_service_files = { x: self.app.core.get_node_service_file( self.node_id, self.service_name, x @@ -231,7 +240,30 @@ class ServiceConfigDialog(Dialog): tab, text="Directories required by this service that are unique for each node.", ) - label.grid() + label.grid(row=0, column=0, sticky="ew") + frame = ttk.Frame(tab, padding=FRAME_PAD) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.grid(row=1, column=0, sticky="nsew") + var = tk.StringVar(value="") + self.directory_entry = ttk.Entry(frame, textvariable=var) + self.directory_entry.grid(row=0, column=0, sticky="ew") + button = ttk.Button(frame, text="...", command=self.find_directory_button) + button.grid(row=0, column=1, sticky="ew") + self.dir_list = ListboxScroll(tab) + self.dir_list.grid(row=2, column=0, sticky="nsew") + self.dir_list.listbox.bind("<>", self.directory_select) + for d in self.temp_directories: + self.dir_list.listbox.insert("end", d) + + frame = ttk.Frame(tab) + frame.grid(row=3, column=0, sticky="nsew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + button = ttk.Button(frame, text="Add", command=self.add_directory) + button.grid(row=0, column=0, sticky="ew") + button = ttk.Button(frame, text="Remove", command=self.remove_directory) + button.grid(row=0, column=1, sticky="ew") def draw_tab_startstop(self): tab = ttk.Frame(self.notebook, padding=FRAME_PAD) @@ -527,3 +559,34 @@ class ServiceConfigDialog(Dialog): shutdown = self.shutdown_commands_listbox.get(0, "end") validate = self.validate_commands_listbox.get(0, "end") return startup, validate, shutdown + + def find_directory_button(self): + d = filedialog.askdirectory(initialdir="/") + self.directory_entry.delete(0, "end") + self.directory_entry.insert("end", d) + + def add_directory(self): + d = self.directory_entry.get() + if os.path.isdir(d): + if d not in self.temp_directories: + self.dir_list.listbox.insert("end", d) + self.temp_directories.append(d) + + def remove_directory(self): + d = self.directory_entry.get() + dirs = self.dir_list.listbox.get(0, "end") + if d and d in self.temp_directories: + self.temp_directories.remove(d) + try: + i = dirs.index(d) + self.dir_list.listbox.delete(i) + except ValueError: + logging.debug("directory is not in the list") + self.directory_entry.delete(0, "end") + + def directory_select(self, event): + i = self.dir_list.listbox.curselection() + if i: + d = self.dir_list.listbox.get(i) + self.directory_entry.delete(0, "end") + self.directory_entry.insert("end", d) From 7574765305ec1b21e6a1aad78e5123ffb9bc3884 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 26 Feb 2020 12:18:55 -0800 Subject: [PATCH 0047/1131] updates to Pipfiles and requirements.txt for pyproj dependency --- daemon/Pipfile | 1 - daemon/Pipfile.lock | 74 ++++++++++++++++++++--------------------- daemon/requirements.txt | 11 +++--- 3 files changed, 42 insertions(+), 44 deletions(-) diff --git a/daemon/Pipfile b/daemon/Pipfile index 3653f82f..d55b248f 100644 --- a/daemon/Pipfile +++ b/daemon/Pipfile @@ -21,4 +21,3 @@ mock = "*" [packages] core = {editable = true,path = "."} -pyproj = "*" diff --git a/daemon/Pipfile.lock b/daemon/Pipfile.lock index 4b720b93..fe07d856 100644 --- a/daemon/Pipfile.lock +++ b/daemon/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f737e90b72e5e8c1f92357272d0827e359848ac923d2e48fde9af1b4a67c855e" + "sha256": "d702e6eed5a1362bf261543572bbffd2e8a87140b8d8cb07b99fb0d25220a2b5" }, "pipfile-spec": 6, "requires": {}, @@ -337,35 +337,33 @@ }, "pyproj": { "hashes": [ - "sha256:0608ac0aed84dcf57c859df87ac315b9acce18268f62bafc04071b7b1ff1c5a9", - "sha256:18265fb755e01df1d2248f1e837d81da4c9625e8f09481d64a9d6282c96f7467", - "sha256:190540946bb6fbfce285f46c08fcfd9d03e9331a0e952a3ef2047e6b8e8d8125", - "sha256:1da7f86d3b5e80ba3dabfd2c904a41bb6997ad9b55b47a934035492eaa0f331e", - "sha256:2ebbaee33e076664058effc3f6c943ed4c19a45df3989203ac081fca4a4722e3", - "sha256:32168c57450a1e6310b7ca331983d62d88393cc3e93b866fd6ea63dac30c7d3b", - "sha256:34b8ccf42032d89ebb8e0a839ae91e943ed222dab9bf3c1373f6fb972f8bcac4", - "sha256:432b4d28030635fac72713610aad2ed7424a7f07746fa1aa620c89761eb5e7a4", - "sha256:55103aa0adf25d207efd6f7f36d79dadee7706f22c1791955cc52033b40071e3", - "sha256:6bc74337edc1239f8c59d0d5b18a7996670b8fd523712d2dac599d5b792feae2", - "sha256:6d2838bec2d9ccd31dba68c76e8e7504bf819a4d4ace86adfca1e009d8f30f19", - "sha256:763ccac4398889cb798668824d34c4135f2e84a50681465a4199554aa1bd8611", - "sha256:8dbf1633ad2abdae6f73fe8989700c74a12dc82cb8597e66af28ff3d990d9c45", - "sha256:8ddffa4bcd9008c963840e8e79f2f3124f85f18d5987d4bbd9e7f38d9839a985", - "sha256:8f225c6186b0cd2cb07fe377786425a2ddc4183ae438fe63c60b4a879c91620f", - "sha256:97844a87cac739e389d1d0c69bc3b36c1d5c50c9f91443ef68bdef8fdf007f02", - "sha256:9d7a13def19a91836a2c84e5c7fcb6dd5e2c9bb205fb75ee102ffba24d80bf32", - "sha256:abd0784a017eedb3b03cd13f51b8852f4c68aa07affbee549bbd421f9b4268bb", - "sha256:acf150ca1506fcdaa52b0570f2903216413a2a4da78dfdf5ff7ee4eb92c2f8d5", - "sha256:b41522f8b77b64553280fb93823555bc8afb2469f77b8ce0e9aeed39abb50adc", - "sha256:c1058da6c02152d8637bb739dca940c6ab72683e59db6065fdcbe9102f66ca46", - "sha256:c70e713748c9c9d4a9d7bc42e1c71a17b1fc9b75b686b408a04eaf4909ead365", - "sha256:d47caa0a89dcb39ecd405e3899e07b69d8eaa6dbf267621087a4a5328da8492a", - "sha256:ed186edb4b610ed1e5589f3ba964d61da33d0bc54e89b8cbf8751da2e18555b3", - "sha256:f2dc8c2128f20ee9ed571783ce4730b181476083c403514714e15000b8b470cf", - "sha256:fba87f98344474da6df19bbfde4ca31c7d98a007069c8ef78cb27189f4bc7f04" + "sha256:0a12982df36f55412597431676e51d3e8fcf9b3e41f18103c31edfb1fc5fa4c0", + "sha256:0b57669a568e4235f09fea9c4e498b9beca2673ea7318989569dbb750ed299c5", + "sha256:155064fde6a95f6328962386ebde043679fd744f1415e512ed88ec47760ed47c", + "sha256:189b8278784655ee2a3bfc51bde3091b5615cc982d0017edabcb10099b2ccb3f", + "sha256:1db407591f99877b551a655897da1fd95f4e82e089c8b0d29bcd8beffcffedb8", + "sha256:226e0c126d6db158dd3da8879e5efab9f05b1d67989c33fc6aa73bf70409bb12", + "sha256:2842412ea3f99383850df92dbbca837847f3e574f98f81eaa8caebc6514a26e2", + "sha256:2d2884e85b1e69ff829bfd54872c322d3d5662dc2120a17fbd1094b9c08f9dc5", + "sha256:341dc836a1a57b74494a95cff0f05029988d93e1f96ba6c190384ec757d482b2", + "sha256:3d69b6a197fc8cf3585290e272e1cdd641d6834a3c71894ec4f2b800d2210d2a", + "sha256:447d5b18d941bea180f04179946d1d4f4aa5012697d78c9a4ceac6081dd32465", + "sha256:4e8f18a8be5653e90f24b0aea74e85e10271d1c537742ede8a11b569d3583125", + "sha256:659b1d748cd7480324841da93f91097a726b898a2de0d192bc771d374006ceb4", + "sha256:6972adfe6bb40da0423c12c38617809bf50ca8b7411a20795a1c6c3d96f10942", + "sha256:75d7ed27e2e081d2036647f7b40a9e3d4f9ec4bde795925f3f7b4c6bb85f742e", + "sha256:7b623a18f70e70cbe594fa429283027c1a73d6d31c70cd04eea65845cd060b76", + "sha256:8112da72b47af9ffcc8f0f42224898ba6371680501b3657091bb7420b7dd5c03", + "sha256:9686c611893d1c182befa63157f4a1d629e7caa464adf21309cf4da5d422a264", + "sha256:98bb690ca7ea50148792f656c0366e799d70dd7e43ab8f0c733b64bd96842e1c", + "sha256:a6ede79fd7ddd176d824e0366f8d326ff8bc082d7332c9b40baf8cb8ae7d51fe", + "sha256:c7e7b6a00a701e166e5ce903159282f2969eef689fd7fb9d7bcf92aaf167e150", + "sha256:cb8c57faf91173c219739a37b909edc1c35a48a86d26be17f1a21ffd9f8728c3", + "sha256:ea6c7cbe2f277ca6b32ebad77d713681819e23b07b17a4a892878ffe245826b7", + "sha256:ec4b2146ec8fcc93c38fbd1dcb0df06e5737d588fe28d833dfb2b241d2736f54", + "sha256:f540f4af0223cb2195b0953db6c5cb45256137da430657db42ad1b076caca361" ], - "index": "pypi", - "version": "==2.4.2.post1" + "version": "==2.5.0" }, "pyyaml": { "hashes": [ @@ -416,10 +414,10 @@ }, "cfgv": { "hashes": [ - "sha256:04b093b14ddf9fd4d17c53ebfd55582d27b76ed30050193c14e560770c5360eb", - "sha256:f22b426ed59cd2ab2b54ff96608d846c33dfb8766a67f0b4a6ce130ce244414f" + "sha256:1ccf53320421aeeb915275a196e23b3b8ae87dea8ac6698b1638001d4a486d53", + "sha256:c8e8f552ffcc6194f4e18dd4f68d9aef0c0d58ae7e7be8c82bee3c5e9edfa513" ], - "version": "==3.0.0" + "version": "==3.1.0" }, "click": { "hashes": [ @@ -628,11 +626,11 @@ }, "pre-commit": { "hashes": [ - "sha256:5295fb6d652a6c5e0b4636cd2c73183efdf253d45b657ce7367183134e806fe1", - "sha256:5387b53bb84ad9abc9b0845775dddd4e3243fd64cdcddaa6db28d3da6fbf06c2" + "sha256:09ebe467f43ce24377f8c2f200fe3cd2570d328eb2ce0568c8e96ce19da45fa6", + "sha256:f8d555e31e2051892c7f7b3ad9f620bd2c09271d87e9eedb2ad831737d6211eb" ], "index": "pypi", - "version": "==2.1.0" + "version": "==2.1.1" }, "protobuf": { "hashes": [ @@ -725,10 +723,10 @@ }, "virtualenv": { "hashes": [ - "sha256:531b142e300d405bb9faedad4adbeb82b4098b918e35209af2adef3129274aae", - "sha256:5dd42a9f56307542bddc446cfd10ef6576f11910366a07609fe8d0d88fa8fb7e" + "sha256:30ea90b21dabd11da5f509710ad3be2ae47d40ccbc717dfdd2efe4367c10f598", + "sha256:4a36a96d785428278edd389d9c36d763c5755844beb7509279194647b1ef47f1" ], - "version": "==20.0.5" + "version": "==20.0.7" }, "wcwidth": { "hashes": [ diff --git a/daemon/requirements.txt b/daemon/requirements.txt index d0defc6b..2118b15e 100644 --- a/daemon/requirements.txt +++ b/daemon/requirements.txt @@ -1,17 +1,18 @@ bcrypt==3.1.7 -cffi==1.13.2 +cffi==1.14.0 cryptography==2.8 fabric==2.5.0 -grpcio==1.26.0 -invoke==1.4.0 -lxml==4.4.2 +grpcio==1.27.2 +invoke==1.4.1 +lxml==4.5.0 Mako==1.1.1 MarkupSafe==1.1.1 netaddr==0.7.19 paramiko==2.7.1 Pillow==7.0.0 -protobuf==3.11.2 +protobuf==3.11.3 pycparser==2.19 PyNaCl==1.3.0 +pyproj==2.5.0 PyYAML==5.3 six==1.14.0 From 21dfaf7d6601c78a9b5f4da8c3e00ee4820235c8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 26 Feb 2020 14:34:35 -0800 Subject: [PATCH 0048/1131] avoid initializing emane event service twice --- daemon/core/emane/emanemanager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 0eb3f9d4..b7f142e7 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -328,7 +328,6 @@ class EmaneManager(ModelManager): nems = [] with self._emane_node_lock: self.buildxml() - self.initeventservice() self.starteventmonitor() if self.numnems() > 0: From e1c9155ba711077ee1fbfc1d439beca93dfca372 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 26 Feb 2020 15:29:19 -0800 Subject: [PATCH 0049/1131] simplify thread daemon usage --- daemon/core/api/grpc/client.py | 5 +++-- daemon/core/api/tlv/corehandlers.py | 2 +- daemon/core/emane/emanemanager.py | 5 +++-- daemon/core/nodes/network.py | 3 +-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 73393004..f0808713 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -146,8 +146,9 @@ def start_streamer(stream: Any, handler: Callable[[core_pb2.Event], None]) -> No :param handler: function that handles an event :return: nothing """ - thread = threading.Thread(target=stream_listener, args=(stream, handler)) - thread.daemon = True + thread = threading.Thread( + target=stream_listener, args=(stream, handler), daemon=True + ) thread.start() diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 2da9acfa..11c985c3 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -949,8 +949,8 @@ class CoreHandler(socketserver.BaseRequestHandler): file_name, {"__file__": file_name, "coreemu": self.coreemu}, ), + daemon=True, ) - thread.daemon = True thread.start() # allow time for session creation time.sleep(0.25) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index b7f142e7..af0d2492 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -682,8 +682,9 @@ class EmaneManager(ModelManager): ) return self.doeventloop = True - self.eventmonthread = threading.Thread(target=self.eventmonitorloop) - self.eventmonthread.daemon = True + self.eventmonthread = threading.Thread( + target=self.eventmonitorloop, daemon=True + ) self.eventmonthread.start() def stopeventmonitor(self) -> None: diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 6e198d48..67955a38 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -70,8 +70,7 @@ class EbtablesQueue: return self.doupdateloop = True - self.updatethread = threading.Thread(target=self.updateloop) - self.updatethread.daemon = True + self.updatethread = threading.Thread(target=self.updateloop, daemon=True) self.updatethread.start() def stopupdateloop(self, wlan: "CoreNetwork") -> None: From 20e3fbc7d9f8e123a51002a60e22867c41385f7d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 26 Feb 2020 15:39:37 -0800 Subject: [PATCH 0050/1131] modify execute python script handling for old gui to wait for script to complete before looking for new session to avoid possible race conditions --- daemon/core/api/tlv/corehandlers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 11c985c3..a5dbb882 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -952,8 +952,7 @@ class CoreHandler(socketserver.BaseRequestHandler): daemon=True, ) thread.start() - # allow time for session creation - time.sleep(0.25) + thread.join() if message.flags & MessageFlags.STRING.value: new_session_ids = set(self.coreemu.sessions.keys()) From c36f060d4499591f7580b304d0308fed84856dc4 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 26 Feb 2020 15:43:31 -0800 Subject: [PATCH 0051/1131] fixed wrong variable used for configuring service in grpcutils, add/delete directories for node's service configuration, clean up some old code --- daemon/core/api/grpc/grpcutils.py | 2 +- daemon/core/gui/coreclient.py | 4 +- daemon/core/gui/dialogs/serviceconfig.py | 56 ++++++++++++------------ 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 94cfce56..43f20442 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -379,7 +379,7 @@ def service_configuration(session: Session, config: core_pb2.ServiceConfig) -> N if config.files: service.configs = tuple(config.files) if config.directories: - service.directories = tuple(config.directories) + service.dirs = tuple(config.directories) if config.startup: service.startup = tuple(config.startup) if config.validate: diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index bd4e4aad..e20f1506 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -500,7 +500,6 @@ class CoreClient: emane_config = {x: self.emane_config[x].value for x in self.emane_config} else: emane_config = None - response = core_pb2.StartSessionResponse(result=False) try: response = self.client.start_session( @@ -619,6 +618,7 @@ class CoreClient: self, node_id: int, service_name: str, + dirs: List[str], files: List[str], startups: List[str], validations: List[str], @@ -628,6 +628,7 @@ class CoreClient: self.session_id, node_id, service_name, + directories=dirs, files=files, startup=startups, validate=validations, @@ -935,6 +936,7 @@ class CoreClient: config_proto = core_pb2.ServiceConfig( node_id=node_id, service=name, + directories=config.dirs, files=config.configs, startup=config.startup, validate=config.validate, diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index 7f0f12e1..c95e8cd7 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -1,6 +1,3 @@ -""" -Service configuration dialog -""" import logging import os import tkinter as tk @@ -79,7 +76,7 @@ class ServiceConfigDialog(Dialog): if not self.has_error: self.draw() - def load(self) -> bool: + def load(self): try: self.app.core.create_nodes_and_links() default_config = self.app.core.get_node_service( @@ -89,15 +86,12 @@ class ServiceConfigDialog(Dialog): self.default_validate = default_config.validate[:] self.default_shutdown = default_config.shutdown[:] self.default_directories = default_config.dirs[:] - custom_configs = self.service_configs - if ( - self.node_id in custom_configs - and self.service_name in custom_configs[self.node_id] - ): - service_config = custom_configs[self.node_id][self.service_name] - else: - service_config = default_config - + custom_service_config = self.service_configs.get(self.node_id, {}).get( + self.service_name, None + ) + service_config = ( + custom_service_config if custom_service_config else default_config + ) self.dependencies = service_config.dependencies[:] self.executables = service_config.executables[:] self.metadata = service_config.meta @@ -115,13 +109,11 @@ class ServiceConfigDialog(Dialog): for x in default_config.configs } self.temp_service_files = dict(self.original_service_files) - file_configs = self.file_configs - if ( - self.node_id in file_configs - and self.service_name in file_configs[self.node_id] - ): - for file, data in file_configs[self.node_id][self.service_name].items(): - self.temp_service_files[file] = data + file_config = self.file_configs.get(self.node_id, {}).get( + self.service_name, {} + ) + for file, data in file_config.items(): + self.temp_service_files[file] = data except grpc.RpcError as e: self.has_error = True show_grpc_error(e, self.master, self.app) @@ -451,23 +443,30 @@ class ServiceConfigDialog(Dialog): def click_apply(self): current_listbox = self.master.current.listbox + all_current = current_listbox.get(0, tk.END) if ( not self.is_custom_command() and not self.is_custom_service_file() and not self.has_new_files() + and not self.is_custom_directory() ): if self.node_id in self.service_configs: self.service_configs[self.node_id].pop(self.service_name, None) - current_listbox.itemconfig(current_listbox.curselection()[0], bg="") + current_listbox.itemconfig(all_current.index(self.service_name), bg="") self.destroy() return try: - if self.is_custom_command() or self.has_new_files(): + if ( + self.is_custom_command() + or self.has_new_files() + or self.is_custom_directory() + ): startup, validate, shutdown = self.get_commands() config = self.core.set_node_service( self.node_id, self.service_name, + dirs=self.temp_directories, files=list(self.filename_combobox["values"]), startups=startup, validations=validate, @@ -487,22 +486,19 @@ class ServiceConfigDialog(Dialog): self.app.core.set_node_service_file( self.node_id, self.service_name, file, self.temp_service_files[file] ) - all_current = current_listbox.get(0, tk.END) current_listbox.itemconfig(all_current.index(self.service_name), bg="green") except grpc.RpcError as e: show_grpc_error(e, self.top, self.app) self.destroy() def display_service_file_data(self, event: tk.Event): - combobox = event.widget - filename = combobox.get() + filename = self.filename_combobox.get() self.service_file_data.text.delete(1.0, "end") self.service_file_data.text.insert("end", self.temp_service_files[filename]) def update_temp_service_file_data(self, event: tk.Event): - scrolledtext = event.widget filename = self.filename_combobox.get() - self.temp_service_files[filename] = scrolledtext.get(1.0, "end") + self.temp_service_files[filename] = self.service_file_data.text.get(1.0, "end") if self.temp_service_files[filename] != self.original_service_files.get( filename, "" ): @@ -524,6 +520,9 @@ class ServiceConfigDialog(Dialog): def is_custom_service_file(self): return len(self.modified_files) > 0 + def is_custom_directory(self): + return set(self.default_directories) != set(self.dir_list.listbox.get(0, "end")) + def click_defaults(self): if self.node_id in self.service_configs: self.service_configs[self.node_id].pop(self.service_name, None) @@ -547,8 +546,9 @@ class ServiceConfigDialog(Dialog): dialog = CopyServiceConfigDialog(self, self.app, self.node_id) dialog.show() + @classmethod def append_commands( - self, commands: List[str], listbox: tk.Listbox, to_add: List[str] + cls, commands: List[str], listbox: tk.Listbox, to_add: List[str] ): for cmd in to_add: commands.append(cmd) From 1cba11d9e05de40636eda5fefb65fbfc26334520 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 27 Feb 2020 10:57:22 -0800 Subject: [PATCH 0052/1131] clean up more code, click defaults in service configuration correctly reset files tab as well as directories tab --- daemon/core/gui/dialogs/serviceconfig.py | 53 ++++++++++++++++++------ 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index c95e8cd7..e610cf94 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -67,6 +67,7 @@ class ServiceConfigDialog(Dialog): self.service_file_data = None self.validation_period_entry = None self.original_service_files = {} + self.default_config = None self.temp_service_files = {} self.modified_files = set() @@ -89,6 +90,7 @@ class ServiceConfigDialog(Dialog): custom_service_config = self.service_configs.get(self.node_id, {}).get( self.service_name, None ) + self.default_config = default_config service_config = ( custom_service_config if custom_service_config else default_config ) @@ -169,7 +171,6 @@ class ServiceConfigDialog(Dialog): button = ttk.Button( frame, image=self.editdelete_img, command=self.delete_filename ) - # button.bind("", self.delete_filename) button.grid(row=0, column=3) frame = ttk.Frame(tab) @@ -442,17 +443,14 @@ class ServiceConfigDialog(Dialog): entry.delete(0, tk.END) def click_apply(self): - current_listbox = self.master.current.listbox - all_current = current_listbox.get(0, tk.END) if ( not self.is_custom_command() and not self.is_custom_service_file() and not self.has_new_files() and not self.is_custom_directory() ): - if self.node_id in self.service_configs: - self.service_configs[self.node_id].pop(self.service_name, None) - current_listbox.itemconfig(all_current.index(self.service_name), bg="") + self.service_configs.get(self.node_id, {}).pop(self.service_name, None) + self.current_service_color("") self.destroy() return @@ -486,7 +484,7 @@ class ServiceConfigDialog(Dialog): self.app.core.set_node_service_file( self.node_id, self.service_name, file, self.temp_service_files[file] ) - current_listbox.itemconfig(all_current.index(self.service_name), bg="green") + self.current_service_color("green") except grpc.RpcError as e: show_grpc_error(e, self.top, self.app) self.destroy() @@ -524,14 +522,26 @@ class ServiceConfigDialog(Dialog): return set(self.default_directories) != set(self.dir_list.listbox.get(0, "end")) def click_defaults(self): - if self.node_id in self.service_configs: - self.service_configs[self.node_id].pop(self.service_name, None) - if self.node_id in self.file_configs: - self.file_configs[self.node_id].pop(self.service_name, None) + """ + clears out any custom configuration permanently + """ + # clear coreclient data + self.service_configs.get(self.node_id, {}).pop(self.service_name, None) + self.file_configs.get(self.node_id, {}).pop(self.service_name, None) self.temp_service_files = dict(self.original_service_files) - filename = self.filename_combobox.get() + self.modified_files.clear() + + # reset files tab + files = list(self.default_config.configs[:]) + self.filenames = files + self.filename_combobox.config(values=files) self.service_file_data.text.delete(1.0, "end") - self.service_file_data.text.insert("end", self.temp_service_files[filename]) + if len(files) > 0: + filename = files[0] + self.filename_combobox.set(filename) + self.service_file_data.text.insert("end", self.temp_service_files[filename]) + + # reset commands self.startup_commands_listbox.delete(0, tk.END) self.validate_commands_listbox.delete(0, tk.END) self.shutdown_commands_listbox.delete(0, tk.END) @@ -542,6 +552,15 @@ class ServiceConfigDialog(Dialog): for cmd in self.default_shutdown: self.shutdown_commands_listbox.insert(tk.END, cmd) + # reset directories + self.directory_entry.delete(0, "end") + self.dir_list.listbox.delete(0, "end") + self.temp_directories = list(self.default_directories) + for d in self.default_directories: + self.dir_list.listbox.insert("end", d) + + self.current_service_color("") + def click_copy(self): dialog = CopyServiceConfigDialog(self, self.app, self.node_id) dialog.show() @@ -590,3 +609,11 @@ class ServiceConfigDialog(Dialog): d = self.dir_list.listbox.get(i) self.directory_entry.delete(0, "end") self.directory_entry.insert("end", d) + + def current_service_color(self, color=""): + """ + change the current service label color + """ + listbox = self.master.current.listbox + services = listbox.get(0, tk.END) + listbox.itemconfig(services.index(self.service_name), bg=color) From 848cda03f74196191a7eba45e20790d5590aa1bd Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 27 Feb 2020 15:24:36 -0800 Subject: [PATCH 0053/1131] design execute python file dialog --- daemon/core/gui/dialogs/executepython.py | 83 ++++++++++++++++++++++++ daemon/core/gui/menubar.py | 7 +- 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 daemon/core/gui/dialogs/executepython.py diff --git a/daemon/core/gui/dialogs/executepython.py b/daemon/core/gui/dialogs/executepython.py new file mode 100644 index 00000000..65e0e0e1 --- /dev/null +++ b/daemon/core/gui/dialogs/executepython.py @@ -0,0 +1,83 @@ +import logging +import tkinter as tk +from tkinter import filedialog, ttk + +from core.gui.dialogs.dialog import Dialog +from core.gui.themes import FRAME_PAD, PADX + + +class ExecutePythonDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Execute Python Script", modal=True) + self.app = app + self.with_options = tk.IntVar(value=0) + self.options = tk.StringVar(value="") + self.option_entry = None + self.file_entry = None + self.draw() + + def draw(self): + i = 0 + frame = ttk.Frame(self.top, padding=FRAME_PAD) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.grid(row=i, column=0, sticky="nsew") + i = i + 1 + var = tk.StringVar(value="") + self.file_entry = ttk.Entry(frame, textvariable=var) + self.file_entry.grid(row=0, column=0, sticky="ew") + button = ttk.Button(frame, text="...", command=self.select_file) + button.grid(row=0, column=1, sticky="ew") + + self.top.columnconfigure(0, weight=1) + button = ttk.Checkbutton( + self.top, + text="With Options", + variable=self.with_options, + command=self.add_options, + ) + button.grid(row=i, column=0, sticky="ew") + i = i + 1 + + label = ttk.Label( + self.top, text="Any command-line options for running the Python script" + ) + label.grid(row=i, column=0, sticky="ew") + i = i + 1 + self.option_entry = ttk.Entry( + self.top, textvariable=self.options, state="disabled" + ) + self.option_entry.grid(row=i, column=0, sticky="ew") + i = i + 1 + + frame = ttk.Frame(self.top, padding=FRAME_PAD) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.grid(row=i, column=0) + button = ttk.Button(frame, text="Execute", command=self.script_execute) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew", padx=PADX) + + def add_options(self): + if self.with_options.get(): + self.option_entry.configure(state="normal") + else: + self.option_entry.configure(state="disabled") + + def select_file(self): + file = filedialog.askopenfilename( + parent=self.top, + initialdir="/", + title="Open python script", + filetypes=((".py Files", "*.py"), ("All Files", "*")), + ) + if file: + self.file_entry.delete(0, "end") + self.file_entry.insert("end", file) + + def script_execute(self): + file = self.file_entry.get() + options = self.option_entry.get() + logging.debug("Execute %s with options %s", file, options) + self.destroy() diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 935e0b92..19c89f34 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING import core.gui.menuaction as action from core.gui.coreclient import OBSERVERS +from core.gui.dialogs.executepython import ExecutePythonDialog if TYPE_CHECKING: from core.gui.app import Application @@ -67,7 +68,7 @@ class Menubar(tk.Menu): menu.add_cascade(label="Recent files", menu=self.recent_menu) menu.add_separator() menu.add_command(label="Export Python script...", state=tk.DISABLED) - menu.add_command(label="Execute XML or Python script...", state=tk.DISABLED) + menu.add_command(label="Execute Python script...", command=self.execute_python) menu.add_command( label="Execute Python script with options...", state=tk.DISABLED ) @@ -439,3 +440,7 @@ class Menubar(tk.Menu): self.app.core.save_xml(xml_file) else: self.menuaction.file_save_as_xml() + + def execute_python(self): + dialog = ExecutePythonDialog(self.app, self.app) + dialog.show() From 67da3e5c228e8693037dfdbe1d8cd4930778f43b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 27 Feb 2020 21:39:18 -0800 Subject: [PATCH 0054/1131] changes to move sdt calls internal to core interactions, which allows it to work with both guis --- daemon/core/api/tlv/corehandlers.py | 7 - daemon/core/emulator/session.py | 10 +- daemon/core/plugins/sdt.py | 434 ++++++++++------------------ 3 files changed, 156 insertions(+), 295 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index a5dbb882..1f3b24e9 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -526,11 +526,6 @@ class CoreHandler(socketserver.BaseRequestHandler): logging.debug( "%s handling message:\n%s", threading.currentThread().getName(), message ) - - # provide to sdt, if enabled - if self.session and self.session.sdt.is_enabled(): - self.session.sdt.handle_distributed(message) - if message.message_type not in self.message_handlers: logging.error("no handler for message type: %s", message.type_str()) return @@ -2042,7 +2037,6 @@ class CoreUdpHandler(CoreHandler): logging.debug("session handling message: %s", session.session_id) self.session = session self.handle_message(message) - self.session.sdt.handle_distributed(message) self.broadcast(message) else: logging.error( @@ -2067,7 +2061,6 @@ class CoreUdpHandler(CoreHandler): if session or message.message_type == MessageTypes.REGISTER.value: self.session = session self.handle_message(message) - self.session.sdt.handle_distributed(message) self.broadcast(message) else: logging.error( diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 2d91bab3..d112eb9c 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -432,6 +432,7 @@ class Session: if node_two: node_two.lock.release() + self.sdt.add_link(node_one_id, node_two_id, is_wireless=False) return node_one_interface, node_two_interface def delete_link( @@ -540,6 +541,8 @@ class Session: if node_two: node_two.lock.release() + self.sdt.delete_link(node_one_id, node_two_id) + def update_link( self, node_one_id: int, @@ -757,6 +760,7 @@ class Session: self.add_remove_control_interface(node=node, remove=False) self.services.boot_services(node) + self.sdt.add_node(node) return node def edit_node(self, node_id: int, options: NodeOptions) -> None: @@ -765,7 +769,7 @@ class Session: :param node_id: id of node to update :param options: data to update node with - :return: True if node updated, False otherwise + :return: nothing :raises core.CoreError: when node to update does not exist """ # get node to update @@ -778,6 +782,8 @@ class Session: node.canvas = options.canvas node.icon = options.icon + self.sdt.edit_node(node) + def set_node_position(self, node: NodeBase, options: NodeOptions) -> None: """ Set position for a node, use lat/lon/alt if needed. @@ -1402,6 +1408,7 @@ class Session: if node: node.shutdown() self.check_shutdown() + self.sdt.delete_node(_id) return node is not None @@ -1413,6 +1420,7 @@ class Session: funcs = [] while self.nodes: _, node = self.nodes.popitem() + self.sdt.delete_node(node.id) funcs.append((node.shutdown, [], {})) utils.threadpool(funcs) self.node_id_gen.id = 0 diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index e5a5a545..aca349ac 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -4,22 +4,15 @@ sdt.py: Scripted Display Tool (SDT3D) helper import logging import socket -from typing import TYPE_CHECKING, Any, Optional +import threading +from typing import TYPE_CHECKING, Optional, Tuple from urllib.parse import urlparse from core import constants -from core.api.tlv.coreapi import CoreLinkMessage, CoreMessage, CoreNodeMessage from core.constants import CORE_DATA_DIR from core.emane.nodes import EmaneNet from core.emulator.data import LinkData, NodeData -from core.emulator.enumerations import ( - EventTypes, - LinkTlvs, - LinkTypes, - MessageFlags, - NodeTlvs, - NodeTypes, -) +from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags from core.errors import CoreError from core.nodes.base import CoreNetworkBase, NodeBase from core.nodes.network import WlanNode @@ -28,19 +21,11 @@ if TYPE_CHECKING: from core.emulator.session import Session -# TODO: A named tuple may be more appropriate, than abusing a class dict like this -class Bunch: - """ - Helper class for recording a collection of attributes. - """ - - def __init__(self, **kwargs: Any) -> None: - """ - Create a Bunch instance. - - :param kwargs: keyword arguments - """ - self.__dict__.update(kwargs) +def link_data_params(link_data: LinkData) -> Tuple[int, int, bool]: + node_one = link_data.node1_id + node_two = link_data.node2_id + is_wireless = link_data.link_type == LinkTypes.WIRELESS.value + return node_one, node_two, is_wireless class Sdt: @@ -74,53 +59,16 @@ class Sdt: :param session: session this manager is tied to """ self.session = session + self.lock = threading.Lock() self.sock = None self.connected = False self.showerror = True self.url = self.DEFAULT_SDT_URL - # node information for remote nodes not in session._objs - # local nodes also appear here since their obj may not exist yet - self.remotes = {} - - # add handler for node updates + self.address = None + self.protocol = None self.session.node_handlers.append(self.handle_node_update) - - # add handler for link updates self.session.link_handlers.append(self.handle_link_update) - def handle_node_update(self, node_data: NodeData) -> None: - """ - Handler for node updates, specifically for updating their location. - - :param node_data: node data being updated - :return: nothing - """ - x = node_data.x_position - y = node_data.y_position - lat = node_data.latitude - lon = node_data.longitude - alt = node_data.altitude - if all([lat is not None, lon is not None, alt is not None]): - self.updatenodegeo(node_data.id, lat, lon, alt) - elif node_data.message_type == 0: - # TODO: z is not currently supported by node messages - self.updatenode(node_data.id, 0, x, y, 0) - - def handle_link_update(self, link_data: LinkData) -> None: - """ - Handler for link updates, checking for wireless link/unlink messages. - - :param link_data: link data being updated - :return: nothing - """ - if link_data.link_type == LinkTypes.WIRELESS.value: - self.updatelink( - link_data.node1_id, - link_data.node2_id, - link_data.message_type, - wireless=True, - ) - def is_enabled(self) -> bool: """ Check for "enablesdt" session option. Return False by default if @@ -137,9 +85,7 @@ class Sdt: :return: nothing """ - url = self.session.options.get_config("stdurl") - if not url: - url = self.DEFAULT_SDT_URL + url = self.session.options.get_config("stdurl", default=self.DEFAULT_SDT_URL) self.url = urlparse(url) self.address = (self.url.hostname, self.url.port) self.protocol = self.url.scheme @@ -178,7 +124,6 @@ class Sdt: # refresh all objects in SDT3D when connecting after session start if not flags & MessageFlags.ADD.value and not self.sendobjs(): return False - return True def initialize(self) -> bool: @@ -234,8 +179,10 @@ class Sdt: """ if self.sock is None: return False + try: cmd = f"{cmdstr}\n".encode() + logging.debug("sdt cmd: %s", cmd) self.sock.sendall(cmd) return True except IOError: @@ -244,91 +191,6 @@ class Sdt: self.connected = False return False - def updatenode( - self, - nodenum: int, - flags: int, - x: Optional[float], - y: Optional[float], - z: Optional[float], - name: str = None, - node_type: str = None, - icon: str = None, - ) -> None: - """ - Node is updated from a Node Message or mobility script. - - :param nodenum: node id to update - :param flags: update flags - :param x: x position - :param y: y position - :param z: z position - :param name: node name - :param node_type: node type - :param icon: node icon - :return: nothing - """ - if not self.connect(): - return - if flags & MessageFlags.DELETE.value: - self.cmd(f"delete node,{nodenum}") - return - if x is None or y is None: - return - lat, lon, alt = self.session.location.getgeo(x, y, z) - pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}" - if flags & MessageFlags.ADD.value: - if icon is not None: - node_type = name - icon = icon.replace("$CORE_DATA_DIR", constants.CORE_DATA_DIR) - icon = icon.replace("$CORE_CONF_DIR", constants.CORE_CONF_DIR) - self.cmd(f"sprite {node_type} image {icon}") - self.cmd(f'node {nodenum} type {node_type} label on,"{name}" {pos}') - else: - self.cmd(f"node {nodenum} {pos}") - - def updatenodegeo(self, nodenum: int, lat: float, lon: float, alt: float) -> None: - """ - Node is updated upon receiving an EMANE Location Event. - - :param nodenum: node id to update geospatial for - :param lat: latitude - :param lon: longitude - :param alt: altitude - :return: nothing - """ - - # TODO: received Node Message with lat/long/alt. - if not self.connect(): - return - pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}" - self.cmd(f"node {nodenum} {pos}") - - def updatelink( - self, node1num: int, node2num: int, flags: int, wireless: bool = False - ) -> None: - """ - Link is updated from a Link Message or by a wireless model. - - :param node1num: node one id - :param node2num: node two id - :param flags: link flags - :param wireless: flag to check if wireless or not - :return: nothing - """ - if node1num is None or node2num is None: - return - if not self.connect(): - return - if flags & MessageFlags.DELETE.value: - self.cmd(f"delete link,{node1num},{node2num}") - elif flags & MessageFlags.ADD.value: - if wireless: - attr = " line green,2" - else: - attr = " line red,2" - self.cmd(f"link {node1num},{node2num}{attr}") - def sendobjs(self) -> None: """ Session has already started, and the SDT3D GUI later connects. @@ -345,171 +207,169 @@ class Sdt: nets.append(node) if not isinstance(node, NodeBase): continue - (x, y, z) = node.getposition() - if x is None or y is None: - continue - self.updatenode( - node.id, - MessageFlags.ADD.value, - x, - y, - z, - node.name, - node.type, - node.icon, - ) - for nodenum in sorted(self.remotes.keys()): - r = self.remotes[nodenum] - x, y, z = r.pos - self.updatenode( - nodenum, MessageFlags.ADD.value, x, y, z, r.name, r.type, r.icon - ) + self.add_node(node) for net in nets: all_links = net.all_link_data(flags=MessageFlags.ADD.value) for link_data in all_links: is_wireless = isinstance(net, (WlanNode, EmaneNet)) - wireless_link = link_data.message_type == LinkTypes.WIRELESS.value if is_wireless and link_data.node1_id == net.id: continue + params = link_data_params(link_data) + self.add_link(*params) - self.updatelink( - link_data.node1_id, - link_data.node2_id, - MessageFlags.ADD.value, - wireless_link, - ) - - for n1num in sorted(self.remotes.keys()): - r = self.remotes[n1num] - for n2num, wireless_link in r.links: - self.updatelink(n1num, n2num, MessageFlags.ADD.value, wireless_link) - - def handle_distributed(self, message: CoreMessage) -> None: + def get_node_position(self, node: NodeBase) -> Optional[str]: """ - Broker handler for processing CORE API messages as they are - received. This is used to snoop the Node messages and update - node positions. + Convenience to generate an SDT position string, given a node. - :param message: message to handle + :param node: + :return: + """ + x, y, z = node.position.get() + if x is None or y is None: + return None + lat, lon, alt = self.session.location.getgeo(x, y, z) + return f"pos {lon:.6f},{lat:.6f},{alt:.6f}" + + def add_node(self, node: NodeBase) -> None: + """ + Handle adding a node in SDT. + + :param node: node to add :return: nothing """ - if isinstance(message, CoreLinkMessage): - self.handlelinkmsg(message) - elif isinstance(message, CoreNodeMessage): - self.handlenodemsg(message) + logging.debug("sdt add node: %s - %s", node.id, node.name) + if not self.connect(): + return + pos = self.get_node_position(node) + if not pos: + return + node_type = node.type + if node_type is None: + node_type = type(node).type + icon = node.icon + if icon: + node_type = node.name + icon = icon.replace("$CORE_DATA_DIR", constants.CORE_DATA_DIR) + icon = icon.replace("$CORE_CONF_DIR", constants.CORE_CONF_DIR) + self.cmd(f"sprite {node_type} image {icon}") + self.cmd(f'node {node.id} type {node_type} label on,"{node.name}" {pos}') - def handlenodemsg(self, msg: CoreNodeMessage) -> None: + def edit_node(self, node: NodeBase) -> None: """ - Process a Node Message to add/delete or move a node on - the SDT display. Node properties are found in a session or - self.remotes for remote nodes (or those not yet instantiated). + Handle updating a node in SDT. - :param msg: node message to handle + :param node: node to update :return: nothing """ - # for distributed sessions to work properly, the SDT option should be - # enabled prior to starting the session - if not self.is_enabled(): + logging.debug("sdt update node: %s - %s", node.id, node.name) + if not self.connect(): return - # node.(_id, type, icon, name) are used. - nodenum = msg.get_tlv(NodeTlvs.NUMBER.value) - if not nodenum: + pos = self.get_node_position(node) + if not pos: return - x = msg.get_tlv(NodeTlvs.X_POSITION.value) - y = msg.get_tlv(NodeTlvs.Y_POSITION.value) - z = None - name = msg.get_tlv(NodeTlvs.NAME.value) + self.cmd(f"node {node.id} {pos}") - nodetype = msg.get_tlv(NodeTlvs.TYPE.value) - model = msg.get_tlv(NodeTlvs.MODEL.value) - icon = msg.get_tlv(NodeTlvs.ICON.value) + def delete_node(self, node_id: int) -> None: + """ + Handle deleting a node in SDT. - net = False - if nodetype == NodeTypes.DEFAULT.value or nodetype == NodeTypes.PHYSICAL.value: - if model is None: - model = "router" - nodetype = model - elif nodetype is not None: - nodetype = NodeTypes(nodetype) - nodetype = self.session.get_node_class(nodetype).type - net = True + :param node_id: node id to delete + :return: nothing + """ + logging.debug("sdt delete node: %s", node_id) + if not self.connect(): + return + self.cmd(f"delete node,{node_id}") + + def handle_node_update(self, node_data: NodeData) -> None: + """ + Handler for node updates, specifically for updating their location. + + :param node_data: node data being updated + :return: nothing + """ + logging.debug("sdt handle node update: %s - %s", node_data.id, node_data.name) + if not self.connect(): + return + + # delete node + if node_data.message_type == MessageFlags.DELETE.value: + self.cmd(f"delete node,{node_data.id}") else: - nodetype = None + x = node_data.x_position + y = node_data.y_position + lat = node_data.latitude + lon = node_data.longitude + alt = node_data.altitude + if all([lat is not None, lon is not None, alt is not None]): + pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}" + self.cmd(f"node {node_data.id} {pos}") + elif node_data.message_type == 0: + lat, lon, alt = self.session.location.getgeo(x, y, 0) + pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}" + self.cmd(f"node {node_data.id} {pos}") + def wireless_net_check(self, node_id: int) -> bool: + """ + Determines if a node is either a wireless node type. + + :param node_id: node id to check + :return: True is a wireless node type, False otherwise + """ + result = False try: - node = self.session.get_node(nodenum) + node = self.session.get_node(node_id) + result = isinstance(node, (WlanNode, EmaneNet)) except CoreError: - node = None - if node: - self.updatenode( - node.id, msg.flags, x, y, z, node.name, node.type, node.icon - ) - else: - if nodenum in self.remotes: - remote = self.remotes[nodenum] - if name is None: - name = remote.name - if nodetype is None: - nodetype = remote.type - if icon is None: - icon = remote.icon - else: - remote = Bunch( - _id=nodenum, - type=nodetype, - icon=icon, - name=name, - net=net, - links=set(), - ) - self.remotes[nodenum] = remote - remote.pos = (x, y, z) - self.updatenode(nodenum, msg.flags, x, y, z, name, nodetype, icon) + pass + return result - def handlelinkmsg(self, msg: CoreLinkMessage) -> None: + def add_link(self, node_one: int, node_two: int, is_wireless: bool) -> None: """ - Process a Link Message to add/remove links on the SDT display. - Links are recorded in the remotes[nodenum1].links set for updating - the SDT display at a later time. + Handle adding a link in SDT. - :param msg: link message to handle + :param node_one: node one id + :param node_two: node two id + :param is_wireless: True if link is wireless, False otherwise :return: nothing """ - if not self.is_enabled(): + logging.debug("sdt add link: %s, %s, %s", node_one, node_two, is_wireless) + if not self.connect(): return - nodenum1 = msg.get_tlv(LinkTlvs.N1_NUMBER.value) - nodenum2 = msg.get_tlv(LinkTlvs.N2_NUMBER.value) - link_msg_type = msg.get_tlv(LinkTlvs.TYPE.value) - # this filters out links to WLAN and EMANE nodes which are not drawn - if self.wlancheck(nodenum1): + if self.wireless_net_check(node_one) or self.wireless_net_check(node_two): return - wl = link_msg_type == LinkTypes.WIRELESS.value - if nodenum1 in self.remotes: - r = self.remotes[nodenum1] - if msg.flags & MessageFlags.DELETE.value: - if (nodenum2, wl) in r.links: - r.links.remove((nodenum2, wl)) - else: - r.links.add((nodenum2, wl)) - self.updatelink(nodenum1, nodenum2, msg.flags, wireless=wl) - - def wlancheck(self, nodenum: int) -> bool: - """ - Helper returns True if a node number corresponds to a WLAN or EMANE node. - - :param nodenum: node id to check - :return: True if node is wlan or emane, False otherwise - """ - if nodenum in self.remotes: - node_type = self.remotes[nodenum].type - if node_type in ("wlan", "emane"): - return True + if is_wireless: + attr = "green,2" else: - try: - n = self.session.get_node(nodenum) - except CoreError: - return False - if isinstance(n, (WlanNode, EmaneNet)): - return True - return False + attr = "red,2" + self.cmd(f"link {node_one},{node_two} line {attr}") + + def delete_link(self, node_one: int, node_two: int) -> None: + """ + Handle deleting a node in SDT. + + :param node_one: node one id + :param node_two: node two id + :return: nothing + """ + logging.debug("sdt delete link: %s, %s", node_one, node_two) + if not self.connect(): + return + if self.wireless_net_check(node_one) or self.wireless_net_check(node_two): + return + self.cmd(f"delete link,{node_one},{node_two}") + + def handle_link_update(self, link_data: LinkData) -> None: + """ + Handle link broadcast messages and push changes to SDT. + + :param link_data: link data to handle + :return: nothing + """ + if link_data.message_type == MessageFlags.ADD.value: + params = link_data_params(link_data) + self.add_link(*params) + elif link_data.message_type == MessageFlags.DELETE.value: + params = link_data_params(link_data) + self.delete_link(*params[:2]) From 9535d40b700ab64b657b516e7ed59ed79f6d0923 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 28 Feb 2020 12:28:41 -0800 Subject: [PATCH 0055/1131] added grpc call to execute python script, to replicate prior gui functionality --- daemon/core/api/grpc/client.py | 6 ++++++ daemon/core/api/grpc/server.py | 22 ++++++++++++++++++++++ daemon/proto/core/api/grpc/core.proto | 10 ++++++++++ 3 files changed, 38 insertions(+) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index f0808713..15122e67 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -27,6 +27,8 @@ from core.api.grpc.configservices_pb2 import ( SetNodeConfigServiceResponse, ) from core.api.grpc.core_pb2 import ( + ExecuteScriptRequest, + ExecuteScriptResponse, GetEmaneEventChannelRequest, GetEmaneEventChannelResponse, ) @@ -1148,6 +1150,10 @@ class CoreGrpcClient: request = GetEmaneEventChannelRequest(session_id=session_id) return self.stub.GetEmaneEventChannel(request) + def execute_script(self, script: str) -> ExecuteScriptResponse: + request = ExecuteScriptRequest(script=script) + return self.stub.ExecuteScript(request) + def connect(self) -> None: """ Open connection to server, must be closed manually. diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index f155867d..ca992481 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -3,6 +3,7 @@ import logging import os import re import tempfile +import threading import time from concurrent import futures from typing import Type @@ -10,6 +11,7 @@ from typing import Type import grpc from grpc import ServicerContext +from core import utils from core.api.grpc import ( common_pb2, configservices_pb2, @@ -33,6 +35,7 @@ from core.api.grpc.configservices_pb2 import ( SetNodeConfigServiceResponse, ) from core.api.grpc.core_pb2 import ( + ExecuteScriptResponse, GetEmaneEventChannelRequest, GetEmaneEventChannelResponse, ) @@ -1645,3 +1648,22 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): if session.emane.eventchannel: group, port, device = session.emane.eventchannel return GetEmaneEventChannelResponse(group=group, port=port, device=device) + + def ExecuteScript(self, request, context): + existing_sessions = set(self.coreemu.sessions.keys()) + thread = threading.Thread( + target=utils.execute_file, + args=( + request.script, + {"__file__": request.script, "coreemu": self.coreemu}, + ), + daemon=True, + ) + thread.start() + thread.join() + current_sessions = set(self.coreemu.sessions.keys()) + new_sessions = list(current_sessions.difference(existing_sessions)) + new_session = -1 + if new_sessions: + new_session = new_sessions[0] + return ExecuteScriptResponse(session_id=new_session) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index e515ab2e..b89e5fb1 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -154,6 +154,8 @@ service CoreApi { } rpc EmaneLink (EmaneLinkRequest) returns (EmaneLinkResponse) { } + rpc ExecuteScript (ExecuteScriptRequest) returns (ExecuteScriptResponse) { + } } // rpc request/response messages @@ -759,6 +761,14 @@ message EmaneLinkResponse { bool result = 1; } +message ExecuteScriptRequest { + string script = 1; +} + +message ExecuteScriptResponse { + int32 session_id = 1; +} + // data structures for messages below message WlanConfig { int32 node_id = 1; From dfc24e107f87131521d0ba48752d77baf7a6c79e Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 2 Mar 2020 09:01:03 -0800 Subject: [PATCH 0056/1131] use grpc method to execute python script, redraw canvas and reset session data --- daemon/core/gui/coreclient.py | 6 ++++++ daemon/core/gui/dialogs/executepython.py | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 7d8e832c..e6663992 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -1064,3 +1064,9 @@ class CoreClient: def service_been_modified(self, node_id: int) -> bool: return node_id in self.modified_service_nodes + + def execute_script(self, script): + response = self.client.execute_script(script) + logging.info("execute python script %s", response) + if response.session_id != -1: + self.join_session(response.session_id) diff --git a/daemon/core/gui/dialogs/executepython.py b/daemon/core/gui/dialogs/executepython.py index 65e0e0e1..37553277 100644 --- a/daemon/core/gui/dialogs/executepython.py +++ b/daemon/core/gui/dialogs/executepython.py @@ -79,5 +79,6 @@ class ExecutePythonDialog(Dialog): def script_execute(self): file = self.file_entry.get() options = self.option_entry.get() - logging.debug("Execute %s with options %s", file, options) + logging.info("Execute %s with options %s", file, options) + self.app.core.execute_script(file) self.destroy() From a7fa0bf6d367a61a724aa76aa810c237805148ac Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 2 Mar 2020 09:17:35 -0800 Subject: [PATCH 0057/1131] use a bigger size font for alert button text to see the scaling effect more easily --- daemon/core/gui/themes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/core/gui/themes.py b/daemon/core/gui/themes.py index 7da0b1dd..e9c5cba3 100644 --- a/daemon/core/gui/themes.py +++ b/daemon/core/gui/themes.py @@ -182,21 +182,21 @@ def theme_change(event: tk.Event): background="green", padding=0, relief=tk.NONE, - font="TkSmallCaptionFont", + font="TkDefaultFont", ) style.configure( Styles.yellow_alert, background="yellow", padding=0, relief=tk.NONE, - font="TkSmallCaptionFont", + font="TkDefaultFont", ) style.configure( Styles.red_alert, background="red", padding=0, relief=tk.NONE, - font="TkSmallCaptionFont", + font="TkDefaultFont", ) From b0a3c85f0e25adc2ad4840a88cca2ebbada2498b Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 2 Mar 2020 09:56:57 -0800 Subject: [PATCH 0058/1131] allow editable scale field for manually setting the app scale value --- daemon/core/gui/dialogs/preferences.py | 6 +++++- daemon/core/gui/validation.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index 83f50f07..c693e025 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -81,7 +81,11 @@ class PreferencesDialog(Dialog): ) scale.grid(row=0, column=0, sticky="ew") entry = ttk.Entry( - scale_frame, textvariable=self.gui_scale, width=4, state="disabled" + scale_frame, + textvariable=self.gui_scale, + width=4, + validate="key", + validatecommand=(self.app.validation.app_scale, "%P"), ) entry.grid(row=0, column=1) diff --git a/daemon/core/gui/validation.py b/daemon/core/gui/validation.py index 78685f9f..af16dadd 100644 --- a/daemon/core/gui/validation.py +++ b/daemon/core/gui/validation.py @@ -11,12 +11,16 @@ from netaddr import IPNetwork if TYPE_CHECKING: from core.gui.app import Application +SMALLEST_SCALE = 0.5 +LARGEST_SCALE = 5.0 + class InputValidation: def __init__(self, app: "Application"): self.master = app.master self.positive_int = None self.positive_float = None + self.app_scale = None self.name = None self.ip4 = None self.rgb = None @@ -26,6 +30,7 @@ class InputValidation: def register(self): self.positive_int = self.master.register(self.check_positive_int) self.positive_float = self.master.register(self.check_positive_float) + self.app_scale = self.master.register(self.check_scale_value) self.name = self.master.register(self.check_node_name) self.ip4 = self.master.register(self.check_ip4) self.rgb = self.master.register(self.check_rbg) @@ -105,6 +110,18 @@ class InputValidation: except ValueError: return False + @classmethod + def check_scale_value(cls, s: str) -> bool: + if not s: + return True + try: + float_value = float(s) + if SMALLEST_SCALE <= float_value <= LARGEST_SCALE or float_value == 0: + return True + return False + except ValueError: + return False + @classmethod def check_ip4(cls, s: str) -> bool: if not s: From ff3b20a9627d7870343c95a7953f81beb9d0a594 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 2 Mar 2020 10:01:36 -0800 Subject: [PATCH 0059/1131] modifications to support optional geo position edits for nodes and to account for geo updates to sdt --- daemon/core/api/grpc/client.py | 5 ++++- daemon/core/api/grpc/grpcutils.py | 4 +++- daemon/core/api/grpc/server.py | 23 ++++++++++++++--------- daemon/core/emulator/session.py | 12 +++++++++--- daemon/core/plugins/sdt.py | 18 +++++++++++++----- daemon/proto/core/api/grpc/core.proto | 11 ++++++++--- 6 files changed, 51 insertions(+), 22 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 15122e67..e90f6ead 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -475,9 +475,10 @@ class CoreGrpcClient: self, session_id: int, node_id: int, - position: core_pb2.Position, + position: core_pb2.Position = None, icon: str = None, source: str = None, + geo: core_pb2.Geo = None, ) -> core_pb2.EditNodeResponse: """ Edit a node, currently only changes position. @@ -487,6 +488,7 @@ class CoreGrpcClient: :param position: position to set node to :param icon: path to icon for gui to use for node :param source: application source editing node + :param geo: lon,lat,alt location for node :return: response with result of success or failure :raises grpc.RpcError: when session or node doesn't exist """ @@ -496,6 +498,7 @@ class CoreGrpcClient: position=position, icon=icon, source=source, + geo=geo, ) return self.stub.EditNode(request) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 94cfce56..633bc237 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -44,7 +44,9 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption position = node_proto.position options.set_position(position.x, position.y) - options.set_location(position.lat, position.lon, position.alt) + if node_proto.HasField("geo"): + geo = node_proto.geo + options.set_location(geo.lat, geo.lon, geo.alt) return _type, _id, options diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index ca992481..ae79cc95 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -688,21 +688,26 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): node = self.get_node(session, request.node_id, context) options = NodeOptions() options.icon = request.icon - x = request.position.x - y = request.position.y - options.set_position(x, y) - lat = request.position.lat - lon = request.position.lon - alt = request.position.alt - options.set_location(lat, lon, alt) + if request.HasField("position"): + x = request.position.x + y = request.position.y + options.set_position(x, y) + lat, lon, alt = None, None, None + has_geo = request.HasField("geo") + if has_geo: + lat = request.geo.lat + lon = request.geo.lon + alt = request.geo.alt + options.set_location(lat, lon, alt) result = True try: session.edit_node(node.id, options) source = None if request.source: source = request.source - node_data = node.data(0, source=source) - session.broadcast_node(node_data) + if not has_geo: + node_data = node.data(0, source=source) + session.broadcast_node(node_data) except CoreError: result = False return core_pb2.EditNodeResponse(result=result) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index d112eb9c..b0e44cb5 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -782,7 +782,8 @@ class Session: node.canvas = options.canvas node.icon = options.icon - self.sdt.edit_node(node) + # provide edits to sdt + self.sdt.edit_node(node, options.lon, options.lat, options.alt) def set_node_position(self, node: NodeBase, options: NodeOptions) -> None: """ @@ -812,9 +813,11 @@ class Session: # broadcast updated location when using lat/lon/alt if using_lat_lon_alt: - self.broadcast_node_location(node) + self.broadcast_node_location(node, lon, lat, alt) - def broadcast_node_location(self, node: NodeBase) -> None: + def broadcast_node_location( + self, node: NodeBase, lon: float, lat: float, alt: float + ) -> None: """ Broadcast node location to all listeners. @@ -826,6 +829,9 @@ class Session: id=node.id, x_position=node.position.x, y_position=node.position.y, + latitude=lat, + longitude=lon, + altitude=alt, ) self.broadcast_node(node_data) diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index aca349ac..1ccf40a5 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -255,20 +255,28 @@ class Sdt: self.cmd(f"sprite {node_type} image {icon}") self.cmd(f'node {node.id} type {node_type} label on,"{node.name}" {pos}') - def edit_node(self, node: NodeBase) -> None: + def edit_node(self, node: NodeBase, lon: float, lat: float, alt: float) -> None: """ Handle updating a node in SDT. :param node: node to update + :param lon: node longitude + :param lat: node latitude + :param alt: node altitude :return: nothing """ logging.debug("sdt update node: %s - %s", node.id, node.name) if not self.connect(): return - pos = self.get_node_position(node) - if not pos: - return - self.cmd(f"node {node.id} {pos}") + + if all([lat is not None, lon is not None, alt is not None]): + pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}" + self.cmd(f"node {node.id} {pos}") + else: + pos = self.get_node_position(node) + if not pos: + return + self.cmd(f"node {node.id} {pos}") def delete_node(self, node_id: int) -> None: """ diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index b89e5fb1..53d5c602 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -408,6 +408,7 @@ message EditNodeRequest { Position position = 3; string icon = 4; string source = 5; + Geo geo = 6; } message EditNodeResponse { @@ -977,6 +978,7 @@ message Node { string image = 10; string server = 11; repeated string config_services = 12; + Geo geo = 13; } message Link { @@ -1029,7 +1031,10 @@ message Position { float x = 1; float y = 2; float z = 3; - float lat = 4; - float lon = 5; - float alt = 6; +} + +message Geo { + float lat = 1; + float lon = 2; + float alt = 3; } From 933f409498d885a246d160224d3383d6f9c6127f Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 2 Mar 2020 10:18:37 -0800 Subject: [PATCH 0060/1131] adjust node text and edge text to scale not as fast as other components --- daemon/core/gui/app.py | 8 ++++---- daemon/core/gui/dialogs/preferences.py | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 8b18beeb..10975eef 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -1,3 +1,4 @@ +import math import tkinter as tk from tkinter import font, ttk @@ -47,11 +48,10 @@ class Application(tk.Frame): def setup_scaling(self): self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()} + text_scale = self.app_scale if self.app_scale < 1 else math.sqrt(self.app_scale) themes.scale_fonts(self.fonts_size, self.app_scale) - self.icon_text_font = font.Font( - family="TkIconFont", size=int(12 * self.app_scale) - ) - self.edge_font = font.Font(family="TkDefaultFont", size=int(8 * self.app_scale)) + self.icon_text_font = font.Font(family="TkIconFont", size=int(12 * text_scale)) + self.edge_font = font.Font(family="TkDefaultFont", size=int(8 * text_scale)) def setup_theme(self): themes.load(self.style) diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index c693e025..afba6fed 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -1,4 +1,5 @@ import logging +import math import tkinter as tk from tkinter import ttk from typing import TYPE_CHECKING @@ -127,8 +128,9 @@ class PreferencesDialog(Dialog): # scale fonts scale_fonts(self.app.fonts_size, app_scale) - self.app.icon_text_font.config(size=int(12 * app_scale)) - self.app.edge_font.config(size=int(8 * app_scale)) + text_scale = app_scale if app_scale < 1 else math.sqrt(app_scale) + self.app.icon_text_font.config(size=int(12 * text_scale)) + self.app.edge_font.config(size=int(8 * text_scale)) # scale application window self.app.center() From 58cb5a1a1d51a0f7a9ed3125b475da066fe29cf9 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 2 Mar 2020 11:02:54 -0800 Subject: [PATCH 0061/1131] add a scrollbar next to scale entry to allow scale adjustment in increments of a specific value (since the Scale Slider widget does not support this) --- daemon/core/gui/dialogs/preferences.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index afba6fed..45a3acee 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -11,6 +11,8 @@ from core.gui.themes import FRAME_PAD, PADX, PADY, scale_fonts if TYPE_CHECKING: from core.gui.app import Application +SCALE_INTERVAL = 0.01 + class PreferencesDialog(Dialog): def __init__(self, master: "Application", app: "Application"): @@ -90,6 +92,9 @@ class PreferencesDialog(Dialog): ) entry.grid(row=0, column=1) + scrollbar = ttk.Scrollbar(scale_frame, command=self.adjust_scale) + scrollbar.grid(row=0, column=2) + def draw_buttons(self): frame = ttk.Frame(self.top) frame.grid(sticky="ew") @@ -138,3 +143,16 @@ class PreferencesDialog(Dialog): # scale toolbar and canvas items self.app.toolbar.scale() self.app.canvas.scale_graph() + + def adjust_scale(self, arg1: str, arg2: str, arg3: str): + scale_value = self.gui_scale.get() + if arg2 == "-1": + if scale_value <= 4.9: + self.gui_scale.set(scale_value + SCALE_INTERVAL) + else: + self.gui_scale.set(5.0) + elif arg2 == "1": + if scale_value >= 0.6: + self.gui_scale.set(scale_value - SCALE_INTERVAL) + else: + self.gui_scale.set(0.5) From 9cd6166b9b8e1e3f4ade4b277afab9b4b100bf0a Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 2 Mar 2020 11:20:00 -0800 Subject: [PATCH 0062/1131] use varaibles that represent smallest and largest allowed scale value to replace float numbers --- daemon/core/gui/dialogs/preferences.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index 45a3acee..2f728416 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING from core.gui import appconfig from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY, scale_fonts +from core.gui.validation import LARGEST_SCALE, SMALLEST_SCALE if TYPE_CHECKING: from core.gui.app import Application @@ -76,8 +77,8 @@ class PreferencesDialog(Dialog): scale_frame.columnconfigure(0, weight=1) scale = ttk.Scale( scale_frame, - from_=0.5, - to=5, + from_=SMALLEST_SCALE, + to=LARGEST_SCALE, value=1, orient=tk.HORIZONTAL, variable=self.gui_scale, @@ -147,12 +148,12 @@ class PreferencesDialog(Dialog): def adjust_scale(self, arg1: str, arg2: str, arg3: str): scale_value = self.gui_scale.get() if arg2 == "-1": - if scale_value <= 4.9: - self.gui_scale.set(scale_value + SCALE_INTERVAL) + if scale_value <= LARGEST_SCALE - SCALE_INTERVAL: + self.gui_scale.set(round(scale_value + SCALE_INTERVAL, 2)) else: - self.gui_scale.set(5.0) + self.gui_scale.set(round(LARGEST_SCALE, 2)) elif arg2 == "1": - if scale_value >= 0.6: - self.gui_scale.set(scale_value - SCALE_INTERVAL) + if scale_value >= SMALLEST_SCALE + SCALE_INTERVAL: + self.gui_scale.set(round(scale_value - SCALE_INTERVAL, 2)) else: - self.gui_scale.set(0.5) + self.gui_scale.set(round(SMALLEST_SCALE, 2)) From ea341cbe4565ee13f3fbca678473d08defec70e8 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 2 Mar 2020 14:08:11 -0800 Subject: [PATCH 0063/1131] set the initial directory of executing python scripts to HOME_PATH/scripts --- daemon/core/gui/appconfig.py | 2 ++ daemon/core/gui/dialogs/executepython.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/daemon/core/gui/appconfig.py b/daemon/core/gui/appconfig.py index 97c76065..13c3de30 100644 --- a/daemon/core/gui/appconfig.py +++ b/daemon/core/gui/appconfig.py @@ -16,6 +16,7 @@ MOBILITY_PATH = HOME_PATH.joinpath("mobility") XMLS_PATH = HOME_PATH.joinpath("xmls") CONFIG_PATH = HOME_PATH.joinpath("gui.yaml") LOG_PATH = HOME_PATH.joinpath("gui.log") +SCRIPT_PATH = HOME_PATH.joinpath("scripts") # local paths DATA_PATH = Path(__file__).parent.joinpath("data") @@ -60,6 +61,7 @@ def check_directory(): ICONS_PATH.mkdir() MOBILITY_PATH.mkdir() XMLS_PATH.mkdir() + SCRIPT_PATH.mkdir() copy_files(LOCAL_ICONS_PATH, ICONS_PATH) copy_files(LOCAL_BACKGROUND_PATH, BACKGROUNDS_PATH) diff --git a/daemon/core/gui/dialogs/executepython.py b/daemon/core/gui/dialogs/executepython.py index 37553277..9adf4f93 100644 --- a/daemon/core/gui/dialogs/executepython.py +++ b/daemon/core/gui/dialogs/executepython.py @@ -2,6 +2,7 @@ import logging import tkinter as tk from tkinter import filedialog, ttk +from core.gui.appconfig import SCRIPT_PATH from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX @@ -68,7 +69,7 @@ class ExecutePythonDialog(Dialog): def select_file(self): file = filedialog.askopenfilename( parent=self.top, - initialdir="/", + initialdir=str(SCRIPT_PATH), title="Open python script", filetypes=((".py Files", "*.py"), ("All Files", "*")), ) From 539ca5d22c394445256bec2553c68a2aff139d1f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 3 Mar 2020 22:27:02 -0800 Subject: [PATCH 0064/1131] added docker/lxc to xml read/write, fixed icon retrieval for docker/lxc in new gui --- daemon/core/gui/images.py | 2 ++ daemon/core/xml/corexml.py | 27 +++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/daemon/core/gui/images.py b/daemon/core/gui/images.py index f299c5a5..1f43103c 100644 --- a/daemon/core/gui/images.py +++ b/daemon/core/gui/images.py @@ -106,6 +106,8 @@ class TypeToImage: (core_pb2.NodeType.EMANE, ""): ImageEnum.EMANE, (core_pb2.NodeType.RJ45, ""): ImageEnum.RJ45, (core_pb2.NodeType.TUNNEL, ""): ImageEnum.TUNNEL, + (core_pb2.NodeType.DOCKER, ""): ImageEnum.DOCKER, + (core_pb2.NodeType.LXC, ""): ImageEnum.LXC, } @classmethod diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 1f402510..8eab98c2 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -10,6 +10,8 @@ from core.emulator.data import LinkData from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.enumerations import NodeTypes from core.nodes.base import CoreNetworkBase, CoreNodeBase, NodeBase +from core.nodes.docker import DockerNode +from core.nodes.lxd import LxcNode from core.nodes.network import CtrlNet from core.services.coreservices import CoreService @@ -213,8 +215,21 @@ class DeviceElement(NodeElement): def __init__(self, session: "Session", node: NodeBase) -> None: super().__init__(session, node, "device") add_attribute(self.element, "type", node.type) + self.add_class() self.add_services() + def add_class(self) -> None: + clazz = "" + image = "" + if isinstance(self.node, DockerNode): + clazz = "docker" + image = self.node.image + elif isinstance(self.node, LxcNode): + clazz = "lxc" + image = self.node.image + add_attribute(self.element, "class", clazz) + add_attribute(self.element, "image", image) + def add_services(self) -> None: service_elements = etree.Element("services") for service in self.node.services: @@ -796,9 +811,17 @@ class CoreXmlReader: name = device_element.get("name") model = device_element.get("type") icon = device_element.get("icon") - options = NodeOptions(name, model) + clazz = device_element.get("class") + image = device_element.get("image") + options = NodeOptions(name, model, image) options.icon = icon + node_type = NodeTypes.DEFAULT + if clazz == "docker": + node_type = NodeTypes.DOCKER + elif clazz == "lxc": + node_type = NodeTypes.LXC + service_elements = device_element.find("services") if service_elements is not None: options.services = [x.get("name") for x in service_elements.iterchildren()] @@ -823,7 +846,7 @@ class CoreXmlReader: options.set_location(lat, lon, alt) logging.info("reading node id(%s) model(%s) name(%s)", node_id, model, name) - self.session.add_node(_id=node_id, options=options) + self.session.add_node(_type=node_type, _id=node_id, options=options) def read_network(self, network_element: etree.Element) -> None: node_id = get_int(network_element, "id") From 4093b2244a5019124d0af83bdfb43a03a874c2fd Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 3 Mar 2020 22:38:03 -0800 Subject: [PATCH 0065/1131] fixed new gui removing marker annotations when creating new sessions --- daemon/core/gui/dialogs/marker.py | 6 +++--- daemon/core/gui/graph/graph.py | 2 +- daemon/core/gui/graph/tags.py | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/daemon/core/gui/dialogs/marker.py b/daemon/core/gui/dialogs/marker.py index 1db9ca49..f07376b3 100644 --- a/daemon/core/gui/dialogs/marker.py +++ b/daemon/core/gui/dialogs/marker.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.dialog import Dialog +from core.gui.graph import tags if TYPE_CHECKING: from core.gui.app import Application @@ -19,7 +20,7 @@ class MarkerDialog(Dialog): def __init__( self, master: "Application", app: "Application", initcolor: str = "#000000" ): - super().__init__(master, app, "marker tool", modal=False) + super().__init__(master, app, "Marker Tool", modal=False) self.app = app self.color = initcolor self.radius = MARKER_THICKNESS[0] @@ -56,8 +57,7 @@ class MarkerDialog(Dialog): def clear_marker(self): canvas = self.app.canvas - for i in canvas.find_withtag("marker"): - canvas.delete(i) + canvas.delete(tags.MARKER) def change_color(self, event: tk.Event): color_picker = ColorPickerDialog(self, self.app, self.color) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 5652fa40..a78386a0 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -534,7 +534,7 @@ class CanvasGraph(tk.Canvas): y + r, fill=self.app.toolbar.marker_tool.color, outline="", - tags="marker", + tags=tags.MARKER, ) return if selected is None: diff --git a/daemon/core/gui/graph/tags.py b/daemon/core/gui/graph/tags.py index 763465b5..45f6d0ee 100644 --- a/daemon/core/gui/graph/tags.py +++ b/daemon/core/gui/graph/tags.py @@ -10,6 +10,7 @@ NODE = "node" WALLPAPER = "wallpaper" SELECTION = "selectednodes" THROUGHPUT = "throughput" +MARKER = "marker" ABOVE_WALLPAPER_TAGS = [ GRIDLINE, SHAPE, @@ -33,4 +34,5 @@ COMPONENT_TAGS = [ SELECTION, SHAPE, SHAPE_TEXT, + MARKER, ] From 52689bd2105602255842f7ecac304378aac1b834 Mon Sep 17 00:00:00 2001 From: Jeff Ahrenholz Date: Wed, 4 Mar 2020 11:23:21 -0800 Subject: [PATCH 0066/1131] fix typo in DEFAULT_TERMS make gnome-terminal work --- daemon/core/gui/coreclient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index e20f1506..04505a07 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -46,7 +46,7 @@ DEFAULT_TERMS = { "konsole": "konsole -e", "lxterminal": "lxterminal -e", "xfce4-terminal": "xfce4-terminal -x", - "gnome-terminal": "gnome-terminal --window--", + "gnome-terminal": "gnome-terminal --window --", } From 0d4a86f10e69a5409822fc5ff1b165b083275936 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 4 Mar 2020 11:38:24 -0800 Subject: [PATCH 0067/1131] updated new gui to properly update modified addresses for nodes, added validation for ip4/ip6, fixed redrawing edge labels when node addresses change --- daemon/core/gui/dialogs/nodeconfig.py | 86 +++++++++++++++++++++++++-- daemon/core/gui/graph/edges.py | 13 +++- daemon/core/gui/graph/node.py | 2 + 3 files changed, 94 insertions(+), 7 deletions(-) diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index 7db65dc7..fcca2896 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -1,9 +1,11 @@ import logging import tkinter as tk from functools import partial -from tkinter import ttk +from tkinter import messagebox, ttk from typing import TYPE_CHECKING +import netaddr + from core.gui import nodeutils from core.gui.appconfig import ICONS_PATH from core.gui.dialogs.dialog import Dialog @@ -18,6 +20,58 @@ if TYPE_CHECKING: from core.gui.graph.node import CanvasNode +def check_ip6(parent, name: str, value: str) -> bool: + title = f"IP6 Error for {name}" + if not value: + messagebox.showerror(title, "Empty Value", parent=parent) + return False + values = value.split("/") + if len(values) != 2: + messagebox.showerror( + title, "Must be in the format address/prefix", parent=parent + ) + return False + addr, mask = values + if not netaddr.valid_ipv6(addr): + messagebox.showerror(title, "Invalid IP6 address", parent=parent) + return False + try: + mask = int(mask) + if not (0 <= mask <= 128): + messagebox.showerror(title, "Mask must be between 0-128", parent=parent) + return False + except ValueError: + messagebox.showerror(title, "Invalid Mask", parent=parent) + return False + return True + + +def check_ip4(parent, name: str, value: str) -> bool: + title = f"IP4 Error for {name}" + if not value: + messagebox.showerror(title, "Empty Value", parent=parent) + return False + values = value.split("/") + if len(values) != 2: + messagebox.showerror( + title, "Must be in the format address/prefix", parent=parent + ) + return False + addr, mask = values + if not netaddr.valid_ipv4(addr): + messagebox.showerror(title, "Invalid IP4 address", parent=parent) + return False + try: + mask = int(mask) + if not (0 <= mask <= 32): + messagebox.showerror(title, "Mask must be between 0-32", parent=parent) + return False + except ValueError: + messagebox.showerror(title, "Invalid mask", parent=parent) + return False + return True + + def mac_auto(is_auto: tk.BooleanVar, entry: ttk.Entry): logging.info("mac auto clicked") if is_auto.get(): @@ -203,7 +257,6 @@ class NodeConfigDialog(Dialog): label.grid(row=row, column=0, padx=PADX, pady=PADY) ip4 = tk.StringVar(value=f"{interface.ip4}/{interface.ip4mask}") entry = ttk.Entry(tab, textvariable=ip4) - entry.bind("", self.app.validation.ip_focus_out) entry.grid(row=row, column=1, columnspan=2, sticky="ew") row += 1 @@ -211,7 +264,6 @@ class NodeConfigDialog(Dialog): label.grid(row=row, column=0, padx=PADX, pady=PADY) ip6 = tk.StringVar(value=f"{interface.ip6}/{interface.ip6mask}") entry = ttk.Entry(tab, textvariable=ip6) - entry.bind("", self.app.validation.ip_focus_out) entry.grid(row=row, column=1, columnspan=2, sticky="ew") self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6) @@ -240,6 +292,8 @@ class NodeConfigDialog(Dialog): self.image_file = file_path def config_apply(self): + error = False + # update core node self.node.name = self.name.get() if NodeUtils.is_image_node(self.node.type): @@ -255,9 +309,31 @@ class NodeConfigDialog(Dialog): # update canvas node self.canvas_node.image = self.image + # update node interface data + for interface in self.canvas_node.interfaces: + data = self.interfaces[interface.id] + if check_ip4(self, interface.name, data.ip4.get()): + ip4, ip4mask = data.ip4.get().split("/") + interface.ip4 = ip4 + interface.ip4mask = int(ip4mask) + else: + error = True + data.ip4.set(f"{interface.ip4}/{interface.ip4mask}") + break + if check_ip6(self, interface.name, data.ip6.get()): + ip6, ip6mask = data.ip6.get().split("/") + interface.ip6 = ip6 + interface.ip6mask = int(ip6mask) + interface.mac = data.mac.get() + else: + error = True + data.ip6.set(f"{interface.ip6}/{interface.ip6mask}") + break + # redraw - self.canvas_node.redraw() - self.destroy() + if not error: + self.canvas_node.redraw() + self.destroy() def interface_select(self, event: tk.Event): listbox = event.widget diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 0659767d..fb5f64eb 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -107,8 +107,7 @@ class CanvasEdge: y = (y1 + y2) / 2 return x, y - def draw_labels(self): - x1, y1, x2, y2 = self.get_coordinates() + def create_labels(self): label_one = None if self.link.HasField("interface_one"): label_one = ( @@ -121,6 +120,11 @@ class CanvasEdge: f"{self.link.interface_two.ip4}/{self.link.interface_two.ip4mask}\n" f"{self.link.interface_two.ip6}/{self.link.interface_two.ip6mask}\n" ) + return label_one, label_two + + def draw_labels(self): + x1, y1, x2, y2 = self.get_coordinates() + label_one, label_two = self.create_labels() self.text_src = self.canvas.create_text( x1, y1, @@ -138,6 +142,11 @@ class CanvasEdge: tags=tags.LINK_INFO, ) + def redraw(self): + label_one, label_two = self.create_labels() + self.canvas.itemconfig(self.text_src, text=label_one) + self.canvas.itemconfig(self.text_dst, text=label_two) + def update_labels(self): """ Move edge labels based on current position. diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 3ed5b1d9..7ae64eac 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -107,6 +107,8 @@ class CanvasNode: def redraw(self): self.canvas.itemconfig(self.id, image=self.image) self.canvas.itemconfig(self.text_id, text=self.core_node.name) + for edge in self.edges: + edge.redraw() def _get_label_y(self): image_box = self.canvas.bbox(self.id) From b72ce6a66cec6a81bc40e1894383a1a5a38ff0b4 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 4 Mar 2020 11:49:09 -0800 Subject: [PATCH 0068/1131] allow editable Edit - Preferences - Terminal --- daemon/core/gui/dialogs/preferences.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index 2f728416..fdd613b5 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -57,10 +57,7 @@ class PreferencesDialog(Dialog): label = ttk.Label(frame, text="Terminal") label.grid(row=2, column=0, pady=PADY, padx=PADX, sticky="w") combobox = ttk.Combobox( - frame, - textvariable=self.terminal, - values=appconfig.TERMINALS, - state="readonly", + frame, textvariable=self.terminal, values=appconfig.TERMINALS ) combobox.grid(row=2, column=1, sticky="ew") From 18d88ab797b9bccbf54f721108fe6227650e223d Mon Sep 17 00:00:00 2001 From: Jeff Ahrenholz Date: Wed, 4 Mar 2020 13:03:03 -0800 Subject: [PATCH 0069/1131] fix #387 launch gnome-terminal properly by removing extra quoting try2 --- daemon/core/gui/coreclient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 04505a07..55488792 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -575,7 +575,7 @@ class CoreClient: output = os.popen(f"echo {terminal}").read()[:-1] if output in DEFAULT_TERMS: terminal = DEFAULT_TERMS[output] - cmd = f'{terminal} "{response.terminal}" &' + cmd = f'{terminal} {response.terminal} &' logging.info("launching terminal %s", cmd) os.system(cmd) except grpc.RpcError as e: From c0d576f26de676a2d526c960db4d8908d517da65 Mon Sep 17 00:00:00 2001 From: Jeff Ahrenholz Date: Wed, 4 Mar 2020 13:11:39 -0800 Subject: [PATCH 0070/1131] fix black pre-commit formatting --- daemon/core/gui/coreclient.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 55488792..cd247a70 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -575,7 +575,8 @@ class CoreClient: output = os.popen(f"echo {terminal}").read()[:-1] if output in DEFAULT_TERMS: terminal = DEFAULT_TERMS[output] - cmd = f'{terminal} {response.terminal} &' + + cmd = f"{terminal} {response.terminal} &" logging.info("launching terminal %s", cmd) os.system(cmd) except grpc.RpcError as e: From 91dae87810e6cc4f479d067de54775faff01179a Mon Sep 17 00:00:00 2001 From: Jeff Ahrenholz Date: Wed, 4 Mar 2020 13:23:09 -0800 Subject: [PATCH 0071/1131] properly kill python3-based core-daemon when using 'core-cleanup -d' --- daemon/scripts/core-cleanup | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/scripts/core-cleanup b/daemon/scripts/core-cleanup index f73275df..8182a917 100755 --- a/daemon/scripts/core-cleanup +++ b/daemon/scripts/core-cleanup @@ -19,7 +19,7 @@ PATH="/sbin:/bin:/usr/sbin:/usr/bin" export PATH if [ "z$1" = "z-d" ]; then - pypids=`pidof python python2` + pypids=`pidof python3 python` for p in $pypids; do grep -q core-daemon /proc/$p/cmdline if [ $? = 0 ]; then From 7dee59e86e4c1fbc941ec802e737b95c786f5060 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 4 Mar 2020 13:25:22 -0800 Subject: [PATCH 0072/1131] New Session command deletes the current session if it is not in runtime else prompt save running session, and then creates the new session --- daemon/core/gui/menuaction.py | 4 ++++ daemon/core/gui/menubar.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/daemon/core/gui/menuaction.py b/daemon/core/gui/menuaction.py index 95699d4c..609367b6 100644 --- a/daemon/core/gui/menuaction.py +++ b/daemon/core/gui/menuaction.py @@ -192,3 +192,7 @@ class MenuAction: logging.error("unexpected number of recent files") self.app.save_config() self.app.menubar.update_recent_files() + + def new_session(self): + self.prompt_save_running_session() + self.app.core.create_new_session() diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index f4c12014..385e0ca1 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -49,7 +49,7 @@ class Menubar(tk.Menu): menu.add_command( label="New Session", accelerator="Ctrl+N", - command=self.app.core.create_new_session, + command=self.menuaction.new_session, ) self.app.bind_all("", lambda e: self.app.core.create_new_session()) menu.add_command( From 34895c1f9c34b3846092c634106fc7a23818a449 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 4 Mar 2020 13:30:01 -0800 Subject: [PATCH 0073/1131] changes for initial gui setup and discovery of the terminal program to use, avoid using TERM env variable --- daemon/core/gui/appconfig.py | 34 ++++++++++++++------------ daemon/core/gui/coreclient.py | 24 +++++++----------- daemon/core/gui/dialogs/preferences.py | 6 ++--- 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/daemon/core/gui/appconfig.py b/daemon/core/gui/appconfig.py index 97c76065..28cd6c0a 100644 --- a/daemon/core/gui/appconfig.py +++ b/daemon/core/gui/appconfig.py @@ -25,17 +25,16 @@ LOCAL_XMLS_PATH = DATA_PATH.joinpath("xmls").absolute() LOCAL_MOBILITY_PATH = DATA_PATH.joinpath("mobility").absolute() # configuration data -TERMINALS = [ - "$TERM", - "gnome-terminal --window --", - "lxterminal -e", - "konsole -e", - "xterm -e", - "aterm -e", - "eterm -e", - "rxvt -e", - "xfce4-terminal -x", -] +TERMINALS = { + "xterm": "xterm -e", + "aterm": "aterm -e", + "eterm": "eterm -e", + "rxvt": "rxvt -e", + "konsole": "konsole -e", + "lxterminal": "lxterminal -e", + "xfce4-terminal": "xfce4-terminal -x", + "gnome-terminal": "gnome-terminal --window --", +} EDITORS = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"] @@ -50,6 +49,14 @@ def copy_files(current_path, new_path): shutil.copy(current_file, new_file) +def find_terminal(): + for term in sorted(TERMINALS): + cmd = TERMINALS[term] + if shutil.which(term): + return cmd + return None + + def check_directory(): if HOME_PATH.exists(): return @@ -66,10 +73,7 @@ def check_directory(): copy_files(LOCAL_XMLS_PATH, XMLS_PATH) copy_files(LOCAL_MOBILITY_PATH, MOBILITY_PATH) - if "TERM" in os.environ: - terminal = TERMINALS[0] - else: - terminal = TERMINALS[1] + terminal = find_terminal() if "EDITOR" in os.environ: editor = EDITORS[0] else: diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 04505a07..377144ed 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -5,6 +5,7 @@ import json import logging import os from pathlib import Path +from tkinter import messagebox from typing import TYPE_CHECKING, Dict, List import grpc @@ -38,17 +39,6 @@ OBSERVERS = { "IPSec policies": "setkey -DP", } -DEFAULT_TERMS = { - "xterm": "xterm -e", - "aterm": "aterm -e", - "eterm": "eterm -e", - "rxvt": "rxvt -e", - "konsole": "konsole -e", - "lxterminal": "lxterminal -e", - "xfce4-terminal": "xfce4-terminal -x", - "gnome-terminal": "gnome-terminal --window --", -} - class CoreServer: def __init__(self, name: str, address: str, port: int): @@ -571,11 +561,15 @@ class CoreClient: def launch_terminal(self, node_id: int): try: terminal = self.app.guiconfig["preferences"]["terminal"] + if not terminal: + messagebox.showerror( + "Terminal Error", + "No terminal set, please set within the preferences menu", + parent=self.app, + ) + return response = self.client.get_node_terminal(self.session_id, node_id) - output = os.popen(f"echo {terminal}").read()[:-1] - if output in DEFAULT_TERMS: - terminal = DEFAULT_TERMS[output] - cmd = f'{terminal} "{response.terminal}" &' + cmd = f"{terminal} {response.terminal} &" logging.info("launching terminal %s", cmd) os.system(cmd) except grpc.RpcError as e: diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index 2f728416..73a4fb3b 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -56,11 +56,9 @@ class PreferencesDialog(Dialog): label = ttk.Label(frame, text="Terminal") label.grid(row=2, column=0, pady=PADY, padx=PADX, sticky="w") + terminals = sorted(appconfig.TERMINALS.values()) combobox = ttk.Combobox( - frame, - textvariable=self.terminal, - values=appconfig.TERMINALS, - state="readonly", + frame, textvariable=self.terminal, values=terminals, state="readonly" ) combobox.grid(row=2, column=1, sticky="ew") From f50c1e4db4c712cc2af4474112c40b22593524e5 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 4 Mar 2020 14:15:02 -0800 Subject: [PATCH 0074/1131] keep track of opened, saved file to appropriately prompt save xml when needed, add Save As menu option --- daemon/core/gui/menuaction.py | 2 ++ daemon/core/gui/menubar.py | 1 + 2 files changed, 3 insertions(+) diff --git a/daemon/core/gui/menuaction.py b/daemon/core/gui/menuaction.py index 609367b6..3d7ee154 100644 --- a/daemon/core/gui/menuaction.py +++ b/daemon/core/gui/menuaction.py @@ -90,6 +90,7 @@ class MenuAction: if file_path: self.add_recent_file_to_gui_config(file_path) self.app.core.save_xml(file_path) + self.app.core.xml_file = file_path def file_open_xml(self, event: tk.Event = None): init_dir = self.app.core.xml_dir @@ -196,3 +197,4 @@ class MenuAction: def new_session(self): self.prompt_save_running_session() self.app.core.create_new_session() + self.app.core.xml_file = None diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 385e0ca1..70198fba 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -57,6 +57,7 @@ class Menubar(tk.Menu): ) self.app.bind_all("", self.menuaction.file_open_xml) menu.add_command(label="Save", accelerator="Ctrl+S", command=self.save) + menu.add_command(label="Save As", command=self.menuaction.file_save_as_xml) menu.add_command(label="Reload", underline=0, state=tk.DISABLED) self.app.bind_all("", self.save) From be37f0f279caa690010353d7e4d8957765659eeb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 4 Mar 2020 14:39:28 -0800 Subject: [PATCH 0075/1131] updates in new gui to allow empty ip4/ip6 addresses, fixed display issues related to empty addresses --- daemon/core/gui/dialogs/nodeconfig.py | 53 ++++++++++++++++++--------- daemon/core/gui/graph/edges.py | 18 +++++---- 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index fcca2896..0f6e56d6 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -21,10 +21,9 @@ if TYPE_CHECKING: def check_ip6(parent, name: str, value: str) -> bool: - title = f"IP6 Error for {name}" if not value: - messagebox.showerror(title, "Empty Value", parent=parent) - return False + return True + title = f"IP6 Error for {name}" values = value.split("/") if len(values) != 2: messagebox.showerror( @@ -47,10 +46,9 @@ def check_ip6(parent, name: str, value: str) -> bool: def check_ip4(parent, name: str, value: str) -> bool: - title = f"IP4 Error for {name}" if not value: - messagebox.showerror(title, "Empty Value", parent=parent) - return False + return True + title = f"IP4 Error for {name}" values = value.split("/") if len(values) != 2: messagebox.showerror( @@ -255,14 +253,20 @@ class NodeConfigDialog(Dialog): label = ttk.Label(tab, text="IPv4") label.grid(row=row, column=0, padx=PADX, pady=PADY) - ip4 = tk.StringVar(value=f"{interface.ip4}/{interface.ip4mask}") + ip4_net = "" + if interface.ip4: + ip4_net = f"{interface.ip4}/{interface.ip4mask}" + ip4 = tk.StringVar(value=ip4_net) entry = ttk.Entry(tab, textvariable=ip4) entry.grid(row=row, column=1, columnspan=2, sticky="ew") row += 1 label = ttk.Label(tab, text="IPv6") label.grid(row=row, column=0, padx=PADX, pady=PADY) - ip6 = tk.StringVar(value=f"{interface.ip6}/{interface.ip6mask}") + ip6_net = "" + if interface.ip6: + ip6_net = f"{interface.ip6}/{interface.ip6mask}" + ip6 = tk.StringVar(value=ip6_net) entry = ttk.Entry(tab, textvariable=ip6) entry.grid(row=row, column=1, columnspan=2, sticky="ew") @@ -312,23 +316,36 @@ class NodeConfigDialog(Dialog): # update node interface data for interface in self.canvas_node.interfaces: data = self.interfaces[interface.id] - if check_ip4(self, interface.name, data.ip4.get()): - ip4, ip4mask = data.ip4.get().split("/") - interface.ip4 = ip4 - interface.ip4mask = int(ip4mask) - else: + + # validate ip4 + ip4_net = data.ip4.get() + if not check_ip4(self, interface.name, ip4_net): error = True data.ip4.set(f"{interface.ip4}/{interface.ip4mask}") break - if check_ip6(self, interface.name, data.ip6.get()): - ip6, ip6mask = data.ip6.get().split("/") - interface.ip6 = ip6 - interface.ip6mask = int(ip6mask) - interface.mac = data.mac.get() + if ip4_net: + ip4, ip4mask = ip4_net.split("/") + ip4mask = int(ip4mask) else: + ip4, ip4mask = "", 0 + interface.ip4 = ip4 + interface.ip4mask = ip4mask + + # validate ip6 + ip6_net = data.ip6.get() + if not check_ip6(self, interface.name, ip6_net): error = True data.ip6.set(f"{interface.ip6}/{interface.ip6mask}") break + if ip6_net: + ip6, ip6mask = ip6_net.split("/") + ip6mask = int(ip6mask) + else: + ip6, ip6mask = "", 0 + interface.ip6 = ip6 + interface.ip6mask = ip6mask + + interface.mac = data.mac.get() # redraw if not error: diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index fb5f64eb..37b1a96e 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -110,18 +110,20 @@ class CanvasEdge: def create_labels(self): label_one = None if self.link.HasField("interface_one"): - label_one = ( - f"{self.link.interface_one.ip4}/{self.link.interface_one.ip4mask}\n" - f"{self.link.interface_one.ip6}/{self.link.interface_one.ip6mask}\n" - ) + label_one = self.create_label(self.link.interface_one) label_two = None if self.link.HasField("interface_two"): - label_two = ( - f"{self.link.interface_two.ip4}/{self.link.interface_two.ip4mask}\n" - f"{self.link.interface_two.ip6}/{self.link.interface_two.ip6mask}\n" - ) + label_two = self.create_label(self.link.interface_two) return label_one, label_two + def create_label(self, interface): + label = "" + if interface.ip4: + label = f"{interface.ip4}/{interface.ip4mask}" + if interface.ip6: + label = f"{label}\n{interface.ip6}/{interface.ip6mask}" + return label + def draw_labels(self): x1, y1, x2, y2 = self.get_coordinates() label_one, label_two = self.create_labels() From c4234d33f007d4ad593220006bf9944539e4b300 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 4 Mar 2020 20:09:56 -0800 Subject: [PATCH 0076/1131] updates to allow new gui to recreate session to continue where it left off --- daemon/core/api/grpc/client.py | 10 ++++++++ daemon/core/api/grpc/server.py | 13 ++++++++++ daemon/core/gui/coreclient.py | 36 ++++++++++++++++++++------- daemon/proto/core/api/grpc/core.proto | 10 ++++++++ 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index e90f6ead..cad8b2f2 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -262,6 +262,16 @@ class CoreGrpcClient: """ return self.stub.GetSessions(core_pb2.GetSessionsRequest()) + def check_session(self, session_id: int) -> core_pb2.CheckSessionResponse: + """ + Check if a session exists. + + :param session_id: id of session to check for + :return: response with result if session was found + """ + request = core_pb2.CheckSessionRequest(session_id=session_id) + return self.stub.CheckSession(request) + def get_session(self, session_id: int) -> core_pb2.GetSessionResponse: """ Retrieve a session. diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index ae79cc95..c71800f4 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -453,6 +453,19 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session.metadata = dict(request.config) return core_pb2.SetSessionMetadataResponse(result=True) + def CheckSession( + self, request: core_pb2.GetSessionRequest, context: ServicerContext + ) -> core_pb2.CheckSessionResponse: + """ + Checks if a session exists. + + :param request: check session request + :param context: context object + :return: check session response + """ + result = request.session_id in self.coreemu.sessions + return core_pb2.CheckSessionResponse(result=result) + def GetSession( self, request: core_pb2.GetSessionRequest, context: ServicerContext ) -> core_pb2.GetSessionResponse: diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index c4d1a128..f6dfd71a 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -58,7 +58,7 @@ class CoreClient: """ Create a CoreGrpc instance """ - self.client = client.CoreGrpcClient(proxy=proxy) + self._client = client.CoreGrpcClient(proxy=proxy) self.session_id = None self.node_ids = [] self.app = app @@ -102,6 +102,22 @@ class CoreClient: self.modified_service_nodes = set() + @property + def client(self): + if self.session_id: + response = self._client.check_session(self.session_id) + if not response.result: + throughputs_enabled = self.handling_throughputs is not None + self.cancel_throughputs() + self.cancel_events() + self._client.create_session(self.session_id) + self.handling_events = self._client.events( + self.session_id, self.handle_events + ) + if throughputs_enabled: + self.enable_throughputs() + return self._client + def reset(self): # helpers self.interfaces_manager.reset() @@ -121,12 +137,8 @@ class CoreClient: mobility_player.handle_close() self.mobility_players.clear() # clear streams - if self.handling_throughputs: - self.handling_throughputs.cancel() - self.handling_throughputs = None - if self.handling_events: - self.handling_events.cancel() - self.handling_events = None + self.cancel_throughputs() + self.cancel_events() def set_observer(self, value: str): self.observer = value @@ -217,8 +229,14 @@ class CoreClient: ) def cancel_throughputs(self): - self.handling_throughputs.cancel() - self.handling_throughputs = None + if self.handling_throughputs: + self.handling_throughputs.cancel() + self.handling_throughputs = None + + def cancel_events(self): + if self.handling_events: + self.handling_events.cancel() + self.handling_events = None def handle_throughputs(self, event: core_pb2.ThroughputsEvent): if event.session_id != self.session_id: diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 53d5c602..0a751ddf 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -22,6 +22,8 @@ service CoreApi { } rpc GetSession (GetSessionRequest) returns (GetSessionResponse) { } + rpc CheckSession (CheckSessionRequest) returns (CheckSessionResponse) { + } rpc GetSessionOptions (GetSessionOptionsRequest) returns (GetSessionOptionsResponse) { } rpc SetSessionOptions (SetSessionOptionsRequest) returns (SetSessionOptionsResponse) { @@ -212,6 +214,14 @@ message GetSessionsResponse { repeated SessionSummary sessions = 1; } +message CheckSessionRequest { + int32 session_id = 1; +} + +message CheckSessionResponse { + bool result = 1; +} + message GetSessionRequest { int32 session_id = 1; } From f826a4c5e8b854f9f092365f688b5043affb3c98 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 4 Mar 2020 20:42:40 -0800 Subject: [PATCH 0077/1131] new gui fixed error display when daemon is not running --- daemon/core/gui/app.py | 2 +- daemon/core/gui/coreclient.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 10975eef..195f0e91 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -44,7 +44,7 @@ class Application(tk.Frame): self.core = CoreClient(self, proxy) self.setup_app() self.draw() - self.core.set_up() + self.core.setup() def setup_scaling(self): self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()} diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index f6dfd71a..7c7f8378 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -446,7 +446,7 @@ class CoreClient: master = parent_frame self.app.after(0, show_grpc_error, e, master, self.app) - def set_up(self): + def setup(self): """ Query sessions, if there exist any, prompt whether to join one """ @@ -480,7 +480,7 @@ class CoreClient: x.node_type: set(x.services) for x in response.defaults } except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e, self.app, self.app) + show_grpc_error(e, self.app, self.app) self.app.close() def edit_node(self, core_node: core_pb2.Node): From 105dd4ad7b93c27fe12acbba6f19832bdf5e9e60 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 5 Mar 2020 11:44:13 -0800 Subject: [PATCH 0078/1131] small tweaks to fix/cleanup install.sh --- install.sh | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/install.sh b/install.sh index 629079bc..7d57a2b9 100755 --- a/install.sh +++ b/install.sh @@ -7,10 +7,6 @@ function install_python_depencencies() { sudo python3 -m pip install -r daemon/requirements.txt } -function install_python_dev_dependencies() { - sudo python3 -m pip install pipenv grpcio-tools -} - function install_ospf_mdr() { rm -rf /tmp/ospf-mdr git clone https://github.com/USNavalResearchLaboratory/ospf-mdr /tmp/ospf-mdr @@ -42,8 +38,6 @@ function install_dev_core() { sudo make install cd - cd daemon - pipenv install --dev - cd - } # detect os/ver for install type @@ -70,8 +64,12 @@ shift $((OPTIND - 1)) case ${os} in "ubuntu") echo "Installing CORE for Ubuntu" - sudo apt install -y automake pkg-config gcc libev-dev ebtables gawk iproute2 \ - python3.6 python3.6-dev python3-pip python3-tk tk libtk-img ethtool libtool libreadline-dev autoconf + echo "installing core system dependencies" + sudo apt install -y automake pkg-config gcc libev-dev ebtables iproute2 \ + python3.6 python3.6-dev python3-pip python3-tk tk libtk-img ethtool autoconf + python3 -m pip install grpcio-tools + echo "installing ospf-mdr system dependencies" + sudo apt install -y libtool gawk libreadline-dev install_ospf_mdr if [[ -z ${dev} ]]; then echo "normal install" @@ -80,23 +78,32 @@ case ${os} in install_core else echo "dev install" - install_python_dev_dependencies + python3 -m pip install pipenv build_core install_dev_core + python3 -m pipenv install --dev fi ;; "centos") + echo "Installing CORE for CentOS" + echo "installing core system dependencies" sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel iptables-ebtables iproute \ - python36 python36-devel python3-pip python3-tkinter tk ethtool libtool readline-devel autoconf gawk + python36 python36-devel python3-pip python3-tkinter tk ethtool autoconf + python3 -m pip install grpcio-tools + echo "installing ospf-mdr system dependencies" + sudo apt install -y libtool gawk readline-devel install_ospf_mdr if [[ -z ${dev} ]]; then + echo "normal install" install_python_depencencies build_core --prefix=/usr install_core else - install_python_dev_dependencies + echo "dev install" + sudo python3 -m pip install pipenv build_core --prefix=/usr install_dev_core + sudo python3 -m pipenv install --dev fi ;; *) From e5a446d70fbf1bb2719c66cbd1d1c22f0877a372 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 5 Mar 2020 12:13:38 -0800 Subject: [PATCH 0079/1131] fixed using apt instead of yum and sudo needed for centos in install.sh --- install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 7d57a2b9..8a6a6cc8 100755 --- a/install.sh +++ b/install.sh @@ -89,9 +89,9 @@ case ${os} in echo "installing core system dependencies" sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel iptables-ebtables iproute \ python36 python36-devel python3-pip python3-tkinter tk ethtool autoconf - python3 -m pip install grpcio-tools + sudo python3 -m pip install grpcio-tools echo "installing ospf-mdr system dependencies" - sudo apt install -y libtool gawk readline-devel + sudo yum install -y libtool gawk readline-devel install_ospf_mdr if [[ -z ${dev} ]]; then echo "normal install" From 5c52fbbdecb2f3c7483ce2f4c4cfc88a468de8a1 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 5 Mar 2020 12:58:42 -0800 Subject: [PATCH 0080/1131] update install.sh to only use pipenv sync to avoid package changes, added user/root installations of pipenv for centos dev environment --- install.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 8a6a6cc8..5ba13265 100755 --- a/install.sh +++ b/install.sh @@ -81,7 +81,7 @@ case ${os} in python3 -m pip install pipenv build_core install_dev_core - python3 -m pipenv install --dev + python3 -m pipenv sync --dev fi ;; "centos") @@ -103,7 +103,8 @@ case ${os} in sudo python3 -m pip install pipenv build_core --prefix=/usr install_dev_core - sudo python3 -m pipenv install --dev + sudo python3 -m pipenv sync --dev + python3 -m pipenv sync --dev fi ;; *) From d8cf1373da167b14bb983f0826a264aa9439fb5b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 5 Mar 2020 13:19:20 -0800 Subject: [PATCH 0081/1131] updates to allow install.sh to use newer versions of python, defaults to 3.6 --- install.sh | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/install.sh b/install.sh index 5ba13265..c930a4d3 100755 --- a/install.sh +++ b/install.sh @@ -3,6 +3,9 @@ # exit on error set -e +ubuntu_py=3.6 +centos_py=36 + function install_python_depencencies() { sudo python3 -m pip install -r daemon/requirements.txt } @@ -48,13 +51,18 @@ if [[ -f /etc/os-release ]]; then fi # parse arguments -while getopts ":d" opt; do +while getopts "dv:" opt; do case ${opt} in d) dev=1 ;; + v) + ubuntu_py=${OPTARG} + centos_py=${OPTARG} + ;; \?) - echo "Invalid Option: $OPTARG" 1>&2 + echo "script usage: $(basename $0) [-d] [-v python version]" >&2 + exit 1 ;; esac done @@ -66,7 +74,7 @@ case ${os} in echo "Installing CORE for Ubuntu" echo "installing core system dependencies" sudo apt install -y automake pkg-config gcc libev-dev ebtables iproute2 \ - python3.6 python3.6-dev python3-pip python3-tk tk libtk-img ethtool autoconf + python${ubuntu_py} python${ubuntu_py}-dev python3-pip python3-tk tk libtk-img ethtool autoconf python3 -m pip install grpcio-tools echo "installing ospf-mdr system dependencies" sudo apt install -y libtool gawk libreadline-dev @@ -88,7 +96,7 @@ case ${os} in echo "Installing CORE for CentOS" echo "installing core system dependencies" sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel iptables-ebtables iproute \ - python36 python36-devel python3-pip python3-tkinter tk ethtool autoconf + python${centos_py} python${centos_py}-devel python3-pip python3-tkinter tk ethtool autoconf sudo python3 -m pip install grpcio-tools echo "installing ospf-mdr system dependencies" sudo yum install -y libtool gawk readline-devel From 81382f2899879b0ffeaea9596f26abbcdb02a276 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 5 Mar 2020 13:44:32 -0800 Subject: [PATCH 0082/1131] updated install.sh dev install to setup pre-commit hook as well --- install.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install.sh b/install.sh index c930a4d3..6bd16a71 100755 --- a/install.sh +++ b/install.sh @@ -90,6 +90,7 @@ case ${os} in build_core install_dev_core python3 -m pipenv sync --dev + python3 -m pipenv run pre-commit install fi ;; "centos") @@ -113,6 +114,7 @@ case ${os} in install_dev_core sudo python3 -m pipenv sync --dev python3 -m pipenv sync --dev + python3 -m pipenv run pre-commit install fi ;; *) From eb030aaca7129cfff98638c61c929f8af06110fb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 5 Mar 2020 14:16:14 -0800 Subject: [PATCH 0083/1131] updated devguide to note using install.sh and clear up needing to maintain duplicate content --- docs/devguide.md | 91 +++++++++++++----------------------------------- 1 file changed, 25 insertions(+), 66 deletions(-) diff --git a/docs/devguide.md b/docs/devguide.md index a6fe970c..6e42c774 100644 --- a/docs/devguide.md +++ b/docs/devguide.md @@ -19,84 +19,43 @@ Current development focuses on the Python modules and daemon. Here is a brief de ## Getting started -Overview for setting up the pipenv environment, building core, installing the GUI and netns, then running -the core-daemon for development based on Ubuntu 18.04. +To setup CORE for develop we will leverage to automated install script. -### Install Dependencies -```shell -sudo apt install -y automake pkg-config gcc libev-dev ebtables gawk \ - python3.6 python3.6-dev python3-pip python3-tk tk libtk-img ethtool libtool libreadline-dev autoconf -``` - -### Install OSPF MDR - -```shell -cd ~/Documents -git clone https://github.com/USNavalResearchLaboratory/ospf-mdr -cd ospf-mdr -./bootstrap.sh -./configure --disable-doc --enable-user=root --enable-group=root --with-cflags=-ggdb \ - --sysconfdir=/usr/local/etc/quagga --enable-vtysh \ - --localstatedir=/var/run/quagga -make -sudo make install -``` - -### Clone CORE Repo +## Clone CORE Repo ```shell cd ~/Documents git clone https://github.com/coreemu/core.git cd core +git checkout develop ``` -### Build CORE +## Install the Development Environment + +This command will automatically install system dependencies, clone and build OSPF-MDR, +build CORE, setup the CORE pipenv environment, and install pre-commit hooks. + +This script is currently compatible with Ubuntu and CentOS, tested on Ubuntu 18.04 and +CentOS 7.6. The script also currently defaults to using python3.6, but a different +version of python can be targeted if python3.6 is not available on your system. ```shell -./bootstrap.sh -./configure -make -j8 +# default dev install using python3.6 +./install.sh -d + +# providing a newer python version for ubuntu +./install.sh -d -v 3.7 + +# providing a newer python version for centos +./install.sh -d -v 37 ``` -### Install netns and GUI +### pre-commit -Install legacy GUI if desired and mandatory netns executables. - -```shell -# install GUI -cd $REPO/gui -sudo make install - -# install netns scripts -cd $REPO/netns -sudo make install -``` - -### Setup Python Environment - -To leverage the dev environment you need python 3.6+. - -```shell -# change to daemon directory -cd $REPO/daemon - -# install pipenv -sudo python3 -m pip install pipenv - -# setup a virtual environment and install all required development dependencies -python3 -m pipenv install --dev -``` - -### Setup pre-commit - -Install pre-commit hooks to help automate running tool checks against code. Once installed every time a commit is made -python utilities will be ran to check validity of code, potentially failing and backing out the commit. This allows -one to review changes being made by tools ro the fix the issue noted. Then add the changes and commit again. - -```shell -python3 -m pipenv run pre-commit install -``` +pre-commit hooks help automate running tools to check modified code. Every time a commit is made +python utilities will be ran to check validity of code, potentially failing and backing out the commit. +These changes are currently mandated as part of the current CI, so add the changes and commit again. ### Adding EMANE to Pipenv @@ -121,9 +80,9 @@ make -j8 python3 -m pipenv pip install $EMANEREPO/src/python ``` -### Running CORE +## Running CORE -This will run the core-daemon server using the configuration files within the repo. +Commands below can be used to run the core-daemon, the new core gui, and tests. ```shell # runs for daemon From 595e77a1ef74151b690661e23b95e9564fe390f2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 5 Mar 2020 15:04:29 -0800 Subject: [PATCH 0084/1131] updates to install doc --- docs/install.md | 167 ++++++++++++++++++++++++------------------------ 1 file changed, 83 insertions(+), 84 deletions(-) diff --git a/docs/install.md b/docs/install.md index 11546698..23d9d851 100644 --- a/docs/install.md +++ b/docs/install.md @@ -3,30 +3,30 @@ * Table of Contents {:toc} -# Overview +## Overview This section will describe how to install CORE from source or from a pre-built package. -# Required Hardware +## Required Hardware Any computer capable of running Linux should be able to run CORE. Since the physical machine will be hosting numerous virtual machines, as a general rule you should select a machine having as much RAM and CPU resources as possible. -# Operating System +## Operating System CORE requires a Linux operating system because it uses virtualization provided by the kernel. It does not run on Windows or Mac OS X operating systems (unless it is running within a virtual machine guest.) The virtualization technology that CORE currently uses is Linux network namespaces. -Ubuntu and Fedora/CentOS Linux are the recommended distributions for running CORE. However, these distributions are +Ubuntu and CentOS Linux are the recommended distributions for running CORE. However, these distributions are not strictly required. CORE will likely work on other flavors of Linux as well, assuming dependencies are met. **NOTE: CORE Services determine what run on each node. You may require other software packages depending on the services you wish to use. For example, the HTTP service will require the apache2 package.** -# Installed Files +## Installed Files -CORE files are installed to the following directories, when the installation prefix is **/usr**. +CORE files are installed to the following directories by default, when the installation prefix is **/usr**. Install Path | Description -------------|------------ @@ -43,27 +43,35 @@ Install Path | Description /etc/init.d/core-daemon|SysV startup script for daemon /usr/lib/systemd/system/core-daemon.service|Systemd startup script for daemon -# Pre-Req Installing Python +## Pre-Req Installing Python -You may already have these installed, and can ignore this step if so, but if - needed you can run the following to install python and pip. +Python 3.6 is the minimum required python version. Newer versions can be used if available. +These steps are needed, since the system packages can not provide all the +dependencies needed by CORE. + +### Ubuntu ```shell sudo apt install python3.6 sudo apt install python3-pip ``` -# Pre-Req Python Requirements - -The newly added gRPC API which depends on python library grpcio is not commonly found within system repos. -To account for this it would be recommended to install the python dependencies using the **requirements.txt** found in -the latest [CORE Release](https://github.com/coreemu/core/releases). +### CentOS ```shell -sudo pip3 install -r requirements.txt +sudo yum install python36 +sudo yum install python3-pip ``` -# Pre-Req Installing OSPF MDR +### Dependencies + +Install the current python dependencies. + +```shell +sudo python3 -m pip install -r requirements.txt +``` + +## Pre-Req Installing OSPF MDR Virtual networks generally require some form of routing in order to work (e.g. to automatically populate routing tables for routing packets from one subnet to another.) CORE builds OSPF routing protocol configurations by @@ -73,23 +81,21 @@ default when the blue router node type is used. suite with a modified version of OSPFv3, optimized for use with mobile wireless networks. The **mdr** node type (and the MDR service) requires this variant of Quagga. -## Ubuntu <= 16.04 and Fedora/CentOS - -There is a built package which can be used. +### Ubuntu ```shell -wget https://github.com/USNavalResearchLaboratory/ospf-mdr/releases/download/v0.99.21mr2.2/quagga-mr_0.99.21mr2.2_amd64.deb -sudo dpkg -i quagga-mr_0.99.21mr2.2_amd64.deb +sudo apt install libtool gawk libreadline-dev ``` -## Ubuntu >= 18.04 - -Requires building from source, from the latest nightly snapshot. +### CentOS ```shell -# packages needed beyond what's normally required to build core on ubuntu -sudo apt install libtool libreadline-dev autoconf gawk +sudo yum install libtool gawk readline-devel +``` +### Build and Install + +```shell git clone https://github.com/USNavalResearchLaboratory/ospf-mdr cd ospf-mdr ./bootstrap.sh @@ -112,14 +118,14 @@ error while loading shared libraries libzebra.so.0 this is usually a sign that you have to run ```sudo ldconfig```` to refresh the cache file. -# Installing from Packages +## Installing from Packages -The easiest way to install CORE is using the pre-built packages. The package managers on Ubuntu or Fedora/CentOS +The easiest way to install CORE is using the pre-built packages. The package managers on Ubuntu or CentOS will help in automatically installing most dependencies, except for the python ones described previously. You can obtain the CORE packages from [CORE Releases](https://github.com/coreemu/core/releases). -## Ubuntu +### Ubuntu Ubuntu package defaults to using systemd for running as a service. @@ -127,16 +133,7 @@ Ubuntu package defaults to using systemd for running as a service. sudo apt install ./core_$VERSION_amd64.deb ``` -Run the CORE GUI as a normal user: - -```shell -core-gui -``` - -After running the *core-gui* command, a GUI should appear with a canvas for drawing topologies. -Messages will print out on the console about connecting to the CORE daemon. - -## Fedora/CentOS +### CentOS **NOTE: tkimg is not required for the core-gui, but if you get an error message about it you can install the package on CentOS <= 6, or build from source otherwise** @@ -153,10 +150,6 @@ SELINUX=disabled # add the following to the kernel line in /etc/grub.conf selinux=0 - -# Fedora 15 and newer, disable sandboxd -# reboot in order for this change to take effect -chkconfig sandbox off ``` Turn off firewalls: @@ -176,63 +169,46 @@ iptables -F ip6tables -F ``` -Start the CORE daemon. +## Installing from Source + +Steps for building from cloned source code. Python 3.6 is the minimum required version +a newer version can be used below if available. + +### Distro Requirements + +System packages required to build from source. + +#### Ubuntu ```shell -# systemd -sudo systemctl daemon-reload -sudo systemctl start core-daemon - -# sysv -sudo service core-daemon start +sudo apt install git automake pkg-config gcc libev-dev ebtables iproute2 \ + python3.6 python3.6-dev python3-pip python3-tk tk libtk-img ethtool autoconf ``` -Run the CORE GUI as a normal user: +#### CentOS ```shell -core-gui +sudo yum install git automake pkgconf-pkg-config gcc gcc-c++ libev-devel iptables-ebtables iproute \ + python36 python36-devel python3-pip python3-tkinter tk ethtool autoconf ``` -After running the *core-gui* command, a GUI should appear with a canvas for drawing topologies. Messages will print out on the console about connecting to the CORE daemon. +### Clone Repository -# Building and Installing from Source - -This option is listed here for developers and advanced users who are comfortable patching and building source code. -Please consider using the binary packages instead for a simplified install experience. - -## Download and Extract Source Code - -You can obtain the CORE source from the [CORE GitHub](https://github.com/coreemu/core) page. - -## Install grpcio-tools - -Python module grpcio-tools is currently needed to generate code from the CORE protobuf file during the build. +Clone the CORE repository for building from source. ```shell -sudo pip3 install grpcio-tools +git clone https://github.com/coreemu/core.git ``` -## Distro Requirements +### Install grpcio-tools -### Ubuntu 18.04 Requirements +Python module grpcio-tools is currently needed to generate gRPC protobuf code. ```shell -sudo apt install automake pkg-config gcc iproute2 libev-dev ebtables python3.6 python3.6-dev python3-pip tk libtk-img ethtool python3-tk +sudo python3 -m pip install grpcio-tools ``` -### Ubuntu 16.04 Requirements - -```shell -sudo apt-get install automake ebtables python3-dev libev-dev python3-setuptools libtk-img ethtool -``` - -### CentOS 7 with Gnome Desktop Requirements - -```shell -sudo yum -y install automake gcc python36 python36-devel libev-devel tk ethtool iptables-ebtables iproute python3-pip python3-tkinter -``` - -## Build and Install +### Build and Install ```shell ./bootstrap.sh @@ -241,7 +217,7 @@ make sudo make install ``` -# Building Documentation +## Building Documentation Building documentation requires python-sphinx not noted above. @@ -254,7 +230,7 @@ sudo yum install python3-sphinx make doc ``` -# Building Packages +## Building Packages Build package commands, DESTDIR is used to make install into and then for packaging by fpm. **NOTE: clean the DESTDIR if re-using the same directory** @@ -270,3 +246,26 @@ make fpm DESTDIR=/tmp/core-build ``` This will produce and RPM and Deb package for the currently configured python version. + +## Running CORE + +Start the CORE daemon. + +```shell +# systemd +sudo systemctl daemon-reload +sudo systemctl start core-daemon + +# sysv +sudo service core-daemon start +``` + +Run the GUI + +```shell +# default gui +core-gui + +# new beta gui +coretk-gui +``` From 6b5cd95ac244d02b950f7b95341d0de6eb26bc66 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 5 Mar 2020 21:38:52 -0800 Subject: [PATCH 0085/1131] small updates to new gui exception dialog, fixed error checking and setting interface mac addresses --- daemon/core/gui/dialogs/nodeconfig.py | 21 +++++++++-- daemon/core/gui/dialogs/nodeconfigservice.py | 4 ++- daemon/core/gui/dialogs/nodeservice.py | 5 +-- daemon/core/gui/errors.py | 37 +++++++++++++------- daemon/core/utils.py | 2 +- 5 files changed, 49 insertions(+), 20 deletions(-) diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index 0f6e56d6..d77c69c4 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -240,12 +240,17 @@ class NodeConfigDialog(Dialog): label = ttk.Label(tab, text="MAC") label.grid(row=row, column=0, padx=PADX, pady=PADY) - is_auto = tk.BooleanVar(value=True) + auto_set = not interface.mac + if auto_set: + state = tk.DISABLED + else: + state = tk.NORMAL + is_auto = tk.BooleanVar(value=auto_set) checkbutton = ttk.Checkbutton(tab, text="Auto?", variable=is_auto) checkbutton.var = is_auto checkbutton.grid(row=row, column=1, padx=PADX) mac = tk.StringVar(value=interface.mac) - entry = ttk.Entry(tab, textvariable=mac, state=tk.DISABLED) + entry = ttk.Entry(tab, textvariable=mac, state=state) entry.grid(row=row, column=2, sticky="ew") func = partial(mac_auto, is_auto, entry) checkbutton.config(command=func) @@ -345,7 +350,17 @@ class NodeConfigDialog(Dialog): interface.ip6 = ip6 interface.ip6mask = ip6mask - interface.mac = data.mac.get() + mac = data.mac.get() + if mac and not netaddr.valid_mac(mac): + title = f"MAC Error for {interface.name}" + messagebox.showerror(title, "Invalid MAC Address") + error = True + data.mac.set(interface.mac) + break + else: + mac = netaddr.EUI(mac) + mac.dialect = netaddr.mac_unix_expanded + interface.mac = str(mac) # redraw if not error: diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeconfigservice.py index e41290ed..8bdbc539 100644 --- a/daemon/core/gui/dialogs/nodeconfigservice.py +++ b/daemon/core/gui/dialogs/nodeconfigservice.py @@ -128,7 +128,9 @@ class NodeConfigServiceDialog(Dialog): dialog.show() else: messagebox.showinfo( - "Node service configuration", "Select a service to configure" + "Config Service Configuration", + "Select a service to configure", + parent=self, ) def click_save(self): diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index 9a289aeb..691bd331 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -148,11 +148,12 @@ class NodeServiceDialog(Dialog): dialog.destroy() else: messagebox.showinfo( - "Node service configuration", "Select a service to configure" + "Service Configuration", "Select a service to configure", parent=self ) def click_save(self): - # if node is custom type or current services are not the default services then set core node services and add node to modified services node set + # if node is custom type or current services are not the default services then + # set core node services and add node to modified services node set if ( self.canvas_node.core_node.model not in self.app.core.default_services or self.current_services diff --git a/daemon/core/gui/errors.py b/daemon/core/gui/errors.py index 51c90e35..1f9353d8 100644 --- a/daemon/core/gui/errors.py +++ b/daemon/core/gui/errors.py @@ -1,36 +1,49 @@ from tkinter import ttk from typing import TYPE_CHECKING +import grpc + from core.gui.dialogs.dialog import Dialog from core.gui.images import ImageEnum, Images +from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CodeText if TYPE_CHECKING: - import grpc from core.gui.app import Application class ErrorDialog(Dialog): - def __init__(self, master, app: "Application", title: str, details: str): - super().__init__(master, app, title, modal=True) - self.error_message = None + def __init__(self, master, app: "Application", title: str, details: str) -> None: + super().__init__(master, app, "CORE Exception", modal=True) + self.title = title self.details = details + self.error_message = None self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) - self.top.rowconfigure(0, weight=1) + self.top.rowconfigure(1, weight=1) + + frame = ttk.Frame(self.top, padding=FRAME_PAD) + frame.grid(pady=PADY, sticky="ew") + frame.columnconfigure(1, weight=1) image = Images.get(ImageEnum.ERROR, 36) - label = ttk.Label(self.top, image=image) + label = ttk.Label(frame, image=image) label.image = image - label.grid(row=0, column=0) + label.grid(row=0, column=0, padx=PADX) + label = ttk.Label(frame, text=self.title) + label.grid(row=0, column=1, sticky="ew") + self.error_message = CodeText(self.top) self.error_message.text.insert("1.0", self.details) self.error_message.text.config(state="disabled") - self.error_message.grid(row=1, column=0, sticky="nsew") + self.error_message.grid(sticky="nsew", pady=PADY) + + button = ttk.Button(self.top, text="Close", command=lambda: self.destroy()) + button.grid(sticky="ew") -def show_grpc_error(e: "grpc.RpcError", master, app: "Application"): +def show_grpc_error(e: grpc.RpcError, master, app: "Application"): title = [x.capitalize() for x in e.code().name.lower().split("_")] title = " ".join(title) title = f"GRPC {title}" @@ -40,8 +53,6 @@ def show_grpc_error(e: "grpc.RpcError", master, app: "Application"): def show_grpc_response_exceptions(class_name, exceptions, master, app: "Application"): title = f"Exceptions from {class_name}" - detail = "" - for e in exceptions: - detail = detail + f"{e}\n" + detail = "\n".join([str(x) for x in exceptions]) dialog = ErrorDialog(master, app, title, detail) dialog.show() diff --git a/daemon/core/utils.py b/daemon/core/utils.py index 57e95a4f..7a8b42b8 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -444,7 +444,7 @@ def random_mac() -> str: value = random.randint(0, 0xFFFFFF) value |= 0x00163E << 24 mac = netaddr.EUI(value) - mac.dialect = netaddr.mac_unix + mac.dialect = netaddr.mac_unix_expanded return str(mac) From 0e299d5af455014ae3047614f040a8204b503bd4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Mar 2020 16:41:26 -0800 Subject: [PATCH 0086/1131] update to make use of shutil.which for executable searching --- daemon/core/utils.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/daemon/core/utils.py b/daemon/core/utils.py index 7a8b42b8..0eb9fef1 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -13,6 +13,7 @@ import logging.config import os import random import shlex +import shutil import sys from subprocess import PIPE, STDOUT, Popen from typing import ( @@ -151,16 +152,9 @@ def which(command: str, required: bool) -> str: :return: command location or None :raises ValueError: when not found and required """ - found_path = None - for path in os.environ["PATH"].split(os.pathsep): - command_path = os.path.join(path, command) - if os.path.isfile(command_path) and os.access(command_path, os.X_OK): - found_path = command_path - break - + found_path = shutil.which(command) if found_path is None and required: raise ValueError(f"failed to find required executable({command}) in path") - return found_path From 1e8d1ecd9f954d8ee028e9c6334ab9f1792486df Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Mar 2020 22:35:23 -0800 Subject: [PATCH 0087/1131] changes for sessions to use EventTypes for state/hooks directly --- daemon/core/api/grpc/events.py | 2 +- daemon/core/api/grpc/server.py | 18 ++++--- daemon/core/api/tlv/corehandlers.py | 30 +++++------ daemon/core/emulator/enumerations.py | 3 ++ daemon/core/emulator/session.py | 78 +++++++++++++++------------- daemon/core/location/mobility.py | 20 +++---- daemon/core/plugins/sdt.py | 2 +- daemon/core/xml/corexml.py | 12 ++--- daemon/data/logging.conf | 2 +- daemon/tests/test_grpc.py | 10 ++-- daemon/tests/test_gui.py | 6 +-- daemon/tests/test_xml.py | 7 +-- 12 files changed, 95 insertions(+), 95 deletions(-) diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index 172cec82..cf98a9a2 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -102,7 +102,7 @@ def handle_session_event(event: EventData) -> core_pb2.SessionEvent: event_time = float(event_time) return core_pb2.SessionEvent( node_id=event.node, - event=event.event_type, + event=event.event_type.value, name=event.name, data=event.data, time=event_time, diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index c71800f4..4c8d6640 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -173,7 +173,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): # add all hooks for hook in request.hooks: - session.add_hook(hook.state, hook.file, None, hook.data) + state = EventTypes(hook.state) + session.add_hook(state, hook.file, None, hook.data) # create nodes _, exceptions = grpcutils.create_nodes(session, request.nodes) @@ -279,7 +280,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session.location.setrefgeo(47.57917, -122.13232, 2.0) session.location.refscale = 150000.0 return core_pb2.CreateSessionResponse( - session_id=session.id, state=session.state + session_id=session.id, state=session.state.value ) def DeleteSession( @@ -312,7 +313,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session = self.coreemu.sessions[session_id] session_summary = core_pb2.SessionSummary( id=session_id, - state=session.state, + state=session.state.value, nodes=session.get_node_count(), file=session.file_name, ) @@ -521,7 +522,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): node_links = get_links(session, node) links.extend(node_links) - session_proto = core_pb2.Session(state=session.state, nodes=nodes, links=links) + session_proto = core_pb2.Session( + state=session.state.value, nodes=nodes, links=links + ) return core_pb2.GetSessionResponse(session=session_proto) def AddSessionServer( @@ -896,7 +899,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): for state in session._hooks: state_hooks = session._hooks[state] for file_name, file_data in state_hooks: - hook = core_pb2.Hook(state=state, file=file_name, data=file_data) + hook = core_pb2.Hook(state=state.value, file=file_name, data=file_data) hooks.append(hook) return core_pb2.GetHooksResponse(hooks=hooks) @@ -913,7 +916,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("add hook: %s", request) session = self.get_session(request.session_id, context) hook = request.hook - session.add_hook(hook.state, hook.file, None, hook.data) + state = EventTypes(hook.state) + session.add_hook(state, hook.file, None, hook.data) return core_pb2.AddHookResponse(result=True) def GetMobilityConfigs( @@ -1267,7 +1271,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session.mobility.set_model_config( wlan_config.node_id, BasicRangeModel.name, wlan_config.config ) - if session.state == EventTypes.RUNTIME_STATE.value: + if session.state == EventTypes.RUNTIME_STATE: node = self.get_node(session, wlan_config.node_id, context) node.updatemodel(wlan_config.config) return core_pb2.SetWlanConfigResponse(result=True) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 1f3b24e9..09ac2444 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -228,7 +228,7 @@ class CoreHandler(socketserver.BaseRequestHandler): coreapi.CoreEventTlv, [ (EventTlvs.NODE, event_data.node), - (EventTlvs.TYPE, event_data.event_type), + (EventTlvs.TYPE, event_data.event_type.value), (EventTlvs.NAME, event_data.name), (EventTlvs.DATA, event_data.data), (EventTlvs.TIME, event_data.time), @@ -723,7 +723,7 @@ class CoreHandler(socketserver.BaseRequestHandler): if message.flags & MessageFlags.STRING.value: self.node_status_request[node.id] = True - if self.session.state == EventTypes.RUNTIME_STATE.value: + if self.session.state == EventTypes.RUNTIME_STATE: self.send_node_emulation_id(node.id) elif message.flags & MessageFlags.DELETE.value: with self._shutdown_lock: @@ -966,7 +966,7 @@ class CoreHandler(socketserver.BaseRequestHandler): retries = 10 # wait for session to enter RUNTIME state, to prevent GUI from # connecting while nodes are still being instantiated - while session.state != EventTypes.RUNTIME_STATE.value: + while session.state != EventTypes.RUNTIME_STATE: logging.debug( "waiting for session %d to enter RUNTIME state", sid ) @@ -1375,7 +1375,7 @@ class CoreHandler(socketserver.BaseRequestHandler): parsed_config = ConfigShim.str_to_dict(values_str) self.session.mobility.set_model_config(node_id, object_name, parsed_config) - if self.session.state == EventTypes.RUNTIME_STATE.value and parsed_config: + if self.session.state == EventTypes.RUNTIME_STATE and parsed_config: try: node = self.session.get_node(node_id) if object_name == BasicRangeModel.name: @@ -1502,6 +1502,7 @@ class CoreHandler(socketserver.BaseRequestHandler): logging.error("error setting hook having state '%s'", state) return () state = int(state) + state = EventTypes(state) self.session.add_hook(state, file_name, source_name, data) return () @@ -1538,9 +1539,11 @@ class CoreHandler(socketserver.BaseRequestHandler): :return: reply messages :raises core.CoreError: when event type <= SHUTDOWN_STATE and not a known node id """ + event_type_value = message.get_tlv(EventTlvs.TYPE.value) + event_type = EventTypes(event_type_value) event_data = EventData( node=message.get_tlv(EventTlvs.NODE.value), - event_type=message.get_tlv(EventTlvs.TYPE.value), + event_type=event_type, name=message.get_tlv(EventTlvs.NAME.value), data=message.get_tlv(EventTlvs.DATA.value), time=message.get_tlv(EventTlvs.TIME.value), @@ -1549,7 +1552,6 @@ class CoreHandler(socketserver.BaseRequestHandler): if event_data.event_type is None: raise NotImplementedError("Event message missing event type") - event_type = EventTypes(event_data.event_type) node_id = event_data.node logging.debug("handling event %s at %s", event_type.name, time.ctime()) @@ -1667,25 +1669,19 @@ class CoreHandler(socketserver.BaseRequestHandler): unknown.append(service_name) continue - if ( - event_type == EventTypes.STOP.value - or event_type == EventTypes.RESTART.value - ): + if event_type in [EventTypes.STOP, EventTypes.RESTART]: status = self.session.services.stop_service(node, service) if status: fail += f"Stop {service.name}," - if ( - event_type == EventTypes.START.value - or event_type == EventTypes.RESTART.value - ): + if event_type in [EventTypes.START, EventTypes.RESTART]: status = self.session.services.startup_service(node, service) if status: fail += f"Start ({service.name})," - if event_type == EventTypes.PAUSE.value: + if event_type == EventTypes.PAUSE: status = self.session.services.validate_service(node, service) if status: fail += f"{service.name}," - if event_type == EventTypes.RECONFIGURE.value: + if event_type == EventTypes.RECONFIGURE: self.session.services.service_reconfigure(node, service) fail_data = "" @@ -2052,7 +2048,7 @@ class CoreUdpHandler(CoreHandler): current_session = self.server.mainserver.coreemu.sessions[session_id] current_node_count = current_session.get_node_count() if ( - current_session.state == EventTypes.RUNTIME_STATE.value + current_session.state == EventTypes.RUNTIME_STATE and current_node_count > node_count ): node_count = current_node_count diff --git a/daemon/core/emulator/enumerations.py b/daemon/core/emulator/enumerations.py index f426774e..44f60877 100644 --- a/daemon/core/emulator/enumerations.py +++ b/daemon/core/emulator/enumerations.py @@ -293,6 +293,9 @@ class EventTypes(Enum): RECONFIGURE = 14 INSTANTIATION_COMPLETE = 15 + def should_start(self) -> bool: + return self.value > self.DEFINITION_STATE.value + class SessionTlvs(Enum): """ diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index b0e44cb5..cd78af8f 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -112,8 +112,7 @@ class Session: self.nodes = {} self._nodes_lock = threading.Lock() - # TODO: should the default state be definition? - self.state = EventTypes.NONE.value + self.state = EventTypes.DEFINITION_STATE self._state_time = time.monotonic() self._state_file = os.path.join(self.session_dir, "state") @@ -121,7 +120,7 @@ class Session: self._hooks = {} self._state_hooks = {} self.add_state_hook( - state=EventTypes.RUNTIME_STATE.value, hook=self.runtime_state_hook + state=EventTypes.RUNTIME_STATE, hook=self.runtime_state_hook ) # handlers for broadcasting information @@ -345,7 +344,7 @@ class Session: node_one.name, node_two.name, ) - start = self.state > EventTypes.DEFINITION_STATE.value + start = self.state.should_start() net_one = self.create_node(cls=PtpNet, start=start) # node to network @@ -680,7 +679,7 @@ class Session: node_class = _cls # set node start based on current session state, override and check when rj45 - start = self.state > EventTypes.DEFINITION_STATE.value + start = self.state.should_start() enable_rj45 = self.options.get_config("enablerj45") == "1" if _type == NodeTypes.RJ45 and not enable_rj45: start = False @@ -755,7 +754,7 @@ class Session: # boot nodes after runtime, CoreNodes, Physical, and RJ45 are all nodes is_boot_node = isinstance(node, CoreNodeBase) and not isinstance(node, Rj45Node) - if self.state == EventTypes.RUNTIME_STATE.value and is_boot_node: + if self.state == EventTypes.RUNTIME_STATE and is_boot_node: self.write_nodes() self.add_remove_control_interface(node=node, remove=False) self.services.boot_services(node) @@ -850,10 +849,7 @@ class Session: :return: True if active, False otherwise """ - result = self.state in { - EventTypes.RUNTIME_STATE.value, - EventTypes.DATACOLLECT_STATE.value, - } + result = self.state in {EventTypes.RUNTIME_STATE, EventTypes.DATACOLLECT_STATE} logging.info("session(%s) checking if active: %s", self.id, result) return result @@ -894,7 +890,9 @@ class Session: """ CoreXmlWriter(self).write(file_name) - def add_hook(self, state: int, file_name: str, source_name: str, data: str) -> None: + def add_hook( + self, state: EventTypes, file_name: str, source_name: str, data: str + ) -> None: """ Store a hook from a received file message. @@ -904,9 +902,17 @@ class Session: :param data: hook data :return: nothing """ - # hack to conform with old logic until updated - state = f":{state}" - self.set_hook(state, file_name, source_name, data) + logging.info( + "setting state hook: %s - %s from %s", state, file_name, source_name + ) + hook = file_name, data + state_hooks = self._hooks.setdefault(state, []) + state_hooks.append(hook) + + # immediately run a hook if it is in the current state + if self.state == state: + logging.info("immediately running new state hook") + self.run_hook(hook) def add_node_file( self, node_id: int, source_name: str, file_name: str, data: str @@ -1071,10 +1077,8 @@ class Session: :param send_event: if true, generate core API event messages :return: nothing """ - state_value = state.value state_name = state.name - - if self.state == state_value: + if self.state == state: logging.info( "session(%s) is already in state: %s, skipping change", self.id, @@ -1082,33 +1086,32 @@ class Session: ) return - self.state = state_value + self.state = state self._state_time = time.monotonic() logging.info("changing session(%s) to state %s", self.id, state_name) - - self.write_state(state_value) - self.run_hooks(state_value) - self.run_state_hooks(state_value) + self.write_state(state) + self.run_hooks(state) + self.run_state_hooks(state) if send_event: - event_data = EventData(event_type=state_value, time=str(time.monotonic())) + event_data = EventData(event_type=state, time=str(time.monotonic())) self.broadcast_event(event_data) - def write_state(self, state: int) -> None: + def write_state(self, state: EventTypes) -> None: """ - Write the current state to a state file in the session dir. + Write the state to a state file in the session dir. :param state: state to write to file :return: nothing """ try: state_file = open(self._state_file, "w") - state_file.write(f"{state} {EventTypes(self.state).name}\n") + state_file.write(f"{state.value} {state.name}\n") state_file.close() except IOError: - logging.exception("error writing state file: %s", state) + logging.exception("error writing state file: %s", state.name) - def run_hooks(self, state: int) -> None: + def run_hooks(self, state: EventTypes) -> None: """ Run hook scripts upon changing states. If hooks is not specified, run all hooks in the given state. @@ -1212,7 +1215,7 @@ class Session: except (OSError, subprocess.CalledProcessError): logging.exception("error running hook: %s", file_name) - def run_state_hooks(self, state: int) -> None: + def run_state_hooks(self, state: EventTypes) -> None: """ Run state hooks. @@ -1223,16 +1226,17 @@ class Session: try: hook(state) except Exception: - state_name = EventTypes(self.state).name message = ( - f"exception occured when running {state_name} state hook: {hook}" + f"exception occured when running {state.name} state hook: {hook}" ) logging.exception(message) self.exception( ExceptionLevels.ERROR, "Session.run_state_hooks", None, message ) - def add_state_hook(self, state: int, hook: Callable[[int], None]) -> None: + def add_state_hook( + self, state: EventTypes, hook: Callable[[EventTypes], None] + ) -> None: """ Add a state hook. @@ -1259,14 +1263,14 @@ class Session: hooks = self._state_hooks.setdefault(state, []) hooks.remove(hook) - def runtime_state_hook(self, state: int) -> None: + def runtime_state_hook(self, state: EventTypes) -> None: """ Runtime state hook check. :param state: state to check :return: nothing """ - if state == EventTypes.RUNTIME_STATE.value: + if state == EventTypes.RUNTIME_STATE: self.emane.poststartup() # create session deployed xml @@ -1510,7 +1514,7 @@ class Session: self.mobility.startup() # notify listeners that instantiation is complete - event = EventData(event_type=EventTypes.INSTANTIATION_COMPLETE.value) + event = EventData(event_type=EventTypes.INSTANTIATION_COMPLETE) self.broadcast_event(event) # assume either all nodes have booted already, or there are some @@ -1553,9 +1557,9 @@ class Session: logging.debug( "session(%s) checking if not in runtime state, current state: %s", self.id, - EventTypes(self.state).name, + self.state.name, ) - if self.state == EventTypes.RUNTIME_STATE.value: + if self.state == EventTypes.RUNTIME_STATE: logging.info("valid runtime state found, returning") return diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 55af58d9..7b0ec8ac 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -142,17 +142,11 @@ class MobilityManager(ModelManager): ) continue - if ( - event_type == EventTypes.STOP.value - or event_type == EventTypes.RESTART.value - ): + if event_type in [EventTypes.STOP, EventTypes.RESTART]: model.stop(move_initial=True) - if ( - event_type == EventTypes.START.value - or event_type == EventTypes.RESTART.value - ): + if event_type in [EventTypes.START, EventTypes.RESTART]: model.start() - if event_type == EventTypes.PAUSE.value: + if event_type == EventTypes.PAUSE: model.pause() def sendevent(self, model: "WayPointMobility") -> None: @@ -163,13 +157,13 @@ class MobilityManager(ModelManager): :param model: mobility model to send event for :return: nothing """ - event_type = EventTypes.NONE.value + event_type = EventTypes.NONE if model.state == model.STATE_STOPPED: - event_type = EventTypes.STOP.value + event_type = EventTypes.STOP elif model.state == model.STATE_RUNNING: - event_type = EventTypes.START.value + event_type = EventTypes.START elif model.state == model.STATE_PAUSED: - event_type = EventTypes.PAUSE.value + event_type = EventTypes.PAUSE start_time = int(model.lasttime - model.timezero) end_time = int(model.endtime) diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 1ccf40a5..658ee1e3 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -100,7 +100,7 @@ class Sdt: return False if self.connected: return True - if self.session.state == EventTypes.SHUTDOWN_STATE.value: + if self.session.state == EventTypes.SHUTDOWN_STATE: return False self.seturl() diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 8eab98c2..074a6913 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -8,7 +8,7 @@ import core.nodes.physical from core.emane.nodes import EmaneNet from core.emulator.data import LinkData from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions -from core.emulator.enumerations import NodeTypes +from core.emulator.enumerations import EventTypes, NodeTypes from core.nodes.base import CoreNetworkBase, CoreNodeBase, NodeBase from core.nodes.docker import DockerNode from core.nodes.lxd import LxcNode @@ -324,7 +324,7 @@ class CoreXmlWriter: for file_name, data in self.session._hooks[state]: hook = etree.SubElement(hooks, "hook") add_attribute(hook, "name", file_name) - add_attribute(hook, "state", state) + add_attribute(hook, "state", state.value) hook.text = data if hooks.getchildren(): @@ -666,13 +666,11 @@ class CoreXmlReader: for hook in session_hooks.iterchildren(): name = hook.get("name") - state = hook.get("state") + state = get_int(hook, "state") + state = EventTypes(state) data = hook.text - hook_type = f"hook:{state}" logging.info("reading hook: state(%s) name(%s)", state, name) - self.session.set_hook( - hook_type, file_name=name, source_name=None, data=data - ) + self.session.add_hook(state, name, None, data) def read_session_origin(self) -> None: session_origin = self.scenario.find("session_origin") diff --git a/daemon/data/logging.conf b/daemon/data/logging.conf index 7f3d496f..46de6e92 100644 --- a/daemon/data/logging.conf +++ b/daemon/data/logging.conf @@ -14,7 +14,7 @@ } }, "root": { - "level": "INFO", + "level": "DEBUG", "handlers": ["console"] } } diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index d26c46e4..557265dc 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -127,7 +127,7 @@ class TestGrpc: assert wlan_node.id in session.nodes assert session.nodes[node_one.id].netif(0) is not None assert session.nodes[node_two.id].netif(0) is not None - hook_file, hook_data = session._hooks[core_pb2.SessionState.RUNTIME][0] + hook_file, hook_data = session._hooks[EventTypes.RUNTIME_STATE][0] assert hook_file == hook.file assert hook_data == hook.data assert session.location.refxyz == (location_x, location_y, location_z) @@ -169,7 +169,7 @@ class TestGrpc: assert isinstance(response.state, int) session = grpc_server.coreemu.sessions.get(response.session_id) assert session is not None - assert session.state == response.state + assert session.state == EventTypes(response.state) if session_id is not None: assert response.session_id == session_id assert session.id == session_id @@ -341,7 +341,7 @@ class TestGrpc: # then assert response.result is True - assert session.state == core_pb2.SessionState.DEFINITION + assert session.state == EventTypes.DEFINITION_STATE def test_add_node(self, grpc_server): # given @@ -447,7 +447,7 @@ class TestGrpc: session = grpc_server.coreemu.create_session() file_name = "test" file_data = "echo hello" - session.add_hook(EventTypes.RUNTIME_STATE.value, file_name, None, file_data) + session.add_hook(EventTypes.RUNTIME_STATE, file_name, None, file_data) # then with client.context_connect(): @@ -1065,7 +1065,7 @@ class TestGrpc: client.events(session.id, handle_event) time.sleep(0.1) event = EventData( - event_type=EventTypes.RUNTIME_STATE.value, time=str(time.monotonic()) + event_type=EventTypes.RUNTIME_STATE, time=str(time.monotonic()) ) session.broadcast_event(event) diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index a47aba75..8bf6f4b7 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -376,14 +376,14 @@ class TestGui: assert len(coretlv.coreemu.sessions) == 0 def test_file_hook_add(self, coretlv): - state = EventTypes.DATACOLLECT_STATE.value + state = EventTypes.DATACOLLECT_STATE assert coretlv.session._hooks.get(state) is None file_name = "test.sh" file_data = "echo hello" message = coreapi.CoreFileMessage.create( MessageFlags.ADD.value, [ - (FileTlvs.TYPE, f"hook:{state}"), + (FileTlvs.TYPE, f"hook:{state.value}"), (FileTlvs.NAME, file_name), (FileTlvs.DATA, file_data), ], @@ -514,7 +514,7 @@ class TestGui: coretlv.handle_message(message) - assert coretlv.session.state == state.value + assert coretlv.session.state == state def test_event_schedule(self, coretlv): coretlv.session.add_event = mock.MagicMock() diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index 496623a6..ebbb6b1e 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -3,7 +3,7 @@ from xml.etree import ElementTree import pytest from core.emulator.emudata import LinkOptions, NodeOptions -from core.emulator.enumerations import NodeTypes +from core.emulator.enumerations import EventTypes, NodeTypes from core.errors import CoreError from core.location.mobility import BasicRangeModel from core.services.utility import SshService @@ -20,7 +20,8 @@ class TestXml: # create hook file_name = "runtime_hook.sh" data = "#!/bin/sh\necho hello" - session.set_hook("hook:4", file_name, None, data) + state = EventTypes.RUNTIME_STATE + session.add_hook(state, file_name, None, data) # save xml xml_file = tmpdir.join("session.xml") @@ -38,7 +39,7 @@ class TestXml: session.open_xml(file_path, start=True) # verify nodes have been recreated - runtime_hooks = session._hooks.get(4) + runtime_hooks = session._hooks.get(state) assert runtime_hooks runtime_hook = runtime_hooks[0] assert file_name == runtime_hook[0] From f277e96c9a4bd0e01e49d80b3e51cd2caba7a397 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 10 Mar 2020 22:48:05 -0700 Subject: [PATCH 0088/1131] revert logging back to info, removed Rj45Models enum as it was not being used, updated linktypes enum to be used directly --- daemon/core/api/grpc/events.py | 2 +- daemon/core/api/grpc/grpcutils.py | 2 +- daemon/core/api/grpc/server.py | 2 +- daemon/core/api/tlv/corehandlers.py | 2 +- daemon/core/emane/nodes.py | 2 +- daemon/core/emulator/enumerations.py | 10 ---------- daemon/core/emulator/session.py | 2 +- daemon/core/location/mobility.py | 2 +- daemon/core/nodes/base.py | 2 +- daemon/core/nodes/network.py | 2 +- daemon/core/plugins/sdt.py | 2 +- daemon/core/services/quagga.py | 2 +- daemon/data/logging.conf | 2 +- 13 files changed, 12 insertions(+), 22 deletions(-) diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index cf98a9a2..60cb15a9 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -80,7 +80,7 @@ def handle_link_event(event: LinkData) -> core_pb2.LinkEvent: unidirectional=event.unidirectional, ) link = core_pb2.Link( - type=event.link_type, + type=event.link_type.value, node_one_id=event.node1_id, node_two_id=event.node2_id, interface_one=interface_one, diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 4ee492b9..d765f4bc 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -325,7 +325,7 @@ def convert_link(session: Session, link_data: LinkData) -> core_pb2.Link: ) return core_pb2.Link( - type=link_data.link_type, + type=link_data.link_type.value, node_one_id=link_data.node1_id, node_two_id=link_data.node2_id, interface_one=interface_one, diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 4c8d6640..5c927151 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -1501,7 +1501,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): flag = MessageFlags.DELETE.value link = LinkData( message_type=flag, - link_type=LinkTypes.WIRELESS.value, + link_type=LinkTypes.WIRELESS, node1_id=node_one.id, node2_id=node_two.id, network_id=emane_one.id, diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 09ac2444..1869e275 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -356,7 +356,7 @@ class CoreHandler(socketserver.BaseRequestHandler): (LinkTlvs.BURST, link_data.burst), (LinkTlvs.SESSION, link_data.session), (LinkTlvs.MBURST, link_data.mburst), - (LinkTlvs.TYPE, link_data.link_type), + (LinkTlvs.TYPE, link_data.link_type.value), (LinkTlvs.GUI_ATTRIBUTES, link_data.gui_attributes), (LinkTlvs.UNIDIRECTIONAL, link_data.unidirectional), (LinkTlvs.EMULATION_ID, link_data.emulation_id), diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 3a1834f3..99045c07 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -34,7 +34,7 @@ class EmaneNet(CoreNetworkBase): """ apitype = NodeTypes.EMANE.value - linktype = LinkTypes.WIRED.value + linktype = LinkTypes.WIRED type = "wlan" is_emane = True diff --git a/daemon/core/emulator/enumerations.py b/daemon/core/emulator/enumerations.py index 44f60877..d13c8287 100644 --- a/daemon/core/emulator/enumerations.py +++ b/daemon/core/emulator/enumerations.py @@ -86,16 +86,6 @@ class NodeTypes(Enum): LXC = 16 -class Rj45Models(Enum): - """ - RJ45 model types. - """ - - LINKED = 0 - WIRELESS = 1 - INSTALLED = 2 - - # Link Message TLV Types class LinkTlvs(Enum): """ diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index cd78af8f..a44ba62f 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -577,7 +577,7 @@ class Session: try: # wireless link - if link_options.type == LinkTypes.WIRELESS.value: + if link_options.type == LinkTypes.WIRELESS: raise CoreError("cannot update wireless link") else: if not node_one and not node_two: diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 7b0ec8ac..c6e88611 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -499,7 +499,7 @@ class BasicRangeModel(WirelessModel): node1_id=interface1.node.id, node2_id=interface2.node.id, network_id=self.wlan.id, - link_type=LinkTypes.WIRELESS.value, + link_type=LinkTypes.WIRELESS, ) def sendlinkmsg( diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 9def7777..9b72df72 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -982,7 +982,7 @@ class CoreNetworkBase(NodeBase): Base class for networks """ - linktype = LinkTypes.WIRED.value + linktype = LinkTypes.WIRED is_emane = False def __init__( diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 67955a38..fdfa87f1 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -1030,7 +1030,7 @@ class WlanNode(CoreNetwork): """ apitype = NodeTypes.WIRELESS_LAN.value - linktype = LinkTypes.WIRED.value + linktype = LinkTypes.WIRED policy = "DROP" type = "wlan" diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 658ee1e3..284fe970 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: def link_data_params(link_data: LinkData) -> Tuple[int, int, bool]: node_one = link_data.node1_id node_two = link_data.node2_id - is_wireless = link_data.link_type == LinkTypes.WIRELESS.value + is_wireless = link_data.link_type == LinkTypes.WIRELESS return node_one, node_two, is_wireless diff --git a/daemon/core/services/quagga.py b/daemon/core/services/quagga.py index 11de9c54..7979dd23 100644 --- a/daemon/core/services/quagga.py +++ b/daemon/core/services/quagga.py @@ -551,7 +551,7 @@ class Babel(QuaggaService): @classmethod def generatequaggaifcconfig(cls, node, ifc): - if ifc.net and ifc.net.linktype == LinkTypes.WIRELESS.value: + if ifc.net and ifc.net.linktype == LinkTypes.WIRELESS: return " babel wireless\n no babel split-horizon\n" else: return " babel wired\n babel split-horizon\n" diff --git a/daemon/data/logging.conf b/daemon/data/logging.conf index 46de6e92..7f3d496f 100644 --- a/daemon/data/logging.conf +++ b/daemon/data/logging.conf @@ -14,7 +14,7 @@ } }, "root": { - "level": "DEBUG", + "level": "INFO", "handlers": ["console"] } } From 5cdfd8d8b97546c2c67d8ff0c2fe8b4a87b2f677 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 10 Mar 2020 23:11:37 -0700 Subject: [PATCH 0089/1131] updated NodeTypes to be used directly --- daemon/core/api/grpc/grpcutils.py | 6 +----- daemon/core/api/tlv/dataconversion.py | 2 +- daemon/core/emane/nodes.py | 2 +- daemon/core/nodes/base.py | 2 +- daemon/core/nodes/docker.py | 2 +- daemon/core/nodes/lxd.py | 2 +- daemon/core/nodes/network.py | 8 ++++---- daemon/core/nodes/physical.py | 2 +- daemon/core/xml/corexml.py | 2 +- 9 files changed, 12 insertions(+), 16 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index d765f4bc..6de7c0bd 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -26,11 +26,7 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption :return: node type, id, and options """ _id = node_proto.id - _type = node_proto.type - if _type is None: - _type = NodeTypes.DEFAULT.value - _type = NodeTypes(_type) - + _type = NodeTypes(node_proto.type) options = NodeOptions(name=node_proto.name, model=node_proto.model) options.icon = node_proto.icon options.opaque = node_proto.opaque diff --git a/daemon/core/api/tlv/dataconversion.py b/daemon/core/api/tlv/dataconversion.py index 8d47613d..f2378115 100644 --- a/daemon/core/api/tlv/dataconversion.py +++ b/daemon/core/api/tlv/dataconversion.py @@ -17,7 +17,7 @@ def convert_node(node_data): coreapi.CoreNodeTlv, [ (NodeTlvs.NUMBER, node_data.id), - (NodeTlvs.TYPE, node_data.node_type), + (NodeTlvs.TYPE, node_data.node_type.value), (NodeTlvs.NAME, node_data.name), (NodeTlvs.IP_ADDRESS, node_data.ip_address), (NodeTlvs.MAC_ADDRESS, node_data.mac_address), diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 99045c07..b522a7e2 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -33,7 +33,7 @@ class EmaneNet(CoreNetworkBase): Emane controller object that exists in a session. """ - apitype = NodeTypes.EMANE.value + apitype = NodeTypes.EMANE linktype = LinkTypes.WIRED type = "wlan" is_emane = True diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 9b72df72..9a5da63d 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -472,7 +472,7 @@ class CoreNode(CoreNodeBase): Provides standard core node logic. """ - apitype = NodeTypes.DEFAULT.value + apitype = NodeTypes.DEFAULT valid_address_types = {"inet", "inet6", "inet6link"} def __init__( diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index 4622f4f5..60adfe32 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -72,7 +72,7 @@ class DockerClient: class DockerNode(CoreNode): - apitype = NodeTypes.DOCKER.value + apitype = NodeTypes.DOCKER def __init__( self, diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index b64c0206..3ca399b5 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -66,7 +66,7 @@ class LxdClient: class LxcNode(CoreNode): - apitype = NodeTypes.LXC.value + apitype = NodeTypes.LXC def __init__( self, diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index fdfa87f1..f9bbd7d3 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -999,7 +999,7 @@ class SwitchNode(CoreNetwork): Provides switch functionality within a core node. """ - apitype = NodeTypes.SWITCH.value + apitype = NodeTypes.SWITCH policy = "ACCEPT" type = "lanswitch" @@ -1010,7 +1010,7 @@ class HubNode(CoreNetwork): ports by turning off MAC address learning. """ - apitype = NodeTypes.HUB.value + apitype = NodeTypes.HUB policy = "ACCEPT" type = "hub" @@ -1029,7 +1029,7 @@ class WlanNode(CoreNetwork): Provides wireless lan functionality within a core node. """ - apitype = NodeTypes.WIRELESS_LAN.value + apitype = NodeTypes.WIRELESS_LAN linktype = LinkTypes.WIRED policy = "DROP" type = "wlan" @@ -1140,6 +1140,6 @@ class TunnelNode(GreTapBridge): Provides tunnel functionality in a core node. """ - apitype = NodeTypes.TUNNEL.value + apitype = NodeTypes.TUNNEL policy = "ACCEPT" type = "tunnel" diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 8959fda1..299f1ebb 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -264,7 +264,7 @@ class Rj45Node(CoreNodeBase, CoreInterface): network. """ - apitype = NodeTypes.RJ45.value + apitype = NodeTypes.RJ45 type = "rj45" def __init__( diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 074a6913..64e20674 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -260,7 +260,7 @@ class NetworkElement(NodeElement): def add_type(self) -> None: if self.node.apitype: - node_type = NodeTypes(self.node.apitype).name + node_type = self.node.apitype.name else: node_type = self.node.__class__.__name__ add_attribute(self.element, "type", node_type) From a7790185d4da0f9c8530af999e29c85d7424c03c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 12 Mar 2020 23:12:17 -0700 Subject: [PATCH 0090/1131] updates to use message flags enum directly --- daemon/core/api/grpc/events.py | 4 ++-- daemon/core/api/grpc/grpcutils.py | 2 +- daemon/core/api/grpc/server.py | 6 +++--- daemon/core/api/tlv/corehandlers.py | 12 ++++++------ daemon/core/api/tlv/dataconversion.py | 2 +- daemon/core/emane/emanemanager.py | 2 +- daemon/core/emulator/enumerations.py | 1 + daemon/core/location/mobility.py | 15 +++++++++------ daemon/core/nodes/base.py | 8 ++++---- daemon/core/nodes/interface.py | 3 ++- daemon/core/nodes/network.py | 8 ++++---- daemon/core/plugins/sdt.py | 8 ++++---- daemon/core/services/coreservices.py | 2 +- daemon/core/xml/corexml.py | 2 +- daemon/tests/test_core.py | 2 +- daemon/tests/test_grpc.py | 12 ++++++------ daemon/tests/test_gui.py | 22 +++++++++++----------- daemon/tests/test_links.py | 6 +++--- daemon/tests/test_xml.py | 8 ++++---- 19 files changed, 65 insertions(+), 60 deletions(-) diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index 60cb15a9..0bab096b 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -87,7 +87,7 @@ def handle_link_event(event: LinkData) -> core_pb2.LinkEvent: interface_two=interface_two, options=options, ) - return core_pb2.LinkEvent(message_type=event.message_type, link=link) + return core_pb2.LinkEvent(message_type=event.message_type.value, link=link) def handle_session_event(event: EventData) -> core_pb2.SessionEvent: @@ -158,7 +158,7 @@ def handle_file_event(event: FileData) -> core_pb2.FileEvent: :return: file event """ return core_pb2.FileEvent( - message_type=event.message_type, + message_type=event.message_type.value, node_id=event.node, name=event.name, mode=event.mode, diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 6de7c0bd..77813b34 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -229,7 +229,7 @@ def get_links(session: Session, node: NodeBase): :return: [core.api.grpc.core_pb2.Link] """ links = [] - for link_data in node.all_link_data(0): + for link_data in node.all_link_data(): link = convert_link(session, link_data) links.append(link) return links diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 5c927151..03743263 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -722,7 +722,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): if request.source: source = request.source if not has_geo: - node_data = node.data(0, source=source) + node_data = node.data(source=source) session.broadcast_node(node_data) except CoreError: result = False @@ -1496,9 +1496,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): if emane_one.id == emane_two.id: if request.linked: - flag = MessageFlags.ADD.value + flag = MessageFlags.ADD else: - flag = MessageFlags.DELETE.value + flag = MessageFlags.DELETE link = LinkData( message_type=flag, link_type=LinkTypes.WIRELESS, diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 1869e275..ab737a0a 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -265,7 +265,7 @@ class CoreHandler(socketserver.BaseRequestHandler): (FileTlvs.COMPRESSED_DATA, file_data.compressed_data), ], ) - message = coreapi.CoreFileMessage.pack(file_data.message_type, tlv_data) + message = coreapi.CoreFileMessage.pack(file_data.message_type.value, tlv_data) try: self.sendall(message) @@ -380,7 +380,7 @@ class CoreHandler(socketserver.BaseRequestHandler): ], ) - message = coreapi.CoreLinkMessage.pack(link_data.message_type, tlv_data) + message = coreapi.CoreLinkMessage.pack(link_data.message_type.value, tlv_data) try: self.sendall(message) @@ -1841,11 +1841,11 @@ class CoreHandler(socketserver.BaseRequestHandler): with self.session._nodes_lock: for node_id in self.session.nodes: node = self.session.nodes[node_id] - node_data = node.data(message_type=MessageFlags.ADD.value) + node_data = node.data(message_type=MessageFlags.ADD) if node_data: nodes_data.append(node_data) - node_links = node.all_link_data(flags=MessageFlags.ADD.value) + node_links = node.all_link_data(flags=MessageFlags.ADD) for link_data in node_links: links_data.append(link_data) @@ -1913,7 +1913,7 @@ class CoreHandler(socketserver.BaseRequestHandler): for file_name, config_data in self.session.services.all_files(service): file_data = FileData( - message_type=MessageFlags.ADD.value, + message_type=MessageFlags.ADD, node=node_id, name=str(file_name), type=opaque, @@ -1927,7 +1927,7 @@ class CoreHandler(socketserver.BaseRequestHandler): for state in sorted(self.session._hooks.keys()): for file_name, config_data in self.session._hooks[state]: file_data = FileData( - message_type=MessageFlags.ADD.value, + message_type=MessageFlags.ADD, name=str(file_name), type=f"hook:{state}", data=str(config_data), diff --git a/daemon/core/api/tlv/dataconversion.py b/daemon/core/api/tlv/dataconversion.py index f2378115..35825b6a 100644 --- a/daemon/core/api/tlv/dataconversion.py +++ b/daemon/core/api/tlv/dataconversion.py @@ -38,7 +38,7 @@ def convert_node(node_data): (NodeTlvs.OPAQUE, node_data.opaque), ], ) - return coreapi.CoreNodeMessage.pack(node_data.message_type, tlv_data) + return coreapi.CoreNodeMessage.pack(node_data.message_type.value, tlv_data) def convert_config(config_data): diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index af0d2492..4a3d0142 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -806,7 +806,7 @@ class EmaneManager(ModelManager): # don"t use node.setposition(x,y,z) which generates an event node.position.set(x, y, z) - node_data = node.data(message_type=0, lat=lat, lon=lon, alt=alt) + node_data = node.data(lat=lat, lon=lon, alt=alt) self.session.broadcast_node(node_data) return True diff --git a/daemon/core/emulator/enumerations.py b/daemon/core/emulator/enumerations.py index d13c8287..7ba8a734 100644 --- a/daemon/core/emulator/enumerations.py +++ b/daemon/core/emulator/enumerations.py @@ -30,6 +30,7 @@ class MessageFlags(Enum): CORE message flags. """ + NONE = 0x00 ADD = 0x01 DELETE = 0x02 CRI = 0x04 diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index c6e88611..19750c16 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -220,7 +220,7 @@ class WirelessModel(ConfigurableOptions): self.session = session self.id = _id - def all_link_data(self, flags: int) -> List: + def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List: """ May be used if the model can populate the GUI with wireless (green) link lines. @@ -484,7 +484,10 @@ class BasicRangeModel(WirelessModel): return True def create_link_data( - self, interface1: CoreInterface, interface2: CoreInterface, message_type: int + self, + interface1: CoreInterface, + interface2: CoreInterface, + message_type: MessageFlags, ) -> LinkData: """ Create a wireless link/unlink data message. @@ -514,14 +517,14 @@ class BasicRangeModel(WirelessModel): :return: nothing """ if unlink: - message_type = MessageFlags.DELETE.value + message_type = MessageFlags.DELETE else: - message_type = MessageFlags.ADD.value + message_type = MessageFlags.ADD link_data = self.create_link_data(netif, netif2, message_type) self.session.broadcast_link(link_data) - def all_link_data(self, flags: int) -> List[LinkData]: + def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Return a list of wireless link messages for when the GUI reconnects. @@ -816,7 +819,7 @@ class WayPointMobility(WirelessModel): :return: nothing """ node.position.set(x, y, z) - node_data = node.data(message_type=0) + node_data = node.data() self.session.broadcast_node(node_data) def setendtime(self) -> None: diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 9a5da63d..b981f285 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -14,7 +14,7 @@ from core import utils from core.configservice.dependencies import ConfigServiceDependencies from core.constants import MOUNT_BIN, VNODED_BIN from core.emulator.data import LinkData, NodeData -from core.emulator.enumerations import LinkTypes, NodeTypes +from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes from core.errors import CoreCommandError, CoreError from core.nodes import client from core.nodes.interface import CoreInterface, TunTap, Veth @@ -193,7 +193,7 @@ class NodeBase: def data( self, - message_type: int, + message_type: MessageFlags = MessageFlags.NONE, lat: float = None, lon: float = None, alt: float = None, @@ -244,7 +244,7 @@ class NodeBase: return node_data - def all_link_data(self, flags: int) -> List: + def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Build CORE Link data for this object. There is no default method for PyCoreObjs as PyCoreNodes do not implement this but @@ -1069,7 +1069,7 @@ class CoreNetworkBase(NodeBase): with self._linked_lock: del self._linked[netif] - def all_link_data(self, flags: int) -> List[LinkData]: + def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Build link data objects for this network. Each link object describes a link between this network and a node. diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 9ae01bfd..7b7ec967 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -7,6 +7,7 @@ import time from typing import TYPE_CHECKING, Callable, Dict, List, Tuple from core import utils +from core.emulator.enumerations import MessageFlags from core.errors import CoreCommandError from core.nodes.netclient import get_net_client @@ -554,7 +555,7 @@ class GreTap(CoreInterface): """ return None - def all_link_data(self, flags: int) -> List: + def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List: """ Retrieve link data. diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index f9bbd7d3..af6f20be 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -12,7 +12,7 @@ import netaddr from core import utils from core.constants import EBTABLES_BIN, TC_BIN from core.emulator.data import LinkData, NodeData -from core.emulator.enumerations import LinkTypes, NodeTypes, RegisterTlvs +from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes, RegisterTlvs from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNetworkBase from core.nodes.interface import CoreInterface, GreTap, Veth @@ -848,7 +848,7 @@ class CtrlNet(CoreNetwork): super().shutdown() - def all_link_data(self, flags: int) -> List[LinkData]: + def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Do not include CtrlNet in link messages describing this session. @@ -899,7 +899,7 @@ class PtpNet(CoreNetwork): """ return None - def all_link_data(self, flags: int) -> List[LinkData]: + def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Build CORE API TLVs for a point-to-point link. One Link message describes this network. @@ -1122,7 +1122,7 @@ class WlanNode(CoreNetwork): x, y, z = netif.node.position.get() netif.poshook(netif, x, y, z) - def all_link_data(self, flags: int) -> List[LinkData]: + def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Retrieve all link data. diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 284fe970..156a945d 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -210,7 +210,7 @@ class Sdt: self.add_node(node) for net in nets: - all_links = net.all_link_data(flags=MessageFlags.ADD.value) + all_links = net.all_link_data(flags=MessageFlags.ADD) for link_data in all_links: is_wireless = isinstance(net, (WlanNode, EmaneNet)) if is_wireless and link_data.node1_id == net.id: @@ -302,7 +302,7 @@ class Sdt: return # delete node - if node_data.message_type == MessageFlags.DELETE.value: + if node_data.message_type == MessageFlags.DELETE: self.cmd(f"delete node,{node_data.id}") else: x = node_data.x_position @@ -375,9 +375,9 @@ class Sdt: :param link_data: link data to handle :return: nothing """ - if link_data.message_type == MessageFlags.ADD.value: + if link_data.message_type == MessageFlags.ADD: params = link_data_params(link_data) self.add_link(*params) - elif link_data.message_type == MessageFlags.DELETE.value: + elif link_data.message_type == MessageFlags.DELETE: params = link_data_params(link_data) self.delete_link(*params[:2]) diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index a561b843..9ae8ffa6 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -672,7 +672,7 @@ class CoreServices: filetypestr = "service:%s" % service.name return FileData( - message_type=MessageFlags.ADD.value, + message_type=MessageFlags.ADD, node=node.id, name=filename, type=filetypestr, diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 64e20674..68426905 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -476,7 +476,7 @@ class CoreXmlWriter: self.write_device(node) # add known links - links.extend(node.all_link_data(0)) + links.extend(node.all_link_data()) return links diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 80cf6787..cc9ba2a4 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -114,7 +114,7 @@ class TestCore: session.instantiate() # check link data gets generated - assert ptp_node.all_link_data(MessageFlags.ADD.value) + assert ptp_node.all_link_data(MessageFlags.ADD) # check common nets exist between linked nodes assert node_one.commonnets(node_two) diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 557265dc..e2397b53 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -540,7 +540,7 @@ class TestGrpc: session = grpc_server.coreemu.create_session() switch = session.add_node(_type=NodeTypes.SWITCH) node = session.add_node() - assert len(switch.all_link_data(0)) == 0 + assert len(switch.all_link_data()) == 0 # then interface = interface_helper.create_interface(node.id, 0) @@ -549,7 +549,7 @@ class TestGrpc: # then assert response.result is True - assert len(switch.all_link_data(0)) == 1 + assert len(switch.all_link_data()) == 1 def test_add_link_exception(self, grpc_server, interface_helper): # given @@ -572,7 +572,7 @@ class TestGrpc: interface = ip_prefixes.create_interface(node) session.add_link(node.id, switch.id, interface) options = core_pb2.LinkOptions(bandwidth=30000) - link = switch.all_link_data(0)[0] + link = switch.all_link_data()[0] assert options.bandwidth != link.bandwidth # then @@ -583,7 +583,7 @@ class TestGrpc: # then assert response.result is True - link = switch.all_link_data(0)[0] + link = switch.all_link_data()[0] assert options.bandwidth == link.bandwidth def test_delete_link(self, grpc_server, ip_prefixes): @@ -986,7 +986,7 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() node = session.add_node() - node_data = node.data(message_type=0) + node_data = node.data() queue = Queue() def handle_event(event_data): @@ -1011,7 +1011,7 @@ class TestGrpc: node = session.add_node() interface = ip_prefixes.create_interface(node) session.add_link(node.id, wlan.id, interface) - link_data = wlan.all_link_data(0)[0] + link_data = wlan.all_link_data()[0] queue = Queue() def handle_event(event_data): diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index 8bf6f4b7..52969e93 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -117,7 +117,7 @@ class TestGui: coretlv.handle_message(message) switch_node = coretlv.session.get_node(switch) - all_links = switch_node.all_link_data(0) + all_links = switch_node.all_link_data() assert len(all_links) == 1 def test_link_add_net_to_node(self, coretlv): @@ -141,7 +141,7 @@ class TestGui: coretlv.handle_message(message) switch_node = coretlv.session.get_node(switch) - all_links = switch_node.all_link_data(0) + all_links = switch_node.all_link_data() assert len(all_links) == 1 def test_link_add_node_to_node(self, coretlv): @@ -171,7 +171,7 @@ class TestGui: all_links = [] for node_id in coretlv.session.nodes: node = coretlv.session.nodes[node_id] - all_links += node.all_link_data(0) + all_links += node.all_link_data() assert len(all_links) == 1 def test_link_update(self, coretlv): @@ -193,7 +193,7 @@ class TestGui: ) coretlv.handle_message(message) switch_node = coretlv.session.get_node(switch) - all_links = switch_node.all_link_data(0) + all_links = switch_node.all_link_data() assert len(all_links) == 1 link = all_links[0] assert link.bandwidth is None @@ -211,7 +211,7 @@ class TestGui: coretlv.handle_message(message) switch_node = coretlv.session.get_node(switch) - all_links = switch_node.all_link_data(0) + all_links = switch_node.all_link_data() assert len(all_links) == 1 link = all_links[0] assert link.bandwidth == bandwidth @@ -240,7 +240,7 @@ class TestGui: all_links = [] for node_id in coretlv.session.nodes: node = coretlv.session.nodes[node_id] - all_links += node.all_link_data(0) + all_links += node.all_link_data() assert len(all_links) == 1 message = coreapi.CoreLinkMessage.create( @@ -257,7 +257,7 @@ class TestGui: all_links = [] for node_id in coretlv.session.nodes: node = coretlv.session.nodes[node_id] - all_links += node.all_link_data(0) + all_links += node.all_link_data() assert len(all_links) == 0 def test_link_delete_node_to_net(self, coretlv): @@ -279,7 +279,7 @@ class TestGui: ) coretlv.handle_message(message) switch_node = coretlv.session.get_node(switch) - all_links = switch_node.all_link_data(0) + all_links = switch_node.all_link_data() assert len(all_links) == 1 message = coreapi.CoreLinkMessage.create( @@ -293,7 +293,7 @@ class TestGui: coretlv.handle_message(message) switch_node = coretlv.session.get_node(switch) - all_links = switch_node.all_link_data(0) + all_links = switch_node.all_link_data() assert len(all_links) == 0 def test_link_delete_net_to_node(self, coretlv): @@ -315,7 +315,7 @@ class TestGui: ) coretlv.handle_message(message) switch_node = coretlv.session.get_node(switch) - all_links = switch_node.all_link_data(0) + all_links = switch_node.all_link_data() assert len(all_links) == 1 message = coreapi.CoreLinkMessage.create( @@ -329,7 +329,7 @@ class TestGui: coretlv.handle_message(message) switch_node = coretlv.session.get_node(switch) - all_links = switch_node.all_link_data(0) + all_links = switch_node.all_link_data() assert len(all_links) == 0 def test_session_update(self, coretlv): diff --git a/daemon/tests/test_links.py b/daemon/tests/test_links.py index ceef3a02..d32a1c5f 100644 --- a/daemon/tests/test_links.py +++ b/daemon/tests/test_links.py @@ -43,7 +43,7 @@ class TestLinks: session.add_link(node_one.id, node_two.id, interface_one) # then - assert node_two.all_link_data(0) + assert node_two.all_link_data() assert node_one.netif(interface_one.id) def test_net_to_node(self, session, ip_prefixes): @@ -56,7 +56,7 @@ class TestLinks: session.add_link(node_one.id, node_two.id, interface_two=interface_two) # then - assert node_one.all_link_data(0) + assert node_one.all_link_data() assert node_two.netif(interface_two.id) def test_net_to_net(self, session): @@ -68,7 +68,7 @@ class TestLinks: session.add_link(node_one.id, node_two.id) # then - assert node_one.all_link_data(0) + assert node_one.all_link_data() def test_link_update(self, session, ip_prefixes): # given diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index ebbb6b1e..783e2722 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -270,7 +270,7 @@ class TestXml: switch_two = session.get_node(n2_id) assert switch_one assert switch_two - assert len(switch_one.all_link_data(0) + switch_two.all_link_data(0)) == 1 + assert len(switch_one.all_link_data() + switch_two.all_link_data()) == 1 def test_link_options(self, session, tmpdir, ip_prefixes): """ @@ -330,7 +330,7 @@ class TestXml: links = [] for node_id in session.nodes: node = session.nodes[node_id] - links += node.all_link_data(0) + links += node.all_link_data() link = links[0] assert link_options.per == link.per assert link_options.bandwidth == link.bandwidth @@ -397,7 +397,7 @@ class TestXml: links = [] for node_id in session.nodes: node = session.nodes[node_id] - links += node.all_link_data(0) + links += node.all_link_data() link = links[0] assert link_options.per == link.per assert link_options.bandwidth == link.bandwidth @@ -479,7 +479,7 @@ class TestXml: links = [] for node_id in session.nodes: node = session.nodes[node_id] - links += node.all_link_data(0) + links += node.all_link_data() assert len(links) == 2 link_one = links[0] link_two = links[1] From 3507b65676811443bb564c060498949451c9157e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 12 Mar 2020 23:21:13 -0700 Subject: [PATCH 0091/1131] bump version to 6.2.0 for next release --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 905fbde0..b9369de6 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, 6.1.0) +AC_INIT(core, 6.2.0) # autoconf and automake initialization AC_CONFIG_SRCDIR([netns/version.h.in]) From 102fa410fe2844c04fa1535bfa486fa539a7d9af Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 12 Mar 2020 23:21:48 -0700 Subject: [PATCH 0092/1131] make wlan nodes start with a ebtables change event to trigger default rules when all nodes are disconnected --- daemon/core/nodes/network.py | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 67955a38..3e8e7d8c 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -1067,6 +1067,7 @@ class WlanNode(CoreNetwork): """ super().startup() self.net_client.disable_mac_learning(self.brname) + ebq.ebchange(self) def attach(self, netif: CoreInterface) -> None: """ From 3f17706c28f35f56dd743946ba02e3a4e8b6eed2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 19 Mar 2020 16:40:43 -0700 Subject: [PATCH 0093/1131] small cleanup for interface position hooks, updates to support using a provided altitude when sending emane events based on position hooks --- daemon/core/api/grpc/server.py | 1 - daemon/core/emane/emanemanager.py | 4 +- daemon/core/emane/nodes.py | 63 +++++++++++++++++-------------- daemon/core/emulator/session.py | 1 + daemon/core/location/mobility.py | 8 +--- daemon/core/nodes/base.py | 27 ++++++++++++- daemon/core/nodes/interface.py | 12 +++--- daemon/core/nodes/network.py | 14 ++----- daemon/core/nodes/physical.py | 7 ++-- 9 files changed, 76 insertions(+), 61 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index c71800f4..69fc0c48 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -705,7 +705,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): x = request.position.x y = request.position.y options.set_position(x, y) - lat, lon, alt = None, None, None has_geo = request.HasField("geo") if has_geo: lat = request.geo.lat diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index af0d2492..ea4a019d 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -369,8 +369,7 @@ class EmaneManager(ModelManager): ) emane_node.model.post_startup() for netif in emane_node.netifs(): - x, y, z = netif.node.position.get() - emane_node.setnemposition(netif, x, y, z) + netif.setposition() def reset(self) -> None: """ @@ -806,6 +805,7 @@ class EmaneManager(ModelManager): # don"t use node.setposition(x,y,z) which generates an event node.position.set(x, y, z) + node.position.set_geo(lon, lat, alt) node_data = node.data(message_type=0, lat=lat, lon=lon, alt=alt) self.session.broadcast_node(node_data) return True diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 3a1834f3..b44d16b6 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -4,7 +4,7 @@ share the same MAC+PHY model. """ import logging -from typing import TYPE_CHECKING, Dict, List, Optional, Type +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type from core.emulator.distributed import DistributedServer from core.emulator.enumerations import LinkTypes, NodeTypes, RegisterTlvs @@ -172,8 +172,7 @@ class EmaneNet(CoreNetworkBase): # at this point we register location handlers for generating # EMANE location events netif.poshook = self.setnemposition - x, y, z = netif.node.position.get() - self.setnemposition(netif, x, y, z) + netif.setposition() def deinstallnetifs(self) -> None: """ @@ -186,28 +185,45 @@ class EmaneNet(CoreNetworkBase): netif.shutdown() netif.poshook = None - def setnemposition( - self, netif: CoreInterface, x: float, y: float, z: float - ) -> None: + def _nem_position( + self, netif: CoreInterface + ) -> Optional[Tuple[int, float, float, float]]: """ - Publish a NEM location change event using the EMANE event service. + Creates nem position for emane event for a given interface. + + :param netif: interface to get nem emane position for + :return: nem position tuple, None otherwise """ - if self.session.emane.service is None: - logging.info("position service not available") - return nemid = self.getnemid(netif) ifname = netif.localname if nemid is None: logging.info("nemid for %s is unknown", ifname) return + node = netif.node + x, y, z = node.getposition() lat, lon, alt = self.session.location.getgeo(x, y, z) - event = LocationEvent() - + if node.position.alt is not None: + alt = node.position.alt # altitude must be an integer or warning is printed - # unused: yaw, pitch, roll, azimuth, elevation, velocity alt = int(round(alt)) - event.append(nemid, latitude=lat, longitude=lon, altitude=alt) - self.session.emane.service.publish(0, event) + return nemid, lon, lat, alt + + def setnemposition(self, netif: CoreInterface) -> None: + """ + Publish a NEM location change event using the EMANE event service. + + :param netif: interface to set nem position for + """ + if self.session.emane.service is None: + logging.info("position service not available") + return + + position = self._nem_position(netif) + if position: + nemid, lon, lat, alt = position + event = LocationEvent() + event.append(nemid, latitude=lat, longitude=lon, altitude=alt) + self.session.emane.service.publish(0, event) def setnempositions(self, moved_netifs: List[CoreInterface]) -> None: """ @@ -223,18 +239,9 @@ class EmaneNet(CoreNetworkBase): return event = LocationEvent() - i = 0 for netif in moved_netifs: - nemid = self.getnemid(netif) - ifname = netif.localname - if nemid is None: - logging.info("nemid for %s is unknown", ifname) - continue - x, y, z = netif.node.getposition() - lat, lon, alt = self.session.location.getgeo(x, y, z) - # altitude must be an integer or warning is printed - alt = int(round(alt)) - event.append(nemid, latitude=lat, longitude=lon, altitude=alt) - i += 1 - + position = self._nem_position(netif) + if position: + nemid, lon, lat, alt = position + event.append(nemid, latitude=lat, longitude=lon, altitude=alt) self.session.emane.service.publish(0, event) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index b0e44cb5..3a02412f 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -806,6 +806,7 @@ class Session: using_lat_lon_alt = has_empty_position and has_lat_lon_alt if using_lat_lon_alt: x, y, _ = self.location.getxyz(lat, lon, alt) + node.position.set_geo(lon, lat, alt) # set position and broadcast if None not in [x, y]: diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 55af58d9..bf02385a 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -370,20 +370,16 @@ class BasicRangeModel(WirelessModel): with self._netifslock: return self._netifs[netif] - def set_position( - self, netif: CoreInterface, x: float = None, y: float = None, z: float = None - ) -> None: + def set_position(self, netif: CoreInterface) -> None: """ A node has moved; given an interface, a new (x,y,z) position has been set; calculate the new distance between other nodes and link or unlink node pairs based on the configured range. :param netif: network interface to set position for - :param x: x position - :param y: y position - :param z: z position :return: nothing """ + x, y, z = netif.node.position.get() self._netifslock.acquire() self._netifs[netif] = (x, y, z) if x is None or y is None: diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 9def7777..1c9417a9 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -422,7 +422,7 @@ class CoreNodeBase(NodeBase): changed = super().setposition(x, y, z) if changed: for netif in self.netifs(sort=True): - netif.setposition(x, y, z) + netif.setposition() def commonnets( self, obj: "CoreNodeBase", want_ctrl: bool = False @@ -1173,11 +1173,13 @@ class Position: :param x: x position :param y: y position :param z: z position - :return: """ self.x = x self.y = y self.z = z + self.lon = None + self.lat = None + self.alt = None def set(self, x: float = None, y: float = None, z: float = None) -> bool: """ @@ -1202,3 +1204,24 @@ class Position: :return: x,y,z position tuple """ return self.x, self.y, self.z + + def set_geo(self, lon: float, lat: float, alt: float) -> None: + """ + Set geo position lon, lat, alt. + + :param lon: longitude value + :param lat: latitude value + :param alt: altitude value + :return: nothing + """ + self.lon = lon + self.lat = lat + self.alt = alt + + def get_geo(self) -> Tuple[float, float, float]: + """ + Retrieve current geo position lon, lat, alt. + + :return: lon, lat, alt position tuple + """ + return self.lon, self.lat, self.alt diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 9ae01bfd..ea8e5012 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -50,7 +50,7 @@ class CoreInterface: self.addrlist = [] self.hwaddr = None # placeholder position hook - self.poshook = lambda a, b, c, d: None + self.poshook = lambda x: None # used with EMANE self.transport_type = None # node interface index @@ -209,16 +209,14 @@ class CoreInterface: self._params = getattr(self, name) setattr(self, name, tmp) - def setposition(self, x: float, y: float, z: float) -> None: + def setposition(self) -> None: """ - Dispatch position hook handler. + Dispatch position hook handler when possible. - :param x: x position - :param y: y position - :param z: z position :return: nothing """ - self.poshook(self, x, y, z) + if self.poshook and self.node: + self.poshook(self) def __lt__(self, other: "CoreInterface") -> bool: """ diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 3e8e7d8c..9547e6dc 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -1079,11 +1079,7 @@ class WlanNode(CoreNetwork): super().attach(netif) if self.model: netif.poshook = self.model.position_callback - if netif.node is None: - return - x, y, z = netif.node.position.get() - # invokes any netif.poshook - netif.setposition(x, y, z) + netif.setposition() def setmodel(self, model: "WirelessModelType", config: Dict[str, str]): """ @@ -1098,9 +1094,7 @@ class WlanNode(CoreNetwork): self.model = model(session=self.session, _id=self.id) for netif in self.netifs(): netif.poshook = self.model.position_callback - if netif.poshook and netif.node: - x, y, z = netif.node.position.get() - netif.poshook(netif, x, y, z) + netif.setposition() self.updatemodel(config) elif model.config_type == RegisterTlvs.MOBILITY.value: self.mobility = model(session=self.session, _id=self.id) @@ -1119,9 +1113,7 @@ class WlanNode(CoreNetwork): ) self.model.update_config(config) for netif in self.netifs(): - if netif.poshook and netif.node: - x, y, z = netif.node.position.get() - netif.poshook(netif, x, y, z) + netif.setposition() def all_link_data(self, flags: int) -> List[LinkData]: """ diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 8959fda1..4d9d21f4 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -517,7 +517,7 @@ class Rj45Node(CoreNodeBase, CoreInterface): if self.old_up: self.net_client.device_up(self.localname) - def setposition(self, x: float = None, y: float = None, z: float = None) -> bool: + def setposition(self, x: float = None, y: float = None, z: float = None) -> None: """ Uses setposition from both parent classes. @@ -526,9 +526,8 @@ class Rj45Node(CoreNodeBase, CoreInterface): :param z: z position :return: True if position changed, False otherwise """ - result = CoreNodeBase.setposition(self, x, y, z) - CoreInterface.setposition(self, x, y, z) - return result + CoreNodeBase.setposition(self, x, y, z) + CoreInterface.setposition(self) def termcmdstring(self, sh: str) -> str: """ From 7a5a0f34eaa208939f7f84b14b7d42fe9f808f56 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 22 Mar 2020 14:09:38 -0700 Subject: [PATCH 0094/1131] broke out tlv specific enums into their own module --- daemon/core/api/tlv/coreapi.py | 5 +- daemon/core/api/tlv/corehandlers.py | 22 +-- daemon/core/api/tlv/dataconversion.py | 2 +- daemon/core/api/tlv/enumerations.py | 212 +++++++++++++++++++++++++ daemon/core/emulator/enumerations.py | 213 +------------------------- daemon/scripts/core-daemon | 2 +- daemon/scripts/coresendmsg | 8 +- daemon/tests/test_grpc.py | 8 +- daemon/tests/test_gui.py | 9 +- 9 files changed, 237 insertions(+), 244 deletions(-) create mode 100644 daemon/core/api/tlv/enumerations.py diff --git a/daemon/core/api/tlv/coreapi.py b/daemon/core/api/tlv/coreapi.py index b72c186b..b8021b9f 100644 --- a/daemon/core/api/tlv/coreapi.py +++ b/daemon/core/api/tlv/coreapi.py @@ -13,7 +13,7 @@ from enum import Enum import netaddr from core.api.tlv import structutils -from core.emulator.enumerations import ( +from core.api.tlv.enumerations import ( ConfigTlvs, EventTlvs, ExceptionTlvs, @@ -21,12 +21,11 @@ from core.emulator.enumerations import ( FileTlvs, InterfaceTlvs, LinkTlvs, - MessageFlags, MessageTypes, NodeTlvs, - RegisterTlvs, SessionTlvs, ) +from core.emulator.enumerations import MessageFlags, RegisterTlvs class CoreTlvData: diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index ab737a0a..16094912 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -15,26 +15,28 @@ from queue import Empty, Queue from core import utils from core.api.tlv import coreapi, dataconversion, structutils +from core.api.tlv.enumerations import ( + ConfigFlags, + ConfigTlvs, + EventTlvs, + ExceptionTlvs, + ExecuteTlvs, + FileTlvs, + LinkTlvs, + MessageTypes, + NodeTlvs, + SessionTlvs, +) from core.config import ConfigShim from core.emulator.data import ConfigData, EventData, ExceptionData, FileData from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.enumerations import ( ConfigDataTypes, - ConfigFlags, - ConfigTlvs, - EventTlvs, EventTypes, - ExceptionTlvs, - ExecuteTlvs, - FileTlvs, - LinkTlvs, LinkTypes, MessageFlags, - MessageTypes, - NodeTlvs, NodeTypes, RegisterTlvs, - SessionTlvs, ) from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel diff --git a/daemon/core/api/tlv/dataconversion.py b/daemon/core/api/tlv/dataconversion.py index 35825b6a..1ff25db8 100644 --- a/daemon/core/api/tlv/dataconversion.py +++ b/daemon/core/api/tlv/dataconversion.py @@ -3,7 +3,7 @@ Converts CORE data objects into legacy API messages. """ from core.api.tlv import coreapi, structutils -from core.emulator.enumerations import ConfigTlvs, NodeTlvs +from core.api.tlv.enumerations import ConfigTlvs, NodeTlvs def convert_node(node_data): diff --git a/daemon/core/api/tlv/enumerations.py b/daemon/core/api/tlv/enumerations.py new file mode 100644 index 00000000..ed06bbe7 --- /dev/null +++ b/daemon/core/api/tlv/enumerations.py @@ -0,0 +1,212 @@ +""" +Enumerations specific to the CORE TLV API. +""" +from enum import Enum + +CORE_API_PORT = 4038 + + +class MessageTypes(Enum): + """ + CORE message types. + """ + + NODE = 0x01 + LINK = 0x02 + EXECUTE = 0x03 + REGISTER = 0x04 + CONFIG = 0x05 + FILE = 0x06 + INTERFACE = 0x07 + EVENT = 0x08 + SESSION = 0x09 + EXCEPTION = 0x0A + + +class NodeTlvs(Enum): + """ + Node type, length, value enumerations. + """ + + NUMBER = 0x01 + TYPE = 0x02 + NAME = 0x03 + IP_ADDRESS = 0x04 + MAC_ADDRESS = 0x05 + IP6_ADDRESS = 0x06 + MODEL = 0x07 + EMULATION_SERVER = 0x08 + SESSION = 0x0A + X_POSITION = 0x20 + Y_POSITION = 0x21 + CANVAS = 0x22 + EMULATION_ID = 0x23 + NETWORK_ID = 0x24 + SERVICES = 0x25 + LATITUDE = 0x30 + LONGITUDE = 0x31 + ALTITUDE = 0x32 + ICON = 0x42 + OPAQUE = 0x50 + + +class LinkTlvs(Enum): + """ + Link type, length, value enumerations. + """ + + N1_NUMBER = 0x01 + N2_NUMBER = 0x02 + DELAY = 0x03 + BANDWIDTH = 0x04 + PER = 0x05 + DUP = 0x06 + JITTER = 0x07 + MER = 0x08 + BURST = 0x09 + SESSION = 0x0A + MBURST = 0x10 + TYPE = 0x20 + GUI_ATTRIBUTES = 0x21 + UNIDIRECTIONAL = 0x22 + EMULATION_ID = 0x23 + NETWORK_ID = 0x24 + KEY = 0x25 + INTERFACE1_NUMBER = 0x30 + INTERFACE1_IP4 = 0x31 + INTERFACE1_IP4_MASK = 0x32 + INTERFACE1_MAC = 0x33 + INTERFACE1_IP6 = 0x34 + INTERFACE1_IP6_MASK = 0x35 + INTERFACE2_NUMBER = 0x36 + INTERFACE2_IP4 = 0x37 + INTERFACE2_IP4_MASK = 0x38 + INTERFACE2_MAC = 0x39 + INTERFACE2_IP6 = 0x40 + INTERFACE2_IP6_MASK = 0x41 + INTERFACE1_NAME = 0x42 + INTERFACE2_NAME = 0x43 + OPAQUE = 0x50 + + +class ExecuteTlvs(Enum): + """ + Execute type, length, value enumerations. + """ + + NODE = 0x01 + NUMBER = 0x02 + TIME = 0x03 + COMMAND = 0x04 + RESULT = 0x05 + STATUS = 0x06 + SESSION = 0x0A + + +class ConfigTlvs(Enum): + """ + Configuration type, length, value enumerations. + """ + + NODE = 0x01 + OBJECT = 0x02 + TYPE = 0x03 + DATA_TYPES = 0x04 + VALUES = 0x05 + CAPTIONS = 0x06 + BITMAP = 0x07 + POSSIBLE_VALUES = 0x08 + GROUPS = 0x09 + SESSION = 0x0A + INTERFACE_NUMBER = 0x0B + NETWORK_ID = 0x24 + OPAQUE = 0x50 + + +class ConfigFlags(Enum): + """ + Configuration flags. + """ + + NONE = 0x00 + REQUEST = 0x01 + UPDATE = 0x02 + RESET = 0x03 + + +class FileTlvs(Enum): + """ + File type, length, value enumerations. + """ + + NODE = 0x01 + NAME = 0x02 + MODE = 0x03 + NUMBER = 0x04 + TYPE = 0x05 + SOURCE_NAME = 0x06 + SESSION = 0x0A + DATA = 0x10 + COMPRESSED_DATA = 0x11 + + +class InterfaceTlvs(Enum): + """ + Interface type, length, value enumerations. + """ + + NODE = 0x01 + NUMBER = 0x02 + NAME = 0x03 + IP_ADDRESS = 0x04 + MASK = 0x05 + MAC_ADDRESS = 0x06 + IP6_ADDRESS = 0x07 + IP6_MASK = 0x08 + TYPE = 0x09 + SESSION = 0x0A + STATE = 0x0B + EMULATION_ID = 0x23 + NETWORK_ID = 0x24 + + +class EventTlvs(Enum): + """ + Event type, length, value enumerations. + """ + + NODE = 0x01 + TYPE = 0x02 + NAME = 0x03 + DATA = 0x04 + TIME = 0x05 + SESSION = 0x0A + + +class SessionTlvs(Enum): + """ + Session type, length, value enumerations. + """ + + NUMBER = 0x01 + NAME = 0x02 + FILE = 0x03 + NODE_COUNT = 0x04 + DATE = 0x05 + THUMB = 0x06 + USER = 0x07 + OPAQUE = 0x0A + + +class ExceptionTlvs(Enum): + """ + Exception type, length, value enumerations. + """ + + NODE = 0x01 + SESSION = 0x02 + LEVEL = 0x03 + SOURCE = 0x04 + DATE = 0x05 + TEXT = 0x06 + OPAQUE = 0x0A diff --git a/daemon/core/emulator/enumerations.py b/daemon/core/emulator/enumerations.py index 7ba8a734..937f22d9 100644 --- a/daemon/core/emulator/enumerations.py +++ b/daemon/core/emulator/enumerations.py @@ -1,29 +1,9 @@ """ -Contains all legacy enumerations for interacting with legacy CORE code. +Common enumerations used within CORE. """ from enum import Enum -CORE_API_VERSION = "1.23" -CORE_API_PORT = 4038 - - -class MessageTypes(Enum): - """ - CORE message types. - """ - - NODE = 0x01 - LINK = 0x02 - EXECUTE = 0x03 - REGISTER = 0x04 - CONFIG = 0x05 - FILE = 0x06 - INTERFACE = 0x07 - EVENT = 0x08 - SESSION = 0x09 - EXCEPTION = 0x0A - class MessageFlags(Enum): """ @@ -40,33 +20,6 @@ class MessageFlags(Enum): TTY = 0x40 -class NodeTlvs(Enum): - """ - Node type, length, value enumerations. - """ - - NUMBER = 0x01 - TYPE = 0x02 - NAME = 0x03 - IP_ADDRESS = 0x04 - MAC_ADDRESS = 0x05 - IP6_ADDRESS = 0x06 - MODEL = 0x07 - EMULATION_SERVER = 0x08 - SESSION = 0x0A - X_POSITION = 0x20 - Y_POSITION = 0x21 - CANVAS = 0x22 - EMULATION_ID = 0x23 - NETWORK_ID = 0x24 - SERVICES = 0x25 - LATITUDE = 0x30 - LONGITUDE = 0x31 - ALTITUDE = 0x32 - ICON = 0x42 - OPAQUE = 0x50 - - class NodeTypes(Enum): """ Node types. @@ -87,46 +40,6 @@ class NodeTypes(Enum): LXC = 16 -# Link Message TLV Types -class LinkTlvs(Enum): - """ - Link type, length, value enumerations. - """ - - N1_NUMBER = 0x01 - N2_NUMBER = 0x02 - DELAY = 0x03 - BANDWIDTH = 0x04 - PER = 0x05 - DUP = 0x06 - JITTER = 0x07 - MER = 0x08 - BURST = 0x09 - SESSION = 0x0A - MBURST = 0x10 - TYPE = 0x20 - GUI_ATTRIBUTES = 0x21 - UNIDIRECTIONAL = 0x22 - EMULATION_ID = 0x23 - NETWORK_ID = 0x24 - KEY = 0x25 - INTERFACE1_NUMBER = 0x30 - INTERFACE1_IP4 = 0x31 - INTERFACE1_IP4_MASK = 0x32 - INTERFACE1_MAC = 0x33 - INTERFACE1_IP6 = 0x34 - INTERFACE1_IP6_MASK = 0x35 - INTERFACE2_NUMBER = 0x36 - INTERFACE2_IP4 = 0x37 - INTERFACE2_IP4_MASK = 0x38 - INTERFACE2_MAC = 0x39 - INTERFACE2_IP6 = 0x40 - INTERFACE2_IP6_MASK = 0x41 - INTERFACE1_NAME = 0x42 - INTERFACE2_NAME = 0x43 - OPAQUE = 0x50 - - class LinkTypes(Enum): """ Link types. @@ -136,20 +49,7 @@ class LinkTypes(Enum): WIRED = 1 -class ExecuteTlvs(Enum): - """ - Execute type, length, value enumerations. - """ - - NODE = 0x01 - NUMBER = 0x02 - TIME = 0x03 - COMMAND = 0x04 - RESULT = 0x05 - STATUS = 0x06 - SESSION = 0x0A - - +# TODO: cleanup usage of .value class RegisterTlvs(Enum): """ Register type, length, value enumerations. @@ -164,37 +64,6 @@ class RegisterTlvs(Enum): SESSION = 0x0A -class ConfigTlvs(Enum): - """ - Configuration type, length, value enumerations. - """ - - NODE = 0x01 - OBJECT = 0x02 - TYPE = 0x03 - DATA_TYPES = 0x04 - VALUES = 0x05 - CAPTIONS = 0x06 - BITMAP = 0x07 - POSSIBLE_VALUES = 0x08 - GROUPS = 0x09 - SESSION = 0x0A - INTERFACE_NUMBER = 0x0B - NETWORK_ID = 0x24 - OPAQUE = 0x50 - - -class ConfigFlags(Enum): - """ - Configuration flags. - """ - - NONE = 0x00 - REQUEST = 0x01 - UPDATE = 0x02 - RESET = 0x03 - - class ConfigDataTypes(Enum): """ Configuration data types. @@ -213,55 +82,6 @@ class ConfigDataTypes(Enum): BOOL = 0x0B -class FileTlvs(Enum): - """ - File type, length, value enumerations. - """ - - NODE = 0x01 - NAME = 0x02 - MODE = 0x03 - NUMBER = 0x04 - TYPE = 0x05 - SOURCE_NAME = 0x06 - SESSION = 0x0A - DATA = 0x10 - COMPRESSED_DATA = 0x11 - - -class InterfaceTlvs(Enum): - """ - Interface type, length, value enumerations. - """ - - NODE = 0x01 - NUMBER = 0x02 - NAME = 0x03 - IP_ADDRESS = 0x04 - MASK = 0x05 - MAC_ADDRESS = 0x06 - IP6_ADDRESS = 0x07 - IP6_MASK = 0x08 - TYPE = 0x09 - SESSION = 0x0A - STATE = 0x0B - EMULATION_ID = 0x23 - NETWORK_ID = 0x24 - - -class EventTlvs(Enum): - """ - Event type, length, value enumerations. - """ - - NODE = 0x01 - TYPE = 0x02 - NAME = 0x03 - DATA = 0x04 - TIME = 0x05 - SESSION = 0x0A - - class EventTypes(Enum): """ Event types. @@ -288,35 +108,6 @@ class EventTypes(Enum): return self.value > self.DEFINITION_STATE.value -class SessionTlvs(Enum): - """ - Session type, length, value enumerations. - """ - - NUMBER = 0x01 - NAME = 0x02 - FILE = 0x03 - NODE_COUNT = 0x04 - DATE = 0x05 - THUMB = 0x06 - USER = 0x07 - OPAQUE = 0x0A - - -class ExceptionTlvs(Enum): - """ - Exception type, length, value enumerations. - """ - - NODE = 0x01 - SESSION = 0x02 - LEVEL = 0x03 - SOURCE = 0x04 - DATE = 0x05 - TEXT = 0x06 - OPAQUE = 0x0A - - class ExceptionLevels(Enum): """ Exception levels. diff --git a/daemon/scripts/core-daemon b/daemon/scripts/core-daemon index 6b55c14f..866c5472 100755 --- a/daemon/scripts/core-daemon +++ b/daemon/scripts/core-daemon @@ -17,8 +17,8 @@ from core import constants from core.api.grpc.server import CoreGrpcServer from core.api.tlv.corehandlers import CoreHandler, CoreUdpHandler from core.api.tlv.coreserver import CoreServer, CoreUdpServer +from core.api.tlv.enumerations import CORE_API_PORT from core.constants import CORE_CONF_DIR, COREDPY_VERSION -from core.emulator.enumerations import CORE_API_PORT from core.utils import close_onexec, load_logging_config diff --git a/daemon/scripts/coresendmsg b/daemon/scripts/coresendmsg index c8dccfd7..a909522f 100755 --- a/daemon/scripts/coresendmsg +++ b/daemon/scripts/coresendmsg @@ -9,12 +9,8 @@ import socket import sys from core.api.tlv import coreapi -from core.emulator.enumerations import ( - CORE_API_PORT, - MessageFlags, - MessageTypes, - SessionTlvs, -) +from core.api.tlv.enumerations import CORE_API_PORT, MessageTypes, SessionTlvs +from core.emulator.enumerations import MessageFlags def print_available_tlvs(t, tlv_class): diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index e2397b53..721ca240 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -7,16 +7,12 @@ from mock import patch from core.api.grpc import core_pb2 from core.api.grpc.client import CoreGrpcClient, InterfaceHelper +from core.api.tlv.enumerations import ConfigFlags from core.config import ConfigShim from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emulator.data import EventData from core.emulator.emudata import NodeOptions -from core.emulator.enumerations import ( - ConfigFlags, - EventTypes, - ExceptionLevels, - NodeTypes, -) +from core.emulator.enumerations import EventTypes, ExceptionLevels, NodeTypes from core.errors import CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.xml.corexml import CoreXmlWriter diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index 52969e93..481a0fa9 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -10,21 +10,18 @@ import pytest from mock import MagicMock from core.api.tlv import coreapi -from core.emane.ieee80211abg import EmaneIeee80211abgModel -from core.emulator.enumerations import ( +from core.api.tlv.enumerations import ( ConfigFlags, ConfigTlvs, EventTlvs, - EventTypes, ExecuteTlvs, FileTlvs, LinkTlvs, - MessageFlags, NodeTlvs, - NodeTypes, - RegisterTlvs, SessionTlvs, ) +from core.emane.ieee80211abg import EmaneIeee80211abgModel +from core.emulator.enumerations import EventTypes, MessageFlags, NodeTypes, RegisterTlvs from core.errors import CoreError from core.location.mobility import BasicRangeModel From 39499a4ab46a87594d50d2ce1ff36222a081bc73 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 22 Mar 2020 14:59:42 -0700 Subject: [PATCH 0095/1131] moved ConfigShim to being under tlv, updated RegisterTlvs to use enums directly in non tlv code --- daemon/core/api/tlv/corehandlers.py | 17 ++--- daemon/core/api/tlv/dataconversion.py | 104 ++++++++++++++++++++++++++ daemon/core/config.py | 99 ------------------------ daemon/core/emane/emanemanager.py | 2 +- daemon/core/emane/nodes.py | 4 +- daemon/core/emulator/enumerations.py | 1 - daemon/core/emulator/session.py | 2 +- daemon/core/emulator/sessionconfig.py | 2 +- daemon/core/location/geo.py | 2 +- daemon/core/location/mobility.py | 11 +-- daemon/core/nodes/network.py | 4 +- daemon/core/plugins/sdt.py | 4 +- daemon/core/services/coreservices.py | 2 +- daemon/tests/test_grpc.py | 2 +- 14 files changed, 128 insertions(+), 128 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 16094912..da2d730d 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -15,6 +15,7 @@ from queue import Empty, Queue from core import utils from core.api.tlv import coreapi, dataconversion, structutils +from core.api.tlv.dataconversion import ConfigShim from core.api.tlv.enumerations import ( ConfigFlags, ConfigTlvs, @@ -27,7 +28,6 @@ from core.api.tlv.enumerations import ( NodeTlvs, SessionTlvs, ) -from core.config import ConfigShim from core.emulator.data import ConfigData, EventData, ExceptionData, FileData from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.enumerations import ( @@ -398,7 +398,6 @@ class CoreHandler(socketserver.BaseRequestHandler): logging.info( "GUI has connected to session %d at %s", self.session.id, time.ctime() ) - tlv_data = b"" tlv_data += coreapi.CoreRegisterTlv.pack( RegisterTlvs.EXECUTE_SERVER.value, "core-daemon" @@ -408,29 +407,29 @@ class CoreHandler(socketserver.BaseRequestHandler): ) tlv_data += coreapi.CoreRegisterTlv.pack(RegisterTlvs.UTILITY.value, "broker") tlv_data += coreapi.CoreRegisterTlv.pack( - self.session.location.config_type, self.session.location.name + self.session.location.config_type.value, self.session.location.name ) tlv_data += coreapi.CoreRegisterTlv.pack( - self.session.mobility.config_type, self.session.mobility.name + self.session.mobility.config_type.value, self.session.mobility.name ) for model_name in self.session.mobility.models: model_class = self.session.mobility.models[model_name] tlv_data += coreapi.CoreRegisterTlv.pack( - model_class.config_type, model_class.name + model_class.config_type.value, model_class.name ) tlv_data += coreapi.CoreRegisterTlv.pack( - self.session.services.config_type, self.session.services.name + self.session.services.config_type.value, self.session.services.name ) tlv_data += coreapi.CoreRegisterTlv.pack( - self.session.emane.config_type, self.session.emane.name + self.session.emane.config_type.value, self.session.emane.name ) for model_name in self.session.emane.models: model_class = self.session.emane.models[model_name] tlv_data += coreapi.CoreRegisterTlv.pack( - model_class.config_type, model_class.name + model_class.config_type.value, model_class.name ) tlv_data += coreapi.CoreRegisterTlv.pack( - self.session.options.config_type, self.session.options.name + self.session.options.config_type.value, self.session.options.name ) tlv_data += coreapi.CoreRegisterTlv.pack(RegisterTlvs.UTILITY.value, "metadata") diff --git a/daemon/core/api/tlv/dataconversion.py b/daemon/core/api/tlv/dataconversion.py index 1ff25db8..21730afb 100644 --- a/daemon/core/api/tlv/dataconversion.py +++ b/daemon/core/api/tlv/dataconversion.py @@ -1,9 +1,14 @@ """ Converts CORE data objects into legacy API messages. """ +import logging +from collections import OrderedDict +from typing import Dict, List from core.api.tlv import coreapi, structutils from core.api.tlv.enumerations import ConfigTlvs, NodeTlvs +from core.config import ConfigGroup, ConfigurableOptions +from core.emulator.data import ConfigData def convert_node(node_data): @@ -67,3 +72,102 @@ def convert_config(config_data): ], ) return coreapi.CoreConfMessage.pack(config_data.message_type, tlv_data) + + +class ConfigShim: + """ + Provides helper methods for converting newer configuration values into TLV + compatible formats. + """ + + @classmethod + def str_to_dict(cls, key_values: str) -> Dict[str, str]: + """ + Converts a TLV key/value string into an ordered mapping. + + :param key_values: + :return: ordered mapping of key/value pairs + """ + key_values = key_values.split("|") + values = OrderedDict() + for key_value in key_values: + key, value = key_value.split("=", 1) + values[key] = value + return values + + @classmethod + def groups_to_str(cls, config_groups: List[ConfigGroup]) -> str: + """ + Converts configuration groups to a TLV formatted string. + + :param config_groups: configuration groups to format + :return: TLV configuration group string + """ + group_strings = [] + for config_group in config_groups: + group_string = ( + f"{config_group.name}:{config_group.start}-{config_group.stop}" + ) + group_strings.append(group_string) + return "|".join(group_strings) + + @classmethod + def config_data( + cls, + flags: int, + node_id: int, + type_flags: int, + configurable_options: ConfigurableOptions, + config: Dict[str, str], + ) -> ConfigData: + """ + Convert this class to a Config API message. Some TLVs are defined + by the class, but node number, conf type flags, and values must + be passed in. + + :param flags: message flags + :param node_id: node id + :param type_flags: type flags + :param configurable_options: options to create config data for + :param config: configuration values for options + :return: configuration data object + """ + key_values = None + captions = None + data_types = [] + possible_values = [] + logging.debug("configurable: %s", configurable_options) + logging.debug("configuration options: %s", configurable_options.configurations) + logging.debug("configuration data: %s", config) + for configuration in configurable_options.configurations(): + if not captions: + captions = configuration.label + else: + captions += f"|{configuration.label}" + + data_types.append(configuration.type.value) + + options = ",".join(configuration.options) + possible_values.append(options) + + _id = configuration.id + config_value = config.get(_id, configuration.default) + key_value = f"{_id}={config_value}" + if not key_values: + key_values = key_value + else: + key_values += f"|{key_value}" + + groups_str = cls.groups_to_str(configurable_options.config_groups()) + return ConfigData( + message_type=flags, + node=node_id, + object=configurable_options.name, + type=type_flags, + data_types=tuple(data_types), + data_values=key_values, + captions=captions, + possible_values="|".join(possible_values), + bitmap=configurable_options.bitmap, + groups=groups_str, + ) diff --git a/daemon/core/config.py b/daemon/core/config.py index b7c20362..1f5bc3c0 100644 --- a/daemon/core/config.py +++ b/daemon/core/config.py @@ -7,7 +7,6 @@ from collections import OrderedDict from typing import TYPE_CHECKING, Dict, List, Tuple, Type, Union from core.emane.nodes import EmaneNet -from core.emulator.data import ConfigData from core.emulator.enumerations import ConfigDataTypes from core.nodes.network import WlanNode @@ -110,104 +109,6 @@ class ConfigurableOptions: ) -class ConfigShim: - """ - Provides helper methods for converting newer configuration values into TLV compatible formats. - """ - - @classmethod - def str_to_dict(cls, key_values: str) -> Dict[str, str]: - """ - Converts a TLV key/value string into an ordered mapping. - - :param key_values: - :return: ordered mapping of key/value pairs - """ - key_values = key_values.split("|") - values = OrderedDict() - for key_value in key_values: - key, value = key_value.split("=", 1) - values[key] = value - return values - - @classmethod - def groups_to_str(cls, config_groups: List[ConfigGroup]) -> str: - """ - Converts configuration groups to a TLV formatted string. - - :param config_groups: configuration groups to format - :return: TLV configuration group string - """ - group_strings = [] - for config_group in config_groups: - group_string = ( - f"{config_group.name}:{config_group.start}-{config_group.stop}" - ) - group_strings.append(group_string) - return "|".join(group_strings) - - @classmethod - def config_data( - cls, - flags: int, - node_id: int, - type_flags: int, - configurable_options: ConfigurableOptions, - config: Dict[str, str], - ) -> ConfigData: - """ - Convert this class to a Config API message. Some TLVs are defined - by the class, but node number, conf type flags, and values must - be passed in. - - :param flags: message flags - :param node_id: node id - :param type_flags: type flags - :param configurable_options: options to create config data for - :param config: configuration values for options - :return: configuration data object - """ - key_values = None - captions = None - data_types = [] - possible_values = [] - logging.debug("configurable: %s", configurable_options) - logging.debug("configuration options: %s", configurable_options.configurations) - logging.debug("configuration data: %s", config) - for configuration in configurable_options.configurations(): - if not captions: - captions = configuration.label - else: - captions += f"|{configuration.label}" - - data_types.append(configuration.type.value) - - options = ",".join(configuration.options) - possible_values.append(options) - - _id = configuration.id - config_value = config.get(_id, configuration.default) - key_value = f"{_id}={config_value}" - if not key_values: - key_values = key_value - else: - key_values += f"|{key_value}" - - groups_str = cls.groups_to_str(configurable_options.config_groups()) - return ConfigData( - message_type=flags, - node=node_id, - object=configurable_options.name, - type=type_flags, - data_types=tuple(data_types), - data_values=key_values, - captions=captions, - possible_values="|".join(possible_values), - bitmap=configurable_options.bitmap, - groups=groups_str, - ) - - class ConfigurableManager: """ Provides convenience methods for storing and retrieving configuration options for diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index dfee9463..37185c93 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -60,7 +60,7 @@ class EmaneManager(ModelManager): """ name = "emane" - config_type = RegisterTlvs.EMULATION_SERVER.value + config_type = RegisterTlvs.EMULATION_SERVER SUCCESS, NOT_NEEDED, NOT_READY = (0, 1, 2) EVENTCFGVAR = "LIBEMANEEVENTSERVICECONFIG" DEFAULT_LOG_LEVEL = 3 diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 7c2eb82b..d8984f7c 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -103,12 +103,12 @@ class EmaneNet(CoreNetworkBase): set the EmaneModel associated with this node """ logging.info("adding model: %s", model.name) - if model.config_type == RegisterTlvs.WIRELESS.value: + if model.config_type == RegisterTlvs.WIRELESS: # EmaneModel really uses values from ConfigurableManager # when buildnemxml() is called, not during init() self.model = model(session=self.session, _id=self.id) self.model.update_config(config) - elif model.config_type == RegisterTlvs.MOBILITY.value: + elif model.config_type == RegisterTlvs.MOBILITY: self.mobility = model(session=self.session, _id=self.id) self.mobility.update_config(config) diff --git a/daemon/core/emulator/enumerations.py b/daemon/core/emulator/enumerations.py index 937f22d9..2c6e14db 100644 --- a/daemon/core/emulator/enumerations.py +++ b/daemon/core/emulator/enumerations.py @@ -49,7 +49,6 @@ class LinkTypes(Enum): WIRED = 1 -# TODO: cleanup usage of .value class RegisterTlvs(Enum): """ Register type, length, value enumerations. diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 2f717e0a..8bc10826 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -1897,7 +1897,7 @@ class Session: Return the current time we have been in the runtime state, or zero if not in runtime. """ - if self.state == EventTypes.RUNTIME_STATE.value: + if self.state == EventTypes.RUNTIME_STATE: return time.monotonic() - self._state_time else: return 0.0 diff --git a/daemon/core/emulator/sessionconfig.py b/daemon/core/emulator/sessionconfig.py index 5f2d5916..ffeccdc4 100644 --- a/daemon/core/emulator/sessionconfig.py +++ b/daemon/core/emulator/sessionconfig.py @@ -57,7 +57,7 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions): label="SDT3D URL", ), ] - config_type = RegisterTlvs.UTILITY.value + config_type = RegisterTlvs.UTILITY def __init__(self) -> None: super().__init__() diff --git a/daemon/core/location/geo.py b/daemon/core/location/geo.py index 7d3aea22..1f78f329 100644 --- a/daemon/core/location/geo.py +++ b/daemon/core/location/geo.py @@ -21,7 +21,7 @@ class GeoLocation: """ name = "location" - config_type = RegisterTlvs.UTILITY.value + config_type = RegisterTlvs.UTILITY def __init__(self) -> None: """ diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 802b3c29..62d954fa 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -36,7 +36,7 @@ class MobilityManager(ModelManager): """ name = "MobilityManager" - config_type = RegisterTlvs.WIRELESS.value + config_type = RegisterTlvs.WIRELESS def __init__(self, session: "Session") -> None: """ @@ -121,10 +121,7 @@ class MobilityManager(ModelManager): logging.warning("Ignoring event for unknown model '%s'", model) continue - if cls.config_type in [ - RegisterTlvs.WIRELESS.value, - RegisterTlvs.MOBILITY.value, - ]: + if cls.config_type in [RegisterTlvs.WIRELESS, RegisterTlvs.MOBILITY]: model = node.mobility else: continue @@ -206,7 +203,7 @@ class WirelessModel(ConfigurableOptions): Used for managing arbitrary configuration parameters. """ - config_type = RegisterTlvs.WIRELESS.value + config_type = RegisterTlvs.WIRELESS bitmap = None position_callback = None @@ -575,7 +572,7 @@ class WayPointMobility(WirelessModel): """ name = "waypoint" - config_type = RegisterTlvs.MOBILITY.value + config_type = RegisterTlvs.MOBILITY STATE_STOPPED = 0 STATE_RUNNING = 1 diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index c3a1ad89..dded924d 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -1090,13 +1090,13 @@ class WlanNode(CoreNetwork): :return: nothing """ logging.debug("node(%s) setting model: %s", self.name, model.name) - if model.config_type == RegisterTlvs.WIRELESS.value: + if model.config_type == RegisterTlvs.WIRELESS: self.model = model(session=self.session, _id=self.id) for netif in self.netifs(): netif.poshook = self.model.position_callback netif.setposition() self.updatemodel(config) - elif model.config_type == RegisterTlvs.MOBILITY.value: + elif model.config_type == RegisterTlvs.MOBILITY: self.mobility = model(session=self.session, _id=self.id) self.mobility.update_config(config) diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 156a945d..a759228d 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -90,7 +90,7 @@ class Sdt: self.address = (self.url.hostname, self.url.port) self.protocol = self.url.scheme - def connect(self, flags: int = 0) -> bool: + def connect(self) -> bool: """ Connect to the SDT address/port if enabled. @@ -122,7 +122,7 @@ class Sdt: self.connected = True # refresh all objects in SDT3D when connecting after session start - if not flags & MessageFlags.ADD.value and not self.sendobjs(): + if not self.sendobjs(): return False return True diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index 9ae8ffa6..6323ceab 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -315,7 +315,7 @@ class CoreServices: """ name = "services" - config_type = RegisterTlvs.UTILITY.value + config_type = RegisterTlvs.UTILITY def __init__(self, session: "Session") -> None: """ diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 721ca240..6feacac3 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -7,8 +7,8 @@ from mock import patch from core.api.grpc import core_pb2 from core.api.grpc.client import CoreGrpcClient, InterfaceHelper +from core.api.tlv.dataconversion import ConfigShim from core.api.tlv.enumerations import ConfigFlags -from core.config import ConfigShim from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emulator.data import EventData from core.emulator.emudata import NodeOptions From 33bcc24d88d51c150a6e8b332a23a31d7220ee89 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 22 Mar 2020 16:38:15 -0700 Subject: [PATCH 0096/1131] cleaned up broadcast_node to use nodes directly --- daemon/core/api/grpc/server.py | 3 +- daemon/core/api/tlv/corehandlers.py | 13 ++---- daemon/core/emane/emanemanager.py | 3 +- daemon/core/emulator/session.py | 66 ++++++++++++----------------- daemon/core/location/mobility.py | 3 +- daemon/core/nodes/base.py | 22 +++------- daemon/core/nodes/network.py | 10 +---- daemon/tests/test_grpc.py | 3 +- 8 files changed, 39 insertions(+), 84 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index cabc0f1c..c76bb64f 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -721,8 +721,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): if request.source: source = request.source if not has_geo: - node_data = node.data(source=source) - session.broadcast_node(node_data) + session.broadcast_node(node, source=source) except CoreError: result = False return core_pb2.EditNodeResponse(result=result) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index da2d730d..536f436d 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -1836,24 +1836,16 @@ class CoreHandler(socketserver.BaseRequestHandler): Return API messages that describe the current session. """ # find all nodes and links - - nodes_data = [] links_data = [] with self.session._nodes_lock: for node_id in self.session.nodes: node = self.session.nodes[node_id] - node_data = node.data(message_type=MessageFlags.ADD) - if node_data: - nodes_data.append(node_data) + self.session.broadcast_node(node, MessageFlags.ADD) node_links = node.all_link_data(flags=MessageFlags.ADD) for link_data in node_links: links_data.append(link_data) - # send all nodes first, so that they will exist for any links - for node_data in nodes_data: - self.session.broadcast_node(node_data) - for link_data in links_data: self.session.broadcast_link(link_data) @@ -1960,8 +1952,9 @@ class CoreHandler(socketserver.BaseRequestHandler): ) self.session.broadcast_config(config_data) + node_count = self.session.get_node_count() logging.info( - "informed GUI about %d nodes and %d links", len(nodes_data), len(links_data) + "informed GUI about %d nodes and %d links", node_count, len(links_data) ) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 37185c93..82e37f43 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -806,8 +806,7 @@ class EmaneManager(ModelManager): # don"t use node.setposition(x,y,z) which generates an event node.position.set(x, y, z) node.position.set_geo(lon, lat, alt) - node_data = node.data(lat=lat, lon=lon, alt=alt) - self.session.broadcast_node(node_data) + self.session.broadcast_node(node) return True def emanerunning(self, node: CoreNode) -> bool: diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 8bc10826..ac907911 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -17,14 +17,7 @@ from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type from core import constants, utils from core.emane.emanemanager import EmaneManager from core.emane.nodes import EmaneNet -from core.emulator.data import ( - ConfigData, - EventData, - ExceptionData, - FileData, - LinkData, - NodeData, -) +from core.emulator.data import ConfigData, EventData, ExceptionData, FileData, LinkData from core.emulator.distributed import DistributedController from core.emulator.emudata import ( IdGen, @@ -34,7 +27,13 @@ from core.emulator.emudata import ( create_interface, link_config, ) -from core.emulator.enumerations import EventTypes, ExceptionLevels, LinkTypes, NodeTypes +from core.emulator.enumerations import ( + EventTypes, + ExceptionLevels, + LinkTypes, + MessageFlags, + NodeTypes, +) from core.emulator.sessionconfig import SessionConfig from core.errors import CoreError from core.location.event import EventLoop @@ -805,35 +804,13 @@ class Session: using_lat_lon_alt = has_empty_position and has_lat_lon_alt if using_lat_lon_alt: x, y, _ = self.location.getxyz(lat, lon, alt) - node.position.set_geo(lon, lat, alt) - - # set position and broadcast - if None not in [x, y]: node.setposition(x, y, None) - - # broadcast updated location when using lat/lon/alt - if using_lat_lon_alt: - self.broadcast_node_location(node, lon, lat, alt) - - def broadcast_node_location( - self, node: NodeBase, lon: float, lat: float, alt: float - ) -> None: - """ - Broadcast node location to all listeners. - - :param node: node to broadcast location for - :return: nothing - """ - node_data = NodeData( - message_type=0, - id=node.id, - x_position=node.position.x, - y_position=node.position.y, - latitude=lat, - longitude=lon, - altitude=alt, - ) - self.broadcast_node(node_data) + node.position.set_geo(lon, lat, alt) + self.broadcast_node(node) + else: + if has_empty_position: + x, y = 0, 0 + node.setposition(x, y, None) def start_mobility(self, node_ids: List[int] = None) -> None: """ @@ -1026,14 +1003,23 @@ class Session: for handler in self.exception_handlers: handler(exception_data) - def broadcast_node(self, node_data: NodeData) -> None: + def broadcast_node( + self, + node: NodeBase, + message_type: MessageFlags = MessageFlags.NONE, + source: str = None, + ) -> None: """ Handle node data that should be provided to node handlers. - :param node_data: node data to send out + :param node: node to broadcast + :param message_type: type of message to broadcast, None by default + :param source: source of broadcast, None by default :return: nothing """ - + node_data = node.data(message_type, source) + if not node_data: + return for handler in self.node_handlers: handler(node_data) diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 62d954fa..e4fc8658 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -812,8 +812,7 @@ class WayPointMobility(WirelessModel): :return: nothing """ node.position.set(x, y, z) - node_data = node.data() - self.session.broadcast_node(node_data) + self.session.broadcast_node(node) def setendtime(self) -> None: """ diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 8f5354f2..2df3fd74 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -192,20 +192,12 @@ class NodeBase: return ifindex def data( - self, - message_type: MessageFlags = MessageFlags.NONE, - lat: float = None, - lon: float = None, - alt: float = None, - source: str = None, + self, message_type: MessageFlags = MessageFlags.NONE, source: str = None ) -> NodeData: """ Build a data object for this node. :param message_type: purpose for the data object we are creating - :param lat: latitude - :param lon: longitude - :param alt: altitude :param source: source of node data :return: node data object """ @@ -217,12 +209,10 @@ class NodeBase: server = None if self.server is not None: server = self.server.name - services = self.services if services is not None: services = "|".join([service.name for service in services]) - - node_data = NodeData( + return NodeData( message_type=message_type, id=self.id, node_type=self.apitype, @@ -233,17 +223,15 @@ class NodeBase: opaque=self.opaque, x_position=x, y_position=y, - latitude=lat, - longitude=lon, - altitude=alt, + latitude=self.position.lat, + longitude=self.position.lon, + altitude=self.position.alt, model=model, server=server, services=services, source=source, ) - return node_data - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Build CORE Link data for this object. There is no default diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index dded924d..ff6e6ceb 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -879,21 +879,13 @@ class PtpNet(CoreNetwork): super().attach(netif) def data( - self, - message_type: int, - lat: float = None, - lon: float = None, - alt: float = None, - source: str = None, + self, message_type: MessageFlags = MessageFlags.NONE, source: str = None ) -> NodeData: """ Do not generate a Node Message for point-to-point links. They are built using a link message instead. :param message_type: purpose for the data object we are creating - :param lat: latitude - :param lon: longitude - :param alt: altitude :param source: source of node data :return: node data object """ diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 6feacac3..fadcd8e2 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -982,7 +982,6 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() node = session.add_node() - node_data = node.data() queue = Queue() def handle_event(event_data): @@ -994,7 +993,7 @@ class TestGrpc: with client.context_connect(): client.events(session.id, handle_event) time.sleep(0.1) - session.broadcast_node(node_data) + session.broadcast_node(node) # then queue.get(timeout=5) From 14e708681c643f6111f5aea35183f2c03d2e8481 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 22 Mar 2020 21:08:12 -0700 Subject: [PATCH 0097/1131] small tweak to corehandlers logic --- daemon/core/api/tlv/corehandlers.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 536f436d..117b102e 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -1841,10 +1841,8 @@ class CoreHandler(socketserver.BaseRequestHandler): for node_id in self.session.nodes: node = self.session.nodes[node_id] self.session.broadcast_node(node, MessageFlags.ADD) - node_links = node.all_link_data(flags=MessageFlags.ADD) - for link_data in node_links: - links_data.append(link_data) + links_data.extend(node_links) for link_data in links_data: self.session.broadcast_link(link_data) From 38f9f44cdfed2d7f981206820f1a4139a7b0f675 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 22 Mar 2020 21:15:11 -0700 Subject: [PATCH 0098/1131] fixed type hinting and bad return value --- daemon/core/location/mobility.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index e4fc8658..06a78307 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -474,7 +474,6 @@ class BasicRangeModel(WirelessModel): """ self.values_from_config(config) self.setlinkparams() - return True def create_link_data( self, @@ -790,7 +789,7 @@ class WayPointMobility(WirelessModel): """ self.queue_copy = list(self.queue) - def loopwaypoints(self) -> None: + def loopwaypoints(self) -> bool: """ Restore backup copy of waypoints when looping. From 6a410128575718c42f9b73e46cc160b40b70a78d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 22 Mar 2020 22:57:50 -0700 Subject: [PATCH 0099/1131] updates to break up core.proto into separate logical files --- daemon/core/api/grpc/client.py | 209 ++++++---- daemon/core/api/grpc/grpcutils.py | 7 +- daemon/core/api/grpc/server.py | 229 +++++++---- daemon/core/gui/coreclient.py | 32 +- .../core/gui/dialogs/configserviceconfig.py | 6 +- daemon/core/gui/dialogs/mobilityplayer.py | 2 +- daemon/core/gui/dialogs/serviceconfig.py | 6 +- daemon/proto/core/api/grpc/common.proto | 4 + daemon/proto/core/api/grpc/core.proto | 382 ++---------------- daemon/proto/core/api/grpc/emane.proto | 92 +++++ daemon/proto/core/api/grpc/mobility.proto | 54 +++ daemon/proto/core/api/grpc/services.proto | 149 +++++++ daemon/proto/core/api/grpc/wlan.proto | 36 ++ daemon/tests/test_grpc.py | 20 +- 14 files changed, 673 insertions(+), 555 deletions(-) create mode 100644 daemon/proto/core/api/grpc/emane.proto create mode 100644 daemon/proto/core/api/grpc/mobility.proto create mode 100644 daemon/proto/core/api/grpc/services.proto create mode 100644 daemon/proto/core/api/grpc/wlan.proto diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index cad8b2f2..871e75e7 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -26,11 +26,69 @@ from core.api.grpc.configservices_pb2 import ( SetNodeConfigServiceRequest, SetNodeConfigServiceResponse, ) -from core.api.grpc.core_pb2 import ( - ExecuteScriptRequest, - ExecuteScriptResponse, +from core.api.grpc.core_pb2 import ExecuteScriptRequest, ExecuteScriptResponse +from core.api.grpc.emane_pb2 import ( + EmaneLinkRequest, + EmaneLinkResponse, + EmaneModelConfig, + GetEmaneConfigRequest, + GetEmaneConfigResponse, GetEmaneEventChannelRequest, GetEmaneEventChannelResponse, + GetEmaneModelConfigRequest, + GetEmaneModelConfigResponse, + GetEmaneModelConfigsRequest, + GetEmaneModelConfigsResponse, + GetEmaneModelsRequest, + GetEmaneModelsResponse, + SetEmaneConfigRequest, + SetEmaneConfigResponse, + SetEmaneModelConfigRequest, + SetEmaneModelConfigResponse, +) +from core.api.grpc.mobility_pb2 import ( + GetMobilityConfigRequest, + GetMobilityConfigResponse, + GetMobilityConfigsRequest, + GetMobilityConfigsResponse, + MobilityActionRequest, + MobilityActionResponse, + MobilityConfig, + SetMobilityConfigRequest, + SetMobilityConfigResponse, +) +from core.api.grpc.services_pb2 import ( + GetNodeServiceConfigsRequest, + GetNodeServiceConfigsResponse, + GetNodeServiceFileRequest, + GetNodeServiceFileResponse, + GetNodeServiceRequest, + GetNodeServiceResponse, + GetServiceDefaultsRequest, + GetServiceDefaultsResponse, + GetServicesRequest, + GetServicesResponse, + ServiceAction, + ServiceActionRequest, + ServiceActionResponse, + ServiceConfig, + ServiceDefaults, + ServiceFileConfig, + SetNodeServiceFileRequest, + SetNodeServiceFileResponse, + SetNodeServiceRequest, + SetNodeServiceResponse, + SetServiceDefaultsRequest, + SetServiceDefaultsResponse, +) +from core.api.grpc.wlan_pb2 import ( + GetWlanConfigRequest, + GetWlanConfigResponse, + GetWlanConfigsRequest, + GetWlanConfigsResponse, + SetWlanConfigRequest, + SetWlanConfigResponse, + WlanConfig, ) @@ -178,11 +236,11 @@ class CoreGrpcClient: location: core_pb2.SessionLocation = None, hooks: List[core_pb2.Hook] = None, emane_config: Dict[str, str] = None, - emane_model_configs: List[core_pb2.EmaneModelConfig] = None, - wlan_configs: List[core_pb2.WlanConfig] = None, - mobility_configs: List[core_pb2.MobilityConfig] = None, - service_configs: List[core_pb2.ServiceConfig] = None, - service_file_configs: List[core_pb2.ServiceFileConfig] = None, + emane_model_configs: List[EmaneModelConfig] = None, + wlan_configs: List[WlanConfig] = None, + mobility_configs: List[MobilityConfig] = None, + service_configs: List[ServiceConfig] = None, + service_file_configs: List[ServiceFileConfig] = None, asymmetric_links: List[core_pb2.Link] = None, config_service_configs: List[configservices_pb2.ConfigServiceConfig] = None, ) -> core_pb2.StartSessionResponse: @@ -678,7 +736,7 @@ class CoreGrpcClient: session_id: int, state: core_pb2.SessionState, file_name: str, - file_data: bytes, + file_data: str, ) -> core_pb2.AddHookResponse: """ Add hook scripts. @@ -694,9 +752,7 @@ class CoreGrpcClient: request = core_pb2.AddHookRequest(session_id=session_id, hook=hook) return self.stub.AddHook(request) - def get_mobility_configs( - self, session_id: int - ) -> core_pb2.GetMobilityConfigsResponse: + def get_mobility_configs(self, session_id: int) -> GetMobilityConfigsResponse: """ Get all mobility configurations. @@ -704,12 +760,12 @@ class CoreGrpcClient: :return: response with a dict of node ids to mobility configurations :raises grpc.RpcError: when session doesn't exist """ - request = core_pb2.GetMobilityConfigsRequest(session_id=session_id) + request = GetMobilityConfigsRequest(session_id=session_id) return self.stub.GetMobilityConfigs(request) def get_mobility_config( self, session_id: int, node_id: int - ) -> core_pb2.GetMobilityConfigResponse: + ) -> GetMobilityConfigResponse: """ Get mobility configuration for a node. @@ -718,14 +774,12 @@ class CoreGrpcClient: :return: response with a list of configuration groups :raises grpc.RpcError: when session or node doesn't exist """ - request = core_pb2.GetMobilityConfigRequest( - session_id=session_id, node_id=node_id - ) + request = GetMobilityConfigRequest(session_id=session_id, node_id=node_id) return self.stub.GetMobilityConfig(request) def set_mobility_config( self, session_id: int, node_id: int, config: Dict[str, str] - ) -> core_pb2.SetMobilityConfigResponse: + ) -> SetMobilityConfigResponse: """ Set mobility configuration for a node. @@ -735,15 +789,15 @@ class CoreGrpcClient: :return: response with result of success or failure :raises grpc.RpcError: when session or node doesn't exist """ - mobility_config = core_pb2.MobilityConfig(node_id=node_id, config=config) - request = core_pb2.SetMobilityConfigRequest( + mobility_config = MobilityConfig(node_id=node_id, config=config) + request = SetMobilityConfigRequest( session_id=session_id, mobility_config=mobility_config ) return self.stub.SetMobilityConfig(request) def mobility_action( - self, session_id: int, node_id: int, action: core_pb2.ServiceAction - ) -> core_pb2.MobilityActionResponse: + self, session_id: int, node_id: int, action: ServiceAction + ) -> MobilityActionResponse: """ Send a mobility action for a node. @@ -753,23 +807,21 @@ class CoreGrpcClient: :return: response with result of success or failure :raises grpc.RpcError: when session or node doesn't exist """ - request = core_pb2.MobilityActionRequest( + request = MobilityActionRequest( session_id=session_id, node_id=node_id, action=action ) return self.stub.MobilityAction(request) - def get_services(self) -> core_pb2.GetServicesResponse: + def get_services(self) -> GetServicesResponse: """ Get all currently loaded services. :return: response with a list of services """ - request = core_pb2.GetServicesRequest() + request = GetServicesRequest() return self.stub.GetServices(request) - def get_service_defaults( - self, session_id: int - ) -> core_pb2.GetServiceDefaultsResponse: + def get_service_defaults(self, session_id: int) -> GetServiceDefaultsResponse: """ Get default services for different default node models. @@ -777,12 +829,12 @@ class CoreGrpcClient: :return: response with a dict of node model to a list of services :raises grpc.RpcError: when session doesn't exist """ - request = core_pb2.GetServiceDefaultsRequest(session_id=session_id) + request = GetServiceDefaultsRequest(session_id=session_id) return self.stub.GetServiceDefaults(request) def set_service_defaults( self, session_id: int, service_defaults: Dict[str, List[str]] - ) -> core_pb2.SetServiceDefaultsResponse: + ) -> SetServiceDefaultsResponse: """ Set default services for node models. @@ -794,16 +846,14 @@ class CoreGrpcClient: defaults = [] for node_type in service_defaults: services = service_defaults[node_type] - default = core_pb2.ServiceDefaults(node_type=node_type, services=services) + default = ServiceDefaults(node_type=node_type, services=services) defaults.append(default) - request = core_pb2.SetServiceDefaultsRequest( - session_id=session_id, defaults=defaults - ) + request = SetServiceDefaultsRequest(session_id=session_id, defaults=defaults) return self.stub.SetServiceDefaults(request) def get_node_service_configs( self, session_id: int - ) -> core_pb2.GetNodeServiceConfigsResponse: + ) -> GetNodeServiceConfigsResponse: """ Get service data for a node. @@ -811,12 +861,12 @@ class CoreGrpcClient: :return: response with all node service configs :raises grpc.RpcError: when session doesn't exist """ - request = core_pb2.GetNodeServiceConfigsRequest(session_id=session_id) + request = GetNodeServiceConfigsRequest(session_id=session_id) return self.stub.GetNodeServiceConfigs(request) def get_node_service( self, session_id: int, node_id: int, service: str - ) -> core_pb2.GetNodeServiceResponse: + ) -> GetNodeServiceResponse: """ Get service data for a node. @@ -826,14 +876,14 @@ class CoreGrpcClient: :return: response with node service data :raises grpc.RpcError: when session or node doesn't exist """ - request = core_pb2.GetNodeServiceRequest( + request = GetNodeServiceRequest( session_id=session_id, node_id=node_id, service=service ) return self.stub.GetNodeService(request) def get_node_service_file( self, session_id: int, node_id: int, service: str, file_name: str - ) -> core_pb2.GetNodeServiceFileResponse: + ) -> GetNodeServiceFileResponse: """ Get a service file for a node. @@ -844,7 +894,7 @@ class CoreGrpcClient: :return: response with file data :raises grpc.RpcError: when session or node doesn't exist """ - request = core_pb2.GetNodeServiceFileRequest( + request = GetNodeServiceFileRequest( session_id=session_id, node_id=node_id, service=service, file=file_name ) return self.stub.GetNodeServiceFile(request) @@ -859,7 +909,7 @@ class CoreGrpcClient: startup: List[str] = None, validate: List[str] = None, shutdown: List[str] = None, - ) -> core_pb2.SetNodeServiceResponse: + ) -> SetNodeServiceResponse: """ Set service data for a node. @@ -874,7 +924,7 @@ class CoreGrpcClient: :return: response with result of success or failure :raises grpc.RpcError: when session or node doesn't exist """ - config = core_pb2.ServiceConfig( + config = ServiceConfig( node_id=node_id, service=service, files=files, @@ -883,12 +933,12 @@ class CoreGrpcClient: validate=validate, shutdown=shutdown, ) - request = core_pb2.SetNodeServiceRequest(session_id=session_id, config=config) + request = SetNodeServiceRequest(session_id=session_id, config=config) return self.stub.SetNodeService(request) def set_node_service_file( - self, session_id: int, node_id: int, service: str, file_name: str, data: bytes - ) -> core_pb2.SetNodeServiceFileResponse: + self, session_id: int, node_id: int, service: str, file_name: str, data: str + ) -> SetNodeServiceFileResponse: """ Set a service file for a node. @@ -900,21 +950,15 @@ class CoreGrpcClient: :return: response with result of success or failure :raises grpc.RpcError: when session or node doesn't exist """ - config = core_pb2.ServiceFileConfig( + config = ServiceFileConfig( node_id=node_id, service=service, file=file_name, data=data ) - request = core_pb2.SetNodeServiceFileRequest( - session_id=session_id, config=config - ) + request = SetNodeServiceFileRequest(session_id=session_id, config=config) return self.stub.SetNodeServiceFile(request) def service_action( - self, - session_id: int, - node_id: int, - service: str, - action: core_pb2.ServiceAction, - ) -> core_pb2.ServiceActionResponse: + self, session_id: int, node_id: int, service: str, action: ServiceAction + ) -> ServiceActionResponse: """ Send an action to a service for a node. @@ -926,12 +970,12 @@ class CoreGrpcClient: :return: response with result of success or failure :raises grpc.RpcError: when session or node doesn't exist """ - request = core_pb2.ServiceActionRequest( + request = ServiceActionRequest( session_id=session_id, node_id=node_id, service=service, action=action ) return self.stub.ServiceAction(request) - def get_wlan_configs(self, session_id: int) -> core_pb2.GetWlanConfigsResponse: + def get_wlan_configs(self, session_id: int) -> GetWlanConfigsResponse: """ Get all wlan configurations. @@ -939,12 +983,10 @@ class CoreGrpcClient: :return: response with a dict of node ids to wlan configurations :raises grpc.RpcError: when session doesn't exist """ - request = core_pb2.GetWlanConfigsRequest(session_id=session_id) + request = GetWlanConfigsRequest(session_id=session_id) return self.stub.GetWlanConfigs(request) - def get_wlan_config( - self, session_id: int, node_id: int - ) -> core_pb2.GetWlanConfigResponse: + def get_wlan_config(self, session_id: int, node_id: int) -> GetWlanConfigResponse: """ Get wlan configuration for a node. @@ -953,12 +995,12 @@ class CoreGrpcClient: :return: response with a list of configuration groups :raises grpc.RpcError: when session doesn't exist """ - request = core_pb2.GetWlanConfigRequest(session_id=session_id, node_id=node_id) + request = GetWlanConfigRequest(session_id=session_id, node_id=node_id) return self.stub.GetWlanConfig(request) def set_wlan_config( self, session_id: int, node_id: int, config: Dict[str, str] - ) -> core_pb2.SetWlanConfigResponse: + ) -> SetWlanConfigResponse: """ Set wlan configuration for a node. @@ -968,13 +1010,11 @@ class CoreGrpcClient: :return: response with result of success or failure :raises grpc.RpcError: when session doesn't exist """ - wlan_config = core_pb2.WlanConfig(node_id=node_id, config=config) - request = core_pb2.SetWlanConfigRequest( - session_id=session_id, wlan_config=wlan_config - ) + wlan_config = WlanConfig(node_id=node_id, config=config) + request = SetWlanConfigRequest(session_id=session_id, wlan_config=wlan_config) return self.stub.SetWlanConfig(request) - def get_emane_config(self, session_id: int) -> core_pb2.GetEmaneConfigResponse: + def get_emane_config(self, session_id: int) -> GetEmaneConfigResponse: """ Get session emane configuration. @@ -982,12 +1022,12 @@ class CoreGrpcClient: :return: response with a list of configuration groups :raises grpc.RpcError: when session doesn't exist """ - request = core_pb2.GetEmaneConfigRequest(session_id=session_id) + request = GetEmaneConfigRequest(session_id=session_id) return self.stub.GetEmaneConfig(request) def set_emane_config( self, session_id: int, config: Dict[str, str] - ) -> core_pb2.SetEmaneConfigResponse: + ) -> SetEmaneConfigResponse: """ Set session emane configuration. @@ -996,10 +1036,10 @@ class CoreGrpcClient: :return: response with result of success or failure :raises grpc.RpcError: when session doesn't exist """ - request = core_pb2.SetEmaneConfigRequest(session_id=session_id, config=config) + request = SetEmaneConfigRequest(session_id=session_id, config=config) return self.stub.SetEmaneConfig(request) - def get_emane_models(self, session_id: int) -> core_pb2.GetEmaneModelsResponse: + def get_emane_models(self, session_id: int) -> GetEmaneModelsResponse: """ Get session emane models. @@ -1007,12 +1047,12 @@ class CoreGrpcClient: :return: response with a list of emane models :raises grpc.RpcError: when session doesn't exist """ - request = core_pb2.GetEmaneModelsRequest(session_id=session_id) + request = GetEmaneModelsRequest(session_id=session_id) return self.stub.GetEmaneModels(request) def get_emane_model_config( self, session_id: int, node_id: int, model: str, interface_id: int = -1 - ) -> core_pb2.GetEmaneModelConfigResponse: + ) -> GetEmaneModelConfigResponse: """ Get emane model configuration for a node or a node's interface. @@ -1023,7 +1063,7 @@ class CoreGrpcClient: :return: response with a list of configuration groups :raises grpc.RpcError: when session doesn't exist """ - request = core_pb2.GetEmaneModelConfigRequest( + request = GetEmaneModelConfigRequest( session_id=session_id, node_id=node_id, model=model, interface=interface_id ) return self.stub.GetEmaneModelConfig(request) @@ -1035,7 +1075,7 @@ class CoreGrpcClient: model: str, config: Dict[str, str], interface_id: int = -1, - ) -> core_pb2.SetEmaneModelConfigResponse: + ) -> SetEmaneModelConfigResponse: """ Set emane model configuration for a node or a node's interface. @@ -1047,17 +1087,15 @@ class CoreGrpcClient: :return: response with result of success or failure :raises grpc.RpcError: when session doesn't exist """ - model_config = core_pb2.EmaneModelConfig( + model_config = EmaneModelConfig( node_id=node_id, model=model, config=config, interface_id=interface_id ) - request = core_pb2.SetEmaneModelConfigRequest( + request = SetEmaneModelConfigRequest( session_id=session_id, emane_model_config=model_config ) return self.stub.SetEmaneModelConfig(request) - def get_emane_model_configs( - self, session_id: int - ) -> core_pb2.GetEmaneModelConfigsResponse: + def get_emane_model_configs(self, session_id: int) -> GetEmaneModelConfigsResponse: """ Get all emane model configurations for a session. @@ -1065,7 +1103,7 @@ class CoreGrpcClient: :return: response with a dictionary of node/interface ids to configurations :raises grpc.RpcError: when session doesn't exist """ - request = core_pb2.GetEmaneModelConfigsRequest(session_id=session_id) + request = GetEmaneModelConfigsRequest(session_id=session_id) return self.stub.GetEmaneModelConfigs(request) def save_xml(self, session_id: int, file_path: str) -> core_pb2.SaveXmlResponse: @@ -1096,7 +1134,7 @@ class CoreGrpcClient: def emane_link( self, session_id: int, nem_one: int, nem_two: int, linked: bool - ) -> core_pb2.EmaneLinkResponse: + ) -> EmaneLinkResponse: """ Helps broadcast wireless link/unlink between EMANE nodes. @@ -1106,7 +1144,7 @@ class CoreGrpcClient: :param linked: True to link, False to unlink :return: core_pb2.EmaneLinkResponse """ - request = core_pb2.EmaneLinkRequest( + request = EmaneLinkRequest( session_id=session_id, nem_one=nem_one, nem_two=nem_two, linked=linked ) return self.stub.EmaneLink(request) @@ -1191,7 +1229,8 @@ class CoreGrpcClient: @contextmanager def context_connect(self) -> Generator: """ - Makes a context manager based connection to the server, will close after context ends. + Makes a context manager based connection to the server, will close after + context ends. :return: nothing """ diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 77813b34..e23073a0 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -6,6 +6,7 @@ import netaddr from core import utils from core.api.grpc import common_pb2, core_pb2 +from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig from core.config import ConfigurableOptions from core.emulator.data import LinkData from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions @@ -364,7 +365,7 @@ def session_location(session: Session, location: core_pb2.SessionLocation) -> No session.location.refscale = location.scale -def service_configuration(session: Session, config: core_pb2.ServiceConfig) -> None: +def service_configuration(session: Session, config: ServiceConfig) -> None: """ Convenience method for setting a node service configuration. @@ -386,14 +387,14 @@ def service_configuration(session: Session, config: core_pb2.ServiceConfig) -> N service.shutdown = tuple(config.shutdown) -def get_service_configuration(service: Type[CoreService]) -> core_pb2.NodeServiceData: +def get_service_configuration(service: Type[CoreService]) -> NodeServiceData: """ Convenience for converting a service to service data proto. :param service: service to get proto data for :return: service proto data """ - return core_pb2.NodeServiceData( + return NodeServiceData( executables=service.executables, dependencies=service.dependencies, dirs=service.dirs, diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index c76bb64f..47a30dfd 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -19,6 +19,7 @@ from core.api.grpc import ( core_pb2_grpc, grpcutils, ) +from core.api.grpc.common_pb2 import MappedConfig from core.api.grpc.configservices_pb2 import ( ConfigService, GetConfigServiceDefaultsRequest, @@ -34,10 +35,24 @@ from core.api.grpc.configservices_pb2 import ( SetNodeConfigServiceRequest, SetNodeConfigServiceResponse, ) -from core.api.grpc.core_pb2 import ( - ExecuteScriptResponse, +from core.api.grpc.core_pb2 import ExecuteScriptResponse +from core.api.grpc.emane_pb2 import ( + EmaneLinkRequest, + EmaneLinkResponse, + GetEmaneConfigRequest, + GetEmaneConfigResponse, GetEmaneEventChannelRequest, GetEmaneEventChannelResponse, + GetEmaneModelConfigRequest, + GetEmaneModelConfigResponse, + GetEmaneModelConfigsRequest, + GetEmaneModelConfigsResponse, + GetEmaneModelsRequest, + GetEmaneModelsResponse, + SetEmaneConfigRequest, + SetEmaneConfigResponse, + SetEmaneModelConfigRequest, + SetEmaneModelConfigResponse, ) from core.api.grpc.events import EventStreamer from core.api.grpc.grpcutils import ( @@ -46,6 +61,48 @@ from core.api.grpc.grpcutils import ( get_links, get_net_stats, ) +from core.api.grpc.mobility_pb2 import ( + GetMobilityConfigRequest, + GetMobilityConfigResponse, + GetMobilityConfigsRequest, + GetMobilityConfigsResponse, + MobilityAction, + MobilityActionRequest, + MobilityActionResponse, + SetMobilityConfigRequest, + SetMobilityConfigResponse, +) +from core.api.grpc.services_pb2 import ( + GetNodeServiceConfigsRequest, + GetNodeServiceConfigsResponse, + GetNodeServiceFileRequest, + GetNodeServiceFileResponse, + GetNodeServiceRequest, + GetNodeServiceResponse, + GetServiceDefaultsRequest, + GetServiceDefaultsResponse, + GetServicesRequest, + GetServicesResponse, + Service, + ServiceAction, + ServiceActionRequest, + ServiceActionResponse, + ServiceDefaults, + SetNodeServiceFileRequest, + SetNodeServiceFileResponse, + SetNodeServiceRequest, + SetNodeServiceResponse, + SetServiceDefaultsRequest, + SetServiceDefaultsResponse, +) +from core.api.grpc.wlan_pb2 import ( + GetWlanConfigRequest, + GetWlanConfigResponse, + GetWlanConfigsRequest, + GetWlanConfigsResponse, + SetWlanConfigRequest, + SetWlanConfigResponse, +) from core.emane.nodes import EmaneNet from core.emulator.coreemu import CoreEmu from core.emulator.data import LinkData @@ -919,8 +976,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): return core_pb2.AddHookResponse(result=True) def GetMobilityConfigs( - self, request: core_pb2.GetMobilityConfigsRequest, context: ServicerContext - ) -> core_pb2.GetMobilityConfigsResponse: + self, request: GetMobilityConfigsRequest, context: ServicerContext + ) -> GetMobilityConfigsResponse: """ Retrieve all mobility configurations from a session @@ -931,7 +988,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get mobility configs: %s", request) session = self.get_session(request.session_id, context) - response = core_pb2.GetMobilityConfigsResponse() + response = GetMobilityConfigsResponse() for node_id in session.mobility.node_configurations: model_config = session.mobility.node_configurations[node_id] if node_id == -1: @@ -941,13 +998,13 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): continue current_config = session.mobility.get_model_config(node_id, model_name) config = get_config_options(current_config, Ns2ScriptedMobility) - mapped_config = core_pb2.MappedConfig(config=config) + mapped_config = MappedConfig(config=config) response.configs[node_id].CopyFrom(mapped_config) return response def GetMobilityConfig( - self, request: core_pb2.GetMobilityConfigRequest, context: ServicerContext - ) -> core_pb2.GetMobilityConfigResponse: + self, request: GetMobilityConfigRequest, context: ServicerContext + ) -> GetMobilityConfigResponse: """ Retrieve mobility configuration of a node @@ -962,11 +1019,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): request.node_id, Ns2ScriptedMobility.name ) config = get_config_options(current_config, Ns2ScriptedMobility) - return core_pb2.GetMobilityConfigResponse(config=config) + return GetMobilityConfigResponse(config=config) def SetMobilityConfig( - self, request: core_pb2.SetMobilityConfigRequest, context: ServicerContext - ) -> core_pb2.SetMobilityConfigResponse: + self, request: SetMobilityConfigRequest, context: ServicerContext + ) -> SetMobilityConfigResponse: """ Set mobility configuration of a node @@ -981,11 +1038,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session.mobility.set_model_config( mobility_config.node_id, Ns2ScriptedMobility.name, mobility_config.config ) - return core_pb2.SetMobilityConfigResponse(result=True) + return SetMobilityConfigResponse(result=True) def MobilityAction( - self, request: core_pb2.MobilityActionRequest, context: ServicerContext - ) -> core_pb2.MobilityActionResponse: + self, request: MobilityActionRequest, context: ServicerContext + ) -> MobilityActionResponse: """ Take mobility action whether to start, pause, stop or none of those @@ -998,19 +1055,19 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session = self.get_session(request.session_id, context) node = self.get_node(session, request.node_id, context) result = True - if request.action == core_pb2.MobilityAction.START: + if request.action == MobilityAction.START: node.mobility.start() - elif request.action == core_pb2.MobilityAction.PAUSE: + elif request.action == MobilityAction.PAUSE: node.mobility.pause() - elif request.action == core_pb2.MobilityAction.STOP: + elif request.action == MobilityAction.STOP: node.mobility.stop(move_initial=True) else: result = False - return core_pb2.MobilityActionResponse(result=result) + return MobilityActionResponse(result=result) def GetServices( - self, request: core_pb2.GetServicesRequest, context: ServicerContext - ) -> core_pb2.GetServicesResponse: + self, request: GetServicesRequest, context: ServicerContext + ) -> GetServicesResponse: """ Retrieve all the services that are running @@ -1022,13 +1079,13 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): services = [] for name in ServiceManager.services: service = ServiceManager.services[name] - service_proto = core_pb2.Service(group=service.group, name=service.name) + service_proto = Service(group=service.group, name=service.name) services.append(service_proto) - return core_pb2.GetServicesResponse(services=services) + return GetServicesResponse(services=services) def GetServiceDefaults( - self, request: core_pb2.GetServiceDefaultsRequest, context: ServicerContext - ) -> core_pb2.GetServiceDefaultsResponse: + self, request: GetServiceDefaultsRequest, context: ServicerContext + ) -> GetServiceDefaultsResponse: """ Retrieve all the default services of all node types in a session @@ -1042,15 +1099,13 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): all_service_defaults = [] for node_type in session.services.default_services: services = session.services.default_services[node_type] - service_defaults = core_pb2.ServiceDefaults( - node_type=node_type, services=services - ) + service_defaults = ServiceDefaults(node_type=node_type, services=services) all_service_defaults.append(service_defaults) - return core_pb2.GetServiceDefaultsResponse(defaults=all_service_defaults) + return GetServiceDefaultsResponse(defaults=all_service_defaults) def SetServiceDefaults( - self, request: core_pb2.SetServiceDefaultsRequest, context: ServicerContext - ) -> core_pb2.SetServiceDefaultsResponse: + self, request: SetServiceDefaultsRequest, context: ServicerContext + ) -> SetServiceDefaultsResponse: """ Set new default services to the session after whipping out the old ones :param request: set-service-defaults @@ -1065,11 +1120,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session.services.default_services[ service_defaults.node_type ] = service_defaults.services - return core_pb2.SetServiceDefaultsResponse(result=True) + return SetServiceDefaultsResponse(result=True) def GetNodeServiceConfigs( - self, request: core_pb2.GetNodeServiceConfigsRequest, context: ServicerContext - ) -> core_pb2.GetNodeServiceConfigsResponse: + self, request: GetNodeServiceConfigsRequest, context: ServicerContext + ) -> GetNodeServiceConfigsResponse: """ Retrieve all node service configurations. @@ -1085,18 +1140,18 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): for name in service_configs: service = session.services.get_service(node_id, name) service_proto = grpcutils.get_service_configuration(service) - config = core_pb2.GetNodeServiceConfigsResponse.ServiceConfig( + config = GetNodeServiceConfigsResponse.ServiceConfig( node_id=node_id, service=name, data=service_proto, files=service.config_data, ) configs.append(config) - return core_pb2.GetNodeServiceConfigsResponse(configs=configs) + return GetNodeServiceConfigsResponse(configs=configs) def GetNodeService( - self, request: core_pb2.GetNodeServiceRequest, context: ServicerContext - ) -> core_pb2.GetNodeServiceResponse: + self, request: GetNodeServiceRequest, context: ServicerContext + ) -> GetNodeServiceResponse: """ Retrieve a requested service from a node @@ -1111,11 +1166,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): request.node_id, request.service, default_service=True ) service_proto = grpcutils.get_service_configuration(service) - return core_pb2.GetNodeServiceResponse(service=service_proto) + return GetNodeServiceResponse(service=service_proto) def GetNodeServiceFile( - self, request: core_pb2.GetNodeServiceFileRequest, context: ServicerContext - ) -> core_pb2.GetNodeServiceFileResponse: + self, request: GetNodeServiceFileRequest, context: ServicerContext + ) -> GetNodeServiceFileResponse: """ Retrieve a requested service file from a node @@ -1130,11 +1185,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): file_data = session.services.get_service_file( node, request.service, request.file ) - return core_pb2.GetNodeServiceFileResponse(data=file_data.data) + return GetNodeServiceFileResponse(data=file_data.data) def SetNodeService( - self, request: core_pb2.SetNodeServiceRequest, context: ServicerContext - ) -> core_pb2.SetNodeServiceResponse: + self, request: SetNodeServiceRequest, context: ServicerContext + ) -> SetNodeServiceResponse: """ Set a node service for a node @@ -1147,11 +1202,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session = self.get_session(request.session_id, context) config = request.config grpcutils.service_configuration(session, config) - return core_pb2.SetNodeServiceResponse(result=True) + return SetNodeServiceResponse(result=True) def SetNodeServiceFile( - self, request: core_pb2.SetNodeServiceFileRequest, context: ServicerContext - ) -> core_pb2.SetNodeServiceFileResponse: + self, request: SetNodeServiceFileRequest, context: ServicerContext + ) -> SetNodeServiceFileResponse: """ Store the customized service file in the service config @@ -1166,11 +1221,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session.services.set_service_file( config.node_id, config.service, config.file, config.data ) - return core_pb2.SetNodeServiceFileResponse(result=True) + return SetNodeServiceFileResponse(result=True) def ServiceAction( - self, request: core_pb2.ServiceActionRequest, context: ServicerContext - ) -> core_pb2.ServiceActionResponse: + self, request: ServiceActionRequest, context: ServicerContext + ) -> ServiceActionResponse: """ Take action whether to start, stop, restart, validate the service or none of the above. @@ -1192,26 +1247,26 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): context.abort(grpc.StatusCode.NOT_FOUND, "service not found") status = -1 - if request.action == core_pb2.ServiceAction.START: + if request.action == ServiceAction.START: status = session.services.startup_service(node, service, wait=True) - elif request.action == core_pb2.ServiceAction.STOP: + elif request.action == ServiceAction.STOP: status = session.services.stop_service(node, service) - elif request.action == core_pb2.ServiceAction.RESTART: + elif request.action == ServiceAction.RESTART: status = session.services.stop_service(node, service) if not status: status = session.services.startup_service(node, service, wait=True) - elif request.action == core_pb2.ServiceAction.VALIDATE: + elif request.action == ServiceAction.VALIDATE: status = session.services.validate_service(node, service) result = False if not status: result = True - return core_pb2.ServiceActionResponse(result=result) + return ServiceActionResponse(result=result) def GetWlanConfigs( - self, request: core_pb2.GetWlanConfigsRequest, context: ServicerContext - ) -> core_pb2.GetWlanConfigsResponse: + self, request: GetWlanConfigsRequest, context: ServicerContext + ) -> GetWlanConfigsResponse: """ Retrieve all wireless-lan configurations. @@ -1221,7 +1276,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get wlan configs: %s", request) session = self.get_session(request.session_id, context) - response = core_pb2.GetWlanConfigsResponse() + response = GetWlanConfigsResponse() for node_id in session.mobility.node_configurations: model_config = session.mobility.node_configurations[node_id] if node_id == -1: @@ -1231,13 +1286,13 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): continue current_config = session.mobility.get_model_config(node_id, model_name) config = get_config_options(current_config, BasicRangeModel) - mapped_config = core_pb2.MappedConfig(config=config) + mapped_config = MappedConfig(config=config) response.configs[node_id].CopyFrom(mapped_config) return response def GetWlanConfig( - self, request: core_pb2.GetWlanConfigRequest, context: ServicerContext - ) -> core_pb2.GetWlanConfigResponse: + self, request: GetWlanConfigRequest, context: ServicerContext + ) -> GetWlanConfigResponse: """ Retrieve wireless-lan configuration of a node @@ -1251,11 +1306,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): request.node_id, BasicRangeModel.name ) config = get_config_options(current_config, BasicRangeModel) - return core_pb2.GetWlanConfigResponse(config=config) + return GetWlanConfigResponse(config=config) def SetWlanConfig( - self, request: core_pb2.SetWlanConfigRequest, context: ServicerContext - ) -> core_pb2.SetWlanConfigResponse: + self, request: SetWlanConfigRequest, context: ServicerContext + ) -> SetWlanConfigResponse: """ Set configuration data for a model @@ -1272,11 +1327,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): if session.state == EventTypes.RUNTIME_STATE: node = self.get_node(session, wlan_config.node_id, context) node.updatemodel(wlan_config.config) - return core_pb2.SetWlanConfigResponse(result=True) + return SetWlanConfigResponse(result=True) def GetEmaneConfig( - self, request: core_pb2.GetEmaneConfigRequest, context: ServicerContext - ) -> core_pb2.GetEmaneConfigResponse: + self, request: GetEmaneConfigRequest, context: ServicerContext + ) -> GetEmaneConfigResponse: """ Retrieve EMANE configuration of a session @@ -1288,11 +1343,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session = self.get_session(request.session_id, context) current_config = session.emane.get_configs() config = get_config_options(current_config, session.emane.emane_config) - return core_pb2.GetEmaneConfigResponse(config=config) + return GetEmaneConfigResponse(config=config) def SetEmaneConfig( - self, request: core_pb2.SetEmaneConfigRequest, context: ServicerContext - ) -> core_pb2.SetEmaneConfigResponse: + self, request: SetEmaneConfigRequest, context: ServicerContext + ) -> SetEmaneConfigResponse: """ Set EMANE configuration of a session @@ -1304,11 +1359,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session = self.get_session(request.session_id, context) config = session.emane.get_configs() config.update(request.config) - return core_pb2.SetEmaneConfigResponse(result=True) + return SetEmaneConfigResponse(result=True) def GetEmaneModels( - self, request: core_pb2.GetEmaneModelsRequest, context: ServicerContext - ) -> core_pb2.GetEmaneModelsResponse: + self, request: GetEmaneModelsRequest, context: ServicerContext + ) -> GetEmaneModelsResponse: """ Retrieve all the EMANE models in the session @@ -1323,11 +1378,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): if len(model.split("_")) != 2: continue models.append(model) - return core_pb2.GetEmaneModelsResponse(models=models) + return GetEmaneModelsResponse(models=models) def GetEmaneModelConfig( - self, request: core_pb2.GetEmaneModelConfigRequest, context: ServicerContext - ) -> core_pb2.GetEmaneModelConfigResponse: + self, request: GetEmaneModelConfigRequest, context: ServicerContext + ) -> GetEmaneModelConfigResponse: """ Retrieve EMANE model configuration of a node @@ -1342,11 +1397,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): _id = get_emane_model_id(request.node_id, request.interface) current_config = session.emane.get_model_config(_id, request.model) config = get_config_options(current_config, model) - return core_pb2.GetEmaneModelConfigResponse(config=config) + return GetEmaneModelConfigResponse(config=config) def SetEmaneModelConfig( - self, request: core_pb2.SetEmaneModelConfigRequest, context: ServicerContext - ) -> core_pb2.SetEmaneModelConfigResponse: + self, request: SetEmaneModelConfigRequest, context: ServicerContext + ) -> SetEmaneModelConfigResponse: """ Set EMANE model configuration of a node @@ -1360,11 +1415,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): model_config = request.emane_model_config _id = get_emane_model_id(model_config.node_id, model_config.interface_id) session.emane.set_model_config(_id, model_config.model, model_config.config) - return core_pb2.SetEmaneModelConfigResponse(result=True) + return SetEmaneModelConfigResponse(result=True) def GetEmaneModelConfigs( - self, request: core_pb2.GetEmaneModelConfigsRequest, context: ServicerContext - ) -> core_pb2.GetEmaneModelConfigsResponse: + self, request: GetEmaneModelConfigsRequest, context: ServicerContext + ) -> GetEmaneModelConfigsResponse: """ Retrieve all EMANE model configurations of a session @@ -1388,14 +1443,14 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): current_config = session.emane.get_model_config(_id, model_name) config = get_config_options(current_config, model) node_id, interface = grpcutils.parse_emane_model_id(_id) - model_config = core_pb2.GetEmaneModelConfigsResponse.ModelConfig( + model_config = GetEmaneModelConfigsResponse.ModelConfig( node_id=node_id, model=model_name, interface=interface, config=config, ) configs.append(model_config) - return core_pb2.GetEmaneModelConfigsResponse(configs=configs) + return GetEmaneModelConfigsResponse(configs=configs) def SaveXml( self, request: core_pb2.SaveXmlRequest, context: ServicerContext @@ -1469,8 +1524,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): return core_pb2.GetInterfacesResponse(interfaces=interfaces) def EmaneLink( - self, request: core_pb2.EmaneLinkRequest, context: ServicerContext - ) -> core_pb2.EmaneLinkResponse: + self, request: EmaneLinkRequest, context: ServicerContext + ) -> EmaneLinkResponse: """ Helps broadcast wireless link/unlink between EMANE nodes. @@ -1505,9 +1560,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): network_id=emane_one.id, ) session.broadcast_link(link) - return core_pb2.EmaneLinkResponse(result=True) + return EmaneLinkResponse(result=True) else: - return core_pb2.EmaneLinkResponse(result=False) + return EmaneLinkResponse(result=False) def GetConfigServices( self, request: GetConfigServicesRequest, context: ServicerContext diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 7c7f8378..58efa55d 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -11,6 +11,10 @@ from typing import TYPE_CHECKING, Dict, List import grpc from core.api.grpc import client, common_pb2, configservices_pb2, core_pb2 +from core.api.grpc.emane_pb2 import EmaneModelConfig +from core.api.grpc.mobility_pb2 import MobilityConfig +from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig, ServiceFileConfig +from core.api.grpc.wlan_pb2 import WlanConfig from core.gui import appconfig from core.gui.dialogs.mobilityplayer import MobilityPlayer from core.gui.dialogs.sessions import SessionsDialog @@ -617,9 +621,7 @@ class CoreClient: except grpc.RpcError as e: self.app.after(0, show_grpc_error, e, self.app, self.app) - def get_node_service( - self, node_id: int, service_name: str - ) -> core_pb2.NodeServiceData: + def get_node_service(self, node_id: int, service_name: str) -> NodeServiceData: response = self.client.get_node_service(self.session_id, node_id, service_name) logging.debug( "get node(%s) %s service, response: %s", node_id, service_name, response @@ -635,7 +637,7 @@ class CoreClient: startups: List[str], validations: List[str], shutdowns: List[str], - ) -> core_pb2.NodeServiceData: + ) -> NodeServiceData: response = self.client.set_node_service( self.session_id, node_id, @@ -675,7 +677,7 @@ class CoreClient: return response.data def set_node_service_file( - self, node_id: int, service_name: str, file_name: str, data: bytes + self, node_id: int, service_name: str, file_name: str, data: str ): response = self.client.set_node_service_file( self.session_id, node_id, service_name, file_name, data @@ -912,40 +914,40 @@ class CoreClient: self.links[edge.token] = edge logging.info("Add link between %s and %s", src_node.name, dst_node.name) - def get_wlan_configs_proto(self) -> List[core_pb2.WlanConfig]: + def get_wlan_configs_proto(self) -> List[WlanConfig]: configs = [] for node_id, config in self.wlan_configs.items(): config = {x: config[x].value for x in config} - wlan_config = core_pb2.WlanConfig(node_id=node_id, config=config) + wlan_config = WlanConfig(node_id=node_id, config=config) configs.append(wlan_config) return configs - def get_mobility_configs_proto(self) -> List[core_pb2.MobilityConfig]: + def get_mobility_configs_proto(self) -> List[MobilityConfig]: configs = [] for node_id, config in self.mobility_configs.items(): config = {x: config[x].value for x in config} - mobility_config = core_pb2.MobilityConfig(node_id=node_id, config=config) + mobility_config = MobilityConfig(node_id=node_id, config=config) configs.append(mobility_config) return configs - def get_emane_model_configs_proto(self) -> List[core_pb2.EmaneModelConfig]: + def get_emane_model_configs_proto(self) -> List[EmaneModelConfig]: configs = [] for key, config in self.emane_model_configs.items(): node_id, model, interface = key config = {x: config[x].value for x in config} if interface is None: interface = -1 - config_proto = core_pb2.EmaneModelConfig( + config_proto = EmaneModelConfig( node_id=node_id, interface_id=interface, model=model, config=config ) configs.append(config_proto) return configs - def get_service_configs_proto(self) -> List[core_pb2.ServiceConfig]: + def get_service_configs_proto(self) -> List[ServiceConfig]: configs = [] for node_id, services in self.service_configs.items(): for name, config in services.items(): - config_proto = core_pb2.ServiceConfig( + config_proto = ServiceConfig( node_id=node_id, service=name, directories=config.dirs, @@ -957,12 +959,12 @@ class CoreClient: configs.append(config_proto) return configs - def get_service_file_configs_proto(self) -> List[core_pb2.ServiceFileConfig]: + def get_service_file_configs_proto(self) -> List[ServiceFileConfig]: configs = [] for (node_id, file_configs) in self.file_configs.items(): for service, file_config in file_configs.items(): for file, data in file_config.items(): - config_proto = core_pb2.ServiceFileConfig( + config_proto = ServiceFileConfig( node_id=node_id, service=service, file=file, data=data ) configs.append(config_proto) diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py index 3aaac1a4..36c4ccfe 100644 --- a/daemon/core/gui/dialogs/configserviceconfig.py +++ b/daemon/core/gui/dialogs/configserviceconfig.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any, List import grpc -from core.api.grpc import core_pb2 +from core.api.grpc.services_pb2 import ServiceValidationMode from core.gui.dialogs.dialog import Dialog from core.gui.errors import show_grpc_error from core.gui.themes import FRAME_PAD, PADX, PADY @@ -256,9 +256,9 @@ class ConfigServiceConfigDialog(Dialog): label = ttk.Label(frame, text="Validation Mode") label.grid(row=1, column=0, sticky="w", padx=PADX) - if self.validation_mode == core_pb2.ServiceValidationMode.BLOCKING: + if self.validation_mode == ServiceValidationMode.BLOCKING: mode = "BLOCKING" - elif self.validation_mode == core_pb2.ServiceValidationMode.NON_BLOCKING: + elif self.validation_mode == ServiceValidationMode.NON_BLOCKING: mode = "NON_BLOCKING" else: mode = "TIMER" diff --git a/daemon/core/gui/dialogs/mobilityplayer.py b/daemon/core/gui/dialogs/mobilityplayer.py index bb4d203c..b7cbb400 100644 --- a/daemon/core/gui/dialogs/mobilityplayer.py +++ b/daemon/core/gui/dialogs/mobilityplayer.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any import grpc -from core.api.grpc.core_pb2 import MobilityAction +from core.api.grpc.mobility_pb2 import MobilityAction from core.gui.dialogs.dialog import Dialog from core.gui.errors import show_grpc_error from core.gui.images import ImageEnum, Images diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index e610cf94..94b71787 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any, List import grpc -from core.api.grpc import core_pb2 +from core.api.grpc.services_pb2 import ServiceValidationMode from core.gui.dialogs.copyserviceconfig import CopyServiceConfigDialog from core.gui.dialogs.dialog import Dialog from core.gui.errors import show_grpc_error @@ -331,9 +331,9 @@ class ServiceConfigDialog(Dialog): label = ttk.Label(frame, text="Validation Mode") label.grid(row=1, column=0, sticky="w", padx=PADX) - if self.validation_mode == core_pb2.ServiceValidationMode.BLOCKING: + if self.validation_mode == ServiceValidationMode.BLOCKING: mode = "BLOCKING" - elif self.validation_mode == core_pb2.ServiceValidationMode.NON_BLOCKING: + elif self.validation_mode == ServiceValidationMode.NON_BLOCKING: mode = "NON_BLOCKING" else: mode = "TIMER" diff --git a/daemon/proto/core/api/grpc/common.proto b/daemon/proto/core/api/grpc/common.proto index 590e2262..065bee7a 100644 --- a/daemon/proto/core/api/grpc/common.proto +++ b/daemon/proto/core/api/grpc/common.proto @@ -10,3 +10,7 @@ message ConfigOption { repeated string select = 5; string group = 6; } + +message MappedConfig { + map config = 1; +} diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 0a751ddf..f457222d 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -2,11 +2,12 @@ syntax = "proto3"; package core; -option java_package = "com.core.client.grpc"; -option java_outer_classname = "CoreProto"; - import "core/api/grpc/configservices.proto"; import "core/api/grpc/common.proto"; +import "core/api/grpc/emane.proto"; +import "core/api/grpc/mobility.proto"; +import "core/api/grpc/services.proto"; +import "core/api/grpc/wlan.proto"; service CoreApi { // session rpc @@ -78,33 +79,33 @@ service CoreApi { } // mobility rpc - rpc GetMobilityConfigs (GetMobilityConfigsRequest) returns (GetMobilityConfigsResponse) { + rpc GetMobilityConfigs (mobility.GetMobilityConfigsRequest) returns (mobility.GetMobilityConfigsResponse) { } - rpc GetMobilityConfig (GetMobilityConfigRequest) returns (GetMobilityConfigResponse) { + rpc GetMobilityConfig (mobility.GetMobilityConfigRequest) returns (mobility.GetMobilityConfigResponse) { } - rpc SetMobilityConfig (SetMobilityConfigRequest) returns (SetMobilityConfigResponse) { + rpc SetMobilityConfig (mobility.SetMobilityConfigRequest) returns (mobility.SetMobilityConfigResponse) { } - rpc MobilityAction (MobilityActionRequest) returns (MobilityActionResponse) { + rpc MobilityAction (mobility.MobilityActionRequest) returns (mobility.MobilityActionResponse) { } // service rpc - rpc GetServices (GetServicesRequest) returns (GetServicesResponse) { + rpc GetServices (services.GetServicesRequest) returns (services.GetServicesResponse) { } - rpc GetServiceDefaults (GetServiceDefaultsRequest) returns (GetServiceDefaultsResponse) { + rpc GetServiceDefaults (services.GetServiceDefaultsRequest) returns (services.GetServiceDefaultsResponse) { } - rpc SetServiceDefaults (SetServiceDefaultsRequest) returns (SetServiceDefaultsResponse) { + rpc SetServiceDefaults (services.SetServiceDefaultsRequest) returns (services.SetServiceDefaultsResponse) { } - rpc GetNodeServiceConfigs (GetNodeServiceConfigsRequest) returns (GetNodeServiceConfigsResponse) { + rpc GetNodeServiceConfigs (services.GetNodeServiceConfigsRequest) returns (services.GetNodeServiceConfigsResponse) { } - rpc GetNodeService (GetNodeServiceRequest) returns (GetNodeServiceResponse) { + rpc GetNodeService (services.GetNodeServiceRequest) returns (services.GetNodeServiceResponse) { } - rpc GetNodeServiceFile (GetNodeServiceFileRequest) returns (GetNodeServiceFileResponse) { + rpc GetNodeServiceFile (services.GetNodeServiceFileRequest) returns (services.GetNodeServiceFileResponse) { } - rpc SetNodeService (SetNodeServiceRequest) returns (SetNodeServiceResponse) { + rpc SetNodeService (services.SetNodeServiceRequest) returns (services.SetNodeServiceResponse) { } - rpc SetNodeServiceFile (SetNodeServiceFileRequest) returns (SetNodeServiceFileResponse) { + rpc SetNodeServiceFile (services.SetNodeServiceFileRequest) returns (services.SetNodeServiceFileResponse) { } - rpc ServiceAction (ServiceActionRequest) returns (ServiceActionResponse) { + rpc ServiceAction (services.ServiceActionRequest) returns (services.ServiceActionResponse) { } // config services @@ -122,27 +123,27 @@ service CoreApi { } // wlan rpc - rpc GetWlanConfigs (GetWlanConfigsRequest) returns (GetWlanConfigsResponse) { + rpc GetWlanConfigs (wlan.GetWlanConfigsRequest) returns (wlan.GetWlanConfigsResponse) { } - rpc GetWlanConfig (GetWlanConfigRequest) returns (GetWlanConfigResponse) { + rpc GetWlanConfig (wlan.GetWlanConfigRequest) returns (wlan.GetWlanConfigResponse) { } - rpc SetWlanConfig (SetWlanConfigRequest) returns (SetWlanConfigResponse) { + rpc SetWlanConfig (wlan.SetWlanConfigRequest) returns (wlan.SetWlanConfigResponse) { } // emane rpc - rpc GetEmaneConfig (GetEmaneConfigRequest) returns (GetEmaneConfigResponse) { + rpc GetEmaneConfig (emane.GetEmaneConfigRequest) returns (emane.GetEmaneConfigResponse) { } - rpc SetEmaneConfig (SetEmaneConfigRequest) returns (SetEmaneConfigResponse) { + rpc SetEmaneConfig (emane.SetEmaneConfigRequest) returns (emane.SetEmaneConfigResponse) { } - rpc GetEmaneModels (GetEmaneModelsRequest) returns (GetEmaneModelsResponse) { + rpc GetEmaneModels (emane.GetEmaneModelsRequest) returns (emane.GetEmaneModelsResponse) { } - rpc GetEmaneModelConfig (GetEmaneModelConfigRequest) returns (GetEmaneModelConfigResponse) { + rpc GetEmaneModelConfig (emane.GetEmaneModelConfigRequest) returns (emane.GetEmaneModelConfigResponse) { } - rpc SetEmaneModelConfig (SetEmaneModelConfigRequest) returns (SetEmaneModelConfigResponse) { + rpc SetEmaneModelConfig (emane.SetEmaneModelConfigRequest) returns (emane.SetEmaneModelConfigResponse) { } - rpc GetEmaneModelConfigs (GetEmaneModelConfigsRequest) returns (GetEmaneModelConfigsResponse) { + rpc GetEmaneModelConfigs (emane.GetEmaneModelConfigsRequest) returns (emane.GetEmaneModelConfigsResponse) { } - rpc GetEmaneEventChannel (GetEmaneEventChannelRequest) returns (GetEmaneEventChannelResponse) { + rpc GetEmaneEventChannel (emane.GetEmaneEventChannelRequest) returns (emane.GetEmaneEventChannelResponse) { } // xml rpc @@ -154,7 +155,7 @@ service CoreApi { // utilities rpc GetInterfaces (GetInterfacesRequest) returns (GetInterfacesResponse) { } - rpc EmaneLink (EmaneLinkRequest) returns (EmaneLinkResponse) { + rpc EmaneLink (emane.EmaneLinkRequest) returns (emane.EmaneLinkResponse) { } rpc ExecuteScript (ExecuteScriptRequest) returns (ExecuteScriptResponse) { } @@ -168,11 +169,11 @@ message StartSessionRequest { repeated Hook hooks = 4; SessionLocation location = 5; map emane_config = 6; - repeated WlanConfig wlan_configs = 7; - repeated EmaneModelConfig emane_model_configs = 8; - repeated MobilityConfig mobility_configs = 9; - repeated ServiceConfig service_configs = 10; - repeated ServiceFileConfig service_file_configs = 11; + repeated wlan.WlanConfig wlan_configs = 7; + repeated emane.EmaneModelConfig emane_model_configs = 8; + repeated mobility.MobilityConfig mobility_configs = 9; + repeated services.ServiceConfig service_configs = 10; + repeated services.ServiceFileConfig service_file_configs = 11; repeated Link asymmetric_links = 12; repeated configservices.ConfigServiceConfig config_service_configs = 13; } @@ -515,226 +516,6 @@ message AddHookResponse { bool result = 1; } -message GetMobilityConfigsRequest { - int32 session_id = 1; -} - -message GetMobilityConfigsResponse { - map configs = 1; -} - -message GetMobilityConfigRequest { - int32 session_id = 1; - int32 node_id = 2; -} - -message GetMobilityConfigResponse { - map config = 1; -} - -message SetMobilityConfigRequest { - int32 session_id = 1; - MobilityConfig mobility_config = 2; -} - -message SetMobilityConfigResponse { - bool result = 1; -} - -message MobilityActionRequest { - int32 session_id = 1; - int32 node_id = 2; - MobilityAction.Enum action = 3; -} - -message MobilityActionResponse { - bool result = 1; -} - -message GetServicesRequest { - -} - -message GetServicesResponse { - repeated Service services = 1; -} - -message GetServiceDefaultsRequest { - int32 session_id = 1; -} - -message GetServiceDefaultsResponse { - repeated ServiceDefaults defaults = 1; -} - -message SetServiceDefaultsRequest { - int32 session_id = 1; - repeated ServiceDefaults defaults = 2; -} - -message SetServiceDefaultsResponse { - bool result = 1; -} - -message GetNodeServiceConfigsRequest { - int32 session_id = 1; -} - -message GetNodeServiceConfigsResponse { - message ServiceConfig { - int32 node_id = 1; - string service = 2; - NodeServiceData data = 3; - map files = 4; - } - repeated ServiceConfig configs = 1; -} - -message GetNodeServiceRequest { - int32 session_id = 1; - int32 node_id = 2; - string service = 3; -} - -message GetNodeServiceResponse { - NodeServiceData service = 1; -} - -message GetNodeServiceFileRequest { - int32 session_id = 1; - int32 node_id = 2; - string service = 3; - string file = 4; -} - -message GetNodeServiceFileResponse { - string data = 1; -} - -message SetNodeServiceRequest { - int32 session_id = 1; - ServiceConfig config = 2; -} - -message SetNodeServiceResponse { - bool result = 1; -} - -message SetNodeServiceFileRequest { - int32 session_id = 1; - ServiceFileConfig config = 2; -} - -message SetNodeServiceFileResponse { - bool result = 1; -} - -message ServiceActionRequest { - int32 session_id = 1; - int32 node_id = 2; - string service = 3; - ServiceAction.Enum action = 4; -} - -message ServiceActionResponse { - bool result = 1; -} - -message GetWlanConfigsRequest { - int32 session_id = 1; -} - -message GetWlanConfigsResponse { - map configs = 1; -} - -message GetWlanConfigRequest { - int32 session_id = 1; - int32 node_id = 2; -} - -message GetWlanConfigResponse { - map config = 1; -} - -message SetWlanConfigRequest { - int32 session_id = 1; - WlanConfig wlan_config = 2; -} - -message SetWlanConfigResponse { - bool result = 1; -} - -message GetEmaneConfigRequest { - int32 session_id = 1; -} - -message GetEmaneConfigResponse { - map config = 1; -} - -message SetEmaneConfigRequest { - int32 session_id = 1; - map config = 2; -} - -message SetEmaneConfigResponse { - bool result = 1; -} - -message GetEmaneModelsRequest { - int32 session_id = 1; -} - -message GetEmaneModelsResponse { - repeated string models = 1; -} - -message GetEmaneModelConfigRequest { - int32 session_id = 1; - int32 node_id = 2; - int32 interface = 3; - string model = 4; -} - -message GetEmaneModelConfigResponse { - map config = 1; -} - -message SetEmaneModelConfigRequest { - int32 session_id = 1; - EmaneModelConfig emane_model_config = 2; -} - -message SetEmaneModelConfigResponse { - bool result = 1; -} - -message GetEmaneModelConfigsRequest { - int32 session_id = 1; -} - -message GetEmaneModelConfigsResponse { - message ModelConfig { - int32 node_id = 1; - string model = 2; - int32 interface = 3; - map config = 4; - } - repeated ModelConfig configs = 1; -} - -message GetEmaneEventChannelRequest { - int32 session_id = 1; -} - -message GetEmaneEventChannelResponse { - string group = 1; - int32 port = 2; - string device = 3; -} - message SaveXmlRequest { int32 session_id = 1; } @@ -761,17 +542,6 @@ message GetInterfacesResponse { repeated string interfaces = 1; } -message EmaneLinkRequest { - int32 session_id = 1; - int32 nem_one = 2; - int32 nem_two = 3; - bool linked = 4; -} - -message EmaneLinkResponse { - bool result = 1; -} - message ExecuteScriptRequest { string script = 1; } @@ -781,40 +551,6 @@ message ExecuteScriptResponse { } // data structures for messages below -message WlanConfig { - int32 node_id = 1; - map config = 2; -} - -message MobilityConfig { - int32 node_id = 1; - map config = 2; -} - -message EmaneModelConfig { - int32 node_id = 1; - int32 interface_id = 2; - string model = 3; - map config = 4; -} - -message ServiceConfig { - int32 node_id = 1; - string service = 2; - repeated string startup = 3; - repeated string validate = 4; - repeated string shutdown = 5; - repeated string files = 6; - repeated string directories = 7; -} - -message ServiceFileConfig { - int32 node_id = 1; - string service = 2; - string file = 3; - string data = 4; -} - message EventType { enum Enum { SESSION = 0; @@ -893,31 +629,6 @@ message ConfigOptionType { } } -message ServiceValidationMode { - enum Enum { - BLOCKING = 0; - NON_BLOCKING = 1; - TIMER = 2; - } -} - -message ServiceAction { - enum Enum { - START = 0; - STOP = 1; - RESTART = 2; - VALIDATE = 3; - } -} - -message MobilityAction { - enum Enum { - START = 0; - PAUSE = 1; - STOP = 2; - } -} - message ExceptionLevel { enum Enum { DEFAULT = 0; @@ -934,33 +645,6 @@ message Hook { string data = 3; } -message ServiceDefaults { - string node_type = 1; - repeated string services = 2; -} - -message Service { - string group = 1; - string name = 2; -} - -message NodeServiceData { - repeated string executables = 1; - repeated string dependencies = 2; - repeated string dirs = 3; - repeated string configs = 4; - repeated string startup = 5; - repeated string validate = 6; - ServiceValidationMode.Enum validation_mode = 7; - int32 validation_timer = 8; - repeated string shutdown = 9; - string meta = 10; -} - -message MappedConfig { - map config = 1; -} - message Session { int32 id = 1; SessionState.Enum state = 2; diff --git a/daemon/proto/core/api/grpc/emane.proto b/daemon/proto/core/api/grpc/emane.proto new file mode 100644 index 00000000..33cb1a2a --- /dev/null +++ b/daemon/proto/core/api/grpc/emane.proto @@ -0,0 +1,92 @@ +syntax = "proto3"; + +package emane; + +import "core/api/grpc/common.proto"; + +message GetEmaneConfigRequest { + int32 session_id = 1; +} + +message GetEmaneConfigResponse { + map config = 1; +} + +message SetEmaneConfigRequest { + int32 session_id = 1; + map config = 2; +} + +message SetEmaneConfigResponse { + bool result = 1; +} + +message GetEmaneModelsRequest { + int32 session_id = 1; +} + +message GetEmaneModelsResponse { + repeated string models = 1; +} + +message GetEmaneModelConfigRequest { + int32 session_id = 1; + int32 node_id = 2; + int32 interface = 3; + string model = 4; +} + +message GetEmaneModelConfigResponse { + map config = 1; +} + +message SetEmaneModelConfigRequest { + int32 session_id = 1; + EmaneModelConfig emane_model_config = 2; +} + +message SetEmaneModelConfigResponse { + bool result = 1; +} + +message GetEmaneModelConfigsRequest { + int32 session_id = 1; +} + +message GetEmaneModelConfigsResponse { + message ModelConfig { + int32 node_id = 1; + string model = 2; + int32 interface = 3; + map config = 4; + } + repeated ModelConfig configs = 1; +} + +message GetEmaneEventChannelRequest { + int32 session_id = 1; +} + +message GetEmaneEventChannelResponse { + string group = 1; + int32 port = 2; + string device = 3; +} + +message EmaneLinkRequest { + int32 session_id = 1; + int32 nem_one = 2; + int32 nem_two = 3; + bool linked = 4; +} + +message EmaneLinkResponse { + bool result = 1; +} + +message EmaneModelConfig { + int32 node_id = 1; + int32 interface_id = 2; + string model = 3; + map config = 4; +} diff --git a/daemon/proto/core/api/grpc/mobility.proto b/daemon/proto/core/api/grpc/mobility.proto new file mode 100644 index 00000000..abfad8ef --- /dev/null +++ b/daemon/proto/core/api/grpc/mobility.proto @@ -0,0 +1,54 @@ +syntax = "proto3"; + +package mobility; + +import "core/api/grpc/common.proto"; + +message MobilityAction { + enum Enum { + START = 0; + PAUSE = 1; + STOP = 2; + } +} + +message MobilityConfig { + int32 node_id = 1; + map config = 2; +} + +message GetMobilityConfigsRequest { + int32 session_id = 1; +} + +message GetMobilityConfigsResponse { + map configs = 1; +} + +message GetMobilityConfigRequest { + int32 session_id = 1; + int32 node_id = 2; +} + +message GetMobilityConfigResponse { + map config = 1; +} + +message SetMobilityConfigRequest { + int32 session_id = 1; + MobilityConfig mobility_config = 2; +} + +message SetMobilityConfigResponse { + bool result = 1; +} + +message MobilityActionRequest { + int32 session_id = 1; + int32 node_id = 2; + MobilityAction.Enum action = 3; +} + +message MobilityActionResponse { + bool result = 1; +} diff --git a/daemon/proto/core/api/grpc/services.proto b/daemon/proto/core/api/grpc/services.proto new file mode 100644 index 00000000..7e8498a7 --- /dev/null +++ b/daemon/proto/core/api/grpc/services.proto @@ -0,0 +1,149 @@ +syntax = "proto3"; + +package services; + +message ServiceConfig { + int32 node_id = 1; + string service = 2; + repeated string startup = 3; + repeated string validate = 4; + repeated string shutdown = 5; + repeated string files = 6; + repeated string directories = 7; +} + +message ServiceFileConfig { + int32 node_id = 1; + string service = 2; + string file = 3; + string data = 4; +} + +message ServiceValidationMode { + enum Enum { + BLOCKING = 0; + NON_BLOCKING = 1; + TIMER = 2; + } +} + +message ServiceAction { + enum Enum { + START = 0; + STOP = 1; + RESTART = 2; + VALIDATE = 3; + } +} + +message ServiceDefaults { + string node_type = 1; + repeated string services = 2; +} + +message Service { + string group = 1; + string name = 2; +} + +message NodeServiceData { + repeated string executables = 1; + repeated string dependencies = 2; + repeated string dirs = 3; + repeated string configs = 4; + repeated string startup = 5; + repeated string validate = 6; + ServiceValidationMode.Enum validation_mode = 7; + int32 validation_timer = 8; + repeated string shutdown = 9; + string meta = 10; +} + +message GetServicesRequest { + +} + +message GetServicesResponse { + repeated Service services = 1; +} + +message GetServiceDefaultsRequest { + int32 session_id = 1; +} + +message GetServiceDefaultsResponse { + repeated ServiceDefaults defaults = 1; +} + +message SetServiceDefaultsRequest { + int32 session_id = 1; + repeated ServiceDefaults defaults = 2; +} + +message SetServiceDefaultsResponse { + bool result = 1; +} + +message GetNodeServiceConfigsRequest { + int32 session_id = 1; +} + +message GetNodeServiceConfigsResponse { + message ServiceConfig { + int32 node_id = 1; + string service = 2; + NodeServiceData data = 3; + map files = 4; + } + repeated ServiceConfig configs = 1; +} + +message GetNodeServiceRequest { + int32 session_id = 1; + int32 node_id = 2; + string service = 3; +} + +message GetNodeServiceResponse { + NodeServiceData service = 1; +} + +message GetNodeServiceFileRequest { + int32 session_id = 1; + int32 node_id = 2; + string service = 3; + string file = 4; +} + +message GetNodeServiceFileResponse { + string data = 1; +} + +message SetNodeServiceRequest { + int32 session_id = 1; + ServiceConfig config = 2; +} + +message SetNodeServiceResponse { + bool result = 1; +} + +message SetNodeServiceFileRequest { + int32 session_id = 1; + ServiceFileConfig config = 2; +} + +message SetNodeServiceFileResponse { + bool result = 1; +} + +message ServiceActionRequest { + int32 session_id = 1; + int32 node_id = 2; + string service = 3; + ServiceAction.Enum action = 4; +} + +message ServiceActionResponse { + bool result = 1; +} diff --git a/daemon/proto/core/api/grpc/wlan.proto b/daemon/proto/core/api/grpc/wlan.proto new file mode 100644 index 00000000..139c0a2e --- /dev/null +++ b/daemon/proto/core/api/grpc/wlan.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; + +package wlan; + +import "core/api/grpc/common.proto"; + +message WlanConfig { + int32 node_id = 1; + map config = 2; +} + +message GetWlanConfigsRequest { + int32 session_id = 1; +} + +message GetWlanConfigsResponse { + map configs = 1; +} + +message GetWlanConfigRequest { + int32 session_id = 1; + int32 node_id = 2; +} + +message GetWlanConfigResponse { + map config = 1; +} + +message SetWlanConfigRequest { + int32 session_id = 1; + WlanConfig wlan_config = 2; +} + +message SetWlanConfigResponse { + bool result = 1; +} diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index fadcd8e2..b3bd9a27 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -7,6 +7,10 @@ from mock import patch from core.api.grpc import core_pb2 from core.api.grpc.client import CoreGrpcClient, InterfaceHelper +from core.api.grpc.emane_pb2 import EmaneModelConfig +from core.api.grpc.mobility_pb2 import MobilityAction, MobilityConfig +from core.api.grpc.services_pb2 import ServiceAction, ServiceConfig, ServiceFileConfig +from core.api.grpc.wlan_pb2 import WlanConfig from core.api.tlv.dataconversion import ConfigShim from core.api.tlv.enumerations import ConfigFlags from core.emane.ieee80211abg import EmaneIeee80211abgModel @@ -69,7 +73,7 @@ class TestGrpc: model_node_id = 20 model_config_key = "bandwidth" model_config_value = "500000" - model_config = core_pb2.EmaneModelConfig( + model_config = EmaneModelConfig( node_id=model_node_id, interface_id=-1, model=EmaneIeee80211abgModel.name, @@ -78,21 +82,21 @@ class TestGrpc: model_configs = [model_config] wlan_config_key = "range" wlan_config_value = "333" - wlan_config = core_pb2.WlanConfig( + wlan_config = WlanConfig( node_id=wlan_node.id, config={wlan_config_key: wlan_config_value} ) wlan_configs = [wlan_config] mobility_config_key = "refresh_ms" mobility_config_value = "60" - mobility_config = core_pb2.MobilityConfig( + mobility_config = MobilityConfig( node_id=wlan_node.id, config={mobility_config_key: mobility_config_value} ) mobility_configs = [mobility_config] - service_config = core_pb2.ServiceConfig( + service_config = ServiceConfig( node_id=node_one.id, service="DefaultRoute", validate=["echo hello"] ) service_configs = [service_config] - service_file_config = core_pb2.ServiceFileConfig( + service_file_config = ServiceFileConfig( node_id=node_one.id, service="DefaultRoute", file="defaultroute.sh", @@ -829,9 +833,7 @@ class TestGrpc: # then with client.context_connect(): - response = client.mobility_action( - session.id, wlan.id, core_pb2.MobilityAction.STOP - ) + response = client.mobility_action(session.id, wlan.id, MobilityAction.STOP) # then assert response.result is True @@ -971,7 +973,7 @@ class TestGrpc: # then with client.context_connect(): response = client.service_action( - session.id, node.id, service_name, core_pb2.ServiceAction.STOP + session.id, node.id, service_name, ServiceAction.STOP ) # then From 66e5be757602723346865b9b7d6914c8175fceb1 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 26 Mar 2020 11:21:47 -0700 Subject: [PATCH 0100/1131] updates to basic range model configuration settings to allow 0 values as well as empty values for None, that work in old and new guis --- daemon/core/location/mobility.py | 47 ++++++++++++++++---------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 06a78307..b5a76507 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -302,38 +302,30 @@ class BasicRangeModel(WirelessModel): self.wlan = session.get_node(_id) self._netifs = {} self._netifslock = threading.Lock() - self.range = 0 self.bw = None self.delay = None self.loss = None self.jitter = None - def values_from_config(self, config: Dict[str, str]) -> None: + def _get_config(self, current_value: int, config: Dict[str, str], name: str) -> int: """ - Values to convert to link parameters. + Convenience for updating value to use from a provided configuration. - :param config: values to convert - :return: nothing + :param current_value: current config value to use when one is not provided + :param config: config to get values from + :param name: name of config value to get + :return: current config value when not provided, new value otherwise """ - self.range = int(float(config["range"])) - logging.debug( - "basic range model configured for WLAN %d using range %d", - self.wlan.id, - self.range, - ) - self.bw = int(config["bandwidth"]) - if self.bw == 0: - self.bw = None - self.delay = int(config["delay"]) - if self.delay == 0: - self.delay = None - self.loss = int(float(config["error"])) - if self.loss == 0: - self.loss = None - self.jitter = int(config["jitter"]) - if self.jitter == 0: - self.jitter = None + value = config.get(name) + if value is not None: + if value == "": + value = None + else: + value = int(float(value)) + else: + value = current_value + return value def setlinkparams(self) -> None: """ @@ -472,7 +464,14 @@ class BasicRangeModel(WirelessModel): :param config: values to update configuration :return: nothing """ - self.values_from_config(config) + self.range = self._get_config(self.range, config, "range") + if self.range is None: + self.range = 0 + logging.debug("wlan %s set range to %s", self.wlan.name, self.range) + self.bw = self._get_config(self.bw, config, "bandwidth") + 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") self.setlinkparams() def create_link_data( From fc40c8d7bbd222bc84c4d5e40203f4948375e169 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 26 Mar 2020 22:24:23 -0700 Subject: [PATCH 0101/1131] enabled node context delete/copy and edit menu delete in python gui --- daemon/core/gui/graph/graph.py | 16 ++++++++-------- daemon/core/gui/graph/node.py | 26 +++++++++++++++++--------- daemon/core/gui/menuaction.py | 17 ++++++++++------- daemon/core/gui/menubar.py | 4 ++++ 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index a78386a0..b602ae10 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -1,6 +1,6 @@ import logging import tkinter as tk -from typing import TYPE_CHECKING, List, Tuple +from typing import TYPE_CHECKING, Tuple from PIL import Image, ImageTk @@ -440,7 +440,7 @@ class CanvasGraph(tk.Canvas): if select_id is not None: self.move(select_id, x_offset, y_offset) - def delete_selection_objects(self) -> List[CanvasNode]: + def delete_selected_objects(self) -> None: edges = set() nodes = [] for object_id in self.selection: @@ -484,7 +484,7 @@ class CanvasGraph(tk.Canvas): shape.delete() self.selection.clear() - return nodes + self.core.delete_graph_nodes(nodes) def zoom(self, event: tk.Event, factor: float = None): if not factor: @@ -640,16 +640,17 @@ class CanvasGraph(tk.Canvas): self.select_box.shape_motion(x, y) def click_context(self, event: tk.Event): + logging.info("context: %s", self.context) if not self.context: selected = self.get_selected(event) canvas_node = self.nodes.get(selected) if canvas_node: logging.debug("node context: %s", selected) self.context = canvas_node.create_context() - self.context.bind("", self.hide_context) + self.context.bind("", self.hide_context) self.context.post(event.x_root, event.y_root) - # else: - # self.hide_context() + else: + self.hide_context() def press_delete(self, event: tk.Event): """ @@ -657,8 +658,7 @@ class CanvasGraph(tk.Canvas): """ logging.debug("press delete key") if not self.app.core.is_runtime(): - nodes = self.delete_selection_objects() - self.core.delete_graph_nodes(nodes) + self.delete_selected_objects() else: logging.info("node deletion is disabled during runtime state") diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 7ae64eac..39463054 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -233,17 +233,25 @@ class CanvasNode: label="Link To Selected", command=self.wireless_link_selected ) context.add_command(label="Select Members", state=tk.DISABLED) - context.add_command(label="Select Adjacent", state=tk.DISABLED) - context.add_command(label="Create Link To", state=tk.DISABLED) - context.add_command(label="Assign To", state=tk.DISABLED) - context.add_command(label="Move To", state=tk.DISABLED) - context.add_command(label="Cut", state=tk.DISABLED) - context.add_command(label="Copy", state=tk.DISABLED) - context.add_command(label="Paste", state=tk.DISABLED) - context.add_command(label="Delete", state=tk.DISABLED) - context.add_command(label="Hide", state=tk.DISABLED) + edit_menu = tk.Menu(context) + themes.style_menu(edit_menu) + edit_menu.add_command(label="Cut", state=tk.DISABLED) + edit_menu.add_command(label="Copy", command=self.canvas_copy) + edit_menu.add_command(label="Delete", command=self.canvas_delete) + edit_menu.add_command(label="Hide", state=tk.DISABLED) + context.add_cascade(label="Edit", menu=edit_menu) return context + def canvas_delete(self) -> None: + self.canvas.clear_selection() + self.canvas.selection[self.id] = self + self.canvas.delete_selected_objects() + + def canvas_copy(self) -> None: + self.canvas.clear_selection() + self.canvas.selection[self.id] = self + self.canvas.copy() + def show_config(self): self.canvas.context = None dialog = NodeConfigDialog(self.app, self.app, self) diff --git a/daemon/core/gui/menuaction.py b/daemon/core/gui/menuaction.py index 3d7ee154..ac191a69 100644 --- a/daemon/core/gui/menuaction.py +++ b/daemon/core/gui/menuaction.py @@ -152,31 +152,34 @@ class MenuAction: dialog = ServersDialog(self.app, self.app) dialog.show() - def edit_observer_widgets(self): + def edit_observer_widgets(self) -> None: dialog = ObserverDialog(self.app, self.app) dialog.show() - def show_about(self): + def show_about(self) -> None: dialog = AboutDialog(self.app, self.app) dialog.show() - def throughput(self): + def throughput(self) -> None: if not self.app.core.handling_throughputs: self.app.core.enable_throughputs() else: self.app.core.cancel_throughputs() - def copy(self, event: tk.Event = None): + def copy(self, event: tk.Event = None) -> None: self.app.canvas.copy() - def paste(self, event: tk.Event = None): + def paste(self, event: tk.Event = None) -> None: self.app.canvas.paste() - def config_throughput(self): + def delete(self, event: tk.Event = None) -> None: + self.app.canvas.delete_selected_objects() + + def config_throughput(self) -> None: dialog = ThroughputDialog(self.app, self.app) dialog.show() - def add_recent_file_to_gui_config(self, file_path): + def add_recent_file_to_gui_config(self, file_path) -> None: recent_files = self.app.guiconfig["recentfiles"] num_files = len(recent_files) if num_files == 0: diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index a893dec8..801d236f 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -101,6 +101,9 @@ class Menubar(tk.Menu): menu.add_command( label="Paste", accelerator="Ctrl+V", command=self.menuaction.paste ) + menu.add_command( + label="Delete", accelerator="Ctrl+D", command=self.menuaction.delete + ) menu.add_separator() menu.add_command(label="Select all", accelerator="Ctrl+A", state=tk.DISABLED) menu.add_command( @@ -113,6 +116,7 @@ class Menubar(tk.Menu): self.app.master.bind_all("", self.menuaction.copy) self.app.master.bind_all("", self.menuaction.paste) + self.app.master.bind_all("", self.menuaction.delete) self.edit_menu = menu def draw_canvas_menu(self): From 6479afb7b2caa531cfb20cd69a21da29022605f3 Mon Sep 17 00:00:00 2001 From: Shaun Voigt Date: Fri, 27 Mar 2020 18:19:07 +1030 Subject: [PATCH 0102/1131] Enable OSPFv2 Adjacency Widget to work with FRR --- gui/widget.tcl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/widget.tcl b/gui/widget.tcl index 1a8f6b7a..4e42556c 100644 --- a/gui/widget.tcl +++ b/gui/widget.tcl @@ -1972,7 +1972,7 @@ proc widget_adjacency_init {command} { array unset adjacency_cache * foreach node $node_list { ;# save router-id node pairs for later lookup if { [nodeType $node] != "router" } { continue } - if {[lsearch [getNodeServices $node true] "zebra"] < 0 && + if {[lsearch -regexp [getNodeServices $node true] "(FRR)?zebra"] < 0 && [lsearch [getNodeServices $node true] "OLSR"] < 0 && [lsearch [getNodeServices $node true] "OLSRv2"] < 0} { continue @@ -2041,7 +2041,7 @@ proc widget_adjacency_periodic { now } { foreach node $node_list { if { [nodeType $node] != "router" } { continue } if { [getNodeCanvas $node] != $curcanvas } { continue } - if {[lsearch [getNodeServices $node true] "zebra"] < 0} { + if {[lsearch -regexp [getNodeServices $node true] "(FRR)?zebra"] < 0} { continue } From 3d59cd0ad8f7b7565c7347abc1ab17bec6302a67 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 27 Mar 2020 17:22:44 -0700 Subject: [PATCH 0103/1131] initial logic for working emane links based on emane stats --- daemon/core/emane/emanemanager.py | 48 ++++- daemon/core/emane/linkmonitor.py | 299 ++++++++++++++++++++++++++++++ daemon/core/xml/corexml.py | 6 +- 3 files changed, 343 insertions(+), 10 deletions(-) create mode 100644 daemon/core/emane/linkmonitor.py diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 82e37f43..c65dde63 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -15,6 +15,7 @@ from core.emane.bypass import EmaneBypassModel from core.emane.commeffect import EmaneCommEffectModel from core.emane.emanemodel import EmaneModel from core.emane.ieee80211abg import EmaneIeee80211abgModel +from core.emane.linkmonitor import EmaneLinkMonitor from core.emane.nodes import EmaneNet from core.emane.rfpipe import EmaneRfPipeModel from core.emane.tdma import EmaneTdmaModel @@ -90,6 +91,9 @@ class EmaneManager(ModelManager): self.emane_config = EmaneGlobalModel(session) self.set_configs(self.emane_config.default_values()) + # link monitor + self.link_monitor = EmaneLinkMonitor(self) + self.service = None self.eventchannel = None self.event_device = None @@ -349,9 +353,13 @@ class EmaneManager(ModelManager): f.write(f"{nodename} {ifname} {nemid}\n") except IOError: logging.exception("Error writing EMANE NEMs file: %s") - + if self.links_enabled(): + self.link_monitor.start() return EmaneManager.SUCCESS + def links_enabled(self) -> bool: + return self.get_config("link_enabled") == "1" + def poststartup(self) -> None: """ Retransmit location events now that all NEMs are active. @@ -393,7 +401,9 @@ class EmaneManager(ModelManager): with self._emane_node_lock: if not self._emane_nets: return - logging.info("stopping EMANE daemons.") + logging.info("stopping EMANE daemons") + if self.links_enabled(): + self.link_monitor.stop() self.deinstallnetifs() self.stopdaemons() self.stopeventmonitor() @@ -834,13 +844,37 @@ class EmaneGlobalModel: def __init__(self, session: "Session") -> None: self.session = session - self.nem_config = [ + self.core_config = [ Configuration( _id="nem_id_start", _type=ConfigDataTypes.INT32, default="1", - label="Starting NEM ID (core)", - ) + label="Starting NEM ID", + ), + Configuration( + _id="link_enabled", + _type=ConfigDataTypes.BOOL, + default="1", + label="Enable Links?", + ), + Configuration( + _id="loss_threshold", + _type=ConfigDataTypes.INT32, + default="30", + label="Link Loss Threshold (%)", + ), + Configuration( + _id="link_interval", + _type=ConfigDataTypes.INT32, + default="1", + label="Link Check Interval (sec)", + ), + Configuration( + _id="link_timeout", + _type=ConfigDataTypes.INT32, + default="4", + label="Link Timeout (sec)", + ), ] self.emulator_config = None self.parse_config() @@ -868,14 +902,14 @@ class EmaneGlobalModel: ) def configurations(self) -> List[Configuration]: - return self.emulator_config + self.nem_config + return self.emulator_config + self.core_config def config_groups(self) -> List[ConfigGroup]: emulator_len = len(self.emulator_config) config_len = len(self.configurations()) return [ ConfigGroup("Platform Attributes", 1, emulator_len), - ConfigGroup("NEM Parameters", emulator_len + 1, config_len), + ConfigGroup("CORE Configuration", emulator_len + 1, config_len), ] def default_values(self) -> Dict[str, str]: diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py new file mode 100644 index 00000000..074aaf68 --- /dev/null +++ b/daemon/core/emane/linkmonitor.py @@ -0,0 +1,299 @@ +import logging +import sched +import threading +import time +from typing import TYPE_CHECKING, Dict, List, Tuple + +import emane.shell as emanesh +import netaddr +from lxml import etree + +from core.emulator.data import LinkData +from core.emulator.enumerations import LinkTypes, MessageFlags +from core.nodes.network import CtrlNet + +if TYPE_CHECKING: + from core.emane.emanemanager import EmaneManager + +DEFAULT_PORT = 47_000 +MAC_COMPONENT_INDEX = 1 +EMANE_RFPIPE = "rfpipemaclayer" +EMANE_80211 = "ieee80211abgmaclayer" +EMANE_TDMA = "tdmaeventschedulerradiomodel" +SINR_TABLE = "NeighborStatusTable" +NEM_SELF = 65535 + + +class LossTable: + def __init__(self, losses: Dict[float, float]) -> None: + self.losses = losses + self.sinrs = sorted(self.losses.keys()) + self.loss_lookup = {} + for index, value in enumerate(self.sinrs): + self.loss_lookup[index] = self.losses[value] + self.mac_id = None + + def get_loss(self, sinr: float) -> float: + index = self._get_index(sinr) + loss = 100.0 - self.loss_lookup[index] + return loss + + def _get_index(self, current_sinr: float) -> int: + for index, sinr in enumerate(self.sinrs): + if current_sinr <= sinr: + return index + return len(self.sinrs) - 1 + + +class EmaneLink: + def __init__(self, from_nem: int, to_nem: int, sinr: float) -> None: + self.from_nem = from_nem + self.to_nem = to_nem + self.sinr = sinr + self.last_seen = None + self.touch() + + def update(self, sinr: float) -> None: + self.sinr = sinr + self.touch() + + def touch(self) -> None: + self.last_seen = time.monotonic() + + def is_dead(self, timeout: int) -> bool: + return (time.monotonic() - self.last_seen) >= timeout + + def __repr__(self) -> str: + return f"EmaneLink({self.from_nem}, {self.to_nem}, {self.sinr})" + + +class EmaneClient: + def __init__(self, address: str) -> None: + self.address = address + self.client = emanesh.ControlPortClient(self.address, DEFAULT_PORT) + self.nems = {} + self.setup() + + def setup(self) -> None: + manifest = self.client.getManifest() + for nem_id, components in manifest.items(): + # get mac config + mac_id, _, emane_model = components[MAC_COMPONENT_INDEX] + mac_config = self.client.getConfiguration(mac_id) + logging.debug( + "address(%s) nem(%s) emane(%s)", self.address, nem_id, emane_model + ) + + # create loss table based on current configuration + if emane_model == EMANE_80211: + loss_table = self.handle_80211(mac_config) + elif emane_model == EMANE_RFPIPE: + loss_table = self.handle_rfpipe(mac_config) + else: + logging.warning("unknown emane link model: %s", emane_model) + continue + loss_table.mac_id = mac_id + self.nems[nem_id] = loss_table + + def check_links( + self, links: Dict[Tuple[int, int], EmaneLink], loss_threshold: int + ) -> None: + for from_nem, loss_table in self.nems.items(): + tables = self.client.getStatisticTable(loss_table.mac_id, (SINR_TABLE,)) + table = tables[SINR_TABLE][1:][0] + for row in table: + row = row + to_nem = row[0][0] + sinr = row[5][0] + age = row[-1][0] + + # exclude invalid links + is_self = to_nem == NEM_SELF + has_valid_age = 0 <= age <= 1 + if is_self or not has_valid_age: + continue + + # check if valid link loss + link_key = (from_nem, to_nem) + loss = loss_table.get_loss(sinr) + if loss < loss_threshold: + link = links.get(link_key) + if link: + link.update(sinr) + else: + link = EmaneLink(from_nem, to_nem, sinr) + links[link_key] = link + + def handle_tdma(self, config: Dict[str, Tuple]): + pcr = config["pcrcurveuri"][0][0] + logging.debug("tdma pcr: %s", pcr) + + def handle_80211(self, config: Dict[str, Tuple]) -> LossTable: + unicastrate = config["unicastrate"][0][0] + pcr = config["pcrcurveuri"][0][0] + logging.debug("80211 pcr: %s", pcr) + tree = etree.parse(pcr) + root = tree.getroot() + table = root.find("table") + losses = {} + for rate in table.iter("datarate"): + index = int(rate.get("index")) + if index == unicastrate: + for row in rate.iter("row"): + sinr = float(row.get("sinr")) + por = float(row.get("por")) + losses[sinr] = por + return LossTable(losses) + + def handle_rfpipe(self, config: Dict[str, Tuple]) -> LossTable: + pcr = config["pcrcurveuri"][0][0] + logging.debug("rfpipe pcr: %s", pcr) + tree = etree.parse(pcr) + root = tree.getroot() + table = root.find("table") + losses = {} + for row in table.iter("row"): + sinr = float(row.get("sinr")) + por = float(row.get("por")) + losses[sinr] = por + return LossTable(losses) + + def stop(self) -> None: + self.client.stop() + + +class EmaneLinkMonitor: + def __init__(self, emane_manager: "EmaneManager") -> None: + self.emane_manager = emane_manager + self.clients = [] + self.links = {} + self.complete_links = set() + self.loss_threshold = None + self.link_interval = None + self.link_timeout = None + self.scheduler = None + self.running = False + + def start(self) -> None: + self.loss_threshold = int(self.emane_manager.get_config("loss_threshold")) + self.link_interval = int(self.emane_manager.get_config("link_interval")) + self.link_timeout = int(self.emane_manager.get_config("link_timeout")) + self.initialize() + self.scheduler = sched.scheduler() + self.scheduler.enter(0, 0, self.check_links) + self.running = True + thread = threading.Thread(target=self.scheduler.run, daemon=True) + thread.start() + + def initialize(self) -> None: + addresses = self.get_addresses() + for address in addresses: + client = EmaneClient(address) + self.clients.append(client) + + def get_addresses(self) -> List[str]: + addresses = [] + nodes = self.emane_manager.getnodes() + for node in nodes: + logging.info("link monitor node: %s", node.name) + for netif in node.netifs(): + if isinstance(netif.net, CtrlNet): + ip4 = None + for x in netif.addrlist: + address, prefix = x.split("/") + if netaddr.valid_ipv4(address): + ip4 = address + if ip4: + addresses.append(ip4) + break + return addresses + + def check_links(self) -> None: + # check for new links + previous_links = set(self.links.keys()) + for client in self.clients: + try: + client.check_links(self.links, self.loss_threshold) + except emanesh.ControlPortException: + if self.running: + logging.exception("link monitor error") + + # find new links + current_links = set(self.links.keys()) + new_links = current_links - previous_links + + # find dead links + dead_links = [] + for link_id, link in self.links.items(): + if link.is_dead(self.link_timeout): + dead_links.append(link_id) + + # announce dead links + for link_id in dead_links: + del self.links[link_id] + complete_id = self.get_complete_id(link_id) + if complete_id in self.complete_links: + self.complete_links.remove(complete_id) + self.send_link(MessageFlags.DELETE, complete_id) + + # announce new links + for link_id in new_links: + complete_id = self.get_complete_id(link_id) + if complete_id in self.complete_links: + continue + if self.is_complete_link(link_id): + self.complete_links.add(complete_id) + self.send_link(MessageFlags.ADD, complete_id) + + if self.running: + self.scheduler.enter(self.link_interval, 0, self.check_links) + + def get_complete_id(self, link_id: Tuple[int, int]) -> Tuple[int, int]: + value_one, value_two = link_id + if value_one < value_two: + return value_one, value_two + else: + return value_two, value_one + + def is_complete_link(self, link_id: Tuple[int, int]) -> bool: + reverse_id = link_id[1], link_id[0] + return link_id in self.links and reverse_id in self.links + + def send_link(self, message_type: MessageFlags, link_id: Tuple[int, int]) -> None: + nem_one, nem_two = link_id + emane_one, netif = self.emane_manager.nemlookup(nem_one) + if not emane_one or not netif: + logging.error("invalid nem: %s", nem_one) + return + node_one = netif.node + emane_two, netif = self.emane_manager.nemlookup(nem_two) + if not emane_two or not netif: + logging.error("invalid nem: %s", nem_two) + return + node_two = netif.node + logging.debug( + "%s emane link from %s(%s) to %s(%s)", + message_type.name, + node_one.name, + nem_one, + node_two.name, + nem_two, + ) + self.send_message(message_type, node_one.id, node_two.id, emane_one.id) + + def send_message(self, message_type, node_one, node_two, emane_id) -> None: + link_data = LinkData( + message_type=message_type, + node1_id=node_one, + node2_id=node_two, + network_id=emane_id, + link_type=LinkTypes.WIRELESS, + ) + self.emane_manager.session.broadcast_link(link_data) + + def stop(self) -> None: + self.running = False + for client in self.clients: + client.stop() + self.clients.clear() + self.links.clear() diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 68426905..126b81e4 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -82,9 +82,9 @@ def create_emane_config( add_configuration(emulator_element, emulator_config.id, value) nem_element = etree.SubElement(emane_configuration, "nem") - for nem_config in emane_config.nem_config: - value = config[nem_config.id] - add_configuration(nem_element, nem_config.id, value) + for core_config in emane_config.core_config: + value = config[core_config.id] + add_configuration(nem_element, core_config.id, value) return emane_configuration From 0b30289879f1eb515c4c158b33741e22bf53d830 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 27 Mar 2020 22:47:16 -0700 Subject: [PATCH 0104/1131] emane link monitor clear complete links during shutdown --- daemon/core/emane/linkmonitor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py index 074aaf68..1091c91c 100644 --- a/daemon/core/emane/linkmonitor.py +++ b/daemon/core/emane/linkmonitor.py @@ -297,3 +297,4 @@ class EmaneLinkMonitor: client.stop() self.clients.clear() self.links.clear() + self.complete_links.clear() From 6c5c2c5674e932582339bcf4b2e5d33201c2f30d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 28 Mar 2020 13:06:46 -0700 Subject: [PATCH 0105/1131] fixed core xml to properly write and read emane global configurations --- daemon/core/xml/corexml.py | 48 +++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 68426905..cbc7f937 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -16,7 +16,6 @@ from core.nodes.network import CtrlNet from core.services.coreservices import CoreService if TYPE_CHECKING: - from core.emane.emanemanager import EmaneGlobalModel from core.emane.emanemodel import EmaneModel from core.emulator.session import Session @@ -69,23 +68,17 @@ def create_interface_data(interface_element: etree.Element) -> InterfaceData: return InterfaceData(interface_id, name, mac, ip4, ip4_mask, ip6, ip6_mask) -def create_emane_config( - node_id: int, emane_config: "EmaneGlobalModel", config: Dict[str, str] -) -> etree.Element: - emane_configuration = etree.Element("emane_configuration") - add_attribute(emane_configuration, "node", node_id) - add_attribute(emane_configuration, "model", "emane") - +def create_emane_config(session: "Session") -> etree.Element: + emane_configuration = etree.Element("emane_global_configuration") + config = session.emane.get_configs() emulator_element = etree.SubElement(emane_configuration, "emulator") - for emulator_config in emane_config.emulator_config: + for emulator_config in session.emane.emane_config.emulator_config: value = config[emulator_config.id] add_configuration(emulator_element, emulator_config.id, value) - nem_element = etree.SubElement(emane_configuration, "nem") - for nem_config in emane_config.nem_config: + for nem_config in session.emane.emane_config.nem_config: value = config[nem_config.id] add_configuration(nem_element, nem_config.id, value) - return emane_configuration @@ -360,6 +353,9 @@ class CoreXmlWriter: self.scenario.append(metadata_elements) def write_emane_configs(self) -> None: + emane_global_configuration = create_emane_config(self.session) + self.scenario.append(emane_global_configuration) + emane_configurations = etree.Element("emane_configurations") for node_id in self.session.emane.nodes(): all_configs = self.session.emane.get_all_configs(node_id) @@ -371,17 +367,9 @@ class CoreXmlWriter: logging.debug( "writing emane config node(%s) model(%s)", node_id, model_name ) - if model_name == -1: - emane_configuration = create_emane_config( - node_id, self.session.emane.emane_config, config - ) - else: - model = self.session.emane.models[model_name] - emane_configuration = create_emane_model_config( - node_id, model, config - ) + model = self.session.emane.models[model_name] + emane_configuration = create_emane_model_config(node_id, model, config) emane_configurations.append(emane_configuration) - if emane_configurations.getchildren(): self.scenario.append(emane_configurations) @@ -613,6 +601,7 @@ class CoreXmlReader: self.read_session_origin() self.read_service_configs() self.read_mobility_configs() + self.read_emane_global_config() self.read_emane_configs() self.read_nodes() self.read_configservice_configs() @@ -740,6 +729,21 @@ class CoreXmlReader: files.add(name) service.configs = tuple(files) + def read_emane_global_config(self) -> None: + emane_global_configuration = self.scenario.find("emane_global_configuration") + emulator_configuration = emane_global_configuration.find("emulator") + configs = {} + for config in emulator_configuration.iterchildren(): + name = config.get("name") + value = config.get("value") + configs[name] = value + nem_configuration = emane_global_configuration.find("nem") + for config in nem_configuration.iterchildren(): + name = config.get("name") + value = config.get("value") + configs[name] = value + self.session.emane.set_configs(config=configs) + def read_emane_configs(self) -> None: emane_configurations = self.scenario.find("emane_configurations") if emane_configurations is None: From 6616f104e689cc83daec260131e62d0c0600b233 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 30 Mar 2020 10:19:23 -0700 Subject: [PATCH 0106/1131] tweak github action to sync pipenv and not install newer packages --- .github/workflows/daemon-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/daemon-checks.yml b/.github/workflows/daemon-checks.yml index 00537c8e..c7f3f0ec 100644 --- a/.github/workflows/daemon-checks.yml +++ b/.github/workflows/daemon-checks.yml @@ -19,7 +19,7 @@ jobs: cp setup.py.in setup.py cp core/constants.py.in core/constants.py sed -i 's/True/False/g' core/constants.py - pipenv install --dev + pipenv sync --dev - name: isort run: | cd daemon From 52ff5ce62b515793f211267eb6807440faf2a650 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 30 Mar 2020 10:31:36 -0700 Subject: [PATCH 0107/1131] improvement to github action isort check to print change needed, added pipenv install to be verbose --- .github/workflows/daemon-checks.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/daemon-checks.yml b/.github/workflows/daemon-checks.yml index c7f3f0ec..dc7365f7 100644 --- a/.github/workflows/daemon-checks.yml +++ b/.github/workflows/daemon-checks.yml @@ -19,11 +19,11 @@ jobs: cp setup.py.in setup.py cp core/constants.py.in core/constants.py sed -i 's/True/False/g' core/constants.py - pipenv sync --dev + pipenv -v sync --dev - name: isort run: | cd daemon - pipenv run isort -c + pipenv run isort -c -df - name: black run: | cd daemon From 16cc73c07029ac8ba4eef0524176f54de272a0dc Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 30 Mar 2020 10:36:39 -0700 Subject: [PATCH 0108/1131] import change to help isort pass --- .github/workflows/daemon-checks.yml | 2 +- daemon/core/emane/linkmonitor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/daemon-checks.yml b/.github/workflows/daemon-checks.yml index dc7365f7..ca2de7d8 100644 --- a/.github/workflows/daemon-checks.yml +++ b/.github/workflows/daemon-checks.yml @@ -19,7 +19,7 @@ jobs: cp setup.py.in setup.py cp core/constants.py.in core/constants.py sed -i 's/True/False/g' core/constants.py - pipenv -v sync --dev + pipenv sync --dev - name: isort run: | cd daemon diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py index 1091c91c..2151eb30 100644 --- a/daemon/core/emane/linkmonitor.py +++ b/daemon/core/emane/linkmonitor.py @@ -4,8 +4,8 @@ import threading import time from typing import TYPE_CHECKING, Dict, List, Tuple -import emane.shell as emanesh import netaddr +from emane import shell as emanesh from lxml import etree from core.emulator.data import LinkData From 7b29f6bb82a2b299070424281488efd506bfc228 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 30 Mar 2020 10:46:05 -0700 Subject: [PATCH 0109/1131] change to account for importing slightly older bindings for emane link monitor --- daemon/core/emane/linkmonitor.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py index 2151eb30..7e9c191b 100644 --- a/daemon/core/emane/linkmonitor.py +++ b/daemon/core/emane/linkmonitor.py @@ -5,13 +5,21 @@ import time from typing import TYPE_CHECKING, Dict, List, Tuple import netaddr -from emane import shell as emanesh from lxml import etree from core.emulator.data import LinkData from core.emulator.enumerations import LinkTypes, MessageFlags from core.nodes.network import CtrlNet +try: + from emane import shell +except ImportError: + try: + from emanesh import shell + except ImportError: + logging.debug("compatible emane python bindings not installed") + + if TYPE_CHECKING: from core.emane.emanemanager import EmaneManager @@ -70,7 +78,7 @@ class EmaneLink: class EmaneClient: def __init__(self, address: str) -> None: self.address = address - self.client = emanesh.ControlPortClient(self.address, DEFAULT_PORT) + self.client = shell.ControlPortClient(self.address, DEFAULT_PORT) self.nems = {} self.setup() @@ -214,7 +222,7 @@ class EmaneLinkMonitor: for client in self.clients: try: client.check_links(self.links, self.loss_threshold) - except emanesh.ControlPortException: + except shell.ControlPortException: if self.running: logging.exception("link monitor error") From 7e0efa7020cb3a21314a6ceaa95521313935aafb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 30 Mar 2020 12:00:22 -0700 Subject: [PATCH 0110/1131] updated sdt plugin to support layering core nodes and links as well as wireless links into network layers, also using linkid to support multiple links between nodes --- daemon/core/plugins/sdt.py | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index a759228d..0c23f567 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -21,11 +21,18 @@ if TYPE_CHECKING: from core.emulator.session import Session -def link_data_params(link_data: LinkData) -> Tuple[int, int, bool]: +def link_data_params(link_data: LinkData) -> Tuple[int, int, bool, int]: node_one = link_data.node1_id node_two = link_data.node2_id is_wireless = link_data.link_type == LinkTypes.WIRELESS - return node_one, node_two, is_wireless + network_id = link_data.network_id + return node_one, node_two, is_wireless, network_id + + +CORE_LAYER = "CORE" +NODE_LAYER = "CORE::Nodes" +LINK_LAYER = "CORE::Links" +CORE_LAYERS = [CORE_LAYER, LINK_LAYER, NODE_LAYER] class Sdt: @@ -200,6 +207,10 @@ class Sdt: :return: nothing """ nets = [] + # create layers + for layer in CORE_LAYERS: + self.cmd(f"layer {layer}") + with self.session._nodes_lock: for node_id in self.session.nodes: node = self.session.nodes[node_id] @@ -253,7 +264,10 @@ class Sdt: icon = icon.replace("$CORE_DATA_DIR", constants.CORE_DATA_DIR) icon = icon.replace("$CORE_CONF_DIR", constants.CORE_CONF_DIR) self.cmd(f"sprite {node_type} image {icon}") - self.cmd(f'node {node.id} type {node_type} label on,"{node.name}" {pos}') + self.cmd( + f'node {node.id} nodeLayer "{NODE_LAYER}" ' + f'type {node_type} label on,"{node.name}" {pos}' + ) def edit_node(self, node: NodeBase, lon: float, lat: float, alt: float) -> None: """ @@ -333,13 +347,16 @@ class Sdt: pass return result - def add_link(self, node_one: int, node_two: int, is_wireless: bool) -> None: + def add_link( + self, node_one: int, node_two: int, is_wireless: bool, network_id: int = None + ) -> None: """ Handle adding a link in SDT. :param node_one: node one id :param node_two: node two id :param is_wireless: True if link is wireless, False otherwise + :param network_id: network link is associated with :return: nothing """ logging.debug("sdt add link: %s, %s, %s", node_one, node_two, is_wireless) @@ -351,7 +368,15 @@ class Sdt: attr = "green,2" else: attr = "red,2" - self.cmd(f"link {node_one},{node_two} line {attr}") + link_id = f"{node_one}-{node_two}" + if network_id is not None: + link_id = f"{link_id}-{network_id}" + layer = LINK_LAYER + if network_id: + node = self.session.nodes[network_id] + network_name = node.name + layer = f"{layer}::{network_name}" + self.cmd(f"link {node_one},{node_two},{link_id} linkLayer {layer} line {attr}") def delete_link(self, node_one: int, node_two: int) -> None: """ From 16764c702b1f548ab069a800e2a05cffcf637195 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 30 Mar 2020 12:26:08 -0700 Subject: [PATCH 0111/1131] updated emane link monitor to not run when there is nothing to monitor, added better logging about what is being monitored --- daemon/core/emane/linkmonitor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py index 7e9c191b..6d4daa8d 100644 --- a/daemon/core/emane/linkmonitor.py +++ b/daemon/core/emane/linkmonitor.py @@ -100,6 +100,7 @@ class EmaneClient: else: logging.warning("unknown emane link model: %s", emane_model) continue + logging.info("monitoring links nem(%s) model(%s)", nem_id, emane_model) loss_table.mac_id = mac_id self.nems[nem_id] = loss_table @@ -187,6 +188,9 @@ class EmaneLinkMonitor: self.link_interval = int(self.emane_manager.get_config("link_interval")) self.link_timeout = int(self.emane_manager.get_config("link_timeout")) self.initialize() + if not self.clients: + logging.info("no valid emane models to monitor links") + return self.scheduler = sched.scheduler() self.scheduler.enter(0, 0, self.check_links) self.running = True @@ -197,13 +201,13 @@ class EmaneLinkMonitor: addresses = self.get_addresses() for address in addresses: client = EmaneClient(address) - self.clients.append(client) + if client.nems: + self.clients.append(client) def get_addresses(self) -> List[str]: addresses = [] nodes = self.emane_manager.getnodes() for node in nodes: - logging.info("link monitor node: %s", node.name) for netif in node.netifs(): if isinstance(netif.net, CtrlNet): ip4 = None From 8186c62b195d9b7580c512b4820a41db43417efc Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 31 Mar 2020 09:41:29 -0700 Subject: [PATCH 0112/1131] switched core emulator data files from using namedtuples to backported dataclasses --- daemon/Pipfile.lock | 207 +++++++++++------------ daemon/core/api/grpc/events.py | 4 +- daemon/core/api/tlv/corehandlers.py | 8 +- daemon/core/api/tlv/dataconversion.py | 9 +- daemon/core/emulator/data.py | 230 +++++++++++++------------- daemon/core/emulator/session.py | 2 +- daemon/core/nodes/base.py | 10 +- daemon/core/nodes/network.py | 2 +- daemon/core/services/coreservices.py | 6 +- daemon/requirements.txt | 1 + daemon/setup.py.in | 3 +- 11 files changed, 240 insertions(+), 242 deletions(-) diff --git a/daemon/Pipfile.lock b/daemon/Pipfile.lock index fe07d856..e4a4b55b 100644 --- a/daemon/Pipfile.lock +++ b/daemon/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d702e6eed5a1362bf261543572bbffd2e8a87140b8d8cb07b99fb0d25220a2b5" + "sha256": "199897f713f6f338316b33fcbbe0001e9e55fcd5e5e24b2245a89454ce13321f" }, "pipfile-spec": 6, "requires": {}, @@ -100,6 +100,14 @@ ], "version": "==2.8" }, + "dataclasses": { + "hashes": [ + "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836", + "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6" + ], + "index": "pypi", + "version": "==0.7" + }, "fabric": { "hashes": [ "sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389", @@ -197,9 +205,10 @@ }, "mako": { "hashes": [ - "sha256:2984a6733e1d472796ceef37ad48c26f4a984bb18119bb2dbc37a44d8f6e75a4" + "sha256:3139c5d64aa5d175dbafb95027057128b5fbd05a40c53999f3905ceb53366d9d", + "sha256:8e8b53c71c7e59f3de716b6832c4e401d903af574f6962edbbbf6ecc2a5fe6c9" ], - "version": "==1.1.1" + "version": "==1.1.2" }, "markupsafe": { "hashes": [ @@ -280,34 +289,12 @@ ], "version": "==7.0.0" }, - "protobuf": { - "hashes": [ - "sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab", - "sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f", - "sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a", - "sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0", - "sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4", - "sha256:7774bbbaac81d3ba86de646c39f154afc8156717972bf0450c9dbfa1dc8dbea2", - "sha256:82d7ac987715d8d1eb4068bf997f3053468e0ce0287e2729c30601feb6602fee", - "sha256:8eb9c93798b904f141d9de36a0ba9f9b73cc382869e67c9e642c0aba53b0fc07", - "sha256:adf0e4d57b33881d0c63bb11e7f9038f98ee0c3e334c221f0858f826e8fb0151", - "sha256:c40973a0aee65422d8cb4e7d7cbded95dfeee0199caab54d5ab25b63bce8135a", - "sha256:c77c974d1dadf246d789f6dad1c24426137c9091e930dbf50e0a29c1fcf00b1f", - "sha256:dd9aa4401c36785ea1b6fff0552c674bdd1b641319cb07ed1fe2392388e9b0d7", - "sha256:e11df1ac6905e81b815ab6fd518e79be0a58b5dc427a2cf7208980f30694b956", - "sha256:e2f8a75261c26b2f5f3442b0525d50fd79a71aeca04b5ec270fc123536188306", - "sha256:e512b7f3a4dd780f59f1bf22c302740e27b10b5c97e858a6061772668cd6f961", - "sha256:ef2c2e56aaf9ee914d3dccc3408d42661aaf7d9bb78eaa8f17b2e6282f214481", - "sha256:fac513a9dc2a74b99abd2e17109b53945e364649ca03d9f7a0b96aa8d1807d0a", - "sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80" - ], - "version": "==3.11.3" - }, "pycparser": { "hashes": [ - "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], - "version": "==2.19" + "version": "==2.20" }, "pynacl": { "hashes": [ @@ -337,49 +324,46 @@ }, "pyproj": { "hashes": [ - "sha256:0a12982df36f55412597431676e51d3e8fcf9b3e41f18103c31edfb1fc5fa4c0", - "sha256:0b57669a568e4235f09fea9c4e498b9beca2673ea7318989569dbb750ed299c5", - "sha256:155064fde6a95f6328962386ebde043679fd744f1415e512ed88ec47760ed47c", - "sha256:189b8278784655ee2a3bfc51bde3091b5615cc982d0017edabcb10099b2ccb3f", - "sha256:1db407591f99877b551a655897da1fd95f4e82e089c8b0d29bcd8beffcffedb8", - "sha256:226e0c126d6db158dd3da8879e5efab9f05b1d67989c33fc6aa73bf70409bb12", - "sha256:2842412ea3f99383850df92dbbca837847f3e574f98f81eaa8caebc6514a26e2", - "sha256:2d2884e85b1e69ff829bfd54872c322d3d5662dc2120a17fbd1094b9c08f9dc5", - "sha256:341dc836a1a57b74494a95cff0f05029988d93e1f96ba6c190384ec757d482b2", - "sha256:3d69b6a197fc8cf3585290e272e1cdd641d6834a3c71894ec4f2b800d2210d2a", - "sha256:447d5b18d941bea180f04179946d1d4f4aa5012697d78c9a4ceac6081dd32465", - "sha256:4e8f18a8be5653e90f24b0aea74e85e10271d1c537742ede8a11b569d3583125", - "sha256:659b1d748cd7480324841da93f91097a726b898a2de0d192bc771d374006ceb4", - "sha256:6972adfe6bb40da0423c12c38617809bf50ca8b7411a20795a1c6c3d96f10942", - "sha256:75d7ed27e2e081d2036647f7b40a9e3d4f9ec4bde795925f3f7b4c6bb85f742e", - "sha256:7b623a18f70e70cbe594fa429283027c1a73d6d31c70cd04eea65845cd060b76", - "sha256:8112da72b47af9ffcc8f0f42224898ba6371680501b3657091bb7420b7dd5c03", - "sha256:9686c611893d1c182befa63157f4a1d629e7caa464adf21309cf4da5d422a264", - "sha256:98bb690ca7ea50148792f656c0366e799d70dd7e43ab8f0c733b64bd96842e1c", - "sha256:a6ede79fd7ddd176d824e0366f8d326ff8bc082d7332c9b40baf8cb8ae7d51fe", - "sha256:c7e7b6a00a701e166e5ce903159282f2969eef689fd7fb9d7bcf92aaf167e150", - "sha256:cb8c57faf91173c219739a37b909edc1c35a48a86d26be17f1a21ffd9f8728c3", - "sha256:ea6c7cbe2f277ca6b32ebad77d713681819e23b07b17a4a892878ffe245826b7", - "sha256:ec4b2146ec8fcc93c38fbd1dcb0df06e5737d588fe28d833dfb2b241d2736f54", - "sha256:f540f4af0223cb2195b0953db6c5cb45256137da430657db42ad1b076caca361" + "sha256:0d8196a5ac75fee2cf71c21066b3344427abfa8ad69b536d3404d5c7c9c0b886", + "sha256:12e378a0a21c73f96177f6cf64520f17e6b7aa02fc9cb27bd5c2d5b06ce170af", + "sha256:17738836128704d8f80b771572d77b8733841f0cb0ca42620549236ea62c4663", + "sha256:1a39175944710b225fd1943cb3b8ea0c8e059d3016360022ca10bbb7a6bfc9ae", + "sha256:2566bffb5395c9fbdb02077a0bc3e3ed0b2e4e3cadf65019e3139a8dfe27dd1d", + "sha256:3f43277f21ddaabed93b9885a4e494b785dca56e31fd37a935519d99b07807f0", + "sha256:424304beca6e0b0bc12aa46fc6d14a481ea47b1a4edec4854bb281656de38948", + "sha256:48128d794c8f52fcff2433a481e3aa2ccb0e0b3ccd51d3ad7cc10cc488c3f547", + "sha256:4a16b650722982cddedd45dfc36435b96e0ba83a2aebd4a4c247e5a68c852442", + "sha256:5161f1b5ece8a5263b64d97a32fbc473a4c6fdca5c95478e58e519ef1e97528e", + "sha256:6839ce14635ebfb01c67e456148f4f1fa04b03ef9645551b89d36593f2a3e57d", + "sha256:80e9f85ab81da75289308f23a62e1426a38411a07b0da738958d65ae8cc6c59c", + "sha256:881b44e94c781d02ecf1d9314fc7f44c09e6d54a8eac281869365999ac4db7a1", + "sha256:977542d2f8cf2981cf3ad72cedfebcd6ac56977c7aa830d9b49fa7888b56e83d", + "sha256:9bba6cbff7e23bb6d9062786d516602681b4414e9e423c138a7360e4d2a193e8", + "sha256:9bf64bba03ddc534ed3c6271ba8f9d31040f40cf8e9e7e458b6b1524a6f59082", + "sha256:9c712ceaa01488ebe6e357e1dfa2434c2304aad8a810e5d4c3d2abe21def6d58", + "sha256:b7da17e5a5c6039f85843e88c2f1ca8606d1a4cc13a87e7b68b9f51a54ef201a", + "sha256:bcdf81b3f13d2cc0354a4c3f7a567b71fcf6fe8098e519aaaee8e61f05c9de10", + "sha256:bebd3f987b7196e9d2ccfe55911b0c76ba9ce309bcabfb629ef205cbaaad37c5", + "sha256:c244e923073cd0bab74ba861ba31724aab90efda35b47a9676603c1a8e80b3ba", + "sha256:dacb94a9d570f4d9fc9369a22d44d7b3071cfe4d57d0ff2f57abd7ef6127fe41" ], - "version": "==2.5.0" + "version": "==2.6.0" }, "pyyaml": { "hashes": [ - "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6", - "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf", - "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5", - "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e", - "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811", - "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e", - "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d", - "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20", - "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689", - "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994", - "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615" + "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", + "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", + "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", + "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", + "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", + "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", + "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", + "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", + "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" ], - "version": "==5.3" + "version": "==5.3.1" }, "six": { "hashes": [ @@ -421,10 +405,10 @@ }, "click": { "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc", + "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a" ], - "version": "==7.0" + "version": "==7.1.1" }, "distlib": { "hashes": [ @@ -553,26 +537,26 @@ }, "identify": { "hashes": [ - "sha256:1222b648251bdcb8deb240b294f450fbf704c7984e08baa92507e4ea10b436d5", - "sha256:d824ebe21f38325c771c41b08a95a761db1982f1fc0eee37c6c97df3f1636b96" + "sha256:a7577a1f55cee1d21953a5cf11a3c839ab87f5ef909a4cba6cf52ed72b4c6059", + "sha256:ab246293e6585a1c6361a505b68d5b501a0409310932b7de2c2ead667b564d89" ], - "version": "==1.4.11" + "version": "==1.4.13" }, "importlib-metadata": { "hashes": [ - "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302", - "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b" + "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", + "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" ], "markers": "python_version < '3.8'", - "version": "==1.5.0" + "version": "==1.6.0" }, "importlib-resources": { "hashes": [ - "sha256:6e2783b2538bd5a14678284a3962b0660c715e5a0f10243fd5e00a4b5974f50b", - "sha256:d3279fd0f6f847cced9f7acc19bd3e5df54d34f93a2e7bb5f238f81545787078" + "sha256:4019b6a9082d8ada9def02bece4a76b131518866790d58fdda0b5f8c603b36c2", + "sha256:dd98ceeef3f5ad2ef4cc287b8586da4ebad15877f351e9688987ad663a0a29b8" ], "markers": "python_version < '3.7'", - "version": "==1.0.2" + "version": "==1.4.0" }, "isort": { "hashes": [ @@ -591,11 +575,11 @@ }, "mock": { "hashes": [ - "sha256:2a572b715f09dd2f0a583d8aeb5bb67d7ed7a8fd31d193cf1227a99c16a67bc3", - "sha256:5e48d216809f6f393987ed56920305d8f3c647e6ed35407c1ff2ecb88a9e1151" + "sha256:3f9b2c0196c60d21838f307f5825a7b86b678cedc58ab9e50a8988187b4d81e0", + "sha256:dd33eb70232b6118298d516bbcecd26704689c386594f0f3c4f13867b2c56f72" ], "index": "pypi", - "version": "==4.0.1" + "version": "==4.0.2" }, "more-itertools": { "hashes": [ @@ -612,10 +596,10 @@ }, "packaging": { "hashes": [ - "sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73", - "sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334" + "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", + "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752" ], - "version": "==20.1" + "version": "==20.3" }, "pluggy": { "hashes": [ @@ -626,11 +610,11 @@ }, "pre-commit": { "hashes": [ - "sha256:09ebe467f43ce24377f8c2f200fe3cd2570d328eb2ce0568c8e96ce19da45fa6", - "sha256:f8d555e31e2051892c7f7b3ad9f620bd2c09271d87e9eedb2ad831737d6211eb" + "sha256:487c675916e6f99d355ec5595ad77b325689d423ef4839db1ed2f02f639c9522", + "sha256:c0aa11bce04a7b46c5544723aedf4e81a4d5f64ad1205a30a9ea12d5e81969e1" ], "index": "pypi", - "version": "==2.1.1" + "version": "==2.2.0" }, "protobuf": { "hashes": [ @@ -685,27 +669,27 @@ }, "pytest": { "hashes": [ - "sha256:0d5fe9189a148acc3c3eb2ac8e1ac0742cb7618c084f3d228baaec0c254b318d", - "sha256:ff615c761e25eb25df19edddc0b970302d2a9091fbce0e7213298d85fb61fef6" + "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172", + "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970" ], "index": "pypi", - "version": "==5.3.5" + "version": "==5.4.1" }, "pyyaml": { "hashes": [ - "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6", - "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf", - "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5", - "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e", - "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811", - "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e", - "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d", - "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20", - "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689", - "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994", - "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615" + "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", + "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", + "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", + "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", + "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", + "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", + "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", + "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", + "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" ], - "version": "==5.3" + "version": "==5.3.1" }, "six": { "hashes": [ @@ -723,24 +707,25 @@ }, "virtualenv": { "hashes": [ - "sha256:30ea90b21dabd11da5f509710ad3be2ae47d40ccbc717dfdd2efe4367c10f598", - "sha256:4a36a96d785428278edd389d9c36d763c5755844beb7509279194647b1ef47f1" + "sha256:4e399f48c6b71228bf79f5febd27e3bbb753d9d5905776a86667bc61ab628a25", + "sha256:9e81279f4a9d16d1c0654a127c2c86e5bca2073585341691882c1e66e31ef8a5" ], - "version": "==20.0.7" + "version": "==20.0.15" }, "wcwidth": { "hashes": [ - "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603", - "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8" + "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", + "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" ], - "version": "==0.1.8" + "version": "==0.1.9" }, "zipp": { "hashes": [ - "sha256:12248a63bbdf7548f89cb4c7cda4681e537031eda29c02ea29674bc6854460c2", - "sha256:7c0f8e91abc0dc07a5068f315c52cb30c66bfbc581e5b50704c8a2f6ebae794a" + "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", + "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" ], - "version": "==3.0.0" + "markers": "python_version < '3.8'", + "version": "==3.1.0" } } } diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index 0bab096b..c4317e2e 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -23,14 +23,12 @@ def handle_node_event(event: NodeData) -> core_pb2.NodeEvent: :return: node event that contains node id, name, model, position, and services """ position = core_pb2.Position(x=event.x_position, y=event.y_position) - services = event.services or "" - services = services.split("|") node_proto = core_pb2.Node( id=event.id, name=event.name, model=event.model, position=position, - services=services, + services=event.services, ) return core_pb2.NodeEvent(node=node_proto, source=event.source) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 117b102e..8b6c23a5 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -300,7 +300,7 @@ class CoreHandler(socketserver.BaseRequestHandler): coreapi.CoreExceptionTlv, [ (ExceptionTlvs.NODE, exception_data.node), - (ExceptionTlvs.SESSION, exception_data.session), + (ExceptionTlvs.SESSION, str(exception_data.session)), (ExceptionTlvs.LEVEL, exception_data.level.value), (ExceptionTlvs.SOURCE, exception_data.source), (ExceptionTlvs.DATE, exception_data.date), @@ -639,7 +639,7 @@ class CoreHandler(socketserver.BaseRequestHandler): :return: """ exception_data = ExceptionData( - session=str(self.session.id), + session=self.session.id, node=node, date=time.ctime(), level=level.value, @@ -1891,13 +1891,13 @@ class CoreHandler(socketserver.BaseRequestHandler): node = self.session.get_node(node_id) values = ServiceShim.tovaluelist(node, service) config_data = ConfigData( - message_type=0, + message_type=MessageFlags.NONE, node=node_id, object=self.session.services.name, type=ConfigFlags.UPDATE.value, data_types=data_types, data_values=values, - session=str(self.session.id), + session=self.session.id, opaque=opaque, ) self.session.broadcast_config(config_data) diff --git a/daemon/core/api/tlv/dataconversion.py b/daemon/core/api/tlv/dataconversion.py index 21730afb..2689a1e9 100644 --- a/daemon/core/api/tlv/dataconversion.py +++ b/daemon/core/api/tlv/dataconversion.py @@ -18,6 +18,9 @@ def convert_node(node_data): :param core.emulator.data.NodeData node_data: node data to convert :return: packed node message """ + services = None + if node_data.services is not None: + services = "|".join([x for x in node_data.services]) tlv_data = structutils.pack_values( coreapi.CoreNodeTlv, [ @@ -30,12 +33,12 @@ def convert_node(node_data): (NodeTlvs.MODEL, node_data.model), (NodeTlvs.EMULATION_ID, node_data.emulation_id), (NodeTlvs.EMULATION_SERVER, node_data.server), - (NodeTlvs.SESSION, node_data.session), + (NodeTlvs.SESSION, str(node_data.session)), (NodeTlvs.X_POSITION, int(node_data.x_position)), (NodeTlvs.Y_POSITION, int(node_data.y_position)), (NodeTlvs.CANVAS, node_data.canvas), (NodeTlvs.NETWORK_ID, node_data.network_id), - (NodeTlvs.SERVICES, node_data.services), + (NodeTlvs.SERVICES, services), (NodeTlvs.LATITUDE, str(node_data.latitude)), (NodeTlvs.LONGITUDE, str(node_data.longitude)), (NodeTlvs.ALTITUDE, str(node_data.altitude)), @@ -65,7 +68,7 @@ def convert_config(config_data): (ConfigTlvs.BITMAP, config_data.bitmap), (ConfigTlvs.POSSIBLE_VALUES, config_data.possible_values), (ConfigTlvs.GROUPS, config_data.groups), - (ConfigTlvs.SESSION, config_data.session), + (ConfigTlvs.SESSION, str(config_data.session)), (ConfigTlvs.INTERFACE_NUMBER, config_data.interface_number), (ConfigTlvs.NETWORK_ID, config_data.network_id), (ConfigTlvs.OPAQUE, config_data.opaque), diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index 0ed1fa67..26082f94 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -2,121 +2,129 @@ CORE data objects. """ -import collections +from dataclasses import dataclass +from typing import List, Tuple -ConfigData = collections.namedtuple( - "ConfigData", - [ - "message_type", - "node", - "object", - "type", - "data_types", - "data_values", - "captions", - "bitmap", - "possible_values", - "groups", - "session", - "interface_number", - "network_id", - "opaque", - ], +from core.emulator.enumerations import ( + EventTypes, + ExceptionLevels, + LinkTypes, + MessageFlags, + NodeTypes, ) -ConfigData.__new__.__defaults__ = (None,) * len(ConfigData._fields) -EventData = collections.namedtuple( - "EventData", ["node", "event_type", "name", "data", "time", "session"] -) -EventData.__new__.__defaults__ = (None,) * len(EventData._fields) -ExceptionData = collections.namedtuple( - "ExceptionData", ["node", "session", "level", "source", "date", "text", "opaque"] -) -ExceptionData.__new__.__defaults__ = (None,) * len(ExceptionData._fields) +@dataclass +class ConfigData: + message_type: MessageFlags = None + node: int = None + object: str = None + type: int = None + data_types: Tuple[int] = None + data_values: str = None + captions: str = None + bitmap: str = None + possible_values: str = None + groups: str = None + session: int = None + interface_number: int = None + network_id: int = None + opaque: str = None -FileData = collections.namedtuple( - "FileData", - [ - "message_type", - "node", - "name", - "mode", - "number", - "type", - "source", - "session", - "data", - "compressed_data", - ], -) -FileData.__new__.__defaults__ = (None,) * len(FileData._fields) -NodeData = collections.namedtuple( - "NodeData", - [ - "message_type", - "id", - "node_type", - "name", - "ip_address", - "mac_address", - "ip6_address", - "model", - "emulation_id", - "server", - "session", - "x_position", - "y_position", - "canvas", - "network_id", - "services", - "latitude", - "longitude", - "altitude", - "icon", - "opaque", - "source", - ], -) -NodeData.__new__.__defaults__ = (None,) * len(NodeData._fields) +@dataclass +class EventData: + node: int = None + event_type: EventTypes = None + name: str = None + data: str = None + time: float = None + session: int = None -LinkData = collections.namedtuple( - "LinkData", - [ - "message_type", - "node1_id", - "node2_id", - "delay", - "bandwidth", - "per", - "dup", - "jitter", - "mer", - "burst", - "session", - "mburst", - "link_type", - "gui_attributes", - "unidirectional", - "emulation_id", - "network_id", - "key", - "interface1_id", - "interface1_name", - "interface1_ip4", - "interface1_ip4_mask", - "interface1_mac", - "interface1_ip6", - "interface1_ip6_mask", - "interface2_id", - "interface2_name", - "interface2_ip4", - "interface2_ip4_mask", - "interface2_mac", - "interface2_ip6", - "interface2_ip6_mask", - "opaque", - ], -) -LinkData.__new__.__defaults__ = (None,) * len(LinkData._fields) + +@dataclass +class ExceptionData: + node: int = None + session: int = None + level: ExceptionLevels = None + source: str = None + date: str = None + text: str = None + opaque: str = None + + +@dataclass +class FileData: + message_type: MessageFlags = None + node: int = None + name: str = None + mode: str = None + number: int = None + type: str = None + source: str = None + session: int = None + data: str = None + compressed_data: str = None + + +@dataclass +class NodeData: + message_type: MessageFlags = None + id: int = None + node_type: NodeTypes = None + name: str = None + ip_address: str = None + mac_address: str = None + ip6_address: str = None + model: str = None + emulation_id: int = None + server: str = None + session: int = None + x_position: float = None + y_position: float = None + canvas: int = None + network_id: int = None + services: List[str] = None + latitude: float = None + longitude: float = None + altitude: float = None + icon: str = None + opaque: str = None + source: str = None + + +@dataclass +class LinkData: + message_type: MessageFlags = None + node1_id: int = None + node2_id: int = None + delay: float = None + bandwidth: float = None + per: float = None + dup: float = None + jitter: float = None + mer: float = None + burst: float = None + session: int = None + mburst: float = None + link_type: LinkTypes = None + gui_attributes: str = None + unidirectional: int = None + emulation_id: int = None + network_id: int = None + key: int = None + interface1_id: int = None + interface1_name: str = None + interface1_ip4: str = None + interface1_ip4_mask: int = None + interface1_mac: str = None + interface1_ip6: str = None + interface1_ip6_mask: int = None + interface2_id: int = None + interface2_name: str = None + interface2_ip4: str = None + interface2_ip4_mask: int = None + interface2_mac: str = None + interface2_ip6: str = None + interface2_ip6_mask: int = None + opaque: str = None diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index ac907911..bd59574f 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -1464,7 +1464,7 @@ class Session: """ exception_data = ExceptionData( node=node_id, - session=str(self.id), + session=self.id, level=level, source=source, date=time.ctime(), diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 2df3fd74..098924db 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -193,7 +193,7 @@ class NodeBase: def data( self, message_type: MessageFlags = MessageFlags.NONE, source: str = None - ) -> NodeData: + ) -> Optional[NodeData]: """ Build a data object for this node. @@ -209,9 +209,9 @@ class NodeBase: server = None if self.server is not None: server = self.server.name - services = self.services - if services is not None: - services = "|".join([service.name for service in services]) + services = None + if self.services is not None: + services = [service.name for service in self.services] return NodeData( message_type=message_type, id=self.id, @@ -1131,7 +1131,7 @@ class CoreNetworkBase(NodeBase): netif.swapparams("_params_up") link_data = LinkData( - message_type=0, + message_type=MessageFlags.NONE, node1_id=linked_node.id, node2_id=self.id, link_type=self.linktype, diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index ff6e6ceb..eb179e84 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -880,7 +880,7 @@ class PtpNet(CoreNetwork): def data( self, message_type: MessageFlags = MessageFlags.NONE, source: str = None - ) -> NodeData: + ) -> Optional[NodeData]: """ Do not generate a Node Message for point-to-point links. They are built using a link message instead. diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index 6323ceab..d300ba66 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -637,7 +637,9 @@ class CoreServices: status = -1 return status - def get_service_file(self, node: CoreNode, service_name: str, filename: str) -> str: + def get_service_file( + self, node: CoreNode, service_name: str, filename: str + ) -> FileData: """ Send a File Message when the GUI has requested a service file. The file data is either auto-generated or comes from an existing config. @@ -645,7 +647,7 @@ class CoreServices: :param node: node to get service file from :param service_name: service to get file from :param filename: file name to retrieve - :return: file message for node + :return: file data """ # get service to get file from service = self.get_service(node.id, service_name, default_service=True) diff --git a/daemon/requirements.txt b/daemon/requirements.txt index 2118b15e..b1936855 100644 --- a/daemon/requirements.txt +++ b/daemon/requirements.txt @@ -1,6 +1,7 @@ bcrypt==3.1.7 cffi==1.14.0 cryptography==2.8 +dataclasses==0.7 fabric==2.5.0 grpcio==1.27.2 invoke==1.4.1 diff --git a/daemon/setup.py.in b/daemon/setup.py.in index c4d2ae56..5e4e39c7 100644 --- a/daemon/setup.py.in +++ b/daemon/setup.py.in @@ -34,12 +34,13 @@ setup( version="@PACKAGE_VERSION@", packages=find_packages(), install_requires=[ + "dataclasses", "fabric", "grpcio", - "netaddr", "invoke", "lxml", "mako", + "netaddr", "pillow", "protobuf", "pyproj", From 13ef701b6eb1586475a8da919ae086f764265faf Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 31 Mar 2020 10:08:08 -0700 Subject: [PATCH 0113/1131] ignore reading emane global config from xml when not present --- daemon/core/xml/corexml.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 87bf2e76..9951a994 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -731,6 +731,8 @@ class CoreXmlReader: def read_emane_global_config(self) -> None: emane_global_configuration = self.scenario.find("emane_global_configuration") + if emane_global_configuration is None: + return emulator_configuration = emane_global_configuration.find("emulator") configs = {} for config in emulator_configuration.iterchildren(): From 6e8f980cc9b658f12b64bd0c249ae0b5f36cc4bc Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 31 Mar 2020 14:06:13 -0700 Subject: [PATCH 0114/1131] moved github action to 3.6, since that is the current min requirement --- .github/workflows/daemon-checks.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/daemon-checks.yml b/.github/workflows/daemon-checks.yml index ca2de7d8..85409568 100644 --- a/.github/workflows/daemon-checks.yml +++ b/.github/workflows/daemon-checks.yml @@ -7,10 +7,10 @@ jobs: runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v1 - - name: Set up Python 3.7 + - name: Set up Python 3.6 uses: actions/setup-python@v1 with: - python-version: 3.7 + python-version: 3.6 - name: Install pipenv run: | python -m pip install --upgrade pip From 2532f6605d6ef140cf431a4a61274aae76a07c31 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 31 Mar 2020 14:19:02 -0700 Subject: [PATCH 0115/1131] modify setup.py to denote dataclasses only being needed in 3.6 --- daemon/setup.py.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/setup.py.in b/daemon/setup.py.in index 5e4e39c7..e8c99e67 100644 --- a/daemon/setup.py.in +++ b/daemon/setup.py.in @@ -34,7 +34,7 @@ setup( version="@PACKAGE_VERSION@", packages=find_packages(), install_requires=[ - "dataclasses", + 'dataclasses;python_version=="3.6"', "fabric", "grpcio", "invoke", From 2ce1ef04ae866179db18603bcd612576eeb49726 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 31 Mar 2020 14:28:17 -0700 Subject: [PATCH 0116/1131] updated Pipfile.lock to denote marker for only installing dataclasses in python 3.6 --- daemon/Pipfile.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon/Pipfile.lock b/daemon/Pipfile.lock index e4a4b55b..2fb5c3b8 100644 --- a/daemon/Pipfile.lock +++ b/daemon/Pipfile.lock @@ -106,6 +106,7 @@ "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6" ], "index": "pypi", + "markers": "python_version == '3.6'", "version": "==0.7" }, "fabric": { From 1252f72220aec37b52f72acedf070f7c79839577 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 31 Mar 2020 14:38:48 -0700 Subject: [PATCH 0117/1131] updated requirements.txt to use environment markers for dataclasses dependency --- daemon/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/requirements.txt b/daemon/requirements.txt index b1936855..19d155e5 100644 --- a/daemon/requirements.txt +++ b/daemon/requirements.txt @@ -1,7 +1,7 @@ bcrypt==3.1.7 cffi==1.14.0 cryptography==2.8 -dataclasses==0.7 +dataclasses==0.7; python_version == "3.6" fabric==2.5.0 grpcio==1.27.2 invoke==1.4.1 From 71196004c8484d0063d053db0fe606e5e8f49926 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 31 Mar 2020 15:20:34 -0700 Subject: [PATCH 0118/1131] improved sdt deletion of links by using the id properly --- daemon/core/emulator/session.py | 2 +- daemon/core/plugins/sdt.py | 44 ++++++++++++++++++--------------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index bd59574f..7d597b29 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -430,7 +430,7 @@ class Session: if node_two: node_two.lock.release() - self.sdt.add_link(node_one_id, node_two_id, is_wireless=False) + self.sdt.add_link(node_one_id, node_two_id) return node_one_interface, node_two_interface def delete_link( diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 0c23f567..edfde932 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -5,7 +5,7 @@ sdt.py: Scripted Display Tool (SDT3D) helper import logging import socket import threading -from typing import TYPE_CHECKING, Optional, Tuple +from typing import TYPE_CHECKING, Optional from urllib.parse import urlparse from core import constants @@ -21,12 +21,11 @@ if TYPE_CHECKING: from core.emulator.session import Session -def link_data_params(link_data: LinkData) -> Tuple[int, int, bool, int]: - node_one = link_data.node1_id - node_two = link_data.node2_id - is_wireless = link_data.link_type == LinkTypes.WIRELESS - network_id = link_data.network_id - return node_one, node_two, is_wireless, network_id +def get_link_id(node_one: int, node_two: int, network_id: int) -> str: + link_id = f"{node_one}-{node_two}" + if network_id is not None: + link_id = f"{link_id}-{network_id}" + return link_id CORE_LAYER = "CORE" @@ -226,8 +225,7 @@ class Sdt: is_wireless = isinstance(net, (WlanNode, EmaneNet)) if is_wireless and link_data.node1_id == net.id: continue - params = link_data_params(link_data) - self.add_link(*params) + self.handle_link_update(link_data) def get_node_position(self, node: NodeBase) -> Optional[str]: """ @@ -348,15 +346,19 @@ class Sdt: return result def add_link( - self, node_one: int, node_two: int, is_wireless: bool, network_id: int = None + self, + node_one: int, + node_two: int, + network_id: int = None, + is_wireless: bool = False, ) -> None: """ Handle adding a link in SDT. :param node_one: node one id :param node_two: node two id + :param network_id: network link is associated with, None otherwise :param is_wireless: True if link is wireless, False otherwise - :param network_id: network link is associated with :return: nothing """ logging.debug("sdt add link: %s, %s, %s", node_one, node_two, is_wireless) @@ -368,9 +370,7 @@ class Sdt: attr = "green,2" else: attr = "red,2" - link_id = f"{node_one}-{node_two}" - if network_id is not None: - link_id = f"{link_id}-{network_id}" + link_id = get_link_id(node_one, node_two, network_id) layer = LINK_LAYER if network_id: node = self.session.nodes[network_id] @@ -378,12 +378,13 @@ class Sdt: layer = f"{layer}::{network_name}" self.cmd(f"link {node_one},{node_two},{link_id} linkLayer {layer} line {attr}") - def delete_link(self, node_one: int, node_two: int) -> None: + def delete_link(self, node_one: int, node_two: int, network_id: int = None) -> None: """ Handle deleting a node in SDT. :param node_one: node one id :param node_two: node two id + :param network_id: network link is associated with, None otherwise :return: nothing """ logging.debug("sdt delete link: %s, %s", node_one, node_two) @@ -391,7 +392,8 @@ class Sdt: return if self.wireless_net_check(node_one) or self.wireless_net_check(node_two): return - self.cmd(f"delete link,{node_one},{node_two}") + link_id = get_link_id(node_one, node_two, network_id) + self.cmd(f"delete link,{node_one},{node_two},{link_id}") def handle_link_update(self, link_data: LinkData) -> None: """ @@ -400,9 +402,11 @@ class Sdt: :param link_data: link data to handle :return: nothing """ + node_one = link_data.node1_id + node_two = link_data.node2_id + network_id = link_data.network_id + is_wireless = link_data.link_type == LinkTypes.WIRELESS if link_data.message_type == MessageFlags.ADD: - params = link_data_params(link_data) - self.add_link(*params) + self.add_link(node_one, node_two, network_id, is_wireless) elif link_data.message_type == MessageFlags.DELETE: - params = link_data_params(link_data) - self.delete_link(*params[:2]) + self.delete_link(node_one, node_two, network_id) From 3165bddc92d239053bfc05e4059d79d622d59975 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 31 Mar 2020 16:21:04 -0700 Subject: [PATCH 0119/1131] updates to allow emane to throw an exception when emane python bindings are not present and emane is attempted to be ran --- daemon/core/api/tlv/corehandlers.py | 7 ++++--- daemon/core/emane/emanemanager.py | 7 +++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 8b6c23a5..a16c6701 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -33,6 +33,7 @@ from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.enumerations import ( ConfigDataTypes, EventTypes, + ExceptionLevels, LinkTypes, MessageFlags, NodeTypes, @@ -532,12 +533,12 @@ class CoreHandler(socketserver.BaseRequestHandler): return message_handler = self.message_handlers[message.message_type] - try: # TODO: this needs to be removed, make use of the broadcast message methods replies = message_handler(message) self.dispatch_replies(replies, message) - except Exception: + except Exception as e: + self.send_exception(ExceptionLevels.ERROR, "corehandler", str(e)) logging.exception( "%s: exception while handling message: %s", threading.currentThread().getName(), @@ -642,7 +643,7 @@ class CoreHandler(socketserver.BaseRequestHandler): session=self.session.id, node=node, date=time.ctime(), - level=level.value, + level=level, source=source, text=text, ) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 756cad2a..0b4ae891 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -39,6 +39,9 @@ except ImportError: from emanesh.events import LocationEvent from emanesh.events.eventserviceexception import EventServiceException except ImportError: + EventService = None + LocationEvent = None + EventServiceException = None logging.debug("compatible emane python bindings not installed") EMANE_MODELS = [ @@ -279,6 +282,10 @@ class EmaneManager(ModelManager): logging.debug("no emane nodes in session") return EmaneManager.NOT_NEEDED + # check if bindings were installed + if EventService is None: + raise CoreError("EMANE python bindings are not installed") + # control network bridge required for EMANE 0.9.2 # - needs to exist when eventservice binds to it (initeventservice) otadev = self.get_config("otamanagerdevice") From 091131fe5c481ca9931bd43a395a07f02fa70b54 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 31 Mar 2020 16:39:27 -0700 Subject: [PATCH 0120/1131] tweak to session.exception to default node_id to None when not provided --- daemon/core/api/tlv/corehandlers.py | 2 +- daemon/core/emulator/session.py | 6 +++--- daemon/core/services/coreservices.py | 2 +- daemon/tests/test_grpc.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index a16c6701..4a5e02db 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -637,7 +637,7 @@ class CoreHandler(socketserver.BaseRequestHandler): :param str source: source where exception came from :param str text: details about exception :param int node: node id, if related to a specific node - :return: + :return: nothing """ exception_data = ExceptionData( session=self.session.id, diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 7d597b29..1124ee71 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -1218,7 +1218,7 @@ class Session: ) logging.exception(message) self.exception( - ExceptionLevels.ERROR, "Session.run_state_hooks", None, message + ExceptionLevels.ERROR, "Session.run_state_hooks", message ) def add_state_hook( @@ -1451,15 +1451,15 @@ class Session: ) def exception( - self, level: ExceptionLevels, source: str, node_id: int, text: str + self, level: ExceptionLevels, source: str, text: str, node_id: int = None ) -> None: """ Generate and broadcast an exception event. :param level: exception level :param source: source name - :param node_id: node related to exception :param text: exception message + :param node_id: node related to exception :return: nothing """ exception_data = ExceptionData( diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index d300ba66..827982d2 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -630,8 +630,8 @@ class CoreServices: self.session.exception( ExceptionLevels.ERROR, "services", - node.id, f"error stopping service {service.name}: {e.stderr}", + node.id, ) logging.exception("error running stop command %s", args) status = -1 diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index b3bd9a27..5d8bfa1d 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -1117,7 +1117,7 @@ class TestGrpc: with client.context_connect(): client.events(session.id, handle_event) time.sleep(0.1) - session.exception(exception_level, source, node_id, text) + session.exception(exception_level, source, text, node_id) # then queue.get(timeout=5) From b29f64054716de4af120c53ce515a75aef0a8a57 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 31 Mar 2020 21:09:20 -0700 Subject: [PATCH 0121/1131] modified emane link monitor to send labels containing sinr values to sdt --- daemon/core/emane/linkmonitor.py | 31 +++++++++++++++++++---- daemon/core/emulator/data.py | 1 + daemon/core/plugins/sdt.py | 42 ++++++++++++++++++++++++++++---- 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py index 6d4daa8d..e161ada2 100644 --- a/daemon/core/emane/linkmonitor.py +++ b/daemon/core/emane/linkmonitor.py @@ -19,7 +19,6 @@ except ImportError: except ImportError: logging.debug("compatible emane python bindings not installed") - if TYPE_CHECKING: from core.emane.emanemanager import EmaneManager @@ -59,9 +58,11 @@ class EmaneLink: self.to_nem = to_nem self.sinr = sinr self.last_seen = None + self.updated = False self.touch() def update(self, sinr: float) -> None: + self.updated = self.sinr != sinr self.sinr = sinr self.touch() @@ -234,19 +235,23 @@ class EmaneLinkMonitor: current_links = set(self.links.keys()) new_links = current_links - previous_links - # find dead links + # find updated and dead links dead_links = [] for link_id, link in self.links.items(): + complete_id = self.get_complete_id(link_id) if link.is_dead(self.link_timeout): dead_links.append(link_id) + elif link.updated and complete_id in self.complete_links: + link.updated = False + self.send_link(MessageFlags.NONE, complete_id) # announce dead links for link_id in dead_links: - del self.links[link_id] complete_id = self.get_complete_id(link_id) if complete_id in self.complete_links: self.complete_links.remove(complete_id) self.send_link(MessageFlags.DELETE, complete_id) + del self.links[link_id] # announce new links for link_id in new_links: @@ -271,6 +276,13 @@ class EmaneLinkMonitor: reverse_id = link_id[1], link_id[0] return link_id in self.links and reverse_id in self.links + def get_link_label(self, link_id: Tuple[int, int]) -> str: + source_id = tuple(sorted(link_id)) + source_link = self.links[source_id] + dest_id = link_id[::-1] + dest_link = self.links[dest_id] + return f"{source_link.sinr:.1f} / {dest_link.sinr:.1f}" + def send_link(self, message_type: MessageFlags, link_id: Tuple[int, int]) -> None: nem_one, nem_two = link_id emane_one, netif = self.emane_manager.nemlookup(nem_one) @@ -291,11 +303,20 @@ class EmaneLinkMonitor: node_two.name, nem_two, ) - self.send_message(message_type, node_one.id, node_two.id, emane_one.id) + label = self.get_link_label(link_id) + self.send_message(message_type, label, node_one.id, node_two.id, emane_one.id) - def send_message(self, message_type, node_one, node_two, emane_id) -> None: + def send_message( + self, + message_type: MessageFlags, + label: str, + node_one: int, + node_two: int, + emane_id: int, + ) -> None: link_data = LinkData( message_type=message_type, + label=label, node1_id=node_one, node2_id=node_two, network_id=emane_id, diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index 26082f94..a7d4c9df 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -96,6 +96,7 @@ class NodeData: @dataclass class LinkData: message_type: MessageFlags = None + label: str = None node1_id: int = None node2_id: int = None delay: float = None diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index edfde932..befbf340 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -350,6 +350,7 @@ class Sdt: node_one: int, node_two: int, network_id: int = None, + label: str = None, is_wireless: bool = False, ) -> None: """ @@ -358,10 +359,11 @@ class Sdt: :param node_one: node one id :param node_two: node two id :param network_id: network link is associated with, None otherwise + :param label: label for link :param is_wireless: True if link is wireless, False otherwise :return: nothing """ - logging.debug("sdt add link: %s, %s, %s", node_one, node_two, is_wireless) + logging.debug("sdt add link: %s, %s, %s", node_one, node_two, network_id) if not self.connect(): return if self.wireless_net_check(node_one) or self.wireless_net_check(node_two): @@ -376,18 +378,24 @@ class Sdt: node = self.session.nodes[network_id] network_name = node.name layer = f"{layer}::{network_name}" - self.cmd(f"link {node_one},{node_two},{link_id} linkLayer {layer} line {attr}") + link_label = "" + if label: + link_label = f'linklabel on,"{label}"' + self.cmd( + f"link {node_one},{node_two},{link_id} linkLayer {layer} line {attr} " + f"{link_label}" + ) def delete_link(self, node_one: int, node_two: int, network_id: int = None) -> None: """ - Handle deleting a node in SDT. + Handle deleting a link in SDT. :param node_one: node one id :param node_two: node two id :param network_id: network link is associated with, None otherwise :return: nothing """ - logging.debug("sdt delete link: %s, %s", node_one, node_two) + logging.debug("sdt delete link: %s, %s, %s", node_one, node_two, network_id) if not self.connect(): return if self.wireless_net_check(node_one) or self.wireless_net_check(node_two): @@ -395,6 +403,27 @@ class Sdt: link_id = get_link_id(node_one, node_two, network_id) self.cmd(f"delete link,{node_one},{node_two},{link_id}") + def edit_link( + self, node_one: int, node_two: int, network_id: int, label: str + ) -> None: + """ + Handle editing a link in SDT. + + :param node_one: node one id + :param node_two: node two id + :param network_id: network link is associated with, None otherwise + :param label: label to update + :return: nothing + """ + logging.debug("sdt edit link: %s, %s, %s", node_one, node_two, network_id) + if not self.connect(): + return + if self.wireless_net_check(node_one) or self.wireless_net_check(node_two): + return + link_id = get_link_id(node_one, node_two, network_id) + link_label = f'linklabel on,"{label}"' + self.cmd(f"link {node_one},{node_two},{link_id} {link_label}") + def handle_link_update(self, link_data: LinkData) -> None: """ Handle link broadcast messages and push changes to SDT. @@ -406,7 +435,10 @@ class Sdt: node_two = link_data.node2_id network_id = link_data.network_id is_wireless = link_data.link_type == LinkTypes.WIRELESS + label = link_data.label if link_data.message_type == MessageFlags.ADD: - self.add_link(node_one, node_two, network_id, is_wireless) + self.add_link(node_one, node_two, network_id, label, is_wireless) elif link_data.message_type == MessageFlags.DELETE: self.delete_link(node_one, node_two, network_id) + elif link_data.message_type == MessageFlags.NONE and label: + self.edit_link(node_one, node_two, network_id, label) From 72189a5c28fe86b9978c29bdf684d28153b49506 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 1 Apr 2020 10:56:09 -0700 Subject: [PATCH 0122/1131] fix configdata issue, since most corehandler code did not account for using flags directly --- daemon/core/api/tlv/corehandlers.py | 2 +- daemon/core/api/tlv/dataconversion.py | 10 ++++++++-- daemon/core/emulator/data.py | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 4a5e02db..95ee5e9c 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -1892,7 +1892,7 @@ class CoreHandler(socketserver.BaseRequestHandler): node = self.session.get_node(node_id) values = ServiceShim.tovaluelist(node, service) config_data = ConfigData( - message_type=MessageFlags.NONE, + message_type=0, node=node_id, object=self.session.services.name, type=ConfigFlags.UPDATE.value, diff --git a/daemon/core/api/tlv/dataconversion.py b/daemon/core/api/tlv/dataconversion.py index 2689a1e9..876e72a5 100644 --- a/daemon/core/api/tlv/dataconversion.py +++ b/daemon/core/api/tlv/dataconversion.py @@ -18,6 +18,9 @@ def convert_node(node_data): :param core.emulator.data.NodeData node_data: node data to convert :return: packed node message """ + session = None + if node_data.session is not None: + session = str(node_data.session) services = None if node_data.services is not None: services = "|".join([x for x in node_data.services]) @@ -33,7 +36,7 @@ def convert_node(node_data): (NodeTlvs.MODEL, node_data.model), (NodeTlvs.EMULATION_ID, node_data.emulation_id), (NodeTlvs.EMULATION_SERVER, node_data.server), - (NodeTlvs.SESSION, str(node_data.session)), + (NodeTlvs.SESSION, session), (NodeTlvs.X_POSITION, int(node_data.x_position)), (NodeTlvs.Y_POSITION, int(node_data.y_position)), (NodeTlvs.CANVAS, node_data.canvas), @@ -56,6 +59,9 @@ def convert_config(config_data): :param core.emulator.data.ConfigData config_data: config data to convert :return: packed message """ + session = None + if config_data.session is not None: + session = str(config_data.session) tlv_data = structutils.pack_values( coreapi.CoreConfigTlv, [ @@ -68,7 +74,7 @@ def convert_config(config_data): (ConfigTlvs.BITMAP, config_data.bitmap), (ConfigTlvs.POSSIBLE_VALUES, config_data.possible_values), (ConfigTlvs.GROUPS, config_data.groups), - (ConfigTlvs.SESSION, str(config_data.session)), + (ConfigTlvs.SESSION, session), (ConfigTlvs.INTERFACE_NUMBER, config_data.interface_number), (ConfigTlvs.NETWORK_ID, config_data.network_id), (ConfigTlvs.OPAQUE, config_data.opaque), diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index a7d4c9df..c7141541 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -16,7 +16,7 @@ from core.emulator.enumerations import ( @dataclass class ConfigData: - message_type: MessageFlags = None + message_type: int = None node: int = None object: str = None type: int = None From 7d392c43ac8782083d7a496dd5e3c5d3fc682d50 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 2 Apr 2020 15:12:07 -0700 Subject: [PATCH 0123/1131] improve default route service to detect connected routers and use the addresses of the first one found --- .../configservices/utilservices/services.py | 25 +++++++------ .../utilservices/templates/defaultroute.sh | 4 +-- daemon/core/services/utility.py | 35 +++++++++---------- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/daemon/core/configservices/utilservices/services.py b/daemon/core/configservices/utilservices/services.py index 671f12f1..75b5c745 100644 --- a/daemon/core/configservices/utilservices/services.py +++ b/daemon/core/configservices/utilservices/services.py @@ -1,10 +1,10 @@ -import logging from typing import Any, Dict import netaddr from core import utils from core.configservice.base import ConfigService, ConfigServiceMode +from core.nodes.base import CoreNode GROUP_NAME = "Utility" @@ -24,16 +24,21 @@ class DefaultRouteService(ConfigService): modes = {} def data(self) -> Dict[str, Any]: - addresses = [] - for netif in self.node.netifs(): - if getattr(netif, "control", False): + # only add default routes for linked routing nodes + routes = [] + for other_node in self.node.session.nodes.values(): + if not isinstance(other_node, CoreNode): continue - for addr in netif.addrlist: - logging.info("default route address: %s", addr) - net = netaddr.IPNetwork(addr) - if net[1] != net[-2]: - addresses.append(net[1]) - return dict(addresses=addresses) + if other_node.type not in ["router", "mdr"]: + continue + commonnets = self.node.commonnets(other_node) + if commonnets: + _, _, router_eth = commonnets[0] + for x in router_eth.addrlist: + addr, prefix = x.split("/") + routes.append(addr) + break + return dict(routes=routes) class DefaultMulticastRouteService(ConfigService): diff --git a/daemon/core/configservices/utilservices/templates/defaultroute.sh b/daemon/core/configservices/utilservices/templates/defaultroute.sh index 879a8861..d5cdfd78 100644 --- a/daemon/core/configservices/utilservices/templates/defaultroute.sh +++ b/daemon/core/configservices/utilservices/templates/defaultroute.sh @@ -1,5 +1,5 @@ #!/bin/sh # auto-generated by DefaultRoute service -% for address in addresses: -ip route add default via ${address} +% for route in routes: +ip route add default via ${route} % endfor diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index 0ea05fe8..028c2c0b 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -1,13 +1,13 @@ """ utility.py: defines miscellaneous utility services. """ - import os import netaddr from core import constants, utils from core.errors import CoreCommandError +from core.nodes.base import CoreNode from core.services.coreservices import CoreService, ServiceMode @@ -77,27 +77,26 @@ class DefaultRouteService(UtilService): @classmethod def generate_config(cls, node, filename): + # only add default routes for linked routing nodes + routes = [] + for other_node in node.session.nodes.values(): + if not isinstance(other_node, CoreNode): + continue + if other_node.type not in ["router", "mdr"]: + continue + commonnets = node.commonnets(other_node) + if commonnets: + _, _, router_eth = commonnets[0] + for x in router_eth.addrlist: + addr, prefix = x.split("/") + routes.append(addr) + break cfg = "#!/bin/sh\n" cfg += "# auto-generated by DefaultRoute service (utility.py)\n" - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += "\n".join(map(cls.addrstr, ifc.addrlist)) - cfg += "\n" + for route in routes: + cfg += f"ip route add default via {route}\n" return cfg - @staticmethod - def addrstr(x): - net = netaddr.IPNetwork(x) - if net[1] == net[-2]: - return "" - else: - if os.uname()[0] == "Linux": - rtcmd = "ip route add default via" - else: - raise Exception("unknown platform") - return "%s %s" % (rtcmd, net[1]) - class DefaultMulticastRouteService(UtilService): name = "DefaultMulticastRoute" From 7be7beec4269ce53b6d695a3a1a2c14786c79793 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 2 Apr 2020 17:33:38 -0700 Subject: [PATCH 0124/1131] updated core-daemon thread usage to use thread daemon param --- daemon/scripts/core-daemon | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/daemon/scripts/core-daemon b/daemon/scripts/core-daemon index 866c5472..9f738467 100755 --- a/daemon/scripts/core-daemon +++ b/daemon/scripts/core-daemon @@ -41,8 +41,7 @@ def start_udp(mainserver, server_address): :return: CoreUdpServer """ mainserver.udpserver = CoreUdpServer(server_address, CoreUdpHandler, mainserver) - mainserver.udpthread = threading.Thread(target=mainserver.udpserver.start) - mainserver.udpthread.daemon = True + mainserver.udpthread = threading.Thread(target=mainserver.udpserver.start, daemon=True) mainserver.udpthread.start() @@ -70,8 +69,7 @@ def cored(cfg): address_config = cfg["grpcaddress"] port_config = cfg["grpcport"] grpc_address = f"{address_config}:{port_config}" - grpc_thread = threading.Thread(target=grpc_server.listen, args=(grpc_address,)) - grpc_thread.daemon = True + grpc_thread = threading.Thread(target=grpc_server.listen, args=(grpc_address,), daemon=True) grpc_thread.start() # start udp server From d0c4d4b9356d5fa413815c904ced103ef17da11d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 2 Apr 2020 21:14:08 -0700 Subject: [PATCH 0125/1131] fixed issue where the udp handler would no broadcast node/link changes from coresendmsg --- daemon/core/api/tlv/corehandlers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 95ee5e9c..1cc9523c 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -50,6 +50,8 @@ class CoreHandler(socketserver.BaseRequestHandler): The CoreHandler class uses the RequestHandler class for servicing requests. """ + session_clients = {} + def __init__(self, request, client_address, server): """ Create a CoreRequestHandler instance. @@ -87,7 +89,6 @@ class CoreHandler(socketserver.BaseRequestHandler): thread.start() self.session = None - self.session_clients = {} self.coreemu = server.coreemu utils.close_onexec(request.fileno()) socketserver.BaseRequestHandler.__init__(self, request, client_address, server) @@ -1972,6 +1973,7 @@ class CoreUdpHandler(CoreHandler): } self.session = None self.coreemu = server.mainserver.coreemu + self.tcp_handler = server.RequestHandlerClass socketserver.BaseRequestHandler.__init__(self, request, client_address, server) def setup(self): @@ -2060,7 +2062,7 @@ class CoreUdpHandler(CoreHandler): if not isinstance(message, (coreapi.CoreNodeMessage, coreapi.CoreLinkMessage)): return - clients = self.session_clients[self.session.id] + clients = self.tcp_handler.session_clients[self.session.id] for client in clients: try: client.sendall(message.raw_message) From b6fbedf471a68c651efede9cc2abf3ef2530b057 Mon Sep 17 00:00:00 2001 From: Shaun Voigt Date: Mon, 6 Apr 2020 17:36:32 +0930 Subject: [PATCH 0126/1131] Fix for IPv6 Addresses disappear with FRR #421 --- daemon/core/nodes/netclient.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 0ad62e50..36902d52 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -4,7 +4,7 @@ Clients for dealing with bridge/interface commands. import json from typing import Callable -from core.constants import ETHTOOL_BIN, IP_BIN, OVS_BIN, TC_BIN +from core.constants import ETHTOOL_BIN, IP_BIN, OVS_BIN, TC_BIN, SYSCTL_BIN class LinuxNetClient: @@ -168,6 +168,10 @@ class LinuxNetClient: ) else: self.run(f"{IP_BIN} address add {address} dev {device}") + if ':' in address: + # IPv6 addresses are removed by default on interface down. + # Make sure that the IPv6 address we add is not removed + self.run(f"{SYSCTL_BIN} -w net.ipv6.conf.{device}.keep_addr_on_down=1") def delete_address(self, device: str, address: str) -> None: """ From 953bd80e2ea06aa41d5ed6953062658480329b1a Mon Sep 17 00:00:00 2001 From: Shaun Voigt Date: Mon, 6 Apr 2020 17:54:42 +0930 Subject: [PATCH 0127/1131] isort - sort imports --- daemon/core/nodes/netclient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 36902d52..bea2b0e8 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -4,7 +4,7 @@ Clients for dealing with bridge/interface commands. import json from typing import Callable -from core.constants import ETHTOOL_BIN, IP_BIN, OVS_BIN, TC_BIN, SYSCTL_BIN +from core.constants import ETHTOOL_BIN, IP_BIN, OVS_BIN, SYSCTL_BIN, TC_BIN class LinuxNetClient: From ba1885350945b5ae500a975719e852bab55763e6 Mon Sep 17 00:00:00 2001 From: Shaun Voigt Date: Mon, 6 Apr 2020 18:03:27 +0930 Subject: [PATCH 0128/1131] resolve black formatting --- daemon/core/nodes/netclient.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index bea2b0e8..99e6569a 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -168,10 +168,10 @@ class LinuxNetClient: ) else: self.run(f"{IP_BIN} address add {address} dev {device}") - if ':' in address: - # IPv6 addresses are removed by default on interface down. + if ":" in address: + # IPv6 addresses are removed by default on interface down. # Make sure that the IPv6 address we add is not removed - self.run(f"{SYSCTL_BIN} -w net.ipv6.conf.{device}.keep_addr_on_down=1") + self.run(f"{SYSCTL_BIN} -w net.ipv6.conf.{device}.keep_addr_on_down=1") def delete_address(self, device: str, address: str) -> None: """ From 29fea7e5723b9d537584f75781314775b3d3f29b Mon Sep 17 00:00:00 2001 From: Shaun Voigt Date: Mon, 6 Apr 2020 18:26:29 +0930 Subject: [PATCH 0129/1131] Add IS-IS support to FRR Service #423 --- daemon/core/services/frr.py | 44 ++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py index 52429b26..799c03a5 100644 --- a/daemon/core/services/frr.py +++ b/daemon/core/services/frr.py @@ -211,7 +211,7 @@ bootfrr() if grep -q "^ip route " $FRR_CONF; then bootdaemon "staticd" fi - for r in rip ripng ospf6 ospf bgp babel; do + for r in rip ripng ospf6 ospf bgp babel isis; do if grep -q "^router \\<${r}\\>" $FRR_CONF; then bootdaemon "${r}d" fi @@ -651,3 +651,45 @@ class FRRpimd(FrrService): @classmethod def generatefrrifcconfig(cls, node, ifc): return " ip mfea\n ip igmp\n ip pim\n" + + +class FRRIsis(FrrService): + """ + The ISIS service provides IPv4 and IPv6 routing for wired networks. It does + not build its own configuration file but has hooks for adding to the + unified frr.conf file. + """ + + name = "FRRISIS" + startup = () + shutdown = ("killall isisd",) + validate = ("pidof isisd",) + ipv4_routing = True + ipv6_routing = True + + @staticmethod + def ptpcheck(ifc): + """ + Helper to detect whether interface is connected to a notional + point-to-point link. + """ + if isinstance(ifc.net, PtpNet): + return " isis network point-to-point\n" + return "" + + @classmethod + def generatefrrconfig(cls, node): + cfg = "router isis DEFAULT\n" + cfg += " net 47.0001.0000.1900.%04x.00\n" % node.id + cfg += " metric-style wide\n" + cfg += " is-type level-2-only\n" + cfg += "!\n" + return cfg + + @classmethod + def generatefrrifcconfig(cls, node, ifc): + cfg = " ip router isis DEFAULT\n" + cfg += " ipv6 router isis DEFAULT\n" + cfg += " isis circuit-type level-2-only\n" + cfg += cls.ptpcheck(ifc) + return cfg From 8dfdd6171df7a1fa317c12ae16f229fa28355ee9 Mon Sep 17 00:00:00 2001 From: Shaun Voigt Date: Tue, 7 Apr 2020 07:44:23 +0930 Subject: [PATCH 0130/1131] check for ipv6 address using netaddr.valid_ipv6 --- daemon/core/nodes/netclient.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 99e6569a..84727e7d 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -3,6 +3,7 @@ Clients for dealing with bridge/interface commands. """ import json from typing import Callable +import netaddr from core.constants import ETHTOOL_BIN, IP_BIN, OVS_BIN, SYSCTL_BIN, TC_BIN @@ -168,7 +169,7 @@ class LinuxNetClient: ) else: self.run(f"{IP_BIN} address add {address} dev {device}") - if ":" in address: + if netaddr.valid_ipv6(address.split("/")[0]): # IPv6 addresses are removed by default on interface down. # Make sure that the IPv6 address we add is not removed self.run(f"{SYSCTL_BIN} -w net.ipv6.conf.{device}.keep_addr_on_down=1") From 2750a69e7959c6a43e26c9a83c1c9ad7f43dbd77 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 6 Apr 2020 15:16:16 -0700 Subject: [PATCH 0131/1131] initial route monitor based on searching for core directories --- daemon/scripts/core-route-monitor | 197 ++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100755 daemon/scripts/core-route-monitor diff --git a/daemon/scripts/core-route-monitor b/daemon/scripts/core-route-monitor new file mode 100755 index 00000000..366b747e --- /dev/null +++ b/daemon/scripts/core-route-monitor @@ -0,0 +1,197 @@ +#!/usr/bin/env python +import argparse +import enum +import select +import socket +import subprocess +import time +from argparse import ArgumentDefaultsHelpFormatter +from functools import cmp_to_key +from glob import glob +from queue import Queue +from threading import Thread +from typing import Dict, Tuple + +SDT_HOST = "127.0.0.1" +SDT_PORT = 50000 +ROUTE_LAYER = "CORE Route" +DEAD_TIME = 3 +ROUTE_TIME = 3 +PACKET_CHOICES = ["udp", "tcp", "icmp"] + + +def find_nodes() -> Dict[str, str]: + sessions = glob("/tmp/pycore.*") + session = None + if sessions: + session = sessions[0] + if not session: + raise Exception("failed to find core session") + print("core session: ", session) + nodes = {} + with open(f"{session}/nodes", "r") as f: + for line in f.readlines(): + line = line.strip() + values = line.split() + if values[2] == "NodeTypes.DEFAULT": + print("node: ", values[1]) + nodes[values[0]] = f"{session}/{values[1]}" + return nodes + + +class RouteEnum(enum.Enum): + ADD = 0 + DEL = 1 + + +class SdtClient: + def __init__(self, address: Tuple[str, int]) -> None: + self.sock = socket.create_connection(address) + self.links = [] + self.send(f'layer "{ROUTE_LAYER}"') + + def send(self, cmd: str) -> None: + sdt_cmd = f"{cmd}\n".encode() + print("sdt cmd: ", cmd) + self.sock.sendall(sdt_cmd) + + def add_link(self, node1, node2) -> None: + route_id = f"{node1}-{node2}-r" + link_id = f"{node1},{node2},{route_id}" + cmd = f'link {link_id} linkLayer "{ROUTE_LAYER}" line yellow,2' + self.send(cmd) + self.links.append(link_id) + + def delete_links(self) -> None: + for link_id in self.links: + cmd = f"delete link,{link_id}" + self.send(cmd) + self.links.clear() + + +class RouterMonitor: + def __init__(self, nodes: Dict[str, str], src_id: str, src: str, dst: str, pkt: str, + sdt_host: str, sdt_port: int) -> None: + self.queue = Queue() + self.nodes = nodes + self.src_id = src_id + self.src = src + self.dst = dst + self.pkt = pkt + self.seen = {} + self.running = False + self.route_time = None + self.sdt = SdtClient((sdt_host, sdt_port)) + + def start(self) -> None: + self.running = True + for node_id, node in self.nodes.items(): + thread = Thread(target=self.listen, args=(node_id, node), daemon=True) + thread.start() + self.manage() + + def manage(self) -> None: + self.route_time = time.monotonic() + while self.running: + route_enum, node, seen = self.queue.get() + if route_enum == RouteEnum.ADD: + self.seen[node] = seen + elif node in self.seen: + del self.seen[node] + + if (time.monotonic() - self.route_time) >= ROUTE_TIME: + self.manage_routes() + self.route_time = time.monotonic() + + def route_sort(self, x: Tuple[str, int], y: Tuple[str, int]) -> int: + x_node = x[0] + y_node = y[0] + if x_node == self.src_id: + return 1 + if y_node == self.src_id: + return -1 + x_ttl, y_ttl = x[1], y[1] + return x_ttl - y_ttl + + def manage_routes(self) -> None: + self.sdt.delete_links() + if not self.seen: + return + values = sorted(self.seen.items(), + key=cmp_to_key(self.route_sort), + reverse=True) + print("current route:") + print(values) + for index, node_data in enumerate(values): + next_index = index + 1 + if next_index == len(values): + break + next_node_id = values[next_index][0] + node_id, ttl = node_data + print(f"{node_id} -> {next_node_id}") + self.sdt.add_link(node_id, next_node_id) + + def cleanup(self) -> None: + self.sdt.delete_links() + + def listen(self, node_id, node) -> None: + cmd = ( + f"tcpdump -lnv src host {self.src} and dst host {self.dst} and {self.pkt}" + ) + node_cmd = f"vcmd -c {node} -- {cmd}" + p = subprocess.Popen(node_cmd, shell=True, stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL) + current = time.monotonic() + try: + while not p.poll() and self.running: + ready, _, _ = select.select([p.stdout], [], [], 1) + if ready: + line = p.stdout.readline().strip().decode() + line = line.split("ttl", 1)[1] + ttl = int(line.split(",", 1)[0]) + p.stdout.readline() + self.queue.put((RouteEnum.ADD, node_id, ttl)) + current = time.monotonic() + else: + if (time.monotonic() - current) >= DEAD_TIME: + self.queue.put((RouteEnum.DEL, node_id, None)) + except Exception as e: + print(f"listen error: {e}") + + +def main() -> None: + parser = argparse.ArgumentParser( + description="core route monitor", + formatter_class=ArgumentDefaultsHelpFormatter, + ) + parser.add_argument("--id", required=True, + help="source node id for determining path") + parser.add_argument("--src", default="10.0.0.20", + help="source address for route monitoring") + parser.add_argument("--dst", default="10.0.2.20", + help="destination address for route monitoring") + parser.add_argument("--pkt", default="icmp", choices=PACKET_CHOICES, + help="packet type") + parser.add_argument("--sdt-host", default=SDT_HOST, help="sdt host address") + parser.add_argument("--sdt-port", type=int, default=SDT_PORT, help="sdt port") + args = parser.parse_args() + + nodes = find_nodes() + monitor = RouterMonitor( + nodes, + args.id, + args.src, + args.dst, + args.pkt, + args.sdt_host, + args.sdt_port, + ) + try: + monitor.start() + except KeyboardInterrupt: + monitor.cleanup() + print("ending packet monitor") + + +if __name__ == "__main__": + main() From 6c9c2cbeb0179fb9866839dc08f890002bdb870d Mon Sep 17 00:00:00 2001 From: Shaun Voigt Date: Tue, 7 Apr 2020 07:50:26 +0930 Subject: [PATCH 0132/1131] resolve isort --- daemon/core/nodes/netclient.py | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 84727e7d..12ab8dc1 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -3,6 +3,7 @@ Clients for dealing with bridge/interface commands. """ import json from typing import Callable + import netaddr from core.constants import ETHTOOL_BIN, IP_BIN, OVS_BIN, SYSCTL_BIN, TC_BIN From 0742c08b59ec89fed834ead3a648cbc295be40ba Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 6 Apr 2020 15:46:47 -0700 Subject: [PATCH 0133/1131] added session/node dir to grpc responses and node channels, updating route monitor to use grpc for getting session/node information --- daemon/core/api/grpc/server.py | 27 +++++++++------ daemon/proto/core/api/grpc/core.proto | 4 +++ daemon/scripts/core-route-monitor | 48 +++++++++++++-------------- 3 files changed, 45 insertions(+), 34 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 47a30dfd..73f24176 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -111,7 +111,7 @@ from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags from core.emulator.session import Session from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility -from core.nodes.base import CoreNodeBase, NodeBase +from core.nodes.base import CoreNode, CoreNodeBase, NodeBase from core.nodes.docker import DockerNode from core.nodes.lxd import LxcNode from core.services.coreservices import ServiceManager @@ -373,6 +373,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): state=session.state.value, nodes=session.get_node_count(), file=session.file_name, + dir=session.session_dir, ) sessions.append(session_summary) return core_pb2.GetSessionsResponse(sessions=sessions) @@ -543,7 +544,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): node = session.nodes[_id] if not isinstance(node.id, int): continue - node_type = session.get_node_type(node.__class__) model = getattr(node, "type", None) position = core_pb2.Position( @@ -558,8 +558,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): emane_model = None if isinstance(node, EmaneNet): emane_model = node.model.name + node_dir = None + channel = None + if isinstance(node, CoreNode): + node_dir = node.nodedir + channel = node.ctrlchnlname image = getattr(node, "image", None) - node_proto = core_pb2.Node( id=node.id, name=node.name, @@ -571,16 +575,17 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): icon=node.icon, image=image, config_services=config_services, + dir=node_dir, + channel=channel, ) if isinstance(node, (DockerNode, LxcNode)): node_proto.image = node.image nodes.append(node_proto) - node_links = get_links(session, node) links.extend(node_links) session_proto = core_pb2.Session( - state=session.state.value, nodes=nodes, links=links + state=session.state.value, nodes=nodes, links=links, dir=session.session_dir ) return core_pb2.GetSessionResponse(session=session_proto) @@ -713,21 +718,22 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("get node: %s", request) session = self.get_session(request.session_id, context) node = self.get_node(session, request.node_id, context) - interfaces = [] for interface_id in node._netif: interface = node._netif[interface_id] interface_proto = grpcutils.interface_to_proto(interface) interfaces.append(interface_proto) - emane_model = None if isinstance(node, EmaneNet): emane_model = node.model.name - + node_dir = None + channel = None + if isinstance(node, CoreNode): + node_dir = node.nodedir + channel = node.ctrlchnlname services = [] if node.services: services = [x.name for x in node.services] - position = core_pb2.Position( x=node.position.x, y=node.position.y, z=node.position.z ) @@ -740,10 +746,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): model=node.type, position=position, services=services, + dir=node_dir, + channel=channel, ) if isinstance(node, (DockerNode, LxcNode)): node_proto.image = node.image - return core_pb2.GetNodeResponse(node=node_proto, interfaces=interfaces) def EditNode( diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index f457222d..e7bac450 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -650,6 +650,7 @@ message Session { SessionState.Enum state = 2; repeated Node nodes = 3; repeated Link links = 4; + string dir = 5; } message SessionSummary { @@ -657,6 +658,7 @@ message SessionSummary { SessionState.Enum state = 2; int32 nodes = 3; string file = 4; + string dir = 5; } message Node { @@ -673,6 +675,8 @@ message Node { string server = 11; repeated string config_services = 12; Geo geo = 13; + string dir = 14; + string channel = 15; } message Link { diff --git a/daemon/scripts/core-route-monitor b/daemon/scripts/core-route-monitor index 366b747e..c040b369 100755 --- a/daemon/scripts/core-route-monitor +++ b/daemon/scripts/core-route-monitor @@ -7,11 +7,13 @@ import subprocess import time from argparse import ArgumentDefaultsHelpFormatter from functools import cmp_to_key -from glob import glob from queue import Queue from threading import Thread from typing import Dict, Tuple +from core.api.grpc.client import CoreGrpcClient +from core.api.grpc.core_pb2 import NodeType + SDT_HOST = "127.0.0.1" SDT_PORT = 50000 ROUTE_LAYER = "CORE Route" @@ -20,25 +22,6 @@ ROUTE_TIME = 3 PACKET_CHOICES = ["udp", "tcp", "icmp"] -def find_nodes() -> Dict[str, str]: - sessions = glob("/tmp/pycore.*") - session = None - if sessions: - session = sessions[0] - if not session: - raise Exception("failed to find core session") - print("core session: ", session) - nodes = {} - with open(f"{session}/nodes", "r") as f: - for line in f.readlines(): - line = line.strip() - values = line.split() - if values[2] == "NodeTypes.DEFAULT": - print("node: ", values[1]) - nodes[values[0]] = f"{session}/{values[1]}" - return nodes - - class RouteEnum(enum.Enum): ADD = 0 DEL = 1 @@ -70,10 +53,10 @@ class SdtClient: class RouterMonitor: - def __init__(self, nodes: Dict[str, str], src_id: str, src: str, dst: str, pkt: str, + def __init__(self, src_id: str, src: str, dst: str, pkt: str, sdt_host: str, sdt_port: int) -> None: self.queue = Queue() - self.nodes = nodes + self.core = CoreGrpcClient() self.src_id = src_id self.src = src self.dst = dst @@ -82,6 +65,25 @@ class RouterMonitor: self.running = False self.route_time = None self.sdt = SdtClient((sdt_host, sdt_port)) + self.nodes = self.get_nodes() + + def get_nodes(self) -> Dict[str, str]: + nodes = {} + with self.core.context_connect(): + response = self.core.get_sessions() + sessions = response.sessions + session = None + if sessions: + session = sessions[0] + if not session: + raise Exception("no current core sessions") + print(session.dir) + response = self.core.get_session(session.id) + for node in response.session.nodes: + if node.type != NodeType.DEFAULT: + continue + nodes[node.id] = node.channel + return nodes def start(self) -> None: self.running = True @@ -176,9 +178,7 @@ def main() -> None: parser.add_argument("--sdt-port", type=int, default=SDT_PORT, help="sdt port") args = parser.parse_args() - nodes = find_nodes() monitor = RouterMonitor( - nodes, args.id, args.src, args.dst, From 0aa7c6f1f2779c052f82231b50f552d2892b94cd Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 6 Apr 2020 16:09:01 -0700 Subject: [PATCH 0134/1131] cleaned up how grpc creates node protobuf data for grpc interfaces, cleaned up route monitor script slighly --- daemon/core/api/grpc/grpcutils.py | 42 ++++++++++++++++++ daemon/core/api/grpc/server.py | 71 ++----------------------------- daemon/scripts/core-route-monitor | 32 +++++++++----- 3 files changed, 66 insertions(+), 79 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index e23073a0..0aa5a553 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -8,6 +8,7 @@ from core import utils from core.api.grpc import common_pb2, core_pb2 from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig from core.config import ConfigurableOptions +from core.emane.nodes import EmaneNet from core.emulator.data import LinkData from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.enumerations import LinkTypes, NodeTypes @@ -221,6 +222,47 @@ def get_config_options( return results +def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node: + """ + Convert CORE node to protobuf representation. + + :param session: session containing node + :param node: node to convert + :return: node proto + """ + node_type = session.get_node_type(node.__class__) + position = core_pb2.Position( + x=node.position.x, y=node.position.y, z=node.position.z + ) + services = getattr(node, "services", []) + if services is None: + services = [] + services = [x.name for x in services] + config_services = getattr(node, "config_services", {}) + config_services = [x for x in config_services] + emane_model = None + if isinstance(node, EmaneNet): + emane_model = node.model.name + model = getattr(node, "type", None) + node_dir = getattr(node, "nodedir", None) + channel = getattr(node, "ctrlchnlname", None) + image = getattr(node, "image", None) + return core_pb2.Node( + id=node.id, + name=node.name, + emane=emane_model, + model=model, + type=node_type.value, + position=position, + services=services, + icon=node.icon, + image=image, + config_services=config_services, + dir=node_dir, + channel=channel, + ) + + def get_links(session: Session, node: NodeBase): """ Retrieve a list of links for grpc to use diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 73f24176..c559f8f2 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -103,7 +103,6 @@ from core.api.grpc.wlan_pb2 import ( SetWlanConfigRequest, SetWlanConfigResponse, ) -from core.emane.nodes import EmaneNet from core.emulator.coreemu import CoreEmu from core.emulator.data import LinkData from core.emulator.emudata import LinkOptions, NodeOptions @@ -111,9 +110,7 @@ from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags from core.emulator.session import Session from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility -from core.nodes.base import CoreNode, CoreNodeBase, NodeBase -from core.nodes.docker import DockerNode -from core.nodes.lxd import LxcNode +from core.nodes.base import CoreNodeBase, NodeBase from core.services.coreservices import ServiceManager _ONE_DAY_IN_SECONDS = 60 * 60 * 24 @@ -544,42 +541,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): node = session.nodes[_id] if not isinstance(node.id, int): continue - node_type = session.get_node_type(node.__class__) - model = getattr(node, "type", None) - position = core_pb2.Position( - x=node.position.x, y=node.position.y, z=node.position.z - ) - services = getattr(node, "services", []) - if services is None: - services = [] - services = [x.name for x in services] - config_services = getattr(node, "config_services", {}) - config_services = [x for x in config_services] - emane_model = None - if isinstance(node, EmaneNet): - emane_model = node.model.name - node_dir = None - channel = None - if isinstance(node, CoreNode): - node_dir = node.nodedir - channel = node.ctrlchnlname - image = getattr(node, "image", None) - node_proto = core_pb2.Node( - id=node.id, - name=node.name, - emane=emane_model, - model=model, - type=node_type.value, - position=position, - services=services, - icon=node.icon, - image=image, - config_services=config_services, - dir=node_dir, - channel=channel, - ) - if isinstance(node, (DockerNode, LxcNode)): - node_proto.image = node.image + node_proto = grpcutils.get_node_proto(session, node) nodes.append(node_proto) node_links = get_links(session, node) links.extend(node_links) @@ -723,34 +685,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): interface = node._netif[interface_id] interface_proto = grpcutils.interface_to_proto(interface) interfaces.append(interface_proto) - emane_model = None - if isinstance(node, EmaneNet): - emane_model = node.model.name - node_dir = None - channel = None - if isinstance(node, CoreNode): - node_dir = node.nodedir - channel = node.ctrlchnlname - services = [] - if node.services: - services = [x.name for x in node.services] - position = core_pb2.Position( - x=node.position.x, y=node.position.y, z=node.position.z - ) - node_type = session.get_node_type(node.__class__) - node_proto = core_pb2.Node( - id=node.id, - name=node.name, - type=node_type.value, - emane=emane_model, - model=node.type, - position=position, - services=services, - dir=node_dir, - channel=channel, - ) - if isinstance(node, (DockerNode, LxcNode)): - node_proto.image = node.image + node_proto = grpcutils.get_node_proto(session, node) return core_pb2.GetNodeResponse(node=node_proto, interfaces=interfaces) def EditNode( diff --git a/daemon/scripts/core-route-monitor b/daemon/scripts/core-route-monitor index c040b369..668b3c81 100755 --- a/daemon/scripts/core-route-monitor +++ b/daemon/scripts/core-route-monitor @@ -33,9 +33,11 @@ class SdtClient: self.links = [] self.send(f'layer "{ROUTE_LAYER}"') + def close(self) -> None: + self.sock.close() + def send(self, cmd: str) -> None: sdt_cmd = f"{cmd}\n".encode() - print("sdt cmd: ", cmd) self.sock.sendall(sdt_cmd) def add_link(self, node1, node2) -> None: @@ -64,6 +66,7 @@ class RouterMonitor: self.seen = {} self.running = False self.route_time = None + self.listeners = [] self.sdt = SdtClient((sdt_host, sdt_port)) self.nodes = self.get_nodes() @@ -77,7 +80,7 @@ class RouterMonitor: session = sessions[0] if not session: raise Exception("no current core sessions") - print(session.dir) + print("session: ", session.dir) response = self.core.get_session(session.id) for node in response.session.nodes: if node.type != NodeType.DEFAULT: @@ -88,8 +91,10 @@ class RouterMonitor: def start(self) -> None: self.running = True for node_id, node in self.nodes.items(): + print("listening on node: ", node) thread = Thread(target=self.listen, args=(node_id, node), daemon=True) thread.start() + self.listeners.append(thread) self.manage() def manage(self) -> None: @@ -123,7 +128,6 @@ class RouterMonitor: key=cmp_to_key(self.route_sort), reverse=True) print("current route:") - print(values) for index, node_data in enumerate(values): next_index = index + 1 if next_index == len(values): @@ -133,8 +137,13 @@ class RouterMonitor: print(f"{node_id} -> {next_node_id}") self.sdt.add_link(node_id, next_node_id) - def cleanup(self) -> None: + def stop(self) -> None: + self.running = False self.sdt.delete_links() + self.sdt.close() + for thread in self.listeners: + thread.join() + self.listeners.clear() def listen(self, node_id, node) -> None: cmd = ( @@ -149,16 +158,17 @@ class RouterMonitor: ready, _, _ = select.select([p.stdout], [], [], 1) if ready: line = p.stdout.readline().strip().decode() - line = line.split("ttl", 1)[1] - ttl = int(line.split(",", 1)[0]) - p.stdout.readline() - self.queue.put((RouteEnum.ADD, node_id, ttl)) - current = time.monotonic() + if line: + line = line.split("ttl", 1)[1] + ttl = int(line.split(",", 1)[0]) + p.stdout.readline() + self.queue.put((RouteEnum.ADD, node_id, ttl)) + current = time.monotonic() else: if (time.monotonic() - current) >= DEAD_TIME: self.queue.put((RouteEnum.DEL, node_id, None)) except Exception as e: - print(f"listen error: {e}") + print(f"listener error: {e}") def main() -> None: @@ -189,7 +199,7 @@ def main() -> None: try: monitor.start() except KeyboardInterrupt: - monitor.cleanup() + monitor.stop() print("ending packet monitor") From 87f90cd8e37af102309adfa53bf3d1de57545f6b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 6 Apr 2020 16:47:21 -0700 Subject: [PATCH 0135/1131] added tcpdump check to print a message about its requirement --- daemon/scripts/core-route-monitor | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/daemon/scripts/core-route-monitor b/daemon/scripts/core-route-monitor index 668b3c81..afa7b055 100755 --- a/daemon/scripts/core-route-monitor +++ b/daemon/scripts/core-route-monitor @@ -11,6 +11,7 @@ from queue import Queue from threading import Thread from typing import Dict, Tuple +from core import utils from core.api.grpc.client import CoreGrpcClient from core.api.grpc.core_pb2 import NodeType @@ -172,6 +173,10 @@ class RouterMonitor: def main() -> None: + if not utils.which("tcpdump", required=False): + print("core-route-monitor requires tcpdump to be installed") + return + parser = argparse.ArgumentParser( description="core route monitor", formatter_class=ArgumentDefaultsHelpFormatter, @@ -200,7 +205,7 @@ def main() -> None: monitor.start() except KeyboardInterrupt: monitor.stop() - print("ending packet monitor") + print("ending route monitor") if __name__ == "__main__": From 3a45e9ec7a568ca755a66619e21c8e4f223e8dd8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 7 Apr 2020 21:03:45 -0700 Subject: [PATCH 0136/1131] fix for ipsec service reading file as bytes --- daemon/core/services/security.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/daemon/core/services/security.py b/daemon/core/services/security.py index 5960161f..eb6545b2 100644 --- a/daemon/core/services/security.py +++ b/daemon/core/services/security.py @@ -84,12 +84,11 @@ class IPsec(CoreService): cfg += "# set up static tunnel mode security assocation for service " cfg += "(security.py)\n" fname = "%s/examples/services/sampleIPsec" % constants.CORE_DATA_DIR - try: - cfg += open(fname, "rb").read() + with open(fname, "r") as f: + cfg += f.read() except IOError: logging.exception("Error opening IPsec configuration template (%s)", fname) - return cfg From a5c412b594f1447aac2110a0a92073ed85a835f7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 9 Apr 2020 11:42:27 -0700 Subject: [PATCH 0137/1131] updates to sdt integration to use different colors for each network of wireless links --- daemon/core/emulator/session.py | 4 ++- daemon/core/plugins/sdt.py | 58 +++++++++++++++++++++------------ 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 1124ee71..15dcf7d1 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -1404,8 +1404,8 @@ class Session: if node: node.shutdown() - self.check_shutdown() self.sdt.delete_node(_id) + self.check_shutdown() return node is not None @@ -1602,6 +1602,8 @@ class Session: if node_count == 0: shutdown = True self.set_state(EventTypes.SHUTDOWN_STATE) + # clearing sdt saved data here for legacy gui + self.sdt.shutdown() return shutdown def short_session_id(self) -> str: diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index befbf340..68be2147 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -12,7 +12,7 @@ from core import constants from core.constants import CORE_DATA_DIR from core.emane.nodes import EmaneNet from core.emulator.data import LinkData, NodeData -from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags +from core.emulator.enumerations import EventTypes, MessageFlags from core.errors import CoreError from core.nodes.base import CoreNetworkBase, NodeBase from core.nodes.network import WlanNode @@ -32,6 +32,8 @@ CORE_LAYER = "CORE" NODE_LAYER = "CORE::Nodes" LINK_LAYER = "CORE::Links" CORE_LAYERS = [CORE_LAYER, LINK_LAYER, NODE_LAYER] +DEFAULT_LINK_COLOR = "red" +LINK_COLORS = ["green", "blue", "orange", "purple", "white"] class Sdt: @@ -68,10 +70,11 @@ class Sdt: self.lock = threading.Lock() self.sock = None self.connected = False - self.showerror = True self.url = self.DEFAULT_SDT_URL self.address = None self.protocol = None + self.colors = {} + self.network_layers = set() self.session.node_handlers.append(self.handle_node_update) self.session.link_handlers.append(self.handle_link_update) @@ -171,8 +174,13 @@ class Sdt: :return: nothing """ self.cmd("clear all") + for layer in self.network_layers: + self.cmd(f"delete layer,{layer}") + for layer in CORE_LAYERS[::-1]: + self.cmd(f"delete layer,{layer}") self.disconnect() - self.showerror = True + self.network_layers.clear() + self.colors.clear() def cmd(self, cmdstr: str) -> bool: """ @@ -345,13 +353,26 @@ class Sdt: pass return result + def get_link_line(self, network_id: int) -> str: + """ + Retrieve link line color based on network. + + :param network_id: network id of link, None for wired links + :return: link line configuration + """ + network = self.session.nodes.get(network_id) + if network: + color = self.colors.get(network_id) + if not color: + index = len(self.colors) % len(LINK_COLORS) + color = LINK_COLORS[index] + self.colors[network_id] = color + else: + color = DEFAULT_LINK_COLOR + return f"{color},2" + def add_link( - self, - node_one: int, - node_two: int, - network_id: int = None, - label: str = None, - is_wireless: bool = False, + self, node_one: int, node_two: int, network_id: int = None, label: str = None ) -> None: """ Handle adding a link in SDT. @@ -360,7 +381,6 @@ class Sdt: :param node_two: node two id :param network_id: network link is associated with, None otherwise :param label: label for link - :param is_wireless: True if link is wireless, False otherwise :return: nothing """ logging.debug("sdt add link: %s, %s, %s", node_one, node_two, network_id) @@ -368,21 +388,20 @@ class Sdt: return if self.wireless_net_check(node_one) or self.wireless_net_check(node_two): return - if is_wireless: - attr = "green,2" - else: - attr = "red,2" + line = self.get_link_line(network_id) link_id = get_link_id(node_one, node_two, network_id) layer = LINK_LAYER if network_id: - node = self.session.nodes[network_id] - network_name = node.name - layer = f"{layer}::{network_name}" + node = self.session.nodes.get(network_id) + if node: + network_name = node.name + layer = f"{layer}::{network_name}" + self.network_layers.add(layer) link_label = "" if label: link_label = f'linklabel on,"{label}"' self.cmd( - f"link {node_one},{node_two},{link_id} linkLayer {layer} line {attr} " + f"link {node_one},{node_two},{link_id} linkLayer {layer} line {line} " f"{link_label}" ) @@ -434,10 +453,9 @@ class Sdt: node_one = link_data.node1_id node_two = link_data.node2_id network_id = link_data.network_id - is_wireless = link_data.link_type == LinkTypes.WIRELESS label = link_data.label if link_data.message_type == MessageFlags.ADD: - self.add_link(node_one, node_two, network_id, label, is_wireless) + self.add_link(node_one, node_two, network_id, label) elif link_data.message_type == MessageFlags.DELETE: self.delete_link(node_one, node_two, network_id) elif link_data.message_type == MessageFlags.NONE and label: From ca039946a677fb425375ebe81f6df600b505281c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 9 Apr 2020 14:34:52 -0700 Subject: [PATCH 0138/1131] quick pass with some doc cleanup and reformatting to fix various issues --- README.md | 29 +- docs/architecture.md | 47 +- docs/ctrlnet.md | 98 +++- docs/devguide.md | 8 +- docs/distributed.md | 32 +- docs/emane.md | 183 ++++-- docs/grpc.md | 4 +- docs/gui.md | 738 ++++++++++++++++++++++++ docs/index.md | 12 +- docs/machine.md | 22 - docs/nodetypes.md | 45 ++ docs/performance.md | 37 +- docs/scripting.md | 65 ++- docs/services.md | 9 +- docs/static/gui/document-properties.gif | Bin 0 -> 635 bytes docs/static/gui/host.gif | Bin 0 -> 1189 bytes docs/static/gui/hub.gif | Bin 0 -> 719 bytes docs/static/gui/lanswitch.gif | Bin 0 -> 744 bytes docs/static/gui/link.gif | Bin 0 -> 86 bytes docs/static/gui/marker.gif | Bin 0 -> 375 bytes docs/static/gui/mdr.gif | Bin 0 -> 1276 bytes docs/static/gui/observe.gif | Bin 0 -> 1149 bytes docs/static/gui/oval.gif | Bin 0 -> 174 bytes docs/static/gui/pc.gif | Bin 0 -> 1300 bytes docs/static/gui/rectangle.gif | Bin 0 -> 160 bytes docs/static/gui/rj45.gif | Bin 0 -> 755 bytes docs/static/gui/router.gif | Bin 0 -> 1152 bytes docs/static/gui/router_green.gif | Bin 0 -> 753 bytes docs/static/gui/run.gif | Bin 0 -> 324 bytes docs/static/gui/select.gif | Bin 0 -> 925 bytes docs/static/gui/start.gif | Bin 0 -> 1131 bytes docs/static/gui/stop.gif | Bin 0 -> 1204 bytes docs/static/gui/text.gif | Bin 0 -> 127 bytes docs/static/gui/tunnel.gif | Bin 0 -> 799 bytes docs/static/gui/twonode.gif | Bin 0 -> 220 bytes docs/static/gui/wlan.gif | Bin 0 -> 146 bytes docs/usage.md | 722 ----------------------- 37 files changed, 1160 insertions(+), 891 deletions(-) create mode 100644 docs/gui.md delete mode 100644 docs/machine.md create mode 100644 docs/nodetypes.md create mode 100644 docs/static/gui/document-properties.gif create mode 100644 docs/static/gui/host.gif create mode 100644 docs/static/gui/hub.gif create mode 100644 docs/static/gui/lanswitch.gif create mode 100644 docs/static/gui/link.gif create mode 100644 docs/static/gui/marker.gif create mode 100644 docs/static/gui/mdr.gif create mode 100644 docs/static/gui/observe.gif create mode 100644 docs/static/gui/oval.gif create mode 100644 docs/static/gui/pc.gif create mode 100644 docs/static/gui/rectangle.gif create mode 100644 docs/static/gui/rj45.gif create mode 100644 docs/static/gui/router.gif create mode 100644 docs/static/gui/router_green.gif create mode 100644 docs/static/gui/run.gif create mode 100644 docs/static/gui/select.gif create mode 100644 docs/static/gui/start.gif create mode 100644 docs/static/gui/stop.gif create mode 100644 docs/static/gui/text.gif create mode 100644 docs/static/gui/tunnel.gif create mode 100644 docs/static/gui/twonode.gif create mode 100644 docs/static/gui/wlan.gif delete mode 100644 docs/usage.md diff --git a/README.md b/README.md index cd77ef4d..62f21628 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ CORE: Common Open Research Emulator -Copyright (c)2005-2019 the Boeing Company. +Copyright (c)2005-2020 the Boeing Company. See the LICENSE file included in this distribution. @@ -14,28 +14,11 @@ networks to live networks. CORE consists of a GUI for drawing topologies of lightweight virtual machines, and Python modules for scripting network emulation. -## Documentation and Examples +## Documentation & Support -* Documentation hosted on GitHub - * -* Basic Script Examples - * [Examples](daemon/examples/python) -* Custom Service Example - * [sample.py](daemon/examples/myservices/sample.py) -* Custom Emane Model Example - * [examplemodel.py](daemon/examples/myemane/examplemodel.py) - -## Support - -We are leveraging Discord for persistent chat rooms, voice chat, and -GitHub integration. This allows for more dynamic conversations and the +We are leveraging GitHub hosted documentation and Discord for persistent +chat rooms. This allows for more dynamic conversations and the capability to respond faster. Feel free to join us at the link below. - -## Building CORE - -See [CORE Installation](http://coreemu.github.io/core/install.html) for detailed build instructions. - -### Running CORE - -See [Using the CORE GUI](http://coreemu.github.io/core/usage.html) for more details on running CORE. +* [Documentation](https://coreemu.github.io/core/) +* [Discord Channel](https://discord.gg/AKd7kmP) diff --git a/docs/architecture.md b/docs/architecture.md index e9d4a9e5..605d5ad0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -7,12 +7,14 @@ * CORE Daemon * Manages emulation sessions - * Builds the emulated networks using kernel virtualization for nodes and some form of bridging and packet manipulation for virtual networks - * Nodes and networks come together via interfaces installed on nodes + * Builds the emulated networks using Linux namespaces for nodes and + some form of bridging and packet manipulation for virtual networks + * Nodes and networks come together via interfaces installed on nodes * Controlled via the CORE GUI * Written in python and can be scripted, given direct control of scenarios * CORE GUI - * GUI and daemon communicate using a custom, asynchronous, sockets-based API, known as the CORE API + * GUI and daemon communicate using a custom, asynchronous, sockets-based + API, known as the CORE API * Drag and drop creation for nodes and network interfaces * Can launch terminals for emulated nodes in running scenarios * Can save/open scenario files to recreate previous sessions @@ -20,20 +22,49 @@ ![](static/core-architecture.jpg) +## Sessions + +CORE can create and run multiple sessions at once, below is a high level +overview of the states a session will go between. + +![](static/core-workflow.jpg) + ## How Does it Work? -A CORE node is a lightweight virtual machine. The CORE framework runs on Linux. CORE uses Linux network namespace virtualization to build virtual nodes, and ties them together with virtual networks using Linux Ethernet bridging. +The CORE framework runs on Linux and uses Linux namespacing for creating +node containers. These nodes are linked together using Linux bridging and +virtual interfaces. CORE sessions are a set of nodes and links operating +together for a specific purpose. ### Linux -Linux network namespaces (also known as netns) is the primary virtualization technique used by CORE. Most recent Linux distributions have namespaces-enabled kernels out of the box. A namespace is created using the ```clone()``` system call. Each namespace has its own process environment and private network stack. Network namespaces share the same filesystem in CORE. +Linux network namespaces (also known as netns) is the primary virtualization +technique used by CORE. Most recent Linux distributions have +namespaces-enabled kernels out of the box. Each namespace has its own process +environment and private network stack. Network namespaces share the same +filesystem in CORE. -CORE combines these namespaces with Linux Ethernet bridging to form networks. Link characteristics are applied using Linux Netem queuing disciplines. Ebtables is Ethernet frame filtering on Linux bridges. Wireless networks are emulated by controlling which interfaces can send and receive with ebtables rules. +CORE combines these namespaces with Linux Ethernet bridging to form networks. +Link characteristics are applied using Linux Netem queuing disciplines. +Ebtables is Ethernet frame filtering on Linux bridges. Wireless networks are +emulated by controlling which interfaces can send and receive with ebtables +rules. ## Prior Work -The Tcl/Tk CORE GUI was originally derived from the open source [IMUNES](http://imunes.net) project from the University of Zagreb as a custom project within Boeing Research and Technology's Network Technology research group in 2004. Since then they have developed the CORE framework to use Linux virtualization, have developed a Python framework, and made numerous user- and kernel-space developments, such as support for wireless networks, IPsec, distribute emulation, simulation integration, and more. The IMUNES project also consists of userspace and kernel components. +The Tcl/Tk CORE GUI was originally derived from the open source +[IMUNES](http://imunes.net) project from the University of Zagreb as a custom +project within Boeing Research and Technology's Network Technology research +group in 2004. Since then they have developed the CORE framework to use Linux +virtualization, have developed a Python framework, and made numerous user and +kernel-space developments, such as support for wireless networks, IPsec, +distribute emulation, simulation integration, and more. The IMUNES project +also consists of userspace and kernel components. ## Open Source Project and Resources -CORE has been released by Boeing to the open source community under the BSD license. If you find CORE useful for your work, please contribute back to the project. Contributions can be as simple as reporting a bug, dropping a line of encouragement or technical suggestions to the mailing lists, or can also include submitting patches or maintaining aspects of the tool. For contributing to CORE, please visit [CORE GitHub](https://github.com/coreemu/core). +CORE has been released by Boeing to the open source community under the BSD +license. If you find CORE useful for your work, please contribute back to the +project. Contributions can be as simple as reporting a bug, dropping a line of +encouragement or technical suggestions to the mailing lists, or can also +include submitting patches or maintaining aspects of the tool. diff --git a/docs/ctrlnet.md b/docs/ctrlnet.md index 5b38191a..5b82d7e8 100644 --- a/docs/ctrlnet.md +++ b/docs/ctrlnet.md @@ -5,19 +5,40 @@ ## Overview -The CORE control network allows the virtual nodes to communicate with their host environment. There are two types: the primary control network and auxiliary control networks. The primary control network is used mainly for communicating with the virtual nodes from host machines and for master-slave communications in a multi-server distributed environment. Auxiliary control networks have been introduced to for routing namespace hosted emulation software traffic to the test network. +The CORE control network allows the virtual nodes to communicate with their +host environment. There are two types: the primary control network and +auxiliary control networks. The primary control network is used mainly for +communicating with the virtual nodes from host machines and for master-slave +communications in a multi-server distributed environment. Auxiliary control +networks have been introduced to for routing namespace hosted emulation +software traffic to the test network. ## Activating the Primary Control Network -Under the *Session Menu*, the *Options...* dialog has an option to set a *control network prefix*. +Under the *Session Menu*, the *Options...* dialog has an option to set a +*control network prefix*. -This can be set to a network prefix such as *172.16.0.0/24*. A bridge will be created on the host machine having the last address in the prefix range (e.g. *172.16.0.254*), and each node will have an extra *ctrl0* control interface configured with an address corresponding to its node number (e.g. *172.16.0.3* for *n3*.) +This can be set to a network prefix such as *172.16.0.0/24*. A bridge will +be created on the host machine having the last address in the prefix range +(e.g. *172.16.0.254*), and each node will have an extra *ctrl0* control +interface configured with an address corresponding to its node number +(e.g. *172.16.0.3* for *n3*.) -A default for the primary control network may also be specified by setting the *controlnet* line in the */etc/core/core.conf* configuration file which new sessions will use by default. To simultaneously run multiple sessions with control networks, the session option should be used instead of the *core.conf* default. +A default for the primary control network may also be specified by setting +the *controlnet* line in the */etc/core/core.conf* configuration file which +new sessions will use by default. To simultaneously run multiple sessions with +control networks, the session option should be used instead of the *core.conf* +default. -**NOTE: If you have a large scenario with more than 253 nodes, use a control network prefix that allows more than the suggested */24*, such as */23* or greater.** +> :warning: If you have a large scenario with more than 253 nodes, use a control +network prefix that allows more than the suggested */24*, such as */23* or +greater. -**IMPORTANT: Running a session with a control network can fail if a previous session has set up a control network and the its bridge is still up. Close the previous session first or wait for it to complete. If unable to, the *core-daemon* may need to be restarted and the lingering bridge(s) removed manually.** +> :warning: Running a session with a control network can fail if a previous +session has set up a control network and the its bridge is still up. Close +the previous session first or wait for it to complete. If unable to, the +*core-daemon* may need to be restarted and the lingering bridge(s) removed +manually. ```shell # Restart the CORE Daemon @@ -30,34 +51,60 @@ for cb in $ctrlbridges; do sudo brctl delbr $cb done ``` - -**TIP: If adjustments to the primary control network configuration made in */etc/core/core.conf* do not seem to take affect, check if there is anything set in the *Session Menu*, the *Options...* dialog. They may need to be cleared. These per session settings override the defaults in */etc/core/core.conf*.** + +> :bulb: If adjustments to the primary control network configuration made in +*/etc/core/core.conf* do not seem to take affect, check if there is anything +set in the *Session Menu*, the *Options...* dialog. They may need to be +cleared. These per session settings override the defaults in +*/etc/core/core.conf*. ## Control Network in Distributed Sessions -When the primary control network is activated for a distributed session, a control network bridge will be created on each of the slave servers, with GRE tunnels back to the master server's bridge. The slave control bridges are not assigned an address. From the host, any of the nodes (local or remote) can be accessed, just like the single server case. +When the primary control network is activated for a distributed session, a +control network bridge will be created on each of the slave servers, with +GRE tunnels back to the master server's bridge. The slave control bridges +are not assigned an address. From the host, any of the nodes (local or remote) +can be accessed, just like the single server case. -In some situations, remote emulated nodes need to communicate with the host on which they are running and not the master server. Multiple control network prefixes can be specified in the either the session option or */etc/core/core.conf*, separated by spaces and beginning with the master server. Each entry has the form *"server:prefix"*. For example, if the servers *core1*,*core2*, and *core3* are assigned with nodes in the scenario and using :file:`/etc/core/core.conf` instead of the session option: +In some situations, remote emulated nodes need to communicate with the host +on which they are running and not the master server. Multiple control network +prefixes can be specified in the either the session option or +*/etc/core/core.conf*, separated by spaces and beginning with the master +server. Each entry has the form *"server:prefix"*. For example, if the servers +*core1*,*core2*, and *core3* are assigned with nodes in the scenario and using +*/etc/core/core.conf* instead of the session option. ```shell controlnet=core1:172.16.1.0/24 core2:172.16.2.0/24 core3:172.16.1.0/24 ``` -Then, the control network bridges will be assigned as follows: +Then, the control network bridges will be assigned as follows: * core1 = 172.16.1.254 (assuming it is the master server), * core2 = 172.16.2.254 * core3 = 172.16.3.254 -Tunnels back to the master server will still be built, but it is up to the user to add appropriate routes if networking between control network prefixes is desired. The control network script may help with this. +Tunnels back to the master server will still be built, but it is up to the +user to add appropriate routes if networking between control network prefixes +is desired. The control network script may help with this. ## Control Network Script -A control network script may be specified using the *controlnet_updown_script* option in the */etc/core/core.conf* file. This script will be run after the bridge has been built (and address assigned) with the first argument being the name of the bridge, and the second argument being the keyword *"startup"*. The script will again be invoked prior to bridge removal with the second argument being the keyword *"shutdown"*. +A control network script may be specified using the *controlnet_updown_script* +option in the */etc/core/core.conf* file. This script will be run after the +bridge has been built (and address assigned) with the first argument being the +name of the bridge, and the second argument being the keyword *"startup"*. +The script will again be invoked prior to bridge removal with the second +argument being the keyword *"shutdown"*. ## Auxiliary Control Networks -Starting with EMANE 0.9.2, CORE will run EMANE instances within namespaces. Since it is advisable to separate the OTA traffic from other traffic, we will need more than single channel leading out from the namespace. Up to three auxiliary control networks may be defined. Multiple control networks are set up in */etc/core/core.conf* file. Lines *controlnet1*, *controlnet2* and *controlnet3* define the auxiliary networks. +Starting with EMANE 0.9.2, CORE will run EMANE instances within namespaces. +Since it is advisable to separate the OTA traffic from other traffic, we will +need more than single channel leading out from the namespace. Up to three +auxiliary control networks may be defined. Multiple control networks are set +up in */etc/core/core.conf* file. Lines *controlnet1*, *controlnet2* and +*controlnet3* define the auxiliary networks. For example, having the following */etc/core/core.conf*: @@ -67,13 +114,24 @@ controlnet1 = core1:172.18.1.0/24 core2:172.18.2.0/24 core3:172.18.3.0/24 controlnet2 = core1:172.19.1.0/24 core2:172.19.2.0/24 core3:172.19.3.0/24 ``` -This will activate the primary and two auxiliary control networks and add interfaces *ctrl0*, *ctrl1*, *ctrl2* to each node. One use case would be to assign *ctrl1* to the OTA manager device and *ctrl2* to the Event Service device in the EMANE Options dialog box and leave *ctrl0* for CORE control traffic. +This will activate the primary and two auxiliary control networks and add +interfaces *ctrl0*, *ctrl1*, *ctrl2* to each node. One use case would be to +assign *ctrl1* to the OTA manager device and *ctrl2* to the Event Service +device in the EMANE Options dialog box and leave *ctrl0* for CORE control +traffic. -**NOTE: *controlnet0* may be used in place of *controlnet* to configure the primary control network.** +> :warning: *controlnet0* may be used in place of *controlnet* to configure +>the primary control network. -Unlike the primary control network, the auxiliary control networks will not employ tunneling since their primary purpose is for efficiently transporting multicast EMANE OTA and event traffic. Note that there is no per-session configuration for auxiliary control networks. +Unlike the primary control network, the auxiliary control networks will not +employ tunneling since their primary purpose is for efficiently transporting +multicast EMANE OTA and event traffic. Note that there is no per-session +configuration for auxiliary control networks. -To extend the auxiliary control networks across a distributed test environment, host network interfaces need to be added to them. The following lines in */etc/core/core.conf* will add host devices *eth1*, *eth2* and *eth3* to *controlnet1*, *controlnet2*, *controlnet3*: +To extend the auxiliary control networks across a distributed test +environment, host network interfaces need to be added to them. The following +lines in */etc/core/core.conf* will add host devices *eth1*, *eth2* and *eth3* +to *controlnet1*, *controlnet2*, *controlnet3*: ```shell controlnetif1 = eth1 @@ -81,7 +139,9 @@ controlnetif2 = eth2 controlnetif3 = eth3 ``` -**NOTE: There is no need to assign an interface to the primary control network because tunnels are formed between the master and the slaves using IP addresses that are provided in *servers.conf*.** +> :warning: There is no need to assign an interface to the primary control +>network because tunnels are formed between the master and the slaves using IP +>addresses that are provided in *servers.conf*. Shown below is a representative diagram of the configuration above. diff --git a/docs/devguide.md b/docs/devguide.md index 6e42c774..b6824128 100644 --- a/docs/devguide.md +++ b/docs/devguide.md @@ -5,8 +5,9 @@ ## Repository Overview -The CORE source consists of several different programming languages for historical reasons. -Current development focuses on the Python modules and daemon. Here is a brief description of the source directories. +The CORE source consists of several different programming languages for +historical reasons. Current development focuses on the Python modules and +daemon. Here is a brief description of the source directories. | Directory | Description | |---|---| @@ -14,14 +15,13 @@ Current development focuses on the Python modules and daemon. Here is a brief de |docs|Markdown Documentation currently hosted on GitHub| |gui|Tcl/Tk GUI| |man|Template files for creating man pages for various CORE command line utilities| -|netns|Python C extension modules for creating CORE containers| +|netns|C program for creating CORE containers| |scripts|Template files used for running CORE as a service| ## Getting started To setup CORE for develop we will leverage to automated install script. - ## Clone CORE Repo ```shell diff --git a/docs/distributed.md b/docs/distributed.md index b6ef9f77..f36efc72 100644 --- a/docs/distributed.md +++ b/docs/distributed.md @@ -9,21 +9,22 @@ A large emulation scenario can be deployed on multiple emulation servers and controlled by a single GUI. The GUI, representing the entire topology, can be run on one of the emulation servers or on a separate machine. -Each machine that will act as an emulation will require the installation of a distributed CORE package and -some configuration to allow SSH as root. +Each machine that will act as an emulation will require the installation of a +distributed CORE package and some configuration to allow SSH as root. ## Configuring SSH -Distributed CORE works using the python fabric library to run commands on remote servers over SSH. +Distributed CORE works using the python fabric library to run commands on +remote servers over SSH. ### Remote GUI Terminals -You need to have the same user defined on each server, since the user used +You need to have the same user defined on each server, since the user used for these remote shells is the same user that is running the CORE GUI. **Edit -> Preferences... -> Terminal program:** -Currently recommend setting this to **xterm -e** as the default +Currently recommend setting this to **xterm -e** as the default **gnome-terminal** will not work. May need to install xterm if, not already installed. @@ -34,7 +35,8 @@ sudo apt install xterm ### Distributed Server SSH Configuration -First the distributed servers must be configured to allow passwordless root login over SSH. +First the distributed servers must be configured to allow passwordless root +login over SSH. On distributed server: ```shelll @@ -48,7 +50,7 @@ vi /etc/ssh/sshd_config PermitRootLogin yes PasswordAuthentication yes -# if desired add/modify the following line to allow SSH to +# if desired add/modify the following line to allow SSH to # accept all env variables AcceptEnv * @@ -72,7 +74,7 @@ sudo vi /etc/fabric.yml # set configuration connect_kwargs: {"key_filename": "/home/user/.ssh/core"} -``` +``` On distributed server: ```shell @@ -125,7 +127,7 @@ the **all nodes** button. Servers that have assigned nodes are shown in blue in the server list. Another option is to first select a subset of nodes, then open the **CORE emulation servers** box and use the **selected nodes** button. -**IMPORTANT: Leave the nodes unassigned if they are to be run on the master +**IMPORTANT: Leave the nodes unassigned if they are to be run on the master server. Do not explicitly assign the nodes to the master server.** ## GUI Visualization @@ -137,23 +139,23 @@ will draw the link with a dashed line. Wireless nodes, i.e. those connected to a WLAN node, can be assigned to different emulation servers and participate in the same wireless network -only if an EMANE model is used for the WLAN. The basic range model does -not work across multiple servers due to the Linux bridging and ebtables +only if an EMANE model is used for the WLAN. The basic range model does +not work across multiple servers due to the Linux bridging and ebtables rules that are used. **NOTE: The basic range wireless model does not support distributed emulation, but EMANE does.** - -When nodes are linked across servers **core-daemons** will automatically + +When nodes are linked across servers **core-daemons** will automatically create necessary tunnels between the nodes when executed. Care should be taken -to arrange the topology such that the number of tunnels is minimized. The +to arrange the topology such that the number of tunnels is minimized. The tunnels carry data between servers to connect nodes as specified in the topology. These tunnels are created using GRE tunneling, similar to the Tunnel Tool. ### EMANE Configuration and Issues EMANE needs to have controlnet configured in **core.conf** in order to startup correctly. -The names before the addresses need to match the servers configured in +The names before the addresses need to match the servers configured in **~/.core/servers.conf** previously. ```shell diff --git a/docs/emane.md b/docs/emane.md index d0e8b11a..03039db0 100644 --- a/docs/emane.md +++ b/docs/emane.md @@ -5,24 +5,55 @@ ## What is EMANE? -The Extendable Mobile Ad-hoc Network Emulator (EMANE) allows heterogeneous network emulation using a pluggable MAC and PHY layer architecture. The EMANE framework provides an implementation architecture for modeling different radio interface types in the form of *Network Emulation Modules* (NEMs) and incorporating these modules into a real-time emulation running in a distributed environment. +The Extendable Mobile Ad-hoc Network Emulator (EMANE) allows heterogeneous +network emulation using a pluggable MAC and PHY layer architecture. The +EMANE framework provides an implementation architecture for modeling +different radio interface types in the form of *Network Emulation Modules* +(NEMs) and incorporating these modules into a real-time emulation running +in a distributed environment. -EMANE is developed by U.S. Naval Research Labs (NRL) Code 5522 and Adjacent Link LLC, who maintain these websites: +EMANE is developed by U.S. Naval Research Labs (NRL) Code 5522 and Adjacent +Link LLC, who maintain these websites: * * -Instead of building Linux Ethernet bridging networks with CORE, higher-fidelity wireless networks can be emulated using EMANE bound to virtual devices. CORE emulates layers 3 and above (network, session, application) with its virtual network stacks and process space for protocols and applications, while EMANE emulates layers 1 and 2 (physical and data link) using its pluggable PHY and MAC models. +Instead of building Linux Ethernet bridging networks with CORE, +higher-fidelity wireless networks can be emulated using EMANE bound to virtual +devices. CORE emulates layers 3 and above (network, session, application) with +its virtual network stacks and process space for protocols and applications, +while EMANE emulates layers 1 and 2 (physical and data link) using its +pluggable PHY and MAC models. -The interface between CORE and EMANE is a TAP device. CORE builds the virtual node using Linux network namespaces, installs the TAP device into the namespace and instantiates one EMANE process in the namespace. The EMANE process binds a user space socket to the TAP device for sending and receiving data from CORE. +The interface between CORE and EMANE is a TAP device. CORE builds the virtual +node using Linux network namespaces, installs the TAP device into the namespace +and instantiates one EMANE process in the namespace. The EMANE process binds a +user space socket to the TAP device for sending and receiving data from CORE. -An EMANE instance sends and receives OTA (Over-The-Air) traffic to and from other EMANE instances via a control port (e.g. *ctrl0*, *ctrl1*). It also sends and receives Events to and from the Event Service using the same or a different control port. EMANE models are configured through CORE's WLAN configuration dialog. A corresponding EmaneModel Python class is sub-classed for each supported EMANE model, to provide configuration items and their mapping to XML files. This way new models can be easily supported. When CORE starts the emulation, it generates the appropriate XML files that specify the EMANE NEM configuration, and launches the EMANE daemons. +An EMANE instance sends and receives OTA (Over-The-Air) traffic to and from +other EMANE instances via a control port (e.g. *ctrl0*, *ctrl1*). It also +sends and receives Events to and from the Event Service using the same or a +different control port. EMANE models are configured through CORE's WLAN +configuration dialog. A corresponding EmaneModel Python class is sub-classed +for each supported EMANE model, to provide configuration items and their +mapping to XML files. This way new models can be easily supported. When +CORE starts the emulation, it generates the appropriate XML files that +specify the EMANE NEM configuration, and launches the EMANE daemons. -Some EMANE models support location information to determine when packets should be dropped. EMANE has an event system where location events are broadcast to all NEMs. CORE can generate these location events when nodes are moved on the canvas. The canvas size and scale dialog has controls for mapping the X,Y coordinate system to a latitude, longitude geographic system that EMANE uses. When specified in the *core.conf* configuration file, CORE 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. +Some EMANE models support location information to determine when packets +should be dropped. EMANE has an event system where location events are +broadcast to all NEMs. CORE can generate these location events when nodes +are moved on the canvas. The canvas size and scale dialog has controls for +mapping the X,Y coordinate system to a latitude, longitude geographic system +that EMANE uses. When specified in the *core.conf* configuration file, CORE +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 Configuration -The CORE configuration file */etc/core/core.conf* has options specific to EMANE. An example emane section from the *core.conf* file is shown below: +The CORE configuration file */etc/core/core.conf* has options specific to +EMANE. An example emane section from the *core.conf* file is shown below: ```shell # EMANE configuration @@ -35,7 +66,8 @@ emane_log_level = 2 emane_realtime = True ``` -EMANE can be installed from deb or RPM packages or from source. See the [EMANE GitHub](https://github.com/adjacentlink/emane) for full details. +EMANE can be installed from deb or RPM packages or from source. See the +[EMANE GitHub](https://github.com/adjacentlink/emane) for full details. Here are quick instructions for installing all EMANE packages: @@ -47,15 +79,20 @@ tar xzf emane-1.2.1-release-1.ubuntu-16_04.amd64.tar.gz sudo dpkg -i emane-1.2.1-release-1/deb/ubuntu-16_04/amd64/*.deb ``` -If you have an EMANE event generator (e.g. mobility or pathloss scripts) and want to have CORE subscribe to EMANE location events, set the following line in the */etc/core/core.conf* configuration file: +If you have an EMANE event generator (e.g. mobility or pathloss scripts) and +want to have CORE subscribe to EMANE location events, set the following line +in the */etc/core/core.conf* configuration file: ```shell emane_event_monitor = True ``` -Do not set the above option to True if you want to manually drag nodes around on the canvas to update their location in EMANE. +Do not set the above option to True if you want to manually drag nodes around +on the canvas to update their location in EMANE. -Another common issue is if installing EMANE from source, the default configure prefix will place the DTD files in */usr/local/share/emane/dtd* while CORE expects them in */usr/share/emane/dtd*. +Another common issue is if installing EMANE from source, the default configure +prefix will place the DTD files in */usr/local/share/emane/dtd* while CORE +expects them in */usr/share/emane/dtd*. A symbolic link will fix this: @@ -65,28 +102,66 @@ sudo ln -s /usr/local/share/emane /usr/share/emane ## Custom EMANE Models -CORE supports custom developed EMANE models by way of dynamically loading user created python files that represent the model. Custom EMANE models should be placed within the path defined by **emane_models_dir** in the CORE configuration file. This path cannot end in **/emane**. +CORE supports custom developed EMANE models by way of dynamically loading user +created python files that represent the model. Custom EMANE models should be +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) ## Single PC with EMANE -This section describes running CORE and EMANE on a single machine. This is the default mode of operation when building an EMANE network with CORE. The OTA manager and Event service interface are set to use *ctrl0* and the virtual nodes use the primary control channel for communicating with one another. The primary control channel is automatically activated when a scenario involves EMANE. Using the primary control channel prevents your emulation session from sending multicast traffic on your local network and interfering with other EMANE users. +This section describes running CORE and EMANE on a single machine. This is the +default mode of operation when building an EMANE network with CORE. The OTA +manager and Event service interface are set to use *ctrl0* and the virtual +nodes use the primary control channel for communicating with one another. The +primary control channel is automatically activated when a scenario involves +EMANE. Using the primary control channel prevents your emulation session from +sending multicast traffic on your local network and interfering with other +EMANE users. -EMANE is configured through a WLAN node, because it is all about emulating wireless radio networks. Once a node is linked to a WLAN cloud configured with an EMANE model, the radio interface on that node may also be configured separately (apart from the cloud.) +EMANE is configured through a WLAN node, because it is all about emulating +wireless radio networks. Once a node is linked to a WLAN cloud configured +with an EMANE model, the radio interface on that node may also be configured +separately (apart from the cloud.) -Double-click on a WLAN node to invoke the WLAN configuration dialog. Click the *EMANE* tab; when EMANE has been properly installed, EMANE wireless modules should be listed in the *EMANE Models* list. (You may need to restart the CORE daemon if it was running prior to installing the EMANE Python bindings.) Click on a model name to enable it. +Double-click on a WLAN node to invoke the WLAN configuration dialog. Click +the *EMANE* tab; when EMANE has been properly installed, EMANE wireless modules +should be listed in the *EMANE Models* list. (You may need to restart the +CORE daemon if it was running prior to installing the EMANE Python bindings.) +Click on a model name to enable it. -When an EMANE model is selected in the *EMANE Models* list, clicking on the *model options* button causes the GUI to query the CORE daemon for configuration items. Each model will have different parameters, refer to the EMANE documentation for an explanation of each item. The defaults values are presented in the dialog. Clicking *Apply* and *Apply* again will store the EMANE model selections. +When an EMANE model is selected in the *EMANE Models* list, clicking on the +*model options* button causes the GUI to query the CORE daemon for +configuration items. Each model will have different parameters, refer to the +EMANE documentation for an explanation of each item. The defaults values are +presented in the dialog. Clicking *Apply* and *Apply* again will store the +EMANE model selections. -The *EMANE options* button allows specifying some global parameters for EMANE, some of which are necessary for distributed operation. +The *EMANE options* button allows specifying some global parameters for +EMANE, some of which are necessary for distributed operation. -The RF-PIPE and IEEE 802.11abg models use a Universal PHY that supports geographic location information for determining pathloss between nodes. A default latitude and longitude location is provided by CORE and this location-based pathloss is enabled by default; this is the *pathloss mode* setting for the Universal PHY. Moving a node on the canvas while the emulation is running generates location events for EMANE. To view or change the geographic location or scale of the canvas use the *Canvas Size and Scale* dialog available from the *Canvas* menu. +The RF-PIPE and IEEE 802.11abg models use a Universal PHY that supports +geographic location information for determining pathloss between nodes. A +default latitude and longitude location is provided by CORE and this +location-based pathloss is enabled by default; this is the *pathloss mode* +setting for the Universal PHY. Moving a node on the canvas while the +emulation is running generates location events for EMANE. To view or change +the geographic location or scale of the canvas use the *Canvas Size and Scale* +dialog available from the *Canvas* menu. -Note that conversion between geographic and Cartesian coordinate systems is done using UTM (Universal Transverse Mercator) projection, where different zones of 6 degree longitude bands are defined. The location events generated by CORE may become inaccurate near the zone boundaries for very large scenarios that span multiple UTM zones. It is recommended that EMANE location scripts be used to achieve geo-location accuracy in this situation. +Note that conversion between geographic and Cartesian coordinate systems is +done using UTM (Universal Transverse Mercator) projection, where different +zones of 6 degree longitude bands are defined. The location events generated +by CORE may become inaccurate near the zone boundaries for very large scenarios +that span multiple UTM zones. It is recommended that EMANE location scripts be +used to achieve geo-location accuracy in this situation. -Clicking the green *Start* button launches the emulation and causes TAP devices to be created in the virtual nodes that are linked to the EMANE WLAN. These devices appear with interface names such as eth0, eth1, etc. The EMANE processes should now be running in each namespace. For a four node scenario: +Clicking the green *Start* button launches the emulation and causes TAP devices +to be created in the virtual nodes that are linked to the EMANE WLAN. These +devices appear with interface names such as eth0, eth1, etc. The EMANE processes +should now be running in each namespace. For a four node scenario: ```shell ps -aef | grep emane @@ -96,30 +171,60 @@ root 1179 942 0 11:46 ? 00:00:00 emane -d --logl 3 -r -f /tmp/pycore.59992/eman root 1239 979 0 11:46 ? 00:00:00 emane -d --logl 3 -r -f /tmp/pycore.59992/emane5.log /tmp/pycore.59992/platform5.xml ``` -The example above shows the EMANE processes started by CORE. To view the configuration generated by CORE, look in the */tmp/pycore.nnnnn/* session directory for a *platform.xml* file and other XML files. One easy way to view this information is by double-clicking one of the virtual nodes, and typing *cd ..* in the shell to go up to the session directory. +The example above shows the EMANE processes started by CORE. To view the +configuration generated by CORE, look in the */tmp/pycore.nnnnn/* session +directory for a *platform.xml* file and other XML files. One easy way to view +this information is by double-clicking one of the virtual nodes, and typing +*cd ..* in the shell to go up to the session directory. ![](static/single-pc-emane.png) ## Distributed EMANE -Running CORE and EMANE distributed among two or more emulation servers is similar to running on a single machine. There are a few key configuration items that need to be set in order to be successful, and those are outlined here. +Running CORE and EMANE distributed among two or more emulation servers is +similar to running on a single machine. There are a few key configuration +items that need to be set in order to be successful, and those are outlined here. -It is a good idea to maintain separate networks for data (OTA) and control. The control network may be a shared laboratory network, for example, and you do not want multicast traffic on the data network to interfere with other EMANE users. Furthermore, control traffic could interfere with the OTA latency and thoughput and might affect emulation fidelity. The examples described here will use *eth0* as a control interface and *eth1* as a data interface, although using separate interfaces is not strictly required. Note that these interface names refer to interfaces present on the host machine, not virtual interfaces within a node. +It is a good idea to maintain separate networks for data (OTA) and control. +The control network may be a shared laboratory network, for example, and you do +not want multicast traffic on the data network to interfere with other EMANE +users. Furthermore, control traffic could interfere with the OTA latency and +throughput and might affect emulation fidelity. The examples described here will +use *eth0* as a control interface and *eth1* as a data interface, although +using separate interfaces is not strictly required. Note that these interface +names refer to interfaces present on the host machine, not virtual interfaces +within a node. -**IMPORTANT: If an auxiliary control network is used, an interface on the host has to be assigned to that network.** +**IMPORTANT: If an auxiliary control network is used, an interface on the host +has to be assigned to that network.** -Each machine that will act as an emulation server needs to have CORE and EMANE installed. +Each machine that will act as an emulation server needs to have CORE and EMANE +installed. -The IP addresses of the available servers are configured from the CORE emulation servers dialog box (choose *Session* then *Emulation servers...*). This list of servers is stored in a *~/.core/servers.conf* file. The dialog shows available servers, some or all of which may be assigned to nodes on the canvas. +The IP addresses of the available servers are configured from the CORE emulation +servers dialog box (choose *Session* then *Emulation servers...*). This list of +servers is stored in a *~/.core/servers.conf* file. The dialog shows available +servers, some or all of which may be assigned to nodes on the canvas. -Nodes need to be assigned to emulation servers. Select several nodes, right-click them, and choose *Assign to* and the name of the desired server. When a node is not assigned to any emulation server, it will be emulated locally. The local machine that the GUI connects with is considered the "master" machine, which in turn connects to the other emulation server "slaves". Public key SSH should be configured from the master to the slaves. +Nodes need to be assigned to emulation servers. Select several nodes, +right-click them, and choose *Assign to* and the name of the desired server. +When a node is not assigned to any emulation server, it will be emulated +locally. The local machine that the GUI connects with is considered the +"master" machine, which in turn connects to the other emulation server +"slaves". Public key SSH should be configured from the master to the slaves. -Under the *EMANE* tab of the EMANE WLAN, click on the *EMANE options* button. This brings up the emane configuration dialog. The *enable OTA Manager channel* should be set to *on*. The *OTA Manager device* and *Event Service device* should be set to a control network device. For example, if you have a primary and auxiliary control network (i.e. controlnet and controlnet1), and you want the OTA traffic to have its dedicated network, set the OTA Manager device to *ctrl1* and the Event Service device to *ctrl0*. The EMANE models can be configured. Click *Apply* to save these settings. +Under the *EMANE* tab of the EMANE WLAN, click on the *EMANE options* button. +This brings up the emane configuration dialog. The *enable OTA Manager channel* +should be set to *on*. The *OTA Manager device* and *Event Service device* +should be set to a control network device. For example, if you have a primary +and auxiliary control network (i.e. controlnet and controlnet1), and you want +the OTA traffic to have its dedicated network, set the OTA Manager device to +*ctrl1* and the Event Service device to *ctrl0*. The EMANE models can be +configured. Click *Apply* to save these settings. ![](static/distributed-emane-configuration.png) -**HINT:** - Here is a quick checklist for distributed emulation with EMANE. +> :bulb: Here is a quick checklist for distributed emulation with EMANE. 1. Follow the steps outlined for normal CORE. 2. Under the *EMANE* tab of the EMANE WLAN, click on *EMANE options*. @@ -132,10 +237,22 @@ Under the *EMANE* tab of the EMANE WLAN, click on the *EMANE options* button. Th 6. Press the *Start* button to launch the distributed emulation. -Now when the Start button is used to instantiate the emulation, the local CORE Python daemon will connect to other emulation servers that have been assigned to nodes. Each server will have its own session directory where the *platform.xml* file and other EMANE XML files are generated. The NEM IDs are automatically coordinated across servers so there is no overlap. Each server also gets its own Platform ID. +Now when the Start button is used to instantiate the emulation, the local CORE +Python daemon will connect to other emulation servers that have been assigned +to nodes. Each server will have its own session directory where the +*platform.xml* file and other EMANE XML files are generated. The NEM IDs are +automatically coordinated across servers so there is no overlap. Each server +also gets its own Platform ID. -An Ethernet device is used for disseminating multicast EMANE events, as specified in the *configure emane* dialog. EMANE's Event Service can be run with mobility or pathloss scripts as described in :ref:`Single_PC_with_EMANE`. If CORE is not subscribed to location events, it will generate them as nodes are moved on the canvas. +An Ethernet device is used for disseminating multicast EMANE events, as +specified in the *configure emane* dialog. EMANE's Event Service can be run +with mobility or pathloss scripts as described in :ref:`Single_PC_with_EMANE`. +If CORE is not subscribed to location events, it will generate them as nodes +are moved on the canvas. -Double-clicking on a node during runtime will cause the GUI to attempt to SSH to the emulation server for that node and run an interactive shell. The public key SSH configuration should be tested with all emulation servers prior to starting the emulation. +Double-clicking on a node during runtime will cause the GUI to attempt to SSH +to the emulation server for that node and run an interactive shell. The public +key SSH configuration should be tested with all emulation servers prior to +starting the emulation. ![](static/distributed-emane-network.png) diff --git a/docs/grpc.md b/docs/grpc.md index 46149c54..2430f3ae 100644 --- a/docs/grpc.md +++ b/docs/grpc.md @@ -4,7 +4,8 @@ gRPC is the main API for interfacing with CORE. ## HTTP Proxy -Since gRPC is HTTP2 based, proxy configurations can cause issue. Clear out your proxy when running if needed. +Since gRPC is HTTP2 based, proxy configurations can cause issue. Clear out your +proxy when running if needed. ## Python Client @@ -14,7 +15,6 @@ Below is a small example using it. ```python import logging -from builtins import range from core.api.grpc import client, core_pb2 diff --git a/docs/gui.md b/docs/gui.md new file mode 100644 index 00000000..ac7382e3 --- /dev/null +++ b/docs/gui.md @@ -0,0 +1,738 @@ + +# Using the CORE GUI + +* Table of Contents +{:toc} + +The following image shows the CORE GUI: +![](static/core_screenshot.png) + + +## Overview + +The GUI is used to draw nodes and network devices on a canvas, linking them +together to create an emulated network session. + +After pressing the start button, CORE will proceed through these phases, +staying in the **runtime** phase. After the session is stopped, CORE will +proceed to the **data collection** phase before tearing down the emulated +state. + +CORE can be customized to perform any action at each phase in the workflow +above. See the *Hooks...* entry on the **Session Menu** for details about +when these session states are reached. + +## Prerequisites + +Beyond installing CORE, you must have the CORE daemon running. This is done +on the command line with either systemd or sysv. + +```shell +# systemd +sudo systemctl daemon-reload +sudo systemctl start core-daemon + +# sysv +sudo service core-daemon start +``` + +You can also invoke the daemon directly from the command line, which can be +useful if you'd like to see the logging output directly. + +```shell +# direct invocation +sudo core-daemon +``` + +## Modes of Operation + +The CORE GUI has two primary modes of operation, **Edit** and **Execute** +modes. Running the GUI, by typing **core-gui** with no options, starts in +Edit mode. Nodes are drawn on a blank canvas using the toolbar on the left +and configured from right-click menus or by double-clicking them. The GUI +does not need to be run as root. + +Once editing is complete, pressing the green **Start** button (or choosing +**Execute** from the **Session** menu) instantiates the topology within the +Linux kernel and enters Execute mode. In execute mode, the user can interact +with the running emulated machines by double-clicking or right-clicking on +them. The editing toolbar disappears and is replaced by an execute toolbar, +which provides tools while running the emulation. Pressing the red **Stop** +button (or choosing **Terminate** from the **Session** menu) will destroy +the running emulation and return CORE to Edit mode. + +CORE can be started directly in Execute mode by specifying **--start** and a +topology file on the command line: + +```shell +core-gui --start ~/.core/configs/myfile.imn +``` + +Once the emulation is running, the GUI can be closed, and a prompt will appear +asking if the emulation should be terminated. The emulation may be left +running and the GUI can reconnect to an existing session at a later time. + +The GUI can be run as a normal user on Linux. + +The GUI can be connected to a different address or TCP port using the +**--address** and/or **--port** options. The defaults are shown below. + +```shell +core-gui --address 127.0.0.1 --port 4038 +``` + +## Toolbar + +The toolbar is a row of buttons that runs vertically along the left side of the +CORE GUI window. The toolbar changes depending on the mode of operation. + +### Editing Toolbar + +When CORE is in Edit mode (the default), the vertical Editing Toolbar exists on +the left side of the CORE window. Below are brief descriptions for each toolbar +item, starting from the top. Most of the tools are grouped into related +sub-menus, which appear when you click on their group icon. + +| Icon | Name | Description | +|---|---|---| +| ![](static/gui/select.gif) | Selection Tool | Tool for selecting, moving, configuring nodes. | +| ![](static/gui/start.gif) | Start Button | Starts Execute mode, instantiates the emulation. | +| ![](static/gui/link.gif) | Link | Allows network links to be drawn between two nodes by clicking and dragging the mouse. | + +### CORE Nodes + +These nodes will create a new node container and run associated services. + +| Icon | Name | Description | +|---|---|---| +| ![](static/gui/router.gif) | Router | Runs Quagga OSPFv2 and OSPFv3 routing to forward packets. | +| ![](static/gui/host.gif) | Host | Emulated server machine having a default route, runs SSH server. | +| ![](static/gui/pc.gif) | PC | Basic emulated machine having a default route, runs no processes by default. | +| ![](static/gui/mdr.gif) | MDR | Runs Quagga OSPFv3 MDR routing for MANET-optimized routing. | +| ![](static/gui/router_green.gif) | PRouter | Physical router represents a real testbed machine. | +| ![](static/gui/document-properties.gif) | Edit | Bring up the custom node dialog. | + +### Network Nodes + +These nodes are mostly used to create a Linux bridge that serves the +purpose described below. + +| Icon | Name | Description | +|---|---|---| +| ![](static/gui/hub.gif) | Hub | Ethernet hub forwards incoming packets to every connected node. | +| ![](static/gui/lanswitch.gif) | Switch | Ethernet switch intelligently forwards incoming packets to attached hosts using an Ethernet address hash table. | +| ![](static/gui/wlan.gif) | Wireless LAN | When routers are connected to this WLAN node, they join a wireless network and an antenna is drawn instead of a connecting line; the WLAN node typically controls connectivity between attached wireless nodes based on the distance between them. | +| ![](static/gui/rj45.gif) | RJ45 | RJ45 Physical Interface Tool, emulated nodes can be linked to real physical interfaces; using this tool, real networks and devices can be physically connected to the live-running emulation. | +| ![](static/gui/tunnel.gif) | Tunnel | Tool allows connecting together more than one CORE emulation using GRE tunnels. | + +### Annotation Tools + +| Icon | Name | Description | +|---|---|---| +| ![](static/gui/marker.gif) | Marker | For drawing marks on the canvas. | +| ![](static/gui/oval.gif) | Oval | For drawing circles on the canvas that appear in the background. | +| ![](static/gui/rectangle.gif) | Rectangle | For drawing rectangles on the canvas that appear in the background. | +| ![](static/gui/text.gif) | Text | For placing text captions on the canvas. | + +### Execution Toolbar + +When the Start button is pressed, CORE switches to Execute mode, and the Edit +toolbar on the left of the CORE window is replaced with the Execution toolbar +Below are the items on this toolbar, starting from the top. + +| Icon | Name | Description | +|---|---|---| +| ![](static/gui/select.gif) | Selection Tool | In Execute mode, the Selection Tool can be used for moving nodes around the canvas, and double-clicking on a node will open a shell window for that node; right-clicking on a node invokes a pop-up menu of run-time options for that node. | +| ![](static/gui/stop.gif) | Stop Button | Stops Execute mode, terminates the emulation, returns CORE to edit mode. | +| ![](static/gui/observe.gif) | Observer Widgets Tool | Clicking on this magnifying glass icon invokes a menu for easily selecting an Observer Widget. The icon has a darker gray background when an Observer Widget is active, during which time moving the mouse over a node will pop up an information display for that node. | +| ![](static/gui/marker.gif) | Marker | For drawing freehand lines on the canvas, useful during demonstrations; markings are not saved. | +| ![](static/gui/twonode.gif) | Two-node Tool | Click to choose a starting and ending node, and run a one-time *traceroute* between those nodes or a continuous *ping -R* between nodes. The output is displayed in real time in a results box, while the IP addresses are parsed and the complete network path is highlighted on the CORE display. | +| ![](static/gui/run.gif) | Run Tool | This tool allows easily running a command on all or a subset of all nodes. A list box allows selecting any of the nodes. A text entry box allows entering any command. The command should return immediately, otherwise the display will block awaiting response. The *ping* command, for example, with no parameters, is not a good idea. The result of each command is displayed in a results box. The first occurrence of the special text "NODE" will be replaced with the node name. The command will not be attempted to run on nodes that are not routers, PCs, or hosts, even if they are selected. | + +## Menu + +The menubar runs along the top of the CORE GUI window and provides access to a +variety of features. Some of the menus are detachable, such as the *Widgets* +menu, by clicking the dashed line at the top. + +### File Menu + +The File menu contains options for manipulating the **.imn** Configuration +Files. Generally, these menu items should not be used in Execute mode. + +| Option | Description | +|---|---| +| New | This starts a new file with an empty canvas. | +| Open | Invokes the File Open dialog box for selecting a new **.imn** or XML file to open. You can change the default path used for this dialog in the Preferences Dialog. | +| Save | Saves the current topology. If you have not yet specified a file name, the Save As dialog box is invoked. | +| Save As XML | Invokes the Save As dialog box for selecting a new **.xml** file for saving the current configuration in the XML file. | +| Save As imn | Invokes the Save As dialog box for selecting a new **.imn** topology file for saving the current configuration. Files are saved in the *IMUNES network configuration* file. | +| Export Python script | Prints Python snippets to the console, for inclusion in a CORE Python script. | +| Execute XML or Python script | Invokes a File Open dialog box for selecting an XML file to run or a Python script to run and automatically connect to. If a Python script, the script must create a new CORE Session and add this session to the daemon's list of sessions in order for this to work. | +| Execute Python script with options | Invokes a File Open dialog box for selecting a Python script to run and automatically connect to. After a selection is made, a Python Script Options dialog box is invoked to allow for command-line options to be added. The Python script must create a new CORE Session and add this session to the daemon's list of sessions in order for this to work. | +| Open current file in editor | This opens the current topology file in the **vim** text editor. First you need to save the file. Once the file has been edited with a text editor, you will need to reload the file to see your changes. The text editor can be changed from the Preferences Dialog. | +| Print | This uses the Tcl/Tk postscript command to print the current canvas to a printer. A dialog is invoked where you can specify a printing command, the default being **lpr**. The postscript output is piped to the print command. | +| Save screenshot | Saves the current canvas as a postscript graphic file. | +| Recently used files | Above the Quit menu command is a list of recently use files, if any have been opened. You can clear this list in the Preferences dialog box. You can specify the number of files to keep in this list from the Preferences dialog. Click on one of the file names listed to open that configuration file. | +| Quit | The Quit command should be used to exit the CORE GUI. CORE may prompt for termination if you are currently in Execute mode. Preferences and the recently-used files list are saved. | + +### Edit Menu + +| Option | Description | +|---|---| +| Undo | Attempts to undo the last edit in edit mode. | +| Redo* | Attempts to redo an edit that has been undone. | +| Cut, Copy, Paste | Used to cut, copy, and paste a selection. When nodes are pasted, their node numbers are automatically incremented, and existing links are preserved with new IP addresses assigned. Services and their customizations are copied to the new node, but care should be taken as node IP addresses have changed with possibly old addresses remaining in any custom service configurations. Annotations may also be copied and pasted. +| Select All | Selects all items on the canvas. Selected items can be moved as a group. | +| Select Adjacent | Select all nodes that are linked to the already selected node(s). For wireless nodes this simply selects the WLAN node(s) that the wireless node belongs to. You can use this by clicking on a node and pressing CTRL+N to select the adjacent nodes. | +| Find... | Invokes the *Find* dialog box. The Find dialog can be used to search for nodes by name or number. Results are listed in a table that includes the node or link location and details such as IP addresses or link parameters. Clicking on a result will focus the canvas on that node or link, switching canvases if necessary. | +| Clear marker | Clears any annotations drawn with the marker tool. Also clears any markings used to indicate a node's status. | +| Preferences... | Invokes the Preferences dialog box. | + +### Canvas Menu + +The canvas menu provides commands for adding, removing, changing, and switching +to different editing canvases. + +| Option | Description | +|---|---| +| New | Creates a new empty canvas at the right of all existing canvases. | +| Manage... | Invokes the *Manage Canvases* dialog box, where canvases may be renamed and reordered, and you can easily switch to one of the canvases by selecting it. | +| Delete | Deletes the current canvas and all items that it contains. | +| Size/scale... | Invokes a Canvas Size and Scale dialog that allows configuring the canvas size, scale, and geographic reference point. The size controls allow changing the width and height of the current canvas, in pixels or meters. The scale allows specifying how many meters are equivalent to 100 pixels. The reference point controls specify the latitude, longitude, and altitude reference point used to convert between geographic and Cartesian coordinate systems. By clicking the *Save as default* option, all new canvases will be created with these properties. The default canvas size can also be changed in the Preferences dialog box. +| Wallpaper... | Used for setting the canvas background image. | +| Previous, Next, First, Last | Used for switching the active canvas to the first, last, or adjacent canvas. | + +### View Menu + +The View menu features items for controlling what is displayed on the drawing +canvas. + +| Option | Description | +|---|---| +| Show | Opens a submenu of items that can be displayed or hidden, such as interface names, addresses, and labels. Use these options to help declutter the display. These options are generally saved in the topology files, so scenarios have a more consistent look when copied from one computer to another. | +| Show hidden nodes | Reveal nodes that have been hidden. Nodes are hidden by selecting one or more nodes, right-clicking one and choosing *hide*. | +| Locked | Toggles locked view; when the view is locked, nodes cannot be moved around on the canvas with the mouse. This could be useful when sharing the topology with someone and you do not expect them to change things. | +| 3D GUI... | Launches a 3D GUI by running the command defined under Preferences, *3D GUI command*. This is typically a script that runs the SDT3D display. SDT is the Scripted Display Tool from NRL that is based on NASA's Java-based WorldWind virtual globe software. | +| Zoom In | Magnifies the display. You can also zoom in by clicking *zoom 100%* label in the status bar, or by pressing the **+** (plus) key. | +| Zoom Out | Reduces the size of the display. You can also zoom out by right-clicking *zoom 100%* label in the status bar or by pressing the **-** (minus) key. | + +### Tools Menu + +The tools menu lists different utility functions. + +| Option | Description | +|---|---| +| Autorearrange all | Automatically arranges all nodes on the canvas. Nodes having a greater number of links are moved to the center. This mode can continue to run while placing nodes. To turn off this autorearrange mode, click on a blank area of the canvas with the select tool, or choose this menu option again. | +| Autorearrange selected | Automatically arranges the selected nodes on the canvas. | +| Align to grid | Moves nodes into a grid formation, starting with the smallest-numbered node in the upper-left corner of the canvas, arranging nodes in vertical columns. | +| Traffic... | Invokes the CORE Traffic Flows dialog box, which allows configuring, starting, and stopping MGEN traffic flows for the emulation. | +| IP addresses... | Invokes the IP Addresses dialog box for configuring which IPv4/IPv6 prefixes are used when automatically addressing new interfaces. | +| MAC addresses... | Invokes the MAC Addresses dialog box for configuring the starting number used as the lowest byte when generating each interface MAC address. This value should be changed when tunneling between CORE emulations to prevent MAC address conflicts. | +| Build hosts file... | Invokes the Build hosts File dialog box for generating **/etc/hosts** file entries based on IP addresses used in the emulation. | +| Renumber nodes... | Invokes the Renumber Nodes dialog box, which allows swapping one node number with another in a few clicks. | +| Experimental... | Menu of experimental options, such as a tool to convert ns-2 scripts to IMUNES imn topologies, supporting only basic ns-2 functionality, and a tool for automatically dividing up a topology into partitions. | +| Topology generator | Opens a submenu of topologies to generate. You can first select the type of node that the topology should consist of, or routers will be chosen by default. Nodes may be randomly placed, aligned in grids, or various other topology patterns. All of the supported patterns are listed in the table below. | +| Debugger... | Opens the CORE Debugger window for executing arbitrary Tcl/Tk commands. | + +#### Topology Generator + +| Pattern | Description | +|---|---| +| Random | Nodes are randomly placed about the canvas, but are not linked together. This can be used in conjunction with a WLAN node to quickly create a wireless network. | +| Grid | Nodes are placed in horizontal rows starting in the upper-left corner, evenly spaced to the right; nodes are not linked to each other. | +| Connected Grid | Nodes are placed in an N x M (width and height) rectangular grid, and each node is linked to the node above, below, left and right of itself. | +| Chain | Nodes are linked together one after the other in a chain. | +| Star | One node is placed in the center with N nodes surrounding it in a circular pattern, with each node linked to the center node. | +| Cycle | Nodes are arranged in a circular pattern with every node connected to its neighbor to form a closed circular path. | +| Wheel | The wheel pattern links nodes in a combination of both Star and Cycle patterns. | +| Cube | Generate a cube graph of nodes. | +| Clique | Creates a clique graph of nodes, where every node is connected to every other node. | +| Bipartite | Creates a bipartite graph of nodes, having two disjoint sets of vertices. | + +### Widgets Menu + +Widgets are GUI elements that allow interaction with a running emulation. +Widgets typically automate the running of commands on emulated nodes to report +status information of some type and display this on screen. + +#### Periodic Widgets + +These Widgets are those available from the main *Widgets* menu. More than one +of these Widgets may be run concurrently. An event loop fires once every second +that the emulation is running. If one of these Widgets is enabled, its periodic +routine will be invoked at this time. Each Widget may have a configuration +dialog box which is also accessible from the *Widgets* menu. + +Here are some standard widgets: + +* *Adjacency* - displays router adjacency states for Quagga's OSPFv2 and OSPFv3 + routing protocols. A line is drawn from each router halfway to the router ID + of an adjacent router. The color of the line is based on the OSPF adjacency + state such as Two-way or Full. To learn about the different colors, see the + *Configure Adjacency...* menu item. The **vtysh** command is used to + dump OSPF neighbor information. + Only half of the line is drawn because each + router may be in a different adjacency state with respect to the other. +* *Throughput* - displays the kilobits-per-second throughput above each link, + using statistics gathered from the ng_pipe Netgraph node that implements each + link. If the throughput exceeds a certain threshold, the link will become + highlighted. For wireless nodes which broadcast data to all nodes in range, + the throughput rate is displayed next to the node and the node will become + circled if the threshold is exceeded. + +#### Observer Widgets + +These Widgets are available from the *Observer Widgets* submenu of the +*Widgets* menu, and from the Widgets Tool on the toolbar. Only one Observer Widget may +be used at a time. Mouse over a node while the session is running to pop up +an informational display about that node. + +Available Observer Widgets include IPv4 and IPv6 routing tables, socket +information, list of running processes, and OSPFv2/v3 neighbor information. + +Observer Widgets may be edited by the user and rearranged. Choosing *Edit...* +from the Observer Widget menu will invoke the Observer Widgets dialog. A list +of Observer Widgets is displayed along with up and down arrows for rearranging +the list. Controls are available for renaming each widget, for changing the +command that is run during mouse over, and for adding and deleting items from +the list. Note that specified commands should return immediately to avoid +delays in the GUI display. Changes are saved to a **widgets.conf** file in +the CORE configuration directory. + +### Session Menu + +The Session Menu has entries for starting, stopping, and managing sessions, +in addition to global options such as node types, comments, hooks, servers, +and options. + +| Option | Description | +|---|---| +| Start or Stop | This starts or stops the emulation, performing the same function as the green Start or red Stop button. | +| Change sessions... | Invokes the CORE Sessions dialog box containing a list of active CORE sessions in the daemon. Basic session information such as name, node count, start time, and a thumbnail are displayed. This dialog allows connecting to different sessions, shutting them down, or starting a new session. | +| Node types... | Invokes the CORE Node Types dialog, performing the same function as the Edit button on the Network-Layer Nodes toolbar. | +| Comments... | Invokes the CORE Session Comments window where optional text comments may be specified. These comments are saved at the top of the configuration file, and can be useful for describing the topology or how to use the network. | +| Hooks... | Invokes the CORE Session Hooks window where scripts may be configured for a particular session state. The session states are defined in the table right below. The top of the window has a list of configured hooks, and buttons on the bottom left allow adding, editing, and removing hook scripts. The new or edit button will open a hook script editing window. A hook script is a shell script invoked on the host (not within a virtual node). | +| Reset node positions | If you have moved nodes around using the mouse or by using a mobility module, choosing this item will reset all nodes to their original position on the canvas. The node locations are remembered when you first press the Start button. | +| Emulation servers... | Invokes the CORE emulation servers dialog for configuring. | +| Change Sessions... | Invokes the Sessions dialog for switching between different running sessions. This dialog is presented during startup when one or more sessions are already running. | +| Options... | Presents per-session options, such as the IPv4 prefix to be used, if any, for a control network the ability to preserve the session directory; and an on/off switch for SDT3D support. | + +#### Session States + +| State | Description | +|---|---| +| definition | Used by the GUI to tell the backend to clear any state. | +| configuration | When the user presses the *Start* button, node, link, and other configuration data is sent to the backend. This state is also reached when the user customizes a service. | +| instantiation | After configuration data has been sent, just before the nodes are created. | +| runtime | All nodes and networks have been built and are running. (This is the same state at which the previously-named *global experiment script* was run.) +| datacollect | The user has pressed the *Stop* button, but before services have been stopped and nodes have been shut down. This is a good time to collect log files and other data from the nodes. | +| shutdown | All nodes and networks have been shut down and destroyed. | + +### Help Menu + +| Option | Description | +|---|---| +| CORE Github (www) | Link to the CORE GitHub page. | +| CORE Documentation (www) | Lnk to the CORE Documentation page. | +| About | Invokes the About dialog box for viewing version information. | + +## Connecting with Physical Networks + +CORE's emulated networks run in real time, so they can be connected to live +physical networks. The RJ45 tool and the Tunnel tool help with connecting to +the real world. These tools are available from the *Link-layer nodes* menu. + +When connecting two or more CORE emulations together, MAC address collisions +should be avoided. CORE automatically assigns MAC addresses to interfaces when +the emulation is started, starting with **00:00:00:aa:00:00** and incrementing +the bottom byte. The starting byte should be changed on the second CORE machine +using the *MAC addresses...* option from the *Tools* menu. + +### RJ45 Tool + +The RJ45 node in CORE represents a physical interface on the real CORE machine. +Any real-world network device can be connected to the interface and communicate +with the CORE nodes in real time. + +The main drawback is that one physical interface is required for each +connection. When the physical interface is assigned to CORE, it may not be used +for anything else. Another consideration is that the computer or network that +you are connecting to must be co-located with the CORE machine. + +To place an RJ45 connection, click on the *Link-layer nodes* toolbar and select +the *RJ45 Tool* from the submenu. Click on the canvas near the node you want to +connect to. This could be a router, hub, switch, or WLAN, for example. Now +click on the *Link Tool* and draw a link between the RJ45 and the other node. +The RJ45 node will display "UNASSIGNED". Double-click the RJ45 node to assign a +physical interface. A list of available interfaces will be shown, and one may +be selected by double-clicking its name in the list, or an interface name may +be entered into the text box. + +> :warning: When you press the Start button to instantiate your topology, the + interface assigned to the RJ45 will be connected to the CORE topology. The + interface can no longer be used by the system. For example, if there was an + IP address assigned to the physical interface before execution, the address + will be removed and control given over to CORE. No IP address is needed; the + interface is put into promiscuous mode so it will receive all packets and + send them into the emulated world. + +Multiple RJ45 nodes can be used within CORE and assigned to the same physical +interface if 802.1x VLANs are used. This allows for more RJ45 nodes than +physical ports are available, but the (e.g. switching) hardware connected to +the physical port must support the VLAN tagging, and the available bandwidth +will be shared. + +You need to create separate VLAN virtual devices on the Linux host, +and then assign these devices to RJ45 nodes inside of CORE. The VLANning is +actually performed outside of CORE, so when the CORE emulated node receives a +packet, the VLAN tag will already be removed. + +Here are example commands for creating VLAN devices under Linux: + +```shell +ip link add link eth0 name eth0.1 type vlan id 1 +ip link add link eth0 name eth0.2 type vlan id 2 +ip link add link eth0 name eth0.3 type vlan id 3 +``` + +### Tunnel Tool + +The tunnel tool builds GRE tunnels between CORE emulations or other hosts. +Tunneling can be helpful when the number of physical interfaces is limited or +when the peer is located on a different network. Also a physical interface does +not need to be dedicated to CORE as with the RJ45 tool. + +The peer GRE tunnel endpoint may be another CORE machine or another +host that supports GRE tunneling. When placing a Tunnel node, initially +the node will display "UNASSIGNED". This text should be replaced with the IP +address of the tunnel peer. This is the IP address of the other CORE machine or +physical machine, not an IP address of another virtual node. + +> :warning: Be aware of possible MTU (Maximum Transmission Unit) issues with GRE devices. The *gretap* device + has an interface MTU of 1,458 bytes; when joined to a Linux bridge, the + bridge's MTU + becomes 1,458 bytes. The Linux bridge will not perform fragmentation for + large packets if other bridge ports have a higher MTU such as 1,500 bytes. + +The GRE key is used to identify flows with GRE tunneling. This allows multiple +GRE tunnels to exist between that same pair of tunnel peers. A unique number +should be used when multiple tunnels are used with the same peer. When +configuring the peer side of the tunnel, ensure that the matching keys are +used. + +Here are example commands for building the other end of a tunnel on a Linux +machine. In this example, a router in CORE has the virtual address +**10.0.0.1/24** and the CORE host machine has the (real) address +**198.51.100.34/24**. The Linux box +that will connect with the CORE machine is reachable over the (real) network +at **198.51.100.76/24**. +The emulated router is linked with the Tunnel Node. In the +Tunnel Node configuration dialog, the address **198.51.100.76** is entered, with +the key set to **1**. The gretap interface on the Linux box will be assigned +an address from the subnet of the virtual router node, +**10.0.0.2/24**. + +```shell +# these commands are run on the tunnel peer +sudo ip link add gt0 type gretap remote 198.51.100.34 local 198.51.100.76 key 1 +sudo ip addr add 10.0.0.2/24 dev gt0 +sudo ip link set dev gt0 up +``` + +Now the virtual router should be able to ping the Linux machine: + +```shell +# from the CORE router node +ping 10.0.0.2 +``` + +And the Linux machine should be able to ping inside the CORE emulation: + +```shell +# from the tunnel peer +ping 10.0.0.1 +``` + +To debug this configuration, **tcpdump** can be run on the gretap devices, or +on the physical interfaces on the CORE or Linux machines. Make sure that a +firewall is not blocking the GRE traffic. + +### Communicating with the Host Machine + +The host machine that runs the CORE GUI and/or daemon is not necessarily +accessible from a node. Running an X11 application on a node, for example, +requires some channel of communication for the application to connect with +the X server for graphical display. There are several different ways to +connect from the node to the host and vice versa. + +#### Control Network + +The quickest way to connect with the host machine through the primary control +network. + +With a control network, the host can launch an X11 application on a node. +To run an X11 application on the node, the **SSH** service can be enabled on +the node, and SSH with X11 forwarding can be used from the host to the node. + +```shell +# SSH from host to node n5 to run an X11 app +ssh -X 172.16.0.5 xclock +``` + +Note that the **coresendmsg** utility can be used for a node to send +messages to the CORE daemon running on the host (if the **listenaddr = 0.0.0.0** +is set in the **/etc/core/core.conf** file) to interact with the running +emulation. For example, a node may move itself or other nodes, or change +its icon based on some node state. + +#### Other Methods + +There are still other ways to connect a host with a node. The RJ45 Tool +can be used in conjunction with a dummy interface to access a node: + +```shell +sudo modprobe dummy numdummies=1 +``` + +A **dummy0** interface should appear on the host. Use the RJ45 tool assigned +to **dummy0**, and link this to a node in your scenario. After starting the +session, configure an address on the host. + +```shell +sudo ip link show type bridge +# determine bridge name from the above command +# assign an IP address on the same network as the linked node +sudo ip addr add 10.0.1.2/24 dev b.48304.34658 +``` + +In the example shown above, the host will have the address **10.0.1.2** and +the node linked to the RJ45 may have the address **10.0.1.1**. + +## Building Sample Networks + +### Wired Networks + +Wired networks are created using the *Link Tool* to draw a link between two +nodes. This automatically draws a red line representing an Ethernet link and +creates new interfaces on network-layer nodes. + +Double-click on the link to invoke the *link configuration* dialog box. Here +you can change the Bandwidth, Delay, Loss, and Duplicate +rate parameters for that link. You can also modify the color and width of the +link, affecting its display. + +Link-layer nodes are provided for modeling wired networks. These do not create +a separate network stack when instantiated, but are implemented using Linux bridging. +These are the hub, switch, and wireless LAN nodes. The hub copies each packet from +the incoming link to every connected link, while the switch behaves more like an +Ethernet switch and keeps track of the Ethernet address of the connected peer, +forwarding unicast traffic only to the appropriate ports. + +The wireless LAN (WLAN) is covered in the next section. + +### Wireless Networks + +The wireless LAN node allows you to build wireless networks where moving nodes +around affects the connectivity between them. Connection between a pair of nodes is stronger +when the nodes are closer while connection is weaker when the nodes are further away. +The wireless LAN, or WLAN, node appears as a small cloud. The WLAN offers +several levels of wireless emulation fidelity, depending on your modeling needs. + +The WLAN tool can be extended with plug-ins for different levels of wireless +fidelity. The basic on/off range is the default setting available on all +platforms. Other plug-ins offer higher fidelity at the expense of greater +complexity and CPU usage. The availability of certain plug-ins varies depending +on platform. See the table below for a brief overview of wireless model types. + + +|Model|Type|Supported Platform(s)|Fidelity|Description| +|-----|----|---------------------|--------|-----------| +|Basic|on/off|Linux|Low|Ethernet bridging with ebtables| +|EMANE|Plug-in|Linux|High|TAP device connected to EMANE emulator with pluggable MAC and PHY radio types| + +To quickly build a wireless network, you can first place several router nodes +onto the canvas. If you have the +Quagga MDR software installed, it is +recommended that you use the *mdr* node type for reduced routing overhead. Next +choose the *wireless LAN* from the *Link-layer nodes* submenu. First set the +desired WLAN parameters by double-clicking the cloud icon. Then you can link +all of the routers by right-clicking on the WLAN and choosing *Link to all +routers*. + +Linking a router to the WLAN causes a small antenna to appear, but no red link +line is drawn. Routers can have multiple wireless links and both wireless and +wired links (however, you will need to manually configure route +redistribution.) The mdr node type will generate a routing configuration that +enables OSPFv3 with MANET extensions. This is a Boeing-developed extension to +Quagga's OSPFv3 that reduces flooding overhead and optimizes the flooding +procedure for mobile ad-hoc (MANET) networks. + +The default configuration of the WLAN is set to use the basic range model, +using the *Basic* tab in the WLAN configuration dialog. Having this model +selected causes **core-daemon** to calculate the distance between nodes based +on screen pixels. A numeric range in screen pixels is set for the wireless +network using the *Range* slider. When two wireless nodes are within range of +each other, a green line is drawn between them and they are linked. Two +wireless nodes that are farther than the range pixels apart are not linked. +During Execute mode, users may move wireless nodes around by clicking and +dragging them, and wireless links will be dynamically made or broken. + +The *EMANE* tab lists available EMANE models to use for wireless networking. +See the [EMANE](emane.md) chapter for details on using EMANE. + +### Mobility Scripting + +CORE has a few ways to script mobility. + +| Option | Description | +|---|---| +| ns-2 script | The script specifies either absolute positions or waypoints with a velocity. Locations are given with Cartesian coordinates. | +| CORE API | An external entity can move nodes by sending CORE API Node messages with updated X,Y coordinates; the **coresendmsg** utility allows a shell script to generate these messages. | +| EMANE events | See [EMANE](emane.md) for details on using EMANE scripts to move nodes around. Location information is typically given as latitude, longitude, and altitude. | + +For the first method, you can create a mobility script using a text +editor, or using a tool such as [BonnMotion](http://net.cs.uni-bonn.de/wg/cs/applications/bonnmotion/), and associate the script with one of the wireless +using the WLAN configuration dialog box. Click the *ns-2 mobility script...* +button, and set the *mobility script file* field in the resulting *ns2script* +configuration dialog. + +Here is an example for creating a BonnMotion script for 10 nodes: + +```shell +bm -f sample RandomWaypoint -n 10 -d 60 -x 1000 -y 750 +bm NSFile -f sample +# use the resulting 'sample.ns_movements' file in CORE +``` + +When the Execute mode is started and one of the WLAN nodes has a mobility +script, a mobility script window will appear. This window contains controls for +starting, stopping, and resetting the running time for the mobility script. The +*loop* checkbox causes the script to play continuously. The *resolution* text +box contains the number of milliseconds between each timer event; lower values +cause the mobility to appear smoother but consumes greater CPU time. + +The format of an ns-2 mobility script looks like: + +```shell +# nodes: 3, max time: 35.000000, max x: 600.00, max y: 600.00 +$node_(2) set X_ 144.0 +$node_(2) set Y_ 240.0 +$node_(2) set Z_ 0.00 +$ns_ at 1.00 "$node_(2) setdest 130.0 280.0 15.0" +``` + +The first three lines set an initial position for node 2. The last line in the +above example causes node 2 to move towards the destination **(130, 280)** at +speed **15**. All units are screen coordinates, with speed in units per second. +The total script time is learned after all nodes have reached their waypoints. +Initially, the time slider in the mobility script dialog will not be +accurate. + +Examples mobility scripts (and their associated topology files) can be found +in the **configs/** directory. + +## Multiple Canvases + +CORE supports multiple canvases for organizing emulated nodes. Nodes running on +different canvases may be linked together. + +To create a new canvas, choose *New* from the *Canvas* menu. A new canvas tab +appears in the bottom left corner. Clicking on a canvas tab switches to that +canvas. Double-click on one of the tabs to invoke the *Manage Canvases* dialog +box. Here, canvases may be renamed and reordered, and you can easily switch to +one of the canvases by selecting it. + +Each canvas maintains its own set of nodes and annotations. To link between +canvases, select a node and right-click on it, choose *Create link to*, choose +the target canvas from the list, and from that submenu the desired node. A +pseudo-link will be drawn, representing the link between the two nodes on +different canvases. Double-clicking on the label at the end of the arrow will +jump to the canvas that it links. + +## Check Emulation Light (CEL) + +The |cel| Check Emulation Light, or CEL, is located in the bottom right-hand corner +of the status bar in the CORE GUI. This is a yellow icon that indicates one or +more problems with the running emulation. Clicking on the CEL will invoke the +CEL dialog. + +The Check Emulation Light dialog contains a list of exceptions received from +the CORE daemon. An exception has a time, severity level, optional node number, +and source. When the CEL is blinking, this indicates one or more fatal +exceptions. An exception with a fatal severity level indicates that one or more +of the basic pieces of emulation could not be created, such as failure to +create a bridge or namespace, or the failure to launch EMANE processes for an +EMANE-based network. + +Clicking on an exception displays details for that +exception. If a node number is specified, that node is highlighted on the +canvas when the exception is selected. The exception source is a text string +to help trace where the exception occurred; "service:UserDefined" for example, +would appear for a failed validation command with the UserDefined service. + +Buttons are available at the bottom of the dialog for clearing the exception +list and for viewing the CORE daemon and node log files. + +> :warning: In batch mode, exceptions received from the CORE daemon are displayed on + the console. + +## Configuration Files + +Configurations are saved to **xml** or **.imn** topology files using +the *File* menu. You +can easily edit these files with a text editor. +Any time you edit the topology +file, you will need to stop the emulation if it were running and reload the +file. + +The **.imn** file format comes from IMUNES, and is +basically Tcl lists of nodes, links, etc. +Tabs and spacing in the topology files are important. The file starts by +listing every node, then links, annotations, canvases, and options. Each entity +has a block contained in braces. The first block is indented by four spaces. +Within the **network-config** block (and any *custom-*-config* block), the +indentation is one tab character. + +> :bulb: There are several topology examples included with CORE in + the **configs/** directory. + This directory can be found in **~/.core/configs**, or + installed to the filesystem + under **/usr[/local]/share/examples/configs**. + +> :bulb: When using the **.imn** file format, file paths for things like custom + icons may contain the special variables **$CORE_DATA_DIR** or **$CONFDIR** which + will be substituted with **/usr/share/core** or **~/.core/configs**. + +> :bulb: Feel free to edit the files directly using your favorite text editor. + +## Customizing your Topology's Look + +Several annotation tools are provided for changing the way your topology is +presented. Captions may be added with the Text tool. Ovals and rectangles may +be drawn in the background, helpful for visually grouping nodes together. + +During live demonstrations the marker tool may be helpful for drawing temporary +annotations on the canvas that may be quickly erased. A size and color palette +appears at the bottom of the toolbar when the marker tool is selected. Markings +are only temporary and are not saved in the topology file. + +The basic node icons can be replaced with a custom image of your choice. Icons +appear best when they use the GIF or PNG format with a transparent background. +To change a node's icon, double-click the node to invoke its configuration +dialog and click on the button to the right of the node name that shows the +node's current icon. + +A background image for the canvas may be set using the *Wallpaper...* option +from the *Canvas* menu. The image may be centered, tiled, or scaled to fit the +canvas size. An existing terrain, map, or network diagram could be used as a +background, for example, with CORE nodes drawn on top. + +## Preferences + +The *Preferences* Dialog can be accessed from the **Edit_Menu**. There are +numerous defaults that can be set with this dialog, which are stored in the +**~/.core/prefs.conf** preferences file. + + + diff --git a/docs/index.md b/docs/index.md index cac44e69..f6f059d2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,17 +21,17 @@ networking scenarios, security studies, and increasing the size of physical test | Topic | Description| |-------|------------| |[Architecture](architecture.md)|Overview of the architecture| -|[Installation](install.md)|Installing from source, packages, & other dependencies| -|[Using the GUI](usage.md)|Details on the different node types and options in the GUI| -|[Distributed](distributed.md)|Overview and detals for running CORE across multiple servers| +|[Installation](install.md)|How to install CORE and its requirements| +|[GUI](gui.md)|How to use the GUI| +|[Distributed](distributed.md)|Details for running CORE across multiple servers| |[Python Scripting](scripting.md)|How to write python scripts for creating a CORE session| |[gRPC API](grpc.md)|How to enable and use the gRPC API| -|[Node Types](machine.md)|Overview of node types supported within CORE| +|[Node Types](nodetypes.md)|Overview of node types supported within CORE| |[CTRLNET](ctrlnet.md)|How to use control networks to communicate with nodes from host| |[Services](services.md)|Overview of provided services and creating custom ones| |[EMANE](emane.md)|Overview of EMANE integration and integrating custom EMANE models| |[Performance](performance.md)|Notes on performance when using CORE| -|[Developers Guide](devguide.md)|Overview of topics when developing CORE| +|[Developers Guide](devguide.md)|Overview on how to contribute to CORE| ## Credits @@ -45,4 +45,4 @@ Python framework and has made significant contributions. Claudiu Danilov, Rod Sa Phil Spagnolo, and Ian Chakeres have contributed code to CORE. Dan Mackley helped develop the CORE API, originally to interface with a simulator. Jae Kim and Tom Henderson have supervised the project and provided direction. -Copyright (c) 2005-2018, the Boeing Company. +Copyright (c) 2005-2020, the Boeing Company. diff --git a/docs/machine.md b/docs/machine.md deleted file mode 100644 index bd68d7e1..00000000 --- a/docs/machine.md +++ /dev/null @@ -1,22 +0,0 @@ -# CORE Node Types - -* Table of Contents -{:toc} - -## Overview - -Different node types can be configured in CORE, and each node type has a *machine type* that indicates how the node will be represented at run time. Different machine types allow for different virtualization options. - -## netns nodes - -The *netns* machine type is the default. This is for nodes that will be backed by Linux network namespaces. See :ref:`Linux` for a brief explanation of netns. This default machine type is very lightweight, providing a minimum amount of virtualization in order to emulate a network. Another reason this is designated as the default machine type is because this virtualization technology typically requires no changes to the kernel; it is available out-of-the-box from the latest mainstream Linux distributions. - -## physical nodes - -The *physical* machine type is used for nodes that represent a real Linux-based machine that will participate in the emulated network scenario. This is typically used, for example, to incorporate racks of server machines from an emulation testbed. A physical node is one that is running the CORE daemon (*core-daemon*), but will not be further partitioned into virtual machines. Services that are run on the physical node do not run in an isolated or virtualized environment, but directly on the operating system. - -Physical nodes must be assigned to servers, the same way nodes are assigned to emulation servers with *Distributed Emulation*. The list of available physical nodes currently shares the same dialog box and list as the emulation servers, accessed using the *Emulation Servers...* entry from the *Session* menu. - -Support for physical nodes is under development and may be improved in future releases. Currently, when any node is linked to a physical node, a dashed line is drawn to indicate network tunneling. A GRE tunneling interface will be created on the physical node and used to tunnel traffic to and from the emulated world. - -Double-clicking on a physical node during runtime opens a terminal with an SSH shell to that node. Users should configure public-key SSH login as done with emulation servers. diff --git a/docs/nodetypes.md b/docs/nodetypes.md new file mode 100644 index 00000000..41e7beb7 --- /dev/null +++ b/docs/nodetypes.md @@ -0,0 +1,45 @@ +# CORE Node Types + +* Table of Contents +{:toc} + +## Overview + +Different node types can be configured in CORE, and each node type has a +*machine type* that indicates how the node will be represented at run time. +Different machine types allow for different virtualization options. + +## Netns Nodes + +The *netns* machine type is the default. This is for nodes that will be +backed by Linux network namespaces. This default machine type is very +lightweight, providing a minimum amount of virtualization in order to +emulate a network. Another reason this is designated as the default +machine type is because this virtualization technology typically +requires no changes to the kernel; it is available out-of-the-box from the +latest mainstream Linux distributions. + +## Physical Nodes + +The *physical* machine type is used for nodes that represent a real Linux-based +machine that will participate in the emulated network scenario. This is +typically used, for example, to incorporate racks of server machines from an +emulation testbed. A physical node is one that is running the CORE daemon +(*core-daemon*), but will not be further partitioned into virtual machines. +Services that are run on the physical node do not run in an isolated or +virtualized environment, but directly on the operating system. + +Physical nodes must be assigned to servers, the same way nodes are assigned to +emulation servers with *Distributed Emulation*. The list of available physical +nodes currently shares the same dialog box and list as the emulation servers, +accessed using the *Emulation Servers...* entry from the *Session* menu. + +Support for physical nodes is under development and may be improved in future +releases. Currently, when any node is linked to a physical node, a dashed line +is drawn to indicate network tunneling. A GRE tunneling interface will be +created on the physical node and used to tunnel traffic to and from the +emulated world. + +Double-clicking on a physical node during runtime opens a terminal with an +SSH shell to that node. Users should configure public-key SSH login as done +with emulation servers. diff --git a/docs/performance.md b/docs/performance.md index d048eea1..d106b06e 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -5,9 +5,10 @@ ## Overview -The top question about the performance of CORE is often *how many nodes can it handle?* The answer depends on several factors: +The top question about the performance of CORE is often *how many nodes can it +handle?* The answer depends on several factors: -| Factor | How that factor might affect performance | +| Factor | Performance Impact | |---|---| | Hardware | the number and speed of processors in the computer, the available processor cache, RAM memory, and front-side bus speed may greatly affect overall performance. | | Operating system version | distribution of Linux and the specific kernel versions used will affect overall performance. | @@ -16,16 +17,30 @@ The top question about the performance of CORE is often *how many nodes can it h | GUI usage | widgets that run periodically, mobility scenarios, and other GUI interactions generally consume CPU cycles that may be needed for emulation. | -On a typical single-CPU Xeon 3.0GHz server machine with 2GB RAM running Linux, we have found it reasonable to run 30-75 nodes running OSPFv2 and OSPFv3 routing. On this hardware CORE can instantiate 100 or more nodes, but at that point it becomes critical as to what each of the nodes is doing. +On a typical single-CPU Xeon 3.0GHz server machine with 2GB RAM running Linux, +we have found it reasonable to run 30-75 nodes running OSPFv2 and OSPFv3 +routing. On this hardware CORE can instantiate 100 or more nodes, but at +that point it becomes critical as to what each of the nodes is doing. -Because this software is primarily a network emulator, the more appropriate question is *how much network traffic can it handle?* On the same 3.0GHz server described above, running Linux, about 300,000 packets-per-second can be pushed through the system. The number of hops and the size of the packets is less important. The limiting factor is the number of times that the operating system needs to handle a packet. The 300,000 pps figure represents the number of times the system as a whole needed to deal with a packet. As more network hops are added, this increases the number of context switches and decreases the throughput seen on the full length of the network path. +Because this software is primarily a network emulator, the more appropriate +question is *how much network traffic can it handle?* On the same 3.0GHz +server described above, running Linux, about 300,000 packets-per-second can +be pushed through the system. The number of hops and the size of the packets +is less important. The limiting factor is the number of times that the +operating system needs to handle a packet. The 300,000 pps figure represents +the number of times the system as a whole needed to deal with a packet. As +more network hops are added, this increases the number of context switches +and decreases the throughput seen on the full length of the network path. -**NOTE: The right question to be asking is *"how much traffic?"*, not *"how many nodes?"*.** +> :warning: The right question to be asking is *"how much traffic?"*, not +*"how many nodes?"*.** -For a more detailed study of performance in CORE, refer to the following publications: +For a more detailed study of performance in CORE, refer to the following +publications: -* J\. Ahrenholz, T. Goff, and B. Adamson, Integration of the CORE and EMANE Network Emulators, Proceedings of the IEEE Military Communications Conference 2011, November 2011. - -* Ahrenholz, J., Comparison of CORE Network Emulation Platforms, Proceedings of the IEEE Military Communications Conference 2010, pp. 864-869, November 2010. - -* J\. Ahrenholz, C. Danilov, T. Henderson, and J.H. Kim, CORE: A real-time network emulator, Proceedings of IEEE MILCOM Conference, 2008. +* J\. Ahrenholz, T. Goff, and B. Adamson, Integration of the CORE and EMANE + Network Emulators, Proceedings of the IEEE Military Communications Conference 2011, November 2011. +* Ahrenholz, J., Comparison of CORE Network Emulation Platforms, Proceedings + of the IEEE Military Communications Conference 2010, pp. 864-869, November 2010. +* J\. Ahrenholz, C. Danilov, T. Henderson, and J.H. Kim, CORE: A real-time + network emulator, Proceedings of IEEE MILCOM Conference, 2008. diff --git a/docs/scripting.md b/docs/scripting.md index 38eaa901..0c4f13f3 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -6,11 +6,24 @@ ## Overview -CORE can be used via the GUI or Python scripting. Writing your own Python scripts offers a rich programming environment with complete control over all aspects of the emulation. This chapter provides a brief introduction to scripting. Most of the documentation is available from sample scripts, or online via interactive Python. +Writing your own Python scripts offers a rich programming environment with +complete control over all aspects of the emulation. This chapter provides a +brief introduction to scripting. Most of the documentation is available from +sample scripts, or online via interactive Python. -The best starting point is the sample scripts that are included with CORE. If you have a CORE source tree, the example script files can be found under *core/daemon/examples/api/*. When CORE is installed from packages, the example script files will be in */usr/share/core/examples/api/* (or */usr/local/* prefix when installed from source.) For the most part, the example scripts are self-documenting; see the comments contained within the Python code. +The best starting point is the sample scripts that are included with CORE. +If you have a CORE source tree, the example script files can be found under +*core/daemon/examples/python/*. When CORE is installed from packages, the example +script files will be in */usr/share/core/examples/python/* (or */usr/local/* +prefix when installed from source.) For the most part, the example scripts are +self-documenting; see the comments contained within the Python code. -The scripts should be run with root privileges because they create new network namespaces. In general, a CORE Python script does not connect to the CORE daemon, in fact the *core-daemon* is just another Python script that uses the CORE Python modules and exchanges messages with the GUI. To connect the GUI to your scripts, see the included sample scripts that allow for GUI connections. +The scripts should be run with root privileges because they create new network +namespaces. In general, a CORE Python script does not connect to the CORE +daemon, in fact the *core-daemon* is just another Python script that uses +the CORE Python modules and exchanges messages with the GUI. To connect the +GUI to your scripts, see the included sample scripts that allow for GUI +connections. Here are the basic elements of a CORE Python script: @@ -46,15 +59,23 @@ session.instantiate() coreemu.shutdown() ``` -The above script creates a CORE session having two nodes connected with a switch. The first node pings the second node with 5 ping packets; the result is displayed on screen. +The above script creates a CORE session having two nodes connected with a +switch, Then immediately shutsdown. -A good way to learn about the CORE Python modules is via interactive Python. Scripts can be run using *python -i*. Cut and paste the simple script above and you will have two nodes connected by a hub, with one node running a test ping to the other. +The CORE Python modules are documented with comments in the code. From an +interactive Python shell, you can retrieve online help about the various +classes and methods; for example *help(CoreNode)* or *help(Session)*. -The CORE Python modules are documented with comments in the code. From an interactive Python shell, you can retrieve online help about the various classes and methods; for example *help(nodes.CoreNode)* or *help(Session)*. +> :warning: The CORE daemon *core-daemon* manages a list of sessions and allows +the GUI to connect and control sessions. Your Python script uses the same CORE +modules but runs independently of the daemon. The daemon does not need to be +running for your script to work. -**NOTE: The CORE daemon *core-daemon* manages a list of sessions and allows the GUI to connect and control sessions. Your Python script uses the same CORE modules but runs independently of the daemon. The daemon does not need to be running for your script to work.** - -The session created by a Python script may be viewed in the GUI if certain steps are followed. The GUI has a *File Menu*, *Execute Python script...* option for running a script and automatically connecting to it. Once connected, normal GUI interaction is possible, such as moving and double-clicking nodes, activating Widgets, etc. +The session created by a Python script may be viewed in the GUI if certain +steps are followed. The GUI has a *File Menu*, *Execute Python script...* +option for running a script and automatically connecting to it. Once connected, +normal GUI interaction is possible, such as moving and double-clicking nodes, +activating Widgets, etc. The script should have a line such as the following for running it from the GUI. @@ -63,21 +84,28 @@ if __name__ in ["__main__", "__builtin__"]: main() ``` -A script can add sessions to the core-daemon. A global *coreemu* variable is exposed to the script pointing to the *CoreEmu* object. -The example below has a fallback to a new CoreEmu object, in the case you would like to run the script standalone, outside of the core-daemon. +A script can add sessions to the core-daemon. A global *coreemu* variable is +exposed to the script pointing to the *CoreEmu* object. + +The example below has a fallback to a new CoreEmu object, in the case you would +like to run the script standalone, outside of the core-daemon. ```python coreemu = globals().get("coreemu", CoreEmu()) session = coreemu.create_session() ``` -Finally, nodes and networks need to have their coordinates set to something, otherwise they will be grouped at the coordinates *<0, 0>*. First sketching the topology in the GUI and then using the *Export Python script* option may help here. +Finally, nodes and networks need to have their coordinates set to something, +otherwise they will be grouped at the coordinates *<0, 0>*. First sketching +the topology in the GUI and then using the *Export Python script* option may +help here. ```python switch.setposition(x=80,y=50) ``` -A fully-worked example script that you can launch from the GUI is available in the examples directory. +A fully-worked example script that you can launch from the GUI is available +in the examples directory. ## Configuring Services @@ -87,17 +115,14 @@ Examples setting or configuring custom services for a node. # create session and node coreemu = CoreEmu() session = coreemu.create_session() -node = session.add_node() -# create and retrieve custom service -session.services.set_service(node.id, "ServiceName") -custom_service = session.services.get_service(node.id, "ServiceName") +# create node with custom services +options = NodeOptions() +options.services = ["ServiceName"] +node = session.add_node(options=options) # set custom file data session.services.set_service_file(node.id, "ServiceName", "FileName", "custom file data") - -# set services to a node, using custom services when defined -session.services.add_services(node, node.type, ["Service1", "Service2"]) ``` # Configuring EMANE Models diff --git a/docs/services.md b/docs/services.md index 8fcc5104..8e64e024 100644 --- a/docs/services.md +++ b/docs/services.md @@ -79,8 +79,7 @@ the service customization dialog for that service. The dialog has three tabs for configuring the different aspects of the service: files, directories, and startup/shutdown. -**NOTE:** - A **yellow** customize icon next to a service indicates that service +> :warning: A **yellow** customize icon next to a service indicates that service requires customization (e.g. the *Firewall* service). A **green** customize icon indicates that a custom configuration exists. Click the *Defaults* button when customizing a service to remove any @@ -99,8 +98,7 @@ per-node directories that are defined by the services. For example, the the Zebra service, because Quagga running on each node needs to write separate PID files to that directory. -**NOTE:** - The **/var/log** and **/var/run** directories are +> :warning: The **/var/log** and **/var/run** directories are mounted uniquely per-node by default. Per-node mount targets can be found in **/tmp/pycore.nnnnn/nN.conf/** (where *nnnnn* is the session number and *N* is the node number.) @@ -130,8 +128,7 @@ if a process is running and return zero when found. When a validate command produces a non-zero return value, an exception is generated, which will cause an error to be displayed in the Check Emulation Light. -**TIP:** - To start, stop, and restart services during run-time, right-click a +> :bulb: To start, stop, and restart services during run-time, right-click a node and use the *Services...* menu. ## New Services diff --git a/docs/static/gui/document-properties.gif b/docs/static/gui/document-properties.gif new file mode 100644 index 0000000000000000000000000000000000000000..732d8436455ba607ff30774ef83faa434789f775 GIT binary patch literal 635 zcmZ?wbhEHb6krfwI9A3`UtizZ-QL^V)6>`4+Ydxt6Z*URCUj4n)H`Wv|CDJHr_Go& zZN}v3v!=|LJ$2^nX|v}}pFMZR-1#%-Eu6hz(VT@#<}O+?Z}GAP%a$!&x_sgCm5Z0J zTCxI&Rxe$-dg-b)D^{&twiXE1tzNrs?b`LL*REf)X2YuWo7QaFym8yMZ98^v+r4}D z-hI3FAKZQ9*uj%$j-Nbx?EKkN7cQN-eEIUFs~4`{xO(Hp^_#bD+`N6`*6o|O?p(Wh z_u4HGx_egDy&`;YEE0HVit9zMSJ`0=BM zPwqW>djIj$2algUeDdt!vuDqqJb(1!`IDzF9=~}0?D@;5FJHcR`SRtfmoHwweE#Ou zi?^>|zJ2}b^_!P(-oASC_Vv5>Z$Eu}|LN1G&!0Yi`}Xzw_wPTxfBW&{`%fVH2}D1B z{`&dr*Uvvd@b}MOAo%z99|-;X_y6C25W)u(f3h%gG1N2YFaQB4P8is4G}PBC$jQn` zOH0W%^)$&PmQ9?P6RzIdBv~967UJ*YYhIu;UA!+K3@N%$caBBCo?vDZnK?>>} z4H6mGRzwyX@-nz_KC4-2cu1l_BwJ^i<gzK44%5h8Cn(&TO1gfn3+!O Kwon&fum%8seSYfz literal 0 HcmV?d00001 diff --git a/docs/static/gui/host.gif b/docs/static/gui/host.gif new file mode 100644 index 0000000000000000000000000000000000000000..5bd60ae3d34d9bcd8be206ba2a8a701b19aea319 GIT binary patch literal 1189 zcmb``30Kkw008hm262dT`d1VcH7ko8t5<8Y=8+H2yze8AmB-d>TJ62nYg4kad?v4& zM~5QL6hp;kPN#X`g=i`SsGx=l2;OM*vP`yphyA|8&&!8?{OcqFKn51n00031BLEHn zNC*Id0!Ap-3M58&meYM}^Nii1^A`;HUW+aDPOb$JlayBa^A~QAaVrt~&^ysVU zu~#pg$<9c~y!cbrrA)@p=L&u~%ef84YDQ6|53A zyOhN)Z>=og)RebX6*gBFKC7*0t$xCN#;)aZI%*#_*0XpGPkXscKDVf^?p}MtlfL?r z{)STi^M``Q$GuIDyP7#eFUp6TS;FS0!nVrczj^H)ZQ`~nQ9F0AF*Tvx5@_UB?C>0{ufgN&9cE}$zZ!w_;Nz{*UVt6WVly6 z(m6TOEfNXjqx|X7mt*2l#aPdzNccwFDU}Q)4o~IY1S8%i#nxVJG-+|;J&gZw=hEE2=`yIo!;s0j01%;ph13(4-_1_5q zu>%;8WN#j;`#OSvdLYVS@EK^2sT%CutgEd(_JPmM)a$tvWO-8!ZZMjb+;`VMC~55O zZ8Nu)t;HMEVqV7i7$TIA?NOFFUVpL&d_}6G;!tO9Hwt5&FH99hSJIbFigv>uofpWs zil%hc_f_o8f&6TSug$RC!irVvj|0^3kD=;Q-S+#9SWcSL6V#M4uup%lA`x$XVv5UD z|6pDlk~f#;>(=$uM(SdIU&W(eXoPydho|^6LAH- z5a8TqvnWKEOTGL2@5uXX*GJ%NS|CpVgB{s(IKY-cv>&v9ThKhI(04%|NGmAs4*3eV zF&DNkP4DBLKw}yiL%OoOERP)rdMHmXC1Xvn?aG|Pp=5wzTGz6H@;FVQb$gC}yVs literal 0 HcmV?d00001 diff --git a/docs/static/gui/hub.gif b/docs/static/gui/hub.gif new file mode 100644 index 0000000000000000000000000000000000000000..17f7c4d3ef726f744da685423057366093df33f8 GIT binary patch literal 719 zcmZ?wbhEHbRA3NeIF`X6<6R-+T`3<>s}$0x8qum2(W)NPsSyiAUD`>#I?27dDHHY5 zChDb6G|ZS{kU7OLYpPMsG?V;UrunnXi|3jb&$BFDXi>JnvTT7>*?jBr1(p@_Eh`pS zRm`^nk&CQB{tt%H;S1z=vTxeUh(6(loea&Lqx}}bFOPuPKIM)NwQisM> zt_{l^n^$`_t#EE#=hC{)xow?q+bXxNO@WAtOJ`nH{!xc%(pooDClzr6I=^<~Fy zEI)o@`=tlFu0Gm*?eU&#kM~}Ge1=Lu@h1x-7ehUR4g(N?;)H?yUqgLUb4zPmdq<0u zTu*Obf4`D_=L9(`C!;~HitzeCPr?FNJxqa zbCy%KUAIBbJ2m~}sncho)np9qjxL(X>fF`|N{-XJqoblAaq)7Kbe~IXuu89G zY>4^!<#n%FX}%(x=oD7Zx(x^D7Cd=>$48HrD@OVHGfFIMB$#qu{aN1LM(d zN%JxW&7zjh$r?;@OmoaOK04YXZC%G>x#`KtDFRD*voZx2yYdE7cm|vGV*3$4sHUKQA;Xt`D2tb#>OIgNj>peGaF!W-$oH tY>1eD&xfTUiIIg<%3(%;VvDJ9wZvc%wRbpgMV>J9(izdZ9gfp+0+}KYOA*e5OBqqdk75L4Bk` zeWOBtq(FhHMS!M3gR4h^r$~aQNrI?Dg|0+}u0w{dOogdTg{n`7s!xckMvAgji>+0Q zu1SuzRgA7lkGD#Xw@Q$=SB#%r9qZk@Ytp1W|LyjZ2mSf$H#qQ6_I&0DF?cci~~ zrNDcq!F#8|eW=5Ks>Fe;#fPuQh_T6uvdNFN%aggzn!VDVz|)|@)uF=GrpDN($JnUI z*{jOhtjpW3%-prm-?-G_xzypg)Z)9<;=R}7%*@Qp%*@Qp%*@QpA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=dv*jE#jucwYQHYIwy=OOkz4Hj~p>W&4i3Z zU|?EVaBDeOZhEkLdR2@d7~9=~8y-G!bar@qcWq-WUF1-iLJ&x!2p8np`UC026(iCh zJzDS}A-9DYGIZ!5v5|<6A4!l-8|AaEc70|*i*d`R)429F;{oItU%<%@ul z7Q3Zf+45z~nKf_b+}ZP|Np6FNvUCYkCQX|-b@KEHbQ>Y5MXMskx^$~ki(gevHEK0! zR;ET=O%?)6_2^ZlTAP-%V2Cc;v3TMBba2S8*t>7d8bD;&uG_Lr69G_J06~HV5GGUz aLDL40AWED>vBKp`qSL5Tt6ohg5CA)88DCBS literal 0 HcmV?d00001 diff --git a/docs/static/gui/link.gif b/docs/static/gui/link.gif new file mode 100644 index 0000000000000000000000000000000000000000..55532ecf0d14eecb81f57e71ae8f90cd9c8f2fea GIT binary patch literal 86 zcmZ?wbhEHblwgox_`m=Kia%Kx85kHDbU=KN3M?hZsq{JzSM|!8CHm iM7a#L1g zw35rPkmRJB!>gvuyrA2_kkrJV)Xlf=-J;{txy;PW+SI|;*4E(H*5%&Z_wUx>;o<-P z|NsC0A^8LW002J#ECc`q02lxm000J*z@KnPED`}ofN^OAs3wpIl1UYa7>b8-NL-kM<*DutFxg;BP1>~th8?)DLBExWd|U~ Vk^lwFilNX&NxIh8*xA-W06UFctKt9v literal 0 HcmV?d00001 diff --git a/docs/static/gui/mdr.gif b/docs/static/gui/mdr.gif new file mode 100644 index 0000000000000000000000000000000000000000..d6762f6500828ead06fb73c7a9061165ccdef85b GIT binary patch literal 1276 zcmb``jXTo`0KoCzh-N62mQF9`y{bhS8&Di5W%q3E|!j5ZXj{V1dP z7^69zTAohjVrksC>#Y$b%3K=zJhSp5qbmMJTLzOyzDB>y-XS*imde`bawS84 zmw2%6!-V0cL*B2(Jh`PG1(ilOjJz+z~L3DWyE~ zO;xt!+NFMoy%loArqJ-l_Jm(**X)msz`k|c5+7%ztE@5^S_rHsgwwkfx+OT=Z$;za z1?W%IUsZdN*kB!mhEtzXfV>X7ht>Edn=uv}N8`!@xG`X6rD(8m2lfq+q_=}^$LNJe zrch2W6c0TaQtZgz-JcRb=$y)|Wj z-BiLi_^0&q5I9?(v7Hx8apXh0#<03WyT?yj39`SYQ1$*P0GqVK6ELG(0k9Ks?dS+d z3pf%;4@qAVb9%@ro@T;!2e%9u$e;%Wd0%mEb+qw`b4TNcjXCa=mAQUIuzk0S85EZ* zBUtH@1DtHVzSAt$x4=OWCFxq)a-ePq&RPZ!3`gHsVKo}*{{&+f1UUU>gn=xwL|_AW r4z4Sh`#3ytbr^3Q6*$HAfFoFup6oRk3ar@WthBAq6&LC31nl__5JiQ; literal 0 HcmV?d00001 diff --git a/docs/static/gui/observe.gif b/docs/static/gui/observe.gif new file mode 100644 index 0000000000000000000000000000000000000000..6b66e7305f35484095a2182ed828c6507c72a746 GIT binary patch literal 1149 zcmd_pYfn=L0LJlylbbZAPQ->d0;0q*%+PGvh{hCQEG*WwP@oJp1|%#lOP0+=yD9^E zJMC#%P#A&~sl5%f$fdVaENf{gXltbuDYVeS>4mnZW77@Ay;;_Njy?H(f#=2lWZqG%4!O^wyx&OriRAmhAY&|RN9pvX;*32s2z+p0&S8&Q(U~J z`1HDh)T$s}R}x#5q*i6+PpZl`6}e4S)vl^;SJiZ=Yv^hUU0qAp)G{=mF*Nl|O&t@i zXTqPe;4j#41E_80z%3B`6)#3hlP=O(9c(TGgqR=%a(E!f5Ddi(3PnRA>5yz#qJh;~oqAfY1LaE{BhQ%km%&jd zZ)}wU1Re`@`9oJa7`GpO_2 znf6ZT%y*~P#QJrKeqCd6rkk^g3>#8|2exj&Pd3Cen=<33%!K@ELWXCN zky+%P*>~UUlh66(^ZwC!|AUxE{;{~g*yF%>j3qE(2~5TX71p5gNl<0m(mag`!3!a6 zj6F1E4?SEA|F#s-FGUP<7R!Rey5O)mJx^D>3o)zS8Hdlb>yO>g zpS$Rb*U=ZRf&bZZ{@I%V0OLSG{CB(wAmv}6G4)7z54#w5GAY0Qgx$RVR97jNeyk-K z!%n+IJ#Z+6-;>|?jPuv|H^lhNu21mKffLy5bD4QP?c|#|n0Jp8wco#gbTJ1jr?B#o zpwIZ0DDQaz=@LnV&pIoiXMa6!%xH2@H(fh#3yU%-1l+Ma#DMEE?9C(%k6MeqHBjI^ zNfj8@$s_i!;2S6J=`b1%yJ zjzYQTh5_ro()5X;@-#bfQ6lc4$C74ZnCAn1Vq}@Ai zXX99F^M5aGkwK}khXSw%@jRar$%MOj@_Rb53* zLseZvO+&*#LsMN-TSG%fPg_?@Ti;MmUsumSSJ%+k(AdDh*umJu$k^1_)ZE0}(#*=* z(#pot+Sba}-p06XUk)~1%W z=GOL>_RhA>?vAdW&hFl>u1PZ|Pn|S<=Cnz(7R{J7eaf6Av**s7Id8$7C2Ll!TE1r8 z>UA5|uGz9@%eKuMb{yEe{n+-M+jj2Wv3bvtz5Dml;r#-+uDu;gbhD7>etiD&*~3?# zUc7q#==JB9uU|ZP^Xu`OFR$LbeE9ayleb@AzkBuk{f}4gKfQhb`t^s;?>@YJ`SI7A zk6+$@eE0U#*AJiGfBgL6)0dC$zkdJx_0xxMKfZkb{OSA8&p&)21O&UZ4U+dI^{!gd>D|h#E6iqaFl|zFQsMJyw=OWRyvJa6$Rl!U zR@l1ZMcNNJ3l}mw>9FJ}Z2q#Shs%CVOlO(ttDwnBECCWCiy96tj+>F9s9ci5+v~R4 zt(VKRlY^y$HMgRo`OM^{iN{KBDit4fo9;Yc%kk90LmM147@K$wKlQwB;JwN4(@H0| z)u(1i(xn6iz5D;z&8dl-$AS zrM*MM*<1JUic6;qwoFj+F)Y&%Z06!ucyvOES=E6-%8p~vY3;2Fi@SBUUUBj^*tDgo zN2{iU;jnU_h0_HN#u!6a(QXcBZ~bi#no@LkeR1MeOki2y)~_Pu;>N(uQq(MZ@J~>; z?!kZ+-HH+o4h+K6bT)OUvr9TM2owiQY?jkgTFfED&(h#5pQ+c>=_GGEv0Ye)C&`VO tJw%~hJ~@t1va|g8z`l6e8jtN{jVv^D?$ literal 0 HcmV?d00001 diff --git a/docs/static/gui/rectangle.gif b/docs/static/gui/rectangle.gif new file mode 100644 index 0000000000000000000000000000000000000000..ed271f5737d0e8c2e35e9f03735de27509996893 GIT binary patch literal 160 zcmZ?wbhEHbRA5kGSjfcC(9rPV&GC|Nli|1^))!lG!&+qxHk36QOake}^#rBeM zi_xlcpRkb%J(0K*PL82|tP literal 0 HcmV?d00001 diff --git a/docs/static/gui/rj45.gif b/docs/static/gui/rj45.gif new file mode 100644 index 0000000000000000000000000000000000000000..9ab7ac56dba8946e37182cf05ca160683f53e2e9 GIT binary patch literal 755 zcmZ?wbhEHbRA5kGI2O;~>+9?1=NA+d6cQ2=8X6iN?hzjD84=+X8R;Dr6%`ZX5gi>J z6B82`=NcCmmynQT6n4RsEljD$^YhO@cS6FCQP*6}( zQc_=US6^S>(9qD>*x1z6)Z1&^+iTL%(b3u2Icbu?gb8&MCe%-yIC1ji#;H>)Cr_R{ zZCcfoDO09TFP}cWa{Bb?bLJGyom)6-)~q>m=FFWtcj3b9MT@dltWaIDBy06*okfcl zEnd8M#fp@5>y(!)S+Ze+`id1RR<2yRYSpUMt5>gGyLR2WbsIKp*tBWW=FOY8Y}vAP z>(=etx9`}oW6z#F`}glZaNxkXbJB+n9lCT$_}H;y$B!RBdGh3`Q>RX!K7HoQnKy4Z z-n`*^`AY@?w*HGWo+|t_C-qG3B79HZ>KVjme2?5b& zEz#jV-gD;8n?E<)TfQaAf5D3RfsslI&C!uzn>KIRx+U6NVO@Rn-hKNI9E=XywfoTV zL#jvjoH)DJ{ph-LXCo9Z`uX0xb^Fd;ql*gOuIA>3CMJ58n)ce(Iwr;j78_ zwFCvl;?#p%KCCcW)XlD^vg!rURSp6*7r%Vup0Kc?jagL6VZj0h<|bY?J(HRl`SC3b zyuqttEQOK`I|U_lW>_{XJ;lP^w&RLwAk%&hA&wh48VA%T&VBgp=M(jZ2bpHG$bGPu zdfRWu>8&jPY;I>~*ysT1_lCK_Z;G0L58oIA}pZ@NkT43mOcrUkRiiszUY&owWZYf(DavUI*h z*+R?m`IhAit;!czR?M@im~U0Fz^Y=QHIS@aU=2bGZ7LU9RV}gxp~W^;3vH_wT30W! zt6peVy~w_Lk!{UV+uCJzwM!jq7dzH2v8!9|ShvKkewjnVN{2=uTIJfX%&}>eN7Hi8 zrWKAYYn)ovcr-8fY+m8gvevt0rAzzzz>YO;-J3mnw*+^u4e4I%(YH0ccfI$-ol*T8 zqbF>PnXt)s%5LAOK(r@r;%5Kpd;MqZOPsPLY1-DHSqD<4Z4I1#D0RlR;JHWAXKoLk zcO+!q(X81!vuE$jnX@x&(ea3-r;8WvjaqRodd1n&Mf*w@?=N4nzjE2Zs^tf(mmjKK zakzff;fB>mTGk!w*m$yU>zT57yZQXRtrza^xcp$xwa0s}KRz=GXchvBKUo;L82&TpFaQB4 zPcU%&WBAW07$KqoEo1|3* z!_CHs4~gB{Wjwl{j8FI;7LI4q)tb`Ld9+cY`_9GB?E#YVRbre*mzCJu8YN_JWf(8# z>@kmBq{2Dr(V_#q;`4JRHfIFNX)2%6h}xoYVahuGKn|4$2Or68>$+sav-qg5yleZT z1F{EQ6&e^t<(@SNFFV=0!e!fzj1|m1QimB?R2B#*X}O8mubAUl{8PT+3>%+Lg#jZo zyP&xDo)w8HoMQUPZqw~_6}C39@hP}WC~!Q?E^1uH;kf7sr-*iB!Ruppejj-M;IMAW zj{{9^{fcMi#yL;-o3$(d!6DU*=kH`|K0m)$Jl(wamesE}w|Cd`^Utq&S^qV5`n{@O zJJsWA7O!;-C}nzd{9fJrUiJBotO^$vRQ=p~{C-7Jz!!ez8;L&_GzNq`RCKykx`Bz$ LhNm#efx#L8|Dlf2 literal 0 HcmV?d00001 diff --git a/docs/static/gui/router_green.gif b/docs/static/gui/router_green.gif new file mode 100644 index 0000000000000000000000000000000000000000..76e3ecd57c59ec99767f5641e701f908f09f0258 GIT binary patch literal 753 zcmV$ZwARvGsB7!0$gd`+|BqoR^Cx|C0iYY0J zDJqL9D~&5Hku5KgFE5iXF_tkhmNGJzGBlYqG@CRwn>IF_HaDF&IG;EPKS85GJf%E6r9DBULPDfMJ*GWE zq(eTZK0c^EMy5tVsX<4lNJytiLaIVQt3XMpN=&IvL#;zYu0u|$P(`stMzKamu}4#_ zRY$T%RIOG=vqwj?M@hCxSg=}Juw6^JOJB2LU$bIPyiQ`YWKF$IW3*;ZzE5SgXi&dU zP{2@9!BT3tZc@ThYq)P~xo>Q_Z*979RK-+pyK-^7bymn$R>@X$y?1rKd3C>eSj$*< zzk6EFT7krdfyIV|#)^%~l8(xhlg*iw&YG3ao0iX=n9!e@(Vw8yr=!)Xq}8jY*R816 zucz3rtJ<=#+_<#fy|v!Gw%@Eq<#>~vjA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=my(jE#=El$DGfh?9&+W>zhgGe=rwY)+0Jg^nay zZG3!fWM&|jQh9!Ub|jCOf|fFFeYa;FPI7*AUNAtGy_SqBI$bsnKeZk z1rZZ4K!FI3>S252fd&*X6#oDSAb`LD2Ng7J300IRJ9!Q{YVZ)>j z9!jK$fuo0!Bu}QqoGFqd0h>4*Jb*C40)`MMUew63L&%V$NtcEkS@R|XsZ_0E)ymZ? zSfx)g$2x_2c52nDS+{omiWDwWvQN#Ty}MSg+rDB4%@u4{ZQizg!^$i)AaK;ic-iiK ji-ij!x`Q2SKKvv~q(ODlW;XnF$?4M`P`G$4C=dWUiqcBu literal 0 HcmV?d00001 diff --git a/docs/static/gui/run.gif b/docs/static/gui/run.gif new file mode 100644 index 0000000000000000000000000000000000000000..71dcc67eddc7b829b3221ba4823a04e93c317ba9 GIT binary patch literal 324 zcmZ?wbhEHbRA5kGXkh>WMn*;!78X`kRv8%?d3kvQ0|P@tLt|rOQ&ZEBkdW~3@W#f* zzP`R`)22WfAZwX)2B~gy?XWL&6~Gx-+uY>JSuFtAW4$w*aj%1_PAOIL8t&n-yIt7K68$->CRAkUx!(gbo61M8v(>U}Ah^DZGE(+-Pd9n~wOxE_RahlU()Kf}#eGDh z*YY|kSclnhF*_f*ZL2LVvRih;gcu3Vl7L(W;oNF5dkLndh`{&>RXH)u)s9RO_M-ic zeWf|^96S<2Tyxsf7B~v9GqLh5uUZ+O;*@5-)iu^LcxTxBeY@PPj~q2OIdSsT=`&}~ Ko!3%ium%9#D0q7S literal 0 HcmV?d00001 diff --git a/docs/static/gui/select.gif b/docs/static/gui/select.gif new file mode 100644 index 0000000000000000000000000000000000000000..bb7e128c878317f6287564514a302e5e3c40c10e GIT binary patch literal 925 zcmX|AT}TvB6#nkcsH?a+J25!wWJw!YSVoy{g=r6ESt%99vPu|jWwavpvRe--w$f%s zYflbuf*4Z;~=R4<~Ip225ShbBKwQc6#Xwq9Tr2mq=851@csnR&8f+ycq>V-cDai3y~@!gr8wL0nBC zU{r)Hb5*5f7u61mZ3t|yIUb`VBe1-QqVleY#wih00Goly!xkwyVZ{1lB8I%Sj2 z2m-M!HhYQ+$jnGNeiv{GnB~nh`yh{T`At9|X4{_DxaFbKW*UGN8*$vq~F1bXlQwoK`C%h5?z2 z^9{6>N|K@L0aob!9DW(7DB*YfpQyG!K!#mAI~x7?(4Lbn6<4Dx zBsmXiOOfS?u)5v0FmUO{muJG>%YKj3_qk`!#gV43zkL%=4mf&UnMa1E`pa7HX6 zwqJ$`n~{uVTRP)1JIzL7vZz_If|{*~lS3(L>1YMNREp5@O@UgWlR;}sSO0_k0ekZL z_4(yVp4S^+tEp{9fME3$6#a%3d6h+8OxB~4_u{fkYWYPR?;%tdh>aI$;&(>#2d(q5 zq|ZS55Pb#FeTca%FjoY_pJqd!1@)Oxp9Ml1`7i-t4cyYgh!#e*aJztjI|SS%;9h|a zB09LQQ-@ea61aXdeO(}jc{0Q^LuMvumWN6CNC71urC^MNC<)^vJSd=Gf`Um(ai38{ z8O1oOm|(THknXlgcgI9TOhi;5A6OtpL5zkt4O28s(=ellSv}0@;UNQ$7?@{Zp@4-& z77{Ex<{-(z6Aqg*V)HyUZ^9BLSmGdMfWHe6JVmgA;2$Hb8db|?STjO~hb+(iC2}E= zy;n>}t!$*2i`p!Ao&2Q35-AghO2uI5=DfEu#zz=H@$_Mb?{Rq z{9+lOEYmC(YgRUCGIq4&M5(gkhc7$gRgRhMj=5@EvbrQ*Q!-U!TdHnN9cfQ>p3I%` zKE32ym!#DJX>CAC`=yLudghn1gHmqL|199o2K>39h3w)&E|JY=*YoT7d>;PKe_lXL z%Qpa=2Iup}^G|?k9cpo5kGH%3s7hbT?CS`gs@|e@IvxFC@zo~jk+^@))dtz&Y*T;l z?A{-~bETvTcUU_8qRZQ!rUz#@r|W!B>HK04c84jYlF?$DP{j?^i+0ZQ2+ZIX4SO-=6I%AGsiKi6a3?s^GQOGO8fMnKq);bg%%10X@M4zBI6uI7|O(9f`)8t&J&IEhZ*K% z8p0H3vMt-(>;N^g83y9QrosRx$V4G+SB}zh6w0L>BIQtq|HD3c{rvguotmB!tKG*1 zE-VAE0ptL&09yeP015#11C#)q0w@Kz0PqdK&j3}M2!tvku^OOe6M=9I;5xuH0$ktZ z0aXZc!vk)j#99QYB_Y3{$So3DOF@67l5SyG9hK5R#Tq<48*uD4oq7+)?=tY;nACer z`X3y|eIItSz^mDxWAgMc(LBsFqM1&#;6w|9Xu{EdnM5lKv9ggqFQlJ?40xjhKIq^k zU)07$ZGPwwk7V~J+4&?BlVb6uTR9Y~H`eCk^}vVI%Hwnk{46}Cna4KqIA)QbgHIY3 zl1D`3zs1}xsegxz*BL5&94hP%6%9yzEn==&%crFW;+X`=Xnfd;p0S?9 zTz`wXu$@1jB>69icnv2A%1q*_d#8!8+C0 zTZrvD>>8Vox>4Mdm_Es^ZH+W4(mo!~Z1|$qT$^|IwxO%!+@-oJh{nC8;r*=WWA`-;QAzjtz^Ii7u|z=Wv2fXj#VN>!l;b z?mLBTO~!ZdvdioVmi{Zhp5ZE9)4|Gr^t?m#a)!v5$*HJ3Tk$2o=T;c6)t=K>yv+RC(pN0M-Dd-mC&;N5vlB2PJtvQ!1!r1BjF z+s~Ac1T8?_TUcM_wWIJ%I-4ebdPz@^w*X1jEB<6*W6va?-5+;v-TUT4p_f*)Gl%!N9?d`v_mp{QI;sYb*91-N d=esbwJ0>tWSn~6fcWtY;{pRgIt;xz@4FI19FuMQ% literal 0 HcmV?d00001 diff --git a/docs/static/gui/tunnel.gif b/docs/static/gui/tunnel.gif new file mode 100644 index 0000000000000000000000000000000000000000..d574147f535122637516f4887d931779c5903ead GIT binary patch literal 799 zcmZ?wbhEHbRA5kGI2Oae!0_MK*VoU_FDNJ|BqSs>G&DTiBRt$QBEl;&(mN_DDkjDw zIyyQgCMGVRu= zo;-Qlw5lmnrc9q+K7D%S^y$;*%qf~Xw{X_1S###hnLBsx!iCw37GTOS zvSsVmt=qS6-?3xIo;`c^@85smz=3n;qz@fBbm@}tv17-MA3uKbEDBDn2z=biC+ZOKAtSKatw%=FXNJe4Me>Sj+A1D9KCrS*RkU3* z;lrne-kjRjO)MM=OCIfUlrZJGkdVO4#LcG^5-~xMv7K2WY}*zN!ma^ zbPjCKZ)C4{X}6U-tSN3!tNM$%udfIkJDRAe;MWte!OBrWPFoND ze%zhu(T2k5n;-J6D%2?Cv=W`H(&hR5YrE{RRjOvEka_>yUw;J#YXB&pnhF2_ literal 0 HcmV?d00001 diff --git a/docs/static/gui/twonode.gif b/docs/static/gui/twonode.gif new file mode 100644 index 0000000000000000000000000000000000000000..28e75fac3aadc9286cdb65b1269f3f1355f055d4 GIT binary patch literal 220 zcmZ?wbhEHbRA5kGIK;vL1pmSK$$5tVNI>zQv_`U~f{}rNg+fV2s)AE~YGz)#f^&Xu zL1JDdgW^vXMlJ>x1|5(AAfp(Vn>=>i`Dbv-A$vka-~r3C5f^&JB{~NwA;X2aQL-=@frKKWwv3( zs%b?M|9mdLmiXy?;63*j?HbL7MqLIbcP^&3j?Qk~UiW^@i7u0sr`k2mm^rJirgNbX HCxbNrD`Hob literal 0 HcmV?d00001 diff --git a/docs/static/gui/wlan.gif b/docs/static/gui/wlan.gif new file mode 100644 index 0000000000000000000000000000000000000000..d72fe9c3f8db0c7013424313bc826eca7022c19a GIT binary patch literal 146 zcmZ?wbhEHbRA5kGXkcUjg8%>jEB@otNY*qmFfdba%1_PAOJ`90$->CRz{sEjQUOxS zz!cuozw-23{>32+u5qs4D3yPjAx>eEpJZvqkAmdpb(+5?eJb>ho%FhP{o<=WA`fyl xetyz3Q~L84X{Px{Rg%)5F0}|bV|49q%MP!#xfR=XUU=m{gSY?m^L8c%YXB@FJ0t)A literal 0 HcmV?d00001 diff --git a/docs/usage.md b/docs/usage.md deleted file mode 100644 index 80b96c05..00000000 --- a/docs/usage.md +++ /dev/null @@ -1,722 +0,0 @@ - -# Using the CORE GUI - -* Table of Contents -{:toc} - -The following image shows the CORE GUI: -![](static/core_screenshot.png) - - -## Overview - -CORE can be used via the GUI or [Python_Scripting](scripting.md). Often the GUI is used to draw nodes and network devices on the canvas. A Python script could also be written, that imports the CORE Python module, to configure and instantiate nodes and networks. This chapter primarily covers usage of the CORE GUI. - -The following image shows the various phases of a CORE session: -![](static/core-workflow.jpg) - -After pressing the start button, CORE will proceed through these phases, staying in the **runtime** phase. After the session is stopped, CORE will proceed to the **data collection** phase before tearing down the emulated state. - -CORE can be customized to perform any action at each phase in the workflow above. See the *Hooks...* entry on the **Session Menu** for details about when these session states are reached. - -__Note: The CORE GUI is currently in a state of transition. The replacement candidate is currently in an alpha version, and can be found [here](https://github.com/coreemu/core/wiki/CORE-GUI-Update).__ - -## Prerequisites - -Beyond installing CORE, you must have the CORE daemon running. This is done on the command line with either Systemd or SysV -```shell -# systed -sudo systemctl daemon-reload -sudo systemctl start core-daemon - -# sysv -sudo service core-daemon start -``` - -You can also invoke the daemon directly from the command line, which can be useful if you'd like to see the logging output directly. -``` -# direct invocation -sudo core-daemon -``` - -## Modes of Operation - -The CORE GUI has two primary modes of operation, **Edit** and **Execute** modes. Running the GUI, by typing **core-gui** with no options, starts in Edit mode. Nodes are drawn on a blank canvas using the toolbar on the left and configured from right-click menus or by double-clicking them. The GUI does not need to be run as root. - -Once editing is complete, pressing the green **Start** button (or choosing **Execute** from the **Session** menu) instantiates the topology within the Linux kernel and enters Execute mode. In execute mode, the user can interact with the running emulated machines by double-clicking or right-clicking on them. The editing toolbar disappears and is replaced by an execute toolbar, which provides tools while running the emulation. Pressing the red **Stop** button (or choosing **Terminate** from the **Session** menu) will destroy the running emulation and return CORE to Edit mode. - -CORE can be started directly in Execute mode by specifying **--start** and a topology file on the command line: - -```shell -core-gui --start ~/.core/configs/myfile.imn -``` - -Once the emulation is running, the GUI can be closed, and a prompt will appear asking if the emulation should be terminated. The emulation may be left running and the GUI can reconnect to an existing session at a later time. - -The GUI can be run as a normal user on Linux. - -The GUI can be connected to a different address or TCP port using the **--address** and/or **--port** options. The defaults are shown below. - -```shell -core-gui --address 127.0.0.1 --port 4038 -``` - -## Toolbar - -The toolbar is a row of buttons that runs vertically along the left side of the CORE GUI window. The toolbar changes depending on the mode of operation. - -### Editing Toolbar - -When CORE is in Edit mode (the default), the vertical Editing Toolbar exists on the left side of the CORE window. Below are brief descriptions for each toolbar item, starting from the top. Most of the tools are grouped into related sub-menus, which appear when you click on their group icon. - -| Toolbar item | Functionality | -|---|---| -| ![alt text](../gui/icons/tiny/select.gif) *Selection Tool* | default tool for selecting, moving, configuring nodes. | -| ![alt text](../gui/icons/tiny/start.gif) *Start button* | starts Execute mode, instantiates the emulation. | -| ![alt text](../gui/icons/tiny/link.gif) *Link* | the Link Tool allows network links to be drawn between two nodes by clicking and dragging the mouse. | -### ![alt text](../gui/icons/tiny/router.gif) Network-layer virtual nodes - -| Network-layer node | Description | -|---|---| -| ![alt text](../gui/icons/tiny/router.gif) *Router* | runs Quagga OSPFv2 and OSPFv3 routing to forward packets. | -| ![alt text](../gui/icons/tiny/host.gif) *Host* | emulated server machine having a default route, runs SSH server. | -| ![alt text](../gui/icons/tiny/pc.gif) *PC* | basic emulated machine having a default route, runs no processes by default. | -| ![alt text](../gui/icons/tiny/mdr.gif) *MDR* | runs Quagga OSPFv3 MDR routing for MANET-optimized routing. | -| ![alt text](../gui/icons/tiny/router_green.gif) *PRouter* | physical router represents a real testbed machine. | -| ![alt text](../gui/icons/tiny/document-properties.gif) *Edit* | edit node types button invokes the CORE Node Types dialog. New types of nodes may be created having different icons and names. The default services that are started with each node type can be changed here. | -### ![alt text](../gui/icons/tiny/hub.gif) Link-layer nodes - -| Link-layer node | Description | -|---|---| -| ![alt text](../gui/icons/tiny/hub.gif) *Hub* | the Ethernet hub forwards incoming packets to every connected node. | -| ![alt text](../gui/icons/tiny/lanswitch.gif) *Switch* | the Ethernet switch intelligently forwards incoming packets to attached hosts using an Ethernet address hash table. | -| ![alt text](../gui/icons/tiny/wlan.gif) *Wireless LAN* | when routers are connected to this WLAN node, they join a wireless network and an antenna is drawn instead of a connecting line; the WLAN node typically controls connectivity between attached wireless nodes based on the distance between them. | -| ![alt text](../gui/icons/tiny/rj45.gif) *RJ45* | with the RJ45 Physical Interface Tool, emulated nodes can be linked to real physical interfaces; using this tool, real networks and devices can be physically connected to the live-running emulation. | -| ![alt text](../gui/icons/tiny/tunnel.gif) *Tunnel* | the Tunnel Tool allows connecting together more than one CORE emulation using GRE tunnels. | - -### Anotation Tools - -| Tool | Functionality | -|---|---| -| ![alt text](../gui/icons/tiny/marker.gif) *Marker* | for drawing marks on the canvas. | -| ![alt text](../gui/icons/tiny/oval.gif) *Oval* | for drawing circles on the canvas that appear in the background. | -| ![alt text](../gui/icons/tiny/rectangle.gif) *Rectangle* | for drawing rectangles on the canvas that appear in the background. | -| ![alt text](../gui/icons/tiny/text.gif) *Text* | for placing text captions on the canvas. | - -### Execution Toolbar - -When the Start button is pressed, CORE switches to Execute mode, and the Edit toolbar on the left of the CORE window is replaced with the Execution toolbar Below are the items on this toolbar, starting from the top. - -| Tool | Functionality | -|---|---| -| ![alt text](../gui/icons/tiny/select.gif) *Selection Tool* | in Execute mode, the Selection Tool can be used for moving nodes around the canvas, and double-clicking on a node will open a shell window for that node; right-clicking on a node invokes a pop-up menu of run-time options for that node. | -| ![alt text](../gui/icons/tiny/stop.gif) *Stop button* | stops Execute mode, terminates the emulation, returns CORE to edit mode. | -| ![alt text](../gui/icons/tiny/observe.gif) *Observer Widgets Tool* | clicking on this magnifying glass icon invokes a menu for easily selecting an Observer Widget. The icon has a darker gray background when an Observer Widget is active, during which time moving the mouse over a node will pop up an information display for that node. | -| ![alt text](../gui/icons/tiny/plot.gif) *Plot Tool* | with this tool enabled, clicking on any link will activate the Throughput Widget and draw a small, scrolling throughput plot on the canvas. The plot shows the real-time kbps traffic for that link. The plots may be dragged around the canvas; right-click on a plot to remove it. | -| ![alt text](../gui/icons/tiny/marker.gif) *Marker* | for drawing freehand lines on the canvas, useful during demonstrations; markings are not saved. | -| ![alt text](../gui/icons/tiny/twonode.gif) *Two-node Tool* | click to choose a starting and ending node, and run a one-time *traceroute* between those nodes or a continuous *ping -R* between nodes. The output is displayed in real time in a results box, while the IP addresses are parsed and the complete network path is highlighted on the CORE display. | -| ![alt text](../gui/icons/tiny/run.gif) *Run Tool* | this tool allows easily running a command on all or a subset of all nodes. A list box allows selecting any of the nodes. A text entry box allows entering any command. The command should return immediately, otherwise the display will block awaiting response. The *ping* command, for example, with no parameters, is not a good idea. The result of each command is displayed in a results box. The first occurrence of the special text "NODE" will be replaced with the node name. The command will not be attempted to run on nodes that are not routers, PCs, or hosts, even if they are selected. | - -## Menubar - -The menubar runs along the top of the CORE GUI window and provides access to a -variety of features. Some of the menus are detachable, such as the *Widgets* -menu, by clicking the dashed line at the top. - -### File Menu - -The File menu contains options for manipulating the **.imn** Configuration Files. Generally, these menu items should not be used in -Execute mode. - -| Option | Functionality | -|---|---| -| *New* |this starts a new file with an empty canvas. | -| *Open* | invokes the File Open dialog box for selecting a new **.imn** or XML file to open. You can change the default path used for this dialog in the Preferences Dialog. | -| *Save* | saves the current topology. If you have not yet specified a file name, the Save As dialog box is invoked. | -| *Save As XML* | invokes the Save As dialog box for selecting a new **.xml** file for saving the current configuration in the XML file. | -| *Save As imn* | invokes the Save As dialog box for selecting a new **.imn** topology file for saving the current configuration. Files are saved in the *IMUNES network configuration* file. | -| *Export Python script* | prints Python snippets to the console, for inclusion in a CORE Python script. | -| *Execute XML or Python script* | invokes a File Open dialog box for selecting an XML file to run or a Python script to run and automatically connect to. If a Python script, the script must create a new CORE Session and add this session to the daemon's list of sessions in order for this to work. | -| *Execute Python script with options* | invokes a File Open dialog box for selecting a Python script to run and automatically connect to. After a selection is made, a Python Script Options dialog box is invoked to allow for command-line options to be added. The Python script must create a new CORE Session and add this session to the daemon's list of sessions in order for this to work. | -| *Open current file in editor* | this opens the current topology file in the **vim** text editor. First you need to save the file. Once the file has been edited with a text editor, you will need to reload the file to see your changes. The text editor can be changed from the Preferences Dialog. | -| *Print* | this uses the Tcl/Tk postscript command to print the current canvas to a printer. A dialog is invoked where you can specify a printing command, the default being **lpr**. The postscript output is piped to the print command. | -| *Save screenshot* | saves the current canvas as a postscript graphic file. | -| *Recently used files* | above the Quit menu command is a list of recently use files, if any have been opened. You can clear this list in the Preferences dialog box. You can specify the number of files to keep in this list from the Preferences dialog. Click on one of the file names listed to open that configuration file. | -| *Quit* | the Quit command should be used to exit the CORE GUI. CORE may prompt for termination if you are currently in Execute mode. Preferences and the recently-used files list are saved. | - -### Edit Menu - -| Option | Functionality | -|---|---| -| *Undo* | attempts to undo the last edit in edit mode. | -| *Redo* | attempts to redo an edit that has been undone. | -| *Cut*, *Copy*, *Paste* | used to cut, copy, and paste a selection. When nodes are pasted, their node numbers are automatically incremented, and existing links are preserved with new IP addresses assigned. Services and their customizations are copied to the new node, but care should be taken as node IP addresses have changed with possibly old addresses remaining in any custom service configurations. Annotations may also be copied and pasted. -| *Select All* | selects all items on the canvas. Selected items can be moved as a group. | -| *Select Adjacent* | select all nodes that are linked to the already selected node(s). For wireless nodes this simply selects the WLAN node(s) that the wireless node belongs to. You can use this by clicking on a node and pressing CTRL+N to select the adjacent nodes. | -| *Find...* | invokes the *Find* dialog box. The Find dialog can be used to search for nodes by name or number. Results are listed in a table that includes the node or link location and details such as IP addresses or link parameters. Clicking on a result will focus the canvas on that node or link, switching canvases if necessary. | -| *Clear marker* | clears any annotations drawn with the marker tool. Also clears any markings used to indicate a node's status. | -| *Preferences...* | invokes the Preferences dialog box. | - -### Canvas Menu - -The canvas menu provides commands for adding, removing, changing, and switching to different editing canvases. - -| Option | Functionality | -|---|---| -| *New* | creates a new empty canvas at the right of all existing canvases. | -| *Manage...* | invokes the *Manage Canvases* dialog box, where canvases may be renamed and reordered, and you can easily switch to one of the canvases by selecting it. | -| *Delete* | deletes the current canvas and all items that it contains. | -| *Size/scale...* | invokes a Canvas Size and Scale dialog that allows configuring the canvas size, scale, and geographic reference point. The size controls allow changing the width and height of the current canvas, in pixels or meters. The scale allows specifying how many meters are equivalent to 100 pixels. The reference point controls specify the latitude, longitude, and altitude reference point used to convert between geographic and Cartesian coordinate systems. By clicking the *Save as default* option, all new canvases will be created with these properties. The default canvas size can also be changed in the Preferences dialog box. -| *Wallpaper...* | used for setting the canvas background image. | -| *Previous*, *Next*, *First*, *Last* | used for switching the active canvas to the first, last, or adjacent canvas. | - -### View Menu - -The View menu features items for controlling what is displayed on the drawing -canvas. - -| Option | Functionality | -|---|---| -| *Show* | opens a submenu of items that can be displayed or hidden, such as interface names, addresses, and labels. Use these options to help declutter the display. These options are generally saved in the topology files, so scenarios have a more consistent look when copied from one computer to another. | -| *Show hidden nodes* | reveal nodes that have been hidden. Nodes are hidden by selecting one or more nodes, right-clicking one and choosing *hide*. | -| *Locked* | toggles locked view; when the view is locked, nodes cannot be moved around on the canvas with the mouse. This could be useful when sharing the topology with someone and you do not expect them to change things. | -| *3D GUI...* | launches a 3D GUI by running the command defined under Preferences, *3D GUI command*. This is typically a script that runs the SDT3D display. SDT is the Scripted Display Tool from NRL that is based on NASA's Java-based WorldWind virtual globe software. | -| *Zoom In* | magnifies the display. You can also zoom in by clicking *zoom 100%* label in the status bar, or by pressing the **+** (plus) key. | -| *Zoom Out* | reduces the size of the display. You can also zoom out by right-clicking *zoom 100%* label in the status bar or by pressing the **-** (minus) key. | - -### Tools Menu - -The tools menu lists different utility functions. - -| Option | Functionality | -|---|---| -| *Autorearrange all* |automatically arranges all nodes on the canvas. Nodes having a greater number of links are moved to the center. This mode can continue to run while placing nodes. To turn off this autorearrange mode, click on a blank area of the canvas with the select tool, or choose this menu option again. | -| *Autorearrange selected* | automatically arranges the selected nodes on the canvas. | -| *Align to grid* | moves nodes into a grid formation, starting with the smallest-numbered node in the upper-left corner of the canvas, arranging nodes in vertical columns. | -| *Traffic...* | invokes the CORE Traffic Flows dialog box, which allows configuring, starting, and stopping MGEN traffic flows for the emulation. | -| *IP addresses...* | invokes the IP Addresses dialog box for configuring which IPv4/IPv6 prefixes are used when automatically addressing new interfaces. | -| *MAC addresses...* | invokes the MAC Addresses dialog box for configuring the starting number used as the lowest byte when generating each interface MAC address. This value should be changed when tunneling between CORE emulations to prevent MAC address conflicts. | -| *Build hosts file...* | invokes the Build hosts File dialog box for generating **/etc/hosts** file entries based on IP addresses used in the emulation. | -| *Renumber nodes...* | invokes the Renumber Nodes dialog box, which allows swapping one node number with another in a few clicks. | -| *Experimental...* | menu of experimental options, such as a tool to convert ns-2 scripts to IMUNES imn topologies, supporting only basic ns-2 functionality, and a tool for automatically dividing up a topology into partitions. | -| *Topology generator* | opens a submenu of topologies to generate. You can first select the type of node that the topology should consist of, or routers will be chosen by default. Nodes may be randomly placed, aligned in grids, or various other topology patterns. All of the supported patterns are listed in the table below. | -| *Debugger...* | opens the CORE Debugger window for executing arbitrary Tcl/Tk commands. | - -| Pattern | Description | -|---|---| -| *Random* | nodes are randomly placed about the canvas, but are not linked together. This can be used in conjunction with a WLAN node to quickly create a wireless network. | -| *Grid* | nodes are placed in horizontal rows starting in the upper-left corner, evenly spaced to the right; nodes are not linked to each other. | -| *Connected Grid* | nodes are placed in an N x M (width and height) rectangular grid, and each node is linked to the node above, below, left and right of itself. | -| *Chain* | nodes are linked together one after the other in a chain. | -| *Star* | one node is placed in the center with N nodes surrounding it in a circular pattern, with each node linked to the center node. | -| *Cycle* | nodes are arranged in a circular pattern with every node connected to its neighbor to form a closed circular path. | -| *Wheel* | the wheel pattern links nodes in a combination of both Star and Cycle patterns. | -| *Cube* | generate a cube graph of nodes. | -| *Clique* | creates a clique graph of nodes, where every node is connected to every other node. | -| *Bipartite* | creates a bipartite graph of nodes, having two disjoint sets of vertices. | - -### Widgets Menu - -*Widgets* are GUI elements that allow interaction with a running emulation. -Widgets typically automate the running of commands on emulated nodes to report -status information of some type and display this on screen. - -#### Periodic Widgets - -These Widgets are those available from the main *Widgets* menu. More than one -of these Widgets may be run concurrently. An event loop fires once every second -that the emulation is running. If one of these Widgets is enabled, its periodic -routine will be invoked at this time. Each Widget may have a configuration -dialog box which is also accessible from the *Widgets* menu. - -Here are some standard widgets: - -* *Adjacency* - displays router adjacency states for Quagga's OSPFv2 and OSPFv3 - routing protocols. A line is drawn from each router halfway to the router ID - of an adjacent router. The color of the line is based on the OSPF adjacency - state such as Two-way or Full. To learn about the different colors, see the - *Configure Adjacency...* menu item. The **vtysh** command is used to - dump OSPF neighbor information. - Only half of the line is drawn because each - router may be in a different adjacency state with respect to the other. -* *Throughput* - displays the kilobits-per-second throughput above each link, - using statistics gathered from the ng_pipe Netgraph node that implements each - link. If the throughput exceeds a certain threshold, the link will become - highlighted. For wireless nodes which broadcast data to all nodes in range, - the throughput rate is displayed next to the node and the node will become - circled if the threshold is exceeded. - -#### Observer Widgets - -These Widgets are available from the *Observer Widgets* submenu of the -*Widgets* menu, and from the Widgets Tool on the toolbar. Only one Observer Widget may -be used at a time. Mouse over a node while the session is running to pop up -an informational display about that node. - -Available Observer Widgets include IPv4 and IPv6 routing tables, socket -information, list of running processes, and OSPFv2/v3 neighbor information. - -Observer Widgets may be edited by the user and rearranged. Choosing *Edit...* -from the Observer Widget menu will invoke the Observer Widgets dialog. A list -of Observer Widgets is displayed along with up and down arrows for rearranging -the list. Controls are available for renaming each widget, for changing the -command that is run during mouse over, and for adding and deleting items from -the list. Note that specified commands should return immediately to avoid -delays in the GUI display. Changes are saved to a **widgets.conf** file in -the CORE configuration directory. - -### Session Menu - -The Session Menu has entries for starting, stopping, and managing sessions, -in addition to global options such as node types, comments, hooks, servers, -and options. - -| Option | Functionality | -|---|---| -| *Start* or *Stop* | this starts or stops the emulation, performing the same function as the green Start or red Stop button. | -| *Change sessions...* | invokes the CORE Sessions dialog box containing a list of active CORE sessions in the daemon. Basic session information such as name, node count, start time, and a thumbnail are displayed. This dialog allows connecting to different sessions, shutting them down, or starting a new session. | -| *Node types...* | invokes the CORE Node Types dialog, performing the same function as the Edit button on the Network-Layer Nodes toolbar. | -| *Comments...* | invokes the CORE Session Comments window where optional text comments may be specified. These comments are saved at the top of the configuration file, and can be useful for describing the topology or how to use the network. | -| *Hooks...* | invokes the CORE Session Hooks window where scripts may be configured for a particular session state. The session states are defined in the table right below. The top of the window has a list of configured hooks, and buttons on the bottom left allow adding, editing, and removing hook scripts. The new or edit button will open a hook script editing window. A hook script is a shell script invoked on the host (not within a virtual node). | -| *Reset node positions* | if you have moved nodes around using the mouse or by using a mobility module, choosing this item will reset all nodes to their original position on the canvas. The node locations are remembered when you first press the Start button. | -| *Emulation servers...* | invokes the CORE emulation servers dialog for configuring. | -| *Change Sessions...* | invokes the Sessions dialog for switching between different running sessions. This dialog is presented during startup when one or more sessions are already running. | -| *Options...* | presents per-session options, such as the IPv4 prefix to be used, if any, for a control network the ability to preserve the session directory; and an on/off switch for SDT3D support. | - -| Session state | Description | -|---|---| -| *definition* | used by the GUI to tell the backend to clear any state. | -| *configuration* | when the user presses the *Start* button, node, link, and other configuration data is sent to the backend. This state is also reached when the user customizes a service. | -| *instantiation* | after configuration data has been sent, just before the nodes are created. | -| *runtime* | all nodes and networks have been built and are running. (This is the same state at which the previously-named *global experiment script* was run.) -| *datacollect* | the user has pressed the *Stop* button, but before services have been stopped and nodes have been shut down. This is a good time to collect log files and other data from the nodes. | -| *shutdown* | all nodes and networks have been shut down and destroyed. | - -### Help Menu - -* *Online manual (www)*, *CORE website (www)*, *Mailing list (www)* - these - options attempt to open a web browser with the link to the specified web - resource. -* *About* - invokes the About dialog box for viewing version information - -## Connecting with Physical Networks - -CORE's emulated networks run in real time, so they can be connected to live -physical networks. The RJ45 tool and the Tunnel tool help with connecting to -the real world. These tools are available from the *Link-layer nodes* menu. - -When connecting two or more CORE emulations together, MAC address collisions -should be avoided. CORE automatically assigns MAC addresses to interfaces when -the emulation is started, starting with **00:00:00:aa:00:00** and incrementing -the bottom byte. The starting byte should be changed on the second CORE machine -using the *MAC addresses...* option from the *Tools* menu. - -### RJ45 Tool - -The RJ45 node in CORE represents a physical interface on the real CORE machine. -Any real-world network device can be connected to the interface and communicate -with the CORE nodes in real time. - -The main drawback is that one physical interface is required for each -connection. When the physical interface is assigned to CORE, it may not be used -for anything else. Another consideration is that the computer or network that -you are connecting to must be co-located with the CORE machine. - -To place an RJ45 connection, click on the *Link-layer nodes* toolbar and select -the *RJ45 Tool* from the submenu. Click on the canvas near the node you want to -connect to. This could be a router, hub, switch, or WLAN, for example. Now -click on the *Link Tool* and draw a link between the RJ45 and the other node. -The RJ45 node will display "UNASSIGNED". Double-click the RJ45 node to assign a -physical interface. A list of available interfaces will be shown, and one may -be selected by double-clicking its name in the list, or an interface name may -be entered into the text box. - -**NOTE:** - When you press the Start button to instantiate your topology, the - interface assigned to the RJ45 will be connected to the CORE topology. The - interface can no longer be used by the system. For example, if there was an - IP address assigned to the physical interface before execution, the address - will be removed and control given over to CORE. No IP address is needed; the - interface is put into promiscuous mode so it will receive all packets and - send them into the emulated world. - -Multiple RJ45 nodes can be used within CORE and assigned to the same physical -interface if 802.1x VLANs are used. This allows for more RJ45 nodes than -physical ports are available, but the (e.g. switching) hardware connected to -the physical port must support the VLAN tagging, and the available bandwidth -will be shared. - -You need to create separate VLAN virtual devices on the Linux host, -and then assign these devices to RJ45 nodes inside of CORE. The VLANning is -actually performed outside of CORE, so when the CORE emulated node receives a -packet, the VLAN tag will already be removed. - -Here are example commands for creating VLAN devices under Linux: - -```shell -ip link add link eth0 name eth0.1 type vlan id 1 -ip link add link eth0 name eth0.2 type vlan id 2 -ip link add link eth0 name eth0.3 type vlan id 3 -``` - -### Tunnel Tool - -The tunnel tool builds GRE tunnels between CORE emulations or other hosts. -Tunneling can be helpful when the number of physical interfaces is limited or -when the peer is located on a different network. Also a physical interface does -not need to be dedicated to CORE as with the RJ45 tool. - -The peer GRE tunnel endpoint may be another CORE machine or another -host that supports GRE tunneling. When placing a Tunnel node, initially -the node will display "UNASSIGNED". This text should be replaced with the IP -address of the tunnel peer. This is the IP address of the other CORE machine or -physical machine, not an IP address of another virtual node. - -**NOTE:** - Be aware of possible MTU (Maximum Transmission Unit) issues with GRE devices. The *gretap* device - has an interface MTU of 1,458 bytes; when joined to a Linux bridge, the - bridge's MTU - becomes 1,458 bytes. The Linux bridge will not perform fragmentation for - large packets if other bridge ports have a higher MTU such as 1,500 bytes. - -The GRE key is used to identify flows with GRE tunneling. This allows multiple -GRE tunnels to exist between that same pair of tunnel peers. A unique number -should be used when multiple tunnels are used with the same peer. When -configuring the peer side of the tunnel, ensure that the matching keys are -used. - -Here are example commands for building the other end of a tunnel on a Linux -machine. In this example, a router in CORE has the virtual address -**10.0.0.1/24** and the CORE host machine has the (real) address -**198.51.100.34/24**. The Linux box -that will connect with the CORE machine is reachable over the (real) network -at **198.51.100.76/24**. -The emulated router is linked with the Tunnel Node. In the -Tunnel Node configuration dialog, the address **198.51.100.76** is entered, with -the key set to **1**. The gretap interface on the Linux box will be assigned -an address from the subnet of the virtual router node, -**10.0.0.2/24**. -```shell -# these commands are run on the tunnel peer -sudo ip link add gt0 type gretap remote 198.51.100.34 local 198.51.100.76 key 1 -sudo ip addr add 10.0.0.2/24 dev gt0 -sudo ip link set dev gt0 up -``` - - -Now the virtual router should be able to ping the Linux machine: - -```shell -# from the CORE router node -ping 10.0.0.2 -``` - -And the Linux machine should be able to ping inside the CORE emulation: - -```shell -# from the tunnel peer -ping 10.0.0.1 -``` - -To debug this configuration, **tcpdump** can be run on the gretap devices, or -on the physical interfaces on the CORE or Linux machines. Make sure that a -firewall is not blocking the GRE traffic. - -### Communicating with the Host Machine - -The host machine that runs the CORE GUI and/or daemon is not necessarily -accessible from a node. Running an X11 application on a node, for example, -requires some channel of communication for the application to connect with -the X server for graphical display. There are several different ways to -connect from the node to the host and vice versa. - - -#### Control Network - -The quickest way to connect with the host machine through the primary control network. - -With a control network, the host can launch an X11 application on a node. -To run an X11 application on the node, the **SSH** service can be enabled on -the node, and SSH with X11 forwarding can be used from the host to the node: - -```shell -# SSH from host to node n5 to run an X11 app -ssh -X 172.16.0.5 xclock -``` - -Note that the **coresendmsg** utility can be used for a node to send -messages to the CORE daemon running on the host (if the **listenaddr = 0.0.0.0** -is set in the **/etc/core/core.conf** file) to interact with the running -emulation. For example, a node may move itself or other nodes, or change -its icon based on some node state. - -#### Other Methods - -There are still other ways to connect a host with a node. The RJ45 Tool -can be used in conjunction with a dummy interface to access a node: - -```shell -sudo modprobe dummy numdummies=1 -``` - -A **dummy0** interface should appear on the host. Use the RJ45 tool assigned -to **dummy0**, and link this to a node in your scenario. After starting the -session, configure an address on the host. - -```shell -sudo ip link show type bridge -# determine bridge name from the above command -# assign an IP address on the same network as the linked node -sudo ip addr add 10.0.1.2/24 dev b.48304.34658 -``` - -In the example shown above, the host will have the address **10.0.1.2** and -the node linked to the RJ45 may have the address **10.0.1.1**. - -## Building Sample Networks - -### Wired Networks - -Wired networks are created using the *Link Tool* to draw a link between two -nodes. This automatically draws a red line representing an Ethernet link and -creates new interfaces on network-layer nodes. - -Double-click on the link to invoke the *link configuration* dialog box. Here -you can change the Bandwidth, Delay, Loss, and Duplicate -rate parameters for that link. You can also modify the color and width of the -link, affecting its display. - -Link-layer nodes are provided for modeling wired networks. These do not create -a separate network stack when instantiated, but are implemented using Linux bridging. -These are the hub, switch, and wireless LAN nodes. The hub copies each packet from -the incoming link to every connected link, while the switch behaves more like an -Ethernet switch and keeps track of the Ethernet address of the connected peer, -forwarding unicast traffic only to the appropriate ports. - -The wireless LAN (WLAN) is covered in the next section. - -### Wireless Networks - -The wireless LAN node allows you to build wireless networks where moving nodes -around affects the connectivity between them. Connection between a pair of nodes is stronger -when the nodes are closer while connection is weaker when the nodes are further away. -The wireless LAN, or WLAN, node appears as a small cloud. The WLAN offers -several levels of wireless emulation fidelity, depending on your modeling needs. - -The WLAN tool can be extended with plug-ins for different levels of wireless -fidelity. The basic on/off range is the default setting available on all -platforms. Other plug-ins offer higher fidelity at the expense of greater -complexity and CPU usage. The availability of certain plug-ins varies depending -on platform. See the table below for a brief overview of wireless model types. - - -|Model|Type|Supported Platform(s)|Fidelity|Description| -|-----|----|---------------------|--------|-----------| -|Basic|on/off|Linux|Low|Ethernet bridging with ebtables| -|EMANE|Plug-in|Linux|High|TAP device connected to EMANE emulator with pluggable MAC and PHY radio types| - -To quickly build a wireless network, you can first place several router nodes -onto the canvas. If you have the -Quagga MDR software installed, it is -recommended that you use the *mdr* node type for reduced routing overhead. Next -choose the *wireless LAN* from the *Link-layer nodes* submenu. First set the -desired WLAN parameters by double-clicking the cloud icon. Then you can link -all of the routers by right-clicking on the WLAN and choosing *Link to all -routers*. - -Linking a router to the WLAN causes a small antenna to appear, but no red link -line is drawn. Routers can have multiple wireless links and both wireless and -wired links (however, you will need to manually configure route -redistribution.) The mdr node type will generate a routing configuration that -enables OSPFv3 with MANET extensions. This is a Boeing-developed extension to -Quagga's OSPFv3 that reduces flooding overhead and optimizes the flooding -procedure for mobile ad-hoc (MANET) networks. - -The default configuration of the WLAN is set to use the basic range model, -using the *Basic* tab in the WLAN configuration dialog. Having this model -selected causes **core-daemon** to calculate the distance between nodes based -on screen pixels. A numeric range in screen pixels is set for the wireless -network using the *Range* slider. When two wireless nodes are within range of -each other, a green line is drawn between them and they are linked. Two -wireless nodes that are farther than the range pixels apart are not linked. -During Execute mode, users may move wireless nodes around by clicking and -dragging them, and wireless links will be dynamically made or broken. - -The *EMANE* tab lists available EMANE models to use for wireless networking. -See the [EMANE](emane.md) chapter for details on using EMANE. - -### Mobility Scripting - -CORE has a few ways to script mobility. - -| Option | Description | -|---|---| -| ns-2 script | the script specifies either absolute positions or waypoints with a velocity. Locations are given with Cartesian coordinates. | -| CORE API | an external entity can move nodes by sending CORE API Node messages with updated X,Y coordinates; the **coresendmsg** utility allows a shell script to generate these messages. | -| EMANE events | see [EMANE](emane.md) for details on using EMANE scripts to move nodes around. Location information is typically given as latitude, longitude, and altitude. | - -For the first method, you can create a mobility script using a text -editor, or using a tool such as [BonnMotion](http://net.cs.uni-bonn.de/wg/cs/applications/bonnmotion/), and associate the script with one of the wireless -using the WLAN configuration dialog box. Click the *ns-2 mobility script...* -button, and set the *mobility script file* field in the resulting *ns2script* -configuration dialog. - -Here is an example for creating a BonnMotion script for 10 nodes: - -```shell -bm -f sample RandomWaypoint -n 10 -d 60 -x 1000 -y 750 -bm NSFile -f sample -# use the resulting 'sample.ns_movements' file in CORE -``` - -When the Execute mode is started and one of the WLAN nodes has a mobility -script, a mobility script window will appear. This window contains controls for -starting, stopping, and resetting the running time for the mobility script. The -*loop* checkbox causes the script to play continuously. The *resolution* text -box contains the number of milliseconds between each timer event; lower values -cause the mobility to appear smoother but consumes greater CPU time. - -The format of an ns-2 mobility script looks like: - -```shell -# nodes: 3, max time: 35.000000, max x: 600.00, max y: 600.00 -$node_(2) set X_ 144.0 -$node_(2) set Y_ 240.0 -$node_(2) set Z_ 0.00 -$ns_ at 1.00 "$node_(2) setdest 130.0 280.0 15.0" -``` - -The first three lines set an initial position for node 2. The last line in the -above example causes node 2 to move towards the destination **(130, 280)** at -speed **15**. All units are screen coordinates, with speed in units per second. -The total script time is learned after all nodes have reached their waypoints. -Initially, the time slider in the mobility script dialog will not be -accurate. - -Examples mobility scripts (and their associated topology files) can be found in the **configs/** directory. - -## Multiple Canvases - -CORE supports multiple canvases for organizing emulated nodes. Nodes running on -different canvases may be linked together. - -To create a new canvas, choose *New* from the *Canvas* menu. A new canvas tab -appears in the bottom left corner. Clicking on a canvas tab switches to that -canvas. Double-click on one of the tabs to invoke the *Manage Canvases* dialog -box. Here, canvases may be renamed and reordered, and you can easily switch to -one of the canvases by selecting it. - -Each canvas maintains its own set of nodes and annotations. To link between -canvases, select a node and right-click on it, choose *Create link to*, choose -the target canvas from the list, and from that submenu the desired node. A -pseudo-link will be drawn, representing the link between the two nodes on -different canvases. Double-clicking on the label at the end of the arrow will -jump to the canvas that it links. - -## Check Emulation Light - -The |cel| Check Emulation Light, or CEL, is located in the bottom right-hand corner -of the status bar in the CORE GUI. This is a yellow icon that indicates one or -more problems with the running emulation. Clicking on the CEL will invoke the -CEL dialog. - -The Check Emulation Light dialog contains a list of exceptions received from -the CORE daemon. An exception has a time, severity level, optional node number, -and source. When the CEL is blinking, this indicates one or more fatal -exceptions. An exception with a fatal severity level indicates that one or more -of the basic pieces of emulation could not be created, such as failure to -create a bridge or namespace, or the failure to launch EMANE processes for an -EMANE-based network. - -Clicking on an exception displays details for that -exception. If a node number is specified, that node is highlighted on the -canvas when the exception is selected. The exception source is a text string -to help trace where the exception occurred; "service:UserDefined" for example, -would appear for a failed validation command with the UserDefined service. - -Buttons are available at the bottom of the dialog for clearing the exception -list and for viewing the CORE daemon and node log files. - -**NOTE:** - In batch mode, exceptions received from the CORE daemon are displayed on - the console. - -## Configuration Files - -Configurations are saved to **xml** or **.imn** topology files using -the *File* menu. You -can easily edit these files with a text editor. -Any time you edit the topology -file, you will need to stop the emulation if it were running and reload the -file. - -The **.xml** [file schema is specified by NRL](https://github.com/USNavalResearchLaboratory/NCS-Downloads/blob/master/mnmtools/EmulationScriptSchemaDescription.pdf) and there are two versions to date: -version 0.0 and version 1.0, -with 1.0 as the current default. CORE can open either XML version. However, the -xmlfilever line in **/etc/core/core.conf** controls the version of the XML file -that CORE will create. - -In version 1.0, the XML file is also referred to as the Scenario Plan. The Scenario Plan will be logically -made up of the following: - -| Plan | Description | -|---|---| -| **Network Plan** | describes nodes. hosts, interfaces, and the networks to which they belong. | -| **Motion Plan** | describes position and motion patterns for nodes in an emulation. | -| **Services Plan** | describes services (protocols, applications) and traffic flows that are associated with certain nodes. | -| **Visualization Plan** | meta-data that is not part of the NRL XML schema but used only by CORE. For example, GUI options, canvas and annotation info, etc. are contained here. | -| **Test Bed Mappings** | describes mappings of nodes, interfaces and EMANE modules in the scenario to test bed hardware. CORE includes Test Bed Mappings in XML files that are saved while the scenario is running. | - -The **.imn** file format comes from IMUNES, and is -basically Tcl lists of nodes, links, etc. -Tabs and spacing in the topology files are important. The file starts by -listing every node, then links, annotations, canvases, and options. Each entity -has a block contained in braces. The first block is indented by four spaces. -Within the **network-config** block (and any *custom-*-config* block), the -indentation is one tab character. - -**TIP:** - There are several topology examples included with CORE in - the **configs/** directory. - This directory can be found in **~/.core/configs**, or - installed to the filesystem - under **/usr[/local]/share/examples/configs**. - -**TIP:** - When using the **.imn** file format, file paths for things like custom - icons may contain the special variables **$CORE_DATA_DIR** or **$CONFDIR** which - will be substituted with **/usr/share/core** or **~/.core/configs**. - -**TIP:** - Feel free to edit the files directly using your favorite text editor. - -## Customizing your Topology's Look - -Several annotation tools are provided for changing the way your topology is -presented. Captions may be added with the Text tool. Ovals and rectangles may -be drawn in the background, helpful for visually grouping nodes together. - -During live demonstrations the marker tool may be helpful for drawing temporary -annotations on the canvas that may be quickly erased. A size and color palette -appears at the bottom of the toolbar when the marker tool is selected. Markings -are only temporary and are not saved in the topology file. - -The basic node icons can be replaced with a custom image of your choice. Icons -appear best when they use the GIF or PNG format with a transparent background. -To change a node's icon, double-click the node to invoke its configuration -dialog and click on the button to the right of the node name that shows the -node's current icon. - -A background image for the canvas may be set using the *Wallpaper...* option -from the *Canvas* menu. The image may be centered, tiled, or scaled to fit the -canvas size. An existing terrain, map, or network diagram could be used as a -background, for example, with CORE nodes drawn on top. - -## Preferences - -The *Preferences* Dialog can be accessed from the **Edit_Menu**. There are -numerous defaults that can be set with this dialog, which are stored in the -**~/.core/prefs.conf** preferences file. - - - From 0d490344fc54f23e2ea7e2d27f171a44d560b14f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 9 Apr 2020 14:39:09 -0700 Subject: [PATCH 0139/1131] fixed type on gui.md --- docs/gui.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/gui.md b/docs/gui.md index ac7382e3..303d85a4 100644 --- a/docs/gui.md +++ b/docs/gui.md @@ -181,7 +181,7 @@ Files. Generally, these menu items should not be used in Execute mode. | Option | Description | |---|---| | Undo | Attempts to undo the last edit in edit mode. | -| Redo* | Attempts to redo an edit that has been undone. | +| Redo | Attempts to redo an edit that has been undone. | | Cut, Copy, Paste | Used to cut, copy, and paste a selection. When nodes are pasted, their node numbers are automatically incremented, and existing links are preserved with new IP addresses assigned. Services and their customizations are copied to the new node, but care should be taken as node IP addresses have changed with possibly old addresses remaining in any custom service configurations. Annotations may also be copied and pasted. | Select All | Selects all items on the canvas. Selected items can be moved as a group. | | Select Adjacent | Select all nodes that are linked to the already selected node(s). For wireless nodes this simply selects the WLAN node(s) that the wireless node belongs to. You can use this by clicking on a node and pressing CTRL+N to select the adjacent nodes. | @@ -266,7 +266,7 @@ dialog box which is also accessible from the *Widgets* menu. Here are some standard widgets: -* *Adjacency* - displays router adjacency states for Quagga's OSPFv2 and OSPFv3 +* **Adjacency** - displays router adjacency states for Quagga's OSPFv2 and OSPFv3 routing protocols. A line is drawn from each router halfway to the router ID of an adjacent router. The color of the line is based on the OSPF adjacency state such as Two-way or Full. To learn about the different colors, see the @@ -274,7 +274,7 @@ Here are some standard widgets: dump OSPF neighbor information. Only half of the line is drawn because each router may be in a different adjacency state with respect to the other. -* *Throughput* - displays the kilobits-per-second throughput above each link, +* **Throughput** - displays the kilobits-per-second throughput above each link, using statistics gathered from the ng_pipe Netgraph node that implements each link. If the throughput exceeds a certain threshold, the link will become highlighted. For wireless nodes which broadcast data to all nodes in range, From 36b3243a4b1da04354cb3848245ceb689a40a4b8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 9 Apr 2020 21:33:20 -0700 Subject: [PATCH 0140/1131] updates to NOTEs in documentation --- docs/ctrlnet.md | 10 +++++----- docs/emane.md | 2 +- docs/gui.md | 12 ++++++------ docs/performance.md | 4 ++-- docs/scripting.md | 2 +- docs/services.md | 8 ++++---- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/ctrlnet.md b/docs/ctrlnet.md index 5b82d7e8..9ecc2e3f 100644 --- a/docs/ctrlnet.md +++ b/docs/ctrlnet.md @@ -30,11 +30,11 @@ new sessions will use by default. To simultaneously run multiple sessions with control networks, the session option should be used instead of the *core.conf* default. -> :warning: If you have a large scenario with more than 253 nodes, use a control +> **NOTE:** If you have a large scenario with more than 253 nodes, use a control network prefix that allows more than the suggested */24*, such as */23* or greater. -> :warning: Running a session with a control network can fail if a previous +> **NOTE:** Running a session with a control network can fail if a previous session has set up a control network and the its bridge is still up. Close the previous session first or wait for it to complete. If unable to, the *core-daemon* may need to be restarted and the lingering bridge(s) removed @@ -52,7 +52,7 @@ for cb in $ctrlbridges; do done ``` -> :bulb: If adjustments to the primary control network configuration made in +> **NOTE:** If adjustments to the primary control network configuration made in */etc/core/core.conf* do not seem to take affect, check if there is anything set in the *Session Menu*, the *Options...* dialog. They may need to be cleared. These per session settings override the defaults in @@ -120,7 +120,7 @@ assign *ctrl1* to the OTA manager device and *ctrl2* to the Event Service device in the EMANE Options dialog box and leave *ctrl0* for CORE control traffic. -> :warning: *controlnet0* may be used in place of *controlnet* to configure +> **NOTE:** *controlnet0* may be used in place of *controlnet* to configure >the primary control network. Unlike the primary control network, the auxiliary control networks will not @@ -139,7 +139,7 @@ controlnetif2 = eth2 controlnetif3 = eth3 ``` -> :warning: There is no need to assign an interface to the primary control +> **NOTE:** There is no need to assign an interface to the primary control >network because tunnels are formed between the master and the slaves using IP >addresses that are provided in *servers.conf*. diff --git a/docs/emane.md b/docs/emane.md index 03039db0..b3289c6a 100644 --- a/docs/emane.md +++ b/docs/emane.md @@ -224,7 +224,7 @@ configured. Click *Apply* to save these settings. ![](static/distributed-emane-configuration.png) -> :bulb: Here is a quick checklist for distributed emulation with EMANE. +> **NOTE:** Here is a quick checklist for distributed emulation with EMANE. 1. Follow the steps outlined for normal CORE. 2. Under the *EMANE* tab of the EMANE WLAN, click on *EMANE options*. diff --git a/docs/gui.md b/docs/gui.md index 303d85a4..fc88d01c 100644 --- a/docs/gui.md +++ b/docs/gui.md @@ -369,7 +369,7 @@ physical interface. A list of available interfaces will be shown, and one may be selected by double-clicking its name in the list, or an interface name may be entered into the text box. -> :warning: When you press the Start button to instantiate your topology, the +> **NOTE:** When you press the Start button to instantiate your topology, the interface assigned to the RJ45 will be connected to the CORE topology. The interface can no longer be used by the system. For example, if there was an IP address assigned to the physical interface before execution, the address @@ -409,7 +409,7 @@ the node will display "UNASSIGNED". This text should be replaced with the IP address of the tunnel peer. This is the IP address of the other CORE machine or physical machine, not an IP address of another virtual node. -> :warning: Be aware of possible MTU (Maximum Transmission Unit) issues with GRE devices. The *gretap* device +> **NOTE:** Be aware of possible MTU (Maximum Transmission Unit) issues with GRE devices. The *gretap* device has an interface MTU of 1,458 bytes; when joined to a Linux bridge, the bridge's MTU becomes 1,458 bytes. The Linux bridge will not perform fragmentation for @@ -674,7 +674,7 @@ would appear for a failed validation command with the UserDefined service. Buttons are available at the bottom of the dialog for clearing the exception list and for viewing the CORE daemon and node log files. -> :warning: In batch mode, exceptions received from the CORE daemon are displayed on +> **NOTE:** In batch mode, exceptions received from the CORE daemon are displayed on the console. ## Configuration Files @@ -694,17 +694,17 @@ has a block contained in braces. The first block is indented by four spaces. Within the **network-config** block (and any *custom-*-config* block), the indentation is one tab character. -> :bulb: There are several topology examples included with CORE in +> **NOTE:** There are several topology examples included with CORE in the **configs/** directory. This directory can be found in **~/.core/configs**, or installed to the filesystem under **/usr[/local]/share/examples/configs**. -> :bulb: When using the **.imn** file format, file paths for things like custom +> **NOTE:** When using the **.imn** file format, file paths for things like custom icons may contain the special variables **$CORE_DATA_DIR** or **$CONFDIR** which will be substituted with **/usr/share/core** or **~/.core/configs**. -> :bulb: Feel free to edit the files directly using your favorite text editor. +> **NOTE:** Feel free to edit the files directly using your favorite text editor. ## Customizing your Topology's Look diff --git a/docs/performance.md b/docs/performance.md index d106b06e..b088c65b 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -32,8 +32,8 @@ the number of times the system as a whole needed to deal with a packet. As more network hops are added, this increases the number of context switches and decreases the throughput seen on the full length of the network path. -> :warning: The right question to be asking is *"how much traffic?"*, not -*"how many nodes?"*.** +> **NOTE:** The right question to be asking is *"how much traffic?"*, not +*"how many nodes?"*. For a more detailed study of performance in CORE, refer to the following publications: diff --git a/docs/scripting.md b/docs/scripting.md index 0c4f13f3..aafbb7d3 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -66,7 +66,7 @@ The CORE Python modules are documented with comments in the code. From an interactive Python shell, you can retrieve online help about the various classes and methods; for example *help(CoreNode)* or *help(Session)*. -> :warning: The CORE daemon *core-daemon* manages a list of sessions and allows +> **NOTE:** The CORE daemon *core-daemon* manages a list of sessions and allows the GUI to connect and control sessions. Your Python script uses the same CORE modules but runs independently of the daemon. The daemon does not need to be running for your script to work. diff --git a/docs/services.md b/docs/services.md index 8e64e024..e70a0c75 100644 --- a/docs/services.md +++ b/docs/services.md @@ -15,7 +15,7 @@ set of default services. Each service defines the per-node directories, configuration files, startup index, starting commands, validation commands, shutdown commands, and meta-data associated with a node. -> :warning: **Network namespace nodes do not undergo the normal Linux boot process** +> **NOTE:** **Network namespace nodes do not undergo the normal Linux boot process** using the **init**, **upstart**, or **systemd** frameworks. These lightweight nodes use configured CORE *services*. @@ -79,7 +79,7 @@ the service customization dialog for that service. The dialog has three tabs for configuring the different aspects of the service: files, directories, and startup/shutdown. -> :warning: A **yellow** customize icon next to a service indicates that service +> **NOTE:** A **yellow** customize icon next to a service indicates that service requires customization (e.g. the *Firewall* service). A **green** customize icon indicates that a custom configuration exists. Click the *Defaults* button when customizing a service to remove any @@ -98,7 +98,7 @@ per-node directories that are defined by the services. For example, the the Zebra service, because Quagga running on each node needs to write separate PID files to that directory. -> :warning: The **/var/log** and **/var/run** directories are +> **NOTE:** The **/var/log** and **/var/run** directories are mounted uniquely per-node by default. Per-node mount targets can be found in **/tmp/pycore.nnnnn/nN.conf/** (where *nnnnn* is the session number and *N* is the node number.) @@ -128,7 +128,7 @@ if a process is running and return zero when found. When a validate command produces a non-zero return value, an exception is generated, which will cause an error to be displayed in the Check Emulation Light. -> :bulb: To start, stop, and restart services during run-time, right-click a +> **NOTE:** To start, stop, and restart services during run-time, right-click a node and use the *Services...* menu. ## New Services From 73a96ffcf53c6723dc54bcf135471e21415660f2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 9 Apr 2020 21:42:04 -0700 Subject: [PATCH 0141/1131] added section on install page about using install.sh --- docs/install.md | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/install.md b/docs/install.md index 23d9d851..90fe3389 100644 --- a/docs/install.md +++ b/docs/install.md @@ -21,8 +21,8 @@ technology that CORE currently uses is Linux network namespaces. Ubuntu and CentOS Linux are the recommended distributions for running CORE. However, these distributions are not strictly required. CORE will likely work on other flavors of Linux as well, assuming dependencies are met. -**NOTE: CORE Services determine what run on each node. You may require other software packages depending on the -services you wish to use. For example, the HTTP service will require the apache2 package.** +> **NOTE:** CORE Services determine what run on each node. You may require other software packages depending on the +services you wish to use. For example, the HTTP service will require the apache2 package. ## Installed Files @@ -43,6 +43,30 @@ Install Path | Description /etc/init.d/core-daemon|SysV startup script for daemon /usr/lib/systemd/system/core-daemon.service|Systemd startup script for daemon +## Automated Install + +There is a helper script in the root of the repository that can help automate +the CORE installation. Some steps require commands be ran as sudo and you +will be prompted for a password. This should work on Ubuntu/CentOS and will +install system dependencies, python dependencies, and CORE. This will target +system installations of python 3.6. + +```shell +git clone https://github.com/coreemu/core.git +cd core +./install.sh +``` + +You can target newer system python versions using the **-v** flag. Assuming +these versions are actually available on your system. + +```shell +# ubuntu 3.7 +./install.sh -v 3.7 +# centos 3.7 +./install.sh -v 37 +``` + ## Pre-Req Installing Python Python 3.6 is the minimum required python version. Newer versions can be used if available. From 27a6c76d572cfe23cc37091ed2c6f30ff0573763 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 13 Apr 2020 08:52:37 -0700 Subject: [PATCH 0142/1131] small tweak to gui docs --- docs/gui.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/gui.md b/docs/gui.md index fc88d01c..7b2f061c 100644 --- a/docs/gui.md +++ b/docs/gui.md @@ -18,8 +18,8 @@ staying in the **runtime** phase. After the session is stopped, CORE will proceed to the **data collection** phase before tearing down the emulated state. -CORE can be customized to perform any action at each phase in the workflow -above. See the *Hooks...* entry on the **Session Menu** for details about +CORE can be customized to perform any action at each state. See the +**Hooks...** entry on the [Session Menu](#session-menu) for details about when these session states are reached. ## Prerequisites From 928bfc73dc9983948c904c21cb9289e3dd68c801 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 13 Apr 2020 11:51:12 -0700 Subject: [PATCH 0143/1131] updates to core architecture diagrams leveraging plantuml, removed usage of virtual machine language in docs to help avoid confusion --- docs/architecture.md | 39 ++++++++++++---------- docs/diagrams/architecture.plantuml | 49 ++++++++++++++++++++++++++++ docs/diagrams/workflow.plantuml | 40 +++++++++++++++++++++++ docs/gui.md | 2 +- docs/index.md | 2 +- docs/install.md | 6 ++-- docs/nodetypes.md | 15 ++++----- docs/static/architecture.png | Bin 0 -> 26994 bytes docs/static/core-architecture.jpg | Bin 38730 -> 0 bytes docs/static/core-workflow.jpg | Bin 21243 -> 0 bytes docs/static/workflow.png | Bin 0 -> 17907 bytes 11 files changed, 123 insertions(+), 30 deletions(-) create mode 100644 docs/diagrams/architecture.plantuml create mode 100644 docs/diagrams/workflow.plantuml create mode 100644 docs/static/architecture.png delete mode 100644 docs/static/core-architecture.jpg delete mode 100644 docs/static/core-workflow.jpg create mode 100644 docs/static/workflow.png diff --git a/docs/architecture.md b/docs/architecture.md index 605d5ad0..bc0c628b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -5,29 +5,34 @@ ## Main Components -* CORE Daemon - * Manages emulation sessions - * Builds the emulated networks using Linux namespaces for nodes and - some form of bridging and packet manipulation for virtual networks - * Nodes and networks come together via interfaces installed on nodes +* core-daemon + * Manages emulated sessions of nodes and links for a given network + * Nodes are created using Linux namespaces + * Links are created using Linux bridges and virtual ethernet peers + * Packets sent over links are manipulated using traffic control * Controlled via the CORE GUI - * Written in python and can be scripted, given direct control of scenarios -* CORE GUI - * GUI and daemon communicate using a custom, asynchronous, sockets-based - API, known as the CORE API - * Drag and drop creation for nodes and network interfaces - * Can launch terminals for emulated nodes in running scenarios + * Provides both a custo TLV API and gRPC API + * Python program that leverages a small C binary for node creation +* core-gui + * GUI and daemon communicate over the custom TLV API + * Drag and drop creation for nodes and links + * Can launch terminals for emulated nodes in running sessions * Can save/open scenario files to recreate previous sessions * TCL/TK program +* coresendmsg + * Command line utility for sending TLV API messages to the core-daemon +* vcmd + * Command line utility for sending shell commands to nodes -![](static/core-architecture.jpg) +![](static/architecture.png) ## Sessions -CORE can create and run multiple sessions at once, below is a high level -overview of the states a session will go between. +CORE can create and run multiple emulated sessions at once, below is an +overview of the states a session will transition between during typical +GUI interactions. -![](static/core-workflow.jpg) +![](static/workflow.png) ## How Does it Work? @@ -38,7 +43,7 @@ together for a specific purpose. ### Linux -Linux network namespaces (also known as netns) is the primary virtualization +Linux network namespaces (also known as netns) is the primary technique used by CORE. Most recent Linux distributions have namespaces-enabled kernels out of the box. Each namespace has its own process environment and private network stack. Network namespaces share the same @@ -56,7 +61,7 @@ The Tcl/Tk CORE GUI was originally derived from the open source [IMUNES](http://imunes.net) project from the University of Zagreb as a custom project within Boeing Research and Technology's Network Technology research group in 2004. Since then they have developed the CORE framework to use Linux -virtualization, have developed a Python framework, and made numerous user and +namespacing, have developed a Python framework, and made numerous user and kernel-space developments, such as support for wireless networks, IPsec, distribute emulation, simulation integration, and more. The IMUNES project also consists of userspace and kernel components. diff --git a/docs/diagrams/architecture.plantuml b/docs/diagrams/architecture.plantuml new file mode 100644 index 00000000..a43494d5 --- /dev/null +++ b/docs/diagrams/architecture.plantuml @@ -0,0 +1,49 @@ +@startuml +skinparam { + RoundCorner 8 + ComponentStyle uml2 + ComponentBorderColor #Black + InterfaceBorderColor #Black + InterfaceBackgroundColor #Yellow +} + +package User { + component "core-gui" as gui #DeepSkyBlue + component "coresendmsg" #DeepSkyBlue + component "python scripts" as scripts #DeepSkyBlue + component vcmd #DeepSkyBlue +} +package Server { + component "core-daemon" as daemon #DarkSeaGreen +} +package Python { + component core #LightSteelBlue +} +package "Linux System" { + component nodes #SpringGreen [ + nodes + (linux namespaces) + ] + component links #SpringGreen [ + links + (bridging and traffic manipulation) + ] +} + +package API { + interface TLV as tlv + interface gRPC as grpc +} + +gui <..> tlv +coresendmsg <..> tlv +scripts <..> tlv +scripts <..> grpc +tlv -- daemon +grpc -- daemon +scripts -- core +daemon - core +core <..> nodes +core <..> links +vcmd <..> nodes +@enduml \ No newline at end of file diff --git a/docs/diagrams/workflow.plantuml b/docs/diagrams/workflow.plantuml new file mode 100644 index 00000000..9aa1c04f --- /dev/null +++ b/docs/diagrams/workflow.plantuml @@ -0,0 +1,40 @@ +@startuml +skinparam { + RoundCorner 8 + StateBorderColor #Black + StateBackgroundColor #LightSteelBlue +} + +Definition: Session XML/IMN +Definition: GUI Drawing +Definition: Scripts + +Configuration: Configure Hooks +Configuration: Configure Services +Configuration: Configure WLAN / Mobility +Configuration: Configure EMANE + +Instantiation: Create Nodes +Instantiation: Create Interfaces +Instantiation: Create Bridges +Instantiation: Start Services + +Runtime: Interactive Shells +Runtime: Traffic Scripts +Runtime: Mobility +Runtime: Widgets + +Datacollect: Collect Files +Datacollect: Other Results + +Shutdown: Shutdown Services +Shutdown: Destroy Brdges +Shutdown: Destroy Interfaces +Shutdown: Destroy Nodes + +Definition -> Configuration +Configuration -> Instantiation +Instantiation -> Runtime +Runtime -> Datacollect +Datacollect -> Shutdown +@enduml \ No newline at end of file diff --git a/docs/gui.md b/docs/gui.md index 7b2f061c..8f6f9057 100644 --- a/docs/gui.md +++ b/docs/gui.md @@ -312,7 +312,7 @@ and options. | Change sessions... | Invokes the CORE Sessions dialog box containing a list of active CORE sessions in the daemon. Basic session information such as name, node count, start time, and a thumbnail are displayed. This dialog allows connecting to different sessions, shutting them down, or starting a new session. | | Node types... | Invokes the CORE Node Types dialog, performing the same function as the Edit button on the Network-Layer Nodes toolbar. | | Comments... | Invokes the CORE Session Comments window where optional text comments may be specified. These comments are saved at the top of the configuration file, and can be useful for describing the topology or how to use the network. | -| Hooks... | Invokes the CORE Session Hooks window where scripts may be configured for a particular session state. The session states are defined in the table right below. The top of the window has a list of configured hooks, and buttons on the bottom left allow adding, editing, and removing hook scripts. The new or edit button will open a hook script editing window. A hook script is a shell script invoked on the host (not within a virtual node). | +| Hooks... | Invokes the CORE Session Hooks window where scripts may be configured for a particular session state. The session states are defined in the [table](#session-states) below. The top of the window has a list of configured hooks, and buttons on the bottom left allow adding, editing, and removing hook scripts. The new or edit button will open a hook script editing window. A hook script is a shell script invoked on the host (not within a virtual node). | | Reset node positions | If you have moved nodes around using the mouse or by using a mobility module, choosing this item will reset all nodes to their original position on the canvas. The node locations are remembered when you first press the Start button. | | Emulation servers... | Invokes the CORE emulation servers dialog for configuring. | | Change Sessions... | Invokes the Sessions dialog for switching between different running sessions. This dialog is presented during startup when one or more sessions are already running. | diff --git a/docs/index.md b/docs/index.md index f6f059d2..12932880 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,7 +5,7 @@ CORE (Common Open Research Emulator) is a tool for building virtual networks. As an emulator, CORE builds a representation of a real computer network that runs in real time, as opposed to simulation, where abstract models are used. The live-running emulation can be connected to physical networks and routers. It provides an environment for -running real applications and protocols, taking advantage of virtualization provided by the Linux operating system. +running real applications and protocols, taking advantage of tools provided by the Linux operating system. CORE is typically used for network and protocol research, demonstrations, application and platform testing, evaluating networking scenarios, security studies, and increasing the size of physical test networks. diff --git a/docs/install.md b/docs/install.md index 90fe3389..4ab6ed2e 100644 --- a/docs/install.md +++ b/docs/install.md @@ -10,12 +10,12 @@ This section will describe how to install CORE from source or from a pre-built p ## Required Hardware Any computer capable of running Linux should be able to run CORE. Since the physical machine will be hosting numerous -virtual machines, as a general rule you should select a machine having as much RAM and CPU resources as possible. +containers, as a general rule you should select a machine having as much RAM and CPU resources as possible. ## Operating System -CORE requires a Linux operating system because it uses virtualization provided by the kernel. It does not run on -Windows or Mac OS X operating systems (unless it is running within a virtual machine guest.) The virtualization +CORE requires a Linux operating system because it uses namespacing provided by the kernel. It does not run on +Windows or Mac OS X operating systems (unless it is running within a virtual machine guest.) The technology that CORE currently uses is Linux network namespaces. Ubuntu and CentOS Linux are the recommended distributions for running CORE. However, these distributions are diff --git a/docs/nodetypes.md b/docs/nodetypes.md index 41e7beb7..ffa27855 100644 --- a/docs/nodetypes.md +++ b/docs/nodetypes.md @@ -7,15 +7,14 @@ Different node types can be configured in CORE, and each node type has a *machine type* that indicates how the node will be represented at run time. -Different machine types allow for different virtualization options. +Different machine types allow for different options. ## Netns Nodes The *netns* machine type is the default. This is for nodes that will be -backed by Linux network namespaces. This default machine type is very -lightweight, providing a minimum amount of virtualization in order to -emulate a network. Another reason this is designated as the default -machine type is because this virtualization technology typically +backed by Linux network namespaces. This machine type uses very little +system resources in order to emulate a network. Another reason this is +designated as the default machine type is because this technology typically requires no changes to the kernel; it is available out-of-the-box from the latest mainstream Linux distributions. @@ -25,9 +24,9 @@ The *physical* machine type is used for nodes that represent a real Linux-based machine that will participate in the emulated network scenario. This is typically used, for example, to incorporate racks of server machines from an emulation testbed. A physical node is one that is running the CORE daemon -(*core-daemon*), but will not be further partitioned into virtual machines. -Services that are run on the physical node do not run in an isolated or -virtualized environment, but directly on the operating system. +(*core-daemon*), but will not be further partitioned into containers. +Services that are run on the physical node do not run in an isolated +environment, but directly on the operating system. Physical nodes must be assigned to servers, the same way nodes are assigned to emulation servers with *Distributed Emulation*. The list of available physical diff --git a/docs/static/architecture.png b/docs/static/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..e07542a42fb920d58c1ec86fc9dd8fecef28a934 GIT binary patch literal 26994 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 literal 0 HcmV?d00001 diff --git a/docs/static/core-architecture.jpg b/docs/static/core-architecture.jpg deleted file mode 100644 index 04f6390f417426deea49436e0b76a180c9957db8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38730 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 H%g}!S$Di%@ diff --git a/docs/static/core-workflow.jpg b/docs/static/core-workflow.jpg deleted file mode 100644 index b60eff7dd43bd451c4510912043d5cfcc0622904..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21243 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 Wdova_Sa(5NE9kA4f#%!)zX<>+2HfNT diff --git a/docs/static/workflow.png b/docs/static/workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..cff86d07f0c221c861001558bd2a06e434875cd8 GIT binary patch literal 17907 zcmagFWmHvN7d8wcozfwVpwe(?kdhRT?(XhxNs$I64lUgshek>o>240)-S5WxexB$3 z-ap^|p$r{kv)0^m%{8x>Ve)bk=qSV}FfcIaQj#APVPN2vfS+xVUI2gUGHQPZ{(9#u zrtWNHXYXNcV(JVdVPb3IXy9yOOkwCjVeaf~&&$kgZ*5@f>|$fhWMpUaiiPJj49ts9 z7Ru_*|M_Jt%N!) z9;KryJHOoOBsz~v=<#*`E_K9hxoIP(YC)YV5ws{vJ=fZ->W7(RRV4-9POGXVD(~^z!rLG-b9}1a5pA;g zan{#DMRX!4P4``A`~svIch2J9oK;jy1kmCbSf+y8wvD(I@rS-|JzWtJDtsqj8LY2R zkZiXl{lnAaBA>2RnJI1d)@0J(E#z}P)pCCW<-8(So`j!U=|zlIy45?@`qt~o7feCt zZbubXeA$(d?*6`8V@{{KQRWX@(<;;ljIRw%B6SrzTw0E#luEA7RkolNLf3e%bC-GH z=|{hB#Z5-^YG=N#*+hD}eY>(wQ*W1($1AprSd>KZ$e^tdss>-X7UR_3l@_!mjvY?? z8085mAg{kWQ6RAmaa+0hFyGWHl{q%NffSs{+ue23bvcWtXFqekb4xTid3-kqvt>yc zq3WcZpd*l@6}oQAjdd|2A5S*MJ1m-}D&ujX`Da9~9_KRE@AFSxLI|>$OQ_#`SL0IG zTQGtAj~GV!@H0Z}tOaJ*G#gRUS`00PnaE$*J6u$b%lLBC!jR3z%xcz!Wt|{_1bY=; zT}8Zt0s-el4eWs#LCe;Na8={79T*rQZhfoc);4#s_FDctqT{9 zXOHe%&6_$$Etmt}f9%ewOv<{sxLjS8wy3A<61yJQ-5%&P`I=ll$^Y*$rZAm>fBWAD zX-MIJ|Az(6&iv0q0g@2@-(wyX@ZV?jHT?gcEFP4$`_lpDXflDuNW+yE()SnBdVenE z7YnQ=*icYBe^NHS7jcSDAmOqt(Tv~R;~GWl-ln0jKc3saASESb12wCa>vC|^GpK*x zS=|f-k9~WINmS=^7v*(zm{q1Q@b+!e2|Di+6^Kg#;5a$I6OcKq+C?=g^3?QCrmFy-mr32dG5a;aa`N0D>E+mv7J zrdPkUy8ikHCHRFWhk(yqwnVi2bb$#j?%*{7*zXlM<)`()EC&Ai{_f*y)P1AY=M=#w zE(UQ8AxP%hm%z|3jY^7&zIO*)55!C11kAreBt(otLZbb#reMXbVgolSK{QK^Zq~~`hG<#mW_qml}`QA59kQok^`{KYKv9c(@-`LdjbiE$!_pqlo zHm|FumROqB5;=z9Pi*oUy^u$kWDqN0X_!QjI~TV%(Z_1@^LL{pOz zHrh@lwWHgr_`!Yt24@2U%H6rIEn0?**@QvADcARY2coea6s7nD|NRY!p^GJMTfDrG z?T6san}MmjYs?S~2WQvFs80b=yFCqo zi$ED?a(_E7Xk4;@X3>(N;%BCIb?u;pky>^XiC9RY6Up|3BILBOz>0y<1RRUmxmmjW zmi%5h7(_B`d=5&N%dn!$kohNWuN%90%yc47Z)3aR+1`cbox{)Ev3a&9n1mqr;}sw) z>+0%006%qy6ZqYqghpA93=GiI8pQc}>F$j#`0pD7>A$qJ6q0LI>q!{UchXRCN$jo$^|(9)`1pJE^e1S8PwdL<&wlA;5Ohw{trg?R zWn$Og0^U(dR;m4X78VRG1IuzyhW-F{l7upj-^(ULSFckMj%n{9Y z&yMsnvs}wNbT&4>_{w;ERIy6^X_s%O*jmutbY;Z=A3wo1z4a{7F2*{!+*nhb6)Bc& zJ&+)V?E6UjR8p9QgsMdKZgai~*b(?v>R_l6!7 zNMe}OXsx2j?TF0WNliGI%q~^hB1f;@$D3IHWXWhUu}mz^#yhJhU3&m%whaaXkSrv<@#h9;3G=f{3g z!i`fQC&{0X(2=qE4mB~6v!7?BsWweBkBry+ey39<54T=Em(I}w`^$Q|x^e=lfnS=K zrZg=694CRPF3kL_NG!!a-owc!ICiTO?*yIJ#HYf&LCCin!msIylzoLfG&#wpOku9% zf@E0Y2U|E^EFG2l@cqMUnLnNLpWjC(NIsu?MPBNcAU?0&KkIo+14|wS4XcjRPI-u7 z=Nx3_7@hjW_DYF3ICA4lI#td#Ov)>Eb8*2+f#Rdz15V>-K`x$^QC z38U;C$tkE=tG|L#Wo2a-7Z;iJT0ywxa=RlMM8y8k;pFj6zx+vjMO~KY_V_1Yzzb6 zm^2K{n(YrhAwdeO@evS$8$GUHe{j;!(ObWfve*)m-sWEgF}GQNyl7NDUX&k)cLZ52r=pS zDTB#Oj+6fhgN)7k?vnKCaFLzVe3*g6jr@$1TOHWhiF`VQGu>Wu{)80m2fZelx`sxl z9v>pz_54VY-{R2GMS5blJ3S(3nsf`prYYf}M0=sd5IasdoQjBZBDW8RNJTcNQx0R4 zA2=fE(?WWJ$?pxKx{@$&Cj+1*@Qm{LepR(@#&V^2-)m#`U*1EYJtRzZ(Q;FxZSQeW zrr?^EYMysKU8aMFhqpD7QC?i!Lr{4PUFU$K9sk47y=bkygRS2Lgvq#5smL0 z-d*M_G~1Eq=N3S_$Hrkk?1?RT%0Y%{0?8T;z(2G{dBe|Zt9aY|HP)!0+f+WZ&5*35 zuS07nXJf&j41*uIwoW-Q~Qh9G7{19w-lgG99y_Ah?FU~cp%WZy8m_=CM&Su6V zsqdUy^Bhnu-`yd+3}(DH3w+wNg%69WZ@9`qqfvtab-pih|S3X~6wKc=_5GZhU zPHo}+nj!RGCqG@s1# z5!jV~t2~*756TDatsTOHFODy@Qk)vfgW+3fnBJL$qy^%bzlMuQvEnZyUEvTpY1=n`R~v@|YqFeGifqE$#(K}Wm0RkhWKcI;KrGFHXP z^**HWEXe0oj+@f&AaQS8djj)G#G^mExH{DDn{DnK2;MzR){-tG-x5cMRxMzmP$+&w zC&K0p=a0pe^TOv+)y`g?`Jmy_61tPM2F?<3N^?B4U%JU!LuhN)7e0L_heaC#mKgpg zKrg|2x&B`|^kL3|-nDZu04-(}X!LbvKy4&?&=mWH&dd}48=GN8`Jts2uQ%(`;%1;1 zb{z4F*{P$i<+;)7UiRfnj%vsJeO<_J12ZfW>;CigA2#}$p(ENM9b{*$`1nCqz*{*i zAzUw{`nxC5?L9mOeD=LNu;W>zvg9WwCL}w5L=1l&)#e**2HHJff!H8|9B3^2d|Krx zc&Ej?fqI#X0%;#5u6L{!!eC=4+z`?z4z&0j34? z7VmoiBM5w6M@4i#VanLPGh<3%&LpmHb1J zH^TTgl!!&0c&Zl-rn&;`(NTc~!3sMuGc%_%D<5z8xJboEB7>F6$2`2eKDGZ7AR8rL zcF($fM0xj#q_CoFGwhr1J)_#du97$^DYQES8hZ@iYCZzka#aN6*KCTFAO`l{9UE%M z>67L|{C!&)_U=b{;Ww4AXdbuh{>wnK7lER%YVJ1()-x&}?Y34WRBcG;9N2g6i-vF* zobXLL(y{H|WV2I&dJPFr&ijAR+h+}8I?3*yS*uIJGr;Z}%-7kNyZ`;R8qNIP*A=0S zE5{JsVJ3E|_=o?u#qv9eaiOr_GLOivwmMv`CYWy%aD8O=KROR|AoDd=4GwKccJ_6l z+t*JW6)n|K&ao;pb{3~TRt+Qnu2`{wD=0oe|rJEzC)=W}9NmOF==l;$NZ* zGQ4e#quOl$U0wqkE-EqEzpF~X00WyBr8Y7%DdC--Se&_OKAch-YckGR{O*cjm<2%* zWYbxtw53hcV>f5|n;B&sy#X3H|9iz$3~08$zY*FKFX%yGDcRMVvj|gVx9In+B2&zt zJh|)`iz|M3#4^1^-FcMg71*c)NJex_bWE2>0ApH-fL~+b0{xM#O|KjLmS*zpr7q|13fUY$RinKM1Psj5HMM)ccmG+auf@`gsX5ZvvpdAx4T3Y92Qx zkAWeYnwMwjNOR3sL8h)y=#Cd~de3bOyO~8PL5Uw?t-3Eg%O$vJ%+-TbVeogsO49)k z)WV2WOn(OG6Y3W}ODC^xZy4HSqeVw8D>=hv@!;QxI+f0s>MkTZ6Ci*RMqh%dx1x4f zbn#TQtKFQ3dE~t-ALfD5k=>hbX=>&(IHZx`EM^`h6(AUEHE>z;Vu;!Zo9lolYIMOl z(}Vd_*KgKwC{wYy2h3E{oweJKwaB_WK{y}mT z7l-9^hp2_g-PCyXjPY!CZyg>_(aXq|q_ysl3#+Yo(+l8-r0Uz9tdw(Cw8O_?xCq%4 zm=WM#2CH9IpI(x8-Cwg4>mK3o^j&@ge_2UvDO~)h`AWSizuvF&!0&4f)r?br1X@F5 zQ{qukBZ#+Nrt2>}iNQ}G_ zf~QjXbEC@2*_nf4uvoZ@e!3jHJMa~pj3VL`(HLN1)YXM9m*{r~u9Ds!RZx~Nk817DSFNZ6P@jqx{1gw+ zK3yYAt1B;fefp3i%TH@CmVC=pG3=4NryN$0bior|_0urE{Uq~agftf1kP&v=JBi>b z8Y>BRXKyJVvq`h)o;;ZhPA(-zbGb(NbSrJ~yWNqD!fcYP0wsJ5vvE zN^(}{-}H?Bo>`mY+hn|=8&Pk^aRm+uWt*bbC9Vz<8U~&PJLRivMKfkz9jBB5PafNt zttfT8NLlzS9&sIEjzX(?{ys1y4HuJVoerdb0euj#+qPXWvzWCTN{BAF!jPVm~D@W{U=BOS>j0*Q|o&FQH4(>)PNohWPyVipSwFa!r&z0#EqQ9pb}ag z_63@Y?zVHV!HZa=_mlQ$yRK52B*sAI6{nxcpr^!4L7 zvxYZeWC^IRbvr-54D}BPp&KfJ5VCVSUXCAcbLW-{JSDbFR{ew@;F8Oz*M_6_Y!X~` zyyc+L5O!jnm-=UuEVZ4OLk2XaHrYTg3y)hM568~RDVlhuMkbe%LWKRQ(=U+UtxH!v zzp#)3)LTHWD@%2tTGAP}MyYmR(sUz`GY5{MTY;=N04LmkB?d#ksj=Lu`rqvhBcO(4 zFMV_gf^HYTuo^1tm0B|!>K1hxE8$TWY@m85;^>=i@rE0oaI?54E`LiYE0qs+}{XytT`11M|Ir_oBP;WsB3Vu9|N04gYC6+Whc( zB2$R-L*>e2!3mSGl%sChg%q&WP5xG=GeNKy~sm#Ka zP$GIRB@&=!pbnI^vo!WzrRo6vdtz4bI8ZKOyMCs^bzju`y)A5ldz2$L0muP5Wb_hq zA6?2WD`ENoe>mF3>bj~z64%TtIvu&T5E?&oCPk}~PASfqN<$@cV+LzuCPEXPfF&YF zq6YpTA5Zuq!W3Ae8K0UefSm5P(i`qq$HQj*_Jja`^tH^~`hA=Q(aw*}ksmUS;w}dX z-%B)>K7I?$xrQ)!<=2k6N=>;Iz)6SPp1L%30coN0HG-swX@s2D`PF1C!qDcRs?y`m z@&RdDblMZz?lHmcgyx%uaADD3Ol|`CSH`WG?S+mNzs|1`s;Fq6q&%gX@=D!rGotFgJsVQFzH-UY3#Ps~gX|h3N1p-{FzmQ3tP!fbfLj3IqT1iGynkzRpZfm@o}rR{}0 zO`2UU)I3wJZTuYqIQ~$?ctPI9<8d@Ayr7K3yo^Z;ovsvZOI$dejn$Y}V;%#8jCjfZ zj{MloX4v3Xh)}L%gg(asMjcZS6j~zVladG_d6JT>u^Q_g*CV#&Xp%kjHtT%4-$jHl zV!fn$=^wS17~wMY+WM$U>2xt!>+y>DDGGHXLT`GwFJojWq>kqeUJET{9-_ISc> zsZqyk>6-CE;YzTPe4QS)#|-&rCZy4tBejMtl?2|Szbj^^*|+BvIf#MEaDx!)j!FzA z!TJ~(3FF%7;*oiCLyu`dfz(Nh4)2(M%^hn+8GjV?W4-@X0a-R9yGO!N!a*~e%Cd!qK06-4z<_U1@P%T-k_ zYAu5jsc;Whw8e?+Q(+BJ7Nk$u$z|4(+rosK3#FBnB18tUzjb}Gg6^#Hg$q={i1wGe zes9)TK~m*Vvz{J+wz+{mba*S`o^CZqXFR){bB0E@j$F@(v*?e(uX;%L)77Nk&VT}Y z^;xRUB9Q0?=OS&g0p04;;J|~)3lj7Ywec;RqINWcjmA9v4ha54t%4<_)lVc%jQ?$8 z-yRn!6#qTPZHQ|sz^<57jcg9eF|s2$dIqdz2~;gWU4a+*ZLkpcWBRLe8y(NW}_ z-ZW0H_-nV)f1<()QvxJ!6UzJRED!Cm4NSKU;D+spFBmJ^nN}0LNb+=9IZyAh;4B(; zJYR-dx6OK^lj4P!=@IW~9IUXUv+=`hN7hNHEzMgSm_~(|y=WwJGg$%_cwzyWeUrPy z`$f^YMdeq1p@ym{%0yL(^TgVZIkJn#l!#@n!!sf+Th2QD9xh6Arnu6OP}PuHg~1XC z1!!c>)m~Cv*=Xx*B}THK`ttme{AI9atVjSf(k*@V?ScM^uqR*W6HQT#)f&e+ZT(Kk zgcjY@Wn`%nA%VVWHnqeMkNp1r?vhs(DT87Nf({AUcl-=cMT=qe@B1MM+0xL>b= z7`)Os{B}QVR#)7}4)-lv-vxm}h6k9l)!Dq^0yvKZvXav}Dt1?P5O#FL_EOXH;xAl4 zerFGlZ0`jYWYqt#ntF`;OQ}KaTv)1C2C+0NTb8WkkFkbI0(Nl4OZGIyUT+hlNKNx+ z?5ot1aD=nBgkkemp~H=xj;PqwLMt=3tpd$zC736EN?c)s&t7+1Sl<#OOJ-(2Icybs z7PAV3N2SJTg|@oQh9#X8Vko@Dm4jOV$QS`lUzHDGaZ`yZepW*MKm`wiS~?vTh|zaK z6k^OIu)w{MvKkfM)DK4sxw92wXho*dbE&zwdg(o*sbWoe%z6hMoA@U~he~A#!$GI} z#sb~tAJ;hQ|8Hb+j%Yo}2mq60y`A`y-^t_SFS9%_Rrhel@jtXNS}#os+gV44PHWTG z(P^2NG3Up~OxI@at)g~qYicbPXe91}qzSn|`Sv%H{zRo6N0y7(E+|7Ld{$*Hq_9lo zdt8lXzd`o5lkG(JL>Y}&J$xf_(+k7ukXdmHHTyEL52L9?{tqxeQPD5}RFujmp!Wq3 z3k!F=7`=-+9NjPYx~GH9IPq-y8Wa~(k;IT5wk2Z^G670Plo{}~-ott(!F{yQy zb@a~r36CfJjXl-pEv>XY*>6j{aZY{muh?~$QA%d5pDQ%%|Y80BdX`9VySG0A>ZK8GD2Q(|jp{~=c2GqR`$U;?VD z-MmSz4*B>b`WcEpr;Dzt*+`kQEYcOoy7y0^L`xjpSND$;J}7Jvf|>#K1W; z*}r}GBlxi@HJ~NEX`yuPy-OGak01}oOacFaV-#qe3^RFxkiqw%T?2g#yN;f0%&!P> zrTx&XKmUQZj2QMV=K`_SHC!_5!7GKopUBDNi9U#3L;ef%{8nOodW-jLef=sAQYJJ| z1ZBr zS~>y9!(%5Moh4L9HWV+bju*EX56YdADY_omkqe94k)#2IGQoc>5w<(NW6SU9C}dZ) z{^>fS+Mo;xl8pcT-JLb6t=xj$o+UG?DTWoTpB{bskK265p9oj*4nwtlmMeh-+n zt>~r(22vvi6;p0SV#a;RY^yXAs4x?JFou~Pz#Zht7nT^WF|m1WtZUdEB5+PU4TwQw z@vIa0n0A7b(OQLKc$U1Qsmqs=Cqd6}rK9`O4&WMiN3{9P zlftofZmdIPU^usi>4;xCn~;<`JO{g=;*u^7S0oSOPybQ)7lpLbRy##2*ZR5utSNxS zx3dc_zFKO;Qh*3NyN+7h zNln2TIB?t_APHkC+k<;#-19Z4sXh&VN2be~_7&@)8JiCc5gQrMMr>76T;{PH{1{go zHFj#UW~2-8aY2o5r9NNv=ncH&Ncw(l`HV7jOuUN0wBV`+3;w)*>PRB!RD*O zF?|qSWEMnq+sl4RoBjlMKhE&y1uEK7^HGlJ$Ar{(I8os5A3j@H9YI&Coo&i~+Ua=` zlD@{C9Vu$7f68?6fTyvdJaN1X8ISh~YZ8Qbx{GFIB(~)GG@Myq{^A%eJ6x#&9r*tZ zPJn&nGvpH1^(8_pZZFVY;5R^oeiFn|Oe)_()%^d7jW(uecqnEjkx=M)wo`)5HY@J$ zsciV_uuL-t%WsosVAdMR*CU%SLxq!uqwxT{ho5GL7g3Yi(y{A(iR~KDlj7A%top(; z$m*n-5@wp17$2QlU^t#VS-qX`<@0H%o66)dcL2~=EVXiM&Pvg_fZ=uxfAd3u*aM4c zz+!=$r@rLI&H?l9z@E+uqkbf*fESc7uMwu1DM7sfjhtOnPg|7*Cg&e-ijmvTBjD*@ z*_oq*=Xtner}oaV!R$L;U85Eq6xPqw5ARX~ruf#x8fQV<*`XmTzd20^X+(bGTMMTI zfYW5QX&@mZ&3If=L>o%_U71<-3Y?wr2S*kQ3>R~DPUK!zNOBS~l?j1B0&ZS^i;tIN zHy_S9dL^1)dnQRFCW4Ga5~4@F)~ghM%ZJ+EeViG)gxBs~6dsqHhNuZppy}T~B>G<9 zpwc=5s!tH~dCB8nprc72pwLuucy{6DtWFIlUS-&dybqD$cm?Cd-d)4sC#hxX5INLL z{`Nh9+o))1Jg@>RDBJCmohfT;ue3R(4QD#szrn}9{Fe|HXQY0X4@UW!EF7VskW~HY z&iUvYHerQsj?Woi*0OlwTP0}HRFwy3f{W|rkFvxgUc+4!%+SQzIj@#Lm=3A7gjgd~ z(WA&D9+ey#_Y`F|xHH~6>FoXC!@As29tio#U0RQm1vwW>U-@V?`WmLbCHoA_NUm1H zUvO}3dsO-UR&|S6OVIlI_d3BGpnwOr$>$69_VRM*2 z_aam5Xlhr5QPl|`zejlHFL6<~iVz}kcwCSlO2fzcM#8K}IJA}@( zd&*vOp5CZQ5S5pm#f_6nND0bc!PsD=r?)!(a=h&hPR2gQ%5-YyjWJw3y5A&$aXk}t z!uE-l^>Jm0S-t?V%=NVOK7M-KbY`SVD&x6Q7z$OV?ps2%?Hejx*c;2olna$Qp&AR-% zYXv|LlFyHClFQ;zf!H4m@Ptnzgo;yw2%sEC94@thD%RqUBb5a?!Q)}s-+r*lOqFQ| zcp6DBx3KK^@V^Xu@fpv%cSD4~&dISqzrl23iRY~@+>;=L0#&G!+frnAA4t{`@dUv1 z0aaLUf1@!Es2U8w6(|t%9J}F`=h}!iVHJLjjDWRG`IX=Z@r7L5|rxVe_F#FLa{>g`Lm-*X0$i4 z@z$Sni0s*lt7H@+@G5b2oYs-gMArrrMw`BX^9FzWe`@l&DVuBfJh~!fGviePFeP18 z89#txir(}sf&@&G;6?$O?gB~}o8GBbr2*!}B&A9@jRHwLfAD4)%~p%^1MARLX2ST1 zU%6=Q1F=`xLMuXlP8t5PQ7Vebd&$Y{?rZ8ckFU`OgFO{dYlFnNrv46T0JmS0TxXEh zV-o~*G@JQj#}8l4=awAUhcx*85v-=J{Q6;s5BtrVt$ZO#{vZ4fF;Z#B+$6Zlipv@o zv)PsJ#`A}Ulx?`!6ok8%89d=Xi%BYZfLe&FXjV|_SgT}w8b4|uVd$>#rQ zE8(O!Xy$P&5K;$=iZpSI#Lg*W<|x(B(aAezu|2uu$4N?Crt&g7;^$Vj_ZwZF3IP{t zhw|I0chn)!kWEsgA&#@{>cl7UCsY?t=qC=S6ZBPFeg%K*xCiY07q|cmixua9j4>nT z+Q|v0O@yK3iOH6I*1Q020nJfz_~df29#_Q$Xdf|Zf*o39MDUWnudQmn14$NSfynio z+nt{9r2dFL^&yQdGpj8p>{_ zT5_vJ@V(Z)F6aY$L0F-za3Ec+fq!u?z$GS`_k7KQ7$b~)qEj{H@|i%>GfqyJ8G%Zq-1b7Wqp4Clivwgv@4t>`!#^m*+-MqG({Z=@)iKIcm7E! z#dX!EbBR-&{lBW{Cye`6vQ{Mq$^rikaD=EJCeGSpw`mfEx3|qAp8Ik~$Z*>`X9@Az zyi69Sp?s%Oqu!tY1p8;kv}ca$z~hwwXOYfN5S130kWMHf(D&>VJ@Z;LQh!Q2;`!ti zT}Bhqd)`s?4;>{MA0UN&UA0D*N(n&tYzY?uYluxdJICkgd3VIGXnGEKKAYi`kfRrx z+{(8^%6}A`l<;h|(R?=W__BTYSFcZ(+SXEhauSt36}VG zu=bs_dr6UJnj`?Xo;~O8d>cEo-2e9X9~{Y8q@Grk-~gUKFP?sh<9~ol3^b^J4+?$N z7CCl!|09(DczE6q3->+fYcfI3jgUSf>sdg_WWtRZSYO@o*tzq%uo~UkSn zqBKS77YjwZ19{x|b4^y5Ccuec0W&1U{{wI=Zkd$9p#i1nZ!a!tYPuy%Lct|EI>>Aj zBEZ9E4^Z{5C4Ep642{tDi@}>qMk)*eV!kA%uJDC| zS!M;3HLtRWA~Nkec%IJfXH>J&+uAp z&D6@qa~g@th&UR|{s3x^mq>GU@jD?W(Gv`rH}`8B`}~|Hyv4BkgQHJ?tFqP9oCnbb zoD!W&5N8(4FGd5(hnf^lMp*Ks=}Q~d>KTm$Lm>@~yQE+zMYZBdqkEJ$9-uZsKwOQj zOhW|6SBdw0lg)~)tcZpD8%2J@jU)@FM3owTnDTj_8tg_b>9N;9^Ed$3*Tt=`(bH#X zIVK)ta=D}ul9SIA5!_P%gb?@lu%YmwA1|wSj}8RQb#!6-C{r4JwOefy>-8x1dCVne z09yz*Uj?)W2WX4#b9e1|pz&O93=; zc5a4=7#xZjB>5Go#7&Who~5xuVUCpm zV=SgC^{%uy14XiTFVWK{LTt!04j~V|sw{*vfJ@Cd3xd`H9LY8YtvWP&?4dXrf(je* zrw;v_W4GQcMQAFL5;M&RmB%-C^VX(gZ+-cKMmf5qIQ4_+L8q)HpaRm-(d`j{;|q!> zcUgUd{Jn6}s|64-QD=0`EHCQtX`R{qdnWngx~-p#Ge{p4pKFeh><5tiv8 zHV_C*M{qYaXwlo?#E)Cd3`y~dQkB1=>5NNKlEHX$WnT!gmiBZ|);Lin)`JbcWaTnz z^$fO`Z4*mJumd+9Mgq+RLE#?YYh}R*&tTK5HkCcFVih*GwR|k_G55W)#pLP+4oj*T zzu)F+4I~-VM)=?5hcAFa6jg0z7S9Bt;;=tTs$-QI2m|8fNKXOjBrwhHf{9_^xtLtzA($`G+-%}Fi%g^6R$MBMtEtnN> zX-28ldE8uUX=ohGR)_xD4ycw3EA1$_y8V%yS4{fR#DW5yi|mSNU{v+YOVf` zHuqCzku$D1Zrh*0@jm+)GeS{(QC|m3NSX+O)SNh3@&R8I6SPIsIflX>ONmdoqxdI&~x`2_f_$EN4b*`z}b03)c*q36H>9zI_00uchX z{;E6%9v4e=Ko&I}W8;a5XA1y4$cv}4AaY@P3ey3N0I|#il%gWvf}B^bFdyX}Qh2Ra z0aKlGtko;Kxt{#b8N;bOI7|n-hqb<23M%4a;|!BFdL!BuFHV~R13Mod?ku!* zRaCmQhhCrreE1HYz8|h0=cw;sg>}PAwTi_A(QWDlLG3oSE7$FldZ^!{B~k&nV=wD- z6e(XGv-tHk3o7NX$k|I3;ar#a9C{shhO3!zL-8aZa;;R5PoYMXap zacKS6lc@^wuRWO3MEcSrZYdpLycBVg z`=C0xg`Ll|81V^=rfOVT6Dce6I7rSeoMriG*(Cv{4S}BJ!M9YiTM?b7Fgk$d2`hEWNT$sehXm^g3jp#m>OkAL`MFA{;-jgfS(um z3o&32A2`Y@3b>(e*Fg7`R(SM6D^GhqNFm4&36JG-ZSTcf$W_gFhQ%kWI@8YF)oC=QusX){3Ky)XSRgH6k_P+u7 z(S@t6ZRS#TwbZ0H9A0&fQpFlY!DFm&QVXizA?Q-2xTkpmLmPcTS~@ba8b5L zX`tY7Bfxy#xq^b_x-f%l*$(_V$%I=d(~z%d#U1Y{5&{m{8#WSsl%QtMFVKmu8(^FW znA{qjn``vByX@-vsHLUFK8|l@wpCM8LoE?C@%RqJl%H576fNAgW8SX~u<0}KSQ>%t zx!|tkKS?-(Lg&Nkm0@!RZmT!I)E+mrpxNUaSZu|H7z3;+3=m@9(0rl%xq#!PQV^8) zJu;3W9tu2%Ng1=bEkLHjDNW6t)PoX#*vNoC2~jGy)UERo`T*k`+{oN84^!oY=EEBc z=a;DWfFnz#{2w$jrRj9d2k@E4our}C*@iuA%r*Z3BHWyNeBUQ(i{@K74{M>GW(PDU zm8a(i{~_9gx89BgOCt2}pM693+E&p83~#x4Jv&woioJ1i#S(JrFdes(9OooIkc3f3 z;Q(8L5pA=mk5bxfE|z6dtOQHa@!Qy07$u60A2I_)I9qKGmn7N+O-)U?x$yRMpv;O2 zEPV-E+r!~B0F%ZEJtcnsKEd`pGJuMmi4 zHFN@GCL}MZFTY(DfC3%<;~)jfZGQ$V*Tbpkxw?UzYZ^{{>U_Xp;WWFzu_k*_>!1GQBjH8bC+ zsHpf$7#NklLk|AEx|D@jQ0D*POXR2^_KKro&sGwLoBbGteIe~h+lK86-><;@Cl=5C z4nQ7dzfj~<8*0nJ!R`#c52!%6nU2Vr3&kXVkLwc}61}2b7$&w}Wq*M`Dy^u9eb9ql z@y@}~Fp7-J1u(KrfH?>-hoo?HR^-m7)%&#$<5!s7j{qlMq#m3PyXUiT{f+oMQ#(tZ zWvgvfZ{{~Y%1?v_TR;H<(0g2^>mvX_M!oz=+=)9kj7$^?0}f2K$Y9aj{*e_ViC1a# zH`}oK+SSeEl9KAK15?GU>fX@iSG%s{L~R0&O#5HMI$(60sq(UH!+8C#Vm{ekJ@ z14XHLwp1(4kEbrL-w7~R)6vk#>ygJqJSScCe?b zgaxTBHUPH-J(G-Nnk=KO5n1hra;(gxSEA49D4d#>rlO}!2~@`dWlDeO>$hcX528|w zX@EnVCbA+5+leynOXYBL{`QNPo&Y_2evi`vN9V$@Q{%Z26iLq2-94!i-I(_p<<`bb zm}khiU#gqM>_ii+Ptd5qk`&`P{}R~)n2p5bMf`~LuB5se$C1ci!7`GAhTA`XOIB*d zceU&}0W--nxiUXznq~UYv~T7_JcVTCo_RPNkhX6EKDlF5^cMsJZR)J?LFAsNa@SvD zz9zNw7Tr{H(m%z?Num(2q5UG>&|`n(;s=_A@isp{K#)UKvlCuZj6`)_1d7uBKB;-# z$DP^lJw^70iuAbebe({?2hT~48qQ}hbO3;%Jsn1K3<+833_x*nJ3EHm2PQHT$u55B zmjeR`c2B~4rzn#8#f5vln|rRpfd(UhtLoX|bg1bhH*$sf`t(oVr@@0`=5v_6w}k4Q z*GqsUeYi9Sa~oO=D`7N1xXr*xb(ds!X@51=x8`g9Ygxj* z4Li^Z!Drf4AG5gVMn+x4_`}D;qY>R00t`oo)s7{2!l3$5GO%QrJF!&?C88kUDB7hE zQ>e={OkCHM`! zEQ~GFX~HKb$E>$8ne?-EZ0tLwvJlU$@jQFHu6(EZa3_iw5*$p*>nwRuYGX0;-MK65 zU7iqHOfniIv3<#M@?AxEy`8|$Tgx%b@ z+Q(D!XzbhUr|pkM;U;7t-!rw3h%y>_JjXv+s*phSI_|A#`(WjzkEDrvo2ZXQiAo(5g0`o$xE*6B0{evH5<}xyWq}@q&G06{@K4o_) zjD^YGfGqo>R@>K9T=Q8Q*-l@*aCWv9W}wqBwQ=H_&zJup7;BH zJ9J9ZB^pLA|M^|OOtoa{$qf(5ZBoj3hH2;hyq0;?grs<9UQlY1dXCva&Kr7q)Wljz z4Z!F*`_=~T#fySIWnfx{sC(iid+RDxkDV1o+>(Y`U+L#d9@f}eCFv^uL4eGO8@=B<7DCIEDH-;%v^GZ!}|Ya5VVd7#$e*j|e`! z#=#LSb!~UqYNYR_pKtcQFD)s#+@JkBp`O$L*p9k@pzC%>MjCHhOP?3F{0<8%NdrnU z8~V}Ccs{$?<;!fS}gQryyJp% z-<?h?cR=$~ElHZK@_D5&HAGUW-%H(N?)ich2=$xZ5ut z%-7S=($3Az{miS${XcsLX>%9BvM&Z|w&CYlr_DP=EDSu@wB zja`@VcC*>yhna!ka~S+)0ypTJ-vJ&NAX6&}3hB@B(9JE9tj2a-GJ8aU3z4-z+SOrm zTL*jkmALbP%VYL+&6#oHRwr;D320vu@aTcXCk{meXY1B0fn2%hA@Gm}&t Date: Mon, 13 Apr 2020 12:01:46 -0700 Subject: [PATCH 0144/1131] updated changelog for 6.2.0 --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35ee1ef4..b4c5350b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +## 2020-03-16 CORE 6.2.0 +* gRPC API + * Added call to execute python script +* Enhancements + * \#371 - improved coretk gui scaling + * \#374 - display range visually for wlan in coretk gui, when configuring + * \#377 - improved coretk error dialogs + * \#379 - fixed issues with core converting between x,y and lon,lat for values that would cross utm zones + * \#384 - sdt integration moved internally to core code allowing it to work for coretk gui as well + * \#387 - coretk gui will now auto detect potential valid terminal and command to use for interacting with nodes during runtime + * \#389 - coretk gui will now attempt to reconnect to daemon without need to restart + * \#395 - coretk gui now has "save" and "save as" menu options + * \#402 - coretk will now allow terminal preference to be directly edited +* Bugfixes + * \#375 - fixed issues with emane event monitor handling data + * \#381 - executing a python script will now wait until completion before looking to join a new session + * \#391 - fixed configuring node ip addresses in coretk gui + * \#392 - fixed coretk link display when addresses are cleared out + * \#393 - coretk gui will properly clear marker annotations when switching sessions + * \#396 - Docker and LXC nodes will now properly save to XML + * \#406- WLAN bridge initialization was not ran when all nodes are disconnected + ## 2020-02-20 CORE 6.1.0 * New * config services - these services leverage a proper template engine and have configurable parameters, given enough time may replace existing services From 3c909d4989a07593be9f33d2b7af2c6ae1feda61 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 13 Apr 2020 12:02:55 -0700 Subject: [PATCH 0145/1131] updated version for next release --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index b9369de6..24f322f0 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, 6.2.0) +AC_INIT(core, 6.3.0) # autoconf and automake initialization AC_CONFIG_SRCDIR([netns/version.h.in]) From 04bd3a2f8fee92c44a21a3a587ea85467ca0322c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 13 Apr 2020 14:09:02 -0700 Subject: [PATCH 0146/1131] updated changelog for 6.3.0 release --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4c5350b..1535887b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## 2020-04-13 CORE 6.3.0 +* Features + * \#424 - added FRR IS-IS service +* Enhancements + * \#414 - update GUI OSPFv2 adjacency widget to work with FRR + * \#416 - EMANE links can now be drawn for 80211 and RF Pipe models + * \#418 #409 - code cleanup + * \#425 - added route monitor script for SDT3D integration + * a formal error will now be thrown when EMANE binding are not installed, but attempted to be used + * node positions will now default to 0,0 to avoid GUI errors, when one is not provided + * improved SDT3D integration, multiple link support and usage of custom layers +* Python GUI Enhancements + * enabled edit menu delete + * cleaned up node context menu and enabled delete +* Bugfixes + * \#427 - fixed issue in default route service + * \#426 - fixed issue reading ipsec template file + * \#420 - fixed issue with TLV API udp handler + * \#411 - allow wlan to be configured with 0 values + * \#415 - general EMANE configuration was not being saved/loaded from XML + ## 2020-03-16 CORE 6.2.0 * gRPC API * Added call to execute python script From 971a959a19f463317f656ab056b74dd8e668dcd2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 13 Apr 2020 16:59:55 -0700 Subject: [PATCH 0147/1131] updates to route monitor --- daemon/scripts/core-route-monitor | 90 +++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 28 deletions(-) diff --git a/daemon/scripts/core-route-monitor b/daemon/scripts/core-route-monitor index afa7b055..2d128c33 100755 --- a/daemon/scripts/core-route-monitor +++ b/daemon/scripts/core-route-monitor @@ -4,6 +4,7 @@ import enum import select import socket import subprocess +import sys import time from argparse import ArgumentDefaultsHelpFormatter from functools import cmp_to_key @@ -11,6 +12,8 @@ from queue import Queue from threading import Thread from typing import Dict, Tuple +import grpc + from core import utils from core.api.grpc.client import CoreGrpcClient from core.api.grpc.core_pb2 import NodeType @@ -56,14 +59,17 @@ class SdtClient: class RouterMonitor: - def __init__(self, src_id: str, src: str, dst: str, pkt: str, + def __init__(self, session: int, src: str, dst: str, pkt: str, rate: int, dead: int, sdt_host: str, sdt_port: int) -> None: self.queue = Queue() self.core = CoreGrpcClient() - self.src_id = src_id + self.session = session + self.src_id = None self.src = src self.dst = dst self.pkt = pkt + self.rate = rate + self.dead = dead self.seen = {} self.running = False self.route_time = None @@ -71,23 +77,44 @@ class RouterMonitor: self.sdt = SdtClient((sdt_host, sdt_port)) self.nodes = self.get_nodes() - def get_nodes(self) -> Dict[str, str]: - nodes = {} + def get_nodes(self) -> Dict[int, str]: with self.core.context_connect(): - response = self.core.get_sessions() - sessions = response.sessions - session = None - if sessions: - session = sessions[0] - if not session: - raise Exception("no current core sessions") - print("session: ", session.dir) - response = self.core.get_session(session.id) - for node in response.session.nodes: - if node.type != NodeType.DEFAULT: - continue - nodes[node.id] = node.channel - return nodes + if self.session is None: + self.session = self.get_session() + print("session: ", self.session) + try: + response = self.core.get_session(self.session) + nodes = response.session.nodes + node_map = {} + for node in nodes: + if node.type != NodeType.DEFAULT: + continue + node_map[node.id] = node.channel + if self.src_id is None: + response = self.core.get_node(self.session, node.id) + for netif in response.interfaces: + if self.src == netif.ip4: + self.src_id = node.id + break + except grpc.RpcError: + print(f"invalid session: {self.session}") + sys.exit(1) + if self.src_id is None: + print(f"could not find node with source address: {self.src}") + sys.exit(1) + print(f"monitoring src_id ({self.src_id}) src({self.src}) dst({self.dst}) pkt({self.pkt})") + return node_map + + def get_session(self) -> int: + response = self.core.get_sessions() + sessions = response.sessions + session = None + if sessions: + session = sessions[0] + if not session: + print("no current core sessions") + sys.exit(1) + return session.id def start(self) -> None: self.running = True @@ -107,7 +134,7 @@ class RouterMonitor: elif node in self.seen: del self.seen[node] - if (time.monotonic() - self.route_time) >= ROUTE_TIME: + if (time.monotonic() - self.route_time) >= self.rate: self.manage_routes() self.route_time = time.monotonic() @@ -148,7 +175,7 @@ class RouterMonitor: def listen(self, node_id, node) -> None: cmd = ( - f"tcpdump -lnv src host {self.src} and dst host {self.dst} and {self.pkt}" + f"tcpdump -lnvi any src host {self.src} and dst host {self.dst} and {self.pkt}" ) node_cmd = f"vcmd -c {node} -- {cmd}" p = subprocess.Popen(node_cmd, shell=True, stdout=subprocess.PIPE, @@ -166,7 +193,7 @@ class RouterMonitor: self.queue.put((RouteEnum.ADD, node_id, ttl)) current = time.monotonic() else: - if (time.monotonic() - current) >= DEAD_TIME: + if (time.monotonic() - current) >= self.dead: self.queue.put((RouteEnum.DEL, node_id, None)) except Exception as e: print(f"listener error: {e}") @@ -177,27 +204,34 @@ def main() -> None: print("core-route-monitor requires tcpdump to be installed") return + desc = "core route monitor leverages tcpdump to monitor traffic and find route using TTL" parser = argparse.ArgumentParser( - description="core route monitor", - formatter_class=ArgumentDefaultsHelpFormatter, + description=desc, + formatter_class=ArgumentDefaultsHelpFormatter ) - parser.add_argument("--id", required=True, - help="source node id for determining path") - parser.add_argument("--src", default="10.0.0.20", + parser.add_argument("--src", required=True, help="source address for route monitoring") - parser.add_argument("--dst", default="10.0.2.20", + parser.add_argument("--dst", required=True, help="destination address for route monitoring") + parser.add_argument("--session", type=int, + help="session to monitor route") parser.add_argument("--pkt", default="icmp", choices=PACKET_CHOICES, help="packet type") + parser.add_argument("--rate", type=int, default=ROUTE_TIME, + help="rate to update route, in seconds") + parser.add_argument("--dead", type=int, default=DEAD_TIME, + help="timeout to declare path dead, in seconds") parser.add_argument("--sdt-host", default=SDT_HOST, help="sdt host address") parser.add_argument("--sdt-port", type=int, default=SDT_PORT, help="sdt port") args = parser.parse_args() monitor = RouterMonitor( - args.id, + args.session, args.src, args.dst, args.pkt, + args.rate, + args.dead, args.sdt_host, args.sdt_port, ) From 8c8024df10e1b9a9b1400a25109071556972a588 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 13 Apr 2020 17:02:05 -0700 Subject: [PATCH 0148/1131] updates to formatting for route monitor --- daemon/scripts/core-route-monitor | 66 +++++++++++++++++++------------ 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/daemon/scripts/core-route-monitor b/daemon/scripts/core-route-monitor index 2d128c33..a9b48aff 100755 --- a/daemon/scripts/core-route-monitor +++ b/daemon/scripts/core-route-monitor @@ -59,8 +59,17 @@ class SdtClient: class RouterMonitor: - def __init__(self, session: int, src: str, dst: str, pkt: str, rate: int, dead: int, - sdt_host: str, sdt_port: int) -> None: + def __init__( + self, + session: int, + src: str, + dst: str, + pkt: str, + rate: int, + dead: int, + sdt_host: str, + sdt_port: int, + ) -> None: self.queue = Queue() self.core = CoreGrpcClient() self.session = session @@ -102,7 +111,9 @@ class RouterMonitor: if self.src_id is None: print(f"could not find node with source address: {self.src}") sys.exit(1) - print(f"monitoring src_id ({self.src_id}) src({self.src}) dst({self.dst}) pkt({self.pkt})") + print( + f"monitoring src_id ({self.src_id}) src({self.src}) dst({self.dst}) pkt({self.pkt})" + ) return node_map def get_session(self) -> int: @@ -152,9 +163,9 @@ class RouterMonitor: self.sdt.delete_links() if not self.seen: return - values = sorted(self.seen.items(), - key=cmp_to_key(self.route_sort), - reverse=True) + values = sorted( + self.seen.items(), key=cmp_to_key(self.route_sort), reverse=True + ) print("current route:") for index, node_data in enumerate(values): next_index = index + 1 @@ -174,12 +185,11 @@ class RouterMonitor: self.listeners.clear() def listen(self, node_id, node) -> None: - cmd = ( - f"tcpdump -lnvi any src host {self.src} and dst host {self.dst} and {self.pkt}" - ) + cmd = f"tcpdump -lnvi any src host {self.src} and dst host {self.dst} and {self.pkt}" node_cmd = f"vcmd -c {node} -- {cmd}" - p = subprocess.Popen(node_cmd, shell=True, stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL) + p = subprocess.Popen( + node_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL + ) current = time.monotonic() try: while not p.poll() and self.running: @@ -206,21 +216,27 @@ def main() -> None: desc = "core route monitor leverages tcpdump to monitor traffic and find route using TTL" parser = argparse.ArgumentParser( - description=desc, - formatter_class=ArgumentDefaultsHelpFormatter + description=desc, formatter_class=ArgumentDefaultsHelpFormatter + ) + parser.add_argument( + "--src", required=True, help="source address for route monitoring" + ) + parser.add_argument( + "--dst", required=True, help="destination address for route monitoring" + ) + parser.add_argument("--session", type=int, help="session to monitor route") + parser.add_argument( + "--pkt", default="icmp", choices=PACKET_CHOICES, help="packet type" + ) + parser.add_argument( + "--rate", type=int, default=ROUTE_TIME, help="rate to update route, in seconds" + ) + parser.add_argument( + "--dead", + type=int, + default=DEAD_TIME, + help="timeout to declare path dead, in seconds", ) - parser.add_argument("--src", required=True, - help="source address for route monitoring") - parser.add_argument("--dst", required=True, - help="destination address for route monitoring") - parser.add_argument("--session", type=int, - help="session to monitor route") - parser.add_argument("--pkt", default="icmp", choices=PACKET_CHOICES, - help="packet type") - parser.add_argument("--rate", type=int, default=ROUTE_TIME, - help="rate to update route, in seconds") - parser.add_argument("--dead", type=int, default=DEAD_TIME, - help="timeout to declare path dead, in seconds") parser.add_argument("--sdt-host", default=SDT_HOST, help="sdt host address") parser.add_argument("--sdt-port", type=int, default=SDT_PORT, help="sdt port") args = parser.parse_args() From cd8157eff72abe8e9dd9c1c10707b5883da5e0e8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Apr 2020 10:47:42 -0700 Subject: [PATCH 0149/1131] renamed python gui to be more similar to other core scripts and specific to it being python, some cleanup to pygui edge drawing and updates to allow for edges to have an arc to support multiple links between the same nodes --- daemon/Pipfile | 2 +- daemon/core/gui/graph/edges.py | 196 ++++++++++++++-------- daemon/core/gui/graph/graph.py | 54 +++--- daemon/core/gui/graph/node.py | 14 +- daemon/core/gui/nodeutils.py | 8 +- daemon/scripts/{coretk-gui => core-pygui} | 0 6 files changed, 154 insertions(+), 120 deletions(-) rename daemon/scripts/{coretk-gui => core-pygui} (100%) diff --git a/daemon/Pipfile b/daemon/Pipfile index d55b248f..8bf52787 100644 --- a/daemon/Pipfile +++ b/daemon/Pipfile @@ -5,7 +5,7 @@ verify_ssl = true [scripts] core = "python scripts/core-daemon -f data/core.conf -l data/logging.conf" -coretk = "python scripts/coretk-gui" +core-pygui = "python scripts/core-pygui" test = "pytest -v tests" test-mock = "pytest -v --mock tests" test-emane = "pytest -v tests/emane" diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 37b1a96e..1bd1bffd 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -1,11 +1,13 @@ import logging +import math import tkinter as tk from typing import TYPE_CHECKING, Any, Tuple +from core.api.grpc import core_pb2 from core.gui import themes from core.gui.dialogs.linkconfig import LinkConfigurationDialog from core.gui.graph import tags -from core.gui.nodeutils import EdgeUtils, NodeUtils +from core.gui.nodeutils import NodeUtils if TYPE_CHECKING: from core.gui.graph.graph import CanvasGraph @@ -17,80 +19,142 @@ WIRELESS_WIDTH = 1.5 WIRELESS_COLOR = "#009933" -class CanvasWirelessEdge: - def __init__( - self, - token: Tuple[Any, ...], - position: Tuple[float, float, float, float], - src: int, - dst: int, - canvas: "CanvasGraph", - ): - logging.debug("Draw wireless link from node %s to node %s", src, dst) - self.token = token +def interface_label(interface: core_pb2.Interface) -> str: + label = "" + if interface.ip4: + label = f"{interface.ip4}/{interface.ip4mask}" + if interface.ip6: + label = f"{label}\n{interface.ip6}/{interface.ip6mask}" + return label + + +def create_edge_token(src: int, dst: int) -> Tuple[int, ...]: + return tuple(sorted([src, dst])) + + +class Edge: + tag = tags.EDGE + + def __init__(self, canvas: "CanvasGraph", src: int, dst: int = None) -> None: + self.canvas = canvas + self.id = None self.src = src self.dst = dst - self.canvas = canvas + self.arc = 0 + self.token = None + self.color = EDGE_COLOR + self.width = EDGE_WIDTH + + @classmethod + def create_token(cls, src: int, dst: int) -> Tuple[int, ...]: + return tuple(sorted([src, dst])) + + def _get_midpoint( + self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float] + ) -> Tuple[float, float]: + src_x, src_y = src_pos + dst_x, dst_y = dst_pos + t = math.atan2(dst_y - src_y, dst_x - src_y) + x_mp = (src_x + dst_x) / 2 + self.arc * math.sin(t) + y_mp = (src_y + dst_y) / 2 - self.arc * math.cos(t) + return x_mp, y_mp + + def draw(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None: + mid_pos = self._get_midpoint(src_pos, dst_pos) self.id = self.canvas.create_line( - *position, - tags=tags.WIRELESS_EDGE, - width=WIRELESS_WIDTH * self.canvas.app.app_scale, - fill=WIRELESS_COLOR, + *src_pos, + *mid_pos, + *dst_pos, + smooth=True, + tags=self.tag, + width=self.width * self.canvas.app.app_scale, + fill=self.color, ) - def delete(self): + def move_node(self, node_id: int, x: float, y: float) -> None: + if self.src == node_id: + self.move_src(x, y) + else: + self.move_dst(x, y) + + def move_dst(self, x: float, y: float) -> None: + dst_pos = (x, y) + src_x, src_y, _, _, _, _ = self.canvas.coords(self.id) + src_pos = (src_x, src_y) + mid_pos = self._get_midpoint(src_pos, dst_pos) + self.canvas.coords(self.id, *src_pos, *mid_pos, *dst_pos) + + def move_src(self, x: float, y: float) -> None: + src_pos = (x, y) + _, _, _, _, dst_x, dst_y = self.canvas.coords(self.id) + dst_pos = (dst_x, dst_y) + mid_pos = self._get_midpoint(src_pos, dst_pos) + self.canvas.coords(self.id, *src_pos, *mid_pos, *dst_pos) + + def delete(self) -> None: self.canvas.delete(self.id) -class CanvasEdge: +class CanvasWirelessEdge(Edge): + tag = tags.WIRELESS_EDGE + + def __init__( + self, + canvas: "CanvasGraph", + src: int, + dst: int, + src_pos: Tuple[float, float], + dst_pos: Tuple[float, float], + token: Tuple[Any, ...], + ) -> None: + logging.debug("drawing wireless link from node %s to node %s", src, dst) + super().__init__(canvas, src, dst) + self.token = token + self.width = WIRELESS_WIDTH + self.color = WIRELESS_COLOR + self.draw(src_pos, dst_pos) + + +class CanvasEdge(Edge): """ Canvas edge class """ def __init__( self, - x1: float, - y1: float, - x2: float, - y2: float, - src: int, canvas: "CanvasGraph", - ): + src: int, + src_pos: Tuple[float, float], + dst_pos: Tuple[float, float], + ) -> None: """ Create an instance of canvas edge object """ - self.src = src - self.dst = None + super().__init__(canvas, src) self.src_interface = None self.dst_interface = None - self.canvas = canvas - self.id = self.canvas.create_line( - x1, - y1, - x2, - y2, - tags=tags.EDGE, - width=EDGE_WIDTH * self.canvas.app.app_scale, - fill=EDGE_COLOR, - ) self.text_src = None self.text_dst = None self.text_middle = None - self.token = None self.link = None self.asymmetric_link = None self.throughput = None + self.draw(src_pos, dst_pos) self.set_binding() - def set_binding(self): + def move_node(self, node_id: int, x: float, y: float) -> None: + super().move_node(node_id, x, y) + self.update_labels() + + def set_binding(self) -> None: self.canvas.tag_bind(self.id, "", self.create_context) - def set_link(self, link): + def set_link(self, link) -> None: self.link = link self.draw_labels() def get_coordinates(self) -> [float, float, float, float]: - x1, y1, x2, y2 = self.canvas.coords(self.id) + x1, y1, _, _, x2, y2 = self.canvas.coords(self.id) v1 = x2 - x1 v2 = y2 - y1 ux = TEXT_DISTANCE * v1 @@ -107,24 +171,16 @@ class CanvasEdge: y = (y1 + y2) / 2 return x, y - def create_labels(self): + def create_labels(self) -> Tuple[str, str]: label_one = None if self.link.HasField("interface_one"): - label_one = self.create_label(self.link.interface_one) + label_one = interface_label(self.link.interface_one) label_two = None if self.link.HasField("interface_two"): - label_two = self.create_label(self.link.interface_two) + label_two = interface_label(self.link.interface_two) return label_one, label_two - def create_label(self, interface): - label = "" - if interface.ip4: - label = f"{interface.ip4}/{interface.ip4mask}" - if interface.ip6: - label = f"{label}\n{interface.ip6}/{interface.ip6mask}" - return label - - def draw_labels(self): + def draw_labels(self) -> None: x1, y1, x2, y2 = self.get_coordinates() label_one, label_two = self.create_labels() self.text_src = self.canvas.create_text( @@ -144,12 +200,12 @@ class CanvasEdge: tags=tags.LINK_INFO, ) - def redraw(self): + def redraw(self) -> None: label_one, label_two = self.create_labels() self.canvas.itemconfig(self.text_src, text=label_one) self.canvas.itemconfig(self.text_dst, text=label_two) - def update_labels(self): + def update_labels(self) -> None: """ Move edge labels based on current position. """ @@ -160,7 +216,7 @@ class CanvasEdge: x, y = self.get_midpoint() self.canvas.coords(self.text_middle, x, y) - def set_throughput(self, throughput: float): + def set_throughput(self, throughput: float) -> None: throughput = 0.001 * throughput value = f"{throughput:.3f} kbps" if self.text_middle is None: @@ -179,18 +235,17 @@ class CanvasEdge: width = EDGE_WIDTH self.canvas.itemconfig(self.id, fill=color, width=width) - def complete(self, dst: int): + def complete(self, dst: int) -> None: self.dst = dst - self.token = EdgeUtils.get_token(self.src, self.dst) + self.token = create_edge_token(self.src, self.dst) x, y = self.canvas.coords(self.dst) - x1, y1, _, _ = self.canvas.coords(self.id) - self.canvas.coords(self.id, x1, y1, x, y) + self.move_dst(x, y) self.check_wireless() self.canvas.tag_raise(self.src) self.canvas.tag_raise(self.dst) logging.debug("Draw wired link from node %s to node %s", self.src, dst) - def is_wireless(self) -> [bool, bool]: + 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 @@ -210,12 +265,12 @@ class CanvasEdge: wlan_network[self.dst].add(self.src) return is_src_wireless or is_dst_wireless - def check_wireless(self): + def check_wireless(self) -> None: if self.is_wireless(): self.canvas.itemconfig(self.id, state=tk.HIDDEN) self._check_antenna() - def _check_antenna(self): + def _check_antenna(self) -> None: src_node = self.canvas.nodes[self.src] dst_node = self.canvas.nodes[self.dst] src_node_type = src_node.core_node.type @@ -230,20 +285,19 @@ class CanvasEdge: else: src_node.add_antenna() - def delete(self): + def delete(self) -> None: logging.debug("Delete canvas edge, id: %s", self.id) - self.canvas.delete(self.id) - if self.link: - self.canvas.delete(self.text_src) - self.canvas.delete(self.text_dst) + super().delete() + self.canvas.delete(self.text_src) + self.canvas.delete(self.text_dst) self.canvas.delete(self.text_middle) - def reset(self): + def reset(self) -> None: self.canvas.delete(self.text_middle) self.text_middle = None self.canvas.itemconfig(self.id, fill=EDGE_COLOR, width=EDGE_WIDTH) - def create_context(self, event: tk.Event): + def create_context(self, event: tk.Event) -> None: context = tk.Menu(self.canvas) themes.style_menu(context) context.add_command(label="Configure", command=self.configure) @@ -256,6 +310,6 @@ class CanvasEdge: context.entryconfigure(3, state="disabled") context.post(event.x_root, event.y_root) - def configure(self): + def configure(self) -> None: dialog = LinkConfigurationDialog(self.canvas, self.canvas.app, self) dialog.show() diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index b602ae10..4c0d1abf 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -7,13 +7,18 @@ from PIL import Image, ImageTk from core.api.grpc import core_pb2 from core.gui.dialogs.shapemod import ShapeDialog from core.gui.graph import tags -from core.gui.graph.edges import EDGE_WIDTH, CanvasEdge, CanvasWirelessEdge +from core.gui.graph.edges import ( + EDGE_WIDTH, + CanvasEdge, + CanvasWirelessEdge, + create_edge_token, +) from core.gui.graph.enums import GraphMode, ScaleOption from core.gui.graph.node import CanvasNode from core.gui.graph.shape import Shape from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker from core.gui.images import ImageEnum, Images, TypeToImage -from core.gui.nodeutils import EdgeUtils, NodeUtils +from core.gui.nodeutils import NodeUtils if TYPE_CHECKING: from core.gui.app import Application @@ -197,11 +202,10 @@ class CanvasGraph(tk.Canvas): """ add a wireless edge between 2 canvas nodes """ - token = EdgeUtils.get_token(src.id, dst.id) - x1, y1 = self.coords(src.id) - x2, y2 = self.coords(dst.id) - position = (x1, y1, x2, y2) - edge = CanvasWirelessEdge(token, position, src.id, dst.id, self) + token = create_edge_token(src.id, dst.id) + src_pos = self.coords(src.id) + dst_pos = self.coords(dst.id) + edge = CanvasWirelessEdge(self, src.id, dst.id, src_pos, dst_pos, token) self.wireless_edges[token] = edge src.wireless_edges.add(edge) dst.wireless_edges.add(edge) @@ -209,7 +213,7 @@ class CanvasGraph(tk.Canvas): self.tag_raise(dst.id) def delete_wireless_edge(self, src: CanvasNode, dst: CanvasNode): - token = EdgeUtils.get_token(src.id, dst.id) + token = create_edge_token(src.id, dst.id) edge = self.wireless_edges.pop(token) edge.delete() src.wireless_edges.remove(edge) @@ -246,20 +250,15 @@ class CanvasGraph(tk.Canvas): node_one = canvas_node_one.core_node canvas_node_two = self.core.canvas_nodes[link.node_two_id] node_two = canvas_node_two.core_node - token = EdgeUtils.get_token(canvas_node_one.id, canvas_node_two.id) + token = create_edge_token(canvas_node_one.id, canvas_node_two.id) if link.type == core_pb2.LinkType.WIRELESS: self.add_wireless_edge(canvas_node_one, canvas_node_two) else: if token not in self.edges: - edge = CanvasEdge( - node_one.position.x, - node_one.position.y, - node_two.position.x, - node_two.position.y, - canvas_node_one.id, - self, - ) + src_pos = (node_one.position.x, node_one.position.y) + dst_pos = (node_two.position.x, node_two.position.y) + edge = CanvasEdge(self, canvas_node_one.id, src_pos, dst_pos) edge.token = token edge.dst = canvas_node_two.id edge.set_link(link) @@ -391,7 +390,7 @@ class CanvasGraph(tk.Canvas): return # ignore repeated edges - token = EdgeUtils.get_token(edge.src, self.selected) + token = create_edge_token(edge.src, self.selected) if token in self.edges: edge.delete() return @@ -520,11 +519,10 @@ class CanvasGraph(tk.Canvas): logging.debug("click press offset(%s, %s)", x_check, y_check) is_node = selected in self.nodes if self.mode == GraphMode.EDGE and is_node: - x, y = self.coords(selected) - self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) + pos = self.coords(selected) + self.drawing_edge = CanvasEdge(self, selected, pos, pos) if self.mode == GraphMode.ANNOTATION: - if is_marker(self.annotation_type): r = self.app.toolbar.marker_tool.radius self.create_oval( @@ -603,8 +601,7 @@ class CanvasGraph(tk.Canvas): self.cursor = x, y if self.mode == GraphMode.EDGE and self.drawing_edge is not None: - x1, y1, _, _ = self.coords(self.drawing_edge.id) - self.coords(self.drawing_edge.id, x1, y1, x, y) + self.drawing_edge.move_dst(x, y) if self.mode == GraphMode.ANNOTATION: if is_draw_shape(self.annotation_type) and self.shape_drawing: shape = self.shapes[self.selected] @@ -841,11 +838,10 @@ class CanvasGraph(tk.Canvas): """ create an edge between source node and destination node """ - if (source.id, dest.id) not in self.edges: - pos0 = source.core_node.position - x0 = pos0.x - y0 = pos0.y - edge = CanvasEdge(x0, y0, x0, y0, source.id, self) + 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) @@ -905,7 +901,7 @@ class CanvasGraph(tk.Canvas): dest_node_copy = self.nodes[copy_map[edge.token[1]]] self.create_edge(source_node_copy, dest_node_copy) copy_edge = self.edges[ - EdgeUtils.get_token(source_node_copy.id, dest_node_copy.id) + create_edge_token(source_node_copy.id, dest_node_copy.id) ] copy_link = copy_edge.link options = edge.link.options diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 39463054..df0d64b9 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -148,19 +148,9 @@ class CanvasNode: # move edges for edge in self.edges: - x1, y1, x2, y2 = self.canvas.coords(edge.id) - if edge.src == self.id: - self.canvas.coords(edge.id, x, y, x2, y2) - else: - self.canvas.coords(edge.id, x1, y1, x, y) - edge.update_labels() - + edge.move_node(self.id, x, y) for edge in self.wireless_edges: - x1, y1, x2, y2 = self.canvas.coords(edge.id) - if edge.src == self.id: - self.canvas.coords(edge.id, x, y, x2, y2) - else: - self.canvas.coords(edge.id, x1, y1, x, y) + edge.move_node(self.id, x, y) # set actual coords for node and update core is running real_x, real_y = self.canvas.get_actual_coords(x, y) diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index 81aa2cba..7ccb7ca3 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -1,5 +1,5 @@ import logging -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Union from core.api.grpc.core_pb2 import NodeType from core.gui.images import ImageEnum, Images, TypeToImage @@ -172,9 +172,3 @@ class NodeUtils: cls.NETWORK_NODES.append(node_draw) cls.NODE_ICONS[(node_type, None)] = node_draw.image cls.ANTENNA_ICON = Images.get(ImageEnum.ANTENNA, ANTENNA_SIZE) - - -class EdgeUtils: - @classmethod - def get_token(cls, src: int, dst: int) -> Tuple[int, ...]: - return tuple(sorted([src, dst])) diff --git a/daemon/scripts/coretk-gui b/daemon/scripts/core-pygui similarity index 100% rename from daemon/scripts/coretk-gui rename to daemon/scripts/core-pygui From 3c4a908fd5001e047092680de7329efde64730a3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Apr 2020 15:51:28 -0700 Subject: [PATCH 0150/1131] updates to support multiple links between nodes in pygui, initially handling multiple wireless links --- daemon/core/api/grpc/events.py | 1 + daemon/core/api/grpc/grpcutils.py | 1 + daemon/core/gui/coreclient.py | 10 +++- daemon/core/gui/graph/edges.py | 85 +++++++++++++++++++++++---- daemon/core/gui/graph/graph.py | 19 ++++-- daemon/proto/core/api/grpc/core.proto | 1 + 6 files changed, 97 insertions(+), 20 deletions(-) diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index c4317e2e..1d766424 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -84,6 +84,7 @@ def handle_link_event(event: LinkData) -> core_pb2.LinkEvent: interface_one=interface_one, interface_two=interface_two, options=options, + network_id=event.network_id, ) return core_pb2.LinkEvent(message_type=event.message_type.value, link=link) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 0aa5a553..7b01517d 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -370,6 +370,7 @@ def convert_link(session: Session, link_data: LinkData) -> core_pb2.Link: interface_one=interface_one, interface_two=interface_two, options=options, + network_id=link_data.network_id, ) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 58efa55d..4a7cff64 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -207,13 +207,17 @@ class CoreClient: logging.debug("Link event: %s", event) node_one_id = event.link.node_one_id node_two_id = event.link.node_two_id + network_id = event.link.network_id canvas_node_one = self.canvas_nodes[node_one_id] canvas_node_two = self.canvas_nodes[node_two_id] - if event.message_type == core_pb2.MessageType.ADD: - self.app.canvas.add_wireless_edge(canvas_node_one, canvas_node_two) + self.app.canvas.add_wireless_edge( + canvas_node_one, canvas_node_two, network_id + ) elif event.message_type == core_pb2.MessageType.DELETE: - self.app.canvas.delete_wireless_edge(canvas_node_one, canvas_node_two) + self.app.canvas.delete_wireless_edge( + canvas_node_one, canvas_node_two, network_id + ) else: logging.warning("unknown link event: %s", event.message_type) diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 1bd1bffd..728026db 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -17,6 +17,7 @@ EDGE_WIDTH = 3 EDGE_COLOR = "#ff0000" WIRELESS_WIDTH = 1.5 WIRELESS_COLOR = "#009933" +ARC_DISTANCE = 50 def interface_label(interface: core_pb2.Interface) -> str: @@ -28,8 +29,40 @@ def interface_label(interface: core_pb2.Interface) -> str: return label -def create_edge_token(src: int, dst: int) -> Tuple[int, ...]: - return tuple(sorted([src, dst])) +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 arc_edges(edges) -> None: + if not edges: + return + mid_index = len(edges) // 2 + if mid_index == 0: + arc_step = ARC_DISTANCE + else: + arc_step = ARC_DISTANCE / mid_index + # below edges + arc = 0 + for edge in edges[:mid_index]: + arc -= arc_step + edge.arc = arc + edge.redraw() + # mid edge + if len(edges) % 2 != 0: + arc = 0 + edge = edges[mid_index] + edge.arc = arc + edge.redraw() + mid_index += 1 + # above edges + arc = 0 + for edge in edges[mid_index:]: + arc += arc_step + edge.arc = arc + edge.redraw() class Edge: @@ -49,21 +82,40 @@ class Edge: def create_token(cls, src: int, dst: int) -> Tuple[int, ...]: return tuple(sorted([src, dst])) - def _get_midpoint( + def _get_arcpoint( self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float] ) -> Tuple[float, float]: src_x, src_y = src_pos dst_x, dst_y = dst_pos - t = math.atan2(dst_y - src_y, dst_x - src_y) - x_mp = (src_x + dst_x) / 2 + self.arc * math.sin(t) - y_mp = (src_y + dst_y) / 2 - self.arc * math.cos(t) - return x_mp, y_mp + mp_x = (src_x + dst_x) / 2 + mp_y = (src_y + dst_y) / 2 + slope_denominator = src_x - dst_x + slope_numerator = src_y - dst_y + # vertical line + if slope_denominator == 0: + return mp_x + self.arc, mp_y + # horizontal line + if slope_numerator == 0: + return mp_x, mp_y + self.arc + # everything else + m = slope_numerator / slope_denominator + perp_m = -1 / m + b = mp_y - (perp_m * mp_x) + # get arc x and y + offset = math.sqrt(self.arc ** 2 / (1 + (1 / m ** 2))) + arc_x = mp_x + if self.arc >= 0: + arc_x += offset + else: + arc_x -= offset + arc_y = (perp_m * arc_x) + b + return arc_x, arc_y def draw(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None: - mid_pos = self._get_midpoint(src_pos, dst_pos) + arc_pos = self._get_arcpoint(src_pos, dst_pos) self.id = self.canvas.create_line( *src_pos, - *mid_pos, + *arc_pos, *dst_pos, smooth=True, tags=self.tag, @@ -71,6 +123,12 @@ class Edge: fill=self.color, ) + def redraw(self): + width = self.width * self.canvas.app.app_scale + self.canvas.itemconfig(self.id, width=width, fill=self.color) + src_x, src_y, _, _, _, _ = self.canvas.coords(self.id) + self.move_src(src_x, src_y) + def move_node(self, node_id: int, x: float, y: float) -> None: if self.src == node_id: self.move_src(x, y) @@ -81,15 +139,15 @@ class Edge: dst_pos = (x, y) src_x, src_y, _, _, _, _ = self.canvas.coords(self.id) src_pos = (src_x, src_y) - mid_pos = self._get_midpoint(src_pos, dst_pos) - self.canvas.coords(self.id, *src_pos, *mid_pos, *dst_pos) + arc_pos = self._get_arcpoint(src_pos, dst_pos) + self.canvas.coords(self.id, *src_pos, *arc_pos, *dst_pos) def move_src(self, x: float, y: float) -> None: src_pos = (x, y) _, _, _, _, dst_x, dst_y = self.canvas.coords(self.id) dst_pos = (dst_x, dst_y) - mid_pos = self._get_midpoint(src_pos, dst_pos) - self.canvas.coords(self.id, *src_pos, *mid_pos, *dst_pos) + arc_pos = self._get_arcpoint(src_pos, dst_pos) + self.canvas.coords(self.id, *src_pos, *arc_pos, *dst_pos) def delete(self) -> None: self.canvas.delete(self.id) @@ -201,6 +259,7 @@ class CanvasEdge(Edge): ) def redraw(self) -> None: + super().redraw() label_one, label_two = self.create_labels() self.canvas.itemconfig(self.text_src, text=label_one) self.canvas.itemconfig(self.text_dst, text=label_two) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 4c0d1abf..5f672ead 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -11,6 +11,7 @@ from core.gui.graph.edges import ( EDGE_WIDTH, CanvasEdge, CanvasWirelessEdge, + arc_edges, create_edge_token, ) from core.gui.graph.enums import GraphMode, ScaleOption @@ -198,11 +199,13 @@ class CanvasGraph(tk.Canvas): self.tag_lower(tags.GRIDLINE) self.tag_lower(self.grid) - def add_wireless_edge(self, src: CanvasNode, dst: CanvasNode): + def add_wireless_edge( + self, src: CanvasNode, dst: CanvasNode, network_id: int = None + ): """ add a wireless edge between 2 canvas nodes """ - token = create_edge_token(src.id, dst.id) + token = create_edge_token(src.id, dst.id, network_id) src_pos = self.coords(src.id) dst_pos = self.coords(dst.id) edge = CanvasWirelessEdge(self, src.id, dst.id, src_pos, dst_pos, token) @@ -211,13 +214,21 @@ class CanvasGraph(tk.Canvas): 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) - def delete_wireless_edge(self, src: CanvasNode, dst: CanvasNode): - token = create_edge_token(src.id, dst.id) + def delete_wireless_edge( + self, src: CanvasNode, dst: CanvasNode, network_id: int = None + ): + token = create_edge_token(src.id, dst.id, network_id) 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) def draw_session(self, session: core_pb2.Session): """ diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index e7bac450..4c0f3db6 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -686,6 +686,7 @@ message Link { Interface interface_one = 4; Interface interface_two = 5; LinkOptions options = 6; + int32 network_id = 7; } message LinkOptions { From 2b97b311abe1e1f5bc4c514f03e7be0bf8d533cb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Apr 2020 17:08:42 -0700 Subject: [PATCH 0151/1131] pygui ignore adding/removing duplicate wireless link events, ignore wireless link events for node to itself --- daemon/core/gui/coreclient.py | 3 +++ daemon/core/gui/graph/graph.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 4a7cff64..d4a5496d 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -208,6 +208,9 @@ class CoreClient: node_one_id = event.link.node_one_id node_two_id = event.link.node_two_id network_id = event.link.network_id + if node_one_id == node_two_id: + logging.warning("ignoring invalid link: %s", event) + return canvas_node_one = self.canvas_nodes[node_one_id] canvas_node_two = self.canvas_nodes[node_two_id] if event.message_type == core_pb2.MessageType.ADD: diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 5f672ead..0cacec15 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -206,6 +206,8 @@ class CanvasGraph(tk.Canvas): add a wireless edge between 2 canvas nodes """ token = create_edge_token(src.id, dst.id, network_id) + if token in self.wireless_edges: + 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) @@ -222,6 +224,8 @@ class CanvasGraph(tk.Canvas): self, src: CanvasNode, dst: CanvasNode, network_id: int = None ): token = create_edge_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) From 86b0c077647f9a378b90c5eb969696acb539384f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 15 Apr 2020 11:44:14 -0700 Subject: [PATCH 0152/1131] fixed issue when reading xml file and not associating node with emane model, causing error for grpc --- daemon/core/errors.py | 8 ++++++++ daemon/core/xml/corexml.py | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/daemon/core/errors.py b/daemon/core/errors.py index 319bd190..d299f5ae 100644 --- a/daemon/core/errors.py +++ b/daemon/core/errors.py @@ -22,3 +22,11 @@ class CoreError(Exception): """ pass + + +class CoreXmlError(Exception): + """ + Used when there was an error parsing a CORE xml file. + """ + + pass diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 9951a994..234f59da 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -9,6 +9,7 @@ from core.emane.nodes import EmaneNet from core.emulator.data import LinkData from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.enumerations import EventTypes, NodeTypes +from core.errors import CoreXmlError from core.nodes.base import CoreNetworkBase, CoreNodeBase, NodeBase from core.nodes.docker import DockerNode from core.nodes.lxd import LxcNode @@ -602,8 +603,8 @@ class CoreXmlReader: self.read_service_configs() self.read_mobility_configs() self.read_emane_global_config() - self.read_emane_configs() self.read_nodes() + self.read_emane_configs() self.read_configservice_configs() self.read_links() @@ -756,6 +757,18 @@ class CoreXmlReader: model_name = emane_configuration.get("model") configs = {} + # validate node and model + node = self.session.nodes.get(node_id) + if not node: + raise CoreXmlError(f"node for emane config doesn't exist: {node_id}") + if not isinstance(node, EmaneNet): + raise CoreXmlError(f"invalid node for emane config: {node.name}") + model = self.session.emane.models.get(model_name) + if not model: + raise CoreXmlError(f"invalid emane model: {model_name}") + node.setmodel(model, {}) + + # read and set emane model configuration mac_configuration = emane_configuration.find("mac") for config in mac_configuration.iterchildren(): name = config.get("name") From 23562cd294a83f451bb4252d8dab0a19af296bc3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 15 Apr 2020 12:41:09 -0700 Subject: [PATCH 0153/1131] updates for working label drawing on wireless links in pygui, will display sinr values on emane links --- daemon/core/api/grpc/events.py | 1 + daemon/core/api/grpc/grpcutils.py | 1 + daemon/core/gui/coreclient.py | 13 +++--- daemon/core/gui/graph/edges.py | 64 ++++++++++++++------------- daemon/core/gui/graph/graph.py | 26 ++++++++--- daemon/proto/core/api/grpc/core.proto | 1 + 6 files changed, 64 insertions(+), 42 deletions(-) diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index 1d766424..7505f1b4 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -85,6 +85,7 @@ def handle_link_event(event: LinkData) -> core_pb2.LinkEvent: interface_two=interface_two, options=options, network_id=event.network_id, + label=event.label, ) return core_pb2.LinkEvent(message_type=event.message_type.value, link=link) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 7b01517d..adc2e28d 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -371,6 +371,7 @@ def convert_link(session: Session, link_data: LinkData) -> core_pb2.Link: interface_two=interface_two, options=options, network_id=link_data.network_id, + label=link_data.label, ) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index d4a5496d..61350d05 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -207,22 +207,25 @@ class CoreClient: logging.debug("Link event: %s", event) node_one_id = event.link.node_one_id node_two_id = event.link.node_two_id - network_id = event.link.network_id if node_one_id == node_two_id: - logging.warning("ignoring invalid link: %s", event) + logging.warning("ignoring links with loops: %s", event) return canvas_node_one = self.canvas_nodes[node_one_id] canvas_node_two = self.canvas_nodes[node_two_id] if event.message_type == core_pb2.MessageType.ADD: self.app.canvas.add_wireless_edge( - canvas_node_one, canvas_node_two, network_id + canvas_node_one, canvas_node_two, event.link ) elif event.message_type == core_pb2.MessageType.DELETE: self.app.canvas.delete_wireless_edge( - canvas_node_one, canvas_node_two, network_id + canvas_node_one, canvas_node_two, event.link + ) + elif event.message_type == core_pb2.MessageType.NONE: + self.app.canvas.update_wireless_edge( + canvas_node_one, canvas_node_two, event.link ) else: - logging.warning("unknown link event: %s", event.message_type) + logging.warning("unknown link event: %s", event) def handle_node_event(self, event: core_pb2.NodeEvent): logging.debug("node event: %s", event) diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 728026db..a812e93e 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -75,6 +75,7 @@ class Edge: self.dst = dst self.arc = 0 self.token = None + self.middle_label = None self.color = EDGE_COLOR self.width = EDGE_WIDTH @@ -82,6 +83,9 @@ class Edge: 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 + def _get_arcpoint( self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float] ) -> Tuple[float, float]: @@ -119,16 +123,28 @@ class Edge: *dst_pos, smooth=True, tags=self.tag, - width=self.width * self.canvas.app.app_scale, + width=self.scaled_width(), fill=self.color, ) def redraw(self): - width = self.width * self.canvas.app.app_scale - self.canvas.itemconfig(self.id, width=width, fill=self.color) + self.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color) src_x, src_y, _, _, _, _ = self.canvas.coords(self.id) self.move_src(src_x, src_y) + def middle_label_pos(self) -> Tuple[float, float]: + _, _, x, y, _, _ = self.canvas.coords(self.id) + return x, y + + def middle_label_text(self, text: str) -> None: + if self.middle_label is None: + x, y = self.middle_label_pos() + self.middle_label = self.canvas.create_text( + x, y, font=self.canvas.app.edge_font, text=text + ) + else: + self.canvas.itemconfig(self.middle_label, text=text) + def move_node(self, node_id: int, x: float, y: float) -> None: if self.src == node_id: self.move_src(x, y) @@ -139,18 +155,23 @@ class Edge: dst_pos = (x, y) src_x, src_y, _, _, _, _ = self.canvas.coords(self.id) src_pos = (src_x, src_y) - arc_pos = self._get_arcpoint(src_pos, dst_pos) - self.canvas.coords(self.id, *src_pos, *arc_pos, *dst_pos) + self.moved(src_pos, dst_pos) def move_src(self, x: float, y: float) -> None: src_pos = (x, y) _, _, _, _, dst_x, dst_y = self.canvas.coords(self.id) dst_pos = (dst_x, dst_y) + self.moved(src_pos, dst_pos) + + def moved(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None: arc_pos = self._get_arcpoint(src_pos, dst_pos) self.canvas.coords(self.id, *src_pos, *arc_pos, *dst_pos) + if self.middle_label: + self.canvas.coords(self.middle_label, *arc_pos) def delete(self) -> None: self.canvas.delete(self.id) + self.canvas.delete(self.middle_label) class CanvasWirelessEdge(Edge): @@ -193,7 +214,6 @@ class CanvasEdge(Edge): self.dst_interface = None self.text_src = None self.text_dst = None - self.text_middle = None self.link = None self.asymmetric_link = None self.throughput = None @@ -223,12 +243,6 @@ class CanvasEdge(Edge): y2 = y2 - uy return x1, y1, x2, y2 - def get_midpoint(self) -> [float, float]: - x1, y1, x2, y2 = self.canvas.coords(self.id) - x = (x1 + x2) / 2 - y = (y1 + y2) / 2 - return x, y - def create_labels(self) -> Tuple[str, str]: label_one = None if self.link.HasField("interface_one"): @@ -271,27 +285,18 @@ class CanvasEdge(Edge): x1, y1, x2, y2 = self.get_coordinates() self.canvas.coords(self.text_src, x1, y1) self.canvas.coords(self.text_dst, x2, y2) - if self.text_middle is not None: - x, y = self.get_midpoint() - self.canvas.coords(self.text_middle, x, y) def set_throughput(self, throughput: float) -> None: throughput = 0.001 * throughput - value = f"{throughput:.3f} kbps" - if self.text_middle is None: - x, y = self.get_midpoint() - self.text_middle = self.canvas.create_text( - x, y, tags=tags.THROUGHPUT, font=self.canvas.app.edge_font, text=value - ) - else: - self.canvas.itemconfig(self.text_middle, text=value) - + text = f"{throughput:.3f} kbps" + self.middle_label_text(text) + self.canvas.addtag(self.middle_label, tags.THROUGHPUT) if throughput > self.canvas.throughput_threshold: color = self.canvas.throughput_color width = self.canvas.throughput_width else: - color = EDGE_COLOR - width = EDGE_WIDTH + color = self.color + width = self.scaled_width() self.canvas.itemconfig(self.id, fill=color, width=width) def complete(self, dst: int) -> None: @@ -349,12 +354,11 @@ class CanvasEdge(Edge): super().delete() self.canvas.delete(self.text_src) self.canvas.delete(self.text_dst) - self.canvas.delete(self.text_middle) def reset(self) -> None: - self.canvas.delete(self.text_middle) - self.text_middle = None - self.canvas.itemconfig(self.id, fill=EDGE_COLOR, width=EDGE_WIDTH) + self.canvas.delete(self.middle_label) + self.middle_label = None + self.canvas.itemconfig(self.id, fill=self.color, width=self.scaled_width()) def create_context(self, event: tk.Event) -> None: context = tk.Menu(self.canvas) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 0cacec15..82ae2fef 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -200,17 +200,18 @@ class CanvasGraph(tk.Canvas): self.tag_lower(self.grid) def add_wireless_edge( - self, src: CanvasNode, dst: CanvasNode, network_id: int = None - ): - """ - add a wireless edge between 2 canvas nodes - """ + self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link + ) -> None: + network_id = link.network_id if link.network_id else None token = create_edge_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) + if link.label: + edge.middle_label_text(link.label) self.wireless_edges[token] = edge src.wireless_edges.add(edge) dst.wireless_edges.add(edge) @@ -221,8 +222,9 @@ class CanvasGraph(tk.Canvas): arc_edges(common_edges) def delete_wireless_edge( - self, src: CanvasNode, dst: CanvasNode, network_id: int = None - ): + self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link + ) -> None: + network_id = link.network_id if link.network_id else None token = create_edge_token(src.id, dst.id, network_id) if token not in self.wireless_edges: return @@ -234,6 +236,16 @@ class CanvasGraph(tk.Canvas): common_edges = list(src.wireless_edges & dst.wireless_edges) arc_edges(common_edges) + def update_wireless_edge( + self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link + ) -> None: + 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) + edge = self.wireless_edges[token] + edge.middle_label_text(link.label) + def draw_session(self, session: core_pb2.Session): """ Draw existing session. diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 4c0f3db6..8394acd5 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -687,6 +687,7 @@ message Link { Interface interface_two = 5; LinkOptions options = 6; int32 network_id = 7; + string label = 8; } message LinkOptions { From 6f87986364b4a84cf7632a3e66ec0214474aae95 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 15 Apr 2020 12:51:35 -0700 Subject: [PATCH 0154/1131] pygui cleanup of edge code to use position tuples instead of individual params --- daemon/core/gui/graph/edges.py | 27 +++++++++++++-------------- daemon/core/gui/graph/graph.py | 2 +- daemon/core/gui/graph/node.py | 8 ++++---- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index a812e93e..cb716f4e 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -130,7 +130,8 @@ class Edge: def redraw(self): self.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color) src_x, src_y, _, _, _, _ = self.canvas.coords(self.id) - self.move_src(src_x, src_y) + src_pos = src_x, src_y + self.move_src(src_pos) def middle_label_pos(self) -> Tuple[float, float]: _, _, x, y, _, _ = self.canvas.coords(self.id) @@ -145,22 +146,20 @@ class Edge: else: self.canvas.itemconfig(self.middle_label, text=text) - def move_node(self, node_id: int, x: float, y: float) -> None: + def move_node(self, node_id: int, pos: Tuple[float, float]) -> None: if self.src == node_id: - self.move_src(x, y) + self.move_src(pos) else: - self.move_dst(x, y) + self.move_dst(pos) - def move_dst(self, x: float, y: float) -> None: - dst_pos = (x, y) + def move_dst(self, dst_pos: Tuple[float, float]) -> None: src_x, src_y, _, _, _, _ = self.canvas.coords(self.id) - src_pos = (src_x, src_y) + src_pos = src_x, src_y self.moved(src_pos, dst_pos) - def move_src(self, x: float, y: float) -> None: - src_pos = (x, y) + def move_src(self, src_pos: Tuple[float, float]) -> None: _, _, _, _, dst_x, dst_y = self.canvas.coords(self.id) - dst_pos = (dst_x, dst_y) + dst_pos = dst_x, dst_y self.moved(src_pos, dst_pos) def moved(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None: @@ -220,8 +219,8 @@ class CanvasEdge(Edge): self.draw(src_pos, dst_pos) self.set_binding() - def move_node(self, node_id: int, x: float, y: float) -> None: - super().move_node(node_id, x, y) + def move_node(self, node_id: int, pos: Tuple[float, float]) -> None: + super().move_node(node_id, pos) self.update_labels() def set_binding(self) -> None: @@ -302,8 +301,8 @@ class CanvasEdge(Edge): def complete(self, dst: int) -> None: self.dst = dst self.token = create_edge_token(self.src, self.dst) - x, y = self.canvas.coords(self.dst) - self.move_dst(x, y) + dst_pos = self.canvas.coords(self.dst) + self.move_dst(dst_pos) self.check_wireless() self.canvas.tag_raise(self.src) self.canvas.tag_raise(self.dst) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 82ae2fef..c26d3701 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -628,7 +628,7 @@ class CanvasGraph(tk.Canvas): self.cursor = x, y if self.mode == GraphMode.EDGE and self.drawing_edge is not None: - self.drawing_edge.move_dst(x, y) + self.drawing_edge.move_dst(self.cursor) if self.mode == GraphMode.ANNOTATION: if is_draw_shape(self.annotation_type) and self.shape_drawing: shape = self.shapes[self.selected] diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index df0d64b9..47bac1b8 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -130,7 +130,7 @@ class CanvasNode: def motion(self, x_offset: int, y_offset: int, update: bool = True): original_position = self.canvas.coords(self.id) self.canvas.move(self.id, x_offset, y_offset) - x, y = self.canvas.coords(self.id) + pos = self.canvas.coords(self.id) # check new position bbox = self.canvas.bbox(self.id) @@ -148,12 +148,12 @@ class CanvasNode: # move edges for edge in self.edges: - edge.move_node(self.id, x, y) + edge.move_node(self.id, pos) for edge in self.wireless_edges: - edge.move_node(self.id, x, y) + edge.move_node(self.id, pos) # set actual coords for node and update core is running - real_x, real_y = self.canvas.get_actual_coords(x, y) + real_x, real_y = self.canvas.get_actual_coords(*pos) self.core_node.position.x = real_x self.core_node.position.y = real_y if self.app.core.is_runtime() and update: From 0203d4178da6771a4999f19063e1e4a4ef285dc7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 15 Apr 2020 12:57:01 -0700 Subject: [PATCH 0155/1131] pygui removed unused throughput tag --- daemon/core/gui/graph/edges.py | 1 - daemon/core/gui/graph/tags.py | 1 - 2 files changed, 2 deletions(-) diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index cb716f4e..d01cb740 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -289,7 +289,6 @@ class CanvasEdge(Edge): throughput = 0.001 * throughput text = f"{throughput:.3f} kbps" self.middle_label_text(text) - self.canvas.addtag(self.middle_label, tags.THROUGHPUT) if throughput > self.canvas.throughput_threshold: color = self.canvas.throughput_color width = self.canvas.throughput_width diff --git a/daemon/core/gui/graph/tags.py b/daemon/core/gui/graph/tags.py index 45f6d0ee..53d547ac 100644 --- a/daemon/core/gui/graph/tags.py +++ b/daemon/core/gui/graph/tags.py @@ -9,7 +9,6 @@ NODE_NAME = "nodename" NODE = "node" WALLPAPER = "wallpaper" SELECTION = "selectednodes" -THROUGHPUT = "throughput" MARKER = "marker" ABOVE_WALLPAPER_TAGS = [ GRIDLINE, From 42979f1bb37e0ffe1b6e7e2991f0d6095e2e97af Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 15 Apr 2020 13:39:11 -0700 Subject: [PATCH 0156/1131] pygui edge code cleanup for node label drawing --- daemon/core/gui/graph/edges.py | 109 +++++++++++++++++---------------- 1 file changed, 57 insertions(+), 52 deletions(-) diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index d01cb740..1606da88 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -75,7 +75,9 @@ class Edge: self.dst = dst self.arc = 0 self.token = None + self.src_label = None self.middle_label = None + self.dst_label = None self.color = EDGE_COLOR self.width = EDGE_WIDTH @@ -146,6 +148,44 @@ class Edge: else: self.canvas.itemconfig(self.middle_label, text=text) + 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) + + def src_label_text(self, text: str) -> None: + if self.src_label is None: + src_pos, _ = self.node_label_positions() + self.src_label = self.canvas.create_text( + *src_pos, + text=text, + justify=tk.CENTER, + font=self.canvas.app.edge_font, + tags=tags.LINK_INFO, + ) + else: + self.canvas.itemconfig(self.src_label, text=text) + + def dst_label_text(self, text: str) -> None: + if self.dst_label is None: + _, dst_pos = self.node_label_positions() + self.dst_label = self.canvas.create_text( + *dst_pos, + text=text, + justify=tk.CENTER, + font=self.canvas.app.edge_font, + tags=tags.LINK_INFO, + ) + else: + self.canvas.itemconfig(self.dst_label, text=text) + def move_node(self, node_id: int, pos: Tuple[float, float]) -> None: if self.src == node_id: self.move_src(pos) @@ -167,10 +207,22 @@ class Edge: self.canvas.coords(self.id, *src_pos, *arc_pos, *dst_pos) if self.middle_label: self.canvas.coords(self.middle_label, *arc_pos) + src_pos, dst_pos = self.node_label_positions() + if self.src_label: + self.canvas.coords(self.src_label, *src_pos) + if self.dst_label: + self.canvas.coords(self.dst_label, *dst_pos) def delete(self) -> None: + logging.debug("deleting canvas edge, id: %s", self.id) self.canvas.delete(self.id) + self.canvas.delete(self.src_label) self.canvas.delete(self.middle_label) + self.canvas.delete(self.dst_label) + self.id = None + self.src_label = None + self.middle_label = None + self.dst_label = None class CanvasWirelessEdge(Edge): @@ -219,10 +271,6 @@ class CanvasEdge(Edge): self.draw(src_pos, dst_pos) self.set_binding() - def move_node(self, node_id: int, pos: Tuple[float, float]) -> None: - super().move_node(node_id, pos) - self.update_labels() - def set_binding(self) -> None: self.canvas.tag_bind(self.id, "", self.create_context) @@ -230,19 +278,7 @@ class CanvasEdge(Edge): self.link = link self.draw_labels() - def get_coordinates(self) -> [float, float, float, float]: - x1, y1, _, _, x2, y2 = self.canvas.coords(self.id) - v1 = x2 - x1 - v2 = y2 - y1 - ux = TEXT_DISTANCE * v1 - uy = TEXT_DISTANCE * v2 - x1 = x1 + ux - y1 = y1 + uy - x2 = x2 - ux - y2 = y2 - uy - return x1, y1, x2, y2 - - def create_labels(self) -> Tuple[str, str]: + def create_node_labels(self) -> Tuple[str, str]: label_one = None if self.link.HasField("interface_one"): label_one = interface_label(self.link.interface_one) @@ -252,38 +288,13 @@ class CanvasEdge(Edge): return label_one, label_two def draw_labels(self) -> None: - x1, y1, x2, y2 = self.get_coordinates() - label_one, label_two = self.create_labels() - self.text_src = self.canvas.create_text( - x1, - y1, - text=label_one, - justify=tk.CENTER, - font=self.canvas.app.edge_font, - tags=tags.LINK_INFO, - ) - self.text_dst = self.canvas.create_text( - x2, - y2, - text=label_two, - justify=tk.CENTER, - font=self.canvas.app.edge_font, - tags=tags.LINK_INFO, - ) + src_text, dst_text = self.create_node_labels() + self.src_label_text(src_text) + self.dst_label_text(dst_text) def redraw(self) -> None: super().redraw() - label_one, label_two = self.create_labels() - self.canvas.itemconfig(self.text_src, text=label_one) - self.canvas.itemconfig(self.text_dst, text=label_two) - - def update_labels(self) -> None: - """ - Move edge labels based on current position. - """ - x1, y1, x2, y2 = self.get_coordinates() - self.canvas.coords(self.text_src, x1, y1) - self.canvas.coords(self.text_dst, x2, y2) + self.draw_labels() def set_throughput(self, throughput: float) -> None: throughput = 0.001 * throughput @@ -347,12 +358,6 @@ class CanvasEdge(Edge): else: src_node.add_antenna() - def delete(self) -> None: - logging.debug("Delete canvas edge, id: %s", self.id) - super().delete() - self.canvas.delete(self.text_src) - self.canvas.delete(self.text_dst) - def reset(self) -> None: self.canvas.delete(self.middle_label) self.middle_label = None From e2490dee4a0a35a82904b18c653fb830c8eda057 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 15 Apr 2020 15:41:37 -0700 Subject: [PATCH 0157/1131] modified wireless links to obtain colors based on connected network from the session, LinkData will no provide a color itself --- daemon/core/api/grpc/events.py | 1 + daemon/core/api/grpc/grpcutils.py | 1 + daemon/core/api/grpc/server.py | 2 ++ daemon/core/emane/linkmonitor.py | 2 ++ daemon/core/emulator/data.py | 1 + daemon/core/emulator/session.py | 17 +++++++++++++++++ daemon/core/gui/graph/graph.py | 2 ++ daemon/core/location/mobility.py | 2 ++ daemon/core/plugins/sdt.py | 26 ++++---------------------- daemon/proto/core/api/grpc/core.proto | 1 + 10 files changed, 33 insertions(+), 22 deletions(-) diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index 7505f1b4..a77f50cf 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -86,6 +86,7 @@ def handle_link_event(event: LinkData) -> core_pb2.LinkEvent: options=options, network_id=event.network_id, label=event.label, + color=event.color, ) return core_pb2.LinkEvent(message_type=event.message_type.value, link=link) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index adc2e28d..6c5623ba 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -372,6 +372,7 @@ def convert_link(session: Session, link_data: LinkData) -> core_pb2.Link: options=options, network_id=link_data.network_id, label=link_data.label, + color=link_data.color, ) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index c559f8f2..b14412ee 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -1494,12 +1494,14 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): flag = MessageFlags.ADD else: flag = MessageFlags.DELETE + color = session.get_link_color(emane_one.id) link = LinkData( message_type=flag, link_type=LinkTypes.WIRELESS, node1_id=node_one.id, node2_id=node_two.id, network_id=emane_one.id, + color=color, ) session.broadcast_link(link) return EmaneLinkResponse(result=True) diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py index e161ada2..7eb903fd 100644 --- a/daemon/core/emane/linkmonitor.py +++ b/daemon/core/emane/linkmonitor.py @@ -314,6 +314,7 @@ class EmaneLinkMonitor: node_two: int, emane_id: int, ) -> None: + color = self.emane_manager.session.get_link_color(emane_id) link_data = LinkData( message_type=message_type, label=label, @@ -321,6 +322,7 @@ class EmaneLinkMonitor: node2_id=node_two, network_id=emane_id, link_type=LinkTypes.WIRELESS, + color=color, ) self.emane_manager.session.broadcast_link(link_data) diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index c7141541..7dff6be0 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -129,3 +129,4 @@ class LinkData: interface2_ip6: str = None interface2_ip6_mask: int = None opaque: str = None + color: str = None diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 15dcf7d1..80538fc3 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -76,6 +76,7 @@ NODES = { } NODES_TYPE = {NODES[x]: x for x in NODES} CTRL_NET_ID = 9001 +LINK_COLORS = ["green", "blue", "orange", "purple", "turquoise"] class Session: @@ -105,6 +106,7 @@ class Session: self.thumbnail = None self.user = None self.event_loop = EventLoop() + self.link_colors = {} # dict of nodes: all nodes and nets self.node_id_gen = IdGen() @@ -927,6 +929,7 @@ class Session: self.location.reset() self.services.reset() self.mobility.config_reset() + self.link_colors.clear() def start_events(self) -> None: """ @@ -1956,3 +1959,17 @@ class Session: else: node = self.get_node(node_id) node.cmd(data, wait=False) + + def get_link_color(self, network_id: int) -> str: + """ + Assign a color for links associated with a network. + + :param network_id: network to get a link color for + :return: link color + """ + color = self.link_colors.get(network_id) + if not color: + index = len(self.link_colors) % len(LINK_COLORS) + color = LINK_COLORS[index] + self.link_colors[network_id] = color + return color diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index c26d3701..19fde443 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -212,6 +212,8 @@ class CanvasGraph(tk.Canvas): edge = CanvasWirelessEdge(self, src.id, dst.id, src_pos, dst_pos, token) if link.label: edge.middle_label_text(link.label) + if link.color: + edge.color = link.color self.wireless_edges[token] = edge src.wireless_edges.add(edge) dst.wireless_edges.add(edge) diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index b5a76507..05a6ac3e 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -488,12 +488,14 @@ class BasicRangeModel(WirelessModel): :param message_type: link message type :return: link data """ + color = self.session.get_link_color(self.wlan.id) return LinkData( message_type=message_type, node1_id=interface1.node.id, node2_id=interface2.node.id, network_id=self.wlan.id, link_type=LinkTypes.WIRELESS, + color=color, ) def sendlinkmsg( diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 68be2147..93663052 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -33,7 +33,6 @@ NODE_LAYER = "CORE::Nodes" LINK_LAYER = "CORE::Links" CORE_LAYERS = [CORE_LAYER, LINK_LAYER, NODE_LAYER] DEFAULT_LINK_COLOR = "red" -LINK_COLORS = ["green", "blue", "orange", "purple", "white"] class Sdt: @@ -73,7 +72,6 @@ class Sdt: self.url = self.DEFAULT_SDT_URL self.address = None self.protocol = None - self.colors = {} self.network_layers = set() self.session.node_handlers.append(self.handle_node_update) self.session.link_handlers.append(self.handle_link_update) @@ -180,7 +178,6 @@ class Sdt: self.cmd(f"delete layer,{layer}") self.disconnect() self.network_layers.clear() - self.colors.clear() def cmd(self, cmdstr: str) -> bool: """ @@ -353,24 +350,6 @@ class Sdt: pass return result - def get_link_line(self, network_id: int) -> str: - """ - Retrieve link line color based on network. - - :param network_id: network id of link, None for wired links - :return: link line configuration - """ - network = self.session.nodes.get(network_id) - if network: - color = self.colors.get(network_id) - if not color: - index = len(self.colors) % len(LINK_COLORS) - color = LINK_COLORS[index] - self.colors[network_id] = color - else: - color = DEFAULT_LINK_COLOR - return f"{color},2" - def add_link( self, node_one: int, node_two: int, network_id: int = None, label: str = None ) -> None: @@ -388,7 +367,10 @@ class Sdt: return if self.wireless_net_check(node_one) or self.wireless_net_check(node_two): return - line = self.get_link_line(network_id) + color = DEFAULT_LINK_COLOR + if network_id: + color = self.session.get_link_color(network_id) + line = f"{color},2" link_id = get_link_id(node_one, node_two, network_id) layer = LINK_LAYER if network_id: diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 8394acd5..997d5287 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -688,6 +688,7 @@ message Link { LinkOptions options = 6; int32 network_id = 7; string label = 8; + string color = 9; } message LinkOptions { From 7e7bf8c7b718a7fc4fc00c34969b48d80d6bc6f8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 15 Apr 2020 16:36:03 -0700 Subject: [PATCH 0158/1131] fix p2p upstream link data not using enum, consolidated grpc logic for getting link protobufs --- daemon/core/api/grpc/events.py | 51 ++----------------------------- daemon/core/api/grpc/grpcutils.py | 35 ++++++--------------- daemon/core/api/grpc/server.py | 4 +-- daemon/core/nodes/base.py | 1 + daemon/core/nodes/network.py | 4 ++- 5 files changed, 18 insertions(+), 77 deletions(-) diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index a77f50cf..a53ad971 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -3,7 +3,7 @@ from queue import Empty, Queue from typing import Iterable from core.api.grpc import core_pb2 -from core.api.grpc.grpcutils import convert_value +from core.api.grpc.grpcutils import convert_link from core.emulator.data import ( ConfigData, EventData, @@ -40,54 +40,7 @@ def handle_link_event(event: LinkData) -> core_pb2.LinkEvent: :param event: link data :return: link event that has message type and link information """ - interface_one = None - if event.interface1_id is not None: - interface_one = core_pb2.Interface( - id=event.interface1_id, - name=event.interface1_name, - mac=convert_value(event.interface1_mac), - ip4=convert_value(event.interface1_ip4), - ip4mask=event.interface1_ip4_mask, - ip6=convert_value(event.interface1_ip6), - ip6mask=event.interface1_ip6_mask, - ) - - interface_two = None - if event.interface2_id is not None: - interface_two = core_pb2.Interface( - id=event.interface2_id, - name=event.interface2_name, - mac=convert_value(event.interface2_mac), - ip4=convert_value(event.interface2_ip4), - ip4mask=event.interface2_ip4_mask, - ip6=convert_value(event.interface2_ip6), - ip6mask=event.interface2_ip6_mask, - ) - - options = core_pb2.LinkOptions( - opaque=event.opaque, - jitter=event.jitter, - key=event.key, - mburst=event.mburst, - mer=event.mer, - per=event.per, - bandwidth=event.bandwidth, - burst=event.burst, - delay=event.delay, - dup=event.dup, - unidirectional=event.unidirectional, - ) - link = core_pb2.Link( - type=event.link_type.value, - node_one_id=event.node1_id, - node_two_id=event.node2_id, - interface_one=interface_one, - interface_two=interface_two, - options=options, - network_id=event.network_id, - label=event.label, - color=event.color, - ) + link = convert_link(event) return core_pb2.LinkEvent(message_type=event.message_type.value, link=link) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 6c5623ba..4736f017 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -13,7 +13,7 @@ from core.emulator.data import LinkData from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.enumerations import LinkTypes, NodeTypes from core.emulator.session import Session -from core.nodes.base import CoreNetworkBase, NodeBase +from core.nodes.base import NodeBase from core.nodes.interface import CoreInterface from core.services.coreservices import CoreService @@ -263,17 +263,16 @@ def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node: ) -def get_links(session: Session, node: NodeBase): +def get_links(node: NodeBase): """ - Retrieve a list of links for grpc to use + Retrieve a list of links for grpc to use. - :param session: node's section :param node: node to get links from - :return: [core.api.grpc.core_pb2.Link] + :return: protobuf links """ links = [] for link_data in node.all_link_data(): - link = convert_link(session, link_data) + link = convert_link(link_data) links.append(link) return links @@ -307,48 +306,35 @@ def parse_emane_model_id(_id: int) -> Tuple[int, int]: return node_id, interface -def convert_link(session: Session, link_data: LinkData) -> core_pb2.Link: +def convert_link(link_data: LinkData) -> core_pb2.Link: """ - Convert link_data into core protobuf Link + Convert link_data into core protobuf link. - :param session: - :param link_data: + :param link_data: link to convert :return: core protobuf Link """ interface_one = None if link_data.interface1_id is not None: - node = session.get_node(link_data.node1_id) - interface_name = None - if not isinstance(node, CoreNetworkBase): - interface = node.netif(link_data.interface1_id) - interface_name = interface.name interface_one = core_pb2.Interface( id=link_data.interface1_id, - name=interface_name, + name=link_data.interface1_name, mac=convert_value(link_data.interface1_mac), ip4=convert_value(link_data.interface1_ip4), ip4mask=link_data.interface1_ip4_mask, ip6=convert_value(link_data.interface1_ip6), ip6mask=link_data.interface1_ip6_mask, ) - interface_two = None if link_data.interface2_id is not None: - node = session.get_node(link_data.node2_id) - interface_name = None - if not isinstance(node, CoreNetworkBase): - interface = node.netif(link_data.interface2_id) - interface_name = interface.name interface_two = core_pb2.Interface( id=link_data.interface2_id, - name=interface_name, + name=link_data.interface2_name, mac=convert_value(link_data.interface2_mac), ip4=convert_value(link_data.interface2_ip4), ip4mask=link_data.interface2_ip4_mask, ip6=convert_value(link_data.interface2_ip6), ip6mask=link_data.interface2_ip6_mask, ) - options = core_pb2.LinkOptions( opaque=link_data.opaque, jitter=link_data.jitter, @@ -362,7 +348,6 @@ def convert_link(session: Session, link_data: LinkData) -> core_pb2.Link: dup=link_data.dup, unidirectional=link_data.unidirectional, ) - return core_pb2.Link( type=link_data.link_type.value, node_one_id=link_data.node1_id, diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index b14412ee..d9c23628 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -543,7 +543,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): continue node_proto = grpcutils.get_node_proto(session, node) nodes.append(node_proto) - node_links = get_links(session, node) + node_links = get_links(node) links.extend(node_links) session_proto = core_pb2.Session( @@ -788,7 +788,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("get node links: %s", request) session = self.get_session(request.session_id, context) node = self.get_node(session, request.node_id, context) - links = get_links(session, node) + links = get_links(node) return core_pb2.GetNodeLinksResponse(links=links) def AddLink( diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 098924db..721f643f 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -1112,6 +1112,7 @@ class CoreNetworkBase(NodeBase): link_type=self.linktype, unidirectional=unidirectional, interface2_id=linked_node.getifindex(netif), + interface2_name=netif.name, interface2_mac=netif.hwaddr, interface2_ip4=interface2_ip4, interface2_ip4_mask=interface2_ip4_mask, diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index eb179e84..f2c16bd0 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -949,12 +949,14 @@ class PtpNet(CoreNetwork): dup=if1.getparam("duplicate"), jitter=if1.getparam("jitter"), interface1_id=if1.node.getifindex(if1), + interface1_name=if1.name, interface1_mac=if1.hwaddr, interface1_ip4=interface1_ip4, interface1_ip4_mask=interface1_ip4_mask, interface1_ip6=interface1_ip6, interface1_ip6_mask=interface1_ip6_mask, interface2_id=if2.node.getifindex(if2), + interface2_name=if2.name, interface2_mac=if2.hwaddr, interface2_ip4=interface2_ip4, interface2_ip4_mask=interface2_ip4_mask, @@ -968,7 +970,7 @@ class PtpNet(CoreNetwork): # (swap if1 and if2) if unidirectional: link_data = LinkData( - message_type=0, + message_type=MessageFlags.NONE, link_type=self.linktype, node1_id=if2.node.id, node2_id=if1.node.id, From 9bd13dce1e84561bc9d4bd04513199def2e88b3f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 16 Apr 2020 09:19:50 -0700 Subject: [PATCH 0159/1131] updates to allow setting 0 services for a node, but old gui does not send data in a way that can be compatible --- daemon/core/emulator/emudata.py | 2 +- daemon/core/services/coreservices.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index 6a0ec8a6..a5e9bfff 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -74,7 +74,7 @@ class NodeOptions: self.canvas = None self.icon = None self.opaque = None - self.services = [] + self.services = None self.config_services = [] self.x = None self.y = None diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index 827982d2..f5a46c08 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -404,12 +404,11 @@ class CoreServices: :param services: names of services to add to node :return: nothing """ - if not services: + if services is None: logging.info( "using default services for node(%s) type(%s)", node.name, node_type ) services = self.default_services.get(node_type, []) - logging.info("setting services for node(%s): %s", node.name, services) for service_name in services: service = self.get_service(node.id, service_name, default_service=True) From ca292cb11e39d2673d19571704f5d283f68135d2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 16 Apr 2020 10:16:35 -0700 Subject: [PATCH 0160/1131] added -r flag to install.sh to provide a convenience to reinstall latest code from current git branch --- install.sh | 138 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 85 insertions(+), 53 deletions(-) diff --git a/install.sh b/install.sh index 6bd16a71..4e32bccd 100755 --- a/install.sh +++ b/install.sh @@ -5,6 +5,7 @@ set -e ubuntu_py=3.6 centos_py=36 +reinstall= function install_python_depencencies() { sudo python3 -m pip install -r daemon/requirements.txt @@ -33,6 +34,12 @@ function install_core() { sudo make install } +function uninstall_core() { + sudo make uninstall + make clean + ./bootstrap.sh clean +} + function install_dev_core() { cd gui sudo make install @@ -51,7 +58,7 @@ if [[ -f /etc/os-release ]]; then fi # parse arguments -while getopts "dv:" opt; do +while getopts "drv:" opt; do case ${opt} in d) dev=1 @@ -60,64 +67,89 @@ while getopts "dv:" opt; do ubuntu_py=${OPTARG} centos_py=${OPTARG} ;; + r) + reinstall=1 + ;; \?) - echo "script usage: $(basename $0) [-d] [-v python version]" >&2 + echo "script usage: $(basename $0) [-d] [-r] [-v python version]" >&2 exit 1 ;; esac done shift $((OPTIND - 1)) -# check install was found -case ${os} in -"ubuntu") - echo "Installing CORE for Ubuntu" - echo "installing core system dependencies" - sudo apt install -y automake pkg-config gcc libev-dev ebtables iproute2 \ - python${ubuntu_py} python${ubuntu_py}-dev python3-pip python3-tk tk libtk-img ethtool autoconf - python3 -m pip install grpcio-tools - echo "installing ospf-mdr system dependencies" - sudo apt install -y libtool gawk libreadline-dev - install_ospf_mdr - if [[ -z ${dev} ]]; then - echo "normal install" - install_python_depencencies +# check if we are reinstalling or installing +if [ -z "${reinstall}" ]; then + echo "installing CORE for ${os}" + case ${os} in + "ubuntu") + echo "installing core system dependencies" + sudo apt install -y automake pkg-config gcc libev-dev ebtables iproute2 \ + python${ubuntu_py} python${ubuntu_py}-dev python3-pip python3-tk tk libtk-img ethtool autoconf + python3 -m pip install grpcio-tools + echo "installing ospf-mdr system dependencies" + sudo apt install -y libtool gawk libreadline-dev + install_ospf_mdr + if [[ -z ${dev} ]]; then + echo "normal install" + install_python_depencencies + build_core + install_core + else + echo "dev install" + python3 -m pip install pipenv + build_core + install_dev_core + python3 -m pipenv sync --dev + python3 -m pipenv run pre-commit install + fi + ;; + "centos") + echo "installing core system dependencies" + sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel iptables-ebtables iproute \ + python${centos_py} python${centos_py}-devel python3-pip python3-tkinter tk ethtool autoconf + sudo python3 -m pip install grpcio-tools + echo "installing ospf-mdr system dependencies" + sudo yum install -y libtool gawk readline-devel + install_ospf_mdr + if [[ -z ${dev} ]]; then + echo "normal install" + install_python_depencencies + build_core --prefix=/usr + install_core + else + echo "dev install" + sudo python3 -m pip install pipenv + build_core --prefix=/usr + install_dev_core + sudo python3 -m pipenv sync --dev + python3 -m pipenv sync --dev + python3 -m pipenv run pre-commit install + fi + ;; + *) + echo "unknown OS ID ${os} cannot install" + ;; + esac +else + branch=$(git symbolic-ref --short HEAD) + echo "reinstalling CORE on ${os} with latest ${branch}" + echo "uninstalling CORE" + uninstall_core + echo "pulling latest code" + git pull + echo "building CORE" + case ${os} in + "ubuntu") build_core - install_core - else - echo "dev install" - python3 -m pip install pipenv - build_core - install_dev_core - python3 -m pipenv sync --dev - python3 -m pipenv run pre-commit install - fi - ;; -"centos") - echo "Installing CORE for CentOS" - echo "installing core system dependencies" - sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel iptables-ebtables iproute \ - python${centos_py} python${centos_py}-devel python3-pip python3-tkinter tk ethtool autoconf - sudo python3 -m pip install grpcio-tools - echo "installing ospf-mdr system dependencies" - sudo yum install -y libtool gawk readline-devel - install_ospf_mdr - if [[ -z ${dev} ]]; then - echo "normal install" - install_python_depencencies + ;; + "centos") build_core --prefix=/usr - install_core - else - echo "dev install" - sudo python3 -m pip install pipenv - build_core --prefix=/usr - install_dev_core - sudo python3 -m pipenv sync --dev - python3 -m pipenv sync --dev - python3 -m pipenv run pre-commit install - fi - ;; -*) - echo "unknown OS ID ${os} cannot install" - ;; -esac + ;; + *) + echo "unknown OS ID ${os} cannot reinstall" + ;; + esac + echo "installing CORE" + install_core +fi From 78d442b5746d93d8fd7f4d0a41ecc37c6a0fa435 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 16 Apr 2020 10:44:33 -0700 Subject: [PATCH 0161/1131] add service file content to xml as cdata to avoid escaping --- daemon/core/xml/corexml.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 234f59da..d3d4b94b 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -163,14 +163,12 @@ class ServiceElement: self.element.append(directories) def add_files(self) -> None: - # get custom files file_elements = etree.Element("files") for file_name in self.service.config_data: data = self.service.config_data[file_name] file_element = etree.SubElement(file_elements, "file") add_attribute(file_element, "name", file_name) - file_element.text = data - + file_element.text = etree.CDATA(data) if file_elements.getchildren(): self.element.append(file_elements) From 5dcf2f45c5485837ea9625f668f6ad6b1d1e1924 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 16 Apr 2020 11:32:50 -0700 Subject: [PATCH 0162/1131] updates to allow building python docs again, also added checks for requirements to build the python docs --- configure.ac | 21 ++++++++------------- daemon/core/api/grpc/server.py | 7 +++---- daemon/doc/Makefile.am | 2 +- daemon/doc/conf.py.in | 2 +- 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/configure.ac b/configure.ac index 24f322f0..588f3f34 100644 --- a/configure.ac +++ b/configure.ac @@ -43,6 +43,11 @@ AC_ARG_ENABLE([gui], [build and install the GUI (default is yes)])], [], [enable_gui=yes]) AC_SUBST(enable_gui) +AC_ARG_ENABLE([docs], + [AS_HELP_STRING([--enable-docs[=ARG]], + [build python documentation (default is no)])], + [], [enable_docs=no]) +AC_SUBST(enable_docs) AC_ARG_ENABLE([python], [AS_HELP_STRING([--enable-python[=ARG]], @@ -191,8 +196,7 @@ if [ test "x$enable_daemon" = "xyes" || test "x$enable_vnodedonly" = "xyes" ] ; fi want_docs=no -if test "x$enable_docs" = "xyes" ; then - +if [test "x$want_python" = "xyes" && test "x$enable_docs" = "xyes"] ; then AC_CHECK_PROG(help2man, help2man, yes, no, $SEARCHPATH) if test "x$help2man" = "xno" ; then @@ -210,21 +214,12 @@ if test "x$enable_docs" = "xyes" ; then # check for sphinx required during make AC_CHECK_PROG(sphinxapi_path, sphinx-apidoc, $as_dir, no, $SEARCHPATH) if test "x$sphinxapi_path" = "xno" ; then - AC_MSG_ERROR(["Could not location sphinx-apidoc, from the python-sphinx package"]) + AC_MSG_ERROR(["Could not locate sphinx-apidoc, install python3 -m pip install sphinx"]) want_docs=no fi + AS_IF([$PYTHON -c "import sphinx_rtd_theme" &> /dev/null], [], [AC_MSG_ERROR([doc dependency missing, please install python3 -m pip install sphinx-rtd-theme])]) fi -#AC_PATH_PROGS(tcl_path, [tclsh tclsh8.5 tclsh8.4], no) -#if test "x$tcl_path" = "xno" ; then -# AC_MSG_ERROR([Could not locate tclsh. Please install Tcl/Tk.]) -#fi - -#AC_PATH_PROGS(wish_path, [wish wish8.5 wish8.4], no) -#if test "x$wish_path" = "xno" ; then -# AC_MSG_ERROR([Could not locate wish. Please install Tcl/Tk.]) -#fi - AC_ARG_WITH([startup], [AS_HELP_STRING([--with-startup=option], [option=systemd,suse,none to install systemd/SUSE init scripts])], diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index d9c23628..ca5eb0ad 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -1031,8 +1031,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ Retrieve all the default services of all node types in a session - :param request: - get-default-service request + :param request: get-default-service request :param context: context object :return: get-service-defaults response about all the available default services """ @@ -1050,8 +1049,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): ) -> SetServiceDefaultsResponse: """ Set new default services to the session after whipping out the old ones - :param request: set-service-defaults - request + + :param request: set-service-defaults request :param context: context object :return: set-service-defaults response """ diff --git a/daemon/doc/Makefile.am b/daemon/doc/Makefile.am index 6f287b09..e46f7d32 100644 --- a/daemon/doc/Makefile.am +++ b/daemon/doc/Makefile.am @@ -10,7 +10,7 @@ # extra cruft to remove DISTCLEANFILES = conf.py Makefile Makefile.in stamp-vti *.rst -all: index.rst +all: html # auto-generated Python documentation using Sphinx index.rst: diff --git a/daemon/doc/conf.py.in b/daemon/doc/conf.py.in index eee03477..99929cee 100644 --- a/daemon/doc/conf.py.in +++ b/daemon/doc/conf.py.in @@ -121,7 +121,7 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +#html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. From 1117522c211a54f3f8a8a033a4fac350624ea06f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 16 Apr 2020 12:31:48 -0700 Subject: [PATCH 0163/1131] reverting node service change until protobuf changes are in place --- daemon/core/emulator/emudata.py | 2 +- daemon/core/services/coreservices.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index a5e9bfff..6a0ec8a6 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -74,7 +74,7 @@ class NodeOptions: self.canvas = None self.icon = None self.opaque = None - self.services = None + self.services = [] self.config_services = [] self.x = None self.y = None diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index f5a46c08..35cd3ed3 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -404,7 +404,7 @@ class CoreServices: :param services: names of services to add to node :return: nothing """ - if services is None: + if not services: logging.info( "using default services for node(%s) type(%s)", node.name, node_type ) From 02c8604d6a67ed28b6b53f739b6f84f8066f7cad Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 16 Apr 2020 18:48:46 -0700 Subject: [PATCH 0164/1131] added a bit more context on install docs for tested distributions and the iproute2 requirement --- docs/install.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/install.md b/docs/install.md index 4ab6ed2e..4a39218d 100644 --- a/docs/install.md +++ b/docs/install.md @@ -6,6 +6,11 @@ ## Overview This section will describe how to install CORE from source or from a pre-built package. +CORE has been vetted on Ubuntu 18 and CentOS 7.6. Other versions and distributions +can work, assuming you can get the required packages and versions similar to those +noted below for the tested distributions. + +> **NOTE:** iproute2 4.5+ is a requirement for bridge related commands ## Required Hardware From c09e3e90d62703c59ccce91f73ac2c5c54585a27 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 17 Apr 2020 23:18:11 -0700 Subject: [PATCH 0165/1131] pygui pass at removing disabled menu items, small reorg and cleanup --- daemon/core/gui/menubar.py | 256 +++++-------------------------------- 1 file changed, 32 insertions(+), 224 deletions(-) diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 801d236f..0aa22e0c 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -53,31 +53,21 @@ class Menubar(tk.Menu): command=self.menuaction.new_session, ) self.app.bind_all("", lambda e: self.app.core.create_new_session()) + menu.add_command(label="Save", accelerator="Ctrl+S", command=self.save) + self.app.bind_all("", self.save) + menu.add_command(label="Save As...", command=self.menuaction.file_save_as_xml) menu.add_command( label="Open...", command=self.menuaction.file_open_xml, accelerator="Ctrl+O" ) self.app.bind_all("", self.menuaction.file_open_xml) - menu.add_command(label="Save", accelerator="Ctrl+S", command=self.save) - menu.add_command(label="Save As", command=self.menuaction.file_save_as_xml) - menu.add_command(label="Reload", underline=0, state=tk.DISABLED) - self.app.bind_all("", self.save) - self.recent_menu = tk.Menu(menu) for i in self.app.guiconfig["recentfiles"]: self.recent_menu.add_command( label=i, command=partial(self.open_recent_files, i) ) - menu.add_cascade(label="Recent files", menu=self.recent_menu) + menu.add_cascade(label="Recent Files", menu=self.recent_menu) menu.add_separator() - menu.add_command(label="Export Python script...", state=tk.DISABLED) - menu.add_command(label="Execute Python script...", command=self.execute_python) - menu.add_command( - label="Execute Python script with options...", state=tk.DISABLED - ) - menu.add_separator() - menu.add_command(label="Open current file in editor...", state=tk.DISABLED) - menu.add_command(label="Print...", underline=0, state=tk.DISABLED) - menu.add_command(label="Save screenshot...", state=tk.DISABLED) + menu.add_command(label="Execute Python Script...", command=self.execute_python) menu.add_separator() menu.add_command( label="Quit", accelerator="Ctrl+Q", command=self.menuaction.on_quit @@ -104,14 +94,6 @@ class Menubar(tk.Menu): menu.add_command( label="Delete", accelerator="Ctrl+D", command=self.menuaction.delete ) - menu.add_separator() - menu.add_command(label="Select all", accelerator="Ctrl+A", state=tk.DISABLED) - menu.add_command( - label="Select Adjacent", accelerator="Ctrl+J", state=tk.DISABLED - ) - menu.add_separator() - menu.add_command(label="Find...", accelerator="Ctrl+F", state=tk.DISABLED) - menu.add_command(label="Clear marker", state=tk.DISABLED) self.add_cascade(label="Edit", menu=menu) self.app.master.bind_all("", self.menuaction.copy) @@ -125,41 +107,18 @@ class Menubar(tk.Menu): """ menu = tk.Menu(self) menu.add_command( - label="Size/scale...", command=self.menuaction.canvas_size_and_scale + label="Size / Scale", command=self.menuaction.canvas_size_and_scale ) menu.add_command( - label="Wallpaper...", command=self.menuaction.canvas_set_wallpaper + label="Wallpaper", command=self.menuaction.canvas_set_wallpaper ) - menu.add_separator() - menu.add_command(label="New", state=tk.DISABLED) - menu.add_command(label="Manage...", state=tk.DISABLED) - menu.add_command(label="Delete", state=tk.DISABLED) - menu.add_separator() - menu.add_command(label="Previous", accelerator="PgUp", state=tk.DISABLED) - menu.add_command(label="Next", accelerator="PgDown", state=tk.DISABLED) - menu.add_command(label="First", accelerator="Home", state=tk.DISABLED) - menu.add_command(label="Last", accelerator="End", state=tk.DISABLED) self.add_cascade(label="Canvas", menu=menu) def draw_view_menu(self): """ Create view menu """ - view_menu = tk.Menu(self) - self.create_show_menu(view_menu) - view_menu.add_command(label="Show hidden nodes", state=tk.DISABLED) - view_menu.add_command(label="Locked", state=tk.DISABLED) - view_menu.add_command(label="3D GUI...", state=tk.DISABLED) - view_menu.add_separator() - view_menu.add_command(label="Zoom in", accelerator="+", state=tk.DISABLED) - view_menu.add_command(label="Zoom out", accelerator="-", state=tk.DISABLED) - self.add_cascade(label="View", menu=view_menu) - - def create_show_menu(self, view_menu: tk.Menu): - """ - Create the menu items in View/Show - """ - menu = tk.Menu(view_menu) + menu = tk.Menu(self) menu.add_command(label="All", state=tk.DISABLED) menu.add_command(label="None", state=tk.DISABLED) menu.add_separator() @@ -169,170 +128,16 @@ class Menubar(tk.Menu): menu.add_command(label="Node Labels", state=tk.DISABLED) menu.add_command(label="Annotations", state=tk.DISABLED) menu.add_command(label="Grid", state=tk.DISABLED) - menu.add_command(label="API Messages", state=tk.DISABLED) - view_menu.add_cascade(label="Show", menu=menu) - - def create_experimental_menu(self, tools_menu: tk.Menu): - """ - Create experimental menu item and the sub menu items inside - """ - menu = tk.Menu(tools_menu) - menu.add_command(label="Plugins...", state=tk.DISABLED) - menu.add_command(label="ns2immunes converter...", state=tk.DISABLED) - menu.add_command(label="Topology partitioning...", state=tk.DISABLED) - tools_menu.add_cascade(label="Experimental", menu=menu) - - def create_random_menu(self, topology_generator_menu: tk.Menu): - """ - Create random menu item and the sub menu items inside - """ - menu = tk.Menu(topology_generator_menu) - # list of number of random nodes to create - nums = [1, 5, 10, 15, 20, 30, 40, 50, 75, 100] - for i in nums: - label = f"R({i})" - menu.add_command(label=label, state=tk.DISABLED) - topology_generator_menu.add_cascade(label="Random", menu=menu) - - def create_grid_menu(self, topology_generator_menu: tk.Menu): - """ - Create grid menu item and the sub menu items inside - """ - menu = tk.Menu(topology_generator_menu) - # list of number of nodes to create - nums = [1, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 100] - for i in nums: - label = f"G({i})" - menu.add_command(label=label, state=tk.DISABLED) - topology_generator_menu.add_cascade(label="Grid", menu=menu) - - def create_connected_grid_menu(self, topology_generator_menu: tk.Menu): - """ - Create connected grid menu items and the sub menu items inside - """ - menu = tk.Menu(topology_generator_menu) - for i in range(1, 11, 1): - submenu = tk.Menu(menu) - for j in range(1, 11, 1): - label = f"{i} X {j}" - submenu.add_command(label=label, state=tk.DISABLED) - label = str(i) + " X N" - menu.add_cascade(label=label, menu=submenu) - topology_generator_menu.add_cascade(label="Connected Grid", menu=menu) - - def create_chain_menu(self, topology_generator_menu: tk.Menu): - """ - Create chain menu item and the sub menu items inside - """ - menu = tk.Menu(topology_generator_menu) - # number of nodes to create - nums = list(range(2, 25, 1)) + [32, 64, 128] - for i in nums: - label = f"P({i})" - menu.add_command(label=label, state=tk.DISABLED) - topology_generator_menu.add_cascade(label="Chain", menu=menu) - - def create_star_menu(self, topology_generator_menu: tk.Menu): - """ - Create star menu item and the sub menu items inside - """ - menu = tk.Menu(topology_generator_menu) - for i in range(3, 26, 1): - label = f"C({i})" - menu.add_command(label=label, state=tk.DISABLED) - topology_generator_menu.add_cascade(label="Star", menu=menu) - - def create_cycle_menu(self, topology_generator_menu: tk.Menu): - """ - Create cycle menu item and the sub items inside - """ - menu = tk.Menu(topology_generator_menu) - for i in range(3, 25, 1): - label = f"C({i})" - menu.add_command(label=label, state=tk.DISABLED) - topology_generator_menu.add_cascade(label="Cycle", menu=menu) - - def create_wheel_menu(self, topology_generator_menu: tk.Menu): - """ - Create wheel menu item and the sub menu items inside - """ - menu = tk.Menu(topology_generator_menu) - for i in range(4, 26, 1): - label = f"W({i})" - menu.add_command(label=label, state=tk.DISABLED) - topology_generator_menu.add_cascade(label="Wheel", menu=menu) - - def create_cube_menu(self, topology_generator_menu: tk.Menu): - """ - Create cube menu item and the sub menu items inside - """ - menu = tk.Menu(topology_generator_menu) - for i in range(2, 7, 1): - label = f"Q({i})" - menu.add_command(label=label, state=tk.DISABLED) - topology_generator_menu.add_cascade(label="Cube", menu=menu) - - def create_clique_menu(self, topology_generator_menu: tk.Menu): - """ - Create clique menu item and the sub menu items inside - """ - menu = tk.Menu(topology_generator_menu) - for i in range(3, 25, 1): - label = f"K({i})" - menu.add_command(label=label, state=tk.DISABLED) - topology_generator_menu.add_cascade(label="Clique", menu=menu) - - def create_bipartite_menu(self, topology_generator_menu: tk.Menu): - """ - Create bipartite menu item and the sub menu items inside - """ - menu = tk.Menu(topology_generator_menu) - temp = 24 - for i in range(1, 13, 1): - submenu = tk.Menu(menu) - for j in range(i, temp, 1): - label = f"K({i} X {j})" - submenu.add_command(label=label, state=tk.DISABLED) - label = f"K({i})" - menu.add_cascade(label=label, menu=submenu) - temp = temp - 1 - topology_generator_menu.add_cascade(label="Bipartite", menu=menu) - - def create_topology_generator_menu(self, tools_menu: tk.Menu): - """ - Create topology menu item and its sub menu items - """ - menu = tk.Menu(tools_menu) - self.create_random_menu(menu) - self.create_grid_menu(menu) - self.create_connected_grid_menu(menu) - self.create_chain_menu(menu) - self.create_star_menu(menu) - self.create_cycle_menu(menu) - self.create_wheel_menu(menu) - self.create_cube_menu(menu) - self.create_clique_menu(menu) - self.create_bipartite_menu(menu) - tools_menu.add_cascade(label="Topology generator", menu=menu) + self.add_cascade(label="View", menu=menu) def draw_tools_menu(self): """ Create tools menu """ menu = tk.Menu(self) - menu.add_command(label="Auto rearrange all", state=tk.DISABLED) - menu.add_command(label="Auto rearrange selected", state=tk.DISABLED) - menu.add_separator() - menu.add_command(label="Align to grid", state=tk.DISABLED) - menu.add_separator() - menu.add_command(label="Traffic...", state=tk.DISABLED) - menu.add_command(label="IP addresses...", state=tk.DISABLED) - menu.add_command(label="MAC addresses...", state=tk.DISABLED) - menu.add_command(label="Build hosts file...", state=tk.DISABLED) - menu.add_command(label="Renumber nodes...", state=tk.DISABLED) - self.create_experimental_menu(menu) - self.create_topology_generator_menu(menu) - menu.add_command(label="Debugger...", state=tk.DISABLED) + menu.add_command(label="Auto Grid", state=tk.DISABLED) + menu.add_command(label="IP Addresses", state=tk.DISABLED) + menu.add_command(label="MAC Addresses", state=tk.DISABLED) self.add_cascade(label="Tools", menu=menu) def create_observer_widgets_menu(self, widget_menu: tk.Menu): @@ -375,12 +180,23 @@ class Menubar(tk.Menu): Create adjacency menu item and the sub menu items inside """ menu = tk.Menu(widget_menu) - menu.add_command(label="OSPFv2", state=tk.DISABLED) - menu.add_command(label="OSPFv3", state=tk.DISABLED) - menu.add_command(label="OSLR", state=tk.DISABLED) - menu.add_command(label="OSLRv2", state=tk.DISABLED) + menu.add_command(label="Configure Adjacency", state=tk.DISABLED) + menu.add_command(label="Enable OSPFv2?", state=tk.DISABLED) + menu.add_command(label="Enable OSPFv3?", state=tk.DISABLED) + menu.add_command(label="Enable OSLR?", state=tk.DISABLED) + menu.add_command(label="Enable OSLRv2?", state=tk.DISABLED) widget_menu.add_cascade(label="Adjacency", menu=menu) + def create_throughput_menu(self, widget_menu: tk.Menu): + menu = tk.Menu(widget_menu) + menu.add_command( + label="Configure Throughput", command=self.menuaction.config_throughput + ) + menu.add_checkbutton( + label="Enable Throughput?", command=self.menuaction.throughput + ) + widget_menu.add_cascade(label="Throughput", menu=menu) + def draw_widgets_menu(self): """ Create widget menu @@ -388,12 +204,7 @@ class Menubar(tk.Menu): menu = tk.Menu(self) self.create_observer_widgets_menu(menu) self.create_adjacency_menu(menu) - menu.add_checkbutton(label="Throughput", command=self.menuaction.throughput) - menu.add_separator() - menu.add_command(label="Configure Adjacency...", state=tk.DISABLED) - menu.add_command( - label="Configure Throughput...", command=self.menuaction.config_throughput - ) + self.create_throughput_menu(menu) self.add_cascade(label="Widgets", menu=menu) def draw_session_menu(self): @@ -402,14 +213,11 @@ class Menubar(tk.Menu): """ menu = tk.Menu(self) menu.add_command( - label="Sessions...", command=self.menuaction.session_change_sessions + label="Sessions", command=self.menuaction.session_change_sessions ) - menu.add_separator() - menu.add_command(label="Options...", command=self.menuaction.session_options) - menu.add_command(label="Servers...", command=self.menuaction.session_servers) - menu.add_command(label="Hooks...", command=self.menuaction.session_hooks) - menu.add_command(label="Reset Nodes", state=tk.DISABLED) - menu.add_command(label="Comments...", state=tk.DISABLED) + menu.add_command(label="Servers", command=self.menuaction.session_servers) + menu.add_command(label="Options", command=self.menuaction.session_options) + menu.add_command(label="Hooks", command=self.menuaction.session_hooks) self.add_cascade(label="Session", menu=menu) def draw_help_menu(self): From c43afa4b403e25c2c9bcc98ce5b321f3886e3365 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 17 Apr 2020 23:28:45 -0700 Subject: [PATCH 0166/1131] pygui removed unwanted buttons from run toolbar --- daemon/core/gui/toolbar.py | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 3b4828d0..efd93b76 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -60,9 +60,7 @@ class Toolbar(ttk.Frame): # runtime buttons self.runtime_select_button = None self.stop_button = None - self.plot_button = None self.runtime_marker_button = None - self.node_command_button = None self.run_command_button = None # frames @@ -75,8 +73,8 @@ class Toolbar(ttk.Frame): # dialog self.marker_tool = None - # these variables help keep track of what images being drawn so that scaling is possible - # since ImageTk.PhotoImage does not have resize method + # these variables help keep track of what images being drawn so that scaling + # is possible since ImageTk.PhotoImage does not have resize method self.node_enum = None self.network_enum = None self.annotation_enum = None @@ -133,9 +131,7 @@ class Toolbar(ttk.Frame): logging.debug("selecting runtime button: %s", button) self.runtime_select_button.state(["!pressed"]) self.stop_button.state(["!pressed"]) - self.plot_button.state(["!pressed"]) self.runtime_marker_button.state(["!pressed"]) - self.node_command_button.state(["!pressed"]) self.run_command_button.state(["!pressed"]) button.state(["pressed"]) @@ -143,7 +139,6 @@ class Toolbar(ttk.Frame): self.runtime_frame = ttk.Frame(self) self.runtime_frame.grid(row=0, column=0, sticky="nsew") self.runtime_frame.columnconfigure(0, weight=1) - self.stop_button = self.create_button( self.runtime_frame, self.get_icon(ImageEnum.STOP), @@ -156,24 +151,12 @@ class Toolbar(ttk.Frame): self.click_runtime_selection, "selection tool", ) - self.plot_button = self.create_button( - self.runtime_frame, - self.get_icon(ImageEnum.PLOT), - self.click_plot_button, - "plot", - ) self.runtime_marker_button = self.create_button( self.runtime_frame, icon(ImageEnum.MARKER), self.click_marker_button, "marker", ) - self.node_command_button = self.create_button( - self.runtime_frame, - icon(ImageEnum.TWONODE), - self.click_two_node_button, - "run command from one node to another", - ) self.run_command_button = self.create_button( self.runtime_frame, icon(ImageEnum.RUN), self.click_run_button, "run" ) @@ -503,9 +486,6 @@ class Toolbar(ttk.Frame): def click_run_button(self): logging.debug("Click on RUN button") - def click_plot_button(self): - logging.debug("Click on plot button") - def click_marker_button(self): logging.debug("Click on marker button") self.runtime_select(self.runtime_marker_button) @@ -532,10 +512,7 @@ class Toolbar(ttk.Frame): self.scale_button(self.node_button, self.node_enum) self.scale_button(self.network_button, self.network_enum) self.scale_button(self.annotation_button, self.annotation_enum) - self.scale_button(self.runtime_select_button, ImageEnum.SELECT) self.scale_button(self.stop_button, ImageEnum.STOP) - self.scale_button(self.plot_button, ImageEnum.PLOT) self.scale_button(self.runtime_marker_button, ImageEnum.MARKER) - self.scale_button(self.node_command_button, ImageEnum.TWONODE) self.scale_button(self.run_command_button, ImageEnum.RUN) From 7da7ea5d627a3fb4334caedfb8e3a1b81f2de148 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 18 Apr 2020 00:33:22 -0700 Subject: [PATCH 0167/1131] pygui consolidated menubar and menuaction code into one file, small updates to observer widgets to avoid using ifconfig --- daemon/core/gui/app.py | 4 +- daemon/core/gui/coreclient.py | 19 ++- daemon/core/gui/menuaction.py | 203 ----------------------- daemon/core/gui/menubar.py | 295 +++++++++++++++++++++++++--------- daemon/core/gui/task.py | 3 +- daemon/core/gui/toolbar.py | 1 - 6 files changed, 231 insertions(+), 294 deletions(-) delete mode 100644 daemon/core/gui/menuaction.py diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 195f0e91..a7dd6549 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -6,7 +6,6 @@ from core.gui import appconfig, themes from core.gui.coreclient import CoreClient from core.gui.graph.graph import CanvasGraph from core.gui.images import ImageEnum, Images -from core.gui.menuaction import MenuAction from core.gui.menubar import Menubar from core.gui.nodeutils import NodeUtils from core.gui.statusbar import StatusBar @@ -104,8 +103,7 @@ class Application(tk.Frame): self.statusbar.pack(side=tk.BOTTOM, fill=tk.X) def on_closing(self): - menu_action = MenuAction(self, self.master) - menu_action.on_quit() + self.menubar.prompt_save_running_session(True) def save_config(self): appconfig.save(self.guiconfig) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 61350d05..76bbc424 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -32,15 +32,15 @@ if TYPE_CHECKING: GUI_SOURCE = "gui" OBSERVERS = { - "processes": "ps", - "ifconfig": "ifconfig", + "List Processes": "ps", + "Show Interfaces": "ip address", "IPV4 Routes": "ip -4 ro", "IPV6 Routes": "ip -6 ro", - "Listening sockets": "netstat -tuwnl", - "IPv4 MFC entries": "ip -4 mroute show", - "IPv6 MFC entries": "ip -6 mroute show", - "firewall rules": "iptables -L", - "IPSec policies": "setkey -DP", + "Listening Sockets": "netstat -tuwnl", + "IPv4 MFC Entries": "ip -4 mroute show", + "IPv6 MFC Entries": "ip -6 mroute show", + "Firewall Rules": "iptables -L", + "IPSec Policies": "setkey -DP", } @@ -100,10 +100,8 @@ class CoreClient: self.mobility_players = {} self.handling_throughputs = None self.handling_events = None - self.xml_dir = None self.xml_file = None - self.modified_service_nodes = set() @property @@ -454,7 +452,8 @@ class CoreClient: response = self.client.delete_session(session_id) logging.info("deleted session(%s), Result: %s", session_id, response) except grpc.RpcError as e: - # use the right master widget so the error dialog displays right on top of it + # use the right master widget so the error dialog displays + # right on top of it master = self.app if parent_frame: master = parent_frame diff --git a/daemon/core/gui/menuaction.py b/daemon/core/gui/menuaction.py deleted file mode 100644 index ac191a69..00000000 --- a/daemon/core/gui/menuaction.py +++ /dev/null @@ -1,203 +0,0 @@ -""" -The actions taken when each menubar option is clicked -""" - -import logging -import os -import tkinter as tk -import webbrowser -from tkinter import filedialog, messagebox -from typing import TYPE_CHECKING - -import grpc - -from core.gui.appconfig import XMLS_PATH -from core.gui.dialogs.about import AboutDialog -from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog -from core.gui.dialogs.canvaswallpaper import CanvasWallpaperDialog -from core.gui.dialogs.hooks import HooksDialog -from core.gui.dialogs.observers import ObserverDialog -from core.gui.dialogs.preferences import PreferencesDialog -from core.gui.dialogs.servers import ServersDialog -from core.gui.dialogs.sessionoptions import SessionOptionsDialog -from core.gui.dialogs.sessions import SessionsDialog -from core.gui.dialogs.throughput import ThroughputDialog -from core.gui.task import BackgroundTask - -MAX_FILES = 3 - -if TYPE_CHECKING: - from core.gui.app import Application - - -class MenuAction: - def __init__(self, app: "Application", master: tk.Tk): - self.master = master - self.app = app - self.canvas = app.canvas - - def cleanup_old_session(self, session_id: int): - try: - res = self.app.core.client.get_session(session_id) - logging.debug("retrieve session(%s), %s", session_id, res) - stop_response = self.app.core.stop_session() - logging.debug("stop session(%s), result: %s", session_id, stop_response) - delete_response = self.app.core.delete_session(session_id) - logging.debug( - "deleted session(%s), result: %s", session_id, delete_response - ) - except grpc.RpcError: - logging.debug("session is not alive") - - def prompt_save_running_session(self, quitapp: bool = False): - """ - Prompt use to stop running session before application is closed - """ - result = True - if self.app.core.is_runtime(): - result = messagebox.askyesnocancel("Exit", "Stop the running session?") - - if result: - callback = None - if quitapp: - callback = self.app.quit - task = BackgroundTask( - self.app, - self.cleanup_old_session, - callback, - (self.app.core.session_id,), - ) - task.start() - elif quitapp: - self.app.quit() - - def on_quit(self, event: tk.Event = None): - """ - Prompt user whether so save running session, and then close the application - """ - self.prompt_save_running_session(quitapp=True) - - def file_save_as_xml(self, event: tk.Event = None): - init_dir = self.app.core.xml_dir - if not init_dir: - init_dir = str(XMLS_PATH) - file_path = filedialog.asksaveasfilename( - initialdir=init_dir, - title="Save As", - filetypes=(("EmulationScript XML files", "*.xml"), ("All files", "*")), - defaultextension=".xml", - ) - if file_path: - self.add_recent_file_to_gui_config(file_path) - self.app.core.save_xml(file_path) - self.app.core.xml_file = file_path - - def file_open_xml(self, event: tk.Event = None): - init_dir = self.app.core.xml_dir - if not init_dir: - init_dir = str(XMLS_PATH) - file_path = filedialog.askopenfilename( - initialdir=init_dir, - title="Open", - filetypes=(("XML Files", "*.xml"), ("All Files", "*")), - ) - self.open_xml_task(file_path) - - def open_xml_task(self, filename): - if filename: - self.add_recent_file_to_gui_config(filename) - self.app.core.xml_file = filename - self.app.core.xml_dir = str(os.path.dirname(filename)) - self.prompt_save_running_session() - self.app.statusbar.progress_bar.start(5) - task = BackgroundTask(self.app, self.app.core.open_xml, args=(filename,)) - task.start() - - def gui_preferences(self): - dialog = PreferencesDialog(self.app, self.app) - dialog.show() - - def canvas_size_and_scale(self): - dialog = SizeAndScaleDialog(self.app, self.app) - dialog.show() - - def canvas_set_wallpaper(self): - dialog = CanvasWallpaperDialog(self.app, self.app) - dialog.show() - - def help_core_github(self): - webbrowser.open_new("https://github.com/coreemu/core") - - def help_core_documentation(self): - webbrowser.open_new("http://coreemu.github.io/core/") - - def session_options(self): - logging.debug("Click options") - dialog = SessionOptionsDialog(self.app, self.app) - if not dialog.has_error: - dialog.show() - - def session_change_sessions(self): - logging.debug("Click change sessions") - dialog = SessionsDialog(self.app, self.app) - dialog.show() - - def session_hooks(self): - logging.debug("Click hooks") - dialog = HooksDialog(self.app, self.app) - dialog.show() - - def session_servers(self): - logging.debug("Click emulation servers") - dialog = ServersDialog(self.app, self.app) - dialog.show() - - def edit_observer_widgets(self) -> None: - dialog = ObserverDialog(self.app, self.app) - dialog.show() - - def show_about(self) -> None: - dialog = AboutDialog(self.app, self.app) - dialog.show() - - def throughput(self) -> None: - if not self.app.core.handling_throughputs: - self.app.core.enable_throughputs() - else: - self.app.core.cancel_throughputs() - - def copy(self, event: tk.Event = None) -> None: - self.app.canvas.copy() - - def paste(self, event: tk.Event = None) -> None: - self.app.canvas.paste() - - def delete(self, event: tk.Event = None) -> None: - self.app.canvas.delete_selected_objects() - - def config_throughput(self) -> None: - dialog = ThroughputDialog(self.app, self.app) - dialog.show() - - def add_recent_file_to_gui_config(self, file_path) -> None: - recent_files = self.app.guiconfig["recentfiles"] - num_files = len(recent_files) - if num_files == 0: - recent_files.insert(0, file_path) - elif 0 < num_files <= MAX_FILES: - if file_path in recent_files: - recent_files.remove(file_path) - recent_files.insert(0, file_path) - else: - if num_files == MAX_FILES: - recent_files.pop() - recent_files.insert(0, file_path) - else: - logging.error("unexpected number of recent files") - self.app.save_config() - self.app.menubar.update_recent_files() - - def new_session(self): - self.prompt_save_running_session() - self.app.core.create_new_session() - self.app.core.xml_file = None diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 0aa22e0c..97977f83 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -1,35 +1,50 @@ import logging import os import tkinter as tk +import webbrowser from functools import partial +from tkinter import filedialog, messagebox from typing import TYPE_CHECKING -import core.gui.menuaction as action +from core.gui.appconfig import XMLS_PATH from core.gui.coreclient import OBSERVERS +from core.gui.dialogs.about import AboutDialog +from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog +from core.gui.dialogs.canvaswallpaper import CanvasWallpaperDialog from core.gui.dialogs.executepython import ExecutePythonDialog +from core.gui.dialogs.hooks import HooksDialog +from core.gui.dialogs.observers import ObserverDialog +from core.gui.dialogs.preferences import PreferencesDialog +from core.gui.dialogs.servers import ServersDialog +from core.gui.dialogs.sessionoptions import SessionOptionsDialog +from core.gui.dialogs.sessions import SessionsDialog +from core.gui.dialogs.throughput import ThroughputDialog +from core.gui.task import BackgroundTask if TYPE_CHECKING: from core.gui.app import Application +MAX_FILES = 3 + class Menubar(tk.Menu): """ Core menubar """ - def __init__(self, master: tk.Tk, app: "Application", cnf={}, **kwargs): + def __init__(self, master: tk.Tk, app: "Application", **kwargs) -> None: """ Create a CoreMenubar instance """ - super().__init__(master, cnf, **kwargs) + super().__init__(master, **kwargs) self.master.config(menu=self) self.app = app - self.menuaction = action.MenuAction(app, master) + self.core = app.core self.recent_menu = None self.edit_menu = None self.draw() - def draw(self): + def draw(self) -> None: """ Create core menubar and bind the hot keys to their matching command """ @@ -42,24 +57,22 @@ class Menubar(tk.Menu): self.draw_session_menu() self.draw_help_menu() - def draw_file_menu(self): + def draw_file_menu(self) -> None: """ Create file menu """ menu = tk.Menu(self) menu.add_command( - label="New Session", - accelerator="Ctrl+N", - command=self.menuaction.new_session, + label="New Session", accelerator="Ctrl+N", command=self.click_new ) - self.app.bind_all("", lambda e: self.app.core.create_new_session()) - menu.add_command(label="Save", accelerator="Ctrl+S", command=self.save) - self.app.bind_all("", self.save) - menu.add_command(label="Save As...", command=self.menuaction.file_save_as_xml) + self.app.bind_all("", lambda e: self.click_new()) + menu.add_command(label="Save", accelerator="Ctrl+S", command=self.click_save) + self.app.bind_all("", self.click_save) + menu.add_command(label="Save As...", command=self.click_save_xml) menu.add_command( - label="Open...", command=self.menuaction.file_open_xml, accelerator="Ctrl+O" + label="Open...", command=self.click_open_xml, accelerator="Ctrl+O" ) - self.app.bind_all("", self.menuaction.file_open_xml) + self.app.bind_all("", self.click_open_xml) self.recent_menu = tk.Menu(menu) for i in self.app.guiconfig["recentfiles"]: self.recent_menu.add_command( @@ -70,51 +83,47 @@ class Menubar(tk.Menu): menu.add_command(label="Execute Python Script...", command=self.execute_python) menu.add_separator() menu.add_command( - label="Quit", accelerator="Ctrl+Q", command=self.menuaction.on_quit + label="Quit", + accelerator="Ctrl+Q", + command=lambda: self.prompt_save_running_session(True), + ) + self.app.bind_all( + "", lambda _: self.prompt_save_running_session(True) ) - self.app.bind_all("", self.menuaction.on_quit) self.add_cascade(label="File", menu=menu) - def draw_edit_menu(self): + def draw_edit_menu(self) -> None: """ Create edit menu """ menu = tk.Menu(self) - menu.add_command(label="Preferences", command=self.menuaction.gui_preferences) + menu.add_command(label="Preferences", command=self.click_preferences) menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED) menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED) menu.add_separator() menu.add_command(label="Cut", accelerator="Ctrl+X", state=tk.DISABLED) + menu.add_command(label="Copy", accelerator="Ctrl+C", command=self.click_copy) + menu.add_command(label="Paste", accelerator="Ctrl+V", command=self.click_paste) menu.add_command( - label="Copy", accelerator="Ctrl+C", command=self.menuaction.copy - ) - menu.add_command( - label="Paste", accelerator="Ctrl+V", command=self.menuaction.paste - ) - menu.add_command( - label="Delete", accelerator="Ctrl+D", command=self.menuaction.delete + label="Delete", accelerator="Ctrl+D", command=self.click_delete ) self.add_cascade(label="Edit", menu=menu) - self.app.master.bind_all("", self.menuaction.copy) - self.app.master.bind_all("", self.menuaction.paste) - self.app.master.bind_all("", self.menuaction.delete) + self.app.master.bind_all("", self.click_copy) + self.app.master.bind_all("", self.click_paste) + self.app.master.bind_all("", self.click_delete) self.edit_menu = menu - def draw_canvas_menu(self): + def draw_canvas_menu(self) -> None: """ Create canvas menu """ menu = tk.Menu(self) - menu.add_command( - label="Size / Scale", command=self.menuaction.canvas_size_and_scale - ) - menu.add_command( - label="Wallpaper", command=self.menuaction.canvas_set_wallpaper - ) + menu.add_command(label="Size / Scale", command=self.click_canvas_size_and_scale) + menu.add_command(label="Wallpaper", command=self.click_canvas_wallpaper) self.add_cascade(label="Canvas", menu=menu) - def draw_view_menu(self): + def draw_view_menu(self) -> None: """ Create view menu """ @@ -130,7 +139,7 @@ class Menubar(tk.Menu): menu.add_command(label="Grid", state=tk.DISABLED) self.add_cascade(label="View", menu=menu) - def draw_tools_menu(self): + def draw_tools_menu(self) -> None: """ Create tools menu """ @@ -140,7 +149,7 @@ class Menubar(tk.Menu): menu.add_command(label="MAC Addresses", state=tk.DISABLED) self.add_cascade(label="Tools", menu=menu) - def create_observer_widgets_menu(self, widget_menu: tk.Menu): + def create_observer_widgets_menu(self, widget_menu: tk.Menu) -> None: """ Create observer widget menu item and create the sub menu items inside """ @@ -148,14 +157,14 @@ class Menubar(tk.Menu): menu = tk.Menu(widget_menu) menu.var = var menu.add_command( - label="Edit Observers", command=self.menuaction.edit_observer_widgets + label="Edit Observers", command=self.click_edit_observer_widgets ) menu.add_separator() menu.add_radiobutton( label="None", variable=var, value="none", - command=lambda: self.app.core.set_observer(None), + command=lambda: self.core.set_observer(None), ) for name in sorted(OBSERVERS): cmd = OBSERVERS[name] @@ -163,19 +172,19 @@ class Menubar(tk.Menu): label=name, variable=var, value=name, - command=partial(self.app.core.set_observer, cmd), + command=partial(self.core.set_observer, cmd), ) - for name in sorted(self.app.core.custom_observers): - observer = self.app.core.custom_observers[name] + for name in sorted(self.core.custom_observers): + observer = self.core.custom_observers[name] menu.add_radiobutton( label=name, variable=var, value=name, - command=partial(self.app.core.set_observer, observer.cmd), + command=partial(self.core.set_observer, observer.cmd), ) widget_menu.add_cascade(label="Observer Widgets", menu=menu) - def create_adjacency_menu(self, widget_menu: tk.Menu): + def create_adjacency_menu(self, widget_menu: tk.Menu) -> None: """ Create adjacency menu item and the sub menu items inside """ @@ -187,17 +196,15 @@ class Menubar(tk.Menu): menu.add_command(label="Enable OSLRv2?", state=tk.DISABLED) widget_menu.add_cascade(label="Adjacency", menu=menu) - def create_throughput_menu(self, widget_menu: tk.Menu): + def create_throughput_menu(self, widget_menu: tk.Menu) -> None: menu = tk.Menu(widget_menu) menu.add_command( - label="Configure Throughput", command=self.menuaction.config_throughput - ) - menu.add_checkbutton( - label="Enable Throughput?", command=self.menuaction.throughput + label="Configure Throughput", command=self.click_config_throughput ) + menu.add_checkbutton(label="Enable Throughput?", command=self.click_throughput) widget_menu.add_cascade(label="Throughput", menu=menu) - def draw_widgets_menu(self): + def draw_widgets_menu(self) -> None: """ Create widget menu """ @@ -207,60 +214,107 @@ class Menubar(tk.Menu): self.create_throughput_menu(menu) self.add_cascade(label="Widgets", menu=menu) - def draw_session_menu(self): + def draw_session_menu(self) -> None: """ Create session menu """ menu = tk.Menu(self) - menu.add_command( - label="Sessions", command=self.menuaction.session_change_sessions - ) - menu.add_command(label="Servers", command=self.menuaction.session_servers) - menu.add_command(label="Options", command=self.menuaction.session_options) - menu.add_command(label="Hooks", command=self.menuaction.session_hooks) + menu.add_command(label="Sessions", command=self.click_sessions) + menu.add_command(label="Servers", command=self.click_servers) + menu.add_command(label="Options", command=self.click_session_options) + menu.add_command(label="Hooks", command=self.click_hooks) self.add_cascade(label="Session", menu=menu) - def draw_help_menu(self): + def draw_help_menu(self) -> None: """ Create help menu """ menu = tk.Menu(self) - menu.add_command( - label="Core GitHub (www)", command=self.menuaction.help_core_github - ) - menu.add_command( - label="Core Documentation (www)", - command=self.menuaction.help_core_documentation, - ) - menu.add_command(label="About", command=self.menuaction.show_about) + menu.add_command(label="Core GitHub (www)", command=self.click_core_github) + menu.add_command(label="Core Documentation (www)", command=self.click_core_doc) + menu.add_command(label="About", command=self.click_about) self.add_cascade(label="Help", menu=menu) - def open_recent_files(self, filename: str): + def open_recent_files(self, filename: str) -> None: if os.path.isfile(filename): logging.debug("Open recent file %s", filename) - self.menuaction.open_xml_task(filename) + self.open_xml_task(filename) else: logging.warning("File does not exist %s", filename) - def update_recent_files(self): + def update_recent_files(self) -> None: self.recent_menu.delete(0, tk.END) for i in self.app.guiconfig["recentfiles"]: self.recent_menu.add_command( label=i, command=partial(self.open_recent_files, i) ) - def save(self, event=None): - xml_file = self.app.core.xml_file + def click_save(self, _event=None) -> None: + xml_file = self.core.xml_file if xml_file: - self.app.core.save_xml(xml_file) + self.core.save_xml(xml_file) else: - self.menuaction.file_save_as_xml() + self.click_save_xml() + + def click_save_xml(self, _event: tk.Event = None) -> None: + init_dir = self.core.xml_dir + if not init_dir: + init_dir = str(XMLS_PATH) + file_path = filedialog.asksaveasfilename( + initialdir=init_dir, + title="Save As", + filetypes=(("XML files", "*.xml"), ("All files", "*")), + defaultextension=".xml", + ) + if file_path: + self.add_recent_file_to_gui_config(file_path) + self.core.save_xml(file_path) + self.core.xml_file = file_path + + def click_open_xml(self, _event: tk.Event = None) -> None: + init_dir = self.core.xml_dir + if not init_dir: + init_dir = str(XMLS_PATH) + file_path = filedialog.askopenfilename( + initialdir=init_dir, + title="Open", + filetypes=(("XML Files", "*.xml"), ("All Files", "*")), + ) + if file_path: + self.open_xml_task(file_path) + + def open_xml_task(self, filename: str) -> None: + self.add_recent_file_to_gui_config(filename) + self.core.xml_file = filename + self.core.xml_dir = str(os.path.dirname(filename)) + self.prompt_save_running_session() + self.app.statusbar.progress_bar.start(5) + task = BackgroundTask(self.app, self.core.open_xml, args=(filename,)) + task.start() def execute_python(self): dialog = ExecutePythonDialog(self.app, self.app) dialog.show() - def change_menubar_item_state(self, is_runtime: bool): + def add_recent_file_to_gui_config(self, file_path) -> None: + recent_files = self.app.guiconfig["recentfiles"] + num_files = len(recent_files) + if num_files == 0: + recent_files.insert(0, file_path) + elif 0 < num_files <= MAX_FILES: + if file_path in recent_files: + recent_files.remove(file_path) + recent_files.insert(0, file_path) + else: + if num_files == MAX_FILES: + recent_files.pop() + recent_files.insert(0, file_path) + else: + logging.error("unexpected number of recent files") + self.app.save_config() + self.app.menubar.update_recent_files() + + def change_menubar_item_state(self, is_runtime: bool) -> None: for i in range(self.edit_menu.index("end")): try: label_name = self.edit_menu.entrycget(i, "label") @@ -271,3 +325,92 @@ class Menubar(tk.Menu): self.edit_menu.entryconfig(i, state="normal") except tk.TclError: logging.debug("Ignore separators") + + def prompt_save_running_session(self, quit_app: bool = False) -> None: + """ + Prompt use to stop running session before application is closed + + :param quit_app: True to quit app, False otherwise + """ + result = True + if self.core.is_runtime(): + result = messagebox.askyesnocancel("Exit", "Stop the running session?") + if result: + callback = None + if quit_app: + callback = self.app.quit + task = BackgroundTask(self.app, self.core.delete_session, callback) + task.start() + elif quit_app: + self.app.quit() + + def click_new(self) -> None: + self.prompt_save_running_session() + self.core.create_new_session() + self.core.xml_file = None + + def click_preferences(self) -> None: + dialog = PreferencesDialog(self.app, self.app) + dialog.show() + + def click_canvas_size_and_scale(self) -> None: + dialog = SizeAndScaleDialog(self.app, self.app) + dialog.show() + + def click_canvas_wallpaper(self) -> None: + dialog = CanvasWallpaperDialog(self.app, self.app) + dialog.show() + + def click_core_github(self) -> None: + webbrowser.open_new("https://github.com/coreemu/core") + + def click_core_doc(self) -> None: + webbrowser.open_new("http://coreemu.github.io/core/") + + def click_about(self) -> None: + dialog = AboutDialog(self.app, self.app) + dialog.show() + + def click_throughput(self) -> None: + if not self.core.handling_throughputs: + self.core.enable_throughputs() + else: + self.core.cancel_throughputs() + + def click_config_throughput(self) -> None: + dialog = ThroughputDialog(self.app, self.app) + dialog.show() + + def click_copy(self, _event: tk.Event = None) -> None: + self.app.canvas.copy() + + def click_paste(self, _event: tk.Event = None) -> None: + self.app.canvas.paste() + + def click_delete(self, _event: tk.Event = None) -> None: + self.app.canvas.delete_selected_objects() + + def click_session_options(self) -> None: + logging.debug("Click options") + dialog = SessionOptionsDialog(self.app, self.app) + if not dialog.has_error: + dialog.show() + + def click_sessions(self) -> None: + logging.debug("Click change sessions") + dialog = SessionsDialog(self.app, self.app) + dialog.show() + + def click_hooks(self) -> None: + logging.debug("Click hooks") + dialog = HooksDialog(self.app, self.app) + dialog.show() + + def click_servers(self) -> None: + logging.debug("Click emulation servers") + dialog = ServersDialog(self.app, self.app) + dialog.show() + + def click_edit_observer_widgets(self) -> None: + dialog = ObserverDialog(self.app, self.app) + dialog.show() diff --git a/daemon/core/gui/task.py b/daemon/core/gui/task.py index eb6655f8..bee69be3 100644 --- a/daemon/core/gui/task.py +++ b/daemon/core/gui/task.py @@ -21,7 +21,8 @@ class BackgroundTask: def run(self): result = self.task(*self.args) logging.info("task completed") - # if start session fails, a response with Result: False and a list of exceptions is returned + # if start session fails, a response with Result: False and a list of + # exceptions is returned if not getattr(result, "result", True): if len(getattr(result, "exceptions", [])) > 0: self.master.after( diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index efd93b76..d6707fb2 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -499,7 +499,6 @@ class Toolbar(ttk.Frame): def click_two_node_button(self): logging.debug("Click TWONODE button") - # def scale_button(cls, button, image_enum, scale): def scale_button(self, button, image_enum): image = icon(image_enum, int(TOOLBAR_SIZE * self.app.app_scale)) button.config(image=image) From d659a5c1396d7b2e3fd4d683e95f6d3dc1e28fb8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 18 Apr 2020 08:11:12 -0700 Subject: [PATCH 0168/1131] python examples, removed params common local module to avoid confusion, clean things up a bit and added a module doc to help explain the file --- daemon/examples/python/emane80211.py | 29 +++++++++--------- daemon/examples/python/params.py | 32 -------------------- daemon/examples/python/switch.py | 39 +++++++++++-------------- daemon/examples/python/switch_inject.py | 15 ++++++++-- daemon/examples/python/wlan.py | 35 ++++++++++------------ 5 files changed, 57 insertions(+), 93 deletions(-) delete mode 100644 daemon/examples/python/params.py diff --git a/daemon/examples/python/emane80211.py b/daemon/examples/python/emane80211.py index 3a10321b..27ae494b 100644 --- a/daemon/examples/python/emane80211.py +++ b/daemon/examples/python/emane80211.py @@ -1,14 +1,20 @@ -import datetime +""" +This is a standalone script to run a small EMANE scenario and will not interact +with the GUI. You also must have installed OSPF MDR as noted in the documentation +installation page. +""" + import logging -import parser from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes, NodeTypes +NODES = 2 -def example(args): + +def main(): # ip generator for example prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") @@ -19,7 +25,8 @@ def example(args): # must be in configuration state for nodes to start, when using "node_add" below session.set_state(EventTypes.CONFIGURATION_STATE) - # create emane network node + # create emane network node, emane determines connectivity based on + # location, so the session and nodes must be configured to provide one session.set_location(47.57917, -122.13232, 2.00000, 1.0) options = NodeOptions() options.set_position(80, 50) @@ -28,7 +35,7 @@ def example(args): # create nodes options = NodeOptions(model="mdr") - for i in range(args.nodes): + for i in range(NODES): node = session.add_node(options=options) node.setposition(x=150 * (i + 1), y=150) interface = prefixes.create_interface(node) @@ -42,16 +49,6 @@ def example(args): coreemu.shutdown() -def main(): - logging.basicConfig(level=logging.INFO) - args = parser.parse("emane80211") - start = datetime.datetime.now() - logging.info( - "running emane 80211 example: nodes(%s) time(%s)", args.nodes, args.time - ) - example(args) - logging.info("elapsed time: %s", datetime.datetime.now() - start) - - if __name__ == "__main__" or __name__ == "__builtin__": + logging.basicConfig(level=logging.INFO) main() diff --git a/daemon/examples/python/params.py b/daemon/examples/python/params.py deleted file mode 100644 index 1fc64ca9..00000000 --- a/daemon/examples/python/params.py +++ /dev/null @@ -1,32 +0,0 @@ -import argparse - -DEFAULT_NODES = 2 -DEFAULT_TIME = 10 -DEFAULT_STEP = 1 - - -def parse(name): - parser = argparse.ArgumentParser(description=f"Run {name} example") - parser.add_argument( - "-n", - "--nodes", - type=int, - default=DEFAULT_NODES, - help="number of nodes to create in this example", - ) - parser.add_argument( - "-c", - "--count", - type=int, - default=DEFAULT_TIME, - help="number of time to ping node", - ) - - args = parser.parse_args() - - if args.nodes < 2: - parser.error(f"invalid min number of nodes: {args.nodes}") - if args.count < 1: - parser.error(f"invalid ping count({args.count}), count must be greater than 0") - - return args diff --git a/daemon/examples/python/switch.py b/daemon/examples/python/switch.py index afb7c472..b4903457 100644 --- a/daemon/examples/python/switch.py +++ b/daemon/examples/python/switch.py @@ -1,13 +1,18 @@ -import logging -import time +""" +This is a standalone script to run a small switch based scenario and will not +interact with the GUI. +""" + +import logging -import params from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes from core.emulator.enumerations import EventTypes, NodeTypes +NODES = 2 -def example(args): + +def main(): # ip generator for example prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") @@ -19,10 +24,10 @@ def example(args): session.set_state(EventTypes.CONFIGURATION_STATE) # create switch network node - switch = session.add_node(_type=NodeTypes.SWITCH) + switch = session.add_node(_type=NodeTypes.SWITCH, _id=100) # create nodes - for _ in range(args.nodes): + for _ in range(NODES): node = session.add_node() interface = prefixes.create_interface(node) session.add_link(node.id, switch.id, interface_one=interface) @@ -31,27 +36,17 @@ def example(args): session.instantiate() # get nodes to run example - first_node = session.get_node(2) - last_node = session.get_node(args.nodes + 1) - first_node_address = prefixes.ip4_address(first_node) - logging.info("node %s pinging %s", last_node.name, first_node_address) - output = last_node.cmd(f"ping -c {args.count} {first_node_address}") + first_node = session.get_node(1) + last_node = session.get_node(NODES) + address = prefixes.ip4_address(first_node) + logging.info("node %s pinging %s", last_node.name, address) + output = last_node.cmd(f"ping -c 3 {address}") logging.info(output) # shutdown session coreemu.shutdown() -def main(): - logging.basicConfig(level=logging.INFO) - args = params.parse("switch") - start = time.perf_counter() - logging.info( - "running switch example: nodes(%s) ping count(%s)", args.nodes, args.count - ) - example(args) - logging.info("elapsed time: %s", time.perf_counter() - start) - - if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) main() diff --git a/daemon/examples/python/switch_inject.py b/daemon/examples/python/switch_inject.py index 0a87afd2..e85880e6 100644 --- a/daemon/examples/python/switch_inject.py +++ b/daemon/examples/python/switch_inject.py @@ -1,10 +1,19 @@ +""" +This is a script to run a small switch based scenario and depends on +the user running this script through the "Execute Python Script" option +in the GUI. The usage of globals() below allows this script to leverage the +same CoreEmu instance the GUI is using. +""" + import logging from core.emulator.emudata import IpPrefixes from core.emulator.enumerations import EventTypes, NodeTypes +NODES = 2 -def example(nodes): + +def main(): # ip generator for example prefixes = IpPrefixes("10.83.0.0/16") @@ -19,7 +28,7 @@ def example(nodes): switch = session.add_node(_type=NodeTypes.SWITCH) # create nodes - for _ in range(nodes): + for _ in range(NODES): node = session.add_node() interface = prefixes.create_interface(node) session.add_link(node.id, switch.id, interface_one=interface) @@ -30,4 +39,4 @@ def example(nodes): if __name__ in {"__main__", "__builtin__"}: logging.basicConfig(level=logging.INFO) - example(2) + main() diff --git a/daemon/examples/python/wlan.py b/daemon/examples/python/wlan.py index f601dced..e9ae47f4 100644 --- a/daemon/examples/python/wlan.py +++ b/daemon/examples/python/wlan.py @@ -1,14 +1,19 @@ -import logging -import time +""" +This is a standalone script to run a small WLAN based scenario and will not +interact with the GUI. +""" + +import logging -import params from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes, NodeTypes from core.location.mobility import BasicRangeModel +NODES = 2 -def example(args): + +def main(): # ip generator for example prefixes = IpPrefixes("10.83.0.0/16") @@ -20,13 +25,13 @@ def example(args): session.set_state(EventTypes.CONFIGURATION_STATE) # create wlan network node - wlan = session.add_node(_type=NodeTypes.WIRELESS_LAN) + wlan = session.add_node(_type=NodeTypes.WIRELESS_LAN, _id=100) session.mobility.set_model(wlan, BasicRangeModel) # create nodes, must set a position for wlan basic range model options = NodeOptions(model="mdr") options.set_position(0, 0) - for _ in range(args.nodes): + for _ in range(NODES): node = session.add_node(options=options) interface = prefixes.create_interface(node) session.add_link(node.id, wlan.id, interface_one=interface) @@ -35,27 +40,17 @@ def example(args): session.instantiate() # get nodes for example run - first_node = session.get_node(2) - last_node = session.get_node(args.nodes + 1) + first_node = session.get_node(1) + last_node = session.get_node(NODES) address = prefixes.ip4_address(first_node) logging.info("node %s pinging %s", last_node.name, address) - output = last_node.cmd(f"ping -c {args.count} {address}") + output = last_node.cmd(f"ping -c 3 {address}") logging.info(output) # shutdown session coreemu.shutdown() -def main(): - logging.basicConfig(level=logging.INFO) - args = params.parse("wlan") - start = time.perf_counter() - logging.info( - "running wlan example: nodes(%s) ping count(%s)", args.nodes, args.count - ) - example(args) - logging.info("elapsed time: %s", time.perf_counter() - start) - - if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) main() From d1f7eafc570fd50a7718a3d7029360488eee0bc1 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 18 Apr 2020 08:18:29 -0700 Subject: [PATCH 0169/1131] fixed emane python example and changed it to be a simple ping example --- daemon/examples/python/emane80211.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/daemon/examples/python/emane80211.py b/daemon/examples/python/emane80211.py index 27ae494b..e9764a09 100644 --- a/daemon/examples/python/emane80211.py +++ b/daemon/examples/python/emane80211.py @@ -5,6 +5,7 @@ installation page. """ import logging +import time from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emulator.coreemu import CoreEmu @@ -12,6 +13,7 @@ from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes, NodeTypes NODES = 2 +EMANE_DELAY = 10 def main(): @@ -30,7 +32,7 @@ def main(): session.set_location(47.57917, -122.13232, 2.00000, 1.0) options = NodeOptions() options.set_position(80, 50) - emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) + emane_network = session.add_node(_type=NodeTypes.EMANE, options=options, _id=100) session.emane.set_model(emane_network, EmaneIeee80211abgModel) # create nodes @@ -44,8 +46,19 @@ def main(): # instantiate session session.instantiate() + # OSPF MDR requires some time for routes to be created + logging.info("waiting %s seconds for OSPF MDR to create routes", EMANE_DELAY) + time.sleep(EMANE_DELAY) + + # get nodes to run example + first_node = session.get_node(1) + last_node = session.get_node(NODES) + address = prefixes.ip4_address(first_node) + logging.info("node %s pinging %s", last_node.name, address) + output = last_node.cmd(f"ping -c 3 {address}") + logging.info(output) + # shutdown session - input("press enter to exit...") coreemu.shutdown() From be332a2a29e54da9bdb96ecd270b66dd9652b22d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 18 Apr 2020 08:24:26 -0700 Subject: [PATCH 0170/1131] updated all distributed examples to remove usage of common local module to avoid confusion, even if duplicate code --- daemon/examples/python/distributed_emane.py | 23 ++++++++++++++++++-- daemon/examples/python/distributed_lxd.py | 23 ++++++++++++++++++-- daemon/examples/python/distributed_parser.py | 15 ------------- daemon/examples/python/distributed_ptp.py | 23 ++++++++++++++++++-- daemon/examples/python/distributed_switch.py | 23 ++++++++++++++++++-- 5 files changed, 84 insertions(+), 23 deletions(-) delete mode 100644 daemon/examples/python/distributed_parser.py diff --git a/daemon/examples/python/distributed_emane.py b/daemon/examples/python/distributed_emane.py index 6b61e505..4b748803 100644 --- a/daemon/examples/python/distributed_emane.py +++ b/daemon/examples/python/distributed_emane.py @@ -1,12 +1,31 @@ +""" +Example for scripting a standalone distributed EMANE session that does not interact +with the GUI. +""" + +import argparse import logging -import distributed_parser from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes, NodeTypes +def parse(name): + parser = argparse.ArgumentParser(description=f"Run {name} example") + parser.add_argument( + "-a", + "--address", + help="local address that distributed servers will use for gre tunneling", + ) + parser.add_argument( + "-s", "--server", help="distributed server to use for creating nodes" + ) + options = parser.parse_args() + return options + + def main(args): # ip generator for example prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") @@ -55,5 +74,5 @@ def main(args): if __name__ == "__main__": logging.basicConfig(level=logging.INFO) - args = distributed_parser.parse(__file__) + args = parse(__file__) main(args) diff --git a/daemon/examples/python/distributed_lxd.py b/daemon/examples/python/distributed_lxd.py index 73d24b5a..8d46d599 100644 --- a/daemon/examples/python/distributed_lxd.py +++ b/daemon/examples/python/distributed_lxd.py @@ -1,11 +1,30 @@ +""" +Example for scripting a standalone distributed LXD session that does not interact +with the GUI. +""" + +import argparse import logging -import distributed_parser from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes, NodeTypes +def parse(name): + parser = argparse.ArgumentParser(description=f"Run {name} example") + parser.add_argument( + "-a", + "--address", + help="local address that distributed servers will use for gre tunneling", + ) + parser.add_argument( + "-s", "--server", help="distributed server to use for creating nodes" + ) + options = parser.parse_args() + return options + + def main(args): # ip generator for example prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") @@ -44,5 +63,5 @@ def main(args): if __name__ == "__main__": logging.basicConfig(level=logging.INFO) - args = distributed_parser.parse(__file__) + args = parse(__file__) main(args) diff --git a/daemon/examples/python/distributed_parser.py b/daemon/examples/python/distributed_parser.py deleted file mode 100644 index 5557efd8..00000000 --- a/daemon/examples/python/distributed_parser.py +++ /dev/null @@ -1,15 +0,0 @@ -import argparse - - -def parse(name): - parser = argparse.ArgumentParser(description=f"Run {name} example") - parser.add_argument( - "-a", - "--address", - help="local address that distributed servers will use for gre tunneling", - ) - parser.add_argument( - "-s", "--server", help="distributed server to use for creating nodes" - ) - options = parser.parse_args() - return options diff --git a/daemon/examples/python/distributed_ptp.py b/daemon/examples/python/distributed_ptp.py index 0138dab3..85069603 100644 --- a/daemon/examples/python/distributed_ptp.py +++ b/daemon/examples/python/distributed_ptp.py @@ -1,11 +1,30 @@ +""" +Example for scripting a standalone distributed peer to peer session that does not +interact with the GUI. +""" + +import argparse import logging -import distributed_parser from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes +def parse(name): + parser = argparse.ArgumentParser(description=f"Run {name} example") + parser.add_argument( + "-a", + "--address", + help="local address that distributed servers will use for gre tunneling", + ) + parser.add_argument( + "-s", "--server", help="distributed server to use for creating nodes" + ) + options = parser.parse_args() + return options + + def main(args): # ip generator for example prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") @@ -44,5 +63,5 @@ def main(args): if __name__ == "__main__": logging.basicConfig(level=logging.INFO) - args = distributed_parser.parse(__file__) + args = parse(__file__) main(args) diff --git a/daemon/examples/python/distributed_switch.py b/daemon/examples/python/distributed_switch.py index f659abd2..57c6141b 100644 --- a/daemon/examples/python/distributed_switch.py +++ b/daemon/examples/python/distributed_switch.py @@ -1,11 +1,30 @@ +""" +Example for scripting a standalone distributed switch session that does not +interact with the GUI. +""" + +import argparse import logging -import distributed_parser from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes, NodeTypes +def parse(name): + parser = argparse.ArgumentParser(description=f"Run {name} example") + parser.add_argument( + "-a", + "--address", + help="local address that distributed servers will use for gre tunneling", + ) + parser.add_argument( + "-s", "--server", help="distributed server to use for creating nodes" + ) + options = parser.parse_args() + return options + + def main(args): # ip generator for example prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") @@ -48,5 +67,5 @@ def main(args): if __name__ == "__main__": logging.basicConfig(level=logging.INFO) - args = distributed_parser.parse(__file__) + args = parse(__file__) main(args) From f45a11076f563dc1c7d4cb2ecbfee43688886023 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 18 Apr 2020 09:02:15 -0700 Subject: [PATCH 0171/1131] pygui implemented auto grid layout, to auto distance node icons with padding based on canvas size in rows and columns --- daemon/core/gui/images.py | 3 ++- daemon/core/gui/menubar.py | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/daemon/core/gui/images.py b/daemon/core/gui/images.py index 1f43103c..3a953054 100644 --- a/daemon/core/gui/images.py +++ b/daemon/core/gui/images.py @@ -47,7 +47,8 @@ class Images: except KeyError: messagebox.showwarning( "Missing image file", - f"{name}.png is missing at daemon/core/gui/data/icons, drop image file at daemon/core/gui/data/icons and restart the gui", + f"{name}.png is missing at daemon/core/gui/data/icons, drop image " + f"file at daemon/core/gui/data/icons and restart the gui", ) diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 97977f83..08056071 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -19,6 +19,7 @@ from core.gui.dialogs.servers import ServersDialog from core.gui.dialogs.sessionoptions import SessionOptionsDialog from core.gui.dialogs.sessions import SessionsDialog from core.gui.dialogs.throughput import ThroughputDialog +from core.gui.nodeutils import ICON_SIZE from core.gui.task import BackgroundTask if TYPE_CHECKING: @@ -144,7 +145,7 @@ class Menubar(tk.Menu): Create tools menu """ menu = tk.Menu(self) - menu.add_command(label="Auto Grid", state=tk.DISABLED) + menu.add_command(label="Auto Grid", command=self.click_autogrid) menu.add_command(label="IP Addresses", state=tk.DISABLED) menu.add_command(label="MAC Addresses", state=tk.DISABLED) self.add_cascade(label="Tools", menu=menu) @@ -414,3 +415,18 @@ class Menubar(tk.Menu): def click_edit_observer_widgets(self) -> None: dialog = ObserverDialog(self.app, self.app) dialog.show() + + def click_autogrid(self) -> None: + width, height = self.app.canvas.current_dimensions + padding = (ICON_SIZE / 2) + 10 + layout_size = padding + ICON_SIZE + col_count = width // layout_size + logging.info( + "auto grid layout: dimens(%s, %s) col(%s)", width, height, col_count + ) + for i, node in enumerate(self.app.canvas.nodes.values()): + col = i % col_count + row = i // col_count + x = (col * layout_size) + padding + y = (row * layout_size) + padding + node.move(x, y) From d26c4fc4ab1b75660a2870551197ed96a88fca51 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 19 Apr 2020 15:47:07 -0700 Subject: [PATCH 0172/1131] pygui initial implementation for supporting the view menu for showing and hiding canvas elements --- daemon/core/gui/app.py | 2 +- daemon/core/gui/coreclient.py | 5 -- daemon/core/gui/dialogs/canvaswallpaper.py | 12 +---- daemon/core/gui/graph/edges.py | 38 +++++++++----- daemon/core/gui/graph/graph.py | 39 ++++++++++---- daemon/core/gui/graph/node.py | 5 +- daemon/core/gui/graph/shape.py | 4 ++ daemon/core/gui/graph/tags.py | 14 ++--- daemon/core/gui/menubar.py | 61 ++++++++++++++++++---- 9 files changed, 119 insertions(+), 61 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index a7dd6549..0f40a594 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -78,11 +78,11 @@ class Application(tk.Frame): def draw(self): self.master.option_add("*tearOff", tk.FALSE) - self.menubar = Menubar(self.master, self) self.toolbar = Toolbar(self, self) self.toolbar.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) self.draw_canvas() self.draw_status() + self.menubar = Menubar(self.master, self) def draw_canvas(self): width = self.guiconfig["preferences"]["width"] diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 76bbc424..b8914808 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -369,21 +369,16 @@ class CoreClient: logging.debug("canvas metadata: %s", canvas_config) if canvas_config: canvas_config = json.loads(canvas_config) - gridlines = canvas_config.get("gridlines", True) self.app.canvas.show_grid.set(gridlines) - fit_image = canvas_config.get("fit_image", False) self.app.canvas.adjust_to_dim.set(fit_image) - wallpaper_style = canvas_config.get("wallpaper-style", 1) self.app.canvas.scale_option.set(wallpaper_style) - width = self.app.guiconfig["preferences"]["width"] height = self.app.guiconfig["preferences"]["height"] dimensions = canvas_config.get("dimensions", [width, height]) self.app.canvas.redraw_canvas(dimensions) - wallpaper = canvas_config.get("wallpaper") if wallpaper: wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) diff --git a/daemon/core/gui/dialogs/canvaswallpaper.py b/daemon/core/gui/dialogs/canvaswallpaper.py index 093d93b0..fe3fbd79 100644 --- a/daemon/core/gui/dialogs/canvaswallpaper.py +++ b/daemon/core/gui/dialogs/canvaswallpaper.py @@ -24,7 +24,6 @@ class CanvasWallpaperDialog(Dialog): super().__init__(master, app, "Canvas Background", modal=True) self.canvas = self.app.canvas self.scale_option = tk.IntVar(value=self.canvas.scale_option.get()) - self.show_grid = tk.BooleanVar(value=self.canvas.show_grid.get()) self.adjust_to_dim = tk.BooleanVar(value=self.canvas.adjust_to_dim.get()) self.filename = tk.StringVar(value=self.canvas.wallpaper_file) self.image_label = None @@ -103,11 +102,6 @@ class CanvasWallpaperDialog(Dialog): self.options.append(button) def draw_additional_options(self): - checkbutton = ttk.Checkbutton( - self.top, text="Show grid", variable=self.show_grid - ) - checkbutton.grid(sticky="ew", padx=PADX) - checkbutton = ttk.Checkbutton( self.top, text="Adjust canvas size to image dimensions", @@ -163,17 +157,13 @@ class CanvasWallpaperDialog(Dialog): def click_apply(self): self.canvas.scale_option.set(self.scale_option.get()) - self.canvas.show_grid.set(self.show_grid.get()) self.canvas.adjust_to_dim.set(self.adjust_to_dim.get()) - self.canvas.update_grid() - + self.canvas.show_grid.click_handler() filename = self.filename.get() if not filename: filename = None - try: self.canvas.set_wallpaper(filename) except FileNotFoundError: logging.error("invalid background: %s", filename) - self.destroy() diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 1606da88..b70fe6b2 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -20,15 +20,6 @@ WIRELESS_COLOR = "#009933" ARC_DISTANCE = 50 -def interface_label(interface: core_pb2.Interface) -> str: - label = "" - if interface.ip4: - label = f"{interface.ip4}/{interface.ip4mask}" - if interface.ip6: - label = f"{label}\n{interface.ip6}/{interface.ip6mask}" - return label - - def create_edge_token(src: int, dst: int, network: int = None) -> Tuple[int, ...]: values = [src, dst] if network is not None: @@ -143,7 +134,12 @@ class Edge: if self.middle_label is None: x, y = self.middle_label_pos() self.middle_label = self.canvas.create_text( - x, y, font=self.canvas.app.edge_font, text=text + x, + y, + font=self.canvas.app.edge_font, + text=text, + tags=tags.LINK_LABEL, + state=self.canvas.show_link_labels.state(), ) else: self.canvas.itemconfig(self.middle_label, text=text) @@ -168,7 +164,8 @@ class Edge: text=text, justify=tk.CENTER, font=self.canvas.app.edge_font, - tags=tags.LINK_INFO, + tags=tags.LINK_LABEL, + state=self.canvas.show_link_labels.state(), ) else: self.canvas.itemconfig(self.src_label, text=text) @@ -181,7 +178,8 @@ class Edge: text=text, justify=tk.CENTER, font=self.canvas.app.edge_font, - tags=tags.LINK_INFO, + tags=tags.LINK_LABEL, + state=self.canvas.show_link_labels.state(), ) else: self.canvas.itemconfig(self.dst_label, text=text) @@ -278,13 +276,25 @@ class CanvasEdge(Edge): self.link = link self.draw_labels() + def interface_label(self, interface: core_pb2.Interface) -> str: + label = "" + if interface.name and self.canvas.show_interface_names.get(): + label = f"{interface.name}" + if interface.ip4 and self.canvas.show_ip4s.get(): + label = f"{label}\n" if label else "" + label += f"{interface.ip4}/{interface.ip4mask}" + if interface.ip6 and self.canvas.show_ip6s.get(): + label = f"{label}\n" if label else "" + label += f"{interface.ip6}/{interface.ip6mask}" + return label + def create_node_labels(self) -> Tuple[str, str]: label_one = None if self.link.HasField("interface_one"): - label_one = interface_label(self.link.interface_one) + label_one = self.interface_label(self.link.interface_one) label_two = None if self.link.HasField("interface_two"): - label_two = interface_label(self.link.interface_two) + label_two = self.interface_label(self.link.interface_two) return label_one, label_two def draw_labels(self) -> None: diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 19fde443..3b1efaea 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -1,5 +1,6 @@ import logging import tkinter as tk +from tkinter import BooleanVar from typing import TYPE_CHECKING, Tuple from PIL import Image, ImageTk @@ -30,6 +31,19 @@ ZOOM_OUT = 0.9 ICON_SIZE = 48 +class ShowVar(BooleanVar): + def __init__(self, canvas: "CanvasGraph", tag: str, value: bool) -> None: + super().__init__(value=value) + self.canvas = canvas + self.tag = tag + + def state(self) -> str: + return tk.NORMAL if self.get() else tk.HIDDEN + + def click_handler(self): + self.canvas.itemconfigure(self.tag, state=self.state()) + + class CanvasGraph(tk.Canvas): def __init__( self, master: "Application", core: "CoreClient", width: int, height: int @@ -69,7 +83,6 @@ class CanvasGraph(tk.Canvas): self.wallpaper_drawn = None self.wallpaper_file = "" self.scale_option = tk.IntVar(value=1) - self.show_grid = tk.BooleanVar(value=True) self.adjust_to_dim = tk.BooleanVar(value=False) # throughput related @@ -77,6 +90,17 @@ class CanvasGraph(tk.Canvas): self.throughput_width = 10 self.throughput_color = "#FF0000" + # drawing related + self.show_node_labels = ShowVar(self, tags.NODE_LABEL, value=True) + self.show_link_labels = ShowVar(self, tags.LINK_LABEL, value=True) + self.show_grid = ShowVar(self, tags.GRIDLINE, value=True) + self.show_shapes = ShowVar(self, tags.SHAPE, value=True) + self.show_shape_labels = ShowVar(self, tags.SHAPE_TEXT, value=True) + self.show_marker = ShowVar(self, tags.MARKER, value=True) + self.show_interface_names = BooleanVar(value=False) + self.show_ip4s = BooleanVar(value=True) + self.show_ip6s = BooleanVar(value=True) + # bindings self.setup_bindings() @@ -562,6 +586,7 @@ class CanvasGraph(tk.Canvas): fill=self.app.toolbar.marker_tool.color, outline="", tags=tags.MARKER, + state=self.show_marker.state(), ) return if selected is None: @@ -818,7 +843,7 @@ class CanvasGraph(tk.Canvas): # redraw gridlines to new canvas size self.delete(tags.GRIDLINE) self.draw_grid() - self.update_grid() + self.app.canvas.show_grid.click_handler() def redraw_wallpaper(self): if self.adjust_to_dim.get(): @@ -840,13 +865,6 @@ class CanvasGraph(tk.Canvas): for component in tags.ABOVE_WALLPAPER_TAGS: self.tag_raise(component) - def update_grid(self): - logging.debug("updating grid show grid: %s", self.show_grid.get()) - if self.show_grid.get(): - self.itemconfig(tags.GRIDLINE, state=tk.NORMAL) - else: - self.itemconfig(tags.GRIDLINE, state=tk.HIDDEN) - def set_wallpaper(self, filename: str): logging.debug("setting wallpaper: %s", filename) if filename: @@ -906,7 +924,8 @@ class CanvasGraph(tk.Canvas): self.master, scaled_x, scaled_y, copy, self.nodes[canvas_nid].image ) - # add new node to modified_service_nodes set if that set contains the to_copy node + # add new node to modified_service_nodes set if that set contains the + # to_copy node if self.app.core.service_been_modified(core_node.id): self.app.core.modified_service_nodes.add(copy.id) diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 47bac1b8..c9cc3dd0 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -47,9 +47,10 @@ class CanvasNode: x, label_y, text=self.core_node.name, - tags=tags.NODE_NAME, + tags=tags.NODE_LABEL, font=self.app.icon_text_font, fill="#0000CD", + state=self.canvas.show_node_labels.state(), ) self.tooltip = CanvasTooltip(self.canvas) self.edges = set() @@ -195,7 +196,6 @@ class CanvasNode: label="Mobility Player", command=self.show_mobility_player ) context.add_command(label="Select Adjacent", state=tk.DISABLED) - context.add_command(label="Hide", state=tk.DISABLED) if NodeUtils.is_container_node(self.core_node.type): context.add_command(label="Shell Window", state=tk.DISABLED) context.add_command(label="Tcpdump", state=tk.DISABLED) @@ -228,7 +228,6 @@ class CanvasNode: edit_menu.add_command(label="Cut", state=tk.DISABLED) edit_menu.add_command(label="Copy", command=self.canvas_copy) edit_menu.add_command(label="Delete", command=self.canvas_delete) - edit_menu.add_command(label="Hide", state=tk.DISABLED) context.add_cascade(label="Edit", menu=edit_menu) return context diff --git a/daemon/core/gui/graph/shape.py b/daemon/core/gui/graph/shape.py index 82b58a71..12f70884 100644 --- a/daemon/core/gui/graph/shape.py +++ b/daemon/core/gui/graph/shape.py @@ -85,6 +85,7 @@ class Shape: fill=self.shape_data.fill_color, outline=self.shape_data.border_color, width=self.shape_data.border_width, + state=self.canvas.show_shapes.state(), ) self.draw_shape_text() elif self.shape_type == ShapeType.RECTANGLE: @@ -98,6 +99,7 @@ class Shape: fill=self.shape_data.fill_color, outline=self.shape_data.border_color, width=self.shape_data.border_width, + state=self.canvas.show_shapes.state(), ) self.draw_shape_text() elif self.shape_type == ShapeType.TEXT: @@ -109,6 +111,7 @@ class Shape: text=self.shape_data.text, fill=self.shape_data.text_color, font=font, + state=self.canvas.show_shapes.state(), ) else: logging.error("unknown shape type: %s", self.shape_type) @@ -136,6 +139,7 @@ class Shape: text=self.shape_data.text, fill=self.shape_data.text_color, font=font, + state=self.canvas.show_shape_labels.state(), ) def shape_motion(self, x1: float, y1: float): diff --git a/daemon/core/gui/graph/tags.py b/daemon/core/gui/graph/tags.py index 53d547ac..5caf802a 100644 --- a/daemon/core/gui/graph/tags.py +++ b/daemon/core/gui/graph/tags.py @@ -2,10 +2,10 @@ GRIDLINE = "gridline" SHAPE = "shape" SHAPE_TEXT = "shapetext" EDGE = "edge" -LINK_INFO = "linkinfo" +LINK_LABEL = "linklabel" WIRELESS_EDGE = "wireless" ANTENNA = "antenna" -NODE_NAME = "nodename" +NODE_LABEL = "nodename" NODE = "node" WALLPAPER = "wallpaper" SELECTION = "selectednodes" @@ -15,19 +15,19 @@ ABOVE_WALLPAPER_TAGS = [ SHAPE, SHAPE_TEXT, EDGE, - LINK_INFO, + LINK_LABEL, WIRELESS_EDGE, ANTENNA, NODE, - NODE_NAME, + NODE_LABEL, ] -ABOVE_SHAPE = [GRIDLINE, EDGE, LINK_INFO, WIRELESS_EDGE, ANTENNA, NODE, NODE_NAME] +ABOVE_SHAPE = [GRIDLINE, EDGE, LINK_LABEL, WIRELESS_EDGE, ANTENNA, NODE, NODE_LABEL] COMPONENT_TAGS = [ EDGE, NODE, - NODE_NAME, + NODE_LABEL, WALLPAPER, - LINK_INFO, + LINK_LABEL, ANTENNA, WIRELESS_EDGE, SELECTION, diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 08056071..2a2cde5a 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -3,7 +3,7 @@ import os import tkinter as tk import webbrowser from functools import partial -from tkinter import filedialog, messagebox +from tkinter import BooleanVar, filedialog, messagebox from typing import TYPE_CHECKING from core.gui.appconfig import XMLS_PATH @@ -41,8 +41,10 @@ class Menubar(tk.Menu): self.master.config(menu=self) self.app = app self.core = app.core + self.canvas = app.canvas self.recent_menu = None self.edit_menu = None + self.show_annotations = BooleanVar(value=True) self.draw() def draw(self) -> None: @@ -129,15 +131,41 @@ class Menubar(tk.Menu): Create view menu """ menu = tk.Menu(self) - menu.add_command(label="All", state=tk.DISABLED) - menu.add_command(label="None", state=tk.DISABLED) - menu.add_separator() - menu.add_command(label="Interface Names", state=tk.DISABLED) - menu.add_command(label="IPv4 Addresses", state=tk.DISABLED) - menu.add_command(label="IPv6 Addresses", state=tk.DISABLED) - menu.add_command(label="Node Labels", state=tk.DISABLED) - menu.add_command(label="Annotations", state=tk.DISABLED) - menu.add_command(label="Grid", state=tk.DISABLED) + menu.add_checkbutton( + label="Interface Names", + command=self.click_edge_label_change, + variable=self.canvas.show_interface_names, + ) + menu.add_checkbutton( + label="IPv4 Addresses", + command=self.click_edge_label_change, + variable=self.canvas.show_ip4s, + ) + menu.add_checkbutton( + label="IPv6 Addresses", + command=self.click_edge_label_change, + variable=self.canvas.show_ip6s, + ) + menu.add_checkbutton( + label="Node Labels", + command=self.canvas.show_node_labels.click_handler, + variable=self.canvas.show_node_labels, + ) + menu.add_checkbutton( + label="Link Labels", + command=self.canvas.show_link_labels.click_handler, + variable=self.canvas.show_link_labels, + ) + menu.add_checkbutton( + label="Annotations", + command=self.click_show_annotations, + variable=self.show_annotations, + ) + menu.add_checkbutton( + label="Canvas Grid", + command=self.canvas.show_grid.click_handler, + variable=self.canvas.show_grid, + ) self.add_cascade(label="View", menu=menu) def draw_tools_menu(self) -> None: @@ -430,3 +458,16 @@ class Menubar(tk.Menu): x = (col * layout_size) + padding y = (row * layout_size) + padding node.move(x, y) + + def click_edge_label_change(self) -> None: + for edge in self.canvas.edges.values(): + edge.draw_labels() + + def click_show_annotations(self) -> None: + value = self.show_annotations.get() + self.canvas.show_shapes.set(value) + self.canvas.show_shape_labels.set(value) + self.canvas.show_marker.set(value) + self.canvas.show_shapes.click_handler() + self.canvas.show_shape_labels.click_handler() + self.canvas.show_marker.click_handler() From 3233d8ab58b7bbc1525e4228067e38b4682df669 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 19 Apr 2020 15:57:59 -0700 Subject: [PATCH 0173/1131] pygui simplify show/hiding annotations --- daemon/core/gui/dialogs/shapemod.py | 2 +- daemon/core/gui/graph/graph.py | 10 ++++------ daemon/core/gui/graph/shape.py | 16 ++++++++-------- daemon/core/gui/graph/tags.py | 1 + daemon/core/gui/menubar.py | 16 +++------------- 5 files changed, 17 insertions(+), 28 deletions(-) diff --git a/daemon/core/gui/dialogs/shapemod.py b/daemon/core/gui/dialogs/shapemod.py index 791e1f71..f47cb7b3 100644 --- a/daemon/core/gui/dialogs/shapemod.py +++ b/daemon/core/gui/dialogs/shapemod.py @@ -235,7 +235,7 @@ class ShapeDialog(Dialog): text=shape_text, fill=self.text_color, font=text_font, - tags=tags.SHAPE_TEXT, + tags=(tags.SHAPE_TEXT, tags.ANNOTATION), ) self.shape.created = True else: diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 3b1efaea..e7908d35 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -94,9 +94,7 @@ class CanvasGraph(tk.Canvas): self.show_node_labels = ShowVar(self, tags.NODE_LABEL, value=True) self.show_link_labels = ShowVar(self, tags.LINK_LABEL, value=True) self.show_grid = ShowVar(self, tags.GRIDLINE, value=True) - self.show_shapes = ShowVar(self, tags.SHAPE, value=True) - self.show_shape_labels = ShowVar(self, tags.SHAPE_TEXT, value=True) - self.show_marker = ShowVar(self, tags.MARKER, value=True) + self.show_annotations = ShowVar(self, tags.ANNOTATION, value=True) self.show_interface_names = BooleanVar(value=False) self.show_ip4s = BooleanVar(value=True) self.show_ip6s = BooleanVar(value=True) @@ -585,8 +583,8 @@ class CanvasGraph(tk.Canvas): y + r, fill=self.app.toolbar.marker_tool.color, outline="", - tags=tags.MARKER, - state=self.show_marker.state(), + tags=(tags.MARKER, tags.ANNOTATION), + state=self.show_annotations.state(), ) return if selected is None: @@ -669,7 +667,7 @@ class CanvasGraph(tk.Canvas): y + r, fill=self.app.toolbar.marker_tool.color, outline="", - tags="marker", + tags=(tags.MARKER, tags.ANNOTATION), ) return diff --git a/daemon/core/gui/graph/shape.py b/daemon/core/gui/graph/shape.py index 12f70884..6e3d682d 100644 --- a/daemon/core/gui/graph/shape.py +++ b/daemon/core/gui/graph/shape.py @@ -80,12 +80,12 @@ class Shape: self.y1, self.x2, self.y2, - tags=tags.SHAPE, + tags=(tags.SHAPE, tags.ANNOTATION), dash=dash, fill=self.shape_data.fill_color, outline=self.shape_data.border_color, width=self.shape_data.border_width, - state=self.canvas.show_shapes.state(), + state=self.canvas.show_annotations.state(), ) self.draw_shape_text() elif self.shape_type == ShapeType.RECTANGLE: @@ -94,12 +94,12 @@ class Shape: self.y1, self.x2, self.y2, - tags=tags.SHAPE, + tags=(tags.SHAPE, tags.ANNOTATION), dash=dash, fill=self.shape_data.fill_color, outline=self.shape_data.border_color, width=self.shape_data.border_width, - state=self.canvas.show_shapes.state(), + state=self.canvas.show_annotations.state(), ) self.draw_shape_text() elif self.shape_type == ShapeType.TEXT: @@ -107,11 +107,11 @@ class Shape: self.id = self.canvas.create_text( self.x1, self.y1, - tags=tags.SHAPE_TEXT, + tags=(tags.SHAPE_TEXT, tags.ANNOTATION), text=self.shape_data.text, fill=self.shape_data.text_color, font=font, - state=self.canvas.show_shapes.state(), + state=self.canvas.show_annotations.state(), ) else: logging.error("unknown shape type: %s", self.shape_type) @@ -135,11 +135,11 @@ class Shape: self.text_id = self.canvas.create_text( x, y, - tags=tags.SHAPE_TEXT, + tags=(tags.SHAPE_TEXT, tags.ANNOTATION), text=self.shape_data.text, fill=self.shape_data.text_color, font=font, - state=self.canvas.show_shape_labels.state(), + state=self.canvas.show_annotations.state(), ) def shape_motion(self, x1: float, y1: float): diff --git a/daemon/core/gui/graph/tags.py b/daemon/core/gui/graph/tags.py index 5caf802a..8ac6476b 100644 --- a/daemon/core/gui/graph/tags.py +++ b/daemon/core/gui/graph/tags.py @@ -1,3 +1,4 @@ +ANNOTATION = "annotation" GRIDLINE = "gridline" SHAPE = "shape" SHAPE_TEXT = "shapetext" diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 2a2cde5a..0f016374 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -3,7 +3,7 @@ import os import tkinter as tk import webbrowser from functools import partial -from tkinter import BooleanVar, filedialog, messagebox +from tkinter import filedialog, messagebox from typing import TYPE_CHECKING from core.gui.appconfig import XMLS_PATH @@ -44,7 +44,6 @@ class Menubar(tk.Menu): self.canvas = app.canvas self.recent_menu = None self.edit_menu = None - self.show_annotations = BooleanVar(value=True) self.draw() def draw(self) -> None: @@ -158,8 +157,8 @@ class Menubar(tk.Menu): ) menu.add_checkbutton( label="Annotations", - command=self.click_show_annotations, - variable=self.show_annotations, + command=self.canvas.show_annotations.click_handler, + variable=self.canvas.show_annotations, ) menu.add_checkbutton( label="Canvas Grid", @@ -462,12 +461,3 @@ class Menubar(tk.Menu): def click_edge_label_change(self) -> None: for edge in self.canvas.edges.values(): edge.draw_labels() - - def click_show_annotations(self) -> None: - value = self.show_annotations.get() - self.canvas.show_shapes.set(value) - self.canvas.show_shape_labels.set(value) - self.canvas.show_marker.set(value) - self.canvas.show_shapes.click_handler() - self.canvas.show_shape_labels.click_handler() - self.canvas.show_marker.click_handler() From b4de016a2411ef0b3fe4716bb557c1493d488e66 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 19 Apr 2020 23:02:25 -0700 Subject: [PATCH 0174/1131] pygui cleanup sessions dialog --- daemon/core/gui/coreclient.py | 10 +- daemon/core/gui/dialogs/sessions.py | 153 +++++++++++++--------------- 2 files changed, 78 insertions(+), 85 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index b8914808..dc9bb82b 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -283,6 +283,12 @@ class CoreClient: self.session_id, self.handle_events ) + # get session service defaults + response = self.client.get_service_defaults(self.session_id) + self.default_services = { + x.node_type: set(x.services) for x in response.defaults + } + # get location if query_location: response = self.client.get_session_location(self.session_id) @@ -483,10 +489,6 @@ class CoreClient: else: dialog = SessionsDialog(self.app, self.app, True) dialog.show() - response = self.client.get_service_defaults(self.session_id) - self.default_services = { - x.node_type: set(x.services) for x in response.defaults - } except grpc.RpcError as e: show_grpc_error(e, self.app, self.app) self.app.close() diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index e540a3ca..6a3cf380 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -1,7 +1,7 @@ import logging import tkinter as tk -from tkinter import ttk -from typing import TYPE_CHECKING, Iterable +from tkinter import messagebox, ttk +from typing import TYPE_CHECKING, List import grpc @@ -19,35 +19,35 @@ if TYPE_CHECKING: class SessionsDialog(Dialog): def __init__( self, master: "Application", app: "Application", is_start_app: bool = False - ): + ) -> None: super().__init__(master, app, "Sessions", modal=True) self.is_start_app = is_start_app - self.selected = False + self.selected_session = None self.selected_id = None self.tree = None - self.has_error = False self.sessions = self.get_sessions() - if not self.has_error: - self.draw() + self.connect_button = None + self.delete_button = None + self.protocol("WM_DELETE_WINDOW", self.on_closing) + self.draw() - def get_sessions(self) -> Iterable[core_pb2.SessionSummary]: + def get_sessions(self) -> List[core_pb2.SessionSummary]: try: response = self.app.core.client.get_sessions() logging.info("sessions: %s", response) return response.sessions except grpc.RpcError as e: show_grpc_error(e, self.app, self.app) - self.has_error = True self.destroy() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(1, weight=1) self.draw_description() self.draw_tree() self.draw_buttons() - def draw_description(self): + def draw_description(self) -> None: """ write a short description """ @@ -61,13 +61,16 @@ class SessionsDialog(Dialog): ) label.grid(pady=PADY) - def draw_tree(self): + def draw_tree(self) -> None: frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) frame.grid(sticky="nsew", pady=PADY) self.tree = ttk.Treeview( - frame, columns=("id", "state", "nodes"), show="headings" + frame, + columns=("id", "state", "nodes"), + show="headings", + selectmode=tk.BROWSE, ) self.tree.grid(sticky="nsew") self.tree.column("id", stretch=tk.YES) @@ -85,7 +88,7 @@ class SessionsDialog(Dialog): text=str(session.id), values=(session.id, state_name, session.nodes), ) - self.tree.bind("", self.on_selected) + self.tree.bind("", self.double_click_join) self.tree.bind("<>", self.click_select) yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) @@ -96,9 +99,9 @@ class SessionsDialog(Dialog): xscrollbar.grid(row=1, sticky="ew") self.tree.configure(xscrollcommand=xscrollbar.set) - def draw_buttons(self): + def draw_buttons(self) -> None: frame = ttk.Frame(self.top) - for i in range(5): + for i in range(4): frame.columnconfigure(i, weight=1) frame.grid(sticky="ew") @@ -110,42 +113,37 @@ class SessionsDialog(Dialog): b.grid(row=0, padx=PADX, sticky="ew") image = Images.get(ImageEnum.FILEOPEN, 16) - b = ttk.Button( + self.connect_button = ttk.Button( frame, image=image, text="Connect", compound=tk.LEFT, command=self.click_connect, + state=tk.DISABLED, ) - b.image = image - b.grid(row=0, column=1, padx=PADX, sticky="ew") - - image = Images.get(ImageEnum.SHUTDOWN, 16) - b = ttk.Button( - frame, - image=image, - text="Shutdown", - compound=tk.LEFT, - command=self.click_shutdown, - ) - b.image = image - b.grid(row=0, column=2, padx=PADX, sticky="ew") + self.connect_button.image = image + self.connect_button.grid(row=0, column=1, padx=PADX, sticky="ew") image = Images.get(ImageEnum.DELETE, 16) - b = ttk.Button( + self.delete_button = ttk.Button( frame, image=image, text="Delete", compound=tk.LEFT, command=self.click_delete, + state=tk.DISABLED, ) - b.image = image - b.grid(row=0, column=3, padx=PADX, sticky="ew") + self.delete_button.image = image + self.delete_button.grid(row=0, column=2, padx=PADX, sticky="ew") image = Images.get(ImageEnum.CANCEL, 16) if self.is_start_app: b = ttk.Button( - frame, image=image, text="Exit", compound=tk.LEFT, command=self.destroy + frame, + image=image, + text="Exit", + compound=tk.LEFT, + command=self.click_exit, ) else: b = ttk.Button( @@ -156,42 +154,32 @@ class SessionsDialog(Dialog): command=self.destroy, ) b.image = image - b.grid(row=0, column=4, sticky="ew") + b.grid(row=0, column=3, sticky="ew") - def click_new(self): + def click_new(self) -> None: self.app.core.create_new_session() self.destroy() - def click_select(self, event: tk.Event): + def click_select(self, _event: tk.Event = None) -> None: item = self.tree.selection() - session_id = int(self.tree.item(item, "text")) - self.selected = True - self.selected_id = session_id - - def click_connect(self): - """ - if no session is selected yet, create a new one else join that session - """ - if self.selected and self.selected_id is not None: - self.join_session(self.selected_id) - elif not self.selected and self.selected_id is None: - self.click_new() + if item: + self.selected_session = int(self.tree.item(item, "text")) + self.selected_id = item + self.delete_button.config(state=tk.NORMAL) + self.connect_button.config(state=tk.NORMAL) else: - logging.error("sessions invalid state") + self.selected_session = None + self.selected_id = None + self.delete_button.config(state=tk.DISABLED) + self.connect_button.config(state=tk.DISABLED) + logging.debug("selected session: %s", self.selected_session) - def click_shutdown(self): - """ - if no session is currently selected create a new session else shut the selected - session down. - """ - if self.selected and self.selected_id is not None: - self.shutdown_session(self.selected_id) - elif not self.selected and self.selected_id is None: - self.click_new() - else: - logging.error("querysessiondrawing.py invalid state") + def click_connect(self) -> None: + if not self.selected_session: + return + self.join_session(self.selected_session) - def join_session(self, session_id: int): + def join_session(self, session_id: int) -> None: if self.app.core.xml_file: self.app.core.xml_file = None self.app.statusbar.progress_bar.start(5) @@ -199,26 +187,29 @@ class SessionsDialog(Dialog): task.start() self.destroy() - def on_selected(self, event: tk.Event): + def double_click_join(self, _event: tk.Event) -> None: item = self.tree.selection() - sid = int(self.tree.item(item, "text")) - self.join_session(sid) + if item is None: + return + session_id = int(self.tree.item(item, "text")) + self.join_session(session_id) - def shutdown_session(self, sid: int): - self.app.core.stop_session(sid) - self.click_new() + def click_delete(self) -> None: + if not self.selected_session: + return + logging.debug("delete session: %s", self.selected_session) + # self.app.core.delete_session(self.selected_id, self.top) + self.tree.delete(self.selected_id) + self.app.core.delete_session(self.selected_session) + if self.selected_session == self.app.core.session_id: + self.click_new() + self.destroy() + self.click_select() + + def click_exit(self) -> None: self.destroy() + self.app.close() - def click_delete(self): - logging.debug("Click delete") - item = self.tree.selection() - if item: - sid = int(self.tree.item(item, "text")) - self.app.core.delete_session(sid, self.top) - self.tree.delete(item[0]) - if sid == self.app.core.session_id: - self.click_new() - selections = self.tree.get_children() - if selections: - self.tree.focus(selections[0]) - self.tree.selection_set(selections[0]) + def on_closing(self) -> None: + if self.is_start_app and messagebox.askokcancel("Exit", "Quit?", parent=self): + self.click_exit() From efa5506c804d768aeb225aab0b06af65dc850a1a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 20 Apr 2020 15:56:25 -0700 Subject: [PATCH 0175/1131] fix issue when tcp handlers has no other clients for a session --- daemon/core/api/tlv/corehandlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 1cc9523c..54016a40 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -2062,7 +2062,7 @@ class CoreUdpHandler(CoreHandler): if not isinstance(message, (coreapi.CoreNodeMessage, coreapi.CoreLinkMessage)): return - clients = self.tcp_handler.session_clients[self.session.id] + clients = self.tcp_handler.session_clients.get(self.session.id, []) for client in clients: try: client.sendall(message.raw_message) From 54eab4576df27dae5632a075edabe9637ec9d2e8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 20 Apr 2020 23:20:39 -0700 Subject: [PATCH 0176/1131] pygui add in cut functionality, currently not including configurations --- daemon/core/gui/coreclient.py | 33 ++++++++++---------- daemon/core/gui/graph/graph.py | 56 ++++++++++++++++++---------------- daemon/core/gui/graph/node.py | 6 +++- daemon/core/gui/menubar.py | 37 ++++++++++++---------- 4 files changed, 72 insertions(+), 60 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index dc9bb82b..9b1ab9bc 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -1061,31 +1061,32 @@ class CoreClient: self.canvas_nodes[_to].core_node.services[:] = services logging.debug("copying node %s service to node %s", _from, _to) - def copy_node_config(self, _from: int, _to: int): - node_type = self.canvas_nodes[_from].core_node.type + def copy_node_config(self, src_node: core_pb2.Node, dst_id: int): + node_type = src_node.type if node_type == core_pb2.NodeType.DEFAULT: - services = self.canvas_nodes[_from].core_node.services - self.canvas_nodes[_to].core_node.services[:] = services - config = self.service_configs.get(_from) + services = src_node.services + dst_node = self.canvas_nodes[dst_id] + dst_node.core_node.services[:] = services + config = self.service_configs.get(src_node.id) if config: - self.service_configs[_to] = config - file_configs = self.file_configs.get(_from) + self.service_configs[dst_id] = config + file_configs = self.file_configs.get(src_node.id) if file_configs: for key, value in file_configs.items(): - if _to not in self.file_configs: - self.file_configs[_to] = {} - self.file_configs[_to][key] = value + if dst_id not in self.file_configs: + self.file_configs[dst_id] = {} + self.file_configs[dst_id][key] = value elif node_type == core_pb2.NodeType.WIRELESS_LAN: - config = self.wlan_configs.get(_from) + config = self.wlan_configs.get(src_node.id) if config: - self.wlan_configs[_to] = config - config = self.mobility_configs.get(_from) + self.wlan_configs[dst_id] = config + config = self.mobility_configs.get(src_node.id) if config: - self.mobility_configs[_to] = config + self.mobility_configs[dst_id] = config elif node_type == core_pb2.NodeType.EMANE: - config = self.emane_model_configs.get(_from) + config = self.emane_model_configs.get(src_node.id) if config: - self.emane_model_configs[_to] = config + self.emane_model_configs[dst_id] = config def service_been_modified(self, node_id: int) -> bool: return node_id in self.modified_service_nodes diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index e7908d35..abe045a3 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -894,61 +894,63 @@ class CanvasGraph(tk.Canvas): self.core.create_link(edge, source, dest) def copy(self): - if self.app.core.is_runtime(): + if self.core.is_runtime(): logging.info("copy is disabled during runtime state") return if self.selection: - logging.debug("to copy %s nodes", len(self.selection)) - self.to_copy = self.selection.keys() + logging.info("to copy nodes: %s", self.selection) + self.to_copy.clear() + for node_id in self.selection.keys(): + canvas_node = self.nodes[node_id] + self.to_copy.append(canvas_node) def paste(self): - if self.app.core.is_runtime(): + if self.core.is_runtime(): logging.info("paste is disabled during runtime state") return # maps original node canvas id to copy node canvas id copy_map = {} # the edges that will be copy over to_copy_edges = [] - for canvas_nid in self.to_copy: - core_node = self.nodes[canvas_nid].core_node + for canvas_node in self.to_copy: + core_node = canvas_node.core_node actual_x = core_node.position.x + 50 actual_y = core_node.position.y + 50 scaled_x, scaled_y = self.get_scaled_coords(actual_x, actual_y) - copy = self.core.create_node( actual_x, actual_y, core_node.type, core_node.model ) - node = CanvasNode( - self.master, scaled_x, scaled_y, copy, self.nodes[canvas_nid].image - ) + node = CanvasNode(self.master, scaled_x, scaled_y, copy, canvas_node.image) # add new node to modified_service_nodes set if that set contains the # to_copy node - if self.app.core.service_been_modified(core_node.id): - self.app.core.modified_service_nodes.add(copy.id) + if self.core.service_been_modified(core_node.id): + self.core.modified_service_nodes.add(copy.id) - copy_map[canvas_nid] = node.id + copy_map[canvas_node.id] = node.id self.core.canvas_nodes[copy.id] = node self.nodes[node.id] = node - self.core.copy_node_config(core_node.id, copy.id) - - edges = self.nodes[canvas_nid].edges - for edge in edges: + self.core.copy_node_config(core_node, copy.id) + for edge in canvas_node.edges: if edge.src not in self.to_copy or edge.dst not in self.to_copy: - if canvas_nid == edge.src: - self.create_edge(node, self.nodes[edge.dst]) - elif canvas_nid == edge.dst: - self.create_edge(self.nodes[edge.src], node) + if canvas_node.id == edge.src: + dst_node = self.nodes[edge.dst] + self.create_edge(node, dst_node) + elif canvas_node.id == edge.dst: + src_node = self.nodes[edge.src] + self.create_edge(src_node, node) else: to_copy_edges.append(edge) + # copy link and link config for edge in to_copy_edges: - source_node_copy = self.nodes[copy_map[edge.token[0]]] - dest_node_copy = self.nodes[copy_map[edge.token[1]]] - self.create_edge(source_node_copy, dest_node_copy) - copy_edge = self.edges[ - create_edge_token(source_node_copy.id, dest_node_copy.id) - ] + src_node_id = copy_map[edge.token[0]] + dst_node_id = copy_map[edge.token[1]] + 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_link = copy_edge.link options = edge.link.options copy_link.options.CopyFrom(options) diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index c9cc3dd0..9bfc621e 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -225,12 +225,16 @@ class CanvasNode: context.add_command(label="Select Members", state=tk.DISABLED) edit_menu = tk.Menu(context) themes.style_menu(edit_menu) - edit_menu.add_command(label="Cut", state=tk.DISABLED) + edit_menu.add_command(label="Cut", command=self.click_cut) edit_menu.add_command(label="Copy", command=self.canvas_copy) edit_menu.add_command(label="Delete", command=self.canvas_delete) context.add_cascade(label="Edit", menu=edit_menu) return context + def click_cut(self) -> None: + self.canvas_copy() + self.canvas_delete() + def canvas_delete(self) -> None: self.canvas.clear_selection() self.canvas.selection[self.id] = self diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 0f016374..5f85b91f 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -103,14 +103,14 @@ class Menubar(tk.Menu): menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED) menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED) menu.add_separator() - menu.add_command(label="Cut", accelerator="Ctrl+X", state=tk.DISABLED) + menu.add_command(label="Cut", accelerator="Ctrl+X", command=self.click_cut) menu.add_command(label="Copy", accelerator="Ctrl+C", command=self.click_copy) menu.add_command(label="Paste", accelerator="Ctrl+V", command=self.click_paste) menu.add_command( label="Delete", accelerator="Ctrl+D", command=self.click_delete ) self.add_cascade(label="Edit", menu=menu) - + self.app.master.bind_all("", self.click_cut) self.app.master.bind_all("", self.click_copy) self.app.master.bind_all("", self.click_paste) self.app.master.bind_all("", self.click_delete) @@ -343,16 +343,17 @@ class Menubar(tk.Menu): self.app.menubar.update_recent_files() def change_menubar_item_state(self, is_runtime: bool) -> None: - for i in range(self.edit_menu.index("end")): + labels = {"Copy", "Paste", "Delete", "Cut"} + for i in range(self.edit_menu.index(tk.END) + 1): try: - label_name = self.edit_menu.entrycget(i, "label") - if label_name in ["Copy", "Paste"]: - if is_runtime: - self.edit_menu.entryconfig(i, state="disabled") - else: - self.edit_menu.entryconfig(i, state="normal") + label = self.edit_menu.entrycget(i, "label") + logging.info("menu label: %s", label) + if label not in labels: + continue + state = tk.DISABLED if is_runtime else tk.NORMAL + self.edit_menu.entryconfig(i, state=state) except tk.TclError: - logging.debug("Ignore separators") + pass def prompt_save_running_session(self, quit_app: bool = False) -> None: """ @@ -410,13 +411,17 @@ class Menubar(tk.Menu): dialog.show() def click_copy(self, _event: tk.Event = None) -> None: - self.app.canvas.copy() + self.canvas.copy() def click_paste(self, _event: tk.Event = None) -> None: - self.app.canvas.paste() + self.canvas.paste() def click_delete(self, _event: tk.Event = None) -> None: - self.app.canvas.delete_selected_objects() + self.canvas.delete_selected_objects() + + def click_cut(self, _event: tk.Event = None) -> None: + self.canvas.copy() + self.canvas.delete_selected_objects() def click_session_options(self) -> None: logging.debug("Click options") @@ -444,14 +449,14 @@ class Menubar(tk.Menu): dialog.show() def click_autogrid(self) -> None: - width, height = self.app.canvas.current_dimensions + width, height = self.canvas.current_dimensions padding = (ICON_SIZE / 2) + 10 layout_size = padding + ICON_SIZE col_count = width // layout_size logging.info( - "auto grid layout: dimens(%s, %s) col(%s)", width, height, col_count + "auto grid layout: dimension(%s, %s) col(%s)", width, height, col_count ) - for i, node in enumerate(self.app.canvas.nodes.values()): + for i, node in enumerate(self.canvas.nodes.values()): col = i % col_count row = i // col_count x = (col * layout_size) + padding From bd30d0d9ffe32f969cff8c4eef9de27b2e94c9d3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 21 Apr 2020 00:38:36 -0700 Subject: [PATCH 0177/1131] changes to support nodes containing their configurations, starting with emane, making copying easier and reducing code --- daemon/core/gui/coreclient.py | 86 ++++++++++---------------- daemon/core/gui/dialogs/emaneconfig.py | 30 +++++---- daemon/core/gui/dialogs/nodeconfig.py | 4 +- daemon/core/gui/graph/graph.py | 4 ++ daemon/core/gui/graph/node.py | 4 ++ 5 files changed, 60 insertions(+), 68 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 9b1ab9bc..520bf6f9 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -92,7 +92,6 @@ class CoreClient: self.hooks = {} self.wlan_configs = {} self.mobility_configs = {} - self.emane_model_configs = {} self.emane_config = None self.service_configs = {} self.config_service_configs = {} @@ -130,7 +129,6 @@ class CoreClient: self.hooks.clear() self.wlan_configs.clear() self.mobility_configs.clear() - self.emane_model_configs.clear() self.emane_config = None self.service_configs.clear() self.file_configs.clear() @@ -303,25 +301,29 @@ class CoreClient: for hook in response.hooks: self.hooks[hook.file] = hook + # get emane config + response = self.client.get_emane_config(self.session_id) + self.emane_config = response.config + + # draw session + self.app.canvas.reset_and_redraw(session) + # get mobility configs response = self.client.get_mobility_configs(self.session_id) for node_id in response.configs: node_config = response.configs[node_id].config self.mobility_configs[node_id] = node_config - # get emane config - response = self.client.get_emane_config(self.session_id) - self.emane_config = response.config - # get emane model config response = self.client.get_emane_model_configs(self.session_id) for config in response.configs: interface = None if config.interface != -1: interface = config.interface - self.set_emane_model_config( - config.node_id, config.model, config.config, interface - ) + canvas_node = self.canvas_nodes[config.node_id] + canvas_node.emane_model_configs[ + (config.model, interface) + ] = config.config # get wlan configurations response = self.client.get_wlan_configs(self.session_id) @@ -353,13 +355,9 @@ class CoreClient: if config.config: service_config["config"] = config.config - # draw session - self.app.canvas.reset_and_redraw(session) - # get metadata response = self.client.get_session_metadata(self.session_id) self.parse_metadata(response.config) - except grpc.RpcError as e: self.app.after(0, show_grpc_error, e, self.app, self.app) @@ -848,10 +846,6 @@ class CoreClient: del self.mobility_configs[node_id] if node_id in self.wlan_configs: del self.wlan_configs[node_id] - for key in list(self.emane_model_configs): - node_id, _, _ = key - if node_id == node_id: - del self.emane_model_configs[key] for edge in canvas_node.edges: if edge in edges: @@ -938,15 +932,19 @@ class CoreClient: def get_emane_model_configs_proto(self) -> List[EmaneModelConfig]: configs = [] - for key, config in self.emane_model_configs.items(): - node_id, model, interface = key - config = {x: config[x].value for x in config} - if interface is None: - interface = -1 - config_proto = EmaneModelConfig( - node_id=node_id, interface_id=interface, model=model, config=config - ) - configs.append(config_proto) + for canvas_node in self.canvas_nodes.values(): + if canvas_node.core_node.type != core_pb2.NodeType.EMANE: + continue + node_id = canvas_node.core_node.id + for key, config in canvas_node.emane_model_configs.items(): + model, interface = key + config = {x: config[x].value for x in config} + if interface is None: + interface = -1 + config_proto = EmaneModelConfig( + node_id=node_id, interface_id=interface, model=model, config=config + ) + configs.append(config_proto) return configs def get_service_configs_proto(self) -> List[ServiceConfig]: @@ -1023,14 +1021,12 @@ class CoreClient: def get_emane_model_config( self, node_id: int, model: str, interface: int = None ) -> Dict[str, common_pb2.ConfigOption]: - config = self.emane_model_configs.get((node_id, model, interface)) - if not config: - if interface is None: - interface = -1 - response = self.client.get_emane_model_config( - self.session_id, node_id, model, interface - ) - config = response.config + if interface is None: + interface = -1 + response = self.client.get_emane_model_config( + self.session_id, node_id, model, interface + ) + config = response.config logging.debug( "get emane model config: node id: %s, EMANE model: %s, interface: %s, config: %s", node_id, @@ -1038,23 +1034,7 @@ class CoreClient: interface, config, ) - return config - - def set_emane_model_config( - self, - node_id: int, - model: str, - config: Dict[str, common_pb2.ConfigOption], - interface: int = None, - ): - logging.info( - "set emane model config: node id: %s, EMANE Model: %s, interface: %s, config: %s", - node_id, - model, - interface, - config, - ) - self.emane_model_configs[(node_id, model, interface)] = config + return dict(config) def copy_node_service(self, _from: int, _to: int): services = self.canvas_nodes[_from].core_node.services @@ -1083,10 +1063,6 @@ class CoreClient: config = self.mobility_configs.get(src_node.id) if config: self.mobility_configs[dst_id] = config - elif node_type == core_pb2.NodeType.EMANE: - config = self.emane_model_configs.get(src_node.id) - if config: - self.emane_model_configs[dst_id] = config def service_been_modified(self, node_id: int) -> bool: return node_id in self.modified_service_nodes diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index e5f403fe..d0fcaed6 100644 --- a/daemon/core/gui/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Any import grpc -from core.api.grpc import core_pb2 from core.gui.dialogs.dialog import Dialog from core.gui.errors import show_grpc_error from core.gui.images import ImageEnum, Images @@ -56,20 +55,30 @@ class EmaneModelDialog(Dialog): self, master: Any, app: "Application", - node: core_pb2.Node, + canvas_node: "CanvasNode", model: str, interface: int = None, ): - super().__init__(master, app, f"{node.name} {model} Configuration", modal=True) - self.node = node + super().__init__( + master, + app, + f"{canvas_node.core_node.name} {model} Configuration", + modal=True, + ) + self.canvas_node = canvas_node + self.node = canvas_node.core_node self.model = f"emane_{model}" self.interface = interface self.config_frame = None self.has_error = False try: - self.config = self.app.core.get_emane_model_config( - self.node.id, self.model, self.interface + self.config = self.canvas_node.emane_model_configs.get( + (self.model, self.interface) ) + if not self.config: + self.config = self.app.core.get_emane_model_config( + self.node.id, self.model, self.interface + ) self.draw() except grpc.RpcError as e: show_grpc_error(e, self.app, self.app) @@ -98,9 +107,8 @@ class EmaneModelDialog(Dialog): def click_apply(self): self.config_frame.parse_config() - self.app.core.set_emane_model_config( - self.node.id, self.model, self.config, self.interface - ) + key = (self.model, self.interface) + self.canvas_node.emane_model_configs[key] = dict(self.config) self.destroy() @@ -224,9 +232,7 @@ class EmaneConfigDialog(Dialog): draw emane model configuration """ model_name = self.emane_model.get() - dialog = EmaneModelDialog( - self, self.app, self.canvas_node.core_node, model_name - ) + dialog = EmaneModelDialog(self, self.app, self.canvas_node, model_name) if not dialog.has_error: dialog.show() diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index d77c69c4..ce8ea802 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -290,7 +290,9 @@ class NodeConfigDialog(Dialog): button.grid(row=0, column=1, sticky="ew") def click_emane_config(self, emane_model: str, interface_id: int): - dialog = EmaneModelDialog(self, self.app, self.node, emane_model, interface_id) + dialog = EmaneModelDialog( + self, self.app, self.canvas_node, emane_model, interface_id + ) dialog.show() def click_icon(self): diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index abe045a3..c95c1eac 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -1,5 +1,6 @@ import logging import tkinter as tk +from copy import deepcopy from tkinter import BooleanVar from typing import TYPE_CHECKING, Tuple @@ -922,6 +923,9 @@ class CanvasGraph(tk.Canvas): ) node = CanvasNode(self.master, scaled_x, scaled_y, copy, canvas_node.image) + # copy configurations + node.emane_model_configs = deepcopy(canvas_node.emane_model_configs) + # add new node to modified_service_nodes set if that set contains the # to_copy node if self.core.service_been_modified(core_node.id): diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 9bfc621e..63a85ad7 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -58,6 +58,10 @@ class CanvasNode: self.wireless_edges = set() self.antennas = [] self.antenna_images = {} + # possible configurations + self.emane_model_configs = {} + self.wlan_config = {} + self.mobility_config = {} self.setup_bindings() def setup_bindings(self): From 85b4a81f8a4808ed4897b6ea2ed7ef543d5663fe Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 21 Apr 2020 09:34:30 -0700 Subject: [PATCH 0178/1131] updated wlan/mobility configs to be directly associated with a node and allow them to be copied --- daemon/core/gui/coreclient.py | 75 ++++++++++------------- daemon/core/gui/dialogs/emaneconfig.py | 2 +- daemon/core/gui/dialogs/mobilityconfig.py | 6 +- daemon/core/gui/dialogs/mobilityplayer.py | 5 +- daemon/core/gui/dialogs/wlanconfig.py | 6 +- daemon/core/gui/graph/graph.py | 9 ++- daemon/core/gui/widgets.py | 1 - 7 files changed, 52 insertions(+), 52 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 520bf6f9..c9c5fcc5 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -90,8 +90,6 @@ class CoreClient: self.location = None self.links = {} self.hooks = {} - self.wlan_configs = {} - self.mobility_configs = {} self.emane_config = None self.service_configs = {} self.config_service_configs = {} @@ -127,8 +125,6 @@ class CoreClient: self.canvas_nodes.clear() self.links.clear() self.hooks.clear() - self.wlan_configs.clear() - self.mobility_configs.clear() self.emane_config = None self.service_configs.clear() self.file_configs.clear() @@ -311,8 +307,9 @@ class CoreClient: # get mobility configs response = self.client.get_mobility_configs(self.session_id) for node_id in response.configs: - node_config = response.configs[node_id].config - self.mobility_configs[node_id] = node_config + config = response.configs[node_id].config + canvas_node = self.canvas_nodes[node_id] + canvas_node.mobility_config = dict(config) # get emane model config response = self.client.get_emane_model_configs(self.session_id) @@ -321,15 +318,16 @@ class CoreClient: if config.interface != -1: interface = config.interface canvas_node = self.canvas_nodes[config.node_id] - canvas_node.emane_model_configs[ - (config.model, interface) - ] = config.config + canvas_node.emane_model_configs[(config.model, interface)] = dict( + config.config + ) # get wlan configurations response = self.client.get_wlan_configs(self.session_id) for _id in response.configs: mapped_config = response.configs[_id] - self.wlan_configs[_id] = mapped_config.config + canvas_node = self.canvas_nodes[_id] + canvas_node.wlan_config = dict(mapped_config.config) # get service configurations response = self.client.get_node_service_configs(self.session_id) @@ -554,11 +552,16 @@ class CoreClient: return response def show_mobility_players(self): - for node_id, config in self.mobility_configs.items(): - canvas_node = self.canvas_nodes[node_id] - mobility_player = MobilityPlayer(self.app, self.app, canvas_node, config) - mobility_player.show() - self.mobility_players[node_id] = mobility_player + for canvas_node in self.canvas_nodes.values(): + if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: + continue + if canvas_node.mobility_config: + mobility_player = MobilityPlayer( + self.app, self.app, canvas_node, canvas_node.mobility_config + ) + node_id = canvas_node.core_node.id + self.mobility_players[node_id] = mobility_player + mobility_player.show() def set_metadata(self): # create canvas data @@ -839,14 +842,7 @@ class CoreClient: logging.error("unknown node: %s", node_id) continue del self.canvas_nodes[node_id] - self.modified_service_nodes.discard(node_id) - - if node_id in self.mobility_configs: - del self.mobility_configs[node_id] - if node_id in self.wlan_configs: - del self.wlan_configs[node_id] - for edge in canvas_node.edges: if edge in edges: continue @@ -916,16 +912,24 @@ class CoreClient: def get_wlan_configs_proto(self) -> List[WlanConfig]: configs = [] - for node_id, config in self.wlan_configs.items(): + for canvas_node in self.canvas_nodes.values(): + if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: + continue + config = canvas_node.wlan_config config = {x: config[x].value for x in config} + node_id = canvas_node.core_node.id wlan_config = WlanConfig(node_id=node_id, config=config) configs.append(wlan_config) return configs def get_mobility_configs_proto(self) -> List[MobilityConfig]: configs = [] - for node_id, config in self.mobility_configs.items(): + for canvas_node in self.canvas_nodes.values(): + if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: + continue + config = canvas_node.mobility_config config = {x: config[x].value for x in config} + node_id = canvas_node.core_node.id mobility_config = MobilityConfig(node_id=node_id, config=config) configs.append(mobility_config) return configs @@ -995,28 +999,24 @@ class CoreClient: return self.client.node_command(self.session_id, node_id, self.observer).output def get_wlan_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]: - config = self.wlan_configs.get(node_id) - if not config: - response = self.client.get_wlan_config(self.session_id, node_id) - config = response.config + response = self.client.get_wlan_config(self.session_id, node_id) + config = response.config logging.debug( "get wlan configuration from node %s, result configuration: %s", node_id, config, ) - return config + return dict(config) def get_mobility_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]: - config = self.mobility_configs.get(node_id) - if not config: - response = self.client.get_mobility_config(self.session_id, node_id) - config = response.config + response = self.client.get_mobility_config(self.session_id, node_id) + config = response.config logging.debug( "get mobility config from node %s, result configuration: %s", node_id, config, ) - return config + return dict(config) def get_emane_model_config( self, node_id: int, model: str, interface: int = None @@ -1056,13 +1056,6 @@ class CoreClient: if dst_id not in self.file_configs: self.file_configs[dst_id] = {} self.file_configs[dst_id][key] = value - elif node_type == core_pb2.NodeType.WIRELESS_LAN: - config = self.wlan_configs.get(src_node.id) - if config: - self.wlan_configs[dst_id] = config - config = self.mobility_configs.get(src_node.id) - if config: - self.mobility_configs[dst_id] = config def service_been_modified(self, node_id: int) -> bool: return node_id in self.modified_service_nodes diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index d0fcaed6..f200cd6e 100644 --- a/daemon/core/gui/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -108,7 +108,7 @@ class EmaneModelDialog(Dialog): def click_apply(self): self.config_frame.parse_config() key = (self.model, self.interface) - self.canvas_node.emane_model_configs[key] = dict(self.config) + self.canvas_node.emane_model_configs[key] = self.config self.destroy() diff --git a/daemon/core/gui/dialogs/mobilityconfig.py b/daemon/core/gui/dialogs/mobilityconfig.py index f7192ca4..61cbfc14 100644 --- a/daemon/core/gui/dialogs/mobilityconfig.py +++ b/daemon/core/gui/dialogs/mobilityconfig.py @@ -31,7 +31,9 @@ class MobilityConfigDialog(Dialog): self.config_frame = None self.has_error = False try: - self.config = self.app.core.get_mobility_config(self.node.id) + self.config = self.canvas_node.mobility_config + if not self.config: + self.config = self.app.core.get_mobility_config(self.node.id) self.draw() except grpc.RpcError as e: self.has_error = True @@ -60,5 +62,5 @@ class MobilityConfigDialog(Dialog): def click_apply(self): self.config_frame.parse_config() - self.app.core.mobility_configs[self.node.id] = self.config + self.canvas_node.mobility_config = self.config self.destroy() diff --git a/daemon/core/gui/dialogs/mobilityplayer.py b/daemon/core/gui/dialogs/mobilityplayer.py index b7cbb400..6b7b7869 100644 --- a/daemon/core/gui/dialogs/mobilityplayer.py +++ b/daemon/core/gui/dialogs/mobilityplayer.py @@ -48,8 +48,9 @@ class MobilityPlayer: self.dialog.show() def handle_close(self): - self.dialog.destroy() - self.dialog = None + if self.dialog: + self.dialog.destroy() + self.dialog = None def set_play(self): self.state = MobilityAction.START diff --git a/daemon/core/gui/dialogs/wlanconfig.py b/daemon/core/gui/dialogs/wlanconfig.py index c0c8c845..a62c28ca 100644 --- a/daemon/core/gui/dialogs/wlanconfig.py +++ b/daemon/core/gui/dialogs/wlanconfig.py @@ -32,7 +32,9 @@ class WlanConfigDialog(Dialog): self.ranges = {} self.positive_int = self.app.master.register(self.validate_and_update) try: - self.config = self.app.core.get_wlan_config(self.node.id) + self.config = self.canvas_node.wlan_config + if not self.config: + self.config = self.app.core.get_wlan_config(self.node.id) self.init_draw_range() self.draw() except grpc.RpcError as e: @@ -83,7 +85,7 @@ class WlanConfigDialog(Dialog): retrieve user's wlan configuration and store the new configuration values """ config = self.config_frame.parse_config() - self.app.core.wlan_configs[self.node.id] = self.config + self.canvas_node.wlan_config = self.config if self.app.core.is_runtime(): session_id = self.app.core.session_id self.app.core.client.set_wlan_config(session_id, self.node.id, config) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index c95c1eac..5b337f75 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -305,7 +305,7 @@ class CanvasGraph(tk.Canvas): token = create_edge_token(canvas_node_one.id, canvas_node_two.id) if link.type == core_pb2.LinkType.WIRELESS: - self.add_wireless_edge(canvas_node_one, canvas_node_two) + self.add_wireless_edge(canvas_node_one, canvas_node_two, link) else: if token not in self.edges: src_pos = (node_one.position.x, node_one.position.y) @@ -352,6 +352,7 @@ class CanvasGraph(tk.Canvas): """ Convert window coordinate to canvas coordinate """ + logging.info("event type: %s", type(event)) x = self.canvasx(event.x) y = self.canvasy(event.y) return x, y @@ -421,7 +422,7 @@ class CanvasGraph(tk.Canvas): self.mode = GraphMode.NODE self.selected = None - def handle_edge_release(self, event: tk.Event): + def handle_edge_release(self, _event: tk.Event): edge = self.drawing_edge self.drawing_edge = None @@ -702,7 +703,7 @@ class CanvasGraph(tk.Canvas): else: self.hide_context() - def press_delete(self, event: tk.Event): + def press_delete(self, _event: tk.Event): """ delete selected nodes and any data that relates to it """ @@ -925,6 +926,8 @@ class CanvasGraph(tk.Canvas): # copy configurations node.emane_model_configs = deepcopy(canvas_node.emane_model_configs) + node.wlan_config = deepcopy(canvas_node.wlan_config) + node.mobility_config = deepcopy(canvas_node.mobility_config) # add new node to modified_service_nodes set if that set contains the # to_copy node diff --git a/daemon/core/gui/widgets.py b/daemon/core/gui/widgets.py index 9c07e8c7..bf54bba9 100644 --- a/daemon/core/gui/widgets.py +++ b/daemon/core/gui/widgets.py @@ -181,7 +181,6 @@ class ConfigFrame(ttk.Notebook): option.value = "0" else: option.value = config_value - return {x: self.config[x].value for x in self.config} def set_values(self, config: Dict[str, str]) -> None: From d7ebb90329c17143170f706a675d5915af5065a3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 21 Apr 2020 10:31:20 -0700 Subject: [PATCH 0179/1131] pygui updated node service configurations to be self contained and copyable --- daemon/core/gui/coreclient.py | 54 +++++++++--------------- daemon/core/gui/dialogs/nodeservice.py | 23 ++++------ daemon/core/gui/dialogs/serviceconfig.py | 47 ++++++++++----------- daemon/core/gui/graph/graph.py | 7 +-- daemon/core/gui/graph/node.py | 2 + daemon/core/gui/menubar.py | 1 - 6 files changed, 57 insertions(+), 77 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index c9c5fcc5..e026b44b 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -91,9 +91,7 @@ class CoreClient: self.links = {} self.hooks = {} self.emane_config = None - self.service_configs = {} self.config_service_configs = {} - self.file_configs = {} self.mobility_players = {} self.handling_throughputs = None self.handling_events = None @@ -126,8 +124,6 @@ class CoreClient: self.links.clear() self.hooks.clear() self.emane_config = None - self.service_configs.clear() - self.file_configs.clear() self.modified_service_nodes.clear() for mobility_player in self.mobility_players.values(): mobility_player.handle_close() @@ -332,13 +328,14 @@ class CoreClient: # get service configurations response = self.client.get_node_service_configs(self.session_id) for config in response.configs: - service_configs = self.service_configs.setdefault(config.node_id, {}) - service_configs[config.service] = config.data + canvas_node = self.canvas_nodes[config.node_id] + canvas_node.service_configs[config.service] = config.data logging.debug("service file configs: %s", config.files) for file_name in config.files: - file_configs = self.file_configs.setdefault(config.node_id, {}) - files = file_configs.setdefault(config.service, {}) data = config.files[file_name] + files = canvas_node.service_file_configs.setdefault( + config.service, {} + ) files[file_name] = data # get config service configurations @@ -953,8 +950,13 @@ class CoreClient: def get_service_configs_proto(self) -> List[ServiceConfig]: configs = [] - for node_id, services in self.service_configs.items(): - for name, config in services.items(): + for canvas_node in self.canvas_nodes.values(): + if not NodeUtils.is_container_node(canvas_node.core_node.type): + continue + if not canvas_node.service_configs: + continue + node_id = canvas_node.core_node.id + for name, config in canvas_node.service_configs.items(): config_proto = ServiceConfig( node_id=node_id, service=name, @@ -969,9 +971,14 @@ class CoreClient: def get_service_file_configs_proto(self) -> List[ServiceFileConfig]: configs = [] - for (node_id, file_configs) in self.file_configs.items(): - for service, file_config in file_configs.items(): - for file, data in file_config.items(): + for canvas_node in self.canvas_nodes.values(): + if not NodeUtils.is_container_node(canvas_node.core_node.type): + continue + if not canvas_node.service_file_configs: + continue + node_id = canvas_node.core_node.id + for service, file_configs in canvas_node.service_file_configs.items(): + for file, data in file_configs.items(): config_proto = ServiceFileConfig( node_id=node_id, service=service, file=file, data=data ) @@ -1036,27 +1043,6 @@ class CoreClient: ) return dict(config) - def copy_node_service(self, _from: int, _to: int): - services = self.canvas_nodes[_from].core_node.services - self.canvas_nodes[_to].core_node.services[:] = services - logging.debug("copying node %s service to node %s", _from, _to) - - def copy_node_config(self, src_node: core_pb2.Node, dst_id: int): - node_type = src_node.type - if node_type == core_pb2.NodeType.DEFAULT: - services = src_node.services - dst_node = self.canvas_nodes[dst_id] - dst_node.core_node.services[:] = services - config = self.service_configs.get(src_node.id) - if config: - self.service_configs[dst_id] = config - file_configs = self.file_configs.get(src_node.id) - if file_configs: - for key, value in file_configs.items(): - if dst_id not in self.file_configs: - self.file_configs[dst_id] = {} - self.file_configs[dst_id][key] = value - def service_been_modified(self, node_id: int) -> bool: return node_id in self.modified_service_nodes diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index 691bd331..4927fece 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -135,10 +135,11 @@ class NodeServiceDialog(Dialog): current_selection = self.current.listbox.curselection() if len(current_selection): dialog = ServiceConfigDialog( - master=self, - app=self.app, - service_name=self.current.listbox.get(current_selection[0]), - node_id=self.node_id, + self, + self.app, + self.current.listbox.get(current_selection[0]), + self.canvas_node, + self.node_id, ) # if error occurred when creating ServiceConfigDialog, don't show the dialog @@ -182,14 +183,6 @@ class NodeServiceDialog(Dialog): return def is_custom_service(self, service: str) -> bool: - service_configs = self.app.core.service_configs - file_configs = self.app.core.file_configs - if self.node_id in service_configs and service in service_configs[self.node_id]: - return True - if ( - self.node_id in file_configs - and service in file_configs[self.node_id] - and file_configs[self.node_id][service] - ): - return True - return False + has_service_config = service in self.canvas_node.service_configs + has_file_config = service in self.canvas_node.service_file_configs + return has_service_config or has_file_config diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index 94b71787..038564a3 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -16,22 +16,26 @@ from core.gui.widgets import CodeText, ListboxScroll if TYPE_CHECKING: from core.gui.app import Application + from core.gui.graph.node import CanvasNode class ServiceConfigDialog(Dialog): def __init__( - self, master: Any, app: "Application", service_name: str, node_id: int + self, + master: Any, + app: "Application", + service_name: str, + canvas_node: "CanvasNode", + node_id: int, ): title = f"{service_name} Service" super().__init__(master, app, title, modal=True) self.master = master self.app = app self.core = app.core + self.canvas_node = canvas_node self.node_id = node_id self.service_name = service_name - self.service_configs = app.core.service_configs - self.file_configs = app.core.file_configs - self.radiovar = tk.IntVar() self.radiovar.set(2) self.metadata = "" @@ -54,7 +58,6 @@ class ServiceConfigDialog(Dialog): ImageEnum.DOCUMENTNEW, int(16 * app.app_scale) ) self.editdelete_img = Images.get(ImageEnum.EDITDELETE, int(16 * app.app_scale)) - self.notebook = None self.metadata_entry = None self.filename_combobox = None @@ -70,9 +73,7 @@ class ServiceConfigDialog(Dialog): self.default_config = None self.temp_service_files = {} self.modified_files = set() - self.has_error = False - self.load() if not self.has_error: self.draw() @@ -87,8 +88,8 @@ class ServiceConfigDialog(Dialog): self.default_validate = default_config.validate[:] self.default_shutdown = default_config.shutdown[:] self.default_directories = default_config.dirs[:] - custom_service_config = self.service_configs.get(self.node_id, {}).get( - self.service_name, None + custom_service_config = self.canvas_node.service_configs.get( + self.service_name ) self.default_config = default_config service_config = ( @@ -111,10 +112,11 @@ class ServiceConfigDialog(Dialog): for x in default_config.configs } self.temp_service_files = dict(self.original_service_files) - file_config = self.file_configs.get(self.node_id, {}).get( + + file_configs = self.canvas_node.service_file_configs.get( self.service_name, {} ) - for file, data in file_config.items(): + for file, data in file_configs.items(): self.temp_service_files[file] = data except grpc.RpcError as e: self.has_error = True @@ -449,7 +451,7 @@ class ServiceConfigDialog(Dialog): and not self.has_new_files() and not self.is_custom_directory() ): - self.service_configs.get(self.node_id, {}).pop(self.service_name, None) + self.canvas_node.service_configs.pop(self.service_name, None) self.current_service_color("") self.destroy() return @@ -470,17 +472,13 @@ class ServiceConfigDialog(Dialog): validations=validate, shutdowns=shutdown, ) - if self.node_id not in self.service_configs: - self.service_configs[self.node_id] = {} - self.service_configs[self.node_id][self.service_name] = config + self.canvas_node.service_configs[self.service_name] = config for file in self.modified_files: - if self.node_id not in self.file_configs: - self.file_configs[self.node_id] = {} - if self.service_name not in self.file_configs[self.node_id]: - self.file_configs[self.node_id][self.service_name] = {} - self.file_configs[self.node_id][self.service_name][ - file - ] = self.temp_service_files[file] + file_configs = self.canvas_node.service_file_configs.setdefault( + self.service_name, {} + ) + file_configs[file] = self.temp_service_files[file] + # TODO: check if this is really needed self.app.core.set_node_service_file( self.node_id, self.service_name, file, self.temp_service_files[file] ) @@ -526,8 +524,9 @@ class ServiceConfigDialog(Dialog): clears out any custom configuration permanently """ # clear coreclient data - self.service_configs.get(self.node_id, {}).pop(self.service_name, None) - self.file_configs.get(self.node_id, {}).pop(self.service_name, None) + self.canvas_node.service_configs.pop(self.service_name, None) + file_configs = self.canvas_node.service_file_configs.pop(self.service_name, {}) + file_configs.pop(self.service_name, None) self.temp_service_files = dict(self.original_service_files) self.modified_files.clear() diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 5b337f75..5d66667e 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -352,7 +352,6 @@ class CanvasGraph(tk.Canvas): """ Convert window coordinate to canvas coordinate """ - logging.info("event type: %s", type(event)) x = self.canvasx(event.x) y = self.canvasy(event.y) return x, y @@ -924,10 +923,13 @@ class CanvasGraph(tk.Canvas): ) node = CanvasNode(self.master, scaled_x, scaled_y, copy, canvas_node.image) - # copy configurations + # copy configurations and services + node.core_node.services[:] = canvas_node.core_node.services node.emane_model_configs = deepcopy(canvas_node.emane_model_configs) node.wlan_config = deepcopy(canvas_node.wlan_config) node.mobility_config = deepcopy(canvas_node.mobility_config) + node.service_configs = deepcopy(canvas_node.service_configs) + node.service_file_configs = deepcopy(canvas_node.service_file_configs) # add new node to modified_service_nodes set if that set contains the # to_copy node @@ -937,7 +939,6 @@ class CanvasGraph(tk.Canvas): copy_map[canvas_node.id] = node.id self.core.canvas_nodes[copy.id] = node self.nodes[node.id] = node - self.core.copy_node_config(core_node, copy.id) for edge in canvas_node.edges: if edge.src not in self.to_copy or edge.dst not in self.to_copy: if canvas_node.id == edge.src: diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 63a85ad7..e788b584 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -62,6 +62,8 @@ class CanvasNode: self.emane_model_configs = {} self.wlan_config = {} self.mobility_config = {} + self.service_configs = {} + self.service_file_configs = {} self.setup_bindings() def setup_bindings(self): diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 5f85b91f..b445845a 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -347,7 +347,6 @@ class Menubar(tk.Menu): for i in range(self.edit_menu.index(tk.END) + 1): try: label = self.edit_menu.entrycget(i, "label") - logging.info("menu label: %s", label) if label not in labels: continue state = tk.DISABLED if is_runtime else tk.NORMAL From b04da98f4480aefc5f8fb9880a8c1b38cab88b32 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 21 Apr 2020 11:13:41 -0700 Subject: [PATCH 0180/1131] pygui updated config services to be associated with nodes directly and copyable --- daemon/core/gui/coreclient.py | 16 +++++---- .../core/gui/dialogs/configserviceconfig.py | 32 ++++++++++------- daemon/core/gui/dialogs/nodeconfigservice.py | 36 +++++++++---------- daemon/core/gui/graph/graph.py | 2 ++ daemon/core/gui/graph/node.py | 1 + 5 files changed, 49 insertions(+), 38 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index e026b44b..7c69f954 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -91,7 +91,6 @@ class CoreClient: self.links = {} self.hooks = {} self.emane_config = None - self.config_service_configs = {} self.mobility_players = {} self.handling_throughputs = None self.handling_events = None @@ -341,10 +340,10 @@ class CoreClient: # get config service configurations response = self.client.get_node_config_service_configs(self.session_id) for config in response.configs: - node_configs = self.config_service_configs.setdefault( - config.node_id, {} + canvas_node = self.canvas_nodes[config.node_id] + service_config = canvas_node.config_service_configs.setdefault( + config.name, {} ) - service_config = node_configs.setdefault(config.name, {}) if config.templates: service_config["templates"] = config.templates if config.config: @@ -989,8 +988,13 @@ class CoreClient: self ) -> List[configservices_pb2.ConfigServiceConfig]: config_service_protos = [] - for node_id, node_config in self.config_service_configs.items(): - for name, service_config in node_config.items(): + for canvas_node in self.canvas_nodes.values(): + if not NodeUtils.is_container_node(canvas_node.core_node.type): + continue + if not canvas_node.config_service_configs: + continue + node_id = canvas_node.core_node.id + for name, service_config in canvas_node.config_service_configs.items(): config = service_config.get("config", {}) config_proto = configservices_pb2.ConfigServiceConfig( node_id=node_id, diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py index 36c4ccfe..2239034e 100644 --- a/daemon/core/gui/dialogs/configserviceconfig.py +++ b/daemon/core/gui/dialogs/configserviceconfig.py @@ -16,21 +16,26 @@ from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll if TYPE_CHECKING: from core.gui.app import Application + from core.gui.graph.node import CanvasNode class ConfigServiceConfigDialog(Dialog): def __init__( - self, master: Any, app: "Application", service_name: str, node_id: int + self, + master: Any, + app: "Application", + service_name: str, + canvas_node: "CanvasNode", + node_id: int, ): title = f"{service_name} Config Service" super().__init__(master, app, title, modal=True) self.master = master self.app = app self.core = app.core + self.canvas_node = canvas_node self.node_id = node_id self.service_name = service_name - self.service_configs = app.core.config_service_configs - self.radiovar = tk.IntVar() self.radiovar.set(2) self.directories = [] @@ -95,9 +100,9 @@ class ConfigServiceConfigDialog(Dialog): self.modes = sorted(x.name for x in response.modes) self.mode_configs = {x.name: x.config for x in response.modes} - node_configs = self.service_configs.get(self.node_id, {}) - service_config = node_configs.get(self.service_name, {}) - + service_config = self.canvas_node.config_service_configs.get( + self.service_name, {} + ) self.config = response.config self.default_config = {x.name: x.value for x in self.config.values()} custom_config = service_config.get("config") @@ -313,15 +318,15 @@ class ConfigServiceConfigDialog(Dialog): def click_apply(self): current_listbox = self.master.current.listbox if not self.is_custom(): - if self.node_id in self.service_configs: - self.service_configs[self.node_id].pop(self.service_name, None) + self.canvas_node.config_service_configs.pop(self.service_name, None) current_listbox.itemconfig(current_listbox.curselection()[0], bg="") self.destroy() return try: - node_config = self.service_configs.setdefault(self.node_id, {}) - service_config = node_config.setdefault(self.service_name, {}) + service_config = self.canvas_node.config_service_configs.setdefault( + self.service_name, {} + ) if self.config_frame: self.config_frame.parse_config() service_config["config"] = { @@ -365,9 +370,10 @@ class ConfigServiceConfigDialog(Dialog): return has_custom_templates or has_custom_config def click_defaults(self): - if self.node_id in self.service_configs: - node_config = self.service_configs.get(self.node_id, {}) - node_config.pop(self.service_name, None) + self.canvas_node.config_service_configs.pop(self.service_name, None) + logging.info( + "cleared config service config: %s", self.canvas_node.config_service_configs + ) self.temp_service_files = dict(self.original_service_files) filename = self.templates_combobox.get() self.template_text.text.delete(1.0, "end") diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeconfigservice.py index 8bdbc539..c86d8887 100644 --- a/daemon/core/gui/dialogs/nodeconfigservice.py +++ b/daemon/core/gui/dialogs/nodeconfigservice.py @@ -70,12 +70,10 @@ class NodeConfigServiceDialog(Dialog): label_frame.grid(row=0, column=2, sticky="nsew") label_frame.rowconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1) + self.current = ListboxScroll(label_frame) self.current.grid(sticky="nsew") - for service in sorted(self.current_services): - self.current.listbox.insert(tk.END, service) - if self.is_custom_service(service): - self.current.listbox.itemconfig(tk.END, bg="green") + self.draw_current_services() frame = ttk.Frame(self.top) frame.grid(stick="ew") @@ -108,24 +106,22 @@ class NodeConfigServiceDialog(Dialog): self.current_services.add(name) elif not var.get() and name in self.current_services: self.current_services.remove(name) - self.current.listbox.delete(0, tk.END) - for name in sorted(self.current_services): - self.current.listbox.insert(tk.END, name) - if self.is_custom_service(name): - self.current.listbox.itemconfig(tk.END, bg="green") + self.draw_current_services() self.canvas_node.core_node.config_services[:] = self.current_services def click_configure(self): current_selection = self.current.listbox.curselection() if len(current_selection): dialog = ConfigServiceConfigDialog( - master=self, - app=self.app, - service_name=self.current.listbox.get(current_selection[0]), - node_id=self.node_id, + self, + self.app, + self.current.listbox.get(current_selection[0]), + self.canvas_node, + self.node_id, ) if not dialog.has_error: dialog.show() + self.draw_current_services() else: messagebox.showinfo( "Config Service Configuration", @@ -133,6 +129,13 @@ class NodeConfigServiceDialog(Dialog): parent=self, ) + def draw_current_services(self): + self.current.listbox.delete(0, tk.END) + for name in sorted(self.current_services): + self.current.listbox.insert(tk.END, name) + if self.is_custom_service(name): + self.current.listbox.itemconfig(tk.END, bg="green") + def click_save(self): self.canvas_node.core_node.config_services[:] = self.current_services logging.info( @@ -156,9 +159,4 @@ class NodeConfigServiceDialog(Dialog): return def is_custom_service(self, service: str) -> bool: - node_configs = self.app.core.config_service_configs.get(self.node_id, {}) - service_config = node_configs.get(service) - if node_configs and service_config: - return True - else: - return False + return service in self.canvas_node.config_service_configs diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 5d66667e..e4963f45 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -925,11 +925,13 @@ class CanvasGraph(tk.Canvas): # copy configurations and services node.core_node.services[:] = canvas_node.core_node.services + node.core_node.config_services[:] = canvas_node.core_node.config_services node.emane_model_configs = deepcopy(canvas_node.emane_model_configs) node.wlan_config = deepcopy(canvas_node.wlan_config) node.mobility_config = deepcopy(canvas_node.mobility_config) node.service_configs = deepcopy(canvas_node.service_configs) node.service_file_configs = deepcopy(canvas_node.service_file_configs) + node.config_service_configs = deepcopy(canvas_node.config_service_configs) # add new node to modified_service_nodes set if that set contains the # to_copy node diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index e788b584..90896284 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -64,6 +64,7 @@ class CanvasNode: self.mobility_config = {} self.service_configs = {} self.service_file_configs = {} + self.config_service_configs = {} self.setup_bindings() def setup_bindings(self): From 219218eebce364a4d2f5d9ac9725572ae2c8fb9d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 21 Apr 2020 13:06:14 -0700 Subject: [PATCH 0181/1131] updated sample8 ipsec imn due to outdated format --- gui/configs/sample8-ipsec-service.imn | 34 +-------------------------- 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/gui/configs/sample8-ipsec-service.imn b/gui/configs/sample8-ipsec-service.imn index ba409185..fb82881f 100644 --- a/gui/configs/sample8-ipsec-service.imn +++ b/gui/configs/sample8-ipsec-service.imn @@ -345,13 +345,7 @@ node n1 { custom-config-id service:IPsec custom-command IPsec config { - - ('ipsec.sh', 'test1.key', 'test1.pem', 'ca-cert.pem', 'copycerts.sh', ) - 60 - ('sh copycerts.sh', 'sh ipsec.sh', ) - ('killall racoon', ) - - + files=('ipsec.sh', 'test1.key', 'test1.pem', 'ca-cert.pem', 'copycerts.sh', ) } } services {zebra OSPFv2 OSPFv3 IPForward IPsec} @@ -513,19 +507,6 @@ node n2 { echo "running racoon -d -f $PWD/racoon.conf..." racoon -d -f $PWD/racoon.conf -l racoon.log - } - } - custom-config { - custom-config-id service:IPsec - custom-command IPsec - config { - - ('ipsec.sh', ) - 60 - ('sh ipsec.sh', ) - ('killall racoon', ) - - } } services {zebra OSPFv2 OSPFv3 IPForward IPsec} @@ -682,19 +663,6 @@ node n3 { echo "running racoon -d -f $PWD/racoon.conf..." racoon -d -f $PWD/racoon.conf -l racoon.log - } - } - custom-config { - custom-config-id service:IPsec - custom-command IPsec - config { - - ('ipsec.sh', ) - 60 - ('sh ipsec.sh', ) - ('killall racoon', ) - - } } services {zebra OSPFv2 OSPFv3 IPForward IPsec} From ec8a15794beb9e993672cab784b55aa48fca3ab2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 21 Apr 2020 13:11:00 -0700 Subject: [PATCH 0182/1131] pygui fixed wlan drawing range circles using the diameter for the radius --- daemon/core/gui/dialogs/wlanconfig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/gui/dialogs/wlanconfig.py b/daemon/core/gui/dialogs/wlanconfig.py index a62c28ca..a777c7d4 100644 --- a/daemon/core/gui/dialogs/wlanconfig.py +++ b/daemon/core/gui/dialogs/wlanconfig.py @@ -104,7 +104,7 @@ class WlanConfigDialog(Dialog): if len(s) == 0: return True try: - int_value = int(s) + int_value = int(s) / 2 if int_value >= 0: net_range = int_value * self.canvas.ratio if self.canvas_node.id in self.canvas.wireless_network: From 715bae6f7484722177176c5c64bd534b515b288e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 21 Apr 2020 13:14:33 -0700 Subject: [PATCH 0183/1131] pygui avoid sending configs for empty mobility and wlan configurations --- daemon/core/gui/coreclient.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 7c69f954..b526a99b 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -911,6 +911,8 @@ class CoreClient: for canvas_node in self.canvas_nodes.values(): if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: continue + if not canvas_node.wlan_config: + continue config = canvas_node.wlan_config config = {x: config[x].value for x in config} node_id = canvas_node.core_node.id @@ -923,6 +925,8 @@ class CoreClient: for canvas_node in self.canvas_nodes.values(): if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: continue + if not canvas_node.mobility_config: + continue config = canvas_node.mobility_config config = {x: config[x].value for x in config} node_id = canvas_node.core_node.id From 20ecdf70d05b08e54958ffaed0011be82c28d9f2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 21 Apr 2020 13:22:21 -0700 Subject: [PATCH 0184/1131] pygui fixed emane link updates when rejoining session --- daemon/core/gui/graph/graph.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index e4963f45..40a941b1 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -268,8 +268,11 @@ class CanvasGraph(tk.Canvas): return network_id = link.network_id if link.network_id else None token = create_edge_token(src.id, dst.id, network_id) - edge = self.wireless_edges[token] - edge.middle_label_text(link.label) + if token not in self.wireless_edges: + self.add_wireless_edge(src, dst, link) + else: + edge = self.wireless_edges[token] + edge.middle_label_text(link.label) def draw_session(self, session: core_pb2.Session): """ From ba6a6f06b174607d221fbd9b12de701fa5b40fef Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 21 Apr 2020 22:56:56 -0700 Subject: [PATCH 0185/1131] pygui moved observers to menu class, added initial functioning ip address tool --- daemon/core/gui/appconfig.py | 14 ++- daemon/core/gui/coreclient.py | 11 -- daemon/core/gui/dialogs/ipdialog.py | 153 +++++++++++++++++++++++++++ daemon/core/gui/dialogs/macdialog.py | 49 +++++++++ daemon/core/gui/interface.py | 25 ++--- daemon/core/gui/menubar.py | 26 ++++- daemon/core/gui/widgets.py | 5 +- 7 files changed, 254 insertions(+), 29 deletions(-) create mode 100644 daemon/core/gui/dialogs/ipdialog.py create mode 100644 daemon/core/gui/dialogs/macdialog.py diff --git a/daemon/core/gui/appconfig.py b/daemon/core/gui/appconfig.py index 11265857..36a5d1b7 100644 --- a/daemon/core/gui/appconfig.py +++ b/daemon/core/gui/appconfig.py @@ -37,6 +37,10 @@ TERMINALS = { "gnome-terminal": "gnome-terminal --window --", } EDITORS = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"] +DEFAULT_IP4S = ["10.0.0.0", "192.168.0.0", "172.16.0.0"] +DEFAULT_IP4 = DEFAULT_IP4S[0] +DEFAULT_IP6S = ["2001::", "2002::", "a::"] +DEFAULT_IP6 = DEFAULT_IP6S[0] class IndentDumper(yaml.Dumper): @@ -98,11 +102,17 @@ def check_directory(): "alt": 2.0, "scale": 150.0, }, - "servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}], + "servers": [], "nodes": [], "recentfiles": [], - "observers": [{"name": "hello", "cmd": "echo hello"}], + "observers": [], "scale": 1.0, + "ips": { + "ip4": DEFAULT_IP4, + "ip6": DEFAULT_IP6, + "ip4s": DEFAULT_IP4S, + "ip6s": DEFAULT_IP6S, + }, } save(config) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index b526a99b..5cc822de 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -31,17 +31,6 @@ if TYPE_CHECKING: from core.gui.app import Application GUI_SOURCE = "gui" -OBSERVERS = { - "List Processes": "ps", - "Show Interfaces": "ip address", - "IPV4 Routes": "ip -4 ro", - "IPV6 Routes": "ip -6 ro", - "Listening Sockets": "netstat -tuwnl", - "IPv4 MFC Entries": "ip -4 mroute show", - "IPv6 MFC Entries": "ip -6 mroute show", - "Firewall Rules": "iptables -L", - "IPSec Policies": "setkey -DP", -} class CoreServer: diff --git a/daemon/core/gui/dialogs/ipdialog.py b/daemon/core/gui/dialogs/ipdialog.py new file mode 100644 index 00000000..58c06fb2 --- /dev/null +++ b/daemon/core/gui/dialogs/ipdialog.py @@ -0,0 +1,153 @@ +import tkinter as tk +from tkinter import messagebox, ttk +from typing import TYPE_CHECKING + +import netaddr + +from core.gui import appconfig +from core.gui.dialogs.dialog import Dialog +from core.gui.themes import FRAME_PAD, PADX, PADY +from core.gui.widgets import ListboxScroll + +if TYPE_CHECKING: + from core.gui.app import Application + + +class IpConfigDialog(Dialog): + def __init__(self, master: "Application", app: "Application") -> None: + super().__init__(master, app, "IP Configuration", modal=True) + ip_config = self.app.guiconfig.setdefault("ips") + self.ip4 = ip_config.setdefault("ip4", appconfig.DEFAULT_IP4) + self.ip6 = ip_config.setdefault("ip6", appconfig.DEFAULT_IP6) + self.ip4s = ip_config.setdefault("ip4s", appconfig.DEFAULT_IP4S) + self.ip6s = ip_config.setdefault("ip6s", appconfig.DEFAULT_IP6S) + self.ip4_entry = None + self.ip4_listbox = None + self.ip6_entry = None + self.ip6_listbox = None + self.draw() + + def draw(self) -> None: + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + + # draw ip4 and ip6 lists + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.rowconfigure(0, weight=1) + frame.grid(sticky="nsew", pady=PADY) + + ip4_frame = ttk.LabelFrame(frame, text="IPv4", padding=FRAME_PAD) + ip4_frame.columnconfigure(0, weight=1) + ip4_frame.rowconfigure(0, weight=1) + ip4_frame.grid(row=0, column=0, stick="nsew") + self.ip4_listbox = ListboxScroll(ip4_frame) + self.ip4_listbox.listbox.bind("<>", self.select_ip4) + self.ip4_listbox.grid(sticky="nsew", pady=PADY) + for index, ip4 in enumerate(self.ip4s): + self.ip4_listbox.listbox.insert(tk.END, ip4) + if self.ip4 == ip4: + self.ip4_listbox.listbox.select_set(index) + self.ip4_entry = ttk.Entry(ip4_frame) + self.ip4_entry.grid(sticky="ew", pady=PADY) + ip4_button_frame = ttk.Frame(ip4_frame) + ip4_button_frame.columnconfigure(0, weight=1) + ip4_button_frame.columnconfigure(1, weight=1) + ip4_button_frame.grid(sticky="ew") + ip4_add = ttk.Button(ip4_button_frame, text="Add", command=self.click_add_ip4) + ip4_add.grid(row=0, column=0, sticky="ew") + ip4_del = ttk.Button( + ip4_button_frame, text="Delete", command=self.click_del_ip4 + ) + ip4_del.grid(row=0, column=1, sticky="ew") + + ip6_frame = ttk.LabelFrame(frame, text="IPv6", padding=FRAME_PAD) + ip6_frame.columnconfigure(0, weight=1) + ip6_frame.rowconfigure(0, weight=1) + ip6_frame.grid(row=0, column=1, stick="nsew") + self.ip6_listbox = ListboxScroll(ip6_frame) + self.ip6_listbox.listbox.bind("<>", self.select_ip6) + self.ip6_listbox.grid(sticky="nsew", pady=PADY) + for index, ip6 in enumerate(self.ip6s): + self.ip6_listbox.listbox.insert(tk.END, ip6) + if self.ip6 == ip6: + self.ip6_listbox.listbox.select_set(index) + self.ip6_entry = ttk.Entry(ip6_frame) + self.ip6_entry.grid(sticky="ew", pady=PADY) + ip6_button_frame = ttk.Frame(ip6_frame) + ip6_button_frame.columnconfigure(0, weight=1) + ip6_button_frame.columnconfigure(1, weight=1) + ip6_button_frame.grid(sticky="ew") + ip6_add = ttk.Button(ip6_button_frame, text="Add", command=self.click_add_ip6) + ip6_add.grid(row=0, column=0, sticky="ew") + ip6_del = ttk.Button( + ip6_button_frame, text="Delete", command=self.click_del_ip6 + ) + ip6_del.grid(row=0, column=1, sticky="ew") + + # draw buttons + frame = ttk.Frame(self.top) + frame.grid(stick="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Save", command=self.click_save) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_add_ip4(self) -> None: + ip4 = self.ip4_entry.get() + if not ip4 or not netaddr.valid_ipv4(ip4): + messagebox.showerror("IPv4 Error", f"Invalid IPv4 {ip4}") + else: + self.ip4_listbox.listbox.insert(tk.END, ip4) + + def click_del_ip4(self) -> None: + if self.ip4_listbox.listbox.size() == 1: + messagebox.showerror("IPv4 Error", "Must have at least one address") + else: + selection = self.ip4_listbox.listbox.curselection() + self.ip4_listbox.listbox.delete(selection) + self.ip4_listbox.listbox.select_set(0) + + def click_add_ip6(self) -> None: + ip6 = self.ip6_entry.get() + if not ip6 or not netaddr.valid_ipv6(ip6): + messagebox.showerror("IPv6 Error", f"Invalid IPv6 {ip6}") + else: + self.ip6_listbox.listbox.insert(tk.END, ip6) + + def click_del_ip6(self) -> None: + if self.ip6_listbox.listbox.size() == 1: + messagebox.showerror("IPv6 Error", "Must have at least one address") + else: + selection = self.ip6_listbox.listbox.curselection() + self.ip6_listbox.listbox.delete(selection) + self.ip6_listbox.listbox.select_set(0) + + def select_ip4(self, _event: tk.Event) -> None: + selection = self.ip4_listbox.listbox.curselection() + self.ip4 = self.ip4_listbox.listbox.get(selection) + + def select_ip6(self, _event: tk.Event) -> None: + selection = self.ip6_listbox.listbox.curselection() + self.ip6 = self.ip6_listbox.listbox.get(selection) + + def click_save(self) -> None: + ip4s = [] + for index in range(self.ip4_listbox.listbox.size()): + ip4 = self.ip4_listbox.listbox.get(index) + ip4s.append(ip4) + ip6s = [] + for index in range(self.ip6_listbox.listbox.size()): + ip6 = self.ip6_listbox.listbox.get(index) + ip6s.append(ip6) + ip_config = self.app.guiconfig["ips"] + ip_config["ip4"] = self.ip4 + ip_config["ip6"] = self.ip6 + ip_config["ip4s"] = ip4s + ip_config["ip6s"] = ip6s + self.app.core.interfaces_manager.update_ips(self.ip4, self.ip6) + self.app.save_config() + self.destroy() diff --git a/daemon/core/gui/dialogs/macdialog.py b/daemon/core/gui/dialogs/macdialog.py new file mode 100644 index 00000000..a880e204 --- /dev/null +++ b/daemon/core/gui/dialogs/macdialog.py @@ -0,0 +1,49 @@ +from tkinter import ttk +from typing import TYPE_CHECKING + +from core.gui.dialogs.dialog import Dialog +from core.gui.themes import PADX, PADY + +if TYPE_CHECKING: + from core.gui.app import Application + + +class MacConfigDialog(Dialog): + def __init__(self, master: "Application", app: "Application") -> None: + super().__init__(master, app, "MAC Configuration", modal=True) + self.draw() + + def draw(self) -> None: + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + + # draw explanation label + text = ( + "MAC addresses will be generated for nodes starting with the\n" + "provided value below and increment by value in order." + ) + label = ttk.Label(self.top, text=text) + label.grid(sticky="ew", pady=PADY) + + # draw input + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=3) + frame.grid(stick="ew", pady=PADY) + label = ttk.Label(frame, text="Starting MAC") + label.grid(row=0, column=0, sticky="ew", padx=PADX) + entry = ttk.Entry(frame) + entry.grid(row=0, column=1, sticky="ew") + + # draw buttons + frame = ttk.Frame(self.top) + frame.grid(stick="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Save", command=self.click_save) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_save(self) -> None: + pass diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index fadabec4..b5c24fc4 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -32,21 +32,22 @@ class Subnets: class InterfaceManager: - def __init__( - self, - app: "Application", - ip4: str = "10.0.0.0", - ip4_mask: int = 24, - ip6: str = "2001::", - ip6_mask=64, - ) -> None: + def __init__(self, app: "Application") -> None: self.app = app - self.ip4_mask = ip4_mask - self.ip6_mask = ip6_mask - self.ip4_subnets = IPNetwork(f"{ip4}/{ip4_mask}") - self.ip6_subnets = IPNetwork(f"{ip6}/{ip6_mask}") + ip_config = self.app.guiconfig.get("ips", {}) + ip4 = ip_config.get("ip4", "10.0.0.0") + ip6 = ip_config.get("ip6", "2001::") + self.ip4_mask = 24 + self.ip6_mask = 64 + self.ip4_subnets = IPNetwork(f"{ip4}/{self.ip4_mask}") + self.ip6_subnets = IPNetwork(f"{ip6}/{self.ip6_mask}") self.current_subnets = None + def update_ips(self, ip4: str, ip6: str) -> None: + self.reset() + self.ip4_subnets = IPNetwork(f"{ip4}/{self.ip4_mask}") + self.ip6_subnets = IPNetwork(f"{ip6}/{self.ip6_mask}") + def next_subnets(self) -> Subnets: # define currently used subnets used_subnets = set() diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index b445845a..6373b042 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -7,12 +7,13 @@ from tkinter import filedialog, messagebox from typing import TYPE_CHECKING from core.gui.appconfig import XMLS_PATH -from core.gui.coreclient import OBSERVERS from core.gui.dialogs.about import AboutDialog from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog from core.gui.dialogs.canvaswallpaper import CanvasWallpaperDialog from core.gui.dialogs.executepython import ExecutePythonDialog from core.gui.dialogs.hooks import HooksDialog +from core.gui.dialogs.ipdialog import IpConfigDialog +from core.gui.dialogs.macdialog import MacConfigDialog from core.gui.dialogs.observers import ObserverDialog from core.gui.dialogs.preferences import PreferencesDialog from core.gui.dialogs.servers import ServersDialog @@ -26,6 +27,17 @@ if TYPE_CHECKING: from core.gui.app import Application MAX_FILES = 3 +OBSERVERS = { + "List Processes": "ps", + "Show Interfaces": "ip address", + "IPV4 Routes": "ip -4 ro", + "IPV6 Routes": "ip -6 ro", + "Listening Sockets": "netstat -tuwnl", + "IPv4 MFC Entries": "ip -4 mroute show", + "IPv6 MFC Entries": "ip -6 mroute show", + "Firewall Rules": "iptables -L", + "IPSec Policies": "setkey -DP", +} class Menubar(tk.Menu): @@ -173,8 +185,8 @@ class Menubar(tk.Menu): """ menu = tk.Menu(self) menu.add_command(label="Auto Grid", command=self.click_autogrid) - menu.add_command(label="IP Addresses", state=tk.DISABLED) - menu.add_command(label="MAC Addresses", state=tk.DISABLED) + menu.add_command(label="IP Addresses", command=self.click_ip_config) + menu.add_command(label="MAC Addresses", command=self.click_mac_config) self.add_cascade(label="Tools", menu=menu) def create_observer_widgets_menu(self, widget_menu: tk.Menu) -> None: @@ -465,3 +477,11 @@ class Menubar(tk.Menu): def click_edge_label_change(self) -> None: for edge in self.canvas.edges.values(): edge.draw_labels() + + def click_mac_config(self) -> None: + dialog = MacConfigDialog(self.app, self.app) + dialog.show() + + def click_ip_config(self) -> None: + dialog = IpConfigDialog(self.app, self.app) + dialog.show() diff --git a/daemon/core/gui/widgets.py b/daemon/core/gui/widgets.py index bf54bba9..5750e286 100644 --- a/daemon/core/gui/widgets.py +++ b/daemon/core/gui/widgets.py @@ -203,7 +203,10 @@ class ListboxScroll(ttk.Frame): self.scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL) self.scrollbar.grid(row=0, column=1, sticky="ns") self.listbox = tk.Listbox( - self, selectmode=tk.SINGLE, yscrollcommand=self.scrollbar.set + self, + selectmode=tk.BROWSE, + yscrollcommand=self.scrollbar.set, + exportselection=False, ) themes.style_listbox(self.listbox) self.listbox.grid(row=0, column=0, sticky="nsew") From f521fe4141a8fc55482b4e0ff52db0ef12383e00 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 22 Apr 2020 00:20:18 -0700 Subject: [PATCH 0186/1131] fixed issue where actually sending interface names to tcl gui would cause issue, no longer sending link interfave names --- daemon/core/api/tlv/corehandlers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 54016a40..76bc16fe 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -367,14 +367,12 @@ class CoreHandler(socketserver.BaseRequestHandler): (LinkTlvs.NETWORK_ID, link_data.network_id), (LinkTlvs.KEY, link_data.key), (LinkTlvs.INTERFACE1_NUMBER, link_data.interface1_id), - (LinkTlvs.INTERFACE1_NAME, link_data.interface1_name), (LinkTlvs.INTERFACE1_IP4, link_data.interface1_ip4), (LinkTlvs.INTERFACE1_IP4_MASK, link_data.interface1_ip4_mask), (LinkTlvs.INTERFACE1_MAC, link_data.interface1_mac), (LinkTlvs.INTERFACE1_IP6, link_data.interface1_ip6), (LinkTlvs.INTERFACE1_IP6_MASK, link_data.interface1_ip6_mask), (LinkTlvs.INTERFACE2_NUMBER, link_data.interface2_id), - (LinkTlvs.INTERFACE2_NAME, link_data.interface2_name), (LinkTlvs.INTERFACE2_IP4, link_data.interface2_ip4), (LinkTlvs.INTERFACE2_IP4_MASK, link_data.interface2_ip4_mask), (LinkTlvs.INTERFACE2_MAC, link_data.interface2_mac), From 039cf2a3b9bcefb68ab149146c6ca7bf1f1be3d0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 22 Apr 2020 11:37:58 -0700 Subject: [PATCH 0187/1131] pygui updates to properly dynamically update the observer widgets menu as changes are made --- daemon/core/gui/dialogs/observers.py | 2 ++ daemon/core/gui/menubar.py | 32 +++++++++++++++++----------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/daemon/core/gui/dialogs/observers.py b/daemon/core/gui/dialogs/observers.py index 9fe3f79e..dd26b505 100644 --- a/daemon/core/gui/dialogs/observers.py +++ b/daemon/core/gui/dialogs/observers.py @@ -104,6 +104,7 @@ class ObserverDialog(Dialog): observer = Observer(name, cmd) self.app.core.custom_observers[name] = observer self.observers.insert(tk.END, name) + self.app.menubar.draw_custom_observers() def click_save(self): name = self.name.get() @@ -129,6 +130,7 @@ class ObserverDialog(Dialog): self.observers.selection_clear(0, tk.END) self.save_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED) + self.app.menubar.draw_custom_observers() def handle_observer_change(self, event: tk.Event): selection = self.observers.curselection() diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 6373b042..74a93df7 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -56,6 +56,9 @@ class Menubar(tk.Menu): self.canvas = app.canvas self.recent_menu = None self.edit_menu = None + self.observers_menu = None + self.observers_var = tk.StringVar(value=tk.NONE) + self.observers_custom_index = None self.draw() def draw(self) -> None: @@ -193,36 +196,41 @@ class Menubar(tk.Menu): """ Create observer widget menu item and create the sub menu items inside """ - var = tk.StringVar(value="none") - menu = tk.Menu(widget_menu) - menu.var = var - menu.add_command( + self.observers_menu = tk.Menu(widget_menu) + self.observers_menu.add_command( label="Edit Observers", command=self.click_edit_observer_widgets ) - menu.add_separator() - menu.add_radiobutton( + self.observers_menu.add_separator() + self.observers_menu.add_radiobutton( label="None", - variable=var, + variable=self.observers_var, value="none", command=lambda: self.core.set_observer(None), ) for name in sorted(OBSERVERS): cmd = OBSERVERS[name] - menu.add_radiobutton( + self.observers_menu.add_radiobutton( label=name, - variable=var, + variable=self.observers_var, value=name, command=partial(self.core.set_observer, cmd), ) + self.observers_custom_index = self.observers_menu.index(tk.END) + 1 + self.draw_custom_observers() + widget_menu.add_cascade(label="Observer Widgets", menu=self.observers_menu) + + def draw_custom_observers(self) -> None: + current_observers_index = self.observers_menu.index(tk.END) + 1 + if self.observers_custom_index < current_observers_index: + self.observers_menu.delete(self.observers_custom_index, tk.END) for name in sorted(self.core.custom_observers): observer = self.core.custom_observers[name] - menu.add_radiobutton( + self.observers_menu.add_radiobutton( label=name, - variable=var, + variable=self.observers_var, value=name, command=partial(self.core.set_observer, observer.cmd), ) - widget_menu.add_cascade(label="Observer Widgets", menu=menu) def create_adjacency_menu(self, widget_menu: tk.Menu) -> None: """ From 6fe2845051bcfcd5ae3828551cda360ecfcd0d2a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 22 Apr 2020 11:41:09 -0700 Subject: [PATCH 0188/1131] pygui added error dialog for duplicate observer names and cleared out values on success --- daemon/core/gui/dialogs/observers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/daemon/core/gui/dialogs/observers.py b/daemon/core/gui/dialogs/observers.py index dd26b505..51e9fe88 100644 --- a/daemon/core/gui/dialogs/observers.py +++ b/daemon/core/gui/dialogs/observers.py @@ -1,5 +1,5 @@ import tkinter as tk -from tkinter import ttk +from tkinter import messagebox, ttk from typing import TYPE_CHECKING from core.gui.coreclient import Observer @@ -104,7 +104,11 @@ class ObserverDialog(Dialog): observer = Observer(name, cmd) self.app.core.custom_observers[name] = observer self.observers.insert(tk.END, name) + self.name.set("") + self.cmd.set("") self.app.menubar.draw_custom_observers() + else: + messagebox.showerror("Observer Error", f"{name} already exists") def click_save(self): name = self.name.get() From 3394f0240ab75363b5f3ba9102872528e61610b7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 22 Apr 2020 12:07:42 -0700 Subject: [PATCH 0189/1131] update reading session xml options to updating these values instead of clearing out existing settings, avoids issue wiping ovs settings etc --- daemon/core/xml/corexml.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index d3d4b94b..6ad210b8 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -638,14 +638,14 @@ class CoreXmlReader: session_options = self.scenario.find("session_options") if session_options is None: return - - configs = {} - for config in session_options.iterchildren(): - name = config.get("name") - value = config.get("value") - configs[name] = value - logging.info("reading session options: %s", configs) - self.session.options.set_configs(configs) + xml_config = {} + for configuration in session_options.iterchildren(): + name = configuration.get("name") + value = configuration.get("value") + xml_config[name] = value + logging.info("reading session options: %s", xml_config) + config = self.session.options.get_configs() + config.update(xml_config) def read_session_hooks(self) -> None: session_hooks = self.scenario.find("session_hooks") From 03e291d215cbdebf497c24412e688e6f00927dd5 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 22 Apr 2020 15:38:29 -0700 Subject: [PATCH 0190/1131] implement run tool that allows running command on more than one node conveniently --- Pipfile | 11 +++ daemon/core/gui/dialogs/runtool.py | 123 +++++++++++++++++++++++++++++ daemon/core/gui/toolbar.py | 3 + 3 files changed, 137 insertions(+) create mode 100644 Pipfile create mode 100644 daemon/core/gui/dialogs/runtool.py diff --git a/Pipfile b/Pipfile new file mode 100644 index 00000000..b723d019 --- /dev/null +++ b/Pipfile @@ -0,0 +1,11 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] + +[requires] +python_version = "3.7" diff --git a/daemon/core/gui/dialogs/runtool.py b/daemon/core/gui/dialogs/runtool.py new file mode 100644 index 00000000..8d9d9cb0 --- /dev/null +++ b/daemon/core/gui/dialogs/runtool.py @@ -0,0 +1,123 @@ +import tkinter as tk +from tkinter import ttk + +from core.api.grpc import core_pb2 +from core.gui.dialogs.dialog import Dialog +from core.gui.themes import FRAME_PAD, PADY +from core.gui.widgets import CodeText, ListboxScroll + + +class RunToolDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Run Tool", modal=True) + + self.cmd = tk.StringVar(value="ps ax") + self.app = app + self.result = None + self.node_list = None + self.executable_nodes = {} + + self.store_nodes() + self.draw() + + def store_nodes(self): + """ + store all CORE nodes (nodes that execute commands) from all existing nodes + """ + for nid, node in self.app.core.canvas_nodes.items(): + if node.core_node.type == core_pb2.NodeType.DEFAULT: + self.executable_nodes[node.core_node.name] = nid + + def draw(self): + self.top.rowconfigure(0, weight=1) + self.top.columnconfigure(0, weight=5) + self.top.columnconfigure(1, weight=1) + self.draw_command_frame() + self.draw_nodes_frame() + return + + def draw_command_frame(self): + # the main frame + frame = ttk.Frame(self.top) + frame.grid(row=0, column=0, sticky="nsew") + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + frame.rowconfigure(1, weight=1) + + labeled_frame = ttk.LabelFrame(frame, text="Command line", padding=FRAME_PAD) + labeled_frame.grid(row=0, column=0, sticky="nsew", pady=PADY) + labeled_frame.rowconfigure(0, weight=1) + labeled_frame.columnconfigure(0, weight=1) + entry = ttk.Entry(labeled_frame, textvariable=self.cmd) + entry.grid(sticky="ew") + + # results frame + labeled_frame = ttk.LabelFrame(frame, text="Command results", padding=FRAME_PAD) + labeled_frame.grid(row=1, column=0, sticky="nsew", pady=PADY) + labeled_frame.columnconfigure(0, weight=1) + labeled_frame.rowconfigure(0, weight=5) + labeled_frame.rowconfigure(1, weight=1) + + self.result = CodeText(labeled_frame) + self.result.text.config(state=tk.DISABLED, height=15) + self.result.grid(sticky="nsew") + button_frame = ttk.Frame(labeled_frame, padding=FRAME_PAD) + button_frame.grid(sticky="nsew") + button_frame.columnconfigure(0, weight=1) + button_frame.columnconfigure(1, weight=1) + button = ttk.Button(button_frame, text="Run", command=self.click_run) + button.grid(sticky="nsew") + button = ttk.Button(button_frame, text="Close", command=self.destroy) + button.grid(row=0, column=1, sticky="nsew") + + def draw_nodes_frame(self): + labeled_frame = ttk.LabelFrame( + self.top, text="Run on these nodes", padding=FRAME_PAD + ) + labeled_frame.grid(row=0, column=1, sticky="nsew") + labeled_frame.columnconfigure(0, weight=1) + labeled_frame.rowconfigure(0, weight=5) + labeled_frame.rowconfigure(1, weight=1) + + self.node_list = ListboxScroll(labeled_frame) + self.node_list.listbox.config(selectmode=tk.MULTIPLE) + self.node_list.grid(sticky="nsew") + for n in sorted(self.executable_nodes.keys()): + self.node_list.listbox.insert(tk.END, n) + + button_frame = ttk.Frame(labeled_frame, padding=FRAME_PAD) + button_frame.grid(sticky="nsew") + button_frame.columnconfigure(0, weight=1) + button_frame.columnconfigure(1, weight=1) + + button = ttk.Button(button_frame, text="All", command=self.click_all) + button.grid(sticky="nsew") + button = ttk.Button(button_frame, text="None", command=self.click_none) + button.grid(row=0, column=1, sticky="nsew") + + def click_all(self): + self.node_list.listbox.selection_set(0, self.node_list.listbox.size() - 1) + + def click_none(self): + self.node_list.listbox.selection_clear(0, self.node_list.listbox.size() - 1) + + def click_run(self): + """ + run the command on each of the selected nodes and display the output to result text box + """ + command = self.cmd.get().strip() + self.result.text.config(state=tk.NORMAL) + self.result.text.delete("1.0", tk.END) + self.result.text.insert( + tk.END, f"> {command}\n" * len(self.node_list.listbox.curselection()) + ) + for selection in self.node_list.listbox.curselection(): + node_name = self.node_list.listbox.get(selection) + node_id = self.executable_nodes[node_name] + response = self.app.core.client.node_command( + self.app.core.session_id, node_id, command + ) + self.result.text.insert( + tk.END, f"> {node_name} > {command}:\n{response.output}\n" + ) + self.result.text.config(state=tk.DISABLED) diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index d6707fb2..11e2896a 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Callable from core.api.grpc import core_pb2 from core.gui.dialogs.customnodes import CustomNodesDialog from core.gui.dialogs.marker import MarkerDialog +from core.gui.dialogs.runtool import RunToolDialog from core.gui.graph.enums import GraphMode from core.gui.graph.shapeutils import ShapeType, is_marker from core.gui.images import ImageEnum, Images @@ -485,6 +486,8 @@ class Toolbar(ttk.Frame): def click_run_button(self): logging.debug("Click on RUN button") + dialog = RunToolDialog(self.app, self.app) + dialog.show() def click_marker_button(self): logging.debug("Click on marker button") From 870d5dc41cb0446d7931c2188c33a75fa6f7912f Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 22 Apr 2020 15:49:30 -0700 Subject: [PATCH 0191/1131] change the command to start the new core gui --- docs/devguide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/devguide.md b/docs/devguide.md index b6824128..c10bb007 100644 --- a/docs/devguide.md +++ b/docs/devguide.md @@ -89,7 +89,7 @@ Commands below can be used to run the core-daemon, the new core gui, and tests. sudo python3 -m pipenv run core # runs coretk gui -python3 -m pipenv run coretk +python3 -m pipenv run core-pygui # runs mocked unit tests python3 -m pipenv run test-mock From f1e0c7245f3f36258f65a46ea5c42ed0957a6efd Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 22 Apr 2020 16:01:38 -0700 Subject: [PATCH 0192/1131] rm core/Pipfile --- Pipfile | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 Pipfile diff --git a/Pipfile b/Pipfile deleted file mode 100644 index b723d019..00000000 --- a/Pipfile +++ /dev/null @@ -1,11 +0,0 @@ -[[source]] -name = "pypi" -url = "https://pypi.org/simple" -verify_ssl = true - -[dev-packages] - -[packages] - -[requires] -python_version = "3.7" From 7054e606aea969366a2efd3265ce43b94da3fd59 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 22 Apr 2020 23:00:07 -0700 Subject: [PATCH 0193/1131] pygui implemented mac config and fixed issue with manually assigning mac addresses --- daemon/core/gui/appconfig.py | 2 ++ daemon/core/gui/coreclient.py | 19 ++++++++++++++++--- daemon/core/gui/dialogs/macdialog.py | 18 +++++++++++++++--- daemon/core/gui/dialogs/nodeconfig.py | 24 +++++++++--------------- daemon/core/gui/interface.py | 22 +++++++++++++++++++--- 5 files changed, 61 insertions(+), 24 deletions(-) diff --git a/daemon/core/gui/appconfig.py b/daemon/core/gui/appconfig.py index 36a5d1b7..c19fb029 100644 --- a/daemon/core/gui/appconfig.py +++ b/daemon/core/gui/appconfig.py @@ -41,6 +41,7 @@ DEFAULT_IP4S = ["10.0.0.0", "192.168.0.0", "172.16.0.0"] DEFAULT_IP4 = DEFAULT_IP4S[0] DEFAULT_IP6S = ["2001::", "2002::", "a::"] DEFAULT_IP6 = DEFAULT_IP6S[0] +DEFAULT_MAC = "00:00:00:aa:00:00" class IndentDumper(yaml.Dumper): @@ -113,6 +114,7 @@ def check_directory(): "ip4s": DEFAULT_IP4S, "ip6s": DEFAULT_IP6S, }, + "mac": DEFAULT_MAC, } save(config) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 5cc822de..d93cdf51 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -483,7 +483,17 @@ class CoreClient: self.app.after(0, show_grpc_error, e, self.app, self.app) def start_session(self) -> core_pb2.StartSessionResponse: + self.interfaces_manager.reset_mac() nodes = [x.core_node for x in self.canvas_nodes.values()] + links = [] + for edge in self.links.values(): + link = edge.link + logging.info("link: %s", link) + if link.HasField("interface_one") and not link.interface_one.mac: + link.interface_one.mac = self.interfaces_manager.next_mac() + if link.HasField("interface_two") and not link.interface_two.mac: + link.interface_two.mac = self.interfaces_manager.next_mac() + links.append(link) links = [x.link for x in self.links.values()] wlan_configs = self.get_wlan_configs_proto() mobility_configs = self.get_mobility_configs_proto() @@ -849,7 +859,6 @@ class CoreClient: ip6=ip6, ip6mask=ip6_mask, ) - canvas_node.interfaces.append(interface) logging.debug( "create node(%s) interface(%s) IPv4(%s) IPv6(%s)", node.name, @@ -875,13 +884,11 @@ class CoreClient: src_interface = None if NodeUtils.is_container_node(src_node.type): src_interface = self.create_interface(canvas_src_node) - edge.src_interface = src_interface self.interface_to_edge[(src_node.id, src_interface.id)] = edge.token dst_interface = None if NodeUtils.is_container_node(dst_node.type): dst_interface = self.create_interface(canvas_dst_node) - edge.dst_interface = dst_interface self.interface_to_edge[(dst_node.id, dst_interface.id)] = edge.token link = core_pb2.Link( @@ -891,6 +898,12 @@ class CoreClient: interface_one=src_interface, interface_two=dst_interface, ) + if src_interface: + edge.src_interface = link.interface_one + canvas_src_node.interfaces.append(link.interface_one) + if dst_interface: + edge.dst_interface = link.interface_two + canvas_dst_node.interfaces.append(link.interface_two) edge.set_link(link) self.links[edge.token] = edge logging.info("Add link between %s and %s", src_node.name, dst_node.name) diff --git a/daemon/core/gui/dialogs/macdialog.py b/daemon/core/gui/dialogs/macdialog.py index a880e204..6b6faf95 100644 --- a/daemon/core/gui/dialogs/macdialog.py +++ b/daemon/core/gui/dialogs/macdialog.py @@ -1,6 +1,10 @@ -from tkinter import ttk +import tkinter as tk +from tkinter import messagebox, ttk from typing import TYPE_CHECKING +import netaddr + +from core.gui import appconfig from core.gui.dialogs.dialog import Dialog from core.gui.themes import PADX, PADY @@ -11,6 +15,8 @@ if TYPE_CHECKING: class MacConfigDialog(Dialog): def __init__(self, master: "Application", app: "Application") -> None: super().__init__(master, app, "MAC Configuration", modal=True) + mac = self.app.guiconfig.get("mac", appconfig.DEFAULT_MAC) + self.mac_var = tk.StringVar(value=mac) self.draw() def draw(self) -> None: @@ -32,7 +38,7 @@ class MacConfigDialog(Dialog): frame.grid(stick="ew", pady=PADY) label = ttk.Label(frame, text="Starting MAC") label.grid(row=0, column=0, sticky="ew", padx=PADX) - entry = ttk.Entry(frame) + entry = ttk.Entry(frame, textvariable=self.mac_var) entry.grid(row=0, column=1, sticky="ew") # draw buttons @@ -46,4 +52,10 @@ class MacConfigDialog(Dialog): button.grid(row=0, column=1, sticky="ew") def click_save(self) -> None: - pass + mac = self.mac_var.get() + if not netaddr.valid_mac(mac): + messagebox.showerror("MAC Error", f"{mac} is an invalid mac") + else: + self.app.core.interfaces_manager.mac = netaddr.EUI(mac) + self.app.guiconfig["mac"] = mac + self.app.save_config() diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index ce8ea802..9d10a083 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -70,16 +70,12 @@ def check_ip4(parent, name: str, value: str) -> bool: return True -def mac_auto(is_auto: tk.BooleanVar, entry: ttk.Entry): - logging.info("mac auto clicked") +def mac_auto(is_auto: tk.BooleanVar, entry: ttk.Entry, mac: tk.StringVar) -> None: if is_auto.get(): - logging.info("disabling mac") - entry.delete(0, tk.END) - entry.insert(tk.END, "") + mac.set("") entry.config(state=tk.DISABLED) else: - entry.delete(0, tk.END) - entry.insert(tk.END, "00:00:00:00:00:00") + mac.set("00:00:00:00:00:00") entry.config(state=tk.NORMAL) @@ -252,7 +248,7 @@ class NodeConfigDialog(Dialog): mac = tk.StringVar(value=interface.mac) entry = ttk.Entry(tab, textvariable=mac, state=state) entry.grid(row=row, column=2, sticky="ew") - func = partial(mac_auto, is_auto, entry) + func = partial(mac_auto, is_auto, entry, mac) checkbutton.config(command=func) row += 1 @@ -283,7 +279,7 @@ class NodeConfigDialog(Dialog): frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - button = ttk.Button(frame, text="Apply", command=self.config_apply) + button = ttk.Button(frame, text="Apply", command=self.click_apply) button.grid(row=0, column=0, padx=PADX, sticky="ew") button = ttk.Button(frame, text="Cancel", command=self.destroy) @@ -302,7 +298,7 @@ class NodeConfigDialog(Dialog): self.image_button.config(image=self.image) self.image_file = file_path - def config_apply(self): + def click_apply(self): error = False # update core node @@ -328,7 +324,6 @@ class NodeConfigDialog(Dialog): ip4_net = data.ip4.get() if not check_ip4(self, interface.name, ip4_net): error = True - data.ip4.set(f"{interface.ip4}/{interface.ip4mask}") break if ip4_net: ip4, ip4mask = ip4_net.split("/") @@ -342,7 +337,6 @@ class NodeConfigDialog(Dialog): ip6_net = data.ip6.get() if not check_ip6(self, interface.name, ip6_net): error = True - data.ip6.set(f"{interface.ip6}/{interface.ip6mask}") break if ip6_net: ip6, ip6mask = ip6_net.split("/") @@ -353,13 +347,13 @@ class NodeConfigDialog(Dialog): interface.ip6mask = ip6mask mac = data.mac.get() - if mac and not netaddr.valid_mac(mac): + auto_mac = data.is_auto.get() + if not auto_mac and not netaddr.valid_mac(mac): title = f"MAC Error for {interface.name}" messagebox.showerror(title, "Invalid MAC Address") error = True - data.mac.set(interface.mac) break - else: + elif not auto_mac: mac = netaddr.EUI(mac) mac.dialect = netaddr.mac_unix_expanded interface.mac = str(mac) diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index b5c24fc4..359dba8e 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -2,8 +2,10 @@ import logging import random from typing import TYPE_CHECKING, Set, Union -from netaddr import IPNetwork +import netaddr +from netaddr import EUI, IPNetwork +from core.gui import appconfig from core.gui.nodeutils import NodeUtils if TYPE_CHECKING: @@ -35,12 +37,15 @@ class InterfaceManager: def __init__(self, app: "Application") -> None: self.app = app ip_config = self.app.guiconfig.get("ips", {}) - ip4 = ip_config.get("ip4", "10.0.0.0") - ip6 = ip_config.get("ip6", "2001::") + ip4 = ip_config.get("ip4", appconfig.DEFAULT_IP4) + ip6 = ip_config.get("ip6", appconfig.DEFAULT_IP6) self.ip4_mask = 24 self.ip6_mask = 64 self.ip4_subnets = IPNetwork(f"{ip4}/{self.ip4_mask}") self.ip6_subnets = IPNetwork(f"{ip6}/{self.ip6_mask}") + mac = self.app.guiconfig.get("mac", appconfig.DEFAULT_MAC) + self.mac = EUI(mac) + self.current_mac = None self.current_subnets = None def update_ips(self, ip4: str, ip6: str) -> None: @@ -48,6 +53,17 @@ class InterfaceManager: self.ip4_subnets = IPNetwork(f"{ip4}/{self.ip4_mask}") self.ip6_subnets = IPNetwork(f"{ip6}/{self.ip6_mask}") + def reset_mac(self) -> None: + self.current_mac = self.mac + self.current_mac.dialect = netaddr.mac_unix_expanded + + def next_mac(self) -> str: + mac = str(self.current_mac) + value = self.current_mac.value + 1 + self.current_mac = EUI(value) + self.current_mac.dialect = netaddr.mac_unix_expanded + return mac + def next_subnets(self) -> Subnets: # define currently used subnets used_subnets = set() From aa2537753e3b90b139e67173a66c50f1508f08a8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 23 Apr 2020 00:11:13 -0700 Subject: [PATCH 0194/1131] pygui small tweaks to run tool dialog to simplify text and properly resize --- daemon/core/gui/dialogs/runtool.py | 64 +++++++++++++----------------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/daemon/core/gui/dialogs/runtool.py b/daemon/core/gui/dialogs/runtool.py index 8d9d9cb0..c937de08 100644 --- a/daemon/core/gui/dialogs/runtool.py +++ b/daemon/core/gui/dialogs/runtool.py @@ -1,87 +1,78 @@ import tkinter as tk from tkinter import ttk -from core.api.grpc import core_pb2 from core.gui.dialogs.dialog import Dialog -from core.gui.themes import FRAME_PAD, PADY +from core.gui.nodeutils import NodeUtils +from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CodeText, ListboxScroll class RunToolDialog(Dialog): - def __init__(self, master, app): + def __init__(self, master, app) -> None: super().__init__(master, app, "Run Tool", modal=True) - self.cmd = tk.StringVar(value="ps ax") self.app = app self.result = None self.node_list = None self.executable_nodes = {} - self.store_nodes() self.draw() - def store_nodes(self): + def store_nodes(self) -> None: """ store all CORE nodes (nodes that execute commands) from all existing nodes """ for nid, node in self.app.core.canvas_nodes.items(): - if node.core_node.type == core_pb2.NodeType.DEFAULT: + if NodeUtils.is_container_node(node.core_node.type): self.executable_nodes[node.core_node.name] = nid - def draw(self): + def draw(self) -> None: self.top.rowconfigure(0, weight=1) - self.top.columnconfigure(0, weight=5) - self.top.columnconfigure(1, weight=1) + self.top.columnconfigure(0, weight=1) self.draw_command_frame() self.draw_nodes_frame() - return - def draw_command_frame(self): + def draw_command_frame(self) -> None: # the main frame frame = ttk.Frame(self.top) - frame.grid(row=0, column=0, sticky="nsew") + frame.grid(row=0, column=0, sticky="nsew", padx=PADX) frame.columnconfigure(0, weight=1) - frame.rowconfigure(0, weight=1) frame.rowconfigure(1, weight=1) - labeled_frame = ttk.LabelFrame(frame, text="Command line", padding=FRAME_PAD) - labeled_frame.grid(row=0, column=0, sticky="nsew", pady=PADY) + labeled_frame = ttk.LabelFrame(frame, text="Command", padding=FRAME_PAD) + labeled_frame.grid(sticky="ew", pady=PADY) labeled_frame.rowconfigure(0, weight=1) labeled_frame.columnconfigure(0, weight=1) entry = ttk.Entry(labeled_frame, textvariable=self.cmd) entry.grid(sticky="ew") # results frame - labeled_frame = ttk.LabelFrame(frame, text="Command results", padding=FRAME_PAD) - labeled_frame.grid(row=1, column=0, sticky="nsew", pady=PADY) + labeled_frame = ttk.LabelFrame(frame, text="Output", padding=FRAME_PAD) + labeled_frame.grid(sticky="nsew", pady=PADY) labeled_frame.columnconfigure(0, weight=1) - labeled_frame.rowconfigure(0, weight=5) - labeled_frame.rowconfigure(1, weight=1) + labeled_frame.rowconfigure(0, weight=1) self.result = CodeText(labeled_frame) self.result.text.config(state=tk.DISABLED, height=15) - self.result.grid(sticky="nsew") - button_frame = ttk.Frame(labeled_frame, padding=FRAME_PAD) + self.result.grid(sticky="nsew", pady=PADY) + button_frame = ttk.Frame(labeled_frame) button_frame.grid(sticky="nsew") button_frame.columnconfigure(0, weight=1) button_frame.columnconfigure(1, weight=1) button = ttk.Button(button_frame, text="Run", command=self.click_run) - button.grid(sticky="nsew") + button.grid(sticky="ew", padx=PADX) button = ttk.Button(button_frame, text="Close", command=self.destroy) - button.grid(row=0, column=1, sticky="nsew") + button.grid(row=0, column=1, sticky="ew") - def draw_nodes_frame(self): - labeled_frame = ttk.LabelFrame( - self.top, text="Run on these nodes", padding=FRAME_PAD - ) + def draw_nodes_frame(self) -> None: + labeled_frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD) labeled_frame.grid(row=0, column=1, sticky="nsew") labeled_frame.columnconfigure(0, weight=1) - labeled_frame.rowconfigure(0, weight=5) - labeled_frame.rowconfigure(1, weight=1) + labeled_frame.rowconfigure(0, weight=1) self.node_list = ListboxScroll(labeled_frame) self.node_list.listbox.config(selectmode=tk.MULTIPLE) - self.node_list.grid(sticky="nsew") + self.node_list.grid(sticky="nsew", pady=PADY) for n in sorted(self.executable_nodes.keys()): self.node_list.listbox.insert(tk.END, n) @@ -91,19 +82,20 @@ class RunToolDialog(Dialog): button_frame.columnconfigure(1, weight=1) button = ttk.Button(button_frame, text="All", command=self.click_all) - button.grid(sticky="nsew") + button.grid(sticky="nsew", padx=PADX) button = ttk.Button(button_frame, text="None", command=self.click_none) button.grid(row=0, column=1, sticky="nsew") - def click_all(self): + def click_all(self) -> None: self.node_list.listbox.selection_set(0, self.node_list.listbox.size() - 1) - def click_none(self): + def click_none(self) -> None: self.node_list.listbox.selection_clear(0, self.node_list.listbox.size() - 1) - def click_run(self): + def click_run(self) -> None: """ - run the command on each of the selected nodes and display the output to result text box + Run the command on each of the selected nodes and display the output to result + text box. """ command = self.cmd.get().strip() self.result.text.config(state=tk.NORMAL) From ea99b628fc8ca09904d33e0a82dfe81fbeeff9ec Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 23 Apr 2020 08:54:43 -0700 Subject: [PATCH 0195/1131] pygui removed dumping commands multiple times in run window as they are included before node output --- daemon/core/gui/dialogs/runtool.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/daemon/core/gui/dialogs/runtool.py b/daemon/core/gui/dialogs/runtool.py index c937de08..21a2f44b 100644 --- a/daemon/core/gui/dialogs/runtool.py +++ b/daemon/core/gui/dialogs/runtool.py @@ -100,9 +100,6 @@ class RunToolDialog(Dialog): command = self.cmd.get().strip() self.result.text.config(state=tk.NORMAL) self.result.text.delete("1.0", tk.END) - self.result.text.insert( - tk.END, f"> {command}\n" * len(self.node_list.listbox.curselection()) - ) for selection in self.node_list.listbox.curselection(): node_name = self.node_list.listbox.get(selection) node_id = self.executable_nodes[node_name] From 01b41b0276f564aced6664046cd165ed4bf77e6f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 23 Apr 2020 09:06:56 -0700 Subject: [PATCH 0196/1131] pygui cleaned up node service configuration dialog directory tab layout --- daemon/core/gui/dialogs/serviceconfig.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index 038564a3..33d38323 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -229,6 +229,7 @@ class ServiceConfigDialog(Dialog): tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab.grid(sticky="nsew") tab.columnconfigure(0, weight=1) + tab.rowconfigure(2, weight=1) self.notebook.add(tab, text="Directories") label = ttk.Label( @@ -238,15 +239,14 @@ class ServiceConfigDialog(Dialog): label.grid(row=0, column=0, sticky="ew") frame = ttk.Frame(tab, padding=FRAME_PAD) frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) frame.grid(row=1, column=0, sticky="nsew") var = tk.StringVar(value="") self.directory_entry = ttk.Entry(frame, textvariable=var) - self.directory_entry.grid(row=0, column=0, sticky="ew") + self.directory_entry.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="...", command=self.find_directory_button) button.grid(row=0, column=1, sticky="ew") self.dir_list = ListboxScroll(tab) - self.dir_list.grid(row=2, column=0, sticky="nsew") + self.dir_list.grid(row=2, column=0, sticky="nsew", pady=PADY) self.dir_list.listbox.bind("<>", self.directory_select) for d in self.temp_directories: self.dir_list.listbox.insert("end", d) @@ -256,7 +256,7 @@ class ServiceConfigDialog(Dialog): frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Add", command=self.add_directory) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Remove", command=self.remove_directory) button.grid(row=0, column=1, sticky="ew") From b5f457161841593bc384fb843c154659755360db Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 23 Apr 2020 10:26:12 -0700 Subject: [PATCH 0197/1131] fixed edit node using a 0,0 position when not intending to move node, side effect of trying to give new nodes a default position --- daemon/core/emulator/session.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 80538fc3..7d1d3228 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -695,6 +695,7 @@ class Session: # generate name if not provided if not options: options = NodeOptions() + options.set_position(0, 0) name = options.name if not name: name = f"{node_class.__name__}{_id}" @@ -809,9 +810,7 @@ class Session: node.setposition(x, y, None) node.position.set_geo(lon, lat, alt) self.broadcast_node(node) - else: - if has_empty_position: - x, y = 0, 0 + elif not has_empty_position: node.setposition(x, y, None) def start_mobility(self, node_ids: List[int] = None) -> None: From 8e8ffb3ffb4ba8ba03d7aa239c093e28f3e3eb35 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 23 Apr 2020 14:00:23 -0700 Subject: [PATCH 0198/1131] pygui close mac config dialog on save --- daemon/core/gui/dialogs/macdialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon/core/gui/dialogs/macdialog.py b/daemon/core/gui/dialogs/macdialog.py index 6b6faf95..fa5d81ad 100644 --- a/daemon/core/gui/dialogs/macdialog.py +++ b/daemon/core/gui/dialogs/macdialog.py @@ -59,3 +59,4 @@ class MacConfigDialog(Dialog): self.app.core.interfaces_manager.mac = netaddr.EUI(mac) self.app.guiconfig["mac"] = mac self.app.save_config() + self.destroy() From b7adbd289c2dba40d47bff39bfac69e1aa64d8d5 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 23 Apr 2020 14:04:22 -0700 Subject: [PATCH 0199/1131] pygui copy links when generating mac to avoid retaining generated macs --- daemon/core/gui/coreclient.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index d93cdf51..b6b13c58 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -487,14 +487,13 @@ class CoreClient: nodes = [x.core_node for x in self.canvas_nodes.values()] links = [] for edge in self.links.values(): - link = edge.link - logging.info("link: %s", link) + link = core_pb2.Link() + link.CopyFrom(edge.link) if link.HasField("interface_one") and not link.interface_one.mac: link.interface_one.mac = self.interfaces_manager.next_mac() if link.HasField("interface_two") and not link.interface_two.mac: link.interface_two.mac = self.interfaces_manager.next_mac() links.append(link) - links = [x.link for x in self.links.values()] wlan_configs = self.get_wlan_configs_proto() mobility_configs = self.get_mobility_configs_proto() emane_model_configs = self.get_emane_model_configs_proto() From 62c0011caa0cbaf085d9cc0f4581d787a9952b18 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 28 Apr 2020 09:35:21 -0700 Subject: [PATCH 0200/1131] avoid configuring links for wireless networks --- daemon/core/emulator/session.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 7d1d3228..95aa7c0b 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -357,7 +357,9 @@ class Session: ) interface = create_interface(node_one, net_one, interface_one) node_one_interface = interface - link_config(net_one, interface, link_options) + wireless_net = isinstance(net_one, (EmaneNet, WlanNode)) + if not wireless_net: + link_config(net_one, interface, link_options) # network to node if node_two and net_one: @@ -368,7 +370,8 @@ class Session: ) interface = create_interface(node_two, net_one, interface_two) node_two_interface = interface - if not link_options.unidirectional: + wireless_net = isinstance(net_one, (EmaneNet, WlanNode)) + if not link_options.unidirectional and not wireless_net: link_config(net_one, interface, link_options) # network to network From 275e8f4c30f6f4012e30f776711a213cc7ff25f5 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 29 Apr 2020 16:19:40 -0700 Subject: [PATCH 0201/1131] finish writing a Find tool that allows find a node based on node name --- daemon/core/gui/dialogs/find.py | 144 ++++++++++++++++++++++++++++++++ daemon/core/gui/menubar.py | 7 ++ 2 files changed, 151 insertions(+) create mode 100644 daemon/core/gui/dialogs/find.py diff --git a/daemon/core/gui/dialogs/find.py b/daemon/core/gui/dialogs/find.py new file mode 100644 index 00000000..73cc459b --- /dev/null +++ b/daemon/core/gui/dialogs/find.py @@ -0,0 +1,144 @@ +import logging +import tkinter as tk +from tkinter import ttk + +from core.gui.dialogs.dialog import Dialog +from core.gui.themes import FRAME_PAD, PADX, PADY + + +class FindDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Find", modal=True) + + self.find_text = tk.StringVar(value="") + self.tree = None + self.draw() + self.protocol("WM_DELETE_WINDOW", self.close_dialog) + self.bind("", self.find_node) + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + self.top.rowconfigure(1, weight=5) + self.top.rowconfigure(2, weight=1) + + # Find node frame + frame = ttk.Frame(self.top, padding=FRAME_PAD) + frame.grid(sticky="nsew") + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="Find:") + label.grid() + entry = ttk.Entry(frame, textvariable=self.find_text) + entry.grid(row=0, column=1, sticky="nsew") + + # node list frame + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + frame.grid(sticky="nsew", padx=PADX, pady=PADY) + self.tree = ttk.Treeview( + frame, + columns=("nodeid", "name", "location", "detail"), + show="headings", + selectmode=tk.BROWSE, + ) + self.tree.grid(sticky="nsew") + style = ttk.Style() + heading_size = int(self.app.guiconfig["scale"] * 10) + style.configure("Treeview.Heading", font=(None, heading_size, "bold")) + self.tree.column("nodeid", stretch=tk.YES, anchor="center") + self.tree.heading("nodeid", text="Node ID") + self.tree.column("name", stretch=tk.YES, anchor="center") + self.tree.heading("name", text="Name") + self.tree.column("location", stretch=tk.YES, anchor="center") + self.tree.heading("location", text="Location") + self.tree.column("detail", stretch=tk.YES, anchor="center") + self.tree.heading("detail", text="Detail") + + self.tree.bind("<>", self.click_select) + + yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) + yscrollbar.grid(row=0, column=1, sticky="ns") + self.tree.configure(yscrollcommand=yscrollbar.set) + + xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) + xscrollbar.grid(row=1, sticky="ew") + self.tree.configure(xscrollcommand=xscrollbar.set) + + # button frame + frame = ttk.Frame(self.top) + frame.grid(sticky="nsew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + button = ttk.Button(frame, text="Find", command=self.find_node) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Cancel", command=self.close_dialog) + button.grid(row=0, column=1, sticky="ew", padx=PADX) + + def clear_treeview_items(self): + """ + clear all items in the treeview + :return: + """ + for i in list(self.tree.get_children("")): + self.tree.delete(i) + + def find_node(self, event=None): + """ + Query nodes that have the same node name as our search key, + display results to tree view + """ + node_name = self.find_text.get().strip() + self.clear_treeview_items() + for node_id, node in sorted( + self.app.core.canvas_nodes.items(), key=lambda x: x[0] + ): + name = node.core_node.name + if not node_name or node_name == name: + location = f"<{node.core_node.position.x}, {node.core_node.position.y}>" + # TODO I am not sure what to insert for Detail column, leaving in blank for now + self.tree.insert( + "", tk.END, text=str(node_id), values=(node_id, name, location, "") + ) + + results = self.tree.get_children("") + if results: + self.tree.selection_set(results[0]) + + def close_dialog(self): + self.app.canvas.delete("find") + self.destroy() + + def click_select(self, _event: tk.Event = None) -> None: + item = self.tree.selection() + if item: + self.app.canvas.delete("find") + node_id = int(self.tree.item(item, "text")) + canvas_node = self.app.core.canvas_nodes[node_id] + + x0, y0, x1, y1 = self.app.canvas.bbox(canvas_node.id) + dist = 5 * self.app.guiconfig["scale"] + self.app.canvas.create_oval( + x0 - dist, + y0 - dist, + x1 + dist, + y1 + dist, + tags="find", + outline="red", + width=3.0 * self.app.guiconfig["scale"], + ) + + _x, _y, _, _ = self.app.canvas.bbox(canvas_node.id) + oid = self.app.canvas.find_withtag("rectangle") + x0, y0, x1, y1 = self.app.canvas.bbox(oid[0]) + logging.debug("Dist to most left: %s", abs(x0 - _x)) + logging.debug("White canvas width: %s", abs(x0 - x1)) + + # calculate the node's location + # (as fractions of white canvas's width and height) + # and instantly scroll the x and y scrollbar to that location + + xscroll_fraction = abs(x0 - _x) / abs(x0 - x1) + yscroll_fraction = abs(y0 - _y) / abs(y0 - y1) + self.app.canvas.xview_moveto(xscroll_fraction) + self.app.canvas.yview_moveto(yscroll_fraction) diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 74a93df7..fafe6b5c 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -11,6 +11,7 @@ from core.gui.dialogs.about import AboutDialog from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog from core.gui.dialogs.canvaswallpaper import CanvasWallpaperDialog from core.gui.dialogs.executepython import ExecutePythonDialog +from core.gui.dialogs.find import FindDialog from core.gui.dialogs.hooks import HooksDialog from core.gui.dialogs.ipdialog import IpConfigDialog from core.gui.dialogs.macdialog import MacConfigDialog @@ -114,6 +115,7 @@ class Menubar(tk.Menu): Create edit menu """ menu = tk.Menu(self) + menu.add_command(label="Find", accelerator="Ctrl+F", command=self.click_find) menu.add_command(label="Preferences", command=self.click_preferences) menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED) menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED) @@ -125,6 +127,7 @@ class Menubar(tk.Menu): label="Delete", accelerator="Ctrl+D", command=self.click_delete ) self.add_cascade(label="Edit", menu=menu) + self.app.master.bind_all("", self.click_find) self.app.master.bind_all("", self.click_cut) self.app.master.bind_all("", self.click_copy) self.app.master.bind_all("", self.click_paste) @@ -397,6 +400,10 @@ class Menubar(tk.Menu): self.core.create_new_session() self.core.xml_file = None + def click_find(self, _event: tk.Event = None) -> None: + dialog = FindDialog(self.app, self.app) + dialog.show() + def click_preferences(self) -> None: dialog = PreferencesDialog(self.app, self.app) dialog.show() From c45202e61b09c2f07734ea47e388b343ed398511 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 29 Apr 2020 16:36:12 -0700 Subject: [PATCH 0202/1131] add type checking to class methods --- daemon/core/gui/dialogs/find.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/daemon/core/gui/dialogs/find.py b/daemon/core/gui/dialogs/find.py index 73cc459b..190d9410 100644 --- a/daemon/core/gui/dialogs/find.py +++ b/daemon/core/gui/dialogs/find.py @@ -7,7 +7,7 @@ from core.gui.themes import FRAME_PAD, PADX, PADY class FindDialog(Dialog): - def __init__(self, master, app): + def __init__(self, master, app) -> None: super().__init__(master, app, "Find", modal=True) self.find_text = tk.StringVar(value="") @@ -16,7 +16,7 @@ class FindDialog(Dialog): self.protocol("WM_DELETE_WINDOW", self.close_dialog) self.bind("", self.find_node) - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) self.top.rowconfigure(1, weight=5) @@ -75,15 +75,14 @@ class FindDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.close_dialog) button.grid(row=0, column=1, sticky="ew", padx=PADX) - def clear_treeview_items(self): + def clear_treeview_items(self) -> None: """ clear all items in the treeview - :return: """ for i in list(self.tree.get_children("")): self.tree.delete(i) - def find_node(self, event=None): + def find_node(self, _event: tk.Event = None) -> None: """ Query nodes that have the same node name as our search key, display results to tree view @@ -105,11 +104,16 @@ class FindDialog(Dialog): if results: self.tree.selection_set(results[0]) - def close_dialog(self): + def close_dialog(self) -> None: self.app.canvas.delete("find") self.destroy() def click_select(self, _event: tk.Event = None) -> None: + """ + find the node that matches search criteria, circle around that node + and scroll the x and y scrollbar to be able to see the node if + it is out of sight + """ item = self.tree.selection() if item: self.app.canvas.delete("find") @@ -137,7 +141,7 @@ class FindDialog(Dialog): # calculate the node's location # (as fractions of white canvas's width and height) # and instantly scroll the x and y scrollbar to that location - + # looks a bit ugly when zoom too much xscroll_fraction = abs(x0 - _x) / abs(x0 - x1) yscroll_fraction = abs(y0 - _y) / abs(y0 - y1) self.app.canvas.xview_moveto(xscroll_fraction) From e9ca4a5b581c07a8c569806e107b4cd6b52cb806 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 29 Apr 2020 16:53:06 -0700 Subject: [PATCH 0203/1131] Session dialog: Bold heading text so that it stands out more, allign heading text with column text --- daemon/core/gui/dialogs/sessions.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index 6a3cf380..3671b308 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -72,12 +72,15 @@ class SessionsDialog(Dialog): show="headings", selectmode=tk.BROWSE, ) + style = ttk.Style() + heading_size = int(self.app.guiconfig["scale"] * 10) + style.configure("Treeview.Heading", font=(None, heading_size, "bold")) self.tree.grid(sticky="nsew") - self.tree.column("id", stretch=tk.YES) + self.tree.column("id", stretch=tk.YES, anchor="center") self.tree.heading("id", text="ID") - self.tree.column("state", stretch=tk.YES) + self.tree.column("state", stretch=tk.YES, anchor="center") self.tree.heading("state", text="State") - self.tree.column("nodes", stretch=tk.YES) + self.tree.column("nodes", stretch=tk.YES, anchor="center") self.tree.heading("nodes", text="Node Count") for index, session in enumerate(self.sessions): @@ -213,3 +216,5 @@ class SessionsDialog(Dialog): def on_closing(self) -> None: if self.is_start_app and messagebox.askokcancel("Exit", "Quit?", parent=self): self.click_exit() + if not self.is_start_app: + self.destroy() From 64657b20a816e16307b6c4e24996d7e445fa207e Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 29 Apr 2020 17:09:17 -0700 Subject: [PATCH 0204/1131] add more logic to scrolling the scrollbar to get a bit nicer view --- daemon/core/gui/dialogs/find.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/daemon/core/gui/dialogs/find.py b/daemon/core/gui/dialogs/find.py index 190d9410..e0500da3 100644 --- a/daemon/core/gui/dialogs/find.py +++ b/daemon/core/gui/dialogs/find.py @@ -141,8 +141,14 @@ class FindDialog(Dialog): # calculate the node's location # (as fractions of white canvas's width and height) # and instantly scroll the x and y scrollbar to that location - # looks a bit ugly when zoom too much xscroll_fraction = abs(x0 - _x) / abs(x0 - x1) yscroll_fraction = abs(y0 - _y) / abs(y0 - y1) + # scroll a little more to the left or a little bit up so that the node + # doesn't always fall in the most top-left corner + for i in range(2): + if xscroll_fraction > 0.05: + xscroll_fraction = xscroll_fraction - 0.05 + if yscroll_fraction > 0.05: + yscroll_fraction = yscroll_fraction - 0.05 self.app.canvas.xview_moveto(xscroll_fraction) self.app.canvas.yview_moveto(yscroll_fraction) From 1f8d16df0890fbc9b5bb0482f0309f27294d5140 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 29 Apr 2020 17:17:57 -0700 Subject: [PATCH 0205/1131] touch up --- daemon/core/gui/dialogs/find.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/daemon/core/gui/dialogs/find.py b/daemon/core/gui/dialogs/find.py index e0500da3..4d7bc6bd 100644 --- a/daemon/core/gui/dialogs/find.py +++ b/daemon/core/gui/dialogs/find.py @@ -42,7 +42,7 @@ class FindDialog(Dialog): show="headings", selectmode=tk.BROWSE, ) - self.tree.grid(sticky="nsew") + self.tree.grid(sticky="nsew", pady=PADY) style = ttk.Style() heading_size = int(self.app.guiconfig["scale"] * 10) style.configure("Treeview.Heading", font=(None, heading_size, "bold")) @@ -94,10 +94,14 @@ class FindDialog(Dialog): ): name = node.core_node.name if not node_name or node_name == name: - location = f"<{node.core_node.position.x}, {node.core_node.position.y}>" + pos_x = round(node.core_node.position.x, 1) + pos_y = round(node.core_node.position.y, 1) # TODO I am not sure what to insert for Detail column, leaving in blank for now self.tree.insert( - "", tk.END, text=str(node_id), values=(node_id, name, location, "") + "", + tk.END, + text=str(node_id), + values=(node_id, name, f"<{pos_x}, {pos_y}>", ""), ) results = self.tree.get_children("") From 47ef5ec14dd49e883f413bcac1d1badc0948bb1b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 30 Apr 2020 11:19:23 -0700 Subject: [PATCH 0206/1131] avoid writing link options to xml for emane/wlan links --- daemon/core/xml/corexml.py | 47 +++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 6ad210b8..891db1cd 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -13,7 +13,7 @@ from core.errors import CoreXmlError from core.nodes.base import CoreNetworkBase, CoreNodeBase, NodeBase from core.nodes.docker import DockerNode from core.nodes.lxd import LxcNode -from core.nodes.network import CtrlNet +from core.nodes.network import CtrlNet, WlanNode from core.services.coreservices import CoreService if TYPE_CHECKING: @@ -559,26 +559,31 @@ class CoreXmlWriter: ) link_element.append(interface_two) - # check for options - options = etree.Element("options") - add_attribute(options, "delay", link_data.delay) - add_attribute(options, "bandwidth", link_data.bandwidth) - add_attribute(options, "per", link_data.per) - add_attribute(options, "dup", link_data.dup) - add_attribute(options, "jitter", link_data.jitter) - add_attribute(options, "mer", link_data.mer) - add_attribute(options, "burst", link_data.burst) - add_attribute(options, "mburst", link_data.mburst) - add_attribute(options, "type", link_data.link_type) - add_attribute(options, "gui_attributes", link_data.gui_attributes) - add_attribute(options, "unidirectional", link_data.unidirectional) - add_attribute(options, "emulation_id", link_data.emulation_id) - add_attribute(options, "network_id", link_data.network_id) - add_attribute(options, "key", link_data.key) - add_attribute(options, "opaque", link_data.opaque) - add_attribute(options, "session", link_data.session) - if options.items(): - link_element.append(options) + # check for options, don't write for emane/wlan links + node_one = self.session.get_node(link_data.node1_id) + node_two = self.session.get_node(link_data.node2_id) + is_node_one_wireless = isinstance(node_one, (WlanNode, EmaneNet)) + is_node_two_wireless = isinstance(node_two, (WlanNode, EmaneNet)) + if not any([is_node_one_wireless, is_node_two_wireless]): + options = etree.Element("options") + add_attribute(options, "delay", link_data.delay) + add_attribute(options, "bandwidth", link_data.bandwidth) + add_attribute(options, "per", link_data.per) + add_attribute(options, "dup", link_data.dup) + add_attribute(options, "jitter", link_data.jitter) + add_attribute(options, "mer", link_data.mer) + add_attribute(options, "burst", link_data.burst) + add_attribute(options, "mburst", link_data.mburst) + add_attribute(options, "type", link_data.link_type) + add_attribute(options, "gui_attributes", link_data.gui_attributes) + add_attribute(options, "unidirectional", link_data.unidirectional) + add_attribute(options, "emulation_id", link_data.emulation_id) + add_attribute(options, "network_id", link_data.network_id) + add_attribute(options, "key", link_data.key) + add_attribute(options, "opaque", link_data.opaque) + add_attribute(options, "session", link_data.session) + if options.items(): + link_element.append(options) return link_element From 4037da49c28de14ccbddf2e83f7825cb8dfeeca4 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 30 Apr 2020 12:48:51 -0700 Subject: [PATCH 0207/1131] Fix issue: node's services won't save when clearing all the services and add default services back to the node. Set core node's services to default services (instead of leaving it empty) when a new node is created. --- daemon/core/gui/coreclient.py | 4 ++++ daemon/core/gui/dialogs/nodeservice.py | 19 ++++++++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index b6b13c58..4940d7ad 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -815,6 +815,10 @@ class CoreClient: if NodeUtils.is_custom(node_type, model): services = NodeUtils.get_custom_node_services(self.app.guiconfig, model) node.services[:] = services + else: + services = self.default_services.get(model, None) + if services: + node.services[:] = services logging.info( "add node(%s) to session(%s), coordinates(%s, %s)", node.name, diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index 4927fece..e8f67220 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -153,18 +153,19 @@ class NodeServiceDialog(Dialog): ) def click_save(self): - # if node is custom type or current services are not the default services then - # set core node services and add node to modified services node set + core_node = self.canvas_node.core_node + # custom type node or CORE node with custom services if ( - self.canvas_node.core_node.model not in self.app.core.default_services - or self.current_services - != self.app.core.default_services[self.canvas_node.core_node.model] + core_node.model not in self.app.core.default_services + or self.current_services != self.app.core.default_services[core_node.model] ): - self.canvas_node.core_node.services[:] = self.current_services - self.app.core.modified_service_nodes.add(self.canvas_node.core_node.id) + core_node.services[:] = self.current_services + self.app.core.modified_service_nodes.add(core_node.id) + # custom services CORE node but modified back to having default services + # or just CORE nodes that don't get any change else: - if len(self.canvas_node.core_node.services) > 0: - self.canvas_node.core_node.services[:] = [] + core_node.services[:] = self.current_services + self.app.core.modified_service_nodes.discard(core_node.id) self.destroy() def click_cancel(self): From d945e7c41e20593d551e3883bcf411c08c15178b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 30 Apr 2020 12:57:05 -0700 Subject: [PATCH 0208/1131] formatted sdn.py after recent merge --- daemon/core/services/sdn.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/daemon/core/services/sdn.py b/daemon/core/services/sdn.py index 9a810b3d..ab46f551 100644 --- a/daemon/core/services/sdn.py +++ b/daemon/core/services/sdn.py @@ -77,9 +77,15 @@ class OvsService(SdnService): # add interfaces to bridge # Make port numbers explicit so they're easier to follow in reading the script cfg += "## Add the CORE interface to the switch\n" - cfg += "ovs-vsctl add-port ovsbr0 eth%s -- set Interface eth%s ofport_request=%d\n" % (ifnum, ifnum, portnum) + cfg += ( + "ovs-vsctl add-port ovsbr0 eth%s -- set Interface eth%s ofport_request=%d\n" + % (ifnum, ifnum, portnum) + ) cfg += "## And then add its sibling veth interface\n" - cfg += "ovs-vsctl add-port ovsbr0 sw%s -- set Interface sw%s ofport_request=%d\n" % (ifnum, ifnum, portnum+1) + cfg += ( + "ovs-vsctl add-port ovsbr0 sw%s -- set Interface sw%s ofport_request=%d\n" + % (ifnum, ifnum, portnum + 1) + ) cfg += "## start them up so we can send/receive data\n" cfg += "ovs-ofctl mod-port ovsbr0 eth%s up\n" % ifnum cfg += "ovs-ofctl mod-port ovsbr0 sw%s up\n" % ifnum @@ -100,8 +106,14 @@ class OvsService(SdnService): if hasattr(ifc, "control") and ifc.control is True: continue cfg += "## Take the data from the CORE interface and put it on the veth and vice versa\n" - cfg += "ovs-ofctl add-flow ovsbr0 priority=1000,in_port=%d,action=output:%d\n" % (portnum, portnum + 1) - cfg += "ovs-ofctl add-flow ovsbr0 priority=1000,in_port=%d,action=output:%d\n" % (portnum + 1, portnum) + cfg += ( + "ovs-ofctl add-flow ovsbr0 priority=1000,in_port=%d,action=output:%d\n" + % (portnum, portnum + 1) + ) + cfg += ( + "ovs-ofctl add-flow ovsbr0 priority=1000,in_port=%d,action=output:%d\n" + % (portnum + 1, portnum) + ) portnum += 2 return cfg From 7e0ead0766679be891bd73c6170fd4f19effed23 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 30 Apr 2020 13:23:00 -0700 Subject: [PATCH 0209/1131] fixed formatting for quagga fast convergence merge --- daemon/core/services/quagga.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/daemon/core/services/quagga.py b/daemon/core/services/quagga.py index ed0d6dbd..331e23da 100644 --- a/daemon/core/services/quagga.py +++ b/daemon/core/services/quagga.py @@ -347,17 +347,21 @@ class Ospfv2(QuaggaService): return cfg @classmethod - def generatequaggaifcconfig(cls, node, ifc): + def generatequaggaifcconfig(cls, node, ifc): cfg = cls.mtucheck(ifc) # external RJ45 connections will use default OSPF timers if cls.rj45check(ifc): return cfg cfg += cls.ptpcheck(ifc) - return cfg + """\ + return ( + cfg + + """\ ip ospf hello-interval 2 ip ospf dead-interval 6 ip ospf retransmit-interval 5 """ + ) + class Ospfv3(QuaggaService): """ From 580641f5d96168aaffd7e77c5ef5a16ceef20973 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 30 Apr 2020 13:47:45 -0700 Subject: [PATCH 0210/1131] remove CoreClient.modified_service_node. When a new CORE node is created, assign default services right away (instead of leaving it empty), therefore no more confusion whether [] means empty service or means CORE node with default services --- daemon/core/gui/coreclient.py | 7 +--- daemon/core/gui/dialogs/nodeservice.py | 49 +++----------------------- daemon/core/gui/graph/graph.py | 5 --- 3 files changed, 6 insertions(+), 55 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 4940d7ad..f3ec6612 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -85,7 +85,6 @@ class CoreClient: self.handling_events = None self.xml_dir = None self.xml_file = None - self.modified_service_nodes = set() @property def client(self): @@ -112,7 +111,6 @@ class CoreClient: self.links.clear() self.hooks.clear() self.emane_config = None - self.modified_service_nodes.clear() for mobility_player in self.mobility_players.values(): mobility_player.handle_close() self.mobility_players.clear() @@ -815,6 +813,7 @@ class CoreClient: if NodeUtils.is_custom(node_type, model): services = NodeUtils.get_custom_node_services(self.app.guiconfig, model) node.services[:] = services + # assign default services to CORE node else: services = self.default_services.get(model, None) if services: @@ -840,7 +839,6 @@ class CoreClient: logging.error("unknown node: %s", node_id) continue del self.canvas_nodes[node_id] - self.modified_service_nodes.discard(node_id) for edge in canvas_node.edges: if edge in edges: continue @@ -1056,9 +1054,6 @@ class CoreClient: ) return dict(config) - def service_been_modified(self, node_id: int) -> bool: - return node_id in self.modified_service_nodes - def execute_script(self, script): response = self.client.execute_script(script) logging.info("execute python script %s", response) diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index e8f67220..3e716627 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -3,11 +3,10 @@ core node services """ import tkinter as tk from tkinter import messagebox, ttk -from typing import TYPE_CHECKING, Any, Set +from typing import TYPE_CHECKING, Any from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.serviceconfig import ServiceConfigDialog -from core.gui.nodeutils import NodeUtils from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CheckboxList, ListboxScroll @@ -17,13 +16,7 @@ if TYPE_CHECKING: class NodeServiceDialog(Dialog): - def __init__( - self, - master: Any, - app: "Application", - canvas_node: "CanvasNode", - services: Set[str] = None, - ): + def __init__(self, master: Any, app: "Application", canvas_node: "CanvasNode"): title = f"{canvas_node.core_node.name} Services" super().__init__(master, app, title, modal=True) self.app = app @@ -32,24 +25,7 @@ class NodeServiceDialog(Dialog): self.groups = None self.services = None self.current = None - if services is None: - services = canvas_node.core_node.services - model = canvas_node.core_node.model - if len(services) == 0: - # not custom node type and node's services haven't been modified before - if not NodeUtils.is_custom( - canvas_node.core_node.type, canvas_node.core_node.model - ) and not self.app.core.service_been_modified(self.node_id): - services = set(self.app.core.default_services[model]) - # services of default type nodes were modified to be empty - elif canvas_node.core_node.id in self.app.core.modified_service_nodes: - services = set() - else: - services = set( - NodeUtils.get_custom_node_services(self.app.guiconfig, model) - ) - else: - services = set(services) + services = set(canvas_node.core_node.services) self.current_services = services self.draw() @@ -103,7 +79,7 @@ class NodeServiceDialog(Dialog): button.grid(row=0, column=1, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Remove", command=self.click_remove) button.grid(row=0, column=2, sticky="ew", padx=PADX) - button = ttk.Button(frame, text="Cancel", command=self.click_cancel) + button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=3, sticky="ew") # trigger group change @@ -154,22 +130,7 @@ class NodeServiceDialog(Dialog): def click_save(self): core_node = self.canvas_node.core_node - # custom type node or CORE node with custom services - if ( - core_node.model not in self.app.core.default_services - or self.current_services != self.app.core.default_services[core_node.model] - ): - core_node.services[:] = self.current_services - self.app.core.modified_service_nodes.add(core_node.id) - # custom services CORE node but modified back to having default services - # or just CORE nodes that don't get any change - else: - core_node.services[:] = self.current_services - self.app.core.modified_service_nodes.discard(core_node.id) - self.destroy() - - def click_cancel(self): - self.current_services = None + core_node.services[:] = self.current_services self.destroy() def click_remove(self): diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 40a941b1..fb0f39e0 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -936,11 +936,6 @@ class CanvasGraph(tk.Canvas): node.service_file_configs = deepcopy(canvas_node.service_file_configs) node.config_service_configs = deepcopy(canvas_node.config_service_configs) - # add new node to modified_service_nodes set if that set contains the - # to_copy node - if self.core.service_been_modified(core_node.id): - self.core.modified_service_nodes.add(copy.id) - copy_map[canvas_node.id] = node.id self.core.canvas_nodes[copy.id] = node self.nodes[node.id] = node From 9a42368221e68abf271ef9d4733b9410abcc27cc Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 May 2020 13:39:27 -0700 Subject: [PATCH 0211/1131] initial changes to mimic prior address creation --- daemon/core/gui/coreclient.py | 8 +++- daemon/core/gui/interface.py | 87 +++++++++++++++++++++++------------ daemon/core/gui/nodeutils.py | 7 ++- 3 files changed, 70 insertions(+), 32 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index b6b13c58..523c8bb8 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -830,6 +830,7 @@ class CoreClient: such as link, configurations, interfaces """ edges = set() + removed_links = [] for canvas_node in canvas_nodes: node_id = canvas_node.core_node.id if node_id not in self.canvas_nodes: @@ -841,11 +842,14 @@ class CoreClient: if edge in edges: continue edges.add(edge) - self.links.pop(edge.token, None) + edge = self.links.pop(edge.token, None) + if edge is not None: + removed_links.append(edge.link) + self.interfaces_manager.removed(removed_links) def create_interface(self, canvas_node: CanvasNode) -> core_pb2.Interface: node = canvas_node.core_node - ip4, ip6 = self.interfaces_manager.get_ips(node.id) + ip4, ip6 = self.interfaces_manager.get_ips(node) ip4_mask = self.interfaces_manager.ip4_mask ip6_mask = self.interfaces_manager.ip6_mask interface_id = len(canvas_node.interfaces) diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index 359dba8e..3310da90 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -1,6 +1,5 @@ import logging -import random -from typing import TYPE_CHECKING, Set, Union +from typing import TYPE_CHECKING, List, Set, Tuple, Union import netaddr from netaddr import EUI, IPNetwork @@ -14,20 +13,20 @@ if TYPE_CHECKING: from core.gui.graph.node import CanvasNode -def random_mac(): - return ("{:02x}" * 6).format(*[random.randrange(256) for _ in range(6)]) - - class Subnets: def __init__(self, ip4: IPNetwork, ip6: IPNetwork) -> None: self.ip4 = ip4 self.ip6 = ip6 + self.used_indexes = set() def __eq__(self, other: "Subnets") -> bool: - return (self.ip4, self.ip6) == (other.ip4, other.ip6) + return self.key() == other.key() def __hash__(self) -> int: - return hash((self.ip4, self.ip6)) + return hash(self.key()) + + def key(self) -> Tuple[IPNetwork, IPNetwork]: + return self.ip4, self.ip6 def next(self) -> "Subnets": return Subnets(self.ip4.next(), self.ip6.next()) @@ -47,6 +46,7 @@ class InterfaceManager: self.mac = EUI(mac) self.current_mac = None self.current_subnets = None + self.used_subnets = {} def update_ips(self, ip4: str, ip6: str) -> None: self.reset() @@ -65,37 +65,66 @@ class InterfaceManager: return mac def next_subnets(self) -> Subnets: - # define currently used subnets - used_subnets = set() - for edge in self.app.core.links.values(): - link = edge.link - subnets = None - if link.HasField("interface_one"): - subnets = self.get_subnets(link.interface_one) - if link.HasField("interface_two"): - subnets = self.get_subnets(link.interface_two) - if subnets: - used_subnets.add(subnets) - - # find next available subnets - subnets = Subnets(self.ip4_subnets, self.ip6_subnets) - while subnets in used_subnets: + subnets = self.current_subnets + if subnets is None: + subnets = Subnets(self.ip4_subnets, self.ip6_subnets) + while subnets.key() in self.used_subnets: subnets = subnets.next() + self.used_subnets[subnets.key()] = subnets return subnets def reset(self): self.current_subnets = None + self.used_subnets.clear() - def get_ips(self, node_id: int) -> [str, str]: - ip4 = self.current_subnets.ip4[node_id] - ip6 = self.current_subnets.ip6[node_id] + def removed(self, links: List["core_pb2.Link"]): + # get remaining subnets + remaining_subnets = set() + + for link in links: + if link.HasField("interface_one"): + subnets = self.get_subnets(link.interface_one) + if subnets not in remaining_subnets: + self.used_subnets.pop(subnets.key(), None) + if link.HasField("interface_two"): + subnets = self.get_subnets(link.interface_two) + if subnets not in remaining_subnets: + self.used_subnets.pop(subnets.key(), None) + + def initialize_links(self, links: List["core_pb2.Link"]): + for link in links: + if link.HasField("interface_one"): + subnets = self.get_subnets(link.interface_one) + if subnets.key() not in self.used_subnets: + self.used_subnets[subnets.key()] = subnets + if link.HasField("interface_two"): + subnets = self.get_subnets(link.interface_two) + if subnets.key() not in self.used_subnets: + self.used_subnets[subnets.key()] = subnets + + def next_index(self, node: "core_pb2.Node") -> int: + if NodeUtils.is_router_node(node): + index = 1 + else: + index = 20 + while True: + if index not in self.current_subnets.used_indexes: + self.current_subnets.used_indexes.add(index) + break + index += 1 + return index + + def get_ips(self, node: "core_pb2.Node") -> [str, str]: + index = self.next_index(node) + ip4 = self.current_subnets.ip4[index] + ip6 = self.current_subnets.ip6[index] return str(ip4), str(ip6) - @classmethod - def get_subnets(cls, interface: "core_pb2.Interface") -> Subnets: + def get_subnets(self, interface: "core_pb2.Interface") -> Subnets: ip4_subnet = IPNetwork(f"{interface.ip4}/{interface.ip4mask}").cidr ip6_subnet = IPNetwork(f"{interface.ip6}/{interface.ip6mask}").cidr - return Subnets(ip4_subnet, ip6_subnet) + subnets = Subnets(ip4_subnet, ip6_subnet) + return self.used_subnets.get(subnets.key(), subnets) def determine_subnets( self, canvas_src_node: "CanvasNode", canvas_dst_node: "CanvasNode" diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index 7ccb7ca3..24c01f06 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -1,7 +1,7 @@ import logging from typing import TYPE_CHECKING, Dict, List, Optional, Set, Union -from core.api.grpc.core_pb2 import NodeType +from core.api.grpc.core_pb2 import Node, NodeType from core.gui.images import ImageEnum, Images, TypeToImage if TYPE_CHECKING: @@ -64,8 +64,13 @@ class NodeUtils: RJ45_NODES = {NodeType.RJ45} IGNORE_NODES = {NodeType.CONTROL_NET, NodeType.PEER_TO_PEER} NODE_MODELS = {"router", "host", "PC", "mdr", "prouter"} + ROUTER_NODES = {"router", "mdr"} ANTENNA_ICON = None + @classmethod + def is_router_node(cls, node: Node) -> bool: + return cls.is_model_node(node.type) and node.model in cls.ROUTER_NODES + @classmethod def is_ignore_node(cls, node_type: NodeType) -> bool: return node_type in cls.IGNORE_NODES From 4a7abe71e406786e6b673164acb7dd545a578146 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 May 2020 13:42:15 -0700 Subject: [PATCH 0212/1131] removed unwanted grpc client stream log --- daemon/core/api/grpc/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 871e75e7..76e20426 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -493,7 +493,6 @@ class CoreGrpcClient: """ request = core_pb2.EventsRequest(session_id=session_id, events=events) stream = self.stub.Events(request) - logging.info("STREAM TYPE: %s", type(stream)) start_streamer(stream, handler) return stream From f7281459ed73dbd4101635128d0405a075c38712 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 May 2020 14:13:44 -0700 Subject: [PATCH 0213/1131] pygui changes to avoid deleting session and open xml race conditions, fix to reset canvas view options when creating a new session --- daemon/core/emulator/coreemu.py | 1 - daemon/core/gui/coreclient.py | 2 +- daemon/core/gui/graph/graph.py | 9 +++++++++ daemon/core/gui/menubar.py | 8 ++------ 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py index 6c6f1418..90f75427 100644 --- a/daemon/core/emulator/coreemu.py +++ b/daemon/core/emulator/coreemu.py @@ -128,5 +128,4 @@ class CoreEmu: result = True else: logging.error("session to delete did not exist: %s", _id) - return result diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index f3ec6612..79347baa 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -614,7 +614,7 @@ class CoreClient: Open core xml """ try: - response = self.client.open_xml(file_path) + response = self._client.open_xml(file_path) logging.info("open xml file %s, response: %s", file_path, response) self.join_session(response.session_id) except grpc.RpcError as e: diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index fb0f39e0..7c241bf7 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -133,6 +133,15 @@ class CanvasGraph(tk.Canvas): # hide context self.hide_context() + # reset view options to default state + self.show_node_labels.set(True) + self.show_link_labels.set(True) + self.show_grid.set(True) + self.show_annotations.set(True) + self.show_interface_names.set(False) + self.show_ip4s.set(True) + self.show_ip6s.set(True) + # delete any existing drawn items for tag in tags.COMPONENT_TAGS: self.delete(tag) diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index fafe6b5c..b5ae9ac7 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -387,12 +387,8 @@ class Menubar(tk.Menu): if self.core.is_runtime(): result = messagebox.askyesnocancel("Exit", "Stop the running session?") if result: - callback = None - if quit_app: - callback = self.app.quit - task = BackgroundTask(self.app, self.core.delete_session, callback) - task.start() - elif quit_app: + self.core.delete_session() + if quit_app: self.app.quit() def click_new(self) -> None: From 2e9968c306bd36a27ed669c74a8d50ba7b611063 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 May 2020 15:35:53 -0700 Subject: [PATCH 0214/1131] pygui further changes to mimic old gui behavior, parsing link data when joining and removing link data when they are removed --- daemon/core/gui/coreclient.py | 3 ++ daemon/core/gui/interface.py | 68 +++++++++++++++++++++++++---------- 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index ef50ace2..8c05a30f 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -283,6 +283,9 @@ class CoreClient: response = self.client.get_emane_config(self.session_id) self.emane_config = response.config + # update interface manager + self.interfaces_manager.joined(session.links) + # draw session self.app.canvas.reset_and_redraw(session) diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index 3310da90..8e2f4aa6 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -1,5 +1,5 @@ import logging -from typing import TYPE_CHECKING, List, Set, Tuple, Union +from typing import TYPE_CHECKING, Any, List, Optional, Set, Tuple import netaddr from netaddr import EUI, IPNetwork @@ -13,13 +13,22 @@ if TYPE_CHECKING: from core.gui.graph.node import CanvasNode +def get_index(interface: "core_pb2.Interface") -> int: + net = netaddr.IPNetwork(f"{interface.ip4}/{interface.ip4mask}") + ip_value = net.value + cidr_value = net.cidr.value + return ip_value - cidr_value + + class Subnets: def __init__(self, ip4: IPNetwork, ip6: IPNetwork) -> None: self.ip4 = ip4 self.ip6 = ip6 self.used_indexes = set() - def __eq__(self, other: "Subnets") -> bool: + def __eq__(self, other: Any) -> bool: + if not isinstance(other, Subnets): + return False return self.key() == other.key() def __hash__(self) -> int: @@ -73,34 +82,55 @@ class InterfaceManager: self.used_subnets[subnets.key()] = subnets return subnets - def reset(self): + def reset(self) -> None: self.current_subnets = None self.used_subnets.clear() - def removed(self, links: List["core_pb2.Link"]): + def removed(self, links: List["core_pb2.Link"]) -> None: # get remaining subnets remaining_subnets = set() - - for link in links: + for edge in self.app.core.links.values(): + link = edge.link if link.HasField("interface_one"): subnets = self.get_subnets(link.interface_one) - if subnets not in remaining_subnets: - self.used_subnets.pop(subnets.key(), None) + remaining_subnets.add(subnets) if link.HasField("interface_two"): subnets = self.get_subnets(link.interface_two) - if subnets not in remaining_subnets: - self.used_subnets.pop(subnets.key(), None) + remaining_subnets.add(subnets) - def initialize_links(self, links: List["core_pb2.Link"]): + # remove all subnets from used subnets when no longer present + # or remove used indexes from subnet + interfaces = [] for link in links: if link.HasField("interface_one"): - subnets = self.get_subnets(link.interface_one) - if subnets.key() not in self.used_subnets: - self.used_subnets[subnets.key()] = subnets + interfaces.append(link.interface_one) if link.HasField("interface_two"): - subnets = self.get_subnets(link.interface_two) - if subnets.key() not in self.used_subnets: - self.used_subnets[subnets.key()] = subnets + interfaces.append(link.interface_two) + for interface in interfaces: + subnets = self.get_subnets(interface) + if subnets not in remaining_subnets: + if self.current_subnets == subnets: + self.current_subnets = None + self.used_subnets.pop(subnets.key(), None) + else: + index = get_index(interface) + subnets.used_indexes.discard(index) + + def joined(self, links: List["core_pb2.Link"]) -> None: + interfaces = [] + for link in links: + if link.HasField("interface_one"): + interfaces.append(link.interface_one) + if link.HasField("interface_two"): + interfaces.append(link.interface_two) + + # add to used subnets and mark used indexes + for interface in interfaces: + subnets = self.get_subnets(interface) + index = get_index(interface) + subnets.used_indexes.add(index) + if subnets.key() not in self.used_subnets: + self.used_subnets[subnets.key()] = subnets def next_index(self, node: "core_pb2.Node") -> int: if NodeUtils.is_router_node(node): @@ -128,7 +158,7 @@ class InterfaceManager: def determine_subnets( self, canvas_src_node: "CanvasNode", canvas_dst_node: "CanvasNode" - ): + ) -> None: src_node = canvas_src_node.core_node dst_node = canvas_dst_node.core_node is_src_container = NodeUtils.is_container_node(src_node.type) @@ -152,7 +182,7 @@ class InterfaceManager: def find_subnets( self, canvas_node: "CanvasNode", visited: Set[int] = None - ) -> Union[IPNetwork, None]: + ) -> Optional[IPNetwork]: logging.info("finding subnet for node: %s", canvas_node.core_node.name) canvas = self.app.canvas subnets = None From 4ae5936bdc01cbc79f5d992425a4bc59500a1e70 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 May 2020 17:28:28 -0700 Subject: [PATCH 0215/1131] pygui raise copied nodes above copied edges --- daemon/core/gui/graph/graph.py | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 7c241bf7..9e60f131 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -998,6 +998,7 @@ class CanvasGraph(tk.Canvas): width=self.itemcget(edge.id, "width"), fill=self.itemcget(edge.id, "fill"), ) + self.tag_raise(tags.NODE) def scale_graph(self): for nid, canvas_node in self.nodes.items(): From 686026d9f2ce02a9aeae195def64d13e272d35c3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 May 2020 17:40:53 -0700 Subject: [PATCH 0216/1131] improved netaddr mac dialect usage to leverage constructor parameter --- daemon/core/api/tlv/coreapi.py | 3 +-- daemon/core/gui/dialogs/nodeconfig.py | 3 +-- daemon/core/gui/interface.py | 6 ++---- daemon/core/utils.py | 6 ++---- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/daemon/core/api/tlv/coreapi.py b/daemon/core/api/tlv/coreapi.py index b8021b9f..df60e374 100644 --- a/daemon/core/api/tlv/coreapi.py +++ b/daemon/core/api/tlv/coreapi.py @@ -350,8 +350,7 @@ class CoreTlvDataMacAddr(CoreTlvDataObj): """ # only use 48 bits value = binascii.hexlify(value[2:]).decode() - mac = netaddr.EUI(value) - mac.dialect = netaddr.mac_unix + mac = netaddr.EUI(value, dialect=netaddr.mac_unix_expanded) return str(mac) diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index 9d10a083..3048b15c 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -354,8 +354,7 @@ class NodeConfigDialog(Dialog): error = True break elif not auto_mac: - mac = netaddr.EUI(mac) - mac.dialect = netaddr.mac_unix_expanded + mac = netaddr.EUI(mac, dialect=netaddr.mac_unix_expanded) interface.mac = str(mac) # redraw diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index 8e2f4aa6..6f5f5fff 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -52,7 +52,7 @@ class InterfaceManager: self.ip4_subnets = IPNetwork(f"{ip4}/{self.ip4_mask}") self.ip6_subnets = IPNetwork(f"{ip6}/{self.ip6_mask}") mac = self.app.guiconfig.get("mac", appconfig.DEFAULT_MAC) - self.mac = EUI(mac) + self.mac = EUI(mac, dialect=netaddr.mac_unix_expanded) self.current_mac = None self.current_subnets = None self.used_subnets = {} @@ -64,13 +64,11 @@ class InterfaceManager: def reset_mac(self) -> None: self.current_mac = self.mac - self.current_mac.dialect = netaddr.mac_unix_expanded def next_mac(self) -> str: mac = str(self.current_mac) value = self.current_mac.value + 1 - self.current_mac = EUI(value) - self.current_mac.dialect = netaddr.mac_unix_expanded + self.current_mac = EUI(value, dialect=netaddr.mac_unix_expanded) return mac def next_subnets(self) -> Subnets: diff --git a/daemon/core/utils.py b/daemon/core/utils.py index 0eb9fef1..f1f74dbe 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -437,8 +437,7 @@ def random_mac() -> str: """ value = random.randint(0, 0xFFFFFF) value |= 0x00163E << 24 - mac = netaddr.EUI(value) - mac.dialect = netaddr.mac_unix_expanded + mac = netaddr.EUI(value, dialect=netaddr.mac_unix_expanded) return str(mac) @@ -450,8 +449,7 @@ def validate_mac(value: str) -> str: :return: unix formatted mac """ try: - mac = netaddr.EUI(value) - mac.dialect = netaddr.mac_unix_expanded + mac = netaddr.EUI(value, dialect=netaddr.mac_unix_expanded) return str(mac) except netaddr.AddrFormatError as e: raise CoreError(f"invalid mac address {value}: {e}") From 0ee679d978f6c106a9741f9f5b87046ae1d14890 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 May 2020 18:05:54 -0700 Subject: [PATCH 0217/1131] pygui changes to disable most widgets related to configuring a node during runtime --- daemon/core/gui/dialogs/nodeconfig.py | 29 ++++++++++++++++----------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index 3048b15c..6af82746 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -122,6 +122,10 @@ class NodeConfigDialog(Dialog): self.top.columnconfigure(0, weight=1) row = 0 + # field states + state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL + combo_state = tk.DISABLED if self.app.core.is_runtime() else "readonly" + # field frame frame = ttk.Frame(self.top) frame.grid(sticky="ew") @@ -148,6 +152,7 @@ class NodeConfigDialog(Dialog): textvariable=self.name, validate="key", validatecommand=(self.app.validation.name, "%P"), + state=state, ) entry.bind( "", lambda event: self.app.validation.focus_out(event, "noname") @@ -163,7 +168,7 @@ class NodeConfigDialog(Dialog): frame, textvariable=self.type, values=list(NodeUtils.NODE_MODELS), - state="readonly", + state=combo_state, ) combobox.grid(row=row, column=1, sticky="ew") row += 1 @@ -172,7 +177,7 @@ class NodeConfigDialog(Dialog): if NodeUtils.is_image_node(self.node.type): label = ttk.Label(frame, text="Image") label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) - entry = ttk.Entry(frame, textvariable=self.container_image) + entry = ttk.Entry(frame, textvariable=self.container_image, state=state) entry.grid(row=row, column=1, sticky="ew") row += 1 @@ -185,7 +190,7 @@ class NodeConfigDialog(Dialog): servers = ["localhost"] servers.extend(list(sorted(self.app.core.servers.keys()))) combobox = ttk.Combobox( - frame, textvariable=self.server, values=servers, state="readonly" + frame, textvariable=self.server, values=servers, state=combo_state ) combobox.grid(row=row, column=1, sticky="ew") row += 1 @@ -194,6 +199,7 @@ class NodeConfigDialog(Dialog): response = self.app.core.client.get_interfaces() logging.debug("host machine available interfaces: %s", response) interfaces = ListboxScroll(frame) + interfaces.listbox.config(state=state) interfaces.grid( row=row, column=0, columnspan=2, sticky="ew", padx=PADX, pady=PADY ) @@ -213,7 +219,7 @@ class NodeConfigDialog(Dialog): notebook = ttk.Notebook(self.top) notebook.grid(sticky="nsew", pady=PADY) self.top.rowconfigure(notebook.grid_info()["row"], weight=1) - + state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL for interface in self.canvas_node.interfaces: logging.info("interface: %s", interface) tab = ttk.Frame(notebook, padding=FRAME_PAD) @@ -237,16 +243,15 @@ class NodeConfigDialog(Dialog): label = ttk.Label(tab, text="MAC") label.grid(row=row, column=0, padx=PADX, pady=PADY) auto_set = not interface.mac - if auto_set: - state = tk.DISABLED - else: - state = tk.NORMAL + mac_state = tk.DISABLED if auto_set else tk.NORMAL is_auto = tk.BooleanVar(value=auto_set) - checkbutton = ttk.Checkbutton(tab, text="Auto?", variable=is_auto) + checkbutton = ttk.Checkbutton( + tab, text="Auto?", variable=is_auto, state=state + ) checkbutton.var = is_auto checkbutton.grid(row=row, column=1, padx=PADX) mac = tk.StringVar(value=interface.mac) - entry = ttk.Entry(tab, textvariable=mac, state=state) + entry = ttk.Entry(tab, textvariable=mac, state=mac_state) entry.grid(row=row, column=2, sticky="ew") func = partial(mac_auto, is_auto, entry, mac) checkbutton.config(command=func) @@ -258,7 +263,7 @@ class NodeConfigDialog(Dialog): if interface.ip4: ip4_net = f"{interface.ip4}/{interface.ip4mask}" ip4 = tk.StringVar(value=ip4_net) - entry = ttk.Entry(tab, textvariable=ip4) + entry = ttk.Entry(tab, textvariable=ip4, state=state) entry.grid(row=row, column=1, columnspan=2, sticky="ew") row += 1 @@ -268,7 +273,7 @@ class NodeConfigDialog(Dialog): if interface.ip6: ip6_net = f"{interface.ip6}/{interface.ip6mask}" ip6 = tk.StringVar(value=ip6_net) - entry = ttk.Entry(tab, textvariable=ip6) + entry = ttk.Entry(tab, textvariable=ip6, state=state) entry.grid(row=row, column=1, columnspan=2, sticky="ew") self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6) From ea4271d7cb23a7972f2f2530dd359d133bc2525f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 May 2020 19:15:53 -0700 Subject: [PATCH 0218/1131] changed defaultroute service to behave similarly as before and use the first interface for a default .1 address --- .../configservices/utilservices/services.py | 21 +++++++----------- daemon/core/services/utility.py | 22 +++++++------------ 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/daemon/core/configservices/utilservices/services.py b/daemon/core/configservices/utilservices/services.py index 75b5c745..8ddf1cc7 100644 --- a/daemon/core/configservices/utilservices/services.py +++ b/daemon/core/configservices/utilservices/services.py @@ -4,7 +4,6 @@ import netaddr from core import utils from core.configservice.base import ConfigService, ConfigServiceMode -from core.nodes.base import CoreNode GROUP_NAME = "Utility" @@ -26,18 +25,14 @@ class DefaultRouteService(ConfigService): def data(self) -> Dict[str, Any]: # only add default routes for linked routing nodes routes = [] - for other_node in self.node.session.nodes.values(): - if not isinstance(other_node, CoreNode): - continue - if other_node.type not in ["router", "mdr"]: - continue - commonnets = self.node.commonnets(other_node) - if commonnets: - _, _, router_eth = commonnets[0] - for x in router_eth.addrlist: - addr, prefix = x.split("/") - routes.append(addr) - break + netifs = self.node.netifs(sort=True) + if netifs: + netif = netifs[0] + for x in netif.addrlist: + net = netaddr.IPNetwork(x).cidr + if net.size > 1: + router = net[1] + routes.append(str(router)) return dict(routes=routes) diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index 028c2c0b..8a6e828b 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -7,7 +7,6 @@ import netaddr from core import constants, utils from core.errors import CoreCommandError -from core.nodes.base import CoreNode from core.services.coreservices import CoreService, ServiceMode @@ -77,20 +76,15 @@ class DefaultRouteService(UtilService): @classmethod def generate_config(cls, node, filename): - # only add default routes for linked routing nodes routes = [] - for other_node in node.session.nodes.values(): - if not isinstance(other_node, CoreNode): - continue - if other_node.type not in ["router", "mdr"]: - continue - commonnets = node.commonnets(other_node) - if commonnets: - _, _, router_eth = commonnets[0] - for x in router_eth.addrlist: - addr, prefix = x.split("/") - routes.append(addr) - break + netifs = node.netifs(sort=True) + if netifs: + netif = netifs[0] + for x in netif.addrlist: + net = netaddr.IPNetwork(x).cidr + if net.size > 1: + router = net[1] + routes.append(str(router)) cfg = "#!/bin/sh\n" cfg += "# auto-generated by DefaultRoute service (utility.py)\n" for route in routes: From 9d1f5cfcc6f6ff3927eb0bfb7793bc94e51ddb19 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 May 2020 19:56:57 -0700 Subject: [PATCH 0219/1131] pygui most dialogs are modal, default dialogs to modal --- daemon/core/gui/dialogs/about.py | 2 +- daemon/core/gui/dialogs/alerts.py | 4 ++-- daemon/core/gui/dialogs/canvassizeandscale.py | 2 +- daemon/core/gui/dialogs/canvaswallpaper.py | 2 +- daemon/core/gui/dialogs/colorpicker.py | 2 +- daemon/core/gui/dialogs/configserviceconfig.py | 2 +- daemon/core/gui/dialogs/copyserviceconfig.py | 4 ++-- daemon/core/gui/dialogs/customnodes.py | 4 ++-- daemon/core/gui/dialogs/dialog.py | 2 +- daemon/core/gui/dialogs/emaneconfig.py | 9 +++------ daemon/core/gui/dialogs/executepython.py | 2 +- daemon/core/gui/dialogs/find.py | 2 +- daemon/core/gui/dialogs/hooks.py | 4 ++-- daemon/core/gui/dialogs/ipdialog.py | 2 +- daemon/core/gui/dialogs/linkconfig.py | 2 +- daemon/core/gui/dialogs/macdialog.py | 2 +- daemon/core/gui/dialogs/mobilityconfig.py | 5 +---- daemon/core/gui/dialogs/nodeconfig.py | 4 +--- daemon/core/gui/dialogs/nodeconfigservice.py | 2 +- daemon/core/gui/dialogs/nodeservice.py | 2 +- daemon/core/gui/dialogs/observers.py | 2 +- daemon/core/gui/dialogs/preferences.py | 2 +- daemon/core/gui/dialogs/runtool.py | 2 +- daemon/core/gui/dialogs/servers.py | 2 +- daemon/core/gui/dialogs/serviceconfig.py | 2 +- daemon/core/gui/dialogs/sessionoptions.py | 2 +- daemon/core/gui/dialogs/sessions.py | 2 +- daemon/core/gui/dialogs/shapemod.py | 2 +- daemon/core/gui/dialogs/throughput.py | 2 +- daemon/core/gui/dialogs/wlanconfig.py | 2 +- daemon/core/gui/errors.py | 2 +- 31 files changed, 37 insertions(+), 45 deletions(-) diff --git a/daemon/core/gui/dialogs/about.py b/daemon/core/gui/dialogs/about.py index bf498bb8..5402b1ab 100644 --- a/daemon/core/gui/dialogs/about.py +++ b/daemon/core/gui/dialogs/about.py @@ -36,7 +36,7 @@ THE POSSIBILITY OF SUCH DAMAGE.\ class AboutDialog(Dialog): def __init__(self, master: "Application", app: "Application"): - super().__init__(master, app, "About CORE", modal=True) + super().__init__(master, app, "About CORE") self.draw() def draw(self): diff --git a/daemon/core/gui/dialogs/alerts.py b/daemon/core/gui/dialogs/alerts.py index 6c07f214..b425a30c 100644 --- a/daemon/core/gui/dialogs/alerts.py +++ b/daemon/core/gui/dialogs/alerts.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: class AlertsDialog(Dialog): def __init__(self, master: "Application", app: "Application"): - super().__init__(master, app, "Alerts", modal=True) + super().__init__(master, app, "Alerts") self.app = app self.tree = None self.codetext = None @@ -125,7 +125,7 @@ class AlertsDialog(Dialog): class DaemonLog(Dialog): def __init__(self, master: tk.Widget, app: "Application"): - super().__init__(master, app, "core-daemon log", modal=True) + super().__init__(master, app, "core-daemon log") self.columnconfigure(0, weight=1) self.path = tk.StringVar(value="/var/log/core-daemon.log") self.draw() diff --git a/daemon/core/gui/dialogs/canvassizeandscale.py b/daemon/core/gui/dialogs/canvassizeandscale.py index f04b991c..9543d8c6 100644 --- a/daemon/core/gui/dialogs/canvassizeandscale.py +++ b/daemon/core/gui/dialogs/canvassizeandscale.py @@ -19,7 +19,7 @@ class SizeAndScaleDialog(Dialog): """ create an instance for size and scale object """ - super().__init__(master, app, "Canvas Size and Scale", modal=True) + super().__init__(master, app, "Canvas Size and Scale") self.canvas = self.app.canvas self.validation = app.validation self.section_font = font.Font(weight="bold") diff --git a/daemon/core/gui/dialogs/canvaswallpaper.py b/daemon/core/gui/dialogs/canvaswallpaper.py index fe3fbd79..3b32572e 100644 --- a/daemon/core/gui/dialogs/canvaswallpaper.py +++ b/daemon/core/gui/dialogs/canvaswallpaper.py @@ -21,7 +21,7 @@ class CanvasWallpaperDialog(Dialog): """ create an instance of CanvasWallpaper object """ - super().__init__(master, app, "Canvas Background", modal=True) + super().__init__(master, app, "Canvas Background") self.canvas = self.app.canvas self.scale_option = tk.IntVar(value=self.canvas.scale_option.get()) self.adjust_to_dim = tk.BooleanVar(value=self.canvas.adjust_to_dim.get()) diff --git a/daemon/core/gui/dialogs/colorpicker.py b/daemon/core/gui/dialogs/colorpicker.py index b5a7d924..742e64f2 100644 --- a/daemon/core/gui/dialogs/colorpicker.py +++ b/daemon/core/gui/dialogs/colorpicker.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: class ColorPickerDialog(Dialog): def __init__(self, master: Any, app: "Application", initcolor: str = "#000000"): - super().__init__(master, app, "color picker", modal=True) + super().__init__(master, app, "color picker") self.red_entry = None self.blue_entry = None self.green_entry = None diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py index 2239034e..45ea3a76 100644 --- a/daemon/core/gui/dialogs/configserviceconfig.py +++ b/daemon/core/gui/dialogs/configserviceconfig.py @@ -29,7 +29,7 @@ class ConfigServiceConfigDialog(Dialog): node_id: int, ): title = f"{service_name} Config Service" - super().__init__(master, app, title, modal=True) + super().__init__(master, app, title) self.master = master self.app = app self.core = app.core diff --git a/daemon/core/gui/dialogs/copyserviceconfig.py b/daemon/core/gui/dialogs/copyserviceconfig.py index c1b4376c..87c86fd3 100644 --- a/daemon/core/gui/dialogs/copyserviceconfig.py +++ b/daemon/core/gui/dialogs/copyserviceconfig.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: class CopyServiceConfigDialog(Dialog): def __init__(self, master: Any, app: "Application", node_id: int): - super().__init__(master, app, f"Copy services to node {node_id}", modal=True) + super().__init__(master, app, f"Copy services to node {node_id}") self.parent = master self.app = app self.node_id = node_id @@ -177,7 +177,7 @@ class ViewConfigDialog(Dialog): data: str, filename: str = None, ): - super().__init__(master, app, f"n{node_id} config data", modal=True) + super().__init__(master, app, f"n{node_id} config data") self.data = data self.service_data = None self.filepath = tk.StringVar(value=f"/tmp/services.tmp-n{node_id}-{filename}") diff --git a/daemon/core/gui/dialogs/customnodes.py b/daemon/core/gui/dialogs/customnodes.py index 2cad2fef..e154483a 100644 --- a/daemon/core/gui/dialogs/customnodes.py +++ b/daemon/core/gui/dialogs/customnodes.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: class ServicesSelectDialog(Dialog): def __init__(self, master: Any, app: "Application", current_services: Set[str]): - super().__init__(master, app, "Node Services", modal=True) + super().__init__(master, app, "Node Services") self.groups = None self.services = None self.current = None @@ -101,7 +101,7 @@ class ServicesSelectDialog(Dialog): class CustomNodesDialog(Dialog): def __init__(self, master: "Application", app: "Application"): - super().__init__(master, app, "Custom Nodes", modal=True) + super().__init__(master, app, "Custom Nodes") self.edit_button = None self.delete_button = None self.nodes_list = None diff --git a/daemon/core/gui/dialogs/dialog.py b/daemon/core/gui/dialogs/dialog.py index 00532793..32708450 100644 --- a/daemon/core/gui/dialogs/dialog.py +++ b/daemon/core/gui/dialogs/dialog.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: class Dialog(tk.Toplevel): def __init__( - self, master: tk.Widget, app: "Application", title: str, modal: bool = False + self, master: tk.Widget, app: "Application", title: str, modal: bool = True ): super().__init__(master) self.withdraw() diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index f200cd6e..a7835751 100644 --- a/daemon/core/gui/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -21,7 +21,7 @@ if TYPE_CHECKING: class GlobalEmaneDialog(Dialog): def __init__(self, master: Any, app: "Application"): - super().__init__(master, app, "EMANE Configuration", modal=True) + super().__init__(master, app, "EMANE Configuration") self.config_frame = None self.draw() @@ -60,10 +60,7 @@ class EmaneModelDialog(Dialog): interface: int = None, ): super().__init__( - master, - app, - f"{canvas_node.core_node.name} {model} Configuration", - modal=True, + master, app, f"{canvas_node.core_node.name} {model} Configuration" ) self.canvas_node = canvas_node self.node = canvas_node.core_node @@ -117,7 +114,7 @@ class EmaneConfigDialog(Dialog): self, master: "Application", app: "Application", canvas_node: "CanvasNode" ): super().__init__( - master, app, f"{canvas_node.core_node.name} EMANE Configuration", modal=True + master, app, f"{canvas_node.core_node.name} EMANE Configuration" ) self.app = app self.canvas_node = canvas_node diff --git a/daemon/core/gui/dialogs/executepython.py b/daemon/core/gui/dialogs/executepython.py index 9adf4f93..e0a1a40d 100644 --- a/daemon/core/gui/dialogs/executepython.py +++ b/daemon/core/gui/dialogs/executepython.py @@ -9,7 +9,7 @@ from core.gui.themes import FRAME_PAD, PADX class ExecutePythonDialog(Dialog): def __init__(self, master, app): - super().__init__(master, app, "Execute Python Script", modal=True) + super().__init__(master, app, "Execute Python Script") self.app = app self.with_options = tk.IntVar(value=0) self.options = tk.StringVar(value="") diff --git a/daemon/core/gui/dialogs/find.py b/daemon/core/gui/dialogs/find.py index 4d7bc6bd..74543aa3 100644 --- a/daemon/core/gui/dialogs/find.py +++ b/daemon/core/gui/dialogs/find.py @@ -8,7 +8,7 @@ from core.gui.themes import FRAME_PAD, PADX, PADY class FindDialog(Dialog): def __init__(self, master, app) -> None: - super().__init__(master, app, "Find", modal=True) + super().__init__(master, app, "Find", modal=False) self.find_text = tk.StringVar(value="") self.tree = None diff --git a/daemon/core/gui/dialogs/hooks.py b/daemon/core/gui/dialogs/hooks.py index ad8ad533..f9da431a 100644 --- a/daemon/core/gui/dialogs/hooks.py +++ b/daemon/core/gui/dialogs/hooks.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: class HookDialog(Dialog): def __init__(self, master: Any, app: "Application"): - super().__init__(master, app, "Hook", modal=True) + super().__init__(master, app, "Hook") self.name = tk.StringVar() self.codetext = None self.hook = core_pb2.Hook() @@ -89,7 +89,7 @@ class HookDialog(Dialog): class HooksDialog(Dialog): def __init__(self, master: "Application", app: "Application"): - super().__init__(master, app, "Hooks", modal=True) + super().__init__(master, app, "Hooks") self.listbox = None self.edit_button = None self.delete_button = None diff --git a/daemon/core/gui/dialogs/ipdialog.py b/daemon/core/gui/dialogs/ipdialog.py index 58c06fb2..72e0d73a 100644 --- a/daemon/core/gui/dialogs/ipdialog.py +++ b/daemon/core/gui/dialogs/ipdialog.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: class IpConfigDialog(Dialog): def __init__(self, master: "Application", app: "Application") -> None: - super().__init__(master, app, "IP Configuration", modal=True) + super().__init__(master, app, "IP Configuration") ip_config = self.app.guiconfig.setdefault("ips") self.ip4 = ip_config.setdefault("ip4", appconfig.DEFAULT_IP4) self.ip6 = ip_config.setdefault("ip6", appconfig.DEFAULT_IP6) diff --git a/daemon/core/gui/dialogs/linkconfig.py b/daemon/core/gui/dialogs/linkconfig.py index f809059a..5a93d3fa 100644 --- a/daemon/core/gui/dialogs/linkconfig.py +++ b/daemon/core/gui/dialogs/linkconfig.py @@ -33,7 +33,7 @@ def get_float(var: tk.StringVar) -> Union[float, None]: class LinkConfigurationDialog(Dialog): def __init__(self, master: "CanvasGraph", app: "Application", edge: "CanvasEdge"): - super().__init__(master, app, "Link Configuration", modal=True) + super().__init__(master, app, "Link Configuration") self.app = app self.edge = edge self.is_symmetric = edge.link.options.unidirectional is False diff --git a/daemon/core/gui/dialogs/macdialog.py b/daemon/core/gui/dialogs/macdialog.py index fa5d81ad..558c3c29 100644 --- a/daemon/core/gui/dialogs/macdialog.py +++ b/daemon/core/gui/dialogs/macdialog.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: class MacConfigDialog(Dialog): def __init__(self, master: "Application", app: "Application") -> None: - super().__init__(master, app, "MAC Configuration", modal=True) + super().__init__(master, app, "MAC Configuration") mac = self.app.guiconfig.get("mac", appconfig.DEFAULT_MAC) self.mac_var = tk.StringVar(value=mac) self.draw() diff --git a/daemon/core/gui/dialogs/mobilityconfig.py b/daemon/core/gui/dialogs/mobilityconfig.py index 61cbfc14..2222e06f 100644 --- a/daemon/core/gui/dialogs/mobilityconfig.py +++ b/daemon/core/gui/dialogs/mobilityconfig.py @@ -21,10 +21,7 @@ class MobilityConfigDialog(Dialog): self, master: "Application", app: "Application", canvas_node: "CanvasNode" ): super().__init__( - master, - app, - f"{canvas_node.core_node.name} Mobility Configuration", - modal=True, + master, app, f"{canvas_node.core_node.name} Mobility Configuration" ) self.canvas_node = canvas_node self.node = canvas_node.core_node diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index 6af82746..ff21f886 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -100,9 +100,7 @@ class NodeConfigDialog(Dialog): """ create an instance of node configuration """ - super().__init__( - master, app, f"{canvas_node.core_node.name} Configuration", modal=True - ) + super().__init__(master, app, f"{canvas_node.core_node.name} Configuration") self.canvas_node = canvas_node self.node = canvas_node.core_node self.image = canvas_node.image diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeconfigservice.py index c86d8887..f593526f 100644 --- a/daemon/core/gui/dialogs/nodeconfigservice.py +++ b/daemon/core/gui/dialogs/nodeconfigservice.py @@ -25,7 +25,7 @@ class NodeConfigServiceDialog(Dialog): services: Set[str] = None, ): title = f"{canvas_node.core_node.name} Config Services" - super().__init__(master, app, title, modal=True) + super().__init__(master, app, title) self.app = app self.canvas_node = canvas_node self.node_id = canvas_node.core_node.id diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index 3e716627..08abd308 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: class NodeServiceDialog(Dialog): def __init__(self, master: Any, app: "Application", canvas_node: "CanvasNode"): title = f"{canvas_node.core_node.name} Services" - super().__init__(master, app, title, modal=True) + super().__init__(master, app, title) self.app = app self.canvas_node = canvas_node self.node_id = canvas_node.core_node.id diff --git a/daemon/core/gui/dialogs/observers.py b/daemon/core/gui/dialogs/observers.py index 51e9fe88..1282789e 100644 --- a/daemon/core/gui/dialogs/observers.py +++ b/daemon/core/gui/dialogs/observers.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: class ObserverDialog(Dialog): def __init__(self, master: "Application", app: "Application"): - super().__init__(master, app, "Observer Widgets", modal=True) + super().__init__(master, app, "Observer Widgets") self.observers = None self.save_button = None self.delete_button = None diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index 0df71e7f..9c6ba5b9 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -17,7 +17,7 @@ SCALE_INTERVAL = 0.01 class PreferencesDialog(Dialog): def __init__(self, master: "Application", app: "Application"): - super().__init__(master, app, "Preferences", modal=True) + super().__init__(master, app, "Preferences") self.gui_scale = tk.DoubleVar(value=self.app.app_scale) preferences = self.app.guiconfig["preferences"] self.editor = tk.StringVar(value=preferences["editor"]) diff --git a/daemon/core/gui/dialogs/runtool.py b/daemon/core/gui/dialogs/runtool.py index 21a2f44b..c3e3dec9 100644 --- a/daemon/core/gui/dialogs/runtool.py +++ b/daemon/core/gui/dialogs/runtool.py @@ -9,7 +9,7 @@ from core.gui.widgets import CodeText, ListboxScroll class RunToolDialog(Dialog): def __init__(self, master, app) -> None: - super().__init__(master, app, "Run Tool", modal=True) + super().__init__(master, app, "Run Tool") self.cmd = tk.StringVar(value="ps ax") self.app = app self.result = None diff --git a/daemon/core/gui/dialogs/servers.py b/daemon/core/gui/dialogs/servers.py index c57e97d3..26a76835 100644 --- a/daemon/core/gui/dialogs/servers.py +++ b/daemon/core/gui/dialogs/servers.py @@ -17,7 +17,7 @@ DEFAULT_PORT = 50051 class ServersDialog(Dialog): def __init__(self, master: "Application", app: "Application"): - super().__init__(master, app, "CORE Servers", modal=True) + super().__init__(master, app, "CORE Servers") self.name = tk.StringVar(value=DEFAULT_NAME) self.address = tk.StringVar(value=DEFAULT_ADDRESS) self.port = tk.IntVar(value=DEFAULT_PORT) diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index 33d38323..8fc85394 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -29,7 +29,7 @@ class ServiceConfigDialog(Dialog): node_id: int, ): title = f"{service_name} Service" - super().__init__(master, app, title, modal=True) + super().__init__(master, app, title) self.master = master self.app = app self.core = app.core diff --git a/daemon/core/gui/dialogs/sessionoptions.py b/daemon/core/gui/dialogs/sessionoptions.py index a3f738a7..c042eef4 100644 --- a/daemon/core/gui/dialogs/sessionoptions.py +++ b/daemon/core/gui/dialogs/sessionoptions.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: class SessionOptionsDialog(Dialog): def __init__(self, master: "Application", app: "Application"): - super().__init__(master, app, "Session Options", modal=True) + super().__init__(master, app, "Session Options") self.config_frame = None self.has_error = False self.config = self.get_config() diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index 3671b308..288f8b4b 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -20,7 +20,7 @@ class SessionsDialog(Dialog): def __init__( self, master: "Application", app: "Application", is_start_app: bool = False ) -> None: - super().__init__(master, app, "Sessions", modal=True) + super().__init__(master, app, "Sessions") self.is_start_app = is_start_app self.selected_session = None self.selected_id = None diff --git a/daemon/core/gui/dialogs/shapemod.py b/daemon/core/gui/dialogs/shapemod.py index f47cb7b3..9efb9fa3 100644 --- a/daemon/core/gui/dialogs/shapemod.py +++ b/daemon/core/gui/dialogs/shapemod.py @@ -25,7 +25,7 @@ class ShapeDialog(Dialog): title = "Add Shape" else: title = "Add Text" - super().__init__(master, app, title, modal=True) + super().__init__(master, app, title) self.canvas = app.canvas self.fill = None self.border = None diff --git a/daemon/core/gui/dialogs/throughput.py b/daemon/core/gui/dialogs/throughput.py index 96aa3bc5..5c6b1d28 100644 --- a/daemon/core/gui/dialogs/throughput.py +++ b/daemon/core/gui/dialogs/throughput.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: class ThroughputDialog(Dialog): def __init__(self, master: "Application", app: "Application"): - super().__init__(master, app, "Throughput Config", modal=False) + super().__init__(master, app, "Throughput Config") self.app = app self.canvas = app.canvas self.show_throughput = tk.IntVar(value=1) diff --git a/daemon/core/gui/dialogs/wlanconfig.py b/daemon/core/gui/dialogs/wlanconfig.py index a777c7d4..d5d0c673 100644 --- a/daemon/core/gui/dialogs/wlanconfig.py +++ b/daemon/core/gui/dialogs/wlanconfig.py @@ -21,7 +21,7 @@ class WlanConfigDialog(Dialog): self, master: "Application", app: "Application", canvas_node: "CanvasNode" ): super().__init__( - master, app, f"{canvas_node.core_node.name} Wlan Configuration", modal=True + master, app, f"{canvas_node.core_node.name} Wlan Configuration" ) self.canvas_node = canvas_node self.node = canvas_node.core_node diff --git a/daemon/core/gui/errors.py b/daemon/core/gui/errors.py index 1f9353d8..782a795d 100644 --- a/daemon/core/gui/errors.py +++ b/daemon/core/gui/errors.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: class ErrorDialog(Dialog): def __init__(self, master, app: "Application", title: str, details: str) -> None: - super().__init__(master, app, "CORE Exception", modal=True) + super().__init__(master, app, "CORE Exception") self.title = title self.details = details self.error_message = None From d158fc99c6fc20ad3019bdad1623bed57b7fc319 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 May 2020 19:59:41 -0700 Subject: [PATCH 0220/1131] pygui small cleanup to layout of find dialog --- daemon/core/gui/dialogs/find.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/daemon/core/gui/dialogs/find.py b/daemon/core/gui/dialogs/find.py index 74543aa3..2541c74e 100644 --- a/daemon/core/gui/dialogs/find.py +++ b/daemon/core/gui/dialogs/find.py @@ -9,7 +9,6 @@ from core.gui.themes import FRAME_PAD, PADX, PADY class FindDialog(Dialog): def __init__(self, master, app) -> None: super().__init__(master, app, "Find", modal=False) - self.find_text = tk.StringVar(value="") self.tree = None self.draw() @@ -18,13 +17,11 @@ class FindDialog(Dialog): def draw(self) -> None: self.top.columnconfigure(0, weight=1) - self.top.rowconfigure(0, weight=1) - self.top.rowconfigure(1, weight=5) - self.top.rowconfigure(2, weight=1) + self.top.rowconfigure(1, weight=1) # Find node frame frame = ttk.Frame(self.top, padding=FRAME_PAD) - frame.grid(sticky="nsew") + frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Find:") label.grid() @@ -35,7 +32,7 @@ class FindDialog(Dialog): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - frame.grid(sticky="nsew", padx=PADX, pady=PADY) + frame.grid(sticky="nsew", pady=PADY) self.tree = ttk.Treeview( frame, columns=("nodeid", "name", "location", "detail"), @@ -54,26 +51,23 @@ class FindDialog(Dialog): self.tree.heading("location", text="Location") self.tree.column("detail", stretch=tk.YES, anchor="center") self.tree.heading("detail", text="Detail") - self.tree.bind("<>", self.click_select) - yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) yscrollbar.grid(row=0, column=1, sticky="ns") self.tree.configure(yscrollcommand=yscrollbar.set) - xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) xscrollbar.grid(row=1, sticky="ew") self.tree.configure(xscrollcommand=xscrollbar.set) # button frame frame = ttk.Frame(self.top) - frame.grid(sticky="nsew") + frame.grid(sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Find", command=self.find_node) button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.close_dialog) - button.grid(row=0, column=1, sticky="ew", padx=PADX) + button.grid(row=0, column=1, sticky="ew") def clear_treeview_items(self) -> None: """ From 5a8984de106d613e64d03b23069579c9abd2048b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 May 2020 23:36:33 -0700 Subject: [PATCH 0221/1131] pygui some delete node/link cleanup and added unlink option to node context menu for an easier unlinking --- daemon/core/gui/coreclient.py | 24 +++++++++--------------- daemon/core/gui/graph/graph.py | 29 ++++++++++++++++++++++------- daemon/core/gui/graph/node.py | 18 ++++++++++++++++++ 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 8c05a30f..fc4fc64f 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -6,7 +6,7 @@ import logging import os from pathlib import Path from tkinter import messagebox -from typing import TYPE_CHECKING, Dict, List +from typing import TYPE_CHECKING, Dict, Iterable, List import grpc @@ -830,27 +830,21 @@ class CoreClient: ) return node - def delete_graph_nodes(self, canvas_nodes: List[core_pb2.Node]): + def deleted_graph_nodes(self, canvas_nodes: List[core_pb2.Node]): """ remove the nodes selected by the user and anything related to that node such as link, configurations, interfaces """ - edges = set() - removed_links = [] for canvas_node in canvas_nodes: node_id = canvas_node.core_node.id - if node_id not in self.canvas_nodes: - logging.error("unknown node: %s", node_id) - continue del self.canvas_nodes[node_id] - for edge in canvas_node.edges: - if edge in edges: - continue - edges.add(edge) - edge = self.links.pop(edge.token, None) - if edge is not None: - removed_links.append(edge.link) - self.interfaces_manager.removed(removed_links) + + def deleted_graph_edges(self, edges: Iterable[CanvasEdge]) -> None: + links = [] + for edge in edges: + del self.links[edge.token] + links.append(edge.link) + self.interfaces_manager.removed(links) def create_interface(self, canvas_node: CanvasNode) -> core_pb2.Interface: node = canvas_node.core_node diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 9e60f131..22d21b51 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -517,15 +517,13 @@ class CanvasGraph(tk.Canvas): canvas_node.delete() nodes.append(canvas_node) is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type) - # delete related edges for edge in canvas_node.edges: if edge in edges: continue edges.add(edge) - self.edges.pop(edge.token, None) + del self.edges[edge.token] edge.delete() - # update node connected to edge being deleted other_id = edge.src other_interface = edge.src_interface @@ -534,10 +532,8 @@ class CanvasGraph(tk.Canvas): other_interface = edge.dst_interface other_node = self.nodes[other_id] other_node.edges.remove(edge) - try: + if other_interface in other_node.interfaces: other_node.interfaces.remove(other_interface) - except ValueError: - pass if is_wireless: other_node.delete_antenna() @@ -547,7 +543,26 @@ class CanvasGraph(tk.Canvas): shape.delete() self.selection.clear() - self.core.delete_graph_nodes(nodes) + self.core.deleted_graph_nodes(nodes) + self.core.deleted_graph_edges(edges) + + def delete_edge(self, edge: CanvasEdge): + edge.delete() + del self.edges[edge.token] + src_node = self.nodes[edge.src] + src_node.edges.discard(edge) + if edge.src_interface in src_node.interfaces: + src_node.interfaces.remove(edge.src_interface) + dst_node = self.nodes[edge.dst] + dst_node.edges.discard(edge) + if edge.dst_interface in dst_node.interfaces: + dst_node.interfaces.remove(edge.dst_interface) + src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type) + if src_wireless: + dst_node.delete_antenna() + dst_wireless = NodeUtils.is_wireless_node(dst_node.core_node.type) + if dst_wireless: + src_node.delete_antenna() def zoom(self, event: tk.Event, factor: float = None): if not factor: diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 90896284..5b3aed10 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -1,3 +1,4 @@ +import functools import logging import tkinter as tk from typing import TYPE_CHECKING @@ -15,6 +16,7 @@ from core.gui.dialogs.nodeservice import NodeServiceDialog from core.gui.dialogs.wlanconfig import WlanConfigDialog from core.gui.errors import show_grpc_error from core.gui.graph import tags +from core.gui.graph.edges import CanvasEdge from core.gui.graph.tooltip import CanvasTooltip from core.gui.images import ImageEnum, Images from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils @@ -230,6 +232,18 @@ class CanvasNode: label="Link To Selected", command=self.wireless_link_selected ) context.add_command(label="Select Members", state=tk.DISABLED) + unlink_menu = tk.Menu(context) + for edge in self.edges: + other_id = edge.src + if self.id == other_id: + other_id = edge.dst + other_node = self.canvas.nodes[other_id] + func_unlink = functools.partial(self.click_unlink, edge) + unlink_menu.add_command( + label=other_node.core_node.name, command=func_unlink + ) + themes.style_menu(unlink_menu) + context.add_cascade(label="Unlink", menu=unlink_menu) edit_menu = tk.Menu(context) themes.style_menu(edit_menu) edit_menu.add_command(label="Cut", command=self.click_cut) @@ -242,6 +256,10 @@ class CanvasNode: self.canvas_copy() self.canvas_delete() + def click_unlink(self, edge: CanvasEdge) -> None: + self.canvas.delete_edge(edge) + self.app.core.deleted_graph_edges([edge]) + def canvas_delete(self) -> None: self.canvas.clear_selection() self.canvas.selection[self.id] = self From 491f2a8e9342b6abd049ee1809001aec4e122604 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 May 2020 23:47:37 -0700 Subject: [PATCH 0222/1131] pygui enabled delete on link context menu, removed split/merge for now, set edge labels to bold to stand out until better solution --- daemon/core/gui/app.py | 4 +++- daemon/core/gui/graph/edges.py | 9 ++++----- daemon/core/gui/graph/graph.py | 1 + daemon/core/gui/graph/node.py | 1 - 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 0f40a594..13d10dd0 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -50,7 +50,9 @@ class Application(tk.Frame): text_scale = self.app_scale if self.app_scale < 1 else math.sqrt(self.app_scale) themes.scale_fonts(self.fonts_size, self.app_scale) self.icon_text_font = font.Font(family="TkIconFont", size=int(12 * text_scale)) - self.edge_font = font.Font(family="TkDefaultFont", size=int(8 * text_scale)) + self.edge_font = font.Font( + family="TkDefaultFont", size=int(8 * text_scale), weight=font.BOLD + ) def setup_theme(self): themes.load(self.style) diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index b70fe6b2..8aa63de4 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -377,15 +377,14 @@ class CanvasEdge(Edge): context = tk.Menu(self.canvas) themes.style_menu(context) context.add_command(label="Configure", command=self.configure) - context.add_command(label="Delete") - context.add_command(label="Split") - context.add_command(label="Merge") + context.add_command(label="Delete", command=self.click_delete) if self.canvas.app.core.is_runtime(): context.entryconfigure(1, state="disabled") - context.entryconfigure(2, state="disabled") - context.entryconfigure(3, state="disabled") context.post(event.x_root, event.y_root) + def click_delete(self): + self.canvas.delete_edge(self) + def configure(self) -> None: dialog = LinkConfigurationDialog(self.canvas, self.canvas.app, self) dialog.show() diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 22d21b51..74040e64 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -563,6 +563,7 @@ class CanvasGraph(tk.Canvas): dst_wireless = NodeUtils.is_wireless_node(dst_node.core_node.type) if dst_wireless: src_node.delete_antenna() + self.core.deleted_graph_edges([edge]) def zoom(self, event: tk.Event, factor: float = None): if not factor: diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 5b3aed10..cee0e822 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -258,7 +258,6 @@ class CanvasNode: def click_unlink(self, edge: CanvasEdge) -> None: self.canvas.delete_edge(edge) - self.app.core.deleted_graph_edges([edge]) def canvas_delete(self) -> None: self.canvas.clear_selection() From 65466909d33c1077bb36d14caa82181f145c9d03 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 2 May 2020 08:41:10 -0700 Subject: [PATCH 0223/1131] pygui improved edge context by properly using tk_popup --- daemon/core/gui/graph/edges.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 8aa63de4..17809dcb 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -268,9 +268,16 @@ class CanvasEdge(Edge): self.throughput = None self.draw(src_pos, dst_pos) self.set_binding() + self.context = tk.Menu(self.canvas) + self.create_context() + + def create_context(self): + themes.style_menu(self.context) + self.context.add_command(label="Configure", command=self.click_configure) + self.context.add_command(label="Delete", command=self.click_delete) def set_binding(self) -> None: - self.canvas.tag_bind(self.id, "", self.create_context) + self.canvas.tag_bind(self.id, "", self.show_context) def set_link(self, link) -> None: self.link = link @@ -373,18 +380,14 @@ class CanvasEdge(Edge): self.middle_label = None self.canvas.itemconfig(self.id, fill=self.color, width=self.scaled_width()) - def create_context(self, event: tk.Event) -> None: - context = tk.Menu(self.canvas) - themes.style_menu(context) - context.add_command(label="Configure", command=self.configure) - context.add_command(label="Delete", command=self.click_delete) - if self.canvas.app.core.is_runtime(): - context.entryconfigure(1, state="disabled") - context.post(event.x_root, event.y_root) + def show_context(self, event: tk.Event) -> None: + state = tk.DISABLED if self.canvas.core.is_runtime() else tk.NORMAL + self.context.entryconfigure(1, state=state) + self.context.tk_popup(event.x_root, event.y_root) def click_delete(self): self.canvas.delete_edge(self) - def configure(self) -> None: + def click_configure(self) -> None: dialog = LinkConfigurationDialog(self.canvas, self.canvas.app, self) dialog.show() From ac2d60dad6121313452aaf325ecb53104bb705a3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 2 May 2020 09:20:36 -0700 Subject: [PATCH 0224/1131] pygui improved node context to properly use tk_popup, avoiding bandage code to compensate for other issues --- daemon/core/gui/graph/graph.py | 85 +++++++++++----------------------- daemon/core/gui/graph/node.py | 69 ++++++++++++++------------- daemon/core/gui/toolbar.py | 2 - 3 files changed, 62 insertions(+), 94 deletions(-) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 74040e64..60df2440 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -58,7 +58,6 @@ class CanvasGraph(tk.Canvas): self.select_box = None self.selected = None self.node_draw = None - self.context = None self.nodes = {} self.edges = {} self.shapes = {} @@ -130,9 +129,6 @@ class CanvasGraph(tk.Canvas): client. :param session: session to draw """ - # hide context - self.hide_context() - # reset view options to default state self.show_node_labels.set(True) self.show_link_labels.set(True) @@ -166,7 +162,6 @@ class CanvasGraph(tk.Canvas): self.bind("", self.click_press) self.bind("", self.click_release) self.bind("", self.click_motion) - self.bind("", self.click_context) self.bind("", self.press_delete) self.bind("", self.ctrl_click) self.bind("", self.double_click) @@ -176,11 +171,6 @@ class CanvasGraph(tk.Canvas): self.bind("", lambda e: self.scan_mark(e.x, e.y)) self.bind("", lambda e: self.scan_dragto(e.x, e.y, gain=1)) - def hide_context(self, event=None): - if self.context: - self.context.unpost() - self.context = None - def get_actual_coords(self, x: float, y: float) -> [float, float]: actual_x = (x - self.offset[0]) / self.ratio actual_y = (y - self.offset[1]) / self.ratio @@ -396,41 +386,35 @@ class CanvasGraph(tk.Canvas): x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): return - - if self.context: - self.hide_context() + if self.mode == GraphMode.ANNOTATION: + self.focus_set() + if self.shape_drawing: + shape = self.shapes[self.selected] + shape.shape_complete(x, y) + self.shape_drawing = False + elif self.mode == GraphMode.SELECT: + self.focus_set() + if self.select_box: + x0, y0, x1, y1 = self.coords(self.select_box.id) + inside = [ + x + for x in self.find_enclosed(x0, y0, x1, y1) + if "node" in self.gettags(x) or "shape" in self.gettags(x) + ] + for i in inside: + self.select_object(i, True) + self.select_box.disappear() + self.select_box = None else: - if self.mode == GraphMode.ANNOTATION: - self.focus_set() - if self.shape_drawing: - shape = self.shapes[self.selected] - shape.shape_complete(x, y) - self.shape_drawing = False - elif self.mode == GraphMode.SELECT: - self.focus_set() - if self.select_box: - x0, y0, x1, y1 = self.coords(self.select_box.id) - inside = [ - x - for x in self.find_enclosed(x0, y0, x1, y1) - if "node" in self.gettags(x) or "shape" in self.gettags(x) - ] - for i in inside: - self.select_object(i, True) - self.select_box.disappear() - self.select_box = None - else: - self.focus_set() - self.selected = self.get_selected(event) - logging.debug( - f"click release selected({self.selected}) mode({self.mode})" - ) - if self.mode == GraphMode.EDGE: - self.handle_edge_release(event) - elif self.mode == GraphMode.NODE: - self.add_node(x, y) - elif self.mode == GraphMode.PICKNODE: - self.mode = GraphMode.NODE + self.focus_set() + self.selected = self.get_selected(event) + logging.debug(f"click release selected({self.selected}) mode({self.mode})") + if self.mode == GraphMode.EDGE: + self.handle_edge_release(event) + elif self.mode == GraphMode.NODE: + self.add_node(x, y) + elif self.mode == GraphMode.PICKNODE: + self.mode = GraphMode.NODE self.selected = None def handle_edge_release(self, _event: tk.Event): @@ -717,19 +701,6 @@ class CanvasGraph(tk.Canvas): if self.select_box and self.mode == GraphMode.SELECT: self.select_box.shape_motion(x, y) - def click_context(self, event: tk.Event): - logging.info("context: %s", self.context) - if not self.context: - selected = self.get_selected(event) - canvas_node = self.nodes.get(selected) - if canvas_node: - logging.debug("node context: %s", selected) - self.context = canvas_node.create_context() - self.context.bind("", self.hide_context) - self.context.post(event.x_root, event.y_root) - else: - self.hide_context() - def press_delete(self, _event: tk.Event): """ delete selected nodes and any data that relates to it diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index cee0e822..5bc92db0 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -68,11 +68,14 @@ class CanvasNode: self.service_file_configs = {} self.config_service_configs = {} self.setup_bindings() + self.context = tk.Menu(self.canvas) + themes.style_menu(self.context) def setup_bindings(self): self.canvas.tag_bind(self.id, "", self.double_click) self.canvas.tag_bind(self.id, "", self.on_enter) self.canvas.tag_bind(self.id, "", self.on_leave) + self.canvas.tag_bind(self.id, "", self.show_context) def delete(self): logging.debug("Delete canvas node for %s", self.core_node) @@ -188,51 +191,55 @@ class CanvasNode: else: self.show_config() - def create_context(self) -> tk.Menu: + def show_context(self, event: tk.Event) -> None: + # clear existing menu + self.context.delete(0, tk.END) is_wlan = self.core_node.type == NodeType.WIRELESS_LAN is_emane = self.core_node.type == NodeType.EMANE - context = tk.Menu(self.canvas) - themes.style_menu(context) if self.app.core.is_runtime(): - context.add_command(label="Configure", command=self.show_config) + self.context.add_command(label="Configure", command=self.show_config) if NodeUtils.is_container_node(self.core_node.type): - context.add_command(label="Services", state=tk.DISABLED) - context.add_command(label="Config Services", state=tk.DISABLED) + self.context.add_command(label="Services", state=tk.DISABLED) + self.context.add_command(label="Config Services", state=tk.DISABLED) if is_wlan: - context.add_command(label="WLAN Config", command=self.show_wlan_config) + self.context.add_command( + label="WLAN Config", command=self.show_wlan_config + ) if is_wlan and self.core_node.id in self.app.core.mobility_players: - context.add_command( + self.context.add_command( label="Mobility Player", command=self.show_mobility_player ) - context.add_command(label="Select Adjacent", state=tk.DISABLED) + self.context.add_command(label="Select Adjacent", state=tk.DISABLED) if NodeUtils.is_container_node(self.core_node.type): - context.add_command(label="Shell Window", state=tk.DISABLED) - context.add_command(label="Tcpdump", state=tk.DISABLED) - context.add_command(label="Tshark", state=tk.DISABLED) - context.add_command(label="Wireshark", state=tk.DISABLED) - context.add_command(label="View Log", state=tk.DISABLED) + self.context.add_command(label="Shell Window", state=tk.DISABLED) + self.context.add_command(label="Tcpdump", state=tk.DISABLED) + self.context.add_command(label="Tshark", state=tk.DISABLED) + self.context.add_command(label="Wireshark", state=tk.DISABLED) + self.context.add_command(label="View Log", state=tk.DISABLED) else: - context.add_command(label="Configure", command=self.show_config) + self.context.add_command(label="Configure", command=self.show_config) if NodeUtils.is_container_node(self.core_node.type): - context.add_command(label="Services", command=self.show_services) - context.add_command( + self.context.add_command(label="Services", command=self.show_services) + self.context.add_command( label="Config Services", command=self.show_config_services ) if is_emane: - context.add_command( + self.context.add_command( label="EMANE Config", command=self.show_emane_config ) if is_wlan: - context.add_command(label="WLAN Config", command=self.show_wlan_config) - context.add_command( + self.context.add_command( + label="WLAN Config", command=self.show_wlan_config + ) + self.context.add_command( label="Mobility Config", command=self.show_mobility_config ) if NodeUtils.is_wireless_node(self.core_node.type): - context.add_command( + self.context.add_command( label="Link To Selected", command=self.wireless_link_selected ) - context.add_command(label="Select Members", state=tk.DISABLED) - unlink_menu = tk.Menu(context) + self.context.add_command(label="Select Members", state=tk.DISABLED) + unlink_menu = tk.Menu(self.context) for edge in self.edges: other_id = edge.src if self.id == other_id: @@ -243,14 +250,14 @@ class CanvasNode: label=other_node.core_node.name, command=func_unlink ) themes.style_menu(unlink_menu) - context.add_cascade(label="Unlink", menu=unlink_menu) - edit_menu = tk.Menu(context) + self.context.add_cascade(label="Unlink", menu=unlink_menu) + edit_menu = tk.Menu(self.context) themes.style_menu(edit_menu) edit_menu.add_command(label="Cut", command=self.click_cut) edit_menu.add_command(label="Copy", command=self.canvas_copy) edit_menu.add_command(label="Delete", command=self.canvas_delete) - context.add_cascade(label="Edit", menu=edit_menu) - return context + self.context.add_cascade(label="Edit", menu=edit_menu) + self.context.tk_popup(event.x_root, event.y_root) def click_cut(self) -> None: self.canvas_copy() @@ -270,39 +277,32 @@ class CanvasNode: self.canvas.copy() def show_config(self): - self.canvas.context = None dialog = NodeConfigDialog(self.app, self.app, self) dialog.show() def show_wlan_config(self): - self.canvas.context = None dialog = WlanConfigDialog(self.app, self.app, self) if not dialog.has_error: dialog.show() def show_mobility_config(self): - self.canvas.context = None dialog = MobilityConfigDialog(self.app, self.app, self) if not dialog.has_error: dialog.show() def show_mobility_player(self): - self.canvas.context = None mobility_player = self.app.core.mobility_players[self.core_node.id] mobility_player.show() def show_emane_config(self): - self.canvas.context = None dialog = EmaneConfigDialog(self.app, self.app, self) dialog.show() def show_services(self): - self.canvas.context = None dialog = NodeServiceDialog(self.app.master, self.app, self) dialog.show() def show_config_services(self): - self.canvas.context = None dialog = NodeConfigServiceDialog(self.app.master, self.app, self) dialog.show() @@ -324,7 +324,6 @@ class CanvasNode: return result def wireless_link_selected(self): - self.canvas.context = None for canvas_nid in [ x for x in self.canvas.selection if "node" in self.canvas.gettags(x) ]: diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 11e2896a..2304f010 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -263,7 +263,6 @@ class Toolbar(ttk.Frame): Start session handler redraw buttons, send node and link messages to grpc server. """ - self.app.canvas.hide_context() self.app.menubar.change_menubar_item_state(is_runtime=True) self.app.statusbar.progress_bar.start(5) self.app.canvas.mode = GraphMode.SELECT @@ -453,7 +452,6 @@ class Toolbar(ttk.Frame): redraw buttons on the toolbar, send node and link messages to grpc server """ logging.info("Click stop button") - self.app.canvas.hide_context() self.app.menubar.change_menubar_item_state(is_runtime=False) self.app.statusbar.progress_bar.start(5) self.time = time.perf_counter() From be70c5383ef67aaadd337807828b9152f8f06f79 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 2 May 2020 09:23:06 -0700 Subject: [PATCH 0225/1131] pygui removed manage members context from wireless node --- daemon/core/gui/graph/node.py | 1 - 1 file changed, 1 deletion(-) diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 5bc92db0..54f2e9fb 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -238,7 +238,6 @@ class CanvasNode: self.context.add_command( label="Link To Selected", command=self.wireless_link_selected ) - self.context.add_command(label="Select Members", state=tk.DISABLED) unlink_menu = tk.Menu(self.context) for edge in self.edges: other_id = edge.src From b858e66c499fca51b024dbb12bdfd2a5fc71629f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 2 May 2020 23:51:42 -0700 Subject: [PATCH 0226/1131] pygui updated main app frame to use grid layout like everything else --- daemon/core/gui/app.py | 37 +++++++++++++------- daemon/core/gui/dialogs/nodeconfigservice.py | 4 +-- daemon/core/gui/dialogs/nodeservice.py | 6 ++-- daemon/core/gui/graph/graph.py | 33 +++++++++-------- daemon/core/gui/graph/node.py | 4 +-- daemon/core/gui/statusbar.py | 2 +- daemon/core/gui/themes.py | 9 +++-- daemon/core/gui/toolbar.py | 3 +- 8 files changed, 60 insertions(+), 38 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 13d10dd0..7781cc0d 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -16,7 +16,7 @@ WIDTH = 1000 HEIGHT = 800 -class Application(tk.Frame): +class Application(ttk.Frame): def __init__(self, proxy: bool): super().__init__(master=None) # load node icons @@ -25,6 +25,7 @@ class Application(tk.Frame): # widgets self.menubar = None self.toolbar = None + self.right_frame = None self.canvas = None self.statusbar = None self.validation = None @@ -66,8 +67,8 @@ class Application(tk.Frame): self.master.protocol("WM_DELETE_WINDOW", self.on_closing) image = Images.get(ImageEnum.CORE, 16) self.master.tk.call("wm", "iconphoto", self.master._w, image) - self.pack(fill=tk.BOTH, expand=True) self.validation = InputValidation(self) + self.master.option_add("*tearOff", tk.FALSE) def center(self): screen_width = self.master.winfo_screenwidth() @@ -79,9 +80,17 @@ class Application(tk.Frame): ) def draw(self): - self.master.option_add("*tearOff", tk.FALSE) + self.master.rowconfigure(0, weight=1) + self.master.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.columnconfigure(1, weight=1) + self.grid(sticky="nsew") self.toolbar = Toolbar(self, self) - self.toolbar.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) + self.toolbar.grid(sticky="ns") + self.right_frame = ttk.Frame(self) + self.right_frame.columnconfigure(0, weight=1) + self.right_frame.rowconfigure(0, weight=1) + self.right_frame.grid(row=0, column=1, sticky="nsew") self.draw_canvas() self.draw_status() self.menubar = Menubar(self.master, self) @@ -89,20 +98,24 @@ class Application(tk.Frame): def draw_canvas(self): width = self.guiconfig["preferences"]["width"] height = self.guiconfig["preferences"]["height"] - self.canvas = CanvasGraph(self, self.core, width, height) - self.canvas.pack(fill=tk.BOTH, expand=True) + canvas_frame = ttk.Frame(self.right_frame) + canvas_frame.rowconfigure(0, weight=1) + canvas_frame.columnconfigure(0, weight=1) + canvas_frame.grid(sticky="nsew", pady=1) + self.canvas = CanvasGraph(canvas_frame, self, self.core, width, height) + self.canvas.grid(sticky="nsew") + scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview) + scroll_y.grid(row=0, column=1, sticky="ns") scroll_x = ttk.Scrollbar( - self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview + canvas_frame, orient=tk.HORIZONTAL, command=self.canvas.xview ) - scroll_x.pack(side=tk.BOTTOM, fill=tk.X) - scroll_y = ttk.Scrollbar(self.canvas, command=self.canvas.yview) - scroll_y.pack(side=tk.RIGHT, fill=tk.Y) + scroll_x.grid(row=1, column=0, sticky="ew") self.canvas.configure(xscrollcommand=scroll_x.set) self.canvas.configure(yscrollcommand=scroll_y.set) def draw_status(self): - self.statusbar = StatusBar(master=self, app=self) - self.statusbar.pack(side=tk.BOTTOM, fill=tk.X) + self.statusbar = StatusBar(self.right_frame, self) + self.statusbar.grid(sticky="ew") def on_closing(self): self.menubar.prompt_save_running_session(True) diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeconfigservice.py index f593526f..0e5ba7bb 100644 --- a/daemon/core/gui/dialogs/nodeconfigservice.py +++ b/daemon/core/gui/dialogs/nodeconfigservice.py @@ -4,7 +4,7 @@ core node services import logging import tkinter as tk from tkinter import messagebox, ttk -from typing import TYPE_CHECKING, Any, Set +from typing import TYPE_CHECKING, Set from core.gui.dialogs.configserviceconfig import ConfigServiceConfigDialog from core.gui.dialogs.dialog import Dialog @@ -19,7 +19,7 @@ if TYPE_CHECKING: class NodeConfigServiceDialog(Dialog): def __init__( self, - master: Any, + master: tk.Widget, app: "Application", canvas_node: "CanvasNode", services: Set[str] = None, diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index 08abd308..6641bf56 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -3,7 +3,7 @@ core node services """ import tkinter as tk from tkinter import messagebox, ttk -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.serviceconfig import ServiceConfigDialog @@ -16,7 +16,9 @@ if TYPE_CHECKING: class NodeServiceDialog(Dialog): - def __init__(self, master: Any, app: "Application", canvas_node: "CanvasNode"): + def __init__( + self, master: tk.Widget, app: "Application", canvas_node: "CanvasNode" + ): title = f"{canvas_node.core_node.name} Services" super().__init__(master, app, title) self.app = app diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 60df2440..9f0e7bce 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -47,10 +47,15 @@ class ShowVar(BooleanVar): class CanvasGraph(tk.Canvas): def __init__( - self, master: "Application", core: "CoreClient", width: int, height: int + self, + master: tk.Widget, + app: "Application", + core: "CoreClient", + width: int, + height: int, ): super().__init__(master, highlightthickness=0, background="#cccccc") - self.app = master + self.app = app self.core = core self.mode = GraphMode.SELECT self.annotation_type = None @@ -67,7 +72,7 @@ class CanvasGraph(tk.Canvas): self.wireless_network = {} self.drawing_edge = None - self.grid = None + self.rect = None self.shape_drawing = False self.default_dimensions = (width, height) self.current_dimensions = self.default_dimensions @@ -107,12 +112,12 @@ class CanvasGraph(tk.Canvas): self.draw_grid() def draw_canvas(self, dimensions: Tuple[int, int] = None): - if self.grid is not None: - self.delete(self.grid) + if self.rect is not None: + self.delete(self.rect) if not dimensions: dimensions = self.default_dimensions self.current_dimensions = dimensions - self.grid = self.create_rectangle( + self.rect = self.create_rectangle( 0, 0, *dimensions, @@ -182,7 +187,7 @@ class CanvasGraph(tk.Canvas): return scaled_x, scaled_y def inside_canvas(self, x: float, y: float) -> [bool, bool]: - x1, y1, x2, y2 = self.bbox(self.grid) + x1, y1, x2, y2 = self.bbox(self.rect) valid_x = x1 <= x <= x2 valid_y = y1 <= y <= y2 return valid_x and valid_y @@ -219,7 +224,7 @@ class CanvasGraph(tk.Canvas): for i in range(0, height, 27): self.create_line(0, i, width, i, dash=(2, 4), tags=tags.GRIDLINE) self.tag_lower(tags.GRIDLINE) - self.tag_lower(self.grid) + self.tag_lower(self.rect) def add_wireless_edge( self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link @@ -293,7 +298,7 @@ class CanvasGraph(tk.Canvas): ) x = core_node.position.x y = core_node.position.y - node = CanvasNode(self.master, x, y, core_node, image) + node = CanvasNode(self.app, x, y, core_node, image) self.nodes[node.id] = node self.core.canvas_nodes[core_node.id] = node @@ -732,7 +737,7 @@ class CanvasGraph(tk.Canvas): self.node_draw.image = Images.get_custom( self.node_draw.image_file, int(ICON_SIZE * self.app.app_scale) ) - node = CanvasNode(self.master, x, y, core_node, self.node_draw.image) + node = CanvasNode(self.app, x, y, core_node, self.node_draw.image) self.core.canvas_nodes[core_node.id] = node self.nodes[node.id] = node return node @@ -741,7 +746,7 @@ class CanvasGraph(tk.Canvas): """ retrieve canvas width and height in pixels """ - x0, y0, x1, y1 = self.coords(self.grid) + x0, y0, x1, y1 = self.coords(self.rect) canvas_w = abs(x0 - x1) canvas_h = abs(y0 - y1) return canvas_w, canvas_h @@ -756,7 +761,7 @@ class CanvasGraph(tk.Canvas): self, image: ImageTk.PhotoImage, x: float = None, y: float = None ): if x is None and y is None: - x1, y1, x2, y2 = self.bbox(self.grid) + x1, y1, x2, y2 = self.bbox(self.rect) x = (x1 + x2) / 2 y = (y1 + y2) / 2 self.wallpaper_id = self.create_image((x, y), image=image, tags=tags.WALLPAPER) @@ -778,7 +783,7 @@ class CanvasGraph(tk.Canvas): image = ImageTk.PhotoImage(cropped) # draw on canvas - x1, y1, _, _ = self.bbox(self.grid) + x1, y1, _, _ = self.bbox(self.rect) x = (cropx / 2) + x1 y = (cropy / 2) + y1 self.draw_wallpaper(image, x, y) @@ -920,7 +925,7 @@ class CanvasGraph(tk.Canvas): copy = self.core.create_node( actual_x, actual_y, core_node.type, core_node.model ) - node = CanvasNode(self.master, scaled_x, scaled_y, copy, canvas_node.image) + node = CanvasNode(self.app, scaled_x, scaled_y, copy, canvas_node.image) # copy configurations and services node.core_node.services[:] = canvas_node.core_node.services diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 54f2e9fb..2ec0dbf8 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -298,11 +298,11 @@ class CanvasNode: dialog.show() def show_services(self): - dialog = NodeServiceDialog(self.app.master, self.app, self) + dialog = NodeServiceDialog(self.app, self.app, self) dialog.show() def show_config_services(self): - dialog = NodeConfigServiceDialog(self.app.master, self.app, self) + dialog = NodeConfigServiceDialog(self.app, self.app, self) dialog.show() def has_emane_link(self, interface_id: int) -> core_pb2.Node: diff --git a/daemon/core/gui/statusbar.py b/daemon/core/gui/statusbar.py index 7524e318..cd630b82 100644 --- a/daemon/core/gui/statusbar.py +++ b/daemon/core/gui/statusbar.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: class StatusBar(ttk.Frame): - def __init__(self, master: "Application", app: "Application", **kwargs): + def __init__(self, master: tk.Widget, app: "Application", **kwargs): super().__init__(master, **kwargs) self.app = app self.status = None diff --git a/daemon/core/gui/themes.py b/daemon/core/gui/themes.py index e9c5cba3..141a7a5c 100644 --- a/daemon/core/gui/themes.py +++ b/daemon/core/gui/themes.py @@ -181,21 +181,24 @@ def theme_change(event: tk.Event): Styles.green_alert, background="green", padding=0, - relief=tk.NONE, + relief=tk.RIDGE, + borderwidth=1, font="TkDefaultFont", ) style.configure( Styles.yellow_alert, background="yellow", padding=0, - relief=tk.NONE, + relief=tk.RIDGE, + borderwidth=1, font="TkDefaultFont", ) style.configure( Styles.red_alert, background="red", padding=0, - relief=tk.NONE, + relief=tk.RIDGE, + borderwidth=1, font="TkDefaultFont", ) diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 2304f010..144fdd1f 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -41,13 +41,12 @@ class Toolbar(ttk.Frame): Core toolbar class """ - def __init__(self, master: "Application", app: "Application", **kwargs): + def __init__(self, master: tk.Widget, app: "Application", **kwargs): """ Create a CoreToolbar instance """ super().__init__(master, **kwargs) self.app = app - self.master = app.master self.time = None # design buttons From 835675480b1c91bd5a252bee99127e03a01a0391 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 2 May 2020 23:57:27 -0700 Subject: [PATCH 0227/1131] pygui removed unimplemented runtime node context options and moved find node to tools menu --- daemon/core/gui/graph/node.py | 10 ---------- daemon/core/gui/menubar.py | 5 +++-- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 2ec0dbf8..758dbd26 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -198,9 +198,6 @@ class CanvasNode: is_emane = self.core_node.type == NodeType.EMANE if self.app.core.is_runtime(): self.context.add_command(label="Configure", command=self.show_config) - if NodeUtils.is_container_node(self.core_node.type): - self.context.add_command(label="Services", state=tk.DISABLED) - self.context.add_command(label="Config Services", state=tk.DISABLED) if is_wlan: self.context.add_command( label="WLAN Config", command=self.show_wlan_config @@ -209,13 +206,6 @@ class CanvasNode: self.context.add_command( label="Mobility Player", command=self.show_mobility_player ) - self.context.add_command(label="Select Adjacent", state=tk.DISABLED) - if NodeUtils.is_container_node(self.core_node.type): - self.context.add_command(label="Shell Window", state=tk.DISABLED) - self.context.add_command(label="Tcpdump", state=tk.DISABLED) - self.context.add_command(label="Tshark", state=tk.DISABLED) - self.context.add_command(label="Wireshark", state=tk.DISABLED) - self.context.add_command(label="View Log", state=tk.DISABLED) else: self.context.add_command(label="Configure", command=self.show_config) if NodeUtils.is_container_node(self.core_node.type): diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index b5ae9ac7..fa203c03 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -115,8 +115,8 @@ class Menubar(tk.Menu): Create edit menu """ menu = tk.Menu(self) - menu.add_command(label="Find", accelerator="Ctrl+F", command=self.click_find) menu.add_command(label="Preferences", command=self.click_preferences) + menu.add_separator() menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED) menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED) menu.add_separator() @@ -127,7 +127,6 @@ class Menubar(tk.Menu): label="Delete", accelerator="Ctrl+D", command=self.click_delete ) self.add_cascade(label="Edit", menu=menu) - self.app.master.bind_all("", self.click_find) self.app.master.bind_all("", self.click_cut) self.app.master.bind_all("", self.click_copy) self.app.master.bind_all("", self.click_paste) @@ -190,6 +189,8 @@ class Menubar(tk.Menu): Create tools menu """ menu = tk.Menu(self) + menu.add_command(label="Find", accelerator="Ctrl+F", command=self.click_find) + self.app.master.bind_all("", self.click_find) menu.add_command(label="Auto Grid", command=self.click_autogrid) menu.add_command(label="IP Addresses", command=self.click_ip_config) menu.add_command(label="MAC Addresses", command=self.click_mac_config) From 0999fabb1417461616fc89d8d59e5aa04126d9ef Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 3 May 2020 10:41:36 -0700 Subject: [PATCH 0228/1131] pygui revamped progress bar functionality into app task calls to simplify and commonize the functionality, handle and display task exceptions --- daemon/core/api/grpc/server.py | 1 - daemon/core/gui/app.py | 22 +++++++++- daemon/core/gui/dialogs/sessions.py | 10 ++--- daemon/core/gui/errors.py | 13 +++--- daemon/core/gui/menubar.py | 7 ++- daemon/core/gui/statusbar.py | 18 +++----- daemon/core/gui/task.py | 67 +++++++++++++---------------- daemon/core/gui/toolbar.py | 31 +++++-------- 8 files changed, 82 insertions(+), 87 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index ca5eb0ad..b6a298db 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -297,7 +297,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): for service_exception in boot_exception.args: exceptions.append(str(service_exception)) return core_pb2.StartSessionResponse(result=False, exceptions=exceptions) - return core_pb2.StartSessionResponse(result=True) def StopSession( diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 7781cc0d..e797f1de 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -1,6 +1,8 @@ import math +import time import tkinter as tk from tkinter import font, ttk +from tkinter.ttk import Progressbar from core.gui import appconfig, themes from core.gui.coreclient import CoreClient @@ -9,6 +11,7 @@ from core.gui.images import ImageEnum, Images from core.gui.menubar import Menubar from core.gui.nodeutils import NodeUtils from core.gui.statusbar import StatusBar +from core.gui.task import ProgressTask from core.gui.toolbar import Toolbar from core.gui.validation import InputValidation @@ -29,6 +32,8 @@ class Application(ttk.Frame): self.canvas = None self.statusbar = None self.validation = None + self.progress = None + self.time = None # fonts self.fonts_size = None @@ -93,6 +98,7 @@ class Application(ttk.Frame): self.right_frame.grid(row=0, column=1, sticky="nsew") self.draw_canvas() self.draw_status() + self.progress = Progressbar(self.right_frame, mode="indeterminate") self.menubar = Menubar(self.master, self) def draw_canvas(self): @@ -117,6 +123,21 @@ class Application(ttk.Frame): self.statusbar = StatusBar(self.right_frame, self) self.statusbar.grid(sticky="ew") + def progress_task(self, task: ProgressTask) -> None: + self.progress.grid(sticky="ew") + self.progress.start() + self.time = time.perf_counter() + task.app = self + task.start() + + def progress_task_complete(self) -> None: + self.progress.stop() + self.progress.grid_forget() + total = time.perf_counter() - self.time + self.time = None + message = f"Task ran for {total:.3f} seconds" + self.statusbar.set_status(message) + def on_closing(self): self.menubar.prompt_save_running_session(True) @@ -124,7 +145,6 @@ class Application(ttk.Frame): appconfig.save(self.guiconfig) def joined_session_update(self): - self.statusbar.progress_bar.stop() if self.core.is_runtime(): self.toolbar.set_runtime() else: diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index 288f8b4b..79a153f7 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -9,7 +9,7 @@ from core.api.grpc import core_pb2 from core.gui.dialogs.dialog import Dialog from core.gui.errors import show_grpc_error from core.gui.images import ImageEnum, Images -from core.gui.task import BackgroundTask +from core.gui.task import ProgressTask from core.gui.themes import PADX, PADY if TYPE_CHECKING: @@ -183,12 +183,11 @@ class SessionsDialog(Dialog): self.join_session(self.selected_session) def join_session(self, session_id: int) -> None: + self.destroy() if self.app.core.xml_file: self.app.core.xml_file = None - self.app.statusbar.progress_bar.start(5) - task = BackgroundTask(self.app, self.app.core.join_session, args=(session_id,)) - task.start() - self.destroy() + task = ProgressTask(self.app.core.join_session, args=(session_id,)) + self.app.progress_task(task) def double_click_join(self, _event: tk.Event) -> None: item = self.tree.selection() @@ -201,7 +200,6 @@ class SessionsDialog(Dialog): if not self.selected_session: return logging.debug("delete session: %s", self.selected_session) - # self.app.core.delete_session(self.selected_id, self.top) self.tree.delete(self.selected_id) self.app.core.delete_session(self.selected_session) if self.selected_session == self.app.core.session_id: diff --git a/daemon/core/gui/errors.py b/daemon/core/gui/errors.py index 782a795d..b11e684b 100644 --- a/daemon/core/gui/errors.py +++ b/daemon/core/gui/errors.py @@ -43,7 +43,12 @@ class ErrorDialog(Dialog): button.grid(sticky="ew") -def show_grpc_error(e: grpc.RpcError, master, app: "Application"): +def show_exception(app: "Application", title: str, exception: Exception) -> None: + dialog = ErrorDialog(app, app, title, str(exception)) + dialog.show() + + +def show_grpc_error(e: grpc.RpcError, master, app: "Application") -> None: title = [x.capitalize() for x in e.code().name.lower().split("_")] title = " ".join(title) title = f"GRPC {title}" @@ -51,8 +56,6 @@ def show_grpc_error(e: grpc.RpcError, master, app: "Application"): dialog.show() -def show_grpc_response_exceptions(class_name, exceptions, master, app: "Application"): - title = f"Exceptions from {class_name}" - detail = "\n".join([str(x) for x in exceptions]) - dialog = ErrorDialog(master, app, title, detail) +def show_error(app: "Application", title: str, message: str) -> None: + dialog = ErrorDialog(app, app, title, message) dialog.show() diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index fa203c03..a77913c3 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -22,7 +22,7 @@ from core.gui.dialogs.sessionoptions import SessionOptionsDialog from core.gui.dialogs.sessions import SessionsDialog from core.gui.dialogs.throughput import ThroughputDialog from core.gui.nodeutils import ICON_SIZE -from core.gui.task import BackgroundTask +from core.gui.task import ProgressTask if TYPE_CHECKING: from core.gui.app import Application @@ -340,9 +340,8 @@ class Menubar(tk.Menu): self.core.xml_file = filename self.core.xml_dir = str(os.path.dirname(filename)) self.prompt_save_running_session() - self.app.statusbar.progress_bar.start(5) - task = BackgroundTask(self.app, self.core.open_xml, args=(filename,)) - task.start() + task = ProgressTask(self.core.open_xml, args=(filename,)) + self.app.progress_task(task) def execute_python(self): dialog = ExecutePythonDialog(self.app, self.app) diff --git a/daemon/core/gui/statusbar.py b/daemon/core/gui/statusbar.py index cd630b82..1f882b08 100644 --- a/daemon/core/gui/statusbar.py +++ b/daemon/core/gui/statusbar.py @@ -18,7 +18,6 @@ class StatusBar(ttk.Frame): self.app = app self.status = None self.statusvar = tk.StringVar() - self.progress_bar = None self.zoom = None self.cpu_usage = None self.memory = None @@ -28,19 +27,14 @@ class StatusBar(ttk.Frame): self.draw() def draw(self): - self.columnconfigure(0, weight=1) - self.columnconfigure(1, weight=5) + self.columnconfigure(0, weight=7) + self.columnconfigure(1, weight=1) self.columnconfigure(2, weight=1) self.columnconfigure(3, weight=1) - self.columnconfigure(4, weight=1) frame = ttk.Frame(self, borderwidth=1, relief=tk.RIDGE) frame.grid(row=0, column=0, sticky="ew") frame.columnconfigure(0, weight=1) - self.progress_bar = ttk.Progressbar( - frame, orient="horizontal", mode="indeterminate" - ) - self.progress_bar.grid(sticky="ew") self.status = ttk.Label( self, @@ -49,7 +43,7 @@ class StatusBar(ttk.Frame): borderwidth=1, relief=tk.RIDGE, ) - self.status.grid(row=0, column=1, sticky="ew") + self.status.grid(row=0, column=0, sticky="ew") self.zoom = ttk.Label( self, @@ -58,17 +52,17 @@ class StatusBar(ttk.Frame): borderwidth=1, relief=tk.RIDGE, ) - self.zoom.grid(row=0, column=2, sticky="ew") + self.zoom.grid(row=0, column=1, sticky="ew") self.cpu_usage = ttk.Label( self, text="CPU TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE ) - self.cpu_usage.grid(row=0, column=3, sticky="ew") + self.cpu_usage.grid(row=0, column=2, sticky="ew") self.alerts_button = ttk.Button( self, text="Alerts", command=self.click_alerts, style=Styles.green_alert ) - self.alerts_button.grid(row=0, column=4, sticky="ew") + self.alerts_button.grid(row=0, column=3, sticky="ew") def click_alerts(self): dialog = AlertsDialog(self.app, self.app) diff --git a/daemon/core/gui/task.py b/daemon/core/gui/task.py index bee69be3..c88a0151 100644 --- a/daemon/core/gui/task.py +++ b/daemon/core/gui/task.py @@ -1,46 +1,39 @@ import logging import threading -from typing import Any, Callable +from typing import Any, Callable, Tuple -from core.gui.errors import show_grpc_response_exceptions +from core.gui.errors import show_exception -class BackgroundTask: - def __init__(self, master: Any, task: Callable, callback: Callable = None, args=()): - self.master = master - self.args = args +class ProgressTask: + def __init__( + self, task: Callable, callback: Callable = None, args: Tuple[Any] = None + ): + self.app = None self.task = task self.callback = callback - self.thread = None + self.args = args + if self.args is None: + self.args = () - def start(self): - logging.info("starting task") - self.thread = threading.Thread(target=self.run, daemon=True) - self.thread.start() + def start(self) -> None: + thread = threading.Thread(target=self.run, daemon=True) + thread.start() - def run(self): - result = self.task(*self.args) - logging.info("task completed") - # if start session fails, a response with Result: False and a list of - # exceptions is returned - if not getattr(result, "result", True): - if len(getattr(result, "exceptions", [])) > 0: - self.master.after( - 0, - show_grpc_response_exceptions, - *( - result.__class__.__name__, - result.exceptions, - self.master, - self.master, - ) - ) - if self.callback: - if result is None: - args = () - elif isinstance(result, (list, tuple)): - args = result - else: - args = (result,) - logging.info("calling callback: %s", args) - self.master.after(0, self.callback, *args) + def run(self) -> None: + logging.info("running task") + try: + values = self.task(*self.args) + if values is None: + values = () + elif values and not isinstance(values, tuple): + values = (values,) + if self.callback: + logging.info("calling callback") + self.app.after(0, self.callback, *values) + except Exception as e: + logging.exception("progress task exception") + args = (self.app, "Task Error", e) + self.app.after(0, show_exception, *args) + finally: + self.app.after(0, self.app.progress_task_complete) diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 144fdd1f..374a4eaf 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -1,5 +1,4 @@ import logging -import time import tkinter as tk from enum import Enum from functools import partial @@ -10,11 +9,12 @@ from core.api.grpc import core_pb2 from core.gui.dialogs.customnodes import CustomNodesDialog from core.gui.dialogs.marker import MarkerDialog from core.gui.dialogs.runtool import RunToolDialog +from core.gui.errors import show_error from core.gui.graph.enums import GraphMode from core.gui.graph.shapeutils import ShapeType, is_marker from core.gui.images import ImageEnum, Images from core.gui.nodeutils import NodeDraw, NodeUtils -from core.gui.task import BackgroundTask +from core.gui.task import ProgressTask from core.gui.themes import Styles from core.gui.tooltip import Tooltip @@ -47,7 +47,6 @@ class Toolbar(ttk.Frame): """ super().__init__(master, **kwargs) self.app = app - self.time = None # design buttons self.play_button = None @@ -263,22 +262,18 @@ class Toolbar(ttk.Frame): server. """ self.app.menubar.change_menubar_item_state(is_runtime=True) - self.app.statusbar.progress_bar.start(5) self.app.canvas.mode = GraphMode.SELECT - self.time = time.perf_counter() - task = BackgroundTask(self, self.app.core.start_session, self.start_callback) - task.start() + task = ProgressTask(self.app.core.start_session, self.start_callback) + self.app.progress_task(task) def start_callback(self, response: core_pb2.StartSessionResponse): - self.app.statusbar.progress_bar.stop() - total = time.perf_counter() - self.time - message = f"Start ran for {total:.3f} seconds" - self.app.statusbar.set_status(message) - self.time = None if response.result: self.set_runtime() self.app.core.set_metadata() self.app.core.show_mobility_players() + else: + message = "\n".join(response.exceptions) + show_error(self.app, "Start Session Error", message) def set_runtime(self): self.runtime_frame.tkraise() @@ -450,19 +445,13 @@ class Toolbar(ttk.Frame): """ redraw buttons on the toolbar, send node and link messages to grpc server """ - logging.info("Click stop button") + logging.info("clicked stop button") self.app.menubar.change_menubar_item_state(is_runtime=False) - self.app.statusbar.progress_bar.start(5) - self.time = time.perf_counter() - task = BackgroundTask(self, self.app.core.stop_session, self.stop_callback) - task.start() + task = ProgressTask(self.app.core.stop_session, self.stop_callback) + self.app.progress_task(task) def stop_callback(self, response: core_pb2.StopSessionResponse): - self.app.statusbar.progress_bar.stop() self.set_design() - total = time.perf_counter() - self.time - message = f"Stopped in {total:.3f} seconds" - self.app.statusbar.set_status(message) self.app.canvas.stopped_session() def update_annotation( From 1dd45f442434b6ac769a6b0494d552c3bde623fa Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 3 May 2020 12:42:56 -0700 Subject: [PATCH 0229/1131] pygui cleaned up error display by creating top level app methods for displaying exceptions and errors, logging exceptions, and making sure they work for background tasks --- daemon/core/gui/app.py | 16 ++++++++++ daemon/core/gui/coreclient.py | 31 +++++++++---------- .../core/gui/dialogs/configserviceconfig.py | 30 +++++++----------- daemon/core/gui/dialogs/emaneconfig.py | 3 +- .../core/gui/{errors.py => dialogs/error.py} | 20 ------------ daemon/core/gui/dialogs/mobilityconfig.py | 3 +- daemon/core/gui/dialogs/mobilityplayer.py | 7 ++--- daemon/core/gui/dialogs/serviceconfig.py | 5 ++- daemon/core/gui/dialogs/sessionoptions.py | 5 ++- daemon/core/gui/dialogs/sessions.py | 3 +- daemon/core/gui/dialogs/wlanconfig.py | 5 ++- daemon/core/gui/graph/node.py | 3 +- daemon/core/gui/task.py | 5 +-- daemon/core/gui/toolbar.py | 3 +- 14 files changed, 57 insertions(+), 82 deletions(-) rename daemon/core/gui/{errors.py => dialogs/error.py} (70%) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index e797f1de..0f37b370 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -1,11 +1,15 @@ +import logging import math import time import tkinter as tk from tkinter import font, ttk from tkinter.ttk import Progressbar +import grpc + from core.gui import appconfig, themes from core.gui.coreclient import CoreClient +from core.gui.dialogs.error import ErrorDialog from core.gui.graph.graph import CanvasGraph from core.gui.images import ImageEnum, Images from core.gui.menubar import Menubar @@ -138,6 +142,18 @@ class Application(ttk.Frame): message = f"Task ran for {total:.3f} seconds" self.statusbar.set_status(message) + def show_grpc_exception(self, title: str, e: grpc.RpcError) -> None: + logging.exception("app grpc exception", exc_info=e) + message = e.details() + self.show_error(title, message) + + def show_exception(self, title: str, e: Exception) -> None: + logging.exception("app exception", exc_info=e) + self.show_error(title, str(e)) + + def show_error(self, title: str, message: str) -> None: + self.after(0, lambda: ErrorDialog(self, self, title, message).show()) + def on_closing(self): self.menubar.prompt_save_running_session(True) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index fc4fc64f..86329591 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -16,9 +16,9 @@ from core.api.grpc.mobility_pb2 import MobilityConfig from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig, ServiceFileConfig from core.api.grpc.wlan_pb2 import WlanConfig from core.gui import appconfig +from core.gui.dialogs.error import ErrorDialog from core.gui.dialogs.mobilityplayer import MobilityPlayer from core.gui.dialogs.sessions import SessionsDialog -from core.gui.errors import show_grpc_error from core.gui.graph import tags from core.gui.graph.edges import CanvasEdge from core.gui.graph.node import CanvasNode @@ -343,7 +343,7 @@ class CoreClient: response = self.client.get_session_metadata(self.session_id) self.parse_metadata(response.config) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e, self.app, self.app) + self.app.show_grpc_exception("Join Session Error", e) # update ui to represent current state self.app.after(0, self.app.joined_session_update) @@ -426,21 +426,16 @@ class CoreClient: ) self.join_session(response.session_id, query_location=False) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e, self.app, self.app) + self.app.show_grpc_exception("New Session Error", e) - def delete_session(self, session_id: int = None, parent_frame=None): + def delete_session(self, session_id: int = None): if session_id is None: session_id = self.session_id try: response = self.client.delete_session(session_id) logging.info("deleted session(%s), Result: %s", session_id, response) except grpc.RpcError as e: - # use the right master widget so the error dialog displays - # right on top of it - master = self.app - if parent_frame: - master = parent_frame - self.app.after(0, show_grpc_error, e, master, self.app) + self.app.show_grpc_exception("Delete Session Error", e) def setup(self): """ @@ -472,7 +467,9 @@ class CoreClient: dialog = SessionsDialog(self.app, self.app, True) dialog.show() except grpc.RpcError as e: - show_grpc_error(e, self.app, self.app) + logging.exception("core setup error") + dialog = ErrorDialog(self.app, self.app, "Setup Error", e.details()) + dialog.show() self.app.close() def edit_node(self, core_node: core_pb2.Node): @@ -481,7 +478,7 @@ class CoreClient: self.session_id, core_node.id, core_node.position, source=GUI_SOURCE ) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e, self.app, self.app) + self.app.show_grpc_exception("Edit Node Error", e) def start_session(self) -> core_pb2.StartSessionResponse: self.interfaces_manager.reset_mac() @@ -532,7 +529,7 @@ class CoreClient: if response.result: self.set_metadata() except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e, self.app, self.app) + self.app.show_grpc_exception("Start Session Error", e) return response def stop_session(self, session_id: int = None) -> core_pb2.StartSessionResponse: @@ -543,7 +540,7 @@ class CoreClient: response = self.client.stop_session(session_id) logging.info("stopped session(%s), result: %s", session_id, response) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e, self.app, self.app) + self.app.show_grpc_exception("Stop Session Error", e) return response def show_mobility_players(self): @@ -597,7 +594,7 @@ class CoreClient: logging.info("launching terminal %s", cmd) os.system(cmd) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e, self.app, self.app) + self.app.show_grpc_exception("Node Terminal Error", e) def save_xml(self, file_path: str): """ @@ -610,7 +607,7 @@ class CoreClient: response = self.client.save_xml(self.session_id, file_path) logging.info("saved xml file %s, result: %s", file_path, response) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e, self.app, self.app) + self.app.show_grpc_exception("Save XML Error", e) def open_xml(self, file_path: str): """ @@ -621,7 +618,7 @@ class CoreClient: logging.info("open xml file %s, response: %s", file_path, response) self.join_session(response.session_id) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e, self.app, self.app) + self.app.show_grpc_exception("Open XML Error", e) def get_node_service(self, node_id: int, service_name: str) -> NodeServiceData: response = self.client.get_node_service(self.session_id, node_id, service_name) diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py index 45ea3a76..aaf67869 100644 --- a/daemon/core/gui/dialogs/configserviceconfig.py +++ b/daemon/core/gui/dialogs/configserviceconfig.py @@ -10,7 +10,6 @@ import grpc from core.api.grpc.services_pb2 import ServiceValidationMode from core.gui.dialogs.dialog import Dialog -from core.gui.errors import show_grpc_error from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll @@ -116,8 +115,8 @@ class ConfigServiceConfigDialog(Dialog): self.modified_files.add(file) self.temp_service_files[file] = data except grpc.RpcError as e: + self.app.show_grpc_exception("Get Config Service Error", e) self.has_error = True - show_grpc_error(e, self.app, self.app) def draw(self): self.top.columnconfigure(0, weight=1) @@ -323,22 +322,17 @@ class ConfigServiceConfigDialog(Dialog): self.destroy() return - try: - service_config = self.canvas_node.config_service_configs.setdefault( - self.service_name, {} - ) - if self.config_frame: - self.config_frame.parse_config() - service_config["config"] = { - x.name: x.value for x in self.config.values() - } - templates_config = service_config.setdefault("templates", {}) - for file in self.modified_files: - templates_config[file] = self.temp_service_files[file] - all_current = current_listbox.get(0, tk.END) - current_listbox.itemconfig(all_current.index(self.service_name), bg="green") - except grpc.RpcError as e: - show_grpc_error(e, self.top, self.app) + service_config = self.canvas_node.config_service_configs.setdefault( + self.service_name, {} + ) + if self.config_frame: + self.config_frame.parse_config() + service_config["config"] = {x.name: x.value for x in self.config.values()} + templates_config = service_config.setdefault("templates", {}) + for file in self.modified_files: + templates_config[file] = self.temp_service_files[file] + all_current = current_listbox.get(0, tk.END) + current_listbox.itemconfig(all_current.index(self.service_name), bg="green") self.destroy() def handle_template_changed(self, event: tk.Event): diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index a7835751..8914dc2c 100644 --- a/daemon/core/gui/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -9,7 +9,6 @@ from typing import TYPE_CHECKING, Any import grpc from core.gui.dialogs.dialog import Dialog -from core.gui.errors import show_grpc_error from core.gui.images import ImageEnum, Images from core.gui.themes import PADX, PADY from core.gui.widgets import ConfigFrame @@ -78,7 +77,7 @@ class EmaneModelDialog(Dialog): ) self.draw() except grpc.RpcError as e: - show_grpc_error(e, self.app, self.app) + self.app.show_grpc_exception("Get EMANE Config Error", e) self.has_error = True self.destroy() diff --git a/daemon/core/gui/errors.py b/daemon/core/gui/dialogs/error.py similarity index 70% rename from daemon/core/gui/errors.py rename to daemon/core/gui/dialogs/error.py index b11e684b..3703b533 100644 --- a/daemon/core/gui/errors.py +++ b/daemon/core/gui/dialogs/error.py @@ -1,8 +1,6 @@ from tkinter import ttk from typing import TYPE_CHECKING -import grpc - from core.gui.dialogs.dialog import Dialog from core.gui.images import ImageEnum, Images from core.gui.themes import FRAME_PAD, PADX, PADY @@ -41,21 +39,3 @@ class ErrorDialog(Dialog): button = ttk.Button(self.top, text="Close", command=lambda: self.destroy()) button.grid(sticky="ew") - - -def show_exception(app: "Application", title: str, exception: Exception) -> None: - dialog = ErrorDialog(app, app, title, str(exception)) - dialog.show() - - -def show_grpc_error(e: grpc.RpcError, master, app: "Application") -> None: - title = [x.capitalize() for x in e.code().name.lower().split("_")] - title = " ".join(title) - title = f"GRPC {title}" - dialog = ErrorDialog(master, app, title, e.details()) - dialog.show() - - -def show_error(app: "Application", title: str, message: str) -> None: - dialog = ErrorDialog(app, app, title, message) - dialog.show() diff --git a/daemon/core/gui/dialogs/mobilityconfig.py b/daemon/core/gui/dialogs/mobilityconfig.py index 2222e06f..b4a6a163 100644 --- a/daemon/core/gui/dialogs/mobilityconfig.py +++ b/daemon/core/gui/dialogs/mobilityconfig.py @@ -7,7 +7,6 @@ from typing import TYPE_CHECKING import grpc from core.gui.dialogs.dialog import Dialog -from core.gui.errors import show_grpc_error from core.gui.themes import PADX, PADY from core.gui.widgets import ConfigFrame @@ -33,8 +32,8 @@ class MobilityConfigDialog(Dialog): self.config = self.app.core.get_mobility_config(self.node.id) self.draw() except grpc.RpcError as e: + self.app.show_grpc_exception("Get Mobility Config Error", e) self.has_error = True - show_grpc_error(e, self.app, self.app) self.destroy() def draw(self): diff --git a/daemon/core/gui/dialogs/mobilityplayer.py b/daemon/core/gui/dialogs/mobilityplayer.py index 6b7b7869..e822aa4b 100644 --- a/daemon/core/gui/dialogs/mobilityplayer.py +++ b/daemon/core/gui/dialogs/mobilityplayer.py @@ -6,7 +6,6 @@ import grpc from core.api.grpc.mobility_pb2 import MobilityAction from core.gui.dialogs.dialog import Dialog -from core.gui.errors import show_grpc_error from core.gui.images import ImageEnum, Images from core.gui.themes import PADX, PADY @@ -154,7 +153,7 @@ class MobilityPlayerDialog(Dialog): session_id, self.node.id, MobilityAction.START ) except grpc.RpcError as e: - show_grpc_error(e, self.top, self.app) + self.app.show_grpc_exception("Mobility Error", e) def click_pause(self): self.set_pause() @@ -164,7 +163,7 @@ class MobilityPlayerDialog(Dialog): session_id, self.node.id, MobilityAction.PAUSE ) except grpc.RpcError as e: - show_grpc_error(e, self.top, self.app) + self.app.show_grpc_exception("Mobility Error", e) def click_stop(self): self.set_stop() @@ -174,4 +173,4 @@ class MobilityPlayerDialog(Dialog): session_id, self.node.id, MobilityAction.STOP ) except grpc.RpcError as e: - show_grpc_error(e, self.top, self.app) + self.app.show_grpc_exception("Mobility Error", e) diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index 8fc85394..79e8871f 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -9,7 +9,6 @@ import grpc from core.api.grpc.services_pb2 import ServiceValidationMode from core.gui.dialogs.copyserviceconfig import CopyServiceConfigDialog from core.gui.dialogs.dialog import Dialog -from core.gui.errors import show_grpc_error from core.gui.images import ImageEnum, Images from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CodeText, ListboxScroll @@ -119,8 +118,8 @@ class ServiceConfigDialog(Dialog): for file, data in file_configs.items(): self.temp_service_files[file] = data except grpc.RpcError as e: + self.app.show_grpc_exception("Get Node Service Error", e) self.has_error = True - show_grpc_error(e, self.master, self.app) def draw(self): self.top.columnconfigure(0, weight=1) @@ -484,7 +483,7 @@ class ServiceConfigDialog(Dialog): ) self.current_service_color("green") except grpc.RpcError as e: - show_grpc_error(e, self.top, self.app) + self.app.show_grpc_exception("Save Service Config Error", e) self.destroy() def display_service_file_data(self, event: tk.Event): diff --git a/daemon/core/gui/dialogs/sessionoptions.py b/daemon/core/gui/dialogs/sessionoptions.py index c042eef4..b2c5ede5 100644 --- a/daemon/core/gui/dialogs/sessionoptions.py +++ b/daemon/core/gui/dialogs/sessionoptions.py @@ -5,7 +5,6 @@ from typing import TYPE_CHECKING import grpc from core.gui.dialogs.dialog import Dialog -from core.gui.errors import show_grpc_error from core.gui.themes import PADX, PADY from core.gui.widgets import ConfigFrame @@ -28,8 +27,8 @@ class SessionOptionsDialog(Dialog): response = self.app.core.client.get_session_options(session_id) return response.config except grpc.RpcError as e: + self.app.show_grpc_exception("Get Session Options Error", e) self.has_error = True - show_grpc_error(e, self.app, self.app) self.destroy() def draw(self): @@ -56,5 +55,5 @@ class SessionOptionsDialog(Dialog): response = self.app.core.client.set_session_options(session_id, config) logging.info("saved session config: %s", response) except grpc.RpcError as e: - show_grpc_error(e, self.top, self.app) + self.app.show_grpc_exception("Set Session Options Error", e) self.destroy() diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index 79a153f7..251f74b0 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -7,7 +7,6 @@ import grpc from core.api.grpc import core_pb2 from core.gui.dialogs.dialog import Dialog -from core.gui.errors import show_grpc_error from core.gui.images import ImageEnum, Images from core.gui.task import ProgressTask from core.gui.themes import PADX, PADY @@ -37,7 +36,7 @@ class SessionsDialog(Dialog): logging.info("sessions: %s", response) return response.sessions except grpc.RpcError as e: - show_grpc_error(e, self.app, self.app) + self.app.show_grpc_exception("Get Sessions Error", e) self.destroy() def draw(self) -> None: diff --git a/daemon/core/gui/dialogs/wlanconfig.py b/daemon/core/gui/dialogs/wlanconfig.py index d5d0c673..5096cd0f 100644 --- a/daemon/core/gui/dialogs/wlanconfig.py +++ b/daemon/core/gui/dialogs/wlanconfig.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING import grpc from core.gui.dialogs.dialog import Dialog -from core.gui.errors import show_grpc_error from core.gui.themes import PADX, PADY from core.gui.widgets import ConfigFrame @@ -21,7 +20,7 @@ class WlanConfigDialog(Dialog): self, master: "Application", app: "Application", canvas_node: "CanvasNode" ): super().__init__( - master, app, f"{canvas_node.core_node.name} Wlan Configuration" + master, app, f"{canvas_node.core_node.name} WLAN Configuration" ) self.canvas_node = canvas_node self.node = canvas_node.core_node @@ -38,7 +37,7 @@ class WlanConfigDialog(Dialog): self.init_draw_range() self.draw() except grpc.RpcError as e: - show_grpc_error(e, self.app, self.app) + self.app.show_grpc_exception("WLAN Config Error", e) self.has_error = True self.destroy() diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 758dbd26..c90be311 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -14,7 +14,6 @@ from core.gui.dialogs.nodeconfig import NodeConfigDialog from core.gui.dialogs.nodeconfigservice import NodeConfigServiceDialog from core.gui.dialogs.nodeservice import NodeServiceDialog from core.gui.dialogs.wlanconfig import WlanConfigDialog -from core.gui.errors import show_grpc_error from core.gui.graph import tags from core.gui.graph.edges import CanvasEdge from core.gui.graph.tooltip import CanvasTooltip @@ -180,7 +179,7 @@ class CanvasNode: output = self.app.core.run(self.core_node.id) self.tooltip.text.set(output) except grpc.RpcError as e: - show_grpc_error(e, self.app, self.app) + self.app.show_grpc_exception("Observer Error", e) def on_leave(self, event: tk.Event): self.tooltip.on_leave(event) diff --git a/daemon/core/gui/task.py b/daemon/core/gui/task.py index c88a0151..05855945 100644 --- a/daemon/core/gui/task.py +++ b/daemon/core/gui/task.py @@ -2,8 +2,6 @@ import logging import threading from typing import Any, Callable, Tuple -from core.gui.errors import show_exception - class ProgressTask: def __init__( @@ -33,7 +31,6 @@ class ProgressTask: self.app.after(0, self.callback, *values) except Exception as e: logging.exception("progress task exception") - args = (self.app, "Task Error", e) - self.app.after(0, show_exception, *args) + self.app.show_exception("Task Error", e) finally: self.app.after(0, self.app.progress_task_complete) diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 374a4eaf..c735707a 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -9,7 +9,6 @@ from core.api.grpc import core_pb2 from core.gui.dialogs.customnodes import CustomNodesDialog from core.gui.dialogs.marker import MarkerDialog from core.gui.dialogs.runtool import RunToolDialog -from core.gui.errors import show_error from core.gui.graph.enums import GraphMode from core.gui.graph.shapeutils import ShapeType, is_marker from core.gui.images import ImageEnum, Images @@ -273,7 +272,7 @@ class Toolbar(ttk.Frame): self.app.core.show_mobility_players() else: message = "\n".join(response.exceptions) - show_error(self.app, "Start Session Error", message) + self.app.show_error("Start Session Error", message) def set_runtime(self): self.runtime_frame.tkraise() From 4ec6ef25fe6859ca9a3aa476b0c3ea23cfe6673b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 3 May 2020 21:47:58 -0700 Subject: [PATCH 0230/1131] pygui updated progress tasks to be self contained and leverage a title value to display runtime with more context to user --- daemon/core/gui/app.py | 18 ----------------- daemon/core/gui/dialogs/sessions.py | 6 ++++-- daemon/core/gui/menubar.py | 4 ++-- daemon/core/gui/task.py | 30 +++++++++++++++++++++++++---- daemon/core/gui/toolbar.py | 12 ++++++++---- 5 files changed, 40 insertions(+), 30 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 0f37b370..0d54e627 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -1,6 +1,5 @@ import logging import math -import time import tkinter as tk from tkinter import font, ttk from tkinter.ttk import Progressbar @@ -15,7 +14,6 @@ from core.gui.images import ImageEnum, Images from core.gui.menubar import Menubar from core.gui.nodeutils import NodeUtils from core.gui.statusbar import StatusBar -from core.gui.task import ProgressTask from core.gui.toolbar import Toolbar from core.gui.validation import InputValidation @@ -37,7 +35,6 @@ class Application(ttk.Frame): self.statusbar = None self.validation = None self.progress = None - self.time = None # fonts self.fonts_size = None @@ -127,21 +124,6 @@ class Application(ttk.Frame): self.statusbar = StatusBar(self.right_frame, self) self.statusbar.grid(sticky="ew") - def progress_task(self, task: ProgressTask) -> None: - self.progress.grid(sticky="ew") - self.progress.start() - self.time = time.perf_counter() - task.app = self - task.start() - - def progress_task_complete(self) -> None: - self.progress.stop() - self.progress.grid_forget() - total = time.perf_counter() - self.time - self.time = None - message = f"Task ran for {total:.3f} seconds" - self.statusbar.set_status(message) - def show_grpc_exception(self, title: str, e: grpc.RpcError) -> None: logging.exception("app grpc exception", exc_info=e) message = e.details() diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index 251f74b0..f7c4722a 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -185,8 +185,10 @@ class SessionsDialog(Dialog): self.destroy() if self.app.core.xml_file: self.app.core.xml_file = None - task = ProgressTask(self.app.core.join_session, args=(session_id,)) - self.app.progress_task(task) + task = ProgressTask( + self.app, "Join", self.app.core.join_session, args=(session_id,) + ) + task.start() def double_click_join(self, _event: tk.Event) -> None: item = self.tree.selection() diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index a77913c3..9c6dd7a9 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -340,8 +340,8 @@ class Menubar(tk.Menu): self.core.xml_file = filename self.core.xml_dir = str(os.path.dirname(filename)) self.prompt_save_running_session() - task = ProgressTask(self.core.open_xml, args=(filename,)) - self.app.progress_task(task) + task = ProgressTask(self.app, "Open XML", self.core.open_xml, args=(filename,)) + task.start() def execute_python(self): dialog = ExecutePythonDialog(self.app, self.app) diff --git a/daemon/core/gui/task.py b/daemon/core/gui/task.py index 05855945..2f055a90 100644 --- a/daemon/core/gui/task.py +++ b/daemon/core/gui/task.py @@ -1,20 +1,34 @@ import logging import threading -from typing import Any, Callable, Tuple +import time +from typing import TYPE_CHECKING, Any, Callable, Tuple + +if TYPE_CHECKING: + from core.gui.app import Application class ProgressTask: def __init__( - self, task: Callable, callback: Callable = None, args: Tuple[Any] = None + self, + app: "Application", + title: str, + task: Callable, + callback: Callable = None, + args: Tuple[Any] = None, ): - self.app = None + self.app = app + self.title = title self.task = task self.callback = callback self.args = args if self.args is None: self.args = () + self.time = None def start(self) -> None: + self.app.progress.grid(sticky="ew") + self.app.progress.start() + self.time = time.perf_counter() thread = threading.Thread(target=self.run, daemon=True) thread.start() @@ -33,4 +47,12 @@ class ProgressTask: logging.exception("progress task exception") self.app.show_exception("Task Error", e) finally: - self.app.after(0, self.app.progress_task_complete) + self.app.after(0, self.complete) + + def complete(self): + self.app.progress.stop() + self.app.progress.grid_forget() + total = time.perf_counter() - self.time + self.time = None + message = f"{self.title} ran for {total:.3f} seconds" + self.app.statusbar.set_status(message) diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index c735707a..ee37df32 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -262,8 +262,10 @@ class Toolbar(ttk.Frame): """ self.app.menubar.change_menubar_item_state(is_runtime=True) self.app.canvas.mode = GraphMode.SELECT - task = ProgressTask(self.app.core.start_session, self.start_callback) - self.app.progress_task(task) + task = ProgressTask( + self.app, "Start", self.app.core.start_session, self.start_callback + ) + task.start() def start_callback(self, response: core_pb2.StartSessionResponse): if response.result: @@ -446,8 +448,10 @@ class Toolbar(ttk.Frame): """ logging.info("clicked stop button") self.app.menubar.change_menubar_item_state(is_runtime=False) - task = ProgressTask(self.app.core.stop_session, self.stop_callback) - self.app.progress_task(task) + task = ProgressTask( + self.app, "Stop", self.app.core.stop_session, self.stop_callback + ) + task.start() def stop_callback(self, response: core_pb2.StopSessionResponse): self.set_design() From 0e082421285cb7425d0f0baa09d4d6b4adff5908 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 3 May 2020 21:55:34 -0700 Subject: [PATCH 0231/1131] pygui close mobility players when stopping session --- daemon/core/gui/coreclient.py | 7 +++++-- daemon/core/gui/dialogs/mobilityplayer.py | 4 ++-- daemon/core/gui/toolbar.py | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 86329591..64255903 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -111,13 +111,16 @@ class CoreClient: self.links.clear() self.hooks.clear() self.emane_config = None - for mobility_player in self.mobility_players.values(): - mobility_player.handle_close() + self.close_mobility_players() self.mobility_players.clear() # clear streams self.cancel_throughputs() self.cancel_events() + def close_mobility_players(self): + for mobility_player in self.mobility_players.values(): + mobility_player.close() + def set_observer(self, value: str): self.observer = value diff --git a/daemon/core/gui/dialogs/mobilityplayer.py b/daemon/core/gui/dialogs/mobilityplayer.py index e822aa4b..1cd62684 100644 --- a/daemon/core/gui/dialogs/mobilityplayer.py +++ b/daemon/core/gui/dialogs/mobilityplayer.py @@ -37,7 +37,7 @@ class MobilityPlayer: self.dialog = MobilityPlayerDialog( self.master, self.app, self.canvas_node, self.config ) - self.dialog.protocol("WM_DELETE_WINDOW", self.handle_close) + self.dialog.protocol("WM_DELETE_WINDOW", self.close) if self.state == MobilityAction.START: self.set_play() elif self.state == MobilityAction.PAUSE: @@ -46,7 +46,7 @@ class MobilityPlayer: self.set_stop() self.dialog.show() - def handle_close(self): + def close(self): if self.dialog: self.dialog.destroy() self.dialog = None diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index ee37df32..49a4ab0c 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -448,6 +448,7 @@ class Toolbar(ttk.Frame): """ logging.info("clicked stop button") self.app.menubar.change_menubar_item_state(is_runtime=False) + self.app.core.close_mobility_players() task = ProgressTask( self.app, "Stop", self.app.core.stop_session, self.stop_callback ) From 828254dccddedc73e7131f0891cedabc5645b017 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 3 May 2020 22:01:21 -0700 Subject: [PATCH 0232/1131] pygui switched netstat socket observer to use ss instead --- daemon/core/gui/menubar.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 9c6dd7a9..456ad7e8 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -31,9 +31,9 @@ MAX_FILES = 3 OBSERVERS = { "List Processes": "ps", "Show Interfaces": "ip address", - "IPV4 Routes": "ip -4 ro", - "IPV6 Routes": "ip -6 ro", - "Listening Sockets": "netstat -tuwnl", + "IPV4 Routes": "ip -4 route", + "IPV6 Routes": "ip -6 route", + "Listening Sockets": "ss -tuwnl", "IPv4 MFC Entries": "ip -4 mroute show", "IPv6 MFC Entries": "ip -6 mroute show", "Firewall Rules": "iptables -L", From 185c6736b3ba14e39db04c9088f41244e9640563 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 3 May 2020 22:47:46 -0700 Subject: [PATCH 0233/1131] pygui moved custom nodes dialog to menubar and small layout cleanup --- daemon/core/gui/dialogs/customnodes.py | 4 ++-- daemon/core/gui/menubar.py | 6 ++++++ daemon/core/gui/toolbar.py | 15 --------------- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/daemon/core/gui/dialogs/customnodes.py b/daemon/core/gui/dialogs/customnodes.py index e154483a..947e6312 100644 --- a/daemon/core/gui/dialogs/customnodes.py +++ b/daemon/core/gui/dialogs/customnodes.py @@ -137,11 +137,11 @@ class CustomNodesDialog(Dialog): frame.grid(row=0, column=2, sticky="nsew") frame.columnconfigure(0, weight=1) entry = ttk.Entry(frame, textvariable=self.name) - entry.grid(sticky="ew") + entry.grid(sticky="ew", pady=PADY) self.image_button = ttk.Button( frame, text="Icon", compound=tk.LEFT, command=self.click_icon ) - self.image_button.grid(sticky="ew") + self.image_button.grid(sticky="ew", pady=PADY) button = ttk.Button(frame, text="Services", command=self.click_services) button.grid(sticky="ew") diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 456ad7e8..0d2f8e4e 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -10,6 +10,7 @@ from core.gui.appconfig import XMLS_PATH from core.gui.dialogs.about import AboutDialog from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog from core.gui.dialogs.canvaswallpaper import CanvasWallpaperDialog +from core.gui.dialogs.customnodes import CustomNodesDialog from core.gui.dialogs.executepython import ExecutePythonDialog from core.gui.dialogs.find import FindDialog from core.gui.dialogs.hooks import HooksDialog @@ -116,6 +117,7 @@ class Menubar(tk.Menu): """ menu = tk.Menu(self) menu.add_command(label="Preferences", command=self.click_preferences) + menu.add_command(label="Custom Nodes", command=self.click_custom_nodes) menu.add_separator() menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED) menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED) @@ -496,3 +498,7 @@ class Menubar(tk.Menu): def click_ip_config(self) -> None: dialog = IpConfigDialog(self.app, self.app) dialog.show() + + def click_custom_nodes(self) -> None: + dialog = CustomNodesDialog(self.app, self.app) + dialog.show() diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 49a4ab0c..0989affd 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -6,7 +6,6 @@ from tkinter import ttk from typing import TYPE_CHECKING, Callable from core.api.grpc import core_pb2 -from core.gui.dialogs.customnodes import CustomNodesDialog from core.gui.dialogs.marker import MarkerDialog from core.gui.dialogs.runtool import RunToolDialog from core.gui.graph.enums import GraphMode @@ -193,12 +192,6 @@ class Toolbar(ttk.Frame): node_draw.image_file, ) self.create_picker_button(image, func, self.node_picker, name) - # draw edit node - # image = icon(ImageEnum.EDITNODE, PICKER_SIZE) - image = self.get_icon(ImageEnum.EDITNODE, PICKER_SIZE) - self.create_picker_button( - image, self.click_edit_node, self.node_picker, "Custom" - ) self.design_select(self.node_button) self.node_button.after( 0, lambda: self.show_picker(self.node_button, self.node_picker) @@ -289,11 +282,6 @@ class Toolbar(ttk.Frame): self.design_select(self.link_button) self.app.canvas.mode = GraphMode.EDGE - def click_edit_node(self): - self.hide_pickers() - dialog = CustomNodesDialog(self.app, self.app) - dialog.show() - def update_button( self, button: ttk.Button, @@ -489,9 +477,6 @@ class Toolbar(ttk.Frame): self.marker_tool = MarkerDialog(self.app, self.app) self.marker_tool.show() - def click_two_node_button(self): - logging.debug("Click TWONODE button") - def scale_button(self, button, image_enum): image = icon(image_enum, int(TOOLBAR_SIZE * self.app.app_scale)) button.config(image=image) From 1d620a0b17e9b8bf3750c7e76a490444cb039d7c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 4 May 2020 22:50:59 -0700 Subject: [PATCH 0234/1131] pygui some cleanup for dialog constructors to avoid passing duplicate parameters in most cases --- daemon/core/gui/app.py | 2 +- daemon/core/gui/coreclient.py | 4 +- daemon/core/gui/dialogs/about.py | 4 +- daemon/core/gui/dialogs/alerts.py | 47 ++----------------- daemon/core/gui/dialogs/canvassizeandscale.py | 4 +- daemon/core/gui/dialogs/canvaswallpaper.py | 4 +- daemon/core/gui/dialogs/colorpicker.py | 8 ++-- .../core/gui/dialogs/configserviceconfig.py | 8 ++-- daemon/core/gui/dialogs/copyserviceconfig.py | 11 ++--- daemon/core/gui/dialogs/customnodes.py | 12 +++-- daemon/core/gui/dialogs/dialog.py | 8 +++- daemon/core/gui/dialogs/emaneconfig.py | 19 +++----- daemon/core/gui/dialogs/error.py | 4 +- daemon/core/gui/dialogs/executepython.py | 9 ++-- daemon/core/gui/dialogs/find.py | 11 +++-- daemon/core/gui/dialogs/hooks.py | 10 ++-- daemon/core/gui/dialogs/ipdialog.py | 4 +- daemon/core/gui/dialogs/linkconfig.py | 7 ++- daemon/core/gui/dialogs/macdialog.py | 4 +- daemon/core/gui/dialogs/marker.py | 7 +-- daemon/core/gui/dialogs/mobilityconfig.py | 8 +--- daemon/core/gui/dialogs/mobilityplayer.py | 21 ++------- daemon/core/gui/dialogs/nodeconfig.py | 6 +-- daemon/core/gui/dialogs/nodeconfigservice.py | 9 +--- daemon/core/gui/dialogs/nodeservice.py | 7 +-- daemon/core/gui/dialogs/observers.py | 4 +- daemon/core/gui/dialogs/preferences.py | 4 +- daemon/core/gui/dialogs/runtool.py | 9 ++-- daemon/core/gui/dialogs/servers.py | 4 +- daemon/core/gui/dialogs/serviceconfig.py | 8 ++-- daemon/core/gui/dialogs/sessionoptions.py | 4 +- daemon/core/gui/dialogs/sessions.py | 6 +-- daemon/core/gui/dialogs/shapemod.py | 4 +- daemon/core/gui/dialogs/throughput.py | 5 +- daemon/core/gui/dialogs/wlanconfig.py | 8 +--- daemon/core/gui/graph/edges.py | 2 +- daemon/core/gui/graph/graph.py | 2 +- daemon/core/gui/graph/node.py | 12 ++--- daemon/core/gui/graph/shape.py | 2 +- daemon/core/gui/menubar.py | 32 ++++++------- daemon/core/gui/statusbar.py | 2 +- daemon/core/gui/toolbar.py | 6 +-- 42 files changed, 143 insertions(+), 209 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 0d54e627..de0a5260 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -134,7 +134,7 @@ class Application(ttk.Frame): self.show_error(title, str(e)) def show_error(self, title: str, message: str) -> None: - self.after(0, lambda: ErrorDialog(self, self, title, message).show()) + self.after(0, lambda: ErrorDialog(self, title, message).show()) def on_closing(self): self.menubar.prompt_save_running_session(True) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 64255903..118138c6 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -467,11 +467,11 @@ class CoreClient: if len(sessions) == 0: self.create_new_session() else: - dialog = SessionsDialog(self.app, self.app, True) + dialog = SessionsDialog(self.app, True) dialog.show() except grpc.RpcError as e: logging.exception("core setup error") - dialog = ErrorDialog(self.app, self.app, "Setup Error", e.details()) + dialog = ErrorDialog(self.app, "Setup Error", e.details()) dialog.show() self.app.close() diff --git a/daemon/core/gui/dialogs/about.py b/daemon/core/gui/dialogs/about.py index 5402b1ab..2e649169 100644 --- a/daemon/core/gui/dialogs/about.py +++ b/daemon/core/gui/dialogs/about.py @@ -35,8 +35,8 @@ THE POSSIBILITY OF SUCH DAMAGE.\ class AboutDialog(Dialog): - def __init__(self, master: "Application", app: "Application"): - super().__init__(master, app, "About CORE") + def __init__(self, app: "Application"): + super().__init__(app, "About CORE") self.draw() def draw(self): diff --git a/daemon/core/gui/dialogs/alerts.py b/daemon/core/gui/dialogs/alerts.py index b425a30c..a0c3e68b 100644 --- a/daemon/core/gui/dialogs/alerts.py +++ b/daemon/core/gui/dialogs/alerts.py @@ -15,9 +15,8 @@ if TYPE_CHECKING: class AlertsDialog(Dialog): - def __init__(self, master: "Application", app: "Application"): - super().__init__(master, app, "Alerts") - self.app = app + def __init__(self, app: "Application"): + super().__init__(app, "Alerts") self.tree = None self.codetext = None self.alarm_map = {} @@ -93,16 +92,10 @@ class AlertsDialog(Dialog): frame.grid(sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=1) - frame.columnconfigure(3, weight=1) button = ttk.Button(frame, text="Reset", command=self.reset_alerts) button.grid(row=0, column=0, sticky="ew", padx=PADX) - button = ttk.Button(frame, text="Daemon Log", command=self.daemon_log) - button.grid(row=0, column=1, sticky="ew", padx=PADX) - button = ttk.Button(frame, text="Node Log") - button.grid(row=0, column=2, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Close", command=self.destroy) - button.grid(row=0, column=3, sticky="ew") + button.grid(row=0, column=1, sticky="ew") def reset_alerts(self): self.codetext.text.delete("1.0", tk.END) @@ -110,10 +103,6 @@ class AlertsDialog(Dialog): self.tree.delete(item) self.app.statusbar.core_alarms.clear() - def daemon_log(self): - dialog = DaemonLog(self, self.app) - dialog.show() - def click_select(self, event: tk.Event): current = self.tree.selection()[0] alarm = self.alarm_map[current] @@ -121,33 +110,3 @@ class AlertsDialog(Dialog): self.codetext.text.delete("1.0", "end") self.codetext.text.insert("1.0", alarm.exception_event.text) self.codetext.text.config(state=tk.DISABLED) - - -class DaemonLog(Dialog): - def __init__(self, master: tk.Widget, app: "Application"): - super().__init__(master, app, "core-daemon log") - self.columnconfigure(0, weight=1) - self.path = tk.StringVar(value="/var/log/core-daemon.log") - self.draw() - - def draw(self): - self.top.columnconfigure(0, weight=1) - self.top.rowconfigure(1, weight=1) - frame = ttk.Frame(self.top) - frame.grid(row=0, column=0, sticky="ew", pady=PADY) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=9) - label = ttk.Label(frame, text="File", anchor="w") - label.grid(row=0, column=0, sticky="ew") - entry = ttk.Entry(frame, textvariable=self.path, state="disabled") - entry.grid(row=0, column=1, sticky="ew") - try: - file = open("/var/log/core-daemon.log", "r") - log = file.readlines() - except FileNotFoundError: - log = "Log file not found" - codetext = CodeText(self.top) - codetext.text.insert("1.0", log) - codetext.text.see("end") - codetext.text.config(state=tk.DISABLED) - codetext.grid(row=1, column=0, sticky="nsew") diff --git a/daemon/core/gui/dialogs/canvassizeandscale.py b/daemon/core/gui/dialogs/canvassizeandscale.py index 9543d8c6..3c3b8540 100644 --- a/daemon/core/gui/dialogs/canvassizeandscale.py +++ b/daemon/core/gui/dialogs/canvassizeandscale.py @@ -15,11 +15,11 @@ PIXEL_SCALE = 100 class SizeAndScaleDialog(Dialog): - def __init__(self, master: "Application", app: "Application"): + def __init__(self, app: "Application"): """ create an instance for size and scale object """ - super().__init__(master, app, "Canvas Size and Scale") + super().__init__(app, "Canvas Size and Scale") self.canvas = self.app.canvas self.validation = app.validation self.section_font = font.Font(weight="bold") diff --git a/daemon/core/gui/dialogs/canvaswallpaper.py b/daemon/core/gui/dialogs/canvaswallpaper.py index 3b32572e..5e8460be 100644 --- a/daemon/core/gui/dialogs/canvaswallpaper.py +++ b/daemon/core/gui/dialogs/canvaswallpaper.py @@ -17,11 +17,11 @@ if TYPE_CHECKING: class CanvasWallpaperDialog(Dialog): - def __init__(self, master: "Application", app: "Application"): + def __init__(self, app: "Application"): """ create an instance of CanvasWallpaper object """ - super().__init__(master, app, "Canvas Background") + super().__init__(app, "Canvas Background") self.canvas = self.app.canvas self.scale_option = tk.IntVar(value=self.canvas.scale_option.get()) self.adjust_to_dim = tk.BooleanVar(value=self.canvas.adjust_to_dim.get()) diff --git a/daemon/core/gui/dialogs/colorpicker.py b/daemon/core/gui/dialogs/colorpicker.py index 742e64f2..c4268788 100644 --- a/daemon/core/gui/dialogs/colorpicker.py +++ b/daemon/core/gui/dialogs/colorpicker.py @@ -3,7 +3,7 @@ custom color picker """ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from core.gui.dialogs.dialog import Dialog @@ -12,8 +12,10 @@ if TYPE_CHECKING: class ColorPickerDialog(Dialog): - def __init__(self, master: Any, app: "Application", initcolor: str = "#000000"): - super().__init__(master, app, "color picker") + def __init__( + self, master: tk.BaseWidget, app: "Application", initcolor: str = "#000000" + ): + super().__init__(app, "color picker", master=master) self.red_entry = None self.blue_entry = None self.green_entry = None diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py index aaf67869..42041a8e 100644 --- a/daemon/core/gui/dialogs/configserviceconfig.py +++ b/daemon/core/gui/dialogs/configserviceconfig.py @@ -4,7 +4,7 @@ Service configuration dialog import logging import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING, Any, List +from typing import TYPE_CHECKING, List import grpc @@ -21,16 +21,14 @@ if TYPE_CHECKING: class ConfigServiceConfigDialog(Dialog): def __init__( self, - master: Any, + master: tk.BaseWidget, app: "Application", service_name: str, canvas_node: "CanvasNode", node_id: int, ): title = f"{service_name} Config Service" - super().__init__(master, app, title) - self.master = master - self.app = app + super().__init__(app, title, master=master) self.core = app.core self.canvas_node = canvas_node self.node_id = node_id diff --git a/daemon/core/gui/dialogs/copyserviceconfig.py b/daemon/core/gui/dialogs/copyserviceconfig.py index 87c86fd3..ff75a59a 100644 --- a/daemon/core/gui/dialogs/copyserviceconfig.py +++ b/daemon/core/gui/dialogs/copyserviceconfig.py @@ -4,7 +4,7 @@ copy service config dialog import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING, Any, Tuple +from typing import TYPE_CHECKING, Tuple from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX @@ -15,10 +15,9 @@ if TYPE_CHECKING: class CopyServiceConfigDialog(Dialog): - def __init__(self, master: Any, app: "Application", node_id: int): - super().__init__(master, app, f"Copy services to node {node_id}") + def __init__(self, master: tk.BaseWidget, app: "Application", node_id: int): + super().__init__(app, f"Copy services to node {node_id}", master=master) self.parent = master - self.app = app self.node_id = node_id self.service_configs = app.core.service_configs self.file_configs = app.core.file_configs @@ -171,13 +170,13 @@ class CopyServiceConfigDialog(Dialog): class ViewConfigDialog(Dialog): def __init__( self, - master: Any, + master: tk.BaseWidget, app: "Application", node_id: int, data: str, filename: str = None, ): - super().__init__(master, app, f"n{node_id} config data") + super().__init__(app, f"n{node_id} config data", master=master) self.data = data self.service_data = None self.filepath = tk.StringVar(value=f"/tmp/services.tmp-n{node_id}-{filename}") diff --git a/daemon/core/gui/dialogs/customnodes.py b/daemon/core/gui/dialogs/customnodes.py index 947e6312..28f33ffe 100644 --- a/daemon/core/gui/dialogs/customnodes.py +++ b/daemon/core/gui/dialogs/customnodes.py @@ -2,7 +2,7 @@ import logging import tkinter as tk from pathlib import Path from tkinter import ttk -from typing import TYPE_CHECKING, Any, Set +from typing import TYPE_CHECKING, Set from core.gui import nodeutils from core.gui.appconfig import ICONS_PATH @@ -17,8 +17,10 @@ if TYPE_CHECKING: class ServicesSelectDialog(Dialog): - def __init__(self, master: Any, app: "Application", current_services: Set[str]): - super().__init__(master, app, "Node Services") + def __init__( + self, master: tk.BaseWidget, app: "Application", current_services: Set[str] + ): + super().__init__(app, "Node Services", master=master) self.groups = None self.services = None self.current = None @@ -100,8 +102,8 @@ class ServicesSelectDialog(Dialog): class CustomNodesDialog(Dialog): - def __init__(self, master: "Application", app: "Application"): - super().__init__(master, app, "Custom Nodes") + def __init__(self, app: "Application"): + super().__init__(app, "Custom Nodes") self.edit_button = None self.delete_button = None self.nodes_list = None diff --git a/daemon/core/gui/dialogs/dialog.py b/daemon/core/gui/dialogs/dialog.py index 32708450..f3742c50 100644 --- a/daemon/core/gui/dialogs/dialog.py +++ b/daemon/core/gui/dialogs/dialog.py @@ -11,8 +11,14 @@ if TYPE_CHECKING: class Dialog(tk.Toplevel): def __init__( - self, master: tk.Widget, app: "Application", title: str, modal: bool = True + self, + app: "Application", + title: str, + modal: bool = True, + master: tk.BaseWidget = None, ): + if master is None: + master = app super().__init__(master) self.withdraw() self.app = app diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index 8914dc2c..000ebb05 100644 --- a/daemon/core/gui/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -4,7 +4,7 @@ emane configuration import tkinter as tk import webbrowser from tkinter import ttk -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING import grpc @@ -19,8 +19,8 @@ if TYPE_CHECKING: class GlobalEmaneDialog(Dialog): - def __init__(self, master: Any, app: "Application"): - super().__init__(master, app, "EMANE Configuration") + def __init__(self, master: tk.BaseWidget, app: "Application"): + super().__init__(app, "EMANE Configuration", master=master) self.config_frame = None self.draw() @@ -52,14 +52,14 @@ class GlobalEmaneDialog(Dialog): class EmaneModelDialog(Dialog): def __init__( self, - master: Any, + master: tk.BaseWidget, app: "Application", canvas_node: "CanvasNode", model: str, interface: int = None, ): super().__init__( - master, app, f"{canvas_node.core_node.name} {model} Configuration" + app, f"{canvas_node.core_node.name} {model} Configuration", master=master ) self.canvas_node = canvas_node self.node = canvas_node.core_node @@ -109,13 +109,8 @@ class EmaneModelDialog(Dialog): class EmaneConfigDialog(Dialog): - def __init__( - self, master: "Application", app: "Application", canvas_node: "CanvasNode" - ): - super().__init__( - master, app, f"{canvas_node.core_node.name} EMANE Configuration" - ) - self.app = app + def __init__(self, app: "Application", canvas_node: "CanvasNode"): + super().__init__(app, f"{canvas_node.core_node.name} EMANE Configuration") self.canvas_node = canvas_node self.node = canvas_node.core_node self.radiovar = tk.IntVar() diff --git a/daemon/core/gui/dialogs/error.py b/daemon/core/gui/dialogs/error.py index 3703b533..5ff1dbc5 100644 --- a/daemon/core/gui/dialogs/error.py +++ b/daemon/core/gui/dialogs/error.py @@ -11,8 +11,8 @@ if TYPE_CHECKING: class ErrorDialog(Dialog): - def __init__(self, master, app: "Application", title: str, details: str) -> None: - super().__init__(master, app, "CORE Exception") + def __init__(self, app: "Application", title: str, details: str) -> None: + super().__init__(app, "CORE Exception") self.title = title self.details = details self.error_message = None diff --git a/daemon/core/gui/dialogs/executepython.py b/daemon/core/gui/dialogs/executepython.py index e0a1a40d..dd60c778 100644 --- a/daemon/core/gui/dialogs/executepython.py +++ b/daemon/core/gui/dialogs/executepython.py @@ -1,16 +1,19 @@ import logging import tkinter as tk from tkinter import filedialog, ttk +from typing import TYPE_CHECKING from core.gui.appconfig import SCRIPT_PATH from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX +if TYPE_CHECKING: + from core.gui.app import Application + class ExecutePythonDialog(Dialog): - def __init__(self, master, app): - super().__init__(master, app, "Execute Python Script") - self.app = app + def __init__(self, app: "Application"): + super().__init__(app, "Execute Python Script") self.with_options = tk.IntVar(value=0) self.options = tk.StringVar(value="") self.option_entry = None diff --git a/daemon/core/gui/dialogs/find.py b/daemon/core/gui/dialogs/find.py index 2541c74e..8f0094d4 100644 --- a/daemon/core/gui/dialogs/find.py +++ b/daemon/core/gui/dialogs/find.py @@ -1,14 +1,18 @@ import logging import tkinter as tk from tkinter import ttk +from typing import TYPE_CHECKING from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY +if TYPE_CHECKING: + from core.gui.app import Application + class FindDialog(Dialog): - def __init__(self, master, app) -> None: - super().__init__(master, app, "Find", modal=False) + def __init__(self, app: "Application") -> None: + super().__init__(app, "Find", modal=False) self.find_text = tk.StringVar(value="") self.tree = None self.draw() @@ -90,7 +94,8 @@ class FindDialog(Dialog): if not node_name or node_name == name: pos_x = round(node.core_node.position.x, 1) pos_y = round(node.core_node.position.y, 1) - # TODO I am not sure what to insert for Detail column, leaving in blank for now + # TODO: I am not sure what to insert for Detail column + # leaving it blank for now self.tree.insert( "", tk.END, diff --git a/daemon/core/gui/dialogs/hooks.py b/daemon/core/gui/dialogs/hooks.py index f9da431a..5895a2e1 100644 --- a/daemon/core/gui/dialogs/hooks.py +++ b/daemon/core/gui/dialogs/hooks.py @@ -1,6 +1,6 @@ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from core.api.grpc import core_pb2 from core.gui.dialogs.dialog import Dialog @@ -12,8 +12,8 @@ if TYPE_CHECKING: class HookDialog(Dialog): - def __init__(self, master: Any, app: "Application"): - super().__init__(master, app, "Hook") + def __init__(self, master: tk.BaseWidget, app: "Application"): + super().__init__(app, "Hook", master=master) self.name = tk.StringVar() self.codetext = None self.hook = core_pb2.Hook() @@ -88,8 +88,8 @@ class HookDialog(Dialog): class HooksDialog(Dialog): - def __init__(self, master: "Application", app: "Application"): - super().__init__(master, app, "Hooks") + def __init__(self, app: "Application"): + super().__init__(app, "Hooks") self.listbox = None self.edit_button = None self.delete_button = None diff --git a/daemon/core/gui/dialogs/ipdialog.py b/daemon/core/gui/dialogs/ipdialog.py index 72e0d73a..3c6944ab 100644 --- a/daemon/core/gui/dialogs/ipdialog.py +++ b/daemon/core/gui/dialogs/ipdialog.py @@ -14,8 +14,8 @@ if TYPE_CHECKING: class IpConfigDialog(Dialog): - def __init__(self, master: "Application", app: "Application") -> None: - super().__init__(master, app, "IP Configuration") + def __init__(self, app: "Application") -> None: + super().__init__(app, "IP Configuration") ip_config = self.app.guiconfig.setdefault("ips") self.ip4 = ip_config.setdefault("ip4", appconfig.DEFAULT_IP4) self.ip6 = ip_config.setdefault("ip6", appconfig.DEFAULT_IP6) diff --git a/daemon/core/gui/dialogs/linkconfig.py b/daemon/core/gui/dialogs/linkconfig.py index 5a93d3fa..4f569ef2 100644 --- a/daemon/core/gui/dialogs/linkconfig.py +++ b/daemon/core/gui/dialogs/linkconfig.py @@ -12,7 +12,7 @@ from core.gui.themes import PADX, PADY if TYPE_CHECKING: from core.gui.app import Application - from core.gui.graph.graph import CanvasGraph, CanvasEdge + from core.gui.graph.graph import CanvasEdge def get_int(var: tk.StringVar) -> Union[int, None]: @@ -32,9 +32,8 @@ def get_float(var: tk.StringVar) -> Union[float, None]: class LinkConfigurationDialog(Dialog): - def __init__(self, master: "CanvasGraph", app: "Application", edge: "CanvasEdge"): - super().__init__(master, app, "Link Configuration") - self.app = app + def __init__(self, app: "Application", edge: "CanvasEdge"): + super().__init__(app, "Link Configuration") self.edge = edge self.is_symmetric = edge.link.options.unidirectional is False if self.is_symmetric: diff --git a/daemon/core/gui/dialogs/macdialog.py b/daemon/core/gui/dialogs/macdialog.py index 558c3c29..18a330ba 100644 --- a/daemon/core/gui/dialogs/macdialog.py +++ b/daemon/core/gui/dialogs/macdialog.py @@ -13,8 +13,8 @@ if TYPE_CHECKING: class MacConfigDialog(Dialog): - def __init__(self, master: "Application", app: "Application") -> None: - super().__init__(master, app, "MAC Configuration") + def __init__(self, app: "Application") -> None: + super().__init__(app, "MAC Configuration") mac = self.app.guiconfig.get("mac", appconfig.DEFAULT_MAC) self.mac_var = tk.StringVar(value=mac) self.draw() diff --git a/daemon/core/gui/dialogs/marker.py b/daemon/core/gui/dialogs/marker.py index f07376b3..91cbfd06 100644 --- a/daemon/core/gui/dialogs/marker.py +++ b/daemon/core/gui/dialogs/marker.py @@ -17,11 +17,8 @@ MARKER_THICKNESS = [3, 5, 8, 10] class MarkerDialog(Dialog): - def __init__( - self, master: "Application", app: "Application", initcolor: str = "#000000" - ): - super().__init__(master, app, "Marker Tool", modal=False) - self.app = app + def __init__(self, app: "Application", initcolor: str = "#000000"): + super().__init__(app, "Marker Tool", modal=False) self.color = initcolor self.radius = MARKER_THICKNESS[0] self.marker_thickness = tk.IntVar(value=MARKER_THICKNESS[0]) diff --git a/daemon/core/gui/dialogs/mobilityconfig.py b/daemon/core/gui/dialogs/mobilityconfig.py index b4a6a163..dced5e44 100644 --- a/daemon/core/gui/dialogs/mobilityconfig.py +++ b/daemon/core/gui/dialogs/mobilityconfig.py @@ -16,12 +16,8 @@ if TYPE_CHECKING: class MobilityConfigDialog(Dialog): - def __init__( - self, master: "Application", app: "Application", canvas_node: "CanvasNode" - ): - super().__init__( - master, app, f"{canvas_node.core_node.name} Mobility Configuration" - ) + def __init__(self, app: "Application", canvas_node: "CanvasNode"): + super().__init__(app, f"{canvas_node.core_node.name} Mobility Configuration") self.canvas_node = canvas_node self.node = canvas_node.core_node self.config_frame = None diff --git a/daemon/core/gui/dialogs/mobilityplayer.py b/daemon/core/gui/dialogs/mobilityplayer.py index 1cd62684..e3baf140 100644 --- a/daemon/core/gui/dialogs/mobilityplayer.py +++ b/daemon/core/gui/dialogs/mobilityplayer.py @@ -1,6 +1,6 @@ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING import grpc @@ -17,14 +17,7 @@ ICON_SIZE = 16 class MobilityPlayer: - def __init__( - self, - master: "Application", - app: "Application", - canvas_node: "CanvasNode", - config, - ): - self.master = master + def __init__(self, app: "Application", canvas_node: "CanvasNode", config): self.app = app self.canvas_node = canvas_node self.config = config @@ -34,9 +27,7 @@ class MobilityPlayer: def show(self): if self.dialog: self.dialog.destroy() - self.dialog = MobilityPlayerDialog( - self.master, self.app, self.canvas_node, self.config - ) + self.dialog = MobilityPlayerDialog(self.app, self.canvas_node, self.config) self.dialog.protocol("WM_DELETE_WINDOW", self.close) if self.state == MobilityAction.START: self.set_play() @@ -68,11 +59,9 @@ class MobilityPlayer: class MobilityPlayerDialog(Dialog): - def __init__( - self, master: Any, app: "Application", canvas_node: "CanvasNode", config - ): + def __init__(self, app: "Application", canvas_node: "CanvasNode", config): super().__init__( - master, app, f"{canvas_node.core_node.name} Mobility Player", modal=False + app, f"{canvas_node.core_node.name} Mobility Player", modal=False ) self.resizable(False, False) self.geometry("") diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index ff21f886..85a839e5 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -94,13 +94,11 @@ class InterfaceData: class NodeConfigDialog(Dialog): - def __init__( - self, master: "Application", app: "Application", canvas_node: "CanvasNode" - ): + def __init__(self, app: "Application", canvas_node: "CanvasNode"): """ create an instance of node configuration """ - super().__init__(master, app, f"{canvas_node.core_node.name} Configuration") + super().__init__(app, f"{canvas_node.core_node.name} Configuration") self.canvas_node = canvas_node self.node = canvas_node.core_node self.image = canvas_node.image diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeconfigservice.py index 0e5ba7bb..5f77ece3 100644 --- a/daemon/core/gui/dialogs/nodeconfigservice.py +++ b/daemon/core/gui/dialogs/nodeconfigservice.py @@ -18,15 +18,10 @@ if TYPE_CHECKING: class NodeConfigServiceDialog(Dialog): def __init__( - self, - master: tk.Widget, - app: "Application", - canvas_node: "CanvasNode", - services: Set[str] = None, + self, app: "Application", canvas_node: "CanvasNode", services: Set[str] = None ): title = f"{canvas_node.core_node.name} Config Services" - super().__init__(master, app, title) - self.app = app + super().__init__(app, title) self.canvas_node = canvas_node self.node_id = canvas_node.core_node.id self.groups = None diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index 6641bf56..13490d8c 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -16,12 +16,9 @@ if TYPE_CHECKING: class NodeServiceDialog(Dialog): - def __init__( - self, master: tk.Widget, app: "Application", canvas_node: "CanvasNode" - ): + def __init__(self, app: "Application", canvas_node: "CanvasNode"): title = f"{canvas_node.core_node.name} Services" - super().__init__(master, app, title) - self.app = app + super().__init__(app, title) self.canvas_node = canvas_node self.node_id = canvas_node.core_node.id self.groups = None diff --git a/daemon/core/gui/dialogs/observers.py b/daemon/core/gui/dialogs/observers.py index 1282789e..4ec03185 100644 --- a/daemon/core/gui/dialogs/observers.py +++ b/daemon/core/gui/dialogs/observers.py @@ -12,8 +12,8 @@ if TYPE_CHECKING: class ObserverDialog(Dialog): - def __init__(self, master: "Application", app: "Application"): - super().__init__(master, app, "Observer Widgets") + def __init__(self, app: "Application"): + super().__init__(app, "Observer Widgets") self.observers = None self.save_button = None self.delete_button = None diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index 9c6ba5b9..c650f42a 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -16,8 +16,8 @@ SCALE_INTERVAL = 0.01 class PreferencesDialog(Dialog): - def __init__(self, master: "Application", app: "Application"): - super().__init__(master, app, "Preferences") + def __init__(self, app: "Application"): + super().__init__(app, "Preferences") self.gui_scale = tk.DoubleVar(value=self.app.app_scale) preferences = self.app.guiconfig["preferences"] self.editor = tk.StringVar(value=preferences["editor"]) diff --git a/daemon/core/gui/dialogs/runtool.py b/daemon/core/gui/dialogs/runtool.py index c3e3dec9..98be730f 100644 --- a/daemon/core/gui/dialogs/runtool.py +++ b/daemon/core/gui/dialogs/runtool.py @@ -1,17 +1,20 @@ import tkinter as tk from tkinter import ttk +from typing import TYPE_CHECKING from core.gui.dialogs.dialog import Dialog from core.gui.nodeutils import NodeUtils from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CodeText, ListboxScroll +if TYPE_CHECKING: + from core.gui.app import Application + class RunToolDialog(Dialog): - def __init__(self, master, app) -> None: - super().__init__(master, app, "Run Tool") + def __init__(self, app: "Application") -> None: + super().__init__(app, "Run Tool") self.cmd = tk.StringVar(value="ps ax") - self.app = app self.result = None self.node_list = None self.executable_nodes = {} diff --git a/daemon/core/gui/dialogs/servers.py b/daemon/core/gui/dialogs/servers.py index 26a76835..62bcc675 100644 --- a/daemon/core/gui/dialogs/servers.py +++ b/daemon/core/gui/dialogs/servers.py @@ -16,8 +16,8 @@ DEFAULT_PORT = 50051 class ServersDialog(Dialog): - def __init__(self, master: "Application", app: "Application"): - super().__init__(master, app, "CORE Servers") + def __init__(self, app: "Application"): + super().__init__(app, "CORE Servers") self.name = tk.StringVar(value=DEFAULT_NAME) self.address = tk.StringVar(value=DEFAULT_ADDRESS) self.port = tk.IntVar(value=DEFAULT_PORT) diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index 79e8871f..30607163 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -2,7 +2,7 @@ import logging import os import tkinter as tk from tkinter import filedialog, ttk -from typing import TYPE_CHECKING, Any, List +from typing import TYPE_CHECKING, List import grpc @@ -21,16 +21,14 @@ if TYPE_CHECKING: class ServiceConfigDialog(Dialog): def __init__( self, - master: Any, + master: tk.BaseWidget, app: "Application", service_name: str, canvas_node: "CanvasNode", node_id: int, ): title = f"{service_name} Service" - super().__init__(master, app, title) - self.master = master - self.app = app + super().__init__(app, title, master=master) self.core = app.core self.canvas_node = canvas_node self.node_id = node_id diff --git a/daemon/core/gui/dialogs/sessionoptions.py b/daemon/core/gui/dialogs/sessionoptions.py index b2c5ede5..c1455399 100644 --- a/daemon/core/gui/dialogs/sessionoptions.py +++ b/daemon/core/gui/dialogs/sessionoptions.py @@ -13,8 +13,8 @@ if TYPE_CHECKING: class SessionOptionsDialog(Dialog): - def __init__(self, master: "Application", app: "Application"): - super().__init__(master, app, "Session Options") + def __init__(self, app: "Application"): + super().__init__(app, "Session Options") self.config_frame = None self.has_error = False self.config = self.get_config() diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index f7c4722a..160854a6 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -16,10 +16,8 @@ if TYPE_CHECKING: class SessionsDialog(Dialog): - def __init__( - self, master: "Application", app: "Application", is_start_app: bool = False - ) -> None: - super().__init__(master, app, "Sessions") + def __init__(self, app: "Application", is_start_app: bool = False) -> None: + super().__init__(app, "Sessions") self.is_start_app = is_start_app self.selected_session = None self.selected_id = None diff --git a/daemon/core/gui/dialogs/shapemod.py b/daemon/core/gui/dialogs/shapemod.py index 9efb9fa3..4c84991b 100644 --- a/daemon/core/gui/dialogs/shapemod.py +++ b/daemon/core/gui/dialogs/shapemod.py @@ -20,12 +20,12 @@ BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] class ShapeDialog(Dialog): - def __init__(self, master: "Application", app: "Application", shape: "Shape"): + def __init__(self, app: "Application", shape: "Shape"): if is_draw_shape(shape.shape_type): title = "Add Shape" else: title = "Add Text" - super().__init__(master, app, title) + super().__init__(app, title) self.canvas = app.canvas self.fill = None self.border = None diff --git a/daemon/core/gui/dialogs/throughput.py b/daemon/core/gui/dialogs/throughput.py index 5c6b1d28..5210fe59 100644 --- a/daemon/core/gui/dialogs/throughput.py +++ b/daemon/core/gui/dialogs/throughput.py @@ -14,9 +14,8 @@ if TYPE_CHECKING: class ThroughputDialog(Dialog): - def __init__(self, master: "Application", app: "Application"): - super().__init__(master, app, "Throughput Config") - self.app = app + def __init__(self, app: "Application"): + super().__init__(app, "Throughput Config") self.canvas = app.canvas self.show_throughput = tk.IntVar(value=1) self.exponential_weight = tk.IntVar(value=1) diff --git a/daemon/core/gui/dialogs/wlanconfig.py b/daemon/core/gui/dialogs/wlanconfig.py index 5096cd0f..b0435a2f 100644 --- a/daemon/core/gui/dialogs/wlanconfig.py +++ b/daemon/core/gui/dialogs/wlanconfig.py @@ -16,12 +16,8 @@ RANGE_WIDTH = 3 class WlanConfigDialog(Dialog): - def __init__( - self, master: "Application", app: "Application", canvas_node: "CanvasNode" - ): - super().__init__( - master, app, f"{canvas_node.core_node.name} WLAN Configuration" - ) + def __init__(self, app: "Application", canvas_node: "CanvasNode"): + super().__init__(app, f"{canvas_node.core_node.name} WLAN Configuration") self.canvas_node = canvas_node self.node = canvas_node.core_node self.config_frame = None diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 17809dcb..68c3823b 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -389,5 +389,5 @@ class CanvasEdge(Edge): self.canvas.delete_edge(self) def click_configure(self) -> None: - dialog = LinkConfigurationDialog(self.canvas, self.canvas.app, self) + dialog = LinkConfigurationDialog(self.canvas.app, self) dialog.show() diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 9f0e7bce..fa169598 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -720,7 +720,7 @@ class CanvasGraph(tk.Canvas): selected = self.get_selected(event) if selected is not None and selected in self.shapes: shape = self.shapes[selected] - dialog = ShapeDialog(self.app, self.app, shape) + dialog = ShapeDialog(self.app, shape) dialog.show() def add_node(self, x: float, y: float) -> CanvasNode: diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index c90be311..451298e0 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -265,16 +265,16 @@ class CanvasNode: self.canvas.copy() def show_config(self): - dialog = NodeConfigDialog(self.app, self.app, self) + dialog = NodeConfigDialog(self.app, self) dialog.show() def show_wlan_config(self): - dialog = WlanConfigDialog(self.app, self.app, self) + dialog = WlanConfigDialog(self.app, self) if not dialog.has_error: dialog.show() def show_mobility_config(self): - dialog = MobilityConfigDialog(self.app, self.app, self) + dialog = MobilityConfigDialog(self.app, self) if not dialog.has_error: dialog.show() @@ -283,15 +283,15 @@ class CanvasNode: mobility_player.show() def show_emane_config(self): - dialog = EmaneConfigDialog(self.app, self.app, self) + dialog = EmaneConfigDialog(self.app, self) dialog.show() def show_services(self): - dialog = NodeServiceDialog(self.app, self.app, self) + dialog = NodeServiceDialog(self.app, self) dialog.show() def show_config_services(self): - dialog = NodeConfigServiceDialog(self.app, self.app, self) + dialog = NodeConfigServiceDialog(self.app, self) dialog.show() def has_emane_link(self, interface_id: int) -> core_pb2.Node: diff --git a/daemon/core/gui/graph/shape.py b/daemon/core/gui/graph/shape.py index 6e3d682d..eeda09fd 100644 --- a/daemon/core/gui/graph/shape.py +++ b/daemon/core/gui/graph/shape.py @@ -148,7 +148,7 @@ class Shape: def shape_complete(self, x: float, y: float): for component in tags.ABOVE_SHAPE: self.canvas.tag_raise(component) - s = ShapeDialog(self.app, self.app, self) + s = ShapeDialog(self.app, self) s.show() def disappear(self): diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 0d2f8e4e..c3ed071a 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -345,8 +345,8 @@ class Menubar(tk.Menu): task = ProgressTask(self.app, "Open XML", self.core.open_xml, args=(filename,)) task.start() - def execute_python(self): - dialog = ExecutePythonDialog(self.app, self.app) + def execute_python(self) -> None: + dialog = ExecutePythonDialog(self.app) dialog.show() def add_recent_file_to_gui_config(self, file_path) -> None: @@ -399,19 +399,19 @@ class Menubar(tk.Menu): self.core.xml_file = None def click_find(self, _event: tk.Event = None) -> None: - dialog = FindDialog(self.app, self.app) + dialog = FindDialog(self.app) dialog.show() def click_preferences(self) -> None: - dialog = PreferencesDialog(self.app, self.app) + dialog = PreferencesDialog(self.app) dialog.show() def click_canvas_size_and_scale(self) -> None: - dialog = SizeAndScaleDialog(self.app, self.app) + dialog = SizeAndScaleDialog(self.app) dialog.show() def click_canvas_wallpaper(self) -> None: - dialog = CanvasWallpaperDialog(self.app, self.app) + dialog = CanvasWallpaperDialog(self.app) dialog.show() def click_core_github(self) -> None: @@ -421,7 +421,7 @@ class Menubar(tk.Menu): webbrowser.open_new("http://coreemu.github.io/core/") def click_about(self) -> None: - dialog = AboutDialog(self.app, self.app) + dialog = AboutDialog(self.app) dialog.show() def click_throughput(self) -> None: @@ -431,7 +431,7 @@ class Menubar(tk.Menu): self.core.cancel_throughputs() def click_config_throughput(self) -> None: - dialog = ThroughputDialog(self.app, self.app) + dialog = ThroughputDialog(self.app) dialog.show() def click_copy(self, _event: tk.Event = None) -> None: @@ -449,27 +449,27 @@ class Menubar(tk.Menu): def click_session_options(self) -> None: logging.debug("Click options") - dialog = SessionOptionsDialog(self.app, self.app) + dialog = SessionOptionsDialog(self.app) if not dialog.has_error: dialog.show() def click_sessions(self) -> None: logging.debug("Click change sessions") - dialog = SessionsDialog(self.app, self.app) + dialog = SessionsDialog(self.app) dialog.show() def click_hooks(self) -> None: logging.debug("Click hooks") - dialog = HooksDialog(self.app, self.app) + dialog = HooksDialog(self.app) dialog.show() def click_servers(self) -> None: logging.debug("Click emulation servers") - dialog = ServersDialog(self.app, self.app) + dialog = ServersDialog(self.app) dialog.show() def click_edit_observer_widgets(self) -> None: - dialog = ObserverDialog(self.app, self.app) + dialog = ObserverDialog(self.app) dialog.show() def click_autogrid(self) -> None: @@ -492,13 +492,13 @@ class Menubar(tk.Menu): edge.draw_labels() def click_mac_config(self) -> None: - dialog = MacConfigDialog(self.app, self.app) + dialog = MacConfigDialog(self.app) dialog.show() def click_ip_config(self) -> None: - dialog = IpConfigDialog(self.app, self.app) + dialog = IpConfigDialog(self.app) dialog.show() def click_custom_nodes(self) -> None: - dialog = CustomNodesDialog(self.app, self.app) + dialog = CustomNodesDialog(self.app) dialog.show() diff --git a/daemon/core/gui/statusbar.py b/daemon/core/gui/statusbar.py index 1f882b08..6c2e5e19 100644 --- a/daemon/core/gui/statusbar.py +++ b/daemon/core/gui/statusbar.py @@ -65,7 +65,7 @@ class StatusBar(ttk.Frame): self.alerts_button.grid(row=0, column=3, sticky="ew") def click_alerts(self): - dialog = AlertsDialog(self.app, self.app) + dialog = AlertsDialog(self.app) dialog.show() def set_status(self, message: str): diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 0989affd..01a6bc1b 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -459,12 +459,12 @@ class Toolbar(ttk.Frame): if is_marker(shape_type): if self.marker_tool: self.marker_tool.destroy() - self.marker_tool = MarkerDialog(self.app, self.app) + self.marker_tool = MarkerDialog(self.app) self.marker_tool.show() def click_run_button(self): logging.debug("Click on RUN button") - dialog = RunToolDialog(self.app, self.app) + dialog = RunToolDialog(self.app) dialog.show() def click_marker_button(self): @@ -474,7 +474,7 @@ class Toolbar(ttk.Frame): self.app.canvas.annotation_type = ShapeType.MARKER if self.marker_tool: self.marker_tool.destroy() - self.marker_tool = MarkerDialog(self.app, self.app) + self.marker_tool = MarkerDialog(self.app) self.marker_tool.show() def scale_button(self, button, image_enum): From 41b46b7e7a2c32a56272afa3cdc7626ff251a7f2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 5 May 2020 12:55:25 -0700 Subject: [PATCH 0235/1131] pygui display error and link to emane docs when attempting to use emane node and it is not installed, fix dialog refactoring breaking mobility player, updated emane docs --- daemon/core/gui/coreclient.py | 13 ++++-- daemon/core/gui/dialogs/emaneinstall.py | 25 ++++++++++++ daemon/core/gui/graph/graph.py | 38 ++++++++++-------- docs/emane.md | 53 ++++++++++++++----------- 4 files changed, 84 insertions(+), 45 deletions(-) create mode 100644 daemon/core/gui/dialogs/emaneinstall.py diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 118138c6..950d7013 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -6,7 +6,7 @@ import logging import os from pathlib import Path from tkinter import messagebox -from typing import TYPE_CHECKING, Dict, Iterable, List +from typing import TYPE_CHECKING, Dict, Iterable, List, Optional import grpc @@ -16,6 +16,7 @@ from core.api.grpc.mobility_pb2 import MobilityConfig from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig, ServiceFileConfig from core.api.grpc.wlan_pb2 import WlanConfig from core.gui import appconfig +from core.gui.dialogs.emaneinstall import EmaneInstallDialog from core.gui.dialogs.error import ErrorDialog from core.gui.dialogs.mobilityplayer import MobilityPlayer from core.gui.dialogs.sessions import SessionsDialog @@ -552,7 +553,7 @@ class CoreClient: continue if canvas_node.mobility_config: mobility_player = MobilityPlayer( - self.app, self.app, canvas_node, canvas_node.mobility_config + self.app, canvas_node, canvas_node.mobility_config ) node_id = canvas_node.core_node.id self.mobility_players[node_id] = mobility_player @@ -785,7 +786,7 @@ class CoreClient: def create_node( self, x: float, y: float, node_type: core_pb2.NodeType, model: str - ) -> core_pb2.Node: + ) -> Optional[core_pb2.Node]: """ Add node, with information filled in, to grpc manager """ @@ -796,6 +797,10 @@ class CoreClient: image = "ubuntu:latest" emane = None if node_type == core_pb2.NodeType.EMANE: + if not self.emane_models: + dialog = EmaneInstallDialog(self.app) + dialog.show() + return emane = self.emane_models[0] name = f"EMANE{node_id}" elif node_type == core_pb2.NodeType.WIRELESS_LAN: @@ -818,7 +823,7 @@ class CoreClient: node.services[:] = services # assign default services to CORE node else: - services = self.default_services.get(model, None) + services = self.default_services.get(model) if services: node.services[:] = services logging.info( diff --git a/daemon/core/gui/dialogs/emaneinstall.py b/daemon/core/gui/dialogs/emaneinstall.py new file mode 100644 index 00000000..93cf2ac4 --- /dev/null +++ b/daemon/core/gui/dialogs/emaneinstall.py @@ -0,0 +1,25 @@ +import webbrowser +from tkinter import ttk + +from core.gui.dialogs.dialog import Dialog +from core.gui.themes import PADY + + +class EmaneInstallDialog(Dialog): + def __init__(self, app) -> None: + super().__init__(app, "EMANE Error") + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + label = ttk.Label(self.top, text="EMANE needs to be installed!") + label.grid(sticky="ew", pady=PADY) + button = ttk.Button( + self.top, text="EMANE Documentation", command=self.click_doc + ) + button.grid(sticky="ew", pady=PADY) + button = ttk.Button(self.top, text="Close", command=self.destroy) + button.grid(sticky="ew") + + def click_doc(self): + webbrowser.open_new("https://coreemu.github.io/core/emane.html") diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index fa169598..6913dd58 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -723,24 +723,26 @@ class CanvasGraph(tk.Canvas): dialog = ShapeDialog(self.app, shape) dialog.show() - def add_node(self, x: float, y: float) -> CanvasNode: - if self.selected is None or self.selected in self.shapes: - actual_x, actual_y = self.get_actual_coords(x, y) - core_node = self.core.create_node( - actual_x, actual_y, self.node_draw.node_type, self.node_draw.model + def add_node(self, x: float, y: float) -> None: + if self.selected is not None and self.selected not in self.shapes: + return + actual_x, actual_y = self.get_actual_coords(x, y) + core_node = self.core.create_node( + actual_x, actual_y, self.node_draw.node_type, self.node_draw.model + ) + if not core_node: + return + try: + self.node_draw.image = Images.get( + self.node_draw.image_enum, int(ICON_SIZE * self.app.app_scale) ) - try: - self.node_draw.image = Images.get( - self.node_draw.image_enum, int(ICON_SIZE * self.app.app_scale) - ) - except AttributeError: - self.node_draw.image = Images.get_custom( - self.node_draw.image_file, int(ICON_SIZE * self.app.app_scale) - ) - node = CanvasNode(self.app, x, y, core_node, self.node_draw.image) - self.core.canvas_nodes[core_node.id] = node - self.nodes[node.id] = node - return node + except AttributeError: + self.node_draw.image = Images.get_custom( + self.node_draw.image_file, int(ICON_SIZE * self.app.app_scale) + ) + node = CanvasNode(self.app, x, y, core_node, self.node_draw.image) + self.core.canvas_nodes[core_node.id] = node + self.nodes[node.id] = node def width_and_height(self): """ @@ -925,6 +927,8 @@ class CanvasGraph(tk.Canvas): copy = self.core.create_node( actual_x, actual_y, core_node.type, core_node.model ) + if not copy: + continue node = CanvasNode(self.app, scaled_x, scaled_y, copy, canvas_node.image) # copy configurations and services diff --git a/docs/emane.md b/docs/emane.md index b3289c6a..716d7059 100644 --- a/docs/emane.md +++ b/docs/emane.md @@ -50,10 +50,27 @@ 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 Installation + +EMANE can be installed from deb or RPM packages or from source. See the +[EMANE GitHub](https://github.com/adjacentlink/emane) for full details. + +Here are quick instructions for installing all EMANE packages for Ubuntu 18.04: +```shell +# install dependencies +sudo apt-get install libssl-dev libxml-libxml-perl libxml-simple-perl +wget https://adjacentlink.com/downloads/emane/emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz +tar xzf emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz +# install base emane packages +sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/emane*.deb +# install python3 bindings +sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/python3*.deb +``` + ## EMANE Configuration -The CORE configuration file */etc/core/core.conf* has options specific to -EMANE. An example emane section from the *core.conf* file is shown below: +The CORE configuration file **/etc/core/core.conf** has options specific to +EMANE. An example emane section from the **core.conf** file is shown below: ```shell # EMANE configuration @@ -64,40 +81,28 @@ emane_event_monitor = False # EMANE log level range [0,4] default: 2 emane_log_level = 2 emane_realtime = True -``` - -EMANE can be installed from deb or RPM packages or from source. See the -[EMANE GitHub](https://github.com/adjacentlink/emane) for full details. - -Here are quick instructions for installing all EMANE packages: - -```shell -# install dependencies -sudo apt-get install libssl-dev libxml-libxml-perl libxml-simple-perl -wget https://adjacentlink.com/downloads/emane/emane-1.2.1-release-1.ubuntu-16_04.amd64.tar.gz -tar xzf emane-1.2.1-release-1.ubuntu-16_04.amd64.tar.gz -sudo dpkg -i emane-1.2.1-release-1/deb/ubuntu-16_04/amd64/*.deb +# prefix used for emane installation +# emane_prefix = /usr ``` If you have an EMANE event generator (e.g. mobility or pathloss scripts) and want to have CORE subscribe to EMANE location events, set the following line -in the */etc/core/core.conf* configuration file: +in the **core.conf** configuration file. + +> **NOTE:** Do not set this option to True if you want to manually drag nodes around +on the canvas to update their location in EMANE. ```shell emane_event_monitor = True ``` -Do not set the above option to True if you want to manually drag nodes around -on the canvas to update their location in EMANE. - Another common issue is if installing EMANE from source, the default configure -prefix will place the DTD files in */usr/local/share/emane/dtd* while CORE -expects them in */usr/share/emane/dtd*. - -A symbolic link will fix this: +prefix will place the DTD files in **/usr/local/share/emane/dtd** while CORE +expects them in **/usr/share/emane/dtd**. +Update the EMANE prefix configuration to resolve this problem. ```shell -sudo ln -s /usr/local/share/emane /usr/share/emane +emane_prefix = /usr/local ``` ## Custom EMANE Models From d9f48d14a7797667d70d0b84e7737163553b52e8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 5 May 2020 13:00:22 -0700 Subject: [PATCH 0236/1131] pygui fixed button layout on session options dialog --- daemon/core/gui/dialogs/sessionoptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/gui/dialogs/sessionoptions.py b/daemon/core/gui/dialogs/sessionoptions.py index c1455399..d31a5fb5 100644 --- a/daemon/core/gui/dialogs/sessionoptions.py +++ b/daemon/core/gui/dialogs/sessionoptions.py @@ -46,7 +46,7 @@ class SessionOptionsDialog(Dialog): button = ttk.Button(frame, text="Save", command=self.save) button.grid(row=0, column=0, padx=PADX, sticky="ew") button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, padx=PADX, sticky="ew") + button.grid(row=0, column=1, sticky="ew") def save(self): config = self.config_frame.parse_config() From 86ae87eafe29a8eaa8aa1c9f46d0074712093edf Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 8 May 2020 00:16:25 -0700 Subject: [PATCH 0237/1131] pygui: revamped config to leverage classes mapped to yaml, removes need for using keys all over and type hinting on glasses, future changes should support defaults better --- daemon/core/gui/app.py | 8 +- daemon/core/gui/appconfig.py | 187 ++++++++++++++---- daemon/core/gui/coreclient.py | 52 ++--- daemon/core/gui/dialogs/canvassizeandscale.py | 22 +-- daemon/core/gui/dialogs/customnodes.py | 20 +- daemon/core/gui/dialogs/find.py | 6 +- daemon/core/gui/dialogs/ipdialog.py | 20 +- daemon/core/gui/dialogs/macdialog.py | 5 +- daemon/core/gui/dialogs/observers.py | 10 +- daemon/core/gui/dialogs/preferences.py | 23 ++- daemon/core/gui/dialogs/servers.py | 37 +--- daemon/core/gui/dialogs/sessions.py | 2 +- daemon/core/gui/graph/graph.py | 6 +- daemon/core/gui/interface.py | 8 +- daemon/core/gui/menubar.py | 6 +- daemon/core/gui/nodeutils.py | 47 ++--- 16 files changed, 251 insertions(+), 208 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index de0a5260..5ca95ab5 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -43,7 +43,7 @@ class Application(ttk.Frame): # setup self.guiconfig = appconfig.read() - self.app_scale = self.guiconfig["scale"] + self.app_scale = self.guiconfig.scale self.setup_scaling() self.style = ttk.Style() self.setup_theme() @@ -65,7 +65,7 @@ class Application(ttk.Frame): themes.load(self.style) self.master.bind_class("Menu", "<>", themes.theme_change_menu) self.master.bind("<>", themes.theme_change) - self.style.theme_use(self.guiconfig["preferences"]["theme"]) + self.style.theme_use(self.guiconfig.preferences.theme) def setup_app(self): self.master.title("CORE") @@ -103,8 +103,8 @@ class Application(ttk.Frame): self.menubar = Menubar(self.master, self) def draw_canvas(self): - width = self.guiconfig["preferences"]["width"] - height = self.guiconfig["preferences"]["height"] + width = self.guiconfig.preferences.width + height = self.guiconfig.preferences.height canvas_frame = ttk.Frame(self.right_frame) canvas_frame.rowconfigure(0, weight=1) canvas_frame.columnconfigure(0, weight=1) diff --git a/daemon/core/gui/appconfig.py b/daemon/core/gui/appconfig.py index c19fb029..d1d9bcc2 100644 --- a/daemon/core/gui/appconfig.py +++ b/daemon/core/gui/appconfig.py @@ -1,6 +1,7 @@ import os import shutil from pathlib import Path +from typing import List, Optional import yaml @@ -37,11 +38,6 @@ TERMINALS = { "gnome-terminal": "gnome-terminal --window --", } EDITORS = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"] -DEFAULT_IP4S = ["10.0.0.0", "192.168.0.0", "172.16.0.0"] -DEFAULT_IP4 = DEFAULT_IP4S[0] -DEFAULT_IP6S = ["2001::", "2002::", "a::"] -DEFAULT_IP6 = DEFAULT_IP6S[0] -DEFAULT_MAC = "00:00:00:aa:00:00" class IndentDumper(yaml.Dumper): @@ -49,13 +45,151 @@ class IndentDumper(yaml.Dumper): return super().increase_indent(flow, False) -def copy_files(current_path, new_path): +class CustomNode(yaml.YAMLObject): + yaml_tag = "!CustomNode" + yaml_loader = yaml.SafeLoader + + def __init__(self, name: str, image: str, services: List[str]) -> None: + self.name = name + self.image = image + self.services = services + + +class CoreServer(yaml.YAMLObject): + yaml_tag = "!CoreServer" + yaml_loader = yaml.SafeLoader + + def __init__(self, name: str, address: str) -> None: + self.name = name + self.address = address + + +class Observer(yaml.YAMLObject): + yaml_tag = "!Observer" + yaml_loader = yaml.SafeLoader + + def __init__(self, name: str, cmd: str) -> None: + self.name = name + self.cmd = cmd + + +class PreferencesConfig(yaml.YAMLObject): + yaml_tag = "!PreferencesConfig" + yaml_loader = yaml.SafeLoader + + def __init__( + self, + editor: str = EDITORS[1], + terminal: str = None, + theme: str = themes.THEME_DARK, + gui3d: str = "/usr/local/bin/std3d.sh", + width: int = 1000, + height: int = 750, + ) -> None: + self.theme = theme + self.editor = editor + self.terminal = terminal + self.gui3d = gui3d + self.width = width + self.height = height + + +class LocationConfig(yaml.YAMLObject): + yaml_tag = "!LocationConfig" + yaml_loader = yaml.SafeLoader + + def __init__( + self, + x: float = 0.0, + y: float = 0.0, + z: float = 0.0, + lat: float = 47.5791667, + lon: float = -122.132322, + alt: float = 2.0, + scale: float = 150.0, + ) -> None: + self.x = x + self.y = y + self.z = z + self.lat = lat + self.lon = lon + self.alt = alt + self.scale = scale + + +class IpConfigs(yaml.YAMLObject): + yaml_tag = "!IpConfigs" + yaml_loader = yaml.SafeLoader + + def __init__( + self, + ip4: str = None, + ip6: str = None, + ip4s: List[str] = None, + ip6s: List[str] = None, + ) -> None: + if ip4s is None: + ip4s = ["10.0.0.0", "192.168.0.0", "172.16.0.0"] + self.ip4s = ip4s + if ip6s is None: + ip6s = ["2001::", "2002::", "a::"] + self.ip6s = ip6s + if ip4 is None: + ip4 = self.ip4s[0] + self.ip4 = ip4 + if ip6 is None: + ip6 = self.ip6s[0] + self.ip6 = ip6 + + +class GuiConfig(yaml.YAMLObject): + yaml_tag = "!GuiConfig" + yaml_loader = yaml.SafeLoader + + def __init__( + self, + preferences: PreferencesConfig = None, + location: LocationConfig = None, + servers: List[CoreServer] = None, + nodes: List[CustomNode] = None, + recentfiles: List[str] = None, + observers: List[Observer] = None, + scale: float = 1.0, + ips: IpConfigs = None, + mac: str = "00:00:00:aa:00:00", + ) -> None: + if preferences is None: + preferences = PreferencesConfig() + self.preferences = preferences + if location is None: + location = LocationConfig() + self.location = location + if servers is None: + servers = [] + self.servers = servers + if nodes is None: + nodes = [] + self.nodes = nodes + if recentfiles is None: + recentfiles = [] + self.recentfiles = recentfiles + if observers is None: + observers = [] + self.observers = observers + self.scale = scale + if ips is None: + ips = IpConfigs() + self.ips = ips + self.mac = mac + + +def copy_files(current_path, new_path) -> None: for current_file in current_path.glob("*"): new_file = new_path.joinpath(current_file.name) shutil.copy(current_file, new_file) -def find_terminal(): +def find_terminal() -> Optional[str]: for term in sorted(TERMINALS): cmd = TERMINALS[term] if shutil.which(term): @@ -63,7 +197,7 @@ def find_terminal(): return None -def check_directory(): +def check_directory() -> None: if HOME_PATH.exists(): return HOME_PATH.mkdir() @@ -85,45 +219,16 @@ def check_directory(): editor = EDITORS[0] else: editor = EDITORS[1] - config = { - "preferences": { - "theme": themes.THEME_DARK, - "editor": editor, - "terminal": terminal, - "gui3d": "/usr/local/bin/std3d.sh", - "width": 1000, - "height": 750, - }, - "location": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "lat": 47.5791667, - "lon": -122.132322, - "alt": 2.0, - "scale": 150.0, - }, - "servers": [], - "nodes": [], - "recentfiles": [], - "observers": [], - "scale": 1.0, - "ips": { - "ip4": DEFAULT_IP4, - "ip6": DEFAULT_IP6, - "ip4s": DEFAULT_IP4S, - "ip6s": DEFAULT_IP6S, - }, - "mac": DEFAULT_MAC, - } + preferences = PreferencesConfig(editor, terminal) + config = GuiConfig(preferences=preferences) save(config) -def read(): +def read() -> GuiConfig: with CONFIG_PATH.open("r") as f: return yaml.load(f, Loader=yaml.SafeLoader) -def save(config): +def save(config: GuiConfig) -> None: with CONFIG_PATH.open("w") as f: yaml.dump(config, f, Dumper=IndentDumper, default_flow_style=False) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 950d7013..bc9cdc37 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -34,19 +34,6 @@ if TYPE_CHECKING: GUI_SOURCE = "gui" -class CoreServer: - def __init__(self, name: str, address: str, port: int): - self.name = name - self.address = address - self.port = port - - -class Observer: - def __init__(self, name: str, cmd: str): - self.name = name - self.cmd = cmd - - class CoreClient: def __init__(self, app: "Application", proxy: bool): """ @@ -126,22 +113,17 @@ class CoreClient: self.observer = value def read_config(self): - # read distributed server - for config in self.app.guiconfig.get("servers", []): - server = CoreServer(config["name"], config["address"], config["port"]) + # read distributed servers + for server in self.app.guiconfig.servers: self.servers[server.name] = server # read custom nodes - for config in self.app.guiconfig.get("nodes", []): - name = config["name"] - image_file = config["image"] - services = set(config["services"]) - node_draw = NodeDraw.from_custom(name, image_file, services) - self.custom_nodes[name] = node_draw + for custom_node in self.app.guiconfig.nodes: + node_draw = NodeDraw.from_custom(custom_node) + self.custom_nodes[custom_node.name] = node_draw # read observers - for config in self.app.guiconfig.get("observers", []): - observer = Observer(config["name"], config["cmd"]) + for observer in self.app.guiconfig.observers: self.custom_observers[observer.name] = observer def handle_events(self, event: core_pb2.Event): @@ -367,8 +349,8 @@ class CoreClient: self.app.canvas.adjust_to_dim.set(fit_image) wallpaper_style = canvas_config.get("wallpaper-style", 1) self.app.canvas.scale_option.set(wallpaper_style) - width = self.app.guiconfig["preferences"]["width"] - height = self.app.guiconfig["preferences"]["height"] + width = self.app.guiconfig.preferences.width + height = self.app.guiconfig.preferences.height dimensions = canvas_config.get("dimensions", [width, height]) self.app.canvas.redraw_canvas(dimensions) wallpaper = canvas_config.get("wallpaper") @@ -418,15 +400,15 @@ class CoreClient: try: response = self.client.create_session() logging.info("created session: %s", response) - location_config = self.app.guiconfig["location"] + location_config = self.app.guiconfig.location self.location = core_pb2.SessionLocation( - x=location_config["x"], - y=location_config["y"], - z=location_config["z"], - lat=location_config["lat"], - lon=location_config["lon"], - alt=location_config["alt"], - scale=location_config["scale"], + x=location_config.x, + y=location_config.y, + z=location_config.z, + lat=location_config.lat, + lon=location_config.lon, + alt=location_config.alt, + scale=location_config.scale, ) self.join_session(response.session_id, query_location=False) except grpc.RpcError as e: @@ -585,7 +567,7 @@ class CoreClient: def launch_terminal(self, node_id: int): try: - terminal = self.app.guiconfig["preferences"]["terminal"] + terminal = self.app.guiconfig.preferences.terminal if not terminal: messagebox.showerror( "Terminal Error", diff --git a/daemon/core/gui/dialogs/canvassizeandscale.py b/daemon/core/gui/dialogs/canvassizeandscale.py index 3c3b8540..5a042468 100644 --- a/daemon/core/gui/dialogs/canvassizeandscale.py +++ b/daemon/core/gui/dialogs/canvassizeandscale.py @@ -241,16 +241,16 @@ class SizeAndScaleDialog(Dialog): location.alt = self.alt.get() location.scale = self.scale.get() if self.save_default.get(): - location_config = self.app.guiconfig["location"] - location_config["x"] = location.x - location_config["y"] = location.y - location_config["z"] = location.z - location_config["lat"] = location.lat - location_config["lon"] = location.lon - location_config["alt"] = location.alt - location_config["scale"] = location.scale - preferences = self.app.guiconfig["preferences"] - preferences["width"] = width - preferences["height"] = height + location_config = self.app.guiconfig.location + location_config.x = location.x + location_config.y = location.y + location_config.z = location.z + location_config.lat = location.lat + location_config.lon = location.lon + location_config.alt = location.alt + location_config.scale = location.scale + preferences = self.app.guiconfig.preferences + preferences.width = width + preferences.height = height self.app.save_config() self.destroy() diff --git a/daemon/core/gui/dialogs/customnodes.py b/daemon/core/gui/dialogs/customnodes.py index 28f33ffe..56012780 100644 --- a/daemon/core/gui/dialogs/customnodes.py +++ b/daemon/core/gui/dialogs/customnodes.py @@ -5,7 +5,7 @@ from tkinter import ttk from typing import TYPE_CHECKING, Set from core.gui import nodeutils -from core.gui.appconfig import ICONS_PATH +from core.gui.appconfig import ICONS_PATH, CustomNode from core.gui.dialogs.dialog import Dialog from core.gui.images import Images from core.gui.nodeutils import NodeDraw @@ -201,17 +201,12 @@ class CustomNodesDialog(Dialog): self.services.update(dialog.current_services) def click_save(self): - self.app.guiconfig["nodes"].clear() - for name in sorted(self.app.core.custom_nodes): + self.app.guiconfig.nodes.clear() + for name in self.app.core.custom_nodes: node_draw = self.app.core.custom_nodes[name] - self.app.guiconfig["nodes"].append( - { - "name": name, - "image": node_draw.image_file, - "services": list(node_draw.services), - } - ) - logging.info("saving custom nodes: %s", self.app.guiconfig["nodes"]) + custom_node = CustomNode(name, node_draw.image_file, node_draw.services) + self.app.guiconfig.nodes.append(custom_node) + logging.info("saving custom nodes: %s", self.app.guiconfig.nodes) self.app.save_config() self.destroy() @@ -219,7 +214,8 @@ class CustomNodesDialog(Dialog): name = self.name.get() if name not in self.app.core.custom_nodes: image_file = Path(self.image_file).stem - node_draw = NodeDraw.from_custom(name, image_file, set(self.services)) + custom_node = CustomNode(name, image_file, list(self.services)) + node_draw = NodeDraw.from_custom(custom_node) logging.info( "created new custom node (%s), image file (%s), services: (%s)", name, diff --git a/daemon/core/gui/dialogs/find.py b/daemon/core/gui/dialogs/find.py index 8f0094d4..25da4b19 100644 --- a/daemon/core/gui/dialogs/find.py +++ b/daemon/core/gui/dialogs/find.py @@ -45,7 +45,7 @@ class FindDialog(Dialog): ) self.tree.grid(sticky="nsew", pady=PADY) style = ttk.Style() - heading_size = int(self.app.guiconfig["scale"] * 10) + heading_size = int(self.app.guiconfig.scale * 10) style.configure("Treeview.Heading", font=(None, heading_size, "bold")) self.tree.column("nodeid", stretch=tk.YES, anchor="center") self.tree.heading("nodeid", text="Node ID") @@ -124,7 +124,7 @@ class FindDialog(Dialog): canvas_node = self.app.core.canvas_nodes[node_id] x0, y0, x1, y1 = self.app.canvas.bbox(canvas_node.id) - dist = 5 * self.app.guiconfig["scale"] + dist = 5 * self.app.guiconfig.scale self.app.canvas.create_oval( x0 - dist, y0 - dist, @@ -132,7 +132,7 @@ class FindDialog(Dialog): y1 + dist, tags="find", outline="red", - width=3.0 * self.app.guiconfig["scale"], + width=3.0 * self.app.guiconfig.scale, ) _x, _y, _, _ = self.app.canvas.bbox(canvas_node.id) diff --git a/daemon/core/gui/dialogs/ipdialog.py b/daemon/core/gui/dialogs/ipdialog.py index 3c6944ab..62f5d0ba 100644 --- a/daemon/core/gui/dialogs/ipdialog.py +++ b/daemon/core/gui/dialogs/ipdialog.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING import netaddr -from core.gui import appconfig from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import ListboxScroll @@ -16,11 +15,10 @@ if TYPE_CHECKING: class IpConfigDialog(Dialog): def __init__(self, app: "Application") -> None: super().__init__(app, "IP Configuration") - ip_config = self.app.guiconfig.setdefault("ips") - self.ip4 = ip_config.setdefault("ip4", appconfig.DEFAULT_IP4) - self.ip6 = ip_config.setdefault("ip6", appconfig.DEFAULT_IP6) - self.ip4s = ip_config.setdefault("ip4s", appconfig.DEFAULT_IP4S) - self.ip6s = ip_config.setdefault("ip6s", appconfig.DEFAULT_IP6S) + self.ip4 = self.app.guiconfig.ips.ip4 + self.ip6 = self.app.guiconfig.ips.ip6 + self.ip4s = self.app.guiconfig.ips.ip4s + self.ip6s = self.app.guiconfig.ips.ip6s self.ip4_entry = None self.ip4_listbox = None self.ip6_entry = None @@ -143,11 +141,11 @@ class IpConfigDialog(Dialog): for index in range(self.ip6_listbox.listbox.size()): ip6 = self.ip6_listbox.listbox.get(index) ip6s.append(ip6) - ip_config = self.app.guiconfig["ips"] - ip_config["ip4"] = self.ip4 - ip_config["ip6"] = self.ip6 - ip_config["ip4s"] = ip4s - ip_config["ip6s"] = ip6s + ip_config = self.app.guiconfig.ips + ip_config.ip4 = self.ip4 + ip_config.ip6 = self.ip6 + ip_config.ip4s = ip4s + ip_config.ip6s = ip6s self.app.core.interfaces_manager.update_ips(self.ip4, self.ip6) self.app.save_config() self.destroy() diff --git a/daemon/core/gui/dialogs/macdialog.py b/daemon/core/gui/dialogs/macdialog.py index 18a330ba..caca9fd0 100644 --- a/daemon/core/gui/dialogs/macdialog.py +++ b/daemon/core/gui/dialogs/macdialog.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING import netaddr -from core.gui import appconfig from core.gui.dialogs.dialog import Dialog from core.gui.themes import PADX, PADY @@ -15,7 +14,7 @@ if TYPE_CHECKING: class MacConfigDialog(Dialog): def __init__(self, app: "Application") -> None: super().__init__(app, "MAC Configuration") - mac = self.app.guiconfig.get("mac", appconfig.DEFAULT_MAC) + mac = self.app.guiconfig.mac self.mac_var = tk.StringVar(value=mac) self.draw() @@ -57,6 +56,6 @@ class MacConfigDialog(Dialog): messagebox.showerror("MAC Error", f"{mac} is an invalid mac") else: self.app.core.interfaces_manager.mac = netaddr.EUI(mac) - self.app.guiconfig["mac"] = mac + self.app.guiconfig.mac = mac self.app.save_config() self.destroy() diff --git a/daemon/core/gui/dialogs/observers.py b/daemon/core/gui/dialogs/observers.py index 4ec03185..6911e1b8 100644 --- a/daemon/core/gui/dialogs/observers.py +++ b/daemon/core/gui/dialogs/observers.py @@ -2,7 +2,7 @@ import tkinter as tk from tkinter import messagebox, ttk from typing import TYPE_CHECKING -from core.gui.coreclient import Observer +from core.gui.appconfig import Observer from core.gui.dialogs.dialog import Dialog from core.gui.themes import PADX, PADY from core.gui.widgets import ListboxScroll @@ -89,11 +89,9 @@ class ObserverDialog(Dialog): button.grid(row=0, column=1, sticky="ew") def click_save_config(self): - observers = [] - for name in sorted(self.app.core.custom_observers): - observer = self.app.core.custom_observers[name] - observers.append({"name": observer.name, "cmd": observer.cmd}) - self.app.guiconfig["observers"] = observers + self.app.guiconfig.observers.clear() + for observer in self.app.core.custom_observers.values(): + self.app.guiconfig.observers.append(observer) self.app.save_config() self.destroy() diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index c650f42a..9c9ba16f 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -19,11 +19,11 @@ class PreferencesDialog(Dialog): def __init__(self, app: "Application"): super().__init__(app, "Preferences") self.gui_scale = tk.DoubleVar(value=self.app.app_scale) - preferences = self.app.guiconfig["preferences"] - self.editor = tk.StringVar(value=preferences["editor"]) - self.theme = tk.StringVar(value=preferences["theme"]) - self.terminal = tk.StringVar(value=preferences["terminal"]) - self.gui3d = tk.StringVar(value=preferences["gui3d"]) + preferences = self.app.guiconfig.preferences + self.editor = tk.StringVar(value=preferences.editor) + self.theme = tk.StringVar(value=preferences.theme) + self.terminal = tk.StringVar(value=preferences.terminal) + self.gui3d = tk.StringVar(value=preferences.gui3d) self.draw() def draw(self): @@ -110,15 +110,14 @@ class PreferencesDialog(Dialog): self.app.style.theme_use(theme) def click_save(self): - preferences = self.app.guiconfig["preferences"] - preferences["terminal"] = self.terminal.get() - preferences["editor"] = self.editor.get() - preferences["gui3d"] = self.gui3d.get() - preferences["theme"] = self.theme.get() + preferences = self.app.guiconfig.preferences + preferences.terminal = self.terminal.get() + preferences.editor = self.editor.get() + preferences.gui3d = self.gui3d.get() + preferences.theme = self.theme.get() self.gui_scale.set(round(self.gui_scale.get(), 2)) app_scale = self.gui_scale.get() - self.app.guiconfig["scale"] = app_scale - + self.app.guiconfig.scale = app_scale self.app.save_config() self.scale_adjust() self.destroy() diff --git a/daemon/core/gui/dialogs/servers.py b/daemon/core/gui/dialogs/servers.py index 62bcc675..7ca96e9f 100644 --- a/daemon/core/gui/dialogs/servers.py +++ b/daemon/core/gui/dialogs/servers.py @@ -2,7 +2,7 @@ import tkinter as tk from tkinter import ttk from typing import TYPE_CHECKING -from core.gui.coreclient import CoreServer +from core.gui.appconfig import CoreServer from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import ListboxScroll @@ -20,7 +20,6 @@ class ServersDialog(Dialog): super().__init__(app, "CORE Servers") self.name = tk.StringVar(value=DEFAULT_NAME) self.address = tk.StringVar(value=DEFAULT_ADDRESS) - self.port = tk.IntVar(value=DEFAULT_PORT) self.servers = None self.selected_index = None self.selected = None @@ -54,31 +53,17 @@ class ServersDialog(Dialog): frame.grid(pady=PADY, sticky="ew") frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) - frame.columnconfigure(5, weight=1) label = ttk.Label(frame, text="Name") - label.grid(row=0, column=0, sticky="w", padx=PADX, pady=PADY) + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=1, sticky="ew") label = ttk.Label(frame, text="Address") - label.grid(row=0, column=2, sticky="w", padx=PADX, pady=PADY) + label.grid(row=0, column=2, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.address) entry.grid(row=0, column=3, sticky="ew") - label = ttk.Label(frame, text="Port") - label.grid(row=0, column=4, sticky="w", padx=PADX, pady=PADY) - entry = ttk.Entry( - frame, - textvariable=self.port, - validate="key", - validatecommand=(self.app.validation.positive_int, "%P"), - ) - entry.bind( - "", lambda event: self.app.validation.focus_out(event, "50051") - ) - entry.grid(row=0, column=5, sticky="ew") - def draw_servers_buttons(self): frame = ttk.Frame(self.top) frame.grid(pady=PADY, sticky="ew") @@ -113,13 +98,9 @@ class ServersDialog(Dialog): button.grid(row=0, column=1, sticky="ew") def click_save_configuration(self): - servers = [] - for name in sorted(self.app.core.servers): - server = self.app.core.servers[name] - servers.append( - {"name": server.name, "address": server.address, "port": server.port} - ) - self.app.guiconfig["servers"] = servers + self.app.guiconfig.servers.clear() + for server in self.app.core.servers.values(): + self.app.guiconfig.servers.append(server) self.app.save_config() self.destroy() @@ -127,8 +108,7 @@ class ServersDialog(Dialog): name = self.name.get() if name not in self.app.core.servers: address = self.address.get() - port = self.port.get() - server = CoreServer(name, address, port) + server = CoreServer(name, address) self.app.core.servers[name] = server self.servers.insert(tk.END, name) @@ -140,7 +120,6 @@ class ServersDialog(Dialog): server = self.app.core.servers.pop(previous_name) server.name = name server.address = self.address.get() - server.port = self.port.get() self.app.core.servers[name] = server self.servers.delete(self.selected_index) self.servers.insert(self.selected_index, name) @@ -154,7 +133,6 @@ class ServersDialog(Dialog): self.selected_index = None self.name.set(DEFAULT_NAME) self.address.set(DEFAULT_ADDRESS) - self.port.set(DEFAULT_PORT) self.servers.selection_clear(0, tk.END) self.save_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED) @@ -167,7 +145,6 @@ class ServersDialog(Dialog): server = self.app.core.servers[self.selected] self.name.set(server.name) self.address.set(server.address) - self.port.set(server.port) self.save_button.config(state=tk.NORMAL) self.delete_button.config(state=tk.NORMAL) else: diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index 160854a6..9aa71a13 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -70,7 +70,7 @@ class SessionsDialog(Dialog): selectmode=tk.BROWSE, ) style = ttk.Style() - heading_size = int(self.app.guiconfig["scale"] * 10) + heading_size = int(self.app.guiconfig.scale * 10) style.configure("Treeview.Heading", font=(None, heading_size, "bold")) self.tree.grid(sticky="nsew") self.tree.column("id", stretch=tk.YES, anchor="center") diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 6913dd58..220e122f 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -1002,10 +1002,10 @@ class CanvasGraph(tk.Canvas): if NodeUtils.is_custom( canvas_node.core_node.type, canvas_node.core_node.model ): - for custom_node in self.app.guiconfig["nodes"]: - if custom_node["name"] == canvas_node.core_node.model: + for custom_node in self.app.guiconfig.nodes: + if custom_node.name == canvas_node.core_node.model: img = Images.get_custom( - custom_node["image"], int(ICON_SIZE * self.app.app_scale) + custom_node.image, int(ICON_SIZE * self.app.app_scale) ) else: image_enum = TypeToImage.get( diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index 6f5f5fff..0ff018c7 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Any, List, Optional, Set, Tuple import netaddr from netaddr import EUI, IPNetwork -from core.gui import appconfig from core.gui.nodeutils import NodeUtils if TYPE_CHECKING: @@ -44,14 +43,13 @@ class Subnets: class InterfaceManager: def __init__(self, app: "Application") -> None: self.app = app - ip_config = self.app.guiconfig.get("ips", {}) - ip4 = ip_config.get("ip4", appconfig.DEFAULT_IP4) - ip6 = ip_config.get("ip6", appconfig.DEFAULT_IP6) + ip4 = self.app.guiconfig.ips.ip4 + ip6 = self.app.guiconfig.ips.ip6 self.ip4_mask = 24 self.ip6_mask = 64 self.ip4_subnets = IPNetwork(f"{ip4}/{self.ip4_mask}") self.ip6_subnets = IPNetwork(f"{ip6}/{self.ip6_mask}") - mac = self.app.guiconfig.get("mac", appconfig.DEFAULT_MAC) + mac = self.app.guiconfig.mac self.mac = EUI(mac, dialect=netaddr.mac_unix_expanded) self.current_mac = None self.current_subnets = None diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index c3ed071a..2e07ed0a 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -93,7 +93,7 @@ class Menubar(tk.Menu): ) self.app.bind_all("", self.click_open_xml) self.recent_menu = tk.Menu(menu) - for i in self.app.guiconfig["recentfiles"]: + for i in self.app.guiconfig.recentfiles: self.recent_menu.add_command( label=i, command=partial(self.open_recent_files, i) ) @@ -298,7 +298,7 @@ class Menubar(tk.Menu): def update_recent_files(self) -> None: self.recent_menu.delete(0, tk.END) - for i in self.app.guiconfig["recentfiles"]: + for i in self.app.guiconfig.recentfiles: self.recent_menu.add_command( label=i, command=partial(self.open_recent_files, i) ) @@ -350,7 +350,7 @@ class Menubar(tk.Menu): dialog.show() def add_recent_file_to_gui_config(self, file_path) -> None: - recent_files = self.app.guiconfig["recentfiles"] + recent_files = self.app.guiconfig.recentfiles num_files = len(recent_files) if num_files == 0: recent_files.insert(0, file_path) diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index 24c01f06..4c2cec07 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -1,7 +1,8 @@ import logging -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Union +from typing import TYPE_CHECKING, List, Optional, Set from core.api.grpc.core_pb2 import Node, NodeType +from core.gui.appconfig import CustomNode, GuiConfig from core.gui.images import ImageEnum, Images, TypeToImage if TYPE_CHECKING: @@ -41,16 +42,16 @@ class NodeDraw: return node_draw @classmethod - def from_custom(cls, name: str, image_file: str, services: Set[str]): + def from_custom(cls, custom_node: CustomNode): node_draw = NodeDraw() node_draw.custom = True - node_draw.image_file = image_file - node_draw.image = Images.get_custom(image_file, ICON_SIZE) + node_draw.image_file = custom_node.image + node_draw.image = Images.get_custom(custom_node.image, ICON_SIZE) node_draw.node_type = NodeType.DEFAULT - node_draw.services = services - node_draw.label = name - node_draw.model = name - node_draw.tooltip = name + node_draw.services = custom_node.services + node_draw.label = custom_node.name + node_draw.model = custom_node.name + node_draw.tooltip = custom_node.name return node_draw @@ -97,11 +98,7 @@ class NodeUtils: @classmethod def node_icon( - cls, - node_type: NodeType, - model: str, - gui_config: Dict[str, List[Dict[str, str]]], - scale=1.0, + cls, node_type: NodeType, model: str, gui_config: GuiConfig, scale=1.0 ) -> "ImageTk.PhotoImage": image_enum = TypeToImage.get(node_type, model) @@ -114,10 +111,7 @@ class NodeUtils: @classmethod def node_image( - cls, - core_node: "core_pb2.Node", - gui_config: Dict[str, List[Dict[str, str]]], - scale=1.0, + cls, core_node: "core_pb2.Node", gui_config: GuiConfig, scale=1.0 ) -> "ImageTk.PhotoImage": image = cls.node_icon(core_node.type, core_node.model, gui_config, scale) if core_node.icon: @@ -132,20 +126,17 @@ class NodeUtils: return node_type == NodeType.DEFAULT and model not in cls.NODE_MODELS @classmethod - def get_custom_node_services( - cls, gui_config: Dict[str, List[Dict[str, str]]], name: str - ) -> List[str]: - for m in gui_config["nodes"]: - if m["name"] == name: - return m["services"] + def get_custom_node_services(cls, gui_config: GuiConfig, name: str) -> List[str]: + for custom_node in gui_config.nodes: + if custom_node.name == name: + return custom_node.services return [] @classmethod - def get_image_file(cls, gui_config, name: str) -> Union[str, None]: - if "nodes" in gui_config: - for m in gui_config["nodes"]: - if m["name"] == name: - return m["image"] + def get_image_file(cls, gui_config: GuiConfig, name: str) -> Optional[str]: + for custom_node in gui_config.nodes: + if custom_node.name == name: + return custom_node.image return None @classmethod From 32558d15d22c27b2c322f68f5000659c4720702c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 8 May 2020 00:46:02 -0700 Subject: [PATCH 0238/1131] pygui: removed comment in appconfig --- daemon/core/gui/appconfig.py | 1 - 1 file changed, 1 deletion(-) diff --git a/daemon/core/gui/appconfig.py b/daemon/core/gui/appconfig.py index d1d9bcc2..049b9bfc 100644 --- a/daemon/core/gui/appconfig.py +++ b/daemon/core/gui/appconfig.py @@ -5,7 +5,6 @@ from typing import List, Optional import yaml -# gui home paths from core.gui import themes HOME_PATH = Path.home().joinpath(".coretk") From 4379ef32e9b6c9e64c799016de2982b51ff22a1c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 8 May 2020 08:29:45 -0700 Subject: [PATCH 0239/1131] pygui: removed restriction on wlan nodes context linking to mdrs only, since custom nodes and other types may be configured and desired to be linked in the same way --- daemon/core/gui/graph/node.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 451298e0..41b4704a 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -312,12 +312,10 @@ class CanvasNode: return result def wireless_link_selected(self): - for canvas_nid in [ - x for x in self.canvas.selection if "node" in self.canvas.gettags(x) - ]: - core_node = self.canvas.nodes[canvas_nid].core_node - if core_node.type == core_pb2.NodeType.DEFAULT and core_node.model == "mdr": - self.canvas.create_edge(self, self.canvas.nodes[canvas_nid]) + nodes = [x for x in self.canvas.selection if x in self.canvas.nodes] + for node_id in nodes: + canvas_node = self.canvas.nodes[node_id] + self.canvas.create_edge(self, canvas_node) self.canvas.clear_selection() def scale_antennas(self): From 0aba1aa9287c8e7e30143f707574dbd893ccee9d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 8 May 2020 09:08:01 -0700 Subject: [PATCH 0240/1131] pygui: updated gui home directory to ~/.coregui and changed config file name to config.yaml from gui.yaml to be more explicit --- daemon/core/gui/appconfig.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daemon/core/gui/appconfig.py b/daemon/core/gui/appconfig.py index 049b9bfc..077f938d 100644 --- a/daemon/core/gui/appconfig.py +++ b/daemon/core/gui/appconfig.py @@ -7,14 +7,14 @@ import yaml from core.gui import themes -HOME_PATH = Path.home().joinpath(".coretk") +HOME_PATH = Path.home().joinpath(".coregui") BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds") CUSTOM_EMANE_PATH = HOME_PATH.joinpath("custom_emane") CUSTOM_SERVICE_PATH = HOME_PATH.joinpath("custom_services") ICONS_PATH = HOME_PATH.joinpath("icons") MOBILITY_PATH = HOME_PATH.joinpath("mobility") XMLS_PATH = HOME_PATH.joinpath("xmls") -CONFIG_PATH = HOME_PATH.joinpath("gui.yaml") +CONFIG_PATH = HOME_PATH.joinpath("config.yaml") LOG_PATH = HOME_PATH.joinpath("gui.log") SCRIPT_PATH = HOME_PATH.joinpath("scripts") From 9b4802a5aeb3e6512f67208abc4ca78d7bedcde0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 8 May 2020 09:22:22 -0700 Subject: [PATCH 0241/1131] updated install.sh to attempt to install the latest python dependencies on reinstall in case new dependencies have been introduced --- install.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install.sh b/install.sh index 4e32bccd..a12072f1 100755 --- a/install.sh +++ b/install.sh @@ -138,6 +138,8 @@ else uninstall_core echo "pulling latest code" git pull + echo "installing python dependencies" + install_python_depencencies echo "building CORE" case ${os} in "ubuntu") From 5d99244596180d651acb6f341686f6fc2b9a3a2c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 9 May 2020 21:50:16 -0700 Subject: [PATCH 0242/1131] removed docker service and associated documentation, was not functioning and will cause confusion with new support in the new GUI --- daemon/core/services/dockersvc.py | 176 ------------------------------ docs/services.md | 1 - docs/services/docker.md | 43 -------- 3 files changed, 220 deletions(-) delete mode 100644 daemon/core/services/dockersvc.py delete mode 100644 docs/services/docker.md diff --git a/daemon/core/services/dockersvc.py b/daemon/core/services/dockersvc.py deleted file mode 100644 index 2c815e04..00000000 --- a/daemon/core/services/dockersvc.py +++ /dev/null @@ -1,176 +0,0 @@ -""" -Docker service allows running docker containers within CORE nodes. - -The running of Docker within a CORE node allows for additional extensibility to -the CORE services. This allows network applications and protocols to be easily -packaged and run on any node. - -This service that will add a new group to the services list. This -will have a service called Docker which will just start the docker service -within the node but not run anything. It will also scan all docker images on -the host machine. If any are tagged with 'core' then they will be added as a -service to the Docker group. The image will then be auto run if that service is -selected. - -This requires a recent version of Docker. This was tested using a PPA on Ubuntu - with version 1.2.0. The version in the standard Ubuntu repo is to old for -this purpose (we need --net host). - -It also requires docker-py (https://pypi.python.org/pypi/docker-py) which can be -installed with 'pip install docker-py'. This is used to interface with Docker -from the python service. - -An example use case is to pull an image from Docker.com. A test image has been -uploaded for this purpose: - -sudo docker pull stuartmarsden/multicastping - -This downloads an image which is based on Ubuntu 14.04 with python and twisted. -It runs a simple program that sends a multicast ping and listens and records -any it receives. - -In order for this to appear as a docker service it must be tagged with core. -Find out the id by running 'sudo docker images'. You should see all installed -images and the one you want looks like this: - -stuartmarsden/multicastping latest 4833487e66d2 20 hours -ago 487 MB - -The id will be different on your machine so use it in the following command: - -sudo docker tag 4833487e66d2 stuartmarsden/multicastping:core - -This image will be listed in the services after we restart the core-daemon: - -sudo service core-daemon restart - -You can set up a simple network with a number of PCs connected to a switch. Set -the stuartmarsden/multicastping service for all the PCs. When started they will -all begin sending Multicast pings. - -In order to see what is happening you can go in to the terminal of a node and -look at the docker log. Easy shorthand is: - -docker logs $(docker ps -q) - -Which just shows the log of the running docker container (usually just one per -node). I have added this as an observer node to my setup: Name: docker logs -Command: bash -c 'docker logs $(docker ps -q) | tail -20' - -So I can just hover over to see the log which looks like this: - -Datagram 'Client: Ping' received from ('10.0.0.20', 8005) -Datagram 'Client: Ping' received from ('10.0.5.21', 8005) -Datagram 'Client: Ping' received from ('10.0.3.20', 8005) -Datagram 'Client: Ping' received from ('10.0.4.20', 8005) -Datagram 'Client: Ping' received from ('10.0.4.20', 8005) -Datagram 'Client: Ping' received from ('10.0.1.21', 8005) -Datagram 'Client: Ping' received from ('10.0.4.21', 8005) -Datagram 'Client: Ping' received from ('10.0.4.21', 8005) -Datagram 'Client: Ping' received from ('10.0.5.20', 8005) -Datagram 'Client: Ping' received from ('10.0.0.21', 8005) -Datagram 'Client: Ping' received from ('10.0.3.21', 8005) -Datagram 'Client: Ping' received from ('10.0.0.20', 8005) -Datagram 'Client: Ping' received from ('10.0.5.21', 8005) -Datagram 'Client: Ping' received from ('10.0.3.20', 8005) -Datagram 'Client: Ping' received from ('10.0.4.20', 8005) -Datagram 'Client: Ping' received from ('10.0.4.20', 8005) -Datagram 'Client: Ping' received from ('10.0.1.21', 8005) -Datagram 'Client: Ping' received from ('10.0.4.21', 8005) -Datagram 'Client: Ping' received from ('10.0.4.21', 8005) -Datagram 'Client: Ping' received from ('10.0.5.20', 8005) - -Limitations: - -1. Docker images must be downloaded on the host as usually a CORE node does not - have access to the internet. -2. Each node isolates running containers (keeps things simple) -3. Recent version of docker needed so that --net host can be used. This does - not further abstract the network within a node and allows multicast which - is not enabled within Docker containers at the moment. -4. The core-daemon must be restarted for new images to show up. -5. A Docker-daemon is run within each node but the images are shared. This - does mean that the daemon attempts to access an SQLlite database within the - host. At startup all the nodes will try to access this and it will be locked - for most due to contention. The service just does a hackish wait for 1 second - and retry. This means all the docker containers can take a while to come up - depending on how many nodes you have. -""" - -import logging - -from core.services.coreservices import CoreService, ServiceManager - -try: - from docker import Client -except ImportError: - logging.debug("missing python docker bindings") - - -class DockerService(CoreService): - """ - This is a service which will allow running docker containers in a CORE - node. - """ - name = "Docker" - executables = ("docker",) - group = "Docker" - dirs = ('/var/lib/docker/containers/', '/run/shm', '/run/resolvconf',) - configs = ('docker.sh',) - startup = ('sh docker.sh',) - shutdown = ('service docker stop',) - # Container image to start - image = "" - - @classmethod - def generate_config(cls, node, filename): - """ - Returns a string having contents of a docker.sh script that - can be modified to start a specific docker image. - """ - cfg = "#!/bin/sh\n" - cfg += "# auto-generated by Docker (docker.py)\n" - # Docker likes to think it has DNS set up or it complains. - # Unless your network was attached to the Internet this is - # non-functional but hides error messages. - cfg += 'echo "nameserver 8.8.8.8" > /run/resolvconf/resolv.conf\n' - # Starts the docker service. In Ubuntu this is docker.io; in other - # distros may just be docker - cfg += 'service docker start\n' - cfg += "# you could add a command to start a image here eg:\n" - if not cls.image: - cfg += "# docker run -d --net host --name coreDock \n" - else: - cfg += """\ -result=1 -until [ $result -eq 0 ]; do - docker run -d --net host --name coreDock %s - result=$? - # this is to alleviate contention to docker's SQLite database - sleep 0.3 -done -""" % (cls.image,) - return cfg - - @classmethod - def on_load(cls): - logging.debug("loading custom docker services") - - if "Client" in globals(): - client = Client(version="1.10") - images = client.images() - del client - else: - images = [] - - for image in images: - if u"" in image["RepoTags"][0]: - continue - for repo in image["RepoTags"]: - if u":core" not in repo: - continue - dockerid = repo.encode("ascii", "ignore").split(":")[0] - sub_class = type("SubClass", (DockerService,), {"_name": dockerid, "_image": dockerid}) - ServiceManager.add(sub_class) - - del images diff --git a/docs/services.md b/docs/services.md index e70a0c75..d2911d81 100644 --- a/docs/services.md +++ b/docs/services.md @@ -24,7 +24,6 @@ shutdown commands, and meta-data associated with a node. | Service Group | Services | |---|---| |[BIRD](services/bird.md)|BGP, OSPF, RADV, RIP, Static| -|[Docker](services/docker.md)|Docker| |[EMANE](services/emane.md)|Transport Service| |[FRR](services/frr.md)|BABEL, BGP, OSPFv2, OSPFv3, PIMD, RIP, RIPNG, Zebra| |[NRL](services/nrl.md)|arouted, MGEN Sink, MGEN Actor, NHDP, OLSR, OLSRORG, OLSRv2, SMF| diff --git a/docs/services/docker.md b/docs/services/docker.md deleted file mode 100644 index 5757de1f..00000000 --- a/docs/services/docker.md +++ /dev/null @@ -1,43 +0,0 @@ -# Docker - -* Table of Contents -{:toc} - -## Overview - -Docker service allows running docker containers within CORE nodes. -The running of Docker within a CORE node allows for additional extensibility to -the CORE services. This allows network applications and protocols to be easily -packaged and run on any node. - -This service will add a new group to the services list. This will have a service called Docker which will just start the docker service within the node but not run anything. It will also scan all docker images on the host machine. If any are tagged with 'core' then they will be added as a service to the Docker group. The image will then be auto run if that service is selected. - -This requires a recent version of Docker. This was tested using a PPA on Ubuntu with version 1.2.0. The version in the standard Ubuntu repo is to old for this purpose (we need --net host). - -## Docker Installation - -To use Docker services, you must first install the Docker python image. This is used to interface with Docker from the python service. - -```shell -sudo apt-get install docker.io -sudo apt-get install python-pip -pip install docker-py -``` -Once everything runs successfully, a Docker group under services will appear. An example use case is to pull an image from [Docker](https://hub.docker.com/). A test image has been uploaded for this purpose: -```shell -sudo docker pull stuartmarsden/multicastping -``` -This downloads an image which is based on Ubuntu 14.04 with python and twisted. It runs a simple program that sends a multicast ping and listens and records any it receives. In order for this to appear as a docker service it must be tagged with core. -Find out the id by running 'sudo docker images'. You should see all installed images and the one you want looks like this: -```shell -stuartmarsden/multicastping latest 4833487e66d2 20 hours -ago 487 MB -``` -The id will be different on your machine so use it in the following command: -```shell -sudo docker tag 4833487e66d2 stuartmarsden/multicastping:core -``` -This image will be listed in the services after we restart the core-daemon: -```shell -sudo service core-daemon restart -``` From f77f37ef86957e77a858a56ea7626680bbd912b1 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 May 2020 07:59:13 -0700 Subject: [PATCH 0243/1131] bumped version for next release --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 588f3f34..90b731a9 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, 6.3.0) +AC_INIT(core, 6.4.0) # autoconf and automake initialization AC_CONFIG_SRCDIR([netns/version.h.in]) From a36674aba99172638ccb136145926111d9b2142b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 May 2020 08:33:56 -0700 Subject: [PATCH 0244/1131] pygui: adjustment to compensate for ip4/ip6 address not being present on joined links --- daemon/core/gui/interface.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index 0ff018c7..9df1f667 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -147,8 +147,13 @@ class InterfaceManager: return str(ip4), str(ip6) def get_subnets(self, interface: "core_pb2.Interface") -> Subnets: - ip4_subnet = IPNetwork(f"{interface.ip4}/{interface.ip4mask}").cidr - ip6_subnet = IPNetwork(f"{interface.ip6}/{interface.ip6mask}").cidr + logging.info("get subnets for interface: %s", interface) + ip4_subnet = self.ip4_subnets + if interface.ip4: + ip4_subnet = IPNetwork(f"{interface.ip4}/{interface.ip4mask}").cidr + ip6_subnet = self.ip6_subnets + if interface.ip6: + ip6_subnet = IPNetwork(f"{interface.ip6}/{interface.ip6mask}").cidr subnets = Subnets(ip4_subnet, ip6_subnet) return self.used_subnets.get(subnets.key(), subnets) From 88a98fff820d8d727862154cb07f7d91b2b4f988 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 May 2020 12:26:05 -0700 Subject: [PATCH 0245/1131] docs: added initial documentation for the python beta gui based on prior gui documentation --- daemon/scripts/core-pygui | 2 +- docs/index.md | 1 + docs/pygui.md | 653 ++++++++++++++++++++++ docs/static/core-pygui.png | Bin 0 -> 652318 bytes docs/static/pygui/OVS.gif | Bin 0 -> 744 bytes docs/static/pygui/alert.png | Bin 0 -> 2019 bytes docs/static/pygui/antenna.gif | Bin 0 -> 230 bytes docs/static/pygui/cancel.png | Bin 0 -> 1322 bytes docs/static/pygui/core-icon.png | Bin 0 -> 2931 bytes docs/static/pygui/delete.png | Bin 0 -> 387 bytes docs/static/pygui/docker.png | Bin 0 -> 1533 bytes docs/static/pygui/document-new.gif | Bin 0 -> 1054 bytes docs/static/pygui/document-properties.gif | Bin 0 -> 635 bytes docs/static/pygui/document-save.gif | Bin 0 -> 1049 bytes docs/static/pygui/edit-delete.gif | Bin 0 -> 1006 bytes docs/static/pygui/edit-node.png | Bin 0 -> 3050 bytes docs/static/pygui/emane.png | Bin 0 -> 3334 bytes docs/static/pygui/error.png | Bin 0 -> 2258 bytes docs/static/pygui/fileopen.gif | Bin 0 -> 1095 bytes docs/static/pygui/host.png | Bin 0 -> 642 bytes docs/static/pygui/hub.png | Bin 0 -> 2242 bytes docs/static/pygui/lanswitch.png | Bin 0 -> 1138 bytes docs/static/pygui/link.png | Bin 0 -> 1692 bytes docs/static/pygui/lxc.png | Bin 0 -> 1553 bytes docs/static/pygui/marker.png | Bin 0 -> 1211 bytes docs/static/pygui/markerclear.png | Bin 0 -> 1370 bytes docs/static/pygui/mdr.png | Bin 0 -> 2786 bytes docs/static/pygui/observe.gif | Bin 0 -> 1149 bytes docs/static/pygui/oval.png | Bin 0 -> 2407 bytes docs/static/pygui/pause.png | Bin 0 -> 2368 bytes docs/static/pygui/pc.png | Bin 0 -> 828 bytes docs/static/pygui/plot.gif | Bin 0 -> 265 bytes docs/static/pygui/prouter.png | Bin 0 -> 2590 bytes docs/static/pygui/rectangle.png | Bin 0 -> 259 bytes docs/static/pygui/rj45.png | Bin 0 -> 1121 bytes docs/static/pygui/router.png | Bin 0 -> 2082 bytes docs/static/pygui/run.png | Bin 0 -> 1805 bytes docs/static/pygui/select.png | Bin 0 -> 1038 bytes docs/static/pygui/shutdown.png | Bin 0 -> 1546 bytes docs/static/pygui/start.png | Bin 0 -> 3010 bytes docs/static/pygui/stop.png | Bin 0 -> 2305 bytes docs/static/pygui/text.png | Bin 0 -> 314 bytes docs/static/pygui/tunnel.png | Bin 0 -> 2256 bytes docs/static/pygui/twonode.png | Bin 0 -> 2494 bytes docs/static/pygui/wlan.png | Bin 0 -> 3457 bytes 45 files changed, 655 insertions(+), 1 deletion(-) create mode 100644 docs/pygui.md create mode 100644 docs/static/core-pygui.png create mode 100755 docs/static/pygui/OVS.gif create mode 100644 docs/static/pygui/alert.png create mode 100644 docs/static/pygui/antenna.gif create mode 100644 docs/static/pygui/cancel.png create mode 100644 docs/static/pygui/core-icon.png create mode 100644 docs/static/pygui/delete.png create mode 100644 docs/static/pygui/docker.png create mode 100644 docs/static/pygui/document-new.gif create mode 100644 docs/static/pygui/document-properties.gif create mode 100644 docs/static/pygui/document-save.gif create mode 100644 docs/static/pygui/edit-delete.gif create mode 100644 docs/static/pygui/edit-node.png create mode 100644 docs/static/pygui/emane.png create mode 100644 docs/static/pygui/error.png create mode 100644 docs/static/pygui/fileopen.gif create mode 100644 docs/static/pygui/host.png create mode 100644 docs/static/pygui/hub.png create mode 100644 docs/static/pygui/lanswitch.png create mode 100644 docs/static/pygui/link.png create mode 100644 docs/static/pygui/lxc.png create mode 100644 docs/static/pygui/marker.png create mode 100644 docs/static/pygui/markerclear.png create mode 100644 docs/static/pygui/mdr.png create mode 100644 docs/static/pygui/observe.gif create mode 100644 docs/static/pygui/oval.png create mode 100644 docs/static/pygui/pause.png create mode 100644 docs/static/pygui/pc.png create mode 100644 docs/static/pygui/plot.gif create mode 100644 docs/static/pygui/prouter.png create mode 100644 docs/static/pygui/rectangle.png create mode 100644 docs/static/pygui/rj45.png create mode 100644 docs/static/pygui/router.png create mode 100644 docs/static/pygui/run.png create mode 100644 docs/static/pygui/select.png create mode 100644 docs/static/pygui/shutdown.png create mode 100644 docs/static/pygui/start.png create mode 100644 docs/static/pygui/stop.png create mode 100644 docs/static/pygui/text.png create mode 100644 docs/static/pygui/tunnel.png create mode 100644 docs/static/pygui/twonode.png create mode 100644 docs/static/pygui/wlan.png diff --git a/daemon/scripts/core-pygui b/daemon/scripts/core-pygui index 9723297b..f30b531b 100755 --- a/daemon/scripts/core-pygui +++ b/daemon/scripts/core-pygui @@ -9,7 +9,7 @@ from core.gui.images import Images if __name__ == "__main__": # parse flags - parser = argparse.ArgumentParser(description=f"CORE Python Tk GUI") + parser = argparse.ArgumentParser(description=f"CORE Python GUI") parser.add_argument("-l", "--level", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], default="INFO", help="logging level") parser.add_argument("-p", "--proxy", action="store_true", help="enable proxy") diff --git a/docs/index.md b/docs/index.md index 12932880..4e215916 100644 --- a/docs/index.md +++ b/docs/index.md @@ -23,6 +23,7 @@ networking scenarios, security studies, and increasing the size of physical test |[Architecture](architecture.md)|Overview of the architecture| |[Installation](install.md)|How to install CORE and its requirements| |[GUI](gui.md)|How to use the GUI| +|[(BETA) Python GUI](gui.md)|How to use the BETA python based GUI| |[Distributed](distributed.md)|Details for running CORE across multiple servers| |[Python Scripting](scripting.md)|How to write python scripts for creating a CORE session| |[gRPC API](grpc.md)|How to enable and use the gRPC API| diff --git a/docs/pygui.md b/docs/pygui.md new file mode 100644 index 00000000..4ed3fe09 --- /dev/null +++ b/docs/pygui.md @@ -0,0 +1,653 @@ + +# (BETA) Python GUI + +* Table of Contents +{:toc} + +![](static/core-pygui.png) + +## Overview + +The GUI is used to draw nodes and network devices on a canvas, linking them +together to create an emulated network session. + +After pressing the start button, CORE will proceed through these phases, +staying in the **runtime** phase. After the session is stopped, CORE will +proceed to the **data collection** phase before tearing down the emulated +state. + +CORE can be customized to perform any action at each state. See the +**Hooks...** entry on the [Session Menu](#session-menu) for details about +when these session states are reached. + +## Prerequisites + +Beyond installing CORE, you must have the CORE daemon running. This is done +on the command line with either systemd or sysv. + +```shell +# systemd service +sudo systemctl daemon-reload +sudo systemctl start core-daemon + +# sysv service +sudo service core-daemon start + +# direct invocation +sudo core-daemon +``` + +## GUI Files + +> **NOTE:** Previously the BETA GUI placed files under ~/.coretk, this has been +> updated to be ~/.coregui. The prior config file named ~/.coretk/gui.yaml is +> also now known as ~/.coregui/config.yaml and has a slightly different format + +The GUI will create a directory in your home directory on first run called +~/.coregui. This directory will help layout various files that the GUI may use. + +* .coregui/ + * backgrounds/ + * place backgrounds used for display in the GUI + * custom_emane/ + * place to keep custom emane models to use with the core-daemon + * custom_services/ + * place to keep custom services to use with the core-daemon + * icons/ + * icons the GUI uses along with customs icons desired + * mobility/ + * place to keep custom mobility files + * scripts/ + * place to keep core related scripts + * xmls/ + * place to keep saved session xml files + * gui.log + * log file when running the gui, look here when issues occur for exceptions etc + * config.yaml + * configuration file used to save/load various gui related settings (custom nodes, layouts, addresses, etc) + +## Modes of Operation + +The CORE GUI has two primary modes of operation, **Edit** and **Execute** +modes. Running the GUI, by typing **core-pygui** with no options, starts in +Edit mode. Nodes are drawn on a blank canvas using the toolbar on the left +and configured from right-click menus or by double-clicking them. The GUI +does not need to be run as root. + +Once editing is complete, pressing the green **Start** button instantiates +the topology and enters Execute mode. In execute mode, +the user can interact with the running emulated machines by double-clicking or +right-clicking on them. The editing toolbar disappears and is replaced by an +execute toolbar, which provides tools while running the emulation. Pressing +the red **Stop** button will destroy the running emulation and return CORE +to Edit mode. + +Once the emulation is running, the GUI can be closed, and a prompt will appear +asking if the emulation should be terminated. The emulation may be left +running and the GUI can reconnect to an existing session at a later time. + +The GUI can be run as a normal user on Linux. + +The python GUI currently provides the following options on startup. + +```shell +usage: core-pygui [-h] [-l {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [-p] + +CORE Python GUI + +optional arguments: + -h, --help show this help message and exit + -l {DEBUG,INFO,WARNING,ERROR,CRITICAL}, --level {DEBUG,INFO,WARNING,ERROR,CRITICAL} + logging level + -p, --proxy enable proxy +``` + +## Toolbar + +The toolbar is a row of buttons that runs vertically along the left side of the +CORE GUI window. The toolbar changes depending on the mode of operation. + +### Editing Toolbar + +When CORE is in Edit mode (the default), the vertical Editing Toolbar exists on +the left side of the CORE window. Below are brief descriptions for each toolbar +item, starting from the top. Most of the tools are grouped into related +sub-menus, which appear when you click on their group icon. + +| Icon | Name | Description | +|---|---|---| +| ![](static/pygui/select.png) | Selection Tool | Tool for selecting, moving, configuring nodes. | +| ![](static/pygui/start.png) | Start Button | Starts Execute mode, instantiates the emulation. | +| ![](static/pygui/link.png) | Link | Allows network links to be drawn between two nodes by clicking and dragging the mouse. | + +### CORE Nodes + +These nodes will create a new node container and run associated services. + +| Icon | Name | Description | +|---|---|---| +| ![](static/pygui/router.png) | Router | Runs Quagga OSPFv2 and OSPFv3 routing to forward packets. | +| ![](static/pygui/host.png) | Host | Emulated server machine having a default route, runs SSH server. | +| ![](static/pygui/pc.png) | PC | Basic emulated machine having a default route, runs no processes by default. | +| ![](static/pygui/mdr.png) | MDR | Runs Quagga OSPFv3 MDR routing for MANET-optimized routing. | +| ![](static/pygui/router.png) | PRouter | Physical router represents a real testbed machine. | + +### Network Nodes + +These nodes are mostly used to create a Linux bridge that serves the +purpose described below. + +| Icon | Name | Description | +|---|---|---| +| ![](static/pygui/hub.png) | Hub | Ethernet hub forwards incoming packets to every connected node. | +| ![](static/pygui/lanswitch.png) | Switch | Ethernet switch intelligently forwards incoming packets to attached hosts using an Ethernet address hash table. | +| ![](static/pygui/wlan.png) | Wireless LAN | When routers are connected to this WLAN node, they join a wireless network and an antenna is drawn instead of a connecting line; the WLAN node typically controls connectivity between attached wireless nodes based on the distance between them. | +| ![](static/pygui/rj45.png) | RJ45 | RJ45 Physical Interface Tool, emulated nodes can be linked to real physical interfaces; using this tool, real networks and devices can be physically connected to the live-running emulation. | +| ![](static/pygui/tunnel.png) | Tunnel | Tool allows connecting together more than one CORE emulation using GRE tunnels. | + +### Annotation Tools + +| Icon | Name | Description | +|---|---|---| +| ![](static/pygui/marker.png) | Marker | For drawing marks on the canvas. | +| ![](static/pygui/oval.png) | Oval | For drawing circles on the canvas that appear in the background. | +| ![](static/pygui/rectangle.png) | Rectangle | For drawing rectangles on the canvas that appear in the background. | +| ![](static/pygui/text.png) | Text | For placing text captions on the canvas. | + +### Execution Toolbar + +When the Start button is pressed, CORE switches to Execute mode, and the Edit +toolbar on the left of the CORE window is replaced with the Execution toolbar +Below are the items on this toolbar, starting from the top. + +| Icon | Name | Description | +|---|---|---| +| ![](static/pygui/stop.png) | Stop Button | Stops Execute mode, terminates the emulation, returns CORE to edit mode. | +| ![](static/pygui/select.png) | Selection Tool | In Execute mode, the Selection Tool can be used for moving nodes around the canvas, and double-clicking on a node will open a shell window for that node; right-clicking on a node invokes a pop-up menu of run-time options for that node. | +| ![](static/pygui/marker.png) | Marker | For drawing freehand lines on the canvas, useful during demonstrations; markings are not saved. | +| ![](static/pygui/run.png) | Run Tool | This tool allows easily running a command on all or a subset of all nodes. A list box allows selecting any of the nodes. A text entry box allows entering any command. The command should return immediately, otherwise the display will block awaiting response. The *ping* command, for example, with no parameters, is not a good idea. The result of each command is displayed in a results box. The first occurrence of the special text "NODE" will be replaced with the node name. The command will not be attempted to run on nodes that are not routers, PCs, or hosts, even if they are selected. | + +## Menu + +The menubar runs along the top of the CORE GUI window and provides access to a +variety of features. Some of the menus are detachable, such as the *Widgets* +menu, by clicking the dashed line at the top. + +### File Menu + +The File menu contains options for manipulating the **.imn** Configuration +Files. Generally, these menu items should not be used in Execute mode. + +| Option | Description | +|---|---| +| New Session | This starts a new session with an empty canvas. | +| Save | Saves the current topology. If you have not yet specified a file name, the Save As dialog box is invoked. | +| Save As | Invokes the Save As dialog box for selecting a new **.xml** file for saving the current configuration in the XML file. | +| Open | Invokes the File Open dialog box for selecting a new XML file to open. | +| Recently used files | Above the Quit menu command is a list of recently use files, if any have been opened. You can clear this list in the Preferences dialog box. You can specify the number of files to keep in this list from the Preferences dialog. Click on one of the file names listed to open that configuration file. | +| Execute Python Script | Invokes a File Open dialog box for selecting a Python script to run and automatically connect to. After a selection is made, a Python Script Options dialog box is invoked to allow for command-line options to be added. The Python script must create a new CORE Session and add this session to the daemon's list of sessions in order for this to work. | +| Quit | The Quit command should be used to exit the CORE GUI. CORE may prompt for termination if you are currently in Execute mode. Preferences and the recently-used files list are saved. | + +### Edit Menu + +| Option | Description | +|---|---| +| Preferences | Invokes the Preferences dialog box. | +| Custom Nodes | Custom node creation dialog box. | +| Undo | (Disabled) Attempts to undo the last edit in edit mode. | +| Redo | (Disabled) Attempts to redo an edit that has been undone. | +| Cut, Copy, Paste, Delete | Used to cut, copy, paste, and delete a selection. When nodes are pasted, their node numbers are automatically incremented, and existing links are preserved with new IP addresses assigned. Services and their customizations are copied to the new node, but care should be taken as node IP addresses have changed with possibly old addresses remaining in any custom service configurations. Annotations may also be copied and pasted. + +### Canvas Menu + +The canvas menu provides commands related to the editing canvas. + +| Option | Description | +|---|---| +| Size/scale | Invokes a Canvas Size and Scale dialog that allows configuring the canvas size, scale, and geographic reference point. The size controls allow changing the width and height of the current canvas, in pixels or meters. The scale allows specifying how many meters are equivalent to 100 pixels. The reference point controls specify the latitude, longitude, and altitude reference point used to convert between geographic and Cartesian coordinate systems. By clicking the *Save as default* option, all new canvases will be created with these properties. The default canvas size can also be changed in the Preferences dialog box. +| Wallpaper | Used for setting the canvas background image. | + +### View Menu + +The View menu features items for toggling on and off their display on the canvas. + +| Option | Description | +|---|---| +| Interface Names | Display interface names on links. | +| IPv4 Addresses | Display IPv4 addresses on links. | +| IPv6 Addresses | Display IPv6 addresses on links. | +| Node Labels | Display node names. | +| Link Labels | Display link labels. | +| Annotations | Display annotations. | +| Canvas Grid | Display the canvas grid. | + +### Tools Menu + +The tools menu lists different utility functions. + +| Option | Description | +|---|---| +| Find | Display find dialog used for highlighting a node on the canvas. | +| Auto Grid | Automatically layout nodes in a grid. | +| IP addresses | Invokes the IP Addresses dialog box for configuring which IPv4/IPv6 prefixes are used when automatically addressing new interfaces. | +| MAC addresses | Invokes the MAC Addresses dialog box for configuring the starting number used as the lowest byte when generating each interface MAC address. This value should be changed when tunneling between CORE emulations to prevent MAC address conflicts. | + +### Widgets Menu + +Widgets are GUI elements that allow interaction with a running emulation. +Widgets typically automate the running of commands on emulated nodes to report +status information of some type and display this on screen. + +#### Periodic Widgets + +These Widgets are those available from the main *Widgets* menu. More than one +of these Widgets may be run concurrently. An event loop fires once every second +that the emulation is running. If one of these Widgets is enabled, its periodic +routine will be invoked at this time. Each Widget may have a configuration +dialog box which is also accessible from the *Widgets* menu. + +Here are some standard widgets: + +* **Adjacency** - displays router adjacency states for Quagga's OSPFv2 and OSPFv3 + routing protocols. A line is drawn from each router halfway to the router ID + of an adjacent router. The color of the line is based on the OSPF adjacency + state such as Two-way or Full. To learn about the different colors, see the + *Configure Adjacency...* menu item. The **vtysh** command is used to + dump OSPF neighbor information. + Only half of the line is drawn because each + router may be in a different adjacency state with respect to the other. +* **Throughput** - displays the kilobits-per-second throughput above each link, + using statistics gathered from the ng_pipe Netgraph node that implements each + link. If the throughput exceeds a certain threshold, the link will become + highlighted. For wireless nodes which broadcast data to all nodes in range, + the throughput rate is displayed next to the node and the node will become + circled if the threshold is exceeded. + +#### Observer Widgets + +These Widgets are available from the **Observer Widgets** submenu of the +**Widgets** menu, and from the Widgets Tool on the toolbar. Only one Observer Widget may +be used at a time. Mouse over a node while the session is running to pop up +an informational display about that node. + +Available Observer Widgets include IPv4 and IPv6 routing tables, socket +information, list of running processes, and OSPFv2/v3 neighbor information. + +Observer Widgets may be edited by the user and rearranged. Choosing +**Widgets->Observer Widgets->Edit Observers** from the Observer Widget menu will +invoke the Observer Widgets dialog. A list of Observer Widgets is displayed along +with up and down arrows for rearranging the list. Controls are available for +renaming each widget, for changing the command that is run during mouse over, and +for adding and deleting items from the list. Note that specified commands should +return immediately to avoid delays in the GUI display. Changes are saved to a +**config.yaml** file in the CORE configuration directory. + +### Session Menu + +The Session Menu has entries for starting, stopping, and managing sessions, +in addition to global options such as node types, comments, hooks, servers, +and options. + +| Option | Description | +|---|---| +| Sessions | Invokes the CORE Sessions dialog box containing a list of active CORE sessions in the daemon. Basic session information such as name, node count, start time, and a thumbnail are displayed. This dialog allows connecting to different sessions, shutting them down, or starting a new session. | +| Servers | Invokes the CORE emulation servers dialog for configuring. | +| Options | Presents per-session options, such as the IPv4 prefix to be used, if any, for a control network the ability to preserve the session directory; and an on/off switch for SDT3D support. | +| Hooks | Invokes the CORE Session Hooks window where scripts may be configured for a particular session state. The session states are defined in the [table](#session-states) below. The top of the window has a list of configured hooks, and buttons on the bottom left allow adding, editing, and removing hook scripts. The new or edit button will open a hook script editing window. A hook script is a shell script invoked on the host (not within a virtual node). | + +#### Session States + +| State | Description | +|---|---| +| Definition | Used by the GUI to tell the backend to clear any state. | +| Configuration | When the user presses the *Start* button, node, link, and other configuration data is sent to the backend. This state is also reached when the user customizes a service. | +| Instantiation | After configuration data has been sent, just before the nodes are created. | +| Runtime | All nodes and networks have been built and are running. (This is the same state at which the previously-named *global experiment script* was run.) +| Datacollect | The user has pressed the *Stop* button, but before services have been stopped and nodes have been shut down. This is a good time to collect log files and other data from the nodes. | +| Shutdown | All nodes and networks have been shut down and destroyed. | + +### Help Menu + +| Option | Description | +|---|---| +| CORE Github (www) | Link to the CORE GitHub page. | +| CORE Documentation (www) | Lnk to the CORE Documentation page. | +| About | Invokes the About dialog box for viewing version information. | + +## Connecting with Physical Networks + +CORE's emulated networks run in real time, so they can be connected to live +physical networks. The RJ45 tool and the Tunnel tool help with connecting to +the real world. These tools are available from the **Link-layer nodes** menu. + +When connecting two or more CORE emulations together, MAC address collisions +should be avoided. CORE automatically assigns MAC addresses to interfaces when +the emulation is started, starting with **00:00:00:aa:00:00** and incrementing +the bottom byte. The starting byte should be changed on the second CORE machine +using the **Tools->MAC Addresses** option the menu. + +### RJ45 Tool + +The RJ45 node in CORE represents a physical interface on the real CORE machine. +Any real-world network device can be connected to the interface and communicate +with the CORE nodes in real time. + +The main drawback is that one physical interface is required for each +connection. When the physical interface is assigned to CORE, it may not be used +for anything else. Another consideration is that the computer or network that +you are connecting to must be co-located with the CORE machine. + +To place an RJ45 connection, click on the **Link-layer nodes** toolbar and select +the **RJ45 Tool** from the submenu. Click on the canvas near the node you want to +connect to. This could be a router, hub, switch, or WLAN, for example. Now +click on the *Link Tool* and draw a link between the RJ45 and the other node. +The RJ45 node will display "UNASSIGNED". Double-click the RJ45 node to assign a +physical interface. A list of available interfaces will be shown, and one may +be selected by double-clicking its name in the list, or an interface name may +be entered into the text box. + +> **NOTE:** When you press the Start button to instantiate your topology, the + interface assigned to the RJ45 will be connected to the CORE topology. The + interface can no longer be used by the system. For example, if there was an + IP address assigned to the physical interface before execution, the address + will be removed and control given over to CORE. No IP address is needed; the + interface is put into promiscuous mode so it will receive all packets and + send them into the emulated world. + +Multiple RJ45 nodes can be used within CORE and assigned to the same physical +interface if 802.1x VLANs are used. This allows for more RJ45 nodes than +physical ports are available, but the (e.g. switching) hardware connected to +the physical port must support the VLAN tagging, and the available bandwidth +will be shared. + +You need to create separate VLAN virtual devices on the Linux host, +and then assign these devices to RJ45 nodes inside of CORE. The VLANning is +actually performed outside of CORE, so when the CORE emulated node receives a +packet, the VLAN tag will already be removed. + +Here are example commands for creating VLAN devices under Linux: + +```shell +ip link add link eth0 name eth0.1 type vlan id 1 +ip link add link eth0 name eth0.2 type vlan id 2 +ip link add link eth0 name eth0.3 type vlan id 3 +``` + +### Tunnel Tool + +The tunnel tool builds GRE tunnels between CORE emulations or other hosts. +Tunneling can be helpful when the number of physical interfaces is limited or +when the peer is located on a different network. Also a physical interface does +not need to be dedicated to CORE as with the RJ45 tool. + +The peer GRE tunnel endpoint may be another CORE machine or another +host that supports GRE tunneling. When placing a Tunnel node, initially +the node will display "UNASSIGNED". This text should be replaced with the IP +address of the tunnel peer. This is the IP address of the other CORE machine or +physical machine, not an IP address of another virtual node. + +> **NOTE:** Be aware of possible MTU (Maximum Transmission Unit) issues with GRE devices. The *gretap* device + has an interface MTU of 1,458 bytes; when joined to a Linux bridge, the + bridge's MTU + becomes 1,458 bytes. The Linux bridge will not perform fragmentation for + large packets if other bridge ports have a higher MTU such as 1,500 bytes. + +The GRE key is used to identify flows with GRE tunneling. This allows multiple +GRE tunnels to exist between that same pair of tunnel peers. A unique number +should be used when multiple tunnels are used with the same peer. When +configuring the peer side of the tunnel, ensure that the matching keys are +used. + +Here are example commands for building the other end of a tunnel on a Linux +machine. In this example, a router in CORE has the virtual address +**10.0.0.1/24** and the CORE host machine has the (real) address +**198.51.100.34/24**. The Linux box +that will connect with the CORE machine is reachable over the (real) network +at **198.51.100.76/24**. +The emulated router is linked with the Tunnel Node. In the +Tunnel Node configuration dialog, the address **198.51.100.76** is entered, with +the key set to **1**. The gretap interface on the Linux box will be assigned +an address from the subnet of the virtual router node, +**10.0.0.2/24**. + +```shell +# these commands are run on the tunnel peer +sudo ip link add gt0 type gretap remote 198.51.100.34 local 198.51.100.76 key 1 +sudo ip addr add 10.0.0.2/24 dev gt0 +sudo ip link set dev gt0 up +``` + +Now the virtual router should be able to ping the Linux machine: + +```shell +# from the CORE router node +ping 10.0.0.2 +``` + +And the Linux machine should be able to ping inside the CORE emulation: + +```shell +# from the tunnel peer +ping 10.0.0.1 +``` + +To debug this configuration, **tcpdump** can be run on the gretap devices, or +on the physical interfaces on the CORE or Linux machines. Make sure that a +firewall is not blocking the GRE traffic. + +### Communicating with the Host Machine + +The host machine that runs the CORE GUI and/or daemon is not necessarily +accessible from a node. Running an X11 application on a node, for example, +requires some channel of communication for the application to connect with +the X server for graphical display. There are several different ways to +connect from the node to the host and vice versa. + +#### Control Network + +The quickest way to connect with the host machine through the primary control +network. + +With a control network, the host can launch an X11 application on a node. +To run an X11 application on the node, the **SSH** service can be enabled on +the node, and SSH with X11 forwarding can be used from the host to the node. + +```shell +# SSH from host to node n5 to run an X11 app +ssh -X 172.16.0.5 xclock +``` + +Note that the **coresendmsg** utility can be used for a node to send +messages to the CORE daemon running on the host (if the **listenaddr = 0.0.0.0** +is set in the **/etc/core/core.conf** file) to interact with the running +emulation. For example, a node may move itself or other nodes, or change +its icon based on some node state. + +#### Other Methods + +There are still other ways to connect a host with a node. The RJ45 Tool +can be used in conjunction with a dummy interface to access a node: + +```shell +sudo modprobe dummy numdummies=1 +``` + +A **dummy0** interface should appear on the host. Use the RJ45 tool assigned +to **dummy0**, and link this to a node in your scenario. After starting the +session, configure an address on the host. + +```shell +sudo ip link show type bridge +# determine bridge name from the above command +# assign an IP address on the same network as the linked node +sudo ip addr add 10.0.1.2/24 dev b.48304.34658 +``` + +In the example shown above, the host will have the address **10.0.1.2** and +the node linked to the RJ45 may have the address **10.0.1.1**. + +## Building Sample Networks + +### Wired Networks + +Wired networks are created using the **Link Tool** to draw a link between two +nodes. This automatically draws a red line representing an Ethernet link and +creates new interfaces on network-layer nodes. + +Double-click on the link to invoke the **link configuration** dialog box. Here +you can change the Bandwidth, Delay, Loss, and Duplicate +rate parameters for that link. You can also modify the color and width of the +link, affecting its display. + +Link-layer nodes are provided for modeling wired networks. These do not create +a separate network stack when instantiated, but are implemented using Linux bridging. +These are the hub, switch, and wireless LAN nodes. The hub copies each packet from +the incoming link to every connected link, while the switch behaves more like an +Ethernet switch and keeps track of the Ethernet address of the connected peer, +forwarding unicast traffic only to the appropriate ports. + +The wireless LAN (WLAN) is covered in the next section. + +### Wireless Networks + +The wireless LAN node allows you to build wireless networks where moving nodes +around affects the connectivity between them. Connection between a pair of nodes is stronger +when the nodes are closer while connection is weaker when the nodes are further away. +The wireless LAN, or WLAN, node appears as a small cloud. The WLAN offers +several levels of wireless emulation fidelity, depending on your modeling needs. + +The WLAN tool can be extended with plug-ins for different levels of wireless +fidelity. The basic on/off range is the default setting available on all +platforms. Other plug-ins offer higher fidelity at the expense of greater +complexity and CPU usage. The availability of certain plug-ins varies depending +on platform. See the table below for a brief overview of wireless model types. + + +|Model|Type|Supported Platform(s)|Fidelity|Description| +|-----|----|---------------------|--------|-----------| +|Basic|on/off|Linux|Low|Ethernet bridging with ebtables| +|EMANE|Plug-in|Linux|High|TAP device connected to EMANE emulator with pluggable MAC and PHY radio types| + +To quickly build a wireless network, you can first place several router nodes +onto the canvas. If you have the +Quagga MDR software installed, it is +recommended that you use the **mdr** node type for reduced routing overhead. Next +choose the **WLAN** from the **Link-layer nodes** submenu. First set the +desired WLAN parameters by double-clicking the cloud icon. Then you can link +all selected right-clicking on the WLAN and choosing **Link to Selected**. + +Linking a router to the WLAN causes a small antenna to appear, but no red link +line is drawn. Routers can have multiple wireless links and both wireless and +wired links (however, you will need to manually configure route +redistribution.) The mdr node type will generate a routing configuration that +enables OSPFv3 with MANET extensions. This is a Boeing-developed extension to +Quagga's OSPFv3 that reduces flooding overhead and optimizes the flooding +procedure for mobile ad-hoc (MANET) networks. + +The default configuration of the WLAN is set to use the basic range model. Having this model +selected causes **core-daemon** to calculate the distance between nodes based +on screen pixels. A numeric range in screen pixels is set for the wireless +network using the **Range** slider. When two wireless nodes are within range of +each other, a green line is drawn between them and they are linked. Two +wireless nodes that are farther than the range pixels apart are not linked. +During Execute mode, users may move wireless nodes around by clicking and +dragging them, and wireless links will be dynamically made or broken. + +The **EMANE Nodes** leverage available EMANE models to use for wireless networking. +See the [EMANE](emane.md) chapter for details on using EMANE. + +### Mobility Scripting + +CORE has a few ways to script mobility. + +| Option | Description | +|---|---| +| ns-2 script | The script specifies either absolute positions or waypoints with a velocity. Locations are given with Cartesian coordinates. | +| CORE API | An external entity can move nodes by sending CORE API Node messages with updated X,Y coordinates; the **coresendmsg** utility allows a shell script to generate these messages. | +| EMANE events | See [EMANE](emane.md) for details on using EMANE scripts to move nodes around. Location information is typically given as latitude, longitude, and altitude. | + +For the first method, you can create a mobility script using a text +editor, or using a tool such as [BonnMotion](http://net.cs.uni-bonn.de/wg/cs/applications/bonnmotion/), and associate the script with one of the wireless +using the WLAN configuration dialog box. Click the *ns-2 mobility script...* +button, and set the *mobility script file* field in the resulting *ns2script* +configuration dialog. + +Here is an example for creating a BonnMotion script for 10 nodes: + +```shell +bm -f sample RandomWaypoint -n 10 -d 60 -x 1000 -y 750 +bm NSFile -f sample +# use the resulting 'sample.ns_movements' file in CORE +``` + +When the Execute mode is started and one of the WLAN nodes has a mobility +script, a mobility script window will appear. This window contains controls for +starting, stopping, and resetting the running time for the mobility script. The +**loop** checkbox causes the script to play continuously. The **resolution** text +box contains the number of milliseconds between each timer event; lower values +cause the mobility to appear smoother but consumes greater CPU time. + +The format of an ns-2 mobility script looks like: + +```shell +# nodes: 3, max time: 35.000000, max x: 600.00, max y: 600.00 +$node_(2) set X_ 144.0 +$node_(2) set Y_ 240.0 +$node_(2) set Z_ 0.00 +$ns_ at 1.00 "$node_(2) setdest 130.0 280.0 15.0" +``` + +The first three lines set an initial position for node 2. The last line in the +above example causes node 2 to move towards the destination **(130, 280)** at +speed **15**. All units are screen coordinates, with speed in units per second. +The total script time is learned after all nodes have reached their waypoints. +Initially, the time slider in the mobility script dialog will not be +accurate. + +Examples mobility scripts (and their associated topology files) can be found +in the **configs/** directory. + +## Alerts + +The alerts button is located in the bottom right-hand corner +of the status bar in the CORE GUI. This will change colors to indicate one or +more problems with the running emulation. Clicking on the alerts button will invoke the +alerts dialog. + +The alerts dialog contains a list of alerts received from +the CORE daemon. An alert has a time, severity level, optional node number, +and source. When the alerts button is red, this indicates one or more fatal +exceptions. An alert with a fatal severity level indicates that one or more +of the basic pieces of emulation could not be created, such as failure to +create a bridge or namespace, or the failure to launch EMANE processes for an +EMANE-based network. + +Clicking on an alert displays details for that +exceptio. The exception source is a text string +to help trace where the exception occurred; "service:UserDefined" for example, +would appear for a failed validation command with the UserDefined service. + +A button is available at the bottom of the dialog for clearing the exception +list. + +## Customizing your Topology's Look + +Several annotation tools are provided for changing the way your topology is +presented. Captions may be added with the Text tool. Ovals and rectangles may +be drawn in the background, helpful for visually grouping nodes together. + +During live demonstrations the marker tool may be helpful for drawing temporary +annotations on the canvas that may be quickly erased. A size and color palette +appears at the bottom of the toolbar when the marker tool is selected. Markings +are only temporary and are not saved in the topology file. + +The basic node icons can be replaced with a custom image of your choice. Icons +appear best when they use the GIF or PNG format with a transparent background. +To change a node's icon, double-click the node to invoke its configuration +dialog and click on the button to the right of the node name that shows the +node's current icon. + +A background image for the canvas may be set using the *Wallpaper...* option +from the *Canvas* menu. The image may be centered, tiled, or scaled to fit the +canvas size. An existing terrain, map, or network diagram could be used as a +background, for example, with CORE nodes drawn on top. diff --git a/docs/static/core-pygui.png b/docs/static/core-pygui.png new file mode 100644 index 0000000000000000000000000000000000000000..6d0fbd40cac02bef07abbabdd9c8aa6106dbef60 GIT binary patch literal 652318 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°??t*p1B~b+1mBnw3m#-N|it-paal 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 z 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ν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 !W+ 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%0Dsy((Pabtcg~UaQ<&Z1(Ckw57naXytHEPOL@$8s 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 literal 0 HcmV?d00001 diff --git a/docs/static/pygui/OVS.gif b/docs/static/pygui/OVS.gif new file mode 100755 index 0000000000000000000000000000000000000000..38fcbb2ea684f2cb3724f55bb668d66dd952ee4d GIT binary patch literal 744 zcmVJ9wZvc%wRbpgMV>J9(izdZ9gfp+0+}KYOA*e5OBqqdk75L4Bk` zeWOBtq(FhHMS!M3gR4h^r$~aQNrI?Dg|0+}u0w{dOogdTg{n`7s!xckMvAgji>+0Q zu1SuzRgA7lkGD#Xw@Q$=SB#%r9qZk@Ytp1W|LyjZ2mSf$H#qQ6_I&0DF?cci~~ zrNDcq!F#8|eW=5Ks>Fe;#fPuQh_T6uvdNFN%aggzn!VDVz|)|@)uF=GrpDN($JnUI z*{jOhtjpW3%-prm-?-G_xzypg)Z)9<;=R}7%*@Qp%*@Qp%*@QpA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=dv*jE#jucwYQHYIwy=OOkz4Hj~p>W&4i3Z zU|?EVaBDeOZhEkLdR2@d7~9=~8y-G!bar@qcWq-WUF1-iLJ&x!2p8np`UC026(iCh zJzDS}A-9DYGIZ!5v5|<6A4!l-8|AaEc70|*i*d`R)429F;{oItU%<%@ul z7Q3Zf+45z~nKf_b+}ZP|Np6FNvUCYkCQX|-b@KEHbQ>Y5MXMskx^$~ki(gevHEK0! zR;ET=O%?)6_2^ZlTAP-%V2Cc;v3TMBba2S8*t>7d8bD;&uG_Lr69G_J06~HV5GGUz aLDL40AWED>vBKp`qSL5Tt6ohg5CA)88DCBS literal 0 HcmV?d00001 diff --git a/docs/static/pygui/alert.png b/docs/static/pygui/alert.png new file mode 100644 index 0000000000000000000000000000000000000000..718fa9f1f2794dc3165f2cf71db560247721c52b GIT binary patch literal 2019 zcmV<92ORi`P)!Qp@lUfU%Db%W|ox?Bz|gZ z*$7A@n3z;FF-GY6FJY@OxU>;W{P&Sqq-~ZzOZ;Hshq}xxsKHjFsiN6x)MAUWrGc_2 zyVK)`ohj?ieQ)l&?@V`gdw_kR z|GUf&^2iDv0b1gIgwMZqfsyke2s+3p%KeKj?a)EK7AjAIj3~1WcsxPkm{j;us6m~K z!}(`#A>1kzp3EvUa{zP`S5o9K(3>K4)deBN`3+7XY?KOr%_29`6r=-m)5U($oDZvR zI`VKnz{MyB^ja>+B0JkV;6lEM@*%Ib3qcsoB)D>Er8Jh^_aTw^% zqF^OYM;V>pU>+EdD!=n->@@-ugc{!^tMeB+ugK{r(D?ve!1vVi;r5$?O56q96kyB( zD=Y{(oZp~CnNRpMY#Al}C~J6~qbQf>FsPxe#8wtkFZ}1n2^vfjkNe$9Rz;W)t+WF9N zKT+G{e1NN{aa&r0^Z*E-F*WD^c~=`{XG2>oEc3A=?Pg*CbdamTbJ3L*yi z;WEAeOteY*Cf&sPq?^ejK-o^VK|Tb|>$=5mdY3cmrq z(+1J&xsms|Zny&=e8k)Y;YHrYH_YW)Q)Cx#++1#fo86aPsJn44ls)n@UN@I(j7)MG zWskXFWyqt*0#L-jMP<;uJP&}+GXKDMH41~vj$)NdUF4kJu3!$hm)e@i{L z(8C-24d}}wX9*{%@(zJBi!U~hfD`m%>wJKHxt(8T8@>DyWj}B`&=2&~|N4R3Q4Y|{ zA9Rq9<GC%aCsMQ)4+mz zBLjjfECkL-m7Ty={HDigV2e~4 zS(<&ysQAv{zDX3DeVde${5Nokq3RiCdAA&5HjfY;pqsU9LD-BkfYeix%CY)S%?W0C zI(W`+=sAZe$*!fXWSp9oses{T81&J@r^#9JQFrWk{jYuehaK2DACyVgh)k-AIB2ST zf&26pdfFh@6dht6nkxz9M)HCRA(xbqvB1wr_-Pno>eK7D;*b`ac$3_Jv^2au-Q++$wW^gBC$hluu>-Vj=|AhRPH_0ms)xR;lwF zqy*usGR0Hp+#oIU(AiJ;mfG^UmySeBj4b+e@$zjdDipNL{`1NC4eCMIpYtT9fgv5V zuYayNpa1trBoc{4B9TZW5{X12kw_#Gi9{m){15#$Llp7S@m>G`002ovPDHLkV1gy+ B*;oJo literal 0 HcmV?d00001 diff --git a/docs/static/pygui/antenna.gif b/docs/static/pygui/antenna.gif new file mode 100644 index 0000000000000000000000000000000000000000..55814324be26242f061339e6cd85d0ebfa67c8b1 GIT binary patch literal 230 zcmZ?wbhEHb)Me0RIK;{T1Zin$?H&D_H*bFS?Ad=9Q2fWIk*sNCU|^=;l%JZJm(HO0 zlZBCsft^7Iqz9y%fq7=dt~>t>PI<20Yw>z*%LIA$wu$OaDpThjU$rSV+3545te3L4 z4c2bo87$x;EGEftbE0tw!@SeeF1l5zS^GY_bn3O=?wHJw67KH-e4D?=Sg*L9zEOBr z-}0AbT~_ipKg-wE^ENhdx3uO}bToH$b2Cop7bHOy`C3Coz bFfU_Yv1-jSmJO>mux#46e%q>aP6lfLpq*ob literal 0 HcmV?d00001 diff --git a/docs/static/pygui/cancel.png b/docs/static/pygui/cancel.png new file mode 100644 index 0000000000000000000000000000000000000000..1d95ba0ce40155f397a587da9463123509c9fac7 GIT binary patch literal 1322 zcmV+_1=aeAP)U*#STqfkhaVU`Am#aP!|dqJ4`b$?;aOS z`Z4cK=f0T%7xce8_uO;-|93ymz4yU1rg3jWY7s=cy4=xgzqTo?KqaA~3lQd@G!(LA zQ2r2=i-@L0BvrR`a7el_)#k2BKz7yQdB(^_lox@fh~2NCa?mmK#`_1aRLY?u0VAs) ze~6o2k037tGb>GAz)clC*Sx>uzW(&zQ9DN#km+oD0p(+0cGSLQa8;2_jeUcMqja(s zpozqc%q(}m!20Oi?4fX=;U9mqq|>)-{H+A&Gl}?(toH@z>gwoOLinb>&hL_A>1;Vy zr<{W(5;N{9c>rb2jjZ>TcD0mUUu6YkX1V+CD)}*JXXbkSN9DYxoI;u8;&mVgYey>5_L2hrO}0JYzy;vJJHpH!*NpGaFSW&%oI--`;r}NOFxz##U8NdB1!UWo z%q&;^EL(VMgQWCTuILIK;MWr|!eg@H)${b#Hl+ zx(^Nz+qxTE*D3`t(>LCRu!evD?Z!b_XBD7!)fI3K2pIjbZ3!^fCZ@Bn1uyy31cO6t%MQHul|l0DE62!>*`Dmcncr;1 zA7~oA`b#3fq1d3X!ZxBgcZS^C+rjt8jjl^<>+VS-;Vn-Rd!y)U8$<5B-I#M{Y|~*J z=_s~&0Vh}#<%E|XNCDf4ZR;5)SeQ-#+`1X~@)$$z-CgASzKxIxL?ti+0j`;3+fP7C zq{!T!4wFOxr~w6>2?Jdr`9i=m4SmCpO|*pVLUSsJOnx90oKE7@kUW3{xP=L5s4)7t z2orKsRIQRc0FA{4`)XnfDYb%hinhuTCLmL3u(5sK2?;CZ>x2bHiVa}`{;VYO#3D5Y z)ydPzqS#Fh4R8yq1DB$P4JB_3xp%tB^=zBGu&WRu$i-q?K!5|MqXaEUzOZ0Jcc%(P zs0O(O1c*q=HlWko8YJ)gQX&MI@>-GA(tN&qWb68u+N^D$r8xZ-e@($ptpZBBPi2?5a?$k9! zLRH^QsIp|w59_9Gh=zN{TBYaEaJ^{bjQ?t-Y~q1vB(9ZxhF%qB$F z0rMhuzYKcNQLi_w!rG!XzgfF+Mh6xxGLCeJNJiR gzged-je8yc0S7nSp}o%sl>h($07*qoM6N<$f=vT~fdBvi literal 0 HcmV?d00001 diff --git a/docs/static/pygui/core-icon.png b/docs/static/pygui/core-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0b0ff5aa3a51f71807691032f516d7a15d7da896 GIT binary patch literal 2931 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 literal 0 HcmV?d00001 diff --git a/docs/static/pygui/delete.png b/docs/static/pygui/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..01b498d4cb54e3e1f71db21b1ef0408faf28dbda GIT binary patch literal 387 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 Nm&*gnpz<`}>|gl)4y^zH literal 0 HcmV?d00001 diff --git a/docs/static/pygui/document-properties.gif b/docs/static/pygui/document-properties.gif new file mode 100644 index 0000000000000000000000000000000000000000..732d8436455ba607ff30774ef83faa434789f775 GIT binary patch literal 635 zcmZ?wbhEHb6krfwI9A3`UtizZ-QL^V)6>`4+Ydxt6Z*URCUj4n)H`Wv|CDJHr_Go& zZN}v3v!=|LJ$2^nX|v}}pFMZR-1#%-Eu6hz(VT@#<}O+?Z}GAP%a$!&x_sgCm5Z0J zTCxI&Rxe$-dg-b)D^{&twiXE1tzNrs?b`LL*REf)X2YuWo7QaFym8yMZ98^v+r4}D z-hI3FAKZQ9*uj%$j-Nbx?EKkN7cQN-eEIUFs~4`{xO(Hp^_#bD+`N6`*6o|O?p(Wh z_u4HGx_egDy&`;YEE0HVit9zMSJ`0=BM zPwqW>djIj$2algUeDdt!vuDqqJb(1!`IDzF9=~}0?D@;5FJHcR`SRtfmoHwweE#Ou zi?^>|zJ2}b^_!P(-oASC_Vv5>Z$Eu}|LN1G&!0Yi`}Xzw_wPTxfBW&{`%fVH2}D1B z{`&dr*Uvvd@b}MOAo%z99|-;X_y6C25W)u(f3h%gG1N2YFaQB4P8is4G}PBC$jQn` zOH0W%^)$&PmQ9?P6RzIdBv~967UJ*YYhIu;UA!+K3@N%$caBBCo?vDZnK?>>} z4H6mGRzwyX@-nz_KC4-2cu1l_BwJ^i<gzK44%5h8Cn(&TO1gfn3+!O Kwon&fum%8seSYfz literal 0 HcmV?d00001 diff --git a/docs/static/pygui/document-save.gif b/docs/static/pygui/document-save.gif new file mode 100644 index 0000000000000000000000000000000000000000..165bcb908bbcf12dfb5d0c06a933898ed8f3944c GIT binary patch literal 1049 zcmZ?wbhEHb6krfw_}@YinU=Z|&e<14NFF4ld44ZmzCg z9-cm4UOwJlzTVz`KE46IegS^|fqwo${sBS$K0$$j!2!Xcfx)3cp)oNjndzz7*?C3z zMP~yN%>eKber00=&?_cmGkk~>bNa@fGjG?NeY^JT=M^U&Y&`d1?~!8%PM$h?>ddk8*N$Dd za{9`Zi`TASzIprlov%mkeLME($C-x@?>~Na|H+SwPk&x|{`1<4pVuBfe(>n=gU62_ zK7R7>$Hy=K} z|M>C!r_Ue1e*OIQ+n2B3zJB}m^~aANzyJLH144iP{{8#^|9^&2K;sZl{K>+|z|h5@ z1F{E{Cm1-sF^F=Ccsy!8$gjkvB_r{uXI8tKnApdQix+hmo2Jb%Ol)TFmv^iC!Pvqf zz}X|^|3g!G#rb}@hBp$Mij5v0GmpRb7dnSnV*-j_uJ)us;Cr9J~LHYW5xwW<>NUDdbt97Hy(V% z$)(wGNaN(?!^}$Z!Q1vIwxqgFGm4t@LgC3m=0&Gx2#Oj_k-W&p?c^l?(@>>DK-r^7 z#8T-<69WeWk4D0U1x##QykZIr7Y;NuvhgYeEGT%$)W&^S(;-8_p_!5K0y9g4-(LnM KMj0k125SJ$J7sYI literal 0 HcmV?d00001 diff --git a/docs/static/pygui/edit-delete.gif b/docs/static/pygui/edit-delete.gif new file mode 100644 index 0000000000000000000000000000000000000000..d23f758c7ddf6f31eb685d8d30930ae6017d500f GIT binary patch literal 1006 zcmZ?wbhEHb6krfwc;3#SqOPv4qo<*(uc>FCZD^!zXsl;qqGw`eXkuh+ZenC%VQOh< zZe?Ll3!6;)m&HC-rC;Y-q_XA*4fqE)7jqL z(bCh|+S}FH+uPj_ME#wUCiG6}pEPyq;;P#E?lx?!SW@GRxDVwV)3f=%U5q$v3C82^&2;=+q`!D*3BEXZP~nI z!{!~Ex9-}yZTIHwyLa#0y?fXG?RyUF-+y5LzJvSsA3S*I=#k^6j+{Ps{Pelgr_Y@| zeg5>B^XJc8IDh`qg^O1%UAlVq($&kCu3frv{p!^l*RI~Ue(mO^Yd0@nyLtWQ?MpXr zU%7Gn`i&~^i_io<3f9L*#8xI~mdi?ak_?%XjbJeE9JC{fBoSKD_<%`Qzs=AHIBf|K-ca|D%9jA)xq^g^`QlKZ6bf5PpV3N^QRbX&vc*H5LFT%M>QaGKnot4Mr zibW8c*9qoImY*Ay)4iuC`K?fR`DxR+c}~qdf=ox$efcB}b=G$6JKk1V5QxKE#`RT&>|v=%>_EGRItEZYX>VOJtMJI25sS(Icmq NtJ&5n@o+F$0|0Y`xn=+W literal 0 HcmV?d00001 diff --git a/docs/static/pygui/edit-node.png b/docs/static/pygui/edit-node.png new file mode 100644 index 0000000000000000000000000000000000000000..28490eff07edae4f5c5a58b1f04ed3cbdb9b6d48 GIT binary patch literal 3050 zcmVh{ER5N1j zzfMF7v6+R%UnMhmCZ?`q=}Zqj`6G}yK=)YsbwNHVlO-#-C{_+*5&$g>cE{Gaz}Q&$ zGr&8DOp?rCUbG!WJpft|ya{CY6 zYNF8LR#rjoR-4sZZT1wZXFG<)ujUiZoEYVIbd=<~8ZA5g3Dl_g6_v2^bDOt64RPYM zKU*E!3QtW4m=+)4iu1$lhx{1v^#o{s@F$2a^kbtoYqr_UUlrn{ffbo3wfqYaxmhxU zZ+b57wV)OTyQ6GDHPx$)niQA8-`)?gVYkcVUJo7H3c=d~bWV%UkmvfyL6!2 z?M}n|+7P40jr(_yDTCC=6cZ^m69` z5bI7)6*`ZqvUG!))YE9iuSV6eR*nLo2Pd~c81KDCu=5ZcJm$wjm`h8SVufEpgR=+9GX6Il0hb3KB=g8sdxn{&Ii3 z#Si|glI;gwG$P9y>F6>?#zY($s}I|T{A2zCFm|XFSMiHV;b4)Adr-}Ba#_Rx=;2^` z_>{7Dj-P^aRemhMq2q=Yi(F7se1JT6jp!0B2u9ohjr;`I_7J~E#LFuDi?<`++_Hre zib~<>1>cAdaN?9J6Bf$g$QMPn)6bRLIyigIuLa0I9)6Ny!O;?U>h(&FmR$JvbCqrz zk{waI$N^CG9e_Z8h>Ke+p`+otn5$qmdi0lTNs=Ter1INix%UuGkc!+`!XR zAK{i?Rj2p>=UmStBR5_LPXJxqx~)xGE58=Pl$K#^HeH))@sFLXKg~2xc5ODIc98=h z(#EyhTC{1QH1mJ{0)Qp!Z4S5@4m!Yz_~4}>Mb^$i3R2$z5D~IEm`OvGA(*=~WTayU z=i&pTwQ_q8k+pNu2efwSwNA1-nwvx&C{kv>?SEPONQpWYAD~@pxA$P=2{6M&oy^W+ zH|?`|(Z}AxkrE~NddRW(0Nv8P`DzF!0q}V?EzCvf=9dxHY_plM*nK+kB0fN$47cr7 zBhFkLSQI1EpPrqgvyQWApUsrlD%pS3nV4S14_MH%i|Z6*AOIRUicV+4Gz#hehi`ELsO$JHUW!i>&s-vW?#kic>`3$^l*i zAQ9iBi5vitougICR|q`ca<@@z{9mhyidA*2qSja=19QOUkAgd9l~g zx@8~$Qn?8OkHF@=%7)#s-&FBS`_^!^w*c5^#pXyCMYJp#(~A&mo0l3?vuUp~b4iu! z_qrMb<1P=7>}fe&K9{WN$0J?kEVMScUaA1Fey7chH>;@hxg{A7(pn3n2YXCE;!HD# zoATZQux^LV3-6daiWqC|8X54uiW@Q+0K4=&?FJ6%Xzj8DLl+U#s+c$av;s zrx!c+BoeeiXVvw8s>=M8A^!RAxPE}pH=_qz+;ExCEfSHLA~?!1*3&jSi-{wZX` zi|^Si+t@@C{&~g>wYc{h-{)uYDLX#jO#xQ{kUn!OQ2lfDzbh)?`8TUr9fR{a(IQ~M zJvjmH$q8Wj-5(U0>Gsn=>VE)1K&k87n0j{%}hY{j=(>Vrd!dDp2+mDtijU@1C8m z=jBilNKb`9-7SXpwiwhy{HL1>J-j7dgPz_X@_bxs)Zpan^Nya|dhDe6lNHU9;IW(Q z7at(C6yBWS(@2$5!GhoY$#Y7(&EcRy|_ z;sav6;75Ega4_rEA!Sz$>LC%=y(3n{f=Oco4DH=S#7EXr%wvA7_zpn!6|>8)>8S&I zcA?Cx4H1|)Cg5@7J08fF*xV!ogT7Cw8oA@)jR7!*i^(P zA7PBCujm@P>#^gfZOa)X*SZbD9aje!bxD&EKM$20lV4Rgo^an^`gs4`ZPv!Ug&OfR z{kY2m-0_1ZBmQ=%y1r3=y11H+Ykr@xVw3g7mVI`;uW61LXz{=e7UK6edSZgCrRwZB z>GoW)Y0FV*enUr0E4;LCY1XiQ*7~~i9opOC{xN|jAigRvhZ06dLwtzA4>&AdJmt~c zElfXBsun=sOyS81Nl~1c8sj9Yzm%-0M!$<%o0v4g4Y^MYT)o|1ez;h>O}#ytR?TDK z)erI(R6b-n-`D9W+74<`ydyY1tu3?S_On%y2>-0rgI-oOy%PJMu{t=p1%cH2P#y;D z8f*XckdNprDy)~?d~nYM@%tgS-IG#i(e7?6>k(9Y#i<&55Sd9aubPAW`jb+9?fb`F-6gX&1&68sO9ZII6q zT}4P%Qblg8(@aA&ssPkR3xXX{dkD}Mq(4YMARW~<2x)xt4)bZ05=2fxji0l{+KEPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D44X+rK~#8N?VWva zRMi#0&wcxLNj6`GMm|Ec6^ubpEQ*c@(oh5pUlqtgtBiC;rp|QgDAZPGsw1_rSS@1x zqn#F}A4*%xina*Ul8Ss|t*B)XQc$dzghU_-Az%CP-tD>Xoow>W9#sB zRkNiDjt@Zj9N!2xz)TK&I-cz$G+l#_A=-;(x`4xReBZka_*<&WQ~lM4x zC@(LUT{%US4A{2;_#J3+>8~V=f{$v>*zc9LGn;qq+7;H#&cRUu&>ZhAx{on<1l6l_ z>!5KG7=Jq4cJ6mh3WQ?*CEaSKZcYGT%n;}*RH4fgf)ERX!u0;u#U*fa8M1_YZunBO7uewd zlI2;c01&vRPI^2Hd|mJe60 zk$`hfmfo-9eFUi%Cf6D6!x<6 zU4DNX=}94Gr)L*giszTn&S!YvdF&@a+np~s>%9OURt2=b8O2RqeK2J9YB-(GSRCJ4s7HVoE_~ z{1}Rt=@y-uCqvLGcaWA`3PL$h_C!<=0GhEs7@MsAAZaN`megnP$>99Y9)^@TUvS>t z4z;UD>7h`ST~Cv2J4tB)v*s@ztE%in6e9tzuT-G+#-#RlF)2Be1I3#SEkR+7Ut7I( z^9j;zBPlI_tL#yHGW@NkuZBoUJy-;Fq@x_Lp;bU0(_9e|383cT|96jC+9}IQu{gJ!a~<5)^$B}ZkZPRitex2foy>XZryG%m#5wlH#m%#| zgNe<303S<&26Nlq<$Ph7+c-4h49ZBf1-m^mf}SZ`5dD@!yxm8 z4?cU|!W$G-p2ZoTj0d|GO~dgahVZ6i*FA`j;r1*y4i=*t3t-eQNHHSjDq8T-h{7@a za0j@H?|5^|=EK^n17tt}5QCGJc789O$OCwU31p5-ng_rd4u^5X{K8N#yyW-$jYH{v zkZ=l`F{g4~a|f@%hPgfw=L^&{=mQcyNAv*$z5#u5?2fs6SeNVFSfD99VlpCXjrC+RX_p%2M`u&}x_esA3APT4ynQujLvTiX` zzbFjqS7ZohaR_@@XBJ%VKmz+1@W_+&Uj)Ai0NBXK&-6HH?rS#c@%l5D^-01`06 zLVEoY#gPl?PDLwo@fm}F_lb*0G++wAtXT_(VGA%U!v#wFypzgcL9o<#gUNtTsHpH= zPHF^`XH~Z(=64KzLrYG2jbfcZORmXhOaZu_QD|&%n zb*Tp53QPfzn6ZL6!yuwcCf{1LfOJ?mOj8(`7CbF_SDqPZ*#zgRX=2XY0wk%PM2(W_ zEDT)SNz)8b2%ibx^+sasG>H*R0gxeFLt=zH#dx3$@yQDtDl#?j2FK)EQvmX_WibIK z7M~;JHP~98dQpY5Hekr@Riiu#esXVq5wQdo4tn75B%wJ#ebJ0Kaxqg!Y5k9(NEh!QB z$Kj5%+ek5B833`N5##VvQX`CbH`5u%W5he=^GvE1wFHsN`mC3ao`SvDh@M`spcSwDW$~{3ZW5a0nK^H9 zDF^9A98=#)rVK_7V$NTl$Q~031wbGwz4pC(WH}Bj#J)eHb?c=L|5)5=R&V$JK`6Sd zeUc?E zWx!Z5=a7pRg00`JNTn6sfy zXXReLK~kYcJXY=D)*BM10fTZInry^XFC0&*2Hql;J$JiSQlD0&WeZ>&Mv@po1+aBj z6T3*;6m+qW2|$WT%K)errFvGjbRexx`Um^@7)6DZ9KZ)0hLKr^!RKh(gwN~N-Of&u z)$yc70DPOct7Ifpp@YR%+wscuf*7E+bq(;ie~I``gtKvc09GF0*(?mNp;Kp*slU8Y zpuob_)0v}}=$zdLaL&;A>j&U`!JX$_;P?TWqg%kL-Fzu$@G_|phGqfWIszbvszBO; z0RL(PXtKE0ae%f22WWm$9fxBBAoBjG5%6C;LBsc|CgTun5n5bPXCB6p*sC7{yxl;C z9!?b84r5m{`qejw;n);(Wig07=nT#uKNO(yYP0hNGJXi&lxpI( zfYl#zPs=%2gKwOGxw!iIX-xbSNlaGlxrwvVI{#yhs%?K!aaHZc+OKvj7i)8U=*uK)R3Zd1042i|JneaN7{ksEV4nL#Qj*>D0DbHgzqe8 z9JW69HTG+I$&!cm@Jc-BG8B#57AFWEV?cet>2`o;-y@}mr1t^n{9XBBSc?O}K9iwi zudDCx<5Mx+?Il%1+~M3eKK4nArnlh;z_F;k3h~03f)|b>e!w{z$KHmXst4>!;Uits zIobr^KcV0AM{?BggZn!P_J|4#&0t!OGoy37+9!NR1#y!nX_u7)2Y)*R|nD z7zbd@7GLJX4_Nu!G`5*k$KmJz2v+Uk8!@%qLu#VDv~C)++^NbbI5q`!ldr%EOzwXt zH4(NKDDWMU9gE`wAf}UR8(}`)Q$K}je|9GiV7soWZGZ(%pNbYZJ_YsW;obZym1!$t z-$3AiA{511UB+dum|J9<|O|RA? QVE_OC07*qoM6N<$g150mDgXcg literal 0 HcmV?d00001 diff --git a/docs/static/pygui/error.png b/docs/static/pygui/error.png new file mode 100644 index 0000000000000000000000000000000000000000..d73d1dd402887d094863eba351eb62b72dec7d22 GIT binary patch literal 2258 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 literal 0 HcmV?d00001 diff --git a/docs/static/pygui/host.png b/docs/static/pygui/host.png new file mode 100644 index 0000000000000000000000000000000000000000..e6efda088f82d791ba5e3122f707581e05644165 GIT binary patch literal 642 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 MZDEIdh4F~{7sF09RR910 literal 0 HcmV?d00001 diff --git a/docs/static/pygui/lanswitch.png b/docs/static/pygui/lanswitch.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9ba59397d4a6f64e9f0b3e5340fba31b56f5e0 GIT binary patch literal 1138 zcmV-&1daQNP)voKryltj5h*UR{qBdw4dN?6pRmZ z&Uzm0zxTSFgGo0a)t`z*XJQvPa1ubIoDWgKTrl1pFI0DBGueX70i+s`>QBX@jy=E` z-!4-hLQmtfRVkS!NS(P>q52|y`Ty84T{U}Nrcn3_NcZh)j&L>tbElnBTxPi&AMSnl zMx;piON;;~Wa;LNY2?V(Hc`Q#&?U>2seNMYej1e(jZzOGnFits0ple zG6gH**^IhEw7~eeytTdshnq66EnCa@#y0=}k~K+mHXp~Tc%AQKQv>ECVhJ4Ia16?R8+8TCyy*QT?RX&2x&TKpF$#GiiTad6<;_in& zIIiQ#F&9Etz#{xuoT&l-p?Yzq1}uTf#aUYe7-MK}5PozLI$9d==ZxCo<94qcj_csY zyW4pF<%Q?R_I($Wh$V2S>7bUoP8pLkleqmpi?7q)lpNpJ1|(~eIIylm+jAFD`Ki3} zZK!VzXsvHSdqcbOo13%G&I$c}sr!bP6C)TL8-!addhxMJQr-Z@7~0nEs+jqnbB>{p z!%F7+g(z)+ZP_@mu4BE>-e6ZrY|Q(aO?dYEthj8QZi5Er}F51bk6m_KHDW zoc;Roy%b+Z4&vf$Trw_*i?eabILCEx^W7cf#&bG`UyyOhxFRoVzYCp@l5qh*$+$96 zGA=|Y85aPQj0=EJCgUP3f>dLhuHgeiTQcs*`V2O$-lSuAVWCRK)!DVkG#x~pUF-MI zQYaax5hdd!P%>^FO2#dYl5u~bWSmBnjMIpcaT-xFPBY!VNl-FQGfKv3M#(tMC>f_2 zCF3+RJ{hN*@yR&dC>f_2CF3-cX~1_c1`c8}&I41T1>&zR20n?_804d9fy}FmheT@( zB78~9yu7;yfGb*O0M}x?SF}jD0gv|Idkx0BMe9P3@hkT`?!Of+liCHv3)Ni!p2*aO z)bQu=)u&_%r5cdUWDD`xsx*MhFnc<3!T6=m6ZPrb$*Z$62PpgBSnYYRXG8Ho4*;V89$Sp}%1k?i5JCtcgb+dqA;e6-0Yy6CHFJ9NV*mgE07*qoM6N<$ Ef}dIza{vGU literal 0 HcmV?d00001 diff --git a/docs/static/pygui/link.png b/docs/static/pygui/link.png new file mode 100644 index 0000000000000000000000000000000000000000..d6b6745b29e839599b85ea86df290f709977529a GIT binary patch literal 1692 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 Ze*ha8+La8vOAG)2002ovPDHLkV1g1)B>eyY literal 0 HcmV?d00001 diff --git a/docs/static/pygui/markerclear.png b/docs/static/pygui/markerclear.png new file mode 100644 index 0000000000000000000000000000000000000000..6f58c0059fa6447d91b1bbeb9d9153ab22af4e95 GIT binary patch literal 1370 zcmV-g1*Q6lP)Kcxg%+F)2cF(Mz(F z;MuF7f>zU}RC+MPG-*Kui5DSWgj#DNf|tZY^N8;_SrPb>p z+fhS|jipL!x2FN|W|#q34~bcKV0D}>!c!@CRQu)dA0X&V>D#_3)pDg7aDVvZX%ga^ zYNyQz0eN?MWkI1%4l#DDeSOXSRRD)%Ynzgom@BZiVV+P^h*??L9B8FajM4oIaE>C?#_hLby-NMDUe>+GDm_aJj& z8qR@(a1I;<0F*y?52)2*>(lAUAu~P+IE0O<^YCu|7F%C|#-pp_?cw{R`v=kS>e<+4 zg42^jW?~AI&4M~zWX?^($sLKMufSu{(dka%`@|(-d-(Nn7$6~W0lS6o6Sah$!mp1< zvxLM2w7r<$HGH3R{~$VEc`NkQ(Qe`U=#-GSfXdZ*Y~EN7^x5=t&xg7?3KIUq2&I0) zko-Bo^H83<99)9p;YcQYv0lQ))C9bnzf1KI1$wT6VwPY__<+tEv6v*-6h5H)PB>x- zwuKMq763#h!N%|b69fRECD|lBA23M( zpd=xE;#nZw_-mLLK48irXgiXJBb&nqOcVgfOV|aI!v{LnavSKLhw_KB zfoFnu`xYRQeLggC0p<568~!RK(#@2Bhoj1;AEUZ>HGF@PBraep+(-X3D3+b91`>3I z4`@0GKT1NZ+lV_!5CWC?Ic?zs8ZSftFCmdV-0c#CK;@HI=GsjW;`G?4x&MvP=WvX33vmq-$`&v+|!vZwJ0yM({G{XWk zqw>P;{aw<>zjy6?!d}~YH{ct=+aQ%CNAXP^ju27sD_N!!QiPFbu;m c3}X-f0nFy52u5{MYybcN07*qoM6N<$f=T6tMF0Q* literal 0 HcmV?d00001 diff --git a/docs/static/pygui/mdr.png b/docs/static/pygui/mdr.png new file mode 100644 index 0000000000000000000000000000000000000000..b0678ee7f9453539170ac9c51112f31a363536e9 GIT binary patch literal 2786 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 literal 0 HcmV?d00001 diff --git a/docs/static/pygui/observe.gif b/docs/static/pygui/observe.gif new file mode 100644 index 0000000000000000000000000000000000000000..6b66e7305f35484095a2182ed828c6507c72a746 GIT binary patch literal 1149 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 Z{tNmR$e-W{4OjpG002ovPDHLkV1nyAevtqG literal 0 HcmV?d00001 diff --git a/docs/static/pygui/pause.png b/docs/static/pygui/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..9ac4e6eab0939e105f042d7b97722579b98b5a82 GIT binary patch literal 2368 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 gLkAR`nDB?m&P-g{{?ui+8M z{_E`i?Ck#S?f&lW|9_tDA^saEV<{jqFflS9bZBKDLT_bhZf77vZ*6d4Zg~J9`2+z9 z01p5x00000AOIi$00fut$w{ldIO|=y|6nLmg=3y*s-^|GzOdO&Wo)CfRNr_0?7Wx{ z7<>(jwj&azT$&g-m(u|NL}|P#3J?fbWzrac;9XF=-EZ@i;HCisbU;vTDio=PTh2!- z4^;pSCRKfgT}KOh00w)9eM1P2UXPDMla-Z2nVfzkot>MYnWCeXpQfiYsj51xheIN= PvmP6^x45~wyAc38TK{^> literal 0 HcmV?d00001 diff --git a/docs/static/pygui/prouter.png b/docs/static/pygui/prouter.png new file mode 100644 index 0000000000000000000000000000000000000000..b0ccf6641d7dccccc4502cbb2a8c5fcdf08eaa7e GIT binary patch literal 2590 zcmV+(3gPvMP)H!I{Uu%Ie?9gjg5_sjg5_sO}0UqHXs#M z%~kWFin?4;m132+n1Ese<}ok>Kt4wQ0>I}01_}6_fh!F30XU-oz44gqSXUtUpN#zo z4W^U>d|_WMV$Sufs815p8U~iYx{VB`lOR6Kih3w_#B(4R2;LH5uLv^kfcpA+<>=BA zkAZj#1DgTlNiK32Kr4uYIZ=0KFc3^gE-xh+bHM6`I`7Q{dM^ zZ+4Y+jby@rv^&5T_T?g>JOiemFmQjm%TERYpD@#Ia>aii_Jwbzx$N{&-cspfrac5K zN@uwSxCG)C+WoEXq^rQRIKUtBFNy=N0Vo%#yK4RlFcXemIVw^XU}_^*{JF3%d{M0M z-6*mP@P+*TIM4&Zm0y@whz$$YW8=bgD9jT)gb-J7+;P$u@(08UCn-vnaA{|$#}ReE z45l5TwF|s+ux-g!cpY8wBlBL{*i)G4O~;-Ep+~`|)u>5)UVsj#RW( z6pIuU=792W`Gf38ybD11Qv!_6cvjxGQdl~ZEKn1UgJn(C3q=VEaX?jbRRQA80|0~> zb|*%kY$-FNaflxzMeP(dCRlbrX=kY?rl{=z9u{EFU6|?HQ;iiOAmFPmS1erGS?aN> zW!V9asQWiE23)|}>_+CyXpF}kQTNMMMJzd>yruFxV0zZ75y=qo6w90$0RWhGY-+Bm zwjyELGJnXwh?xFhrSMdU>c=I|j6lS1SQ(U^W;4ctJpg8z&96n*h5hKdu>Y?A|Kb}z zPnqwRfB%&%lO|&}De9{L{AMznY5~5G-w(iNChv5FfXecgN>lY=*a5z14~@>PDAo; zIucLnNvO*Ktf;!gY;L0{>KZ-ibnOL=1rURmi zx*USyhYIrwvHg)c%*`nfAt#i4Pd@4vKP~boQ&AJjGR;KVDvHo300}?TGO`{F(ouJs z4p51OIsmxc)W0sKsary3*GM|*PIC|@ph%246Lg8NVc~kr&H1vB!P!*#3)YG1?~}rS zbq4^LpX{GnCsTS^F>e|2y|d8N*MdkQB1BUQufvO)B7e$m(CF_=Iy+-ZT6cG%XB{xp zkR>K4%qzsUC0nJ1k=#jI#~@}-48k`6$nTsndeNe(uLb?X{lc|-K0S`X$Pj9Z{ON23pJ^q~(*iU_L~u27 z4GkadlT!KP^bhyrjgx!v-&r4~9Q4zYt zY;MEE;vA4W;yFOTkTsLC2Lgt2qVB_b(&}wgtY!E}QgJ4J$G`=n`KCJrocnBO*6T*| z8FoO}7rv>8ttft=;L9Y07 z9Cv&hz-+S`sVH5v!Blukvb~}YFZbZxfo>6IC1cQ`s5{MOHXW7{_JuDJ(-sKYs-8&q zz|Y4^8=tY}yc%u2YUF9G00ScQCStT77d3uN z!XI`h8`|z`|5#DfjR2&)as0!}J-XhKt~Z@YE~-pxMZQ&CXmD;@Wm|7d zRUQS?KSk<}qxEFVobfPl+CltyXLU$uoRskB=c05tJgdqMvm0w>Nb41)aCN9XC+1qrzzbmdIGtr{ za0$TkH?GfKoX*MzGWa34(#}$kGwQAb(~Ar|lp!d&`38XN7`@MS1wbE| z&ak5Px>e;^s5&(D;^A#z_PXGV_07*qoM6N<$f*@Dt Ae*gdg literal 0 HcmV?d00001 diff --git a/docs/static/pygui/rectangle.png b/docs/static/pygui/rectangle.png new file mode 100644 index 0000000000000000000000000000000000000000..ca6c8c06a1b6ae83e78bb593209b5820fd314f01 GIT binary patch literal 259 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoY)RhkE)4%caKYZ?lYt^TJY5_^ zD(1YsxRJ9VK!DY8tKsYoKC?F%^|JDoJ~W;j_i4vmHJ~yOa5LQ>_wDhY!s9=-ef@a- z^nL3c!U}2*J`FP%3>lM{j<9ra3ea2t;u3}f?s{*ZGZ*|h(Xa(1;_2$=vd$@?2>=~K BO+f$v literal 0 HcmV?d00001 diff --git a/docs/static/pygui/rj45.png b/docs/static/pygui/rj45.png new file mode 100644 index 0000000000000000000000000000000000000000..c9d87cfdc52472db6adb4f7d234172e13e7a2b68 GIT binary patch literal 1121 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 literal 0 HcmV?d00001 diff --git a/docs/static/pygui/select.png b/docs/static/pygui/select.png new file mode 100644 index 0000000000000000000000000000000000000000..04e18891bc3a3a1c3cea14a66c03019ecae22d20 GIT binary patch literal 1038 zcmV+p1o8WcP)7Hg;m8Hd-p$ z*|`*og{YNa;{p~oB9=)ZLQo+DB!!BIB!Xsh!9 z0sa6-WMsxmIR+SD3^*yGG*ZenAX}Ft#KuUa1{mNPFwMW@xTwYe1Ka}YeT$BXY7H>J z17N0a;jvH)0}Rjx%=N523TkD50iFR1{mKuYS{h)0E?}A06$DPL4KTo4V71Rhgp3OV z4DbQi=y4sv;>rL+lBDkNx0En(X@EhK)RMS1z@SNLQG6I+&?L1Wz6>yElA0Hv24qW< z)Cou!V9+FW94P|~a2rihMGY`$k}7V1A>~P`^zgHl+9Xwa@DnSQNvibFb1QX8s`S9~ zRjQIy>0uYD)Fi3WgDzI7NK&PTT&ki^Ql$rMQ&A_W(!;f_zj*j0NQ6^HV4s!p23Y5E z2WmiZMu7$#YM$<@aU?t^tOLdh>2ten^#Yq6&0w4~XX8JC0g^!vWU4Py-6Vj8EG1AH*QbtAV2!ucR7s2%;X2@iw1}yUj|=E9u*lbfsv)#b zz%Ss5#ddRm91_wPJ!i2qphHMs^fZ!u9cg32ZW)R3a~C@U9!Uv|oo(0| zFa~T8(ilDM*qLxsN?`09#Lj>bV5yMCsHrE3vTaN_FQhSg&R}Q2Ao{(r6BYwMurp!5 zkVYW9$5Lz=1nyaGyUL}IQp9Qiz;(;*`kkssIjs~w{RHy_U8rm#d8Vk40jK4prH5wV zHItA0De+TgX?bX=4%i8-CAmO9#=}2fD2k#eilQirq9{|4fA_FisMx}b!vFvP07*qo IM6N<$f(2yHl>h($ literal 0 HcmV?d00001 diff --git a/docs/static/pygui/shutdown.png b/docs/static/pygui/shutdown.png new file mode 100644 index 0000000000000000000000000000000000000000..532f2cb9762971efd01089debdf87bad56ec812b GIT binary patch literal 1546 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 Ef+`}3D*ylh literal 0 HcmV?d00001 diff --git a/docs/static/pygui/stop.png b/docs/static/pygui/stop.png new file mode 100644 index 0000000000000000000000000000000000000000..1e87c929740d1a896decf7f4eca06e55ad65c745 GIT binary patch literal 2305 zcmV+c3I6tpP)^Hgx(Sl}$FqzCbBOD$t>7#Uddk*+i#Pr6@M< zo9ub~klnD^Y&QFH?!8Oy{e9ZKFVFeU`R}>+?mg!`2e2^}GFw18bZ*}5bQaw#XOLS; z%o9X+Fi=8_c>utK0Rs9Y^Z$gv0D(gz!wWjgUp+Kpvh*3rI|HtGc9j&7T*u7o3Ai7? zau8eqvK<~TXDCtbg5?9#Ik-3<7kwzjZ1Jm)C}?FUe#S2;Q}#6&OPCJvOI z_6_S%fG!5~cUy*uDPr#iM$0~nxQkM~I?`W1izm9g`HF(!J> z{h<3<+P`0_3aZS2kE*`?xv(f`0%(h>n0d-c$pt&hg1`NjieO3%=&owGj|9ICV6lpL zw*jGu;7v;c{;RnL61fKG_0+8<;xn*L)AY}W^BN<%7$`H&L=EsU1_`TN+u^|WI$)_6AKH%@*=Et!_9<{ zL-6W*1O6N7o{;uUm;rKx)y~X<6Gbv4RAR7 z_5xU@M3%K!O7r_5!F(wlC==DCF^0F!*t03II62P_8nZt~kG_{F!5G&$jwi z$#x{a2M9a^)&0dH=dK6i&_OFRo=#d+;)Y(&u6v?`rfk$w)4iqP?!=8#3}6x2iToL{ zHk;7MA-|Kjaf$(wvC|km7KErSanmFNLLP5LbV#_3K0Mgv@jej0VR8!~+HR;E3qlEX zoOlWPnt@s@2{AUtf1fbmif2~|fTYi=HVPp;^zYi*iN5p+1BwuN4E8H$BPbXcnYT9f z<<=#Lc)cldtO#L4?Bgv1m{yvh#flK~irDA47J&OqQDa5o>RJHf-HxzFI~Yl6w9dy3 za2DMxhyD7&NVI~Yn`J=M02vW3bM;sdCN~f@fCc88sl|#As04@_Ab=8cZ(Y;t4#5(M;_EyQX_qECU?B}hw9Ps_O{@7`SAP9P2^Ee~WzFZ}xd1lPBf+O*pC=5M zTR3nAz^EC5EJk?F5YEQFOc)T&D0|TqNfv^E3wOWKJP`Yy_%iO4!FsI7Tk&5e8Nfs@ z8>_{7I54@6CmB%M>iY|TzZ$B@TKsuYn{U$7=G6WGrlW?*u^0?ACvKZ!fPi=r_G<|} z7{e&Nl(=z<0gHo;9|34JM3R*t1WJGJ`#5oP(k@9R`Z=I*k}v>BWw}oEN4-`?L>x%A zBdGz=oAK)^6^#77Q)M_^8M&NuI?zcl@XB;)@uy#@Lh!SYq%7n4;Q9A({rQ7%F3<0D z(jy($l+Ka|07CL#|Mg zTEddx1rS!E%o>ami-vR-l1po7Vp*&IT`)b9yDn?Mz#ezokw2z$Y}zSl7@*#54FP7h zGpVHJxT~$HiqiZeoA%&b0$3yYT>znMmCbq>U0R{Tn{MR;8IF#=H{idaapW5Slq}yd zt6*H0(6%gUIsBhYPRn?B`l8^`4+YCg*yBm#=_7&7Wx>XbpBA&4AFw#se36*8!+tUX zfG~l-q#C*M;y48}zaaeCbhZ`)Z zvb5cIK9?X$G9zBA@h;OOy#}CEMZCO+(WN1>X;JX#hq;DQGDmDlz<<>tcnyH#D&plc z476)b#~Kxt59qg);R~33t1IRAQikz_YP^p?k`J>c?LBuexW2Tl`GV@mx=d4b2mI$g z`Mh%_G5-L-?K}Z+h>3nW8ouL8dQ?7Oz=F=9s=88;I0#CfygFYYqPB?8zOVOM^M)-g z9`blAAZRZbJB%zh3r@16ALsgbnJ$J`M9AZ<0MR}GHOQYFMG+8=Nz$Gx3pBoOWWlEW z68~A_{cMD3J2Sih9yLkEWdi%g#qz={CM5Gp(1UaAi17F_XAiCz>QR6n36Mw@xF@=0$-Bg({qM!HhywM zK9F|?B;$kH+QQ;+@lvLUi=-%q;5%7zF%cC505kWKpngdDFALcZ;?RI&_}Yq=7W-|j bO=|uR`17L!0e5Q$00000NkvXXu0mjf09{27 literal 0 HcmV?d00001 diff --git a/docs/static/pygui/text.png b/docs/static/pygui/text.png new file mode 100644 index 0000000000000000000000000000000000000000..14a85dc02261ccdfc217bad1a38ff926fffcab17 GIT binary patch literal 314 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoY)RhkE)4%caKYZ?lYt`dJzX3_ zD(1Ys>&VySAi&_L^6&rYs;6m6hs}Ol*YgHT1G9@vg$QUwG vUEqf7{zl94q^+)4-KyO%F9_N*uF6a@fbW^{an^LB{Ts5sLo;l literal 0 HcmV?d00001 diff --git a/docs/static/pygui/tunnel.png b/docs/static/pygui/tunnel.png new file mode 100644 index 0000000000000000000000000000000000000000..2871b74f01e8f4080a3a88e30cabaed82a182b3a GIT binary patch literal 2256 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 literal 0 HcmV?d00001 From 3fac6bc7ece7cc30bde17d97d33d519ef35ad555 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 May 2020 12:27:47 -0700 Subject: [PATCH 0246/1131] docs: fixed link to pygui from index.md --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 4e215916..e9c690d8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -23,7 +23,7 @@ networking scenarios, security studies, and increasing the size of physical test |[Architecture](architecture.md)|Overview of the architecture| |[Installation](install.md)|How to install CORE and its requirements| |[GUI](gui.md)|How to use the GUI| -|[(BETA) Python GUI](gui.md)|How to use the BETA python based GUI| +|[(BETA) Python GUI](pygui.md)|How to use the BETA python based GUI| |[Distributed](distributed.md)|Details for running CORE across multiple servers| |[Python Scripting](scripting.md)|How to write python scripts for creating a CORE session| |[gRPC API](grpc.md)|How to enable and use the gRPC API| From 124d655dc67dc72ffec1a19217aca3cf92a16922 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 May 2020 12:41:57 -0700 Subject: [PATCH 0247/1131] fixed issue when sorting hook when saving to xml, due to enum refactoring, updated test case to hit this potential issue in the future --- daemon/core/xml/corexml.py | 2 +- daemon/tests/test_xml.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 891db1cd..deedd139 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -312,7 +312,7 @@ class CoreXmlWriter: def write_session_hooks(self) -> None: # hook scripts hooks = etree.Element("session_hooks") - for state in sorted(self.session._hooks.keys()): + for state in sorted(self.session._hooks, key=lambda x: x.value): for file_name, data in self.session._hooks[state]: hook = etree.SubElement(hooks, "hook") add_attribute(hook, "name", file_name) diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index 783e2722..04f1192d 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -17,12 +17,17 @@ class TestXml: :param session: session for test :param tmpdir: tmpdir to create data in """ - # create hook + # create hooks file_name = "runtime_hook.sh" data = "#!/bin/sh\necho hello" state = EventTypes.RUNTIME_STATE session.add_hook(state, file_name, None, data) + file_name = "instantiation_hook.sh" + data = "#!/bin/sh\necho hello" + state = EventTypes.INSTANTIATION_STATE + session.add_hook(state, file_name, None, data) + # save xml xml_file = tmpdir.join("session.xml") file_path = xml_file.strpath From 12ed9c84224f2008b4ff212ccfddba37668ec996 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 May 2020 13:00:29 -0700 Subject: [PATCH 0248/1131] updated changelog for next release --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1535887b..f4557a3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +## 2020-05-11 CORE 6.4.0 +* Enhancements + * updates to core-route-monitor, allow specific session, configurable settings, and properly + listen on all interfaces + * install.sh now has a "-r" option to help with reinstalling from current branch and installing + current python dependencies + * \#202 - enable OSPFv2 fast convergence + * \#178 - added comments to OVS service +* Python GUI Enhancements + * added initial documentation to help support usage + * supports drawing multiple links for wireless connections + * supports differentiating wireless networks with different colored links + * implemented unlink in node context menu to delete links to other nodes + * implemented node run tool dialog + * implemented find node dialog + * implemented address configuration dialog + * implemented mac configuration dialog + * updated link address creation to more closely mimic prior behavior + * updated configuration to use yaml class based configs + * implemented auto grid layout for nodes + * fixed drawn wlan ranges during configuration +* Bugfixes + * no longer writes link option data for WLAN/EMANE links in XML + * avoid configuring links for WLAN/EMANE link options in XML, due to them being written to XML prior + * updates to allow building python docs again + * \#431 - peer to peer node uplink link data was not using an enum properly due to code changes + * \#432 - loading XML was not setting EMANE nodes model + * \#435 - loading XML was not maintaining existing session options + * \#448 - fixed issue sorting hooks being saved to XML + ## 2020-04-13 CORE 6.3.0 * Features * \#424 - added FRR IS-IS service From 150db074977d857d76c19e10b13e01130ea2e44f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 May 2020 16:02:23 -0700 Subject: [PATCH 0249/1131] pygui: updated canvas size and scale dialog to allow negative values for lon,lat,alt --- daemon/core/gui/dialogs/canvassizeandscale.py | 6 +++--- daemon/core/gui/validation.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/daemon/core/gui/dialogs/canvassizeandscale.py b/daemon/core/gui/dialogs/canvassizeandscale.py index 5a042468..3418af8b 100644 --- a/daemon/core/gui/dialogs/canvassizeandscale.py +++ b/daemon/core/gui/dialogs/canvassizeandscale.py @@ -183,7 +183,7 @@ class SizeAndScaleDialog(Dialog): frame, textvariable=self.lat, validate="key", - validatecommand=(self.validation.positive_float, "%P"), + validatecommand=(self.validation.float, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=1, sticky="ew", padx=PADX) @@ -194,7 +194,7 @@ class SizeAndScaleDialog(Dialog): frame, textvariable=self.lon, validate="key", - validatecommand=(self.validation.positive_float, "%P"), + validatecommand=(self.validation.float, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=3, sticky="ew", padx=PADX) @@ -205,7 +205,7 @@ class SizeAndScaleDialog(Dialog): frame, textvariable=self.alt, validate="key", - validatecommand=(self.validation.positive_float, "%P"), + validatecommand=(self.validation.float, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=5, sticky="ew") diff --git a/daemon/core/gui/validation.py b/daemon/core/gui/validation.py index af16dadd..fee075ad 100644 --- a/daemon/core/gui/validation.py +++ b/daemon/core/gui/validation.py @@ -20,6 +20,7 @@ class InputValidation: self.master = app.master self.positive_int = None self.positive_float = None + self.float = None self.app_scale = None self.name = None self.ip4 = None @@ -30,6 +31,7 @@ class InputValidation: def register(self): self.positive_int = self.master.register(self.check_positive_int) self.positive_float = self.master.register(self.check_positive_float) + self.float = self.master.register(self.check_float) self.app_scale = self.master.register(self.check_scale_value) self.name = self.master.register(self.check_node_name) self.ip4 = self.master.register(self.check_ip4) @@ -63,6 +65,16 @@ class InputValidation: except ValueError: return False + @classmethod + def check_float(cls, s: str) -> bool: + if len(s) == 0: + return True + try: + float(s) + return True + except ValueError: + return False + @classmethod def check_positive_float(cls, s: str) -> bool: if len(s) == 0: From 22d813df63e960aa608f96d45503e15e02e5a649 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 May 2020 22:00:52 -0700 Subject: [PATCH 0250/1131] pygui: updated validation to be wrapper classes around ttk.Entry for convenience and less code --- daemon/core/gui/app.py | 3 - daemon/core/gui/dialogs/canvassizeandscale.py | 82 +------ daemon/core/gui/dialogs/colorpicker.py | 33 +-- daemon/core/gui/dialogs/linkconfig.py | 78 ++----- daemon/core/gui/dialogs/nodeconfig.py | 13 +- daemon/core/gui/dialogs/preferences.py | 10 +- daemon/core/gui/validation.py | 219 ++++++------------ daemon/core/gui/widgets.py | 37 +-- 8 files changed, 125 insertions(+), 350 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 5ca95ab5..73aabb17 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -15,7 +15,6 @@ from core.gui.menubar import Menubar from core.gui.nodeutils import NodeUtils from core.gui.statusbar import StatusBar from core.gui.toolbar import Toolbar -from core.gui.validation import InputValidation WIDTH = 1000 HEIGHT = 800 @@ -33,7 +32,6 @@ class Application(ttk.Frame): self.right_frame = None self.canvas = None self.statusbar = None - self.validation = None self.progress = None # fonts @@ -73,7 +71,6 @@ class Application(ttk.Frame): self.master.protocol("WM_DELETE_WINDOW", self.on_closing) image = Images.get(ImageEnum.CORE, 16) self.master.tk.call("wm", "iconphoto", self.master._w, image) - self.validation = InputValidation(self) self.master.option_add("*tearOff", tk.FALSE) def center(self): diff --git a/daemon/core/gui/dialogs/canvassizeandscale.py b/daemon/core/gui/dialogs/canvassizeandscale.py index 3418af8b..6a63a1ae 100644 --- a/daemon/core/gui/dialogs/canvassizeandscale.py +++ b/daemon/core/gui/dialogs/canvassizeandscale.py @@ -5,6 +5,7 @@ import tkinter as tk from tkinter import font, ttk from typing import TYPE_CHECKING +from core.gui import validation from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY @@ -21,7 +22,6 @@ class SizeAndScaleDialog(Dialog): """ super().__init__(app, "Canvas Size and Scale") self.canvas = self.app.canvas - self.validation = app.validation self.section_font = font.Font(weight="bold") width, height = self.canvas.current_dimensions self.pixel_width = tk.IntVar(value=width) @@ -59,23 +59,11 @@ class SizeAndScaleDialog(Dialog): frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") label.grid(row=0, column=0, sticky="w", padx=PADX) - entry = ttk.Entry( - frame, - textvariable=self.pixel_width, - validate="key", - validatecommand=(self.validation.positive_int, "%P"), - ) - entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_width) entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="x Height") label.grid(row=0, column=2, sticky="w", padx=PADX) - entry = ttk.Entry( - frame, - textvariable=self.pixel_height, - validate="key", - validatecommand=(self.validation.positive_int, "%P"), - ) - entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_height) entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Pixels") label.grid(row=0, column=4, sticky="w") @@ -87,23 +75,11 @@ class SizeAndScaleDialog(Dialog): frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") label.grid(row=0, column=0, sticky="w", padx=PADX) - entry = ttk.Entry( - frame, - textvariable=self.meters_width, - validate="key", - validatecommand=(self.validation.positive_float, "%P"), - ) - entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry = validation.PositiveFloatEntry(frame, textvariable=self.meters_width) entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="x Height") label.grid(row=0, column=2, sticky="w", padx=PADX) - entry = ttk.Entry( - frame, - textvariable=self.meters_height, - validate="key", - validatecommand=(self.validation.positive_float, "%P"), - ) - entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry = validation.PositiveFloatEntry(frame, textvariable=self.meters_height) entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=4, sticky="w") @@ -118,13 +94,7 @@ class SizeAndScaleDialog(Dialog): frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text=f"{PIXEL_SCALE} Pixels =") label.grid(row=0, column=0, sticky="w", padx=PADX) - entry = ttk.Entry( - frame, - textvariable=self.scale, - validate="key", - validatecommand=(self.validation.positive_float, "%P"), - ) - entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry = validation.PositiveFloatEntry(frame, textvariable=self.scale) entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=2, sticky="w") @@ -148,24 +118,12 @@ class SizeAndScaleDialog(Dialog): label = ttk.Label(frame, text="X") label.grid(row=0, column=0, sticky="w", padx=PADX) - entry = ttk.Entry( - frame, - textvariable=self.x, - validate="key", - validatecommand=(self.validation.positive_float, "%P"), - ) - entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry = validation.PositiveFloatEntry(frame, textvariable=self.x) entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Y") label.grid(row=0, column=2, sticky="w", padx=PADX) - entry = ttk.Entry( - frame, - textvariable=self.y, - validate="key", - validatecommand=(self.validation.positive_float, "%P"), - ) - entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry = validation.PositiveFloatEntry(frame, textvariable=self.y) entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(label_frame, text="Translates To") @@ -179,35 +137,17 @@ class SizeAndScaleDialog(Dialog): label = ttk.Label(frame, text="Lat") label.grid(row=0, column=0, sticky="w", padx=PADX) - entry = ttk.Entry( - frame, - textvariable=self.lat, - validate="key", - validatecommand=(self.validation.float, "%P"), - ) - entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry = validation.FloatEntry(frame, textvariable=self.lat) entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Lon") label.grid(row=0, column=2, sticky="w", padx=PADX) - entry = ttk.Entry( - frame, - textvariable=self.lon, - validate="key", - validatecommand=(self.validation.float, "%P"), - ) - entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry = validation.FloatEntry(frame, textvariable=self.lon) entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Alt") label.grid(row=0, column=4, sticky="w", padx=PADX) - entry = ttk.Entry( - frame, - textvariable=self.alt, - validate="key", - validatecommand=(self.validation.float, "%P"), - ) - entry.bind("", lambda event: self.validation.focus_out(event, "0")) + entry = validation.FloatEntry(frame, textvariable=self.alt) entry.grid(row=0, column=5, sticky="ew") def draw_save_as_default(self): diff --git a/daemon/core/gui/dialogs/colorpicker.py b/daemon/core/gui/dialogs/colorpicker.py index c4268788..9087d6df 100644 --- a/daemon/core/gui/dialogs/colorpicker.py +++ b/daemon/core/gui/dialogs/colorpicker.py @@ -5,6 +5,7 @@ import tkinter as tk from tkinter import ttk from typing import TYPE_CHECKING +from core.gui import validation from core.gui.dialogs.dialog import Dialog if TYPE_CHECKING: @@ -50,13 +51,7 @@ class ColorPickerDialog(Dialog): frame.columnconfigure(3, weight=2) label = ttk.Label(frame, text="R: ") label.grid(row=0, column=0) - self.red_entry = ttk.Entry( - frame, - width=4, - textvariable=self.red, - validate="key", - validatecommand=(self.app.validation.rgb, "%P"), - ) + self.red_entry = validation.RgbEntry(frame, width=4, textvariable=self.red) self.red_entry.grid(row=0, column=1, sticky="nsew") scale = ttk.Scale( frame, @@ -82,20 +77,13 @@ class ColorPickerDialog(Dialog): frame.columnconfigure(3, weight=2) label = ttk.Label(frame, text="G: ") label.grid(row=0, column=0) - self.green_entry = ttk.Entry( - frame, - width=4, - textvariable=self.green, - validate="key", - validatecommand=(self.app.validation.rgb, "%P"), - ) + self.green_entry = validation.RgbEntry(frame, width=4, textvariable=self.green) self.green_entry.grid(row=0, column=1, sticky="nsew") scale = ttk.Scale( frame, from_=0, to=255, value=0, - # length=200, orient=tk.HORIZONTAL, variable=self.green_scale, command=lambda x: self.scale_callback(self.green_scale, self.green), @@ -114,13 +102,7 @@ class ColorPickerDialog(Dialog): frame.columnconfigure(3, weight=2) label = ttk.Label(frame, text="B: ") label.grid(row=0, column=0) - self.blue_entry = ttk.Entry( - frame, - width=4, - textvariable=self.blue, - validate="key", - validatecommand=(self.app.validation.rgb, "%P"), - ) + self.blue_entry = validation.RgbEntry(frame, width=4, textvariable=self.blue) self.blue_entry.grid(row=0, column=1, sticky="nsew") scale = ttk.Scale( frame, @@ -144,12 +126,7 @@ class ColorPickerDialog(Dialog): frame.columnconfigure(0, weight=1) label = ttk.Label(frame, text="Selection: ") label.grid(row=0, column=0, sticky="nsew") - self.hex_entry = ttk.Entry( - frame, - textvariable=self.hex, - validate="key", - validatecommand=(self.app.validation.hex, "%P"), - ) + self.hex_entry = validation.HexEntry(frame, textvariable=self.hex) self.hex_entry.grid(row=1, column=0, sticky="nsew") self.display = tk.Frame(frame, background=self.color, width=100, height=100) self.display.grid(row=2, column=0) diff --git a/daemon/core/gui/dialogs/linkconfig.py b/daemon/core/gui/dialogs/linkconfig.py index 4f569ef2..92361ed4 100644 --- a/daemon/core/gui/dialogs/linkconfig.py +++ b/daemon/core/gui/dialogs/linkconfig.py @@ -6,6 +6,7 @@ from tkinter import ttk from typing import TYPE_CHECKING, Union from core.api.grpc import core_pb2 +from core.gui import validation from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.dialog import Dialog from core.gui.themes import PADX, PADY @@ -120,95 +121,65 @@ class LinkConfigurationDialog(Dialog): label = ttk.Label(frame, text="Bandwidth (bps)") label.grid(row=row, column=0, sticky="ew") - entry = ttk.Entry( - frame, - textvariable=self.bandwidth, - validate="key", - validatecommand=(self.app.validation.positive_int, "%P"), + entry = validation.PositiveIntEntry( + frame, empty_enabled=False, textvariable=self.bandwidth ) entry.grid(row=row, column=1, sticky="ew", pady=PADY) if not self.is_symmetric: - entry = ttk.Entry( - frame, - textvariable=self.down_bandwidth, - validate="key", - validatecommand=(self.app.validation.positive_int, "%P"), + entry = validation.PositiveIntEntry( + frame, empty_enabled=False, textvariable=self.down_bandwidth ) entry.grid(row=row, column=2, sticky="ew", pady=PADY) row = row + 1 label = ttk.Label(frame, text="Delay (us)") label.grid(row=row, column=0, sticky="ew") - entry = ttk.Entry( - frame, - textvariable=self.delay, - validate="key", - validatecommand=(self.app.validation.positive_int, "%P"), + entry = validation.PositiveIntEntry( + frame, empty_enabled=False, textvariable=self.delay ) entry.grid(row=row, column=1, sticky="ew", pady=PADY) if not self.is_symmetric: - entry = ttk.Entry( - frame, - textvariable=self.down_delay, - validate="key", - validatecommand=(self.app.validation.positive_int, "%P"), + entry = validation.PositiveIntEntry( + frame, empty_enabled=False, textvariable=self.down_delay ) entry.grid(row=row, column=2, sticky="ew", pady=PADY) row = row + 1 label = ttk.Label(frame, text="Jitter (us)") label.grid(row=row, column=0, sticky="ew") - entry = ttk.Entry( - frame, - textvariable=self.jitter, - validate="key", - validatecommand=(self.app.validation.positive_int, "%P"), + entry = validation.PositiveIntEntry( + frame, empty_enabled=False, textvariable=self.jitter ) entry.grid(row=row, column=1, sticky="ew", pady=PADY) if not self.is_symmetric: - entry = ttk.Entry( - frame, - textvariable=self.down_jitter, - validate="key", - validatecommand=(self.app.validation.positive_int, "%P"), + entry = validation.PositiveIntEntry( + frame, empty_enabled=False, textvariable=self.down_jitter ) entry.grid(row=row, column=2, sticky="ew", pady=PADY) row = row + 1 label = ttk.Label(frame, text="Loss (%)") label.grid(row=row, column=0, sticky="ew") - entry = ttk.Entry( - frame, - textvariable=self.loss, - validate="key", - validatecommand=(self.app.validation.positive_float, "%P"), + entry = validation.PositiveFloatEntry( + frame, empty_enabled=False, textvariable=self.loss ) entry.grid(row=row, column=1, sticky="ew", pady=PADY) if not self.is_symmetric: - entry = ttk.Entry( - frame, - textvariable=self.down_loss, - validate="key", - validatecommand=(self.app.validation.positive_float, "%P"), + entry = validation.PositiveFloatEntry( + frame, empty_enabled=False, textvariable=self.down_loss ) entry.grid(row=row, column=2, sticky="ew", pady=PADY) row = row + 1 label = ttk.Label(frame, text="Duplicate (%)") label.grid(row=row, column=0, sticky="ew") - entry = ttk.Entry( - frame, - textvariable=self.duplicate, - validate="key", - validatecommand=(self.app.validation.positive_int, "%P"), + entry = validation.PositiveIntEntry( + frame, empty_enabled=False, textvariable=self.duplicate ) entry.grid(row=row, column=1, sticky="ew", pady=PADY) if not self.is_symmetric: - entry = ttk.Entry( - frame, - textvariable=self.down_duplicate, - validate="key", - validatecommand=(self.app.validation.positive_int, "%P"), + entry = validation.PositiveIntEntry( + frame, empty_enabled=False, textvariable=self.down_duplicate ) entry.grid(row=row, column=2, sticky="ew", pady=PADY) row = row + 1 @@ -229,11 +200,8 @@ class LinkConfigurationDialog(Dialog): label = ttk.Label(frame, text="Width") label.grid(row=row, column=0, sticky="ew") - entry = ttk.Entry( - frame, - textvariable=self.width, - validate="key", - validatecommand=(self.app.validation.positive_float, "%P"), + entry = validation.PositiveFloatEntry( + frame, empty_enabled=False, textvariable=self.width ) entry.grid(row=row, column=1, sticky="ew", pady=PADY) diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index 85a839e5..73f0ac09 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING import netaddr -from core.gui import nodeutils +from core.gui import nodeutils, validation from core.gui.appconfig import ICONS_PATH from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.emaneconfig import EmaneModelDialog @@ -143,16 +143,7 @@ class NodeConfigDialog(Dialog): # name field label = ttk.Label(frame, text="Name") label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) - entry = ttk.Entry( - frame, - textvariable=self.name, - validate="key", - validatecommand=(self.app.validation.name, "%P"), - state=state, - ) - entry.bind( - "", lambda event: self.app.validation.focus_out(event, "noname") - ) + entry = validation.NodeNameEntry(frame, textvariable=self.name, state=state) entry.grid(row=row, column=1, sticky="ew") row += 1 diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index 9c9ba16f..11d1ba95 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -4,7 +4,7 @@ import tkinter as tk from tkinter import ttk from typing import TYPE_CHECKING -from core.gui import appconfig +from core.gui import appconfig, validation from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY, scale_fonts from core.gui.validation import LARGEST_SCALE, SMALLEST_SCALE @@ -80,12 +80,8 @@ class PreferencesDialog(Dialog): variable=self.gui_scale, ) scale.grid(row=0, column=0, sticky="ew") - entry = ttk.Entry( - scale_frame, - textvariable=self.gui_scale, - width=4, - validate="key", - validatecommand=(self.app.validation.app_scale, "%P"), + entry = validation.AppScaleEntry( + scale_frame, textvariable=self.gui_scale, width=4 ) entry.grid(row=0, column=1) diff --git a/daemon/core/gui/validation.py b/daemon/core/gui/validation.py index fee075ad..873db189 100644 --- a/daemon/core/gui/validation.py +++ b/daemon/core/gui/validation.py @@ -3,71 +3,63 @@ input validation """ import re import tkinter as tk -from typing import TYPE_CHECKING - -import netaddr -from netaddr import IPNetwork - -if TYPE_CHECKING: - from core.gui.app import Application +from tkinter import ttk SMALLEST_SCALE = 0.5 LARGEST_SCALE = 5.0 +HEX_REGEX = re.compile("^([#]([0-9]|[a-f])+)$|^[#]$") -class InputValidation: - def __init__(self, app: "Application"): - self.master = app.master - self.positive_int = None - self.positive_float = None - self.float = None - self.app_scale = None - self.name = None - self.ip4 = None - self.rgb = None - self.hex = None - self.register() +class ValidationEntry(ttk.Entry): + empty = None - def register(self): - self.positive_int = self.master.register(self.check_positive_int) - self.positive_float = self.master.register(self.check_positive_float) - self.float = self.master.register(self.check_float) - self.app_scale = self.master.register(self.check_scale_value) - self.name = self.master.register(self.check_node_name) - self.ip4 = self.master.register(self.check_ip4) - self.rgb = self.master.register(self.check_rbg) - self.hex = self.master.register(self.check_hex) + def __init__(self, master=None, widget=None, empty_enabled=True, **kwargs) -> None: + super().__init__(master, widget, **kwargs) + cmd = self.register(self.is_valid) + self.configure(validate="key", validatecommand=(cmd, "%P")) + if self.empty is not None and empty_enabled: + self.bind("", self.focus_out) - @classmethod - def ip_focus_out(cls, event: tk.Event): - value = event.widget.get() - try: - IPNetwork(value) - except netaddr.core.AddrFormatError: - event.widget.delete(0, tk.END) - event.widget.insert(tk.END, "invalid") + def is_valid(self, s: str) -> bool: + raise NotImplementedError - @classmethod - def focus_out(cls, event: tk.Event, default: str): - value = event.widget.get() - if value == "": - event.widget.insert(tk.END, default) + def focus_out(self, _event: tk.Event) -> None: + value = self.get() + if not value: + self.insert(tk.END, self.empty) - @classmethod - def check_positive_int(cls, s: str) -> bool: - if len(s) == 0: + +class PositiveIntEntry(ValidationEntry): + empty = "0" + + def is_valid(self, s: str) -> bool: + if not s: return True try: - int_value = int(s) - if int_value >= 0: - return True - return False + value = int(s) + return value >= 0 except ValueError: return False - @classmethod - def check_float(cls, s: str) -> bool: - if len(s) == 0: + +class PositiveFloatEntry(ValidationEntry): + empty = "0.0" + + def is_valid(self, s: str) -> bool: + if not s: + return True + try: + value = float(s) + return value >= 0.0 + except ValueError: + return False + + +class FloatEntry(ValidationEntry): + empty = "0.0" + + def is_valid(self, s: str) -> bool: + if not s: return True try: float(s) @@ -75,109 +67,50 @@ class InputValidation: except ValueError: return False - @classmethod - def check_positive_float(cls, s: str) -> bool: - if len(s) == 0: - return True - try: - float_value = float(s) - if float_value >= 0.0: - return True - return False - except ValueError: - return False - @classmethod - def check_node_name(cls, s: str) -> bool: - if len(s) < 0: - return False - if len(s) == 0: - return True - for char in s: - if not char.isalnum() and char != "_": - return False - return True - - @classmethod - def check_canvas_int(cls, s: str) -> bool: - if len(s) == 0: - return True - try: - int_value = int(s) - if int_value >= 0: - return True - return False - except ValueError: - return False - - @classmethod - def check_canvas_float(cls, s: str) -> bool: - if not s: - return True - try: - float_value = float(s) - if float_value >= 0.0: - return True - return False - except ValueError: - return False - - @classmethod - def check_scale_value(cls, s: str) -> bool: - if not s: - return True - try: - float_value = float(s) - if SMALLEST_SCALE <= float_value <= LARGEST_SCALE or float_value == 0: - return True - return False - except ValueError: - return False - - @classmethod - def check_ip4(cls, s: str) -> bool: - if not s: - return True - pat = re.compile("^([0-9]+[.])*[0-9]*$") - if pat.match(s) is not None: - _32bits = s.split(".") - if len(_32bits) > 4: - return False - for _8bits in _32bits: - if ( - (_8bits and int(_8bits) > 255) - or len(_8bits) > 3 - or (_8bits.startswith("0") and len(_8bits) > 1) - ): - return False - return True - else: - return False - - @classmethod - def check_rbg(cls, s: str) -> bool: +class RgbEntry(ValidationEntry): + def is_valid(self, s: str) -> bool: if not s: return True if s.startswith("0") and len(s) >= 2: return False try: value = int(s) - if 0 <= value <= 255: - return True - else: - return False + return 0 <= value <= 255 except ValueError: return False - @classmethod - def check_hex(cls, s: str) -> bool: + +class HexEntry(ValidationEntry): + def is_valid(self, s: str) -> bool: if not s: return True - pat = re.compile("^([#]([0-9]|[a-f])+)$|^[#]$") - if pat.match(s): - if 0 <= len(s) <= 7: - return True - else: - return False + if HEX_REGEX.match(s): + return 0 <= len(s) <= 7 else: return False + + +class NodeNameEntry(ValidationEntry): + empty = "noname" + + def is_valid(self, s: str) -> bool: + if len(s) < 0: + return False + if len(s) == 0: + return True + for x in s: + if not x.isalnum() and x != "_": + return False + return True + + +class AppScaleEntry(ValidationEntry): + def is_valid(self, s: str) -> bool: + if not s: + return True + try: + float_value = float(s) + return SMALLEST_SCALE <= float_value <= LARGEST_SCALE or float_value == 0 + except ValueError: + return False diff --git a/daemon/core/gui/widgets.py b/daemon/core/gui/widgets.py index 5750e286..6f51bd8c 100644 --- a/daemon/core/gui/widgets.py +++ b/daemon/core/gui/widgets.py @@ -6,7 +6,7 @@ from tkinter import filedialog, font, ttk from typing import TYPE_CHECKING, Dict from core.api.grpc import common_pb2, core_pb2 -from core.gui import themes +from core.gui import themes, validation from core.gui.themes import FRAME_PAD, PADX, PADY if TYPE_CHECKING: @@ -127,43 +127,16 @@ class ConfigFrame(ttk.Notebook): button = ttk.Button(file_frame, text="...", command=func) button.grid(row=0, column=1) else: - if "controlnet" in option.name and "script" not in option.name: - entry = ttk.Entry( - tab.frame, - textvariable=value, - validate="key", - validatecommand=(self.app.validation.ip4, "%P"), - ) - entry.grid(row=index, column=1, sticky="ew") - else: - entry = ttk.Entry(tab.frame, textvariable=value) - entry.grid(row=index, column=1, sticky="ew") + entry = ttk.Entry(tab.frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew") elif option.type in INT_TYPES: value.set(option.value) - entry = ttk.Entry( - tab.frame, - textvariable=value, - validate="key", - validatecommand=(self.app.validation.positive_int, "%P"), - ) - entry.bind( - "", - lambda event: self.app.validation.focus_out(event, "0"), - ) + entry = validation.PositiveIntEntry(tab.frame, textvariable=value) entry.grid(row=index, column=1, sticky="ew") elif option.type == core_pb2.ConfigOptionType.FLOAT: value.set(option.value) - entry = ttk.Entry( - tab.frame, - textvariable=value, - validate="key", - validatecommand=(self.app.validation.positive_float, "%P"), - ) - entry.bind( - "", - lambda event: self.app.validation.focus_out(event, "0"), - ) + entry = validation.PositiveFloatEntry(tab.frame, textvariable=value) entry.grid(row=index, column=1, sticky="ew") else: logging.error("unhandled config option type: %s", option.type) From fa163c3ed6721be2ec1a91d8c9e71f9239c76c72 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 12 May 2020 08:31:53 -0700 Subject: [PATCH 0251/1131] pygui: update file dialogs to hide hidden files by default and provide a hidden file toggle --- daemon/core/gui/app.py | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 73aabb17..90e5c36c 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -21,8 +21,8 @@ HEIGHT = 800 class Application(ttk.Frame): - def __init__(self, proxy: bool): - super().__init__(master=None) + def __init__(self, proxy: bool) -> None: + super().__init__() # load node icons NodeUtils.setup() @@ -50,7 +50,7 @@ class Application(ttk.Frame): self.draw() self.core.setup() - def setup_scaling(self): + def setup_scaling(self) -> None: self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()} text_scale = self.app_scale if self.app_scale < 1 else math.sqrt(self.app_scale) themes.scale_fonts(self.fonts_size, self.app_scale) @@ -59,21 +59,37 @@ class Application(ttk.Frame): family="TkDefaultFont", size=int(8 * text_scale), weight=font.BOLD ) - def setup_theme(self): + def setup_theme(self) -> None: themes.load(self.style) self.master.bind_class("Menu", "<>", themes.theme_change_menu) self.master.bind("<>", themes.theme_change) self.style.theme_use(self.guiconfig.preferences.theme) - def setup_app(self): + def setup_app(self) -> None: self.master.title("CORE") self.center() self.master.protocol("WM_DELETE_WINDOW", self.on_closing) image = Images.get(ImageEnum.CORE, 16) self.master.tk.call("wm", "iconphoto", self.master._w, image) self.master.option_add("*tearOff", tk.FALSE) + self.setup_file_dialogs() - def center(self): + def setup_file_dialogs(self) -> None: + """ + Hack code that needs to initialize a bad dialog so that we can apply, + global settings for dialogs to not show hidden files by default and display + the hidden file toggle. + + :return: nothing + """ + try: + self.master.tk.call("tk_getOpenFile", "-foobar") + except tk.TclError: + pass + self.master.tk.call("set", "::tk::dialog::file::showHiddenBtn", "1") + self.master.tk.call("set", "::tk::dialog::file::showHiddenVar", "0") + + def center(self) -> None: screen_width = self.master.winfo_screenwidth() screen_height = self.master.winfo_screenheight() x = int((screen_width / 2) - (WIDTH * self.app_scale / 2)) @@ -82,7 +98,7 @@ class Application(ttk.Frame): f"{int(WIDTH * self.app_scale)}x{int(HEIGHT * self.app_scale)}+{x}+{y}" ) - def draw(self): + def draw(self) -> None: self.master.rowconfigure(0, weight=1) self.master.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) @@ -99,7 +115,7 @@ class Application(ttk.Frame): self.progress = Progressbar(self.right_frame, mode="indeterminate") self.menubar = Menubar(self.master, self) - def draw_canvas(self): + def draw_canvas(self) -> None: width = self.guiconfig.preferences.width height = self.guiconfig.preferences.height canvas_frame = ttk.Frame(self.right_frame) @@ -117,7 +133,7 @@ class Application(ttk.Frame): self.canvas.configure(xscrollcommand=scroll_x.set) self.canvas.configure(yscrollcommand=scroll_y.set) - def draw_status(self): + def draw_status(self) -> None: self.statusbar = StatusBar(self.right_frame, self) self.statusbar.grid(sticky="ew") @@ -133,17 +149,17 @@ class Application(ttk.Frame): def show_error(self, title: str, message: str) -> None: self.after(0, lambda: ErrorDialog(self, title, message).show()) - def on_closing(self): + def on_closing(self) -> None: self.menubar.prompt_save_running_session(True) - def save_config(self): + def save_config(self) -> None: appconfig.save(self.guiconfig) - def joined_session_update(self): + def joined_session_update(self) -> None: if self.core.is_runtime(): self.toolbar.set_runtime() else: self.toolbar.set_design() - def close(self): + def close(self) -> None: self.master.destroy() From 454dc8091ec40016ef693772830e6ae781cb84f3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 13 May 2020 09:25:56 -0700 Subject: [PATCH 0252/1131] coresendmsg: small usage cleanup, removed printing enum values when listing tlvs, updated examples to use current expected values --- daemon/scripts/coresendmsg | 45 ++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/daemon/scripts/coresendmsg b/daemon/scripts/coresendmsg index a909522f..9f5fa776 100755 --- a/daemon/scripts/coresendmsg +++ b/daemon/scripts/coresendmsg @@ -19,7 +19,7 @@ def print_available_tlvs(t, tlv_class): """ print(f"TLVs available for {t} message:") for tlv in sorted([tlv for tlv in tlv_class.tlv_type_map], key=lambda x: x.name): - print(f"{tlv.value}:{tlv.name}") + print(tlv.name) def print_examples(name): @@ -27,27 +27,26 @@ def print_examples(name): Print example usage of this script. """ examples = [ - ("link n1number=2 n2number=3 delay=15000", - "set a 15ms delay on the link between n2 and n3"), - ("link n1number=2 n2number=3 guiattr=\"color=blue\"", - "change the color of the link between n2 and n3"), - ("node number=3 xpos=125 ypos=525", + ("NODE NUMBER=3 X_POSITION=125 Y_POSITION=525", "move node number 3 to x,y=(125,525)"), - ("node number=4 icon=/usr/local/share/core/icons/normal/router_red.gif", + ("NODE NUMBER=4 ICON=/usr/local/share/core/icons/normal/router_red.gif", "change node number 4\"s icon to red"), - ("node flags=add number=5 type=0 name=\"n5\" xpos=500 ypos=500", + ("NODE flags=ADD NUMBER=5 TYPE=0 NAME=\"n5\" X_POSITION=500 Y_POSITION=500", "add a new router node n5"), - ("link flags=add n1number=4 n2number=5 if1ip4=\"10.0.3.2\" " \ - "if1ip4mask=24 if2ip4=\"10.0.3.1\" if2ip4mask=24", + ("LINK N1_NUMBER=2 N2_NUMBER=3 DELAY=15000", + "set a 15ms delay on the link between n2 and n3"), + ("LINK N1_NUMBER=2 N2_NUMBER=3 GUI_ATTRIBUTES=\"color=blue\"", + "change the color of the link between n2 and n3"), + ("LINK flags=ADD N1_NUMBER=4 N2_NUMBER=5 INTERFACE1_IP4=\"10.0.3.2\" " + "INTERFACE1_IP4_MASK=24 INTERFACE2_IP4=\"10.0.3.1\" INTERFACE2_IP4_MASK=24", "link node n5 with n4 using the given interface addresses"), - ("exec flags=str,txt node=1 num=1000 cmd=\"uname -a\" -l", + ("EXECUTE flags=STRING,TEXT NODE=1 NUMBER=1000 COMMAND=\"uname -a\" -l", "run a command on node 1 and wait for the result"), - ("exec node=2 num=1001 cmd=\"killall ospfd\"", + ("EXECUTE NODE=2 NUMBER=1001 COMMAND=\"killall ospfd\"", "run a command on node 2 and ignore the result"), - ("file flags=add node=1 name=\"/var/log/test.log\" data=\"Hello World.\"", + ("FILE flags=ADD NODE=1 NAME=\"/var/log/test.log\" DATA=\"Hello World.\"", "write a test.log file on node 1 with the given contents"), - ("file flags=add node=2 name=\"test.log\" " \ - "srcname=\"./test.log\"", + ("FILE flags=ADD NODE=2 NAME=\"test.log\" SOURCE_NAME=\"./test.log\"", "move a test.log file from host to node 2"), ] print(f"Example {name} invocations:") @@ -154,10 +153,14 @@ def main(): """ types = [message_type.name for message_type in MessageTypes] flags = [flag.name for flag in MessageFlags] - usagestr = "usage: %prog [-h|-H] [options] [message-type] [flags=flags] " - usagestr += "[message-TLVs]\n\n" - usagestr += f"Supported message types:\n {types}\n" - usagestr += f"Supported message flags (flags=f1,f2,...):\n {flags}" + types_usage = " ".join(types) + flags_usage = " ".join(flags) + usagestr = ( + "usage: %prog [-h|-H] [options] [message-type] [flags=flags] " + "[message-TLVs]\n\n" + f"Supported message types:\n {types_usage}\n" + f"Supported message flags (flags=f1,f2,...):\n {flags_usage}" + ) parser = optparse.OptionParser(usage=usagestr) default_address = "localhost" default_session = None @@ -188,9 +191,9 @@ def main(): help=f"Use TCP instead of UDP and connect to a session default: {default_tcp}") def usage(msg=None, err=0): - sys.stdout.write("\n") + print() if msg: - sys.stdout.write(msg + "\n\n") + print(f"{msg}\n") parser.print_help() sys.exit(err) From 95d3a6ca8ca95a4a6fa36fe2dc089ede46f50146 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 13 May 2020 12:01:28 -0700 Subject: [PATCH 0253/1131] updates to force CoreCommandError to contain string values for stderr and stdout, couple bugfixes in handling bad commands when using execute commands from tlv based api or coresendmsg, also updates to coresendmsg to display everything in lowercase to mimic previous look and feel, however coresendmg will now work regardless of casing to avoid breaking things again --- daemon/core/api/tlv/corehandlers.py | 7 +---- daemon/core/emane/emanemanager.py | 1 - daemon/core/nodes/base.py | 1 - daemon/core/nodes/docker.py | 2 +- daemon/core/nodes/lxd.py | 2 +- daemon/core/utils.py | 9 ++++-- daemon/scripts/coresendmsg | 43 ++++++++++++++--------------- 7 files changed, 29 insertions(+), 36 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 76bc16fe..d7e41a6c 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -880,12 +880,7 @@ class CoreHandler(socketserver.BaseRequestHandler): except CoreCommandError as e: res = e.stderr status = e.returncode - logging.info( - "done exec cmd=%s with status=%d res=(%d bytes)", - command, - status, - len(res), - ) + logging.info("done exec cmd=%s with status=%d", command, status) if message.flags & MessageFlags.TEXT.value: tlv_data += coreapi.CoreExecuteTlv.pack( ExecuteTlvs.RESULT.value, res diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 0b4ae891..8b4bade2 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -836,7 +836,6 @@ class EmaneManager(ModelManager): result = True except CoreCommandError: result = False - return result diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 721f643f..2749323a 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -522,7 +522,6 @@ class CoreNode(CoreNodeBase): self.host_cmd(f"kill -0 {self.pid}") except CoreCommandError: return False - return True def startup(self) -> None: diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index 60adfe32..f1335747 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -35,7 +35,7 @@ class DockerClient: output = self.run(args) data = json.loads(output) if not data: - raise CoreCommandError(-1, args, f"docker({self.name}) not present") + raise CoreCommandError(1, args, f"docker({self.name}) not present") return data[0] def is_alive(self) -> bool: diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index 3ca399b5..31623394 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -34,7 +34,7 @@ class LxdClient: output = self.run(args) data = json.loads(output) if not data: - raise CoreCommandError(-1, args, f"LXC({self.name}) not present") + raise CoreCommandError(1, args, f"LXC({self.name}) not present") return data[0] def is_alive(self) -> bool: diff --git a/daemon/core/utils.py b/daemon/core/utils.py index f1f74dbe..8a988ede 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -231,14 +231,17 @@ def cmd( p = Popen(args, stdout=PIPE, stderr=PIPE, env=env, cwd=cwd, shell=shell) if wait: stdout, stderr = p.communicate() + stdout = stdout.decode("utf-8").strip() + stderr = stderr.decode("utf-8").strip() status = p.wait() if status != 0: raise CoreCommandError(status, args, stdout, stderr) - return stdout.decode("utf-8").strip() + return stdout else: return "" - except OSError: - raise CoreCommandError(-1, args) + except OSError as e: + logging.error("cmd error: %s", e.strerror) + raise CoreCommandError(1, args, "", e.strerror) def file_munge(pathname: str, header: str, text: str) -> None: diff --git a/daemon/scripts/coresendmsg b/daemon/scripts/coresendmsg index 9f5fa776..ae89ecb1 100755 --- a/daemon/scripts/coresendmsg +++ b/daemon/scripts/coresendmsg @@ -19,7 +19,7 @@ def print_available_tlvs(t, tlv_class): """ print(f"TLVs available for {t} message:") for tlv in sorted([tlv for tlv in tlv_class.tlv_type_map], key=lambda x: x.name): - print(tlv.name) + print(tlv.name.lower()) def print_examples(name): @@ -27,26 +27,26 @@ def print_examples(name): Print example usage of this script. """ examples = [ - ("NODE NUMBER=3 X_POSITION=125 Y_POSITION=525", + ("node number=3 x_position=125 y_position=525", "move node number 3 to x,y=(125,525)"), - ("NODE NUMBER=4 ICON=/usr/local/share/core/icons/normal/router_red.gif", + ("node number=4 icon=/usr/local/share/core/icons/normal/router_red.gif", "change node number 4\"s icon to red"), - ("NODE flags=ADD NUMBER=5 TYPE=0 NAME=\"n5\" X_POSITION=500 Y_POSITION=500", + ("node flags=add number=5 type=0 name=\"n5\" x_position=500 y_position=500", "add a new router node n5"), - ("LINK N1_NUMBER=2 N2_NUMBER=3 DELAY=15000", + ("link n1_number=2 n2_number=3 delay=15000", "set a 15ms delay on the link between n2 and n3"), - ("LINK N1_NUMBER=2 N2_NUMBER=3 GUI_ATTRIBUTES=\"color=blue\"", + ("link n1_number=2 n2_number=3 gui_attributes=\"color=blue\"", "change the color of the link between n2 and n3"), - ("LINK flags=ADD N1_NUMBER=4 N2_NUMBER=5 INTERFACE1_IP4=\"10.0.3.2\" " - "INTERFACE1_IP4_MASK=24 INTERFACE2_IP4=\"10.0.3.1\" INTERFACE2_IP4_MASK=24", + ("link flags=add n1_number=4 n2_number=5 interface1_ip4=\"10.0.3.2\" " + "interface1_ip4_mask=24 interface2_ip4=\"10.0.3.1\" interface2_ip4_mask=24", "link node n5 with n4 using the given interface addresses"), - ("EXECUTE flags=STRING,TEXT NODE=1 NUMBER=1000 COMMAND=\"uname -a\" -l", + ("execute flags=string,text node=1 number=1000 command=\"uname -a\" -l", "run a command on node 1 and wait for the result"), - ("EXECUTE NODE=2 NUMBER=1001 COMMAND=\"killall ospfd\"", + ("execute node=2 number=1001 command=\"killall ospfd\"", "run a command on node 2 and ignore the result"), - ("FILE flags=ADD NODE=1 NAME=\"/var/log/test.log\" DATA=\"Hello World.\"", + ("file flags=add node=1 name=\"/var/log/test.log\" data=\"hello world.\"", "write a test.log file on node 1 with the given contents"), - ("FILE flags=ADD NODE=2 NAME=\"test.log\" SOURCE_NAME=\"./test.log\"", + ("file flags=add node=2 name=\"test.log\" source_name=\"./test.log\"", "move a test.log file from host to node 2"), ] print(f"Example {name} invocations:") @@ -151,8 +151,8 @@ def main(): """ Parse command-line arguments to build and send a CORE message. """ - types = [message_type.name for message_type in MessageTypes] - flags = [flag.name for flag in MessageFlags] + types = [message_type.name.lower() for message_type in MessageTypes] + flags = [flag.name.lower() for flag in MessageFlags] types_usage = " ".join(types) flags_usage = " ".join(flags) usagestr = ( @@ -174,7 +174,6 @@ def main(): tlvs=False, tcp=default_tcp ) - parser.add_option("-H", dest="examples", action="store_true", help="show example usage help message and exit") parser.add_option("-p", "--port", dest="port", type=int, @@ -207,9 +206,10 @@ def main(): # given a message type t, determine the message and TLV classes t = args.pop(0) + t = t.lower() if t not in types: usage(f"Unknown message type requested: {t}") - message_type = MessageTypes[t] + message_type = MessageTypes[t.upper()] msg_cls = coreapi.CLASS_MAP[message_type.value] tlv_cls = msg_cls.tlv_class @@ -225,26 +225,23 @@ def main(): typevalue = a.split("=") if len(typevalue) < 2: usage(f"Use \"type=value\" syntax instead of \"{a}\".") - tlv_typestr = typevalue[0] + tlv_typestr = typevalue[0].lower() tlv_valstr = "=".join(typevalue[1:]) if tlv_typestr == "flags": flagstr = tlv_valstr continue - - tlv_name = tlv_typestr try: - tlv_type = tlv_cls.tlv_type_map[tlv_name] + tlv_type = tlv_cls.tlv_type_map[tlv_typestr.upper()] tlvdata += tlv_cls.pack_string(tlv_type.value, tlv_valstr) except KeyError: - usage(f"Unknown TLV: \"{tlv_name}\"") + usage(f"Unknown TLV: \"{tlv_typestr}\"") flags = 0 for f in flagstr.split(","): if f == "": continue - try: - flag_enum = MessageFlags[f] + flag_enum = MessageFlags[f.upper()] n = flag_enum.value flags |= n except KeyError: From 79d7d66bff0b7704b70571edae36fdf7bba587a8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 13 May 2020 16:16:45 -0700 Subject: [PATCH 0254/1131] updates to sample3-bgp to avoid issues related to older formatted imn --- gui/configs/sample3-bgp.imn | 97 ------------------------------------- 1 file changed, 97 deletions(-) diff --git a/gui/configs/sample3-bgp.imn b/gui/configs/sample3-bgp.imn index d4a396ae..b31693ef 100644 --- a/gui/configs/sample3-bgp.imn +++ b/gui/configs/sample3-bgp.imn @@ -46,18 +46,6 @@ node n1 { ! } } - custom-config { - custom-config-id service:zebra - custom-command zebra - config { - ('/usr/local/etc/quagga', '/var/run/quagga') - ('/usr/local/etc/quagga/Quagga.conf', 'quaggaboot.sh') - 35 - ('sh quaggaboot.sh zebra',) - ('killall zebra',) - - } - } } node n2 { @@ -108,18 +96,6 @@ node n2 { ! } } - custom-config { - custom-config-id service:zebra - custom-command zebra - config { - ('/usr/local/etc/quagga', '/var/run/quagga') - ('/usr/local/etc/quagga/Quagga.conf', 'quaggaboot.sh') - 35 - ('sh quaggaboot.sh zebra',) - ('killall zebra',) - - } - } } node n3 { @@ -161,18 +137,6 @@ node n3 { ! } } - custom-config { - custom-config-id service:zebra - custom-command zebra - config { - ('/usr/local/etc/quagga', '/var/run/quagga') - ('/usr/local/etc/quagga/Quagga.conf', 'quaggaboot.sh') - 35 - ('sh quaggaboot.sh zebra',) - ('killall zebra',) - - } - } } node n4 { @@ -222,18 +186,6 @@ node n4 { ! } } - custom-config { - custom-config-id service:zebra - custom-command zebra - config { - ('/usr/local/etc/quagga', '/var/run/quagga') - ('/usr/local/etc/quagga/Quagga.conf', 'quaggaboot.sh') - 35 - ('sh quaggaboot.sh zebra',) - ('killall zebra',) - - } - } } node n5 { @@ -283,18 +235,6 @@ node n5 { ! } } - custom-config { - custom-config-id service:zebra - custom-command zebra - config { - ('/usr/local/etc/quagga', '/var/run/quagga') - ('/usr/local/etc/quagga/Quagga.conf', 'quaggaboot.sh') - 35 - ('sh quaggaboot.sh zebra',) - ('killall zebra',) - - } - } } node n6 { @@ -344,18 +284,6 @@ node n6 { ! } } - custom-config { - custom-config-id service:zebra - custom-command zebra - config { - ('/usr/local/etc/quagga', '/var/run/quagga') - ('/usr/local/etc/quagga/Quagga.conf', 'quaggaboot.sh') - 35 - ('sh quaggaboot.sh zebra',) - ('killall zebra',) - - } - } } node n7 { @@ -397,18 +325,6 @@ node n7 { ! } } - custom-config { - custom-config-id service:zebra - custom-command zebra - config { - ('/usr/local/etc/quagga', '/var/run/quagga') - ('/usr/local/etc/quagga/Quagga.conf', 'quaggaboot.sh') - 35 - ('sh quaggaboot.sh zebra',) - ('killall zebra',) - - } - } } node n8 { @@ -576,18 +492,6 @@ node n16 { ! } } - custom-config { - custom-config-id service:zebra - custom-command zebra - config { - ('/usr/local/etc/quagga', '/var/run/quagga') - ('/usr/local/etc/quagga/Quagga.conf', 'quaggaboot.sh') - 35 - ('sh quaggaboot.sh zebra',) - ('killall zebra',) - - } - } } link l0 { @@ -751,4 +655,3 @@ option global { annotations yes grid yes } - From 433fe4ae580f02c8e4c08f310e50d9357e094e82 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 13 May 2020 23:59:00 -0700 Subject: [PATCH 0255/1131] pygui: removed undesired logging in interface manager --- daemon/core/gui/interface.py | 1 - 1 file changed, 1 deletion(-) diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index 9df1f667..fc5185f5 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -147,7 +147,6 @@ class InterfaceManager: return str(ip4), str(ip6) def get_subnets(self, interface: "core_pb2.Interface") -> Subnets: - logging.info("get subnets for interface: %s", interface) ip4_subnet = self.ip4_subnets if interface.ip4: ip4_subnet = IPNetwork(f"{interface.ip4}/{interface.ip4mask}").cidr From df03f1e173cde193ad01248fa6180e3e37fa810f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 14 May 2020 16:24:22 -0700 Subject: [PATCH 0256/1131] pygui: improvements to handling grpc events and updating gui --- daemon/core/gui/coreclient.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index bc9cdc37..a2a114ea 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -4,9 +4,10 @@ Incorporate grpc into python tkinter GUI import json import logging import os +from functools import partial from pathlib import Path from tkinter import messagebox -from typing import TYPE_CHECKING, Dict, Iterable, List, Optional +from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Optional import grpc @@ -84,7 +85,7 @@ class CoreClient: self.cancel_events() self._client.create_session(self.session_id) self.handling_events = self._client.events( - self.session_id, self.handle_events + self.session_id, self.handle_stream(self.handle_events) ) if throughputs_enabled: self.enable_throughputs() @@ -126,6 +127,9 @@ class CoreClient: for observer in self.app.guiconfig.observers: self.custom_observers[observer.name] = observer + def handle_stream(self, func: Callable) -> Callable: + return partial(self.app.after, 0, func) + def handle_events(self, event: core_pb2.Event): if event.session_id != self.session_id: logging.warning( @@ -199,7 +203,7 @@ class CoreClient: def enable_throughputs(self): self.handling_throughputs = self.client.throughputs( - self.session_id, self.handle_throughputs + self.session_id, self.handle_stream(self.handle_throughputs) ) def cancel_throughputs(self): From 3b1a9bc3e306839b678b770ad5e802eac51112e4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 14 May 2020 17:57:32 -0700 Subject: [PATCH 0257/1131] pygui: changes to improve grpc event handling --- daemon/core/gui/coreclient.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index a2a114ea..e882227c 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -4,10 +4,9 @@ Incorporate grpc into python tkinter GUI import json import logging import os -from functools import partial from pathlib import Path from tkinter import messagebox -from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Optional +from typing import TYPE_CHECKING, Dict, Iterable, List, Optional import grpc @@ -85,7 +84,7 @@ class CoreClient: self.cancel_events() self._client.create_session(self.session_id) self.handling_events = self._client.events( - self.session_id, self.handle_stream(self.handle_events) + self.session_id, self.handle_events ) if throughputs_enabled: self.enable_throughputs() @@ -127,9 +126,6 @@ class CoreClient: for observer in self.app.guiconfig.observers: self.custom_observers[observer.name] = observer - def handle_stream(self, func: Callable) -> Callable: - return partial(self.app.after, 0, func) - def handle_events(self, event: core_pb2.Event): if event.session_id != self.session_id: logging.warning( @@ -140,7 +136,7 @@ class CoreClient: return if event.HasField("link_event"): - self.handle_link_event(event.link_event) + self.app.after(0, self.handle_link_event, event.link_event) elif event.HasField("session_event"): logging.info("session event: %s", event) session_event = event.session_event @@ -159,7 +155,7 @@ class CoreClient: else: logging.warning("unknown session event: %s", session_event) elif event.HasField("node_event"): - self.handle_node_event(event.node_event) + self.app.after(0, self.handle_node_event, event.node_event) elif event.HasField("config_event"): logging.info("config event: %s", event) elif event.HasField("exception_event"): @@ -203,7 +199,7 @@ class CoreClient: def enable_throughputs(self): self.handling_throughputs = self.client.throughputs( - self.session_id, self.handle_stream(self.handle_throughputs) + self.session_id, self.handle_throughputs ) def cancel_throughputs(self): @@ -225,7 +221,7 @@ class CoreClient: ) return logging.debug("handling throughputs event: %s", event) - self.app.canvas.set_throughputs(event) + self.app.after(0, self.app.canvas.set_throughputs, event) def handle_exception_event(self, event: core_pb2.ExceptionEvent): logging.info("exception event: %s", event) From 5e69ea48b3cc4091102a5f41f9c1c3b3eb4725fb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 14 May 2020 23:07:21 -0700 Subject: [PATCH 0258/1131] pygui: fixed tracking for throughputs when joining a session --- daemon/core/gui/graph/graph.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 220e122f..2920f9b0 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -327,9 +327,15 @@ class CanvasGraph(tk.Canvas): self.edges[edge.token] = edge self.core.links[edge.token] = edge if link.HasField("interface_one"): + self.core.interface_to_edge[ + (node_one.id, link.interface_one.id) + ] = token canvas_node_one.interfaces.append(link.interface_one) edge.src_interface = link.interface_one if link.HasField("interface_two"): + self.core.interface_to_edge[ + (node_two.id, link.interface_two.id) + ] = edge.token canvas_node_two.interfaces.append(link.interface_two) edge.dst_interface = link.interface_two elif link.options.unidirectional: From ee5d5b98640e2dd52106cdae9d802aba3de47a55 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 15 May 2020 11:41:18 -0700 Subject: [PATCH 0259/1131] pygui: removed duplicate get_icon functionality, added more type hints, added enable/disable of toolbar button when running start/stop --- daemon/core/gui/toolbar.py | 116 ++++++++++++++++++++----------------- 1 file changed, 62 insertions(+), 54 deletions(-) diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 01a6bc1b..46b70bee 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -5,6 +5,8 @@ from functools import partial from tkinter import ttk from typing import TYPE_CHECKING, Callable +from PIL.ImageTk import PhotoImage + from core.api.grpc import core_pb2 from core.gui.dialogs.marker import MarkerDialog from core.gui.dialogs.runtool import RunToolDialog @@ -18,7 +20,6 @@ from core.gui.tooltip import Tooltip if TYPE_CHECKING: from core.gui.app import Application - from PIL import ImageTk TOOLBAR_SIZE = 32 PICKER_SIZE = 24 @@ -30,8 +31,10 @@ class NodeTypeEnum(Enum): OTHER = 2 -def icon(image_enum, width=TOOLBAR_SIZE): - return Images.get(image_enum, width) +def enable_buttons(frame: ttk.Frame, enabled: bool) -> None: + state = tk.NORMAL if enabled else tk.DISABLED + for child in frame.winfo_children(): + child.configure(state=state) class Toolbar(ttk.Frame): @@ -39,7 +42,7 @@ class Toolbar(ttk.Frame): Core toolbar class """ - def __init__(self, master: tk.Widget, app: "Application", **kwargs): + def __init__(self, master: tk.Widget, app: "Application", **kwargs) -> None: """ Create a CoreToolbar instance """ @@ -71,7 +74,7 @@ class Toolbar(ttk.Frame): self.marker_tool = None # these variables help keep track of what images being drawn so that scaling - # is possible since ImageTk.PhotoImage does not have resize method + # is possible since PhotoImage does not have resize method self.node_enum = None self.network_enum = None self.annotation_enum = None @@ -79,35 +82,35 @@ class Toolbar(ttk.Frame): # draw components self.draw() - def get_icon(self, image_enum, width=TOOLBAR_SIZE): + def get_icon(self, image_enum: ImageEnum, width: int) -> PhotoImage: return Images.get(image_enum, int(width * self.app.app_scale)) - def draw(self): + def draw(self) -> None: self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.draw_design_frame() self.draw_runtime_frame() self.design_frame.tkraise() - def draw_design_frame(self): + def draw_design_frame(self) -> None: self.design_frame = ttk.Frame(self) self.design_frame.grid(row=0, column=0, sticky="nsew") self.design_frame.columnconfigure(0, weight=1) self.play_button = self.create_button( self.design_frame, - self.get_icon(ImageEnum.START), + self.get_icon(ImageEnum.START, TOOLBAR_SIZE), self.click_start, "start the session", ) self.select_button = self.create_button( self.design_frame, - self.get_icon(ImageEnum.SELECT), + self.get_icon(ImageEnum.SELECT, TOOLBAR_SIZE), self.click_selection, "selection tool", ) self.link_button = self.create_button( self.design_frame, - self.get_icon(ImageEnum.LINK), + self.get_icon(ImageEnum.LINK, TOOLBAR_SIZE), self.click_link, "link tool", ) @@ -115,7 +118,7 @@ class Toolbar(ttk.Frame): self.create_network_button() self.create_annotation_button() - def design_select(self, button: ttk.Button): + def design_select(self, button: ttk.Button) -> None: logging.debug("selecting design button: %s", button) self.select_button.state(["!pressed"]) self.link_button.state(["!pressed"]) @@ -124,7 +127,7 @@ class Toolbar(ttk.Frame): self.annotation_button.state(["!pressed"]) button.state(["pressed"]) - def runtime_select(self, button: ttk.Button): + def runtime_select(self, button: ttk.Button) -> None: logging.debug("selecting runtime button: %s", button) self.runtime_select_button.state(["!pressed"]) self.stop_button.state(["!pressed"]) @@ -132,33 +135,36 @@ class Toolbar(ttk.Frame): self.run_command_button.state(["!pressed"]) button.state(["pressed"]) - def draw_runtime_frame(self): + def draw_runtime_frame(self) -> None: self.runtime_frame = ttk.Frame(self) self.runtime_frame.grid(row=0, column=0, sticky="nsew") self.runtime_frame.columnconfigure(0, weight=1) self.stop_button = self.create_button( self.runtime_frame, - self.get_icon(ImageEnum.STOP), + self.get_icon(ImageEnum.STOP, TOOLBAR_SIZE), self.click_stop, "stop the session", ) self.runtime_select_button = self.create_button( self.runtime_frame, - self.get_icon(ImageEnum.SELECT), + self.get_icon(ImageEnum.SELECT, TOOLBAR_SIZE), self.click_runtime_selection, "selection tool", ) self.runtime_marker_button = self.create_button( self.runtime_frame, - icon(ImageEnum.MARKER), + self.get_icon(ImageEnum.MARKER, TOOLBAR_SIZE), self.click_marker_button, "marker", ) self.run_command_button = self.create_button( - self.runtime_frame, icon(ImageEnum.RUN), self.click_run_button, "run" + self.runtime_frame, + self.get_icon(ImageEnum.RUN, TOOLBAR_SIZE), + self.click_run_button, + "run", ) - def draw_node_picker(self): + def draw_node_picker(self) -> None: self.hide_pickers() self.node_picker = ttk.Frame(self.master) # draw default nodes @@ -197,7 +203,7 @@ class Toolbar(ttk.Frame): 0, lambda: self.show_picker(self.node_button, self.node_picker) ) - def show_picker(self, button: ttk.Button, picker: ttk.Frame): + def show_picker(self, button: ttk.Button, picker: ttk.Frame) -> None: x = self.winfo_width() + 1 y = button.winfo_rooty() - picker.master.winfo_rooty() - 1 picker.place(x=x, y=y) @@ -208,8 +214,8 @@ class Toolbar(ttk.Frame): self.app.unbind_all("") def create_picker_button( - self, image: "ImageTk.PhotoImage", func: Callable, frame: ttk.Frame, label: str - ): + self, image: PhotoImage, func: Callable, frame: ttk.Frame, label: str + ) -> None: """ Create button and put it on the frame @@ -226,58 +232,58 @@ class Toolbar(ttk.Frame): button.grid(pady=1) def create_button( - self, - frame: ttk.Frame, - image: "ImageTk.PhotoImage", - func: Callable, - tooltip: str, - ): + self, frame: ttk.Frame, image: PhotoImage, func: Callable, tooltip: str + ) -> ttk.Button: button = ttk.Button(frame, image=image, command=func) button.image = image button.grid(sticky="ew") Tooltip(button, tooltip) return button - def click_selection(self): + def click_selection(self) -> None: logging.debug("clicked selection tool") self.design_select(self.select_button) self.app.canvas.mode = GraphMode.SELECT - def click_runtime_selection(self): + def click_runtime_selection(self) -> None: logging.debug("clicked selection tool") self.runtime_select(self.runtime_select_button) self.app.canvas.mode = GraphMode.SELECT - def click_start(self): + def click_start(self) -> None: """ Start session handler redraw buttons, send node and link messages to grpc server. """ self.app.menubar.change_menubar_item_state(is_runtime=True) self.app.canvas.mode = GraphMode.SELECT + enable_buttons(self.design_frame, enabled=False) task = ProgressTask( self.app, "Start", self.app.core.start_session, self.start_callback ) task.start() - def start_callback(self, response: core_pb2.StartSessionResponse): + def start_callback(self, response: core_pb2.StartSessionResponse) -> None: if response.result: self.set_runtime() self.app.core.set_metadata() self.app.core.show_mobility_players() else: + enable_buttons(self.design_frame, enabled=True) message = "\n".join(response.exceptions) self.app.show_error("Start Session Error", message) - def set_runtime(self): + def set_runtime(self) -> None: + enable_buttons(self.runtime_frame, enabled=True) self.runtime_frame.tkraise() self.click_runtime_selection() - def set_design(self): + def set_design(self) -> None: + enable_buttons(self.design_frame, enabled=True) self.design_frame.tkraise() self.click_selection() - def click_link(self): + def click_link(self) -> None: logging.debug("Click LINK button") self.design_select(self.link_button) self.app.canvas.mode = GraphMode.EDGE @@ -285,11 +291,11 @@ class Toolbar(ttk.Frame): def update_button( self, button: ttk.Button, - image: "ImageTk", + image: PhotoImage, node_draw: NodeDraw, type_enum, image_enum, - ): + ) -> None: logging.debug("update button(%s): %s", button, node_draw) self.hide_pickers() button.configure(image=image) @@ -301,7 +307,7 @@ class Toolbar(ttk.Frame): elif type_enum == NodeTypeEnum.NETWORK: self.network_enum = image_enum - def hide_pickers(self): + def hide_pickers(self) -> None: logging.debug("hiding pickers") if self.node_picker: self.node_picker.destroy() @@ -313,7 +319,7 @@ class Toolbar(ttk.Frame): self.annotation_picker.destroy() self.annotation_picker = None - def create_node_button(self): + def create_node_button(self) -> None: """ Create network layer button """ @@ -326,7 +332,7 @@ class Toolbar(ttk.Frame): Tooltip(self.node_button, "Network-layer virtual nodes") self.node_enum = ImageEnum.ROUTER - def draw_network_picker(self): + def draw_network_picker(self) -> None: """ Draw the options for link-layer button. """ @@ -353,7 +359,7 @@ class Toolbar(ttk.Frame): 0, lambda: self.show_picker(self.network_button, self.network_picker) ) - def create_network_button(self): + def create_network_button(self) -> None: """ Create link-layer node button and the options that represent different link-layer node types. @@ -367,7 +373,7 @@ class Toolbar(ttk.Frame): Tooltip(self.network_button, "link-layer nodes") self.network_enum = ImageEnum.HUB - def draw_annotation_picker(self): + def draw_annotation_picker(self) -> None: """ Draw the options for marker button. """ @@ -393,7 +399,7 @@ class Toolbar(ttk.Frame): 0, lambda: self.show_picker(self.annotation_button, self.annotation_picker) ) - def create_annotation_button(self): + def create_annotation_button(self) -> None: """ Create marker button and options that represent different marker types """ @@ -406,9 +412,10 @@ class Toolbar(ttk.Frame): Tooltip(self.annotation_button, "background annotation tools") self.annotation_enum = ImageEnum.MARKER - def create_observe_button(self): + def create_observe_button(self) -> None: + image = self.get_icon(ImageEnum.OBSERVE, TOOLBAR_SIZE) menu_button = ttk.Menubutton( - self.runtime_frame, image=icon(ImageEnum.OBSERVE), direction=tk.RIGHT + self.runtime_frame, image=image, direction=tk.RIGHT ) menu_button.grid(sticky="ew") menu = tk.Menu(menu_button, tearoff=0) @@ -430,25 +437,26 @@ class Toolbar(ttk.Frame): menu.add_command(label="PIM neighbors") menu.add_command(label="Edit...") - def click_stop(self): + def click_stop(self) -> None: """ redraw buttons on the toolbar, send node and link messages to grpc server """ logging.info("clicked stop button") self.app.menubar.change_menubar_item_state(is_runtime=False) self.app.core.close_mobility_players() + enable_buttons(self.runtime_frame, enabled=False) task = ProgressTask( self.app, "Stop", self.app.core.stop_session, self.stop_callback ) task.start() - def stop_callback(self, response: core_pb2.StopSessionResponse): + def stop_callback(self, response: core_pb2.StopSessionResponse) -> None: self.set_design() self.app.canvas.stopped_session() def update_annotation( - self, image: "ImageTk.PhotoImage", shape_type: ShapeType, image_enum - ): + self, image: PhotoImage, shape_type: ShapeType, image_enum + ) -> None: logging.debug("clicked annotation: ") self.hide_pickers() self.annotation_button.configure(image=image) @@ -462,12 +470,12 @@ class Toolbar(ttk.Frame): self.marker_tool = MarkerDialog(self.app) self.marker_tool.show() - def click_run_button(self): + def click_run_button(self) -> None: logging.debug("Click on RUN button") dialog = RunToolDialog(self.app) dialog.show() - def click_marker_button(self): + def click_marker_button(self) -> None: logging.debug("Click on marker button") self.runtime_select(self.runtime_marker_button) self.app.canvas.mode = GraphMode.ANNOTATION @@ -477,12 +485,12 @@ class Toolbar(ttk.Frame): self.marker_tool = MarkerDialog(self.app) self.marker_tool.show() - def scale_button(self, button, image_enum): - image = icon(image_enum, int(TOOLBAR_SIZE * self.app.app_scale)) + def scale_button(self, button, image_enum) -> None: + image = self.get_icon(image_enum, TOOLBAR_SIZE) button.config(image=image) button.image = image - def scale(self): + def scale(self) -> None: self.scale_button(self.play_button, ImageEnum.START) self.scale_button(self.select_button, ImageEnum.SELECT) self.scale_button(self.link_button, ImageEnum.LINK) From 0dcfcbf4ea1623617327e271a0a332716f6c3619 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 15 May 2020 11:43:54 -0700 Subject: [PATCH 0260/1131] pygui: simplified toolbar constructor, since there is no need for something more complicated --- daemon/core/gui/app.py | 2 +- daemon/core/gui/toolbar.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 90e5c36c..7ada71eb 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -104,7 +104,7 @@ class Application(ttk.Frame): self.rowconfigure(0, weight=1) self.columnconfigure(1, weight=1) self.grid(sticky="nsew") - self.toolbar = Toolbar(self, self) + self.toolbar = Toolbar(self) self.toolbar.grid(sticky="ns") self.right_frame = ttk.Frame(self) self.right_frame.columnconfigure(0, weight=1) diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 46b70bee..2bf4e63c 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -42,11 +42,11 @@ class Toolbar(ttk.Frame): Core toolbar class """ - def __init__(self, master: tk.Widget, app: "Application", **kwargs) -> None: + def __init__(self, app: "Application") -> None: """ Create a CoreToolbar instance """ - super().__init__(master, **kwargs) + super().__init__(app) self.app = app # design buttons From 4eaecd6a7b260f00c1fabf74ca6a248b4c2a51d4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 15 May 2020 14:46:35 -0700 Subject: [PATCH 0261/1131] pygui: simplified a couple of the other widget constructors --- daemon/core/gui/app.py | 7 +++---- daemon/core/gui/graph/graph.py | 37 +++++++++++++++------------------- daemon/core/gui/menubar.py | 5 ++--- daemon/core/gui/statusbar.py | 4 ++-- 4 files changed, 23 insertions(+), 30 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 7ada71eb..fe5c3659 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -113,16 +113,15 @@ class Application(ttk.Frame): self.draw_canvas() self.draw_status() self.progress = Progressbar(self.right_frame, mode="indeterminate") - self.menubar = Menubar(self.master, self) + self.menubar = Menubar(self) + self.master.config(menu=self.menubar) def draw_canvas(self) -> None: - width = self.guiconfig.preferences.width - height = self.guiconfig.preferences.height canvas_frame = ttk.Frame(self.right_frame) canvas_frame.rowconfigure(0, weight=1) canvas_frame.columnconfigure(0, weight=1) canvas_frame.grid(sticky="nsew", pady=1) - self.canvas = CanvasGraph(canvas_frame, self, self.core, width, height) + self.canvas = CanvasGraph(canvas_frame, self, self.core) self.canvas.grid(sticky="nsew") scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview) scroll_y.grid(row=0, column=1, sticky="ns") diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 2920f9b0..74ca3bc2 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -41,19 +41,12 @@ class ShowVar(BooleanVar): def state(self) -> str: return tk.NORMAL if self.get() else tk.HIDDEN - def click_handler(self): + def click_handler(self) -> None: self.canvas.itemconfigure(self.tag, state=self.state()) class CanvasGraph(tk.Canvas): - def __init__( - self, - master: tk.Widget, - app: "Application", - core: "CoreClient", - width: int, - height: int, - ): + def __init__(self, master: tk.Widget, app: "Application", core: "CoreClient"): super().__init__(master, highlightthickness=0, background="#cccccc") self.app = app self.core = core @@ -74,6 +67,8 @@ class CanvasGraph(tk.Canvas): self.drawing_edge = None self.rect = None self.shape_drawing = False + width = self.app.guiconfig.preferences.width + height = self.app.guiconfig.preferences.height self.default_dimensions = (width, height) self.current_dimensions = self.default_dimensions self.ratio = 1.0 @@ -571,10 +566,10 @@ class CanvasGraph(tk.Canvas): self.offset[0] * factor + event.x * (1 - factor), self.offset[1] * factor + event.y * (1 - factor), ) - logging.info("ratio: %s", self.ratio) - logging.info("offset: %s", self.offset) - self.app.statusbar.zoom.config(text="%s" % (int(self.ratio * 100)) + "%") - + logging.debug("ratio: %s", self.ratio) + logging.debug("offset: %s", self.offset) + zoom_label = f"{self.ratio * 100:.0f}%" + self.app.statusbar.zoom.config(text=zoom_label) if self.wallpaper: self.redraw_wallpaper() @@ -720,7 +715,7 @@ class CanvasGraph(tk.Canvas): if not self.app.core.is_runtime(): self.delete_selected_objects() else: - logging.info("node deletion is disabled during runtime state") + logging.debug("node deletion is disabled during runtime state") def double_click(self, event: tk.Event): selected = self.get_selected(event) @@ -836,10 +831,10 @@ class CanvasGraph(tk.Canvas): self.draw_wallpaper(image) def redraw_canvas(self, dimensions: Tuple[int, int] = None): - logging.info("redrawing canvas to dimensions: %s", dimensions) + logging.debug("redrawing canvas to dimensions: %s", dimensions) # reset scale and move back to original position - logging.info("resetting scaling: %s %s", self.ratio, self.offset) + logging.debug("resetting scaling: %s %s", self.ratio, self.offset) factor = 1 / self.ratio self.scale(tk.ALL, self.offset[0], self.offset[1], factor, factor) self.move(tk.ALL, -self.offset[0], -self.offset[1]) @@ -858,11 +853,11 @@ class CanvasGraph(tk.Canvas): def redraw_wallpaper(self): if self.adjust_to_dim.get(): - logging.info("drawing wallpaper to canvas dimensions") + logging.debug("drawing wallpaper to canvas dimensions") self.resize_to_wallpaper() else: option = ScaleOption(self.scale_option.get()) - logging.info("drawing canvas using scaling option: %s", option) + logging.debug("drawing canvas using scaling option: %s", option) if option == ScaleOption.UPPER_LEFT: self.wallpaper_upper_left() elif option == ScaleOption.CENTERED: @@ -908,10 +903,10 @@ class CanvasGraph(tk.Canvas): def copy(self): if self.core.is_runtime(): - logging.info("copy is disabled during runtime state") + logging.debug("copy is disabled during runtime state") return if self.selection: - logging.info("to copy nodes: %s", self.selection) + logging.debug("to copy nodes: %s", self.selection) self.to_copy.clear() for node_id in self.selection.keys(): canvas_node = self.nodes[node_id] @@ -919,7 +914,7 @@ class CanvasGraph(tk.Canvas): def paste(self): if self.core.is_runtime(): - logging.info("paste is disabled during runtime state") + logging.debug("paste is disabled during runtime state") return # maps original node canvas id to copy node canvas id copy_map = {} diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 2e07ed0a..69db0092 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -47,12 +47,11 @@ class Menubar(tk.Menu): Core menubar """ - def __init__(self, master: tk.Tk, app: "Application", **kwargs) -> None: + def __init__(self, app: "Application") -> None: """ Create a CoreMenubar instance """ - super().__init__(master, **kwargs) - self.master.config(menu=self) + super().__init__(app) self.app = app self.core = app.core self.canvas = app.canvas diff --git a/daemon/core/gui/statusbar.py b/daemon/core/gui/statusbar.py index 6c2e5e19..3f58e7a0 100644 --- a/daemon/core/gui/statusbar.py +++ b/daemon/core/gui/statusbar.py @@ -13,8 +13,8 @@ if TYPE_CHECKING: class StatusBar(ttk.Frame): - def __init__(self, master: tk.Widget, app: "Application", **kwargs): - super().__init__(master, **kwargs) + def __init__(self, master: tk.Widget, app: "Application"): + super().__init__(master) self.app = app self.status = None self.statusvar = tk.StringVar() From 29fc5acb996691f4fdf3d8f9553fa676deefa712 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 15 May 2020 23:23:07 -0700 Subject: [PATCH 0262/1131] pygui: toolbar cleanup for buttonbar frames --- daemon/core/gui/app.py | 5 +- daemon/core/gui/toolbar.py | 198 +++++++++++++------------------------ 2 files changed, 74 insertions(+), 129 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index fe5c3659..2644a46d 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -1,7 +1,7 @@ import logging import math import tkinter as tk -from tkinter import font, ttk +from tkinter import PhotoImage, font, ttk from tkinter.ttk import Progressbar import grpc @@ -160,5 +160,8 @@ class Application(ttk.Frame): else: self.toolbar.set_design() + def get_icon(self, image_enum: ImageEnum, width: int) -> PhotoImage: + return Images.get(image_enum, int(width * self.app_scale)) + def close(self) -> None: self.master.destroy() diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 2bf4e63c..9fc81a74 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -37,6 +37,30 @@ def enable_buttons(frame: ttk.Frame, enabled: bool) -> None: child.configure(state=state) +class ButtonBar(ttk.Frame): + def __init__(self, master: tk.Widget, app: "Application"): + super().__init__(master) + self.app = app + self.radio_buttons = [] + + def create_button( + self, image_enum: ImageEnum, func: Callable, tooltip: str, radio: bool = False + ) -> ttk.Button: + image = self.app.get_icon(image_enum, TOOLBAR_SIZE) + button = ttk.Button(self, image=image, command=func) + button.image = image + button.grid(sticky="ew") + Tooltip(button, tooltip) + if radio: + self.radio_buttons.append(button) + return button + + def select_radio(self, selected: ttk.Button) -> None: + for button in self.radio_buttons: + button.state(["!pressed"]) + selected.state(["pressed"]) + + class Toolbar(ttk.Frame): """ Core toolbar class @@ -82,9 +106,6 @@ class Toolbar(ttk.Frame): # draw components self.draw() - def get_icon(self, image_enum: ImageEnum, width: int) -> PhotoImage: - return Images.get(image_enum, int(width * self.app.app_scale)) - def draw(self) -> None: self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) @@ -93,84 +114,59 @@ class Toolbar(ttk.Frame): self.design_frame.tkraise() def draw_design_frame(self) -> None: - self.design_frame = ttk.Frame(self) + self.design_frame = ButtonBar(self, self.app) self.design_frame.grid(row=0, column=0, sticky="nsew") self.design_frame.columnconfigure(0, weight=1) - self.play_button = self.create_button( - self.design_frame, - self.get_icon(ImageEnum.START, TOOLBAR_SIZE), - self.click_start, - "start the session", + self.play_button = self.design_frame.create_button( + ImageEnum.START, self.click_start, "Start Session" ) - self.select_button = self.create_button( - self.design_frame, - self.get_icon(ImageEnum.SELECT, TOOLBAR_SIZE), - self.click_selection, - "selection tool", + self.select_button = self.design_frame.create_button( + ImageEnum.SELECT, self.click_selection, "Selection Tool", radio=True ) - self.link_button = self.create_button( - self.design_frame, - self.get_icon(ImageEnum.LINK, TOOLBAR_SIZE), - self.click_link, - "link tool", + self.link_button = self.design_frame.create_button( + ImageEnum.LINK, self.click_link, "Link Tool", radio=True + ) + self.node_enum = ImageEnum.ROUTER + self.node_button = self.design_frame.create_button( + self.node_enum, self.draw_node_picker, "Container Nodes", radio=True + ) + self.network_enum = ImageEnum.HUB + self.network_button = self.design_frame.create_button( + self.network_enum, self.draw_network_picker, "Link Layer Nodes", radio=True + ) + self.annotation_enum = ImageEnum.MARKER + self.annotation_button = self.design_frame.create_button( + self.annotation_enum, + self.draw_annotation_picker, + "Annotation Tools", + radio=True, ) - self.create_node_button() - self.create_network_button() - self.create_annotation_button() - - def design_select(self, button: ttk.Button) -> None: - logging.debug("selecting design button: %s", button) - self.select_button.state(["!pressed"]) - self.link_button.state(["!pressed"]) - self.node_button.state(["!pressed"]) - self.network_button.state(["!pressed"]) - self.annotation_button.state(["!pressed"]) - button.state(["pressed"]) - - def runtime_select(self, button: ttk.Button) -> None: - logging.debug("selecting runtime button: %s", button) - self.runtime_select_button.state(["!pressed"]) - self.stop_button.state(["!pressed"]) - self.runtime_marker_button.state(["!pressed"]) - self.run_command_button.state(["!pressed"]) - button.state(["pressed"]) def draw_runtime_frame(self) -> None: - self.runtime_frame = ttk.Frame(self) + self.runtime_frame = ButtonBar(self, self.app) self.runtime_frame.grid(row=0, column=0, sticky="nsew") self.runtime_frame.columnconfigure(0, weight=1) - self.stop_button = self.create_button( - self.runtime_frame, - self.get_icon(ImageEnum.STOP, TOOLBAR_SIZE), - self.click_stop, - "stop the session", + self.stop_button = self.runtime_frame.create_button( + ImageEnum.STOP, self.click_stop, "Stop Session" ) - self.runtime_select_button = self.create_button( - self.runtime_frame, - self.get_icon(ImageEnum.SELECT, TOOLBAR_SIZE), - self.click_runtime_selection, - "selection tool", + self.runtime_select_button = self.runtime_frame.create_button( + ImageEnum.SELECT, self.click_runtime_selection, "Selection Tool", radio=True ) - self.runtime_marker_button = self.create_button( - self.runtime_frame, - self.get_icon(ImageEnum.MARKER, TOOLBAR_SIZE), - self.click_marker_button, - "marker", + self.runtime_marker_button = self.runtime_frame.create_button( + ImageEnum.MARKER, self.click_marker_button, "Marker Tool", radio=True ) - self.run_command_button = self.create_button( - self.runtime_frame, - self.get_icon(ImageEnum.RUN, TOOLBAR_SIZE), - self.click_run_button, - "run", + self.run_command_button = self.runtime_frame.create_button( + ImageEnum.RUN, self.click_run_button, "Run Tool" ) def draw_node_picker(self) -> None: + self.design_frame.select_radio(self.node_button) self.hide_pickers() self.node_picker = ttk.Frame(self.master) # draw default nodes for node_draw in NodeUtils.NODES: - toolbar_image = self.get_icon(node_draw.image_enum, TOOLBAR_SIZE) - image = self.get_icon(node_draw.image_enum, PICKER_SIZE) + toolbar_image = self.app.get_icon(node_draw.image_enum, TOOLBAR_SIZE) + image = self.app.get_icon(node_draw.image_enum, PICKER_SIZE) func = partial( self.update_button, self.node_button, @@ -198,7 +194,6 @@ class Toolbar(ttk.Frame): node_draw.image_file, ) self.create_picker_button(image, func, self.node_picker, name) - self.design_select(self.node_button) self.node_button.after( 0, lambda: self.show_picker(self.node_button, self.node_picker) ) @@ -231,23 +226,12 @@ class Toolbar(ttk.Frame): button.bind("", lambda e: func()) button.grid(pady=1) - def create_button( - self, frame: ttk.Frame, image: PhotoImage, func: Callable, tooltip: str - ) -> ttk.Button: - button = ttk.Button(frame, image=image, command=func) - button.image = image - button.grid(sticky="ew") - Tooltip(button, tooltip) - return button - def click_selection(self) -> None: - logging.debug("clicked selection tool") - self.design_select(self.select_button) + self.design_frame.select_radio(self.select_button) self.app.canvas.mode = GraphMode.SELECT def click_runtime_selection(self) -> None: - logging.debug("clicked selection tool") - self.runtime_select(self.runtime_select_button) + self.runtime_frame.select_radio(self.runtime_select_button) self.app.canvas.mode = GraphMode.SELECT def click_start(self) -> None: @@ -284,8 +268,7 @@ class Toolbar(ttk.Frame): self.click_selection() def click_link(self) -> None: - logging.debug("Click LINK button") - self.design_select(self.link_button) + self.design_frame.select_radio(self.link_button) self.app.canvas.mode = GraphMode.EDGE def update_button( @@ -319,28 +302,16 @@ class Toolbar(ttk.Frame): self.annotation_picker.destroy() self.annotation_picker = None - def create_node_button(self) -> None: - """ - Create network layer button - """ - image = self.get_icon(ImageEnum.ROUTER, TOOLBAR_SIZE) - self.node_button = ttk.Button( - self.design_frame, image=image, command=self.draw_node_picker - ) - self.node_button.image = image - self.node_button.grid(sticky="ew") - Tooltip(self.node_button, "Network-layer virtual nodes") - self.node_enum = ImageEnum.ROUTER - def draw_network_picker(self) -> None: """ Draw the options for link-layer button. """ + self.design_frame.select_radio(self.network_button) self.hide_pickers() self.network_picker = ttk.Frame(self.master) for node_draw in NodeUtils.NETWORK_NODES: - toolbar_image = self.get_icon(node_draw.image_enum, TOOLBAR_SIZE) - image = self.get_icon(node_draw.image_enum, PICKER_SIZE) + toolbar_image = self.app.get_icon(node_draw.image_enum, TOOLBAR_SIZE) + image = self.app.get_icon(node_draw.image_enum, PICKER_SIZE) self.create_picker_button( image, partial( @@ -354,29 +325,15 @@ class Toolbar(ttk.Frame): self.network_picker, node_draw.label, ) - self.design_select(self.network_button) self.network_button.after( 0, lambda: self.show_picker(self.network_button, self.network_picker) ) - def create_network_button(self) -> None: - """ - Create link-layer node button and the options that represent different - link-layer node types. - """ - image = self.get_icon(ImageEnum.HUB, TOOLBAR_SIZE) - self.network_button = ttk.Button( - self.design_frame, image=image, command=self.draw_network_picker - ) - self.network_button.image = image - self.network_button.grid(sticky="ew") - Tooltip(self.network_button, "link-layer nodes") - self.network_enum = ImageEnum.HUB - def draw_annotation_picker(self) -> None: """ Draw the options for marker button. """ + self.design_frame.select_radio(self.annotation_button) self.hide_pickers() self.annotation_picker = ttk.Frame(self.master) nodes = [ @@ -386,34 +343,20 @@ class Toolbar(ttk.Frame): (ImageEnum.TEXT, ShapeType.TEXT), ] for image_enum, shape_type in nodes: - toolbar_image = self.get_icon(image_enum, TOOLBAR_SIZE) - image = self.get_icon(image_enum, PICKER_SIZE) + toolbar_image = self.app.get_icon(image_enum, TOOLBAR_SIZE) + image = self.app.get_icon(image_enum, PICKER_SIZE) self.create_picker_button( image, partial(self.update_annotation, toolbar_image, shape_type, image_enum), self.annotation_picker, shape_type.value, ) - self.design_select(self.annotation_button) self.annotation_button.after( 0, lambda: self.show_picker(self.annotation_button, self.annotation_picker) ) - def create_annotation_button(self) -> None: - """ - Create marker button and options that represent different marker types - """ - image = self.get_icon(ImageEnum.MARKER, TOOLBAR_SIZE) - self.annotation_button = ttk.Button( - self.design_frame, image=image, command=self.draw_annotation_picker - ) - self.annotation_button.image = image - self.annotation_button.grid(sticky="ew") - Tooltip(self.annotation_button, "background annotation tools") - self.annotation_enum = ImageEnum.MARKER - def create_observe_button(self) -> None: - image = self.get_icon(ImageEnum.OBSERVE, TOOLBAR_SIZE) + image = self.app.get_icon(ImageEnum.OBSERVE, TOOLBAR_SIZE) menu_button = ttk.Menubutton( self.runtime_frame, image=image, direction=tk.RIGHT ) @@ -476,8 +419,7 @@ class Toolbar(ttk.Frame): dialog.show() def click_marker_button(self) -> None: - logging.debug("Click on marker button") - self.runtime_select(self.runtime_marker_button) + self.runtime_frame.select_radio(self.runtime_marker_button) self.app.canvas.mode = GraphMode.ANNOTATION self.app.canvas.annotation_type = ShapeType.MARKER if self.marker_tool: @@ -486,7 +428,7 @@ class Toolbar(ttk.Frame): self.marker_tool.show() def scale_button(self, button, image_enum) -> None: - image = self.get_icon(image_enum, TOOLBAR_SIZE) + image = self.app.get_icon(image_enum, TOOLBAR_SIZE) button.config(image=image) button.image = image From 50816b3b80ce620a8efa81c98e0e7adedfc1db95 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 16 May 2020 01:14:48 -0700 Subject: [PATCH 0263/1131] pygui: cleaned up toolbar picker code, fixed closing app when a picker is showing --- daemon/core/gui/app.py | 5 ++ daemon/core/gui/toolbar.py | 178 ++++++++++++++----------------------- 2 files changed, 71 insertions(+), 112 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 2644a46d..c795a46a 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -149,6 +149,8 @@ class Application(ttk.Frame): self.after(0, lambda: ErrorDialog(self, title, message).show()) def on_closing(self) -> None: + if self.toolbar.picker: + self.toolbar.picker.destroy() self.menubar.prompt_save_running_session(True) def save_config(self) -> None: @@ -163,5 +165,8 @@ class Application(ttk.Frame): def get_icon(self, image_enum: ImageEnum, width: int) -> PhotoImage: return Images.get(image_enum, int(width * self.app_scale)) + def get_custom_icon(self, image_file: str, width: int) -> PhotoImage: + return Images.get_custom(image_file, int(width * self.app_scale)) + def close(self) -> None: self.master.destroy() diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 9fc81a74..572a523d 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -12,7 +12,7 @@ from core.gui.dialogs.marker import MarkerDialog from core.gui.dialogs.runtool import RunToolDialog from core.gui.graph.enums import GraphMode from core.gui.graph.shapeutils import ShapeType, is_marker -from core.gui.images import ImageEnum, Images +from core.gui.images import ImageEnum from core.gui.nodeutils import NodeDraw, NodeUtils from core.gui.task import ProgressTask from core.gui.themes import Styles @@ -37,6 +37,46 @@ def enable_buttons(frame: ttk.Frame, enabled: bool) -> None: child.configure(state=state) +class PickerFrame(ttk.Frame): + def __init__(self, app: "Application", button: ttk.Button) -> None: + super().__init__(app) + self.app = app + self.button = button + + def create_button(self, label: str, image_enum: ImageEnum, func: Callable) -> None: + bar_image = self.app.get_icon(image_enum, TOOLBAR_SIZE) + image = self.app.get_icon(image_enum, PICKER_SIZE) + self._create_button(label, image, bar_image, func) + + def create_custom_button(self, label: str, image_file: str, func: Callable) -> None: + bar_image = self.app.get_custom_icon(image_file, TOOLBAR_SIZE) + image = self.app.get_custom_icon(image_file, PICKER_SIZE) + self._create_button(label, image, bar_image, func) + + def _create_button( + self, label: str, image: PhotoImage, bar_image: PhotoImage, func: Callable + ) -> None: + button = ttk.Button( + self, image=image, text=label, compound=tk.TOP, style=Styles.picker_button + ) + button.image = image + button.bind("", lambda e: func(bar_image)) + button.grid(pady=1) + + def show(self) -> None: + self.button.after(0, self._show) + + def _show(self) -> None: + x = self.button.winfo_width() + 1 + y = self.button.winfo_rooty() - self.app.winfo_rooty() - 1 + self.place(x=x, y=y) + self.app.bind_all("", lambda e: self.destroy()) + self.wait_visibility() + self.grab_set() + self.wait_window() + self.app.unbind_all("") + + class ButtonBar(ttk.Frame): def __init__(self, master: tk.Widget, app: "Application"): super().__init__(master) @@ -90,9 +130,7 @@ class Toolbar(ttk.Frame): # frames self.design_frame = None self.runtime_frame = None - self.node_picker = None - self.network_picker = None - self.annotation_picker = None + self.picker = None # dialog self.marker_tool = None @@ -161,70 +199,23 @@ class Toolbar(ttk.Frame): def draw_node_picker(self) -> None: self.design_frame.select_radio(self.node_button) - self.hide_pickers() - self.node_picker = ttk.Frame(self.master) + self.picker = PickerFrame(self.app, self.node_button) # draw default nodes for node_draw in NodeUtils.NODES: - toolbar_image = self.app.get_icon(node_draw.image_enum, TOOLBAR_SIZE) - image = self.app.get_icon(node_draw.image_enum, PICKER_SIZE) func = partial( - self.update_button, - self.node_button, - toolbar_image, - node_draw, - NodeTypeEnum.NODE, - node_draw.image_enum, + self.update_button, self.node_button, node_draw, NodeTypeEnum.NODE ) - self.create_picker_button(image, func, self.node_picker, node_draw.label) + self.picker.create_button(node_draw.label, node_draw.image_enum, func) # draw custom nodes for name in sorted(self.app.core.custom_nodes): node_draw = self.app.core.custom_nodes[name] - toolbar_image = Images.get_custom( - node_draw.image_file, int(TOOLBAR_SIZE * self.app.app_scale) - ) - image = Images.get_custom( - node_draw.image_file, int(PICKER_SIZE * self.app.app_scale) - ) func = partial( - self.update_button, - self.node_button, - toolbar_image, - node_draw, - NodeTypeEnum, - node_draw.image_file, + self.update_button, self.node_button, node_draw, NodeTypeEnum.NODE ) - self.create_picker_button(image, func, self.node_picker, name) - self.node_button.after( - 0, lambda: self.show_picker(self.node_button, self.node_picker) - ) - - def show_picker(self, button: ttk.Button, picker: ttk.Frame) -> None: - x = self.winfo_width() + 1 - y = button.winfo_rooty() - picker.master.winfo_rooty() - 1 - picker.place(x=x, y=y) - self.app.bind_all("", lambda e: self.hide_pickers()) - picker.wait_visibility() - picker.grab_set() - self.wait_window(picker) - self.app.unbind_all("") - - def create_picker_button( - self, image: PhotoImage, func: Callable, frame: ttk.Frame, label: str - ) -> None: - """ - Create button and put it on the frame - - :param image: button image - :param func: the command that is executed when button is clicked - :param frame: frame that contains the button - :param label: button label - """ - button = ttk.Button( - frame, image=image, text=label, compound=tk.TOP, style=Styles.picker_button - ) - button.image = image - button.bind("", lambda e: func()) - button.grid(pady=1) + self.picker.create_custom_button( + node_draw.label, node_draw.image_file, func + ) + self.picker.show() def click_selection(self) -> None: self.design_frame.select_radio(self.select_button) @@ -274,68 +265,39 @@ class Toolbar(ttk.Frame): def update_button( self, button: ttk.Button, - image: PhotoImage, node_draw: NodeDraw, - type_enum, - image_enum, + type_enum: NodeTypeEnum, + image: PhotoImage, ) -> None: logging.debug("update button(%s): %s", button, node_draw) - self.hide_pickers() button.configure(image=image) button.image = image self.app.canvas.mode = GraphMode.NODE self.app.canvas.node_draw = node_draw if type_enum == NodeTypeEnum.NODE: - self.node_enum = image_enum + self.node_enum = node_draw.image_enum elif type_enum == NodeTypeEnum.NETWORK: - self.network_enum = image_enum - - def hide_pickers(self) -> None: - logging.debug("hiding pickers") - if self.node_picker: - self.node_picker.destroy() - self.node_picker = None - if self.network_picker: - self.network_picker.destroy() - self.network_picker = None - if self.annotation_picker: - self.annotation_picker.destroy() - self.annotation_picker = None + self.network_enum = node_draw.image_enum def draw_network_picker(self) -> None: """ Draw the options for link-layer button. """ self.design_frame.select_radio(self.network_button) - self.hide_pickers() - self.network_picker = ttk.Frame(self.master) + self.picker = PickerFrame(self.app, self.network_button) for node_draw in NodeUtils.NETWORK_NODES: - toolbar_image = self.app.get_icon(node_draw.image_enum, TOOLBAR_SIZE) - image = self.app.get_icon(node_draw.image_enum, PICKER_SIZE) - self.create_picker_button( - image, - partial( - self.update_button, - self.network_button, - toolbar_image, - node_draw, - NodeTypeEnum.NETWORK, - node_draw.image_enum, - ), - self.network_picker, - node_draw.label, + func = partial( + self.update_button, self.network_button, node_draw, NodeTypeEnum.NETWORK ) - self.network_button.after( - 0, lambda: self.show_picker(self.network_button, self.network_picker) - ) + self.picker.create_button(node_draw.label, node_draw.image_enum, func) + self.picker.show() def draw_annotation_picker(self) -> None: """ Draw the options for marker button. """ self.design_frame.select_radio(self.annotation_button) - self.hide_pickers() - self.annotation_picker = ttk.Frame(self.master) + self.picker = PickerFrame(self.app, self.annotation_button) nodes = [ (ImageEnum.MARKER, ShapeType.MARKER), (ImageEnum.OVAL, ShapeType.OVAL), @@ -343,17 +305,10 @@ class Toolbar(ttk.Frame): (ImageEnum.TEXT, ShapeType.TEXT), ] for image_enum, shape_type in nodes: - toolbar_image = self.app.get_icon(image_enum, TOOLBAR_SIZE) - image = self.app.get_icon(image_enum, PICKER_SIZE) - self.create_picker_button( - image, - partial(self.update_annotation, toolbar_image, shape_type, image_enum), - self.annotation_picker, - shape_type.value, - ) - self.annotation_button.after( - 0, lambda: self.show_picker(self.annotation_button, self.annotation_picker) - ) + label = shape_type.value + func = partial(self.update_annotation, shape_type, image_enum) + self.picker.create_button(label, image_enum, func) + self.picker.show() def create_observe_button(self) -> None: image = self.app.get_icon(ImageEnum.OBSERVE, TOOLBAR_SIZE) @@ -398,10 +353,9 @@ class Toolbar(ttk.Frame): self.app.canvas.stopped_session() def update_annotation( - self, image: PhotoImage, shape_type: ShapeType, image_enum + self, shape_type: ShapeType, image_enum: ImageEnum, image: PhotoImage ) -> None: - logging.debug("clicked annotation: ") - self.hide_pickers() + logging.debug("clicked annotation") self.annotation_button.configure(image=image) self.annotation_button.image = image self.app.canvas.mode = GraphMode.ANNOTATION From 91220078f1787f55656a259ae49d91310ae7bbf1 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 16 May 2020 14:12:08 -0700 Subject: [PATCH 0264/1131] pygui: created a singular func for ordering items on canvas by tags, updates so that drawing edges are behind nodes --- daemon/core/gui/coreclient.py | 5 +---- daemon/core/gui/graph/edges.py | 2 -- daemon/core/gui/graph/graph.py | 10 ++++++---- daemon/core/gui/graph/shape.py | 3 +-- daemon/core/gui/graph/tags.py | 10 ++++++---- 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index e882227c..dd7f8308 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -20,7 +20,6 @@ from core.gui.dialogs.emaneinstall import EmaneInstallDialog from core.gui.dialogs.error import ErrorDialog from core.gui.dialogs.mobilityplayer import MobilityPlayer from core.gui.dialogs.sessions import SessionsDialog -from core.gui.graph import tags from core.gui.graph.edges import CanvasEdge from core.gui.graph.node import CanvasNode from core.gui.graph.shape import AnnotationData, Shape @@ -389,9 +388,7 @@ class CoreClient: self.app.canvas.shapes[shape.id] = shape except ValueError: logging.exception("unknown shape: %s", shape_type) - - for tag in tags.ABOVE_WALLPAPER_TAGS: - self.app.canvas.tag_raise(tag) + self.app.canvas.organize() def create_new_session(self): """ diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 68c3823b..00268c88 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -331,8 +331,6 @@ class CanvasEdge(Edge): dst_pos = self.canvas.coords(self.dst) self.move_dst(dst_pos) self.check_wireless() - self.canvas.tag_raise(self.src) - self.canvas.tag_raise(self.dst) logging.debug("Draw wired link from node %s to node %s", self.src, dst) def is_wireless(self) -> bool: diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 74ca3bc2..eacbf1f4 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -139,7 +139,7 @@ class CanvasGraph(tk.Canvas): self.show_ip6s.set(True) # delete any existing drawn items - for tag in tags.COMPONENT_TAGS: + for tag in tags.RESET_TAGS: self.delete(tag) # set the private variables to default value @@ -591,6 +591,7 @@ class CanvasGraph(tk.Canvas): if self.mode == GraphMode.EDGE and is_node: pos = self.coords(selected) self.drawing_edge = CanvasEdge(self, selected, pos, pos) + self.organize() if self.mode == GraphMode.ANNOTATION: if is_marker(self.annotation_type): @@ -866,10 +867,11 @@ class CanvasGraph(tk.Canvas): self.wallpaper_scaled() elif option == ScaleOption.TILED: logging.warning("tiled background not implemented yet") + self.organize() - # raise items above wallpaper - for component in tags.ABOVE_WALLPAPER_TAGS: - self.tag_raise(component) + def organize(self) -> None: + for tag in tags.ORGANIZE_TAGS: + self.tag_raise(tag) def set_wallpaper(self, filename: str): logging.debug("setting wallpaper: %s", filename) diff --git a/daemon/core/gui/graph/shape.py b/daemon/core/gui/graph/shape.py index eeda09fd..9dd01772 100644 --- a/daemon/core/gui/graph/shape.py +++ b/daemon/core/gui/graph/shape.py @@ -146,8 +146,7 @@ class Shape: self.canvas.coords(self.id, self.x1, self.y1, x1, y1) def shape_complete(self, x: float, y: float): - for component in tags.ABOVE_SHAPE: - self.canvas.tag_raise(component) + self.canvas.organize() s = ShapeDialog(self.app, self) s.show() diff --git a/daemon/core/gui/graph/tags.py b/daemon/core/gui/graph/tags.py index 8ac6476b..c0721193 100644 --- a/daemon/core/gui/graph/tags.py +++ b/daemon/core/gui/graph/tags.py @@ -11,19 +11,21 @@ NODE = "node" WALLPAPER = "wallpaper" SELECTION = "selectednodes" MARKER = "marker" -ABOVE_WALLPAPER_TAGS = [ +ORGANIZE_TAGS = [ + WALLPAPER, GRIDLINE, SHAPE, SHAPE_TEXT, EDGE, - LINK_LABEL, WIRELESS_EDGE, + LINK_LABEL, ANTENNA, NODE, NODE_LABEL, + SELECTION, + MARKER, ] -ABOVE_SHAPE = [GRIDLINE, EDGE, LINK_LABEL, WIRELESS_EDGE, ANTENNA, NODE, NODE_LABEL] -COMPONENT_TAGS = [ +RESET_TAGS = [ EDGE, NODE, NODE_LABEL, From 06e3d848620596465675296320cd6b2be2ec8fbf Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 16 May 2020 23:35:19 -0700 Subject: [PATCH 0265/1131] pygui: fixed interface creation after deletion, fixed issue reusing deleted subnets --- daemon/core/gui/coreclient.py | 17 ++++++++-------- daemon/core/gui/dialogs/nodeconfig.py | 6 +++--- daemon/core/gui/graph/graph.py | 29 +++++++++++++-------------- daemon/core/gui/graph/node.py | 8 +++++++- daemon/core/gui/interface.py | 3 +-- 5 files changed, 33 insertions(+), 30 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index dd7f8308..99966f02 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -330,6 +330,9 @@ class CoreClient: except grpc.RpcError as e: self.app.show_grpc_exception("Join Session Error", e) + # organize canvas + self.app.canvas.organize() + # update ui to represent current state self.app.after(0, self.app.joined_session_update) @@ -388,7 +391,6 @@ class CoreClient: self.app.canvas.shapes[shape.id] = shape except ValueError: logging.exception("unknown shape: %s", shape_type) - self.app.canvas.organize() def create_new_session(self): """ @@ -835,7 +837,7 @@ class CoreClient: ip4, ip6 = self.interfaces_manager.get_ips(node) ip4_mask = self.interfaces_manager.ip4_mask ip6_mask = self.interfaces_manager.ip6_mask - interface_id = len(canvas_node.interfaces) + interface_id = canvas_node.next_interface_id() name = f"eth{interface_id}" interface = core_pb2.Interface( id=interface_id, @@ -845,7 +847,8 @@ class CoreClient: ip6=ip6, ip6mask=ip6_mask, ) - logging.debug( + canvas_node.interfaces[interface.id] = interface + logging.info( "create node(%s) interface(%s) IPv4(%s) IPv6(%s)", node.name, interface.name, @@ -870,11 +873,13 @@ class CoreClient: src_interface = None if NodeUtils.is_container_node(src_node.type): src_interface = self.create_interface(canvas_src_node) + edge.src_interface = src_interface self.interface_to_edge[(src_node.id, src_interface.id)] = edge.token dst_interface = None if NodeUtils.is_container_node(dst_node.type): dst_interface = self.create_interface(canvas_dst_node) + edge.dst_interface = dst_interface self.interface_to_edge[(dst_node.id, dst_interface.id)] = edge.token link = core_pb2.Link( @@ -884,12 +889,6 @@ class CoreClient: interface_one=src_interface, interface_two=dst_interface, ) - if src_interface: - edge.src_interface = link.interface_one - canvas_src_node.interfaces.append(link.interface_one) - if dst_interface: - edge.dst_interface = link.interface_two - canvas_dst_node.interfaces.append(link.interface_two) edge.set_link(link) self.links[edge.token] = edge logging.info("Add link between %s and %s", src_node.name, dst_node.name) diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index 73f0ac09..0d46ae06 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -207,8 +207,8 @@ class NodeConfigDialog(Dialog): notebook.grid(sticky="nsew", pady=PADY) self.top.rowconfigure(notebook.grid_info()["row"], weight=1) state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL - for interface in self.canvas_node.interfaces: - logging.info("interface: %s", interface) + for interface_id in sorted(self.canvas_node.interfaces): + interface = self.canvas_node.interfaces[interface_id] tab = ttk.Frame(notebook, padding=FRAME_PAD) tab.grid(sticky="nsew", pady=PADY) tab.columnconfigure(1, weight=1) @@ -309,7 +309,7 @@ class NodeConfigDialog(Dialog): self.canvas_node.image = self.image # update node interface data - for interface in self.canvas_node.interfaces: + for interface in self.canvas_node.interfaces.values(): data = self.interfaces[interface.id] # validate ip4 diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index eacbf1f4..fcc8c81b 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -322,26 +322,25 @@ class CanvasGraph(tk.Canvas): self.edges[edge.token] = edge self.core.links[edge.token] = edge if link.HasField("interface_one"): + interface_one = link.interface_one self.core.interface_to_edge[ - (node_one.id, link.interface_one.id) + (node_one.id, interface_one.id) ] = token - canvas_node_one.interfaces.append(link.interface_one) - edge.src_interface = link.interface_one + canvas_node_one.interfaces[interface_one.id] = interface_one + edge.src_interface = interface_one if link.HasField("interface_two"): + interface_two = link.interface_two self.core.interface_to_edge[ - (node_two.id, link.interface_two.id) + (node_two.id, interface_two.id) ] = edge.token - canvas_node_two.interfaces.append(link.interface_two) - edge.dst_interface = link.interface_two + canvas_node_two.interfaces[interface_two.id] = interface_two + edge.dst_interface = interface_two elif link.options.unidirectional: edge = self.edges[token] edge.asymmetric_link = link else: logging.error("duplicate link received: %s", link) - # raise the nodes so they on top of the links - self.tag_raise(tags.NODE) - def stopped_session(self): # clear wireless edges for edge in self.wireless_edges.values(): @@ -522,8 +521,8 @@ class CanvasGraph(tk.Canvas): other_interface = edge.dst_interface other_node = self.nodes[other_id] other_node.edges.remove(edge) - if other_interface in other_node.interfaces: - other_node.interfaces.remove(other_interface) + if other_interface: + del other_node.interfaces[other_interface.id] if is_wireless: other_node.delete_antenna() @@ -541,12 +540,12 @@ class CanvasGraph(tk.Canvas): del self.edges[edge.token] src_node = self.nodes[edge.src] src_node.edges.discard(edge) - if edge.src_interface in src_node.interfaces: - src_node.interfaces.remove(edge.src_interface) + if edge.src_interface: + del src_node.interfaces[edge.src_interface.id] dst_node = self.nodes[edge.dst] dst_node.edges.discard(edge) - if edge.dst_interface in dst_node.interfaces: - dst_node.interfaces.remove(edge.dst_interface) + if edge.dst_interface: + del dst_node.interfaces[edge.dst_interface.id] src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type) if src_wireless: dst_node.delete_antenna() diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 41b4704a..0b4f2bc9 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -55,7 +55,7 @@ class CanvasNode: ) self.tooltip = CanvasTooltip(self.canvas) self.edges = set() - self.interfaces = [] + self.interfaces = {} self.wireless_edges = set() self.antennas = [] self.antenna_images = {} @@ -70,6 +70,12 @@ class CanvasNode: self.context = tk.Menu(self.canvas) themes.style_menu(self.context) + def next_interface_id(self) -> int: + i = 0 + while i in self.interfaces: + i += 1 + return i + def setup_bindings(self): self.canvas.tag_bind(self.id, "", self.double_click) self.canvas.tag_bind(self.id, "", self.on_enter) diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index fc5185f5..437bd37c 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -105,12 +105,11 @@ class InterfaceManager: for interface in interfaces: subnets = self.get_subnets(interface) if subnets not in remaining_subnets: - if self.current_subnets == subnets: - self.current_subnets = None self.used_subnets.pop(subnets.key(), None) else: index = get_index(interface) subnets.used_indexes.discard(index) + self.current_subnets = None def joined(self, links: List["core_pb2.Link"]) -> None: interfaces = [] From 41df8a57b82c6bca0b2fc14d77d225800aa774c1 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 16 May 2020 23:59:36 -0700 Subject: [PATCH 0266/1131] pygui: revert change to keep references for created interfaces properly --- daemon/core/gui/coreclient.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 99966f02..2b565e7f 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -847,7 +847,6 @@ class CoreClient: ip6=ip6, ip6mask=ip6_mask, ) - canvas_node.interfaces[interface.id] = interface logging.info( "create node(%s) interface(%s) IPv4(%s) IPv6(%s)", node.name, @@ -873,13 +872,11 @@ class CoreClient: src_interface = None if NodeUtils.is_container_node(src_node.type): src_interface = self.create_interface(canvas_src_node) - edge.src_interface = src_interface self.interface_to_edge[(src_node.id, src_interface.id)] = edge.token dst_interface = None if NodeUtils.is_container_node(dst_node.type): dst_interface = self.create_interface(canvas_dst_node) - edge.dst_interface = dst_interface self.interface_to_edge[(dst_node.id, dst_interface.id)] = edge.token link = core_pb2.Link( @@ -889,6 +886,15 @@ class CoreClient: interface_one=src_interface, interface_two=dst_interface, ) + # assign after creating link proto, since interfaces are copied + if src_interface: + interface_one = link.interface_one + edge.src_interface = interface_one + canvas_src_node.interfaces[interface_one.id] = interface_one + if dst_interface: + interface_two = link.interface_two + edge.dst_interface = interface_two + canvas_dst_node.interfaces[interface_two.id] = interface_two edge.set_link(link) self.links[edge.token] = edge logging.info("Add link between %s and %s", src_node.name, dst_node.name) From 8979c861875ab5ed27caf7e84d003301556a89a7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 17 May 2020 00:11:28 -0700 Subject: [PATCH 0267/1131] pygui: fixed issue with moving text shapes --- daemon/core/gui/graph/shape.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/daemon/core/gui/graph/shape.py b/daemon/core/gui/graph/shape.py index 9dd01772..70f67d14 100644 --- a/daemon/core/gui/graph/shape.py +++ b/daemon/core/gui/graph/shape.py @@ -157,10 +157,11 @@ class Shape: original_position = self.canvas.coords(self.id) self.canvas.move(self.id, x_offset, y_offset) coords = self.canvas.coords(self.id) + if self.shape_type == ShapeType.TEXT: + coords = coords * 2 if not self.canvas.valid_position(*coords): self.canvas.coords(self.id, original_position) return - self.canvas.move_selection(self.id, x_offset, y_offset) if self.text_id is not None: self.canvas.move(self.text_id, x_offset, y_offset) From 34f86174a26f4f0f44558289fcd8371d3f751508 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 17 May 2020 00:28:03 -0700 Subject: [PATCH 0268/1131] pygui: cleaned up color picker layout --- daemon/core/gui/dialogs/colorpicker.py | 82 ++++++++++++-------------- 1 file changed, 38 insertions(+), 44 deletions(-) diff --git a/daemon/core/gui/dialogs/colorpicker.py b/daemon/core/gui/dialogs/colorpicker.py index 9087d6df..b1968cd4 100644 --- a/daemon/core/gui/dialogs/colorpicker.py +++ b/daemon/core/gui/dialogs/colorpicker.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING from core.gui import validation from core.gui.dialogs.dialog import Dialog +from core.gui.themes import PADX, PADY if TYPE_CHECKING: from core.gui.app import Application @@ -16,7 +17,7 @@ class ColorPickerDialog(Dialog): def __init__( self, master: tk.BaseWidget, app: "Application", initcolor: str = "#000000" ): - super().__init__(app, "color picker", master=master) + super().__init__(app, "Color Picker", master=master) self.red_entry = None self.blue_entry = None self.green_entry = None @@ -43,42 +44,40 @@ class ColorPickerDialog(Dialog): def draw(self): self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(3, weight=1) + # rgb frames frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=6) - frame.columnconfigure(3, weight=2) - label = ttk.Label(frame, text="R: ") - label.grid(row=0, column=0) - self.red_entry = validation.RgbEntry(frame, width=4, textvariable=self.red) - self.red_entry.grid(row=0, column=1, sticky="nsew") + frame.grid(row=0, column=0, sticky="ew", pady=PADY) + frame.columnconfigure(2, weight=3) + frame.columnconfigure(3, weight=1) + label = ttk.Label(frame, text="R") + label.grid(row=0, column=0, padx=PADX) + self.red_entry = validation.RgbEntry(frame, width=3, textvariable=self.red) + self.red_entry.grid(row=0, column=1, sticky="ew", padx=PADX) scale = ttk.Scale( frame, from_=0, to=255, value=0, - # length=200, orient=tk.HORIZONTAL, variable=self.red_scale, command=lambda x: self.scale_callback(self.red_scale, self.red), ) - scale.grid(row=0, column=2, sticky="nsew") + scale.grid(row=0, column=2, sticky="ew", padx=PADX) self.red_label = ttk.Label( frame, background="#%02x%02x%02x" % (self.red.get(), 0, 0), width=5 ) - self.red_label.grid(row=0, column=3, sticky="nsew") - frame.grid(row=0, column=0, sticky="nsew") + self.red_label.grid(row=0, column=3, sticky="ew") frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=6) - frame.columnconfigure(3, weight=2) - label = ttk.Label(frame, text="G: ") - label.grid(row=0, column=0) - self.green_entry = validation.RgbEntry(frame, width=4, textvariable=self.green) - self.green_entry.grid(row=0, column=1, sticky="nsew") + frame.grid(row=1, column=0, sticky="ew", pady=PADY) + frame.columnconfigure(2, weight=3) + frame.columnconfigure(3, weight=1) + label = ttk.Label(frame, text="G") + label.grid(row=0, column=0, padx=PADX) + self.green_entry = validation.RgbEntry(frame, width=3, textvariable=self.green) + self.green_entry.grid(row=0, column=1, sticky="ew", padx=PADX) scale = ttk.Scale( frame, from_=0, @@ -88,59 +87,54 @@ class ColorPickerDialog(Dialog): variable=self.green_scale, command=lambda x: self.scale_callback(self.green_scale, self.green), ) - scale.grid(row=0, column=2, sticky="nsew") + scale.grid(row=0, column=2, sticky="ew", padx=PADX) self.green_label = ttk.Label( frame, background="#%02x%02x%02x" % (0, self.green.get(), 0), width=5 ) - self.green_label.grid(row=0, column=3, sticky="nsew") - frame.grid(row=1, column=0, sticky="nsew") + self.green_label.grid(row=0, column=3, sticky="ew") frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=6) - frame.columnconfigure(3, weight=2) - label = ttk.Label(frame, text="B: ") - label.grid(row=0, column=0) - self.blue_entry = validation.RgbEntry(frame, width=4, textvariable=self.blue) - self.blue_entry.grid(row=0, column=1, sticky="nsew") + frame.grid(row=2, column=0, sticky="ew", pady=PADY) + frame.columnconfigure(2, weight=3) + frame.columnconfigure(3, weight=1) + label = ttk.Label(frame, text="B") + label.grid(row=0, column=0, padx=PADX) + self.blue_entry = validation.RgbEntry(frame, width=3, textvariable=self.blue) + self.blue_entry.grid(row=0, column=1, sticky="ew", padx=PADX) scale = ttk.Scale( frame, from_=0, to=255, value=0, - # length=200, orient=tk.HORIZONTAL, variable=self.blue_scale, command=lambda x: self.scale_callback(self.blue_scale, self.blue), ) - scale.grid(row=0, column=2, sticky="nsew") + scale.grid(row=0, column=2, sticky="ew", padx=PADX) self.blue_label = ttk.Label( frame, background="#%02x%02x%02x" % (0, 0, self.blue.get()), width=5 ) - self.blue_label.grid(row=0, column=3, sticky="nsew") - frame.grid(row=2, column=0, sticky="nsew") + self.blue_label.grid(row=0, column=3, sticky="ew") # hex code and color display frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) - label = ttk.Label(frame, text="Selection: ") - label.grid(row=0, column=0, sticky="nsew") + frame.rowconfigure(1, weight=1) self.hex_entry = validation.HexEntry(frame, textvariable=self.hex) - self.hex_entry.grid(row=1, column=0, sticky="nsew") + self.hex_entry.grid(sticky="ew", pady=PADY) self.display = tk.Frame(frame, background=self.color, width=100, height=100) - self.display.grid(row=2, column=0) - frame.grid(row=3, column=0, sticky="nsew") + self.display.grid(sticky="nsew") + frame.grid(row=3, column=0, sticky="nsew", pady=PADY) # button frame frame = ttk.Frame(self.top) + frame.grid(row=4, column=0, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="OK", command=self.button_ok) - button.grid(row=0, column=0, sticky="nsew") + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="nsew") - frame.grid(row=4, column=0, sticky="nsew") + button.grid(row=0, column=1, sticky="ew") def set_bindings(self): self.red_entry.bind("", lambda x: self.current_focus("rgb")) From d0520bf21d238fb1844a93d15dc154f939144c9c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 17 May 2020 08:51:51 -0700 Subject: [PATCH 0269/1131] pygui: fixed resizing toolbar with custom node selected --- daemon/core/gui/toolbar.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 572a523d..1c497b0d 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -138,6 +138,7 @@ class Toolbar(ttk.Frame): # these variables help keep track of what images being drawn so that scaling # is possible since PhotoImage does not have resize method self.node_enum = None + self.node_file = None self.network_enum = None self.annotation_enum = None @@ -275,7 +276,12 @@ class Toolbar(ttk.Frame): self.app.canvas.mode = GraphMode.NODE self.app.canvas.node_draw = node_draw if type_enum == NodeTypeEnum.NODE: - self.node_enum = node_draw.image_enum + if node_draw.image_enum: + self.node_enum = node_draw.image_enum + self.node_file = None + elif node_draw.image_file: + self.node_file = node_draw.image_file + self.node_enum = None elif type_enum == NodeTypeEnum.NETWORK: self.network_enum = node_draw.image_enum @@ -381,16 +387,26 @@ class Toolbar(ttk.Frame): self.marker_tool = MarkerDialog(self.app) self.marker_tool.show() - def scale_button(self, button, image_enum) -> None: - image = self.app.get_icon(image_enum, TOOLBAR_SIZE) - button.config(image=image) - button.image = image + def scale_button( + self, button: ttk.Button, image_enum: ImageEnum = None, image_file: str = None + ) -> None: + image = None + if image_enum: + image = self.app.get_icon(image_enum, TOOLBAR_SIZE) + elif image_file: + image = self.app.get_custom_icon(image_file, TOOLBAR_SIZE) + if image: + button.config(image=image) + button.image = image def scale(self) -> None: self.scale_button(self.play_button, ImageEnum.START) self.scale_button(self.select_button, ImageEnum.SELECT) self.scale_button(self.link_button, ImageEnum.LINK) - self.scale_button(self.node_button, self.node_enum) + if self.node_enum: + self.scale_button(self.node_button, self.node_enum) + if self.node_file: + self.scale_button(self.node_button, image_file=self.node_file) self.scale_button(self.network_button, self.network_enum) self.scale_button(self.annotation_button, self.annotation_enum) self.scale_button(self.runtime_select_button, ImageEnum.SELECT) From bd897efd0587b2d732afcaa3d6729da5160a6f53 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 17 May 2020 10:21:54 -0700 Subject: [PATCH 0270/1131] pygui: allow shapes to be moved in annotation/select modes and nodes in node/select modes --- daemon/core/gui/graph/graph.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index fcc8c81b..ad5b22b6 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -30,6 +30,8 @@ if TYPE_CHECKING: ZOOM_IN = 1.1 ZOOM_OUT = 0.9 ICON_SIZE = 48 +MOVE_NODE_MODES = {GraphMode.NODE, GraphMode.SELECT} +MOVE_SHAPE_MODES = {GraphMode.ANNOTATION, GraphMode.SELECT} class ShowVar(BooleanVar): @@ -653,9 +655,6 @@ class CanvasGraph(tk.Canvas): self.select_object(selected, choose_multiple=True) def click_motion(self, event: tk.Event): - """ - Redraw drawing edge according to the current position of the mouse - """ x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): if self.select_box: @@ -677,6 +676,7 @@ class CanvasGraph(tk.Canvas): if is_draw_shape(self.annotation_type) and self.shape_drawing: shape = self.shapes[self.selected] shape.shape_motion(x, y) + return elif is_marker(self.annotation_type): r = self.app.toolbar.marker_tool.radius self.create_oval( @@ -688,7 +688,7 @@ class CanvasGraph(tk.Canvas): outline="", tags=(tags.MARKER, tags.ANNOTATION), ) - return + return if self.mode == GraphMode.EDGE: return @@ -696,11 +696,11 @@ class CanvasGraph(tk.Canvas): # move selected objects if self.selection: for selected_id in self.selection: - if selected_id in self.shapes: + if self.mode in MOVE_SHAPE_MODES and selected_id in self.shapes: shape = self.shapes[selected_id] shape.motion(x_offset, y_offset) - if selected_id in self.nodes: + if self.mode in MOVE_NODE_MODES and selected_id in self.nodes: node = self.nodes[selected_id] node.motion(x_offset, y_offset, update=self.core.is_runtime()) else: From cde053da738ac676f19f53cd2711c3ca23b9036e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 17 May 2020 23:08:53 -0700 Subject: [PATCH 0271/1131] pygui: implemented toolbar based marker configuration, fixed some issues when switching between different node bar states --- daemon/core/gui/dialogs/marker.py | 72 ------------- daemon/core/gui/graph/graph.py | 8 +- daemon/core/gui/nodeutils.py | 1 + daemon/core/gui/toolbar.py | 166 ++++++++++++++++++++---------- 4 files changed, 119 insertions(+), 128 deletions(-) delete mode 100644 daemon/core/gui/dialogs/marker.py diff --git a/daemon/core/gui/dialogs/marker.py b/daemon/core/gui/dialogs/marker.py deleted file mode 100644 index 91cbfd06..00000000 --- a/daemon/core/gui/dialogs/marker.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -marker dialog -""" - -import tkinter as tk -from tkinter import ttk -from typing import TYPE_CHECKING - -from core.gui.dialogs.colorpicker import ColorPickerDialog -from core.gui.dialogs.dialog import Dialog -from core.gui.graph import tags - -if TYPE_CHECKING: - from core.gui.app import Application - -MARKER_THICKNESS = [3, 5, 8, 10] - - -class MarkerDialog(Dialog): - def __init__(self, app: "Application", initcolor: str = "#000000"): - super().__init__(app, "Marker Tool", modal=False) - self.color = initcolor - self.radius = MARKER_THICKNESS[0] - self.marker_thickness = tk.IntVar(value=MARKER_THICKNESS[0]) - self.draw() - - def draw(self): - button = ttk.Button(self.top, text="clear", command=self.clear_marker) - button.grid(row=0, column=0, sticky="nsew") - - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - frame.grid(row=1, column=0, sticky="nsew") - label = ttk.Label(frame, text="Thickness: ") - label.grid(row=0, column=0, sticky="nsew") - combobox = ttk.Combobox( - frame, - textvariable=self.marker_thickness, - values=MARKER_THICKNESS, - state="readonly", - ) - combobox.grid(row=0, column=1, sticky="nsew") - combobox.bind("<>", self.change_thickness) - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - frame.grid(row=2, column=0, sticky="nsew") - label = ttk.Label(frame, text="Color: ") - label.grid(row=0, column=0, sticky="nsew") - label = ttk.Label(frame, background=self.color) - label.grid(row=0, column=1, sticky="nsew") - label.bind("", self.change_color) - - def clear_marker(self): - canvas = self.app.canvas - canvas.delete(tags.MARKER) - - def change_color(self, event: tk.Event): - color_picker = ColorPickerDialog(self, self.app, self.color) - color = color_picker.askcolor() - event.widget.configure(background=color) - self.color = color - - def change_thickness(self, event: tk.Event): - self.radius = self.marker_thickness.get() - - def show(self): - super().show() - self.geometry( - f"+{self.app.canvas.winfo_rootx()}+{self.app.canvas.master.winfo_rooty()}" - ) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index ad5b22b6..512e9cad 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -596,13 +596,13 @@ class CanvasGraph(tk.Canvas): if self.mode == GraphMode.ANNOTATION: if is_marker(self.annotation_type): - r = self.app.toolbar.marker_tool.radius + r = self.app.toolbar.marker_frame.size.get() self.create_oval( x - r, y - r, x + r, y + r, - fill=self.app.toolbar.marker_tool.color, + fill=self.app.toolbar.marker_frame.color, outline="", tags=(tags.MARKER, tags.ANNOTATION), state=self.show_annotations.state(), @@ -678,13 +678,13 @@ class CanvasGraph(tk.Canvas): shape.shape_motion(x, y) return elif is_marker(self.annotation_type): - r = self.app.toolbar.marker_tool.radius + r = self.app.toolbar.marker_frame.size.get() self.create_oval( x - r, y - r, x + r, y + r, - fill=self.app.toolbar.marker_tool.color, + fill=self.app.toolbar.marker_frame.color, outline="", tags=(tags.MARKER, tags.ANNOTATION), ) diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index 4c2cec07..40204662 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -22,6 +22,7 @@ class NodeDraw: self.node_type: core_pb2.NodeType = None self.model: Optional[str] = None self.services: Set[str] = set() + self.label = None @classmethod def from_setup( diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 1c497b0d..c985a503 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -8,8 +8,9 @@ from typing import TYPE_CHECKING, Callable from PIL.ImageTk import PhotoImage from core.api.grpc import core_pb2 -from core.gui.dialogs.marker import MarkerDialog +from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.runtool import RunToolDialog +from core.gui.graph import tags from core.gui.graph.enums import GraphMode from core.gui.graph.shapeutils import ShapeType, is_marker from core.gui.images import ImageEnum @@ -43,19 +44,24 @@ class PickerFrame(ttk.Frame): self.app = app self.button = button - def create_button(self, label: str, image_enum: ImageEnum, func: Callable) -> None: - bar_image = self.app.get_icon(image_enum, TOOLBAR_SIZE) - image = self.app.get_icon(image_enum, PICKER_SIZE) - self._create_button(label, image, bar_image, func) + def create_node_button(self, node_draw: NodeDraw, func: Callable) -> None: + self.create_button( + node_draw.label, func, node_draw.image_enum, node_draw.image_file + ) - def create_custom_button(self, label: str, image_file: str, func: Callable) -> None: - bar_image = self.app.get_custom_icon(image_file, TOOLBAR_SIZE) - image = self.app.get_custom_icon(image_file, PICKER_SIZE) - self._create_button(label, image, bar_image, func) - - def _create_button( - self, label: str, image: PhotoImage, bar_image: PhotoImage, func: Callable + def create_button( + self, + label: str, + func: Callable, + image_enum: ImageEnum = None, + image_file: str = None, ) -> None: + if image_enum: + bar_image = self.app.get_icon(image_enum, TOOLBAR_SIZE) + image = self.app.get_icon(image_enum, PICKER_SIZE) + else: + bar_image = self.app.get_custom_icon(image_file, TOOLBAR_SIZE) + image = self.app.get_custom_icon(image_file, PICKER_SIZE) button = ttk.Button( self, image=image, text=label, compound=tk.TOP, style=Styles.picker_button ) @@ -101,6 +107,51 @@ class ButtonBar(ttk.Frame): selected.state(["pressed"]) +class MarkerFrame(ttk.Frame): + PAD = 3 + + def __init__(self, master: tk.BaseWidget, app: "Application") -> None: + super().__init__(master, padding=self.PAD) + self.app = app + self.color = "#000000" + self.size = tk.DoubleVar() + self.color_frame = None + self.draw() + + def draw(self) -> None: + self.columnconfigure(0, weight=1) + + image = self.app.get_icon(ImageEnum.DELETE, 16) + button = ttk.Button(self, image=image, width=2, command=self.click_clear) + button.image = image + button.grid(sticky="ew", pady=self.PAD) + Tooltip(button, "Delete Marker") + + sizes = [1, 3, 8, 10] + self.size.set(sizes[0]) + sizes = ttk.Combobox( + self, state="readonly", textvariable=self.size, value=sizes, width=2 + ) + sizes.grid(sticky="ew", pady=self.PAD) + Tooltip(sizes, "Marker Size") + + frame_size = TOOLBAR_SIZE + self.color_frame = tk.Frame( + self, background=self.color, height=frame_size, width=frame_size + ) + self.color_frame.grid(sticky="ew") + self.color_frame.bind("", self.click_color) + Tooltip(self.color_frame, "Marker Color") + + def click_clear(self): + self.app.canvas.delete(tags.MARKER) + + def click_color(self, _event: tk.Event) -> None: + dialog = ColorPickerDialog(self.app, self.app, self.color) + self.color = dialog.askcolor() + self.color_frame.config(background=self.color) + + class Toolbar(ttk.Frame): """ Core toolbar class @@ -130,17 +181,15 @@ class Toolbar(ttk.Frame): # frames self.design_frame = None self.runtime_frame = None + self.marker_frame = None self.picker = None - # dialog - self.marker_tool = None - # these variables help keep track of what images being drawn so that scaling # is possible since PhotoImage does not have resize method - self.node_enum = None - self.node_file = None - self.network_enum = None - self.annotation_enum = None + self.current_node = NodeUtils.NODES[0] + self.current_network = NodeUtils.NETWORK_NODES[0] + self.current_annotation = ShapeType.MARKER + self.annotation_enum = ImageEnum.MARKER # draw components self.draw() @@ -151,6 +200,7 @@ class Toolbar(ttk.Frame): self.draw_design_frame() self.draw_runtime_frame() self.design_frame.tkraise() + self.marker_frame = MarkerFrame(self, self.app) def draw_design_frame(self) -> None: self.design_frame = ButtonBar(self, self.app) @@ -165,15 +215,18 @@ class Toolbar(ttk.Frame): self.link_button = self.design_frame.create_button( ImageEnum.LINK, self.click_link, "Link Tool", radio=True ) - self.node_enum = ImageEnum.ROUTER self.node_button = self.design_frame.create_button( - self.node_enum, self.draw_node_picker, "Container Nodes", radio=True + self.current_node.image_enum, + self.draw_node_picker, + "Container Nodes", + radio=True, ) - self.network_enum = ImageEnum.HUB self.network_button = self.design_frame.create_button( - self.network_enum, self.draw_network_picker, "Link Layer Nodes", radio=True + self.current_network.image_enum, + self.draw_network_picker, + "Link Layer Nodes", + radio=True, ) - self.annotation_enum = ImageEnum.MARKER self.annotation_button = self.design_frame.create_button( self.annotation_enum, self.draw_annotation_picker, @@ -199,6 +252,9 @@ class Toolbar(ttk.Frame): ) def draw_node_picker(self) -> None: + self.hide_marker() + self.app.canvas.mode = GraphMode.NODE + self.app.canvas.node_draw = self.current_node self.design_frame.select_radio(self.node_button) self.picker = PickerFrame(self.app, self.node_button) # draw default nodes @@ -206,25 +262,25 @@ class Toolbar(ttk.Frame): func = partial( self.update_button, self.node_button, node_draw, NodeTypeEnum.NODE ) - self.picker.create_button(node_draw.label, node_draw.image_enum, func) + self.picker.create_node_button(node_draw, func) # draw custom nodes for name in sorted(self.app.core.custom_nodes): node_draw = self.app.core.custom_nodes[name] func = partial( self.update_button, self.node_button, node_draw, NodeTypeEnum.NODE ) - self.picker.create_custom_button( - node_draw.label, node_draw.image_file, func - ) + self.picker.create_node_button(node_draw, func) self.picker.show() def click_selection(self) -> None: self.design_frame.select_radio(self.select_button) self.app.canvas.mode = GraphMode.SELECT + self.hide_marker() def click_runtime_selection(self) -> None: self.runtime_frame.select_radio(self.runtime_select_button) self.app.canvas.mode = GraphMode.SELECT + self.hide_marker() def click_start(self) -> None: """ @@ -253,15 +309,18 @@ class Toolbar(ttk.Frame): enable_buttons(self.runtime_frame, enabled=True) self.runtime_frame.tkraise() self.click_runtime_selection() + self.hide_marker() def set_design(self) -> None: enable_buttons(self.design_frame, enabled=True) self.design_frame.tkraise() self.click_selection() + self.hide_marker() def click_link(self) -> None: self.design_frame.select_radio(self.link_button) self.app.canvas.mode = GraphMode.EDGE + self.hide_marker() def update_button( self, @@ -273,29 +332,26 @@ class Toolbar(ttk.Frame): logging.debug("update button(%s): %s", button, node_draw) button.configure(image=image) button.image = image - self.app.canvas.mode = GraphMode.NODE self.app.canvas.node_draw = node_draw if type_enum == NodeTypeEnum.NODE: - if node_draw.image_enum: - self.node_enum = node_draw.image_enum - self.node_file = None - elif node_draw.image_file: - self.node_file = node_draw.image_file - self.node_enum = None + self.current_node = node_draw elif type_enum == NodeTypeEnum.NETWORK: - self.network_enum = node_draw.image_enum + self.current_network = node_draw def draw_network_picker(self) -> None: """ Draw the options for link-layer button. """ + self.hide_marker() + self.app.canvas.mode = GraphMode.NODE + self.app.canvas.node_draw = self.current_network self.design_frame.select_radio(self.network_button) self.picker = PickerFrame(self.app, self.network_button) for node_draw in NodeUtils.NETWORK_NODES: func = partial( self.update_button, self.network_button, node_draw, NodeTypeEnum.NETWORK ) - self.picker.create_button(node_draw.label, node_draw.image_enum, func) + self.picker.create_node_button(node_draw, func) self.picker.show() def draw_annotation_picker(self) -> None: @@ -303,6 +359,10 @@ class Toolbar(ttk.Frame): Draw the options for marker button. """ self.design_frame.select_radio(self.annotation_button) + self.app.canvas.mode = GraphMode.ANNOTATION + self.app.canvas.annotation_type = self.current_annotation + if is_marker(self.current_annotation): + self.show_marker() self.picker = PickerFrame(self.app, self.annotation_button) nodes = [ (ImageEnum.MARKER, ShapeType.MARKER), @@ -313,7 +373,7 @@ class Toolbar(ttk.Frame): for image_enum, shape_type in nodes: label = shape_type.value func = partial(self.update_annotation, shape_type, image_enum) - self.picker.create_button(label, image_enum, func) + self.picker.create_button(label, func, image_enum) self.picker.show() def create_observe_button(self) -> None: @@ -364,14 +424,19 @@ class Toolbar(ttk.Frame): logging.debug("clicked annotation") self.annotation_button.configure(image=image) self.annotation_button.image = image - self.app.canvas.mode = GraphMode.ANNOTATION self.app.canvas.annotation_type = shape_type + self.current_annotation = shape_type self.annotation_enum = image_enum if is_marker(shape_type): - if self.marker_tool: - self.marker_tool.destroy() - self.marker_tool = MarkerDialog(self.app) - self.marker_tool.show() + self.show_marker() + else: + self.hide_marker() + + def hide_marker(self) -> None: + self.marker_frame.grid_forget() + + def show_marker(self) -> None: + self.marker_frame.grid() def click_run_button(self) -> None: logging.debug("Click on RUN button") @@ -382,10 +447,7 @@ class Toolbar(ttk.Frame): self.runtime_frame.select_radio(self.runtime_marker_button) self.app.canvas.mode = GraphMode.ANNOTATION self.app.canvas.annotation_type = ShapeType.MARKER - if self.marker_tool: - self.marker_tool.destroy() - self.marker_tool = MarkerDialog(self.app) - self.marker_tool.show() + self.show_marker() def scale_button( self, button: ttk.Button, image_enum: ImageEnum = None, image_file: str = None @@ -403,11 +465,11 @@ class Toolbar(ttk.Frame): self.scale_button(self.play_button, ImageEnum.START) self.scale_button(self.select_button, ImageEnum.SELECT) self.scale_button(self.link_button, ImageEnum.LINK) - if self.node_enum: - self.scale_button(self.node_button, self.node_enum) - if self.node_file: - self.scale_button(self.node_button, image_file=self.node_file) - self.scale_button(self.network_button, self.network_enum) + if self.current_node.image_enum: + self.scale_button(self.node_button, self.current_node) + else: + self.scale_button(self.node_button, image_file=self.current_node.image_file) + self.scale_button(self.network_button, self.current_network.image_enum) self.scale_button(self.annotation_button, self.annotation_enum) self.scale_button(self.runtime_select_button, ImageEnum.SELECT) self.scale_button(self.stop_button, ImageEnum.STOP) From 773f733cb8ecb82e1a9c38ce71d3cfc21288b50b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 18 May 2020 23:25:42 -0700 Subject: [PATCH 0272/1131] pygui: changes to leverage common icon scaling function, fix issue with scaling toolbar --- daemon/core/gui/dialogs/mobilityplayer.py | 8 ++++---- daemon/core/gui/dialogs/serviceconfig.py | 8 ++++---- daemon/core/gui/graph/graph.py | 22 ++++++++-------------- daemon/core/gui/graph/node.py | 8 +++----- daemon/core/gui/toolbar.py | 2 +- 5 files changed, 20 insertions(+), 28 deletions(-) diff --git a/daemon/core/gui/dialogs/mobilityplayer.py b/daemon/core/gui/dialogs/mobilityplayer.py index e3baf140..b4801bcf 100644 --- a/daemon/core/gui/dialogs/mobilityplayer.py +++ b/daemon/core/gui/dialogs/mobilityplayer.py @@ -6,7 +6,7 @@ import grpc from core.api.grpc.mobility_pb2 import MobilityAction from core.gui.dialogs.dialog import Dialog -from core.gui.images import ImageEnum, Images +from core.gui.images import ImageEnum from core.gui.themes import PADX, PADY if TYPE_CHECKING: @@ -89,17 +89,17 @@ class MobilityPlayerDialog(Dialog): for i in range(3): frame.columnconfigure(i, weight=1) - image = Images.get(ImageEnum.START, width=int(ICON_SIZE * self.app.app_scale)) + image = self.app.get_icon(ImageEnum.START, ICON_SIZE) self.play_button = ttk.Button(frame, image=image, command=self.click_play) self.play_button.image = image self.play_button.grid(row=0, column=0, sticky="ew", padx=PADX) - image = Images.get(ImageEnum.PAUSE, width=int(ICON_SIZE * self.app.app_scale)) + image = self.app.get_icon(ImageEnum.PAUSE, ICON_SIZE) self.pause_button = ttk.Button(frame, image=image, command=self.click_pause) self.pause_button.image = image self.pause_button.grid(row=0, column=1, sticky="ew", padx=PADX) - image = Images.get(ImageEnum.STOP, width=int(ICON_SIZE * self.app.app_scale)) + image = self.app.get_icon(ImageEnum.STOP, ICON_SIZE) self.stop_button = ttk.Button(frame, image=image, command=self.click_stop) self.stop_button.image = image self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX) diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index 30607163..efeefa09 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -17,6 +17,8 @@ if TYPE_CHECKING: from core.gui.app import Application from core.gui.graph.node import CanvasNode +ICON_SIZE = 16 + class ServiceConfigDialog(Dialog): def __init__( @@ -51,10 +53,8 @@ class ServiceConfigDialog(Dialog): self.directory_entry = None self.default_directories = [] self.temp_directories = [] - self.documentnew_img = Images.get( - ImageEnum.DOCUMENTNEW, int(16 * app.app_scale) - ) - self.editdelete_img = Images.get(ImageEnum.EDITDELETE, int(16 * app.app_scale)) + self.documentnew_img = self.app.get_icon(ImageEnum.DOCUMENTNEW, ICON_SIZE) + self.editdelete_img = self.app.get_icon(ImageEnum.EDITDELETE, ICON_SIZE) self.notebook = None self.metadata_entry = None self.filename_combobox = None diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 512e9cad..3d6fd369 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -20,7 +20,7 @@ from core.gui.graph.enums import GraphMode, ScaleOption from core.gui.graph.node import CanvasNode from core.gui.graph.shape import Shape from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker -from core.gui.images import ImageEnum, Images, TypeToImage +from core.gui.images import ImageEnum, TypeToImage from core.gui.nodeutils import NodeUtils if TYPE_CHECKING: @@ -290,9 +290,7 @@ class CanvasGraph(tk.Canvas): ) # if the gui can't find node's image, default to the "edit-node" image if not image: - image = Images.get( - ImageEnum.EDITNODE, int(ICON_SIZE * self.app.app_scale) - ) + image = self.app.get_icon(ImageEnum.EDITNODE, ICON_SIZE) x = core_node.position.x y = core_node.position.y node = CanvasNode(self.app, x, y, core_node, image) @@ -734,13 +732,11 @@ class CanvasGraph(tk.Canvas): if not core_node: return try: - self.node_draw.image = Images.get( - self.node_draw.image_enum, int(ICON_SIZE * self.app.app_scale) - ) + image_enum = self.node_draw.image_enum + self.node_draw.image = self.app.get_icon(image_enum, ICON_SIZE) except AttributeError: - self.node_draw.image = Images.get_custom( - self.node_draw.image_file, int(ICON_SIZE * self.app.app_scale) - ) + image_file = self.node_draw.image_file + self.node_draw.image = self.app.get_custom_icon(image_file, ICON_SIZE) node = CanvasNode(self.app, x, y, core_node, self.node_draw.image) self.core.canvas_nodes[core_node.id] = node self.nodes[node.id] = node @@ -1006,14 +1002,12 @@ class CanvasGraph(tk.Canvas): ): for custom_node in self.app.guiconfig.nodes: if custom_node.name == canvas_node.core_node.model: - img = Images.get_custom( - custom_node.image, int(ICON_SIZE * self.app.app_scale) - ) + img = self.app.get_custom_icon(custom_node.image, ICON_SIZE) else: image_enum = TypeToImage.get( canvas_node.core_node.type, canvas_node.core_node.model ) - img = Images.get(image_enum, int(ICON_SIZE * self.app.app_scale)) + img = self.app.get_icon(image_enum, ICON_SIZE) self.itemconfig(nid, image=img) canvas_node.image = img diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 0b4f2bc9..8ad3f02a 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -17,7 +17,7 @@ from core.gui.dialogs.wlanconfig import WlanConfigDialog from core.gui.graph import tags from core.gui.graph.edges import CanvasEdge from core.gui.graph.tooltip import CanvasTooltip -from core.gui.images import ImageEnum, Images +from core.gui.images import ImageEnum from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils if TYPE_CHECKING: @@ -91,7 +91,7 @@ class CanvasNode: def add_antenna(self): x, y = self.canvas.coords(self.id) offset = len(self.antennas) * 8 * self.app.app_scale - img = Images.get(ImageEnum.ANTENNA, int(ANTENNA_SIZE * self.app.app_scale)) + img = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE) antenna_id = self.canvas.create_image( x - 16 + offset, y - int(23 * self.app.app_scale), @@ -327,9 +327,7 @@ class CanvasNode: def scale_antennas(self): for i in range(len(self.antennas)): antenna_id = self.antennas[i] - image = Images.get( - ImageEnum.ANTENNA, int(ANTENNA_SIZE * self.app.app_scale) - ) + image = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE) self.canvas.itemconfig(antenna_id, image=image) self.antenna_images[antenna_id] = image node_x, node_y = self.canvas.coords(self.id) diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index c985a503..da20948e 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -466,7 +466,7 @@ class Toolbar(ttk.Frame): self.scale_button(self.select_button, ImageEnum.SELECT) self.scale_button(self.link_button, ImageEnum.LINK) if self.current_node.image_enum: - self.scale_button(self.node_button, self.current_node) + self.scale_button(self.node_button, self.current_node.image_enum) else: self.scale_button(self.node_button, image_file=self.current_node.image_file) self.scale_button(self.network_button, self.current_network.image_enum) From 8bae0611a43377a18f8505790d242102c21b29ce Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 19 May 2020 00:35:48 -0700 Subject: [PATCH 0273/1131] pygui: updated icon for observers tool on run menu, added observers to run menu and created custom observers widget --- daemon/core/gui/data/icons/observe.gif | Bin 1149 -> 0 bytes daemon/core/gui/data/icons/observe.png | Bin 0 -> 3182 bytes daemon/core/gui/dialogs/observers.py | 6 ++- daemon/core/gui/menubar.py | 49 +----------------- daemon/core/gui/observers.py | 66 +++++++++++++++++++++++++ daemon/core/gui/toolbar.py | 26 +++------- 6 files changed, 80 insertions(+), 67 deletions(-) delete mode 100644 daemon/core/gui/data/icons/observe.gif create mode 100644 daemon/core/gui/data/icons/observe.png create mode 100644 daemon/core/gui/observers.py diff --git a/daemon/core/gui/data/icons/observe.gif b/daemon/core/gui/data/icons/observe.gif deleted file mode 100644 index 6b66e7305f35484095a2182ed828c6507c72a746..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1149 zcmd_pYfn=L0LJlylbbZAPQ->d0;0q*%+PGvh{hCQEG*WwP@oJp1|%#lOP0+=yD9^E zJMC#%P#A&~sl5%f$fdVaENf{gXltbuDYVeS>4mnZW77@Ay;;_Njy?H(f#=2lWZqG%4!O^wyx&OriRAmhAY&|RN9pvX;*32s2z+p0&S8&Q(U~J z`1HDh)T$s}R}x#5q*i6+PpZl`6}e4S)vl^;SJiZ=Yv^hUU0qAp)G{=mF*Nl|O&t@i zXTqPe;4j#41E_80z%3B`6)#3hlP=O(9c(TGgqR=%a(E!f5Ddi(3PnRA>5yz#qJh;~oqAfY1LaE{BhQ%km%&jd zZ)}wU1Re`@`9oJa7`GpO_2 znf6ZT%y*~P#QJrKeqCd6rkk^g3>#8|2exj&Pd3Cen=<33%!K@ELWXCN zky+%P*>~UUlh66(^ZwC!|AUxE{;{~g*yF%>j3qE(2~5TX71p5gNl<0m(mag`!3!a6 zj6F1E4?SEA|F#s-FGUP<7R!Rey5O)mJx^D>3o)zS8Hdlb>yO>g zpS$Rb*U=ZRf&bZZ{@I%V0OLSG{CB(wAmv}6G4)7z54#w5GAY0Qgx$RVR97jNeyk-K z!%n+IJ#Z+6-;>|?jPuv|H^lhNu21mKffLy5bD4QP?c|#|n0Jp8wco#gbTJ1jr?B#o zpwIZ0DDQaz=@LnV&pIoiXMa6!%xH2@H(fh#3yU%-1l+Ma#DMEE?9C(%k6MeqHBjI^ zNfj8@$s_i!;2S6J=`b1%yJ zjzYQTh5_ro()5X;@-#bkr;C3s1KUs+AeNEHWZP~Kr zc&4>nis=T_)YLr0%zgkxhDGF01E$Ue70m>^Q$cy9V0SJ^%>n>$ECNIafp8Dd-vJJ_ zgFBA`{pXXB*3ZoA1A#zuawW1B(+sGqt5aHATVEierwnCWR1TWG94M~>Z$eq}D3EGXlMFdUP_DfLz~}S5NJQV&O^|;TlrQ}NG-DC$?Q|2UWDaQh zLZIWb_zPUlqEIN57Yqh>*eGrsDI4JP`BoFr3wrTXG6$|Zp9JNVrkX|CP<8<@YZ=ge z0toB&$8N8zto$q(3?8sk+eK2kE!VAEcb%dr9|I_q{^+#%pxYh?xjbo!GYu^Yv1c>* z+#$UbAtI{s`~4^Elm!543fGXLD8GkZ<(FOq<<`f5BrD(R2BvuxOvxn_cnG;F;C99D zj}eU#jsX{IQ~c|f;44(xgr+$HUc6*6-Gz@3N>@YzPvPe65b^49)3m1`t9$^;&EGIo`KkgeouOjI%=9ZC01C5! zl`~yfI^6}AfznXX)xg59>qVCl(GSyCWgNp#z?wB{3bL}YTBSqE#k1l~4^3PHIc~y= zauq>BKJYPY3KO;6_i$tKp8Q``( zQmM?`6$}QYk!F)j6SPhiNYfT399~c;-b{GiMp*MP2)Ps|Zft#LK!VaNcV&$kr7LRlQ$cz?p{r{u)%*0Hqr z3+!l(p>H_#ufERiUrtfE zp;66&D4>n0=hGJicAwFpnJ^$66Tt3v4X4MLzyKmz;%lk&TA*--G)@TdX!`0BW3(f7 zpDa+Bz1+~n;TYpzr((FSSiyy1#;4uF)ZPvQ*we0|uar@ir=TrlDke1y{j2LjeERgccOqP;Cbjm_R@<+o&5*onF(y<@T&D=zb*-)Oeh`MDr z6C+PRzATUo4mn>D=#+(sq-8e~BL;Y-q3R@=?J@&ZcUX2(7~o(bMhqB`hQ^Ya6P)D4 zB6=~F4@ny_KrSLSWUi7-G9oVvGiTlaB9eE-2fIxdlPP#mUPRrpn~0Im(9fjd{a5tX ztnuiV<%rc;8!rM>5okpWH;QtPcLe})pG%W z@s5~$_3)|xn5QH|h_=1baAtnnj{J?z2{*~Yxvw4QIccsU8{w|wKUhPzMbE>~yf-`SAFl>O|@9$^kS7dSAdkTE~J@Zsq3?JJG z^vOG4MD&uK=S(Cu936>7UIHNhX#Ql|712B9zBAwhf76RTud3?HcIrvWfTpIVUS|G@ zG*ZOCANYZ2*c4%w5FG@6uvu?!0GRoi`uh5iosyE?q!$bZgH=^k_W-zjMQ(Hu=sXIV zxdc#B+5Kc4tVO}^|2F>VEQ@D;FAxa)$Yxn7&4{t4Y3l$;?-cH7h4`QW(7DSpf))ku zuGiZi1^|7EqEy4?{b4|g+f?`O-``tVS$Pb=x1F};Ln*IFdb6I{a5M~leU^oPv*$yIs z>l|jp1XFIwy1F{Gt*vbn;s^3goZ<8Zpc}s&-yC5g+;tp$_^pX{!y6?`L|^m!{qLHg zD-mY42~=HO{VM=Z8%kX`1Ip}Mf%3}u67f_B_ki2?f!p?i^+z>R`enOJFuhUs`FyJs zMe!$dLLzy~KxNm13(Mnw=amAUyf0SY#iHQoK)k~HJ3yiK_%xz?CQFYC0G~z>$n`w-dUI1CnM{(!lDA?_P0h8VXD_U<;`ft0G}}P4ggK5Oe21$y1MFgXK3&6h?a2l zaMXl#uQxYsO+~DXj2~aWe*O9>(P(rf5zR;ZQ15vFN)SKz_2S-y0RVl7A4PnGhz=9c zK~+_EnXz=_z6T%Pa3K None: @@ -201,42 +189,9 @@ class Menubar(tk.Menu): """ Create observer widget menu item and create the sub menu items inside """ - self.observers_menu = tk.Menu(widget_menu) - self.observers_menu.add_command( - label="Edit Observers", command=self.click_edit_observer_widgets - ) - self.observers_menu.add_separator() - self.observers_menu.add_radiobutton( - label="None", - variable=self.observers_var, - value="none", - command=lambda: self.core.set_observer(None), - ) - for name in sorted(OBSERVERS): - cmd = OBSERVERS[name] - self.observers_menu.add_radiobutton( - label=name, - variable=self.observers_var, - value=name, - command=partial(self.core.set_observer, cmd), - ) - self.observers_custom_index = self.observers_menu.index(tk.END) + 1 - self.draw_custom_observers() + self.observers_menu = ObserversMenu(widget_menu, self.app) widget_menu.add_cascade(label="Observer Widgets", menu=self.observers_menu) - def draw_custom_observers(self) -> None: - current_observers_index = self.observers_menu.index(tk.END) + 1 - if self.observers_custom_index < current_observers_index: - self.observers_menu.delete(self.observers_custom_index, tk.END) - for name in sorted(self.core.custom_observers): - observer = self.core.custom_observers[name] - self.observers_menu.add_radiobutton( - label=name, - variable=self.observers_var, - value=name, - command=partial(self.core.set_observer, observer.cmd), - ) - def create_adjacency_menu(self, widget_menu: tk.Menu) -> None: """ Create adjacency menu item and the sub menu items inside diff --git a/daemon/core/gui/observers.py b/daemon/core/gui/observers.py new file mode 100644 index 00000000..27d0a26e --- /dev/null +++ b/daemon/core/gui/observers.py @@ -0,0 +1,66 @@ +import tkinter as tk +from functools import partial +from typing import TYPE_CHECKING + +from core.gui.dialogs.observers import ObserverDialog + +if TYPE_CHECKING: + from core.gui.app import Application + +OBSERVERS = { + "List Processes": "ps", + "Show Interfaces": "ip address", + "IPV4 Routes": "ip -4 route", + "IPV6 Routes": "ip -6 route", + "Listening Sockets": "ss -tuwnl", + "IPv4 MFC Entries": "ip -4 mroute show", + "IPv6 MFC Entries": "ip -6 mroute show", + "Firewall Rules": "iptables -L", + "IPSec Policies": "setkey -DP", +} + + +class ObserversMenu(tk.Menu): + def __init__(self, master: tk.BaseWidget, app: "Application") -> None: + super().__init__(master) + self.app = app + self.observer = tk.StringVar(value=tk.NONE) + self.custom_index = 0 + self.draw() + + def draw(self) -> None: + self.add_command(label="Edit Observers", command=self.click_edit) + self.add_separator() + self.add_radiobutton( + label="None", + variable=self.observer, + value="none", + command=lambda: self.app.core.set_observer(None), + ) + for name in sorted(OBSERVERS): + cmd = OBSERVERS[name] + self.add_radiobutton( + label=name, + variable=self.observer, + value=name, + command=partial(self.app.core.set_observer, cmd), + ) + self.custom_index = self.index(tk.END) + 1 + self.draw_custom() + + def draw_custom(self) -> None: + current_index = self.index(tk.END) + 1 + if self.custom_index < current_index: + self.delete(self.custom_index, tk.END) + for name in sorted(self.app.core.custom_observers): + observer = self.app.core.custom_observers[name] + self.add_radiobutton( + label=name, + variable=self.observer, + value=name, + command=partial(self.app.core.set_observer, observer.cmd), + ) + + def click_edit(self) -> None: + dialog = ObserverDialog(self.app) + dialog.show() diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index da20948e..54fac126 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -15,6 +15,7 @@ from core.gui.graph.enums import GraphMode from core.gui.graph.shapeutils import ShapeType, is_marker from core.gui.images import ImageEnum from core.gui.nodeutils import NodeDraw, NodeUtils +from core.gui.observers import ObserversMenu from core.gui.task import ProgressTask from core.gui.themes import Styles from core.gui.tooltip import Tooltip @@ -184,6 +185,9 @@ class Toolbar(ttk.Frame): self.marker_frame = None self.picker = None + # observers + self.observers_menu = None + # these variables help keep track of what images being drawn so that scaling # is possible since PhotoImage does not have resize method self.current_node = NodeUtils.NODES[0] @@ -244,6 +248,7 @@ class Toolbar(ttk.Frame): self.runtime_select_button = self.runtime_frame.create_button( ImageEnum.SELECT, self.click_runtime_selection, "Selection Tool", radio=True ) + self.create_observe_button() self.runtime_marker_button = self.runtime_frame.create_button( ImageEnum.MARKER, self.click_marker_button, "Marker Tool", radio=True ) @@ -381,25 +386,10 @@ class Toolbar(ttk.Frame): menu_button = ttk.Menubutton( self.runtime_frame, image=image, direction=tk.RIGHT ) + menu_button.image = image menu_button.grid(sticky="ew") - menu = tk.Menu(menu_button, tearoff=0) - menu_button["menu"] = menu - menu.add_command(label="None") - menu.add_command(label="processes") - menu.add_command(label="ifconfig") - menu.add_command(label="IPv4 routes") - menu.add_command(label="IPv6 routes") - menu.add_command(label="OSPFv2 neighbors") - menu.add_command(label="OSPFv3 neighbors") - menu.add_command(label="Listening sockets") - menu.add_command(label="IPv4 MFC entries") - menu.add_command(label="IPv6 MFC entries") - menu.add_command(label="firewall rules") - menu.add_command(label="IPSec policies") - menu.add_command(label="docker logs") - menu.add_command(label="OSPFv3 MDR level") - menu.add_command(label="PIM neighbors") - menu.add_command(label="Edit...") + self.observers_menu = ObserversMenu(menu_button, self.app) + menu_button["menu"] = self.observers_menu def click_stop(self) -> None: """ From d14056393b3dbc3776254474505c1f8e8e425f0e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 19 May 2020 16:46:44 -0700 Subject: [PATCH 0274/1131] added grpc call to allow direct control of nodes connected through wlan to be linked or not --- daemon/core/api/grpc/client.py | 14 ++++++++++ daemon/core/api/grpc/server.py | 37 ++++++++++++++++++++++++++- daemon/core/location/mobility.py | 6 +---- daemon/proto/core/api/grpc/core.proto | 2 ++ daemon/proto/core/api/grpc/wlan.proto | 12 +++++++++ 5 files changed, 65 insertions(+), 6 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 76e20426..c5bbf50f 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -88,6 +88,8 @@ from core.api.grpc.wlan_pb2 import ( GetWlanConfigsResponse, SetWlanConfigRequest, SetWlanConfigResponse, + SetWlanLinkRequest, + SetWlanLinkResponse, WlanConfig, ) @@ -1204,6 +1206,18 @@ class CoreGrpcClient: request = ExecuteScriptRequest(script=script) return self.stub.ExecuteScript(request) + def set_wlan_link( + self, session_id: int, wlan: int, node_one: int, node_two: int, linked: bool + ) -> SetWlanLinkResponse: + request = SetWlanLinkRequest( + session_id=session_id, + wlan=wlan, + node_one=node_one, + node_two=node_two, + linked=linked, + ) + return self.stub.SetWlanLink(request) + def connect(self) -> None: """ Open connection to server, must be closed manually. diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index b6a298db..4e450a2a 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -102,6 +102,8 @@ from core.api.grpc.wlan_pb2 import ( GetWlanConfigsResponse, SetWlanConfigRequest, SetWlanConfigResponse, + SetWlanLinkRequest, + SetWlanLinkResponse, ) from core.emulator.coreemu import CoreEmu from core.emulator.data import LinkData @@ -111,6 +113,7 @@ from core.emulator.session import Session from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.nodes.base import CoreNodeBase, NodeBase +from core.nodes.network import WlanNode from core.services.coreservices import ServiceManager _ONE_DAY_IN_SECONDS = 60 * 60 * 24 @@ -177,7 +180,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param session: session that has the node :param node_id: node id - :param context: + :param context: request :return: node object that satisfies. If node not found then raise an exception. :raises Exception: raises grpc exception when node does not exist """ @@ -1684,3 +1687,35 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): if new_sessions: new_session = new_sessions[0] return ExecuteScriptResponse(session_id=new_session) + + def SetWlanLink( + self, request: SetWlanLinkRequest, context: ServicerContext + ) -> SetWlanLinkResponse: + session = self.get_session(request.session_id, context) + wlan = self.get_node(session, request.wlan, context) + if not isinstance(wlan, WlanNode): + context.abort( + grpc.StatusCode.NOT_FOUND, f"wlan id {request.wlan} is not a wlan node" + ) + if not isinstance(wlan.model, BasicRangeModel): + context.abort( + grpc.StatusCode.NOT_FOUND, + f"wlan node {request.wlan} does not using BasicRangeModel", + ) + n1 = self.get_node(session, request.node_one, context) + n2 = self.get_node(session, request.node_two, context) + n1_netif, n2_netif = None, None + for net, netif1, netif2 in n1.commonnets(n2): + if net == wlan: + n1_netif = netif1 + n2_netif = netif2 + break + if n1_netif and n2_netif: + if request.linked: + wlan.link(n1_netif, n2_netif) + else: + wlan.unlink(n1_netif, n2_netif) + wlan.model.sendlinkmsg(n1_netif, n2_netif, unlink=not request.linked) + return SetWlanLinkResponse(result=True) + else: + return SetWlanLinkResponse(result=False) diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 05a6ac3e..d2cb0c8a 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -509,11 +509,7 @@ class BasicRangeModel(WirelessModel): :param unlink: unlink or not :return: nothing """ - if unlink: - message_type = MessageFlags.DELETE - else: - message_type = MessageFlags.ADD - + message_type = MessageFlags.DELETE if unlink else MessageFlags.ADD link_data = self.create_link_data(netif, netif2, message_type) self.session.broadcast_link(link_data) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 997d5287..421b6f0f 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -129,6 +129,8 @@ service CoreApi { } rpc SetWlanConfig (wlan.SetWlanConfigRequest) returns (wlan.SetWlanConfigResponse) { } + rpc SetWlanLink (wlan.SetWlanLinkRequest) returns (wlan.SetWlanLinkResponse) { + } // emane rpc rpc GetEmaneConfig (emane.GetEmaneConfigRequest) returns (emane.GetEmaneConfigResponse) { diff --git a/daemon/proto/core/api/grpc/wlan.proto b/daemon/proto/core/api/grpc/wlan.proto index 139c0a2e..a37c511f 100644 --- a/daemon/proto/core/api/grpc/wlan.proto +++ b/daemon/proto/core/api/grpc/wlan.proto @@ -34,3 +34,15 @@ message SetWlanConfigRequest { message SetWlanConfigResponse { bool result = 1; } + +message SetWlanLinkRequest { + int32 session_id = 1; + int32 wlan = 2; + int32 node_one = 3; + int32 node_two = 4; + bool linked = 5; +} + +message SetWlanLinkResponse { + bool result = 1; +} From 0a792f7b3fb0445b8a50b487725976f035e0cd78 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 19 May 2020 22:36:10 -0700 Subject: [PATCH 0275/1131] updates to grpc rpc wlan link, added node class type checking and hinting in grpc server code --- daemon/core/api/grpc/client.py | 12 ++--- daemon/core/api/grpc/server.py | 78 ++++++++++++++++----------- daemon/proto/core/api/grpc/core.proto | 2 +- daemon/proto/core/api/grpc/wlan.proto | 4 +- 4 files changed, 56 insertions(+), 40 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index c5bbf50f..a645c756 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -88,9 +88,9 @@ from core.api.grpc.wlan_pb2 import ( GetWlanConfigsResponse, SetWlanConfigRequest, SetWlanConfigResponse, - SetWlanLinkRequest, - SetWlanLinkResponse, WlanConfig, + WlanLinkRequest, + WlanLinkResponse, ) @@ -1206,17 +1206,17 @@ class CoreGrpcClient: request = ExecuteScriptRequest(script=script) return self.stub.ExecuteScript(request) - def set_wlan_link( + def wlan_link( self, session_id: int, wlan: int, node_one: int, node_two: int, linked: bool - ) -> SetWlanLinkResponse: - request = SetWlanLinkRequest( + ) -> WlanLinkResponse: + request = WlanLinkRequest( session_id=session_id, wlan=wlan, node_one=node_one, node_two=node_two, linked=linked, ) - return self.stub.SetWlanLink(request) + return self.stub.WlanLink(request) def connect(self) -> None: """ diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 4e450a2a..77e6bf08 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -6,7 +6,7 @@ import tempfile import threading import time from concurrent import futures -from typing import Type +from typing import Type, TypeVar import grpc from grpc import ServicerContext @@ -102,8 +102,8 @@ from core.api.grpc.wlan_pb2 import ( GetWlanConfigsResponse, SetWlanConfigRequest, SetWlanConfigResponse, - SetWlanLinkRequest, - SetWlanLinkResponse, + WlanLinkRequest, + WlanLinkResponse, ) from core.emulator.coreemu import CoreEmu from core.emulator.data import LinkData @@ -112,12 +112,13 @@ from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags from core.emulator.session import Session from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility -from core.nodes.base import CoreNodeBase, NodeBase +from core.nodes.base import CoreNode, CoreNodeBase, NodeBase from core.nodes.network import WlanNode from core.services.coreservices import ServiceManager _ONE_DAY_IN_SECONDS = 60 * 60 * 24 _INTERFACE_REGEX = re.compile(r"veth(?P[0-9a-fA-F]+)") +T = TypeVar("T") class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): @@ -173,19 +174,34 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): return session def get_node( - self, session: Session, node_id: int, context: ServicerContext - ) -> NodeBase: + self, + session: Session, + node_id: int, + context: ServicerContext, + node_class: Type[T], + ) -> T: """ Retrieve node given session and node id :param session: session that has the node :param node_id: node id :param context: request + :param node_class: type of node we are expecting :return: node object that satisfies. If node not found then raise an exception. :raises Exception: raises grpc exception when node does not exist """ try: - return session.get_node(node_id) + node = session.get_node(node_id) + if isinstance(node, node_class): + return node + else: + actual = node.__class__.__name__ + expected = node_class.__name__ + context.abort( + grpc.StatusCode.NOT_FOUND, + f"node({node_id}) class({actual}) " + f"was not expected class({expected})", + ) except CoreError: context.abort(grpc.StatusCode.NOT_FOUND, f"node {node_id} not found") @@ -264,7 +280,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): # config service configs for config in request.config_service_configs: - node = self.get_node(session, config.node_id, context) + node = self.get_node(session, config.node_id, context, CoreNode) service = node.config_services[config.name] if config.config: service.set_config(config.config) @@ -681,7 +697,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get node: %s", request) session = self.get_session(request.session_id, context) - node = self.get_node(session, request.node_id, context) + node = self.get_node(session, request.node_id, context, NodeBase) interfaces = [] for interface_id in node._netif: interface = node._netif[interface_id] @@ -702,7 +718,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("edit node: %s", request) session = self.get_session(request.session_id, context) - node = self.get_node(session, request.node_id, context) + node = self.get_node(session, request.node_id, context, NodeBase) options = NodeOptions() options.icon = request.icon if request.HasField("position"): @@ -754,7 +770,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("sending node command: %s", request) session = self.get_session(request.session_id, context) - node = self.get_node(session, request.node_id, context) + node = self.get_node(session, request.node_id, context, CoreNode) try: output = node.cmd(request.command) except CoreCommandError as e: @@ -773,7 +789,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("getting node terminal: %s", request) session = self.get_session(request.session_id, context) - node = self.get_node(session, request.node_id, context) + node = self.get_node(session, request.node_id, context, CoreNode) terminal = node.termcmdstring("/bin/bash") return core_pb2.GetNodeTerminalResponse(terminal=terminal) @@ -789,7 +805,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get node links: %s", request) session = self.get_session(request.session_id, context) - node = self.get_node(session, request.node_id, context) + node = self.get_node(session, request.node_id, context, NodeBase) links = get_links(node) return core_pb2.GetNodeLinksResponse(links=links) @@ -806,8 +822,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("add link: %s", request) # validate session and nodes session = self.get_session(request.session_id, context) - self.get_node(session, request.link.node_one_id, context) - self.get_node(session, request.link.node_two_id, context) + self.get_node(session, request.link.node_one_id, context, NodeBase) + self.get_node(session, request.link.node_two_id, context, NodeBase) node_one_id = request.link.node_one_id node_two_id = request.link.node_two_id @@ -997,7 +1013,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("mobility action: %s", request) session = self.get_session(request.session_id, context) - node = self.get_node(session, request.node_id, context) + node = self.get_node(session, request.node_id, context, WlanNode) result = True if request.action == MobilityAction.START: node.mobility.start() @@ -1124,7 +1140,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get node service file: %s", request) session = self.get_session(request.session_id, context) - node = self.get_node(session, request.node_id, context) + node = self.get_node(session, request.node_id, context, CoreNode) file_data = session.services.get_service_file( node, request.service, request.file ) @@ -1179,7 +1195,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("service action: %s", request) session = self.get_session(request.session_id, context) - node = self.get_node(session, request.node_id, context) + node = self.get_node(session, request.node_id, context, CoreNode) service = None for current_service in node.services: if current_service.name == request.service: @@ -1268,7 +1284,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): wlan_config.node_id, BasicRangeModel.name, wlan_config.config ) if session.state == EventTypes.RUNTIME_STATE: - node = self.get_node(session, wlan_config.node_id, context) + node = self.get_node(session, wlan_config.node_id, context, WlanNode) node.updatemodel(wlan_config.config) return SetWlanConfigResponse(result=True) @@ -1549,7 +1565,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :return: get node config service response """ session = self.get_session(request.session_id, context) - node = self.get_node(session, request.node_id, context) + node = self.get_node(session, request.node_id, context, CoreNode) self.validate_service(request.name, context) service = node.config_services.get(request.name) if service: @@ -1631,7 +1647,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :return: get node config services response """ session = self.get_session(request.session_id, context) - node = self.get_node(session, request.node_id, context) + node = self.get_node(session, request.node_id, context, CoreNode) services = node.config_services.keys() return GetNodeConfigServicesResponse(services=services) @@ -1646,7 +1662,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :return: set node config service response """ session = self.get_session(request.session_id, context) - node = self.get_node(session, request.node_id, context) + node = self.get_node(session, request.node_id, context, CoreNode) self.validate_service(request.name, context) service = node.config_services.get(request.name) if service: @@ -1688,11 +1704,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): new_session = new_sessions[0] return ExecuteScriptResponse(session_id=new_session) - def SetWlanLink( - self, request: SetWlanLinkRequest, context: ServicerContext - ) -> SetWlanLinkResponse: + def WlanLink( + self, request: WlanLinkRequest, context: ServicerContext + ) -> WlanLinkResponse: session = self.get_session(request.session_id, context) - wlan = self.get_node(session, request.wlan, context) + wlan = self.get_node(session, request.wlan, context, WlanNode) if not isinstance(wlan, WlanNode): context.abort( grpc.StatusCode.NOT_FOUND, f"wlan id {request.wlan} is not a wlan node" @@ -1702,20 +1718,20 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): grpc.StatusCode.NOT_FOUND, f"wlan node {request.wlan} does not using BasicRangeModel", ) - n1 = self.get_node(session, request.node_one, context) - n2 = self.get_node(session, request.node_two, context) + n1 = self.get_node(session, request.node_one, context, CoreNode) + n2 = self.get_node(session, request.node_two, context, CoreNode) n1_netif, n2_netif = None, None for net, netif1, netif2 in n1.commonnets(n2): if net == wlan: n1_netif = netif1 n2_netif = netif2 break + result = False if n1_netif and n2_netif: if request.linked: wlan.link(n1_netif, n2_netif) else: wlan.unlink(n1_netif, n2_netif) wlan.model.sendlinkmsg(n1_netif, n2_netif, unlink=not request.linked) - return SetWlanLinkResponse(result=True) - else: - return SetWlanLinkResponse(result=False) + result = True + return WlanLinkResponse(result=result) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 421b6f0f..b0ae6642 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -129,7 +129,7 @@ service CoreApi { } rpc SetWlanConfig (wlan.SetWlanConfigRequest) returns (wlan.SetWlanConfigResponse) { } - rpc SetWlanLink (wlan.SetWlanLinkRequest) returns (wlan.SetWlanLinkResponse) { + rpc WlanLink (wlan.WlanLinkRequest) returns (wlan.WlanLinkResponse) { } // emane rpc diff --git a/daemon/proto/core/api/grpc/wlan.proto b/daemon/proto/core/api/grpc/wlan.proto index a37c511f..bbb9757f 100644 --- a/daemon/proto/core/api/grpc/wlan.proto +++ b/daemon/proto/core/api/grpc/wlan.proto @@ -35,7 +35,7 @@ message SetWlanConfigResponse { bool result = 1; } -message SetWlanLinkRequest { +message WlanLinkRequest { int32 session_id = 1; int32 wlan = 2; int32 node_one = 3; @@ -43,6 +43,6 @@ message SetWlanLinkRequest { bool linked = 5; } -message SetWlanLinkResponse { +message WlanLinkResponse { bool result = 1; } From d5254e6a91d9573542cdb948819f147778683e4c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 20 May 2020 14:44:34 -0700 Subject: [PATCH 0276/1131] changes to support better type checking for retrieving an arbitrary node from a session, get_node now requires an expected class that the node would be an instance of, if the returned node is not an instance a CoreError is thrown, this also helps editors pick up expected types to account for variable/function usage better as well --- daemon/core/api/grpc/server.py | 31 ++++---------- daemon/core/api/tlv/corehandlers.py | 13 +++--- daemon/core/emane/commeffect.py | 3 +- daemon/core/emane/emanemanager.py | 8 ++-- daemon/core/emane/emanemodel.py | 3 +- daemon/core/emulator/distributed.py | 2 +- daemon/core/emulator/session.py | 33 ++++++++------- daemon/core/location/mobility.py | 12 +++--- daemon/core/nodes/base.py | 22 ++++++++++ daemon/core/nodes/physical.py | 1 - daemon/core/plugins/sdt.py | 2 +- daemon/core/services/emaneservices.py | 7 +++- daemon/core/xml/corexml.py | 13 +++--- daemon/examples/python/emane80211.py | 5 ++- daemon/examples/python/switch.py | 5 ++- daemon/examples/python/wlan.py | 5 ++- daemon/tests/emane/test_emane.py | 12 +++--- daemon/tests/test_grpc.py | 5 ++- daemon/tests/test_gui.py | 30 +++++++------- daemon/tests/test_nodes.py | 3 +- daemon/tests/test_xml.py | 60 ++++++++++++++------------- 21 files changed, 149 insertions(+), 126 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 77e6bf08..19779320 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -6,7 +6,7 @@ import tempfile import threading import time from concurrent import futures -from typing import Type, TypeVar +from typing import Type import grpc from grpc import ServicerContext @@ -109,7 +109,7 @@ from core.emulator.coreemu import CoreEmu from core.emulator.data import LinkData from core.emulator.emudata import LinkOptions, NodeOptions from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags -from core.emulator.session import Session +from core.emulator.session import NT, Session from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.nodes.base import CoreNode, CoreNodeBase, NodeBase @@ -118,7 +118,6 @@ from core.services.coreservices import ServiceManager _ONE_DAY_IN_SECONDS = 60 * 60 * 24 _INTERFACE_REGEX = re.compile(r"veth(?P[0-9a-fA-F]+)") -T = TypeVar("T") class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): @@ -174,36 +173,22 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): return session def get_node( - self, - session: Session, - node_id: int, - context: ServicerContext, - node_class: Type[T], - ) -> T: + self, session: Session, node_id: int, context: ServicerContext, _class: Type[NT] + ) -> NT: """ Retrieve node given session and node id :param session: session that has the node :param node_id: node id :param context: request - :param node_class: type of node we are expecting + :param _class: type of node we are expecting :return: node object that satisfies. If node not found then raise an exception. :raises Exception: raises grpc exception when node does not exist """ try: - node = session.get_node(node_id) - if isinstance(node, node_class): - return node - else: - actual = node.__class__.__name__ - expected = node_class.__name__ - context.abort( - grpc.StatusCode.NOT_FOUND, - f"node({node_id}) class({actual}) " - f"was not expected class({expected})", - ) - except CoreError: - context.abort(grpc.StatusCode.NOT_FOUND, f"node {node_id} not found") + return session.get_node(node_id, _class) + except CoreError as e: + context.abort(grpc.StatusCode.NOT_FOUND, str(e)) def validate_service( self, name: str, context: ServicerContext diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index d7e41a6c..7f647873 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -41,6 +41,7 @@ from core.emulator.enumerations import ( ) from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel +from core.nodes.base import CoreNodeBase, NodeBase from core.nodes.network import WlanNode from core.services.coreservices import ServiceManager, ServiceShim @@ -836,7 +837,7 @@ class CoreHandler(socketserver.BaseRequestHandler): return () try: - node = self.session.get_node(node_num) + node = self.session.get_node(node_num, CoreNodeBase) # build common TLV items for reply tlv_data = b"" @@ -1228,7 +1229,7 @@ class CoreHandler(socketserver.BaseRequestHandler): if not node_id: return replies - node = self.session.get_node(node_id) + node = self.session.get_node(node_id, CoreNodeBase) if node is None: logging.warning( "request to configure service for unknown node %s", node_id @@ -1373,7 +1374,7 @@ class CoreHandler(socketserver.BaseRequestHandler): self.session.mobility.set_model_config(node_id, object_name, parsed_config) if self.session.state == EventTypes.RUNTIME_STATE and parsed_config: try: - node = self.session.get_node(node_id) + node = self.session.get_node(node_id, WlanNode) if object_name == BasicRangeModel.name: node.updatemodel(parsed_config) except CoreError: @@ -1553,7 +1554,7 @@ class CoreHandler(socketserver.BaseRequestHandler): logging.debug("handling event %s at %s", event_type.name, time.ctime()) if event_type.value <= EventTypes.SHUTDOWN_STATE.value: if node_id is not None: - node = self.session.get_node(node_id) + node = self.session.get_node(node_id, NodeBase) # configure mobility models for WLAN added during runtime if event_type == EventTypes.INSTANTIATION_STATE and isinstance( @@ -1647,7 +1648,7 @@ class CoreHandler(socketserver.BaseRequestHandler): name = event_data.name try: - node = self.session.get_node(node_id) + node = self.session.get_node(node_id, CoreNodeBase) except CoreError: logging.warning( "ignoring event for service '%s', unknown node '%s'", name, node_id @@ -1883,7 +1884,7 @@ class CoreHandler(socketserver.BaseRequestHandler): data_types = tuple( repeat(ConfigDataTypes.STRING.value, len(ServiceShim.keys)) ) - node = self.session.get_node(node_id) + node = self.session.get_node(node_id, CoreNodeBase) values = ServiceShim.tovaluelist(node, service) config_data = ConfigData( message_type=0, diff --git a/daemon/core/emane/commeffect.py b/daemon/core/emane/commeffect.py index 635291e0..f98f2454 100644 --- a/daemon/core/emane/commeffect.py +++ b/daemon/core/emane/commeffect.py @@ -10,6 +10,7 @@ from lxml import etree from core.config import ConfigGroup, Configuration from core.emane import emanemanifest, emanemodel +from core.emane.nodes import EmaneNet from core.nodes.interface import CoreInterface from core.xml import emanexml @@ -137,7 +138,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): # TODO: batch these into multiple events per transmission # TODO: may want to split out seconds portion of delay and jitter event = CommEffectEvent() - emane_node = self.session.get_node(self.id) + emane_node = self.session.get_node(self.id, EmaneNet) nemid = emane_node.getnemid(netif) nemid2 = emane_node.getnemid(netif2) mbw = bw diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 8b4bade2..16680e0e 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -21,7 +21,7 @@ from core.emane.rfpipe import EmaneRfPipeModel from core.emane.tdma import EmaneTdmaModel from core.emulator.enumerations import ConfigDataTypes, RegisterTlvs from core.errors import CoreCommandError, CoreError -from core.nodes.base import CoreNode +from core.nodes.base import CoreNode, NodeBase from core.nodes.interface import CoreInterface from core.nodes.network import CtrlNet from core.xml import emanexml @@ -801,8 +801,8 @@ class EmaneManager(ModelManager): zbit_check = z.bit_length() > 16 or z < 0 if any([xbit_check, ybit_check, zbit_check]): logging.error( - "Unable to build node location message, received lat/long/alt exceeds coordinate " - "space: NEM %s (%d, %d, %d)", + "Unable to build node location message, received lat/long/alt " + "exceeds coordinate space: NEM %s (%d, %d, %d)", nemid, x, y, @@ -812,7 +812,7 @@ class EmaneManager(ModelManager): # generate a node message for this location update try: - node = self.session.get_node(n) + node = self.session.get_node(n, NodeBase) except CoreError: logging.exception( "location event NEM %s has no corresponding node %s", nemid, n diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index 001ea8b0..57a73012 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -7,6 +7,7 @@ from typing import Dict, List from core.config import ConfigGroup, Configuration from core.emane import emanemanifest +from core.emane.nodes import EmaneNet from core.emulator.enumerations import ConfigDataTypes from core.errors import CoreError from core.location.mobility import WirelessModel @@ -148,7 +149,7 @@ class EmaneModel(WirelessModel): :return: nothing """ try: - wlan = self.session.get_node(self.id) + wlan = self.session.get_node(self.id, EmaneNet) wlan.setnempositions(moved_netifs) except CoreError: logging.exception("error during update") diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index 30becfb5..4e7fcdde 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -244,7 +244,7 @@ class DistributedController: ) return key & 0xFFFFFFFF - def get_tunnel(self, n1_id: int, n2_id: int) -> Tuple[GreTap, GreTap]: + def get_tunnel(self, n1_id: int, n2_id: int) -> GreTap: """ Return the GreTap between two nodes if it exists. diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 95aa7c0b..17c46749 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -12,7 +12,7 @@ import subprocess import tempfile import threading import time -from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type +from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, TypeVar from core import constants, utils from core.emane.emanemanager import EmaneManager @@ -77,6 +77,7 @@ NODES = { NODES_TYPE = {NODES[x]: x for x in NODES} CTRL_NET_ID = 9001 LINK_COLORS = ["green", "blue", "orange", "purple", "turquoise"] +NT = TypeVar("NT", bound=NodeBase) class Session: @@ -194,7 +195,7 @@ class Session: def _link_nodes( self, node_one_id: int, node_two_id: int ) -> Tuple[ - CoreNode, CoreNode, CoreNetworkBase, CoreNetworkBase, Tuple[GreTap, GreTap] + Optional[NodeBase], Optional[NodeBase], CoreNetworkBase, CoreNetworkBase, GreTap ]: """ Convenience method for retrieving nodes within link data. @@ -212,8 +213,8 @@ class Session: net_two = None # retrieve node one - node_one = self.get_node(node_one_id) - node_two = self.get_node(node_two_id) + node_one = self.get_node(node_one_id, NodeBase) + node_two = self.get_node(node_two_id, NodeBase) # both node ids are provided tunnel = self.distributed.get_tunnel(node_one_id, node_two_id) @@ -225,6 +226,7 @@ class Session: else: node_two = None # physical node connected via gre tap tunnel + # TODO: double check this cases type elif tunnel: if tunnel.remotenum == node_one_id: node_one = None @@ -777,7 +779,7 @@ class Session: :raises core.CoreError: when node to update does not exist """ # get node to update - node = self.get_node(node_id) + node = self.get_node(node_id, NodeBase) # set node position and broadcast it self.set_node_position(node, options) @@ -908,9 +910,7 @@ class Session: :param data: file data :return: nothing """ - - node = self.get_node(node_id) - + node = self.get_node(node_id, CoreNodeBase) if source_name is not None: node.addfile(source_name, file_name) elif data is not None: @@ -1381,17 +1381,23 @@ class Session: self.nodes[node.id] = node return node - def get_node(self, _id: int) -> NodeBase: + def get_node(self, _id: int, _class: Type[NT]) -> NT: """ Get a session node. :param _id: node id to retrieve + :param _class: expected node class :return: node for the given id :raises core.CoreError: when node does not exist """ if _id not in self.nodes: raise CoreError(f"unknown node id {_id}") - return self.nodes[_id] + node = self.nodes[_id] + if not isinstance(node, _class): + actual = node.__class__.__name__ + expected = _class.__name__ + raise CoreError(f"node class({actual}) is not expected({expected})") + return node def delete_node(self, _id: int) -> bool: """ @@ -1709,10 +1715,7 @@ class Session: :return: control net :raises CoreError: when control net is not found """ - node = self.get_node(CTRL_NET_ID + net_index) - if not isinstance(node, CtrlNet): - raise CoreError("node is not a valid CtrlNet: %s", node.name) - return node + return self.get_node(CTRL_NET_ID + net_index, CtrlNet) def add_remove_control_net( self, net_index: int, remove: bool = False, conf_required: bool = True @@ -1959,7 +1962,7 @@ class Session: if not node_id: utils.mute_detach(data) else: - node = self.get_node(node_id) + node = self.get_node(node_id, CoreNodeBase) node.cmd(data, wait=False) def get_link_color(self, network_id: int) -> str: diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index d2cb0c8a..f2a47c1f 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -24,6 +24,7 @@ from core.emulator.enumerations import ( from core.errors import CoreError from core.nodes.base import CoreNode, NodeBase from core.nodes.interface import CoreInterface +from core.nodes.network import WlanNode if TYPE_CHECKING: from core.emulator.session import Session @@ -75,7 +76,7 @@ class MobilityManager(ModelManager): ) try: - node = self.session.get_node(node_id) + node = self.session.get_node(node_id, WlanNode) except CoreError: logging.warning( "skipping mobility configuration for unknown node: %s", node_id @@ -103,9 +104,8 @@ class MobilityManager(ModelManager): event_type = event_data.event_type node_id = event_data.node name = event_data.name - try: - node = self.session.get_node(node_id) + node = self.session.get_node(node_id, WlanNode) except CoreError: logging.exception( "Ignoring event for model '%s', unknown node '%s'", name, node_id @@ -190,7 +190,7 @@ class MobilityManager(ModelManager): """ for node_id in self.nodes(): try: - node = self.session.get_node(node_id) + node = self.session.get_node(node_id, WlanNode) except CoreError: continue if node.model: @@ -299,7 +299,7 @@ class BasicRangeModel(WirelessModel): """ super().__init__(session, _id) self.session = session - self.wlan = session.get_node(_id) + self.wlan = session.get_node(_id, WlanNode) self._netifs = {} self._netifslock = threading.Lock() self.range = 0 @@ -590,7 +590,7 @@ class WayPointMobility(WirelessModel): self.initial = {} self.lasttime = None self.endtime = None - self.wlan = session.get_node(_id) + self.wlan = session.get_node(_id, WlanNode) # these are really set in child class via confmatrix self.loop = False self.refresh_ms = 50 diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 2749323a..61e9e8fb 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -433,6 +433,28 @@ class CoreNodeBase(NodeBase): common.append((netif1.net, netif1, netif2)) return common + def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: + """ + Create a node file with a given mode. + + :param filename: name of file to create + :param contents: contents of file + :param mode: mode for file + :return: nothing + """ + raise NotImplementedError + + def addfile(self, srcname: str, filename: str) -> None: + """ + Add a file. + + :param srcname: source file name + :param filename: file name to add + :return: nothing + :raises CoreCommandError: when a non-zero exit status occurs + """ + raise NotImplementedError + def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: """ Runs a command within a node container. diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index e8c999c6..baef7922 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -296,7 +296,6 @@ class Rj45Node(CoreNodeBase, CoreInterface): self.localname = name self.old_up = False self.old_addrs = [] - if start: self.startup() diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 93663052..06c23de5 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -344,7 +344,7 @@ class Sdt: """ result = False try: - node = self.session.get_node(node_id) + node = self.session.get_node(node_id, NodeBase) result = isinstance(node, (WlanNode, EmaneNet)) except CoreError: pass diff --git a/daemon/core/services/emaneservices.py b/daemon/core/services/emaneservices.py index e145e842..9d09516e 100644 --- a/daemon/core/services/emaneservices.py +++ b/daemon/core/services/emaneservices.py @@ -1,4 +1,5 @@ from core.emane.nodes import EmaneNet +from core.errors import CoreError from core.services.coreservices import CoreService from core.xml import emanexml @@ -20,8 +21,8 @@ class EmaneTransportService(CoreService): if filename == cls.configs[0]: transport_commands = [] for interface in node.netifs(sort=True): - network_node = node.session.get_node(interface.net.id) - if isinstance(network_node, EmaneNet): + try: + network_node = node.session.get_node(interface.net.id, EmaneNet) config = node.session.emane.get_configs( network_node.id, network_node.model.name ) @@ -32,6 +33,8 @@ class EmaneTransportService(CoreService): % nem_id ) transport_commands.append(command) + except CoreError: + pass transport_commands = "\n".join(transport_commands) return """ emanegentransportxml -o ../ ../platform%s.xml diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index deedd139..3d174db0 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -10,7 +10,7 @@ from core.emulator.data import LinkData from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.enumerations import EventTypes, NodeTypes from core.errors import CoreXmlError -from core.nodes.base import CoreNetworkBase, CoreNodeBase, NodeBase +from core.nodes.base import CoreNodeBase, NodeBase from core.nodes.docker import DockerNode from core.nodes.lxd import LxcNode from core.nodes.network import CtrlNet, WlanNode @@ -505,9 +505,9 @@ class CoreXmlWriter: ip6_mask: int, ) -> etree.Element: interface = etree.Element(element_name) - node = self.session.get_node(node_id) + node = self.session.get_node(node_id, NodeBase) interface_name = None - if not isinstance(node, CoreNetworkBase): + if isinstance(node, CoreNodeBase): node_interface = node.netif(interface_id) interface_name = node_interface.name @@ -523,7 +523,6 @@ class CoreXmlWriter: add_attribute(interface, "ip4_mask", ip4_mask) add_attribute(interface, "ip6", ip6) add_attribute(interface, "ip6_mask", ip6_mask) - return interface def create_link_element(self, link_data: LinkData) -> etree.Element: @@ -560,8 +559,8 @@ class CoreXmlWriter: link_element.append(interface_two) # check for options, don't write for emane/wlan links - node_one = self.session.get_node(link_data.node1_id) - node_two = self.session.get_node(link_data.node2_id) + node_one = self.session.get_node(link_data.node1_id, NodeBase) + node_two = self.session.get_node(link_data.node2_id, NodeBase) is_node_one_wireless = isinstance(node_one, (WlanNode, EmaneNet)) is_node_two_wireless = isinstance(node_two, (WlanNode, EmaneNet)) if not any([is_node_one_wireless, is_node_two_wireless]): @@ -902,7 +901,7 @@ class CoreXmlReader: for configservice_element in configservice_configs.iterchildren(): name = configservice_element.get("name") node_id = get_int(configservice_element, "node") - node = self.session.get_node(node_id) + node = self.session.get_node(node_id, CoreNodeBase) service = node.config_services[name] configs_element = configservice_element.find("configs") diff --git a/daemon/examples/python/emane80211.py b/daemon/examples/python/emane80211.py index e9764a09..6d8655f3 100644 --- a/daemon/examples/python/emane80211.py +++ b/daemon/examples/python/emane80211.py @@ -11,6 +11,7 @@ from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes, NodeTypes +from core.nodes.base import CoreNode NODES = 2 EMANE_DELAY = 10 @@ -51,8 +52,8 @@ def main(): time.sleep(EMANE_DELAY) # get nodes to run example - first_node = session.get_node(1) - last_node = session.get_node(NODES) + first_node = session.get_node(1, CoreNode) + last_node = session.get_node(NODES, CoreNode) address = prefixes.ip4_address(first_node) logging.info("node %s pinging %s", last_node.name, address) output = last_node.cmd(f"ping -c 3 {address}") diff --git a/daemon/examples/python/switch.py b/daemon/examples/python/switch.py index b4903457..d16303e6 100644 --- a/daemon/examples/python/switch.py +++ b/daemon/examples/python/switch.py @@ -8,6 +8,7 @@ import logging from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes from core.emulator.enumerations import EventTypes, NodeTypes +from core.nodes.base import CoreNode NODES = 2 @@ -36,8 +37,8 @@ def main(): session.instantiate() # get nodes to run example - first_node = session.get_node(1) - last_node = session.get_node(NODES) + first_node = session.get_node(1, CoreNode) + last_node = session.get_node(NODES, CoreNode) address = prefixes.ip4_address(first_node) logging.info("node %s pinging %s", last_node.name, address) output = last_node.cmd(f"ping -c 3 {address}") diff --git a/daemon/examples/python/wlan.py b/daemon/examples/python/wlan.py index e9ae47f4..886d3ca9 100644 --- a/daemon/examples/python/wlan.py +++ b/daemon/examples/python/wlan.py @@ -9,6 +9,7 @@ from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes, NodeTypes from core.location.mobility import BasicRangeModel +from core.nodes.base import CoreNode NODES = 2 @@ -40,8 +41,8 @@ def main(): session.instantiate() # get nodes for example run - first_node = session.get_node(1) - last_node = session.get_node(NODES) + first_node = session.get_node(1, CoreNode) + last_node = session.get_node(NODES, CoreNode) address = prefixes.ip4_address(first_node) logging.info("node %s pinging %s", last_node.name, address) output = last_node.cmd(f"ping -c 3 {address}") diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py index 4c507eee..ada8e903 100644 --- a/daemon/tests/emane/test_emane.py +++ b/daemon/tests/emane/test_emane.py @@ -9,11 +9,13 @@ import pytest from core.emane.bypass import EmaneBypassModel from core.emane.commeffect import EmaneCommEffectModel from core.emane.ieee80211abg import EmaneIeee80211abgModel +from core.emane.nodes import EmaneNet from core.emane.rfpipe import EmaneRfPipeModel from core.emane.tdma import EmaneTdmaModel from core.emulator.emudata import NodeOptions from core.emulator.enumerations import NodeTypes from core.errors import CoreCommandError, CoreError +from core.nodes.base import CoreNode _EMANE_MODELS = [ EmaneIeee80211abgModel, @@ -133,9 +135,9 @@ class TestEmane: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id) + assert not session.get_node(n1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id) + assert not session.get_node(n2_id, CoreNode) # load saved xml session.open_xml(file_path, start=True) @@ -146,7 +148,7 @@ class TestEmane: ) # verify nodes and configuration were restored - assert session.get_node(n1_id) - assert session.get_node(n2_id) - assert session.get_node(emane_id) + assert session.get_node(n1_id, CoreNode) + assert session.get_node(n2_id, CoreNode) + assert session.get_node(emane_id, EmaneNet) assert value == config_value diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 5d8bfa1d..2580020a 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -19,6 +19,7 @@ from core.emulator.emudata import NodeOptions from core.emulator.enumerations import EventTypes, ExceptionLevels, NodeTypes from core.errors import CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility +from core.nodes.base import CoreNode from core.xml.corexml import CoreXmlWriter @@ -355,7 +356,7 @@ class TestGrpc: # then assert response.node_id is not None - assert session.get_node(response.node_id) is not None + assert session.get_node(response.node_id, CoreNode) is not None def test_get_node(self, grpc_server): # given @@ -402,7 +403,7 @@ class TestGrpc: assert response.result is expected if expected is True: with pytest.raises(CoreError): - assert session.get_node(node.id) + assert session.get_node(node.id, CoreNode) def test_node_command(self, request, grpc_server): if request.config.getoption("mock"): diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index 481a0fa9..40bc3d0b 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -24,6 +24,8 @@ from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emulator.enumerations import EventTypes, MessageFlags, NodeTypes, RegisterTlvs from core.errors import CoreError from core.location.mobility import BasicRangeModel +from core.nodes.base import CoreNode, NodeBase +from core.nodes.network import SwitchNode def dict_to_str(values): @@ -57,8 +59,7 @@ class TestGui: ) coretlv.handle_message(message) - - assert coretlv.session.get_node(node_id) is not None + assert coretlv.session.get_node(node_id, NodeBase) is not None def test_node_update(self, coretlv): node_id = 1 @@ -76,7 +77,7 @@ class TestGui: coretlv.handle_message(message) - node = coretlv.session.get_node(node_id) + node = coretlv.session.get_node(node_id, NodeBase) assert node is not None assert node.position.x == x assert node.position.y == y @@ -91,7 +92,7 @@ class TestGui: coretlv.handle_message(message) with pytest.raises(CoreError): - coretlv.session.get_node(node_id) + coretlv.session.get_node(node_id, NodeBase) def test_link_add_node_to_net(self, coretlv): node_one = 1 @@ -113,7 +114,7 @@ class TestGui: coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch) + switch_node = coretlv.session.get_node(switch, SwitchNode) all_links = switch_node.all_link_data() assert len(all_links) == 1 @@ -137,7 +138,7 @@ class TestGui: coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch) + switch_node = coretlv.session.get_node(switch, SwitchNode) all_links = switch_node.all_link_data() assert len(all_links) == 1 @@ -189,7 +190,7 @@ class TestGui: ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch) + switch_node = coretlv.session.get_node(switch, SwitchNode) all_links = switch_node.all_link_data() assert len(all_links) == 1 link = all_links[0] @@ -207,7 +208,7 @@ class TestGui: ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch) + switch_node = coretlv.session.get_node(switch, SwitchNode) all_links = switch_node.all_link_data() assert len(all_links) == 1 link = all_links[0] @@ -275,7 +276,7 @@ class TestGui: ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch) + switch_node = coretlv.session.get_node(switch, SwitchNode) all_links = switch_node.all_link_data() assert len(all_links) == 1 @@ -289,7 +290,7 @@ class TestGui: ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch) + switch_node = coretlv.session.get_node(switch, SwitchNode) all_links = switch_node.all_link_data() assert len(all_links) == 0 @@ -311,7 +312,7 @@ class TestGui: ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch) + switch_node = coretlv.session.get_node(switch, SwitchNode) all_links = switch_node.all_link_data() assert len(all_links) == 1 @@ -325,7 +326,7 @@ class TestGui: ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch) + switch_node = coretlv.session.get_node(switch, SwitchNode) all_links = switch_node.all_link_data() assert len(all_links) == 0 @@ -556,8 +557,7 @@ class TestGui: ) coretlv.handle_message(message) - - assert coretlv.session.get_node(node.id) + assert coretlv.session.get_node(node.id, NodeBase) @pytest.mark.parametrize( "state", @@ -619,7 +619,7 @@ class TestGui: coretlv.handle_message(message) - assert coretlv.coreemu.sessions[1].get_node(node.id) + assert coretlv.coreemu.sessions[1].get_node(node.id, CoreNode) def test_register_python(self, coretlv, tmpdir): xml_file = tmpdir.join("test.py") diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index 8c85e0ca..42202f93 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -3,6 +3,7 @@ import pytest from core.emulator.emudata import NodeOptions from core.emulator.enumerations import NodeTypes from core.errors import CoreError +from core.nodes.base import CoreNode MODELS = ["router", "host", "PC", "mdr"] NET_TYPES = [NodeTypes.SWITCH, NodeTypes.HUB, NodeTypes.WIRELESS_LAN] @@ -45,7 +46,7 @@ class TestNodes: # then with pytest.raises(CoreError): - session.get_node(node.id) + session.get_node(node.id, CoreNode) def test_node_sethwaddr(self, session): # given diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index 04f1192d..3d0a67d3 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -6,6 +6,8 @@ from core.emulator.emudata import LinkOptions, NodeOptions from core.emulator.enumerations import EventTypes, NodeTypes from core.errors import CoreError from core.location.mobility import BasicRangeModel +from core.nodes.base import CoreNode +from core.nodes.network import SwitchNode, WlanNode from core.services.utility import SshService @@ -91,16 +93,16 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id) + assert not session.get_node(n1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id) + assert not session.get_node(n2_id, CoreNode) # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - assert session.get_node(n1_id) - assert session.get_node(n2_id) + assert session.get_node(n1_id, CoreNode) + assert session.get_node(n2_id, CoreNode) def test_xml_ptp_services(self, session, tmpdir, ip_prefixes): """ @@ -152,9 +154,9 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id) + assert not session.get_node(n1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id) + assert not session.get_node(n2_id, CoreNode) # load saved xml session.open_xml(file_path, start=True) @@ -163,8 +165,8 @@ class TestXml: service = session.services.get_service(node_one.id, SshService.name) # verify nodes have been recreated - assert session.get_node(n1_id) - assert session.get_node(n2_id) + assert session.get_node(n1_id, CoreNode) + assert session.get_node(n2_id, CoreNode) assert service.config_data.get(service_file) == file_data def test_xml_mobility(self, session, tmpdir, ip_prefixes): @@ -212,9 +214,9 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id) + assert not session.get_node(n1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id) + assert not session.get_node(n2_id, CoreNode) # load saved xml session.open_xml(file_path, start=True) @@ -223,9 +225,9 @@ class TestXml: value = str(session.mobility.get_config("test", wlan_id, BasicRangeModel.name)) # verify nodes and configuration were restored - assert session.get_node(n1_id) - assert session.get_node(n2_id) - assert session.get_node(wlan_id) + assert session.get_node(n1_id, CoreNode) + assert session.get_node(n2_id, CoreNode) + assert session.get_node(wlan_id, WlanNode) assert value == "1" def test_network_to_network(self, session, tmpdir): @@ -263,16 +265,16 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id) + assert not session.get_node(n1_id, SwitchNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id) + assert not session.get_node(n2_id, SwitchNode) # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - switch_one = session.get_node(n1_id) - switch_two = session.get_node(n2_id) + switch_one = session.get_node(n1_id, SwitchNode) + switch_two = session.get_node(n2_id, SwitchNode) assert switch_one assert switch_two assert len(switch_one.all_link_data() + switch_two.all_link_data()) == 1 @@ -322,16 +324,16 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id) + assert not session.get_node(n1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id) + assert not session.get_node(n2_id, SwitchNode) # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - assert session.get_node(n1_id) - assert session.get_node(n2_id) + assert session.get_node(n1_id, CoreNode) + assert session.get_node(n2_id, SwitchNode) links = [] for node_id in session.nodes: node = session.nodes[node_id] @@ -389,16 +391,16 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id) + assert not session.get_node(n1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id) + assert not session.get_node(n2_id, CoreNode) # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - assert session.get_node(n1_id) - assert session.get_node(n2_id) + assert session.get_node(n1_id, CoreNode) + assert session.get_node(n2_id, CoreNode) links = [] for node_id in session.nodes: node = session.nodes[node_id] @@ -471,16 +473,16 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id) + assert not session.get_node(n1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id) + assert not session.get_node(n2_id, CoreNode) # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - assert session.get_node(n1_id) - assert session.get_node(n2_id) + assert session.get_node(n1_id, CoreNode) + assert session.get_node(n2_id, CoreNode) links = [] for node_id in session.nodes: node = session.nodes[node_id] From c07766e1ebd7e5073006c348010d24517c28cee9 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 20 May 2020 22:14:03 -0700 Subject: [PATCH 0277/1131] updated session.add_node to use better type hinting and force usage of classes directly, instead of indirectly through NodeTypes --- daemon/core/api/grpc/grpcutils.py | 3 +- daemon/core/api/grpc/server.py | 3 +- daemon/core/api/tlv/corehandlers.py | 7 +- daemon/core/emulator/session.py | 51 +++++-------- daemon/core/xml/corexml.py | 6 +- daemon/examples/configservices/testing.py | 10 +-- daemon/examples/docker/docker2core.py | 8 ++- daemon/examples/docker/docker2docker.py | 7 +- daemon/examples/docker/switch.py | 13 ++-- daemon/examples/lxd/lxd2core.py | 8 ++- daemon/examples/lxd/lxd2lxd.py | 7 +- daemon/examples/lxd/switch.py | 13 ++-- daemon/examples/python/distributed_emane.py | 10 +-- daemon/examples/python/distributed_lxd.py | 7 +- daemon/examples/python/distributed_ptp.py | 5 +- daemon/examples/python/distributed_switch.py | 10 +-- daemon/examples/python/emane80211.py | 7 +- daemon/examples/python/switch.py | 7 +- daemon/examples/python/switch_inject.py | 11 +-- daemon/examples/python/wlan.py | 7 +- daemon/tests/emane/test_emane.py | 13 ++-- daemon/tests/test_conf.py | 9 +-- daemon/tests/test_core.py | 36 +++++----- daemon/tests/test_distributed.py | 7 +- daemon/tests/test_grpc.py | 74 +++++++++---------- daemon/tests/test_gui.py | 75 ++++++++++---------- daemon/tests/test_links.py | 31 ++++---- daemon/tests/test_nodes.py | 20 +++--- daemon/tests/test_services.py | 29 ++++---- daemon/tests/test_xml.py | 38 +++++----- docs/scripting.md | 59 +++++++++------ 31 files changed, 315 insertions(+), 276 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 4736f017..6281ec67 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -123,7 +123,8 @@ def create_nodes( funcs = [] for node_proto in node_protos: _type, _id, options = add_node_data(node_proto) - args = (_type, _id, options) + _class = session.get_node_class(_type) + args = (_class, _id, options) funcs.append((session.add_node, args, {})) start = time.monotonic() results, exceptions = utils.threadpool(funcs) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 19779320..5fae97dc 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -667,7 +667,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("add node: %s", request) session = self.get_session(request.session_id, context) _type, _id, options = grpcutils.add_node_data(request.node) - node = session.add_node(_type=_type, _id=_id, options=options) + _class = session.get_node_class(_type) + node = session.add_node(_class, _id, options) return core_pb2.AddNodeResponse(node_id=node.id) def GetNode( diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 7f647873..02a6294c 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -41,7 +41,7 @@ from core.emulator.enumerations import ( ) from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel -from core.nodes.base import CoreNodeBase, NodeBase +from core.nodes.base import CoreNode, CoreNodeBase, NodeBase from core.nodes.network import WlanNode from core.services.coreservices import ServiceManager, ServiceShim @@ -682,10 +682,11 @@ class CoreHandler(socketserver.BaseRequestHandler): logging.warning("ignoring invalid message: add and delete flag both set") return () - node_type = None + _class = CoreNode node_type_value = message.get_tlv(NodeTlvs.TYPE.value) if node_type_value is not None: node_type = NodeTypes(node_type_value) + _class = self.session.get_node_class(node_type) node_id = message.get_tlv(NodeTlvs.NUMBER.value) @@ -720,7 +721,7 @@ class CoreHandler(socketserver.BaseRequestHandler): options.services = services.split("|") if message.flags & MessageFlags.ADD.value: - node = self.session.add_node(node_type, node_id, options) + node = self.session.add_node(_class, node_id, options) if node: if message.flags & MessageFlags.STRING.value: self.node_status_request[node.id] = True diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 17c46749..6f112ccf 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -75,6 +75,7 @@ NODES = { NodeTypes.LXC: LxcNode, } NODES_TYPE = {NODES[x]: x for x in NODES} +CONTAINER_NODES = {DockerNode, LxcNode} CTRL_NET_ID = 9001 LINK_COLORS = ["green", "blue", "orange", "purple", "turquoise"] NT = TypeVar("NT", bound=NodeBase) @@ -348,7 +349,7 @@ class Session: node_two.name, ) start = self.state.should_start() - net_one = self.create_node(cls=PtpNet, start=start) + net_one = self.create_node(_class=PtpNet, start=start) # node to network if node_one and net_one: @@ -662,32 +663,21 @@ class Session: node_two.lock.release() def add_node( - self, - _type: NodeTypes = NodeTypes.DEFAULT, - _id: int = None, - options: NodeOptions = None, - _cls: Type[NodeBase] = None, - ) -> NodeBase: + self, _class: Type[NT], _id: int = None, options: NodeOptions = None + ) -> NT: """ Add a node to the session, based on the provided node data. - :param _type: type of node to create + :param _class: node class to create :param _id: id for node, defaults to None for generated id :param options: data to create node with - :param _cls: optional custom class to use for a created node :return: created node :raises core.CoreError: when an invalid node type is given """ - # validate node type, get class, or throw error - if _cls is None: - node_class = self.get_node_class(_type) - else: - node_class = _cls - # set node start based on current session state, override and check when rj45 start = self.state.should_start() enable_rj45 = self.options.get_config("enablerj45") == "1" - if _type == NodeTypes.RJ45 and not enable_rj45: + if _class == Rj45Node and not enable_rj45: start = False # determine node id @@ -703,7 +693,7 @@ class Session: options.set_position(0, 0) name = options.name if not name: - name = f"{node_class.__name__}{_id}" + name = f"{_class.__name__}{_id}" # verify distributed server server = self.distributed.servers.get(options.server) @@ -713,24 +703,15 @@ class Session: # create node logging.info( "creating node(%s) id(%s) name(%s) start(%s)", - node_class.__name__, + _class.__name__, _id, name, start, ) - if _type in [NodeTypes.DOCKER, NodeTypes.LXC]: - node = self.create_node( - cls=node_class, - _id=_id, - name=name, - start=start, - image=options.image, - server=server, - ) - else: - node = self.create_node( - cls=node_class, _id=_id, name=name, start=start, server=server - ) + kwargs = dict(_id=_id, name=name, start=start, server=server) + if _class in CONTAINER_NODES: + kwargs["image"] = options.image + node = self.create_node(_class, **kwargs) # set node attributes node.icon = options.icon @@ -1363,17 +1344,17 @@ class Session: break return node_id - def create_node(self, cls: Type[NodeBase], *args: Any, **kwargs: Any) -> NodeBase: + def create_node(self, _class: Type[NT], *args: Any, **kwargs: Any) -> NT: """ Create an emulation node. - :param cls: node class to create + :param _class: node class to create :param args: list of arguments for the class to create :param kwargs: dictionary of arguments for the class to create :return: the created node instance :raises core.CoreError: when id of the node to create already exists """ - node = cls(self, *args, **kwargs) + node = _class(self, *args, **kwargs) with self._nodes_lock: if node.id in self.nodes: node.shutdown() @@ -1791,7 +1772,7 @@ class Session: server_interface, ) control_net = self.create_node( - cls=CtrlNet, + _class=CtrlNet, _id=_id, prefix=prefix, assign_address=True, diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 3d174db0..ddb51b28 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -840,6 +840,7 @@ class CoreXmlReader: node_type = NodeTypes.DOCKER elif clazz == "lxc": node_type = NodeTypes.LXC + _class = self.session.get_node_class(node_type) service_elements = device_element.find("services") if service_elements is not None: @@ -865,12 +866,13 @@ class CoreXmlReader: options.set_location(lat, lon, alt) logging.info("reading node id(%s) model(%s) name(%s)", node_id, model, name) - self.session.add_node(_type=node_type, _id=node_id, options=options) + self.session.add_node(_class, node_id, options) def read_network(self, network_element: etree.Element) -> None: node_id = get_int(network_element, "id") name = network_element.get("name") node_type = NodeTypes[network_element.get("type")] + _class = self.session.get_node_class(node_type) icon = network_element.get("icon") options = NodeOptions(name) options.icon = icon @@ -891,7 +893,7 @@ class CoreXmlReader: logging.info( "reading node id(%s) node_type(%s) name(%s)", node_id, node_type, name ) - self.session.add_node(_type=node_type, _id=node_id, options=options) + self.session.add_node(_class, node_id, options) def read_configservice_configs(self) -> None: configservice_configs = self.scenario.find("configservice_configurations") diff --git a/daemon/examples/configservices/testing.py b/daemon/examples/configservices/testing.py index 5b193aee..bc67ff46 100644 --- a/daemon/examples/configservices/testing.py +++ b/daemon/examples/configservices/testing.py @@ -2,7 +2,9 @@ import logging from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions -from core.emulator.enumerations import EventTypes, NodeTypes +from core.emulator.enumerations import EventTypes +from core.nodes.base import CoreNode +from core.nodes.network import SwitchNode if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) @@ -13,16 +15,16 @@ if __name__ == "__main__": coreemu = CoreEmu() session = coreemu.create_session() session.set_state(EventTypes.CONFIGURATION_STATE) - switch = session.add_node(_type=NodeTypes.SWITCH) + switch = session.add_node(SwitchNode) # node one options.config_services = ["DefaultRoute", "IPForward"] - node_one = session.add_node(options=options) + node_one = session.add_node(CoreNode, options=options) interface = prefixes.create_interface(node_one) session.add_link(node_one.id, switch.id, interface_one=interface) # node two - node_two = session.add_node(options=options) + node_two = session.add_node(CoreNode, options=options) interface = prefixes.create_interface(node_two) session.add_link(node_two.id, switch.id, interface_one=interface) diff --git a/daemon/examples/docker/docker2core.py b/daemon/examples/docker/docker2core.py index 86cf3dfe..1211a16f 100644 --- a/daemon/examples/docker/docker2core.py +++ b/daemon/examples/docker/docker2core.py @@ -2,7 +2,9 @@ import logging from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions -from core.emulator.enumerations import EventTypes, NodeTypes +from core.emulator.enumerations import EventTypes +from core.nodes.base import CoreNode +from core.nodes.docker import DockerNode if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) @@ -15,11 +17,11 @@ if __name__ == "__main__": options = NodeOptions(model=None, image="ubuntu") # create node one - node_one = session.add_node(_type=NodeTypes.DOCKER, options=options) + node_one = session.add_node(DockerNode, options=options) interface_one = prefixes.create_interface(node_one) # create node two - node_two = session.add_node() + node_two = session.add_node(CoreNode) interface_two = prefixes.create_interface(node_two) # add link diff --git a/daemon/examples/docker/docker2docker.py b/daemon/examples/docker/docker2docker.py index 261a8f67..9e1ae11f 100644 --- a/daemon/examples/docker/docker2docker.py +++ b/daemon/examples/docker/docker2docker.py @@ -2,7 +2,8 @@ import logging from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions -from core.emulator.enumerations import EventTypes, NodeTypes +from core.emulator.enumerations import EventTypes +from core.nodes.docker import DockerNode if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) @@ -17,11 +18,11 @@ if __name__ == "__main__": options = NodeOptions(model=None, image="ubuntu") # create node one - node_one = session.add_node(_type=NodeTypes.DOCKER, options=options) + node_one = session.add_node(DockerNode, options=options) interface_one = prefixes.create_interface(node_one) # create node two - node_two = session.add_node(_type=NodeTypes.DOCKER, options=options) + node_two = session.add_node(DockerNode, options=options) interface_two = prefixes.create_interface(node_two) # add link diff --git a/daemon/examples/docker/switch.py b/daemon/examples/docker/switch.py index f66863e5..74d58fe0 100644 --- a/daemon/examples/docker/switch.py +++ b/daemon/examples/docker/switch.py @@ -2,7 +2,10 @@ import logging from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions -from core.emulator.enumerations import EventTypes, NodeTypes +from core.emulator.enumerations import EventTypes +from core.nodes.base import CoreNode +from core.nodes.docker import DockerNode +from core.nodes.network import SwitchNode if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) @@ -16,18 +19,18 @@ if __name__ == "__main__": options = NodeOptions(model=None, image="ubuntu") # create switch - switch = session.add_node(_type=NodeTypes.SWITCH) + switch = session.add_node(SwitchNode) # node one - node_one = session.add_node(_type=NodeTypes.DOCKER, options=options) + node_one = session.add_node(DockerNode, options=options) interface_one = prefixes.create_interface(node_one) # node two - node_two = session.add_node(_type=NodeTypes.DOCKER, options=options) + node_two = session.add_node(DockerNode, options=options) interface_two = prefixes.create_interface(node_two) # node three - node_three = session.add_node() + node_three = session.add_node(CoreNode) interface_three = prefixes.create_interface(node_three) # add links diff --git a/daemon/examples/lxd/lxd2core.py b/daemon/examples/lxd/lxd2core.py index 06b2b6ba..1365bd4c 100644 --- a/daemon/examples/lxd/lxd2core.py +++ b/daemon/examples/lxd/lxd2core.py @@ -2,7 +2,9 @@ import logging from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions -from core.emulator.enumerations import EventTypes, NodeTypes +from core.emulator.enumerations import EventTypes +from core.nodes.base import CoreNode +from core.nodes.lxd import LxcNode if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) @@ -15,11 +17,11 @@ if __name__ == "__main__": options = NodeOptions(image="ubuntu") # create node one - node_one = session.add_node(_type=NodeTypes.LXC, options=options) + node_one = session.add_node(LxcNode, options=options) interface_one = prefixes.create_interface(node_one) # create node two - node_two = session.add_node() + node_two = session.add_node(CoreNode) interface_two = prefixes.create_interface(node_two) # add link diff --git a/daemon/examples/lxd/lxd2lxd.py b/daemon/examples/lxd/lxd2lxd.py index 2449a223..53a360e8 100644 --- a/daemon/examples/lxd/lxd2lxd.py +++ b/daemon/examples/lxd/lxd2lxd.py @@ -2,7 +2,8 @@ import logging from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions -from core.emulator.enumerations import EventTypes, NodeTypes +from core.emulator.enumerations import EventTypes +from core.nodes.lxd import LxcNode if __name__ == "__main__": logging.basicConfig(level=logging.INFO) @@ -17,11 +18,11 @@ if __name__ == "__main__": options = NodeOptions(image="ubuntu:18.04") # create node one - node_one = session.add_node(_type=NodeTypes.LXC, options=options) + node_one = session.add_node(LxcNode, options=options) interface_one = prefixes.create_interface(node_one) # create node two - node_two = session.add_node(_type=NodeTypes.LXC, options=options) + node_two = session.add_node(LxcNode, options=options) interface_two = prefixes.create_interface(node_two) # add link diff --git a/daemon/examples/lxd/switch.py b/daemon/examples/lxd/switch.py index 7deaae5f..3b6226e4 100644 --- a/daemon/examples/lxd/switch.py +++ b/daemon/examples/lxd/switch.py @@ -2,7 +2,10 @@ import logging from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions -from core.emulator.enumerations import EventTypes, NodeTypes +from core.emulator.enumerations import EventTypes +from core.nodes.base import CoreNode +from core.nodes.lxd import LxcNode +from core.nodes.network import SwitchNode if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) @@ -16,18 +19,18 @@ if __name__ == "__main__": options = NodeOptions(image="ubuntu") # create switch - switch = session.add_node(_type=NodeTypes.SWITCH) + switch = session.add_node(SwitchNode) # node one - node_one = session.add_node(_type=NodeTypes.LXC, options=options) + node_one = session.add_node(LxcNode, options=options) interface_one = prefixes.create_interface(node_one) # node two - node_two = session.add_node(_type=NodeTypes.LXC, options=options) + node_two = session.add_node(LxcNode, options=options) interface_two = prefixes.create_interface(node_two) # node three - node_three = session.add_node() + node_three = session.add_node(CoreNode) interface_three = prefixes.create_interface(node_three) # add links diff --git a/daemon/examples/python/distributed_emane.py b/daemon/examples/python/distributed_emane.py index 4b748803..3248a8e3 100644 --- a/daemon/examples/python/distributed_emane.py +++ b/daemon/examples/python/distributed_emane.py @@ -7,9 +7,11 @@ import argparse import logging from core.emane.ieee80211abg import EmaneIeee80211abgModel +from core.emane.nodes import EmaneNet from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions -from core.emulator.enumerations import EventTypes, NodeTypes +from core.emulator.enumerations import EventTypes +from core.nodes.base import CoreNode def parse(name): @@ -50,11 +52,11 @@ def main(args): # create local node, switch, and remote nodes options = NodeOptions(model="mdr") options.set_position(0, 0) - node_one = session.add_node(options=options) - emane_net = session.add_node(_type=NodeTypes.EMANE) + node_one = session.add_node(CoreNode, options=options) + emane_net = session.add_node(EmaneNet) session.emane.set_model(emane_net, EmaneIeee80211abgModel) options.server = server_name - node_two = session.add_node(options=options) + node_two = session.add_node(CoreNode, options=options) # create node interfaces and link interface_one = prefixes.create_interface(node_one) diff --git a/daemon/examples/python/distributed_lxd.py b/daemon/examples/python/distributed_lxd.py index 8d46d599..de919012 100644 --- a/daemon/examples/python/distributed_lxd.py +++ b/daemon/examples/python/distributed_lxd.py @@ -8,7 +8,8 @@ import logging from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions -from core.emulator.enumerations import EventTypes, NodeTypes +from core.emulator.enumerations import EventTypes +from core.nodes.lxd import LxcNode def parse(name): @@ -42,9 +43,9 @@ def main(args): # create local node, switch, and remote nodes options = NodeOptions(image="ubuntu:18.04") - node_one = session.add_node(_type=NodeTypes.LXC, options=options) + node_one = session.add_node(LxcNode, options=options) options.server = server_name - node_two = session.add_node(_type=NodeTypes.LXC, options=options) + node_two = session.add_node(LxcNode, options=options) # create node interfaces and link interface_one = prefixes.create_interface(node_one) diff --git a/daemon/examples/python/distributed_ptp.py b/daemon/examples/python/distributed_ptp.py index 85069603..26531399 100644 --- a/daemon/examples/python/distributed_ptp.py +++ b/daemon/examples/python/distributed_ptp.py @@ -9,6 +9,7 @@ import logging from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes +from core.nodes.base import CoreNode def parse(name): @@ -42,9 +43,9 @@ def main(args): # create local node, switch, and remote nodes options = NodeOptions() - node_one = session.add_node(options=options) + node_one = session.add_node(CoreNode, options=options) options.server = server_name - node_two = session.add_node(options=options) + node_two = session.add_node(CoreNode, options=options) # create node interfaces and link interface_one = prefixes.create_interface(node_one) diff --git a/daemon/examples/python/distributed_switch.py b/daemon/examples/python/distributed_switch.py index 57c6141b..c52c1cc1 100644 --- a/daemon/examples/python/distributed_switch.py +++ b/daemon/examples/python/distributed_switch.py @@ -8,7 +8,9 @@ import logging from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions -from core.emulator.enumerations import EventTypes, NodeTypes +from core.emulator.enumerations import EventTypes +from core.nodes.base import CoreNode +from core.nodes.network import SwitchNode def parse(name): @@ -43,11 +45,11 @@ def main(args): session.set_state(EventTypes.CONFIGURATION_STATE) # create local node, switch, and remote nodes - node_one = session.add_node() - switch = session.add_node(_type=NodeTypes.SWITCH) + node_one = session.add_node(CoreNode) + switch = session.add_node(SwitchNode) options = NodeOptions() options.server = server_name - node_two = session.add_node(options=options) + node_two = session.add_node(CoreNode, options=options) # create node interfaces and link interface_one = prefixes.create_interface(node_one) diff --git a/daemon/examples/python/emane80211.py b/daemon/examples/python/emane80211.py index 6d8655f3..da93026b 100644 --- a/daemon/examples/python/emane80211.py +++ b/daemon/examples/python/emane80211.py @@ -8,9 +8,10 @@ import logging import time from core.emane.ieee80211abg import EmaneIeee80211abgModel +from core.emane.nodes import EmaneNet from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions -from core.emulator.enumerations import EventTypes, NodeTypes +from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode NODES = 2 @@ -33,13 +34,13 @@ def main(): session.set_location(47.57917, -122.13232, 2.00000, 1.0) options = NodeOptions() options.set_position(80, 50) - emane_network = session.add_node(_type=NodeTypes.EMANE, options=options, _id=100) + emane_network = session.add_node(EmaneNet, options=options, _id=100) session.emane.set_model(emane_network, EmaneIeee80211abgModel) # create nodes options = NodeOptions(model="mdr") for i in range(NODES): - node = session.add_node(options=options) + node = session.add_node(CoreNode, options=options) node.setposition(x=150 * (i + 1), y=150) interface = prefixes.create_interface(node) session.add_link(node.id, emane_network.id, interface_one=interface) diff --git a/daemon/examples/python/switch.py b/daemon/examples/python/switch.py index d16303e6..9475fc47 100644 --- a/daemon/examples/python/switch.py +++ b/daemon/examples/python/switch.py @@ -7,8 +7,9 @@ import logging from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes -from core.emulator.enumerations import EventTypes, NodeTypes +from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode +from core.nodes.network import SwitchNode NODES = 2 @@ -25,11 +26,11 @@ def main(): session.set_state(EventTypes.CONFIGURATION_STATE) # create switch network node - switch = session.add_node(_type=NodeTypes.SWITCH, _id=100) + switch = session.add_node(SwitchNode, _id=100) # create nodes for _ in range(NODES): - node = session.add_node() + node = session.add_node(CoreNode) interface = prefixes.create_interface(node) session.add_link(node.id, switch.id, interface_one=interface) diff --git a/daemon/examples/python/switch_inject.py b/daemon/examples/python/switch_inject.py index e85880e6..8c929e91 100644 --- a/daemon/examples/python/switch_inject.py +++ b/daemon/examples/python/switch_inject.py @@ -7,8 +7,11 @@ same CoreEmu instance the GUI is using. import logging +from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes -from core.emulator.enumerations import EventTypes, NodeTypes +from core.emulator.enumerations import EventTypes +from core.nodes.base import CoreNode +from core.nodes.network import SwitchNode NODES = 2 @@ -18,18 +21,18 @@ def main(): prefixes = IpPrefixes("10.83.0.0/16") # create emulator instance for creating sessions and utility methods - coreemu = globals()["coreemu"] + coreemu: CoreEmu = globals()["coreemu"] session = coreemu.create_session() # must be in configuration state for nodes to start, when using "node_add" below session.set_state(EventTypes.CONFIGURATION_STATE) # create switch network node - switch = session.add_node(_type=NodeTypes.SWITCH) + switch = session.add_node(SwitchNode) # create nodes for _ in range(NODES): - node = session.add_node() + node = session.add_node(CoreNode) interface = prefixes.create_interface(node) session.add_link(node.id, switch.id, interface_one=interface) diff --git a/daemon/examples/python/wlan.py b/daemon/examples/python/wlan.py index 886d3ca9..b09ae5ce 100644 --- a/daemon/examples/python/wlan.py +++ b/daemon/examples/python/wlan.py @@ -7,9 +7,10 @@ import logging from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions -from core.emulator.enumerations import EventTypes, NodeTypes +from core.emulator.enumerations import EventTypes from core.location.mobility import BasicRangeModel from core.nodes.base import CoreNode +from core.nodes.network import WlanNode NODES = 2 @@ -26,14 +27,14 @@ def main(): session.set_state(EventTypes.CONFIGURATION_STATE) # create wlan network node - wlan = session.add_node(_type=NodeTypes.WIRELESS_LAN, _id=100) + wlan = session.add_node(WlanNode, _id=100) session.mobility.set_model(wlan, BasicRangeModel) # create nodes, must set a position for wlan basic range model options = NodeOptions(model="mdr") options.set_position(0, 0) for _ in range(NODES): - node = session.add_node(options=options) + node = session.add_node(CoreNode, options=options) interface = prefixes.create_interface(node) session.add_link(node.id, wlan.id, interface_one=interface) diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py index ada8e903..b3499337 100644 --- a/daemon/tests/emane/test_emane.py +++ b/daemon/tests/emane/test_emane.py @@ -13,7 +13,6 @@ from core.emane.nodes import EmaneNet from core.emane.rfpipe import EmaneRfPipeModel from core.emane.tdma import EmaneTdmaModel from core.emulator.emudata import NodeOptions -from core.emulator.enumerations import NodeTypes from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNode @@ -52,7 +51,7 @@ class TestEmane: session.set_location(47.57917, -122.13232, 2.00000, 1.0) options = NodeOptions() options.set_position(80, 50) - emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) + emane_network = session.add_node(EmaneNet, options=options) session.emane.set_model(emane_network, model) # configure tdma @@ -66,9 +65,9 @@ class TestEmane: # create nodes options = NodeOptions(model="mdr") options.set_position(150, 150) - node_one = session.add_node(options=options) + node_one = session.add_node(CoreNode, options=options) options.set_position(300, 150) - node_two = session.add_node(options=options) + node_two = session.add_node(CoreNode, options=options) for i, node in enumerate([node_one, node_two]): node.setposition(x=150 * (i + 1), y=150) @@ -94,7 +93,7 @@ class TestEmane: session.set_location(47.57917, -122.13232, 2.00000, 1.0) options = NodeOptions() options.set_position(80, 50) - emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) + emane_network = session.add_node(EmaneNet, options=options) config_key = "txpower" config_value = "10" session.emane.set_model( @@ -104,9 +103,9 @@ class TestEmane: # create nodes options = NodeOptions(model="mdr") options.set_position(150, 150) - node_one = session.add_node(options=options) + node_one = session.add_node(CoreNode, options=options) options.set_position(300, 150) - node_two = session.add_node(options=options) + node_two = session.add_node(CoreNode, options=options) for i, node in enumerate([node_one, node_two]): node.setposition(x=150 * (i + 1), y=150) diff --git a/daemon/tests/test_conf.py b/daemon/tests/test_conf.py index 10b36df7..55f6260a 100644 --- a/daemon/tests/test_conf.py +++ b/daemon/tests/test_conf.py @@ -7,8 +7,9 @@ from core.config import ( ModelManager, ) from core.emane.ieee80211abg import EmaneIeee80211abgModel -from core.emulator.enumerations import ConfigDataTypes, NodeTypes +from core.emulator.enumerations import ConfigDataTypes from core.location.mobility import BasicRangeModel +from core.nodes.network import WlanNode class TestConfigurableOptions(ConfigurableOptions): @@ -147,7 +148,7 @@ class TestConf: def test_model_set(self, session): # given - wlan_node = session.add_node(_type=NodeTypes.WIRELESS_LAN) + wlan_node = session.add_node(WlanNode) # when session.mobility.set_model(wlan_node, BasicRangeModel) @@ -157,7 +158,7 @@ class TestConf: def test_model_set_error(self, session): # given - wlan_node = session.add_node(_type=NodeTypes.WIRELESS_LAN) + wlan_node = session.add_node(WlanNode) # when / then with pytest.raises(ValueError): @@ -165,7 +166,7 @@ class TestConf: def test_get_models(self, session): # given - wlan_node = session.add_node(_type=NodeTypes.WIRELESS_LAN) + wlan_node = session.add_node(WlanNode) session.mobility.set_model(wlan_node, BasicRangeModel) # when diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index cc9ba2a4..e663b85f 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -8,13 +8,15 @@ import threading import pytest from core.emulator.emudata import NodeOptions -from core.emulator.enumerations import MessageFlags, NodeTypes +from core.emulator.enumerations import MessageFlags from core.errors import CoreCommandError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility +from core.nodes.base import CoreNode +from core.nodes.network import HubNode, PtpNet, SwitchNode, WlanNode _PATH = os.path.abspath(os.path.dirname(__file__)) _MOBILITY_FILE = os.path.join(_PATH, "mobility.scen") -_WIRED = [NodeTypes.PEER_TO_PEER, NodeTypes.HUB, NodeTypes.SWITCH] +_WIRED = [PtpNet, HubNode, SwitchNode] def ping(from_node, to_node, ip_prefixes): @@ -39,11 +41,11 @@ class TestCore: """ # create net node - net_node = session.add_node(_type=net_type) + net_node = session.add_node(net_type) # create nodes - node_one = session.add_node() - node_two = session.add_node() + node_one = session.add_node(CoreNode) + node_two = session.add_node(CoreNode) # link nodes to net node for node in [node_one, node_two]: @@ -66,11 +68,11 @@ class TestCore: :param ip_prefixes: generates ip addresses for nodes """ # create ptp - ptp_node = session.add_node(_type=NodeTypes.PEER_TO_PEER) + ptp_node = session.add_node(PtpNet) # create nodes - node_one = session.add_node() - node_two = session.add_node() + node_one = session.add_node(CoreNode) + node_two = session.add_node(CoreNode) # link nodes to ptp net for node in [node_one, node_two]: @@ -99,11 +101,11 @@ class TestCore: """ # create ptp - ptp_node = session.add_node(_type=NodeTypes.PEER_TO_PEER) + ptp_node = session.add_node(PtpNet) # create nodes - node_one = session.add_node() - node_two = session.add_node() + node_one = session.add_node(CoreNode) + node_two = session.add_node(CoreNode) # link nodes to ptp net for node in [node_one, node_two]: @@ -143,14 +145,14 @@ class TestCore: """ # create wlan - wlan_node = session.add_node(_type=NodeTypes.WIRELESS_LAN) + wlan_node = session.add_node(WlanNode) session.mobility.set_model(wlan_node, BasicRangeModel) # create nodes options = NodeOptions(model="mdr") options.set_position(0, 0) - node_one = session.add_node(options=options) - node_two = session.add_node(options=options) + node_one = session.add_node(CoreNode, options=options) + node_two = session.add_node(CoreNode, options=options) # link nodes for node in [node_one, node_two]: @@ -173,14 +175,14 @@ class TestCore: """ # create wlan - wlan_node = session.add_node(_type=NodeTypes.WIRELESS_LAN) + wlan_node = session.add_node(WlanNode) session.mobility.set_model(wlan_node, BasicRangeModel) # create nodes options = NodeOptions(model="mdr") options.set_position(0, 0) - node_one = session.add_node(options=options) - node_two = session.add_node(options=options) + node_one = session.add_node(CoreNode, options=options) + node_two = session.add_node(CoreNode, options=options) # link nodes for node in [node_one, node_two]: diff --git a/daemon/tests/test_distributed.py b/daemon/tests/test_distributed.py index 07f17ecb..2308db3d 100644 --- a/daemon/tests/test_distributed.py +++ b/daemon/tests/test_distributed.py @@ -1,5 +1,6 @@ from core.emulator.emudata import NodeOptions -from core.emulator.enumerations import NodeTypes +from core.nodes.base import CoreNode +from core.nodes.network import HubNode class TestDistributed: @@ -12,7 +13,7 @@ class TestDistributed: session.distributed.add_server(server_name, host) options = NodeOptions() options.server = server_name - node = session.add_node(options=options) + node = session.add_node(CoreNode, options=options) session.instantiate() # then @@ -30,7 +31,7 @@ class TestDistributed: session.distributed.add_server(server_name, host) options = NodeOptions() options.server = server_name - node = session.add_node(_type=NodeTypes.HUB, options=options) + node = session.add_node(HubNode, options=options) session.instantiate() # then diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 2580020a..5f34e2e2 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -14,12 +14,14 @@ from core.api.grpc.wlan_pb2 import WlanConfig from core.api.tlv.dataconversion import ConfigShim from core.api.tlv.enumerations import ConfigFlags from core.emane.ieee80211abg import EmaneIeee80211abgModel +from core.emane.nodes import EmaneNet from core.emulator.data import EventData from core.emulator.emudata import NodeOptions from core.emulator.enumerations import EventTypes, ExceptionLevels, NodeTypes from core.errors import CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.nodes.base import CoreNode +from core.nodes.network import SwitchNode, WlanNode from core.xml.corexml import CoreXmlWriter @@ -195,7 +197,7 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - session.add_node() + session.add_node(CoreNode) session.set_state(EventTypes.DEFINITION_STATE) # then @@ -362,7 +364,7 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - node = session.add_node() + node = session.add_node(CoreNode) # then with client.context_connect(): @@ -375,7 +377,7 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - node = session.add_node() + node = session.add_node(CoreNode) # then x, y = 10, 10 @@ -393,7 +395,7 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - node = session.add_node() + node = session.add_node(CoreNode) # then with client.context_connect(): @@ -414,7 +416,7 @@ class TestGrpc: session = grpc_server.coreemu.create_session() session.set_state(EventTypes.CONFIGURATION_STATE) options = NodeOptions(model="Host") - node = session.add_node(options=options) + node = session.add_node(CoreNode, options=options) session.instantiate() output = "hello world" @@ -432,7 +434,7 @@ class TestGrpc: session = grpc_server.coreemu.create_session() session.set_state(EventTypes.CONFIGURATION_STATE) options = NodeOptions(model="Host") - node = session.add_node(options=options) + node = session.add_node(CoreNode, options=options) session.instantiate() # then @@ -509,8 +511,8 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - switch = session.add_node(_type=NodeTypes.SWITCH) - node = session.add_node() + switch = session.add_node(SwitchNode) + node = session.add_node(CoreNode) interface = ip_prefixes.create_interface(node) session.add_link(node.id, switch.id, interface) @@ -525,8 +527,8 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - switch = session.add_node(_type=NodeTypes.SWITCH) - node = session.add_node() + switch = session.add_node(SwitchNode) + node = session.add_node(CoreNode) interface = ip_prefixes.create_interface(node) session.add_link(node.id, switch.id, interface) @@ -539,8 +541,8 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - switch = session.add_node(_type=NodeTypes.SWITCH) - node = session.add_node() + switch = session.add_node(SwitchNode) + node = session.add_node(CoreNode) assert len(switch.all_link_data()) == 0 # then @@ -556,7 +558,7 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - node = session.add_node() + node = session.add_node(CoreNode) # then interface = interface_helper.create_interface(node.id, 0) @@ -568,8 +570,8 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - switch = session.add_node(_type=NodeTypes.SWITCH) - node = session.add_node() + switch = session.add_node(SwitchNode) + node = session.add_node(CoreNode) interface = ip_prefixes.create_interface(node) session.add_link(node.id, switch.id, interface) options = core_pb2.LinkOptions(bandwidth=30000) @@ -591,9 +593,9 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - node_one = session.add_node() + node_one = session.add_node(CoreNode) interface_one = ip_prefixes.create_interface(node_one) - node_two = session.add_node() + node_two = session.add_node(CoreNode) interface_two = ip_prefixes.create_interface(node_two) session.add_link(node_one.id, node_two.id, interface_one, interface_two) link_node = None @@ -618,7 +620,7 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - wlan = session.add_node(_type=NodeTypes.WIRELESS_LAN) + wlan = session.add_node(WlanNode) # then with client.context_connect(): @@ -632,7 +634,7 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() session.set_state(EventTypes.CONFIGURATION_STATE) - wlan = session.add_node(_type=NodeTypes.WIRELESS_LAN) + wlan = session.add_node(WlanNode) wlan.setmodel(BasicRangeModel, BasicRangeModel.default_values()) session.instantiate() range_key = "range" @@ -695,7 +697,7 @@ class TestGrpc: session.set_location(47.57917, -122.13232, 2.00000, 1.0) options = NodeOptions() options.emane = EmaneIeee80211abgModel.name - emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) + emane_network = session.add_node(EmaneNet, options=options) session.emane.set_model(emane_network, EmaneIeee80211abgModel) config_key = "platform_id_start" config_value = "2" @@ -722,7 +724,7 @@ class TestGrpc: session.set_location(47.57917, -122.13232, 2.00000, 1.0) options = NodeOptions() options.emane = EmaneIeee80211abgModel.name - emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) + emane_network = session.add_node(EmaneNet, options=options) session.emane.set_model(emane_network, EmaneIeee80211abgModel) config_key = "bandwidth" config_value = "900000" @@ -750,7 +752,7 @@ class TestGrpc: session.set_location(47.57917, -122.13232, 2.00000, 1.0) options = NodeOptions() options.emane = EmaneIeee80211abgModel.name - emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) + emane_network = session.add_node(EmaneNet, options=options) session.emane.set_model(emane_network, EmaneIeee80211abgModel) # then @@ -778,7 +780,7 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - wlan = session.add_node(_type=NodeTypes.WIRELESS_LAN) + wlan = session.add_node(WlanNode) session.mobility.set_model_config(wlan.id, Ns2ScriptedMobility.name, {}) # then @@ -795,7 +797,7 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - wlan = session.add_node(_type=NodeTypes.WIRELESS_LAN) + wlan = session.add_node(WlanNode) session.mobility.set_model_config(wlan.id, Ns2ScriptedMobility.name, {}) # then @@ -809,7 +811,7 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - wlan = session.add_node(_type=NodeTypes.WIRELESS_LAN) + wlan = session.add_node(WlanNode) config_key = "refresh_ms" config_value = "60" @@ -828,7 +830,7 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - wlan = session.add_node(_type=NodeTypes.WIRELESS_LAN) + wlan = session.add_node(WlanNode) session.mobility.set_model_config(wlan.id, Ns2ScriptedMobility.name, {}) session.instantiate() @@ -881,7 +883,7 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - node = session.add_node() + node = session.add_node(CoreNode) service_name = "DefaultRoute" session.services.set_service(node.id, service_name) @@ -899,7 +901,7 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - node = session.add_node() + node = session.add_node(CoreNode) # then with client.context_connect(): @@ -912,7 +914,7 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - node = session.add_node() + node = session.add_node(CoreNode) # then with client.context_connect(): @@ -927,7 +929,7 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - node = session.add_node() + node = session.add_node(CoreNode) service_name = "DefaultRoute" validate = ["echo hello"] @@ -948,7 +950,7 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - node = session.add_node() + node = session.add_node(CoreNode) service_name = "DefaultRoute" file_name = "defaultroute.sh" file_data = "echo hello" @@ -968,7 +970,7 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - node = session.add_node() + node = session.add_node(CoreNode) service_name = "DefaultRoute" # then @@ -984,7 +986,7 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - node = session.add_node() + node = session.add_node(CoreNode) queue = Queue() def handle_event(event_data): @@ -1005,8 +1007,8 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - wlan = session.add_node(_type=NodeTypes.WIRELESS_LAN) - node = session.add_node() + wlan = session.add_node(WlanNode) + node = session.add_node(CoreNode) interface = ip_prefixes.create_interface(node) session.add_link(node.id, wlan.id, interface) link_data = wlan.all_link_data()[0] @@ -1127,7 +1129,7 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - node = session.add_node() + node = session.add_node(CoreNode) queue = Queue() def handle_event(event_data): diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index 40bc3d0b..4a086e53 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -25,7 +25,7 @@ from core.emulator.enumerations import EventTypes, MessageFlags, NodeTypes, Regi from core.errors import CoreError from core.location.mobility import BasicRangeModel from core.nodes.base import CoreNode, NodeBase -from core.nodes.network import SwitchNode +from core.nodes.network import SwitchNode, WlanNode def dict_to_str(values): @@ -63,7 +63,7 @@ class TestGui: def test_node_update(self, coretlv): node_id = 1 - coretlv.session.add_node(_id=node_id) + coretlv.session.add_node(CoreNode, _id=node_id) x = 50 y = 100 message = coreapi.CoreNodeMessage.create( @@ -84,7 +84,7 @@ class TestGui: def test_node_delete(self, coretlv): node_id = 1 - coretlv.session.add_node(_id=node_id) + coretlv.session.add_node(CoreNode, _id=node_id) message = coreapi.CoreNodeMessage.create( MessageFlags.DELETE.value, [(NodeTlvs.NUMBER, node_id)] ) @@ -96,9 +96,9 @@ class TestGui: def test_link_add_node_to_net(self, coretlv): node_one = 1 - coretlv.session.add_node(_id=node_one) + coretlv.session.add_node(CoreNode, _id=node_one) switch = 2 - coretlv.session.add_node(_id=switch, _type=NodeTypes.SWITCH) + coretlv.session.add_node(SwitchNode, _id=switch) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") interface_one = str(ip_prefix[node_one]) message = coreapi.CoreLinkMessage.create( @@ -120,9 +120,9 @@ class TestGui: def test_link_add_net_to_node(self, coretlv): node_one = 1 - coretlv.session.add_node(_id=node_one) + coretlv.session.add_node(CoreNode, _id=node_one) switch = 2 - coretlv.session.add_node(_id=switch, _type=NodeTypes.SWITCH) + coretlv.session.add_node(SwitchNode, _id=switch) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") interface_one = str(ip_prefix[node_one]) message = coreapi.CoreLinkMessage.create( @@ -144,9 +144,9 @@ class TestGui: def test_link_add_node_to_node(self, coretlv): node_one = 1 - coretlv.session.add_node(_id=node_one) + coretlv.session.add_node(CoreNode, _id=node_one) node_two = 2 - coretlv.session.add_node(_id=node_two) + coretlv.session.add_node(CoreNode, _id=node_two) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") interface_one = str(ip_prefix[node_one]) interface_two = str(ip_prefix[node_two]) @@ -174,9 +174,9 @@ class TestGui: def test_link_update(self, coretlv): node_one = 1 - coretlv.session.add_node(_id=node_one) + coretlv.session.add_node(CoreNode, _id=node_one) switch = 2 - coretlv.session.add_node(_id=switch, _type=NodeTypes.SWITCH) + coretlv.session.add_node(SwitchNode, _id=switch) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") interface_one = str(ip_prefix[node_one]) message = coreapi.CoreLinkMessage.create( @@ -216,9 +216,9 @@ class TestGui: def test_link_delete_node_to_node(self, coretlv): node_one = 1 - coretlv.session.add_node(_id=node_one) + coretlv.session.add_node(CoreNode, _id=node_one) node_two = 2 - coretlv.session.add_node(_id=node_two) + coretlv.session.add_node(CoreNode, _id=node_two) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") interface_one = str(ip_prefix[node_one]) interface_two = str(ip_prefix[node_two]) @@ -260,9 +260,9 @@ class TestGui: def test_link_delete_node_to_net(self, coretlv): node_one = 1 - coretlv.session.add_node(_id=node_one) + coretlv.session.add_node(CoreNode, _id=node_one) switch = 2 - coretlv.session.add_node(_id=switch, _type=NodeTypes.SWITCH) + coretlv.session.add_node(SwitchNode, _id=switch) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") interface_one = str(ip_prefix[node_one]) message = coreapi.CoreLinkMessage.create( @@ -296,9 +296,9 @@ class TestGui: def test_link_delete_net_to_node(self, coretlv): node_one = 1 - coretlv.session.add_node(_id=node_one) + coretlv.session.add_node(CoreNode, _id=node_one) switch = 2 - coretlv.session.add_node(_id=switch, _type=NodeTypes.SWITCH) + coretlv.session.add_node(SwitchNode, _id=switch) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") interface_one = str(ip_prefix[node_one]) message = coreapi.CoreLinkMessage.create( @@ -396,7 +396,7 @@ class TestGui: assert file_data == data def test_file_service_file_set(self, coretlv): - node = coretlv.session.add_node() + node = coretlv.session.add_node(CoreNode) service = "DefaultRoute" file_name = "defaultroute.sh" file_data = "echo hello" @@ -419,7 +419,7 @@ class TestGui: def test_file_node_file_copy(self, request, coretlv): file_name = "/var/log/test/node.log" - node = coretlv.session.add_node() + node = coretlv.session.add_node(CoreNode) node.makenodedir() file_data = "echo hello" message = coreapi.CoreFileMessage.create( @@ -441,7 +441,7 @@ class TestGui: def test_exec_node_tty(self, coretlv): coretlv.dispatch_replies = mock.MagicMock() - node = coretlv.session.add_node() + node = coretlv.session.add_node(CoreNode) message = coreapi.CoreExecMessage.create( MessageFlags.TTY.value, [ @@ -462,7 +462,7 @@ class TestGui: pytest.skip("mocking calls") coretlv.dispatch_replies = mock.MagicMock() - node = coretlv.session.add_node() + node = coretlv.session.add_node(CoreNode) cmd = "echo hello" message = coreapi.CoreExecMessage.create( MessageFlags.TEXT.value | MessageFlags.LOCAL.value, @@ -481,7 +481,7 @@ class TestGui: def test_exec_node_command(self, coretlv): coretlv.dispatch_replies = mock.MagicMock() - node = coretlv.session.add_node() + node = coretlv.session.add_node(CoreNode) cmd = "echo hello" message = coreapi.CoreExecMessage.create( MessageFlags.TEXT.value, @@ -516,7 +516,7 @@ class TestGui: def test_event_schedule(self, coretlv): coretlv.session.add_event = mock.MagicMock() - node = coretlv.session.add_node() + node = coretlv.session.add_node(CoreNode) message = coreapi.CoreEventMessage.create( MessageFlags.ADD.value, [ @@ -535,7 +535,7 @@ class TestGui: def test_event_save_xml(self, coretlv, tmpdir): xml_file = tmpdir.join("coretlv.session.xml") file_path = xml_file.strpath - coretlv.session.add_node() + coretlv.session.add_node(CoreNode) message = coreapi.CoreEventMessage.create( 0, [(EventTlvs.TYPE, EventTypes.FILE_SAVE.value), (EventTlvs.NAME, file_path)], @@ -548,7 +548,7 @@ class TestGui: def test_event_open_xml(self, coretlv, tmpdir): xml_file = tmpdir.join("coretlv.session.xml") file_path = xml_file.strpath - node = coretlv.session.add_node() + node = coretlv.session.add_node(CoreNode) coretlv.session.save_xml(file_path) coretlv.session.delete_node(node.id) message = coreapi.CoreEventMessage.create( @@ -571,7 +571,7 @@ class TestGui: ) def test_event_service(self, coretlv, state): coretlv.session.broadcast_event = mock.MagicMock() - node = coretlv.session.add_node() + node = coretlv.session.add_node(CoreNode) message = coreapi.CoreEventMessage.create( 0, [ @@ -609,7 +609,7 @@ class TestGui: def test_register_xml(self, coretlv, tmpdir): xml_file = tmpdir.join("coretlv.session.xml") file_path = xml_file.strpath - node = coretlv.session.add_node() + node = coretlv.session.add_node(CoreNode) coretlv.session.save_xml(file_path) coretlv.session.delete_node(node.id) message = coreapi.CoreRegMessage.create( @@ -625,9 +625,10 @@ class TestGui: xml_file = tmpdir.join("test.py") file_path = xml_file.strpath with open(file_path, "w") as f: + f.write("from core.nodes.base import CoreNode\n") f.write("coreemu = globals()['coreemu']\n") f.write(f"session = coreemu.sessions[{coretlv.session.id}]\n") - f.write("session.add_node()\n") + f.write("session.add_node(CoreNode)\n") message = coreapi.CoreRegMessage.create( 0, [(RegisterTlvs.EXECUTE_SERVER, file_path)] ) @@ -773,7 +774,7 @@ class TestGui: coretlv.handle_broadcast_config.assert_called_once() def test_config_services_request_specific(self, coretlv): - node = coretlv.session.add_node() + node = coretlv.session.add_node(CoreNode) message = coreapi.CoreConfMessage.create( 0, [ @@ -790,7 +791,7 @@ class TestGui: coretlv.handle_broadcast_config.assert_called_once() def test_config_services_request_specific_file(self, coretlv): - node = coretlv.session.add_node() + node = coretlv.session.add_node(CoreNode) message = coreapi.CoreConfMessage.create( 0, [ @@ -807,7 +808,7 @@ class TestGui: coretlv.session.broadcast_file.assert_called_once() def test_config_services_reset(self, coretlv): - node = coretlv.session.add_node() + node = coretlv.session.add_node(CoreNode) service = "DefaultRoute" coretlv.session.services.set_service(node.id, service) message = coreapi.CoreConfMessage.create( @@ -824,7 +825,7 @@ class TestGui: assert coretlv.session.services.get_service(node.id, service) is None def test_config_services_set(self, coretlv): - node = coretlv.session.add_node() + node = coretlv.session.add_node(CoreNode) service = "DefaultRoute" values = {"meta": "metadata"} message = coreapi.CoreConfMessage.create( @@ -844,7 +845,7 @@ class TestGui: assert coretlv.session.services.get_service(node.id, service) is not None def test_config_mobility_reset(self, coretlv): - wlan = coretlv.session.add_node(_type=NodeTypes.WIRELESS_LAN) + wlan = coretlv.session.add_node(WlanNode) message = coreapi.CoreConfMessage.create( 0, [ @@ -860,7 +861,7 @@ class TestGui: assert len(coretlv.session.mobility.node_configurations) == 0 def test_config_mobility_model_request(self, coretlv): - wlan = coretlv.session.add_node(_type=NodeTypes.WIRELESS_LAN) + wlan = coretlv.session.add_node(WlanNode) message = coreapi.CoreConfMessage.create( 0, [ @@ -876,7 +877,7 @@ class TestGui: coretlv.handle_broadcast_config.assert_called_once() def test_config_mobility_model_update(self, coretlv): - wlan = coretlv.session.add_node(_type=NodeTypes.WIRELESS_LAN) + wlan = coretlv.session.add_node(WlanNode) config_key = "range" config_value = "1000" values = {config_key: config_value} @@ -898,7 +899,7 @@ class TestGui: assert config[config_key] == config_value def test_config_emane_model_request(self, coretlv): - wlan = coretlv.session.add_node(_type=NodeTypes.WIRELESS_LAN) + wlan = coretlv.session.add_node(WlanNode) message = coreapi.CoreConfMessage.create( 0, [ @@ -914,7 +915,7 @@ class TestGui: coretlv.handle_broadcast_config.assert_called_once() def test_config_emane_model_update(self, coretlv): - wlan = coretlv.session.add_node(_type=NodeTypes.WIRELESS_LAN) + wlan = coretlv.session.add_node(WlanNode) config_key = "distance" config_value = "50051" values = {config_key: config_value} diff --git a/daemon/tests/test_links.py b/daemon/tests/test_links.py index d32a1c5f..afbdaab1 100644 --- a/daemon/tests/test_links.py +++ b/daemon/tests/test_links.py @@ -1,11 +1,12 @@ from core.emulator.emudata import LinkOptions -from core.emulator.enumerations import NodeTypes +from core.nodes.base import CoreNode +from core.nodes.network import SwitchNode def create_ptp_network(session, ip_prefixes): # create nodes - node_one = session.add_node() - node_two = session.add_node() + node_one = session.add_node(CoreNode) + node_two = session.add_node(CoreNode) # link nodes to net node interface_one = ip_prefixes.create_interface(node_one) @@ -21,8 +22,8 @@ def create_ptp_network(session, ip_prefixes): class TestLinks: def test_ptp(self, session, ip_prefixes): # given - node_one = session.add_node() - node_two = session.add_node() + node_one = session.add_node(CoreNode) + node_two = session.add_node(CoreNode) interface_one = ip_prefixes.create_interface(node_one) interface_two = ip_prefixes.create_interface(node_two) @@ -35,8 +36,8 @@ class TestLinks: def test_node_to_net(self, session, ip_prefixes): # given - node_one = session.add_node() - node_two = session.add_node(_type=NodeTypes.SWITCH) + node_one = session.add_node(CoreNode) + node_two = session.add_node(SwitchNode) interface_one = ip_prefixes.create_interface(node_one) # when @@ -48,8 +49,8 @@ class TestLinks: def test_net_to_node(self, session, ip_prefixes): # given - node_one = session.add_node(_type=NodeTypes.SWITCH) - node_two = session.add_node() + node_one = session.add_node(SwitchNode) + node_two = session.add_node(CoreNode) interface_two = ip_prefixes.create_interface(node_two) # when @@ -61,8 +62,8 @@ class TestLinks: def test_net_to_net(self, session): # given - node_one = session.add_node(_type=NodeTypes.SWITCH) - node_two = session.add_node(_type=NodeTypes.SWITCH) + node_one = session.add_node(SwitchNode) + node_two = session.add_node(SwitchNode) # when session.add_link(node_one.id, node_two.id) @@ -77,8 +78,8 @@ class TestLinks: per = 25 dup = 25 jitter = 10 - node_one = session.add_node() - node_two = session.add_node(_type=NodeTypes.SWITCH) + node_one = session.add_node(CoreNode) + node_two = session.add_node(SwitchNode) interface_one_data = ip_prefixes.create_interface(node_one) session.add_link(node_one.id, node_two.id, interface_one_data) interface_one = node_one.netif(interface_one_data.id) @@ -111,8 +112,8 @@ class TestLinks: def test_link_delete(self, session, ip_prefixes): # given - node_one = session.add_node() - node_two = session.add_node() + node_one = session.add_node(CoreNode) + node_two = session.add_node(CoreNode) interface_one = ip_prefixes.create_interface(node_one) interface_two = ip_prefixes.create_interface(node_two) session.add_link(node_one.id, node_two.id, interface_one, interface_two) diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index 42202f93..f87e8e80 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -1,12 +1,12 @@ import pytest from core.emulator.emudata import NodeOptions -from core.emulator.enumerations import NodeTypes from core.errors import CoreError from core.nodes.base import CoreNode +from core.nodes.network import HubNode, SwitchNode, WlanNode MODELS = ["router", "host", "PC", "mdr"] -NET_TYPES = [NodeTypes.SWITCH, NodeTypes.HUB, NodeTypes.WIRELESS_LAN] +NET_TYPES = [SwitchNode, HubNode, WlanNode] class TestNodes: @@ -16,7 +16,7 @@ class TestNodes: options = NodeOptions(model=model) # when - node = session.add_node(options=options) + node = session.add_node(CoreNode, options=options) # then assert node @@ -25,7 +25,7 @@ class TestNodes: def test_node_update(self, session): # given - node = session.add_node() + node = session.add_node(CoreNode) position_value = 100 update_options = NodeOptions() update_options.set_position(x=position_value, y=position_value) @@ -39,7 +39,7 @@ class TestNodes: def test_node_delete(self, session): # given - node = session.add_node() + node = session.add_node(CoreNode) # when session.delete_node(node.id) @@ -50,7 +50,7 @@ class TestNodes: def test_node_sethwaddr(self, session): # given - node = session.add_node() + node = session.add_node(CoreNode) index = node.newnetif() interface = node.netif(index) mac = "aa:aa:aa:ff:ff:ff" @@ -63,7 +63,7 @@ class TestNodes: def test_node_sethwaddr_exception(self, session): # given - node = session.add_node() + node = session.add_node(CoreNode) index = node.newnetif() node.netif(index) mac = "aa:aa:aa:ff:ff:fff" @@ -74,7 +74,7 @@ class TestNodes: def test_node_addaddr(self, session): # given - node = session.add_node() + node = session.add_node(CoreNode) index = node.newnetif() interface = node.netif(index) addr = "192.168.0.1/24" @@ -87,7 +87,7 @@ class TestNodes: def test_node_addaddr_exception(self, session): # given - node = session.add_node() + node = session.add_node(CoreNode) index = node.newnetif() node.netif(index) addr = "256.168.0.1/24" @@ -101,7 +101,7 @@ class TestNodes: # given # when - node = session.add_node(_type=net_type) + node = session.add_node(net_type) # then assert node diff --git a/daemon/tests/test_services.py b/daemon/tests/test_services.py index 489a9ab7..c5a51461 100644 --- a/daemon/tests/test_services.py +++ b/daemon/tests/test_services.py @@ -4,6 +4,7 @@ import pytest from mock import MagicMock from core.errors import CoreCommandError +from core.nodes.base import CoreNode from core.services.coreservices import CoreService, ServiceDependencies, ServiceManager _PATH = os.path.abspath(os.path.dirname(__file__)) @@ -52,7 +53,7 @@ class TestServices: # given ServiceManager.add_services(_SERVICES_PATH) file_name = "myservice.sh" - node = session.add_node() + node = session.add_node(CoreNode) # when session.services.set_service_file(node.id, SERVICE_ONE, file_name, "# test") @@ -66,7 +67,7 @@ class TestServices: def test_service_all_configs(self, session): # given ServiceManager.add_services(_SERVICES_PATH) - node = session.add_node() + node = session.add_node(CoreNode) # when session.services.set_service(node.id, SERVICE_ONE) @@ -80,7 +81,7 @@ class TestServices: def test_service_add_services(self, session): # given ServiceManager.add_services(_SERVICES_PATH) - node = session.add_node() + node = session.add_node(CoreNode) total_service = len(node.services) # when @@ -94,7 +95,7 @@ class TestServices: # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_ONE) - node = session.add_node() + node = session.add_node(CoreNode) file_name = my_service.configs[0] file_path = node.hostfilename(file_name) @@ -109,7 +110,7 @@ class TestServices: # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_ONE) - node = session.add_node() + node = session.add_node(CoreNode) session.services.create_service_files(node, my_service) # when @@ -122,7 +123,7 @@ class TestServices: # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_TWO) - node = session.add_node() + node = session.add_node(CoreNode) session.services.create_service_files(node, my_service) node.cmd = MagicMock(side_effect=CoreCommandError(-1, "invalid")) @@ -136,7 +137,7 @@ class TestServices: # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_ONE) - node = session.add_node() + node = session.add_node(CoreNode) session.services.create_service_files(node, my_service) # when @@ -149,7 +150,7 @@ class TestServices: # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_TWO) - node = session.add_node() + node = session.add_node(CoreNode) session.services.create_service_files(node, my_service) node.cmd = MagicMock(side_effect=CoreCommandError(-1, "invalid")) @@ -163,7 +164,7 @@ class TestServices: # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_ONE) - node = session.add_node() + node = session.add_node(CoreNode) session.services.create_service_files(node, my_service) # when @@ -176,7 +177,7 @@ class TestServices: # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_TWO) - node = session.add_node() + node = session.add_node(CoreNode) session.services.create_service_files(node, my_service) node.cmd = MagicMock(side_effect=CoreCommandError(-1, "invalid")) @@ -190,7 +191,7 @@ class TestServices: # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_ONE) - node = session.add_node() + node = session.add_node(CoreNode) # when session.services.set_service(node.id, my_service.name) @@ -204,8 +205,8 @@ class TestServices: # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_ONE) - node_one = session.add_node() - node_two = session.add_node() + node_one = session.add_node(CoreNode) + node_two = session.add_node(CoreNode) file_name = my_service.configs[0] file_data_one = "# custom file one" file_data_two = "# custom file two" @@ -234,7 +235,7 @@ class TestServices: # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_ONE) - node = session.add_node() + node = session.add_node(CoreNode) # when no_service = session.services.get_service(node.id, SERVICE_ONE) diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index 3d0a67d3..897bb6fb 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -3,11 +3,11 @@ from xml.etree import ElementTree import pytest from core.emulator.emudata import LinkOptions, NodeOptions -from core.emulator.enumerations import EventTypes, NodeTypes +from core.emulator.enumerations import EventTypes from core.errors import CoreError from core.location.mobility import BasicRangeModel from core.nodes.base import CoreNode -from core.nodes.network import SwitchNode, WlanNode +from core.nodes.network import PtpNet, SwitchNode, WlanNode from core.services.utility import SshService @@ -61,11 +61,11 @@ class TestXml: :param ip_prefixes: generates ip addresses for nodes """ # create ptp - ptp_node = session.add_node(_type=NodeTypes.PEER_TO_PEER) + ptp_node = session.add_node(PtpNet) # create nodes - node_one = session.add_node() - node_two = session.add_node() + node_one = session.add_node(CoreNode) + node_two = session.add_node(CoreNode) # link nodes to ptp net for node in [node_one, node_two]: @@ -113,12 +113,12 @@ class TestXml: :param ip_prefixes: generates ip addresses for nodes """ # create ptp - ptp_node = session.add_node(_type=NodeTypes.PEER_TO_PEER) + ptp_node = session.add_node(PtpNet) # create nodes options = NodeOptions(model="host") - node_one = session.add_node(options=options) - node_two = session.add_node() + node_one = session.add_node(CoreNode, options=options) + node_two = session.add_node(CoreNode) # link nodes to ptp net for node in [node_one, node_two]: @@ -178,14 +178,14 @@ class TestXml: :param ip_prefixes: generates ip addresses for nodes """ # create wlan - wlan_node = session.add_node(_type=NodeTypes.WIRELESS_LAN) + wlan_node = session.add_node(WlanNode) session.mobility.set_model(wlan_node, BasicRangeModel, {"test": "1"}) # create nodes options = NodeOptions(model="mdr") options.set_position(0, 0) - node_one = session.add_node(options=options) - node_two = session.add_node(options=options) + node_one = session.add_node(CoreNode, options=options) + node_two = session.add_node(CoreNode, options=options) # link nodes for node in [node_one, node_two]: @@ -238,8 +238,8 @@ class TestXml: :param tmpdir: tmpdir to create data in """ # create nodes - switch_one = session.add_node(_type=NodeTypes.SWITCH) - switch_two = session.add_node(_type=NodeTypes.SWITCH) + switch_one = session.add_node(SwitchNode) + switch_two = session.add_node(SwitchNode) # link nodes session.add_link(switch_one.id, switch_two.id) @@ -288,9 +288,9 @@ class TestXml: :param ip_prefixes: generates ip addresses for nodes """ # create nodes - node_one = session.add_node() + node_one = session.add_node(CoreNode) interface_one = ip_prefixes.create_interface(node_one) - switch = session.add_node(_type=NodeTypes.SWITCH) + switch = session.add_node(SwitchNode) # create link link_options = LinkOptions() @@ -354,9 +354,9 @@ class TestXml: :param ip_prefixes: generates ip addresses for nodes """ # create nodes - node_one = session.add_node() + node_one = session.add_node(CoreNode) interface_one = ip_prefixes.create_interface(node_one) - node_two = session.add_node() + node_two = session.add_node(CoreNode) interface_two = ip_prefixes.create_interface(node_two) # create link @@ -421,9 +421,9 @@ class TestXml: :param ip_prefixes: generates ip addresses for nodes """ # create nodes - node_one = session.add_node() + node_one = session.add_node(CoreNode) interface_one = ip_prefixes.create_interface(node_one) - node_two = session.add_node() + node_two = session.add_node(CoreNode) interface_two = ip_prefixes.create_interface(node_two) # create link diff --git a/docs/scripting.md b/docs/scripting.md index aafbb7d3..7c8205c3 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -28,35 +28,54 @@ connections. Here are the basic elements of a CORE Python script: ```python +""" +This is a standalone script to run a small switch based scenario and will not +interact with the GUI. +""" + +import logging + from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes from core.emulator.enumerations import EventTypes -from core.emulator.enumerations import NodeTypes +from core.nodes.base import CoreNode +from core.nodes.network import SwitchNode -# ip generator for example -prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") +NODES = 2 -# create emulator instance for creating sessions and utility methods -coreemu = CoreEmu() -session = coreemu.create_session() -# must be in configuration state for nodes to start, when using "node_add" below -session.set_state(EventTypes.CONFIGURATION_STATE) +def main(): + # ip generator for example + prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") -# create switch network node -switch = session.add_node(_type=NodeTypes.SWITCH) + # create emulator instance for creating sessions and utility methods + coreemu = CoreEmu() + session = coreemu.create_session() -# create nodes -for _ in range(2): - node = session.add_node() - interface = prefixes.create_interface(node) - session.add_link(node.id, switch.id, interface_one=interface) + # must be in configuration state for nodes to start, when using "node_add" below + session.set_state(EventTypes.CONFIGURATION_STATE) -# instantiate session -session.instantiate() + # create switch network node + switch = session.add_node(SwitchNode, _id=100) -# shutdown session -coreemu.shutdown() + # create nodes + for _ in range(NODES): + node = session.add_node(CoreNode) + interface = prefixes.create_interface(node) + session.add_link(node.id, switch.id, interface_one=interface) + + # instantiate session + session.instantiate() + + # run any desired logic here + + # shutdown session + coreemu.shutdown() + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + main() ``` The above script creates a CORE session having two nodes connected with a @@ -136,7 +155,7 @@ session = coreemu.create_session() session.set_location(47.57917, -122.13232, 2.00000, 1.0) options = NodeOptions() options.set_position(80, 50) -emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) +emane_network = session.add_node(EmaneNet, options=options) # set custom emane model config config = {} From d5016bf44fa81bf884b3d624a31daf4b105d938f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 20 May 2020 22:36:04 -0700 Subject: [PATCH 0278/1131] removed pointless wlan instance check in grpc wlan_link api since it is already being done when retrieving the wlan node --- daemon/core/api/grpc/server.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 5fae97dc..1095c3f7 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -1695,10 +1695,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): ) -> WlanLinkResponse: session = self.get_session(request.session_id, context) wlan = self.get_node(session, request.wlan, context, WlanNode) - if not isinstance(wlan, WlanNode): - context.abort( - grpc.StatusCode.NOT_FOUND, f"wlan id {request.wlan} is not a wlan node" - ) if not isinstance(wlan.model, BasicRangeModel): context.abort( grpc.StatusCode.NOT_FOUND, From 4b6ba9033152560d790a0f34e3a92f0103b26c27 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 20 May 2020 23:27:17 -0700 Subject: [PATCH 0279/1131] fixed bad type hint for EventData --- daemon/core/emulator/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index 7dff6be0..d3283974 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -38,7 +38,7 @@ class EventData: event_type: EventTypes = None name: str = None data: str = None - time: float = None + time: str = None session: int = None From bcd9e4ceb16d54e9558e22225f7060cfb99bb3c6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 May 2020 00:20:05 -0700 Subject: [PATCH 0280/1131] fixed session.add_hook to not require a source, since it was not typically used an None was being passed, cleaned up some bad type hinting in related to session.py --- daemon/core/api/grpc/server.py | 4 ++-- daemon/core/api/tlv/corehandlers.py | 2 +- daemon/core/emulator/emudata.py | 10 +++++----- daemon/core/emulator/session.py | 12 ++++++++---- daemon/core/nodes/base.py | 12 ++++++------ daemon/core/xml/corexml.py | 2 +- daemon/tests/test_grpc.py | 2 +- daemon/tests/test_xml.py | 4 ++-- 8 files changed, 26 insertions(+), 22 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 5fae97dc..ae3ec1ec 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -232,7 +232,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): # add all hooks for hook in request.hooks: state = EventTypes(hook.state) - session.add_hook(state, hook.file, None, hook.data) + session.add_hook(state, hook.file, hook.data) # create nodes _, exceptions = grpcutils.create_nodes(session, request.nodes) @@ -918,7 +918,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session = self.get_session(request.session_id, context) hook = request.hook state = EventTypes(hook.state) - session.add_hook(state, hook.file, None, hook.data) + session.add_hook(state, hook.file, hook.data) return core_pb2.AddHookResponse(result=True) def GetMobilityConfigs( diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 02a6294c..1a22cedd 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -1501,7 +1501,7 @@ class CoreHandler(socketserver.BaseRequestHandler): return () state = int(state) state = EventTypes(state) - self.session.add_hook(state, file_name, source_name, data) + self.session.add_hook(state, file_name, data, source_name) return () # writing a file to the host diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index 6a0ec8a6..79c586a3 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Optional, Union import netaddr @@ -21,7 +21,7 @@ class IdGen: def link_config( - network: CoreNetworkBase, + node: Union[CoreNetworkBase, PhysicalNode], interface: CoreInterface, link_options: LinkOptions, devname: str = None, @@ -30,7 +30,7 @@ def link_config( """ Convenience method for configuring a link, - :param network: network to configure link for + :param node: network to configure link for :param interface: interface to configure :param link_options: data to configure link with :param devname: device name, default is None @@ -49,10 +49,10 @@ def link_config( # hacky check here, because physical and emane nodes do not conform to the same # linkconfig interface - if not isinstance(network, (EmaneNet, PhysicalNode)): + if not isinstance(node, (EmaneNet, PhysicalNode)): config["devname"] = devname - network.linkconfig(**config) + node.linkconfig(**config) class NodeOptions: diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 6f112ccf..8259803d 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -196,7 +196,11 @@ class Session: def _link_nodes( self, node_one_id: int, node_two_id: int ) -> Tuple[ - Optional[NodeBase], Optional[NodeBase], CoreNetworkBase, CoreNetworkBase, GreTap + Optional[CoreNode], + Optional[CoreNode], + Optional[CoreNetworkBase], + Optional[CoreNetworkBase], + GreTap, ]: """ Convenience method for retrieving nodes within link data. @@ -856,19 +860,19 @@ class Session: CoreXmlWriter(self).write(file_name) def add_hook( - self, state: EventTypes, file_name: str, source_name: str, data: str + self, state: EventTypes, file_name: str, data: str, source_name: str = None ) -> None: """ Store a hook from a received file message. :param state: when to run hook :param file_name: file name for hook - :param source_name: source name :param data: hook data + :param source_name: source name :return: nothing """ logging.info( - "setting state hook: %s - %s from %s", state, file_name, source_name + "setting state hook: %s - %s source(%s)", state, file_name, source_name ) hook = file_name, data state_hooks = self._hooks.setdefault(state, []) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 61e9e8fb..71870cd9 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -413,14 +413,14 @@ class CoreNodeBase(NodeBase): netif.setposition() def commonnets( - self, obj: "CoreNodeBase", want_ctrl: bool = False - ) -> List[Tuple[NodeBase, CoreInterface, CoreInterface]]: + self, node: "CoreNodeBase", want_ctrl: bool = False + ) -> List[Tuple["CoreNetworkBase", CoreInterface, CoreInterface]]: """ Given another node or net object, return common networks between this node and that object. A list of tuples is returned, with each tuple consisting of (network, interface1, interface2). - :param obj: object to get common network with + :param node: node to get common network with :param want_ctrl: flag set to determine if control network are wanted :return: tuples of common networks """ @@ -428,7 +428,7 @@ class CoreNodeBase(NodeBase): for netif1 in self.netifs(): if not want_ctrl and hasattr(netif1, "control"): continue - for netif2 in obj.netifs(): + for netif2 in node.netifs(): if netif1.net == netif2.net: common.append((netif1.net, netif1, netif2)) return common @@ -1041,7 +1041,7 @@ class CoreNetworkBase(NodeBase): """ pass - def getlinknetif(self, net: "CoreNetworkBase") -> CoreInterface: + def getlinknetif(self, net: "CoreNetworkBase") -> Optional[CoreInterface]: """ Return the interface of that links this net with another net. @@ -1049,7 +1049,7 @@ class CoreNetworkBase(NodeBase): :return: interface the provided network is linked to """ for netif in self.netifs(): - if hasattr(netif, "othernet") and netif.othernet == net: + if getattr(netif, "othernet", None) == net: return netif return None diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index ddb51b28..efbf85c8 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -662,7 +662,7 @@ class CoreXmlReader: state = EventTypes(state) data = hook.text logging.info("reading hook: state(%s) name(%s)", state, name) - self.session.add_hook(state, name, None, data) + self.session.add_hook(state, name, data) def read_session_origin(self) -> None: session_origin = self.scenario.find("session_origin") diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 5f34e2e2..5e765f42 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -450,7 +450,7 @@ class TestGrpc: session = grpc_server.coreemu.create_session() file_name = "test" file_data = "echo hello" - session.add_hook(EventTypes.RUNTIME_STATE, file_name, None, file_data) + session.add_hook(EventTypes.RUNTIME_STATE, file_name, file_data) # then with client.context_connect(): diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index 897bb6fb..bb5a6bf9 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -23,12 +23,12 @@ class TestXml: file_name = "runtime_hook.sh" data = "#!/bin/sh\necho hello" state = EventTypes.RUNTIME_STATE - session.add_hook(state, file_name, None, data) + session.add_hook(state, file_name, data) file_name = "instantiation_hook.sh" data = "#!/bin/sh\necho hello" state = EventTypes.INSTANTIATION_STATE - session.add_hook(state, file_name, None, data) + session.add_hook(state, file_name, data) # save xml xml_file = tmpdir.join("session.xml") From 56fbc0e3c5e28b8e66ff8bb7567d36e5b9fefc50 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 May 2020 22:26:54 -0700 Subject: [PATCH 0281/1131] docker changes to avoid issues running commands without mount and running containers as prvileged to allow changing files in /sys --- daemon/core/nodes/docker.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index f1335747..4899b8f4 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -24,8 +24,9 @@ class DockerClient: def create_container(self) -> str: self.run( - f"docker run -td --init --net=none --hostname {self.name} --name {self.name} " - f"--sysctl net.ipv6.conf.all.disable_ipv6=0 {self.image} /bin/bash" + f"docker run -td --init --net=none --hostname {self.name} " + f"--name {self.name} --sysctl net.ipv6.conf.all.disable_ipv6=0 " + f"--privileged {self.image} /bin/bash" ) self.pid = self.get_pid() return self.pid @@ -53,11 +54,7 @@ class DockerClient: return utils.cmd(f"docker exec {self.name} {cmd}", wait=wait, shell=shell) def create_ns_cmd(self, cmd: str) -> str: - return f"nsenter -t {self.pid} -u -i -p -n {cmd}" - - def ns_cmd(self, cmd: str, wait: bool) -> str: - args = f"nsenter -t {self.pid} -u -i -p -n {cmd}" - return utils.cmd(args, wait=wait) + return f"nsenter -t {self.pid} -a {cmd}" def get_pid(self) -> str: args = f"docker inspect -f '{{{{.State.Pid}}}}' {self.name}" From fe09b3781998e528aef0014faff47c0aa80f3128 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 May 2020 22:41:03 -0700 Subject: [PATCH 0282/1131] removed bootsh from CoreNode types as it was not being used --- daemon/core/nodes/base.py | 5 ----- daemon/core/nodes/docker.py | 4 +--- daemon/core/nodes/lxd.py | 4 +--- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 71870cd9..74268129 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -491,7 +491,6 @@ class CoreNode(CoreNodeBase): _id: int = None, name: str = None, nodedir: str = None, - bootsh: str = "boot.sh", start: bool = True, server: "DistributedServer" = None, ) -> None: @@ -502,7 +501,6 @@ class CoreNode(CoreNodeBase): :param _id: object id :param name: object name :param nodedir: node directory - :param bootsh: boot shell to use :param start: start flag :param server: remote server node will run on, default is None for localhost @@ -516,11 +514,8 @@ class CoreNode(CoreNodeBase): self.pid = None self.lock = threading.RLock() self._mounts = [] - self.bootsh = bootsh - use_ovs = session.options.get_config("ovs") == "True" self.node_net_client = self.create_node_net_client(use_ovs) - if start: self.startup() diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index 4899b8f4..684e8452 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -77,7 +77,6 @@ class DockerNode(CoreNode): _id: int = None, name: str = None, nodedir: str = None, - bootsh: str = "boot.sh", start: bool = True, server: DistributedServer = None, image: str = None @@ -89,7 +88,6 @@ class DockerNode(CoreNode): :param _id: object id :param name: object name :param nodedir: node directory - :param bootsh: boot shell to use :param start: start flag :param server: remote server node will run on, default is None for localhost @@ -98,7 +96,7 @@ class DockerNode(CoreNode): if image is None: image = "ubuntu" self.image = image - super().__init__(session, _id, name, nodedir, bootsh, start, server) + super().__init__(session, _id, name, nodedir, start, server) def create_node_net_client(self, use_ovs: bool) -> LinuxNetClient: """ diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index 31623394..3b4c88c0 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -74,7 +74,6 @@ class LxcNode(CoreNode): _id: int = None, name: str = None, nodedir: str = None, - bootsh: str = "boot.sh", start: bool = True, server: DistributedServer = None, image: str = None, @@ -86,7 +85,6 @@ class LxcNode(CoreNode): :param _id: object id :param name: object name :param nodedir: node directory - :param bootsh: boot shell to use :param start: start flag :param server: remote server node will run on, default is None for localhost @@ -95,7 +93,7 @@ class LxcNode(CoreNode): if image is None: image = "ubuntu" self.image = image - super().__init__(session, _id, name, nodedir, bootsh, start, server) + super().__init__(session, _id, name, nodedir, start, server) def alive(self) -> bool: """ From dd13bc83795a0f40048ab3120154c24cce218951 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 22 May 2020 23:44:10 -0700 Subject: [PATCH 0283/1131] moved linkconfig to CoreNetworkBase and made linkconfig defined the same across the board --- daemon/core/emane/commeffect.py | 1 + daemon/core/emane/emanemodel.py | 2 ++ daemon/core/emane/nodes.py | 9 ++------- daemon/core/emulator/emudata.py | 27 ++++++++++----------------- daemon/core/location/mobility.py | 7 +------ daemon/core/nodes/base.py | 26 ++++++++++++++++++++++++++ daemon/core/nodes/physical.py | 9 ++------- 7 files changed, 44 insertions(+), 37 deletions(-) diff --git a/daemon/core/emane/commeffect.py b/daemon/core/emane/commeffect.py index f98f2454..90ea5c91 100644 --- a/daemon/core/emane/commeffect.py +++ b/daemon/core/emane/commeffect.py @@ -121,6 +121,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): duplicate: float = None, jitter: float = None, netif2: CoreInterface = None, + devname: str = None, ) -> None: """ Generate CommEffect events when a Link Message is received having diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index 57a73012..3b80e8aa 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -163,6 +163,7 @@ class EmaneModel(WirelessModel): duplicate: float = None, jitter: float = None, netif2: CoreInterface = None, + devname: str = None, ) -> None: """ Invoked when a Link Message is received. Default is unimplemented. @@ -174,6 +175,7 @@ class EmaneModel(WirelessModel): :param duplicate: duplicate percentage to set to :param jitter: jitter to set to :param netif2: interface two + :param devname: device name :return: nothing """ logging.warning( diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index d8984f7c..33023ac1 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -62,6 +62,7 @@ class EmaneNet(CoreNetworkBase): duplicate: float = None, jitter: float = None, netif2: CoreInterface = None, + devname: str = None, ) -> None: """ The CommEffect model supports link configuration. @@ -69,13 +70,7 @@ class EmaneNet(CoreNetworkBase): if not self.model: return self.model.linkconfig( - netif=netif, - bw=bw, - delay=delay, - loss=loss, - duplicate=duplicate, - jitter=jitter, - netif2=netif2, + netif, bw, delay, loss, duplicate, jitter, netif2, devname ) def config(self, conf: str) -> None: diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index 79c586a3..796396e4 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -4,7 +4,6 @@ import netaddr from core import utils from core.api.grpc.core_pb2 import LinkOptions -from core.emane.nodes import EmaneNet from core.emulator.enumerations import LinkTypes from core.nodes.base import CoreNetworkBase, CoreNode from core.nodes.interface import CoreInterface @@ -37,22 +36,16 @@ def link_config( :param interface_two: other interface associated, default is None :return: nothing """ - config = { - "netif": interface, - "bw": link_options.bandwidth, - "delay": link_options.delay, - "loss": link_options.per, - "duplicate": link_options.dup, - "jitter": link_options.jitter, - "netif2": interface_two, - } - - # hacky check here, because physical and emane nodes do not conform to the same - # linkconfig interface - if not isinstance(node, (EmaneNet, PhysicalNode)): - config["devname"] = devname - - node.linkconfig(**config) + node.linkconfig( + interface, + link_options.bandwidth, + link_options.delay, + link_options.per, + link_options.dup, + link_options.jitter, + interface_two, + devname, + ) class NodeOptions: diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index f2a47c1f..5041f144 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -335,12 +335,7 @@ class BasicRangeModel(WirelessModel): with self._netifslock: for netif in self._netifs: self.wlan.linkconfig( - netif, - bw=self.bw, - delay=self.delay, - loss=self.loss, - duplicate=None, - jitter=self.jitter, + netif, self.bw, self.delay, self.loss, jitter=self.jitter ) def get_position(self, netif: CoreInterface) -> Tuple[float, float, float]: diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 74268129..da1aef38 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -1165,6 +1165,32 @@ class CoreNetworkBase(NodeBase): return all_links + def linkconfig( + self, + netif: CoreInterface, + bw: float = None, + delay: float = None, + loss: float = None, + duplicate: float = None, + jitter: float = None, + netif2: float = None, + devname: str = None, + ) -> None: + """ + Configure link parameters by applying tc queuing disciplines on the interface. + + :param netif: interface one + :param bw: bandwidth to set to + :param delay: packet delay to set to + :param loss: packet loss to set to + :param duplicate: duplicate percentage to set to + :param jitter: jitter to set to + :param netif2: interface two + :param devname: device name + :return: nothing + """ + raise NotImplementedError + class Position: """ diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index baef7922..bca374fb 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -151,6 +151,7 @@ class PhysicalNode(CoreNodeBase): duplicate: float = None, jitter: float = None, netif2: CoreInterface = None, + devname: str = None, ) -> None: """ Apply tc queing disciplines using linkconfig. @@ -158,13 +159,7 @@ class PhysicalNode(CoreNodeBase): linux_bridge = CoreNetwork(session=self.session, start=False) linux_bridge.up = True linux_bridge.linkconfig( - netif, - bw=bw, - delay=delay, - loss=loss, - duplicate=duplicate, - jitter=jitter, - netif2=netif2, + netif, bw, delay, loss, duplicate, jitter, netif2, devname ) del linux_bridge From 26b0868f6512fbee0eb66fc45aad2fc71c05498f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 23 May 2020 00:00:40 -0700 Subject: [PATCH 0284/1131] removed devname from linkconfig as the only usage was of it was using a parametr that was already being passed in --- daemon/core/emane/commeffect.py | 1 - daemon/core/emane/emanemodel.py | 6 +----- daemon/core/emane/nodes.py | 5 +---- daemon/core/emulator/emudata.py | 3 --- daemon/core/emulator/session.py | 27 +++++---------------------- daemon/core/nodes/base.py | 2 -- daemon/core/nodes/network.py | 5 +---- daemon/core/nodes/physical.py | 5 +---- 8 files changed, 9 insertions(+), 45 deletions(-) diff --git a/daemon/core/emane/commeffect.py b/daemon/core/emane/commeffect.py index 90ea5c91..f98f2454 100644 --- a/daemon/core/emane/commeffect.py +++ b/daemon/core/emane/commeffect.py @@ -121,7 +121,6 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): duplicate: float = None, jitter: float = None, netif2: CoreInterface = None, - devname: str = None, ) -> None: """ Generate CommEffect events when a Link Message is received having diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index 3b80e8aa..4104d3d5 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -163,7 +163,6 @@ class EmaneModel(WirelessModel): duplicate: float = None, jitter: float = None, netif2: CoreInterface = None, - devname: str = None, ) -> None: """ Invoked when a Link Message is received. Default is unimplemented. @@ -175,9 +174,6 @@ class EmaneModel(WirelessModel): :param duplicate: duplicate percentage to set to :param jitter: jitter to set to :param netif2: interface two - :param devname: device name :return: nothing """ - logging.warning( - "emane model(%s) does not support link configuration", self.name - ) + logging.warning("emane model(%s) does not support link config", self.name) diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 33023ac1..5b435fbf 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -62,16 +62,13 @@ class EmaneNet(CoreNetworkBase): duplicate: float = None, jitter: float = None, netif2: CoreInterface = None, - devname: str = None, ) -> None: """ The CommEffect model supports link configuration. """ if not self.model: return - self.model.linkconfig( - netif, bw, delay, loss, duplicate, jitter, netif2, devname - ) + self.model.linkconfig(netif, bw, delay, loss, duplicate, jitter, netif2) def config(self, conf: str) -> None: self.conf = conf diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index 796396e4..4e3ebf8a 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -23,7 +23,6 @@ def link_config( node: Union[CoreNetworkBase, PhysicalNode], interface: CoreInterface, link_options: LinkOptions, - devname: str = None, interface_two: CoreInterface = None, ) -> None: """ @@ -32,7 +31,6 @@ def link_config( :param node: network to configure link for :param interface: interface to configure :param link_options: data to configure link with - :param devname: device name, default is None :param interface_two: other interface associated, default is None :return: nothing """ @@ -44,7 +42,6 @@ def link_config( link_options.dup, link_options.jitter, interface_two, - devname, ) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 8259803d..de7b1fe8 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -394,9 +394,7 @@ class Session: if not link_options.unidirectional: interface.swapparams("_params_up") - link_config( - net_two, interface, link_options, devname=interface.name - ) + link_config(net_two, interface, link_options) interface.swapparams("_params_up") # a tunnel node was found for the nodes @@ -606,9 +604,7 @@ class Session: if upstream: interface.swapparams("_params_up") - link_config( - net_one, interface, link_options, devname=interface.name - ) + link_config(net_one, interface, link_options) interface.swapparams("_params_up") else: link_config(net_one, interface, link_options) @@ -618,12 +614,7 @@ class Session: link_config(net_two, interface, link_options) else: interface.swapparams("_params_up") - link_config( - net_two, - interface, - link_options, - devname=interface.name, - ) + link_config(net_two, interface, link_options) interface.swapparams("_params_up") else: raise CoreError("modify link for unknown nodes") @@ -647,18 +638,10 @@ class Session: ): continue - link_config( - net_one, - interface_one, - link_options, - interface_two=interface_two, - ) + link_config(net_one, interface_one, link_options, interface_two) if not link_options.unidirectional: link_config( - net_one, - interface_two, - link_options, - interface_two=interface_one, + net_one, interface_two, link_options, interface_one ) finally: if node_one: diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index da1aef38..efad8c0a 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -1174,7 +1174,6 @@ class CoreNetworkBase(NodeBase): duplicate: float = None, jitter: float = None, netif2: float = None, - devname: str = None, ) -> None: """ Configure link parameters by applying tc queuing disciplines on the interface. @@ -1186,7 +1185,6 @@ class CoreNetworkBase(NodeBase): :param duplicate: duplicate percentage to set to :param jitter: jitter to set to :param netif2: interface two - :param devname: device name :return: nothing """ raise NotImplementedError diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index f2c16bd0..92a8c336 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -448,7 +448,6 @@ class CoreNetwork(CoreNetworkBase): duplicate: float = None, jitter: float = None, netif2: float = None, - devname: str = None, ) -> None: """ Configure link parameters by applying tc queuing disciplines on the interface. @@ -460,11 +459,9 @@ class CoreNetwork(CoreNetworkBase): :param duplicate: duplicate percentage to set to :param jitter: jitter to set to :param netif2: interface two - :param devname: device name :return: nothing """ - if devname is None: - devname = netif.localname + devname = netif.localname tc = f"{TC_BIN} qdisc replace dev {devname}" parent = "root" changed = False diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index bca374fb..3f45c9ab 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -151,16 +151,13 @@ class PhysicalNode(CoreNodeBase): duplicate: float = None, jitter: float = None, netif2: CoreInterface = None, - devname: str = None, ) -> None: """ Apply tc queing disciplines using linkconfig. """ linux_bridge = CoreNetwork(session=self.session, start=False) linux_bridge.up = True - linux_bridge.linkconfig( - netif, bw, delay, loss, duplicate, jitter, netif2, devname - ) + linux_bridge.linkconfig(netif, bw, delay, loss, duplicate, jitter, netif2) del linux_bridge def newifindex(self) -> int: From 37ff989aa41a6216cc74e595f080c3f137361849 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 23 May 2020 00:19:32 -0700 Subject: [PATCH 0285/1131] fixed bad check in linkconfig --- daemon/core/nodes/network.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 92a8c336..1b5f702c 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -467,11 +467,10 @@ class CoreNetwork(CoreNetworkBase): changed = False if netif.setparam("bw", bw): # from tc-tbf(8): minimum value for burst is rate / kernel_hz - if bw is not None: - burst = max(2 * netif.mtu, bw / 1000) - # max IP payload - limit = 0xFFFF - tbf = f"tbf rate {bw} burst {burst} limit {limit}" + burst = max(2 * netif.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}" From c580e15f8e55eeeba98a22bebb5860880ecf2cf5 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 23 May 2020 01:05:46 -0700 Subject: [PATCH 0286/1131] moved common variables up and localname to CoreInterface, they were being created in all subclasses and avoids type hinting errors --- daemon/core/emane/nodes.py | 1 - daemon/core/nodes/base.py | 6 ++---- daemon/core/nodes/interface.py | 23 +++++++++-------------- daemon/core/nodes/netclient.py | 2 +- daemon/core/nodes/network.py | 3 +-- daemon/core/nodes/physical.py | 4 +--- 6 files changed, 14 insertions(+), 25 deletions(-) diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 5b435fbf..d5f243cb 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -48,7 +48,6 @@ class EmaneNet(CoreNetworkBase): ) -> None: super().__init__(session, _id, name, start, server) self.conf = "" - self.up = False self.nemidmap = {} self.model = None self.mobility = None diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index efad8c0a..1dbdcf53 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -65,17 +65,15 @@ class NodeBase: name = f"o{self.id}" self.name = name self.server = server - self.type = None self.services = None - # ifindex is key, CoreInterface instance is value self._netif = {} self.ifindex = 0 self.canvas = None self.icon = None self.opaque = None self.position = Position() - + self.up = False use_ovs = session.options.get_config("ovs") == "True" self.net_client = get_net_client(use_ovs, self.host_cmd) @@ -272,7 +270,6 @@ class CoreNodeBase(NodeBase): self.config_services = {} self.nodedir = None self.tmpnodedir = False - self.up = False def add_config_service(self, service_class: "ConfigServiceType") -> None: """ @@ -1008,6 +1005,7 @@ class CoreNetworkBase(NodeBase): will run on, default is None for localhost """ super().__init__(session, _id, name, start, server) + self.brname = None self._linked = {} self._linked_lock = threading.Lock() diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 8da7c95b..8235878c 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -27,6 +27,7 @@ class CoreInterface: session: "Session", node: "CoreNode", name: str, + localname: str, mtu: int, server: "DistributedServer" = None, ) -> None: @@ -36,6 +37,7 @@ class CoreInterface: :param session: core session instance :param node: node for interface :param name: interface name + :param localname: interface local name :param mtu: mtu value :param server: remote server node will run on, default is None for localhost @@ -43,6 +45,8 @@ class CoreInterface: self.session = session self.node = node self.name = name + self.localname = localname + self.up = False if not isinstance(mtu, int): raise ValueError self.mtu = mtu @@ -258,9 +262,7 @@ class Veth(CoreInterface): :raises CoreCommandError: when there is a command exception """ # note that net arg is ignored - super().__init__(session, node, name, mtu, server) - self.localname = localname - self.up = False + super().__init__(session, node, name, localname, mtu, server) if start: self.startup() @@ -326,9 +328,7 @@ class TunTap(CoreInterface): will run on, default is None for localhost :param start: start flag """ - super().__init__(session, node, name, mtu, server) - self.localname = localname - self.up = False + super().__init__(session, node, name, localname, mtu, server) self.transport_type = "virtual" if start: self.startup() @@ -509,22 +509,17 @@ class GreTap(CoreInterface): will run on, default is None for localhost :raises CoreCommandError: when there is a command exception """ - super().__init__(session, node, name, mtu, server) if _id is None: - # from PyCoreObj _id = ((id(self) >> 16) ^ (id(self) & 0xFFFF)) & 0xFFFF self.id = _id - sessionid = self.session.short_session_id() - # interface name on the local host machine - self.localname = f"gt.{self.id}.{sessionid}" + sessionid = session.short_session_id() + localname = f"gt.{self.id}.{sessionid}" + super().__init__(session, node, name, localname, mtu, server) self.transport_type = "raw" if not start: - self.up = False return - if remoteip is None: raise ValueError("missing remote IP required for GRE TAP device") - self.net_client.create_gretap(self.localname, remoteip, localip, ttl, key) self.net_client.device_up(self.localname) self.up = True diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 12ab8dc1..5062dead 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -243,7 +243,7 @@ class LinuxNetClient: def create_interface(self, bridge_name: str, interface_name: str) -> None: """ - Create an interface associated with a Linux bridge. + Assign interface master to a Linux bridge. :param bridge_name: bridge name :param interface_name: interface name diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 1b5f702c..17fb4fc2 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -284,7 +284,6 @@ class CoreNetwork(CoreNetworkBase): self.name = name sessionid = self.session.short_session_id() self.brname = f"b.{self.id}.{sessionid}" - self.up = False self.has_ebtables_chain = False if start: self.startup() @@ -561,7 +560,7 @@ class CoreNetwork(CoreNetworkBase): netif = Veth(self.session, None, name, localname, start=self.up) self.attach(netif) - if net.up: + if net.up and net.brname: # this is similar to net.attach() but uses netif.name instead of localname netif.net_client.create_interface(net.brname, netif.name) i = net.newifindex() diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 3f45c9ab..b6ae8e8d 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -280,12 +280,10 @@ class Rj45Node(CoreNodeBase, CoreInterface): will run on, default is None for localhost """ CoreNodeBase.__init__(self, session, _id, name, start, server) - CoreInterface.__init__(self, session, self, name, mtu, server) + CoreInterface.__init__(self, session, self, name, name, mtu, server) self.lock = threading.RLock() self.ifindex = None - # the following are PyCoreNetIf attributes self.transport_type = "raw" - self.localname = name self.old_up = False self.old_addrs = [] if start: From 964f78f06a15cfd618be09f00019e7d1454509a2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 23 May 2020 01:14:47 -0700 Subject: [PATCH 0287/1131] added othernet to CoreInterface to avoid hasattr checks --- daemon/core/nodes/base.py | 4 ++-- daemon/core/nodes/interface.py | 1 + daemon/core/nodes/network.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 1dbdcf53..e1267530 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -1086,11 +1086,11 @@ class CoreNetworkBase(NodeBase): for netif in self.netifs(sort=True): if not hasattr(netif, "node"): continue - linked_node = netif.node uni = False + linked_node = netif.node if linked_node is None: # two layer-2 switches/hubs linked together via linknet() - if not hasattr(netif, "othernet"): + if not netif.othernet: continue linked_node = netif.othernet if linked_node.id == self.id: diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 8235878c..97b494b7 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -51,6 +51,7 @@ class CoreInterface: raise ValueError self.mtu = mtu self.net = None + self.othernet = None self._params = {} self.addrlist = [] self.hwaddr = None diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 17fb4fc2..5f6c635c 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -580,7 +580,7 @@ class CoreNetwork(CoreNetworkBase): :return: interface the provided network is linked to """ for netif in self.netifs(): - if hasattr(netif, "othernet") and netif.othernet == net: + if netif.othernet == net: return netif return None From ba8b16ec3499fa494727c30b8d15e710a7677fa2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 24 May 2020 23:37:38 -0700 Subject: [PATCH 0288/1131] added some type hinting with tests to help in refactoring in the future --- daemon/core/services/coreservices.py | 2 +- daemon/tests/test_conf.py | 15 ++-- daemon/tests/test_core.py | 15 ++-- daemon/tests/test_distributed.py | 5 +- daemon/tests/test_grpc.py | 129 +++++++++++++++------------ daemon/tests/test_gui.py | 90 ++++++++++--------- daemon/tests/test_links.py | 19 ++-- daemon/tests/test_nodes.py | 13 +-- daemon/tests/test_services.py | 27 +++--- daemon/tests/test_utils.py | 8 +- daemon/tests/test_xml.py | 32 +++++-- 11 files changed, 199 insertions(+), 156 deletions(-) diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index 35cd3ed3..491113ff 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -39,7 +39,7 @@ class ServiceDependencies: that all services will be booted and that all dependencies exist within the services provided. """ - def __init__(self, services: List["CoreService"]) -> None: + def __init__(self, services: List[Type["CoreService"]]) -> None: # helpers to check validity self.dependents = {} self.booted = set() diff --git a/daemon/tests/test_conf.py b/daemon/tests/test_conf.py index 55f6260a..1973dcee 100644 --- a/daemon/tests/test_conf.py +++ b/daemon/tests/test_conf.py @@ -8,6 +8,7 @@ from core.config import ( ) from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emulator.enumerations import ConfigDataTypes +from core.emulator.session import Session from core.location.mobility import BasicRangeModel from core.nodes.network import WlanNode @@ -41,7 +42,7 @@ class TestConf: def test_nodes(self): # given config_manager = ConfigurableManager() - test_config = {1: 2} + test_config = {"1": "2"} node_id = 1 config_manager.set_configs(test_config) config_manager.set_configs(test_config, node_id=node_id) @@ -56,7 +57,7 @@ class TestConf: def test_config_reset_all(self): # given config_manager = ConfigurableManager() - test_config = {1: 2} + test_config = {"1": "2"} node_id = 1 config_manager.set_configs(test_config) config_manager.set_configs(test_config, node_id=node_id) @@ -70,7 +71,7 @@ class TestConf: def test_config_reset_node(self): # given config_manager = ConfigurableManager() - test_config = {1: 2} + test_config = {"1": "2"} node_id = 1 config_manager.set_configs(test_config) config_manager.set_configs(test_config, node_id=node_id) @@ -85,7 +86,7 @@ class TestConf: def test_configs_setget(self): # given config_manager = ConfigurableManager() - test_config = {1: 2} + test_config = {"1": "2"} node_id = 1 config_manager.set_configs(test_config) config_manager.set_configs(test_config, node_id=node_id) @@ -146,7 +147,7 @@ class TestConf: with pytest.raises(ValueError): manager.get_model_config(1, bad_name) - def test_model_set(self, session): + def test_model_set(self, session: Session): # given wlan_node = session.add_node(WlanNode) @@ -156,7 +157,7 @@ class TestConf: # then assert session.mobility.get_model_config(wlan_node.id, BasicRangeModel.name) - def test_model_set_error(self, session): + def test_model_set_error(self, session: Session): # given wlan_node = session.add_node(WlanNode) @@ -164,7 +165,7 @@ class TestConf: with pytest.raises(ValueError): session.mobility.set_model(wlan_node, EmaneIeee80211abgModel) - def test_get_models(self, session): + def test_get_models(self, session: Session): # given wlan_node = session.add_node(WlanNode) session.mobility.set_model(wlan_node, BasicRangeModel) diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index e663b85f..b9e0c1df 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -7,8 +7,9 @@ import threading import pytest -from core.emulator.emudata import NodeOptions +from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.enumerations import MessageFlags +from core.emulator.session import Session from core.errors import CoreCommandError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.nodes.base import CoreNode @@ -59,7 +60,7 @@ class TestCore: status = ping(node_one, node_two, ip_prefixes) assert not status - def test_vnode_client(self, request, session, ip_prefixes): + def test_vnode_client(self, request, session: Session, ip_prefixes: IpPrefixes): """ Test vnode client methods. @@ -92,7 +93,7 @@ class TestCore: if not request.config.getoption("mock"): assert client.check_cmd("echo hello") == "hello" - def test_netif(self, session, ip_prefixes): + def test_netif(self, session: Session, ip_prefixes: IpPrefixes): """ Test netif methods. @@ -123,8 +124,8 @@ class TestCore: assert node_two.commonnets(node_one) # check we can retrieve netif index - assert node_one.getifindex(0) - assert node_two.getifindex(0) + assert node_one.ifname(0) + assert node_two.ifname(0) # check interface parameters interface = node_one.netif(0) @@ -136,7 +137,7 @@ class TestCore: node_one.delnetif(0) assert not node_one.netif(0) - def test_wlan_ping(self, session, ip_prefixes): + def test_wlan_ping(self, session: Session, ip_prefixes: IpPrefixes): """ Test basic wlan network. @@ -166,7 +167,7 @@ class TestCore: status = ping(node_one, node_two, ip_prefixes) assert not status - def test_mobility(self, session, ip_prefixes): + def test_mobility(self, session: Session, ip_prefixes: IpPrefixes): """ Test basic wlan network. diff --git a/daemon/tests/test_distributed.py b/daemon/tests/test_distributed.py index 2308db3d..86ddaf99 100644 --- a/daemon/tests/test_distributed.py +++ b/daemon/tests/test_distributed.py @@ -1,10 +1,11 @@ from core.emulator.emudata import NodeOptions +from core.emulator.session import Session from core.nodes.base import CoreNode from core.nodes.network import HubNode class TestDistributed: - def test_remote_node(self, session): + def test_remote_node(self, session: Session): # given server_name = "core2" host = "127.0.0.1" @@ -21,7 +22,7 @@ class TestDistributed: assert node.server.name == server_name assert node.server.host == host - def test_remote_bridge(self, session): + def test_remote_bridge(self, session: Session): # given server_name = "core2" host = "127.0.0.1" diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 5e765f42..5e55f346 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -1,5 +1,7 @@ import time from queue import Queue +from tempfile import TemporaryFile +from typing import Optional import grpc import pytest @@ -9,6 +11,7 @@ from core.api.grpc import core_pb2 from core.api.grpc.client import CoreGrpcClient, InterfaceHelper from core.api.grpc.emane_pb2 import EmaneModelConfig from core.api.grpc.mobility_pb2 import MobilityAction, MobilityConfig +from core.api.grpc.server import CoreGrpcServer from core.api.grpc.services_pb2 import ServiceAction, ServiceConfig, ServiceFileConfig from core.api.grpc.wlan_pb2 import WlanConfig from core.api.tlv.dataconversion import ConfigShim @@ -16,7 +19,7 @@ from core.api.tlv.enumerations import ConfigFlags from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emane.nodes import EmaneNet from core.emulator.data import EventData -from core.emulator.emudata import NodeOptions +from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes, ExceptionLevels, NodeTypes from core.errors import CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility @@ -26,7 +29,7 @@ from core.xml.corexml import CoreXmlWriter class TestGrpc: - def test_start_session(self, grpc_server): + def test_start_session(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -159,7 +162,9 @@ class TestGrpc: assert service_file.data == service_file_config.data @pytest.mark.parametrize("session_id", [None, 6013]) - def test_create_session(self, grpc_server, session_id): + def test_create_session( + self, grpc_server: CoreGrpcServer, session_id: Optional[int] + ): # given client = CoreGrpcClient() @@ -178,7 +183,9 @@ class TestGrpc: assert session.id == session_id @pytest.mark.parametrize("session_id, expected", [(None, True), (6013, False)]) - def test_delete_session(self, grpc_server, session_id, expected): + def test_delete_session( + self, grpc_server: CoreGrpcServer, session_id: Optional[int], expected: bool + ): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -193,7 +200,7 @@ class TestGrpc: assert response.result is expected assert grpc_server.coreemu.sessions.get(session_id) is None - def test_get_session(self, grpc_server): + def test_get_session(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -209,7 +216,7 @@ class TestGrpc: assert len(response.session.nodes) == 1 assert len(response.session.links) == 0 - def test_get_sessions(self, grpc_server): + def test_get_sessions(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -227,7 +234,7 @@ class TestGrpc: assert len(response.sessions) == 1 assert found_session is not None - def test_get_session_options(self, grpc_server): + def test_get_session_options(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -239,7 +246,7 @@ class TestGrpc: # then assert len(response.config) > 0 - def test_get_session_location(self, grpc_server): + def test_get_session_location(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -257,7 +264,7 @@ class TestGrpc: assert response.location.lon == 0 assert response.location.alt == 0 - def test_set_session_location(self, grpc_server): + def test_set_session_location(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -284,7 +291,7 @@ class TestGrpc: assert session.location.refscale == scale assert session.location.refgeo == lat_lon_alt - def test_set_session_options(self, grpc_server): + def test_set_session_options(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -301,7 +308,7 @@ class TestGrpc: config = session.options.get_configs() assert len(config) > 0 - def test_set_session_metadata(self, grpc_server): + def test_set_session_metadata(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -316,7 +323,7 @@ class TestGrpc: assert response.result is True assert session.metadata[key] == value - def test_get_session_metadata(self, grpc_server): + def test_get_session_metadata(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -331,7 +338,7 @@ class TestGrpc: # then assert response.config[key] == value - def test_set_session_state(self, grpc_server): + def test_set_session_state(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -346,7 +353,7 @@ class TestGrpc: assert response.result is True assert session.state == EventTypes.DEFINITION_STATE - def test_add_node(self, grpc_server): + def test_add_node(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -360,7 +367,7 @@ class TestGrpc: assert response.node_id is not None assert session.get_node(response.node_id, CoreNode) is not None - def test_get_node(self, grpc_server): + def test_get_node(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -373,7 +380,7 @@ class TestGrpc: # then assert response.node.id == node.id - def test_edit_node(self, grpc_server): + def test_edit_node(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -391,7 +398,9 @@ class TestGrpc: assert node.position.y == y @pytest.mark.parametrize("node_id, expected", [(1, True), (2, False)]) - def test_delete_node(self, grpc_server, node_id, expected): + def test_delete_node( + self, grpc_server: CoreGrpcServer, node_id: int, expected: bool + ): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -407,7 +416,7 @@ class TestGrpc: with pytest.raises(CoreError): assert session.get_node(node.id, CoreNode) - def test_node_command(self, request, grpc_server): + def test_node_command(self, request, grpc_server: CoreGrpcServer): if request.config.getoption("mock"): pytest.skip("mocking calls") @@ -428,7 +437,7 @@ class TestGrpc: # then assert response.output == output - def test_get_node_terminal(self, grpc_server): + def test_get_node_terminal(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -444,7 +453,7 @@ class TestGrpc: # then assert response.terminal is not None - def test_get_hooks(self, grpc_server): + def test_get_hooks(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -463,7 +472,7 @@ class TestGrpc: assert hook.file == file_name assert hook.data == file_data - def test_add_hook(self, grpc_server): + def test_add_hook(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -479,7 +488,7 @@ class TestGrpc: # then assert response.result is True - def test_save_xml(self, grpc_server, tmpdir): + def test_save_xml(self, grpc_server: CoreGrpcServer, tmpdir: TemporaryFile): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -492,7 +501,7 @@ class TestGrpc: # then assert tmp.exists() - def test_open_xml_hook(self, grpc_server, tmpdir): + def test_open_xml_hook(self, grpc_server: CoreGrpcServer, tmpdir: TemporaryFile): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -507,7 +516,7 @@ class TestGrpc: assert response.result is True assert response.session_id is not None - def test_get_node_links(self, grpc_server, ip_prefixes): + def test_get_node_links(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -523,7 +532,9 @@ class TestGrpc: # then assert len(response.links) == 1 - def test_get_node_links_exception(self, grpc_server, ip_prefixes): + def test_get_node_links_exception( + self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes + ): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -537,7 +548,9 @@ class TestGrpc: with client.context_connect(): client.get_node_links(session.id, 3) - def test_add_link(self, grpc_server, interface_helper): + def test_add_link( + self, grpc_server: CoreGrpcServer, interface_helper: InterfaceHelper + ): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -554,7 +567,9 @@ class TestGrpc: assert response.result is True assert len(switch.all_link_data()) == 1 - def test_add_link_exception(self, grpc_server, interface_helper): + def test_add_link_exception( + self, grpc_server: CoreGrpcServer, interface_helper: InterfaceHelper + ): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -566,7 +581,7 @@ class TestGrpc: with client.context_connect(): client.add_link(session.id, 1, 3, interface) - def test_edit_link(self, grpc_server, ip_prefixes): + def test_edit_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -589,7 +604,7 @@ class TestGrpc: link = switch.all_link_data()[0] assert options.bandwidth == link.bandwidth - def test_delete_link(self, grpc_server, ip_prefixes): + def test_delete_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -616,7 +631,7 @@ class TestGrpc: assert response.result is True assert len(link_node.all_link_data(0)) == 0 - def test_get_wlan_config(self, grpc_server): + def test_get_wlan_config(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -629,7 +644,7 @@ class TestGrpc: # then assert len(response.config) > 0 - def test_set_wlan_config(self, grpc_server): + def test_set_wlan_config(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -661,7 +676,7 @@ class TestGrpc: assert config[range_key] == range_value assert wlan.model.range == int(range_value) - def test_get_emane_config(self, grpc_server): + def test_get_emane_config(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -673,7 +688,7 @@ class TestGrpc: # then assert len(response.config) > 0 - def test_set_emane_config(self, grpc_server): + def test_set_emane_config(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -690,7 +705,7 @@ class TestGrpc: assert len(config) > 1 assert config[config_key] == config_value - def test_get_emane_model_configs(self, grpc_server): + def test_get_emane_model_configs(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -717,7 +732,7 @@ class TestGrpc: assert len(model_config.config) > 0 assert model_config.interface == -1 - def test_set_emane_model_config(self, grpc_server): + def test_set_emane_model_config(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -745,7 +760,7 @@ class TestGrpc: ) assert config[config_key] == config_value - def test_get_emane_model_config(self, grpc_server): + def test_get_emane_model_config(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -764,7 +779,7 @@ class TestGrpc: # then assert len(response.config) > 0 - def test_get_emane_models(self, grpc_server): + def test_get_emane_models(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -776,7 +791,7 @@ class TestGrpc: # then assert len(response.models) > 0 - def test_get_mobility_configs(self, grpc_server): + def test_get_mobility_configs(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -793,7 +808,7 @@ class TestGrpc: mapped_config = response.configs[wlan.id] assert len(mapped_config.config) > 0 - def test_get_mobility_config(self, grpc_server): + def test_get_mobility_config(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -807,7 +822,7 @@ class TestGrpc: # then assert len(response.config) > 0 - def test_set_mobility_config(self, grpc_server): + def test_set_mobility_config(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -826,7 +841,7 @@ class TestGrpc: config = session.mobility.get_model_config(wlan.id, Ns2ScriptedMobility.name) assert config[config_key] == config_value - def test_mobility_action(self, grpc_server): + def test_mobility_action(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -841,7 +856,7 @@ class TestGrpc: # then assert response.result is True - def test_get_services(self, grpc_server): + def test_get_services(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() @@ -852,7 +867,7 @@ class TestGrpc: # then assert len(response.services) > 0 - def test_get_service_defaults(self, grpc_server): + def test_get_service_defaults(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -864,7 +879,7 @@ class TestGrpc: # then assert len(response.defaults) > 0 - def test_set_service_defaults(self, grpc_server): + def test_set_service_defaults(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -879,7 +894,7 @@ class TestGrpc: assert response.result is True assert session.services.default_services[node_type] == services - def test_get_node_service_configs(self, grpc_server): + def test_get_node_service_configs(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -897,7 +912,7 @@ class TestGrpc: assert service_config.node_id == node.id assert service_config.service == service_name - def test_get_node_service(self, grpc_server): + def test_get_node_service(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -910,7 +925,7 @@ class TestGrpc: # then assert len(response.service.configs) > 0 - def test_get_node_service_file(self, grpc_server): + def test_get_node_service_file(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -925,7 +940,7 @@ class TestGrpc: # then assert response.data is not None - def test_set_node_service(self, grpc_server): + def test_set_node_service(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -946,7 +961,7 @@ class TestGrpc: ) assert service.validate == tuple(validate) - def test_set_node_service_file(self, grpc_server): + def test_set_node_service_file(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -966,7 +981,7 @@ class TestGrpc: service_file = session.services.get_service_file(node, service_name, file_name) assert service_file.data == file_data - def test_service_action(self, grpc_server): + def test_service_action(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -982,7 +997,7 @@ class TestGrpc: # then assert response.result is True - def test_node_events(self, grpc_server): + def test_node_events(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -1003,7 +1018,7 @@ class TestGrpc: # then queue.get(timeout=5) - def test_link_events(self, grpc_server, ip_prefixes): + def test_link_events(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -1028,7 +1043,7 @@ class TestGrpc: # then queue.get(timeout=5) - def test_throughputs(self, request, grpc_server): + def test_throughputs(self, request, grpc_server: CoreGrpcServer): if request.config.getoption("mock"): pytest.skip("mocking calls") @@ -1049,7 +1064,7 @@ class TestGrpc: # then queue.get(timeout=5) - def test_session_events(self, grpc_server): + def test_session_events(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -1072,7 +1087,7 @@ class TestGrpc: # then queue.get(timeout=5) - def test_config_events(self, grpc_server): + def test_config_events(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -1096,7 +1111,7 @@ class TestGrpc: # then queue.get(timeout=5) - def test_exception_events(self, grpc_server): + def test_exception_events(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -1125,7 +1140,7 @@ class TestGrpc: # then queue.get(timeout=5) - def test_file_events(self, grpc_server): + def test_file_events(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index 4a086e53..89dcd7ab 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -3,6 +3,7 @@ Tests for testing tlv message handling. """ import os import time +from typing import Optional import mock import netaddr @@ -10,6 +11,7 @@ import pytest from mock import MagicMock from core.api.tlv import coreapi +from core.api.tlv.corehandlers import CoreHandler from core.api.tlv.enumerations import ( ConfigFlags, ConfigTlvs, @@ -28,7 +30,7 @@ from core.nodes.base import CoreNode, NodeBase from core.nodes.network import SwitchNode, WlanNode -def dict_to_str(values): +def dict_to_str(values) -> str: return "|".join(f"{x}={values[x]}" for x in values) @@ -44,7 +46,9 @@ class TestGui: (NodeTypes.TUNNEL, None), ], ) - def test_node_add(self, coretlv, node_type, model): + def test_node_add( + self, coretlv: CoreHandler, node_type: NodeTypes, model: Optional[str] + ): node_id = 1 message = coreapi.CoreNodeMessage.create( MessageFlags.ADD.value, @@ -61,7 +65,7 @@ class TestGui: coretlv.handle_message(message) assert coretlv.session.get_node(node_id, NodeBase) is not None - def test_node_update(self, coretlv): + def test_node_update(self, coretlv: CoreHandler): node_id = 1 coretlv.session.add_node(CoreNode, _id=node_id) x = 50 @@ -82,7 +86,7 @@ class TestGui: assert node.position.x == x assert node.position.y == y - def test_node_delete(self, coretlv): + def test_node_delete(self, coretlv: CoreHandler): node_id = 1 coretlv.session.add_node(CoreNode, _id=node_id) message = coreapi.CoreNodeMessage.create( @@ -94,7 +98,7 @@ class TestGui: with pytest.raises(CoreError): coretlv.session.get_node(node_id, NodeBase) - def test_link_add_node_to_net(self, coretlv): + def test_link_add_node_to_net(self, coretlv: CoreHandler): node_one = 1 coretlv.session.add_node(CoreNode, _id=node_one) switch = 2 @@ -118,7 +122,7 @@ class TestGui: all_links = switch_node.all_link_data() assert len(all_links) == 1 - def test_link_add_net_to_node(self, coretlv): + def test_link_add_net_to_node(self, coretlv: CoreHandler): node_one = 1 coretlv.session.add_node(CoreNode, _id=node_one) switch = 2 @@ -142,7 +146,7 @@ class TestGui: all_links = switch_node.all_link_data() assert len(all_links) == 1 - def test_link_add_node_to_node(self, coretlv): + def test_link_add_node_to_node(self, coretlv: CoreHandler): node_one = 1 coretlv.session.add_node(CoreNode, _id=node_one) node_two = 2 @@ -172,7 +176,7 @@ class TestGui: all_links += node.all_link_data() assert len(all_links) == 1 - def test_link_update(self, coretlv): + def test_link_update(self, coretlv: CoreHandler): node_one = 1 coretlv.session.add_node(CoreNode, _id=node_one) switch = 2 @@ -214,7 +218,7 @@ class TestGui: link = all_links[0] assert link.bandwidth == bandwidth - def test_link_delete_node_to_node(self, coretlv): + def test_link_delete_node_to_node(self, coretlv: CoreHandler): node_one = 1 coretlv.session.add_node(CoreNode, _id=node_one) node_two = 2 @@ -258,7 +262,7 @@ class TestGui: all_links += node.all_link_data() assert len(all_links) == 0 - def test_link_delete_node_to_net(self, coretlv): + def test_link_delete_node_to_net(self, coretlv: CoreHandler): node_one = 1 coretlv.session.add_node(CoreNode, _id=node_one) switch = 2 @@ -294,7 +298,7 @@ class TestGui: all_links = switch_node.all_link_data() assert len(all_links) == 0 - def test_link_delete_net_to_node(self, coretlv): + def test_link_delete_net_to_node(self, coretlv: CoreHandler): node_one = 1 coretlv.session.add_node(CoreNode, _id=node_one) switch = 2 @@ -330,7 +334,7 @@ class TestGui: all_links = switch_node.all_link_data() assert len(all_links) == 0 - def test_session_update(self, coretlv): + def test_session_update(self, coretlv: CoreHandler): session_id = coretlv.session.id name = "test" message = coreapi.CoreSessionMessage.create( @@ -341,7 +345,7 @@ class TestGui: assert coretlv.session.name == name - def test_session_query(self, coretlv): + def test_session_query(self, coretlv: CoreHandler): coretlv.dispatch_replies = mock.MagicMock() message = coreapi.CoreSessionMessage.create(MessageFlags.STRING.value, []) @@ -351,7 +355,7 @@ class TestGui: replies = args[0] assert len(replies) == 1 - def test_session_join(self, coretlv): + def test_session_join(self, coretlv: CoreHandler): coretlv.dispatch_replies = mock.MagicMock() session_id = coretlv.session.id message = coreapi.CoreSessionMessage.create( @@ -362,7 +366,7 @@ class TestGui: assert coretlv.session.id == session_id - def test_session_delete(self, coretlv): + def test_session_delete(self, coretlv: CoreHandler): assert len(coretlv.coreemu.sessions) == 1 session_id = coretlv.session.id message = coreapi.CoreSessionMessage.create( @@ -373,7 +377,7 @@ class TestGui: assert len(coretlv.coreemu.sessions) == 0 - def test_file_hook_add(self, coretlv): + def test_file_hook_add(self, coretlv: CoreHandler): state = EventTypes.DATACOLLECT_STATE assert coretlv.session._hooks.get(state) is None file_name = "test.sh" @@ -395,7 +399,7 @@ class TestGui: assert file_name == name assert file_data == data - def test_file_service_file_set(self, coretlv): + def test_file_service_file_set(self, coretlv: CoreHandler): node = coretlv.session.add_node(CoreNode) service = "DefaultRoute" file_name = "defaultroute.sh" @@ -417,7 +421,7 @@ class TestGui: ) assert file_data == service_file.data - def test_file_node_file_copy(self, request, coretlv): + def test_file_node_file_copy(self, request, coretlv: CoreHandler): file_name = "/var/log/test/node.log" node = coretlv.session.add_node(CoreNode) node.makenodedir() @@ -439,7 +443,7 @@ class TestGui: create_path = os.path.join(node.nodedir, created_directory, basename) assert os.path.exists(create_path) - def test_exec_node_tty(self, coretlv): + def test_exec_node_tty(self, coretlv: CoreHandler): coretlv.dispatch_replies = mock.MagicMock() node = coretlv.session.add_node(CoreNode) message = coreapi.CoreExecMessage.create( @@ -457,7 +461,7 @@ class TestGui: replies = args[0] assert len(replies) == 1 - def test_exec_local_command(self, request, coretlv): + def test_exec_local_command(self, request, coretlv: CoreHandler): if request.config.getoption("mock"): pytest.skip("mocking calls") @@ -479,7 +483,7 @@ class TestGui: replies = args[0] assert len(replies) == 1 - def test_exec_node_command(self, coretlv): + def test_exec_node_command(self, coretlv: CoreHandler): coretlv.dispatch_replies = mock.MagicMock() node = coretlv.session.add_node(CoreNode) cmd = "echo hello" @@ -514,7 +518,7 @@ class TestGui: assert coretlv.session.state == state - def test_event_schedule(self, coretlv): + def test_event_schedule(self, coretlv: CoreHandler): coretlv.session.add_event = mock.MagicMock() node = coretlv.session.add_node(CoreNode) message = coreapi.CoreEventMessage.create( @@ -602,7 +606,7 @@ class TestGui: coretlv.handle_message(message) - def test_register_gui(self, coretlv): + def test_register_gui(self, coretlv: CoreHandler): message = coreapi.CoreRegMessage.create(0, [(RegisterTlvs.GUI, "gui")]) coretlv.handle_message(message) @@ -638,7 +642,7 @@ class TestGui: assert len(coretlv.session.nodes) == 1 - def test_config_all(self, coretlv): + def test_config_all(self, coretlv: CoreHandler): message = coreapi.CoreConfMessage.create( MessageFlags.ADD.value, [(ConfigTlvs.OBJECT, "all"), (ConfigTlvs.TYPE, ConfigFlags.RESET.value)], @@ -649,7 +653,7 @@ class TestGui: assert coretlv.session.location.refxyz == (0, 0, 0) - def test_config_options_request(self, coretlv): + def test_config_options_request(self, coretlv: CoreHandler): message = coreapi.CoreConfMessage.create( 0, [ @@ -663,7 +667,7 @@ class TestGui: coretlv.handle_broadcast_config.assert_called_once() - def test_config_options_update(self, coretlv): + def test_config_options_update(self, coretlv: CoreHandler): test_key = "test" test_value = "test" values = {test_key: test_value} @@ -680,7 +684,7 @@ class TestGui: assert coretlv.session.options.get_config(test_key) == test_value - def test_config_location_reset(self, coretlv): + def test_config_location_reset(self, coretlv: CoreHandler): message = coreapi.CoreConfMessage.create( 0, [ @@ -694,7 +698,7 @@ class TestGui: assert coretlv.session.location.refxyz == (0, 0, 0) - def test_config_location_update(self, coretlv): + def test_config_location_update(self, coretlv: CoreHandler): message = coreapi.CoreConfMessage.create( 0, [ @@ -710,7 +714,7 @@ class TestGui: assert coretlv.session.location.refgeo == (70, 50, 0) assert coretlv.session.location.refscale == 0.5 - def test_config_metadata_request(self, coretlv): + def test_config_metadata_request(self, coretlv: CoreHandler): message = coreapi.CoreConfMessage.create( 0, [ @@ -724,7 +728,7 @@ class TestGui: coretlv.handle_broadcast_config.assert_called_once() - def test_config_metadata_update(self, coretlv): + def test_config_metadata_update(self, coretlv: CoreHandler): test_key = "test" test_value = "test" values = {test_key: test_value} @@ -741,7 +745,7 @@ class TestGui: assert coretlv.session.metadata[test_key] == test_value - def test_config_broker_request(self, coretlv): + def test_config_broker_request(self, coretlv: CoreHandler): server = "test" host = "10.0.0.1" port = 50000 @@ -759,7 +763,7 @@ class TestGui: coretlv.session.distributed.add_server.assert_called_once_with(server, host) - def test_config_services_request_all(self, coretlv): + def test_config_services_request_all(self, coretlv: CoreHandler): message = coreapi.CoreConfMessage.create( 0, [ @@ -773,7 +777,7 @@ class TestGui: coretlv.handle_broadcast_config.assert_called_once() - def test_config_services_request_specific(self, coretlv): + def test_config_services_request_specific(self, coretlv: CoreHandler): node = coretlv.session.add_node(CoreNode) message = coreapi.CoreConfMessage.create( 0, @@ -790,7 +794,7 @@ class TestGui: coretlv.handle_broadcast_config.assert_called_once() - def test_config_services_request_specific_file(self, coretlv): + def test_config_services_request_specific_file(self, coretlv: CoreHandler): node = coretlv.session.add_node(CoreNode) message = coreapi.CoreConfMessage.create( 0, @@ -807,7 +811,7 @@ class TestGui: coretlv.session.broadcast_file.assert_called_once() - def test_config_services_reset(self, coretlv): + def test_config_services_reset(self, coretlv: CoreHandler): node = coretlv.session.add_node(CoreNode) service = "DefaultRoute" coretlv.session.services.set_service(node.id, service) @@ -824,7 +828,7 @@ class TestGui: assert coretlv.session.services.get_service(node.id, service) is None - def test_config_services_set(self, coretlv): + def test_config_services_set(self, coretlv: CoreHandler): node = coretlv.session.add_node(CoreNode) service = "DefaultRoute" values = {"meta": "metadata"} @@ -844,7 +848,7 @@ class TestGui: assert coretlv.session.services.get_service(node.id, service) is not None - def test_config_mobility_reset(self, coretlv): + def test_config_mobility_reset(self, coretlv: CoreHandler): wlan = coretlv.session.add_node(WlanNode) message = coreapi.CoreConfMessage.create( 0, @@ -860,7 +864,7 @@ class TestGui: assert len(coretlv.session.mobility.node_configurations) == 0 - def test_config_mobility_model_request(self, coretlv): + def test_config_mobility_model_request(self, coretlv: CoreHandler): wlan = coretlv.session.add_node(WlanNode) message = coreapi.CoreConfMessage.create( 0, @@ -876,7 +880,7 @@ class TestGui: coretlv.handle_broadcast_config.assert_called_once() - def test_config_mobility_model_update(self, coretlv): + def test_config_mobility_model_update(self, coretlv: CoreHandler): wlan = coretlv.session.add_node(WlanNode) config_key = "range" config_value = "1000" @@ -898,7 +902,7 @@ class TestGui: ) assert config[config_key] == config_value - def test_config_emane_model_request(self, coretlv): + def test_config_emane_model_request(self, coretlv: CoreHandler): wlan = coretlv.session.add_node(WlanNode) message = coreapi.CoreConfMessage.create( 0, @@ -914,7 +918,7 @@ class TestGui: coretlv.handle_broadcast_config.assert_called_once() - def test_config_emane_model_update(self, coretlv): + def test_config_emane_model_update(self, coretlv: CoreHandler): wlan = coretlv.session.add_node(WlanNode) config_key = "distance" config_value = "50051" @@ -936,7 +940,7 @@ class TestGui: ) assert config[config_key] == config_value - def test_config_emane_request(self, coretlv): + def test_config_emane_request(self, coretlv: CoreHandler): message = coreapi.CoreConfMessage.create( 0, [ @@ -950,7 +954,7 @@ class TestGui: coretlv.handle_broadcast_config.assert_called_once() - def test_config_emane_update(self, coretlv): + def test_config_emane_update(self, coretlv: CoreHandler): config_key = "eventservicedevice" config_value = "eth4" values = {config_key: config_value} diff --git a/daemon/tests/test_links.py b/daemon/tests/test_links.py index afbdaab1..94b2e53f 100644 --- a/daemon/tests/test_links.py +++ b/daemon/tests/test_links.py @@ -1,9 +1,14 @@ -from core.emulator.emudata import LinkOptions +from typing import Tuple + +from core.emulator.emudata import IpPrefixes, LinkOptions +from core.emulator.session import Session from core.nodes.base import CoreNode from core.nodes.network import SwitchNode -def create_ptp_network(session, ip_prefixes): +def create_ptp_network( + session: Session, ip_prefixes: IpPrefixes +) -> Tuple[CoreNode, CoreNode]: # create nodes node_one = session.add_node(CoreNode) node_two = session.add_node(CoreNode) @@ -20,7 +25,7 @@ def create_ptp_network(session, ip_prefixes): class TestLinks: - def test_ptp(self, session, ip_prefixes): + def test_ptp(self, session: Session, ip_prefixes: IpPrefixes): # given node_one = session.add_node(CoreNode) node_two = session.add_node(CoreNode) @@ -34,7 +39,7 @@ class TestLinks: assert node_one.netif(interface_one.id) assert node_two.netif(interface_two.id) - def test_node_to_net(self, session, ip_prefixes): + def test_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given node_one = session.add_node(CoreNode) node_two = session.add_node(SwitchNode) @@ -47,7 +52,7 @@ class TestLinks: assert node_two.all_link_data() assert node_one.netif(interface_one.id) - def test_net_to_node(self, session, ip_prefixes): + def test_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given node_one = session.add_node(SwitchNode) node_two = session.add_node(CoreNode) @@ -71,7 +76,7 @@ class TestLinks: # then assert node_one.all_link_data() - def test_link_update(self, session, ip_prefixes): + def test_link_update(self, session: Session, ip_prefixes: IpPrefixes): # given delay = 50 bandwidth = 5000000 @@ -110,7 +115,7 @@ class TestLinks: assert interface_one.getparam("duplicate") == dup assert interface_one.getparam("jitter") == jitter - def test_link_delete(self, session, ip_prefixes): + def test_link_delete(self, session: Session, ip_prefixes: IpPrefixes): # given node_one = session.add_node(CoreNode) node_two = session.add_node(CoreNode) diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index f87e8e80..65b17949 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -1,6 +1,7 @@ import pytest from core.emulator.emudata import NodeOptions +from core.emulator.session import Session from core.errors import CoreError from core.nodes.base import CoreNode from core.nodes.network import HubNode, SwitchNode, WlanNode @@ -11,7 +12,7 @@ NET_TYPES = [SwitchNode, HubNode, WlanNode] class TestNodes: @pytest.mark.parametrize("model", MODELS) - def test_node_add(self, session, model): + def test_node_add(self, session: Session, model: str): # given options = NodeOptions(model=model) @@ -23,7 +24,7 @@ class TestNodes: assert node.alive() assert node.up - def test_node_update(self, session): + def test_node_update(self, session: Session): # given node = session.add_node(CoreNode) position_value = 100 @@ -37,7 +38,7 @@ class TestNodes: assert node.position.x == position_value assert node.position.y == position_value - def test_node_delete(self, session): + def test_node_delete(self, session: Session): # given node = session.add_node(CoreNode) @@ -48,7 +49,7 @@ class TestNodes: with pytest.raises(CoreError): session.get_node(node.id, CoreNode) - def test_node_sethwaddr(self, session): + def test_node_sethwaddr(self, session: Session): # given node = session.add_node(CoreNode) index = node.newnetif() @@ -61,7 +62,7 @@ class TestNodes: # then assert interface.hwaddr == mac - def test_node_sethwaddr_exception(self, session): + def test_node_sethwaddr_exception(self, session: Session): # given node = session.add_node(CoreNode) index = node.newnetif() @@ -72,7 +73,7 @@ class TestNodes: with pytest.raises(CoreError): node.sethwaddr(index, mac) - def test_node_addaddr(self, session): + def test_node_addaddr(self, session: Session): # given node = session.add_node(CoreNode) index = node.newnetif() diff --git a/daemon/tests/test_services.py b/daemon/tests/test_services.py index c5a51461..e304a275 100644 --- a/daemon/tests/test_services.py +++ b/daemon/tests/test_services.py @@ -3,6 +3,7 @@ import os import pytest from mock import MagicMock +from core.emulator.session import Session from core.errors import CoreCommandError from core.nodes.base import CoreNode from core.services.coreservices import CoreService, ServiceDependencies, ServiceManager @@ -49,7 +50,7 @@ class ServiceCycleDependency(CoreService): class TestServices: - def test_service_all_files(self, session): + def test_service_all_files(self, session: Session): # given ServiceManager.add_services(_SERVICES_PATH) file_name = "myservice.sh" @@ -64,7 +65,7 @@ class TestServices: assert service assert all_files and len(all_files) == 1 - def test_service_all_configs(self, session): + def test_service_all_configs(self, session: Session): # given ServiceManager.add_services(_SERVICES_PATH) node = session.add_node(CoreNode) @@ -78,7 +79,7 @@ class TestServices: assert all_configs assert len(all_configs) == 2 - def test_service_add_services(self, session): + def test_service_add_services(self, session: Session): # given ServiceManager.add_services(_SERVICES_PATH) node = session.add_node(CoreNode) @@ -91,7 +92,7 @@ class TestServices: assert node.services assert len(node.services) == total_service + 2 - def test_service_file(self, request, session): + def test_service_file(self, request, session: Session): # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_ONE) @@ -106,7 +107,7 @@ class TestServices: if not request.config.getoption("mock"): assert os.path.exists(file_path) - def test_service_validate(self, session): + def test_service_validate(self, session: Session): # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_ONE) @@ -119,7 +120,7 @@ class TestServices: # then assert not status - def test_service_validate_error(self, session): + def test_service_validate_error(self, session: Session): # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_TWO) @@ -133,7 +134,7 @@ class TestServices: # then assert status - def test_service_startup(self, session): + def test_service_startup(self, session: Session): # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_ONE) @@ -146,7 +147,7 @@ class TestServices: # then assert not status - def test_service_startup_error(self, session): + def test_service_startup_error(self, session: Session): # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_TWO) @@ -160,7 +161,7 @@ class TestServices: # then assert status - def test_service_stop(self, session): + def test_service_stop(self, session: Session): # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_ONE) @@ -173,7 +174,7 @@ class TestServices: # then assert not status - def test_service_stop_error(self, session): + def test_service_stop_error(self, session: Session): # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_TWO) @@ -187,7 +188,7 @@ class TestServices: # then assert status - def test_service_custom_startup(self, session): + def test_service_custom_startup(self, session: Session): # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_ONE) @@ -201,7 +202,7 @@ class TestServices: # then assert my_service.startup != custom_my_service.startup - def test_service_set_file(self, session): + def test_service_set_file(self, session: Session): # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_ONE) @@ -231,7 +232,7 @@ class TestServices: assert ServiceManager.get(SERVICE_ONE) assert ServiceManager.get(SERVICE_TWO) - def test_service_setget(self, session): + def test_service_setget(self, session: Session): # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_ONE) diff --git a/daemon/tests/test_utils.py b/daemon/tests/test_utils.py index 0c6b84c4..3e43b789 100644 --- a/daemon/tests/test_utils.py +++ b/daemon/tests/test_utils.py @@ -34,12 +34,12 @@ class TestUtils: ("2001::/64", "2001::/64"), ], ) - def test_validate_ip(self, data, expected): + def test_validate_ip(self, data: str, expected: str): value = utils.validate_ip(data) assert value == expected @pytest.mark.parametrize("data", ["256", "1270.0.0.1", "127.0.0.0.1"]) - def test_validate_ip_exception(self, data): + def test_validate_ip_exception(self, data: str): with pytest.raises(CoreError): utils.validate_ip("") @@ -50,14 +50,14 @@ class TestUtils: ("00:00:00:FF:FF:FF", "00:00:00:ff:ff:ff"), ], ) - def test_validate_mac(self, data, expected): + def test_validate_mac(self, data: str, expected: str): value = utils.validate_mac(data) assert value == expected @pytest.mark.parametrize( "data", ["AAA:AA:AA:FF:FF:FF", "AA:AA:AA:FF:FF", "AA/AA/AA/FF/FF/FF"] ) - def test_validate_mac_exception(self, data): + def test_validate_mac_exception(self, data: str): with pytest.raises(CoreError): utils.validate_mac(data) diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index bb5a6bf9..70117fb8 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -1,9 +1,11 @@ +from tempfile import TemporaryFile from xml.etree import ElementTree import pytest -from core.emulator.emudata import LinkOptions, NodeOptions +from core.emulator.emudata import IpPrefixes, LinkOptions, NodeOptions from core.emulator.enumerations import EventTypes +from core.emulator.session import Session from core.errors import CoreError from core.location.mobility import BasicRangeModel from core.nodes.base import CoreNode @@ -12,7 +14,7 @@ from core.services.utility import SshService class TestXml: - def test_xml_hooks(self, session, tmpdir): + def test_xml_hooks(self, session: Session, tmpdir: TemporaryFile): """ Test save/load hooks in xml. @@ -52,7 +54,9 @@ class TestXml: assert file_name == runtime_hook[0] assert data == runtime_hook[1] - def test_xml_ptp(self, session, tmpdir, ip_prefixes): + def test_xml_ptp( + self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes + ): """ Test xml client methods for a ptp network. @@ -104,7 +108,9 @@ class TestXml: assert session.get_node(n1_id, CoreNode) assert session.get_node(n2_id, CoreNode) - def test_xml_ptp_services(self, session, tmpdir, ip_prefixes): + def test_xml_ptp_services( + self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes + ): """ Test xml client methods for a ptp neetwork. @@ -169,7 +175,9 @@ class TestXml: assert session.get_node(n2_id, CoreNode) assert service.config_data.get(service_file) == file_data - def test_xml_mobility(self, session, tmpdir, ip_prefixes): + def test_xml_mobility( + self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes + ): """ Test xml client methods for mobility. @@ -230,7 +238,7 @@ class TestXml: assert session.get_node(wlan_id, WlanNode) assert value == "1" - def test_network_to_network(self, session, tmpdir): + def test_network_to_network(self, session: Session, tmpdir: TemporaryFile): """ Test xml generation when dealing with network to network nodes. @@ -279,7 +287,9 @@ class TestXml: assert switch_two assert len(switch_one.all_link_data() + switch_two.all_link_data()) == 1 - def test_link_options(self, session, tmpdir, ip_prefixes): + def test_link_options( + self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes + ): """ Test xml client methods for a ptp network. @@ -345,7 +355,9 @@ class TestXml: assert link_options.delay == link.delay assert link_options.dup == link.dup - def test_link_options_ptp(self, session, tmpdir, ip_prefixes): + def test_link_options_ptp( + self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes + ): """ Test xml client methods for a ptp network. @@ -412,7 +424,9 @@ class TestXml: assert link_options.delay == link.delay assert link_options.dup == link.dup - def test_link_options_bidirectional(self, session, tmpdir, ip_prefixes): + def test_link_options_bidirectional( + self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes + ): """ Test xml client methods for a ptp network. From 2e7802524991750f446f464cc9905a1108b7442e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 May 2020 00:16:58 -0700 Subject: [PATCH 0289/1131] started type hinting on class variables for nodes/base --- daemon/core/nodes/base.py | 77 +++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index e1267530..662815ef 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -6,6 +6,7 @@ import logging import os import shutil import threading +from threading import RLock from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type import netaddr @@ -16,7 +17,7 @@ from core.constants import MOUNT_BIN, VNODED_BIN from core.emulator.data import LinkData, NodeData from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes from core.errors import CoreCommandError, CoreError -from core.nodes import client +from core.nodes.client import VnodeClient from core.nodes.interface import CoreInterface, TunTap, Veth from core.nodes.netclient import LinuxNetClient, get_net_client @@ -24,7 +25,9 @@ if TYPE_CHECKING: from core.emulator.distributed import DistributedServer from core.emulator.session import Session from core.configservice.base import ConfigService + from core.services.coreservices import CoreService + CoreServices = List[CoreService] ConfigServiceType = Type[ConfigService] _DEFAULT_MTU = 1500 @@ -35,7 +38,7 @@ class NodeBase: Base class for CORE nodes (nodes and networks) """ - apitype = None + apitype: Optional[NodeTypes] = None # TODO: appears start has no usage, verify and remove def __init__( @@ -57,25 +60,25 @@ class NodeBase: will run on, default is None for localhost """ - self.session = session + self.session: "Session" = session if _id is None: _id = session.get_node_id() - self.id = _id + self.id: int = _id if name is None: name = f"o{self.id}" - self.name = name - self.server = server - self.type = None - self.services = None - self._netif = {} - self.ifindex = 0 - self.canvas = None - self.icon = None - self.opaque = None - self.position = Position() - self.up = False + self.name: str = name + self.server: "DistributedServer" = server + self.type: Optional[str] = None + self.services: CoreServices = [] + self._netif: Dict[int, CoreInterface] = {} + self.ifindex: int = 0 + self.canvas: Optional[int] = None + self.icon: Optional[str] = None + self.opaque: Optional[str] = None + self.position: Position = Position() + self.up: bool = False use_ovs = session.options.get_config("ovs") == "True" - self.net_client = get_net_client(use_ovs, self.host_cmd) + self.net_client: LinuxNetClient = get_net_client(use_ovs, self.host_cmd) def startup(self) -> None: """ @@ -207,9 +210,7 @@ class NodeBase: server = None if self.server is not None: server = self.server.name - services = None - if self.services is not None: - services = [service.name for service in self.services] + services = [service.name for service in self.services] return NodeData( message_type=message_type, id=self.id, @@ -266,10 +267,9 @@ class CoreNodeBase(NodeBase): will run on, default is None for localhost """ super().__init__(session, _id, name, start, server) - self.services = [] - self.config_services = {} - self.nodedir = None - self.tmpnodedir = False + self.config_services: Dict[str, "ConfigService"] = {} + self.nodedir: Optional[str] = None + self.tmpnodedir: bool = False def add_config_service(self, service_class: "ConfigServiceType") -> None: """ @@ -298,7 +298,7 @@ class CoreNodeBase(NodeBase): def start_config_services(self) -> None: """ - Determins startup paths and starts configuration services, based on their + Determines startup paths and starts configuration services, based on their dependency chains. :return: nothing @@ -330,7 +330,6 @@ class CoreNodeBase(NodeBase): preserve = self.session.options.get_config("preservedir") == "1" if preserve: return - if self.tmpnodedir: self.host_cmd(f"rm -rf {self.nodedir}") @@ -503,16 +502,16 @@ class CoreNode(CoreNodeBase): will run on, default is None for localhost """ super().__init__(session, _id, name, start, server) - self.nodedir = nodedir - self.ctrlchnlname = os.path.abspath( + self.nodedir: Optional[str] = nodedir + self.ctrlchnlname: str = os.path.abspath( os.path.join(self.session.session_dir, self.name) ) - self.client = None - self.pid = None - self.lock = threading.RLock() - self._mounts = [] + self.client: Optional[VnodeClient] = None + self.pid: Optional[int] = None + self.lock: RLock = RLock() + self._mounts: List[Tuple[str, str]] = [] use_ovs = session.options.get_config("ovs") == "True" - self.node_net_client = self.create_node_net_client(use_ovs) + self.node_net_client: LinuxNetClient = self.create_node_net_client(use_ovs) if start: self.startup() @@ -567,7 +566,7 @@ class CoreNode(CoreNodeBase): logging.debug("node(%s) pid: %s", self.name, self.pid) # create vnode client - self.client = client.VnodeClient(self.name, self.ctrlchnlname) + self.client = VnodeClient(self.name, self.ctrlchnlname) # bring up the loopback interface logging.debug("bringing up loopback interface") @@ -1201,12 +1200,12 @@ class Position: :param y: y position :param z: z position """ - self.x = x - self.y = y - self.z = z - self.lon = None - self.lat = None - self.alt = None + self.x: float = x + self.y: float = y + self.z: float = z + self.lon: Optional[float] = None + self.lat: Optional[float] = None + self.alt: Optional[float] = None def set(self, x: float = None, y: float = None, z: float = None) -> bool: """ From f95a8113c9ba0601a9e14c43d4d27593e1f9b174 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 May 2020 11:17:28 -0700 Subject: [PATCH 0290/1131] added type hinting to nodes/network.py class variables --- daemon/core/emulator/session.py | 5 +- daemon/core/nodes/network.py | 105 +++++++++++++++----------------- 2 files changed, 52 insertions(+), 58 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index de7b1fe8..135c58dd 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -1759,10 +1759,9 @@ class Session: server_interface, ) control_net = self.create_node( - _class=CtrlNet, + CtrlNet, + prefix, _id=_id, - prefix=prefix, - assign_address=True, updown_script=updown_script, serverintf=server_interface, ) diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 5f6c635c..973346ad 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -21,7 +21,7 @@ from core.nodes.netclient import get_net_client if TYPE_CHECKING: from core.emulator.distributed import DistributedServer from core.emulator.session import Session - from core.location.mobility import WirelessModel + from core.location.mobility import WirelessModel, WayPointMobility WirelessModelType = Type[WirelessModel] @@ -36,26 +36,26 @@ class EbtablesQueue: """ # update rate is every 300ms - rate = 0.3 + rate: float = 0.3 # ebtables - atomic_file = "/tmp/pycore.ebtables.atomic" + atomic_file: str = "/tmp/pycore.ebtables.atomic" def __init__(self) -> None: """ Initialize the helper class, but don't start the update thread until a WLAN is instantiated. """ - self.doupdateloop = False - self.updatethread = None + self.doupdateloop: bool = False + self.updatethread: Optional[threading.Thread] = None # this lock protects cmds and updates lists - self.updatelock = threading.Lock() + self.updatelock: threading.Lock = threading.Lock() # list of pending ebtables commands - self.cmds = [] + self.cmds: List[str] = [] # list of WLANs requiring update - self.updates = [] + self.updates: List["CoreNetwork"] = [] # timestamps of last WLAN update; this keeps track of WLANs that are # using this queue - self.last_update_time = {} + self.last_update_time: Dict["CoreNetwork", float] = {} def startupdateloop(self, wlan: "CoreNetwork") -> None: """ @@ -65,10 +65,8 @@ class EbtablesQueue: """ with self.updatelock: self.last_update_time[wlan] = time.monotonic() - if self.doupdateloop: return - self.doupdateloop = True self.updatethread = threading.Thread(target=self.updateloop, daemon=True) self.updatethread.start() @@ -86,10 +84,8 @@ class EbtablesQueue: logging.exception( "error deleting last update time for wlan, ignored before: %s", wlan ) - if len(self.last_update_time) > 0: return - self.doupdateloop = False if self.updatethread: self.updatethread.join() @@ -233,7 +229,7 @@ class EbtablesQueue: # a global object because all WLANs share the same queue # cannot have multiple threads invoking the ebtables commnd -ebq = EbtablesQueue() +ebq: EbtablesQueue = EbtablesQueue() def ebtablescmds(call: Callable[..., str], cmds: List[str]) -> None: @@ -254,7 +250,7 @@ class CoreNetwork(CoreNetworkBase): Provides linux bridge network functionality for core nodes. """ - policy = "DROP" + policy: str = "DROP" def __init__( self, @@ -281,10 +277,10 @@ class CoreNetwork(CoreNetworkBase): name = str(self.id) if policy is not None: self.policy = policy - self.name = name + self.name: Optional[str] = name sessionid = self.session.short_session_id() - self.brname = f"b.{self.id}.{sessionid}" - self.has_ebtables_chain = False + self.brname: str = f"b.{self.id}.{sessionid}" + self.has_ebtables_chain: bool = False if start: self.startup() ebq.startupdateloop(self) @@ -633,17 +629,16 @@ class GreTapBridge(CoreNetwork): will run on, default is None for localhost """ CoreNetwork.__init__(self, session, _id, name, False, server, policy) - self.grekey = key - if self.grekey is None: - self.grekey = self.session.id ^ self.id - self.localnum = None - self.remotenum = None - self.remoteip = remoteip - self.localip = localip - self.ttl = ttl - if remoteip is None: - self.gretap = None - else: + if key is None: + key = self.session.id ^ self.id + self.grekey: int = key + self.localnum: Optional[int] = None + self.remotenum: Optional[int] = None + self.remoteip: Optional[str] = remoteip + self.localip: Optional[str] = localip + self.ttl: int = ttl + self.gretap: Optional[GreTap] = None + if remoteip is not None: self.gretap = GreTap( node=self, session=session, @@ -718,10 +713,10 @@ class CtrlNet(CoreNetwork): Control network functionality. """ - policy = "ACCEPT" + policy: str = "ACCEPT" # base control interface index - CTRLIF_IDX_BASE = 99 - DEFAULT_PREFIX_LIST = [ + CTRLIF_IDX_BASE: int = 99 + DEFAULT_PREFIX_LIST: List[str] = [ "172.16.0.0/24 172.16.1.0/24 172.16.2.0/24 172.16.3.0/24 172.16.4.0/24", "172.17.0.0/24 172.17.1.0/24 172.17.2.0/24 172.17.3.0/24 172.17.4.0/24", "172.18.0.0/24 172.18.1.0/24 172.18.2.0/24 172.18.3.0/24 172.18.4.0/24", @@ -731,15 +726,15 @@ class CtrlNet(CoreNetwork): def __init__( self, session: "Session", + prefix: str, _id: int = None, name: str = None, - prefix: str = None, hostid: int = None, start: bool = True, server: "DistributedServer" = None, assign_address: bool = True, updown_script: str = None, - serverintf: CoreInterface = None, + serverintf: str = None, ) -> None: """ Creates a CtrlNet instance. @@ -757,11 +752,11 @@ class CtrlNet(CoreNetwork): :param serverintf: server interface :return: """ - self.prefix = netaddr.IPNetwork(prefix).cidr - self.hostid = hostid - self.assign_address = assign_address - self.updown_script = updown_script - self.serverintf = serverintf + self.prefix: netaddr.IPNetwork = netaddr.IPNetwork(prefix).cidr + self.hostid: Optional[int] = hostid + self.assign_address: bool = assign_address + self.updown_script: Optional[str] = updown_script + self.serverintf: Optional[str] = serverintf super().__init__(session, _id, name, start, server) def add_addresses(self, index: int) -> None: @@ -858,7 +853,7 @@ class PtpNet(CoreNetwork): Peer to peer network node. """ - policy = "ACCEPT" + policy: str = "ACCEPT" def attach(self, netif: CoreInterface) -> None: """ @@ -988,9 +983,9 @@ class SwitchNode(CoreNetwork): Provides switch functionality within a core node. """ - apitype = NodeTypes.SWITCH - policy = "ACCEPT" - type = "lanswitch" + apitype: NodeTypes = NodeTypes.SWITCH + policy: str = "ACCEPT" + type: str = "lanswitch" class HubNode(CoreNetwork): @@ -999,9 +994,9 @@ class HubNode(CoreNetwork): ports by turning off MAC address learning. """ - apitype = NodeTypes.HUB - policy = "ACCEPT" - type = "hub" + apitype: NodeTypes = NodeTypes.HUB + policy: str = "ACCEPT" + type: str = "hub" def startup(self) -> None: """ @@ -1018,10 +1013,10 @@ class WlanNode(CoreNetwork): Provides wireless lan functionality within a core node. """ - apitype = NodeTypes.WIRELESS_LAN - linktype = LinkTypes.WIRED - policy = "DROP" - type = "wlan" + apitype: NodeTypes = NodeTypes.WIRELESS_LAN + linktype: LinkTypes = LinkTypes.WIRED + policy: str = "DROP" + type: str = "wlan" def __init__( self, @@ -1045,8 +1040,8 @@ class WlanNode(CoreNetwork): """ super().__init__(session, _id, name, start, server, policy) # wireless and mobility models (BasicRangeModel, Ns2WaypointMobility) - self.model = None - self.mobility = None + self.model: Optional[WirelessModel] = None + self.mobility: Optional[WayPointMobility] = None def startup(self) -> None: """ @@ -1122,6 +1117,6 @@ class TunnelNode(GreTapBridge): Provides tunnel functionality in a core node. """ - apitype = NodeTypes.TUNNEL - policy = "ACCEPT" - type = "tunnel" + apitype: NodeTypes = NodeTypes.TUNNEL + policy: str = "ACCEPT" + type: str = "tunnel" From 8fed201fd8641202b85082c9645d6ed7d1574d46 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 May 2020 11:33:59 -0700 Subject: [PATCH 0291/1131] added type hints to class variables in nodes/physical.py --- daemon/core/nodes/physical.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index b6ae8e8d..fac361e7 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -5,7 +5,7 @@ PhysicalNode class for including real systems in the emulated network. import logging import os import threading -from typing import IO, TYPE_CHECKING, List, Optional +from typing import IO, TYPE_CHECKING, List, Optional, Tuple from core import utils from core.constants import MOUNT_BIN, UMOUNT_BIN @@ -23,7 +23,7 @@ if TYPE_CHECKING: class PhysicalNode(CoreNodeBase): def __init__( self, - session, + session: "Session", _id: int = None, name: str = None, nodedir: str = None, @@ -33,10 +33,10 @@ class PhysicalNode(CoreNodeBase): super().__init__(session, _id, name, start, server) if not self.server: raise CoreError("physical nodes must be assigned to a remote server") - self.nodedir = nodedir - self.up = start - self.lock = threading.RLock() - self._mounts = [] + self.nodedir: Optional[str] = nodedir + self.up: bool = start + self.lock: threading.RLock = threading.RLock() + self._mounts: List[Tuple[str, str]] = [] if start: self.startup() @@ -112,7 +112,7 @@ class PhysicalNode(CoreNodeBase): logging.exception("trying to delete unknown address: %s", addr) if self.up: - self.net_client.delete_address(interface.name, str(addr)) + self.net_client.delete_address(interface.name, addr) def adoptnetif( self, netif: CoreInterface, ifindex: int, hwaddr: str, addrlist: List[str] @@ -256,8 +256,8 @@ class Rj45Node(CoreNodeBase, CoreInterface): network. """ - apitype = NodeTypes.RJ45 - type = "rj45" + apitype: NodeTypes = NodeTypes.RJ45 + type: str = "rj45" def __init__( self, @@ -281,11 +281,11 @@ class Rj45Node(CoreNodeBase, CoreInterface): """ CoreNodeBase.__init__(self, session, _id, name, start, server) CoreInterface.__init__(self, session, self, name, name, mtu, server) - self.lock = threading.RLock() - self.ifindex = None - self.transport_type = "raw" - self.old_up = False - self.old_addrs = [] + self.lock: threading.RLock = threading.RLock() + self.ifindex: Optional[int] = None + self.transport_type: str = "raw" + self.old_up: bool = False + self.old_addrs: List[str] = [] if start: self.startup() From 7e4ef0b280052adea9f09ac0f9833dccd364d0fa Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 May 2020 23:18:20 -0700 Subject: [PATCH 0292/1131] changes to make rj45 maintain the interface information, instead of trying to be 2 classes at once --- daemon/core/emulator/session.py | 2 +- daemon/core/nodes/netclient.py | 14 ++++- daemon/core/nodes/physical.py | 101 ++++++++++---------------------- daemon/core/services/frr.py | 2 +- daemon/core/services/quagga.py | 2 +- 5 files changed, 47 insertions(+), 74 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 135c58dd..9193196c 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -353,7 +353,7 @@ class Session: node_two.name, ) start = self.state.should_start() - net_one = self.create_node(_class=PtpNet, start=start) + net_one = self.create_node(PtpNet, start=start) # node to network if node_one and net_one: diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 5062dead..51ac075e 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -71,13 +71,22 @@ class LinuxNetClient: def device_show(self, device: str) -> str: """ - Show information for a device. + Show link information for a device. :param device: device to get information for :return: device information """ return self.run(f"{IP_BIN} link show {device}") + def address_show(self, device: str) -> str: + """ + Show address information for a device. + + :param device: device name + :return: address information + """ + return self.run(f"{IP_BIN} address show {device}") + def get_mac(self, device: str) -> str: """ Retrieve MAC address for a given device. @@ -114,7 +123,8 @@ class LinuxNetClient: :return: nothing """ self.run( - f"[ -e /sys/class/net/{device} ] && {IP_BIN} -6 address flush dev {device} || true", + f"[ -e /sys/class/net/{device} ] && " + f"{IP_BIN} address flush dev {device} || true", shell=True, ) diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index fac361e7..2fc743fa 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -250,7 +250,7 @@ class PhysicalNode(CoreNodeBase): return self.host_cmd(args, wait=wait) -class Rj45Node(CoreNodeBase, CoreInterface): +class Rj45Node(CoreNodeBase): """ RJ45Node is a physical interface on the host linked to the emulated network. @@ -279,13 +279,13 @@ class Rj45Node(CoreNodeBase, CoreInterface): :param server: remote server node will run on, default is None for localhost """ - CoreNodeBase.__init__(self, session, _id, name, start, server) - CoreInterface.__init__(self, session, self, name, name, mtu, server) + super().__init__(session, _id, name, start, server) + self.interface = CoreInterface(session, self, name, name, mtu, server) + self.interface.transport_type = "raw" self.lock: threading.RLock = threading.RLock() self.ifindex: Optional[int] = None - self.transport_type: str = "raw" self.old_up: bool = False - self.old_addrs: List[str] = [] + self.old_addrs: List[Tuple[str, Optional[str]]] = [] if start: self.startup() @@ -298,7 +298,7 @@ class Rj45Node(CoreNodeBase, CoreInterface): """ # interface will also be marked up during net.attach() self.savestate() - self.net_client.device_up(self.localname) + self.net_client.device_up(self.interface.localname) self.up = True def shutdown(self) -> None: @@ -310,38 +310,16 @@ class Rj45Node(CoreNodeBase, CoreInterface): """ if not self.up: return - + localname = self.interface.localname + self.net_client.device_down(localname) + self.net_client.device_flush(localname) try: - self.net_client.device_down(self.localname) - self.net_client.device_flush(self.localname) - self.net_client.delete_tc(self.localname) + self.net_client.delete_tc(localname) except CoreCommandError: - logging.exception("error shutting down") - + pass self.up = False self.restorestate() - # TODO: issue in that both classes inherited from provide the same method with - # different signatures - def attachnet(self, net: CoreNetworkBase) -> None: - """ - Attach a network. - - :param net: network to attach - :return: nothing - """ - CoreInterface.attachnet(self, net) - - # TODO: issue in that both classes inherited from provide the same method with - # different signatures - def detachnet(self) -> None: - """ - Detach a network. - - :return: nothing - """ - CoreInterface.detachnet(self) - def newnetif( self, net: CoreNetworkBase = None, @@ -366,22 +344,15 @@ class Rj45Node(CoreNodeBase, CoreInterface): with self.lock: if ifindex is None: ifindex = 0 - - if self.net is not None: + if self.interface.net is not None: raise ValueError("RJ45 nodes support at most 1 network interface") - - self._netif[ifindex] = self - # PyCoreNetIf.node is self - self.node = self + self._netif[ifindex] = self.interface self.ifindex = ifindex - if net is not None: - self.attachnet(net) - + self.interface.attachnet(net) if addrlist: for addr in utils.make_tuple(addrlist): self.addaddr(addr) - return ifindex def delnetif(self, ifindex: int) -> None: @@ -393,9 +364,7 @@ class Rj45Node(CoreNodeBase, CoreInterface): """ if ifindex is None: ifindex = 0 - self._netif.pop(ifindex) - if ifindex == self.ifindex: self.shutdown() else: @@ -413,15 +382,12 @@ class Rj45Node(CoreNodeBase, CoreInterface): :param net: network to retrieve :return: a network interface """ - if net is not None and net == self.net: - return self - + if net is not None and net == self.interface.net: + return self.interface if ifindex is None: ifindex = 0 - if ifindex == self.ifindex: - return self - + return self.interface return None def getifindex(self, netif: CoreInterface) -> Optional[int]: @@ -432,7 +398,7 @@ class Rj45Node(CoreNodeBase, CoreInterface): index for :return: interface index, None otherwise """ - if netif != self: + if netif != self.interface: return None return self.ifindex @@ -447,7 +413,7 @@ class Rj45Node(CoreNodeBase, CoreInterface): addr = utils.validate_ip(addr) if self.up: self.net_client.create_address(self.name, addr) - CoreInterface.addaddr(self, addr) + self.interface.addaddr(addr) def deladdr(self, addr: str) -> None: """ @@ -458,8 +424,8 @@ class Rj45Node(CoreNodeBase, CoreInterface): :raises CoreCommandError: when there is a command exception """ if self.up: - self.net_client.delete_address(self.name, str(addr)) - CoreInterface.deladdr(self, addr) + self.net_client.delete_address(self.name, addr) + self.interface.deladdr(addr) def savestate(self) -> None: """ @@ -470,14 +436,14 @@ class Rj45Node(CoreNodeBase, CoreInterface): :raises CoreCommandError: when there is a command exception """ self.old_up = False - self.old_addrs = [] - output = self.net_client.device_show(self.localname) + self.old_addrs: List[Tuple[str, Optional[str]]] = [] + localname = self.interface.localname + output = self.net_client.address_show(localname) for line in output.split("\n"): items = line.split() if len(items) < 2: continue - - if items[1] == f"{self.localname}:": + if items[1] == f"{localname}:": flags = items[2][1:-1].split(",") if "UP" in flags: self.old_up = True @@ -487,6 +453,7 @@ class Rj45Node(CoreNodeBase, CoreInterface): if items[1][:4] == "fe80": continue self.old_addrs.append((items[1], None)) + logging.info("saved rj45 state: addrs(%s) up(%s)", self.old_addrs, self.old_up) def restorestate(self) -> None: """ @@ -495,16 +462,12 @@ class Rj45Node(CoreNodeBase, CoreInterface): :return: nothing :raises CoreCommandError: when there is a command exception """ + localname = self.interface.localname + logging.info("restoring rj45 state: %s", localname) for addr in self.old_addrs: - if addr[1] is None: - self.net_client.create_address(self.localname, addr[0]) - else: - self.net_client.create_address( - self.localname, addr[0], broadcast=addr[1] - ) - + self.net_client.create_address(localname, addr[0], addr[1]) if self.old_up: - self.net_client.device_up(self.localname) + self.net_client.device_up(localname) def setposition(self, x: float = None, y: float = None, z: float = None) -> None: """ @@ -515,8 +478,8 @@ class Rj45Node(CoreNodeBase, CoreInterface): :param z: z position :return: True if position changed, False otherwise """ - CoreNodeBase.setposition(self, x, y, z) - CoreInterface.setposition(self) + super().setposition(x, y, z) + self.interface.setposition() def termcmdstring(self, sh: str) -> str: """ diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py index 799c03a5..9a344339 100644 --- a/daemon/core/services/frr.py +++ b/daemon/core/services/frr.py @@ -354,7 +354,7 @@ class FrrService(CoreService): for peerifc in ifc.net.netifs(): if peerifc == ifc: continue - if isinstance(peerifc, Rj45Node): + if isinstance(peerifc.node, Rj45Node): return True return False diff --git a/daemon/core/services/quagga.py b/daemon/core/services/quagga.py index 331e23da..a62cbc5c 100644 --- a/daemon/core/services/quagga.py +++ b/daemon/core/services/quagga.py @@ -272,7 +272,7 @@ class QuaggaService(CoreService): for peerifc in ifc.net.netifs(): if peerifc == ifc: continue - if isinstance(peerifc, Rj45Node): + if isinstance(peerifc.node, Rj45Node): return True return False From 4ab415e37d9adac06e57d7a3b534dc5b17b6d469 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 28 May 2020 16:12:11 -0700 Subject: [PATCH 0293/1131] grpc: updated node events to contain geo data when present --- daemon/core/api/grpc/events.py | 2 ++ daemon/tests/test_grpc.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index a53ad971..837860e3 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -23,11 +23,13 @@ def handle_node_event(event: NodeData) -> core_pb2.NodeEvent: :return: node event that contains node id, name, model, position, and services """ position = core_pb2.Position(x=event.x_position, y=event.y_position) + geo = core_pb2.Geo(lat=event.latitude, lon=event.longitude, alt=event.altitude) node_proto = core_pb2.Node( id=event.id, name=event.name, model=event.model, position=position, + geo=geo, services=event.services, ) return core_pb2.NodeEvent(node=node_proto, source=event.source) diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 5e55f346..47cfe744 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -1002,11 +1002,18 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() node = session.add_node(CoreNode) + node.position.lat = 10.0 + node.position.lon = 20.0 + node.position.alt = 5.0 queue = Queue() def handle_event(event_data): assert event_data.session_id == session.id assert event_data.HasField("node_event") + event_node = event_data.node_event.node + assert event_node.geo.lat == node.position.lat + assert event_node.geo.lon == node.position.lon + assert event_node.geo.alt == node.position.alt queue.put(event_data) # then From 183ffda570331a6b1bd55c04edf49dc889700e4d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 29 May 2020 11:48:00 -0700 Subject: [PATCH 0294/1131] daemon: changes to support providing emane wireless links in all_link_data, which makes it accessible over grpc --- daemon/core/emane/emanemanager.py | 35 ++++++++++++++++++++++++++++--- daemon/core/emane/linkmonitor.py | 25 +++++----------------- daemon/core/emane/nodes.py | 32 +++++++++++++++++++++++++++- 3 files changed, 68 insertions(+), 24 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 16680e0e..d5c787f5 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -6,7 +6,7 @@ import logging import os import threading from collections import OrderedDict -from typing import TYPE_CHECKING, Dict, List, Set, Tuple, Type +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type from core import utils from core.config import ConfigGroup, Configuration, ModelManager @@ -19,7 +19,13 @@ from core.emane.linkmonitor import EmaneLinkMonitor from core.emane.nodes import EmaneNet from core.emane.rfpipe import EmaneRfPipeModel from core.emane.tdma import EmaneTdmaModel -from core.emulator.enumerations import ConfigDataTypes, RegisterTlvs +from core.emulator.data import LinkData +from core.emulator.enumerations import ( + ConfigDataTypes, + LinkTypes, + MessageFlags, + RegisterTlvs, +) from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNode, NodeBase from core.nodes.interface import CoreInterface @@ -458,7 +464,7 @@ class EmaneManager(ModelManager): model_class = self.models[model_name] emane_node.setmodel(model_class, config) - def nemlookup(self, nemid) -> Tuple[EmaneNet, CoreInterface]: + def nemlookup(self, nemid) -> Tuple[Optional[EmaneNet], Optional[CoreInterface]]: """ Look for the given numerical NEM ID and return the first matching EMANE network and NEM interface. @@ -476,6 +482,29 @@ class EmaneManager(ModelManager): return emane_node, netif + def get_nem_link( + self, nem1: int, nem2: int, flags: MessageFlags = MessageFlags.NONE + ) -> Optional[LinkData]: + emane1, netif = self.nemlookup(nem1) + if not emane1 or not netif: + logging.error("invalid nem: %s", nem1) + return None + node1 = netif.node + emane2, netif = self.nemlookup(nem2) + if not emane2 or not netif: + logging.error("invalid nem: %s", nem2) + return None + node2 = netif.node + color = self.session.get_link_color(emane1.id) + return LinkData( + message_type=flags, + node1_id=node1.id, + node2_id=node2.id, + network_id=emane1.id, + link_type=LinkTypes.WIRELESS, + color=color, + ) + def numnems(self) -> int: """ Return the number of NEMs emulated locally. diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py index 7eb903fd..861c108c 100644 --- a/daemon/core/emane/linkmonitor.py +++ b/daemon/core/emane/linkmonitor.py @@ -285,26 +285,11 @@ class EmaneLinkMonitor: def send_link(self, message_type: MessageFlags, link_id: Tuple[int, int]) -> None: nem_one, nem_two = link_id - emane_one, netif = self.emane_manager.nemlookup(nem_one) - if not emane_one or not netif: - logging.error("invalid nem: %s", nem_one) - return - node_one = netif.node - emane_two, netif = self.emane_manager.nemlookup(nem_two) - if not emane_two or not netif: - logging.error("invalid nem: %s", nem_two) - return - node_two = netif.node - logging.debug( - "%s emane link from %s(%s) to %s(%s)", - message_type.name, - node_one.name, - nem_one, - node_two.name, - nem_two, - ) - label = self.get_link_label(link_id) - self.send_message(message_type, label, node_one.id, node_two.id, emane_one.id) + link = self.emane_manager.get_nem_link(nem_one, nem_two, message_type) + if link: + label = self.get_link_label(link_id) + link.label = label + self.emane_manager.session.broadcast_link(link) def send_message( self, diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index d5f243cb..3032cda7 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -6,8 +6,9 @@ share the same MAC+PHY model. import logging from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type +from core.emulator.data import LinkData from core.emulator.distributed import DistributedServer -from core.emulator.enumerations import LinkTypes, NodeTypes, RegisterTlvs +from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes, RegisterTlvs from core.nodes.base import CoreNetworkBase from core.nodes.interface import CoreInterface @@ -236,3 +237,32 @@ class EmaneNet(CoreNetworkBase): nemid, lon, lat, alt = position event.append(nemid, latitude=lat, longitude=lon, altitude=alt) self.session.emane.service.publish(0, event) + + def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: + logging.info("gathering emane links: %s", self.id) + links = super().all_link_data(flags) + # gather current emane links + nem_ids = set(self.nemidmap.values()) + logging.info("known nems: %s", nem_ids) + emane_manager = self.session.emane + emane_links = emane_manager.link_monitor.links + considered = set() + for link_key in emane_links: + considered_key = tuple(sorted(link_key)) + if considered_key in considered: + continue + considered.add(considered_key) + logging.info("considering emane link: %s", considered_key) + nem1, nem2 = considered_key + # ignore links not related to this node + if nem1 not in nem_ids and nem2 not in nem_ids: + logging.info("ignore emane link not within network: %s", (nem1, nem2)) + continue + # ignore incomplete links + if (nem2, nem1) not in emane_links: + logging.info("ignore emane link not complete: %s", (nem1, nem2)) + continue + link = emane_manager.get_nem_link(nem1, nem2) + if link: + links.append(link) + return links From b88df84d6210886dfc4e0c98dc08cd448133ba28 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 29 May 2020 14:42:38 -0700 Subject: [PATCH 0295/1131] removed logs from emane network all_link_data --- daemon/core/emane/nodes.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 3032cda7..d8a58806 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -239,11 +239,9 @@ class EmaneNet(CoreNetworkBase): self.session.emane.service.publish(0, event) def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: - logging.info("gathering emane links: %s", self.id) links = super().all_link_data(flags) # gather current emane links nem_ids = set(self.nemidmap.values()) - logging.info("known nems: %s", nem_ids) emane_manager = self.session.emane emane_links = emane_manager.link_monitor.links considered = set() @@ -252,15 +250,12 @@ class EmaneNet(CoreNetworkBase): if considered_key in considered: continue considered.add(considered_key) - logging.info("considering emane link: %s", considered_key) nem1, nem2 = considered_key # ignore links not related to this node if nem1 not in nem_ids and nem2 not in nem_ids: - logging.info("ignore emane link not within network: %s", (nem1, nem2)) continue # ignore incomplete links if (nem2, nem1) not in emane_links: - logging.info("ignore emane link not complete: %s", (nem1, nem2)) continue link = emane_manager.get_nem_link(nem1, nem2) if link: From 10fd844397a827b2f0ce7ca618956d0f5e6bad05 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 29 May 2020 21:41:58 -0700 Subject: [PATCH 0296/1131] further type hinting for tests --- daemon/tests/emane/test_emane.py | 11 ++++++++--- daemon/tests/test_core.py | 7 +++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py index b3499337..328aa94b 100644 --- a/daemon/tests/emane/test_emane.py +++ b/daemon/tests/emane/test_emane.py @@ -2,17 +2,20 @@ Unit tests for testing CORE EMANE networks. """ import os +from tempfile import TemporaryFile from xml.etree import ElementTree import pytest from core.emane.bypass import EmaneBypassModel from core.emane.commeffect import EmaneCommEffectModel +from core.emane.emanemodel import EmaneModel from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emane.nodes import EmaneNet from core.emane.rfpipe import EmaneRfPipeModel from core.emane.tdma import EmaneTdmaModel -from core.emulator.emudata import NodeOptions +from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.session import Session from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNode @@ -38,7 +41,7 @@ def ping(from_node, to_node, ip_prefixes, count=3): class TestEmane: @pytest.mark.parametrize("model", _EMANE_MODELS) - def test_models(self, session, model, ip_prefixes): + def test_models(self, session: Session, model: EmaneModel, ip_prefixes: IpPrefixes): """ Test emane models within a basic network. @@ -81,7 +84,9 @@ class TestEmane: status = ping(node_one, node_two, ip_prefixes, count=5) assert not status - def test_xml_emane(self, session, tmpdir, ip_prefixes): + def test_xml_emane( + self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes + ): """ Test xml client methods for emane. diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index b9e0c1df..1c40393e 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -4,6 +4,7 @@ Unit tests for testing basic CORE networks. import os import threading +from typing import Type import pytest @@ -12,7 +13,7 @@ from core.emulator.enumerations import MessageFlags from core.emulator.session import Session from core.errors import CoreCommandError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility -from core.nodes.base import CoreNode +from core.nodes.base import CoreNode, NodeBase from core.nodes.network import HubNode, PtpNet, SwitchNode, WlanNode _PATH = os.path.abspath(os.path.dirname(__file__)) @@ -32,7 +33,9 @@ def ping(from_node, to_node, ip_prefixes): class TestCore: @pytest.mark.parametrize("net_type", _WIRED) - def test_wired_ping(self, session, net_type, ip_prefixes): + def test_wired_ping( + self, session: Session, net_type: Type[NodeBase], ip_prefixes: IpPrefixes + ): """ Test ptp node network. From 8ad3f7961ab3331797dbeba6d35e8336e2e5847b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 29 May 2020 22:07:50 -0700 Subject: [PATCH 0297/1131] renamed netclient create_interface to set_interface_master to better describe its purpose --- daemon/core/emulator/distributed.py | 4 ++-- daemon/core/nodes/netclient.py | 4 ++-- daemon/core/nodes/network.py | 7 +++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index 4e7fcdde..5f188cb0 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -212,7 +212,7 @@ class DistributedController: "local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key ) local_tap = GreTap(session=self.session, remoteip=host, key=key) - local_tap.net_client.create_interface(node.brname, local_tap.localname) + local_tap.net_client.set_interface_master(node.brname, local_tap.localname) # server to local logging.info( @@ -221,7 +221,7 @@ class DistributedController: remote_tap = GreTap( session=self.session, remoteip=self.address, key=key, server=server ) - remote_tap.net_client.create_interface(node.brname, remote_tap.localname) + remote_tap.net_client.set_interface_master(node.brname, remote_tap.localname) # save tunnels for shutdown tunnel = (local_tap, remote_tap) diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 51ac075e..091938de 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -251,7 +251,7 @@ class LinuxNetClient: self.device_down(name) self.run(f"{IP_BIN} link delete {name} type bridge") - def create_interface(self, bridge_name: str, interface_name: str) -> None: + def set_interface_master(self, bridge_name: str, interface_name: str) -> None: """ Assign interface master to a Linux bridge. @@ -330,7 +330,7 @@ class OvsNetClient(LinuxNetClient): self.device_down(name) self.run(f"{OVS_BIN} del-br {name}") - def create_interface(self, bridge_name: str, interface_name: str) -> None: + def set_interface_master(self, bridge_name: str, interface_name: str) -> None: """ Create an interface associated with a network bridge. diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 973346ad..f0b2fefa 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -360,7 +360,7 @@ class CoreNetwork(CoreNetworkBase): :return: nothing """ if self.up: - netif.net_client.create_interface(self.brname, netif.localname) + netif.net_client.set_interface_master(self.brname, netif.localname) super().attach(netif) def detach(self, netif: CoreInterface) -> None: @@ -557,8 +557,7 @@ class CoreNetwork(CoreNetworkBase): netif = Veth(self.session, None, name, localname, start=self.up) self.attach(netif) if net.up and net.brname: - # this is similar to net.attach() but uses netif.name instead of localname - netif.net_client.create_interface(net.brname, netif.name) + netif.net_client.set_interface_master(net.brname, netif.name) i = net.newifindex() net._netif[i] = netif with net._linked_lock: @@ -807,7 +806,7 @@ class CtrlNet(CoreNetwork): self.host_cmd(f"{self.updown_script} {self.brname} startup") if self.serverintf: - self.net_client.create_interface(self.brname, self.serverintf) + self.net_client.set_interface_master(self.brname, self.serverintf) def shutdown(self) -> None: """ From b034ba6cc3e9b43096bfaa37c50c565280d7b13a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 29 May 2020 22:48:00 -0700 Subject: [PATCH 0298/1131] turned transport type usages of raw and virtual across the board to an enumerated type --- daemon/core/emane/commeffect.py | 7 +++-- daemon/core/emane/emanemanager.py | 5 +-- daemon/core/emane/emanemodel.py | 8 ++--- daemon/core/emane/nodes.py | 10 ++++-- daemon/core/emulator/enumerations.py | 5 +++ daemon/core/nodes/interface.py | 46 +++++++++++++--------------- daemon/core/nodes/physical.py | 4 +-- daemon/core/xml/emanexml.py | 25 +++++++-------- 8 files changed, 61 insertions(+), 49 deletions(-) diff --git a/daemon/core/emane/commeffect.py b/daemon/core/emane/commeffect.py index f98f2454..99fdb9b1 100644 --- a/daemon/core/emane/commeffect.py +++ b/daemon/core/emane/commeffect.py @@ -11,6 +11,7 @@ from lxml import etree from core.config import ConfigGroup, Configuration from core.emane import emanemanifest, emanemodel from core.emane.nodes import EmaneNet +from core.emulator.enumerations import TransportType from core.nodes.interface import CoreInterface from core.xml import emanexml @@ -79,9 +80,9 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): # create and write nem document nem_element = etree.Element("nem", name=f"{self.name} NEM", type="unstructured") - transport_type = "virtual" - if interface and interface.transport_type == "raw": - transport_type = "raw" + transport_type = TransportType.VIRTUAL + if interface and interface.transport_type == TransportType.RAW: + transport_type = TransportType.RAW transport_file = emanexml.transport_file_name(self.id, transport_type) etree.SubElement(nem_element, "transport", definition=transport_file) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index d5c787f5..438fde00 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -30,6 +30,7 @@ from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNode, NodeBase from core.nodes.interface import CoreInterface from core.nodes.network import CtrlNet +from core.nodes.physical import Rj45Node from core.xml import emanexml if TYPE_CHECKING: @@ -596,7 +597,7 @@ class EmaneManager(ModelManager): run_emane_on_host = False for node in self.getnodes(): - if hasattr(node, "transport_type") and node.transport_type == "raw": + if isinstance(node, Rj45Node): run_emane_on_host = True continue path = self.session.session_dir @@ -655,7 +656,7 @@ class EmaneManager(ModelManager): kill_transortd = "killall -q emanetransportd" stop_emane_on_host = False for node in self.getnodes(): - if hasattr(node, "transport_type") and node.transport_type == "raw": + if isinstance(node, Rj45Node): stop_emane_on_host = True continue diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index 4104d3d5..3a21643b 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -8,7 +8,7 @@ from typing import Dict, List from core.config import ConfigGroup, Configuration from core.emane import emanemanifest from core.emane.nodes import EmaneNet -from core.emulator.enumerations import ConfigDataTypes +from core.emulator.enumerations import ConfigDataTypes, TransportType from core.errors import CoreError from core.location.mobility import WirelessModel from core.nodes.interface import CoreInterface @@ -111,9 +111,9 @@ class EmaneModel(WirelessModel): server = interface.node.server # check if this is external - transport_type = "virtual" - if interface and interface.transport_type == "raw": - transport_type = "raw" + transport_type = TransportType.VIRTUAL + if interface and interface.transport_type == TransportType.RAW: + transport_type = TransportType.RAW transport_name = emanexml.transport_file_name(self.id, transport_type) # create nem xml file diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index d8a58806..be398329 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -8,7 +8,13 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type from core.emulator.data import LinkData from core.emulator.distributed import DistributedServer -from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes, RegisterTlvs +from core.emulator.enumerations import ( + LinkTypes, + MessageFlags, + NodeTypes, + RegisterTlvs, + TransportType, +) from core.nodes.base import CoreNetworkBase from core.nodes.interface import CoreInterface @@ -173,7 +179,7 @@ class EmaneNet(CoreNetworkBase): emanetransportd terminates. """ for netif in self.netifs(): - if "virtual" in netif.transport_type.lower(): + if netif.transport_type == TransportType.VIRTUAL: netif.shutdown() netif.poshook = None diff --git a/daemon/core/emulator/enumerations.py b/daemon/core/emulator/enumerations.py index 2c6e14db..f210c992 100644 --- a/daemon/core/emulator/enumerations.py +++ b/daemon/core/emulator/enumerations.py @@ -117,3 +117,8 @@ class ExceptionLevels(Enum): ERROR = 2 WARNING = 3 NOTICE = 4 + + +class TransportType(Enum): + RAW = "raw" + VIRTUAL = "virtual" diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 97b494b7..16c242e9 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -4,12 +4,12 @@ virtual ethernet classes that implement the interfaces available under Linux. import logging import time -from typing import TYPE_CHECKING, Callable, Dict, List, Tuple +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple from core import utils -from core.emulator.enumerations import MessageFlags +from core.emulator.enumerations import MessageFlags, TransportType from core.errors import CoreCommandError -from core.nodes.netclient import get_net_client +from core.nodes.netclient import LinuxNetClient, get_net_client if TYPE_CHECKING: from core.emulator.distributed import DistributedServer @@ -42,32 +42,30 @@ class CoreInterface: :param server: remote server node will run on, default is None for localhost """ - self.session = session - self.node = node - self.name = name - self.localname = localname - self.up = False - if not isinstance(mtu, int): - raise ValueError - self.mtu = mtu - self.net = None - self.othernet = None + self.session: "Session" = session + self.node: "CoreNode" = node + self.name: str = name + self.localname: str = localname + self.up: bool = False + self.mtu: int = mtu + self.net: Optional[CoreNetworkBase] = None + self.othernet: Optional[CoreNetworkBase] = None self._params = {} - self.addrlist = [] - self.hwaddr = None + self.addrlist: List[str] = [] + self.hwaddr: Optional[str] = None # placeholder position hook - self.poshook = lambda x: None + self.poshook: Callable[[CoreInterface], None] = lambda x: None # used with EMANE - self.transport_type = None + self.transport_type: Optional[TransportType] = None # node interface index - self.netindex = None + self.netindex: Optional[int] = None # net interface index - self.netifi = None + self.netifi: Optional[int] = None # index used to find flow data - self.flow_id = None - self.server = server + self.flow_id: Optional[int] = None + self.server: Optional["DistributedServer"] = server use_ovs = session.options.get_config("ovs") == "True" - self.net_client = get_net_client(use_ovs, self.host_cmd) + self.net_client: LinuxNetClient = get_net_client(use_ovs, self.host_cmd) def host_cmd( self, @@ -330,7 +328,7 @@ class TunTap(CoreInterface): :param start: start flag """ super().__init__(session, node, name, localname, mtu, server) - self.transport_type = "virtual" + self.transport_type = TransportType.VIRTUAL if start: self.startup() @@ -516,7 +514,7 @@ class GreTap(CoreInterface): sessionid = session.short_session_id() localname = f"gt.{self.id}.{sessionid}" super().__init__(session, node, name, localname, mtu, server) - self.transport_type = "raw" + self.transport_type = TransportType.RAW if not start: return if remoteip is None: diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 2fc743fa..e5db8a80 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -10,7 +10,7 @@ from typing import IO, TYPE_CHECKING, List, Optional, Tuple from core import utils from core.constants import MOUNT_BIN, UMOUNT_BIN from core.emulator.distributed import DistributedServer -from core.emulator.enumerations import NodeTypes +from core.emulator.enumerations import NodeTypes, TransportType from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNetworkBase, CoreNodeBase from core.nodes.interface import CoreInterface, Veth @@ -281,7 +281,7 @@ class Rj45Node(CoreNodeBase): """ super().__init__(session, _id, name, start, server) self.interface = CoreInterface(session, self, name, name, mtu, server) - self.interface.transport_type = "raw" + self.interface.transport_type = TransportType.RAW self.lock: threading.RLock = threading.RLock() self.ifindex: Optional[int] = None self.old_up: bool = False diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index 2c5cc9c0..2589edd9 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -9,6 +9,7 @@ from core import utils from core.config import Configuration from core.emane.nodes import EmaneNet from core.emulator.distributed import DistributedServer +from core.emulator.enumerations import TransportType from core.nodes.interface import CoreInterface from core.nodes.network import CtrlNet from core.xml import corexml @@ -182,7 +183,7 @@ def build_node_platform_xml( transport_type = netif.transport_type if not transport_type: logging.info("warning: %s interface type unsupported!", netif.name) - transport_type = "raw" + transport_type = TransportType.RAW transport_file = transport_file_name(node.id, transport_type) transport_element = etree.SubElement( nem_element, "transport", definition=transport_file @@ -196,7 +197,7 @@ def build_node_platform_xml( # merging code key = netif.node.id - if netif.transport_type == "raw": + if netif.transport_type == TransportType.RAW: key = "host" otadev = control_net.brname eventdev = control_net.brname @@ -276,8 +277,8 @@ def build_xml_files(emane_manager: "EmaneManager", node: EmaneNet) -> None: # build XML for specific interface (NEM) configs need_virtual = False need_raw = False - vtype = "virtual" - rtype = "raw" + vtype = TransportType.VIRTUAL + rtype = TransportType.RAW for netif in node.netifs(): # check for interface specific emane configuration and write xml files @@ -286,7 +287,7 @@ def build_xml_files(emane_manager: "EmaneManager", node: EmaneNet) -> None: node.model.build_xml_files(config, netif) # check transport type needed for interface - if "virtual" in netif.transport_type: + if netif.transport_type == TransportType.VIRTUAL: need_virtual = True vtype = netif.transport_type else: @@ -301,7 +302,7 @@ def build_xml_files(emane_manager: "EmaneManager", node: EmaneNet) -> None: def build_transport_xml( - emane_manager: "EmaneManager", node: EmaneNet, transport_type: str + emane_manager: "EmaneManager", node: EmaneNet, transport_type: TransportType ) -> None: """ Build transport xml file for node and transport type. @@ -314,8 +315,8 @@ def build_transport_xml( """ transport_element = etree.Element( "transport", - name=f"{transport_type.capitalize()} Transport", - library=f"trans{transport_type.lower()}", + name=f"{transport_type.value.capitalize()} Transport", + library=f"trans{transport_type.value.lower()}", ) # add bitrate @@ -325,7 +326,7 @@ def build_transport_xml( config = emane_manager.get_configs(node.id, node.model.name) flowcontrol = config.get("flowcontrolenable", "0") == "1" - if "virtual" in transport_type.lower(): + if transport_type == TransportType.VIRTUAL: device_path = "/dev/net/tun_flowctl" if not os.path.exists(device_path): device_path = "/dev/net/tun" @@ -482,7 +483,7 @@ def create_event_service_xml( create_file(event_element, "emaneeventmsgsvc", file_path, server) -def transport_file_name(node_id: int, transport_type: str) -> str: +def transport_file_name(node_id: int, transport_type: TransportType) -> str: """ Create name for a transport xml file. @@ -490,7 +491,7 @@ def transport_file_name(node_id: int, transport_type: str) -> str: :param transport_type: transport type to generate transport file :return: """ - return f"n{node_id}trans{transport_type}.xml" + return f"n{node_id}trans{transport_type.value}.xml" def _basename(emane_model: "EmaneModel", interface: CoreInterface = None) -> str: @@ -521,7 +522,7 @@ def nem_file_name(emane_model: "EmaneModel", interface: CoreInterface = None) -> """ basename = _basename(emane_model, interface) append = "" - if interface and interface.transport_type == "raw": + if interface and interface.transport_type == TransportType.RAW: append = "_raw" return f"{basename}nem{append}.xml" From 73a556708446ba51da4174f060417b2c177cfe1d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 29 May 2020 23:22:21 -0700 Subject: [PATCH 0299/1131] added network policy enum to avoid string usage --- daemon/core/emulator/enumerations.py | 5 ++++ daemon/core/nodes/network.py | 40 ++++++++++++++++------------ 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/daemon/core/emulator/enumerations.py b/daemon/core/emulator/enumerations.py index 2c6e14db..4c72d56e 100644 --- a/daemon/core/emulator/enumerations.py +++ b/daemon/core/emulator/enumerations.py @@ -117,3 +117,8 @@ class ExceptionLevels(Enum): ERROR = 2 WARNING = 3 NOTICE = 4 + + +class NetworkPolicy(Enum): + ACCEPT = "ACCEPT" + DROP = "DROP" diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index f0b2fefa..b08d87d4 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -12,7 +12,13 @@ import netaddr from core import utils from core.constants import EBTABLES_BIN, TC_BIN from core.emulator.data import LinkData, NodeData -from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes, RegisterTlvs +from core.emulator.enumerations import ( + LinkTypes, + MessageFlags, + NetworkPolicy, + NodeTypes, + RegisterTlvs, +) from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNetworkBase from core.nodes.interface import CoreInterface, GreTap, Veth @@ -204,21 +210,21 @@ class EbtablesQueue: wlan.has_ebtables_chain = True self.cmds.extend( [ - f"-N {wlan.brname} -P {wlan.policy}", + f"-N {wlan.brname} -P {wlan.policy.value}", f"-A FORWARD --logical-in {wlan.brname} -j {wlan.brname}", ] ) # rebuild the chain for netif1, v in wlan._linked.items(): for netif2, linked in v.items(): - if wlan.policy == "DROP" and linked: + if wlan.policy == NetworkPolicy.DROP and linked: self.cmds.extend( [ f"-A {wlan.brname} -i {netif1.localname} -o {netif2.localname} -j ACCEPT", f"-A {wlan.brname} -o {netif1.localname} -i {netif2.localname} -j ACCEPT", ] ) - elif wlan.policy == "ACCEPT" and not linked: + elif wlan.policy == NetworkPolicy.ACCEPT and not linked: self.cmds.extend( [ f"-A {wlan.brname} -i {netif1.localname} -o {netif2.localname} -j DROP", @@ -250,7 +256,7 @@ class CoreNetwork(CoreNetworkBase): Provides linux bridge network functionality for core nodes. """ - policy: str = "DROP" + policy: NetworkPolicy = NetworkPolicy.DROP def __init__( self, @@ -259,7 +265,7 @@ class CoreNetwork(CoreNetworkBase): name: str = None, start: bool = True, server: "DistributedServer" = None, - policy: str = None, + policy: NetworkPolicy = None, ) -> None: """ Creates a LxBrNet instance. @@ -392,12 +398,12 @@ class CoreNetwork(CoreNetworkBase): try: linked = self._linked[netif1][netif2] except KeyError: - if self.policy == "ACCEPT": + if self.policy == NetworkPolicy.ACCEPT: linked = True - elif self.policy == "DROP": + elif self.policy == NetworkPolicy.DROP: linked = False else: - raise Exception(f"unknown policy: {self.policy}") + raise Exception(f"unknown policy: {self.policy.value}") self._linked[netif1][netif2] = linked return linked @@ -605,7 +611,7 @@ class GreTapBridge(CoreNetwork): remoteip: str = None, _id: int = None, name: str = None, - policy: str = "ACCEPT", + policy: NetworkPolicy = NetworkPolicy.ACCEPT, localip: str = None, ttl: int = 255, key: int = None, @@ -712,7 +718,7 @@ class CtrlNet(CoreNetwork): Control network functionality. """ - policy: str = "ACCEPT" + policy: NetworkPolicy = NetworkPolicy.ACCEPT # base control interface index CTRLIF_IDX_BASE: int = 99 DEFAULT_PREFIX_LIST: List[str] = [ @@ -852,7 +858,7 @@ class PtpNet(CoreNetwork): Peer to peer network node. """ - policy: str = "ACCEPT" + policy: NetworkPolicy = NetworkPolicy.ACCEPT def attach(self, netif: CoreInterface) -> None: """ @@ -983,7 +989,7 @@ class SwitchNode(CoreNetwork): """ apitype: NodeTypes = NodeTypes.SWITCH - policy: str = "ACCEPT" + policy: NetworkPolicy = NetworkPolicy.ACCEPT type: str = "lanswitch" @@ -994,7 +1000,7 @@ class HubNode(CoreNetwork): """ apitype: NodeTypes = NodeTypes.HUB - policy: str = "ACCEPT" + policy: NetworkPolicy = NetworkPolicy.ACCEPT type: str = "hub" def startup(self) -> None: @@ -1014,7 +1020,7 @@ class WlanNode(CoreNetwork): apitype: NodeTypes = NodeTypes.WIRELESS_LAN linktype: LinkTypes = LinkTypes.WIRED - policy: str = "DROP" + policy: NetworkPolicy = NetworkPolicy.DROP type: str = "wlan" def __init__( @@ -1024,7 +1030,7 @@ class WlanNode(CoreNetwork): name: str = None, start: bool = True, server: "DistributedServer" = None, - policy: str = None, + policy: NetworkPolicy = None, ) -> None: """ Create a WlanNode instance. @@ -1117,5 +1123,5 @@ class TunnelNode(GreTapBridge): """ apitype: NodeTypes = NodeTypes.TUNNEL - policy: str = "ACCEPT" + policy: NetworkPolicy = NetworkPolicy.ACCEPT type: str = "tunnel" From c6a06baf295985dc729aff73c7f1af73d4553cf5 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 30 May 2020 14:24:38 -0700 Subject: [PATCH 0300/1131] add geo to grpc calls getting node values, updated emane position hook to set lon/lat/alt values --- daemon/core/api/grpc/grpcutils.py | 4 ++++ daemon/core/emane/nodes.py | 1 + 2 files changed, 5 insertions(+) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 6281ec67..8a69c40f 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -235,6 +235,9 @@ def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node: position = core_pb2.Position( x=node.position.x, y=node.position.y, z=node.position.z ) + geo = core_pb2.Geo( + lat=node.position.lat, lon=node.position.lon, alt=node.position.alt + ) services = getattr(node, "services", []) if services is None: services = [] @@ -255,6 +258,7 @@ def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node: model=model, type=node_type.value, position=position, + geo=geo, services=services, icon=node.icon, image=image, diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index be398329..bbe59b95 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -202,6 +202,7 @@ class EmaneNet(CoreNetworkBase): lat, lon, alt = self.session.location.getgeo(x, y, z) if node.position.alt is not None: alt = node.position.alt + node.position.set_geo(lon, lat, alt) # altitude must be an integer or warning is printed alt = int(round(alt)) return nemid, lon, lat, alt From e323f8965e2bca3f619e8977ba4c6d4170a4284c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 30 May 2020 21:36:44 -0700 Subject: [PATCH 0301/1131] removed docs link to example service and embedded example into docs --- docs/services.md | 120 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) diff --git a/docs/services.md b/docs/services.md index d2911d81..9f47ae48 100644 --- a/docs/services.md +++ b/docs/services.md @@ -145,7 +145,7 @@ ideas for a service before adding a new service type. ### Creating New Services -1. Modify the [Example Service File](../daemon/examples/myservices/sample.py) +1. Modify the example service shown below to do what you want. It could generate config/script files, mount per-node directories, start processes/scripts, etc. sample.py is a Python file that defines one or more classes to be imported. You can create multiple Python @@ -174,3 +174,121 @@ ideas for a service before adding a new service type. If you have created a new service type that may be useful to others, please consider contributing it to the CORE project. + +#### Example Custom Service + +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 +the **startup** commands would be supplied, which typically tends to be +running the shell files generated. + +```python +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 ifc in node.netifs(): + cfg += f'echo "Node {node.name} has interface {ifc.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 +``` From 0d2bcccf3e0077d31140bee7c0faa82444e4ee4f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 2 Jun 2020 14:48:57 -0700 Subject: [PATCH 0302/1131] added initial files to support transitioning to using poetry/invoke to provide an environment for core --- daemon/poetry.lock | 1061 +++++++++++++++++++++++++++++++++++++++++ daemon/pyproject.toml | 33 ++ tasks.py | 14 + 3 files changed, 1108 insertions(+) create mode 100644 daemon/poetry.lock create mode 100644 daemon/pyproject.toml create mode 100644 tasks.py diff --git a/daemon/poetry.lock b/daemon/poetry.lock new file mode 100644 index 00000000..c5e1ebb6 --- /dev/null +++ b/daemon/poetry.lock @@ -0,0 +1,1061 @@ +[[package]] +category = "dev" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +name = "appdirs" +optional = false +python-versions = "*" +version = "1.4.4" + +[[package]] +category = "dev" +description = "Atomic file writes." +marker = "sys_platform == \"win32\"" +name = "atomicwrites" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.4.0" + +[[package]] +category = "dev" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.3.0" + +[package.extras] +azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] +dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + +[[package]] +category = "main" +description = "Modern password hashing for your software and your servers" +name = "bcrypt" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.1.7" + +[package.dependencies] +cffi = ">=1.1" +six = ">=1.4.1" + +[package.extras] +tests = ["pytest (>=3.2.1,<3.3.0 || >3.3.0)"] + +[[package]] +category = "dev" +description = "The uncompromising code formatter." +name = "black" +optional = false +python-versions = ">=3.6" +version = "19.3b0" + +[package.dependencies] +appdirs = "*" +attrs = ">=18.1.0" +click = ">=6.5" +toml = ">=0.9.4" + +[package.extras] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] + +[[package]] +category = "main" +description = "Foreign Function Interface for Python calling C code." +name = "cffi" +optional = false +python-versions = "*" +version = "1.14.0" + +[package.dependencies] +pycparser = "*" + +[[package]] +category = "dev" +description = "Validate configuration and produce human readable error messages." +name = "cfgv" +optional = false +python-versions = ">=3.6" +version = "3.0.0" + +[[package]] +category = "dev" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.2" + +[[package]] +category = "dev" +description = "Cross-platform colored terminal text." +marker = "sys_platform == \"win32\"" +name = "colorama" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.3" + +[[package]] +category = "main" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +name = "cryptography" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +version = "2.9.2" + +[package.dependencies] +cffi = ">=1.8,<1.11.3 || >1.11.3" +six = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0)", "sphinx-rtd-theme"] +docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +idna = ["idna (>=2.1)"] +pep8test = ["flake8", "flake8-import-order", "pep8-naming"] +test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"] + +[[package]] +category = "main" +description = "A backport of the dataclasses module for Python 3.6" +marker = "python_version == \"3.6\"" +name = "dataclasses" +optional = false +python-versions = ">=3.6, <3.7" +version = "0.7" + +[[package]] +category = "dev" +description = "Distribution utilities" +name = "distlib" +optional = false +python-versions = "*" +version = "0.3.0" + +[[package]] +category = "main" +description = "High level SSH command execution" +name = "fabric" +optional = false +python-versions = "*" +version = "2.5.0" + +[package.dependencies] +invoke = ">=1.3,<2.0" +paramiko = ">=2.4" + +[package.extras] +pytest = ["mock (>=2.0.0,<3.0)", "pytest (>=3.2.5,<4.0)"] +testing = ["mock (>=2.0.0,<3.0)"] + +[[package]] +category = "dev" +description = "A platform independent file lock." +name = "filelock" +optional = false +python-versions = "*" +version = "3.0.12" + +[[package]] +category = "dev" +description = "the modular source code checker: pep8 pyflakes and co" +name = "flake8" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "3.8.2" + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.6.0a1,<2.7.0" +pyflakes = ">=2.2.0,<2.3.0" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = "*" + +[[package]] +category = "main" +description = "HTTP/2-based RPC framework" +name = "grpcio" +optional = false +python-versions = "*" +version = "1.29.0" + +[package.dependencies] +six = ">=1.5.2" + +[[package]] +category = "dev" +description = "Protobuf code generator for gRPC" +name = "grpcio-tools" +optional = false +python-versions = "*" +version = "1.29.0" + +[package.dependencies] +grpcio = ">=1.29.0" +protobuf = ">=3.5.0.post1" + +[[package]] +category = "dev" +description = "File identification library for Python" +name = "identify" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "1.4.18" + +[package.extras] +license = ["editdistance"] + +[[package]] +category = "dev" +description = "Read metadata from Python packages" +marker = "python_version < \"3.8\"" +name = "importlib-metadata" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.6.0" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "importlib-resources"] + +[[package]] +category = "dev" +description = "Read resources from Python packages" +marker = "python_version < \"3.7\"" +name = "importlib-resources" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.5.0" + +[package.dependencies] +[package.dependencies.importlib-metadata] +python = "<3.8" +version = "*" + +[package.dependencies.zipp] +python = "<3.8" +version = ">=0.4" + +[package.extras] +docs = ["sphinx", "rst.linker", "jaraco.packaging"] + +[[package]] +category = "main" +description = "Pythonic task execution" +name = "invoke" +optional = false +python-versions = "*" +version = "1.4.1" + +[[package]] +category = "dev" +description = "A Python utility / library to sort Python imports." +name = "isort" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "4.3.21" + +[package.extras] +pipfile = ["pipreqs", "requirementslib"] +pyproject = ["toml"] +requirements = ["pipreqs", "pip-api"] +xdg_home = ["appdirs (>=1.4.0)"] + +[[package]] +category = "main" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +name = "lxml" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +version = "4.5.1" + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["beautifulsoup4"] +source = ["Cython (>=0.29.7)"] + +[[package]] +category = "main" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +name = "mako" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.1.3" + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["babel"] +lingua = ["lingua"] + +[[package]] +category = "main" +description = "Safely add untrusted strings to HTML/XML markup." +name = "markupsafe" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.1.1" + +[[package]] +category = "dev" +description = "McCabe checker, plugin for flake8" +name = "mccabe" +optional = false +python-versions = "*" +version = "0.6.1" + +[[package]] +category = "dev" +description = "Rolling backport of unittest.mock for all Pythons" +name = "mock" +optional = false +python-versions = ">=3.6" +version = "4.0.2" + +[package.extras] +build = ["twine", "wheel", "blurb"] +docs = ["sphinx"] +test = ["pytest", "pytest-cov"] + +[[package]] +category = "dev" +description = "More routines for operating on iterables, beyond itertools" +name = "more-itertools" +optional = false +python-versions = ">=3.5" +version = "8.3.0" + +[[package]] +category = "main" +description = "A network address manipulation library for Python" +name = "netaddr" +optional = false +python-versions = "*" +version = "0.7.19" + +[[package]] +category = "dev" +description = "Node.js virtual environment builder" +name = "nodeenv" +optional = false +python-versions = "*" +version = "1.4.0" + +[[package]] +category = "dev" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "20.4" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + +[[package]] +category = "main" +description = "SSH2 protocol library" +name = "paramiko" +optional = false +python-versions = "*" +version = "2.7.1" + +[package.dependencies] +bcrypt = ">=3.1.3" +cryptography = ">=2.5" +pynacl = ">=1.0.1" + +[package.extras] +all = ["pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "bcrypt (>=3.1.3)", "invoke (>=1.3)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +ed25519 = ["pynacl (>=1.0.1)", "bcrypt (>=3.1.3)"] +gssapi = ["pyasn1 (>=0.1.7)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +invoke = ["invoke (>=1.3)"] + +[[package]] +category = "main" +description = "Python Imaging Library (Fork)" +name = "pillow" +optional = false +python-versions = ">=3.5" +version = "7.1.2" + +[[package]] +category = "dev" +description = "plugin and hook calling mechanisms for python" +name = "pluggy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.13.1" + +[package.dependencies] +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +category = "dev" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +name = "pre-commit" +optional = false +python-versions = ">=3.6" +version = "2.1.1" + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +toml = "*" +virtualenv = ">=15.2" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = "*" + +[package.dependencies.importlib-resources] +python = "<3.7" +version = "*" + +[[package]] +category = "main" +description = "Protocol Buffers" +name = "protobuf" +optional = false +python-versions = "*" +version = "3.12.2" + +[package.dependencies] +setuptools = "*" +six = ">=1.9" + +[[package]] +category = "dev" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +name = "py" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.8.1" + +[[package]] +category = "dev" +description = "Python style guide checker" +name = "pycodestyle" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.6.0" + +[[package]] +category = "main" +description = "C parser in Python" +name = "pycparser" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.20" + +[[package]] +category = "dev" +description = "passive checker of Python programs" +name = "pyflakes" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.2.0" + +[[package]] +category = "main" +description = "Python binding to the Networking and Cryptography (NaCl) library" +name = "pynacl" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.4.0" + +[package.dependencies] +cffi = ">=1.4.1" +six = "*" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["pytest (>=3.2.1,<3.3.0 || >3.3.0)", "hypothesis (>=3.27.0)"] + +[[package]] +category = "dev" +description = "Python parsing module" +name = "pyparsing" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.4.7" + +[[package]] +category = "main" +description = "Python interface to PROJ (cartographic projections and coordinate transformations library)" +name = "pyproj" +optional = false +python-versions = ">=3.5" +version = "2.6.1.post1" + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +name = "pytest" +optional = false +python-versions = ">=3.5" +version = "5.4.3" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +colorama = "*" +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +wcwidth = "*" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +checkqa-mypy = ["mypy (v0.761)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +category = "main" +description = "YAML parser and emitter for Python" +name = "pyyaml" +optional = false +python-versions = "*" +version = "5.3.1" + +[[package]] +category = "main" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.15.0" + +[[package]] +category = "dev" +description = "Python Library for Tom's Obvious, Minimal Language" +name = "toml" +optional = false +python-versions = "*" +version = "0.10.1" + +[[package]] +category = "dev" +description = "Virtual Python Environment builder" +name = "virtualenv" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "20.0.21" + +[package.dependencies] +appdirs = ">=1.4.3,<2" +distlib = ">=0.3.0,<1" +filelock = ">=3.0.0,<4" +six = ">=1.9.0,<2" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12,<2" + +[package.dependencies.importlib-resources] +python = "<3.7" +version = ">=1.0,<2" + +[package.extras] +docs = ["sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)", "proselint (>=0.10.2)"] +testing = ["pytest (>=4)", "coverage (>=5)", "coverage-enable-subprocess (>=1)", "pytest-xdist (>=1.31.0)", "pytest-mock (>=2)", "pytest-env (>=0.6.2)", "pytest-randomly (>=1)", "pytest-timeout", "packaging (>=20.0)", "xonsh (>=0.9.16)"] + +[[package]] +category = "dev" +description = "Measures the displayed width of unicode strings in a terminal" +name = "wcwidth" +optional = false +python-versions = "*" +version = "0.2.3" + +[[package]] +category = "dev" +description = "Backport of pathlib-compatible object wrapper for zip files" +marker = "python_version < \"3.8\"" +name = "zipp" +optional = false +python-versions = ">=3.6" +version = "3.1.0" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["jaraco.itertools", "func-timeout"] + +[metadata] +content-hash = "ff2407f8ca447047101b8e0c8656027d07d2f15e51b3a950f2c2d789f929da6b" +python-versions = "^3.6" + +[metadata.files] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, + {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, +] +bcrypt = [ + {file = "bcrypt-3.1.7-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7"}, + {file = "bcrypt-3.1.7-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31"}, + {file = "bcrypt-3.1.7-cp27-cp27m-win32.whl", hash = "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161"}, + {file = "bcrypt-3.1.7-cp27-cp27m-win_amd64.whl", hash = "sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e"}, + {file = "bcrypt-3.1.7-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0"}, + {file = "bcrypt-3.1.7-cp34-abi3-macosx_10_6_intel.whl", hash = "sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052"}, + {file = "bcrypt-3.1.7-cp34-abi3-manylinux1_x86_64.whl", hash = "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105"}, + {file = "bcrypt-3.1.7-cp34-cp34m-win32.whl", hash = "sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de"}, + {file = "bcrypt-3.1.7-cp34-cp34m-win_amd64.whl", hash = "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133"}, + {file = "bcrypt-3.1.7-cp35-cp35m-win32.whl", hash = "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5"}, + {file = "bcrypt-3.1.7-cp35-cp35m-win_amd64.whl", hash = "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09"}, + {file = "bcrypt-3.1.7-cp36-cp36m-win32.whl", hash = "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c"}, + {file = "bcrypt-3.1.7-cp36-cp36m-win_amd64.whl", hash = "sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89"}, + {file = "bcrypt-3.1.7-cp37-cp37m-win32.whl", hash = "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294"}, + {file = "bcrypt-3.1.7-cp37-cp37m-win_amd64.whl", hash = "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc"}, + {file = "bcrypt-3.1.7-cp38-cp38-win32.whl", hash = "sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1"}, + {file = "bcrypt-3.1.7-cp38-cp38-win_amd64.whl", hash = "sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752"}, + {file = "bcrypt-3.1.7.tar.gz", hash = "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42"}, +] +black = [ + {file = "black-19.3b0-py36-none-any.whl", hash = "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf"}, + {file = "black-19.3b0.tar.gz", hash = "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"}, +] +cffi = [ + {file = "cffi-1.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384"}, + {file = "cffi-1.14.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30"}, + {file = "cffi-1.14.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"}, + {file = "cffi-1.14.0-cp27-cp27m-win32.whl", hash = "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78"}, + {file = "cffi-1.14.0-cp27-cp27m-win_amd64.whl", hash = "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793"}, + {file = "cffi-1.14.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e"}, + {file = "cffi-1.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a"}, + {file = "cffi-1.14.0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff"}, + {file = "cffi-1.14.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f"}, + {file = "cffi-1.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa"}, + {file = "cffi-1.14.0-cp35-cp35m-win32.whl", hash = "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5"}, + {file = "cffi-1.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4"}, + {file = "cffi-1.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d"}, + {file = "cffi-1.14.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc"}, + {file = "cffi-1.14.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac"}, + {file = "cffi-1.14.0-cp36-cp36m-win32.whl", hash = "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f"}, + {file = "cffi-1.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b"}, + {file = "cffi-1.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3"}, + {file = "cffi-1.14.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66"}, + {file = "cffi-1.14.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0"}, + {file = "cffi-1.14.0-cp37-cp37m-win32.whl", hash = "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f"}, + {file = "cffi-1.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26"}, + {file = "cffi-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd"}, + {file = "cffi-1.14.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55"}, + {file = "cffi-1.14.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2"}, + {file = "cffi-1.14.0-cp38-cp38-win32.whl", hash = "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8"}, + {file = "cffi-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b"}, + {file = "cffi-1.14.0.tar.gz", hash = "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6"}, +] +cfgv = [ + {file = "cfgv-3.0.0-py2.py3-none-any.whl", hash = "sha256:f22b426ed59cd2ab2b54ff96608d846c33dfb8766a67f0b4a6ce130ce244414f"}, + {file = "cfgv-3.0.0.tar.gz", hash = "sha256:04b093b14ddf9fd4d17c53ebfd55582d27b76ed30050193c14e560770c5360eb"}, +] +click = [ + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] +colorama = [ + {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, + {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, +] +cryptography = [ + {file = "cryptography-2.9.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e"}, + {file = "cryptography-2.9.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b"}, + {file = "cryptography-2.9.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:c447cf087cf2dbddc1add6987bbe2f767ed5317adb2d08af940db517dd704365"}, + {file = "cryptography-2.9.2-cp27-cp27m-win32.whl", hash = "sha256:f118a95c7480f5be0df8afeb9a11bd199aa20afab7a96bcf20409b411a3a85f0"}, + {file = "cryptography-2.9.2-cp27-cp27m-win_amd64.whl", hash = "sha256:c4fd17d92e9d55b84707f4fd09992081ba872d1a0c610c109c18e062e06a2e55"}, + {file = "cryptography-2.9.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d0d5aeaedd29be304848f1c5059074a740fa9f6f26b84c5b63e8b29e73dfc270"}, + {file = "cryptography-2.9.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e4014639d3d73fbc5ceff206049c5a9a849cefd106a49fa7aaaa25cc0ce35cf"}, + {file = "cryptography-2.9.2-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:96c080ae7118c10fcbe6229ab43eb8b090fccd31a09ef55f83f690d1ef619a1d"}, + {file = "cryptography-2.9.2-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:e993468c859d084d5579e2ebee101de8f5a27ce8e2159959b6673b418fd8c785"}, + {file = "cryptography-2.9.2-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:88c881dd5a147e08d1bdcf2315c04972381d026cdb803325c03fe2b4a8ed858b"}, + {file = "cryptography-2.9.2-cp35-cp35m-win32.whl", hash = "sha256:651448cd2e3a6bc2bb76c3663785133c40d5e1a8c1a9c5429e4354201c6024ae"}, + {file = "cryptography-2.9.2-cp35-cp35m-win_amd64.whl", hash = "sha256:726086c17f94747cedbee6efa77e99ae170caebeb1116353c6cf0ab67ea6829b"}, + {file = "cryptography-2.9.2-cp36-cp36m-win32.whl", hash = "sha256:091d31c42f444c6f519485ed528d8b451d1a0c7bf30e8ca583a0cac44b8a0df6"}, + {file = "cryptography-2.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:bb1f0281887d89617b4c68e8db9a2c42b9efebf2702a3c5bf70599421a8623e3"}, + {file = "cryptography-2.9.2-cp37-cp37m-win32.whl", hash = "sha256:18452582a3c85b96014b45686af264563e3e5d99d226589f057ace56196ec78b"}, + {file = "cryptography-2.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:22e91636a51170df0ae4dcbd250d318fd28c9f491c4e50b625a49964b24fe46e"}, + {file = "cryptography-2.9.2-cp38-cp38-win32.whl", hash = "sha256:844a76bc04472e5135b909da6aed84360f522ff5dfa47f93e3dd2a0b84a89fa0"}, + {file = "cryptography-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5"}, + {file = "cryptography-2.9.2.tar.gz", hash = "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229"}, +] +dataclasses = [ + {file = "dataclasses-0.7-py3-none-any.whl", hash = "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836"}, + {file = "dataclasses-0.7.tar.gz", hash = "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"}, +] +distlib = [ + {file = "distlib-0.3.0.zip", hash = "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"}, +] +fabric = [ + {file = "fabric-2.5.0-py2.py3-none-any.whl", hash = "sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389"}, + {file = "fabric-2.5.0.tar.gz", hash = "sha256:24842d7d51556adcabd885ac3cf5e1df73fc622a1708bf3667bf5927576cdfa6"}, +] +filelock = [ + {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, + {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, +] +flake8 = [ + {file = "flake8-3.8.2-py2.py3-none-any.whl", hash = "sha256:ccaa799ef9893cebe69fdfefed76865aeaefbb94cb8545617b2298786a4de9a5"}, + {file = "flake8-3.8.2.tar.gz", hash = "sha256:c69ac1668e434d37a2d2880b3ca9aafd54b3a10a3ac1ab101d22f29e29cf8634"}, +] +grpcio = [ + {file = "grpcio-1.29.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e90f3d11185c36593186e5ff1f581acc6ddfa4190f145b0366e579de1f52803b"}, + {file = "grpcio-1.29.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:5024b26e17a1bfc9390fb3b8077bf886eee02970af780fd23072970ef08cefe8"}, + {file = "grpcio-1.29.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:23bc395a32c2465564cb242e48bdd2fdbe5a4aebf307649a800da1b971ee7f29"}, + {file = "grpcio-1.29.0-cp27-cp27m-win32.whl", hash = "sha256:886d48c32960b39e059494637eb0157a694956248d03b0de814447c188b74799"}, + {file = "grpcio-1.29.0-cp27-cp27m-win_amd64.whl", hash = "sha256:da0ca9b1089d00e39a8b83deec799a4e5c37ec1b44d804495424acde50531868"}, + {file = "grpcio-1.29.0-cp27-cp27mu-linux_armv7l.whl", hash = "sha256:ebf0ccb782027ef9e213e03b6d00bbd8dabd80959db7d468c0738e6d94b5204c"}, + {file = "grpcio-1.29.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:2637ce96b7c954d2b71060f50eb4c72f81668f1b2faa6cbdc74677e405978901"}, + {file = "grpcio-1.29.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:75b2247307a7ecaf6abc9eb2bd04af8f88816c111b87bf0044d7924396e9549c"}, + {file = "grpcio-1.29.0-cp35-cp35m-linux_armv7l.whl", hash = "sha256:7bf3cb1e0f4a9c89f7b748583b994bdce183103d89d5ff486da48a7668a052c7"}, + {file = "grpcio-1.29.0-cp35-cp35m-macosx_10_7_intel.whl", hash = "sha256:a6dddb177b3cfa0cfe299fb9e07d6a3382cc79466bef48fe9c4326d5c5b1dcb8"}, + {file = "grpcio-1.29.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:b49f243936b0f6ae8eb6adf88a1e54e736f1c6724a1bff6b591d105d708263ad"}, + {file = "grpcio-1.29.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:9cfb4b71cc3c8757f137d47000f9d90d4bd818733f9ab4f78bd447e052a4cb9a"}, + {file = "grpcio-1.29.0-cp35-cp35m-win32.whl", hash = "sha256:10cdc8946a7c2284bbc8e16d346eaa2beeaae86ea598f345df86d4ef7dfedb84"}, + {file = "grpcio-1.29.0-cp35-cp35m-win_amd64.whl", hash = "sha256:806c9759f5589b3761561187408e0313a35c5c53f075c7590effab8d27d67dfe"}, + {file = "grpcio-1.29.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:57c8cc2ae8cb94c3a89671af7e1380a4cdfcd6bab7ba303f4461ec32ded250ae"}, + {file = "grpcio-1.29.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:97b72bf2242a351a89184134adbb0ae3b422e6893c6c712bc7669e2eab21501b"}, + {file = "grpcio-1.29.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:517538a54afdd67162ea2af1ac3326c0752c5d13e6ddadbc4885f6a28e91ab28"}, + {file = "grpcio-1.29.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:eede3039c3998e2cc0f6713f4ac70f235bd32967c9b958a17bf937aceebc12c3"}, + {file = "grpcio-1.29.0-cp36-cp36m-win32.whl", hash = "sha256:54e4658c09084b09cd83a5ea3a8bce78e4031ff1010bb8908c399a22a76a6f08"}, + {file = "grpcio-1.29.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7e02a7c40304eecee203f809a982732bd37fad4e798acad98fe73c66e44ff2db"}, + {file = "grpcio-1.29.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ff7931241351521b8df01d7448800ce0d59364321d8d82c49b826d455678ff08"}, + {file = "grpcio-1.29.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:5fd9ffe938e9225c654c60eb21ff011108cc27302db85200413807e0eda99a4a"}, + {file = "grpcio-1.29.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:9ef0370bcf629ece4e7e37796e4604e2514b920669be2911fc3f9c163a73a57b"}, + {file = "grpcio-1.29.0-cp37-cp37m-win32.whl", hash = "sha256:3d8c510b6eabce5192ce126003d74d7751c7218d3e2ad39fcf02400d7ec43abe"}, + {file = "grpcio-1.29.0-cp37-cp37m-win_amd64.whl", hash = "sha256:81bbf78a399e0ee516c81ddad8601f12af3fc9b30f2e4b2fbd64efd327304a4d"}, + {file = "grpcio-1.29.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:80e9f9f6265149ca7c84e1c8c31c2cf3e2869c45776fbe8880a3133a11d6d290"}, + {file = "grpcio-1.29.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:524ae8d3da61b856cf08abb3d0947df05402919e4be1f88328e0c1004031f72e"}, + {file = "grpcio-1.29.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c3a0ef12ee86f6e72db50e01c3dba7735a76d8c30104b9b0f7fd9d65ceb9d93f"}, + {file = "grpcio-1.29.0-cp38-cp38-win32.whl", hash = "sha256:97fcbdf1f12e0079d26db73da11ee35a09adc870b1e72fbff0211f6a8003a4e8"}, + {file = "grpcio-1.29.0-cp38-cp38-win_amd64.whl", hash = "sha256:b85f355fc24b68a6c52f2750e7141110d1fcd07dfdc9b282de0000550fe0511b"}, + {file = "grpcio-1.29.0.tar.gz", hash = "sha256:a97ea91e31863c9a3879684b5fb3c6ab4b17c5431787548fc9f52b9483ea9c25"}, +] +grpcio-tools = [ + {file = "grpcio-tools-1.29.0.tar.gz", hash = "sha256:0f681c1ebd5472b804baa391b16dc59d92b065903999566f4776bfbd010bcec9"}, + {file = "grpcio_tools-1.29.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b504e844e6f3610f279e0fba719052a73d5acc858a82d5a1151155b3c2304478"}, + {file = "grpcio_tools-1.29.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c52bcc2e5e9d93b805e6f292e543cbabeb9a751dc9d4d451c39d4c30ee311142"}, + {file = "grpcio_tools-1.29.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:5beffd530b496866b8e8dc811e942815a6e637669350c1341b5972bb692465cc"}, + {file = "grpcio_tools-1.29.0-cp27-cp27m-win32.whl", hash = "sha256:49dcf4c11ba2766d065c90a61eb1cefc55d5d094f93c1f66a4d98bfcbc5f740c"}, + {file = "grpcio_tools-1.29.0-cp27-cp27m-win_amd64.whl", hash = "sha256:bab2a3d627f114091a758d8a7ae48af54bff717f84bb34538fed5114982e73a5"}, + {file = "grpcio_tools-1.29.0-cp27-cp27mu-linux_armv7l.whl", hash = "sha256:2a1f27a21d09e864cdfcff22265af86d9a548ea9a775e5d6a27d7abb71c3b5aa"}, + {file = "grpcio_tools-1.29.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:56aade8ed52a6cca74a4703279aaae4aa2e2b87d0ccb5778f95d31267e74fc6b"}, + {file = "grpcio_tools-1.29.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:78075ee7459001cf5c81b1f2e3f047b63d35ed018b9e15e3abeda59b70af0a4e"}, + {file = "grpcio_tools-1.29.0-cp35-cp35m-linux_armv7l.whl", hash = "sha256:1626cd01a484f29cc9b33c3902851490149d40a550b92a382978571ca7e712cf"}, + {file = "grpcio_tools-1.29.0-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:2f1d80e3988d86477633fb39442a2310513d02fcc48881b359257a4be3cfd336"}, + {file = "grpcio_tools-1.29.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:8ffdcb1cbbc1bdfe249eb08c9fc6557b4f83b9f6145b5914bfd2973013d6dc1f"}, + {file = "grpcio_tools-1.29.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:7e52c8ed5e0157ff85493f93540e3c897c7d97be03afc73230d1022ba7b80528"}, + {file = "grpcio_tools-1.29.0-cp35-cp35m-win32.whl", hash = "sha256:f464d2efe04a46a17cf9493d67e6839aa535bb8a904cc6a2b588f1b156c9265d"}, + {file = "grpcio_tools-1.29.0-cp35-cp35m-win_amd64.whl", hash = "sha256:9de112c090ab67e90b8c36eee5876278c8d037bf7c55052848886c1e8a2dd1c2"}, + {file = "grpcio_tools-1.29.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:38ab9e8afdf34289eab85ce2343c451c36837bf2521b927b30d9a845304abf4c"}, + {file = "grpcio_tools-1.29.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1038b3d6cfd7206caf7c0a54ed06896e2aeb0a7d213a40d9000a70595e2fca21"}, + {file = "grpcio_tools-1.29.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:2a681ebfde0d83b70117cac745a97a3e5dc258fd817c1c1dd2bf99579b663a28"}, + {file = "grpcio_tools-1.29.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:47d13ddbbc2bd0e21a6109f74e731049b1d8738b5d0124580efca3721fe77fd2"}, + {file = "grpcio_tools-1.29.0-cp36-cp36m-win32.whl", hash = "sha256:fb9c46b8a0ee1a5990f29d891d6023cb92fdab9aed408194667df04f72e9caf6"}, + {file = "grpcio_tools-1.29.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f672a606a59145bacc58cf4c4bb407f107abe1289f607c09e9224c99e897ed1a"}, + {file = "grpcio_tools-1.29.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1a606f2f5b23822e2e5271bf0df98c140ceed154ea6bf5c04ea85a37a0317771"}, + {file = "grpcio_tools-1.29.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d89a43d14fb3043c1876e78d7ad5018c762b0ce51c199c588fa9142442546005"}, + {file = "grpcio_tools-1.29.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:faf845f71fcb6cb5088429c676ae644116d56e5de41c639be4d7399bf71b9637"}, + {file = "grpcio_tools-1.29.0-cp37-cp37m-win32.whl", hash = "sha256:05f214bc904c8e4ebf0240993a868895ff96184172243c0c61b323f6f029863d"}, + {file = "grpcio_tools-1.29.0-cp37-cp37m-win_amd64.whl", hash = "sha256:afcb030067ba1b6c371a7bfd1ffd77375534144000d47d245ca77ebbd195901d"}, + {file = "grpcio_tools-1.29.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b55346fa75df4b1581627022a2c79cfeb58cdaebf719cdbf63ff8ae6d7d7704b"}, + {file = "grpcio_tools-1.29.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:22d91ceb853f6846bcc23f15d8a936574eeb9fc7e8941bb8a1a5f8fcf4f566b2"}, + {file = "grpcio_tools-1.29.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:6eddefcd10f261d2aef6c122fb0651a53fcaee86e47d407492c9acf57107c91a"}, + {file = "grpcio_tools-1.29.0-cp38-cp38-win32.whl", hash = "sha256:658e131e983f4c3bec2e096c3cc048e6420acad2b19fad82328c481088ce0d1a"}, + {file = "grpcio_tools-1.29.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c52f68e864f60ed51ea59a3fd18d0989720bbf2e32d47b4096eba7b0b7f7086"}, +] +identify = [ + {file = "identify-1.4.18-py2.py3-none-any.whl", hash = "sha256:9f53e80371f2ac7c969eefda8efaabd4f77c6300f5f8fc4b634744a0db8fe5cc"}, + {file = "identify-1.4.18.tar.gz", hash = "sha256:de4e1de6c23f52b71c8a54ff558219f3783ff011b432f29360d84a8a31ba561c"}, +] +importlib-metadata = [ + {file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"}, + {file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"}, +] +importlib-resources = [ + {file = "importlib_resources-1.5.0-py2.py3-none-any.whl", hash = "sha256:85dc0b9b325ff78c8bef2e4ff42616094e16b98ebd5e3b50fe7e2f0bbcdcde49"}, + {file = "importlib_resources-1.5.0.tar.gz", hash = "sha256:6f87df66833e1942667108628ec48900e02a4ab4ad850e25fbf07cb17cf734ca"}, +] +invoke = [ + {file = "invoke-1.4.1-py2-none-any.whl", hash = "sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134"}, + {file = "invoke-1.4.1-py3-none-any.whl", hash = "sha256:87b3ef9d72a1667e104f89b159eaf8a514dbf2f3576885b2bbdefe74c3fb2132"}, + {file = "invoke-1.4.1.tar.gz", hash = "sha256:de3f23bfe669e3db1085789fd859eb8ca8e0c5d9c20811e2407fa042e8a5e15d"}, +] +isort = [ + {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, + {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, +] +lxml = [ + {file = "lxml-4.5.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ee2be8b8f72a2772e72ab926a3bccebf47bb727bda41ae070dc91d1fb759b726"}, + {file = "lxml-4.5.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:fadd2a63a2bfd7fb604508e553d1cf68eca250b2fbdbd81213b5f6f2fbf23529"}, + {file = "lxml-4.5.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4f282737d187ae723b2633856085c31ae5d4d432968b7f3f478a48a54835f5c4"}, + {file = "lxml-4.5.1-cp27-cp27m-win32.whl", hash = "sha256:7fd88cb91a470b383aafad554c3fe1ccf6dfb2456ff0e84b95335d582a799804"}, + {file = "lxml-4.5.1-cp27-cp27m-win_amd64.whl", hash = "sha256:0790ddca3f825dd914978c94c2545dbea5f56f008b050e835403714babe62a5f"}, + {file = "lxml-4.5.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9144ce36ca0824b29ebc2e02ca186e54040ebb224292072250467190fb613b96"}, + {file = "lxml-4.5.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a636346c6c0e1092ffc202d97ec1843a75937d8c98aaf6771348ad6422e44bb0"}, + {file = "lxml-4.5.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:f95d28193c3863132b1f55c1056036bf580b5a488d908f7d22a04ace8935a3a9"}, + {file = "lxml-4.5.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b26719890c79a1dae7d53acac5f089d66fd8cc68a81f4e4bd355e45470dc25e1"}, + {file = "lxml-4.5.1-cp35-cp35m-win32.whl", hash = "sha256:a9e3b8011388e7e373565daa5e92f6c9cb844790dc18e43073212bb3e76f7007"}, + {file = "lxml-4.5.1-cp35-cp35m-win_amd64.whl", hash = "sha256:2754d4406438c83144f9ffd3628bbe2dcc6d62b20dbc5c1ec4bc4385e5d44b42"}, + {file = "lxml-4.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:925baf6ff1ef2c45169f548cc85204433e061360bfa7d01e1be7ae38bef73194"}, + {file = "lxml-4.5.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a87dbee7ad9dce3aaefada2081843caf08a44a8f52e03e0a4cc5819f8398f2f4"}, + {file = "lxml-4.5.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:51bb4edeb36d24ec97eb3e6a6007be128b720114f9a875d6b370317d62ac80b9"}, + {file = "lxml-4.5.1-cp36-cp36m-win32.whl", hash = "sha256:c79e5debbe092e3c93ca4aee44c9a7631bdd407b2871cb541b979fd350bbbc29"}, + {file = "lxml-4.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b7462cdab6fffcda853338e1741ce99706cdf880d921b5a769202ea7b94e8528"}, + {file = "lxml-4.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06748c7192eab0f48e3d35a7adae609a329c6257495d5e53878003660dc0fec6"}, + {file = "lxml-4.5.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1aa7a6197c1cdd65d974f3e4953764eee3d9c7b67e3966616b41fab7f8f516b7"}, + {file = "lxml-4.5.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:afb53edf1046599991fb4a7d03e601ab5f5422a5435c47ee6ba91ec3b61416a6"}, + {file = "lxml-4.5.1-cp37-cp37m-win32.whl", hash = "sha256:2d1ddce96cf15f1254a68dba6935e6e0f1fe39247de631c115e84dd404a6f031"}, + {file = "lxml-4.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:22c6d34fdb0e65d5f782a4d1a1edb52e0a8365858dafb1c08cb1d16546cf0786"}, + {file = "lxml-4.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c47a8a5d00060122ca5908909478abce7bbf62d812e3fc35c6c802df8fb01fe7"}, + {file = "lxml-4.5.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b77975465234ff49fdad871c08aa747aae06f5e5be62866595057c43f8d2f62c"}, + {file = "lxml-4.5.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2b02c106709466a93ed424454ce4c970791c486d5fcdf52b0d822a7e29789626"}, + {file = "lxml-4.5.1-cp38-cp38-win32.whl", hash = "sha256:7eee37c1b9815e6505847aa5e68f192e8a1b730c5c7ead39ff317fde9ce29448"}, + {file = "lxml-4.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:d8d40e0121ca1606aa9e78c28a3a7d88a05c06b3ca61630242cded87d8ce55fa"}, + {file = "lxml-4.5.1.tar.gz", hash = "sha256:27ee0faf8077c7c1a589573b1450743011117f1aa1a91d5ae776bbc5ca6070f2"}, +] +mako = [ + {file = "Mako-1.1.3-py2.py3-none-any.whl", hash = "sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9"}, + {file = "Mako-1.1.3.tar.gz", hash = "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27"}, +] +markupsafe = [ + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mock = [ + {file = "mock-4.0.2-py3-none-any.whl", hash = "sha256:3f9b2c0196c60d21838f307f5825a7b86b678cedc58ab9e50a8988187b4d81e0"}, + {file = "mock-4.0.2.tar.gz", hash = "sha256:dd33eb70232b6118298d516bbcecd26704689c386594f0f3c4f13867b2c56f72"}, +] +more-itertools = [ + {file = "more-itertools-8.3.0.tar.gz", hash = "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be"}, + {file = "more_itertools-8.3.0-py3-none-any.whl", hash = "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982"}, +] +netaddr = [ + {file = "netaddr-0.7.19-py2.py3-none-any.whl", hash = "sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca"}, + {file = "netaddr-0.7.19.tar.gz", hash = "sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd"}, +] +nodeenv = [ + {file = "nodeenv-1.4.0-py2.py3-none-any.whl", hash = "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc"}, +] +packaging = [ + {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, + {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, +] +paramiko = [ + {file = "paramiko-2.7.1-py2.py3-none-any.whl", hash = "sha256:9c980875fa4d2cb751604664e9a2d0f69096643f5be4db1b99599fe114a97b2f"}, + {file = "paramiko-2.7.1.tar.gz", hash = "sha256:920492895db8013f6cc0179293147f830b8c7b21fdfc839b6bad760c27459d9f"}, +] +pillow = [ + {file = "Pillow-7.1.2-cp35-cp35m-macosx_10_10_intel.whl", hash = "sha256:ae2b270f9a0b8822b98655cb3a59cdb1bd54a34807c6c56b76dd2e786c3b7db3"}, + {file = "Pillow-7.1.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:d23e2aa9b969cf9c26edfb4b56307792b8b374202810bd949effd1c6e11ebd6d"}, + {file = "Pillow-7.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b532bcc2f008e96fd9241177ec580829dee817b090532f43e54074ecffdcd97f"}, + {file = "Pillow-7.1.2-cp35-cp35m-win32.whl", hash = "sha256:12e4bad6bddd8546a2f9771485c7e3d2b546b458ae8ff79621214119ac244523"}, + {file = "Pillow-7.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9744350687459234867cbebfe9df8f35ef9e1538f3e729adbd8fde0761adb705"}, + {file = "Pillow-7.1.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:f54be399340aa602066adb63a86a6a5d4f395adfdd9da2b9a0162ea808c7b276"}, + {file = "Pillow-7.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:1f694e28c169655c50bb89a3fa07f3b854d71eb47f50783621de813979ba87f3"}, + {file = "Pillow-7.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f784aad988f12c80aacfa5b381ec21fd3f38f851720f652b9f33facc5101cf4d"}, + {file = "Pillow-7.1.2-cp36-cp36m-win32.whl", hash = "sha256:b37bb3bd35edf53125b0ff257822afa6962649995cbdfde2791ddb62b239f891"}, + {file = "Pillow-7.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:b67a6c47ed963c709ed24566daa3f95a18f07d3831334da570c71da53d97d088"}, + {file = "Pillow-7.1.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:eaa83729eab9c60884f362ada982d3a06beaa6cc8b084cf9f76cae7739481dfa"}, + {file = "Pillow-7.1.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f46e0e024346e1474083c729d50de909974237c72daca05393ee32389dabe457"}, + {file = "Pillow-7.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0e2a3bceb0fd4e0cb17192ae506d5f082b309ffe5fc370a5667959c9b2f85fa3"}, + {file = "Pillow-7.1.2-cp37-cp37m-win32.whl", hash = "sha256:ccc9ad2460eb5bee5642eaf75a0438d7f8887d484490d5117b98edd7f33118b7"}, + {file = "Pillow-7.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b943e71c2065ade6fef223358e56c167fc6ce31c50bc7a02dd5c17ee4338e8ac"}, + {file = "Pillow-7.1.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:04766c4930c174b46fd72d450674612ab44cca977ebbcc2dde722c6933290107"}, + {file = "Pillow-7.1.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:f455efb7a98557412dc6f8e463c1faf1f1911ec2432059fa3e582b6000fc90e2"}, + {file = "Pillow-7.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ee94fce8d003ac9fd206496f2707efe9eadcb278d94c271f129ab36aa7181344"}, + {file = "Pillow-7.1.2-cp38-cp38-win32.whl", hash = "sha256:4b02b9c27fad2054932e89f39703646d0c543f21d3cc5b8e05434215121c28cd"}, + {file = "Pillow-7.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:3d25dd8d688f7318dca6d8cd4f962a360ee40346c15893ae3b95c061cdbc4079"}, + {file = "Pillow-7.1.2-pp373-pypy36_pp73-win32.whl", hash = "sha256:0f01e63c34f0e1e2580cc0b24e86a5ccbbfa8830909a52ee17624c4193224cd9"}, + {file = "Pillow-7.1.2-py3.8-macosx-10.9-x86_64.egg", hash = "sha256:70e3e0d99a0dcda66283a185f80697a9b08806963c6149c8e6c5f452b2aa59c0"}, + {file = "Pillow-7.1.2.tar.gz", hash = "sha256:a0b49960110bc6ff5fead46013bcb8825d101026d466f3a4de3476defe0fb0dd"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +pre-commit = [ + {file = "pre_commit-2.1.1-py2.py3-none-any.whl", hash = "sha256:09ebe467f43ce24377f8c2f200fe3cd2570d328eb2ce0568c8e96ce19da45fa6"}, + {file = "pre_commit-2.1.1.tar.gz", hash = "sha256:f8d555e31e2051892c7f7b3ad9f620bd2c09271d87e9eedb2ad831737d6211eb"}, +] +protobuf = [ + {file = "protobuf-3.12.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e1464a4a2cf12f58f662c8e6421772c07947266293fb701cb39cd9c1e183f63c"}, + {file = "protobuf-3.12.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:6f349adabf1c004aba53f7b4633459f8ca8a09654bf7e69b509c95a454755776"}, + {file = "protobuf-3.12.2-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:be04fe14ceed7f8641e30f36077c1a654ff6f17d0c7a5283b699d057d150d82a"}, + {file = "protobuf-3.12.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f4b73736108a416c76c17a8a09bc73af3d91edaa26c682aaa460ef91a47168d3"}, + {file = "protobuf-3.12.2-cp35-cp35m-win32.whl", hash = "sha256:5524c7020eb1fb7319472cb75c4c3206ef18b34d6034d2ee420a60f99cddeb07"}, + {file = "protobuf-3.12.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bff02030bab8b969f4de597543e55bd05e968567acb25c0a87495a31eb09e925"}, + {file = "protobuf-3.12.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c9ca9f76805e5a637605f171f6c4772fc4a81eced4e2f708f79c75166a2c99ea"}, + {file = "protobuf-3.12.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:304e08440c4a41a0f3592d2a38934aad6919d692bb0edfb355548786728f9a5e"}, + {file = "protobuf-3.12.2-cp36-cp36m-win32.whl", hash = "sha256:b5a114ea9b7fc90c2cc4867a866512672a47f66b154c6d7ee7e48ddb68b68122"}, + {file = "protobuf-3.12.2-cp36-cp36m-win_amd64.whl", hash = "sha256:85b94d2653b0fdf6d879e39d51018bf5ccd86c81c04e18a98e9888694b98226f"}, + {file = "protobuf-3.12.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7ab28a8f1f043c58d157bceb64f80e4d2f7f1b934bc7ff5e7f7a55a337ea8b0"}, + {file = "protobuf-3.12.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:eafe9fa19fcefef424ee089fb01ac7177ff3691af7cc2ae8791ae523eb6ca907"}, + {file = "protobuf-3.12.2-cp37-cp37m-win32.whl", hash = "sha256:612bc97e42b22af10ba25e4140963fbaa4c5181487d163f4eb55b0b15b3dfcd2"}, + {file = "protobuf-3.12.2-cp37-cp37m-win_amd64.whl", hash = "sha256:e72736dd822748b0721f41f9aaaf6a5b6d5cfc78f6c8690263aef8bba4457f0e"}, + {file = "protobuf-3.12.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:87535dc2d2ef007b9d44e309d2b8ea27a03d2fa09556a72364d706fcb7090828"}, + {file = "protobuf-3.12.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:50b5fee674878b14baea73b4568dc478c46a31dd50157a5b5d2f71138243b1a9"}, + {file = "protobuf-3.12.2-py2.py3-none-any.whl", hash = "sha256:a96f8fc625e9ff568838e556f6f6ae8eca8b4837cdfb3f90efcb7c00e342a2eb"}, + {file = "protobuf-3.12.2.tar.gz", hash = "sha256:49ef8ab4c27812a89a76fa894fe7a08f42f2147078392c0dee51d4a444ef6df5"}, +] +py = [ + {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"}, + {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"}, +] +pycodestyle = [ + {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, + {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, +] +pycparser = [ + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] +pyflakes = [ + {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, + {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, +] +pynacl = [ + {file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80"}, + {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, + {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, + {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win32.whl", hash = "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"}, + {file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pyproj = [ + {file = "pyproj-2.6.1.post1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:457ad3856014ac26af1d86def6dc8cf69c1fa377b6e2fd6e97912d51cf66bdbe"}, + {file = "pyproj-2.6.1.post1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6f3f36440ea61f5f6da4e6beb365dddcbe159815450001d9fb753545affa45ff"}, + {file = "pyproj-2.6.1.post1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6a212d0e5c7efa33d039f0c8b0a489e2204fcd28b56206567852ad7f5f2a653e"}, + {file = "pyproj-2.6.1.post1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:451a3d1c563b672458029ebc04acbb3266cd8b3025268eb871a9176dc3638911"}, + {file = "pyproj-2.6.1.post1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e015f900b4b84e908f8035ab16ebf02d67389c1c216c17a2196fc2e515c00762"}, + {file = "pyproj-2.6.1.post1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a13e5731b3a360ee7fbd1e9199ec9203fafcece8ebd0b1351f16d0a90cad6828"}, + {file = "pyproj-2.6.1.post1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:33c1c2968a4f4f87d517c4275a18b557e5c13907cf2609371fadea8463c3ba05"}, + {file = "pyproj-2.6.1.post1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3fef83a01c1e86dd9fa99d8214f749837cfafc34d9d6230b4b0a998fa7a68a1a"}, + {file = "pyproj-2.6.1.post1-cp36-cp36m-win32.whl", hash = "sha256:a6ac4861979cd05a0f5400fefa41d26c0269a5fb8237618aef7c998907db39e1"}, + {file = "pyproj-2.6.1.post1-cp36-cp36m-win_amd64.whl", hash = "sha256:cbf6ccf990860b06c5262ff97c4b78e1d07883981635cd53a6aa438a68d92945"}, + {file = "pyproj-2.6.1.post1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:adacb67a9f71fb54ca1b887a6ab20f32dd536fcdf2acec84a19e25ad768f7965"}, + {file = "pyproj-2.6.1.post1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e50d5d20b87758acf8f13f39a3b3eb21d5ef32339d2bc8cdeb8092416e0051df"}, + {file = "pyproj-2.6.1.post1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2518d1606e2229b82318e704b40290e02a2a52d77b40cdcb2978973d6fc27b20"}, + {file = "pyproj-2.6.1.post1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:33a5d1cfbb40a019422eb80709a0e270704390ecde7278fdc0b88f3647c56a39"}, + {file = "pyproj-2.6.1.post1-cp37-cp37m-win32.whl", hash = "sha256:daf2998e3f5bcdd579a18faf009f37f53538e9b7d0a252581a610297d31e8536"}, + {file = "pyproj-2.6.1.post1-cp37-cp37m-win_amd64.whl", hash = "sha256:a8b7c8accdc61dac8e91acab7c1f7b4590d1e102f2ee9b1f1e6399fad225958e"}, + {file = "pyproj-2.6.1.post1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9f097e8f341a162438918e908be86d105a28194ff6224633b2e9616c5031153f"}, + {file = "pyproj-2.6.1.post1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:d90a5d1fdd066b0e9b22409b0f5e81933469918fa04c2cf7f9a76ce84cb29dad"}, + {file = "pyproj-2.6.1.post1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f5a8015c74ec8f6508aebf493b58ba20ccb4da8168bf05f0c2a37faccb518da9"}, + {file = "pyproj-2.6.1.post1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d87836be6b720fb4d9c112136aa47621b6ca09a554e645c1081561eb8e2fa1f4"}, + {file = "pyproj-2.6.1.post1-cp38-cp38-win32.whl", hash = "sha256:bc2f3a15d065e206d63edd2cc4739aa0a35c05338ee276ab1dc72f56f1944bda"}, + {file = "pyproj-2.6.1.post1-cp38-cp38-win_amd64.whl", hash = "sha256:93cbad7b699e8e80def7de80c350617f35e6a0b82862f8ce3c014657c25fdb3c"}, + {file = "pyproj-2.6.1.post1.tar.gz", hash = "sha256:4f5b02b4abbd41610397c635b275a8ee4a2b5bc72a75572b98ac6ae7befa471e"}, +] +pytest = [ + {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, + {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, +] +pyyaml = [ + {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, + {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, + {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +toml = [ + {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, + {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, +] +virtualenv = [ + {file = "virtualenv-20.0.21-py2.py3-none-any.whl", hash = "sha256:a730548b27366c5e6cbdf6f97406d861cccece2e22275e8e1a757aeff5e00c70"}, + {file = "virtualenv-20.0.21.tar.gz", hash = "sha256:a116629d4e7f4d03433b8afa27f43deba09d48bc48f5ecefa4f015a178efb6cf"}, +] +wcwidth = [ + {file = "wcwidth-0.2.3-py2.py3-none-any.whl", hash = "sha256:980fbf4f3c196c0f329cdcd1e84c554d6a211f18e252e525a0cf4223154a41d6"}, + {file = "wcwidth-0.2.3.tar.gz", hash = "sha256:edbc2b718b4db6cdf393eefe3a420183947d6aa312505ce6754516f458ff8830"}, +] +zipp = [ + {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, + {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, +] diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml new file mode 100644 index 00000000..6df5f10e --- /dev/null +++ b/daemon/pyproject.toml @@ -0,0 +1,33 @@ +[tool.poetry] +name = "core" +version = "6.4.0" +description = "" +authors = [] + +[tool.poetry.dependencies] +python = "^3.6" +dataclasses = { version = "*", python = "3.6" } +fabric = "*" +grpcio = "*" +invoke = "*" +lxml = "*" +mako = "*" +netaddr = "*" +pillow = "*" +protobuf = "*" +pyproj = "*" +pyyaml = "*" + +[tool.poetry.dev-dependencies] +black = "==19.3b0" +flake8 = "*" +grpcio-tools = "*" +isort = "*" +mock = "*" +pre-commit = "*" +pytest = "*" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" + diff --git a/tasks.py b/tasks.py new file mode 100644 index 00000000..74e6af76 --- /dev/null +++ b/tasks.py @@ -0,0 +1,14 @@ +from invoke import task + + +@task +def core(c): + c.run( + "poetry run sudo python3 scripts/core-daemon " + "-f data/core.conf -l data/logging.conf" + ) + + +@task +def core_pygui(c): + c.run("poetry run python3 scripts/core-pygui") From 1884103cb4f54f475a8ab88ef5e31663a4bc3fdf Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 3 Jun 2020 08:47:36 -0700 Subject: [PATCH 0303/1131] grpc: added call to stream node movements using geo/xy and tests to validate usage, fixed potential exception when not setting session geo ref and using conversions --- daemon/core/api/grpc/client.py | 13 ++++- daemon/core/api/grpc/server.py | 34 +++++++++++++ daemon/core/location/geo.py | 4 +- daemon/proto/core/api/grpc/core.proto | 15 ++++++ daemon/tests/test_grpc.py | 70 ++++++++++++++++++++++++++- 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index a645c756..280b1cd8 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -5,7 +5,7 @@ gRpc client for interfacing with CORE, when gRPC mode is enabled. import logging import threading from contextlib import contextmanager -from typing import Any, Callable, Dict, Generator, List +from typing import Any, Callable, Dict, Generator, Iterable, List import grpc import netaddr @@ -571,6 +571,17 @@ class CoreGrpcClient: ) return self.stub.EditNode(request) + def move_nodes( + self, move_iterator: Iterable[core_pb2.MoveNodesRequest] + ) -> core_pb2.MoveNodesResponse: + """ + Stream node movements using the provided iterator. + + :param move_iterator: iterator for generating node movements + :return: move nodes response + """ + return self.stub.MoveNodes(move_iterator) + def delete_node(self, session_id: int, node_id: int) -> core_pb2.DeleteNodeResponse: """ Delete node from session. diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 16da7e6b..972153e7 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -692,6 +692,40 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): node_proto = grpcutils.get_node_proto(session, node) return core_pb2.GetNodeResponse(node=node_proto, interfaces=interfaces) + def MoveNodes( + self, request_iterator, context: ServicerContext + ) -> core_pb2.MoveNodesResponse: + """ + Stream node movements + + :param request_iterator: move nodes request iterator + :param context: context object + :return: move nodes response + """ + for request in request_iterator: + if not request.WhichOneof("move_type"): + raise CoreError("move nodes must provide a move type") + session = self.get_session(request.session_id, context) + node = self.get_node(session, request.node_id, context, NodeBase) + options = NodeOptions() + has_geo = request.HasField("geo") + if has_geo: + logging.info("has geo") + lat = request.geo.lat + lon = request.geo.lon + alt = request.geo.alt + options.set_location(lat, lon, alt) + else: + x = request.position.x + y = request.position.y + logging.info("has pos: %s,%s", x, y) + options.set_position(x, y) + session.edit_node(node.id, options) + source = request.source if request.source else None + if not has_geo: + session.broadcast_node(node, source=source) + return core_pb2.MoveNodesResponse() + def EditNode( self, request: core_pb2.EditNodeRequest, context: ServicerContext ) -> core_pb2.EditNodeResponse: diff --git a/daemon/core/location/geo.py b/daemon/core/location/geo.py index 1f78f329..4ff56dd6 100644 --- a/daemon/core/location/geo.py +++ b/daemon/core/location/geo.py @@ -31,7 +31,7 @@ class GeoLocation: CRS_WGS84, CRS_PROJ, always_xy=True ) self.to_geo = pyproj.Transformer.from_crs(CRS_PROJ, CRS_WGS84, always_xy=True) - self.refproj = (0.0, 0.0) + self.refproj = (0.0, 0.0, 0.0) self.refgeo = (0.0, 0.0, 0.0) self.refxyz = (0.0, 0.0, 0.0) self.refscale = 1.0 @@ -58,7 +58,7 @@ class GeoLocation: self.refxyz = (0.0, 0.0, 0.0) self.refgeo = (0.0, 0.0, 0.0) self.refscale = 1.0 - self.refproj = self.to_pixels.transform(self.refgeo[0], self.refgeo[1]) + self.refproj = self.to_pixels.transform(*self.refgeo) def pixels2meters(self, value: float) -> float: """ diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index b0ae6642..cdcd9686 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -61,6 +61,8 @@ service CoreApi { } rpc GetNodeTerminal (GetNodeTerminalRequest) returns (GetNodeTerminalResponse) { } + rpc MoveNodes (stream MoveNodesRequest) returns (MoveNodesResponse) { + } // link rpc rpc GetNodeLinks (GetNodeLinksRequest) returns (GetNodeLinksResponse) { @@ -446,6 +448,19 @@ message GetNodeTerminalResponse { string terminal = 1; } +message MoveNodesRequest { + int32 session_id = 1; + int32 node_id = 2; + string source = 3; + oneof move_type { + Position position = 4; + Geo geo = 5; + } +} + +message MoveNodesResponse { +} + message NodeCommandRequest { int32 session_id = 1; int32 node_id = 2; diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 47cfe744..128863b4 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -18,7 +18,7 @@ from core.api.tlv.dataconversion import ConfigShim from core.api.tlv.enumerations import ConfigFlags from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emane.nodes import EmaneNet -from core.emulator.data import EventData +from core.emulator.data import EventData, NodeData from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes, ExceptionLevels, NodeTypes from core.errors import CoreError @@ -1170,3 +1170,71 @@ class TestGrpc: # then queue.get(timeout=5) + + def test_move_nodes(self, grpc_server: CoreGrpcServer): + # given + client = CoreGrpcClient() + session = grpc_server.coreemu.create_session() + node = session.add_node(CoreNode) + x, y = 10.0, 15.0 + + def move_iter(): + yield core_pb2.MoveNodesRequest( + session_id=session.id, + node_id=node.id, + position=core_pb2.Position(x=x, y=y), + ) + + # then + with client.context_connect(): + client.move_nodes(move_iter()) + + # assert + assert node.position.x == x + assert node.position.y == y + + def test_move_nodes_geo(self, grpc_server: CoreGrpcServer): + # given + client = CoreGrpcClient() + session = grpc_server.coreemu.create_session() + node = session.add_node(CoreNode) + lon, lat, alt = 10.0, 15.0, 5.0 + queue = Queue() + + def node_handler(node_data: NodeData): + assert node_data.longitude == lon + assert node_data.latitude == lat + assert node_data.altitude == alt + queue.put(node_data) + + session.node_handlers.append(node_handler) + + def move_iter(): + yield core_pb2.MoveNodesRequest( + session_id=session.id, + node_id=node.id, + geo=core_pb2.Geo(lon=lon, lat=lat, alt=alt), + ) + + # then + with client.context_connect(): + client.move_nodes(move_iter()) + + # assert + assert node.position.lon == lon + assert node.position.lat == lat + assert node.position.alt == alt + assert queue.get(timeout=5) + + def test_move_nodes_exception(self, grpc_server: CoreGrpcServer): + # given + client = CoreGrpcClient() + grpc_server.coreemu.create_session() + + def move_iter(): + yield core_pb2.MoveNodesRequest() + + # then + with pytest.raises(grpc.RpcError): + with client.context_connect(): + client.move_nodes(move_iter()) From 3b0ca1638c2725e481e6b52cbfd9dec253892e33 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 3 Jun 2020 14:35:17 -0700 Subject: [PATCH 0304/1131] grpc: implemened initial support for streaming emane pathloss events --- daemon/core/api/grpc/client.py | 13 +++++++++++++ daemon/core/api/grpc/grpcutils.py | 24 +++++++++++++++++++++++- daemon/core/api/grpc/server.py | 18 +++++++++++++++++- daemon/core/emane/emanemanager.py | 18 +++++++++++++++++- daemon/proto/core/api/grpc/core.proto | 2 ++ daemon/proto/core/api/grpc/emane.proto | 13 +++++++++++++ 6 files changed, 85 insertions(+), 3 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 280b1cd8..6aaf7fac 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -31,6 +31,8 @@ from core.api.grpc.emane_pb2 import ( EmaneLinkRequest, EmaneLinkResponse, EmaneModelConfig, + EmanePathlossesRequest, + EmanePathlossesResponse, GetEmaneConfigRequest, GetEmaneConfigResponse, GetEmaneEventChannelRequest, @@ -1229,6 +1231,17 @@ class CoreGrpcClient: ) return self.stub.WlanLink(request) + def emane_pathlosses( + self, pathloss_iter: Iterable[EmanePathlossesRequest] + ) -> EmanePathlossesResponse: + """ + Stream EMANE pathloss events. + + :param pathloss_iter: iterator for sending EMANE pathloss events + :return: EMANE pathloss response + """ + return self.stub.EmanePathlosses(pathloss_iter) + def connect(self) -> None: """ Open connection to server, must be closed manually. diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 8a69c40f..b0c1e614 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -2,7 +2,9 @@ import logging import time from typing import Any, Dict, List, Tuple, Type +import grpc import netaddr +from grpc import ServicerContext from core import utils from core.api.grpc import common_pb2, core_pb2 @@ -13,7 +15,7 @@ from core.emulator.data import LinkData from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.enumerations import LinkTypes, NodeTypes from core.emulator.session import Session -from core.nodes.base import NodeBase +from core.nodes.base import CoreNode, NodeBase from core.nodes.interface import CoreInterface from core.services.coreservices import CoreService @@ -478,3 +480,23 @@ def interface_to_proto(interface: CoreInterface) -> core_pb2.Interface: ip6=ip6, ip6mask=ip6mask, ) + + +def get_nem_id(node: CoreNode, netif_id: int, context: ServicerContext) -> int: + """ + Get nem id for a given node and interface id. + + :param node: node to get nem id for + :param netif_id: id of interface on node to get nem id for + :param context: request context + :return: nem id + """ + netif = node.netif(netif_id) + if not netif: + message = f"{node.name} missing interface {netif_id}" + context.abort(grpc.StatusCode.NOT_FOUND, message) + net = netif.net + if not isinstance(net, EmaneNet): + message = f"{node.name} interface {netif_id} is not an EMANE network" + context.abort(grpc.StatusCode.INVALID_ARGUMENT, message) + return net.getnemid(netif) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 972153e7..1d13ec63 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -6,7 +6,7 @@ import tempfile import threading import time from concurrent import futures -from typing import Type +from typing import Iterable, Type import grpc from grpc import ServicerContext @@ -39,6 +39,8 @@ from core.api.grpc.core_pb2 import ExecuteScriptResponse from core.api.grpc.emane_pb2 import ( EmaneLinkRequest, EmaneLinkResponse, + EmanePathlossesRequest, + EmanePathlossesResponse, GetEmaneConfigRequest, GetEmaneConfigResponse, GetEmaneEventChannelRequest, @@ -1751,3 +1753,17 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): wlan.model.sendlinkmsg(n1_netif, n2_netif, unlink=not request.linked) result = True return WlanLinkResponse(result=result) + + def EmanePathlosses( + self, + request_iterator: Iterable[EmanePathlossesRequest], + context: ServicerContext, + ) -> EmanePathlossesResponse: + for request in request_iterator: + session = self.get_session(request.session_id, context) + n1 = self.get_node(session, request.node_one, context, CoreNode) + nem1 = grpcutils.get_nem_id(n1, request.interface_one_id, context) + n2 = self.get_node(session, request.node_two, context, CoreNode) + nem2 = grpcutils.get_nem_id(n2, request.interface_two_id, context) + session.emane.publish_pathloss(nem1, nem2, request.rx_one, request.rx_two) + return EmanePathlossesResponse() diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 438fde00..12b477f0 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -37,7 +37,7 @@ if TYPE_CHECKING: from core.emulator.session import Session try: - from emane.events import EventService + from emane.events import EventService, PathlossEvent from emane.events import LocationEvent from emane.events.eventserviceexception import EventServiceException except ImportError: @@ -48,6 +48,7 @@ except ImportError: except ImportError: EventService = None LocationEvent = None + PathlossEvent = None EventServiceException = None logging.debug("compatible emane python bindings not installed") @@ -868,6 +869,21 @@ class EmaneManager(ModelManager): result = False return result + def publish_pathloss(self, nem1: int, nem2: int, rx1: float, rx2: float) -> None: + """ + Publish pathloss events between provided nems, using provided rx power. + :param nem1: interface one for pathloss + :param nem2: interface two for pathloss + :param rx1: received power from nem2 to nem1 + :param rx2: received power from nem1 to nem2 + :return: nothing + """ + event = PathlossEvent() + event.append(nem1, forward=rx1) + event.append(nem2, forward=rx2) + self.service.publish(nem1, event) + self.service.publish(nem2, event) + class EmaneGlobalModel: """ diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index cdcd9686..1d967d49 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -149,6 +149,8 @@ service CoreApi { } rpc GetEmaneEventChannel (emane.GetEmaneEventChannelRequest) returns (emane.GetEmaneEventChannelResponse) { } + rpc EmanePathlosses (stream emane.EmanePathlossesRequest) returns (emane.EmanePathlossesResponse) { + } // xml rpc rpc SaveXml (SaveXmlRequest) returns (SaveXmlResponse) { diff --git a/daemon/proto/core/api/grpc/emane.proto b/daemon/proto/core/api/grpc/emane.proto index 33cb1a2a..8c3ee4ca 100644 --- a/daemon/proto/core/api/grpc/emane.proto +++ b/daemon/proto/core/api/grpc/emane.proto @@ -90,3 +90,16 @@ message EmaneModelConfig { string model = 3; map config = 4; } + +message EmanePathlossesRequest { + int32 session_id = 1; + int32 node_one = 2; + float rx_one = 3; + int32 interface_one_id = 4; + int32 node_two = 5; + float rx_two = 6; + int32 interface_two_id = 7; +} + +message EmanePathlossesResponse { +} From 29d09c8397b71b54966566e1cf16b2d09e92c5cc Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 3 Jun 2020 14:58:29 -0700 Subject: [PATCH 0305/1131] updates to move_nodes and emane_pathlosses type hinting and naming --- daemon/core/api/grpc/client.py | 6 +++--- daemon/core/api/grpc/server.py | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 6aaf7fac..1b353bd7 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -1232,15 +1232,15 @@ class CoreGrpcClient: return self.stub.WlanLink(request) def emane_pathlosses( - self, pathloss_iter: Iterable[EmanePathlossesRequest] + self, pathloss_iterator: Iterable[EmanePathlossesRequest] ) -> EmanePathlossesResponse: """ Stream EMANE pathloss events. - :param pathloss_iter: iterator for sending EMANE pathloss events + :param pathloss_iterator: iterator for sending EMANE pathloss events :return: EMANE pathloss response """ - return self.stub.EmanePathlosses(pathloss_iter) + return self.stub.EmanePathlosses(pathloss_iterator) def connect(self) -> None: """ diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 1d13ec63..fcf69d99 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -695,7 +695,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): return core_pb2.GetNodeResponse(node=node_proto, interfaces=interfaces) def MoveNodes( - self, request_iterator, context: ServicerContext + self, + request_iterator: Iterable[core_pb2.MoveNodesRequest], + context: ServicerContext, ) -> core_pb2.MoveNodesResponse: """ Stream node movements From 7b2dd59c81edeb10caa982c22e0bd297c3b7c7ec Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 4 Jun 2020 13:48:25 -0700 Subject: [PATCH 0306/1131] grpc: node_command improvements to include return code and options for wait and shell when running commands --- daemon/core/api/grpc/client.py | 15 +++++++++++++-- daemon/core/api/grpc/server.py | 6 ++++-- daemon/proto/core/api/grpc/core.proto | 3 +++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 1b353bd7..68310c67 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -597,7 +597,12 @@ class CoreGrpcClient: return self.stub.DeleteNode(request) def node_command( - self, session_id: int, node_id: int, command: str + self, + session_id: int, + node_id: int, + command: str, + wait: bool = True, + shell: bool = False, ) -> core_pb2.NodeCommandResponse: """ Send command to a node and get the output. @@ -605,11 +610,17 @@ class CoreGrpcClient: :param session_id: session id :param node_id: node id :param command: command to run on node + :param wait: wait for command to complete + :param shell: send shell command :return: response with command combined stdout/stderr :raises grpc.RpcError: when session or node doesn't exist """ request = core_pb2.NodeCommandRequest( - session_id=session_id, node_id=node_id, command=command + session_id=session_id, + node_id=node_id, + command=command, + wait=wait, + shell=shell, ) return self.stub.NodeCommand(request) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index fcf69d99..f85529e6 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -796,10 +796,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session = self.get_session(request.session_id, context) node = self.get_node(session, request.node_id, context, CoreNode) try: - output = node.cmd(request.command) + output = node.cmd(request.command, request.wait, request.shell) + return_code = 0 except CoreCommandError as e: output = e.stderr - return core_pb2.NodeCommandResponse(output=output) + return_code = e.returncode + return core_pb2.NodeCommandResponse(output=output, return_code=return_code) def GetNodeTerminal( self, request: core_pb2.GetNodeTerminalRequest, context: ServicerContext diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 1d967d49..d602f9d3 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -467,10 +467,13 @@ message NodeCommandRequest { int32 session_id = 1; int32 node_id = 2; string command = 3; + bool wait = 4; + bool shell = 5; } message NodeCommandResponse { string output = 1; + int32 return_code = 2; } message GetNodeLinksRequest { From eaa05c34babf0e5bc7d87b229aa4dfd746134794 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 4 Jun 2020 21:14:11 -0700 Subject: [PATCH 0307/1131] avoid piping subprocess command output when not waiting for results --- daemon/core/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/daemon/core/utils.py b/daemon/core/utils.py index 8a988ede..c16d18b5 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -228,7 +228,8 @@ def cmd( if shell is False: args = shlex.split(args) try: - p = Popen(args, stdout=PIPE, stderr=PIPE, env=env, cwd=cwd, shell=shell) + output = PIPE if wait else DEVNULL + p = Popen(args, stdout=output, stderr=output, env=env, cwd=cwd, shell=shell) if wait: stdout, stderr = p.communicate() stdout = stdout.decode("utf-8").strip() From 9a5fc94ba22406e12dd84ebb72acd95ff47820fc Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 5 Jun 2020 08:44:19 -0700 Subject: [PATCH 0308/1131] improvements for grpc docs and upates to grpc client pydocs --- daemon/core/api/grpc/client.py | 97 ++++++++++++++++++++++++++++++---- 1 file changed, 86 insertions(+), 11 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 68310c67..cabd6dda 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -1,5 +1,5 @@ """ -gRpc client for interfacing with CORE, when gRPC mode is enabled. +gRpc client for interfacing with CORE. """ import logging @@ -289,6 +289,7 @@ class CoreGrpcClient: :param session_id: id of session :return: stop session response + :raises grpc.RpcError: when session doesn't exist """ request = core_pb2.StopSessionRequest(session_id=session_id) return self.stub.StopSession(request) @@ -581,6 +582,7 @@ class CoreGrpcClient: :param move_iterator: iterator for generating node movements :return: move nodes response + :raises grpc.RpcError: when session or nodes do not exist """ return self.stub.MoveNodes(move_iterator) @@ -1122,9 +1124,9 @@ class CoreGrpcClient: def get_emane_model_configs(self, session_id: int) -> GetEmaneModelConfigsResponse: """ - Get all emane model configurations for a session. + Get all EMANE model configurations for a session. - :param session_id: session id + :param session_id: session to get emane model configs :return: response with a dictionary of node/interface ids to configurations :raises grpc.RpcError: when session doesn't exist """ @@ -1135,9 +1137,10 @@ class CoreGrpcClient: """ Save the current scenario to an XML file. - :param session_id: session id + :param session_id: session to save xml file for :param file_path: local path to save scenario XML file to :return: nothing + :raises grpc.RpcError: when session doesn't exist """ request = core_pb2.SaveXmlRequest(session_id=session_id) response = self.stub.SaveXml(request) @@ -1163,11 +1166,12 @@ class CoreGrpcClient: """ Helps broadcast wireless link/unlink between EMANE nodes. - :param session_id: session id - :param nem_one: - :param nem_two: + :param session_id: session to emane link + :param nem_one: first nem for emane link + :param nem_two: second nem for emane link :param linked: True to link, False to unlink - :return: core_pb2.EmaneLinkResponse + :return: get emane link response + :raises grpc.RpcError: when session or nodes related to nems do not exist """ request = EmaneLinkRequest( session_id=session_id, nem_one=nem_one, nem_two=nem_two, linked=linked @@ -1179,30 +1183,57 @@ class CoreGrpcClient: Retrieves a list of interfaces available on the host machine that are not a part of a CORE session. - :return: core_pb2.GetInterfacesResponse + :return: get interfaces response """ request = core_pb2.GetInterfacesRequest() return self.stub.GetInterfaces(request) def get_config_services(self) -> GetConfigServicesResponse: + """ + Retrieve all known config services. + + :return: get config services response + """ request = GetConfigServicesRequest() return self.stub.GetConfigServices(request) def get_config_service_defaults( self, name: str ) -> GetConfigServiceDefaultsResponse: + """ + Retrieves config service default values. + + :param name: name of service to get defaults for + :return: get config service defaults + """ request = GetConfigServiceDefaultsRequest(name=name) return self.stub.GetConfigServiceDefaults(request) def get_node_config_service_configs( self, session_id: int ) -> GetNodeConfigServiceConfigsResponse: + """ + Retrieves all node config service configurations for a session. + + :param session_id: session to get config service configurations for + :return: get node config service configs response + :raises grpc.RpcError: when session doesn't exist + """ request = GetNodeConfigServiceConfigsRequest(session_id=session_id) return self.stub.GetNodeConfigServiceConfigs(request) def get_node_config_service( self, session_id: int, node_id: int, name: str ) -> GetNodeConfigServiceResponse: + """ + Retrieves information for a specific config service on a node. + + :param session_id: session node belongs to + :param node_id: id of node to get service information from + :param name: name of service + :return: get node config service response + :raises grpc.RpcError: when session or node doesn't exist + """ request = GetNodeConfigServiceRequest( session_id=session_id, node_id=node_id, name=name ) @@ -1211,28 +1242,70 @@ class CoreGrpcClient: def get_node_config_services( self, session_id: int, node_id: int ) -> GetNodeConfigServicesResponse: + """ + Retrieves the config services currently assigned to a node. + + :param session_id: session node belongs to + :param node_id: id of node to get config services for + :return: get node config services response + :raises grpc.RpcError: when session or node doesn't exist + """ request = GetNodeConfigServicesRequest(session_id=session_id, node_id=node_id) return self.stub.GetNodeConfigServices(request) def set_node_config_service( self, session_id: int, node_id: int, name: str, config: Dict[str, str] ) -> SetNodeConfigServiceResponse: + """ + Assigns a config service to a node with the provided configuration. + + :param session_id: session node belongs to + :param node_id: id of node to assign config service to + :param name: name of service + :param config: service configuration + :return: set node config service response + :raises grpc.RpcError: when session or node doesn't exist + """ request = SetNodeConfigServiceRequest( session_id=session_id, node_id=node_id, name=name, config=config ) return self.stub.SetNodeConfigService(request) def get_emane_event_channel(self, session_id: int) -> GetEmaneEventChannelResponse: + """ + Retrieves the current emane event channel being used for a session. + + :param session_id: session to get emane event channel for + :return: emane event channel response + :raises grpc.RpcError: when session doesn't exist + """ request = GetEmaneEventChannelRequest(session_id=session_id) return self.stub.GetEmaneEventChannel(request) def execute_script(self, script: str) -> ExecuteScriptResponse: + """ + Executes a python script given context of the current CoreEmu object. + + :param script: script to execute + :return: execute script response + """ request = ExecuteScriptRequest(script=script) return self.stub.ExecuteScript(request) def wlan_link( self, session_id: int, wlan: int, node_one: int, node_two: int, linked: bool ) -> WlanLinkResponse: + """ + Links/unlinks nodes on the same WLAN. + + :param session_id: session id containing wlan and nodes + :param wlan: wlan nodes must belong to + :param node_one: first node of pair to link/unlink + :param node_two: second node of pair to link/unlin + :param linked: True to link, False to unlink + :return: wlan link response + :raises grpc.RpcError: when session or one of the nodes do not exist + """ request = WlanLinkRequest( session_id=session_id, wlan=wlan, @@ -1248,8 +1321,10 @@ class CoreGrpcClient: """ Stream EMANE pathloss events. - :param pathloss_iterator: iterator for sending EMANE pathloss events - :return: EMANE pathloss response + :param pathloss_iterator: iterator for sending emane pathloss events + :return: emane pathloss response + :raises grpc.RpcError: when a pathloss event session or one of the nodes do not + exist """ return self.stub.EmanePathlosses(pathloss_iterator) From 75d5bced9cd5b8c5bb6f044b93e2b992fc7464c2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 5 Jun 2020 11:20:23 -0700 Subject: [PATCH 0309/1131] grpc doc improvements, grpc examples additions, small tweak to grpc client for setting emane models not requiring a config when using default configuration --- daemon/core/api/grpc/client.py | 2 +- daemon/examples/grpc/distributed_switch.py | 21 +++--- daemon/examples/grpc/emane80211.py | 74 +++++++++++++++++++ daemon/examples/grpc/large.py | 54 -------------- daemon/examples/grpc/switch.py | 57 +++++++++------ daemon/examples/grpc/wlan.py | 82 ++++++++++++++++++++++ docs/grpc.md | 78 ++++++-------------- 7 files changed, 220 insertions(+), 148 deletions(-) create mode 100644 daemon/examples/grpc/emane80211.py delete mode 100644 daemon/examples/grpc/large.py create mode 100644 daemon/examples/grpc/wlan.py diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index cabd6dda..c1d0e2fd 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -1100,7 +1100,7 @@ class CoreGrpcClient: session_id: int, node_id: int, model: str, - config: Dict[str, str], + config: Dict[str, str] = None, interface_id: int = -1, ) -> SetEmaneModelConfigResponse: """ diff --git a/daemon/examples/grpc/distributed_switch.py b/daemon/examples/grpc/distributed_switch.py index 9cc35f72..0477efdd 100644 --- a/daemon/examples/grpc/distributed_switch.py +++ b/daemon/examples/grpc/distributed_switch.py @@ -1,7 +1,8 @@ import argparse import logging -from core.api.grpc import client, core_pb2 +from core.api.grpc import client +from core.api.grpc.core_pb2 import Node, NodeType, Position, SessionState def log_event(event): @@ -26,13 +27,11 @@ def main(args): core.events(session_id, log_event) # change session state - response = core.set_session_state( - session_id, core_pb2.SessionState.CONFIGURATION - ) + response = core.set_session_state(session_id, SessionState.CONFIGURATION) logging.info("set session state: %s", response) # create switch node - switch = core_pb2.Node(type=core_pb2.NodeType.SWITCH) + switch = Node(type=NodeType.SWITCH) response = core.add_node(session_id, switch) logging.info("created switch: %s", response) switch_id = response.node_id @@ -41,8 +40,8 @@ def main(args): interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/16") # create node one - position = core_pb2.Position(x=100, y=50) - node = core_pb2.Node(position=position) + position = Position(x=100, y=50) + node = Node(position=position) response = core.add_node(session_id, node) logging.info("created node one: %s", response) node_one_id = response.node_id @@ -53,8 +52,8 @@ def main(args): logging.info("created link from node one to switch: %s", response) # create node two - position = core_pb2.Position(x=200, y=50) - node = core_pb2.Node(position=position, server=server_name) + position = Position(x=200, y=50) + node = Node(position=position, server=server_name) response = core.add_node(session_id, node) logging.info("created node two: %s", response) node_two_id = response.node_id @@ -65,9 +64,7 @@ def main(args): logging.info("created link from node two to switch: %s", response) # change session state - response = core.set_session_state( - session_id, core_pb2.SessionState.INSTANTIATION - ) + response = core.set_session_state(session_id, SessionState.INSTANTIATION) logging.info("set session state: %s", response) diff --git a/daemon/examples/grpc/emane80211.py b/daemon/examples/grpc/emane80211.py new file mode 100644 index 00000000..5656268c --- /dev/null +++ b/daemon/examples/grpc/emane80211.py @@ -0,0 +1,74 @@ +""" +Example using gRPC API to create a simple EMANE 80211 network. +""" + +import logging + +from core.api.grpc import client +from core.api.grpc.core_pb2 import Node, NodeType, Position, SessionState +from core.emane.ieee80211abg import EmaneIeee80211abgModel + + +def log_event(event): + logging.info("event: %s", event) + + +def main(): + # helper to create interface addresses + interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/24") + + # create grpc client and start connection context, which auto closes connection + core = client.CoreGrpcClient() + with core.context_connect(): + # create session + response = core.create_session() + logging.info("created session: %s", response) + + # handle events session may broadcast + session_id = response.session_id + core.events(session_id, log_event) + + # change session state to configuration so that nodes get started when added + response = core.set_session_state(session_id, SessionState.CONFIGURATION) + logging.info("set session state: %s", response) + + # create emane node + position = Position(x=200, y=200) + emane = Node(type=NodeType.EMANE, position=position) + response = core.add_node(session_id, emane) + logging.info("created emane: %s", response) + emane_id = response.node_id + + # an emane model must be configured for use, by the emane node + core.set_emane_model_config(session_id, emane_id, EmaneIeee80211abgModel.name) + + # create node one + position = Position(x=100, y=100) + node1 = Node(type=NodeType.DEFAULT, position=position) + response = core.add_node(session_id, node1) + logging.info("created node: %s", response) + node1_id = response.node_id + + # create node two + position = Position(x=300, y=100) + node2 = Node(type=NodeType.DEFAULT, position=position) + response = core.add_node(session_id, node2) + logging.info("created node: %s", response) + node2_id = response.node_id + + # links nodes to switch + interface_one = interface_helper.create_interface(node1_id, 0) + response = core.add_link(session_id, node1_id, emane_id, interface_one) + logging.info("created link: %s", response) + interface_one = interface_helper.create_interface(node2_id, 0) + response = core.add_link(session_id, node2_id, emane_id, interface_one) + logging.info("created link: %s", response) + + # change session state + response = core.set_session_state(session_id, SessionState.INSTANTIATION) + logging.info("set session state: %s", response) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + main() diff --git a/daemon/examples/grpc/large.py b/daemon/examples/grpc/large.py deleted file mode 100644 index ef1e6cc4..00000000 --- a/daemon/examples/grpc/large.py +++ /dev/null @@ -1,54 +0,0 @@ -import logging - -from core.api.grpc import client, core_pb2 - - -def log_event(event): - logging.info("event: %s", event) - - -def main(): - core = client.CoreGrpcClient() - - with core.context_connect(): - # create session - response = core.create_session() - session_id = response.session_id - logging.info("created session: %s", response) - - # create nodes for session - nodes = [] - position = core_pb2.Position(x=50, y=100) - switch = core_pb2.Node(id=1, type=core_pb2.NodeType.SWITCH, position=position) - nodes.append(switch) - for i in range(2, 50): - position = core_pb2.Position(x=50 + 50 * i, y=50) - node = core_pb2.Node(id=i, position=position, model="PC") - nodes.append(node) - - # create links - interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/16") - links = [] - for node in nodes: - interface_one = interface_helper.create_interface(node.id, 0) - link = core_pb2.Link( - type=core_pb2.LinkType.WIRED, - node_one_id=node.id, - node_two_id=switch.id, - interface_one=interface_one, - ) - links.append(link) - - # start session - response = core.start_session(session_id, nodes, links) - logging.info("started session: %s", response) - - input("press enter to shutdown session") - - response = core.stop_session(session_id) - logging.info("stop sessionL %s", response) - - -if __name__ == "__main__": - logging.basicConfig(level=logging.DEBUG) - main() diff --git a/daemon/examples/grpc/switch.py b/daemon/examples/grpc/switch.py index 48aa63bc..3ab0e0ba 100644 --- a/daemon/examples/grpc/switch.py +++ b/daemon/examples/grpc/switch.py @@ -1,6 +1,11 @@ +""" +Example using gRPC API to create a simple switch network. +""" + import logging -from core.api.grpc import client, core_pb2 +from core.api.grpc import client +from core.api.grpc.core_pb2 import Node, NodeType, Position, SessionState def log_event(event): @@ -8,8 +13,11 @@ def log_event(event): def main(): - core = client.CoreGrpcClient() + # helper to create interface addresses + interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/24") + # create grpc client and start connection context, which auto closes connection + core = client.CoreGrpcClient() with core.context_connect(): # create session response = core.create_session() @@ -19,38 +27,41 @@ def main(): session_id = response.session_id core.events(session_id, log_event) - # change session state - response = core.set_session_state( - session_id, core_pb2.SessionState.CONFIGURATION - ) + # change session state to configuration so that nodes get started when added + response = core.set_session_state(session_id, SessionState.CONFIGURATION) logging.info("set session state: %s", response) # create switch node - switch = core_pb2.Node(type=core_pb2.NodeType.SWITCH) + position = Position(x=200, y=200) + switch = Node(type=NodeType.SWITCH, position=position) response = core.add_node(session_id, switch) logging.info("created switch: %s", response) switch_id = response.node_id - # helper to create interfaces - interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/16") + # create node one + position = Position(x=100, y=100) + node1 = Node(type=NodeType.DEFAULT, position=position) + response = core.add_node(session_id, node1) + logging.info("created node: %s", response) + node1_id = response.node_id - for i in range(2): - # create node - position = core_pb2.Position(x=50 + 50 * i, y=50) - node = core_pb2.Node(position=position) - response = core.add_node(session_id, node) - logging.info("created node: %s", response) - node_id = response.node_id + # create node two + position = Position(x=300, y=100) + node2 = Node(type=NodeType.DEFAULT, position=position) + response = core.add_node(session_id, node2) + logging.info("created node: %s", response) + node2_id = response.node_id - # create link - interface_one = interface_helper.create_interface(node_id, 0) - response = core.add_link(session_id, node_id, switch_id, interface_one) - logging.info("created link: %s", response) + # links nodes to switch + interface_one = interface_helper.create_interface(node1_id, 0) + response = core.add_link(session_id, node1_id, switch_id, interface_one) + logging.info("created link: %s", response) + interface_one = interface_helper.create_interface(node2_id, 0) + response = core.add_link(session_id, node2_id, switch_id, interface_one) + logging.info("created link: %s", response) # change session state - response = core.set_session_state( - session_id, core_pb2.SessionState.INSTANTIATION - ) + response = core.set_session_state(session_id, SessionState.INSTANTIATION) logging.info("set session state: %s", response) diff --git a/daemon/examples/grpc/wlan.py b/daemon/examples/grpc/wlan.py new file mode 100644 index 00000000..6118ae4c --- /dev/null +++ b/daemon/examples/grpc/wlan.py @@ -0,0 +1,82 @@ +""" +Example using gRPC API to create a simple wlan network. +""" + +import logging + +from core.api.grpc import client +from core.api.grpc.core_pb2 import Node, NodeType, Position, SessionState + + +def log_event(event): + logging.info("event: %s", event) + + +def main(): + # helper to create interface addresses + interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/24") + + # create grpc client and start connection context, which auto closes connection + core = client.CoreGrpcClient() + with core.context_connect(): + # create session + response = core.create_session() + logging.info("created session: %s", response) + + # handle events session may broadcast + session_id = response.session_id + core.events(session_id, log_event) + + # change session state to configuration so that nodes get started when added + response = core.set_session_state(session_id, SessionState.CONFIGURATION) + logging.info("set session state: %s", response) + + # create wlan node + position = Position(x=200, y=200) + wlan = Node(type=NodeType.WIRELESS_LAN, position=position) + response = core.add_node(session_id, wlan) + logging.info("created wlan: %s", response) + wlan_id = response.node_id + + # change/configure wlan if desired + # NOTE: error = loss, and named this way for legacy purposes for now + config = { + "bandwidth": "54000000", + "range": "500", + "jitter": "0", + "delay": "5000", + "error": "0", + } + response = core.set_wlan_config(session_id, wlan_id, config) + logging.info("set wlan config: %s", response) + + # create node one + position = Position(x=100, y=100) + node1 = Node(type=NodeType.DEFAULT, position=position) + response = core.add_node(session_id, node1) + logging.info("created node: %s", response) + node1_id = response.node_id + + # create node two + position = Position(x=300, y=100) + node2 = Node(type=NodeType.DEFAULT, position=position) + response = core.add_node(session_id, node2) + logging.info("created node: %s", response) + node2_id = response.node_id + + # links nodes to switch + interface_one = interface_helper.create_interface(node1_id, 0) + response = core.add_link(session_id, node1_id, wlan_id, interface_one) + logging.info("created link: %s", response) + interface_one = interface_helper.create_interface(node2_id, 0) + response = core.add_link(session_id, node2_id, wlan_id, interface_one) + logging.info("created link: %s", response) + + # change session state + response = core.set_session_state(session_id, SessionState.INSTANTIATION) + logging.info("set session state: %s", response) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + main() diff --git a/docs/grpc.md b/docs/grpc.md index 2430f3ae..69cf4aed 100644 --- a/docs/grpc.md +++ b/docs/grpc.md @@ -1,72 +1,34 @@ # Using the gRPC API -gRPC is the main API for interfacing with CORE. +[gRPC](https://grpc.io/) is the main API for interfacing with CORE and used by +the python GUI for driving all functionality. + +Currently we are providing a python client that wraps the generated files for +leveraging the API, but proto files noted below can also be leveraged to generate +bindings for other languages as well. ## HTTP Proxy -Since gRPC is HTTP2 based, proxy configurations can cause issue. Clear out your -proxy when running if needed. +Since gRPC is HTTP2 based, proxy configurations can cause issue. You can either +properly account for this issue or clear out your proxy when running if needed. ## Python Client -A python client wrapper is provided at **core.api.grpc.client.CoreGrpcClient**. +A python client wrapper is provided at +[CoreGrpcClient](../daemon/core/api/grpc/client.py) to help provide some +conveniences when using the API. -Below is a small example using it. +## Proto Files -```python -import logging +Proto files are used to define the API and protobuf messages that are used for +interfaces with this API. -from core.api.grpc import client, core_pb2 +They can be found [here](../daemon/proto/core/api/grpc) to see the specifics of +what is going on and response message values that would be returned. +## Examples -def log_event(event): - logging.info("event: %s", event) +Example usage of this API can be found [here](../daemon/examples/grpc). These +examples will create a session using the gRPC API when the core-daemon is running. - -def main(): - core = client.CoreGrpcClient() - - with core.context_connect(): - # create session - response = core.create_session() - logging.info("created session: %s", response) - - # handle events session may broadcast - session_id = response.session_id - core.events(session_id, log_event) - - # change session state - response = core.set_session_state(session_id, core_pb2.SessionState.CONFIGURATION) - logging.info("set session state: %s", response) - - # create switch node - switch = core_pb2.Node(type=core_pb2.NodeType.SWITCH) - response = core.add_node(session_id, switch) - logging.info("created switch: %s", response) - switch_id = response.node_id - - # helper to create interfaces - interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/16") - - for i in range(2): - # create node - position = core_pb2.Position(x=50 + 50 * i, y=50) - node = core_pb2.Node(position=position) - response = core.add_node(session_id, node) - logging.info("created node: %s", response) - node_id = response.node_id - - # create link - interface_one = interface_helper.create_interface(node_id, 0) - response = core.add_link(session_id, node_id, switch_id, interface_one) - logging.info("created link: %s", response) - - # change session state - response = core.set_session_state(session_id, core_pb2.SessionState.INSTANTIATION) - logging.info("set session state: %s", response) - - -if __name__ == "__main__": - logging.basicConfig(level=logging.DEBUG) - main() -``` +You can then switch to and attach to these sessions using either of the CORE GUIs. From bf1bc511e28b429355ef2a5d20a704f4aaf714be Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 5 Jun 2020 14:34:19 -0700 Subject: [PATCH 0310/1131] removed configuration option for number of for corehandler threads as it cannot properly deal with anything more than 1, updated man pages to current 6.4 versions for now --- daemon/core/api/tlv/corehandlers.py | 12 +++--------- daemon/data/core.conf | 1 - daemon/scripts/core-daemon | 11 +---------- daemon/scripts/core-imn-to-xml | 2 +- daemon/scripts/core-manage | 2 +- daemon/scripts/core-pygui | 2 +- daemon/scripts/core-route-monitor | 2 +- daemon/scripts/core-service-update | 2 +- daemon/scripts/coresendmsg | 2 +- daemon/tests/conftest.py | 6 +++--- man/core-cleanup.1 | 6 +++--- man/core-daemon.1 | 24 +++++++++++------------- man/core-gui.1 | 6 +++--- man/core-manage.1 | 6 +++--- man/coresendmsg.1 | 10 +++++----- man/netns.1 | 6 +++--- man/vcmd.1 | 6 +++--- man/vnoded.1 | 6 +++--- 18 files changed, 47 insertions(+), 65 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 1a22cedd..7e2cd040 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -79,15 +79,9 @@ class CoreHandler(socketserver.BaseRequestHandler): self._sessions_lock = threading.Lock() self.handler_threads = [] - num_threads = int(server.config["numthreads"]) - if num_threads < 1: - raise ValueError(f"invalid number of threads: {num_threads}") - - logging.debug("launching core server handler threads: %s", num_threads) - for _ in range(num_threads): - thread = threading.Thread(target=self.handler_thread) - self.handler_threads.append(thread) - thread.start() + thread = threading.Thread(target=self.handler_thread, daemon=True) + thread.start() + self.handler_threads.append(thread) self.session = None self.coreemu = server.coreemu diff --git a/daemon/data/core.conf b/daemon/data/core.conf index 13b50785..5ff0be7f 100644 --- a/daemon/data/core.conf +++ b/daemon/data/core.conf @@ -4,7 +4,6 @@ listenaddr = localhost port = 4038 grpcaddress = localhost grpcport = 50051 -numthreads = 1 quagga_bin_search = "/usr/local/bin /usr/bin /usr/lib/quagga" quagga_sbin_search = "/usr/local/sbin /usr/sbin /usr/lib/quagga" frr_bin_search = "/usr/local/bin /usr/bin /usr/lib/frr" diff --git a/daemon/scripts/core-daemon b/daemon/scripts/core-daemon index 9f738467..a95e59fa 100755 --- a/daemon/scripts/core-daemon +++ b/daemon/scripts/core-daemon @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """ core-daemon: the CORE daemon is a server process that receives CORE API messages and instantiates emulated nodes and networks within the kernel. Various @@ -93,12 +93,10 @@ def get_merged_config(filename): # these are the defaults used in the config file default_log = os.path.join(constants.CORE_CONF_DIR, "logging.conf") default_grpc_port = "50051" - default_threads = "1" default_address = "localhost" defaults = { "port": str(CORE_API_PORT), "listenaddr": default_address, - "numthreads": default_threads, "grpcport": default_grpc_port, "grpcaddress": default_address, "logfile": default_log @@ -110,8 +108,6 @@ def get_merged_config(filename): help=f"read config from specified file; default = {filename}") parser.add_argument("-p", "--port", dest="port", type=int, help=f"port number to listen on; default = {CORE_API_PORT}") - parser.add_argument("-n", "--numthreads", dest="numthreads", type=int, - help=f"number of server threads; default = {default_threads}") parser.add_argument("--ovs", action="store_true", help="enable experimental ovs mode, default is false") parser.add_argument("--grpc-port", dest="grpcport", help=f"grpc port to listen on; default {default_grpc_port}") @@ -148,14 +144,9 @@ def main(): :return: nothing """ - # get a configuration merged from config file and command-line arguments cfg = get_merged_config(f"{CORE_CONF_DIR}/core.conf") - - # load logging configuration load_logging_config(cfg["logfile"]) - banner() - try: cored(cfg) except KeyboardInterrupt: diff --git a/daemon/scripts/core-imn-to-xml b/daemon/scripts/core-imn-to-xml index 725d7119..495093ed 100755 --- a/daemon/scripts/core-imn-to-xml +++ b/daemon/scripts/core-imn-to-xml @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import argparse import re import sys diff --git a/daemon/scripts/core-manage b/daemon/scripts/core-manage index 14e10e5b..5587c9ae 100755 --- a/daemon/scripts/core-manage +++ b/daemon/scripts/core-manage @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """ core-manage: Helper tool to add, remove, or check for services, models, and node types in a CORE installation. diff --git a/daemon/scripts/core-pygui b/daemon/scripts/core-pygui index f30b531b..46860ce9 100755 --- a/daemon/scripts/core-pygui +++ b/daemon/scripts/core-pygui @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import argparse import logging from logging.handlers import TimedRotatingFileHandler diff --git a/daemon/scripts/core-route-monitor b/daemon/scripts/core-route-monitor index a9b48aff..b12e6205 100755 --- a/daemon/scripts/core-route-monitor +++ b/daemon/scripts/core-route-monitor @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import argparse import enum import select diff --git a/daemon/scripts/core-service-update b/daemon/scripts/core-service-update index 6d0be06c..d0ca863f 100755 --- a/daemon/scripts/core-service-update +++ b/daemon/scripts/core-service-update @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import argparse import re from io import TextIOWrapper diff --git a/daemon/scripts/coresendmsg b/daemon/scripts/coresendmsg index ae89ecb1..13e20b5c 100755 --- a/daemon/scripts/coresendmsg +++ b/daemon/scripts/coresendmsg @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """ coresendmsg: utility for generating CORE messages """ diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index 0c021b25..9d54d9c2 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -44,8 +44,8 @@ class PatchManager: class MockServer: - def __init__(self, config, coreemu): - self.config = config + def __init__(self, coreemu): + self.config = {} self.coreemu = coreemu @@ -108,7 +108,7 @@ def module_grpc(global_coreemu): def module_coretlv(patcher, global_coreemu, global_session): request_mock = MagicMock() request_mock.fileno = MagicMock(return_value=1) - server = MockServer({"numthreads": "1"}, global_coreemu) + server = MockServer(global_coreemu) request_handler = CoreHandler(request_mock, "", server) request_handler.session = global_session request_handler.add_session_handlers() diff --git a/man/core-cleanup.1 b/man/core-cleanup.1 index 64aa18fb..0f56c14c 100644 --- a/man/core-cleanup.1 +++ b/man/core-cleanup.1 @@ -1,7 +1,7 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3. -.TH CORE-CLEANUP "1" "June 2019" "CORE" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.6. +.TH CORE-CLEANUP "1" "June 2020" "CORE" "User Commands" .SH NAME -core-cleanup \- manual page for core-cleanup 5.3.0 +core-cleanup \- manual page for core-cleanup 6.4.0 .SH DESCRIPTION usage: ../daemon/scripts/core\-cleanup [\-d [\-l]] .IP diff --git a/man/core-daemon.1 b/man/core-daemon.1 index c8061e0d..01799894 100644 --- a/man/core-daemon.1 +++ b/man/core-daemon.1 @@ -1,14 +1,14 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3. -.TH CORE-DAEMON "1" "June 2019" "CORE" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.6. +.TH CORE-DAEMON "1" "June 2020" "CORE" "User Commands" .SH NAME -core-daemon \- manual page for core-daemon 5.3.0 +core-daemon \- manual page for core-daemon 6.4.0 .SH DESCRIPTION -usage: core\-daemon [\-h] [\-f CONFIGFILE] [\-p PORT] [\-n NUMTHREADS] [\-\-ovs] +usage: core\-daemon [\-h] [\-f CONFIGFILE] [\-p PORT] [\-\-ovs] .IP -[\-\-grpc] [\-\-grpc\-port GRPCPORT] -[\-\-grpc\-address GRPCADDRESS] +[\-\-grpc\-port GRPCPORT] [\-\-grpc\-address GRPCADDRESS] +[\-l LOGFILE] .PP -CORE daemon v.5.3.0 instantiates Linux network namespace nodes. +CORE daemon v.6.4.0 instantiates Linux network namespace nodes. .SS "optional arguments:" .TP \fB\-h\fR, \fB\-\-help\fR @@ -21,17 +21,15 @@ read config from specified file; default = \fB\-p\fR PORT, \fB\-\-port\fR PORT port number to listen on; default = 4038 .TP -\fB\-n\fR NUMTHREADS, \fB\-\-numthreads\fR NUMTHREADS -number of server threads; default = 1 -.TP \fB\-\-ovs\fR enable experimental ovs mode, default is false .TP -\fB\-\-grpc\fR -enable grpc api, default is false -.TP \fB\-\-grpc\-port\fR GRPCPORT grpc port to listen on; default 50051 .TP \fB\-\-grpc\-address\fR GRPCADDRESS grpc address to listen on; default localhost +.TP +\fB\-l\fR LOGFILE, \fB\-\-logfile\fR LOGFILE +core logging configuration; default +\fI\,/etc/core/logging.conf\/\fP diff --git a/man/core-gui.1 b/man/core-gui.1 index 5792e0c6..2a7b95ac 100644 --- a/man/core-gui.1 +++ b/man/core-gui.1 @@ -1,7 +1,7 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3. -.TH CORE-GUI "1" "June 2019" "CORE" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.6. +.TH CORE-GUI "1" "June 2020" "CORE" "User Commands" .SH NAME -core-gui \- manual page for core-gui version 5.3.0 (20190607) +core-gui \- manual page for core-gui version 6.4.0 (20200513) .SH SYNOPSIS .B core-gui [\fI\,-h|-v\/\fR] [\fI\,-b|-c \/\fR] [\fI\,-s\/\fR] [\fI\,-a address\/\fR] [\fI\,-p port\/\fR] diff --git a/man/core-manage.1 b/man/core-manage.1 index 7e7c6f42..594a5f64 100644 --- a/man/core-manage.1 +++ b/man/core-manage.1 @@ -1,7 +1,7 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3. -.TH CORE-MANAGE "1" "June 2019" "CORE" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.6. +.TH CORE-MANAGE "1" "June 2020" "CORE" "User Commands" .SH NAME -core-manage \- manual page for core-manage 5.3.0 +core-manage \- manual page for core-manage 6.4.0 .SH SYNOPSIS .B core-manage [\fI\,-h\/\fR] [\fI\,options\/\fR] \fI\, \/\fR diff --git a/man/coresendmsg.1 b/man/coresendmsg.1 index 9a42e29a..8815b189 100644 --- a/man/coresendmsg.1 +++ b/man/coresendmsg.1 @@ -1,17 +1,17 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3. -.TH CORESENDMSG "1" "June 2019" "CORE" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.6. +.TH CORESENDMSG "1" "June 2020" "CORE" "User Commands" .SH NAME -coresendmsg \- manual page for coresendmsg 5.3.0 +coresendmsg \- manual page for coresendmsg 6.4.0 .SH SYNOPSIS .B coresendmsg [\fI\,-h|-H\/\fR] [\fI\,options\/\fR] [\fI\,message-type\/\fR] [\fI\,flags=flags\/\fR] [\fI\,message-TLVs\/\fR] .SH DESCRIPTION .SS "Supported message types:" .IP -['NODE', 'LINK', 'EXECUTE', 'REGISTER', 'CONFIG', 'FILE', 'INTERFACE', 'EVENT', 'SESSION', 'EXCEPTION'] +node link execute register config file interface event session exception .SS "Supported message flags (flags=f1,f2,...):" .IP -['ADD', 'DELETE', 'CRI', 'LOCAL', 'STRING', 'TEXT', 'TTY'] +none add delete cri local string text tty .SH OPTIONS .TP \fB\-h\fR, \fB\-\-help\fR diff --git a/man/netns.1 b/man/netns.1 index 5cb6e312..abc62ee2 100644 --- a/man/netns.1 +++ b/man/netns.1 @@ -1,7 +1,7 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3. -.TH NETNS "1" "June 2019" "CORE" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.6. +.TH NETNS "1" "June 2020" "CORE" "User Commands" .SH NAME -netns \- manual page for netns version 5.3.0 +netns \- manual page for netns version 6.4.0 .SH SYNOPSIS .B netns [\fI\,-h|-V\/\fR] [\fI\,-w\/\fR] \fI\,-- command \/\fR[\fI\,args\/\fR...] diff --git a/man/vcmd.1 b/man/vcmd.1 index 79f14f50..f5438978 100644 --- a/man/vcmd.1 +++ b/man/vcmd.1 @@ -1,7 +1,7 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3. -.TH VCMD "1" "June 2019" "CORE" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.6. +.TH VCMD "1" "June 2020" "CORE" "User Commands" .SH NAME -vcmd \- manual page for vcmd version 5.3.0 +vcmd \- manual page for vcmd version 6.4.0 .SH SYNOPSIS .B vcmd [\fI\,-h|-V\/\fR] [\fI\,-v\/\fR] [\fI\,-q|-i|-I\/\fR] \fI\,-c -- command args\/\fR... diff --git a/man/vnoded.1 b/man/vnoded.1 index 841b00e9..11874e39 100644 --- a/man/vnoded.1 +++ b/man/vnoded.1 @@ -1,7 +1,7 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3. -.TH VNODED "1" "June 2019" "CORE" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.6. +.TH VNODED "1" "June 2020" "CORE" "User Commands" .SH NAME -vnoded \- manual page for vnoded version 5.3.0 +vnoded \- manual page for vnoded version 6.4.0 .SH SYNOPSIS .B vnoded [\fI\,-h|-V\/\fR] [\fI\,-v\/\fR] [\fI\,-n\/\fR] [\fI\,-C \/\fR] [\fI\,-l \/\fR] [\fI\,-p \/\fR] \fI\,-c \/\fR From 7ffbf457be6020c9934f434c11000db3b813cc2c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 8 Jun 2020 09:55:05 -0700 Subject: [PATCH 0311/1131] update to netclient existing bridge check to avoid using the -j flag, which requires version 4.7+ vs 4.5+ that we currently expect --- daemon/core/nodes/netclient.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 091938de..29a70d18 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -1,7 +1,6 @@ """ Clients for dealing with bridge/interface commands. """ -import json from typing import Callable import netaddr @@ -279,12 +278,13 @@ class LinuxNetClient: :param _id: node id to check bridges for :return: True if there are existing bridges, False otherwise """ - output = self.run(f"{IP_BIN} -j link show type bridge") - bridges = json.loads(output) - for bridge in bridges: - name = bridge.get("ifname") - if not name: + output = self.run(f"{IP_BIN} -o link show type bridge") + lines = output.split("\n") + for line in lines: + values = line.split(":") + if not len(values) >= 2: continue + name = values[1] fields = name.split(".") if len(fields) != 3: continue From 199c4618f5e40e77e0bb42677b0b883c60a136d1 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 8 Jun 2020 10:08:26 -0700 Subject: [PATCH 0312/1131] removed comments about rj45 removing addresses and setting to promiscuous, as that is not true and misleading --- docs/gui.md | 6 +----- docs/pygui.md | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/docs/gui.md b/docs/gui.md index 8f6f9057..85bbb6cd 100644 --- a/docs/gui.md +++ b/docs/gui.md @@ -371,11 +371,7 @@ be entered into the text box. > **NOTE:** When you press the Start button to instantiate your topology, the interface assigned to the RJ45 will be connected to the CORE topology. The - interface can no longer be used by the system. For example, if there was an - IP address assigned to the physical interface before execution, the address - will be removed and control given over to CORE. No IP address is needed; the - interface is put into promiscuous mode so it will receive all packets and - send them into the emulated world. + interface can no longer be used by the system. Multiple RJ45 nodes can be used within CORE and assigned to the same physical interface if 802.1x VLANs are used. This allows for more RJ45 nodes than diff --git a/docs/pygui.md b/docs/pygui.md index 4ed3fe09..f3e2c592 100644 --- a/docs/pygui.md +++ b/docs/pygui.md @@ -348,11 +348,7 @@ be entered into the text box. > **NOTE:** When you press the Start button to instantiate your topology, the interface assigned to the RJ45 will be connected to the CORE topology. The - interface can no longer be used by the system. For example, if there was an - IP address assigned to the physical interface before execution, the address - will be removed and control given over to CORE. No IP address is needed; the - interface is put into promiscuous mode so it will receive all packets and - send them into the emulated world. + interface can no longer be used by the system. Multiple RJ45 nodes can be used within CORE and assigned to the same physical interface if 802.1x VLANs are used. This allows for more RJ45 nodes than From 6ddf1ac9a460018515895245761873fc88abdb65 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 9 Jun 2020 00:56:34 -0700 Subject: [PATCH 0313/1131] removed IdGen class, added simple function to find next valid node id --- daemon/core/emulator/emudata.py | 9 --------- daemon/core/emulator/session.py | 21 ++++++++++++++------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index 4e3ebf8a..26e09fe8 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -10,15 +10,6 @@ from core.nodes.interface import CoreInterface from core.nodes.physical import PhysicalNode -class IdGen: - def __init__(self, _id: int = 0) -> None: - self.id = _id - - def next(self) -> int: - self.id += 1 - return self.id - - def link_config( node: Union[CoreNetworkBase, PhysicalNode], interface: CoreInterface, diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 9193196c..24fe05bd 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -20,7 +20,6 @@ from core.emane.nodes import EmaneNet from core.emulator.data import ConfigData, EventData, ExceptionData, FileData, LinkData from core.emulator.distributed import DistributedController from core.emulator.emudata import ( - IdGen, InterfaceData, LinkOptions, NodeOptions, @@ -111,7 +110,6 @@ class Session: self.link_colors = {} # dict of nodes: all nodes and nets - self.node_id_gen = IdGen() self.nodes = {} self._nodes_lock = threading.Lock() @@ -649,6 +647,19 @@ class Session: if node_two: node_two.lock.release() + def _next_node_id(self) -> int: + """ + Find the next valid node id, starting from 1. + + :return: next node id + """ + _id = 1 + while True: + if _id not in self.nodes: + break + _id += 1 + return _id + def add_node( self, _class: Type[NT], _id: int = None, options: NodeOptions = None ) -> NT: @@ -669,10 +680,7 @@ class Session: # determine node id if not _id: - while True: - _id = self.node_id_gen.next() - if _id not in self.nodes: - break + _id = self._next_node_id() # generate name if not provided if not options: @@ -1399,7 +1407,6 @@ class Session: self.sdt.delete_node(node.id) funcs.append((node.shutdown, [], {})) utils.threadpool(funcs) - self.node_id_gen.id = 0 def write_nodes(self) -> None: """ From 18044f947411938e3f8a20f4422b521f982e1a91 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 9 Jun 2020 08:48:18 -0700 Subject: [PATCH 0314/1131] daemon: cleaned up InterfaceData class, it now leverages dataclass, removed extra bloat and no longer requires parameters as they are optional --- daemon/core/api/grpc/grpcutils.py | 16 +++-- daemon/core/api/tlv/corehandlers.py | 4 +- daemon/core/emulator/emudata.py | 90 ++++++----------------------- daemon/tests/emane/test_emane.py | 4 +- daemon/tests/test_core.py | 2 +- 5 files changed, 31 insertions(+), 85 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index b0c1e614..cf3f2a95 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -59,19 +59,17 @@ def link_interface(interface_proto: core_pb2.Interface) -> InterfaceData: """ interface = None if interface_proto: - name = interface_proto.name - if name == "": - name = None - mac = interface_proto.mac - if mac == "": - mac = None + name = interface_proto.name if interface_proto.name else None + mac = interface_proto.mac if interface_proto.mac else None + ip4 = interface_proto.ip4 if interface_proto.ip4 else None + ip6 = interface_proto.ip6 if interface_proto.ip6 else None interface = InterfaceData( - _id=interface_proto.id, + id=interface_proto.id, name=name, mac=mac, - ip4=interface_proto.ip4, + ip4=ip4, ip4_mask=interface_proto.ip4mask, - ip6=interface_proto.ip6, + ip6=ip6, ip6_mask=interface_proto.ip6mask, ) return interface diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 7e2cd040..a79d7d6d 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -749,7 +749,7 @@ class CoreHandler(socketserver.BaseRequestHandler): node_two_id = message.get_tlv(LinkTlvs.N2_NUMBER.value) interface_one = InterfaceData( - _id=message.get_tlv(LinkTlvs.INTERFACE1_NUMBER.value), + id=message.get_tlv(LinkTlvs.INTERFACE1_NUMBER.value), name=message.get_tlv(LinkTlvs.INTERFACE1_NAME.value), mac=message.get_tlv(LinkTlvs.INTERFACE1_MAC.value), ip4=message.get_tlv(LinkTlvs.INTERFACE1_IP4.value), @@ -758,7 +758,7 @@ class CoreHandler(socketserver.BaseRequestHandler): ip6_mask=message.get_tlv(LinkTlvs.INTERFACE1_IP6_MASK.value), ) interface_two = InterfaceData( - _id=message.get_tlv(LinkTlvs.INTERFACE2_NUMBER.value), + id=message.get_tlv(LinkTlvs.INTERFACE2_NUMBER.value), name=message.get_tlv(LinkTlvs.INTERFACE2_NAME.value), mac=message.get_tlv(LinkTlvs.INTERFACE2_MAC.value), ip4=message.get_tlv(LinkTlvs.INTERFACE2_IP4.value), diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index 26e09fe8..3c33ce65 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -1,4 +1,5 @@ -from typing import List, Optional, Union +from dataclasses import dataclass +from typing import List, Union import netaddr @@ -122,87 +123,32 @@ class LinkOptions: self.opaque = None +@dataclass class InterfaceData: """ Convenience class for storing interface data. """ - def __init__( - self, - _id: int, - name: str, - mac: str, - ip4: str, - ip4_mask: int, - ip6: str, - ip6_mask: int, - ) -> None: - """ - Creates an InterfaceData object. - - :param _id: interface id - :param name: name for interface - :param mac: mac address - :param ip4: ipv4 address - :param ip4_mask: ipv4 bit mask - :param ip6: ipv6 address - :param ip6_mask: ipv6 bit mask - """ - self.id = _id - self.name = name - self.mac = mac - self.ip4 = ip4 - self.ip4_mask = ip4_mask - self.ip6 = ip6 - self.ip6_mask = ip6_mask - - def has_ip4(self) -> bool: - """ - Determines if interface has an ip4 address. - - :return: True if has ip4, False otherwise - """ - return all([self.ip4, self.ip4_mask]) - - def has_ip6(self) -> bool: - """ - Determines if interface has an ip6 address. - - :return: True if has ip6, False otherwise - """ - return all([self.ip6, self.ip6_mask]) - - def ip4_address(self) -> Optional[str]: - """ - Retrieve a string representation of the ip4 address and netmask. - - :return: ip4 string or None - """ - if self.has_ip4(): - return f"{self.ip4}/{self.ip4_mask}" - else: - return None - - def ip6_address(self) -> Optional[str]: - """ - Retrieve a string representation of the ip6 address and netmask. - - :return: ip4 string or None - """ - if self.has_ip6(): - return f"{self.ip6}/{self.ip6_mask}" - else: - return None + id: int = None + name: str = None + mac: str = None + ip4: str = None + ip4_mask: int = None + ip6: str = None + ip6_mask: int = None def get_addresses(self) -> List[str]: """ - Returns a list of ip4 and ip6 address when present. + Returns a list of ip4 and ip6 addresses when present. :return: list of addresses """ - ip4 = self.ip4_address() - ip6 = self.ip6_address() - return [i for i in [ip4, ip6] if i] + addresses = [] + if self.ip4 and self.ip4_mask: + addresses.append(f"{self.ip4}/{self.ip4_mask}") + if self.ip6 and self.ip6_mask: + addresses.append(f"{self.ip6}/{self.ip6_mask}") + return addresses class IpPrefixes: @@ -285,7 +231,7 @@ class IpPrefixes: mac = utils.random_mac() return InterfaceData( - _id=inteface_id, + id=inteface_id, name=name, ip4=ip4, ip4_mask=ip4_mask, diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py index 328aa94b..62fe15e1 100644 --- a/daemon/tests/emane/test_emane.py +++ b/daemon/tests/emane/test_emane.py @@ -29,7 +29,9 @@ _EMANE_MODELS = [ _DIR = os.path.dirname(os.path.abspath(__file__)) -def ping(from_node, to_node, ip_prefixes, count=3): +def ping( + from_node: CoreNode, to_node: CoreNode, ip_prefixes: IpPrefixes, count: int = 3 +): address = ip_prefixes.ip4_address(to_node) try: from_node.cmd(f"ping -c {count} {address}") diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 1c40393e..88a40906 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -21,7 +21,7 @@ _MOBILITY_FILE = os.path.join(_PATH, "mobility.scen") _WIRED = [PtpNet, HubNode, SwitchNode] -def ping(from_node, to_node, ip_prefixes): +def ping(from_node: CoreNode, to_node: CoreNode, ip_prefixes: IpPrefixes): address = ip_prefixes.ip4_address(to_node) try: from_node.cmd(f"ping -c 1 {address}") From b5e53e573ac48295a3017832ac8d300e0754c3aa Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 9 Jun 2020 09:12:31 -0700 Subject: [PATCH 0315/1131] daemon: LinkOptions now leverage dataclass and has type hinting, improve test_gui type hinting --- daemon/core/api/grpc/grpcutils.py | 10 ++------ daemon/core/api/tlv/corehandlers.py | 2 +- daemon/core/emulator/emudata.py | 40 ++++++++++++----------------- daemon/tests/test_gui.py | 14 +++++----- 4 files changed, 27 insertions(+), 39 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index cf3f2a95..7797c86e 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -86,13 +86,8 @@ def add_link_data( """ interface_one = link_interface(link_proto.interface_one) interface_two = link_interface(link_proto.interface_two) - - link_type = None - link_type_value = link_proto.type - if link_type_value is not None: - link_type = LinkTypes(link_type_value) - - options = LinkOptions(_type=link_type) + link_type = LinkTypes(link_proto.type) + options = LinkOptions(type=link_type) options_data = link_proto.options if options_data: options.delay = options_data.delay @@ -106,7 +101,6 @@ def add_link_data( options.unidirectional = options_data.unidirectional options.key = options_data.key options.opaque = options_data.opaque - return interface_one, interface_two, options diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index a79d7d6d..f3e1fbaa 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -772,7 +772,7 @@ class CoreHandler(socketserver.BaseRequestHandler): if link_type_value is not None: link_type = LinkTypes(link_type_value) - link_options = LinkOptions(_type=link_type) + link_options = LinkOptions(type=link_type) link_options.delay = message.get_tlv(LinkTlvs.DELAY.value) link_options.bandwidth = message.get_tlv(LinkTlvs.BANDWIDTH.value) link_options.session = message.get_tlv(LinkTlvs.SESSION.value) diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index 3c33ce65..f47da004 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -93,34 +93,28 @@ class NodeOptions: self.alt = alt +@dataclass class LinkOptions: """ Options for creating and updating links within core. """ - def __init__(self, _type: LinkTypes = LinkTypes.WIRED) -> None: - """ - Create a LinkOptions object. - - :param _type: type of link, defaults to - wired - """ - self.type = _type - self.session = None - self.delay = None - self.bandwidth = None - self.per = None - self.dup = None - self.jitter = None - self.mer = None - self.burst = None - self.mburst = None - self.gui_attributes = None - self.unidirectional = None - self.emulation_id = None - self.network_id = None - self.key = None - self.opaque = None + type: LinkTypes = LinkTypes.WIRED + session: int = None + delay: int = None + bandwidth: int = None + per: float = None + dup: int = None + jitter: int = None + mer: int = None + burst: int = None + mburst: int = None + gui_attributes: str = None + unidirectional: bool = None + emulation_id: int = None + network_id: int = None + key: int = None + opaque: str = None @dataclass diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index 89dcd7ab..800a8e62 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -511,7 +511,7 @@ class TestGui: EventTypes.DEFINITION_STATE, ], ) - def test_event_state(self, coretlv, state): + def test_event_state(self, coretlv: CoreHandler, state: EventTypes): message = coreapi.CoreEventMessage.create(0, [(EventTlvs.TYPE, state.value)]) coretlv.handle_message(message) @@ -536,7 +536,7 @@ class TestGui: coretlv.session.add_event.assert_called_once() - def test_event_save_xml(self, coretlv, tmpdir): + def test_event_save_xml(self, coretlv: CoreHandler, tmpdir): xml_file = tmpdir.join("coretlv.session.xml") file_path = xml_file.strpath coretlv.session.add_node(CoreNode) @@ -549,7 +549,7 @@ class TestGui: assert os.path.exists(file_path) - def test_event_open_xml(self, coretlv, tmpdir): + def test_event_open_xml(self, coretlv: CoreHandler, tmpdir): xml_file = tmpdir.join("coretlv.session.xml") file_path = xml_file.strpath node = coretlv.session.add_node(CoreNode) @@ -573,7 +573,7 @@ class TestGui: EventTypes.RECONFIGURE, ], ) - def test_event_service(self, coretlv, state): + def test_event_service(self, coretlv: CoreHandler, state: EventTypes): coretlv.session.broadcast_event = mock.MagicMock() node = coretlv.session.add_node(CoreNode) message = coreapi.CoreEventMessage.create( @@ -599,7 +599,7 @@ class TestGui: EventTypes.RECONFIGURE, ], ) - def test_event_mobility(self, coretlv, state): + def test_event_mobility(self, coretlv: CoreHandler, state: EventTypes): message = coreapi.CoreEventMessage.create( 0, [(EventTlvs.TYPE, state.value), (EventTlvs.NAME, "mobility:ns2script")] ) @@ -610,7 +610,7 @@ class TestGui: message = coreapi.CoreRegMessage.create(0, [(RegisterTlvs.GUI, "gui")]) coretlv.handle_message(message) - def test_register_xml(self, coretlv, tmpdir): + def test_register_xml(self, coretlv: CoreHandler, tmpdir): xml_file = tmpdir.join("coretlv.session.xml") file_path = xml_file.strpath node = coretlv.session.add_node(CoreNode) @@ -625,7 +625,7 @@ class TestGui: assert coretlv.coreemu.sessions[1].get_node(node.id, CoreNode) - def test_register_python(self, coretlv, tmpdir): + def test_register_python(self, coretlv: CoreHandler, tmpdir): xml_file = tmpdir.join("test.py") file_path = xml_file.strpath with open(file_path, "w") as f: From 7d2034df71d59a1c9ef3764e7590b5101233ee64 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 9 Jun 2020 10:45:18 -0700 Subject: [PATCH 0316/1131] daemon: updated NodeOptions to leverage dataclass --- daemon/core/api/grpc/grpcutils.py | 16 ++++++----- daemon/core/api/grpc/server.py | 3 +- daemon/core/emulator/emudata.py | 46 +++++++++++++------------------ daemon/core/xml/corexml.py | 6 ++-- daemon/tests/test_distributed.py | 6 ++-- daemon/tests/test_grpc.py | 9 ++---- docs/scripting.md | 8 ++---- 7 files changed, 39 insertions(+), 55 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 7797c86e..5c6f3a80 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -31,17 +31,19 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption """ _id = node_proto.id _type = NodeTypes(node_proto.type) - options = NodeOptions(name=node_proto.name, model=node_proto.model) - options.icon = node_proto.icon - options.opaque = node_proto.opaque - options.image = node_proto.image - options.services = node_proto.services - options.config_services = node_proto.config_services + options = NodeOptions( + name=node_proto.name, + model=node_proto.model, + icon=node_proto.icon, + opaque=node_proto.opaque, + image=node_proto.image, + services=node_proto.services, + config_services=node_proto.config_services, + ) if node_proto.emane: options.emane = node_proto.emane if node_proto.server: options.server = node_proto.server - position = node_proto.position options.set_position(position.x, position.y) if node_proto.HasField("geo"): diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index f85529e6..03cef387 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -743,8 +743,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("edit node: %s", request) session = self.get_session(request.session_id, context) node = self.get_node(session, request.node_id, context, NodeBase) - options = NodeOptions() - options.icon = request.icon + options = NodeOptions(icon=request.icon) if request.HasField("position"): x = request.position.x y = request.position.y diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index f47da004..7a9daf4f 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -1,5 +1,5 @@ -from dataclasses import dataclass -from typing import List, Union +from dataclasses import dataclass, field +from typing import List, Optional, Union import netaddr @@ -37,36 +37,28 @@ def link_config( ) +@dataclass class NodeOptions: """ Options for creating and updating nodes within core. """ - def __init__(self, name: str = None, model: str = "PC", image: str = None) -> None: - """ - Create a NodeOptions object. - - :param name: name of node, defaults to node class name postfix with its id - :param model: defines services for default and physical nodes, defaults to - "router" - :param image: image to use for docker nodes - """ - self.name = name - self.model = model - self.canvas = None - self.icon = None - self.opaque = None - self.services = [] - self.config_services = [] - self.x = None - self.y = None - self.lat = None - self.lon = None - self.alt = None - self.emulation_id = None - self.server = None - self.image = image - self.emane = None + name: str = None + model: Optional[str] = "PC" + canvas: int = None + icon: str = None + opaque: str = None + services: List[str] = field(default_factory=list) + config_services: List[str] = field(default_factory=list) + x: float = None + y: float = None + lat: float = None + lon: float = None + alt: float = None + emulation_id: int = None + server: str = None + image: str = None + emane: str = None def set_position(self, x: float, y: float) -> None: """ diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index efbf85c8..33005c97 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -832,8 +832,7 @@ class CoreXmlReader: icon = device_element.get("icon") clazz = device_element.get("class") image = device_element.get("image") - options = NodeOptions(name, model, image) - options.icon = icon + options = NodeOptions(name=name, model=model, image=image, icon=icon) node_type = NodeTypes.DEFAULT if clazz == "docker": @@ -874,8 +873,7 @@ class CoreXmlReader: node_type = NodeTypes[network_element.get("type")] _class = self.session.get_node_class(node_type) icon = network_element.get("icon") - options = NodeOptions(name) - options.icon = icon + options = NodeOptions(name=name, icon=icon) position_element = network_element.find("position") if position_element is not None: diff --git a/daemon/tests/test_distributed.py b/daemon/tests/test_distributed.py index 86ddaf99..0f4b1731 100644 --- a/daemon/tests/test_distributed.py +++ b/daemon/tests/test_distributed.py @@ -12,8 +12,7 @@ class TestDistributed: # when session.distributed.add_server(server_name, host) - options = NodeOptions() - options.server = server_name + options = NodeOptions(server=server_name) node = session.add_node(CoreNode, options=options) session.instantiate() @@ -30,8 +29,7 @@ class TestDistributed: # when session.distributed.add_server(server_name, host) - options = NodeOptions() - options.server = server_name + options = NodeOptions(server=server_name) node = session.add_node(HubNode, options=options) session.instantiate() diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 128863b4..c0686d71 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -710,8 +710,7 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() session.set_location(47.57917, -122.13232, 2.00000, 1.0) - options = NodeOptions() - options.emane = EmaneIeee80211abgModel.name + options = NodeOptions(emane=EmaneIeee80211abgModel.name) emane_network = session.add_node(EmaneNet, options=options) session.emane.set_model(emane_network, EmaneIeee80211abgModel) config_key = "platform_id_start" @@ -737,8 +736,7 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() session.set_location(47.57917, -122.13232, 2.00000, 1.0) - options = NodeOptions() - options.emane = EmaneIeee80211abgModel.name + options = NodeOptions(emane=EmaneIeee80211abgModel.name) emane_network = session.add_node(EmaneNet, options=options) session.emane.set_model(emane_network, EmaneIeee80211abgModel) config_key = "bandwidth" @@ -765,8 +763,7 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() session.set_location(47.57917, -122.13232, 2.00000, 1.0) - options = NodeOptions() - options.emane = EmaneIeee80211abgModel.name + options = NodeOptions(emane=EmaneIeee80211abgModel.name) emane_network = session.add_node(EmaneNet, options=options) session.emane.set_model(emane_network, EmaneIeee80211abgModel) diff --git a/docs/scripting.md b/docs/scripting.md index 7c8205c3..8c1a705c 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -136,8 +136,7 @@ coreemu = CoreEmu() session = coreemu.create_session() # create node with custom services -options = NodeOptions() -options.services = ["ServiceName"] +options = NodeOptions(services=["ServiceName"]) node = session.add_node(options=options) # set custom file data @@ -157,7 +156,6 @@ options = NodeOptions() options.set_position(80, 50) emane_network = session.add_node(EmaneNet, options=options) -# set custom emane model config -config = {} -session.emane.set_model(emane_network, EmaneIeee80211abgModel, config) +# set custom emane model config defaults +session.emane.set_model(emane_network, EmaneIeee80211abgModel) ``` From 3691c6029f33724c670422680767f7999d7fb6ce Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 9 Jun 2020 10:48:50 -0700 Subject: [PATCH 0317/1131] updated corexml InterfaceData instantiation to use named params --- daemon/core/xml/corexml.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 33005c97..cb25e717 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -66,7 +66,15 @@ def create_interface_data(interface_element: etree.Element) -> InterfaceData: ip4_mask = get_int(interface_element, "ip4_mask") ip6 = interface_element.get("ip6") ip6_mask = get_int(interface_element, "ip6_mask") - return InterfaceData(interface_id, name, mac, ip4, ip4_mask, ip6, ip6_mask) + return InterfaceData( + id=interface_id, + name=name, + mac=mac, + ip4=ip4, + ip4_mask=ip4_mask, + ip6=ip6, + ip6_mask=ip6_mask, + ) def create_emane_config(session: "Session") -> etree.Element: From 3be15a131604b87a9661c952c891bce7c2e48513 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 9 Jun 2020 12:42:15 -0700 Subject: [PATCH 0318/1131] daemon: update CoreNode.newnetif to require parameters, CoreNode.newnetif now depends on being provided InterfaceData --- daemon/core/emulator/emudata.py | 39 +++++++----------------- daemon/core/emulator/session.py | 49 +++++++++++------------------- daemon/core/nodes/base.py | 42 ++++++++------------------ daemon/core/nodes/physical.py | 53 ++++++++++----------------------- daemon/tests/test_nodes.py | 18 +++++++---- 5 files changed, 71 insertions(+), 130 deletions(-) diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index 7a9daf4f..b950e58c 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -1,18 +1,22 @@ from dataclasses import dataclass, field -from typing import List, Optional, Union +from typing import TYPE_CHECKING, List, Optional, Union import netaddr from core import utils from core.api.grpc.core_pb2 import LinkOptions from core.emulator.enumerations import LinkTypes -from core.nodes.base import CoreNetworkBase, CoreNode from core.nodes.interface import CoreInterface -from core.nodes.physical import PhysicalNode + +if TYPE_CHECKING: + from core.nodes.base import CoreNetworkBase, CoreNode + from core.nodes.physical import PhysicalNode + + LinkConfigNode = Union[CoreNetworkBase, PhysicalNode] def link_config( - node: Union[CoreNetworkBase, PhysicalNode], + node: "LinkConfigNode", interface: CoreInterface, link_options: LinkOptions, interface_two: CoreInterface = None, @@ -160,7 +164,7 @@ class IpPrefixes: if ip6_prefix: self.ip6 = netaddr.IPNetwork(ip6_prefix) - def ip4_address(self, node: CoreNode) -> str: + def ip4_address(self, node: "CoreNode") -> str: """ Convenience method to return the IP4 address for a node. @@ -171,7 +175,7 @@ class IpPrefixes: raise ValueError("ip4 prefixes have not been set") return str(self.ip4[node.id]) - def ip6_address(self, node: CoreNode) -> str: + def ip6_address(self, node: "CoreNode") -> str: """ Convenience method to return the IP6 address for a node. @@ -183,7 +187,7 @@ class IpPrefixes: return str(self.ip6[node.id]) def create_interface( - self, node: CoreNode, name: str = None, mac: str = None + self, node: "CoreNode", name: str = None, mac: str = None ) -> InterfaceData: """ Creates interface data for linking nodes, using the nodes unique id for @@ -225,24 +229,3 @@ class IpPrefixes: ip6_mask=ip6_mask, mac=mac, ) - - -def create_interface( - node: CoreNode, network: CoreNetworkBase, interface_data: InterfaceData -): - """ - Create an interface for a node on a network using provided interface data. - - :param node: node to create interface for - :param network: network to associate interface with - :param interface_data: interface data - :return: created interface - """ - node.newnetif( - network, - addrlist=interface_data.get_addresses(), - hwaddr=interface_data.mac, - ifindex=interface_data.id, - ifname=interface_data.name, - ) - return node.netif(interface_data.id) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 24fe05bd..d258ce9f 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -19,13 +19,7 @@ from core.emane.emanemanager import EmaneManager from core.emane.nodes import EmaneNet from core.emulator.data import ConfigData, EventData, ExceptionData, FileData, LinkData from core.emulator.distributed import DistributedController -from core.emulator.emudata import ( - InterfaceData, - LinkOptions, - NodeOptions, - create_interface, - link_config, -) +from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions, link_config from core.emulator.enumerations import ( EventTypes, ExceptionLevels, @@ -360,11 +354,11 @@ class Session: node_one.name, net_one.name, ) - interface = create_interface(node_one, net_one, interface_one) - node_one_interface = interface + ifindex = node_one.newnetif(net_one, interface_one) + node_one_interface = node_one.netif(ifindex) wireless_net = isinstance(net_one, (EmaneNet, WlanNode)) if not wireless_net: - link_config(net_one, interface, link_options) + link_config(net_one, node_one_interface, link_options) # network to node if node_two and net_one: @@ -373,11 +367,11 @@ class Session: node_two.name, net_one.name, ) - interface = create_interface(node_two, net_one, interface_two) - node_two_interface = interface + ifindex = node_two.newnetif(net_one, interface_two) + node_two_interface = node_two.netif(ifindex) wireless_net = isinstance(net_one, (EmaneNet, WlanNode)) if not link_options.unidirectional and not wireless_net: - link_config(net_one, interface, link_options) + link_config(net_one, node_two_interface, link_options) # network to network if net_one and net_two: @@ -1797,35 +1791,28 @@ class Session: control_net = self.add_remove_control_net(net_index, remove, conf_required) if not control_net: return - if not node: return - # ctrl# already exists if node.netif(control_net.CTRLIF_IDX_BASE + net_index): return - - control_ip = node.id - try: - address = control_net.prefix[control_ip] - prefix = control_net.prefix.prefixlen - addrlist = [f"{address}/{prefix}"] + ip4 = control_net.prefix[node.id] + ip4_mask = control_net.prefix.prefixlen + interface = InterfaceData( + id=control_net.CTRLIF_IDX_BASE + net_index, + name=f"ctrl{net_index}", + mac=utils.random_mac(), + ip4=ip4, + ip4_mask=ip4_mask, + ) + ifindex = node.newnetif(control_net, interface) + node.netif(ifindex).control = True except ValueError: msg = f"Control interface not added to node {node.id}. " msg += f"Invalid control network prefix ({control_net.prefix}). " msg += "A longer prefix length may be required for this many nodes." logging.exception(msg) - return - - interface1 = node.newnetif( - net=control_net, - ifindex=control_net.CTRLIF_IDX_BASE + net_index, - ifname=f"ctrl{net_index}", - hwaddr=utils.random_mac(), - addrlist=addrlist, - ) - node.netif(interface1).control = True def update_control_interface_hosts( self, net_index: int = 0, remove: bool = False diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 662815ef..e88912ac 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -15,6 +15,7 @@ from core import utils from core.configservice.dependencies import ConfigServiceDependencies from core.constants import MOUNT_BIN, VNODED_BIN from core.emulator.data import LinkData, NodeData +from core.emulator.emudata import InterfaceData from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes from core.errors import CoreCommandError, CoreError from core.nodes.client import VnodeClient @@ -845,53 +846,36 @@ class CoreNode(CoreNodeBase): interface_name = self.ifname(ifindex) self.node_net_client.device_up(interface_name) - def newnetif( - self, - net: "CoreNetworkBase" = None, - addrlist: List[str] = None, - hwaddr: str = None, - ifindex: int = None, - ifname: str = None, - ) -> int: + def newnetif(self, net: "CoreNetworkBase", interface: InterfaceData) -> int: """ Create a new network interface. :param net: network to associate with - :param addrlist: addresses to add on the interface - :param hwaddr: hardware address to set for interface - :param ifindex: index of interface to create - :param ifname: name for interface + :param interface: interface data for new interface :return: interface index """ - if not addrlist: - addrlist = [] - + addresses = interface.get_addresses() with self.lock: # TODO: emane specific code - if net is not None and net.is_emane is True: - ifindex = self.newtuntap(ifindex, ifname) + if net.is_emane is True: + ifindex = self.newtuntap(interface.id, interface.name) # TUN/TAP is not ready for addressing yet; the device may # take some time to appear, and installing it into a # namespace after it has been bound removes addressing; # save addresses with the interface now self.attachnet(ifindex, net) netif = self.netif(ifindex) - netif.sethwaddr(hwaddr) - for address in utils.make_tuple(addrlist): + netif.sethwaddr(interface.mac) + for address in addresses: netif.addaddr(address) return ifindex else: - ifindex = self.newveth(ifindex, ifname) - - if net is not None: - self.attachnet(ifindex, net) - - if hwaddr: - self.sethwaddr(ifindex, hwaddr) - - for address in utils.make_tuple(addrlist): + ifindex = self.newveth(interface.id, interface.name) + self.attachnet(ifindex, net) + if interface.mac: + self.sethwaddr(ifindex, interface.mac) + for address in addresses: self.addaddr(ifindex, address) - self.ifup(ifindex) return ifindex diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index e5db8a80..ec531505 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -10,10 +10,11 @@ from typing import IO, TYPE_CHECKING, List, Optional, Tuple from core import utils from core.constants import MOUNT_BIN, UMOUNT_BIN from core.emulator.distributed import DistributedServer +from core.emulator.emudata import InterfaceData from core.emulator.enumerations import NodeTypes, TransportType from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNetworkBase, CoreNodeBase -from core.nodes.interface import CoreInterface, Veth +from core.nodes.interface import CoreInterface from core.nodes.network import CoreNetwork, GreTap if TYPE_CHECKING: @@ -168,37 +169,25 @@ class PhysicalNode(CoreNodeBase): self.ifindex += 1 return ifindex - def newnetif( - self, - net: Veth = None, - addrlist: List[str] = None, - hwaddr: str = None, - ifindex: int = None, - ifname: str = None, - ) -> int: + def newnetif(self, net: CoreNetworkBase, interface: InterfaceData) -> int: logging.info("creating interface") - if not addrlist: - addrlist = [] - - if self.up and net is None: - raise NotImplementedError - + addresses = interface.get_addresses() + ifindex = interface.id if ifindex is None: ifindex = self.newifindex() - - if ifname is None: - ifname = f"gt{ifindex}" - + name = interface.name + if name is None: + name = f"gt{ifindex}" if self.up: # this is reached when this node is linked to a network node # tunnel to net not built yet, so build it now and adopt it _, remote_tap = self.session.distributed.create_gre_tunnel(net, self.server) - self.adoptnetif(remote_tap, ifindex, hwaddr, addrlist) + self.adoptnetif(remote_tap, ifindex, interface.mac, addresses) return ifindex else: # this is reached when configuring services (self.up=False) - netif = GreTap(node=self, name=ifname, session=self.session, start=False) - self.adoptnetif(netif, ifindex, hwaddr, addrlist) + netif = GreTap(node=self, name=name, session=self.session, start=False) + self.adoptnetif(netif, ifindex, interface.mac, addresses) return ifindex def privatedir(self, path: str) -> None: @@ -320,28 +309,19 @@ class Rj45Node(CoreNodeBase): self.up = False self.restorestate() - def newnetif( - self, - net: CoreNetworkBase = None, - addrlist: List[str] = None, - hwaddr: str = None, - ifindex: int = None, - ifname: str = None, - ) -> int: + def newnetif(self, net: CoreNetworkBase, interface: InterfaceData) -> int: """ This is called when linking with another node. Since this node represents an interface, we do not create another object here, but attach ourselves to the given network. :param net: new network instance - :param addrlist: address list - :param hwaddr: hardware address - :param ifindex: interface index - :param ifname: interface name + :param interface: interface data for new interface :return: interface index :raises ValueError: when an interface has already been created, one max """ with self.lock: + ifindex = interface.id if ifindex is None: ifindex = 0 if self.interface.net is not None: @@ -350,9 +330,8 @@ class Rj45Node(CoreNodeBase): self.ifindex = ifindex if net is not None: self.interface.attachnet(net) - if addrlist: - for addr in utils.make_tuple(addrlist): - self.addaddr(addr) + for addr in interface.get_addresses(): + self.addaddr(addr) return ifindex def delnetif(self, ifindex: int) -> None: diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index 65b17949..26e78367 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -1,6 +1,6 @@ import pytest -from core.emulator.emudata import NodeOptions +from core.emulator.emudata import InterfaceData, NodeOptions from core.emulator.session import Session from core.errors import CoreError from core.nodes.base import CoreNode @@ -52,7 +52,9 @@ class TestNodes: def test_node_sethwaddr(self, session: Session): # given node = session.add_node(CoreNode) - index = node.newnetif() + switch = session.add_node(SwitchNode) + interface_data = InterfaceData() + index = node.newnetif(switch, interface_data) interface = node.netif(index) mac = "aa:aa:aa:ff:ff:ff" @@ -65,7 +67,9 @@ class TestNodes: def test_node_sethwaddr_exception(self, session: Session): # given node = session.add_node(CoreNode) - index = node.newnetif() + switch = session.add_node(SwitchNode) + interface_data = InterfaceData() + index = node.newnetif(switch, interface_data) node.netif(index) mac = "aa:aa:aa:ff:ff:fff" @@ -76,7 +80,9 @@ class TestNodes: def test_node_addaddr(self, session: Session): # given node = session.add_node(CoreNode) - index = node.newnetif() + switch = session.add_node(SwitchNode) + interface_data = InterfaceData() + index = node.newnetif(switch, interface_data) interface = node.netif(index) addr = "192.168.0.1/24" @@ -89,7 +95,9 @@ class TestNodes: def test_node_addaddr_exception(self, session): # given node = session.add_node(CoreNode) - index = node.newnetif() + switch = session.add_node(SwitchNode) + interface_data = InterfaceData() + index = node.newnetif(switch, interface_data) node.netif(index) addr = "256.168.0.1/24" From 2965273f58a3782d75f5d10700c0e5fbe50d0ea6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 9 Jun 2020 13:41:31 -0700 Subject: [PATCH 0319/1131] daemon: CoreNetworkBase.linkconfig now takes a LinkOptions object, removed usage of emudata.link_config --- daemon/core/emane/commeffect.py | 23 ++++++++------------- daemon/core/emane/emanemodel.py | 16 +++------------ daemon/core/emane/nodes.py | 12 +++-------- daemon/core/emulator/emudata.py | 35 ++------------------------------ daemon/core/emulator/session.py | 34 +++++++++++++++---------------- daemon/core/location/mobility.py | 9 ++++++-- daemon/core/nodes/base.py | 17 +++------------- daemon/core/nodes/network.py | 21 ++++++++----------- daemon/core/nodes/physical.py | 13 +++--------- 9 files changed, 53 insertions(+), 127 deletions(-) diff --git a/daemon/core/emane/commeffect.py b/daemon/core/emane/commeffect.py index 99fdb9b1..b7060e96 100644 --- a/daemon/core/emane/commeffect.py +++ b/daemon/core/emane/commeffect.py @@ -11,6 +11,7 @@ from lxml import etree from core.config import ConfigGroup, Configuration from core.emane import emanemanifest, emanemodel from core.emane.nodes import EmaneNet +from core.emulator.emudata import LinkOptions from core.emulator.enumerations import TransportType from core.nodes.interface import CoreInterface from core.xml import emanexml @@ -114,14 +115,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): emanexml.create_file(shim_element, "shim", shim_file) def linkconfig( - self, - netif: CoreInterface, - bw: float = None, - delay: float = None, - loss: float = None, - duplicate: float = None, - jitter: float = None, - netif2: CoreInterface = None, + self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None ) -> None: """ Generate CommEffect events when a Link Message is received having @@ -142,15 +136,14 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): emane_node = self.session.get_node(self.id, EmaneNet) nemid = emane_node.getnemid(netif) nemid2 = emane_node.getnemid(netif2) - mbw = bw logging.info("sending comm effect event") event.append( nemid, - latency=convert_none(delay), - jitter=convert_none(jitter), - loss=convert_none(loss), - duplicate=convert_none(duplicate), - unicast=int(convert_none(bw)), - broadcast=int(convert_none(mbw)), + latency=convert_none(options.delay), + jitter=convert_none(options.jitter), + loss=convert_none(options.per), + duplicate=convert_none(options.dup), + unicast=int(convert_none(options.bandwidth)), + broadcast=int(convert_none(options.bandwidth)), ) service.publish(nemid2, event) diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index 3a21643b..f42caa14 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -8,6 +8,7 @@ from typing import Dict, List from core.config import ConfigGroup, Configuration from core.emane import emanemanifest from core.emane.nodes import EmaneNet +from core.emulator.emudata import LinkOptions from core.emulator.enumerations import ConfigDataTypes, TransportType from core.errors import CoreError from core.location.mobility import WirelessModel @@ -155,24 +156,13 @@ class EmaneModel(WirelessModel): logging.exception("error during update") def linkconfig( - self, - netif: CoreInterface, - bw: float = None, - delay: float = None, - loss: float = None, - duplicate: float = None, - jitter: float = None, - netif2: CoreInterface = None, + self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None ) -> None: """ Invoked when a Link Message is received. Default is unimplemented. :param netif: interface one - :param bw: bandwidth to set to - :param delay: packet delay to set to - :param loss: packet loss to set to - :param duplicate: duplicate percentage to set to - :param jitter: jitter to set to + :param options: options for configuring link :param netif2: interface two :return: nothing """ diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index bbe59b95..f4de8f47 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type from core.emulator.data import LinkData from core.emulator.distributed import DistributedServer +from core.emulator.emudata import LinkOptions from core.emulator.enumerations import ( LinkTypes, MessageFlags, @@ -60,21 +61,14 @@ class EmaneNet(CoreNetworkBase): self.mobility = None def linkconfig( - self, - netif: CoreInterface, - bw: float = None, - delay: float = None, - loss: float = None, - duplicate: float = None, - jitter: float = None, - netif2: CoreInterface = None, + self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None ) -> None: """ The CommEffect model supports link configuration. """ if not self.model: return - self.model.linkconfig(netif, bw, delay, loss, duplicate, jitter, netif2) + self.model.linkconfig(netif, options, netif2) def config(self, conf: str) -> None: self.conf = conf diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index b950e58c..3ccf11cc 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -1,44 +1,13 @@ from dataclasses import dataclass, field -from typing import TYPE_CHECKING, List, Optional, Union +from typing import TYPE_CHECKING, List, Optional import netaddr from core import utils -from core.api.grpc.core_pb2 import LinkOptions from core.emulator.enumerations import LinkTypes -from core.nodes.interface import CoreInterface if TYPE_CHECKING: - from core.nodes.base import CoreNetworkBase, CoreNode - from core.nodes.physical import PhysicalNode - - LinkConfigNode = Union[CoreNetworkBase, PhysicalNode] - - -def link_config( - node: "LinkConfigNode", - interface: CoreInterface, - link_options: LinkOptions, - interface_two: CoreInterface = None, -) -> None: - """ - Convenience method for configuring a link, - - :param node: network to configure link for - :param interface: interface to configure - :param link_options: data to configure link with - :param interface_two: other interface associated, default is None - :return: nothing - """ - node.linkconfig( - interface, - link_options.bandwidth, - link_options.delay, - link_options.per, - link_options.dup, - link_options.jitter, - interface_two, - ) + from core.nodes.base import CoreNode @dataclass diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index d258ce9f..59bd3cf3 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -19,7 +19,7 @@ from core.emane.emanemanager import EmaneManager from core.emane.nodes import EmaneNet from core.emulator.data import ConfigData, EventData, ExceptionData, FileData, LinkData from core.emulator.distributed import DistributedController -from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions, link_config +from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.enumerations import ( EventTypes, ExceptionLevels, @@ -358,7 +358,7 @@ class Session: node_one_interface = node_one.netif(ifindex) wireless_net = isinstance(net_one, (EmaneNet, WlanNode)) if not wireless_net: - link_config(net_one, node_one_interface, link_options) + net_one.linkconfig(node_one_interface, link_options) # network to node if node_two and net_one: @@ -371,7 +371,7 @@ class Session: node_two_interface = node_two.netif(ifindex) wireless_net = isinstance(net_one, (EmaneNet, WlanNode)) if not link_options.unidirectional and not wireless_net: - link_config(net_one, node_two_interface, link_options) + net_one.linkconfig(node_two_interface, link_options) # network to network if net_one and net_two: @@ -382,18 +382,16 @@ class Session: ) interface = net_one.linknet(net_two) node_one_interface = interface - link_config(net_one, interface, link_options) - + net_one.linkconfig(interface, link_options) if not link_options.unidirectional: interface.swapparams("_params_up") - link_config(net_two, interface, link_options) + net_two.linkconfig(interface, link_options) interface.swapparams("_params_up") # a tunnel node was found for the nodes addresses = [] if not node_one and all([net_one, interface_one]): addresses.extend(interface_one.get_addresses()) - if not node_two and all([net_two, interface_two]): addresses.extend(interface_two.get_addresses()) @@ -418,14 +416,14 @@ class Session: node_one.adoptnetif( tunnel, interface_one.id, interface_one.mac, addresses ) - link_config(node_one, tunnel, link_options) + node_one.linkconfig(tunnel, link_options) elif node_two and isinstance(node_two, PhysicalNode): logging.info("adding link for physical node: %s", node_two.name) addresses = interface_two.get_addresses() node_two.adoptnetif( tunnel, interface_two.id, interface_two.mac, addresses ) - link_config(node_two, tunnel, link_options) + node_two.linkconfig(tunnel, link_options) finally: if node_one: node_one.lock.release() @@ -596,28 +594,28 @@ class Session: if upstream: interface.swapparams("_params_up") - link_config(net_one, interface, link_options) + net_one.linkconfig(interface, link_options) interface.swapparams("_params_up") else: - link_config(net_one, interface, link_options) + net_one.linkconfig(interface, link_options) if not link_options.unidirectional: if upstream: - link_config(net_two, interface, link_options) + net_two.linkconfig(interface, link_options) else: interface.swapparams("_params_up") - link_config(net_two, interface, link_options) + net_two.linkconfig(interface, link_options) interface.swapparams("_params_up") else: raise CoreError("modify link for unknown nodes") elif not node_one: # node1 = layer 2node, node2 = layer3 node interface = node_two.netif(interface_two_id) - link_config(net_one, interface, link_options) + net_one.linkconfig(interface, link_options) elif not node_two: # node2 = layer 2node, node1 = layer3 node interface = node_one.netif(interface_one_id) - link_config(net_one, interface, link_options) + net_one.linkconfig(interface, link_options) else: common_networks = node_one.commonnets(node_two) if not common_networks: @@ -630,10 +628,10 @@ class Session: ): continue - link_config(net_one, interface_one, link_options, interface_two) + net_one.linkconfig(interface_one, link_options, interface_two) if not link_options.unidirectional: - link_config( - net_one, interface_two, link_options, interface_one + net_one.linkconfig( + interface_two, link_options, interface_one ) finally: if node_one: diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 5041f144..3ca46418 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -14,6 +14,7 @@ from typing import TYPE_CHECKING, Dict, List, Tuple from core import utils from core.config import ConfigGroup, ConfigurableOptions, Configuration, ModelManager from core.emulator.data import EventData, LinkData +from core.emulator.emudata import LinkOptions from core.emulator.enumerations import ( ConfigDataTypes, EventTypes, @@ -334,9 +335,13 @@ class BasicRangeModel(WirelessModel): """ with self._netifslock: for netif in self._netifs: - self.wlan.linkconfig( - netif, self.bw, self.delay, self.loss, jitter=self.jitter + options = LinkOptions( + bandwidth=self.bw, + delay=self.delay, + per=self.loss, + jitter=self.jitter, ) + self.wlan.linkconfig(netif, options) def get_position(self, netif: CoreInterface) -> Tuple[float, float, float]: """ diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index e88912ac..0c76d6a2 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -15,7 +15,7 @@ from core import utils from core.configservice.dependencies import ConfigServiceDependencies from core.constants import MOUNT_BIN, VNODED_BIN from core.emulator.data import LinkData, NodeData -from core.emulator.emudata import InterfaceData +from core.emulator.emudata import InterfaceData, LinkOptions from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes from core.errors import CoreCommandError, CoreError from core.nodes.client import VnodeClient @@ -1147,24 +1147,13 @@ class CoreNetworkBase(NodeBase): return all_links def linkconfig( - self, - netif: CoreInterface, - bw: float = None, - delay: float = None, - loss: float = None, - duplicate: float = None, - jitter: float = None, - netif2: float = None, + self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None ) -> None: """ Configure link parameters by applying tc queuing disciplines on the interface. :param netif: interface one - :param bw: bandwidth to set to - :param delay: packet delay to set to - :param loss: packet loss to set to - :param duplicate: duplicate percentage to set to - :param jitter: jitter to set to + :param options: options for configuring link :param netif2: interface two :return: nothing """ diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index b08d87d4..095fbe9b 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -12,6 +12,7 @@ import netaddr from core import utils from core.constants import EBTABLES_BIN, TC_BIN from core.emulator.data import LinkData, NodeData +from core.emulator.emudata import LinkOptions from core.emulator.enumerations import ( LinkTypes, MessageFlags, @@ -441,24 +442,13 @@ class CoreNetwork(CoreNetworkBase): ebq.ebchange(self) def linkconfig( - self, - netif: CoreInterface, - bw: float = None, - delay: float = None, - loss: float = None, - duplicate: float = None, - jitter: float = None, - netif2: float = None, + self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None ) -> None: """ Configure link parameters by applying tc queuing disciplines on the interface. :param netif: interface one - :param bw: bandwidth to set to - :param delay: packet delay to set to - :param loss: packet loss to set to - :param duplicate: duplicate percentage to set to - :param jitter: jitter to set to + :param options: options for configuring link :param netif2: interface two :return: nothing """ @@ -466,6 +456,7 @@ class CoreNetwork(CoreNetworkBase): tc = f"{TC_BIN} qdisc replace dev {devname}" parent = "root" changed = False + bw = options.bandwidth if netif.setparam("bw", bw): # from tc-tbf(8): minimum value for burst is rate / kernel_hz burst = max(2 * netif.mtu, int(bw / 1000)) @@ -489,13 +480,17 @@ class CoreNetwork(CoreNetworkBase): if netif.getparam("has_tbf"): parent = "parent 1:1" netem = "netem" + delay = options.delay changed = max(changed, netif.setparam("delay", delay)) + loss = options.per if loss is not None: loss = float(loss) changed = max(changed, netif.setparam("loss", loss)) + duplicate = options.dup if duplicate is not None: duplicate = int(duplicate) changed = max(changed, netif.setparam("duplicate", duplicate)) + jitter = options.jitter changed = max(changed, netif.setparam("jitter", jitter)) if not changed: return diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index ec531505..018ca60d 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -10,7 +10,7 @@ from typing import IO, TYPE_CHECKING, List, Optional, Tuple from core import utils from core.constants import MOUNT_BIN, UMOUNT_BIN from core.emulator.distributed import DistributedServer -from core.emulator.emudata import InterfaceData +from core.emulator.emudata import InterfaceData, LinkOptions from core.emulator.enumerations import NodeTypes, TransportType from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNetworkBase, CoreNodeBase @@ -144,21 +144,14 @@ class PhysicalNode(CoreNodeBase): self.net_client.device_up(netif.localname) def linkconfig( - self, - netif: CoreInterface, - bw: float = None, - delay: float = None, - loss: float = None, - duplicate: float = None, - jitter: float = None, - netif2: CoreInterface = None, + self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None ) -> None: """ Apply tc queing disciplines using linkconfig. """ linux_bridge = CoreNetwork(session=self.session, start=False) linux_bridge.up = True - linux_bridge.linkconfig(netif, bw, delay, loss, duplicate, jitter, netif2) + linux_bridge.linkconfig(netif, options, netif2) del linux_bridge def newifindex(self) -> int: From 21da67069803f323f1cb1e57290a278a9d53fc09 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 9 Jun 2020 13:46:26 -0700 Subject: [PATCH 0320/1131] daemon: renamed link_options to options in both session.add_link and session.update_link --- daemon/core/api/grpc/server.py | 2 +- daemon/core/emulator/session.py | 60 ++++++++++++++++----------------- daemon/tests/test_links.py | 2 +- daemon/tests/test_xml.py | 4 +-- 4 files changed, 32 insertions(+), 36 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 03cef387..7d7f7c80 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -854,7 +854,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): node_two_id = request.link.node_two_id interface_one, interface_two, options = grpcutils.add_link_data(request.link) node_one_interface, node_two_interface = session.add_link( - node_one_id, node_two_id, interface_one, interface_two, link_options=options + node_one_id, node_two_id, interface_one, interface_two, options=options ) interface_one_proto = None interface_two_proto = None diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 59bd3cf3..a1e71612 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -299,7 +299,7 @@ class Session: node_two_id: int, interface_one: InterfaceData = None, interface_two: InterfaceData = None, - link_options: LinkOptions = None, + options: LinkOptions = None, ) -> Tuple[CoreInterface, CoreInterface]: """ Add a link between nodes. @@ -310,12 +310,12 @@ class Session: data, defaults to none :param interface_two: node two interface data, defaults to none - :param link_options: data for creating link, + :param options: data for creating link, defaults to no options :return: tuple of created core interfaces, depending on link """ - if not link_options: - link_options = LinkOptions() + if not options: + options = LinkOptions() # get node objects identified by link data node_one, node_two, net_one, net_two, tunnel = self._link_nodes( @@ -332,7 +332,7 @@ class Session: try: # wireless link - if link_options.type == LinkTypes.WIRELESS: + if options.type == LinkTypes.WIRELESS: objects = [node_one, node_two, net_one, net_two] self._link_wireless(objects, connect=True) # wired link @@ -358,7 +358,7 @@ class Session: node_one_interface = node_one.netif(ifindex) wireless_net = isinstance(net_one, (EmaneNet, WlanNode)) if not wireless_net: - net_one.linkconfig(node_one_interface, link_options) + net_one.linkconfig(node_one_interface, options) # network to node if node_two and net_one: @@ -370,8 +370,8 @@ class Session: ifindex = node_two.newnetif(net_one, interface_two) node_two_interface = node_two.netif(ifindex) wireless_net = isinstance(net_one, (EmaneNet, WlanNode)) - if not link_options.unidirectional and not wireless_net: - net_one.linkconfig(node_two_interface, link_options) + if not options.unidirectional and not wireless_net: + net_one.linkconfig(node_two_interface, options) # network to network if net_one and net_two: @@ -382,10 +382,10 @@ class Session: ) interface = net_one.linknet(net_two) node_one_interface = interface - net_one.linkconfig(interface, link_options) - if not link_options.unidirectional: + net_one.linkconfig(interface, options) + if not options.unidirectional: interface.swapparams("_params_up") - net_two.linkconfig(interface, link_options) + net_two.linkconfig(interface, options) interface.swapparams("_params_up") # a tunnel node was found for the nodes @@ -396,7 +396,7 @@ class Session: addresses.extend(interface_two.get_addresses()) # tunnel node logic - key = link_options.key + key = options.key if key and isinstance(net_one, TunnelNode): logging.info("setting tunnel key for: %s", net_one.name) net_one.setkey(key) @@ -416,14 +416,14 @@ class Session: node_one.adoptnetif( tunnel, interface_one.id, interface_one.mac, addresses ) - node_one.linkconfig(tunnel, link_options) + node_one.linkconfig(tunnel, options) elif node_two and isinstance(node_two, PhysicalNode): logging.info("adding link for physical node: %s", node_two.name) addresses = interface_two.get_addresses() node_two.adoptnetif( tunnel, interface_two.id, interface_two.mac, addresses ) - node_two.linkconfig(tunnel, link_options) + node_two.linkconfig(tunnel, options) finally: if node_one: node_one.lock.release() @@ -547,7 +547,7 @@ class Session: node_two_id: int, interface_one_id: int = None, interface_two_id: int = None, - link_options: LinkOptions = None, + options: LinkOptions = None, ) -> None: """ Update link information between nodes. @@ -556,13 +556,13 @@ class Session: :param node_two_id: node two id :param interface_one_id: interface id for node one :param interface_two_id: interface id for node two - :param link_options: data to update link with + :param options: data to update link with :return: nothing :raises core.CoreError: when updating a wireless type link, when there is a unknown link between networks """ - if not link_options: - link_options = LinkOptions() + if not options: + options = LinkOptions() # get node objects identified by link data node_one, node_two, net_one, net_two, _tunnel = self._link_nodes( @@ -576,7 +576,7 @@ class Session: try: # wireless link - if link_options.type == LinkTypes.WIRELESS: + if options.type == LinkTypes.WIRELESS: raise CoreError("cannot update wireless link") else: if not node_one and not node_two: @@ -594,28 +594,28 @@ class Session: if upstream: interface.swapparams("_params_up") - net_one.linkconfig(interface, link_options) + net_one.linkconfig(interface, options) interface.swapparams("_params_up") else: - net_one.linkconfig(interface, link_options) + net_one.linkconfig(interface, options) - if not link_options.unidirectional: + if not options.unidirectional: if upstream: - net_two.linkconfig(interface, link_options) + net_two.linkconfig(interface, options) else: interface.swapparams("_params_up") - net_two.linkconfig(interface, link_options) + net_two.linkconfig(interface, options) interface.swapparams("_params_up") else: raise CoreError("modify link for unknown nodes") elif not node_one: # node1 = layer 2node, node2 = layer3 node interface = node_two.netif(interface_two_id) - net_one.linkconfig(interface, link_options) + net_one.linkconfig(interface, options) elif not node_two: # node2 = layer 2node, node1 = layer3 node interface = node_one.netif(interface_one_id) - net_one.linkconfig(interface, link_options) + net_one.linkconfig(interface, options) else: common_networks = node_one.commonnets(node_two) if not common_networks: @@ -628,11 +628,9 @@ class Session: ): continue - net_one.linkconfig(interface_one, link_options, interface_two) - if not link_options.unidirectional: - net_one.linkconfig( - interface_two, link_options, interface_one - ) + net_one.linkconfig(interface_one, options, interface_two) + if not options.unidirectional: + net_one.linkconfig(interface_two, options, interface_one) finally: if node_one: node_one.lock.release() diff --git a/daemon/tests/test_links.py b/daemon/tests/test_links.py index 94b2e53f..9736537e 100644 --- a/daemon/tests/test_links.py +++ b/daemon/tests/test_links.py @@ -105,7 +105,7 @@ class TestLinks: node_one.id, node_two.id, interface_one_id=interface_one_data.id, - link_options=link_options, + options=link_options, ) # then diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index 70117fb8..c40a9ef3 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -309,9 +309,7 @@ class TestXml: link_options.jitter = 10 link_options.delay = 30 link_options.dup = 5 - session.add_link( - node_one.id, switch.id, interface_one, link_options=link_options - ) + session.add_link(node_one.id, switch.id, interface_one, options=link_options) # instantiate session session.instantiate() From d71d84fae7da24cabe7a94aee2b60a84786959b0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 9 Jun 2020 18:40:50 -0700 Subject: [PATCH 0321/1131] daemon: updated IpPrefixes and InterfaceHelper to remove duplicate code --- daemon/core/api/grpc/client.py | 77 ++++--------------------------- daemon/core/emulator/emudata.py | 78 +++++++++++++++++--------------- daemon/tests/emane/test_emane.py | 2 +- daemon/tests/test_core.py | 2 +- 4 files changed, 53 insertions(+), 106 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index c1d0e2fd..64b8d29f 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -8,9 +8,7 @@ from contextlib import contextmanager from typing import Any, Callable, Dict, Generator, Iterable, List import grpc -import netaddr -from core import utils from core.api.grpc import configservices_pb2, core_pb2, core_pb2_grpc from core.api.grpc.configservices_pb2 import ( GetConfigServiceDefaultsRequest, @@ -94,6 +92,7 @@ from core.api.grpc.wlan_pb2 import ( WlanLinkRequest, WlanLinkResponse, ) +from core.emulator.emudata import IpPrefixes class InterfaceHelper: @@ -109,78 +108,20 @@ class InterfaceHelper: :param ip6_prefix: ip6 prefix to use for generation :raises ValueError: when both ip4 and ip6 prefixes have not been provided """ - if not ip4_prefix and not ip6_prefix: - raise ValueError("ip4 or ip6 must be provided") - - self.ip4 = None - if ip4_prefix: - self.ip4 = netaddr.IPNetwork(ip4_prefix) - self.ip6 = None - if ip6_prefix: - self.ip6 = netaddr.IPNetwork(ip6_prefix) - - def ip4_address(self, node_id: int) -> str: - """ - Convenience method to return the IP4 address for a node. - - :param node_id: node id to get IP4 address for - :return: IP4 address or None - """ - if not self.ip4: - raise ValueError("ip4 prefixes have not been set") - return str(self.ip4[node_id]) - - def ip6_address(self, node_id: int) -> str: - """ - Convenience method to return the IP6 address for a node. - - :param node_id: node id to get IP6 address for - :return: IP4 address or None - """ - if not self.ip6: - raise ValueError("ip6 prefixes have not been set") - return str(self.ip6[node_id]) + self.prefixes = IpPrefixes(ip4_prefix, ip6_prefix) def create_interface( self, node_id: int, interface_id: int, name: str = None, mac: str = None ) -> core_pb2.Interface: - """ - Creates interface data for linking nodes, using the nodes unique id for - generation, along with a random mac address, unless provided. - - :param node_id: node id to create interface for - :param interface_id: interface id for interface - :param name: name to set for interface, default is eth{id} - :param mac: mac address to use for this interface, default is random - generation - :return: new interface data for the provided node - """ - # generate ip4 data - ip4 = None - ip4_mask = None - if self.ip4: - ip4 = self.ip4_address(node_id) - ip4_mask = self.ip4.prefixlen - - # generate ip6 data - ip6 = None - ip6_mask = None - if self.ip6: - ip6 = self.ip6_address(node_id) - ip6_mask = self.ip6.prefixlen - - # random mac - if not mac: - mac = utils.random_mac() - + interface_data = self.prefixes.gen_interface(node_id, name, mac) return core_pb2.Interface( id=interface_id, - name=name, - ip4=ip4, - ip4mask=ip4_mask, - ip6=ip6, - ip6mask=ip6_mask, - mac=str(mac), + name=interface_data.name, + ip4=interface_data.ip4, + ip4mask=interface_data.ip4_mask, + ip6=interface_data.ip6, + ip6mask=interface_data.ip6_mask, + mac=interface_data.mac, ) diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index 3ccf11cc..b6dbd57c 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -133,27 +133,60 @@ class IpPrefixes: if ip6_prefix: self.ip6 = netaddr.IPNetwork(ip6_prefix) - def ip4_address(self, node: "CoreNode") -> str: + def ip4_address(self, node_id: int) -> str: """ Convenience method to return the IP4 address for a node. - :param node: node to get IP4 address for + :param node_id: node id to get IP4 address for :return: IP4 address or None """ if not self.ip4: raise ValueError("ip4 prefixes have not been set") - return str(self.ip4[node.id]) + return str(self.ip4[node_id]) - def ip6_address(self, node: "CoreNode") -> str: + def ip6_address(self, node_id: int) -> str: """ Convenience method to return the IP6 address for a node. - :param node: node to get IP6 address for + :param node_id: node id to get IP6 address for :return: IP4 address or None """ if not self.ip6: raise ValueError("ip6 prefixes have not been set") - return str(self.ip6[node.id]) + return str(self.ip6[node_id]) + + def gen_interface(self, node_id: int, name: str = None, mac: str = None): + """ + Creates interface data for linking nodes, using the nodes unique id for + generation, along with a random mac address, unless provided. + + :param node_id: node id to create an interface for + :param name: name to set for interface, default is eth{id} + :param mac: mac address to use for this interface, default is random + generation + :return: new interface data for the provided node + """ + # generate ip4 data + ip4 = None + ip4_mask = None + if self.ip4: + ip4 = self.ip4_address(node_id) + ip4_mask = self.ip4.prefixlen + + # generate ip6 data + ip6 = None + ip6_mask = None + if self.ip6: + ip6 = self.ip6_address(node_id) + ip6_mask = self.ip6.prefixlen + + # random mac + if not mac: + mac = utils.random_mac() + + return InterfaceData( + name=name, ip4=ip4, ip4_mask=ip4_mask, ip6=ip6, ip6_mask=ip6_mask, mac=mac + ) def create_interface( self, node: "CoreNode", name: str = None, mac: str = None @@ -168,33 +201,6 @@ class IpPrefixes: generation :return: new interface data for the provided node """ - # interface id - inteface_id = node.newifindex() - - # generate ip4 data - ip4 = None - ip4_mask = None - if self.ip4: - ip4 = self.ip4_address(node) - ip4_mask = self.ip4.prefixlen - - # generate ip6 data - ip6 = None - ip6_mask = None - if self.ip6: - ip6 = self.ip6_address(node) - ip6_mask = self.ip6.prefixlen - - # random mac - if not mac: - mac = utils.random_mac() - - return InterfaceData( - id=inteface_id, - name=name, - ip4=ip4, - ip4_mask=ip4_mask, - ip6=ip6, - ip6_mask=ip6_mask, - mac=mac, - ) + interface = self.gen_interface(node.id, name, mac) + interface.id = node.newifindex() + return interface diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py index 62fe15e1..2d90ebcc 100644 --- a/daemon/tests/emane/test_emane.py +++ b/daemon/tests/emane/test_emane.py @@ -32,7 +32,7 @@ _DIR = os.path.dirname(os.path.abspath(__file__)) def ping( from_node: CoreNode, to_node: CoreNode, ip_prefixes: IpPrefixes, count: int = 3 ): - address = ip_prefixes.ip4_address(to_node) + address = ip_prefixes.ip4_address(to_node.id) try: from_node.cmd(f"ping -c {count} {address}") status = 0 diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 88a40906..68515a41 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -22,7 +22,7 @@ _WIRED = [PtpNet, HubNode, SwitchNode] def ping(from_node: CoreNode, to_node: CoreNode, ip_prefixes: IpPrefixes): - address = ip_prefixes.ip4_address(to_node) + address = ip_prefixes.ip4_address(to_node.id) try: from_node.cmd(f"ping -c 1 {address}") status = 0 From f73c617ecfc81ce4b393e39b631a591e144ca725 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 9 Jun 2020 18:53:42 -0700 Subject: [PATCH 0322/1131] daemon: removed utils.make_tuple and last remaining usage --- daemon/core/nodes/physical.py | 7 +------ daemon/core/utils.py | 13 ------------- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 018ca60d..ee00c705 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -126,20 +126,15 @@ class PhysicalNode(CoreNodeBase): netif.name = f"gt{ifindex}" netif.node = self self.addnetif(netif, ifindex) - # use a more reasonable name, e.g. "gt0" instead of "gt.56286.150" if self.up: self.net_client.device_down(netif.localname) self.net_client.device_name(netif.localname, netif.name) - netif.localname = netif.name - if hwaddr: self.sethwaddr(ifindex, hwaddr) - - for addr in utils.make_tuple(addrlist): + for addr in addrlist: self.addaddr(ifindex, addr) - if self.up: self.net_client.device_up(netif.localname) diff --git a/daemon/core/utils.py b/daemon/core/utils.py index c16d18b5..3b1ea46a 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -158,19 +158,6 @@ def which(command: str, required: bool) -> str: return found_path -def make_tuple(obj: Generic[T]) -> Tuple[T]: - """ - Create a tuple from an object, or return the object itself. - - :param obj: object to convert to a tuple - :return: converted tuple or the object itself - """ - if hasattr(obj, "__iter__"): - return tuple(obj) - else: - return (obj,) - - def make_tuple_fromstr(s: str, value_type: Callable[[str], T]) -> Tuple[T]: """ Create a tuple from a string. From 4cc9d3debfc17461a87abfe2bfd6e9da1e8fe67f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 9 Jun 2020 18:59:14 -0700 Subject: [PATCH 0323/1131] added pydoc for grpc client InterfaceHelper --- daemon/core/api/grpc/client.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 64b8d29f..0361a69b 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -113,6 +113,15 @@ class InterfaceHelper: def create_interface( self, node_id: int, interface_id: int, name: str = None, mac: str = None ) -> core_pb2.Interface: + """ + Create an interface protobuf object. + + :param node_id: node id to create interface for + :param interface_id: interface id + :param name: name of interface + :param mac: mac address for interface + :return: interface protobuf + """ interface_data = self.prefixes.gen_interface(node_id, name, mac) return core_pb2.Interface( id=interface_id, From a79ba1b8d32efd4416156a3cb4edb8b6348b10f8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 9 Jun 2020 19:48:29 -0700 Subject: [PATCH 0324/1131] daemon: added type hints to CoreEmu --- daemon/core/emulator/coreemu.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py index 90f75427..6a7f8b80 100644 --- a/daemon/core/emulator/coreemu.py +++ b/daemon/core/emulator/coreemu.py @@ -3,7 +3,7 @@ import logging import os import signal import sys -from typing import Mapping, Type +from typing import Dict, List, Type import core.services from core import configservices @@ -36,7 +36,7 @@ class CoreEmu: Provides logic for creating and configuring CORE sessions and the nodes within them. """ - def __init__(self, config: Mapping[str, str] = None) -> None: + def __init__(self, config: Dict[str, str] = None) -> None: """ Create a CoreEmu object. @@ -48,17 +48,17 @@ class CoreEmu: # configuration if config is None: config = {} - self.config = config + self.config: Dict[str, str] = config # session management - self.sessions = {} + self.sessions: Dict[int, Session] = {} # load services - self.service_errors = [] + self.service_errors: List[str] = [] self.load_services() # config services - self.service_manager = ConfigServiceManager() + self.service_manager: ConfigServiceManager = ConfigServiceManager() config_services_path = os.path.abspath(os.path.dirname(configservices.__file__)) self.service_manager.load(config_services_path) custom_dir = self.config.get("custom_config_services_dir") From 32ad8a9b683bd7625b5f9aff57e2cedaa4a33bb3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 9 Jun 2020 20:03:32 -0700 Subject: [PATCH 0325/1131] daemon: added type hinting to Session --- daemon/core/emulator/session.py | 70 ++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index a1e71612..a2b2670b 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -15,9 +15,17 @@ import time from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, TypeVar from core import constants, utils +from core.configservice.manager import ConfigServiceManager from core.emane.emanemanager import EmaneManager from core.emane.nodes import EmaneNet -from core.emulator.data import ConfigData, EventData, ExceptionData, FileData, LinkData +from core.emulator.data import ( + ConfigData, + EventData, + ExceptionData, + FileData, + LinkData, + NodeData, +) from core.emulator.distributed import DistributedController from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.enumerations import ( @@ -89,62 +97,62 @@ class Session: :param config: session configuration :param mkdir: flag to determine if a directory should be made """ - self.id = _id + self.id: int = _id # define and create session directory when desired - self.session_dir = os.path.join(tempfile.gettempdir(), f"pycore.{self.id}") + self.session_dir: str = os.path.join(tempfile.gettempdir(), f"pycore.{self.id}") if mkdir: os.mkdir(self.session_dir) - self.name = None - self.file_name = None - self.thumbnail = None - self.user = None - self.event_loop = EventLoop() - self.link_colors = {} + self.name: Optional[str] = None + self.file_name: Optional[str] = None + self.thumbnail: Optional[str] = None + self.user: Optional[str] = None + self.event_loop: EventLoop = EventLoop() + self.link_colors: Dict[int, str] = {} # dict of nodes: all nodes and nets - self.nodes = {} + self.nodes: Dict[int, NodeBase] = {} self._nodes_lock = threading.Lock() - self.state = EventTypes.DEFINITION_STATE - self._state_time = time.monotonic() - self._state_file = os.path.join(self.session_dir, "state") + self.state: EventTypes = EventTypes.DEFINITION_STATE + self._state_time: float = time.monotonic() + self._state_file: str = os.path.join(self.session_dir, "state") # hooks handlers - self._hooks = {} - self._state_hooks = {} + self._hooks: Dict[EventTypes, Tuple[str, str]] = {} + self._state_hooks: Dict[EventTypes, Callable[[int], None]] = {} self.add_state_hook( state=EventTypes.RUNTIME_STATE, hook=self.runtime_state_hook ) # handlers for broadcasting information - self.event_handlers = [] - self.exception_handlers = [] - self.node_handlers = [] - self.link_handlers = [] - self.file_handlers = [] - self.config_handlers = [] - self.shutdown_handlers = [] + self.event_handlers: List[Callable[[EventData], None]] = [] + self.exception_handlers: List[Callable[[ExceptionData], None]] = [] + self.node_handlers: List[Callable[[NodeData], None]] = [] + self.link_handlers: List[Callable[[LinkData], None]] = [] + self.file_handlers: List[Callable[[FileData], None]] = [] + self.config_handlers: List[Callable[[ConfigData], None]] = [] + self.shutdown_handlers: List[Callable[[Session], None]] = [] # session options/metadata - self.options = SessionConfig() + self.options: SessionConfig = SessionConfig() if not config: config = {} for key in config: value = config[key] self.options.set_config(key, value) - self.metadata = {} + self.metadata: Dict[str, str] = {} # distributed support and logic - self.distributed = DistributedController(self) + self.distributed: DistributedController = DistributedController(self) # initialize session feature helpers - self.location = GeoLocation() - self.mobility = MobilityManager(session=self) - self.services = CoreServices(session=self) - self.emane = EmaneManager(session=self) - self.sdt = Sdt(session=self) + self.location: GeoLocation = GeoLocation() + self.mobility: MobilityManager = MobilityManager(self) + self.services: CoreServices = CoreServices(self) + self.emane: EmaneManager = EmaneManager(self) + self.sdt: Sdt = Sdt(self) # initialize default node services self.services.default_services = { @@ -156,7 +164,7 @@ class Session: } # config services - self.service_manager = None + self.service_manager: Optional[ConfigServiceManager] = None @classmethod def get_node_class(cls, _type: NodeTypes) -> Type[NodeBase]: From 452e0720f2232f748c26a04fe1631d349e08cab3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 9 Jun 2020 21:03:19 -0700 Subject: [PATCH 0326/1131] daemon: added type hinting to DistributedControll and removed bad logic looking for tunnels during add_link --- daemon/core/emulator/distributed.py | 32 +++++------------- daemon/core/emulator/session.py | 48 ++++----------------------- daemon/core/emulator/sessionconfig.py | 8 ++--- 3 files changed, 18 insertions(+), 70 deletions(-) diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index 5f188cb0..3753e1c2 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -37,10 +37,10 @@ class DistributedServer: :param name: convenience name to associate with host :param host: host to connect to """ - self.name = name - self.host = host - self.conn = Connection(host, user="root") - self.lock = threading.Lock() + self.name: str = name + self.host: str = host + self.conn: Connection = Connection(host, user="root") + self.lock: threading.Lock = threading.Lock() def remote_cmd( self, cmd: str, env: Dict[str, str] = None, cwd: str = None, wait: bool = True @@ -117,10 +117,10 @@ class DistributedController: :param session: session """ - self.session = session - self.servers = OrderedDict() - self.tunnels = {} - self.address = self.session.options.get_config( + self.session: "Session" = session + self.servers: Dict[str, DistributedServer] = OrderedDict() + self.tunnels: Dict[int, Tuple[GreTap, GreTap]] = {} + self.address: str = self.session.options.get_config( "distributed_address", default=None ) @@ -178,13 +178,10 @@ class DistributedController: """ for node_id in self.session.nodes: node = self.session.nodes[node_id] - if not isinstance(node, CoreNetwork): continue - if isinstance(node, CtrlNet) and node.serverintf is not None: continue - for name in self.servers: server = self.servers[name] self.create_gre_tunnel(node, server) @@ -195,7 +192,6 @@ class DistributedController: """ Create gre tunnel using a pair of gre taps between the local and remote server. - :param node: node to create gre tunnel for :param server: server to create tunnel for @@ -243,15 +239,3 @@ class DistributedController: (self.session.id << 16) ^ utils.hashkey(n1_id) ^ (utils.hashkey(n2_id) << 8) ) return key & 0xFFFFFFFF - - def get_tunnel(self, n1_id: int, n2_id: int) -> GreTap: - """ - Return the GreTap between two nodes if it exists. - - :param n1_id: node one id - :param n2_id: node two id - :return: gre tap between nodes or None - """ - key = self.tunnel_key(n1_id, n2_id) - logging.debug("checking for tunnel key(%s) in: %s", key, self.tunnels) - return self.tunnels.get(key) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index a2b2670b..45c17743 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -42,7 +42,7 @@ from core.location.geo import GeoLocation from core.location.mobility import BasicRangeModel, MobilityManager from core.nodes.base import CoreNetworkBase, CoreNode, CoreNodeBase, NodeBase from core.nodes.docker import DockerNode -from core.nodes.interface import CoreInterface, GreTap +from core.nodes.interface import CoreInterface from core.nodes.lxd import LxcNode from core.nodes.network import ( CtrlNet, @@ -200,7 +200,6 @@ class Session: Optional[CoreNode], Optional[CoreNetworkBase], Optional[CoreNetworkBase], - GreTap, ]: """ Convenience method for retrieving nodes within link data. @@ -221,23 +220,6 @@ class Session: node_one = self.get_node(node_one_id, NodeBase) node_two = self.get_node(node_two_id, NodeBase) - # both node ids are provided - tunnel = self.distributed.get_tunnel(node_one_id, node_two_id) - logging.debug("tunnel between nodes: %s", tunnel) - if isinstance(tunnel, GreTapBridge): - net_one = tunnel - if tunnel.remotenum == node_one_id: - node_one = None - else: - node_two = None - # physical node connected via gre tap tunnel - # TODO: double check this cases type - elif tunnel: - if tunnel.remotenum == node_one_id: - node_one = None - else: - node_two = None - if isinstance(node_one, CoreNetworkBase): if not net_one: net_one = node_one @@ -253,14 +235,13 @@ class Session: node_two = None logging.debug( - "link node types n1(%s) n2(%s) net1(%s) net2(%s) tunnel(%s)", + "link node types n1(%s) n2(%s) net1(%s) net2(%s)", node_one, node_two, net_one, net_two, - tunnel, ) - return node_one, node_two, net_one, net_two, tunnel + return node_one, node_two, net_one, net_two def _link_wireless(self, objects: Iterable[CoreNodeBase], connect: bool) -> None: """ @@ -326,7 +307,7 @@ class Session: options = LinkOptions() # get node objects identified by link data - node_one, node_two, net_one, net_two, tunnel = self._link_nodes( + node_one, node_two, net_one, net_two = self._link_nodes( node_one_id, node_two_id ) @@ -415,23 +396,6 @@ class Session: net_two.setkey(key) if addresses: net_two.addrconfig(addresses) - - # physical node connected with tunnel - if not net_one and not net_two and (node_one or node_two): - if node_one and isinstance(node_one, PhysicalNode): - logging.info("adding link for physical node: %s", node_one.name) - addresses = interface_one.get_addresses() - node_one.adoptnetif( - tunnel, interface_one.id, interface_one.mac, addresses - ) - node_one.linkconfig(tunnel, options) - elif node_two and isinstance(node_two, PhysicalNode): - logging.info("adding link for physical node: %s", node_two.name) - addresses = interface_two.get_addresses() - node_two.adoptnetif( - tunnel, interface_two.id, interface_two.mac, addresses - ) - node_two.linkconfig(tunnel, options) finally: if node_one: node_one.lock.release() @@ -461,7 +425,7 @@ class Session: :raises core.CoreError: when no common network is found for link being deleted """ # get node objects identified by link data - node_one, node_two, net_one, net_two, _tunnel = self._link_nodes( + node_one, node_two, net_one, net_two = self._link_nodes( node_one_id, node_two_id ) @@ -573,7 +537,7 @@ class Session: options = LinkOptions() # get node objects identified by link data - node_one, node_two, net_one, net_two, _tunnel = self._link_nodes( + node_one, node_two, net_one, net_two = self._link_nodes( node_one_id, node_two_id ) diff --git a/daemon/core/emulator/sessionconfig.py b/daemon/core/emulator/sessionconfig.py index ffeccdc4..e22e852e 100644 --- a/daemon/core/emulator/sessionconfig.py +++ b/daemon/core/emulator/sessionconfig.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, List from core.config import ConfigurableManager, ConfigurableOptions, Configuration from core.emulator.enumerations import ConfigDataTypes, RegisterTlvs @@ -10,8 +10,8 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions): Provides session configuration. """ - name = "session" - options = [ + name: str = "session" + options: List[Configuration] = [ Configuration( _id="controlnet", _type=ConfigDataTypes.STRING, label="Control Network" ), @@ -57,7 +57,7 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions): label="SDT3D URL", ), ] - config_type = RegisterTlvs.UTILITY + config_type: RegisterTlvs = RegisterTlvs.UTILITY def __init__(self) -> None: super().__init__() From 6ee9590bdc3629e61ee128d77238041b58ac3c65 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 10 Jun 2020 08:52:51 -0700 Subject: [PATCH 0327/1131] daemon: finished class variable type hinting for core.nodes --- daemon/core/nodes/client.py | 4 ++-- daemon/core/nodes/docker.py | 12 ++++++------ daemon/core/nodes/lxd.py | 12 ++++++------ daemon/core/nodes/netclient.py | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/daemon/core/nodes/client.py b/daemon/core/nodes/client.py index d7642863..c004b814 100644 --- a/daemon/core/nodes/client.py +++ b/daemon/core/nodes/client.py @@ -20,8 +20,8 @@ class VnodeClient: :param name: name for client :param ctrlchnlname: control channel name """ - self.name = name - self.ctrlchnlname = ctrlchnlname + self.name: str = name + self.ctrlchnlname: str = ctrlchnlname def _verify_connection(self) -> None: """ diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index 684e8452..fa4b8f8b 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -2,7 +2,7 @@ import json import logging import os from tempfile import NamedTemporaryFile -from typing import TYPE_CHECKING, Callable, Dict +from typing import TYPE_CHECKING, Callable, Dict, Optional from core import utils from core.emulator.distributed import DistributedServer @@ -17,10 +17,10 @@ if TYPE_CHECKING: class DockerClient: def __init__(self, name: str, image: str, run: Callable[..., str]) -> None: - self.name = name - self.image = image - self.run = run - self.pid = None + self.name: str = name + self.image: str = image + self.run: Callable[..., str] = run + self.pid: Optional[str] = None def create_container(self) -> str: self.run( @@ -95,7 +95,7 @@ class DockerNode(CoreNode): """ if image is None: image = "ubuntu" - self.image = image + self.image: str = image super().__init__(session, _id, name, nodedir, start, server) def create_node_net_client(self, use_ovs: bool) -> LinuxNetClient: diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index 3b4c88c0..af906f01 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -3,7 +3,7 @@ import logging import os import time from tempfile import NamedTemporaryFile -from typing import TYPE_CHECKING, Callable, Dict +from typing import TYPE_CHECKING, Callable, Dict, Optional from core import utils from core.emulator.distributed import DistributedServer @@ -18,10 +18,10 @@ if TYPE_CHECKING: class LxdClient: def __init__(self, name: str, image: str, run: Callable[..., str]) -> None: - self.name = name - self.image = image - self.run = run - self.pid = None + self.name: str = name + self.image: str = image + self.run: Callable[..., str] = run + self.pid: Optional[int] = None def create_container(self) -> int: self.run(f"lxc launch {self.image} {self.name}") @@ -92,7 +92,7 @@ class LxcNode(CoreNode): """ if image is None: image = "ubuntu" - self.image = image + self.image: str = image super().__init__(session, _id, name, nodedir, start, server) def alive(self) -> bool: diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 29a70d18..25a10b99 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -19,7 +19,7 @@ class LinuxNetClient: :param run: function to run commands with """ - self.run = run + self.run: Callable[..., str] = run def set_hostname(self, name: str) -> None: """ From fd341bd69bd0874656b0e7f178e5cad0ba8e9fbb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 10 Jun 2020 09:01:38 -0700 Subject: [PATCH 0328/1131] daemon: added class variable type hinting to core.plugins --- daemon/core/plugins/sdt.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 06c23de5..8b4ec39f 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -5,7 +5,7 @@ sdt.py: Scripted Display Tool (SDT3D) helper import logging import socket import threading -from typing import TYPE_CHECKING, Optional +from typing import IO, TYPE_CHECKING, Dict, Optional, Set, Tuple from urllib.parse import urlparse from core import constants @@ -42,11 +42,11 @@ class Sdt: when a node position or link has changed. """ - DEFAULT_SDT_URL = "tcp://127.0.0.1:50000/" + DEFAULT_SDT_URL: str = "tcp://127.0.0.1:50000/" # default altitude (in meters) for flyto view - DEFAULT_ALT = 2500 + DEFAULT_ALT: int = 2500 # TODO: read in user"s nodes.conf here; below are default node types from the GUI - DEFAULT_SPRITES = [ + DEFAULT_SPRITES: Dict[str, str] = [ ("router", "router.gif"), ("host", "host.gif"), ("PC", "pc.gif"), @@ -65,14 +65,14 @@ class Sdt: :param session: session this manager is tied to """ - self.session = session - self.lock = threading.Lock() - self.sock = None - self.connected = False - self.url = self.DEFAULT_SDT_URL - self.address = None - self.protocol = None - self.network_layers = set() + 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 + self.address: Optional[Tuple[Optional[str], Optional[int]]] = None + self.protocol: Optional[str] = None + self.network_layers: Set[str] = set() self.session.node_handlers.append(self.handle_node_update) self.session.link_handlers.append(self.handle_link_update) From 784c4d241976a0385c4ee96886f49db329b27b26 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 10 Jun 2020 10:24:44 -0700 Subject: [PATCH 0329/1131] daemon: added core.location class variable type hinting --- daemon/core/emane/emanemodel.py | 5 +- daemon/core/location/event.py | 81 ++++++++-------- daemon/core/location/geo.py | 19 ++-- daemon/core/location/mobility.py | 152 +++++++++++++++---------------- daemon/tests/test_mobility.py | 10 +- 5 files changed, 137 insertions(+), 130 deletions(-) diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index f42caa14..7b5ff417 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -12,6 +12,7 @@ from core.emulator.emudata import LinkOptions from core.emulator.enumerations import ConfigDataTypes, TransportType 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 @@ -139,13 +140,13 @@ class EmaneModel(WirelessModel): """ logging.debug("emane model(%s) has no post setup tasks", self.name) - def update(self, moved: bool, moved_netifs: List[CoreInterface]) -> None: + def update(self, moved: List[CoreNode], moved_netifs: 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: were nodes moved + :param moved: moved nodes :param moved_netifs: interfaces that were moved :return: nothing """ diff --git a/daemon/core/location/event.py b/daemon/core/location/event.py index 8826c42b..7f8a33a1 100644 --- a/daemon/core/location/event.py +++ b/daemon/core/location/event.py @@ -6,7 +6,7 @@ import heapq import threading import time from functools import total_ordering -from typing import Any, Callable +from typing import Any, Callable, Dict, List, Optional, Tuple class Timer(threading.Thread): @@ -16,34 +16,33 @@ class Timer(threading.Thread): """ def __init__( - self, interval: float, function: Callable, args: Any = None, kwargs: Any = None + self, + interval: float, + func: Callable[..., None], + args: Tuple[Any] = None, + kwargs: Dict[Any, Any] = None, ) -> None: """ Create a Timer instance. :param interval: time interval - :param function: function to call when timer finishes + :param func: function to call when timer finishes :param args: function arguments :param kwargs: function keyword arguments """ super().__init__() - self.interval = interval - self.function = function - - self.finished = threading.Event() - self._running = threading.Lock() - + self.interval: float = interval + self.func: Callable[..., None] = func + self.finished: threading.Event = threading.Event() + self._running: threading.Lock = threading.Lock() # validate arguments were provided - if args: - self.args = args - else: - self.args = [] - + if args is None: + args = () + self.args: Tuple[Any] = args # validate keyword arguments were provided - if kwargs: - self.kwargs = kwargs - else: - self.kwargs = {} + if kwargs is None: + kwargs = {} + self.kwargs: Dict[Any, Any] = kwargs def cancel(self) -> bool: """ @@ -67,7 +66,7 @@ class Timer(threading.Thread): self.finished.wait(self.interval) with self._running: if not self.finished.is_set(): - self.function(*self.args, **self.kwargs) + self.func(*self.args, **self.kwargs) self.finished.set() @@ -78,7 +77,12 @@ class Event: """ def __init__( - self, eventnum: int, event_time: float, func: Callable, *args: Any, **kwds: Any + self, + eventnum: int, + event_time: float, + func: Callable[..., None], + *args: Any, + **kwds: Any ) -> None: """ Create an Event instance. @@ -89,12 +93,12 @@ class Event: :param args: function arguments :param kwds: function keyword arguments """ - self.eventnum = eventnum - self.time = event_time - self.func = func - self.args = args - self.kwds = kwds - self.canceled = False + self.eventnum: int = eventnum + self.time: float = event_time + self.func: Callable[..., None] = func + self.args: Tuple[Any] = args + self.kwds: Dict[Any, Any] = kwds + self.canceled: bool = False def __lt__(self, other: "Event") -> bool: result = self.time < other.time @@ -118,7 +122,6 @@ class Event: :return: nothing """ - # XXX not thread-safe self.canceled = True @@ -131,14 +134,14 @@ class EventLoop: """ Creates a EventLoop instance. """ - self.lock = threading.RLock() - self.queue = [] - self.eventnum = 0 - self.timer = None - self.running = False - self.start = None + self.lock: threading.RLock = threading.RLock() + self.queue: List[Event] = [] + self.eventnum: int = 0 + self.timer: Optional[Timer] = None + self.running: bool = False + self.start: Optional[float] = None - def __run_events(self) -> None: + def _run_events(self) -> None: """ Run events. @@ -161,9 +164,9 @@ class EventLoop: with self.lock: self.timer = None if schedule: - self.__schedule_event() + self._schedule_event() - def __schedule_event(self) -> None: + def _schedule_event(self) -> None: """ Schedule event. @@ -177,7 +180,7 @@ class EventLoop: delay = self.queue[0].time - time.monotonic() if self.timer: raise ValueError("timer was already set") - self.timer = Timer(delay, self.__run_events) + self.timer = Timer(delay, self._run_events) self.timer.daemon = True self.timer.start() @@ -194,7 +197,7 @@ class EventLoop: self.start = time.monotonic() for event in self.queue: event.time += self.start - self.__schedule_event() + self._schedule_event() def stop(self) -> None: """ @@ -242,5 +245,5 @@ class EventLoop: if self.timer is not None and self.timer.cancel(): self.timer = None if self.running and self.timer is None: - self.__schedule_event() + self._schedule_event() return event diff --git a/daemon/core/location/geo.py b/daemon/core/location/geo.py index 4ff56dd6..6c8eb651 100644 --- a/daemon/core/location/geo.py +++ b/daemon/core/location/geo.py @@ -6,6 +6,7 @@ import logging from typing import Tuple import pyproj +from pyproj import Transformer from core.emulator.enumerations import RegisterTlvs @@ -20,21 +21,23 @@ class GeoLocation: defined projections. """ - name = "location" - config_type = RegisterTlvs.UTILITY + name: str = "location" + config_type: RegisterTlvs = RegisterTlvs.UTILITY def __init__(self) -> None: """ Creates a GeoLocation instance. """ - self.to_pixels = pyproj.Transformer.from_crs( + self.to_pixels: Transformer = pyproj.Transformer.from_crs( CRS_WGS84, CRS_PROJ, always_xy=True ) - self.to_geo = pyproj.Transformer.from_crs(CRS_PROJ, CRS_WGS84, always_xy=True) - self.refproj = (0.0, 0.0, 0.0) - self.refgeo = (0.0, 0.0, 0.0) - self.refxyz = (0.0, 0.0, 0.0) - self.refscale = 1.0 + self.to_geo: Transformer = pyproj.Transformer.from_crs( + CRS_PROJ, CRS_WGS84, always_xy=True + ) + self.refproj: Tuple[float, float, float] = (0.0, 0.0, 0.0) + self.refgeo: Tuple[float, float, float] = (0.0, 0.0, 0.0) + self.refxyz: Tuple[float, float, float] = (0.0, 0.0, 0.0) + self.refscale: float = 1.0 def setrefgeo(self, lat: float, lon: float, alt: float) -> None: """ diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 3ca46418..87cd7141 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -9,7 +9,7 @@ import os import threading import time from functools import total_ordering -from typing import TYPE_CHECKING, Dict, List, Tuple +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple from core import utils from core.config import ConfigGroup, ConfigurableOptions, Configuration, ModelManager @@ -23,7 +23,7 @@ from core.emulator.enumerations import ( RegisterTlvs, ) from core.errors import CoreError -from core.nodes.base import CoreNode, NodeBase +from core.nodes.base import CoreNode from core.nodes.interface import CoreInterface from core.nodes.network import WlanNode @@ -47,7 +47,7 @@ class MobilityManager(ModelManager): :param session: session this manager is tied to """ super().__init__() - self.session = session + self.session: "Session" = session self.models[BasicRangeModel.name] = BasicRangeModel self.models[Ns2ScriptedMobility.name] = Ns2ScriptedMobility @@ -178,7 +178,7 @@ class MobilityManager(ModelManager): self.session.broadcast_event(event_data) def updatewlans( - self, moved: List[NodeBase], moved_netifs: List[CoreInterface] + self, moved: List[CoreNode], moved_netifs: List[CoreInterface] ) -> None: """ A mobility script has caused nodes in the 'moved' list to move. @@ -204,21 +204,21 @@ class WirelessModel(ConfigurableOptions): Used for managing arbitrary configuration parameters. """ - config_type = RegisterTlvs.WIRELESS - bitmap = None - position_callback = None + config_type: RegisterTlvs = RegisterTlvs.WIRELESS + bitmap: str = None + position_callback: Callable[[CoreInterface], None] = None - def __init__(self, session: "Session", _id: int): + def __init__(self, session: "Session", _id: int) -> None: """ Create a WirelessModel instance. :param session: core session we are tied to :param _id: object id """ - self.session = session - self.id = _id + self.session: "Session" = session + self.id: int = _id - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List: + def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ May be used if the model can populate the GUI with wireless (green) link lines. @@ -228,11 +228,11 @@ class WirelessModel(ConfigurableOptions): """ return [] - def update(self, moved: bool, moved_netifs: List[CoreInterface]) -> None: + def update(self, moved: List[CoreNode], moved_netifs: List[CoreInterface]) -> None: """ Update this wireless model. - :param moved: flag is it was moved + :param moved: moved nodes :param moved_netifs: moved network interfaces :return: nothing """ @@ -256,8 +256,8 @@ class BasicRangeModel(WirelessModel): the GUI. """ - name = "basic_range" - options = [ + name: str = "basic_range" + options: List[Configuration] = [ Configuration( _id="range", _type=ConfigDataTypes.UINT32, @@ -299,15 +299,15 @@ class BasicRangeModel(WirelessModel): :param _id: object id """ super().__init__(session, _id) - self.session = session - self.wlan = session.get_node(_id, WlanNode) - self._netifs = {} - self._netifslock = threading.Lock() - self.range = 0 - self.bw = None - self.delay = None - self.loss = None - self.jitter = None + self.session: "Session" = session + self.wlan: WlanNode = session.get_node(_id, WlanNode) + self._netifs: Dict[CoreInterface, Tuple[float, float, float]] = {} + self._netifslock: threading.Lock = threading.Lock() + self.range: int = 0 + self.bw: Optional[int] = None + self.delay: Optional[int] = None + self.loss: Optional[float] = None + self.jitter: Optional[int] = None def _get_config(self, current_value: int, config: Dict[str, str], name: str) -> int: """ @@ -374,14 +374,14 @@ class BasicRangeModel(WirelessModel): position_callback = set_position - def update(self, moved: bool, moved_netifs: List[CoreInterface]) -> None: + def update(self, moved: List[CoreNode], moved_netifs: 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: flag is it was moved + :param moved: moved nodes :param moved_netifs: moved network interfaces :return: nothing """ @@ -535,29 +535,35 @@ class WayPoint: Maintains information regarding waypoints. """ - def __init__(self, time: float, nodenum: int, coords, speed: float): + def __init__( + self, + _time: float, + node_id: int, + coords: Tuple[float, float, float], + speed: float, + ) -> None: """ Creates a WayPoint instance. - :param time: waypoint time - :param nodenum: node id + :param _time: waypoint time + :param node_id: node id :param coords: waypoint coordinates :param speed: waypoint speed """ - self.time = time - self.nodenum = nodenum - self.coords = coords - self.speed = speed + self.time: float = _time + self.node_id: int = node_id + self.coords: Tuple[float, float, float] = coords + self.speed: float = speed def __eq__(self, other: "WayPoint") -> bool: - return (self.time, self.nodenum) == (other.time, other.nodenum) + return (self.time, self.node_id) == (other.time, other.node_id) def __ne__(self, other: "WayPoint") -> bool: return not self == other def __lt__(self, other: "WayPoint") -> bool: if self.time == other.time: - return self.nodenum < other.nodenum + return self.node_id < other.node_id else: return self.time < other.time @@ -567,12 +573,11 @@ class WayPointMobility(WirelessModel): Abstract class for mobility models that set node waypoints. """ - name = "waypoint" - config_type = RegisterTlvs.MOBILITY - - STATE_STOPPED = 0 - STATE_RUNNING = 1 - STATE_PAUSED = 2 + name: str = "waypoint" + config_type: RegisterTlvs = RegisterTlvs.MOBILITY + STATE_STOPPED: int = 0 + STATE_RUNNING: int = 1 + STATE_PAUSED: int = 2 def __init__(self, session: "Session", _id: int) -> None: """ @@ -583,20 +588,21 @@ class WayPointMobility(WirelessModel): :return: """ super().__init__(session=session, _id=_id) - self.state = self.STATE_STOPPED - self.queue = [] - self.queue_copy = [] - self.points = {} - self.initial = {} - self.lasttime = None - self.endtime = None - self.wlan = session.get_node(_id, WlanNode) + self.state: int = self.STATE_STOPPED + self.queue: List[WayPoint] = [] + self.queue_copy: List[WayPoint] = [] + self.points: Dict[int, WayPoint] = {} + self.initial: Dict[int, WayPoint] = {} + self.lasttime: Optional[float] = None + self.endtime: Optional[int] = None + self.timezero: float = 0.0 + self.wlan: WlanNode = session.get_node(_id, WlanNode) # these are really set in child class via confmatrix - self.loop = False - self.refresh_ms = 50 + self.loop: bool = False + self.refresh_ms: int = 50 # flag whether to stop scheduling when queue is empty # (ns-3 sets this to False as new waypoints may be added from trace) - self.empty_queue_stop = True + self.empty_queue_stop: bool = True def runround(self) -> None: """ @@ -684,16 +690,11 @@ class WayPointMobility(WirelessModel): self.setnodeposition(node, x2, y2, z2) del self.points[node.id] return True - # speed can be a velocity vector or speed value - if isinstance(speed, (float, int)): - # linear speed value - alpha = math.atan2(y2 - y1, x2 - x1) - sx = speed * math.cos(alpha) - sy = speed * math.sin(alpha) - else: - # velocity vector - sx = speed[0] - sy = speed[1] + + # linear speed value + alpha = math.atan2(y2 - y1, x2 - x1) + sx = speed * math.cos(alpha) + sy = speed * math.sin(alpha) # calculate dt * speed = distance moved dx = sx * dt @@ -776,7 +777,7 @@ class WayPointMobility(WirelessModel): if self.queue[0].time > now: break wp = heapq.heappop(self.queue) - self.points[wp.nodenum] = wp + self.points[wp.node_id] = wp def copywaypoints(self) -> None: """ @@ -876,8 +877,8 @@ class Ns2ScriptedMobility(WayPointMobility): BonnMotion. """ - name = "ns2script" - options = [ + name: str = "ns2script" + options: List[Configuration] = [ Configuration( _id="file", _type=ConfigDataTypes.STRING, label="mobility script file" ), @@ -923,7 +924,7 @@ class Ns2ScriptedMobility(WayPointMobility): ConfigGroup("ns-2 Mobility Script Parameters", 1, len(cls.configurations())) ] - def __init__(self, session: "Session", _id: int): + def __init__(self, session: "Session", _id: int) -> None: """ Creates a Ns2ScriptedMobility instance. @@ -931,17 +932,14 @@ class Ns2ScriptedMobility(WayPointMobility): :param _id: object id """ super().__init__(session, _id) - self._netifs = {} - self._netifslock = threading.Lock() - - self.file = None - self.refresh_ms = None - self.loop = None - self.autostart = None - self.nodemap = {} - self.script_start = None - self.script_pause = None - self.script_stop = None + self.file: Optional[str] = None + self.refresh_ms: Optional[int] = None + self.loop: Optional[bool] = None + self.autostart: Optional[str] = None + self.nodemap: Dict[int, int] = {} + self.script_start: Optional[str] = None + self.script_pause: Optional[str] = None + self.script_stop: Optional[str] = None def update_config(self, config: Dict[str, str]) -> None: self.file = config["file"] diff --git a/daemon/tests/test_mobility.py b/daemon/tests/test_mobility.py index e2e8f90e..aab7b30f 100644 --- a/daemon/tests/test_mobility.py +++ b/daemon/tests/test_mobility.py @@ -2,15 +2,17 @@ import pytest from core.location.mobility import WayPoint +POSITION = (0.0, 0.0, 0.0) + class TestMobility: @pytest.mark.parametrize( "wp1, wp2, expected", [ - (WayPoint(10.0, 1, [0, 0], 1.0), WayPoint(1.0, 2, [0, 0], 1.0), False), - (WayPoint(1.0, 1, [0, 0], 1.0), WayPoint(10.0, 2, [0, 0], 1.0), True), - (WayPoint(1.0, 1, [0, 0], 1.0), WayPoint(1.0, 2, [0, 0], 1.0), True), - (WayPoint(1.0, 2, [0, 0], 1.0), WayPoint(1.0, 1, [0, 0], 1.0), False), + (WayPoint(10.0, 1, POSITION, 1.0), WayPoint(1.0, 2, POSITION, 1.0), False), + (WayPoint(1.0, 1, POSITION, 1.0), WayPoint(10.0, 2, POSITION, 1.0), True), + (WayPoint(1.0, 1, POSITION, 1.0), WayPoint(1.0, 2, POSITION, 1.0), True), + (WayPoint(1.0, 2, POSITION, 1.0), WayPoint(1.0, 1, POSITION, 1.0), False), ], ) def test_waypoint_lessthan(self, wp1, wp2, expected): From a389dc6240ba744418152855cb093367c33c06dd Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 10 Jun 2020 10:31:13 -0700 Subject: [PATCH 0330/1131] daemon: improve type hinting for WayPoint --- daemon/core/location/mobility.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 87cd7141..e9efa16b 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -539,7 +539,7 @@ class WayPoint: self, _time: float, node_id: int, - coords: Tuple[float, float, float], + coords: Tuple[float, float, Optional[float]], speed: float, ) -> None: """ @@ -552,7 +552,7 @@ class WayPoint: """ self.time: float = _time self.node_id: int = node_id - self.coords: Tuple[float, float, float] = coords + self.coords: Tuple[float, float, Optional[float]] = coords self.speed: float = speed def __eq__(self, other: "WayPoint") -> bool: @@ -737,7 +737,13 @@ class WayPointMobility(WirelessModel): self.session.mobility.updatewlans(moved, moved_netifs) def addwaypoint( - self, _time: float, nodenum: int, x: float, y: float, z: float, speed: float + self, + _time: float, + nodenum: int, + x: float, + y: float, + z: Optional[float], + speed: float, ) -> None: """ Waypoints are pushed to a heapq, sorted by time. From 39fd11efb304d93377fcb599f5ed82bf5d1574b0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 10 Jun 2020 10:40:24 -0700 Subject: [PATCH 0331/1131] daemon: added missing type hint to core.nodes.interface.CoreInterface --- daemon/core/nodes/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 16c242e9..e73e2989 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -50,7 +50,7 @@ class CoreInterface: self.mtu: int = mtu self.net: Optional[CoreNetworkBase] = None self.othernet: Optional[CoreNetworkBase] = None - self._params = {} + self._params: Dict[str, float] = {} self.addrlist: List[str] = [] self.hwaddr: Optional[str] = None # placeholder position hook From 9ed42cfba894c760b70a5711e46c55dec7ee5473 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 10 Jun 2020 11:04:33 -0700 Subject: [PATCH 0332/1131] pygui: avoid issue when joining opened xml that has a node with no ip4 address --- daemon/core/gui/interface.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index 437bd37c..1973fe99 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -12,7 +12,9 @@ if TYPE_CHECKING: from core.gui.graph.node import CanvasNode -def get_index(interface: "core_pb2.Interface") -> int: +def get_index(interface: "core_pb2.Interface") -> Optional[int]: + if not interface.ip4: + return None net = netaddr.IPNetwork(f"{interface.ip4}/{interface.ip4mask}") ip_value = net.value cidr_value = net.cidr.value @@ -108,7 +110,8 @@ class InterfaceManager: self.used_subnets.pop(subnets.key(), None) else: index = get_index(interface) - subnets.used_indexes.discard(index) + if index is not None: + subnets.used_indexes.discard(index) self.current_subnets = None def joined(self, links: List["core_pb2.Link"]) -> None: @@ -123,6 +126,8 @@ class InterfaceManager: for interface in interfaces: subnets = self.get_subnets(interface) index = get_index(interface) + if index is None: + continue subnets.used_indexes.add(index) if subnets.key() not in self.used_subnets: self.used_subnets[subnets.key()] = subnets From ccf2646c00fd5641137d20f578ab448e97e99ee9 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 11 Jun 2020 13:59:29 -0700 Subject: [PATCH 0333/1131] daemon: refactored add_link,update_link,delete_link to have more specific logic, refactored CoreNodeBase to have newnetif and for it to return the interface created --- daemon/core/api/tlv/corehandlers.py | 2 +- daemon/core/emulator/session.py | 510 ++++++++++------------------ daemon/core/nodes/base.py | 37 +- daemon/core/nodes/network.py | 8 +- daemon/core/nodes/physical.py | 10 +- daemon/tests/test_nodes.py | 20 +- 6 files changed, 232 insertions(+), 355 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index f3e1fbaa..5531e5af 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -767,7 +767,7 @@ class CoreHandler(socketserver.BaseRequestHandler): ip6_mask=message.get_tlv(LinkTlvs.INTERFACE2_IP6_MASK.value), ) - link_type = None + link_type = LinkTypes.WIRED link_type_value = message.get_tlv(LinkTlvs.TYPE.value) if link_type_value is not None: link_type = LinkTypes(link_type_value) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 45c17743..54486bfb 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -12,7 +12,7 @@ import subprocess import tempfile import threading import time -from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, TypeVar +from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar from core import constants, utils from core.configservice.manager import ConfigServiceManager @@ -193,76 +193,28 @@ class Session: raise CoreError(f"invalid node class: {_class}") return node_type - def _link_nodes( - self, node_one_id: int, node_two_id: int - ) -> Tuple[ - Optional[CoreNode], - Optional[CoreNode], - Optional[CoreNetworkBase], - Optional[CoreNetworkBase], - ]: - """ - Convenience method for retrieving nodes within link data. - - :param node_one_id: node one id - :param node_two_id: node two id - :return: nodes, network nodes if present, and tunnel if present - """ - logging.debug( - "link message between node1(%s) and node2(%s)", node_one_id, node_two_id - ) - - # values to fill - net_one = None - net_two = None - - # retrieve node one - node_one = self.get_node(node_one_id, NodeBase) - node_two = self.get_node(node_two_id, NodeBase) - - if isinstance(node_one, CoreNetworkBase): - if not net_one: - net_one = node_one - else: - net_two = node_one - node_one = None - - if isinstance(node_two, CoreNetworkBase): - if not net_one: - net_one = node_two - else: - net_two = node_two - node_two = None - - logging.debug( - "link node types n1(%s) n2(%s) net1(%s) net2(%s)", - node_one, - node_two, - net_one, - net_two, - ) - return node_one, node_two, net_one, net_two - - def _link_wireless(self, objects: Iterable[CoreNodeBase], connect: bool) -> None: + def _link_wireless( + self, node_one: CoreNodeBase, node_two: CoreNodeBase, connect: bool + ) -> None: """ Objects to deal with when connecting/disconnecting wireless links. - :param objects: possible objects to deal with + :param node_one: node one for wireless link + :param node_two: node two for wireless link :param connect: link interfaces if True, unlink otherwise :return: nothing :raises core.CoreError: when objects to link is less than 2, or no common networks are found """ - objects = [x for x in objects if x] - if len(objects) < 2: - raise CoreError(f"wireless link failure: {objects}") - logging.debug( - "handling wireless linking objects(%s) connect(%s)", objects, connect + logging.info( + "handling wireless linking node1(%s) node2(%s): %s", + node_one.name, + node_two.name, + connect, ) - common_networks = objects[0].commonnets(objects[1]) + common_networks = node_one.commonnets(node_one) if not common_networks: raise CoreError("no common network found for wireless link/unlink") - for common_network, interface_one, interface_two in common_networks: if not isinstance(common_network, (WlanNode, EmaneNet)): logging.info( @@ -270,13 +222,6 @@ class Session: common_network, ) continue - - logging.info( - "wireless linking connect(%s): %s - %s", - connect, - interface_one, - interface_two, - ) if connect: common_network.link(interface_one, interface_two) else: @@ -305,105 +250,70 @@ class Session: """ if not options: options = LinkOptions() + node1 = self.get_node(node_one_id, NodeBase) + node2 = self.get_node(node_two_id, NodeBase) + node1_interface = None + node2_interface = None - # get node objects identified by link data - node_one, node_two, net_one, net_two = self._link_nodes( - node_one_id, node_two_id - ) - - if node_one: - node_one.lock.acquire() - if node_two: - node_two.lock.acquire() - - node_one_interface = None - node_two_interface = None - - try: - # wireless link - if options.type == LinkTypes.WIRELESS: - objects = [node_one, node_two, net_one, net_two] - self._link_wireless(objects, connect=True) - # wired link + # wireless link + if options.type == LinkTypes.WIRELESS: + if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): + self._link_wireless(node1, node2, connect=True) else: - # 2 nodes being linked, ptp network - if all([node_one, node_two]) and not net_one: - logging.info( - "adding link for peer to peer nodes: %s - %s", - node_one.name, - node_two.name, - ) - start = self.state.should_start() - net_one = self.create_node(PtpNet, start=start) - - # node to network - if node_one and net_one: - logging.info( - "adding link from node to network: %s - %s", - node_one.name, - net_one.name, - ) - ifindex = node_one.newnetif(net_one, interface_one) - node_one_interface = node_one.netif(ifindex) - wireless_net = isinstance(net_one, (EmaneNet, WlanNode)) - if not wireless_net: - net_one.linkconfig(node_one_interface, options) - - # network to node - if node_two and net_one: - logging.info( - "adding link from network to node: %s - %s", - node_two.name, - net_one.name, - ) - ifindex = node_two.newnetif(net_one, interface_two) - node_two_interface = node_two.netif(ifindex) - wireless_net = isinstance(net_one, (EmaneNet, WlanNode)) - if not options.unidirectional and not wireless_net: - net_one.linkconfig(node_two_interface, options) - - # network to network - if net_one and net_two: - logging.info( - "adding link from network to network: %s - %s", - net_one.name, - net_two.name, - ) - interface = net_one.linknet(net_two) - node_one_interface = interface - net_one.linkconfig(interface, options) - if not options.unidirectional: - interface.swapparams("_params_up") - net_two.linkconfig(interface, options) - interface.swapparams("_params_up") - - # a tunnel node was found for the nodes - addresses = [] - if not node_one and all([net_one, interface_one]): - addresses.extend(interface_one.get_addresses()) - if not node_two and all([net_two, interface_two]): - addresses.extend(interface_two.get_addresses()) - - # tunnel node logic - key = options.key - if key and isinstance(net_one, TunnelNode): - logging.info("setting tunnel key for: %s", net_one.name) - net_one.setkey(key) - if addresses: - net_one.addrconfig(addresses) - if key and isinstance(net_two, TunnelNode): - logging.info("setting tunnel key for: %s", net_two.name) - net_two.setkey(key) - if addresses: - net_two.addrconfig(addresses) - finally: - if node_one: - node_one.lock.release() - if node_two: - node_two.lock.release() + raise CoreError( + f"cannot wireless link node1({type(node1)}) node2({type(node2)})" + ) + # wired link + else: + # peer to peer link + if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): + logging.info("linking ptp: %s - %s", node1.name, node2.name) + start = self.state.should_start() + ptp = self.create_node(PtpNet, start=start) + node1_interface = node1.newnetif(ptp, interface_one) + node2_interface = node2.newnetif(ptp, interface_two) + ptp.linkconfig(node1_interface, options) + if not options.unidirectional: + ptp.linkconfig(node2_interface, options) + # link node to net + elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase): + node1_interface = node1.newnetif(node2, interface_one) + if not isinstance(node2, (EmaneNet, WlanNode)): + node2.linkconfig(node1_interface, options) + # link net to node + elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): + node2_interface = node2.newnetif(node1, interface_two) + wireless_net = isinstance(node1, (EmaneNet, WlanNode)) + if not options.unidirectional and not wireless_net: + node1.linkconfig(node2_interface, options) + # network to network + elif isinstance(node1, CoreNetworkBase) and isinstance( + node2, CoreNetworkBase + ): + logging.info( + "linking network to network: %s - %s", node1.name, node2.name + ) + node1_interface = node1.linknet(node2) + node1.linkconfig(node1_interface, options) + if not options.unidirectional: + node1_interface.swapparams("_params_up") + node2.linkconfig(node1_interface, options) + node1_interface.swapparams("_params_up") + else: + raise CoreError( + f"cannot link node1({type(node1)}) node2({type(node2)})" + ) + # configure tunnel nodes + key = options.key + if isinstance(node1, TunnelNode): + logging.info("setting tunnel key for: %s", node1.name) + node1.setkey(key, interface_one) + if isinstance(node2, TunnelNode): + logging.info("setting tunnel key for: %s", node2.name) + node2.setkey(key, interface_two) self.sdt.add_link(node_one_id, node_two_id) - return node_one_interface, node_two_interface + return node1_interface, node2_interface def delete_link( self, @@ -424,93 +334,52 @@ class Session: :return: nothing :raises core.CoreError: when no common network is found for link being deleted """ - # get node objects identified by link data - node_one, node_two, net_one, net_two = self._link_nodes( - node_one_id, node_two_id + node1 = self.get_node(node_one_id, NodeBase) + node2 = self.get_node(node_two_id, NodeBase) + logging.info( + "deleting link(%s) node(%s):interface(%s) node(%s):interface(%s)", + link_type.name, + node1.name, + interface_one_id, + node2.name, + interface_two_id, ) - if node_one: - node_one.lock.acquire() - if node_two: - node_two.lock.acquire() - - try: - # wireless link - if link_type == LinkTypes.WIRELESS: - objects = [node_one, node_two, net_one, net_two] - self._link_wireless(objects, connect=False) - # wired link + # wireless link + if link_type == LinkTypes.WIRELESS: + if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): + self._link_wireless(node1, node2, connect=False) else: - if all([node_one, node_two]): - # TODO: fix this for the case where ifindex[1,2] are not specified - # a wired unlink event, delete the connecting bridge - interface_one = node_one.netif(interface_one_id) - interface_two = node_two.netif(interface_two_id) - - # get interfaces from common network, if no network node - # otherwise get interfaces between a node and network - if not interface_one and not interface_two: - common_networks = node_one.commonnets(node_two) - for ( - network, - common_interface_one, - common_interface_two, - ) in common_networks: - if (net_one and network == net_one) or not net_one: - interface_one = common_interface_one - interface_two = common_interface_two - break - - if all([interface_one, interface_two]) and any( - [interface_one.net, interface_two.net] - ): - if interface_one.net != interface_two.net and all( - [interface_one.up, interface_two.up] - ): - raise CoreError("no common network found") - - logging.info( - "deleting link node(%s):interface(%s) node(%s):interface(%s)", - node_one.name, - interface_one.name, - node_two.name, - interface_two.name, - ) - net_one = interface_one.net - interface_one.detachnet() - interface_two.detachnet() - if net_one.numnetif() == 0: - self.delete_node(net_one.id) - node_one.delnetif(interface_one.netindex) - node_two.delnetif(interface_two.netindex) - elif node_one and net_one: - interface = node_one.netif(interface_one_id) - if interface: - logging.info( - "deleting link node(%s):interface(%s) node(%s)", - node_one.name, - interface.name, - net_one.name, - ) - interface.detachnet() - node_one.delnetif(interface.netindex) - elif node_two and net_one: - interface = node_two.netif(interface_two_id) - if interface: - logging.info( - "deleting link node(%s):interface(%s) node(%s)", - node_two.name, - interface.name, - net_one.name, - ) - interface.detachnet() - node_two.delnetif(interface.netindex) - finally: - if node_one: - node_one.lock.release() - if node_two: - node_two.lock.release() - + raise CoreError( + "cannot delete wireless link " + f"node1({type(node1)}) node2({type(node2)})" + ) + # wired link + else: + if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): + interface1 = node1.netif(interface_one_id) + interface2 = node2.netif(interface_two_id) + if not interface1: + raise CoreError( + f"node({node1.name}) missing interface({interface_one_id})" + ) + if not interface2: + raise CoreError( + f"node({node2.name}) missing interface({interface_two_id})" + ) + if interface1.net != interface2.net: + raise CoreError( + f"node1({node1.name}) node2({node2.name}) " + "not connected to same net" + ) + ptp = interface1.net + node1.delnetif(interface_one_id) + node2.delnetif(interface_two_id) + self.delete_node(ptp.id) + elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase): + node1.delnetif(interface_one_id) + elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): + node2.delnetif(interface_two_id) self.sdt.delete_link(node_one_id, node_two_id) def update_link( @@ -530,84 +399,79 @@ class Session: :param interface_two_id: interface id for node two :param options: data to update link with :return: nothing - :raises core.CoreError: when updating a wireless type link, when there is a unknown - link between networks + :raises core.CoreError: when updating a wireless type link, when there is a + unknown link between networks """ if not options: options = LinkOptions() - - # get node objects identified by link data - node_one, node_two, net_one, net_two = self._link_nodes( - node_one_id, node_two_id + node1 = self.get_node(node_one_id, NodeBase) + node2 = self.get_node(node_two_id, NodeBase) + logging.info( + "update link(%s) node(%s):interface(%s) node(%s):interface(%s)", + options.type.name, + node1.name, + interface_one_id, + node2.name, + interface_two_id, ) - if node_one: - node_one.lock.acquire() - if node_two: - node_two.lock.acquire() - - try: - # wireless link - if options.type == LinkTypes.WIRELESS: - raise CoreError("cannot update wireless link") - else: - if not node_one and not node_two: - if net_one and net_two: - # modify link between nets - interface = net_one.getlinknetif(net_two) - upstream = False - - if not interface: - upstream = True - interface = net_two.getlinknetif(net_one) - - if not interface: - raise CoreError("modify unknown link between nets") - - if upstream: - interface.swapparams("_params_up") - net_one.linkconfig(interface, options) - interface.swapparams("_params_up") - else: - net_one.linkconfig(interface, options) - - if not options.unidirectional: - if upstream: - net_two.linkconfig(interface, options) - else: - interface.swapparams("_params_up") - net_two.linkconfig(interface, options) - interface.swapparams("_params_up") - else: - raise CoreError("modify link for unknown nodes") - elif not node_one: - # node1 = layer 2node, node2 = layer3 node - interface = node_two.netif(interface_two_id) - net_one.linkconfig(interface, options) - elif not node_two: - # node2 = layer 2node, node1 = layer3 node - interface = node_one.netif(interface_one_id) - net_one.linkconfig(interface, options) + # wireless link + if options.type == LinkTypes.WIRELESS: + raise CoreError("cannot update wireless link") + else: + if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): + interface1 = node1.netif(interface_one_id) + interface2 = node2.netif(interface_two_id) + if not interface1: + raise CoreError( + f"node({node1.name}) missing interface({interface_one_id})" + ) + if not interface2: + raise CoreError( + f"node({node2.name}) missing interface({interface_two_id})" + ) + if interface1.net != interface2.net: + raise CoreError( + f"node1({node1.name}) node2({node2.name}) " + "not connected to same net" + ) + ptp = interface1.net + ptp.linkconfig(interface1, options, interface2) + if not options.unidirectional: + ptp.linkconfig(interface2, options, interface1) + elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase): + interface = node1.netif(interface_one_id) + node2.linkconfig(interface, options) + elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): + interface = node2.netif(interface_two_id) + node1.linkconfig(interface, options) + elif isinstance(node1, CoreNetworkBase) and isinstance( + node2, CoreNetworkBase + ): + interface = node1.getlinknetif(node2) + upstream = False + if not interface: + upstream = True + interface = node2.getlinknetif(node1) + if not interface: + raise CoreError("modify unknown link between nets") + if upstream: + interface.swapparams("_params_up") + node1.linkconfig(interface, options) + interface.swapparams("_params_up") else: - common_networks = node_one.commonnets(node_two) - if not common_networks: - raise CoreError("no common network found") - - for net_one, interface_one, interface_two in common_networks: - if ( - interface_one_id is not None - and interface_one_id != node_one.getifindex(interface_one) - ): - continue - - net_one.linkconfig(interface_one, options, interface_two) - if not options.unidirectional: - net_one.linkconfig(interface_two, options, interface_one) - finally: - if node_one: - node_one.lock.release() - if node_two: - node_two.lock.release() + node1.linkconfig(interface, options) + if not options.unidirectional: + if upstream: + node2.linkconfig(interface, options) + else: + interface.swapparams("_params_up") + node2.linkconfig(interface, options) + interface.swapparams("_params_up") + else: + raise CoreError( + f"cannot update link node1({type(node1)}) node2({type(node2)})" + ) def _next_node_id(self) -> int: """ @@ -1345,17 +1209,15 @@ class Session: :return: True if node deleted, False otherwise """ # delete node and check for session shutdown if a node was removed - logging.info("deleting node(%s)", _id) node = None with self._nodes_lock: if _id in self.nodes: node = self.nodes.pop(_id) - + logging.info("deleted node(%s)", node.name) if node: node.shutdown() self.sdt.delete_node(_id) self.check_shutdown() - return node is not None def delete_nodes(self) -> None: @@ -1767,15 +1629,15 @@ class Session: try: ip4 = control_net.prefix[node.id] ip4_mask = control_net.prefix.prefixlen - interface = InterfaceData( + interface_data = InterfaceData( id=control_net.CTRLIF_IDX_BASE + net_index, name=f"ctrl{net_index}", mac=utils.random_mac(), ip4=ip4, ip4_mask=ip4_mask, ) - ifindex = node.newnetif(control_net, interface) - node.netif(ifindex).control = True + interface = node.newnetif(control_net, interface_data) + interface.control = True except ValueError: msg = f"Control interface not added to node {node.id}. " msg += f"Invalid control network prefix ({control_net.prefix}). " diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 0c76d6a2..498a9beb 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -355,10 +355,11 @@ class CoreNodeBase(NodeBase): :return: nothing """ if ifindex not in self._netif: - raise ValueError(f"ifindex {ifindex} does not exist") + raise CoreError(f"node({self.name}) ifindex({ifindex}) does not exist") netif = self._netif.pop(ifindex) + logging.info("node(%s) removing interface(%s)", self.name, netif.name) + netif.detachnet() netif.shutdown() - del netif def netif(self, ifindex: int) -> Optional[CoreInterface]: """ @@ -473,6 +474,18 @@ class CoreNodeBase(NodeBase): """ raise NotImplementedError + def newnetif( + self, net: "CoreNetworkBase", interface: InterfaceData + ) -> CoreInterface: + """ + Create a new network interface. + + :param net: network to associate with + :param interface: interface data for new interface + :return: interface index + """ + raise NotImplementedError + class CoreNode(CoreNodeBase): """ @@ -846,7 +859,9 @@ class CoreNode(CoreNodeBase): interface_name = self.ifname(ifindex) self.node_net_client.device_up(interface_name) - def newnetif(self, net: "CoreNetworkBase", interface: InterfaceData) -> int: + def newnetif( + self, net: "CoreNetworkBase", interface: InterfaceData + ) -> CoreInterface: """ Create a new network interface. @@ -868,16 +883,16 @@ class CoreNode(CoreNodeBase): netif.sethwaddr(interface.mac) for address in addresses: netif.addaddr(address) - return ifindex else: ifindex = self.newveth(interface.id, interface.name) - self.attachnet(ifindex, net) - if interface.mac: - self.sethwaddr(ifindex, interface.mac) - for address in addresses: - self.addaddr(ifindex, address) - self.ifup(ifindex) - return ifindex + self.attachnet(ifindex, net) + if interface.mac: + self.sethwaddr(ifindex, interface.mac) + for address in addresses: + self.addaddr(ifindex, address) + self.ifup(ifindex) + netif = self.netif(ifindex) + return netif def addfile(self, srcname: str, filename: str) -> None: """ diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 095fbe9b..6d6ad589 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -12,7 +12,7 @@ import netaddr from core import utils from core.constants import EBTABLES_BIN, TC_BIN from core.emulator.data import LinkData, NodeData -from core.emulator.emudata import LinkOptions +from core.emulator.emudata import InterfaceData, LinkOptions from core.emulator.enumerations import ( LinkTypes, MessageFlags, @@ -697,15 +697,19 @@ class GreTapBridge(CoreNetwork): ) self.attach(self.gretap) - def setkey(self, key: int) -> None: + def setkey(self, key: int, interface_data: InterfaceData) -> None: """ Set the GRE key used for the GreTap device. This needs to be set prior to instantiating the GreTap device (before addrconfig). :param key: gre key + :param interface_data: interface data for setting up tunnel key :return: nothing """ self.grekey = key + addresses = interface_data.get_addresses() + if addresses: + self.addrconfig(addresses) class CtrlNet(CoreNetwork): diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index ee00c705..6faa7824 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -157,7 +157,7 @@ class PhysicalNode(CoreNodeBase): self.ifindex += 1 return ifindex - def newnetif(self, net: CoreNetworkBase, interface: InterfaceData) -> int: + def newnetif(self, net: CoreNetworkBase, interface: InterfaceData) -> CoreInterface: logging.info("creating interface") addresses = interface.get_addresses() ifindex = interface.id @@ -171,12 +171,12 @@ class PhysicalNode(CoreNodeBase): # tunnel to net not built yet, so build it now and adopt it _, remote_tap = self.session.distributed.create_gre_tunnel(net, self.server) self.adoptnetif(remote_tap, ifindex, interface.mac, addresses) - return ifindex + return remote_tap else: # this is reached when configuring services (self.up=False) netif = GreTap(node=self, name=name, session=self.session, start=False) self.adoptnetif(netif, ifindex, interface.mac, addresses) - return ifindex + return netif def privatedir(self, path: str) -> None: if path[0] != "/": @@ -297,7 +297,7 @@ class Rj45Node(CoreNodeBase): self.up = False self.restorestate() - def newnetif(self, net: CoreNetworkBase, interface: InterfaceData) -> int: + def newnetif(self, net: CoreNetworkBase, interface: InterfaceData) -> CoreInterface: """ This is called when linking with another node. Since this node represents an interface, we do not create another object here, @@ -320,7 +320,7 @@ class Rj45Node(CoreNodeBase): self.interface.attachnet(net) for addr in interface.get_addresses(): self.addaddr(addr) - return ifindex + return self.interface def delnetif(self, ifindex: int) -> None: """ diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index 26e78367..0cbdb8ae 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -54,12 +54,11 @@ class TestNodes: node = session.add_node(CoreNode) switch = session.add_node(SwitchNode) interface_data = InterfaceData() - index = node.newnetif(switch, interface_data) - interface = node.netif(index) + interface = node.newnetif(switch, interface_data) mac = "aa:aa:aa:ff:ff:ff" # when - node.sethwaddr(index, mac) + node.sethwaddr(interface.netindex, mac) # then assert interface.hwaddr == mac @@ -69,25 +68,23 @@ class TestNodes: node = session.add_node(CoreNode) switch = session.add_node(SwitchNode) interface_data = InterfaceData() - index = node.newnetif(switch, interface_data) - node.netif(index) + interface = node.newnetif(switch, interface_data) mac = "aa:aa:aa:ff:ff:fff" # when with pytest.raises(CoreError): - node.sethwaddr(index, mac) + node.sethwaddr(interface.netindex, mac) def test_node_addaddr(self, session: Session): # given node = session.add_node(CoreNode) switch = session.add_node(SwitchNode) interface_data = InterfaceData() - index = node.newnetif(switch, interface_data) - interface = node.netif(index) + interface = node.newnetif(switch, interface_data) addr = "192.168.0.1/24" # when - node.addaddr(index, addr) + node.addaddr(interface.netindex, addr) # then assert interface.addrlist[0] == addr @@ -97,13 +94,12 @@ class TestNodes: node = session.add_node(CoreNode) switch = session.add_node(SwitchNode) interface_data = InterfaceData() - index = node.newnetif(switch, interface_data) - node.netif(index) + interface = node.newnetif(switch, interface_data) addr = "256.168.0.1/24" # when with pytest.raises(CoreError): - node.addaddr(index, addr) + node.addaddr(interface.netindex, addr) @pytest.mark.parametrize("net_type", NET_TYPES) def test_net(self, session, net_type): From e325bcfc5549be738fae934790a87020958c6484 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 11 Jun 2020 14:41:05 -0700 Subject: [PATCH 0334/1131] bumped version for release --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 90b731a9..ae2d0c8d 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, 6.4.0) +AC_INIT(core, 6.5.0) # autoconf and automake initialization AC_CONFIG_SRCDIR([netns/version.h.in]) From f2409d0604892d9727d08b6205d6e91386bf6088 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 11 Jun 2020 15:40:25 -0700 Subject: [PATCH 0335/1131] updated changelog for 6.5.0 --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4557a3a..96f7b30a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,40 @@ +## 2020-06-11 CORE 6.5.0 +* Breaking Changes + * CoreNode.newnetif - both parameters are required and now takes an InterfaceData object as its second parameter + * CoreNetworkBase.linkconfig - now takes a LinkOptions parameter instead of a subset of some of the options (ie bandwidth, delay, etc) + * \#453 - Session.add_node and Session.get_node now requires the node class you expect to create/retrieve + * \#458 - rj45 cleanup to only inherit from one class +* Enhancements + * fixed issues with handling bad commands for TLV execute messages + * removed unused boot.sh from CoreNode types + * added linkconfig to CoreNetworkBase and cleaned up function signature + * emane position hook now saves geo position to node + * emane pathloss support + * core.emulator.emudata leveraged dataclass and type hinting + * \#459 - updated transport type usage to an enum + * \#460 - updated network policy type usage to an enum +* Python GUI Enhancements + * fixed throughput events do not work for joined sessions + * fixed exiting app with a toolbar picker showing + * fixed issue with creating interfaces and reusing subnets after deletion + * fixed issue with moving text shapes + * fixed scaling with custom node selected + * fixed toolbar state switching issues + * enable/disable toolbar when running stop/start + * marker config integrated into toolbar + * improved color picker layout + * shapes can now be moved while drawing shapes + * added observers to toolbar in run mode +* gRPC API + * node events will now have geo positional data + * node geo data is now returned in get_session and get_node calls + * \#451 - added wlan link api to allow direct linking/unlinking of wireless links between nodes + * \#462 - added streaming call for sending node position/geo changes + * \#463 - added streaming call for emane pathloss events +* Bugfixes + * \#454 - fixed issue creating docker nodes, but containers are now required to have networking tools + * \#466 - fixed issue in python gui when xml file is loading nodes with no ip4 addresses + ## 2020-05-11 CORE 6.4.0 * Enhancements * updates to core-route-monitor, allow specific session, configurable settings, and properly From c64094ac1c219c5dc9b0d56f07279c702b3115cf Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 11 Jun 2020 19:01:38 -0700 Subject: [PATCH 0336/1131] daemon: updated session.delete_link to have the interface ids default to none, since only one may need to be provided, updated link tests to account for more cases --- daemon/core/api/tlv/corehandlers.py | 1 - daemon/core/emulator/session.py | 4 +- daemon/tests/test_links.py | 136 +++++++++++++++++++++++++--- 3 files changed, 126 insertions(+), 15 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 5531e5af..3adaed63 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -788,7 +788,6 @@ class CoreHandler(socketserver.BaseRequestHandler): link_options.network_id = message.get_tlv(LinkTlvs.NETWORK_ID.value) link_options.key = message.get_tlv(LinkTlvs.KEY.value) link_options.opaque = message.get_tlv(LinkTlvs.OPAQUE.value) - if message.flags & MessageFlags.ADD.value: self.session.add_link( node_one_id, node_two_id, interface_one, interface_two, link_options diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 54486bfb..854d5cc8 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -319,8 +319,8 @@ class Session: self, node_one_id: int, node_two_id: int, - interface_one_id: int, - interface_two_id: int, + interface_one_id: int = None, + interface_two_id: int = None, link_type: LinkTypes = LinkTypes.WIRED, ) -> None: """ diff --git a/daemon/tests/test_links.py b/daemon/tests/test_links.py index 9736537e..71942e4b 100644 --- a/daemon/tests/test_links.py +++ b/daemon/tests/test_links.py @@ -25,7 +25,7 @@ def create_ptp_network( class TestLinks: - def test_ptp(self, session: Session, ip_prefixes: IpPrefixes): + def test_add_ptp(self, session: Session, ip_prefixes: IpPrefixes): # given node_one = session.add_node(CoreNode) node_two = session.add_node(CoreNode) @@ -39,20 +39,20 @@ class TestLinks: assert node_one.netif(interface_one.id) assert node_two.netif(interface_two.id) - def test_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): + def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given node_one = session.add_node(CoreNode) node_two = session.add_node(SwitchNode) interface_one = ip_prefixes.create_interface(node_one) # when - session.add_link(node_one.id, node_two.id, interface_one) + session.add_link(node_one.id, node_two.id, interface_one=interface_one) # then assert node_two.all_link_data() assert node_one.netif(interface_one.id) - def test_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): + def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given node_one = session.add_node(SwitchNode) node_two = session.add_node(CoreNode) @@ -76,7 +76,7 @@ class TestLinks: # then assert node_one.all_link_data() - def test_link_update(self, session: Session, ip_prefixes: IpPrefixes): + def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given delay = 50 bandwidth = 5000000 @@ -95,12 +95,9 @@ class TestLinks: assert interface_one.getparam("jitter") != jitter # when - link_options = LinkOptions() - link_options.delay = delay - link_options.bandwidth = bandwidth - link_options.per = per - link_options.dup = dup - link_options.jitter = jitter + link_options = LinkOptions( + delay=delay, bandwidth=bandwidth, per=per, dup=dup, jitter=jitter + ) session.update_link( node_one.id, node_two.id, @@ -115,7 +112,94 @@ class TestLinks: assert interface_one.getparam("duplicate") == dup assert interface_one.getparam("jitter") == jitter - def test_link_delete(self, session: Session, ip_prefixes: IpPrefixes): + def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): + # given + delay = 50 + bandwidth = 5000000 + per = 25 + dup = 25 + jitter = 10 + node_one = session.add_node(SwitchNode) + node_two = session.add_node(CoreNode) + interface_two_data = ip_prefixes.create_interface(node_two) + session.add_link(node_one.id, node_two.id, interface_two=interface_two_data) + interface_two = node_two.netif(interface_two_data.id) + assert interface_two.getparam("delay") != delay + assert interface_two.getparam("bw") != bandwidth + assert interface_two.getparam("loss") != per + assert interface_two.getparam("duplicate") != dup + assert interface_two.getparam("jitter") != jitter + + # when + link_options = LinkOptions( + delay=delay, bandwidth=bandwidth, per=per, dup=dup, jitter=jitter + ) + session.update_link( + node_one.id, + node_two.id, + interface_two_id=interface_two_data.id, + options=link_options, + ) + + # then + assert interface_two.getparam("delay") == delay + assert interface_two.getparam("bw") == bandwidth + assert interface_two.getparam("loss") == per + assert interface_two.getparam("duplicate") == dup + assert interface_two.getparam("jitter") == jitter + + def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes): + # given + delay = 50 + bandwidth = 5000000 + per = 25 + dup = 25 + jitter = 10 + node_one = session.add_node(CoreNode) + node_two = session.add_node(CoreNode) + interface_one_data = ip_prefixes.create_interface(node_one) + interface_two_data = ip_prefixes.create_interface(node_two) + session.add_link( + node_one.id, node_two.id, interface_one_data, interface_two_data + ) + interface_one = node_one.netif(interface_one_data.id) + interface_two = node_two.netif(interface_two_data.id) + assert interface_one.getparam("delay") != delay + assert interface_one.getparam("bw") != bandwidth + assert interface_one.getparam("loss") != per + assert interface_one.getparam("duplicate") != dup + assert interface_one.getparam("jitter") != jitter + assert interface_two.getparam("delay") != delay + assert interface_two.getparam("bw") != bandwidth + assert interface_two.getparam("loss") != per + assert interface_two.getparam("duplicate") != dup + assert interface_two.getparam("jitter") != jitter + + # when + link_options = LinkOptions( + delay=delay, bandwidth=bandwidth, per=per, dup=dup, jitter=jitter + ) + session.update_link( + node_one.id, + node_two.id, + interface_one_data.id, + interface_two_data.id, + link_options, + ) + + # then + assert interface_one.getparam("delay") == delay + assert interface_one.getparam("bw") == bandwidth + assert interface_one.getparam("loss") == per + assert interface_one.getparam("duplicate") == dup + assert interface_one.getparam("jitter") == jitter + assert interface_two.getparam("delay") == delay + assert interface_two.getparam("bw") == bandwidth + assert interface_two.getparam("loss") == per + assert interface_two.getparam("duplicate") == dup + assert interface_two.getparam("jitter") == jitter + + def test_delete_ptp(self, session: Session, ip_prefixes: IpPrefixes): # given node_one = session.add_node(CoreNode) node_two = session.add_node(CoreNode) @@ -133,3 +217,31 @@ class TestLinks: # then assert not node_one.netif(interface_one.id) assert not node_two.netif(interface_two.id) + + def test_delete_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): + # given + node_one = session.add_node(CoreNode) + node_two = session.add_node(SwitchNode) + interface_one = ip_prefixes.create_interface(node_one) + session.add_link(node_one.id, node_two.id, interface_one) + assert node_one.netif(interface_one.id) + + # when + session.delete_link(node_one.id, node_two.id, interface_one_id=interface_one.id) + + # then + assert not node_one.netif(interface_one.id) + + def test_delete_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): + # given + node_one = session.add_node(SwitchNode) + node_two = session.add_node(CoreNode) + interface_two = ip_prefixes.create_interface(node_two) + session.add_link(node_one.id, node_two.id, interface_two=interface_two) + assert node_two.netif(interface_two.id) + + # when + session.delete_link(node_one.id, node_two.id, interface_two_id=interface_two.id) + + # then + assert not node_two.netif(interface_two.id) From 00cda5c55067db0f995817bcd0d4e04f1fe2eae8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 11 Jun 2020 19:08:50 -0700 Subject: [PATCH 0337/1131] fixed test_link name --- daemon/tests/test_links.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/tests/test_links.py b/daemon/tests/test_links.py index 71942e4b..9f693da1 100644 --- a/daemon/tests/test_links.py +++ b/daemon/tests/test_links.py @@ -65,7 +65,7 @@ class TestLinks: assert node_one.all_link_data() assert node_two.netif(interface_two.id) - def test_net_to_net(self, session): + def test_add_net_to_net(self, session): # given node_one = session.add_node(SwitchNode) node_two = session.add_node(SwitchNode) From e72e332babe9aa8502b903bac2f6a8bd0662f2c9 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 11 Jun 2020 19:12:51 -0700 Subject: [PATCH 0338/1131] daemon: removed need to use getaddr for CoreInterface.othernet as it now has a default of None --- daemon/core/nodes/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 498a9beb..4b8d513b 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -1040,7 +1040,7 @@ class CoreNetworkBase(NodeBase): :return: interface the provided network is linked to """ for netif in self.netifs(): - if getattr(netif, "othernet", None) == net: + if netif.othernet == net: return netif return None From cfaa9397ada5df0a5cde683376e0bf3de4bfe807 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 12 Jun 2020 08:34:02 -0700 Subject: [PATCH 0339/1131] daemon: added class variable type hinting to core.api.grpc --- daemon/core/api/grpc/client.py | 12 ++++++------ daemon/core/api/grpc/events.py | 6 +++--- daemon/core/api/grpc/grpcutils.py | 7 ++++--- daemon/core/api/grpc/server.py | 8 ++++---- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 0361a69b..1bc88069 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -5,7 +5,7 @@ gRpc client for interfacing with CORE. import logging import threading from contextlib import contextmanager -from typing import Any, Callable, Dict, Generator, Iterable, List +from typing import Any, Callable, Dict, Generator, Iterable, List, Optional import grpc @@ -108,7 +108,7 @@ class InterfaceHelper: :param ip6_prefix: ip6 prefix to use for generation :raises ValueError: when both ip4 and ip6 prefixes have not been provided """ - self.prefixes = IpPrefixes(ip4_prefix, ip6_prefix) + self.prefixes: IpPrefixes = IpPrefixes(ip4_prefix, ip6_prefix) def create_interface( self, node_id: int, interface_id: int, name: str = None, mac: str = None @@ -177,10 +177,10 @@ class CoreGrpcClient: :param address: grpc server address to connect to """ - self.address = address - self.stub = None - self.channel = None - self.proxy = proxy + self.address: str = address + self.stub: Optional[core_pb2_grpc.CoreApiStub] = None + self.channel: Optional[grpc.Channel] = None + self.proxy: bool = proxy def start_session( self, diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index 837860e3..82cf1eac 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -140,9 +140,9 @@ class EventStreamer: :param session: session to process events for :param event_types: types of events to process """ - self.session = session - self.event_types = event_types - self.queue = Queue() + self.session: Session = session + self.event_types: Iterable[core_pb2.EventType] = event_types + self.queue: Queue = Queue() self.add_handlers() def add_handlers(self) -> None: diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 5c6f3a80..73d19a2a 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -1,6 +1,6 @@ import logging import time -from typing import Any, Dict, List, Tuple, Type +from typing import Any, Dict, List, Tuple, Type, Union import grpc import netaddr @@ -190,7 +190,8 @@ def convert_value(value: Any) -> str: def get_config_options( - config: Dict[str, str], configurable_options: Type[ConfigurableOptions] + config: Dict[str, str], + configurable_options: Union[ConfigurableOptions, Type[ConfigurableOptions]], ) -> Dict[str, common_pb2.ConfigOption]: """ Retrieve configuration options in a form that is used by the grpc server. @@ -418,7 +419,7 @@ def service_configuration(session: Session, config: ServiceConfig) -> None: service.shutdown = tuple(config.shutdown) -def get_service_configuration(service: Type[CoreService]) -> NodeServiceData: +def get_service_configuration(service: CoreService) -> NodeServiceData: """ Convenience for converting a service to service data proto. diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 7d7f7c80..adddff14 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -6,7 +6,7 @@ import tempfile import threading import time from concurrent import futures -from typing import Iterable, Type +from typing import Iterable, Optional, Type import grpc from grpc import ServicerContext @@ -131,9 +131,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): def __init__(self, coreemu: CoreEmu) -> None: super().__init__() - self.coreemu = coreemu - self.running = True - self.server = None + self.coreemu: CoreEmu = coreemu + self.running: bool = True + self.server: Optional[grpc.Server] = None atexit.register(self._exit_handler) def _exit_handler(self) -> None: From ef3cf5697d0080b45a5bf5894c5e87f2b105c0ea Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 12 Jun 2020 08:54:06 -0700 Subject: [PATCH 0340/1131] daemon: added class variable type hinting for core.xml --- daemon/core/config.py | 7 ++++--- daemon/core/xml/corexml.py | 28 +++++++++++----------------- daemon/core/xml/corexmldeployment.py | 6 +++--- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/daemon/core/config.py b/daemon/core/config.py index 1f5bc3c0..d4ba6164 100644 --- a/daemon/core/config.py +++ b/daemon/core/config.py @@ -4,7 +4,7 @@ Common support for configurable CORE objects. import logging from collections import OrderedDict -from typing import TYPE_CHECKING, Dict, List, Tuple, Type, Union +from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Type, Union from core.emane.nodes import EmaneNet from core.emulator.enumerations import ConfigDataTypes @@ -136,7 +136,8 @@ class ConfigurableManager: """ Clears all configurations or configuration for a specific node. - :param node_id: node id to clear configurations for, default is None and clears all configurations + :param node_id: node id to clear configurations for, default is None and clears + all configurations :return: nothing """ if not node_id: @@ -222,7 +223,7 @@ class ConfigurableManager: result = node_configs.get(config_type) return result - def get_all_configs(self, node_id: int = _default_node) -> List[Dict[str, str]]: + def get_all_configs(self, node_id: int = _default_node) -> Dict[str, Any]: """ Retrieve all current configuration types for a node. diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index cb25e717..973eb77f 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -124,9 +124,9 @@ def add_configuration(parent: etree.Element, name: str, value: str) -> None: class NodeElement: def __init__(self, session: "Session", node: NodeBase, element_name: str) -> None: - self.session = session - self.node = node - self.element = etree.Element(element_name) + self.session: "Session" = session + self.node: NodeBase = node + self.element: etree.Element = etree.Element(element_name) add_attribute(self.element, "id", node.id) add_attribute(self.element, "name", node.name) add_attribute(self.element, "icon", node.icon) @@ -151,8 +151,8 @@ class NodeElement: class ServiceElement: def __init__(self, service: Type[CoreService]) -> None: - self.service = service - self.element = etree.Element("service") + self.service: Type[CoreService] = service + self.element: etree.Element = etree.Element("service") add_attribute(self.element, "name", service.name) self.add_directories() self.add_startup() @@ -268,10 +268,10 @@ class NetworkElement(NodeElement): class CoreXmlWriter: def __init__(self, session: "Session") -> None: - self.session = session - self.scenario = etree.Element("scenario") - self.networks = None - self.devices = None + self.session: "Session" = session + self.scenario: etree.Element = etree.Element("scenario") + self.networks: etree.SubElement = etree.SubElement(self.scenario, "networks") + self.devices: etree.SubElement = etree.SubElement(self.scenario, "devices") self.write_session() def write_session(self) -> None: @@ -362,13 +362,11 @@ class CoreXmlWriter: def write_emane_configs(self) -> None: emane_global_configuration = create_emane_config(self.session) self.scenario.append(emane_global_configuration) - emane_configurations = etree.Element("emane_configurations") for node_id in self.session.emane.nodes(): all_configs = self.session.emane.get_all_configs(node_id) if not all_configs: continue - for model_name in all_configs: config = all_configs[model_name] logging.debug( @@ -453,9 +451,6 @@ class CoreXmlWriter: self.scenario.append(node_types) def write_nodes(self) -> List[LinkData]: - self.networks = etree.SubElement(self.scenario, "networks") - self.devices = etree.SubElement(self.scenario, "devices") - links = [] for node_id in self.session.nodes: node = self.session.nodes[node_id] @@ -472,7 +467,6 @@ class CoreXmlWriter: # add known links links.extend(node.all_link_data()) - return links def write_network(self, node: NodeBase) -> None: @@ -597,8 +591,8 @@ class CoreXmlWriter: class CoreXmlReader: def __init__(self, session: "Session") -> None: - self.session = session - self.scenario = None + self.session: "Session" = session + self.scenario: Optional[etree.ElementTree] = None def read(self, file_name: str) -> None: xml_tree = etree.parse(file_name) diff --git a/daemon/core/xml/corexmldeployment.py b/daemon/core/xml/corexmldeployment.py index 5f340b69..04915bf1 100644 --- a/daemon/core/xml/corexmldeployment.py +++ b/daemon/core/xml/corexmldeployment.py @@ -101,9 +101,9 @@ def get_ipv4_addresses(hostname: str) -> List[Tuple[str, str]]: class CoreXmlDeployment: def __init__(self, session: "Session", scenario: etree.Element) -> None: - self.session = session - self.scenario = scenario - self.root = etree.SubElement( + self.session: "Session" = session + self.scenario: etree.Element = scenario + self.root: etree.SubElement = etree.SubElement( scenario, "container", id="TestBed", name="TestBed" ) self.add_deployment() From 6201875b782c266335f23518a4cfd8cfc30f5b42 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 12 Jun 2020 09:52:01 -0700 Subject: [PATCH 0341/1131] daemon: added class variable type hinting to core.emane --- daemon/core/emane/bypass.py | 13 ++++--- daemon/core/emane/commeffect.py | 16 ++++---- daemon/core/emane/emanemanager.py | 44 +++++++++++---------- daemon/core/emane/emanemanifest.py | 1 + daemon/core/emane/emanemodel.py | 26 +++++++------ daemon/core/emane/ieee80211abg.py | 6 +-- daemon/core/emane/linkmonitor.py | 61 ++++++++++++++++-------------- daemon/core/emane/nodes.py | 26 +++++++------ daemon/core/emane/rfpipe.py | 6 +-- daemon/core/emane/tdma.py | 13 ++++--- daemon/core/nodes/network.py | 4 +- 11 files changed, 116 insertions(+), 100 deletions(-) diff --git a/daemon/core/emane/bypass.py b/daemon/core/emane/bypass.py index 83f3b6e8..8aabc3f9 100644 --- a/daemon/core/emane/bypass.py +++ b/daemon/core/emane/bypass.py @@ -1,6 +1,7 @@ """ EMANE Bypass model for CORE """ +from typing import List, Set from core.config import Configuration from core.emane import emanemodel @@ -8,14 +9,14 @@ from core.emulator.enumerations import ConfigDataTypes class EmaneBypassModel(emanemodel.EmaneModel): - name = "emane_bypass" + name: str = "emane_bypass" # values to ignore, when writing xml files - config_ignore = {"none"} + config_ignore: Set[str] = {"none"} # mac definitions - mac_library = "bypassmaclayer" - mac_config = [ + mac_library: str = "bypassmaclayer" + mac_config: List[Configuration] = [ Configuration( _id="none", _type=ConfigDataTypes.BOOL, @@ -25,8 +26,8 @@ class EmaneBypassModel(emanemodel.EmaneModel): ] # phy definitions - phy_library = "bypassphylayer" - phy_config = [] + phy_library: str = "bypassphylayer" + phy_config: List[Configuration] = [] @classmethod def load(cls, emane_prefix: str) -> None: diff --git a/daemon/core/emane/commeffect.py b/daemon/core/emane/commeffect.py index b7060e96..71acb199 100644 --- a/daemon/core/emane/commeffect.py +++ b/daemon/core/emane/commeffect.py @@ -22,6 +22,7 @@ except ImportError: try: from emanesh.events.commeffectevent import CommEffectEvent except ImportError: + CommEffectEvent = None logging.debug("compatible emane python bindings not installed") @@ -38,16 +39,15 @@ def convert_none(x: float) -> int: class EmaneCommEffectModel(emanemodel.EmaneModel): - name = "emane_commeffect" - - shim_library = "commeffectshim" - shim_xml = "commeffectshim.xml" - shim_defaults = {} - config_shim = [] + name: str = "emane_commeffect" + shim_library: str = "commeffectshim" + shim_xml: str = "commeffectshim.xml" + shim_defaults: Dict[str, str] = {} + config_shim: List[Configuration] = [] # comm effect does not need the default phy and external configurations - phy_config = [] - external_config = [] + phy_config: List[Configuration] = [] + external_config: List[Configuration] = [] @classmethod def load(cls, emane_prefix: str) -> None: diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 12b477f0..146d186f 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -70,11 +70,13 @@ class EmaneManager(ModelManager): controlling the EMANE daemons. """ - name = "emane" - config_type = RegisterTlvs.EMULATION_SERVER - SUCCESS, NOT_NEEDED, NOT_READY = (0, 1, 2) - EVENTCFGVAR = "LIBEMANEEVENTSERVICECONFIG" - DEFAULT_LOG_LEVEL = 3 + name: str = "emane" + config_type: RegisterTlvs = RegisterTlvs.EMULATION_SERVER + SUCCESS: int = 0 + NOT_NEEDED: int = 1 + NOT_READY: int = 2 + EVENTCFGVAR: str = "LIBEMANEEVENTSERVICECONFIG" + DEFAULT_LOG_LEVEL: int = 3 def __init__(self, session: "Session") -> None: """ @@ -84,29 +86,29 @@ class EmaneManager(ModelManager): :return: nothing """ super().__init__() - self.session = session - self._emane_nets = {} - self._emane_node_lock = threading.Lock() + self.session: "Session" = session + self._emane_nets: Dict[int, EmaneNet] = {} + self._emane_node_lock: threading.Lock = threading.Lock() # port numbers are allocated from these counters - self.platformport = self.session.options.get_config_int( + self.platformport: int = self.session.options.get_config_int( "emane_platform_port", 8100 ) - self.transformport = self.session.options.get_config_int( + self.transformport: int = self.session.options.get_config_int( "emane_transform_port", 8200 ) - self.doeventloop = False - self.eventmonthread = None + self.doeventloop: bool = False + self.eventmonthread: Optional[threading.Thread] = None # model for global EMANE configuration options - self.emane_config = EmaneGlobalModel(session) + self.emane_config: EmaneGlobalModel = EmaneGlobalModel(session) self.set_configs(self.emane_config.default_values()) # link monitor - self.link_monitor = EmaneLinkMonitor(self) + self.link_monitor: EmaneLinkMonitor = EmaneLinkMonitor(self) - self.service = None - self.eventchannel = None - self.event_device = None + self.service: Optional[EventService] = None + self.eventchannel: Optional[Tuple[str, int, str]] = None + self.event_device: Optional[str] = None self.emane_check() def getifcconfig( @@ -890,12 +892,12 @@ class EmaneGlobalModel: Global EMANE configuration options. """ - name = "emane" - bitmap = None + name: str = "emane" + bitmap: Optional[str] = None def __init__(self, session: "Session") -> None: - self.session = session - self.core_config = [ + self.session: "Session" = session + self.core_config: List[Configuration] = [ Configuration( _id="platform_id_start", _type=ConfigDataTypes.INT32, diff --git a/daemon/core/emane/emanemanifest.py b/daemon/core/emane/emanemanifest.py index 914b4f83..41dc7beb 100644 --- a/daemon/core/emane/emanemanifest.py +++ b/daemon/core/emane/emanemanifest.py @@ -11,6 +11,7 @@ except ImportError: try: from emanesh import manifest except ImportError: + manifest = None logging.debug("compatible emane python bindings not installed") diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index 7b5ff417..78d5ec5e 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -3,7 +3,7 @@ Defines Emane Models used within CORE. """ import logging import os -from typing import Dict, List +from typing import Dict, List, Optional, Set from core.config import ConfigGroup, Configuration from core.emane import emanemanifest @@ -25,19 +25,23 @@ class EmaneModel(WirelessModel): """ # default mac configuration settings - mac_library = None - mac_xml = None - mac_defaults = {} - mac_config = [] + mac_library: Optional[str] = None + mac_xml: Optional[str] = None + mac_defaults: Dict[str, str] = {} + mac_config: List[Configuration] = [] # default phy configuration settings, using the universal model - phy_library = None - phy_xml = "emanephy.xml" - phy_defaults = {"subid": "1", "propagationmodel": "2ray", "noisemode": "none"} - phy_config = [] + phy_library: Optional[str] = None + phy_xml: str = "emanephy.xml" + phy_defaults: Dict[str, str] = { + "subid": "1", + "propagationmodel": "2ray", + "noisemode": "none", + } + phy_config: List[Configuration] = [] # support for external configurations - external_config = [ + external_config: List[Configuration] = [ Configuration("external", ConfigDataTypes.BOOL, default="0"), Configuration( "platformendpoint", ConfigDataTypes.STRING, default="127.0.0.1:40001" @@ -47,7 +51,7 @@ class EmaneModel(WirelessModel): ), ] - config_ignore = set() + config_ignore: Set[str] = set() @classmethod def load(cls, emane_prefix: str) -> None: diff --git a/daemon/core/emane/ieee80211abg.py b/daemon/core/emane/ieee80211abg.py index ecfd3694..0d58ec9e 100644 --- a/daemon/core/emane/ieee80211abg.py +++ b/daemon/core/emane/ieee80211abg.py @@ -8,11 +8,11 @@ from core.emane import emanemodel class EmaneIeee80211abgModel(emanemodel.EmaneModel): # model name - name = "emane_ieee80211abg" + name: str = "emane_ieee80211abg" # mac configuration - mac_library = "ieee80211abgmaclayer" - mac_xml = "ieee80211abgmaclayer.xml" + mac_library: str = "ieee80211abgmaclayer" + mac_xml: str = "ieee80211abgmaclayer.xml" @classmethod def load(cls, emane_prefix: str) -> None: diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py index 861c108c..b9fd9a2a 100644 --- a/daemon/core/emane/linkmonitor.py +++ b/daemon/core/emane/linkmonitor.py @@ -2,7 +2,7 @@ import logging import sched import threading import time -from typing import TYPE_CHECKING, Dict, List, Tuple +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple import netaddr from lxml import etree @@ -17,28 +17,29 @@ except ImportError: try: from emanesh import shell except ImportError: + shell = None logging.debug("compatible emane python bindings not installed") if TYPE_CHECKING: from core.emane.emanemanager import EmaneManager -DEFAULT_PORT = 47_000 -MAC_COMPONENT_INDEX = 1 -EMANE_RFPIPE = "rfpipemaclayer" -EMANE_80211 = "ieee80211abgmaclayer" -EMANE_TDMA = "tdmaeventschedulerradiomodel" -SINR_TABLE = "NeighborStatusTable" -NEM_SELF = 65535 +DEFAULT_PORT: int = 47_000 +MAC_COMPONENT_INDEX: int = 1 +EMANE_RFPIPE: str = "rfpipemaclayer" +EMANE_80211: str = "ieee80211abgmaclayer" +EMANE_TDMA: str = "tdmaeventschedulerradiomodel" +SINR_TABLE: str = "NeighborStatusTable" +NEM_SELF: int = 65535 class LossTable: def __init__(self, losses: Dict[float, float]) -> None: - self.losses = losses - self.sinrs = sorted(self.losses.keys()) - self.loss_lookup = {} + self.losses: Dict[float, float] = losses + self.sinrs: List[float] = sorted(self.losses.keys()) + self.loss_lookup: Dict[int, float] = {} for index, value in enumerate(self.sinrs): self.loss_lookup[index] = self.losses[value] - self.mac_id = None + self.mac_id: Optional[str] = None def get_loss(self, sinr: float) -> float: index = self._get_index(sinr) @@ -54,11 +55,11 @@ class LossTable: class EmaneLink: def __init__(self, from_nem: int, to_nem: int, sinr: float) -> None: - self.from_nem = from_nem - self.to_nem = to_nem - self.sinr = sinr - self.last_seen = None - self.updated = False + self.from_nem: int = from_nem + self.to_nem: int = to_nem + self.sinr: float = sinr + self.last_seen: Optional[float] = None + self.updated: bool = False self.touch() def update(self, sinr: float) -> None: @@ -78,9 +79,11 @@ class EmaneLink: class EmaneClient: def __init__(self, address: str) -> None: - self.address = address - self.client = shell.ControlPortClient(self.address, DEFAULT_PORT) - self.nems = {} + self.address: str = address + self.client: shell.ControlPortClient = shell.ControlPortClient( + self.address, DEFAULT_PORT + ) + self.nems: Dict[int, LossTable] = {} self.setup() def setup(self) -> None: @@ -174,15 +177,15 @@ class EmaneClient: class EmaneLinkMonitor: def __init__(self, emane_manager: "EmaneManager") -> None: - self.emane_manager = emane_manager - self.clients = [] - self.links = {} - self.complete_links = set() - self.loss_threshold = None - self.link_interval = None - self.link_timeout = None - self.scheduler = None - self.running = False + self.emane_manager: "EmaneManager" = emane_manager + self.clients: List[EmaneClient] = [] + self.links: Dict[Tuple[int, int], EmaneLink] = {} + self.complete_links: Set[Tuple[int, int]] = set() + self.loss_threshold: Optional[int] = None + self.link_interval: Optional[int] = None + self.link_timeout: Optional[int] = None + self.scheduler: Optional[sched.scheduler] = None + self.running: bool = False def start(self) -> None: self.loss_threshold = int(self.emane_manager.get_config("loss_threshold")) diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index f4de8f47..e88cb194 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -16,13 +16,16 @@ from core.emulator.enumerations import ( RegisterTlvs, TransportType, ) +from core.errors import CoreError from core.nodes.base import CoreNetworkBase from core.nodes.interface import CoreInterface if TYPE_CHECKING: + from core.emane.emanemodel import EmaneModel from core.emulator.session import Session - from core.location.mobility import WirelessModel + from core.location.mobility import WirelessModel, WayPointMobility + OptionalEmaneModel = Optional[EmaneModel] WirelessModelType = Type[WirelessModel] try: @@ -31,6 +34,7 @@ except ImportError: try: from emanesh.events import LocationEvent except ImportError: + LocationEvent = None logging.debug("compatible emane python bindings not installed") @@ -41,10 +45,10 @@ class EmaneNet(CoreNetworkBase): Emane controller object that exists in a session. """ - apitype = NodeTypes.EMANE - linktype = LinkTypes.WIRED - type = "wlan" - is_emane = True + apitype: NodeTypes = NodeTypes.EMANE + linktype: LinkTypes = LinkTypes.WIRED + type: str = "wlan" + is_emane: bool = True def __init__( self, @@ -55,10 +59,10 @@ class EmaneNet(CoreNetworkBase): server: DistributedServer = None, ) -> None: super().__init__(session, _id, name, start, server) - self.conf = "" - self.nemidmap = {} - self.model = None - self.mobility = None + self.conf: str = "" + self.nemidmap: Dict[CoreInterface, int] = {} + self.model: "OptionalEmaneModel" = None + self.mobility: Optional[WayPointMobility] = None def linkconfig( self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None @@ -84,11 +88,11 @@ class EmaneNet(CoreNetworkBase): def updatemodel(self, config: Dict[str, str]) -> None: if not self.model: - raise ValueError("no model set to update for node(%s)", self.id) + raise CoreError(f"no model set to update for node({self.name})") logging.info( "node(%s) updating model(%s): %s", self.id, self.model.name, config ) - self.model.set_configs(config, node_id=self.id) + self.model.update_config(config) def setmodel(self, model: "WirelessModelType", config: Dict[str, str]) -> None: """ diff --git a/daemon/core/emane/rfpipe.py b/daemon/core/emane/rfpipe.py index 23790b3c..068ef800 100644 --- a/daemon/core/emane/rfpipe.py +++ b/daemon/core/emane/rfpipe.py @@ -8,11 +8,11 @@ from core.emane import emanemodel class EmaneRfPipeModel(emanemodel.EmaneModel): # model name - name = "emane_rfpipe" + name: str = "emane_rfpipe" # mac configuration - mac_library = "rfpipemaclayer" - mac_xml = "rfpipemaclayer.xml" + mac_library: str = "rfpipemaclayer" + mac_xml: str = "rfpipemaclayer.xml" @classmethod def load(cls, emane_prefix: str) -> None: diff --git a/daemon/core/emane/tdma.py b/daemon/core/emane/tdma.py index 17f5328f..ee80f3d7 100644 --- a/daemon/core/emane/tdma.py +++ b/daemon/core/emane/tdma.py @@ -4,6 +4,7 @@ tdma.py: EMANE TDMA model bindings for CORE import logging import os +from typing import Set from core import constants, utils from core.config import Configuration @@ -13,18 +14,18 @@ from core.emulator.enumerations import ConfigDataTypes class EmaneTdmaModel(emanemodel.EmaneModel): # model name - name = "emane_tdma" + name: str = "emane_tdma" # mac configuration - mac_library = "tdmaeventschedulerradiomodel" - mac_xml = "tdmaeventschedulerradiomodel.xml" + mac_library: str = "tdmaeventschedulerradiomodel" + mac_xml: str = "tdmaeventschedulerradiomodel.xml" # add custom schedule options and ignore it when writing emane xml - schedule_name = "schedule" - default_schedule = os.path.join( + schedule_name: str = "schedule" + default_schedule: str = os.path.join( constants.CORE_DATA_DIR, "examples", "tdma", "schedule.xml" ) - config_ignore = {schedule_name} + config_ignore: Set[str] = {schedule_name} @classmethod def load(cls, emane_prefix: str) -> None: diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 6d6ad589..235b43f2 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -1090,12 +1090,12 @@ class WlanNode(CoreNetwork): def update_mobility(self, config: Dict[str, str]) -> None: if not self.mobility: - raise ValueError(f"no mobility set to update for node({self.id})") + raise CoreError(f"no mobility set to update for node({self.name})") self.mobility.update_config(config) def updatemodel(self, config: Dict[str, str]) -> None: if not self.model: - raise ValueError(f"no model set to update for node({self.id})") + raise CoreError(f"no model set to update for node({self.name})") logging.debug( "node(%s) updating model(%s): %s", self.id, self.model.name, config ) From b28ef76d65a483c09bc19f28bec8e1c6635568d8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 12 Jun 2020 10:05:49 -0700 Subject: [PATCH 0342/1131] daemon: added class variable type hinting to core.config --- daemon/core/config.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/daemon/core/config.py b/daemon/core/config.py index d4ba6164..618e1273 100644 --- a/daemon/core/config.py +++ b/daemon/core/config.py @@ -4,7 +4,7 @@ Common support for configurable CORE objects. import logging from collections import OrderedDict -from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Type, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union from core.emane.nodes import EmaneNet from core.emulator.enumerations import ConfigDataTypes @@ -29,9 +29,9 @@ class ConfigGroup: :param start: configurations start index for this group :param stop: configurations stop index for this group """ - self.name = name - self.start = start - self.stop = stop + self.name: str = name + self.start: int = start + self.stop: int = stop class Configuration: @@ -56,18 +56,21 @@ class Configuration: :param default: default value for configuration :param options: list options if this is a configuration with a combobox """ - self.id = _id - self.type = _type - self.default = default + self.id: str = _id + self.type: ConfigDataTypes = _type + self.default: str = default if not options: options = [] - self.options = options + self.options: List[str] = options if not label: label = _id - self.label = label + self.label: str = label def __str__(self): - return f"{self.__class__.__name__}(id={self.id}, type={self.type}, default={self.default}, options={self.options})" + return ( + f"{self.__class__.__name__}(id={self.id}, type={self.type}, " + f"default={self.default}, options={self.options})" + ) class ConfigurableOptions: @@ -75,9 +78,9 @@ class ConfigurableOptions: Provides a base for defining configuration options within CORE. """ - name = None - bitmap = None - options = [] + name: Optional[str] = None + bitmap: Optional[str] = None + options: List[Configuration] = [] @classmethod def configurations(cls) -> List[Configuration]: @@ -115,8 +118,8 @@ class ConfigurableManager: nodes. """ - _default_node = -1 - _default_type = _default_node + _default_node: int = -1 + _default_type: int = _default_node def __init__(self) -> None: """ @@ -243,8 +246,8 @@ class ModelManager(ConfigurableManager): Creates a ModelManager object. """ super().__init__() - self.models = {} - self.node_models = {} + self.models: Dict[str, Any] = {} + self.node_models: Dict[int, str] = {} def set_model_config( self, node_id: int, model_name: str, config: Dict[str, str] = None From 76305f72577b273050818148c3303c9c9102232c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 12 Jun 2020 12:49:53 -0700 Subject: [PATCH 0343/1131] converted usages of per to loss --- daemon/core/api/grpc/grpcutils.py | 4 ++-- daemon/core/api/grpc/server.py | 27 +++++++++++++------------- daemon/core/api/tlv/coreapi.py | 2 +- daemon/core/api/tlv/corehandlers.py | 10 +++++----- daemon/core/api/tlv/enumerations.py | 2 +- daemon/core/emane/commeffect.py | 2 +- daemon/core/emulator/data.py | 2 +- daemon/core/emulator/emudata.py | 2 +- daemon/core/gui/dialogs/linkconfig.py | 8 ++++---- daemon/core/location/mobility.py | 2 +- daemon/core/nodes/base.py | 4 ++-- daemon/core/nodes/network.py | 6 +++--- daemon/core/xml/corexml.py | 4 ++-- daemon/proto/core/api/grpc/core.proto | 2 +- daemon/tests/test_links.py | 28 +++++++++++++-------------- daemon/tests/test_xml.py | 16 +++++++-------- 16 files changed, 61 insertions(+), 60 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 73d19a2a..4acecad9 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -94,7 +94,7 @@ def add_link_data( if options_data: options.delay = options_data.delay options.bandwidth = options_data.bandwidth - options.per = options_data.per + options.loss = options_data.loss options.dup = options_data.dup options.jitter = options_data.jitter options.mer = options_data.mer @@ -343,7 +343,7 @@ def convert_link(link_data: LinkData) -> core_pb2.Link: key=link_data.key, mburst=link_data.mburst, mer=link_data.mer, - per=link_data.per, + loss=link_data.loss, bandwidth=link_data.bandwidth, burst=link_data.burst, delay=link_data.delay, diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index adddff14..9ea4e555 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -885,20 +885,21 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): interface_one_id = request.interface_one_id interface_two_id = request.interface_two_id options_data = request.options - link_options = LinkOptions() - link_options.delay = options_data.delay - link_options.bandwidth = options_data.bandwidth - link_options.per = options_data.per - link_options.dup = options_data.dup - link_options.jitter = options_data.jitter - link_options.mer = options_data.mer - link_options.burst = options_data.burst - link_options.mburst = options_data.mburst - link_options.unidirectional = options_data.unidirectional - link_options.key = options_data.key - link_options.opaque = options_data.opaque + options = LinkOptions( + delay=options_data.delay, + bandwidth=options_data.bandwidth, + loss=options_data.loss, + dup=options_data.dup, + jitter=options_data.jitter, + mer=options_data.mer, + burst=options_data.burst, + mburst=options_data.mburst, + unidirectional=options_data.unidirectional, + key=options_data.key, + opaque=options_data.opaque, + ) session.update_link( - node_one_id, node_two_id, interface_one_id, interface_two_id, link_options + node_one_id, node_two_id, interface_one_id, interface_two_id, options ) return core_pb2.EditLinkResponse(result=True) diff --git a/daemon/core/api/tlv/coreapi.py b/daemon/core/api/tlv/coreapi.py index df60e374..088a7631 100644 --- a/daemon/core/api/tlv/coreapi.py +++ b/daemon/core/api/tlv/coreapi.py @@ -495,7 +495,7 @@ class CoreLinkTlv(CoreTlv): LinkTlvs.N2_NUMBER.value: CoreTlvDataUint32, LinkTlvs.DELAY.value: CoreTlvDataUint64, LinkTlvs.BANDWIDTH.value: CoreTlvDataUint64, - LinkTlvs.PER.value: CoreTlvDataString, + LinkTlvs.LOSS.value: CoreTlvDataString, LinkTlvs.DUP.value: CoreTlvDataString, LinkTlvs.JITTER.value: CoreTlvDataUint64, LinkTlvs.MER.value: CoreTlvDataUint16, diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 3adaed63..d02c274d 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -334,9 +334,9 @@ class CoreHandler(socketserver.BaseRequestHandler): :return: nothing """ logging.debug("handling broadcast link: %s", link_data) - per = "" - if link_data.per is not None: - per = str(link_data.per) + loss = "" + if link_data.loss is not None: + loss = str(link_data.loss) dup = "" if link_data.dup is not None: dup = str(link_data.dup) @@ -348,7 +348,7 @@ class CoreHandler(socketserver.BaseRequestHandler): (LinkTlvs.N2_NUMBER, link_data.node2_id), (LinkTlvs.DELAY, link_data.delay), (LinkTlvs.BANDWIDTH, link_data.bandwidth), - (LinkTlvs.PER, per), + (LinkTlvs.LOSS, loss), (LinkTlvs.DUP, dup), (LinkTlvs.JITTER, link_data.jitter), (LinkTlvs.MER, link_data.mer), @@ -776,7 +776,7 @@ class CoreHandler(socketserver.BaseRequestHandler): link_options.delay = message.get_tlv(LinkTlvs.DELAY.value) link_options.bandwidth = message.get_tlv(LinkTlvs.BANDWIDTH.value) link_options.session = message.get_tlv(LinkTlvs.SESSION.value) - link_options.per = message.get_tlv(LinkTlvs.PER.value) + link_options.loss = message.get_tlv(LinkTlvs.LOSS.value) link_options.dup = message.get_tlv(LinkTlvs.DUP.value) link_options.jitter = message.get_tlv(LinkTlvs.JITTER.value) link_options.mer = message.get_tlv(LinkTlvs.MER.value) diff --git a/daemon/core/api/tlv/enumerations.py b/daemon/core/api/tlv/enumerations.py index ed06bbe7..0efb7c99 100644 --- a/daemon/core/api/tlv/enumerations.py +++ b/daemon/core/api/tlv/enumerations.py @@ -59,7 +59,7 @@ class LinkTlvs(Enum): N2_NUMBER = 0x02 DELAY = 0x03 BANDWIDTH = 0x04 - PER = 0x05 + LOSS = 0x05 DUP = 0x06 JITTER = 0x07 MER = 0x08 diff --git a/daemon/core/emane/commeffect.py b/daemon/core/emane/commeffect.py index 71acb199..21252b6f 100644 --- a/daemon/core/emane/commeffect.py +++ b/daemon/core/emane/commeffect.py @@ -141,7 +141,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): nemid, latency=convert_none(options.delay), jitter=convert_none(options.jitter), - loss=convert_none(options.per), + loss=convert_none(options.loss), duplicate=convert_none(options.dup), unicast=int(convert_none(options.bandwidth)), broadcast=int(convert_none(options.bandwidth)), diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index d3283974..819716e3 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -101,7 +101,7 @@ class LinkData: node2_id: int = None delay: float = None bandwidth: float = None - per: float = None + loss: float = None dup: float = None jitter: float = None mer: float = None diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index b6dbd57c..992b9cd2 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -68,7 +68,7 @@ class LinkOptions: session: int = None delay: int = None bandwidth: int = None - per: float = None + loss: float = None dup: int = None jitter: int = None mer: int = None diff --git a/daemon/core/gui/dialogs/linkconfig.py b/daemon/core/gui/dialogs/linkconfig.py index 92361ed4..c553bb94 100644 --- a/daemon/core/gui/dialogs/linkconfig.py +++ b/daemon/core/gui/dialogs/linkconfig.py @@ -223,7 +223,7 @@ class LinkConfigurationDialog(Dialog): duplicate = get_int(self.duplicate) loss = get_float(self.loss) options = core_pb2.LinkOptions( - bandwidth=bandwidth, jitter=jitter, delay=delay, dup=duplicate, per=loss + bandwidth=bandwidth, jitter=jitter, delay=delay, dup=duplicate, loss=loss ) link.options.CopyFrom(options) @@ -252,7 +252,7 @@ class LinkConfigurationDialog(Dialog): jitter=down_jitter, delay=down_delay, dup=down_duplicate, - per=down_loss, + loss=down_loss, unidirectional=True, ) self.edge.asymmetric_link = core_pb2.Link( @@ -317,12 +317,12 @@ class LinkConfigurationDialog(Dialog): self.bandwidth.set(str(link.options.bandwidth)) self.jitter.set(str(link.options.jitter)) self.duplicate.set(str(link.options.dup)) - self.loss.set(str(link.options.per)) + self.loss.set(str(link.options.loss)) self.delay.set(str(link.options.delay)) if not self.is_symmetric: asym_link = self.edge.asymmetric_link self.down_bandwidth.set(str(asym_link.options.bandwidth)) self.down_jitter.set(str(asym_link.options.jitter)) self.down_duplicate.set(str(asym_link.options.dup)) - self.down_loss.set(str(asym_link.options.per)) + self.down_loss.set(str(asym_link.options.loss)) self.down_delay.set(str(asym_link.options.delay)) diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index e9efa16b..43996ba3 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -338,7 +338,7 @@ class BasicRangeModel(WirelessModel): options = LinkOptions( bandwidth=self.bw, delay=self.delay, - per=self.loss, + loss=self.loss, jitter=self.jitter, ) self.wlan.linkconfig(netif, options) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 4b8d513b..2b3c7751 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -1134,7 +1134,7 @@ class CoreNetworkBase(NodeBase): bandwidth=netif.getparam("bw"), dup=netif.getparam("duplicate"), jitter=netif.getparam("jitter"), - per=netif.getparam("loss"), + loss=netif.getparam("loss"), ) all_links.append(link_data) @@ -1153,7 +1153,7 @@ class CoreNetworkBase(NodeBase): bandwidth=netif.getparam("bw"), dup=netif.getparam("duplicate"), jitter=netif.getparam("jitter"), - per=netif.getparam("loss"), + loss=netif.getparam("loss"), ) netif.swapparams("_params_up") diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 235b43f2..8ac1939e 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -482,7 +482,7 @@ class CoreNetwork(CoreNetworkBase): netem = "netem" delay = options.delay changed = max(changed, netif.setparam("delay", delay)) - loss = options.per + loss = options.loss if loss is not None: loss = float(loss) changed = max(changed, netif.setparam("loss", loss)) @@ -939,7 +939,7 @@ class PtpNet(CoreNetwork): unidirectional=unidirectional, delay=if1.getparam("delay"), bandwidth=if1.getparam("bw"), - per=if1.getparam("loss"), + loss=if1.getparam("loss"), dup=if1.getparam("duplicate"), jitter=if1.getparam("jitter"), interface1_id=if1.node.getifindex(if1), @@ -970,7 +970,7 @@ class PtpNet(CoreNetwork): node2_id=if1.node.id, delay=if2.getparam("delay"), bandwidth=if2.getparam("bw"), - per=if2.getparam("loss"), + loss=if2.getparam("loss"), dup=if2.getparam("duplicate"), jitter=if2.getparam("jitter"), unidirectional=1, diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 973eb77f..820f1cea 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -569,7 +569,7 @@ class CoreXmlWriter: options = etree.Element("options") add_attribute(options, "delay", link_data.delay) add_attribute(options, "bandwidth", link_data.bandwidth) - add_attribute(options, "per", link_data.per) + add_attribute(options, "per", link_data.loss) add_attribute(options, "dup", link_data.dup) add_attribute(options, "jitter", link_data.jitter) add_attribute(options, "mer", link_data.mer) @@ -957,7 +957,7 @@ class CoreXmlReader: link_options.mburst = get_int(options_element, "mburst") link_options.jitter = get_int(options_element, "jitter") link_options.key = get_int(options_element, "key") - link_options.per = get_float(options_element, "per") + link_options.loss = get_float(options_element, "per") link_options.unidirectional = get_int(options_element, "unidirectional") link_options.session = options_element.get("session") link_options.emulation_id = get_int(options_element, "emulation_id") diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index d602f9d3..c9c2d94b 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -719,7 +719,7 @@ message LinkOptions { int32 key = 3; int32 mburst = 4; int32 mer = 5; - float per = 6; + float loss = 6; int64 bandwidth = 7; int32 burst = 8; int64 delay = 9; diff --git a/daemon/tests/test_links.py b/daemon/tests/test_links.py index 9f693da1..9c4fd4f2 100644 --- a/daemon/tests/test_links.py +++ b/daemon/tests/test_links.py @@ -80,7 +80,7 @@ class TestLinks: # given delay = 50 bandwidth = 5000000 - per = 25 + loss = 25 dup = 25 jitter = 10 node_one = session.add_node(CoreNode) @@ -90,13 +90,13 @@ class TestLinks: interface_one = node_one.netif(interface_one_data.id) assert interface_one.getparam("delay") != delay assert interface_one.getparam("bw") != bandwidth - assert interface_one.getparam("loss") != per + assert interface_one.getparam("loss") != loss assert interface_one.getparam("duplicate") != dup assert interface_one.getparam("jitter") != jitter # when link_options = LinkOptions( - delay=delay, bandwidth=bandwidth, per=per, dup=dup, jitter=jitter + delay=delay, bandwidth=bandwidth, loss=loss, dup=dup, jitter=jitter ) session.update_link( node_one.id, @@ -108,7 +108,7 @@ class TestLinks: # then assert interface_one.getparam("delay") == delay assert interface_one.getparam("bw") == bandwidth - assert interface_one.getparam("loss") == per + assert interface_one.getparam("loss") == loss assert interface_one.getparam("duplicate") == dup assert interface_one.getparam("jitter") == jitter @@ -116,7 +116,7 @@ class TestLinks: # given delay = 50 bandwidth = 5000000 - per = 25 + loss = 25 dup = 25 jitter = 10 node_one = session.add_node(SwitchNode) @@ -126,13 +126,13 @@ class TestLinks: interface_two = node_two.netif(interface_two_data.id) assert interface_two.getparam("delay") != delay assert interface_two.getparam("bw") != bandwidth - assert interface_two.getparam("loss") != per + assert interface_two.getparam("loss") != loss assert interface_two.getparam("duplicate") != dup assert interface_two.getparam("jitter") != jitter # when link_options = LinkOptions( - delay=delay, bandwidth=bandwidth, per=per, dup=dup, jitter=jitter + delay=delay, bandwidth=bandwidth, loss=loss, dup=dup, jitter=jitter ) session.update_link( node_one.id, @@ -144,7 +144,7 @@ class TestLinks: # then assert interface_two.getparam("delay") == delay assert interface_two.getparam("bw") == bandwidth - assert interface_two.getparam("loss") == per + assert interface_two.getparam("loss") == loss assert interface_two.getparam("duplicate") == dup assert interface_two.getparam("jitter") == jitter @@ -152,7 +152,7 @@ class TestLinks: # given delay = 50 bandwidth = 5000000 - per = 25 + loss = 25 dup = 25 jitter = 10 node_one = session.add_node(CoreNode) @@ -166,18 +166,18 @@ class TestLinks: interface_two = node_two.netif(interface_two_data.id) assert interface_one.getparam("delay") != delay assert interface_one.getparam("bw") != bandwidth - assert interface_one.getparam("loss") != per + assert interface_one.getparam("loss") != loss assert interface_one.getparam("duplicate") != dup assert interface_one.getparam("jitter") != jitter assert interface_two.getparam("delay") != delay assert interface_two.getparam("bw") != bandwidth - assert interface_two.getparam("loss") != per + assert interface_two.getparam("loss") != loss assert interface_two.getparam("duplicate") != dup assert interface_two.getparam("jitter") != jitter # when link_options = LinkOptions( - delay=delay, bandwidth=bandwidth, per=per, dup=dup, jitter=jitter + delay=delay, bandwidth=bandwidth, loss=loss, dup=dup, jitter=jitter ) session.update_link( node_one.id, @@ -190,12 +190,12 @@ class TestLinks: # then assert interface_one.getparam("delay") == delay assert interface_one.getparam("bw") == bandwidth - assert interface_one.getparam("loss") == per + assert interface_one.getparam("loss") == loss assert interface_one.getparam("duplicate") == dup assert interface_one.getparam("jitter") == jitter assert interface_two.getparam("delay") == delay assert interface_two.getparam("bw") == bandwidth - assert interface_two.getparam("loss") == per + assert interface_two.getparam("loss") == loss assert interface_two.getparam("duplicate") == dup assert interface_two.getparam("jitter") == jitter diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index c40a9ef3..0345daed 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -304,7 +304,7 @@ class TestXml: # create link link_options = LinkOptions() - link_options.per = 10.5 + link_options.loss = 10.5 link_options.bandwidth = 50000 link_options.jitter = 10 link_options.delay = 30 @@ -347,7 +347,7 @@ class TestXml: node = session.nodes[node_id] links += node.all_link_data() link = links[0] - assert link_options.per == link.per + assert link_options.loss == link.loss assert link_options.bandwidth == link.bandwidth assert link_options.jitter == link.jitter assert link_options.delay == link.delay @@ -371,7 +371,7 @@ class TestXml: # create link link_options = LinkOptions() - link_options.per = 10.5 + link_options.loss = 10.5 link_options.bandwidth = 50000 link_options.jitter = 10 link_options.delay = 30 @@ -416,7 +416,7 @@ class TestXml: node = session.nodes[node_id] links += node.all_link_data() link = links[0] - assert link_options.per == link.per + assert link_options.loss == link.loss assert link_options.bandwidth == link.bandwidth assert link_options.jitter == link.jitter assert link_options.delay == link.delay @@ -443,7 +443,7 @@ class TestXml: link_options_one.unidirectional = 1 link_options_one.bandwidth = 5000 link_options_one.delay = 10 - link_options_one.per = 10.5 + link_options_one.loss = 10.5 link_options_one.dup = 5 link_options_one.jitter = 5 session.add_link( @@ -453,7 +453,7 @@ class TestXml: link_options_two.unidirectional = 1 link_options_two.bandwidth = 10000 link_options_two.delay = 20 - link_options_two.per = 10 + link_options_two.loss = 10 link_options_two.dup = 10 link_options_two.jitter = 10 session.update_link( @@ -504,11 +504,11 @@ class TestXml: link_two = links[1] assert link_options_one.bandwidth == link_one.bandwidth assert link_options_one.delay == link_one.delay - assert link_options_one.per == link_one.per + assert link_options_one.loss == link_one.loss assert link_options_one.dup == link_one.dup assert link_options_one.jitter == link_one.jitter assert link_options_two.bandwidth == link_two.bandwidth assert link_options_two.delay == link_two.delay - assert link_options_two.per == link_two.per + assert link_options_two.loss == link_two.loss assert link_options_two.dup == link_two.dup assert link_options_two.jitter == link_two.jitter From 876699e8efeb03daee471b0662d7d3eba8183917 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 12 Jun 2020 16:52:41 -0700 Subject: [PATCH 0344/1131] variable/grpc cleanup to rename everything using spelt out numbers instead of actual numbers --- daemon/core/api/grpc/client.py | 94 +++---- daemon/core/api/grpc/grpcutils.py | 38 +-- daemon/core/api/grpc/server.py | 116 ++++----- daemon/core/api/tlv/corehandlers.py | 54 ++-- daemon/core/emane/linkmonitor.py | 20 +- daemon/core/emulator/distributed.py | 12 +- daemon/core/emulator/session.py | 154 +++++------ daemon/core/gui/coreclient.py | 58 ++--- daemon/core/gui/dialogs/linkconfig.py | 48 ++-- daemon/core/gui/graph/edges.py | 14 +- daemon/core/gui/graph/graph.py | 78 +++--- daemon/core/gui/interface.py | 24 +- daemon/core/plugins/sdt.py | 56 ++-- daemon/core/xml/corexml.py | 56 ++-- daemon/examples/configservices/testing.py | 14 +- daemon/examples/docker/docker2core.py | 10 +- daemon/examples/docker/docker2docker.py | 10 +- daemon/examples/docker/switch.py | 12 +- daemon/examples/grpc/distributed_switch.py | 12 +- daemon/examples/grpc/emane80211.py | 8 +- daemon/examples/grpc/switch.py | 8 +- daemon/examples/grpc/wlan.py | 8 +- daemon/examples/lxd/lxd2core.py | 10 +- daemon/examples/lxd/lxd2lxd.py | 10 +- daemon/examples/lxd/switch.py | 18 +- daemon/examples/python/distributed_emane.py | 12 +- daemon/examples/python/distributed_lxd.py | 10 +- daemon/examples/python/distributed_ptp.py | 10 +- daemon/examples/python/distributed_switch.py | 12 +- daemon/examples/python/emane80211.py | 2 +- daemon/examples/python/switch.py | 2 +- daemon/examples/python/switch_inject.py | 2 +- daemon/examples/python/wlan.py | 2 +- daemon/proto/core/api/grpc/core.proto | 28 +- daemon/proto/core/api/grpc/emane.proto | 16 +- daemon/proto/core/api/grpc/wlan.proto | 4 +- daemon/tests/emane/test_emane.py | 37 +-- daemon/tests/test_conf.py | 16 +- daemon/tests/test_core.py | 64 ++--- daemon/tests/test_grpc.py | 54 ++-- daemon/tests/test_gui.py | 159 ++++++------ daemon/tests/test_links.py | 246 +++++++++--------- daemon/tests/test_services.py | 20 +- daemon/tests/test_xml.py | 258 +++++++++---------- docs/scripting.md | 2 +- 45 files changed, 932 insertions(+), 966 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 1bc88069..3a16d4fd 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -609,30 +609,30 @@ class CoreGrpcClient: def add_link( self, session_id: int, - node_one_id: int, - node_two_id: int, - interface_one: core_pb2.Interface = None, - interface_two: core_pb2.Interface = None, + node1_id: int, + node2_id: int, + interface1: core_pb2.Interface = None, + interface2: core_pb2.Interface = None, options: core_pb2.LinkOptions = None, ) -> core_pb2.AddLinkResponse: """ Add a link between nodes. :param session_id: session id - :param node_one_id: node one id - :param node_two_id: node two id - :param interface_one: node one interface data - :param interface_two: node two interface data + :param node1_id: node one id + :param node2_id: node two id + :param interface1: node one interface data + :param interface2: node two interface data :param options: options for link (jitter, bandwidth, etc) :return: response with result of success or failure :raises grpc.RpcError: when session or one of the nodes don't exist """ link = core_pb2.Link( - node_one_id=node_one_id, - node_two_id=node_two_id, + node1_id=node1_id, + node2_id=node2_id, type=core_pb2.LinkType.WIRED, - interface_one=interface_one, - interface_two=interface_two, + interface1=interface1, + interface2=interface2, options=options, ) request = core_pb2.AddLinkRequest(session_id=session_id, link=link) @@ -641,59 +641,59 @@ class CoreGrpcClient: def edit_link( self, session_id: int, - node_one_id: int, - node_two_id: int, + node1_id: int, + node2_id: int, options: core_pb2.LinkOptions, - interface_one_id: int = None, - interface_two_id: int = None, + interface1_id: int = None, + interface2_id: int = None, ) -> core_pb2.EditLinkResponse: """ Edit a link between nodes. :param session_id: session id - :param node_one_id: node one id - :param node_two_id: node two id + :param node1_id: node one id + :param node2_id: node two id :param options: options for link (jitter, bandwidth, etc) - :param interface_one_id: node one interface id - :param interface_two_id: node two interface id + :param interface1_id: node one interface id + :param interface2_id: node two interface id :return: response with result of success or failure :raises grpc.RpcError: when session or one of the nodes don't exist """ request = core_pb2.EditLinkRequest( session_id=session_id, - node_one_id=node_one_id, - node_two_id=node_two_id, + node1_id=node1_id, + node2_id=node2_id, options=options, - interface_one_id=interface_one_id, - interface_two_id=interface_two_id, + interface1_id=interface1_id, + interface2_id=interface2_id, ) return self.stub.EditLink(request) def delete_link( self, session_id: int, - node_one_id: int, - node_two_id: int, - interface_one_id: int = None, - interface_two_id: int = None, + node1_id: int, + node2_id: int, + interface1_id: int = None, + interface2_id: int = None, ) -> core_pb2.DeleteLinkResponse: """ Delete a link between nodes. :param session_id: session id - :param node_one_id: node one id - :param node_two_id: node two id - :param interface_one_id: node one interface id - :param interface_two_id: node two interface id + :param node1_id: node one id + :param node2_id: node two id + :param interface1_id: node one interface id + :param interface2_id: node two interface id :return: response with result of success or failure :raises grpc.RpcError: when session doesn't exist """ request = core_pb2.DeleteLinkRequest( session_id=session_id, - node_one_id=node_one_id, - node_two_id=node_two_id, - interface_one_id=interface_one_id, - interface_two_id=interface_two_id, + node1_id=node1_id, + node2_id=node2_id, + interface1_id=interface1_id, + interface2_id=interface2_id, ) return self.stub.DeleteLink(request) @@ -1111,20 +1111,20 @@ class CoreGrpcClient: return self.stub.OpenXml(request) def emane_link( - self, session_id: int, nem_one: int, nem_two: int, linked: bool + self, session_id: int, nem1: int, nem2: int, linked: bool ) -> EmaneLinkResponse: """ Helps broadcast wireless link/unlink between EMANE nodes. :param session_id: session to emane link - :param nem_one: first nem for emane link - :param nem_two: second nem for emane link + :param nem1: first nem for emane link + :param nem2: second nem for emane link :param linked: True to link, False to unlink :return: get emane link response :raises grpc.RpcError: when session or nodes related to nems do not exist """ request = EmaneLinkRequest( - session_id=session_id, nem_one=nem_one, nem_two=nem_two, linked=linked + session_id=session_id, nem1=nem1, nem2=nem2, linked=linked ) return self.stub.EmaneLink(request) @@ -1243,24 +1243,24 @@ class CoreGrpcClient: return self.stub.ExecuteScript(request) def wlan_link( - self, session_id: int, wlan: int, node_one: int, node_two: int, linked: bool + self, session_id: int, wlan_id: int, node1_id: int, node2_id: int, linked: bool ) -> WlanLinkResponse: """ Links/unlinks nodes on the same WLAN. :param session_id: session id containing wlan and nodes - :param wlan: wlan nodes must belong to - :param node_one: first node of pair to link/unlink - :param node_two: second node of pair to link/unlin + :param wlan_id: wlan nodes must belong to + :param node1_id: first node of pair to link/unlink + :param node2_id: second node of pair to link/unlin :param linked: True to link, False to unlink :return: wlan link response :raises grpc.RpcError: when session or one of the nodes do not exist """ request = WlanLinkRequest( session_id=session_id, - wlan=wlan, - node_one=node_one, - node_two=node_two, + wlan=wlan_id, + node1_id=node1_id, + node2_id=node2_id, linked=linked, ) return self.stub.WlanLink(request) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 73d19a2a..539face1 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -86,8 +86,8 @@ def add_link_data( :param link_proto: link proto :return: link interfaces and options """ - interface_one = link_interface(link_proto.interface_one) - interface_two = link_interface(link_proto.interface_two) + interface1_data = link_interface(link_proto.interface1) + interface2_data = link_interface(link_proto.interface2) link_type = LinkTypes(link_proto.type) options = LinkOptions(type=link_type) options_data = link_proto.options @@ -103,7 +103,7 @@ def add_link_data( options.unidirectional = options_data.unidirectional options.key = options_data.key options.opaque = options_data.opaque - return interface_one, interface_two, options + return interface1_data, interface2_data, options def create_nodes( @@ -141,10 +141,10 @@ def create_links( """ funcs = [] for link_proto in link_protos: - node_one_id = link_proto.node_one_id - node_two_id = link_proto.node_two_id - interface_one, interface_two, options = add_link_data(link_proto) - args = (node_one_id, node_two_id, interface_one, interface_two, options) + node1_id = link_proto.node1_id + node2_id = link_proto.node2_id + interface1, interface2, options = add_link_data(link_proto) + args = (node1_id, node2_id, interface1, interface2, options) funcs.append((session.add_link, args, {})) start = time.monotonic() results, exceptions = utils.threadpool(funcs) @@ -165,10 +165,10 @@ def edit_links( """ funcs = [] for link_proto in link_protos: - node_one_id = link_proto.node_one_id - node_two_id = link_proto.node_two_id - interface_one, interface_two, options = add_link_data(link_proto) - args = (node_one_id, node_two_id, interface_one.id, interface_two.id, options) + node1_id = link_proto.node1_id + node2_id = link_proto.node2_id + interface1, interface2, options = add_link_data(link_proto) + args = (node1_id, node2_id, interface1.id, interface2.id, options) funcs.append((session.update_link, args, {})) start = time.monotonic() results, exceptions = utils.threadpool(funcs) @@ -315,9 +315,9 @@ def convert_link(link_data: LinkData) -> core_pb2.Link: :param link_data: link to convert :return: core protobuf Link """ - interface_one = None + interface1 = None if link_data.interface1_id is not None: - interface_one = core_pb2.Interface( + interface1 = core_pb2.Interface( id=link_data.interface1_id, name=link_data.interface1_name, mac=convert_value(link_data.interface1_mac), @@ -326,9 +326,9 @@ def convert_link(link_data: LinkData) -> core_pb2.Link: ip6=convert_value(link_data.interface1_ip6), ip6mask=link_data.interface1_ip6_mask, ) - interface_two = None + interface2 = None if link_data.interface2_id is not None: - interface_two = core_pb2.Interface( + interface2 = core_pb2.Interface( id=link_data.interface2_id, name=link_data.interface2_name, mac=convert_value(link_data.interface2_mac), @@ -352,10 +352,10 @@ def convert_link(link_data: LinkData) -> core_pb2.Link: ) return core_pb2.Link( type=link_data.link_type.value, - node_one_id=link_data.node1_id, - node_two_id=link_data.node2_id, - interface_one=interface_one, - interface_two=interface_two, + node1_id=link_data.node1_id, + node2_id=link_data.node2_id, + interface1=interface1, + interface2=interface2, options=options, network_id=link_data.network_id, label=link_data.label, diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index adddff14..a0ddf806 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -845,27 +845,23 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :return: add-link response """ logging.debug("add link: %s", request) - # validate session and nodes session = self.get_session(request.session_id, context) - self.get_node(session, request.link.node_one_id, context, NodeBase) - self.get_node(session, request.link.node_two_id, context, NodeBase) - - node_one_id = request.link.node_one_id - node_two_id = request.link.node_two_id - interface_one, interface_two, options = grpcutils.add_link_data(request.link) - node_one_interface, node_two_interface = session.add_link( - node_one_id, node_two_id, interface_one, interface_two, options=options + node1_id = request.link.node1_id + node2_id = request.link.node2_id + self.get_node(session, node1_id, context, NodeBase) + self.get_node(session, node2_id, context, NodeBase) + interface1, interface2, options = grpcutils.add_link_data(request.link) + node1_interface, node2_interface = session.add_link( + node1_id, node2_id, interface1, interface2, options=options ) - interface_one_proto = None - interface_two_proto = None - if node_one_interface: - interface_one_proto = grpcutils.interface_to_proto(node_one_interface) - if node_two_interface: - interface_two_proto = grpcutils.interface_to_proto(node_two_interface) + interface1_proto = None + interface2_proto = None + if node1_interface: + interface1_proto = grpcutils.interface_to_proto(node1_interface) + if node2_interface: + interface2_proto = grpcutils.interface_to_proto(node2_interface) return core_pb2.AddLinkResponse( - result=True, - interface_one=interface_one_proto, - interface_two=interface_two_proto, + result=True, interface1=interface1_proto, interface2=interface2_proto ) def EditLink( @@ -880,10 +876,10 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("edit link: %s", request) session = self.get_session(request.session_id, context) - node_one_id = request.node_one_id - node_two_id = request.node_two_id - interface_one_id = request.interface_one_id - interface_two_id = request.interface_two_id + node1_id = request.node1_id + node2_id = request.node2_id + interface1_id = request.interface1_id + interface2_id = request.interface2_id options_data = request.options link_options = LinkOptions() link_options.delay = options_data.delay @@ -898,7 +894,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): link_options.key = options_data.key link_options.opaque = options_data.opaque session.update_link( - node_one_id, node_two_id, interface_one_id, interface_two_id, link_options + node1_id, node2_id, interface1_id, interface2_id, link_options ) return core_pb2.EditLinkResponse(result=True) @@ -914,13 +910,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("delete link: %s", request) session = self.get_session(request.session_id, context) - node_one_id = request.node_one_id - node_two_id = request.node_two_id - interface_one_id = request.interface_one_id - interface_two_id = request.interface_two_id - session.delete_link( - node_one_id, node_two_id, interface_one_id, interface_two_id - ) + node1_id = request.node1_id + node2_id = request.node2_id + interface1_id = request.interface1_id + interface2_id = request.interface2_id + session.delete_link(node1_id, node2_id, interface1_id, interface2_id) return core_pb2.DeleteLinkResponse(result=True) def GetHooks( @@ -1519,30 +1513,30 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("emane link: %s", request) session = self.get_session(request.session_id, context) - nem_one = request.nem_one - emane_one, netif = session.emane.nemlookup(nem_one) - if not emane_one or not netif: - context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem_one} not found") - node_one = netif.node + nem1 = request.nem1 + emane1, netif = session.emane.nemlookup(nem1) + if not emane1 or not netif: + context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem1} not found") + node1 = netif.node - nem_two = request.nem_two - emane_two, netif = session.emane.nemlookup(nem_two) - if not emane_two or not netif: - context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem_two} not found") - node_two = netif.node + nem2 = request.nem2 + emane2, netif = session.emane.nemlookup(nem2) + if not emane2 or not netif: + context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem2} not found") + node2 = netif.node - if emane_one.id == emane_two.id: + if emane1.id == emane2.id: if request.linked: flag = MessageFlags.ADD else: flag = MessageFlags.DELETE - color = session.get_link_color(emane_one.id) + color = session.get_link_color(emane1.id) link = LinkData( message_type=flag, link_type=LinkTypes.WIRELESS, - node1_id=node_one.id, - node2_id=node_two.id, - network_id=emane_one.id, + node1_id=node1.id, + node2_id=node2.id, + network_id=emane1.id, color=color, ) session.broadcast_link(link) @@ -1739,21 +1733,23 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): grpc.StatusCode.NOT_FOUND, f"wlan node {request.wlan} does not using BasicRangeModel", ) - n1 = self.get_node(session, request.node_one, context, CoreNode) - n2 = self.get_node(session, request.node_two, context, CoreNode) - n1_netif, n2_netif = None, None - for net, netif1, netif2 in n1.commonnets(n2): + node1 = self.get_node(session, request.node1_id, context, CoreNode) + node2 = self.get_node(session, request.node2_id, context, CoreNode) + node1_interface, node2_interface = None, None + for net, interface1, interface2 in node1.commonnets(node2): if net == wlan: - n1_netif = netif1 - n2_netif = netif2 + node1_interface = interface1 + node2_interface = interface2 break result = False - if n1_netif and n2_netif: + if node1_interface and node2_interface: if request.linked: - wlan.link(n1_netif, n2_netif) + wlan.link(node1_interface, node2_interface) else: - wlan.unlink(n1_netif, n2_netif) - wlan.model.sendlinkmsg(n1_netif, n2_netif, unlink=not request.linked) + wlan.unlink(node1_interface, node2_interface) + wlan.model.sendlinkmsg( + node1_interface, node2_interface, unlink=not request.linked + ) result = True return WlanLinkResponse(result=result) @@ -1764,9 +1760,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): ) -> EmanePathlossesResponse: for request in request_iterator: session = self.get_session(request.session_id, context) - n1 = self.get_node(session, request.node_one, context, CoreNode) - nem1 = grpcutils.get_nem_id(n1, request.interface_one_id, context) - n2 = self.get_node(session, request.node_two, context, CoreNode) - nem2 = grpcutils.get_nem_id(n2, request.interface_two_id, context) - session.emane.publish_pathloss(nem1, nem2, request.rx_one, request.rx_two) + node1 = self.get_node(session, request.node1_id, context, CoreNode) + nem1 = grpcutils.get_nem_id(node1, request.interface1_id, context) + node2 = self.get_node(session, request.node2_id, context, CoreNode) + nem2 = grpcutils.get_nem_id(node2, request.interface2_id, context) + session.emane.publish_pathloss(nem1, nem2, request.rx1, request.rx2) return EmanePathlossesResponse() diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 3adaed63..e7a67b3e 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -745,10 +745,9 @@ class CoreHandler(socketserver.BaseRequestHandler): :param core.api.tlv.coreapi.CoreLinkMessage message: link message to handle :return: link message replies """ - node_one_id = message.get_tlv(LinkTlvs.N1_NUMBER.value) - node_two_id = message.get_tlv(LinkTlvs.N2_NUMBER.value) - - interface_one = InterfaceData( + node1_id = message.get_tlv(LinkTlvs.N1_NUMBER.value) + node2_id = message.get_tlv(LinkTlvs.N2_NUMBER.value) + interface1_data = InterfaceData( id=message.get_tlv(LinkTlvs.INTERFACE1_NUMBER.value), name=message.get_tlv(LinkTlvs.INTERFACE1_NAME.value), mac=message.get_tlv(LinkTlvs.INTERFACE1_MAC.value), @@ -757,7 +756,7 @@ class CoreHandler(socketserver.BaseRequestHandler): ip6=message.get_tlv(LinkTlvs.INTERFACE1_IP6.value), ip6_mask=message.get_tlv(LinkTlvs.INTERFACE1_IP6_MASK.value), ) - interface_two = InterfaceData( + interface2_data = InterfaceData( id=message.get_tlv(LinkTlvs.INTERFACE2_NUMBER.value), name=message.get_tlv(LinkTlvs.INTERFACE2_NAME.value), mac=message.get_tlv(LinkTlvs.INTERFACE2_MAC.value), @@ -766,45 +765,38 @@ class CoreHandler(socketserver.BaseRequestHandler): ip6=message.get_tlv(LinkTlvs.INTERFACE2_IP6.value), ip6_mask=message.get_tlv(LinkTlvs.INTERFACE2_IP6_MASK.value), ) - link_type = LinkTypes.WIRED link_type_value = message.get_tlv(LinkTlvs.TYPE.value) if link_type_value is not None: link_type = LinkTypes(link_type_value) - - link_options = LinkOptions(type=link_type) - link_options.delay = message.get_tlv(LinkTlvs.DELAY.value) - link_options.bandwidth = message.get_tlv(LinkTlvs.BANDWIDTH.value) - link_options.session = message.get_tlv(LinkTlvs.SESSION.value) - link_options.per = message.get_tlv(LinkTlvs.PER.value) - link_options.dup = message.get_tlv(LinkTlvs.DUP.value) - link_options.jitter = message.get_tlv(LinkTlvs.JITTER.value) - link_options.mer = message.get_tlv(LinkTlvs.MER.value) - link_options.burst = message.get_tlv(LinkTlvs.BURST.value) - link_options.mburst = message.get_tlv(LinkTlvs.MBURST.value) - link_options.gui_attributes = message.get_tlv(LinkTlvs.GUI_ATTRIBUTES.value) - link_options.unidirectional = message.get_tlv(LinkTlvs.UNIDIRECTIONAL.value) - link_options.emulation_id = message.get_tlv(LinkTlvs.EMULATION_ID.value) - link_options.network_id = message.get_tlv(LinkTlvs.NETWORK_ID.value) - link_options.key = message.get_tlv(LinkTlvs.KEY.value) - link_options.opaque = message.get_tlv(LinkTlvs.OPAQUE.value) + options = LinkOptions(type=link_type) + options.delay = message.get_tlv(LinkTlvs.DELAY.value) + options.bandwidth = message.get_tlv(LinkTlvs.BANDWIDTH.value) + options.session = message.get_tlv(LinkTlvs.SESSION.value) + options.per = message.get_tlv(LinkTlvs.PER.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.gui_attributes = message.get_tlv(LinkTlvs.GUI_ATTRIBUTES.value) + options.unidirectional = message.get_tlv(LinkTlvs.UNIDIRECTIONAL.value) + options.emulation_id = message.get_tlv(LinkTlvs.EMULATION_ID.value) + options.network_id = message.get_tlv(LinkTlvs.NETWORK_ID.value) + options.key = message.get_tlv(LinkTlvs.KEY.value) + options.opaque = message.get_tlv(LinkTlvs.OPAQUE.value) if message.flags & MessageFlags.ADD.value: self.session.add_link( - node_one_id, node_two_id, interface_one, interface_two, link_options + node1_id, node2_id, interface1_data, interface2_data, options ) elif message.flags & MessageFlags.DELETE.value: self.session.delete_link( - node_one_id, node_two_id, interface_one.id, interface_two.id + node1_id, node2_id, interface1_data.id, interface2_data.id ) else: self.session.update_link( - node_one_id, - node_two_id, - interface_one.id, - interface_two.id, - link_options, + node1_id, node2_id, interface1_data.id, interface2_data.id, options ) - return () def handle_execute_message(self, message): diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py index b9fd9a2a..ca9f4493 100644 --- a/daemon/core/emane/linkmonitor.py +++ b/daemon/core/emane/linkmonitor.py @@ -269,11 +269,11 @@ class EmaneLinkMonitor: self.scheduler.enter(self.link_interval, 0, self.check_links) def get_complete_id(self, link_id: Tuple[int, int]) -> Tuple[int, int]: - value_one, value_two = link_id - if value_one < value_two: - return value_one, value_two + value1, value2 = link_id + if value1 < value2: + return value1, value2 else: - return value_two, value_one + return value2, value1 def is_complete_link(self, link_id: Tuple[int, int]) -> bool: reverse_id = link_id[1], link_id[0] @@ -287,8 +287,8 @@ class EmaneLinkMonitor: return f"{source_link.sinr:.1f} / {dest_link.sinr:.1f}" def send_link(self, message_type: MessageFlags, link_id: Tuple[int, int]) -> None: - nem_one, nem_two = link_id - link = self.emane_manager.get_nem_link(nem_one, nem_two, message_type) + nem1, nem2 = link_id + link = self.emane_manager.get_nem_link(nem1, nem2, message_type) if link: label = self.get_link_label(link_id) link.label = label @@ -298,16 +298,16 @@ class EmaneLinkMonitor: self, message_type: MessageFlags, label: str, - node_one: int, - node_two: int, + node1: int, + node2: int, emane_id: int, ) -> None: color = self.emane_manager.session.get_link_color(emane_id) link_data = LinkData( message_type=message_type, label=label, - node1_id=node_one, - node2_id=node_two, + node1_id=node1, + node2_id=node2, network_id=emane_id, link_type=LinkTypes.WIRELESS, color=color, diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index 3753e1c2..75081447 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -224,18 +224,20 @@ class DistributedController: self.tunnels[key] = tunnel return tunnel - def tunnel_key(self, n1_id: int, n2_id: int) -> int: + def tunnel_key(self, node1_id: int, node2_id: int) -> int: """ Compute a 32-bit key used to uniquely identify a GRE tunnel. The hash(n1num), hash(n2num) values are used, so node numbers may be None or string values (used for e.g. "ctrlnet"). - :param n1_id: node one id - :param n2_id: node two id + :param node1_id: node one id + :param node2_id: node two id :return: tunnel key for the node pair """ - logging.debug("creating tunnel key for: %s, %s", n1_id, n2_id) + logging.debug("creating tunnel key for: %s, %s", node1_id, node2_id) key = ( - (self.session.id << 16) ^ utils.hashkey(n1_id) ^ (utils.hashkey(n2_id) << 8) + (self.session.id << 16) + ^ utils.hashkey(node1_id) + ^ (utils.hashkey(node2_id) << 8) ) return key & 0xFFFFFFFF diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 854d5cc8..0a90b943 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -194,13 +194,13 @@ class Session: return node_type def _link_wireless( - self, node_one: CoreNodeBase, node_two: CoreNodeBase, connect: bool + self, node1: CoreNodeBase, node2: CoreNodeBase, connect: bool ) -> None: """ Objects to deal with when connecting/disconnecting wireless links. - :param node_one: node one for wireless link - :param node_two: node two for wireless link + :param node1: node one for wireless link + :param node2: node two for wireless link :param connect: link interfaces if True, unlink otherwise :return: nothing :raises core.CoreError: when objects to link is less than 2, or no common @@ -208,14 +208,14 @@ class Session: """ logging.info( "handling wireless linking node1(%s) node2(%s): %s", - node_one.name, - node_two.name, + node1.name, + node2.name, connect, ) - common_networks = node_one.commonnets(node_one) + common_networks = node1.commonnets(node1) if not common_networks: raise CoreError("no common network found for wireless link/unlink") - for common_network, interface_one, interface_two in common_networks: + for common_network, interface1, interface2 in common_networks: if not isinstance(common_network, (WlanNode, EmaneNet)): logging.info( "skipping common network that is not wireless/emane: %s", @@ -223,26 +223,26 @@ class Session: ) continue if connect: - common_network.link(interface_one, interface_two) + common_network.link(interface1, interface2) else: - common_network.unlink(interface_one, interface_two) + common_network.unlink(interface1, interface2) def add_link( self, - node_one_id: int, - node_two_id: int, - interface_one: InterfaceData = None, - interface_two: InterfaceData = None, + node1_id: int, + node2_id: int, + interface1_data: InterfaceData = None, + interface2_data: InterfaceData = None, options: LinkOptions = None, ) -> Tuple[CoreInterface, CoreInterface]: """ Add a link between nodes. - :param node_one_id: node one id - :param node_two_id: node two id - :param interface_one: node one interface + :param node1_id: node one id + :param node2_id: node two id + :param interface1_data: node one interface data, defaults to none - :param interface_two: node two interface + :param interface2_data: node two interface data, defaults to none :param options: data for creating link, defaults to no options @@ -250,10 +250,10 @@ class Session: """ if not options: options = LinkOptions() - node1 = self.get_node(node_one_id, NodeBase) - node2 = self.get_node(node_two_id, NodeBase) - node1_interface = None - node2_interface = None + node1 = self.get_node(node1_id, NodeBase) + node2 = self.get_node(node2_id, NodeBase) + interface1 = None + interface2 = None # wireless link if options.type == LinkTypes.WIRELESS: @@ -270,22 +270,22 @@ class Session: logging.info("linking ptp: %s - %s", node1.name, node2.name) start = self.state.should_start() ptp = self.create_node(PtpNet, start=start) - node1_interface = node1.newnetif(ptp, interface_one) - node2_interface = node2.newnetif(ptp, interface_two) - ptp.linkconfig(node1_interface, options) + interface1 = node1.newnetif(ptp, interface1_data) + interface2 = node2.newnetif(ptp, interface2_data) + ptp.linkconfig(interface1, options) if not options.unidirectional: - ptp.linkconfig(node2_interface, options) + ptp.linkconfig(interface2, options) # link node to net elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase): - node1_interface = node1.newnetif(node2, interface_one) + interface1 = node1.newnetif(node2, interface1_data) if not isinstance(node2, (EmaneNet, WlanNode)): - node2.linkconfig(node1_interface, options) + node2.linkconfig(interface1, options) # link net to node elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): - node2_interface = node2.newnetif(node1, interface_two) + interface2 = node2.newnetif(node1, interface2_data) wireless_net = isinstance(node1, (EmaneNet, WlanNode)) if not options.unidirectional and not wireless_net: - node1.linkconfig(node2_interface, options) + node1.linkconfig(interface2, options) # network to network elif isinstance(node1, CoreNetworkBase) and isinstance( node2, CoreNetworkBase @@ -293,12 +293,12 @@ class Session: logging.info( "linking network to network: %s - %s", node1.name, node2.name ) - node1_interface = node1.linknet(node2) - node1.linkconfig(node1_interface, options) + interface1 = node1.linknet(node2) + node1.linkconfig(interface1, options) if not options.unidirectional: - node1_interface.swapparams("_params_up") - node2.linkconfig(node1_interface, options) - node1_interface.swapparams("_params_up") + interface1.swapparams("_params_up") + node2.linkconfig(interface1, options) + interface1.swapparams("_params_up") else: raise CoreError( f"cannot link node1({type(node1)}) node2({type(node2)})" @@ -308,41 +308,41 @@ class Session: key = options.key if isinstance(node1, TunnelNode): logging.info("setting tunnel key for: %s", node1.name) - node1.setkey(key, interface_one) + node1.setkey(key, interface1_data) if isinstance(node2, TunnelNode): logging.info("setting tunnel key for: %s", node2.name) - node2.setkey(key, interface_two) - self.sdt.add_link(node_one_id, node_two_id) - return node1_interface, node2_interface + node2.setkey(key, interface2_data) + self.sdt.add_link(node1_id, node2_id) + return interface1, interface2 def delete_link( self, - node_one_id: int, - node_two_id: int, - interface_one_id: int = None, - interface_two_id: int = None, + node1_id: int, + node2_id: int, + interface1_id: int = None, + interface2_id: int = None, link_type: LinkTypes = LinkTypes.WIRED, ) -> None: """ Delete a link between nodes. - :param node_one_id: node one id - :param node_two_id: node two id - :param interface_one_id: interface id for node one - :param interface_two_id: interface id for node two + :param node1_id: node one id + :param node2_id: node two id + :param interface1_id: interface id for node one + :param interface2_id: interface id for node two :param link_type: link type to delete :return: nothing :raises core.CoreError: when no common network is found for link being deleted """ - node1 = self.get_node(node_one_id, NodeBase) - node2 = self.get_node(node_two_id, NodeBase) + node1 = self.get_node(node1_id, NodeBase) + node2 = self.get_node(node2_id, NodeBase) logging.info( "deleting link(%s) node(%s):interface(%s) node(%s):interface(%s)", link_type.name, node1.name, - interface_one_id, + interface1_id, node2.name, - interface_two_id, + interface2_id, ) # wireless link @@ -357,15 +357,15 @@ class Session: # wired link else: if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): - interface1 = node1.netif(interface_one_id) - interface2 = node2.netif(interface_two_id) + interface1 = node1.netif(interface1_id) + interface2 = node2.netif(interface2_id) if not interface1: raise CoreError( - f"node({node1.name}) missing interface({interface_one_id})" + f"node({node1.name}) missing interface({interface1_id})" ) if not interface2: raise CoreError( - f"node({node2.name}) missing interface({interface_two_id})" + f"node({node2.name}) missing interface({interface2_id})" ) if interface1.net != interface2.net: raise CoreError( @@ -373,30 +373,30 @@ class Session: "not connected to same net" ) ptp = interface1.net - node1.delnetif(interface_one_id) - node2.delnetif(interface_two_id) + node1.delnetif(interface1_id) + node2.delnetif(interface2_id) self.delete_node(ptp.id) elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase): - node1.delnetif(interface_one_id) + node1.delnetif(interface1_id) elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): - node2.delnetif(interface_two_id) - self.sdt.delete_link(node_one_id, node_two_id) + node2.delnetif(interface2_id) + self.sdt.delete_link(node1_id, node2_id) def update_link( self, - node_one_id: int, - node_two_id: int, - interface_one_id: int = None, - interface_two_id: int = None, + node1_id: int, + node2_id: int, + interface1_id: int = None, + interface2_id: int = None, options: LinkOptions = None, ) -> None: """ Update link information between nodes. - :param node_one_id: node one id - :param node_two_id: node two id - :param interface_one_id: interface id for node one - :param interface_two_id: interface id for node two + :param node1_id: node one id + :param node2_id: node two id + :param interface1_id: interface id for node one + :param interface2_id: interface id for node two :param options: data to update link with :return: nothing :raises core.CoreError: when updating a wireless type link, when there is a @@ -404,15 +404,15 @@ class Session: """ if not options: options = LinkOptions() - node1 = self.get_node(node_one_id, NodeBase) - node2 = self.get_node(node_two_id, NodeBase) + node1 = self.get_node(node1_id, NodeBase) + node2 = self.get_node(node2_id, NodeBase) logging.info( "update link(%s) node(%s):interface(%s) node(%s):interface(%s)", options.type.name, node1.name, - interface_one_id, + interface1_id, node2.name, - interface_two_id, + interface2_id, ) # wireless link @@ -420,15 +420,15 @@ class Session: raise CoreError("cannot update wireless link") else: if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): - interface1 = node1.netif(interface_one_id) - interface2 = node2.netif(interface_two_id) + interface1 = node1.netif(interface1_id) + interface2 = node2.netif(interface2_id) if not interface1: raise CoreError( - f"node({node1.name}) missing interface({interface_one_id})" + f"node({node1.name}) missing interface({interface1_id})" ) if not interface2: raise CoreError( - f"node({node2.name}) missing interface({interface_two_id})" + f"node({node2.name}) missing interface({interface2_id})" ) if interface1.net != interface2.net: raise CoreError( @@ -440,10 +440,10 @@ class Session: if not options.unidirectional: ptp.linkconfig(interface2, options, interface1) elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase): - interface = node1.netif(interface_one_id) + interface = node1.netif(interface1_id) node2.linkconfig(interface, options) elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): - interface = node2.netif(interface_two_id) + interface = node2.netif(interface2_id) node1.linkconfig(interface, options) elif isinstance(node1, CoreNetworkBase) and isinstance( node2, CoreNetworkBase diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 2b565e7f..5c1c52a0 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -164,25 +164,19 @@ class CoreClient: def handle_link_event(self, event: core_pb2.LinkEvent): logging.debug("Link event: %s", event) - node_one_id = event.link.node_one_id - node_two_id = event.link.node_two_id - if node_one_id == node_two_id: + node1_id = event.link.node1_id + node2_id = event.link.node2_id + if node1_id == node2_id: logging.warning("ignoring links with loops: %s", event) return - canvas_node_one = self.canvas_nodes[node_one_id] - canvas_node_two = self.canvas_nodes[node_two_id] + canvas_node1 = self.canvas_nodes[node1_id] + canvas_node2 = self.canvas_nodes[node2_id] if event.message_type == core_pb2.MessageType.ADD: - self.app.canvas.add_wireless_edge( - canvas_node_one, canvas_node_two, event.link - ) + self.app.canvas.add_wireless_edge(canvas_node1, canvas_node2, event.link) elif event.message_type == core_pb2.MessageType.DELETE: - self.app.canvas.delete_wireless_edge( - canvas_node_one, canvas_node_two, event.link - ) + self.app.canvas.delete_wireless_edge(canvas_node1, canvas_node2, event.link) elif event.message_type == core_pb2.MessageType.NONE: - self.app.canvas.update_wireless_edge( - canvas_node_one, canvas_node_two, event.link - ) + self.app.canvas.update_wireless_edge(canvas_node1, canvas_node2, event.link) else: logging.warning("unknown link event: %s", event) @@ -472,10 +466,10 @@ class CoreClient: for edge in self.links.values(): link = core_pb2.Link() link.CopyFrom(edge.link) - if link.HasField("interface_one") and not link.interface_one.mac: - link.interface_one.mac = self.interfaces_manager.next_mac() - if link.HasField("interface_two") and not link.interface_two.mac: - link.interface_two.mac = self.interfaces_manager.next_mac() + if link.HasField("interface1") and not link.interface1.mac: + link.interface1.mac = self.interfaces_manager.next_mac() + if link.HasField("interface2") and not link.interface2.mac: + link.interface2.mac = self.interfaces_manager.next_mac() links.append(link) wlan_configs = self.get_wlan_configs_proto() mobility_configs = self.get_mobility_configs_proto() @@ -693,10 +687,10 @@ class CoreClient: for link_proto in link_protos: response = self.client.add_link( self.session_id, - link_proto.node_one_id, - link_proto.node_two_id, - link_proto.interface_one, - link_proto.interface_two, + link_proto.node1_id, + link_proto.node2_id, + link_proto.interface1, + link_proto.interface2, link_proto.options, ) logging.debug("create link: %s", response) @@ -881,20 +875,20 @@ class CoreClient: link = core_pb2.Link( type=core_pb2.LinkType.WIRED, - node_one_id=src_node.id, - node_two_id=dst_node.id, - interface_one=src_interface, - interface_two=dst_interface, + node1_id=src_node.id, + node2_id=dst_node.id, + interface1=src_interface, + interface2=dst_interface, ) # assign after creating link proto, since interfaces are copied if src_interface: - interface_one = link.interface_one - edge.src_interface = interface_one - canvas_src_node.interfaces[interface_one.id] = interface_one + interface1 = link.interface1 + edge.src_interface = interface1 + canvas_src_node.interfaces[interface1.id] = interface1 if dst_interface: - interface_two = link.interface_two - edge.dst_interface = interface_two - canvas_dst_node.interfaces[interface_two.id] = interface_two + interface2 = link.interface2 + edge.dst_interface = interface2 + canvas_dst_node.interfaces[interface2.id] = interface2 edge.set_link(link) self.links[edge.token] = edge logging.info("Add link between %s and %s", src_node.name, dst_node.name) diff --git a/daemon/core/gui/dialogs/linkconfig.py b/daemon/core/gui/dialogs/linkconfig.py index 92361ed4..1c20e2e1 100644 --- a/daemon/core/gui/dialogs/linkconfig.py +++ b/daemon/core/gui/dialogs/linkconfig.py @@ -227,21 +227,21 @@ class LinkConfigurationDialog(Dialog): ) link.options.CopyFrom(options) - interface_one = None - if link.HasField("interface_one"): - interface_one = link.interface_one.id - interface_two = None - if link.HasField("interface_two"): - interface_two = link.interface_two.id + interface1_id = None + if link.HasField("interface1"): + interface1_id = link.interface1.id + interface2_id = None + if link.HasField("interface2"): + interface2_id = link.interface2.id if not self.is_symmetric: link.options.unidirectional = True - asym_interface_one = None - if interface_one: - asym_interface_one = core_pb2.Interface(id=interface_one) - asym_interface_two = None - if interface_two: - asym_interface_two = core_pb2.Interface(id=interface_two) + asym_interface1 = None + if interface1_id: + asym_interface1 = core_pb2.Interface(id=interface1_id) + asym_interface2 = None + if interface2_id: + asym_interface2 = core_pb2.Interface(id=interface2_id) down_bandwidth = get_int(self.down_bandwidth) down_jitter = get_int(self.down_jitter) down_delay = get_int(self.down_delay) @@ -256,10 +256,10 @@ class LinkConfigurationDialog(Dialog): unidirectional=True, ) self.edge.asymmetric_link = core_pb2.Link( - node_one_id=link.node_two_id, - node_two_id=link.node_one_id, - interface_one=asym_interface_one, - interface_two=asym_interface_two, + node1_id=link.node2_id, + node2_id=link.node1_id, + interface1=asym_interface1, + interface2=asym_interface2, options=options, ) else: @@ -270,20 +270,20 @@ class LinkConfigurationDialog(Dialog): session_id = self.app.core.session_id self.app.core.client.edit_link( session_id, - link.node_one_id, - link.node_two_id, + link.node1_id, + link.node2_id, link.options, - interface_one, - interface_two, + interface1_id, + interface2_id, ) if self.edge.asymmetric_link: self.app.core.client.edit_link( session_id, - link.node_two_id, - link.node_one_id, + link.node2_id, + link.node1_id, self.edge.asymmetric_link.options, - interface_one, - interface_two, + interface1_id, + interface2_id, ) self.destroy() diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 00268c88..1d2264eb 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -296,13 +296,13 @@ class CanvasEdge(Edge): return label def create_node_labels(self) -> Tuple[str, str]: - label_one = None - if self.link.HasField("interface_one"): - label_one = self.interface_label(self.link.interface_one) - label_two = None - if self.link.HasField("interface_two"): - label_two = self.interface_label(self.link.interface_two) - return label_one, label_two + label1 = None + if self.link.HasField("interface1"): + label1 = self.interface_label(self.link.interface1) + label2 = None + if self.link.HasField("interface2"): + label2 = self.interface_label(self.link.interface2) + return label1, label2 def draw_labels(self) -> None: src_text, dst_text = self.create_node_labels() diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 3d6fd369..90dcd9f6 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -300,41 +300,39 @@ class CanvasGraph(tk.Canvas): # draw existing links for link in session.links: logging.debug("drawing link: %s", link) - canvas_node_one = self.core.canvas_nodes[link.node_one_id] - node_one = canvas_node_one.core_node - canvas_node_two = self.core.canvas_nodes[link.node_two_id] - node_two = canvas_node_two.core_node - token = create_edge_token(canvas_node_one.id, canvas_node_two.id) + canvas_node1 = self.core.canvas_nodes[link.node1_id] + node1 = canvas_node1.core_node + canvas_node2 = self.core.canvas_nodes[link.node2_id] + node2 = canvas_node2.core_node + token = create_edge_token(canvas_node1.id, canvas_node2.id) if link.type == core_pb2.LinkType.WIRELESS: - self.add_wireless_edge(canvas_node_one, canvas_node_two, link) + self.add_wireless_edge(canvas_node1, canvas_node2, link) else: if token not in self.edges: - src_pos = (node_one.position.x, node_one.position.y) - dst_pos = (node_two.position.x, node_two.position.y) - edge = CanvasEdge(self, canvas_node_one.id, src_pos, dst_pos) + src_pos = (node1.position.x, node1.position.y) + dst_pos = (node2.position.x, node2.position.y) + edge = CanvasEdge(self, canvas_node1.id, src_pos, dst_pos) edge.token = token - edge.dst = canvas_node_two.id + edge.dst = canvas_node2.id edge.set_link(link) edge.check_wireless() - canvas_node_one.edges.add(edge) - canvas_node_two.edges.add(edge) + canvas_node1.edges.add(edge) + canvas_node2.edges.add(edge) self.edges[edge.token] = edge self.core.links[edge.token] = edge - if link.HasField("interface_one"): - interface_one = link.interface_one + if link.HasField("interface1"): + interface1 = link.interface1 + self.core.interface_to_edge[(node1.id, interface1.id)] = token + canvas_node1.interfaces[interface1.id] = interface1 + edge.src_interface = interface1 + if link.HasField("interface2"): + interface2 = link.interface2 self.core.interface_to_edge[ - (node_one.id, interface_one.id) - ] = token - canvas_node_one.interfaces[interface_one.id] = interface_one - edge.src_interface = interface_one - if link.HasField("interface_two"): - interface_two = link.interface_two - self.core.interface_to_edge[ - (node_two.id, interface_two.id) + (node2.id, interface2.id) ] = edge.token - canvas_node_two.interfaces[interface_two.id] = interface_two - edge.dst_interface = interface_two + canvas_node2.interfaces[interface2.id] = interface2 + edge.dst_interface = interface2 elif link.options.unidirectional: edge = self.edges[token] edge.asymmetric_link = link @@ -965,26 +963,26 @@ class CanvasGraph(tk.Canvas): copy_link = copy_edge.link options = edge.link.options copy_link.options.CopyFrom(options) - interface_one = None - if copy_link.HasField("interface_one"): - interface_one = copy_link.interface_one.id - interface_two = None - if copy_link.HasField("interface_two"): - interface_two = copy_link.interface_two.id + interface1_id = None + if copy_link.HasField("interface1"): + interface1_id = copy_link.interface1.id + interface2_id = None + if copy_link.HasField("interface2"): + interface2_id = copy_link.interface2.id if not options.unidirectional: copy_edge.asymmetric_link = None else: - asym_interface_one = None - if interface_one: - asym_interface_one = core_pb2.Interface(id=interface_one) - asym_interface_two = None - if interface_two: - asym_interface_two = core_pb2.Interface(id=interface_two) + asym_interface1 = None + if interface1_id: + asym_interface1 = core_pb2.Interface(id=interface1_id) + asym_interface2 = None + if interface2_id: + asym_interface2 = core_pb2.Interface(id=interface2_id) copy_edge.asymmetric_link = core_pb2.Link( - node_one_id=copy_link.node_two_id, - node_two_id=copy_link.node_one_id, - interface_one=asym_interface_one, - interface_two=asym_interface_two, + node1_id=copy_link.node2_id, + node2_id=copy_link.node1_id, + interface1=asym_interface1, + interface2=asym_interface2, options=edge.asymmetric_link.options, ) self.itemconfig( diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index 1973fe99..34270f56 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -89,21 +89,21 @@ class InterfaceManager: remaining_subnets = set() for edge in self.app.core.links.values(): link = edge.link - if link.HasField("interface_one"): - subnets = self.get_subnets(link.interface_one) + if link.HasField("interface1"): + subnets = self.get_subnets(link.interface1) remaining_subnets.add(subnets) - if link.HasField("interface_two"): - subnets = self.get_subnets(link.interface_two) + if link.HasField("interface2"): + subnets = self.get_subnets(link.interface2) remaining_subnets.add(subnets) # remove all subnets from used subnets when no longer present # or remove used indexes from subnet interfaces = [] for link in links: - if link.HasField("interface_one"): - interfaces.append(link.interface_one) - if link.HasField("interface_two"): - interfaces.append(link.interface_two) + if link.HasField("interface1"): + interfaces.append(link.interface1) + if link.HasField("interface2"): + interfaces.append(link.interface2) for interface in interfaces: subnets = self.get_subnets(interface) if subnets not in remaining_subnets: @@ -117,10 +117,10 @@ class InterfaceManager: def joined(self, links: List["core_pb2.Link"]) -> None: interfaces = [] for link in links: - if link.HasField("interface_one"): - interfaces.append(link.interface_one) - if link.HasField("interface_two"): - interfaces.append(link.interface_two) + if link.HasField("interface1"): + interfaces.append(link.interface1) + if link.HasField("interface2"): + interfaces.append(link.interface2) # add to used subnets and mark used indexes for interface in interfaces: diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 8b4ec39f..062217cb 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -21,8 +21,8 @@ if TYPE_CHECKING: from core.emulator.session import Session -def get_link_id(node_one: int, node_two: int, network_id: int) -> str: - link_id = f"{node_one}-{node_two}" +def get_link_id(node1_id: int, node2_id: int, network_id: int) -> str: + link_id = f"{node1_id}-{node2_id}" if network_id is not None: link_id = f"{link_id}-{network_id}" return link_id @@ -351,27 +351,27 @@ class Sdt: return result def add_link( - self, node_one: int, node_two: int, network_id: int = None, label: str = None + self, node1_id: int, node2_id: int, network_id: int = None, label: str = None ) -> None: """ Handle adding a link in SDT. - :param node_one: node one id - :param node_two: node two id + :param node1_id: node one id + :param node2_id: node two id :param network_id: network link is associated with, None otherwise :param label: label for link :return: nothing """ - logging.debug("sdt add link: %s, %s, %s", node_one, node_two, network_id) + logging.debug("sdt add link: %s, %s, %s", node1_id, node2_id, network_id) if not self.connect(): return - if self.wireless_net_check(node_one) or self.wireless_net_check(node_two): + if self.wireless_net_check(node1_id) or self.wireless_net_check(node2_id): return color = DEFAULT_LINK_COLOR if network_id: color = self.session.get_link_color(network_id) line = f"{color},2" - link_id = get_link_id(node_one, node_two, network_id) + link_id = get_link_id(node1_id, node2_id, network_id) layer = LINK_LAYER if network_id: node = self.session.nodes.get(network_id) @@ -383,47 +383,47 @@ class Sdt: if label: link_label = f'linklabel on,"{label}"' self.cmd( - f"link {node_one},{node_two},{link_id} linkLayer {layer} line {line} " + f"link {node1_id},{node2_id},{link_id} linkLayer {layer} line {line} " f"{link_label}" ) - def delete_link(self, node_one: int, node_two: int, network_id: int = None) -> None: + def delete_link(self, node1_id: int, node2_id: int, network_id: int = None) -> None: """ Handle deleting a link in SDT. - :param node_one: node one id - :param node_two: node two id + :param node1_id: node one id + :param node2_id: node two id :param network_id: network link is associated with, None otherwise :return: nothing """ - logging.debug("sdt delete link: %s, %s, %s", node_one, node_two, network_id) + logging.debug("sdt delete link: %s, %s, %s", node1_id, node2_id, network_id) if not self.connect(): return - if self.wireless_net_check(node_one) or self.wireless_net_check(node_two): + if self.wireless_net_check(node1_id) or self.wireless_net_check(node2_id): return - link_id = get_link_id(node_one, node_two, network_id) - self.cmd(f"delete link,{node_one},{node_two},{link_id}") + link_id = get_link_id(node1_id, node2_id, network_id) + self.cmd(f"delete link,{node1_id},{node2_id},{link_id}") def edit_link( - self, node_one: int, node_two: int, network_id: int, label: str + self, node1_id: int, node2_id: int, network_id: int, label: str ) -> None: """ Handle editing a link in SDT. - :param node_one: node one id - :param node_two: node two id + :param node1_id: node one id + :param node2_id: node two id :param network_id: network link is associated with, None otherwise :param label: label to update :return: nothing """ - logging.debug("sdt edit link: %s, %s, %s", node_one, node_two, network_id) + logging.debug("sdt edit link: %s, %s, %s", node1_id, node2_id, network_id) if not self.connect(): return - if self.wireless_net_check(node_one) or self.wireless_net_check(node_two): + if self.wireless_net_check(node1_id) or self.wireless_net_check(node2_id): return - link_id = get_link_id(node_one, node_two, network_id) + link_id = get_link_id(node1_id, node2_id, network_id) link_label = f'linklabel on,"{label}"' - self.cmd(f"link {node_one},{node_two},{link_id} {link_label}") + self.cmd(f"link {node1_id},{node2_id},{link_id} {link_label}") def handle_link_update(self, link_data: LinkData) -> None: """ @@ -432,13 +432,13 @@ class Sdt: :param link_data: link data to handle :return: nothing """ - node_one = link_data.node1_id - node_two = link_data.node2_id + node1_id = link_data.node1_id + node2_id = link_data.node2_id network_id = link_data.network_id label = link_data.label if link_data.message_type == MessageFlags.ADD: - self.add_link(node_one, node_two, network_id, label) + self.add_link(node1_id, node2_id, network_id, label) elif link_data.message_type == MessageFlags.DELETE: - self.delete_link(node_one, node_two, network_id) + self.delete_link(node1_id, node2_id, network_id) elif link_data.message_type == MessageFlags.NONE and label: - self.edit_link(node_one, node_two, network_id, label) + self.edit_link(node1_id, node2_id, network_id, label) diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 973eb77f..afc1d826 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -534,7 +534,7 @@ class CoreXmlWriter: # check for interface one if link_data.interface1_id is not None: - interface_one = self.create_interface_element( + interface1 = self.create_interface_element( "interface_one", link_data.node1_id, link_data.interface1_id, @@ -544,11 +544,11 @@ class CoreXmlWriter: link_data.interface1_ip6, link_data.interface1_ip6_mask, ) - link_element.append(interface_one) + link_element.append(interface1) # check for interface two if link_data.interface2_id is not None: - interface_two = self.create_interface_element( + interface2 = self.create_interface_element( "interface_two", link_data.node2_id, link_data.interface2_id, @@ -558,14 +558,14 @@ class CoreXmlWriter: link_data.interface2_ip6, link_data.interface2_ip6_mask, ) - link_element.append(interface_two) + link_element.append(interface2) # check for options, don't write for emane/wlan links - node_one = self.session.get_node(link_data.node1_id, NodeBase) - node_two = self.session.get_node(link_data.node2_id, NodeBase) - is_node_one_wireless = isinstance(node_one, (WlanNode, EmaneNet)) - is_node_two_wireless = isinstance(node_two, (WlanNode, EmaneNet)) - if not any([is_node_one_wireless, is_node_two_wireless]): + node1 = self.session.get_node(link_data.node1_id, NodeBase) + node2 = self.session.get_node(link_data.node2_id, NodeBase) + is_node1_wireless = isinstance(node1, (WlanNode, EmaneNet)) + is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet)) + if not any([is_node1_wireless, is_node2_wireless]): options = etree.Element("options") add_attribute(options, "delay", link_data.delay) add_attribute(options, "bandwidth", link_data.bandwidth) @@ -932,19 +932,19 @@ class CoreXmlReader: node_sets = set() for link_element in link_elements.iterchildren(): - node_one = get_int(link_element, "node_one") - node_two = get_int(link_element, "node_two") - node_set = frozenset((node_one, node_two)) + node1_id = get_int(link_element, "node_one") + node2_id = get_int(link_element, "node_two") + node_set = frozenset((node1_id, node2_id)) - interface_one_element = link_element.find("interface_one") - interface_one = None - if interface_one_element is not None: - interface_one = create_interface_data(interface_one_element) + interface1_element = link_element.find("interface_one") + interface1_data = None + if interface1_element is not None: + interface1_data = create_interface_data(interface1_element) - interface_two_element = link_element.find("interface_two") - interface_two = None - if interface_two_element is not None: - interface_two = create_interface_data(interface_two_element) + interface2_element = link_element.find("interface_two") + interface2_data = None + if interface2_element is not None: + interface2_data = create_interface_data(interface2_element) options_element = link_element.find("options") link_options = LinkOptions() @@ -966,18 +966,18 @@ class CoreXmlReader: link_options.gui_attributes = options_element.get("gui_attributes") if link_options.unidirectional == 1 and node_set in node_sets: - logging.info( - "updating link node_one(%s) node_two(%s)", node_one, node_two - ) + logging.info("updating link node1(%s) node2(%s)", node1_id, node2_id) self.session.update_link( - node_one, node_two, interface_one.id, interface_two.id, link_options + node1_id, + node2_id, + interface1_data.id, + interface2_data.id, + link_options, ) else: - logging.info( - "adding link node_one(%s) node_two(%s)", node_one, node_two - ) + logging.info("adding link node1(%s) node2(%s)", node1_id, node2_id) self.session.add_link( - node_one, node_two, interface_one, interface_two, link_options + node1_id, node2_id, interface1_data, interface2_data, link_options ) node_sets.add(node_set) diff --git a/daemon/examples/configservices/testing.py b/daemon/examples/configservices/testing.py index bc67ff46..948ec739 100644 --- a/daemon/examples/configservices/testing.py +++ b/daemon/examples/configservices/testing.py @@ -11,7 +11,7 @@ if __name__ == "__main__": # setup basic network prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") - options = NodeOptions(model="nothing") + options = NodeOptions(model=None) coreemu = CoreEmu() session = coreemu.create_session() session.set_state(EventTypes.CONFIGURATION_STATE) @@ -19,14 +19,14 @@ if __name__ == "__main__": # node one options.config_services = ["DefaultRoute", "IPForward"] - node_one = session.add_node(CoreNode, options=options) - interface = prefixes.create_interface(node_one) - session.add_link(node_one.id, switch.id, interface_one=interface) + node1 = session.add_node(CoreNode, options=options) + interface = prefixes.create_interface(node1) + session.add_link(node1.id, switch.id, interface1_data=interface) # node two - node_two = session.add_node(CoreNode, options=options) - interface = prefixes.create_interface(node_two) - session.add_link(node_two.id, switch.id, interface_one=interface) + node2 = session.add_node(CoreNode, options=options) + interface = prefixes.create_interface(node2) + session.add_link(node2.id, switch.id, interface1_data=interface) # start session and run services session.instantiate() diff --git a/daemon/examples/docker/docker2core.py b/daemon/examples/docker/docker2core.py index 1211a16f..8151a590 100644 --- a/daemon/examples/docker/docker2core.py +++ b/daemon/examples/docker/docker2core.py @@ -17,15 +17,15 @@ if __name__ == "__main__": options = NodeOptions(model=None, image="ubuntu") # create node one - node_one = session.add_node(DockerNode, options=options) - interface_one = prefixes.create_interface(node_one) + node1 = session.add_node(DockerNode, options=options) + interface1_data = prefixes.create_interface(node1) # create node two - node_two = session.add_node(CoreNode) - interface_two = prefixes.create_interface(node_two) + node2 = session.add_node(CoreNode) + interface2_data = prefixes.create_interface(node2) # add link - session.add_link(node_one.id, node_two.id, interface_one, interface_two) + session.add_link(node1.id, node2.id, interface1_data, interface2_data) # instantiate session.instantiate() diff --git a/daemon/examples/docker/docker2docker.py b/daemon/examples/docker/docker2docker.py index 9e1ae11f..a7a70534 100644 --- a/daemon/examples/docker/docker2docker.py +++ b/daemon/examples/docker/docker2docker.py @@ -18,15 +18,15 @@ if __name__ == "__main__": options = NodeOptions(model=None, image="ubuntu") # create node one - node_one = session.add_node(DockerNode, options=options) - interface_one = prefixes.create_interface(node_one) + node1 = session.add_node(DockerNode, options=options) + interface1_data = prefixes.create_interface(node1) # create node two - node_two = session.add_node(DockerNode, options=options) - interface_two = prefixes.create_interface(node_two) + node2 = session.add_node(DockerNode, options=options) + interface2_data = prefixes.create_interface(node2) # add link - session.add_link(node_one.id, node_two.id, interface_one, interface_two) + session.add_link(node1.id, node2.id, interface1_data, interface2_data) # instantiate session.instantiate() diff --git a/daemon/examples/docker/switch.py b/daemon/examples/docker/switch.py index 74d58fe0..ef057945 100644 --- a/daemon/examples/docker/switch.py +++ b/daemon/examples/docker/switch.py @@ -22,20 +22,20 @@ if __name__ == "__main__": switch = session.add_node(SwitchNode) # node one - node_one = session.add_node(DockerNode, options=options) - interface_one = prefixes.create_interface(node_one) + node1 = session.add_node(DockerNode, options=options) + interface1_data = prefixes.create_interface(node1) # node two - node_two = session.add_node(DockerNode, options=options) - interface_two = prefixes.create_interface(node_two) + node2 = session.add_node(DockerNode, options=options) + interface2_data = prefixes.create_interface(node2) # node three node_three = session.add_node(CoreNode) interface_three = prefixes.create_interface(node_three) # add links - session.add_link(node_one.id, switch.id, interface_one) - session.add_link(node_two.id, switch.id, interface_two) + session.add_link(node1.id, switch.id, interface1_data) + session.add_link(node2.id, switch.id, interface2_data) session.add_link(node_three.id, switch.id, interface_three) # instantiate diff --git a/daemon/examples/grpc/distributed_switch.py b/daemon/examples/grpc/distributed_switch.py index 0477efdd..e847016f 100644 --- a/daemon/examples/grpc/distributed_switch.py +++ b/daemon/examples/grpc/distributed_switch.py @@ -44,11 +44,11 @@ def main(args): node = Node(position=position) response = core.add_node(session_id, node) logging.info("created node one: %s", response) - node_one_id = response.node_id + node1_id = response.node_id # create link - interface_one = interface_helper.create_interface(node_one_id, 0) - response = core.add_link(session_id, node_one_id, switch_id, interface_one) + interface1 = interface_helper.create_interface(node1_id, 0) + response = core.add_link(session_id, node1_id, switch_id, interface1) logging.info("created link from node one to switch: %s", response) # create node two @@ -56,11 +56,11 @@ def main(args): node = Node(position=position, server=server_name) response = core.add_node(session_id, node) logging.info("created node two: %s", response) - node_two_id = response.node_id + node2_id = response.node_id # create link - interface_one = interface_helper.create_interface(node_two_id, 0) - response = core.add_link(session_id, node_two_id, switch_id, interface_one) + interface1 = interface_helper.create_interface(node2_id, 0) + response = core.add_link(session_id, node2_id, switch_id, interface1) logging.info("created link from node two to switch: %s", response) # change session state diff --git a/daemon/examples/grpc/emane80211.py b/daemon/examples/grpc/emane80211.py index 5656268c..24532266 100644 --- a/daemon/examples/grpc/emane80211.py +++ b/daemon/examples/grpc/emane80211.py @@ -57,11 +57,11 @@ def main(): node2_id = response.node_id # links nodes to switch - interface_one = interface_helper.create_interface(node1_id, 0) - response = core.add_link(session_id, node1_id, emane_id, interface_one) + interface1 = interface_helper.create_interface(node1_id, 0) + response = core.add_link(session_id, node1_id, emane_id, interface1) logging.info("created link: %s", response) - interface_one = interface_helper.create_interface(node2_id, 0) - response = core.add_link(session_id, node2_id, emane_id, interface_one) + interface1 = interface_helper.create_interface(node2_id, 0) + response = core.add_link(session_id, node2_id, emane_id, interface1) logging.info("created link: %s", response) # change session state diff --git a/daemon/examples/grpc/switch.py b/daemon/examples/grpc/switch.py index 3ab0e0ba..74e315c6 100644 --- a/daemon/examples/grpc/switch.py +++ b/daemon/examples/grpc/switch.py @@ -53,11 +53,11 @@ def main(): node2_id = response.node_id # links nodes to switch - interface_one = interface_helper.create_interface(node1_id, 0) - response = core.add_link(session_id, node1_id, switch_id, interface_one) + interface1 = interface_helper.create_interface(node1_id, 0) + response = core.add_link(session_id, node1_id, switch_id, interface1) logging.info("created link: %s", response) - interface_one = interface_helper.create_interface(node2_id, 0) - response = core.add_link(session_id, node2_id, switch_id, interface_one) + interface1 = interface_helper.create_interface(node2_id, 0) + response = core.add_link(session_id, node2_id, switch_id, interface1) logging.info("created link: %s", response) # change session state diff --git a/daemon/examples/grpc/wlan.py b/daemon/examples/grpc/wlan.py index 6118ae4c..d60ca1be 100644 --- a/daemon/examples/grpc/wlan.py +++ b/daemon/examples/grpc/wlan.py @@ -65,11 +65,11 @@ def main(): node2_id = response.node_id # links nodes to switch - interface_one = interface_helper.create_interface(node1_id, 0) - response = core.add_link(session_id, node1_id, wlan_id, interface_one) + interface1 = interface_helper.create_interface(node1_id, 0) + response = core.add_link(session_id, node1_id, wlan_id, interface1) logging.info("created link: %s", response) - interface_one = interface_helper.create_interface(node2_id, 0) - response = core.add_link(session_id, node2_id, wlan_id, interface_one) + interface1 = interface_helper.create_interface(node2_id, 0) + response = core.add_link(session_id, node2_id, wlan_id, interface1) logging.info("created link: %s", response) # change session state diff --git a/daemon/examples/lxd/lxd2core.py b/daemon/examples/lxd/lxd2core.py index 1365bd4c..49b68943 100644 --- a/daemon/examples/lxd/lxd2core.py +++ b/daemon/examples/lxd/lxd2core.py @@ -17,15 +17,15 @@ if __name__ == "__main__": options = NodeOptions(image="ubuntu") # create node one - node_one = session.add_node(LxcNode, options=options) - interface_one = prefixes.create_interface(node_one) + node1 = session.add_node(LxcNode, options=options) + interface1_data = prefixes.create_interface(node1) # create node two - node_two = session.add_node(CoreNode) - interface_two = prefixes.create_interface(node_two) + node2 = session.add_node(CoreNode) + interface2_data = prefixes.create_interface(node2) # add link - session.add_link(node_one.id, node_two.id, interface_one, interface_two) + session.add_link(node1.id, node2.id, interface1_data, interface2_data) # instantiate session.instantiate() diff --git a/daemon/examples/lxd/lxd2lxd.py b/daemon/examples/lxd/lxd2lxd.py index 53a360e8..18af8037 100644 --- a/daemon/examples/lxd/lxd2lxd.py +++ b/daemon/examples/lxd/lxd2lxd.py @@ -18,15 +18,15 @@ if __name__ == "__main__": options = NodeOptions(image="ubuntu:18.04") # create node one - node_one = session.add_node(LxcNode, options=options) - interface_one = prefixes.create_interface(node_one) + node1 = session.add_node(LxcNode, options=options) + interface1_data = prefixes.create_interface(node1) # create node two - node_two = session.add_node(LxcNode, options=options) - interface_two = prefixes.create_interface(node_two) + node2 = session.add_node(LxcNode, options=options) + interface2_data = prefixes.create_interface(node2) # add link - session.add_link(node_one.id, node_two.id, interface_one, interface_two) + session.add_link(node1.id, node2.id, interface1_data, interface2_data) # instantiate session.instantiate() diff --git a/daemon/examples/lxd/switch.py b/daemon/examples/lxd/switch.py index 3b6226e4..31a79887 100644 --- a/daemon/examples/lxd/switch.py +++ b/daemon/examples/lxd/switch.py @@ -22,21 +22,21 @@ if __name__ == "__main__": switch = session.add_node(SwitchNode) # node one - node_one = session.add_node(LxcNode, options=options) - interface_one = prefixes.create_interface(node_one) + node1 = session.add_node(LxcNode, options=options) + interface1_data = prefixes.create_interface(node1) # node two - node_two = session.add_node(LxcNode, options=options) - interface_two = prefixes.create_interface(node_two) + node2 = session.add_node(LxcNode, options=options) + interface2_data = prefixes.create_interface(node2) # node three - node_three = session.add_node(CoreNode) - interface_three = prefixes.create_interface(node_three) + node3 = session.add_node(CoreNode) + interface3_data = prefixes.create_interface(node3) # add links - session.add_link(node_one.id, switch.id, interface_one) - session.add_link(node_two.id, switch.id, interface_two) - session.add_link(node_three.id, switch.id, interface_three) + session.add_link(node1.id, switch.id, interface1_data) + session.add_link(node2.id, switch.id, interface2_data) + session.add_link(node3.id, switch.id, interface3_data) # instantiate session.instantiate() diff --git a/daemon/examples/python/distributed_emane.py b/daemon/examples/python/distributed_emane.py index 3248a8e3..d9b41ea4 100644 --- a/daemon/examples/python/distributed_emane.py +++ b/daemon/examples/python/distributed_emane.py @@ -52,17 +52,17 @@ def main(args): # create local node, switch, and remote nodes options = NodeOptions(model="mdr") options.set_position(0, 0) - node_one = session.add_node(CoreNode, options=options) + node1 = session.add_node(CoreNode, options=options) emane_net = session.add_node(EmaneNet) session.emane.set_model(emane_net, EmaneIeee80211abgModel) options.server = server_name - node_two = session.add_node(CoreNode, options=options) + node2 = session.add_node(CoreNode, options=options) # create node interfaces and link - interface_one = prefixes.create_interface(node_one) - interface_two = prefixes.create_interface(node_two) - session.add_link(node_one.id, emane_net.id, interface_one=interface_one) - session.add_link(node_two.id, emane_net.id, interface_one=interface_two) + interface1_data = prefixes.create_interface(node1) + interface2_data = prefixes.create_interface(node2) + session.add_link(node1.id, emane_net.id, interface1_data=interface1_data) + session.add_link(node2.id, emane_net.id, interface1_data=interface2_data) # instantiate session session.instantiate() diff --git a/daemon/examples/python/distributed_lxd.py b/daemon/examples/python/distributed_lxd.py index de919012..affb16a8 100644 --- a/daemon/examples/python/distributed_lxd.py +++ b/daemon/examples/python/distributed_lxd.py @@ -43,14 +43,14 @@ def main(args): # create local node, switch, and remote nodes options = NodeOptions(image="ubuntu:18.04") - node_one = session.add_node(LxcNode, options=options) + node1 = session.add_node(LxcNode, options=options) options.server = server_name - node_two = session.add_node(LxcNode, options=options) + node2 = session.add_node(LxcNode, options=options) # create node interfaces and link - interface_one = prefixes.create_interface(node_one) - interface_two = prefixes.create_interface(node_two) - session.add_link(node_one.id, node_two.id, interface_one, interface_two) + interface1_data = prefixes.create_interface(node1) + interface2_data = prefixes.create_interface(node2) + session.add_link(node1.id, node2.id, interface1_data, interface2_data) # instantiate session session.instantiate() diff --git a/daemon/examples/python/distributed_ptp.py b/daemon/examples/python/distributed_ptp.py index 26531399..6bf33474 100644 --- a/daemon/examples/python/distributed_ptp.py +++ b/daemon/examples/python/distributed_ptp.py @@ -43,14 +43,14 @@ def main(args): # create local node, switch, and remote nodes options = NodeOptions() - node_one = session.add_node(CoreNode, options=options) + node1 = session.add_node(CoreNode, options=options) options.server = server_name - node_two = session.add_node(CoreNode, options=options) + node2 = session.add_node(CoreNode, options=options) # create node interfaces and link - interface_one = prefixes.create_interface(node_one) - interface_two = prefixes.create_interface(node_two) - session.add_link(node_one.id, node_two.id, interface_one, interface_two) + interface1_data = prefixes.create_interface(node1) + interface2_data = prefixes.create_interface(node2) + session.add_link(node1.id, node2.id, interface1_data, interface2_data) # instantiate session session.instantiate() diff --git a/daemon/examples/python/distributed_switch.py b/daemon/examples/python/distributed_switch.py index c52c1cc1..8991161e 100644 --- a/daemon/examples/python/distributed_switch.py +++ b/daemon/examples/python/distributed_switch.py @@ -45,17 +45,17 @@ def main(args): session.set_state(EventTypes.CONFIGURATION_STATE) # create local node, switch, and remote nodes - node_one = session.add_node(CoreNode) + node1 = session.add_node(CoreNode) switch = session.add_node(SwitchNode) options = NodeOptions() options.server = server_name - node_two = session.add_node(CoreNode, options=options) + node2 = session.add_node(CoreNode, options=options) # create node interfaces and link - interface_one = prefixes.create_interface(node_one) - interface_two = prefixes.create_interface(node_two) - session.add_link(node_one.id, switch.id, interface_one=interface_one) - session.add_link(node_two.id, switch.id, interface_one=interface_two) + interface1_data = prefixes.create_interface(node1) + interface2_data = prefixes.create_interface(node2) + session.add_link(node1.id, switch.id, interface1_data=interface1_data) + session.add_link(node2.id, switch.id, interface1_data=interface2_data) # instantiate session session.instantiate() diff --git a/daemon/examples/python/emane80211.py b/daemon/examples/python/emane80211.py index da93026b..d3f6652a 100644 --- a/daemon/examples/python/emane80211.py +++ b/daemon/examples/python/emane80211.py @@ -43,7 +43,7 @@ def main(): node = session.add_node(CoreNode, options=options) node.setposition(x=150 * (i + 1), y=150) interface = prefixes.create_interface(node) - session.add_link(node.id, emane_network.id, interface_one=interface) + session.add_link(node.id, emane_network.id, interface1_data=interface) # instantiate session session.instantiate() diff --git a/daemon/examples/python/switch.py b/daemon/examples/python/switch.py index 9475fc47..1b939cd7 100644 --- a/daemon/examples/python/switch.py +++ b/daemon/examples/python/switch.py @@ -32,7 +32,7 @@ def main(): for _ in range(NODES): node = session.add_node(CoreNode) interface = prefixes.create_interface(node) - session.add_link(node.id, switch.id, interface_one=interface) + session.add_link(node.id, switch.id, interface1_data=interface) # instantiate session session.instantiate() diff --git a/daemon/examples/python/switch_inject.py b/daemon/examples/python/switch_inject.py index 8c929e91..59816b19 100644 --- a/daemon/examples/python/switch_inject.py +++ b/daemon/examples/python/switch_inject.py @@ -34,7 +34,7 @@ def main(): for _ in range(NODES): node = session.add_node(CoreNode) interface = prefixes.create_interface(node) - session.add_link(node.id, switch.id, interface_one=interface) + session.add_link(node.id, switch.id, interface1_data=interface) # instantiate session session.instantiate() diff --git a/daemon/examples/python/wlan.py b/daemon/examples/python/wlan.py index b09ae5ce..0302bbd3 100644 --- a/daemon/examples/python/wlan.py +++ b/daemon/examples/python/wlan.py @@ -36,7 +36,7 @@ def main(): for _ in range(NODES): node = session.add_node(CoreNode, options=options) interface = prefixes.create_interface(node) - session.add_link(node.id, wlan.id, interface_one=interface) + session.add_link(node.id, wlan.id, interface1_data=interface) # instantiate session session.instantiate() diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index d602f9d3..8062a731 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -492,16 +492,16 @@ message AddLinkRequest { message AddLinkResponse { bool result = 1; - Interface interface_one = 2; - Interface interface_two = 3; + Interface interface1 = 2; + Interface interface2 = 3; } message EditLinkRequest { int32 session_id = 1; - int32 node_one_id = 2; - int32 node_two_id = 3; - int32 interface_one_id = 4; - int32 interface_two_id = 5; + int32 node1_id = 2; + int32 node2_id = 3; + int32 interface1_id = 4; + int32 interface2_id = 5; LinkOptions options = 6; } @@ -511,10 +511,10 @@ message EditLinkResponse { message DeleteLinkRequest { int32 session_id = 1; - int32 node_one_id = 2; - int32 node_two_id = 3; - int32 interface_one_id = 4; - int32 interface_two_id = 5; + int32 node1_id = 2; + int32 node2_id = 3; + int32 interface1_id = 4; + int32 interface2_id = 5; } message DeleteLinkResponse { @@ -702,11 +702,11 @@ message Node { } message Link { - int32 node_one_id = 1; - int32 node_two_id = 2; + int32 node1_id = 1; + int32 node2_id = 2; LinkType.Enum type = 3; - Interface interface_one = 4; - Interface interface_two = 5; + Interface interface1 = 4; + Interface interface2 = 5; LinkOptions options = 6; int32 network_id = 7; string label = 8; diff --git a/daemon/proto/core/api/grpc/emane.proto b/daemon/proto/core/api/grpc/emane.proto index 8c3ee4ca..e4189700 100644 --- a/daemon/proto/core/api/grpc/emane.proto +++ b/daemon/proto/core/api/grpc/emane.proto @@ -75,8 +75,8 @@ message GetEmaneEventChannelResponse { message EmaneLinkRequest { int32 session_id = 1; - int32 nem_one = 2; - int32 nem_two = 3; + int32 nem1 = 2; + int32 nem2 = 3; bool linked = 4; } @@ -93,12 +93,12 @@ message EmaneModelConfig { message EmanePathlossesRequest { int32 session_id = 1; - int32 node_one = 2; - float rx_one = 3; - int32 interface_one_id = 4; - int32 node_two = 5; - float rx_two = 6; - int32 interface_two_id = 7; + int32 node1_id = 2; + float rx1 = 3; + int32 interface1_id = 4; + int32 node2_id = 5; + float rx2 = 6; + int32 interface2_id = 7; } message EmanePathlossesResponse { diff --git a/daemon/proto/core/api/grpc/wlan.proto b/daemon/proto/core/api/grpc/wlan.proto index bbb9757f..9605d633 100644 --- a/daemon/proto/core/api/grpc/wlan.proto +++ b/daemon/proto/core/api/grpc/wlan.proto @@ -38,8 +38,8 @@ message SetWlanConfigResponse { message WlanLinkRequest { int32 session_id = 1; int32 wlan = 2; - int32 node_one = 3; - int32 node_two = 4; + int32 node1_id = 3; + int32 node2_id = 4; bool linked = 5; } diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py index 2d90ebcc..15e3d869 100644 --- a/daemon/tests/emane/test_emane.py +++ b/daemon/tests/emane/test_emane.py @@ -3,6 +3,7 @@ Unit tests for testing CORE EMANE networks. """ import os from tempfile import TemporaryFile +from typing import Type from xml.etree import ElementTree import pytest @@ -43,7 +44,9 @@ def ping( class TestEmane: @pytest.mark.parametrize("model", _EMANE_MODELS) - def test_models(self, session: Session, model: EmaneModel, ip_prefixes: IpPrefixes): + def test_models( + self, session: Session, model: Type[EmaneModel], ip_prefixes: IpPrefixes + ): """ Test emane models within a basic network. @@ -70,20 +73,20 @@ class TestEmane: # create nodes options = NodeOptions(model="mdr") options.set_position(150, 150) - node_one = session.add_node(CoreNode, options=options) + node1 = session.add_node(CoreNode, options=options) options.set_position(300, 150) - node_two = session.add_node(CoreNode, options=options) + node2 = session.add_node(CoreNode, options=options) - for i, node in enumerate([node_one, node_two]): + for i, node in enumerate([node1, node2]): node.setposition(x=150 * (i + 1), y=150) interface = ip_prefixes.create_interface(node) - session.add_link(node.id, emane_network.id, interface_one=interface) + session.add_link(node.id, emane_network.id, interface1_data=interface) # instantiate session session.instantiate() - # ping n2 from n1 and assert success - status = ping(node_one, node_two, ip_prefixes, count=5) + # ping node2 from node1 and assert success + status = ping(node1, node2, ip_prefixes, count=5) assert not status def test_xml_emane( @@ -110,22 +113,22 @@ class TestEmane: # create nodes options = NodeOptions(model="mdr") options.set_position(150, 150) - node_one = session.add_node(CoreNode, options=options) + node1 = session.add_node(CoreNode, options=options) options.set_position(300, 150) - node_two = session.add_node(CoreNode, options=options) + node2 = session.add_node(CoreNode, options=options) - for i, node in enumerate([node_one, node_two]): + for i, node in enumerate([node1, node2]): node.setposition(x=150 * (i + 1), y=150) interface = ip_prefixes.create_interface(node) - session.add_link(node.id, emane_network.id, interface_one=interface) + session.add_link(node.id, emane_network.id, interface1_data=interface) # instantiate session session.instantiate() # get ids for nodes emane_id = emane_network.id - n1_id = node_one.id - n2_id = node_two.id + node1_id = node1.id + node2_id = node2.id # save xml xml_file = tmpdir.join("session.xml") @@ -141,9 +144,9 @@ class TestEmane: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id, CoreNode) + assert not session.get_node(node1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id, CoreNode) + assert not session.get_node(node2_id, CoreNode) # load saved xml session.open_xml(file_path, start=True) @@ -154,7 +157,7 @@ class TestEmane: ) # verify nodes and configuration were restored - assert session.get_node(n1_id, CoreNode) - assert session.get_node(n2_id, CoreNode) + assert session.get_node(node1_id, CoreNode) + assert session.get_node(node2_id, CoreNode) assert session.get_node(emane_id, EmaneNet) assert value == config_value diff --git a/daemon/tests/test_conf.py b/daemon/tests/test_conf.py index 1973dcee..e90acfbd 100644 --- a/daemon/tests/test_conf.py +++ b/daemon/tests/test_conf.py @@ -14,11 +14,11 @@ from core.nodes.network import WlanNode class TestConfigurableOptions(ConfigurableOptions): - name_one = "value1" - name_two = "value2" + name1 = "value1" + name2 = "value2" options = [ - Configuration(_id=name_one, _type=ConfigDataTypes.STRING, label=name_one), - Configuration(_id=name_two, _type=ConfigDataTypes.STRING, label=name_two), + Configuration(_id=name1, _type=ConfigDataTypes.STRING, label=name1), + Configuration(_id=name2, _type=ConfigDataTypes.STRING, label=name2), ] @@ -33,11 +33,11 @@ class TestConf: # then assert len(default_values) == 2 - assert TestConfigurableOptions.name_one in default_values - assert TestConfigurableOptions.name_two in default_values + assert TestConfigurableOptions.name1 in default_values + assert TestConfigurableOptions.name2 in default_values assert len(instance_default_values) == 2 - assert TestConfigurableOptions.name_one in instance_default_values - assert TestConfigurableOptions.name_two in instance_default_values + assert TestConfigurableOptions.name1 in instance_default_values + assert TestConfigurableOptions.name2 in instance_default_values def test_nodes(self): # given diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 68515a41..626f84a7 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -48,19 +48,19 @@ class TestCore: net_node = session.add_node(net_type) # create nodes - node_one = session.add_node(CoreNode) - node_two = session.add_node(CoreNode) + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) # link nodes to net node - for node in [node_one, node_two]: + for node in [node1, node2]: interface = ip_prefixes.create_interface(node) - session.add_link(node.id, net_node.id, interface_one=interface) + session.add_link(node.id, net_node.id, interface1_data=interface) # instantiate session session.instantiate() - # ping n2 from n1 and assert success - status = ping(node_one, node_two, ip_prefixes) + # ping node2 from node1 and assert success + status = ping(node1, node2, ip_prefixes) assert not status def test_vnode_client(self, request, session: Session, ip_prefixes: IpPrefixes): @@ -75,16 +75,16 @@ class TestCore: ptp_node = session.add_node(PtpNet) # create nodes - node_one = session.add_node(CoreNode) - node_two = session.add_node(CoreNode) + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) # link nodes to ptp net - for node in [node_one, node_two]: + for node in [node1, node2]: interface = ip_prefixes.create_interface(node) - session.add_link(node.id, ptp_node.id, interface_one=interface) + session.add_link(node.id, ptp_node.id, interface1_data=interface) # get node client for testing - client = node_one.client + client = node1.client # instantiate session session.instantiate() @@ -108,13 +108,13 @@ class TestCore: ptp_node = session.add_node(PtpNet) # create nodes - node_one = session.add_node(CoreNode) - node_two = session.add_node(CoreNode) + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) # link nodes to ptp net - for node in [node_one, node_two]: + for node in [node1, node2]: interface = ip_prefixes.create_interface(node) - session.add_link(node.id, ptp_node.id, interface_one=interface) + session.add_link(node.id, ptp_node.id, interface1_data=interface) # instantiate session session.instantiate() @@ -123,22 +123,22 @@ class TestCore: assert ptp_node.all_link_data(MessageFlags.ADD) # check common nets exist between linked nodes - assert node_one.commonnets(node_two) - assert node_two.commonnets(node_one) + assert node1.commonnets(node2) + assert node2.commonnets(node1) # check we can retrieve netif index - assert node_one.ifname(0) - assert node_two.ifname(0) + assert node1.ifname(0) + assert node2.ifname(0) # check interface parameters - interface = node_one.netif(0) + interface = node1.netif(0) interface.setparam("test", 1) assert interface.getparam("test") == 1 assert interface.getparams() # delete netif and test that if no longer exists - node_one.delnetif(0) - assert not node_one.netif(0) + node1.delnetif(0) + assert not node1.netif(0) def test_wlan_ping(self, session: Session, ip_prefixes: IpPrefixes): """ @@ -155,19 +155,19 @@ class TestCore: # create nodes options = NodeOptions(model="mdr") options.set_position(0, 0) - node_one = session.add_node(CoreNode, options=options) - node_two = session.add_node(CoreNode, options=options) + node1 = session.add_node(CoreNode, options=options) + node2 = session.add_node(CoreNode, options=options) # link nodes - for node in [node_one, node_two]: + for node in [node1, node2]: interface = ip_prefixes.create_interface(node) - session.add_link(node.id, wlan_node.id, interface_one=interface) + session.add_link(node.id, wlan_node.id, interface1_data=interface) # instantiate session session.instantiate() - # ping n2 from n1 and assert success - status = ping(node_one, node_two, ip_prefixes) + # ping node2 from node1 and assert success + status = ping(node1, node2, ip_prefixes) assert not status def test_mobility(self, session: Session, ip_prefixes: IpPrefixes): @@ -185,13 +185,13 @@ class TestCore: # create nodes options = NodeOptions(model="mdr") options.set_position(0, 0) - node_one = session.add_node(CoreNode, options=options) - node_two = session.add_node(CoreNode, options=options) + node1 = session.add_node(CoreNode, options=options) + node2 = session.add_node(CoreNode, options=options) # link nodes - for node in [node_one, node_two]: + for node in [node1, node2]: interface = ip_prefixes.create_interface(node) - session.add_link(node.id, wlan_node.id, interface_one=interface) + session.add_link(node.id, wlan_node.id, interface1_data=interface) # configure mobility script for session config = { diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index c0686d71..131af93d 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -34,23 +34,23 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() position = core_pb2.Position(x=50, y=100) - node_one = core_pb2.Node(id=1, position=position, model="PC") + node1 = core_pb2.Node(id=1, position=position, model="PC") position = core_pb2.Position(x=100, y=100) - node_two = core_pb2.Node(id=2, position=position, model="PC") + node2 = core_pb2.Node(id=2, position=position, model="PC") position = core_pb2.Position(x=200, y=200) wlan_node = core_pb2.Node( id=3, type=NodeTypes.WIRELESS_LAN.value, position=position ) - nodes = [node_one, node_two, wlan_node] + nodes = [node1, node2, wlan_node] interface_helper = InterfaceHelper(ip4_prefix="10.83.0.0/16") - interface_one = interface_helper.create_interface(node_one.id, 0) - interface_two = interface_helper.create_interface(node_two.id, 0) + interface1 = interface_helper.create_interface(node1.id, 0) + interface2 = interface_helper.create_interface(node2.id, 0) link = core_pb2.Link( type=core_pb2.LinkType.WIRED, - node_one_id=node_one.id, - node_two_id=node_two.id, - interface_one=interface_one, - interface_two=interface_two, + node1_id=node1.id, + node2_id=node2.id, + interface1=interface1, + interface2=interface2, ) links = [link] hook = core_pb2.Hook( @@ -99,11 +99,11 @@ class TestGrpc: ) mobility_configs = [mobility_config] service_config = ServiceConfig( - node_id=node_one.id, service="DefaultRoute", validate=["echo hello"] + node_id=node1.id, service="DefaultRoute", validate=["echo hello"] ) service_configs = [service_config] service_file_config = ServiceFileConfig( - node_id=node_one.id, + node_id=node1.id, service="DefaultRoute", file="defaultroute.sh", data="echo hello", @@ -128,11 +128,11 @@ class TestGrpc: ) # then - assert node_one.id in session.nodes - assert node_two.id in session.nodes + assert node1.id in session.nodes + assert node2.id in session.nodes assert wlan_node.id in session.nodes - assert session.nodes[node_one.id].netif(0) is not None - assert session.nodes[node_two.id].netif(0) is not None + assert session.nodes[node1.id].netif(0) is not None + assert session.nodes[node2.id].netif(0) is not None hook_file, hook_data = session._hooks[EventTypes.RUNTIME_STATE][0] assert hook_file == hook.file assert hook_data == hook.data @@ -153,11 +153,11 @@ class TestGrpc: ) assert set_model_config[model_config_key] == model_config_value service = session.services.get_service( - node_one.id, service_config.service, default_service=True + node1.id, service_config.service, default_service=True ) assert service.validate == tuple(service_config.validate) service_file = session.services.get_service_file( - node_one, service_file_config.service, service_file_config.file + node1, service_file_config.service, service_file_config.file ) assert service_file.data == service_file_config.data @@ -596,7 +596,7 @@ class TestGrpc: # then with client.context_connect(): response = client.edit_link( - session.id, node.id, switch.id, options, interface_one_id=interface.id + session.id, node.id, switch.id, options, interface1_id=interface.id ) # then @@ -608,28 +608,28 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - node_one = session.add_node(CoreNode) - interface_one = ip_prefixes.create_interface(node_one) - node_two = session.add_node(CoreNode) - interface_two = ip_prefixes.create_interface(node_two) - session.add_link(node_one.id, node_two.id, interface_one, interface_two) + node1 = session.add_node(CoreNode) + interface1 = ip_prefixes.create_interface(node1) + node2 = session.add_node(CoreNode) + interface2 = ip_prefixes.create_interface(node2) + session.add_link(node1.id, node2.id, interface1, interface2) link_node = None for node_id in session.nodes: node = session.nodes[node_id] - if node.id not in {node_one.id, node_two.id}: + if node.id not in {node1.id, node2.id}: link_node = node break - assert len(link_node.all_link_data(0)) == 1 + assert len(link_node.all_link_data()) == 1 # then with client.context_connect(): response = client.delete_link( - session.id, node_one.id, node_two.id, interface_one.id, interface_two.id + session.id, node1.id, node2.id, interface1.id, interface2.id ) # then assert response.result is True - assert len(link_node.all_link_data(0)) == 0 + assert len(link_node.all_link_data()) == 0 def test_get_wlan_config(self, grpc_server: CoreGrpcServer): # given diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index 800a8e62..1187b4d7 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -50,12 +50,13 @@ class TestGui: self, coretlv: CoreHandler, node_type: NodeTypes, model: Optional[str] ): node_id = 1 + name = "node1" message = coreapi.CoreNodeMessage.create( MessageFlags.ADD.value, [ (NodeTlvs.NUMBER, node_id), (NodeTlvs.TYPE, node_type.value), - (NodeTlvs.NAME, "n1"), + (NodeTlvs.NAME, name), (NodeTlvs.X_POSITION, 0), (NodeTlvs.Y_POSITION, 0), (NodeTlvs.MODEL, model), @@ -63,7 +64,9 @@ class TestGui: ) coretlv.handle_message(message) - assert coretlv.session.get_node(node_id, NodeBase) is not None + node = coretlv.session.get_node(node_id, NodeBase) + assert node + assert node.name == name def test_node_update(self, coretlv: CoreHandler): node_id = 1 @@ -99,71 +102,71 @@ class TestGui: coretlv.session.get_node(node_id, NodeBase) def test_link_add_node_to_net(self, coretlv: CoreHandler): - node_one = 1 - coretlv.session.add_node(CoreNode, _id=node_one) - switch = 2 - coretlv.session.add_node(SwitchNode, _id=switch) + node1_id = 1 + coretlv.session.add_node(CoreNode, _id=node1_id) + switch_id = 2 + coretlv.session.add_node(SwitchNode, _id=switch_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface_one = str(ip_prefix[node_one]) + interface1_ip4 = str(ip_prefix[node1_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ - (LinkTlvs.N1_NUMBER, node_one), - (LinkTlvs.N2_NUMBER, switch), + (LinkTlvs.N1_NUMBER, node1_id), + (LinkTlvs.N2_NUMBER, switch_id), (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE1_IP4, interface_one), + (LinkTlvs.INTERFACE1_IP4, interface1_ip4), (LinkTlvs.INTERFACE1_IP4_MASK, 24), ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch, SwitchNode) + switch_node = coretlv.session.get_node(switch_id, SwitchNode) all_links = switch_node.all_link_data() assert len(all_links) == 1 def test_link_add_net_to_node(self, coretlv: CoreHandler): - node_one = 1 - coretlv.session.add_node(CoreNode, _id=node_one) - switch = 2 - coretlv.session.add_node(SwitchNode, _id=switch) + node1_id = 1 + coretlv.session.add_node(CoreNode, _id=node1_id) + switch_id = 2 + coretlv.session.add_node(SwitchNode, _id=switch_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface_one = str(ip_prefix[node_one]) + interface2_ip4 = str(ip_prefix[node1_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ - (LinkTlvs.N1_NUMBER, switch), - (LinkTlvs.N2_NUMBER, node_one), + (LinkTlvs.N1_NUMBER, switch_id), + (LinkTlvs.N2_NUMBER, node1_id), (LinkTlvs.INTERFACE2_NUMBER, 0), - (LinkTlvs.INTERFACE2_IP4, interface_one), + (LinkTlvs.INTERFACE2_IP4, interface2_ip4), (LinkTlvs.INTERFACE2_IP4_MASK, 24), ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch, SwitchNode) + switch_node = coretlv.session.get_node(switch_id, SwitchNode) all_links = switch_node.all_link_data() assert len(all_links) == 1 def test_link_add_node_to_node(self, coretlv: CoreHandler): - node_one = 1 - coretlv.session.add_node(CoreNode, _id=node_one) - node_two = 2 - coretlv.session.add_node(CoreNode, _id=node_two) + node1_id = 1 + coretlv.session.add_node(CoreNode, _id=node1_id) + node2_id = 2 + coretlv.session.add_node(CoreNode, _id=node2_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface_one = str(ip_prefix[node_one]) - interface_two = str(ip_prefix[node_two]) + interface1_ip4 = str(ip_prefix[node1_id]) + interface2_ip4 = str(ip_prefix[node2_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ - (LinkTlvs.N1_NUMBER, node_one), - (LinkTlvs.N2_NUMBER, node_two), + (LinkTlvs.N1_NUMBER, node1_id), + (LinkTlvs.N2_NUMBER, node2_id), (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE1_IP4, interface_one), + (LinkTlvs.INTERFACE1_IP4, interface1_ip4), (LinkTlvs.INTERFACE1_IP4_MASK, 24), (LinkTlvs.INTERFACE2_NUMBER, 0), - (LinkTlvs.INTERFACE2_IP4, interface_two), + (LinkTlvs.INTERFACE2_IP4, interface2_ip4), (LinkTlvs.INTERFACE2_IP4_MASK, 24), ], ) @@ -177,24 +180,24 @@ class TestGui: assert len(all_links) == 1 def test_link_update(self, coretlv: CoreHandler): - node_one = 1 - coretlv.session.add_node(CoreNode, _id=node_one) - switch = 2 - coretlv.session.add_node(SwitchNode, _id=switch) + node1_id = 1 + coretlv.session.add_node(CoreNode, _id=node1_id) + switch_id = 2 + coretlv.session.add_node(SwitchNode, _id=switch_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface_one = str(ip_prefix[node_one]) + interface1_ip4 = str(ip_prefix[node1_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ - (LinkTlvs.N1_NUMBER, node_one), - (LinkTlvs.N2_NUMBER, switch), + (LinkTlvs.N1_NUMBER, node1_id), + (LinkTlvs.N2_NUMBER, switch_id), (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE1_IP4, interface_one), + (LinkTlvs.INTERFACE1_IP4, interface1_ip4), (LinkTlvs.INTERFACE1_IP4_MASK, 24), ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch, SwitchNode) + switch_node = coretlv.session.get_node(switch_id, SwitchNode) all_links = switch_node.all_link_data() assert len(all_links) == 1 link = all_links[0] @@ -204,37 +207,37 @@ class TestGui: message = coreapi.CoreLinkMessage.create( 0, [ - (LinkTlvs.N1_NUMBER, node_one), - (LinkTlvs.N2_NUMBER, switch), + (LinkTlvs.N1_NUMBER, node1_id), + (LinkTlvs.N2_NUMBER, switch_id), (LinkTlvs.INTERFACE1_NUMBER, 0), (LinkTlvs.BANDWIDTH, bandwidth), ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch, SwitchNode) + switch_node = coretlv.session.get_node(switch_id, SwitchNode) all_links = switch_node.all_link_data() assert len(all_links) == 1 link = all_links[0] assert link.bandwidth == bandwidth def test_link_delete_node_to_node(self, coretlv: CoreHandler): - node_one = 1 - coretlv.session.add_node(CoreNode, _id=node_one) - node_two = 2 - coretlv.session.add_node(CoreNode, _id=node_two) + node1_id = 1 + coretlv.session.add_node(CoreNode, _id=node1_id) + node2_id = 2 + coretlv.session.add_node(CoreNode, _id=node2_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface_one = str(ip_prefix[node_one]) - interface_two = str(ip_prefix[node_two]) + interface1_ip4 = str(ip_prefix[node1_id]) + interface2_ip4 = str(ip_prefix[node2_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ - (LinkTlvs.N1_NUMBER, node_one), - (LinkTlvs.N2_NUMBER, node_two), + (LinkTlvs.N1_NUMBER, node1_id), + (LinkTlvs.N2_NUMBER, node2_id), (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE1_IP4, interface_one), + (LinkTlvs.INTERFACE1_IP4, interface1_ip4), (LinkTlvs.INTERFACE1_IP4_MASK, 24), - (LinkTlvs.INTERFACE2_IP4, interface_two), + (LinkTlvs.INTERFACE2_IP4, interface2_ip4), (LinkTlvs.INTERFACE2_IP4_MASK, 24), ], ) @@ -248,8 +251,8 @@ class TestGui: message = coreapi.CoreLinkMessage.create( MessageFlags.DELETE.value, [ - (LinkTlvs.N1_NUMBER, node_one), - (LinkTlvs.N2_NUMBER, node_two), + (LinkTlvs.N1_NUMBER, node1_id), + (LinkTlvs.N2_NUMBER, node2_id), (LinkTlvs.INTERFACE1_NUMBER, 0), (LinkTlvs.INTERFACE2_NUMBER, 0), ], @@ -263,74 +266,74 @@ class TestGui: assert len(all_links) == 0 def test_link_delete_node_to_net(self, coretlv: CoreHandler): - node_one = 1 - coretlv.session.add_node(CoreNode, _id=node_one) - switch = 2 - coretlv.session.add_node(SwitchNode, _id=switch) + node1_id = 1 + coretlv.session.add_node(CoreNode, _id=node1_id) + switch_id = 2 + coretlv.session.add_node(SwitchNode, _id=switch_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface_one = str(ip_prefix[node_one]) + interface1_ip4 = str(ip_prefix[node1_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ - (LinkTlvs.N1_NUMBER, node_one), - (LinkTlvs.N2_NUMBER, switch), + (LinkTlvs.N1_NUMBER, node1_id), + (LinkTlvs.N2_NUMBER, switch_id), (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE1_IP4, interface_one), + (LinkTlvs.INTERFACE1_IP4, interface1_ip4), (LinkTlvs.INTERFACE1_IP4_MASK, 24), ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch, SwitchNode) + switch_node = coretlv.session.get_node(switch_id, SwitchNode) all_links = switch_node.all_link_data() assert len(all_links) == 1 message = coreapi.CoreLinkMessage.create( MessageFlags.DELETE.value, [ - (LinkTlvs.N1_NUMBER, node_one), - (LinkTlvs.N2_NUMBER, switch), + (LinkTlvs.N1_NUMBER, node1_id), + (LinkTlvs.N2_NUMBER, switch_id), (LinkTlvs.INTERFACE1_NUMBER, 0), ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch, SwitchNode) + switch_node = coretlv.session.get_node(switch_id, SwitchNode) all_links = switch_node.all_link_data() assert len(all_links) == 0 def test_link_delete_net_to_node(self, coretlv: CoreHandler): - node_one = 1 - coretlv.session.add_node(CoreNode, _id=node_one) - switch = 2 - coretlv.session.add_node(SwitchNode, _id=switch) + node1_id = 1 + coretlv.session.add_node(CoreNode, _id=node1_id) + switch_id = 2 + coretlv.session.add_node(SwitchNode, _id=switch_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface_one = str(ip_prefix[node_one]) + interface1_ip4 = str(ip_prefix[node1_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ - (LinkTlvs.N1_NUMBER, node_one), - (LinkTlvs.N2_NUMBER, switch), + (LinkTlvs.N1_NUMBER, node1_id), + (LinkTlvs.N2_NUMBER, switch_id), (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE1_IP4, interface_one), + (LinkTlvs.INTERFACE1_IP4, interface1_ip4), (LinkTlvs.INTERFACE1_IP4_MASK, 24), ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch, SwitchNode) + switch_node = coretlv.session.get_node(switch_id, SwitchNode) all_links = switch_node.all_link_data() assert len(all_links) == 1 message = coreapi.CoreLinkMessage.create( MessageFlags.DELETE.value, [ - (LinkTlvs.N1_NUMBER, switch), - (LinkTlvs.N2_NUMBER, node_one), + (LinkTlvs.N1_NUMBER, switch_id), + (LinkTlvs.N2_NUMBER, node1_id), (LinkTlvs.INTERFACE2_NUMBER, 0), ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch, SwitchNode) + switch_node = coretlv.session.get_node(switch_id, SwitchNode) all_links = switch_node.all_link_data() assert len(all_links) == 0 diff --git a/daemon/tests/test_links.py b/daemon/tests/test_links.py index 9f693da1..61f9d13d 100644 --- a/daemon/tests/test_links.py +++ b/daemon/tests/test_links.py @@ -10,71 +10,71 @@ def create_ptp_network( session: Session, ip_prefixes: IpPrefixes ) -> Tuple[CoreNode, CoreNode]: # create nodes - node_one = session.add_node(CoreNode) - node_two = session.add_node(CoreNode) + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) # link nodes to net node - interface_one = ip_prefixes.create_interface(node_one) - interface_two = ip_prefixes.create_interface(node_two) - session.add_link(node_one.id, node_two.id, interface_one, interface_two) + interface1_data = ip_prefixes.create_interface(node1) + interface2_data = ip_prefixes.create_interface(node2) + session.add_link(node1.id, node2.id, interface1_data, interface2_data) # instantiate session session.instantiate() - return node_one, node_two + return node1, node2 class TestLinks: def test_add_ptp(self, session: Session, ip_prefixes: IpPrefixes): # given - node_one = session.add_node(CoreNode) - node_two = session.add_node(CoreNode) - interface_one = ip_prefixes.create_interface(node_one) - interface_two = ip_prefixes.create_interface(node_two) + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) + interface1_data = ip_prefixes.create_interface(node1) + interface2_data = ip_prefixes.create_interface(node2) # when - session.add_link(node_one.id, node_two.id, interface_one, interface_two) + session.add_link(node1.id, node2.id, interface1_data, interface2_data) # then - assert node_one.netif(interface_one.id) - assert node_two.netif(interface_two.id) + assert node1.netif(interface1_data.id) + assert node2.netif(interface2_data.id) def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given - node_one = session.add_node(CoreNode) - node_two = session.add_node(SwitchNode) - interface_one = ip_prefixes.create_interface(node_one) + node1 = session.add_node(CoreNode) + node2 = session.add_node(SwitchNode) + interface1_data = ip_prefixes.create_interface(node1) # when - session.add_link(node_one.id, node_two.id, interface_one=interface_one) + session.add_link(node1.id, node2.id, interface1_data=interface1_data) # then - assert node_two.all_link_data() - assert node_one.netif(interface_one.id) + assert node2.all_link_data() + assert node1.netif(interface1_data.id) def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given - node_one = session.add_node(SwitchNode) - node_two = session.add_node(CoreNode) - interface_two = ip_prefixes.create_interface(node_two) + node1 = session.add_node(SwitchNode) + node2 = session.add_node(CoreNode) + interface2_data = ip_prefixes.create_interface(node2) # when - session.add_link(node_one.id, node_two.id, interface_two=interface_two) + session.add_link(node1.id, node2.id, interface2_data=interface2_data) # then - assert node_one.all_link_data() - assert node_two.netif(interface_two.id) + assert node1.all_link_data() + assert node2.netif(interface2_data.id) def test_add_net_to_net(self, session): # given - node_one = session.add_node(SwitchNode) - node_two = session.add_node(SwitchNode) + node1 = session.add_node(SwitchNode) + node2 = session.add_node(SwitchNode) # when - session.add_link(node_one.id, node_two.id) + session.add_link(node1.id, node2.id) # then - assert node_one.all_link_data() + assert node1.all_link_data() def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -83,34 +83,31 @@ class TestLinks: per = 25 dup = 25 jitter = 10 - node_one = session.add_node(CoreNode) - node_two = session.add_node(SwitchNode) - interface_one_data = ip_prefixes.create_interface(node_one) - session.add_link(node_one.id, node_two.id, interface_one_data) - interface_one = node_one.netif(interface_one_data.id) - assert interface_one.getparam("delay") != delay - assert interface_one.getparam("bw") != bandwidth - assert interface_one.getparam("loss") != per - assert interface_one.getparam("duplicate") != dup - assert interface_one.getparam("jitter") != jitter + node1 = session.add_node(CoreNode) + node2 = session.add_node(SwitchNode) + interface1_data = ip_prefixes.create_interface(node1) + session.add_link(node1.id, node2.id, interface1_data) + interface1 = node1.netif(interface1_data.id) + assert interface1.getparam("delay") != delay + assert interface1.getparam("bw") != bandwidth + assert interface1.getparam("loss") != per + assert interface1.getparam("duplicate") != dup + assert interface1.getparam("jitter") != jitter # when - link_options = LinkOptions( + options = LinkOptions( delay=delay, bandwidth=bandwidth, per=per, dup=dup, jitter=jitter ) session.update_link( - node_one.id, - node_two.id, - interface_one_id=interface_one_data.id, - options=link_options, + node1.id, node2.id, interface1_id=interface1_data.id, options=options ) # then - assert interface_one.getparam("delay") == delay - assert interface_one.getparam("bw") == bandwidth - assert interface_one.getparam("loss") == per - assert interface_one.getparam("duplicate") == dup - assert interface_one.getparam("jitter") == jitter + assert interface1.getparam("delay") == delay + assert interface1.getparam("bw") == bandwidth + assert interface1.getparam("loss") == per + assert interface1.getparam("duplicate") == dup + assert interface1.getparam("jitter") == jitter def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -119,34 +116,31 @@ class TestLinks: per = 25 dup = 25 jitter = 10 - node_one = session.add_node(SwitchNode) - node_two = session.add_node(CoreNode) - interface_two_data = ip_prefixes.create_interface(node_two) - session.add_link(node_one.id, node_two.id, interface_two=interface_two_data) - interface_two = node_two.netif(interface_two_data.id) - assert interface_two.getparam("delay") != delay - assert interface_two.getparam("bw") != bandwidth - assert interface_two.getparam("loss") != per - assert interface_two.getparam("duplicate") != dup - assert interface_two.getparam("jitter") != jitter + node1 = session.add_node(SwitchNode) + node2 = session.add_node(CoreNode) + interface2_data = ip_prefixes.create_interface(node2) + session.add_link(node1.id, node2.id, interface2_data=interface2_data) + interface2 = node2.netif(interface2_data.id) + assert interface2.getparam("delay") != delay + assert interface2.getparam("bw") != bandwidth + assert interface2.getparam("loss") != per + assert interface2.getparam("duplicate") != dup + assert interface2.getparam("jitter") != jitter # when - link_options = LinkOptions( + options = LinkOptions( delay=delay, bandwidth=bandwidth, per=per, dup=dup, jitter=jitter ) session.update_link( - node_one.id, - node_two.id, - interface_two_id=interface_two_data.id, - options=link_options, + node1.id, node2.id, interface2_id=interface2_data.id, options=options ) # then - assert interface_two.getparam("delay") == delay - assert interface_two.getparam("bw") == bandwidth - assert interface_two.getparam("loss") == per - assert interface_two.getparam("duplicate") == dup - assert interface_two.getparam("jitter") == jitter + assert interface2.getparam("delay") == delay + assert interface2.getparam("bw") == bandwidth + assert interface2.getparam("loss") == per + assert interface2.getparam("duplicate") == dup + assert interface2.getparam("jitter") == jitter def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -155,93 +149,85 @@ class TestLinks: per = 25 dup = 25 jitter = 10 - node_one = session.add_node(CoreNode) - node_two = session.add_node(CoreNode) - interface_one_data = ip_prefixes.create_interface(node_one) - interface_two_data = ip_prefixes.create_interface(node_two) - session.add_link( - node_one.id, node_two.id, interface_one_data, interface_two_data - ) - interface_one = node_one.netif(interface_one_data.id) - interface_two = node_two.netif(interface_two_data.id) - assert interface_one.getparam("delay") != delay - assert interface_one.getparam("bw") != bandwidth - assert interface_one.getparam("loss") != per - assert interface_one.getparam("duplicate") != dup - assert interface_one.getparam("jitter") != jitter - assert interface_two.getparam("delay") != delay - assert interface_two.getparam("bw") != bandwidth - assert interface_two.getparam("loss") != per - assert interface_two.getparam("duplicate") != dup - assert interface_two.getparam("jitter") != jitter + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) + interface1_data = ip_prefixes.create_interface(node1) + interface2_data = ip_prefixes.create_interface(node2) + session.add_link(node1.id, node2.id, interface1_data, interface2_data) + interface1 = node1.netif(interface1_data.id) + interface2 = node2.netif(interface2_data.id) + assert interface1.getparam("delay") != delay + assert interface1.getparam("bw") != bandwidth + assert interface1.getparam("loss") != per + assert interface1.getparam("duplicate") != dup + assert interface1.getparam("jitter") != jitter + assert interface2.getparam("delay") != delay + assert interface2.getparam("bw") != bandwidth + assert interface2.getparam("loss") != per + assert interface2.getparam("duplicate") != dup + assert interface2.getparam("jitter") != jitter # when - link_options = LinkOptions( + options = LinkOptions( delay=delay, bandwidth=bandwidth, per=per, dup=dup, jitter=jitter ) session.update_link( - node_one.id, - node_two.id, - interface_one_data.id, - interface_two_data.id, - link_options, + node1.id, node2.id, interface1_data.id, interface2_data.id, options ) # then - assert interface_one.getparam("delay") == delay - assert interface_one.getparam("bw") == bandwidth - assert interface_one.getparam("loss") == per - assert interface_one.getparam("duplicate") == dup - assert interface_one.getparam("jitter") == jitter - assert interface_two.getparam("delay") == delay - assert interface_two.getparam("bw") == bandwidth - assert interface_two.getparam("loss") == per - assert interface_two.getparam("duplicate") == dup - assert interface_two.getparam("jitter") == jitter + assert interface1.getparam("delay") == delay + assert interface1.getparam("bw") == bandwidth + assert interface1.getparam("loss") == per + assert interface1.getparam("duplicate") == dup + assert interface1.getparam("jitter") == jitter + assert interface2.getparam("delay") == delay + assert interface2.getparam("bw") == bandwidth + assert interface2.getparam("loss") == per + assert interface2.getparam("duplicate") == dup + assert interface2.getparam("jitter") == jitter def test_delete_ptp(self, session: Session, ip_prefixes: IpPrefixes): # given - node_one = session.add_node(CoreNode) - node_two = session.add_node(CoreNode) - interface_one = ip_prefixes.create_interface(node_one) - interface_two = ip_prefixes.create_interface(node_two) - session.add_link(node_one.id, node_two.id, interface_one, interface_two) - assert node_one.netif(interface_one.id) - assert node_two.netif(interface_two.id) + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) + interface1_data = ip_prefixes.create_interface(node1) + interface2_data = ip_prefixes.create_interface(node2) + session.add_link(node1.id, node2.id, interface1_data, interface2_data) + assert node1.netif(interface1_data.id) + assert node2.netif(interface2_data.id) # when - session.delete_link( - node_one.id, node_two.id, interface_one.id, interface_two.id - ) + session.delete_link(node1.id, node2.id, interface1_data.id, interface2_data.id) # then - assert not node_one.netif(interface_one.id) - assert not node_two.netif(interface_two.id) + assert not node1.netif(interface1_data.id) + assert not node2.netif(interface2_data.id) def test_delete_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given - node_one = session.add_node(CoreNode) - node_two = session.add_node(SwitchNode) - interface_one = ip_prefixes.create_interface(node_one) - session.add_link(node_one.id, node_two.id, interface_one) - assert node_one.netif(interface_one.id) + node1 = session.add_node(CoreNode) + node2 = session.add_node(SwitchNode) + interface1_data = ip_prefixes.create_interface(node1) + session.add_link(node1.id, node2.id, interface1_data) + assert node1.netif(interface1_data.id) # when - session.delete_link(node_one.id, node_two.id, interface_one_id=interface_one.id) + session.delete_link(node1.id, node2.id, interface1_id=interface1_data.id) # then - assert not node_one.netif(interface_one.id) + assert not node1.netif(interface1_data.id) def test_delete_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given - node_one = session.add_node(SwitchNode) - node_two = session.add_node(CoreNode) - interface_two = ip_prefixes.create_interface(node_two) - session.add_link(node_one.id, node_two.id, interface_two=interface_two) - assert node_two.netif(interface_two.id) + node1 = session.add_node(SwitchNode) + node2 = session.add_node(CoreNode) + interface2_data = ip_prefixes.create_interface(node2) + session.add_link(node1.id, node2.id, interface2_data=interface2_data) + assert node2.netif(interface2_data.id) # when - session.delete_link(node_one.id, node_two.id, interface_two_id=interface_two.id) + session.delete_link(node1.id, node2.id, interface2_id=interface2_data.id) # then - assert not node_two.netif(interface_two.id) + assert not node2.netif(interface2_data.id) diff --git a/daemon/tests/test_services.py b/daemon/tests/test_services.py index e304a275..264a6566 100644 --- a/daemon/tests/test_services.py +++ b/daemon/tests/test_services.py @@ -206,23 +206,23 @@ class TestServices: # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_ONE) - node_one = session.add_node(CoreNode) - node_two = session.add_node(CoreNode) + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) file_name = my_service.configs[0] - file_data_one = "# custom file one" - file_data_two = "# custom file two" + file_data1 = "# custom file one" + file_data2 = "# custom file two" session.services.set_service_file( - node_one.id, my_service.name, file_name, file_data_one + node1.id, my_service.name, file_name, file_data1 ) session.services.set_service_file( - node_two.id, my_service.name, file_name, file_data_two + node2.id, my_service.name, file_name, file_data2 ) # when - custom_service_one = session.services.get_service(node_one.id, my_service.name) - session.services.create_service_files(node_one, custom_service_one) - custom_service_two = session.services.get_service(node_two.id, my_service.name) - session.services.create_service_files(node_two, custom_service_two) + custom_service1 = session.services.get_service(node1.id, my_service.name) + session.services.create_service_files(node1, custom_service1) + custom_service2 = session.services.get_service(node2.id, my_service.name) + session.services.create_service_files(node2, custom_service2) def test_service_import(self): """ diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index c40a9ef3..35e03f7d 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -68,20 +68,20 @@ class TestXml: ptp_node = session.add_node(PtpNet) # create nodes - node_one = session.add_node(CoreNode) - node_two = session.add_node(CoreNode) + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) # link nodes to ptp net - for node in [node_one, node_two]: + for node in [node1, node2]: interface = ip_prefixes.create_interface(node) - session.add_link(node.id, ptp_node.id, interface_one=interface) + session.add_link(node.id, ptp_node.id, interface1_data=interface) # instantiate session session.instantiate() # get ids for nodes - n1_id = node_one.id - n2_id = node_two.id + node1_id = node1.id + node2_id = node2.id # save xml xml_file = tmpdir.join("session.xml") @@ -97,16 +97,16 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id, CoreNode) + assert not session.get_node(node1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id, CoreNode) + assert not session.get_node(node2_id, CoreNode) # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - assert session.get_node(n1_id, CoreNode) - assert session.get_node(n2_id, CoreNode) + assert session.get_node(node1_id, CoreNode) + assert session.get_node(node2_id, CoreNode) def test_xml_ptp_services( self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes @@ -123,28 +123,28 @@ class TestXml: # create nodes options = NodeOptions(model="host") - node_one = session.add_node(CoreNode, options=options) - node_two = session.add_node(CoreNode) + node1 = session.add_node(CoreNode, options=options) + node2 = session.add_node(CoreNode) # link nodes to ptp net - for node in [node_one, node_two]: + for node in [node1, node2]: interface = ip_prefixes.create_interface(node) - session.add_link(node.id, ptp_node.id, interface_one=interface) + session.add_link(node.id, ptp_node.id, interface1_data=interface) # set custom values for node service - session.services.set_service(node_one.id, SshService.name) + session.services.set_service(node1.id, SshService.name) service_file = SshService.configs[0] file_data = "# test" session.services.set_service_file( - node_one.id, SshService.name, service_file, file_data + node1.id, SshService.name, service_file, file_data ) # instantiate session session.instantiate() # get ids for nodes - n1_id = node_one.id - n2_id = node_two.id + node1_id = node1.id + node2_id = node2.id # save xml xml_file = tmpdir.join("session.xml") @@ -160,19 +160,19 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id, CoreNode) + assert not session.get_node(node1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id, CoreNode) + assert not session.get_node(node2_id, CoreNode) # load saved xml session.open_xml(file_path, start=True) # retrieve custom service - service = session.services.get_service(node_one.id, SshService.name) + service = session.services.get_service(node1.id, SshService.name) # verify nodes have been recreated - assert session.get_node(n1_id, CoreNode) - assert session.get_node(n2_id, CoreNode) + assert session.get_node(node1_id, CoreNode) + assert session.get_node(node2_id, CoreNode) assert service.config_data.get(service_file) == file_data def test_xml_mobility( @@ -192,21 +192,21 @@ class TestXml: # create nodes options = NodeOptions(model="mdr") options.set_position(0, 0) - node_one = session.add_node(CoreNode, options=options) - node_two = session.add_node(CoreNode, options=options) + node1 = session.add_node(CoreNode, options=options) + node2 = session.add_node(CoreNode, options=options) # link nodes - for node in [node_one, node_two]: + for node in [node1, node2]: interface = ip_prefixes.create_interface(node) - session.add_link(node.id, wlan_node.id, interface_one=interface) + session.add_link(node.id, wlan_node.id, interface1_data=interface) # instantiate session session.instantiate() # get ids for nodes wlan_id = wlan_node.id - n1_id = node_one.id - n2_id = node_two.id + node1_id = node1.id + node2_id = node2.id # save xml xml_file = tmpdir.join("session.xml") @@ -222,9 +222,9 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id, CoreNode) + assert not session.get_node(node1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id, CoreNode) + assert not session.get_node(node2_id, CoreNode) # load saved xml session.open_xml(file_path, start=True) @@ -233,8 +233,8 @@ class TestXml: value = str(session.mobility.get_config("test", wlan_id, BasicRangeModel.name)) # verify nodes and configuration were restored - assert session.get_node(n1_id, CoreNode) - assert session.get_node(n2_id, CoreNode) + assert session.get_node(node1_id, CoreNode) + assert session.get_node(node2_id, CoreNode) assert session.get_node(wlan_id, WlanNode) assert value == "1" @@ -246,18 +246,18 @@ class TestXml: :param tmpdir: tmpdir to create data in """ # create nodes - switch_one = session.add_node(SwitchNode) - switch_two = session.add_node(SwitchNode) + switch1 = session.add_node(SwitchNode) + switch2 = session.add_node(SwitchNode) # link nodes - session.add_link(switch_one.id, switch_two.id) + session.add_link(switch1.id, switch2.id) # instantiate session session.instantiate() # get ids for nodes - n1_id = switch_one.id - n2_id = switch_two.id + node1_id = switch1.id + node2_id = switch2.id # save xml xml_file = tmpdir.join("session.xml") @@ -273,19 +273,19 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id, SwitchNode) + assert not session.get_node(node1_id, SwitchNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id, SwitchNode) + assert not session.get_node(node2_id, SwitchNode) # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - switch_one = session.get_node(n1_id, SwitchNode) - switch_two = session.get_node(n2_id, SwitchNode) - assert switch_one - assert switch_two - assert len(switch_one.all_link_data() + switch_two.all_link_data()) == 1 + switch1 = session.get_node(node1_id, SwitchNode) + switch2 = session.get_node(node2_id, SwitchNode) + assert switch1 + assert switch2 + assert len(switch1.all_link_data() + switch2.all_link_data()) == 1 def test_link_options( self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes @@ -298,25 +298,25 @@ class TestXml: :param ip_prefixes: generates ip addresses for nodes """ # create nodes - node_one = session.add_node(CoreNode) - interface_one = ip_prefixes.create_interface(node_one) + node1 = session.add_node(CoreNode) + interface1_data = ip_prefixes.create_interface(node1) switch = session.add_node(SwitchNode) # create link - link_options = LinkOptions() - link_options.per = 10.5 - link_options.bandwidth = 50000 - link_options.jitter = 10 - link_options.delay = 30 - link_options.dup = 5 - session.add_link(node_one.id, switch.id, interface_one, options=link_options) + options = LinkOptions() + options.per = 10.5 + options.bandwidth = 50000 + options.jitter = 10 + options.delay = 30 + options.dup = 5 + session.add_link(node1.id, switch.id, interface1_data, options=options) # instantiate session session.instantiate() # get ids for nodes - n1_id = node_one.id - n2_id = switch.id + node1_id = node1.id + node2_id = switch.id # save xml xml_file = tmpdir.join("session.xml") @@ -332,26 +332,26 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id, CoreNode) + assert not session.get_node(node1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id, SwitchNode) + assert not session.get_node(node2_id, SwitchNode) # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - assert session.get_node(n1_id, CoreNode) - assert session.get_node(n2_id, SwitchNode) + assert session.get_node(node1_id, CoreNode) + assert session.get_node(node2_id, SwitchNode) links = [] for node_id in session.nodes: node = session.nodes[node_id] links += node.all_link_data() link = links[0] - assert link_options.per == link.per - assert link_options.bandwidth == link.bandwidth - assert link_options.jitter == link.jitter - assert link_options.delay == link.delay - assert link_options.dup == link.dup + assert options.per == link.per + assert options.bandwidth == link.bandwidth + assert options.jitter == link.jitter + assert options.delay == link.delay + assert options.dup == link.dup def test_link_options_ptp( self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes @@ -364,28 +364,26 @@ class TestXml: :param ip_prefixes: generates ip addresses for nodes """ # create nodes - node_one = session.add_node(CoreNode) - interface_one = ip_prefixes.create_interface(node_one) - node_two = session.add_node(CoreNode) - interface_two = ip_prefixes.create_interface(node_two) + node1 = session.add_node(CoreNode) + interface1_data = ip_prefixes.create_interface(node1) + node2 = session.add_node(CoreNode) + interface2_data = ip_prefixes.create_interface(node2) # create link - link_options = LinkOptions() - link_options.per = 10.5 - link_options.bandwidth = 50000 - link_options.jitter = 10 - link_options.delay = 30 - link_options.dup = 5 - session.add_link( - node_one.id, node_two.id, interface_one, interface_two, link_options - ) + options = LinkOptions() + options.per = 10.5 + options.bandwidth = 50000 + options.jitter = 10 + options.delay = 30 + options.dup = 5 + session.add_link(node1.id, node2.id, interface1_data, interface2_data, options) # instantiate session session.instantiate() # get ids for nodes - n1_id = node_one.id - n2_id = node_two.id + node1_id = node1.id + node2_id = node2.id # save xml xml_file = tmpdir.join("session.xml") @@ -401,26 +399,26 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id, CoreNode) + assert not session.get_node(node1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id, CoreNode) + assert not session.get_node(node2_id, CoreNode) # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - assert session.get_node(n1_id, CoreNode) - assert session.get_node(n2_id, CoreNode) + assert session.get_node(node1_id, CoreNode) + assert session.get_node(node2_id, CoreNode) links = [] for node_id in session.nodes: node = session.nodes[node_id] links += node.all_link_data() link = links[0] - assert link_options.per == link.per - assert link_options.bandwidth == link.bandwidth - assert link_options.jitter == link.jitter - assert link_options.delay == link.delay - assert link_options.dup == link.dup + assert options.per == link.per + assert options.bandwidth == link.bandwidth + assert options.jitter == link.jitter + assert options.delay == link.delay + assert options.dup == link.dup def test_link_options_bidirectional( self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes @@ -433,43 +431,37 @@ class TestXml: :param ip_prefixes: generates ip addresses for nodes """ # create nodes - node_one = session.add_node(CoreNode) - interface_one = ip_prefixes.create_interface(node_one) - node_two = session.add_node(CoreNode) - interface_two = ip_prefixes.create_interface(node_two) + node1 = session.add_node(CoreNode) + interface1_data = ip_prefixes.create_interface(node1) + node2 = session.add_node(CoreNode) + interface2_data = ip_prefixes.create_interface(node2) # create link - link_options_one = LinkOptions() - link_options_one.unidirectional = 1 - link_options_one.bandwidth = 5000 - link_options_one.delay = 10 - link_options_one.per = 10.5 - link_options_one.dup = 5 - link_options_one.jitter = 5 - session.add_link( - node_one.id, node_two.id, interface_one, interface_two, link_options_one - ) - link_options_two = LinkOptions() - link_options_two.unidirectional = 1 - link_options_two.bandwidth = 10000 - link_options_two.delay = 20 - link_options_two.per = 10 - link_options_two.dup = 10 - link_options_two.jitter = 10 + options1 = LinkOptions() + options1.unidirectional = 1 + options1.bandwidth = 5000 + options1.delay = 10 + options1.per = 10.5 + options1.dup = 5 + options1.jitter = 5 + session.add_link(node1.id, node2.id, interface1_data, interface2_data, options1) + options2 = LinkOptions() + options2.unidirectional = 1 + options2.bandwidth = 10000 + options2.delay = 20 + options2.per = 10 + options2.dup = 10 + options2.jitter = 10 session.update_link( - node_two.id, - node_one.id, - interface_two.id, - interface_one.id, - link_options_two, + node2.id, node1.id, interface2_data.id, interface1_data.id, options2 ) # instantiate session session.instantiate() # get ids for nodes - n1_id = node_one.id - n2_id = node_two.id + node1_id = node1.id + node2_id = node2.id # save xml xml_file = tmpdir.join("session.xml") @@ -485,30 +477,30 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(n1_id, CoreNode) + assert not session.get_node(node1_id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(n2_id, CoreNode) + assert not session.get_node(node2_id, CoreNode) # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - assert session.get_node(n1_id, CoreNode) - assert session.get_node(n2_id, CoreNode) + assert session.get_node(node1_id, CoreNode) + assert session.get_node(node2_id, CoreNode) links = [] for node_id in session.nodes: node = session.nodes[node_id] links += node.all_link_data() assert len(links) == 2 - link_one = links[0] - link_two = links[1] - assert link_options_one.bandwidth == link_one.bandwidth - assert link_options_one.delay == link_one.delay - assert link_options_one.per == link_one.per - assert link_options_one.dup == link_one.dup - assert link_options_one.jitter == link_one.jitter - assert link_options_two.bandwidth == link_two.bandwidth - assert link_options_two.delay == link_two.delay - assert link_options_two.per == link_two.per - assert link_options_two.dup == link_two.dup - assert link_options_two.jitter == link_two.jitter + link1 = links[0] + link2 = links[1] + assert options1.bandwidth == link1.bandwidth + assert options1.delay == link1.delay + assert options1.per == link1.per + assert options1.dup == link1.dup + assert options1.jitter == link1.jitter + assert options2.bandwidth == link2.bandwidth + assert options2.delay == link2.delay + assert options2.per == link2.per + assert options2.dup == link2.dup + assert options2.jitter == link2.jitter diff --git a/docs/scripting.md b/docs/scripting.md index 8c1a705c..59bc02ae 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -62,7 +62,7 @@ def main(): for _ in range(NODES): node = session.add_node(CoreNode) interface = prefixes.create_interface(node) - session.add_link(node.id, switch.id, interface_one=interface) + session.add_link(node.id, switch.id, interface1_data=interface) # instantiate session session.instantiate() From 178d12b32761a1a5708b70cfe8948a09115aaf4a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 12 Jun 2020 17:32:55 -0700 Subject: [PATCH 0345/1131] daemon: updated variables for InterfaceData to be denote data to make it more clear --- daemon/core/api/grpc/grpcutils.py | 6 +++--- daemon/core/emulator/emudata.py | 6 +++--- daemon/core/nodes/base.py | 20 ++++++++++---------- daemon/core/nodes/physical.py | 24 ++++++++++++++---------- 4 files changed, 30 insertions(+), 26 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 539face1..9a944bbe 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -59,13 +59,13 @@ def link_interface(interface_proto: core_pb2.Interface) -> InterfaceData: :param interface_proto: interface proto :return: interface data """ - interface = None + interface_data = None if interface_proto: name = interface_proto.name if interface_proto.name else None mac = interface_proto.mac if interface_proto.mac else None ip4 = interface_proto.ip4 if interface_proto.ip4 else None ip6 = interface_proto.ip6 if interface_proto.ip6 else None - interface = InterfaceData( + interface_data = InterfaceData( id=interface_proto.id, name=name, mac=mac, @@ -74,7 +74,7 @@ def link_interface(interface_proto: core_pb2.Interface) -> InterfaceData: ip6=ip6, ip6_mask=interface_proto.ip6mask, ) - return interface + return interface_data def add_link_data( diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index b6dbd57c..24b9495a 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -201,6 +201,6 @@ class IpPrefixes: generation :return: new interface data for the provided node """ - interface = self.gen_interface(node.id, name, mac) - interface.id = node.newifindex() - return interface + interface_data = self.gen_interface(node.id, name, mac) + interface_data.id = node.newifindex() + return interface_data diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 4b8d513b..37a41b81 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -475,13 +475,13 @@ class CoreNodeBase(NodeBase): raise NotImplementedError def newnetif( - self, net: "CoreNetworkBase", interface: InterfaceData + self, net: "CoreNetworkBase", interface_data: InterfaceData ) -> CoreInterface: """ Create a new network interface. :param net: network to associate with - :param interface: interface data for new interface + :param interface_data: interface data for new interface :return: interface index """ raise NotImplementedError @@ -860,34 +860,34 @@ class CoreNode(CoreNodeBase): self.node_net_client.device_up(interface_name) def newnetif( - self, net: "CoreNetworkBase", interface: InterfaceData + self, net: "CoreNetworkBase", interface_data: InterfaceData ) -> CoreInterface: """ Create a new network interface. :param net: network to associate with - :param interface: interface data for new interface + :param interface_data: interface data for new interface :return: interface index """ - addresses = interface.get_addresses() + addresses = interface_data.get_addresses() with self.lock: # TODO: emane specific code if net.is_emane is True: - ifindex = self.newtuntap(interface.id, interface.name) + ifindex = self.newtuntap(interface_data.id, interface_data.name) # TUN/TAP is not ready for addressing yet; the device may # take some time to appear, and installing it into a # namespace after it has been bound removes addressing; # save addresses with the interface now self.attachnet(ifindex, net) netif = self.netif(ifindex) - netif.sethwaddr(interface.mac) + netif.sethwaddr(interface_data.mac) for address in addresses: netif.addaddr(address) else: - ifindex = self.newveth(interface.id, interface.name) + ifindex = self.newveth(interface_data.id, interface_data.name) self.attachnet(ifindex, net) - if interface.mac: - self.sethwaddr(ifindex, interface.mac) + if interface_data.mac: + self.sethwaddr(ifindex, interface_data.mac) for address in addresses: self.addaddr(ifindex, address) self.ifup(ifindex) diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 6faa7824..a72ff128 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -157,25 +157,27 @@ class PhysicalNode(CoreNodeBase): self.ifindex += 1 return ifindex - def newnetif(self, net: CoreNetworkBase, interface: InterfaceData) -> CoreInterface: + def newnetif( + self, net: CoreNetworkBase, interface_data: InterfaceData + ) -> CoreInterface: logging.info("creating interface") - addresses = interface.get_addresses() - ifindex = interface.id + addresses = interface_data.get_addresses() + ifindex = interface_data.id if ifindex is None: ifindex = self.newifindex() - name = interface.name + name = interface_data.name if name is None: name = f"gt{ifindex}" if self.up: # this is reached when this node is linked to a network node # tunnel to net not built yet, so build it now and adopt it _, remote_tap = self.session.distributed.create_gre_tunnel(net, self.server) - self.adoptnetif(remote_tap, ifindex, interface.mac, addresses) + self.adoptnetif(remote_tap, ifindex, interface_data.mac, addresses) return remote_tap else: # this is reached when configuring services (self.up=False) netif = GreTap(node=self, name=name, session=self.session, start=False) - self.adoptnetif(netif, ifindex, interface.mac, addresses) + self.adoptnetif(netif, ifindex, interface_data.mac, addresses) return netif def privatedir(self, path: str) -> None: @@ -297,19 +299,21 @@ class Rj45Node(CoreNodeBase): self.up = False self.restorestate() - def newnetif(self, net: CoreNetworkBase, interface: InterfaceData) -> CoreInterface: + def newnetif( + self, net: CoreNetworkBase, interface_data: InterfaceData + ) -> CoreInterface: """ This is called when linking with another node. Since this node represents an interface, we do not create another object here, but attach ourselves to the given network. :param net: new network instance - :param interface: interface data for new interface + :param interface_data: interface data for new interface :return: interface index :raises ValueError: when an interface has already been created, one max """ with self.lock: - ifindex = interface.id + ifindex = interface_data.id if ifindex is None: ifindex = 0 if self.interface.net is not None: @@ -318,7 +322,7 @@ class Rj45Node(CoreNodeBase): self.ifindex = ifindex if net is not None: self.interface.attachnet(net) - for addr in interface.get_addresses(): + for addr in interface_data.get_addresses(): self.addaddr(addr) return self.interface From 23d957679e5bf121607fff392c3213ce8b0637b3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 12 Jun 2020 20:22:51 -0700 Subject: [PATCH 0346/1131] daemon: Session cleanup, removed unused functions, used context managers for writing files, made variables used externally no longer private --- daemon/core/api/grpc/server.py | 4 +- daemon/core/api/tlv/corehandlers.py | 12 +- daemon/core/emane/emanemanager.py | 2 +- daemon/core/emulator/session.py | 277 +++++++-------------------- daemon/core/nodes/base.py | 2 +- daemon/core/plugins/sdt.py | 2 +- daemon/core/services/coreservices.py | 8 +- daemon/core/xml/corexml.py | 4 +- daemon/tests/test_grpc.py | 2 +- daemon/tests/test_gui.py | 4 +- daemon/tests/test_xml.py | 2 +- 11 files changed, 99 insertions(+), 220 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index a0ddf806..ca9e0133 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -930,8 +930,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("get hooks: %s", request) session = self.get_session(request.session_id, context) hooks = [] - for state in session._hooks: - state_hooks = session._hooks[state] + for state in session.hooks: + state_hooks = session.hooks[state] for file_name, file_data in state_hooks: hook = core_pb2.Hook(state=state.value, file=file_name, data=file_data) hooks.append(hook) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index e7a67b3e..33222cf3 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -12,6 +12,7 @@ import threading import time from itertools import repeat from queue import Empty, Queue +from typing import Optional from core import utils from core.api.tlv import coreapi, dataconversion, structutils @@ -39,6 +40,7 @@ from core.emulator.enumerations import ( NodeTypes, RegisterTlvs, ) +from core.emulator.session import Session from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel from core.nodes.base import CoreNode, CoreNodeBase, NodeBase @@ -83,7 +85,7 @@ class CoreHandler(socketserver.BaseRequestHandler): thread.start() self.handler_threads.append(thread) - self.session = None + self.session: Optional[Session] = None self.coreemu = server.coreemu utils.close_onexec(request.fileno()) socketserver.BaseRequestHandler.__init__(self, request, client_address, server) @@ -176,7 +178,7 @@ class CoreHandler(socketserver.BaseRequestHandler): node_count_list.append(str(session.get_node_count())) - date_list.append(time.ctime(session._state_time)) + date_list.append(time.ctime(session.state_time)) thumb = session.thumbnail if not thumb: @@ -1819,7 +1821,7 @@ class CoreHandler(socketserver.BaseRequestHandler): """ # find all nodes and links links_data = [] - with self.session._nodes_lock: + with self.session.nodes_lock: for node_id in self.session.nodes: node = self.session.nodes[node_id] self.session.broadcast_node(node, MessageFlags.ADD) @@ -1897,8 +1899,8 @@ class CoreHandler(socketserver.BaseRequestHandler): # TODO: send location info # send hook scripts - for state in sorted(self.session._hooks.keys()): - for file_name, config_data in self.session._hooks[state]: + for state in sorted(self.session.hooks.keys()): + for file_name, config_data in self.session.hooks[state]: file_data = FileData( message_type=MessageFlags.ADD, name=str(file_name), diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 146d186f..cb978cb9 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -279,7 +279,7 @@ class EmaneManager(ModelManager): logging.debug("emane setup") # TODO: drive this from the session object - with self.session._nodes_lock: + with self.session.nodes_lock: for node_id in self.session.nodes: node = self.session.nodes[node_id] if isinstance(node, EmaneNet): diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 0a90b943..f3506048 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -6,7 +6,6 @@ that manages a CORE session. import logging import os import pwd -import random import shutil import subprocess import tempfile @@ -113,15 +112,13 @@ class Session: # dict of nodes: all nodes and nets self.nodes: Dict[int, NodeBase] = {} - self._nodes_lock = threading.Lock() + self.nodes_lock = threading.Lock() + # states and hooks handlers self.state: EventTypes = EventTypes.DEFINITION_STATE - self._state_time: float = time.monotonic() - self._state_file: str = os.path.join(self.session_dir, "state") - - # hooks handlers - self._hooks: Dict[EventTypes, Tuple[str, str]] = {} - self._state_hooks: Dict[EventTypes, Callable[[int], None]] = {} + self.state_time: float = time.monotonic() + self.hooks: Dict[EventTypes, Tuple[str, str]] = {} + self.state_hooks: Dict[EventTypes, List[Callable[[EventTypes], None]]] = {} self.add_state_hook( state=EventTypes.RUNTIME_STATE, hook=self.runtime_state_hook ) @@ -154,15 +151,6 @@ class Session: self.emane: EmaneManager = EmaneManager(self) self.sdt: Sdt = Sdt(self) - # initialize default node services - self.services.default_services = { - "mdr": ("zebra", "OSPFv3MDR", "IPForward"), - "PC": ("DefaultRoute",), - "prouter": (), - "router": ("zebra", "OSPFv2", "OSPFv3", "IPForward"), - "host": ("DefaultRoute", "SSH"), - } - # config services self.service_manager: Optional[ConfigServiceManager] = None @@ -473,7 +461,7 @@ class Session: f"cannot update link node1({type(node1)}) node2({type(node2)})" ) - def _next_node_id(self) -> int: + def next_node_id(self) -> int: """ Find the next valid node id, starting from 1. @@ -506,7 +494,7 @@ class Session: # determine node id if not _id: - _id = self._next_node_id() + _id = self.next_node_id() # generate name if not provided if not options: @@ -692,7 +680,7 @@ class Session: "setting state hook: %s - %s source(%s)", state, file_name, source_name ) hook = file_name, data - state_hooks = self._hooks.setdefault(state, []) + state_hooks = self.hooks.setdefault(state, []) state_hooks.append(hook) # immediately run a hook if it is in the current state @@ -727,7 +715,7 @@ class Session: self.emane.shutdown() self.delete_nodes() self.distributed.shutdown() - self.del_hooks() + self.hooks.clear() self.emane.reset() self.emane.config_reset() self.location.reset() @@ -795,7 +783,6 @@ class Session: :param event_data: event data to send out :return: nothing """ - for handler in self.event_handlers: handler(event_data) @@ -806,7 +793,6 @@ class Session: :param exception_data: exception data to send out :return: nothing """ - for handler in self.exception_handlers: handler(exception_data) @@ -837,7 +823,6 @@ class Session: :param file_data: file data to send out :return: nothing """ - for handler in self.file_handlers: handler(file_data) @@ -848,7 +833,6 @@ class Session: :param config_data: config data to send out :return: nothing """ - for handler in self.config_handlers: handler(config_data) @@ -859,7 +843,6 @@ class Session: :param link_data: link data to send out :return: nothing """ - for handler in self.link_handlers: handler(link_data) @@ -871,22 +854,14 @@ class Session: :param send_event: if true, generate core API event messages :return: nothing """ - state_name = state.name if self.state == state: - logging.info( - "session(%s) is already in state: %s, skipping change", - self.id, - state_name, - ) return - self.state = state - self._state_time = time.monotonic() - logging.info("changing session(%s) to state %s", self.id, state_name) + self.state_time = time.monotonic() + logging.info("changing session(%s) to state %s", self.id, state.name) self.write_state(state) self.run_hooks(state) self.run_state_hooks(state) - if send_event: event_data = EventData(event_type=state, time=str(time.monotonic())) self.broadcast_event(event_data) @@ -898,10 +873,10 @@ class Session: :param state: state to write to file :return: nothing """ + state_file = os.path.join(self.session_dir, "state") try: - state_file = open(self._state_file, "w") - state_file.write(f"{state.value} {state.name}\n") - state_file.close() + with open(state_file, "w") as f: + f.write(f"{state.value} {state.name}\n") except IOError: logging.exception("error writing state file: %s", state.name) @@ -913,61 +888,10 @@ class Session: :param state: state to run hooks for :return: nothing """ - - # check that state change hooks exist - if state not in self._hooks: - return - - # retrieve all state hooks - hooks = self._hooks.get(state, []) - - # execute all state hooks - if hooks: - for hook in hooks: - self.run_hook(hook) - else: - logging.info("no state hooks for %s", state) - - def set_hook( - self, hook_type: str, file_name: str, source_name: str, data: str - ) -> None: - """ - Store a hook from a received file message. - - :param hook_type: hook type - :param file_name: file name for hook - :param source_name: source name - :param data: hook data - :return: nothing - """ - logging.info( - "setting state hook: %s - %s from %s", hook_type, file_name, source_name - ) - - _hook_id, state = hook_type.split(":")[:2] - if not state.isdigit(): - logging.error("error setting hook having state '%s'", state) - return - - state = int(state) - hook = file_name, data - - # append hook to current state hooks - state_hooks = self._hooks.setdefault(state, []) - state_hooks.append(hook) - - # immediately run a hook if it is in the current state - # (this allows hooks in the definition and configuration states) - if self.state == state: - logging.info("immediately running new state hook") + hooks = self.hooks.get(state, []) + for hook in hooks: self.run_hook(hook) - def del_hooks(self) -> None: - """ - Clear the hook scripts dict. - """ - self._hooks.clear() - def run_hook(self, hook: Tuple[str, str]) -> None: """ Run a hook. @@ -977,37 +901,23 @@ class Session: """ file_name, data = hook logging.info("running hook %s", file_name) - - # write data to hook file + file_path = os.path.join(self.session_dir, file_name) + log_path = os.path.join(self.session_dir, f"{file_name}.log") try: - hook_file = open(os.path.join(self.session_dir, file_name), "w") - hook_file.write(data) - hook_file.close() - except IOError: - logging.exception("error writing hook '%s'", file_name) - - # setup hook stdout and stderr - try: - stdout = open(os.path.join(self.session_dir, file_name + ".log"), "w") - stderr = subprocess.STDOUT - except IOError: - logging.exception("error setting up hook stderr and stdout") - stdout = None - stderr = None - - # execute hook file - try: - args = ["/bin/sh", file_name] - subprocess.check_call( - args, - stdout=stdout, - stderr=stderr, - close_fds=True, - cwd=self.session_dir, - env=self.get_environment(), - ) - except (OSError, subprocess.CalledProcessError): - logging.exception("error running hook: %s", file_name) + with open(file_path, "w") as f: + f.write(data) + with open(log_path, "w") as f: + args = ["/bin/sh", file_name] + subprocess.check_call( + args, + stdout=f, + stderr=subprocess.STDOUT, + close_fds=True, + cwd=self.session_dir, + env=self.get_environment(), + ) + except (IOError, subprocess.CalledProcessError): + logging.exception("error running hook: %s", file_path) def run_state_hooks(self, state: EventTypes) -> None: """ @@ -1016,17 +926,16 @@ class Session: :param state: state to run hooks for :return: nothing """ - for hook in self._state_hooks.get(state, []): - try: - hook(state) - except Exception: - message = ( - f"exception occured when running {state.name} state hook: {hook}" - ) - logging.exception(message) - self.exception( - ExceptionLevels.ERROR, "Session.run_state_hooks", message - ) + for hook in self.state_hooks.get(state, []): + self.run_state_hook(state, hook) + + def run_state_hook(self, state: EventTypes, hook: Callable[[EventTypes], None]): + try: + hook(state) + except Exception: + message = f"exception occurred when running {state.name} state hook: {hook}" + logging.exception(message) + self.exception(ExceptionLevels.ERROR, "Session.run_state_hooks", message) def add_state_hook( self, state: EventTypes, hook: Callable[[EventTypes], None] @@ -1038,15 +947,16 @@ class Session: :param hook: hook callback for the state :return: nothing """ - hooks = self._state_hooks.setdefault(state, []) + hooks = self.state_hooks.setdefault(state, []) if hook in hooks: raise CoreError("attempting to add duplicate state hook") hooks.append(hook) - if self.state == state: - hook(state) + self.run_state_hook(state, hook) - def del_state_hook(self, state: int, hook: Callable[[int], None]) -> None: + def del_state_hook( + self, state: EventTypes, hook: Callable[[EventTypes], None] + ) -> None: """ Delete a state hook. @@ -1054,24 +964,23 @@ class Session: :param hook: hook to delete :return: nothing """ - hooks = self._state_hooks.setdefault(state, []) - hooks.remove(hook) + hooks = self.state_hooks.get(state, []) + if hook in hooks: + hooks.remove(hook) - def runtime_state_hook(self, state: EventTypes) -> None: + def runtime_state_hook(self, _state: EventTypes) -> None: """ Runtime state hook check. - :param state: state to check + :param _state: state to check :return: nothing """ - if state == EventTypes.RUNTIME_STATE: - self.emane.poststartup() - - # create session deployed xml - xml_file_name = os.path.join(self.session_dir, "session-deployed.xml") - xml_writer = corexml.CoreXmlWriter(self) - corexmldeployment.CoreXmlDeployment(self, xml_writer.scenario) - xml_writer.write(xml_file_name) + self.emane.poststartup() + # create session deployed xml + xml_file_name = os.path.join(self.session_dir, "session-deployed.xml") + xml_writer = corexml.CoreXmlWriter(self) + corexmldeployment.CoreXmlDeployment(self, xml_writer.scenario) + xml_writer.write(xml_file_name) def get_environment(self, state: bool = True) -> Dict[str, str]: """ @@ -1090,10 +999,8 @@ class Session: env["SESSION_FILENAME"] = str(self.file_name) env["SESSION_USER"] = str(self.user) env["SESSION_NODE_COUNT"] = str(self.get_node_count()) - 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: @@ -1104,7 +1011,6 @@ class Session: "environment configuration file does not exist: %s", environment_config_file, ) - # attempt to read and add user environment file if self.user: environment_user_file = os.path.join( @@ -1117,7 +1023,6 @@ class Session: "user core environment settings file not present: %s", environment_user_file, ) - return env def set_thumbnail(self, thumb_file: str) -> None: @@ -1131,7 +1036,6 @@ class Session: logging.error("thumbnail file to set does not exist: %s", thumb_file) self.thumbnail = None return - destination_file = os.path.join(self.session_dir, os.path.basename(thumb_file)) shutil.copy(thumb_file, destination_file) self.thumbnail = destination_file @@ -1151,20 +1055,8 @@ class Session: os.chown(self.session_dir, uid, gid) except IOError: logging.exception("failed to set permission on %s", self.session_dir) - self.user = user - def get_node_id(self) -> int: - """ - Return a unique, new node id. - """ - with self._nodes_lock: - while True: - node_id = random.randint(1, 0xFFFF) - if node_id not in self.nodes: - break - return node_id - def create_node(self, _class: Type[NT], *args: Any, **kwargs: Any) -> NT: """ Create an emulation node. @@ -1176,7 +1068,7 @@ class Session: :raises core.CoreError: when id of the node to create already exists """ node = _class(self, *args, **kwargs) - with self._nodes_lock: + with self.nodes_lock: if node.id in self.nodes: node.shutdown() raise CoreError(f"duplicate node id {node.id} for {node.name}") @@ -1192,9 +1084,9 @@ class Session: :return: node for the given id :raises core.CoreError: when node does not exist """ - if _id not in self.nodes: + node = self.nodes.get(_id) + if node is None: raise CoreError(f"unknown node id {_id}") - node = self.nodes[_id] if not isinstance(node, _class): actual = node.__class__.__name__ expected = _class.__name__ @@ -1210,7 +1102,7 @@ class Session: """ # delete node and check for session shutdown if a node was removed node = None - with self._nodes_lock: + with self.nodes_lock: if _id in self.nodes: node = self.nodes.pop(_id) logging.info("deleted node(%s)", node.name) @@ -1224,7 +1116,7 @@ class Session: """ Clear the nodes dictionary, and call shutdown for each node. """ - with self._nodes_lock: + with self.nodes_lock: funcs = [] while self.nodes: _, node = self.nodes.popitem() @@ -1237,29 +1129,15 @@ class Session: Write nodes to a 'nodes' file in the session dir. The 'nodes' file lists: number, name, api-type, class-type """ + file_path = os.path.join(self.session_dir, "nodes") try: - with self._nodes_lock: - file_path = os.path.join(self.session_dir, "nodes") + with self.nodes_lock: with open(file_path, "w") as f: - for _id in self.nodes.keys(): - node = self.nodes[_id] + for _id, node in self.nodes.items(): f.write(f"{_id} {node.name} {node.apitype} {type(node)}\n") except IOError: logging.exception("error writing nodes file") - def dump_session(self) -> None: - """ - Log information about the session in its current state. - """ - logging.info("session id=%s name=%s state=%s", self.id, self.name, self.state) - logging.info( - "file=%s thumbnail=%s node_count=%s/%s", - self.file_name, - self.thumbnail, - self.get_node_count(), - len(self.nodes), - ) - def exception( self, level: ExceptionLevels, source: str, text: str, node_id: int = None ) -> None: @@ -1327,17 +1205,15 @@ class Session: :return: created node count """ - with self._nodes_lock: + with self.nodes_lock: count = 0 - for node_id in self.nodes: - node = self.nodes[node_id] + for node in self.nodes.values(): is_p2p_ctrlnet = isinstance(node, (PtpNet, CtrlNet)) is_tap = isinstance(node, GreTapBridge) and not isinstance( node, TunnelNode ) if is_p2p_ctrlnet or is_tap: continue - count += 1 return count @@ -1359,7 +1235,6 @@ class Session: if self.state == EventTypes.RUNTIME_STATE: logging.info("valid runtime state found, returning") return - # start event loop and set to runtime self.event_loop.run() self.set_state(EventTypes.RUNTIME_STATE, send_event=True) @@ -1375,7 +1250,7 @@ class Session: self.event_loop.stop() # stop node services - with self._nodes_lock: + with self.nodes_lock: funcs = [] for node_id in self.nodes: node = self.nodes[node_id] @@ -1447,7 +1322,7 @@ class Session: :return: service boot exceptions """ - with self._nodes_lock: + with self.nodes_lock: funcs = [] start = time.monotonic() for _id in self.nodes: @@ -1545,7 +1420,6 @@ class Session: else: prefix_spec = CtrlNet.DEFAULT_PREFIX_LIST[net_index] logging.debug("prefix spec: %s", prefix_spec) - server_interface = self.get_control_net_server_interfaces()[net_index] # return any existing controlnet bridge @@ -1685,7 +1559,7 @@ class Session: if not in runtime. """ if self.state == EventTypes.RUNTIME_STATE: - return time.monotonic() - self._state_time + return time.monotonic() - self.state_time else: return 0.0 @@ -1708,7 +1582,6 @@ class Session: """ event_time = float(event_time) current_time = self.runtime() - if current_time > 0: if event_time <= current_time: logging.warning( @@ -1718,11 +1591,9 @@ class Session: ) return event_time = event_time - current_time - self.event_loop.add_event( event_time, self.run_event, node=node, name=name, data=data ) - if not name: name = "" logging.info( @@ -1732,8 +1603,6 @@ class Session: data, ) - # TODO: if data is None, this blows up, but this ties into how event functions - # are ran, need to clean that up def run_event( self, node_id: int = None, name: str = None, data: str = None ) -> None: @@ -1745,10 +1614,12 @@ class Session: :param data: event data :return: nothing """ + if data is None: + logging.warning("no data for event node(%s) name(%s)", node_id, name) + return now = self.runtime() if not name: name = "" - logging.info("running event %s at time %s cmd=%s", name, now, data) if not node_id: utils.mute_detach(data) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 37a41b81..66ea41a0 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -63,7 +63,7 @@ class NodeBase: self.session: "Session" = session if _id is None: - _id = session.get_node_id() + _id = session.next_node_id() self.id: int = _id if name is None: name = f"o{self.id}" diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 062217cb..04fff3e4 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -215,7 +215,7 @@ class Sdt: for layer in CORE_LAYERS: self.cmd(f"layer {layer}") - with self.session._nodes_lock: + with self.session.nodes_lock: for node_id in self.session.nodes: node = self.session.nodes[node_id] if isinstance(node, CoreNetworkBase): diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index 491113ff..391b53d1 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -325,7 +325,13 @@ class CoreServices: """ self.session = session # dict of default services tuples, key is node type - self.default_services = {} + self.default_services = { + "mdr": ("zebra", "OSPFv3MDR", "IPForward"), + "PC": ("DefaultRoute",), + "prouter": (), + "router": ("zebra", "OSPFv2", "OSPFv3", "IPForward"), + "host": ("DefaultRoute", "SSH"), + } # dict of node ids to dict of custom services by name self.custom_services = {} diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index afc1d826..45e8d9c5 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -320,8 +320,8 @@ class CoreXmlWriter: def write_session_hooks(self) -> None: # hook scripts hooks = etree.Element("session_hooks") - for state in sorted(self.session._hooks, key=lambda x: x.value): - for file_name, data in self.session._hooks[state]: + for state in sorted(self.session.hooks, key=lambda x: x.value): + for file_name, data in self.session.hooks[state]: hook = etree.SubElement(hooks, "hook") add_attribute(hook, "name", file_name) add_attribute(hook, "state", state.value) diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 131af93d..8beb4b9a 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -133,7 +133,7 @@ class TestGrpc: assert wlan_node.id in session.nodes assert session.nodes[node1.id].netif(0) is not None assert session.nodes[node2.id].netif(0) is not None - hook_file, hook_data = session._hooks[EventTypes.RUNTIME_STATE][0] + hook_file, hook_data = session.hooks[EventTypes.RUNTIME_STATE][0] assert hook_file == hook.file assert hook_data == hook.data assert session.location.refxyz == (location_x, location_y, location_z) diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index 1187b4d7..d3b9362d 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -382,7 +382,7 @@ class TestGui: def test_file_hook_add(self, coretlv: CoreHandler): state = EventTypes.DATACOLLECT_STATE - assert coretlv.session._hooks.get(state) is None + assert coretlv.session.hooks.get(state) is None file_name = "test.sh" file_data = "echo hello" message = coreapi.CoreFileMessage.create( @@ -396,7 +396,7 @@ class TestGui: coretlv.handle_message(message) - hooks = coretlv.session._hooks.get(state) + hooks = coretlv.session.hooks.get(state) assert len(hooks) == 1 name, data = hooks[0] assert file_name == name diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index 35e03f7d..b28e0986 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -48,7 +48,7 @@ class TestXml: session.open_xml(file_path, start=True) # verify nodes have been recreated - runtime_hooks = session._hooks.get(state) + runtime_hooks = session.hooks.get(state) assert runtime_hooks runtime_hook = runtime_hooks[0] assert file_name == runtime_hook[0] From e18ffaafce70b5c467d03ad11c6777e17197097c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 13 Jun 2020 17:41:13 -0700 Subject: [PATCH 0347/1131] daemon: xml files will now write and read loss, but fallback to looking for per for compatibility --- daemon/core/xml/corexml.py | 42 ++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 820f1cea..2b15aa5a 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -569,7 +569,7 @@ class CoreXmlWriter: options = etree.Element("options") add_attribute(options, "delay", link_data.delay) add_attribute(options, "bandwidth", link_data.bandwidth) - add_attribute(options, "per", link_data.loss) + add_attribute(options, "loss", link_data.loss) add_attribute(options, "dup", link_data.dup) add_attribute(options, "jitter", link_data.jitter) add_attribute(options, "mer", link_data.mer) @@ -947,37 +947,39 @@ class CoreXmlReader: interface_two = create_interface_data(interface_two_element) options_element = link_element.find("options") - link_options = LinkOptions() + options = LinkOptions() if options_element is not None: - link_options.bandwidth = get_int(options_element, "bandwidth") - link_options.burst = get_int(options_element, "burst") - link_options.delay = get_int(options_element, "delay") - link_options.dup = get_int(options_element, "dup") - link_options.mer = get_int(options_element, "mer") - link_options.mburst = get_int(options_element, "mburst") - link_options.jitter = get_int(options_element, "jitter") - link_options.key = get_int(options_element, "key") - link_options.loss = get_float(options_element, "per") - link_options.unidirectional = get_int(options_element, "unidirectional") - link_options.session = options_element.get("session") - link_options.emulation_id = get_int(options_element, "emulation_id") - link_options.network_id = get_int(options_element, "network_id") - link_options.opaque = options_element.get("opaque") - link_options.gui_attributes = options_element.get("gui_attributes") + options.bandwidth = get_int(options_element, "bandwidth") + options.burst = get_int(options_element, "burst") + options.delay = get_int(options_element, "delay") + options.dup = get_int(options_element, "dup") + options.mer = get_int(options_element, "mer") + options.mburst = get_int(options_element, "mburst") + options.jitter = get_int(options_element, "jitter") + options.key = get_int(options_element, "key") + options.loss = get_float(options_element, "loss") + if options.loss is None: + options.loss = get_float(options_element, "per") + options.unidirectional = get_int(options_element, "unidirectional") + options.session = options_element.get("session") + options.emulation_id = get_int(options_element, "emulation_id") + options.network_id = get_int(options_element, "network_id") + options.opaque = options_element.get("opaque") + options.gui_attributes = options_element.get("gui_attributes") - if link_options.unidirectional == 1 and node_set in node_sets: + if options.unidirectional == 1 and node_set in node_sets: logging.info( "updating link node_one(%s) node_two(%s)", node_one, node_two ) self.session.update_link( - node_one, node_two, interface_one.id, interface_two.id, link_options + node_one, node_two, interface_one.id, interface_two.id, options ) else: logging.info( "adding link node_one(%s) node_two(%s)", node_one, node_two ) self.session.add_link( - node_one, node_two, interface_one, interface_two, link_options + node_one, node_two, interface_one, interface_two, options ) node_sets.add(node_set) From 5df2e36083acaec721d3ef6b78fe2110c6d411e2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 13 Jun 2020 21:48:51 -0700 Subject: [PATCH 0348/1131] daemon: fixed session.add_event parameter to be specific to node_id --- daemon/core/api/tlv/corehandlers.py | 32 ++++++++++++++--------------- daemon/core/emulator/session.py | 11 +++------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 562f8c89..2cd7bfac 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -809,38 +809,38 @@ class CoreHandler(socketserver.BaseRequestHandler): :param core.api.tlv.coreapi.CoreExecMessage message: execute message to handle :return: reply messages """ - node_num = message.get_tlv(ExecuteTlvs.NODE.value) + node_id = message.get_tlv(ExecuteTlvs.NODE.value) execute_num = message.get_tlv(ExecuteTlvs.NUMBER.value) execute_time = message.get_tlv(ExecuteTlvs.TIME.value) command = message.get_tlv(ExecuteTlvs.COMMAND.value) # local flag indicates command executed locally, not on a node - if node_num is None and not message.flags & MessageFlags.LOCAL.value: + if node_id is None and not message.flags & MessageFlags.LOCAL.value: raise ValueError("Execute Message is missing node number.") if execute_num is None: raise ValueError("Execute Message is missing execution number.") if execute_time is not None: - self.session.add_event(execute_time, node=node_num, name=None, data=command) + self.session.add_event( + float(execute_time), node_id=node_id, name=None, data=command + ) return () try: - node = self.session.get_node(node_num, CoreNodeBase) + node = self.session.get_node(node_id, CoreNodeBase) # build common TLV items for reply tlv_data = b"" - if node_num is not None: - tlv_data += coreapi.CoreExecuteTlv.pack( - ExecuteTlvs.NODE.value, node_num - ) + if node_id is not None: + tlv_data += coreapi.CoreExecuteTlv.pack(ExecuteTlvs.NODE.value, node_id) tlv_data += coreapi.CoreExecuteTlv.pack( ExecuteTlvs.NUMBER.value, execute_num ) tlv_data += coreapi.CoreExecuteTlv.pack(ExecuteTlvs.COMMAND.value, command) if message.flags & MessageFlags.TTY.value: - if node_num is None: + if node_id is None: raise NotImplementedError # echo back exec message with cmd for spawning interactive terminal if command == "bash": @@ -850,7 +850,6 @@ class CoreHandler(socketserver.BaseRequestHandler): reply = coreapi.CoreExecMessage.pack(MessageFlags.TTY.value, tlv_data) return (reply,) else: - logging.info("execute message with cmd=%s", command) # execute command and send a response if ( message.flags & MessageFlags.STRING.value @@ -870,7 +869,6 @@ class CoreHandler(socketserver.BaseRequestHandler): except CoreCommandError as e: res = e.stderr status = e.returncode - logging.info("done exec cmd=%s with status=%d", command, status) if message.flags & MessageFlags.TEXT.value: tlv_data += coreapi.CoreExecuteTlv.pack( ExecuteTlvs.RESULT.value, res @@ -888,7 +886,7 @@ class CoreHandler(socketserver.BaseRequestHandler): else: node.cmd(command, wait=False) except CoreError: - logging.exception("error getting object: %s", node_num) + logging.exception("error getting object: %s", node_id) # XXX wait and queue this message to try again later # XXX maybe this should be done differently if not message.flags & MessageFlags.LOCAL.value: @@ -1549,11 +1547,11 @@ class CoreHandler(socketserver.BaseRequestHandler): if event_type == EventTypes.INSTANTIATION_STATE and isinstance( node, WlanNode ): - self.session.start_mobility(node_ids=(node.id,)) + self.session.start_mobility(node_ids=[node.id]) return () logging.warning( - "dropping unhandled event message for node: %s", node_id + "dropping unhandled event message for node: %s", node.name ) return () self.session.set_state(event_type) @@ -1611,14 +1609,16 @@ class CoreHandler(socketserver.BaseRequestHandler): self.session.save_xml(filename) elif event_type == EventTypes.SCHEDULED: etime = event_data.time - node = event_data.node + node_id = event_data.node name = event_data.name data = event_data.data if etime is None: logging.warning("Event message scheduled event missing start time") return () if message.flags & MessageFlags.ADD.value: - self.session.add_event(float(etime), node=node, name=name, data=data) + self.session.add_event( + float(etime), node_id=node_id, name=name, data=data + ) else: raise NotImplementedError diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index f3506048..2225bb6f 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -1564,23 +1564,18 @@ class Session: return 0.0 def add_event( - self, - event_time: float, - node: CoreNode = None, - name: str = None, - data: str = None, + self, event_time: float, node_id: int = None, name: str = None, data: str = None ) -> None: """ Add an event to the event queue, with a start time relative to the start of the runtime state. :param event_time: event time - :param node: node to add event for + :param node_id: node to add event for :param name: name of event :param data: data for event :return: nothing """ - event_time = float(event_time) current_time = self.runtime() if current_time > 0: if event_time <= current_time: @@ -1592,7 +1587,7 @@ class Session: return event_time = event_time - current_time self.event_loop.add_event( - event_time, self.run_event, node=node, name=name, data=data + event_time, self.run_event, node_id=node_id, name=name, data=data ) if not name: name = "" From 8d48393525094387f7598d192e21a68cbdabfae0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 13 Jun 2020 21:53:09 -0700 Subject: [PATCH 0349/1131] daemon: updated usage of if1/2 to be consistent with interface1/2 for now --- daemon/core/nodes/network.py | 54 +++++++++++++++++------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 8ac1939e..9b46dfd5 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -898,16 +898,16 @@ class PtpNet(CoreNetwork): if len(self._netif) != 2: return all_links - if1, if2 = self._netif.values() + interface1, interface2 = self._netif.values() unidirectional = 0 - if if1.getparams() != if2.getparams(): + if interface1.getparams() != interface2.getparams(): unidirectional = 1 interface1_ip4 = None interface1_ip4_mask = None interface1_ip6 = None interface1_ip6_mask = None - for address in if1.addrlist: + for address in interface1.addrlist: ip, _sep, mask = address.partition("/") mask = int(mask) if netaddr.valid_ipv4(ip): @@ -921,7 +921,7 @@ class PtpNet(CoreNetwork): interface2_ip4_mask = None interface2_ip6 = None interface2_ip6_mask = None - for address in if2.addrlist: + for address in interface2.addrlist: ip, _sep, mask = address.partition("/") mask = int(mask) if netaddr.valid_ipv4(ip): @@ -933,31 +933,30 @@ class PtpNet(CoreNetwork): link_data = LinkData( message_type=flags, - node1_id=if1.node.id, - node2_id=if2.node.id, + node1_id=interface1.node.id, + node2_id=interface2.node.id, link_type=self.linktype, unidirectional=unidirectional, - delay=if1.getparam("delay"), - bandwidth=if1.getparam("bw"), - loss=if1.getparam("loss"), - dup=if1.getparam("duplicate"), - jitter=if1.getparam("jitter"), - interface1_id=if1.node.getifindex(if1), - interface1_name=if1.name, - interface1_mac=if1.hwaddr, + delay=interface1.getparam("delay"), + bandwidth=interface1.getparam("bw"), + loss=interface1.getparam("loss"), + dup=interface1.getparam("duplicate"), + jitter=interface1.getparam("jitter"), + interface1_id=interface1.node.getifindex(interface1), + interface1_name=interface1.name, + interface1_mac=interface1.hwaddr, interface1_ip4=interface1_ip4, interface1_ip4_mask=interface1_ip4_mask, interface1_ip6=interface1_ip6, interface1_ip6_mask=interface1_ip6_mask, - interface2_id=if2.node.getifindex(if2), - interface2_name=if2.name, - interface2_mac=if2.hwaddr, + interface2_id=interface2.node.getifindex(interface2), + interface2_name=interface2.name, + interface2_mac=interface2.hwaddr, interface2_ip4=interface2_ip4, interface2_ip4_mask=interface2_ip4_mask, interface2_ip6=interface2_ip6, interface2_ip6_mask=interface2_ip6_mask, ) - all_links.append(link_data) # build a 2nd link message for the upstream link parameters @@ -966,19 +965,18 @@ class PtpNet(CoreNetwork): link_data = LinkData( message_type=MessageFlags.NONE, link_type=self.linktype, - node1_id=if2.node.id, - node2_id=if1.node.id, - delay=if2.getparam("delay"), - bandwidth=if2.getparam("bw"), - loss=if2.getparam("loss"), - dup=if2.getparam("duplicate"), - jitter=if2.getparam("jitter"), + node1_id=interface2.node.id, + node2_id=interface1.node.id, + delay=interface2.getparam("delay"), + bandwidth=interface2.getparam("bw"), + loss=interface2.getparam("loss"), + dup=interface2.getparam("duplicate"), + jitter=interface2.getparam("jitter"), unidirectional=1, - interface1_id=if2.node.getifindex(if2), - interface2_id=if1.node.getifindex(if1), + interface1_id=interface2.node.getifindex(interface2), + interface2_id=interface1.node.getifindex(interface1), ) all_links.append(link_data) - return all_links From 91f1f7f004dba90735b735753a492b8e193424a8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 13 Jun 2020 22:01:07 -0700 Subject: [PATCH 0350/1131] daemon: added global type hinting to core.emulator.session and core.api.grpc.server --- daemon/core/api/grpc/server.py | 6 +++--- daemon/core/emulator/session.py | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 3374df2e..8b349b67 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -6,7 +6,7 @@ import tempfile import threading import time from concurrent import futures -from typing import Iterable, Optional, Type +from typing import Iterable, Optional, Pattern, Type import grpc from grpc import ServicerContext @@ -118,8 +118,8 @@ from core.nodes.base import CoreNode, CoreNodeBase, NodeBase from core.nodes.network import WlanNode from core.services.coreservices import ServiceManager -_ONE_DAY_IN_SECONDS = 60 * 60 * 24 -_INTERFACE_REGEX = re.compile(r"veth(?P[0-9a-fA-F]+)") +_ONE_DAY_IN_SECONDS: int = 60 * 60 * 24 +_INTERFACE_REGEX: Pattern = re.compile(r"veth(?P[0-9a-fA-F]+)") class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 2225bb6f..53f5156d 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -11,7 +11,7 @@ import subprocess import tempfile import threading import time -from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, TypeVar from core import constants, utils from core.configservice.manager import ConfigServiceManager @@ -59,7 +59,7 @@ from core.xml import corexml, corexmldeployment from core.xml.corexml import CoreXmlReader, CoreXmlWriter # maps for converting from API call node type values to classes and vice versa -NODES = { +NODES: Dict[NodeTypes, Type[NodeBase]] = { NodeTypes.DEFAULT: CoreNode, NodeTypes.PHYSICAL: PhysicalNode, NodeTypes.SWITCH: SwitchNode, @@ -74,11 +74,11 @@ NODES = { NodeTypes.DOCKER: DockerNode, NodeTypes.LXC: LxcNode, } -NODES_TYPE = {NODES[x]: x for x in NODES} -CONTAINER_NODES = {DockerNode, LxcNode} -CTRL_NET_ID = 9001 -LINK_COLORS = ["green", "blue", "orange", "purple", "turquoise"] -NT = TypeVar("NT", bound=NodeBase) +NODES_TYPE: Dict[Type[NodeBase], NodeTypes] = {NODES[x]: x for x in NODES} +CONTAINER_NODES: Set[Type[NodeBase]] = {DockerNode, LxcNode} +CTRL_NET_ID: int = 9001 +LINK_COLORS: List[str] = ["green", "blue", "orange", "purple", "turquoise"] +NT: TypeVar = TypeVar("NT", bound=NodeBase) class Session: From d94bae6b42aabddd92d42a99a1fa7b4a9f4cab6b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 13 Jun 2020 22:25:38 -0700 Subject: [PATCH 0351/1131] daemon: added class variable type hinting to core.services.coreservices --- daemon/core/services/coreservices.py | 83 ++++++++++++++-------------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index 391b53d1..d22bc7a5 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -10,7 +10,7 @@ services. import enum import logging import time -from typing import TYPE_CHECKING, Iterable, List, Tuple, Type +from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple, Type from core import utils from core.constants import which @@ -36,14 +36,15 @@ class ServiceMode(enum.Enum): class ServiceDependencies: """ Can generate boot paths for services, based on their dependencies. Will validate - that all services will be booted and that all dependencies exist within the services provided. + that all services will be booted and that all dependencies exist within the services + provided. """ - def __init__(self, services: List[Type["CoreService"]]) -> None: + def __init__(self, services: List["CoreService"]) -> None: # helpers to check validity - self.dependents = {} - self.booted = set() - self.node_services = {} + self.dependents: Dict[str, Set[str]] = {} + self.booted: Set[str] = set() + self.node_services: Dict[str, "CoreService"] = {} for service in services: self.node_services[service.name] = service for dependency in service.dependencies: @@ -51,9 +52,9 @@ class ServiceDependencies: dependents.add(service.name) # used to find paths - self.path = [] - self.visited = set() - self.visiting = set() + self.path: List["CoreService"] = [] + self.visited: Set[str] = set() + self.visiting: Set[str] = set() def boot_paths(self) -> List[List["CoreService"]]: """ @@ -131,7 +132,7 @@ class ServiceDependencies: class ServiceShim: - keys = [ + keys: List[str] = [ "dirs", "files", "startidx", @@ -241,10 +242,10 @@ class ServiceManager: Manages services available for CORE nodes to use. """ - services = {} + services: Dict[str, Type["CoreService"]] = {} @classmethod - def add(cls, service: "CoreService") -> None: + def add(cls, service: Type["CoreService"]) -> None: """ Add a service to manager. @@ -314,8 +315,8 @@ class CoreServices: custom service configuration. A CoreService is not a Configurable. """ - name = "services" - config_type = RegisterTlvs.UTILITY + name: str = "services" + config_type: RegisterTlvs = RegisterTlvs.UTILITY def __init__(self, session: "Session") -> None: """ @@ -323,17 +324,17 @@ class CoreServices: :param session: session this manager is tied to """ - self.session = session + self.session: "Session" = session # dict of default services tuples, key is node type - self.default_services = { - "mdr": ("zebra", "OSPFv3MDR", "IPForward"), - "PC": ("DefaultRoute",), - "prouter": (), - "router": ("zebra", "OSPFv2", "OSPFv3", "IPForward"), - "host": ("DefaultRoute", "SSH"), + self.default_services: Dict[str, List[str]] = { + "mdr": ["zebra", "OSPFv3MDR", "IPForward"], + "PC": ["DefaultRoute"], + "prouter": [], + "router": ["zebra", "OSPFv2", "OSPFv3", "IPForward"], + "host": ["DefaultRoute", "SSH"], } # dict of node ids to dict of custom services by name - self.custom_services = {} + self.custom_services: Dict[int, Dict[str, "CoreService"]] = {} def reset(self) -> None: """ @@ -425,7 +426,7 @@ class CoreServices: continue node.services.append(service) - def all_configs(self) -> List[Tuple[int, Type["CoreService"]]]: + def all_configs(self) -> List[Tuple[int, "CoreService"]]: """ Return (node_id, service) tuples for all stored configs. Used when reconnecting to a session or opening XML. @@ -808,50 +809,50 @@ class CoreService: """ # service name should not include spaces - name = None + name: Optional[str] = None # executables that must exist for service to run - executables = () + executables: Tuple[str, ...] = () # sets service requirements that must be started prior to this service starting - dependencies = () + dependencies: Tuple[str, ...] = () # group string allows grouping services together - group = None + group: Optional[str] = None # private, per-node directories required by this service - dirs = () + dirs: Tuple[str, ...] = () # config files written by this service - configs = () + configs: Tuple[str, ...] = () # config file data - config_data = {} + config_data: Dict[str, str] = {} # list of startup commands - startup = () + startup: Tuple[str, ...] = () # list of shutdown commands - shutdown = () + shutdown: Tuple[str, ...] = () # list of validate commands - validate = () + validate: Tuple[str, ...] = () # validation mode, used to determine startup success - validation_mode = ServiceMode.NON_BLOCKING + validation_mode: ServiceMode = ServiceMode.NON_BLOCKING # time to wait in seconds for determining if service started successfully - validation_timer = 5 + validation_timer: int = 5 # validation period in seconds, how frequent validation is attempted - validation_period = 0.5 + validation_period: float = 0.5 # metadata associated with this service - meta = None + meta: Optional[str] = None # custom configuration text - custom = False - custom_needed = False + custom: bool = False + custom_needed: bool = False def __init__(self) -> None: """ @@ -859,8 +860,8 @@ class CoreService: against their config. Services are instantiated when a custom configuration is used to override their default parameters. """ - self.custom = True - self.config_data = self.__class__.config_data.copy() + self.custom: bool = True + self.config_data: Dict[str, str] = self.__class__.config_data.copy() @classmethod def on_load(cls) -> None: From 8587da062161130c9ab81b072548c79a7e860b5d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 13 Jun 2020 23:50:08 -0700 Subject: [PATCH 0352/1131] daemon: moved node instantiation into lock to guarantee id uniqueness, removed node count from environment as it also attmpts to use lock and wouldnt be accurate either --- daemon/core/emulator/session.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 53f5156d..826e3f0a 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -998,7 +998,6 @@ class Session: env["SESSION_NAME"] = str(self.name) env["SESSION_FILENAME"] = str(self.file_name) env["SESSION_USER"] = str(self.user) - env["SESSION_NODE_COUNT"] = str(self.get_node_count()) if state: env["SESSION_STATE"] = str(self.state) # attempt to read and add environment config file @@ -1067,8 +1066,8 @@ class Session: :return: the created node instance :raises core.CoreError: when id of the node to create already exists """ - node = _class(self, *args, **kwargs) with self.nodes_lock: + node = _class(self, *args, **kwargs) if node.id in self.nodes: node.shutdown() raise CoreError(f"duplicate node id {node.id} for {node.name}") From 3243a69afa891a23d75fcd149b2cc0d1b98261fb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 14 Jun 2020 00:46:11 -0700 Subject: [PATCH 0353/1131] daemon: updated xml files to use node1 and interface1 instead of node_one and interface_one, will still fallback to parse old names --- daemon/core/xml/corexml.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 831ffae6..759de680 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -45,11 +45,11 @@ def get_type(element: etree.Element, name: str, _type: Generic[T]) -> Optional[T return value -def get_float(element: etree.Element, name: str) -> float: +def get_float(element: etree.Element, name: str) -> Optional[float]: return get_type(element, name, float) -def get_int(element: etree.Element, name: str) -> int: +def get_int(element: etree.Element, name: str) -> Optional[int]: return get_type(element, name, int) @@ -529,13 +529,13 @@ class CoreXmlWriter: def create_link_element(self, link_data: LinkData) -> etree.Element: link_element = etree.Element("link") - add_attribute(link_element, "node_one", link_data.node1_id) - add_attribute(link_element, "node_two", link_data.node2_id) + add_attribute(link_element, "node1", link_data.node1_id) + add_attribute(link_element, "node2", link_data.node2_id) # check for interface one if link_data.interface1_id is not None: interface1 = self.create_interface_element( - "interface_one", + "interface1", link_data.node1_id, link_data.interface1_id, link_data.interface1_mac, @@ -549,7 +549,7 @@ class CoreXmlWriter: # check for interface two if link_data.interface2_id is not None: interface2 = self.create_interface_element( - "interface_two", + "interface2", link_data.node2_id, link_data.interface2_id, link_data.interface2_mac, @@ -932,16 +932,24 @@ class CoreXmlReader: node_sets = set() for link_element in link_elements.iterchildren(): - node1_id = get_int(link_element, "node_one") - node2_id = get_int(link_element, "node_two") + node1_id = get_int(link_element, "node1") + if node1_id is None: + node1_id = get_int(link_element, "node_one") + node2_id = get_int(link_element, "node2") + if node2_id is None: + node2_id = get_int(link_element, "node_two") node_set = frozenset((node1_id, node2_id)) - interface1_element = link_element.find("interface_one") + interface1_element = link_element.find("interface1") + if interface1_element is None: + interface1_element = link_element.find("interface_one") interface1_data = None if interface1_element is not None: interface1_data = create_interface_data(interface1_element) - interface2_element = link_element.find("interface_two") + interface2_element = link_element.find("interface2") + if interface2_element is None: + interface2_element = link_element.find("interface_two") interface2_data = None if interface2_element is not None: interface2_data = create_interface_data(interface2_element) From c4c667bb741fbd2027eedd61c61ddb35c38ca414 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 14 Jun 2020 09:37:58 -0700 Subject: [PATCH 0354/1131] daemon: removed node.startup from inside constructor, session is now responsible, providing more control and avoiding issues when using super calls where you dont want to start just yet --- daemon/core/emane/nodes.py | 3 +-- daemon/core/emulator/session.py | 14 ++++++++++---- daemon/core/nodes/base.py | 26 ++++++++------------------ daemon/core/nodes/docker.py | 4 +--- daemon/core/nodes/lxd.py | 4 +--- daemon/core/nodes/network.py | 20 +++++--------------- daemon/core/nodes/physical.py | 15 ++++----------- 7 files changed, 30 insertions(+), 56 deletions(-) diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index e88cb194..68c1bc05 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -55,10 +55,9 @@ class EmaneNet(CoreNetworkBase): session: "Session", _id: int = None, name: str = None, - start: bool = True, server: DistributedServer = None, ) -> None: - super().__init__(session, _id, name, start, server) + super().__init__(session, _id, name, server) self.conf: str = "" self.nemidmap: Dict[CoreInterface, int] = {} self.model: "OptionalEmaneModel" = None diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 826e3f0a..e63c30c7 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -257,7 +257,7 @@ class Session: if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): logging.info("linking ptp: %s - %s", node1.name, node2.name) start = self.state.should_start() - ptp = self.create_node(PtpNet, start=start) + ptp = self.create_node(PtpNet, start) interface1 = node1.newnetif(ptp, interface1_data) interface2 = node2.newnetif(ptp, interface2_data) ptp.linkconfig(interface1, options) @@ -517,10 +517,10 @@ class Session: name, start, ) - kwargs = dict(_id=_id, name=name, start=start, server=server) + kwargs = dict(_id=_id, name=name, server=server) if _class in CONTAINER_NODES: kwargs["image"] = options.image - node = self.create_node(_class, **kwargs) + node = self.create_node(_class, start, **kwargs) # set node attributes node.icon = options.icon @@ -1056,11 +1056,14 @@ class Session: logging.exception("failed to set permission on %s", self.session_dir) self.user = user - def create_node(self, _class: Type[NT], *args: Any, **kwargs: Any) -> NT: + def create_node( + self, _class: Type[NT], start: bool, *args: Any, **kwargs: Any + ) -> NT: """ Create an emulation node. :param _class: node class to create + :param start: True to start node, False otherwise :param args: list of arguments for the class to create :param kwargs: dictionary of arguments for the class to create :return: the created node instance @@ -1072,6 +1075,8 @@ class Session: node.shutdown() raise CoreError(f"duplicate node id {node.id} for {node.name}") self.nodes[node.id] = node + if start: + node.startup() return node def get_node(self, _id: int, _class: Type[NT]) -> NT: @@ -1464,6 +1469,7 @@ class Session: ) control_net = self.create_node( CtrlNet, + True, prefix, _id=_id, updown_script=updown_script, diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index cd77f857..49fe7620 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -7,7 +7,7 @@ import os import shutil import threading from threading import RLock -from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type import netaddr @@ -47,7 +47,6 @@ class NodeBase: session: "Session", _id: int = None, name: str = None, - start: bool = True, server: "DistributedServer" = None, ) -> None: """ @@ -56,7 +55,6 @@ class NodeBase: :param session: CORE session object :param _id: id :param name: object name - :param start: start value :param server: remote server node will run on, default is None for localhost """ @@ -254,7 +252,6 @@ class CoreNodeBase(NodeBase): session: "Session", _id: int = None, name: str = None, - start: bool = True, server: "DistributedServer" = None, ) -> None: """ @@ -263,11 +260,10 @@ class CoreNodeBase(NodeBase): :param session: CORE session object :param _id: object id :param name: object name - :param start: boolean for starting :param server: remote server node will run on, default is None for localhost """ - super().__init__(session, _id, name, start, server) + super().__init__(session, _id, name, server) self.config_services: Dict[str, "ConfigService"] = {} self.nodedir: Optional[str] = None self.tmpnodedir: bool = False @@ -492,8 +488,8 @@ class CoreNode(CoreNodeBase): Provides standard core node logic. """ - apitype = NodeTypes.DEFAULT - valid_address_types = {"inet", "inet6", "inet6link"} + apitype: NodeTypes = NodeTypes.DEFAULT + valid_address_types: Set[str] = {"inet", "inet6", "inet6link"} def __init__( self, @@ -501,7 +497,6 @@ class CoreNode(CoreNodeBase): _id: int = None, name: str = None, nodedir: str = None, - start: bool = True, server: "DistributedServer" = None, ) -> None: """ @@ -511,11 +506,10 @@ class CoreNode(CoreNodeBase): :param _id: object id :param name: object name :param nodedir: node directory - :param start: start flag :param server: remote server node will run on, default is None for localhost """ - super().__init__(session, _id, name, start, server) + super().__init__(session, _id, name, server) self.nodedir: Optional[str] = nodedir self.ctrlchnlname: str = os.path.abspath( os.path.join(self.session.session_dir, self.name) @@ -526,8 +520,6 @@ class CoreNode(CoreNodeBase): self._mounts: List[Tuple[str, str]] = [] use_ovs = session.options.get_config("ovs") == "True" self.node_net_client: LinuxNetClient = self.create_node_net_client(use_ovs) - if start: - self.startup() def create_node_net_client(self, use_ovs: bool) -> LinuxNetClient: """ @@ -981,15 +973,14 @@ class CoreNetworkBase(NodeBase): Base class for networks """ - linktype = LinkTypes.WIRED - is_emane = False + linktype: LinkTypes = LinkTypes.WIRED + is_emane: bool = False def __init__( self, session: "Session", _id: int, name: str, - start: bool = True, server: "DistributedServer" = None, ) -> None: """ @@ -998,11 +989,10 @@ class CoreNetworkBase(NodeBase): :param session: CORE session object :param _id: object id :param name: object name - :param start: should object start :param server: remote server node will run on, default is None for localhost """ - super().__init__(session, _id, name, start, server) + super().__init__(session, _id, name, server) self.brname = None self._linked = {} self._linked_lock = threading.Lock() diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index fa4b8f8b..e911db74 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -77,7 +77,6 @@ class DockerNode(CoreNode): _id: int = None, name: str = None, nodedir: str = None, - start: bool = True, server: DistributedServer = None, image: str = None ) -> None: @@ -88,7 +87,6 @@ class DockerNode(CoreNode): :param _id: object id :param name: object name :param nodedir: node directory - :param start: start flag :param server: remote server node will run on, default is None for localhost :param image: image to start container with @@ -96,7 +94,7 @@ class DockerNode(CoreNode): if image is None: image = "ubuntu" self.image: str = image - super().__init__(session, _id, name, nodedir, start, server) + super().__init__(session, _id, name, nodedir, server) def create_node_net_client(self, use_ovs: bool) -> LinuxNetClient: """ diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index af906f01..a66791ce 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -74,7 +74,6 @@ class LxcNode(CoreNode): _id: int = None, name: str = None, nodedir: str = None, - start: bool = True, server: DistributedServer = None, image: str = None, ) -> None: @@ -85,7 +84,6 @@ class LxcNode(CoreNode): :param _id: object id :param name: object name :param nodedir: node directory - :param start: start flag :param server: remote server node will run on, default is None for localhost :param image: image to start container with @@ -93,7 +91,7 @@ class LxcNode(CoreNode): if image is None: image = "ubuntu" self.image: str = image - super().__init__(session, _id, name, nodedir, start, server) + super().__init__(session, _id, name, nodedir, server) def alive(self) -> bool: """ diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 9b46dfd5..b85c2eee 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -264,7 +264,6 @@ class CoreNetwork(CoreNetworkBase): session: "Session", _id: int = None, name: str = None, - start: bool = True, server: "DistributedServer" = None, policy: NetworkPolicy = None, ) -> None: @@ -274,12 +273,11 @@ class CoreNetwork(CoreNetworkBase): :param session: core session instance :param _id: object id :param name: object name - :param start: start flag :param server: remote server node will run on, default is None for localhost :param policy: network policy """ - super().__init__(session, _id, name, start, server) + super().__init__(session, _id, name, server) if name is None: name = str(self.id) if policy is not None: @@ -288,9 +286,6 @@ class CoreNetwork(CoreNetworkBase): sessionid = self.session.short_session_id() self.brname: str = f"b.{self.id}.{sessionid}" self.has_ebtables_chain: bool = False - if start: - self.startup() - ebq.startupdateloop(self) def host_cmd( self, @@ -327,6 +322,7 @@ class CoreNetwork(CoreNetworkBase): self.net_client.create_bridge(self.brname) self.has_ebtables_chain = False self.up = True + ebq.startupdateloop(self) def shutdown(self) -> None: """ @@ -610,7 +606,6 @@ class GreTapBridge(CoreNetwork): localip: str = None, ttl: int = 255, key: int = None, - start: bool = True, server: "DistributedServer" = None, ) -> None: """ @@ -628,7 +623,7 @@ class GreTapBridge(CoreNetwork): :param server: remote server node will run on, default is None for localhost """ - CoreNetwork.__init__(self, session, _id, name, False, server, policy) + CoreNetwork.__init__(self, session, _id, name, server, policy) if key is None: key = self.session.id ^ self.id self.grekey: int = key @@ -647,8 +642,6 @@ class GreTapBridge(CoreNetwork): ttl=ttl, key=self.grekey, ) - if start: - self.startup() def startup(self) -> None: """ @@ -734,7 +727,6 @@ class CtrlNet(CoreNetwork): _id: int = None, name: str = None, hostid: int = None, - start: bool = True, server: "DistributedServer" = None, assign_address: bool = True, updown_script: str = None, @@ -748,7 +740,6 @@ class CtrlNet(CoreNetwork): :param name: node namee :param prefix: control network ipv4 prefix :param hostid: host id - :param start: start flag :param server: remote server node will run on, default is None for localhost :param assign_address: assigned address @@ -761,7 +752,7 @@ class CtrlNet(CoreNetwork): self.assign_address: bool = assign_address self.updown_script: Optional[str] = updown_script self.serverintf: Optional[str] = serverintf - super().__init__(session, _id, name, start, server) + super().__init__(session, _id, name, server) def add_addresses(self, index: int) -> None: """ @@ -1025,7 +1016,6 @@ class WlanNode(CoreNetwork): session: "Session", _id: int = None, name: str = None, - start: bool = True, server: "DistributedServer" = None, policy: NetworkPolicy = None, ) -> None: @@ -1040,7 +1030,7 @@ class WlanNode(CoreNetwork): will run on, default is None for localhost :param policy: wlan policy """ - super().__init__(session, _id, name, start, server, policy) + super().__init__(session, _id, name, server, policy) # wireless and mobility models (BasicRangeModel, Ns2WaypointMobility) self.model: Optional[WirelessModel] = None self.mobility: Optional[WayPointMobility] = None diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index a72ff128..a2e39d49 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -28,22 +28,19 @@ class PhysicalNode(CoreNodeBase): _id: int = None, name: str = None, nodedir: str = None, - start: bool = True, server: DistributedServer = None, ) -> None: - super().__init__(session, _id, name, start, server) + super().__init__(session, _id, name, server) if not self.server: raise CoreError("physical nodes must be assigned to a remote server") self.nodedir: Optional[str] = nodedir - self.up: bool = start self.lock: threading.RLock = threading.RLock() self._mounts: List[Tuple[str, str]] = [] - if start: - self.startup() def startup(self) -> None: with self.lock: self.makenodedir() + self.up = True def shutdown(self) -> None: if not self.up: @@ -144,7 +141,7 @@ class PhysicalNode(CoreNodeBase): """ Apply tc queing disciplines using linkconfig. """ - linux_bridge = CoreNetwork(session=self.session, start=False) + linux_bridge = CoreNetwork(self.session) linux_bridge.up = True linux_bridge.linkconfig(netif, options, netif2) del linux_bridge @@ -244,7 +241,6 @@ class Rj45Node(CoreNodeBase): _id: int = None, name: str = None, mtu: int = 1500, - start: bool = True, server: DistributedServer = None, ) -> None: """ @@ -254,19 +250,16 @@ class Rj45Node(CoreNodeBase): :param _id: node id :param name: node name :param mtu: rj45 mtu - :param start: start flag :param server: remote server node will run on, default is None for localhost """ - super().__init__(session, _id, name, start, server) + super().__init__(session, _id, name, server) self.interface = CoreInterface(session, self, name, name, mtu, server) self.interface.transport_type = TransportType.RAW self.lock: threading.RLock = threading.RLock() self.ifindex: Optional[int] = None self.old_up: bool = False self.old_addrs: List[Tuple[str, Optional[str]]] = [] - if start: - self.startup() def startup(self) -> None: """ From cf4194889410c1faab5d04ca307a85f4f9b4f656 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 14 Jun 2020 12:36:07 -0700 Subject: [PATCH 0355/1131] daemon: fixed error with EmaneNet startup throwing an error, updated Rj45Node and PhysicalNode to implement all abstract methods --- daemon/core/emane/nodes.py | 3 +++ daemon/core/nodes/physical.py | 26 ++++++++++++++++---------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 68c1bc05..8383cdd1 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -76,6 +76,9 @@ class EmaneNet(CoreNetworkBase): def config(self, conf: str) -> None: self.conf = conf + def startup(self) -> None: + pass + def shutdown(self) -> None: pass diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index a2e39d49..13214093 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -217,14 +217,17 @@ class PhysicalNode(CoreNodeBase): return open(hostfilename, mode) def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: - with self.opennodefile(filename, "w") as node_file: - node_file.write(contents) - os.chmod(node_file.name, mode) - logging.info("created nodefile: '%s'; mode: 0%o", node_file.name, mode) + with self.opennodefile(filename, "w") as f: + f.write(contents) + os.chmod(f.name, mode) + logging.info("created nodefile: '%s'; mode: 0%o", f.name, mode) def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: return self.host_cmd(args, wait=wait) + def addfile(self, srcname: str, filename: str) -> None: + raise NotImplementedError + class Rj45Node(CoreNodeBase): """ @@ -446,10 +449,13 @@ class Rj45Node(CoreNodeBase): self.interface.setposition() def termcmdstring(self, sh: str) -> str: - """ - Create a terminal command string. - - :param sh: shell to execute command in - :return: str - """ + raise NotImplementedError + + def addfile(self, srcname: str, filename: str) -> None: + raise NotImplementedError + + def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: + raise NotImplementedError + + def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: raise NotImplementedError From f5916fab5b041c3dcb20ba8589abd4f7e6e698df Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 14 Jun 2020 12:44:51 -0700 Subject: [PATCH 0356/1131] daemon: added not implemented methods to CoreNodeBase --- daemon/core/nodes/base.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 49fe7620..3e9dfe7a 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -41,7 +41,6 @@ class NodeBase: apitype: Optional[NodeTypes] = None - # TODO: appears start has no usage, verify and remove def __init__( self, session: "Session", @@ -268,6 +267,12 @@ class CoreNodeBase(NodeBase): self.nodedir: Optional[str] = None self.tmpnodedir: bool = False + def startup(self) -> None: + raise NotImplementedError + + def shutdown(self) -> None: + raise NotImplementedError + def add_config_service(self, service_class: "ConfigServiceType") -> None: """ Adds a configuration service to the node. From 0462c1b0841bebdc0516d885344b3c52a3a03096 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 14 Jun 2020 13:35:06 -0700 Subject: [PATCH 0357/1131] daemon: added usage of ABC to NodeBase, CoreNodeBase, and CoreNetworkBase to help enforce accounting for abstract functions --- daemon/core/emane/nodes.py | 3 + daemon/core/nodes/base.py | 155 ++++++++++++++++++---------------- daemon/core/nodes/physical.py | 10 +-- 3 files changed, 92 insertions(+), 76 deletions(-) diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 8383cdd1..c4c3428b 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -88,6 +88,9 @@ class EmaneNet(CoreNetworkBase): def unlink(self, netif1: CoreInterface, netif2: CoreInterface) -> None: pass + def linknet(self, net: "CoreNetworkBase") -> CoreInterface: + raise CoreError("emane networks cannot be linked to other networks") + def updatemodel(self, config: Dict[str, str]) -> None: if not self.model: raise CoreError(f"no model set to update for node({self.name})") diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 3e9dfe7a..6c7ebcf0 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -1,7 +1,7 @@ """ Defines the base logic for nodes used within core. """ - +import abc import logging import os import shutil @@ -34,7 +34,7 @@ if TYPE_CHECKING: _DEFAULT_MTU = 1500 -class NodeBase: +class NodeBase(abc.ABC): """ Base class for CORE nodes (nodes and networks) """ @@ -78,6 +78,7 @@ class NodeBase: use_ovs = session.options.get_config("ovs") == "True" self.net_client: LinuxNetClient = get_net_client(use_ovs, self.host_cmd) + @abc.abstractmethod def startup(self) -> None: """ Each object implements its own startup method. @@ -86,6 +87,7 @@ class NodeBase: """ raise NotImplementedError + @abc.abstractmethod def shutdown(self) -> None: """ Each object implements its own shutdown method. @@ -267,12 +269,74 @@ class CoreNodeBase(NodeBase): self.nodedir: Optional[str] = None self.tmpnodedir: bool = False + @abc.abstractmethod def startup(self) -> None: raise NotImplementedError + @abc.abstractmethod def shutdown(self) -> None: raise NotImplementedError + @abc.abstractmethod + def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: + """ + Create a node file with a given mode. + + :param filename: name of file to create + :param contents: contents of file + :param mode: mode for file + :return: nothing + """ + raise NotImplementedError + + @abc.abstractmethod + def addfile(self, srcname: str, filename: str) -> None: + """ + Add a file. + + :param srcname: source file name + :param filename: file name to add + :return: nothing + :raises CoreCommandError: when a non-zero exit status occurs + """ + raise NotImplementedError + + @abc.abstractmethod + def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: + """ + Runs a command within a node container. + + :param args: command to run + :param wait: True to wait for status, False otherwise + :param shell: True to use shell, False otherwise + :return: combined stdout and stderr + :raises CoreCommandError: when a non-zero exit status occurs + """ + raise NotImplementedError + + @abc.abstractmethod + def termcmdstring(self, sh: str) -> str: + """ + Create a terminal command string. + + :param sh: shell to execute command in + :return: str + """ + raise NotImplementedError + + @abc.abstractmethod + def newnetif( + self, net: "CoreNetworkBase", interface_data: InterfaceData + ) -> CoreInterface: + """ + Create a new network interface. + + :param net: network to associate with + :param interface_data: interface data for new interface + :return: interface index + """ + raise NotImplementedError + def add_config_service(self, service_class: "ConfigServiceType") -> None: """ Adds a configuration service to the node. @@ -432,61 +496,6 @@ class CoreNodeBase(NodeBase): common.append((netif1.net, netif1, netif2)) return common - def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: - """ - Create a node file with a given mode. - - :param filename: name of file to create - :param contents: contents of file - :param mode: mode for file - :return: nothing - """ - raise NotImplementedError - - def addfile(self, srcname: str, filename: str) -> None: - """ - Add a file. - - :param srcname: source file name - :param filename: file name to add - :return: nothing - :raises CoreCommandError: when a non-zero exit status occurs - """ - raise NotImplementedError - - def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: - """ - Runs a command within a node container. - - :param args: command to run - :param wait: True to wait for status, False otherwise - :param shell: True to use shell, False otherwise - :return: combined stdout and stderr - :raises CoreCommandError: when a non-zero exit status occurs - """ - raise NotImplementedError - - def termcmdstring(self, sh: str) -> str: - """ - Create a terminal command string. - - :param sh: shell to execute command in - :return: str - """ - raise NotImplementedError - - def newnetif( - self, net: "CoreNetworkBase", interface_data: InterfaceData - ) -> CoreInterface: - """ - Create a new network interface. - - :param net: network to associate with - :param interface_data: interface data for new interface - :return: interface index - """ - raise NotImplementedError - class CoreNode(CoreNodeBase): """ @@ -1002,6 +1011,7 @@ class CoreNetworkBase(NodeBase): self._linked = {} self._linked_lock = threading.Lock() + @abc.abstractmethod def startup(self) -> None: """ Each object implements its own startup method. @@ -1010,6 +1020,7 @@ class CoreNetworkBase(NodeBase): """ raise NotImplementedError + @abc.abstractmethod def shutdown(self) -> None: """ Each object implements its own shutdown method. @@ -1018,6 +1029,7 @@ class CoreNetworkBase(NodeBase): """ raise NotImplementedError + @abc.abstractmethod def linknet(self, net: "CoreNetworkBase") -> CoreInterface: """ Link network to another. @@ -1025,7 +1037,21 @@ class CoreNetworkBase(NodeBase): :param net: network to link with :return: created interface """ - pass + raise NotImplementedError + + @abc.abstractmethod + def linkconfig( + self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None + ) -> None: + """ + Configure link parameters by applying tc queuing disciplines on the interface. + + :param netif: interface one + :param options: options for configuring link + :param netif2: interface two + :return: nothing + """ + raise NotImplementedError def getlinknetif(self, net: "CoreNetworkBase") -> Optional[CoreInterface]: """ @@ -1156,19 +1182,6 @@ class CoreNetworkBase(NodeBase): return all_links - def linkconfig( - self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None - ) -> None: - """ - Configure link parameters by applying tc queuing disciplines on the interface. - - :param netif: interface one - :param options: options for configuring link - :param netif2: interface two - :return: nothing - """ - raise NotImplementedError - class Position: """ diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 13214093..741fe7d5 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -226,7 +226,7 @@ class PhysicalNode(CoreNodeBase): return self.host_cmd(args, wait=wait) def addfile(self, srcname: str, filename: str) -> None: - raise NotImplementedError + raise CoreError("physical node does not support addfile") class Rj45Node(CoreNodeBase): @@ -449,13 +449,13 @@ class Rj45Node(CoreNodeBase): self.interface.setposition() def termcmdstring(self, sh: str) -> str: - raise NotImplementedError + raise CoreError("rj45 does not support terminal commands") def addfile(self, srcname: str, filename: str) -> None: - raise NotImplementedError + raise CoreError("rj45 does not support addfile") def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: - raise NotImplementedError + raise CoreError("rj45 does not support nodefile") def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: - raise NotImplementedError + raise CoreError("rj45 does not support cmds") From 0725199d6d2f9f5420628201861e0e78b64bd4b7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 16 Jun 2020 09:30:16 -0700 Subject: [PATCH 0358/1131] initial sweeping changes to call all usages of various interface related variables and functions (netif, interface, if, ifc, etc) to use a consistent name iface --- daemon/core/api/grpc/client.py | 72 ++-- daemon/core/api/grpc/events.py | 2 +- daemon/core/api/grpc/grpcutils.py | 134 +++--- daemon/core/api/grpc/server.py | 117 +++--- daemon/core/api/tlv/coreapi.py | 26 +- daemon/core/api/tlv/corehandlers.py | 88 ++-- daemon/core/api/tlv/dataconversion.py | 2 +- daemon/core/api/tlv/enumerations.py | 26 +- .../configservices/frrservices/services.py | 88 ++-- .../frrservices/templates/frr.conf | 6 +- .../frrservices/templates/frrboot.sh | 6 +- .../configservices/nrlservices/services.py | 42 +- .../nrlservices/templates/nrlnhdp.sh | 4 +- .../nrlservices/templates/nrlolsrv2.sh | 4 +- .../nrlservices/templates/olsrd.sh | 4 +- .../nrlservices/templates/startsmf.sh | 4 +- .../configservices/quaggaservices/services.py | 98 ++--- .../quaggaservices/templates/Quagga.conf | 6 +- .../sercurityservices/services.py | 12 +- .../configservices/utilservices/services.py | 64 ++- .../utilservices/templates/index.html | 4 +- daemon/core/emane/commeffect.py | 18 +- daemon/core/emane/emanemanager.py | 84 ++-- daemon/core/emane/emanemodel.py | 28 +- daemon/core/emane/linkmonitor.py | 6 +- daemon/core/emane/nodes.py | 80 ++-- daemon/core/emulator/data.py | 30 +- daemon/core/emulator/distributed.py | 4 +- daemon/core/emulator/emudata.py | 10 +- daemon/core/emulator/session.py | 184 ++++----- daemon/core/gui/coreclient.py | 117 +++--- daemon/core/gui/dialogs/emaneconfig.py | 10 +- daemon/core/gui/dialogs/ipdialog.py | 2 +- daemon/core/gui/dialogs/linkconfig.py | 36 +- daemon/core/gui/dialogs/macdialog.py | 2 +- daemon/core/gui/dialogs/nodeconfig.py | 76 ++-- daemon/core/gui/graph/edges.py | 26 +- daemon/core/gui/graph/graph.py | 84 ++-- daemon/core/gui/graph/node.py | 14 +- daemon/core/gui/interface.py | 64 +-- daemon/core/gui/menubar.py | 2 +- daemon/core/location/mobility.py | 127 +++--- daemon/core/nodes/base.py | 388 ++++++++---------- daemon/core/nodes/docker.py | 2 +- daemon/core/nodes/interface.py | 13 +- daemon/core/nodes/lxd.py | 6 +- daemon/core/nodes/netclient.py | 34 +- daemon/core/nodes/network.py | 282 ++++++------- daemon/core/nodes/physical.py | 193 ++++----- daemon/core/services/bird.py | 34 +- daemon/core/services/emaneservices.py | 6 +- daemon/core/services/frr.py | 144 +++---- daemon/core/services/nrl.py | 58 ++- daemon/core/services/quagga.py | 138 +++---- daemon/core/services/sdn.py | 24 +- daemon/core/services/security.py | 18 +- daemon/core/services/utility.py | 62 ++- daemon/core/services/xorp.py | 100 ++--- daemon/core/xml/corexml.py | 118 +++--- daemon/core/xml/corexmldeployment.py | 36 +- daemon/core/xml/emanexml.py | 76 ++-- daemon/examples/configservices/testing.py | 8 +- daemon/examples/docker/docker2core.py | 4 +- daemon/examples/docker/docker2docker.py | 4 +- daemon/examples/docker/switch.py | 6 +- daemon/examples/grpc/distributed_switch.py | 4 +- daemon/examples/grpc/emane80211.py | 4 +- daemon/examples/grpc/switch.py | 4 +- daemon/examples/grpc/wlan.py | 4 +- daemon/examples/lxd/lxd2core.py | 4 +- daemon/examples/lxd/lxd2lxd.py | 4 +- daemon/examples/lxd/switch.py | 6 +- daemon/examples/myservices/sample.py | 4 +- daemon/examples/python/distributed_emane.py | 8 +- daemon/examples/python/distributed_lxd.py | 4 +- daemon/examples/python/distributed_ptp.py | 4 +- daemon/examples/python/distributed_switch.py | 8 +- daemon/examples/python/emane80211.py | 4 +- daemon/examples/python/switch.py | 4 +- daemon/examples/python/switch_inject.py | 4 +- daemon/examples/python/wlan.py | 4 +- daemon/proto/core/api/grpc/core.proto | 26 +- daemon/proto/core/api/grpc/emane.proto | 10 +- daemon/scripts/core-route-monitor | 4 +- daemon/tests/conftest.py | 2 +- daemon/tests/emane/test_emane.py | 8 +- daemon/tests/test_core.py | 44 +- daemon/tests/test_grpc.py | 60 +-- daemon/tests/test_gui.py | 80 ++-- daemon/tests/test_links.py | 174 ++++---- daemon/tests/test_nodes.py | 28 +- daemon/tests/test_xml.py | 30 +- docs/scripting.md | 4 +- 93 files changed, 1955 insertions(+), 2156 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 3a16d4fd..47aaef63 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -110,27 +110,27 @@ class InterfaceHelper: """ self.prefixes: IpPrefixes = IpPrefixes(ip4_prefix, ip6_prefix) - def create_interface( - self, node_id: int, interface_id: int, name: str = None, mac: str = None + def create_iface( + self, node_id: int, iface_id: int, name: str = None, mac: str = None ) -> core_pb2.Interface: """ Create an interface protobuf object. :param node_id: node id to create interface for - :param interface_id: interface id + :param iface_id: interface id :param name: name of interface :param mac: mac address for interface :return: interface protobuf """ - interface_data = self.prefixes.gen_interface(node_id, name, mac) + iface_data = self.prefixes.gen_iface(node_id, name, mac) return core_pb2.Interface( - id=interface_id, - name=interface_data.name, - ip4=interface_data.ip4, - ip4mask=interface_data.ip4_mask, - ip6=interface_data.ip6, - ip6mask=interface_data.ip6_mask, - mac=interface_data.mac, + id=iface_id, + name=iface_data.name, + ip4=iface_data.ip4, + ip4mask=iface_data.ip4_mask, + ip6=iface_data.ip6, + ip6mask=iface_data.ip6_mask, + mac=iface_data.mac, ) @@ -611,8 +611,8 @@ class CoreGrpcClient: session_id: int, node1_id: int, node2_id: int, - interface1: core_pb2.Interface = None, - interface2: core_pb2.Interface = None, + iface1: core_pb2.Interface = None, + iface2: core_pb2.Interface = None, options: core_pb2.LinkOptions = None, ) -> core_pb2.AddLinkResponse: """ @@ -621,8 +621,8 @@ class CoreGrpcClient: :param session_id: session id :param node1_id: node one id :param node2_id: node two id - :param interface1: node one interface data - :param interface2: node two interface data + :param iface1: node one interface data + :param iface2: node two interface data :param options: options for link (jitter, bandwidth, etc) :return: response with result of success or failure :raises grpc.RpcError: when session or one of the nodes don't exist @@ -631,8 +631,8 @@ class CoreGrpcClient: node1_id=node1_id, node2_id=node2_id, type=core_pb2.LinkType.WIRED, - interface1=interface1, - interface2=interface2, + iface1=iface1, + iface2=iface2, options=options, ) request = core_pb2.AddLinkRequest(session_id=session_id, link=link) @@ -644,8 +644,8 @@ class CoreGrpcClient: node1_id: int, node2_id: int, options: core_pb2.LinkOptions, - interface1_id: int = None, - interface2_id: int = None, + iface1_id: int = None, + iface2_id: int = None, ) -> core_pb2.EditLinkResponse: """ Edit a link between nodes. @@ -654,8 +654,8 @@ class CoreGrpcClient: :param node1_id: node one id :param node2_id: node two id :param options: options for link (jitter, bandwidth, etc) - :param interface1_id: node one interface id - :param interface2_id: node two interface id + :param iface1_id: node one interface id + :param iface2_id: node two interface id :return: response with result of success or failure :raises grpc.RpcError: when session or one of the nodes don't exist """ @@ -664,8 +664,8 @@ class CoreGrpcClient: node1_id=node1_id, node2_id=node2_id, options=options, - interface1_id=interface1_id, - interface2_id=interface2_id, + iface1_id=iface1_id, + iface2_id=iface2_id, ) return self.stub.EditLink(request) @@ -674,8 +674,8 @@ class CoreGrpcClient: session_id: int, node1_id: int, node2_id: int, - interface1_id: int = None, - interface2_id: int = None, + iface1_id: int = None, + iface2_id: int = None, ) -> core_pb2.DeleteLinkResponse: """ Delete a link between nodes. @@ -683,8 +683,8 @@ class CoreGrpcClient: :param session_id: session id :param node1_id: node one id :param node2_id: node two id - :param interface1_id: node one interface id - :param interface2_id: node two interface id + :param iface1_id: node one interface id + :param iface2_id: node two interface id :return: response with result of success or failure :raises grpc.RpcError: when session doesn't exist """ @@ -692,8 +692,8 @@ class CoreGrpcClient: session_id=session_id, node1_id=node1_id, node2_id=node2_id, - interface1_id=interface1_id, - interface2_id=interface2_id, + iface1_id=iface1_id, + iface2_id=iface2_id, ) return self.stub.DeleteLink(request) @@ -1028,7 +1028,7 @@ class CoreGrpcClient: return self.stub.GetEmaneModels(request) def get_emane_model_config( - self, session_id: int, node_id: int, model: str, interface_id: int = -1 + self, session_id: int, node_id: int, model: str, iface_id: int = -1 ) -> GetEmaneModelConfigResponse: """ Get emane model configuration for a node or a node's interface. @@ -1036,12 +1036,12 @@ class CoreGrpcClient: :param session_id: session id :param node_id: node id :param model: emane model name - :param interface_id: node interface id + :param iface_id: node interface id :return: response with a list of configuration groups :raises grpc.RpcError: when session doesn't exist """ request = GetEmaneModelConfigRequest( - session_id=session_id, node_id=node_id, model=model, interface=interface_id + session_id=session_id, node_id=node_id, model=model, iface_id=iface_id ) return self.stub.GetEmaneModelConfig(request) @@ -1051,7 +1051,7 @@ class CoreGrpcClient: node_id: int, model: str, config: Dict[str, str] = None, - interface_id: int = -1, + iface_id: int = -1, ) -> SetEmaneModelConfigResponse: """ Set emane model configuration for a node or a node's interface. @@ -1060,12 +1060,12 @@ class CoreGrpcClient: :param node_id: node id :param model: emane model name :param config: emane model configuration - :param interface_id: node interface id + :param iface_id: node interface id :return: response with result of success or failure :raises grpc.RpcError: when session doesn't exist """ model_config = EmaneModelConfig( - node_id=node_id, model=model, config=config, interface_id=interface_id + node_id=node_id, model=model, config=config, iface_id=iface_id ) request = SetEmaneModelConfigRequest( session_id=session_id, emane_model_config=model_config @@ -1128,7 +1128,7 @@ class CoreGrpcClient: ) return self.stub.EmaneLink(request) - def get_interfaces(self) -> core_pb2.GetInterfacesResponse: + def get_ifaces(self) -> core_pb2.GetInterfacesResponse: """ Retrieves a list of interfaces available on the host machine that are not a part of a CORE session. diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index 82cf1eac..ff65142d 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -82,7 +82,7 @@ def handle_config_event(event: ConfigData) -> core_pb2.ConfigEvent: data_values=event.data_values, possible_values=event.possible_values, groups=event.groups, - interface=event.interface_number, + iface_id=event.iface_id, network_id=event.network_id, opaque=event.opaque, data_types=event.data_types, diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index c9b76b73..f2f85798 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -52,29 +52,29 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption return _type, _id, options -def link_interface(interface_proto: core_pb2.Interface) -> InterfaceData: +def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData: """ Create interface data from interface proto. - :param interface_proto: interface proto + :param iface_proto: interface proto :return: interface data """ - interface_data = None - if interface_proto: - name = interface_proto.name if interface_proto.name else None - mac = interface_proto.mac if interface_proto.mac else None - ip4 = interface_proto.ip4 if interface_proto.ip4 else None - ip6 = interface_proto.ip6 if interface_proto.ip6 else None - interface_data = InterfaceData( - id=interface_proto.id, + iface_data = None + if iface_proto: + name = iface_proto.name if iface_proto.name else None + mac = iface_proto.mac if iface_proto.mac else None + ip4 = iface_proto.ip4 if iface_proto.ip4 else None + ip6 = iface_proto.ip6 if iface_proto.ip6 else None + iface_data = InterfaceData( + id=iface_proto.id, name=name, mac=mac, ip4=ip4, - ip4_mask=interface_proto.ip4mask, + ip4_mask=iface_proto.ip4mask, ip6=ip6, - ip6_mask=interface_proto.ip6mask, + ip6_mask=iface_proto.ip6mask, ) - return interface_data + return iface_data def add_link_data( @@ -86,8 +86,8 @@ def add_link_data( :param link_proto: link proto :return: link interfaces and options """ - interface1_data = link_interface(link_proto.interface1) - interface2_data = link_interface(link_proto.interface2) + iface1_data = link_iface(link_proto.iface1) + iface2_data = link_iface(link_proto.iface2) link_type = LinkTypes(link_proto.type) options = LinkOptions(type=link_type) options_data = link_proto.options @@ -103,7 +103,7 @@ def add_link_data( options.unidirectional = options_data.unidirectional options.key = options_data.key options.opaque = options_data.opaque - return interface1_data, interface2_data, options + return iface1_data, iface2_data, options def create_nodes( @@ -143,8 +143,8 @@ def create_links( for link_proto in link_protos: node1_id = link_proto.node1_id node2_id = link_proto.node2_id - interface1, interface2, options = add_link_data(link_proto) - args = (node1_id, node2_id, interface1, interface2, options) + iface1, iface2, options = add_link_data(link_proto) + args = (node1_id, node2_id, iface1, iface2, options) funcs.append((session.add_link, args, {})) start = time.monotonic() results, exceptions = utils.threadpool(funcs) @@ -167,8 +167,8 @@ def edit_links( for link_proto in link_protos: node1_id = link_proto.node1_id node2_id = link_proto.node2_id - interface1, interface2, options = add_link_data(link_proto) - args = (node1_id, node2_id, interface1.id, interface2.id, options) + iface1, iface2, options = add_link_data(link_proto) + args = (node1_id, node2_id, iface1.id, iface2.id, options) funcs.append((session.update_link, args, {})) start = time.monotonic() results, exceptions = utils.threadpool(funcs) @@ -279,16 +279,16 @@ def get_links(node: NodeBase): return links -def get_emane_model_id(node_id: int, interface_id: int) -> int: +def get_emane_model_id(node_id: int, iface_id: int) -> int: """ Get EMANE model id :param node_id: node id - :param interface_id: interface id + :param iface_id: interface id :return: EMANE model id """ - if interface_id >= 0: - return node_id * 1000 + interface_id + if iface_id >= 0: + return node_id * 1000 + iface_id else: return node_id @@ -300,12 +300,12 @@ def parse_emane_model_id(_id: int) -> Tuple[int, int]: :param _id: id to parse :return: node id and interface id """ - interface = -1 + iface_id = -1 node_id = _id if _id >= 1000: - interface = _id % 1000 + iface_id = _id % 1000 node_id = int(_id / 1000) - return node_id, interface + return node_id, iface_id def convert_link(link_data: LinkData) -> core_pb2.Link: @@ -315,27 +315,27 @@ def convert_link(link_data: LinkData) -> core_pb2.Link: :param link_data: link to convert :return: core protobuf Link """ - interface1 = None - if link_data.interface1_id is not None: - interface1 = core_pb2.Interface( - id=link_data.interface1_id, - name=link_data.interface1_name, - mac=convert_value(link_data.interface1_mac), - ip4=convert_value(link_data.interface1_ip4), - ip4mask=link_data.interface1_ip4_mask, - ip6=convert_value(link_data.interface1_ip6), - ip6mask=link_data.interface1_ip6_mask, + iface1 = None + if link_data.iface1_id is not None: + iface1 = core_pb2.Interface( + id=link_data.iface1_id, + name=link_data.iface1_name, + mac=convert_value(link_data.iface1_mac), + ip4=convert_value(link_data.iface1_ip4), + ip4mask=link_data.iface1_ip4_mask, + ip6=convert_value(link_data.iface1_ip6), + ip6mask=link_data.iface1_ip6_mask, ) - interface2 = None - if link_data.interface2_id is not None: - interface2 = core_pb2.Interface( - id=link_data.interface2_id, - name=link_data.interface2_name, - mac=convert_value(link_data.interface2_mac), - ip4=convert_value(link_data.interface2_ip4), - ip4mask=link_data.interface2_ip4_mask, - ip6=convert_value(link_data.interface2_ip6), - ip6mask=link_data.interface2_ip6_mask, + iface2 = None + if link_data.iface2_id is not None: + iface2 = core_pb2.Interface( + id=link_data.iface2_id, + name=link_data.iface2_name, + mac=convert_value(link_data.iface2_mac), + ip4=convert_value(link_data.iface2_ip4), + ip4mask=link_data.iface2_ip4_mask, + ip6=convert_value(link_data.iface2_ip6), + ip6mask=link_data.iface2_ip6_mask, ) options = core_pb2.LinkOptions( opaque=link_data.opaque, @@ -354,8 +354,8 @@ def convert_link(link_data: LinkData) -> core_pb2.Link: type=link_data.link_type.value, node1_id=link_data.node1_id, node2_id=link_data.node2_id, - interface1=interface1, - interface2=interface2, + iface1=iface1, + iface2=iface2, options=options, network_id=link_data.network_id, label=link_data.label, @@ -440,20 +440,20 @@ def get_service_configuration(service: CoreService) -> NodeServiceData: ) -def interface_to_proto(interface: CoreInterface) -> core_pb2.Interface: +def iface_to_proto(iface: CoreInterface) -> core_pb2.Interface: """ Convenience for converting a core interface to the protobuf representation. - :param interface: interface to convert + :param iface: interface to convert :return: interface proto """ net_id = None - if interface.net: - net_id = interface.net.id + if iface.net: + net_id = iface.net.id ip4 = None ip4mask = None ip6 = None ip6mask = None - for addr in interface.addrlist: + for addr in iface.addrlist: network = netaddr.IPNetwork(addr) mask = network.prefixlen ip = str(network.ip) @@ -464,12 +464,12 @@ def interface_to_proto(interface: CoreInterface) -> core_pb2.Interface: ip6 = ip ip6mask = mask return core_pb2.Interface( - id=interface.netindex, + id=iface.node_id, netid=net_id, - name=interface.name, - mac=str(interface.hwaddr), - mtu=interface.mtu, - flowid=interface.flow_id, + name=iface.name, + mac=str(iface.hwaddr), + mtu=iface.mtu, + flowid=iface.flow_id, ip4=ip4, ip4mask=ip4mask, ip6=ip6, @@ -477,21 +477,21 @@ def interface_to_proto(interface: CoreInterface) -> core_pb2.Interface: ) -def get_nem_id(node: CoreNode, netif_id: int, context: ServicerContext) -> int: +def get_nem_id(node: CoreNode, iface_id: int, context: ServicerContext) -> int: """ Get nem id for a given node and interface id. :param node: node to get nem id for - :param netif_id: id of interface on node to get nem id for + :param iface_id: id of interface on node to get nem id for :param context: request context :return: nem id """ - netif = node.netif(netif_id) - if not netif: - message = f"{node.name} missing interface {netif_id}" + iface = node.ifaces.get(iface_id) + if not iface: + message = f"{node.name} missing interface {iface_id}" context.abort(grpc.StatusCode.NOT_FOUND, message) - net = netif.net + net = iface.net if not isinstance(net, EmaneNet): - message = f"{node.name} interface {netif_id} is not an EMANE network" + message = f"{node.name} interface {iface_id} is not an EMANE network" context.abort(grpc.StatusCode.INVALID_ARGUMENT, message) - return net.getnemid(netif) + return net.getnemid(iface) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 8b349b67..87b69a77 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -246,7 +246,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): config = session.emane.get_configs() config.update(request.emane_config) for config in request.emane_model_configs: - _id = get_emane_model_id(config.node_id, config.interface_id) + _id = get_emane_model_id(config.node_id, config.iface_id) session.emane.set_model_config(_id, config.model, config.config) # wlan configs @@ -625,16 +625,14 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): key = key.split(".") node_id = _INTERFACE_REGEX.search(key[0]).group("node") node_id = int(node_id, base=16) - interface_id = int(key[1], base=16) + iface_id = int(key[1], base=16) session_id = int(key[2], base=16) if session.id != session_id: continue - interface_throughput = ( - throughputs_event.interface_throughputs.add() - ) - interface_throughput.node_id = node_id - interface_throughput.interface_id = interface_id - interface_throughput.throughput = throughput + iface_throughput = throughputs_event.iface_throughputs.add() + iface_throughput.node_id = node_id + iface_throughput.iface_id = iface_id + iface_throughput.throughput = throughput elif key.startswith("b."): try: key = key.split(".") @@ -686,13 +684,13 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("get node: %s", request) session = self.get_session(request.session_id, context) node = self.get_node(session, request.node_id, context, NodeBase) - interfaces = [] - for interface_id in node._netif: - interface = node._netif[interface_id] - interface_proto = grpcutils.interface_to_proto(interface) - interfaces.append(interface_proto) + ifaces = [] + for iface_id in node.ifaces: + iface = node.ifaces[iface_id] + iface_proto = grpcutils.iface_to_proto(iface) + ifaces.append(iface_proto) node_proto = grpcutils.get_node_proto(session, node) - return core_pb2.GetNodeResponse(node=node_proto, interfaces=interfaces) + return core_pb2.GetNodeResponse(node=node_proto, ifaces=ifaces) def MoveNodes( self, @@ -850,18 +848,18 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): node2_id = request.link.node2_id self.get_node(session, node1_id, context, NodeBase) self.get_node(session, node2_id, context, NodeBase) - interface1, interface2, options = grpcutils.add_link_data(request.link) - node1_interface, node2_interface = session.add_link( - node1_id, node2_id, interface1, interface2, options=options + iface1_data, iface2_data, options = grpcutils.add_link_data(request.link) + node1_iface, node2_iface = session.add_link( + node1_id, node2_id, iface1_data, iface2_data, options=options ) - interface1_proto = None - interface2_proto = None - if node1_interface: - interface1_proto = grpcutils.interface_to_proto(node1_interface) - if node2_interface: - interface2_proto = grpcutils.interface_to_proto(node2_interface) + iface1_proto = None + iface2_proto = None + if node1_iface: + iface1_proto = grpcutils.iface_to_proto(node1_iface) + if node2_iface: + iface2_proto = grpcutils.iface_to_proto(node2_iface) return core_pb2.AddLinkResponse( - result=True, interface1=interface1_proto, interface2=interface2_proto + result=True, iface1=iface1_proto, iface2=iface2_proto ) def EditLink( @@ -878,8 +876,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session = self.get_session(request.session_id, context) node1_id = request.node1_id node2_id = request.node2_id - interface1_id = request.interface1_id - interface2_id = request.interface2_id + iface1_id = request.iface1_id + iface2_id = request.iface2_id options_data = request.options options = LinkOptions( delay=options_data.delay, @@ -894,7 +892,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): key=options_data.key, opaque=options_data.opaque, ) - session.update_link(node1_id, node2_id, interface1_id, interface2_id, options) + session.update_link(node1_id, node2_id, iface1_id, iface2_id, options) return core_pb2.EditLinkResponse(result=True) def DeleteLink( @@ -911,9 +909,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session = self.get_session(request.session_id, context) node1_id = request.node1_id node2_id = request.node2_id - interface1_id = request.interface1_id - interface2_id = request.interface2_id - session.delete_link(node1_id, node2_id, interface1_id, interface2_id) + iface1_id = request.iface1_id + iface2_id = request.iface2_id + session.delete_link(node1_id, node2_id, iface1_id, iface2_id) return core_pb2.DeleteLinkResponse(result=True) def GetHooks( @@ -1371,7 +1369,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("get emane model config: %s", request) session = self.get_session(request.session_id, context) model = session.emane.models[request.model] - _id = get_emane_model_id(request.node_id, request.interface) + _id = get_emane_model_id(request.node_id, request.iface_id) current_config = session.emane.get_model_config(_id, request.model) config = get_config_options(current_config, model) return GetEmaneModelConfigResponse(config=config) @@ -1390,7 +1388,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("set emane model config: %s", request) session = self.get_session(request.session_id, context) model_config = request.emane_model_config - _id = get_emane_model_id(model_config.node_id, model_config.interface_id) + _id = get_emane_model_id(model_config.node_id, model_config.iface_id) session.emane.set_model_config(_id, model_config.model, model_config.config) return SetEmaneModelConfigResponse(result=True) @@ -1419,12 +1417,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): model = session.emane.models[model_name] current_config = session.emane.get_model_config(_id, model_name) config = get_config_options(current_config, model) - node_id, interface = grpcutils.parse_emane_model_id(_id) + node_id, iface_id = grpcutils.parse_emane_model_id(_id) model_config = GetEmaneModelConfigsResponse.ModelConfig( - node_id=node_id, - model=model_name, - interface=interface, - config=config, + node_id=node_id, model=model_name, iface_id=iface_id, config=config ) configs.append(model_config) return GetEmaneModelConfigsResponse(configs=configs) @@ -1489,16 +1484,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: get-interfaces response that has all the system's interfaces """ - interfaces = [] - for interface in os.listdir("/sys/class/net"): - if ( - interface.startswith("b.") - or interface.startswith("veth") - or interface == "lo" - ): + ifaces = [] + for iface in os.listdir("/sys/class/net"): + if iface.startswith("b.") or iface.startswith("veth") or iface == "lo": continue - interfaces.append(interface) - return core_pb2.GetInterfacesResponse(interfaces=interfaces) + ifaces.append(iface) + return core_pb2.GetInterfacesResponse(ifaces=ifaces) def EmaneLink( self, request: EmaneLinkRequest, context: ServicerContext @@ -1513,16 +1504,16 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("emane link: %s", request) session = self.get_session(request.session_id, context) nem1 = request.nem1 - emane1, netif = session.emane.nemlookup(nem1) - if not emane1 or not netif: + emane1, iface = session.emane.nemlookup(nem1) + if not emane1 or not iface: context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem1} not found") - node1 = netif.node + node1 = iface.node nem2 = request.nem2 - emane2, netif = session.emane.nemlookup(nem2) - if not emane2 or not netif: + emane2, iface = session.emane.nemlookup(nem2) + if not emane2 or not iface: context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem2} not found") - node2 = netif.node + node2 = iface.node if emane1.id == emane2.id: if request.linked: @@ -1734,21 +1725,19 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): ) node1 = self.get_node(session, request.node1_id, context, CoreNode) node2 = self.get_node(session, request.node2_id, context, CoreNode) - node1_interface, node2_interface = None, None - for net, interface1, interface2 in node1.commonnets(node2): + node1_iface, node2_iface = None, None + for net, iface1, iface2 in node1.commonnets(node2): if net == wlan: - node1_interface = interface1 - node2_interface = interface2 + node1_iface = iface1 + node2_iface = iface2 break result = False - if node1_interface and node2_interface: + if node1_iface and node2_iface: if request.linked: - wlan.link(node1_interface, node2_interface) + wlan.link(node1_iface, node2_iface) else: - wlan.unlink(node1_interface, node2_interface) - wlan.model.sendlinkmsg( - node1_interface, node2_interface, unlink=not request.linked - ) + wlan.unlink(node1_iface, node2_iface) + wlan.model.sendlinkmsg(node1_iface, node2_iface, unlink=not request.linked) result = True return WlanLinkResponse(result=result) @@ -1760,8 +1749,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): for request in request_iterator: session = self.get_session(request.session_id, context) node1 = self.get_node(session, request.node1_id, context, CoreNode) - nem1 = grpcutils.get_nem_id(node1, request.interface1_id, context) + nem1 = grpcutils.get_nem_id(node1, request.iface1_id, context) node2 = self.get_node(session, request.node2_id, context, CoreNode) - nem2 = grpcutils.get_nem_id(node2, request.interface2_id, context) + nem2 = grpcutils.get_nem_id(node2, request.iface2_id, context) session.emane.publish_pathloss(nem1, nem2, request.rx1, request.rx2) return EmanePathlossesResponse() diff --git a/daemon/core/api/tlv/coreapi.py b/daemon/core/api/tlv/coreapi.py index 088a7631..5d0b08e7 100644 --- a/daemon/core/api/tlv/coreapi.py +++ b/daemon/core/api/tlv/coreapi.py @@ -508,18 +508,18 @@ class CoreLinkTlv(CoreTlv): LinkTlvs.EMULATION_ID.value: CoreTlvDataUint32, LinkTlvs.NETWORK_ID.value: CoreTlvDataUint32, LinkTlvs.KEY.value: CoreTlvDataUint32, - LinkTlvs.INTERFACE1_NUMBER.value: CoreTlvDataUint16, - LinkTlvs.INTERFACE1_IP4.value: CoreTlvDataIpv4Addr, - LinkTlvs.INTERFACE1_IP4_MASK.value: CoreTlvDataUint16, - LinkTlvs.INTERFACE1_MAC.value: CoreTlvDataMacAddr, - LinkTlvs.INTERFACE1_IP6.value: CoreTlvDataIPv6Addr, - LinkTlvs.INTERFACE1_IP6_MASK.value: CoreTlvDataUint16, - LinkTlvs.INTERFACE2_NUMBER.value: CoreTlvDataUint16, - LinkTlvs.INTERFACE2_IP4.value: CoreTlvDataIpv4Addr, - LinkTlvs.INTERFACE2_IP4_MASK.value: CoreTlvDataUint16, - LinkTlvs.INTERFACE2_MAC.value: CoreTlvDataMacAddr, - LinkTlvs.INTERFACE2_IP6.value: CoreTlvDataIPv6Addr, - LinkTlvs.INTERFACE2_IP6_MASK.value: CoreTlvDataUint16, + LinkTlvs.IFACE1_NUMBER.value: CoreTlvDataUint16, + LinkTlvs.IFACE1_IP4.value: CoreTlvDataIpv4Addr, + LinkTlvs.IFACE1_IP4_MASK.value: CoreTlvDataUint16, + LinkTlvs.IFACE1_MAC.value: CoreTlvDataMacAddr, + LinkTlvs.IFACE1_IP6.value: CoreTlvDataIPv6Addr, + LinkTlvs.IFACE1_IP6_MASK.value: CoreTlvDataUint16, + LinkTlvs.IFACE2_NUMBER.value: CoreTlvDataUint16, + LinkTlvs.IFACE2_IP4.value: CoreTlvDataIpv4Addr, + LinkTlvs.IFACE2_IP4_MASK.value: CoreTlvDataUint16, + LinkTlvs.IFACE2_MAC.value: CoreTlvDataMacAddr, + LinkTlvs.IFACE2_IP6.value: CoreTlvDataIPv6Addr, + LinkTlvs.IFACE2_IP6_MASK.value: CoreTlvDataUint16, LinkTlvs.INTERFACE1_NAME.value: CoreTlvDataString, LinkTlvs.INTERFACE2_NAME.value: CoreTlvDataString, LinkTlvs.OPAQUE.value: CoreTlvDataString, @@ -577,7 +577,7 @@ class CoreConfigTlv(CoreTlv): ConfigTlvs.POSSIBLE_VALUES.value: CoreTlvDataString, ConfigTlvs.GROUPS.value: CoreTlvDataString, ConfigTlvs.SESSION.value: CoreTlvDataString, - ConfigTlvs.INTERFACE_NUMBER.value: CoreTlvDataUint16, + ConfigTlvs.IFACE_ID.value: CoreTlvDataUint16, ConfigTlvs.NETWORK_ID.value: CoreTlvDataUint32, ConfigTlvs.OPAQUE.value: CoreTlvDataString, } diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 2cd7bfac..b09a37fe 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -71,7 +71,7 @@ class CoreHandler(socketserver.BaseRequestHandler): MessageTypes.REGISTER.value: self.handle_register_message, MessageTypes.CONFIG.value: self.handle_config_message, MessageTypes.FILE.value: self.handle_file_message, - MessageTypes.INTERFACE.value: self.handle_interface_message, + MessageTypes.INTERFACE.value: self.handle_iface_message, MessageTypes.EVENT.value: self.handle_event_message, MessageTypes.SESSION.value: self.handle_session_message, } @@ -363,18 +363,18 @@ class CoreHandler(socketserver.BaseRequestHandler): (LinkTlvs.EMULATION_ID, link_data.emulation_id), (LinkTlvs.NETWORK_ID, link_data.network_id), (LinkTlvs.KEY, link_data.key), - (LinkTlvs.INTERFACE1_NUMBER, link_data.interface1_id), - (LinkTlvs.INTERFACE1_IP4, link_data.interface1_ip4), - (LinkTlvs.INTERFACE1_IP4_MASK, link_data.interface1_ip4_mask), - (LinkTlvs.INTERFACE1_MAC, link_data.interface1_mac), - (LinkTlvs.INTERFACE1_IP6, link_data.interface1_ip6), - (LinkTlvs.INTERFACE1_IP6_MASK, link_data.interface1_ip6_mask), - (LinkTlvs.INTERFACE2_NUMBER, link_data.interface2_id), - (LinkTlvs.INTERFACE2_IP4, link_data.interface2_ip4), - (LinkTlvs.INTERFACE2_IP4_MASK, link_data.interface2_ip4_mask), - (LinkTlvs.INTERFACE2_MAC, link_data.interface2_mac), - (LinkTlvs.INTERFACE2_IP6, link_data.interface2_ip6), - (LinkTlvs.INTERFACE2_IP6_MASK, link_data.interface2_ip6_mask), + (LinkTlvs.IFACE1_NUMBER, link_data.iface1_id), + (LinkTlvs.IFACE1_IP4, link_data.iface1_ip4), + (LinkTlvs.IFACE1_IP4_MASK, link_data.iface1_ip4_mask), + (LinkTlvs.IFACE1_MAC, link_data.iface1_mac), + (LinkTlvs.IFACE1_IP6, link_data.iface1_ip6), + (LinkTlvs.IFACE1_IP6_MASK, link_data.iface1_ip6_mask), + (LinkTlvs.IFACE2_NUMBER, link_data.iface2_id), + (LinkTlvs.IFACE2_IP4, link_data.iface2_ip4), + (LinkTlvs.IFACE2_IP4_MASK, link_data.iface2_ip4_mask), + (LinkTlvs.IFACE2_MAC, link_data.iface2_mac), + (LinkTlvs.IFACE2_IP6, link_data.iface2_ip6), + (LinkTlvs.IFACE2_IP6_MASK, link_data.iface2_ip6_mask), (LinkTlvs.OPAQUE, link_data.opaque), ], ) @@ -749,23 +749,23 @@ class CoreHandler(socketserver.BaseRequestHandler): """ node1_id = message.get_tlv(LinkTlvs.N1_NUMBER.value) node2_id = message.get_tlv(LinkTlvs.N2_NUMBER.value) - interface1_data = InterfaceData( - id=message.get_tlv(LinkTlvs.INTERFACE1_NUMBER.value), + iface1_data = InterfaceData( + id=message.get_tlv(LinkTlvs.IFACE1_NUMBER.value), name=message.get_tlv(LinkTlvs.INTERFACE1_NAME.value), - mac=message.get_tlv(LinkTlvs.INTERFACE1_MAC.value), - ip4=message.get_tlv(LinkTlvs.INTERFACE1_IP4.value), - ip4_mask=message.get_tlv(LinkTlvs.INTERFACE1_IP4_MASK.value), - ip6=message.get_tlv(LinkTlvs.INTERFACE1_IP6.value), - ip6_mask=message.get_tlv(LinkTlvs.INTERFACE1_IP6_MASK.value), + mac=message.get_tlv(LinkTlvs.IFACE1_MAC.value), + ip4=message.get_tlv(LinkTlvs.IFACE1_IP4.value), + ip4_mask=message.get_tlv(LinkTlvs.IFACE1_IP4_MASK.value), + ip6=message.get_tlv(LinkTlvs.IFACE1_IP6.value), + ip6_mask=message.get_tlv(LinkTlvs.IFACE1_IP6_MASK.value), ) - interface2_data = InterfaceData( - id=message.get_tlv(LinkTlvs.INTERFACE2_NUMBER.value), + iface2_data = InterfaceData( + id=message.get_tlv(LinkTlvs.IFACE2_NUMBER.value), name=message.get_tlv(LinkTlvs.INTERFACE2_NAME.value), - mac=message.get_tlv(LinkTlvs.INTERFACE2_MAC.value), - ip4=message.get_tlv(LinkTlvs.INTERFACE2_IP4.value), - ip4_mask=message.get_tlv(LinkTlvs.INTERFACE2_IP4_MASK.value), - ip6=message.get_tlv(LinkTlvs.INTERFACE2_IP6.value), - ip6_mask=message.get_tlv(LinkTlvs.INTERFACE2_IP6_MASK.value), + mac=message.get_tlv(LinkTlvs.IFACE2_MAC.value), + ip4=message.get_tlv(LinkTlvs.IFACE2_IP4.value), + ip4_mask=message.get_tlv(LinkTlvs.IFACE2_IP4_MASK.value), + ip6=message.get_tlv(LinkTlvs.IFACE2_IP6.value), + ip6_mask=message.get_tlv(LinkTlvs.IFACE2_IP6_MASK.value), ) link_type = LinkTypes.WIRED link_type_value = message.get_tlv(LinkTlvs.TYPE.value) @@ -789,16 +789,12 @@ class CoreHandler(socketserver.BaseRequestHandler): options.opaque = message.get_tlv(LinkTlvs.OPAQUE.value) if message.flags & MessageFlags.ADD.value: - self.session.add_link( - node1_id, node2_id, interface1_data, interface2_data, options - ) + self.session.add_link(node1_id, node2_id, iface1_data, iface2_data, options) elif message.flags & MessageFlags.DELETE.value: - self.session.delete_link( - node1_id, node2_id, interface1_data.id, interface2_data.id - ) + self.session.delete_link(node1_id, node2_id, iface1_data.id, iface2_data.id) else: self.session.update_link( - node1_id, node2_id, interface1_data.id, interface2_data.id, options + node1_id, node2_id, iface1_data.id, iface2_data.id, options ) return () @@ -1008,7 +1004,7 @@ class CoreHandler(socketserver.BaseRequestHandler): possible_values=message.get_tlv(ConfigTlvs.POSSIBLE_VALUES.value), groups=message.get_tlv(ConfigTlvs.GROUPS.value), session=message.get_tlv(ConfigTlvs.SESSION.value), - interface_number=message.get_tlv(ConfigTlvs.INTERFACE_NUMBER.value), + iface_id=message.get_tlv(ConfigTlvs.IFACE_ID.value), network_id=message.get_tlv(ConfigTlvs.NETWORK_ID.value), opaque=message.get_tlv(ConfigTlvs.OPAQUE.value), ) @@ -1325,11 +1321,11 @@ class CoreHandler(socketserver.BaseRequestHandler): replies = [] node_id = config_data.node object_name = config_data.object - interface_id = config_data.interface_number + iface_id = config_data.iface_id values_str = config_data.data_values - if interface_id is not None: - node_id = node_id * 1000 + interface_id + if iface_id is not None: + node_id = node_id * 1000 + iface_id logging.debug( "received configure message for %s nodenum: %s", object_name, node_id @@ -1375,11 +1371,11 @@ class CoreHandler(socketserver.BaseRequestHandler): replies = [] node_id = config_data.node object_name = config_data.object - interface_id = config_data.interface_number + iface_id = config_data.iface_id values_str = config_data.data_values - if interface_id is not None: - node_id = node_id * 1000 + interface_id + if iface_id is not None: + node_id = node_id * 1000 + iface_id logging.debug( "received configure message for %s nodenum: %s", object_name, node_id @@ -1407,11 +1403,11 @@ class CoreHandler(socketserver.BaseRequestHandler): replies = [] node_id = config_data.node object_name = config_data.object - interface_id = config_data.interface_number + iface_id = config_data.iface_id values_str = config_data.data_values - if interface_id is not None: - node_id = node_id * 1000 + interface_id + if iface_id is not None: + node_id = node_id * 1000 + iface_id logging.debug( "received configure message for %s nodenum: %s", object_name, node_id @@ -1505,7 +1501,7 @@ class CoreHandler(socketserver.BaseRequestHandler): return () - def handle_interface_message(self, message): + def handle_iface_message(self, message): """ Interface Message handler. @@ -1950,7 +1946,7 @@ class CoreUdpHandler(CoreHandler): MessageTypes.REGISTER.value: self.handle_register_message, MessageTypes.CONFIG.value: self.handle_config_message, MessageTypes.FILE.value: self.handle_file_message, - MessageTypes.INTERFACE.value: self.handle_interface_message, + MessageTypes.INTERFACE.value: self.handle_iface_message, MessageTypes.EVENT.value: self.handle_event_message, MessageTypes.SESSION.value: self.handle_session_message, } diff --git a/daemon/core/api/tlv/dataconversion.py b/daemon/core/api/tlv/dataconversion.py index 876e72a5..cd10ef04 100644 --- a/daemon/core/api/tlv/dataconversion.py +++ b/daemon/core/api/tlv/dataconversion.py @@ -75,7 +75,7 @@ def convert_config(config_data): (ConfigTlvs.POSSIBLE_VALUES, config_data.possible_values), (ConfigTlvs.GROUPS, config_data.groups), (ConfigTlvs.SESSION, session), - (ConfigTlvs.INTERFACE_NUMBER, config_data.interface_number), + (ConfigTlvs.IFACE_ID, config_data.iface_id), (ConfigTlvs.NETWORK_ID, config_data.network_id), (ConfigTlvs.OPAQUE, config_data.opaque), ], diff --git a/daemon/core/api/tlv/enumerations.py b/daemon/core/api/tlv/enumerations.py index 0efb7c99..b4ec254a 100644 --- a/daemon/core/api/tlv/enumerations.py +++ b/daemon/core/api/tlv/enumerations.py @@ -72,18 +72,18 @@ class LinkTlvs(Enum): EMULATION_ID = 0x23 NETWORK_ID = 0x24 KEY = 0x25 - INTERFACE1_NUMBER = 0x30 - INTERFACE1_IP4 = 0x31 - INTERFACE1_IP4_MASK = 0x32 - INTERFACE1_MAC = 0x33 - INTERFACE1_IP6 = 0x34 - INTERFACE1_IP6_MASK = 0x35 - INTERFACE2_NUMBER = 0x36 - INTERFACE2_IP4 = 0x37 - INTERFACE2_IP4_MASK = 0x38 - INTERFACE2_MAC = 0x39 - INTERFACE2_IP6 = 0x40 - INTERFACE2_IP6_MASK = 0x41 + IFACE1_NUMBER = 0x30 + IFACE1_IP4 = 0x31 + IFACE1_IP4_MASK = 0x32 + IFACE1_MAC = 0x33 + IFACE1_IP6 = 0x34 + IFACE1_IP6_MASK = 0x35 + IFACE2_NUMBER = 0x36 + IFACE2_IP4 = 0x37 + IFACE2_IP4_MASK = 0x38 + IFACE2_MAC = 0x39 + IFACE2_IP6 = 0x40 + IFACE2_IP6_MASK = 0x41 INTERFACE1_NAME = 0x42 INTERFACE2_NAME = 0x43 OPAQUE = 0x50 @@ -118,7 +118,7 @@ class ConfigTlvs(Enum): POSSIBLE_VALUES = 0x08 GROUPS = 0x09 SESSION = 0x0A - INTERFACE_NUMBER = 0x0B + IFACE_ID = 0x0B NETWORK_ID = 0x24 OPAQUE = 0x50 diff --git a/daemon/core/configservices/frrservices/services.py b/daemon/core/configservices/frrservices/services.py index c4502f86..8764e32c 100644 --- a/daemon/core/configservices/frrservices/services.py +++ b/daemon/core/configservices/frrservices/services.py @@ -13,33 +13,33 @@ from core.nodes.network import WlanNode GROUP = "FRR" -def has_mtu_mismatch(ifc: CoreInterface) -> bool: +def has_mtu_mismatch(iface: CoreInterface) -> bool: """ Helper to detect MTU mismatch and add the appropriate FRR mtu-ignore command. This is needed when e.g. a node is linked via a GreTap device. """ - if ifc.mtu != 1500: + if iface.mtu != 1500: return True - if not ifc.net: + if not iface.net: return False - for i in ifc.net.netifs(): - if i.mtu != ifc.mtu: + for iface in iface.net.get_ifaces(): + if iface.mtu != iface.mtu: return True return False -def get_min_mtu(ifc): +def get_min_mtu(iface): """ Helper to discover the minimum MTU of interfaces linked with the given interface. """ - mtu = ifc.mtu - if not ifc.net: + mtu = iface.mtu + if not iface.net: return mtu - for i in ifc.net.netifs(): - if i.mtu < mtu: - mtu = i.mtu + for iface in iface.net.get_ifaces(): + if iface.mtu < mtu: + mtu = iface.mtu return mtu @@ -47,10 +47,8 @@ def get_router_id(node: CoreNodeBase) -> str: """ Helper to return the first IPv4 address of a node as its router ID. """ - for ifc in node.netifs(): - if getattr(ifc, "control", False): - continue - for a in ifc.addrlist: + for iface in node.get_ifaces(control=False): + for a in iface.addrlist: a = a.split("/")[0] if netaddr.valid_ipv4(a): return a @@ -97,25 +95,25 @@ class FRRZebra(ConfigService): want_ip6 = True services.append(service) - interfaces = [] - for ifc in self.node.netifs(): + ifaces = [] + for iface in self.node.get_ifaces(): ip4s = [] ip6s = [] - for x in ifc.addrlist: + for x in iface.addrlist: addr = x.split("/")[0] if netaddr.valid_ipv4(addr): ip4s.append(x) else: ip6s.append(x) - is_control = getattr(ifc, "control", False) - interfaces.append((ifc, ip4s, ip6s, is_control)) + is_control = getattr(iface, "control", False) + ifaces.append((iface, ip4s, ip6s, is_control)) return dict( frr_conf=frr_conf, frr_sbin_search=frr_sbin_search, frr_bin_search=frr_bin_search, frr_state_dir=constants.FRR_STATE_DIR, - interfaces=interfaces, + ifaces=ifaces, want_ip4=want_ip4, want_ip6=want_ip6, services=services, @@ -138,7 +136,7 @@ class FrrService(abc.ABC): ipv6_routing = False @abc.abstractmethod - def frr_interface_config(self, ifc: CoreInterface) -> str: + def frr_iface_config(self, iface: CoreInterface) -> str: raise NotImplementedError @abc.abstractmethod @@ -162,10 +160,8 @@ class FRROspfv2(FrrService, ConfigService): def frr_config(self) -> str: router_id = get_router_id(self.node) addresses = [] - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue - for a in ifc.addrlist: + for iface in self.node.get_ifaces(control=False): + for a in iface.addrlist: addr = a.split("/")[0] if netaddr.valid_ipv4(addr): addresses.append(a) @@ -180,8 +176,8 @@ class FRROspfv2(FrrService, ConfigService): """ return self.render_text(text, data) - def frr_interface_config(self, ifc: CoreInterface) -> str: - if has_mtu_mismatch(ifc): + def frr_iface_config(self, iface: CoreInterface) -> str: + if has_mtu_mismatch(iface): return "ip ospf mtu-ignore" else: return "" @@ -203,10 +199,8 @@ class FRROspfv3(FrrService, ConfigService): def frr_config(self) -> str: router_id = get_router_id(self.node) ifnames = [] - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue - ifnames.append(ifc.name) + for iface in self.node.get_ifaces(control=False): + ifnames.append(iface.name) data = dict(router_id=router_id, ifnames=ifnames) text = """ router ospf6 @@ -218,9 +212,9 @@ class FRROspfv3(FrrService, ConfigService): """ return self.render_text(text, data) - def frr_interface_config(self, ifc: CoreInterface) -> str: - mtu = get_min_mtu(ifc) - if mtu < ifc.mtu: + def frr_iface_config(self, iface: CoreInterface) -> str: + mtu = get_min_mtu(iface) + if mtu < iface.mtu: return f"ipv6 ospf6 ifmtu {mtu}" else: return "" @@ -254,7 +248,7 @@ class FRRBgp(FrrService, ConfigService): """ return self.clean_text(text) - def frr_interface_config(self, ifc: CoreInterface) -> str: + def frr_iface_config(self, iface: CoreInterface) -> str: return "" @@ -279,7 +273,7 @@ class FRRRip(FrrService, ConfigService): """ return self.clean_text(text) - def frr_interface_config(self, ifc: CoreInterface) -> str: + def frr_iface_config(self, iface: CoreInterface) -> str: return "" @@ -304,7 +298,7 @@ class FRRRipng(FrrService, ConfigService): """ return self.clean_text(text) - def frr_interface_config(self, ifc: CoreInterface) -> str: + def frr_iface_config(self, iface: CoreInterface) -> str: return "" @@ -321,10 +315,8 @@ class FRRBabel(FrrService, ConfigService): def frr_config(self) -> str: ifnames = [] - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue - ifnames.append(ifc.name) + for iface in self.node.get_ifaces(control=False): + ifnames.append(iface.name) text = """ router babel % for ifname in ifnames: @@ -337,8 +329,8 @@ class FRRBabel(FrrService, ConfigService): data = dict(ifnames=ifnames) return self.render_text(text, data) - def frr_interface_config(self, ifc: CoreInterface) -> str: - if isinstance(ifc.net, (WlanNode, EmaneNet)): + def frr_iface_config(self, iface: CoreInterface) -> str: + if isinstance(iface.net, (WlanNode, EmaneNet)): text = """ babel wireless no babel split-horizon @@ -363,9 +355,9 @@ class FRRpimd(FrrService, ConfigService): def frr_config(self) -> str: ifname = "eth0" - for ifc in self.node.netifs(): - if ifc.name != "lo": - ifname = ifc.name + for iface in self.node.get_ifaces(): + if iface.name != "lo": + ifname = iface.name break text = f""" @@ -382,7 +374,7 @@ class FRRpimd(FrrService, ConfigService): """ return self.clean_text(text) - def frr_interface_config(self, ifc: CoreInterface) -> str: + def frr_iface_config(self, iface: CoreInterface) -> str: text = """ ip mfea ip igmp diff --git a/daemon/core/configservices/frrservices/templates/frr.conf b/daemon/core/configservices/frrservices/templates/frr.conf index 748c8692..8e036136 100644 --- a/daemon/core/configservices/frrservices/templates/frr.conf +++ b/daemon/core/configservices/frrservices/templates/frr.conf @@ -1,5 +1,5 @@ -% for ifc, ip4s, ip6s, is_control in interfaces: -interface ${ifc.name} +% for iface, ip4s, ip6s, is_control in ifaces: +interface ${iface.name} % if want_ip4: % for addr in ip4s: ip address ${addr} @@ -12,7 +12,7 @@ interface ${ifc.name} % endif % if not is_control: % for service in services: - % for line in service.frr_interface_config(ifc).split("\n"): + % for line in service.frr_iface_config(iface).split("\n"): ${line} % endfor % endfor diff --git a/daemon/core/configservices/frrservices/templates/frrboot.sh b/daemon/core/configservices/frrservices/templates/frrboot.sh index 3c14cd1a..db47b6d1 100644 --- a/daemon/core/configservices/frrservices/templates/frrboot.sh +++ b/daemon/core/configservices/frrservices/templates/frrboot.sh @@ -98,8 +98,8 @@ confcheck bootfrr # reset interfaces -% for ifc, _, _ , _ in interfaces: -ip link set dev ${ifc.name} down +% for iface, _, _ , _ in ifaces: +ip link set dev ${iface.name} down sleep 1 -ip link set dev ${ifc.name} up +ip link set dev ${iface.name} up % endfor diff --git a/daemon/core/configservices/nrlservices/services.py b/daemon/core/configservices/nrlservices/services.py index 3dddf1ba..ca95b8f6 100644 --- a/daemon/core/configservices/nrlservices/services.py +++ b/daemon/core/configservices/nrlservices/services.py @@ -24,8 +24,8 @@ class MgenSinkService(ConfigService): def data(self) -> Dict[str, Any]: ifnames = [] - for ifc in self.node.netifs(): - name = utils.sysctl_devname(ifc.name) + for iface in self.node.get_ifaces(): + name = utils.sysctl_devname(iface.name) ifnames.append(name) return dict(ifnames=ifnames) @@ -47,10 +47,8 @@ class NrlNhdp(ConfigService): def data(self) -> Dict[str, Any]: has_smf = "SMF" in self.node.config_services ifnames = [] - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue - ifnames.append(ifc.name) + for iface in self.node.get_ifaces(control=False): + ifnames.append(iface.name) return dict(has_smf=has_smf, ifnames=ifnames) @@ -74,13 +72,11 @@ class NrlSmf(ConfigService): has_olsr = "OLSR" in self.node.config_services ifnames = [] ip4_prefix = None - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue - ifnames.append(ifc.name) + for iface in self.node.get_ifaces(control=False): + ifnames.append(iface.name) if ip4_prefix: continue - for a in ifc.addrlist: + for a in iface.addrlist: a = a.split("/")[0] if netaddr.valid_ipv4(a): ip4_prefix = f"{a}/{24}" @@ -112,10 +108,8 @@ class NrlOlsr(ConfigService): has_smf = "SMF" in self.node.config_services has_zebra = "zebra" in self.node.config_services ifname = None - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue - ifname = ifc.name + for iface in self.node.get_ifaces(control=False): + ifname = iface.name break return dict(has_smf=has_smf, has_zebra=has_zebra, ifname=ifname) @@ -137,10 +131,8 @@ class NrlOlsrv2(ConfigService): def data(self) -> Dict[str, Any]: has_smf = "SMF" in self.node.config_services ifnames = [] - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue - ifnames.append(ifc.name) + for iface in self.node.get_ifaces(control=False): + ifnames.append(iface.name) return dict(has_smf=has_smf, ifnames=ifnames) @@ -161,10 +153,8 @@ class OlsrOrg(ConfigService): def data(self) -> Dict[str, Any]: has_smf = "SMF" in self.node.config_services ifnames = [] - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue - ifnames.append(ifc.name) + for iface in self.node.get_ifaces(control=False): + ifnames.append(iface.name) return dict(has_smf=has_smf, ifnames=ifnames) @@ -199,12 +189,10 @@ class Arouted(ConfigService): def data(self) -> Dict[str, Any]: ip4_prefix = None - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue + for iface in self.node.get_ifaces(control=False): if ip4_prefix: continue - for a in ifc.addrlist: + for a in iface.addrlist: a = a.split("/")[0] if netaddr.valid_ipv4(a): ip4_prefix = f"{a}/{24}" diff --git a/daemon/core/configservices/nrlservices/templates/nrlnhdp.sh b/daemon/core/configservices/nrlservices/templates/nrlnhdp.sh index 00b7e11d..4513dfe9 100644 --- a/daemon/core/configservices/nrlservices/templates/nrlnhdp.sh +++ b/daemon/core/configservices/nrlservices/templates/nrlnhdp.sh @@ -1,7 +1,7 @@ <% - interfaces = "-i " + " -i ".join(ifnames) + ifaces = "-i " + " -i ".join(ifnames) smf = "" if has_smf: smf = "-flooding ecds -smfClient %s_smf" % node.name %> -nrlnhdp -l /var/log/nrlnhdp.log -rpipe ${node.name}_nhdp ${smf} ${interfaces} +nrlnhdp -l /var/log/nrlnhdp.log -rpipe ${node.name}_nhdp ${smf} ${ifaces} diff --git a/daemon/core/configservices/nrlservices/templates/nrlolsrv2.sh b/daemon/core/configservices/nrlservices/templates/nrlolsrv2.sh index d7a8d3b6..81196e26 100644 --- a/daemon/core/configservices/nrlservices/templates/nrlolsrv2.sh +++ b/daemon/core/configservices/nrlservices/templates/nrlolsrv2.sh @@ -1,7 +1,7 @@ <% - interfaces = "-i " + " -i ".join(ifnames) + ifaces = "-i " + " -i ".join(ifnames) smf = "" if has_smf: smf = "-flooding ecds -smfClient %s_smf" % node.name %> -nrlolsrv2 -l /var/log/nrlolsrv2.log -rpipe ${node.name}_olsrv2 -p olsr ${smf} ${interfaces} +nrlolsrv2 -l /var/log/nrlolsrv2.log -rpipe ${node.name}_olsrv2 -p olsr ${smf} ${ifaces} diff --git a/daemon/core/configservices/nrlservices/templates/olsrd.sh b/daemon/core/configservices/nrlservices/templates/olsrd.sh index 076f049b..3040ca6b 100644 --- a/daemon/core/configservices/nrlservices/templates/olsrd.sh +++ b/daemon/core/configservices/nrlservices/templates/olsrd.sh @@ -1,4 +1,4 @@ <% - interfaces = "-i " + " -i ".join(ifnames) + ifaces = "-i " + " -i ".join(ifnames) %> -olsrd ${interfaces} +olsrd ${ifaces} diff --git a/daemon/core/configservices/nrlservices/templates/startsmf.sh b/daemon/core/configservices/nrlservices/templates/startsmf.sh index 67fc0fe6..921568de 100644 --- a/daemon/core/configservices/nrlservices/templates/startsmf.sh +++ b/daemon/core/configservices/nrlservices/templates/startsmf.sh @@ -1,5 +1,5 @@ <% - interfaces = ",".join(ifnames) + ifaces = ",".join(ifnames) arouted = "" if has_arouted: arouted = "tap %s_tap unicast %s push lo,%s resequence on" % (node.name, ip4_prefix, ifnames[0]) @@ -12,4 +12,4 @@ %> #!/bin/sh # auto-generated by NrlSmf service -nrlsmf instance ${node.name}_smf ${interfaces} ${arouted} ${flood} hash MD5 log /var/log/nrlsmf.log < /dev/null > /dev/null 2>&1 & +nrlsmf instance ${node.name}_smf ${ifaces} ${arouted} ${flood} hash MD5 log /var/log/nrlsmf.log < /dev/null > /dev/null 2>&1 & diff --git a/daemon/core/configservices/quaggaservices/services.py b/daemon/core/configservices/quaggaservices/services.py index 32ce99be..19e21476 100644 --- a/daemon/core/configservices/quaggaservices/services.py +++ b/daemon/core/configservices/quaggaservices/services.py @@ -14,33 +14,33 @@ from core.nodes.network import WlanNode GROUP = "Quagga" -def has_mtu_mismatch(ifc: CoreInterface) -> bool: +def has_mtu_mismatch(iface: CoreInterface) -> bool: """ Helper to detect MTU mismatch and add the appropriate OSPF mtu-ignore command. This is needed when e.g. a node is linked via a GreTap device. """ - if ifc.mtu != 1500: + if iface.mtu != 1500: return True - if not ifc.net: + if not iface.net: return False - for i in ifc.net.netifs(): - if i.mtu != ifc.mtu: + for iface in iface.net.get_ifaces(): + if iface.mtu != iface.mtu: return True return False -def get_min_mtu(ifc): +def get_min_mtu(iface: CoreInterface): """ Helper to discover the minimum MTU of interfaces linked with the given interface. """ - mtu = ifc.mtu - if not ifc.net: + mtu = iface.mtu + if not iface.net: return mtu - for i in ifc.net.netifs(): - if i.mtu < mtu: - mtu = i.mtu + for iface in iface.net.get_ifaces(): + if iface.mtu < mtu: + mtu = iface.mtu return mtu @@ -48,10 +48,8 @@ def get_router_id(node: CoreNodeBase) -> str: """ Helper to return the first IPv4 address of a node as its router ID. """ - for ifc in node.netifs(): - if getattr(ifc, "control", False): - continue - for a in ifc.addrlist: + for iface in node.get_ifaces(control=False): + for a in iface.addrlist: a = a.split("/")[0] if netaddr.valid_ipv4(a): return a @@ -98,25 +96,25 @@ class Zebra(ConfigService): want_ip6 = True services.append(service) - interfaces = [] - for ifc in self.node.netifs(): + ifaces = [] + for iface in self.node.get_ifaces(): ip4s = [] ip6s = [] - for x in ifc.addrlist: + for x in iface.addrlist: addr = x.split("/")[0] if netaddr.valid_ipv4(addr): ip4s.append(x) else: ip6s.append(x) - is_control = getattr(ifc, "control", False) - interfaces.append((ifc, ip4s, ip6s, is_control)) + is_control = getattr(iface, "control", False) + ifaces.append((iface, ip4s, ip6s, is_control)) return dict( quagga_bin_search=quagga_bin_search, quagga_sbin_search=quagga_sbin_search, quagga_state_dir=quagga_state_dir, quagga_conf=quagga_conf, - interfaces=interfaces, + ifaces=ifaces, want_ip4=want_ip4, want_ip6=want_ip6, services=services, @@ -139,7 +137,7 @@ class QuaggaService(abc.ABC): ipv6_routing = False @abc.abstractmethod - def quagga_interface_config(self, ifc: CoreInterface) -> str: + def quagga_iface_config(self, iface: CoreInterface) -> str: raise NotImplementedError @abc.abstractmethod @@ -159,8 +157,8 @@ class Ospfv2(QuaggaService, ConfigService): shutdown = ["killall ospfd"] ipv4_routing = True - def quagga_interface_config(self, ifc: CoreInterface) -> str: - if has_mtu_mismatch(ifc): + def quagga_iface_config(self, iface: CoreInterface) -> str: + if has_mtu_mismatch(iface): return "ip ospf mtu-ignore" else: return "" @@ -168,10 +166,8 @@ class Ospfv2(QuaggaService, ConfigService): def quagga_config(self) -> str: router_id = get_router_id(self.node) addresses = [] - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue - for a in ifc.addrlist: + for iface in self.node.get_ifaces(control=False): + for a in iface.addrlist: addr = a.split("/")[0] if netaddr.valid_ipv4(addr): addresses.append(a) @@ -200,9 +196,9 @@ class Ospfv3(QuaggaService, ConfigService): ipv4_routing = True ipv6_routing = True - def quagga_interface_config(self, ifc: CoreInterface) -> str: - mtu = get_min_mtu(ifc) - if mtu < ifc.mtu: + def quagga_iface_config(self, iface: CoreInterface) -> str: + mtu = get_min_mtu(iface) + if mtu < iface.mtu: return f"ipv6 ospf6 ifmtu {mtu}" else: return "" @@ -210,10 +206,8 @@ class Ospfv3(QuaggaService, ConfigService): def quagga_config(self) -> str: router_id = get_router_id(self.node) ifnames = [] - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue - ifnames.append(ifc.name) + for iface in self.node.get_ifaces(control=False): + ifnames.append(iface.name) data = dict(router_id=router_id, ifnames=ifnames) text = """ router ospf6 @@ -238,14 +232,14 @@ class Ospfv3mdr(Ospfv3): name = "OSPFv3MDR" def data(self) -> Dict[str, Any]: - for ifc in self.node.netifs(): - is_wireless = isinstance(ifc.net, (WlanNode, EmaneNet)) + for iface in self.node.get_ifaces(): + is_wireless = isinstance(iface.net, (WlanNode, EmaneNet)) logging.info("MDR wireless: %s", is_wireless) return dict() - def quagga_interface_config(self, ifc: CoreInterface) -> str: - config = super().quagga_interface_config(ifc) - if isinstance(ifc.net, (WlanNode, EmaneNet)): + def quagga_iface_config(self, iface: CoreInterface) -> str: + config = super().quagga_iface_config(iface) + if isinstance(iface.net, (WlanNode, EmaneNet)): config = self.clean_text( f""" {config} @@ -277,7 +271,7 @@ class Bgp(QuaggaService, ConfigService): def quagga_config(self) -> str: return "" - def quagga_interface_config(self, ifc: CoreInterface) -> str: + def quagga_iface_config(self, iface: CoreInterface) -> str: router_id = get_router_id(self.node) text = f""" ! BGP configuration @@ -313,7 +307,7 @@ class Rip(QuaggaService, ConfigService): """ return self.clean_text(text) - def quagga_interface_config(self, ifc: CoreInterface) -> str: + def quagga_iface_config(self, iface: CoreInterface) -> str: return "" @@ -338,7 +332,7 @@ class Ripng(QuaggaService, ConfigService): """ return self.clean_text(text) - def quagga_interface_config(self, ifc: CoreInterface) -> str: + def quagga_iface_config(self, iface: CoreInterface) -> str: return "" @@ -355,10 +349,8 @@ class Babel(QuaggaService, ConfigService): def quagga_config(self) -> str: ifnames = [] - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue - ifnames.append(ifc.name) + for iface in self.node.get_ifaces(control=False): + ifnames.append(iface.name) text = """ router babel % for ifname in ifnames: @@ -371,8 +363,8 @@ class Babel(QuaggaService, ConfigService): data = dict(ifnames=ifnames) return self.render_text(text, data) - def quagga_interface_config(self, ifc: CoreInterface) -> str: - if isinstance(ifc.net, (WlanNode, EmaneNet)): + def quagga_iface_config(self, iface: CoreInterface) -> str: + if isinstance(iface.net, (WlanNode, EmaneNet)): text = """ babel wireless no babel split-horizon @@ -397,9 +389,9 @@ class Xpimd(QuaggaService, ConfigService): def quagga_config(self) -> str: ifname = "eth0" - for ifc in self.node.netifs(): - if ifc.name != "lo": - ifname = ifc.name + for iface in self.node.get_ifaces(): + if iface.name != "lo": + ifname = iface.name break text = f""" @@ -416,7 +408,7 @@ class Xpimd(QuaggaService, ConfigService): """ return self.clean_text(text) - def quagga_interface_config(self, ifc: CoreInterface) -> str: + def quagga_iface_config(self, iface: CoreInterface) -> str: text = """ ip mfea ip pim diff --git a/daemon/core/configservices/quaggaservices/templates/Quagga.conf b/daemon/core/configservices/quaggaservices/templates/Quagga.conf index 853b1707..1d69838f 100644 --- a/daemon/core/configservices/quaggaservices/templates/Quagga.conf +++ b/daemon/core/configservices/quaggaservices/templates/Quagga.conf @@ -1,5 +1,5 @@ -% for ifc, ip4s, ip6s, is_control in interfaces: -interface ${ifc.name} +% for iface, ip4s, ip6s, is_control in ifaces: +interface ${iface.name} % if want_ip4: % for addr in ip4s: ip address ${addr} @@ -12,7 +12,7 @@ interface ${ifc.name} % endif % if not is_control: % for service in services: - % for line in service.quagga_interface_config(ifc).split("\n"): + % for line in service.quagga_iface_config(iface).split("\n"): ${line} % endfor % endfor diff --git a/daemon/core/configservices/sercurityservices/services.py b/daemon/core/configservices/sercurityservices/services.py index 17f081cd..6e92bf62 100644 --- a/daemon/core/configservices/sercurityservices/services.py +++ b/daemon/core/configservices/sercurityservices/services.py @@ -78,10 +78,8 @@ class VpnServer(ConfigService): def data(self) -> Dict[str, Any]: address = None - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue - for x in ifc.addrlist: + for iface in self.node.get_ifaces(control=False): + for x in iface.addrlist: addr = x.split("/")[0] if netaddr.valid_ipv4(addr): address = addr @@ -134,8 +132,6 @@ class Nat(ConfigService): def data(self) -> Dict[str, Any]: ifnames = [] - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue - ifnames.append(ifc.name) + for iface in self.node.get_ifaces(control=False): + ifnames.append(iface.name) return dict(ifnames=ifnames) diff --git a/daemon/core/configservices/utilservices/services.py b/daemon/core/configservices/utilservices/services.py index 8ddf1cc7..5aa3bb54 100644 --- a/daemon/core/configservices/utilservices/services.py +++ b/daemon/core/configservices/utilservices/services.py @@ -25,10 +25,10 @@ class DefaultRouteService(ConfigService): def data(self) -> Dict[str, Any]: # only add default routes for linked routing nodes routes = [] - netifs = self.node.netifs(sort=True) - if netifs: - netif = netifs[0] - for x in netif.addrlist: + ifaces = self.node.get_ifaces() + if ifaces: + iface = ifaces[0] + for x in iface.addrlist: net = netaddr.IPNetwork(x).cidr if net.size > 1: router = net[1] @@ -52,10 +52,8 @@ class DefaultMulticastRouteService(ConfigService): def data(self) -> Dict[str, Any]: ifname = None - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue - ifname = ifc.name + for iface in self.node.get_ifaces(control=False): + ifname = iface.name break return dict(ifname=ifname) @@ -76,10 +74,8 @@ class StaticRouteService(ConfigService): def data(self) -> Dict[str, Any]: routes = [] - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue - for x in ifc.addrlist: + for iface in self.node.get_ifaces(control=False): + for x in iface.addrlist: addr = x.split("/")[0] if netaddr.valid_ipv6(addr): dst = "3ffe:4::/64" @@ -107,8 +103,8 @@ class IpForwardService(ConfigService): def data(self) -> Dict[str, Any]: devnames = [] - for ifc in self.node.netifs(): - devname = utils.sysctl_devname(ifc.name) + for iface in self.node.get_ifaces(): + devname = utils.sysctl_devname(iface.name) devnames.append(devname) return dict(devnames=devnames) @@ -151,10 +147,8 @@ class DhcpService(ConfigService): def data(self) -> Dict[str, Any]: subnets = [] - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue - for x in ifc.addrlist: + for iface in self.node.get_ifaces(control=False): + for x in iface.addrlist: addr = x.split("/")[0] if netaddr.valid_ipv4(addr): net = netaddr.IPNetwork(x) @@ -182,10 +176,8 @@ class DhcpClientService(ConfigService): def data(self) -> Dict[str, Any]: ifnames = [] - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue - ifnames.append(ifc.name) + for iface in self.node.get_ifaces(control=False): + ifnames.append(iface.name) return dict(ifnames=ifnames) @@ -220,10 +212,8 @@ class PcapService(ConfigService): def data(self) -> Dict[str, Any]: ifnames = [] - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue - ifnames.append(ifc.name) + for iface in self.node.get_ifaces(control=False): + ifnames.append(iface.name) return dict() @@ -242,19 +232,17 @@ class RadvdService(ConfigService): modes = {} def data(self) -> Dict[str, Any]: - interfaces = [] - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue + ifaces = [] + for iface in self.node.get_ifaces(control=False): prefixes = [] - for x in ifc.addrlist: + for x in iface.addrlist: addr = x.split("/")[0] if netaddr.valid_ipv6(addr): prefixes.append(x) if not prefixes: continue - interfaces.append((ifc.name, prefixes)) - return dict(interfaces=interfaces) + ifaces.append((iface.name, prefixes)) + return dict(ifaces=ifaces) class AtdService(ConfigService): @@ -294,9 +282,7 @@ class HttpService(ConfigService): modes = {} def data(self) -> Dict[str, Any]: - interfaces = [] - for ifc in self.node.netifs(): - if getattr(ifc, "control", False): - continue - interfaces.append(ifc) - return dict(interfaces=interfaces) + ifaces = [] + for iface in self.node.get_ifaces(control=False): + ifaces.append(iface) + return dict(ifaces=ifaces) diff --git a/daemon/core/configservices/utilservices/templates/index.html b/daemon/core/configservices/utilservices/templates/index.html index aaf9d9fa..bed270ae 100644 --- a/daemon/core/configservices/utilservices/templates/index.html +++ b/daemon/core/configservices/utilservices/templates/index.html @@ -5,8 +5,8 @@

This is the default web page for this server.

The web server software is running but no content has been added, yet.

    -% for ifc in interfaces: -
  • ${ifc.name} - ${ifc.addrlist}
  • +% for iface in ifaces: +
  • ${iface.name} - ${iface.addrlist}
  • % endfor
diff --git a/daemon/core/emane/commeffect.py b/daemon/core/emane/commeffect.py index 21252b6f..0f441d76 100644 --- a/daemon/core/emane/commeffect.py +++ b/daemon/core/emane/commeffect.py @@ -63,7 +63,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): return [ConfigGroup("CommEffect SHIM Parameters", 1, len(cls.configurations()))] def build_xml_files( - self, config: Dict[str, str], interface: CoreInterface = None + self, config: Dict[str, str], iface: CoreInterface = None ) -> None: """ Build the necessary nem and commeffect XMLs in the given path. @@ -72,17 +72,17 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): nXXemane_commeffectnem.xml, nXXemane_commeffectshim.xml are used. :param config: emane model configuration for the node and interface - :param interface: interface for the emane node + :param iface: interface for the emane node :return: nothing """ # retrieve xml names - nem_name = emanexml.nem_file_name(self, interface) - shim_name = emanexml.shim_file_name(self, interface) + nem_name = emanexml.nem_file_name(self, iface) + shim_name = emanexml.shim_file_name(self, iface) # create and write nem document nem_element = etree.Element("nem", name=f"{self.name} NEM", type="unstructured") transport_type = TransportType.VIRTUAL - if interface and interface.transport_type == TransportType.RAW: + if iface and iface.transport_type == TransportType.RAW: transport_type = TransportType.RAW transport_file = emanexml.transport_file_name(self.id, transport_type) etree.SubElement(nem_element, "transport", definition=transport_file) @@ -115,7 +115,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): emanexml.create_file(shim_element, "shim", shim_file) def linkconfig( - self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None + self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None ) -> None: """ Generate CommEffect events when a Link Message is received having @@ -126,7 +126,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): logging.warning("%s: EMANE event service unavailable", self.name) return - if netif is None or netif2 is None: + if iface is None or iface2 is None: logging.warning("%s: missing NEM information", self.name) return @@ -134,8 +134,8 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): # TODO: may want to split out seconds portion of delay and jitter event = CommEffectEvent() emane_node = self.session.get_node(self.id, EmaneNet) - nemid = emane_node.getnemid(netif) - nemid2 = emane_node.getnemid(netif2) + nemid = emane_node.getnemid(iface) + nemid2 = emane_node.getnemid(iface2) logging.info("sending comm effect event") event.append( nemid, diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index cb978cb9..58b85080 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -111,41 +111,39 @@ class EmaneManager(ModelManager): self.event_device: Optional[str] = None self.emane_check() - def getifcconfig( - self, node_id: int, interface: CoreInterface, model_name: str + def get_iface_config( + self, node_id: int, iface: CoreInterface, model_name: str ) -> Dict[str, str]: """ Retrieve interface configuration or node configuration if not provided. :param node_id: node id - :param interface: node interface + :param iface: node interface :param model_name: model to get configuration for :return: node/interface model configuration """ # use the network-wide config values or interface(NEM)-specific values? - if interface is None: + if iface is None: return self.get_configs(node_id=node_id, config_type=model_name) else: # don"t use default values when interface config is the same as net - # note here that using ifc.node.id as key allows for only one type + # note here that using iface.node.id as key allows for only one type # of each model per node; # TODO: use both node and interface as key - # Adamson change: first check for iface config keyed by "node:ifc.name" + # Adamson change: first check for iface config keyed by "node:iface.name" # (so that nodes w/ multiple interfaces of same conftype can have # different configs for each separate interface) - key = 1000 * interface.node.id - if interface.netindex is not None: - key += interface.netindex + key = 1000 * iface.node.id + if iface.node_id is not None: + key += iface.node_id # try retrieve interface specific configuration, avoid getting defaults config = self.get_configs(node_id=key, config_type=model_name) # otherwise retrieve the interfaces node configuration, avoid using defaults if not config: - config = self.get_configs( - node_id=interface.node.id, config_type=model_name - ) + config = self.get_configs(node_id=iface.node.id, config_type=model_name) # get non interface config, when none found if not config: @@ -265,8 +263,8 @@ class EmaneManager(ModelManager): # assumes self._objslock already held nodes = set() for emane_net in self._emane_nets.values(): - for netif in emane_net.netifs(): - nodes.add(netif.node) + for iface in emane_net.get_ifaces(): + nodes.add(iface.node) return nodes def setup(self) -> int: @@ -352,13 +350,13 @@ class EmaneManager(ModelManager): if self.numnems() > 0: self.startdaemons() - self.installnetifs() + self.install_ifaces() for node_id in self._emane_nets: emane_node = self._emane_nets[node_id] - for netif in emane_node.netifs(): + for iface in emane_node.get_ifaces(): nems.append( - (netif.node.name, netif.name, emane_node.getnemid(netif)) + (iface.node.name, iface.name, emane_node.getnemid(iface)) ) if nems: @@ -392,8 +390,8 @@ class EmaneManager(ModelManager): emane_node.name, ) emane_node.model.post_startup() - for netif in emane_node.netifs(): - netif.setposition() + for iface in emane_node.get_ifaces(): + iface.setposition() def reset(self) -> None: """ @@ -420,7 +418,7 @@ class EmaneManager(ModelManager): logging.info("stopping EMANE daemons") if self.links_enabled(): self.link_monitor.stop() - self.deinstallnetifs() + self.deinstall_ifaces() self.stopdaemons() self.stopeventmonitor() @@ -474,31 +472,31 @@ class EmaneManager(ModelManager): EMANE network and NEM interface. """ emane_node = None - netif = None + iface = None for node_id in self._emane_nets: emane_node = self._emane_nets[node_id] - netif = emane_node.getnemnetif(nemid) - if netif is not None: + iface = emane_node.get_nem_iface(nemid) + if iface is not None: break else: emane_node = None - return emane_node, netif + return emane_node, iface def get_nem_link( self, nem1: int, nem2: int, flags: MessageFlags = MessageFlags.NONE ) -> Optional[LinkData]: - emane1, netif = self.nemlookup(nem1) - if not emane1 or not netif: + emane1, iface = self.nemlookup(nem1) + if not emane1 or not iface: logging.error("invalid nem: %s", nem1) return None - node1 = netif.node - emane2, netif = self.nemlookup(nem2) - if not emane2 or not netif: + node1 = iface.node + emane2, iface = self.nemlookup(nem2) + if not emane2 or not iface: logging.error("invalid nem: %s", nem2) return None - node2 = netif.node + node2 = iface.node color = self.session.get_link_color(emane1.id) return LinkData( message_type=flags, @@ -516,7 +514,7 @@ class EmaneManager(ModelManager): count = 0 for node_id in self._emane_nets: emane_node = self._emane_nets[node_id] - count += len(emane_node.netifs()) + count += len(emane_node.ifaces) return count def buildplatformxml(self, ctrlnet: CtrlNet) -> None: @@ -607,19 +605,19 @@ class EmaneManager(ModelManager): n = node.id # control network not yet started here - self.session.add_remove_control_interface( + self.session.add_remove_control_iface( node, 0, remove=False, conf_required=False ) if otanetidx > 0: logging.info("adding ota device ctrl%d", otanetidx) - self.session.add_remove_control_interface( + self.session.add_remove_control_iface( node, otanetidx, remove=False, conf_required=False ) if eventservicenetidx >= 0: logging.info("adding event service device ctrl%d", eventservicenetidx) - self.session.add_remove_control_interface( + self.session.add_remove_control_iface( node, eventservicenetidx, remove=False, conf_required=False ) @@ -676,23 +674,23 @@ class EmaneManager(ModelManager): except CoreCommandError: logging.exception("error shutting down emane daemons") - def installnetifs(self) -> None: + def install_ifaces(self) -> None: """ Install TUN/TAP virtual interfaces into their proper namespaces now that the EMANE daemons are running. """ for key in sorted(self._emane_nets.keys()): - emane_node = self._emane_nets[key] - logging.info("emane install netifs for node: %d", key) - emane_node.installnetifs() + node = self._emane_nets[key] + logging.info("emane install interface for node(%s): %d", node.name, key) + node.install_ifaces() - def deinstallnetifs(self) -> None: + def deinstall_ifaces(self) -> None: """ Uninstall TUN/TAP virtual interfaces. """ for key in sorted(self._emane_nets.keys()): emane_node = self._emane_nets[key] - emane_node.deinstallnetifs() + emane_node.deinstall_ifaces() def doeventmonitor(self) -> bool: """ @@ -808,12 +806,12 @@ class EmaneManager(ModelManager): Returns True if successfully parsed and a Node Message was sent. """ # convert nemid to node number - _emanenode, netif = self.nemlookup(nemid) - if netif is None: + _emanenode, iface = self.nemlookup(nemid) + if iface is None: logging.info("location event for unknown NEM %s", nemid) return False - n = netif.node.id + n = iface.node.id # convert from lat/long/alt to x,y,z coordinates x, y, z = self.session.location.getxyz(lat, lon, alt) x = int(x) diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index 78d5ec5e..1a14011a 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -97,28 +97,28 @@ class EmaneModel(WirelessModel): ] def build_xml_files( - self, config: Dict[str, str], interface: CoreInterface = None + self, config: Dict[str, str], iface: CoreInterface = None ) -> None: """ Builds xml files for this emane model. Creates a nem.xml file that points to both mac.xml and phy.xml definitions. :param config: emane model configuration for the node and interface - :param interface: interface for the emane node + :param iface: interface for the emane node :return: nothing """ - nem_name = emanexml.nem_file_name(self, interface) - mac_name = emanexml.mac_file_name(self, interface) - phy_name = emanexml.phy_file_name(self, interface) + nem_name = emanexml.nem_file_name(self, iface) + mac_name = emanexml.mac_file_name(self, iface) + phy_name = emanexml.phy_file_name(self, iface) # remote server for file server = None - if interface is not None: - server = interface.node.server + if iface is not None: + server = iface.node.server # check if this is external transport_type = TransportType.VIRTUAL - if interface and interface.transport_type == TransportType.RAW: + if iface and iface.transport_type == TransportType.RAW: transport_type = TransportType.RAW transport_name = emanexml.transport_file_name(self.id, transport_type) @@ -144,31 +144,31 @@ class EmaneModel(WirelessModel): """ logging.debug("emane model(%s) has no post setup tasks", self.name) - def update(self, moved: List[CoreNode], moved_netifs: List[CoreInterface]) -> None: + def update(self, moved: List[CoreNode], 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_netifs: interfaces that were moved + :param moved_ifaces: interfaces that were moved :return: nothing """ try: wlan = self.session.get_node(self.id, EmaneNet) - wlan.setnempositions(moved_netifs) + wlan.setnempositions(moved_ifaces) except CoreError: logging.exception("error during update") def linkconfig( - self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None + self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None ) -> None: """ Invoked when a Link Message is received. Default is unimplemented. - :param netif: interface one + :param iface: interface one :param options: options for configuring link - :param netif2: interface two + :param iface2: interface two :return: nothing """ logging.warning("emane model(%s) does not support link config", self.name) diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py index ca9f4493..097080c3 100644 --- a/daemon/core/emane/linkmonitor.py +++ b/daemon/core/emane/linkmonitor.py @@ -212,10 +212,10 @@ class EmaneLinkMonitor: addresses = [] nodes = self.emane_manager.getnodes() for node in nodes: - for netif in node.netifs(): - if isinstance(netif.net, CtrlNet): + for iface in node.get_ifaces(): + if isinstance(iface.net, CtrlNet): ip4 = None - for x in netif.addrlist: + for x in iface.addrlist: address, prefix = x.split("/") if netaddr.valid_ipv4(address): ip4 = address diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index c4c3428b..eed51ff2 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -64,14 +64,14 @@ class EmaneNet(CoreNetworkBase): self.mobility: Optional[WayPointMobility] = None def linkconfig( - self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None + self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None ) -> None: """ The CommEffect model supports link configuration. """ if not self.model: return - self.model.linkconfig(netif, options, netif2) + self.model.linkconfig(iface, options, iface2) def config(self, conf: str) -> None: self.conf = conf @@ -82,10 +82,10 @@ class EmaneNet(CoreNetworkBase): def shutdown(self) -> None: pass - def link(self, netif1: CoreInterface, netif2: CoreInterface) -> None: + def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None: pass - def unlink(self, netif1: CoreInterface, netif2: CoreInterface) -> None: + def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None: pass def linknet(self, net: "CoreNetworkBase") -> CoreInterface: @@ -113,39 +113,33 @@ class EmaneNet(CoreNetworkBase): self.mobility = model(session=self.session, _id=self.id) self.mobility.update_config(config) - def setnemid(self, netif: CoreInterface, nemid: int) -> None: + def setnemid(self, iface: CoreInterface, nemid: int) -> None: """ Record an interface to numerical ID mapping. The Emane controller object manages and assigns these IDs for all NEMs. """ - self.nemidmap[netif] = nemid + self.nemidmap[iface] = nemid - def getnemid(self, netif: CoreInterface) -> Optional[int]: + def getnemid(self, iface: CoreInterface) -> Optional[int]: """ Given an interface, return its numerical ID. """ - if netif not in self.nemidmap: + if iface not in self.nemidmap: return None else: - return self.nemidmap[netif] + return self.nemidmap[iface] - def getnemnetif(self, nemid: int) -> Optional[CoreInterface]: + def get_nem_iface(self, nemid: int) -> Optional[CoreInterface]: """ Given a numerical NEM ID, return its interface. This returns the first interface that matches the given NEM ID. """ - for netif in self.nemidmap: - if self.nemidmap[netif] == nemid: - return netif + for iface in self.nemidmap: + if self.nemidmap[iface] == nemid: + return iface return None - def netifs(self, sort: bool = True) -> List[CoreInterface]: - """ - Retrieve list of linked interfaces sorted by node number. - """ - return sorted(self._netif.values(), key=lambda ifc: ifc.node.id) - - def installnetifs(self) -> None: + def install_ifaces(self) -> None: """ Install TAP devices into their namespaces. This is done after EMANE daemons have been started, because that is their only chance @@ -159,48 +153,48 @@ class EmaneNet(CoreNetworkBase): warntxt += "Python bindings failed to load" logging.error(warntxt) - for netif in self.netifs(): + for iface in self.get_ifaces(): external = self.session.emane.get_config( "external", self.id, self.model.name ) if external == "0": - netif.setaddrs() + iface.setaddrs() if not self.session.emane.genlocationevents(): - netif.poshook = None + iface.poshook = None continue # at this point we register location handlers for generating # EMANE location events - netif.poshook = self.setnemposition - netif.setposition() + iface.poshook = self.setnemposition + iface.setposition() - def deinstallnetifs(self) -> None: + def deinstall_ifaces(self) -> None: """ Uninstall TAP devices. This invokes their shutdown method for any required cleanup; the device may be actually removed when emanetransportd terminates. """ - for netif in self.netifs(): - if netif.transport_type == TransportType.VIRTUAL: - netif.shutdown() - netif.poshook = None + for iface in self.get_ifaces(): + if iface.transport_type == TransportType.VIRTUAL: + iface.shutdown() + iface.poshook = None def _nem_position( - self, netif: CoreInterface + self, iface: CoreInterface ) -> Optional[Tuple[int, float, float, float]]: """ Creates nem position for emane event for a given interface. - :param netif: interface to get nem emane position for + :param iface: interface to get nem emane position for :return: nem position tuple, None otherwise """ - nemid = self.getnemid(netif) - ifname = netif.localname + nemid = self.getnemid(iface) + ifname = iface.localname if nemid is None: logging.info("nemid for %s is unknown", ifname) return - node = netif.node + node = iface.node x, y, z = node.getposition() lat, lon, alt = self.session.location.getgeo(x, y, z) if node.position.alt is not None: @@ -210,30 +204,30 @@ class EmaneNet(CoreNetworkBase): alt = int(round(alt)) return nemid, lon, lat, alt - def setnemposition(self, netif: CoreInterface) -> None: + def setnemposition(self, iface: CoreInterface) -> None: """ Publish a NEM location change event using the EMANE event service. - :param netif: interface to set nem position for + :param iface: interface to set nem position for """ if self.session.emane.service is None: logging.info("position service not available") return - position = self._nem_position(netif) + position = self._nem_position(iface) if position: nemid, lon, lat, alt = position event = LocationEvent() event.append(nemid, latitude=lat, longitude=lon, altitude=alt) self.session.emane.service.publish(0, event) - def setnempositions(self, moved_netifs: List[CoreInterface]) -> None: + def setnempositions(self, moved_ifaces: List[CoreInterface]) -> None: """ Several NEMs have moved, from e.g. a WaypointMobilityModel calculation. Generate an EMANE Location Event having several - entries for each netif that has moved. + entries for each interface that has moved. """ - if len(moved_netifs) == 0: + if len(moved_ifaces) == 0: return if self.session.emane.service is None: @@ -241,8 +235,8 @@ class EmaneNet(CoreNetworkBase): return event = LocationEvent() - for netif in moved_netifs: - position = self._nem_position(netif) + for iface in moved_ifaces: + position = self._nem_position(iface) if position: nemid, lon, lat, alt = position event.append(nemid, latitude=lat, longitude=lon, altitude=alt) diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index 819716e3..47f45820 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -27,7 +27,7 @@ class ConfigData: possible_values: str = None groups: str = None session: int = None - interface_number: int = None + iface_id: int = None network_id: int = None opaque: str = None @@ -114,19 +114,19 @@ class LinkData: emulation_id: int = None network_id: int = None key: int = None - interface1_id: int = None - interface1_name: str = None - interface1_ip4: str = None - interface1_ip4_mask: int = None - interface1_mac: str = None - interface1_ip6: str = None - interface1_ip6_mask: int = None - interface2_id: int = None - interface2_name: str = None - interface2_ip4: str = None - interface2_ip4_mask: int = None - interface2_mac: str = None - interface2_ip6: str = None - interface2_ip6_mask: int = None + iface1_id: int = None + iface1_name: str = None + iface1_ip4: str = None + iface1_ip4_mask: int = None + iface1_mac: str = None + iface1_ip6: str = None + iface1_ip6_mask: int = None + iface2_id: int = None + iface2_name: str = None + iface2_ip4: str = None + iface2_ip4_mask: int = None + iface2_mac: str = None + iface2_ip6: str = None + iface2_ip6_mask: int = None opaque: str = None color: str = None diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index 75081447..381eb019 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -208,7 +208,7 @@ class DistributedController: "local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key ) local_tap = GreTap(session=self.session, remoteip=host, key=key) - local_tap.net_client.set_interface_master(node.brname, local_tap.localname) + local_tap.net_client.set_iface_master(node.brname, local_tap.localname) # server to local logging.info( @@ -217,7 +217,7 @@ class DistributedController: remote_tap = GreTap( session=self.session, remoteip=self.address, key=key, server=server ) - remote_tap.net_client.set_interface_master(node.brname, remote_tap.localname) + remote_tap.net_client.set_iface_master(node.brname, remote_tap.localname) # save tunnels for shutdown tunnel = (local_tap, remote_tap) diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index 2aecdace..25ce71ac 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -155,7 +155,7 @@ class IpPrefixes: raise ValueError("ip6 prefixes have not been set") return str(self.ip6[node_id]) - def gen_interface(self, node_id: int, name: str = None, mac: str = None): + def gen_iface(self, node_id: int, name: str = None, mac: str = None): """ Creates interface data for linking nodes, using the nodes unique id for generation, along with a random mac address, unless provided. @@ -188,7 +188,7 @@ class IpPrefixes: name=name, ip4=ip4, ip4_mask=ip4_mask, ip6=ip6, ip6_mask=ip6_mask, mac=mac ) - def create_interface( + def create_iface( self, node: "CoreNode", name: str = None, mac: str = None ) -> InterfaceData: """ @@ -201,6 +201,6 @@ class IpPrefixes: generation :return: new interface data for the provided node """ - interface_data = self.gen_interface(node.id, name, mac) - interface_data.id = node.newifindex() - return interface_data + iface_data = self.gen_iface(node.id, name, mac) + iface_data.id = node.next_iface_id() + return iface_data diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index e63c30c7..2dc5ad12 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -203,7 +203,7 @@ class Session: common_networks = node1.commonnets(node1) if not common_networks: raise CoreError("no common network found for wireless link/unlink") - for common_network, interface1, interface2 in common_networks: + for common_network, iface1, iface2 in common_networks: if not isinstance(common_network, (WlanNode, EmaneNet)): logging.info( "skipping common network that is not wireless/emane: %s", @@ -211,16 +211,16 @@ class Session: ) continue if connect: - common_network.link(interface1, interface2) + common_network.link(iface1, iface2) else: - common_network.unlink(interface1, interface2) + common_network.unlink(iface1, iface2) def add_link( self, node1_id: int, node2_id: int, - interface1_data: InterfaceData = None, - interface2_data: InterfaceData = None, + iface1_data: InterfaceData = None, + iface2_data: InterfaceData = None, options: LinkOptions = None, ) -> Tuple[CoreInterface, CoreInterface]: """ @@ -228,9 +228,9 @@ class Session: :param node1_id: node one id :param node2_id: node two id - :param interface1_data: node one interface + :param iface1_data: node one interface data, defaults to none - :param interface2_data: node two interface + :param iface2_data: node two interface data, defaults to none :param options: data for creating link, defaults to no options @@ -240,8 +240,8 @@ class Session: options = LinkOptions() node1 = self.get_node(node1_id, NodeBase) node2 = self.get_node(node2_id, NodeBase) - interface1 = None - interface2 = None + iface1 = None + iface2 = None # wireless link if options.type == LinkTypes.WIRELESS: @@ -258,22 +258,22 @@ class Session: logging.info("linking ptp: %s - %s", node1.name, node2.name) start = self.state.should_start() ptp = self.create_node(PtpNet, start) - interface1 = node1.newnetif(ptp, interface1_data) - interface2 = node2.newnetif(ptp, interface2_data) - ptp.linkconfig(interface1, options) + iface1 = node1.new_iface(ptp, iface1_data) + iface2 = node2.new_iface(ptp, iface2_data) + ptp.linkconfig(iface1, options) if not options.unidirectional: - ptp.linkconfig(interface2, options) + ptp.linkconfig(iface2, options) # link node to net elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase): - interface1 = node1.newnetif(node2, interface1_data) + iface1 = node1.new_iface(node2, iface1_data) if not isinstance(node2, (EmaneNet, WlanNode)): - node2.linkconfig(interface1, options) + node2.linkconfig(iface1, options) # link net to node elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): - interface2 = node2.newnetif(node1, interface2_data) + iface2 = node2.new_iface(node1, iface2_data) wireless_net = isinstance(node1, (EmaneNet, WlanNode)) if not options.unidirectional and not wireless_net: - node1.linkconfig(interface2, options) + node1.linkconfig(iface2, options) # network to network elif isinstance(node1, CoreNetworkBase) and isinstance( node2, CoreNetworkBase @@ -281,12 +281,12 @@ class Session: logging.info( "linking network to network: %s - %s", node1.name, node2.name ) - interface1 = node1.linknet(node2) - node1.linkconfig(interface1, options) + iface1 = node1.linknet(node2) + node1.linkconfig(iface1, options) if not options.unidirectional: - interface1.swapparams("_params_up") - node2.linkconfig(interface1, options) - interface1.swapparams("_params_up") + iface1.swapparams("_params_up") + node2.linkconfig(iface1, options) + iface1.swapparams("_params_up") else: raise CoreError( f"cannot link node1({type(node1)}) node2({type(node2)})" @@ -296,19 +296,19 @@ class Session: key = options.key if isinstance(node1, TunnelNode): logging.info("setting tunnel key for: %s", node1.name) - node1.setkey(key, interface1_data) + node1.setkey(key, iface1_data) if isinstance(node2, TunnelNode): logging.info("setting tunnel key for: %s", node2.name) - node2.setkey(key, interface2_data) + node2.setkey(key, iface2_data) self.sdt.add_link(node1_id, node2_id) - return interface1, interface2 + return iface1, iface2 def delete_link( self, node1_id: int, node2_id: int, - interface1_id: int = None, - interface2_id: int = None, + iface1_id: int = None, + iface2_id: int = None, link_type: LinkTypes = LinkTypes.WIRED, ) -> None: """ @@ -316,8 +316,8 @@ class Session: :param node1_id: node one id :param node2_id: node two id - :param interface1_id: interface id for node one - :param interface2_id: interface id for node two + :param iface1_id: interface id for node one + :param iface2_id: interface id for node two :param link_type: link type to delete :return: nothing :raises core.CoreError: when no common network is found for link being deleted @@ -328,9 +328,9 @@ class Session: "deleting link(%s) node(%s):interface(%s) node(%s):interface(%s)", link_type.name, node1.name, - interface1_id, + iface1_id, node2.name, - interface2_id, + iface2_id, ) # wireless link @@ -345,37 +345,29 @@ class Session: # wired link else: if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): - interface1 = node1.netif(interface1_id) - interface2 = node2.netif(interface2_id) - if not interface1: - raise CoreError( - f"node({node1.name}) missing interface({interface1_id})" - ) - if not interface2: - raise CoreError( - f"node({node2.name}) missing interface({interface2_id})" - ) - if interface1.net != interface2.net: + iface1 = node1.get_iface(iface1_id) + iface2 = node2.get_iface(iface2_id) + if iface1.net != iface2.net: raise CoreError( f"node1({node1.name}) node2({node2.name}) " "not connected to same net" ) - ptp = interface1.net - node1.delnetif(interface1_id) - node2.delnetif(interface2_id) + ptp = iface1.net + node1.delete_iface(iface1_id) + node2.delete_iface(iface2_id) self.delete_node(ptp.id) elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase): - node1.delnetif(interface1_id) + node1.delete_iface(iface1_id) elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): - node2.delnetif(interface2_id) + node2.delete_iface(iface2_id) self.sdt.delete_link(node1_id, node2_id) def update_link( self, node1_id: int, node2_id: int, - interface1_id: int = None, - interface2_id: int = None, + iface1_id: int = None, + iface2_id: int = None, options: LinkOptions = None, ) -> None: """ @@ -383,8 +375,8 @@ class Session: :param node1_id: node one id :param node2_id: node two id - :param interface1_id: interface id for node one - :param interface2_id: interface id for node two + :param iface1_id: interface id for node one + :param iface2_id: interface id for node two :param options: data to update link with :return: nothing :raises core.CoreError: when updating a wireless type link, when there is a @@ -398,9 +390,9 @@ class Session: "update link(%s) node(%s):interface(%s) node(%s):interface(%s)", options.type.name, node1.name, - interface1_id, + iface1_id, node2.name, - interface2_id, + iface2_id, ) # wireless link @@ -408,54 +400,54 @@ class Session: raise CoreError("cannot update wireless link") else: if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): - interface1 = node1.netif(interface1_id) - interface2 = node2.netif(interface2_id) - if not interface1: + iface1 = node1.ifaces.get(iface1_id) + iface2 = node2.ifaces.get(iface2_id) + if not iface1: raise CoreError( - f"node({node1.name}) missing interface({interface1_id})" + f"node({node1.name}) missing interface({iface1_id})" ) - if not interface2: + if not iface2: raise CoreError( - f"node({node2.name}) missing interface({interface2_id})" + f"node({node2.name}) missing interface({iface2_id})" ) - if interface1.net != interface2.net: + if iface1.net != iface2.net: raise CoreError( f"node1({node1.name}) node2({node2.name}) " "not connected to same net" ) - ptp = interface1.net - ptp.linkconfig(interface1, options, interface2) + ptp = iface1.net + ptp.linkconfig(iface1, options, iface2) if not options.unidirectional: - ptp.linkconfig(interface2, options, interface1) + ptp.linkconfig(iface2, options, iface1) elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase): - interface = node1.netif(interface1_id) - node2.linkconfig(interface, options) + iface = node1.get_iface(iface1_id) + node2.linkconfig(iface, options) elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): - interface = node2.netif(interface2_id) - node1.linkconfig(interface, options) + iface = node2.get_iface(iface2_id) + node1.linkconfig(iface, options) elif isinstance(node1, CoreNetworkBase) and isinstance( node2, CoreNetworkBase ): - interface = node1.getlinknetif(node2) + iface = node1.get_linked_iface(node2) upstream = False - if not interface: + if not iface: upstream = True - interface = node2.getlinknetif(node1) - if not interface: + iface = node2.get_linked_iface(node1) + if not iface: raise CoreError("modify unknown link between nets") if upstream: - interface.swapparams("_params_up") - node1.linkconfig(interface, options) - interface.swapparams("_params_up") + iface.swapparams("_params_up") + node1.linkconfig(iface, options) + iface.swapparams("_params_up") else: - node1.linkconfig(interface, options) + node1.linkconfig(iface, options) if not options.unidirectional: if upstream: - node2.linkconfig(interface, options) + node2.linkconfig(iface, options) else: - interface.swapparams("_params_up") - node2.linkconfig(interface, options) - interface.swapparams("_params_up") + iface.swapparams("_params_up") + node2.linkconfig(iface, options) + iface.swapparams("_params_up") else: raise CoreError( f"cannot update link node1({type(node1)}) node2({type(node2)})" @@ -553,7 +545,7 @@ class Session: is_boot_node = isinstance(node, CoreNodeBase) and not isinstance(node, Rj45Node) if self.state == EventTypes.RUNTIME_STATE and is_boot_node: self.write_nodes() - self.add_remove_control_interface(node=node, remove=False) + self.add_remove_control_iface(node=node, remove=False) self.services.boot_services(node) self.sdt.add_node(node) @@ -1268,7 +1260,7 @@ class Session: self.emane.shutdown() # update control interface hosts - self.update_control_interface_hosts(remove=True) + self.update_control_iface_hosts(remove=True) # remove all four possible control networks self.add_remove_control_net(0, remove=True) @@ -1314,7 +1306,7 @@ class Session: :return: nothing """ logging.info("booting node(%s): %s", node.name, [x.name for x in node.services]) - self.add_remove_control_interface(node=node, remove=False) + self.add_remove_control_iface(node=node, remove=False) self.services.boot_services(node) node.start_config_services() @@ -1338,7 +1330,7 @@ class Session: total = time.monotonic() - start logging.debug("boot run time: %s", total) if not exceptions: - self.update_control_interface_hosts() + self.update_control_iface_hosts() return exceptions def get_control_net_prefixes(self) -> List[str]: @@ -1356,7 +1348,7 @@ class Session: p0 = p return [p0, p1, p2, p3] - def get_control_net_server_interfaces(self) -> List[str]: + def get_control_net_server_ifaces(self) -> List[str]: """ Retrieve control net server interfaces. @@ -1424,7 +1416,7 @@ class Session: else: prefix_spec = CtrlNet.DEFAULT_PREFIX_LIST[net_index] logging.debug("prefix spec: %s", prefix_spec) - server_interface = self.get_control_net_server_interfaces()[net_index] + server_iface = self.get_control_net_server_ifaces()[net_index] # return any existing controlnet bridge try: @@ -1465,7 +1457,7 @@ class Session: _id, prefix, updown_script, - server_interface, + server_iface, ) control_net = self.create_node( CtrlNet, @@ -1473,11 +1465,11 @@ class Session: prefix, _id=_id, updown_script=updown_script, - serverintf=server_interface, + serverintf=server_iface, ) return control_net - def add_remove_control_interface( + def add_remove_control_iface( self, node: CoreNode, net_index: int = 0, @@ -1503,27 +1495,27 @@ class Session: if not node: return # ctrl# already exists - if node.netif(control_net.CTRLIF_IDX_BASE + net_index): + if node.ifaces.get(control_net.CTRLIF_IDX_BASE + net_index): return try: ip4 = control_net.prefix[node.id] ip4_mask = control_net.prefix.prefixlen - interface_data = InterfaceData( + iface_data = InterfaceData( id=control_net.CTRLIF_IDX_BASE + net_index, name=f"ctrl{net_index}", mac=utils.random_mac(), ip4=ip4, ip4_mask=ip4_mask, ) - interface = node.newnetif(control_net, interface_data) - interface.control = True + iface = node.new_iface(control_net, iface_data) + iface.control = True except ValueError: msg = f"Control interface not added to node {node.id}. " msg += f"Invalid control network prefix ({control_net.prefix}). " msg += "A longer prefix length may be required for this many nodes." logging.exception(msg) - def update_control_interface_hosts( + def update_control_iface_hosts( self, net_index: int = 0, remove: bool = False ) -> None: """ @@ -1549,9 +1541,9 @@ class Session: return entries = [] - for interface in control_net.netifs(): - name = interface.node.name - for address in interface.addrlist: + for iface in control_net.get_ifaces(): + name = iface.node.name + for address in iface.addrlist: address = address.split("/")[0] entries.append(f"{address} {name}") diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 5c1c52a0..3be58e17 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -57,8 +57,8 @@ class CoreClient: self.read_config() # helpers - self.interface_to_edge = {} - self.interfaces_manager = InterfaceManager(self.app) + self.iface_to_edge = {} + self.ifaces_manager = InterfaceManager(self.app) # session data self.state = None @@ -91,8 +91,8 @@ class CoreClient: def reset(self): # helpers - self.interfaces_manager.reset() - self.interface_to_edge.clear() + self.ifaces_manager.reset() + self.iface_to_edge.clear() # session data self.canvas_nodes.clear() self.links.clear() @@ -263,7 +263,7 @@ class CoreClient: self.emane_config = response.config # update interface manager - self.interfaces_manager.joined(session.links) + self.ifaces_manager.joined(session.links) # draw session self.app.canvas.reset_and_redraw(session) @@ -278,11 +278,11 @@ class CoreClient: # get emane model config response = self.client.get_emane_model_configs(self.session_id) for config in response.configs: - interface = None - if config.interface != -1: - interface = config.interface + iface_id = None + if config.iface_id != -1: + iface_id = config.iface_id canvas_node = self.canvas_nodes[config.node_id] - canvas_node.emane_model_configs[(config.model, interface)] = dict( + canvas_node.emane_model_configs[(config.model, iface_id)] = dict( config.config ) @@ -460,16 +460,16 @@ class CoreClient: self.app.show_grpc_exception("Edit Node Error", e) def start_session(self) -> core_pb2.StartSessionResponse: - self.interfaces_manager.reset_mac() + self.ifaces_manager.reset_mac() nodes = [x.core_node for x in self.canvas_nodes.values()] links = [] for edge in self.links.values(): link = core_pb2.Link() link.CopyFrom(edge.link) - if link.HasField("interface1") and not link.interface1.mac: - link.interface1.mac = self.interfaces_manager.next_mac() - if link.HasField("interface2") and not link.interface2.mac: - link.interface2.mac = self.interfaces_manager.next_mac() + if link.HasField("iface1") and not link.iface1.mac: + link.iface1.mac = self.ifaces_manager.next_mac() + if link.HasField("iface2") and not link.iface2.mac: + link.iface2.mac = self.ifaces_manager.next_mac() links.append(link) wlan_configs = self.get_wlan_configs_proto() mobility_configs = self.get_mobility_configs_proto() @@ -689,8 +689,8 @@ class CoreClient: self.session_id, link_proto.node1_id, link_proto.node2_id, - link_proto.interface1, - link_proto.interface2, + link_proto.iface1, + link_proto.iface2, link_proto.options, ) logging.debug("create link: %s", response) @@ -733,7 +733,7 @@ class CoreClient: config_proto.node_id, config_proto.model, config_proto.config, - config_proto.interface_id, + config_proto.iface_id, ) if self.emane_config: config = {x: self.emane_config[x].value for x in self.emane_config} @@ -824,31 +824,26 @@ class CoreClient: for edge in edges: del self.links[edge.token] links.append(edge.link) - self.interfaces_manager.removed(links) + self.ifaces_manager.removed(links) - def create_interface(self, canvas_node: CanvasNode) -> core_pb2.Interface: + def create_iface(self, canvas_node: CanvasNode) -> core_pb2.Interface: node = canvas_node.core_node - ip4, ip6 = self.interfaces_manager.get_ips(node) - ip4_mask = self.interfaces_manager.ip4_mask - ip6_mask = self.interfaces_manager.ip6_mask - interface_id = canvas_node.next_interface_id() - name = f"eth{interface_id}" - interface = core_pb2.Interface( - id=interface_id, - name=name, - ip4=ip4, - ip4mask=ip4_mask, - ip6=ip6, - ip6mask=ip6_mask, + ip4, ip6 = self.ifaces_manager.get_ips(node) + ip4_mask = self.ifaces_manager.ip4_mask + ip6_mask = self.ifaces_manager.ip6_mask + iface_id = canvas_node.next_iface_id() + name = f"eth{iface_id}" + iface = core_pb2.Interface( + id=iface_id, name=name, ip4=ip4, ip4mask=ip4_mask, ip6=ip6, ip6mask=ip6_mask ) logging.info( "create node(%s) interface(%s) IPv4(%s) IPv6(%s)", node.name, - interface.name, - interface.ip4, - interface.ip6, + iface.name, + iface.ip4, + iface.ip6, ) - return interface + return iface def create_link( self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode @@ -861,34 +856,34 @@ class CoreClient: dst_node = canvas_dst_node.core_node # determine subnet - self.interfaces_manager.determine_subnets(canvas_src_node, canvas_dst_node) + self.ifaces_manager.determine_subnets(canvas_src_node, canvas_dst_node) - src_interface = None + src_iface = None if NodeUtils.is_container_node(src_node.type): - src_interface = self.create_interface(canvas_src_node) - self.interface_to_edge[(src_node.id, src_interface.id)] = edge.token + src_iface = self.create_iface(canvas_src_node) + self.iface_to_edge[(src_node.id, src_iface.id)] = edge.token - dst_interface = None + dst_iface = None if NodeUtils.is_container_node(dst_node.type): - dst_interface = self.create_interface(canvas_dst_node) - self.interface_to_edge[(dst_node.id, dst_interface.id)] = edge.token + dst_iface = self.create_iface(canvas_dst_node) + self.iface_to_edge[(dst_node.id, dst_iface.id)] = edge.token link = core_pb2.Link( type=core_pb2.LinkType.WIRED, node1_id=src_node.id, node2_id=dst_node.id, - interface1=src_interface, - interface2=dst_interface, + iface1=src_iface, + iface2=dst_iface, ) # assign after creating link proto, since interfaces are copied - if src_interface: - interface1 = link.interface1 - edge.src_interface = interface1 - canvas_src_node.interfaces[interface1.id] = interface1 - if dst_interface: - interface2 = link.interface2 - edge.dst_interface = interface2 - canvas_dst_node.interfaces[interface2.id] = interface2 + if src_iface: + iface1 = link.iface1 + edge.src_iface = iface1 + canvas_src_node.ifaces[iface1.id] = iface1 + if dst_iface: + iface2 = link.iface2 + edge.dst_iface = iface2 + canvas_dst_node.ifaces[iface2.id] = iface2 edge.set_link(link) self.links[edge.token] = edge logging.info("Add link between %s and %s", src_node.name, dst_node.name) @@ -928,12 +923,12 @@ class CoreClient: continue node_id = canvas_node.core_node.id for key, config in canvas_node.emane_model_configs.items(): - model, interface = key + model, iface_id = key config = {x: config[x].value for x in config} - if interface is None: - interface = -1 + if iface_id is None: + iface_id = -1 config_proto = EmaneModelConfig( - node_id=node_id, interface_id=interface, model=model, config=config + node_id=node_id, iface_id=iface_id, model=model, config=config ) configs.append(config_proto) return configs @@ -1021,19 +1016,19 @@ class CoreClient: return dict(config) def get_emane_model_config( - self, node_id: int, model: str, interface: int = None + self, node_id: int, model: str, iface_id: int = None ) -> Dict[str, common_pb2.ConfigOption]: - if interface is None: - interface = -1 + if iface_id is None: + iface_id = -1 response = self.client.get_emane_model_config( - self.session_id, node_id, model, interface + self.session_id, node_id, model, iface_id ) config = response.config logging.debug( "get emane model config: node id: %s, EMANE model: %s, interface: %s, config: %s", node_id, model, - interface, + iface_id, config, ) return dict(config) diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index 000ebb05..8f7ca089 100644 --- a/daemon/core/gui/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -56,7 +56,7 @@ class EmaneModelDialog(Dialog): app: "Application", canvas_node: "CanvasNode", model: str, - interface: int = None, + iface_id: int = None, ): super().__init__( app, f"{canvas_node.core_node.name} {model} Configuration", master=master @@ -64,16 +64,16 @@ class EmaneModelDialog(Dialog): self.canvas_node = canvas_node self.node = canvas_node.core_node self.model = f"emane_{model}" - self.interface = interface + self.iface_id = iface_id self.config_frame = None self.has_error = False try: self.config = self.canvas_node.emane_model_configs.get( - (self.model, self.interface) + (self.model, self.iface_id) ) if not self.config: self.config = self.app.core.get_emane_model_config( - self.node.id, self.model, self.interface + self.node.id, self.model, self.iface_id ) self.draw() except grpc.RpcError as e: @@ -103,7 +103,7 @@ class EmaneModelDialog(Dialog): def click_apply(self): self.config_frame.parse_config() - key = (self.model, self.interface) + key = (self.model, self.iface_id) self.canvas_node.emane_model_configs[key] = self.config self.destroy() diff --git a/daemon/core/gui/dialogs/ipdialog.py b/daemon/core/gui/dialogs/ipdialog.py index 62f5d0ba..d31dcdff 100644 --- a/daemon/core/gui/dialogs/ipdialog.py +++ b/daemon/core/gui/dialogs/ipdialog.py @@ -146,6 +146,6 @@ class IpConfigDialog(Dialog): ip_config.ip6 = self.ip6 ip_config.ip4s = ip4s ip_config.ip6s = ip6s - self.app.core.interfaces_manager.update_ips(self.ip4, self.ip6) + self.app.core.ifaces_manager.update_ips(self.ip4, self.ip6) self.app.save_config() self.destroy() diff --git a/daemon/core/gui/dialogs/linkconfig.py b/daemon/core/gui/dialogs/linkconfig.py index 9c3fc987..adf8156f 100644 --- a/daemon/core/gui/dialogs/linkconfig.py +++ b/daemon/core/gui/dialogs/linkconfig.py @@ -227,21 +227,21 @@ class LinkConfigurationDialog(Dialog): ) link.options.CopyFrom(options) - interface1_id = None - if link.HasField("interface1"): - interface1_id = link.interface1.id - interface2_id = None - if link.HasField("interface2"): - interface2_id = link.interface2.id + iface1_id = None + if link.HasField("iface1"): + iface1_id = link.iface1.id + iface2_id = None + if link.HasField("iface2"): + iface2_id = link.iface2.id if not self.is_symmetric: link.options.unidirectional = True - asym_interface1 = None - if interface1_id: - asym_interface1 = core_pb2.Interface(id=interface1_id) - asym_interface2 = None - if interface2_id: - asym_interface2 = core_pb2.Interface(id=interface2_id) + asym_iface1 = None + if iface1_id: + asym_iface1 = core_pb2.Interface(id=iface1_id) + asym_iface2 = None + if iface2_id: + asym_iface2 = core_pb2.Interface(id=iface2_id) down_bandwidth = get_int(self.down_bandwidth) down_jitter = get_int(self.down_jitter) down_delay = get_int(self.down_delay) @@ -258,8 +258,8 @@ class LinkConfigurationDialog(Dialog): self.edge.asymmetric_link = core_pb2.Link( node1_id=link.node2_id, node2_id=link.node1_id, - interface1=asym_interface1, - interface2=asym_interface2, + iface1=asym_iface1, + iface2=asym_iface2, options=options, ) else: @@ -273,8 +273,8 @@ class LinkConfigurationDialog(Dialog): link.node1_id, link.node2_id, link.options, - interface1_id, - interface2_id, + iface1_id, + iface2_id, ) if self.edge.asymmetric_link: self.app.core.client.edit_link( @@ -282,8 +282,8 @@ class LinkConfigurationDialog(Dialog): link.node2_id, link.node1_id, self.edge.asymmetric_link.options, - interface1_id, - interface2_id, + iface1_id, + iface2_id, ) self.destroy() diff --git a/daemon/core/gui/dialogs/macdialog.py b/daemon/core/gui/dialogs/macdialog.py index caca9fd0..46414cf9 100644 --- a/daemon/core/gui/dialogs/macdialog.py +++ b/daemon/core/gui/dialogs/macdialog.py @@ -55,7 +55,7 @@ class MacConfigDialog(Dialog): if not netaddr.valid_mac(mac): messagebox.showerror("MAC Error", f"{mac} is an invalid mac") else: - self.app.core.interfaces_manager.mac = netaddr.EUI(mac) + self.app.core.ifaces_manager.mac = netaddr.EUI(mac) self.app.guiconfig.mac = mac self.app.save_config() self.destroy() diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index 0d46ae06..29ce2010 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -111,7 +111,7 @@ class NodeConfigDialog(Dialog): if self.node.server: server = self.node.server self.server = tk.StringVar(value=server) - self.interfaces = {} + self.ifaces = {} self.draw() def draw(self): @@ -183,53 +183,53 @@ class NodeConfigDialog(Dialog): row += 1 if NodeUtils.is_rj45_node(self.node.type): - response = self.app.core.client.get_interfaces() + response = self.app.core.client.get_ifaces() logging.debug("host machine available interfaces: %s", response) - interfaces = ListboxScroll(frame) - interfaces.listbox.config(state=state) - interfaces.grid( + ifaces = ListboxScroll(frame) + ifaces.listbox.config(state=state) + ifaces.grid( row=row, column=0, columnspan=2, sticky="ew", padx=PADX, pady=PADY ) - for inf in sorted(response.interfaces[:]): - interfaces.listbox.insert(tk.END, inf) + for inf in sorted(response.ifaces[:]): + ifaces.listbox.insert(tk.END, inf) row += 1 - interfaces.listbox.bind("<>", self.interface_select) + ifaces.listbox.bind("<>", self.iface_select) # interfaces - if self.canvas_node.interfaces: - self.draw_interfaces() + if self.canvas_node.ifaces: + self.draw_ifaces() self.draw_spacer() self.draw_buttons() - def draw_interfaces(self): + def draw_ifaces(self): notebook = ttk.Notebook(self.top) notebook.grid(sticky="nsew", pady=PADY) self.top.rowconfigure(notebook.grid_info()["row"], weight=1) state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL - for interface_id in sorted(self.canvas_node.interfaces): - interface = self.canvas_node.interfaces[interface_id] + for iface_id in sorted(self.canvas_node.ifaces): + iface = self.canvas_node.ifaces[iface_id] tab = ttk.Frame(notebook, padding=FRAME_PAD) tab.grid(sticky="nsew", pady=PADY) tab.columnconfigure(1, weight=1) tab.columnconfigure(2, weight=1) - notebook.add(tab, text=interface.name) + notebook.add(tab, text=iface.name) row = 0 - emane_node = self.canvas_node.has_emane_link(interface.id) + emane_node = self.canvas_node.has_emane_link(iface.id) if emane_node: emane_model = emane_node.emane.split("_")[1] button = ttk.Button( tab, text=f"Configure EMANE {emane_model}", - command=lambda: self.click_emane_config(emane_model, interface.id), + command=lambda: self.click_emane_config(emane_model, iface.id), ) button.grid(row=row, sticky="ew", columnspan=3, pady=PADY) row += 1 label = ttk.Label(tab, text="MAC") label.grid(row=row, column=0, padx=PADX, pady=PADY) - auto_set = not interface.mac + auto_set = not iface.mac mac_state = tk.DISABLED if auto_set else tk.NORMAL is_auto = tk.BooleanVar(value=auto_set) checkbutton = ttk.Checkbutton( @@ -237,7 +237,7 @@ class NodeConfigDialog(Dialog): ) checkbutton.var = is_auto checkbutton.grid(row=row, column=1, padx=PADX) - mac = tk.StringVar(value=interface.mac) + mac = tk.StringVar(value=iface.mac) entry = ttk.Entry(tab, textvariable=mac, state=mac_state) entry.grid(row=row, column=2, sticky="ew") func = partial(mac_auto, is_auto, entry, mac) @@ -247,8 +247,8 @@ class NodeConfigDialog(Dialog): label = ttk.Label(tab, text="IPv4") label.grid(row=row, column=0, padx=PADX, pady=PADY) ip4_net = "" - if interface.ip4: - ip4_net = f"{interface.ip4}/{interface.ip4mask}" + if iface.ip4: + ip4_net = f"{iface.ip4}/{iface.ip4mask}" ip4 = tk.StringVar(value=ip4_net) entry = ttk.Entry(tab, textvariable=ip4, state=state) entry.grid(row=row, column=1, columnspan=2, sticky="ew") @@ -257,13 +257,13 @@ class NodeConfigDialog(Dialog): label = ttk.Label(tab, text="IPv6") label.grid(row=row, column=0, padx=PADX, pady=PADY) ip6_net = "" - if interface.ip6: - ip6_net = f"{interface.ip6}/{interface.ip6mask}" + if iface.ip6: + ip6_net = f"{iface.ip6}/{iface.ip6mask}" ip6 = tk.StringVar(value=ip6_net) entry = ttk.Entry(tab, textvariable=ip6, state=state) entry.grid(row=row, column=1, columnspan=2, sticky="ew") - self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6) + self.ifaces[iface.id] = InterfaceData(is_auto, mac, ip4, ip6) def draw_buttons(self): frame = ttk.Frame(self.top) @@ -277,9 +277,9 @@ class NodeConfigDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def click_emane_config(self, emane_model: str, interface_id: int): + def click_emane_config(self, emane_model: str, iface_id: int): dialog = EmaneModelDialog( - self, self.app, self.canvas_node, emane_model, interface_id + self, self.app, self.canvas_node, emane_model, iface_id ) dialog.show() @@ -309,12 +309,12 @@ class NodeConfigDialog(Dialog): self.canvas_node.image = self.image # update node interface data - for interface in self.canvas_node.interfaces.values(): - data = self.interfaces[interface.id] + 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, interface.name, ip4_net): + if not check_ip4(self, iface.name, ip4_net): error = True break if ip4_net: @@ -322,12 +322,12 @@ class NodeConfigDialog(Dialog): ip4mask = int(ip4mask) else: ip4, ip4mask = "", 0 - interface.ip4 = ip4 - interface.ip4mask = ip4mask + iface.ip4 = ip4 + iface.ip4mask = ip4mask # validate ip6 ip6_net = data.ip6.get() - if not check_ip6(self, interface.name, ip6_net): + if not check_ip6(self, iface.name, ip6_net): error = True break if ip6_net: @@ -335,28 +335,28 @@ class NodeConfigDialog(Dialog): ip6mask = int(ip6mask) else: ip6, ip6mask = "", 0 - interface.ip6 = ip6 - interface.ip6mask = ip6mask + iface.ip6 = ip6 + iface.ip6mask = ip6mask 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 {interface.name}" + 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) - interface.mac = str(mac) + iface.mac = str(mac) # redraw if not error: self.canvas_node.redraw() self.destroy() - def interface_select(self, event: tk.Event): + def iface_select(self, event: tk.Event): listbox = event.widget cur = listbox.curselection() if cur: - interface = listbox.get(cur[0]) - self.name.set(interface) + iface = listbox.get(cur[0]) + self.name.set(iface) diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 1d2264eb..152e1a2f 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -259,8 +259,8 @@ class CanvasEdge(Edge): Create an instance of canvas edge object """ super().__init__(canvas, src) - self.src_interface = None - self.dst_interface = None + self.src_iface = None + self.dst_iface = None self.text_src = None self.text_dst = None self.link = None @@ -283,25 +283,25 @@ class CanvasEdge(Edge): self.link = link self.draw_labels() - def interface_label(self, interface: core_pb2.Interface) -> str: + def iface_label(self, iface: core_pb2.Interface) -> str: label = "" - if interface.name and self.canvas.show_interface_names.get(): - label = f"{interface.name}" - if interface.ip4 and self.canvas.show_ip4s.get(): + if iface.name and self.canvas.show_iface_names.get(): + label = f"{iface.name}" + if iface.ip4 and self.canvas.show_ip4s.get(): label = f"{label}\n" if label else "" - label += f"{interface.ip4}/{interface.ip4mask}" - if interface.ip6 and self.canvas.show_ip6s.get(): + label += f"{iface.ip4}/{iface.ip4mask}" + if iface.ip6 and self.canvas.show_ip6s.get(): label = f"{label}\n" if label else "" - label += f"{interface.ip6}/{interface.ip6mask}" + label += f"{iface.ip6}/{iface.ip6mask}" return label def create_node_labels(self) -> Tuple[str, str]: label1 = None - if self.link.HasField("interface1"): - label1 = self.interface_label(self.link.interface1) + if self.link.HasField("iface1"): + label1 = self.iface_label(self.link.iface1) label2 = None - if self.link.HasField("interface2"): - label2 = self.interface_label(self.link.interface2) + if self.link.HasField("iface2"): + label2 = self.iface_label(self.link.iface2) return label1, label2 def draw_labels(self) -> None: diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 90dcd9f6..269e3973 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -97,7 +97,7 @@ class CanvasGraph(tk.Canvas): self.show_link_labels = ShowVar(self, tags.LINK_LABEL, value=True) self.show_grid = ShowVar(self, tags.GRIDLINE, value=True) self.show_annotations = ShowVar(self, tags.ANNOTATION, value=True) - self.show_interface_names = BooleanVar(value=False) + self.show_iface_names = BooleanVar(value=False) self.show_ip4s = BooleanVar(value=True) self.show_ip6s = BooleanVar(value=True) @@ -136,7 +136,7 @@ class CanvasGraph(tk.Canvas): self.show_link_labels.set(True) self.show_grid.set(True) self.show_annotations.set(True) - self.show_interface_names.set(False) + self.show_iface_names.set(False) self.show_ip4s.set(True) self.show_ip6s.set(True) @@ -195,19 +195,19 @@ class CanvasGraph(tk.Canvas): return valid_topleft and valid_bottomright def set_throughputs(self, throughputs_event: core_pb2.ThroughputsEvent): - for interface_throughput in throughputs_event.interface_throughputs: - node_id = interface_throughput.node_id - interface_id = interface_throughput.interface_id - throughput = interface_throughput.throughput - interface_to_edge_id = (node_id, interface_id) - token = self.core.interface_to_edge.get(interface_to_edge_id) + for iface_throughput in throughputs_event.iface_throughputs: + node_id = iface_throughput.node_id + 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) if edge: edge.set_throughput(throughput) else: - del self.core.interface_to_edge[interface_to_edge_id] + del self.core.iface_to_edge[iface_to_edge_id] def draw_grid(self): """ @@ -321,18 +321,16 @@ class CanvasGraph(tk.Canvas): canvas_node2.edges.add(edge) self.edges[edge.token] = edge self.core.links[edge.token] = edge - if link.HasField("interface1"): - interface1 = link.interface1 - self.core.interface_to_edge[(node1.id, interface1.id)] = token - canvas_node1.interfaces[interface1.id] = interface1 - edge.src_interface = interface1 - if link.HasField("interface2"): - interface2 = link.interface2 - self.core.interface_to_edge[ - (node2.id, interface2.id) - ] = edge.token - canvas_node2.interfaces[interface2.id] = interface2 - edge.dst_interface = interface2 + if link.HasField("iface1"): + iface1 = link.iface1 + self.core.iface_to_edge[(node1.id, iface1.id)] = token + canvas_node1.ifaces[iface1.id] = iface1 + edge.src_iface = iface1 + if link.HasField("iface2"): + iface2 = link.iface2 + self.core.iface_to_edge[(node2.id, iface2.id)] = edge.token + canvas_node2.ifaces[iface2.id] = iface2 + edge.dst_iface = iface2 elif link.options.unidirectional: edge = self.edges[token] edge.asymmetric_link = link @@ -513,14 +511,14 @@ class CanvasGraph(tk.Canvas): edge.delete() # update node connected to edge being deleted other_id = edge.src - other_interface = edge.src_interface + other_iface = edge.src_iface if edge.src == object_id: other_id = edge.dst - other_interface = edge.dst_interface + other_iface = edge.dst_iface other_node = self.nodes[other_id] other_node.edges.remove(edge) - if other_interface: - del other_node.interfaces[other_interface.id] + if other_iface: + del other_node.ifaces[other_iface.id] if is_wireless: other_node.delete_antenna() @@ -538,12 +536,12 @@ class CanvasGraph(tk.Canvas): del self.edges[edge.token] src_node = self.nodes[edge.src] src_node.edges.discard(edge) - if edge.src_interface: - del src_node.interfaces[edge.src_interface.id] + if edge.src_iface: + del src_node.ifaces[edge.src_iface.id] dst_node = self.nodes[edge.dst] dst_node.edges.discard(edge) - if edge.dst_interface: - del dst_node.interfaces[edge.dst_interface.id] + if edge.dst_iface: + del dst_node.ifaces[edge.dst_iface.id] src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type) if src_wireless: dst_node.delete_antenna() @@ -963,26 +961,26 @@ class CanvasGraph(tk.Canvas): copy_link = copy_edge.link options = edge.link.options copy_link.options.CopyFrom(options) - interface1_id = None - if copy_link.HasField("interface1"): - interface1_id = copy_link.interface1.id - interface2_id = None - if copy_link.HasField("interface2"): - interface2_id = copy_link.interface2.id + iface1_id = None + if copy_link.HasField("iface1"): + iface1_id = copy_link.iface1.id + iface2_id = None + if copy_link.HasField("iface2"): + iface2_id = copy_link.iface2.id if not options.unidirectional: copy_edge.asymmetric_link = None else: - asym_interface1 = None - if interface1_id: - asym_interface1 = core_pb2.Interface(id=interface1_id) - asym_interface2 = None - if interface2_id: - asym_interface2 = core_pb2.Interface(id=interface2_id) + asym_iface1 = None + if iface1_id: + asym_iface1 = core_pb2.Interface(id=iface1_id) + asym_iface2 = None + if iface2_id: + asym_iface2 = core_pb2.Interface(id=iface2_id) copy_edge.asymmetric_link = core_pb2.Link( node1_id=copy_link.node2_id, node2_id=copy_link.node1_id, - interface1=asym_interface1, - interface2=asym_interface2, + iface1=asym_iface1, + iface2=asym_iface2, options=edge.asymmetric_link.options, ) self.itemconfig( diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 8ad3f02a..3ba4b3f7 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -55,7 +55,7 @@ class CanvasNode: ) self.tooltip = CanvasTooltip(self.canvas) self.edges = set() - self.interfaces = {} + self.ifaces = {} self.wireless_edges = set() self.antennas = [] self.antenna_images = {} @@ -70,9 +70,9 @@ class CanvasNode: self.context = tk.Menu(self.canvas) themes.style_menu(self.context) - def next_interface_id(self) -> int: + def next_iface_id(self) -> int: i = 0 - while i in self.interfaces: + while i in self.ifaces: i += 1 return i @@ -300,16 +300,16 @@ class CanvasNode: dialog = NodeConfigServiceDialog(self.app, self) dialog.show() - def has_emane_link(self, interface_id: int) -> core_pb2.Node: + def has_emane_link(self, iface_id: int) -> core_pb2.Node: result = None for edge in self.edges: if self.id == edge.src: other_id = edge.dst - edge_interface_id = edge.src_interface.id + edge_iface_id = edge.src_iface.id else: other_id = edge.src - edge_interface_id = edge.dst_interface.id - if edge_interface_id != interface_id: + edge_iface_id = edge.dst_iface.id + if edge_iface_id != iface_id: continue other_node = self.canvas.nodes[other_id] if other_node.core_node.type == NodeType.EMANE: diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index 34270f56..14cba024 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -12,10 +12,10 @@ if TYPE_CHECKING: from core.gui.graph.node import CanvasNode -def get_index(interface: "core_pb2.Interface") -> Optional[int]: - if not interface.ip4: +def get_index(iface: "core_pb2.Interface") -> Optional[int]: + if not iface.ip4: return None - net = netaddr.IPNetwork(f"{interface.ip4}/{interface.ip4mask}") + net = netaddr.IPNetwork(f"{iface.ip4}/{iface.ip4mask}") ip_value = net.value cidr_value = net.cidr.value return ip_value - cidr_value @@ -89,43 +89,43 @@ class InterfaceManager: remaining_subnets = set() for edge in self.app.core.links.values(): link = edge.link - if link.HasField("interface1"): - subnets = self.get_subnets(link.interface1) + if link.HasField("iface1"): + subnets = self.get_subnets(link.iface1) remaining_subnets.add(subnets) - if link.HasField("interface2"): - subnets = self.get_subnets(link.interface2) + if link.HasField("iface2"): + subnets = self.get_subnets(link.iface2) remaining_subnets.add(subnets) # remove all subnets from used subnets when no longer present # or remove used indexes from subnet - interfaces = [] + ifaces = [] for link in links: - if link.HasField("interface1"): - interfaces.append(link.interface1) - if link.HasField("interface2"): - interfaces.append(link.interface2) - for interface in interfaces: - subnets = self.get_subnets(interface) + if link.HasField("iface1"): + ifaces.append(link.iface1) + if link.HasField("iface2"): + ifaces.append(link.iface2) + for iface in ifaces: + subnets = self.get_subnets(iface) if subnets not in remaining_subnets: self.used_subnets.pop(subnets.key(), None) else: - index = get_index(interface) + index = get_index(iface) if index is not None: subnets.used_indexes.discard(index) self.current_subnets = None def joined(self, links: List["core_pb2.Link"]) -> None: - interfaces = [] + ifaces = [] for link in links: - if link.HasField("interface1"): - interfaces.append(link.interface1) - if link.HasField("interface2"): - interfaces.append(link.interface2) + if link.HasField("iface1"): + ifaces.append(link.iface1) + if link.HasField("iface2"): + ifaces.append(link.iface2) # add to used subnets and mark used indexes - for interface in interfaces: - subnets = self.get_subnets(interface) - index = get_index(interface) + for iface in ifaces: + subnets = self.get_subnets(iface) + index = get_index(iface) if index is None: continue subnets.used_indexes.add(index) @@ -150,13 +150,13 @@ class InterfaceManager: ip6 = self.current_subnets.ip6[index] return str(ip4), str(ip6) - def get_subnets(self, interface: "core_pb2.Interface") -> Subnets: + def get_subnets(self, iface: "core_pb2.Interface") -> Subnets: ip4_subnet = self.ip4_subnets - if interface.ip4: - ip4_subnet = IPNetwork(f"{interface.ip4}/{interface.ip4mask}").cidr + if iface.ip4: + ip4_subnet = IPNetwork(f"{iface.ip4}/{iface.ip4mask}").cidr ip6_subnet = self.ip6_subnets - if interface.ip6: - ip6_subnet = IPNetwork(f"{interface.ip6}/{interface.ip6mask}").cidr + if iface.ip6: + ip6_subnet = IPNetwork(f"{iface.ip6}/{iface.ip6mask}").cidr subnets = Subnets(ip4_subnet, ip6_subnet) return self.used_subnets.get(subnets.key(), subnets) @@ -196,16 +196,16 @@ class InterfaceManager: for edge in canvas_node.edges: src_node = canvas.nodes[edge.src] dst_node = canvas.nodes[edge.dst] - interface = edge.src_interface + iface = edge.src_iface check_node = src_node if src_node == canvas_node: - interface = edge.dst_interface + iface = edge.dst_iface check_node = dst_node if check_node.core_node.id in visited: continue visited.add(check_node.core_node.id) - if interface: - subnets = self.get_subnets(interface) + if iface: + subnets = self.get_subnets(iface) else: subnets = self.find_subnets(check_node, visited) if subnets: diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 62a9ceae..cf4216d8 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -139,7 +139,7 @@ class Menubar(tk.Menu): menu.add_checkbutton( label="Interface Names", command=self.click_edge_label_change, - variable=self.canvas.show_interface_names, + variable=self.canvas.show_iface_names, ) menu.add_checkbutton( label="IPv4 Addresses", diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 43996ba3..d56c40aa 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -178,7 +178,7 @@ class MobilityManager(ModelManager): self.session.broadcast_event(event_data) def updatewlans( - self, moved: List[CoreNode], moved_netifs: List[CoreInterface] + self, moved: List[CoreNode], moved_ifaces: List[CoreInterface] ) -> None: """ A mobility script has caused nodes in the 'moved' list to move. @@ -186,7 +186,7 @@ class MobilityManager(ModelManager): were to recalculate for each individual node movement. :param moved: moved nodes - :param moved_netifs: moved network interfaces + :param moved_ifaces: moved network interfaces :return: nothing """ for node_id in self.nodes(): @@ -195,7 +195,7 @@ class MobilityManager(ModelManager): except CoreError: continue if node.model: - node.model.update(moved, moved_netifs) + node.model.update(moved, moved_ifaces) class WirelessModel(ConfigurableOptions): @@ -228,12 +228,12 @@ class WirelessModel(ConfigurableOptions): """ return [] - def update(self, moved: List[CoreNode], moved_netifs: List[CoreInterface]) -> None: + def update(self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]) -> None: """ Update this wireless model. :param moved: moved nodes - :param moved_netifs: moved network interfaces + :param moved_ifaces: moved network interfaces :return: nothing """ raise NotImplementedError @@ -301,8 +301,8 @@ class BasicRangeModel(WirelessModel): super().__init__(session, _id) self.session: "Session" = session self.wlan: WlanNode = session.get_node(_id, WlanNode) - self._netifs: Dict[CoreInterface, Tuple[float, float, float]] = {} - self._netifslock: threading.Lock = threading.Lock() + self.iface_to_pos: Dict[CoreInterface, Tuple[float, float, float]] = {} + self.iface_lock: threading.Lock = threading.Lock() self.range: int = 0 self.bw: Optional[int] = None self.delay: Optional[int] = None @@ -333,48 +333,48 @@ class BasicRangeModel(WirelessModel): Apply link parameters to all interfaces. This is invoked from WlanNode.setmodel() after the position callback has been set. """ - with self._netifslock: - for netif in self._netifs: + with self.iface_lock: + for iface in self.iface_to_pos: options = LinkOptions( bandwidth=self.bw, delay=self.delay, loss=self.loss, jitter=self.jitter, ) - self.wlan.linkconfig(netif, options) + self.wlan.linkconfig(iface, options) - def get_position(self, netif: CoreInterface) -> Tuple[float, float, float]: + def get_position(self, iface: CoreInterface) -> Tuple[float, float, float]: """ Retrieve network interface position. - :param netif: network interface position to retrieve + :param iface: network interface position to retrieve :return: network interface position """ - with self._netifslock: - return self._netifs[netif] + with self.iface_lock: + return self.iface_to_pos[iface] - def set_position(self, netif: CoreInterface) -> None: + def set_position(self, iface: CoreInterface) -> None: """ A node has moved; given an interface, a new (x,y,z) position has been set; calculate the new distance between other nodes and link or unlink node pairs based on the configured range. - :param netif: network interface to set position for + :param iface: network interface to set position for :return: nothing """ - x, y, z = netif.node.position.get() - self._netifslock.acquire() - self._netifs[netif] = (x, y, z) + x, y, z = iface.node.position.get() + self.iface_lock.acquire() + self.iface_to_pos[iface] = (x, y, z) if x is None or y is None: - self._netifslock.release() + self.iface_lock.release() return - for netif2 in self._netifs: - self.calclink(netif, netif2) - self._netifslock.release() + for iface2 in self.iface_to_pos: + self.calclink(iface, iface2) + self.iface_lock.release() position_callback = set_position - def update(self, moved: List[CoreNode], moved_netifs: List[CoreInterface]) -> None: + def update(self, moved: List[CoreNode], 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. @@ -382,37 +382,37 @@ class BasicRangeModel(WirelessModel): one of the nodes has moved. :param moved: moved nodes - :param moved_netifs: moved network interfaces + :param moved_ifaces: moved network interfaces :return: nothing """ - with self._netifslock: - while len(moved_netifs): - netif = moved_netifs.pop() - nx, ny, nz = netif.node.getposition() - if netif in self._netifs: - self._netifs[netif] = (nx, ny, nz) - for netif2 in self._netifs: - if netif2 in moved_netifs: + with self.iface_lock: + while len(moved_ifaces): + iface = moved_ifaces.pop() + nx, ny, nz = iface.node.getposition() + if iface in self.iface_to_pos: + self.iface_to_pos[iface] = (nx, ny, nz) + for iface2 in self.iface_to_pos: + if iface2 in moved_ifaces: continue - self.calclink(netif, netif2) + self.calclink(iface, iface2) - def calclink(self, netif: CoreInterface, netif2: CoreInterface) -> None: + def calclink(self, iface: CoreInterface, iface2: CoreInterface) -> None: """ Helper used by set_position() and update() to calculate distance between two interfaces and perform linking/unlinking. Sends link/unlink messages and updates the WlanNode's linked dict. - :param netif: interface one - :param netif2: interface two + :param iface: interface one + :param iface2: interface two :return: nothing """ - if netif == netif2: + if iface == iface2: return try: - x, y, z = self._netifs[netif] - x2, y2, z2 = self._netifs[netif2] + x, y, z = self.iface_to_pos[iface] + x2, y2, z2 = self.iface_to_pos[iface2] if x2 is None or y2 is None: return @@ -420,8 +420,8 @@ class BasicRangeModel(WirelessModel): d = self.calcdistance((x, y, z), (x2, y2, z2)) # ordering is important, to keep the wlan._linked dict organized - a = min(netif, netif2) - b = max(netif, netif2) + a = min(iface, iface2) + b = max(iface, iface2) with self.wlan._linked_lock: linked = self.wlan.linked(a, b) @@ -475,42 +475,39 @@ class BasicRangeModel(WirelessModel): self.setlinkparams() def create_link_data( - self, - interface1: CoreInterface, - interface2: CoreInterface, - message_type: MessageFlags, + self, iface1: CoreInterface, iface2: CoreInterface, message_type: MessageFlags ) -> LinkData: """ Create a wireless link/unlink data message. - :param interface1: interface one - :param interface2: interface two + :param iface1: interface one + :param iface2: interface two :param message_type: link message type :return: link data """ color = self.session.get_link_color(self.wlan.id) return LinkData( message_type=message_type, - node1_id=interface1.node.id, - node2_id=interface2.node.id, + node1_id=iface1.node.id, + node2_id=iface2.node.id, network_id=self.wlan.id, link_type=LinkTypes.WIRELESS, color=color, ) def sendlinkmsg( - self, netif: CoreInterface, netif2: CoreInterface, unlink: bool = False + self, iface: CoreInterface, iface2: CoreInterface, unlink: bool = False ) -> None: """ Send a wireless link/unlink API message to the GUI. - :param netif: interface one - :param netif2: interface two + :param iface: interface one + :param iface2: interface two :param unlink: unlink or not :return: nothing """ message_type = MessageFlags.DELETE if unlink else MessageFlags.ADD - link_data = self.create_link_data(netif, netif2, message_type) + link_data = self.create_link_data(iface, iface2, message_type) self.session.broadcast_link(link_data) def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: @@ -643,17 +640,17 @@ class WayPointMobility(WirelessModel): return return self.run() - # only move netifs attached to self.wlan, or all nodenum in script? + # only move interfaces attached to self.wlan, or all nodenum in script? moved = [] - moved_netifs = [] - for netif in self.wlan.netifs(): - node = netif.node + moved_ifaces = [] + for iface in self.wlan.get_ifaces(): + node = iface.node if self.movenode(node, dt): moved.append(node) - moved_netifs.append(netif) + moved_ifaces.append(iface) # calculate all ranges after moving nodes; this saves calculations - self.session.mobility.updatewlans(moved, moved_netifs) + self.session.mobility.updatewlans(moved, moved_ifaces) # TODO: check session state self.session.event_loop.add_event(0.001 * self.refresh_ms, self.runround) @@ -725,16 +722,16 @@ class WayPointMobility(WirelessModel): :return: nothing """ moved = [] - moved_netifs = [] - for netif in self.wlan.netifs(): - node = netif.node + moved_ifaces = [] + for iface in self.wlan.get_ifaces(): + node = iface.node if node.id not in self.initial: continue x, y, z = self.initial[node.id].coords self.setnodeposition(node, x, y, z) moved.append(node) - moved_netifs.append(netif) - self.session.mobility.updatewlans(moved, moved_netifs) + moved_ifaces.append(iface) + self.session.mobility.updatewlans(moved, moved_ifaces) def addwaypoint( self, diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 6c7ebcf0..40aae6a8 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -68,8 +68,8 @@ class NodeBase(abc.ABC): self.server: "DistributedServer" = server self.type: Optional[str] = None self.services: CoreServices = [] - self._netif: Dict[int, CoreInterface] = {} - self.ifindex: int = 0 + self.ifaces: Dict[int, CoreInterface] = {} + self.iface_id: int = 0 self.canvas: Optional[int] = None self.icon: Optional[str] = None self.opaque: Optional[str] = None @@ -139,58 +139,50 @@ class NodeBase(abc.ABC): """ return self.position.get() - def ifname(self, ifindex: int) -> str: - """ - Retrieve interface name for index. + def get_iface(self, iface_id: int) -> CoreInterface: + if iface_id not in self.ifaces: + raise CoreError(f"node({self.name}) does not have interface({iface_id})") + return self.ifaces[iface_id] - :param ifindex: interface index - :return: interface name + def get_ifaces(self, control: bool = True) -> List[CoreInterface]: """ - return self._netif[ifindex].name + Retrieve sorted list of interfaces, optionally do not include control + interfaces. - def netifs(self, sort: bool = False) -> List[CoreInterface]: + :param control: False to exclude control interfaces, included otherwise + :return: list of interfaces """ - Retrieve network interfaces, sorted if desired. + ifaces = [] + for iface_id in sorted(self.ifaces): + iface = self.ifaces[iface_id] + if not control and getattr(iface, "control", False): + continue + ifaces.append(iface) + return ifaces - :param sort: boolean used to determine if interfaces should be sorted - :return: network interfaces + def get_iface_id(self, iface: CoreInterface) -> int: """ - if sort: - return [self._netif[x] for x in sorted(self._netif)] - else: - return list(self._netif.values()) + Retrieve id for an interface. - def numnetif(self) -> int: - """ - Return the attached interface count. - - :return: number of network interfaces - """ - return len(self._netif) - - def getifindex(self, netif: CoreInterface) -> int: - """ - Retrieve index for an interface. - - :param netif: interface to get index for + :param iface: interface to get id for :return: interface index if found, -1 otherwise """ - for ifindex in self._netif: - if self._netif[ifindex] is netif: - return ifindex - return -1 + for iface_id, local_iface in self.ifaces.items(): + if local_iface is iface: + return iface_id + raise CoreError(f"node({self.name}) does not have interface({iface.name})") - def newifindex(self) -> int: + def next_iface_id(self) -> int: """ Create a new interface index. :return: interface index """ - while self.ifindex in self._netif: - self.ifindex += 1 - ifindex = self.ifindex - self.ifindex += 1 - return ifindex + while self.iface_id in self.ifaces: + self.iface_id += 1 + iface_id = self.iface_id + self.iface_id += 1 + return iface_id def data( self, message_type: MessageFlags = MessageFlags.NONE, source: str = None @@ -325,14 +317,14 @@ class CoreNodeBase(NodeBase): raise NotImplementedError @abc.abstractmethod - def newnetif( - self, net: "CoreNetworkBase", interface_data: InterfaceData + def new_iface( + self, net: "CoreNetworkBase", iface_data: InterfaceData ) -> CoreInterface: """ - Create a new network interface. + Create a new interface. :param net: network to associate with - :param interface_data: interface data for new interface + :param iface_data: interface data for new interface :return: interface index """ raise NotImplementedError @@ -399,67 +391,53 @@ class CoreNodeBase(NodeBase): if self.tmpnodedir: self.host_cmd(f"rm -rf {self.nodedir}") - def addnetif(self, netif: CoreInterface, ifindex: int) -> None: + def add_iface(self, iface: CoreInterface, iface_id: int) -> None: """ Add network interface to node and set the network interface index if successful. - :param netif: network interface to add - :param ifindex: interface index + :param iface: network interface to add + :param iface_id: interface id :return: nothing """ - if ifindex in self._netif: - raise ValueError(f"ifindex {ifindex} already exists") - self._netif[ifindex] = netif - netif.netindex = ifindex + if iface_id in self.ifaces: + raise CoreError(f"interface({iface_id}) already exists") + self.ifaces[iface_id] = iface + iface.node_id = iface_id - def delnetif(self, ifindex: int) -> None: + def delete_iface(self, iface_id: int) -> None: """ Delete a network interface - :param ifindex: interface index to delete + :param iface_id: interface index to delete :return: nothing """ - if ifindex not in self._netif: - raise CoreError(f"node({self.name}) ifindex({ifindex}) does not exist") - netif = self._netif.pop(ifindex) - logging.info("node(%s) removing interface(%s)", self.name, netif.name) - netif.detachnet() - netif.shutdown() + if iface_id not in self.ifaces: + raise CoreError(f"node({self.name}) interface({iface_id}) does not exist") + iface = self.ifaces.pop(iface_id) + logging.info("node(%s) removing interface(%s)", self.name, iface.name) + iface.detachnet() + iface.shutdown() - def netif(self, ifindex: int) -> Optional[CoreInterface]: - """ - Retrieve network interface. - - :param ifindex: index of interface to retrieve - :return: network interface, or None if not found - """ - if ifindex in self._netif: - return self._netif[ifindex] - else: - return None - - def attachnet(self, ifindex: int, net: "CoreNetworkBase") -> None: + def attachnet(self, iface_id: int, net: "CoreNetworkBase") -> None: """ Attach a network. - :param ifindex: interface of index to attach + :param iface_id: interface of index to attach :param net: network to attach :return: nothing """ - if ifindex not in self._netif: - raise ValueError(f"ifindex {ifindex} does not exist") - self._netif[ifindex].attachnet(net) + iface = self.get_iface(iface_id) + iface.attachnet(net) - def detachnet(self, ifindex: int) -> None: + def detachnet(self, iface_id: int) -> None: """ Detach network interface. - :param ifindex: interface index to detach + :param iface_id: interface id to detach :return: nothing """ - if ifindex not in self._netif: - raise ValueError(f"ifindex {ifindex} does not exist") - self._netif[ifindex].detachnet() + iface = self.get_iface(iface_id) + iface.detachnet() def setposition(self, x: float = None, y: float = None, z: float = None) -> None: """ @@ -472,8 +450,8 @@ class CoreNodeBase(NodeBase): """ changed = super().setposition(x, y, z) if changed: - for netif in self.netifs(sort=True): - netif.setposition() + for iface in self.get_ifaces(): + iface.setposition() def commonnets( self, node: "CoreNodeBase", want_ctrl: bool = False @@ -488,12 +466,10 @@ class CoreNodeBase(NodeBase): :return: tuples of common networks """ common = [] - for netif1 in self.netifs(): - if not want_ctrl and hasattr(netif1, "control"): - continue - for netif2 in node.netifs(): - if netif1.net == netif2.net: - common.append((netif1.net, netif1, netif2)) + for iface1 in self.get_ifaces(control=want_ctrl): + for iface2 in node.get_ifaces(): + if iface1.net == iface2.net: + common.append((iface1.net, iface1, iface2)) return common @@ -620,8 +596,8 @@ class CoreNode(CoreNodeBase): self._mounts = [] # shutdown all interfaces - for netif in self.netifs(): - netif.shutdown() + for iface in self.get_ifaces(): + iface.shutdown() # kill node process if present try: @@ -636,7 +612,7 @@ class CoreNode(CoreNodeBase): logging.exception("error removing node directory") # clear interface data, close client, and mark self and not up - self._netif.clear() + self.ifaces.clear() self.client.close() self.up = False except OSError: @@ -704,36 +680,36 @@ class CoreNode(CoreNodeBase): self.cmd(f"{MOUNT_BIN} -n --bind {source} {target}") self._mounts.append((source, target)) - def newifindex(self) -> int: + def next_iface_id(self) -> int: """ Retrieve a new interface index. :return: new interface index """ with self.lock: - return super().newifindex() + return super().next_iface_id() - def newveth(self, ifindex: int = None, ifname: str = None) -> int: + def newveth(self, iface_id: int = None, ifname: str = None) -> int: """ Create a new interface. - :param ifindex: index for the new interface + :param iface_id: id for the new interface :param ifname: name for the new interface :return: nothing """ with self.lock: - if ifindex is None: - ifindex = self.newifindex() + if iface_id is None: + iface_id = self.next_iface_id() if ifname is None: - ifname = f"eth{ifindex}" + ifname = f"eth{iface_id}" sessionid = self.session.short_session_id() try: - suffix = f"{self.id:x}.{ifindex}.{sessionid}" + suffix = f"{self.id:x}.{iface_id}.{sessionid}" except TypeError: - suffix = f"{self.id}.{ifindex}.{sessionid}" + suffix = f"{self.id}.{iface_id}.{sessionid}" localname = f"veth{suffix}" if len(localname) >= 16: @@ -765,140 +741,138 @@ class CoreNode(CoreNodeBase): try: # add network interface to the node. If unsuccessful, destroy the # network interface and raise exception. - self.addnetif(veth, ifindex) + self.add_iface(veth, iface_id) except ValueError as e: veth.shutdown() del veth raise e - return ifindex + return iface_id - def newtuntap(self, ifindex: int = None, ifname: str = None) -> int: + def newtuntap(self, iface_id: int = None, ifname: str = None) -> int: """ Create a new tunnel tap. - :param ifindex: interface index + :param iface_id: interface id :param ifname: interface name :return: interface index """ with self.lock: - if ifindex is None: - ifindex = self.newifindex() + if iface_id is None: + iface_id = self.next_iface_id() if ifname is None: - ifname = f"eth{ifindex}" + ifname = f"eth{iface_id}" sessionid = self.session.short_session_id() - localname = f"tap{self.id}.{ifindex}.{sessionid}" + localname = f"tap{self.id}.{iface_id}.{sessionid}" name = ifname tuntap = TunTap(self.session, self, name, localname, start=self.up) try: - self.addnetif(tuntap, ifindex) + self.add_iface(tuntap, iface_id) except ValueError as e: tuntap.shutdown() del tuntap raise e - return ifindex + return iface_id - def sethwaddr(self, ifindex: int, addr: str) -> None: + def sethwaddr(self, iface_id: int, addr: str) -> None: """ - Set hardware addres for an interface. + Set hardware address for an interface. - :param ifindex: index of interface to set hardware address for + :param iface_id: id of interface to set hardware address for :param addr: hardware address to set :return: nothing :raises CoreCommandError: when a non-zero exit status occurs """ addr = utils.validate_mac(addr) - interface = self._netif[ifindex] - interface.sethwaddr(addr) + iface = self.get_iface(iface_id) + iface.sethwaddr(addr) if self.up: - self.node_net_client.device_mac(interface.name, addr) + self.node_net_client.device_mac(iface.name, addr) - def addaddr(self, ifindex: int, addr: str) -> None: + def addaddr(self, iface_id: int, addr: str) -> None: """ Add interface address. - :param ifindex: index of interface to add address to + :param iface_id: id of interface to add address to :param addr: address to add to interface :return: nothing """ addr = utils.validate_ip(addr) - interface = self._netif[ifindex] - interface.addaddr(addr) + iface = self.get_iface(iface_id) + iface.addaddr(addr) if self.up: # ipv4 check broadcast = None if netaddr.valid_ipv4(addr): broadcast = "+" - self.node_net_client.create_address(interface.name, addr, broadcast) + self.node_net_client.create_address(iface.name, addr, broadcast) - def deladdr(self, ifindex: int, addr: str) -> None: + def deladdr(self, iface_id: int, addr: str) -> None: """ Delete address from an interface. - :param ifindex: index of interface to delete address from + :param iface_id: id of interface to delete address from :param addr: address to delete from interface :return: nothing :raises CoreCommandError: when a non-zero exit status occurs """ - interface = self._netif[ifindex] - + iface = self.get_iface(iface_id) try: - interface.deladdr(addr) + iface.deladdr(addr) except ValueError: logging.exception("trying to delete unknown address: %s", addr) - if self.up: - self.node_net_client.delete_address(interface.name, addr) + self.node_net_client.delete_address(iface.name, addr) - def ifup(self, ifindex: int) -> None: + def ifup(self, iface_id: int) -> None: """ Bring an interface up. - :param ifindex: index of interface to bring up + :param iface_id: index of interface to bring up :return: nothing """ if self.up: - interface_name = self.ifname(ifindex) - self.node_net_client.device_up(interface_name) + iface = self.get_iface(iface_id) + self.node_net_client.device_up(iface.name) - def newnetif( - self, net: "CoreNetworkBase", interface_data: InterfaceData + def new_iface( + self, net: "CoreNetworkBase", iface_data: InterfaceData ) -> CoreInterface: """ Create a new network interface. :param net: network to associate with - :param interface_data: interface data for new interface + :param iface_data: interface data for new interface :return: interface index """ - addresses = interface_data.get_addresses() + addresses = iface_data.get_addresses() with self.lock: # TODO: emane specific code if net.is_emane is True: - ifindex = self.newtuntap(interface_data.id, interface_data.name) + iface_id = self.newtuntap(iface_data.id, iface_data.name) # TUN/TAP is not ready for addressing yet; the device may # take some time to appear, and installing it into a # namespace after it has been bound removes addressing; # save addresses with the interface now - self.attachnet(ifindex, net) - netif = self.netif(ifindex) - netif.sethwaddr(interface_data.mac) + self.attachnet(iface_id, net) + iface = self.get_iface(iface_id) + iface.sethwaddr(iface_data.mac) for address in addresses: - netif.addaddr(address) + iface.addaddr(address) else: - ifindex = self.newveth(interface_data.id, interface_data.name) - self.attachnet(ifindex, net) - if interface_data.mac: - self.sethwaddr(ifindex, interface_data.mac) + iface_id = self.newveth(iface_data.id, iface_data.name) + self.attachnet(iface_id, net) + if iface_data.mac: + self.sethwaddr(iface_id, iface_data.mac) for address in addresses: - self.addaddr(ifindex, address) - self.ifup(ifindex) - netif = self.netif(ifindex) - return netif + self.addaddr(iface_id, address) + self.ifup(iface_id) + iface = self.get_iface(iface_id) + return iface def addfile(self, srcname: str, filename: str) -> None: """ @@ -1041,54 +1015,54 @@ class CoreNetworkBase(NodeBase): @abc.abstractmethod def linkconfig( - self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None + self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None ) -> None: """ Configure link parameters by applying tc queuing disciplines on the interface. - :param netif: interface one + :param iface: interface one :param options: options for configuring link - :param netif2: interface two + :param iface2: interface two :return: nothing """ raise NotImplementedError - def getlinknetif(self, net: "CoreNetworkBase") -> Optional[CoreInterface]: + def get_linked_iface(self, net: "CoreNetworkBase") -> Optional[CoreInterface]: """ - Return the interface of that links this net with another net. + Return the interface that links this net with another net. :param net: interface to get link for :return: interface the provided network is linked to """ - for netif in self.netifs(): - if netif.othernet == net: - return netif + for iface in self.get_ifaces(): + if iface.othernet == net: + return iface return None - def attach(self, netif: CoreInterface) -> None: + def attach(self, iface: CoreInterface) -> None: """ Attach network interface. - :param netif: network interface to attach + :param iface: network interface to attach :return: nothing """ - i = self.newifindex() - self._netif[i] = netif - netif.netifi = i + i = self.next_iface_id() + self.ifaces[i] = iface + iface.net_id = i with self._linked_lock: - self._linked[netif] = {} + self._linked[iface] = {} - def detach(self, netif: CoreInterface) -> None: + def detach(self, iface: CoreInterface) -> None: """ Detach network interface. - :param netif: network interface to detach + :param iface: network interface to detach :return: nothing """ - del self._netif[netif.netifi] - netif.netifi = None + del self.ifaces[iface.net_id] + iface.net_id = None with self._linked_lock: - del self._linked[netif] + del self._linked[iface] def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ @@ -1102,41 +1076,39 @@ class CoreNetworkBase(NodeBase): # build a link message from this network node to each node having a # connected interface - for netif in self.netifs(sort=True): - if not hasattr(netif, "node"): - continue + for iface in self.get_ifaces(): uni = False - linked_node = netif.node + linked_node = iface.node if linked_node is None: # two layer-2 switches/hubs linked together via linknet() - if not netif.othernet: + if not iface.othernet: continue - linked_node = netif.othernet + linked_node = iface.othernet if linked_node.id == self.id: continue - netif.swapparams("_params_up") - upstream_params = netif.getparams() - netif.swapparams("_params_up") - if netif.getparams() != upstream_params: + iface.swapparams("_params_up") + upstream_params = iface.getparams() + iface.swapparams("_params_up") + if iface.getparams() != upstream_params: uni = True unidirectional = 0 if uni: unidirectional = 1 - interface2_ip4 = None - interface2_ip4_mask = None - interface2_ip6 = None - interface2_ip6_mask = None - for address in netif.addrlist: + iface2_ip4 = None + iface2_ip4_mask = None + iface2_ip6 = None + iface2_ip6_mask = None + for address in iface.addrlist: ip, _sep, mask = address.partition("/") mask = int(mask) if netaddr.valid_ipv4(ip): - interface2_ip4 = ip - interface2_ip4_mask = mask + iface2_ip4 = ip + iface2_ip4_mask = mask else: - interface2_ip6 = ip - interface2_ip6_mask = mask + iface2_ip6 = ip + iface2_ip6_mask = mask link_data = LinkData( message_type=flags, @@ -1144,42 +1116,38 @@ class CoreNetworkBase(NodeBase): node2_id=linked_node.id, link_type=self.linktype, unidirectional=unidirectional, - interface2_id=linked_node.getifindex(netif), - interface2_name=netif.name, - interface2_mac=netif.hwaddr, - interface2_ip4=interface2_ip4, - interface2_ip4_mask=interface2_ip4_mask, - interface2_ip6=interface2_ip6, - interface2_ip6_mask=interface2_ip6_mask, - delay=netif.getparam("delay"), - bandwidth=netif.getparam("bw"), - dup=netif.getparam("duplicate"), - jitter=netif.getparam("jitter"), - loss=netif.getparam("loss"), + iface2_id=linked_node.get_iface_id(iface), + iface2_name=iface.name, + iface2_mac=iface.hwaddr, + iface2_ip4=iface2_ip4, + iface2_ip4_mask=iface2_ip4_mask, + iface2_ip6=iface2_ip6, + iface2_ip6_mask=iface2_ip6_mask, + delay=iface.getparam("delay"), + bandwidth=iface.getparam("bw"), + dup=iface.getparam("duplicate"), + jitter=iface.getparam("jitter"), + loss=iface.getparam("loss"), ) - all_links.append(link_data) if not uni: continue - - netif.swapparams("_params_up") + iface.swapparams("_params_up") link_data = LinkData( message_type=MessageFlags.NONE, node1_id=linked_node.id, node2_id=self.id, link_type=self.linktype, unidirectional=1, - delay=netif.getparam("delay"), - bandwidth=netif.getparam("bw"), - dup=netif.getparam("duplicate"), - jitter=netif.getparam("jitter"), - loss=netif.getparam("loss"), + delay=iface.getparam("delay"), + bandwidth=iface.getparam("bw"), + dup=iface.getparam("duplicate"), + jitter=iface.getparam("jitter"), + loss=iface.getparam("loss"), ) - netif.swapparams("_params_up") - + iface.swapparams("_params_up") all_links.append(link_data) - return all_links diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index e911db74..1ef814ee 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -141,7 +141,7 @@ class DockerNode(CoreNode): return with self.lock: - self._netif.clear() + self.ifaces.clear() self.client.stop_container() self.up = False diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index e73e2989..dc16517f 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -57,11 +57,11 @@ class CoreInterface: self.poshook: Callable[[CoreInterface], None] = lambda x: None # used with EMANE self.transport_type: Optional[TransportType] = None - # node interface index - self.netindex: Optional[int] = None - # net interface index - self.netifi: Optional[int] = None - # index used to find flow data + # id of interface for node + self.node_id: Optional[int] = None + # id of interface for network + self.net_id: Optional[int] = None + # id used to find flow data self.flow_id: Optional[int] = None self.server: Optional["DistributedServer"] = server use_ovs = session.options.get_config("ovs") == "True" @@ -284,19 +284,16 @@ class Veth(CoreInterface): """ if not self.up: return - if self.node: try: self.node.node_net_client.device_flush(self.name) except CoreCommandError: logging.exception("error shutting down interface") - if self.localname: try: self.net_client.delete_device(self.localname) except CoreCommandError: logging.info("link already removed: %s", self.localname) - self.up = False diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index a66791ce..9773cb95 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -126,7 +126,7 @@ class LxcNode(CoreNode): return with self.lock: - self._netif.clear() + self.ifaces.clear() self.client.stop_container() self.up = False @@ -215,7 +215,7 @@ class LxcNode(CoreNode): self.client.copy_file(source, filename) self.cmd(f"chmod {mode:o} {filename}") - def addnetif(self, netif: CoreInterface, ifindex: int) -> None: - super().addnetif(netif, ifindex) + def add_iface(self, iface: CoreInterface, iface_id: int) -> None: + super().add_iface(iface, iface_id) # adding small delay to allow time for adding addresses to work correctly time.sleep(0.5) diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 25a10b99..b6c164b5 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -155,14 +155,14 @@ class LinuxNetClient: """ self.run(f"{TC_BIN} qdisc delete dev {device} root") - def checksums_off(self, interface_name: str) -> None: + def checksums_off(self, iface_name: str) -> None: """ Turns interface checksums off. - :param interface_name: interface to update + :param iface_name: interface to update :return: nothing """ - self.run(f"{ETHTOOL_BIN} -K {interface_name} rx off tx off") + self.run(f"{ETHTOOL_BIN} -K {iface_name} rx off tx off") def create_address(self, device: str, address: str, broadcast: str = None) -> None: """ @@ -250,26 +250,26 @@ class LinuxNetClient: self.device_down(name) self.run(f"{IP_BIN} link delete {name} type bridge") - def set_interface_master(self, bridge_name: str, interface_name: str) -> None: + def set_iface_master(self, bridge_name: str, iface_name: str) -> None: """ Assign interface master to a Linux bridge. :param bridge_name: bridge name - :param interface_name: interface name + :param iface_name: interface name :return: nothing """ - self.run(f"{IP_BIN} link set dev {interface_name} master {bridge_name}") - self.device_up(interface_name) + self.run(f"{IP_BIN} link set dev {iface_name} master {bridge_name}") + self.device_up(iface_name) - def delete_interface(self, bridge_name: str, interface_name: str) -> None: + def delete_iface(self, bridge_name: str, iface_name: str) -> None: """ Delete an interface associated with a Linux bridge. :param bridge_name: bridge name - :param interface_name: interface name + :param iface_name: interface name :return: nothing """ - self.run(f"{IP_BIN} link set dev {interface_name} nomaster") + self.run(f"{IP_BIN} link set dev {iface_name} nomaster") def existing_bridges(self, _id: int) -> bool: """ @@ -330,26 +330,26 @@ class OvsNetClient(LinuxNetClient): self.device_down(name) self.run(f"{OVS_BIN} del-br {name}") - def set_interface_master(self, bridge_name: str, interface_name: str) -> None: + def set_iface_master(self, bridge_name: str, iface_name: str) -> None: """ Create an interface associated with a network bridge. :param bridge_name: bridge name - :param interface_name: interface name + :param iface_name: interface name :return: nothing """ - self.run(f"{OVS_BIN} add-port {bridge_name} {interface_name}") - self.device_up(interface_name) + self.run(f"{OVS_BIN} add-port {bridge_name} {iface_name}") + self.device_up(iface_name) - def delete_interface(self, bridge_name: str, interface_name: str) -> None: + def delete_iface(self, bridge_name: str, iface_name: str) -> None: """ Delete an interface associated with a OVS bridge. :param bridge_name: bridge name - :param interface_name: interface name + :param iface_name: interface name :return: nothing """ - self.run(f"{OVS_BIN} del-port {bridge_name} {interface_name}") + self.run(f"{OVS_BIN} del-port {bridge_name} {iface_name}") def existing_bridges(self, _id: int) -> bool: """ diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index b85c2eee..85e3e488 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -216,20 +216,20 @@ class EbtablesQueue: ] ) # rebuild the chain - for netif1, v in wlan._linked.items(): - for netif2, linked in v.items(): + for iface1, v in wlan._linked.items(): + for oface2, linked in v.items(): if wlan.policy == NetworkPolicy.DROP and linked: self.cmds.extend( [ - f"-A {wlan.brname} -i {netif1.localname} -o {netif2.localname} -j ACCEPT", - f"-A {wlan.brname} -o {netif1.localname} -i {netif2.localname} -j ACCEPT", + f"-A {wlan.brname} -i {iface1.localname} -o {oface2.localname} -j ACCEPT", + f"-A {wlan.brname} -o {iface1.localname} -i {oface2.localname} -j ACCEPT", ] ) elif wlan.policy == NetworkPolicy.ACCEPT and not linked: self.cmds.extend( [ - f"-A {wlan.brname} -i {netif1.localname} -o {netif2.localname} -j DROP", - f"-A {wlan.brname} -o {netif1.localname} -i {netif2.localname} -j DROP", + f"-A {wlan.brname} -i {iface1.localname} -o {oface2.localname} -j DROP", + f"-A {wlan.brname} -o {iface1.localname} -i {oface2.localname} -j DROP", ] ) @@ -347,53 +347,53 @@ class CoreNetwork(CoreNetworkBase): logging.exception("error during shutdown") # removes veth pairs used for bridge-to-bridge connections - for netif in self.netifs(): - netif.shutdown() + for iface in self.get_ifaces(): + iface.shutdown() - self._netif.clear() + self.ifaces.clear() self._linked.clear() del self.session self.up = False - def attach(self, netif: CoreInterface) -> None: + def attach(self, iface: CoreInterface) -> None: """ Attach a network interface. - :param netif: network interface to attach + :param iface: network interface to attach :return: nothing """ if self.up: - netif.net_client.set_interface_master(self.brname, netif.localname) - super().attach(netif) + iface.net_client.set_iface_master(self.brname, iface.localname) + super().attach(iface) - def detach(self, netif: CoreInterface) -> None: + def detach(self, iface: CoreInterface) -> None: """ Detach a network interface. - :param netif: network interface to detach + :param iface: network interface to detach :return: nothing """ if self.up: - netif.net_client.delete_interface(self.brname, netif.localname) - super().detach(netif) + iface.net_client.delete_iface(self.brname, iface.localname) + super().detach(iface) - def linked(self, netif1: CoreInterface, netif2: CoreInterface) -> bool: + def linked(self, iface1: CoreInterface, iface2: CoreInterface) -> bool: """ Determine if the provided network interfaces are linked. - :param netif1: interface one - :param netif2: interface two + :param iface1: interface one + :param iface2: interface two :return: True if interfaces are linked, False otherwise """ # check if the network interfaces are attached to this network - if self._netif[netif1.netifi] != netif1: - raise ValueError(f"inconsistency for netif {netif1.name}") + if self.ifaces[iface1.net_id] != iface1: + raise ValueError(f"inconsistency for interface {iface1.name}") - if self._netif[netif2.netifi] != netif2: - raise ValueError(f"inconsistency for netif {netif2.name}") + if self.ifaces[iface2.net_id] != iface2: + raise ValueError(f"inconsistency for interface {iface2.name}") try: - linked = self._linked[netif1][netif2] + linked = self._linked[iface1][iface2] except KeyError: if self.policy == NetworkPolicy.ACCEPT: linked = True @@ -401,93 +401,93 @@ class CoreNetwork(CoreNetworkBase): linked = False else: raise Exception(f"unknown policy: {self.policy.value}") - self._linked[netif1][netif2] = linked + self._linked[iface1][iface2] = linked return linked - def unlink(self, netif1: CoreInterface, netif2: CoreInterface) -> None: + def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None: """ Unlink two interfaces, resulting in adding or removing ebtables filtering rules. - :param netif1: interface one - :param netif2: interface two + :param iface1: interface one + :param iface2: interface two :return: nothing """ with self._linked_lock: - if not self.linked(netif1, netif2): + if not self.linked(iface1, iface2): return - self._linked[netif1][netif2] = False + self._linked[iface1][iface2] = False ebq.ebchange(self) - def link(self, netif1: CoreInterface, netif2: CoreInterface) -> None: + def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None: """ Link two interfaces together, resulting in adding or removing ebtables filtering rules. - :param netif1: interface one - :param netif2: interface two + :param iface1: interface one + :param iface2: interface two :return: nothing """ with self._linked_lock: - if self.linked(netif1, netif2): + if self.linked(iface1, iface2): return - self._linked[netif1][netif2] = True + self._linked[iface1][iface2] = True ebq.ebchange(self) def linkconfig( - self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None + self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None ) -> None: """ Configure link parameters by applying tc queuing disciplines on the interface. - :param netif: interface one + :param iface: interface one :param options: options for configuring link - :param netif2: interface two + :param iface2: interface two :return: nothing """ - devname = netif.localname + devname = iface.localname tc = f"{TC_BIN} qdisc replace dev {devname}" parent = "root" changed = False bw = options.bandwidth - if netif.setparam("bw", bw): + if iface.setparam("bw", bw): # from tc-tbf(8): minimum value for burst is rate / kernel_hz - burst = max(2 * netif.mtu, int(bw / 1000)) + 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}" - netif.host_cmd(cmd) - netif.setparam("has_tbf", True) + iface.host_cmd(cmd) + iface.setparam("has_tbf", True) changed = True - elif netif.getparam("has_tbf") and bw <= 0: + elif iface.getparam("has_tbf") and bw <= 0: if self.up: cmd = f"{TC_BIN} qdisc delete dev {devname} {parent}" - netif.host_cmd(cmd) - netif.setparam("has_tbf", False) + iface.host_cmd(cmd) + iface.setparam("has_tbf", False) # removing the parent removes the child - netif.setparam("has_netem", False) + iface.setparam("has_netem", False) changed = True - if netif.getparam("has_tbf"): + if iface.getparam("has_tbf"): parent = "parent 1:1" netem = "netem" delay = options.delay - changed = max(changed, netif.setparam("delay", delay)) + changed = max(changed, iface.setparam("delay", delay)) loss = options.loss if loss is not None: loss = float(loss) - changed = max(changed, netif.setparam("loss", loss)) + changed = max(changed, iface.setparam("loss", loss)) duplicate = options.dup if duplicate is not None: duplicate = int(duplicate) - changed = max(changed, netif.setparam("duplicate", duplicate)) + changed = max(changed, iface.setparam("duplicate", duplicate)) jitter = options.jitter - changed = max(changed, netif.setparam("jitter", jitter)) + changed = max(changed, iface.setparam("jitter", jitter)) if not changed: return # jitter and delay use the same delay statement @@ -510,19 +510,19 @@ class CoreNetwork(CoreNetworkBase): 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 - if not netif.getparam("has_netem"): + if not iface.getparam("has_netem"): return if self.up: cmd = f"{TC_BIN} qdisc delete dev {devname} {parent} handle 10:" - netif.host_cmd(cmd) - netif.setparam("has_netem", False) + iface.host_cmd(cmd) + iface.setparam("has_netem", False) elif len(netem) > 1: if self.up: cmd = ( f"{TC_BIN} qdisc replace dev {devname} {parent} handle 10: {netem}" ) - netif.host_cmd(cmd) - netif.setparam("has_netem", True) + iface.host_cmd(cmd) + iface.setparam("has_netem", True) def linknet(self, net: CoreNetworkBase) -> CoreInterface: """ @@ -551,19 +551,19 @@ class CoreNetwork(CoreNetworkBase): if len(name) >= 16: raise ValueError(f"interface name {name} too long") - netif = Veth(self.session, None, name, localname, start=self.up) - self.attach(netif) + iface = Veth(self.session, None, name, localname, start=self.up) + self.attach(iface) if net.up and net.brname: - netif.net_client.set_interface_master(net.brname, netif.name) - i = net.newifindex() - net._netif[i] = netif + iface.net_client.set_iface_master(net.brname, iface.name) + i = net.next_iface_id() + net.ifaces[i] = iface with net._linked_lock: - net._linked[netif] = {} - netif.net = self - netif.othernet = net - return netif + net._linked[iface] = {} + iface.net = self + iface.othernet = net + return iface - def getlinknetif(self, net: CoreNetworkBase) -> Optional[CoreInterface]: + def get_linked_iface(self, net: CoreNetworkBase) -> Optional[CoreInterface]: """ Return the interface of that links this net with another net (that were linked using linknet()). @@ -571,9 +571,9 @@ class CoreNetwork(CoreNetworkBase): :param net: interface to get link for :return: interface the provided network is linked to """ - for netif in self.netifs(): - if netif.othernet == net: - return netif + for iface in self.get_ifaces(): + if iface.othernet == net: + return iface return None def addrconfig(self, addrlist: List[str]) -> None: @@ -690,17 +690,17 @@ class GreTapBridge(CoreNetwork): ) self.attach(self.gretap) - def setkey(self, key: int, interface_data: InterfaceData) -> None: + def setkey(self, key: int, iface_data: InterfaceData) -> None: """ Set the GRE key used for the GreTap device. This needs to be set prior to instantiating the GreTap device (before addrconfig). :param key: gre key - :param interface_data: interface data for setting up tunnel key + :param iface_data: interface data for setting up tunnel key :return: nothing """ self.grekey = key - addresses = interface_data.get_addresses() + addresses = iface_data.get_addresses() if addresses: self.addrconfig(addresses) @@ -802,7 +802,7 @@ class CtrlNet(CoreNetwork): self.host_cmd(f"{self.updown_script} {self.brname} startup") if self.serverintf: - self.net_client.set_interface_master(self.brname, self.serverintf) + self.net_client.set_iface_master(self.brname, self.serverintf) def shutdown(self) -> None: """ @@ -812,7 +812,7 @@ class CtrlNet(CoreNetwork): """ if self.serverintf is not None: try: - self.net_client.delete_interface(self.brname, self.serverintf) + self.net_client.delete_iface(self.brname, self.serverintf) except CoreCommandError: logging.exception( "error deleting server interface %s from bridge %s", @@ -850,18 +850,18 @@ class PtpNet(CoreNetwork): policy: NetworkPolicy = NetworkPolicy.ACCEPT - def attach(self, netif: CoreInterface) -> None: + def attach(self, iface: CoreInterface) -> None: """ Attach a network interface, but limit attachment to two interfaces. - :param netif: network interface + :param iface: network interface :return: nothing """ - if len(self._netif) >= 2: + if len(self.ifaces) >= 2: raise ValueError( "Point-to-point links support at most 2 network interfaces" ) - super().attach(netif) + super().attach(iface) def data( self, message_type: MessageFlags = MessageFlags.NONE, source: str = None @@ -886,67 +886,67 @@ class PtpNet(CoreNetwork): """ all_links = [] - if len(self._netif) != 2: + if len(self.ifaces) != 2: return all_links - interface1, interface2 = self._netif.values() + iface1, iface2 = self.get_ifaces() unidirectional = 0 - if interface1.getparams() != interface2.getparams(): + if iface1.getparams() != iface2.getparams(): unidirectional = 1 - interface1_ip4 = None - interface1_ip4_mask = None - interface1_ip6 = None - interface1_ip6_mask = None - for address in interface1.addrlist: + iface1_ip4 = None + iface1_ip4_mask = None + iface1_ip6 = None + iface1_ip6_mask = None + for address in iface1.addrlist: ip, _sep, mask = address.partition("/") mask = int(mask) if netaddr.valid_ipv4(ip): - interface1_ip4 = ip - interface1_ip4_mask = mask + iface1_ip4 = ip + iface1_ip4_mask = mask else: - interface1_ip6 = ip - interface1_ip6_mask = mask + iface1_ip6 = ip + iface1_ip6_mask = mask - interface2_ip4 = None - interface2_ip4_mask = None - interface2_ip6 = None - interface2_ip6_mask = None - for address in interface2.addrlist: + iface2_ip4 = None + iface2_ip4_mask = None + iface2_ip6 = None + iface2_ip6_mask = None + for address in iface2.addrlist: ip, _sep, mask = address.partition("/") mask = int(mask) if netaddr.valid_ipv4(ip): - interface2_ip4 = ip - interface2_ip4_mask = mask + iface2_ip4 = ip + iface2_ip4_mask = mask else: - interface2_ip6 = ip - interface2_ip6_mask = mask + iface2_ip6 = ip + iface2_ip6_mask = mask link_data = LinkData( message_type=flags, - node1_id=interface1.node.id, - node2_id=interface2.node.id, + node1_id=iface1.node.id, + node2_id=iface2.node.id, link_type=self.linktype, unidirectional=unidirectional, - delay=interface1.getparam("delay"), - bandwidth=interface1.getparam("bw"), - loss=interface1.getparam("loss"), - dup=interface1.getparam("duplicate"), - jitter=interface1.getparam("jitter"), - interface1_id=interface1.node.getifindex(interface1), - interface1_name=interface1.name, - interface1_mac=interface1.hwaddr, - interface1_ip4=interface1_ip4, - interface1_ip4_mask=interface1_ip4_mask, - interface1_ip6=interface1_ip6, - interface1_ip6_mask=interface1_ip6_mask, - interface2_id=interface2.node.getifindex(interface2), - interface2_name=interface2.name, - interface2_mac=interface2.hwaddr, - interface2_ip4=interface2_ip4, - interface2_ip4_mask=interface2_ip4_mask, - interface2_ip6=interface2_ip6, - interface2_ip6_mask=interface2_ip6_mask, + delay=iface1.getparam("delay"), + bandwidth=iface1.getparam("bw"), + loss=iface1.getparam("loss"), + dup=iface1.getparam("duplicate"), + jitter=iface1.getparam("jitter"), + iface1_id=iface1.node.get_iface_id(iface1), + iface1_name=iface1.name, + iface1_mac=iface1.hwaddr, + iface1_ip4=iface1_ip4, + iface1_ip4_mask=iface1_ip4_mask, + iface1_ip6=iface1_ip6, + iface1_ip6_mask=iface1_ip6_mask, + iface2_id=iface2.node.get_iface_id(iface2), + iface2_name=iface2.name, + iface2_mac=iface2.hwaddr, + iface2_ip4=iface2_ip4, + iface2_ip4_mask=iface2_ip4_mask, + iface2_ip6=iface2_ip6, + iface2_ip6_mask=iface2_ip6_mask, ) all_links.append(link_data) @@ -956,16 +956,16 @@ class PtpNet(CoreNetwork): link_data = LinkData( message_type=MessageFlags.NONE, link_type=self.linktype, - node1_id=interface2.node.id, - node2_id=interface1.node.id, - delay=interface2.getparam("delay"), - bandwidth=interface2.getparam("bw"), - loss=interface2.getparam("loss"), - dup=interface2.getparam("duplicate"), - jitter=interface2.getparam("jitter"), + node1_id=iface2.node.id, + node2_id=iface1.node.id, + delay=iface2.getparam("delay"), + bandwidth=iface2.getparam("bw"), + loss=iface2.getparam("loss"), + dup=iface2.getparam("duplicate"), + jitter=iface2.getparam("jitter"), unidirectional=1, - interface1_id=interface2.node.getifindex(interface2), - interface2_id=interface1.node.getifindex(interface1), + iface1_id=iface2.node.get_iface_id(iface2), + iface2_id=iface1.node.get_iface_id(iface1), ) all_links.append(link_data) return all_links @@ -1045,17 +1045,17 @@ class WlanNode(CoreNetwork): self.net_client.disable_mac_learning(self.brname) ebq.ebchange(self) - def attach(self, netif: CoreInterface) -> None: + def attach(self, iface: CoreInterface) -> None: """ Attach a network interface. - :param netif: network interface + :param iface: network interface :return: nothing """ - super().attach(netif) + super().attach(iface) if self.model: - netif.poshook = self.model.position_callback - netif.setposition() + iface.poshook = self.model.position_callback + iface.setposition() def setmodel(self, model: "WirelessModelType", config: Dict[str, str]): """ @@ -1068,9 +1068,9 @@ class WlanNode(CoreNetwork): logging.debug("node(%s) setting model: %s", self.name, model.name) if model.config_type == RegisterTlvs.WIRELESS: self.model = model(session=self.session, _id=self.id) - for netif in self.netifs(): - netif.poshook = self.model.position_callback - netif.setposition() + for iface in self.get_ifaces(): + iface.poshook = self.model.position_callback + iface.setposition() self.updatemodel(config) elif model.config_type == RegisterTlvs.MOBILITY: self.mobility = model(session=self.session, _id=self.id) @@ -1088,8 +1088,8 @@ class WlanNode(CoreNetwork): "node(%s) updating model(%s): %s", self.id, self.model.name, config ) self.model.update_config(config) - for netif in self.netifs(): - netif.setposition() + for iface in self.get_ifaces(): + iface.setposition() def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 741fe7d5..555e0ec9 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -51,8 +51,8 @@ class PhysicalNode(CoreNodeBase): _source, target = self._mounts.pop(-1) self.umount(target) - for netif in self.netifs(): - netif.shutdown() + for iface in self.get_ifaces(): + iface.shutdown() self.rmnodedir() @@ -65,117 +65,115 @@ class PhysicalNode(CoreNodeBase): """ return sh - def sethwaddr(self, ifindex: int, addr: str) -> None: + def sethwaddr(self, iface_id: int, addr: str) -> None: """ Set hardware address for an interface. - :param ifindex: index of interface to set hardware address for + :param iface_id: index of interface to set hardware address for :param addr: hardware address to set :return: nothing :raises CoreCommandError: when a non-zero exit status occurs """ addr = utils.validate_mac(addr) - interface = self._netif[ifindex] - interface.sethwaddr(addr) + iface = self.ifaces[iface_id] + iface.sethwaddr(addr) if self.up: - self.net_client.device_mac(interface.name, addr) + self.net_client.device_mac(iface.name, addr) - def addaddr(self, ifindex: int, addr: str) -> None: + def addaddr(self, iface_id: int, addr: str) -> None: """ Add an address to an interface. - :param ifindex: index of interface to add address to + :param iface_id: index of interface to add address to :param addr: address to add :return: nothing """ addr = utils.validate_ip(addr) - interface = self._netif[ifindex] + iface = self.get_iface(iface_id) if self.up: - self.net_client.create_address(interface.name, addr) - interface.addaddr(addr) + self.net_client.create_address(iface.name, addr) + iface.addaddr(addr) - def deladdr(self, ifindex: int, addr: str) -> None: + def deladdr(self, iface_id: int, addr: str) -> None: """ Delete an address from an interface. - :param ifindex: index of interface to delete + :param iface_id: index of interface to delete :param addr: address to delete :return: nothing """ - interface = self._netif[ifindex] - + iface = self.ifaces[iface_id] try: - interface.deladdr(addr) + iface.deladdr(addr) except ValueError: logging.exception("trying to delete unknown address: %s", addr) - if self.up: - self.net_client.delete_address(interface.name, addr) + self.net_client.delete_address(iface.name, addr) - def adoptnetif( - self, netif: CoreInterface, ifindex: int, hwaddr: str, addrlist: List[str] + def adopt_iface( + self, iface: CoreInterface, iface_id: int, hwaddr: str, addrlist: List[str] ) -> None: """ When a link message is received linking this node to another part of the emulation, no new interface is created; instead, adopt the - GreTap netif as the node interface. + GreTap interface as the node interface. """ - netif.name = f"gt{ifindex}" - netif.node = self - self.addnetif(netif, ifindex) + iface.name = f"gt{iface_id}" + iface.node = self + self.add_iface(iface, iface_id) # use a more reasonable name, e.g. "gt0" instead of "gt.56286.150" if self.up: - self.net_client.device_down(netif.localname) - self.net_client.device_name(netif.localname, netif.name) - netif.localname = netif.name + self.net_client.device_down(iface.localname) + self.net_client.device_name(iface.localname, iface.name) + iface.localname = iface.name if hwaddr: - self.sethwaddr(ifindex, hwaddr) + self.sethwaddr(iface_id, hwaddr) for addr in addrlist: - self.addaddr(ifindex, addr) + self.addaddr(iface_id, addr) if self.up: - self.net_client.device_up(netif.localname) + self.net_client.device_up(iface.localname) def linkconfig( - self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None + self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None ) -> None: """ Apply tc queing disciplines using linkconfig. """ linux_bridge = CoreNetwork(self.session) linux_bridge.up = True - linux_bridge.linkconfig(netif, options, netif2) + linux_bridge.linkconfig(iface, options, iface2) del linux_bridge - def newifindex(self) -> int: + def next_iface_id(self) -> int: with self.lock: - while self.ifindex in self._netif: - self.ifindex += 1 - ifindex = self.ifindex - self.ifindex += 1 - return ifindex + while self.iface_id in self.ifaces: + self.iface_id += 1 + iface_id = self.iface_id + self.iface_id += 1 + return iface_id - def newnetif( - self, net: CoreNetworkBase, interface_data: InterfaceData + def new_iface( + self, net: CoreNetworkBase, iface_data: InterfaceData ) -> CoreInterface: logging.info("creating interface") - addresses = interface_data.get_addresses() - ifindex = interface_data.id - if ifindex is None: - ifindex = self.newifindex() - name = interface_data.name + addresses = iface_data.get_addresses() + iface_id = iface_data.id + if iface_id is None: + iface_id = self.next_iface_id() + name = iface_data.name if name is None: - name = f"gt{ifindex}" + name = f"gt{iface_id}" if self.up: # this is reached when this node is linked to a network node # tunnel to net not built yet, so build it now and adopt it _, remote_tap = self.session.distributed.create_gre_tunnel(net, self.server) - self.adoptnetif(remote_tap, ifindex, interface_data.mac, addresses) + self.adopt_iface(remote_tap, iface_id, iface_data.mac, addresses) return remote_tap else: # this is reached when configuring services (self.up=False) - netif = GreTap(node=self, name=name, session=self.session, start=False) - self.adoptnetif(netif, ifindex, interface_data.mac, addresses) - return netif + iface = GreTap(node=self, name=name, session=self.session, start=False) + self.adopt_iface(iface, iface_id, iface_data.mac, addresses) + return iface def privatedir(self, path: str) -> None: if path[0] != "/": @@ -257,10 +255,10 @@ class Rj45Node(CoreNodeBase): will run on, default is None for localhost """ super().__init__(session, _id, name, server) - self.interface = CoreInterface(session, self, name, name, mtu, server) - self.interface.transport_type = TransportType.RAW + self.iface = CoreInterface(session, self, name, name, mtu, server) + self.iface.transport_type = TransportType.RAW self.lock: threading.RLock = threading.RLock() - self.ifindex: Optional[int] = None + self.iface_id: Optional[int] = None self.old_up: bool = False self.old_addrs: List[Tuple[str, Optional[str]]] = [] @@ -273,7 +271,7 @@ class Rj45Node(CoreNodeBase): """ # interface will also be marked up during net.attach() self.savestate() - self.net_client.device_up(self.interface.localname) + self.net_client.device_up(self.iface.localname) self.up = True def shutdown(self) -> None: @@ -285,7 +283,7 @@ class Rj45Node(CoreNodeBase): """ if not self.up: return - localname = self.interface.localname + localname = self.iface.localname self.net_client.device_down(localname) self.net_client.device_flush(localname) try: @@ -295,8 +293,8 @@ class Rj45Node(CoreNodeBase): self.up = False self.restorestate() - def newnetif( - self, net: CoreNetworkBase, interface_data: InterfaceData + def new_iface( + self, net: CoreNetworkBase, iface_data: InterfaceData ) -> CoreInterface: """ This is called when linking with another node. Since this node @@ -304,70 +302,51 @@ class Rj45Node(CoreNodeBase): but attach ourselves to the given network. :param net: new network instance - :param interface_data: interface data for new interface + :param iface_data: interface data for new interface :return: interface index :raises ValueError: when an interface has already been created, one max """ with self.lock: - ifindex = interface_data.id - if ifindex is None: - ifindex = 0 - if self.interface.net is not None: - raise ValueError("RJ45 nodes support at most 1 network interface") - self._netif[ifindex] = self.interface - self.ifindex = ifindex + iface_id = iface_data.id + 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") + self.ifaces[iface_id] = self.iface + self.iface_id = iface_id if net is not None: - self.interface.attachnet(net) - for addr in interface_data.get_addresses(): + self.iface.attachnet(net) + for addr in iface_data.get_addresses(): self.addaddr(addr) - return self.interface + return self.iface - def delnetif(self, ifindex: int) -> None: + def delete_iface(self, iface_id: int) -> None: """ Delete a network interface. - :param ifindex: interface index to delete + :param iface_id: interface index to delete :return: nothing """ - if ifindex is None: - ifindex = 0 - self._netif.pop(ifindex) - if ifindex == self.ifindex: - self.shutdown() - else: - raise ValueError(f"ifindex {ifindex} does not exist") + self.get_iface(iface_id) + self.ifaces.pop(iface_id) + self.shutdown() - def netif( - self, ifindex: int, net: CoreNetworkBase = None - ) -> Optional[CoreInterface]: - """ - This object is considered the network interface, so we only - return self here. This keeps the RJ45Node compatible with - real nodes. + def get_iface(self, iface_id: int) -> CoreInterface: + if iface_id != self.iface_id or iface_id not in self.ifaces: + raise CoreError(f"node({self.name}) interface({iface_id}) does not exist") + return self.iface - :param ifindex: interface index to retrieve - :param net: network to retrieve - :return: a network interface - """ - if net is not None and net == self.interface.net: - return self.interface - if ifindex is None: - ifindex = 0 - if ifindex == self.ifindex: - return self.interface - return None - - def getifindex(self, netif: CoreInterface) -> Optional[int]: + def get_iface_id(self, iface: CoreInterface) -> Optional[int]: """ Retrieve network interface index. - :param netif: network interface to retrieve + :param iface: network interface to retrieve index for :return: interface index, None otherwise """ - if netif != self.interface: - return None - return self.ifindex + if iface is not self.iface: + raise CoreError(f"node({self.name}) does not have interface({iface.name})") + return self.iface_id def addaddr(self, addr: str) -> None: """ @@ -380,7 +359,7 @@ class Rj45Node(CoreNodeBase): addr = utils.validate_ip(addr) if self.up: self.net_client.create_address(self.name, addr) - self.interface.addaddr(addr) + self.iface.addaddr(addr) def deladdr(self, addr: str) -> None: """ @@ -392,7 +371,7 @@ class Rj45Node(CoreNodeBase): """ if self.up: self.net_client.delete_address(self.name, addr) - self.interface.deladdr(addr) + self.iface.deladdr(addr) def savestate(self) -> None: """ @@ -404,7 +383,7 @@ class Rj45Node(CoreNodeBase): """ self.old_up = False self.old_addrs: List[Tuple[str, Optional[str]]] = [] - localname = self.interface.localname + localname = self.iface.localname output = self.net_client.address_show(localname) for line in output.split("\n"): items = line.split() @@ -429,7 +408,7 @@ class Rj45Node(CoreNodeBase): :return: nothing :raises CoreCommandError: when there is a command exception """ - localname = self.interface.localname + localname = self.iface.localname logging.info("restoring rj45 state: %s", localname) for addr in self.old_addrs: self.net_client.create_address(localname, addr[0], addr[1]) @@ -446,7 +425,7 @@ class Rj45Node(CoreNodeBase): :return: True if position changed, False otherwise """ super().setposition(x, y, z) - self.interface.setposition() + self.iface.setposition() def termcmdstring(self, sh: str) -> str: raise CoreError("rj45 does not support terminal commands") diff --git a/daemon/core/services/bird.py b/daemon/core/services/bird.py index 4901ea56..16f0bb84 100644 --- a/daemon/core/services/bird.py +++ b/daemon/core/services/bird.py @@ -35,10 +35,8 @@ class Bird(CoreService): """ Helper to return the first IPv4 address of a node as its router ID. """ - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - for a in ifc.addrlist: + for iface in node.get_ifaces(control=False): + for a in iface.addrlist: a = a.split("/")[0] if netaddr.valid_ipv4(a): return a @@ -84,7 +82,7 @@ protocol device { for s in node.services: if cls.name not in s.dependencies: continue - cfg += s.generatebirdconfig(node) + cfg += s.generate_bird_config(node) return cfg @@ -106,11 +104,11 @@ class BirdService(CoreService): meta = "The config file for this service can be found in the bird service." @classmethod - def generatebirdconfig(cls, node): + def generate_bird_config(cls, node): return "" @classmethod - def generatebirdifcconfig(cls, node): + def generate_bird_iface_config(cls, node): """ Use only bare interfaces descriptions in generated protocol configurations. This has the slight advantage of being the same @@ -118,10 +116,8 @@ class BirdService(CoreService): """ cfg = "" - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += ' interface "%s";\n' % ifc.name + for iface in node.get_ifaces(control=False): + cfg += ' interface "%s";\n' % iface.name return cfg @@ -135,7 +131,7 @@ class BirdBgp(BirdService): custom_needed = True @classmethod - def generatebirdconfig(cls, node): + def generate_bird_config(cls, node): return """ /* This is a sample config that should be customized with appropriate AS numbers * and peers; add one section like this for each neighbor */ @@ -165,7 +161,7 @@ class BirdOspf(BirdService): name = "BIRD_OSPFv2" @classmethod - def generatebirdconfig(cls, node): + def generate_bird_config(cls, node): cfg = "protocol ospf {\n" cfg += " export filter {\n" cfg += " if source = RTS_BGP then {\n" @@ -175,7 +171,7 @@ class BirdOspf(BirdService): cfg += " accept;\n" cfg += " };\n" cfg += " area 0.0.0.0 {\n" - cfg += cls.generatebirdifcconfig(node) + cfg += cls.generate_bird_iface_config(node) cfg += " };\n" cfg += "}\n\n" @@ -190,12 +186,12 @@ class BirdRadv(BirdService): name = "BIRD_RADV" @classmethod - def generatebirdconfig(cls, node): + def generate_bird_config(cls, node): cfg = "/* This is a sample config that must be customized */\n" cfg += "protocol radv {\n" cfg += " # auto configuration on all interfaces\n" - cfg += cls.generatebirdifcconfig(node) + cfg += cls.generate_bird_iface_config(node) cfg += " # Advertise DNS\n" cfg += " rdnss {\n" cfg += "# lifetime mult 10;\n" @@ -218,11 +214,11 @@ class BirdRip(BirdService): name = "BIRD_RIP" @classmethod - def generatebirdconfig(cls, node): + def generate_bird_config(cls, node): cfg = "protocol rip {\n" cfg += " period 10;\n" cfg += " garbage time 60;\n" - cfg += cls.generatebirdifcconfig(node) + cfg += cls.generate_bird_iface_config(node) cfg += " honor neighbor;\n" cfg += " authentication none;\n" cfg += " import all;\n" @@ -241,7 +237,7 @@ class BirdStatic(BirdService): custom_needed = True @classmethod - def generatebirdconfig(cls, node): + def generate_bird_config(cls, node): cfg = "/* This is a sample config that must be customized */\n" cfg += "protocol static {\n" cfg += "# route 0.0.0.0/0 via 198.51.100.130; # Default route. Do NOT advertise on BGP !\n" diff --git a/daemon/core/services/emaneservices.py b/daemon/core/services/emaneservices.py index 9d09516e..da438bab 100644 --- a/daemon/core/services/emaneservices.py +++ b/daemon/core/services/emaneservices.py @@ -20,14 +20,14 @@ class EmaneTransportService(CoreService): def generate_config(cls, node, filename): if filename == cls.configs[0]: transport_commands = [] - for interface in node.netifs(sort=True): + for iface in node.get_ifaces(): try: - network_node = node.session.get_node(interface.net.id, EmaneNet) + network_node = node.session.get_node(iface.net.id, EmaneNet) config = node.session.emane.get_configs( network_node.id, network_node.model.name ) if config and emanexml.is_external(config): - nem_id = network_node.getnemid(interface) + nem_id = network_node.getnemid(iface) command = ( "emanetransportd -r -l 0 -d ../transportdaemon%s.xml" % nem_id diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py index 9a344339..97a8b334 100644 --- a/daemon/core/services/frr.py +++ b/daemon/core/services/frr.py @@ -59,12 +59,12 @@ class FRRZebra(CoreService): """ # we could verify here that filename == frr.conf cfg = "" - for ifc in node.netifs(): - cfg += "interface %s\n" % ifc.name + for iface in node.get_ifaces(): + cfg += "interface %s\n" % iface.name # include control interfaces in addressing but not routing daemons - if hasattr(ifc, "control") and ifc.control is True: + if hasattr(iface, "control") and iface.control is True: cfg += " " - cfg += "\n ".join(map(cls.addrstr, ifc.addrlist)) + cfg += "\n ".join(map(cls.addrstr, iface.addrlist)) cfg += "\n" continue cfgv4 = "" @@ -74,18 +74,18 @@ class FRRZebra(CoreService): for s in node.services: if cls.name not in s.dependencies: continue - ifccfg = s.generatefrrifcconfig(node, ifc) + iface_config = s.generate_frr_iface_config(node, iface) if s.ipv4_routing: want_ipv4 = True if s.ipv6_routing: want_ipv6 = True - cfgv6 += ifccfg + cfgv6 += iface_config else: - cfgv4 += ifccfg + cfgv4 += iface_config if want_ipv4: ipv4list = filter( - lambda x: netaddr.valid_ipv4(x.split("/")[0]), ifc.addrlist + lambda x: netaddr.valid_ipv4(x.split("/")[0]), iface.addrlist ) cfg += " " cfg += "\n ".join(map(cls.addrstr, ipv4list)) @@ -93,7 +93,7 @@ class FRRZebra(CoreService): cfg += cfgv4 if want_ipv6: ipv6list = filter( - lambda x: netaddr.valid_ipv6(x.split("/")[0]), ifc.addrlist + lambda x: netaddr.valid_ipv6(x.split("/")[0]), iface.addrlist ) cfg += " " cfg += "\n ".join(map(cls.addrstr, ipv6list)) @@ -104,7 +104,7 @@ class FRRZebra(CoreService): for s in node.services: if cls.name not in s.dependencies: continue - cfg += s.generatefrrconfig(node) + cfg += s.generate_frr_config(node) return cfg @staticmethod @@ -237,10 +237,10 @@ bootfrr frr_bin_search, constants.FRR_STATE_DIR, ) - for ifc in node.netifs(): - cfg += f"ip link set dev {ifc.name} down\n" + for iface in node.get_ifaces(): + cfg += f"ip link set dev {iface.name} down\n" cfg += "sleep 1\n" - cfg += f"ip link set dev {ifc.name} up\n" + cfg += f"ip link set dev {iface.name} up\n" return cfg @classmethod @@ -334,10 +334,8 @@ class FrrService(CoreService): """ Helper to return the first IPv4 address of a node as its router ID. """ - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - for a in ifc.addrlist: + for iface in node.get_ifaces(control=False): + for a in iface.addrlist: a = a.split("/")[0] if netaddr.valid_ipv4(a): return a @@ -345,16 +343,16 @@ class FrrService(CoreService): return "0.0.0.0" @staticmethod - def rj45check(ifc): + def rj45check(iface): """ Helper to detect whether interface is connected an external RJ45 link. """ - if ifc.net: - for peerifc in ifc.net.netifs(): - if peerifc == ifc: + if iface.net: + for peer_iface in iface.net.get_ifaces(): + if peer_iface == iface: continue - if isinstance(peerifc.node, Rj45Node): + if isinstance(peer_iface.node, Rj45Node): return True return False @@ -363,11 +361,11 @@ class FrrService(CoreService): return "" @classmethod - def generatefrrifcconfig(cls, node, ifc): + def generate_frr_iface_config(cls, node, iface): return "" @classmethod - def generatefrrconfig(cls, node): + def generate_frr_config(cls, node): return "" @@ -385,43 +383,41 @@ class FRROspfv2(FrrService): ipv4_routing = True @staticmethod - def mtucheck(ifc): + def mtucheck(iface): """ Helper to detect MTU mismatch and add the appropriate OSPF mtu-ignore command. This is needed when e.g. a node is linked via a GreTap device. """ - if ifc.mtu != 1500: + if iface.mtu != 1500: # a workaround for PhysicalNode GreTap, which has no knowledge of # the other nodes/nets return " ip ospf mtu-ignore\n" - if not ifc.net: + if not iface.net: return "" - for i in ifc.net.netifs(): - if i.mtu != ifc.mtu: + for iface in iface.net.get_ifaces(): + if iface.mtu != iface.mtu: return " ip ospf mtu-ignore\n" return "" @staticmethod - def ptpcheck(ifc): + def ptpcheck(iface): """ Helper to detect whether interface is connected to a notional point-to-point link. """ - if isinstance(ifc.net, PtpNet): + if isinstance(iface.net, PtpNet): return " ip ospf network point-to-point\n" return "" @classmethod - def generatefrrconfig(cls, node): + def generate_frr_config(cls, node): cfg = "router ospf\n" rtrid = cls.routerid(node) cfg += " router-id %s\n" % rtrid # network 10.0.0.0/24 area 0 - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - for a in ifc.addrlist: + for iface in node.get_ifaces(control=False): + for a in iface.addrlist: addr = a.split("/")[0] if not netaddr.valid_ipv4(addr): continue @@ -430,8 +426,8 @@ class FRROspfv2(FrrService): return cfg @classmethod - def generatefrrifcconfig(cls, node, ifc): - return cls.mtucheck(ifc) + def generate_frr_iface_config(cls, node, iface): + return cls.mtucheck(iface) class FRROspfv3(FrrService): @@ -449,57 +445,55 @@ class FRROspfv3(FrrService): ipv6_routing = True @staticmethod - def minmtu(ifc): + def minmtu(iface): """ Helper to discover the minimum MTU of interfaces linked with the given interface. """ - mtu = ifc.mtu - if not ifc.net: + mtu = iface.mtu + if not iface.net: return mtu - for i in ifc.net.netifs(): - if i.mtu < mtu: - mtu = i.mtu + for iface in iface.net.get_ifaces(): + if iface.mtu < mtu: + mtu = iface.mtu return mtu @classmethod - def mtucheck(cls, ifc): + def mtucheck(cls, iface): """ Helper to detect MTU mismatch and add the appropriate OSPFv3 ifmtu command. This is needed when e.g. a node is linked via a GreTap device. """ - minmtu = cls.minmtu(ifc) - if minmtu < ifc.mtu: + minmtu = cls.minmtu(iface) + if minmtu < iface.mtu: return " ipv6 ospf6 ifmtu %d\n" % minmtu else: return "" @staticmethod - def ptpcheck(ifc): + def ptpcheck(iface): """ Helper to detect whether interface is connected to a notional point-to-point link. """ - if isinstance(ifc.net, PtpNet): + if isinstance(iface.net, PtpNet): return " ipv6 ospf6 network point-to-point\n" return "" @classmethod - def generatefrrconfig(cls, node): + def generate_frr_config(cls, node): cfg = "router ospf6\n" rtrid = cls.routerid(node) cfg += " router-id %s\n" % rtrid - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += " interface %s area 0.0.0.0\n" % ifc.name + for iface in node.get_ifaces(control=False): + cfg += " interface %s area 0.0.0.0\n" % iface.name cfg += "!\n" return cfg @classmethod - def generatefrrifcconfig(cls, node, ifc): - return cls.mtucheck(ifc) + def generate_frr_iface_config(cls, node, iface): + return cls.mtucheck(iface) # cfg = cls.mtucheck(ifc) # external RJ45 connections will use default OSPF timers # if cls.rj45check(ifc): @@ -531,7 +525,7 @@ class FRRBgp(FrrService): ipv6_routing = True @classmethod - def generatefrrconfig(cls, node): + def generate_frr_config(cls, node): cfg = "!\n! BGP configuration\n!\n" cfg += "! You should configure the AS number below,\n" cfg += "! along with this router's peers.\n!\n" @@ -555,7 +549,7 @@ class FRRRip(FrrService): ipv4_routing = True @classmethod - def generatefrrconfig(cls, node): + def generate_frr_config(cls, node): cfg = """\ router rip redistribute static @@ -579,7 +573,7 @@ class FRRRipng(FrrService): ipv6_routing = True @classmethod - def generatefrrconfig(cls, node): + def generate_frr_config(cls, node): cfg = """\ router ripng redistribute static @@ -604,18 +598,16 @@ class FRRBabel(FrrService): ipv6_routing = True @classmethod - def generatefrrconfig(cls, node): + def generate_frr_config(cls, node): cfg = "router babel\n" - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += " network %s\n" % ifc.name + for iface in node.get_ifaces(control=False): + cfg += " network %s\n" % iface.name cfg += " redistribute static\n redistribute ipv4 connected\n" return cfg @classmethod - def generatefrrifcconfig(cls, node, ifc): - if ifc.net and isinstance(ifc.net, (EmaneNet, WlanNode)): + def generate_frr_iface_config(cls, node, iface): + if iface.net and isinstance(iface.net, (EmaneNet, WlanNode)): return " babel wireless\n no babel split-horizon\n" else: return " babel wired\n babel split-horizon\n" @@ -633,11 +625,11 @@ class FRRpimd(FrrService): ipv4_routing = True @classmethod - def generatefrrconfig(cls, node): + def generate_frr_config(cls, node): ifname = "eth0" - for ifc in node.netifs(): - if ifc.name != "lo": - ifname = ifc.name + for iface in node.get_ifaces(): + if iface.name != "lo": + ifname = iface.name break cfg = "router mfea\n!\n" cfg += "router igmp\n!\n" @@ -649,7 +641,7 @@ class FRRpimd(FrrService): return cfg @classmethod - def generatefrrifcconfig(cls, node, ifc): + def generate_frr_iface_config(cls, node, iface): return " ip mfea\n ip igmp\n ip pim\n" @@ -668,17 +660,17 @@ class FRRIsis(FrrService): ipv6_routing = True @staticmethod - def ptpcheck(ifc): + def ptpcheck(iface): """ Helper to detect whether interface is connected to a notional point-to-point link. """ - if isinstance(ifc.net, PtpNet): + if isinstance(iface.net, PtpNet): return " isis network point-to-point\n" return "" @classmethod - def generatefrrconfig(cls, node): + def generate_frr_config(cls, node): cfg = "router isis DEFAULT\n" cfg += " net 47.0001.0000.1900.%04x.00\n" % node.id cfg += " metric-style wide\n" @@ -687,9 +679,9 @@ class FRRIsis(FrrService): return cfg @classmethod - def generatefrrifcconfig(cls, node, ifc): + def generate_frr_iface_config(cls, node, iface): cfg = " ip router isis DEFAULT\n" cfg += " ipv6 router isis DEFAULT\n" cfg += " isis circuit-type level-2-only\n" - cfg += cls.ptpcheck(ifc) + cfg += cls.ptpcheck(iface) return cfg diff --git a/daemon/core/services/nrl.py b/daemon/core/services/nrl.py index 3c9f262d..38b90d48 100644 --- a/daemon/core/services/nrl.py +++ b/daemon/core/services/nrl.py @@ -32,10 +32,8 @@ class NrlService(CoreService): prefix of a node, using the supplied prefix length. This ignores the interface's prefix length, so e.g. '/32' can turn into '/24'. """ - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - for a in ifc.addrlist: + for iface in node.get_ifaces(control=False): + for a in iface.addrlist: a = a.split("/")[0] if netaddr.valid_ipv4(a): return f"{a}/{prefixlen}" @@ -54,8 +52,8 @@ class MgenSinkService(NrlService): @classmethod def generate_config(cls, node, filename): cfg = "0.0 LISTEN UDP 5000\n" - for ifc in node.netifs(): - name = utils.sysctl_devname(ifc.name) + for iface in node.get_ifaces(): + name = utils.sysctl_devname(iface.name) cfg += "0.0 Join 224.225.1.2 INTERFACE %s\n" % name return cfg @@ -91,11 +89,11 @@ class NrlNhdp(NrlService): cmd += " -flooding ecds" cmd += " -smfClient %s_smf" % node.name - netifs = list(filter(lambda x: not getattr(x, "control", False), node.netifs())) - if len(netifs) > 0: - interfacenames = map(lambda x: x.name, netifs) + ifaces = node.get_ifaces(control=False) + if len(ifaces) > 0: + iface_names = map(lambda x: x.name, ifaces) cmd += " -i " - cmd += " -i ".join(interfacenames) + cmd += " -i ".join(iface_names) return (cmd,) @@ -125,16 +123,16 @@ class NrlSmf(NrlService): cmd = "nrlsmf instance %s_smf" % node.name servicenames = map(lambda x: x.name, node.services) - netifs = list(filter(lambda x: not getattr(x, "control", False), node.netifs())) - if len(netifs) == 0: + ifaces = node.get_ifaces(control=False) + if len(ifaces) == 0: return "" if "arouted" in servicenames: comments += "# arouted service is enabled\n" cmd += " tap %s_tap" % (node.name,) cmd += " unicast %s" % cls.firstipv4prefix(node, 24) - cmd += " push lo,%s resequence on" % netifs[0].name - if len(netifs) > 0: + cmd += " push lo,%s resequence on" % ifaces[0].name + if len(ifaces) > 0: if "NHDP" in servicenames: comments += "# NHDP service is enabled\n" cmd += " ecds " @@ -143,8 +141,8 @@ class NrlSmf(NrlService): cmd += " smpr " else: cmd += " cf " - interfacenames = map(lambda x: x.name, netifs) - cmd += ",".join(interfacenames) + iface_names = map(lambda x: x.name, ifaces) + cmd += ",".join(iface_names) cmd += " hash MD5" cmd += " log /var/log/nrlsmf.log" @@ -171,10 +169,10 @@ class NrlOlsr(NrlService): """ cmd = cls.startup[0] # are multiple interfaces supported? No. - netifs = list(node.netifs()) - if len(netifs) > 0: - ifc = netifs[0] - cmd += " -i %s" % ifc.name + ifaces = node.get_ifaces() + if len(ifaces) > 0: + iface = ifaces[0] + cmd += " -i %s" % iface.name cmd += " -l /var/log/nrlolsrd.log" cmd += " -rpipe %s_olsr" % node.name @@ -215,11 +213,11 @@ class NrlOlsrv2(NrlService): cmd += " -p olsr" - netifs = list(filter(lambda x: not getattr(x, "control", False), node.netifs())) - if len(netifs) > 0: - interfacenames = map(lambda x: x.name, netifs) + ifaces = node.get_ifaces(control=False) + if len(ifaces) > 0: + iface_names = map(lambda x: x.name, ifaces) cmd += " -i " - cmd += " -i ".join(interfacenames) + cmd += " -i ".join(iface_names) return (cmd,) @@ -243,11 +241,11 @@ class OlsrOrg(NrlService): Generate the appropriate command-line based on node interfaces. """ cmd = cls.startup[0] - netifs = list(filter(lambda x: not getattr(x, "control", False), node.netifs())) - if len(netifs) > 0: - interfacenames = map(lambda x: x.name, netifs) + ifaces = node.get_ifaces(control=False) + if len(ifaces) > 0: + iface_names = map(lambda x: x.name, ifaces) cmd += " -i " - cmd += " -i ".join(interfacenames) + cmd += " -i ".join(iface_names) return (cmd,) @@ -607,8 +605,8 @@ class MgenActor(NrlService): comments = "" cmd = "mgenBasicActor.py -n %s -a 0.0.0.0" % node.name - netifs = [x for x in node.netifs() if not getattr(x, "control", False)] - if len(netifs) == 0: + ifaces = node.get_ifaces(control=False) + if len(ifaces) == 0: return "" cfg += comments + cmd + " < /dev/null > /dev/null 2>&1 &\n\n" diff --git a/daemon/core/services/quagga.py b/daemon/core/services/quagga.py index a62cbc5c..41cfa3d8 100644 --- a/daemon/core/services/quagga.py +++ b/daemon/core/services/quagga.py @@ -56,12 +56,12 @@ class Zebra(CoreService): """ # we could verify here that filename == Quagga.conf cfg = "" - for ifc in node.netifs(): - cfg += "interface %s\n" % ifc.name + for iface in node.get_ifaces(): + cfg += "interface %s\n" % iface.name # include control interfaces in addressing but not routing daemons - if hasattr(ifc, "control") and ifc.control is True: + if hasattr(iface, "control") and iface.control is True: cfg += " " - cfg += "\n ".join(map(cls.addrstr, ifc.addrlist)) + cfg += "\n ".join(map(cls.addrstr, iface.addrlist)) cfg += "\n" continue cfgv4 = "" @@ -71,18 +71,18 @@ class Zebra(CoreService): for s in node.services: if cls.name not in s.dependencies: continue - ifccfg = s.generatequaggaifcconfig(node, ifc) + iface_config = s.generate_quagga_iface_config(node, iface) if s.ipv4_routing: want_ipv4 = True if s.ipv6_routing: want_ipv6 = True - cfgv6 += ifccfg + cfgv6 += iface_config else: - cfgv4 += ifccfg + cfgv4 += iface_config if want_ipv4: ipv4list = filter( - lambda x: netaddr.valid_ipv4(x.split("/")[0]), ifc.addrlist + lambda x: netaddr.valid_ipv4(x.split("/")[0]), iface.addrlist ) cfg += " " cfg += "\n ".join(map(cls.addrstr, ipv4list)) @@ -90,7 +90,7 @@ class Zebra(CoreService): cfg += cfgv4 if want_ipv6: ipv6list = filter( - lambda x: netaddr.valid_ipv6(x.split("/")[0]), ifc.addrlist + lambda x: netaddr.valid_ipv6(x.split("/")[0]), iface.addrlist ) cfg += " " cfg += "\n ".join(map(cls.addrstr, ipv6list)) @@ -101,7 +101,7 @@ class Zebra(CoreService): for s in node.services: if cls.name not in s.dependencies: continue - cfg += s.generatequaggaconfig(node) + cfg += s.generate_quagga_config(node) return cfg @staticmethod @@ -252,10 +252,8 @@ class QuaggaService(CoreService): """ Helper to return the first IPv4 address of a node as its router ID. """ - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - for a in ifc.addrlist: + for iface in node.get_ifaces(control=False): + for a in iface.addrlist: a = a.split("/")[0] if netaddr.valid_ipv4(a): return a @@ -263,16 +261,16 @@ class QuaggaService(CoreService): return "0.0.0.%d" % node.id @staticmethod - def rj45check(ifc): + def rj45check(iface): """ Helper to detect whether interface is connected an external RJ45 link. """ - if ifc.net: - for peerifc in ifc.net.netifs(): - if peerifc == ifc: + if iface.net: + for peer_iface in iface.net.get_ifaces(): + if peer_iface == iface: continue - if isinstance(peerifc.node, Rj45Node): + if isinstance(peer_iface.node, Rj45Node): return True return False @@ -281,11 +279,11 @@ class QuaggaService(CoreService): return "" @classmethod - def generatequaggaifcconfig(cls, node, ifc): + def generate_quagga_iface_config(cls, node, iface): return "" @classmethod - def generatequaggaconfig(cls, node): + def generate_quagga_config(cls, node): return "" @@ -303,43 +301,41 @@ class Ospfv2(QuaggaService): ipv4_routing = True @staticmethod - def mtucheck(ifc): + def mtucheck(iface): """ Helper to detect MTU mismatch and add the appropriate OSPF mtu-ignore command. This is needed when e.g. a node is linked via a GreTap device. """ - if ifc.mtu != 1500: + if iface.mtu != 1500: # a workaround for PhysicalNode GreTap, which has no knowledge of # the other nodes/nets return " ip ospf mtu-ignore\n" - if not ifc.net: + if not iface.net: return "" - for i in ifc.net.netifs(): - if i.mtu != ifc.mtu: + for iface in iface.net.get_ifaces(): + if iface.mtu != iface.mtu: return " ip ospf mtu-ignore\n" return "" @staticmethod - def ptpcheck(ifc): + def ptpcheck(iface): """ Helper to detect whether interface is connected to a notional point-to-point link. """ - if isinstance(ifc.net, PtpNet): + if isinstance(iface.net, PtpNet): return " ip ospf network point-to-point\n" return "" @classmethod - def generatequaggaconfig(cls, node): + def generate_quagga_config(cls, node): cfg = "router ospf\n" rtrid = cls.routerid(node) cfg += " router-id %s\n" % rtrid # network 10.0.0.0/24 area 0 - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - for a in ifc.addrlist: + for iface in node.get_ifaces(control=False): + for a in iface.addrlist: addr = a.split("/")[0] if netaddr.valid_ipv4(addr): cfg += " network %s area 0\n" % a @@ -347,12 +343,12 @@ class Ospfv2(QuaggaService): return cfg @classmethod - def generatequaggaifcconfig(cls, node, ifc): - cfg = cls.mtucheck(ifc) + def generate_quagga_iface_config(cls, node, iface): + cfg = cls.mtucheck(iface) # external RJ45 connections will use default OSPF timers - if cls.rj45check(ifc): + if cls.rj45check(iface): return cfg - cfg += cls.ptpcheck(ifc) + cfg += cls.ptpcheck(iface) return ( cfg + """\ @@ -378,58 +374,56 @@ class Ospfv3(QuaggaService): ipv6_routing = True @staticmethod - def minmtu(ifc): + def minmtu(iface): """ Helper to discover the minimum MTU of interfaces linked with the given interface. """ - mtu = ifc.mtu - if not ifc.net: + mtu = iface.mtu + if not iface.net: return mtu - for i in ifc.net.netifs(): - if i.mtu < mtu: - mtu = i.mtu + for iface in iface.net.get_ifaces(): + if iface.mtu < mtu: + mtu = iface.mtu return mtu @classmethod - def mtucheck(cls, ifc): + def mtucheck(cls, iface): """ Helper to detect MTU mismatch and add the appropriate OSPFv3 ifmtu command. This is needed when e.g. a node is linked via a GreTap device. """ - minmtu = cls.minmtu(ifc) - if minmtu < ifc.mtu: + minmtu = cls.minmtu(iface) + if minmtu < iface.mtu: return " ipv6 ospf6 ifmtu %d\n" % minmtu else: return "" @staticmethod - def ptpcheck(ifc): + def ptpcheck(iface): """ Helper to detect whether interface is connected to a notional point-to-point link. """ - if isinstance(ifc.net, PtpNet): + if isinstance(iface.net, PtpNet): return " ipv6 ospf6 network point-to-point\n" return "" @classmethod - def generatequaggaconfig(cls, node): + def generate_quagga_config(cls, node): cfg = "router ospf6\n" rtrid = cls.routerid(node) cfg += " instance-id 65\n" cfg += " router-id %s\n" % rtrid - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += " interface %s area 0.0.0.0\n" % ifc.name + for iface in node.get_ifaces(control=False): + cfg += " interface %s area 0.0.0.0\n" % iface.name cfg += "!\n" return cfg @classmethod - def generatequaggaifcconfig(cls, node, ifc): - return cls.mtucheck(ifc) + def generate_quagga_iface_config(cls, node, iface): + return cls.mtucheck(iface) class Ospfv3mdr(Ospfv3): @@ -444,9 +438,9 @@ class Ospfv3mdr(Ospfv3): ipv4_routing = True @classmethod - def generatequaggaifcconfig(cls, node, ifc): - cfg = cls.mtucheck(ifc) - if ifc.net is not None and isinstance(ifc.net, (WlanNode, EmaneNet)): + def generate_quagga_iface_config(cls, node, iface): + cfg = cls.mtucheck(iface) + if iface.net is not None and isinstance(iface.net, (WlanNode, EmaneNet)): return ( cfg + """\ @@ -479,7 +473,7 @@ class Bgp(QuaggaService): ipv6_routing = True @classmethod - def generatequaggaconfig(cls, node): + def generate_quagga_config(cls, node): cfg = "!\n! BGP configuration\n!\n" cfg += "! You should configure the AS number below,\n" cfg += "! along with this router's peers.\n!\n" @@ -503,7 +497,7 @@ class Rip(QuaggaService): ipv4_routing = True @classmethod - def generatequaggaconfig(cls, node): + def generate_quagga_config(cls, node): cfg = """\ router rip redistribute static @@ -527,7 +521,7 @@ class Ripng(QuaggaService): ipv6_routing = True @classmethod - def generatequaggaconfig(cls, node): + def generate_quagga_config(cls, node): cfg = """\ router ripng redistribute static @@ -552,18 +546,16 @@ class Babel(QuaggaService): ipv6_routing = True @classmethod - def generatequaggaconfig(cls, node): + def generate_quagga_config(cls, node): cfg = "router babel\n" - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += " network %s\n" % ifc.name + for iface in node.get_ifaces(control=False): + cfg += " network %s\n" % iface.name cfg += " redistribute static\n redistribute connected\n" return cfg @classmethod - def generatequaggaifcconfig(cls, node, ifc): - if ifc.net and ifc.net.linktype == LinkTypes.WIRELESS: + def generate_quagga_iface_config(cls, node, iface): + if iface.net and iface.net.linktype == LinkTypes.WIRELESS: return " babel wireless\n no babel split-horizon\n" else: return " babel wired\n babel split-horizon\n" @@ -581,11 +573,11 @@ class Xpimd(QuaggaService): ipv4_routing = True @classmethod - def generatequaggaconfig(cls, node): + def generate_quagga_config(cls, node): ifname = "eth0" - for ifc in node.netifs(): - if ifc.name != "lo": - ifname = ifc.name + for iface in node.get_ifaces(): + if iface.name != "lo": + ifname = iface.name break cfg = "router mfea\n!\n" cfg += "router igmp\n!\n" @@ -597,5 +589,5 @@ class Xpimd(QuaggaService): return cfg @classmethod - def generatequaggaifcconfig(cls, node, ifc): + def generate_quagga_iface_config(cls, node, iface): return " ip mfea\n ip igmp\n ip pim\n" diff --git a/daemon/core/services/sdn.py b/daemon/core/services/sdn.py index ab46f551..71ab815f 100644 --- a/daemon/core/services/sdn.py +++ b/daemon/core/services/sdn.py @@ -49,10 +49,8 @@ class OvsService(SdnService): cfg += "\n## Now add all our interfaces as ports to the switch\n" portnum = 1 - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - ifnumstr = re.findall(r"\d+", ifc.name) + for iface in node.get_ifaces(control=False): + ifnumstr = re.findall(r"\d+", iface.name) ifnum = ifnumstr[0] # create virtual interfaces @@ -61,18 +59,18 @@ class OvsService(SdnService): # remove ip address of eths because quagga/zebra will assign same IPs to rtr interfaces # or assign them manually to rtr interfaces if zebra is not running - for ifcaddr in ifc.addrlist: - addr = ifcaddr.split("/")[0] + for addr in iface.addrlist: + addr = addr.split("/")[0] if netaddr.valid_ipv4(addr): - cfg += "ip addr del %s dev %s\n" % (ifcaddr, ifc.name) + cfg += "ip addr del %s dev %s\n" % (addr, iface.name) if has_zebra == 0: - cfg += "ip addr add %s dev rtr%s\n" % (ifcaddr, ifnum) + cfg += "ip addr add %s dev rtr%s\n" % (addr, ifnum) elif netaddr.valid_ipv6(addr): - cfg += "ip -6 addr del %s dev %s\n" % (ifcaddr, ifc.name) + cfg += "ip -6 addr del %s dev %s\n" % (addr, iface.name) if has_zebra == 0: - cfg += "ip -6 addr add %s dev rtr%s\n" % (ifcaddr, ifnum) + cfg += "ip -6 addr add %s dev rtr%s\n" % (addr, ifnum) else: - raise ValueError("invalid address: %s" % ifcaddr) + raise ValueError("invalid address: %s" % addr) # add interfaces to bridge # Make port numbers explicit so they're easier to follow in reading the script @@ -102,9 +100,7 @@ class OvsService(SdnService): cfg += "## if the above controller will be present then you probably want to delete them\n" # Setup default flows portnum = 1 - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue + for iface in node.get_ifaces(control=False): cfg += "## Take the data from the CORE interface and put it on the veth and vice versa\n" cfg += ( "ovs-ofctl add-flow ovsbr0 priority=1000,in_port=%d,action=output:%d\n" diff --git a/daemon/core/services/security.py b/daemon/core/services/security.py index eb6545b2..91c942f1 100644 --- a/daemon/core/services/security.py +++ b/daemon/core/services/security.py @@ -131,18 +131,18 @@ class Nat(CoreService): custom_needed = False @classmethod - def generateifcnatrule(cls, ifc, line_prefix=""): + def generate_iface_nat_rule(cls, iface, line_prefix=""): """ Generate a NAT line for one interface. """ cfg = line_prefix + "iptables -t nat -A POSTROUTING -o " - cfg += ifc.name + " -j MASQUERADE\n" + cfg += iface.name + " -j MASQUERADE\n" - cfg += line_prefix + "iptables -A FORWARD -i " + ifc.name + cfg += line_prefix + "iptables -A FORWARD -i " + iface.name cfg += " -m state --state RELATED,ESTABLISHED -j ACCEPT\n" cfg += line_prefix + "iptables -A FORWARD -i " - cfg += ifc.name + " -j DROP\n" + cfg += iface.name + " -j DROP\n" return cfg @classmethod @@ -154,14 +154,12 @@ class Nat(CoreService): cfg += "# generated by security.py\n" cfg += "# NAT out the first interface by default\n" have_nat = False - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue + for iface in node.get_ifaces(control=False): if have_nat: - cfg += cls.generateifcnatrule(ifc, line_prefix="#") + cfg += cls.generate_iface_nat_rule(iface, line_prefix="#") else: have_nat = True - cfg += "# NAT out the " + ifc.name + " interface\n" - cfg += cls.generateifcnatrule(ifc) + cfg += "# NAT out the " + iface.name + " interface\n" + cfg += cls.generate_iface_nat_rule(iface) cfg += "\n" return cfg diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index 8a6e828b..273318e1 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -55,8 +55,8 @@ class IPForwardService(UtilService): """ % { "sysctl": constants.SYSCTL_BIN } - for ifc in node.netifs(): - name = utils.sysctl_devname(ifc.name) + for iface in node.get_ifaces(): + name = utils.sysctl_devname(iface.name) cfg += "%s -w net.ipv4.conf.%s.forwarding=1\n" % ( constants.SYSCTL_BIN, name, @@ -77,10 +77,10 @@ class DefaultRouteService(UtilService): @classmethod def generate_config(cls, node, filename): routes = [] - netifs = node.netifs(sort=True) - if netifs: - netif = netifs[0] - for x in netif.addrlist: + ifaces = node.get_ifaces() + if ifaces: + iface = ifaces[0] + for x in iface.addrlist: net = netaddr.IPNetwork(x).cidr if net.size > 1: router = net[1] @@ -104,14 +104,12 @@ class DefaultMulticastRouteService(UtilService): cfg += "# the first interface is chosen below; please change it " cfg += "as needed\n" - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue + for iface in node.get_ifaces(control=False): if os.uname()[0] == "Linux": rtcmd = "ip route add 224.0.0.0/4 dev" else: raise Exception("unknown platform") - cfg += "%s %s\n" % (rtcmd, ifc.name) + cfg += "%s %s\n" % (rtcmd, iface.name) cfg += "\n" break return cfg @@ -129,10 +127,8 @@ class StaticRouteService(UtilService): cfg += "# auto-generated by StaticRoute service (utility.py)\n#\n" cfg += "# NOTE: this service must be customized to be of any use\n" cfg += "# Below are samples that you can uncomment and edit.\n#\n" - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += "\n".join(map(cls.routestr, ifc.addrlist)) + for iface in node.get_ifaces(control=False): + cfg += "\n".join(map(cls.routestr, iface.addrlist)) cfg += "\n" return cfg @@ -259,10 +255,8 @@ max-lease-time 7200; ddns-update-style none; """ - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += "\n".join(map(cls.subnetentry, ifc.addrlist)) + for iface in node.get_ifaces(control=False): + cfg += "\n".join(map(cls.subnetentry, iface.addrlist)) cfg += "\n" return cfg @@ -320,13 +314,11 @@ class DhcpClientService(UtilService): cfg += "side DNS\n# resolution based on the DHCP server response.\n" cfg += "#mkdir -p /var/run/resolvconf/interface\n" - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += "#ln -s /var/run/resolvconf/interface/%s.dhclient" % ifc.name + for iface in node.get_ifaces(control=False): + cfg += "#ln -s /var/run/resolvconf/interface/%s.dhclient" % iface.name cfg += " /var/run/resolvconf/resolv.conf\n" - cfg += "/sbin/dhclient -nw -pf /var/run/dhclient-%s.pid" % ifc.name - cfg += " -lf /var/run/dhclient-%s.lease %s\n" % (ifc.name, ifc.name) + cfg += "/sbin/dhclient -nw -pf /var/run/dhclient-%s.pid" % iface.name + cfg += " -lf /var/run/dhclient-%s.lease %s\n" % (iface.name, iface.name) return cfg @@ -585,10 +577,8 @@ export LANG """ % node.name ) - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - body += "
  • %s - %s
  • \n" % (ifc.name, ifc.addrlist) + for iface in node.get_ifaces(control=False): + body += "
  • %s - %s
  • \n" % (iface.name, iface.addrlist) return "%s" % body @@ -619,14 +609,14 @@ DUMPOPTS="-s 12288 -C 10 -n" if [ "x$1" = "xstart" ]; then """ - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: + for iface in node.get_ifaces(): + if hasattr(iface, "control") and iface.control is True: cfg += "# " redir = "< /dev/null" cfg += "tcpdump ${DUMPOPTS} -w %s.%s.pcap -i %s %s &\n" % ( node.name, - ifc.name, - ifc.name, + iface.name, + iface.name, redir, ) cfg += """ @@ -654,10 +644,8 @@ class RadvdService(UtilService): using the network address of each interface. """ cfg = "# auto-generated by RADVD service (utility.py)\n" - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - prefixes = list(map(cls.subnetentry, ifc.addrlist)) + for iface in node.get_ifaces(control=False): + prefixes = list(map(cls.subnetentry, iface.addrlist)) if len(prefixes) < 1: continue cfg += ( @@ -670,7 +658,7 @@ interface %s AdvDefaultPreference low; AdvHomeAgentFlag off; """ - % ifc.name + % iface.name ) for prefix in prefixes: if prefix == "": diff --git a/daemon/core/services/xorp.py b/daemon/core/services/xorp.py index 2312e6d4..3dfef56a 100644 --- a/daemon/core/services/xorp.py +++ b/daemon/core/services/xorp.py @@ -35,11 +35,11 @@ class XorpRtrmgr(CoreService): invoked here. Filename currently ignored. """ cfg = "interfaces {\n" - for ifc in node.netifs(): - cfg += " interface %s {\n" % ifc.name - cfg += "\tvif %s {\n" % ifc.name - cfg += "".join(map(cls.addrstr, ifc.addrlist)) - cfg += cls.lladdrstr(ifc) + for iface in node.get_ifaces(): + cfg += " interface %s {\n" % iface.name + cfg += "\tvif %s {\n" % iface.name + cfg += "".join(map(cls.addrstr, iface.addrlist)) + cfg += cls.lladdrstr(iface) cfg += "\t}\n" cfg += " }\n" cfg += "}\n\n" @@ -65,11 +65,11 @@ class XorpRtrmgr(CoreService): return cfg @staticmethod - def lladdrstr(ifc): + def lladdrstr(iface): """ helper for adding link-local address entries (required by OSPFv3) """ - cfg = "\t address %s {\n" % ifc.hwaddr.tolinklocal() + cfg = "\t address %s {\n" % iface.hwaddr.tolinklocal() cfg += "\t\tprefix-length: 64\n" cfg += "\t }\n" return cfg @@ -104,15 +104,15 @@ class XorpService(CoreService): return cfg @staticmethod - def mfea(forwarding, ifcs): + def mfea(forwarding, ifaces): """ Helper to add a multicast forwarding engine entry to the config file. """ names = [] - for ifc in ifcs: - if hasattr(ifc, "control") and ifc.control is True: + for iface in ifaces: + if hasattr(iface, "control") and iface.control is True: continue - names.append(ifc.name) + names.append(iface.name) names.append("register_vif") cfg = "plumbing {\n" @@ -148,10 +148,8 @@ class XorpService(CoreService): """ Helper to return the first IPv4 address of a node as its router ID. """ - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - for a in ifc.addrlist: + for iface in node.get_ifaces(control=False): + for a in iface.addrlist: a = a.split("/")[0] if netaddr.valid_ipv4(a): return a @@ -184,12 +182,10 @@ class XorpOspfv2(XorpService): cfg += " ospf4 {\n" cfg += "\trouter-id: %s\n" % rtrid cfg += "\tarea 0.0.0.0 {\n" - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += "\t interface %s {\n" % ifc.name - cfg += "\t\tvif %s {\n" % ifc.name - for a in ifc.addrlist: + for iface in node.get_ifaces(control=False): + cfg += "\t interface %s {\n" % iface.name + cfg += "\t\tvif %s {\n" % iface.name + for a in iface.addrlist: addr = a.split("/")[0] if not netaddr.valid_ipv4(addr): continue @@ -220,11 +216,9 @@ class XorpOspfv3(XorpService): cfg += " ospf6 0 { /* Instance ID 0 */\n" cfg += "\trouter-id: %s\n" % rtrid cfg += "\tarea 0.0.0.0 {\n" - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += "\t interface %s {\n" % ifc.name - cfg += "\t\tvif %s {\n" % ifc.name + for iface in node.get_ifaces(control=False): + cfg += "\t interface %s {\n" % iface.name + cfg += "\t\tvif %s {\n" % iface.name cfg += "\t\t}\n" cfg += "\t }\n" cfg += "\t}\n" @@ -277,12 +271,10 @@ class XorpRip(XorpService): cfg += "\nprotocols {\n" cfg += " rip {\n" cfg += '\texport: "export-connected"\n' - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += "\tinterface %s {\n" % ifc.name - cfg += "\t vif %s {\n" % ifc.name - for a in ifc.addrlist: + for iface in node.get_ifaces(control=False): + cfg += "\tinterface %s {\n" % iface.name + cfg += "\t vif %s {\n" % iface.name + for a in iface.addrlist: addr = a.split("/")[0] if not netaddr.valid_ipv4(addr): continue @@ -310,12 +302,10 @@ class XorpRipng(XorpService): cfg += "\nprotocols {\n" cfg += " ripng {\n" cfg += '\texport: "export-connected"\n' - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += "\tinterface %s {\n" % ifc.name - cfg += "\t vif %s {\n" % ifc.name - cfg += "\t\taddress %s {\n" % ifc.hwaddr.tolinklocal() + for iface in node.get_ifaces(control=False): + cfg += "\tinterface %s {\n" % iface.name + cfg += "\t vif %s {\n" % iface.name + cfg += "\t\taddress %s {\n" % iface.hwaddr.tolinklocal() cfg += "\t\t disable: false\n" cfg += "\t\t}\n" cfg += "\t }\n" @@ -334,17 +324,15 @@ class XorpPimSm4(XorpService): @classmethod def generatexorpconfig(cls, node): - cfg = cls.mfea("mfea4", node.netifs()) + cfg = cls.mfea("mfea4", node.get_ifaces()) cfg += "\nprotocols {\n" cfg += " igmp {\n" names = [] - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - names.append(ifc.name) - cfg += "\tinterface %s {\n" % ifc.name - cfg += "\t vif %s {\n" % ifc.name + for iface in node.get_ifaces(control=False): + names.append(iface.name) + cfg += "\tinterface %s {\n" % iface.name + cfg += "\t vif %s {\n" % iface.name cfg += "\t\tdisable: false\n" cfg += "\t }\n" cfg += "\t}\n" @@ -394,17 +382,15 @@ class XorpPimSm6(XorpService): @classmethod def generatexorpconfig(cls, node): - cfg = cls.mfea("mfea6", node.netifs()) + cfg = cls.mfea("mfea6", node.get_ifaces()) cfg += "\nprotocols {\n" cfg += " mld {\n" names = [] - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - names.append(ifc.name) - cfg += "\tinterface %s {\n" % ifc.name - cfg += "\t vif %s {\n" % ifc.name + for iface in node.get_ifaces(control=False): + names.append(iface.name) + cfg += "\tinterface %s {\n" % iface.name + cfg += "\t vif %s {\n" % iface.name cfg += "\t\tdisable: false\n" cfg += "\t }\n" cfg += "\t}\n" @@ -459,12 +445,10 @@ class XorpOlsr(XorpService): cfg += "\nprotocols {\n" cfg += " olsr4 {\n" cfg += "\tmain-address: %s\n" % rtrid - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - cfg += "\tinterface %s {\n" % ifc.name - cfg += "\t vif %s {\n" % ifc.name - for a in ifc.addrlist: + for iface in node.get_ifaces(control=False): + cfg += "\tinterface %s {\n" % iface.name + cfg += "\t vif %s {\n" % iface.name + for a in iface.addrlist: addr = a.split("/")[0] if not netaddr.valid_ipv4(addr): continue diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 759de680..fe596d7a 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -58,16 +58,16 @@ def add_attribute(element: etree.Element, name: str, value: Any) -> None: element.set(name, str(value)) -def create_interface_data(interface_element: etree.Element) -> InterfaceData: - interface_id = int(interface_element.get("id")) - name = interface_element.get("name") - mac = interface_element.get("mac") - ip4 = interface_element.get("ip4") - ip4_mask = get_int(interface_element, "ip4_mask") - ip6 = interface_element.get("ip6") - ip6_mask = get_int(interface_element, "ip6_mask") +def create_iface_data(iface_element: etree.Element) -> InterfaceData: + iface_id = int(iface_element.get("id")) + name = iface_element.get("name") + mac = iface_element.get("mac") + ip4 = iface_element.get("ip4") + ip4_mask = get_int(iface_element, "ip4_mask") + ip6 = iface_element.get("ip6") + ip6_mask = get_int(iface_element, "ip6_mask") return InterfaceData( - id=interface_id, + id=iface_id, name=name, mac=mac, ip4=ip4, @@ -482,7 +482,7 @@ class CoreXmlWriter: # add link data for link_data in links: # skip basic range links - if link_data.interface1_id is None and link_data.interface2_id is None: + if link_data.iface1_id is None and link_data.iface2_id is None: continue link_element = self.create_link_element(link_data) @@ -495,37 +495,37 @@ class CoreXmlWriter: device = DeviceElement(self.session, node) self.devices.append(device.element) - def create_interface_element( + def create_iface_element( self, element_name: str, node_id: int, - interface_id: int, + iface_id: int, mac: str, ip4: str, ip4_mask: int, ip6: str, ip6_mask: int, ) -> etree.Element: - interface = etree.Element(element_name) + iface = etree.Element(element_name) node = self.session.get_node(node_id, NodeBase) - interface_name = None + iface_name = None if isinstance(node, CoreNodeBase): - node_interface = node.netif(interface_id) - interface_name = node_interface.name + node_iface = node.get_iface(iface_id) + iface_name = node_iface.name # check if emane interface - if isinstance(node_interface.net, EmaneNet): - nem = node_interface.net.getnemid(node_interface) - add_attribute(interface, "nem", nem) + if isinstance(node_iface.net, EmaneNet): + nem = node_iface.net.getnemid(node_iface) + add_attribute(iface, "nem", nem) - add_attribute(interface, "id", interface_id) - add_attribute(interface, "name", interface_name) - add_attribute(interface, "mac", mac) - add_attribute(interface, "ip4", ip4) - add_attribute(interface, "ip4_mask", ip4_mask) - add_attribute(interface, "ip6", ip6) - add_attribute(interface, "ip6_mask", ip6_mask) - return interface + add_attribute(iface, "id", iface_id) + add_attribute(iface, "name", iface_name) + add_attribute(iface, "mac", mac) + add_attribute(iface, "ip4", ip4) + add_attribute(iface, "ip4_mask", ip4_mask) + add_attribute(iface, "ip6", ip6) + add_attribute(iface, "ip6_mask", ip6_mask) + return iface def create_link_element(self, link_data: LinkData) -> etree.Element: link_element = etree.Element("link") @@ -533,32 +533,32 @@ class CoreXmlWriter: add_attribute(link_element, "node2", link_data.node2_id) # check for interface one - if link_data.interface1_id is not None: - interface1 = self.create_interface_element( + if link_data.iface1_id is not None: + iface1 = self.create_iface_element( "interface1", link_data.node1_id, - link_data.interface1_id, - link_data.interface1_mac, - link_data.interface1_ip4, - link_data.interface1_ip4_mask, - link_data.interface1_ip6, - link_data.interface1_ip6_mask, + link_data.iface1_id, + link_data.iface1_mac, + link_data.iface1_ip4, + link_data.iface1_ip4_mask, + link_data.iface1_ip6, + link_data.iface1_ip6_mask, ) - link_element.append(interface1) + link_element.append(iface1) # check for interface two - if link_data.interface2_id is not None: - interface2 = self.create_interface_element( + if link_data.iface2_id is not None: + iface2 = self.create_iface_element( "interface2", link_data.node2_id, - link_data.interface2_id, - link_data.interface2_mac, - link_data.interface2_ip4, - link_data.interface2_ip4_mask, - link_data.interface2_ip6, - link_data.interface2_ip6_mask, + link_data.iface2_id, + link_data.iface2_mac, + link_data.iface2_ip4, + link_data.iface2_ip4_mask, + link_data.iface2_ip6, + link_data.iface2_ip6_mask, ) - link_element.append(interface2) + link_element.append(iface2) # check for options, don't write for emane/wlan links node1 = self.session.get_node(link_data.node1_id, NodeBase) @@ -940,19 +940,19 @@ class CoreXmlReader: node2_id = get_int(link_element, "node_two") node_set = frozenset((node1_id, node2_id)) - interface1_element = link_element.find("interface1") - if interface1_element is None: - interface1_element = link_element.find("interface_one") - interface1_data = None - if interface1_element is not None: - interface1_data = create_interface_data(interface1_element) + iface1_element = link_element.find("interface1") + if iface1_element is None: + iface1_element = link_element.find("interface_one") + iface1_data = None + if iface1_element is not None: + iface1_data = create_iface_data(iface1_element) - interface2_element = link_element.find("interface2") - if interface2_element is None: - interface2_element = link_element.find("interface_two") - interface2_data = None - if interface2_element is not None: - interface2_data = create_interface_data(interface2_element) + iface2_element = link_element.find("interface2") + if iface2_element is None: + iface2_element = link_element.find("interface_two") + iface2_data = None + if iface2_element is not None: + iface2_data = create_iface_data(iface2_element) options_element = link_element.find("options") options = LinkOptions() @@ -978,12 +978,12 @@ class CoreXmlReader: if options.unidirectional == 1 and node_set in node_sets: logging.info("updating link node1(%s) node2(%s)", node1_id, node2_id) self.session.update_link( - node1_id, node2_id, interface1_data.id, interface2_data.id, options + node1_id, node2_id, iface1_data.id, iface2_data.id, options ) else: logging.info("adding link node1(%s) node2(%s)", node1_id, node2_id) self.session.add_link( - node1_id, node2_id, interface1_data, interface2_data, options + node1_id, node2_id, iface1_data, iface2_data, options ) node_sets.add(node_set) diff --git a/daemon/core/xml/corexmldeployment.py b/daemon/core/xml/corexmldeployment.py index 04915bf1..7954b71a 100644 --- a/daemon/core/xml/corexmldeployment.py +++ b/daemon/core/xml/corexmldeployment.py @@ -24,25 +24,25 @@ def add_address( parent_element: etree.Element, address_type: str, address: str, - interface_name: str = None, + iface_name: str = None, ) -> None: address_element = etree.SubElement(parent_element, "address", type=address_type) address_element.text = address - if interface_name is not None: - address_element.set("iface", interface_name) + if iface_name is not None: + address_element.set("iface", iface_name) def add_mapping(parent_element: etree.Element, maptype: str, mapref: str) -> None: etree.SubElement(parent_element, "mapping", type=maptype, ref=mapref) -def add_emane_interface( +def add_emane_iface( host_element: etree.Element, - netif: CoreInterface, + iface: CoreInterface, platform_name: str = "p1", transport_name: str = "t1", ) -> etree.Element: - nem_id = netif.net.nemidmap[netif] + nem_id = iface.net.nemidmap[iface] host_id = host_element.get("id") # platform data @@ -89,10 +89,10 @@ def get_ipv4_addresses(hostname: str) -> List[Tuple[str, str]]: split = line.split() if not split: continue - interface_name = split[1] + iface_name = split[1] address = split[3] if not address.startswith("127."): - addresses.append((interface_name, address)) + addresses.append((iface_name, address)) return addresses else: # TODO: handle other hosts @@ -112,11 +112,11 @@ class CoreXmlDeployment: device = self.scenario.find(f"devices/device[@name='{name}']") return device - def find_interface(self, device: NodeBase, name: str) -> etree.Element: - interface = self.scenario.find( + def find_iface(self, device: NodeBase, name: str) -> etree.Element: + iface = self.scenario.find( f"devices/device[@name='{device.name}']/interfaces/interface[@name='{name}']" ) - return interface + return iface def add_deployment(self) -> None: physical_host = self.add_physical_host(socket.gethostname()) @@ -136,8 +136,8 @@ class CoreXmlDeployment: add_type(host_element, "physical") # add ipv4 addresses - for interface_name, address in get_ipv4_addresses("localhost"): - add_address(host_element, "IPv4", address, interface_name) + for iface_name, address in get_ipv4_addresses("localhost"): + add_address(host_element, "IPv4", address, iface_name) return host_element @@ -155,15 +155,15 @@ class CoreXmlDeployment: # add host type add_type(host_element, "virtual") - for netif in node.netifs(): + for iface in node.get_ifaces(): emane_element = None - if isinstance(netif.net, EmaneNet): - emane_element = add_emane_interface(host_element, netif) + if isinstance(iface.net, EmaneNet): + emane_element = add_emane_iface(host_element, iface) parent_element = host_element if emane_element is not None: parent_element = emane_element - for address in netif.addrlist: + for address in iface.addrlist: address_type = get_address_type(address) - add_address(parent_element, address_type, address, netif.name) + add_address(parent_element, address_type, address, iface.name) diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index 2589edd9..4f511476 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -158,19 +158,19 @@ def build_node_platform_xml( logging.warning("warning: EMANE network %s has no associated model", node.name) return nem_id - for netif in node.netifs(): + for iface in node.get_ifaces(): logging.debug( - "building platform xml for interface(%s) nem_id(%s)", netif.name, nem_id + "building platform xml for interface(%s) nem_id(%s)", iface.name, nem_id ) # build nem xml - nem_definition = nem_file_name(node.model, netif) + nem_definition = nem_file_name(node.model, iface) nem_element = etree.Element( - "nem", id=str(nem_id), name=netif.localname, definition=nem_definition + "nem", id=str(nem_id), name=iface.localname, definition=nem_definition ) # check if this is an external transport, get default config if an interface # specific one does not exist - config = emane_manager.getifcconfig(node.model.id, netif, node.model.name) + config = emane_manager.get_iface_config(node.model.id, iface, node.model.name) if is_external(config): nem_element.set("transport", "external") @@ -180,9 +180,9 @@ def build_node_platform_xml( add_param(nem_element, transport_endpoint, config[transport_endpoint]) else: # build transport xml - transport_type = netif.transport_type + transport_type = iface.transport_type if not transport_type: - logging.info("warning: %s interface type unsupported!", netif.name) + logging.info("warning: %s interface type unsupported!", iface.name) transport_type = TransportType.RAW transport_file = transport_file_name(node.id, transport_type) transport_element = etree.SubElement( @@ -190,14 +190,14 @@ def build_node_platform_xml( ) # add transport parameter - add_param(transport_element, "device", netif.name) + add_param(transport_element, "device", iface.name) # add nem entry - nem_entries[netif] = nem_element + nem_entries[iface] = nem_element # merging code - key = netif.node.id - if netif.transport_type == TransportType.RAW: + key = iface.node.id + if iface.transport_type == TransportType.RAW: key = "host" otadev = control_net.brname eventdev = control_net.brname @@ -229,10 +229,10 @@ def build_node_platform_xml( platform_element.append(nem_element) - node.setnemid(netif, nem_id) + node.setnemid(iface, nem_id) macstr = _hwaddr_prefix + ":00:00:" macstr += f"{(nem_id >> 8) & 0xFF:02X}:{nem_id & 0xFF:02X}" - netif.sethwaddr(macstr) + iface.sethwaddr(macstr) # increment nem id nem_id += 1 @@ -280,19 +280,19 @@ def build_xml_files(emane_manager: "EmaneManager", node: EmaneNet) -> None: vtype = TransportType.VIRTUAL rtype = TransportType.RAW - for netif in node.netifs(): + for iface in node.get_ifaces(): # check for interface specific emane configuration and write xml files - config = emane_manager.getifcconfig(node.model.id, netif, node.model.name) + config = emane_manager.get_iface_config(node.model.id, iface, node.model.name) if config: - node.model.build_xml_files(config, netif) + node.model.build_xml_files(config, iface) # check transport type needed for interface - if netif.transport_type == TransportType.VIRTUAL: + if iface.transport_type == TransportType.VIRTUAL: need_virtual = True - vtype = netif.transport_type + vtype = iface.transport_type else: need_raw = True - rtype = netif.transport_type + rtype = iface.transport_type if need_virtual: build_transport_xml(emane_manager, node, vtype) @@ -494,70 +494,70 @@ def transport_file_name(node_id: int, transport_type: TransportType) -> str: return f"n{node_id}trans{transport_type.value}.xml" -def _basename(emane_model: "EmaneModel", interface: CoreInterface = None) -> str: +def _basename(emane_model: "EmaneModel", iface: CoreInterface = None) -> str: """ Create name that is leveraged for configuration file creation. :param emane_model: emane model to create name for - :param interface: interface for this model + :param iface: interface for this model :return: basename used for file creation """ name = f"n{emane_model.id}" - if interface: - node_id = interface.node.id - if emane_model.session.emane.getifcconfig(node_id, interface, emane_model.name): - name = interface.localname.replace(".", "_") + if iface: + node_id = iface.node.id + if emane_model.session.emane.get_iface_config(node_id, iface, emane_model.name): + name = iface.localname.replace(".", "_") return f"{name}{emane_model.name}" -def nem_file_name(emane_model: "EmaneModel", interface: CoreInterface = None) -> str: +def nem_file_name(emane_model: "EmaneModel", iface: CoreInterface = None) -> str: """ Return the string name for the NEM XML file, e.g. "n3rfpipenem.xml" :param emane_model: emane model to create file - :param interface: interface for this model + :param iface: interface for this model :return: nem xml filename """ - basename = _basename(emane_model, interface) + basename = _basename(emane_model, iface) append = "" - if interface and interface.transport_type == TransportType.RAW: + if iface and iface.transport_type == TransportType.RAW: append = "_raw" return f"{basename}nem{append}.xml" -def shim_file_name(emane_model: "EmaneModel", interface: CoreInterface = None) -> str: +def shim_file_name(emane_model: "EmaneModel", iface: CoreInterface = None) -> str: """ Return the string name for the SHIM XML file, e.g. "commeffectshim.xml" :param emane_model: emane model to create file - :param interface: interface for this model + :param iface: interface for this model :return: shim xml filename """ - name = _basename(emane_model, interface) + name = _basename(emane_model, iface) return f"{name}shim.xml" -def mac_file_name(emane_model: "EmaneModel", interface: CoreInterface = None) -> str: +def mac_file_name(emane_model: "EmaneModel", iface: CoreInterface = None) -> str: """ Return the string name for the MAC XML file, e.g. "n3rfpipemac.xml" :param emane_model: emane model to create file - :param interface: interface for this model + :param iface: interface for this model :return: mac xml filename """ - name = _basename(emane_model, interface) + name = _basename(emane_model, iface) return f"{name}mac.xml" -def phy_file_name(emane_model: "EmaneModel", interface: CoreInterface = None) -> str: +def phy_file_name(emane_model: "EmaneModel", iface: CoreInterface = None) -> str: """ Return the string name for the PHY XML file, e.g. "n3rfpipephy.xml" :param emane_model: emane model to create file - :param interface: interface for this model + :param iface: interface for this model :return: phy xml filename """ - name = _basename(emane_model, interface) + name = _basename(emane_model, iface) return f"{name}phy.xml" diff --git a/daemon/examples/configservices/testing.py b/daemon/examples/configservices/testing.py index 948ec739..767d0f45 100644 --- a/daemon/examples/configservices/testing.py +++ b/daemon/examples/configservices/testing.py @@ -20,13 +20,13 @@ if __name__ == "__main__": # node one options.config_services = ["DefaultRoute", "IPForward"] node1 = session.add_node(CoreNode, options=options) - interface = prefixes.create_interface(node1) - session.add_link(node1.id, switch.id, interface1_data=interface) + interface = prefixes.create_iface(node1) + session.add_link(node1.id, switch.id, iface1_data=interface) # node two node2 = session.add_node(CoreNode, options=options) - interface = prefixes.create_interface(node2) - session.add_link(node2.id, switch.id, interface1_data=interface) + interface = prefixes.create_iface(node2) + session.add_link(node2.id, switch.id, iface1_data=interface) # start session and run services session.instantiate() diff --git a/daemon/examples/docker/docker2core.py b/daemon/examples/docker/docker2core.py index 8151a590..c38f96af 100644 --- a/daemon/examples/docker/docker2core.py +++ b/daemon/examples/docker/docker2core.py @@ -18,11 +18,11 @@ if __name__ == "__main__": # create node one node1 = session.add_node(DockerNode, options=options) - interface1_data = prefixes.create_interface(node1) + interface1_data = prefixes.create_iface(node1) # create node two node2 = session.add_node(CoreNode) - interface2_data = prefixes.create_interface(node2) + interface2_data = prefixes.create_iface(node2) # add link session.add_link(node1.id, node2.id, interface1_data, interface2_data) diff --git a/daemon/examples/docker/docker2docker.py b/daemon/examples/docker/docker2docker.py index a7a70534..5b62d433 100644 --- a/daemon/examples/docker/docker2docker.py +++ b/daemon/examples/docker/docker2docker.py @@ -19,11 +19,11 @@ if __name__ == "__main__": # create node one node1 = session.add_node(DockerNode, options=options) - interface1_data = prefixes.create_interface(node1) + interface1_data = prefixes.create_iface(node1) # create node two node2 = session.add_node(DockerNode, options=options) - interface2_data = prefixes.create_interface(node2) + interface2_data = prefixes.create_iface(node2) # add link session.add_link(node1.id, node2.id, interface1_data, interface2_data) diff --git a/daemon/examples/docker/switch.py b/daemon/examples/docker/switch.py index ef057945..161cd823 100644 --- a/daemon/examples/docker/switch.py +++ b/daemon/examples/docker/switch.py @@ -23,15 +23,15 @@ if __name__ == "__main__": # node one node1 = session.add_node(DockerNode, options=options) - interface1_data = prefixes.create_interface(node1) + interface1_data = prefixes.create_iface(node1) # node two node2 = session.add_node(DockerNode, options=options) - interface2_data = prefixes.create_interface(node2) + interface2_data = prefixes.create_iface(node2) # node three node_three = session.add_node(CoreNode) - interface_three = prefixes.create_interface(node_three) + interface_three = prefixes.create_iface(node_three) # add links session.add_link(node1.id, switch.id, interface1_data) diff --git a/daemon/examples/grpc/distributed_switch.py b/daemon/examples/grpc/distributed_switch.py index e847016f..0d781c19 100644 --- a/daemon/examples/grpc/distributed_switch.py +++ b/daemon/examples/grpc/distributed_switch.py @@ -47,7 +47,7 @@ def main(args): node1_id = response.node_id # create link - interface1 = interface_helper.create_interface(node1_id, 0) + interface1 = interface_helper.create_iface(node1_id, 0) response = core.add_link(session_id, node1_id, switch_id, interface1) logging.info("created link from node one to switch: %s", response) @@ -59,7 +59,7 @@ def main(args): node2_id = response.node_id # create link - interface1 = interface_helper.create_interface(node2_id, 0) + interface1 = interface_helper.create_iface(node2_id, 0) response = core.add_link(session_id, node2_id, switch_id, interface1) logging.info("created link from node two to switch: %s", response) diff --git a/daemon/examples/grpc/emane80211.py b/daemon/examples/grpc/emane80211.py index 24532266..b8036db0 100644 --- a/daemon/examples/grpc/emane80211.py +++ b/daemon/examples/grpc/emane80211.py @@ -57,10 +57,10 @@ def main(): node2_id = response.node_id # links nodes to switch - interface1 = interface_helper.create_interface(node1_id, 0) + interface1 = interface_helper.create_iface(node1_id, 0) response = core.add_link(session_id, node1_id, emane_id, interface1) logging.info("created link: %s", response) - interface1 = interface_helper.create_interface(node2_id, 0) + interface1 = interface_helper.create_iface(node2_id, 0) response = core.add_link(session_id, node2_id, emane_id, interface1) logging.info("created link: %s", response) diff --git a/daemon/examples/grpc/switch.py b/daemon/examples/grpc/switch.py index 74e315c6..1ed7c684 100644 --- a/daemon/examples/grpc/switch.py +++ b/daemon/examples/grpc/switch.py @@ -53,10 +53,10 @@ def main(): node2_id = response.node_id # links nodes to switch - interface1 = interface_helper.create_interface(node1_id, 0) + interface1 = interface_helper.create_iface(node1_id, 0) response = core.add_link(session_id, node1_id, switch_id, interface1) logging.info("created link: %s", response) - interface1 = interface_helper.create_interface(node2_id, 0) + interface1 = interface_helper.create_iface(node2_id, 0) response = core.add_link(session_id, node2_id, switch_id, interface1) logging.info("created link: %s", response) diff --git a/daemon/examples/grpc/wlan.py b/daemon/examples/grpc/wlan.py index d60ca1be..715d4706 100644 --- a/daemon/examples/grpc/wlan.py +++ b/daemon/examples/grpc/wlan.py @@ -65,10 +65,10 @@ def main(): node2_id = response.node_id # links nodes to switch - interface1 = interface_helper.create_interface(node1_id, 0) + interface1 = interface_helper.create_iface(node1_id, 0) response = core.add_link(session_id, node1_id, wlan_id, interface1) logging.info("created link: %s", response) - interface1 = interface_helper.create_interface(node2_id, 0) + interface1 = interface_helper.create_iface(node2_id, 0) response = core.add_link(session_id, node2_id, wlan_id, interface1) logging.info("created link: %s", response) diff --git a/daemon/examples/lxd/lxd2core.py b/daemon/examples/lxd/lxd2core.py index 49b68943..3d8eef6a 100644 --- a/daemon/examples/lxd/lxd2core.py +++ b/daemon/examples/lxd/lxd2core.py @@ -18,11 +18,11 @@ if __name__ == "__main__": # create node one node1 = session.add_node(LxcNode, options=options) - interface1_data = prefixes.create_interface(node1) + interface1_data = prefixes.create_iface(node1) # create node two node2 = session.add_node(CoreNode) - interface2_data = prefixes.create_interface(node2) + interface2_data = prefixes.create_iface(node2) # add link session.add_link(node1.id, node2.id, interface1_data, interface2_data) diff --git a/daemon/examples/lxd/lxd2lxd.py b/daemon/examples/lxd/lxd2lxd.py index 18af8037..a7209b5c 100644 --- a/daemon/examples/lxd/lxd2lxd.py +++ b/daemon/examples/lxd/lxd2lxd.py @@ -19,11 +19,11 @@ if __name__ == "__main__": # create node one node1 = session.add_node(LxcNode, options=options) - interface1_data = prefixes.create_interface(node1) + interface1_data = prefixes.create_iface(node1) # create node two node2 = session.add_node(LxcNode, options=options) - interface2_data = prefixes.create_interface(node2) + interface2_data = prefixes.create_iface(node2) # add link session.add_link(node1.id, node2.id, interface1_data, interface2_data) diff --git a/daemon/examples/lxd/switch.py b/daemon/examples/lxd/switch.py index 31a79887..9b6801f5 100644 --- a/daemon/examples/lxd/switch.py +++ b/daemon/examples/lxd/switch.py @@ -23,15 +23,15 @@ if __name__ == "__main__": # node one node1 = session.add_node(LxcNode, options=options) - interface1_data = prefixes.create_interface(node1) + interface1_data = prefixes.create_iface(node1) # node two node2 = session.add_node(LxcNode, options=options) - interface2_data = prefixes.create_interface(node2) + interface2_data = prefixes.create_iface(node2) # node three node3 = session.add_node(CoreNode) - interface3_data = prefixes.create_interface(node3) + interface3_data = prefixes.create_iface(node3) # add links session.add_link(node1.id, switch.id, interface1_data) diff --git a/daemon/examples/myservices/sample.py b/daemon/examples/myservices/sample.py index 8c6dbe06..e0c9a232 100644 --- a/daemon/examples/myservices/sample.py +++ b/daemon/examples/myservices/sample.py @@ -80,8 +80,8 @@ class MyService(CoreService): if filename == cls.configs[0]: cfg += "# auto-generated by MyService (sample.py)\n" - for ifc in node.netifs(): - 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" diff --git a/daemon/examples/python/distributed_emane.py b/daemon/examples/python/distributed_emane.py index d9b41ea4..3ee56108 100644 --- a/daemon/examples/python/distributed_emane.py +++ b/daemon/examples/python/distributed_emane.py @@ -59,10 +59,10 @@ def main(args): node2 = session.add_node(CoreNode, options=options) # create node interfaces and link - interface1_data = prefixes.create_interface(node1) - interface2_data = prefixes.create_interface(node2) - session.add_link(node1.id, emane_net.id, interface1_data=interface1_data) - session.add_link(node2.id, emane_net.id, interface1_data=interface2_data) + interface1_data = prefixes.create_iface(node1) + interface2_data = prefixes.create_iface(node2) + session.add_link(node1.id, emane_net.id, iface1_data=interface1_data) + session.add_link(node2.id, emane_net.id, iface1_data=interface2_data) # instantiate session session.instantiate() diff --git a/daemon/examples/python/distributed_lxd.py b/daemon/examples/python/distributed_lxd.py index affb16a8..1573836a 100644 --- a/daemon/examples/python/distributed_lxd.py +++ b/daemon/examples/python/distributed_lxd.py @@ -48,8 +48,8 @@ def main(args): node2 = session.add_node(LxcNode, options=options) # create node interfaces and link - interface1_data = prefixes.create_interface(node1) - interface2_data = prefixes.create_interface(node2) + interface1_data = prefixes.create_iface(node1) + interface2_data = prefixes.create_iface(node2) session.add_link(node1.id, node2.id, interface1_data, interface2_data) # instantiate session diff --git a/daemon/examples/python/distributed_ptp.py b/daemon/examples/python/distributed_ptp.py index 6bf33474..1486c237 100644 --- a/daemon/examples/python/distributed_ptp.py +++ b/daemon/examples/python/distributed_ptp.py @@ -48,8 +48,8 @@ def main(args): node2 = session.add_node(CoreNode, options=options) # create node interfaces and link - interface1_data = prefixes.create_interface(node1) - interface2_data = prefixes.create_interface(node2) + interface1_data = prefixes.create_iface(node1) + interface2_data = prefixes.create_iface(node2) session.add_link(node1.id, node2.id, interface1_data, interface2_data) # instantiate session diff --git a/daemon/examples/python/distributed_switch.py b/daemon/examples/python/distributed_switch.py index 8991161e..e9eb1e81 100644 --- a/daemon/examples/python/distributed_switch.py +++ b/daemon/examples/python/distributed_switch.py @@ -52,10 +52,10 @@ def main(args): node2 = session.add_node(CoreNode, options=options) # create node interfaces and link - interface1_data = prefixes.create_interface(node1) - interface2_data = prefixes.create_interface(node2) - session.add_link(node1.id, switch.id, interface1_data=interface1_data) - session.add_link(node2.id, switch.id, interface1_data=interface2_data) + interface1_data = prefixes.create_iface(node1) + interface2_data = prefixes.create_iface(node2) + session.add_link(node1.id, switch.id, iface1_data=interface1_data) + session.add_link(node2.id, switch.id, iface1_data=interface2_data) # instantiate session session.instantiate() diff --git a/daemon/examples/python/emane80211.py b/daemon/examples/python/emane80211.py index d3f6652a..322e569f 100644 --- a/daemon/examples/python/emane80211.py +++ b/daemon/examples/python/emane80211.py @@ -42,8 +42,8 @@ def main(): for i in range(NODES): node = session.add_node(CoreNode, options=options) node.setposition(x=150 * (i + 1), y=150) - interface = prefixes.create_interface(node) - session.add_link(node.id, emane_network.id, interface1_data=interface) + interface = prefixes.create_iface(node) + session.add_link(node.id, emane_network.id, iface1_data=interface) # instantiate session session.instantiate() diff --git a/daemon/examples/python/switch.py b/daemon/examples/python/switch.py index 1b939cd7..902e79e0 100644 --- a/daemon/examples/python/switch.py +++ b/daemon/examples/python/switch.py @@ -31,8 +31,8 @@ def main(): # create nodes for _ in range(NODES): node = session.add_node(CoreNode) - interface = prefixes.create_interface(node) - session.add_link(node.id, switch.id, interface1_data=interface) + interface = prefixes.create_iface(node) + session.add_link(node.id, switch.id, iface1_data=interface) # instantiate session session.instantiate() diff --git a/daemon/examples/python/switch_inject.py b/daemon/examples/python/switch_inject.py index 59816b19..89f70e05 100644 --- a/daemon/examples/python/switch_inject.py +++ b/daemon/examples/python/switch_inject.py @@ -33,8 +33,8 @@ def main(): # create nodes for _ in range(NODES): node = session.add_node(CoreNode) - interface = prefixes.create_interface(node) - session.add_link(node.id, switch.id, interface1_data=interface) + interface = prefixes.create_iface(node) + session.add_link(node.id, switch.id, iface1_data=interface) # instantiate session session.instantiate() diff --git a/daemon/examples/python/wlan.py b/daemon/examples/python/wlan.py index 0302bbd3..547a5860 100644 --- a/daemon/examples/python/wlan.py +++ b/daemon/examples/python/wlan.py @@ -35,8 +35,8 @@ def main(): options.set_position(0, 0) for _ in range(NODES): node = session.add_node(CoreNode, options=options) - interface = prefixes.create_interface(node) - session.add_link(node.id, wlan.id, interface1_data=interface) + interface = prefixes.create_iface(node) + session.add_link(node.id, wlan.id, iface1_data=interface) # instantiate session session.instantiate() diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 828b41fb..f691621a 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -319,12 +319,12 @@ message ThroughputsRequest { message ThroughputsEvent { int32 session_id = 1; repeated BridgeThroughput bridge_throughputs = 2; - repeated InterfaceThroughput interface_throughputs = 3; + repeated InterfaceThroughput iface_throughputs = 3; } message InterfaceThroughput { int32 node_id = 1; - int32 interface_id = 2; + int32 iface_id = 2; double throughput = 3; } @@ -374,7 +374,7 @@ message ConfigEvent { string bitmap = 8; string possible_values = 9; string groups = 10; - int32 interface = 11; + int32 iface_id = 11; int32 network_id = 12; string opaque = 13; } @@ -416,7 +416,7 @@ message GetNodeRequest { message GetNodeResponse { Node node = 1; - repeated Interface interfaces = 2; + repeated Interface ifaces = 2; } message EditNodeRequest { @@ -492,16 +492,16 @@ message AddLinkRequest { message AddLinkResponse { bool result = 1; - Interface interface1 = 2; - Interface interface2 = 3; + Interface iface1 = 2; + Interface iface2 = 3; } message EditLinkRequest { int32 session_id = 1; int32 node1_id = 2; int32 node2_id = 3; - int32 interface1_id = 4; - int32 interface2_id = 5; + int32 iface1_id = 4; + int32 iface2_id = 5; LinkOptions options = 6; } @@ -513,8 +513,8 @@ message DeleteLinkRequest { int32 session_id = 1; int32 node1_id = 2; int32 node2_id = 3; - int32 interface1_id = 4; - int32 interface2_id = 5; + int32 iface1_id = 4; + int32 iface2_id = 5; } message DeleteLinkResponse { @@ -561,7 +561,7 @@ message GetInterfacesRequest { } message GetInterfacesResponse { - repeated string interfaces = 1; + repeated string ifaces = 1; } message ExecuteScriptRequest { @@ -705,8 +705,8 @@ message Link { int32 node1_id = 1; int32 node2_id = 2; LinkType.Enum type = 3; - Interface interface1 = 4; - Interface interface2 = 5; + Interface iface1 = 4; + Interface iface2 = 5; LinkOptions options = 6; int32 network_id = 7; string label = 8; diff --git a/daemon/proto/core/api/grpc/emane.proto b/daemon/proto/core/api/grpc/emane.proto index e4189700..ac5456fd 100644 --- a/daemon/proto/core/api/grpc/emane.proto +++ b/daemon/proto/core/api/grpc/emane.proto @@ -32,7 +32,7 @@ message GetEmaneModelsResponse { message GetEmaneModelConfigRequest { int32 session_id = 1; int32 node_id = 2; - int32 interface = 3; + int32 iface_id = 3; string model = 4; } @@ -57,7 +57,7 @@ message GetEmaneModelConfigsResponse { message ModelConfig { int32 node_id = 1; string model = 2; - int32 interface = 3; + int32 iface_id = 3; map config = 4; } repeated ModelConfig configs = 1; @@ -86,7 +86,7 @@ message EmaneLinkResponse { message EmaneModelConfig { int32 node_id = 1; - int32 interface_id = 2; + int32 iface_id = 2; string model = 3; map config = 4; } @@ -95,10 +95,10 @@ message EmanePathlossesRequest { int32 session_id = 1; int32 node1_id = 2; float rx1 = 3; - int32 interface1_id = 4; + int32 iface1_id = 4; int32 node2_id = 5; float rx2 = 6; - int32 interface2_id = 7; + int32 iface2_id = 7; } message EmanePathlossesResponse { diff --git a/daemon/scripts/core-route-monitor b/daemon/scripts/core-route-monitor index b12e6205..d644ae1b 100755 --- a/daemon/scripts/core-route-monitor +++ b/daemon/scripts/core-route-monitor @@ -101,8 +101,8 @@ class RouterMonitor: node_map[node.id] = node.channel if self.src_id is None: response = self.core.get_node(self.session, node.id) - for netif in response.interfaces: - if self.src == netif.ip4: + for iface in response.ifaces: + if self.src == iface.ip4: self.src_id = node.id break except grpc.RpcError: diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index 9d54d9c2..c3315e7c 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -89,7 +89,7 @@ def ip_prefixes(): @pytest.fixture(scope="session") -def interface_helper(): +def iface_helper(): return InterfaceHelper(ip4_prefix="10.83.0.0/16") diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py index 15e3d869..e1c7938b 100644 --- a/daemon/tests/emane/test_emane.py +++ b/daemon/tests/emane/test_emane.py @@ -79,8 +79,8 @@ class TestEmane: for i, node in enumerate([node1, node2]): node.setposition(x=150 * (i + 1), y=150) - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, emane_network.id, interface1_data=interface) + iface_data = ip_prefixes.create_iface(node) + session.add_link(node.id, emane_network.id, iface1_data=iface_data) # instantiate session session.instantiate() @@ -119,8 +119,8 @@ class TestEmane: for i, node in enumerate([node1, node2]): node.setposition(x=150 * (i + 1), y=150) - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, emane_network.id, interface1_data=interface) + iface_data = ip_prefixes.create_iface(node) + session.add_link(node.id, emane_network.id, iface1_data=iface_data) # instantiate session session.instantiate() diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 626f84a7..5771f7ad 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -53,8 +53,8 @@ class TestCore: # link nodes to net node for node in [node1, node2]: - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, net_node.id, interface1_data=interface) + iface_data = ip_prefixes.create_iface(node) + session.add_link(node.id, net_node.id, iface1_data=iface_data) # instantiate session session.instantiate() @@ -80,8 +80,8 @@ class TestCore: # link nodes to ptp net for node in [node1, node2]: - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, ptp_node.id, interface1_data=interface) + iface_data = ip_prefixes.create_iface(node) + session.add_link(node.id, ptp_node.id, iface1_data=iface_data) # get node client for testing client = node1.client @@ -96,9 +96,9 @@ class TestCore: if not request.config.getoption("mock"): assert client.check_cmd("echo hello") == "hello" - def test_netif(self, session: Session, ip_prefixes: IpPrefixes): + def test_iface(self, session: Session, ip_prefixes: IpPrefixes): """ - Test netif methods. + Test interface methods. :param session: session for test :param ip_prefixes: generates ip addresses for nodes @@ -113,8 +113,8 @@ class TestCore: # link nodes to ptp net for node in [node1, node2]: - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, ptp_node.id, interface1_data=interface) + iface = ip_prefixes.create_iface(node) + session.add_link(node.id, ptp_node.id, iface1_data=iface) # instantiate session session.instantiate() @@ -126,19 +126,19 @@ class TestCore: assert node1.commonnets(node2) assert node2.commonnets(node1) - # check we can retrieve netif index - assert node1.ifname(0) - assert node2.ifname(0) + # check we can retrieve interface id + assert 0 in node1.ifaces + assert 0 in node2.ifaces # check interface parameters - interface = node1.netif(0) - interface.setparam("test", 1) - assert interface.getparam("test") == 1 - assert interface.getparams() + iface = node1.get_iface(0) + iface.setparam("test", 1) + assert iface.getparam("test") == 1 + assert iface.getparams() - # delete netif and test that if no longer exists - node1.delnetif(0) - assert not node1.netif(0) + # delete interface and test that if no longer exists + node1.delete_iface(0) + assert 0 not in node1.ifaces def test_wlan_ping(self, session: Session, ip_prefixes: IpPrefixes): """ @@ -160,8 +160,8 @@ class TestCore: # link nodes for node in [node1, node2]: - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, wlan_node.id, interface1_data=interface) + iface_id = ip_prefixes.create_iface(node) + session.add_link(node.id, wlan_node.id, iface1_data=iface_id) # instantiate session session.instantiate() @@ -190,8 +190,8 @@ class TestCore: # link nodes for node in [node1, node2]: - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, wlan_node.id, interface1_data=interface) + iface_id = ip_prefixes.create_iface(node) + session.add_link(node.id, wlan_node.id, iface1_data=iface_id) # configure mobility script for session config = { diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 8beb4b9a..23ff0301 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -42,15 +42,17 @@ class TestGrpc: id=3, type=NodeTypes.WIRELESS_LAN.value, position=position ) nodes = [node1, node2, wlan_node] - interface_helper = InterfaceHelper(ip4_prefix="10.83.0.0/16") - interface1 = interface_helper.create_interface(node1.id, 0) - interface2 = interface_helper.create_interface(node2.id, 0) + iface_helper = InterfaceHelper(ip4_prefix="10.83.0.0/16") + iface1_id = 0 + iface1 = iface_helper.create_iface(node1.id, iface1_id) + iface2_id = 0 + iface2 = iface_helper.create_iface(node2.id, iface2_id) link = core_pb2.Link( type=core_pb2.LinkType.WIRED, node1_id=node1.id, node2_id=node2.id, - interface1=interface1, - interface2=interface2, + iface1=iface1, + iface2=iface2, ) links = [link] hook = core_pb2.Hook( @@ -81,7 +83,7 @@ class TestGrpc: model_config_value = "500000" model_config = EmaneModelConfig( node_id=model_node_id, - interface_id=-1, + iface_id=-1, model=EmaneIeee80211abgModel.name, config={model_config_key: model_config_value}, ) @@ -131,8 +133,8 @@ class TestGrpc: assert node1.id in session.nodes assert node2.id in session.nodes assert wlan_node.id in session.nodes - assert session.nodes[node1.id].netif(0) is not None - assert session.nodes[node2.id].netif(0) is not None + assert iface1_id in session.nodes[node1.id].ifaces + assert iface2_id in session.nodes[node2.id].ifaces hook_file, hook_data = session.hooks[EventTypes.RUNTIME_STATE][0] assert hook_file == hook.file assert hook_data == hook.data @@ -522,8 +524,8 @@ class TestGrpc: session = grpc_server.coreemu.create_session() switch = session.add_node(SwitchNode) node = session.add_node(CoreNode) - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, switch.id, interface) + iface_data = ip_prefixes.create_iface(node) + session.add_link(node.id, switch.id, iface_data) # then with client.context_connect(): @@ -540,17 +542,15 @@ class TestGrpc: session = grpc_server.coreemu.create_session() switch = session.add_node(SwitchNode) node = session.add_node(CoreNode) - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, switch.id, interface) + iface_data = ip_prefixes.create_iface(node) + session.add_link(node.id, switch.id, iface_data) # then with pytest.raises(grpc.RpcError): with client.context_connect(): client.get_node_links(session.id, 3) - def test_add_link( - self, grpc_server: CoreGrpcServer, interface_helper: InterfaceHelper - ): + def test_add_link(self, grpc_server: CoreGrpcServer, iface_helper: InterfaceHelper): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -559,16 +559,16 @@ class TestGrpc: assert len(switch.all_link_data()) == 0 # then - interface = interface_helper.create_interface(node.id, 0) + iface = iface_helper.create_iface(node.id, 0) with client.context_connect(): - response = client.add_link(session.id, node.id, switch.id, interface) + response = client.add_link(session.id, node.id, switch.id, iface) # then assert response.result is True assert len(switch.all_link_data()) == 1 def test_add_link_exception( - self, grpc_server: CoreGrpcServer, interface_helper: InterfaceHelper + self, grpc_server: CoreGrpcServer, iface_helper: InterfaceHelper ): # given client = CoreGrpcClient() @@ -576,10 +576,10 @@ class TestGrpc: node = session.add_node(CoreNode) # then - interface = interface_helper.create_interface(node.id, 0) + iface = iface_helper.create_iface(node.id, 0) with pytest.raises(grpc.RpcError): with client.context_connect(): - client.add_link(session.id, 1, 3, interface) + client.add_link(session.id, 1, 3, iface) def test_edit_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes): # given @@ -587,8 +587,8 @@ class TestGrpc: session = grpc_server.coreemu.create_session() switch = session.add_node(SwitchNode) node = session.add_node(CoreNode) - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, switch.id, interface) + iface = ip_prefixes.create_iface(node) + session.add_link(node.id, switch.id, iface) options = core_pb2.LinkOptions(bandwidth=30000) link = switch.all_link_data()[0] assert options.bandwidth != link.bandwidth @@ -596,7 +596,7 @@ class TestGrpc: # then with client.context_connect(): response = client.edit_link( - session.id, node.id, switch.id, options, interface1_id=interface.id + session.id, node.id, switch.id, options, iface1_id=iface.id ) # then @@ -609,10 +609,10 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() node1 = session.add_node(CoreNode) - interface1 = ip_prefixes.create_interface(node1) + iface1 = ip_prefixes.create_iface(node1) node2 = session.add_node(CoreNode) - interface2 = ip_prefixes.create_interface(node2) - session.add_link(node1.id, node2.id, interface1, interface2) + iface2 = ip_prefixes.create_iface(node2) + session.add_link(node1.id, node2.id, iface1, iface2) link_node = None for node_id in session.nodes: node = session.nodes[node_id] @@ -624,7 +624,7 @@ class TestGrpc: # then with client.context_connect(): response = client.delete_link( - session.id, node1.id, node2.id, interface1.id, interface2.id + session.id, node1.id, node2.id, iface1.id, iface2.id ) # then @@ -729,7 +729,7 @@ class TestGrpc: assert emane_network.id == model_config.node_id assert model_config.model == EmaneIeee80211abgModel.name assert len(model_config.config) > 0 - assert model_config.interface == -1 + assert model_config.iface_id == -1 def test_set_emane_model_config(self, grpc_server: CoreGrpcServer): # given @@ -1028,8 +1028,8 @@ class TestGrpc: session = grpc_server.coreemu.create_session() wlan = session.add_node(WlanNode) node = session.add_node(CoreNode) - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, wlan.id, interface) + iface = ip_prefixes.create_iface(node) + session.add_link(node.id, wlan.id, iface) link_data = wlan.all_link_data()[0] queue = Queue() diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index d3b9362d..c413295a 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -107,15 +107,15 @@ class TestGui: switch_id = 2 coretlv.session.add_node(SwitchNode, _id=switch_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface1_ip4 = str(ip_prefix[node1_id]) + iface1_ip4 = str(ip_prefix[node1_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ (LinkTlvs.N1_NUMBER, node1_id), (LinkTlvs.N2_NUMBER, switch_id), - (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE1_IP4, interface1_ip4), - (LinkTlvs.INTERFACE1_IP4_MASK, 24), + (LinkTlvs.IFACE1_NUMBER, 0), + (LinkTlvs.IFACE1_IP4, iface1_ip4), + (LinkTlvs.IFACE1_IP4_MASK, 24), ], ) @@ -131,15 +131,15 @@ class TestGui: switch_id = 2 coretlv.session.add_node(SwitchNode, _id=switch_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface2_ip4 = str(ip_prefix[node1_id]) + iface2_ip4 = str(ip_prefix[node1_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ (LinkTlvs.N1_NUMBER, switch_id), (LinkTlvs.N2_NUMBER, node1_id), - (LinkTlvs.INTERFACE2_NUMBER, 0), - (LinkTlvs.INTERFACE2_IP4, interface2_ip4), - (LinkTlvs.INTERFACE2_IP4_MASK, 24), + (LinkTlvs.IFACE2_NUMBER, 0), + (LinkTlvs.IFACE2_IP4, iface2_ip4), + (LinkTlvs.IFACE2_IP4_MASK, 24), ], ) @@ -155,19 +155,19 @@ class TestGui: node2_id = 2 coretlv.session.add_node(CoreNode, _id=node2_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface1_ip4 = str(ip_prefix[node1_id]) - interface2_ip4 = str(ip_prefix[node2_id]) + iface1_ip4 = str(ip_prefix[node1_id]) + iface2_ip4 = str(ip_prefix[node2_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ (LinkTlvs.N1_NUMBER, node1_id), (LinkTlvs.N2_NUMBER, node2_id), - (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE1_IP4, interface1_ip4), - (LinkTlvs.INTERFACE1_IP4_MASK, 24), - (LinkTlvs.INTERFACE2_NUMBER, 0), - (LinkTlvs.INTERFACE2_IP4, interface2_ip4), - (LinkTlvs.INTERFACE2_IP4_MASK, 24), + (LinkTlvs.IFACE1_NUMBER, 0), + (LinkTlvs.IFACE1_IP4, iface1_ip4), + (LinkTlvs.IFACE1_IP4_MASK, 24), + (LinkTlvs.IFACE2_NUMBER, 0), + (LinkTlvs.IFACE2_IP4, iface2_ip4), + (LinkTlvs.IFACE2_IP4_MASK, 24), ], ) @@ -185,15 +185,15 @@ class TestGui: switch_id = 2 coretlv.session.add_node(SwitchNode, _id=switch_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface1_ip4 = str(ip_prefix[node1_id]) + iface1_ip4 = str(ip_prefix[node1_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ (LinkTlvs.N1_NUMBER, node1_id), (LinkTlvs.N2_NUMBER, switch_id), - (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE1_IP4, interface1_ip4), - (LinkTlvs.INTERFACE1_IP4_MASK, 24), + (LinkTlvs.IFACE1_NUMBER, 0), + (LinkTlvs.IFACE1_IP4, iface1_ip4), + (LinkTlvs.IFACE1_IP4_MASK, 24), ], ) coretlv.handle_message(message) @@ -209,7 +209,7 @@ class TestGui: [ (LinkTlvs.N1_NUMBER, node1_id), (LinkTlvs.N2_NUMBER, switch_id), - (LinkTlvs.INTERFACE1_NUMBER, 0), + (LinkTlvs.IFACE1_NUMBER, 0), (LinkTlvs.BANDWIDTH, bandwidth), ], ) @@ -227,18 +227,18 @@ class TestGui: node2_id = 2 coretlv.session.add_node(CoreNode, _id=node2_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface1_ip4 = str(ip_prefix[node1_id]) - interface2_ip4 = str(ip_prefix[node2_id]) + iface1_ip4 = str(ip_prefix[node1_id]) + iface2_ip4 = str(ip_prefix[node2_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ (LinkTlvs.N1_NUMBER, node1_id), (LinkTlvs.N2_NUMBER, node2_id), - (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE1_IP4, interface1_ip4), - (LinkTlvs.INTERFACE1_IP4_MASK, 24), - (LinkTlvs.INTERFACE2_IP4, interface2_ip4), - (LinkTlvs.INTERFACE2_IP4_MASK, 24), + (LinkTlvs.IFACE1_NUMBER, 0), + (LinkTlvs.IFACE1_IP4, iface1_ip4), + (LinkTlvs.IFACE1_IP4_MASK, 24), + (LinkTlvs.IFACE2_IP4, iface2_ip4), + (LinkTlvs.IFACE2_IP4_MASK, 24), ], ) coretlv.handle_message(message) @@ -253,8 +253,8 @@ class TestGui: [ (LinkTlvs.N1_NUMBER, node1_id), (LinkTlvs.N2_NUMBER, node2_id), - (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE2_NUMBER, 0), + (LinkTlvs.IFACE1_NUMBER, 0), + (LinkTlvs.IFACE2_NUMBER, 0), ], ) coretlv.handle_message(message) @@ -271,15 +271,15 @@ class TestGui: switch_id = 2 coretlv.session.add_node(SwitchNode, _id=switch_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface1_ip4 = str(ip_prefix[node1_id]) + iface1_ip4 = str(ip_prefix[node1_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ (LinkTlvs.N1_NUMBER, node1_id), (LinkTlvs.N2_NUMBER, switch_id), - (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE1_IP4, interface1_ip4), - (LinkTlvs.INTERFACE1_IP4_MASK, 24), + (LinkTlvs.IFACE1_NUMBER, 0), + (LinkTlvs.IFACE1_IP4, iface1_ip4), + (LinkTlvs.IFACE1_IP4_MASK, 24), ], ) coretlv.handle_message(message) @@ -292,7 +292,7 @@ class TestGui: [ (LinkTlvs.N1_NUMBER, node1_id), (LinkTlvs.N2_NUMBER, switch_id), - (LinkTlvs.INTERFACE1_NUMBER, 0), + (LinkTlvs.IFACE1_NUMBER, 0), ], ) coretlv.handle_message(message) @@ -307,15 +307,15 @@ class TestGui: switch_id = 2 coretlv.session.add_node(SwitchNode, _id=switch_id) ip_prefix = netaddr.IPNetwork("10.0.0.0/24") - interface1_ip4 = str(ip_prefix[node1_id]) + iface1_ip4 = str(ip_prefix[node1_id]) message = coreapi.CoreLinkMessage.create( MessageFlags.ADD.value, [ (LinkTlvs.N1_NUMBER, node1_id), (LinkTlvs.N2_NUMBER, switch_id), - (LinkTlvs.INTERFACE1_NUMBER, 0), - (LinkTlvs.INTERFACE1_IP4, interface1_ip4), - (LinkTlvs.INTERFACE1_IP4_MASK, 24), + (LinkTlvs.IFACE1_NUMBER, 0), + (LinkTlvs.IFACE1_IP4, iface1_ip4), + (LinkTlvs.IFACE1_IP4_MASK, 24), ], ) coretlv.handle_message(message) @@ -328,7 +328,7 @@ class TestGui: [ (LinkTlvs.N1_NUMBER, switch_id), (LinkTlvs.N2_NUMBER, node1_id), - (LinkTlvs.INTERFACE2_NUMBER, 0), + (LinkTlvs.IFACE2_NUMBER, 0), ], ) coretlv.handle_message(message) diff --git a/daemon/tests/test_links.py b/daemon/tests/test_links.py index 819e2be8..fea4f4f8 100644 --- a/daemon/tests/test_links.py +++ b/daemon/tests/test_links.py @@ -14,9 +14,9 @@ def create_ptp_network( node2 = session.add_node(CoreNode) # link nodes to net node - interface1_data = ip_prefixes.create_interface(node1) - interface2_data = ip_prefixes.create_interface(node2) - session.add_link(node1.id, node2.id, interface1_data, interface2_data) + iface1_data = ip_prefixes.create_iface(node1) + iface2_data = ip_prefixes.create_iface(node2) + session.add_link(node1.id, node2.id, iface1_data, iface2_data) # instantiate session session.instantiate() @@ -29,41 +29,41 @@ class TestLinks: # given node1 = session.add_node(CoreNode) node2 = session.add_node(CoreNode) - interface1_data = ip_prefixes.create_interface(node1) - interface2_data = ip_prefixes.create_interface(node2) + iface1_data = ip_prefixes.create_iface(node1) + iface2_data = ip_prefixes.create_iface(node2) # when - session.add_link(node1.id, node2.id, interface1_data, interface2_data) + session.add_link(node1.id, node2.id, iface1_data, iface2_data) # then - assert node1.netif(interface1_data.id) - assert node2.netif(interface2_data.id) + assert node1.get_iface(iface1_data.id) + assert node2.get_iface(iface2_data.id) def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given node1 = session.add_node(CoreNode) node2 = session.add_node(SwitchNode) - interface1_data = ip_prefixes.create_interface(node1) + iface1_data = ip_prefixes.create_iface(node1) # when - session.add_link(node1.id, node2.id, interface1_data=interface1_data) + session.add_link(node1.id, node2.id, iface1_data=iface1_data) # then assert node2.all_link_data() - assert node1.netif(interface1_data.id) + assert node1.get_iface(iface1_data.id) def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given node1 = session.add_node(SwitchNode) node2 = session.add_node(CoreNode) - interface2_data = ip_prefixes.create_interface(node2) + iface2_data = ip_prefixes.create_iface(node2) # when - session.add_link(node1.id, node2.id, interface2_data=interface2_data) + session.add_link(node1.id, node2.id, iface2_data=iface2_data) # then assert node1.all_link_data() - assert node2.netif(interface2_data.id) + assert node2.get_iface(iface2_data.id) def test_add_net_to_net(self, session): # given @@ -85,29 +85,29 @@ class TestLinks: jitter = 10 node1 = session.add_node(CoreNode) node2 = session.add_node(SwitchNode) - interface1_data = ip_prefixes.create_interface(node1) - session.add_link(node1.id, node2.id, interface1_data) - interface1 = node1.netif(interface1_data.id) - assert interface1.getparam("delay") != delay - assert interface1.getparam("bw") != bandwidth - assert interface1.getparam("loss") != loss - assert interface1.getparam("duplicate") != dup - assert interface1.getparam("jitter") != jitter + iface1_data = ip_prefixes.create_iface(node1) + session.add_link(node1.id, node2.id, iface1_data) + iface1 = node1.get_iface(iface1_data.id) + assert iface1.getparam("delay") != delay + assert iface1.getparam("bw") != bandwidth + assert iface1.getparam("loss") != loss + assert iface1.getparam("duplicate") != dup + assert iface1.getparam("jitter") != jitter # when options = LinkOptions( delay=delay, bandwidth=bandwidth, loss=loss, dup=dup, jitter=jitter ) session.update_link( - node1.id, node2.id, interface1_id=interface1_data.id, options=options + node1.id, node2.id, iface1_id=iface1_data.id, options=options ) # then - assert interface1.getparam("delay") == delay - assert interface1.getparam("bw") == bandwidth - assert interface1.getparam("loss") == loss - assert interface1.getparam("duplicate") == dup - assert interface1.getparam("jitter") == jitter + assert iface1.getparam("delay") == delay + assert iface1.getparam("bw") == bandwidth + assert iface1.getparam("loss") == loss + assert iface1.getparam("duplicate") == dup + assert iface1.getparam("jitter") == jitter def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -118,29 +118,29 @@ class TestLinks: jitter = 10 node1 = session.add_node(SwitchNode) node2 = session.add_node(CoreNode) - interface2_data = ip_prefixes.create_interface(node2) - session.add_link(node1.id, node2.id, interface2_data=interface2_data) - interface2 = node2.netif(interface2_data.id) - assert interface2.getparam("delay") != delay - assert interface2.getparam("bw") != bandwidth - assert interface2.getparam("loss") != loss - assert interface2.getparam("duplicate") != dup - assert interface2.getparam("jitter") != jitter + iface2_data = ip_prefixes.create_iface(node2) + session.add_link(node1.id, node2.id, iface2_data=iface2_data) + iface2 = node2.get_iface(iface2_data.id) + 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 # when options = LinkOptions( delay=delay, bandwidth=bandwidth, loss=loss, dup=dup, jitter=jitter ) session.update_link( - node1.id, node2.id, interface2_id=interface2_data.id, options=options + node1.id, node2.id, iface2_id=iface2_data.id, options=options ) # then - assert interface2.getparam("delay") == delay - assert interface2.getparam("bw") == bandwidth - assert interface2.getparam("loss") == loss - assert interface2.getparam("duplicate") == dup - assert interface2.getparam("jitter") == jitter + 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 def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -151,83 +151,81 @@ class TestLinks: jitter = 10 node1 = session.add_node(CoreNode) node2 = session.add_node(CoreNode) - interface1_data = ip_prefixes.create_interface(node1) - interface2_data = ip_prefixes.create_interface(node2) - session.add_link(node1.id, node2.id, interface1_data, interface2_data) - interface1 = node1.netif(interface1_data.id) - interface2 = node2.netif(interface2_data.id) - assert interface1.getparam("delay") != delay - assert interface1.getparam("bw") != bandwidth - assert interface1.getparam("loss") != loss - assert interface1.getparam("duplicate") != dup - assert interface1.getparam("jitter") != jitter - assert interface2.getparam("delay") != delay - assert interface2.getparam("bw") != bandwidth - assert interface2.getparam("loss") != loss - assert interface2.getparam("duplicate") != dup - assert interface2.getparam("jitter") != jitter + iface1_data = ip_prefixes.create_iface(node1) + iface2_data = ip_prefixes.create_iface(node2) + session.add_link(node1.id, node2.id, iface1_data, iface2_data) + iface1 = node1.get_iface(iface1_data.id) + iface2 = node2.get_iface(iface2_data.id) + assert iface1.getparam("delay") != delay + assert iface1.getparam("bw") != bandwidth + assert iface1.getparam("loss") != loss + assert iface1.getparam("duplicate") != dup + assert iface1.getparam("jitter") != jitter + 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 # when options = LinkOptions( delay=delay, bandwidth=bandwidth, loss=loss, dup=dup, jitter=jitter ) - session.update_link( - node1.id, node2.id, interface1_data.id, interface2_data.id, options - ) + session.update_link(node1.id, node2.id, iface1_data.id, iface2_data.id, options) # then - assert interface1.getparam("delay") == delay - assert interface1.getparam("bw") == bandwidth - assert interface1.getparam("loss") == loss - assert interface1.getparam("duplicate") == dup - assert interface1.getparam("jitter") == jitter - assert interface2.getparam("delay") == delay - assert interface2.getparam("bw") == bandwidth - assert interface2.getparam("loss") == loss - assert interface2.getparam("duplicate") == dup - assert interface2.getparam("jitter") == jitter + assert iface1.getparam("delay") == delay + assert iface1.getparam("bw") == bandwidth + assert iface1.getparam("loss") == loss + assert iface1.getparam("duplicate") == dup + assert iface1.getparam("jitter") == jitter + 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 def test_delete_ptp(self, session: Session, ip_prefixes: IpPrefixes): # given node1 = session.add_node(CoreNode) node2 = session.add_node(CoreNode) - interface1_data = ip_prefixes.create_interface(node1) - interface2_data = ip_prefixes.create_interface(node2) - session.add_link(node1.id, node2.id, interface1_data, interface2_data) - assert node1.netif(interface1_data.id) - assert node2.netif(interface2_data.id) + iface1_data = ip_prefixes.create_iface(node1) + iface2_data = ip_prefixes.create_iface(node2) + session.add_link(node1.id, node2.id, iface1_data, iface2_data) + assert node1.get_iface(iface1_data.id) + assert node2.get_iface(iface2_data.id) # when - session.delete_link(node1.id, node2.id, interface1_data.id, interface2_data.id) + session.delete_link(node1.id, node2.id, iface1_data.id, iface2_data.id) # then - assert not node1.netif(interface1_data.id) - assert not node2.netif(interface2_data.id) + assert iface1_data.id not in node1.ifaces + assert iface2_data.id not in node2.ifaces def test_delete_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given node1 = session.add_node(CoreNode) node2 = session.add_node(SwitchNode) - interface1_data = ip_prefixes.create_interface(node1) - session.add_link(node1.id, node2.id, interface1_data) - assert node1.netif(interface1_data.id) + iface1_data = ip_prefixes.create_iface(node1) + session.add_link(node1.id, node2.id, iface1_data) + assert node1.get_iface(iface1_data.id) # when - session.delete_link(node1.id, node2.id, interface1_id=interface1_data.id) + session.delete_link(node1.id, node2.id, iface1_id=iface1_data.id) # then - assert not node1.netif(interface1_data.id) + assert iface1_data.id not in node1.ifaces def test_delete_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given node1 = session.add_node(SwitchNode) node2 = session.add_node(CoreNode) - interface2_data = ip_prefixes.create_interface(node2) - session.add_link(node1.id, node2.id, interface2_data=interface2_data) - assert node2.netif(interface2_data.id) + iface2_data = ip_prefixes.create_iface(node2) + session.add_link(node1.id, node2.id, iface2_data=iface2_data) + assert node2.get_iface(iface2_data.id) # when - session.delete_link(node1.id, node2.id, interface2_id=interface2_data.id) + session.delete_link(node1.id, node2.id, iface2_id=iface2_data.id) # then - assert not node2.netif(interface2_data.id) + assert iface2_data.id not in node2.ifaces diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index 0cbdb8ae..d7e435ab 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -53,53 +53,53 @@ class TestNodes: # given node = session.add_node(CoreNode) switch = session.add_node(SwitchNode) - interface_data = InterfaceData() - interface = node.newnetif(switch, interface_data) + iface_data = InterfaceData() + iface = node.new_iface(switch, iface_data) mac = "aa:aa:aa:ff:ff:ff" # when - node.sethwaddr(interface.netindex, mac) + node.sethwaddr(iface.node_id, mac) # then - assert interface.hwaddr == mac + assert iface.hwaddr == mac def test_node_sethwaddr_exception(self, session: Session): # given node = session.add_node(CoreNode) switch = session.add_node(SwitchNode) - interface_data = InterfaceData() - interface = node.newnetif(switch, interface_data) + iface_data = InterfaceData() + iface = node.new_iface(switch, iface_data) mac = "aa:aa:aa:ff:ff:fff" # when with pytest.raises(CoreError): - node.sethwaddr(interface.netindex, mac) + node.sethwaddr(iface.node_id, mac) def test_node_addaddr(self, session: Session): # given node = session.add_node(CoreNode) switch = session.add_node(SwitchNode) - interface_data = InterfaceData() - interface = node.newnetif(switch, interface_data) + iface_data = InterfaceData() + iface = node.new_iface(switch, iface_data) addr = "192.168.0.1/24" # when - node.addaddr(interface.netindex, addr) + node.addaddr(iface.node_id, addr) # then - assert interface.addrlist[0] == addr + assert iface.addrlist[0] == addr def test_node_addaddr_exception(self, session): # given node = session.add_node(CoreNode) switch = session.add_node(SwitchNode) - interface_data = InterfaceData() - interface = node.newnetif(switch, interface_data) + iface_data = InterfaceData() + iface = node.new_iface(switch, iface_data) addr = "256.168.0.1/24" # when with pytest.raises(CoreError): - node.addaddr(interface.netindex, addr) + node.addaddr(iface.node_id, addr) @pytest.mark.parametrize("net_type", NET_TYPES) def test_net(self, session, net_type): diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index 0b44a354..55f5a2ab 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -73,8 +73,8 @@ class TestXml: # link nodes to ptp net for node in [node1, node2]: - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, ptp_node.id, interface1_data=interface) + iface_data = ip_prefixes.create_iface(node) + session.add_link(node.id, ptp_node.id, iface1_data=iface_data) # instantiate session session.instantiate() @@ -128,8 +128,8 @@ class TestXml: # link nodes to ptp net for node in [node1, node2]: - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, ptp_node.id, interface1_data=interface) + iface_data = ip_prefixes.create_iface(node) + session.add_link(node.id, ptp_node.id, iface1_data=iface_data) # set custom values for node service session.services.set_service(node1.id, SshService.name) @@ -197,8 +197,8 @@ class TestXml: # link nodes for node in [node1, node2]: - interface = ip_prefixes.create_interface(node) - session.add_link(node.id, wlan_node.id, interface1_data=interface) + iface_data = ip_prefixes.create_iface(node) + session.add_link(node.id, wlan_node.id, iface1_data=iface_data) # instantiate session session.instantiate() @@ -299,7 +299,7 @@ class TestXml: """ # create nodes node1 = session.add_node(CoreNode) - interface1_data = ip_prefixes.create_interface(node1) + iface1_data = ip_prefixes.create_iface(node1) switch = session.add_node(SwitchNode) # create link @@ -309,7 +309,7 @@ class TestXml: options.jitter = 10 options.delay = 30 options.dup = 5 - session.add_link(node1.id, switch.id, interface1_data, options=options) + session.add_link(node1.id, switch.id, iface1_data, options=options) # instantiate session session.instantiate() @@ -365,9 +365,9 @@ class TestXml: """ # create nodes node1 = session.add_node(CoreNode) - interface1_data = ip_prefixes.create_interface(node1) + iface1_data = ip_prefixes.create_iface(node1) node2 = session.add_node(CoreNode) - interface2_data = ip_prefixes.create_interface(node2) + iface2_data = ip_prefixes.create_iface(node2) # create link options = LinkOptions() @@ -376,7 +376,7 @@ class TestXml: options.jitter = 10 options.delay = 30 options.dup = 5 - session.add_link(node1.id, node2.id, interface1_data, interface2_data, options) + session.add_link(node1.id, node2.id, iface1_data, iface2_data, options) # instantiate session session.instantiate() @@ -432,9 +432,9 @@ class TestXml: """ # create nodes node1 = session.add_node(CoreNode) - interface1_data = ip_prefixes.create_interface(node1) + iface1_data = ip_prefixes.create_iface(node1) node2 = session.add_node(CoreNode) - interface2_data = ip_prefixes.create_interface(node2) + iface2_data = ip_prefixes.create_iface(node2) # create link options1 = LinkOptions() @@ -444,7 +444,7 @@ class TestXml: options1.loss = 10.5 options1.dup = 5 options1.jitter = 5 - session.add_link(node1.id, node2.id, interface1_data, interface2_data, options1) + session.add_link(node1.id, node2.id, iface1_data, iface2_data, options1) options2 = LinkOptions() options2.unidirectional = 1 options2.bandwidth = 10000 @@ -453,7 +453,7 @@ class TestXml: options2.dup = 10 options2.jitter = 10 session.update_link( - node2.id, node1.id, interface2_data.id, interface1_data.id, options2 + node2.id, node1.id, iface2_data.id, iface1_data.id, options2 ) # instantiate session diff --git a/docs/scripting.md b/docs/scripting.md index 59bc02ae..18666a9a 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -61,8 +61,8 @@ def main(): # create nodes for _ in range(NODES): node = session.add_node(CoreNode) - interface = prefixes.create_interface(node) - session.add_link(node.id, switch.id, interface1_data=interface) + interface = prefixes.create_iface(node) + session.add_link(node.id, switch.id, iface1_data=interface) # instantiate session session.instantiate() From eeca33e72240aa83e355f584753c8f1e2038f91a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 16 Jun 2020 12:50:24 -0700 Subject: [PATCH 0359/1131] combined core.emulator.data and core.emulator.emudata, updated LinkData to leverage InterfaceData, instead of repeated interface fields, removed session from LinkData and LinkOptions --- daemon/core/api/grpc/client.py | 2 +- daemon/core/api/grpc/grpcutils.py | 39 ++-- daemon/core/api/grpc/server.py | 3 +- daemon/core/api/tlv/corehandlers.py | 43 ++-- daemon/core/emane/commeffect.py | 2 +- daemon/core/emane/emanemodel.py | 2 +- daemon/core/emane/nodes.py | 3 +- daemon/core/emulator/data.py | 221 +++++++++++++++++-- daemon/core/emulator/emudata.py | 206 ----------------- daemon/core/emulator/session.py | 4 +- daemon/core/location/mobility.py | 3 +- daemon/core/nodes/base.py | 26 +-- daemon/core/nodes/network.py | 55 ++--- daemon/core/nodes/physical.py | 2 +- daemon/core/xml/corexml.py | 71 ++---- daemon/examples/configservices/testing.py | 2 +- daemon/examples/docker/docker2core.py | 2 +- daemon/examples/docker/docker2docker.py | 2 +- daemon/examples/docker/switch.py | 2 +- daemon/examples/lxd/lxd2core.py | 2 +- daemon/examples/lxd/lxd2lxd.py | 2 +- daemon/examples/lxd/switch.py | 2 +- daemon/examples/python/distributed_emane.py | 2 +- daemon/examples/python/distributed_lxd.py | 2 +- daemon/examples/python/distributed_ptp.py | 2 +- daemon/examples/python/distributed_switch.py | 2 +- daemon/examples/python/emane80211.py | 2 +- daemon/examples/python/switch.py | 2 +- daemon/examples/python/switch_inject.py | 2 +- daemon/examples/python/wlan.py | 2 +- daemon/tests/conftest.py | 2 +- daemon/tests/emane/test_emane.py | 2 +- daemon/tests/test_core.py | 2 +- daemon/tests/test_distributed.py | 2 +- daemon/tests/test_grpc.py | 3 +- daemon/tests/test_links.py | 2 +- daemon/tests/test_nodes.py | 2 +- daemon/tests/test_xml.py | 2 +- docs/scripting.md | 2 +- 39 files changed, 332 insertions(+), 399 deletions(-) delete mode 100644 daemon/core/emulator/emudata.py diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 47aaef63..68bfc502 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -92,7 +92,7 @@ from core.api.grpc.wlan_pb2 import ( WlanLinkRequest, WlanLinkResponse, ) -from core.emulator.emudata import IpPrefixes +from core.emulator.data import IpPrefixes class InterfaceHelper: diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index f2f85798..095c4d0c 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -11,8 +11,7 @@ from core.api.grpc import common_pb2, core_pb2 from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig from core.config import ConfigurableOptions from core.emane.nodes import EmaneNet -from core.emulator.data import LinkData -from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions +from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions from core.emulator.enumerations import LinkTypes, NodeTypes from core.emulator.session import Session from core.nodes.base import CoreNode, NodeBase @@ -308,6 +307,18 @@ def parse_emane_model_id(_id: int) -> Tuple[int, int]: return node_id, iface_id +def convert_iface(iface_data: InterfaceData) -> core_pb2.Interface: + return core_pb2.Interface( + id=iface_data.id, + name=iface_data.name, + mac=iface_data.mac, + ip4=iface_data.ip4, + ip4mask=iface_data.ip4_mask, + ip6=iface_data.ip6, + ip6mask=iface_data.ip6_mask, + ) + + def convert_link(link_data: LinkData) -> core_pb2.Link: """ Convert link_data into core protobuf link. @@ -316,27 +327,11 @@ def convert_link(link_data: LinkData) -> core_pb2.Link: :return: core protobuf Link """ iface1 = None - if link_data.iface1_id is not None: - iface1 = core_pb2.Interface( - id=link_data.iface1_id, - name=link_data.iface1_name, - mac=convert_value(link_data.iface1_mac), - ip4=convert_value(link_data.iface1_ip4), - ip4mask=link_data.iface1_ip4_mask, - ip6=convert_value(link_data.iface1_ip6), - ip6mask=link_data.iface1_ip6_mask, - ) + if link_data.iface1 is not None: + iface1 = convert_iface(link_data.iface1) iface2 = None - if link_data.iface2_id is not None: - iface2 = core_pb2.Interface( - id=link_data.iface2_id, - name=link_data.iface2_name, - mac=convert_value(link_data.iface2_mac), - ip4=convert_value(link_data.iface2_ip4), - ip4mask=link_data.iface2_ip4_mask, - ip6=convert_value(link_data.iface2_ip6), - ip6mask=link_data.iface2_ip6_mask, - ) + if link_data.iface2 is not None: + iface2 = convert_iface(link_data.iface2) options = core_pb2.LinkOptions( opaque=link_data.opaque, jitter=link_data.jitter, diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 87b69a77..1be60116 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -108,8 +108,7 @@ from core.api.grpc.wlan_pb2 import ( WlanLinkResponse, ) from core.emulator.coreemu import CoreEmu -from core.emulator.data import LinkData -from core.emulator.emudata import LinkOptions, NodeOptions +from core.emulator.data import LinkData, LinkOptions, NodeOptions from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags from core.emulator.session import NT, Session from core.errors import CoreCommandError, CoreError diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index b09a37fe..88906e0c 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -29,8 +29,15 @@ from core.api.tlv.enumerations import ( NodeTlvs, SessionTlvs, ) -from core.emulator.data import ConfigData, EventData, ExceptionData, FileData -from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions +from core.emulator.data import ( + ConfigData, + EventData, + ExceptionData, + FileData, + InterfaceData, + LinkOptions, + NodeOptions, +) from core.emulator.enumerations import ( ConfigDataTypes, EventTypes, @@ -342,6 +349,12 @@ class CoreHandler(socketserver.BaseRequestHandler): dup = "" if link_data.dup is not None: dup = str(link_data.dup) + iface1 = link_data.iface1 + if iface1 is None: + iface1 = InterfaceData() + iface2 = link_data.iface2 + if iface2 is None: + iface2 = InterfaceData() tlv_data = structutils.pack_values( coreapi.CoreLinkTlv, @@ -355,7 +368,6 @@ class CoreHandler(socketserver.BaseRequestHandler): (LinkTlvs.JITTER, link_data.jitter), (LinkTlvs.MER, link_data.mer), (LinkTlvs.BURST, link_data.burst), - (LinkTlvs.SESSION, link_data.session), (LinkTlvs.MBURST, link_data.mburst), (LinkTlvs.TYPE, link_data.link_type.value), (LinkTlvs.GUI_ATTRIBUTES, link_data.gui_attributes), @@ -363,18 +375,18 @@ class CoreHandler(socketserver.BaseRequestHandler): (LinkTlvs.EMULATION_ID, link_data.emulation_id), (LinkTlvs.NETWORK_ID, link_data.network_id), (LinkTlvs.KEY, link_data.key), - (LinkTlvs.IFACE1_NUMBER, link_data.iface1_id), - (LinkTlvs.IFACE1_IP4, link_data.iface1_ip4), - (LinkTlvs.IFACE1_IP4_MASK, link_data.iface1_ip4_mask), - (LinkTlvs.IFACE1_MAC, link_data.iface1_mac), - (LinkTlvs.IFACE1_IP6, link_data.iface1_ip6), - (LinkTlvs.IFACE1_IP6_MASK, link_data.iface1_ip6_mask), - (LinkTlvs.IFACE2_NUMBER, link_data.iface2_id), - (LinkTlvs.IFACE2_IP4, link_data.iface2_ip4), - (LinkTlvs.IFACE2_IP4_MASK, link_data.iface2_ip4_mask), - (LinkTlvs.IFACE2_MAC, link_data.iface2_mac), - (LinkTlvs.IFACE2_IP6, link_data.iface2_ip6), - (LinkTlvs.IFACE2_IP6_MASK, link_data.iface2_ip6_mask), + (LinkTlvs.IFACE1_NUMBER, iface1.id), + (LinkTlvs.IFACE1_IP4, iface1.ip4), + (LinkTlvs.IFACE1_IP4_MASK, iface1.ip4_mask), + (LinkTlvs.IFACE1_MAC, iface1.mac), + (LinkTlvs.IFACE1_IP6, iface1.ip6), + (LinkTlvs.IFACE1_IP6_MASK, iface1.ip6_mask), + (LinkTlvs.IFACE2_NUMBER, iface2.id), + (LinkTlvs.IFACE2_IP4, iface2.ip4), + (LinkTlvs.IFACE2_IP4_MASK, iface2.ip4_mask), + (LinkTlvs.IFACE2_MAC, iface2.mac), + (LinkTlvs.IFACE2_IP6, iface2.ip6), + (LinkTlvs.IFACE2_IP6_MASK, iface2.ip6_mask), (LinkTlvs.OPAQUE, link_data.opaque), ], ) @@ -774,7 +786,6 @@ class CoreHandler(socketserver.BaseRequestHandler): options = LinkOptions(type=link_type) options.delay = message.get_tlv(LinkTlvs.DELAY.value) options.bandwidth = message.get_tlv(LinkTlvs.BANDWIDTH.value) - options.session = message.get_tlv(LinkTlvs.SESSION.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) diff --git a/daemon/core/emane/commeffect.py b/daemon/core/emane/commeffect.py index 0f441d76..610099f1 100644 --- a/daemon/core/emane/commeffect.py +++ b/daemon/core/emane/commeffect.py @@ -11,7 +11,7 @@ from lxml import etree from core.config import ConfigGroup, Configuration from core.emane import emanemanifest, emanemodel from core.emane.nodes import EmaneNet -from core.emulator.emudata import LinkOptions +from core.emulator.data import LinkOptions from core.emulator.enumerations import TransportType from core.nodes.interface import CoreInterface from core.xml import emanexml diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index 1a14011a..43fbc0fb 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -8,7 +8,7 @@ from typing import Dict, List, Optional, Set from core.config import ConfigGroup, Configuration from core.emane import emanemanifest from core.emane.nodes import EmaneNet -from core.emulator.emudata import LinkOptions +from core.emulator.data import LinkOptions from core.emulator.enumerations import ConfigDataTypes, TransportType from core.errors import CoreError from core.location.mobility import WirelessModel diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index eed51ff2..c28f1382 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -6,9 +6,8 @@ share the same MAC+PHY model. import logging from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type -from core.emulator.data import LinkData +from core.emulator.data import LinkData, LinkOptions from core.emulator.distributed import DistributedServer -from core.emulator.emudata import LinkOptions from core.emulator.enumerations import ( LinkTypes, MessageFlags, diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index 47f45820..c08a70f0 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -1,10 +1,12 @@ """ CORE data objects. """ +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, List, Optional, Tuple -from dataclasses import dataclass -from typing import List, Tuple +import netaddr +from core import utils from core.emulator.enumerations import ( EventTypes, ExceptionLevels, @@ -13,6 +15,9 @@ from core.emulator.enumerations import ( NodeTypes, ) +if TYPE_CHECKING: + from core.nodes.base import CoreNode + @dataclass class ConfigData: @@ -93,6 +98,57 @@ class NodeData: source: str = None +@dataclass +class InterfaceData: + """ + Convenience class for storing interface data. + """ + + id: int = None + name: str = None + mac: str = None + ip4: str = None + ip4_mask: int = None + ip6: str = None + ip6_mask: int = None + + def get_addresses(self) -> List[str]: + """ + Returns a list of ip4 and ip6 addresses when present. + + :return: list of addresses + """ + addresses = [] + if self.ip4 and self.ip4_mask: + addresses.append(f"{self.ip4}/{self.ip4_mask}") + if self.ip6 and self.ip6_mask: + addresses.append(f"{self.ip6}/{self.ip6_mask}") + return addresses + + +@dataclass +class LinkOptions: + """ + Options for creating and updating links within core. + """ + + type: LinkTypes = LinkTypes.WIRED + delay: int = None + bandwidth: int = None + loss: float = None + dup: int = None + jitter: int = None + mer: int = None + burst: int = None + mburst: int = None + gui_attributes: str = None + unidirectional: bool = None + emulation_id: int = None + network_id: int = None + key: int = None + opaque: str = None + + @dataclass class LinkData: message_type: MessageFlags = None @@ -106,7 +162,6 @@ class LinkData: jitter: float = None mer: float = None burst: float = None - session: int = None mburst: float = None link_type: LinkTypes = None gui_attributes: str = None @@ -114,19 +169,151 @@ class LinkData: emulation_id: int = None network_id: int = None key: int = None - iface1_id: int = None - iface1_name: str = None - iface1_ip4: str = None - iface1_ip4_mask: int = None - iface1_mac: str = None - iface1_ip6: str = None - iface1_ip6_mask: int = None - iface2_id: int = None - iface2_name: str = None - iface2_ip4: str = None - iface2_ip4_mask: int = None - iface2_mac: str = None - iface2_ip6: str = None - iface2_ip6_mask: int = None + iface1: InterfaceData = None + iface2: InterfaceData = None opaque: str = None color: str = None + + +class IpPrefixes: + """ + Convenience class to help generate IP4 and IP6 addresses for nodes within CORE. + """ + + def __init__(self, ip4_prefix: str = None, ip6_prefix: str = None) -> None: + """ + Creates an IpPrefixes object. + + :param ip4_prefix: ip4 prefix to use for generation + :param ip6_prefix: ip6 prefix to use for generation + :raises ValueError: when both ip4 and ip6 prefixes have not been provided + """ + if not ip4_prefix and not ip6_prefix: + raise ValueError("ip4 or ip6 must be provided") + + self.ip4 = None + if ip4_prefix: + self.ip4 = netaddr.IPNetwork(ip4_prefix) + self.ip6 = None + if ip6_prefix: + self.ip6 = netaddr.IPNetwork(ip6_prefix) + + def ip4_address(self, node_id: int) -> str: + """ + Convenience method to return the IP4 address for a node. + + :param node_id: node id to get IP4 address for + :return: IP4 address or None + """ + if not self.ip4: + raise ValueError("ip4 prefixes have not been set") + return str(self.ip4[node_id]) + + def ip6_address(self, node_id: int) -> str: + """ + Convenience method to return the IP6 address for a node. + + :param node_id: node id to get IP6 address for + :return: IP4 address or None + """ + if not self.ip6: + raise ValueError("ip6 prefixes have not been set") + return str(self.ip6[node_id]) + + def gen_iface(self, node_id: int, name: str = None, mac: str = None): + """ + Creates interface data for linking nodes, using the nodes unique id for + generation, along with a random mac address, unless provided. + + :param node_id: node id to create an interface for + :param name: name to set for interface, default is eth{id} + :param mac: mac address to use for this interface, default is random + generation + :return: new interface data for the provided node + """ + # generate ip4 data + ip4 = None + ip4_mask = None + if self.ip4: + ip4 = self.ip4_address(node_id) + ip4_mask = self.ip4.prefixlen + + # generate ip6 data + ip6 = None + ip6_mask = None + if self.ip6: + ip6 = self.ip6_address(node_id) + ip6_mask = self.ip6.prefixlen + + # random mac + if not mac: + mac = utils.random_mac() + + return InterfaceData( + name=name, ip4=ip4, ip4_mask=ip4_mask, ip6=ip6, ip6_mask=ip6_mask, mac=mac + ) + + def create_iface( + self, node: "CoreNode", name: str = None, mac: str = None + ) -> InterfaceData: + """ + Creates interface data for linking nodes, using the nodes unique id for + generation, along with a random mac address, unless provided. + + :param node: node to create interface for + :param name: name to set for interface, default is eth{id} + :param mac: mac address to use for this interface, default is random + generation + :return: new interface data for the provided node + """ + iface_data = self.gen_iface(node.id, name, mac) + iface_data.id = node.next_iface_id() + return iface_data + + +@dataclass +class NodeOptions: + """ + Options for creating and updating nodes within core. + """ + + name: str = None + model: Optional[str] = "PC" + canvas: int = None + icon: str = None + opaque: str = None + services: List[str] = field(default_factory=list) + config_services: List[str] = field(default_factory=list) + x: float = None + y: float = None + lat: float = None + lon: float = None + alt: float = None + emulation_id: int = None + server: str = None + image: str = None + emane: str = None + + def set_position(self, x: float, y: float) -> None: + """ + Convenience method for setting position. + + :param x: x position + :param y: y position + :return: nothing + """ + self.x = x + self.y = y + + def set_location(self, lat: float, lon: float, alt: float) -> None: + """ + Convenience method for setting location. + + :param lat: latitude + :param lon: longitude + :param alt: altitude + :return: nothing + """ + self.lat = lat + self.lon = lon + self.alt = alt diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py deleted file mode 100644 index 25ce71ac..00000000 --- a/daemon/core/emulator/emudata.py +++ /dev/null @@ -1,206 +0,0 @@ -from dataclasses import dataclass, field -from typing import TYPE_CHECKING, List, Optional - -import netaddr - -from core import utils -from core.emulator.enumerations import LinkTypes - -if TYPE_CHECKING: - from core.nodes.base import CoreNode - - -@dataclass -class NodeOptions: - """ - Options for creating and updating nodes within core. - """ - - name: str = None - model: Optional[str] = "PC" - canvas: int = None - icon: str = None - opaque: str = None - services: List[str] = field(default_factory=list) - config_services: List[str] = field(default_factory=list) - x: float = None - y: float = None - lat: float = None - lon: float = None - alt: float = None - emulation_id: int = None - server: str = None - image: str = None - emane: str = None - - def set_position(self, x: float, y: float) -> None: - """ - Convenience method for setting position. - - :param x: x position - :param y: y position - :return: nothing - """ - self.x = x - self.y = y - - def set_location(self, lat: float, lon: float, alt: float) -> None: - """ - Convenience method for setting location. - - :param lat: latitude - :param lon: longitude - :param alt: altitude - :return: nothing - """ - self.lat = lat - self.lon = lon - self.alt = alt - - -@dataclass -class LinkOptions: - """ - Options for creating and updating links within core. - """ - - type: LinkTypes = LinkTypes.WIRED - session: int = None - delay: int = None - bandwidth: int = None - loss: float = None - dup: int = None - jitter: int = None - mer: int = None - burst: int = None - mburst: int = None - gui_attributes: str = None - unidirectional: bool = None - emulation_id: int = None - network_id: int = None - key: int = None - opaque: str = None - - -@dataclass -class InterfaceData: - """ - Convenience class for storing interface data. - """ - - id: int = None - name: str = None - mac: str = None - ip4: str = None - ip4_mask: int = None - ip6: str = None - ip6_mask: int = None - - def get_addresses(self) -> List[str]: - """ - Returns a list of ip4 and ip6 addresses when present. - - :return: list of addresses - """ - addresses = [] - if self.ip4 and self.ip4_mask: - addresses.append(f"{self.ip4}/{self.ip4_mask}") - if self.ip6 and self.ip6_mask: - addresses.append(f"{self.ip6}/{self.ip6_mask}") - return addresses - - -class IpPrefixes: - """ - Convenience class to help generate IP4 and IP6 addresses for nodes within CORE. - """ - - def __init__(self, ip4_prefix: str = None, ip6_prefix: str = None) -> None: - """ - Creates an IpPrefixes object. - - :param ip4_prefix: ip4 prefix to use for generation - :param ip6_prefix: ip6 prefix to use for generation - :raises ValueError: when both ip4 and ip6 prefixes have not been provided - """ - if not ip4_prefix and not ip6_prefix: - raise ValueError("ip4 or ip6 must be provided") - - self.ip4 = None - if ip4_prefix: - self.ip4 = netaddr.IPNetwork(ip4_prefix) - self.ip6 = None - if ip6_prefix: - self.ip6 = netaddr.IPNetwork(ip6_prefix) - - def ip4_address(self, node_id: int) -> str: - """ - Convenience method to return the IP4 address for a node. - - :param node_id: node id to get IP4 address for - :return: IP4 address or None - """ - if not self.ip4: - raise ValueError("ip4 prefixes have not been set") - return str(self.ip4[node_id]) - - def ip6_address(self, node_id: int) -> str: - """ - Convenience method to return the IP6 address for a node. - - :param node_id: node id to get IP6 address for - :return: IP4 address or None - """ - if not self.ip6: - raise ValueError("ip6 prefixes have not been set") - return str(self.ip6[node_id]) - - def gen_iface(self, node_id: int, name: str = None, mac: str = None): - """ - Creates interface data for linking nodes, using the nodes unique id for - generation, along with a random mac address, unless provided. - - :param node_id: node id to create an interface for - :param name: name to set for interface, default is eth{id} - :param mac: mac address to use for this interface, default is random - generation - :return: new interface data for the provided node - """ - # generate ip4 data - ip4 = None - ip4_mask = None - if self.ip4: - ip4 = self.ip4_address(node_id) - ip4_mask = self.ip4.prefixlen - - # generate ip6 data - ip6 = None - ip6_mask = None - if self.ip6: - ip6 = self.ip6_address(node_id) - ip6_mask = self.ip6.prefixlen - - # random mac - if not mac: - mac = utils.random_mac() - - return InterfaceData( - name=name, ip4=ip4, ip4_mask=ip4_mask, ip6=ip6, ip6_mask=ip6_mask, mac=mac - ) - - def create_iface( - self, node: "CoreNode", name: str = None, mac: str = None - ) -> InterfaceData: - """ - Creates interface data for linking nodes, using the nodes unique id for - generation, along with a random mac address, unless provided. - - :param node: node to create interface for - :param name: name to set for interface, default is eth{id} - :param mac: mac address to use for this interface, default is random - generation - :return: new interface data for the provided node - """ - iface_data = self.gen_iface(node.id, name, mac) - iface_data.id = node.next_iface_id() - return iface_data diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 2dc5ad12..f2514e67 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -22,11 +22,13 @@ from core.emulator.data import ( EventData, ExceptionData, FileData, + InterfaceData, LinkData, + LinkOptions, NodeData, + NodeOptions, ) from core.emulator.distributed import DistributedController -from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.enumerations import ( EventTypes, ExceptionLevels, diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index d56c40aa..91a8baae 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -13,8 +13,7 @@ from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple from core import utils from core.config import ConfigGroup, ConfigurableOptions, Configuration, ModelManager -from core.emulator.data import EventData, LinkData -from core.emulator.emudata import LinkOptions +from core.emulator.data import EventData, LinkData, LinkOptions from core.emulator.enumerations import ( ConfigDataTypes, EventTypes, diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 40aae6a8..3c754aa2 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -14,8 +14,7 @@ import netaddr from core import utils from core.configservice.dependencies import ConfigServiceDependencies from core.constants import MOUNT_BIN, VNODED_BIN -from core.emulator.data import LinkData, NodeData -from core.emulator.emudata import InterfaceData, LinkOptions +from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeData from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes from core.errors import CoreCommandError, CoreError from core.nodes.client import VnodeClient @@ -1096,19 +1095,18 @@ class CoreNetworkBase(NodeBase): if uni: unidirectional = 1 - iface2_ip4 = None - iface2_ip4_mask = None - iface2_ip6 = None - iface2_ip6_mask = None + iface2 = InterfaceData( + id=linked_node.get_iface_id(iface), name=iface.name, mac=iface.hwaddr + ) for address in iface.addrlist: ip, _sep, mask = address.partition("/") mask = int(mask) if netaddr.valid_ipv4(ip): - iface2_ip4 = ip - iface2_ip4_mask = mask + iface2.ip4 = ip + iface2.ip4_mask = mask else: - iface2_ip6 = ip - iface2_ip6_mask = mask + iface2.ip6 = ip + iface2.ip6_mask = mask link_data = LinkData( message_type=flags, @@ -1116,13 +1114,7 @@ class CoreNetworkBase(NodeBase): node2_id=linked_node.id, link_type=self.linktype, unidirectional=unidirectional, - iface2_id=linked_node.get_iface_id(iface), - iface2_name=iface.name, - iface2_mac=iface.hwaddr, - iface2_ip4=iface2_ip4, - iface2_ip4_mask=iface2_ip4_mask, - iface2_ip6=iface2_ip6, - iface2_ip6_mask=iface2_ip6_mask, + iface2=iface2, delay=iface.getparam("delay"), bandwidth=iface.getparam("bw"), dup=iface.getparam("duplicate"), diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 85e3e488..b2f6bbf3 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -11,8 +11,7 @@ import netaddr from core import utils from core.constants import EBTABLES_BIN, TC_BIN -from core.emulator.data import LinkData, NodeData -from core.emulator.emudata import InterfaceData, LinkOptions +from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeData from core.emulator.enumerations import ( LinkTypes, MessageFlags, @@ -894,33 +893,31 @@ class PtpNet(CoreNetwork): if iface1.getparams() != iface2.getparams(): unidirectional = 1 - iface1_ip4 = None - iface1_ip4_mask = None - iface1_ip6 = None - iface1_ip6_mask = None + iface1_data = InterfaceData( + id=iface1.node.get_iface_id(iface1), name=iface1.name, mac=iface1.hwaddr + ) for address in iface1.addrlist: ip, _sep, mask = address.partition("/") mask = int(mask) if netaddr.valid_ipv4(ip): - iface1_ip4 = ip - iface1_ip4_mask = mask + iface1.ip4 = ip + iface1.ip4_mask = mask else: - iface1_ip6 = ip - iface1_ip6_mask = mask + iface1.ip6 = ip + iface1.ip6_mask = mask - iface2_ip4 = None - iface2_ip4_mask = None - iface2_ip6 = None - iface2_ip6_mask = None + iface2_data = InterfaceData( + id=iface2.node.get_iface_id(iface2), name=iface2.name, mac=iface2.hwaddr + ) for address in iface2.addrlist: ip, _sep, mask = address.partition("/") mask = int(mask) if netaddr.valid_ipv4(ip): - iface2_ip4 = ip - iface2_ip4_mask = mask + iface2.ip4 = ip + iface2.ip4_mask = mask else: - iface2_ip6 = ip - iface2_ip6_mask = mask + iface2.ip6 = ip + iface2.ip6_mask = mask link_data = LinkData( message_type=flags, @@ -933,26 +930,16 @@ class PtpNet(CoreNetwork): loss=iface1.getparam("loss"), dup=iface1.getparam("duplicate"), jitter=iface1.getparam("jitter"), - iface1_id=iface1.node.get_iface_id(iface1), - iface1_name=iface1.name, - iface1_mac=iface1.hwaddr, - iface1_ip4=iface1_ip4, - iface1_ip4_mask=iface1_ip4_mask, - iface1_ip6=iface1_ip6, - iface1_ip6_mask=iface1_ip6_mask, - iface2_id=iface2.node.get_iface_id(iface2), - iface2_name=iface2.name, - iface2_mac=iface2.hwaddr, - iface2_ip4=iface2_ip4, - iface2_ip4_mask=iface2_ip4_mask, - iface2_ip6=iface2_ip6, - iface2_ip6_mask=iface2_ip6_mask, + iface1=iface1_data, + iface2=iface2_data, ) all_links.append(link_data) # build a 2nd link message for the upstream link parameters # (swap if1 and if2) if unidirectional: + iface1_data = InterfaceData(id=iface2.node.get_iface_id(iface2)) + iface2_data = InterfaceData(id=iface1.node.get_iface_id(iface1)) link_data = LinkData( message_type=MessageFlags.NONE, link_type=self.linktype, @@ -964,8 +951,8 @@ class PtpNet(CoreNetwork): dup=iface2.getparam("duplicate"), jitter=iface2.getparam("jitter"), unidirectional=1, - iface1_id=iface2.node.get_iface_id(iface2), - iface2_id=iface1.node.get_iface_id(iface1), + iface1=iface1_data, + iface2=iface2_data, ) all_links.append(link_data) return all_links diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 555e0ec9..36bcb267 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -9,8 +9,8 @@ from typing import IO, TYPE_CHECKING, List, Optional, Tuple from core import utils from core.constants import MOUNT_BIN, UMOUNT_BIN +from core.emulator.data import InterfaceData, LinkOptions from core.emulator.distributed import DistributedServer -from core.emulator.emudata import InterfaceData, LinkOptions from core.emulator.enumerations import NodeTypes, TransportType from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNetworkBase, CoreNodeBase diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index fe596d7a..1f92502c 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -6,8 +6,7 @@ from lxml import etree import core.nodes.base import core.nodes.physical from core.emane.nodes import EmaneNet -from core.emulator.data import LinkData -from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions +from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions from core.emulator.enumerations import EventTypes, NodeTypes from core.errors import CoreXmlError from core.nodes.base import CoreNodeBase, NodeBase @@ -482,12 +481,10 @@ class CoreXmlWriter: # add link data for link_data in links: # skip basic range links - if link_data.iface1_id is None and link_data.iface2_id is None: + if link_data.iface1 is None and link_data.iface2 is None: continue - link_element = self.create_link_element(link_data) link_elements.append(link_element) - if link_elements.getchildren(): self.scenario.append(link_elements) @@ -496,36 +493,24 @@ class CoreXmlWriter: self.devices.append(device.element) def create_iface_element( - self, - element_name: str, - node_id: int, - iface_id: int, - mac: str, - ip4: str, - ip4_mask: int, - ip6: str, - ip6_mask: int, + self, element_name: str, node_id: int, iface_data: InterfaceData ) -> etree.Element: - iface = etree.Element(element_name) + iface_element = etree.Element(element_name) node = self.session.get_node(node_id, NodeBase) - iface_name = None if isinstance(node, CoreNodeBase): - node_iface = node.get_iface(iface_id) - iface_name = node_iface.name - + iface = node.get_iface(iface_data.id) # check if emane interface - if isinstance(node_iface.net, EmaneNet): - nem = node_iface.net.getnemid(node_iface) - add_attribute(iface, "nem", nem) - - add_attribute(iface, "id", iface_id) - add_attribute(iface, "name", iface_name) - add_attribute(iface, "mac", mac) - add_attribute(iface, "ip4", ip4) - add_attribute(iface, "ip4_mask", ip4_mask) - add_attribute(iface, "ip6", ip6) - add_attribute(iface, "ip6_mask", ip6_mask) - return iface + if isinstance(iface.net, EmaneNet): + nem = iface.net.getnemid(iface) + add_attribute(iface_element, "nem", nem) + add_attribute(iface_element, "id", iface_data.id) + add_attribute(iface_element, "name", iface_data.name) + add_attribute(iface_element, "mac", iface_data.mac) + add_attribute(iface_element, "ip4", iface_data.ip4) + add_attribute(iface_element, "ip4_mask", iface_data.ip4_mask) + add_attribute(iface_element, "ip6", iface_data.ip6) + add_attribute(iface_element, "ip6_mask", iface_data.ip6_mask) + return iface_element def create_link_element(self, link_data: LinkData) -> etree.Element: link_element = etree.Element("link") @@ -533,30 +518,16 @@ class CoreXmlWriter: add_attribute(link_element, "node2", link_data.node2_id) # check for interface one - if link_data.iface1_id is not None: + if link_data.iface1 is not None: iface1 = self.create_iface_element( - "interface1", - link_data.node1_id, - link_data.iface1_id, - link_data.iface1_mac, - link_data.iface1_ip4, - link_data.iface1_ip4_mask, - link_data.iface1_ip6, - link_data.iface1_ip6_mask, + "interface1", link_data.node1_id, link_data.iface1 ) link_element.append(iface1) # check for interface two - if link_data.iface2_id is not None: + if link_data.iface2 is not None: iface2 = self.create_iface_element( - "interface2", - link_data.node2_id, - link_data.iface2_id, - link_data.iface2_mac, - link_data.iface2_ip4, - link_data.iface2_ip4_mask, - link_data.iface2_ip6, - link_data.iface2_ip6_mask, + "interface2", link_data.node2_id, link_data.iface2 ) link_element.append(iface2) @@ -582,7 +553,6 @@ class CoreXmlWriter: add_attribute(options, "network_id", link_data.network_id) add_attribute(options, "key", link_data.key) add_attribute(options, "opaque", link_data.opaque) - add_attribute(options, "session", link_data.session) if options.items(): link_element.append(options) @@ -969,7 +939,6 @@ class CoreXmlReader: if options.loss is None: options.loss = get_float(options_element, "per") options.unidirectional = get_int(options_element, "unidirectional") - options.session = options_element.get("session") options.emulation_id = get_int(options_element, "emulation_id") options.network_id = get_int(options_element, "network_id") options.opaque = options_element.get("opaque") diff --git a/daemon/examples/configservices/testing.py b/daemon/examples/configservices/testing.py index 767d0f45..9706f2c9 100644 --- a/daemon/examples/configservices/testing.py +++ b/daemon/examples/configservices/testing.py @@ -1,7 +1,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode from core.nodes.network import SwitchNode diff --git a/daemon/examples/docker/docker2core.py b/daemon/examples/docker/docker2core.py index c38f96af..ae7dae79 100644 --- a/daemon/examples/docker/docker2core.py +++ b/daemon/examples/docker/docker2core.py @@ -1,7 +1,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode from core.nodes.docker import DockerNode diff --git a/daemon/examples/docker/docker2docker.py b/daemon/examples/docker/docker2docker.py index 5b62d433..308fd00f 100644 --- a/daemon/examples/docker/docker2docker.py +++ b/daemon/examples/docker/docker2docker.py @@ -1,7 +1,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.docker import DockerNode diff --git a/daemon/examples/docker/switch.py b/daemon/examples/docker/switch.py index 161cd823..fa9e4e40 100644 --- a/daemon/examples/docker/switch.py +++ b/daemon/examples/docker/switch.py @@ -1,7 +1,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode from core.nodes.docker import DockerNode diff --git a/daemon/examples/lxd/lxd2core.py b/daemon/examples/lxd/lxd2core.py index 3d8eef6a..b41520d8 100644 --- a/daemon/examples/lxd/lxd2core.py +++ b/daemon/examples/lxd/lxd2core.py @@ -1,7 +1,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode from core.nodes.lxd import LxcNode diff --git a/daemon/examples/lxd/lxd2lxd.py b/daemon/examples/lxd/lxd2lxd.py index a7209b5c..3a55e2e1 100644 --- a/daemon/examples/lxd/lxd2lxd.py +++ b/daemon/examples/lxd/lxd2lxd.py @@ -1,7 +1,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.lxd import LxcNode diff --git a/daemon/examples/lxd/switch.py b/daemon/examples/lxd/switch.py index 9b6801f5..12767e71 100644 --- a/daemon/examples/lxd/switch.py +++ b/daemon/examples/lxd/switch.py @@ -1,7 +1,7 @@ import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode from core.nodes.lxd import LxcNode diff --git a/daemon/examples/python/distributed_emane.py b/daemon/examples/python/distributed_emane.py index 3ee56108..4421283f 100644 --- a/daemon/examples/python/distributed_emane.py +++ b/daemon/examples/python/distributed_emane.py @@ -9,7 +9,7 @@ import logging from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emane.nodes import EmaneNet from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode diff --git a/daemon/examples/python/distributed_lxd.py b/daemon/examples/python/distributed_lxd.py index 1573836a..26f7caa6 100644 --- a/daemon/examples/python/distributed_lxd.py +++ b/daemon/examples/python/distributed_lxd.py @@ -7,7 +7,7 @@ import argparse import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.lxd import LxcNode diff --git a/daemon/examples/python/distributed_ptp.py b/daemon/examples/python/distributed_ptp.py index 1486c237..fe714e1d 100644 --- a/daemon/examples/python/distributed_ptp.py +++ b/daemon/examples/python/distributed_ptp.py @@ -7,7 +7,7 @@ import argparse import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode diff --git a/daemon/examples/python/distributed_switch.py b/daemon/examples/python/distributed_switch.py index e9eb1e81..35de1cad 100644 --- a/daemon/examples/python/distributed_switch.py +++ b/daemon/examples/python/distributed_switch.py @@ -7,7 +7,7 @@ import argparse import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode from core.nodes.network import SwitchNode diff --git a/daemon/examples/python/emane80211.py b/daemon/examples/python/emane80211.py index 322e569f..9d6def4a 100644 --- a/daemon/examples/python/emane80211.py +++ b/daemon/examples/python/emane80211.py @@ -10,7 +10,7 @@ import time from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emane.nodes import EmaneNet from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode diff --git a/daemon/examples/python/switch.py b/daemon/examples/python/switch.py index 902e79e0..f05176a3 100644 --- a/daemon/examples/python/switch.py +++ b/daemon/examples/python/switch.py @@ -6,7 +6,7 @@ interact with the GUI. import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes +from core.emulator.data import IpPrefixes from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode from core.nodes.network import SwitchNode diff --git a/daemon/examples/python/switch_inject.py b/daemon/examples/python/switch_inject.py index 89f70e05..18a75a49 100644 --- a/daemon/examples/python/switch_inject.py +++ b/daemon/examples/python/switch_inject.py @@ -8,7 +8,7 @@ same CoreEmu instance the GUI is using. import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes +from core.emulator.data import IpPrefixes from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode from core.nodes.network import SwitchNode diff --git a/daemon/examples/python/wlan.py b/daemon/examples/python/wlan.py index 547a5860..de26ab97 100644 --- a/daemon/examples/python/wlan.py +++ b/daemon/examples/python/wlan.py @@ -6,7 +6,7 @@ interact with the GUI. import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes from core.location.mobility import BasicRangeModel from core.nodes.base import CoreNode diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index c3315e7c..be62fc03 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -14,8 +14,8 @@ from core.api.grpc.server import CoreGrpcServer from core.api.tlv.corehandlers import CoreHandler from core.emane.emanemanager import EmaneManager from core.emulator.coreemu import CoreEmu +from core.emulator.data import IpPrefixes from core.emulator.distributed import DistributedServer -from core.emulator.emudata import IpPrefixes from core.emulator.enumerations import EventTypes from core.emulator.session import Session from core.nodes.base import CoreNode diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py index e1c7938b..f51e30b9 100644 --- a/daemon/tests/emane/test_emane.py +++ b/daemon/tests/emane/test_emane.py @@ -15,7 +15,7 @@ from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emane.nodes import EmaneNet from core.emane.rfpipe import EmaneRfPipeModel from core.emane.tdma import EmaneTdmaModel -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.session import Session from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNode diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 5771f7ad..2623b0df 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -8,7 +8,7 @@ from typing import Type import pytest -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.enumerations import MessageFlags from core.emulator.session import Session from core.errors import CoreCommandError diff --git a/daemon/tests/test_distributed.py b/daemon/tests/test_distributed.py index 0f4b1731..01362cae 100644 --- a/daemon/tests/test_distributed.py +++ b/daemon/tests/test_distributed.py @@ -1,4 +1,4 @@ -from core.emulator.emudata import NodeOptions +from core.emulator.data import NodeOptions from core.emulator.session import Session from core.nodes.base import CoreNode from core.nodes.network import HubNode diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 23ff0301..b2a1c312 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -18,8 +18,7 @@ from core.api.tlv.dataconversion import ConfigShim from core.api.tlv.enumerations import ConfigFlags from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emane.nodes import EmaneNet -from core.emulator.data import EventData, NodeData -from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.data import EventData, IpPrefixes, NodeData, NodeOptions from core.emulator.enumerations import EventTypes, ExceptionLevels, NodeTypes from core.errors import CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility diff --git a/daemon/tests/test_links.py b/daemon/tests/test_links.py index fea4f4f8..4078d8bc 100644 --- a/daemon/tests/test_links.py +++ b/daemon/tests/test_links.py @@ -1,6 +1,6 @@ from typing import Tuple -from core.emulator.emudata import IpPrefixes, LinkOptions +from core.emulator.data import IpPrefixes, LinkOptions from core.emulator.session import Session from core.nodes.base import CoreNode from core.nodes.network import SwitchNode diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index d7e435ab..327137d2 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -1,6 +1,6 @@ import pytest -from core.emulator.emudata import InterfaceData, NodeOptions +from core.emulator.data import InterfaceData, NodeOptions from core.emulator.session import Session from core.errors import CoreError from core.nodes.base import CoreNode diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index 55f5a2ab..d81fe471 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -3,7 +3,7 @@ from xml.etree import ElementTree import pytest -from core.emulator.emudata import IpPrefixes, LinkOptions, NodeOptions +from core.emulator.data import IpPrefixes, LinkOptions, NodeOptions from core.emulator.enumerations import EventTypes from core.emulator.session import Session from core.errors import CoreError diff --git a/docs/scripting.md b/docs/scripting.md index 18666a9a..f65d66a3 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -36,7 +36,7 @@ interact with the GUI. import logging from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes +from core.emulator.data import IpPrefixes from core.emulator.enumerations import EventTypes from core.nodes.base import CoreNode from core.nodes.network import SwitchNode From a29a7a558277e9438e288dbeaa1bbd6839bc7dcf Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 16 Jun 2020 14:18:19 -0700 Subject: [PATCH 0360/1131] refactored LinkOptions to be used within LinkData, instead of duplicating data, removed session from LinkOptions and LinkData --- daemon/core/api/grpc/grpcutils.py | 30 ++++++++++++---------- daemon/core/api/tlv/corehandlers.py | 31 +++++++++++----------- daemon/core/emulator/data.py | 25 ++++++------------ daemon/core/nodes/base.py | 20 ++++----------- daemon/core/nodes/interface.py | 29 +++++++++++++++++++++ daemon/core/nodes/network.py | 21 +++++---------- daemon/core/xml/corexml.py | 29 ++++++++++----------- daemon/tests/test_grpc.py | 4 +-- daemon/tests/test_gui.py | 4 +-- daemon/tests/test_xml.py | 40 ++++++++++++++--------------- 10 files changed, 120 insertions(+), 113 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 095c4d0c..5213a835 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -319,6 +319,22 @@ def convert_iface(iface_data: InterfaceData) -> core_pb2.Interface: ) +def convert_link_options(options_data: LinkOptions) -> core_pb2.LinkOptions: + return core_pb2.LinkOptions( + opaque=options_data.opaque, + jitter=options_data.jitter, + key=options_data.key, + mburst=options_data.mburst, + mer=options_data.mer, + loss=options_data.loss, + bandwidth=options_data.bandwidth, + burst=options_data.burst, + delay=options_data.delay, + dup=options_data.dup, + unidirectional=options_data.unidirectional, + ) + + def convert_link(link_data: LinkData) -> core_pb2.Link: """ Convert link_data into core protobuf link. @@ -332,19 +348,7 @@ def convert_link(link_data: LinkData) -> core_pb2.Link: iface2 = None if link_data.iface2 is not None: iface2 = convert_iface(link_data.iface2) - options = core_pb2.LinkOptions( - opaque=link_data.opaque, - jitter=link_data.jitter, - key=link_data.key, - mburst=link_data.mburst, - mer=link_data.mer, - loss=link_data.loss, - bandwidth=link_data.bandwidth, - burst=link_data.burst, - delay=link_data.delay, - dup=link_data.dup, - unidirectional=link_data.unidirectional, - ) + options = convert_link_options(link_data.options) return core_pb2.Link( type=link_data.link_type.value, node1_id=link_data.node1_id, diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 88906e0c..3a4351f1 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -343,12 +343,13 @@ class CoreHandler(socketserver.BaseRequestHandler): :return: nothing """ logging.debug("handling broadcast link: %s", link_data) + options_data = link_data.options loss = "" - if link_data.loss is not None: - loss = str(link_data.loss) + if options_data.loss is not None: + loss = str(options_data.loss) dup = "" - if link_data.dup is not None: - dup = str(link_data.dup) + if options_data.dup is not None: + dup = str(options_data.dup) iface1 = link_data.iface1 if iface1 is None: iface1 = InterfaceData() @@ -361,20 +362,20 @@ class CoreHandler(socketserver.BaseRequestHandler): [ (LinkTlvs.N1_NUMBER, link_data.node1_id), (LinkTlvs.N2_NUMBER, link_data.node2_id), - (LinkTlvs.DELAY, link_data.delay), - (LinkTlvs.BANDWIDTH, link_data.bandwidth), + (LinkTlvs.DELAY, options_data.delay), + (LinkTlvs.BANDWIDTH, options_data.bandwidth), (LinkTlvs.LOSS, loss), (LinkTlvs.DUP, dup), - (LinkTlvs.JITTER, link_data.jitter), - (LinkTlvs.MER, link_data.mer), - (LinkTlvs.BURST, link_data.burst), - (LinkTlvs.MBURST, link_data.mburst), + (LinkTlvs.JITTER, options_data.jitter), + (LinkTlvs.MER, options_data.mer), + (LinkTlvs.BURST, options_data.burst), + (LinkTlvs.MBURST, options_data.mburst), (LinkTlvs.TYPE, link_data.link_type.value), - (LinkTlvs.GUI_ATTRIBUTES, link_data.gui_attributes), - (LinkTlvs.UNIDIRECTIONAL, link_data.unidirectional), - (LinkTlvs.EMULATION_ID, link_data.emulation_id), + (LinkTlvs.GUI_ATTRIBUTES, options_data.gui_attributes), + (LinkTlvs.UNIDIRECTIONAL, options_data.unidirectional), + (LinkTlvs.EMULATION_ID, options_data.emulation_id), (LinkTlvs.NETWORK_ID, link_data.network_id), - (LinkTlvs.KEY, link_data.key), + (LinkTlvs.KEY, options_data.key), (LinkTlvs.IFACE1_NUMBER, iface1.id), (LinkTlvs.IFACE1_IP4, iface1.ip4), (LinkTlvs.IFACE1_IP4_MASK, iface1.ip4_mask), @@ -387,7 +388,7 @@ class CoreHandler(socketserver.BaseRequestHandler): (LinkTlvs.IFACE2_MAC, iface2.mac), (LinkTlvs.IFACE2_IP6, iface2.ip6), (LinkTlvs.IFACE2_IP6_MASK, iface2.ip6_mask), - (LinkTlvs.OPAQUE, link_data.opaque), + (LinkTlvs.OPAQUE, options_data.opaque), ], ) diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index c08a70f0..0c263135 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -142,36 +142,27 @@ class LinkOptions: burst: int = None mburst: int = None gui_attributes: str = None - unidirectional: bool = None + unidirectional: int = None emulation_id: int = None - network_id: int = None key: int = None opaque: str = None @dataclass class LinkData: + """ + Represents all data associated with a link. + """ + message_type: MessageFlags = None + link_type: LinkTypes = None label: str = None node1_id: int = None node2_id: int = None - delay: float = None - bandwidth: float = None - loss: float = None - dup: float = None - jitter: float = None - mer: float = None - burst: float = None - mburst: float = None - link_type: LinkTypes = None - gui_attributes: str = None - unidirectional: int = None - emulation_id: int = None - network_id: int = None - key: int = None iface1: InterfaceData = None iface2: InterfaceData = None - opaque: str = None + options: LinkOptions = LinkOptions() + network_id: int = None color: str = None diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 3c754aa2..a6e4f147 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -224,9 +224,7 @@ class NodeBase(abc.ABC): def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ - Build CORE Link data for this object. There is no default - method for PyCoreObjs as PyCoreNodes do not implement this but - PyCoreNets do. + Build link data for this node. :param flags: message flags :return: list of link data @@ -1108,35 +1106,27 @@ class CoreNetworkBase(NodeBase): iface2.ip6 = ip iface2.ip6_mask = mask + options_data = iface.get_link_options(unidirectional) link_data = LinkData( message_type=flags, node1_id=self.id, node2_id=linked_node.id, link_type=self.linktype, - unidirectional=unidirectional, iface2=iface2, - delay=iface.getparam("delay"), - bandwidth=iface.getparam("bw"), - dup=iface.getparam("duplicate"), - jitter=iface.getparam("jitter"), - loss=iface.getparam("loss"), + options=options_data, ) all_links.append(link_data) if not uni: continue iface.swapparams("_params_up") + options_data = iface.get_link_options(unidirectional) link_data = LinkData( message_type=MessageFlags.NONE, node1_id=linked_node.id, node2_id=self.id, link_type=self.linktype, - unidirectional=1, - delay=iface.getparam("delay"), - bandwidth=iface.getparam("bw"), - dup=iface.getparam("duplicate"), - jitter=iface.getparam("jitter"), - loss=iface.getparam("loss"), + options=options_data, ) iface.swapparams("_params_up") all_links.append(link_data) diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index dc16517f..1fb8b894 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -7,6 +7,7 @@ import time from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple from core import utils +from core.emulator.data import LinkOptions from core.emulator.enumerations import MessageFlags, TransportType from core.errors import CoreCommandError from core.nodes.netclient import LinuxNetClient, get_net_client @@ -169,6 +170,34 @@ class CoreInterface: """ return self._params.get(key) + def get_link_options(self, unidirectional: int) -> LinkOptions: + """ + Get currently set params as link options. + + :param unidirectional: unidirectional setting + :return: link options + """ + delay = self.getparam("delay") + if delay is not None: + delay = int(delay) + bandwidth = self.getparam("bw") + if bandwidth is not None: + bandwidth = int(bandwidth) + dup = self.getparam("duplicate") + if dup is not None: + dup = int(dup) + jitter = self.getparam("jitter") + if jitter is not None: + jitter = int(jitter) + return LinkOptions( + delay=delay, + bandwidth=bandwidth, + dup=dup, + jitter=jitter, + loss=self.getparam("loss"), + unidirectional=unidirectional, + ) + def getparams(self) -> List[Tuple[str, float]]: """ Return (key, value) pairs for parameters. diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index b2f6bbf3..972d54f9 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -884,11 +884,12 @@ class PtpNet(CoreNetwork): :return: list of link data """ all_links = [] - if len(self.ifaces) != 2: return all_links - iface1, iface2 = self.get_ifaces() + ifaces = self.get_ifaces() + iface1 = ifaces[0] + iface2 = ifaces[1] unidirectional = 0 if iface1.getparams() != iface2.getparams(): unidirectional = 1 @@ -919,19 +920,15 @@ class PtpNet(CoreNetwork): iface2.ip6 = ip iface2.ip6_mask = mask + options_data = iface1.get_link_options(unidirectional) link_data = LinkData( message_type=flags, node1_id=iface1.node.id, node2_id=iface2.node.id, link_type=self.linktype, - unidirectional=unidirectional, - delay=iface1.getparam("delay"), - bandwidth=iface1.getparam("bw"), - loss=iface1.getparam("loss"), - dup=iface1.getparam("duplicate"), - jitter=iface1.getparam("jitter"), iface1=iface1_data, iface2=iface2_data, + options=options_data, ) all_links.append(link_data) @@ -940,19 +937,15 @@ class PtpNet(CoreNetwork): if unidirectional: iface1_data = InterfaceData(id=iface2.node.get_iface_id(iface2)) iface2_data = InterfaceData(id=iface1.node.get_iface_id(iface1)) + options_data = iface2.get_link_options(unidirectional) link_data = LinkData( message_type=MessageFlags.NONE, link_type=self.linktype, node1_id=iface2.node.id, node2_id=iface1.node.id, - delay=iface2.getparam("delay"), - bandwidth=iface2.getparam("bw"), - loss=iface2.getparam("loss"), - dup=iface2.getparam("duplicate"), - jitter=iface2.getparam("jitter"), - unidirectional=1, iface1=iface1_data, iface2=iface2_data, + options=options_data, ) all_links.append(link_data) return all_links diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 1f92502c..4febe71f 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -537,22 +537,22 @@ class CoreXmlWriter: is_node1_wireless = isinstance(node1, (WlanNode, EmaneNet)) is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet)) if not any([is_node1_wireless, is_node2_wireless]): + options_data = link_data.options options = etree.Element("options") - add_attribute(options, "delay", link_data.delay) - add_attribute(options, "bandwidth", link_data.bandwidth) - add_attribute(options, "loss", link_data.loss) - add_attribute(options, "dup", link_data.dup) - add_attribute(options, "jitter", link_data.jitter) - add_attribute(options, "mer", link_data.mer) - add_attribute(options, "burst", link_data.burst) - add_attribute(options, "mburst", link_data.mburst) - add_attribute(options, "type", link_data.link_type) - add_attribute(options, "gui_attributes", link_data.gui_attributes) - add_attribute(options, "unidirectional", link_data.unidirectional) - add_attribute(options, "emulation_id", link_data.emulation_id) + add_attribute(options, "delay", options_data.delay) + add_attribute(options, "bandwidth", options_data.bandwidth) + add_attribute(options, "loss", options_data.loss) + add_attribute(options, "dup", options_data.dup) + add_attribute(options, "jitter", options_data.jitter) + add_attribute(options, "mer", options_data.mer) + add_attribute(options, "burst", options_data.burst) + add_attribute(options, "mburst", options_data.mburst) + add_attribute(options, "gui_attributes", options_data.gui_attributes) + add_attribute(options, "unidirectional", options_data.unidirectional) + add_attribute(options, "emulation_id", options_data.emulation_id) add_attribute(options, "network_id", link_data.network_id) - add_attribute(options, "key", link_data.key) - add_attribute(options, "opaque", link_data.opaque) + add_attribute(options, "key", options_data.key) + add_attribute(options, "opaque", options_data.opaque) if options.items(): link_element.append(options) @@ -940,7 +940,6 @@ class CoreXmlReader: options.loss = get_float(options_element, "per") options.unidirectional = get_int(options_element, "unidirectional") options.emulation_id = get_int(options_element, "emulation_id") - options.network_id = get_int(options_element, "network_id") options.opaque = options_element.get("opaque") options.gui_attributes = options_element.get("gui_attributes") diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index b2a1c312..cff7cd85 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -590,7 +590,7 @@ class TestGrpc: session.add_link(node.id, switch.id, iface) options = core_pb2.LinkOptions(bandwidth=30000) link = switch.all_link_data()[0] - assert options.bandwidth != link.bandwidth + assert options.bandwidth != link.options.bandwidth # then with client.context_connect(): @@ -601,7 +601,7 @@ class TestGrpc: # then assert response.result is True link = switch.all_link_data()[0] - assert options.bandwidth == link.bandwidth + assert options.bandwidth == link.options.bandwidth def test_delete_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes): # given diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index c413295a..8f01a2bf 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -201,7 +201,7 @@ class TestGui: all_links = switch_node.all_link_data() assert len(all_links) == 1 link = all_links[0] - assert link.bandwidth is None + assert link.options.bandwidth is None bandwidth = 50000 message = coreapi.CoreLinkMessage.create( @@ -219,7 +219,7 @@ class TestGui: all_links = switch_node.all_link_data() assert len(all_links) == 1 link = all_links[0] - assert link.bandwidth == bandwidth + assert link.options.bandwidth == bandwidth def test_link_delete_node_to_node(self, coretlv: CoreHandler): node1_id = 1 diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index d81fe471..91b598f3 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -347,11 +347,11 @@ class TestXml: node = session.nodes[node_id] links += node.all_link_data() link = links[0] - assert options.loss == link.loss - assert options.bandwidth == link.bandwidth - assert options.jitter == link.jitter - assert options.delay == link.delay - assert options.dup == link.dup + assert options.loss == link.options.loss + assert options.bandwidth == link.options.bandwidth + assert options.jitter == link.options.jitter + assert options.delay == link.options.delay + assert options.dup == link.options.dup def test_link_options_ptp( self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes @@ -414,11 +414,11 @@ class TestXml: node = session.nodes[node_id] links += node.all_link_data() link = links[0] - assert options.loss == link.loss - assert options.bandwidth == link.bandwidth - assert options.jitter == link.jitter - assert options.delay == link.delay - assert options.dup == link.dup + assert options.loss == link.options.loss + assert options.bandwidth == link.options.bandwidth + assert options.jitter == link.options.jitter + assert options.delay == link.options.delay + assert options.dup == link.options.dup def test_link_options_bidirectional( self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes @@ -494,13 +494,13 @@ class TestXml: assert len(links) == 2 link1 = links[0] link2 = links[1] - assert options1.bandwidth == link1.bandwidth - assert options1.delay == link1.delay - assert options1.loss == link1.loss - assert options1.dup == link1.dup - assert options1.jitter == link1.jitter - assert options2.bandwidth == link2.bandwidth - assert options2.delay == link2.delay - assert options2.loss == link2.loss - assert options2.dup == link2.dup - assert options2.jitter == link2.jitter + assert options1.bandwidth == link1.options.bandwidth + assert options1.delay == link1.options.delay + assert options1.loss == link1.options.loss + assert options1.dup == link1.options.dup + assert options1.jitter == link1.options.jitter + assert 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 From 351b99aae003b3852240dd9effb825677252574d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 16 Jun 2020 21:53:12 -0700 Subject: [PATCH 0361/1131] daemon: renamed LinkData.link_type to LinkData.type and removed LinkOptions.type to remove redundant information, link_type param added to session.add_link, delete_link, and update_link functions --- daemon/core/api/grpc/grpcutils.py | 16 ++++++++-------- daemon/core/api/grpc/server.py | 8 +++++--- daemon/core/api/tlv/corehandlers.py | 14 +++++++++----- daemon/core/emane/emanemanager.py | 2 +- daemon/core/emane/linkmonitor.py | 2 +- daemon/core/emulator/data.py | 3 +-- daemon/core/emulator/session.py | 10 +++++++--- daemon/core/location/mobility.py | 2 +- daemon/core/nodes/base.py | 4 ++-- daemon/core/nodes/network.py | 4 ++-- 10 files changed, 37 insertions(+), 28 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 5213a835..a8e0a792 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -78,7 +78,7 @@ def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData: def add_link_data( link_proto: core_pb2.Link -) -> Tuple[InterfaceData, InterfaceData, LinkOptions]: +) -> Tuple[InterfaceData, InterfaceData, LinkOptions, LinkTypes]: """ Convert link proto to link interfaces and options data. @@ -88,7 +88,7 @@ def add_link_data( iface1_data = link_iface(link_proto.iface1) iface2_data = link_iface(link_proto.iface2) link_type = LinkTypes(link_proto.type) - options = LinkOptions(type=link_type) + options = LinkOptions() options_data = link_proto.options if options_data: options.delay = options_data.delay @@ -102,7 +102,7 @@ def add_link_data( options.unidirectional = options_data.unidirectional options.key = options_data.key options.opaque = options_data.opaque - return iface1_data, iface2_data, options + return iface1_data, iface2_data, options, link_type def create_nodes( @@ -142,8 +142,8 @@ def create_links( for link_proto in link_protos: node1_id = link_proto.node1_id node2_id = link_proto.node2_id - iface1, iface2, options = add_link_data(link_proto) - args = (node1_id, node2_id, iface1, iface2, options) + iface1, iface2, options, link_type = add_link_data(link_proto) + args = (node1_id, node2_id, iface1, iface2, options, link_type) funcs.append((session.add_link, args, {})) start = time.monotonic() results, exceptions = utils.threadpool(funcs) @@ -166,8 +166,8 @@ def edit_links( for link_proto in link_protos: node1_id = link_proto.node1_id node2_id = link_proto.node2_id - iface1, iface2, options = add_link_data(link_proto) - args = (node1_id, node2_id, iface1.id, iface2.id, options) + iface1, iface2, options, link_type = add_link_data(link_proto) + args = (node1_id, node2_id, iface1.id, iface2.id, options, link_type) funcs.append((session.update_link, args, {})) start = time.monotonic() results, exceptions = utils.threadpool(funcs) @@ -350,7 +350,7 @@ def convert_link(link_data: LinkData) -> core_pb2.Link: iface2 = convert_iface(link_data.iface2) options = convert_link_options(link_data.options) return core_pb2.Link( - type=link_data.link_type.value, + type=link_data.type.value, node1_id=link_data.node1_id, node2_id=link_data.node2_id, iface1=iface1, diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 1be60116..b9e0e0aa 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -847,9 +847,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): node2_id = request.link.node2_id self.get_node(session, node1_id, context, NodeBase) self.get_node(session, node2_id, context, NodeBase) - iface1_data, iface2_data, options = grpcutils.add_link_data(request.link) + iface1_data, iface2_data, options, link_type = grpcutils.add_link_data( + request.link + ) node1_iface, node2_iface = session.add_link( - node1_id, node2_id, iface1_data, iface2_data, options=options + node1_id, node2_id, iface1_data, iface2_data, options, link_type ) iface1_proto = None iface2_proto = None @@ -1522,7 +1524,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): color = session.get_link_color(emane1.id) link = LinkData( message_type=flag, - link_type=LinkTypes.WIRELESS, + type=LinkTypes.WIRELESS, node1_id=node1.id, node2_id=node2.id, network_id=emane1.id, diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 3a4351f1..631cd538 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -370,7 +370,7 @@ class CoreHandler(socketserver.BaseRequestHandler): (LinkTlvs.MER, options_data.mer), (LinkTlvs.BURST, options_data.burst), (LinkTlvs.MBURST, options_data.mburst), - (LinkTlvs.TYPE, link_data.link_type.value), + (LinkTlvs.TYPE, link_data.type.value), (LinkTlvs.GUI_ATTRIBUTES, options_data.gui_attributes), (LinkTlvs.UNIDIRECTIONAL, options_data.unidirectional), (LinkTlvs.EMULATION_ID, options_data.emulation_id), @@ -784,7 +784,7 @@ class CoreHandler(socketserver.BaseRequestHandler): link_type_value = message.get_tlv(LinkTlvs.TYPE.value) if link_type_value is not None: link_type = LinkTypes(link_type_value) - options = LinkOptions(type=link_type) + 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) @@ -801,12 +801,16 @@ class CoreHandler(socketserver.BaseRequestHandler): options.opaque = message.get_tlv(LinkTlvs.OPAQUE.value) if message.flags & MessageFlags.ADD.value: - self.session.add_link(node1_id, node2_id, iface1_data, iface2_data, options) + self.session.add_link( + node1_id, node2_id, iface1_data, iface2_data, options, link_type + ) elif message.flags & MessageFlags.DELETE.value: - self.session.delete_link(node1_id, node2_id, iface1_data.id, iface2_data.id) + self.session.delete_link( + node1_id, node2_id, iface1_data.id, iface2_data.id, link_type + ) else: self.session.update_link( - node1_id, node2_id, iface1_data.id, iface2_data.id, options + node1_id, node2_id, iface1_data.id, iface2_data.id, options, link_type ) return () diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 58b85080..fc561b5f 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -500,10 +500,10 @@ class EmaneManager(ModelManager): color = self.session.get_link_color(emane1.id) return LinkData( message_type=flags, + type=LinkTypes.WIRELESS, node1_id=node1.id, node2_id=node2.id, network_id=emane1.id, - link_type=LinkTypes.WIRELESS, color=color, ) diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py index 097080c3..1a9ac41a 100644 --- a/daemon/core/emane/linkmonitor.py +++ b/daemon/core/emane/linkmonitor.py @@ -305,11 +305,11 @@ class EmaneLinkMonitor: color = self.emane_manager.session.get_link_color(emane_id) link_data = LinkData( message_type=message_type, + type=LinkTypes.WIRELESS, label=label, node1_id=node1, node2_id=node2, network_id=emane_id, - link_type=LinkTypes.WIRELESS, color=color, ) self.emane_manager.session.broadcast_link(link_data) diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index 0c263135..899d32ae 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -132,7 +132,6 @@ class LinkOptions: Options for creating and updating links within core. """ - type: LinkTypes = LinkTypes.WIRED delay: int = None bandwidth: int = None loss: float = None @@ -155,7 +154,7 @@ class LinkData: """ message_type: MessageFlags = None - link_type: LinkTypes = None + type: LinkTypes = LinkTypes.WIRED label: str = None node1_id: int = None node2_id: int = None diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index f2514e67..814c89d9 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -224,6 +224,7 @@ class Session: iface1_data: InterfaceData = None, iface2_data: InterfaceData = None, options: LinkOptions = None, + link_type: LinkTypes = LinkTypes.WIRED, ) -> Tuple[CoreInterface, CoreInterface]: """ Add a link between nodes. @@ -236,6 +237,7 @@ class Session: data, defaults to none :param options: data for creating link, defaults to no options + :param link_type: type of link to add :return: tuple of created core interfaces, depending on link """ if not options: @@ -246,7 +248,7 @@ class Session: iface2 = None # wireless link - if options.type == LinkTypes.WIRELESS: + if link_type == LinkTypes.WIRELESS: if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): self._link_wireless(node1, node2, connect=True) else: @@ -371,6 +373,7 @@ class Session: iface1_id: int = None, iface2_id: int = None, options: LinkOptions = None, + link_type: LinkTypes = LinkTypes.WIRED, ) -> None: """ Update link information between nodes. @@ -380,6 +383,7 @@ class Session: :param iface1_id: interface id for node one :param iface2_id: interface id for node two :param options: data to update link with + :param link_type: type of link to update :return: nothing :raises core.CoreError: when updating a wireless type link, when there is a unknown link between networks @@ -390,7 +394,7 @@ class Session: node2 = self.get_node(node2_id, NodeBase) logging.info( "update link(%s) node(%s):interface(%s) node(%s):interface(%s)", - options.type.name, + link_type.name, node1.name, iface1_id, node2.name, @@ -398,7 +402,7 @@ class Session: ) # wireless link - if options.type == LinkTypes.WIRELESS: + if link_type == LinkTypes.WIRELESS: raise CoreError("cannot update wireless link") else: if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 91a8baae..9bb2966e 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -487,10 +487,10 @@ class BasicRangeModel(WirelessModel): color = self.session.get_link_color(self.wlan.id) return LinkData( message_type=message_type, + type=LinkTypes.WIRELESS, node1_id=iface1.node.id, node2_id=iface2.node.id, network_id=self.wlan.id, - link_type=LinkTypes.WIRELESS, color=color, ) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index a6e4f147..97164cb6 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -1109,9 +1109,9 @@ class CoreNetworkBase(NodeBase): options_data = iface.get_link_options(unidirectional) link_data = LinkData( message_type=flags, + type=self.linktype, node1_id=self.id, node2_id=linked_node.id, - link_type=self.linktype, iface2=iface2, options=options_data, ) @@ -1123,9 +1123,9 @@ class CoreNetworkBase(NodeBase): options_data = iface.get_link_options(unidirectional) link_data = LinkData( message_type=MessageFlags.NONE, + type=self.linktype, node1_id=linked_node.id, node2_id=self.id, - link_type=self.linktype, options=options_data, ) iface.swapparams("_params_up") diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 972d54f9..04d4e8f8 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -923,9 +923,9 @@ class PtpNet(CoreNetwork): options_data = iface1.get_link_options(unidirectional) link_data = LinkData( message_type=flags, + type=self.linktype, node1_id=iface1.node.id, node2_id=iface2.node.id, - link_type=self.linktype, iface1=iface1_data, iface2=iface2_data, options=options_data, @@ -940,7 +940,7 @@ class PtpNet(CoreNetwork): options_data = iface2.get_link_options(unidirectional) link_data = LinkData( message_type=MessageFlags.NONE, - link_type=self.linktype, + type=self.linktype, node1_id=iface2.node.id, node2_id=iface1.node.id, iface1=iface1_data, From a1734c3bc0cb8d4eaa17c414db72a3835d464ab7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 16 Jun 2020 22:05:36 -0700 Subject: [PATCH 0362/1131] grpc: updated Interface proto fields to be more consistent with code, ip4mask to ip4_mask, ip6mask to ip6_mask, netid to net_id, flowid to flow_id --- daemon/core/api/grpc/client.py | 4 ++-- daemon/core/api/grpc/grpcutils.py | 26 +++++++++++++------------- daemon/core/gui/coreclient.py | 7 ++++++- daemon/core/gui/dialogs/nodeconfig.py | 20 ++++++++++---------- daemon/core/gui/graph/edges.py | 4 ++-- daemon/core/gui/interface.py | 6 +++--- daemon/proto/core/api/grpc/core.proto | 8 ++++---- 7 files changed, 40 insertions(+), 35 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 68bfc502..db908e05 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -127,9 +127,9 @@ class InterfaceHelper: id=iface_id, name=iface_data.name, ip4=iface_data.ip4, - ip4mask=iface_data.ip4_mask, + ip4_mask=iface_data.ip4_mask, ip6=iface_data.ip6, - ip6mask=iface_data.ip6_mask, + ip6_mask=iface_data.ip6_mask, mac=iface_data.mac, ) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index a8e0a792..9d26e4cf 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -69,9 +69,9 @@ def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData: name=name, mac=mac, ip4=ip4, - ip4_mask=iface_proto.ip4mask, + ip4_mask=iface_proto.ip4_mask, ip6=ip6, - ip6_mask=iface_proto.ip6mask, + ip6_mask=iface_proto.ip6_mask, ) return iface_data @@ -313,9 +313,9 @@ def convert_iface(iface_data: InterfaceData) -> core_pb2.Interface: name=iface_data.name, mac=iface_data.mac, ip4=iface_data.ip4, - ip4mask=iface_data.ip4_mask, + ip4_mask=iface_data.ip4_mask, ip6=iface_data.ip6, - ip6mask=iface_data.ip6_mask, + ip6_mask=iface_data.ip6_mask, ) @@ -449,30 +449,30 @@ def iface_to_proto(iface: CoreInterface) -> core_pb2.Interface: if iface.net: net_id = iface.net.id ip4 = None - ip4mask = None + ip4_mask = None ip6 = None - ip6mask = None + ip6_mask = None for addr in iface.addrlist: network = netaddr.IPNetwork(addr) mask = network.prefixlen ip = str(network.ip) if netaddr.valid_ipv4(ip) and not ip4: ip4 = ip - ip4mask = mask + ip4_mask = mask elif netaddr.valid_ipv6(ip) and not ip6: ip6 = ip - ip6mask = mask + ip6_mask = mask return core_pb2.Interface( id=iface.node_id, - netid=net_id, + net_id=net_id, name=iface.name, - mac=str(iface.hwaddr), + mac=iface.hwaddr, mtu=iface.mtu, - flowid=iface.flow_id, + flow_id=iface.flow_id, ip4=ip4, - ip4mask=ip4mask, + ip4_mask=ip4_mask, ip6=ip6, - ip6mask=ip6mask, + ip6_mask=ip6_mask, ) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 3be58e17..8b0c423c 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -834,7 +834,12 @@ class CoreClient: iface_id = canvas_node.next_iface_id() name = f"eth{iface_id}" iface = core_pb2.Interface( - id=iface_id, name=name, ip4=ip4, ip4mask=ip4_mask, ip6=ip6, ip6mask=ip6_mask + id=iface_id, + name=name, + ip4=ip4, + ip4_mask=ip4_mask, + ip6=ip6, + ip6_mask=ip6_mask, ) logging.info( "create node(%s) interface(%s) IPv4(%s) IPv6(%s)", diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index 29ce2010..cec9e9f9 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -248,7 +248,7 @@ class NodeConfigDialog(Dialog): label.grid(row=row, column=0, padx=PADX, pady=PADY) ip4_net = "" if iface.ip4: - ip4_net = f"{iface.ip4}/{iface.ip4mask}" + ip4_net = f"{iface.ip4}/{iface.ip4_mask}" ip4 = tk.StringVar(value=ip4_net) entry = ttk.Entry(tab, textvariable=ip4, state=state) entry.grid(row=row, column=1, columnspan=2, sticky="ew") @@ -258,7 +258,7 @@ class NodeConfigDialog(Dialog): label.grid(row=row, column=0, padx=PADX, pady=PADY) ip6_net = "" if iface.ip6: - ip6_net = f"{iface.ip6}/{iface.ip6mask}" + ip6_net = f"{iface.ip6}/{iface.ip6_mask}" ip6 = tk.StringVar(value=ip6_net) entry = ttk.Entry(tab, textvariable=ip6, state=state) entry.grid(row=row, column=1, columnspan=2, sticky="ew") @@ -318,12 +318,12 @@ class NodeConfigDialog(Dialog): error = True break if ip4_net: - ip4, ip4mask = ip4_net.split("/") - ip4mask = int(ip4mask) + ip4, ip4_mask = ip4_net.split("/") + ip4_mask = int(ip4_mask) else: - ip4, ip4mask = "", 0 + ip4, ip4_mask = "", 0 iface.ip4 = ip4 - iface.ip4mask = ip4mask + iface.ip4_mask = ip4_mask # validate ip6 ip6_net = data.ip6.get() @@ -331,12 +331,12 @@ class NodeConfigDialog(Dialog): error = True break if ip6_net: - ip6, ip6mask = ip6_net.split("/") - ip6mask = int(ip6mask) + ip6, ip6_mask = ip6_net.split("/") + ip6_mask = int(ip6_mask) else: - ip6, ip6mask = "", 0 + ip6, ip6_mask = "", 0 iface.ip6 = ip6 - iface.ip6mask = ip6mask + iface.ip6_mask = ip6_mask mac = data.mac.get() auto_mac = data.is_auto.get() diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 152e1a2f..ac637b28 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -289,10 +289,10 @@ class CanvasEdge(Edge): label = f"{iface.name}" if iface.ip4 and self.canvas.show_ip4s.get(): label = f"{label}\n" if label else "" - label += f"{iface.ip4}/{iface.ip4mask}" + label += f"{iface.ip4}/{iface.ip4_mask}" if iface.ip6 and self.canvas.show_ip6s.get(): label = f"{label}\n" if label else "" - label += f"{iface.ip6}/{iface.ip6mask}" + label += f"{iface.ip6}/{iface.ip6_mask}" return label def create_node_labels(self) -> Tuple[str, str]: diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index 14cba024..6c82ca51 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: def get_index(iface: "core_pb2.Interface") -> Optional[int]: if not iface.ip4: return None - net = netaddr.IPNetwork(f"{iface.ip4}/{iface.ip4mask}") + net = netaddr.IPNetwork(f"{iface.ip4}/{iface.ip4_mask}") ip_value = net.value cidr_value = net.cidr.value return ip_value - cidr_value @@ -153,10 +153,10 @@ class InterfaceManager: def get_subnets(self, iface: "core_pb2.Interface") -> Subnets: ip4_subnet = self.ip4_subnets if iface.ip4: - ip4_subnet = IPNetwork(f"{iface.ip4}/{iface.ip4mask}").cidr + ip4_subnet = IPNetwork(f"{iface.ip4}/{iface.ip4_mask}").cidr ip6_subnet = self.ip6_subnets if iface.ip6: - ip6_subnet = IPNetwork(f"{iface.ip6}/{iface.ip6mask}").cidr + ip6_subnet = IPNetwork(f"{iface.ip6}/{iface.ip6_mask}").cidr subnets = Subnets(ip4_subnet, ip6_subnet) return self.used_subnets.get(subnets.key(), subnets) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index f691621a..2819c5ea 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -732,11 +732,11 @@ message Interface { string name = 2; string mac = 3; string ip4 = 4; - int32 ip4mask = 5; + int32 ip4_mask = 5; string ip6 = 6; - int32 ip6mask = 7; - int32 netid = 8; - int32 flowid = 9; + int32 ip6_mask = 7; + int32 net_id = 8; + int32 flow_id = 9; int32 mtu = 10; } From f4671ab2b894c82693484e5f2bcb52a9c707529e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 16 Jun 2020 23:25:26 -0700 Subject: [PATCH 0363/1131] daemon: refactored usages of hwaddr to mac and be consistent everywhere --- daemon/core/api/grpc/grpcutils.py | 2 +- daemon/core/nodes/base.py | 22 +++++++++++----------- daemon/core/nodes/interface.py | 14 +++++++------- daemon/core/nodes/network.py | 4 ++-- daemon/core/nodes/physical.py | 18 +++++++++--------- daemon/core/services/xorp.py | 4 ++-- daemon/core/xml/emanexml.py | 6 +++--- daemon/tests/test_nodes.py | 10 +++++----- 8 files changed, 40 insertions(+), 40 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 9d26e4cf..6f2911a4 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -466,7 +466,7 @@ def iface_to_proto(iface: CoreInterface) -> core_pb2.Interface: id=iface.node_id, net_id=net_id, name=iface.name, - mac=iface.hwaddr, + mac=iface.mac, mtu=iface.mtu, flow_id=iface.flow_id, ip4=ip4, diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 97164cb6..97da63a4 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -731,9 +731,9 @@ class CoreNode(CoreNodeBase): flow_id = self.node_net_client.get_ifindex(veth.name) veth.flow_id = int(flow_id) logging.debug("interface flow index: %s - %s", veth.name, veth.flow_id) - hwaddr = self.node_net_client.get_mac(veth.name) - logging.debug("interface mac: %s - %s", veth.name, hwaddr) - veth.sethwaddr(hwaddr) + mac = self.node_net_client.get_mac(veth.name) + logging.debug("interface mac: %s - %s", veth.name, mac) + veth.set_mac(mac) try: # add network interface to the node. If unsuccessful, destroy the @@ -775,20 +775,20 @@ class CoreNode(CoreNodeBase): return iface_id - def sethwaddr(self, iface_id: int, addr: str) -> None: + def set_mac(self, iface_id: int, mac: str) -> None: """ Set hardware address for an interface. :param iface_id: id of interface to set hardware address for - :param addr: hardware address to set + :param mac: mac address to set :return: nothing :raises CoreCommandError: when a non-zero exit status occurs """ - addr = utils.validate_mac(addr) + mac = utils.validate_mac(mac) iface = self.get_iface(iface_id) - iface.sethwaddr(addr) + iface.set_mac(mac) if self.up: - self.node_net_client.device_mac(iface.name, addr) + self.node_net_client.device_mac(iface.name, mac) def addaddr(self, iface_id: int, addr: str) -> None: """ @@ -857,14 +857,14 @@ class CoreNode(CoreNodeBase): # save addresses with the interface now self.attachnet(iface_id, net) iface = self.get_iface(iface_id) - iface.sethwaddr(iface_data.mac) + iface.set_mac(iface_data.mac) for address in addresses: iface.addaddr(address) else: iface_id = self.newveth(iface_data.id, iface_data.name) self.attachnet(iface_id, net) if iface_data.mac: - self.sethwaddr(iface_id, iface_data.mac) + self.set_mac(iface_id, iface_data.mac) for address in addresses: self.addaddr(iface_id, address) self.ifup(iface_id) @@ -1094,7 +1094,7 @@ class CoreNetworkBase(NodeBase): unidirectional = 1 iface2 = InterfaceData( - id=linked_node.get_iface_id(iface), name=iface.name, mac=iface.hwaddr + id=linked_node.get_iface_id(iface), name=iface.name, mac=iface.mac ) for address in iface.addrlist: ip, _sep, mask = address.partition("/") diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 1fb8b894..287723a7 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -53,7 +53,7 @@ class CoreInterface: self.othernet: Optional[CoreNetworkBase] = None self._params: Dict[str, float] = {} self.addrlist: List[str] = [] - self.hwaddr: Optional[str] = None + self.mac: Optional[str] = None # placeholder position hook self.poshook: Callable[[CoreInterface], None] = lambda x: None # used with EMANE @@ -150,16 +150,16 @@ class CoreInterface: """ self.addrlist.remove(addr) - def sethwaddr(self, addr: str) -> None: + def set_mac(self, mac: str) -> None: """ - Set hardware address. + Set mac address. - :param addr: hardware address to set to. + :param mac: mac address to set :return: nothing """ - if addr is not None: - addr = utils.validate_mac(addr) - self.hwaddr = addr + if mac is not None: + mac = utils.validate_mac(mac) + self.mac = mac def getparam(self, key: str) -> float: """ diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 04d4e8f8..ef9456db 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -895,7 +895,7 @@ class PtpNet(CoreNetwork): unidirectional = 1 iface1_data = InterfaceData( - id=iface1.node.get_iface_id(iface1), name=iface1.name, mac=iface1.hwaddr + id=iface1.node.get_iface_id(iface1), name=iface1.name, mac=iface1.mac ) for address in iface1.addrlist: ip, _sep, mask = address.partition("/") @@ -908,7 +908,7 @@ class PtpNet(CoreNetwork): iface1.ip6_mask = mask iface2_data = InterfaceData( - id=iface2.node.get_iface_id(iface2), name=iface2.name, mac=iface2.hwaddr + id=iface2.node.get_iface_id(iface2), name=iface2.name, mac=iface2.mac ) for address in iface2.addrlist: ip, _sep, mask = address.partition("/") diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 36bcb267..0ce8946a 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -65,20 +65,20 @@ class PhysicalNode(CoreNodeBase): """ return sh - def sethwaddr(self, iface_id: int, addr: str) -> None: + def set_mac(self, iface_id: int, mac: str) -> None: """ - Set hardware address for an interface. + Set mac address for an interface. :param iface_id: index of interface to set hardware address for - :param addr: hardware address to set + :param mac: mac address to set :return: nothing :raises CoreCommandError: when a non-zero exit status occurs """ - addr = utils.validate_mac(addr) + mac = utils.validate_mac(mac) iface = self.ifaces[iface_id] - iface.sethwaddr(addr) + iface.set_mac(mac) if self.up: - self.net_client.device_mac(iface.name, addr) + self.net_client.device_mac(iface.name, mac) def addaddr(self, iface_id: int, addr: str) -> None: """ @@ -111,7 +111,7 @@ class PhysicalNode(CoreNodeBase): self.net_client.delete_address(iface.name, addr) def adopt_iface( - self, iface: CoreInterface, iface_id: int, hwaddr: str, addrlist: List[str] + self, iface: CoreInterface, iface_id: int, mac: str, addrlist: List[str] ) -> None: """ When a link message is received linking this node to another part of @@ -126,8 +126,8 @@ class PhysicalNode(CoreNodeBase): self.net_client.device_down(iface.localname) self.net_client.device_name(iface.localname, iface.name) iface.localname = iface.name - if hwaddr: - self.sethwaddr(iface_id, hwaddr) + if mac: + self.set_mac(iface_id, mac) for addr in addrlist: self.addaddr(iface_id, addr) if self.up: diff --git a/daemon/core/services/xorp.py b/daemon/core/services/xorp.py index 3dfef56a..10b4fd9f 100644 --- a/daemon/core/services/xorp.py +++ b/daemon/core/services/xorp.py @@ -69,7 +69,7 @@ class XorpRtrmgr(CoreService): """ helper for adding link-local address entries (required by OSPFv3) """ - cfg = "\t address %s {\n" % iface.hwaddr.tolinklocal() + cfg = "\t address %s {\n" % iface.mac.tolinklocal() cfg += "\t\tprefix-length: 64\n" cfg += "\t }\n" return cfg @@ -305,7 +305,7 @@ class XorpRipng(XorpService): for iface in node.get_ifaces(control=False): cfg += "\tinterface %s {\n" % iface.name cfg += "\t vif %s {\n" % iface.name - cfg += "\t\taddress %s {\n" % iface.hwaddr.tolinklocal() + cfg += "\t\taddress %s {\n" % iface.mac.tolinklocal() cfg += "\t\t disable: false\n" cfg += "\t\t}\n" cfg += "\t }\n" diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index 4f511476..d716777b 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: from core.emane.emanemanager import EmaneManager from core.emane.emanemodel import EmaneModel -_hwaddr_prefix = "02:02" +_MAC_PREFIX = "02:02" def is_external(config: Dict[str, str]) -> bool: @@ -230,9 +230,9 @@ def build_node_platform_xml( platform_element.append(nem_element) node.setnemid(iface, nem_id) - macstr = _hwaddr_prefix + ":00:00:" + macstr = _MAC_PREFIX + ":00:00:" macstr += f"{(nem_id >> 8) & 0xFF:02X}:{nem_id & 0xFF:02X}" - iface.sethwaddr(macstr) + iface.set_mac(macstr) # increment nem id nem_id += 1 diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index 327137d2..8af2e895 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -49,7 +49,7 @@ class TestNodes: with pytest.raises(CoreError): session.get_node(node.id, CoreNode) - def test_node_sethwaddr(self, session: Session): + def test_node_set_mac(self, session: Session): # given node = session.add_node(CoreNode) switch = session.add_node(SwitchNode) @@ -58,12 +58,12 @@ class TestNodes: mac = "aa:aa:aa:ff:ff:ff" # when - node.sethwaddr(iface.node_id, mac) + node.set_mac(iface.node_id, mac) # then - assert iface.hwaddr == mac + assert iface.mac == mac - def test_node_sethwaddr_exception(self, session: Session): + def test_node_set_mac_exception(self, session: Session): # given node = session.add_node(CoreNode) switch = session.add_node(SwitchNode) @@ -73,7 +73,7 @@ class TestNodes: # when with pytest.raises(CoreError): - node.sethwaddr(iface.node_id, mac) + node.set_mac(iface.node_id, mac) def test_node_addaddr(self, session: Session): # given From a64047e221cb83d3f27d9c3c75f4d81afbea4036 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 16 Jun 2020 23:27:17 -0700 Subject: [PATCH 0364/1131] fixed issue with xorp service depending on old MacAddress class --- daemon/core/services/xorp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daemon/core/services/xorp.py b/daemon/core/services/xorp.py index 10b4fd9f..776b1d16 100644 --- a/daemon/core/services/xorp.py +++ b/daemon/core/services/xorp.py @@ -69,7 +69,7 @@ class XorpRtrmgr(CoreService): """ helper for adding link-local address entries (required by OSPFv3) """ - cfg = "\t address %s {\n" % iface.mac.tolinklocal() + cfg = "\t address %s {\n" % netaddr.EUI(iface.mac).eui64() cfg += "\t\tprefix-length: 64\n" cfg += "\t }\n" return cfg @@ -305,7 +305,7 @@ class XorpRipng(XorpService): for iface in node.get_ifaces(control=False): cfg += "\tinterface %s {\n" % iface.name cfg += "\t vif %s {\n" % iface.name - cfg += "\t\taddress %s {\n" % iface.mac.tolinklocal() + cfg += "\t\taddress %s {\n" % netaddr.EUI(iface.mac).eui64() cfg += "\t\t disable: false\n" cfg += "\t\t}\n" cfg += "\t }\n" From b92ff0586a6e04d1dae8a36f159e9f6449df7489 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 17 Jun 2020 22:43:13 -0700 Subject: [PATCH 0365/1131] daemon: renamed NodeData.node_type to type, removed NodeData/NodeOptions fields that were not being used for clarity --- daemon/core/api/grpc/grpcutils.py | 1 - daemon/core/api/tlv/corehandlers.py | 1 - daemon/core/api/tlv/dataconversion.py | 12 +-- daemon/core/emulator/data.py | 123 ++++++++++++-------------- daemon/core/emulator/session.py | 1 - daemon/core/nodes/base.py | 11 +-- daemon/proto/core/api/grpc/core.proto | 13 ++- 7 files changed, 70 insertions(+), 92 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 6f2911a4..7c517caf 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -34,7 +34,6 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption name=node_proto.name, model=node_proto.model, icon=node_proto.icon, - opaque=node_proto.opaque, image=node_proto.image, services=node_proto.services, config_services=node_proto.config_services, diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 631cd538..981bdb15 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -722,7 +722,6 @@ class CoreHandler(socketserver.BaseRequestHandler): options.icon = message.get_tlv(NodeTlvs.ICON.value) options.canvas = message.get_tlv(NodeTlvs.CANVAS.value) - options.opaque = message.get_tlv(NodeTlvs.OPAQUE.value) options.server = message.get_tlv(NodeTlvs.EMULATION_SERVER.value) services = message.get_tlv(NodeTlvs.SERVICES.value) diff --git a/daemon/core/api/tlv/dataconversion.py b/daemon/core/api/tlv/dataconversion.py index cd10ef04..62b51d39 100644 --- a/daemon/core/api/tlv/dataconversion.py +++ b/daemon/core/api/tlv/dataconversion.py @@ -18,9 +18,6 @@ def convert_node(node_data): :param core.emulator.data.NodeData node_data: node data to convert :return: packed node message """ - session = None - if node_data.session is not None: - session = str(node_data.session) services = None if node_data.services is not None: services = "|".join([x for x in node_data.services]) @@ -28,25 +25,18 @@ def convert_node(node_data): coreapi.CoreNodeTlv, [ (NodeTlvs.NUMBER, node_data.id), - (NodeTlvs.TYPE, node_data.node_type.value), + (NodeTlvs.TYPE, node_data.type.value), (NodeTlvs.NAME, node_data.name), - (NodeTlvs.IP_ADDRESS, node_data.ip_address), - (NodeTlvs.MAC_ADDRESS, node_data.mac_address), - (NodeTlvs.IP6_ADDRESS, node_data.ip6_address), (NodeTlvs.MODEL, node_data.model), - (NodeTlvs.EMULATION_ID, node_data.emulation_id), (NodeTlvs.EMULATION_SERVER, node_data.server), - (NodeTlvs.SESSION, session), (NodeTlvs.X_POSITION, int(node_data.x_position)), (NodeTlvs.Y_POSITION, int(node_data.y_position)), (NodeTlvs.CANVAS, node_data.canvas), - (NodeTlvs.NETWORK_ID, node_data.network_id), (NodeTlvs.SERVICES, services), (NodeTlvs.LATITUDE, str(node_data.latitude)), (NodeTlvs.LONGITUDE, str(node_data.longitude)), (NodeTlvs.ALTITUDE, str(node_data.altitude)), (NodeTlvs.ICON, node_data.icon), - (NodeTlvs.OPAQUE, node_data.opaque), ], ) return coreapi.CoreNodeMessage.pack(node_data.message_type.value, tlv_data) diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index 899d32ae..1a7a6096 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -73,28 +73,71 @@ class FileData: @dataclass -class NodeData: - message_type: MessageFlags = None - id: int = None - node_type: NodeTypes = None +class NodeOptions: + """ + Options for creating and updating nodes within core. + """ + name: str = None - ip_address: str = None - mac_address: str = None - ip6_address: str = None - model: str = None - emulation_id: int = None + model: Optional[str] = "PC" + canvas: int = None + icon: str = None + services: List[str] = field(default_factory=list) + config_services: List[str] = field(default_factory=list) + x: float = None + y: float = None + lat: float = None + lon: float = None + alt: float = None server: str = None - session: int = None + image: str = None + emane: str = None + + def set_position(self, x: float, y: float) -> None: + """ + Convenience method for setting position. + + :param x: x position + :param y: y position + :return: nothing + """ + self.x = x + self.y = y + + def set_location(self, lat: float, lon: float, alt: float) -> None: + """ + Convenience method for setting location. + + :param lat: latitude + :param lon: longitude + :param alt: altitude + :return: nothing + """ + self.lat = lat + self.lon = lon + self.alt = alt + + +@dataclass +class NodeData: + """ + Used to represent nodes being broadcasted. + """ + + message_type: MessageFlags = None + type: NodeTypes = None + id: int = None + name: str = None + model: str = None + server: str = None + icon: str = None + canvas: int = None + services: List[str] = None x_position: float = None y_position: float = None - canvas: int = None - network_id: int = None - services: List[str] = None latitude: float = None longitude: float = None altitude: float = None - icon: str = None - opaque: str = None source: str = None @@ -158,10 +201,10 @@ class LinkData: label: str = None node1_id: int = None node2_id: int = None + network_id: int = None iface1: InterfaceData = None iface2: InterfaceData = None options: LinkOptions = LinkOptions() - network_id: int = None color: str = None @@ -259,51 +302,3 @@ class IpPrefixes: iface_data = self.gen_iface(node.id, name, mac) iface_data.id = node.next_iface_id() return iface_data - - -@dataclass -class NodeOptions: - """ - Options for creating and updating nodes within core. - """ - - name: str = None - model: Optional[str] = "PC" - canvas: int = None - icon: str = None - opaque: str = None - services: List[str] = field(default_factory=list) - config_services: List[str] = field(default_factory=list) - x: float = None - y: float = None - lat: float = None - lon: float = None - alt: float = None - emulation_id: int = None - server: str = None - image: str = None - emane: str = None - - def set_position(self, x: float, y: float) -> None: - """ - Convenience method for setting position. - - :param x: x position - :param y: y position - :return: nothing - """ - self.x = x - self.y = y - - def set_location(self, lat: float, lon: float, alt: float) -> None: - """ - Convenience method for setting location. - - :param lat: latitude - :param lon: longitude - :param alt: altitude - :return: nothing - """ - self.lat = lat - self.lon = lon - self.alt = alt diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 814c89d9..ccabeddb 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -523,7 +523,6 @@ class Session: # set node attributes node.icon = options.icon node.canvas = options.canvas - node.opaque = options.opaque # set node position and broadcast it self.set_node_position(node, options) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 97da63a4..0ecc9085 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -195,29 +195,26 @@ class NodeBase(abc.ABC): """ if self.apitype is None: return None - x, y, _ = self.getposition() model = self.type server = None if self.server is not None: server = self.server.name - services = [service.name for service in self.services] + services = [x.name for x in self.services] return NodeData( message_type=message_type, + type=self.apitype, id=self.id, - node_type=self.apitype, name=self.name, - emulation_id=self.id, + model=model, + server=server, canvas=self.canvas, icon=self.icon, - opaque=self.opaque, x_position=x, y_position=y, latitude=self.position.lat, longitude=self.position.lon, altitude=self.position.alt, - model=model, - server=server, services=services, source=source, ) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 2819c5ea..46e1da91 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -692,13 +692,12 @@ message Node { repeated string services = 6; string emane = 7; string icon = 8; - string opaque = 9; - string image = 10; - string server = 11; - repeated string config_services = 12; - Geo geo = 13; - string dir = 14; - string channel = 15; + string image = 9; + string server = 10; + repeated string config_services = 11; + Geo geo = 12; + string dir = 13; + string channel = 14; } message Link { From 5d34a2b752631580e4d5ca01daaf0714fc707e44 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 17 Jun 2020 22:59:50 -0700 Subject: [PATCH 0366/1131] daemon: removed opaque from NodeBase, since it is not used --- daemon/core/nodes/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 0ecc9085..378549ab 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -71,7 +71,6 @@ class NodeBase(abc.ABC): self.iface_id: int = 0 self.canvas: Optional[int] = None self.icon: Optional[str] = None - self.opaque: Optional[str] = None self.position: Position = Position() self.up: bool = False use_ovs = session.options.get_config("ovs") == "True" From 3d7d775bfbf5c673837baeecd25faa078e3906a6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 18 Jun 2020 00:15:44 -0700 Subject: [PATCH 0367/1131] daemon: removed unused variables from LinkOptions --- daemon/core/api/grpc/grpcutils.py | 26 ++++++++++++-------------- daemon/core/api/grpc/server.py | 23 +++++++++++------------ daemon/core/api/tlv/corehandlers.py | 7 ------- daemon/core/emulator/data.py | 3 --- daemon/core/xml/corexml.py | 6 ------ 5 files changed, 23 insertions(+), 42 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 7c517caf..d95b7555 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -88,19 +88,18 @@ def add_link_data( iface2_data = link_iface(link_proto.iface2) link_type = LinkTypes(link_proto.type) options = LinkOptions() - options_data = link_proto.options - if options_data: - options.delay = options_data.delay - options.bandwidth = options_data.bandwidth - options.loss = options_data.loss - options.dup = options_data.dup - options.jitter = options_data.jitter - options.mer = options_data.mer - options.burst = options_data.burst - options.mburst = options_data.mburst - options.unidirectional = options_data.unidirectional - options.key = options_data.key - options.opaque = options_data.opaque + options_proto = link_proto.options + if options_proto: + options.delay = options_proto.delay + options.bandwidth = options_proto.bandwidth + options.loss = options_proto.loss + options.dup = options_proto.dup + options.jitter = options_proto.jitter + options.mer = options_proto.mer + options.burst = options_proto.burst + options.mburst = options_proto.mburst + options.unidirectional = options_proto.unidirectional + options.key = options_proto.key return iface1_data, iface2_data, options, link_type @@ -320,7 +319,6 @@ def convert_iface(iface_data: InterfaceData) -> core_pb2.Interface: def convert_link_options(options_data: LinkOptions) -> core_pb2.LinkOptions: return core_pb2.LinkOptions( - opaque=options_data.opaque, jitter=options_data.jitter, key=options_data.key, mburst=options_data.mburst, diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index b9e0e0aa..1964b6e8 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -879,19 +879,18 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): node2_id = request.node2_id iface1_id = request.iface1_id iface2_id = request.iface2_id - options_data = request.options + options_proto = request.options options = LinkOptions( - delay=options_data.delay, - bandwidth=options_data.bandwidth, - loss=options_data.loss, - dup=options_data.dup, - jitter=options_data.jitter, - mer=options_data.mer, - burst=options_data.burst, - mburst=options_data.mburst, - unidirectional=options_data.unidirectional, - key=options_data.key, - opaque=options_data.opaque, + delay=options_proto.delay, + bandwidth=options_proto.bandwidth, + loss=options_proto.loss, + dup=options_proto.dup, + jitter=options_proto.jitter, + mer=options_proto.mer, + burst=options_proto.burst, + mburst=options_proto.mburst, + unidirectional=options_proto.unidirectional, + key=options_proto.key, ) session.update_link(node1_id, node2_id, iface1_id, iface2_id, options) return core_pb2.EditLinkResponse(result=True) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 981bdb15..379b739e 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -371,9 +371,7 @@ class CoreHandler(socketserver.BaseRequestHandler): (LinkTlvs.BURST, options_data.burst), (LinkTlvs.MBURST, options_data.mburst), (LinkTlvs.TYPE, link_data.type.value), - (LinkTlvs.GUI_ATTRIBUTES, options_data.gui_attributes), (LinkTlvs.UNIDIRECTIONAL, options_data.unidirectional), - (LinkTlvs.EMULATION_ID, options_data.emulation_id), (LinkTlvs.NETWORK_ID, link_data.network_id), (LinkTlvs.KEY, options_data.key), (LinkTlvs.IFACE1_NUMBER, iface1.id), @@ -388,7 +386,6 @@ class CoreHandler(socketserver.BaseRequestHandler): (LinkTlvs.IFACE2_MAC, iface2.mac), (LinkTlvs.IFACE2_IP6, iface2.ip6), (LinkTlvs.IFACE2_IP6_MASK, iface2.ip6_mask), - (LinkTlvs.OPAQUE, options_data.opaque), ], ) @@ -792,12 +789,8 @@ class CoreHandler(socketserver.BaseRequestHandler): 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.gui_attributes = message.get_tlv(LinkTlvs.GUI_ATTRIBUTES.value) options.unidirectional = message.get_tlv(LinkTlvs.UNIDIRECTIONAL.value) - options.emulation_id = message.get_tlv(LinkTlvs.EMULATION_ID.value) - options.network_id = message.get_tlv(LinkTlvs.NETWORK_ID.value) options.key = message.get_tlv(LinkTlvs.KEY.value) - options.opaque = message.get_tlv(LinkTlvs.OPAQUE.value) if message.flags & MessageFlags.ADD.value: self.session.add_link( diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index 1a7a6096..c57f8b24 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -183,11 +183,8 @@ class LinkOptions: mer: int = None burst: int = None mburst: int = None - gui_attributes: str = None unidirectional: int = None - emulation_id: int = None key: int = None - opaque: str = None @dataclass diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 4febe71f..190cf8f7 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -547,12 +547,9 @@ class CoreXmlWriter: add_attribute(options, "mer", options_data.mer) add_attribute(options, "burst", options_data.burst) add_attribute(options, "mburst", options_data.mburst) - add_attribute(options, "gui_attributes", options_data.gui_attributes) add_attribute(options, "unidirectional", options_data.unidirectional) - add_attribute(options, "emulation_id", options_data.emulation_id) add_attribute(options, "network_id", link_data.network_id) add_attribute(options, "key", options_data.key) - add_attribute(options, "opaque", options_data.opaque) if options.items(): link_element.append(options) @@ -939,9 +936,6 @@ class CoreXmlReader: if options.loss is None: options.loss = get_float(options_element, "per") options.unidirectional = get_int(options_element, "unidirectional") - options.emulation_id = get_int(options_element, "emulation_id") - options.opaque = options_element.get("opaque") - options.gui_attributes = options_element.get("gui_attributes") if options.unidirectional == 1 and node_set in node_sets: logging.info("updating link node1(%s) node2(%s)", node1_id, node2_id) From 1702fe256f2ae153e65baabcb963c0d6638f1397 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 18 Jun 2020 00:30:39 -0700 Subject: [PATCH 0368/1131] doc: updated refactored example in documentation --- docs/services.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/services.md b/docs/services.md index 9f47ae48..2ce52e99 100644 --- a/docs/services.md +++ b/docs/services.md @@ -263,7 +263,7 @@ class MyService(CoreService): if filename == cls.configs[0]: cfg += "# auto-generated by MyService (sample.py)\n" - for ifc in node.netifs(): + for ifc in node.get_ifaces(): cfg += f'echo "Node {node.name} has interface {ifc.name}"\n' elif filename == cls.configs[1]: cfg += "echo hello" From ecc3eb1c891b8458e944158b9a14b0f43f9348d8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 18 Jun 2020 09:06:31 -0700 Subject: [PATCH 0369/1131] daemon: refactored NodeData to reference a node instead of replicating fields as an intermediate passthrough, removed data() functions from nodes due to this change --- daemon/core/api/grpc/events.py | 22 +++++++++------- daemon/core/api/tlv/corehandlers.py | 1 - daemon/core/api/tlv/dataconversion.py | 36 ++++++++++++++----------- daemon/core/emulator/data.py | 19 +++----------- daemon/core/emulator/session.py | 4 +-- daemon/core/nodes/base.py | 38 +-------------------------- daemon/core/nodes/interface.py | 21 +-------------- daemon/core/nodes/network.py | 15 +---------- daemon/core/plugins/sdt.py | 18 +++++-------- daemon/tests/test_grpc.py | 7 ++--- 10 files changed, 52 insertions(+), 129 deletions(-) diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index ff65142d..75f9eb2e 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -15,24 +15,28 @@ from core.emulator.data import ( from core.emulator.session import Session -def handle_node_event(event: NodeData) -> core_pb2.NodeEvent: +def handle_node_event(node_data: NodeData) -> core_pb2.NodeEvent: """ Handle node event when there is a node event - :param event: node data + :param node_data: node data :return: node event that contains node id, name, model, position, and services """ - position = core_pb2.Position(x=event.x_position, y=event.y_position) - geo = core_pb2.Geo(lat=event.latitude, lon=event.longitude, alt=event.altitude) + node = node_data.node + x, y, _ = node.position.get() + position = core_pb2.Position(x=x, y=y) + lon, lat, alt = node.position.get_geo() + geo = core_pb2.Geo(lon=lon, lat=lat, alt=alt) + services = [x.name for x in node.services] node_proto = core_pb2.Node( - id=event.id, - name=event.name, - model=event.model, + id=node.id, + name=node.name, + model=node.type, position=position, geo=geo, - services=event.services, + services=services, ) - return core_pb2.NodeEvent(node=node_proto, source=event.source) + return core_pb2.NodeEvent(node=node_proto, source=node_data.source) def handle_link_event(event: LinkData) -> core_pb2.LinkEvent: diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 379b739e..d01f15a3 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -329,7 +329,6 @@ class CoreHandler(socketserver.BaseRequestHandler): """ logging.debug("handling broadcast node: %s", node_data) message = dataconversion.convert_node(node_data) - try: self.sendall(message) except IOError: diff --git a/daemon/core/api/tlv/dataconversion.py b/daemon/core/api/tlv/dataconversion.py index 62b51d39..8a26300a 100644 --- a/daemon/core/api/tlv/dataconversion.py +++ b/daemon/core/api/tlv/dataconversion.py @@ -8,35 +8,39 @@ from typing import Dict, List from core.api.tlv import coreapi, structutils from core.api.tlv.enumerations import ConfigTlvs, NodeTlvs from core.config import ConfigGroup, ConfigurableOptions -from core.emulator.data import ConfigData +from core.emulator.data import ConfigData, NodeData -def convert_node(node_data): +def convert_node(node_data: NodeData): """ Convenience method for converting NodeData to a packed TLV message. :param core.emulator.data.NodeData node_data: node data to convert :return: packed node message """ + node = node_data.node services = None - if node_data.services is not None: - services = "|".join([x for x in node_data.services]) + if node.services is not None: + services = "|".join([x.name for x in node.services]) + server = None + if node.server is not None: + server = node.server.name tlv_data = structutils.pack_values( coreapi.CoreNodeTlv, [ - (NodeTlvs.NUMBER, node_data.id), - (NodeTlvs.TYPE, node_data.type.value), - (NodeTlvs.NAME, node_data.name), - (NodeTlvs.MODEL, node_data.model), - (NodeTlvs.EMULATION_SERVER, node_data.server), - (NodeTlvs.X_POSITION, int(node_data.x_position)), - (NodeTlvs.Y_POSITION, int(node_data.y_position)), - (NodeTlvs.CANVAS, node_data.canvas), + (NodeTlvs.NUMBER, node.id), + (NodeTlvs.TYPE, node.apitype.value), + (NodeTlvs.NAME, node.name), + (NodeTlvs.MODEL, node.type), + (NodeTlvs.EMULATION_SERVER, server), + (NodeTlvs.X_POSITION, int(node.position.x)), + (NodeTlvs.Y_POSITION, int(node.position.y)), + (NodeTlvs.CANVAS, node.canvas), (NodeTlvs.SERVICES, services), - (NodeTlvs.LATITUDE, str(node_data.latitude)), - (NodeTlvs.LONGITUDE, str(node_data.longitude)), - (NodeTlvs.ALTITUDE, str(node_data.altitude)), - (NodeTlvs.ICON, node_data.icon), + (NodeTlvs.LATITUDE, str(node.position.lat)), + (NodeTlvs.LONGITUDE, str(node.position.lon)), + (NodeTlvs.ALTITUDE, str(node.position.alt)), + (NodeTlvs.ICON, node.icon), ], ) return coreapi.CoreNodeMessage.pack(node_data.message_type.value, tlv_data) diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index c57f8b24..5b6479ae 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -12,11 +12,10 @@ from core.emulator.enumerations import ( ExceptionLevels, LinkTypes, MessageFlags, - NodeTypes, ) if TYPE_CHECKING: - from core.nodes.base import CoreNode + from core.nodes.base import CoreNode, NodeBase @dataclass @@ -121,23 +120,11 @@ class NodeOptions: @dataclass class NodeData: """ - Used to represent nodes being broadcasted. + Node to broadcast. """ + node: "NodeBase" message_type: MessageFlags = None - type: NodeTypes = None - id: int = None - name: str = None - model: str = None - server: str = None - icon: str = None - canvas: int = None - services: List[str] = None - x_position: float = None - y_position: float = None - latitude: float = None - longitude: float = None - altitude: float = None source: str = None diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index ccabeddb..0b97da93 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -807,9 +807,9 @@ class Session: :param source: source of broadcast, None by default :return: nothing """ - node_data = node.data(message_type, source) - if not node_data: + if not node.apitype: return + node_data = NodeData(node=node, message_type=message_type, source=source) for handler in self.node_handlers: handler(node_data) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 378549ab..8a5c579a 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -14,7 +14,7 @@ import netaddr from core import utils from core.configservice.dependencies import ConfigServiceDependencies from core.constants import MOUNT_BIN, VNODED_BIN -from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeData +from core.emulator.data import InterfaceData, LinkData, LinkOptions from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes from core.errors import CoreCommandError, CoreError from core.nodes.client import VnodeClient @@ -182,42 +182,6 @@ class NodeBase(abc.ABC): self.iface_id += 1 return iface_id - def data( - self, message_type: MessageFlags = MessageFlags.NONE, source: str = None - ) -> Optional[NodeData]: - """ - Build a data object for this node. - - :param message_type: purpose for the data object we are creating - :param source: source of node data - :return: node data object - """ - if self.apitype is None: - return None - x, y, _ = self.getposition() - model = self.type - server = None - if self.server is not None: - server = self.server.name - services = [x.name for x in self.services] - return NodeData( - message_type=message_type, - type=self.apitype, - id=self.id, - name=self.name, - model=model, - server=server, - canvas=self.canvas, - icon=self.icon, - x_position=x, - y_position=y, - latitude=self.position.lat, - longitude=self.position.lon, - altitude=self.position.alt, - services=services, - source=source, - ) - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Build link data for this node. diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 287723a7..680def1b 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple from core import utils from core.emulator.data import LinkOptions -from core.emulator.enumerations import MessageFlags, TransportType +from core.emulator.enumerations import TransportType from core.errors import CoreCommandError from core.nodes.netclient import LinuxNetClient, get_net_client @@ -561,23 +561,4 @@ class GreTap(CoreInterface): self.net_client.delete_device(self.localname) except CoreCommandError: logging.exception("error during shutdown") - self.localname = None - - def data(self, message_type: int) -> None: - """ - Data for a gre tap. - - :param message_type: message type for data - :return: None - """ - return None - - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List: - """ - Retrieve link data. - - :param flags: link flags - :return: link data - """ - return [] diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index ef9456db..f5baf326 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -11,7 +11,7 @@ import netaddr from core import utils from core.constants import EBTABLES_BIN, TC_BIN -from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeData +from core.emulator.data import InterfaceData, LinkData, LinkOptions from core.emulator.enumerations import ( LinkTypes, MessageFlags, @@ -862,19 +862,6 @@ class PtpNet(CoreNetwork): ) super().attach(iface) - def data( - self, message_type: MessageFlags = MessageFlags.NONE, source: str = None - ) -> Optional[NodeData]: - """ - Do not generate a Node Message for point-to-point links. They are - built using a link message instead. - - :param message_type: purpose for the data object we are creating - :param source: source of node data - :return: node data object - """ - return None - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Build CORE API TLVs for a point-to-point link. One Link message diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 04fff3e4..84c90730 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -314,26 +314,22 @@ class Sdt: :param node_data: node data being updated :return: nothing """ - logging.debug("sdt handle node update: %s - %s", node_data.id, node_data.name) if not self.connect(): return - - # delete node + node = node_data.node + logging.debug("sdt handle node update: %s - %s", node.id, node.name) if node_data.message_type == MessageFlags.DELETE: - self.cmd(f"delete node,{node_data.id}") + self.cmd(f"delete node,{node.id}") else: - x = node_data.x_position - y = node_data.y_position - lat = node_data.latitude - lon = node_data.longitude - alt = node_data.altitude + x, y, _ = node.position.get() + lon, lat, alt = node.position.get_geo() if all([lat is not None, lon is not None, alt is not None]): pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}" - self.cmd(f"node {node_data.id} {pos}") + self.cmd(f"node {node.id} {pos}") elif node_data.message_type == 0: lat, lon, alt = self.session.location.getgeo(x, y, 0) pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}" - self.cmd(f"node {node_data.id} {pos}") + self.cmd(f"node {node.id} {pos}") def wireless_net_check(self, node_id: int) -> bool: """ diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index cff7cd85..8abf33aa 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -1198,9 +1198,10 @@ class TestGrpc: queue = Queue() def node_handler(node_data: NodeData): - assert node_data.longitude == lon - assert node_data.latitude == lat - assert node_data.altitude == alt + n = node_data.node + assert n.position.lon == lon + assert n.position.lat == lat + assert n.position.alt == alt queue.put(node_data) session.node_handlers.append(node_handler) From e46a072f744c198e702e5fce5347538aa0ac1456 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 18 Jun 2020 09:33:54 -0700 Subject: [PATCH 0370/1131] daemon: removed missing params from python docs, updated node ValueErrors to CoreErrors --- daemon/core/nodes/interface.py | 4 ++-- daemon/core/nodes/network.py | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 680def1b..42522362 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple from core import utils from core.emulator.data import LinkOptions from core.emulator.enumerations import TransportType -from core.errors import CoreCommandError +from core.errors import CoreCommandError, CoreError from core.nodes.netclient import LinuxNetClient, get_net_client if TYPE_CHECKING: @@ -544,7 +544,7 @@ class GreTap(CoreInterface): if not start: return if remoteip is None: - raise ValueError("missing remote IP required for GRE TAP device") + raise CoreError("missing remote IP required for GRE TAP device") self.net_client.create_gretap(self.localname, remoteip, localip, ttl, key) self.net_client.device_up(self.localname) self.up = True diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index f5baf326..f20b6dfb 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -618,7 +618,6 @@ class GreTapBridge(CoreNetwork): :param localip: local address :param ttl: ttl value :param key: gre tap key - :param start: start flag :param server: remote server node will run on, default is None for localhost """ @@ -857,9 +856,7 @@ class PtpNet(CoreNetwork): :return: nothing """ if len(self.ifaces) >= 2: - raise ValueError( - "Point-to-point links support at most 2 network interfaces" - ) + raise CoreError("ptp links support at most 2 network interfaces") super().attach(iface) def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: @@ -992,7 +989,6 @@ class WlanNode(CoreNetwork): :param session: core session instance :param _id: node id :param name: node name - :param start: start flag :param server: remote server node will run on, default is None for localhost :param policy: wlan policy From cd74a44558596d259e0b878b49ac605edf389665 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 18 Jun 2020 12:54:36 -0700 Subject: [PATCH 0371/1131] daemon: added type hinting throughout all services and made small tweaks/fixes that were ran across --- daemon/core/nodes/base.py | 4 +- daemon/core/services/bird.py | 86 +++++------ daemon/core/services/emaneservices.py | 25 ++-- daemon/core/services/frr.py | 202 ++++++++++++------------- daemon/core/services/nrl.py | 154 +++++++++---------- daemon/core/services/quagga.py | 196 ++++++++++++------------ daemon/core/services/sdn.py | 44 +++--- daemon/core/services/security.py | 112 +++++++------- daemon/core/services/ucarp.py | 42 +++--- daemon/core/services/utility.py | 206 ++++++++++++-------------- daemon/core/services/xorp.py | 125 +++++++--------- 11 files changed, 560 insertions(+), 636 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 8a5c579a..4fc6b873 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -7,7 +7,7 @@ import os import shutil import threading from threading import RLock -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, Union import netaddr @@ -27,7 +27,7 @@ if TYPE_CHECKING: from core.configservice.base import ConfigService from core.services.coreservices import CoreService - CoreServices = List[CoreService] + CoreServices = List[Union[CoreService, Type[CoreService]]] ConfigServiceType = Type[ConfigService] _DEFAULT_MTU = 1500 diff --git a/daemon/core/services/bird.py b/daemon/core/services/bird.py index 16f0bb84..a5052942 100644 --- a/daemon/core/services/bird.py +++ b/daemon/core/services/bird.py @@ -1,8 +1,11 @@ """ bird.py: defines routing services provided by the BIRD Internet Routing Daemon. """ +from typing import Optional, Tuple + import netaddr +from core.nodes.base import CoreNode from core.services.coreservices import CoreService @@ -11,27 +14,27 @@ class Bird(CoreService): Bird router support """ - name = "bird" - executables = ("bird",) - group = "BIRD" - dirs = ("/etc/bird",) - configs = ("/etc/bird/bird.conf",) - startup = ("bird -c %s" % (configs[0]),) - shutdown = ("killall bird",) - validate = ("pidof bird",) + name: str = "bird" + group: str = "BIRD" + executables: Tuple[str, ...] = ("bird",) + dirs: Tuple[str, ...] = ("/etc/bird",) + configs: Tuple[str, ...] = ("/etc/bird/bird.conf",) + startup: Tuple[str, ...] = ("bird -c %s" % (configs[0]),) + shutdown: Tuple[str, ...] = ("killall bird",) + validate: Tuple[str, ...] = ("pidof bird",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return the bird.conf file contents. """ if filename == cls.configs[0]: - return cls.generateBirdConf(node) + return cls.generate_bird_config(node) else: raise ValueError @staticmethod - def routerid(node): + def router_id(node: CoreNode) -> str: """ Helper to return the first IPv4 address of a node as its router ID. """ @@ -40,15 +43,13 @@ class Bird(CoreService): a = a.split("/")[0] if netaddr.valid_ipv4(a): return a - # raise ValueError, "no IPv4 address found for router ID" return "0.0.0.0" @classmethod - def generateBirdConf(cls, node): + def generate_bird_config(cls, node: CoreNode) -> str: """ Returns configuration file text. Other services that depend on bird - will have generatebirdifcconfig() and generatebirdconfig() - hooks that are invoked here. + will have hooks that are invoked here. """ cfg = """\ /* Main configuration file for BIRD. This is ony a template, @@ -75,15 +76,16 @@ protocol device { """ % ( cls.name, - cls.routerid(node), + cls.router_id(node), ) - # Generate protocol specific configurations + # generate protocol specific configurations for s in node.services: if cls.name not in s.dependencies: continue + if not (isinstance(s, BirdService) or issubclass(s, BirdService)): + continue cfg += s.generate_bird_config(node) - return cfg @@ -93,32 +95,26 @@ class BirdService(CoreService): common to Bird's routing daemons. """ - name = None - executables = ("bird",) - group = "BIRD" - dependencies = ("bird",) - dirs = () - configs = () - startup = () - shutdown = () - meta = "The config file for this service can be found in the bird service." + name: Optional[str] = None + group: str = "BIRD" + executables: Tuple[str, ...] = ("bird",) + dependencies: Tuple[str, ...] = ("bird",) + meta: str = "The config file for this service can be found in the bird service." @classmethod - def generate_bird_config(cls, node): + def generate_bird_config(cls, node: CoreNode) -> str: return "" @classmethod - def generate_bird_iface_config(cls, node): + def generate_bird_iface_config(cls, node: CoreNode) -> str: """ Use only bare interfaces descriptions in generated protocol configurations. This has the slight advantage of being the same everywhere. """ cfg = "" - for iface in node.get_ifaces(control=False): cfg += ' interface "%s";\n' % iface.name - return cfg @@ -127,11 +123,11 @@ class BirdBgp(BirdService): BGP BIRD Service (configuration generation) """ - name = "BIRD_BGP" - custom_needed = True + name: str = "BIRD_BGP" + custom_needed: bool = True @classmethod - def generate_bird_config(cls, node): + def generate_bird_config(cls, node: CoreNode) -> str: return """ /* This is a sample config that should be customized with appropriate AS numbers * and peers; add one section like this for each neighbor */ @@ -158,10 +154,10 @@ class BirdOspf(BirdService): OSPF BIRD Service (configuration generation) """ - name = "BIRD_OSPFv2" + name: str = "BIRD_OSPFv2" @classmethod - def generate_bird_config(cls, node): + def generate_bird_config(cls, node: CoreNode) -> str: cfg = "protocol ospf {\n" cfg += " export filter {\n" cfg += " if source = RTS_BGP then {\n" @@ -174,7 +170,6 @@ class BirdOspf(BirdService): cfg += cls.generate_bird_iface_config(node) cfg += " };\n" cfg += "}\n\n" - return cfg @@ -183,12 +178,11 @@ class BirdRadv(BirdService): RADV BIRD Service (configuration generation) """ - name = "BIRD_RADV" + name: str = "BIRD_RADV" @classmethod - def generate_bird_config(cls, node): + def generate_bird_config(cls, node: CoreNode) -> str: cfg = "/* This is a sample config that must be customized */\n" - cfg += "protocol radv {\n" cfg += " # auto configuration on all interfaces\n" cfg += cls.generate_bird_iface_config(node) @@ -202,7 +196,6 @@ class BirdRadv(BirdService): cfg += "# ns 2001:0DB8:1234::12;\n" cfg += " };\n" cfg += "}\n\n" - return cfg @@ -211,10 +204,10 @@ class BirdRip(BirdService): RIP BIRD Service (configuration generation) """ - name = "BIRD_RIP" + name: str = "BIRD_RIP" @classmethod - def generate_bird_config(cls, node): + def generate_bird_config(cls, node: CoreNode) -> str: cfg = "protocol rip {\n" cfg += " period 10;\n" cfg += " garbage time 60;\n" @@ -224,7 +217,6 @@ class BirdRip(BirdService): cfg += " import all;\n" cfg += " export all;\n" cfg += "}\n\n" - return cfg @@ -233,11 +225,11 @@ class BirdStatic(BirdService): Static Bird Service (configuration generation) """ - name = "BIRD_static" - custom_needed = True + name: str = "BIRD_static" + custom_needed: bool = True @classmethod - def generate_bird_config(cls, node): + def generate_bird_config(cls, node: CoreNode) -> str: cfg = "/* This is a sample config that must be customized */\n" cfg += "protocol static {\n" cfg += "# route 0.0.0.0/0 via 198.51.100.130; # Default route. Do NOT advertise on BGP !\n" diff --git a/daemon/core/services/emaneservices.py b/daemon/core/services/emaneservices.py index da438bab..ef188fab 100644 --- a/daemon/core/services/emaneservices.py +++ b/daemon/core/services/emaneservices.py @@ -1,23 +1,26 @@ +from typing import Tuple + from core.emane.nodes import EmaneNet from core.errors import CoreError +from core.nodes.base import CoreNode from core.services.coreservices import CoreService from core.xml import emanexml class EmaneTransportService(CoreService): - name = "transportd" - executables = ("emanetransportd", "emanegentransportxml") - group = "EMANE" - dependencies = () - dirs = () - configs = ("emanetransport.sh",) - startup = ("sh %s" % configs[0],) - validate = ("pidof %s" % executables[0],) - validation_timer = 0.5 - shutdown = ("killall %s" % executables[0],) + name: str = "transportd" + group: str = "EMANE" + executables: Tuple[str, ...] = ("emanetransportd", "emanegentransportxml") + dependencies: Tuple[str, ...] = () + dirs: Tuple[str, ...] = () + configs: Tuple[str, ...] = ("emanetransport.sh",) + startup: Tuple[str, ...] = ("sh %s" % configs[0],) + validate: Tuple[str, ...] = ("pidof %s" % executables[0],) + validation_timer: float = 0.5 + shutdown: Tuple[str, ...] = ("killall %s" % executables[0],) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: if filename == cls.configs[0]: transport_commands = [] for iface in node.get_ifaces(): diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py index 97a8b334..e75d8f56 100644 --- a/daemon/core/services/frr.py +++ b/daemon/core/services/frr.py @@ -2,60 +2,63 @@ frr.py: defines routing services provided by FRRouting. Assumes installation of FRR via https://deb.frrouting.org/ """ +from typing import Optional, Tuple + import netaddr from core import constants from core.emane.nodes import EmaneNet +from core.nodes.base import CoreNode +from core.nodes.interface import CoreInterface from core.nodes.network import PtpNet, WlanNode from core.nodes.physical import Rj45Node from core.services.coreservices import CoreService class FRRZebra(CoreService): - name = "FRRzebra" - group = "FRR" - dirs = ("/usr/local/etc/frr", "/var/run/frr", "/var/log/frr") - configs = ( + name: str = "FRRzebra" + group: str = "FRR" + dirs: Tuple[str, ...] = ("/usr/local/etc/frr", "/var/run/frr", "/var/log/frr") + configs: Tuple[str, ...] = ( "/usr/local/etc/frr/frr.conf", "frrboot.sh", "/usr/local/etc/frr/vtysh.conf", "/usr/local/etc/frr/daemons", ) - startup = ("sh frrboot.sh zebra",) - shutdown = ("killall zebra",) - validate = ("pidof zebra",) + startup: Tuple[str, ...] = ("sh frrboot.sh zebra",) + shutdown: Tuple[str, ...] = ("killall zebra",) + validate: Tuple[str, ...] = ("pidof zebra",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return the frr.conf or frrboot.sh file contents. """ if filename == cls.configs[0]: - return cls.generateFrrConf(node) + return cls.generate_frr_conf(node) elif filename == cls.configs[1]: - return cls.generateFrrBoot(node) + return cls.generate_frr_boot(node) elif filename == cls.configs[2]: - return cls.generateVtyshConf(node) + return cls.generate_vtysh_conf(node) elif filename == cls.configs[3]: - return cls.generateFrrDaemons(node) + return cls.generate_frr_daemons(node) else: raise ValueError( "file name (%s) is not a known configuration: %s", filename, cls.configs ) @classmethod - def generateVtyshConf(cls, node): + def generate_vtysh_conf(cls, node: CoreNode) -> str: """ Returns configuration file text. """ return "service integrated-vtysh-config\n" @classmethod - def generateFrrConf(cls, node): + def generate_frr_conf(cls, node: CoreNode) -> str: """ Returns configuration file text. Other services that depend on zebra - will have generatefrrifcconfig() and generatefrrconfig() - hooks that are invoked here. + will have hooks that are invoked here. """ # we could verify here that filename == frr.conf cfg = "" @@ -108,7 +111,7 @@ class FRRZebra(CoreService): return cfg @staticmethod - def addrstr(x): + def addrstr(x: str) -> str: """ helper for mapping IP addresses to zebra config statements """ @@ -121,7 +124,7 @@ class FRRZebra(CoreService): raise ValueError("invalid address: %s", x) @classmethod - def generateFrrBoot(cls, node): + def generate_frr_boot(cls, node: CoreNode) -> str: """ Generate a shell script used to boot the FRR daemons. """ @@ -244,7 +247,7 @@ bootfrr return cfg @classmethod - def generateFrrDaemons(cls, node): + def generate_frr_daemons(cls, node: CoreNode) -> str: """ Returns configuration file text. """ @@ -317,20 +320,15 @@ class FrrService(CoreService): common to FRR's routing daemons. """ - name = None - group = "FRR" - dependencies = ("FRRzebra",) - dirs = () - configs = () - startup = () - shutdown = () - meta = "The config file for this service can be found in the Zebra service." - - ipv4_routing = False - ipv6_routing = False + name: Optional[str] = None + group: str = "FRR" + dependencies: Tuple[str, ...] = ("FRRzebra",) + meta: str = "The config file for this service can be found in the Zebra service." + ipv4_routing: bool = False + ipv6_routing: bool = False @staticmethod - def routerid(node): + def router_id(node: CoreNode) -> str: """ Helper to return the first IPv4 address of a node as its router ID. """ @@ -339,11 +337,10 @@ class FrrService(CoreService): a = a.split("/")[0] if netaddr.valid_ipv4(a): return a - # raise ValueError, "no IPv4 address found for router ID" return "0.0.0.0" @staticmethod - def rj45check(iface): + def rj45check(iface: CoreInterface) -> bool: """ Helper to detect whether interface is connected an external RJ45 link. @@ -357,15 +354,15 @@ class FrrService(CoreService): return False @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: return "" @classmethod - def generate_frr_iface_config(cls, node, iface): + def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: return "" @classmethod - def generate_frr_config(cls, node): + def generate_frr_config(cls, node: CoreNode) -> str: return "" @@ -376,14 +373,13 @@ class FRROspfv2(FrrService): unified frr.conf file. """ - name = "FRROSPFv2" - startup = () - shutdown = ("killall ospfd",) - validate = ("pidof ospfd",) - ipv4_routing = True + name: str = "FRROSPFv2" + shutdown: Tuple[str, ...] = ("killall ospfd",) + validate: Tuple[str, ...] = ("pidof ospfd",) + ipv4_routing: bool = True @staticmethod - def mtucheck(iface): + def mtu_check(iface: CoreInterface) -> str: """ Helper to detect MTU mismatch and add the appropriate OSPF mtu-ignore command. This is needed when e.g. a node is linked via a @@ -401,7 +397,7 @@ class FRROspfv2(FrrService): return "" @staticmethod - def ptpcheck(iface): + def ptp_check(iface: CoreInterface) -> str: """ Helper to detect whether interface is connected to a notional point-to-point link. @@ -411,9 +407,9 @@ class FRROspfv2(FrrService): return "" @classmethod - def generate_frr_config(cls, node): + def generate_frr_config(cls, node: CoreNode) -> str: cfg = "router ospf\n" - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += " router-id %s\n" % rtrid # network 10.0.0.0/24 area 0 for iface in node.get_ifaces(control=False): @@ -426,8 +422,8 @@ class FRROspfv2(FrrService): return cfg @classmethod - def generate_frr_iface_config(cls, node, iface): - return cls.mtucheck(iface) + def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: + return cls.mtu_check(iface) class FRROspfv3(FrrService): @@ -437,15 +433,14 @@ class FRROspfv3(FrrService): unified frr.conf file. """ - name = "FRROSPFv3" - startup = () - shutdown = ("killall ospf6d",) - validate = ("pidof ospf6d",) - ipv4_routing = True - ipv6_routing = True + name: str = "FRROSPFv3" + shutdown: Tuple[str, ...] = ("killall ospf6d",) + validate: Tuple[str, ...] = ("pidof ospf6d",) + ipv4_routing: bool = True + ipv6_routing: bool = True @staticmethod - def minmtu(iface): + def min_mtu(iface: CoreInterface) -> int: """ Helper to discover the minimum MTU of interfaces linked with the given interface. @@ -459,20 +454,20 @@ class FRROspfv3(FrrService): return mtu @classmethod - def mtucheck(cls, iface): + def mtu_check(cls, iface: CoreInterface) -> str: """ Helper to detect MTU mismatch and add the appropriate OSPFv3 ifmtu command. This is needed when e.g. a node is linked via a GreTap device. """ - minmtu = cls.minmtu(iface) + minmtu = cls.min_mtu(iface) if minmtu < iface.mtu: return " ipv6 ospf6 ifmtu %d\n" % minmtu else: return "" @staticmethod - def ptpcheck(iface): + def ptp_check(iface: CoreInterface) -> str: """ Helper to detect whether interface is connected to a notional point-to-point link. @@ -482,9 +477,9 @@ class FRROspfv3(FrrService): return "" @classmethod - def generate_frr_config(cls, node): + def generate_frr_config(cls, node: CoreNode) -> str: cfg = "router ospf6\n" - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += " router-id %s\n" % rtrid for iface in node.get_ifaces(control=False): cfg += " interface %s area 0.0.0.0\n" % iface.name @@ -492,14 +487,13 @@ class FRROspfv3(FrrService): return cfg @classmethod - def generate_frr_iface_config(cls, node, iface): - return cls.mtucheck(iface) + def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: + return cls.mtu_check(iface) # cfg = cls.mtucheck(ifc) # external RJ45 connections will use default OSPF timers # if cls.rj45check(ifc): # return cfg # cfg += cls.ptpcheck(ifc) - # return cfg + """\ @@ -516,21 +510,20 @@ class FRRBgp(FrrService): having the same AS number. """ - name = "FRRBGP" - startup = () - shutdown = ("killall bgpd",) - validate = ("pidof bgpd",) - custom_needed = True - ipv4_routing = True - ipv6_routing = True + name: str = "FRRBGP" + shutdown: Tuple[str, ...] = ("killall bgpd",) + validate: Tuple[str, ...] = ("pidof bgpd",) + custom_needed: bool = True + ipv4_routing: bool = True + ipv6_routing: bool = True @classmethod - def generate_frr_config(cls, node): + def generate_frr_config(cls, node: CoreNode) -> str: cfg = "!\n! BGP configuration\n!\n" cfg += "! You should configure the AS number below,\n" cfg += "! along with this router's peers.\n!\n" cfg += "router bgp %s\n" % node.id - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += " bgp router-id %s\n" % rtrid cfg += " redistribute connected\n" cfg += "! neighbor 1.2.3.4 remote-as 555\n!\n" @@ -542,14 +535,13 @@ class FRRRip(FrrService): The RIP service provides IPv4 routing for wired networks. """ - name = "FRRRIP" - startup = () - shutdown = ("killall ripd",) - validate = ("pidof ripd",) - ipv4_routing = True + name: str = "FRRRIP" + shutdown: Tuple[str, ...] = ("killall ripd",) + validate: Tuple[str, ...] = ("pidof ripd",) + ipv4_routing: bool = True @classmethod - def generate_frr_config(cls, node): + def generate_frr_config(cls, node: CoreNode) -> str: cfg = """\ router rip redistribute static @@ -566,14 +558,13 @@ class FRRRipng(FrrService): The RIP NG service provides IPv6 routing for wired networks. """ - name = "FRRRIPNG" - startup = () - shutdown = ("killall ripngd",) - validate = ("pidof ripngd",) - ipv6_routing = True + name: str = "FRRRIPNG" + shutdown: Tuple[str, ...] = ("killall ripngd",) + validate: Tuple[str, ...] = ("pidof ripngd",) + ipv6_routing: bool = True @classmethod - def generate_frr_config(cls, node): + def generate_frr_config(cls, node: CoreNode) -> str: cfg = """\ router ripng redistribute static @@ -591,14 +582,13 @@ class FRRBabel(FrrService): protocol for IPv6 and IPv4 with fast convergence properties. """ - name = "FRRBabel" - startup = () - shutdown = ("killall babeld",) - validate = ("pidof babeld",) - ipv6_routing = True + name: str = "FRRBabel" + shutdown: Tuple[str, ...] = ("killall babeld",) + validate: Tuple[str, ...] = ("pidof babeld",) + ipv6_routing: bool = True @classmethod - def generate_frr_config(cls, node): + def generate_frr_config(cls, node: CoreNode) -> str: cfg = "router babel\n" for iface in node.get_ifaces(control=False): cfg += " network %s\n" % iface.name @@ -606,7 +596,7 @@ class FRRBabel(FrrService): return cfg @classmethod - def generate_frr_iface_config(cls, node, iface): + def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: if iface.net and isinstance(iface.net, (EmaneNet, WlanNode)): return " babel wireless\n no babel split-horizon\n" else: @@ -618,14 +608,13 @@ class FRRpimd(FrrService): PIM multicast routing based on XORP. """ - name = "FRRpimd" - startup = () - shutdown = ("killall pimd",) - validate = ("pidof pimd",) - ipv4_routing = True + name: str = "FRRpimd" + shutdown: Tuple[str, ...] = ("killall pimd",) + validate: Tuple[str, ...] = ("pidof pimd",) + ipv4_routing: bool = True @classmethod - def generate_frr_config(cls, node): + def generate_frr_config(cls, node: CoreNode) -> str: ifname = "eth0" for iface in node.get_ifaces(): if iface.name != "lo": @@ -641,7 +630,7 @@ class FRRpimd(FrrService): return cfg @classmethod - def generate_frr_iface_config(cls, node, iface): + def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: return " ip mfea\n ip igmp\n ip pim\n" @@ -652,15 +641,14 @@ class FRRIsis(FrrService): unified frr.conf file. """ - name = "FRRISIS" - startup = () - shutdown = ("killall isisd",) - validate = ("pidof isisd",) - ipv4_routing = True - ipv6_routing = True + name: str = "FRRISIS" + shutdown: Tuple[str, ...] = ("killall isisd",) + validate: Tuple[str, ...] = ("pidof isisd",) + ipv4_routing: bool = True + ipv6_routing: bool = True @staticmethod - def ptpcheck(iface): + def ptp_check(iface: CoreInterface) -> str: """ Helper to detect whether interface is connected to a notional point-to-point link. @@ -670,7 +658,7 @@ class FRRIsis(FrrService): return "" @classmethod - def generate_frr_config(cls, node): + def generate_frr_config(cls, node: CoreNode) -> str: cfg = "router isis DEFAULT\n" cfg += " net 47.0001.0000.1900.%04x.00\n" % node.id cfg += " metric-style wide\n" @@ -679,9 +667,9 @@ class FRRIsis(FrrService): return cfg @classmethod - def generate_frr_iface_config(cls, node, iface): + def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: cfg = " ip router isis DEFAULT\n" cfg += " ipv6 router isis DEFAULT\n" cfg += " isis circuit-type level-2-only\n" - cfg += cls.ptpcheck(iface) + cfg += cls.ptp_check(iface) return cfg diff --git a/daemon/core/services/nrl.py b/daemon/core/services/nrl.py index 38b90d48..9933b130 100644 --- a/daemon/core/services/nrl.py +++ b/daemon/core/services/nrl.py @@ -2,9 +2,12 @@ nrl.py: defines services provided by NRL protolib tools hosted here: http://www.nrl.navy.mil/itd/ncs/products """ +from typing import Optional, Tuple + import netaddr from core import utils +from core.nodes.base import CoreNode from core.services.coreservices import CoreService @@ -14,19 +17,15 @@ class NrlService(CoreService): common to NRL's routing daemons. """ - name = None - group = "ProtoSvc" - dirs = () - configs = () - startup = () - shutdown = () + name: Optional[str] = None + group: str = "ProtoSvc" @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: return "" @staticmethod - def firstipv4prefix(node, prefixlen=24): + def firstipv4prefix(node: CoreNode, prefixlen: int = 24) -> str: """ Similar to QuaggaService.routerid(). Helper to return the first IPv4 prefix of a node, using the supplied prefix length. This ignores the @@ -37,20 +36,19 @@ class NrlService(CoreService): a = a.split("/")[0] if netaddr.valid_ipv4(a): return f"{a}/{prefixlen}" - # raise ValueError, "no IPv4 address found" return "0.0.0.0/%s" % prefixlen class MgenSinkService(NrlService): - name = "MGEN_Sink" - executables = ("mgen",) - configs = ("sink.mgen",) - startup = ("mgen input sink.mgen",) - validate = ("pidof mgen",) - shutdown = ("killall mgen",) + name: str = "MGEN_Sink" + executables: Tuple[str, ...] = ("mgen",) + configs: Tuple[str, ...] = ("sink.mgen",) + startup: Tuple[str, ...] = ("mgen input sink.mgen",) + validate: Tuple[str, ...] = ("pidof mgen",) + shutdown: Tuple[str, ...] = ("killall mgen",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: cfg = "0.0 LISTEN UDP 5000\n" for iface in node.get_ifaces(): name = utils.sysctl_devname(iface.name) @@ -58,7 +56,7 @@ class MgenSinkService(NrlService): return cfg @classmethod - def get_startup(cls, node): + def get_startup(cls, node: CoreNode) -> Tuple[str, ...]: cmd = cls.startup[0] cmd += " output /tmp/mgen_%s.log" % node.name return (cmd,) @@ -69,32 +67,29 @@ class NrlNhdp(NrlService): NeighborHood Discovery Protocol for MANET networks. """ - name = "NHDP" - executables = ("nrlnhdp",) - startup = ("nrlnhdp",) - shutdown = ("killall nrlnhdp",) - validate = ("pidof nrlnhdp",) + name: str = "NHDP" + executables: Tuple[str, ...] = ("nrlnhdp",) + startup: Tuple[str, ...] = ("nrlnhdp",) + shutdown: Tuple[str, ...] = ("killall nrlnhdp",) + validate: Tuple[str, ...] = ("pidof nrlnhdp",) @classmethod - def get_startup(cls, node): + def get_startup(cls, node: CoreNode) -> Tuple[str, ...]: """ Generate the appropriate command-line based on node interfaces. """ cmd = cls.startup[0] cmd += " -l /var/log/nrlnhdp.log" cmd += " -rpipe %s_nhdp" % node.name - servicenames = map(lambda x: x.name, node.services) if "SMF" in servicenames: cmd += " -flooding ecds" cmd += " -smfClient %s_smf" % node.name - ifaces = node.get_ifaces(control=False) if len(ifaces) > 0: iface_names = map(lambda x: x.name, ifaces) cmd += " -i " cmd += " -i ".join(iface_names) - return (cmd,) @@ -103,15 +98,15 @@ class NrlSmf(NrlService): Simplified Multicast Forwarding for MANET networks. """ - name = "SMF" - executables = ("nrlsmf",) - startup = ("sh startsmf.sh",) - shutdown = ("killall nrlsmf",) - validate = ("pidof nrlsmf",) - configs = ("startsmf.sh",) + name: str = "SMF" + executables: Tuple[str, ...] = ("nrlsmf",) + startup: Tuple[str, ...] = ("sh startsmf.sh",) + shutdown: Tuple[str, ...] = ("killall nrlsmf",) + validate: Tuple[str, ...] = ("pidof nrlsmf",) + configs: Tuple[str, ...] = ("startsmf.sh",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Generate a startup script for SMF. Because nrlsmf does not daemonize, it can cause problems in some situations when launched @@ -146,7 +141,6 @@ class NrlSmf(NrlService): cmd += " hash MD5" cmd += " log /var/log/nrlsmf.log" - cfg += comments + cmd + " < /dev/null > /dev/null 2>&1 &\n\n" return cfg @@ -156,14 +150,14 @@ class NrlOlsr(NrlService): Optimized Link State Routing protocol for MANET networks. """ - name = "OLSR" - executables = ("nrlolsrd",) - startup = ("nrlolsrd",) - shutdown = ("killall nrlolsrd",) - validate = ("pidof nrlolsrd",) + name: str = "OLSR" + executables: Tuple[str, ...] = ("nrlolsrd",) + startup: Tuple[str, ...] = ("nrlolsrd",) + shutdown: Tuple[str, ...] = ("killall nrlolsrd",) + validate: Tuple[str, ...] = ("pidof nrlolsrd",) @classmethod - def get_startup(cls, node): + def get_startup(cls, node: CoreNode) -> Tuple[str, ...]: """ Generate the appropriate command-line based on node interfaces. """ @@ -175,14 +169,12 @@ class NrlOlsr(NrlService): cmd += " -i %s" % iface.name cmd += " -l /var/log/nrlolsrd.log" cmd += " -rpipe %s_olsr" % node.name - servicenames = map(lambda x: x.name, node.services) if "SMF" in servicenames and "NHDP" not in servicenames: cmd += " -flooding s-mpr" cmd += " -smfClient %s_smf" % node.name if "zebra" in servicenames: cmd += " -z" - return (cmd,) @@ -191,34 +183,30 @@ class NrlOlsrv2(NrlService): Optimized Link State Routing protocol version 2 for MANET networks. """ - name = "OLSRv2" - executables = ("nrlolsrv2",) - startup = ("nrlolsrv2",) - shutdown = ("killall nrlolsrv2",) - validate = ("pidof nrlolsrv2",) + name: str = "OLSRv2" + executables: Tuple[str, ...] = ("nrlolsrv2",) + startup: Tuple[str, ...] = ("nrlolsrv2",) + shutdown: Tuple[str, ...] = ("killall nrlolsrv2",) + validate: Tuple[str, ...] = ("pidof nrlolsrv2",) @classmethod - def get_startup(cls, node): + def get_startup(cls, node: CoreNode) -> Tuple[str, ...]: """ Generate the appropriate command-line based on node interfaces. """ cmd = cls.startup[0] cmd += " -l /var/log/nrlolsrv2.log" cmd += " -rpipe %s_olsrv2" % node.name - servicenames = map(lambda x: x.name, node.services) if "SMF" in servicenames: cmd += " -flooding ecds" cmd += " -smfClient %s_smf" % node.name - cmd += " -p olsr" - ifaces = node.get_ifaces(control=False) if len(ifaces) > 0: iface_names = map(lambda x: x.name, ifaces) cmd += " -i " cmd += " -i ".join(iface_names) - return (cmd,) @@ -227,16 +215,16 @@ class OlsrOrg(NrlService): Optimized Link State Routing protocol from olsr.org for MANET networks. """ - name = "OLSRORG" - executables = ("olsrd",) - configs = ("/etc/olsrd/olsrd.conf",) - dirs = ("/etc/olsrd",) - startup = ("olsrd",) - shutdown = ("killall olsrd",) - validate = ("pidof olsrd",) + name: str = "OLSRORG" + executables: Tuple[str, ...] = ("olsrd",) + configs: Tuple[str, ...] = ("/etc/olsrd/olsrd.conf",) + dirs: Tuple[str, ...] = ("/etc/olsrd",) + startup: Tuple[str, ...] = ("olsrd",) + shutdown: Tuple[str, ...] = ("killall olsrd",) + validate: Tuple[str, ...] = ("pidof olsrd",) @classmethod - def get_startup(cls, node): + def get_startup(cls, node: CoreNode) -> Tuple[str, ...]: """ Generate the appropriate command-line based on node interfaces. """ @@ -246,13 +234,13 @@ class OlsrOrg(NrlService): iface_names = map(lambda x: x.name, ifaces) cmd += " -i " cmd += " -i ".join(iface_names) - return (cmd,) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ - Generate a default olsrd config file to use the broadcast address of 255.255.255.255. + Generate a default olsrd config file to use the broadcast address of + 255.255.255.255. """ cfg = """\ # @@ -577,24 +565,16 @@ class MgenActor(NrlService): """ # a unique name is required, without spaces - name = "MgenActor" - executables = ("mgen",) - # you can create your own group here - group = "ProtoSvc" - # per-node directories - dirs = () - # generated files (without a full path this file goes in the node's dir, - # e.g. /tmp/pycore.12345/n1.conf/) - configs = ("start_mgen_actor.sh",) - # list of startup commands, also may be generated during startup - startup = ("sh start_mgen_actor.sh",) - # list of validation commands - validate = ("pidof mgen",) - # list of shutdown commands - shutdown = ("killall mgen",) + name: str = "MgenActor" + group: str = "ProtoSvc" + executables: Tuple[str, ...] = ("mgen",) + configs: Tuple[str, ...] = ("start_mgen_actor.sh",) + startup: Tuple[str, ...] = ("sh start_mgen_actor.sh",) + validate: Tuple[str, ...] = ("pidof mgen",) + shutdown: Tuple[str, ...] = ("killall mgen",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Generate a startup script for MgenActor. Because mgenActor does not daemonize, it can cause problems in some situations when launched @@ -604,11 +584,9 @@ class MgenActor(NrlService): cfg += "# auto-generated by nrl.py:MgenActor.generateconfig()\n" comments = "" cmd = "mgenBasicActor.py -n %s -a 0.0.0.0" % node.name - ifaces = node.get_ifaces(control=False) if len(ifaces) == 0: return "" - cfg += comments + cmd + " < /dev/null > /dev/null 2>&1 &\n\n" return cfg @@ -618,15 +596,15 @@ class Arouted(NrlService): Adaptive Routing """ - name = "arouted" - executables = ("arouted",) - configs = ("startarouted.sh",) - startup = ("sh startarouted.sh",) - shutdown = ("pkill arouted",) - validate = ("pidof arouted",) + name: str = "arouted" + executables: Tuple[str, ...] = ("arouted",) + configs: Tuple[str, ...] = ("startarouted.sh",) + startup: Tuple[str, ...] = ("sh startarouted.sh",) + shutdown: Tuple[str, ...] = ("pkill arouted",) + validate: Tuple[str, ...] = ("pidof arouted",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return the Quagga.conf or quaggaboot.sh file contents. """ diff --git a/daemon/core/services/quagga.py b/daemon/core/services/quagga.py index 41cfa3d8..30d14353 100644 --- a/daemon/core/services/quagga.py +++ b/daemon/core/services/quagga.py @@ -1,65 +1,68 @@ """ quagga.py: defines routing services provided by Quagga. """ +from typing import Optional, Tuple + import netaddr from core import constants 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.network import PtpNet, WlanNode from core.nodes.physical import Rj45Node from core.services.coreservices import CoreService class Zebra(CoreService): - name = "zebra" - group = "Quagga" - dirs = ("/usr/local/etc/quagga", "/var/run/quagga") - configs = ( + name: str = "zebra" + group: str = "Quagga" + dirs: Tuple[str, ...] = ("/usr/local/etc/quagga", "/var/run/quagga") + configs: Tuple[str, ...] = ( "/usr/local/etc/quagga/Quagga.conf", "quaggaboot.sh", "/usr/local/etc/quagga/vtysh.conf", ) - startup = ("sh quaggaboot.sh zebra",) - shutdown = ("killall zebra",) - validate = ("pidof zebra",) + startup: Tuple[str, ...] = ("sh quaggaboot.sh zebra",) + shutdown: Tuple[str, ...] = ("killall zebra",) + validate: Tuple[str, ...] = ("pidof zebra",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return the Quagga.conf or quaggaboot.sh file contents. """ if filename == cls.configs[0]: - return cls.generateQuaggaConf(node) + return cls.generate_quagga_conf(node) elif filename == cls.configs[1]: - return cls.generateQuaggaBoot(node) + return cls.generate_quagga_boot(node) elif filename == cls.configs[2]: - return cls.generateVtyshConf(node) + return cls.generate_vtysh_conf(node) else: raise ValueError( "file name (%s) is not a known configuration: %s", filename, cls.configs ) @classmethod - def generateVtyshConf(cls, node): + def generate_vtysh_conf(cls, node: CoreNode) -> str: """ Returns configuration file text. """ return "service integrated-vtysh-config\n" @classmethod - def generateQuaggaConf(cls, node): + def generate_quagga_conf(cls, node: CoreNode) -> str: """ Returns configuration file text. Other services that depend on zebra - will have generatequaggaifcconfig() and generatequaggaconfig() - hooks that are invoked here. + will have hooks that are invoked here. """ # we could verify here that filename == Quagga.conf cfg = "" for iface in node.get_ifaces(): cfg += "interface %s\n" % iface.name # include control interfaces in addressing but not routing daemons - if hasattr(iface, "control") and iface.control is True: + if getattr(iface, "control", False): cfg += " " cfg += "\n ".join(map(cls.addrstr, iface.addrlist)) cfg += "\n" @@ -71,6 +74,8 @@ class Zebra(CoreService): for s in node.services: if cls.name not in s.dependencies: continue + if not (isinstance(s, QuaggaService) or issubclass(s, QuaggaService)): + continue iface_config = s.generate_quagga_iface_config(node, iface) if s.ipv4_routing: want_ipv4 = True @@ -101,11 +106,13 @@ class Zebra(CoreService): for s in node.services: if cls.name not in s.dependencies: continue + if not (isinstance(s, QuaggaService) or issubclass(s, QuaggaService)): + continue cfg += s.generate_quagga_config(node) return cfg @staticmethod - def addrstr(x): + def addrstr(x: str) -> str: """ helper for mapping IP addresses to zebra config statements """ @@ -118,7 +125,7 @@ class Zebra(CoreService): raise ValueError("invalid address: %s", x) @classmethod - def generateQuaggaBoot(cls, node): + def generate_quagga_boot(cls, node: CoreNode) -> str: """ Generate a shell script used to boot the Quagga daemons. """ @@ -235,20 +242,15 @@ class QuaggaService(CoreService): common to Quagga's routing daemons. """ - name = None - group = "Quagga" - dependencies = ("zebra",) - dirs = () - configs = () - startup = () - shutdown = () - meta = "The config file for this service can be found in the Zebra service." - - ipv4_routing = False - ipv6_routing = False + name: Optional[str] = None + group: str = "Quagga" + dependencies: Tuple[str, ...] = (Zebra.name,) + meta: str = "The config file for this service can be found in the Zebra service." + ipv4_routing: bool = False + ipv6_routing: bool = False @staticmethod - def routerid(node): + def router_id(node: CoreNode) -> str: """ Helper to return the first IPv4 address of a node as its router ID. """ @@ -257,11 +259,10 @@ class QuaggaService(CoreService): a = a.split("/")[0] if netaddr.valid_ipv4(a): return a - # raise ValueError, "no IPv4 address found for router ID" - return "0.0.0.%d" % node.id + return f"0.0.0.{node.id:d}" @staticmethod - def rj45check(iface): + def rj45check(iface: CoreInterface) -> bool: """ Helper to detect whether interface is connected an external RJ45 link. @@ -275,15 +276,15 @@ class QuaggaService(CoreService): return False @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: return "" @classmethod - def generate_quagga_iface_config(cls, node, iface): + def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: return "" @classmethod - def generate_quagga_config(cls, node): + def generate_quagga_config(cls, node: CoreNode) -> str: return "" @@ -294,14 +295,13 @@ class Ospfv2(QuaggaService): unified Quagga.conf file. """ - name = "OSPFv2" - startup = () - shutdown = ("killall ospfd",) - validate = ("pidof ospfd",) - ipv4_routing = True + name: str = "OSPFv2" + shutdown: Tuple[str, ...] = ("killall ospfd",) + validate: Tuple[str, ...] = ("pidof ospfd",) + ipv4_routing: bool = True @staticmethod - def mtucheck(iface): + def mtu_check(iface: CoreInterface) -> str: """ Helper to detect MTU mismatch and add the appropriate OSPF mtu-ignore command. This is needed when e.g. a node is linked via a @@ -319,7 +319,7 @@ class Ospfv2(QuaggaService): return "" @staticmethod - def ptpcheck(iface): + def ptp_check(iface: CoreInterface) -> str: """ Helper to detect whether interface is connected to a notional point-to-point link. @@ -329,9 +329,9 @@ class Ospfv2(QuaggaService): return "" @classmethod - def generate_quagga_config(cls, node): + def generate_quagga_config(cls, node: CoreNode) -> str: cfg = "router ospf\n" - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += " router-id %s\n" % rtrid # network 10.0.0.0/24 area 0 for iface in node.get_ifaces(control=False): @@ -343,12 +343,12 @@ class Ospfv2(QuaggaService): return cfg @classmethod - def generate_quagga_iface_config(cls, node, iface): - cfg = cls.mtucheck(iface) + def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: + cfg = cls.mtu_check(iface) # external RJ45 connections will use default OSPF timers if cls.rj45check(iface): return cfg - cfg += cls.ptpcheck(iface) + cfg += cls.ptp_check(iface) return ( cfg + """\ @@ -366,15 +366,14 @@ class Ospfv3(QuaggaService): unified Quagga.conf file. """ - name = "OSPFv3" - startup = () - shutdown = ("killall ospf6d",) - validate = ("pidof ospf6d",) - ipv4_routing = True - ipv6_routing = True + name: str = "OSPFv3" + shutdown: Tuple[str, ...] = ("killall ospf6d",) + validate: Tuple[str, ...] = ("pidof ospf6d",) + ipv4_routing: bool = True + ipv6_routing: bool = True @staticmethod - def minmtu(iface): + def min_mtu(iface: CoreInterface) -> int: """ Helper to discover the minimum MTU of interfaces linked with the given interface. @@ -388,20 +387,20 @@ class Ospfv3(QuaggaService): return mtu @classmethod - def mtucheck(cls, iface): + def mtu_check(cls, iface: CoreInterface) -> str: """ Helper to detect MTU mismatch and add the appropriate OSPFv3 ifmtu command. This is needed when e.g. a node is linked via a GreTap device. """ - minmtu = cls.minmtu(iface) + minmtu = cls.min_mtu(iface) if minmtu < iface.mtu: return " ipv6 ospf6 ifmtu %d\n" % minmtu else: return "" @staticmethod - def ptpcheck(iface): + def ptp_check(iface: CoreInterface) -> str: """ Helper to detect whether interface is connected to a notional point-to-point link. @@ -411,9 +410,9 @@ class Ospfv3(QuaggaService): return "" @classmethod - def generate_quagga_config(cls, node): + def generate_quagga_config(cls, node: CoreNode) -> str: cfg = "router ospf6\n" - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += " instance-id 65\n" cfg += " router-id %s\n" % rtrid for iface in node.get_ifaces(control=False): @@ -422,8 +421,8 @@ class Ospfv3(QuaggaService): return cfg @classmethod - def generate_quagga_iface_config(cls, node, iface): - return cls.mtucheck(iface) + def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: + return cls.mtu_check(iface) class Ospfv3mdr(Ospfv3): @@ -434,12 +433,12 @@ class Ospfv3mdr(Ospfv3): unified Quagga.conf file. """ - name = "OSPFv3MDR" - ipv4_routing = True + name: str = "OSPFv3MDR" + ipv4_routing: bool = True @classmethod - def generate_quagga_iface_config(cls, node, iface): - cfg = cls.mtucheck(iface) + def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: + cfg = cls.mtu_check(iface) if iface.net is not None and isinstance(iface.net, (WlanNode, EmaneNet)): return ( cfg @@ -464,21 +463,20 @@ class Bgp(QuaggaService): having the same AS number. """ - name = "BGP" - startup = () - shutdown = ("killall bgpd",) - validate = ("pidof bgpd",) - custom_needed = True - ipv4_routing = True - ipv6_routing = True + name: str = "BGP" + shutdown: Tuple[str, ...] = ("killall bgpd",) + validate: Tuple[str, ...] = ("pidof bgpd",) + custom_needed: bool = True + ipv4_routing: bool = True + ipv6_routing: bool = True @classmethod - def generate_quagga_config(cls, node): + def generate_quagga_config(cls, node: CoreNode) -> str: cfg = "!\n! BGP configuration\n!\n" cfg += "! You should configure the AS number below,\n" cfg += "! along with this router's peers.\n!\n" cfg += "router bgp %s\n" % node.id - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += " bgp router-id %s\n" % rtrid cfg += " redistribute connected\n" cfg += "! neighbor 1.2.3.4 remote-as 555\n!\n" @@ -490,14 +488,13 @@ class Rip(QuaggaService): The RIP service provides IPv4 routing for wired networks. """ - name = "RIP" - startup = () - shutdown = ("killall ripd",) - validate = ("pidof ripd",) - ipv4_routing = True + name: str = "RIP" + shutdown: Tuple[str, ...] = ("killall ripd",) + validate: Tuple[str, ...] = ("pidof ripd",) + ipv4_routing: bool = True @classmethod - def generate_quagga_config(cls, node): + def generate_quagga_config(cls, node: CoreNode) -> str: cfg = """\ router rip redistribute static @@ -514,14 +511,13 @@ class Ripng(QuaggaService): The RIP NG service provides IPv6 routing for wired networks. """ - name = "RIPNG" - startup = () - shutdown = ("killall ripngd",) - validate = ("pidof ripngd",) - ipv6_routing = True + name: str = "RIPNG" + shutdown: Tuple[str, ...] = ("killall ripngd",) + validate: Tuple[str, ...] = ("pidof ripngd",) + ipv6_routing: bool = True @classmethod - def generate_quagga_config(cls, node): + def generate_quagga_config(cls, node: CoreNode) -> str: cfg = """\ router ripng redistribute static @@ -539,14 +535,13 @@ class Babel(QuaggaService): protocol for IPv6 and IPv4 with fast convergence properties. """ - name = "Babel" - startup = () - shutdown = ("killall babeld",) - validate = ("pidof babeld",) - ipv6_routing = True + name: str = "Babel" + shutdown: Tuple[str, ...] = ("killall babeld",) + validate: Tuple[str, ...] = ("pidof babeld",) + ipv6_routing: bool = True @classmethod - def generate_quagga_config(cls, node): + def generate_quagga_config(cls, node: CoreNode) -> str: cfg = "router babel\n" for iface in node.get_ifaces(control=False): cfg += " network %s\n" % iface.name @@ -554,7 +549,7 @@ class Babel(QuaggaService): return cfg @classmethod - def generate_quagga_iface_config(cls, node, iface): + def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: if iface.net and iface.net.linktype == LinkTypes.WIRELESS: return " babel wireless\n no babel split-horizon\n" else: @@ -566,14 +561,13 @@ class Xpimd(QuaggaService): PIM multicast routing based on XORP. """ - name = "Xpimd" - startup = () - shutdown = ("killall xpimd",) - validate = ("pidof xpimd",) - ipv4_routing = True + name: str = "Xpimd" + shutdown: Tuple[str, ...] = ("killall xpimd",) + validate: Tuple[str, ...] = ("pidof xpimd",) + ipv4_routing: bool = True @classmethod - def generate_quagga_config(cls, node): + def generate_quagga_config(cls, node: CoreNode) -> str: ifname = "eth0" for iface in node.get_ifaces(): if iface.name != "lo": @@ -589,5 +583,5 @@ class Xpimd(QuaggaService): return cfg @classmethod - def generate_quagga_iface_config(cls, node, iface): + def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str: return " ip mfea\n ip igmp\n ip pim\n" diff --git a/daemon/core/services/sdn.py b/daemon/core/services/sdn.py index 71ab815f..1f17201d 100644 --- a/daemon/core/services/sdn.py +++ b/daemon/core/services/sdn.py @@ -3,9 +3,11 @@ sdn.py defines services to start Open vSwitch and the Ryu SDN Controller. """ import re +from typing import Tuple import netaddr +from core.nodes.base import CoreNode from core.services.coreservices import CoreService @@ -14,24 +16,28 @@ class SdnService(CoreService): Parent class for SDN services. """ - group = "SDN" + group: str = "SDN" @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: return "" class OvsService(SdnService): - name = "OvsService" - executables = ("ovs-ofctl", "ovs-vsctl") - group = "SDN" - dirs = ("/etc/openvswitch", "/var/run/openvswitch", "/var/log/openvswitch") - configs = ("OvsService.sh",) - startup = ("sh OvsService.sh",) - shutdown = ("killall ovs-vswitchd", "killall ovsdb-server") + name: str = "OvsService" + group: str = "SDN" + executables: Tuple[str, ...] = ("ovs-ofctl", "ovs-vsctl") + dirs: Tuple[str, ...] = ( + "/etc/openvswitch", + "/var/run/openvswitch", + "/var/log/openvswitch", + ) + configs: Tuple[str, ...] = ("OvsService.sh",) + startup: Tuple[str, ...] = ("sh OvsService.sh",) + shutdown: Tuple[str, ...] = ("killall ovs-vswitchd", "killall ovsdb-server") @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: # Check whether the node is running zebra has_zebra = 0 for s in node.services: @@ -46,8 +52,8 @@ class OvsService(SdnService): cfg += "## this stops it from routing traffic without defined flows.\n" cfg += "## remove the -- and everything after if you want it to act as a regular switch\n" cfg += "ovs-vsctl add-br ovsbr0 -- set Bridge ovsbr0 fail-mode=secure\n" - cfg += "\n## Now add all our interfaces as ports to the switch\n" + portnum = 1 for iface in node.get_ifaces(control=False): ifnumstr = re.findall(r"\d+", iface.name) @@ -111,21 +117,19 @@ class OvsService(SdnService): % (portnum + 1, portnum) ) portnum += 2 - return cfg class RyuService(SdnService): - name = "ryuService" - executables = ("ryu-manager",) - group = "SDN" - dirs = () - configs = ("ryuService.sh",) - startup = ("sh ryuService.sh",) - shutdown = ("killall ryu-manager",) + name: str = "ryuService" + group: str = "SDN" + executables: Tuple[str, ...] = ("ryu-manager",) + configs: Tuple[str, ...] = ("ryuService.sh",) + startup: Tuple[str, ...] = ("sh ryuService.sh",) + shutdown: Tuple[str, ...] = ("killall ryu-manager",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return a string that will be written to filename, or sent to the GUI for user customization. diff --git a/daemon/core/services/security.py b/daemon/core/services/security.py index 91c942f1..b813579e 100644 --- a/daemon/core/services/security.py +++ b/daemon/core/services/security.py @@ -4,78 +4,79 @@ firewall) """ import logging +from typing import Tuple from core import constants +from core.nodes.base import CoreNode +from core.nodes.interface import CoreInterface from core.services.coreservices import CoreService class VPNClient(CoreService): - name = "VPNClient" - group = "Security" - configs = ("vpnclient.sh",) - startup = ("sh vpnclient.sh",) - shutdown = ("killall openvpn",) - validate = ("pidof openvpn",) - custom_needed = True + name: str = "VPNClient" + group: str = "Security" + configs: Tuple[str, ...] = ("vpnclient.sh",) + startup: Tuple[str, ...] = ("sh vpnclient.sh",) + shutdown: Tuple[str, ...] = ("killall openvpn",) + validate: Tuple[str, ...] = ("pidof openvpn",) + custom_needed: bool = True @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return the client.conf and vpnclient.sh file contents to """ cfg = "#!/bin/sh\n" cfg += "# custom VPN Client configuration for service (security.py)\n" - fname = "%s/examples/services/sampleVPNClient" % constants.CORE_DATA_DIR - + fname = f"{constants.CORE_DATA_DIR}/examples/services/sampleVPNClient" try: - cfg += open(fname, "rb").read() + with open(fname, "r") as f: + cfg += f.read() except IOError: logging.exception( - "Error opening VPN client configuration template (%s)", fname + "error opening VPN client configuration template (%s)", fname ) - return cfg class VPNServer(CoreService): - name = "VPNServer" - group = "Security" - configs = ("vpnserver.sh",) - startup = ("sh vpnserver.sh",) - shutdown = ("killall openvpn",) - validate = ("pidof openvpn",) - custom_needed = True + name: str = "VPNServer" + group: str = "Security" + configs: Tuple[str, ...] = ("vpnserver.sh",) + startup: Tuple[str, ...] = ("sh vpnserver.sh",) + shutdown: Tuple[str, ...] = ("killall openvpn",) + validate: Tuple[str, ...] = ("pidof openvpn",) + custom_needed: bool = True @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return the sample server.conf and vpnserver.sh file contents to GUI for user customization. """ cfg = "#!/bin/sh\n" cfg += "# custom VPN Server Configuration for service (security.py)\n" - fname = "%s/examples/services/sampleVPNServer" % constants.CORE_DATA_DIR - + fname = f"{constants.CORE_DATA_DIR}/examples/services/sampleVPNServer" try: - cfg += open(fname, "rb").read() + with open(fname, "r") as f: + cfg += f.read() except IOError: logging.exception( "Error opening VPN server configuration template (%s)", fname ) - return cfg class IPsec(CoreService): - name = "IPsec" - group = "Security" - configs = ("ipsec.sh",) - startup = ("sh ipsec.sh",) - shutdown = ("killall racoon",) - custom_needed = True + name: str = "IPsec" + group: str = "Security" + configs: Tuple[str, ...] = ("ipsec.sh",) + startup: Tuple[str, ...] = ("sh ipsec.sh",) + shutdown: Tuple[str, ...] = ("killall racoon",) + custom_needed: bool = True @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return the ipsec.conf and racoon.conf file contents to GUI for user customization. @@ -83,7 +84,7 @@ class IPsec(CoreService): cfg = "#!/bin/sh\n" cfg += "# set up static tunnel mode security assocation for service " cfg += "(security.py)\n" - fname = "%s/examples/services/sampleIPsec" % constants.CORE_DATA_DIR + fname = f"{constants.CORE_DATA_DIR}/examples/services/sampleIPsec" try: with open(fname, "r") as f: cfg += f.read() @@ -93,28 +94,27 @@ class IPsec(CoreService): class Firewall(CoreService): - name = "Firewall" - group = "Security" - configs = ("firewall.sh",) - startup = ("sh firewall.sh",) - custom_needed = True + name: str = "Firewall" + group: str = "Security" + configs: Tuple[str, ...] = ("firewall.sh",) + startup: Tuple[str, ...] = ("sh firewall.sh",) + custom_needed: bool = True @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return the firewall rule examples to GUI for user customization. """ cfg = "#!/bin/sh\n" cfg += "# custom node firewall rules for service (security.py)\n" - fname = "%s/examples/services/sampleFirewall" % constants.CORE_DATA_DIR - + fname = f"{constants.CORE_DATA_DIR}/examples/services/sampleFirewall" try: - cfg += open(fname, "rb").read() + with open(fname, "r") as f: + cfg += f.read() except IOError: logging.exception( "Error opening Firewall configuration template (%s)", fname ) - return cfg @@ -123,30 +123,28 @@ class Nat(CoreService): IPv4 source NAT service. """ - name = "NAT" - executables = ("iptables",) - group = "Security" - configs = ("nat.sh",) - startup = ("sh nat.sh",) - custom_needed = False + name: str = "NAT" + group: str = "Security" + executables: Tuple[str, ...] = ("iptables",) + configs: Tuple[str, ...] = ("nat.sh",) + startup: Tuple[str, ...] = ("sh nat.sh",) + custom_needed: bool = False @classmethod - def generate_iface_nat_rule(cls, iface, line_prefix=""): + def generate_iface_nat_rule(cls, iface: CoreInterface, prefix: str = "") -> str: """ Generate a NAT line for one interface. """ - cfg = line_prefix + "iptables -t nat -A POSTROUTING -o " + cfg = prefix + "iptables -t nat -A POSTROUTING -o " cfg += iface.name + " -j MASQUERADE\n" - - cfg += line_prefix + "iptables -A FORWARD -i " + iface.name + cfg += prefix + "iptables -A FORWARD -i " + iface.name cfg += " -m state --state RELATED,ESTABLISHED -j ACCEPT\n" - - cfg += line_prefix + "iptables -A FORWARD -i " + cfg += prefix + "iptables -A FORWARD -i " cfg += iface.name + " -j DROP\n" return cfg @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ NAT out the first interface """ @@ -156,7 +154,7 @@ class Nat(CoreService): have_nat = False for iface in node.get_ifaces(control=False): if have_nat: - cfg += cls.generate_iface_nat_rule(iface, line_prefix="#") + cfg += cls.generate_iface_nat_rule(iface, prefix="#") else: have_nat = True cfg += "# NAT out the " + iface.name + " interface\n" diff --git a/daemon/core/services/ucarp.py b/daemon/core/services/ucarp.py index 1eb80179..8ac92dd3 100644 --- a/daemon/core/services/ucarp.py +++ b/daemon/core/services/ucarp.py @@ -1,52 +1,52 @@ """ ucarp.py: defines high-availability IP address controlled by ucarp """ +from typing import Tuple +from core.nodes.base import CoreNode from core.services.coreservices import CoreService UCARP_ETC = "/usr/local/etc/ucarp" class Ucarp(CoreService): - name = "ucarp" - group = "Utility" - dirs = (UCARP_ETC,) - configs = ( + name: str = "ucarp" + group: str = "Utility" + dirs: Tuple[str, ...] = (UCARP_ETC,) + configs: Tuple[str, ...] = ( UCARP_ETC + "/default.sh", UCARP_ETC + "/default-up.sh", UCARP_ETC + "/default-down.sh", "ucarpboot.sh", ) - startup = ("sh ucarpboot.sh",) - shutdown = ("killall ucarp",) - validate = ("pidof ucarp",) + startup: Tuple[str, ...] = ("sh ucarpboot.sh",) + shutdown: Tuple[str, ...] = ("killall ucarp",) + validate: Tuple[str, ...] = ("pidof ucarp",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Return the default file contents """ if filename == cls.configs[0]: - return cls.generateUcarpConf(node) + return cls.generate_ucarp_conf(node) elif filename == cls.configs[1]: - return cls.generateVipUp(node) + return cls.generate_vip_up(node) elif filename == cls.configs[2]: - return cls.generateVipDown(node) + return cls.generate_vip_down(node) elif filename == cls.configs[3]: - return cls.generateUcarpBoot(node) + return cls.generate_ucarp_boot(node) else: raise ValueError @classmethod - def generateUcarpConf(cls, node): + def generate_ucarp_conf(cls, node: CoreNode) -> str: """ Returns configuration file text. """ - try: - ucarp_bin = node.session.cfg["ucarp_bin"] - except KeyError: - ucarp_bin = "/usr/sbin/ucarp" - + ucarp_bin = node.session.options.get_config( + "ucarp_bin", default="/usr/sbin/ucarp" + ) return """\ #!/bin/sh # Location of UCARP executable @@ -110,7 +110,7 @@ ${UCARP_EXEC} -B ${UCARP_OPTS} ) @classmethod - def generateUcarpBoot(cls, node): + def generate_ucarp_boot(cls, node: CoreNode) -> str: """ Generate a shell script used to boot the Ucarp daemons. """ @@ -130,7 +130,7 @@ ${UCARP_CFGDIR}/default.sh ) @classmethod - def generateVipUp(cls, node): + def generate_vip_up(cls, node: CoreNode) -> str: """ Generate a shell script used to start the virtual ip """ @@ -152,7 +152,7 @@ fi """ @classmethod - def generateVipDown(cls, node): + def generate_vip_down(cls, node: CoreNode) -> str: """ Generate a shell script used to stop the virtual ip """ diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index 273318e1..a44037f6 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -1,12 +1,13 @@ """ utility.py: defines miscellaneous utility services. """ -import os +from typing import Optional, Tuple import netaddr from core import constants, utils from core.errors import CoreCommandError +from core.nodes.base import CoreNode from core.services.coreservices import CoreService, ServiceMode @@ -15,32 +16,25 @@ class UtilService(CoreService): Parent class for utility services. """ - name = None - group = "Utility" - dirs = () - configs = () - startup = () - shutdown = () + name: Optional[str] = None + group: str = "Utility" @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: return "" class IPForwardService(UtilService): - name = "IPForward" - configs = ("ipforward.sh",) - startup = ("sh ipforward.sh",) + name: str = "IPForward" + configs: Tuple[str, ...] = ("ipforward.sh",) + startup: Tuple[str, ...] = ("sh ipforward.sh",) @classmethod - def generate_config(cls, node, filename): - if os.uname()[0] == "Linux": - return cls.generateconfiglinux(node, filename) - else: - raise Exception("unknown platform") + def generate_config(cls, node: CoreNode, filename: str) -> str: + return cls.generateconfiglinux(node, filename) @classmethod - def generateconfiglinux(cls, node, filename): + def generateconfiglinux(cls, node: CoreNode, filename: str) -> str: cfg = """\ #!/bin/sh # auto-generated by IPForward service (utility.py) @@ -70,12 +64,12 @@ class IPForwardService(UtilService): class DefaultRouteService(UtilService): - name = "DefaultRoute" - configs = ("defaultroute.sh",) - startup = ("sh defaultroute.sh",) + name: str = "DefaultRoute" + configs: Tuple[str, ...] = ("defaultroute.sh",) + startup: Tuple[str, ...] = ("sh defaultroute.sh",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: routes = [] ifaces = node.get_ifaces() if ifaces: @@ -93,22 +87,18 @@ class DefaultRouteService(UtilService): class DefaultMulticastRouteService(UtilService): - name = "DefaultMulticastRoute" - configs = ("defaultmroute.sh",) - startup = ("sh defaultmroute.sh",) + name: str = "DefaultMulticastRoute" + configs: Tuple[str, ...] = ("defaultmroute.sh",) + startup: Tuple[str, ...] = ("sh defaultmroute.sh",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: cfg = "#!/bin/sh\n" cfg += "# auto-generated by DefaultMulticastRoute service (utility.py)\n" cfg += "# the first interface is chosen below; please change it " cfg += "as needed\n" - for iface in node.get_ifaces(control=False): - if os.uname()[0] == "Linux": - rtcmd = "ip route add 224.0.0.0/4 dev" - else: - raise Exception("unknown platform") + rtcmd = "ip route add 224.0.0.0/4 dev" cfg += "%s %s\n" % (rtcmd, iface.name) cfg += "\n" break @@ -116,13 +106,13 @@ class DefaultMulticastRouteService(UtilService): class StaticRouteService(UtilService): - name = "StaticRoute" - configs = ("staticroute.sh",) - startup = ("sh staticroute.sh",) - custom_needed = True + name: str = "StaticRoute" + configs: Tuple[str, ...] = ("staticroute.sh",) + startup: Tuple[str, ...] = ("sh staticroute.sh",) + custom_needed: bool = True @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: cfg = "#!/bin/sh\n" cfg += "# auto-generated by StaticRoute service (utility.py)\n#\n" cfg += "# NOTE: this service must be customized to be of any use\n" @@ -133,7 +123,7 @@ class StaticRouteService(UtilService): return cfg @staticmethod - def routestr(x): + def routestr(x: str) -> str: addr = x.split("/")[0] if netaddr.valid_ipv6(addr): dst = "3ffe:4::/64" @@ -143,24 +133,20 @@ class StaticRouteService(UtilService): if net[-2] == net[1]: return "" else: - if os.uname()[0] == "Linux": - rtcmd = "#/sbin/ip route add %s via" % dst - else: - raise Exception("unknown platform") + rtcmd = "#/sbin/ip route add %s via" % dst return "%s %s" % (rtcmd, net[1]) class SshService(UtilService): - name = "SSH" - configs = ("startsshd.sh", "/etc/ssh/sshd_config") - dirs = ("/etc/ssh", "/var/run/sshd") - startup = ("sh startsshd.sh",) - shutdown = ("killall sshd",) - validate = () - validation_mode = ServiceMode.BLOCKING + name: str = "SSH" + configs: Tuple[str, ...] = ("startsshd.sh", "/etc/ssh/sshd_config") + dirs: Tuple[str, ...] = ("/etc/ssh", "/var/run/sshd") + startup: Tuple[str, ...] = ("sh startsshd.sh",) + shutdown: Tuple[str, ...] = ("killall sshd",) + validation_mode: ServiceMode = ServiceMode.BLOCKING @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Use a startup script for launching sshd in order to wait for host key generation. @@ -228,15 +214,15 @@ UseDNS no class DhcpService(UtilService): - name = "DHCP" - configs = ("/etc/dhcp/dhcpd.conf",) - dirs = ("/etc/dhcp", "/var/lib/dhcp") - startup = ("touch /var/lib/dhcp/dhcpd.leases", "dhcpd") - shutdown = ("killall dhcpd",) - validate = ("pidof dhcpd",) + name: str = "DHCP" + configs: Tuple[str, ...] = ("/etc/dhcp/dhcpd.conf",) + dirs: Tuple[str, ...] = ("/etc/dhcp", "/var/lib/dhcp") + startup: Tuple[str, ...] = ("touch /var/lib/dhcp/dhcpd.leases", "dhcpd") + shutdown: Tuple[str, ...] = ("killall dhcpd",) + validate: Tuple[str, ...] = ("pidof dhcpd",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Generate a dhcpd config file using the network address of each interface. @@ -261,7 +247,7 @@ ddns-update-style none; return cfg @staticmethod - def subnetentry(x): + def subnetentry(x: str) -> str: """ Generate a subnet declaration block given an IPv4 prefix string for inclusion in the dhcpd3 config file. @@ -297,14 +283,14 @@ class DhcpClientService(UtilService): Use a DHCP client for all interfaces for addressing. """ - name = "DHCPClient" - configs = ("startdhcpclient.sh",) - startup = ("sh startdhcpclient.sh",) - shutdown = ("killall dhclient",) - validate = ("pidof dhclient",) + name: str = "DHCPClient" + configs: Tuple[str, ...] = ("startdhcpclient.sh",) + startup: Tuple[str, ...] = ("sh startdhcpclient.sh",) + shutdown: Tuple[str, ...] = ("killall dhclient",) + validate: Tuple[str, ...] = ("pidof dhclient",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Generate a script to invoke dhclient on all interfaces. """ @@ -313,7 +299,6 @@ class DhcpClientService(UtilService): cfg += "# uncomment this mkdir line and symlink line to enable client-" cfg += "side DNS\n# resolution based on the DHCP server response.\n" cfg += "#mkdir -p /var/run/resolvconf/interface\n" - for iface in node.get_ifaces(control=False): cfg += "#ln -s /var/run/resolvconf/interface/%s.dhclient" % iface.name cfg += " /var/run/resolvconf/resolv.conf\n" @@ -327,15 +312,15 @@ class FtpService(UtilService): Start a vsftpd server. """ - name = "FTP" - configs = ("vsftpd.conf",) - dirs = ("/var/run/vsftpd/empty", "/var/ftp") - startup = ("vsftpd ./vsftpd.conf",) - shutdown = ("killall vsftpd",) - validate = ("pidof vsftpd",) + name: str = "FTP" + configs: Tuple[str, ...] = ("vsftpd.conf",) + dirs: Tuple[str, ...] = ("/var/run/vsftpd/empty", "/var/ftp") + startup: Tuple[str, ...] = ("vsftpd ./vsftpd.conf",) + shutdown: Tuple[str, ...] = ("killall vsftpd",) + validate: Tuple[str, ...] = ("pidof vsftpd",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Generate a vsftpd.conf configuration file. """ @@ -360,13 +345,13 @@ class HttpService(UtilService): Start an apache server. """ - name = "HTTP" - configs = ( + name: str = "HTTP" + configs: Tuple[str, ...] = ( "/etc/apache2/apache2.conf", "/etc/apache2/envvars", "/var/www/index.html", ) - dirs = ( + dirs: Tuple[str, ...] = ( "/etc/apache2", "/var/run/apache2", "/var/log/apache2", @@ -374,14 +359,14 @@ class HttpService(UtilService): "/var/lock/apache2", "/var/www", ) - startup = ("chown www-data /var/lock/apache2", "apache2ctl start") - shutdown = ("apache2ctl stop",) - validate = ("pidof apache2",) - - APACHEVER22, APACHEVER24 = (22, 24) + startup: Tuple[str, ...] = ("chown www-data /var/lock/apache2", "apache2ctl start") + shutdown: Tuple[str, ...] = ("apache2ctl stop",) + validate: Tuple[str, ...] = ("pidof apache2",) + APACHEVER22: int = 22 + APACHEVER24: int = 24 @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Generate an apache2.conf configuration file. """ @@ -395,7 +380,7 @@ class HttpService(UtilService): return "" @classmethod - def detectversionfromcmd(cls): + def detectversionfromcmd(cls) -> int: """ Detect the apache2 version using the 'a2query' command. """ @@ -405,14 +390,12 @@ class HttpService(UtilService): except CoreCommandError as e: status = e.returncode result = e.stderr - if status == 0 and result[:3] == "2.4": return cls.APACHEVER24 - return cls.APACHEVER22 @classmethod - def generateapache2conf(cls, node, filename): + def generateapache2conf(cls, node: CoreNode, filename: str) -> str: lockstr = { cls.APACHEVER22: "LockFile ${APACHE_LOCK_DIR}/accept.lock\n", cls.APACHEVER24: "Mutex file:${APACHE_LOCK_DIR} default\n", @@ -421,22 +404,18 @@ class HttpService(UtilService): cls.APACHEVER22: "", cls.APACHEVER24: "LoadModule mpm_worker_module /usr/lib/apache2/modules/mod_mpm_worker.so\n", } - permstr = { cls.APACHEVER22: " Order allow,deny\n Deny from all\n Satisfy all\n", cls.APACHEVER24: " Require all denied\n", } - authstr = { cls.APACHEVER22: "LoadModule authz_default_module /usr/lib/apache2/modules/mod_authz_default.so\n", cls.APACHEVER24: "LoadModule authz_core_module /usr/lib/apache2/modules/mod_authz_core.so\n", } - permstr2 = { cls.APACHEVER22: "\t\tOrder allow,deny\n\t\tallow from all\n", cls.APACHEVER24: "\t\tRequire all granted\n", } - version = cls.detectversionfromcmd() cfg = "# apache2.conf generated by utility.py:HttpService\n" cfg += lockstr[version] @@ -552,7 +531,7 @@ TraceEnable Off return cfg @classmethod - def generateenvvars(cls, node, filename): + def generateenvvars(cls, node: CoreNode, filename: str) -> str: return """\ # this file is used by apache2ctl - generated by utility.py:HttpService # these settings come from a default Ubuntu apache2 installation @@ -567,7 +546,7 @@ export LANG """ @classmethod - def generatehtml(cls, node, filename): + def generatehtml(cls, node: CoreNode, filename: str) -> str: body = ( """\ @@ -587,16 +566,15 @@ class PcapService(UtilService): Pcap service for logging packets. """ - name = "pcap" - configs = ("pcap.sh",) - dirs = () - startup = ("sh pcap.sh start",) - shutdown = ("sh pcap.sh stop",) - validate = ("pidof tcpdump",) - meta = "logs network traffic to pcap packet capture files" + name: str = "pcap" + configs: Tuple[str, ...] = ("pcap.sh",) + startup: Tuple[str, ...] = ("sh pcap.sh start",) + shutdown: Tuple[str, ...] = ("sh pcap.sh stop",) + validate: Tuple[str, ...] = ("pidof tcpdump",) + meta: str = "logs network traffic to pcap packet capture files" @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Generate a startpcap.sh traffic logging script. """ @@ -630,15 +608,17 @@ fi; class RadvdService(UtilService): - name = "radvd" - configs = ("/etc/radvd/radvd.conf",) - dirs = ("/etc/radvd",) - startup = ("radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log",) - shutdown = ("pkill radvd",) - validate = ("pidof radvd",) + name: str = "radvd" + configs: Tuple[str, ...] = ("/etc/radvd/radvd.conf",) + dirs: Tuple[str, ...] = ("/etc/radvd",) + startup: Tuple[str, ...] = ( + "radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log", + ) + shutdown: Tuple[str, ...] = ("pkill radvd",) + validate: Tuple[str, ...] = ("pidof radvd",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Generate a RADVD router advertisement daemon config file using the network address of each interface. @@ -678,7 +658,7 @@ interface %s return cfg @staticmethod - def subnetentry(x): + def subnetentry(x: str) -> str: """ Generate a subnet declaration block given an IPv6 prefix string for inclusion in the RADVD config file. @@ -695,14 +675,14 @@ class AtdService(UtilService): Atd service for scheduling at jobs """ - name = "atd" - configs = ("startatd.sh",) - dirs = ("/var/spool/cron/atjobs", "/var/spool/cron/atspool") - startup = ("sh startatd.sh",) - shutdown = ("pkill atd",) + name: str = "atd" + configs: Tuple[str, ...] = ("startatd.sh",) + dirs: Tuple[str, ...] = ("/var/spool/cron/atjobs", "/var/spool/cron/atspool") + startup: Tuple[str, ...] = ("sh startatd.sh",) + shutdown: Tuple[str, ...] = ("pkill atd",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: return """ #!/bin/sh echo 00001 > /var/spool/cron/atjobs/.SEQ @@ -717,5 +697,5 @@ class UserDefinedService(UtilService): Dummy service allowing customization of anything. """ - name = "UserDefined" - meta = "Customize this service to do anything upon startup." + name: str = "UserDefined" + meta: str = "Customize this service to do anything upon startup." diff --git a/daemon/core/services/xorp.py b/daemon/core/services/xorp.py index 776b1d16..42082377 100644 --- a/daemon/core/services/xorp.py +++ b/daemon/core/services/xorp.py @@ -2,10 +2,12 @@ xorp.py: defines routing services provided by the XORP routing suite. """ -import logging +from typing import Optional, Tuple import netaddr +from core.nodes.base import CoreNode +from core.nodes.interface import CoreInterface from core.services.coreservices import CoreService @@ -15,20 +17,20 @@ class XorpRtrmgr(CoreService): enabled XORP services, and launches necessary daemons upon startup. """ - name = "xorp_rtrmgr" - executables = ("xorp_rtrmgr",) - group = "XORP" - dirs = ("/etc/xorp",) - configs = ("/etc/xorp/config.boot",) - startup = ( + name: str = "xorp_rtrmgr" + group: str = "XORP" + executables: Tuple[str, ...] = ("xorp_rtrmgr",) + dirs: Tuple[str, ...] = ("/etc/xorp",) + configs: Tuple[str, ...] = ("/etc/xorp/config.boot",) + startup: Tuple[str, ...] = ( "xorp_rtrmgr -d -b %s -l /var/log/%s.log -P /var/run/%s.pid" % (configs[0], name, name), ) - shutdown = ("killall xorp_rtrmgr",) - validate = ("pidof xorp_rtrmgr",) + shutdown: Tuple[str, ...] = ("killall xorp_rtrmgr",) + validate: Tuple[str, ...] = ("pidof xorp_rtrmgr",) @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ Returns config.boot configuration file text. Other services that depend on this will have generatexorpconfig() hooks that are @@ -45,16 +47,15 @@ class XorpRtrmgr(CoreService): cfg += "}\n\n" for s in node.services: - try: - s.dependencies.index(cls.name) - cfg += s.generatexorpconfig(node) - except ValueError: - logging.exception("error getting value from service: %s", cls.name) - + if cls.name not in s.dependencies: + continue + if not (isinstance(s, XorpService) or issubclass(s, XorpService)): + continue + cfg += s.generate_xorp_config(node) return cfg @staticmethod - def addrstr(x): + def addrstr(x: str) -> str: """ helper for mapping IP addresses to XORP config statements """ @@ -65,7 +66,7 @@ class XorpRtrmgr(CoreService): return cfg @staticmethod - def lladdrstr(iface): + def lladdrstr(iface: CoreInterface) -> str: """ helper for adding link-local address entries (required by OSPFv3) """ @@ -81,18 +82,16 @@ class XorpService(CoreService): common to XORP's routing daemons. """ - name = None - executables = ("xorp_rtrmgr",) - group = "XORP" - dependencies = ("xorp_rtrmgr",) - dirs = () - configs = () - startup = () - shutdown = () - meta = "The config file for this service can be found in the xorp_rtrmgr service." + name: Optional[str] = None + group: str = "XORP" + executables: Tuple[str, ...] = ("xorp_rtrmgr",) + dependencies: Tuple[str, ...] = ("xorp_rtrmgr",) + meta: str = ( + "The config file for this service can be found in the xorp_rtrmgr service." + ) @staticmethod - def fea(forwarding): + def fea(forwarding: str) -> str: """ Helper to add a forwarding engine entry to the config file. """ @@ -104,17 +103,14 @@ class XorpService(CoreService): return cfg @staticmethod - def mfea(forwarding, ifaces): + def mfea(forwarding, node: CoreNode) -> str: """ Helper to add a multicast forwarding engine entry to the config file. """ names = [] - for iface in ifaces: - if hasattr(iface, "control") and iface.control is True: - continue + for iface in node.get_ifaces(control=False): names.append(iface.name) names.append("register_vif") - cfg = "plumbing {\n" cfg += " %s {\n" % forwarding for name in names: @@ -128,7 +124,7 @@ class XorpService(CoreService): return cfg @staticmethod - def policyexportconnected(): + def policyexportconnected() -> str: """ Helper to add a policy statement for exporting connected routes. """ @@ -144,7 +140,7 @@ class XorpService(CoreService): return cfg @staticmethod - def routerid(node): + def router_id(node: CoreNode) -> str: """ Helper to return the first IPv4 address of a node as its router ID. """ @@ -153,15 +149,14 @@ class XorpService(CoreService): a = a.split("/")[0] if netaddr.valid_ipv4(a): return a - # raise ValueError, "no IPv4 address found for router ID" return "0.0.0.0" @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: return "" @classmethod - def generatexorpconfig(cls, node): + def generate_xorp_config(cls, node: CoreNode) -> str: return "" @@ -172,12 +167,12 @@ class XorpOspfv2(XorpService): unified XORP configuration file. """ - name = "XORP_OSPFv2" + name: str = "XORP_OSPFv2" @classmethod - def generatexorpconfig(cls, node): + def generate_xorp_config(cls, node: CoreNode) -> str: cfg = cls.fea("unicast-forwarding4") - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += "\nprotocols {\n" cfg += " ospf4 {\n" cfg += "\trouter-id: %s\n" % rtrid @@ -206,12 +201,12 @@ class XorpOspfv3(XorpService): unified XORP configuration file. """ - name = "XORP_OSPFv3" + name: str = "XORP_OSPFv3" @classmethod - def generatexorpconfig(cls, node): + def generate_xorp_config(cls, node: CoreNode) -> str: cfg = cls.fea("unicast-forwarding6") - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += "\nprotocols {\n" cfg += " ospf6 0 { /* Instance ID 0 */\n" cfg += "\trouter-id: %s\n" % rtrid @@ -232,16 +227,16 @@ class XorpBgp(XorpService): IPv4 inter-domain routing. AS numbers and peers must be customized. """ - name = "XORP_BGP" - custom_needed = True + name: str = "XORP_BGP" + custom_needed: bool = True @classmethod - def generatexorpconfig(cls, node): + def generate_xorp_config(cls, node: CoreNode) -> str: cfg = "/* This is a sample config that should be customized with\n" cfg += " appropriate AS numbers and peers */\n" cfg += cls.fea("unicast-forwarding4") cfg += cls.policyexportconnected() - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += "\nprotocols {\n" cfg += " bgp {\n" cfg += "\tbgp-id: %s\n" % rtrid @@ -262,10 +257,10 @@ class XorpRip(XorpService): RIP IPv4 unicast routing. """ - name = "XORP_RIP" + name: str = "XORP_RIP" @classmethod - def generatexorpconfig(cls, node): + def generate_xorp_config(cls, node: CoreNode) -> str: cfg = cls.fea("unicast-forwarding4") cfg += cls.policyexportconnected() cfg += "\nprotocols {\n" @@ -293,10 +288,10 @@ class XorpRipng(XorpService): RIP NG IPv6 unicast routing. """ - name = "XORP_RIPNG" + name: str = "XORP_RIPNG" @classmethod - def generatexorpconfig(cls, node): + def generate_xorp_config(cls, node: CoreNode) -> str: cfg = cls.fea("unicast-forwarding6") cfg += cls.policyexportconnected() cfg += "\nprotocols {\n" @@ -320,12 +315,11 @@ class XorpPimSm4(XorpService): PIM Sparse Mode IPv4 multicast routing. """ - name = "XORP_PIMSM4" + name: str = "XORP_PIMSM4" @classmethod - def generatexorpconfig(cls, node): - cfg = cls.mfea("mfea4", node.get_ifaces()) - + def generate_xorp_config(cls, node: CoreNode) -> str: + cfg = cls.mfea("mfea4", node) cfg += "\nprotocols {\n" cfg += " igmp {\n" names = [] @@ -338,7 +332,6 @@ class XorpPimSm4(XorpService): cfg += "\t}\n" cfg += " }\n" cfg += "}\n" - cfg += "\nprotocols {\n" cfg += " pimsm4 {\n" @@ -361,10 +354,8 @@ class XorpPimSm4(XorpService): cfg += "\t\t}\n" cfg += "\t }\n" cfg += "\t}\n" - cfg += " }\n" cfg += "}\n" - cfg += "\nprotocols {\n" cfg += " fib2mrib {\n" cfg += "\tdisable: false\n" @@ -378,12 +369,11 @@ class XorpPimSm6(XorpService): PIM Sparse Mode IPv6 multicast routing. """ - name = "XORP_PIMSM6" + name: str = "XORP_PIMSM6" @classmethod - def generatexorpconfig(cls, node): - cfg = cls.mfea("mfea6", node.get_ifaces()) - + def generate_xorp_config(cls, node: CoreNode) -> str: + cfg = cls.mfea("mfea6", node) cfg += "\nprotocols {\n" cfg += " mld {\n" names = [] @@ -396,7 +386,6 @@ class XorpPimSm6(XorpService): cfg += "\t}\n" cfg += " }\n" cfg += "}\n" - cfg += "\nprotocols {\n" cfg += " pimsm6 {\n" @@ -419,10 +408,8 @@ class XorpPimSm6(XorpService): cfg += "\t\t}\n" cfg += "\t }\n" cfg += "\t}\n" - cfg += " }\n" cfg += "}\n" - cfg += "\nprotocols {\n" cfg += " fib2mrib {\n" cfg += "\tdisable: false\n" @@ -436,12 +423,12 @@ class XorpOlsr(XorpService): OLSR IPv4 unicast MANET routing. """ - name = "XORP_OLSR" + name: str = "XORP_OLSR" @classmethod - def generatexorpconfig(cls, node): + def generate_xorp_config(cls, node: CoreNode) -> str: cfg = cls.fea("unicast-forwarding4") - rtrid = cls.routerid(node) + rtrid = cls.router_id(node) cfg += "\nprotocols {\n" cfg += " olsr4 {\n" cfg += "\tmain-address: %s\n" % rtrid From b2ea8cbbf65c316ef2f06cd48f478df82028eb6d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 18 Jun 2020 14:15:45 -0700 Subject: [PATCH 0372/1131] daemon: added type hinting throughout config services --- daemon/core/configservice/base.py | 16 +- daemon/core/configservice/dependencies.py | 14 +- daemon/core/configservice/manager.py | 11 +- .../configservices/frrservices/services.py | 122 +++---- .../configservices/nrlservices/services.py | 197 ++++++------ .../configservices/quaggaservices/services.py | 119 +++---- .../sercurityservices/services.py | 124 ++++---- daemon/core/configservices/simpleservice.py | 26 +- .../configservices/utilservices/services.py | 297 +++++++++--------- 9 files changed, 471 insertions(+), 455 deletions(-) diff --git a/daemon/core/configservice/base.py b/daemon/core/configservice/base.py index 82598988..bb97e321 100644 --- a/daemon/core/configservice/base.py +++ b/daemon/core/configservice/base.py @@ -14,7 +14,7 @@ from core.config import Configuration from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNode -TEMPLATES_DIR = "templates" +TEMPLATES_DIR: str = "templates" class ConfigServiceMode(enum.Enum): @@ -33,10 +33,10 @@ class ConfigService(abc.ABC): """ # validation period in seconds, how frequent validation is attempted - validation_period = 0.5 + validation_period: float = 0.5 # time to wait in seconds for determining if service started successfully - validation_timer = 5 + validation_timer: int = 5 def __init__(self, node: CoreNode) -> None: """ @@ -44,13 +44,13 @@ class ConfigService(abc.ABC): :param node: node this service is assigned to """ - self.node = node + self.node: CoreNode = node class_file = inspect.getfile(self.__class__) templates_path = pathlib.Path(class_file).parent.joinpath(TEMPLATES_DIR) - self.templates = TemplateLookup(directories=templates_path) - self.config = {} - self.custom_templates = {} - self.custom_config = {} + self.templates: TemplateLookup = TemplateLookup(directories=templates_path) + self.config: Dict[str, Configuration] = {} + self.custom_templates: Dict[str, str] = {} + self.custom_config: Dict[str, str] = {} configs = self.default_configs[:] self._define_config(configs) diff --git a/daemon/core/configservice/dependencies.py b/daemon/core/configservice/dependencies.py index 92eede79..be1c45e7 100644 --- a/daemon/core/configservice/dependencies.py +++ b/daemon/core/configservice/dependencies.py @@ -1,5 +1,5 @@ import logging -from typing import TYPE_CHECKING, Dict, List +from typing import TYPE_CHECKING, Dict, List, Set if TYPE_CHECKING: from core.configservice.base import ConfigService @@ -17,9 +17,9 @@ class ConfigServiceDependencies: :param services: services for determining dependency sets """ # helpers to check validity - self.dependents = {} - self.started = set() - self.node_services = {} + self.dependents: Dict[str, Set[str]] = {} + self.started: Set[str] = set() + self.node_services: Dict[str, "ConfigService"] = {} for service in services.values(): self.node_services[service.name] = service for dependency in service.dependencies: @@ -27,9 +27,9 @@ class ConfigServiceDependencies: dependents.add(service.name) # used to find paths - self.path = [] - self.visited = set() - self.visiting = set() + self.path: List["ConfigService"] = [] + self.visited: Set[str] = set() + self.visiting: Set[str] = set() def startup_paths(self) -> List[List["ConfigService"]]: """ diff --git a/daemon/core/configservice/manager.py b/daemon/core/configservice/manager.py index 1f806f7b..ecea6e68 100644 --- a/daemon/core/configservice/manager.py +++ b/daemon/core/configservice/manager.py @@ -1,6 +1,6 @@ import logging import pathlib -from typing import List, Type +from typing import Dict, List, Type from core import utils from core.configservice.base import ConfigService @@ -16,7 +16,7 @@ class ConfigServiceManager: """ Create a ConfigServiceManager instance. """ - self.services = {} + self.services: Dict[str, Type[ConfigService]] = {} def get_service(self, name: str) -> Type[ConfigService]: """ @@ -31,7 +31,7 @@ class ConfigServiceManager: raise CoreError(f"service does not exit {name}") return service_class - def add(self, service: ConfigService) -> None: + def add(self, service: Type[ConfigService]) -> None: """ Add service to manager, checking service requirements have been met. @@ -40,7 +40,9 @@ class ConfigServiceManager: :raises CoreError: when service is a duplicate or has unmet executables """ name = service.name - logging.debug("loading service: class(%s) name(%s)", service.__class__, name) + logging.debug( + "loading service: class(%s) name(%s)", service.__class__.__name__, name + ) # avoid duplicate services if name in self.services: @@ -73,7 +75,6 @@ class ConfigServiceManager: logging.debug("loading config services from: %s", subdir) services = utils.load_classes(str(subdir), ConfigService) for service in services: - logging.debug("found service: %s", service) try: self.add(service) except CoreError as e: diff --git a/daemon/core/configservices/frrservices/services.py b/daemon/core/configservices/frrservices/services.py index 8764e32c..2e24b40a 100644 --- a/daemon/core/configservices/frrservices/services.py +++ b/daemon/core/configservices/frrservices/services.py @@ -1,16 +1,17 @@ import abc -from typing import Any, Dict +from typing import Any, Dict, List import netaddr from core import constants +from core.config import Configuration 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.network import WlanNode -GROUP = "FRR" +GROUP: str = "FRR" def has_mtu_mismatch(iface: CoreInterface) -> bool: @@ -29,7 +30,7 @@ def has_mtu_mismatch(iface: CoreInterface) -> bool: return False -def get_min_mtu(iface): +def get_min_mtu(iface: CoreInterface) -> int: """ Helper to discover the minimum MTU of interfaces linked with the given interface. @@ -56,23 +57,23 @@ def get_router_id(node: CoreNodeBase) -> str: class FRRZebra(ConfigService): - name = "FRRzebra" - group = GROUP - directories = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"] - files = [ + name: str = "FRRzebra" + group: str = GROUP + directories: List[str] = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"] + files: List[str] = [ "/usr/local/etc/frr/frr.conf", "frrboot.sh", "/usr/local/etc/frr/vtysh.conf", "/usr/local/etc/frr/daemons", ] - executables = ["zebra"] - dependencies = [] - startup = ["sh frrboot.sh zebra"] - validate = ["pidof zebra"] - shutdown = ["killall zebra"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + executables: List[str] = ["zebra"] + dependencies: List[str] = [] + startup: List[str] = ["sh frrboot.sh zebra"] + validate: List[str] = ["pidof zebra"] + shutdown: List[str] = ["killall zebra"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: frr_conf = self.files[0] @@ -89,6 +90,8 @@ class FRRZebra(ConfigService): for service in self.node.config_services.values(): if self.name not in service.dependencies: continue + if not isinstance(service, FrrService): + continue if service.ipv4_routing: want_ip4 = True if service.ipv6_routing: @@ -121,19 +124,19 @@ class FRRZebra(ConfigService): class FrrService(abc.ABC): - group = GROUP - directories = [] - files = [] - executables = [] - dependencies = ["FRRzebra"] - startup = [] - validate = [] - shutdown = [] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} - ipv4_routing = False - ipv6_routing = False + group: str = GROUP + directories: List[str] = [] + files: List[str] = [] + executables: List[str] = [] + dependencies: List[str] = ["FRRzebra"] + startup: List[str] = [] + validate: List[str] = [] + shutdown: List[str] = [] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} + ipv4_routing: bool = False + ipv6_routing: bool = False @abc.abstractmethod def frr_iface_config(self, iface: CoreInterface) -> str: @@ -151,11 +154,10 @@ class FRROspfv2(FrrService, ConfigService): unified frr.conf file. """ - name = "FRROSPFv2" - startup = () - shutdown = ["killall ospfd"] - validate = ["pidof ospfd"] - ipv4_routing = True + name: str = "FRROSPFv2" + shutdown: List[str] = ["killall ospfd"] + validate: List[str] = ["pidof ospfd"] + ipv4_routing: bool = True def frr_config(self) -> str: router_id = get_router_id(self.node) @@ -190,11 +192,11 @@ class FRROspfv3(FrrService, ConfigService): unified frr.conf file. """ - name = "FRROSPFv3" - shutdown = ["killall ospf6d"] - validate = ["pidof ospf6d"] - ipv4_routing = True - ipv6_routing = True + name: str = "FRROSPFv3" + shutdown: List[str] = ["killall ospf6d"] + validate: List[str] = ["pidof ospf6d"] + ipv4_routing: bool = True + ipv6_routing: bool = True def frr_config(self) -> str: router_id = get_router_id(self.node) @@ -227,12 +229,12 @@ class FRRBgp(FrrService, ConfigService): having the same AS number. """ - name = "FRRBGP" - shutdown = ["killall bgpd"] - validate = ["pidof bgpd"] - custom_needed = True - ipv4_routing = True - ipv6_routing = True + name: str = "FRRBGP" + shutdown: List[str] = ["killall bgpd"] + validate: List[str] = ["pidof bgpd"] + custom_needed: bool = True + ipv4_routing: bool = True + ipv6_routing: bool = True def frr_config(self) -> str: router_id = get_router_id(self.node) @@ -257,10 +259,10 @@ class FRRRip(FrrService, ConfigService): The RIP service provides IPv4 routing for wired networks. """ - name = "FRRRIP" - shutdown = ["killall ripd"] - validate = ["pidof ripd"] - ipv4_routing = True + name: str = "FRRRIP" + shutdown: List[str] = ["killall ripd"] + validate: List[str] = ["pidof ripd"] + ipv4_routing: bool = True def frr_config(self) -> str: text = """ @@ -282,10 +284,10 @@ class FRRRipng(FrrService, ConfigService): The RIP NG service provides IPv6 routing for wired networks. """ - name = "FRRRIPNG" - shutdown = ["killall ripngd"] - validate = ["pidof ripngd"] - ipv6_routing = True + name: str = "FRRRIPNG" + shutdown: List[str] = ["killall ripngd"] + validate: List[str] = ["pidof ripngd"] + ipv6_routing: bool = True def frr_config(self) -> str: text = """ @@ -308,10 +310,10 @@ class FRRBabel(FrrService, ConfigService): protocol for IPv6 and IPv4 with fast convergence properties. """ - name = "FRRBabel" - shutdown = ["killall babeld"] - validate = ["pidof babeld"] - ipv6_routing = True + name: str = "FRRBabel" + shutdown: List[str] = ["killall babeld"] + validate: List[str] = ["pidof babeld"] + ipv6_routing: bool = True def frr_config(self) -> str: ifnames = [] @@ -348,10 +350,10 @@ class FRRpimd(FrrService, ConfigService): PIM multicast routing based on XORP. """ - name = "FRRpimd" - shutdown = ["killall pimd"] - validate = ["pidof pimd"] - ipv4_routing = True + name: str = "FRRpimd" + shutdown: List[str] = ["killall pimd"] + validate: List[str] = ["pidof pimd"] + ipv4_routing: bool = True def frr_config(self) -> str: ifname = "eth0" diff --git a/daemon/core/configservices/nrlservices/services.py b/daemon/core/configservices/nrlservices/services.py index ca95b8f6..0a5e8baf 100644 --- a/daemon/core/configservices/nrlservices/services.py +++ b/daemon/core/configservices/nrlservices/services.py @@ -1,26 +1,27 @@ -from typing import Any, Dict +from typing import Any, Dict, List import netaddr from core import utils +from core.config import Configuration from core.configservice.base import ConfigService, ConfigServiceMode -GROUP = "ProtoSvc" +GROUP: str = "ProtoSvc" class MgenSinkService(ConfigService): - name = "MGEN_Sink" - group = GROUP - directories = [] - files = ["mgensink.sh", "sink.mgen"] - executables = ["mgen"] - dependencies = [] - startup = ["sh mgensink.sh"] - validate = ["pidof mgen"] - shutdown = ["killall mgen"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "MGEN_Sink" + group: str = GROUP + directories: List[str] = [] + files: List[str] = ["mgensink.sh", "sink.mgen"] + executables: List[str] = ["mgen"] + dependencies: List[str] = [] + startup: List[str] = ["sh mgensink.sh"] + validate: List[str] = ["pidof mgen"] + shutdown: List[str] = ["killall mgen"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: ifnames = [] @@ -31,18 +32,18 @@ class MgenSinkService(ConfigService): class NrlNhdp(ConfigService): - name = "NHDP" - group = GROUP - directories = [] - files = ["nrlnhdp.sh"] - executables = ["nrlnhdp"] - dependencies = [] - startup = ["sh nrlnhdp.sh"] - validate = ["pidof nrlnhdp"] - shutdown = ["killall nrlnhdp"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "NHDP" + group: str = GROUP + directories: List[str] = [] + files: List[str] = ["nrlnhdp.sh"] + executables: List[str] = ["nrlnhdp"] + dependencies: List[str] = [] + startup: List[str] = ["sh nrlnhdp.sh"] + validate: List[str] = ["pidof nrlnhdp"] + shutdown: List[str] = ["killall nrlnhdp"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: has_smf = "SMF" in self.node.config_services @@ -53,18 +54,18 @@ class NrlNhdp(ConfigService): class NrlSmf(ConfigService): - name = "SMF" - group = GROUP - directories = [] - files = ["startsmf.sh"] - executables = ["nrlsmf", "killall"] - dependencies = [] - startup = ["sh startsmf.sh"] - validate = ["pidof nrlsmf"] - shutdown = ["killall nrlsmf"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "SMF" + group: str = GROUP + directories: List[str] = [] + files: List[str] = ["startsmf.sh"] + executables: List[str] = ["nrlsmf", "killall"] + dependencies: List[str] = [] + startup: List[str] = ["sh startsmf.sh"] + validate: List[str] = ["pidof nrlsmf"] + shutdown: List[str] = ["killall nrlsmf"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: has_arouted = "arouted" in self.node.config_services @@ -91,18 +92,18 @@ class NrlSmf(ConfigService): class NrlOlsr(ConfigService): - name = "OLSR" - group = GROUP - directories = [] - files = ["nrlolsrd.sh"] - executables = ["nrlolsrd"] - dependencies = [] - startup = ["sh nrlolsrd.sh"] - validate = ["pidof nrlolsrd"] - shutdown = ["killall nrlolsrd"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "OLSR" + group: str = GROUP + directories: List[str] = [] + files: List[str] = ["nrlolsrd.sh"] + executables: List[str] = ["nrlolsrd"] + dependencies: List[str] = [] + startup: List[str] = ["sh nrlolsrd.sh"] + validate: List[str] = ["pidof nrlolsrd"] + shutdown: List[str] = ["killall nrlolsrd"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: has_smf = "SMF" in self.node.config_services @@ -115,18 +116,18 @@ class NrlOlsr(ConfigService): class NrlOlsrv2(ConfigService): - name = "OLSRv2" - group = GROUP - directories = [] - files = ["nrlolsrv2.sh"] - executables = ["nrlolsrv2"] - dependencies = [] - startup = ["sh nrlolsrv2.sh"] - validate = ["pidof nrlolsrv2"] - shutdown = ["killall nrlolsrv2"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "OLSRv2" + group: str = GROUP + directories: List[str] = [] + files: List[str] = ["nrlolsrv2.sh"] + executables: List[str] = ["nrlolsrv2"] + dependencies: List[str] = [] + startup: List[str] = ["sh nrlolsrv2.sh"] + validate: List[str] = ["pidof nrlolsrv2"] + shutdown: List[str] = ["killall nrlolsrv2"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: has_smf = "SMF" in self.node.config_services @@ -137,18 +138,18 @@ class NrlOlsrv2(ConfigService): class OlsrOrg(ConfigService): - name = "OLSRORG" - group = GROUP - directories = ["/etc/olsrd"] - files = ["olsrd.sh", "/etc/olsrd/olsrd.conf"] - executables = ["olsrd"] - dependencies = [] - startup = ["sh olsrd.sh"] - validate = ["pidof olsrd"] - shutdown = ["killall olsrd"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "OLSRORG" + group: str = GROUP + directories: List[str] = ["/etc/olsrd"] + files: List[str] = ["olsrd.sh", "/etc/olsrd/olsrd.conf"] + executables: List[str] = ["olsrd"] + dependencies: List[str] = [] + startup: List[str] = ["sh olsrd.sh"] + validate: List[str] = ["pidof olsrd"] + shutdown: List[str] = ["killall olsrd"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: has_smf = "SMF" in self.node.config_services @@ -159,33 +160,33 @@ class OlsrOrg(ConfigService): class MgenActor(ConfigService): - name = "MgenActor" - group = GROUP - directories = [] - files = ["start_mgen_actor.sh"] - executables = ["mgen"] - dependencies = [] - startup = ["sh start_mgen_actor.sh"] - validate = ["pidof mgen"] - shutdown = ["killall mgen"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "MgenActor" + group: str = GROUP + directories: List[str] = [] + files: List[str] = ["start_mgen_actor.sh"] + executables: List[str] = ["mgen"] + dependencies: List[str] = [] + startup: List[str] = ["sh start_mgen_actor.sh"] + validate: List[str] = ["pidof mgen"] + shutdown: List[str] = ["killall mgen"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} class Arouted(ConfigService): - name = "arouted" - group = GROUP - directories = [] - files = ["startarouted.sh"] - executables = ["arouted"] - dependencies = [] - startup = ["sh startarouted.sh"] - validate = ["pidof arouted"] - shutdown = ["pkill arouted"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "arouted" + group: str = GROUP + directories: List[str] = [] + files: List[str] = ["startarouted.sh"] + executables: List[str] = ["arouted"] + dependencies: List[str] = [] + startup: List[str] = ["sh startarouted.sh"] + validate: List[str] = ["pidof arouted"] + shutdown: List[str] = ["pkill arouted"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: ip4_prefix = None diff --git a/daemon/core/configservices/quaggaservices/services.py b/daemon/core/configservices/quaggaservices/services.py index 19e21476..40a1d7d3 100644 --- a/daemon/core/configservices/quaggaservices/services.py +++ b/daemon/core/configservices/quaggaservices/services.py @@ -1,17 +1,18 @@ import abc import logging -from typing import Any, Dict +from typing import Any, Dict, List import netaddr from core import constants +from core.config import Configuration 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.network import WlanNode -GROUP = "Quagga" +GROUP: str = "Quagga" def has_mtu_mismatch(iface: CoreInterface) -> bool: @@ -57,22 +58,22 @@ def get_router_id(node: CoreNodeBase) -> str: class Zebra(ConfigService): - name = "zebra" - group = GROUP - directories = ["/usr/local/etc/quagga", "/var/run/quagga"] - files = [ + name: str = "zebra" + group: str = GROUP + directories: List[str] = ["/usr/local/etc/quagga", "/var/run/quagga"] + files: List[str] = [ "/usr/local/etc/quagga/Quagga.conf", "quaggaboot.sh", "/usr/local/etc/quagga/vtysh.conf", ] - executables = ["zebra"] - dependencies = [] - startup = ["sh quaggaboot.sh zebra"] - validate = ["pidof zebra"] - shutdown = ["killall zebra"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + executables: List[str] = ["zebra"] + dependencies: List[str] = [] + startup: List[str] = ["sh quaggaboot.sh zebra"] + validate: List[str] = ["pidof zebra"] + shutdown: List[str] = ["killall zebra"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: quagga_bin_search = self.node.session.options.get_config( @@ -90,6 +91,8 @@ class Zebra(ConfigService): for service in self.node.config_services.values(): if self.name not in service.dependencies: continue + if not isinstance(service, QuaggaService): + continue if service.ipv4_routing: want_ip4 = True if service.ipv6_routing: @@ -122,19 +125,19 @@ class Zebra(ConfigService): class QuaggaService(abc.ABC): - group = GROUP - directories = [] - files = [] - executables = [] - dependencies = ["zebra"] - startup = [] - validate = [] - shutdown = [] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} - ipv4_routing = False - ipv6_routing = False + group: str = GROUP + directories: List[str] = [] + files: List[str] = [] + executables: List[str] = [] + dependencies: List[str] = ["zebra"] + startup: List[str] = [] + validate: List[str] = [] + shutdown: List[str] = [] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} + ipv4_routing: bool = False + ipv6_routing: bool = False @abc.abstractmethod def quagga_iface_config(self, iface: CoreInterface) -> str: @@ -152,10 +155,10 @@ class Ospfv2(QuaggaService, ConfigService): unified Quagga.conf file. """ - name = "OSPFv2" - validate = ["pidof ospfd"] - shutdown = ["killall ospfd"] - ipv4_routing = True + name: str = "OSPFv2" + validate: List[str] = ["pidof ospfd"] + shutdown: List[str] = ["killall ospfd"] + ipv4_routing: bool = True def quagga_iface_config(self, iface: CoreInterface) -> str: if has_mtu_mismatch(iface): @@ -190,11 +193,11 @@ class Ospfv3(QuaggaService, ConfigService): unified Quagga.conf file. """ - name = "OSPFv3" - shutdown = ("killall ospf6d",) - validate = ("pidof ospf6d",) - ipv4_routing = True - ipv6_routing = True + name: str = "OSPFv3" + shutdown: List[str] = ["killall ospf6d"] + validate: List[str] = ["pidof ospf6d"] + ipv4_routing: bool = True + ipv6_routing: bool = True def quagga_iface_config(self, iface: CoreInterface) -> str: mtu = get_min_mtu(iface) @@ -229,7 +232,7 @@ class Ospfv3mdr(Ospfv3): unified Quagga.conf file. """ - name = "OSPFv3MDR" + name: str = "OSPFv3MDR" def data(self) -> Dict[str, Any]: for iface in self.node.get_ifaces(): @@ -262,11 +265,11 @@ class Bgp(QuaggaService, ConfigService): having the same AS number. """ - name = "BGP" - shutdown = ["killall bgpd"] - validate = ["pidof bgpd"] - ipv4_routing = True - ipv6_routing = True + name: str = "BGP" + shutdown: List[str] = ["killall bgpd"] + validate: List[str] = ["pidof bgpd"] + ipv4_routing: bool = True + ipv6_routing: bool = True def quagga_config(self) -> str: return "" @@ -291,10 +294,10 @@ class Rip(QuaggaService, ConfigService): The RIP service provides IPv4 routing for wired networks. """ - name = "RIP" - shutdown = ["killall ripd"] - validate = ["pidof ripd"] - ipv4_routing = True + name: str = "RIP" + shutdown: List[str] = ["killall ripd"] + validate: List[str] = ["pidof ripd"] + ipv4_routing: bool = True def quagga_config(self) -> str: text = """ @@ -316,10 +319,10 @@ class Ripng(QuaggaService, ConfigService): The RIP NG service provides IPv6 routing for wired networks. """ - name = "RIPNG" - shutdown = ["killall ripngd"] - validate = ["pidof ripngd"] - ipv6_routing = True + name: str = "RIPNG" + shutdown: List[str] = ["killall ripngd"] + validate: List[str] = ["pidof ripngd"] + ipv6_routing: bool = True def quagga_config(self) -> str: text = """ @@ -342,10 +345,10 @@ class Babel(QuaggaService, ConfigService): protocol for IPv6 and IPv4 with fast convergence properties. """ - name = "Babel" - shutdown = ["killall babeld"] - validate = ["pidof babeld"] - ipv6_routing = True + name: str = "Babel" + shutdown: List[str] = ["killall babeld"] + validate: List[str] = ["pidof babeld"] + ipv6_routing: bool = True def quagga_config(self) -> str: ifnames = [] @@ -382,10 +385,10 @@ class Xpimd(QuaggaService, ConfigService): PIM multicast routing based on XORP. """ - name = "Xpimd" - shutdown = ["killall xpimd"] - validate = ["pidof xpimd"] - ipv4_routing = True + name: str = "Xpimd" + shutdown: List[str] = ["killall xpimd"] + validate: List[str] = ["pidof xpimd"] + ipv4_routing: bool = True def quagga_config(self) -> str: ifname = "eth0" diff --git a/daemon/core/configservices/sercurityservices/services.py b/daemon/core/configservices/sercurityservices/services.py index 6e92bf62..5766b0db 100644 --- a/daemon/core/configservices/sercurityservices/services.py +++ b/daemon/core/configservices/sercurityservices/services.py @@ -1,4 +1,4 @@ -from typing import Any, Dict +from typing import Any, Dict, List import netaddr @@ -6,21 +6,21 @@ from core.config import Configuration from core.configservice.base import ConfigService, ConfigServiceMode from core.emulator.enumerations import ConfigDataTypes -GROUP_NAME = "Security" +GROUP_NAME: str = "Security" class VpnClient(ConfigService): - name = "VPNClient" - group = GROUP_NAME - directories = [] - files = ["vpnclient.sh"] - executables = ["openvpn", "ip", "killall"] - dependencies = [] - startup = ["sh vpnclient.sh"] - validate = ["pidof openvpn"] - shutdown = ["killall openvpn"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [ + name: str = "VPNClient" + group: str = GROUP_NAME + directories: List[str] = [] + files: List[str] = ["vpnclient.sh"] + executables: List[str] = ["openvpn", "ip", "killall"] + dependencies: List[str] = [] + startup: List[str] = ["sh vpnclient.sh"] + validate: List[str] = ["pidof openvpn"] + shutdown: List[str] = ["killall openvpn"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [ Configuration( _id="keydir", _type=ConfigDataTypes.STRING, @@ -40,21 +40,21 @@ class VpnClient(ConfigService): default="10.0.2.10", ), ] - modes = {} + modes: Dict[str, Dict[str, str]] = {} class VpnServer(ConfigService): - name = "VPNServer" - group = GROUP_NAME - directories = [] - files = ["vpnserver.sh"] - executables = ["openvpn", "ip", "killall"] - dependencies = [] - startup = ["sh vpnserver.sh"] - validate = ["pidof openvpn"] - shutdown = ["killall openvpn"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [ + name: str = "VPNServer" + group: str = GROUP_NAME + directories: List[str] = [] + files: List[str] = ["vpnserver.sh"] + executables: List[str] = ["openvpn", "ip", "killall"] + dependencies: List[str] = [] + startup: List[str] = ["sh vpnserver.sh"] + validate: List[str] = ["pidof openvpn"] + shutdown: List[str] = ["killall openvpn"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [ Configuration( _id="keydir", _type=ConfigDataTypes.STRING, @@ -74,7 +74,7 @@ class VpnServer(ConfigService): default="10.0.200.0", ), ] - modes = {} + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: address = None @@ -87,48 +87,48 @@ class VpnServer(ConfigService): class IPsec(ConfigService): - name = "IPsec" - group = GROUP_NAME - directories = [] - files = ["ipsec.sh"] - executables = ["racoon", "ip", "setkey", "killall"] - dependencies = [] - startup = ["sh ipsec.sh"] - validate = ["pidof racoon"] - shutdown = ["killall racoon"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "IPsec" + group: str = GROUP_NAME + directories: List[str] = [] + files: List[str] = ["ipsec.sh"] + executables: List[str] = ["racoon", "ip", "setkey", "killall"] + dependencies: List[str] = [] + startup: List[str] = ["sh ipsec.sh"] + validate: List[str] = ["pidof racoon"] + shutdown: List[str] = ["killall racoon"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} class Firewall(ConfigService): - name = "Firewall" - group = GROUP_NAME - directories = [] - files = ["firewall.sh"] - executables = ["iptables"] - dependencies = [] - startup = ["sh firewall.sh"] - validate = [] - shutdown = [] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "Firewall" + group: str = GROUP_NAME + directories: List[str] = [] + files: List[str] = ["firewall.sh"] + executables: List[str] = ["iptables"] + dependencies: List[str] = [] + startup: List[str] = ["sh firewall.sh"] + validate: List[str] = [] + shutdown: List[str] = [] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} class Nat(ConfigService): - name = "NAT" - group = GROUP_NAME - directories = [] - files = ["nat.sh"] - executables = ["iptables"] - dependencies = [] - startup = ["sh nat.sh"] - validate = [] - shutdown = [] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "NAT" + group: str = GROUP_NAME + directories: List[str] = [] + files: List[str] = ["nat.sh"] + executables: List[str] = ["iptables"] + dependencies: List[str] = [] + startup: List[str] = ["sh nat.sh"] + validate: List[str] = [] + shutdown: List[str] = [] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: ifnames = [] diff --git a/daemon/core/configservices/simpleservice.py b/daemon/core/configservices/simpleservice.py index e727fe82..c2e7242f 100644 --- a/daemon/core/configservices/simpleservice.py +++ b/daemon/core/configservices/simpleservice.py @@ -1,20 +1,22 @@ +from typing import Dict, List + from core.config import Configuration from core.configservice.base import ConfigService, ConfigServiceMode from core.emulator.enumerations import ConfigDataTypes class SimpleService(ConfigService): - name = "Simple" - group = "SimpleGroup" - directories = ["/etc/quagga", "/usr/local/lib"] - files = ["test1.sh", "test2.sh"] - executables = [] - dependencies = [] - startup = [] - validate = [] - shutdown = [] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [ + name: str = "Simple" + group: str = "SimpleGroup" + directories: List[str] = ["/etc/quagga", "/usr/local/lib"] + files: List[str] = ["test1.sh", "test2.sh"] + executables: List[str] = [] + dependencies: List[str] = [] + startup: List[str] = [] + validate: List[str] = [] + shutdown: List[str] = [] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [ Configuration(_id="value1", _type=ConfigDataTypes.STRING, label="Text"), Configuration(_id="value2", _type=ConfigDataTypes.BOOL, label="Boolean"), Configuration( @@ -24,7 +26,7 @@ class SimpleService(ConfigService): options=["value1", "value2", "value3"], ), ] - modes = { + modes: Dict[str, Dict[str, str]] = { "mode1": {"value1": "value1", "value2": "0", "value3": "value2"}, "mode2": {"value1": "value2", "value2": "1", "value3": "value3"}, "mode3": {"value1": "value3", "value2": "0", "value3": "value1"}, diff --git a/daemon/core/configservices/utilservices/services.py b/daemon/core/configservices/utilservices/services.py index 5aa3bb54..983f6cff 100644 --- a/daemon/core/configservices/utilservices/services.py +++ b/daemon/core/configservices/utilservices/services.py @@ -1,26 +1,27 @@ -from typing import Any, Dict +from typing import Any, Dict, List import netaddr from core import utils +from core.config import Configuration from core.configservice.base import ConfigService, ConfigServiceMode GROUP_NAME = "Utility" class DefaultRouteService(ConfigService): - name = "DefaultRoute" - group = GROUP_NAME - directories = [] - files = ["defaultroute.sh"] - executables = ["ip"] - dependencies = [] - startup = ["sh defaultroute.sh"] - validate = [] - shutdown = [] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "DefaultRoute" + group: str = GROUP_NAME + directories: List[str] = [] + files: List[str] = ["defaultroute.sh"] + executables: List[str] = ["ip"] + dependencies: List[str] = [] + startup: List[str] = ["sh defaultroute.sh"] + validate: List[str] = [] + shutdown: List[str] = [] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: # only add default routes for linked routing nodes @@ -37,18 +38,18 @@ class DefaultRouteService(ConfigService): class DefaultMulticastRouteService(ConfigService): - name = "DefaultMulticastRoute" - group = GROUP_NAME - directories = [] - files = ["defaultmroute.sh"] - executables = [] - dependencies = [] - startup = ["sh defaultmroute.sh"] - validate = [] - shutdown = [] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "DefaultMulticastRoute" + group: str = GROUP_NAME + directories: List[str] = [] + files: List[str] = ["defaultmroute.sh"] + executables: List[str] = [] + dependencies: List[str] = [] + startup: List[str] = ["sh defaultmroute.sh"] + validate: List[str] = [] + shutdown: List[str] = [] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: ifname = None @@ -59,18 +60,18 @@ class DefaultMulticastRouteService(ConfigService): class StaticRouteService(ConfigService): - name = "StaticRoute" - group = GROUP_NAME - directories = [] - files = ["staticroute.sh"] - executables = [] - dependencies = [] - startup = ["sh staticroute.sh"] - validate = [] - shutdown = [] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "StaticRoute" + group: str = GROUP_NAME + directories: List[str] = [] + files: List[str] = ["staticroute.sh"] + executables: List[str] = [] + dependencies: List[str] = [] + startup: List[str] = ["sh staticroute.sh"] + validate: List[str] = [] + shutdown: List[str] = [] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: routes = [] @@ -88,18 +89,18 @@ class StaticRouteService(ConfigService): class IpForwardService(ConfigService): - name = "IPForward" - group = GROUP_NAME - directories = [] - files = ["ipforward.sh"] - executables = ["sysctl"] - dependencies = [] - startup = ["sh ipforward.sh"] - validate = [] - shutdown = [] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "IPForward" + group: str = GROUP_NAME + directories: List[str] = [] + files: List[str] = ["ipforward.sh"] + executables: List[str] = ["sysctl"] + dependencies: List[str] = [] + startup: List[str] = ["sh ipforward.sh"] + validate: List[str] = [] + shutdown: List[str] = [] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: devnames = [] @@ -110,18 +111,18 @@ class IpForwardService(ConfigService): class SshService(ConfigService): - name = "SSH" - group = GROUP_NAME - directories = ["/etc/ssh", "/var/run/sshd"] - files = ["startsshd.sh", "/etc/ssh/sshd_config"] - executables = ["sshd"] - dependencies = [] - startup = ["sh startsshd.sh"] - validate = [] - shutdown = ["killall sshd"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "SSH" + group: str = GROUP_NAME + directories: List[str] = ["/etc/ssh", "/var/run/sshd"] + files: List[str] = ["startsshd.sh", "/etc/ssh/sshd_config"] + executables: List[str] = ["sshd"] + dependencies: List[str] = [] + startup: List[str] = ["sh startsshd.sh"] + validate: List[str] = [] + shutdown: List[str] = ["killall sshd"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: return dict( @@ -132,18 +133,18 @@ class SshService(ConfigService): class DhcpService(ConfigService): - name = "DHCP" - group = GROUP_NAME - directories = ["/etc/dhcp", "/var/lib/dhcp"] - files = ["/etc/dhcp/dhcpd.conf"] - executables = ["dhcpd"] - dependencies = [] - startup = ["touch /var/lib/dhcp/dhcpd.leases", "dhcpd"] - validate = ["pidof dhcpd"] - shutdown = ["killall dhcpd"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "DHCP" + group: str = GROUP_NAME + directories: List[str] = ["/etc/dhcp", "/var/lib/dhcp"] + files: List[str] = ["/etc/dhcp/dhcpd.conf"] + executables: List[str] = ["dhcpd"] + dependencies: List[str] = [] + startup: List[str] = ["touch /var/lib/dhcp/dhcpd.leases", "dhcpd"] + validate: List[str] = ["pidof dhcpd"] + shutdown: List[str] = ["killall dhcpd"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: subnets = [] @@ -161,18 +162,18 @@ class DhcpService(ConfigService): class DhcpClientService(ConfigService): - name = "DHCPClient" - group = GROUP_NAME - directories = [] - files = ["startdhcpclient.sh"] - executables = ["dhclient"] - dependencies = [] - startup = ["sh startdhcpclient.sh"] - validate = ["pidof dhclient"] - shutdown = ["killall dhclient"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "DHCPClient" + group: str = GROUP_NAME + directories: List[str] = [] + files: List[str] = ["startdhcpclient.sh"] + executables: List[str] = ["dhclient"] + dependencies: List[str] = [] + startup: List[str] = ["sh startdhcpclient.sh"] + validate: List[str] = ["pidof dhclient"] + shutdown: List[str] = ["killall dhclient"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: ifnames = [] @@ -182,33 +183,33 @@ class DhcpClientService(ConfigService): class FtpService(ConfigService): - name = "FTP" - group = GROUP_NAME - directories = ["/var/run/vsftpd/empty", "/var/ftp"] - files = ["vsftpd.conf"] - executables = ["vsftpd"] - dependencies = [] - startup = ["vsftpd ./vsftpd.conf"] - validate = ["pidof vsftpd"] - shutdown = ["killall vsftpd"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "FTP" + group: str = GROUP_NAME + directories: List[str] = ["/var/run/vsftpd/empty", "/var/ftp"] + files: List[str] = ["vsftpd.conf"] + executables: List[str] = ["vsftpd"] + dependencies: List[str] = [] + startup: List[str] = ["vsftpd ./vsftpd.conf"] + validate: List[str] = ["pidof vsftpd"] + shutdown: List[str] = ["killall vsftpd"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} class PcapService(ConfigService): - name = "pcap" - group = GROUP_NAME - directories = [] - files = ["pcap.sh"] - executables = ["tcpdump"] - dependencies = [] - startup = ["sh pcap.sh start"] - validate = ["pidof tcpdump"] - shutdown = ["sh pcap.sh stop"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "pcap" + group: str = GROUP_NAME + directories: List[str] = [] + files: List[str] = ["pcap.sh"] + executables: List[str] = ["tcpdump"] + dependencies: List[str] = [] + startup: List[str] = ["sh pcap.sh start"] + validate: List[str] = ["pidof tcpdump"] + shutdown: List[str] = ["sh pcap.sh stop"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: ifnames = [] @@ -218,18 +219,20 @@ class PcapService(ConfigService): class RadvdService(ConfigService): - name = "radvd" - group = GROUP_NAME - directories = ["/etc/radvd"] - files = ["/etc/radvd/radvd.conf"] - executables = ["radvd"] - dependencies = [] - startup = ["radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log"] - validate = ["pidof radvd"] - shutdown = ["pkill radvd"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "radvd" + group: str = GROUP_NAME + directories: List[str] = ["/etc/radvd"] + files: List[str] = ["/etc/radvd/radvd.conf"] + executables: List[str] = ["radvd"] + dependencies: List[str] = [] + startup: List[str] = [ + "radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log" + ] + validate: List[str] = ["pidof radvd"] + shutdown: List[str] = ["pkill radvd"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: ifaces = [] @@ -246,24 +249,24 @@ class RadvdService(ConfigService): class AtdService(ConfigService): - name = "atd" - group = GROUP_NAME - directories = ["/var/spool/cron/atjobs", "/var/spool/cron/atspool"] - files = ["startatd.sh"] - executables = ["atd"] - dependencies = [] - startup = ["sh startatd.sh"] - validate = ["pidof atd"] - shutdown = ["pkill atd"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + name: str = "atd" + group: str = GROUP_NAME + directories: List[str] = ["/var/spool/cron/atjobs", "/var/spool/cron/atspool"] + files: List[str] = ["startatd.sh"] + executables: List[str] = ["atd"] + dependencies: List[str] = [] + startup: List[str] = ["sh startatd.sh"] + validate: List[str] = ["pidof atd"] + shutdown: List[str] = ["pkill atd"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} class HttpService(ConfigService): - name = "HTTP" - group = GROUP_NAME - directories = [ + name: str = "HTTP" + group: str = GROUP_NAME + directories: List[str] = [ "/etc/apache2", "/var/run/apache2", "/var/log/apache2", @@ -271,15 +274,19 @@ class HttpService(ConfigService): "/var/lock/apache2", "/var/www", ] - files = ["/etc/apache2/apache2.conf", "/etc/apache2/envvars", "/var/www/index.html"] - executables = ["apache2ctl"] - dependencies = [] - startup = ["chown www-data /var/lock/apache2", "apache2ctl start"] - validate = ["pidof apache2"] - shutdown = ["apache2ctl stop"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} + files: List[str] = [ + "/etc/apache2/apache2.conf", + "/etc/apache2/envvars", + "/var/www/index.html", + ] + executables: List[str] = ["apache2ctl"] + dependencies: List[str] = [] + startup: List[str] = ["chown www-data /var/lock/apache2", "apache2ctl start"] + validate: List[str] = ["pidof apache2"] + shutdown: List[str] = ["apache2ctl stop"] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} def data(self) -> Dict[str, Any]: ifaces = [] From ca2b1c9e4cb90f82380492bbea61dd59a06f987a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 18 Jun 2020 21:33:28 -0700 Subject: [PATCH 0373/1131] daemon: refactored all_link_data to links --- daemon/core/api/grpc/grpcutils.py | 6 +++--- daemon/core/api/tlv/corehandlers.py | 12 ++++++------ daemon/core/emane/nodes.py | 4 ++-- daemon/core/location/mobility.py | 4 ++-- daemon/core/nodes/base.py | 4 ++-- daemon/core/nodes/network.py | 12 ++++++------ daemon/core/plugins/sdt.py | 2 +- daemon/core/xml/corexml.py | 2 +- daemon/tests/test_core.py | 2 +- daemon/tests/test_grpc.py | 14 +++++++------- daemon/tests/test_gui.py | 22 +++++++++++----------- daemon/tests/test_links.py | 6 +++--- daemon/tests/test_xml.py | 8 ++++---- 13 files changed, 49 insertions(+), 49 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index d95b7555..2c13315c 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -270,9 +270,9 @@ def get_links(node: NodeBase): :return: protobuf links """ links = [] - for link_data in node.all_link_data(): - link = convert_link(link_data) - links.append(link) + for link in node.links(): + link_proto = convert_link(link) + links.append(link_proto) return links diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index d01f15a3..bb4f2ecd 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -1824,16 +1824,16 @@ class CoreHandler(socketserver.BaseRequestHandler): Return API messages that describe the current session. """ # find all nodes and links - links_data = [] + all_links = [] with self.session.nodes_lock: for node_id in self.session.nodes: node = self.session.nodes[node_id] self.session.broadcast_node(node, MessageFlags.ADD) - node_links = node.all_link_data(flags=MessageFlags.ADD) - links_data.extend(node_links) + links = node.links(flags=MessageFlags.ADD) + all_links.extend(links) - for link_data in links_data: - self.session.broadcast_link(link_data) + for link in all_links: + self.session.broadcast_link(link) # send mobility model info for node_id in self.session.mobility.nodes(): @@ -1940,7 +1940,7 @@ class CoreHandler(socketserver.BaseRequestHandler): node_count = self.session.get_node_count() logging.info( - "informed GUI about %d nodes and %d links", node_count, len(links_data) + "informed GUI about %d nodes and %d links", node_count, len(all_links) ) diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index c28f1382..9173fbfc 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -241,8 +241,8 @@ class EmaneNet(CoreNetworkBase): event.append(nemid, latitude=lat, longitude=lon, altitude=alt) self.session.emane.service.publish(0, event) - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: - links = super().all_link_data(flags) + def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: + links = super().links(flags) # gather current emane links nem_ids = set(self.nemidmap.values()) emane_manager = self.session.emane diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 9bb2966e..f2e0f470 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -217,7 +217,7 @@ class WirelessModel(ConfigurableOptions): self.session: "Session" = session self.id: int = _id - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: + def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ May be used if the model can populate the GUI with wireless (green) link lines. @@ -509,7 +509,7 @@ class BasicRangeModel(WirelessModel): link_data = self.create_link_data(iface, iface2, message_type) self.session.broadcast_link(link_data) - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: + def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Return a list of wireless link messages for when the GUI reconnects. diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 4fc6b873..2c8ca06c 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -182,7 +182,7 @@ class NodeBase(abc.ABC): self.iface_id += 1 return iface_id - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: + def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Build link data for this node. @@ -1021,7 +1021,7 @@ class CoreNetworkBase(NodeBase): with self._linked_lock: del self._linked[iface] - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: + def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Build link data objects for this network. Each link object describes a link between this network and a node. diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index f20b6dfb..62443fb8 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -831,7 +831,7 @@ class CtrlNet(CoreNetwork): super().shutdown() - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: + def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Do not include CtrlNet in link messages describing this session. @@ -859,7 +859,7 @@ class PtpNet(CoreNetwork): raise CoreError("ptp links support at most 2 network interfaces") super().attach(iface) - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: + def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Build CORE API TLVs for a point-to-point link. One Link message describes this network. @@ -1054,17 +1054,17 @@ class WlanNode(CoreNetwork): for iface in self.get_ifaces(): iface.setposition() - def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: + def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ Retrieve all link data. :param flags: message flags :return: list of link data """ - all_links = super().all_link_data(flags) + links = super().links(flags) if self.model: - all_links.extend(self.model.all_link_data(flags)) - return all_links + links.extend(self.model.links(flags)) + return links class TunnelNode(GreTapBridge): diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 84c90730..ef36b0a4 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -225,7 +225,7 @@ class Sdt: self.add_node(node) for net in nets: - all_links = net.all_link_data(flags=MessageFlags.ADD) + all_links = net.links(flags=MessageFlags.ADD) for link_data in all_links: is_wireless = isinstance(net, (WlanNode, EmaneNet)) if is_wireless and link_data.node1_id == net.id: diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 190cf8f7..d3cc85d8 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -465,7 +465,7 @@ class CoreXmlWriter: self.write_device(node) # add known links - links.extend(node.all_link_data()) + links.extend(node.links()) return links def write_network(self, node: NodeBase) -> None: diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 2623b0df..c4465863 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -120,7 +120,7 @@ class TestCore: session.instantiate() # check link data gets generated - assert ptp_node.all_link_data(MessageFlags.ADD) + assert ptp_node.links(MessageFlags.ADD) # check common nets exist between linked nodes assert node1.commonnets(node2) diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 8abf33aa..a4efd6d9 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -555,7 +555,7 @@ class TestGrpc: session = grpc_server.coreemu.create_session() switch = session.add_node(SwitchNode) node = session.add_node(CoreNode) - assert len(switch.all_link_data()) == 0 + assert len(switch.links()) == 0 # then iface = iface_helper.create_iface(node.id, 0) @@ -564,7 +564,7 @@ class TestGrpc: # then assert response.result is True - assert len(switch.all_link_data()) == 1 + assert len(switch.links()) == 1 def test_add_link_exception( self, grpc_server: CoreGrpcServer, iface_helper: InterfaceHelper @@ -589,7 +589,7 @@ class TestGrpc: iface = ip_prefixes.create_iface(node) session.add_link(node.id, switch.id, iface) options = core_pb2.LinkOptions(bandwidth=30000) - link = switch.all_link_data()[0] + link = switch.links()[0] assert options.bandwidth != link.options.bandwidth # then @@ -600,7 +600,7 @@ class TestGrpc: # then assert response.result is True - link = switch.all_link_data()[0] + link = switch.links()[0] assert options.bandwidth == link.options.bandwidth def test_delete_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes): @@ -618,7 +618,7 @@ class TestGrpc: if node.id not in {node1.id, node2.id}: link_node = node break - assert len(link_node.all_link_data()) == 1 + assert len(link_node.links()) == 1 # then with client.context_connect(): @@ -628,7 +628,7 @@ class TestGrpc: # then assert response.result is True - assert len(link_node.all_link_data()) == 0 + assert len(link_node.links()) == 0 def test_get_wlan_config(self, grpc_server: CoreGrpcServer): # given @@ -1029,7 +1029,7 @@ class TestGrpc: node = session.add_node(CoreNode) iface = ip_prefixes.create_iface(node) session.add_link(node.id, wlan.id, iface) - link_data = wlan.all_link_data()[0] + link_data = wlan.links()[0] queue = Queue() def handle_event(event_data): diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index 8f01a2bf..a0b3bd8a 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -122,7 +122,7 @@ class TestGui: coretlv.handle_message(message) switch_node = coretlv.session.get_node(switch_id, SwitchNode) - all_links = switch_node.all_link_data() + all_links = switch_node.links() assert len(all_links) == 1 def test_link_add_net_to_node(self, coretlv: CoreHandler): @@ -146,7 +146,7 @@ class TestGui: coretlv.handle_message(message) switch_node = coretlv.session.get_node(switch_id, SwitchNode) - all_links = switch_node.all_link_data() + all_links = switch_node.links() assert len(all_links) == 1 def test_link_add_node_to_node(self, coretlv: CoreHandler): @@ -176,7 +176,7 @@ class TestGui: all_links = [] for node_id in coretlv.session.nodes: node = coretlv.session.nodes[node_id] - all_links += node.all_link_data() + all_links += node.links() assert len(all_links) == 1 def test_link_update(self, coretlv: CoreHandler): @@ -198,7 +198,7 @@ class TestGui: ) coretlv.handle_message(message) switch_node = coretlv.session.get_node(switch_id, SwitchNode) - all_links = switch_node.all_link_data() + all_links = switch_node.links() assert len(all_links) == 1 link = all_links[0] assert link.options.bandwidth is None @@ -216,7 +216,7 @@ class TestGui: coretlv.handle_message(message) switch_node = coretlv.session.get_node(switch_id, SwitchNode) - all_links = switch_node.all_link_data() + all_links = switch_node.links() assert len(all_links) == 1 link = all_links[0] assert link.options.bandwidth == bandwidth @@ -245,7 +245,7 @@ class TestGui: all_links = [] for node_id in coretlv.session.nodes: node = coretlv.session.nodes[node_id] - all_links += node.all_link_data() + all_links += node.links() assert len(all_links) == 1 message = coreapi.CoreLinkMessage.create( @@ -262,7 +262,7 @@ class TestGui: all_links = [] for node_id in coretlv.session.nodes: node = coretlv.session.nodes[node_id] - all_links += node.all_link_data() + all_links += node.links() assert len(all_links) == 0 def test_link_delete_node_to_net(self, coretlv: CoreHandler): @@ -284,7 +284,7 @@ class TestGui: ) coretlv.handle_message(message) switch_node = coretlv.session.get_node(switch_id, SwitchNode) - all_links = switch_node.all_link_data() + all_links = switch_node.links() assert len(all_links) == 1 message = coreapi.CoreLinkMessage.create( @@ -298,7 +298,7 @@ class TestGui: coretlv.handle_message(message) switch_node = coretlv.session.get_node(switch_id, SwitchNode) - all_links = switch_node.all_link_data() + all_links = switch_node.links() assert len(all_links) == 0 def test_link_delete_net_to_node(self, coretlv: CoreHandler): @@ -320,7 +320,7 @@ class TestGui: ) coretlv.handle_message(message) switch_node = coretlv.session.get_node(switch_id, SwitchNode) - all_links = switch_node.all_link_data() + all_links = switch_node.links() assert len(all_links) == 1 message = coreapi.CoreLinkMessage.create( @@ -334,7 +334,7 @@ class TestGui: coretlv.handle_message(message) switch_node = coretlv.session.get_node(switch_id, SwitchNode) - all_links = switch_node.all_link_data() + all_links = switch_node.links() assert len(all_links) == 0 def test_session_update(self, coretlv: CoreHandler): diff --git a/daemon/tests/test_links.py b/daemon/tests/test_links.py index 4078d8bc..535ad837 100644 --- a/daemon/tests/test_links.py +++ b/daemon/tests/test_links.py @@ -49,7 +49,7 @@ class TestLinks: session.add_link(node1.id, node2.id, iface1_data=iface1_data) # then - assert node2.all_link_data() + assert node2.links() assert node1.get_iface(iface1_data.id) def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): @@ -62,7 +62,7 @@ class TestLinks: session.add_link(node1.id, node2.id, iface2_data=iface2_data) # then - assert node1.all_link_data() + assert node1.links() assert node2.get_iface(iface2_data.id) def test_add_net_to_net(self, session): @@ -74,7 +74,7 @@ class TestLinks: session.add_link(node1.id, node2.id) # then - assert node1.all_link_data() + assert node1.links() def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index 91b598f3..fb8bc4d9 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -285,7 +285,7 @@ class TestXml: switch2 = session.get_node(node2_id, SwitchNode) assert switch1 assert switch2 - assert len(switch1.all_link_data() + switch2.all_link_data()) == 1 + assert len(switch1.links() + switch2.links()) == 1 def test_link_options( self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes @@ -345,7 +345,7 @@ class TestXml: links = [] for node_id in session.nodes: node = session.nodes[node_id] - links += node.all_link_data() + links += node.links() link = links[0] assert options.loss == link.options.loss assert options.bandwidth == link.options.bandwidth @@ -412,7 +412,7 @@ class TestXml: links = [] for node_id in session.nodes: node = session.nodes[node_id] - links += node.all_link_data() + links += node.links() link = links[0] assert options.loss == link.options.loss assert options.bandwidth == link.options.bandwidth @@ -490,7 +490,7 @@ class TestXml: links = [] for node_id in session.nodes: node = session.nodes[node_id] - links += node.all_link_data() + links += node.links() assert len(links) == 2 link1 = links[0] link2 = links[1] From d88f3a253548ff08f9204488b2c36f32b6d35a97 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 19 Jun 2020 08:50:36 -0700 Subject: [PATCH 0374/1131] daemon: refactored CoreInterface.addrlist storing strings into CoreInterface.ip4s and ip6s, stored as netaddr.IPNetwork objects --- daemon/core/api/grpc/grpcutils.py | 19 +++---- .../configservices/frrservices/services.py | 25 ++++----- .../configservices/nrlservices/services.py | 24 +++------ .../configservices/quaggaservices/services.py | 25 ++++----- .../sercurityservices/services.py | 10 ++-- .../configservices/utilservices/services.py | 36 ++++++------- daemon/core/emane/linkmonitor.py | 12 ++--- daemon/core/emulator/session.py | 5 +- daemon/core/nodes/base.py | 21 ++++---- daemon/core/nodes/interface.py | 46 ++++++++++++---- daemon/core/nodes/network.py | 34 ++++++------ daemon/core/services/bird.py | 9 ++-- daemon/core/services/frr.py | 40 ++++++-------- daemon/core/services/nrl.py | 9 ++-- daemon/core/services/quagga.py | 39 ++++++-------- daemon/core/services/sdn.py | 22 +++----- daemon/core/services/utility.py | 53 ++++++++++--------- daemon/core/services/xorp.py | 37 +++++-------- daemon/core/xml/corexmldeployment.py | 3 +- daemon/tests/test_nodes.py | 2 +- 20 files changed, 209 insertions(+), 262 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 2c13315c..adaf2549 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -3,7 +3,6 @@ import time from typing import Any, Dict, List, Tuple, Type, Union import grpc -import netaddr from grpc import ServicerContext from core import utils @@ -447,18 +446,16 @@ def iface_to_proto(iface: CoreInterface) -> core_pb2.Interface: net_id = iface.net.id ip4 = None ip4_mask = None + ip4_net = iface.get_ip4() + if ip4_net: + ip4 = str(ip4_net.ip) + ip4_mask = ip4_net.prefixlen ip6 = None ip6_mask = None - for addr in iface.addrlist: - network = netaddr.IPNetwork(addr) - mask = network.prefixlen - ip = str(network.ip) - if netaddr.valid_ipv4(ip) and not ip4: - ip4 = ip - ip4_mask = mask - elif netaddr.valid_ipv6(ip) and not ip6: - ip6 = ip - ip6_mask = mask + ip6_net = iface.get_ip6() + if ip6_net: + ip6 = str(ip6_net.ip) + ip6_mask = ip6_net.prefixlen return core_pb2.Interface( id=iface.node_id, net_id=net_id, diff --git a/daemon/core/configservices/frrservices/services.py b/daemon/core/configservices/frrservices/services.py index 2e24b40a..ce8c305c 100644 --- a/daemon/core/configservices/frrservices/services.py +++ b/daemon/core/configservices/frrservices/services.py @@ -1,8 +1,6 @@ import abc from typing import Any, Dict, List -import netaddr - from core import constants from core.config import Configuration from core.configservice.base import ConfigService, ConfigServiceMode @@ -49,10 +47,9 @@ def get_router_id(node: CoreNodeBase) -> str: Helper to return the first IPv4 address of a node as its router ID. """ for iface in node.get_ifaces(control=False): - for a in iface.addrlist: - a = a.split("/")[0] - if netaddr.valid_ipv4(a): - return a + ip4 = iface.get_ip4() + if ip4: + return str(ip4.ip) return "0.0.0.0" @@ -102,12 +99,10 @@ class FRRZebra(ConfigService): for iface in self.node.get_ifaces(): ip4s = [] ip6s = [] - for x in iface.addrlist: - addr = x.split("/")[0] - if netaddr.valid_ipv4(addr): - ip4s.append(x) - else: - ip6s.append(x) + for ip4 in iface.ip4s: + ip4s.append(str(ip4.ip)) + for ip6 in iface.ip6s: + ip6s.append(str(ip6.ip)) is_control = getattr(iface, "control", False) ifaces.append((iface, ip4s, ip6s, is_control)) @@ -163,10 +158,8 @@ class FRROspfv2(FrrService, ConfigService): router_id = get_router_id(self.node) addresses = [] for iface in self.node.get_ifaces(control=False): - for a in iface.addrlist: - addr = a.split("/")[0] - if netaddr.valid_ipv4(addr): - addresses.append(a) + for ip4 in iface.ip4s: + addresses.append(str(ip4.ip)) data = dict(router_id=router_id, addresses=addresses) text = """ router ospf diff --git a/daemon/core/configservices/nrlservices/services.py b/daemon/core/configservices/nrlservices/services.py index 0a5e8baf..cf9b4c88 100644 --- a/daemon/core/configservices/nrlservices/services.py +++ b/daemon/core/configservices/nrlservices/services.py @@ -1,7 +1,5 @@ from typing import Any, Dict, List -import netaddr - from core import utils from core.config import Configuration from core.configservice.base import ConfigService, ConfigServiceMode @@ -75,13 +73,10 @@ class NrlSmf(ConfigService): ip4_prefix = None for iface in self.node.get_ifaces(control=False): ifnames.append(iface.name) - if ip4_prefix: - continue - for a in iface.addrlist: - a = a.split("/")[0] - if netaddr.valid_ipv4(a): - ip4_prefix = f"{a}/{24}" - break + ip4 = iface.get_ip4() + if ip4: + ip4_prefix = f"{ip4.ip}/{24}" + break return dict( has_arouted=has_arouted, has_nhdp=has_nhdp, @@ -191,11 +186,8 @@ class Arouted(ConfigService): def data(self) -> Dict[str, Any]: ip4_prefix = None for iface in self.node.get_ifaces(control=False): - if ip4_prefix: - continue - for a in iface.addrlist: - a = a.split("/")[0] - if netaddr.valid_ipv4(a): - ip4_prefix = f"{a}/{24}" - break + ip4 = iface.get_ip4() + if ip4: + ip4_prefix = f"{ip4.ip}/{24}" + break return dict(ip4_prefix=ip4_prefix) diff --git a/daemon/core/configservices/quaggaservices/services.py b/daemon/core/configservices/quaggaservices/services.py index 40a1d7d3..e18e8a1a 100644 --- a/daemon/core/configservices/quaggaservices/services.py +++ b/daemon/core/configservices/quaggaservices/services.py @@ -2,8 +2,6 @@ import abc import logging from typing import Any, Dict, List -import netaddr - from core import constants from core.config import Configuration from core.configservice.base import ConfigService, ConfigServiceMode @@ -50,10 +48,9 @@ def get_router_id(node: CoreNodeBase) -> str: Helper to return the first IPv4 address of a node as its router ID. """ for iface in node.get_ifaces(control=False): - for a in iface.addrlist: - a = a.split("/")[0] - if netaddr.valid_ipv4(a): - return a + ip4 = iface.get_ip4() + if ip4: + return str(ip4.ip) return "0.0.0.0" @@ -103,12 +100,10 @@ class Zebra(ConfigService): for iface in self.node.get_ifaces(): ip4s = [] ip6s = [] - for x in iface.addrlist: - addr = x.split("/")[0] - if netaddr.valid_ipv4(addr): - ip4s.append(x) - else: - ip6s.append(x) + for ip4 in iface.ip4s: + ip4s.append(str(ip4.ip)) + for ip6 in iface.ip6s: + ip6s.append(str(ip6.ip)) is_control = getattr(iface, "control", False) ifaces.append((iface, ip4s, ip6s, is_control)) @@ -170,10 +165,8 @@ class Ospfv2(QuaggaService, ConfigService): router_id = get_router_id(self.node) addresses = [] for iface in self.node.get_ifaces(control=False): - for a in iface.addrlist: - addr = a.split("/")[0] - if netaddr.valid_ipv4(addr): - addresses.append(a) + for ip4 in iface.ip4s: + addresses.append(str(ip4.ip)) data = dict(router_id=router_id, addresses=addresses) text = """ router ospf diff --git a/daemon/core/configservices/sercurityservices/services.py b/daemon/core/configservices/sercurityservices/services.py index 5766b0db..4a58fd8c 100644 --- a/daemon/core/configservices/sercurityservices/services.py +++ b/daemon/core/configservices/sercurityservices/services.py @@ -1,7 +1,5 @@ from typing import Any, Dict, List -import netaddr - from core.config import Configuration from core.configservice.base import ConfigService, ConfigServiceMode from core.emulator.enumerations import ConfigDataTypes @@ -79,10 +77,10 @@ class VpnServer(ConfigService): def data(self) -> Dict[str, Any]: address = None for iface in self.node.get_ifaces(control=False): - for x in iface.addrlist: - addr = x.split("/")[0] - if netaddr.valid_ipv4(addr): - address = addr + ip4 = iface.get_ip4() + if ip4: + address = str(ip4.ip) + break return dict(address=address) diff --git a/daemon/core/configservices/utilservices/services.py b/daemon/core/configservices/utilservices/services.py index 983f6cff..8013bc41c 100644 --- a/daemon/core/configservices/utilservices/services.py +++ b/daemon/core/configservices/utilservices/services.py @@ -29,8 +29,8 @@ class DefaultRouteService(ConfigService): ifaces = self.node.get_ifaces() if ifaces: iface = ifaces[0] - for x in iface.addrlist: - net = netaddr.IPNetwork(x).cidr + for ip in iface.all_ips(): + net = ip.cidr if net.size > 1: router = net[1] routes.append(str(router)) @@ -76,15 +76,14 @@ class StaticRouteService(ConfigService): def data(self) -> Dict[str, Any]: routes = [] for iface in self.node.get_ifaces(control=False): - for x in iface.addrlist: - addr = x.split("/")[0] - if netaddr.valid_ipv6(addr): + for ip in iface.all_ips(): + address = str(ip.ip) + if netaddr.valid_ipv6(address): dst = "3ffe:4::/64" else: dst = "10.9.8.0/24" - net = netaddr.IPNetwork(x) - if net[-2] != net[1]: - routes.append((dst, net[1])) + if ip[-2] != ip[1]: + routes.append((dst, ip[1])) return dict(routes=routes) @@ -149,15 +148,12 @@ class DhcpService(ConfigService): def data(self) -> Dict[str, Any]: subnets = [] for iface in self.node.get_ifaces(control=False): - for x in iface.addrlist: - addr = x.split("/")[0] - if netaddr.valid_ipv4(addr): - net = netaddr.IPNetwork(x) - # divide the address space in half - index = (net.size - 2) / 2 - rangelow = net[index] - rangehigh = net[-2] - subnets.append((net.ip, net.netmask, rangelow, rangehigh, addr)) + for ip4 in iface.ip4s: + # divide the address space in half + index = (ip4.size - 2) / 2 + rangelow = ip4[index] + rangehigh = ip4[-2] + subnets.append((ip4.ip, ip4.netmask, rangelow, rangehigh, str(ip4.ip))) return dict(subnets=subnets) @@ -238,10 +234,8 @@ class RadvdService(ConfigService): ifaces = [] for iface in self.node.get_ifaces(control=False): prefixes = [] - for x in iface.addrlist: - addr = x.split("/")[0] - if netaddr.valid_ipv6(addr): - prefixes.append(x) + for ip6 in iface.ip6s: + prefixes.append(str(ip6)) if not prefixes: continue ifaces.append((iface.name, prefixes)) diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py index 1a9ac41a..295aaa1e 100644 --- a/daemon/core/emane/linkmonitor.py +++ b/daemon/core/emane/linkmonitor.py @@ -4,7 +4,6 @@ import threading import time from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple -import netaddr from lxml import etree from core.emulator.data import LinkData @@ -214,13 +213,12 @@ class EmaneLinkMonitor: for node in nodes: for iface in node.get_ifaces(): if isinstance(iface.net, CtrlNet): - ip4 = None - for x in iface.addrlist: - address, prefix = x.split("/") - if netaddr.valid_ipv4(address): - ip4 = address + address = None + ip4 = iface.get_ip4() if ip4: - addresses.append(ip4) + address = str(ip4.ip) + if address: + addresses.append(address) break return addresses diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 0b97da93..b0507269 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -1548,9 +1548,8 @@ class Session: entries = [] for iface in control_net.get_ifaces(): name = iface.node.name - for address in iface.addrlist: - address = address.split("/")[0] - entries.append(f"{address} {name}") + for ip in iface.all_ips(): + entries.append(f"{ip.ip} {name}") logging.info("Adding %d /etc/hosts file entries.", len(entries)) utils.file_munge("/etc/hosts", header, "\n".join(entries) + "\n") diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 2c8ca06c..90be59af 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -1053,18 +1053,17 @@ class CoreNetworkBase(NodeBase): if uni: unidirectional = 1 - iface2 = InterfaceData( + iface2_data = InterfaceData( id=linked_node.get_iface_id(iface), name=iface.name, mac=iface.mac ) - for address in iface.addrlist: - ip, _sep, mask = address.partition("/") - mask = int(mask) - if netaddr.valid_ipv4(ip): - iface2.ip4 = ip - iface2.ip4_mask = mask - else: - iface2.ip6 = ip - iface2.ip6_mask = mask + ip4 = iface.get_ip4() + if ip4: + iface2_data.ip4 = str(ip4.ip) + iface2_data.ip4_mask = ip4.prefixlen + ip6 = iface.get_ip6() + if ip6: + iface2_data.ip6 = str(ip6.ip) + iface2_data.ip6_mask = ip6.prefixlen options_data = iface.get_link_options(unidirectional) link_data = LinkData( @@ -1072,7 +1071,7 @@ class CoreNetworkBase(NodeBase): type=self.linktype, node1_id=self.id, node2_id=linked_node.id, - iface2=iface2, + iface2=iface2_data, options=options_data, ) all_links.append(link_data) diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 42522362..c1603a21 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -6,6 +6,8 @@ import logging import time from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple +import netaddr + from core import utils from core.emulator.data import LinkOptions from core.emulator.enumerations import TransportType @@ -52,7 +54,8 @@ class CoreInterface: self.net: Optional[CoreNetworkBase] = None self.othernet: Optional[CoreNetworkBase] = None self._params: Dict[str, float] = {} - self.addrlist: List[str] = [] + self.ip4s: List[netaddr.IPNetwork] = [] + self.ip6s: List[netaddr.IPNetwork] = [] self.mac: Optional[str] = None # placeholder position hook self.poshook: Callable[[CoreInterface], None] = lambda x: None @@ -131,15 +134,22 @@ class CoreInterface: if self.net is not None: self.net.detach(self) - def addaddr(self, addr: str) -> None: + def addaddr(self, address: str) -> None: """ - Add address. + Add ip address in the format "10.0.0.1/24". - :param addr: address to add + :param address: address to add :return: nothing """ - addr = utils.validate_ip(addr) - self.addrlist.append(addr) + try: + ip = netaddr.IPNetwork(address) + value = str(ip.ip) + if netaddr.valid_ipv4(value): + self.ip4s.append(ip) + else: + self.ip6s.append(ip) + except netaddr.AddrFormatError: + raise CoreError(f"adding invalid address {address}") def deladdr(self, addr: str) -> None: """ @@ -148,7 +158,23 @@ class CoreInterface: :param addr: address to delete :return: nothing """ - self.addrlist.remove(addr) + if netaddr.valid_ipv4(addr): + ip4 = netaddr.IPNetwork(addr) + self.ip4s.remove(ip4) + elif netaddr.valid_ipv6(addr): + ip6 = netaddr.IPNetwork(addr) + self.ip6s.remove(ip6) + else: + raise CoreError(f"deleting invalid address {addr}") + + def get_ip4(self) -> Optional[netaddr.IPNetwork]: + return next(iter(self.ip4s), None) + + def get_ip6(self) -> Optional[netaddr.IPNetwork]: + return next(iter(self.ip6s), None) + + def all_ips(self) -> List[netaddr.IPNetwork]: + return self.ip4s + self.ip6s def set_mac(self, mac: str) -> None: """ @@ -487,13 +513,13 @@ class TunTap(CoreInterface): def setaddrs(self) -> None: """ - Set interface addresses based on self.addrlist. + Set interface addresses. :return: nothing """ self.waitfordevicenode() - for addr in self.addrlist: - self.node.node_net_client.create_address(self.name, str(addr)) + for ip in self.all_ips(): + self.node.node_net_client.create_address(self.name, str(ip)) class GreTap(CoreInterface): diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 62443fb8..3f4ebfba 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -881,28 +881,26 @@ class PtpNet(CoreNetwork): iface1_data = InterfaceData( id=iface1.node.get_iface_id(iface1), name=iface1.name, mac=iface1.mac ) - for address in iface1.addrlist: - ip, _sep, mask = address.partition("/") - mask = int(mask) - if netaddr.valid_ipv4(ip): - iface1.ip4 = ip - iface1.ip4_mask = mask - else: - iface1.ip6 = ip - iface1.ip6_mask = mask + ip4 = iface1.get_ip4() + if ip4: + iface1_data.ip4 = str(ip4.ip) + iface1_data.ip4_mask = ip4.prefixlen + ip6 = iface1.get_ip6() + if ip6: + iface1_data.ip6 = str(ip6.ip) + iface1_data.ip6_mask = ip6.prefixlen iface2_data = InterfaceData( id=iface2.node.get_iface_id(iface2), name=iface2.name, mac=iface2.mac ) - for address in iface2.addrlist: - ip, _sep, mask = address.partition("/") - mask = int(mask) - if netaddr.valid_ipv4(ip): - iface2.ip4 = ip - iface2.ip4_mask = mask - else: - iface2.ip6 = ip - iface2.ip6_mask = mask + ip4 = iface2.get_ip4() + if ip4: + iface2_data.ip4 = str(ip4.ip) + iface2_data.ip4_mask = ip4.prefixlen + ip6 = iface2.get_ip6() + if ip6: + iface2_data.ip6 = str(ip6.ip) + iface2_data.ip6_mask = ip6.prefixlen options_data = iface1.get_link_options(unidirectional) link_data = LinkData( diff --git a/daemon/core/services/bird.py b/daemon/core/services/bird.py index a5052942..ffb177f3 100644 --- a/daemon/core/services/bird.py +++ b/daemon/core/services/bird.py @@ -3,8 +3,6 @@ bird.py: defines routing services provided by the BIRD Internet Routing Daemon. """ from typing import Optional, Tuple -import netaddr - from core.nodes.base import CoreNode from core.services.coreservices import CoreService @@ -39,10 +37,9 @@ class Bird(CoreService): Helper to return the first IPv4 address of a node as its router ID. """ for iface in node.get_ifaces(control=False): - for a in iface.addrlist: - a = a.split("/")[0] - if netaddr.valid_ipv4(a): - return a + ip4 = iface.get_ip4() + if ip4: + return str(ip4.ip) return "0.0.0.0" @classmethod diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py index e75d8f56..6b9ada3c 100644 --- a/daemon/core/services/frr.py +++ b/daemon/core/services/frr.py @@ -67,7 +67,7 @@ class FRRZebra(CoreService): # include control interfaces in addressing but not routing daemons if hasattr(iface, "control") and iface.control is True: cfg += " " - cfg += "\n ".join(map(cls.addrstr, iface.addrlist)) + cfg += "\n ".join(map(cls.addrstr, iface.all_ips())) cfg += "\n" continue cfgv4 = "" @@ -87,19 +87,13 @@ class FRRZebra(CoreService): cfgv4 += iface_config if want_ipv4: - ipv4list = filter( - lambda x: netaddr.valid_ipv4(x.split("/")[0]), iface.addrlist - ) cfg += " " - cfg += "\n ".join(map(cls.addrstr, ipv4list)) + cfg += "\n ".join(map(cls.addrstr, iface.ip4s)) cfg += "\n" cfg += cfgv4 if want_ipv6: - ipv6list = filter( - lambda x: netaddr.valid_ipv6(x.split("/")[0]), iface.addrlist - ) cfg += " " - cfg += "\n ".join(map(cls.addrstr, ipv6list)) + cfg += "\n ".join(map(cls.addrstr, iface.ip6s)) cfg += "\n" cfg += cfgv6 cfg += "!\n" @@ -111,17 +105,17 @@ class FRRZebra(CoreService): return cfg @staticmethod - def addrstr(x: str) -> str: + def addrstr(ip: netaddr.IPNetwork) -> str: """ helper for mapping IP addresses to zebra config statements """ - addr = x.split("/")[0] - if netaddr.valid_ipv4(addr): - return "ip address %s" % x - elif netaddr.valid_ipv6(addr): - return "ipv6 address %s" % x + address = str(ip.ip) + if netaddr.valid_ipv4(address): + return "ip address %s" % ip + elif netaddr.valid_ipv6(address): + return "ipv6 address %s" % ip else: - raise ValueError("invalid address: %s", x) + raise ValueError("invalid address: %s", ip) @classmethod def generate_frr_boot(cls, node: CoreNode) -> str: @@ -333,10 +327,9 @@ class FrrService(CoreService): Helper to return the first IPv4 address of a node as its router ID. """ for iface in node.get_ifaces(control=False): - for a in iface.addrlist: - a = a.split("/")[0] - if netaddr.valid_ipv4(a): - return a + ip4 = iface.get_ip4() + if ip4: + return str(ip4.ip) return "0.0.0.0" @staticmethod @@ -413,11 +406,8 @@ class FRROspfv2(FrrService): cfg += " router-id %s\n" % rtrid # network 10.0.0.0/24 area 0 for iface in node.get_ifaces(control=False): - for a in iface.addrlist: - addr = a.split("/")[0] - if not netaddr.valid_ipv4(addr): - continue - cfg += " network %s area 0\n" % a + for ip4 in iface.ip4s: + cfg += f" network {ip4} area 0\n" cfg += "!\n" return cfg diff --git a/daemon/core/services/nrl.py b/daemon/core/services/nrl.py index 9933b130..697f4eee 100644 --- a/daemon/core/services/nrl.py +++ b/daemon/core/services/nrl.py @@ -4,8 +4,6 @@ nrl.py: defines services provided by NRL protolib tools hosted here: """ from typing import Optional, Tuple -import netaddr - from core import utils from core.nodes.base import CoreNode from core.services.coreservices import CoreService @@ -32,10 +30,9 @@ class NrlService(CoreService): interface's prefix length, so e.g. '/32' can turn into '/24'. """ for iface in node.get_ifaces(control=False): - for a in iface.addrlist: - a = a.split("/")[0] - if netaddr.valid_ipv4(a): - return f"{a}/{prefixlen}" + ip4 = iface.get_ip4() + if ip4: + return f"{ip4.ip}/{prefixlen}" return "0.0.0.0/%s" % prefixlen diff --git a/daemon/core/services/quagga.py b/daemon/core/services/quagga.py index 30d14353..7f717e59 100644 --- a/daemon/core/services/quagga.py +++ b/daemon/core/services/quagga.py @@ -64,7 +64,7 @@ class Zebra(CoreService): # include control interfaces in addressing but not routing daemons if getattr(iface, "control", False): cfg += " " - cfg += "\n ".join(map(cls.addrstr, iface.addrlist)) + cfg += "\n ".join(map(cls.addrstr, iface.all_ips())) cfg += "\n" continue cfgv4 = "" @@ -86,19 +86,13 @@ class Zebra(CoreService): cfgv4 += iface_config if want_ipv4: - ipv4list = filter( - lambda x: netaddr.valid_ipv4(x.split("/")[0]), iface.addrlist - ) cfg += " " - cfg += "\n ".join(map(cls.addrstr, ipv4list)) + cfg += "\n ".join(map(cls.addrstr, iface.ip4s)) cfg += "\n" cfg += cfgv4 if want_ipv6: - ipv6list = filter( - lambda x: netaddr.valid_ipv6(x.split("/")[0]), iface.addrlist - ) cfg += " " - cfg += "\n ".join(map(cls.addrstr, ipv6list)) + cfg += "\n ".join(map(cls.addrstr, iface.ip6s)) cfg += "\n" cfg += cfgv6 cfg += "!\n" @@ -112,17 +106,17 @@ class Zebra(CoreService): return cfg @staticmethod - def addrstr(x: str) -> str: + def addrstr(ip: netaddr.IPNetwork) -> str: """ helper for mapping IP addresses to zebra config statements """ - addr = x.split("/")[0] - if netaddr.valid_ipv4(addr): - return "ip address %s" % x - elif netaddr.valid_ipv6(addr): - return "ipv6 address %s" % x + address = str(ip.ip) + if netaddr.valid_ipv4(address): + return "ip address %s" % ip + elif netaddr.valid_ipv6(address): + return "ipv6 address %s" % ip else: - raise ValueError("invalid address: %s", x) + raise ValueError("invalid address: %s", ip) @classmethod def generate_quagga_boot(cls, node: CoreNode) -> str: @@ -255,10 +249,9 @@ class QuaggaService(CoreService): Helper to return the first IPv4 address of a node as its router ID. """ for iface in node.get_ifaces(control=False): - for a in iface.addrlist: - a = a.split("/")[0] - if netaddr.valid_ipv4(a): - return a + ip4 = iface.get_ip4() + if ip4: + return str(ip4.ip) return f"0.0.0.{node.id:d}" @staticmethod @@ -335,10 +328,8 @@ class Ospfv2(QuaggaService): cfg += " router-id %s\n" % rtrid # network 10.0.0.0/24 area 0 for iface in node.get_ifaces(control=False): - for a in iface.addrlist: - addr = a.split("/")[0] - if netaddr.valid_ipv4(addr): - cfg += " network %s area 0\n" % a + for ip4 in iface.ip4s: + cfg += f" network {ip4} area 0\n" cfg += "!\n" return cfg diff --git a/daemon/core/services/sdn.py b/daemon/core/services/sdn.py index 1f17201d..ef077662 100644 --- a/daemon/core/services/sdn.py +++ b/daemon/core/services/sdn.py @@ -5,8 +5,6 @@ sdn.py defines services to start Open vSwitch and the Ryu SDN Controller. import re from typing import Tuple -import netaddr - from core.nodes.base import CoreNode from core.services.coreservices import CoreService @@ -65,18 +63,14 @@ class OvsService(SdnService): # remove ip address of eths because quagga/zebra will assign same IPs to rtr interfaces # or assign them manually to rtr interfaces if zebra is not running - for addr in iface.addrlist: - addr = addr.split("/")[0] - if netaddr.valid_ipv4(addr): - cfg += "ip addr del %s dev %s\n" % (addr, iface.name) - if has_zebra == 0: - cfg += "ip addr add %s dev rtr%s\n" % (addr, ifnum) - elif netaddr.valid_ipv6(addr): - cfg += "ip -6 addr del %s dev %s\n" % (addr, iface.name) - if has_zebra == 0: - cfg += "ip -6 addr add %s dev rtr%s\n" % (addr, ifnum) - else: - raise ValueError("invalid address: %s" % addr) + for ip4 in iface.ip4s: + cfg += "ip addr del %s dev %s\n" % (ip4.ip, iface.name) + if has_zebra == 0: + cfg += "ip addr add %s dev rtr%s\n" % (ip4.ip, ifnum) + for ip6 in iface.ip6s: + cfg += "ip -6 addr del %s dev %s\n" % (ip6.ip, iface.name) + if has_zebra == 0: + cfg += "ip -6 addr add %s dev rtr%s\n" % (ip6.ip, ifnum) # add interfaces to bridge # Make port numbers explicit so they're easier to follow in reading the script diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index a44037f6..5efade1a 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -74,8 +74,8 @@ class DefaultRouteService(UtilService): ifaces = node.get_ifaces() if ifaces: iface = ifaces[0] - for x in iface.addrlist: - net = netaddr.IPNetwork(x).cidr + for ip in iface.all_ips(): + net = ip.cidr if net.size > 1: router = net[1] routes.append(str(router)) @@ -118,23 +118,22 @@ class StaticRouteService(UtilService): cfg += "# NOTE: this service must be customized to be of any use\n" cfg += "# Below are samples that you can uncomment and edit.\n#\n" for iface in node.get_ifaces(control=False): - cfg += "\n".join(map(cls.routestr, iface.addrlist)) + cfg += "\n".join(map(cls.routestr, iface.all_ips())) cfg += "\n" return cfg @staticmethod - def routestr(x: str) -> str: - addr = x.split("/")[0] - if netaddr.valid_ipv6(addr): + def routestr(ip: netaddr.IPNetwork) -> str: + address = str(ip.ip) + if netaddr.valid_ipv6(address): dst = "3ffe:4::/64" else: dst = "10.9.8.0/24" - net = netaddr.IPNetwork(x) - if net[-2] == net[1]: + if ip[-2] == ip[1]: return "" else: rtcmd = "#/sbin/ip route add %s via" % dst - return "%s %s" % (rtcmd, net[1]) + return "%s %s" % (rtcmd, ip[1]) class SshService(UtilService): @@ -242,25 +241,24 @@ max-lease-time 7200; ddns-update-style none; """ for iface in node.get_ifaces(control=False): - cfg += "\n".join(map(cls.subnetentry, iface.addrlist)) + cfg += "\n".join(map(cls.subnetentry, iface.all_ips())) cfg += "\n" return cfg @staticmethod - def subnetentry(x: str) -> str: + def subnetentry(ip: netaddr.IPNetwork) -> str: """ Generate a subnet declaration block given an IPv4 prefix string for inclusion in the dhcpd3 config file. """ - addr = x.split("/")[0] - if netaddr.valid_ipv6(addr): + address = str(ip.ip) + if netaddr.valid_ipv6(address): return "" else: - net = netaddr.IPNetwork(x) # divide the address space in half - index = (net.size - 2) / 2 - rangelow = net[index] - rangehigh = net[-2] + index = (ip.size - 2) / 2 + rangelow = ip[index] + rangehigh = ip[-2] return """ subnet %s netmask %s { pool { @@ -270,11 +268,11 @@ subnet %s netmask %s { } } """ % ( - net.ip, - net.netmask, + ip.ip, + ip.netmask, rangelow, rangehigh, - addr, + address, ) @@ -557,7 +555,10 @@ export LANG % node.name ) for iface in node.get_ifaces(control=False): - body += "
  • %s - %s
  • \n" % (iface.name, iface.addrlist) + body += "
  • %s - %s
  • \n" % ( + iface.name, + [str(x) for x in iface.all_ips()], + ) return "%s" % body @@ -625,7 +626,7 @@ class RadvdService(UtilService): """ cfg = "# auto-generated by RADVD service (utility.py)\n" for iface in node.get_ifaces(control=False): - prefixes = list(map(cls.subnetentry, iface.addrlist)) + prefixes = list(map(cls.subnetentry, iface.all_ips())) if len(prefixes) < 1: continue cfg += ( @@ -658,14 +659,14 @@ interface %s return cfg @staticmethod - def subnetentry(x: str) -> str: + def subnetentry(ip: netaddr.IPNetwork) -> str: """ Generate a subnet declaration block given an IPv6 prefix string for inclusion in the RADVD config file. """ - addr = x.split("/")[0] - if netaddr.valid_ipv6(addr): - return x + address = str(ip.ip) + if netaddr.valid_ipv6(address): + return str(ip) else: return "" diff --git a/daemon/core/services/xorp.py b/daemon/core/services/xorp.py index 42082377..7c24478a 100644 --- a/daemon/core/services/xorp.py +++ b/daemon/core/services/xorp.py @@ -40,7 +40,7 @@ class XorpRtrmgr(CoreService): for iface in node.get_ifaces(): cfg += " interface %s {\n" % iface.name cfg += "\tvif %s {\n" % iface.name - cfg += "".join(map(cls.addrstr, iface.addrlist)) + cfg += "".join(map(cls.addrstr, iface.all_ips())) cfg += cls.lladdrstr(iface) cfg += "\t}\n" cfg += " }\n" @@ -55,13 +55,12 @@ class XorpRtrmgr(CoreService): return cfg @staticmethod - def addrstr(x: str) -> str: + def addrstr(ip: netaddr.IPNetwork) -> str: """ helper for mapping IP addresses to XORP config statements """ - addr, plen = x.split("/") - cfg = "\t address %s {\n" % addr - cfg += "\t\tprefix-length: %s\n" % plen + cfg = "\t address %s {\n" % ip.ip + cfg += "\t\tprefix-length: %s\n" % ip.prefixlen cfg += "\t }\n" return cfg @@ -145,10 +144,9 @@ class XorpService(CoreService): Helper to return the first IPv4 address of a node as its router ID. """ for iface in node.get_ifaces(control=False): - for a in iface.addrlist: - a = a.split("/")[0] - if netaddr.valid_ipv4(a): - return a + ip4 = iface.get_ip4() + if ip4: + return str(ip4.ip) return "0.0.0.0" @classmethod @@ -180,11 +178,8 @@ class XorpOspfv2(XorpService): for iface in node.get_ifaces(control=False): cfg += "\t interface %s {\n" % iface.name cfg += "\t\tvif %s {\n" % iface.name - for a in iface.addrlist: - addr = a.split("/")[0] - if not netaddr.valid_ipv4(addr): - continue - cfg += "\t\t address %s {\n" % addr + for ip4 in iface.ip4s: + cfg += "\t\t address %s {\n" % ip4.ip cfg += "\t\t }\n" cfg += "\t\t}\n" cfg += "\t }\n" @@ -269,11 +264,8 @@ class XorpRip(XorpService): for iface in node.get_ifaces(control=False): cfg += "\tinterface %s {\n" % iface.name cfg += "\t vif %s {\n" % iface.name - for a in iface.addrlist: - addr = a.split("/")[0] - if not netaddr.valid_ipv4(addr): - continue - cfg += "\t\taddress %s {\n" % addr + for ip4 in iface.ip4s: + cfg += "\t\taddress %s {\n" % ip4.ip cfg += "\t\t disable: false\n" cfg += "\t\t}\n" cfg += "\t }\n" @@ -435,11 +427,8 @@ class XorpOlsr(XorpService): for iface in node.get_ifaces(control=False): cfg += "\tinterface %s {\n" % iface.name cfg += "\t vif %s {\n" % iface.name - for a in iface.addrlist: - addr = a.split("/")[0] - if not netaddr.valid_ipv4(addr): - continue - cfg += "\t\taddress %s {\n" % addr + for ip4 in iface.ip4s: + cfg += "\t\taddress %s {\n" % ip4.ip cfg += "\t\t}\n" cfg += "\t }\n" cfg += "\t}\n" diff --git a/daemon/core/xml/corexmldeployment.py b/daemon/core/xml/corexmldeployment.py index 7954b71a..d84f2246 100644 --- a/daemon/core/xml/corexmldeployment.py +++ b/daemon/core/xml/corexmldeployment.py @@ -164,6 +164,7 @@ class CoreXmlDeployment: if emane_element is not None: parent_element = emane_element - for address in iface.addrlist: + for ip in iface.all_ips(): + address = str(ip.ip) address_type = get_address_type(address) add_address(parent_element, address_type, address, iface.name) diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index 8af2e895..1e89f5e4 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -87,7 +87,7 @@ class TestNodes: node.addaddr(iface.node_id, addr) # then - assert iface.addrlist[0] == addr + assert str(iface.get_ip4()) == addr def test_node_addaddr_exception(self, session): # given From 20feea8f12fe3abaf598d485f9c38395c0ab6299 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 19 Jun 2020 10:54:58 -0700 Subject: [PATCH 0375/1131] daemon: refactored usages of addr to ip and updated functions to align --- .../configservices/utilservices/services.py | 4 +- daemon/core/emulator/session.py | 2 +- daemon/core/nodes/base.py | 65 +++++++++-------- daemon/core/nodes/interface.py | 56 ++++++++++----- daemon/core/nodes/network.py | 23 +++--- daemon/core/nodes/physical.py | 71 ++++++++++--------- daemon/core/services/frr.py | 2 +- daemon/core/services/quagga.py | 2 +- daemon/core/services/utility.py | 13 ++-- daemon/core/services/xorp.py | 2 +- daemon/core/xml/corexmldeployment.py | 2 +- daemon/tests/test_nodes.py | 14 ++-- 12 files changed, 138 insertions(+), 118 deletions(-) diff --git a/daemon/core/configservices/utilservices/services.py b/daemon/core/configservices/utilservices/services.py index 8013bc41c..b6bc0eb5 100644 --- a/daemon/core/configservices/utilservices/services.py +++ b/daemon/core/configservices/utilservices/services.py @@ -29,7 +29,7 @@ class DefaultRouteService(ConfigService): ifaces = self.node.get_ifaces() if ifaces: iface = ifaces[0] - for ip in iface.all_ips(): + for ip in iface.ips(): net = ip.cidr if net.size > 1: router = net[1] @@ -76,7 +76,7 @@ class StaticRouteService(ConfigService): def data(self) -> Dict[str, Any]: routes = [] for iface in self.node.get_ifaces(control=False): - for ip in iface.all_ips(): + for ip in iface.ips(): address = str(ip.ip) if netaddr.valid_ipv6(address): dst = "3ffe:4::/64" diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index b0507269..630e1a0f 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -1548,7 +1548,7 @@ class Session: entries = [] for iface in control_net.get_ifaces(): name = iface.node.name - for ip in iface.all_ips(): + for ip in iface.ips(): entries.append(f"{ip.ip} {name}") logging.info("Adding %d /etc/hosts file entries.", len(entries)) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 90be59af..7eff9b12 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -7,7 +7,7 @@ import os import shutil import threading from threading import RLock -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union import netaddr @@ -138,6 +138,13 @@ class NodeBase(abc.ABC): return self.position.get() def get_iface(self, iface_id: int) -> CoreInterface: + """ + Retrieve interface based on id. + + :param iface_id: id of interface to retrieve + :return: interface + :raises CoreError: when interface does not exist + """ if iface_id not in self.ifaces: raise CoreError(f"node({self.name}) does not have interface({iface_id})") return self.ifaces[iface_id] @@ -436,7 +443,6 @@ class CoreNode(CoreNodeBase): """ apitype: NodeTypes = NodeTypes.DEFAULT - valid_address_types: Set[str] = {"inet", "inet6", "inet6link"} def __init__( self, @@ -750,40 +756,39 @@ class CoreNode(CoreNodeBase): if self.up: self.node_net_client.device_mac(iface.name, mac) - def addaddr(self, iface_id: int, addr: str) -> None: + def add_ip(self, iface_id: int, ip: str) -> None: """ - Add interface address. + Add an ip address to an interface in the format "10.0.0.1/24". :param iface_id: id of interface to add address to - :param addr: address to add to interface - :return: nothing - """ - addr = utils.validate_ip(addr) - iface = self.get_iface(iface_id) - iface.addaddr(addr) - if self.up: - # ipv4 check - broadcast = None - if netaddr.valid_ipv4(addr): - broadcast = "+" - self.node_net_client.create_address(iface.name, addr, broadcast) - - def deladdr(self, iface_id: int, addr: str) -> None: - """ - Delete address from an interface. - - :param iface_id: id of interface to delete address from - :param addr: address to delete from interface + :param ip: address to add to interface :return: nothing + :raises CoreError: when ip address provided is invalid :raises CoreCommandError: when a non-zero exit status occurs """ iface = self.get_iface(iface_id) - try: - iface.deladdr(addr) - except ValueError: - logging.exception("trying to delete unknown address: %s", addr) + iface.add_ip(ip) if self.up: - self.node_net_client.delete_address(iface.name, addr) + # ipv4 check + broadcast = None + if netaddr.valid_ipv4(ip): + broadcast = "+" + self.node_net_client.create_address(iface.name, ip, broadcast) + + def remove_ip(self, iface_id: int, ip: str) -> None: + """ + Remove an ip address from an interface in the format "10.0.0.1/24". + + :param iface_id: id of interface to delete address from + :param ip: ip address to remove from interface + :return: nothing + :raises CoreError: when ip address provided is invalid + :raises CoreCommandError: when a non-zero exit status occurs + """ + iface = self.get_iface(iface_id) + iface.remove_ip(ip) + if self.up: + self.node_net_client.delete_address(iface.name, ip) def ifup(self, iface_id: int) -> None: """ @@ -819,14 +824,14 @@ class CoreNode(CoreNodeBase): iface = self.get_iface(iface_id) iface.set_mac(iface_data.mac) for address in addresses: - iface.addaddr(address) + iface.add_ip(address) else: iface_id = self.newveth(iface_data.id, iface_data.name) self.attachnet(iface_id, net) if iface_data.mac: self.set_mac(iface_id, iface_data.mac) for address in addresses: - self.addaddr(iface_id, address) + self.add_ip(iface_id, address) self.ifup(iface_id) iface = self.get_iface(iface_id) return iface diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index c1603a21..c613f0cd 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -134,46 +134,64 @@ class CoreInterface: if self.net is not None: self.net.detach(self) - def addaddr(self, address: str) -> None: + def add_ip(self, ip: str) -> None: """ Add ip address in the format "10.0.0.1/24". - :param address: address to add + :param ip: ip address to add :return: nothing + :raises CoreError: when ip address provided is invalid """ try: - ip = netaddr.IPNetwork(address) - value = str(ip.ip) - if netaddr.valid_ipv4(value): + ip = netaddr.IPNetwork(ip) + address = str(ip.ip) + if netaddr.valid_ipv4(address): self.ip4s.append(ip) else: self.ip6s.append(ip) except netaddr.AddrFormatError: - raise CoreError(f"adding invalid address {address}") + raise CoreError(f"adding invalid address {ip}") - def deladdr(self, addr: str) -> None: + def remove_ip(self, ip: str) -> None: """ - Delete address. + Remove ip address in the format "10.0.0.1/24". - :param addr: address to delete + :param ip: ip address to delete :return: nothing + :raises CoreError: when ip address provided is invalid """ - if netaddr.valid_ipv4(addr): - ip4 = netaddr.IPNetwork(addr) - self.ip4s.remove(ip4) - elif netaddr.valid_ipv6(addr): - ip6 = netaddr.IPNetwork(addr) - self.ip6s.remove(ip6) - else: - raise CoreError(f"deleting invalid address {addr}") + try: + ip = netaddr.IPNetwork(ip) + address = str(ip.ip) + if netaddr.valid_ipv4(address): + self.ip4s.remove(ip) + else: + self.ip6s.remove(ip) + except (netaddr.AddrFormatError, ValueError): + raise CoreError(f"deleting invalid address {ip}") def get_ip4(self) -> Optional[netaddr.IPNetwork]: + """ + Looks for the first ip4 address. + + :return: ip4 address, None otherwise + """ return next(iter(self.ip4s), None) def get_ip6(self) -> Optional[netaddr.IPNetwork]: + """ + Looks for the first ip6 address. + + :return: ip6 address, None otherwise + """ return next(iter(self.ip6s), None) - def all_ips(self) -> List[netaddr.IPNetwork]: + def ips(self) -> List[netaddr.IPNetwork]: + """ + Retrieve a list of all ip4 and ip6 addresses combined. + + :return: ip4 and ip6 addresses + """ return self.ip4s + self.ip6s def set_mac(self, mac: str) -> None: @@ -518,7 +536,7 @@ class TunTap(CoreInterface): :return: nothing """ self.waitfordevicenode() - for ip in self.all_ips(): + for ip in self.ips(): self.node.node_net_client.create_address(self.name, str(ip)) diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 3f4ebfba..559b7ece 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -575,18 +575,17 @@ class CoreNetwork(CoreNetworkBase): return iface return None - def addrconfig(self, addrlist: List[str]) -> None: + def add_ips(self, ips: List[str]) -> None: """ - Set addresses on the bridge. + Add ip addresses on the bridge in the format "10.0.0.1/24". - :param addrlist: address list + :param ips: ip address to add :return: nothing """ if not self.up: return - - for addr in addrlist: - self.net_client.create_address(self.brname, str(addr)) + for ip in ips: + self.net_client.create_address(self.brname, ip) class GreTapBridge(CoreNetwork): @@ -663,22 +662,22 @@ class GreTapBridge(CoreNetwork): self.gretap = None super().shutdown() - def addrconfig(self, addrlist: List[str]) -> None: + def add_ips(self, ips: List[str]) -> None: """ Set the remote tunnel endpoint. This is a one-time method for creating the GreTap device, which requires the remoteip at startup. The 1st address in the provided list is remoteip, 2nd optionally specifies localip. - :param addrlist: address list + :param ips: address list :return: nothing """ if self.gretap: raise ValueError(f"gretap already exists for {self.name}") - remoteip = addrlist[0].split("/")[0] + remoteip = ips[0].split("/")[0] localip = None - if len(addrlist) > 1: - localip = addrlist[1].split("/")[0] + if len(ips) > 1: + localip = ips[1].split("/")[0] self.gretap = GreTap( session=self.session, remoteip=remoteip, @@ -700,7 +699,7 @@ class GreTapBridge(CoreNetwork): self.grekey = key addresses = iface_data.get_addresses() if addresses: - self.addrconfig(addresses) + self.add_ips(addresses) class CtrlNet(CoreNetwork): diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 0ce8946a..96440bcb 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -75,43 +75,43 @@ class PhysicalNode(CoreNodeBase): :raises CoreCommandError: when a non-zero exit status occurs """ mac = utils.validate_mac(mac) - iface = self.ifaces[iface_id] + iface = self.get_iface(iface_id) iface.set_mac(mac) if self.up: self.net_client.device_mac(iface.name, mac) - def addaddr(self, iface_id: int, addr: str) -> None: + def add_ip(self, iface_id: int, ip: str) -> None: """ - Add an address to an interface. + Add an ip address to an interface in the format "10.0.0.1/24". - :param iface_id: index of interface to add address to - :param addr: address to add + :param iface_id: id of interface to add address to + :param ip: address to add to interface :return: nothing + :raises CoreError: when ip address provided is invalid + :raises CoreCommandError: when a non-zero exit status occurs """ - addr = utils.validate_ip(addr) iface = self.get_iface(iface_id) + iface.add_ip(ip) if self.up: - self.net_client.create_address(iface.name, addr) - iface.addaddr(addr) + self.net_client.create_address(iface.name, ip) - def deladdr(self, iface_id: int, addr: str) -> None: + def remove_ip(self, iface_id: int, ip: str) -> None: """ - Delete an address from an interface. + Remove an ip address from an interface in the format "10.0.0.1/24". - :param iface_id: index of interface to delete - :param addr: address to delete + :param iface_id: id of interface to delete address from + :param ip: ip address to remove from interface :return: nothing + :raises CoreError: when ip address provided is invalid + :raises CoreCommandError: when a non-zero exit status occurs """ - iface = self.ifaces[iface_id] - try: - iface.deladdr(addr) - except ValueError: - logging.exception("trying to delete unknown address: %s", addr) + iface = self.get_iface(iface_id) + iface.remove_ip(ip) if self.up: - self.net_client.delete_address(iface.name, addr) + self.net_client.delete_address(iface.name, ip) def adopt_iface( - self, iface: CoreInterface, iface_id: int, mac: str, addrlist: List[str] + self, iface: CoreInterface, iface_id: int, mac: str, ips: List[str] ) -> None: """ When a link message is received linking this node to another part of @@ -128,8 +128,8 @@ class PhysicalNode(CoreNodeBase): iface.localname = iface.name if mac: self.set_mac(iface_id, mac) - for addr in addrlist: - self.addaddr(iface_id, addr) + for ip in ips: + self.add_ip(iface_id, ip) if self.up: self.net_client.device_up(iface.localname) @@ -317,7 +317,7 @@ class Rj45Node(CoreNodeBase): if net is not None: self.iface.attachnet(net) for addr in iface_data.get_addresses(): - self.addaddr(addr) + self.add_ip(addr) return self.iface def delete_iface(self, iface_id: int) -> None: @@ -348,30 +348,31 @@ class Rj45Node(CoreNodeBase): raise CoreError(f"node({self.name}) does not have interface({iface.name})") return self.iface_id - def addaddr(self, addr: str) -> None: + def add_ip(self, ip: str) -> None: """ - Add address to to network interface. + Add an ip address to an interface in the format "10.0.0.1/24". - :param addr: address to add + :param ip: address to add to interface :return: nothing - :raises CoreCommandError: when there is a command exception + :raises CoreError: when ip address provided is invalid + :raises CoreCommandError: when a non-zero exit status occurs """ - addr = utils.validate_ip(addr) + self.iface.add_ip(ip) if self.up: - self.net_client.create_address(self.name, addr) - self.iface.addaddr(addr) + self.net_client.create_address(self.name, ip) - def deladdr(self, addr: str) -> None: + def remove_ip(self, ip: str) -> None: """ - Delete address from network interface. + Remove an ip address from an interface in the format "10.0.0.1/24". - :param addr: address to delete + :param ip: ip address to remove from interface :return: nothing - :raises CoreCommandError: when there is a command exception + :raises CoreError: when ip address provided is invalid + :raises CoreCommandError: when a non-zero exit status occurs """ + self.iface.remove_ip(ip) if self.up: - self.net_client.delete_address(self.name, addr) - self.iface.deladdr(addr) + self.net_client.delete_address(self.name, ip) def savestate(self) -> None: """ diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py index 6b9ada3c..632b4557 100644 --- a/daemon/core/services/frr.py +++ b/daemon/core/services/frr.py @@ -67,7 +67,7 @@ class FRRZebra(CoreService): # include control interfaces in addressing but not routing daemons if hasattr(iface, "control") and iface.control is True: cfg += " " - cfg += "\n ".join(map(cls.addrstr, iface.all_ips())) + cfg += "\n ".join(map(cls.addrstr, iface.ips())) cfg += "\n" continue cfgv4 = "" diff --git a/daemon/core/services/quagga.py b/daemon/core/services/quagga.py index 7f717e59..cb9e6b08 100644 --- a/daemon/core/services/quagga.py +++ b/daemon/core/services/quagga.py @@ -64,7 +64,7 @@ class Zebra(CoreService): # include control interfaces in addressing but not routing daemons if getattr(iface, "control", False): cfg += " " - cfg += "\n ".join(map(cls.addrstr, iface.all_ips())) + cfg += "\n ".join(map(cls.addrstr, iface.ips())) cfg += "\n" continue cfgv4 = "" diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index 5efade1a..414f994e 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -74,7 +74,7 @@ class DefaultRouteService(UtilService): ifaces = node.get_ifaces() if ifaces: iface = ifaces[0] - for ip in iface.all_ips(): + for ip in iface.ips(): net = ip.cidr if net.size > 1: router = net[1] @@ -118,7 +118,7 @@ class StaticRouteService(UtilService): cfg += "# NOTE: this service must be customized to be of any use\n" cfg += "# Below are samples that you can uncomment and edit.\n#\n" for iface in node.get_ifaces(control=False): - cfg += "\n".join(map(cls.routestr, iface.all_ips())) + cfg += "\n".join(map(cls.routestr, iface.ips())) cfg += "\n" return cfg @@ -241,7 +241,7 @@ max-lease-time 7200; ddns-update-style none; """ for iface in node.get_ifaces(control=False): - cfg += "\n".join(map(cls.subnetentry, iface.all_ips())) + cfg += "\n".join(map(cls.subnetentry, iface.ips())) cfg += "\n" return cfg @@ -555,10 +555,7 @@ export LANG % node.name ) for iface in node.get_ifaces(control=False): - body += "
  • %s - %s
  • \n" % ( - iface.name, - [str(x) for x in iface.all_ips()], - ) + body += "
  • %s - %s
  • \n" % (iface.name, [str(x) for x in iface.ips()]) return "%s" % body @@ -626,7 +623,7 @@ class RadvdService(UtilService): """ cfg = "# auto-generated by RADVD service (utility.py)\n" for iface in node.get_ifaces(control=False): - prefixes = list(map(cls.subnetentry, iface.all_ips())) + prefixes = list(map(cls.subnetentry, iface.ips())) if len(prefixes) < 1: continue cfg += ( diff --git a/daemon/core/services/xorp.py b/daemon/core/services/xorp.py index 7c24478a..a9687d45 100644 --- a/daemon/core/services/xorp.py +++ b/daemon/core/services/xorp.py @@ -40,7 +40,7 @@ class XorpRtrmgr(CoreService): for iface in node.get_ifaces(): cfg += " interface %s {\n" % iface.name cfg += "\tvif %s {\n" % iface.name - cfg += "".join(map(cls.addrstr, iface.all_ips())) + cfg += "".join(map(cls.addrstr, iface.ips())) cfg += cls.lladdrstr(iface) cfg += "\t}\n" cfg += " }\n" diff --git a/daemon/core/xml/corexmldeployment.py b/daemon/core/xml/corexmldeployment.py index d84f2246..6035bd26 100644 --- a/daemon/core/xml/corexmldeployment.py +++ b/daemon/core/xml/corexmldeployment.py @@ -164,7 +164,7 @@ class CoreXmlDeployment: if emane_element is not None: parent_element = emane_element - for ip in iface.all_ips(): + for ip in iface.ips(): address = str(ip.ip) address_type = get_address_type(address) add_address(parent_element, address_type, address, iface.name) diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index 1e89f5e4..25a62c5f 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -75,31 +75,31 @@ class TestNodes: with pytest.raises(CoreError): node.set_mac(iface.node_id, mac) - def test_node_addaddr(self, session: Session): + def test_node_add_ip(self, session: Session): # given node = session.add_node(CoreNode) switch = session.add_node(SwitchNode) iface_data = InterfaceData() iface = node.new_iface(switch, iface_data) - addr = "192.168.0.1/24" + ip = "192.168.0.1/24" # when - node.addaddr(iface.node_id, addr) + node.add_ip(iface.node_id, ip) # then - assert str(iface.get_ip4()) == addr + assert str(iface.get_ip4()) == ip - def test_node_addaddr_exception(self, session): + def test_node_add_ip_exception(self, session): # given node = session.add_node(CoreNode) switch = session.add_node(SwitchNode) iface_data = InterfaceData() iface = node.new_iface(switch, iface_data) - addr = "256.168.0.1/24" + ip = "256.168.0.1/24" # when with pytest.raises(CoreError): - node.addaddr(iface.node_id, addr) + node.add_ip(iface.node_id, ip) @pytest.mark.parametrize("net_type", NET_TYPES) def test_net(self, session, net_type): From 9e4429fbbc0212b2810a4ff4892f88a300c7739e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 19 Jun 2020 11:11:45 -0700 Subject: [PATCH 0376/1131] daemon: refactored InterfaceData.get_addresses to InterfaceData.get_ips --- daemon/core/emane/linkmonitor.py | 2 -- daemon/core/emulator/data.py | 12 ++++++------ daemon/core/nodes/base.py | 10 +++++----- daemon/core/nodes/network.py | 6 +++--- daemon/core/nodes/physical.py | 10 +++++----- 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py index 295aaa1e..56473f62 100644 --- a/daemon/core/emane/linkmonitor.py +++ b/daemon/core/emane/linkmonitor.py @@ -213,11 +213,9 @@ class EmaneLinkMonitor: for node in nodes: for iface in node.get_ifaces(): if isinstance(iface.net, CtrlNet): - address = None ip4 = iface.get_ip4() if ip4: address = str(ip4.ip) - if address: addresses.append(address) break return addresses diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index 5b6479ae..22d10d2d 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -142,18 +142,18 @@ class InterfaceData: ip6: str = None ip6_mask: int = None - def get_addresses(self) -> List[str]: + def get_ips(self) -> List[str]: """ Returns a list of ip4 and ip6 addresses when present. - :return: list of addresses + :return: list of ip addresses """ - addresses = [] + ips = [] if self.ip4 and self.ip4_mask: - addresses.append(f"{self.ip4}/{self.ip4_mask}") + ips.append(f"{self.ip4}/{self.ip4_mask}") if self.ip6 and self.ip6_mask: - addresses.append(f"{self.ip6}/{self.ip6_mask}") - return addresses + ips.append(f"{self.ip6}/{self.ip6_mask}") + return ips @dataclass diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 7eff9b12..50f19a82 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -811,7 +811,7 @@ class CoreNode(CoreNodeBase): :param iface_data: interface data for new interface :return: interface index """ - addresses = iface_data.get_addresses() + ips = iface_data.get_ips() with self.lock: # TODO: emane specific code if net.is_emane is True: @@ -823,15 +823,15 @@ class CoreNode(CoreNodeBase): self.attachnet(iface_id, net) iface = self.get_iface(iface_id) iface.set_mac(iface_data.mac) - for address in addresses: - iface.add_ip(address) + for ip in ips: + iface.add_ip(ip) else: iface_id = self.newveth(iface_data.id, iface_data.name) self.attachnet(iface_id, net) if iface_data.mac: self.set_mac(iface_id, iface_data.mac) - for address in addresses: - self.add_ip(iface_id, address) + for ip in ips: + self.add_ip(iface_id, ip) self.ifup(iface_id) iface = self.get_iface(iface_id) return iface diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 559b7ece..5b95c3b2 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -697,9 +697,9 @@ class GreTapBridge(CoreNetwork): :return: nothing """ self.grekey = key - addresses = iface_data.get_addresses() - if addresses: - self.add_ips(addresses) + ips = iface_data.get_ips() + if ips: + self.add_ips(ips) class CtrlNet(CoreNetwork): diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 96440bcb..8fd828d8 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -156,7 +156,7 @@ class PhysicalNode(CoreNodeBase): self, net: CoreNetworkBase, iface_data: InterfaceData ) -> CoreInterface: logging.info("creating interface") - addresses = iface_data.get_addresses() + ips = iface_data.get_ips() iface_id = iface_data.id if iface_id is None: iface_id = self.next_iface_id() @@ -167,12 +167,12 @@ class PhysicalNode(CoreNodeBase): # this is reached when this node is linked to a network node # tunnel to net not built yet, so build it now and adopt it _, remote_tap = self.session.distributed.create_gre_tunnel(net, self.server) - self.adopt_iface(remote_tap, iface_id, iface_data.mac, addresses) + self.adopt_iface(remote_tap, iface_id, iface_data.mac, ips) return remote_tap else: # this is reached when configuring services (self.up=False) iface = GreTap(node=self, name=name, session=self.session, start=False) - self.adopt_iface(iface, iface_id, iface_data.mac, addresses) + self.adopt_iface(iface, iface_id, iface_data.mac, ips) return iface def privatedir(self, path: str) -> None: @@ -316,8 +316,8 @@ class Rj45Node(CoreNodeBase): self.iface_id = iface_id if net is not None: self.iface.attachnet(net) - for addr in iface_data.get_addresses(): - self.add_ip(addr) + for ip in iface_data.get_ips(): + self.add_ip(ip) return self.iface def delete_iface(self, iface_id: int) -> None: From 19af9c3f51d7b088fcdaa2f4e7dd3ce670f99b0d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 19 Jun 2020 11:18:39 -0700 Subject: [PATCH 0377/1131] daemon: added proper checks for FRRService calls --- daemon/core/services/frr.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py index 632b4557..13569772 100644 --- a/daemon/core/services/frr.py +++ b/daemon/core/services/frr.py @@ -77,6 +77,8 @@ class FRRZebra(CoreService): for s in node.services: if cls.name not in s.dependencies: continue + if not (isinstance(s, FrrService) or issubclass(s, FrrService)): + continue iface_config = s.generate_frr_iface_config(node, iface) if s.ipv4_routing: want_ipv4 = True @@ -101,6 +103,8 @@ class FRRZebra(CoreService): for s in node.services: if cls.name not in s.dependencies: continue + if not (isinstance(s, FrrService) or issubclass(s, FrrService)): + continue cfg += s.generate_frr_config(node) return cfg From 88fe860f97dd519dbfbfca3f07bdcf20b16ccb58 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 19 Jun 2020 13:25:47 -0700 Subject: [PATCH 0378/1131] fixed examples using IpPrefixes class --- daemon/examples/python/emane80211.py | 2 +- daemon/examples/python/switch.py | 2 +- daemon/examples/python/wlan.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/examples/python/emane80211.py b/daemon/examples/python/emane80211.py index 9d6def4a..48133ce0 100644 --- a/daemon/examples/python/emane80211.py +++ b/daemon/examples/python/emane80211.py @@ -55,7 +55,7 @@ def main(): # get nodes to run example first_node = session.get_node(1, CoreNode) last_node = session.get_node(NODES, CoreNode) - address = prefixes.ip4_address(first_node) + address = prefixes.ip4_address(first_node.id) logging.info("node %s pinging %s", last_node.name, address) output = last_node.cmd(f"ping -c 3 {address}") logging.info(output) diff --git a/daemon/examples/python/switch.py b/daemon/examples/python/switch.py index f05176a3..c5e62e4a 100644 --- a/daemon/examples/python/switch.py +++ b/daemon/examples/python/switch.py @@ -40,7 +40,7 @@ def main(): # get nodes to run example first_node = session.get_node(1, CoreNode) last_node = session.get_node(NODES, CoreNode) - address = prefixes.ip4_address(first_node) + address = prefixes.ip4_address(first_node.id) logging.info("node %s pinging %s", last_node.name, address) output = last_node.cmd(f"ping -c 3 {address}") logging.info(output) diff --git a/daemon/examples/python/wlan.py b/daemon/examples/python/wlan.py index de26ab97..7c16bad8 100644 --- a/daemon/examples/python/wlan.py +++ b/daemon/examples/python/wlan.py @@ -44,7 +44,7 @@ def main(): # get nodes for example run first_node = session.get_node(1, CoreNode) last_node = session.get_node(NODES, CoreNode) - address = prefixes.ip4_address(first_node) + address = prefixes.ip4_address(first_node.id) logging.info("node %s pinging %s", last_node.name, address) output = last_node.cmd(f"ping -c 3 {address}") logging.info(output) From df9216e0f0cba624137a19e64832a05fdef0fd47 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 19 Jun 2020 13:28:11 -0700 Subject: [PATCH 0379/1131] updated scripting docs to use new naming and fixed out bad example --- docs/scripting.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/scripting.md b/docs/scripting.md index f65d66a3..06ca483a 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -61,8 +61,8 @@ def main(): # create nodes for _ in range(NODES): node = session.add_node(CoreNode) - interface = prefixes.create_iface(node) - session.add_link(node.id, switch.id, iface1_data=interface) + iface_data = prefixes.create_iface(node) + session.add_link(node.id, switch.id, iface1_data=iface_data) # instantiate session session.instantiate() @@ -137,7 +137,7 @@ session = coreemu.create_session() # create node with custom services options = NodeOptions(services=["ServiceName"]) -node = session.add_node(options=options) +node = session.add_node(CoreNode, options=options) # set custom file data session.services.set_service_file(node.id, "ServiceName", "FileName", "custom file data") From cd6083aed95d2587923b039fcc426fdda506ed9d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 19 Jun 2020 13:44:28 -0700 Subject: [PATCH 0380/1131] daemon: fixed issue not checking if an emane interface is a TunTap before using a specific function, fixed issue not looking for possible iface specific configuration for external --- daemon/core/emane/nodes.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 9173fbfc..19d5a9e1 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -17,7 +17,7 @@ from core.emulator.enumerations import ( ) from core.errors import CoreError from core.nodes.base import CoreNetworkBase -from core.nodes.interface import CoreInterface +from core.nodes.interface import CoreInterface, TunTap if TYPE_CHECKING: from core.emane.emanemodel import EmaneModel @@ -151,18 +151,16 @@ class EmaneNet(CoreNetworkBase): warntxt = "unable to publish EMANE events because the eventservice " warntxt += "Python bindings failed to load" logging.error(warntxt) - for iface in self.get_ifaces(): - external = self.session.emane.get_config( - "external", self.id, self.model.name + config = self.session.emane.get_iface_config( + self.id, iface, self.model.name ) - if external == "0": + external = config["external"] + if isinstance(iface, TunTap) and external == "0": iface.setaddrs() - if not self.session.emane.genlocationevents(): iface.poshook = None continue - # at this point we register location handlers for generating # EMANE location events iface.poshook = self.setnemposition From f07176dd43aa06ad6724af5b82dda7b82c5f62f4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 19 Jun 2020 13:51:11 -0700 Subject: [PATCH 0381/1131] daemon: provide safe fallback for emane install ifaces, in case external configuration does not exist --- daemon/core/emane/nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 19d5a9e1..1186f928 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -155,7 +155,7 @@ class EmaneNet(CoreNetworkBase): config = self.session.emane.get_iface_config( self.id, iface, self.model.name ) - external = config["external"] + external = config.get("external", "0") if isinstance(iface, TunTap) and external == "0": iface.setaddrs() if not self.session.emane.genlocationevents(): From cfda9509a2020b1cca86c2f7ba016a2ca0eced94 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 19 Jun 2020 13:52:59 -0700 Subject: [PATCH 0382/1131] daemon: refactored TunTap setaddrs to set_ips to be more consistent with new naming --- daemon/core/emane/nodes.py | 2 +- daemon/core/nodes/interface.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 1186f928..8cc9cd87 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -157,7 +157,7 @@ class EmaneNet(CoreNetworkBase): ) external = config.get("external", "0") if isinstance(iface, TunTap) and external == "0": - iface.setaddrs() + iface.set_ips() if not self.session.emane.genlocationevents(): iface.poshook = None continue diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index c613f0cd..d0e55c7e 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -529,9 +529,9 @@ class TunTap(CoreInterface): self.node.node_net_client.device_name(self.localname, self.name) self.node.node_net_client.device_up(self.name) - def setaddrs(self) -> None: + def set_ips(self) -> None: """ - Set interface addresses. + Set interface ip addresses. :return: nothing """ From 1829a8e2f8dac16d3fd891f5d44194725c5cc1e0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 19 Jun 2020 15:21:45 -0700 Subject: [PATCH 0383/1131] daemon: refactored CoreInterface.mac from a string to a netaddr.EUI object, providing more functionality --- daemon/core/api/grpc/grpcutils.py | 2 +- daemon/core/nodes/base.py | 3 +-- daemon/core/nodes/interface.py | 25 +++++++++++++++---------- daemon/core/nodes/network.py | 4 ++-- daemon/core/nodes/physical.py | 2 -- daemon/core/services/xorp.py | 4 ++-- daemon/core/xml/emanexml.py | 6 +++--- daemon/tests/test_nodes.py | 2 +- 8 files changed, 25 insertions(+), 23 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index adaf2549..b63cb895 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -460,7 +460,7 @@ def iface_to_proto(iface: CoreInterface) -> core_pb2.Interface: id=iface.node_id, net_id=net_id, name=iface.name, - mac=iface.mac, + mac=str(iface.mac), mtu=iface.mtu, flow_id=iface.flow_id, ip4=ip4, diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 50f19a82..aae59b70 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -750,7 +750,6 @@ class CoreNode(CoreNodeBase): :return: nothing :raises CoreCommandError: when a non-zero exit status occurs """ - mac = utils.validate_mac(mac) iface = self.get_iface(iface_id) iface.set_mac(mac) if self.up: @@ -1059,7 +1058,7 @@ class CoreNetworkBase(NodeBase): unidirectional = 1 iface2_data = InterfaceData( - id=linked_node.get_iface_id(iface), name=iface.name, mac=iface.mac + id=linked_node.get_iface_id(iface), name=iface.name, mac=str(iface.mac) ) ip4 = iface.get_ip4() if ip4: diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index d0e55c7e..22ecb620 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -56,7 +56,7 @@ class CoreInterface: self._params: Dict[str, float] = {} self.ip4s: List[netaddr.IPNetwork] = [] self.ip6s: List[netaddr.IPNetwork] = [] - self.mac: Optional[str] = None + self.mac: Optional[netaddr.EUI] = None # placeholder position hook self.poshook: Callable[[CoreInterface], None] = lambda x: None # used with EMANE @@ -149,8 +149,8 @@ class CoreInterface: self.ip4s.append(ip) else: self.ip6s.append(ip) - except netaddr.AddrFormatError: - raise CoreError(f"adding invalid address {ip}") + except netaddr.AddrFormatError as e: + raise CoreError(f"adding invalid address {ip}: {e}") def remove_ip(self, ip: str) -> None: """ @@ -167,8 +167,8 @@ class CoreInterface: self.ip4s.remove(ip) else: self.ip6s.remove(ip) - except (netaddr.AddrFormatError, ValueError): - raise CoreError(f"deleting invalid address {ip}") + except (netaddr.AddrFormatError, ValueError) as e: + raise CoreError(f"deleting invalid address {ip}: {e}") def get_ip4(self) -> Optional[netaddr.IPNetwork]: """ @@ -194,16 +194,21 @@ class CoreInterface: """ return self.ip4s + self.ip6s - def set_mac(self, mac: str) -> None: + def set_mac(self, mac: Optional[str]) -> None: """ Set mac address. - :param mac: mac address to set + :param mac: mac address to set, None for random mac :return: nothing + :raises CoreError: when there is an invalid mac address """ - if mac is not None: - mac = utils.validate_mac(mac) - self.mac = mac + if mac is None: + self.mac = mac + else: + try: + self.mac = netaddr.EUI(mac, dialect=netaddr.mac_unix_expanded) + except netaddr.AddrFormatError as e: + raise CoreError(f"invalid mac address({mac}): {e}") def getparam(self, key: str) -> float: """ diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 5b95c3b2..7d8f805e 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -878,7 +878,7 @@ class PtpNet(CoreNetwork): unidirectional = 1 iface1_data = InterfaceData( - id=iface1.node.get_iface_id(iface1), name=iface1.name, mac=iface1.mac + id=iface1.node.get_iface_id(iface1), name=iface1.name, mac=str(iface1.mac) ) ip4 = iface1.get_ip4() if ip4: @@ -890,7 +890,7 @@ class PtpNet(CoreNetwork): iface1_data.ip6_mask = ip6.prefixlen iface2_data = InterfaceData( - id=iface2.node.get_iface_id(iface2), name=iface2.name, mac=iface2.mac + id=iface2.node.get_iface_id(iface2), name=iface2.name, mac=str(iface2.mac) ) ip4 = iface2.get_ip4() if ip4: diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 8fd828d8..3751d9ee 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -7,7 +7,6 @@ import os import threading from typing import IO, TYPE_CHECKING, List, Optional, Tuple -from core import utils from core.constants import MOUNT_BIN, UMOUNT_BIN from core.emulator.data import InterfaceData, LinkOptions from core.emulator.distributed import DistributedServer @@ -74,7 +73,6 @@ class PhysicalNode(CoreNodeBase): :return: nothing :raises CoreCommandError: when a non-zero exit status occurs """ - mac = utils.validate_mac(mac) iface = self.get_iface(iface_id) iface.set_mac(mac) if self.up: diff --git a/daemon/core/services/xorp.py b/daemon/core/services/xorp.py index a9687d45..485fe159 100644 --- a/daemon/core/services/xorp.py +++ b/daemon/core/services/xorp.py @@ -69,7 +69,7 @@ class XorpRtrmgr(CoreService): """ helper for adding link-local address entries (required by OSPFv3) """ - cfg = "\t address %s {\n" % netaddr.EUI(iface.mac).eui64() + cfg = "\t address %s {\n" % iface.mac.eui64() cfg += "\t\tprefix-length: 64\n" cfg += "\t }\n" return cfg @@ -292,7 +292,7 @@ class XorpRipng(XorpService): for iface in node.get_ifaces(control=False): cfg += "\tinterface %s {\n" % iface.name cfg += "\t vif %s {\n" % iface.name - cfg += "\t\taddress %s {\n" % netaddr.EUI(iface.mac).eui64() + cfg += "\t\taddress %s {\n" % iface.mac.eui64() cfg += "\t\t disable: false\n" cfg += "\t\t}\n" cfg += "\t }\n" diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index d716777b..eece57c9 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -230,9 +230,9 @@ def build_node_platform_xml( platform_element.append(nem_element) node.setnemid(iface, nem_id) - macstr = _MAC_PREFIX + ":00:00:" - macstr += f"{(nem_id >> 8) & 0xFF:02X}:{nem_id & 0xFF:02X}" - iface.set_mac(macstr) + mac = _MAC_PREFIX + ":00:00:" + mac += f"{(nem_id >> 8) & 0xFF:02X}:{nem_id & 0xFF:02X}" + iface.set_mac(mac) # increment nem id nem_id += 1 diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index 25a62c5f..a827fe25 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -61,7 +61,7 @@ class TestNodes: node.set_mac(iface.node_id, mac) # then - assert iface.mac == mac + assert str(iface.mac) == mac def test_node_set_mac_exception(self, session: Session): # given From 0d4a360e89319f568b9d10515da0f04781db1d0c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 19 Jun 2020 15:32:17 -0700 Subject: [PATCH 0384/1131] daemon: removed utils.validate_ip and shifted tests to test_nodes --- daemon/core/utils.py | 14 -------------- daemon/tests/test_nodes.py | 17 ++++++++++++++--- daemon/tests/test_utils.py | 18 ------------------ 3 files changed, 14 insertions(+), 35 deletions(-) diff --git a/daemon/core/utils.py b/daemon/core/utils.py index 3b1ea46a..4b932485 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -444,17 +444,3 @@ def validate_mac(value: str) -> str: return str(mac) except netaddr.AddrFormatError as e: raise CoreError(f"invalid mac address {value}: {e}") - - -def validate_ip(value: str) -> str: - """ - Validate ip address with prefix and return formatted version. - - :param value: address to validate - :return: formatted ip address - """ - try: - ip = netaddr.IPNetwork(value) - return str(ip) - except (ValueError, netaddr.AddrFormatError) as e: - raise CoreError(f"invalid ip address {value}: {e}") diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index a827fe25..1741622e 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -75,19 +75,30 @@ class TestNodes: with pytest.raises(CoreError): node.set_mac(iface.node_id, mac) - def test_node_add_ip(self, session: Session): + @pytest.mark.parametrize( + "ip,expected,is_ip6", + [ + ("127", "127.0.0.0/32", False), + ("10.0.0.1/24", "10.0.0.1/24", False), + ("2001::", "2001::/128", True), + ("2001::/64", "2001::/64", True), + ], + ) + def test_node_add_ip(self, session: Session, ip: str, expected: str, is_ip6: bool): # given node = session.add_node(CoreNode) switch = session.add_node(SwitchNode) iface_data = InterfaceData() iface = node.new_iface(switch, iface_data) - ip = "192.168.0.1/24" # when node.add_ip(iface.node_id, ip) # then - assert str(iface.get_ip4()) == ip + if is_ip6: + assert str(iface.get_ip6()) == expected + else: + assert str(iface.get_ip4()) == expected def test_node_add_ip_exception(self, session): # given diff --git a/daemon/tests/test_utils.py b/daemon/tests/test_utils.py index 3e43b789..22bf0ee5 100644 --- a/daemon/tests/test_utils.py +++ b/daemon/tests/test_utils.py @@ -25,24 +25,6 @@ class TestUtils: assert len(two_args) == 2 assert len(unicode_args) == 3 - @pytest.mark.parametrize( - "data,expected", - [ - ("127", "127.0.0.0/32"), - ("10.0.0.1/24", "10.0.0.1/24"), - ("2001::", "2001::/128"), - ("2001::/64", "2001::/64"), - ], - ) - def test_validate_ip(self, data: str, expected: str): - value = utils.validate_ip(data) - assert value == expected - - @pytest.mark.parametrize("data", ["256", "1270.0.0.1", "127.0.0.0.1"]) - def test_validate_ip_exception(self, data: str): - with pytest.raises(CoreError): - utils.validate_ip("") - @pytest.mark.parametrize( "data,expected", [ From adfce5263232e89971173b864d6609ef99a1af64 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 19 Jun 2020 15:41:41 -0700 Subject: [PATCH 0385/1131] daemon: removed utils.validate_mac and shifted tests to test_nodes --- daemon/core/utils.py | 16 +--------------- daemon/tests/test_nodes.py | 18 +++++++++++++----- daemon/tests/test_utils.py | 20 -------------------- 3 files changed, 14 insertions(+), 40 deletions(-) diff --git a/daemon/core/utils.py b/daemon/core/utils.py index 4b932485..0e082187 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -33,7 +33,7 @@ from typing import ( import netaddr -from core.errors import CoreCommandError, CoreError +from core.errors import CoreCommandError if TYPE_CHECKING: from core.emulator.session import Session @@ -430,17 +430,3 @@ def random_mac() -> str: value |= 0x00163E << 24 mac = netaddr.EUI(value, dialect=netaddr.mac_unix_expanded) return str(mac) - - -def validate_mac(value: str) -> str: - """ - Validate mac and return unix formatted version. - - :param value: address to validate - :return: unix formatted mac - """ - try: - mac = netaddr.EUI(value, dialect=netaddr.mac_unix_expanded) - return str(mac) - except netaddr.AddrFormatError as e: - raise CoreError(f"invalid mac address {value}: {e}") diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index 1741622e..8ed21f27 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -49,27 +49,35 @@ class TestNodes: with pytest.raises(CoreError): session.get_node(node.id, CoreNode) - def test_node_set_mac(self, session: Session): + @pytest.mark.parametrize( + "mac,expected", + [ + ("AA-AA-AA-FF-FF-FF", "aa:aa:aa:ff:ff:ff"), + ("00:00:00:FF:FF:FF", "00:00:00:ff:ff:ff"), + ], + ) + def test_node_set_mac(self, session: Session, mac: str, expected: str): # given node = session.add_node(CoreNode) switch = session.add_node(SwitchNode) iface_data = InterfaceData() iface = node.new_iface(switch, iface_data) - mac = "aa:aa:aa:ff:ff:ff" # when node.set_mac(iface.node_id, mac) # then - assert str(iface.mac) == mac + assert str(iface.mac) == expected - def test_node_set_mac_exception(self, session: Session): + @pytest.mark.parametrize( + "mac", ["AAA:AA:AA:FF:FF:FF", "AA:AA:AA:FF:FF", "AA/AA/AA/FF/FF/FF"] + ) + def test_node_set_mac_exception(self, session: Session, mac: str): # given node = session.add_node(CoreNode) switch = session.add_node(SwitchNode) iface_data = InterfaceData() iface = node.new_iface(switch, iface_data) - mac = "aa:aa:aa:ff:ff:fff" # when with pytest.raises(CoreError): diff --git a/daemon/tests/test_utils.py b/daemon/tests/test_utils.py index 22bf0ee5..5a4f25a4 100644 --- a/daemon/tests/test_utils.py +++ b/daemon/tests/test_utils.py @@ -1,8 +1,6 @@ import netaddr -import pytest from core import utils -from core.errors import CoreError class TestUtils: @@ -25,24 +23,6 @@ class TestUtils: assert len(two_args) == 2 assert len(unicode_args) == 3 - @pytest.mark.parametrize( - "data,expected", - [ - ("AA-AA-AA-FF-FF-FF", "aa:aa:aa:ff:ff:ff"), - ("00:00:00:FF:FF:FF", "00:00:00:ff:ff:ff"), - ], - ) - def test_validate_mac(self, data: str, expected: str): - value = utils.validate_mac(data) - assert value == expected - - @pytest.mark.parametrize( - "data", ["AAA:AA:AA:FF:FF:FF", "AA:AA:AA:FF:FF", "AA/AA/AA/FF/FF/FF"] - ) - def test_validate_mac_exception(self, data: str): - with pytest.raises(CoreError): - utils.validate_mac(data) - def test_random_mac(self): value = utils.random_mac() assert netaddr.EUI(value) is not None From 0356f3b19c637a3a8f43fccc56e78418d34c63c0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 19 Jun 2020 22:08:24 -0700 Subject: [PATCH 0386/1131] pygui: added type hinting to everything under base core.gui --- daemon/core/api/grpc/client.py | 4 +- daemon/core/gui/app.py | 32 ++--- daemon/core/gui/appconfig.py | 136 ++++++++++---------- daemon/core/gui/coreclient.py | 225 ++++++++++++++++++--------------- daemon/core/gui/graph/graph.py | 4 +- daemon/core/gui/images.py | 56 ++++---- daemon/core/gui/interface.py | 40 +++--- daemon/core/gui/menubar.py | 18 +-- daemon/core/gui/nodeutils.py | 60 +++++---- daemon/core/gui/observers.py | 10 +- daemon/core/gui/statusbar.py | 28 ++-- daemon/core/gui/task.py | 20 +-- daemon/core/gui/themes.py | 61 ++++----- daemon/core/gui/toolbar.py | 68 +++++----- daemon/core/gui/tooltip.py | 18 +-- daemon/core/gui/validation.py | 21 ++- daemon/core/gui/widgets.py | 102 ++++++++------- 17 files changed, 473 insertions(+), 430 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index db908e05..5aa6713d 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -436,7 +436,7 @@ class CoreGrpcClient: session_id: int, handler: Callable[[core_pb2.Event], None], events: List[core_pb2.Event] = None, - ) -> Any: + ) -> grpc.Channel: """ Listen for session events. @@ -453,7 +453,7 @@ class CoreGrpcClient: def throughputs( self, session_id: int, handler: Callable[[core_pb2.ThroughputsEvent], None] - ) -> Any: + ) -> grpc.Channel: """ Listen for throughput events with information for interfaces and bridges. diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index c795a46a..cb385e9e 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -3,10 +3,12 @@ import math import tkinter as tk from tkinter import PhotoImage, font, ttk from tkinter.ttk import Progressbar +from typing import Dict, Optional import grpc from core.gui import appconfig, themes +from core.gui.appconfig import GuiConfig from core.gui.coreclient import CoreClient from core.gui.dialogs.error import ErrorDialog from core.gui.graph.graph import CanvasGraph @@ -16,8 +18,8 @@ from core.gui.nodeutils import NodeUtils from core.gui.statusbar import StatusBar from core.gui.toolbar import Toolbar -WIDTH = 1000 -HEIGHT = 800 +WIDTH: int = 1000 +HEIGHT: int = 800 class Application(ttk.Frame): @@ -27,25 +29,25 @@ class Application(ttk.Frame): NodeUtils.setup() # widgets - self.menubar = None - self.toolbar = None - self.right_frame = None - self.canvas = None - self.statusbar = None - self.progress = None + self.menubar: Optional[Menubar] = None + self.toolbar: Optional[Toolbar] = None + self.right_frame: Optional[ttk.Frame] = None + self.canvas: Optional[CanvasGraph] = None + self.statusbar: Optional[StatusBar] = None + self.progress: Optional[Progressbar] = None # fonts - self.fonts_size = None - self.icon_text_font = None - self.edge_font = None + self.fonts_size: Dict[str, int] = {} + self.icon_text_font: Optional[font.Font] = None + self.edge_font: Optional[font.Font] = None # setup - self.guiconfig = appconfig.read() - self.app_scale = self.guiconfig.scale + self.guiconfig: GuiConfig = appconfig.read() + self.app_scale: float = self.guiconfig.scale self.setup_scaling() - self.style = ttk.Style() + self.style: ttk.Style = ttk.Style() self.setup_theme() - self.core = CoreClient(self, proxy) + self.core: CoreClient = CoreClient(self, proxy) self.setup_app() self.draw() self.core.setup() diff --git a/daemon/core/gui/appconfig.py b/daemon/core/gui/appconfig.py index 077f938d..6bc213eb 100644 --- a/daemon/core/gui/appconfig.py +++ b/daemon/core/gui/appconfig.py @@ -1,32 +1,32 @@ import os import shutil from pathlib import Path -from typing import List, Optional +from typing import Dict, List, Optional, Type import yaml from core.gui import themes -HOME_PATH = Path.home().joinpath(".coregui") -BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds") -CUSTOM_EMANE_PATH = HOME_PATH.joinpath("custom_emane") -CUSTOM_SERVICE_PATH = HOME_PATH.joinpath("custom_services") -ICONS_PATH = HOME_PATH.joinpath("icons") -MOBILITY_PATH = HOME_PATH.joinpath("mobility") -XMLS_PATH = HOME_PATH.joinpath("xmls") -CONFIG_PATH = HOME_PATH.joinpath("config.yaml") -LOG_PATH = HOME_PATH.joinpath("gui.log") -SCRIPT_PATH = HOME_PATH.joinpath("scripts") +HOME_PATH: Path = Path.home().joinpath(".coregui") +BACKGROUNDS_PATH: Path = HOME_PATH.joinpath("backgrounds") +CUSTOM_EMANE_PATH: Path = HOME_PATH.joinpath("custom_emane") +CUSTOM_SERVICE_PATH: Path = HOME_PATH.joinpath("custom_services") +ICONS_PATH: Path = HOME_PATH.joinpath("icons") +MOBILITY_PATH: Path = HOME_PATH.joinpath("mobility") +XMLS_PATH: Path = HOME_PATH.joinpath("xmls") +CONFIG_PATH: Path = HOME_PATH.joinpath("config.yaml") +LOG_PATH: Path = HOME_PATH.joinpath("gui.log") +SCRIPT_PATH: Path = HOME_PATH.joinpath("scripts") # local paths -DATA_PATH = Path(__file__).parent.joinpath("data") -LOCAL_ICONS_PATH = DATA_PATH.joinpath("icons").absolute() -LOCAL_BACKGROUND_PATH = DATA_PATH.joinpath("backgrounds").absolute() -LOCAL_XMLS_PATH = DATA_PATH.joinpath("xmls").absolute() -LOCAL_MOBILITY_PATH = DATA_PATH.joinpath("mobility").absolute() +DATA_PATH: Path = Path(__file__).parent.joinpath("data") +LOCAL_ICONS_PATH: Path = DATA_PATH.joinpath("icons").absolute() +LOCAL_BACKGROUND_PATH: Path = DATA_PATH.joinpath("backgrounds").absolute() +LOCAL_XMLS_PATH: Path = DATA_PATH.joinpath("xmls").absolute() +LOCAL_MOBILITY_PATH: Path = DATA_PATH.joinpath("mobility").absolute() # configuration data -TERMINALS = { +TERMINALS: Dict[str, str] = { "xterm": "xterm -e", "aterm": "aterm -e", "eterm": "eterm -e", @@ -36,45 +36,45 @@ TERMINALS = { "xfce4-terminal": "xfce4-terminal -x", "gnome-terminal": "gnome-terminal --window --", } -EDITORS = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"] +EDITORS: List[str] = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"] class IndentDumper(yaml.Dumper): - def increase_indent(self, flow=False, indentless=False): - return super().increase_indent(flow, False) + def increase_indent(self, flow: bool = False, indentless: bool = False) -> None: + super().increase_indent(flow, False) class CustomNode(yaml.YAMLObject): - yaml_tag = "!CustomNode" - yaml_loader = yaml.SafeLoader + yaml_tag: str = "!CustomNode" + yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader def __init__(self, name: str, image: str, services: List[str]) -> None: - self.name = name - self.image = image - self.services = services + self.name: str = name + self.image: str = image + self.services: List[str] = services class CoreServer(yaml.YAMLObject): - yaml_tag = "!CoreServer" - yaml_loader = yaml.SafeLoader + yaml_tag: str = "!CoreServer" + yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader def __init__(self, name: str, address: str) -> None: - self.name = name - self.address = address + self.name: str = name + self.address: str = address class Observer(yaml.YAMLObject): - yaml_tag = "!Observer" - yaml_loader = yaml.SafeLoader + yaml_tag: str = "!Observer" + yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader def __init__(self, name: str, cmd: str) -> None: - self.name = name - self.cmd = cmd + self.name: str = name + self.cmd: str = cmd class PreferencesConfig(yaml.YAMLObject): - yaml_tag = "!PreferencesConfig" - yaml_loader = yaml.SafeLoader + yaml_tag: str = "!PreferencesConfig" + yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader def __init__( self, @@ -85,17 +85,17 @@ class PreferencesConfig(yaml.YAMLObject): width: int = 1000, height: int = 750, ) -> None: - self.theme = theme - self.editor = editor - self.terminal = terminal - self.gui3d = gui3d - self.width = width - self.height = height + self.theme: str = theme + self.editor: str = editor + self.terminal: str = terminal + self.gui3d: str = gui3d + self.width: int = width + self.height: int = height class LocationConfig(yaml.YAMLObject): - yaml_tag = "!LocationConfig" - yaml_loader = yaml.SafeLoader + yaml_tag: str = "!LocationConfig" + yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader def __init__( self, @@ -107,18 +107,18 @@ class LocationConfig(yaml.YAMLObject): alt: float = 2.0, scale: float = 150.0, ) -> None: - self.x = x - self.y = y - self.z = z - self.lat = lat - self.lon = lon - self.alt = alt - self.scale = scale + self.x: float = x + self.y: float = y + self.z: float = z + self.lat: float = lat + self.lon: float = lon + self.alt: float = alt + self.scale: float = scale class IpConfigs(yaml.YAMLObject): - yaml_tag = "!IpConfigs" - yaml_loader = yaml.SafeLoader + yaml_tag: str = "!IpConfigs" + yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader def __init__( self, @@ -129,21 +129,21 @@ class IpConfigs(yaml.YAMLObject): ) -> None: if ip4s is None: ip4s = ["10.0.0.0", "192.168.0.0", "172.16.0.0"] - self.ip4s = ip4s + self.ip4s: List[str] = ip4s if ip6s is None: ip6s = ["2001::", "2002::", "a::"] - self.ip6s = ip6s + self.ip6s: List[str] = ip6s if ip4 is None: ip4 = self.ip4s[0] - self.ip4 = ip4 + self.ip4: str = ip4 if ip6 is None: ip6 = self.ip6s[0] - self.ip6 = ip6 + self.ip6: str = ip6 class GuiConfig(yaml.YAMLObject): - yaml_tag = "!GuiConfig" - yaml_loader = yaml.SafeLoader + yaml_tag: str = "!GuiConfig" + yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader def __init__( self, @@ -159,30 +159,30 @@ class GuiConfig(yaml.YAMLObject): ) -> None: if preferences is None: preferences = PreferencesConfig() - self.preferences = preferences + self.preferences: PreferencesConfig = preferences if location is None: location = LocationConfig() - self.location = location + self.location: LocationConfig = location if servers is None: servers = [] - self.servers = servers + self.servers: List[CoreServer] = servers if nodes is None: nodes = [] - self.nodes = nodes + self.nodes: List[CustomNode] = nodes if recentfiles is None: recentfiles = [] - self.recentfiles = recentfiles + self.recentfiles: List[str] = recentfiles if observers is None: observers = [] - self.observers = observers - self.scale = scale + self.observers: List[Observer] = observers + self.scale: float = scale if ips is None: ips = IpConfigs() - self.ips = ips - self.mac = mac + self.ips: IpConfigs = ips + self.mac: str = mac -def copy_files(current_path, new_path) -> None: +def copy_files(current_path: Path, new_path: Path) -> None: for current_file in current_path.glob("*"): new_file = new_path.joinpath(current_file.name) shutil.copy(current_file, new_file) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 8b0c423c..24708769 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -4,18 +4,41 @@ Incorporate grpc into python tkinter GUI import json import logging import os +import tkinter as tk from pathlib import Path from tkinter import messagebox -from typing import TYPE_CHECKING, Dict, Iterable, List, Optional +from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple import grpc -from core.api.grpc import client, common_pb2, configservices_pb2, core_pb2 +from core.api.grpc import client +from core.api.grpc.common_pb2 import ConfigOption +from core.api.grpc.configservices_pb2 import ConfigService, ConfigServiceConfig +from core.api.grpc.core_pb2 import ( + Event, + ExceptionEvent, + Hook, + Interface, + Link, + LinkEvent, + LinkType, + MessageType, + Node, + NodeEvent, + NodeType, + Position, + SessionLocation, + SessionState, + StartSessionResponse, + StopSessionResponse, + ThroughputsEvent, +) from core.api.grpc.emane_pb2 import EmaneModelConfig from core.api.grpc.mobility_pb2 import MobilityConfig from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig, ServiceFileConfig from core.api.grpc.wlan_pb2 import WlanConfig from core.gui import appconfig +from core.gui.appconfig import CoreServer from core.gui.dialogs.emaneinstall import EmaneInstallDialog from core.gui.dialogs.error import ErrorDialog from core.gui.dialogs.mobilityplayer import MobilityPlayer @@ -34,47 +57,46 @@ GUI_SOURCE = "gui" class CoreClient: - def __init__(self, app: "Application", proxy: bool): + def __init__(self, app: "Application", proxy: bool) -> None: """ Create a CoreGrpc instance """ - self._client = client.CoreGrpcClient(proxy=proxy) - self.session_id = None - self.node_ids = [] - self.app = app - self.master = app.master - self.services = {} - self.config_services_groups = {} - self.config_services = {} - self.default_services = {} - self.emane_models = [] - self.observer = None + self.app: "Application" = app + self.master: tk.Tk = app.master + self._client: client.CoreGrpcClient = client.CoreGrpcClient(proxy=proxy) + self.session_id: Optional[int] = None + self.services: Dict[str, Set[str]] = {} + self.config_services_groups: Dict[str, Set[str]] = {} + self.config_services: Dict[str, ConfigService] = {} + self.default_services: Dict[NodeType, Set[str]] = {} + self.emane_models: List[str] = [] + self.observer: Optional[str] = None # loaded configuration data - self.servers = {} - self.custom_nodes = {} - self.custom_observers = {} + self.servers: Dict[str, CoreServer] = {} + self.custom_nodes: Dict[str, NodeDraw] = {} + self.custom_observers: Dict[str, str] = {} self.read_config() # helpers - self.iface_to_edge = {} - self.ifaces_manager = InterfaceManager(self.app) + self.iface_to_edge: Dict[Tuple[int, int], Tuple[int, int]] = {} + self.ifaces_manager: InterfaceManager = InterfaceManager(self.app) # session data - self.state = None - self.canvas_nodes = {} - self.location = None - self.links = {} - self.hooks = {} - self.emane_config = None - self.mobility_players = {} - self.handling_throughputs = None - self.handling_events = None - self.xml_dir = None - self.xml_file = None + self.state: Optional[SessionState] = None + self.canvas_nodes: Dict[int, CanvasNode] = {} + self.location: Optional[SessionLocation] = None + self.links: Dict[Tuple[int, int], CanvasEdge] = {} + self.hooks: Dict[str, Hook] = {} + self.emane_config: Dict[str, ConfigOption] = {} + self.mobility_players: Dict[int, MobilityPlayer] = {} + self.handling_throughputs: Optional[grpc.Channel] = None + self.handling_events: Optional[grpc.Channel] = None + self.xml_dir: Optional[str] = None + self.xml_file: Optional[str] = None @property - def client(self): + def client(self) -> client.CoreGrpcClient: if self.session_id: response = self._client.check_session(self.session_id) if not response.result: @@ -89,7 +111,7 @@ class CoreClient: self.enable_throughputs() return self._client - def reset(self): + def reset(self) -> None: # helpers self.ifaces_manager.reset() self.iface_to_edge.clear() @@ -104,14 +126,14 @@ class CoreClient: self.cancel_throughputs() self.cancel_events() - def close_mobility_players(self): + def close_mobility_players(self) -> None: for mobility_player in self.mobility_players.values(): mobility_player.close() - def set_observer(self, value: str): + def set_observer(self, value: Optional[str]) -> None: self.observer = value - def read_config(self): + def read_config(self) -> None: # read distributed servers for server in self.app.guiconfig.servers: self.servers[server.name] = server @@ -125,7 +147,7 @@ class CoreClient: for observer in self.app.guiconfig.observers: self.custom_observers[observer.name] = observer - def handle_events(self, event: core_pb2.Event): + def handle_events(self, event: Event) -> None: if event.session_id != self.session_id: logging.warning( "ignoring event session(%s) current(%s)", @@ -139,7 +161,7 @@ class CoreClient: elif event.HasField("session_event"): logging.info("session event: %s", event) session_event = event.session_event - if session_event.event <= core_pb2.SessionState.SHUTDOWN: + if session_event.event <= SessionState.SHUTDOWN: self.state = event.session_event.event elif session_event.event in {7, 8, 9}: node_id = session_event.node_id @@ -162,7 +184,7 @@ class CoreClient: else: logging.info("unhandled event: %s", event) - def handle_link_event(self, event: core_pb2.LinkEvent): + def handle_link_event(self, event: LinkEvent) -> None: logging.debug("Link event: %s", event) node1_id = event.link.node1_id node2_id = event.link.node2_id @@ -171,16 +193,16 @@ class CoreClient: return canvas_node1 = self.canvas_nodes[node1_id] canvas_node2 = self.canvas_nodes[node2_id] - if event.message_type == core_pb2.MessageType.ADD: + if event.message_type == MessageType.ADD: self.app.canvas.add_wireless_edge(canvas_node1, canvas_node2, event.link) - elif event.message_type == core_pb2.MessageType.DELETE: + elif event.message_type == MessageType.DELETE: self.app.canvas.delete_wireless_edge(canvas_node1, canvas_node2, event.link) - elif event.message_type == core_pb2.MessageType.NONE: + elif event.message_type == MessageType.NONE: self.app.canvas.update_wireless_edge(canvas_node1, canvas_node2, event.link) else: logging.warning("unknown link event: %s", event) - def handle_node_event(self, event: core_pb2.NodeEvent): + def handle_node_event(self, event: NodeEvent) -> None: logging.debug("node event: %s", event) if event.source == GUI_SOURCE: return @@ -190,22 +212,22 @@ class CoreClient: canvas_node = self.canvas_nodes[node_id] canvas_node.move(x, y) - def enable_throughputs(self): + def enable_throughputs(self) -> None: self.handling_throughputs = self.client.throughputs( self.session_id, self.handle_throughputs ) - def cancel_throughputs(self): + def cancel_throughputs(self) -> None: if self.handling_throughputs: self.handling_throughputs.cancel() self.handling_throughputs = None - def cancel_events(self): + def cancel_events(self) -> None: if self.handling_events: self.handling_events.cancel() self.handling_events = None - def handle_throughputs(self, event: core_pb2.ThroughputsEvent): + def handle_throughputs(self, event: ThroughputsEvent) -> None: if event.session_id != self.session_id: logging.warning( "ignoring throughput event session(%s) current(%s)", @@ -216,11 +238,11 @@ class CoreClient: logging.debug("handling throughputs event: %s", event) self.app.after(0, self.app.canvas.set_throughputs, event) - def handle_exception_event(self, event: core_pb2.ExceptionEvent): + def handle_exception_event(self, event: ExceptionEvent) -> None: logging.info("exception event: %s", event) self.app.statusbar.core_alarms.append(event) - def join_session(self, session_id: int, query_location: bool = True): + def join_session(self, session_id: int, query_location: bool = True) -> None: logging.info("join session(%s)", session_id) # update session and title self.session_id = session_id @@ -331,9 +353,9 @@ class CoreClient: self.app.after(0, self.app.joined_session_update) def is_runtime(self) -> bool: - return self.state == core_pb2.SessionState.RUNTIME + return self.state == SessionState.RUNTIME - def parse_metadata(self, config: Dict[str, str]): + def parse_metadata(self, config: Dict[str, str]) -> None: # canvas setting canvas_config = config.get("canvas") logging.debug("canvas metadata: %s", canvas_config) @@ -386,7 +408,7 @@ class CoreClient: except ValueError: logging.exception("unknown shape: %s", shape_type) - def create_new_session(self): + def create_new_session(self) -> None: """ Create a new session """ @@ -394,7 +416,7 @@ class CoreClient: response = self.client.create_session() logging.info("created session: %s", response) location_config = self.app.guiconfig.location - self.location = core_pb2.SessionLocation( + self.location = SessionLocation( x=location_config.x, y=location_config.y, z=location_config.z, @@ -407,7 +429,7 @@ class CoreClient: except grpc.RpcError as e: self.app.show_grpc_exception("New Session Error", e) - def delete_session(self, session_id: int = None): + def delete_session(self, session_id: int = None) -> None: if session_id is None: session_id = self.session_id try: @@ -416,7 +438,7 @@ class CoreClient: except grpc.RpcError as e: self.app.show_grpc_exception("Delete Session Error", e) - def setup(self): + def setup(self) -> None: """ Query sessions, if there exist any, prompt whether to join one """ @@ -451,7 +473,7 @@ class CoreClient: dialog.show() self.app.close() - def edit_node(self, core_node: core_pb2.Node): + def edit_node(self, core_node: Node) -> None: try: self.client.edit_node( self.session_id, core_node.id, core_node.position, source=GUI_SOURCE @@ -459,12 +481,12 @@ class CoreClient: except grpc.RpcError as e: self.app.show_grpc_exception("Edit Node Error", e) - def start_session(self) -> core_pb2.StartSessionResponse: + def start_session(self) -> StartSessionResponse: self.ifaces_manager.reset_mac() nodes = [x.core_node for x in self.canvas_nodes.values()] links = [] for edge in self.links.values(): - link = core_pb2.Link() + link = Link() link.CopyFrom(edge.link) if link.HasField("iface1") and not link.iface1.mac: link.iface1.mac = self.ifaces_manager.next_mac() @@ -485,7 +507,7 @@ class CoreClient: emane_config = {x: self.emane_config[x].value for x in self.emane_config} else: emane_config = None - response = core_pb2.StartSessionResponse(result=False) + response = StartSessionResponse(result=False) try: response = self.client.start_session( self.session_id, @@ -511,10 +533,10 @@ class CoreClient: self.app.show_grpc_exception("Start Session Error", e) return response - def stop_session(self, session_id: int = None) -> core_pb2.StartSessionResponse: + def stop_session(self, session_id: int = None) -> StopSessionResponse: if not session_id: session_id = self.session_id - response = core_pb2.StopSessionResponse(result=False) + response = StopSessionResponse(result=False) try: response = self.client.stop_session(session_id) logging.info("stopped session(%s), result: %s", session_id, response) @@ -522,9 +544,9 @@ class CoreClient: self.app.show_grpc_exception("Stop Session Error", e) return response - def show_mobility_players(self): + def show_mobility_players(self) -> None: for canvas_node in self.canvas_nodes.values(): - if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: + if canvas_node.core_node.type != NodeType.WIRELESS_LAN: continue if canvas_node.mobility_config: mobility_player = MobilityPlayer( @@ -534,7 +556,7 @@ class CoreClient: self.mobility_players[node_id] = mobility_player mobility_player.show() - def set_metadata(self): + def set_metadata(self) -> None: # create canvas data wallpaper = None if self.app.canvas.wallpaper_file: @@ -558,7 +580,7 @@ class CoreClient: response = self.client.set_session_metadata(self.session_id, metadata) logging.info("set session metadata %s, result: %s", metadata, response) - def launch_terminal(self, node_id: int): + def launch_terminal(self, node_id: int) -> None: try: terminal = self.app.guiconfig.preferences.terminal if not terminal: @@ -575,12 +597,12 @@ class CoreClient: except grpc.RpcError as e: self.app.show_grpc_exception("Node Terminal Error", e) - def save_xml(self, file_path: str): + def save_xml(self, file_path: str) -> None: """ Save core session as to an xml file """ try: - if self.state != core_pb2.SessionState.RUNTIME: + if self.state != SessionState.RUNTIME: logging.debug("Send session data to the daemon") self.send_data() response = self.client.save_xml(self.session_id, file_path) @@ -588,7 +610,7 @@ class CoreClient: except grpc.RpcError as e: self.app.show_grpc_exception("Save XML Error", e) - def open_xml(self, file_path: str): + def open_xml(self, file_path: str) -> None: """ Open core xml """ @@ -627,7 +649,8 @@ class CoreClient: shutdown=shutdowns, ) logging.info( - "Set %s service for node(%s), files: %s, Startup: %s, Validation: %s, Shutdown: %s, Result: %s", + "Set %s service for node(%s), files: %s, Startup: %s, " + "Validation: %s, Shutdown: %s, Result: %s", service_name, node_id, files, @@ -656,7 +679,7 @@ class CoreClient: def set_node_service_file( self, node_id: int, service_name: str, file_name: str, data: str - ): + ) -> None: response = self.client.set_node_service_file( self.session_id, node_id, service_name, file_name, data ) @@ -669,18 +692,16 @@ class CoreClient: response, ) - def create_nodes_and_links(self): + def create_nodes_and_links(self) -> None: """ create nodes and links that have not been created yet """ node_protos = [x.core_node for x in self.canvas_nodes.values()] link_protos = [x.link for x in self.links.values()] - if self.state != core_pb2.SessionState.DEFINITION: - self.client.set_session_state( - self.session_id, core_pb2.SessionState.DEFINITION - ) + if self.state != SessionState.DEFINITION: + self.client.set_session_state(self.session_id, SessionState.DEFINITION) - self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION) + self.client.set_session_state(self.session_id, SessionState.DEFINITION) for node_proto in node_protos: response = self.client.add_node(self.session_id, node_proto) logging.debug("create node: %s", response) @@ -695,7 +716,7 @@ class CoreClient: ) logging.debug("create link: %s", response) - def send_data(self): + def send_data(self) -> None: """ send to daemon all session info, but don't start the session """ @@ -738,10 +759,9 @@ class CoreClient: if self.emane_config: config = {x: self.emane_config[x].value for x in self.emane_config} self.client.set_emane_config(self.session_id, config) - self.set_metadata() - def close(self): + def close(self) -> None: """ Clean ups when done using grpc """ @@ -760,31 +780,31 @@ class CoreClient: return i def create_node( - self, x: float, y: float, node_type: core_pb2.NodeType, model: str - ) -> Optional[core_pb2.Node]: + self, x: float, y: float, node_type: NodeType, model: str + ) -> Optional[Node]: """ Add node, with information filled in, to grpc manager """ node_id = self.next_node_id() - position = core_pb2.Position(x=x, y=y) + position = Position(x=x, y=y) image = None if NodeUtils.is_image_node(node_type): image = "ubuntu:latest" emane = None - if node_type == core_pb2.NodeType.EMANE: + if node_type == NodeType.EMANE: if not self.emane_models: dialog = EmaneInstallDialog(self.app) dialog.show() return emane = self.emane_models[0] name = f"EMANE{node_id}" - elif node_type == core_pb2.NodeType.WIRELESS_LAN: + elif node_type == NodeType.WIRELESS_LAN: name = f"WLAN{node_id}" - elif node_type in [core_pb2.NodeType.RJ45, core_pb2.NodeType.TUNNEL]: + elif node_type in [NodeType.RJ45, NodeType.TUNNEL]: name = "UNASSIGNED" else: name = f"n{node_id}" - node = core_pb2.Node( + node = Node( id=node_id, type=node_type, name=name, @@ -810,7 +830,7 @@ class CoreClient: ) return node - def deleted_graph_nodes(self, canvas_nodes: List[core_pb2.Node]): + def deleted_graph_nodes(self, canvas_nodes: List[Node]) -> None: """ remove the nodes selected by the user and anything related to that node such as link, configurations, interfaces @@ -826,14 +846,14 @@ class CoreClient: links.append(edge.link) self.ifaces_manager.removed(links) - def create_iface(self, canvas_node: CanvasNode) -> core_pb2.Interface: + def create_iface(self, canvas_node: CanvasNode) -> Interface: node = canvas_node.core_node ip4, ip6 = self.ifaces_manager.get_ips(node) ip4_mask = self.ifaces_manager.ip4_mask ip6_mask = self.ifaces_manager.ip6_mask iface_id = canvas_node.next_iface_id() name = f"eth{iface_id}" - iface = core_pb2.Interface( + iface = Interface( id=iface_id, name=name, ip4=ip4, @@ -852,7 +872,7 @@ class CoreClient: def create_link( self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode - ): + ) -> None: """ Create core link for a pair of canvas nodes, with token referencing the canvas edge. @@ -873,8 +893,8 @@ class CoreClient: dst_iface = self.create_iface(canvas_dst_node) self.iface_to_edge[(dst_node.id, dst_iface.id)] = edge.token - link = core_pb2.Link( - type=core_pb2.LinkType.WIRED, + link = Link( + type=LinkType.WIRED, node1_id=src_node.id, node2_id=dst_node.id, iface1=src_iface, @@ -896,7 +916,7 @@ class CoreClient: def get_wlan_configs_proto(self) -> List[WlanConfig]: configs = [] for canvas_node in self.canvas_nodes.values(): - if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: + if canvas_node.core_node.type != NodeType.WIRELESS_LAN: continue if not canvas_node.wlan_config: continue @@ -910,7 +930,7 @@ class CoreClient: def get_mobility_configs_proto(self) -> List[MobilityConfig]: configs = [] for canvas_node in self.canvas_nodes.values(): - if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: + if canvas_node.core_node.type != NodeType.WIRELESS_LAN: continue if not canvas_node.mobility_config: continue @@ -924,7 +944,7 @@ class CoreClient: def get_emane_model_configs_proto(self) -> List[EmaneModelConfig]: configs = [] for canvas_node in self.canvas_nodes.values(): - if canvas_node.core_node.type != core_pb2.NodeType.EMANE: + if canvas_node.core_node.type != NodeType.EMANE: continue node_id = canvas_node.core_node.id for key, config in canvas_node.emane_model_configs.items(): @@ -975,9 +995,7 @@ class CoreClient: configs.append(config_proto) return configs - def get_config_service_configs_proto( - self - ) -> List[configservices_pb2.ConfigServiceConfig]: + def get_config_service_configs_proto(self) -> List[ConfigServiceConfig]: config_service_protos = [] for canvas_node in self.canvas_nodes.values(): if not NodeUtils.is_container_node(canvas_node.core_node.type): @@ -987,7 +1005,7 @@ class CoreClient: node_id = canvas_node.core_node.id for name, service_config in canvas_node.config_service_configs.items(): config = service_config.get("config", {}) - config_proto = configservices_pb2.ConfigServiceConfig( + config_proto = ConfigServiceConfig( node_id=node_id, name=name, templates=service_config["templates"], @@ -1000,7 +1018,7 @@ class CoreClient: logging.info("running node(%s) cmd: %s", node_id, self.observer) return self.client.node_command(self.session_id, node_id, self.observer).output - def get_wlan_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]: + def get_wlan_config(self, node_id: int) -> Dict[str, ConfigOption]: response = self.client.get_wlan_config(self.session_id, node_id) config = response.config logging.debug( @@ -1010,7 +1028,7 @@ class CoreClient: ) return dict(config) - def get_mobility_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]: + def get_mobility_config(self, node_id: int) -> Dict[str, ConfigOption]: response = self.client.get_mobility_config(self.session_id, node_id) config = response.config logging.debug( @@ -1022,7 +1040,7 @@ class CoreClient: def get_emane_model_config( self, node_id: int, model: str, iface_id: int = None - ) -> Dict[str, common_pb2.ConfigOption]: + ) -> Dict[str, ConfigOption]: if iface_id is None: iface_id = -1 response = self.client.get_emane_model_config( @@ -1030,7 +1048,8 @@ class CoreClient: ) config = response.config logging.debug( - "get emane model config: node id: %s, EMANE model: %s, interface: %s, config: %s", + "get emane model config: node id: %s, EMANE model: %s, " + "interface: %s, config: %s", node_id, model, iface_id, @@ -1038,7 +1057,7 @@ class CoreClient: ) return dict(config) - def execute_script(self, script): + def execute_script(self, script) -> None: response = self.client.execute_script(script) logging.info("execute python script %s", response) if response.session_id != -1: diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 269e3973..834220ea 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -2,7 +2,7 @@ import logging import tkinter as tk from copy import deepcopy from tkinter import BooleanVar -from typing import TYPE_CHECKING, Tuple +from typing import TYPE_CHECKING, Optional, Tuple from PIL import Image, ImageTk @@ -864,7 +864,7 @@ class CanvasGraph(tk.Canvas): for tag in tags.ORGANIZE_TAGS: self.tag_raise(tag) - def set_wallpaper(self, filename: str): + def set_wallpaper(self, filename: Optional[str]): logging.debug("setting wallpaper: %s", filename) if filename: img = Image.open(filename) diff --git a/daemon/core/gui/images.py b/daemon/core/gui/images.py index 3a953054..22719457 100644 --- a/daemon/core/gui/images.py +++ b/daemon/core/gui/images.py @@ -1,46 +1,44 @@ from enum import Enum from tkinter import messagebox +from typing import Dict, Optional, Tuple -from PIL import Image, ImageTk +from PIL import Image +from PIL.ImageTk import PhotoImage -from core.api.grpc import core_pb2 +from core.api.grpc.core_pb2 import NodeType from core.gui.appconfig import LOCAL_ICONS_PATH class Images: - images = {} + images: Dict[str, str] = {} @classmethod - def create(cls, file_path: str, width: int, height: int = None): + def create(cls, file_path: str, width: int, height: int = None) -> PhotoImage: if height is None: height = width image = Image.open(file_path) image = image.resize((width, height), Image.ANTIALIAS) - return ImageTk.PhotoImage(image) + return PhotoImage(image) @classmethod - def load_all(cls): + def load_all(cls) -> None: for image in LOCAL_ICONS_PATH.glob("*"): cls.images[image.stem] = str(image) @classmethod - def get( - cls, image_enum: Enum, width: int, height: int = None - ) -> ImageTk.PhotoImage: + def get(cls, image_enum: Enum, width: int, height: int = None) -> PhotoImage: file_path = cls.images[image_enum.value] return cls.create(file_path, width, height) @classmethod def get_with_image_file( cls, stem: str, width: int, height: int = None - ) -> ImageTk.PhotoImage: + ) -> PhotoImage: file_path = cls.images[stem] return cls.create(file_path, width, height) @classmethod - def get_custom( - cls, name: str, width: int, height: int = None - ) -> ImageTk.PhotoImage: + def get_custom(cls, name: str, width: int, height: int = None) -> PhotoImage: try: file_path = cls.images[name] return cls.create(file_path, width, height) @@ -95,22 +93,22 @@ class ImageEnum(Enum): class TypeToImage: - type_to_image = { - (core_pb2.NodeType.DEFAULT, "router"): ImageEnum.ROUTER, - (core_pb2.NodeType.DEFAULT, "PC"): ImageEnum.PC, - (core_pb2.NodeType.DEFAULT, "host"): ImageEnum.HOST, - (core_pb2.NodeType.DEFAULT, "mdr"): ImageEnum.MDR, - (core_pb2.NodeType.DEFAULT, "prouter"): ImageEnum.PROUTER, - (core_pb2.NodeType.HUB, ""): ImageEnum.HUB, - (core_pb2.NodeType.SWITCH, ""): ImageEnum.SWITCH, - (core_pb2.NodeType.WIRELESS_LAN, ""): ImageEnum.WLAN, - (core_pb2.NodeType.EMANE, ""): ImageEnum.EMANE, - (core_pb2.NodeType.RJ45, ""): ImageEnum.RJ45, - (core_pb2.NodeType.TUNNEL, ""): ImageEnum.TUNNEL, - (core_pb2.NodeType.DOCKER, ""): ImageEnum.DOCKER, - (core_pb2.NodeType.LXC, ""): ImageEnum.LXC, + type_to_image: Dict[Tuple[NodeType, str], ImageEnum] = { + (NodeType.DEFAULT, "router"): ImageEnum.ROUTER, + (NodeType.DEFAULT, "PC"): ImageEnum.PC, + (NodeType.DEFAULT, "host"): ImageEnum.HOST, + (NodeType.DEFAULT, "mdr"): ImageEnum.MDR, + (NodeType.DEFAULT, "prouter"): ImageEnum.PROUTER, + (NodeType.HUB, ""): ImageEnum.HUB, + (NodeType.SWITCH, ""): ImageEnum.SWITCH, + (NodeType.WIRELESS_LAN, ""): ImageEnum.WLAN, + (NodeType.EMANE, ""): ImageEnum.EMANE, + (NodeType.RJ45, ""): ImageEnum.RJ45, + (NodeType.TUNNEL, ""): ImageEnum.TUNNEL, + (NodeType.DOCKER, ""): ImageEnum.DOCKER, + (NodeType.LXC, ""): ImageEnum.LXC, } @classmethod - def get(cls, node_type, model): - return cls.type_to_image.get((node_type, model), None) + def get(cls, node_type, model) -> Optional[ImageEnum]: + return cls.type_to_image.get((node_type, model)) diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index 6c82ca51..f4f2e3cc 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -1,18 +1,18 @@ import logging -from typing import TYPE_CHECKING, Any, List, Optional, Set, Tuple +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple import netaddr from netaddr import EUI, IPNetwork +from core.api.grpc.core_pb2 import Interface, Link, Node +from core.gui.graph.node import CanvasNode from core.gui.nodeutils import NodeUtils if TYPE_CHECKING: from core.gui.app import Application - from core.api.grpc import core_pb2 - from core.gui.graph.node import CanvasNode -def get_index(iface: "core_pb2.Interface") -> Optional[int]: +def get_index(iface: Interface) -> Optional[int]: if not iface.ip4: return None net = netaddr.IPNetwork(f"{iface.ip4}/{iface.ip4_mask}") @@ -44,18 +44,18 @@ class Subnets: class InterfaceManager: def __init__(self, app: "Application") -> None: - self.app = app + self.app: "Application" = app ip4 = self.app.guiconfig.ips.ip4 ip6 = self.app.guiconfig.ips.ip6 - self.ip4_mask = 24 - self.ip6_mask = 64 - self.ip4_subnets = IPNetwork(f"{ip4}/{self.ip4_mask}") - self.ip6_subnets = IPNetwork(f"{ip6}/{self.ip6_mask}") + self.ip4_mask: int = 24 + self.ip6_mask: int = 64 + self.ip4_subnets: IPNetwork = IPNetwork(f"{ip4}/{self.ip4_mask}") + self.ip6_subnets: IPNetwork = IPNetwork(f"{ip6}/{self.ip6_mask}") mac = self.app.guiconfig.mac - self.mac = EUI(mac, dialect=netaddr.mac_unix_expanded) - self.current_mac = None - self.current_subnets = None - self.used_subnets = {} + self.mac: EUI = EUI(mac, dialect=netaddr.mac_unix_expanded) + self.current_mac: Optional[EUI] = None + self.current_subnets: Optional[Subnets] = None + self.used_subnets: Dict[Tuple[IPNetwork, IPNetwork], Subnets] = {} def update_ips(self, ip4: str, ip6: str) -> None: self.reset() @@ -84,7 +84,7 @@ class InterfaceManager: self.current_subnets = None self.used_subnets.clear() - def removed(self, links: List["core_pb2.Link"]) -> None: + def removed(self, links: List[Link]) -> None: # get remaining subnets remaining_subnets = set() for edge in self.app.core.links.values(): @@ -114,7 +114,7 @@ class InterfaceManager: subnets.used_indexes.discard(index) self.current_subnets = None - def joined(self, links: List["core_pb2.Link"]) -> None: + def joined(self, links: List[Link]) -> None: ifaces = [] for link in links: if link.HasField("iface1"): @@ -132,7 +132,7 @@ class InterfaceManager: if subnets.key() not in self.used_subnets: self.used_subnets[subnets.key()] = subnets - def next_index(self, node: "core_pb2.Node") -> int: + def next_index(self, node: Node) -> int: if NodeUtils.is_router_node(node): index = 1 else: @@ -144,13 +144,13 @@ class InterfaceManager: index += 1 return index - def get_ips(self, node: "core_pb2.Node") -> [str, str]: + def get_ips(self, node: Node) -> [str, str]: index = self.next_index(node) ip4 = self.current_subnets.ip4[index] ip6 = self.current_subnets.ip6[index] return str(ip4), str(ip6) - def get_subnets(self, iface: "core_pb2.Interface") -> Subnets: + def get_subnets(self, iface: Interface) -> Subnets: ip4_subnet = self.ip4_subnets if iface.ip4: ip4_subnet = IPNetwork(f"{iface.ip4}/{iface.ip4_mask}").cidr @@ -161,7 +161,7 @@ class InterfaceManager: return self.used_subnets.get(subnets.key(), subnets) def determine_subnets( - self, canvas_src_node: "CanvasNode", canvas_dst_node: "CanvasNode" + self, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode ) -> None: src_node = canvas_src_node.core_node dst_node = canvas_dst_node.core_node @@ -185,7 +185,7 @@ class InterfaceManager: logging.info("ignoring subnet change for link between network nodes") def find_subnets( - self, canvas_node: "CanvasNode", visited: Set[int] = None + self, canvas_node: CanvasNode, visited: Set[int] = None ) -> Optional[IPNetwork]: logging.info("finding subnet for node: %s", canvas_node.core_node.name) canvas = self.app.canvas diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index cf4216d8..523f8f11 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -4,9 +4,10 @@ import tkinter as tk import webbrowser from functools import partial from tkinter import filedialog, messagebox -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from core.gui.appconfig import XMLS_PATH +from core.gui.coreclient import CoreClient from core.gui.dialogs.about import AboutDialog from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog from core.gui.dialogs.canvaswallpaper import CanvasWallpaperDialog @@ -22,6 +23,7 @@ from core.gui.dialogs.servers import ServersDialog from core.gui.dialogs.sessionoptions import SessionOptionsDialog from core.gui.dialogs.sessions import SessionsDialog from core.gui.dialogs.throughput import ThroughputDialog +from core.gui.graph.graph import CanvasGraph from core.gui.nodeutils import ICON_SIZE from core.gui.observers import ObserversMenu from core.gui.task import ProgressTask @@ -29,7 +31,7 @@ from core.gui.task import ProgressTask if TYPE_CHECKING: from core.gui.app import Application -MAX_FILES = 3 +MAX_FILES: int = 3 class Menubar(tk.Menu): @@ -42,12 +44,12 @@ class Menubar(tk.Menu): Create a CoreMenubar instance """ super().__init__(app) - self.app = app - self.core = app.core - self.canvas = app.canvas - self.recent_menu = None - self.edit_menu = None - self.observers_menu = None + self.app: "Application" = app + self.core: CoreClient = app.core + self.canvas: CanvasGraph = app.canvas + self.recent_menu: Optional[tk.Menu] = None + self.edit_menu: Optional[tk.Menu] = None + self.observers_menu: Optional[tk.Menu] = None self.draw() def draw(self) -> None: diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index 40204662..402eca4d 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -1,38 +1,36 @@ import logging -from typing import TYPE_CHECKING, List, Optional, Set +from typing import List, Optional, Set + +from PIL.ImageTk import PhotoImage from core.api.grpc.core_pb2 import Node, NodeType from core.gui.appconfig import CustomNode, GuiConfig from core.gui.images import ImageEnum, Images, TypeToImage -if TYPE_CHECKING: - from core.api.grpc import core_pb2 - from PIL import ImageTk - -ICON_SIZE = 48 -ANTENNA_SIZE = 32 +ICON_SIZE: int = 48 +ANTENNA_SIZE: int = 32 class NodeDraw: - def __init__(self): + def __init__(self) -> None: self.custom: bool = False - self.image = None + self.image: Optional[str] = None self.image_enum: Optional[ImageEnum] = None - self.image_file = None - self.node_type: core_pb2.NodeType = None + self.image_file: Optional[str] = None + self.node_type: NodeType = None self.model: Optional[str] = None self.services: Set[str] = set() - self.label = None + self.label: Optional[str] = None @classmethod def from_setup( cls, image_enum: ImageEnum, - node_type: "core_pb2.NodeType", + node_type: NodeType, label: str, model: str = None, - tooltip=None, - ): + tooltip: str = None, + ) -> "NodeDraw": node_draw = NodeDraw() node_draw.image_enum = image_enum node_draw.image = Images.get(image_enum, ICON_SIZE) @@ -43,7 +41,7 @@ class NodeDraw: return node_draw @classmethod - def from_custom(cls, custom_node: CustomNode): + def from_custom(cls, custom_node: CustomNode) -> "NodeDraw": node_draw = NodeDraw() node_draw.custom = True node_draw.image_file = custom_node.image @@ -57,17 +55,17 @@ class NodeDraw: class NodeUtils: - NODES = [] - NETWORK_NODES = [] + NODES: List[NodeDraw] = [] + NETWORK_NODES: List[NodeDraw] = [] NODE_ICONS = {} - CONTAINER_NODES = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} - IMAGE_NODES = {NodeType.DOCKER, NodeType.LXC} - WIRELESS_NODES = {NodeType.WIRELESS_LAN, NodeType.EMANE} - RJ45_NODES = {NodeType.RJ45} - IGNORE_NODES = {NodeType.CONTROL_NET, NodeType.PEER_TO_PEER} - NODE_MODELS = {"router", "host", "PC", "mdr", "prouter"} - ROUTER_NODES = {"router", "mdr"} - ANTENNA_ICON = None + CONTAINER_NODES: Set[NodeType] = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} + IMAGE_NODES: Set[NodeType] = {NodeType.DOCKER, NodeType.LXC} + WIRELESS_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE} + RJ45_NODES: Set[NodeType] = {NodeType.RJ45} + IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET, NodeType.PEER_TO_PEER} + NODE_MODELS: Set[str] = {"router", "host", "PC", "mdr", "prouter"} + ROUTER_NODES: Set[str] = {"router", "mdr"} + ANTENNA_ICON: PhotoImage = None @classmethod def is_router_node(cls, node: Node) -> bool: @@ -99,8 +97,8 @@ class NodeUtils: @classmethod def node_icon( - cls, node_type: NodeType, model: str, gui_config: GuiConfig, scale=1.0 - ) -> "ImageTk.PhotoImage": + cls, node_type: NodeType, model: str, gui_config: GuiConfig, scale: float = 1.0 + ) -> PhotoImage: image_enum = TypeToImage.get(node_type, model) if image_enum: @@ -112,8 +110,8 @@ class NodeUtils: @classmethod def node_image( - cls, core_node: "core_pb2.Node", gui_config: GuiConfig, scale=1.0 - ) -> "ImageTk.PhotoImage": + cls, core_node: Node, gui_config: GuiConfig, scale: float = 1.0 + ) -> PhotoImage: image = cls.node_icon(core_node.type, core_node.model, gui_config, scale) if core_node.icon: try: @@ -141,7 +139,7 @@ class NodeUtils: return None @classmethod - def setup(cls): + def setup(cls) -> None: nodes = [ (ImageEnum.ROUTER, NodeType.DEFAULT, "Router", "router"), (ImageEnum.HOST, NodeType.DEFAULT, "Host", "host"), diff --git a/daemon/core/gui/observers.py b/daemon/core/gui/observers.py index 27d0a26e..7879494b 100644 --- a/daemon/core/gui/observers.py +++ b/daemon/core/gui/observers.py @@ -1,13 +1,13 @@ import tkinter as tk from functools import partial -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict from core.gui.dialogs.observers import ObserverDialog if TYPE_CHECKING: from core.gui.app import Application -OBSERVERS = { +OBSERVERS: Dict[str, str] = { "List Processes": "ps", "Show Interfaces": "ip address", "IPV4 Routes": "ip -4 route", @@ -23,9 +23,9 @@ OBSERVERS = { class ObserversMenu(tk.Menu): def __init__(self, master: tk.BaseWidget, app: "Application") -> None: super().__init__(master) - self.app = app - self.observer = tk.StringVar(value=tk.NONE) - self.custom_index = 0 + self.app: "Application" = app + self.observer: tk.StringVar = tk.StringVar(value=tk.NONE) + self.custom_index: int = 0 self.draw() def draw(self) -> None: diff --git a/daemon/core/gui/statusbar.py b/daemon/core/gui/statusbar.py index 3f58e7a0..2b597b63 100644 --- a/daemon/core/gui/statusbar.py +++ b/daemon/core/gui/statusbar.py @@ -3,8 +3,9 @@ status bar """ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List, Optional +from core.api.grpc.core_pb2 import ExceptionEvent from core.gui.dialogs.alerts import AlertsDialog from core.gui.themes import Styles @@ -13,20 +14,19 @@ if TYPE_CHECKING: class StatusBar(ttk.Frame): - def __init__(self, master: tk.Widget, app: "Application"): + def __init__(self, master: tk.Widget, app: "Application") -> None: super().__init__(master) - self.app = app - self.status = None - self.statusvar = tk.StringVar() - self.zoom = None - self.cpu_usage = None - self.memory = None - self.alerts_button = None - self.running = False - self.core_alarms = [] + self.app: "Application" = app + self.status: Optional[ttk.Label] = None + self.statusvar: tk.StringVar = tk.StringVar() + self.zoom: Optional[ttk.Label] = None + self.cpu_usage: Optional[ttk.Label] = None + self.alerts_button: Optional[ttk.Button] = None + self.running: bool = False + self.core_alarms: List[ExceptionEvent] = [] self.draw() - def draw(self): + def draw(self) -> None: self.columnconfigure(0, weight=7) self.columnconfigure(1, weight=1) self.columnconfigure(2, weight=1) @@ -64,9 +64,9 @@ class StatusBar(ttk.Frame): ) self.alerts_button.grid(row=0, column=3, sticky="ew") - def click_alerts(self): + def click_alerts(self) -> None: dialog = AlertsDialog(self.app) dialog.show() - def set_status(self, message: str): + def set_status(self, message: str) -> None: self.statusvar.set(message) diff --git a/daemon/core/gui/task.py b/daemon/core/gui/task.py index 2f055a90..b4a5f68f 100644 --- a/daemon/core/gui/task.py +++ b/daemon/core/gui/task.py @@ -1,7 +1,7 @@ import logging import threading import time -from typing import TYPE_CHECKING, Any, Callable, Tuple +from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple if TYPE_CHECKING: from core.gui.app import Application @@ -16,14 +16,14 @@ class ProgressTask: callback: Callable = None, args: Tuple[Any] = None, ): - self.app = app - self.title = title - self.task = task - self.callback = callback - self.args = args - if self.args is None: - self.args = () - self.time = None + self.app: "Application" = app + self.title: str = title + self.task: Callable = task + self.callback: Callable = callback + if args is None: + args = () + self.args: Tuple[Any] = args + self.time: Optional[float] = None def start(self) -> None: self.app.progress.grid(sticky="ew") @@ -49,7 +49,7 @@ class ProgressTask: finally: self.app.after(0, self.complete) - def complete(self): + def complete(self) -> None: self.app.progress.stop() self.app.progress.grid_forget() total = time.perf_counter() - self.time diff --git a/daemon/core/gui/themes.py b/daemon/core/gui/themes.py index 141a7a5c..93a0a599 100644 --- a/daemon/core/gui/themes.py +++ b/daemon/core/gui/themes.py @@ -1,39 +1,40 @@ import tkinter as tk from tkinter import font, ttk +from typing import Dict, Tuple -THEME_DARK = "black" -PADX = (0, 5) -PADY = (0, 5) -FRAME_PAD = 5 -DIALOG_PAD = 5 +THEME_DARK: str = "black" +PADX: Tuple[int, int] = (0, 5) +PADY: Tuple[int, int] = (0, 5) +FRAME_PAD: int = 5 +DIALOG_PAD: int = 5 class Styles: - tooltip = "Tooltip.TLabel" - tooltip_frame = "Tooltip.TFrame" - service_checkbutton = "Service.TCheckbutton" - picker_button = "Picker.TButton" - green_alert = "GAlert.TButton" - red_alert = "RAlert.TButton" - yellow_alert = "YAlert.TButton" + tooltip: str = "Tooltip.TLabel" + tooltip_frame: str = "Tooltip.TFrame" + service_checkbutton: str = "Service.TCheckbutton" + picker_button: str = "Picker.TButton" + green_alert: str = "GAlert.TButton" + red_alert: str = "RAlert.TButton" + yellow_alert: str = "YAlert.TButton" class Colors: - disabledfg = "DarkGrey" - frame = "#424242" - dark = "#222222" - darker = "#121212" - darkest = "black" - lighter = "#626262" - lightest = "#ffffff" - selectbg = "#4a6984" - selectfg = "#ffffff" - white = "white" - black = "black" - listboxbg = "#f2f1f0" + disabledfg: str = "DarkGrey" + frame: str = "#424242" + dark: str = "#222222" + darker: str = "#121212" + darkest: str = "black" + lighter: str = "#626262" + lightest: str = "#ffffff" + selectbg: str = "#4a6984" + selectfg: str = "#ffffff" + white: str = "white" + black: str = "black" + listboxbg: str = "#f2f1f0" -def load(style: ttk.Style): +def load(style: ttk.Style) -> None: style.theme_create( THEME_DARK, "clam", @@ -139,13 +140,13 @@ def load(style: ttk.Style): ) -def theme_change_menu(event: tk.Event): +def theme_change_menu(event: tk.Event) -> None: if not isinstance(event.widget, tk.Menu): return style_menu(event.widget) -def style_menu(widget: tk.Widget): +def style_menu(widget: tk.Widget) -> None: style = ttk.Style() bg = style.lookup(".", "background") fg = style.lookup(".", "foreground") @@ -157,7 +158,7 @@ def style_menu(widget: tk.Widget): ) -def style_listbox(widget: tk.Widget): +def style_listbox(widget: tk.Widget) -> None: style = ttk.Style() bg = style.lookup(".", "background") fg = style.lookup(".", "foreground") @@ -174,7 +175,7 @@ def style_listbox(widget: tk.Widget): ) -def theme_change(event: tk.Event): +def theme_change(event: tk.Event) -> None: style = ttk.Style() style.configure(Styles.picker_button, font="TkSmallCaptionFont") style.configure( @@ -203,7 +204,7 @@ def theme_change(event: tk.Event): ) -def scale_fonts(fonts_size, scale): +def scale_fonts(fonts_size: Dict[str, int], scale: float) -> None: for name in font.names(): f = font.nametofont(name) if name in fonts_size: diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 54fac126..c3e9067f 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -3,7 +3,7 @@ import tkinter as tk from enum import Enum from functools import partial from tkinter import ttk -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Callable, List, Optional from PIL.ImageTk import PhotoImage @@ -23,8 +23,8 @@ from core.gui.tooltip import Tooltip if TYPE_CHECKING: from core.gui.app import Application -TOOLBAR_SIZE = 32 -PICKER_SIZE = 24 +TOOLBAR_SIZE: int = 32 +PICKER_SIZE: int = 24 class NodeTypeEnum(Enum): @@ -42,8 +42,8 @@ def enable_buttons(frame: ttk.Frame, enabled: bool) -> None: class PickerFrame(ttk.Frame): def __init__(self, app: "Application", button: ttk.Button) -> None: super().__init__(app) - self.app = app - self.button = button + self.app: "Application" = app + self.button: ttk.Button = button def create_node_button(self, node_draw: NodeDraw, func: Callable) -> None: self.create_button( @@ -85,10 +85,10 @@ class PickerFrame(ttk.Frame): class ButtonBar(ttk.Frame): - def __init__(self, master: tk.Widget, app: "Application"): + def __init__(self, master: tk.Widget, app: "Application") -> None: super().__init__(master) - self.app = app - self.radio_buttons = [] + self.app: "Application" = app + self.radio_buttons: List[ttk.Button] = [] def create_button( self, image_enum: ImageEnum, func: Callable, tooltip: str, radio: bool = False @@ -109,14 +109,14 @@ class ButtonBar(ttk.Frame): class MarkerFrame(ttk.Frame): - PAD = 3 + PAD: int = 3 def __init__(self, master: tk.BaseWidget, app: "Application") -> None: super().__init__(master, padding=self.PAD) - self.app = app - self.color = "#000000" - self.size = tk.DoubleVar() - self.color_frame = None + self.app: "Application" = app + self.color: str = "#000000" + self.size: tk.DoubleVar = tk.DoubleVar() + self.color_frame: Optional[tk.Frame] = None self.draw() def draw(self) -> None: @@ -144,7 +144,7 @@ class MarkerFrame(ttk.Frame): self.color_frame.bind("", self.click_color) Tooltip(self.color_frame, "Marker Color") - def click_clear(self): + def click_clear(self) -> None: self.app.canvas.delete(tags.MARKER) def click_color(self, _event: tk.Event) -> None: @@ -163,37 +163,37 @@ class Toolbar(ttk.Frame): Create a CoreToolbar instance """ super().__init__(app) - self.app = app + self.app: "Application" = app # design buttons - self.play_button = None - self.select_button = None - self.link_button = None - self.node_button = None - self.network_button = None - self.annotation_button = None + self.play_button: Optional[ttk.Button] = None + self.select_button: Optional[ttk.Button] = None + self.link_button: Optional[ttk.Button] = None + self.node_button: Optional[ttk.Button] = None + self.network_button: Optional[ttk.Button] = None + self.annotation_button: Optional[ttk.Button] = None # runtime buttons - self.runtime_select_button = None - self.stop_button = None - self.runtime_marker_button = None - self.run_command_button = None + self.runtime_select_button: Optional[ttk.Button] = None + self.stop_button: Optional[ttk.Button] = None + self.runtime_marker_button: Optional[ttk.Button] = None + self.run_command_button: Optional[ttk.Button] = None # frames - self.design_frame = None - self.runtime_frame = None - self.marker_frame = None - self.picker = None + self.design_frame: Optional[ButtonBar] = None + self.runtime_frame: Optional[ButtonBar] = None + self.marker_frame: Optional[MarkerFrame] = None + self.picker: Optional[PickerFrame] = None # observers - self.observers_menu = None + self.observers_menu: Optional[ObserversMenu] = None # these variables help keep track of what images being drawn so that scaling # is possible since PhotoImage does not have resize method - self.current_node = NodeUtils.NODES[0] - self.current_network = NodeUtils.NETWORK_NODES[0] - self.current_annotation = ShapeType.MARKER - self.annotation_enum = ImageEnum.MARKER + self.current_node: NodeDraw = NodeUtils.NODES[0] + self.current_network: NodeDraw = NodeUtils.NETWORK_NODES[0] + self.current_annotation: ShapeType = ShapeType.MARKER + self.annotation_enum: ImageEnum = ImageEnum.MARKER # draw components self.draw() diff --git a/daemon/core/gui/tooltip.py b/daemon/core/gui/tooltip.py index bc1ed9b5..c2978510 100644 --- a/daemon/core/gui/tooltip.py +++ b/daemon/core/gui/tooltip.py @@ -1,5 +1,6 @@ import tkinter as tk from tkinter import ttk +from typing import Optional from core.gui.themes import Styles @@ -9,19 +10,19 @@ class Tooltip(object): Create tool tip for a given widget """ - def __init__(self, widget: tk.Widget, text: str = "widget info"): - self.widget = widget - self.text = text + def __init__(self, widget: tk.BaseWidget, text: str = "widget info") -> None: + self.widget: tk.BaseWidget = widget + self.text: str = text self.widget.bind("", self.on_enter) self.widget.bind("", self.on_leave) - self.waittime = 400 - self.id = None - self.tw = None + self.waittime: int = 400 + self.id: Optional[str] = None + self.tw: Optional[tk.Toplevel] = None - def on_enter(self, event: tk.Event = None): + def on_enter(self, event: tk.Event = None) -> None: self.schedule() - def on_leave(self, event: tk.Event = None): + def on_leave(self, event: tk.Event = None) -> None: self.unschedule() self.close(event) @@ -39,7 +40,6 @@ class Tooltip(object): x, y, cx, cy = self.widget.bbox("insert") x += self.widget.winfo_rootx() y += self.widget.winfo_rooty() + 32 - self.tw = tk.Toplevel(self.widget) self.tw.wm_overrideredirect(True) self.tw.wm_geometry("+%d+%d" % (x, y)) diff --git a/daemon/core/gui/validation.py b/daemon/core/gui/validation.py index 873db189..22f12bb8 100644 --- a/daemon/core/gui/validation.py +++ b/daemon/core/gui/validation.py @@ -4,16 +4,23 @@ input validation import re import tkinter as tk from tkinter import ttk +from typing import Any, Optional, Pattern -SMALLEST_SCALE = 0.5 -LARGEST_SCALE = 5.0 -HEX_REGEX = re.compile("^([#]([0-9]|[a-f])+)$|^[#]$") +SMALLEST_SCALE: float = 0.5 +LARGEST_SCALE: float = 5.0 +HEX_REGEX: Pattern = re.compile("^([#]([0-9]|[a-f])+)$|^[#]$") class ValidationEntry(ttk.Entry): - empty = None + empty: Optional[str] = None - def __init__(self, master=None, widget=None, empty_enabled=True, **kwargs) -> None: + def __init__( + self, + master: tk.BaseWidget = None, + widget: tk.BaseWidget = None, + empty_enabled: bool = True, + **kwargs: Any + ) -> None: super().__init__(master, widget, **kwargs) cmd = self.register(self.is_valid) self.configure(validate="key", validatecommand=(cmd, "%P")) @@ -30,7 +37,7 @@ class ValidationEntry(ttk.Entry): class PositiveIntEntry(ValidationEntry): - empty = "0" + empty: str = "0" def is_valid(self, s: str) -> bool: if not s: @@ -92,7 +99,7 @@ class HexEntry(ValidationEntry): class NodeNameEntry(ValidationEntry): - empty = "noname" + empty: str = "noname" def is_valid(self, s: str) -> bool: if len(s) < 0: diff --git a/daemon/core/gui/widgets.py b/daemon/core/gui/widgets.py index 6f51bd8c..2eded212 100644 --- a/daemon/core/gui/widgets.py +++ b/daemon/core/gui/widgets.py @@ -1,53 +1,63 @@ import logging import tkinter as tk from functools import partial -from pathlib import PosixPath +from pathlib import Path from tkinter import filedialog, font, ttk -from typing import TYPE_CHECKING, Dict +from typing import TYPE_CHECKING, Any, Callable, Dict, Set, Type -from core.api.grpc import common_pb2, core_pb2 +from core.api.grpc import core_pb2 +from core.api.grpc.common_pb2 import ConfigOption +from core.api.grpc.core_pb2 import ConfigOptionType from core.gui import themes, validation +from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY if TYPE_CHECKING: from core.gui.app import Application - from core.gui.dialogs.dialog import Dialog -INT_TYPES = { - core_pb2.ConfigOptionType.UINT8, - core_pb2.ConfigOptionType.UINT16, - core_pb2.ConfigOptionType.UINT32, - core_pb2.ConfigOptionType.UINT64, - core_pb2.ConfigOptionType.INT8, - core_pb2.ConfigOptionType.INT16, - core_pb2.ConfigOptionType.INT32, - core_pb2.ConfigOptionType.INT64, +INT_TYPES: Set[ConfigOptionType] = { + ConfigOptionType.UINT8, + ConfigOptionType.UINT16, + ConfigOptionType.UINT32, + ConfigOptionType.UINT64, + ConfigOptionType.INT8, + ConfigOptionType.INT16, + ConfigOptionType.INT32, + ConfigOptionType.INT64, } -def file_button_click(value: tk.StringVar, parent: tk.Widget): +def file_button_click(value: tk.StringVar, parent: tk.Widget) -> None: file_path = filedialog.askopenfilename(title="Select File", parent=parent) if file_path: value.set(file_path) class FrameScroll(ttk.Frame): - def __init__(self, master: tk.Widget, app: "Application", _cls=ttk.Frame, **kw): + def __init__( + self, + master: tk.Widget, + app: "Application", + _cls: Type[ttk.Frame] = ttk.Frame, + **kw: Any + ) -> None: super().__init__(master, **kw) - self.app = app + self.app: "Application" = app self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) bg = self.app.style.lookup(".", "background") - self.canvas = tk.Canvas(self, highlightthickness=0, background=bg) + self.canvas: tk.Canvas = tk.Canvas(self, highlightthickness=0, background=bg) self.canvas.grid(row=0, sticky="nsew", padx=2, pady=2) self.canvas.columnconfigure(0, weight=1) self.canvas.rowconfigure(0, weight=1) - self.scrollbar = ttk.Scrollbar( + self.scrollbar: ttk.Scrollbar = ttk.Scrollbar( self, orient="vertical", command=self.canvas.yview ) self.scrollbar.grid(row=0, column=1, sticky="ns") - self.frame = _cls(self.canvas) - self.frame_id = self.canvas.create_window(0, 0, anchor="nw", window=self.frame) + self.frame: ttk.Frame = _cls(self.canvas) + self.frame_id: int = self.canvas.create_window( + 0, 0, anchor="nw", window=self.frame + ) self.canvas.update_idletasks() self.canvas.configure( scrollregion=self.canvas.bbox("all"), yscrollcommand=self.scrollbar.set @@ -55,16 +65,16 @@ class FrameScroll(ttk.Frame): self.frame.bind("", self._configure_frame) self.canvas.bind("", self._configure_canvas) - def _configure_frame(self, event: tk.Event): + def _configure_frame(self, event: tk.Event) -> None: req_width = self.frame.winfo_reqwidth() if req_width != self.canvas.winfo_reqwidth(): self.canvas.configure(width=req_width) self.canvas.configure(scrollregion=self.canvas.bbox("all")) - def _configure_canvas(self, event: tk.Event): + def _configure_canvas(self, event: tk.Event) -> None: self.canvas.itemconfig(self.frame_id, width=event.width) - def clear(self): + def clear(self) -> None: for widget in self.frame.winfo_children(): widget.destroy() @@ -74,15 +84,15 @@ class ConfigFrame(ttk.Notebook): self, master: tk.Widget, app: "Application", - config: Dict[str, common_pb2.ConfigOption], - **kw - ): + config: Dict[str, ConfigOption], + **kw: Any + ) -> None: super().__init__(master, **kw) - self.app = app - self.config = config - self.values = {} + self.app: "Application" = app + self.config: Dict[str, ConfigOption] = config + self.values: Dict[str, tk.StringVar] = {} - def draw_config(self): + def draw_config(self) -> None: group_mapping = {} for key in self.config: option = self.config[key] @@ -142,7 +152,7 @@ class ConfigFrame(ttk.Notebook): logging.error("unhandled config option type: %s", option.type) self.values[option.name] = value - def parse_config(self): + def parse_config(self) -> Dict[str, str]: for key in self.config: option = self.config[key] value = self.values[key] @@ -169,13 +179,13 @@ class ConfigFrame(ttk.Notebook): class ListboxScroll(ttk.Frame): - def __init__(self, master: tk.Widget = None, **kw): + def __init__(self, master: tk.BaseWidget = None, **kw: Any) -> None: super().__init__(master, **kw) self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) - self.scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL) + self.scrollbar: ttk.Scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL) self.scrollbar.grid(row=0, column=1, sticky="ns") - self.listbox = tk.Listbox( + self.listbox: tk.Listbox = tk.Listbox( self, selectmode=tk.BROWSE, yscrollcommand=self.scrollbar.set, @@ -187,12 +197,18 @@ class ListboxScroll(ttk.Frame): class CheckboxList(FrameScroll): - def __init__(self, master: ttk.Widget, app: "Application", clicked=None, **kw): + def __init__( + self, + master: ttk.Widget, + app: "Application", + clicked: Callable = None, + **kw: Any + ) -> None: super().__init__(master, app, **kw) - self.clicked = clicked + self.clicked: Callable = clicked self.frame.columnconfigure(0, weight=1) - def add(self, name: str, checked: bool): + def add(self, name: str, checked: bool) -> None: var = tk.BooleanVar(value=checked) func = partial(self.clicked, name, var) checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func) @@ -200,16 +216,16 @@ class CheckboxList(FrameScroll): class CodeFont(font.Font): - def __init__(self): + def __init__(self) -> None: super().__init__(font="TkFixedFont", color="green") class CodeText(ttk.Frame): - def __init__(self, master: tk.Widget, **kwargs): + def __init__(self, master: tk.BaseWidget, **kwargs: Any) -> None: super().__init__(master, **kwargs) self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) - self.text = tk.Text( + self.text: tk.Text = tk.Text( self, bd=0, bg="black", @@ -229,14 +245,14 @@ class CodeText(ttk.Frame): class Spinbox(ttk.Entry): - def __init__(self, master: tk.Widget = None, **kwargs): + def __init__(self, master: tk.BaseWidget = None, **kwargs: Any) -> None: super().__init__(master, "ttk::spinbox", **kwargs) - def set(self, value): + def set(self, value: str) -> None: self.tk.call(self._w, "set", value) -def image_chooser(parent: "Dialog", path: PosixPath): +def image_chooser(parent: Dialog, path: Path) -> str: return filedialog.askopenfilename( parent=parent, initialdir=str(path), From 11be40bc90135040897da6a2b0e2372f1fb7f097 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 19 Jun 2020 23:24:07 -0700 Subject: [PATCH 0387/1131] pygui: added class variable type hinting to core.gui.graph --- daemon/core/gui/coreclient.py | 2 +- daemon/core/gui/graph/edges.py | 69 +++++----- daemon/core/gui/graph/graph.py | 190 ++++++++++++++-------------- daemon/core/gui/graph/node.py | 105 ++++++++------- daemon/core/gui/graph/shape.py | 64 +++++----- daemon/core/gui/graph/shapeutils.py | 3 +- daemon/core/gui/graph/tags.py | 32 ++--- daemon/core/gui/graph/tooltip.py | 40 +++--- 8 files changed, 256 insertions(+), 249 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 24708769..5e1bf4c2 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -79,7 +79,7 @@ class CoreClient: self.read_config() # helpers - self.iface_to_edge: Dict[Tuple[int, int], Tuple[int, int]] = {} + self.iface_to_edge: Dict[Tuple[int, ...], Tuple[int, ...]] = {} self.ifaces_manager: InterfaceManager = InterfaceManager(self.app) # session data diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index ac637b28..e9ac2587 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -1,9 +1,10 @@ import logging import math import tkinter as tk -from typing import TYPE_CHECKING, Any, Tuple +from typing import TYPE_CHECKING, Optional, Tuple from core.api.grpc import core_pb2 +from core.api.grpc.core_pb2 import Interface, Link from core.gui import themes from core.gui.dialogs.linkconfig import LinkConfigurationDialog from core.gui.graph import tags @@ -12,12 +13,12 @@ from core.gui.nodeutils import NodeUtils if TYPE_CHECKING: from core.gui.graph.graph import CanvasGraph -TEXT_DISTANCE = 0.30 -EDGE_WIDTH = 3 -EDGE_COLOR = "#ff0000" -WIRELESS_WIDTH = 1.5 -WIRELESS_COLOR = "#009933" -ARC_DISTANCE = 50 +TEXT_DISTANCE: float = 0.30 +EDGE_WIDTH: int = 3 +EDGE_COLOR: str = "#ff0000" +WIRELESS_WIDTH: float = 1.5 +WIRELESS_COLOR: str = "#009933" +ARC_DISTANCE: int = 50 def create_edge_token(src: int, dst: int, network: int = None) -> Tuple[int, ...]: @@ -57,20 +58,20 @@ def arc_edges(edges) -> None: class Edge: - tag = tags.EDGE + tag: str = tags.EDGE def __init__(self, canvas: "CanvasGraph", src: int, dst: int = None) -> None: self.canvas = canvas - self.id = None - self.src = src - self.dst = dst - self.arc = 0 - self.token = None - self.src_label = None - self.middle_label = None - self.dst_label = None - self.color = EDGE_COLOR - self.width = EDGE_WIDTH + self.id: Optional[int] = None + self.src: int = src + self.dst: int = dst + self.arc: int = 0 + self.token: Optional[Tuple[int, ...]] = 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, ...]: @@ -120,7 +121,7 @@ class Edge: fill=self.color, ) - def redraw(self): + def redraw(self) -> None: self.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color) src_x, src_y, _, _, _, _ = self.canvas.coords(self.id) src_pos = src_x, src_y @@ -233,13 +234,13 @@ class CanvasWirelessEdge(Edge): dst: int, src_pos: Tuple[float, float], dst_pos: Tuple[float, float], - token: Tuple[Any, ...], + token: Tuple[int, ...], ) -> None: logging.debug("drawing wireless link from node %s to node %s", src, dst) super().__init__(canvas, src, dst) - self.token = token - self.width = WIRELESS_WIDTH - self.color = WIRELESS_COLOR + self.token: Tuple[int, ...] = token + self.width: float = WIRELESS_WIDTH + self.color: str = WIRELESS_COLOR self.draw(src_pos, dst_pos) @@ -259,19 +260,19 @@ class CanvasEdge(Edge): Create an instance of canvas edge object """ super().__init__(canvas, src) - self.src_iface = None - self.dst_iface = None - self.text_src = None - self.text_dst = None - self.link = None - self.asymmetric_link = None - self.throughput = None + 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.asymmetric_link: Optional[Link] = None + self.throughput: Optional[float] = None self.draw(src_pos, dst_pos) self.set_binding() - self.context = tk.Menu(self.canvas) + self.context: tk.Menu = tk.Menu(self.canvas) self.create_context() - def create_context(self): + def create_context(self) -> None: themes.style_menu(self.context) self.context.add_command(label="Configure", command=self.click_configure) self.context.add_command(label="Delete", command=self.click_delete) @@ -279,7 +280,7 @@ class CanvasEdge(Edge): def set_binding(self) -> None: self.canvas.tag_bind(self.id, "", self.show_context) - def set_link(self, link) -> None: + def set_link(self, link: Link) -> None: self.link = link self.draw_labels() @@ -383,7 +384,7 @@ class CanvasEdge(Edge): self.context.entryconfigure(1, state=state) self.context.tk_popup(event.x_root, event.y_root) - def click_delete(self): + def click_delete(self) -> None: self.canvas.delete_edge(self) def click_configure(self) -> None: diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 834220ea..53115750 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -2,11 +2,12 @@ import logging import tkinter as tk from copy import deepcopy from tkinter import BooleanVar -from typing import TYPE_CHECKING, Optional, Tuple +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple -from PIL import Image, ImageTk +from PIL import Image +from PIL.ImageTk import PhotoImage -from core.api.grpc import core_pb2 +from core.api.grpc.core_pb2 import Interface, Link, LinkType, Session, ThroughputsEvent from core.gui.dialogs.shapemod import ShapeDialog from core.gui.graph import tags from core.gui.graph.edges import ( @@ -21,7 +22,7 @@ from core.gui.graph.node import CanvasNode from core.gui.graph.shape import Shape from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker from core.gui.images import ImageEnum, TypeToImage -from core.gui.nodeutils import NodeUtils +from core.gui.nodeutils import NodeDraw, NodeUtils if TYPE_CHECKING: from core.gui.app import Application @@ -48,58 +49,59 @@ class ShowVar(BooleanVar): class CanvasGraph(tk.Canvas): - def __init__(self, master: tk.Widget, app: "Application", core: "CoreClient"): + def __init__( + self, master: tk.BaseWidget, app: "Application", core: "CoreClient" + ) -> None: super().__init__(master, highlightthickness=0, background="#cccccc") - self.app = app - self.core = core - self.mode = GraphMode.SELECT - self.annotation_type = None - self.selection = {} - self.select_box = None - self.selected = None - self.node_draw = None - self.nodes = {} - self.edges = {} - self.shapes = {} - self.wireless_edges = {} + self.app: "Application" = app + self.core: "CoreClient" = core + self.mode: GraphMode = GraphMode.SELECT + self.annotation_type: Optional[ShapeType] = None + self.selection: Dict[int, int] = {} + self.select_box: Optional[Shape] = None + self.selected: Optional[int] = None + self.node_draw: Optional[NodeDraw] = None + self.nodes: Dict[int, CanvasNode] = {} + self.edges: Dict[int, CanvasEdge] = {} + self.shapes: Dict[int, Shape] = {} + self.wireless_edges: Dict[Tuple[int, ...], CanvasWirelessEdge] = {} # map wireless/EMANE node to the set of MDRs connected to that node - self.wireless_network = {} + self.wireless_network: Dict[int, Set[int]] = {} - self.drawing_edge = None - self.rect = None - self.shape_drawing = False + self.drawing_edge: Optional[CanvasEdge] = None + self.rect: Optional[int] = None + self.shape_drawing: bool = False width = self.app.guiconfig.preferences.width height = self.app.guiconfig.preferences.height - self.default_dimensions = (width, height) - self.current_dimensions = self.default_dimensions - self.ratio = 1.0 - self.offset = (0, 0) - self.cursor = (0, 0) - self.marker_tool = None - self.to_copy = [] + self.default_dimensions: Tuple[int, int] = (width, height) + self.current_dimensions: Tuple[int, int] = self.default_dimensions + self.ratio: float = 1.0 + self.offset: Tuple[int, int] = (0, 0) + self.cursor: Tuple[int, int] = (0, 0) + self.to_copy: List[CanvasNode] = [] # background related - self.wallpaper_id = None - self.wallpaper = None - self.wallpaper_drawn = None - self.wallpaper_file = "" - self.scale_option = tk.IntVar(value=1) - self.adjust_to_dim = tk.BooleanVar(value=False) + self.wallpaper_id: Optional[int] = None + self.wallpaper: Optional[Image.Image] = None + self.wallpaper_drawn: Optional[PhotoImage] = None + self.wallpaper_file: str = "" + self.scale_option: tk.IntVar = tk.IntVar(value=1) + self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(value=False) # throughput related - self.throughput_threshold = 250.0 - self.throughput_width = 10 - self.throughput_color = "#FF0000" + self.throughput_threshold: float = 250.0 + self.throughput_width: int = 10 + self.throughput_color: str = "#FF0000" # drawing related - self.show_node_labels = ShowVar(self, tags.NODE_LABEL, value=True) - self.show_link_labels = ShowVar(self, tags.LINK_LABEL, value=True) - self.show_grid = ShowVar(self, tags.GRIDLINE, value=True) - self.show_annotations = ShowVar(self, tags.ANNOTATION, value=True) - self.show_iface_names = BooleanVar(value=False) - self.show_ip4s = BooleanVar(value=True) - self.show_ip6s = BooleanVar(value=True) + self.show_node_labels: ShowVar = ShowVar(self, tags.NODE_LABEL, value=True) + self.show_link_labels: ShowVar = ShowVar(self, tags.LINK_LABEL, value=True) + self.show_grid: ShowVar = ShowVar(self, tags.GRIDLINE, value=True) + self.show_annotations: ShowVar = ShowVar(self, tags.ANNOTATION, value=True) + self.show_iface_names: BooleanVar = BooleanVar(value=False) + self.show_ip4s: BooleanVar = BooleanVar(value=True) + self.show_ip6s: BooleanVar = BooleanVar(value=True) # bindings self.setup_bindings() @@ -108,7 +110,7 @@ class CanvasGraph(tk.Canvas): self.draw_canvas() self.draw_grid() - def draw_canvas(self, dimensions: Tuple[int, int] = None): + def draw_canvas(self, dimensions: Tuple[int, int] = None) -> None: if self.rect is not None: self.delete(self.rect) if not dimensions: @@ -125,7 +127,7 @@ class CanvasGraph(tk.Canvas): ) self.configure(scrollregion=self.bbox(tk.ALL)) - def reset_and_redraw(self, session: core_pb2.Session): + def reset_and_redraw(self, session: Session) -> None: """ Reset the private variables CanvasGraph object, redraw nodes given the new grpc client. @@ -157,7 +159,7 @@ class CanvasGraph(tk.Canvas): self.drawing_edge = None self.draw_session(session) - def setup_bindings(self): + def setup_bindings(self) -> None: """ Bind any mouse events or hot keys to the matching action """ @@ -173,28 +175,28 @@ class CanvasGraph(tk.Canvas): self.bind("", lambda e: self.scan_mark(e.x, e.y)) self.bind("", lambda e: self.scan_dragto(e.x, e.y, gain=1)) - def get_actual_coords(self, x: float, y: float) -> [float, float]: + def get_actual_coords(self, x: float, y: float) -> Tuple[float, float]: actual_x = (x - self.offset[0]) / self.ratio actual_y = (y - self.offset[1]) / self.ratio return actual_x, actual_y - def get_scaled_coords(self, x: float, y: float) -> [float, float]: + def get_scaled_coords(self, x: float, y: float) -> Tuple[float, float]: scaled_x = (x * self.ratio) + self.offset[0] scaled_y = (y * self.ratio) + self.offset[1] return scaled_x, scaled_y - def inside_canvas(self, x: float, y: float) -> [bool, bool]: + def inside_canvas(self, x: float, y: float) -> Tuple[bool, bool]: x1, y1, x2, y2 = self.bbox(self.rect) valid_x = x1 <= x <= x2 valid_y = y1 <= y <= y2 return valid_x and valid_y - def valid_position(self, x1: int, y1: int, x2: int, y2: int) -> [bool, bool]: + def valid_position(self, x1: int, y1: int, x2: int, y2: int) -> Tuple[bool, bool]: valid_topleft = self.inside_canvas(x1, y1) valid_bottomright = self.inside_canvas(x2, y2) return valid_topleft and valid_bottomright - def set_throughputs(self, throughputs_event: core_pb2.ThroughputsEvent): + def set_throughputs(self, throughputs_event: ThroughputsEvent) -> None: for iface_throughput in throughputs_event.iface_throughputs: node_id = iface_throughput.node_id iface_id = iface_throughput.iface_id @@ -209,7 +211,7 @@ class CanvasGraph(tk.Canvas): else: del self.core.iface_to_edge[iface_to_edge_id] - def draw_grid(self): + def draw_grid(self) -> None: """ Create grid. """ @@ -223,9 +225,7 @@ class CanvasGraph(tk.Canvas): self.tag_lower(tags.GRIDLINE) self.tag_lower(self.rect) - def add_wireless_edge( - self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link - ) -> None: + 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) if token in self.wireless_edges: @@ -248,7 +248,7 @@ class CanvasGraph(tk.Canvas): arc_edges(common_edges) def delete_wireless_edge( - self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link + 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) @@ -263,7 +263,7 @@ class CanvasGraph(tk.Canvas): arc_edges(common_edges) def update_wireless_edge( - self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link + self, src: CanvasNode, dst: CanvasNode, link: Link ) -> None: if not link.label: return @@ -275,7 +275,7 @@ class CanvasGraph(tk.Canvas): edge = self.wireless_edges[token] edge.middle_label_text(link.label) - def draw_session(self, session: core_pb2.Session): + def draw_session(self, session: Session) -> None: """ Draw existing session. """ @@ -306,7 +306,7 @@ class CanvasGraph(tk.Canvas): node2 = canvas_node2.core_node token = create_edge_token(canvas_node1.id, canvas_node2.id) - if link.type == core_pb2.LinkType.WIRELESS: + if link.type == LinkType.WIRELESS: self.add_wireless_edge(canvas_node1, canvas_node2, link) else: if token not in self.edges: @@ -337,7 +337,7 @@ class CanvasGraph(tk.Canvas): else: logging.error("duplicate link received: %s", link) - def stopped_session(self): + def stopped_session(self) -> None: # clear wireless edges for edge in self.wireless_edges.values(): edge.delete() @@ -351,7 +351,7 @@ class CanvasGraph(tk.Canvas): for edge in self.edges.values(): edge.reset() - def canvas_xy(self, event: tk.Event) -> [float, float]: + def canvas_xy(self, event: tk.Event) -> Tuple[float, float]: """ Convert window coordinate to canvas coordinate """ @@ -379,7 +379,7 @@ class CanvasGraph(tk.Canvas): return selected - def click_release(self, event: tk.Event): + def click_release(self, event: tk.Event) -> None: """ Draw a node or finish drawing an edge according to the current graph mode """ @@ -418,7 +418,7 @@ class CanvasGraph(tk.Canvas): self.mode = GraphMode.NODE self.selected = None - def handle_edge_release(self, _event: tk.Event): + def handle_edge_release(self, _event: tk.Event) -> None: edge = self.drawing_edge self.drawing_edge = None @@ -454,7 +454,7 @@ class CanvasGraph(tk.Canvas): node_dst.edges.add(edge) self.core.create_link(edge, node_src, node_dst) - def select_object(self, object_id: int, choose_multiple: bool = False): + def select_object(self, object_id: int, choose_multiple: bool = False) -> None: """ create a bounding box when a node is selected """ @@ -475,7 +475,7 @@ class CanvasGraph(tk.Canvas): selection_id = self.selection.pop(object_id) self.delete(selection_id) - def clear_selection(self): + def clear_selection(self) -> None: """ Clear current selection boxes. """ @@ -483,7 +483,7 @@ class CanvasGraph(tk.Canvas): self.delete(_id) self.selection.clear() - def move_selection(self, object_id: int, x_offset: float, y_offset: float): + def move_selection(self, object_id: int, x_offset: float, y_offset: float) -> None: select_id = self.selection.get(object_id) if select_id is not None: self.move(select_id, x_offset, y_offset) @@ -531,7 +531,7 @@ class CanvasGraph(tk.Canvas): self.core.deleted_graph_nodes(nodes) self.core.deleted_graph_edges(edges) - def delete_edge(self, edge: CanvasEdge): + def delete_edge(self, edge: CanvasEdge) -> None: edge.delete() del self.edges[edge.token] src_node = self.nodes[edge.src] @@ -550,7 +550,7 @@ class CanvasGraph(tk.Canvas): src_node.delete_antenna() self.core.deleted_graph_edges([edge]) - def zoom(self, event: tk.Event, factor: float = None): + def zoom(self, event: tk.Event, factor: float = None) -> None: if not factor: factor = ZOOM_IN if event.delta > 0 else ZOOM_OUT event.x, event.y = self.canvasx(event.x), self.canvasy(event.y) @@ -568,7 +568,7 @@ class CanvasGraph(tk.Canvas): if self.wallpaper: self.redraw_wallpaper() - def click_press(self, event: tk.Event): + def click_press(self, event: tk.Event) -> None: """ Start drawing an edge if mouse click is on a node """ @@ -630,7 +630,7 @@ class CanvasGraph(tk.Canvas): self.select_box = shape self.clear_selection() - def ctrl_click(self, event: tk.Event): + def ctrl_click(self, event: tk.Event) -> None: # update cursor location x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): @@ -648,7 +648,7 @@ class CanvasGraph(tk.Canvas): ): self.select_object(selected, choose_multiple=True) - def click_motion(self, event: tk.Event): + def click_motion(self, event: tk.Event) -> None: x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): if self.select_box: @@ -701,7 +701,7 @@ class CanvasGraph(tk.Canvas): if self.select_box and self.mode == GraphMode.SELECT: self.select_box.shape_motion(x, y) - def press_delete(self, _event: tk.Event): + def press_delete(self, _event: tk.Event) -> None: """ delete selected nodes and any data that relates to it """ @@ -711,7 +711,7 @@ class CanvasGraph(tk.Canvas): else: logging.debug("node deletion is disabled during runtime state") - def double_click(self, event: tk.Event): + def double_click(self, event: tk.Event) -> None: selected = self.get_selected(event) if selected is not None and selected in self.shapes: shape = self.shapes[selected] @@ -737,7 +737,7 @@ class CanvasGraph(tk.Canvas): self.core.canvas_nodes[core_node.id] = node self.nodes[node.id] = node - def width_and_height(self): + def width_and_height(self) -> Tuple[int, int]: """ retrieve canvas width and height in pixels """ @@ -753,8 +753,8 @@ class CanvasGraph(tk.Canvas): return image def draw_wallpaper( - self, image: ImageTk.PhotoImage, x: float = None, y: float = None - ): + self, image: PhotoImage, x: float = None, y: float = None + ) -> None: if x is None and y is None: x1, y1, x2, y2 = self.bbox(self.rect) x = (x1 + x2) / 2 @@ -762,7 +762,7 @@ class CanvasGraph(tk.Canvas): self.wallpaper_id = self.create_image((x, y), image=image, tags=tags.WALLPAPER) self.wallpaper_drawn = image - def wallpaper_upper_left(self): + def wallpaper_upper_left(self) -> None: self.delete(self.wallpaper_id) # create new scaled image, cropped if needed @@ -775,7 +775,7 @@ class CanvasGraph(tk.Canvas): if image.height > height: cropy = image.height cropped = image.crop((0, 0, cropx, cropy)) - image = ImageTk.PhotoImage(cropped) + image = PhotoImage(cropped) # draw on canvas x1, y1, _, _ = self.bbox(self.rect) @@ -783,7 +783,7 @@ class CanvasGraph(tk.Canvas): y = (cropy / 2) + y1 self.draw_wallpaper(image, x, y) - def wallpaper_center(self): + def wallpaper_center(self) -> None: """ place the image at the center of canvas """ @@ -803,26 +803,26 @@ class CanvasGraph(tk.Canvas): x2 = image.width - cropx y2 = image.height - cropy cropped = image.crop((x1, y1, x2, y2)) - image = ImageTk.PhotoImage(cropped) + image = PhotoImage(cropped) self.draw_wallpaper(image) - def wallpaper_scaled(self): + def wallpaper_scaled(self) -> None: """ scale image based on canvas dimension """ self.delete(self.wallpaper_id) canvas_w, canvas_h = self.width_and_height() image = self.wallpaper.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS) - image = ImageTk.PhotoImage(image) + image = PhotoImage(image) self.draw_wallpaper(image) - def resize_to_wallpaper(self): + def resize_to_wallpaper(self) -> None: self.delete(self.wallpaper_id) - image = ImageTk.PhotoImage(self.wallpaper) + image = PhotoImage(self.wallpaper) self.redraw_canvas((image.width(), image.height())) self.draw_wallpaper(image) - def redraw_canvas(self, dimensions: Tuple[int, int] = None): + def redraw_canvas(self, dimensions: Tuple[int, int] = None) -> None: logging.debug("redrawing canvas to dimensions: %s", dimensions) # reset scale and move back to original position @@ -843,7 +843,7 @@ class CanvasGraph(tk.Canvas): self.draw_grid() self.app.canvas.show_grid.click_handler() - def redraw_wallpaper(self): + def redraw_wallpaper(self) -> None: if self.adjust_to_dim.get(): logging.debug("drawing wallpaper to canvas dimensions") self.resize_to_wallpaper() @@ -864,7 +864,7 @@ class CanvasGraph(tk.Canvas): for tag in tags.ORGANIZE_TAGS: self.tag_raise(tag) - def set_wallpaper(self, filename: Optional[str]): + def set_wallpaper(self, filename: Optional[str]) -> None: logging.debug("setting wallpaper: %s", filename) if filename: img = Image.open(filename) @@ -880,7 +880,7 @@ class CanvasGraph(tk.Canvas): def is_selection_mode(self) -> bool: return self.mode == GraphMode.SELECT - def create_edge(self, source: CanvasNode, dest: CanvasNode): + def create_edge(self, source: CanvasNode, dest: CanvasNode) -> None: """ create an edge between source node and destination node """ @@ -894,7 +894,7 @@ class CanvasGraph(tk.Canvas): self.nodes[dest.id].edges.add(edge) self.core.create_link(edge, source, dest) - def copy(self): + def copy(self) -> None: if self.core.is_runtime(): logging.debug("copy is disabled during runtime state") return @@ -905,7 +905,7 @@ class CanvasGraph(tk.Canvas): canvas_node = self.nodes[node_id] self.to_copy.append(canvas_node) - def paste(self): + def paste(self) -> None: if self.core.is_runtime(): logging.debug("paste is disabled during runtime state") return @@ -972,11 +972,11 @@ class CanvasGraph(tk.Canvas): else: asym_iface1 = None if iface1_id: - asym_iface1 = core_pb2.Interface(id=iface1_id) + asym_iface1 = Interface(id=iface1_id) asym_iface2 = None if iface2_id: - asym_iface2 = core_pb2.Interface(id=iface2_id) - copy_edge.asymmetric_link = core_pb2.Link( + asym_iface2 = Interface(id=iface2_id) + copy_edge.asymmetric_link = Link( node1_id=copy_link.node2_id, node2_id=copy_link.node1_id, iface1=asym_iface1, @@ -990,7 +990,7 @@ class CanvasGraph(tk.Canvas): ) self.tag_raise(tags.NODE) - def scale_graph(self): + def scale_graph(self) -> None: for nid, canvas_node in self.nodes.items(): img = None if NodeUtils.is_custom( diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 3ba4b3f7..f936bc79 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -1,12 +1,14 @@ import functools import logging import tkinter as tk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple import grpc +from PIL.ImageTk import PhotoImage -from core.api.grpc import core_pb2 -from core.api.grpc.core_pb2 import NodeType +from core.api.grpc.common_pb2 import ConfigOption +from core.api.grpc.core_pb2 import Interface, Node, NodeType +from core.api.grpc.services_pb2 import NodeServiceData from core.gui import themes from core.gui.dialogs.emaneconfig import EmaneConfigDialog from core.gui.dialogs.mobilityconfig import MobilityConfigDialog @@ -15,36 +17,31 @@ from core.gui.dialogs.nodeconfigservice import NodeConfigServiceDialog from core.gui.dialogs.nodeservice import NodeServiceDialog from core.gui.dialogs.wlanconfig import WlanConfigDialog from core.gui.graph import tags -from core.gui.graph.edges import CanvasEdge +from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge from core.gui.graph.tooltip import CanvasTooltip from core.gui.images import ImageEnum from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils if TYPE_CHECKING: from core.gui.app import Application - from PIL.ImageTk import PhotoImage + from core.gui.graph.graph import CanvasGraph -NODE_TEXT_OFFSET = 5 +NODE_TEXT_OFFSET: int = 5 class CanvasNode: def __init__( - self, - app: "Application", - x: float, - y: float, - core_node: core_pb2.Node, - image: "PhotoImage", + self, app: "Application", x: float, y: float, core_node: Node, image: PhotoImage ): - self.app = app - self.canvas = app.canvas - self.image = image - self.core_node = core_node - self.id = self.canvas.create_image( + self.app: "Application" = app + self.canvas: "CanvasGraph" = app.canvas + self.image: PhotoImage = image + self.core_node: Node = core_node + self.id: int = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE ) label_y = self._get_label_y() - self.text_id = self.canvas.create_text( + self.text_id: int = self.canvas.create_text( x, label_y, text=self.core_node.name, @@ -53,21 +50,21 @@ class CanvasNode: fill="#0000CD", state=self.canvas.show_node_labels.state(), ) - self.tooltip = CanvasTooltip(self.canvas) - self.edges = set() - self.ifaces = {} - self.wireless_edges = set() - self.antennas = [] - self.antenna_images = {} + self.tooltip: CanvasTooltip = CanvasTooltip(self.canvas) + self.edges: Set[CanvasEdge] = set() + self.ifaces: Dict[int, Interface] = {} + self.wireless_edges: Set[CanvasWirelessEdge] = set() + self.antennas: List[int] = [] + self.antenna_images: Dict[int, PhotoImage] = {} # possible configurations - self.emane_model_configs = {} - self.wlan_config = {} - self.mobility_config = {} - self.service_configs = {} - self.service_file_configs = {} - self.config_service_configs = {} + self.emane_model_configs: Dict[Tuple[str, Optional[int]], ConfigOption] = {} + self.wlan_config: Dict[str, ConfigOption] = {} + self.mobility_config: Dict[str, ConfigOption] = {} + self.service_configs: Dict[str, NodeServiceData] = {} + self.service_file_configs: Dict[str, Dict[str, str]] = {} + self.config_service_configs: Dict[str, Any] = {} self.setup_bindings() - self.context = tk.Menu(self.canvas) + self.context: tk.Menu = tk.Menu(self.canvas) themes.style_menu(self.context) def next_iface_id(self) -> int: @@ -76,19 +73,19 @@ class CanvasNode: i += 1 return i - def setup_bindings(self): + def setup_bindings(self) -> None: self.canvas.tag_bind(self.id, "", self.double_click) self.canvas.tag_bind(self.id, "", self.on_enter) self.canvas.tag_bind(self.id, "", self.on_leave) self.canvas.tag_bind(self.id, "", self.show_context) - def delete(self): + def delete(self) -> None: logging.debug("Delete canvas node for %s", self.core_node) self.canvas.delete(self.id) self.canvas.delete(self.text_id) self.delete_antennas() - def add_antenna(self): + def add_antenna(self) -> None: x, y = self.canvas.coords(self.id) offset = len(self.antennas) * 8 * self.app.app_scale img = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE) @@ -102,7 +99,7 @@ class CanvasNode: self.antennas.append(antenna_id) self.antenna_images[antenna_id] = img - def delete_antenna(self): + def delete_antenna(self) -> None: """ delete one antenna """ @@ -112,7 +109,7 @@ class CanvasNode: self.canvas.delete(antenna_id) self.antenna_images.pop(antenna_id, None) - def delete_antennas(self): + def delete_antennas(self) -> None: """ delete all antennas """ @@ -122,30 +119,30 @@ class CanvasNode: self.antennas.clear() self.antenna_images.clear() - def redraw(self): + def redraw(self) -> None: self.canvas.itemconfig(self.id, image=self.image) self.canvas.itemconfig(self.text_id, text=self.core_node.name) for edge in self.edges: edge.redraw() - def _get_label_y(self): + def _get_label_y(self) -> int: image_box = self.canvas.bbox(self.id) return image_box[3] + NODE_TEXT_OFFSET - def scale_text(self): + def scale_text(self) -> None: text_bound = self.canvas.bbox(self.text_id) prev_y = (text_bound[3] + text_bound[1]) / 2 new_y = self._get_label_y() self.canvas.move(self.text_id, 0, new_y - prev_y) - def move(self, x: int, y: int): + def move(self, x: int, y: int) -> None: x, y = self.canvas.get_scaled_coords(x, y) current_x, current_y = self.canvas.coords(self.id) x_offset = x - current_x y_offset = y - current_y self.motion(x_offset, y_offset, update=False) - def motion(self, x_offset: int, y_offset: int, update: bool = True): + def motion(self, x_offset: float, y_offset: float, update: bool = True) -> None: original_position = self.canvas.coords(self.id) self.canvas.move(self.id, x_offset, y_offset) pos = self.canvas.coords(self.id) @@ -177,7 +174,7 @@ class CanvasNode: if self.app.core.is_runtime() and update: self.app.core.edit_node(self.core_node) - def on_enter(self, event: tk.Event): + def on_enter(self, event: tk.Event) -> None: if self.app.core.is_runtime() and self.app.core.observer: self.tooltip.text.set("waiting...") self.tooltip.on_enter(event) @@ -187,10 +184,10 @@ class CanvasNode: except grpc.RpcError as e: self.app.show_grpc_exception("Observer Error", e) - def on_leave(self, event: tk.Event): + def on_leave(self, event: tk.Event) -> None: self.tooltip.on_leave(event) - def double_click(self, event: tk.Event): + def double_click(self, event: tk.Event) -> None: if self.app.core.is_runtime(): self.canvas.core.launch_terminal(self.core_node.id) else: @@ -270,37 +267,37 @@ class CanvasNode: self.canvas.selection[self.id] = self self.canvas.copy() - def show_config(self): + def show_config(self) -> None: dialog = NodeConfigDialog(self.app, self) dialog.show() - def show_wlan_config(self): + def show_wlan_config(self) -> None: dialog = WlanConfigDialog(self.app, self) if not dialog.has_error: dialog.show() - def show_mobility_config(self): + def show_mobility_config(self) -> None: dialog = MobilityConfigDialog(self.app, self) if not dialog.has_error: dialog.show() - def show_mobility_player(self): + def show_mobility_player(self) -> None: mobility_player = self.app.core.mobility_players[self.core_node.id] mobility_player.show() - def show_emane_config(self): + def show_emane_config(self) -> None: dialog = EmaneConfigDialog(self.app, self) dialog.show() - def show_services(self): + def show_services(self) -> None: dialog = NodeServiceDialog(self.app, self) dialog.show() - def show_config_services(self): + def show_config_services(self) -> None: dialog = NodeConfigServiceDialog(self.app, self) dialog.show() - def has_emane_link(self, iface_id: int) -> core_pb2.Node: + def has_emane_link(self, iface_id: int) -> Node: result = None for edge in self.edges: if self.id == edge.src: @@ -317,14 +314,14 @@ class CanvasNode: break return result - def wireless_link_selected(self): + def wireless_link_selected(self) -> None: nodes = [x for x in self.canvas.selection if x in self.canvas.nodes] for node_id in nodes: canvas_node = self.canvas.nodes[node_id] self.canvas.create_edge(self, canvas_node) self.canvas.clear_selection() - def scale_antennas(self): + def scale_antennas(self) -> None: for i in range(len(self.antennas)): antenna_id = self.antennas[i] image = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE) diff --git a/daemon/core/gui/graph/shape.py b/daemon/core/gui/graph/shape.py index 70f67d14..36298655 100644 --- a/daemon/core/gui/graph/shape.py +++ b/daemon/core/gui/graph/shape.py @@ -1,5 +1,5 @@ import logging -from typing import TYPE_CHECKING, Dict, List, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Union from core.gui.dialogs.shapemod import ShapeDialog from core.gui.graph import tags @@ -23,17 +23,17 @@ class AnnotationData: bold: bool = False, italic: bool = False, underline: bool = False, - ): - self.text = text - self.font = font - self.font_size = font_size - self.text_color = text_color - self.fill_color = fill_color - self.border_color = border_color - self.border_width = border_width - self.bold = bold - self.italic = italic - self.underline = underline + ) -> None: + self.text: str = text + self.font: str = font + self.font_size: int = font_size + self.text_color: str = text_color + self.fill_color: str = fill_color + self.border_color: str = border_color + self.border_width: int = border_width + self.bold: bool = bold + self.italic: bool = italic + self.underline: bool = underline class Shape: @@ -47,29 +47,29 @@ class Shape: x2: float = None, y2: float = None, data: AnnotationData = None, - ): - self.app = app - self.canvas = canvas - self.shape_type = shape_type - self.id = None - self.text_id = None - self.x1 = x1 - self.y1 = y1 + ) -> None: + self.app: "Application" = app + self.canvas: "CanvasGraph" = canvas + self.shape_type: ShapeType = shape_type + self.id: Optional[int] = None + self.text_id: Optional[int] = None + self.x1: float = x1 + self.y1: float = y1 if x2 is None: x2 = x1 - self.x2 = x2 + self.x2: float = x2 if y2 is None: y2 = y1 - self.y2 = y2 + self.y2: float = y2 if data is None: - self.created = False - self.shape_data = AnnotationData() + self.created: bool = False + self.shape_data: AnnotationData = AnnotationData() else: - self.created = True + self.created: bool = True self.shape_data = data self.draw() - def draw(self): + def draw(self) -> None: if self.created: dash = None else: @@ -127,7 +127,7 @@ class Shape: font.append("underline") return font - def draw_shape_text(self): + def draw_shape_text(self) -> None: if self.shape_data.text: x = (self.x1 + self.x2) / 2 y = self.y1 + 1.5 * self.shape_data.font_size @@ -142,18 +142,18 @@ class Shape: state=self.canvas.show_annotations.state(), ) - def shape_motion(self, x1: float, y1: float): + def shape_motion(self, x1: float, y1: float) -> None: self.canvas.coords(self.id, self.x1, self.y1, x1, y1) - def shape_complete(self, x: float, y: float): + def shape_complete(self, x: float, y: float) -> None: self.canvas.organize() s = ShapeDialog(self.app, self) s.show() - def disappear(self): + def disappear(self) -> None: self.canvas.delete(self.id) - def motion(self, x_offset: float, y_offset: float): + def motion(self, x_offset: float, y_offset: float) -> None: original_position = self.canvas.coords(self.id) self.canvas.move(self.id, x_offset, y_offset) coords = self.canvas.coords(self.id) @@ -166,7 +166,7 @@ class Shape: if self.text_id is not None: self.canvas.move(self.text_id, x_offset, y_offset) - def delete(self): + def delete(self) -> None: logging.debug("Delete shape, id(%s)", self.id) self.canvas.delete(self.id) self.canvas.delete(self.text_id) diff --git a/daemon/core/gui/graph/shapeutils.py b/daemon/core/gui/graph/shapeutils.py index ce2b7f96..2b62a46c 100644 --- a/daemon/core/gui/graph/shapeutils.py +++ b/daemon/core/gui/graph/shapeutils.py @@ -1,4 +1,5 @@ import enum +from typing import Set class ShapeType(enum.Enum): @@ -8,7 +9,7 @@ class ShapeType(enum.Enum): TEXT = "text" -SHAPES = {ShapeType.OVAL, ShapeType.RECTANGLE} +SHAPES: Set[ShapeType] = {ShapeType.OVAL, ShapeType.RECTANGLE} def is_draw_shape(shape_type: ShapeType) -> bool: diff --git a/daemon/core/gui/graph/tags.py b/daemon/core/gui/graph/tags.py index c0721193..b7b35517 100644 --- a/daemon/core/gui/graph/tags.py +++ b/daemon/core/gui/graph/tags.py @@ -1,17 +1,19 @@ -ANNOTATION = "annotation" -GRIDLINE = "gridline" -SHAPE = "shape" -SHAPE_TEXT = "shapetext" -EDGE = "edge" -LINK_LABEL = "linklabel" -WIRELESS_EDGE = "wireless" -ANTENNA = "antenna" -NODE_LABEL = "nodename" -NODE = "node" -WALLPAPER = "wallpaper" -SELECTION = "selectednodes" -MARKER = "marker" -ORGANIZE_TAGS = [ +from typing import List + +ANNOTATION: str = "annotation" +GRIDLINE: str = "gridline" +SHAPE: str = "shape" +SHAPE_TEXT: str = "shapetext" +EDGE: str = "edge" +LINK_LABEL: str = "linklabel" +WIRELESS_EDGE: str = "wireless" +ANTENNA: str = "antenna" +NODE_LABEL: str = "nodename" +NODE: str = "node" +WALLPAPER: str = "wallpaper" +SELECTION: str = "selectednodes" +MARKER: str = "marker" +ORGANIZE_TAGS: List[str] = [ WALLPAPER, GRIDLINE, SHAPE, @@ -25,7 +27,7 @@ ORGANIZE_TAGS = [ SELECTION, MARKER, ] -RESET_TAGS = [ +RESET_TAGS: List[str] = [ EDGE, NODE, NODE_LABEL, diff --git a/daemon/core/gui/graph/tooltip.py b/daemon/core/gui/graph/tooltip.py index a2193901..6e4aa62f 100644 --- a/daemon/core/gui/graph/tooltip.py +++ b/daemon/core/gui/graph/tooltip.py @@ -1,6 +1,6 @@ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, Tuple from core.gui.themes import Styles @@ -27,39 +27,45 @@ class CanvasTooltip: self, canvas: "CanvasGraph", *, - pad=(5, 3, 5, 3), + pad: Tuple[int, int, int, int] = (5, 3, 5, 3), waittime: int = 400, wraplength: int = 600 - ): + ) -> None: # in miliseconds, originally 500 - self.waittime = waittime + self.waittime: int = waittime # in pixels, originally 180 - self.wraplength = wraplength - self.canvas = canvas - self.text = tk.StringVar() - self.pad = pad - self.id = None - self.tw = None + self.wraplength: int = wraplength + self.canvas: "CanvasGraph" = canvas + self.text: tk.StringVar = tk.StringVar() + self.pad: Tuple[int, int, int, int] = pad + self.id: Optional[str] = None + self.tw: Optional[tk.Toplevel] = None - def on_enter(self, event: tk.Event = None): + def on_enter(self, event: tk.Event = None) -> None: self.schedule() - def on_leave(self, event: tk.Event = None): + def on_leave(self, event: tk.Event = None) -> None: self.unschedule() self.hide() - def schedule(self): + def schedule(self) -> None: self.unschedule() self.id = self.canvas.after(self.waittime, self.show) - def unschedule(self): + def unschedule(self) -> None: id_ = self.id self.id = None if id_: self.canvas.after_cancel(id_) - def show(self, event: tk.Event = None): - def tip_pos_calculator(canvas, label, *, tip_delta=(10, 5), pad=(5, 3, 5, 3)): + def show(self, event: tk.Event = None) -> None: + def tip_pos_calculator( + canvas: "CanvasGraph", + label: ttk.Label, + *, + tip_delta: Tuple[int, int] = (10, 5), + pad: Tuple[int, int, int, int] = (5, 3, 5, 3) + ): c = canvas s_width, s_height = c.winfo_screenwidth(), c.winfo_screenheight() width, height = ( @@ -108,7 +114,7 @@ class CanvasTooltip: x, y = tip_pos_calculator(canvas, label, pad=pad) self.tw.wm_geometry("+%d+%d" % (x, y)) - def hide(self): + def hide(self) -> None: if self.tw: self.tw.destroy() self.tw = None From 527d34e3746d781908abe6a256b90db89dd77d9a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 22 Jun 2020 11:04:33 -0700 Subject: [PATCH 0388/1131] pygui: added type hinting to class variables for core.gui.dialogs --- daemon/core/gui/coreclient.py | 4 +- daemon/core/gui/dialogs/about.py | 4 +- daemon/core/gui/dialogs/alerts.py | 18 +-- daemon/core/gui/dialogs/canvassizeandscale.py | 49 +++--- daemon/core/gui/dialogs/canvaswallpaper.py | 43 ++--- daemon/core/gui/dialogs/colorpicker.py | 52 +++--- .../core/gui/dialogs/configserviceconfig.py | 113 +++++++------ daemon/core/gui/dialogs/copyserviceconfig.py | 15 +- daemon/core/gui/dialogs/customnodes.py | 72 +++++---- daemon/core/gui/dialogs/dialog.py | 12 +- daemon/core/gui/dialogs/emaneconfig.py | 78 ++++----- daemon/core/gui/dialogs/emaneinstall.py | 4 +- daemon/core/gui/dialogs/error.py | 8 +- daemon/core/gui/dialogs/executepython.py | 20 +-- daemon/core/gui/dialogs/find.py | 6 +- daemon/core/gui/dialogs/hooks.py | 40 ++--- daemon/core/gui/dialogs/ipdialog.py | 18 +-- daemon/core/gui/dialogs/linkconfig.py | 57 +++---- daemon/core/gui/dialogs/macdialog.py | 2 +- daemon/core/gui/dialogs/mobilityconfig.py | 29 ++-- daemon/core/gui/dialogs/mobilityplayer.py | 70 ++++---- daemon/core/gui/dialogs/nodeconfig.py | 50 +++--- daemon/core/gui/dialogs/nodeconfigservice.py | 32 ++-- daemon/core/gui/dialogs/nodeservice.py | 28 ++-- daemon/core/gui/dialogs/observers.py | 38 ++--- daemon/core/gui/dialogs/preferences.py | 28 ++-- daemon/core/gui/dialogs/runtool.py | 10 +- daemon/core/gui/dialogs/servers.py | 42 ++--- daemon/core/gui/dialogs/serviceconfig.py | 149 +++++++++--------- daemon/core/gui/dialogs/sessionoptions.py | 17 +- daemon/core/gui/dialogs/sessions.py | 19 +-- daemon/core/gui/dialogs/shapemod.py | 63 ++++---- daemon/core/gui/dialogs/throughput.py | 31 ++-- daemon/core/gui/dialogs/wlanconfig.py | 46 +++--- daemon/core/gui/graph/node.py | 6 +- daemon/core/gui/menubar.py | 2 +- daemon/core/gui/nodeutils.py | 2 +- 37 files changed, 664 insertions(+), 613 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 5e1bf4c2..39ee486a 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -38,7 +38,7 @@ from core.api.grpc.mobility_pb2 import MobilityConfig from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig, ServiceFileConfig from core.api.grpc.wlan_pb2 import WlanConfig from core.gui import appconfig -from core.gui.appconfig import CoreServer +from core.gui.appconfig import CoreServer, Observer from core.gui.dialogs.emaneinstall import EmaneInstallDialog from core.gui.dialogs.error import ErrorDialog from core.gui.dialogs.mobilityplayer import MobilityPlayer @@ -75,7 +75,7 @@ class CoreClient: # loaded configuration data self.servers: Dict[str, CoreServer] = {} self.custom_nodes: Dict[str, NodeDraw] = {} - self.custom_observers: Dict[str, str] = {} + self.custom_observers: Dict[str, Observer] = {} self.read_config() # helpers diff --git a/daemon/core/gui/dialogs/about.py b/daemon/core/gui/dialogs/about.py index 2e649169..fa96e218 100644 --- a/daemon/core/gui/dialogs/about.py +++ b/daemon/core/gui/dialogs/about.py @@ -35,11 +35,11 @@ THE POSSIBILITY OF SUCH DAMAGE.\ class AboutDialog(Dialog): - def __init__(self, app: "Application"): + def __init__(self, app: "Application") -> None: super().__init__(app, "About CORE") self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) diff --git a/daemon/core/gui/dialogs/alerts.py b/daemon/core/gui/dialogs/alerts.py index a0c3e68b..00ef1e8c 100644 --- a/daemon/core/gui/dialogs/alerts.py +++ b/daemon/core/gui/dialogs/alerts.py @@ -3,9 +3,9 @@ check engine light """ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, Optional -from core.api.grpc.core_pb2 import ExceptionLevel +from core.api.grpc.core_pb2 import ExceptionEvent, ExceptionLevel from core.gui.dialogs.dialog import Dialog from core.gui.themes import PADX, PADY from core.gui.widgets import CodeText @@ -15,14 +15,14 @@ if TYPE_CHECKING: class AlertsDialog(Dialog): - def __init__(self, app: "Application"): + def __init__(self, app: "Application") -> None: super().__init__(app, "Alerts") - self.tree = None - self.codetext = None - self.alarm_map = {} + self.tree: Optional[ttk.Treeview] = None + self.codetext: Optional[CodeText] = None + self.alarm_map: Dict[int, ExceptionEvent] = {} self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) self.top.rowconfigure(1, weight=1) @@ -97,13 +97,13 @@ class AlertsDialog(Dialog): button = ttk.Button(frame, text="Close", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def reset_alerts(self): + def reset_alerts(self) -> None: self.codetext.text.delete("1.0", tk.END) for item in self.tree.get_children(): self.tree.delete(item) self.app.statusbar.core_alarms.clear() - def click_select(self, event: tk.Event): + def click_select(self, event: tk.Event) -> None: current = self.tree.selection()[0] alarm = self.alarm_map[current] self.codetext.text.config(state=tk.NORMAL) diff --git a/daemon/core/gui/dialogs/canvassizeandscale.py b/daemon/core/gui/dialogs/canvassizeandscale.py index 6a63a1ae..b93bd920 100644 --- a/daemon/core/gui/dialogs/canvassizeandscale.py +++ b/daemon/core/gui/dialogs/canvassizeandscale.py @@ -7,38 +7,43 @@ from typing import TYPE_CHECKING from core.gui import validation from core.gui.dialogs.dialog import Dialog +from core.gui.graph.graph import CanvasGraph from core.gui.themes import FRAME_PAD, PADX, PADY if TYPE_CHECKING: from core.gui.app import Application -PIXEL_SCALE = 100 +PIXEL_SCALE: int = 100 class SizeAndScaleDialog(Dialog): - def __init__(self, app: "Application"): + def __init__(self, app: "Application") -> None: """ create an instance for size and scale object """ super().__init__(app, "Canvas Size and Scale") - self.canvas = self.app.canvas - self.section_font = font.Font(weight="bold") + self.canvas: CanvasGraph = self.app.canvas + self.section_font: font.Font = font.Font(weight="bold") width, height = self.canvas.current_dimensions - self.pixel_width = tk.IntVar(value=width) - self.pixel_height = tk.IntVar(value=height) + self.pixel_width: tk.IntVar = tk.IntVar(value=width) + self.pixel_height: tk.IntVar = tk.IntVar(value=height) location = self.app.core.location - self.x = tk.DoubleVar(value=location.x) - self.y = tk.DoubleVar(value=location.y) - self.lat = tk.DoubleVar(value=location.lat) - self.lon = tk.DoubleVar(value=location.lon) - self.alt = tk.DoubleVar(value=location.alt) - self.scale = tk.DoubleVar(value=location.scale) - self.meters_width = tk.IntVar(value=width / PIXEL_SCALE * location.scale) - self.meters_height = tk.IntVar(value=height / PIXEL_SCALE * location.scale) - self.save_default = tk.BooleanVar(value=False) + self.x: tk.DoubleVar = tk.DoubleVar(value=location.x) + self.y: tk.DoubleVar = tk.DoubleVar(value=location.y) + self.lat: tk.DoubleVar = tk.DoubleVar(value=location.lat) + self.lon: tk.DoubleVar = tk.DoubleVar(value=location.lon) + self.alt: tk.DoubleVar = tk.DoubleVar(value=location.alt) + self.scale: tk.DoubleVar = tk.DoubleVar(value=location.scale) + self.meters_width: tk.IntVar = tk.IntVar( + value=width / PIXEL_SCALE * location.scale + ) + self.meters_height: tk.IntVar = tk.IntVar( + value=height / PIXEL_SCALE * location.scale + ) + self.save_default: tk.BooleanVar = tk.BooleanVar(value=False) self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.draw_size() self.draw_scale() @@ -47,7 +52,7 @@ class SizeAndScaleDialog(Dialog): self.draw_spacer() self.draw_buttons() - def draw_size(self): + def draw_size(self) -> None: label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -84,7 +89,7 @@ class SizeAndScaleDialog(Dialog): label = ttk.Label(frame, text="Meters") label.grid(row=0, column=4, sticky="w") - def draw_scale(self): + def draw_scale(self) -> None: label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -99,7 +104,7 @@ class SizeAndScaleDialog(Dialog): label = ttk.Label(frame, text="Meters") label.grid(row=0, column=2, sticky="w") - def draw_reference_point(self): + def draw_reference_point(self) -> None: label_frame = ttk.Labelframe( self.top, text="Reference Point", padding=FRAME_PAD ) @@ -150,13 +155,13 @@ class SizeAndScaleDialog(Dialog): entry = validation.FloatEntry(frame, textvariable=self.alt) entry.grid(row=0, column=5, sticky="ew") - def draw_save_as_default(self): + def draw_save_as_default(self) -> None: button = ttk.Checkbutton( self.top, text="Save as default?", variable=self.save_default ) button.grid(sticky="w", pady=PADY) - def draw_buttons(self): + def draw_buttons(self) -> None: frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) @@ -168,7 +173,7 @@ class SizeAndScaleDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def click_apply(self): + def click_apply(self) -> None: width, height = self.pixel_width.get(), self.pixel_height.get() self.canvas.redraw_canvas((width, height)) if self.canvas.wallpaper: diff --git a/daemon/core/gui/dialogs/canvaswallpaper.py b/daemon/core/gui/dialogs/canvaswallpaper.py index 5e8460be..8a1e71d8 100644 --- a/daemon/core/gui/dialogs/canvaswallpaper.py +++ b/daemon/core/gui/dialogs/canvaswallpaper.py @@ -4,10 +4,11 @@ set wallpaper import logging import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List, Optional from core.gui.appconfig import BACKGROUNDS_PATH from core.gui.dialogs.dialog import Dialog +from core.gui.graph.graph import CanvasGraph from core.gui.images import Images from core.gui.themes import PADX, PADY from core.gui.widgets import image_chooser @@ -17,20 +18,22 @@ if TYPE_CHECKING: class CanvasWallpaperDialog(Dialog): - def __init__(self, app: "Application"): + def __init__(self, app: "Application") -> None: """ create an instance of CanvasWallpaper object """ super().__init__(app, "Canvas Background") - self.canvas = self.app.canvas - self.scale_option = tk.IntVar(value=self.canvas.scale_option.get()) - self.adjust_to_dim = tk.BooleanVar(value=self.canvas.adjust_to_dim.get()) - self.filename = tk.StringVar(value=self.canvas.wallpaper_file) - self.image_label = None - self.options = [] + self.canvas: CanvasGraph = self.app.canvas + self.scale_option: tk.IntVar = tk.IntVar(value=self.canvas.scale_option.get()) + self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar( + value=self.canvas.adjust_to_dim.get() + ) + self.filename: tk.StringVar = tk.StringVar(value=self.canvas.wallpaper_file) + self.image_label: Optional[ttk.Label] = None + self.options: List[ttk.Radiobutton] = [] self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.draw_image() self.draw_image_label() @@ -40,19 +43,19 @@ class CanvasWallpaperDialog(Dialog): self.draw_spacer() self.draw_buttons() - def draw_image(self): + def draw_image(self) -> None: self.image_label = ttk.Label( self.top, text="(image preview)", width=32, anchor=tk.CENTER ) self.image_label.grid(pady=PADY) - def draw_image_label(self): + def draw_image_label(self) -> None: label = ttk.Label(self.top, text="Image filename: ") label.grid(sticky="ew") if self.filename.get(): self.draw_preview() - def draw_image_selection(self): + def draw_image_selection(self) -> None: frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=2) frame.columnconfigure(1, weight=1) @@ -69,7 +72,7 @@ class CanvasWallpaperDialog(Dialog): button = ttk.Button(frame, text="Clear", command=self.click_clear) button.grid(row=0, column=2, sticky="ew") - def draw_options(self): + def draw_options(self) -> None: frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) @@ -101,7 +104,7 @@ class CanvasWallpaperDialog(Dialog): button.grid(row=0, column=3, sticky="ew") self.options.append(button) - def draw_additional_options(self): + def draw_additional_options(self) -> None: checkbutton = ttk.Checkbutton( self.top, text="Adjust canvas size to image dimensions", @@ -110,7 +113,7 @@ class CanvasWallpaperDialog(Dialog): ) checkbutton.grid(sticky="ew", padx=PADX) - def draw_buttons(self): + def draw_buttons(self) -> None: frame = ttk.Frame(self.top) frame.grid(pady=PADY, sticky="ew") frame.columnconfigure(0, weight=1) @@ -122,18 +125,18 @@ class CanvasWallpaperDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def click_open_image(self): + def click_open_image(self) -> None: filename = image_chooser(self, BACKGROUNDS_PATH) if filename: self.filename.set(filename) self.draw_preview() - def draw_preview(self): + def draw_preview(self) -> None: image = Images.create(self.filename.get(), 250, 135) self.image_label.config(image=image) self.image_label.image = image - def click_clear(self): + def click_clear(self) -> None: """ delete like shown in image link entry if there is any """ @@ -143,7 +146,7 @@ class CanvasWallpaperDialog(Dialog): self.image_label.config(image="", width=32) self.image_label.image = None - def click_adjust_canvas(self): + def click_adjust_canvas(self) -> None: # deselect all radio buttons and grey them out if self.adjust_to_dim.get(): self.scale_option.set(0) @@ -155,7 +158,7 @@ class CanvasWallpaperDialog(Dialog): for option in self.options: option.config(state=tk.NORMAL) - def click_apply(self): + def click_apply(self) -> None: self.canvas.scale_option.set(self.scale_option.get()) self.canvas.adjust_to_dim.set(self.adjust_to_dim.get()) self.canvas.show_grid.click_handler() diff --git a/daemon/core/gui/dialogs/colorpicker.py b/daemon/core/gui/dialogs/colorpicker.py index b1968cd4..908b8acb 100644 --- a/daemon/core/gui/dialogs/colorpicker.py +++ b/daemon/core/gui/dialogs/colorpicker.py @@ -3,7 +3,7 @@ custom color picker """ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, Tuple from core.gui import validation from core.gui.dialogs.dialog import Dialog @@ -18,23 +18,23 @@ class ColorPickerDialog(Dialog): self, master: tk.BaseWidget, app: "Application", initcolor: str = "#000000" ): super().__init__(app, "Color Picker", master=master) - self.red_entry = None - self.blue_entry = None - self.green_entry = None - self.hex_entry = None - self.red_label = None - self.green_label = None - self.blue_label = None - self.display = None - self.color = initcolor + self.red_entry: Optional[validation.RgbEntry] = None + self.blue_entry: Optional[validation.RgbEntry] = None + self.green_entry: Optional[validation.RgbEntry] = None + self.hex_entry: Optional[validation.HexEntry] = None + self.red_label: Optional[ttk.Label] = None + self.green_label: Optional[ttk.Label] = None + self.blue_label: Optional[ttk.Label] = None + self.display: Optional[tk.Frame] = None + self.color: str = initcolor red, green, blue = self.get_rgb(initcolor) - self.red = tk.IntVar(value=red) - self.blue = tk.IntVar(value=blue) - self.green = tk.IntVar(value=green) - self.hex = tk.StringVar(value=initcolor) - self.red_scale = tk.IntVar(value=red) - self.green_scale = tk.IntVar(value=green) - self.blue_scale = tk.IntVar(value=blue) + self.red: tk.IntVar = tk.IntVar(value=red) + self.blue: tk.IntVar = tk.IntVar(value=blue) + self.green: tk.IntVar = tk.IntVar(value=green) + self.hex: tk.StringVar = tk.StringVar(value=initcolor) + self.red_scale: tk.IntVar = tk.IntVar(value=red) + self.green_scale: tk.IntVar = tk.IntVar(value=green) + self.blue_scale: tk.IntVar = tk.IntVar(value=blue) self.draw() self.set_bindings() @@ -42,7 +42,7 @@ class ColorPickerDialog(Dialog): self.show() return self.color - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(3, weight=1) @@ -136,7 +136,7 @@ class ColorPickerDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def set_bindings(self): + def set_bindings(self) -> None: self.red_entry.bind("", lambda x: self.current_focus("rgb")) self.green_entry.bind("", lambda x: self.current_focus("rgb")) self.blue_entry.bind("", lambda x: self.current_focus("rgb")) @@ -146,7 +146,7 @@ class ColorPickerDialog(Dialog): self.blue.trace_add("write", self.update_color) self.hex.trace_add("write", self.update_color) - def button_ok(self): + def button_ok(self) -> None: self.color = self.hex.get() self.destroy() @@ -159,10 +159,10 @@ class ColorPickerDialog(Dialog): green = self.green_entry.get() return "#%02x%02x%02x" % (int(red), int(green), int(blue)) - def current_focus(self, focus: str): + def current_focus(self, focus: str) -> None: self.focus = focus - def update_color(self, arg1=None, arg2=None, arg3=None): + def update_color(self, arg1=None, arg2=None, arg3=None) -> None: if self.focus == "rgb": red = self.red_entry.get() blue = self.blue_entry.get() @@ -184,7 +184,7 @@ class ColorPickerDialog(Dialog): self.display.config(background=hex_code) self.set_label(str(red), str(green), str(blue)) - def scale_callback(self, var: tk.IntVar, color_var: tk.IntVar): + def scale_callback(self, var: tk.IntVar, color_var: tk.IntVar) -> None: color_var.set(var.get()) self.focus = "rgb" self.update_color() @@ -194,17 +194,17 @@ class ColorPickerDialog(Dialog): self.green_scale.set(green) self.blue_scale.set(blue) - def set_entry(self, red: int, green: int, blue: int): + def set_entry(self, red: int, green: int, blue: int) -> None: self.red.set(red) self.green.set(green) self.blue.set(blue) - def set_label(self, red: str, green: str, blue: str): + def set_label(self, red: str, green: str, blue: str) -> None: self.red_label.configure(background="#%02x%02x%02x" % (int(red), 0, 0)) self.green_label.configure(background="#%02x%02x%02x" % (0, int(green), 0)) self.blue_label.configure(background="#%02x%02x%02x" % (0, 0, int(blue))) - def get_rgb(self, hex_code: str) -> [int, int, int]: + def get_rgb(self, hex_code: str) -> Tuple[int, int, int]: """ convert a valid hex code to RGB values """ diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py index 42041a8e..c2d42ee4 100644 --- a/daemon/core/gui/dialogs/configserviceconfig.py +++ b/daemon/core/gui/dialogs/configserviceconfig.py @@ -4,10 +4,11 @@ Service configuration dialog import logging import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, Dict, List, Optional, Set import grpc +from core.api.grpc.common_pb2 import ConfigOption from core.api.grpc.services_pb2 import ServiceValidationMode from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY @@ -16,6 +17,7 @@ from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll if TYPE_CHECKING: from core.gui.app import Application from core.gui.graph.node import CanvasNode + from core.gui.coreclient import CoreClient class ConfigServiceConfigDialog(Dialog): @@ -26,56 +28,53 @@ class ConfigServiceConfigDialog(Dialog): service_name: str, canvas_node: "CanvasNode", node_id: int, - ): + ) -> None: title = f"{service_name} Config Service" super().__init__(app, title, master=master) - self.core = app.core - self.canvas_node = canvas_node - self.node_id = node_id - self.service_name = service_name - self.radiovar = tk.IntVar() + self.core: "CoreClient" = app.core + self.canvas_node: "CanvasNode" = canvas_node + self.node_id: int = node_id + self.service_name: str = service_name + self.radiovar: tk.IntVar = tk.IntVar() self.radiovar.set(2) - self.directories = [] - self.templates = [] - self.dependencies = [] - self.executables = [] - self.startup_commands = [] - self.validation_commands = [] - self.shutdown_commands = [] - self.default_startup = [] - self.default_validate = [] - self.default_shutdown = [] - self.validation_mode = None - self.validation_time = None - self.validation_period = tk.StringVar() - self.modes = [] - self.mode_configs = {} - - self.notebook = None - self.templates_combobox = None - self.modes_combobox = None - self.startup_commands_listbox = None - self.shutdown_commands_listbox = None - self.validate_commands_listbox = None - self.validation_time_entry = None - self.validation_mode_entry = None - self.template_text = None - self.validation_period_entry = None - self.original_service_files = {} - self.temp_service_files = {} - self.modified_files = set() - self.config_frame = None - self.default_config = None - self.config = None - - self.has_error = False + self.directories: List[str] = [] + self.templates: List[str] = [] + self.dependencies: List[str] = [] + self.executables: List[str] = [] + self.startup_commands: List[str] = [] + self.validation_commands: List[str] = [] + self.shutdown_commands: List[str] = [] + self.default_startup: List[str] = [] + self.default_validate: List[str] = [] + self.default_shutdown: List[str] = [] + self.validation_mode: Optional[ServiceValidationMode] = None + self.validation_time: Optional[int] = None + self.validation_period: tk.StringVar = tk.StringVar() + self.modes: List[str] = [] + self.mode_configs: Dict[str, str] = {} + self.notebook: Optional[ttk.Notebook] = None + self.templates_combobox: Optional[ttk.Combobox] = None + self.modes_combobox: Optional[ttk.Combobox] = None + self.startup_commands_listbox: Optional[tk.Listbox] = None + self.shutdown_commands_listbox: Optional[tk.Listbox] = None + self.validate_commands_listbox: Optional[tk.Listbox] = None + self.validation_time_entry: Optional[ttk.Entry] = None + self.validation_mode_entry: Optional[ttk.Entry] = None + self.template_text: Optional[CodeText] = None + self.validation_period_entry: Optional[ttk.Entry] = None + self.original_service_files: Dict[str, str] = {} + self.temp_service_files: Dict[str, str] = {} + self.modified_files: Set[str] = set() + self.config_frame: Optional[ConfigFrame] = None + self.default_config: Dict[str, str] = {} + self.config: Dict[str, ConfigOption] = {} + self.has_error: bool = False self.load() - if not self.has_error: self.draw() - def load(self): + def load(self) -> None: try: self.core.create_nodes_and_links() service = self.core.config_services[self.service_name] @@ -116,7 +115,7 @@ class ConfigServiceConfigDialog(Dialog): self.app.show_grpc_exception("Get Config Service Error", e) self.has_error = True - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) @@ -130,7 +129,7 @@ class ConfigServiceConfigDialog(Dialog): self.draw_tab_validation() self.draw_buttons() - def draw_tab_files(self): + def draw_tab_files(self) -> None: tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab.grid(sticky="nsew") tab.columnconfigure(0, weight=1) @@ -174,7 +173,7 @@ class ConfigServiceConfigDialog(Dialog): ) self.template_text.text.bind("", self.update_template_file_data) - def draw_tab_config(self): + def draw_tab_config(self) -> None: tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab.grid(sticky="nsew") tab.columnconfigure(0, weight=1) @@ -198,7 +197,7 @@ class ConfigServiceConfigDialog(Dialog): self.config_frame.grid(sticky="nsew", pady=PADY) tab.rowconfigure(self.config_frame.grid_info()["row"], weight=1) - def draw_tab_startstop(self): + def draw_tab_startstop(self) -> None: tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab.grid(sticky="nsew") tab.columnconfigure(0, weight=1) @@ -239,7 +238,7 @@ class ConfigServiceConfigDialog(Dialog): elif i == 2: self.validate_commands_listbox = listbox_scroll.listbox - def draw_tab_validation(self): + def draw_tab_validation(self) -> None: tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab.grid(sticky="ew") tab.columnconfigure(0, weight=1) @@ -298,7 +297,7 @@ class ConfigServiceConfigDialog(Dialog): for dependency in self.dependencies: listbox_scroll.listbox.insert("end", dependency) - def draw_buttons(self): + def draw_buttons(self) -> None: frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(4): @@ -312,7 +311,7 @@ class ConfigServiceConfigDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=3, sticky="ew") - def click_apply(self): + def click_apply(self) -> None: current_listbox = self.master.current.listbox if not self.is_custom(): self.canvas_node.config_service_configs.pop(self.service_name, None) @@ -333,18 +332,18 @@ class ConfigServiceConfigDialog(Dialog): current_listbox.itemconfig(all_current.index(self.service_name), bg="green") self.destroy() - def handle_template_changed(self, event: tk.Event): + def handle_template_changed(self, event: tk.Event) -> None: template = self.templates_combobox.get() self.template_text.text.delete(1.0, "end") self.template_text.text.insert("end", self.temp_service_files[template]) - def handle_mode_changed(self, event: tk.Event): + def handle_mode_changed(self, event: tk.Event) -> None: mode = self.modes_combobox.get() config = self.mode_configs[mode] logging.info("mode config: %s", config) self.config_frame.set_values(config) - def update_template_file_data(self, event: tk.Event): + def update_template_file_data(self, event: tk.Event) -> None: scrolledtext = event.widget template = self.templates_combobox.get() self.temp_service_files[template] = scrolledtext.get(1.0, "end") @@ -353,7 +352,7 @@ class ConfigServiceConfigDialog(Dialog): else: self.modified_files.discard(template) - def is_custom(self): + def is_custom(self) -> bool: has_custom_templates = len(self.modified_files) > 0 has_custom_config = False if self.config_frame: @@ -361,7 +360,7 @@ class ConfigServiceConfigDialog(Dialog): has_custom_config = self.default_config != current return has_custom_templates or has_custom_config - def click_defaults(self): + def click_defaults(self) -> None: self.canvas_node.config_service_configs.pop(self.service_name, None) logging.info( "cleared config service config: %s", self.canvas_node.config_service_configs @@ -374,12 +373,12 @@ class ConfigServiceConfigDialog(Dialog): logging.info("resetting defaults: %s", self.default_config) self.config_frame.set_values(self.default_config) - def click_copy(self): + def click_copy(self) -> None: pass def append_commands( self, commands: List[str], listbox: tk.Listbox, to_add: List[str] - ): + ) -> None: for cmd in to_add: commands.append(cmd) listbox.insert(tk.END, cmd) diff --git a/daemon/core/gui/dialogs/copyserviceconfig.py b/daemon/core/gui/dialogs/copyserviceconfig.py index ff75a59a..35559cb9 100644 --- a/daemon/core/gui/dialogs/copyserviceconfig.py +++ b/daemon/core/gui/dialogs/copyserviceconfig.py @@ -15,17 +15,16 @@ if TYPE_CHECKING: class CopyServiceConfigDialog(Dialog): - def __init__(self, master: tk.BaseWidget, app: "Application", node_id: int): + def __init__(self, master: tk.BaseWidget, app: "Application", node_id: int) -> None: super().__init__(app, f"Copy services to node {node_id}", master=master) self.parent = master self.node_id = node_id self.service_configs = app.core.service_configs self.file_configs = app.core.file_configs - self.tree = None self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.tree = ttk.Treeview(self.top) self.tree.grid(row=0, column=0, sticky="ew", padx=PADX) @@ -88,7 +87,7 @@ class CopyServiceConfigDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=2, sticky="ew", padx=PADX) - def click_copy(self): + def click_copy(self) -> None: selected = self.tree.selection() if selected: item = self.tree.item(selected[0]) @@ -127,7 +126,7 @@ class CopyServiceConfigDialog(Dialog): ) self.destroy() - def click_view(self): + def click_view(self) -> None: selected = self.tree.selection() data = "" if selected: @@ -159,7 +158,7 @@ class CopyServiceConfigDialog(Dialog): ) dialog.show() - def get_node_service(self, selected: Tuple[str]) -> [int, str]: + def get_node_service(self, selected: Tuple[str]) -> Tuple[int, str]: service_tree_id = self.tree.parent(selected[0]) service_name = self.tree.item(service_tree_id)["text"] node_tree_id = self.tree.parent(service_tree_id) @@ -175,14 +174,14 @@ class ViewConfigDialog(Dialog): node_id: int, data: str, filename: str = None, - ): + ) -> None: super().__init__(app, f"n{node_id} config data", master=master) self.data = data self.service_data = None self.filepath = tk.StringVar(value=f"/tmp/services.tmp-n{node_id}-{filename}") self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) frame = ttk.Frame(self.top, padding=FRAME_PAD) frame.columnconfigure(0, weight=1) diff --git a/daemon/core/gui/dialogs/customnodes.py b/daemon/core/gui/dialogs/customnodes.py index 56012780..df3bafa7 100644 --- a/daemon/core/gui/dialogs/customnodes.py +++ b/daemon/core/gui/dialogs/customnodes.py @@ -2,7 +2,9 @@ import logging import tkinter as tk from pathlib import Path from tkinter import ttk -from typing import TYPE_CHECKING, Set +from typing import TYPE_CHECKING, Optional, Set + +from PIL.ImageTk import PhotoImage from core.gui import nodeutils from core.gui.appconfig import ICONS_PATH, CustomNode @@ -19,15 +21,15 @@ if TYPE_CHECKING: class ServicesSelectDialog(Dialog): def __init__( self, master: tk.BaseWidget, app: "Application", current_services: Set[str] - ): + ) -> None: super().__init__(app, "Node Services", master=master) - self.groups = None - self.services = None - self.current = None - self.current_services = set(current_services) + self.groups: Optional[ListboxScroll] = None + self.services: Optional[CheckboxList] = None + self.current: Optional[ListboxScroll] = None + self.current_services: Set[str] = current_services self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) @@ -77,7 +79,7 @@ class ServicesSelectDialog(Dialog): # trigger group change self.groups.listbox.event_generate("<>") - def handle_group_change(self, event: tk.Event): + def handle_group_change(self, event: tk.Event) -> None: selection = self.groups.listbox.curselection() if selection: index = selection[0] @@ -87,7 +89,7 @@ class ServicesSelectDialog(Dialog): checked = name in self.current_services self.services.add(name, checked) - def service_clicked(self, name: str, var: tk.BooleanVar): + def service_clicked(self, name: str, var: tk.BooleanVar) -> None: if var.get() and name not in self.current_services: self.current_services.add(name) elif not var.get() and name in self.current_services: @@ -96,34 +98,34 @@ class ServicesSelectDialog(Dialog): for name in sorted(self.current_services): self.current.listbox.insert(tk.END, name) - def click_cancel(self): + def click_cancel(self) -> None: self.current_services = None self.destroy() class CustomNodesDialog(Dialog): - def __init__(self, app: "Application"): + def __init__(self, app: "Application") -> None: super().__init__(app, "Custom Nodes") - self.edit_button = None - self.delete_button = None - self.nodes_list = None - self.name = tk.StringVar() - self.image_button = None - self.image = None - self.image_file = None - self.services = set() - self.selected = None - self.selected_index = None + self.edit_button: Optional[ttk.Button] = None + self.delete_button: Optional[ttk.Button] = None + self.nodes_list: Optional[ListboxScroll] = None + self.name: tk.StringVar = tk.StringVar() + self.image_button: Optional[ttk.Button] = None + self.image: Optional[PhotoImage] = None + self.image_file: Optional[str] = None + self.services: Set[str] = set() + self.selected: Optional[str] = None + self.selected_index: Optional[int] = None self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) self.draw_node_config() self.draw_node_buttons() self.draw_buttons() - def draw_node_config(self): + def draw_node_config(self) -> None: frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD) frame.grid(sticky="nsew", pady=PADY) frame.columnconfigure(0, weight=1) @@ -147,7 +149,7 @@ class CustomNodesDialog(Dialog): button = ttk.Button(frame, text="Services", command=self.click_services) button.grid(sticky="ew") - def draw_node_buttons(self): + def draw_node_buttons(self) -> None: frame = ttk.Frame(self.top) frame.grid(sticky="ew", pady=PADY) for i in range(3): @@ -166,7 +168,7 @@ class CustomNodesDialog(Dialog): ) self.delete_button.grid(row=0, column=2, sticky="ew") - def draw_buttons(self): + def draw_buttons(self) -> None: frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): @@ -178,14 +180,14 @@ class CustomNodesDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def reset_values(self): + def reset_values(self) -> None: self.name.set("") self.image = None self.image_file = None self.services = set() self.image_button.config(image="") - def click_icon(self): + def click_icon(self) -> None: file_path = image_chooser(self, ICONS_PATH) if file_path: image = Images.create(file_path, nodeutils.ICON_SIZE) @@ -193,24 +195,26 @@ class CustomNodesDialog(Dialog): self.image_file = file_path self.image_button.config(image=self.image) - def click_services(self): + def click_services(self) -> None: dialog = ServicesSelectDialog(self, self.app, self.services) dialog.show() if dialog.current_services is not None: self.services.clear() self.services.update(dialog.current_services) - def click_save(self): + def click_save(self) -> None: self.app.guiconfig.nodes.clear() for name in self.app.core.custom_nodes: node_draw = self.app.core.custom_nodes[name] - custom_node = CustomNode(name, node_draw.image_file, node_draw.services) + custom_node = CustomNode( + name, node_draw.image_file, list(node_draw.services) + ) self.app.guiconfig.nodes.append(custom_node) logging.info("saving custom nodes: %s", self.app.guiconfig.nodes) self.app.save_config() self.destroy() - def click_create(self): + def click_create(self) -> None: name = self.name.get() if name not in self.app.core.custom_nodes: image_file = Path(self.image_file).stem @@ -226,7 +230,7 @@ class CustomNodesDialog(Dialog): self.nodes_list.listbox.insert(tk.END, name) self.reset_values() - def click_edit(self): + def click_edit(self) -> None: name = self.name.get() if self.selected: previous_name = self.selected @@ -247,7 +251,7 @@ class CustomNodesDialog(Dialog): self.nodes_list.listbox.insert(self.selected_index, name) self.nodes_list.listbox.selection_set(self.selected_index) - def click_delete(self): + def click_delete(self) -> None: if self.selected and self.selected in self.app.core.custom_nodes: self.nodes_list.listbox.delete(self.selected_index) del self.app.core.custom_nodes[self.selected] @@ -255,7 +259,7 @@ class CustomNodesDialog(Dialog): self.nodes_list.listbox.selection_clear(0, tk.END) self.nodes_list.listbox.event_generate("<>") - def handle_node_select(self, event: tk.Event): + def handle_node_select(self, event: tk.Event) -> None: selection = self.nodes_list.listbox.curselection() if selection: self.selected_index = selection[0] diff --git a/daemon/core/gui/dialogs/dialog.py b/daemon/core/gui/dialogs/dialog.py index f3742c50..962170e7 100644 --- a/daemon/core/gui/dialogs/dialog.py +++ b/daemon/core/gui/dialogs/dialog.py @@ -16,23 +16,23 @@ class Dialog(tk.Toplevel): title: str, modal: bool = True, master: tk.BaseWidget = None, - ): + ) -> None: if master is None: master = app super().__init__(master) self.withdraw() - self.app = app - self.modal = modal + self.app: "Application" = app + self.modal: bool = modal self.title(title) self.protocol("WM_DELETE_WINDOW", self.destroy) image = Images.get(ImageEnum.CORE, 16) self.tk.call("wm", "iconphoto", self._w, image) self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) - self.top = ttk.Frame(self, padding=DIALOG_PAD) + self.top: ttk.Frame = ttk.Frame(self, padding=DIALOG_PAD) self.top.grid(sticky="nsew") - def show(self): + def show(self) -> None: self.transient(self.master) self.focus_force() self.update() @@ -42,7 +42,7 @@ class Dialog(tk.Toplevel): self.grab_set() self.wait_window() - def draw_spacer(self, row: int = None): + def draw_spacer(self, row: int = None) -> None: frame = ttk.Frame(self.top) frame.grid(row=row, sticky="nsew") frame.rowconfigure(0, weight=1) diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index 8f7ca089..df6c6125 100644 --- a/daemon/core/gui/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -4,10 +4,12 @@ emane configuration import tkinter as tk import webbrowser from tkinter import ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, List, Optional import grpc +from core.api.grpc.common_pb2 import ConfigOption +from core.api.grpc.core_pb2 import Node from core.gui.dialogs.dialog import Dialog from core.gui.images import ImageEnum, Images from core.gui.themes import PADX, PADY @@ -19,12 +21,12 @@ if TYPE_CHECKING: class GlobalEmaneDialog(Dialog): - def __init__(self, master: tk.BaseWidget, app: "Application"): + def __init__(self, master: tk.BaseWidget, app: "Application") -> None: super().__init__(app, "EMANE Configuration", master=master) - self.config_frame = None + self.config_frame: Optional[ConfigFrame] = None self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) self.config_frame = ConfigFrame(self.top, self.app, self.app.core.emane_config) @@ -33,7 +35,7 @@ class GlobalEmaneDialog(Dialog): self.draw_spacer() self.draw_buttons() - def draw_buttons(self): + def draw_buttons(self) -> None: frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): @@ -44,7 +46,7 @@ class GlobalEmaneDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def click_apply(self): + def click_apply(self) -> None: self.config_frame.parse_config() self.destroy() @@ -57,31 +59,32 @@ class EmaneModelDialog(Dialog): canvas_node: "CanvasNode", model: str, iface_id: int = None, - ): + ) -> None: super().__init__( app, f"{canvas_node.core_node.name} {model} Configuration", master=master ) - self.canvas_node = canvas_node - self.node = canvas_node.core_node - self.model = f"emane_{model}" - self.iface_id = iface_id - self.config_frame = None - self.has_error = False + self.canvas_node: "CanvasNode" = canvas_node + self.node: Node = canvas_node.core_node + self.model: str = f"emane_{model}" + self.iface_id: int = iface_id + self.config_frame: Optional[ConfigFrame] = None + self.has_error: bool = False try: - self.config = self.canvas_node.emane_model_configs.get( + config = self.canvas_node.emane_model_configs.get( (self.model, self.iface_id) ) - if not self.config: - self.config = self.app.core.get_emane_model_config( + if not config: + config = self.app.core.get_emane_model_config( self.node.id, self.model, self.iface_id ) + self.config: Dict[str, ConfigOption] = config self.draw() except grpc.RpcError as e: self.app.show_grpc_exception("Get EMANE Config Error", e) - self.has_error = True + self.has_error: bool = True self.destroy() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) self.config_frame = ConfigFrame(self.top, self.app, self.config) @@ -90,7 +93,7 @@ class EmaneModelDialog(Dialog): self.draw_spacer() self.draw_buttons() - def draw_buttons(self): + def draw_buttons(self) -> None: frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): @@ -101,7 +104,7 @@ class EmaneModelDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def click_apply(self): + def click_apply(self) -> None: self.config_frame.parse_config() key = (self.model, self.iface_id) self.canvas_node.emane_model_configs[key] = self.config @@ -109,18 +112,21 @@ class EmaneModelDialog(Dialog): class EmaneConfigDialog(Dialog): - def __init__(self, app: "Application", canvas_node: "CanvasNode"): + def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None: super().__init__(app, f"{canvas_node.core_node.name} EMANE Configuration") - self.canvas_node = canvas_node - self.node = canvas_node.core_node - self.radiovar = tk.IntVar() + self.canvas_node: "CanvasNode" = canvas_node + self.node: Node = canvas_node.core_node + self.radiovar: tk.IntVar = tk.IntVar() self.radiovar.set(1) - self.emane_models = [x.split("_")[1] for x in self.app.core.emane_models] - self.emane_model = tk.StringVar(value=self.node.emane.split("_")[1]) - self.emane_model_button = None + self.emane_models: List[str] = [ + x.split("_")[1] for x in self.app.core.emane_models + ] + model = self.node.emane.split("_")[1] + self.emane_model: tk.StringVar = tk.StringVar(value=model) + self.emane_model_button: Optional[ttk.Button] = None self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.draw_emane_configuration() self.draw_emane_models() @@ -128,7 +134,7 @@ class EmaneConfigDialog(Dialog): self.draw_spacer() self.draw_apply_and_cancel() - def draw_emane_configuration(self): + def draw_emane_configuration(self) -> None: """ draw the main frame for emane configuration """ @@ -153,7 +159,7 @@ class EmaneConfigDialog(Dialog): button.image = image button.grid(sticky="ew", pady=PADY) - def draw_emane_models(self): + def draw_emane_models(self) -> None: """ create a combobox that has all the known emane models """ @@ -174,7 +180,7 @@ class EmaneConfigDialog(Dialog): combobox.grid(row=0, column=1, sticky="ew") combobox.bind("<>", self.emane_model_change) - def draw_emane_buttons(self): + def draw_emane_buttons(self) -> None: frame = ttk.Frame(self.top) frame.grid(sticky="ew", pady=PADY) for i in range(2): @@ -202,7 +208,7 @@ class EmaneConfigDialog(Dialog): button.image = image button.grid(row=0, column=1, sticky="ew") - def draw_apply_and_cancel(self): + def draw_apply_and_cancel(self) -> None: frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): @@ -214,11 +220,11 @@ class EmaneConfigDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def click_emane_config(self): + def click_emane_config(self) -> None: dialog = GlobalEmaneDialog(self, self.app) dialog.show() - def click_model_config(self): + def click_model_config(self) -> None: """ draw emane model configuration """ @@ -227,13 +233,13 @@ class EmaneConfigDialog(Dialog): if not dialog.has_error: dialog.show() - def emane_model_change(self, event: tk.Event): + def emane_model_change(self, event: tk.Event) -> None: """ update emane model options button """ model_name = self.emane_model.get() self.emane_model_button.config(text=f"{model_name} options") - def click_apply(self): + def click_apply(self) -> None: self.node.emane = f"emane_{self.emane_model.get()}" self.destroy() diff --git a/daemon/core/gui/dialogs/emaneinstall.py b/daemon/core/gui/dialogs/emaneinstall.py index 93cf2ac4..3ad9396b 100644 --- a/daemon/core/gui/dialogs/emaneinstall.py +++ b/daemon/core/gui/dialogs/emaneinstall.py @@ -10,7 +10,7 @@ class EmaneInstallDialog(Dialog): super().__init__(app, "EMANE Error") self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) label = ttk.Label(self.top, text="EMANE needs to be installed!") label.grid(sticky="ew", pady=PADY) @@ -21,5 +21,5 @@ class EmaneInstallDialog(Dialog): button = ttk.Button(self.top, text="Close", command=self.destroy) button.grid(sticky="ew") - def click_doc(self): + def click_doc(self) -> None: webbrowser.open_new("https://coreemu.github.io/core/emane.html") diff --git a/daemon/core/gui/dialogs/error.py b/daemon/core/gui/dialogs/error.py index 5ff1dbc5..7fb81077 100644 --- a/daemon/core/gui/dialogs/error.py +++ b/daemon/core/gui/dialogs/error.py @@ -1,5 +1,5 @@ from tkinter import ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from core.gui.dialogs.dialog import Dialog from core.gui.images import ImageEnum, Images @@ -13,9 +13,9 @@ if TYPE_CHECKING: class ErrorDialog(Dialog): def __init__(self, app: "Application", title: str, details: str) -> None: super().__init__(app, "CORE Exception") - self.title = title - self.details = details - self.error_message = None + self.title: str = title + self.details: str = details + self.error_message: Optional[CodeText] = None self.draw() def draw(self) -> None: diff --git a/daemon/core/gui/dialogs/executepython.py b/daemon/core/gui/dialogs/executepython.py index dd60c778..a4516df1 100644 --- a/daemon/core/gui/dialogs/executepython.py +++ b/daemon/core/gui/dialogs/executepython.py @@ -1,7 +1,7 @@ import logging import tkinter as tk from tkinter import filedialog, ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from core.gui.appconfig import SCRIPT_PATH from core.gui.dialogs.dialog import Dialog @@ -12,15 +12,15 @@ if TYPE_CHECKING: class ExecutePythonDialog(Dialog): - def __init__(self, app: "Application"): + def __init__(self, app: "Application") -> None: super().__init__(app, "Execute Python Script") - self.with_options = tk.IntVar(value=0) - self.options = tk.StringVar(value="") - self.option_entry = None - self.file_entry = None + self.with_options: tk.IntVar = tk.IntVar(value=0) + self.options: tk.StringVar = tk.StringVar(value="") + self.option_entry: Optional[ttk.Entry] = None + self.file_entry: Optional[ttk.Entry] = None self.draw() - def draw(self): + def draw(self) -> None: i = 0 frame = ttk.Frame(self.top, padding=FRAME_PAD) frame.columnconfigure(0, weight=1) @@ -63,13 +63,13 @@ class ExecutePythonDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew", padx=PADX) - def add_options(self): + def add_options(self) -> None: if self.with_options.get(): self.option_entry.configure(state="normal") else: self.option_entry.configure(state="disabled") - def select_file(self): + def select_file(self) -> None: file = filedialog.askopenfilename( parent=self.top, initialdir=str(SCRIPT_PATH), @@ -80,7 +80,7 @@ class ExecutePythonDialog(Dialog): self.file_entry.delete(0, "end") self.file_entry.insert("end", file) - def script_execute(self): + def script_execute(self) -> None: file = self.file_entry.get() options = self.option_entry.get() logging.info("Execute %s with options %s", file, options) diff --git a/daemon/core/gui/dialogs/find.py b/daemon/core/gui/dialogs/find.py index 25da4b19..328f673e 100644 --- a/daemon/core/gui/dialogs/find.py +++ b/daemon/core/gui/dialogs/find.py @@ -1,7 +1,7 @@ import logging import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY @@ -13,8 +13,8 @@ if TYPE_CHECKING: class FindDialog(Dialog): def __init__(self, app: "Application") -> None: super().__init__(app, "Find", modal=False) - self.find_text = tk.StringVar(value="") - self.tree = None + self.find_text: tk.StringVar = tk.StringVar(value="") + self.tree: Optional[ttk.Treeview] = None self.draw() self.protocol("WM_DELETE_WINDOW", self.close_dialog) self.bind("", self.find_node) diff --git a/daemon/core/gui/dialogs/hooks.py b/daemon/core/gui/dialogs/hooks.py index 5895a2e1..08d666ba 100644 --- a/daemon/core/gui/dialogs/hooks.py +++ b/daemon/core/gui/dialogs/hooks.py @@ -1,6 +1,6 @@ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from core.api.grpc import core_pb2 from core.gui.dialogs.dialog import Dialog @@ -12,15 +12,15 @@ if TYPE_CHECKING: class HookDialog(Dialog): - def __init__(self, master: tk.BaseWidget, app: "Application"): + def __init__(self, master: tk.BaseWidget, app: "Application") -> None: super().__init__(app, "Hook", master=master) - self.name = tk.StringVar() - self.codetext = None - self.hook = core_pb2.Hook() - self.state = tk.StringVar() + self.name: tk.StringVar = tk.StringVar() + self.codetext: Optional[CodeText] = None + self.hook: core_pb2.Hook = core_pb2.Hook() + self.state: tk.StringVar = tk.StringVar() self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(1, weight=1) @@ -66,11 +66,11 @@ class HookDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=1, sticky="ew") - def state_change(self, event: tk.Event): + def state_change(self, event: tk.Event) -> None: state_name = self.state.get() self.name.set(f"{state_name.lower()}_hook.sh") - def set(self, hook: core_pb2.Hook): + def set(self, hook: core_pb2.Hook) -> None: self.hook = hook self.name.set(hook.file) self.codetext.text.delete(1.0, tk.END) @@ -78,7 +78,7 @@ class HookDialog(Dialog): state_name = core_pb2.SessionState.Enum.Name(hook.state) self.state.set(state_name) - def save(self): + def save(self) -> None: data = self.codetext.text.get("1.0", tk.END).strip() state_value = core_pb2.SessionState.Enum.Value(self.state.get()) self.hook.file = self.name.get() @@ -88,15 +88,15 @@ class HookDialog(Dialog): class HooksDialog(Dialog): - def __init__(self, app: "Application"): + def __init__(self, app: "Application") -> None: super().__init__(app, "Hooks") - self.listbox = None - self.edit_button = None - self.delete_button = None - self.selected = None + self.listbox: Optional[tk.Listbox] = None + self.edit_button: Optional[ttk.Button] = None + self.delete_button: Optional[ttk.Button] = None + self.selected: Optional[str] = None self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) @@ -124,7 +124,7 @@ class HooksDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=3, sticky="ew") - def click_create(self): + def click_create(self) -> None: dialog = HookDialog(self, self.app) dialog.show() hook = dialog.hook @@ -132,19 +132,19 @@ class HooksDialog(Dialog): self.app.core.hooks[hook.file] = hook self.listbox.insert(tk.END, hook.file) - def click_edit(self): + def click_edit(self) -> None: hook = self.app.core.hooks[self.selected] dialog = HookDialog(self, self.app) dialog.set(hook) dialog.show() - def click_delete(self): + def click_delete(self) -> None: del self.app.core.hooks[self.selected] self.listbox.delete(tk.ANCHOR) self.edit_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED) - def select(self, event: tk.Event): + def select(self, event: tk.Event) -> None: if self.listbox.curselection(): index = self.listbox.curselection()[0] self.selected = self.listbox.get(index) diff --git a/daemon/core/gui/dialogs/ipdialog.py b/daemon/core/gui/dialogs/ipdialog.py index d31dcdff..351bfffc 100644 --- a/daemon/core/gui/dialogs/ipdialog.py +++ b/daemon/core/gui/dialogs/ipdialog.py @@ -1,6 +1,6 @@ import tkinter as tk from tkinter import messagebox, ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List, Optional import netaddr @@ -15,14 +15,14 @@ if TYPE_CHECKING: class IpConfigDialog(Dialog): def __init__(self, app: "Application") -> None: super().__init__(app, "IP Configuration") - self.ip4 = self.app.guiconfig.ips.ip4 - self.ip6 = self.app.guiconfig.ips.ip6 - self.ip4s = self.app.guiconfig.ips.ip4s - self.ip6s = self.app.guiconfig.ips.ip6s - self.ip4_entry = None - self.ip4_listbox = None - self.ip6_entry = None - self.ip6_listbox = None + self.ip4: str = self.app.guiconfig.ips.ip4 + self.ip6: str = self.app.guiconfig.ips.ip6 + self.ip4s: List[str] = self.app.guiconfig.ips.ip4s + self.ip6s: List[str] = self.app.guiconfig.ips.ip6s + self.ip4_entry: Optional[ttk.Entry] = None + self.ip4_listbox: Optional[ListboxScroll] = None + self.ip6_entry: Optional[ttk.Entry] = None + self.ip6_listbox: Optional[ListboxScroll] = None self.draw() def draw(self) -> None: diff --git a/daemon/core/gui/dialogs/linkconfig.py b/daemon/core/gui/dialogs/linkconfig.py index adf8156f..b7c618a3 100644 --- a/daemon/core/gui/dialogs/linkconfig.py +++ b/daemon/core/gui/dialogs/linkconfig.py @@ -3,7 +3,7 @@ link configuration """ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Optional from core.api.grpc import core_pb2 from core.gui import validation @@ -16,7 +16,7 @@ if TYPE_CHECKING: from core.gui.graph.graph import CanvasEdge -def get_int(var: tk.StringVar) -> Union[int, None]: +def get_int(var: tk.StringVar) -> Optional[int]: value = var.get() if value != "": return int(value) @@ -24,7 +24,7 @@ def get_int(var: tk.StringVar) -> Union[int, None]: return None -def get_float(var: tk.StringVar) -> Union[float, None]: +def get_float(var: tk.StringVar) -> Optional[float]: value = var.get() if value != "": return float(value) @@ -33,38 +33,39 @@ def get_float(var: tk.StringVar) -> Union[float, None]: class LinkConfigurationDialog(Dialog): - def __init__(self, app: "Application", edge: "CanvasEdge"): + def __init__(self, app: "Application", edge: "CanvasEdge") -> None: super().__init__(app, "Link Configuration") - self.edge = edge - self.is_symmetric = edge.link.options.unidirectional is False + self.edge: "CanvasEdge" = edge + self.is_symmetric: bool = edge.link.options.unidirectional is False if self.is_symmetric: - self.symmetry_var = tk.StringVar(value=">>") + symmetry_var = tk.StringVar(value=">>") else: - self.symmetry_var = tk.StringVar(value="<<") + symmetry_var = tk.StringVar(value="<<") + self.symmetry_var: tk.StringVar = symmetry_var - self.bandwidth = tk.StringVar() - self.delay = tk.StringVar() - self.jitter = tk.StringVar() - self.loss = tk.StringVar() - self.duplicate = tk.StringVar() + self.bandwidth: tk.StringVar = tk.StringVar() + self.delay: tk.StringVar = tk.StringVar() + self.jitter: tk.StringVar = tk.StringVar() + self.loss: tk.StringVar = tk.StringVar() + self.duplicate: tk.StringVar = tk.StringVar() - self.down_bandwidth = tk.StringVar() - self.down_delay = tk.StringVar() - self.down_jitter = tk.StringVar() - self.down_loss = tk.StringVar() - self.down_duplicate = 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.color = tk.StringVar(value="#000000") - self.color_button = None - self.width = tk.DoubleVar() + self.color: tk.StringVar = tk.StringVar(value="#000000") + self.color_button: Optional[tk.Button] = None + self.width: tk.DoubleVar = tk.DoubleVar() self.load_link_config() - self.symmetric_frame = None - self.asymmetric_frame = None + self.symmetric_frame: Optional[ttk.Frame] = None + self.asymmetric_frame: Optional[ttk.Frame] = None self.draw() - def draw(self): + 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 @@ -207,13 +208,13 @@ class LinkConfigurationDialog(Dialog): return frame - def click_color(self): + def click_color(self) -> None: dialog = ColorPickerDialog(self, self.app, self.color.get()) color = dialog.askcolor() self.color.set(color) self.color_button.config(background=color) - def click_apply(self): + 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()) link = self.edge.link @@ -288,7 +289,7 @@ class LinkConfigurationDialog(Dialog): self.destroy() - def change_symmetry(self): + def change_symmetry(self) -> None: if self.is_symmetric: self.is_symmetric = False self.symmetry_var.set("<<") @@ -304,7 +305,7 @@ class LinkConfigurationDialog(Dialog): self.asymmetric_frame.grid_forget() self.symmetric_frame.grid(row=2, column=0) - def load_link_config(self): + def load_link_config(self) -> None: """ populate link config to the table """ diff --git a/daemon/core/gui/dialogs/macdialog.py b/daemon/core/gui/dialogs/macdialog.py index 46414cf9..4d89439b 100644 --- a/daemon/core/gui/dialogs/macdialog.py +++ b/daemon/core/gui/dialogs/macdialog.py @@ -15,7 +15,7 @@ class MacConfigDialog(Dialog): def __init__(self, app: "Application") -> None: super().__init__(app, "MAC Configuration") mac = self.app.guiconfig.mac - self.mac_var = tk.StringVar(value=mac) + self.mac_var: tk.StringVar = tk.StringVar(value=mac) self.draw() def draw(self) -> None: diff --git a/daemon/core/gui/dialogs/mobilityconfig.py b/daemon/core/gui/dialogs/mobilityconfig.py index dced5e44..daaf9ea5 100644 --- a/daemon/core/gui/dialogs/mobilityconfig.py +++ b/daemon/core/gui/dialogs/mobilityconfig.py @@ -2,10 +2,12 @@ mobility configuration """ from tkinter import ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, Optional import grpc +from core.api.grpc.common_pb2 import ConfigOption +from core.api.grpc.core_pb2 import Node from core.gui.dialogs.dialog import Dialog from core.gui.themes import PADX, PADY from core.gui.widgets import ConfigFrame @@ -16,23 +18,24 @@ if TYPE_CHECKING: class MobilityConfigDialog(Dialog): - def __init__(self, app: "Application", canvas_node: "CanvasNode"): + def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None: super().__init__(app, f"{canvas_node.core_node.name} Mobility Configuration") - self.canvas_node = canvas_node - self.node = canvas_node.core_node - self.config_frame = None - self.has_error = False + self.canvas_node: "CanvasNode" = canvas_node + self.node: Node = canvas_node.core_node + self.config_frame: Optional[ConfigFrame] = None + self.has_error: bool = False try: - self.config = self.canvas_node.mobility_config - if not self.config: - self.config = self.app.core.get_mobility_config(self.node.id) + config = self.canvas_node.mobility_config + if not config: + config = self.app.core.get_mobility_config(self.node.id) + self.config: Dict[str, ConfigOption] = config self.draw() except grpc.RpcError as e: self.app.show_grpc_exception("Get Mobility Config Error", e) - self.has_error = True + self.has_error: bool = True self.destroy() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) self.config_frame = ConfigFrame(self.top, self.app, self.config) @@ -40,7 +43,7 @@ class MobilityConfigDialog(Dialog): self.config_frame.grid(sticky="nsew", pady=PADY) self.draw_apply_buttons() - def draw_apply_buttons(self): + def draw_apply_buttons(self) -> None: frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): @@ -52,7 +55,7 @@ class MobilityConfigDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def click_apply(self): + def click_apply(self) -> None: self.config_frame.parse_config() self.canvas_node.mobility_config = self.config self.destroy() diff --git a/daemon/core/gui/dialogs/mobilityplayer.py b/daemon/core/gui/dialogs/mobilityplayer.py index b4801bcf..e6ef62ea 100644 --- a/daemon/core/gui/dialogs/mobilityplayer.py +++ b/daemon/core/gui/dialogs/mobilityplayer.py @@ -1,9 +1,11 @@ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, Optional import grpc +from core.api.grpc.common_pb2 import ConfigOption +from core.api.grpc.core_pb2 import Node from core.api.grpc.mobility_pb2 import MobilityAction from core.gui.dialogs.dialog import Dialog from core.gui.images import ImageEnum @@ -13,18 +15,23 @@ if TYPE_CHECKING: from core.gui.app import Application from core.gui.graph.node import CanvasNode -ICON_SIZE = 16 +ICON_SIZE: int = 16 class MobilityPlayer: - def __init__(self, app: "Application", canvas_node: "CanvasNode", config): - self.app = app - self.canvas_node = canvas_node - self.config = config - self.dialog = None - self.state = None + def __init__( + self, + app: "Application", + canvas_node: "CanvasNode", + config: Dict[str, ConfigOption], + ) -> None: + self.app: "Application" = app + self.canvas_node: "CanvasNode" = canvas_node + self.config: Dict[str, ConfigOption] = config + self.dialog: Optional[MobilityPlayerDialog] = None + self.state: Optional[MobilityAction] = None - def show(self): + def show(self) -> None: if self.dialog: self.dialog.destroy() self.dialog = MobilityPlayerDialog(self.app, self.canvas_node, self.config) @@ -37,44 +44,49 @@ class MobilityPlayer: self.set_stop() self.dialog.show() - def close(self): + def close(self) -> None: if self.dialog: self.dialog.destroy() self.dialog = None - def set_play(self): + def set_play(self) -> None: self.state = MobilityAction.START if self.dialog: self.dialog.set_play() - def set_pause(self): + def set_pause(self) -> None: self.state = MobilityAction.PAUSE if self.dialog: self.dialog.set_pause() - def set_stop(self): + def set_stop(self) -> None: self.state = MobilityAction.STOP if self.dialog: self.dialog.set_stop() class MobilityPlayerDialog(Dialog): - def __init__(self, app: "Application", canvas_node: "CanvasNode", config): + def __init__( + self, + app: "Application", + canvas_node: "CanvasNode", + config: Dict[str, ConfigOption], + ) -> None: super().__init__( app, f"{canvas_node.core_node.name} Mobility Player", modal=False ) self.resizable(False, False) self.geometry("") - self.canvas_node = canvas_node - self.node = canvas_node.core_node - self.config = config - self.play_button = None - self.pause_button = None - self.stop_button = None - self.progressbar = None + self.canvas_node: "CanvasNode" = canvas_node + self.node: Node = canvas_node.core_node + self.config: Dict[str, ConfigOption] = config + self.play_button: Optional[ttk.Button] = None + self.pause_button: Optional[ttk.Button] = None + self.stop_button: Optional[ttk.Button] = None + self.progressbar: Optional[ttk.Progressbar] = None self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) file_name = self.config["file"].value @@ -114,27 +126,27 @@ class MobilityPlayerDialog(Dialog): label = ttk.Label(frame, text=f"rate {rate} ms") label.grid(row=0, column=4) - def clear_buttons(self): + def clear_buttons(self) -> None: self.play_button.state(["!pressed"]) self.pause_button.state(["!pressed"]) self.stop_button.state(["!pressed"]) - def set_play(self): + def set_play(self) -> None: self.clear_buttons() self.play_button.state(["pressed"]) self.progressbar.start() - def set_pause(self): + def set_pause(self) -> None: self.clear_buttons() self.pause_button.state(["pressed"]) self.progressbar.stop() - def set_stop(self): + def set_stop(self) -> None: self.clear_buttons() self.stop_button.state(["pressed"]) self.progressbar.stop() - def click_play(self): + def click_play(self) -> None: self.set_play() session_id = self.app.core.session_id try: @@ -144,7 +156,7 @@ class MobilityPlayerDialog(Dialog): except grpc.RpcError as e: self.app.show_grpc_exception("Mobility Error", e) - def click_pause(self): + def click_pause(self) -> None: self.set_pause() session_id = self.app.core.session_id try: @@ -154,7 +166,7 @@ class MobilityPlayerDialog(Dialog): except grpc.RpcError as e: self.app.show_grpc_exception("Mobility Error", e) - def click_stop(self): + def click_stop(self) -> None: self.set_stop() session_id = self.app.core.session_id try: diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index cec9e9f9..9e958283 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -2,10 +2,12 @@ import logging import tkinter as tk from functools import partial from tkinter import messagebox, ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, Optional import netaddr +from PIL.ImageTk import PhotoImage +from core.api.grpc.core_pb2 import Node from core.gui import nodeutils, validation from core.gui.appconfig import ICONS_PATH from core.gui.dialogs.dialog import Dialog @@ -86,35 +88,35 @@ class InterfaceData: mac: tk.StringVar, ip4: tk.StringVar, ip6: tk.StringVar, - ): - self.is_auto = is_auto - self.mac = mac - self.ip4 = ip4 - self.ip6 = ip6 + ) -> None: + self.is_auto: tk.BooleanVar = is_auto + self.mac: tk.StringVar = mac + self.ip4: tk.StringVar = ip4 + self.ip6: tk.StringVar = ip6 class NodeConfigDialog(Dialog): - def __init__(self, app: "Application", canvas_node: "CanvasNode"): + def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None: """ create an instance of node configuration """ super().__init__(app, f"{canvas_node.core_node.name} Configuration") - self.canvas_node = canvas_node - self.node = canvas_node.core_node - self.image = canvas_node.image - self.image_file = None - self.image_button = None - self.name = tk.StringVar(value=self.node.name) - self.type = tk.StringVar(value=self.node.model) - self.container_image = tk.StringVar(value=self.node.image) + self.canvas_node: "CanvasNode" = canvas_node + self.node: Node = canvas_node.core_node + self.image: PhotoImage = canvas_node.image + self.image_file: Optional[str] = None + self.image_button: Optional[ttk.Button] = 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" if self.node.server: server = self.node.server - self.server = tk.StringVar(value=server) - self.ifaces = {} + self.server: tk.StringVar = tk.StringVar(value=server) + self.ifaces: Dict[int, InterfaceData] = {} self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) row = 0 @@ -202,7 +204,7 @@ class NodeConfigDialog(Dialog): self.draw_spacer() self.draw_buttons() - def draw_ifaces(self): + def draw_ifaces(self) -> None: notebook = ttk.Notebook(self.top) notebook.grid(sticky="nsew", pady=PADY) self.top.rowconfigure(notebook.grid_info()["row"], weight=1) @@ -265,7 +267,7 @@ class NodeConfigDialog(Dialog): self.ifaces[iface.id] = InterfaceData(is_auto, mac, ip4, ip6) - def draw_buttons(self): + def draw_buttons(self) -> None: frame = ttk.Frame(self.top) frame.grid(sticky="ew") frame.columnconfigure(0, weight=1) @@ -277,20 +279,20 @@ class NodeConfigDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def click_emane_config(self, emane_model: str, iface_id: int): + def click_emane_config(self, emane_model: str, iface_id: int) -> None: dialog = EmaneModelDialog( self, self.app, self.canvas_node, emane_model, iface_id ) dialog.show() - def click_icon(self): + def click_icon(self) -> None: file_path = image_chooser(self, ICONS_PATH) if file_path: self.image = Images.create(file_path, nodeutils.ICON_SIZE) self.image_button.config(image=self.image) self.image_file = file_path - def click_apply(self): + def click_apply(self) -> None: error = False # update core node @@ -354,7 +356,7 @@ class NodeConfigDialog(Dialog): self.canvas_node.redraw() self.destroy() - def iface_select(self, event: tk.Event): + def iface_select(self, event: tk.Event) -> None: listbox = event.widget cur = listbox.curselection() if cur: diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeconfigservice.py index 5f77ece3..b5250eba 100644 --- a/daemon/core/gui/dialogs/nodeconfigservice.py +++ b/daemon/core/gui/dialogs/nodeconfigservice.py @@ -4,7 +4,7 @@ core node services import logging import tkinter as tk from tkinter import messagebox, ttk -from typing import TYPE_CHECKING, Set +from typing import TYPE_CHECKING, Optional, Set from core.gui.dialogs.configserviceconfig import ConfigServiceConfigDialog from core.gui.dialogs.dialog import Dialog @@ -19,20 +19,20 @@ if TYPE_CHECKING: class NodeConfigServiceDialog(Dialog): def __init__( self, app: "Application", canvas_node: "CanvasNode", services: Set[str] = None - ): + ) -> None: title = f"{canvas_node.core_node.name} Config Services" super().__init__(app, title) - self.canvas_node = canvas_node - self.node_id = canvas_node.core_node.id - self.groups = None - self.services = None - self.current = None + self.canvas_node: "CanvasNode" = canvas_node + self.node_id: int = canvas_node.core_node.id + self.groups: Optional[ListboxScroll] = None + self.services: Optional[CheckboxList] = None + self.current: Optional[ListboxScroll] = None if services is None: services = set(canvas_node.core_node.config_services) - self.current_services = services + self.current_services: Set[str] = services self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) @@ -86,7 +86,7 @@ class NodeConfigServiceDialog(Dialog): # trigger group change self.groups.listbox.event_generate("<>") - def handle_group_change(self, event: tk.Event = None): + def handle_group_change(self, event: tk.Event = None) -> None: selection = self.groups.listbox.curselection() if selection: index = selection[0] @@ -96,7 +96,7 @@ class NodeConfigServiceDialog(Dialog): checked = name in self.current_services self.services.add(name, checked) - def service_clicked(self, name: str, var: tk.IntVar): + def service_clicked(self, name: str, var: tk.IntVar) -> None: if var.get() and name not in self.current_services: self.current_services.add(name) elif not var.get() and name in self.current_services: @@ -104,7 +104,7 @@ class NodeConfigServiceDialog(Dialog): self.draw_current_services() self.canvas_node.core_node.config_services[:] = self.current_services - def click_configure(self): + def click_configure(self) -> None: current_selection = self.current.listbox.curselection() if len(current_selection): dialog = ConfigServiceConfigDialog( @@ -124,25 +124,25 @@ class NodeConfigServiceDialog(Dialog): parent=self, ) - def draw_current_services(self): + def draw_current_services(self) -> None: self.current.listbox.delete(0, tk.END) for name in sorted(self.current_services): self.current.listbox.insert(tk.END, name) if self.is_custom_service(name): self.current.listbox.itemconfig(tk.END, bg="green") - def click_save(self): + def click_save(self) -> None: self.canvas_node.core_node.config_services[:] = self.current_services logging.info( "saved node config services: %s", self.canvas_node.core_node.config_services ) self.destroy() - def click_cancel(self): + def click_cancel(self) -> None: self.current_services = None self.destroy() - def click_remove(self): + def click_remove(self) -> None: cur = self.current.listbox.curselection() if cur: service = self.current.listbox.get(cur[0]) diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index 13490d8c..f6f5e5cf 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -3,7 +3,7 @@ core node services """ import tkinter as tk from tkinter import messagebox, ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, Set from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.serviceconfig import ServiceConfigDialog @@ -16,19 +16,19 @@ if TYPE_CHECKING: class NodeServiceDialog(Dialog): - def __init__(self, app: "Application", canvas_node: "CanvasNode"): + def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None: title = f"{canvas_node.core_node.name} Services" super().__init__(app, title) - self.canvas_node = canvas_node - self.node_id = canvas_node.core_node.id - self.groups = None - self.services = None - self.current = None + self.canvas_node: "CanvasNode" = canvas_node + self.node_id: int = canvas_node.core_node.id + self.groups: Optional[ListboxScroll] = None + self.services: Optional[CheckboxList] = None + self.current: Optional[ListboxScroll] = None services = set(canvas_node.core_node.services) - self.current_services = services + self.current_services: Set[str] = services self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) @@ -84,7 +84,7 @@ class NodeServiceDialog(Dialog): # trigger group change self.groups.listbox.event_generate("<>") - def handle_group_change(self, event: tk.Event = None): + def handle_group_change(self, event: tk.Event = None) -> None: selection = self.groups.listbox.curselection() if selection: index = selection[0] @@ -94,7 +94,7 @@ class NodeServiceDialog(Dialog): checked = name in self.current_services self.services.add(name, checked) - def service_clicked(self, name: str, var: tk.IntVar): + def service_clicked(self, name: str, var: tk.IntVar) -> None: if var.get() and name not in self.current_services: self.current_services.add(name) elif not var.get() and name in self.current_services: @@ -106,7 +106,7 @@ class NodeServiceDialog(Dialog): self.current.listbox.itemconfig(tk.END, bg="green") self.canvas_node.core_node.services[:] = self.current_services - def click_configure(self): + def click_configure(self) -> None: current_selection = self.current.listbox.curselection() if len(current_selection): dialog = ServiceConfigDialog( @@ -127,12 +127,12 @@ class NodeServiceDialog(Dialog): "Service Configuration", "Select a service to configure", parent=self ) - def click_save(self): + def click_save(self) -> None: core_node = self.canvas_node.core_node core_node.services[:] = self.current_services self.destroy() - def click_remove(self): + def click_remove(self) -> None: cur = self.current.listbox.curselection() if cur: service = self.current.listbox.get(cur[0]) diff --git a/daemon/core/gui/dialogs/observers.py b/daemon/core/gui/dialogs/observers.py index d1812b64..286fc2c9 100644 --- a/daemon/core/gui/dialogs/observers.py +++ b/daemon/core/gui/dialogs/observers.py @@ -1,6 +1,6 @@ import tkinter as tk from tkinter import messagebox, ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from core.gui.appconfig import Observer from core.gui.dialogs.dialog import Dialog @@ -12,18 +12,18 @@ if TYPE_CHECKING: class ObserverDialog(Dialog): - def __init__(self, app: "Application"): + def __init__(self, app: "Application") -> None: super().__init__(app, "Observer Widgets") - self.observers = None - self.save_button = None - self.delete_button = None - self.selected = None - self.selected_index = None - self.name = tk.StringVar() - self.cmd = tk.StringVar() + self.observers: Optional[tk.Listbox] = None + self.save_button: Optional[ttk.Button] = None + self.delete_button: Optional[ttk.Button] = None + self.selected: Optional[str] = None + self.selected_index: Optional[int] = None + self.name: tk.StringVar = tk.StringVar() + self.cmd: tk.StringVar = tk.StringVar() self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) self.draw_listbox() @@ -31,7 +31,7 @@ class ObserverDialog(Dialog): self.draw_config_buttons() self.draw_apply_buttons() - def draw_listbox(self): + def draw_listbox(self) -> None: listbox_scroll = ListboxScroll(self.top) listbox_scroll.grid(sticky="nsew", pady=PADY) listbox_scroll.columnconfigure(0, weight=1) @@ -42,7 +42,7 @@ class ObserverDialog(Dialog): for name in sorted(self.app.core.custom_observers): self.observers.insert(tk.END, name) - def draw_form_fields(self): + def draw_form_fields(self) -> None: frame = ttk.Frame(self.top) frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(1, weight=1) @@ -57,7 +57,7 @@ class ObserverDialog(Dialog): entry = ttk.Entry(frame, textvariable=self.cmd) entry.grid(row=1, column=1, sticky="ew") - def draw_config_buttons(self): + def draw_config_buttons(self) -> None: frame = ttk.Frame(self.top) frame.grid(sticky="ew", pady=PADY) for i in range(3): @@ -76,7 +76,7 @@ class ObserverDialog(Dialog): ) self.delete_button.grid(row=0, column=2, sticky="ew") - def draw_apply_buttons(self): + def draw_apply_buttons(self) -> None: frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): @@ -88,14 +88,14 @@ class ObserverDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def click_save_config(self): + def click_save_config(self) -> None: self.app.guiconfig.observers.clear() for observer in self.app.core.custom_observers.values(): self.app.guiconfig.observers.append(observer) self.app.save_config() self.destroy() - def click_create(self): + def click_create(self) -> None: name = self.name.get() if name not in self.app.core.custom_observers: cmd = self.cmd.get() @@ -109,7 +109,7 @@ class ObserverDialog(Dialog): else: messagebox.showerror("Observer Error", f"{name} already exists") - def click_save(self): + def click_save(self) -> None: name = self.name.get() if self.selected: previous_name = self.selected @@ -122,7 +122,7 @@ class ObserverDialog(Dialog): self.observers.insert(self.selected_index, name) self.observers.selection_set(self.selected_index) - def click_delete(self): + def click_delete(self) -> None: if self.selected: self.observers.delete(self.selected_index) del self.app.core.custom_observers[self.selected] @@ -136,7 +136,7 @@ class ObserverDialog(Dialog): self.app.menubar.observers_menu.draw_custom() self.app.toolbar.observers_menu.draw_custom() - def handle_observer_change(self, event: tk.Event): + def handle_observer_change(self, event: tk.Event) -> None: selection = self.observers.curselection() if selection: self.selected_index = selection[0] diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index 11d1ba95..839ebd3b 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -12,27 +12,27 @@ from core.gui.validation import LARGEST_SCALE, SMALLEST_SCALE if TYPE_CHECKING: from core.gui.app import Application -SCALE_INTERVAL = 0.01 +SCALE_INTERVAL: float = 0.01 class PreferencesDialog(Dialog): - def __init__(self, app: "Application"): + def __init__(self, app: "Application") -> None: super().__init__(app, "Preferences") - self.gui_scale = tk.DoubleVar(value=self.app.app_scale) + self.gui_scale: tk.DoubleVar = tk.DoubleVar(value=self.app.app_scale) preferences = self.app.guiconfig.preferences - self.editor = tk.StringVar(value=preferences.editor) - self.theme = tk.StringVar(value=preferences.theme) - self.terminal = tk.StringVar(value=preferences.terminal) - self.gui3d = tk.StringVar(value=preferences.gui3d) + self.editor: tk.StringVar = tk.StringVar(value=preferences.editor) + self.theme: tk.StringVar = tk.StringVar(value=preferences.theme) + self.terminal: tk.StringVar = tk.StringVar(value=preferences.terminal) + self.gui3d: tk.StringVar = tk.StringVar(value=preferences.gui3d) self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) self.draw_preferences() self.draw_buttons() - def draw_preferences(self): + def draw_preferences(self) -> None: frame = ttk.LabelFrame(self.top, text="Preferences", padding=FRAME_PAD) frame.grid(sticky="nsew", pady=PADY) frame.columnconfigure(1, weight=1) @@ -88,7 +88,7 @@ class PreferencesDialog(Dialog): scrollbar = ttk.Scrollbar(scale_frame, command=self.adjust_scale) scrollbar.grid(row=0, column=2) - def draw_buttons(self): + def draw_buttons(self) -> None: frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): @@ -100,12 +100,12 @@ class PreferencesDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def theme_change(self, event: tk.Event): + def theme_change(self, event: tk.Event) -> None: theme = self.theme.get() logging.info("changing theme: %s", theme) self.app.style.theme_use(theme) - def click_save(self): + def click_save(self) -> None: preferences = self.app.guiconfig.preferences preferences.terminal = self.terminal.get() preferences.editor = self.editor.get() @@ -118,7 +118,7 @@ class PreferencesDialog(Dialog): self.scale_adjust() self.destroy() - def scale_adjust(self): + def scale_adjust(self) -> None: app_scale = self.gui_scale.get() self.app.app_scale = app_scale self.app.master.tk.call("tk", "scaling", app_scale) @@ -136,7 +136,7 @@ class PreferencesDialog(Dialog): self.app.toolbar.scale() self.app.canvas.scale_graph() - def adjust_scale(self, arg1: str, arg2: str, arg3: str): + def adjust_scale(self, arg1: str, arg2: str, arg3: str) -> None: scale_value = self.gui_scale.get() if arg2 == "-1": if scale_value <= LARGEST_SCALE - SCALE_INTERVAL: diff --git a/daemon/core/gui/dialogs/runtool.py b/daemon/core/gui/dialogs/runtool.py index 98be730f..c66fea8f 100644 --- a/daemon/core/gui/dialogs/runtool.py +++ b/daemon/core/gui/dialogs/runtool.py @@ -1,6 +1,6 @@ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, Optional from core.gui.dialogs.dialog import Dialog from core.gui.nodeutils import NodeUtils @@ -14,10 +14,10 @@ if TYPE_CHECKING: class RunToolDialog(Dialog): def __init__(self, app: "Application") -> None: super().__init__(app, "Run Tool") - self.cmd = tk.StringVar(value="ps ax") - self.result = None - self.node_list = None - self.executable_nodes = {} + self.cmd: tk.StringVar = tk.StringVar(value="ps ax") + self.result: Optional[CodeText] = None + self.node_list: Optional[ListboxScroll] = None + self.executable_nodes: Dict[str, int] = {} self.store_nodes() self.draw() diff --git a/daemon/core/gui/dialogs/servers.py b/daemon/core/gui/dialogs/servers.py index 7ca96e9f..45121a20 100644 --- a/daemon/core/gui/dialogs/servers.py +++ b/daemon/core/gui/dialogs/servers.py @@ -1,6 +1,6 @@ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from core.gui.appconfig import CoreServer from core.gui.dialogs.dialog import Dialog @@ -10,24 +10,24 @@ from core.gui.widgets import ListboxScroll if TYPE_CHECKING: from core.gui.app import Application -DEFAULT_NAME = "example" -DEFAULT_ADDRESS = "127.0.0.1" -DEFAULT_PORT = 50051 +DEFAULT_NAME: str = "example" +DEFAULT_ADDRESS: str = "127.0.0.1" +DEFAULT_PORT: int = 50051 class ServersDialog(Dialog): - def __init__(self, app: "Application"): + def __init__(self, app: "Application") -> None: super().__init__(app, "CORE Servers") - self.name = tk.StringVar(value=DEFAULT_NAME) - self.address = tk.StringVar(value=DEFAULT_ADDRESS) - self.servers = None - self.selected_index = None - self.selected = None - self.save_button = None - self.delete_button = None + self.name: tk.StringVar = tk.StringVar(value=DEFAULT_NAME) + self.address: tk.StringVar = tk.StringVar(value=DEFAULT_ADDRESS) + self.servers: Optional[tk.Listbox] = None + self.selected_index: Optional[int] = None + self.selected: Optional[str] = None + self.save_button: Optional[ttk.Button] = None + self.delete_button: Optional[ttk.Button] = None self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) self.draw_servers() @@ -35,7 +35,7 @@ class ServersDialog(Dialog): self.draw_server_configuration() self.draw_apply_buttons() - def draw_servers(self): + def draw_servers(self) -> None: listbox_scroll = ListboxScroll(self.top) listbox_scroll.grid(pady=PADY, sticky="nsew") listbox_scroll.columnconfigure(0, weight=1) @@ -48,7 +48,7 @@ class ServersDialog(Dialog): for server in self.app.core.servers: self.servers.insert(tk.END, server) - def draw_server_configuration(self): + def draw_server_configuration(self) -> None: frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=FRAME_PAD) frame.grid(pady=PADY, sticky="ew") frame.columnconfigure(1, weight=1) @@ -64,7 +64,7 @@ class ServersDialog(Dialog): entry = ttk.Entry(frame, textvariable=self.address) entry.grid(row=0, column=3, sticky="ew") - def draw_servers_buttons(self): + def draw_servers_buttons(self) -> None: frame = ttk.Frame(self.top) frame.grid(pady=PADY, sticky="ew") for i in range(3): @@ -83,7 +83,7 @@ class ServersDialog(Dialog): ) self.delete_button.grid(row=0, column=2, sticky="ew") - def draw_apply_buttons(self): + def draw_apply_buttons(self) -> None: frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): @@ -104,7 +104,7 @@ class ServersDialog(Dialog): self.app.save_config() self.destroy() - def click_create(self): + def click_create(self) -> None: name = self.name.get() if name not in self.app.core.servers: address = self.address.get() @@ -112,7 +112,7 @@ class ServersDialog(Dialog): self.app.core.servers[name] = server self.servers.insert(tk.END, name) - def click_save(self): + def click_save(self) -> None: name = self.name.get() if self.selected: previous_name = self.selected @@ -125,7 +125,7 @@ class ServersDialog(Dialog): self.servers.insert(self.selected_index, name) self.servers.selection_set(self.selected_index) - def click_delete(self): + def click_delete(self) -> None: if self.selected: self.servers.delete(self.selected_index) del self.app.core.servers[self.selected] @@ -137,7 +137,7 @@ class ServersDialog(Dialog): self.save_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED) - def handle_server_change(self, event: tk.Event): + def handle_server_change(self, event: tk.Event) -> None: selection = self.servers.curselection() if selection: self.selected_index = selection[0] diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index efeefa09..5faface7 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -2,11 +2,12 @@ import logging import os import tkinter as tk from tkinter import filedialog, ttk -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple import grpc +from PIL.ImageTk import PhotoImage -from core.api.grpc.services_pb2 import ServiceValidationMode +from core.api.grpc.services_pb2 import NodeServiceData, ServiceValidationMode from core.gui.dialogs.copyserviceconfig import CopyServiceConfigDialog from core.gui.dialogs.dialog import Dialog from core.gui.images import ImageEnum, Images @@ -16,8 +17,9 @@ from core.gui.widgets import CodeText, ListboxScroll if TYPE_CHECKING: from core.gui.app import Application from core.gui.graph.node import CanvasNode + from core.gui.coreclient import CoreClient -ICON_SIZE = 16 +ICON_SIZE: int = 16 class ServiceConfigDialog(Dialog): @@ -28,54 +30,57 @@ class ServiceConfigDialog(Dialog): service_name: str, canvas_node: "CanvasNode", node_id: int, - ): + ) -> None: title = f"{service_name} Service" super().__init__(app, title, master=master) - self.core = app.core - self.canvas_node = canvas_node - self.node_id = node_id - self.service_name = service_name - self.radiovar = tk.IntVar() - self.radiovar.set(2) - self.metadata = "" - self.filenames = [] - self.dependencies = [] - self.executables = [] - self.startup_commands = [] - self.validation_commands = [] - self.shutdown_commands = [] - self.default_startup = [] - self.default_validate = [] - self.default_shutdown = [] - self.validation_mode = None - self.validation_time = None - self.validation_period = None - self.directory_entry = None - self.default_directories = [] - self.temp_directories = [] - self.documentnew_img = self.app.get_icon(ImageEnum.DOCUMENTNEW, ICON_SIZE) - self.editdelete_img = self.app.get_icon(ImageEnum.EDITDELETE, ICON_SIZE) - self.notebook = None - self.metadata_entry = None - self.filename_combobox = None - self.dir_list = None - self.startup_commands_listbox = None - self.shutdown_commands_listbox = None - self.validate_commands_listbox = None - self.validation_time_entry = None - self.validation_mode_entry = None - self.service_file_data = None - self.validation_period_entry = None - self.original_service_files = {} - self.default_config = None - self.temp_service_files = {} - self.modified_files = set() - self.has_error = False + self.core: "CoreClient" = app.core + self.canvas_node: "CanvasNode" = canvas_node + self.node_id: int = node_id + self.service_name: str = service_name + self.radiovar: tk.IntVar = tk.IntVar(value=2) + self.metadata: str = "" + self.filenames: List[str] = [] + self.dependencies: List[str] = [] + self.executables: List[str] = [] + self.startup_commands: List[str] = [] + self.validation_commands: List[str] = [] + self.shutdown_commands: List[str] = [] + self.default_startup: List[str] = [] + self.default_validate: List[str] = [] + self.default_shutdown: List[str] = [] + self.validation_mode: Optional[ServiceValidationMode] = None + self.validation_time: Optional[int] = None + self.validation_period: Optional[float] = None + self.directory_entry: Optional[ttk.Entry] = None + self.default_directories: List[str] = [] + self.temp_directories: List[str] = [] + self.documentnew_img: PhotoImage = self.app.get_icon( + ImageEnum.DOCUMENTNEW, ICON_SIZE + ) + self.editdelete_img: PhotoImage = self.app.get_icon( + ImageEnum.EDITDELETE, ICON_SIZE + ) + self.notebook: Optional[ttk.Notebook] = None + self.metadata_entry: Optional[ttk.Entry] = None + self.filename_combobox: Optional[ttk.Combobox] = None + self.dir_list: Optional[ListboxScroll] = None + self.startup_commands_listbox: Optional[tk.Listbox] = None + self.shutdown_commands_listbox: Optional[tk.Listbox] = None + self.validate_commands_listbox: Optional[tk.Listbox] = None + self.validation_time_entry: Optional[ttk.Entry] = None + self.validation_mode_entry: Optional[ttk.Entry] = None + self.service_file_data: Optional[CodeText] = None + self.validation_period_entry: Optional[ttk.Entry] = None + self.original_service_files: Dict[str, str] = {} + self.default_config: NodeServiceData = None + self.temp_service_files: Dict[str, str] = {} + self.modified_files: Set[str] = set() + self.has_error: bool = False self.load() if not self.has_error: self.draw() - def load(self): + def load(self) -> None: try: self.app.core.create_nodes_and_links() default_config = self.app.core.get_node_service( @@ -119,7 +124,7 @@ class ServiceConfigDialog(Dialog): self.app.show_grpc_exception("Get Node Service Error", e) self.has_error = True - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(1, weight=1) @@ -142,7 +147,7 @@ class ServiceConfigDialog(Dialog): self.draw_buttons() - def draw_tab_files(self): + def draw_tab_files(self) -> None: tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab.grid(sticky="nsew") tab.columnconfigure(0, weight=1) @@ -222,7 +227,7 @@ class ServiceConfigDialog(Dialog): "", self.update_temp_service_file_data ) - def draw_tab_directories(self): + def draw_tab_directories(self) -> None: tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab.grid(sticky="nsew") tab.columnconfigure(0, weight=1) @@ -257,7 +262,7 @@ class ServiceConfigDialog(Dialog): button = ttk.Button(frame, text="Remove", command=self.remove_directory) button.grid(row=0, column=1, sticky="ew") - def draw_tab_startstop(self): + def draw_tab_startstop(self) -> None: tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab.grid(sticky="nsew") tab.columnconfigure(0, weight=1) @@ -311,7 +316,7 @@ class ServiceConfigDialog(Dialog): elif i == 2: self.validate_commands_listbox = listbox_scroll.listbox - def draw_tab_configuration(self): + def draw_tab_configuration(self) -> None: tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab.grid(sticky="nsew") tab.columnconfigure(0, weight=1) @@ -370,7 +375,7 @@ class ServiceConfigDialog(Dialog): for dependency in self.dependencies: listbox_scroll.listbox.insert("end", dependency) - def draw_buttons(self): + def draw_buttons(self) -> None: frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(4): @@ -384,7 +389,7 @@ class ServiceConfigDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=3, sticky="ew") - def add_filename(self): + def add_filename(self) -> None: filename = self.filename_combobox.get() if filename not in self.filename_combobox["values"]: self.filename_combobox["values"] += (filename,) @@ -395,7 +400,7 @@ class ServiceConfigDialog(Dialog): else: logging.debug("file already existed") - def delete_filename(self): + def delete_filename(self) -> None: cbb = self.filename_combobox filename = cbb.get() if filename in cbb["values"]: @@ -407,7 +412,7 @@ class ServiceConfigDialog(Dialog): self.modified_files.remove(filename) @classmethod - def add_command(cls, event: tk.Event): + def add_command(cls, event: tk.Event) -> None: frame_contains_button = event.widget.master listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox command_to_add = frame_contains_button.grid_slaves(row=0, column=0)[0].get() @@ -419,7 +424,7 @@ class ServiceConfigDialog(Dialog): listbox.insert(tk.END, command_to_add) @classmethod - def update_entry(cls, event: tk.Event): + def update_entry(cls, event: tk.Event) -> None: listbox = event.widget current_selection = listbox.curselection() if len(current_selection) > 0: @@ -431,7 +436,7 @@ class ServiceConfigDialog(Dialog): entry.insert(0, cmd) @classmethod - def delete_command(cls, event: tk.Event): + def delete_command(cls, event: tk.Event) -> None: button = event.widget frame_contains_button = button.master listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox @@ -441,7 +446,7 @@ class ServiceConfigDialog(Dialog): entry = frame_contains_button.grid_slaves(row=0, column=0)[0] entry.delete(0, tk.END) - def click_apply(self): + def click_apply(self) -> None: if ( not self.is_custom_command() and not self.is_custom_service_file() @@ -484,12 +489,12 @@ class ServiceConfigDialog(Dialog): self.app.show_grpc_exception("Save Service Config Error", e) self.destroy() - def display_service_file_data(self, event: tk.Event): + def display_service_file_data(self, event: tk.Event) -> None: filename = self.filename_combobox.get() self.service_file_data.text.delete(1.0, "end") self.service_file_data.text.insert("end", self.temp_service_files[filename]) - def update_temp_service_file_data(self, event: tk.Event): + def update_temp_service_file_data(self, event: tk.Event) -> None: filename = self.filename_combobox.get() self.temp_service_files[filename] = self.service_file_data.text.get(1.0, "end") if self.temp_service_files[filename] != self.original_service_files.get( @@ -499,7 +504,7 @@ class ServiceConfigDialog(Dialog): else: self.modified_files.discard(filename) - def is_custom_command(self): + def is_custom_command(self) -> bool: startup, validate, shutdown = self.get_commands() return ( set(self.default_startup) != set(startup) @@ -507,16 +512,16 @@ class ServiceConfigDialog(Dialog): or set(self.default_shutdown) != set(shutdown) ) - def has_new_files(self): + def has_new_files(self) -> bool: return set(self.filenames) != set(self.filename_combobox["values"]) - def is_custom_service_file(self): + def is_custom_service_file(self) -> bool: return len(self.modified_files) > 0 - def is_custom_directory(self): + def is_custom_directory(self) -> bool: return set(self.default_directories) != set(self.dir_list.listbox.get(0, "end")) - def click_defaults(self): + def click_defaults(self) -> None: """ clears out any custom configuration permanently """ @@ -557,37 +562,37 @@ class ServiceConfigDialog(Dialog): self.current_service_color("") - def click_copy(self): + def click_copy(self) -> None: dialog = CopyServiceConfigDialog(self, self.app, self.node_id) dialog.show() @classmethod def append_commands( cls, commands: List[str], listbox: tk.Listbox, to_add: List[str] - ): + ) -> None: for cmd in to_add: commands.append(cmd) listbox.insert(tk.END, cmd) - def get_commands(self): + def get_commands(self) -> Tuple[List[str], List[str], List[str]]: startup = self.startup_commands_listbox.get(0, "end") shutdown = self.shutdown_commands_listbox.get(0, "end") validate = self.validate_commands_listbox.get(0, "end") return startup, validate, shutdown - def find_directory_button(self): + def find_directory_button(self) -> None: d = filedialog.askdirectory(initialdir="/") self.directory_entry.delete(0, "end") self.directory_entry.insert("end", d) - def add_directory(self): + def add_directory(self) -> None: d = self.directory_entry.get() if os.path.isdir(d): if d not in self.temp_directories: self.dir_list.listbox.insert("end", d) self.temp_directories.append(d) - def remove_directory(self): + def remove_directory(self) -> None: d = self.directory_entry.get() dirs = self.dir_list.listbox.get(0, "end") if d and d in self.temp_directories: @@ -599,14 +604,14 @@ class ServiceConfigDialog(Dialog): logging.debug("directory is not in the list") self.directory_entry.delete(0, "end") - def directory_select(self, event): + def directory_select(self, event) -> None: i = self.dir_list.listbox.curselection() if i: d = self.dir_list.listbox.get(i) self.directory_entry.delete(0, "end") self.directory_entry.insert("end", d) - def current_service_color(self, color=""): + def current_service_color(self, color="") -> None: """ change the current service label color """ diff --git a/daemon/core/gui/dialogs/sessionoptions.py b/daemon/core/gui/dialogs/sessionoptions.py index d31a5fb5..8138d854 100644 --- a/daemon/core/gui/dialogs/sessionoptions.py +++ b/daemon/core/gui/dialogs/sessionoptions.py @@ -1,9 +1,10 @@ import logging from tkinter import ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, Optional import grpc +from core.api.grpc.common_pb2 import ConfigOption from core.gui.dialogs.dialog import Dialog from core.gui.themes import PADX, PADY from core.gui.widgets import ConfigFrame @@ -13,15 +14,15 @@ if TYPE_CHECKING: class SessionOptionsDialog(Dialog): - def __init__(self, app: "Application"): + def __init__(self, app: "Application") -> None: super().__init__(app, "Session Options") - self.config_frame = None - self.has_error = False - self.config = self.get_config() + self.config_frame: Optional[ConfigFrame] = None + self.has_error: bool = False + self.config: Dict[str, ConfigOption] = self.get_config() if not self.has_error: self.draw() - def get_config(self): + def get_config(self) -> Dict[str, ConfigOption]: try: session_id = self.app.core.session_id response = self.app.core.client.get_session_options(session_id) @@ -31,7 +32,7 @@ class SessionOptionsDialog(Dialog): self.has_error = True self.destroy() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) @@ -48,7 +49,7 @@ class SessionOptionsDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def save(self): + def save(self) -> None: config = self.config_frame.parse_config() try: session_id = self.app.core.session_id diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index 9aa71a13..a7d702eb 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -1,11 +1,12 @@ import logging import tkinter as tk from tkinter import messagebox, ttk -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, List, Optional import grpc from core.api.grpc import core_pb2 +from core.api.grpc.core_pb2 import SessionSummary from core.gui.dialogs.dialog import Dialog from core.gui.images import ImageEnum, Images from core.gui.task import ProgressTask @@ -18,17 +19,17 @@ if TYPE_CHECKING: class SessionsDialog(Dialog): def __init__(self, app: "Application", is_start_app: bool = False) -> None: super().__init__(app, "Sessions") - self.is_start_app = is_start_app - self.selected_session = None - self.selected_id = None - self.tree = None - self.sessions = self.get_sessions() - self.connect_button = None - self.delete_button = None + self.is_start_app: bool = is_start_app + self.selected_session: Optional[int] = None + self.selected_id: Optional[int] = None + self.tree: Optional[ttk.Treeview] = None + self.sessions: List[SessionSummary] = self.get_sessions() + self.connect_button: Optional[ttk.Button] = None + self.delete_button: Optional[ttk.Button] = None self.protocol("WM_DELETE_WINDOW", self.on_closing) self.draw() - def get_sessions(self) -> List[core_pb2.SessionSummary]: + def get_sessions(self) -> List[SessionSummary]: try: response = self.app.core.client.get_sessions() logging.info("sessions: %s", response) diff --git a/daemon/core/gui/dialogs/shapemod.py b/daemon/core/gui/dialogs/shapemod.py index 4c84991b..2ca06772 100644 --- a/daemon/core/gui/dialogs/shapemod.py +++ b/daemon/core/gui/dialogs/shapemod.py @@ -3,7 +3,7 @@ shape input dialog """ import tkinter as tk from tkinter import font, ttk -from typing import TYPE_CHECKING, List, Union +from typing import TYPE_CHECKING, List, Optional, Union from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.dialog import Dialog @@ -13,40 +13,41 @@ from core.gui.themes import FRAME_PAD, PADX, PADY if TYPE_CHECKING: from core.gui.app import Application + from core.gui.graph.graph import CanvasGraph from core.gui.graph.shape import Shape -FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] -BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +FONT_SIZES: List[int] = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] +BORDER_WIDTH: List[int] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] class ShapeDialog(Dialog): - def __init__(self, app: "Application", shape: "Shape"): + def __init__(self, app: "Application", shape: "Shape") -> None: if is_draw_shape(shape.shape_type): title = "Add Shape" else: title = "Add Text" super().__init__(app, title) - self.canvas = app.canvas - self.fill = None - self.border = None - self.shape = shape + self.canvas: "CanvasGraph" = app.canvas + self.fill: Optional[ttk.Label] = None + self.border: Optional[ttk.Label] = None + self.shape: "Shape" = shape data = shape.shape_data - self.shape_text = tk.StringVar(value=data.text) - self.font = tk.StringVar(value=data.font) - self.font_size = tk.IntVar(value=data.font_size) - self.text_color = data.text_color + self.shape_text: tk.StringVar = tk.StringVar(value=data.text) + self.font: tk.StringVar = tk.StringVar(value=data.font) + self.font_size: tk.IntVar = tk.IntVar(value=data.font_size) + self.text_color: str = data.text_color fill_color = data.fill_color if not fill_color: fill_color = "#CFCFFF" - self.fill_color = fill_color - self.border_color = data.border_color - self.border_width = tk.IntVar(value=0) - self.bold = tk.BooleanVar(value=data.bold) - self.italic = tk.BooleanVar(value=data.italic) - self.underline = tk.BooleanVar(value=data.underline) + self.fill_color: str = fill_color + self.border_color: str = data.border_color + self.border_width: tk.IntVar = tk.IntVar(value=0) + self.bold: tk.BooleanVar = tk.BooleanVar(value=data.bold) + self.italic: tk.BooleanVar = tk.BooleanVar(value=data.italic) + self.underline: tk.BooleanVar = tk.BooleanVar(value=data.underline) self.draw() - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.draw_label_options() if is_draw_shape(self.shape.shape_type): @@ -54,7 +55,7 @@ class ShapeDialog(Dialog): self.draw_spacer() self.draw_buttons() - def draw_label_options(self): + def draw_label_options(self) -> None: label_frame = ttk.LabelFrame(self.top, text="Label", padding=FRAME_PAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -94,7 +95,7 @@ class ShapeDialog(Dialog): button = ttk.Checkbutton(frame, variable=self.underline, text="Underline") button.grid(row=0, column=2, sticky="ew") - def draw_shape_options(self): + def draw_shape_options(self) -> None: label_frame = ttk.LabelFrame(self.top, text="Shape", padding=FRAME_PAD) label_frame.grid(sticky="ew", pady=PADY) label_frame.columnconfigure(0, weight=1) @@ -129,7 +130,7 @@ class ShapeDialog(Dialog): ) combobox.grid(row=0, column=1, sticky="nsew") - def draw_buttons(self): + def draw_buttons(self) -> None: frame = ttk.Frame(self.top) frame.grid(sticky="nsew") frame.columnconfigure(0, weight=1) @@ -139,28 +140,28 @@ class ShapeDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.cancel) button.grid(row=0, column=1, sticky="ew") - def choose_text_color(self): + def choose_text_color(self) -> None: color_picker = ColorPickerDialog(self, self.app, self.text_color) self.text_color = color_picker.askcolor() - def choose_fill_color(self): + def choose_fill_color(self) -> None: color_picker = ColorPickerDialog(self, self.app, self.fill_color) color = color_picker.askcolor() self.fill_color = color self.fill.config(background=color, text=color) - def choose_border_color(self): + def choose_border_color(self) -> None: color_picker = ColorPickerDialog(self, self.app, self.border_color) color = color_picker.askcolor() self.border_color = color self.border.config(background=color, text=color) - def cancel(self): + def cancel(self) -> None: self.shape.delete() self.canvas.shapes.pop(self.shape.id) self.destroy() - def click_add(self): + def click_add(self) -> None: if is_draw_shape(self.shape.shape_type): self.add_shape() elif is_shape_text(self.shape.shape_type): @@ -181,7 +182,7 @@ class ShapeDialog(Dialog): text_font.append("underline") return text_font - def save_text(self): + def save_text(self) -> None: """ save info related to text or shape label """ @@ -194,7 +195,7 @@ class ShapeDialog(Dialog): data.italic = self.italic.get() data.underline = self.underline.get() - def save_shape(self): + def save_shape(self) -> None: """ save info related to shape """ @@ -203,7 +204,7 @@ class ShapeDialog(Dialog): data.border_color = self.border_color data.border_width = int(self.border_width.get()) - def add_text(self): + def add_text(self) -> None: """ add text to canvas """ @@ -214,7 +215,7 @@ class ShapeDialog(Dialog): ) self.save_text() - def add_shape(self): + def add_shape(self) -> None: self.canvas.itemconfig( self.shape.id, fill=self.fill_color, diff --git a/daemon/core/gui/dialogs/throughput.py b/daemon/core/gui/dialogs/throughput.py index 5210fe59..5b3cc9b3 100644 --- a/daemon/core/gui/dialogs/throughput.py +++ b/daemon/core/gui/dialogs/throughput.py @@ -3,10 +3,11 @@ throughput dialog """ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.dialog import Dialog +from core.gui.graph.graph import CanvasGraph from core.gui.themes import FRAME_PAD, PADX, PADY if TYPE_CHECKING: @@ -14,21 +15,23 @@ if TYPE_CHECKING: class ThroughputDialog(Dialog): - def __init__(self, app: "Application"): + def __init__(self, app: "Application") -> None: super().__init__(app, "Throughput Config") - self.canvas = app.canvas - self.show_throughput = tk.IntVar(value=1) - self.exponential_weight = tk.IntVar(value=1) - self.transmission = tk.IntVar(value=1) - self.reception = tk.IntVar(value=1) - self.threshold = tk.DoubleVar(value=self.canvas.throughput_threshold) - self.width = tk.IntVar(value=self.canvas.throughput_width) - self.color = self.canvas.throughput_color - self.color_button = None + self.canvas: CanvasGraph = app.canvas + self.show_throughput: tk.IntVar = tk.IntVar(value=1) + self.exponential_weight: tk.IntVar = tk.IntVar(value=1) + self.transmission: tk.IntVar = tk.IntVar(value=1) + self.reception: tk.IntVar = tk.IntVar(value=1) + self.threshold: tk.DoubleVar = tk.DoubleVar( + value=self.canvas.throughput_threshold + ) + self.width: tk.IntVar = tk.IntVar(value=self.canvas.throughput_width) + self.color: str = self.canvas.throughput_color + self.color_button: Optional[tk.Button] = None self.top.columnconfigure(0, weight=1) self.draw() - def draw(self): + def draw(self) -> None: button = ttk.Checkbutton( self.top, variable=self.show_throughput, @@ -97,12 +100,12 @@ class ThroughputDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def click_color(self): + def click_color(self) -> None: color_picker = ColorPickerDialog(self, self.app, self.color) self.color = color_picker.askcolor() self.color_button.config(bg=self.color, text=self.color, bd=0) - def click_save(self): + def click_save(self) -> None: self.canvas.throughput_threshold = self.threshold.get() self.canvas.throughput_width = self.width.get() self.canvas.throughput_color = self.color diff --git a/daemon/core/gui/dialogs/wlanconfig.py b/daemon/core/gui/dialogs/wlanconfig.py index b0435a2f..326b3195 100644 --- a/daemon/core/gui/dialogs/wlanconfig.py +++ b/daemon/core/gui/dialogs/wlanconfig.py @@ -1,8 +1,10 @@ from tkinter import ttk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, Optional import grpc +from core.api.grpc.common_pb2 import ConfigOption +from core.api.grpc.core_pb2 import Node from core.gui.dialogs.dialog import Dialog from core.gui.themes import PADX, PADY from core.gui.widgets import ConfigFrame @@ -10,34 +12,36 @@ from core.gui.widgets import ConfigFrame if TYPE_CHECKING: from core.gui.app import Application from core.gui.graph.node import CanvasNode + from core.gui.graph.graph import CanvasGraph -RANGE_COLOR = "#009933" -RANGE_WIDTH = 3 +RANGE_COLOR: str = "#009933" +RANGE_WIDTH: int = 3 class WlanConfigDialog(Dialog): - def __init__(self, app: "Application", canvas_node: "CanvasNode"): + def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None: super().__init__(app, f"{canvas_node.core_node.name} WLAN Configuration") - self.canvas_node = canvas_node - self.node = canvas_node.core_node - self.config_frame = None - self.range_entry = None - self.has_error = False - self.canvas = app.canvas - self.ranges = {} - self.positive_int = self.app.master.register(self.validate_and_update) + self.canvas: "CanvasGraph" = app.canvas + self.canvas_node: "CanvasNode" = canvas_node + self.node: Node = canvas_node.core_node + self.config_frame: Optional[ConfigFrame] = None + self.range_entry: Optional[ttk.Entry] = None + self.has_error: bool = False + self.ranges: Dict[int, int] = {} + self.positive_int: int = self.app.master.register(self.validate_and_update) try: - self.config = self.canvas_node.wlan_config - if not self.config: - self.config = self.app.core.get_wlan_config(self.node.id) + config = self.canvas_node.wlan_config + if not config: + config = self.app.core.get_wlan_config(self.node.id) + self.config: Dict[str, ConfigOption] = config self.init_draw_range() self.draw() except grpc.RpcError as e: self.app.show_grpc_exception("WLAN Config Error", e) - self.has_error = True + self.has_error: bool = True self.destroy() - def init_draw_range(self): + def init_draw_range(self) -> None: if self.canvas_node.id in self.canvas.wireless_network: for cid in self.canvas.wireless_network[self.canvas_node.id]: x, y = self.canvas.coords(cid) @@ -46,7 +50,7 @@ class WlanConfigDialog(Dialog): ) self.ranges[cid] = range_id - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) self.config_frame = ConfigFrame(self.top, self.app, self.config) @@ -55,7 +59,7 @@ class WlanConfigDialog(Dialog): self.draw_apply_buttons() self.top.bind("", self.remove_ranges) - def draw_apply_buttons(self): + def draw_apply_buttons(self) -> None: """ create node configuration options """ @@ -75,7 +79,7 @@ class WlanConfigDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def click_apply(self): + def click_apply(self) -> None: """ retrieve user's wlan configuration and store the new configuration values """ @@ -87,7 +91,7 @@ class WlanConfigDialog(Dialog): self.remove_ranges() self.destroy() - def remove_ranges(self, event=None): + def remove_ranges(self, event=None) -> None: for cid in self.canvas.find_withtag("range"): self.canvas.delete(cid) self.ranges.clear() diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index f936bc79..833011d8 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -57,7 +57,9 @@ class CanvasNode: self.antennas: List[int] = [] self.antenna_images: Dict[int, PhotoImage] = {} # possible configurations - self.emane_model_configs: Dict[Tuple[str, Optional[int]], ConfigOption] = {} + self.emane_model_configs: Dict[ + Tuple[str, Optional[int]], Dict[str, ConfigOption] + ] = {} self.wlan_config: Dict[str, ConfigOption] = {} self.mobility_config: Dict[str, ConfigOption] = {} self.service_configs: Dict[str, NodeServiceData] = {} @@ -135,7 +137,7 @@ class CanvasNode: new_y = self._get_label_y() self.canvas.move(self.text_id, 0, new_y - prev_y) - def move(self, x: int, y: int) -> None: + def move(self, x: float, y: float) -> None: x, y = self.canvas.get_scaled_coords(x, y) current_x, current_y = self.canvas.coords(self.id) x_offset = x - current_x diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 523f8f11..75312e95 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -49,7 +49,7 @@ class Menubar(tk.Menu): self.canvas: CanvasGraph = app.canvas self.recent_menu: Optional[tk.Menu] = None self.edit_menu: Optional[tk.Menu] = None - self.observers_menu: Optional[tk.Menu] = None + self.observers_menu: Optional[ObserversMenu] = None self.draw() def draw(self) -> None: diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index 402eca4d..dbb403df 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -14,7 +14,7 @@ ANTENNA_SIZE: int = 32 class NodeDraw: def __init__(self) -> None: self.custom: bool = False - self.image: Optional[str] = None + self.image: Optional[PhotoImage] = None self.image_enum: Optional[ImageEnum] = None self.image_file: Optional[str] = None self.node_type: NodeType = None From 344f35e93e8ab9a4f5e988b652a344f77a1af774 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 22 Jun 2020 19:04:55 -0700 Subject: [PATCH 0389/1131] pygui: updated ConfigFrame to have a disabled display option, updated nodes to stil show emane config during runtime, updated emane dialog and config dialogs to be in a viewable but disabled state during runtime --- daemon/core/gui/dialogs/emaneconfig.py | 33 ++++++++++++++------------ daemon/core/gui/graph/node.py | 4 ++++ daemon/core/gui/widgets.py | 28 +++++++++++++++------- 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index df6c6125..bb334757 100644 --- a/daemon/core/gui/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -24,12 +24,15 @@ class GlobalEmaneDialog(Dialog): def __init__(self, master: tk.BaseWidget, app: "Application") -> None: super().__init__(app, "EMANE Configuration", master=master) self.config_frame: Optional[ConfigFrame] = None + self.enabled: bool = not self.app.core.is_runtime() self.draw() def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) - self.config_frame = ConfigFrame(self.top, self.app, self.app.core.emane_config) + self.config_frame = ConfigFrame( + self.top, self.app, self.app.core.emane_config, self.enabled + ) self.config_frame.draw_config() self.config_frame.grid(sticky="nsew", pady=PADY) self.draw_spacer() @@ -40,9 +43,9 @@ class GlobalEmaneDialog(Dialog): frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = ttk.Button(frame, text="Apply", command=self.click_apply) + state = tk.NORMAL if self.enabled else tk.DISABLED + button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state) button.grid(row=0, column=0, sticky="ew", padx=PADX) - button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") @@ -68,6 +71,7 @@ class EmaneModelDialog(Dialog): self.model: str = f"emane_{model}" self.iface_id: int = iface_id self.config_frame: Optional[ConfigFrame] = None + self.enabled: bool = not self.app.core.is_runtime() self.has_error: bool = False try: config = self.canvas_node.emane_model_configs.get( @@ -87,7 +91,7 @@ class EmaneModelDialog(Dialog): def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) - self.config_frame = ConfigFrame(self.top, self.app, self.config) + self.config_frame = ConfigFrame(self.top, self.app, self.config, self.enabled) self.config_frame.draw_config() self.config_frame.grid(sticky="nsew", pady=PADY) self.draw_spacer() @@ -98,9 +102,9 @@ class EmaneModelDialog(Dialog): frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = ttk.Button(frame, text="Apply", command=self.click_apply) + state = tk.NORMAL if self.enabled else tk.DISABLED + button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state) button.grid(row=0, column=0, sticky="ew", padx=PADX) - button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") @@ -124,6 +128,7 @@ class EmaneConfigDialog(Dialog): model = self.node.emane.split("_")[1] self.emane_model: tk.StringVar = tk.StringVar(value=model) self.emane_model_button: Optional[ttk.Button] = None + self.enabled: bool = not self.app.core.is_runtime() self.draw() def draw(self) -> None: @@ -140,8 +145,9 @@ class EmaneConfigDialog(Dialog): """ label = ttk.Label( self.top, - text="The EMANE emulation system provides more complex wireless radio emulation " - "\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details", + text="The EMANE emulation system provides more complex wireless radio " + "emulation \nusing pluggable MAC and PHY modules. Refer to the wiki " + "for configuration option details", justify=tk.CENTER, ) label.grid(pady=PADY) @@ -171,11 +177,9 @@ class EmaneConfigDialog(Dialog): label.grid(row=0, column=0, sticky="w") # create combo box and its binding + state = "readonly" if self.enabled else tk.DISABLED combobox = ttk.Combobox( - frame, - textvariable=self.emane_model, - values=self.emane_models, - state="readonly", + frame, textvariable=self.emane_model, values=self.emane_models, state=state ) combobox.grid(row=0, column=1, sticky="ew") combobox.bind("<>", self.emane_model_change) @@ -213,10 +217,9 @@ class EmaneConfigDialog(Dialog): frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - - button = ttk.Button(frame, text="Apply", command=self.click_apply) + state = tk.NORMAL if self.enabled else tk.DISABLED + button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state) button.grid(row=0, column=0, padx=PADX, sticky="ew") - button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 833011d8..a86ce4a3 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -202,6 +202,10 @@ class CanvasNode: is_emane = self.core_node.type == NodeType.EMANE if self.app.core.is_runtime(): self.context.add_command(label="Configure", command=self.show_config) + if is_emane: + self.context.add_command( + label="EMANE Config", command=self.show_emane_config + ) if is_wlan: self.context.add_command( label="WLAN Config", command=self.show_wlan_config diff --git a/daemon/core/gui/widgets.py b/daemon/core/gui/widgets.py index 2eded212..81bad0f5 100644 --- a/daemon/core/gui/widgets.py +++ b/daemon/core/gui/widgets.py @@ -85,12 +85,14 @@ class ConfigFrame(ttk.Notebook): master: tk.Widget, app: "Application", config: Dict[str, ConfigOption], + enabled: bool = True, **kw: Any ) -> None: super().__init__(master, **kw) self.app: "Application" = app self.config: Dict[str, ConfigOption] = config self.values: Dict[str, tk.StringVar] = {} + self.enabled: bool = enabled def draw_config(self) -> None: group_mapping = {} @@ -110,8 +112,9 @@ class ConfigFrame(ttk.Notebook): value = tk.StringVar() if option.type == core_pb2.ConfigOptionType.BOOL: select = ("On", "Off") + state = "readonly" if self.enabled else tk.DISABLED combobox = ttk.Combobox( - tab.frame, textvariable=value, values=select, state="readonly" + tab.frame, textvariable=value, values=select, state=state ) combobox.grid(row=index, column=1, sticky="ew") if option.value == "1": @@ -121,32 +124,41 @@ class ConfigFrame(ttk.Notebook): elif option.select: value.set(option.value) select = tuple(option.select) + state = "readonly" if self.enabled else tk.DISABLED combobox = ttk.Combobox( - tab.frame, textvariable=value, values=select, state="readonly" + tab.frame, textvariable=value, values=select, state=state ) combobox.grid(row=index, column=1, sticky="ew") elif option.type == core_pb2.ConfigOptionType.STRING: value.set(option.value) + state = tk.NORMAL if self.enabled else tk.DISABLED if "file" in option.label: file_frame = ttk.Frame(tab.frame) file_frame.grid(row=index, column=1, sticky="ew") file_frame.columnconfigure(0, weight=1) - entry = ttk.Entry(file_frame, textvariable=value) + entry = ttk.Entry(file_frame, textvariable=value, state=state) entry.grid(row=0, column=0, sticky="ew", padx=PADX) func = partial(file_button_click, value, self) - button = ttk.Button(file_frame, text="...", command=func) + button = ttk.Button( + file_frame, text="...", command=func, state=state + ) button.grid(row=0, column=1) else: - entry = ttk.Entry(tab.frame, textvariable=value) + entry = ttk.Entry(tab.frame, textvariable=value, state=state) entry.grid(row=index, column=1, sticky="ew") - elif option.type in INT_TYPES: value.set(option.value) - entry = validation.PositiveIntEntry(tab.frame, textvariable=value) + state = tk.NORMAL if self.enabled else tk.DISABLED + entry = validation.PositiveIntEntry( + tab.frame, textvariable=value, state=state + ) entry.grid(row=index, column=1, sticky="ew") elif option.type == core_pb2.ConfigOptionType.FLOAT: value.set(option.value) - entry = validation.PositiveFloatEntry(tab.frame, textvariable=value) + state = tk.NORMAL if self.enabled else tk.DISABLED + entry = validation.PositiveFloatEntry( + tab.frame, textvariable=value, state=state + ) entry.grid(row=index, column=1, sticky="ew") else: logging.error("unhandled config option type: %s", option.type) From 27e35a52135795c4b3e6da7cf968db08a924cad3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 22 Jun 2020 19:40:42 -0700 Subject: [PATCH 0390/1131] pygui: session options dialog is disabled during runtime --- daemon/core/gui/dialogs/sessionoptions.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/daemon/core/gui/dialogs/sessionoptions.py b/daemon/core/gui/dialogs/sessionoptions.py index 8138d854..fd021fee 100644 --- a/daemon/core/gui/dialogs/sessionoptions.py +++ b/daemon/core/gui/dialogs/sessionoptions.py @@ -1,4 +1,5 @@ import logging +import tkinter as tk from tkinter import ttk from typing import TYPE_CHECKING, Dict, Optional @@ -19,6 +20,7 @@ class SessionOptionsDialog(Dialog): self.config_frame: Optional[ConfigFrame] = None self.has_error: bool = False self.config: Dict[str, ConfigOption] = self.get_config() + self.enabled: bool = not self.app.core.is_runtime() if not self.has_error: self.draw() @@ -35,8 +37,7 @@ class SessionOptionsDialog(Dialog): def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) - - self.config_frame = ConfigFrame(self.top, self.app, config=self.config) + self.config_frame = ConfigFrame(self.top, self.app, self.config, self.enabled) self.config_frame.draw_config() self.config_frame.grid(sticky="nsew", pady=PADY) @@ -44,7 +45,8 @@ class SessionOptionsDialog(Dialog): frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = ttk.Button(frame, text="Save", command=self.save) + state = tk.NORMAL if self.enabled else tk.DISABLED + button = ttk.Button(frame, text="Save", command=self.save, state=state) button.grid(row=0, column=0, padx=PADX, sticky="ew") button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") From f39ab1dee65d811aa6aa077c4378b18a1434f830 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 22 Jun 2020 21:13:24 -0700 Subject: [PATCH 0391/1131] pygui: limit rj45 node to 1 link --- daemon/core/gui/graph/graph.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 53115750..fdf9ba21 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -428,8 +428,9 @@ class CanvasGraph(tk.Canvas): # edge dst must be a node logging.debug("current selected: %s", self.selected) + src_node = self.nodes.get(edge.src) dst_node = self.nodes.get(self.selected) - if not dst_node: + if not dst_node or not src_node: edge.delete() return @@ -444,15 +445,21 @@ class CanvasGraph(tk.Canvas): 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() + return + if NodeUtils.is_rj45_node(dst_node.core_node.type) and dst_node.edges: + edge.delete() + return + # set dst node and snap edge to center edge.complete(self.selected) self.edges[edge.token] = edge - node_src = self.nodes[edge.src] - node_src.edges.add(edge) - node_dst = self.nodes[edge.dst] - node_dst.edges.add(edge) - self.core.create_link(edge, node_src, node_dst) + src_node.edges.add(edge) + dst_node.edges.add(edge) + self.core.create_link(edge, src_node, dst_node) def select_object(self, object_id: int, choose_multiple: bool = False) -> None: """ From 2145c07cb797f4c74bf84f0114d237c84737e8a4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 22 Jun 2020 21:36:39 -0700 Subject: [PATCH 0392/1131] daemon: moved FRR_STATE_DIR from constants.py to frr service files --- daemon/core/configservices/frrservices/services.py | 4 ++-- daemon/core/constants.py.in | 1 - daemon/core/services/frr.py | 5 +++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/daemon/core/configservices/frrservices/services.py b/daemon/core/configservices/frrservices/services.py index ce8c305c..72050077 100644 --- a/daemon/core/configservices/frrservices/services.py +++ b/daemon/core/configservices/frrservices/services.py @@ -1,7 +1,6 @@ import abc from typing import Any, Dict, List -from core import constants from core.config import Configuration from core.configservice.base import ConfigService, ConfigServiceMode from core.emane.nodes import EmaneNet @@ -10,6 +9,7 @@ from core.nodes.interface import CoreInterface from core.nodes.network import WlanNode GROUP: str = "FRR" +FRR_STATE_DIR: str = "/var/run/frr" def has_mtu_mismatch(iface: CoreInterface) -> bool: @@ -110,7 +110,7 @@ class FRRZebra(ConfigService): frr_conf=frr_conf, frr_sbin_search=frr_sbin_search, frr_bin_search=frr_bin_search, - frr_state_dir=constants.FRR_STATE_DIR, + frr_state_dir=FRR_STATE_DIR, ifaces=ifaces, want_ip4=want_ip4, want_ip6=want_ip6, diff --git a/daemon/core/constants.py.in b/daemon/core/constants.py.in index 54f3a1c3..4bf600f3 100644 --- a/daemon/core/constants.py.in +++ b/daemon/core/constants.py.in @@ -4,7 +4,6 @@ COREDPY_VERSION = "@PACKAGE_VERSION@" CORE_CONF_DIR = "@CORE_CONF_DIR@" CORE_DATA_DIR = "@CORE_DATA_DIR@" QUAGGA_STATE_DIR = "@CORE_STATE_DIR@/run/quagga" -FRR_STATE_DIR = "@CORE_STATE_DIR@/run/frr" VNODED_BIN = which("vnoded", required=True) VCMD_BIN = which("vcmd", required=True) diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py index 13569772..ceb04f93 100644 --- a/daemon/core/services/frr.py +++ b/daemon/core/services/frr.py @@ -6,7 +6,6 @@ from typing import Optional, Tuple import netaddr -from core import constants from core.emane.nodes import EmaneNet from core.nodes.base import CoreNode from core.nodes.interface import CoreInterface @@ -14,6 +13,8 @@ from core.nodes.network import PtpNet, WlanNode from core.nodes.physical import Rj45Node from core.services.coreservices import CoreService +FRR_STATE_DIR: str = "/var/run/frr" + class FRRZebra(CoreService): name: str = "FRRzebra" @@ -236,7 +237,7 @@ bootfrr cls.configs[0], frr_sbin_search, frr_bin_search, - constants.FRR_STATE_DIR, + FRR_STATE_DIR, ) for iface in node.get_ifaces(): cfg += f"ip link set dev {iface.name} down\n" From 1ef66181c6674c5352693141395053a9f137ca07 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 22 Jun 2020 21:39:29 -0700 Subject: [PATCH 0393/1131] daemon: moved QUAGGA_STATE_DIR from constants.py to quagga service files --- daemon/core/configservices/quaggaservices/services.py | 4 ++-- daemon/core/constants.py.in | 1 - daemon/core/services/quagga.py | 5 +++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/daemon/core/configservices/quaggaservices/services.py b/daemon/core/configservices/quaggaservices/services.py index e18e8a1a..19430664 100644 --- a/daemon/core/configservices/quaggaservices/services.py +++ b/daemon/core/configservices/quaggaservices/services.py @@ -2,7 +2,6 @@ import abc import logging from typing import Any, Dict, List -from core import constants from core.config import Configuration from core.configservice.base import ConfigService, ConfigServiceMode from core.emane.nodes import EmaneNet @@ -11,6 +10,7 @@ from core.nodes.interface import CoreInterface from core.nodes.network import WlanNode GROUP: str = "Quagga" +QUAGGA_STATE_DIR: str = "/var/run/quagga" def has_mtu_mismatch(iface: CoreInterface) -> bool: @@ -79,7 +79,7 @@ class Zebra(ConfigService): quagga_sbin_search = self.node.session.options.get_config( "quagga_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/quagga" ).strip('"') - quagga_state_dir = constants.QUAGGA_STATE_DIR + quagga_state_dir = QUAGGA_STATE_DIR quagga_conf = self.files[0] services = [] diff --git a/daemon/core/constants.py.in b/daemon/core/constants.py.in index 4bf600f3..dfefb128 100644 --- a/daemon/core/constants.py.in +++ b/daemon/core/constants.py.in @@ -3,7 +3,6 @@ from core.utils import which COREDPY_VERSION = "@PACKAGE_VERSION@" CORE_CONF_DIR = "@CORE_CONF_DIR@" CORE_DATA_DIR = "@CORE_DATA_DIR@" -QUAGGA_STATE_DIR = "@CORE_STATE_DIR@/run/quagga" VNODED_BIN = which("vnoded", required=True) VCMD_BIN = which("vcmd", required=True) diff --git a/daemon/core/services/quagga.py b/daemon/core/services/quagga.py index cb9e6b08..9e2c7cc0 100644 --- a/daemon/core/services/quagga.py +++ b/daemon/core/services/quagga.py @@ -5,7 +5,6 @@ from typing import Optional, Tuple import netaddr -from core import constants from core.emane.nodes import EmaneNet from core.emulator.enumerations import LinkTypes from core.nodes.base import CoreNode @@ -14,6 +13,8 @@ from core.nodes.network import PtpNet, WlanNode from core.nodes.physical import Rj45Node from core.services.coreservices import CoreService +QUAGGA_STATE_DIR: str = "/var/run/quagga" + class Zebra(CoreService): name: str = "zebra" @@ -226,7 +227,7 @@ bootquagga cls.configs[0], quagga_sbin_search, quagga_bin_search, - constants.QUAGGA_STATE_DIR, + QUAGGA_STATE_DIR, ) From c43dd60a42c36bc2a2516ff54deeda0ec2438136 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 22 Jun 2020 21:47:03 -0700 Subject: [PATCH 0394/1131] daemon: small adjustment in sdt.py --- daemon/core/plugins/sdt.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index ef36b0a4..27e54ff3 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -8,8 +8,7 @@ import threading from typing import IO, TYPE_CHECKING, Dict, Optional, Set, Tuple from urllib.parse import urlparse -from core import constants -from core.constants import CORE_DATA_DIR +from core.constants import CORE_CONF_DIR, CORE_DATA_DIR from core.emane.nodes import EmaneNet from core.emulator.data import LinkData, NodeData from core.emulator.enumerations import EventTypes, MessageFlags @@ -264,8 +263,8 @@ class Sdt: icon = node.icon if icon: node_type = node.name - icon = icon.replace("$CORE_DATA_DIR", constants.CORE_DATA_DIR) - icon = icon.replace("$CORE_CONF_DIR", constants.CORE_CONF_DIR) + icon = icon.replace("$CORE_DATA_DIR", CORE_DATA_DIR) + icon = icon.replace("$CORE_CONF_DIR", CORE_CONF_DIR) self.cmd(f"sprite {node_type} image {icon}") self.cmd( f'node {node.id} nodeLayer "{NODE_LAYER}" ' From e0c9f9c8326228aed4134a51cb391fb5cb650d42 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 23 Jun 2020 09:11:37 -0700 Subject: [PATCH 0395/1131] daemon: moved executable check to CoreEmu and separated them into their own module core.executables --- daemon/core/configservice/manager.py | 6 ++---- daemon/core/constants.py.in | 14 -------------- daemon/core/emulator/coreemu.py | 28 +++++++++++++++++++++++++++- daemon/core/executables.py | 24 ++++++++++++++++++++++++ daemon/core/nodes/base.py | 4 ++-- daemon/core/nodes/client.py | 2 +- daemon/core/nodes/netclient.py | 2 +- daemon/core/nodes/network.py | 2 +- daemon/core/nodes/physical.py | 4 ++-- daemon/core/services/coreservices.py | 10 ++++++---- daemon/core/services/utility.py | 17 ++++++----------- daemon/core/utils.py | 4 ++-- daemon/core/xml/corexmldeployment.py | 2 +- 13 files changed, 75 insertions(+), 44 deletions(-) create mode 100644 daemon/core/executables.py diff --git a/daemon/core/configservice/manager.py b/daemon/core/configservice/manager.py index ecea6e68..83657655 100644 --- a/daemon/core/configservice/manager.py +++ b/daemon/core/configservice/manager.py @@ -52,10 +52,8 @@ class ConfigServiceManager: for executable in service.executables: try: utils.which(executable, required=True) - except ValueError: - raise CoreError( - f"service({service.name}) missing executable {executable}" - ) + except CoreError as e: + raise CoreError(f"config service({service.name}): {e}") # make service available self.services[name] = service diff --git a/daemon/core/constants.py.in b/daemon/core/constants.py.in index dfefb128..cb566e40 100644 --- a/daemon/core/constants.py.in +++ b/daemon/core/constants.py.in @@ -1,17 +1,3 @@ -from core.utils import which - COREDPY_VERSION = "@PACKAGE_VERSION@" CORE_CONF_DIR = "@CORE_CONF_DIR@" CORE_DATA_DIR = "@CORE_DATA_DIR@" - -VNODED_BIN = which("vnoded", required=True) -VCMD_BIN = which("vcmd", required=True) -SYSCTL_BIN = which("sysctl", required=True) -IP_BIN = which("ip", required=True) -ETHTOOL_BIN = which("ethtool", required=True) -TC_BIN = which("tc", required=True) -EBTABLES_BIN = which("ebtables", required=True) -MOUNT_BIN = which("mount", required=True) -UMOUNT_BIN = which("umount", required=True) -OVS_BIN = which("ovs-vsctl", required=False) -OVS_FLOW_BIN = which("ovs-ofctl", required=False) diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py index 6a7f8b80..86652013 100644 --- a/daemon/core/emulator/coreemu.py +++ b/daemon/core/emulator/coreemu.py @@ -6,9 +6,10 @@ import sys from typing import Dict, List, Type import core.services -from core import configservices +from core import configservices, utils from core.configservice.manager import ConfigServiceManager from core.emulator.session import Session +from core.executables import COMMON_REQUIREMENTS, OVS_REQUIREMENTS, VCMD_REQUIREMENTS from core.services.coreservices import ServiceManager @@ -65,10 +66,35 @@ class CoreEmu: if custom_dir: self.service_manager.load(custom_dir) + # check executables exist on path + self._validate_env() + # catch exit event atexit.register(self.shutdown) + def _validate_env(self) -> None: + """ + Validates executables CORE depends on exist on path. + + :return: nothing + :raises core.errors.CoreError: when an executable does not exist on path + """ + for requirement in COMMON_REQUIREMENTS: + utils.which(requirement, required=True) + use_ovs = self.config.get("ovs") == "True" + if use_ovs: + for requirement in OVS_REQUIREMENTS: + utils.which(requirement, required=True) + else: + for requirement in VCMD_REQUIREMENTS: + utils.which(requirement, required=True) + def load_services(self) -> None: + """ + Loads default and custom services for use within CORE. + + :return: nothing + """ # load default services self.service_errors = core.services.load() diff --git a/daemon/core/executables.py b/daemon/core/executables.py new file mode 100644 index 00000000..00d9b40f --- /dev/null +++ b/daemon/core/executables.py @@ -0,0 +1,24 @@ +from typing import List + +VNODED_BIN: str = "vnoded" +VCMD_BIN: str = "vcmd" +SYSCTL_BIN: str = "sysctl" +IP_BIN: str = "ip" +ETHTOOL_BIN: str = "ethtool" +TC_BIN: str = "tc" +EBTABLES_BIN: str = "ebtables" +MOUNT_BIN: str = "mount" +UMOUNT_BIN: str = "umount" +OVS_BIN: str = "ovs-vsctl" + +COMMON_REQUIREMENTS: List[str] = [ + SYSCTL_BIN, + IP_BIN, + ETHTOOL_BIN, + TC_BIN, + EBTABLES_BIN, + MOUNT_BIN, + UMOUNT_BIN, +] +VCMD_REQUIREMENTS: List[str] = [VNODED_BIN, VCMD_BIN] +OVS_REQUIREMENTS: List[str] = [OVS_BIN] diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index aae59b70..a691e4f5 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -13,10 +13,10 @@ import netaddr from core import utils from core.configservice.dependencies import ConfigServiceDependencies -from core.constants import MOUNT_BIN, VNODED_BIN from core.emulator.data import InterfaceData, LinkData, LinkOptions from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes from core.errors import CoreCommandError, CoreError +from core.executables import MOUNT_BIN, VNODED_BIN from core.nodes.client import VnodeClient from core.nodes.interface import CoreInterface, TunTap, Veth from core.nodes.netclient import LinuxNetClient, get_net_client @@ -753,7 +753,7 @@ class CoreNode(CoreNodeBase): iface = self.get_iface(iface_id) iface.set_mac(mac) if self.up: - self.node_net_client.device_mac(iface.name, mac) + self.node_net_client.device_mac(iface.name, str(iface.mac)) def add_ip(self, iface_id: int, ip: str) -> None: """ diff --git a/daemon/core/nodes/client.py b/daemon/core/nodes/client.py index c004b814..f8cd3813 100644 --- a/daemon/core/nodes/client.py +++ b/daemon/core/nodes/client.py @@ -5,7 +5,7 @@ The control channel can be accessed via calls using the vcmd shell. """ from core import utils -from core.constants import VCMD_BIN +from core.executables import VCMD_BIN class VnodeClient: diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index b6c164b5..4486bd8f 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -5,7 +5,7 @@ from typing import Callable import netaddr -from core.constants import ETHTOOL_BIN, IP_BIN, OVS_BIN, SYSCTL_BIN, TC_BIN +from core.executables import ETHTOOL_BIN, IP_BIN, OVS_BIN, SYSCTL_BIN, TC_BIN class LinuxNetClient: diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 7d8f805e..2c0c1cca 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -10,7 +10,6 @@ from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Type import netaddr from core import utils -from core.constants import EBTABLES_BIN, TC_BIN from core.emulator.data import InterfaceData, LinkData, LinkOptions from core.emulator.enumerations import ( LinkTypes, @@ -20,6 +19,7 @@ from core.emulator.enumerations import ( RegisterTlvs, ) from core.errors import CoreCommandError, CoreError +from core.executables import EBTABLES_BIN, TC_BIN from core.nodes.base import CoreNetworkBase from core.nodes.interface import CoreInterface, GreTap, Veth from core.nodes.netclient import get_net_client diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 3751d9ee..a025a496 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -7,11 +7,11 @@ import os import threading from typing import IO, TYPE_CHECKING, List, Optional, Tuple -from core.constants import MOUNT_BIN, UMOUNT_BIN from core.emulator.data import InterfaceData, LinkOptions from core.emulator.distributed import DistributedServer from core.emulator.enumerations import NodeTypes, TransportType from core.errors import CoreCommandError, CoreError +from core.executables import MOUNT_BIN, UMOUNT_BIN from core.nodes.base import CoreNetworkBase, CoreNodeBase from core.nodes.interface import CoreInterface from core.nodes.network import CoreNetwork, GreTap @@ -76,7 +76,7 @@ class PhysicalNode(CoreNodeBase): iface = self.get_iface(iface_id) iface.set_mac(mac) if self.up: - self.net_client.device_mac(iface.name, mac) + self.net_client.device_mac(iface.name, str(iface.mac)) def add_ip(self, iface_id: int, ip: str) -> None: """ diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index d22bc7a5..8c41c57d 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -13,10 +13,9 @@ import time from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple, Type from core import utils -from core.constants import which from core.emulator.data import FileData from core.emulator.enumerations import ExceptionLevels, MessageFlags, RegisterTlvs -from core.errors import CoreCommandError +from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNode if TYPE_CHECKING: @@ -262,7 +261,10 @@ class ServiceManager: # validate dependent executables are present for executable in service.executables: - which(executable, required=True) + try: + utils.which(executable, required=True) + except CoreError as e: + raise CoreError(f"service({name}): {e}") # validate service on load succeeds try: @@ -300,7 +302,7 @@ class ServiceManager: try: cls.add(service) - except ValueError as e: + except (CoreError, ValueError) as e: service_errors.append(service.name) logging.debug("not loading service(%s): %s", service.name, e) return service_errors diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index 414f994e..cf76b092 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -5,8 +5,9 @@ from typing import Optional, Tuple import netaddr -from core import constants, utils +from core import utils from core.errors import CoreCommandError +from core.executables import SYSCTL_BIN from core.nodes.base import CoreNode from core.services.coreservices import CoreService, ServiceMode @@ -47,19 +48,13 @@ class IPForwardService(UtilService): %(sysctl)s -w net.ipv4.conf.all.rp_filter=0 %(sysctl)s -w net.ipv4.conf.default.rp_filter=0 """ % { - "sysctl": constants.SYSCTL_BIN + "sysctl": SYSCTL_BIN } for iface in node.get_ifaces(): name = utils.sysctl_devname(iface.name) - cfg += "%s -w net.ipv4.conf.%s.forwarding=1\n" % ( - constants.SYSCTL_BIN, - name, - ) - cfg += "%s -w net.ipv4.conf.%s.send_redirects=0\n" % ( - constants.SYSCTL_BIN, - name, - ) - cfg += "%s -w net.ipv4.conf.%s.rp_filter=0\n" % (constants.SYSCTL_BIN, name) + cfg += "%s -w net.ipv4.conf.%s.forwarding=1\n" % (SYSCTL_BIN, name) + cfg += "%s -w net.ipv4.conf.%s.send_redirects=0\n" % (SYSCTL_BIN, name) + cfg += "%s -w net.ipv4.conf.%s.rp_filter=0\n" % (SYSCTL_BIN, name) return cfg diff --git a/daemon/core/utils.py b/daemon/core/utils.py index 0e082187..459b7d56 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -33,7 +33,7 @@ from typing import ( import netaddr -from core.errors import CoreCommandError +from core.errors import CoreCommandError, CoreError if TYPE_CHECKING: from core.emulator.session import Session @@ -154,7 +154,7 @@ def which(command: str, required: bool) -> str: """ found_path = shutil.which(command) if found_path is None and required: - raise ValueError(f"failed to find required executable({command}) in path") + raise CoreError(f"failed to find required executable({command}) in path") return found_path diff --git a/daemon/core/xml/corexmldeployment.py b/daemon/core/xml/corexmldeployment.py index 6035bd26..2235a798 100644 --- a/daemon/core/xml/corexmldeployment.py +++ b/daemon/core/xml/corexmldeployment.py @@ -6,8 +6,8 @@ import netaddr from lxml import etree from core import utils -from core.constants import IP_BIN from core.emane.nodes import EmaneNet +from core.executables import IP_BIN from core.nodes.base import CoreNodeBase, NodeBase from core.nodes.interface import CoreInterface From 8f19ad057c5305c9e24975a4462e80f11c08228a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 23 Jun 2020 09:24:40 -0700 Subject: [PATCH 0396/1131] daemon: cleaned up requirement check, updated github workflow to modify correct file --- .github/workflows/daemon-checks.yml | 2 +- daemon/core/emulator/coreemu.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/daemon-checks.yml b/.github/workflows/daemon-checks.yml index 85409568..d955ee58 100644 --- a/.github/workflows/daemon-checks.yml +++ b/.github/workflows/daemon-checks.yml @@ -18,7 +18,7 @@ jobs: cd daemon cp setup.py.in setup.py cp core/constants.py.in core/constants.py - sed -i 's/True/False/g' core/constants.py + sed -i 's/required=True/required=False/g' core/emulator/coreemu.py pipenv sync --dev - name: isort run: | diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py index 86652013..71723268 100644 --- a/daemon/core/emulator/coreemu.py +++ b/daemon/core/emulator/coreemu.py @@ -79,15 +79,14 @@ class CoreEmu: :return: nothing :raises core.errors.CoreError: when an executable does not exist on path """ - for requirement in COMMON_REQUIREMENTS: - utils.which(requirement, required=True) + requirements = COMMON_REQUIREMENTS use_ovs = self.config.get("ovs") == "True" if use_ovs: - for requirement in OVS_REQUIREMENTS: - utils.which(requirement, required=True) + requirements += OVS_REQUIREMENTS else: - for requirement in VCMD_REQUIREMENTS: - utils.which(requirement, required=True) + requirements += VCMD_REQUIREMENTS + for requirement in requirements: + utils.which(requirement, required=True) def load_services(self) -> None: """ From 6dd6bc87abcd0045e0a729ee871499c60a31f3cc Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 23 Jun 2020 09:35:11 -0700 Subject: [PATCH 0397/1131] daemon: renamed executable variables to be simpler --- daemon/core/executables.py | 34 +++++------- daemon/core/nodes/base.py | 6 +-- daemon/core/nodes/client.py | 4 +- daemon/core/nodes/netclient.py | 78 ++++++++++++++-------------- daemon/core/nodes/network.py | 18 +++---- daemon/core/nodes/physical.py | 6 +-- daemon/core/services/utility.py | 10 ++-- daemon/core/xml/corexmldeployment.py | 4 +- 8 files changed, 74 insertions(+), 86 deletions(-) diff --git a/daemon/core/executables.py b/daemon/core/executables.py index 00d9b40f..17aecc1d 100644 --- a/daemon/core/executables.py +++ b/daemon/core/executables.py @@ -1,24 +1,16 @@ from typing import List -VNODED_BIN: str = "vnoded" -VCMD_BIN: str = "vcmd" -SYSCTL_BIN: str = "sysctl" -IP_BIN: str = "ip" -ETHTOOL_BIN: str = "ethtool" -TC_BIN: str = "tc" -EBTABLES_BIN: str = "ebtables" -MOUNT_BIN: str = "mount" -UMOUNT_BIN: str = "umount" -OVS_BIN: str = "ovs-vsctl" +VNODED: str = "vnoded" +VCMD: str = "vcmd" +SYSCTL: str = "sysctl" +IP: str = "ip" +ETHTOOL: str = "ethtool" +TC: str = "tc" +EBTABLES: str = "ebtables" +MOUNT: str = "mount" +UMOUNT: str = "umount" +OVS_VSCTL: str = "ovs-vsctl" -COMMON_REQUIREMENTS: List[str] = [ - SYSCTL_BIN, - IP_BIN, - ETHTOOL_BIN, - TC_BIN, - EBTABLES_BIN, - MOUNT_BIN, - UMOUNT_BIN, -] -VCMD_REQUIREMENTS: List[str] = [VNODED_BIN, VCMD_BIN] -OVS_REQUIREMENTS: List[str] = [OVS_BIN] +COMMON_REQUIREMENTS: List[str] = [SYSCTL, IP, ETHTOOL, TC, EBTABLES, MOUNT, UMOUNT] +VCMD_REQUIREMENTS: List[str] = [VNODED, VCMD] +OVS_REQUIREMENTS: List[str] = [OVS_VSCTL] diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index a691e4f5..3999046d 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -16,7 +16,7 @@ from core.configservice.dependencies import ConfigServiceDependencies from core.emulator.data import InterfaceData, LinkData, LinkOptions from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes from core.errors import CoreCommandError, CoreError -from core.executables import MOUNT_BIN, VNODED_BIN +from core.executables import MOUNT, VNODED from core.nodes.client import VnodeClient from core.nodes.interface import CoreInterface, TunTap, Veth from core.nodes.netclient import LinuxNetClient, get_net_client @@ -511,7 +511,7 @@ class CoreNode(CoreNodeBase): # create a new namespace for this node using vnoded vnoded = ( - f"{VNODED_BIN} -v -c {self.ctrlchnlname} -l {self.ctrlchnlname}.log " + f"{VNODED} -v -c {self.ctrlchnlname} -l {self.ctrlchnlname}.log " f"-p {self.ctrlchnlname}.pid" ) if self.nodedir: @@ -640,7 +640,7 @@ class CoreNode(CoreNodeBase): source = os.path.abspath(source) logging.debug("node(%s) mounting: %s at %s", self.name, source, target) self.cmd(f"mkdir -p {target}") - self.cmd(f"{MOUNT_BIN} -n --bind {source} {target}") + self.cmd(f"{MOUNT} -n --bind {source} {target}") self._mounts.append((source, target)) def next_iface_id(self) -> int: diff --git a/daemon/core/nodes/client.py b/daemon/core/nodes/client.py index f8cd3813..93e099cf 100644 --- a/daemon/core/nodes/client.py +++ b/daemon/core/nodes/client.py @@ -5,7 +5,7 @@ The control channel can be accessed via calls using the vcmd shell. """ from core import utils -from core.executables import VCMD_BIN +from core.executables import VCMD class VnodeClient: @@ -50,7 +50,7 @@ class VnodeClient: pass def create_cmd(self, args: str) -> str: - return f"{VCMD_BIN} -c {self.ctrlchnlname} -- {args}" + return f"{VCMD} -c {self.ctrlchnlname} -- {args}" def check_cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: """ diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 4486bd8f..96a1f4be 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -5,7 +5,7 @@ from typing import Callable import netaddr -from core.executables import ETHTOOL_BIN, IP_BIN, OVS_BIN, SYSCTL_BIN, TC_BIN +from core.executables import ETHTOOL, IP, OVS_VSCTL, SYSCTL, TC class LinuxNetClient: @@ -38,7 +38,7 @@ class LinuxNetClient: :param device: device to add route to :return: nothing """ - self.run(f"{IP_BIN} route add {route} dev {device}") + self.run(f"{IP} route add {route} dev {device}") def device_up(self, device: str) -> None: """ @@ -47,7 +47,7 @@ class LinuxNetClient: :param device: device to bring up :return: nothing """ - self.run(f"{IP_BIN} link set {device} up") + self.run(f"{IP} link set {device} up") def device_down(self, device: str) -> None: """ @@ -56,7 +56,7 @@ class LinuxNetClient: :param device: device to bring down :return: nothing """ - self.run(f"{IP_BIN} link set {device} down") + self.run(f"{IP} link set {device} down") def device_name(self, device: str, name: str) -> None: """ @@ -66,7 +66,7 @@ class LinuxNetClient: :param name: name to set :return: nothing """ - self.run(f"{IP_BIN} link set {device} name {name}") + self.run(f"{IP} link set {device} name {name}") def device_show(self, device: str) -> str: """ @@ -75,7 +75,7 @@ class LinuxNetClient: :param device: device to get information for :return: device information """ - return self.run(f"{IP_BIN} link show {device}") + return self.run(f"{IP} link show {device}") def address_show(self, device: str) -> str: """ @@ -84,7 +84,7 @@ class LinuxNetClient: :param device: device name :return: address information """ - return self.run(f"{IP_BIN} address show {device}") + return self.run(f"{IP} address show {device}") def get_mac(self, device: str) -> str: """ @@ -112,7 +112,7 @@ class LinuxNetClient: :param namespace: namespace to set device to :return: nothing """ - self.run(f"{IP_BIN} link set {device} netns {namespace}") + self.run(f"{IP} link set {device} netns {namespace}") def device_flush(self, device: str) -> None: """ @@ -123,7 +123,7 @@ class LinuxNetClient: """ self.run( f"[ -e /sys/class/net/{device} ] && " - f"{IP_BIN} address flush dev {device} || true", + f"{IP} address flush dev {device} || true", shell=True, ) @@ -135,7 +135,7 @@ class LinuxNetClient: :param mac: mac to set :return: nothing """ - self.run(f"{IP_BIN} link set dev {device} address {mac}") + self.run(f"{IP} link set dev {device} address {mac}") def delete_device(self, device: str) -> None: """ @@ -144,7 +144,7 @@ class LinuxNetClient: :param device: device to delete :return: nothing """ - self.run(f"{IP_BIN} link delete {device}") + self.run(f"{IP} link delete {device}") def delete_tc(self, device: str) -> None: """ @@ -153,7 +153,7 @@ class LinuxNetClient: :param device: device to remove tc :return: nothing """ - self.run(f"{TC_BIN} qdisc delete dev {device} root") + self.run(f"{TC} qdisc delete dev {device} root") def checksums_off(self, iface_name: str) -> None: """ @@ -162,7 +162,7 @@ class LinuxNetClient: :param iface_name: interface to update :return: nothing """ - self.run(f"{ETHTOOL_BIN} -K {iface_name} rx off tx off") + self.run(f"{ETHTOOL} -K {iface_name} rx off tx off") def create_address(self, device: str, address: str, broadcast: str = None) -> None: """ @@ -174,15 +174,13 @@ class LinuxNetClient: :return: nothing """ if broadcast is not None: - self.run( - f"{IP_BIN} address add {address} broadcast {broadcast} dev {device}" - ) + self.run(f"{IP} address add {address} broadcast {broadcast} dev {device}") else: - self.run(f"{IP_BIN} address add {address} dev {device}") + self.run(f"{IP} address add {address} dev {device}") if netaddr.valid_ipv6(address.split("/")[0]): # IPv6 addresses are removed by default on interface down. # Make sure that the IPv6 address we add is not removed - self.run(f"{SYSCTL_BIN} -w net.ipv6.conf.{device}.keep_addr_on_down=1") + self.run(f"{SYSCTL} -w net.ipv6.conf.{device}.keep_addr_on_down=1") def delete_address(self, device: str, address: str) -> None: """ @@ -192,7 +190,7 @@ class LinuxNetClient: :param address: address to remove :return: nothing """ - self.run(f"{IP_BIN} address delete {address} dev {device}") + self.run(f"{IP} address delete {address} dev {device}") def create_veth(self, name: str, peer: str) -> None: """ @@ -202,7 +200,7 @@ class LinuxNetClient: :param peer: peer name :return: nothing """ - self.run(f"{IP_BIN} link add name {name} type veth peer name {peer}") + self.run(f"{IP} link add name {name} type veth peer name {peer}") def create_gretap( self, device: str, address: str, local: str, ttl: int, key: int @@ -217,7 +215,7 @@ class LinuxNetClient: :param key: key for tap :return: nothing """ - cmd = f"{IP_BIN} link add {device} type gretap remote {address}" + cmd = f"{IP} link add {device} type gretap remote {address}" if local is not None: cmd += f" local {local}" if ttl is not None: @@ -233,11 +231,11 @@ class LinuxNetClient: :param name: bridge name :return: nothing """ - self.run(f"{IP_BIN} link add name {name} type bridge") - self.run(f"{IP_BIN} link set {name} type bridge stp_state 0") - self.run(f"{IP_BIN} link set {name} type bridge forward_delay 0") - self.run(f"{IP_BIN} link set {name} type bridge mcast_snooping 0") - self.run(f"{IP_BIN} link set {name} type bridge group_fwd_mask 65528") + self.run(f"{IP} link add name {name} type bridge") + self.run(f"{IP} link set {name} type bridge stp_state 0") + self.run(f"{IP} link set {name} type bridge forward_delay 0") + self.run(f"{IP} link set {name} type bridge mcast_snooping 0") + self.run(f"{IP} link set {name} type bridge group_fwd_mask 65528") self.device_up(name) def delete_bridge(self, name: str) -> None: @@ -248,7 +246,7 @@ class LinuxNetClient: :return: nothing """ self.device_down(name) - self.run(f"{IP_BIN} link delete {name} type bridge") + self.run(f"{IP} link delete {name} type bridge") def set_iface_master(self, bridge_name: str, iface_name: str) -> None: """ @@ -258,7 +256,7 @@ class LinuxNetClient: :param iface_name: interface name :return: nothing """ - self.run(f"{IP_BIN} link set dev {iface_name} master {bridge_name}") + self.run(f"{IP} link set dev {iface_name} master {bridge_name}") self.device_up(iface_name) def delete_iface(self, bridge_name: str, iface_name: str) -> None: @@ -269,7 +267,7 @@ class LinuxNetClient: :param iface_name: interface name :return: nothing """ - self.run(f"{IP_BIN} link set dev {iface_name} nomaster") + self.run(f"{IP} link set dev {iface_name} nomaster") def existing_bridges(self, _id: int) -> bool: """ @@ -278,7 +276,7 @@ class LinuxNetClient: :param _id: node id to check bridges for :return: True if there are existing bridges, False otherwise """ - output = self.run(f"{IP_BIN} -o link show type bridge") + output = self.run(f"{IP} -o link show type bridge") lines = output.split("\n") for line in lines: values = line.split(":") @@ -299,7 +297,7 @@ class LinuxNetClient: :param name: bridge name :return: nothing """ - self.run(f"{IP_BIN} link set {name} type bridge ageing_time 0") + self.run(f"{IP} link set {name} type bridge ageing_time 0") class OvsNetClient(LinuxNetClient): @@ -314,10 +312,10 @@ class OvsNetClient(LinuxNetClient): :param name: bridge name :return: nothing """ - self.run(f"{OVS_BIN} add-br {name}") - self.run(f"{OVS_BIN} set bridge {name} stp_enable=false") - self.run(f"{OVS_BIN} set bridge {name} other_config:stp-max-age=6") - self.run(f"{OVS_BIN} set bridge {name} other_config:stp-forward-delay=4") + self.run(f"{OVS_VSCTL} add-br {name}") + self.run(f"{OVS_VSCTL} set bridge {name} stp_enable=false") + self.run(f"{OVS_VSCTL} set bridge {name} other_config:stp-max-age=6") + self.run(f"{OVS_VSCTL} set bridge {name} other_config:stp-forward-delay=4") self.device_up(name) def delete_bridge(self, name: str) -> None: @@ -328,7 +326,7 @@ class OvsNetClient(LinuxNetClient): :return: nothing """ self.device_down(name) - self.run(f"{OVS_BIN} del-br {name}") + self.run(f"{OVS_VSCTL} del-br {name}") def set_iface_master(self, bridge_name: str, iface_name: str) -> None: """ @@ -338,7 +336,7 @@ class OvsNetClient(LinuxNetClient): :param iface_name: interface name :return: nothing """ - self.run(f"{OVS_BIN} add-port {bridge_name} {iface_name}") + self.run(f"{OVS_VSCTL} add-port {bridge_name} {iface_name}") self.device_up(iface_name) def delete_iface(self, bridge_name: str, iface_name: str) -> None: @@ -349,7 +347,7 @@ class OvsNetClient(LinuxNetClient): :param iface_name: interface name :return: nothing """ - self.run(f"{OVS_BIN} del-port {bridge_name} {iface_name}") + self.run(f"{OVS_VSCTL} del-port {bridge_name} {iface_name}") def existing_bridges(self, _id: int) -> bool: """ @@ -358,7 +356,7 @@ class OvsNetClient(LinuxNetClient): :param _id: node id to check bridges for :return: True if there are existing bridges, False otherwise """ - output = self.run(f"{OVS_BIN} list-br") + output = self.run(f"{OVS_VSCTL} list-br") if output: for line in output.split("\n"): fields = line.split(".") @@ -373,7 +371,7 @@ class OvsNetClient(LinuxNetClient): :param name: bridge name :return: nothing """ - self.run(f"{OVS_BIN} set bridge {name} other_config:mac-aging-time=0") + self.run(f"{OVS_VSCTL} set bridge {name} other_config:mac-aging-time=0") def get_net_client(use_ovs: bool, run: Callable[..., str]) -> LinuxNetClient: diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 2c0c1cca..d418a42c 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -19,7 +19,7 @@ from core.emulator.enumerations import ( RegisterTlvs, ) from core.errors import CoreCommandError, CoreError -from core.executables import EBTABLES_BIN, TC_BIN +from core.executables import EBTABLES, TC from core.nodes.base import CoreNetworkBase from core.nodes.interface import CoreInterface, GreTap, Veth from core.nodes.netclient import get_net_client @@ -104,7 +104,7 @@ class EbtablesQueue: :param cmd: ebtable command :return: ebtable atomic command """ - return f"{EBTABLES_BIN} --atomic-file {self.atomic_file} {cmd}" + return f"{EBTABLES} --atomic-file {self.atomic_file} {cmd}" def lastupdate(self, wlan: "CoreNetwork") -> float: """ @@ -338,8 +338,8 @@ class CoreNetwork(CoreNetworkBase): self.net_client.delete_bridge(self.brname) if self.has_ebtables_chain: cmds = [ - f"{EBTABLES_BIN} -D FORWARD --logical-in {self.brname} -j {self.brname}", - f"{EBTABLES_BIN} -X {self.brname}", + f"{EBTABLES} -D FORWARD --logical-in {self.brname} -j {self.brname}", + f"{EBTABLES} -X {self.brname}", ] ebtablescmds(self.host_cmd, cmds) except CoreCommandError: @@ -448,7 +448,7 @@ class CoreNetwork(CoreNetworkBase): :return: nothing """ devname = iface.localname - tc = f"{TC_BIN} qdisc replace dev {devname}" + tc = f"{TC} qdisc replace dev {devname}" parent = "root" changed = False bw = options.bandwidth @@ -466,7 +466,7 @@ class CoreNetwork(CoreNetworkBase): changed = True elif iface.getparam("has_tbf") and bw <= 0: if self.up: - cmd = f"{TC_BIN} qdisc delete dev {devname} {parent}" + cmd = f"{TC} qdisc delete dev {devname} {parent}" iface.host_cmd(cmd) iface.setparam("has_tbf", False) # removing the parent removes the child @@ -512,14 +512,12 @@ class CoreNetwork(CoreNetworkBase): if not iface.getparam("has_netem"): return if self.up: - cmd = f"{TC_BIN} qdisc delete dev {devname} {parent} handle 10:" + cmd = f"{TC} qdisc delete dev {devname} {parent} handle 10:" iface.host_cmd(cmd) iface.setparam("has_netem", False) elif len(netem) > 1: if self.up: - cmd = ( - f"{TC_BIN} qdisc replace dev {devname} {parent} handle 10: {netem}" - ) + cmd = f"{TC} qdisc replace dev {devname} {parent} handle 10: {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 a025a496..f48a0d10 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -11,7 +11,7 @@ from core.emulator.data import InterfaceData, LinkOptions from core.emulator.distributed import DistributedServer from core.emulator.enumerations import NodeTypes, TransportType from core.errors import CoreCommandError, CoreError -from core.executables import MOUNT_BIN, UMOUNT_BIN +from core.executables import MOUNT, UMOUNT from core.nodes.base import CoreNetworkBase, CoreNodeBase from core.nodes.interface import CoreInterface from core.nodes.network import CoreNetwork, GreTap @@ -186,13 +186,13 @@ class PhysicalNode(CoreNodeBase): source = os.path.abspath(source) logging.info("mounting %s at %s", source, target) os.makedirs(target) - self.host_cmd(f"{MOUNT_BIN} --bind {source} {target}", cwd=self.nodedir) + self.host_cmd(f"{MOUNT} --bind {source} {target}", cwd=self.nodedir) self._mounts.append((source, target)) def umount(self, target: str) -> None: logging.info("unmounting '%s'", target) try: - self.host_cmd(f"{UMOUNT_BIN} -l {target}", cwd=self.nodedir) + self.host_cmd(f"{UMOUNT} -l {target}", cwd=self.nodedir) except CoreCommandError: logging.exception("unmounting failed for %s", target) diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index cf76b092..774c4104 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -7,7 +7,7 @@ import netaddr from core import utils from core.errors import CoreCommandError -from core.executables import SYSCTL_BIN +from core.executables import SYSCTL from core.nodes.base import CoreNode from core.services.coreservices import CoreService, ServiceMode @@ -48,13 +48,13 @@ class IPForwardService(UtilService): %(sysctl)s -w net.ipv4.conf.all.rp_filter=0 %(sysctl)s -w net.ipv4.conf.default.rp_filter=0 """ % { - "sysctl": SYSCTL_BIN + "sysctl": SYSCTL } for iface in node.get_ifaces(): name = utils.sysctl_devname(iface.name) - cfg += "%s -w net.ipv4.conf.%s.forwarding=1\n" % (SYSCTL_BIN, name) - cfg += "%s -w net.ipv4.conf.%s.send_redirects=0\n" % (SYSCTL_BIN, name) - cfg += "%s -w net.ipv4.conf.%s.rp_filter=0\n" % (SYSCTL_BIN, name) + cfg += "%s -w net.ipv4.conf.%s.forwarding=1\n" % (SYSCTL, name) + cfg += "%s -w net.ipv4.conf.%s.send_redirects=0\n" % (SYSCTL, name) + cfg += "%s -w net.ipv4.conf.%s.rp_filter=0\n" % (SYSCTL, name) return cfg diff --git a/daemon/core/xml/corexmldeployment.py b/daemon/core/xml/corexmldeployment.py index 2235a798..51201787 100644 --- a/daemon/core/xml/corexmldeployment.py +++ b/daemon/core/xml/corexmldeployment.py @@ -7,7 +7,7 @@ from lxml import etree from core import utils from core.emane.nodes import EmaneNet -from core.executables import IP_BIN +from core.executables import IP from core.nodes.base import CoreNodeBase, NodeBase from core.nodes.interface import CoreInterface @@ -83,7 +83,7 @@ def get_address_type(address: str) -> str: def get_ipv4_addresses(hostname: str) -> List[Tuple[str, str]]: if hostname == "localhost": addresses = [] - args = f"{IP_BIN} -o -f inet address show" + args = f"{IP} -o -f inet address show" output = utils.cmd(args) for line in output.split(os.linesep): split = line.split() From 8e2cfa61c90413b9bfbb560dbcde89134d6c0380 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 23 Jun 2020 10:09:16 -0700 Subject: [PATCH 0398/1131] pygui: size and scale meter width and height are no longer editable, but will dynamically update with changes to related size/scale values --- daemon/core/gui/dialogs/canvassizeandscale.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/daemon/core/gui/dialogs/canvassizeandscale.py b/daemon/core/gui/dialogs/canvassizeandscale.py index b93bd920..38cecc83 100644 --- a/daemon/core/gui/dialogs/canvassizeandscale.py +++ b/daemon/core/gui/dialogs/canvassizeandscale.py @@ -66,10 +66,12 @@ class SizeAndScaleDialog(Dialog): label.grid(row=0, column=0, sticky="w", padx=PADX) entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_width) entry.grid(row=0, column=1, sticky="ew", padx=PADX) + entry.bind("", self.size_scale_keyup) label = ttk.Label(frame, text="x Height") label.grid(row=0, column=2, sticky="w", padx=PADX) entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_height) entry.grid(row=0, column=3, sticky="ew", padx=PADX) + entry.bind("", self.size_scale_keyup) label = ttk.Label(frame, text="Pixels") label.grid(row=0, column=4, sticky="w") @@ -80,11 +82,15 @@ class SizeAndScaleDialog(Dialog): frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") label.grid(row=0, column=0, sticky="w", padx=PADX) - entry = validation.PositiveFloatEntry(frame, textvariable=self.meters_width) + entry = validation.PositiveFloatEntry( + frame, textvariable=self.meters_width, state=tk.DISABLED + ) entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="x Height") label.grid(row=0, column=2, sticky="w", padx=PADX) - entry = validation.PositiveFloatEntry(frame, textvariable=self.meters_height) + entry = validation.PositiveFloatEntry( + frame, textvariable=self.meters_height, state=tk.DISABLED + ) entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=4, sticky="w") @@ -101,6 +107,7 @@ class SizeAndScaleDialog(Dialog): label.grid(row=0, column=0, sticky="w", padx=PADX) entry = validation.PositiveFloatEntry(frame, textvariable=self.scale) entry.grid(row=0, column=1, sticky="ew", padx=PADX) + entry.bind("", self.size_scale_keyup) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=2, sticky="w") @@ -173,6 +180,13 @@ class SizeAndScaleDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") + def size_scale_keyup(self, _event: tk.Event) -> None: + scale = self.scale.get() + width = self.pixel_width.get() + height = self.pixel_height.get() + self.meters_width.set(width / PIXEL_SCALE * scale) + self.meters_height.set(height / PIXEL_SCALE * scale) + def click_apply(self) -> None: width, height = self.pixel_width.get(), self.pixel_height.get() self.canvas.redraw_canvas((width, height)) From 14573184e01ea727ce958640a92be41025433ed3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 23 Jun 2020 13:28:41 -0700 Subject: [PATCH 0399/1131] pygui: fixed syning session location settings when not in runtime mode, for saving xml --- daemon/core/gui/coreclient.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 39ee486a..8050d7f0 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -718,7 +718,7 @@ class CoreClient: def send_data(self) -> None: """ - send to daemon all session info, but don't start the session + Send to daemon all session info, but don't start the session """ self.create_nodes_and_links() for config_proto in self.get_wlan_configs_proto(): @@ -759,6 +759,17 @@ class CoreClient: if self.emane_config: config = {x: self.emane_config[x].value for x in self.emane_config} self.client.set_emane_config(self.session_id, config) + if self.location: + self.client.set_session_location( + self.session_id, + self.location.x, + self.location.y, + self.location.z, + self.location.lat, + self.location.lon, + self.location.alt, + self.location.scale, + ) self.set_metadata() def close(self) -> None: From 9649337f185af4e7cd90ef93029871b2dcb92e60 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 23 Jun 2020 13:31:31 -0700 Subject: [PATCH 0400/1131] daemon: updated xml to save links using consistent iface1/2 naming, still fallback to reading interface_one/two --- daemon/core/xml/corexml.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index d3cc85d8..340d81d0 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -520,14 +520,14 @@ class CoreXmlWriter: # check for interface one if link_data.iface1 is not None: iface1 = self.create_iface_element( - "interface1", link_data.node1_id, link_data.iface1 + "iface1", link_data.node1_id, link_data.iface1 ) link_element.append(iface1) # check for interface two if link_data.iface2 is not None: iface2 = self.create_iface_element( - "interface2", link_data.node2_id, link_data.iface2 + "iface2", link_data.node2_id, link_data.iface2 ) link_element.append(iface2) @@ -907,14 +907,14 @@ class CoreXmlReader: node2_id = get_int(link_element, "node_two") node_set = frozenset((node1_id, node2_id)) - iface1_element = link_element.find("interface1") + iface1_element = link_element.find("iface1") if iface1_element is None: iface1_element = link_element.find("interface_one") iface1_data = None if iface1_element is not None: iface1_data = create_iface_data(iface1_element) - iface2_element = link_element.find("interface2") + iface2_element = link_element.find("iface2") if iface2_element is None: iface2_element = link_element.find("interface_two") iface2_data = None From 7215f852b8b591d20b4eaab39fbb8582ad46557e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 23 Jun 2020 13:34:40 -0700 Subject: [PATCH 0401/1131] grpc: added check for emane pathloss when nem id is None and throw an error --- daemon/core/api/grpc/grpcutils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index b63cb895..8df545cd 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -487,4 +487,8 @@ def get_nem_id(node: CoreNode, iface_id: int, context: ServicerContext) -> int: if not isinstance(net, EmaneNet): message = f"{node.name} interface {iface_id} is not an EMANE network" context.abort(grpc.StatusCode.INVALID_ARGUMENT, message) - return net.getnemid(iface) + nem_id = net.getnemid(iface) + if nem_id is None: + message = f"{node.name} interface {iface_id} nem id does not exist" + context.abort(grpc.StatusCode.INVALID_ARGUMENT, message) + return nem_id From 60d9fe2026add397e9bf004a9ee7dd7057ad21c1 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 23 Jun 2020 14:48:27 -0700 Subject: [PATCH 0402/1131] pygui: clear throughput labels when disabling throughput --- daemon/core/gui/coreclient.py | 1 + daemon/core/gui/graph/edges.py | 7 +++++-- daemon/core/gui/graph/graph.py | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 8050d7f0..d35f62e5 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -221,6 +221,7 @@ class CoreClient: if self.handling_throughputs: self.handling_throughputs.cancel() self.handling_throughputs = None + self.app.canvas.clear_throughputs() def cancel_events(self) -> None: if self.handling_events: diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index e9ac2587..29632086 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -145,6 +145,10 @@ class Edge: else: self.canvas.itemconfig(self.middle_label, text=text) + def clear_middle_label(self) -> None: + self.canvas.delete(self.middle_label) + self.middle_label = 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 @@ -216,11 +220,10 @@ class Edge: logging.debug("deleting canvas edge, id: %s", self.id) self.canvas.delete(self.id) self.canvas.delete(self.src_label) - self.canvas.delete(self.middle_label) self.canvas.delete(self.dst_label) + self.clear_middle_label() self.id = None self.src_label = None - self.middle_label = None self.dst_label = None diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index fdf9ba21..07519c3f 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -997,6 +997,10 @@ class CanvasGraph(tk.Canvas): ) self.tag_raise(tags.NODE) + def clear_throughputs(self) -> None: + for edge in self.edges.values(): + edge.clear_middle_label() + def scale_graph(self) -> None: for nid, canvas_node in self.nodes.items(): img = None From 6490b5b9cbe6e39adb57aeb00675d6b828355746 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 23 Jun 2020 16:11:39 -0700 Subject: [PATCH 0403/1131] pygui: fixed and changed custom service copy to focus only on copying the current file displayed from any other nodes with a customized version --- daemon/core/gui/dialogs/copyserviceconfig.py | 226 ++++++------------- daemon/core/gui/dialogs/serviceconfig.py | 6 +- 2 files changed, 77 insertions(+), 155 deletions(-) diff --git a/daemon/core/gui/dialogs/copyserviceconfig.py b/daemon/core/gui/dialogs/copyserviceconfig.py index 35559cb9..2a01249d 100644 --- a/daemon/core/gui/dialogs/copyserviceconfig.py +++ b/daemon/core/gui/dialogs/copyserviceconfig.py @@ -4,80 +4,58 @@ copy service config dialog import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING, Tuple +from typing import TYPE_CHECKING, Dict, Optional from core.gui.dialogs.dialog import Dialog -from core.gui.themes import FRAME_PAD, PADX -from core.gui.widgets import CodeText +from core.gui.themes import PADX, PADY +from core.gui.widgets import CodeText, ListboxScroll if TYPE_CHECKING: from core.gui.app import Application + from core.gui.dialogs.serviceconfig import ServiceConfigDialog class CopyServiceConfigDialog(Dialog): - def __init__(self, master: tk.BaseWidget, app: "Application", node_id: int) -> None: - super().__init__(app, f"Copy services to node {node_id}", master=master) - self.parent = master - self.node_id = node_id - self.service_configs = app.core.service_configs - self.file_configs = app.core.file_configs - self.tree = None + def __init__( + self, + app: "Application", + dialog: "ServiceConfigDialog", + name: str, + service: str, + file_name: str, + ) -> None: + super().__init__(app, f"Copy Custom File to {name}", master=dialog) + self.dialog: "ServiceConfigDialog" = dialog + self.service: str = service + self.file_name: str = file_name + self.listbox: Optional[tk.Listbox] = None + self.nodes: Dict[str, int] = {} self.draw() def draw(self) -> None: self.top.columnconfigure(0, weight=1) - self.tree = ttk.Treeview(self.top) - self.tree.grid(row=0, column=0, sticky="ew", padx=PADX) - self.tree["columns"] = () - self.tree.column("#0", width=270, minwidth=270, stretch=tk.YES) - self.tree.heading("#0", text="Service configuration items", anchor=tk.CENTER) - custom_nodes = set(self.service_configs).union(set(self.file_configs)) - for nid in custom_nodes: - treeid = self.tree.insert("", "end", text=f"n{nid}", tags="node") - services = self.service_configs.get(nid, None) - files = self.file_configs.get(nid, None) - tree_ids = {} - if services: - for service, config in services.items(): - serviceid = self.tree.insert( - treeid, "end", text=service, tags="service" - ) - tree_ids[service] = serviceid - cmdup = config.startup[:] - cmddown = config.shutdown[:] - cmdval = config.validate[:] - self.tree.insert( - serviceid, - "end", - text=f"cmdup=({str(cmdup)[1:-1]})", - tags=("cmd", "up"), - ) - self.tree.insert( - serviceid, - "end", - text=f"cmddown=({str(cmddown)[1:-1]})", - tags=("cmd", "down"), - ) - self.tree.insert( - serviceid, - "end", - text=f"cmdval=({str(cmdval)[1:-1]})", - tags=("cmd", "val"), - ) - if files: - for service, configs in files.items(): - if service in tree_ids: - serviceid = tree_ids[service] - else: - serviceid = self.tree.insert( - treeid, "end", text=service, tags="service" - ) - tree_ids[service] = serviceid - for filename, data in configs.items(): - self.tree.insert(serviceid, "end", text=filename, tags="file") + self.top.rowconfigure(1, weight=1) + label = ttk.Label( + self.top, text=f"{self.service} - {self.file_name}", anchor=tk.CENTER + ) + label.grid(sticky="ew", pady=PADY) + + listbox_scroll = ListboxScroll(self.top) + listbox_scroll.grid(sticky="nsew", pady=PADY) + self.listbox = listbox_scroll.listbox + for canvas_node in self.app.canvas.nodes.values(): + file_configs = canvas_node.service_file_configs.get(self.service) + if not file_configs: + continue + data = file_configs.get(self.file_name) + if not data: + continue + name = canvas_node.core_node.name + self.nodes[name] = canvas_node.id + self.listbox.insert(tk.END, name) frame = ttk.Frame(self.top) - frame.grid(row=1, column=0) + frame.grid(sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Copy", command=self.click_copy) @@ -85,118 +63,58 @@ class CopyServiceConfigDialog(Dialog): button = ttk.Button(frame, text="View", command=self.click_view) button.grid(row=0, column=1, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=2, sticky="ew", padx=PADX) + button.grid(row=0, column=2, sticky="ew") def click_copy(self) -> None: - selected = self.tree.selection() - if selected: - item = self.tree.item(selected[0]) - if "file" in item["tags"]: - filename = item["text"] - nid, service = self.get_node_service(selected) - data = self.file_configs[nid][service][filename] - if service == self.parent.service_name: - self.parent.temp_service_files[filename] = data - self.parent.modified_files.add(filename) - if self.parent.filename_combobox.get() == filename: - self.parent.service_file_data.text.delete(1.0, "end") - self.parent.service_file_data.text.insert("end", data) - if "cmd" in item["tags"]: - nid, service = self.get_node_service(selected) - if service == self.master.service_name: - cmds = self.service_configs[nid][service] - if "up" in item["tags"]: - self.master.append_commands( - self.master.startup_commands, - self.master.startup_commands_listbox, - cmds.startup, - ) - elif "down" in item["tags"]: - self.master.append_commands( - self.master.shutdown_commands, - self.master.shutdown_commands_listbox, - cmds.shutdown, - ) - - elif "val" in item["tags"]: - self.master.append_commands( - self.master.validate_commands, - self.master.validate_commands_listbox, - cmds.validate, - ) + selection = self.listbox.curselection() + if not selection: + return + name = self.listbox.get(selection) + canvas_node_id = self.nodes[name] + canvas_node = self.app.canvas.nodes[canvas_node_id] + data = canvas_node.service_file_configs[self.service][self.file_name] + self.dialog.temp_service_files[self.file_name] = data + self.dialog.modified_files.add(self.file_name) + self.dialog.service_file_data.text.delete(1.0, tk.END) + self.dialog.service_file_data.text.insert(tk.END, data) self.destroy() def click_view(self) -> None: - selected = self.tree.selection() - data = "" - if selected: - item = self.tree.item(selected[0]) - if "file" in item["tags"]: - nid, service = self.get_node_service(selected) - data = self.file_configs[nid][service][item["text"]] - dialog = ViewConfigDialog( - self, self.app, nid, data, item["text"].split("/")[-1] - ) - dialog.show() - if "cmd" in item["tags"]: - nid, service = self.get_node_service(selected) - cmds = self.service_configs[nid][service] - if "up" in item["tags"]: - data = f"({str(cmds.startup[:])[1:-1]})" - dialog = ViewConfigDialog( - self, self.app, self.node_id, data, "cmdup" - ) - elif "down" in item["tags"]: - data = f"({str(cmds.shutdown[:])[1:-1]})" - dialog = ViewConfigDialog( - self, self.app, self.node_id, data, "cmdup" - ) - elif "val" in item["tags"]: - data = f"({str(cmds.validate[:])[1:-1]})" - dialog = ViewConfigDialog( - self, self.app, self.node_id, data, "cmdup" - ) - dialog.show() - - def get_node_service(self, selected: Tuple[str]) -> Tuple[int, str]: - service_tree_id = self.tree.parent(selected[0]) - service_name = self.tree.item(service_tree_id)["text"] - node_tree_id = self.tree.parent(service_tree_id) - node_id = int(self.tree.item(node_tree_id)["text"][1:]) - return node_id, service_name + selection = self.listbox.curselection() + if not selection: + return + name = self.listbox.get(selection) + canvas_node_id = self.nodes[name] + canvas_node = self.app.canvas.nodes[canvas_node_id] + data = canvas_node.service_file_configs[self.service][self.file_name] + dialog = ViewConfigDialog( + self.app, self, name, self.service, self.file_name, data + ) + dialog.show() class ViewConfigDialog(Dialog): def __init__( self, - master: tk.BaseWidget, app: "Application", - node_id: int, + master: tk.BaseWidget, + name: str, + service: str, + file_name: str, data: str, - filename: str = None, ) -> None: - super().__init__(app, f"n{node_id} config data", master=master) + title = f"{name} Service({service}) File({file_name})" + super().__init__(app, title, master=master) self.data = data self.service_data = None - self.filepath = tk.StringVar(value=f"/tmp/services.tmp-n{node_id}-{filename}") self.draw() def draw(self) -> None: self.top.columnconfigure(0, weight=1) - frame = ttk.Frame(self.top, padding=FRAME_PAD) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=10) - frame.grid(row=0, column=0, sticky="ew") - label = ttk.Label(frame, text="File: ") - label.grid(row=0, column=0, sticky="ew", padx=PADX) - entry = ttk.Entry(frame, textvariable=self.filepath) - entry.config(state="disabled") - entry.grid(row=0, column=1, sticky="ew") - + self.top.rowconfigure(0, weight=1) self.service_data = CodeText(self.top) - self.service_data.grid(row=1, column=0, sticky="nsew") - self.service_data.text.insert("end", self.data) - self.service_data.text.config(state="disabled") - + self.service_data.grid(sticky="nsew", pady=PADY) + self.service_data.text.insert(tk.END, self.data) + self.service_data.text.config(state=tk.DISABLED) button = ttk.Button(self.top, text="Close", command=self.destroy) - button.grid(row=2, column=0, sticky="ew", padx=PADX) + button.grid(sticky="ew") diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index 5faface7..4e615db0 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -563,7 +563,11 @@ class ServiceConfigDialog(Dialog): self.current_service_color("") def click_copy(self) -> None: - dialog = CopyServiceConfigDialog(self, self.app, self.node_id) + file_name = self.filename_combobox.get() + name = self.canvas_node.core_node.name + dialog = CopyServiceConfigDialog( + self.app, self, name, self.service_name, file_name + ) dialog.show() @classmethod From bb2ceaf99307ca75e21018712faf658c29187064 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 23 Jun 2020 22:53:48 -0700 Subject: [PATCH 0404/1131] pygui: draw link options on edges --- daemon/core/gui/dialogs/linkconfig.py | 2 ++ daemon/core/gui/graph/edges.py | 35 +++++++++++++++++++++++++++ daemon/core/gui/graph/graph.py | 1 + 3 files changed, 38 insertions(+) diff --git a/daemon/core/gui/dialogs/linkconfig.py b/daemon/core/gui/dialogs/linkconfig.py index b7c618a3..28798ec1 100644 --- a/daemon/core/gui/dialogs/linkconfig.py +++ b/daemon/core/gui/dialogs/linkconfig.py @@ -287,6 +287,8 @@ class LinkConfigurationDialog(Dialog): iface2_id, ) + # update edge label + self.edge.draw_link_options() self.destroy() def change_symmetry(self) -> None: diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 29632086..6c79787f 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -57,6 +57,18 @@ def arc_edges(edges) -> None: edge.redraw() +def bandwidth_label(bandwidth: int) -> str: + size = {0: "bps", 1: "Kbps", 2: "Mbps", 3: "Gbps"} + unit = 1000 + i = 0 + while bandwidth > unit: + bandwidth /= unit + i += 1 + if i == 3: + break + return f"{bandwidth} {size[i]}" + + class Edge: tag: str = tags.EDGE @@ -140,6 +152,7 @@ class Edge: font=self.canvas.app.edge_font, text=text, tags=tags.LINK_LABEL, + justify=tk.CENTER, state=self.canvas.show_link_labels.state(), ) else: @@ -312,6 +325,7 @@ class CanvasEdge(Edge): src_text, dst_text = self.create_node_labels() self.src_label_text(src_text) self.dst_label_text(dst_text) + self.draw_link_options() def redraw(self) -> None: super().redraw() @@ -393,3 +407,24 @@ class CanvasEdge(Edge): def click_configure(self) -> None: dialog = LinkConfigurationDialog(self.canvas.app, self) dialog.show() + + def draw_link_options(self): + options = self.link.options + lines = [] + bandwidth = options.bandwidth + if bandwidth > 0: + lines.append(bandwidth_label(bandwidth)) + delay = options.delay + jitter = options.jitter + if delay > 0 and jitter > 0: + lines.append(f"{delay} us (\u00B1{jitter} us)") + elif jitter > 0: + lines.append(f"0 us (\u00B1{jitter} us)") + loss = options.loss + if loss > 0: + lines.append(f"loss={loss}%") + dup = options.dup + if dup > 0: + lines.append(f"dup={dup}%") + label = "\n".join(lines) + self.middle_label_text(label) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 07519c3f..7d8ec019 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -1000,6 +1000,7 @@ class CanvasGraph(tk.Canvas): def clear_throughputs(self) -> None: for edge in self.edges.values(): edge.clear_middle_label() + edge.draw_link_options() def scale_graph(self) -> None: for nid, canvas_node in self.nodes.items(): From f582306bb97e5f2281093b9dfe3cd00e4e1285d3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 25 Jun 2020 10:35:01 -0700 Subject: [PATCH 0405/1131] pygui: added support for a details pane, can be toggled on/off, can be used to quickly view details for nodes or links --- daemon/core/gui/app.py | 47 ++++++++++++++++++++++-- daemon/core/gui/frames/__init__.py | 0 daemon/core/gui/frames/base.py | 36 +++++++++++++++++++ daemon/core/gui/frames/default.py | 19 ++++++++++ daemon/core/gui/frames/link.py | 58 ++++++++++++++++++++++++++++++ daemon/core/gui/frames/node.py | 33 +++++++++++++++++ daemon/core/gui/graph/edges.py | 20 ++++------- daemon/core/gui/graph/graph.py | 1 + daemon/core/gui/graph/node.py | 6 ++++ daemon/core/gui/menubar.py | 11 ++++++ daemon/core/gui/task.py | 2 +- daemon/core/gui/utils.py | 10 ++++++ 12 files changed, 226 insertions(+), 17 deletions(-) create mode 100644 daemon/core/gui/frames/__init__.py create mode 100644 daemon/core/gui/frames/base.py create mode 100644 daemon/core/gui/frames/default.py create mode 100644 daemon/core/gui/frames/link.py create mode 100644 daemon/core/gui/frames/node.py create mode 100644 daemon/core/gui/utils.py diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index cb385e9e..e0121d14 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -3,7 +3,7 @@ import math import tkinter as tk from tkinter import PhotoImage, font, ttk from tkinter.ttk import Progressbar -from typing import Dict, Optional +from typing import Any, Dict, Optional, Type import grpc @@ -11,11 +11,14 @@ from core.gui import appconfig, themes from core.gui.appconfig import GuiConfig from core.gui.coreclient import CoreClient from core.gui.dialogs.error import ErrorDialog +from core.gui.frames.base import InfoFrameBase +from core.gui.frames.default import DefaultInfoFrame from core.gui.graph.graph import CanvasGraph from core.gui.images import ImageEnum, Images from core.gui.menubar import Menubar from core.gui.nodeutils import NodeUtils from core.gui.statusbar import StatusBar +from core.gui.themes import PADY from core.gui.toolbar import Toolbar WIDTH: int = 1000 @@ -35,6 +38,9 @@ class Application(ttk.Frame): self.canvas: Optional[CanvasGraph] = None self.statusbar: Optional[StatusBar] = None self.progress: Optional[Progressbar] = None + self.infobar: Optional[ttk.Frame] = None + self.info_frame: Optional[InfoFrameBase] = None + self.show_infobar: tk.BooleanVar = tk.BooleanVar(value=False) # fonts self.fonts_size: Dict[str, int] = {} @@ -113,16 +119,27 @@ class Application(ttk.Frame): self.right_frame.rowconfigure(0, weight=1) self.right_frame.grid(row=0, column=1, sticky="nsew") self.draw_canvas() + self.draw_infobar() self.draw_status() self.progress = Progressbar(self.right_frame, mode="indeterminate") self.menubar = Menubar(self) self.master.config(menu=self.menubar) + def draw_infobar(self) -> None: + self.infobar = ttk.Frame(self.right_frame, padding=5, relief=tk.RAISED) + self.infobar.columnconfigure(0, weight=1) + self.infobar.rowconfigure(1, weight=1) + label_font = font.Font(weight=font.BOLD, underline=tk.TRUE) + label = ttk.Label( + self.infobar, text="Details", anchor=tk.CENTER, font=label_font + ) + label.grid(sticky=tk.EW, pady=PADY) + def draw_canvas(self) -> None: canvas_frame = ttk.Frame(self.right_frame) canvas_frame.rowconfigure(0, weight=1) canvas_frame.columnconfigure(0, weight=1) - canvas_frame.grid(sticky="nsew", pady=1) + canvas_frame.grid(row=0, column=0, sticky="nsew", pady=1) self.canvas = CanvasGraph(canvas_frame, self, self.core) self.canvas.grid(sticky="nsew") scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview) @@ -136,7 +153,31 @@ class Application(ttk.Frame): def draw_status(self) -> None: self.statusbar = StatusBar(self.right_frame, self) - self.statusbar.grid(sticky="ew") + self.statusbar.grid(sticky="ew", columnspan=2) + + def display_info(self, frame_class: Type[InfoFrameBase], **kwargs: Any) -> None: + if not self.show_infobar.get(): + return + self.clear_info() + self.info_frame = frame_class(self.infobar, **kwargs) + self.info_frame.draw() + self.info_frame.grid(sticky="nsew") + + def clear_info(self) -> None: + if self.info_frame: + self.info_frame.destroy() + self.info_frame = None + + def default_info(self) -> None: + self.clear_info() + self.display_info(DefaultInfoFrame, app=self) + + def show_info(self) -> None: + self.default_info() + self.infobar.grid(row=0, column=1, sticky="nsew") + + def hide_info(self) -> None: + self.infobar.grid_forget() def show_grpc_exception(self, title: str, e: grpc.RpcError) -> None: logging.exception("app grpc exception", exc_info=e) diff --git a/daemon/core/gui/frames/__init__.py b/daemon/core/gui/frames/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/daemon/core/gui/frames/base.py b/daemon/core/gui/frames/base.py new file mode 100644 index 00000000..8db952f1 --- /dev/null +++ b/daemon/core/gui/frames/base.py @@ -0,0 +1,36 @@ +import tkinter as tk +from tkinter import ttk +from typing import TYPE_CHECKING + +from core.gui.themes import FRAME_PAD, PADX, PADY + +if TYPE_CHECKING: + from core.gui.app import Application + + +class InfoFrameBase(ttk.Frame): + def __init__(self, master: tk.BaseWidget, app: "Application") -> None: + super().__init__(master, padding=FRAME_PAD) + self.app: "Application" = app + + def draw(self) -> None: + raise NotImplementedError + + +class DetailsFrame(ttk.Frame): + def __init__(self, master: tk.BaseWidget) -> None: + super().__init__(master) + self.columnconfigure(1, weight=1) + self.row = 0 + + def add_detail(self, label: str, value: str) -> None: + label = ttk.Label(self, text=label, anchor=tk.W) + label.grid(row=self.row, sticky=tk.EW, column=0, padx=PADX) + label = ttk.Label(self, text=value, anchor=tk.W, state=tk.DISABLED) + label.grid(row=self.row, sticky=tk.EW, column=1) + self.row += 1 + + def add_separator(self) -> None: + separator = ttk.Separator(self) + separator.grid(row=self.row, sticky=tk.EW, columnspan=2, pady=PADY) + self.row += 1 diff --git a/daemon/core/gui/frames/default.py b/daemon/core/gui/frames/default.py new file mode 100644 index 00000000..e84edb87 --- /dev/null +++ b/daemon/core/gui/frames/default.py @@ -0,0 +1,19 @@ +import tkinter as tk +from tkinter import ttk +from typing import TYPE_CHECKING + +from core.gui.frames.base import InfoFrameBase + +if TYPE_CHECKING: + from core.gui.app import Application + + +class DefaultInfoFrame(InfoFrameBase): + def __init__(self, master: tk.BaseWidget, app: "Application") -> None: + super().__init__(master, app) + + def draw(self) -> None: + label = ttk.Label(self, text="Click a Node/Link", anchor=tk.CENTER) + label.grid(sticky=tk.EW) + label = ttk.Label(self, text="to see details", anchor=tk.CENTER) + label.grid(sticky=tk.EW) diff --git a/daemon/core/gui/frames/link.py b/daemon/core/gui/frames/link.py new file mode 100644 index 00000000..29b3df45 --- /dev/null +++ b/daemon/core/gui/frames/link.py @@ -0,0 +1,58 @@ +import tkinter as tk +from typing import TYPE_CHECKING + +from core.gui.frames.base import DetailsFrame, InfoFrameBase +from core.gui.utils import bandwidth_text + +if TYPE_CHECKING: + from core.gui.app import Application + from core.gui.graph.edges import CanvasEdge + + +class EdgeInfoFrame(InfoFrameBase): + def __init__( + self, master: tk.BaseWidget, app: "Application", edge: "CanvasEdge" + ) -> None: + super().__init__(master, app) + self.edge: "CanvasEdge" = edge + + def draw(self) -> None: + self.columnconfigure(0, weight=1) + link = self.edge.link + options = link.options + src_canvas_node = self.app.core.canvas_nodes[link.node1_id] + src_node = src_canvas_node.core_node + dst_canvas_node = self.app.core.canvas_nodes[link.node2_id] + dst_node = dst_canvas_node.core_node + + frame = DetailsFrame(self) + frame.grid(sticky="ew") + frame.add_detail("Source", src_node.name) + iface1 = link.iface1 + if iface1: + mac = iface1.mac if iface1.mac else "auto" + frame.add_detail("MAC", mac) + ip4 = f"{iface1.ip4}/{iface1.ip4_mask}" if iface1.ip4 else "" + frame.add_detail("IP4", ip4) + ip6 = f"{iface1.ip6}/{iface1.ip6_mask}" if iface1.ip6 else "" + frame.add_detail("IP6", ip6) + + frame.add_separator() + frame.add_detail("Destination", dst_node.name) + iface2 = link.iface2 + if iface2: + mac = iface2.mac if iface2.mac else "auto" + frame.add_detail("MAC", mac) + ip4 = f"{iface2.ip4}/{iface2.ip4_mask}" if iface2.ip4 else "" + frame.add_detail("IP4", ip4) + ip6 = f"{iface2.ip6}/{iface2.ip6_mask}" if iface2.ip6 else "" + frame.add_detail("IP6", ip6) + + if link.HasField("options"): + frame.add_separator() + bandwidth = bandwidth_text(options.bandwidth) + frame.add_detail("Bandwidth", bandwidth) + frame.add_detail("Delay", f"{options.delay} us") + frame.add_detail("Jitter", f"\u00B1{options.jitter} us") + frame.add_detail("Loss", f"{options.loss}%") + frame.add_detail("Duplicate", f"{options.dup}%") diff --git a/daemon/core/gui/frames/node.py b/daemon/core/gui/frames/node.py new file mode 100644 index 00000000..44724f36 --- /dev/null +++ b/daemon/core/gui/frames/node.py @@ -0,0 +1,33 @@ +from typing import TYPE_CHECKING + +from core.api.grpc.core_pb2 import NodeType +from core.gui.frames.base import DetailsFrame, InfoFrameBase +from core.gui.nodeutils import NodeUtils + +if TYPE_CHECKING: + from core.gui.app import Application + from core.gui.graph.node import CanvasNode + + +class NodeInfoFrame(InfoFrameBase): + def __init__(self, master, app: "Application", canvas_node: "CanvasNode") -> None: + super().__init__(master, app) + self.canvas_node: "CanvasNode" = canvas_node + + def draw(self) -> None: + self.columnconfigure(0, weight=1) + node = self.canvas_node.core_node + frame = DetailsFrame(self) + frame.grid(sticky="ew") + frame.add_detail("ID", node.id) + frame.add_detail("Name", node.name) + if NodeUtils.is_model_node(node.type): + frame.add_detail("Type", node.model) + if node.type == NodeType.EMANE: + emane = node.emane.split("_")[1:] + frame.add_detail("EMANE", emane) + if NodeUtils.is_image_node(node.type): + frame.add_detail("Image", node.image) + if NodeUtils.is_container_node(node.type): + server = node.server if node.server else "localhost" + frame.add_detail("Server", server) diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 6c79787f..de063bac 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -7,8 +7,10 @@ from core.api.grpc import core_pb2 from core.api.grpc.core_pb2 import Interface, Link from core.gui import themes from core.gui.dialogs.linkconfig import LinkConfigurationDialog +from core.gui.frames.link import EdgeInfoFrame from core.gui.graph import tags from core.gui.nodeutils import NodeUtils +from core.gui.utils import bandwidth_text if TYPE_CHECKING: from core.gui.graph.graph import CanvasGraph @@ -57,18 +59,6 @@ def arc_edges(edges) -> None: edge.redraw() -def bandwidth_label(bandwidth: int) -> str: - size = {0: "bps", 1: "Kbps", 2: "Mbps", 3: "Gbps"} - unit = 1000 - i = 0 - while bandwidth > unit: - bandwidth /= unit - i += 1 - if i == 3: - break - return f"{bandwidth} {size[i]}" - - class Edge: tag: str = tags.EDGE @@ -295,6 +285,7 @@ class CanvasEdge(Edge): 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 @@ -396,6 +387,9 @@ class CanvasEdge(Edge): self.middle_label = None self.canvas.itemconfig(self.id, fill=self.color, width=self.scaled_width()) + def show_info(self, _event: tk.Event) -> None: + self.canvas.app.display_info(EdgeInfoFrame, app=self.canvas.app, edge=self) + def show_context(self, event: tk.Event) -> None: state = tk.DISABLED if self.canvas.core.is_runtime() else tk.NORMAL self.context.entryconfigure(1, state=state) @@ -413,7 +407,7 @@ class CanvasEdge(Edge): lines = [] bandwidth = options.bandwidth if bandwidth > 0: - lines.append(bandwidth_label(bandwidth)) + lines.append(bandwidth_text(bandwidth)) delay = options.delay jitter = options.jitter if delay > 0 and jitter > 0: diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 7d8ec019..1588f920 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -715,6 +715,7 @@ class CanvasGraph(tk.Canvas): logging.debug("press delete key") if not self.app.core.is_runtime(): self.delete_selected_objects() + self.app.default_info() else: logging.debug("node deletion is disabled during runtime state") diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index a86ce4a3..d98c4e48 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -16,6 +16,7 @@ from core.gui.dialogs.nodeconfig import NodeConfigDialog from core.gui.dialogs.nodeconfigservice import NodeConfigServiceDialog from core.gui.dialogs.nodeservice import NodeServiceDialog from core.gui.dialogs.wlanconfig import WlanConfigDialog +from core.gui.frames.node import NodeInfoFrame from core.gui.graph import tags from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge from core.gui.graph.tooltip import CanvasTooltip @@ -80,6 +81,7 @@ class CanvasNode: self.canvas.tag_bind(self.id, "", self.on_enter) self.canvas.tag_bind(self.id, "", self.on_leave) self.canvas.tag_bind(self.id, "", self.show_context) + self.canvas.tag_bind(self.id, "", self.show_info) def delete(self) -> None: logging.debug("Delete canvas node for %s", self.core_node) @@ -195,6 +197,9 @@ class CanvasNode: else: self.show_config() + def show_info(self, _event: tk.Event) -> None: + self.app.display_info(NodeInfoFrame, app=self.app, canvas_node=self) + def show_context(self, event: tk.Event) -> None: # clear existing menu self.context.delete(0, tk.END) @@ -262,6 +267,7 @@ class CanvasNode: def click_unlink(self, edge: CanvasEdge) -> None: self.canvas.delete_edge(edge) + self.app.default_info() def canvas_delete(self) -> None: self.canvas.clear_selection() diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 75312e95..3b85ac6f 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -138,6 +138,11 @@ class Menubar(tk.Menu): Create view menu """ menu = tk.Menu(self) + menu.add_checkbutton( + label="Details Panel", + command=self.click_infobar_change, + variable=self.app.show_infobar, + ) menu.add_checkbutton( label="Interface Names", command=self.click_edge_label_change, @@ -443,6 +448,12 @@ class Menubar(tk.Menu): y = (row * layout_size) + padding node.move(x, y) + def click_infobar_change(self) -> None: + if self.app.show_infobar.get(): + self.app.show_info() + else: + self.app.hide_info() + def click_edge_label_change(self) -> None: for edge in self.canvas.edges.values(): edge.draw_labels() diff --git a/daemon/core/gui/task.py b/daemon/core/gui/task.py index b4a5f68f..c60350f9 100644 --- a/daemon/core/gui/task.py +++ b/daemon/core/gui/task.py @@ -26,7 +26,7 @@ class ProgressTask: self.time: Optional[float] = None def start(self) -> None: - self.app.progress.grid(sticky="ew") + self.app.progress.grid(sticky="ew", columnspan=2) self.app.progress.start() self.time = time.perf_counter() thread = threading.Thread(target=self.run, daemon=True) diff --git a/daemon/core/gui/utils.py b/daemon/core/gui/utils.py new file mode 100644 index 00000000..ee5ad8cb --- /dev/null +++ b/daemon/core/gui/utils.py @@ -0,0 +1,10 @@ +def bandwidth_text(bandwidth: int) -> str: + size = {0: "bps", 1: "Kbps", 2: "Mbps", 3: "Gbps"} + unit = 1000 + i = 0 + while bandwidth > unit: + bandwidth /= unit + i += 1 + if i == 3: + break + return f"{bandwidth} {size[i]}" From 98e4baca046f4e8ea0b4ba254bd1dec444d067c2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 25 Jun 2020 15:05:24 -0700 Subject: [PATCH 0406/1131] pygui: added services to node info panel --- daemon/core/gui/frames/node.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/daemon/core/gui/frames/node.py b/daemon/core/gui/frames/node.py index 44724f36..7480e056 100644 --- a/daemon/core/gui/frames/node.py +++ b/daemon/core/gui/frames/node.py @@ -23,6 +23,12 @@ class NodeInfoFrame(InfoFrameBase): frame.add_detail("Name", node.name) if NodeUtils.is_model_node(node.type): frame.add_detail("Type", node.model) + if NodeUtils.is_container_node(node.type): + for index, service in enumerate(sorted(node.services)): + if index == 0: + frame.add_detail("Services", service) + else: + frame.add_detail("", service) if node.type == NodeType.EMANE: emane = node.emane.split("_")[1:] frame.add_detail("EMANE", emane) From 3bfc299bfd96343b0a4afed6c7d19c95fec0c700 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 25 Jun 2020 16:22:56 -0700 Subject: [PATCH 0407/1131] daemon: fixed typo in core.configservices.securityservices --- .../{sercurityservices => securityservices}/__init__.py | 0 .../{sercurityservices => securityservices}/services.py | 0 .../{sercurityservices => securityservices}/templates/firewall.sh | 0 .../{sercurityservices => securityservices}/templates/ipsec.sh | 0 .../{sercurityservices => securityservices}/templates/nat.sh | 0 .../templates/vpnclient.sh | 0 .../templates/vpnserver.sh | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename daemon/core/configservices/{sercurityservices => securityservices}/__init__.py (100%) rename daemon/core/configservices/{sercurityservices => securityservices}/services.py (100%) rename daemon/core/configservices/{sercurityservices => securityservices}/templates/firewall.sh (100%) rename daemon/core/configservices/{sercurityservices => securityservices}/templates/ipsec.sh (100%) rename daemon/core/configservices/{sercurityservices => securityservices}/templates/nat.sh (100%) rename daemon/core/configservices/{sercurityservices => securityservices}/templates/vpnclient.sh (100%) rename daemon/core/configservices/{sercurityservices => securityservices}/templates/vpnserver.sh (100%) diff --git a/daemon/core/configservices/sercurityservices/__init__.py b/daemon/core/configservices/securityservices/__init__.py similarity index 100% rename from daemon/core/configservices/sercurityservices/__init__.py rename to daemon/core/configservices/securityservices/__init__.py diff --git a/daemon/core/configservices/sercurityservices/services.py b/daemon/core/configservices/securityservices/services.py similarity index 100% rename from daemon/core/configservices/sercurityservices/services.py rename to daemon/core/configservices/securityservices/services.py diff --git a/daemon/core/configservices/sercurityservices/templates/firewall.sh b/daemon/core/configservices/securityservices/templates/firewall.sh similarity index 100% rename from daemon/core/configservices/sercurityservices/templates/firewall.sh rename to daemon/core/configservices/securityservices/templates/firewall.sh diff --git a/daemon/core/configservices/sercurityservices/templates/ipsec.sh b/daemon/core/configservices/securityservices/templates/ipsec.sh similarity index 100% rename from daemon/core/configservices/sercurityservices/templates/ipsec.sh rename to daemon/core/configservices/securityservices/templates/ipsec.sh diff --git a/daemon/core/configservices/sercurityservices/templates/nat.sh b/daemon/core/configservices/securityservices/templates/nat.sh similarity index 100% rename from daemon/core/configservices/sercurityservices/templates/nat.sh rename to daemon/core/configservices/securityservices/templates/nat.sh diff --git a/daemon/core/configservices/sercurityservices/templates/vpnclient.sh b/daemon/core/configservices/securityservices/templates/vpnclient.sh similarity index 100% rename from daemon/core/configservices/sercurityservices/templates/vpnclient.sh rename to daemon/core/configservices/securityservices/templates/vpnclient.sh diff --git a/daemon/core/configservices/sercurityservices/templates/vpnserver.sh b/daemon/core/configservices/securityservices/templates/vpnserver.sh similarity index 100% rename from daemon/core/configservices/sercurityservices/templates/vpnserver.sh rename to daemon/core/configservices/securityservices/templates/vpnserver.sh From b94d4d35071b8e07f01fbf4469ada87df054ad18 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 25 Jun 2020 21:34:45 -0700 Subject: [PATCH 0408/1131] daemon: updated open xml with start flag to set instantiation state before running instantiate to be consistent with other cases --- daemon/core/emulator/session.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 630e1a0f..92d4b5e1 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -637,19 +637,16 @@ class Session: # clear out existing session self.clear() - if start: - state = EventTypes.CONFIGURATION_STATE - else: - state = EventTypes.DEFINITION_STATE + # set state and read xml + state = EventTypes.CONFIGURATION_STATE if start else EventTypes.DEFINITION_STATE self.set_state(state) self.name = os.path.basename(file_name) self.file_name = file_name - - # write out xml file CoreXmlReader(self).read(file_name) # start session if needed if start: + self.set_state(EventTypes.INSTANTIATION_STATE) self.instantiate() def save_xml(self, file_name: str) -> None: From f4224d1b80060fd7e5c1bc4fd771be29c01f23b5 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 25 Jun 2020 22:05:10 -0700 Subject: [PATCH 0409/1131] daemon: updated ovs option to be a formal session option, will now display within gui, save to and be read from xml --- daemon/core/emulator/coreemu.py | 2 +- daemon/core/emulator/session.py | 3 +++ daemon/core/emulator/sessionconfig.py | 3 +++ daemon/core/nodes/base.py | 10 ++++++---- daemon/core/nodes/interface.py | 5 +++-- daemon/core/nodes/network.py | 2 +- daemon/scripts/core-daemon | 3 +++ 7 files changed, 20 insertions(+), 8 deletions(-) diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py index 71723268..016f2e5b 100644 --- a/daemon/core/emulator/coreemu.py +++ b/daemon/core/emulator/coreemu.py @@ -80,7 +80,7 @@ class CoreEmu: :raises core.errors.CoreError: when an executable does not exist on path """ requirements = COMMON_REQUIREMENTS - use_ovs = self.config.get("ovs") == "True" + use_ovs = self.config.get("ovs") == "1" if use_ovs: requirements += OVS_REQUIREMENTS else: diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 92d4b5e1..c2573578 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -217,6 +217,9 @@ class Session: else: common_network.unlink(iface1, iface2) + def use_ovs(self) -> bool: + return self.options.get_config("ovs") == "1" + def add_link( self, node1_id: int, diff --git a/daemon/core/emulator/sessionconfig.py b/daemon/core/emulator/sessionconfig.py index e22e852e..9b22bcc7 100644 --- a/daemon/core/emulator/sessionconfig.py +++ b/daemon/core/emulator/sessionconfig.py @@ -56,6 +56,9 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions): default=Sdt.DEFAULT_SDT_URL, label="SDT3D URL", ), + Configuration( + _id="ovs", _type=ConfigDataTypes.BOOL, default="0", label="Enable OVS" + ), ] config_type: RegisterTlvs = RegisterTlvs.UTILITY diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 3999046d..05ec87dc 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -73,8 +73,9 @@ class NodeBase(abc.ABC): self.icon: Optional[str] = None self.position: Position = Position() self.up: bool = False - use_ovs = session.options.get_config("ovs") == "True" - self.net_client: LinuxNetClient = get_net_client(use_ovs, self.host_cmd) + self.net_client: LinuxNetClient = get_net_client( + self.session.use_ovs(), self.host_cmd + ) @abc.abstractmethod def startup(self) -> None: @@ -471,8 +472,9 @@ class CoreNode(CoreNodeBase): self.pid: Optional[int] = None self.lock: RLock = RLock() self._mounts: List[Tuple[str, str]] = [] - use_ovs = session.options.get_config("ovs") == "True" - self.node_net_client: LinuxNetClient = self.create_node_net_client(use_ovs) + self.node_net_client: LinuxNetClient = self.create_node_net_client( + self.session.use_ovs() + ) def create_node_net_client(self, use_ovs: bool) -> LinuxNetClient: """ diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 22ecb620..e4d4d0ac 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -68,8 +68,9 @@ class CoreInterface: # id used to find flow data self.flow_id: Optional[int] = None self.server: Optional["DistributedServer"] = server - use_ovs = session.options.get_config("ovs") == "True" - self.net_client: LinuxNetClient = get_net_client(use_ovs, self.host_cmd) + self.net_client: LinuxNetClient = get_net_client( + self.session.use_ovs(), self.host_cmd + ) def host_cmd( self, diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index d418a42c..a55de4cf 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -756,7 +756,7 @@ class CtrlNet(CoreNetwork): :param index: starting address index :return: nothing """ - use_ovs = self.session.options.get_config("ovs") == "True" + use_ovs = self.session.use_ovs() address = self.prefix[index] current = f"{address}/{self.prefix.prefixlen}" net_client = get_net_client(use_ovs, utils.cmd) diff --git a/daemon/scripts/core-daemon b/daemon/scripts/core-daemon index a95e59fa..16b0ac59 100755 --- a/daemon/scripts/core-daemon +++ b/daemon/scripts/core-daemon @@ -118,6 +118,9 @@ def get_merged_config(filename): # parse command line options args = parser.parse_args() + # convert ovs to internal format + args.ovs = "1" if args.ovs else "0" + # read the config file if args.configfile is not None: filename = args.configfile From eac941ce7265eb850ae9f153fd9ea0a88f8eedf1 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 26 Jun 2020 09:13:38 -0700 Subject: [PATCH 0410/1131] pygui: updates to show wireless edges in details panel, increased edge thickness to be the same as normal edges for selection to be easier --- daemon/core/gui/frames/link.py | 57 +++++++++++++++++++++++++++++++++- daemon/core/gui/graph/edges.py | 20 ++++++++++-- daemon/core/gui/graph/graph.py | 6 +--- 3 files changed, 74 insertions(+), 9 deletions(-) diff --git a/daemon/core/gui/frames/link.py b/daemon/core/gui/frames/link.py index 29b3df45..57b1bf66 100644 --- a/daemon/core/gui/frames/link.py +++ b/daemon/core/gui/frames/link.py @@ -1,12 +1,26 @@ import tkinter as tk -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional +from core.api.grpc.core_pb2 import Interface from core.gui.frames.base import DetailsFrame, InfoFrameBase from core.gui.utils import bandwidth_text if TYPE_CHECKING: from core.gui.app import Application from core.gui.graph.edges import CanvasEdge + from core.gui.graph.node import CanvasNode + from core.gui.graph.edges import CanvasWirelessEdge + + +def get_iface(canvas_node: "CanvasNode", net_id: int) -> Optional[Interface]: + iface = None + for edge in canvas_node.edges: + link = edge.link + if link.node1_id == net_id: + iface = link.iface2 + elif link.node2_id == net_id: + iface = link.iface1 + return iface class EdgeInfoFrame(InfoFrameBase): @@ -56,3 +70,44 @@ class EdgeInfoFrame(InfoFrameBase): frame.add_detail("Jitter", f"\u00B1{options.jitter} us") frame.add_detail("Loss", f"{options.loss}%") frame.add_detail("Duplicate", f"{options.dup}%") + + +class WirelessEdgeInfoFrame(InfoFrameBase): + def __init__( + self, master: tk.BaseWidget, app: "Application", edge: "CanvasWirelessEdge" + ) -> None: + super().__init__(master, app) + self.edge: "CanvasWirelessEdge" = edge + + def draw(self) -> None: + link = self.edge.link + src_canvas_node = self.app.core.canvas_nodes[link.node1_id] + src_node = src_canvas_node.core_node + dst_canvas_node = self.app.core.canvas_nodes[link.node2_id] + dst_node = dst_canvas_node.core_node + + # find interface for each node connected to network + net_id = link.network_id + iface1 = get_iface(src_canvas_node, net_id) + iface2 = get_iface(dst_canvas_node, net_id) + + frame = DetailsFrame(self) + frame.grid(sticky="ew") + frame.add_detail("Source", src_node.name) + if iface1: + mac = iface1.mac if iface1.mac else "auto" + frame.add_detail("MAC", mac) + ip4 = f"{iface1.ip4}/{iface1.ip4_mask}" if iface1.ip4 else "" + frame.add_detail("IP4", ip4) + ip6 = f"{iface1.ip6}/{iface1.ip6_mask}" if iface1.ip6 else "" + frame.add_detail("IP6", ip6) + + frame.add_separator() + frame.add_detail("Destination", dst_node.name) + if iface2: + mac = iface2.mac if iface2.mac else "auto" + frame.add_detail("MAC", mac) + ip4 = f"{iface2.ip4}/{iface2.ip4_mask}" if iface2.ip4 else "" + frame.add_detail("IP4", ip4) + ip6 = f"{iface2.ip6}/{iface2.ip6_mask}" if iface2.ip6 else "" + frame.add_detail("IP6", ip6) diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index de063bac..d9085910 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -7,7 +7,7 @@ from core.api.grpc import core_pb2 from core.api.grpc.core_pb2 import Interface, Link from core.gui import themes from core.gui.dialogs.linkconfig import LinkConfigurationDialog -from core.gui.frames.link import EdgeInfoFrame +from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame from core.gui.graph import tags from core.gui.nodeutils import NodeUtils from core.gui.utils import bandwidth_text @@ -18,7 +18,7 @@ if TYPE_CHECKING: TEXT_DISTANCE: float = 0.30 EDGE_WIDTH: int = 3 EDGE_COLOR: str = "#ff0000" -WIRELESS_WIDTH: float = 1.5 +WIRELESS_WIDTH: float = 3 WIRELESS_COLOR: str = "#009933" ARC_DISTANCE: int = 50 @@ -241,13 +241,27 @@ class CanvasWirelessEdge(Edge): 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.link: Link = link self.token: Tuple[int, ...] = token self.width: float = WIRELESS_WIDTH - self.color: str = WIRELESS_COLOR + color = link.color if link.color else WIRELESS_COLOR + self.color: str = color self.draw(src_pos, dst_pos) + if link.label: + self.middle_label_text(link.label) + self.set_binding() + + def set_binding(self) -> None: + self.canvas.tag_bind(self.id, "", self.show_info) + + def show_info(self, _event: tk.Event) -> None: + self.canvas.app.display_info( + WirelessEdgeInfoFrame, app=self.canvas.app, edge=self + ) class CanvasEdge(Edge): diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 1588f920..a3520d22 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -233,11 +233,7 @@ class CanvasGraph(tk.Canvas): 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) - if link.label: - edge.middle_label_text(link.label) - if link.color: - edge.color = link.color + edge = CanvasWirelessEdge(self, src.id, dst.id, src_pos, dst_pos, token, link) self.wireless_edges[token] = edge src.wireless_edges.add(edge) dst.wireless_edges.add(edge) From aebbff8c224a90a818417e5a512ad43204c6a493 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 26 Jun 2020 14:39:12 -0700 Subject: [PATCH 0411/1131] grpc/pygui: shifted source field in node events to base event message to apply to all events, updated add_link/delete_link rpc calls to broadcast events, updated pygui to handle these events --- daemon/core/api/grpc/events.py | 132 ++++++++++++++------------ daemon/core/api/grpc/grpcutils.py | 18 ++++ daemon/core/api/grpc/server.py | 30 +++++- daemon/core/emulator/data.py | 1 + daemon/core/emulator/session.py | 3 +- daemon/core/gui/coreclient.py | 33 +++++-- daemon/core/gui/graph/graph.py | 69 ++++++++------ daemon/proto/core/api/grpc/core.proto | 4 +- 8 files changed, 185 insertions(+), 105 deletions(-) diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index 75f9eb2e..5c873a43 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -1,6 +1,6 @@ import logging from queue import Empty, Queue -from typing import Iterable +from typing import Iterable, Optional from core.api.grpc import core_pb2 from core.api.grpc.grpcutils import convert_link @@ -15,7 +15,7 @@ from core.emulator.data import ( from core.emulator.session import Session -def handle_node_event(node_data: NodeData) -> core_pb2.NodeEvent: +def handle_node_event(node_data: NodeData) -> core_pb2.Event: """ Handle node event when there is a node event @@ -36,98 +36,105 @@ def handle_node_event(node_data: NodeData) -> core_pb2.NodeEvent: geo=geo, services=services, ) - return core_pb2.NodeEvent(node=node_proto, source=node_data.source) + node_event = core_pb2.NodeEvent(node=node_proto) + return core_pb2.Event(node_event=node_event, source=node_data.source) -def handle_link_event(event: LinkData) -> core_pb2.LinkEvent: +def handle_link_event(link_data: LinkData) -> core_pb2.Event: """ Handle link event when there is a link event - :param event: link data + :param link_data: link data :return: link event that has message type and link information """ - link = convert_link(event) - return core_pb2.LinkEvent(message_type=event.message_type.value, link=link) + link = convert_link(link_data) + message_type = link_data.message_type.value + link_event = core_pb2.LinkEvent(message_type=message_type, link=link) + return core_pb2.Event(link_event=link_event, source=link_data.source) -def handle_session_event(event: EventData) -> core_pb2.SessionEvent: +def handle_session_event(event_data: EventData) -> core_pb2.Event: """ Handle session event when there is a session event - :param event: event data + :param event_data: event data :return: session event """ - event_time = event.time + event_time = event_data.time if event_time is not None: event_time = float(event_time) - return core_pb2.SessionEvent( - node_id=event.node, - event=event.event_type.value, - name=event.name, - data=event.data, + session_event = core_pb2.SessionEvent( + node_id=event_data.node, + event=event_data.event_type.value, + name=event_data.name, + data=event_data.data, time=event_time, ) + return core_pb2.Event(session_event=session_event) -def handle_config_event(event: ConfigData) -> core_pb2.ConfigEvent: +def handle_config_event(config_data: ConfigData) -> core_pb2.Event: """ Handle configuration event when there is configuration event - :param event: configuration data + :param config_data: configuration data :return: configuration event """ - return core_pb2.ConfigEvent( - message_type=event.message_type, - node_id=event.node, - object=event.object, - type=event.type, - captions=event.captions, - bitmap=event.bitmap, - data_values=event.data_values, - possible_values=event.possible_values, - groups=event.groups, - iface_id=event.iface_id, - network_id=event.network_id, - opaque=event.opaque, - data_types=event.data_types, + config_event = core_pb2.ConfigEvent( + message_type=config_data.message_type, + node_id=config_data.node, + object=config_data.object, + type=config_data.type, + captions=config_data.captions, + bitmap=config_data.bitmap, + data_values=config_data.data_values, + possible_values=config_data.possible_values, + groups=config_data.groups, + iface_id=config_data.iface_id, + network_id=config_data.network_id, + opaque=config_data.opaque, + data_types=config_data.data_types, ) + return core_pb2.Event(config_event=config_event) -def handle_exception_event(event: ExceptionData) -> core_pb2.ExceptionEvent: +def handle_exception_event(exception_data: ExceptionData) -> core_pb2.Event: """ Handle exception event when there is exception event - :param event: exception data + :param exception_data: exception data :return: exception event """ - return core_pb2.ExceptionEvent( - node_id=event.node, - level=event.level.value, - source=event.source, - date=event.date, - text=event.text, - opaque=event.opaque, + exception_event = core_pb2.ExceptionEvent( + node_id=exception_data.node, + level=exception_data.level.value, + source=exception_data.source, + date=exception_data.date, + text=exception_data.text, + opaque=exception_data.opaque, ) + return core_pb2.Event(exception_event=exception_event) -def handle_file_event(event: FileData) -> core_pb2.FileEvent: +def handle_file_event(file_data: FileData) -> core_pb2.Event: """ Handle file event - :param event: file data + :param file_data: file data :return: file event """ - return core_pb2.FileEvent( - message_type=event.message_type.value, - node_id=event.node, - name=event.name, - mode=event.mode, - number=event.number, - type=event.type, - source=event.source, - data=event.data, - compressed_data=event.compressed_data, + file_event = core_pb2.FileEvent( + message_type=file_data.message_type.value, + node_id=file_data.node, + name=file_data.name, + mode=file_data.mode, + number=file_data.number, + type=file_data.type, + source=file_data.source, + data=file_data.data, + compressed_data=file_data.compressed_data, ) + return core_pb2.Event(file_event=file_event) class EventStreamer: @@ -168,32 +175,33 @@ class EventStreamer: if core_pb2.EventType.SESSION in self.event_types: self.session.event_handlers.append(self.queue.put) - def process(self) -> core_pb2.Event: + def process(self) -> Optional[core_pb2.Event]: """ Process the next event in the queue. :return: grpc event, or None when invalid event or queue timeout """ - event = core_pb2.Event(session_id=self.session.id) + event = None try: data = self.queue.get(timeout=1) if isinstance(data, NodeData): - event.node_event.CopyFrom(handle_node_event(data)) + event = handle_node_event(data) elif isinstance(data, LinkData): - event.link_event.CopyFrom(handle_link_event(data)) + event = handle_link_event(data) elif isinstance(data, EventData): - event.session_event.CopyFrom(handle_session_event(data)) + event = handle_session_event(data) elif isinstance(data, ConfigData): - event.config_event.CopyFrom(handle_config_event(data)) + event = handle_config_event(data) elif isinstance(data, ExceptionData): - event.exception_event.CopyFrom(handle_exception_event(data)) + event = handle_exception_event(data) elif isinstance(data, FileData): - event.file_event.CopyFrom(handle_file_event(data)) + event = handle_file_event(data) else: logging.error("unknown event: %s", data) - event = None except Empty: - event = None + pass + if event: + event.session_id = self.session.id return event def remove_handlers(self) -> None: diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 8df545cd..ed40a75b 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -435,6 +435,24 @@ def get_service_configuration(service: CoreService) -> NodeServiceData: ) +def iface_to_data(iface: CoreInterface) -> InterfaceData: + ip4 = iface.get_ip4() + ip4_addr = str(ip4.ip) if ip4 else None + ip4_mask = ip4.prefixlen if ip4 else None + ip6 = iface.get_ip6() + ip6_addr = str(ip6.ip) if ip6 else None + ip6_mask = ip6.prefixlen if ip6 else None + return InterfaceData( + id=iface.node_id, + name=iface.name, + mac=str(iface.mac), + ip4=ip4_addr, + ip4_mask=ip4_mask, + ip6=ip6_addr, + ip6_mask=ip6_mask, + ) + + def iface_to_proto(iface: CoreInterface) -> core_pb2.Interface: """ Convenience for converting a core interface to the protobuf representation. diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 1964b6e8..2883103d 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -108,7 +108,7 @@ from core.api.grpc.wlan_pb2 import ( WlanLinkResponse, ) from core.emulator.coreemu import CoreEmu -from core.emulator.data import LinkData, LinkOptions, NodeOptions +from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags from core.emulator.session import NT, Session from core.errors import CoreCommandError, CoreError @@ -853,6 +853,22 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): node1_iface, node2_iface = session.add_link( node1_id, node2_id, iface1_data, iface2_data, options, link_type ) + iface1_data = None + if node1_iface: + iface1_data = grpcutils.iface_to_data(node1_iface) + iface2_data = None + if node2_iface: + iface2_data = grpcutils.iface_to_data(node2_iface) + source = request.source if request.source else None + link_data = LinkData( + message_type=MessageFlags.ADD, + node1_id=node1_id, + node2_id=node2_id, + iface1=iface1_data, + iface2=iface2_data, + source=source, + ) + session.broadcast_link(link_data) iface1_proto = None iface2_proto = None if node1_iface: @@ -912,6 +928,18 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): iface1_id = request.iface1_id iface2_id = request.iface2_id session.delete_link(node1_id, node2_id, iface1_id, iface2_id) + iface1 = InterfaceData(id=iface1_id) + iface2 = InterfaceData(id=iface2_id) + source = request.source if request.source else None + link_data = LinkData( + message_type=MessageFlags.DELETE, + node1_id=node1_id, + node2_id=node2_id, + iface1=iface1, + iface2=iface2, + source=source, + ) + session.broadcast_link(link_data) return core_pb2.DeleteLinkResponse(result=True) def GetHooks( diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index 22d10d2d..15d922a9 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -190,6 +190,7 @@ class LinkData: iface2: InterfaceData = None options: LinkOptions = LinkOptions() color: str = None + source: str = None class IpPrefixes: diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index c2573578..d2f64dde 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -833,11 +833,12 @@ class Session: for handler in self.config_handlers: handler(config_data) - def broadcast_link(self, link_data: LinkData) -> None: + def broadcast_link(self, link_data: LinkData, source: str = None) -> None: """ Handle link data that should be provided to link handlers. :param link_data: link data to send out + :param source: source of broadcast, None by default :return: nothing """ for handler in self.link_handlers: diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index d35f62e5..a5b96e17 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -148,6 +148,8 @@ class CoreClient: self.custom_observers[observer.name] = observer def handle_events(self, event: Event) -> None: + if event.source == GUI_SOURCE: + return if event.session_id != self.session_id: logging.warning( "ignoring event session(%s) current(%s)", @@ -193,19 +195,32 @@ class CoreClient: return canvas_node1 = self.canvas_nodes[node1_id] canvas_node2 = self.canvas_nodes[node2_id] - if event.message_type == MessageType.ADD: - self.app.canvas.add_wireless_edge(canvas_node1, canvas_node2, event.link) - elif event.message_type == MessageType.DELETE: - self.app.canvas.delete_wireless_edge(canvas_node1, canvas_node2, event.link) - elif event.message_type == MessageType.NONE: - self.app.canvas.update_wireless_edge(canvas_node1, canvas_node2, event.link) + if event.link.type == LinkType.WIRELESS: + if event.message_type == MessageType.ADD: + self.app.canvas.add_wireless_edge( + canvas_node1, canvas_node2, event.link + ) + elif event.message_type == MessageType.DELETE: + self.app.canvas.delete_wireless_edge( + canvas_node1, canvas_node2, event.link + ) + elif event.message_type == MessageType.NONE: + self.app.canvas.update_wireless_edge( + canvas_node1, canvas_node2, event.link + ) + else: + logging.warning("unknown link event: %s", event) else: - logging.warning("unknown link event: %s", event) + if event.message_type == MessageType.ADD: + 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) + else: + logging.warning("unknown link event: %s", event) def handle_node_event(self, event: NodeEvent) -> None: logging.debug("node event: %s", event) - if event.source == GUI_SOURCE: - return node_id = event.node.id x = event.node.position.x y = event.node.position.y diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index a3520d22..4e0358a5 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -225,6 +225,43 @@ class CanvasGraph(tk.Canvas): self.tag_lower(tags.GRIDLINE) 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) + if token in self.edges and link.options.unidirectional: + edge = self.edges[token] + edge.asymmetric_link = link + elif token not in self.edges: + node1 = src.core_node + node2 = dst.core_node + 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.HasField("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.HasField("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) + edge = self.edges.get(token) + if not edge: + return + self.delete_edge(edge) + 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) @@ -297,41 +334,11 @@ class CanvasGraph(tk.Canvas): for link in session.links: logging.debug("drawing link: %s", link) canvas_node1 = self.core.canvas_nodes[link.node1_id] - node1 = canvas_node1.core_node canvas_node2 = self.core.canvas_nodes[link.node2_id] - node2 = canvas_node2.core_node - token = create_edge_token(canvas_node1.id, canvas_node2.id) - if link.type == LinkType.WIRELESS: self.add_wireless_edge(canvas_node1, canvas_node2, link) else: - if token not in self.edges: - src_pos = (node1.position.x, node1.position.y) - dst_pos = (node2.position.x, node2.position.y) - edge = CanvasEdge(self, canvas_node1.id, src_pos, dst_pos) - edge.token = token - edge.dst = canvas_node2.id - edge.set_link(link) - edge.check_wireless() - canvas_node1.edges.add(edge) - canvas_node2.edges.add(edge) - self.edges[edge.token] = edge - self.core.links[edge.token] = edge - if link.HasField("iface1"): - iface1 = link.iface1 - self.core.iface_to_edge[(node1.id, iface1.id)] = token - canvas_node1.ifaces[iface1.id] = iface1 - edge.src_iface = iface1 - if link.HasField("iface2"): - iface2 = link.iface2 - self.core.iface_to_edge[(node2.id, iface2.id)] = edge.token - canvas_node2.ifaces[iface2.id] = iface2 - edge.dst_iface = iface2 - elif link.options.unidirectional: - edge = self.edges[token] - edge.asymmetric_link = link - else: - logging.error("duplicate link received: %s", link) + self.add_wired_edge(canvas_node1, canvas_node2, link) def stopped_session(self) -> None: # clear wireless edges diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 46e1da91..6b1b304c 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -343,11 +343,11 @@ message Event { FileEvent file_event = 6; } int32 session_id = 7; + string source = 8; } message NodeEvent { Node node = 1; - string source = 2; } message LinkEvent { @@ -488,6 +488,7 @@ message GetNodeLinksResponse { message AddLinkRequest { int32 session_id = 1; Link link = 2; + string source = 3; } message AddLinkResponse { @@ -515,6 +516,7 @@ message DeleteLinkRequest { int32 node2_id = 3; int32 iface1_id = 4; int32 iface2_id = 5; + string source = 6; } message DeleteLinkResponse { From f921fa45c549ea95932a28497f12094c7a18d92a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 26 Jun 2020 14:44:13 -0700 Subject: [PATCH 0412/1131] grpc: updated client methods to allow passing source for add_link/delete_link, None by default --- daemon/core/api/grpc/client.py | 9 ++++++++- daemon/core/emulator/session.py | 3 +-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 5aa6713d..939d7eef 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -614,6 +614,7 @@ class CoreGrpcClient: iface1: core_pb2.Interface = None, iface2: core_pb2.Interface = None, options: core_pb2.LinkOptions = None, + source: str = None, ) -> core_pb2.AddLinkResponse: """ Add a link between nodes. @@ -624,6 +625,7 @@ class CoreGrpcClient: :param iface1: node one interface data :param iface2: node two interface data :param options: options for link (jitter, bandwidth, etc) + :param source: application source adding link :return: response with result of success or failure :raises grpc.RpcError: when session or one of the nodes don't exist """ @@ -635,7 +637,9 @@ class CoreGrpcClient: iface2=iface2, options=options, ) - request = core_pb2.AddLinkRequest(session_id=session_id, link=link) + request = core_pb2.AddLinkRequest( + session_id=session_id, link=link, source=source + ) return self.stub.AddLink(request) def edit_link( @@ -676,6 +680,7 @@ class CoreGrpcClient: node2_id: int, iface1_id: int = None, iface2_id: int = None, + source: str = None, ) -> core_pb2.DeleteLinkResponse: """ Delete a link between nodes. @@ -685,6 +690,7 @@ class CoreGrpcClient: :param node2_id: node two id :param iface1_id: node one interface id :param iface2_id: node two interface id + :param source: application source deleting link :return: response with result of success or failure :raises grpc.RpcError: when session doesn't exist """ @@ -694,6 +700,7 @@ class CoreGrpcClient: node2_id=node2_id, iface1_id=iface1_id, iface2_id=iface2_id, + source=source, ) return self.stub.DeleteLink(request) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index d2f64dde..c2573578 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -833,12 +833,11 @@ class Session: for handler in self.config_handlers: handler(config_data) - def broadcast_link(self, link_data: LinkData, source: str = None) -> None: + def broadcast_link(self, link_data: LinkData) -> None: """ Handle link data that should be provided to link handlers. :param link_data: link data to send out - :param source: source of broadcast, None by default :return: nothing """ for handler in self.link_handlers: From f4a3fe6b7b0ee2dc060cb8758cc4d1c4414c454e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 26 Jun 2020 15:14:30 -0700 Subject: [PATCH 0413/1131] grpc/pygui: edit_link will now broadcast link changes, pygui now handles receiving this data --- daemon/core/api/grpc/client.py | 9 ++++++--- daemon/core/api/grpc/server.py | 14 ++++++++++++++ daemon/core/gui/coreclient.py | 4 ++++ daemon/core/gui/graph/graph.py | 12 +++++++++--- daemon/proto/core/api/grpc/core.proto | 1 + 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 939d7eef..e73b9fc2 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -509,7 +509,7 @@ class CoreGrpcClient: :param node_id: node id :param position: position to set node to :param icon: path to icon for gui to use for node - :param source: application source editing node + :param source: application source :param geo: lon,lat,alt location for node :return: response with result of success or failure :raises grpc.RpcError: when session or node doesn't exist @@ -625,7 +625,7 @@ class CoreGrpcClient: :param iface1: node one interface data :param iface2: node two interface data :param options: options for link (jitter, bandwidth, etc) - :param source: application source adding link + :param source: application source :return: response with result of success or failure :raises grpc.RpcError: when session or one of the nodes don't exist """ @@ -650,6 +650,7 @@ class CoreGrpcClient: options: core_pb2.LinkOptions, iface1_id: int = None, iface2_id: int = None, + source: str = None, ) -> core_pb2.EditLinkResponse: """ Edit a link between nodes. @@ -660,6 +661,7 @@ class CoreGrpcClient: :param options: options for link (jitter, bandwidth, etc) :param iface1_id: node one interface id :param iface2_id: node two interface id + :param source: application source :return: response with result of success or failure :raises grpc.RpcError: when session or one of the nodes don't exist """ @@ -670,6 +672,7 @@ class CoreGrpcClient: options=options, iface1_id=iface1_id, iface2_id=iface2_id, + source=source, ) return self.stub.EditLink(request) @@ -690,7 +693,7 @@ class CoreGrpcClient: :param node2_id: node two id :param iface1_id: node one interface id :param iface2_id: node two interface id - :param source: application source deleting link + :param source: application source :return: response with result of success or failure :raises grpc.RpcError: when session doesn't exist """ diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 2883103d..65c7281e 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -866,6 +866,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): node2_id=node2_id, iface1=iface1_data, iface2=iface2_data, + options=options, source=source, ) session.broadcast_link(link_data) @@ -909,6 +910,19 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): key=options_proto.key, ) session.update_link(node1_id, node2_id, iface1_id, iface2_id, options) + iface1 = InterfaceData(id=iface1_id) + iface2 = InterfaceData(id=iface2_id) + source = request.source if request.source else None + link_data = LinkData( + message_type=MessageFlags.NONE, + node1_id=node1_id, + node2_id=node2_id, + iface1=iface1, + iface2=iface2, + options=options, + source=source, + ) + session.broadcast_link(link_data) return core_pb2.EditLinkResponse(result=True) def DeleteLink( diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index a5b96e17..b29c044e 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -216,6 +216,10 @@ class CoreClient: self.app.canvas.organize() elif event.message_type == MessageType.DELETE: self.app.canvas.delete_wired_edge(canvas_node1, canvas_node2) + elif event.message_type == MessageType.NONE: + self.app.canvas.update_wired_edge( + canvas_node1, canvas_node2, event.link + ) else: logging.warning("unknown link event: %s", event) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 4e0358a5..436de383 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -262,6 +262,13 @@ class CanvasGraph(tk.Canvas): return self.delete_edge(edge) + def update_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None: + token = create_edge_token(src.id, dst.id) + edge = self.edges.get(token) + if not edge: + return + edge.link.options.CopyFrom(link.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) @@ -350,9 +357,8 @@ class CanvasGraph(tk.Canvas): dst_node.wireless_edges.remove(edge) self.wireless_edges.clear() - # clear all middle edge labels - for edge in self.edges.values(): - edge.reset() + # clear throughputs + self.clear_throughputs() def canvas_xy(self, event: tk.Event) -> Tuple[float, float]: """ diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 6b1b304c..7d7592b1 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -504,6 +504,7 @@ message EditLinkRequest { int32 iface1_id = 4; int32 iface2_id = 5; LinkOptions options = 6; + string source = 7; } message EditLinkResponse { From e79645013be7733fabb9e0aaa68ce6873805e89b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 26 Jun 2020 21:45:29 -0700 Subject: [PATCH 0414/1131] grpc/pygui: updated delete_node to use the source, updated pygui to support delete node events --- daemon/core/api/grpc/client.py | 9 +++++++-- daemon/core/api/grpc/events.py | 3 ++- daemon/core/api/grpc/server.py | 9 ++++++++- daemon/core/gui/coreclient.py | 13 ++++++++++--- daemon/core/gui/graph/node.py | 4 ++-- daemon/proto/core/api/grpc/core.proto | 2 ++ 6 files changed, 31 insertions(+), 9 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index e73b9fc2..2740e770 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -536,16 +536,21 @@ class CoreGrpcClient: """ return self.stub.MoveNodes(move_iterator) - def delete_node(self, session_id: int, node_id: int) -> core_pb2.DeleteNodeResponse: + def delete_node( + self, session_id: int, node_id: int, source: str = None + ) -> core_pb2.DeleteNodeResponse: """ Delete node from session. :param session_id: session id :param node_id: node id + :param source: application source :return: response with result of success or failure :raises grpc.RpcError: when session doesn't exist """ - request = core_pb2.DeleteNodeRequest(session_id=session_id, node_id=node_id) + request = core_pb2.DeleteNodeRequest( + session_id=session_id, node_id=node_id, source=source + ) return self.stub.DeleteNode(request) def node_command( diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index 5c873a43..fb6eaff8 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -36,7 +36,8 @@ def handle_node_event(node_data: NodeData) -> core_pb2.Event: geo=geo, services=services, ) - node_event = core_pb2.NodeEvent(node=node_proto) + message_type = node_data.message_type.value + node_event = core_pb2.NodeEvent(message_type=message_type, node=node_proto) return core_pb2.Event(node_event=node_event, source=node_data.source) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 65c7281e..8851116d 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -775,7 +775,14 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("delete node: %s", request) session = self.get_session(request.session_id, context) - result = session.delete_node(request.node_id) + result = False + try: + node = self.get_node(session, request.node_id, context, NodeBase) + result = session.delete_node(node.id) + source = request.source if request.source else None + session.broadcast_node(node, MessageFlags.DELETE, source) + except grpc.RpcError: + pass return core_pb2.DeleteNodeResponse(result=result) def NodeCommand( diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index b29c044e..cf870cf2 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -226,10 +226,17 @@ class CoreClient: def handle_node_event(self, event: NodeEvent) -> None: logging.debug("node event: %s", event) node_id = event.node.id - x = event.node.position.x - y = event.node.position.y canvas_node = self.canvas_nodes[node_id] - canvas_node.move(x, y) + if event.message_type == MessageType.NONE: + x = event.node.position.x + y = event.node.position.y + canvas_node.move(x, y) + elif event.message_type == MessageType.DELETE: + self.app.canvas.clear_selection() + self.app.canvas.select_object(canvas_node.id) + self.app.canvas.delete_selected_objects() + else: + logging.warning("unknown node event: %s", event) def enable_throughputs(self) -> None: self.handling_throughputs = self.client.throughputs( diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index d98c4e48..6e8185b8 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -271,12 +271,12 @@ class CanvasNode: def canvas_delete(self) -> None: self.canvas.clear_selection() - self.canvas.selection[self.id] = self + self.canvas.select_object(self.id) self.canvas.delete_selected_objects() def canvas_copy(self) -> None: self.canvas.clear_selection() - self.canvas.selection[self.id] = self + self.canvas.select_object(self.id) self.canvas.copy() def show_config(self) -> None: diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 7d7592b1..22cd9c54 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -348,6 +348,7 @@ message Event { message NodeEvent { Node node = 1; + MessageType.Enum message_type = 2; } message LinkEvent { @@ -435,6 +436,7 @@ message EditNodeResponse { message DeleteNodeRequest { int32 session_id = 1; int32 node_id = 2; + string source = 3; } message DeleteNodeResponse { From 5eae67aac59fb6e93259750517588901acb651bb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 26 Jun 2020 22:11:36 -0700 Subject: [PATCH 0415/1131] grpc/pygui: updated add_node source support, updated pygui to handle add_node events --- daemon/core/api/grpc/client.py | 7 +++-- daemon/core/api/grpc/server.py | 2 ++ daemon/core/gui/coreclient.py | 6 +++-- daemon/core/gui/graph/graph.py | 39 +++++++++++++++++---------- daemon/proto/core/api/grpc/core.proto | 1 + 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 2740e770..82164fe3 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -468,17 +468,20 @@ class CoreGrpcClient: return stream def add_node( - self, session_id: int, node: core_pb2.Node + self, session_id: int, node: core_pb2.Node, source: str = None ) -> core_pb2.AddNodeResponse: """ Add node to session. :param session_id: session id :param node: node to add + :param source: source application :return: response with node id :raises grpc.RpcError: when session doesn't exist """ - request = core_pb2.AddNodeRequest(session_id=session_id, node=node) + request = core_pb2.AddNodeRequest( + session_id=session_id, node=node, source=source + ) return self.stub.AddNode(request) def get_node(self, session_id: int, node_id: int) -> core_pb2.GetNodeResponse: diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 8851116d..27702629 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -668,6 +668,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): _type, _id, options = grpcutils.add_node_data(request.node) _class = session.get_node_class(_type) node = session.add_node(_class, _id, options) + source = request.source if request.source else None + session.broadcast_node(node, MessageFlags.ADD, source) return core_pb2.AddNodeResponse(node_id=node.id) def GetNode( diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index cf870cf2..cf331676 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -225,16 +225,18 @@ class CoreClient: def handle_node_event(self, event: NodeEvent) -> None: logging.debug("node event: %s", event) - node_id = event.node.id - canvas_node = self.canvas_nodes[node_id] if event.message_type == MessageType.NONE: + canvas_node = self.canvas_nodes[event.node.id] x = event.node.position.x y = event.node.position.y canvas_node.move(x, y) elif event.message_type == MessageType.DELETE: + canvas_node = self.canvas_nodes[event.node.id] self.app.canvas.clear_selection() self.app.canvas.select_object(canvas_node.id) self.app.canvas.delete_selected_objects() + elif event.message_type == MessageType.ADD: + self.app.canvas.add_core_node(event.node) else: logging.warning("unknown node event: %s", event) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 436de383..9cb3b109 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -7,7 +7,14 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from PIL import Image from PIL.ImageTk import PhotoImage -from core.api.grpc.core_pb2 import Interface, Link, LinkType, Session, ThroughputsEvent +from core.api.grpc.core_pb2 import ( + Interface, + Link, + LinkType, + Node, + Session, + ThroughputsEvent, +) from core.gui.dialogs.shapemod import ShapeDialog from core.gui.graph import tags from core.gui.graph.edges import ( @@ -315,29 +322,33 @@ class CanvasGraph(tk.Canvas): edge = self.wireless_edges[token] edge.middle_label_text(link.label) + def add_core_node(self, core_node: Node) -> None: + if core_node.id in self.core.canvas_nodes: + logging.error("core node already exists: %s", core_node) + return + logging.debug("adding node %s", core_node) + # if the gui can't find node's image, default to the "edit-node" image + image = NodeUtils.node_image(core_node, self.app.guiconfig, self.app.app_scale) + if not image: + image = self.app.get_icon(ImageEnum.EDITNODE, ICON_SIZE) + x = core_node.position.x + y = core_node.position.y + node = CanvasNode(self.app, x, y, core_node, image) + self.nodes[node.id] = node + self.core.canvas_nodes[core_node.id] = node + def draw_session(self, session: Session) -> None: """ Draw existing session. """ # draw existing nodes for core_node in session.nodes: - logging.debug("drawing node %s", core_node) # peer to peer node is not drawn on the GUI if NodeUtils.is_ignore_node(core_node.type): continue - image = NodeUtils.node_image( - core_node, self.app.guiconfig, self.app.app_scale - ) - # if the gui can't find node's image, default to the "edit-node" image - if not image: - image = self.app.get_icon(ImageEnum.EDITNODE, ICON_SIZE) - x = core_node.position.x - y = core_node.position.y - node = CanvasNode(self.app, x, y, core_node, image) - self.nodes[node.id] = node - self.core.canvas_nodes[core_node.id] = node + self.add_core_node(core_node) - # draw existing links + # draw existing links for link in session.links: logging.debug("drawing link: %s", link) canvas_node1 = self.core.canvas_nodes[link.node1_id] diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 22cd9c54..8112c9d1 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -404,6 +404,7 @@ message FileEvent { message AddNodeRequest { int32 session_id = 1; Node node = 2; + string source = 3; } message AddNodeResponse { From c8daeb02d82db91914e13753db3d785ad145df1a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 26 Jun 2020 22:29:17 -0700 Subject: [PATCH 0416/1131] grpc: fixed issue with not catching error in delete_node from broadcast changes --- daemon/core/api/grpc/server.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 27702629..e8469177 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -778,13 +778,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("delete node: %s", request) session = self.get_session(request.session_id, context) result = False - try: + if request.node_id in session.nodes: node = self.get_node(session, request.node_id, context, NodeBase) result = session.delete_node(node.id) source = request.source if request.source else None session.broadcast_node(node, MessageFlags.DELETE, source) - except grpc.RpcError: - pass return core_pb2.DeleteNodeResponse(result=result) def NodeCommand( From 59e7395a4f30cccbaaac0e693922c11333bd7f5a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 29 Jun 2020 23:00:33 -0700 Subject: [PATCH 0417/1131] initial addition of core-cli script that can be used to run commands and query information with sessions using grpc, similar in concept to coresendmsg --- daemon/scripts/core-cli | 447 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 447 insertions(+) create mode 100755 daemon/scripts/core-cli diff --git a/daemon/scripts/core-cli b/daemon/scripts/core-cli new file mode 100755 index 00000000..ca9011b5 --- /dev/null +++ b/daemon/scripts/core-cli @@ -0,0 +1,447 @@ +#!/usr/bin/env python3 +import sys +from argparse import ( + ArgumentDefaultsHelpFormatter, + ArgumentParser, + ArgumentTypeError, + Namespace, + _SubParsersAction, +) +from typing import Tuple + +import netaddr +from google.protobuf.json_format import MessageToJson + +from core.api.grpc.client import CoreGrpcClient +from core.api.grpc.core_pb2 import ( + Geo, + Interface, + LinkOptions, + Node, + NodeType, + Position, + SessionState, +) + +NODE_TYPES = [k for k, v in NodeType.Enum.items() if v != NodeType.PEER_TO_PEER] + + +def mac_type(value: str) -> str: + if not netaddr.valid_mac(value): + raise ArgumentTypeError("invalid mac address") + return value + + +def ip4_type(value: str) -> str: + if not netaddr.valid_ipv4(value): + raise ArgumentTypeError("invalid ip4 address") + return value + + +def ip6_type(value: str) -> str: + if not netaddr.valid_ipv6(value): + raise ArgumentTypeError("invalid ip6 address") + return value + + +def position_type(value: str) -> Tuple[float, float]: + error = "invalid position, must be in the format: float,float" + try: + values = [float(x) for x in value.split(",")] + except ValueError: + raise ArgumentTypeError(error) + if len(values) != 2: + raise ArgumentTypeError(error) + x, y = values + return x, y + + +def geo_type(value: str) -> Tuple[float, float, float]: + error = "invalid geo, must be in the format: float,float,float" + try: + values = [float(x) for x in value.split(",")] + except ValueError: + raise ArgumentTypeError(error) + if len(values) != 3: + raise ArgumentTypeError(error) + lon, lat, alt = values + return lon, lat, alt + + +def get_current_session() -> int: + core = CoreGrpcClient() + with core.context_connect(): + response = core.get_sessions() + if not response.sessions: + print("no current session to interact with") + sys.exit(1) + return response.sessions[0].id + + +def print_interface_header() -> None: + print("ID | MAC Address | IP4 Address | IP6 Address") + + +def print_interface(iface: Interface) -> None: + iface_ip4 = f"{iface.ip4}/{iface.ip4_mask}" if iface.ip4 else None + iface_ip6 = f"{iface.ip6}/{iface.ip6_mask}" if iface.ip6 else None + print(f"{iface.id:<3} | {iface.mac:<11} | {iface_ip4:<18} | {iface_ip6}") + + +def query_sessions(args: Namespace) -> None: + core = CoreGrpcClient() + with core.context_connect(): + response = core.get_sessions() + if args.json: + json = MessageToJson(response, preserving_proto_field_name=True) + print(json) + else: + print("Session ID | Session State | Nodes") + for s in response.sessions: + state = SessionState.Enum.Name(s.state) + print(f"{s.id:<10} | {state:<13} | {s.nodes}") + + +def query_session(args: Namespace) -> None: + core = CoreGrpcClient() + with core.context_connect(): + response = core.get_session(args.id) + if args.json: + json = MessageToJson(response, preserving_proto_field_name=True) + print(json) + else: + print("Nodes") + print("Node ID | Node Name | Node Type") + names = {} + for node in response.session.nodes: + names[node.id] = node.name + node_type = NodeType.Enum.Name(node.type) + print(f"{node.id:<7} | {node.name:<9} | {node_type}") + + print("\nLinks") + for link in response.session.links: + n1 = names[link.node1_id] + n2 = names[link.node2_id] + print(f"Node | ", end="") + print_interface_header() + if link.HasField("iface1"): + print(f"{n1:<6} | ", end="") + print_interface(link.iface1) + if link.HasField("iface2"): + print(f"{n2:<6} | ", end="") + print_interface(link.iface2) + print() + + +def query_node(args: Namespace) -> None: + core = CoreGrpcClient() + with core.context_connect(): + response = core.get_node(args.id, args.node) + if args.json: + json = MessageToJson(response, preserving_proto_field_name=True) + print(json) + else: + node = response.node + node_type = NodeType.Enum.Name(node.type) + print("ID | Name | Type") + print(f"{node.id:<4} | {node.name:<7} | {node_type}") + print("Interfaces") + print_interface_header() + for iface in response.ifaces: + print_interface(iface) + + +def add_node(args: Namespace) -> None: + session_id = get_current_session() + node_type = NodeType.Enum.Value(args.type) + pos = None + if args.pos: + x, y = args.pos + pos = Position(x=x, y=y) + geo = None + if args.geo: + lon, lat, alt = args.geo + geo = Geo(lon=lon, lat=lat, alt=alt) + core = CoreGrpcClient() + with core.context_connect(): + node = Node( + id=args.id, + name=args.name, + type=node_type, + model=args.model, + emane=args.emane, + icon=args.icon, + image=args.image, + position=pos, + geo=geo, + ) + response = core.add_node(session_id, node) + if args.json: + json = MessageToJson(response, preserving_proto_field_name=True) + print(json) + else: + print(f"created node: {response.node_id}") + + +def edit_node(args: Namespace) -> None: + session_id = get_current_session() + pos = None + if args.pos: + x, y = args.pos + pos = Position(x=x, y=y) + geo = None + if args.geo: + lon, lat, alt = args.geo + geo = Geo(lon=lon, lat=lat, alt=alt) + core = CoreGrpcClient() + with core.context_connect(): + response = core.edit_node(session_id, args.id, pos, args.icon, geo=geo) + if args.json: + json = MessageToJson(response, preserving_proto_field_name=True) + print(json) + else: + print(f"edit node: {response.result}") + + +def delete_node(args: Namespace) -> None: + session_id = get_current_session() + core = CoreGrpcClient() + with core.context_connect(): + response = core.delete_node(session_id, args.id) + if args.json: + json = MessageToJson(response, preserving_proto_field_name=True) + print(json) + else: + print(f"deleted node: {response.result}") + + +def add_link(args: Namespace) -> None: + session_id = get_current_session() + iface1 = None + if args.iface1_id is not None: + iface1 = Interface( + id=args.iface1_id, + mac=args.iface1_mac, + ip4=args.iface1_ip4, + ip4_mask=args.iface1_ip4_mask, + ip6=args.iface1_ip4, + ip6_mask=args.iface1_ip6_mask, + ) + iface2 = None + if args.iface2_id is not None: + iface2 = Interface( + id=args.iface2_id, + mac=args.iface2_mac, + ip4=args.iface2_ip4, + ip4_mask=args.iface2_ip4_mask, + ip6=args.iface2_ip4, + ip6_mask=args.iface2_ip6_mask, + ) + options = LinkOptions( + bandwidth=args.bandwidth, + loss=args.loss, + jitter=args.jitter, + delay=args.delay, + dup=args.dup, + unidirectional=args.uni, + ) + core = CoreGrpcClient() + with core.context_connect(): + response = core.add_link(session_id, args.node1, args.node2, iface1, iface2, options) + if args.json: + json = MessageToJson(response, preserving_proto_field_name=True) + print(json) + else: + print(f"edit link: {response.result}") + + +def edit_link(args: Namespace) -> None: + session_id = get_current_session() + options = LinkOptions( + bandwidth=args.bandwidth, + loss=args.loss, + jitter=args.jitter, + delay=args.delay, + dup=args.dup, + unidirectional=args.uni, + ) + core = CoreGrpcClient() + with core.context_connect(): + response = core.edit_link( + session_id, args.node1, args.node2, options, args.iface1, args.iface2 + ) + if args.json: + json = MessageToJson(response, preserving_proto_field_name=True) + print(json) + else: + print(f"edit link: {response.result}") + + +def delete_link(args: Namespace) -> None: + session_id = get_current_session() + core = CoreGrpcClient() + with core.context_connect(): + response = core.delete_link(session_id, args.node1, args.node2, args.iface1, args.iface2) + if args.json: + json = MessageToJson(response, preserving_proto_field_name=True) + print(json) + else: + print(f"delete link: {response.result}") + + +def setup_node_parser(parent: _SubParsersAction) -> None: + parser = parent.add_parser("node", help="node interactions") + parser.add_argument("--session", type=int, help="session to interact with") + subparsers = parser.add_subparsers(help="node commands") + subparsers.required = True + subparsers.dest = "command" + + add_parser = subparsers.add_parser("add", help="add a node") + add_parser.formatter_class = ArgumentDefaultsHelpFormatter + add_parser.add_argument("--id", type=int, help="id to use, optional") + add_parser.add_argument("--name", help="name to use, optional") + add_parser.add_argument( + "--type", choices=NODE_TYPES, default="DEFAULT", help="type of node" + ) + add_parser.add_argument("--model", help="used to determine services, optional") + group = add_parser.add_mutually_exclusive_group(required=True) + group.add_argument("--pos", type=position_type, help="x,y position") + group.add_argument("--geo", type=geo_type, help="lon,lat,alt position") + add_parser.add_argument("--icon", help="icon to use, optional") + add_parser.add_argument("--image", help="container image, optional") + add_parser.add_argument( + "--emane", help="emane model, only required for emane nodes" + ) + add_parser.set_defaults(func=add_node) + + edit_parser = subparsers.add_parser("edit", help="edit a node") + edit_parser.formatter_class = ArgumentDefaultsHelpFormatter + edit_parser.add_argument("--id", type=int, help="id to use, optional") + group = edit_parser.add_mutually_exclusive_group(required=True) + group.add_argument("--pos", type=position_type, help="x,y position") + group.add_argument("--geo", type=geo_type, help="lon,lat,alt position") + edit_parser.add_argument("--icon", help="icon to use, optional") + edit_parser.set_defaults(func=edit_node) + + delete_parser = subparsers.add_parser("delete", help="delete a node") + delete_parser.formatter_class = ArgumentDefaultsHelpFormatter + delete_parser.add_argument( + "--id", type=int, help="node id to delete", required=True + ) + delete_parser.set_defaults(func=delete_node) + + +def setup_link_parser(parent: _SubParsersAction) -> None: + parser = parent.add_parser("link", help="link interactions") + parser.add_argument("--session", type=int, help="session to interact with") + subparsers = parser.add_subparsers(help="link commands") + subparsers.required = True + subparsers.dest = "command" + + add_parser = subparsers.add_parser("add", help="add a node") + add_parser.formatter_class = ArgumentDefaultsHelpFormatter + add_parser.add_argument( + "--node1", type=int, help="node1 id for link", required=True + ) + add_parser.add_argument( + "--node2", type=int, help="node1 id for link", required=True + ) + add_parser.add_argument("--iface1-id", type=int, help="node1 interface id for link") + add_parser.add_argument("--iface1-mac", type=mac_type, help="node1 interface mac") + add_parser.add_argument("--iface1-ip4", type=ip4_type, help="node1 interface ip4") + add_parser.add_argument( + "--iface1-ip4-mask", type=int, help="node1 interface ip4 mask" + ) + add_parser.add_argument("--iface1-ip6", type=ip6_type, help="node1 interface ip6") + add_parser.add_argument( + "--iface1-ip6-mask", type=int, help="node1 interface ip6 mask" + ) + add_parser.add_argument("--iface2-id", type=int, help="node1 interface id for link") + add_parser.add_argument("--iface2-mac", type=mac_type, help="node1 interface mac") + add_parser.add_argument("--iface2-ip4", type=ip4_type, help="node1 interface ip4") + add_parser.add_argument( + "--iface2-ip4-mask", type=int, help="node1 interface ip4 mask" + ) + add_parser.add_argument("--iface2-ip6", type=ip6_type, help="node1 interface ip6") + add_parser.add_argument( + "--iface2-ip6-mask", type=int, help="node1 interface ip6 mask" + ) + add_parser.add_argument("--bandwidth", type=int, help="bandwidth (bps) for link") + add_parser.add_argument("--loss", type=float, help="loss (%) for link") + add_parser.add_argument("--jitter", type=int, help="jitter (us) for link") + add_parser.add_argument("--delay", type=int, help="delay (us) for link") + add_parser.add_argument("--dup", type=int, help="duplicate (%) for link") + add_parser.add_argument( + "--uni", action="store_true", help="is link unidirectional?" + ) + add_parser.set_defaults(func=add_link) + + edit_parser = subparsers.add_parser("edit", help="edit a link") + edit_parser.formatter_class = ArgumentDefaultsHelpFormatter + edit_parser.add_argument( + "--node1", type=int, help="node1 id for link", required=True + ) + edit_parser.add_argument( + "--node2", type=int, help="node1 id for link", required=True + ) + edit_parser.add_argument("--iface1", type=int, help="node1 interface id for link") + edit_parser.add_argument("--iface2", type=int, help="node2 interface id for link") + edit_parser.add_argument("--bandwidth", type=int, help="bandwidth (bps) for link") + edit_parser.add_argument("--loss", type=float, help="loss (%) for link") + edit_parser.add_argument("--jitter", type=int, help="jitter (us) for link") + edit_parser.add_argument("--delay", type=int, help="delay (us) for link") + edit_parser.add_argument("--dup", type=int, help="duplicate (%) for link") + edit_parser.add_argument( + "--uni", action="store_true", help="is link unidirectional?" + ) + edit_parser.set_defaults(func=edit_link) + + delete_parser = subparsers.add_parser("delete", help="delete a link") + delete_parser.formatter_class = ArgumentDefaultsHelpFormatter + delete_parser.add_argument( + "--node1", type=int, help="node1 id for link", required=True + ) + delete_parser.add_argument( + "--node2", type=int, help="node1 id for link", required=True + ) + delete_parser.add_argument("--iface1", type=int, help="node1 interface id for link") + delete_parser.add_argument("--iface2", type=int, help="node2 interface id for link") + delete_parser.set_defaults(func=delete_link) + + +def setup_query_parser(parent: _SubParsersAction) -> None: + parser = parent.add_parser("query", help="query interactions") + subparsers = parser.add_subparsers(help="query commands") + subparsers.required = True + subparsers.dest = "command" + + sessions_parser = subparsers.add_parser("sessions", help="query current sessions") + sessions_parser.set_defaults(func=query_sessions) + + session_parser = subparsers.add_parser("session", help="query session") + session_parser.add_argument("--id", type=int, help="session to query", required=True) + session_parser.set_defaults(func=query_session) + + node_parser = subparsers.add_parser("node", help="query node") + node_parser.add_argument("--id", type=int, help="session to query", required=True) + node_parser.add_argument("--node", type=int, help="node to query", required=True) + node_parser.set_defaults(func=query_node) + + +def main() -> None: + parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) + parser.add_argument( + "-j", "--json", action="store_true", help="print responses to terminal as json" + ) + subparsers = parser.add_subparsers(help="supported commands") + subparsers.required = True + subparsers.dest = "command" + setup_node_parser(subparsers) + setup_link_parser(subparsers) + setup_query_parser(subparsers) + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() From ec845b920c4b55340fb579dbafa849b60470b6e4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 30 Jun 2020 08:27:40 -0700 Subject: [PATCH 0418/1131] removed ip mask options from core-cli add link, combined with ip and will parse input to provide simpler interface --- daemon/scripts/core-cli | 92 ++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/daemon/scripts/core-cli b/daemon/scripts/core-cli index ca9011b5..a8354f65 100755 --- a/daemon/scripts/core-cli +++ b/daemon/scripts/core-cli @@ -11,6 +11,7 @@ from typing import Tuple import netaddr from google.protobuf.json_format import MessageToJson +from netaddr import EUI, AddrFormatError, IPNetwork from core.api.grpc.client import CoreGrpcClient from core.api.grpc.core_pb2 import ( @@ -27,21 +28,31 @@ NODE_TYPES = [k for k, v in NodeType.Enum.items() if v != NodeType.PEER_TO_PEER] def mac_type(value: str) -> str: - if not netaddr.valid_mac(value): - raise ArgumentTypeError("invalid mac address") - return value + try: + mac = EUI(value, dialect=netaddr.mac_unix_expanded) + return str(mac) + except AddrFormatError: + raise ArgumentTypeError(f"invalid mac address: {value}") -def ip4_type(value: str) -> str: - if not netaddr.valid_ipv4(value): - raise ArgumentTypeError("invalid ip4 address") - return value +def ip4_type(value: str) -> IPNetwork: + try: + ip = IPNetwork(value) + if not netaddr.valid_ipv4(str(ip.ip)): + raise ArgumentTypeError(f"invalid ip4 address: {value}") + return ip + except AddrFormatError: + raise ArgumentTypeError(f"invalid ip4 address: {value}") -def ip6_type(value: str) -> str: - if not netaddr.valid_ipv6(value): - raise ArgumentTypeError("invalid ip6 address") - return value +def ip6_type(value: str) -> IPNetwork: + try: + ip = IPNetwork(value) + if not netaddr.valid_ipv6(str(ip.ip)): + raise ArgumentTypeError(f"invalid ip6 address: {value}") + return ip + except AddrFormatError: + raise ArgumentTypeError(f"invalid ip6 address: {value}") def position_type(value: str) -> Tuple[float, float]: @@ -78,11 +89,26 @@ def get_current_session() -> int: return response.sessions[0].id -def print_interface_header() -> None: +def create_iface(iface_id: int, mac: str, ip4_net: IPNetwork, ip6_net: IPNetwork) -> Interface: + ip4 = str(ip4_net.ip) if ip4_net else None + ip4_mask = ip4_net.prefixlen if ip4_net else None + ip6 = str(ip6_net.ip) if ip6_net else None + ip6_mask = ip6_net.prefixlen if ip6_net else None + return Interface( + id=iface_id, + mac=mac, + ip4=ip4, + ip4_mask=ip4_mask, + ip6=ip6, + ip6_mask=ip6_mask, + ) + + +def print_iface_header() -> None: print("ID | MAC Address | IP4 Address | IP6 Address") -def print_interface(iface: Interface) -> None: +def print_iface(iface: Interface) -> None: iface_ip4 = f"{iface.ip4}/{iface.ip4_mask}" if iface.ip4 else None iface_ip6 = f"{iface.ip6}/{iface.ip6_mask}" if iface.ip6 else None print(f"{iface.id:<3} | {iface.mac:<11} | {iface_ip4:<18} | {iface_ip6}") @@ -123,13 +149,13 @@ def query_session(args: Namespace) -> None: n1 = names[link.node1_id] n2 = names[link.node2_id] print(f"Node | ", end="") - print_interface_header() + print_iface_header() if link.HasField("iface1"): print(f"{n1:<6} | ", end="") - print_interface(link.iface1) + print_iface(link.iface1) if link.HasField("iface2"): print(f"{n2:<6} | ", end="") - print_interface(link.iface2) + print_iface(link.iface2) print() @@ -146,9 +172,9 @@ def query_node(args: Namespace) -> None: print("ID | Name | Type") print(f"{node.id:<4} | {node.name:<7} | {node_type}") print("Interfaces") - print_interface_header() + print_iface_header() for iface in response.ifaces: - print_interface(iface) + print_iface(iface) def add_node(args: Namespace) -> None: @@ -219,24 +245,10 @@ def add_link(args: Namespace) -> None: session_id = get_current_session() iface1 = None if args.iface1_id is not None: - iface1 = Interface( - id=args.iface1_id, - mac=args.iface1_mac, - ip4=args.iface1_ip4, - ip4_mask=args.iface1_ip4_mask, - ip6=args.iface1_ip4, - ip6_mask=args.iface1_ip6_mask, - ) + iface1 = create_iface(args.iface1_id, args.iface1_mac, args.iface1_ip4, args.iface1_ip6) iface2 = None if args.iface2_id is not None: - iface2 = Interface( - id=args.iface2_id, - mac=args.iface2_mac, - ip4=args.iface2_ip4, - ip4_mask=args.iface2_ip4_mask, - ip6=args.iface2_ip4, - ip6_mask=args.iface2_ip6_mask, - ) + iface2 = create_iface(args.iface2_id, args.iface2_mac, args.iface2_ip4, args.iface2_ip6) options = LinkOptions( bandwidth=args.bandwidth, loss=args.loss, @@ -349,23 +361,11 @@ def setup_link_parser(parent: _SubParsersAction) -> None: add_parser.add_argument("--iface1-id", type=int, help="node1 interface id for link") add_parser.add_argument("--iface1-mac", type=mac_type, help="node1 interface mac") add_parser.add_argument("--iface1-ip4", type=ip4_type, help="node1 interface ip4") - add_parser.add_argument( - "--iface1-ip4-mask", type=int, help="node1 interface ip4 mask" - ) add_parser.add_argument("--iface1-ip6", type=ip6_type, help="node1 interface ip6") - add_parser.add_argument( - "--iface1-ip6-mask", type=int, help="node1 interface ip6 mask" - ) add_parser.add_argument("--iface2-id", type=int, help="node1 interface id for link") add_parser.add_argument("--iface2-mac", type=mac_type, help="node1 interface mac") add_parser.add_argument("--iface2-ip4", type=ip4_type, help="node1 interface ip4") - add_parser.add_argument( - "--iface2-ip4-mask", type=int, help="node1 interface ip4 mask" - ) add_parser.add_argument("--iface2-ip6", type=ip6_type, help="node1 interface ip6") - add_parser.add_argument( - "--iface2-ip6-mask", type=int, help="node1 interface ip6 mask" - ) add_parser.add_argument("--bandwidth", type=int, help="bandwidth (bps) for link") add_parser.add_argument("--loss", type=float, help="loss (%) for link") add_parser.add_argument("--jitter", type=int, help="jitter (us) for link") From aef3fe8d50e47089abf974e4a28f6bafa65e14c4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 30 Jun 2020 09:25:36 -0700 Subject: [PATCH 0419/1131] updated core-cli to use consistent shorthand options and existing longform options --- daemon/scripts/core-cli | 122 +++++++++++++++++----------------------- 1 file changed, 52 insertions(+), 70 deletions(-) diff --git a/daemon/scripts/core-cli b/daemon/scripts/core-cli index a8354f65..65a92994 100755 --- a/daemon/scripts/core-cli +++ b/daemon/scripts/core-cli @@ -254,7 +254,7 @@ def add_link(args: Namespace) -> None: loss=args.loss, jitter=args.jitter, delay=args.delay, - dup=args.dup, + dup=args.duplicate, unidirectional=args.uni, ) core = CoreGrpcClient() @@ -274,7 +274,7 @@ def edit_link(args: Namespace) -> None: loss=args.loss, jitter=args.jitter, delay=args.delay, - dup=args.dup, + dup=args.duplicate, unidirectional=args.uni, ) core = CoreGrpcClient() @@ -303,109 +303,91 @@ def delete_link(args: Namespace) -> None: def setup_node_parser(parent: _SubParsersAction) -> None: parser = parent.add_parser("node", help="node interactions") - parser.add_argument("--session", type=int, help="session to interact with") + parser.add_argument("-s", "--session", type=int, help="session to interact with") subparsers = parser.add_subparsers(help="node commands") subparsers.required = True subparsers.dest = "command" add_parser = subparsers.add_parser("add", help="add a node") add_parser.formatter_class = ArgumentDefaultsHelpFormatter - add_parser.add_argument("--id", type=int, help="id to use, optional") - add_parser.add_argument("--name", help="name to use, optional") + add_parser.add_argument("-i", "--id", type=int, help="id to use, optional") + add_parser.add_argument("-n", "--name", help="name to use, optional") add_parser.add_argument( - "--type", choices=NODE_TYPES, default="DEFAULT", help="type of node" + "-t", "--type", choices=NODE_TYPES, default="DEFAULT", help="type of node" ) - add_parser.add_argument("--model", help="used to determine services, optional") + add_parser.add_argument("-m", "--model", help="used to determine services, optional") group = add_parser.add_mutually_exclusive_group(required=True) - group.add_argument("--pos", type=position_type, help="x,y position") - group.add_argument("--geo", type=geo_type, help="lon,lat,alt position") - add_parser.add_argument("--icon", help="icon to use, optional") - add_parser.add_argument("--image", help="container image, optional") - add_parser.add_argument( - "--emane", help="emane model, only required for emane nodes" - ) + group.add_argument("-p", "--pos", type=position_type, help="x,y position") + group.add_argument("-g", "--geo", type=geo_type, help="lon,lat,alt position") + add_parser.add_argument("-ic", "--icon", help="icon to use, optional") + add_parser.add_argument("-im", "--image", help="container image, optional") + add_parser.add_argument("-e", "--emane", help="emane model, only required for emane nodes") add_parser.set_defaults(func=add_node) edit_parser = subparsers.add_parser("edit", help="edit a node") edit_parser.formatter_class = ArgumentDefaultsHelpFormatter - edit_parser.add_argument("--id", type=int, help="id to use, optional") + edit_parser.add_argument("-i", "--id", type=int, help="id to use, optional") group = edit_parser.add_mutually_exclusive_group(required=True) - group.add_argument("--pos", type=position_type, help="x,y position") - group.add_argument("--geo", type=geo_type, help="lon,lat,alt position") - edit_parser.add_argument("--icon", help="icon to use, optional") + group.add_argument("-p", "--pos", type=position_type, help="x,y position") + group.add_argument("-g", "--geo", type=geo_type, help="lon,lat,alt position") + edit_parser.add_argument("-ic", "--icon", help="icon to use, optional") edit_parser.set_defaults(func=edit_node) delete_parser = subparsers.add_parser("delete", help="delete a node") delete_parser.formatter_class = ArgumentDefaultsHelpFormatter - delete_parser.add_argument( - "--id", type=int, help="node id to delete", required=True - ) + delete_parser.add_argument("-i", "--id", type=int, help="node id", required=True) delete_parser.set_defaults(func=delete_node) def setup_link_parser(parent: _SubParsersAction) -> None: parser = parent.add_parser("link", help="link interactions") - parser.add_argument("--session", type=int, help="session to interact with") + parser.add_argument("-s", "--session", type=int, help="session to interact with") subparsers = parser.add_subparsers(help="link commands") subparsers.required = True subparsers.dest = "command" add_parser = subparsers.add_parser("add", help="add a node") add_parser.formatter_class = ArgumentDefaultsHelpFormatter - add_parser.add_argument( - "--node1", type=int, help="node1 id for link", required=True - ) - add_parser.add_argument( - "--node2", type=int, help="node1 id for link", required=True - ) - add_parser.add_argument("--iface1-id", type=int, help="node1 interface id for link") - add_parser.add_argument("--iface1-mac", type=mac_type, help="node1 interface mac") - add_parser.add_argument("--iface1-ip4", type=ip4_type, help="node1 interface ip4") - add_parser.add_argument("--iface1-ip6", type=ip6_type, help="node1 interface ip6") - add_parser.add_argument("--iface2-id", type=int, help="node1 interface id for link") - add_parser.add_argument("--iface2-mac", type=mac_type, help="node1 interface mac") - add_parser.add_argument("--iface2-ip4", type=ip4_type, help="node1 interface ip4") - add_parser.add_argument("--iface2-ip6", type=ip6_type, help="node1 interface ip6") - add_parser.add_argument("--bandwidth", type=int, help="bandwidth (bps) for link") - add_parser.add_argument("--loss", type=float, help="loss (%) for link") - add_parser.add_argument("--jitter", type=int, help="jitter (us) for link") - add_parser.add_argument("--delay", type=int, help="delay (us) for link") - add_parser.add_argument("--dup", type=int, help="duplicate (%) for link") - add_parser.add_argument( - "--uni", action="store_true", help="is link unidirectional?" - ) + add_parser.add_argument("-n1", "--node1", type=int, help="node1 id", required=True) + add_parser.add_argument("-n2", "--node2", type=int, help="node2 id", required=True) + add_parser.add_argument("-i1-i", "--iface1-id", type=int, help="node1 interface id") + add_parser.add_argument("-i1-m", "--iface1-mac", type=mac_type, help="node1 interface mac") + add_parser.add_argument("-i1-4", "--iface1-ip4", type=ip4_type, help="node1 interface ip4") + add_parser.add_argument("-i1-6", "--iface1-ip6", type=ip6_type, help="node1 interface ip6") + add_parser.add_argument("-i2-i", "--iface2-id", type=int, help="node2 interface id") + add_parser.add_argument("-i2-m", "--iface2-mac", type=mac_type, help="node2 interface mac") + add_parser.add_argument("-i2-4", "--iface2-ip4", type=ip4_type, help="node2 interface ip4") + add_parser.add_argument("-i2-6", "--iface2-ip6", type=ip6_type, help="node2 interface ip6") + add_parser.add_argument("-b", "--bandwidth", type=int, help="bandwidth (bps)") + add_parser.add_argument("-l", "--loss", type=float, help="loss (%%)") + add_parser.add_argument("-j", "--jitter", type=int, help="jitter (us)") + add_parser.add_argument("-de", "--delay", type=int, help="delay (us)") + add_parser.add_argument("-du", "--duplicate", type=int, help="duplicate (%%)") + add_parser.add_argument("-u", "--uni", action="store_true", help="is link unidirectional?") add_parser.set_defaults(func=add_link) edit_parser = subparsers.add_parser("edit", help="edit a link") edit_parser.formatter_class = ArgumentDefaultsHelpFormatter + edit_parser.add_argument("-n1", "--node1", type=int, help="node1 id", required=True) + edit_parser.add_argument("-n2", "--node2", type=int, help="node2 id", required=True) + edit_parser.add_argument("-i1", "--iface1", type=int, help="node1 interface id") + edit_parser.add_argument("-i2", "--iface2", type=int, help="node2 interface id") + edit_parser.add_argument("-b", "--bandwidth", type=int, help="bandwidth (bps)") + edit_parser.add_argument("-l", "--loss", type=float, help="loss (%%)") + edit_parser.add_argument("-j", "--jitter", type=int, help="jitter (us)") + edit_parser.add_argument("-de", "--delay", type=int, help="delay (us)") + edit_parser.add_argument("-du", "--duplicate", type=int, help="duplicate (%%)") edit_parser.add_argument( - "--node1", type=int, help="node1 id for link", required=True - ) - edit_parser.add_argument( - "--node2", type=int, help="node1 id for link", required=True - ) - edit_parser.add_argument("--iface1", type=int, help="node1 interface id for link") - edit_parser.add_argument("--iface2", type=int, help="node2 interface id for link") - edit_parser.add_argument("--bandwidth", type=int, help="bandwidth (bps) for link") - edit_parser.add_argument("--loss", type=float, help="loss (%) for link") - edit_parser.add_argument("--jitter", type=int, help="jitter (us) for link") - edit_parser.add_argument("--delay", type=int, help="delay (us) for link") - edit_parser.add_argument("--dup", type=int, help="duplicate (%) for link") - edit_parser.add_argument( - "--uni", action="store_true", help="is link unidirectional?" + "-u", "--uni", action="store_true", help="is link unidirectional?" ) edit_parser.set_defaults(func=edit_link) delete_parser = subparsers.add_parser("delete", help="delete a link") delete_parser.formatter_class = ArgumentDefaultsHelpFormatter - delete_parser.add_argument( - "--node1", type=int, help="node1 id for link", required=True - ) - delete_parser.add_argument( - "--node2", type=int, help="node1 id for link", required=True - ) - delete_parser.add_argument("--iface1", type=int, help="node1 interface id for link") - delete_parser.add_argument("--iface2", type=int, help="node2 interface id for link") + delete_parser.add_argument("-n1", "--node1", type=int, help="node1 id", required=True) + delete_parser.add_argument("-n2", "--node2", type=int, help="node1 id", required=True) + delete_parser.add_argument("-i1", "--iface1", type=int, help="node1 interface id") + delete_parser.add_argument("-i2", "--iface2", type=int, help="node2 interface id") delete_parser.set_defaults(func=delete_link) @@ -419,19 +401,19 @@ def setup_query_parser(parent: _SubParsersAction) -> None: sessions_parser.set_defaults(func=query_sessions) session_parser = subparsers.add_parser("session", help="query session") - session_parser.add_argument("--id", type=int, help="session to query", required=True) + session_parser.add_argument("-i", "--id", type=int, help="session to query", required=True) session_parser.set_defaults(func=query_session) node_parser = subparsers.add_parser("node", help="query node") - node_parser.add_argument("--id", type=int, help="session to query", required=True) - node_parser.add_argument("--node", type=int, help="node to query", required=True) + node_parser.add_argument("-i", "--id", type=int, help="session to query", required=True) + node_parser.add_argument("-n", "--node", type=int, help="node to query", required=True) node_parser.set_defaults(func=query_node) def main() -> None: parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) parser.add_argument( - "-j", "--json", action="store_true", help="print responses to terminal as json" + "-js", "--json", action="store_true", help="print responses to terminal as json" ) subparsers = parser.add_subparsers(help="supported commands") subparsers.required = True From 69721dc1290a53340df120e4bb27af3993911f5b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 30 Jun 2020 09:32:56 -0700 Subject: [PATCH 0420/1131] grpc: updated client edit_node to have source as last parameter to be consistent with source placement on all other functions --- daemon/core/api/grpc/client.py | 4 ++-- daemon/scripts/core-cli | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 82164fe3..20e193eb 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -502,8 +502,8 @@ class CoreGrpcClient: node_id: int, position: core_pb2.Position = None, icon: str = None, - source: str = None, geo: core_pb2.Geo = None, + source: str = None, ) -> core_pb2.EditNodeResponse: """ Edit a node, currently only changes position. @@ -512,8 +512,8 @@ class CoreGrpcClient: :param node_id: node id :param position: position to set node to :param icon: path to icon for gui to use for node - :param source: application source :param geo: lon,lat,alt location for node + :param source: application source :return: response with result of success or failure :raises grpc.RpcError: when session or node doesn't exist """ diff --git a/daemon/scripts/core-cli b/daemon/scripts/core-cli index 65a92994..df4535ac 100755 --- a/daemon/scripts/core-cli +++ b/daemon/scripts/core-cli @@ -221,7 +221,7 @@ def edit_node(args: Namespace) -> None: geo = Geo(lon=lon, lat=lat, alt=alt) core = CoreGrpcClient() with core.context_connect(): - response = core.edit_node(session_id, args.id, pos, args.icon, geo=geo) + response = core.edit_node(session_id, args.id, pos, args.icon, geo) if args.json: json = MessageToJson(response, preserving_proto_field_name=True) print(json) From d480a1dd4c92458bf3236fa1dd03dedfeb088146 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 30 Jun 2020 09:38:22 -0700 Subject: [PATCH 0421/1131] grpc: removed LinkOptions opaque as it was not being used --- daemon/proto/core/api/grpc/core.proto | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 8112c9d1..3828d474 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -719,17 +719,16 @@ message Link { } message LinkOptions { - string opaque = 1; - int64 jitter = 2; - int32 key = 3; - int32 mburst = 4; - int32 mer = 5; - float loss = 6; - int64 bandwidth = 7; - int32 burst = 8; - int64 delay = 9; - int32 dup = 10; - bool unidirectional = 11; + int64 jitter = 1; + int32 key = 2; + int32 mburst = 3; + int32 mer = 4; + float loss = 5; + int64 bandwidth = 6; + int32 burst = 7; + int64 delay = 8; + int32 dup = 9; + bool unidirectional = 10; } message Interface { From ab17cb1053facc612ed22b2547d0f77686cae304 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 30 Jun 2020 09:50:28 -0700 Subject: [PATCH 0422/1131] grpc: grpc get_session will no longer return peer to peer nodes, they should be invisible to users, updated core-cli to print human readable links better --- daemon/core/api/grpc/server.py | 4 ++-- daemon/core/gui/nodeutils.py | 2 +- daemon/scripts/core-cli | 8 ++++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index e8469177..4f741b22 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -114,7 +114,7 @@ from core.emulator.session import NT, Session from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.nodes.base import CoreNode, CoreNodeBase, NodeBase -from core.nodes.network import WlanNode +from core.nodes.network import PtpNet, WlanNode from core.services.coreservices import ServiceManager _ONE_DAY_IN_SECONDS: int = 60 * 60 * 24 @@ -543,7 +543,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): nodes = [] for _id in session.nodes: node = session.nodes[_id] - if not isinstance(node.id, int): + if isinstance(node, PtpNet): continue node_proto = grpcutils.get_node_proto(session, node) nodes.append(node_proto) diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index dbb403df..08c8f31c 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -62,7 +62,7 @@ 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} - IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET, NodeType.PEER_TO_PEER} + IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET} NODE_MODELS: Set[str] = {"router", "host", "PC", "mdr", "prouter"} ROUTER_NODES: Set[str] = {"router", "mdr"} ANTENNA_ICON: PhotoImage = None diff --git a/daemon/scripts/core-cli b/daemon/scripts/core-cli index df4535ac..05e495e5 100755 --- a/daemon/scripts/core-cli +++ b/daemon/scripts/core-cli @@ -150,12 +150,16 @@ def query_session(args: Namespace) -> None: n2 = names[link.node2_id] print(f"Node | ", end="") print_iface_header() + print(f"{n1:<6} | ", end="") if link.HasField("iface1"): - print(f"{n1:<6} | ", end="") print_iface(link.iface1) + else: + print() + print(f"{n2:<6} | ", end="") if link.HasField("iface2"): - print(f"{n2:<6} | ", end="") print_iface(link.iface2) + else: + print() print() From beaebcfa2496703308bfc7d7fa732155e5d11020 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 30 Jun 2020 12:34:20 -0700 Subject: [PATCH 0423/1131] grpc: added node_id and net2_id data to interface protos to allow querying a node to provide the node and networks an interface is associated with --- daemon/core/api/grpc/grpcutils.py | 35 +++++++++++++++------------ daemon/core/api/grpc/server.py | 6 ++--- daemon/proto/core/api/grpc/core.proto | 2 ++ daemon/scripts/core-cli | 22 ++++++++++++++--- 4 files changed, 42 insertions(+), 23 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index ed40a75b..bd9e808d 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -453,32 +453,35 @@ def iface_to_data(iface: CoreInterface) -> InterfaceData: ) -def iface_to_proto(iface: CoreInterface) -> core_pb2.Interface: +def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface: """ Convenience for converting a core interface to the protobuf representation. + + :param node_id: id of node to convert interface for :param iface: interface to convert :return: interface proto """ - net_id = None - if iface.net: - net_id = iface.net.id - ip4 = None - ip4_mask = None + if iface.node and iface.node.id == node_id: + _id = iface.node_id + else: + _id = iface.net_id + net_id = iface.net.id if iface.net else None + node_id = iface.node.id if iface.node else None + net2_id = iface.othernet.id if iface.othernet else None ip4_net = iface.get_ip4() - if ip4_net: - ip4 = str(ip4_net.ip) - ip4_mask = ip4_net.prefixlen - ip6 = None - ip6_mask = None + ip4 = str(ip4_net.ip) if ip4_net else None + ip4_mask = ip4_net.prefixlen if ip4_net else None ip6_net = iface.get_ip6() - if ip6_net: - ip6 = str(ip6_net.ip) - ip6_mask = ip6_net.prefixlen + ip6 = str(ip6_net.ip) if ip6_net else None + ip6_mask = ip6_net.prefixlen if ip6_net else None + mac = str(iface.mac) if iface.mac else None return core_pb2.Interface( - id=iface.node_id, + id=_id, net_id=net_id, + net2_id=net2_id, + node_id=node_id, name=iface.name, - mac=str(iface.mac), + mac=mac, mtu=iface.mtu, flow_id=iface.flow_id, ip4=ip4, diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 4f741b22..c447ee7c 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -688,7 +688,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): ifaces = [] for iface_id in node.ifaces: iface = node.ifaces[iface_id] - iface_proto = grpcutils.iface_to_proto(iface) + iface_proto = grpcutils.iface_to_proto(request.node_id, iface) ifaces.append(iface_proto) node_proto = grpcutils.get_node_proto(session, node) return core_pb2.GetNodeResponse(node=node_proto, ifaces=ifaces) @@ -880,9 +880,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): iface1_proto = None iface2_proto = None if node1_iface: - iface1_proto = grpcutils.iface_to_proto(node1_iface) + iface1_proto = grpcutils.iface_to_proto(node1_id, node1_iface) if node2_iface: - iface2_proto = grpcutils.iface_to_proto(node2_iface) + iface2_proto = grpcutils.iface_to_proto(node2_id, node2_iface) return core_pb2.AddLinkResponse( result=True, iface1=iface1_proto, iface2=iface2_proto ) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 3828d474..f01fca50 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -742,6 +742,8 @@ message Interface { int32 net_id = 8; int32 flow_id = 9; int32 mtu = 10; + int32 node_id = 11; + int32 net2_id = 12; } message SessionLocation { diff --git a/daemon/scripts/core-cli b/daemon/scripts/core-cli index 05e495e5..c4d97a8a 100755 --- a/daemon/scripts/core-cli +++ b/daemon/scripts/core-cli @@ -109,9 +109,9 @@ def print_iface_header() -> None: def print_iface(iface: Interface) -> None: - iface_ip4 = f"{iface.ip4}/{iface.ip4_mask}" if iface.ip4 else None - iface_ip6 = f"{iface.ip6}/{iface.ip6_mask}" if iface.ip6 else None - print(f"{iface.id:<3} | {iface.mac:<11} | {iface_ip4:<18} | {iface_ip6}") + iface_ip4 = f"{iface.ip4}/{iface.ip4_mask}" if iface.ip4 else "" + iface_ip6 = f"{iface.ip6}/{iface.ip6_mask}" if iface.ip6 else "" + print(f"{iface.id:<3} | {iface.mac:<17} | {iface_ip4:<18} | {iface_ip6}") def query_sessions(args: Namespace) -> None: @@ -166,6 +166,11 @@ def query_session(args: Namespace) -> None: def query_node(args: Namespace) -> None: core = CoreGrpcClient() with core.context_connect(): + names = {} + response = core.get_session(args.id) + for node in response.session.nodes: + names[node.id] = node.name + response = core.get_node(args.id, args.node) if args.json: json = MessageToJson(response, preserving_proto_field_name=True) @@ -176,8 +181,17 @@ def query_node(args: Namespace) -> None: print("ID | Name | Type") print(f"{node.id:<4} | {node.name:<7} | {node_type}") print("Interfaces") + print("Connected To | ", end="") print_iface_header() for iface in response.ifaces: + if iface.net_id == node.id: + if iface.node_id: + name = names[iface.node_id] + else: + name = names[iface.net2_id] + else: + name = names[iface.net_id] + print(f"{name:<12} | ", end="") print_iface(iface) @@ -268,7 +282,7 @@ def add_link(args: Namespace) -> None: json = MessageToJson(response, preserving_proto_field_name=True) print(json) else: - print(f"edit link: {response.result}") + print(f"add link: {response.result}") def edit_link(args: Namespace) -> None: From 4a0fdf3307347065cb41052581efa673af08b106 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 30 Jun 2020 15:21:33 -0700 Subject: [PATCH 0424/1131] core-cli: add function for printing protobuf responses as json --- daemon/scripts/core-cli | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/daemon/scripts/core-cli b/daemon/scripts/core-cli index c4d97a8a..6fe83cf0 100755 --- a/daemon/scripts/core-cli +++ b/daemon/scripts/core-cli @@ -7,7 +7,7 @@ from argparse import ( Namespace, _SubParsersAction, ) -from typing import Tuple +from typing import Any, Tuple import netaddr from google.protobuf.json_format import MessageToJson @@ -114,13 +114,17 @@ def print_iface(iface: Interface) -> None: print(f"{iface.id:<3} | {iface.mac:<17} | {iface_ip4:<18} | {iface_ip6}") +def print_json(message: Any) -> None: + json = MessageToJson(message, preserving_proto_field_name=True) + print(json) + + def query_sessions(args: Namespace) -> None: core = CoreGrpcClient() with core.context_connect(): response = core.get_sessions() if args.json: - json = MessageToJson(response, preserving_proto_field_name=True) - print(json) + print_json(response) else: print("Session ID | Session State | Nodes") for s in response.sessions: @@ -133,8 +137,7 @@ def query_session(args: Namespace) -> None: with core.context_connect(): response = core.get_session(args.id) if args.json: - json = MessageToJson(response, preserving_proto_field_name=True) - print(json) + print_json(response) else: print("Nodes") print("Node ID | Node Name | Node Type") @@ -173,8 +176,7 @@ def query_node(args: Namespace) -> None: response = core.get_node(args.id, args.node) if args.json: - json = MessageToJson(response, preserving_proto_field_name=True) - print(json) + print_json(response) else: node = response.node node_type = NodeType.Enum.Name(node.type) @@ -221,8 +223,7 @@ def add_node(args: Namespace) -> None: ) response = core.add_node(session_id, node) if args.json: - json = MessageToJson(response, preserving_proto_field_name=True) - print(json) + print_json(response) else: print(f"created node: {response.node_id}") @@ -241,8 +242,7 @@ def edit_node(args: Namespace) -> None: with core.context_connect(): response = core.edit_node(session_id, args.id, pos, args.icon, geo) if args.json: - json = MessageToJson(response, preserving_proto_field_name=True) - print(json) + print_json(response) else: print(f"edit node: {response.result}") @@ -253,8 +253,7 @@ def delete_node(args: Namespace) -> None: with core.context_connect(): response = core.delete_node(session_id, args.id) if args.json: - json = MessageToJson(response, preserving_proto_field_name=True) - print(json) + print_json(response) else: print(f"deleted node: {response.result}") @@ -279,8 +278,7 @@ def add_link(args: Namespace) -> None: with core.context_connect(): response = core.add_link(session_id, args.node1, args.node2, iface1, iface2, options) if args.json: - json = MessageToJson(response, preserving_proto_field_name=True) - print(json) + print_json(response) else: print(f"add link: {response.result}") @@ -301,8 +299,7 @@ def edit_link(args: Namespace) -> None: session_id, args.node1, args.node2, options, args.iface1, args.iface2 ) if args.json: - json = MessageToJson(response, preserving_proto_field_name=True) - print(json) + print_json(response) else: print(f"edit link: {response.result}") @@ -313,8 +310,7 @@ def delete_link(args: Namespace) -> None: with core.context_connect(): response = core.delete_link(session_id, args.node1, args.node2, args.iface1, args.iface2) if args.json: - json = MessageToJson(response, preserving_proto_field_name=True) - print(json) + print_json(response) else: print(f"delete link: {response.result}") From f22edd1d25b9d003385751e491c693989bdc956e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 30 Jun 2020 16:16:58 -0700 Subject: [PATCH 0425/1131] grpc: fixed accidental breakage for get_session ptp links --- daemon/core/api/grpc/server.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index c447ee7c..50b15771 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -543,10 +543,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): nodes = [] for _id in session.nodes: node = session.nodes[_id] - if isinstance(node, PtpNet): - continue - node_proto = grpcutils.get_node_proto(session, node) - nodes.append(node_proto) + if not isinstance(node, PtpNet): + node_proto = grpcutils.get_node_proto(session, node) + nodes.append(node_proto) node_links = get_links(node) links.extend(node_links) From 537291b219e731454423fdd2c63775d8ce7c01ba Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 30 Jun 2020 22:16:00 -0700 Subject: [PATCH 0426/1131] core-cli: added open xml command to a session xml and optionally start it --- daemon/scripts/core-cli | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/daemon/scripts/core-cli b/daemon/scripts/core-cli index 6fe83cf0..e4c72996 100755 --- a/daemon/scripts/core-cli +++ b/daemon/scripts/core-cli @@ -7,6 +7,7 @@ from argparse import ( Namespace, _SubParsersAction, ) +from pathlib import Path from typing import Any, Tuple import netaddr @@ -79,6 +80,13 @@ def geo_type(value: str) -> Tuple[float, float, float]: return lon, lat, alt +def file_type(value: str) -> str: + path = Path(value) + if not path.is_file(): + raise ArgumentTypeError(f"invalid file: {value}") + return str(path.absolute()) + + def get_current_session() -> int: core = CoreGrpcClient() with core.context_connect(): @@ -119,6 +127,16 @@ def print_json(message: Any) -> None: print(json) +def open_xml(args: Namespace) -> None: + core = CoreGrpcClient() + with core.context_connect(): + response = core.open_xml(args.file, args.start) + if args.json: + print_json(response) + else: + print(f"opened xml: {response.result}") + + def query_sessions(args: Namespace) -> None: core = CoreGrpcClient() with core.context_connect(): @@ -317,6 +335,7 @@ def delete_link(args: Namespace) -> None: def setup_node_parser(parent: _SubParsersAction) -> None: parser = parent.add_parser("node", help="node interactions") + parser.formatter_class = ArgumentDefaultsHelpFormatter parser.add_argument("-s", "--session", type=int, help="session to interact with") subparsers = parser.add_subparsers(help="node commands") subparsers.required = True @@ -355,6 +374,7 @@ def setup_node_parser(parent: _SubParsersAction) -> None: def setup_link_parser(parent: _SubParsersAction) -> None: parser = parent.add_parser("link", help="link interactions") + parser.formatter_class = ArgumentDefaultsHelpFormatter parser.add_argument("-s", "--session", type=int, help="session to interact with") subparsers = parser.add_subparsers(help="link commands") subparsers.required = True @@ -412,18 +432,29 @@ def setup_query_parser(parent: _SubParsersAction) -> None: subparsers.dest = "command" sessions_parser = subparsers.add_parser("sessions", help="query current sessions") + sessions_parser.formatter_class = ArgumentDefaultsHelpFormatter sessions_parser.set_defaults(func=query_sessions) session_parser = subparsers.add_parser("session", help="query session") + session_parser.formatter_class = ArgumentDefaultsHelpFormatter session_parser.add_argument("-i", "--id", type=int, help="session to query", required=True) session_parser.set_defaults(func=query_session) node_parser = subparsers.add_parser("node", help="query node") + node_parser.formatter_class = ArgumentDefaultsHelpFormatter node_parser.add_argument("-i", "--id", type=int, help="session to query", required=True) node_parser.add_argument("-n", "--node", type=int, help="node to query", required=True) node_parser.set_defaults(func=query_node) +def setup_xml_parser(parent: _SubParsersAction) -> None: + parser = parent.add_parser("xml", help="open session xml") + parser.formatter_class = ArgumentDefaultsHelpFormatter + parser.add_argument("file", type=file_type, help="xml file to open") + parser.add_argument("-s", "--start", action="store_true", help="start the session?") + parser.set_defaults(func=open_xml) + + def main() -> None: parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) parser.add_argument( @@ -435,6 +466,7 @@ def main() -> None: setup_node_parser(subparsers) setup_link_parser(subparsers) setup_query_parser(subparsers) + setup_xml_parser(subparsers) args = parser.parse_args() args.func(args) From 3477e84e9d029bed25fe4d6398e2a515cccb79bb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 1 Jul 2020 09:30:05 -0700 Subject: [PATCH 0427/1131] core-cli: added wlan set/get config, fixed session option for node/link interactions --- daemon/core/api/grpc/server.py | 11 ++--- daemon/scripts/core-cli | 88 ++++++++++++++++++++++++++++++---- 2 files changed, 85 insertions(+), 14 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 50b15771..aa5ec539 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -1343,13 +1343,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("set wlan config: %s", request) session = self.get_session(request.session_id, context) - wlan_config = request.wlan_config - session.mobility.set_model_config( - wlan_config.node_id, BasicRangeModel.name, wlan_config.config - ) + node_id = request.wlan_config.node_id + config = request.wlan_config.config + session.mobility.set_model_config(node_id, BasicRangeModel.name, config) if session.state == EventTypes.RUNTIME_STATE: - node = self.get_node(session, wlan_config.node_id, context, WlanNode) - node.updatemodel(wlan_config.config) + node = self.get_node(session, node_id, context, WlanNode) + node.updatemodel(config) return SetWlanConfigResponse(result=True) def GetEmaneConfig( diff --git a/daemon/scripts/core-cli b/daemon/scripts/core-cli index e4c72996..9dfadfcc 100755 --- a/daemon/scripts/core-cli +++ b/daemon/scripts/core-cli @@ -8,8 +8,9 @@ from argparse import ( _SubParsersAction, ) from pathlib import Path -from typing import Any, Tuple +from typing import Any, Optional, Tuple +import grpc import netaddr from google.protobuf.json_format import MessageToJson from netaddr import EUI, AddrFormatError, IPNetwork @@ -87,7 +88,9 @@ def file_type(value: str) -> str: return str(path.absolute()) -def get_current_session() -> int: +def get_current_session(session_id: Optional[int]) -> int: + if session_id: + return session_id core = CoreGrpcClient() with core.context_connect(): response = core.get_sessions() @@ -127,6 +130,50 @@ def print_json(message: Any) -> None: print(json) +def get_wlan_config(args: Namespace) -> None: + session_id = get_current_session(args.session) + core = CoreGrpcClient() + try: + with core.context_connect(): + response = core.get_wlan_config(session_id, args.node) + if args.json: + print_json(response) + else: + size = 0 + for option in response.config.values(): + size = max(size, len(option.name)) + print(f"{'Name':<{size}.{size}} | Value") + for option in response.config.values(): + print(f"{option.name:<{size}.{size}} | {option.value}") + except grpc.RpcError as e: + print(f"grpc error: {e.details()}") + + +def set_wlan_config(args: Namespace) -> None: + session_id = get_current_session(args.session) + config = {} + if args.bandwidth: + config["bandwidth"] = str(args.bandwidth) + if args.delay: + config["delay"] = str(args.delay) + if args.loss: + config["error"] = str(args.loss) + if args.jitter: + config["jitter"] = str(args.jitter) + if args.range: + config["range"] = str(args.range) + core = CoreGrpcClient() + try: + with core.context_connect(): + response = core.set_wlan_config(session_id, args.node, config) + if args.json: + print_json(response) + else: + print(f"set wlan config: {response.result}") + except grpc.RpcError as e: + print(f"grpc error: {e.details()}") + + def open_xml(args: Namespace) -> None: core = CoreGrpcClient() with core.context_connect(): @@ -216,7 +263,7 @@ def query_node(args: Namespace) -> None: def add_node(args: Namespace) -> None: - session_id = get_current_session() + session_id = get_current_session(args.session) node_type = NodeType.Enum.Value(args.type) pos = None if args.pos: @@ -247,7 +294,7 @@ def add_node(args: Namespace) -> None: def edit_node(args: Namespace) -> None: - session_id = get_current_session() + session_id = get_current_session(args.session) pos = None if args.pos: x, y = args.pos @@ -266,7 +313,7 @@ def edit_node(args: Namespace) -> None: def delete_node(args: Namespace) -> None: - session_id = get_current_session() + session_id = get_current_session(args.session) core = CoreGrpcClient() with core.context_connect(): response = core.delete_node(session_id, args.id) @@ -277,7 +324,7 @@ def delete_node(args: Namespace) -> None: def add_link(args: Namespace) -> None: - session_id = get_current_session() + session_id = get_current_session(args.session) iface1 = None if args.iface1_id is not None: iface1 = create_iface(args.iface1_id, args.iface1_mac, args.iface1_ip4, args.iface1_ip6) @@ -302,7 +349,7 @@ def add_link(args: Namespace) -> None: def edit_link(args: Namespace) -> None: - session_id = get_current_session() + session_id = get_current_session(args.session) options = LinkOptions( bandwidth=args.bandwidth, loss=args.loss, @@ -323,7 +370,7 @@ def edit_link(args: Namespace) -> None: def delete_link(args: Namespace) -> None: - session_id = get_current_session() + session_id = get_current_session(args.session) core = CoreGrpcClient() with core.context_connect(): response = core.delete_link(session_id, args.node1, args.node2, args.iface1, args.iface2) @@ -455,6 +502,30 @@ def setup_xml_parser(parent: _SubParsersAction) -> None: parser.set_defaults(func=open_xml) +def setup_wlan_parser(parent: _SubParsersAction) -> None: + parser = parent.add_parser("wlan", help="wlan specific interactions") + parser.formatter_class = ArgumentDefaultsHelpFormatter + parser.add_argument("-s", "--session", type=int, help="session to interact with") + subparsers = parser.add_subparsers(help="link commands") + subparsers.required = True + subparsers.dest = "command" + + get_parser = subparsers.add_parser("get", help="get wlan configuration") + get_parser.formatter_class = ArgumentDefaultsHelpFormatter + get_parser.add_argument("-n", "--node", type=int, help="wlan node", required=True) + get_parser.set_defaults(func=get_wlan_config) + + set_parser = subparsers.add_parser("set", help="set wlan configuration") + set_parser.formatter_class = ArgumentDefaultsHelpFormatter + set_parser.add_argument("-n", "--node", type=int, help="wlan node", required=True) + set_parser.add_argument("-b", "--bandwidth", type=int, help="bandwidth (bps)") + set_parser.add_argument("-d", "--delay", type=int, help="delay (us)") + set_parser.add_argument("-l", "--loss", type=float, help="loss (%%)") + set_parser.add_argument("-j", "--jitter", type=int, help="jitter (us)") + set_parser.add_argument("-r", "--range", type=int, help="range (pixels)") + set_parser.set_defaults(func=set_wlan_config) + + def main() -> None: parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) parser.add_argument( @@ -467,6 +538,7 @@ def main() -> None: setup_link_parser(subparsers) setup_query_parser(subparsers) setup_xml_parser(subparsers) + setup_wlan_parser(subparsers) args = parser.parse_args() args.func(args) From 7a6c602369ebb44777df7502e63dac348b693b45 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 1 Jul 2020 11:01:44 -0700 Subject: [PATCH 0428/1131] core-cli: cleaned up core client usage by way of a decorator, helps provide convenient grpc error catching --- daemon/scripts/core-cli | 346 ++++++++++++++++++++-------------------- 1 file changed, 170 insertions(+), 176 deletions(-) diff --git a/daemon/scripts/core-cli b/daemon/scripts/core-cli index 9dfadfcc..61b47ae4 100755 --- a/daemon/scripts/core-cli +++ b/daemon/scripts/core-cli @@ -7,6 +7,7 @@ from argparse import ( Namespace, _SubParsersAction, ) +from functools import wraps from pathlib import Path from typing import Any, Optional, Tuple @@ -29,6 +30,19 @@ from core.api.grpc.core_pb2 import ( NODE_TYPES = [k for k, v in NodeType.Enum.items() if v != NodeType.PEER_TO_PEER] +def coreclient(func): + @wraps(func) + def wrapper(*args, **kwargs): + core = CoreGrpcClient() + try: + with core.context_connect(): + return func(core, *args, **kwargs) + except grpc.RpcError as e: + print(f"grpc error: {e.details()}") + + return wrapper + + def mac_type(value: str) -> str: try: mac = EUI(value, dialect=netaddr.mac_unix_expanded) @@ -88,12 +102,10 @@ def file_type(value: str) -> str: return str(path.absolute()) -def get_current_session(session_id: Optional[int]) -> int: +def get_current_session(core: CoreGrpcClient, session_id: Optional[int]) -> int: if session_id: return session_id - core = CoreGrpcClient() - with core.context_connect(): - response = core.get_sessions() + response = core.get_sessions() if not response.sessions: print("no current session to interact with") sys.exit(1) @@ -130,27 +142,24 @@ def print_json(message: Any) -> None: print(json) -def get_wlan_config(args: Namespace) -> None: - session_id = get_current_session(args.session) - core = CoreGrpcClient() - try: - with core.context_connect(): - response = core.get_wlan_config(session_id, args.node) - if args.json: - print_json(response) - else: - size = 0 - for option in response.config.values(): - size = max(size, len(option.name)) - print(f"{'Name':<{size}.{size}} | Value") - for option in response.config.values(): - print(f"{option.name:<{size}.{size}} | {option.value}") - except grpc.RpcError as e: - print(f"grpc error: {e.details()}") +@coreclient +def get_wlan_config(core: CoreGrpcClient, args: Namespace) -> None: + session_id = get_current_session(core, args.session) + response = core.get_wlan_config(session_id, args.node) + if args.json: + print_json(response) + else: + size = 0 + for option in response.config.values(): + size = max(size, len(option.name)) + print(f"{'Name':<{size}.{size}} | Value") + for option in response.config.values(): + print(f"{option.name:<{size}.{size}} | {option.value}") -def set_wlan_config(args: Namespace) -> None: - session_id = get_current_session(args.session) +@coreclient +def set_wlan_config(core: CoreGrpcClient, args: Namespace) -> None: + session_id = get_current_session(core, args.session) config = {} if args.bandwidth: config["bandwidth"] = str(args.bandwidth) @@ -162,108 +171,100 @@ def set_wlan_config(args: Namespace) -> None: config["jitter"] = str(args.jitter) if args.range: config["range"] = str(args.range) - core = CoreGrpcClient() - try: - with core.context_connect(): - response = core.set_wlan_config(session_id, args.node, config) - if args.json: - print_json(response) - else: - print(f"set wlan config: {response.result}") - except grpc.RpcError as e: - print(f"grpc error: {e.details()}") + response = core.set_wlan_config(session_id, args.node, config) + if args.json: + print_json(response) + else: + print(f"set wlan config: {response.result}") -def open_xml(args: Namespace) -> None: - core = CoreGrpcClient() - with core.context_connect(): - response = core.open_xml(args.file, args.start) - if args.json: - print_json(response) - else: - print(f"opened xml: {response.result}") +@coreclient +def open_xml(core: CoreGrpcClient, args: Namespace) -> None: + response = core.open_xml(args.file, args.start) + if args.json: + print_json(response) + else: + print(f"opened xml: {response.result}") -def query_sessions(args: Namespace) -> None: - core = CoreGrpcClient() - with core.context_connect(): - response = core.get_sessions() - if args.json: - print_json(response) - else: - print("Session ID | Session State | Nodes") - for s in response.sessions: - state = SessionState.Enum.Name(s.state) - print(f"{s.id:<10} | {state:<13} | {s.nodes}") +@coreclient +def query_sessions(core: CoreGrpcClient, args: Namespace) -> None: + response = core.get_sessions() + if args.json: + print_json(response) + else: + print("Session ID | Session State | Nodes") + for s in response.sessions: + state = SessionState.Enum.Name(s.state) + print(f"{s.id:<10} | {state:<13} | {s.nodes}") -def query_session(args: Namespace) -> None: - core = CoreGrpcClient() - with core.context_connect(): - response = core.get_session(args.id) - if args.json: - print_json(response) - else: - print("Nodes") - print("Node ID | Node Name | Node Type") - names = {} - for node in response.session.nodes: - names[node.id] = node.name - node_type = NodeType.Enum.Name(node.type) - print(f"{node.id:<7} | {node.name:<9} | {node_type}") - - print("\nLinks") - for link in response.session.links: - n1 = names[link.node1_id] - n2 = names[link.node2_id] - print(f"Node | ", end="") - print_iface_header() - print(f"{n1:<6} | ", end="") - if link.HasField("iface1"): - print_iface(link.iface1) - else: - print() - print(f"{n2:<6} | ", end="") - if link.HasField("iface2"): - print_iface(link.iface2) - else: - print() - print() - - -def query_node(args: Namespace) -> None: - core = CoreGrpcClient() - with core.context_connect(): +@coreclient +def query_session(core: CoreGrpcClient, args: Namespace) -> None: + response = core.get_session(args.id) + if args.json: + print_json(response) + else: + print("Nodes") + print("Node ID | Node Name | Node Type") names = {} - response = core.get_session(args.id) for node in response.session.nodes: names[node.id] = node.name - - response = core.get_node(args.id, args.node) - if args.json: - print_json(response) - else: - node = response.node node_type = NodeType.Enum.Name(node.type) - print("ID | Name | Type") - print(f"{node.id:<4} | {node.name:<7} | {node_type}") - print("Interfaces") - print("Connected To | ", end="") + print(f"{node.id:<7} | {node.name:<9} | {node_type}") + + print("\nLinks") + for link in response.session.links: + n1 = names[link.node1_id] + n2 = names[link.node2_id] + print(f"Node | ", end="") print_iface_header() - for iface in response.ifaces: - if iface.net_id == node.id: - if iface.node_id: - name = names[iface.node_id] - else: - name = names[iface.net2_id] + print(f"{n1:<6} | ", end="") + if link.HasField("iface1"): + print_iface(link.iface1) + else: + print() + print(f"{n2:<6} | ", end="") + if link.HasField("iface2"): + print_iface(link.iface2) + else: + print() + print() + + +@coreclient +def query_node(core: CoreGrpcClient, args: Namespace) -> None: + names = {} + response = core.get_session(args.id) + for node in response.session.nodes: + names[node.id] = node.name + + response = core.get_node(args.id, args.node) + if args.json: + print_json(response) + else: + node = response.node + node_type = NodeType.Enum.Name(node.type) + print("ID | Name | Type") + print(f"{node.id:<4} | {node.name:<7} | {node_type}") + print("Interfaces") + print("Connected To | ", end="") + print_iface_header() + for iface in response.ifaces: + if iface.net_id == node.id: + if iface.node_id: + name = names[iface.node_id] else: - name = names[iface.net_id] - print(f"{name:<12} | ", end="") - print_iface(iface) + name = names[iface.net2_id] + else: + name = names[iface.net_id] + print(f"{name:<12} | ", end="") + print_iface(iface) -def add_node(args: Namespace) -> None: - session_id = get_current_session(args.session) +@coreclient +def add_node(core: CoreGrpcClient, args: Namespace) -> None: + session_id = get_current_session(core, args.session) node_type = NodeType.Enum.Value(args.type) pos = None if args.pos: @@ -273,28 +274,27 @@ def add_node(args: Namespace) -> None: if args.geo: lon, lat, alt = args.geo geo = Geo(lon=lon, lat=lat, alt=alt) - core = CoreGrpcClient() - with core.context_connect(): - node = Node( - id=args.id, - name=args.name, - type=node_type, - model=args.model, - emane=args.emane, - icon=args.icon, - image=args.image, - position=pos, - geo=geo, - ) - response = core.add_node(session_id, node) - if args.json: - print_json(response) - else: - print(f"created node: {response.node_id}") + node = Node( + id=args.id, + name=args.name, + type=node_type, + model=args.model, + emane=args.emane, + icon=args.icon, + image=args.image, + position=pos, + geo=geo, + ) + response = core.add_node(session_id, node) + if args.json: + print_json(response) + else: + print(f"created node: {response.node_id}") -def edit_node(args: Namespace) -> None: - session_id = get_current_session(args.session) +@coreclient +def edit_node(core: CoreGrpcClient, args: Namespace) -> None: + session_id = get_current_session(core, args.session) pos = None if args.pos: x, y = args.pos @@ -303,28 +303,26 @@ def edit_node(args: Namespace) -> None: if args.geo: lon, lat, alt = args.geo geo = Geo(lon=lon, lat=lat, alt=alt) - core = CoreGrpcClient() - with core.context_connect(): - response = core.edit_node(session_id, args.id, pos, args.icon, geo) - if args.json: - print_json(response) - else: - print(f"edit node: {response.result}") + response = core.edit_node(session_id, args.id, pos, args.icon, geo) + if args.json: + print_json(response) + else: + print(f"edit node: {response.result}") -def delete_node(args: Namespace) -> None: - session_id = get_current_session(args.session) - core = CoreGrpcClient() - with core.context_connect(): - response = core.delete_node(session_id, args.id) - if args.json: - print_json(response) - else: - print(f"deleted node: {response.result}") +@coreclient +def delete_node(core: CoreGrpcClient, args: Namespace) -> None: + session_id = get_current_session(core, args.session) + response = core.delete_node(session_id, args.id) + if args.json: + print_json(response) + else: + print(f"deleted node: {response.result}") -def add_link(args: Namespace) -> None: - session_id = get_current_session(args.session) +@coreclient +def add_link(core: CoreGrpcClient, args: Namespace) -> None: + session_id = get_current_session(core, args.session) iface1 = None if args.iface1_id is not None: iface1 = create_iface(args.iface1_id, args.iface1_mac, args.iface1_ip4, args.iface1_ip6) @@ -339,17 +337,16 @@ def add_link(args: Namespace) -> None: dup=args.duplicate, unidirectional=args.uni, ) - core = CoreGrpcClient() - with core.context_connect(): - response = core.add_link(session_id, args.node1, args.node2, iface1, iface2, options) - if args.json: - print_json(response) - else: - print(f"add link: {response.result}") + response = core.add_link(session_id, args.node1, args.node2, iface1, iface2, options) + if args.json: + print_json(response) + else: + print(f"add link: {response.result}") -def edit_link(args: Namespace) -> None: - session_id = get_current_session(args.session) +@coreclient +def edit_link(core: CoreGrpcClient, args: Namespace) -> None: + session_id = get_current_session(core, args.session) options = LinkOptions( bandwidth=args.bandwidth, loss=args.loss, @@ -358,26 +355,23 @@ def edit_link(args: Namespace) -> None: dup=args.duplicate, unidirectional=args.uni, ) - core = CoreGrpcClient() - with core.context_connect(): - response = core.edit_link( - session_id, args.node1, args.node2, options, args.iface1, args.iface2 - ) - if args.json: - print_json(response) - else: - print(f"edit link: {response.result}") + response = core.edit_link( + session_id, args.node1, args.node2, options, args.iface1, args.iface2 + ) + if args.json: + print_json(response) + else: + print(f"edit link: {response.result}") -def delete_link(args: Namespace) -> None: - session_id = get_current_session(args.session) - core = CoreGrpcClient() - with core.context_connect(): - response = core.delete_link(session_id, args.node1, args.node2, args.iface1, args.iface2) - if args.json: - print_json(response) - else: - print(f"delete link: {response.result}") +@coreclient +def delete_link(core: CoreGrpcClient, args: Namespace) -> None: + session_id = get_current_session(core, args.session) + response = core.delete_link(session_id, args.node1, args.node2, args.iface1, args.iface2) + if args.json: + print_json(response) + else: + print(f"delete link: {response.result}") def setup_node_parser(parent: _SubParsersAction) -> None: From 08bbaf463b576270b617e16fbaa58996d3f1311d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 1 Jul 2020 11:06:09 -0700 Subject: [PATCH 0429/1131] core-cli: updated xml command to use a flag argument to be consistent for now --- daemon/scripts/core-cli | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/scripts/core-cli b/daemon/scripts/core-cli index 61b47ae4..471facb3 100755 --- a/daemon/scripts/core-cli +++ b/daemon/scripts/core-cli @@ -491,7 +491,7 @@ def setup_query_parser(parent: _SubParsersAction) -> None: def setup_xml_parser(parent: _SubParsersAction) -> None: parser = parent.add_parser("xml", help="open session xml") parser.formatter_class = ArgumentDefaultsHelpFormatter - parser.add_argument("file", type=file_type, help="xml file to open") + parser.add_argument("-f", "--file", type=file_type, help="xml file to open", required=True) parser.add_argument("-s", "--start", action="store_true", help="start the session?") parser.set_defaults(func=open_xml) From a870c15b43a386a439ee6ef7815714d1a9bf4f07 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 1 Jul 2020 12:11:34 -0700 Subject: [PATCH 0430/1131] pygui: fixed joining sessions with mobility players --- daemon/core/gui/coreclient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index cf331676..7cf8b123 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -377,7 +377,7 @@ class CoreClient: # organize canvas self.app.canvas.organize() - + self.show_mobility_players() # update ui to represent current state self.app.after(0, self.app.joined_session_update) From da9c0d066083e580482822cbe2c42ad08d55c036 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 1 Jul 2020 14:40:19 -0700 Subject: [PATCH 0431/1131] daemon: initial changes to breakout custom interface creation for networks that require it, without being emane specific --- daemon/core/emane/nodes.py | 36 +++++++++++++++++++++++++++++++++--- daemon/core/nodes/base.py | 36 +++++++++++++++++++----------------- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 8cc9cd87..be95e6d0 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -6,9 +6,10 @@ share the same MAC+PHY model. import logging from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type -from core.emulator.data import LinkData, LinkOptions +from core.emulator.data import InterfaceData, LinkData, LinkOptions from core.emulator.distributed import DistributedServer from core.emulator.enumerations import ( + EventTypes, LinkTypes, MessageFlags, NodeTypes, @@ -16,7 +17,7 @@ from core.emulator.enumerations import ( TransportType, ) from core.errors import CoreError -from core.nodes.base import CoreNetworkBase +from core.nodes.base import CoreNetworkBase, CoreNode from core.nodes.interface import CoreInterface, TunTap if TYPE_CHECKING: @@ -47,7 +48,7 @@ class EmaneNet(CoreNetworkBase): apitype: NodeTypes = NodeTypes.EMANE linktype: LinkTypes = LinkTypes.WIRED type: str = "wlan" - is_emane: bool = True + has_custom_iface: bool = True def __init__( self, @@ -262,3 +263,32 @@ class EmaneNet(CoreNetworkBase): if link: links.append(link) return links + + def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface: + # TUN/TAP is not ready for addressing yet; the device may + # take some time to appear, and installing it into a + # namespace after it has been bound removes addressing; + # save addresses with the interface now + iface_id = node.newtuntap(iface_data.id, iface_data.name) + node.attachnet(iface_id, self) + iface = node.get_iface(iface_id) + iface.set_mac(iface_data.mac) + for ip in iface_data.get_ips(): + iface.add_ip(ip) + # TODO: if added during runtime start EMANE + if self.session.state == EventTypes.RUNTIME_STATE: + logging.info("startup emane for node: %s", node.name) + # create specific xml if needed + config = self.session.emane.get_iface_config( + self.model.id, iface, self.model.name + ) + if config: + self.model.build_xml_files(config, iface) + + # start emane daemon + + # install netif + + # add nem to nemfile + + return iface diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 05ec87dc..039008ef 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -812,30 +812,29 @@ class CoreNode(CoreNodeBase): :param iface_data: interface data for new interface :return: interface index """ - ips = iface_data.get_ips() with self.lock: - # TODO: emane specific code - if net.is_emane is True: - iface_id = self.newtuntap(iface_data.id, iface_data.name) - # TUN/TAP is not ready for addressing yet; the device may - # take some time to appear, and installing it into a - # namespace after it has been bound removes addressing; - # save addresses with the interface now - self.attachnet(iface_id, net) - iface = self.get_iface(iface_id) - iface.set_mac(iface_data.mac) - for ip in ips: - iface.add_ip(ip) + if net.has_custom_iface: + return net.custom_iface(self, iface_data) + # if net.is_emane is True: + # iface_id = self.newtuntap(iface_data.id, iface_data.name) + # # TUN/TAP is not ready for addressing yet; the device may + # # take some time to appear, and installing it into a + # # namespace after it has been bound removes addressing; + # # save addresses with the interface now + # self.attachnet(iface_id, net) + # iface = self.get_iface(iface_id) + # iface.set_mac(iface_data.mac) + # for ip in ips: + # iface.add_ip(ip) else: iface_id = self.newveth(iface_data.id, iface_data.name) self.attachnet(iface_id, net) if iface_data.mac: self.set_mac(iface_id, iface_data.mac) - for ip in ips: + for ip in iface_data.get_ips(): self.add_ip(iface_id, ip) self.ifup(iface_id) - iface = self.get_iface(iface_id) - return iface + return self.get_iface(iface_id) def addfile(self, srcname: str, filename: str) -> None: """ @@ -925,7 +924,7 @@ class CoreNetworkBase(NodeBase): """ linktype: LinkTypes = LinkTypes.WIRED - is_emane: bool = False + has_custom_iface: bool = False def __init__( self, @@ -990,6 +989,9 @@ class CoreNetworkBase(NodeBase): """ raise NotImplementedError + def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface: + raise NotImplementedError + def get_linked_iface(self, net: "CoreNetworkBase") -> Optional[CoreInterface]: """ Return the interface that links this net with another net. From e549830e3342effb9ad5aaeea9d2972fa355902b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 1 Jul 2020 15:20:53 -0700 Subject: [PATCH 0432/1131] core-cli: fix to avoid errors for querying nodes with peer to peer links, until there is a proper way to get the other ends node name --- daemon/scripts/core-cli | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/scripts/core-cli b/daemon/scripts/core-cli index 471facb3..a7571471 100755 --- a/daemon/scripts/core-cli +++ b/daemon/scripts/core-cli @@ -257,7 +257,7 @@ def query_node(core: CoreGrpcClient, args: Namespace) -> None: else: name = names[iface.net2_id] else: - name = names[iface.net_id] + name = names.get(iface.net_id, "") print(f"{name:<12} | ", end="") print_iface(iface) From bd48e14348c05800930618aaa02f1bf616fb393c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 2 Jul 2020 15:37:51 -0700 Subject: [PATCH 0433/1131] daemon: initial changes to rework logic to start emane for a given interface --- daemon/core/emane/commeffect.py | 19 +- daemon/core/emane/emanemanager.py | 279 +++++++++-------------- daemon/core/emane/emanemodel.py | 27 +-- daemon/core/emane/nodes.py | 66 +----- daemon/core/emulator/session.py | 4 +- daemon/core/xml/emanexml.py | 363 +++++++++++------------------- 6 files changed, 262 insertions(+), 496 deletions(-) diff --git a/daemon/core/emane/commeffect.py b/daemon/core/emane/commeffect.py index 610099f1..100af9a7 100644 --- a/daemon/core/emane/commeffect.py +++ b/daemon/core/emane/commeffect.py @@ -62,9 +62,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): def config_groups(cls) -> List[ConfigGroup]: return [ConfigGroup("CommEffect SHIM Parameters", 1, len(cls.configurations()))] - def build_xml_files( - self, config: Dict[str, str], iface: CoreInterface = None - ) -> None: + def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None: """ Build the necessary nem and commeffect XMLs in the given path. If an individual NEM has a nonstandard config, we need to build @@ -75,22 +73,25 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): :param iface: interface for the emane node :return: nothing """ + # interface node + node = iface.node + # retrieve xml names - nem_name = emanexml.nem_file_name(self, iface) - shim_name = emanexml.shim_file_name(self, iface) + nem_name = emanexml.nem_file_name(iface) + shim_name = emanexml.shim_file_name(iface) # create and write nem document nem_element = etree.Element("nem", name=f"{self.name} NEM", type="unstructured") transport_type = TransportType.VIRTUAL - if iface and iface.transport_type == TransportType.RAW: + if iface.transport_type == TransportType.RAW: transport_type = TransportType.RAW - transport_file = emanexml.transport_file_name(self.id, transport_type) + transport_file = emanexml.transport_file_name(iface, transport_type) etree.SubElement(nem_element, "transport", definition=transport_file) # set shim configuration etree.SubElement(nem_element, "shim", definition=shim_name) - nem_file = os.path.join(self.session.session_dir, nem_name) + nem_file = os.path.join(node.nodedir, nem_name) emanexml.create_file(nem_element, "nem", nem_file) # create and write shim document @@ -111,7 +112,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): if ff.strip() != "": emanexml.add_param(shim_element, "filterfile", ff) - shim_file = os.path.join(self.session.session_dir, shim_name) + shim_file = os.path.join(node.nodedir, shim_name) emanexml.create_file(shim_element, "shim", shim_file) def linkconfig( diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index fc561b5f..3317a5db 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -6,6 +6,7 @@ import logging import os import threading from collections import OrderedDict +from enum import Enum from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type from core import utils @@ -25,11 +26,11 @@ from core.emulator.enumerations import ( LinkTypes, MessageFlags, RegisterTlvs, + TransportType, ) from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNode, NodeBase -from core.nodes.interface import CoreInterface -from core.nodes.network import CtrlNet +from core.nodes.interface import CoreInterface, TunTap from core.nodes.physical import Rj45Node from core.xml import emanexml @@ -63,6 +64,12 @@ DEFAULT_EMANE_PREFIX = "/usr" DEFAULT_DEV = "ctrl0" +class EmaneState(Enum): + SUCCESS = 0 + NOT_NEEDED = 1 + NOT_READY = 2 + + class EmaneManager(ModelManager): """ EMANE controller object. Lives in a Session instance and is used for @@ -72,8 +79,6 @@ class EmaneManager(ModelManager): name: str = "emane" config_type: RegisterTlvs = RegisterTlvs.EMULATION_SERVER - SUCCESS: int = 0 - NOT_NEEDED: int = 1 NOT_READY: int = 2 EVENTCFGVAR: str = "LIBEMANEEVENTSERVICECONFIG" DEFAULT_LOG_LEVEL: int = 3 @@ -87,6 +92,7 @@ class EmaneManager(ModelManager): """ super().__init__() self.session: "Session" = session + self.nems: Dict[int, CoreInterface] = {} self._emane_nets: Dict[int, EmaneNet] = {} self._emane_node_lock: threading.Lock = threading.Lock() # port numbers are allocated from these counters @@ -111,46 +117,47 @@ class EmaneManager(ModelManager): self.event_device: Optional[str] = None self.emane_check() + def next_nem_id(self) -> int: + nem_id = int(self.get_config("nem_id_start")) + while nem_id in self.nems: + nem_id += 1 + return nem_id + def get_iface_config( - self, node_id: int, iface: CoreInterface, model_name: str + self, emane_net: EmaneNet, iface: CoreInterface ) -> Dict[str, str]: """ - Retrieve interface configuration or node configuration if not provided. + Retrieve configuration for a given interface. - :param node_id: node id - :param iface: node interface - :param model_name: model to get configuration for - :return: node/interface model configuration + :param emane_net: emane network the interface is connected to + :param iface: interface running emane + :return: net, node, or interface model configuration """ + model_name = emane_net.model.name # use the network-wide config values or interface(NEM)-specific values? if iface is None: - return self.get_configs(node_id=node_id, config_type=model_name) + return self.get_configs(node_id=emane_net.id, config_type=model_name) else: # don"t use default values when interface config is the same as net # note here that using iface.node.id as key allows for only one type # of each model per node; # TODO: use both node and interface as key - # Adamson change: first check for iface config keyed by "node:iface.name" # (so that nodes w/ multiple interfaces of same conftype can have # different configs for each separate interface) key = 1000 * iface.node.id if iface.node_id is not None: key += iface.node_id - # try retrieve interface specific configuration, avoid getting defaults config = self.get_configs(node_id=key, config_type=model_name) - # otherwise retrieve the interfaces node configuration, avoid using defaults if not config: config = self.get_configs(node_id=iface.node.id, config_type=model_name) - # get non interface config, when none found if not config: # with EMANE 0.9.2+, we need an extra NEM XML from # model.buildnemxmlfiles(), so defaults are returned here - config = self.get_configs(node_id=node_id, config_type=model_name) - + config = self.get_configs(node_id=emane_net.id, config_type=model_name) return config def config_reset(self, node_id: int = None) -> None: @@ -260,14 +267,13 @@ class EmaneManager(ModelManager): Return a set of CoreNodes that are linked to an EMANE network, e.g. containers having one or more radio interfaces. """ - # assumes self._objslock already held nodes = set() for emane_net in self._emane_nets.values(): for iface in emane_net.get_ifaces(): nodes.add(iface.node) return nodes - def setup(self) -> int: + def setup(self) -> EmaneState: """ Setup duties for EMANE manager. @@ -288,7 +294,7 @@ class EmaneManager(ModelManager): if not self._emane_nets: logging.debug("no emane nodes in session") - return EmaneManager.NOT_NEEDED + return EmaneState.NOT_NEEDED # check if bindings were installed if EventService is None: @@ -304,7 +310,7 @@ class EmaneManager(ModelManager): "EMANE cannot start, check core config. invalid OTA device provided: %s", otadev, ) - return EmaneManager.NOT_READY + return EmaneState.NOT_READY self.session.add_remove_control_net( net_index=netidx, remove=False, conf_required=False @@ -319,16 +325,16 @@ class EmaneManager(ModelManager): "EMANE cannot start, check core config. invalid event service device: %s", eventdev, ) - return EmaneManager.NOT_READY + return EmaneState.NOT_READY self.session.add_remove_control_net( net_index=netidx, remove=False, conf_required=False ) self.check_node_models() - return EmaneManager.SUCCESS + return EmaneState.SUCCESS - def startup(self) -> int: + def startup(self) -> EmaneState: """ After all the EMANE networks have been added, build XML files and start the daemons. @@ -337,39 +343,49 @@ class EmaneManager(ModelManager): instantiation """ self.reset() - r = self.setup() - - # NOT_NEEDED or NOT_READY - if r != EmaneManager.SUCCESS: - return r - - nems = [] + status = self.setup() + if status != EmaneState.SUCCESS: + return status + self.starteventmonitor() + self.buildeventservicexml() with self._emane_node_lock: - self.buildxml() - self.starteventmonitor() - - if self.numnems() > 0: - self.startdaemons() - self.install_ifaces() - - for node_id in self._emane_nets: - emane_node = self._emane_nets[node_id] - for iface in emane_node.get_ifaces(): - nems.append( - (iface.node.name, iface.name, emane_node.getnemid(iface)) + # on master, control network bridge added earlier in startup() + control_net = self.session.add_remove_control_net( + 0, remove=False, conf_required=False + ) + logging.info("emane building xmls...") + for node_id in sorted(self._emane_nets): + emane_net = self._emane_nets[node_id] + if not emane_net.model: + logging.error("emane net(%s) has no model", emane_net.name) + continue + for iface in emane_net.get_ifaces(): + if not iface.node: + logging.error( + "emane net(%s) connected interface missing node", + emane_net.name, + ) + continue + nem_id = self.next_nem_id() + self.nems[nem_id] = iface + self.write_nem(iface, nem_id) + emanexml.build_platform_xml( + self, control_net, emane_net, iface, nem_id ) - - if nems: - emane_nems_filename = os.path.join(self.session.session_dir, "emane_nems") - try: - with open(emane_nems_filename, "w") as f: - for nodename, ifname, nemid in nems: - f.write(f"{nodename} {ifname} {nemid}\n") - except IOError: - logging.exception("Error writing EMANE NEMs file: %s") + emanexml.build_model_xmls(self, emane_net, iface) + self.start_daemon(iface) + self.install_iface(emane_net, iface) if self.links_enabled(): self.link_monitor.start() - return EmaneManager.SUCCESS + return EmaneState.SUCCESS + + def write_nem(self, iface: CoreInterface, nem_id: int) -> None: + path = os.path.join(self.session.session_dir, "emane_nems") + try: + with open(path, "a") as f: + f.write(f"{iface.node.name} {iface.name} {nem_id}\n") + except IOError: + logging.exception("error writing to emane nem file") def links_enabled(self) -> bool: return self.get_config("link_enabled") == "1" @@ -380,17 +396,14 @@ class EmaneManager(ModelManager): """ if not self.genlocationevents(): return - with self._emane_node_lock: - for key in sorted(self._emane_nets.keys()): - emane_node = self._emane_nets[key] + for node_id in sorted(self._emane_nets): + emane_net = self._emane_nets[node_id] logging.debug( - "post startup for emane node: %s - %s", - emane_node.id, - emane_node.name, + "post startup for emane node: %s - %s", emane_net.id, emane_net.name ) - emane_node.model.post_startup() - for iface in emane_node.get_ifaces(): + emane_net.model.post_startup() + for iface in emane_net.get_ifaces(): iface.setposition() def reset(self) -> None: @@ -400,13 +413,7 @@ class EmaneManager(ModelManager): """ with self._emane_node_lock: self._emane_nets.clear() - - self.platformport = self.session.options.get_config_int( - "emane_platform_port", 8100 - ) - self.transformport = self.session.options.get_config_int( - "emane_transform_port", 8200 - ) + self.nems.clear() def shutdown(self) -> None: """ @@ -422,40 +429,23 @@ class EmaneManager(ModelManager): self.stopdaemons() self.stopeventmonitor() - def buildxml(self) -> None: - """ - Build XML files required to run EMANE on each node. - NEMs run inside containers using the control network for passing - events and data. - """ - # assume self._objslock is already held here - logging.info("emane building xml...") - # on master, control network bridge added earlier in startup() - ctrlnet = self.session.add_remove_control_net( - net_index=0, remove=False, conf_required=False - ) - self.buildplatformxml(ctrlnet) - self.buildnemxml() - self.buildeventservicexml() - def check_node_models(self) -> None: """ Associate EMANE model classes with EMANE network nodes. """ for node_id in self._emane_nets: - emane_node = self._emane_nets[node_id] + emane_net = self._emane_nets[node_id] logging.debug("checking emane model for node: %s", node_id) # skip nodes that already have a model set - if emane_node.model: + if emane_net.model: logging.debug( - "node(%s) already has model(%s)", - emane_node.id, - emane_node.model.name, + "node(%s) already has model(%s)", emane_net.id, emane_net.model.name ) continue - # set model configured for node, due to legacy messaging configuration before nodes exist + # set model configured for node, due to legacy messaging configuration + # before nodes exist model_name = self.node_models.get(node_id) if not model_name: logging.error("emane node(%s) has no node model", node_id) @@ -464,7 +454,7 @@ class EmaneManager(ModelManager): config = self.get_model_config(node_id=node_id, model_name=model_name) logging.debug("setting emane model(%s) config(%s)", model_name, config) model_class = self.models[model_name] - emane_node.setmodel(model_class, config) + emane_net.setmodel(model_class, config) def nemlookup(self, nemid) -> Tuple[Optional[EmaneNet], Optional[CoreInterface]]: """ @@ -473,7 +463,6 @@ class EmaneManager(ModelManager): """ emane_node = None iface = None - for node_id in self._emane_nets: emane_node = self._emane_nets[node_id] iface = emane_node.get_nem_iface(nemid) @@ -481,7 +470,6 @@ class EmaneManager(ModelManager): break else: emane_node = None - return emane_node, iface def get_nem_link( @@ -507,38 +495,6 @@ class EmaneManager(ModelManager): color=color, ) - def numnems(self) -> int: - """ - Return the number of NEMs emulated locally. - """ - count = 0 - for node_id in self._emane_nets: - emane_node = self._emane_nets[node_id] - count += len(emane_node.ifaces) - return count - - def buildplatformxml(self, ctrlnet: CtrlNet) -> None: - """ - Build a platform.xml file now that all nodes are configured. - """ - nemid = int(self.get_config("nem_id_start")) - platform_xmls = {} - - # assume self._objslock is already held here - for key in sorted(self._emane_nets.keys()): - emane_node = self._emane_nets[key] - nemid = emanexml.build_node_platform_xml( - self, ctrlnet, emane_node, nemid, platform_xmls - ) - - def buildnemxml(self) -> None: - """ - Builds the nem, mac, and phy xml files for each EMANE network. - """ - for key in sorted(self._emane_nets): - emane_net = self._emane_nets[key] - emanexml.build_xml_files(self, emane_net) - def buildeventservicexml(self) -> None: """ Build the libemaneeventservice.xml file if event service options @@ -571,7 +527,7 @@ class EmaneManager(ModelManager): ) ) - def startdaemons(self) -> None: + def start_daemon(self, iface: CoreInterface) -> None: """ Start one EMANE daemon per node having a radio. Add a control network even if the user has not configured one. @@ -583,69 +539,51 @@ class EmaneManager(ModelManager): if cfgloglevel: logging.info("setting user-defined EMANE log level: %d", cfgloglevel) loglevel = str(cfgloglevel) - emanecmd = f"emane -d -l {loglevel}" if realtime: emanecmd += " -r" - otagroup, _otaport = self.get_config("otamanagergroup").split(":") otadev = self.get_config("otamanagerdevice") otanetidx = self.session.get_control_net_index(otadev) - eventgroup, _eventport = self.get_config("eventservicegroup").split(":") eventdev = self.get_config("eventservicedevice") eventservicenetidx = self.session.get_control_net_index(eventdev) - - run_emane_on_host = False - for node in self.getnodes(): - if isinstance(node, Rj45Node): - run_emane_on_host = True - continue - path = self.session.session_dir - n = node.id - + node = iface.node + if not isinstance(node, Rj45Node): # control network not yet started here self.session.add_remove_control_iface( node, 0, remove=False, conf_required=False ) - if otanetidx > 0: logging.info("adding ota device ctrl%d", otanetidx) self.session.add_remove_control_iface( node, otanetidx, remove=False, conf_required=False ) - if eventservicenetidx >= 0: logging.info("adding event service device ctrl%d", eventservicenetidx) self.session.add_remove_control_iface( node, eventservicenetidx, remove=False, conf_required=False ) - # multicast route is needed for OTA data node.node_net_client.create_route(otagroup, otadev) - # multicast route is also needed for event data if on control network if eventservicenetidx >= 0 and eventgroup != otagroup: node.node_net_client.create_route(eventgroup, eventdev) - # start emane - log_file = os.path.join(path, f"emane{n}.log") - platform_xml = os.path.join(path, f"platform{n}.xml") + log_file = os.path.join(node.nodedir, f"{iface.name}-emane.log") + platform_xml = os.path.join(node.nodedir, f"{iface.name}-platform.xml") args = f"{emanecmd} -f {log_file} {platform_xml}" output = node.cmd(args) logging.info("node(%s) emane daemon running: %s", node.name, args) logging.debug("node(%s) emane daemon output: %s", node.name, output) - - if not run_emane_on_host: - return - - path = self.session.session_dir - log_file = os.path.join(path, "emane.log") - platform_xml = os.path.join(path, "platform.xml") - emanecmd += f" -f {log_file} {platform_xml}" - utils.cmd(emanecmd, cwd=path) - self.session.distributed.execute(lambda x: x.remote_cmd(emanecmd, cwd=path)) - logging.info("host emane daemon running: %s", emanecmd) + else: + path = self.session.session_dir + log_file = os.path.join(path, f"{iface.name}-emane.log") + platform_xml = os.path.join(path, f"{iface.name}-platform.xml") + emanecmd += f" -f {log_file} {platform_xml}" + utils.cmd(emanecmd, cwd=path) + self.session.distributed.execute(lambda x: x.remote_cmd(emanecmd, cwd=path)) + logging.info("host emane daemon running: %s", emanecmd) def stopdaemons(self) -> None: """ @@ -674,23 +612,27 @@ class EmaneManager(ModelManager): except CoreCommandError: logging.exception("error shutting down emane daemons") - def install_ifaces(self) -> None: - """ - Install TUN/TAP virtual interfaces into their proper namespaces - now that the EMANE daemons are running. - """ - for key in sorted(self._emane_nets.keys()): - node = self._emane_nets[key] - logging.info("emane install interface for node(%s): %d", node.name, key) - node.install_ifaces() + def install_iface(self, emane_net: EmaneNet, iface: CoreInterface) -> None: + config = self.get_iface_config(emane_net, iface) + external = config.get("external", "0") + if isinstance(iface, TunTap) and external == "0": + iface.set_ips() + # at this point we register location handlers for generating + # EMANE location events + if self.genlocationevents(): + iface.poshook = emane_net.setnemposition + iface.setposition() def deinstall_ifaces(self) -> None: """ Uninstall TUN/TAP virtual interfaces. """ - for key in sorted(self._emane_nets.keys()): - emane_node = self._emane_nets[key] - emane_node.deinstall_ifaces() + for key in sorted(self._emane_nets): + emane_net = self._emane_nets[key] + for iface in emane_net.get_ifaces(): + if iface.transport_type == TransportType.VIRTUAL: + iface.shutdown() + iface.poshook = None def doeventmonitor(self) -> bool: """ @@ -718,7 +660,6 @@ class EmaneManager(ModelManager): logging.info("emane start event monitor") if not self.doeventmonitor(): return - if self.service is None: logging.error( "Warning: EMANE events will not be generated " diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index 43fbc0fb..0576d6c3 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -96,9 +96,7 @@ class EmaneModel(WirelessModel): ConfigGroup("External Parameters", phy_len + 1, config_len), ] - def build_xml_files( - self, config: Dict[str, str], iface: CoreInterface = None - ) -> None: + def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None: """ Builds xml files for this emane model. Creates a nem.xml file that points to both mac.xml and phy.xml definitions. @@ -107,33 +105,30 @@ class EmaneModel(WirelessModel): :param iface: interface for the emane node :return: nothing """ - nem_name = emanexml.nem_file_name(self, iface) - mac_name = emanexml.mac_file_name(self, iface) - phy_name = emanexml.phy_file_name(self, iface) - - # remote server for file - server = None - if iface is not None: - server = iface.node.server + nem_name = emanexml.nem_file_name(iface) + mac_name = emanexml.mac_file_name(iface) + phy_name = emanexml.phy_file_name(iface) # check if this is external transport_type = TransportType.VIRTUAL - if iface and iface.transport_type == TransportType.RAW: + if iface.transport_type == TransportType.RAW: transport_type = TransportType.RAW - transport_name = emanexml.transport_file_name(self.id, transport_type) + transport_name = emanexml.transport_file_name(iface, transport_type) + node = iface.node + server = node.server # create nem xml file - nem_file = os.path.join(self.session.session_dir, nem_name) + nem_file = os.path.join(node.nodedir, nem_name) emanexml.create_nem_xml( self, config, nem_file, transport_name, mac_name, phy_name, server ) # create mac xml file - mac_file = os.path.join(self.session.session_dir, mac_name) + mac_file = os.path.join(node.nodedir, mac_name) emanexml.create_mac_xml(self, config, mac_file, server) # create phy xml file - phy_file = os.path.join(self.session.session_dir, phy_name) + phy_file = os.path.join(node.nodedir, phy_name) emanexml.create_phy_xml(self, config, phy_file, server) def post_startup(self) -> None: diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index be95e6d0..dca85785 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -8,17 +8,10 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type from core.emulator.data import InterfaceData, LinkData, LinkOptions from core.emulator.distributed import DistributedServer -from core.emulator.enumerations import ( - EventTypes, - LinkTypes, - MessageFlags, - NodeTypes, - RegisterTlvs, - TransportType, -) +from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes, RegisterTlvs from core.errors import CoreError from core.nodes.base import CoreNetworkBase, CoreNode -from core.nodes.interface import CoreInterface, TunTap +from core.nodes.interface import CoreInterface if TYPE_CHECKING: from core.emane.emanemodel import EmaneModel @@ -139,45 +132,6 @@ class EmaneNet(CoreNetworkBase): return iface return None - def install_ifaces(self) -> None: - """ - Install TAP devices into their namespaces. This is done after - EMANE daemons have been started, because that is their only chance - to bind to the TAPs. - """ - if ( - self.session.emane.genlocationevents() - and self.session.emane.service is None - ): - warntxt = "unable to publish EMANE events because the eventservice " - warntxt += "Python bindings failed to load" - logging.error(warntxt) - for iface in self.get_ifaces(): - config = self.session.emane.get_iface_config( - self.id, iface, self.model.name - ) - external = config.get("external", "0") - if isinstance(iface, TunTap) and external == "0": - iface.set_ips() - if not self.session.emane.genlocationevents(): - iface.poshook = None - continue - # at this point we register location handlers for generating - # EMANE location events - iface.poshook = self.setnemposition - iface.setposition() - - def deinstall_ifaces(self) -> None: - """ - Uninstall TAP devices. This invokes their shutdown method for - any required cleanup; the device may be actually removed when - emanetransportd terminates. - """ - for iface in self.get_ifaces(): - if iface.transport_type == TransportType.VIRTUAL: - iface.shutdown() - iface.poshook = None - def _nem_position( self, iface: CoreInterface ) -> Optional[Tuple[int, float, float, float]]: @@ -275,20 +229,4 @@ class EmaneNet(CoreNetworkBase): iface.set_mac(iface_data.mac) for ip in iface_data.get_ips(): iface.add_ip(ip) - # TODO: if added during runtime start EMANE - if self.session.state == EventTypes.RUNTIME_STATE: - logging.info("startup emane for node: %s", node.name) - # create specific xml if needed - config = self.session.emane.get_iface_config( - self.model.id, iface, self.model.name - ) - if config: - self.model.build_xml_files(config, iface) - - # start emane daemon - - # install netif - - # add nem to nemfile - return iface diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index c2573578..9f5364b9 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -15,7 +15,7 @@ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, TypeVa from core import constants, utils from core.configservice.manager import ConfigServiceManager -from core.emane.emanemanager import EmaneManager +from core.emane.emanemanager import EmaneManager, EmaneState from core.emane.nodes import EmaneNet from core.emulator.data import ( ConfigData, @@ -1181,7 +1181,7 @@ class Session: self.distributed.start() # instantiate will be invoked again upon emane configure - if self.emane.startup() == self.emane.NOT_READY: + if self.emane.startup() == EmaneState.NOT_READY: return [] # boot node services and then start mobility diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index eece57c9..32ca0f67 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -10,6 +10,7 @@ from core.config import Configuration from core.emane.nodes import EmaneNet from core.emulator.distributed import DistributedServer from core.emulator.enumerations import TransportType +from core.errors import CoreError from core.nodes.interface import CoreInterface from core.nodes.network import CtrlNet from core.xml import corexml @@ -40,15 +41,11 @@ def _value_to_params(value: str) -> Optional[Tuple[str]]: """ try: values = utils.make_tuple_fromstr(value, str) - if not hasattr(values, "__iter__"): return None - if len(values) < 2: return None - return values - except SyntaxError: logging.exception("error in value string to param list") return None @@ -127,13 +124,13 @@ def add_configurations( add_param(xml_element, name, value) -def build_node_platform_xml( +def build_platform_xml( emane_manager: "EmaneManager", control_net: CtrlNet, - node: EmaneNet, + emane_net: EmaneNet, + iface: CoreInterface, nem_id: int, - platform_xmls: Dict[str, etree.Element], -) -> int: +) -> None: """ Create platform xml for a specific node. @@ -141,175 +138,121 @@ def build_node_platform_xml( configurations :param control_net: control net node for this emane network - :param node: node to write platform xml for - :param nem_id: nem id to use for interfaces for this node - :param platform_xmls: stores platform xml elements to append nem entries to + :param emane_net: emane network associated with interface + :param iface: interface running emane + :param nem_id: nem id to use for this interface :return: the next nem id that can be used for creating platform xml files """ - logging.debug( - "building emane platform xml for node(%s) nem_id(%s): %s", - node, - nem_id, - node.name, + # build nem xml + nem_definition = nem_file_name(iface) + nem_element = etree.Element( + "nem", id=str(nem_id), name=iface.localname, definition=nem_definition ) - nem_entries = {} - if node.model is None: - logging.warning("warning: EMANE network %s has no associated model", node.name) - return nem_id - - for iface in node.get_ifaces(): - logging.debug( - "building platform xml for interface(%s) nem_id(%s)", iface.name, nem_id - ) - # build nem xml - nem_definition = nem_file_name(node.model, iface) - nem_element = etree.Element( - "nem", id=str(nem_id), name=iface.localname, definition=nem_definition + # check if this is an external transport, get default config if an interface + # specific one does not exist + config = emane_manager.get_iface_config(emane_net, iface) + if is_external(config): + nem_element.set("transport", "external") + platform_endpoint = "platformendpoint" + add_param(nem_element, platform_endpoint, config[platform_endpoint]) + transport_endpoint = "transportendpoint" + add_param(nem_element, transport_endpoint, config[transport_endpoint]) + else: + # build transport xml + transport_type = iface.transport_type + if not transport_type: + logging.info("warning: %s interface type unsupported!", iface.name) + transport_type = TransportType.RAW + transport_file = transport_file_name(iface, transport_type) + transport_element = etree.SubElement( + nem_element, "transport", definition=transport_file ) + add_param(transport_element, "device", iface.name) - # check if this is an external transport, get default config if an interface - # specific one does not exist - config = emane_manager.get_iface_config(node.model.id, iface, node.model.name) - - if is_external(config): - nem_element.set("transport", "external") - platform_endpoint = "platformendpoint" - add_param(nem_element, platform_endpoint, config[platform_endpoint]) - transport_endpoint = "transportendpoint" - add_param(nem_element, transport_endpoint, config[transport_endpoint]) - else: - # build transport xml - transport_type = iface.transport_type - if not transport_type: - logging.info("warning: %s interface type unsupported!", iface.name) - transport_type = TransportType.RAW - transport_file = transport_file_name(node.id, transport_type) - transport_element = etree.SubElement( - nem_element, "transport", definition=transport_file - ) - - # add transport parameter - add_param(transport_element, "device", iface.name) - - # add nem entry - nem_entries[iface] = nem_element - - # merging code - key = iface.node.id - if iface.transport_type == TransportType.RAW: - key = "host" - otadev = control_net.brname - eventdev = control_net.brname - else: - otadev = None - eventdev = None - - platform_element = platform_xmls.get(key) - if platform_element is None: - platform_element = etree.Element("platform") - - if otadev: - emane_manager.set_config("otamanagerdevice", otadev) - - if eventdev: - emane_manager.set_config("eventservicedevice", eventdev) - - # append all platform options (except starting id) to doc - for configuration in emane_manager.emane_config.emulator_config: - name = configuration.id - if name == "platform_id_start": - continue - - value = emane_manager.get_config(name) - add_param(platform_element, name, value) - - # add platform xml - platform_xmls[key] = platform_element - - platform_element.append(nem_element) - - node.setnemid(iface, nem_id) - mac = _MAC_PREFIX + ":00:00:" - mac += f"{(nem_id >> 8) & 0xFF:02X}:{nem_id & 0xFF:02X}" - iface.set_mac(mac) - - # increment nem id - nem_id += 1 + # determine platform element to add xml to + key = iface.node.id + if iface.transport_type == TransportType.RAW: + key = "host" + otadev = control_net.brname + eventdev = control_net.brname + else: + otadev = None + eventdev = None + platform_element = etree.Element("platform") + if otadev: + emane_manager.set_config("otamanagerdevice", otadev) + if eventdev: + emane_manager.set_config("eventservicedevice", eventdev) + for configuration in emane_manager.emane_config.emulator_config: + name = configuration.id + value = emane_manager.get_config(name) + add_param(platform_element, name, value) + platform_element.append(nem_element) + emane_net.setnemid(iface, nem_id) + mac = _MAC_PREFIX + ":00:00:" + mac += f"{(nem_id >> 8) & 0xFF:02X}:{nem_id & 0xFF:02X}" + iface.set_mac(mac) doc_name = "platform" - for key in sorted(platform_xmls.keys()): - platform_element = platform_xmls[key] - if key == "host": - file_name = "platform.xml" - file_path = os.path.join(emane_manager.session.session_dir, file_name) - create_file(platform_element, doc_name, file_path) - else: - file_name = f"platform{key}.xml" - file_path = os.path.join(emane_manager.session.session_dir, file_name) - linked_node = emane_manager.session.nodes[key] - create_file(platform_element, doc_name, file_path, linked_node.server) - - return nem_id + server = None + if key == "host": + file_name = "platform.xml" + file_path = os.path.join(emane_manager.session.session_dir, file_name) + else: + node = iface.node + file_name = f"{iface.name}-platform.xml" + file_path = os.path.join(node.nodedir, file_name) + server = node.server + create_file(platform_element, doc_name, file_path, server) -def build_xml_files(emane_manager: "EmaneManager", node: EmaneNet) -> None: +def build_model_xmls( + manager: "EmaneManager", emane_net: EmaneNet, iface: CoreInterface +) -> None: """ Generate emane xml files required for node. - :param emane_manager: emane manager with emane + :param manager: emane manager with emane configurations - :param node: node to write platform xml for + :param emane_net: emane network associated with interface + :param iface: interface to create emane xml for :return: nothing """ - logging.debug("building all emane xml for node(%s): %s", node, node.name) - if node.model is None: - return - - # get model configurations - config = emane_manager.get_configs(node.model.id, node.model.name) - if not config: - return - - # build XML for overall network EMANE configs - node.model.build_xml_files(config) - # build XML for specific interface (NEM) configs + # check for interface specific emane configuration and write xml files + config = manager.get_iface_config(emane_net, iface) + emane_net.model.build_xml_files(config, iface) + + # check transport type needed for interface need_virtual = False need_raw = False vtype = TransportType.VIRTUAL rtype = TransportType.RAW - - for iface in node.get_ifaces(): - # check for interface specific emane configuration and write xml files - config = emane_manager.get_iface_config(node.model.id, iface, node.model.name) - if config: - node.model.build_xml_files(config, iface) - - # check transport type needed for interface - if iface.transport_type == TransportType.VIRTUAL: - need_virtual = True - vtype = iface.transport_type - else: - need_raw = True - rtype = iface.transport_type - + if iface.transport_type == TransportType.VIRTUAL: + need_virtual = True + vtype = iface.transport_type + else: + need_raw = True + rtype = iface.transport_type if need_virtual: - build_transport_xml(emane_manager, node, vtype) - + build_transport_xml(manager, emane_net, iface, vtype) if need_raw: - build_transport_xml(emane_manager, node, rtype) + build_transport_xml(manager, emane_net, iface, rtype) def build_transport_xml( - emane_manager: "EmaneManager", node: EmaneNet, transport_type: TransportType + manager: "EmaneManager", + emane_net: EmaneNet, + iface: CoreInterface, + transport_type: TransportType, ) -> None: """ Build transport xml file for node and transport type. - :param emane_manager: emane manager with emane - configurations - :param node: node to write platform xml for + :param manager: emane manager with emane configurations + :param emane_net: emane network associated with interface + :param iface: interface to build transport xml for :param transport_type: transport type to build xml for :return: nothing """ @@ -318,28 +261,24 @@ def build_transport_xml( name=f"{transport_type.value.capitalize()} Transport", library=f"trans{transport_type.value.lower()}", ) - - # add bitrate add_param(transport_element, "bitrate", "0") # get emane model cnfiguration - config = emane_manager.get_configs(node.id, node.model.name) + config = manager.get_iface_config(emane_net, iface) flowcontrol = config.get("flowcontrolenable", "0") == "1" - if transport_type == TransportType.VIRTUAL: device_path = "/dev/net/tun_flowctl" if not os.path.exists(device_path): device_path = "/dev/net/tun" add_param(transport_element, "devicepath", device_path) - if flowcontrol: add_param(transport_element, "flowcontrolenable", "on") - doc_name = "transport" - file_name = transport_file_name(node.id, transport_type) - file_path = os.path.join(emane_manager.session.session_dir, file_name) + node = iface.node + file_name = transport_file_name(iface, transport_type) + file_path = os.path.join(node.nodedir, file_name) create_file(transport_element, doc_name, file_path) - emane_manager.session.distributed.execute( + manager.session.distributed.execute( lambda x: create_file(transport_element, doc_name, file_path, x) ) @@ -348,7 +287,7 @@ def create_phy_xml( emane_model: "EmaneModel", config: Dict[str, str], file_path: str, - server: DistributedServer, + server: Optional[DistributedServer], ) -> None: """ Create the phy xml document. @@ -363,25 +302,17 @@ def create_phy_xml( phy_element = etree.Element("phy", name=f"{emane_model.name} PHY") if emane_model.phy_library: phy_element.set("library", emane_model.phy_library) - add_configurations( phy_element, emane_model.phy_config, config, emane_model.config_ignore ) - create_file(phy_element, "phy", file_path) - if server is not None: - create_file(phy_element, "phy", file_path, server) - else: - create_file(phy_element, "phy", file_path) - emane_model.session.distributed.execute( - lambda x: create_file(phy_element, "phy", file_path, x) - ) + create_file(phy_element, "phy", file_path, server) def create_mac_xml( emane_model: "EmaneModel", config: Dict[str, str], file_path: str, - server: DistributedServer, + server: Optional[DistributedServer], ) -> None: """ Create the mac xml document. @@ -394,22 +325,14 @@ def create_mac_xml( :return: nothing """ if not emane_model.mac_library: - raise ValueError("must define emane model library") - + raise CoreError("must define emane model library") mac_element = etree.Element( "mac", name=f"{emane_model.name} MAC", library=emane_model.mac_library ) add_configurations( mac_element, emane_model.mac_config, config, emane_model.config_ignore ) - create_file(mac_element, "mac", file_path) - if server is not None: - create_file(mac_element, "mac", file_path, server) - else: - create_file(mac_element, "mac", file_path) - emane_model.session.distributed.execute( - lambda x: create_file(mac_element, "mac", file_path, x) - ) + create_file(mac_element, "mac", file_path, server) def create_nem_xml( @@ -419,7 +342,7 @@ def create_nem_xml( transport_definition: str, mac_definition: str, phy_definition: str, - server: DistributedServer, + server: Optional[DistributedServer], ) -> None: """ Create the nem xml document. @@ -441,13 +364,7 @@ def create_nem_xml( etree.SubElement(nem_element, "transport", definition=transport_definition) etree.SubElement(nem_element, "mac", definition=mac_definition) etree.SubElement(nem_element, "phy", definition=phy_definition) - if server is not None: - create_file(nem_element, "nem", nem_file, server) - else: - create_file(nem_element, "nem", nem_file) - emane_model.session.distributed.execute( - lambda x: create_file(nem_element, "nem", nem_file, x) - ) + create_file(nem_element, "nem", nem_file, server) def create_event_service_xml( @@ -483,81 +400,55 @@ def create_event_service_xml( create_file(event_element, "emaneeventmsgsvc", file_path, server) -def transport_file_name(node_id: int, transport_type: TransportType) -> str: +def transport_file_name(iface: CoreInterface, transport_type: TransportType) -> str: """ Create name for a transport xml file. - :param node_id: node id to generate transport file name for + :param iface: interface running emane :param transport_type: transport type to generate transport file - :return: + :return: transport xml file name """ - return f"n{node_id}trans{transport_type.value}.xml" + return f"{iface.name}-trans-{transport_type.value}.xml" -def _basename(emane_model: "EmaneModel", iface: CoreInterface = None) -> str: +def nem_file_name(iface: CoreInterface) -> str: """ - Create name that is leveraged for configuration file creation. + Return the string name for the NEM XML file, e.g. "eth0-nem.xml" - :param emane_model: emane model to create name for - :param iface: interface for this model - :return: basename used for file creation + :param iface: interface running emane + :return: nem xm file name """ - name = f"n{emane_model.id}" - - if iface: - node_id = iface.node.id - if emane_model.session.emane.get_iface_config(node_id, iface, emane_model.name): - name = iface.localname.replace(".", "_") - - return f"{name}{emane_model.name}" - - -def nem_file_name(emane_model: "EmaneModel", iface: CoreInterface = None) -> str: - """ - Return the string name for the NEM XML file, e.g. "n3rfpipenem.xml" - - :param emane_model: emane model to create file - :param iface: interface for this model - :return: nem xml filename - """ - basename = _basename(emane_model, iface) append = "" if iface and iface.transport_type == TransportType.RAW: - append = "_raw" - return f"{basename}nem{append}.xml" + append = "-raw" + return f"{iface.name}-nem{append}.xml" -def shim_file_name(emane_model: "EmaneModel", iface: CoreInterface = None) -> str: +def shim_file_name(iface: CoreInterface = None) -> str: """ - Return the string name for the SHIM XML file, e.g. "commeffectshim.xml" + Return the string name for the SHIM XML file, e.g. "eth0-shim.xml" - :param emane_model: emane model to create file - :param iface: interface for this model - :return: shim xml filename + :param iface: interface running emane + :return: shim xml file name """ - name = _basename(emane_model, iface) - return f"{name}shim.xml" + return f"{iface.name}-shim.xml" -def mac_file_name(emane_model: "EmaneModel", iface: CoreInterface = None) -> str: +def mac_file_name(iface: CoreInterface) -> str: """ - Return the string name for the MAC XML file, e.g. "n3rfpipemac.xml" + Return the string name for the MAC XML file, e.g. "eth0-mac.xml" - :param emane_model: emane model to create file - :param iface: interface for this model - :return: mac xml filename + :param iface: interface running emane + :return: mac xml file name """ - name = _basename(emane_model, iface) - return f"{name}mac.xml" + return f"{iface.name}-mac.xml" -def phy_file_name(emane_model: "EmaneModel", iface: CoreInterface = None) -> str: +def phy_file_name(iface: CoreInterface) -> str: """ - Return the string name for the PHY XML file, e.g. "n3rfpipephy.xml" + Return the string name for the PHY XML file, e.g. "eth0-phy.xml" - :param emane_model: emane model to create file - :param iface: interface for this model - :return: phy xml filename + :param iface: interface running emane + :return: phy xml file name """ - name = _basename(emane_model, iface) - return f"{name}phy.xml" + return f"{iface.name}-phy.xml" From ce4b61d3b21afe5a4ac4402698f0eb09af1c57ac Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 2 Jul 2020 17:49:56 -0700 Subject: [PATCH 0434/1131] daemon: further heavy cleanup to how emane generates and runs xml files --- daemon/core/emane/commeffect.py | 26 ++--- daemon/core/emane/emanemanager.py | 46 ++++---- daemon/core/emane/emanemodel.py | 34 ++---- daemon/core/emane/nodes.py | 1 - daemon/core/xml/emanexml.py | 175 +++++++++++------------------- 5 files changed, 103 insertions(+), 179 deletions(-) diff --git a/daemon/core/emane/commeffect.py b/daemon/core/emane/commeffect.py index 100af9a7..a812b66d 100644 --- a/daemon/core/emane/commeffect.py +++ b/daemon/core/emane/commeffect.py @@ -12,7 +12,6 @@ from core.config import ConfigGroup, Configuration from core.emane import emanemanifest, emanemodel from core.emane.nodes import EmaneNet from core.emulator.data import LinkOptions -from core.emulator.enumerations import TransportType from core.nodes.interface import CoreInterface from core.xml import emanexml @@ -73,26 +72,16 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): :param iface: interface for the emane node :return: nothing """ - # interface node - node = iface.node - - # retrieve xml names - nem_name = emanexml.nem_file_name(iface) - shim_name = emanexml.shim_file_name(iface) - # create and write nem document nem_element = etree.Element("nem", name=f"{self.name} NEM", type="unstructured") - transport_type = TransportType.VIRTUAL - if iface.transport_type == TransportType.RAW: - transport_type = TransportType.RAW - transport_file = emanexml.transport_file_name(iface, transport_type) - etree.SubElement(nem_element, "transport", definition=transport_file) + transport_name = emanexml.transport_file_name(iface) + etree.SubElement(nem_element, "transport", definition=transport_name) # set shim configuration + nem_name = emanexml.nem_file_name(iface) + shim_name = emanexml.shim_file_name(iface) etree.SubElement(nem_element, "shim", definition=shim_name) - - nem_file = os.path.join(node.nodedir, nem_name) - emanexml.create_file(nem_element, "nem", nem_file) + emanexml.create_iface_file(iface, nem_element, "nem", nem_name) # create and write shim document shim_element = etree.Element( @@ -111,9 +100,10 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): ff = config["filterfile"] if ff.strip() != "": emanexml.add_param(shim_element, "filterfile", ff) + emanexml.create_iface_file(iface, shim_element, "shim", shim_name) - shim_file = os.path.join(node.nodedir, shim_name) - emanexml.create_file(shim_element, "shim", shim_file) + # create transport xml + emanexml.create_transport_xml(iface, config) def linkconfig( self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 3317a5db..4e2984a0 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -281,8 +281,6 @@ class EmaneManager(ModelManager): instantiation """ logging.debug("emane setup") - - # TODO: drive this from the session object with self.session.nodes_lock: for node_id in self.session.nodes: node = self.session.nodes[node_id] @@ -291,7 +289,6 @@ class EmaneManager(ModelManager): "adding emane node: id(%s) name(%s)", node.id, node.name ) self.add_node(node) - if not self._emane_nets: logging.debug("no emane nodes in session") return EmaneState.NOT_NEEDED @@ -322,7 +319,7 @@ class EmaneManager(ModelManager): logging.debug("emane event service device index: %s", netidx) if netidx < 0: logging.error( - "EMANE cannot start, check core config. invalid event service device: %s", + "emane cannot start due to invalid event service device: %s", eventdev, ) return EmaneState.NOT_READY @@ -330,7 +327,6 @@ class EmaneManager(ModelManager): self.session.add_remove_control_net( net_index=netidx, remove=False, conf_required=False ) - self.check_node_models() return EmaneState.SUCCESS @@ -349,10 +345,6 @@ class EmaneManager(ModelManager): self.starteventmonitor() self.buildeventservicexml() with self._emane_node_lock: - # on master, control network bridge added earlier in startup() - control_net = self.session.add_remove_control_net( - 0, remove=False, conf_required=False - ) logging.info("emane building xmls...") for node_id in sorted(self._emane_nets): emane_net = self._emane_nets[node_id] @@ -360,25 +352,31 @@ class EmaneManager(ModelManager): logging.error("emane net(%s) has no model", emane_net.name) continue for iface in emane_net.get_ifaces(): - if not iface.node: - logging.error( - "emane net(%s) connected interface missing node", - emane_net.name, - ) - continue - nem_id = self.next_nem_id() - self.nems[nem_id] = iface - self.write_nem(iface, nem_id) - emanexml.build_platform_xml( - self, control_net, emane_net, iface, nem_id - ) - emanexml.build_model_xmls(self, emane_net, iface) - self.start_daemon(iface) - self.install_iface(emane_net, iface) + self.start_iface(emane_net, iface) if self.links_enabled(): self.link_monitor.start() return EmaneState.SUCCESS + def start_iface(self, emane_net: EmaneNet, iface: CoreInterface) -> None: + if not iface.node: + logging.error( + "emane net(%s) connected interface(%s) missing node", + emane_net.name, + iface.name, + ) + return + control_net = self.session.add_remove_control_net( + 0, remove=False, conf_required=False + ) + nem_id = self.next_nem_id() + self.nems[nem_id] = iface + self.write_nem(iface, nem_id) + emanexml.build_platform_xml(self, control_net, emane_net, iface, nem_id) + config = self.get_iface_config(emane_net, iface) + emane_net.model.build_xml_files(config, iface) + self.start_daemon(iface) + self.install_iface(emane_net, iface) + def write_nem(self, iface: CoreInterface, nem_id: int) -> None: path = os.path.join(self.session.session_dir, "emane_nems") try: diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index 0576d6c3..8672163d 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -9,7 +9,7 @@ from core.config import ConfigGroup, Configuration from core.emane import emanemanifest from core.emane.nodes import EmaneNet from core.emulator.data import LinkOptions -from core.emulator.enumerations import ConfigDataTypes, TransportType +from core.emulator.enumerations import ConfigDataTypes from core.errors import CoreError from core.location.mobility import WirelessModel from core.nodes.base import CoreNode @@ -102,34 +102,14 @@ class EmaneModel(WirelessModel): both mac.xml and phy.xml definitions. :param config: emane model configuration for the node and interface - :param iface: interface for the emane node + :param iface: interface to run emane for :return: nothing """ - nem_name = emanexml.nem_file_name(iface) - mac_name = emanexml.mac_file_name(iface) - phy_name = emanexml.phy_file_name(iface) - - # check if this is external - transport_type = TransportType.VIRTUAL - if iface.transport_type == TransportType.RAW: - transport_type = TransportType.RAW - transport_name = emanexml.transport_file_name(iface, transport_type) - - node = iface.node - server = node.server - # create nem xml file - nem_file = os.path.join(node.nodedir, nem_name) - emanexml.create_nem_xml( - self, config, nem_file, transport_name, mac_name, phy_name, server - ) - - # create mac xml file - mac_file = os.path.join(node.nodedir, mac_name) - emanexml.create_mac_xml(self, config, mac_file, server) - - # create phy xml file - phy_file = os.path.join(node.nodedir, phy_name) - emanexml.create_phy_xml(self, config, phy_file, server) + # create nem, mac, and phy xml files + emanexml.create_nem_xml(self, iface, config) + emanexml.create_mac_xml(self, iface, config) + emanexml.create_phy_xml(self, iface, config) + emanexml.create_transport_xml(iface, config) def post_startup(self) -> None: """ diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index dca85785..7e8a0a4f 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -96,7 +96,6 @@ class EmaneNet(CoreNetworkBase): """ set the EmaneModel associated with this node """ - logging.info("adding model: %s", model.name) if model.config_type == RegisterTlvs.WIRELESS: # EmaneModel really uses values from ConfigurableManager # when buildnemxml() is called, not during init() diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index 32ca0f67..cb605b21 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -13,6 +13,7 @@ from core.emulator.enumerations import TransportType from core.errors import CoreError from core.nodes.interface import CoreInterface from core.nodes.network import CtrlNet +from core.nodes.physical import Rj45Node from core.xml import corexml if TYPE_CHECKING: @@ -63,16 +64,15 @@ def create_file( :param xml_element: root element to write to file :param doc_name: name to use in the emane doctype :param file_path: file path to write xml file to - :param server: remote server node - will run on, default is None for localhost + :param server: remote server to create file on :return: nothing """ doctype = ( f'' ) - if server is not None: + if server: temp = NamedTemporaryFile(delete=False) - create_file(xml_element, doc_name, temp.name) + corexml.write_xml_file(xml_element, temp.name, doctype=doctype) temp.close() server.remote_put(temp.name, file_path) os.unlink(temp.name) @@ -80,6 +80,26 @@ def create_file( corexml.write_xml_file(xml_element, file_path, doctype=doctype) +def create_iface_file( + iface: CoreInterface, xml_element: etree.Element, doc_name: str, file_name: str +) -> None: + """ + Create emane xml for an interface. + + :param iface: interface running emane + :param xml_element: root element to write to file + :param doc_name: name to use in the emane doctype + :param file_name: name of xml file + :return: + """ + node = iface.node + if isinstance(node, Rj45Node): + file_path = os.path.join(node.session.session_dir, file_name) + else: + file_path = os.path.join(node.nodedir, file_name) + create_file(xml_element, doc_name, file_path, node.server) + + def add_param(xml_element: etree.Element, name: str, value: str) -> None: """ Add emane configuration parameter to xml element. @@ -159,21 +179,14 @@ def build_platform_xml( transport_endpoint = "transportendpoint" add_param(nem_element, transport_endpoint, config[transport_endpoint]) else: - # build transport xml - transport_type = iface.transport_type - if not transport_type: - logging.info("warning: %s interface type unsupported!", iface.name) - transport_type = TransportType.RAW - transport_file = transport_file_name(iface, transport_type) + transport_name = transport_file_name(iface) transport_element = etree.SubElement( - nem_element, "transport", definition=transport_file + nem_element, "transport", definition=transport_name ) add_param(transport_element, "device", iface.name) # determine platform element to add xml to - key = iface.node.id if iface.transport_type == TransportType.RAW: - key = "host" otadev = control_net.brname eventdev = control_net.brname else: @@ -195,67 +208,19 @@ def build_platform_xml( iface.set_mac(mac) doc_name = "platform" - server = None - if key == "host": - file_name = "platform.xml" - file_path = os.path.join(emane_manager.session.session_dir, file_name) - else: - node = iface.node - file_name = f"{iface.name}-platform.xml" - file_path = os.path.join(node.nodedir, file_name) - server = node.server - create_file(platform_element, doc_name, file_path, server) + file_name = f"{iface.name}-platform.xml" + create_iface_file(iface, platform_element, doc_name, file_name) -def build_model_xmls( - manager: "EmaneManager", emane_net: EmaneNet, iface: CoreInterface -) -> None: - """ - Generate emane xml files required for node. - - :param manager: emane manager with emane - configurations - :param emane_net: emane network associated with interface - :param iface: interface to create emane xml for - :return: nothing - """ - # build XML for specific interface (NEM) configs - # check for interface specific emane configuration and write xml files - config = manager.get_iface_config(emane_net, iface) - emane_net.model.build_xml_files(config, iface) - - # check transport type needed for interface - need_virtual = False - need_raw = False - vtype = TransportType.VIRTUAL - rtype = TransportType.RAW - if iface.transport_type == TransportType.VIRTUAL: - need_virtual = True - vtype = iface.transport_type - else: - need_raw = True - rtype = iface.transport_type - if need_virtual: - build_transport_xml(manager, emane_net, iface, vtype) - if need_raw: - build_transport_xml(manager, emane_net, iface, rtype) - - -def build_transport_xml( - manager: "EmaneManager", - emane_net: EmaneNet, - iface: CoreInterface, - transport_type: TransportType, -) -> None: +def create_transport_xml(iface: CoreInterface, config: Dict[str, str]) -> None: """ Build transport xml file for node and transport type. - :param manager: emane manager with emane configurations - :param emane_net: emane network associated with interface :param iface: interface to build transport xml for - :param transport_type: transport type to build xml for + :param config: all current configuration values :return: nothing """ + transport_type = get_transport_type(iface) transport_element = etree.Element( "transport", name=f"{transport_type.value.capitalize()} Transport", @@ -264,7 +229,6 @@ def build_transport_xml( add_param(transport_element, "bitrate", "0") # get emane model cnfiguration - config = manager.get_iface_config(emane_net, iface) flowcontrol = config.get("flowcontrolenable", "0") == "1" if transport_type == TransportType.VIRTUAL: device_path = "/dev/net/tun_flowctl" @@ -274,29 +238,19 @@ def build_transport_xml( if flowcontrol: add_param(transport_element, "flowcontrolenable", "on") doc_name = "transport" - node = iface.node - file_name = transport_file_name(iface, transport_type) - file_path = os.path.join(node.nodedir, file_name) - create_file(transport_element, doc_name, file_path) - manager.session.distributed.execute( - lambda x: create_file(transport_element, doc_name, file_path, x) - ) + transport_name = transport_file_name(iface) + create_iface_file(iface, transport_element, doc_name, transport_name) def create_phy_xml( - emane_model: "EmaneModel", - config: Dict[str, str], - file_path: str, - server: Optional[DistributedServer], + emane_model: "EmaneModel", iface: CoreInterface, config: Dict[str, str] ) -> None: """ Create the phy xml document. :param emane_model: emane model to create xml + :param iface: interface to create xml for :param config: all current configuration values - :param file_path: path to write file to - :param server: remote server node - will run on, default is None for localhost :return: nothing """ phy_element = etree.Element("phy", name=f"{emane_model.name} PHY") @@ -305,23 +259,19 @@ def create_phy_xml( add_configurations( phy_element, emane_model.phy_config, config, emane_model.config_ignore ) - create_file(phy_element, "phy", file_path, server) + file_name = phy_file_name(iface) + create_iface_file(iface, phy_element, "phy", file_name) def create_mac_xml( - emane_model: "EmaneModel", - config: Dict[str, str], - file_path: str, - server: Optional[DistributedServer], + emane_model: "EmaneModel", iface: CoreInterface, config: Dict[str, str] ) -> None: """ Create the mac xml document. :param emane_model: emane model to create xml + :param iface: interface to create xml for :param config: all current configuration values - :param file_path: path to write file to - :param server: remote server node - will run on, default is None for localhost :return: nothing """ if not emane_model.mac_library: @@ -332,39 +282,33 @@ def create_mac_xml( add_configurations( mac_element, emane_model.mac_config, config, emane_model.config_ignore ) - create_file(mac_element, "mac", file_path, server) + file_name = mac_file_name(iface) + create_iface_file(iface, mac_element, "mac", file_name) def create_nem_xml( - emane_model: "EmaneModel", - config: Dict[str, str], - nem_file: str, - transport_definition: str, - mac_definition: str, - phy_definition: str, - server: Optional[DistributedServer], + emane_model: "EmaneModel", iface: CoreInterface, config: Dict[str, str] ) -> None: """ Create the nem xml document. :param emane_model: emane model to create xml + :param iface: interface to create xml for :param config: all current configuration values - :param nem_file: nem file path to write - :param transport_definition: transport file definition path - :param mac_definition: mac file definition path - :param phy_definition: phy file definition path - :param server: remote server node - will run on, default is None for localhost :return: nothing """ nem_element = etree.Element("nem", name=f"{emane_model.name} NEM") if is_external(config): nem_element.set("type", "unstructured") else: - etree.SubElement(nem_element, "transport", definition=transport_definition) - etree.SubElement(nem_element, "mac", definition=mac_definition) - etree.SubElement(nem_element, "phy", definition=phy_definition) - create_file(nem_element, "nem", nem_file, server) + transport_name = transport_file_name(iface) + etree.SubElement(nem_element, "transport", definition=transport_name) + mac_name = mac_file_name(iface) + etree.SubElement(nem_element, "mac", definition=mac_name) + phy_name = phy_file_name(iface) + etree.SubElement(nem_element, "phy", definition=phy_name) + nem_name = nem_file_name(iface) + create_iface_file(iface, nem_element, "nem", nem_name) def create_event_service_xml( @@ -400,14 +344,27 @@ def create_event_service_xml( create_file(event_element, "emaneeventmsgsvc", file_path, server) -def transport_file_name(iface: CoreInterface, transport_type: TransportType) -> str: +def get_transport_type(iface: CoreInterface) -> TransportType: + """ + Get transport type for a given interface. + + :param iface: interface to get transport type for + :return: transport type + """ + transport_type = TransportType.VIRTUAL + if iface.transport_type == TransportType.RAW: + transport_type = TransportType.RAW + return transport_type + + +def transport_file_name(iface: CoreInterface) -> str: """ Create name for a transport xml file. :param iface: interface running emane - :param transport_type: transport type to generate transport file :return: transport xml file name """ + transport_type = get_transport_type(iface) return f"{iface.name}-trans-{transport_type.value}.xml" From 5f676b27bacafcd4893564160fe87c5c37b0fbd9 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 2 Jul 2020 22:15:12 -0700 Subject: [PATCH 0435/1131] tests: removed invalid patch due to emane refactoring --- daemon/tests/conftest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index be62fc03..665f2c1a 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -12,7 +12,6 @@ from mock.mock import MagicMock from core.api.grpc.client import InterfaceHelper from core.api.grpc.server import CoreGrpcServer from core.api.tlv.corehandlers import CoreHandler -from core.emane.emanemanager import EmaneManager from core.emulator.coreemu import CoreEmu from core.emulator.data import IpPrefixes from core.emulator.distributed import DistributedServer @@ -63,7 +62,6 @@ def patcher(request): patch_manager.patch_obj(CoreNode, "nodefile") patch_manager.patch_obj(Session, "write_state") patch_manager.patch_obj(Session, "write_nodes") - patch_manager.patch_obj(EmaneManager, "buildxml") yield patch_manager patch_manager.shutdown() From 2b3e26b7c2af9f724a9a060a06a35a94a7445d6c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 2 Jul 2020 23:19:40 -0700 Subject: [PATCH 0436/1131] daemon: cleanup emane transport service in relation to refactoring, silenced stopdaemons for rj45 nodes --- daemon/core/emane/emanemanager.py | 56 +++++++++++---------------- daemon/core/services/emaneservices.py | 46 +++++++--------------- 2 files changed, 37 insertions(+), 65 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 4e2984a0..808e8020 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -134,31 +134,27 @@ class EmaneManager(ModelManager): :return: net, node, or interface model configuration """ model_name = emane_net.model.name - # use the network-wide config values or interface(NEM)-specific values? - if iface is None: - return self.get_configs(node_id=emane_net.id, config_type=model_name) - else: - # don"t use default values when interface config is the same as net - # note here that using iface.node.id as key allows for only one type - # of each model per node; - # TODO: use both node and interface as key - # Adamson change: first check for iface config keyed by "node:iface.name" - # (so that nodes w/ multiple interfaces of same conftype can have - # different configs for each separate interface) - key = 1000 * iface.node.id - if iface.node_id is not None: - key += iface.node_id - # try retrieve interface specific configuration, avoid getting defaults - config = self.get_configs(node_id=key, config_type=model_name) - # otherwise retrieve the interfaces node configuration, avoid using defaults - if not config: - config = self.get_configs(node_id=iface.node.id, config_type=model_name) - # get non interface config, when none found - if not config: - # with EMANE 0.9.2+, we need an extra NEM XML from - # model.buildnemxmlfiles(), so defaults are returned here - config = self.get_configs(node_id=emane_net.id, config_type=model_name) - return config + # don"t use default values when interface config is the same as net + # note here that using iface.node.id as key allows for only one type + # of each model per node; + # TODO: use both node and interface as key + # Adamson change: first check for iface config keyed by "node:iface.name" + # (so that nodes w/ multiple interfaces of same conftype can have + # different configs for each separate interface) + key = 1000 * iface.node.id + if iface.node_id is not None: + key += iface.node_id + # try retrieve interface specific configuration, avoid getting defaults + config = self.get_configs(node_id=key, config_type=model_name) + # otherwise retrieve the interfaces node configuration, avoid using defaults + if not config: + config = self.get_configs(node_id=iface.node.id, config_type=model_name) + # get non interface config, when none found + if not config: + # with EMANE 0.9.2+, we need an extra NEM XML from + # model.buildnemxmlfiles(), so defaults are returned here + config = self.get_configs(node_id=emane_net.id, config_type=model_name) + return config def config_reset(self, node_id: int = None) -> None: super().config_reset(node_id) @@ -587,26 +583,18 @@ class EmaneManager(ModelManager): """ Kill the appropriate EMANE daemons. """ - # TODO: we may want to improve this if we had the PIDs from the specific EMANE - # daemons that we"ve started kill_emaned = "killall -q emane" - kill_transortd = "killall -q emanetransportd" stop_emane_on_host = False for node in self.getnodes(): if isinstance(node, Rj45Node): stop_emane_on_host = True continue - if node.up: node.cmd(kill_emaned, wait=False) - # TODO: RJ45 node - if stop_emane_on_host: try: - utils.cmd(kill_emaned) - utils.cmd(kill_transortd) + utils.cmd(kill_emaned, wait=False) self.session.distributed.execute(lambda x: x.remote_cmd(kill_emaned)) - self.session.distributed.execute(lambda x: x.remote_cmd(kill_transortd)) except CoreCommandError: logging.exception("error shutting down emane daemons") diff --git a/daemon/core/services/emaneservices.py b/daemon/core/services/emaneservices.py index ef188fab..e734851d 100644 --- a/daemon/core/services/emaneservices.py +++ b/daemon/core/services/emaneservices.py @@ -1,7 +1,6 @@ from typing import Tuple from core.emane.nodes import EmaneNet -from core.errors import CoreError from core.nodes.base import CoreNode from core.services.coreservices import CoreService from core.xml import emanexml @@ -14,37 +13,22 @@ class EmaneTransportService(CoreService): dependencies: Tuple[str, ...] = () dirs: Tuple[str, ...] = () configs: Tuple[str, ...] = ("emanetransport.sh",) - startup: Tuple[str, ...] = ("sh %s" % configs[0],) - validate: Tuple[str, ...] = ("pidof %s" % executables[0],) + startup: Tuple[str, ...] = (f"sh {configs[0]}",) + validate: Tuple[str, ...] = (f"pidof {executables[0]}",) validation_timer: float = 0.5 - shutdown: Tuple[str, ...] = ("killall %s" % executables[0],) + shutdown: Tuple[str, ...] = (f"killall {executables[0]}",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: - if filename == cls.configs[0]: - transport_commands = [] - for iface in node.get_ifaces(): - try: - network_node = node.session.get_node(iface.net.id, EmaneNet) - config = node.session.emane.get_configs( - network_node.id, network_node.model.name - ) - if config and emanexml.is_external(config): - nem_id = network_node.getnemid(iface) - command = ( - "emanetransportd -r -l 0 -d ../transportdaemon%s.xml" - % nem_id - ) - transport_commands.append(command) - except CoreError: - pass - transport_commands = "\n".join(transport_commands) - return """ -emanegentransportxml -o ../ ../platform%s.xml -%s -""" % ( - node.id, - transport_commands, - ) - else: - raise ValueError + emane_manager = node.session.emane + cfg = "" + for iface in node.get_ifaces(): + if not isinstance(iface.net, EmaneNet): + continue + emane_net = iface.net + config = emane_manager.get_iface_config(emane_net, iface) + if emanexml.is_external(config): + nem_id = emane_net.getnemid(iface) + cfg += f"emanegentransportxml {iface.name}-platform.xml\n" + cfg += f"emanetransportd -r -l 0 -d transportdaemon{nem_id}.xml\n" + return cfg From ddcb0205f35a3b3a3ac064a4c79356f7ddf845ee Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 2 Jul 2020 23:32:59 -0700 Subject: [PATCH 0437/1131] daemon: cleaned up emane stopdaemons logic --- daemon/core/emane/emanemanager.py | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 808e8020..ca59ad04 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -567,36 +567,31 @@ class EmaneManager(ModelManager): log_file = os.path.join(node.nodedir, f"{iface.name}-emane.log") platform_xml = os.path.join(node.nodedir, f"{iface.name}-platform.xml") args = f"{emanecmd} -f {log_file} {platform_xml}" - output = node.cmd(args) + node.cmd(args) logging.info("node(%s) emane daemon running: %s", node.name, args) - logging.debug("node(%s) emane daemon output: %s", node.name, output) else: path = self.session.session_dir log_file = os.path.join(path, f"{iface.name}-emane.log") platform_xml = os.path.join(path, f"{iface.name}-platform.xml") emanecmd += f" -f {log_file} {platform_xml}" - utils.cmd(emanecmd, cwd=path) - self.session.distributed.execute(lambda x: x.remote_cmd(emanecmd, cwd=path)) - logging.info("host emane daemon running: %s", emanecmd) + node.host_cmd(emanecmd, cwd=path) + logging.info("node(%s) host emane daemon running: %s", node.name, emanecmd) def stopdaemons(self) -> None: """ Kill the appropriate EMANE daemons. """ kill_emaned = "killall -q emane" - stop_emane_on_host = False - for node in self.getnodes(): - if isinstance(node, Rj45Node): - stop_emane_on_host = True - continue - if node.up: - node.cmd(kill_emaned, wait=False) - if stop_emane_on_host: - try: - utils.cmd(kill_emaned, wait=False) - self.session.distributed.execute(lambda x: x.remote_cmd(kill_emaned)) - except CoreCommandError: - logging.exception("error shutting down emane daemons") + for node_id in sorted(self._emane_nets): + emane_net = self._emane_nets[node_id] + for iface in emane_net.get_ifaces(): + node = iface.node + if not node.up: + continue + if isinstance(node, Rj45Node): + node.host_cmd(kill_emaned, wait=False) + else: + node.cmd(kill_emaned, wait=False) def install_iface(self, emane_net: EmaneNet, iface: CoreInterface) -> None: config = self.get_iface_config(emane_net, iface) From ac1c27b1c8b74da40c79ed9e95765191c0e71605 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 3 Jul 2020 08:51:17 -0700 Subject: [PATCH 0438/1131] daemon: fixed issues when emane generated platform.xml for raw interfaces --- daemon/core/emane/emanemanager.py | 23 +++++++++++++---------- daemon/core/xml/emanexml.py | 22 ++++++++-------------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index ca59ad04..476010cb 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -31,7 +31,6 @@ from core.emulator.enumerations import ( from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNode, NodeBase from core.nodes.interface import CoreInterface, TunTap -from core.nodes.physical import Rj45Node from core.xml import emanexml if TYPE_CHECKING: @@ -531,19 +530,21 @@ class EmaneManager(ModelManager): cfgloglevel = self.session.options.get_config_int("emane_log_level") realtime = self.session.options.get_config_bool("emane_realtime", default=True) if cfgloglevel: - logging.info("setting user-defined EMANE log level: %d", cfgloglevel) + logging.info("setting user-defined emane log level: %d", cfgloglevel) loglevel = str(cfgloglevel) emanecmd = f"emane -d -l {loglevel}" if realtime: emanecmd += " -r" - otagroup, _otaport = self.get_config("otamanagergroup").split(":") - otadev = self.get_config("otamanagerdevice") - otanetidx = self.session.get_control_net_index(otadev) - eventgroup, _eventport = self.get_config("eventservicegroup").split(":") - eventdev = self.get_config("eventservicedevice") - eventservicenetidx = self.session.get_control_net_index(eventdev) node = iface.node - if not isinstance(node, Rj45Node): + transport_type = emanexml.get_transport_type(iface) + if not transport_type == TransportType.RAW: + otagroup, _otaport = self.get_config("otamanagergroup").split(":") + otadev = self.get_config("otamanagerdevice") + otanetidx = self.session.get_control_net_index(otadev) + eventgroup, _eventport = self.get_config("eventservicegroup").split(":") + eventdev = self.get_config("eventservicedevice") + eventservicenetidx = self.session.get_control_net_index(eventdev) + # control network not yet started here self.session.add_remove_control_iface( node, 0, remove=False, conf_required=False @@ -559,6 +560,7 @@ class EmaneManager(ModelManager): node, eventservicenetidx, remove=False, conf_required=False ) # multicast route is needed for OTA data + logging.info("OTA GROUP(%s) OTA DEV(%s)", otagroup, otadev) node.node_net_client.create_route(otagroup, otadev) # multicast route is also needed for event data if on control network if eventservicenetidx >= 0 and eventgroup != otagroup: @@ -588,7 +590,8 @@ class EmaneManager(ModelManager): node = iface.node if not node.up: continue - if isinstance(node, Rj45Node): + transport_type = emanexml.get_transport_type(iface) + if transport_type == TransportType.RAW: node.host_cmd(kill_emaned, wait=False) else: node.cmd(kill_emaned, wait=False) diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index cb605b21..cf973f34 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -13,7 +13,6 @@ from core.emulator.enumerations import TransportType from core.errors import CoreError from core.nodes.interface import CoreInterface from core.nodes.network import CtrlNet -from core.nodes.physical import Rj45Node from core.xml import corexml if TYPE_CHECKING: @@ -93,7 +92,8 @@ def create_iface_file( :return: """ node = iface.node - if isinstance(node, Rj45Node): + transport_type = get_transport_type(iface) + if transport_type == TransportType.RAW: file_path = os.path.join(node.session.session_dir, file_name) else: file_path = os.path.join(node.nodedir, file_name) @@ -185,21 +185,15 @@ def build_platform_xml( ) add_param(transport_element, "device", iface.name) - # determine platform element to add xml to - if iface.transport_type == TransportType.RAW: - otadev = control_net.brname - eventdev = control_net.brname - else: - otadev = None - eventdev = None + transport_type = get_transport_type(iface) + transport_configs = {"otamanagerdevice", "eventservicedevice"} platform_element = etree.Element("platform") - if otadev: - emane_manager.set_config("otamanagerdevice", otadev) - if eventdev: - emane_manager.set_config("eventservicedevice", eventdev) for configuration in emane_manager.emane_config.emulator_config: name = configuration.id - value = emane_manager.get_config(name) + if transport_type == TransportType.RAW and name in transport_configs: + value = control_net.brname + else: + value = emane_manager.get_config(name) add_param(platform_element, name, value) platform_element.append(nem_element) emane_net.setnemid(iface, nem_id) From fcda1f9f14432aab23c5bf937964b3054f5eca7b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 3 Jul 2020 09:08:36 -0700 Subject: [PATCH 0439/1131] daemon: CoreInterface now defaults to a virtual transport type, added utility methods to check if an interface is virtual/raw, cleaned up all emane code using these types of checks --- daemon/core/emane/emanemanager.py | 9 +++------ daemon/core/nodes/interface.py | 19 +++++++++++++++++-- daemon/core/xml/emanexml.py | 31 ++++++------------------------- 3 files changed, 26 insertions(+), 33 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 476010cb..15faedcc 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -26,7 +26,6 @@ from core.emulator.enumerations import ( LinkTypes, MessageFlags, RegisterTlvs, - TransportType, ) from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNode, NodeBase @@ -536,8 +535,7 @@ class EmaneManager(ModelManager): if realtime: emanecmd += " -r" node = iface.node - transport_type = emanexml.get_transport_type(iface) - if not transport_type == TransportType.RAW: + if iface.is_virtual(): otagroup, _otaport = self.get_config("otamanagergroup").split(":") otadev = self.get_config("otamanagerdevice") otanetidx = self.session.get_control_net_index(otadev) @@ -590,8 +588,7 @@ class EmaneManager(ModelManager): node = iface.node if not node.up: continue - transport_type = emanexml.get_transport_type(iface) - if transport_type == TransportType.RAW: + if iface.is_raw(): node.host_cmd(kill_emaned, wait=False) else: node.cmd(kill_emaned, wait=False) @@ -614,7 +611,7 @@ class EmaneManager(ModelManager): for key in sorted(self._emane_nets): emane_net = self._emane_nets[key] for iface in emane_net.get_ifaces(): - if iface.transport_type == TransportType.VIRTUAL: + if iface.is_virtual(): iface.shutdown() iface.poshook = None diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index e4d4d0ac..7f33973e 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -60,7 +60,7 @@ class CoreInterface: # placeholder position hook self.poshook: Callable[[CoreInterface], None] = lambda x: None # used with EMANE - self.transport_type: Optional[TransportType] = None + self.transport_type: TransportType = TransportType.VIRTUAL # id of interface for node self.node_id: Optional[int] = None # id of interface for network @@ -310,6 +310,22 @@ class CoreInterface: """ return id(self) < id(other) + def is_raw(self) -> bool: + """ + Used to determine if this interface is considered a raw interface. + + :return: True if raw interface, False otherwise + """ + return self.transport_type == TransportType.RAW + + def is_virtual(self) -> bool: + """ + Used to determine if this interface is considered a virtual interface. + + :return: True if virtual interface, False otherwise + """ + return self.transport_type == TransportType.VIRTUAL + class Veth(CoreInterface): """ @@ -404,7 +420,6 @@ class TunTap(CoreInterface): :param start: start flag """ super().__init__(session, node, name, localname, mtu, server) - self.transport_type = TransportType.VIRTUAL if start: self.startup() diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index cf973f34..0ef13a80 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -9,7 +9,6 @@ from core import utils from core.config import Configuration from core.emane.nodes import EmaneNet from core.emulator.distributed import DistributedServer -from core.emulator.enumerations import TransportType from core.errors import CoreError from core.nodes.interface import CoreInterface from core.nodes.network import CtrlNet @@ -92,8 +91,7 @@ def create_iface_file( :return: """ node = iface.node - transport_type = get_transport_type(iface) - if transport_type == TransportType.RAW: + if iface.is_raw(): file_path = os.path.join(node.session.session_dir, file_name) else: file_path = os.path.join(node.nodedir, file_name) @@ -185,12 +183,11 @@ def build_platform_xml( ) add_param(transport_element, "device", iface.name) - transport_type = get_transport_type(iface) transport_configs = {"otamanagerdevice", "eventservicedevice"} platform_element = etree.Element("platform") for configuration in emane_manager.emane_config.emulator_config: name = configuration.id - if transport_type == TransportType.RAW and name in transport_configs: + if iface.is_raw() and name in transport_configs: value = control_net.brname else: value = emane_manager.get_config(name) @@ -214,7 +211,7 @@ def create_transport_xml(iface: CoreInterface, config: Dict[str, str]) -> None: :param config: all current configuration values :return: nothing """ - transport_type = get_transport_type(iface) + transport_type = iface.transport_type transport_element = etree.Element( "transport", name=f"{transport_type.value.capitalize()} Transport", @@ -224,7 +221,7 @@ def create_transport_xml(iface: CoreInterface, config: Dict[str, str]) -> None: # get emane model cnfiguration flowcontrol = config.get("flowcontrolenable", "0") == "1" - if transport_type == TransportType.VIRTUAL: + if iface.is_virtual(): device_path = "/dev/net/tun_flowctl" if not os.path.exists(device_path): device_path = "/dev/net/tun" @@ -338,19 +335,6 @@ def create_event_service_xml( create_file(event_element, "emaneeventmsgsvc", file_path, server) -def get_transport_type(iface: CoreInterface) -> TransportType: - """ - Get transport type for a given interface. - - :param iface: interface to get transport type for - :return: transport type - """ - transport_type = TransportType.VIRTUAL - if iface.transport_type == TransportType.RAW: - transport_type = TransportType.RAW - return transport_type - - def transport_file_name(iface: CoreInterface) -> str: """ Create name for a transport xml file. @@ -358,8 +342,7 @@ def transport_file_name(iface: CoreInterface) -> str: :param iface: interface running emane :return: transport xml file name """ - transport_type = get_transport_type(iface) - return f"{iface.name}-trans-{transport_type.value}.xml" + return f"{iface.name}-trans-{iface.transport_type.value}.xml" def nem_file_name(iface: CoreInterface) -> str: @@ -369,9 +352,7 @@ def nem_file_name(iface: CoreInterface) -> str: :param iface: interface running emane :return: nem xm file name """ - append = "" - if iface and iface.transport_type == TransportType.RAW: - append = "-raw" + append = "-raw" if iface.is_raw() else "" return f"{iface.name}-nem{append}.xml" From 5cc4d92760137796e9b66f45f0705a67ebed8869 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 5 Jul 2020 21:29:03 -0700 Subject: [PATCH 0440/1131] daemon: removed nem map from individual emane networks, all nems are stored and generated from the emane manager --- daemon/core/api/grpc/grpcutils.py | 7 +++- daemon/core/api/grpc/server.py | 22 +++++----- daemon/core/emane/commeffect.py | 10 ++--- daemon/core/emane/emanemanager.py | 59 ++++++++++++++------------- daemon/core/emane/nodes.py | 41 ++++--------------- daemon/core/services/emaneservices.py | 2 +- daemon/core/xml/corexml.py | 4 +- daemon/core/xml/corexmldeployment.py | 7 ++-- daemon/core/xml/emanexml.py | 1 - 9 files changed, 64 insertions(+), 89 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index bd9e808d..bd3519f7 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -491,10 +491,13 @@ def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface: ) -def get_nem_id(node: CoreNode, iface_id: int, context: ServicerContext) -> int: +def get_nem_id( + session: Session, node: CoreNode, iface_id: int, context: ServicerContext +) -> int: """ Get nem id for a given node and interface id. + :param session: session node belongs to :param node: node to get nem id for :param iface_id: id of interface on node to get nem id for :param context: request context @@ -508,7 +511,7 @@ def get_nem_id(node: CoreNode, iface_id: int, context: ServicerContext) -> int: if not isinstance(net, EmaneNet): message = f"{node.name} interface {iface_id} is not an EMANE network" context.abort(grpc.StatusCode.INVALID_ARGUMENT, message) - nem_id = net.getnemid(iface) + nem_id = session.emane.get_nem_id(iface) if nem_id is None: message = f"{node.name} interface {iface_id} nem id does not exist" context.abort(grpc.StatusCode.INVALID_ARGUMENT, message) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index aa5ec539..5bdebac6 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -1551,29 +1551,29 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("emane link: %s", request) session = self.get_session(request.session_id, context) nem1 = request.nem1 - emane1, iface = session.emane.nemlookup(nem1) - if not emane1 or not iface: + iface1 = session.emane.get_iface(nem1) + if not iface1: context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem1} not found") - node1 = iface.node + node1 = iface1.node nem2 = request.nem2 - emane2, iface = session.emane.nemlookup(nem2) - if not emane2 or not iface: + iface2 = session.emane.get_iface(nem2) + if not iface2: context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem2} not found") - node2 = iface.node + node2 = iface2.node - if emane1.id == emane2.id: + if iface1.net == iface2.net: if request.linked: flag = MessageFlags.ADD else: flag = MessageFlags.DELETE - color = session.get_link_color(emane1.id) + color = session.get_link_color(iface1.net.id) link = LinkData( message_type=flag, type=LinkTypes.WIRELESS, node1_id=node1.id, node2_id=node2.id, - network_id=emane1.id, + network_id=iface1.net.id, color=color, ) session.broadcast_link(link) @@ -1796,8 +1796,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): for request in request_iterator: session = self.get_session(request.session_id, context) node1 = self.get_node(session, request.node1_id, context, CoreNode) - nem1 = grpcutils.get_nem_id(node1, request.iface1_id, context) + nem1 = grpcutils.get_nem_id(session, node1, request.iface1_id, context) node2 = self.get_node(session, request.node2_id, context, CoreNode) - nem2 = grpcutils.get_nem_id(node2, request.iface2_id, context) + nem2 = grpcutils.get_nem_id(session, node2, request.iface2_id, context) session.emane.publish_pathloss(nem1, nem2, request.rx1, request.rx2) return EmanePathlossesResponse() diff --git a/daemon/core/emane/commeffect.py b/daemon/core/emane/commeffect.py index a812b66d..0fa70a92 100644 --- a/daemon/core/emane/commeffect.py +++ b/daemon/core/emane/commeffect.py @@ -10,7 +10,6 @@ from lxml import etree from core.config import ConfigGroup, Configuration from core.emane import emanemanifest, emanemodel -from core.emane.nodes import EmaneNet from core.emulator.data import LinkOptions from core.nodes.interface import CoreInterface from core.xml import emanexml @@ -124,12 +123,11 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): # TODO: batch these into multiple events per transmission # TODO: may want to split out seconds portion of delay and jitter event = CommEffectEvent() - emane_node = self.session.get_node(self.id, EmaneNet) - nemid = emane_node.getnemid(iface) - nemid2 = emane_node.getnemid(iface2) + nem1 = self.session.emane.get_nem_id(iface) + nem2 = self.session.emane.get_nem_id(iface2) logging.info("sending comm effect event") event.append( - nemid, + nem1, latency=convert_none(options.delay), jitter=convert_none(options.jitter), loss=convert_none(options.loss), @@ -137,4 +135,4 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): unicast=int(convert_none(options.bandwidth)), broadcast=int(convert_none(options.bandwidth)), ) - service.publish(nemid2, event) + service.publish(nem2, event) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 15faedcc..2a7f9844 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -90,7 +90,8 @@ class EmaneManager(ModelManager): """ super().__init__() self.session: "Session" = session - self.nems: Dict[int, CoreInterface] = {} + self.nems_to_ifaces: Dict[int, CoreInterface] = {} + self.ifaces_to_nems: Dict[CoreInterface, int] = {} self._emane_nets: Dict[int, EmaneNet] = {} self._emane_node_lock: threading.Lock = threading.Lock() # port numbers are allocated from these counters @@ -117,7 +118,7 @@ class EmaneManager(ModelManager): def next_nem_id(self) -> int: nem_id = int(self.get_config("nem_id_start")) - while nem_id in self.nems: + while nem_id in self.nems_to_ifaces: nem_id += 1 return nem_id @@ -363,7 +364,7 @@ class EmaneManager(ModelManager): 0, remove=False, conf_required=False ) nem_id = self.next_nem_id() - self.nems[nem_id] = iface + self.set_nem(nem_id, iface) self.write_nem(iface, nem_id) emanexml.build_platform_xml(self, control_net, emane_net, iface, nem_id) config = self.get_iface_config(emane_net, iface) @@ -371,6 +372,18 @@ class EmaneManager(ModelManager): self.start_daemon(iface) self.install_iface(emane_net, iface) + def set_nem(self, nem_id: int, iface: CoreInterface) -> None: + if nem_id in self.nems_to_ifaces: + raise CoreError(f"adding duplicate nem: {nem_id}") + self.nems_to_ifaces[nem_id] = iface + self.ifaces_to_nems[iface] = nem_id + + def get_iface(self, nem_id: int) -> Optional[CoreInterface]: + return self.nems_to_ifaces.get(nem_id) + + def get_nem_id(self, iface: CoreInterface) -> Optional[int]: + return self.ifaces_to_nems.get(iface) + def write_nem(self, iface: CoreInterface, nem_id: int) -> None: path = os.path.join(self.session.session_dir, "emane_nems") try: @@ -405,7 +418,8 @@ class EmaneManager(ModelManager): """ with self._emane_node_lock: self._emane_nets.clear() - self.nems.clear() + self.nems_to_ifaces.clear() + self.ifaces_to_nems.clear() def shutdown(self) -> None: """ @@ -448,42 +462,29 @@ class EmaneManager(ModelManager): model_class = self.models[model_name] emane_net.setmodel(model_class, config) - def nemlookup(self, nemid) -> Tuple[Optional[EmaneNet], Optional[CoreInterface]]: - """ - Look for the given numerical NEM ID and return the first matching - EMANE network and NEM interface. - """ - emane_node = None - iface = None - for node_id in self._emane_nets: - emane_node = self._emane_nets[node_id] - iface = emane_node.get_nem_iface(nemid) - if iface is not None: - break - else: - emane_node = None - return emane_node, iface - def get_nem_link( self, nem1: int, nem2: int, flags: MessageFlags = MessageFlags.NONE ) -> Optional[LinkData]: - emane1, iface = self.nemlookup(nem1) - if not emane1 or not iface: + iface1 = self.get_iface(nem1) + if not iface1: logging.error("invalid nem: %s", nem1) return None - node1 = iface.node - emane2, iface = self.nemlookup(nem2) - if not emane2 or not iface: + node1 = iface1.node + iface2 = self.get_iface(nem2) + if not iface2: logging.error("invalid nem: %s", nem2) return None - node2 = iface.node - color = self.session.get_link_color(emane1.id) + node2 = iface2.node + if iface1.net != iface2.net: + return None + emane_net = iface1.net + color = self.session.get_link_color(emane_net.id) return LinkData( message_type=flags, type=LinkTypes.WIRELESS, node1_id=node1.id, node2_id=node2.id, - network_id=emane1.id, + network_id=emane_net.id, color=color, ) @@ -728,7 +729,7 @@ class EmaneManager(ModelManager): Returns True if successfully parsed and a Node Message was sent. """ # convert nemid to node number - _emanenode, iface = self.nemlookup(nemid) + iface = self.get_iface(nemid) if iface is None: logging.info("location event for unknown NEM %s", nemid) return False diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 7e8a0a4f..cfb3342e 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -52,7 +52,6 @@ class EmaneNet(CoreNetworkBase): ) -> None: super().__init__(session, _id, name, server) self.conf: str = "" - self.nemidmap: Dict[CoreInterface, int] = {} self.model: "OptionalEmaneModel" = None self.mobility: Optional[WayPointMobility] = None @@ -105,32 +104,6 @@ class EmaneNet(CoreNetworkBase): self.mobility = model(session=self.session, _id=self.id) self.mobility.update_config(config) - def setnemid(self, iface: CoreInterface, nemid: int) -> None: - """ - Record an interface to numerical ID mapping. The Emane controller - object manages and assigns these IDs for all NEMs. - """ - self.nemidmap[iface] = nemid - - def getnemid(self, iface: CoreInterface) -> Optional[int]: - """ - Given an interface, return its numerical ID. - """ - if iface not in self.nemidmap: - return None - else: - return self.nemidmap[iface] - - def get_nem_iface(self, nemid: int) -> Optional[CoreInterface]: - """ - Given a numerical NEM ID, return its interface. This returns the - first interface that matches the given NEM ID. - """ - for iface in self.nemidmap: - if self.nemidmap[iface] == nemid: - return iface - return None - def _nem_position( self, iface: CoreInterface ) -> Optional[Tuple[int, float, float, float]]: @@ -140,9 +113,9 @@ class EmaneNet(CoreNetworkBase): :param iface: interface to get nem emane position for :return: nem position tuple, None otherwise """ - nemid = self.getnemid(iface) + nem_id = self.session.emane.get_nem_id(iface) ifname = iface.localname - if nemid is None: + if nem_id is None: logging.info("nemid for %s is unknown", ifname) return node = iface.node @@ -153,7 +126,7 @@ class EmaneNet(CoreNetworkBase): node.position.set_geo(lon, lat, alt) # altitude must be an integer or warning is printed alt = int(round(alt)) - return nemid, lon, lat, alt + return nem_id, lon, lat, alt def setnemposition(self, iface: CoreInterface) -> None: """ @@ -164,7 +137,6 @@ class EmaneNet(CoreNetworkBase): if self.session.emane.service is None: logging.info("position service not available") return - position = self._nem_position(iface) if position: nemid, lon, lat, alt = position @@ -195,9 +167,12 @@ class EmaneNet(CoreNetworkBase): def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: links = super().links(flags) - # gather current emane links - nem_ids = set(self.nemidmap.values()) emane_manager = self.session.emane + # gather current emane links + nem_ids = set() + for iface in self.get_ifaces(): + nem_id = emane_manager.get_nem_id(iface) + nem_ids.add(nem_id) emane_links = emane_manager.link_monitor.links considered = set() for link_key in emane_links: diff --git a/daemon/core/services/emaneservices.py b/daemon/core/services/emaneservices.py index e734851d..d694317a 100644 --- a/daemon/core/services/emaneservices.py +++ b/daemon/core/services/emaneservices.py @@ -28,7 +28,7 @@ class EmaneTransportService(CoreService): emane_net = iface.net config = emane_manager.get_iface_config(emane_net, iface) if emanexml.is_external(config): - nem_id = emane_net.getnemid(iface) + nem_id = emane_manager.get_nem_id(iface) cfg += f"emanegentransportxml {iface.name}-platform.xml\n" cfg += f"emanetransportd -r -l 0 -d transportdaemon{nem_id}.xml\n" return cfg diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 340d81d0..ffd07ebd 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -501,8 +501,8 @@ class CoreXmlWriter: iface = node.get_iface(iface_data.id) # check if emane interface if isinstance(iface.net, EmaneNet): - nem = iface.net.getnemid(iface) - add_attribute(iface_element, "nem", nem) + nem_id = self.session.emane.get_nem_id(iface) + add_attribute(iface_element, "nem", nem_id) add_attribute(iface_element, "id", iface_data.id) add_attribute(iface_element, "name", iface_data.name) add_attribute(iface_element, "mac", iface_data.mac) diff --git a/daemon/core/xml/corexmldeployment.py b/daemon/core/xml/corexmldeployment.py index 51201787..c062a1d2 100644 --- a/daemon/core/xml/corexmldeployment.py +++ b/daemon/core/xml/corexmldeployment.py @@ -9,7 +9,6 @@ from core import utils from core.emane.nodes import EmaneNet from core.executables import IP from core.nodes.base import CoreNodeBase, NodeBase -from core.nodes.interface import CoreInterface if TYPE_CHECKING: from core.emulator.session import Session @@ -38,11 +37,10 @@ def add_mapping(parent_element: etree.Element, maptype: str, mapref: str) -> Non def add_emane_iface( host_element: etree.Element, - iface: CoreInterface, + nem_id: int, platform_name: str = "p1", transport_name: str = "t1", ) -> etree.Element: - nem_id = iface.net.nemidmap[iface] host_id = host_element.get("id") # platform data @@ -158,7 +156,8 @@ class CoreXmlDeployment: for iface in node.get_ifaces(): emane_element = None if isinstance(iface.net, EmaneNet): - emane_element = add_emane_iface(host_element, iface) + nem_id = self.session.emane.get_nem_id(iface) + emane_element = add_emane_iface(host_element, nem_id) parent_element = host_element if emane_element is not None: diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index 0ef13a80..88aeaa97 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -193,7 +193,6 @@ def build_platform_xml( value = emane_manager.get_config(name) add_param(platform_element, name, value) platform_element.append(nem_element) - emane_net.setnemid(iface, nem_id) mac = _MAC_PREFIX + ":00:00:" mac += f"{(nem_id >> 8) & 0xFF:02X}:{nem_id & 0xFF:02X}" iface.set_mac(mac) From b3a4b1cb10a47f8a5b7de36e1135096468cf8d61 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 5 Jul 2020 21:56:22 -0700 Subject: [PATCH 0441/1131] daemon: updates to support running emane on the fly for a newly connected link --- daemon/core/emane/emanemanager.py | 4 ++-- daemon/core/emane/nodes.py | 10 +++++++++- daemon/core/emulator/session.py | 4 +++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 2a7f9844..3765ba44 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -252,8 +252,8 @@ class EmaneManager(ModelManager): """ with self._emane_node_lock: if emane_net.id in self._emane_nets: - raise KeyError( - f"non-unique EMANE object id {emane_net.id} for {emane_net}" + raise CoreError( + f"duplicate emane network({emane_net.id}): {emane_net.name}" ) self._emane_nets[emane_net.id] = emane_net diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index cfb3342e..5791f46a 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -8,7 +8,13 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type from core.emulator.data import InterfaceData, LinkData, LinkOptions from core.emulator.distributed import DistributedServer -from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes, RegisterTlvs +from core.emulator.enumerations import ( + EventTypes, + LinkTypes, + MessageFlags, + NodeTypes, + RegisterTlvs, +) from core.errors import CoreError from core.nodes.base import CoreNetworkBase, CoreNode from core.nodes.interface import CoreInterface @@ -203,4 +209,6 @@ class EmaneNet(CoreNetworkBase): iface.set_mac(iface_data.mac) for ip in iface_data.get_ips(): iface.add_ip(ip) + if self.session.state == EventTypes.RUNTIME_STATE: + self.session.emane.start_iface(self, iface) return iface diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 9f5364b9..cad6ae3c 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -531,7 +531,7 @@ class Session: self.set_node_position(node, options) # add services to needed nodes - if isinstance(node, (CoreNode, PhysicalNode, DockerNode, LxcNode)): + if isinstance(node, (CoreNode, PhysicalNode)): node.type = options.model logging.debug("set node type: %s", node.type) self.services.add_services(node, node.type, options.services) @@ -545,6 +545,8 @@ class Session: # ensure default emane configuration if isinstance(node, EmaneNet) and options.emane: self.emane.set_model_config(_id, options.emane) + if self.state == EventTypes.RUNTIME_STATE: + self.emane.add_node(node) # set default wlan config if needed if isinstance(node, WlanNode): self.mobility.set_model_config(_id, BasicRangeModel.name) From 8dc570a98d7843337ace6d7ef22a37c7f27a309f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 6 Jul 2020 21:13:54 -0700 Subject: [PATCH 0442/1131] daemon: removed commented out code --- daemon/core/nodes/base.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 039008ef..7f444480 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -815,17 +815,6 @@ class CoreNode(CoreNodeBase): with self.lock: if net.has_custom_iface: return net.custom_iface(self, iface_data) - # if net.is_emane is True: - # iface_id = self.newtuntap(iface_data.id, iface_data.name) - # # TUN/TAP is not ready for addressing yet; the device may - # # take some time to appear, and installing it into a - # # namespace after it has been bound removes addressing; - # # save addresses with the interface now - # self.attachnet(iface_id, net) - # iface = self.get_iface(iface_id) - # iface.set_mac(iface_data.mac) - # for ip in ips: - # iface.add_ip(ip) else: iface_id = self.newveth(iface_data.id, iface_data.name) self.attachnet(iface_id, net) From 6f7e42d310164bbe1bef9b98de3f48f8355df8b3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 6 Jul 2020 21:32:43 -0700 Subject: [PATCH 0443/1131] daemon: avoid command error logging when checking for emane version as validation for checking if emane is installed --- daemon/core/emane/emanemanager.py | 33 ++++++++++++++++--------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 3765ba44..ec39137d 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -165,23 +165,24 @@ class EmaneManager(ModelManager): :return: nothing """ - try: - # check for emane - args = "emane --version" - emane_version = utils.cmd(args) - logging.info("using EMANE: %s", emane_version) - self.session.distributed.execute(lambda x: x.remote_cmd(args)) - - # load default emane models - self.load_models(EMANE_MODELS) - - # load custom models - custom_models_path = self.session.options.get_config("emane_models_dir") - if custom_models_path: - emane_models = utils.load_classes(custom_models_path, EmaneModel) - self.load_models(emane_models) - except CoreCommandError: + # check for emane + path = utils.which("emane", required=False) + if not path: logging.info("emane is not installed") + return + + # get version + emane_version = utils.cmd("emane --version") + logging.info("using emane: %s", emane_version) + + # load default emane models + self.load_models(EMANE_MODELS) + + # load custom models + custom_models_path = self.session.options.get_config("emane_models_dir") + if custom_models_path: + emane_models = utils.load_classes(custom_models_path, EmaneModel) + self.load_models(emane_models) def deleteeventservice(self) -> None: if self.service: From 0045c8d79c53c89f5079f64fe8b536c7fa38f43c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 6 Jul 2020 21:37:04 -0700 Subject: [PATCH 0444/1131] pygui: avoid trying to bring up a terminal for rj45 nodes --- daemon/core/gui/graph/node.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 6e8185b8..dfe724bd 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -193,7 +193,8 @@ class CanvasNode: def double_click(self, event: tk.Event) -> None: if self.app.core.is_runtime(): - self.canvas.core.launch_terminal(self.core_node.id) + if NodeUtils.is_container_node(self.core_node.type): + self.canvas.core.launch_terminal(self.core_node.id) else: self.show_config() From c761c55ebc5d7f89116cbfe81031953394657b85 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 6 Jul 2020 21:47:46 -0700 Subject: [PATCH 0445/1131] tests: patch utils.which --- daemon/tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index 665f2c1a..0e25dee9 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -55,6 +55,7 @@ def patcher(request): if request.config.getoption("mock"): patch_manager.patch("os.mkdir") patch_manager.patch("core.utils.cmd") + patch_manager.patch("core.utils.which") patch_manager.patch("core.nodes.netclient.get_net_client") patch_manager.patch_obj( LinuxNetClient, "get_mac", return_value="00:00:00:00:00:00" From 6648dc7825279bf59ec857725545aca9ed9809e0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 7 Jul 2020 08:46:47 -0700 Subject: [PATCH 0446/1131] pygui: service and config service dialogs will now properly show services for default group selected --- daemon/core/gui/dialogs/nodeconfigservice.py | 2 +- daemon/core/gui/dialogs/nodeservice.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeconfigservice.py index b5250eba..b9a9a1f5 100644 --- a/daemon/core/gui/dialogs/nodeconfigservice.py +++ b/daemon/core/gui/dialogs/nodeconfigservice.py @@ -84,7 +84,7 @@ class NodeConfigServiceDialog(Dialog): button.grid(row=0, column=3, sticky="ew") # trigger group change - self.groups.listbox.event_generate("<>") + self.handle_group_change() def handle_group_change(self, event: tk.Event = None) -> None: selection = self.groups.listbox.curselection() diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index f6f5e5cf..6fcc2912 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -82,7 +82,7 @@ class NodeServiceDialog(Dialog): button.grid(row=0, column=3, sticky="ew") # trigger group change - self.groups.listbox.event_generate("<>") + self.handle_group_change() def handle_group_change(self, event: tk.Event = None) -> None: selection = self.groups.listbox.curselection() From f1ff1a65770dc446e6cd75f113ced9eacd93d3ea Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 7 Jul 2020 14:24:43 -0700 Subject: [PATCH 0447/1131] pygui: only attempt to run observer commands on container nodes --- daemon/core/gui/graph/node.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index dfe724bd..f765816d 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -179,7 +179,10 @@ class CanvasNode: self.app.core.edit_node(self.core_node) def on_enter(self, event: tk.Event) -> None: - if self.app.core.is_runtime() and self.app.core.observer: + is_runtime = self.app.core.is_runtime() + has_observer = self.app.core.observer is not None + is_container = NodeUtils.is_container_node(self.core_node.type) + if is_runtime and has_observer and is_container: self.tooltip.text.set("waiting...") self.tooltip.on_enter(event) try: From bb4514b93e315c9b5682ee893679b9bb1b764a4c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 7 Jul 2020 15:16:17 -0700 Subject: [PATCH 0448/1131] daemon: changes to saving and restoring server used for nodes in xml --- daemon/core/xml/corexml.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index ffd07ebd..d1c43d9b 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -128,6 +128,8 @@ class NodeElement: self.element: etree.Element = etree.Element(element_name) add_attribute(self.element, "id", node.id) add_attribute(self.element, "name", node.name) + server = self.node.server.name if self.node.server else None + add_attribute(self.element, "server", server) add_attribute(self.element, "icon", node.icon) add_attribute(self.element, "canvas", node.canvas) self.add_position() @@ -801,8 +803,10 @@ class CoreXmlReader: icon = device_element.get("icon") clazz = device_element.get("class") image = device_element.get("image") - options = NodeOptions(name=name, model=model, image=image, icon=icon) - + server = device_element.get("server") + options = NodeOptions( + name=name, model=model, image=image, icon=icon, server=server + ) node_type = NodeTypes.DEFAULT if clazz == "docker": node_type = NodeTypes.DOCKER @@ -842,7 +846,8 @@ class CoreXmlReader: node_type = NodeTypes[network_element.get("type")] _class = self.session.get_node_class(node_type) icon = network_element.get("icon") - options = NodeOptions(name=name, icon=icon) + server = network_element.get("server") + options = NodeOptions(name=name, icon=icon, server=server) position_element = network_element.find("position") if position_element is not None: From fb21909dadaed688ae027313cea34bfe065772f5 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 7 Jul 2020 23:38:12 -0700 Subject: [PATCH 0449/1131] invoke/poetry: updated version in toml file and added invoke commands --- daemon/pyproject.toml | 2 +- tasks.py | 52 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml index 6df5f10e..609fcb08 100644 --- a/daemon/pyproject.toml +++ b/daemon/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "core" -version = "6.4.0" +version = "6.6.0" description = "" authors = [] diff --git a/tasks.py b/tasks.py index 74e6af76..b19e8925 100644 --- a/tasks.py +++ b/tasks.py @@ -2,13 +2,51 @@ from invoke import task @task -def core(c): - c.run( - "poetry run sudo python3 scripts/core-daemon " - "-f data/core.conf -l data/logging.conf" - ) +def daemon(c): + """ + Runs core-daemon. + """ + with c.cd("daemon"): + poetry = c.run("which poetry").stdout.strip() + c.run( + f"sudo {poetry} run scripts/core-daemon " + "-f data/core.conf -l data/logging.conf" + ) @task -def core_pygui(c): - c.run("poetry run python3 scripts/core-pygui") +def gui(c): + """ + Run core-pygui. + """ + with c.cd("daemon"): + c.run("poetry run scripts/core-pygui") + + +@task +def test(c): + """ + Run core tests. + """ + with c.cd("daemon"): + poetry = c.run("which poetry").stdout.strip() + c.run(f"sudo {poetry} run pytest -v --lf -x tests", pty=True) + + +@task +def test_mock(c): + """ + Run core tests using mock to avoid running as sudo. + """ + with c.cd("daemon"): + c.run("poetry run pytest -v --mock --lf -x tests", pty=True) + + +@task +def test_emane(c): + """ + Run core emane tests. + """ + with c.cd("daemon"): + poetry = c.run("which poetry").stdout.strip() + c.run(f"sudo {poetry} run pytest -v --lf -x tests/emane", pty=True) From 43b586a1a1d65d93dd028cae3de044899d56cf76 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 8 Jul 2020 08:24:23 -0700 Subject: [PATCH 0450/1131] daemon: updated xml to write and read session configured distributed servers, updated pygui to send servers before session start or saving xml --- daemon/core/gui/coreclient.py | 6 ++++++ daemon/core/xml/corexml.py | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 7cf8b123..9479cbcb 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -510,6 +510,10 @@ class CoreClient: except grpc.RpcError as e: self.app.show_grpc_exception("Edit Node Error", e) + def send_servers(self) -> None: + for server in self.servers.values(): + self.client.add_session_server(self.session_id, server.name, server.address) + def start_session(self) -> StartSessionResponse: self.ifaces_manager.reset_mac() nodes = [x.core_node for x in self.canvas_nodes.values()] @@ -538,6 +542,7 @@ class CoreClient: emane_config = None response = StartSessionResponse(result=False) try: + self.send_servers() response = self.client.start_session( self.session_id, nodes, @@ -749,6 +754,7 @@ class CoreClient: """ Send to daemon all session info, but don't start the session """ + self.send_servers() self.create_nodes_and_links() for config_proto in self.get_wlan_configs_proto(): self.client.set_wlan_config( diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index d1c43d9b..7e3b35a2 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -284,6 +284,7 @@ class CoreXmlWriter: self.write_service_configs() self.write_configservice_configs() self.write_session_origin() + self.write_servers() self.write_session_hooks() self.write_session_options() self.write_session_metadata() @@ -318,6 +319,15 @@ class CoreXmlWriter: add_attribute(origin, "y", y) add_attribute(origin, "z", z) + def write_servers(self) -> None: + servers = etree.Element("servers") + for server in self.session.distributed.servers.values(): + server_element = etree.SubElement(servers, "server") + add_attribute(server_element, "name", server.name) + add_attribute(server_element, "address", server.host) + if servers.getchildren(): + self.scenario.append(servers) + def write_session_hooks(self) -> None: # hook scripts hooks = etree.Element("session_hooks") @@ -572,6 +582,7 @@ class CoreXmlReader: self.read_session_metadata() self.read_session_options() self.read_session_hooks() + self.read_servers() self.read_session_origin() self.read_service_configs() self.read_mobility_configs() @@ -635,6 +646,16 @@ class CoreXmlReader: logging.info("reading hook: state(%s) name(%s)", state, name) self.session.add_hook(state, name, data) + def read_servers(self) -> None: + servers = self.scenario.find("servers") + if servers is None: + return + for server in servers.iterchildren(): + name = server.get("name") + address = server.get("address") + logging.info("reading server: name(%s) address(%s)", name, address) + self.session.distributed.add_server(name, address) + def read_session_origin(self) -> None: session_origin = self.scenario.find("session_origin") if session_origin is None: From 7a21affbd4514b56f187a2e9cb93ec9066bec1fa Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 8 Jul 2020 08:46:30 -0700 Subject: [PATCH 0451/1131] pygui: update nodes to display assigned server name when not localhost --- daemon/core/gui/graph/node.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index f765816d..7b5cd2f3 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -42,10 +42,11 @@ class CanvasNode: x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE ) label_y = self._get_label_y() + label = self.get_label() self.text_id: int = self.canvas.create_text( x, label_y, - text=self.core_node.name, + text=label, tags=tags.NODE_LABEL, font=self.app.icon_text_font, fill="#0000CD", @@ -123,9 +124,16 @@ class CanvasNode: self.antennas.clear() self.antenna_images.clear() + def get_label(self) -> str: + label = self.core_node.name + if self.core_node.server: + label = f"{self.core_node.name}({self.core_node.server})" + return label + def redraw(self) -> None: self.canvas.itemconfig(self.id, image=self.image) - self.canvas.itemconfig(self.text_id, text=self.core_node.name) + label = self.get_label() + self.canvas.itemconfig(self.text_id, text=label) for edge in self.edges: edge.redraw() From 9fed90832284a716640248c90e3365f852e30ca4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 8 Jul 2020 11:56:23 -0700 Subject: [PATCH 0452/1131] docs: adjustments to distributed documentation to be more complete --- docs/distributed.md | 47 +++++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/docs/distributed.md b/docs/distributed.md index f36efc72..ad3d61f8 100644 --- a/docs/distributed.md +++ b/docs/distributed.md @@ -12,6 +12,28 @@ run on one of the emulation servers or on a separate machine. Each machine that will act as an emulation will require the installation of a distributed CORE package and some configuration to allow SSH as root. +## CORE Configuration + +CORE configuration settings required for using distributed functionality. + +Edit **/etc/core/core.conf** or specific configuration file being used. + +```shell +# uncomment and set this to the address that remote servers +# use to get back to the main host, example below +distributed_address = 129.168.0.101 +``` + +### EMANE Specific Configurations + +EMANE needs to have controlnet configured in **core.conf** in order to startup correctly. +The names before the addresses need to match the names of distributed servers configured. + +```shell +controlnet = core1:172.16.1.0/24 core2:172.16.2.0/24 core3:172.16.3.0/24 core4:172.16.4.0/24 core5:172.16.5.0/24 +emane_event_generate = True +``` + ## Configuring SSH Distributed CORE works using the python fabric library to run commands on @@ -88,6 +110,16 @@ PermitRootLogin without-password sudo systemctl restart sshd ``` +### Fabric Config File + +Make sure the value used below is the absolute path to the file +generated above **~/.ssh/core**" + +Add/update the fabric configuration file **/etc/fabric.yml**: +```yaml +connect_kwargs: {"key_filename": "/home/user/.ssh/core"} +``` + ## Add Emulation Servers in GUI Within the core-gui navigate to menu option: @@ -152,26 +184,13 @@ to arrange the topology such that the number of tunnels is minimized. The tunnels carry data between servers to connect nodes as specified in the topology. These tunnels are created using GRE tunneling, similar to the Tunnel Tool. -### EMANE Configuration and Issues - -EMANE needs to have controlnet configured in **core.conf** in order to startup correctly. -The names before the addresses need to match the servers configured in -**~/.core/servers.conf** previously. - -```shell -controlnet = core1:172.16.1.0/24 core2:172.16.2.0/24 core3:172.16.3.0/24 core4:172.16.4.0/24 core5:172.16.5.0/24 -``` - -```shell -emane_event_generate = True -``` - ## Distributed Checklist 1. Install CORE on master server 1. Install distributed CORE package on all servers needed 1. Installed and configure public-key SSH access on all servers (if you want to use double-click shells or Widgets.) for both the GUI user (for terminals) and root for running CORE commands +1. Update CORE configuration as needed 1. Choose the servers that participate in distributed emulation. 1. Assign nodes to desired servers, empty for master server. 1. Press the **Start** button to launch the distributed emulation. From a236ea2455901140ace5d1cd809f792811e24ec7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 9 Jul 2020 23:01:28 -0700 Subject: [PATCH 0453/1131] updates to poetry based installation --- configure.ac | 12 ------ docs/install2.md | 27 ++++++++++++ install2.sh | 30 +++++++++++++ tasks.py | 107 +++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 146 insertions(+), 30 deletions(-) create mode 100644 docs/install2.md create mode 100755 install2.sh diff --git a/configure.ac b/configure.ac index ae2d0c8d..02102760 100644 --- a/configure.ac +++ b/configure.ac @@ -167,18 +167,6 @@ if test "x$enable_daemon" = "xyes"; then if test "x$ovs_of_path" = "xno" ; then AC_MSG_WARN([Could not locate ovs-ofctl cannot use OVS mode]) fi - - CFLAGS_save=$CFLAGS - CPPFLAGS_save=$CPPFLAGS - if test "x$PYTHON_INCLUDE_DIR" = "x"; then - PYTHON_INCLUDE_DIR=`$PYTHON -c "import distutils.sysconfig; print(distutils.sysconfig.get_python_inc())"` - fi - CFLAGS="-I$PYTHON_INCLUDE_DIR" - CPPFLAGS="-I$PYTHON_INCLUDE_DIR" - AC_CHECK_HEADERS([Python.h], [], - AC_MSG_ERROR([Python bindings require Python development headers (try installing your 'python-devel' or 'python-dev' package)])) - CFLAGS=$CFLAGS_save - CPPFLAGS=$CPPFLAGS_save fi if [ test "x$enable_daemon" = "xyes" || test "x$enable_vnodedonly" = "xyes" ] ; then diff --git a/docs/install2.md b/docs/install2.md new file mode 100644 index 00000000..b8f7c099 --- /dev/null +++ b/docs/install2.md @@ -0,0 +1,27 @@ +# Commands Used Ubuntu + +```shell +# get pip +sudo apt install python3-pip python3-venv + +# install pipx +python3 -m pip install --user pipx +python3 -m pipx ensurepath + +# install invoke +pipx install invoke + +# install core +inv install + +# run daemon +inv daemon + +# run gui +inv gui +``` + +Commands Used CentOS + +```shell +``` diff --git a/install2.sh b/install2.sh new file mode 100755 index 00000000..a8366670 --- /dev/null +++ b/install2.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# exit on error +set -e + +# detect os/ver for install type +os="" +if [[ -f /etc/os-release ]]; then + . /etc/os-release + os=${ID} +fi + +echo "installing CORE for ${os}" +case ${os} in +"ubuntu") + sudo apt install -y python3-pip + + ;; +"centos") + sudo yum install -y python3-pip + ;; +*) + echo "unknown OS ID ${os} cannot install" + ;; +esac + +python3 -m pip install --user pipx +python3 -m pipx ensurepath +python3 -m pipx install invoke +inv install diff --git a/tasks.py b/tasks.py index b19e8925..37b2e10c 100644 --- a/tasks.py +++ b/tasks.py @@ -1,52 +1,123 @@ +import os + from invoke import task +UBUNTU = "ubuntu" +CENTOS = "centos" +DAEMON_DIR = "daemon" +VCMD_DIR = "netns" +GUI_DIR = "gui" + + +def get_python(c): + with c.cd(DAEMON_DIR): + venv = c.run("poetry env info -p", hide=True).stdout.strip() + return os.path.join(venv, "bin", "python") + + +def get_pytest(c): + with c.cd(DAEMON_DIR): + venv = c.run("poetry env info -p", hide=True).stdout.strip() + return os.path.join(venv, "bin", "pytest") + + +def get_os(): + d = {} + with open("/etc/os-release", "r") as f: + for line in f.readlines(): + line = line.strip() + key, value = line.split("=") + d[key] = value + return d["ID"] + + +@task +def install(c): + """ + install core + """ + # get os + os_name = get_os() + # install system dependencies + print("installing system dependencies...") + if os_name == UBUNTU: + c.run( + "sudo apt install -y automake pkg-config gcc libev-dev ebtables iproute2 " + "ethtool tk python3-tk", hide=True + ) + else: + raise Exception(f"unsupported os: {os_name}") + # install grpcio-tools for building proto files + print("installing grpcio-tools...") + c.run("python3 -m pip install --user grpcio-tools", hide=True) + # build core + print("building core...") + c.run("./bootstrap.sh", hide=True) + c.run("./configure", hide=True) + c.run("make -j", hide=True) + # install vcmd + print("installing vcmd...") + with c.cd(VCMD_DIR): + c.run("sudo make install", hide=True) + # install vcmd + print("installing gui...") + with c.cd(GUI_DIR): + c.run("sudo make install", hide=True) + # install poetry environment + print("installing poetry...") + c.run("pipx install poetry", hide=True) + with c.cd(DAEMON_DIR): + print("installing core environment using poetry...") + c.run("poetry install", hide=True) + @task def daemon(c): """ - Runs core-daemon. + start core-daemon """ - with c.cd("daemon"): - poetry = c.run("which poetry").stdout.strip() + python = get_python(c) + with c.cd(DAEMON_DIR): c.run( - f"sudo {poetry} run scripts/core-daemon " - "-f data/core.conf -l data/logging.conf" + f"sudo {python} scripts/core-daemon " + "-f data/core.conf -l data/logging.conf", + pty=True ) @task def gui(c): """ - Run core-pygui. + start core-pygui """ - with c.cd("daemon"): - c.run("poetry run scripts/core-pygui") + with c.cd(DAEMON_DIR): + c.run("poetry run scripts/core-pygui", pty=True) @task def test(c): """ - Run core tests. + run core tests """ - with c.cd("daemon"): - poetry = c.run("which poetry").stdout.strip() - c.run(f"sudo {poetry} run pytest -v --lf -x tests", pty=True) + pytest = get_pytest(c) + with c.cd(DAEMON_DIR): + c.run(f"sudo {pytest} -v --lf -x tests", pty=True) @task def test_mock(c): """ - Run core tests using mock to avoid running as sudo. + run core tests using mock to avoid running as sudo """ - with c.cd("daemon"): + with c.cd(DAEMON_DIR): c.run("poetry run pytest -v --mock --lf -x tests", pty=True) @task def test_emane(c): """ - Run core emane tests. + run core emane tests """ - with c.cd("daemon"): - poetry = c.run("which poetry").stdout.strip() - c.run(f"sudo {poetry} run pytest -v --lf -x tests/emane", pty=True) + pytest = get_pytest(c) + with c.cd(DAEMON_DIR): + c.run(f"{pytest} -v --lf -x tests/emane", pty=True) From 139323146e437e796603cd01178f29a8af667d3d Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 9 Jul 2020 23:27:05 -0700 Subject: [PATCH 0454/1131] Update install2.sh update to account for missing python3-venv package and updating PATH in script to run newly installed commands --- install2.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install2.sh b/install2.sh index a8366670..496906e8 100755 --- a/install2.sh +++ b/install2.sh @@ -13,8 +13,7 @@ fi echo "installing CORE for ${os}" case ${os} in "ubuntu") - sudo apt install -y python3-pip - + sudo apt install -y python3-pip python3-venv ;; "centos") sudo yum install -y python3-pip @@ -26,5 +25,6 @@ esac python3 -m pip install --user pipx python3 -m pipx ensurepath -python3 -m pipx install invoke +export PATH=$PATH:~/.local/bin +pipx install invoke inv install From d4ac9e618f1177da57fcad0c75bbb1f8761fa953 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 08:32:47 -0700 Subject: [PATCH 0455/1131] improvements to invoke tasks for installation --- tasks.py | 117 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 89 insertions(+), 28 deletions(-) diff --git a/tasks.py b/tasks.py index 37b2e10c..9949512c 100644 --- a/tasks.py +++ b/tasks.py @@ -1,74 +1,135 @@ import os +import sys +from enum import Enum -from invoke import task +from invoke import task, Context -UBUNTU = "ubuntu" -CENTOS = "centos" -DAEMON_DIR = "daemon" -VCMD_DIR = "netns" -GUI_DIR = "gui" +DAEMON_DIR: str = "daemon" +VCMD_DIR: str = "netns" +GUI_DIR: str = "gui" -def get_python(c): +class OsName(Enum): + UBUNTU = "ubuntu" + CENTOS = "centos" + + +class OsLike(Enum): + DEBIAN = "debian" + + +class OsInfo: + def __init__(self, name: OsName, like: OsLike, version: str) -> None: + self.name: OsName = name + self.like: OsLike = like + self.version: str = version + + +def get_python(c: Context) -> str: with c.cd(DAEMON_DIR): venv = c.run("poetry env info -p", hide=True).stdout.strip() return os.path.join(venv, "bin", "python") -def get_pytest(c): +def get_pytest(c: Context) -> str: with c.cd(DAEMON_DIR): venv = c.run("poetry env info -p", hide=True).stdout.strip() return os.path.join(venv, "bin", "pytest") -def get_os(): +def get_os() -> OsInfo: d = {} with open("/etc/os-release", "r") as f: for line in f.readlines(): line = line.strip() key, value = line.split("=") - d[key] = value - return d["ID"] + d[key] = value.strip('"') + name_value = d["ID"] + like_value = d["ID_LIKE"] + try: + name = OsName(name_value) + like = OsLike(like_value) + except ValueError: + print(f"unsupported os({name_value}) like({like_value})") + sys.exit(1) + version = d["VERSION_ID"] + return OsInfo(name, like, version) -@task -def install(c): - """ - install core - """ - # get os - os_name = get_os() - # install system dependencies +def install_system(c: Context, os_info: OsInfo) -> None: print("installing system dependencies...") - if os_name == UBUNTU: + if os_info.like == OsLike.DEBIAN: c.run( "sudo apt install -y automake pkg-config gcc libev-dev ebtables iproute2 " "ethtool tk python3-tk", hide=True ) - else: - raise Exception(f"unsupported os: {os_name}") - # install grpcio-tools for building proto files + + +def install_grpcio(c: Context) -> None: print("installing grpcio-tools...") c.run("python3 -m pip install --user grpcio-tools", hide=True) - # build core + + +def build(c: Context) -> None: print("building core...") c.run("./bootstrap.sh", hide=True) c.run("./configure", hide=True) c.run("make -j", hide=True) - # install vcmd + + +def install_core(c: Context) -> None: print("installing vcmd...") with c.cd(VCMD_DIR): c.run("sudo make install", hide=True) - # install vcmd print("installing gui...") with c.cd(GUI_DIR): c.run("sudo make install", hide=True) - # install poetry environment + + +def install_poetry(c: Context, dev: bool) -> None: print("installing poetry...") c.run("pipx install poetry", hide=True) + args = "" if dev else "--no-dev" with c.cd(DAEMON_DIR): print("installing core environment using poetry...") - c.run("poetry install", hide=True) + c.run(f"poetry install {args}", hide=True) + if dev: + c.run("poetry run pre-commit install") + + +def install_ospf_mdr(c: Context, os_info: OsInfo) -> None: + if c.run("which zebra"): + print("quagga already installed, skipping ospf mdr") + return + if os_info.like == OsLike.DEBIAN: + c.run("sudo apt install -y libtool gawk libreadline-dev") + clone_dir = "/tmp/ospf-mdr" + c.run( + f"git clone https://github.com/USNavalResearchLaboratory/ospf-mdr {clone_dir}" + ) + with c.cd(clone_dir): + c.run("./bootstrap.sh") + c.run( + "./configure --disable-doc --enable-user=root --enable-group=root " + "--with-cflags=-ggdb --sysconfdir=/usr/local/etc/quagga --enable-vtysh " + "--localstatedir=/var/run/quagga" + ) + c.run("make -j") + c.run("sudo make install") + + +@task +def install(c, dev=False): + """ + install core + """ + os_info = get_os() + install_system(c, os_info) + install_grpcio(c) + build(c) + install_core(c) + install_poetry(c, dev) + install_ospf_mdr(c, os_info) @task From 8357cddbab3db1d7244ede08f1cce1cd29dd8463 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 08:51:40 -0700 Subject: [PATCH 0456/1131] added developer and verbose flags to poetry install --- install2.sh | 24 ++++++++++++++++++++- tasks.py | 61 ++++++++++++++++++++++++++++------------------------- 2 files changed, 55 insertions(+), 30 deletions(-) diff --git a/install2.sh b/install2.sh index 496906e8..4efaee53 100755 --- a/install2.sh +++ b/install2.sh @@ -10,6 +10,28 @@ if [[ -f /etc/os-release ]]; then os=${ID} fi +# parse arguments +dev="" +verbose="" +while getopts "drv:" opt; do + case ${opt} in + d) + dev="-d" + ;; + v) + verbose="-v" + ;; + \?) + echo "script usage: $(basename $0) [-d] [-v]" >&2 + echo "" >&2 + echo "-v enable verbose install" >&2 + echo "-d enable developer install" >&2 + exit 1 + ;; + esac +done +shift $((OPTIND - 1)) + echo "installing CORE for ${os}" case ${os} in "ubuntu") @@ -27,4 +49,4 @@ python3 -m pip install --user pipx python3 -m pipx ensurepath export PATH=$PATH:~/.local/bin pipx install invoke -inv install +inv install $(dev) $(verbose) diff --git a/tasks.py b/tasks.py index 9949512c..da8fef21 100644 --- a/tasks.py +++ b/tasks.py @@ -56,80 +56,83 @@ def get_os() -> OsInfo: return OsInfo(name, like, version) -def install_system(c: Context, os_info: OsInfo) -> None: +def install_system(c: Context, os_info: OsInfo, hide: bool) -> None: print("installing system dependencies...") if os_info.like == OsLike.DEBIAN: c.run( "sudo apt install -y automake pkg-config gcc libev-dev ebtables iproute2 " - "ethtool tk python3-tk", hide=True + "ethtool tk python3-tk", hide=hide ) -def install_grpcio(c: Context) -> None: +def install_grpcio(c: Context, hide: bool) -> None: print("installing grpcio-tools...") - c.run("python3 -m pip install --user grpcio-tools", hide=True) + c.run("python3 -m pip install --user grpcio-tools", hide=hide) -def build(c: Context) -> None: +def build(c: Context, hide: bool) -> None: print("building core...") - c.run("./bootstrap.sh", hide=True) - c.run("./configure", hide=True) - c.run("make -j", hide=True) + c.run("./bootstrap.sh", hide=hide) + c.run("./configure", hide=hide) + c.run("make -j", hide=hide) -def install_core(c: Context) -> None: +def install_core(c: Context, hide: bool) -> None: print("installing vcmd...") with c.cd(VCMD_DIR): - c.run("sudo make install", hide=True) + c.run("sudo make install", hide=hide) print("installing gui...") with c.cd(GUI_DIR): - c.run("sudo make install", hide=True) + c.run("sudo make install", hide=hide) -def install_poetry(c: Context, dev: bool) -> None: +def install_poetry(c: Context, dev: bool, hide: bool) -> None: print("installing poetry...") - c.run("pipx install poetry", hide=True) + c.run("pipx install poetry", hide=hide) args = "" if dev else "--no-dev" with c.cd(DAEMON_DIR): print("installing core environment using poetry...") - c.run(f"poetry install {args}", hide=True) + c.run(f"poetry install {args}", hide=hide) if dev: c.run("poetry run pre-commit install") -def install_ospf_mdr(c: Context, os_info: OsInfo) -> None: - if c.run("which zebra"): +def install_ospf_mdr(c: Context, os_info: OsInfo, hide: bool) -> None: + if c.run("which zebra", warn=True, hide=hide): print("quagga already installed, skipping ospf mdr") return if os_info.like == OsLike.DEBIAN: - c.run("sudo apt install -y libtool gawk libreadline-dev") + c.run("sudo apt install -y libtool gawk libreadline-dev", hide=hide) clone_dir = "/tmp/ospf-mdr" c.run( - f"git clone https://github.com/USNavalResearchLaboratory/ospf-mdr {clone_dir}" + f"git clone https://github.com/USNavalResearchLaboratory/ospf-mdr {clone_dir}", + hide=hide ) with c.cd(clone_dir): - c.run("./bootstrap.sh") + c.run("./bootstrap.sh", hide=hide) c.run( "./configure --disable-doc --enable-user=root --enable-group=root " "--with-cflags=-ggdb --sysconfdir=/usr/local/etc/quagga --enable-vtysh " - "--localstatedir=/var/run/quagga" + "--localstatedir=/var/run/quagga", + hide=hide ) - c.run("make -j") - c.run("sudo make install") + c.run("make -j", hide=hide) + c.run("sudo make install", hide=hide) @task -def install(c, dev=False): +def install(c, dev=False, verbose=False): """ install core """ + hide = not verbose os_info = get_os() - install_system(c, os_info) - install_grpcio(c) - build(c) - install_core(c) - install_poetry(c, dev) - install_ospf_mdr(c, os_info) + install_system(c, os_info, hide) + install_grpcio(c, hide) + build(c, hide) + install_core(c, hide) + install_poetry(c, dev, hide) + install_ospf_mdr(c, os_info, hide) @task From 41f0c8ef95bd914a92209c6c4a8d19bdc19d5eb1 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 08:56:16 -0700 Subject: [PATCH 0457/1131] fixed bad arguments being passed in install2.sh --- install2.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install2.sh b/install2.sh index 4efaee53..13876776 100755 --- a/install2.sh +++ b/install2.sh @@ -49,4 +49,4 @@ python3 -m pip install --user pipx python3 -m pipx ensurepath export PATH=$PATH:~/.local/bin pipx install invoke -inv install $(dev) $(verbose) +inv install ${dev} ${verbose} From 7dd2b6668016f6c625a3dc4556d0b75c73050100 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 08:58:09 -0700 Subject: [PATCH 0458/1131] added message for installing ospf mdr in install task --- tasks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tasks.py b/tasks.py index da8fef21..50440e4d 100644 --- a/tasks.py +++ b/tasks.py @@ -101,6 +101,7 @@ def install_ospf_mdr(c: Context, os_info: OsInfo, hide: bool) -> None: if c.run("which zebra", warn=True, hide=hide): print("quagga already installed, skipping ospf mdr") return + print("installing ospf mdr...") if os_info.like == OsLike.DEBIAN: c.run("sudo apt install -y libtool gawk libreadline-dev", hide=hide) clone_dir = "/tmp/ospf-mdr" From 51200cf930ca44415a2326a9574168446d24f7d6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 09:20:13 -0700 Subject: [PATCH 0459/1131] added more messages to ospf mdr invoke install --- tasks.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index 50440e4d..af8abfba 100644 --- a/tasks.py +++ b/tasks.py @@ -101,15 +101,17 @@ def install_ospf_mdr(c: Context, os_info: OsInfo, hide: bool) -> None: if c.run("which zebra", warn=True, hide=hide): print("quagga already installed, skipping ospf mdr") return - print("installing ospf mdr...") + print("installing ospf mdr dependencies...") if os_info.like == OsLike.DEBIAN: c.run("sudo apt install -y libtool gawk libreadline-dev", hide=hide) + print("cloning ospf mdr...") clone_dir = "/tmp/ospf-mdr" c.run( f"git clone https://github.com/USNavalResearchLaboratory/ospf-mdr {clone_dir}", hide=hide ) with c.cd(clone_dir): + print("building ospf mdr...") c.run("./bootstrap.sh", hide=hide) c.run( "./configure --disable-doc --enable-user=root --enable-group=root " @@ -118,6 +120,7 @@ def install_ospf_mdr(c: Context, os_info: OsInfo, hide: bool) -> None: hide=hide ) c.run("make -j", hide=hide) + print("installing ospf mdr...") c.run("sudo make install", hide=hide) From a2a825e91df6ee5da5b29adafdbd41f2ff5ed96f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 09:55:30 -0700 Subject: [PATCH 0460/1131] better invoke output and removed -j from building ospf mdr --- tasks.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tasks.py b/tasks.py index af8abfba..59f8b1bd 100644 --- a/tasks.py +++ b/tasks.py @@ -74,14 +74,14 @@ def build(c: Context, hide: bool) -> None: print("building core...") c.run("./bootstrap.sh", hide=hide) c.run("./configure", hide=hide) - c.run("make -j", hide=hide) + c.run("make", hide=hide) def install_core(c: Context, hide: bool) -> None: - print("installing vcmd...") + print("installing core vcmd...") with c.cd(VCMD_DIR): c.run("sudo make install", hide=hide) - print("installing gui...") + print("installing core gui...") with c.cd(GUI_DIR): c.run("sudo make install", hide=hide) @@ -119,7 +119,7 @@ def install_ospf_mdr(c: Context, os_info: OsInfo, hide: bool) -> None: "--localstatedir=/var/run/quagga", hide=hide ) - c.run("make -j", hide=hide) + c.run("make", hide=hide) print("installing ospf mdr...") c.run("sudo make install", hide=hide) From 85cd31ae52a9ef203f6fdac117112f2e5cd0d131 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 10:23:45 -0700 Subject: [PATCH 0461/1131] fixed install2.sh argument parsing --- install2.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install2.sh b/install2.sh index 13876776..bdfd2f7b 100755 --- a/install2.sh +++ b/install2.sh @@ -13,7 +13,7 @@ fi # parse arguments dev="" verbose="" -while getopts "drv:" opt; do +while getopts "dv" opt; do case ${opt} in d) dev="-d" From 9b7dce0861beca7a4ae8ab03870026c6a1241da6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 10:27:17 -0700 Subject: [PATCH 0462/1131] added example output after installation and note about getting a new terminal --- tasks.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tasks.py b/tasks.py index 59f8b1bd..8d04dd72 100644 --- a/tasks.py +++ b/tasks.py @@ -137,6 +137,11 @@ def install(c, dev=False, verbose=False): install_core(c, hide) install_poetry(c, dev, hide) install_ospf_mdr(c, os_info, hide) + print("please open a new terminal or re-login to leverage invoke for running core") + print("# run daemon") + print("inv daemon") + print("# run gui") + print("inv gui") @task From 38e68386970ad2e7579f38ecc257220457e6521c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 10:39:14 -0700 Subject: [PATCH 0463/1131] avoid empty lines when parsing os-release --- tasks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tasks.py b/tasks.py index 8d04dd72..05f23d5f 100644 --- a/tasks.py +++ b/tasks.py @@ -42,6 +42,8 @@ def get_os() -> OsInfo: with open("/etc/os-release", "r") as f: for line in f.readlines(): line = line.strip() + if not line: + continue key, value = line.split("=") d[key] = value.strip('"') name_value = d["ID"] From cd9ecd22570d7ec9f4076e645aa4072bb58f6e18 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 10:45:03 -0700 Subject: [PATCH 0464/1131] added redhat like os to invoke task --- tasks.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tasks.py b/tasks.py index 05f23d5f..272db1df 100644 --- a/tasks.py +++ b/tasks.py @@ -16,13 +16,14 @@ class OsName(Enum): class OsLike(Enum): DEBIAN = "debian" + REDHAT = "rhel fedora" class OsInfo: - def __init__(self, name: OsName, like: OsLike, version: str) -> None: + def __init__(self, name: OsName, like: OsLike, version: float) -> None: self.name: OsName = name self.like: OsLike = like - self.version: str = version + self.version: float = version def get_python(c: Context) -> str: @@ -48,13 +49,16 @@ def get_os() -> OsInfo: d[key] = value.strip('"') name_value = d["ID"] like_value = d["ID_LIKE"] + version_value = d["VERSION_ID"] try: name = OsName(name_value) like = OsLike(like_value) + version = float(version_value) except ValueError: - print(f"unsupported os({name_value}) like({like_value})") + print( + f"unsupported os({name_value}) like({like_value}) version({version_value}" + ) sys.exit(1) - version = d["VERSION_ID"] return OsInfo(name, like, version) From 7821ffb642ab9d5d302e05cb20006c427d4c34e4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 10:51:06 -0700 Subject: [PATCH 0465/1131] python-devel is needed on centos --- tasks.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tasks.py b/tasks.py index 272db1df..938fb567 100644 --- a/tasks.py +++ b/tasks.py @@ -71,8 +71,10 @@ def install_system(c: Context, os_info: OsInfo, hide: bool) -> None: ) -def install_grpcio(c: Context, hide: bool) -> None: +def install_grpcio(c: Context, os_info: OsInfo, hide: bool) -> None: print("installing grpcio-tools...") + if os_info.like == OsLike.REDHAT: + c.run("sudo yum install -y python3-devel", hide=hide) c.run("python3 -m pip install --user grpcio-tools", hide=hide) @@ -138,7 +140,7 @@ def install(c, dev=False, verbose=False): hide = not verbose os_info = get_os() install_system(c, os_info, hide) - install_grpcio(c, hide) + install_grpcio(c, os_info, hide) build(c, hide) install_core(c, hide) install_poetry(c, dev, hide) From 9bf5756a0352be784da5ac38fea3fe5fb248b6ab Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 11:20:27 -0700 Subject: [PATCH 0466/1131] added invoke system dependencies for redhat --- tasks.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tasks.py b/tasks.py index 938fb567..d910d5dc 100644 --- a/tasks.py +++ b/tasks.py @@ -67,14 +67,19 @@ def install_system(c: Context, os_info: OsInfo, hide: bool) -> None: if os_info.like == OsLike.DEBIAN: c.run( "sudo apt install -y automake pkg-config gcc libev-dev ebtables iproute2 " - "ethtool tk python3-tk", hide=hide + "ethtool tk python3-tk", + hide=hide + ) + elif os_info.like == OsLike.REDHAT: + c.run( + "sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel " + "iptables-ebtables iproute python3-devel python3-tkinter tk ethtool", + hide=hide ) -def install_grpcio(c: Context, os_info: OsInfo, hide: bool) -> None: +def install_grpcio(c: Context, hide: bool) -> None: print("installing grpcio-tools...") - if os_info.like == OsLike.REDHAT: - c.run("sudo yum install -y python3-devel", hide=hide) c.run("python3 -m pip install --user grpcio-tools", hide=hide) @@ -140,7 +145,7 @@ def install(c, dev=False, verbose=False): hide = not verbose os_info = get_os() install_system(c, os_info, hide) - install_grpcio(c, os_info, hide) + install_grpcio(c, hide) build(c, hide) install_core(c, hide) install_poetry(c, dev, hide) From 626b977505719d23d484a690f745f21101b34fd4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 11:30:12 -0700 Subject: [PATCH 0467/1131] added ospf mdr redhat dependencies to invoke install --- tasks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tasks.py b/tasks.py index d910d5dc..0e9f625c 100644 --- a/tasks.py +++ b/tasks.py @@ -117,6 +117,8 @@ def install_ospf_mdr(c: Context, os_info: OsInfo, hide: bool) -> None: print("installing ospf mdr dependencies...") if os_info.like == OsLike.DEBIAN: c.run("sudo apt install -y libtool gawk libreadline-dev", hide=hide) + elif os_info.like == OsLike.REDHAT: + c.run("sudo yum install -y libtool gawk readline-devel", hide=hide) print("cloning ospf mdr...") clone_dir = "/tmp/ospf-mdr" c.run( From fe362a10d6b97e0d3aea5c11e2d2f9ffdefc14a9 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 12:05:11 -0700 Subject: [PATCH 0468/1131] poetry changes to help force installing grpcio from binary packages, causing long build times on centos --- daemon/poetry.lock | 156 ++++++++++++++++++++++++------------------ daemon/pyproject.toml | 2 +- 2 files changed, 91 insertions(+), 67 deletions(-) diff --git a/daemon/poetry.lock b/daemon/poetry.lock index c5e1ebb6..c72bc364 100644 --- a/daemon/poetry.lock +++ b/daemon/poetry.lock @@ -180,7 +180,7 @@ description = "HTTP/2-based RPC framework" name = "grpcio" optional = false python-versions = "*" -version = "1.29.0" +version = "1.27.2" [package.dependencies] six = ">=1.5.2" @@ -191,10 +191,10 @@ description = "Protobuf code generator for gRPC" name = "grpcio-tools" optional = false python-versions = "*" -version = "1.29.0" +version = "1.27.2" [package.dependencies] -grpcio = ">=1.29.0" +grpcio = ">=1.27.2" protobuf = ">=3.5.0.post1" [[package]] @@ -602,7 +602,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools", "func-timeout"] [metadata] -content-hash = "ff2407f8ca447047101b8e0c8656027d07d2f15e51b3a950f2c2d789f929da6b" +content-hash = "260c6612feb7c884d03b3b98e5fb22ad4d06a58559876f239bd5c677d14a7ba1" python-versions = "^3.6" [metadata.files] @@ -725,70 +725,94 @@ flake8 = [ {file = "flake8-3.8.2.tar.gz", hash = "sha256:c69ac1668e434d37a2d2880b3ca9aafd54b3a10a3ac1ab101d22f29e29cf8634"}, ] grpcio = [ - {file = "grpcio-1.29.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e90f3d11185c36593186e5ff1f581acc6ddfa4190f145b0366e579de1f52803b"}, - {file = "grpcio-1.29.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:5024b26e17a1bfc9390fb3b8077bf886eee02970af780fd23072970ef08cefe8"}, - {file = "grpcio-1.29.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:23bc395a32c2465564cb242e48bdd2fdbe5a4aebf307649a800da1b971ee7f29"}, - {file = "grpcio-1.29.0-cp27-cp27m-win32.whl", hash = "sha256:886d48c32960b39e059494637eb0157a694956248d03b0de814447c188b74799"}, - {file = "grpcio-1.29.0-cp27-cp27m-win_amd64.whl", hash = "sha256:da0ca9b1089d00e39a8b83deec799a4e5c37ec1b44d804495424acde50531868"}, - {file = "grpcio-1.29.0-cp27-cp27mu-linux_armv7l.whl", hash = "sha256:ebf0ccb782027ef9e213e03b6d00bbd8dabd80959db7d468c0738e6d94b5204c"}, - {file = "grpcio-1.29.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:2637ce96b7c954d2b71060f50eb4c72f81668f1b2faa6cbdc74677e405978901"}, - {file = "grpcio-1.29.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:75b2247307a7ecaf6abc9eb2bd04af8f88816c111b87bf0044d7924396e9549c"}, - {file = "grpcio-1.29.0-cp35-cp35m-linux_armv7l.whl", hash = "sha256:7bf3cb1e0f4a9c89f7b748583b994bdce183103d89d5ff486da48a7668a052c7"}, - {file = "grpcio-1.29.0-cp35-cp35m-macosx_10_7_intel.whl", hash = "sha256:a6dddb177b3cfa0cfe299fb9e07d6a3382cc79466bef48fe9c4326d5c5b1dcb8"}, - {file = "grpcio-1.29.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:b49f243936b0f6ae8eb6adf88a1e54e736f1c6724a1bff6b591d105d708263ad"}, - {file = "grpcio-1.29.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:9cfb4b71cc3c8757f137d47000f9d90d4bd818733f9ab4f78bd447e052a4cb9a"}, - {file = "grpcio-1.29.0-cp35-cp35m-win32.whl", hash = "sha256:10cdc8946a7c2284bbc8e16d346eaa2beeaae86ea598f345df86d4ef7dfedb84"}, - {file = "grpcio-1.29.0-cp35-cp35m-win_amd64.whl", hash = "sha256:806c9759f5589b3761561187408e0313a35c5c53f075c7590effab8d27d67dfe"}, - {file = "grpcio-1.29.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:57c8cc2ae8cb94c3a89671af7e1380a4cdfcd6bab7ba303f4461ec32ded250ae"}, - {file = "grpcio-1.29.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:97b72bf2242a351a89184134adbb0ae3b422e6893c6c712bc7669e2eab21501b"}, - {file = "grpcio-1.29.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:517538a54afdd67162ea2af1ac3326c0752c5d13e6ddadbc4885f6a28e91ab28"}, - {file = "grpcio-1.29.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:eede3039c3998e2cc0f6713f4ac70f235bd32967c9b958a17bf937aceebc12c3"}, - {file = "grpcio-1.29.0-cp36-cp36m-win32.whl", hash = "sha256:54e4658c09084b09cd83a5ea3a8bce78e4031ff1010bb8908c399a22a76a6f08"}, - {file = "grpcio-1.29.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7e02a7c40304eecee203f809a982732bd37fad4e798acad98fe73c66e44ff2db"}, - {file = "grpcio-1.29.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ff7931241351521b8df01d7448800ce0d59364321d8d82c49b826d455678ff08"}, - {file = "grpcio-1.29.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:5fd9ffe938e9225c654c60eb21ff011108cc27302db85200413807e0eda99a4a"}, - {file = "grpcio-1.29.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:9ef0370bcf629ece4e7e37796e4604e2514b920669be2911fc3f9c163a73a57b"}, - {file = "grpcio-1.29.0-cp37-cp37m-win32.whl", hash = "sha256:3d8c510b6eabce5192ce126003d74d7751c7218d3e2ad39fcf02400d7ec43abe"}, - {file = "grpcio-1.29.0-cp37-cp37m-win_amd64.whl", hash = "sha256:81bbf78a399e0ee516c81ddad8601f12af3fc9b30f2e4b2fbd64efd327304a4d"}, - {file = "grpcio-1.29.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:80e9f9f6265149ca7c84e1c8c31c2cf3e2869c45776fbe8880a3133a11d6d290"}, - {file = "grpcio-1.29.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:524ae8d3da61b856cf08abb3d0947df05402919e4be1f88328e0c1004031f72e"}, - {file = "grpcio-1.29.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c3a0ef12ee86f6e72db50e01c3dba7735a76d8c30104b9b0f7fd9d65ceb9d93f"}, - {file = "grpcio-1.29.0-cp38-cp38-win32.whl", hash = "sha256:97fcbdf1f12e0079d26db73da11ee35a09adc870b1e72fbff0211f6a8003a4e8"}, - {file = "grpcio-1.29.0-cp38-cp38-win_amd64.whl", hash = "sha256:b85f355fc24b68a6c52f2750e7141110d1fcd07dfdc9b282de0000550fe0511b"}, - {file = "grpcio-1.29.0.tar.gz", hash = "sha256:a97ea91e31863c9a3879684b5fb3c6ab4b17c5431787548fc9f52b9483ea9c25"}, + {file = "grpcio-1.27.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dbec0a3a154dbf2eb85b38abaddf24964fa1c059ee0a4ad55d6f39211b1a4bca"}, + {file = "grpcio-1.27.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1ef949b15a1f5f30651532a9b54edf3bd7c0b699a10931505fa2c80b2d395942"}, + {file = "grpcio-1.27.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:ed123037896a8db6709b8ad5acc0ed435453726ea0b63361d12de369624c2ab5"}, + {file = "grpcio-1.27.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:f9d632ce9fd485119c968ec6a7a343de698c5e014d17602ae2f110f1b05925ed"}, + {file = "grpcio-1.27.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:80c3d1ce8820dd819d1c9d6b63b6f445148480a831173b572a9174a55e7abd47"}, + {file = "grpcio-1.27.2-cp27-cp27m-win32.whl", hash = "sha256:07f82aefb4a56c7e1e52b78afb77d446847d27120a838a1a0489260182096045"}, + {file = "grpcio-1.27.2-cp27-cp27m-win_amd64.whl", hash = "sha256:28f27c64dd699b8b10f70da5f9320c1cffcaefca7dd76275b44571bd097f276c"}, + {file = "grpcio-1.27.2-cp27-cp27mu-linux_armv7l.whl", hash = "sha256:a25b84e10018875a0f294a7649d07c43e8bc3e6a821714e39e5cd607a36386d7"}, + {file = "grpcio-1.27.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:57949756a3ce1f096fa2b00f812755f5ab2effeccedb19feeb7d0deafa3d1de7"}, + {file = "grpcio-1.27.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f3614dabd2cc8741850597b418bcf644d4f60e73615906c3acc407b78ff720b3"}, + {file = "grpcio-1.27.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:25c77692ea8c0929d4ad400ea9c3dcbcc4936cee84e437e0ef80da58fa73d88a"}, + {file = "grpcio-1.27.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5dab393ab96b2ce4012823b2f2ed4ee907150424d2f02b97bd6f8dd8f17cc866"}, + {file = "grpcio-1.27.2-cp35-cp35m-linux_armv7l.whl", hash = "sha256:bb2987eb3af9bcf46019be39b82c120c3d35639a95bc4ee2d08f36ecdf469345"}, + {file = "grpcio-1.27.2-cp35-cp35m-macosx_10_7_intel.whl", hash = "sha256:6f328a3faaf81a2546a3022b3dfc137cc6d50d81082dbc0c94d1678943f05df3"}, + {file = "grpcio-1.27.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:5ebc13451246de82f130e8ee7e723e8d7ae1827f14b7b0218867667b1b12c88d"}, + {file = "grpcio-1.27.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:355bd7d7ce5ff2917d217f0e8ddac568cb7403e1ce1639b35a924db7d13a39b6"}, + {file = "grpcio-1.27.2-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:d1e5563e3b7f844dbc48d709c9e4a75647e11d0387cc1fa0c861d3e9d34bc844"}, + {file = "grpcio-1.27.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:1ec8fc865d8da6d0713e2092a27eee344cd54628b2c2065a0e77fff94df4ae00"}, + {file = "grpcio-1.27.2-cp35-cp35m-win32.whl", hash = "sha256:706e2dea3de33b0d8884c4d35ecd5911b4ff04d0697c4138096666ce983671a6"}, + {file = "grpcio-1.27.2-cp35-cp35m-win_amd64.whl", hash = "sha256:d18b4c8cacbb141979bb44355ee5813dd4d307e9d79b3a36d66eca7e0a203df8"}, + {file = "grpcio-1.27.2-cp36-cp36m-linux_armv7l.whl", hash = "sha256:02aef8ef1a5ac5f0836b543e462eb421df6048a7974211a906148053b8055ea6"}, + {file = "grpcio-1.27.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b78af4d42985ab3143d9882d0006f48d12f1bc4ba88e78f23762777c3ee64571"}, + {file = "grpcio-1.27.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9c0669ba9aebad540fb05a33beb7e659ea6e5ca35833fc5229c20f057db760e8"}, + {file = "grpcio-1.27.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:68a149a0482d0bc697aac702ec6efb9d380e0afebf9484db5b7e634146528371"}, + {file = "grpcio-1.27.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:a71138366d57901597bfcc52af7f076ab61c046f409c7b429011cd68de8f9fe6"}, + {file = "grpcio-1.27.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:9e9cfe55dc7ac2aa47e0fd3285ff829685f96803197042c9d2f0fb44e4b39b2c"}, + {file = "grpcio-1.27.2-cp36-cp36m-win32.whl", hash = "sha256:d22c897b65b1408509099f1c3334bd3704f5e4eb7c0486c57d0e212f71cb8f54"}, + {file = "grpcio-1.27.2-cp36-cp36m-win_amd64.whl", hash = "sha256:c59b9280284b791377b3524c8e39ca7b74ae2881ba1a6c51b36f4f1bb94cee49"}, + {file = "grpcio-1.27.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6e545908bcc2ae28e5b190ce3170f92d0438cf26a82b269611390114de0106eb"}, + {file = "grpcio-1.27.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6db7ded10b82592c472eeeba34b9f12d7b0ab1e2dcad12f081b08ebdea78d7d6"}, + {file = "grpcio-1.27.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4d3b6e66f32528bf43ca2297caca768280a8e068820b1c3dca0fcf9f03c7d6f1"}, + {file = "grpcio-1.27.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:586d931736912865c9790c60ca2db29e8dc4eace160d5a79fec3e58df79a9386"}, + {file = "grpcio-1.27.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:c03ce53690fe492845e14f4ab7e67d5a429a06db99b226b5c7caa23081c1e2bb"}, + {file = "grpcio-1.27.2-cp37-cp37m-win32.whl", hash = "sha256:209927e65395feb449783943d62a3036982f871d7f4045fadb90b2d82b153ea8"}, + {file = "grpcio-1.27.2-cp37-cp37m-win_amd64.whl", hash = "sha256:9713578f187fb1c4d00ac554fe1edcc6b3ddd62f5d4eb578b81261115802df8e"}, + {file = "grpcio-1.27.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b4efde5524579a9ce0459ca35a57a48ca878a4973514b8bb88cb80d7c9d34c85"}, + {file = "grpcio-1.27.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:fb62996c61eeff56b59ab8abfcaa0859ec2223392c03d6085048b576b567459b"}, + {file = "grpcio-1.27.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a22daaf30037b8e59d6968c76fe0f7ff062c976c7a026e92fbefc4c4bf3fc5a4"}, + {file = "grpcio-1.27.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:4a0a33ada3f6f94f855f92460896ef08c798dcc5f17d9364d1735c5adc9d7e4a"}, + {file = "grpcio-1.27.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:8111b61eee12d7af5c58f82f2c97c2664677a05df9225ef5cbc2f25398c8c454"}, + {file = "grpcio-1.27.2-cp38-cp38-win32.whl", hash = "sha256:5121fa96c79fc0ec81825091d0be5c16865f834f41b31da40b08ee60552f9961"}, + {file = "grpcio-1.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:1cff47297ee614e7ef66243dc34a776883ab6da9ca129ea114a802c5e58af5c1"}, + {file = "grpcio-1.27.2.tar.gz", hash = "sha256:5ae532b93cf9ce5a2a549b74a2c35e3b690b171ece9358519b3039c7b84c887e"}, ] grpcio-tools = [ - {file = "grpcio-tools-1.29.0.tar.gz", hash = "sha256:0f681c1ebd5472b804baa391b16dc59d92b065903999566f4776bfbd010bcec9"}, - {file = "grpcio_tools-1.29.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b504e844e6f3610f279e0fba719052a73d5acc858a82d5a1151155b3c2304478"}, - {file = "grpcio_tools-1.29.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c52bcc2e5e9d93b805e6f292e543cbabeb9a751dc9d4d451c39d4c30ee311142"}, - {file = "grpcio_tools-1.29.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:5beffd530b496866b8e8dc811e942815a6e637669350c1341b5972bb692465cc"}, - {file = "grpcio_tools-1.29.0-cp27-cp27m-win32.whl", hash = "sha256:49dcf4c11ba2766d065c90a61eb1cefc55d5d094f93c1f66a4d98bfcbc5f740c"}, - {file = "grpcio_tools-1.29.0-cp27-cp27m-win_amd64.whl", hash = "sha256:bab2a3d627f114091a758d8a7ae48af54bff717f84bb34538fed5114982e73a5"}, - {file = "grpcio_tools-1.29.0-cp27-cp27mu-linux_armv7l.whl", hash = "sha256:2a1f27a21d09e864cdfcff22265af86d9a548ea9a775e5d6a27d7abb71c3b5aa"}, - {file = "grpcio_tools-1.29.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:56aade8ed52a6cca74a4703279aaae4aa2e2b87d0ccb5778f95d31267e74fc6b"}, - {file = "grpcio_tools-1.29.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:78075ee7459001cf5c81b1f2e3f047b63d35ed018b9e15e3abeda59b70af0a4e"}, - {file = "grpcio_tools-1.29.0-cp35-cp35m-linux_armv7l.whl", hash = "sha256:1626cd01a484f29cc9b33c3902851490149d40a550b92a382978571ca7e712cf"}, - {file = "grpcio_tools-1.29.0-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:2f1d80e3988d86477633fb39442a2310513d02fcc48881b359257a4be3cfd336"}, - {file = "grpcio_tools-1.29.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:8ffdcb1cbbc1bdfe249eb08c9fc6557b4f83b9f6145b5914bfd2973013d6dc1f"}, - {file = "grpcio_tools-1.29.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:7e52c8ed5e0157ff85493f93540e3c897c7d97be03afc73230d1022ba7b80528"}, - {file = "grpcio_tools-1.29.0-cp35-cp35m-win32.whl", hash = "sha256:f464d2efe04a46a17cf9493d67e6839aa535bb8a904cc6a2b588f1b156c9265d"}, - {file = "grpcio_tools-1.29.0-cp35-cp35m-win_amd64.whl", hash = "sha256:9de112c090ab67e90b8c36eee5876278c8d037bf7c55052848886c1e8a2dd1c2"}, - {file = "grpcio_tools-1.29.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:38ab9e8afdf34289eab85ce2343c451c36837bf2521b927b30d9a845304abf4c"}, - {file = "grpcio_tools-1.29.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1038b3d6cfd7206caf7c0a54ed06896e2aeb0a7d213a40d9000a70595e2fca21"}, - {file = "grpcio_tools-1.29.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:2a681ebfde0d83b70117cac745a97a3e5dc258fd817c1c1dd2bf99579b663a28"}, - {file = "grpcio_tools-1.29.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:47d13ddbbc2bd0e21a6109f74e731049b1d8738b5d0124580efca3721fe77fd2"}, - {file = "grpcio_tools-1.29.0-cp36-cp36m-win32.whl", hash = "sha256:fb9c46b8a0ee1a5990f29d891d6023cb92fdab9aed408194667df04f72e9caf6"}, - {file = "grpcio_tools-1.29.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f672a606a59145bacc58cf4c4bb407f107abe1289f607c09e9224c99e897ed1a"}, - {file = "grpcio_tools-1.29.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1a606f2f5b23822e2e5271bf0df98c140ceed154ea6bf5c04ea85a37a0317771"}, - {file = "grpcio_tools-1.29.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d89a43d14fb3043c1876e78d7ad5018c762b0ce51c199c588fa9142442546005"}, - {file = "grpcio_tools-1.29.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:faf845f71fcb6cb5088429c676ae644116d56e5de41c639be4d7399bf71b9637"}, - {file = "grpcio_tools-1.29.0-cp37-cp37m-win32.whl", hash = "sha256:05f214bc904c8e4ebf0240993a868895ff96184172243c0c61b323f6f029863d"}, - {file = "grpcio_tools-1.29.0-cp37-cp37m-win_amd64.whl", hash = "sha256:afcb030067ba1b6c371a7bfd1ffd77375534144000d47d245ca77ebbd195901d"}, - {file = "grpcio_tools-1.29.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b55346fa75df4b1581627022a2c79cfeb58cdaebf719cdbf63ff8ae6d7d7704b"}, - {file = "grpcio_tools-1.29.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:22d91ceb853f6846bcc23f15d8a936574eeb9fc7e8941bb8a1a5f8fcf4f566b2"}, - {file = "grpcio_tools-1.29.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:6eddefcd10f261d2aef6c122fb0651a53fcaee86e47d407492c9acf57107c91a"}, - {file = "grpcio_tools-1.29.0-cp38-cp38-win32.whl", hash = "sha256:658e131e983f4c3bec2e096c3cc048e6420acad2b19fad82328c481088ce0d1a"}, - {file = "grpcio_tools-1.29.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c52f68e864f60ed51ea59a3fd18d0989720bbf2e32d47b4096eba7b0b7f7086"}, + {file = "grpcio-tools-1.27.2.tar.gz", hash = "sha256:845a51305af9fc7f9e2078edaec9a759153195f6cf1fbb12b1fa6f077e56b260"}, + {file = "grpcio_tools-1.27.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:7a2d5fb558ac153a326e742ebfd7020eb781c43d3ffd920abd42b2e6c6fdfb37"}, + {file = "grpcio_tools-1.27.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:99961156a36aae4a402d6b14c1e7efde642794b3ddbf32c51db0cb3a199e8b11"}, + {file = "grpcio_tools-1.27.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:069826dd02ce1886444cf4519c4fe1b05ac9ef41491f26e97400640531db47f6"}, + {file = "grpcio_tools-1.27.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:fae91f30dc050a8d0b32d20dc700e6092f0bd2138d83e9570fff3f0372c1b27e"}, + {file = "grpcio_tools-1.27.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a14dc7a36c845991d908a7179502ca47bcba5ae1817c4426ce68cf2c97b20ad9"}, + {file = "grpcio_tools-1.27.2-cp27-cp27m-win32.whl", hash = "sha256:d1a5e5fa47ba9557a7d3b31605631805adc66cdba9d95b5d10dfc52cca1fed53"}, + {file = "grpcio_tools-1.27.2-cp27-cp27m-win_amd64.whl", hash = "sha256:7b54b283ec83190680903a9037376dc915e1f03852a2d574ba4d981b7a1fd3d0"}, + {file = "grpcio_tools-1.27.2-cp27-cp27mu-linux_armv7l.whl", hash = "sha256:4698c6b6a57f73b14d91a542c69ff33a2da8729691b7060a5d7f6383624d045e"}, + {file = "grpcio_tools-1.27.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:87e8ca2c2d2d3e09b2a2bed5d740d7b3e64028dafb7d6be543b77eec85590736"}, + {file = "grpcio_tools-1.27.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bd7f59ff1252a3db8a143b13ea1c1e93d4b8cf4b852eb48b22ef1e6942f62a84"}, + {file = "grpcio_tools-1.27.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:a8f892378b0b02526635b806f59141abbb429d19bec56e869e04f396502c9651"}, + {file = "grpcio_tools-1.27.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:69c4a63919b9007e845d9f8980becd2f89d808a4a431ca32b9723ee37b521cb1"}, + {file = "grpcio_tools-1.27.2-cp35-cp35m-linux_armv7l.whl", hash = "sha256:dcbc06556f3713a9348c4fce02d05d91e678fc320fb2bcf0ddf8e4bb11d17867"}, + {file = "grpcio_tools-1.27.2-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:16dc3fad04fe18d50777c56af7b2d9b9984cd1cfc71184646eb431196d1645c6"}, + {file = "grpcio_tools-1.27.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1de5a273eaffeb3d126a63345e9e848ea7db740762f700eb8b5d84c5e3e7687d"}, + {file = "grpcio_tools-1.27.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6016c07d6566e3109a3c032cf3861902d66501ecc08a5a84c47e43027302f367"}, + {file = "grpcio_tools-1.27.2-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:915a695bc112517af48126ee0ecdb6aff05ed33f3eeef28f0d076f1f6b52ef5e"}, + {file = "grpcio_tools-1.27.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:ea4b3ad696d976d5eac74ec8df9a2c692113e455446ee38d5b3bd87f8e034fa6"}, + {file = "grpcio_tools-1.27.2-cp35-cp35m-win32.whl", hash = "sha256:a140bf853edb2b5e8692fe94869e3e34077d7599170c113d07a58286c604f4fe"}, + {file = "grpcio_tools-1.27.2-cp35-cp35m-win_amd64.whl", hash = "sha256:77e25c241e33b75612f2aa62985f746c6f6803ec4e452da508bb7f8d90a69db4"}, + {file = "grpcio_tools-1.27.2-cp36-cp36m-linux_armv7l.whl", hash = "sha256:5fd7efc2fd3370bd2c72dc58f31a407a5dff5498befa145da211b2e8c6a52c63"}, + {file = "grpcio_tools-1.27.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9ba88c2d99bcaf7b9cb720925e3290d73b2367d238c5779363fd5598b2dc98c7"}, + {file = "grpcio_tools-1.27.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b56caecc16307b088a431a4038c3b3bb7d0e7f9988cbd0e9fa04ac937455ea38"}, + {file = "grpcio_tools-1.27.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f8514453411d72cc3cf7d481f2b6057e5b7436736d0cd39ee2b2f72088bbf497"}, + {file = "grpcio_tools-1.27.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:c1bb8f47d58e9f7c4825abfe01e6b85eda53c8b31d2267ca4cddf3c4d0829b80"}, + {file = "grpcio_tools-1.27.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e17b2e0936b04ced99769e26111e1e86ba81619d1b2691b1364f795e45560953"}, + {file = "grpcio_tools-1.27.2-cp36-cp36m-win32.whl", hash = "sha256:520b7dafddd0f82cb7e4f6e9c6ba1049aa804d0e207870def9fe7f94d1e14090"}, + {file = "grpcio_tools-1.27.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ee50b0cf0d28748ef9f941894eb50fc464bd61b8e96aaf80c5056bea9b80d580"}, + {file = "grpcio_tools-1.27.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:627c91923df75091d8c4d244af38d5ab7ed8d786d480751d6c2b9267fbb92fe0"}, + {file = "grpcio_tools-1.27.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ef624b6134aef737b3daa4fb7e806cb8c5749efecd0b1fa9ce4f7e060c7a0221"}, + {file = "grpcio_tools-1.27.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e6932518db389ede8bf06b4119bbd3e17f42d4626e72dec2b8955b20ec732cb6"}, + {file = "grpcio_tools-1.27.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:43a1573400527a23e4174d88604fde7a9d9a69bf9473c21936b7f409858f8ebb"}, + {file = "grpcio_tools-1.27.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:57f8b9e2c7f55cd45f6dd930d6de61deb42d3eb7f9788137fbc7155cf724132a"}, + {file = "grpcio_tools-1.27.2-cp37-cp37m-win32.whl", hash = "sha256:2ca280af2cae1a014a238057bd3c0a254527569a6a9169a01c07f0590081d530"}, + {file = "grpcio_tools-1.27.2-cp37-cp37m-win_amd64.whl", hash = "sha256:59fbeb5bb9a7b94eb61642ac2cee1db5233b8094ca76fc56d4e0c6c20b5dd85f"}, + {file = "grpcio_tools-1.27.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:00c5080cfb197ed20ecf0d0ff2d07f1fc9c42c724cad21c40ff2d048de5712b1"}, + {file = "grpcio_tools-1.27.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:f5450aa904e720f9c6407b59e96a8951ed6a95463f49444b6d2594b067d39588"}, + {file = "grpcio_tools-1.27.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:aaa5ae26883c3d58d1a4323981f96b941fa09bb8f0f368d97c6225585280cf04"}, + {file = "grpcio_tools-1.27.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1266b577abe7c720fd16a83d0a4999a192e87c4a98fc9f97e0b99b106b3e155f"}, + {file = "grpcio_tools-1.27.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:a3d2aec4b09c8e59fee8b0d1ed668d09e8c48b738f03f5d8401d7eb409111c47"}, + {file = "grpcio_tools-1.27.2-cp38-cp38-win32.whl", hash = "sha256:8e7738a4b93842bca1158cde81a3587c9b7111823e40a1ddf73292ca9d58e08b"}, + {file = "grpcio_tools-1.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:84724458c86ff9b14c29b49e321f34d80445b379f4cd4d0494c694b49b1d6f88"}, ] identify = [ {file = "identify-1.4.18-py2.py3-none-any.whl", hash = "sha256:9f53e80371f2ac7c969eefda8efaabd4f77c6300f5f8fc4b634744a0db8fe5cc"}, diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml index 609fcb08..0cb32e91 100644 --- a/daemon/pyproject.toml +++ b/daemon/pyproject.toml @@ -8,7 +8,7 @@ authors = [] python = "^3.6" dataclasses = { version = "*", python = "3.6" } fabric = "*" -grpcio = "*" +grpcio = "1.27.2" invoke = "*" lxml = "*" mako = "*" From 9b541d0316f9556f839acdf943d55c0cae82cbd3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 12:09:04 -0700 Subject: [PATCH 0469/1131] adding invoke change to support grpcio binary install --- tasks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index 0e9f625c..d70df69e 100644 --- a/tasks.py +++ b/tasks.py @@ -80,7 +80,9 @@ def install_system(c: Context, os_info: OsInfo, hide: bool) -> None: def install_grpcio(c: Context, hide: bool) -> None: print("installing grpcio-tools...") - c.run("python3 -m pip install --user grpcio-tools", hide=hide) + c.run( + "python3 -m pip install --only-binary \":all:\" --user grpcio-tools", hide=hide + ) def build(c: Context, hide: bool) -> None: From 7c3e42396ac7e97e431340a331cbff5ee665d8f2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 12:20:27 -0700 Subject: [PATCH 0470/1131] invoke install acount for centos prefix issues, add usage of nproc for make -j --- tasks.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tasks.py b/tasks.py index d70df69e..24e2599c 100644 --- a/tasks.py +++ b/tasks.py @@ -85,11 +85,12 @@ def install_grpcio(c: Context, hide: bool) -> None: ) -def build(c: Context, hide: bool) -> None: +def build(c: Context, os_info: OsInfo, hide: bool) -> None: print("building core...") c.run("./bootstrap.sh", hide=hide) - c.run("./configure", hide=hide) - c.run("make", hide=hide) + prefix = "--prefix=/usr" if os_info.like == OsLike.REDHAT else "" + c.run(f"./configure {prefix}", hide=hide) + c.run("make -j$(nproc)", hide=hide) def install_core(c: Context, hide: bool) -> None: @@ -109,7 +110,7 @@ def install_poetry(c: Context, dev: bool, hide: bool) -> None: print("installing core environment using poetry...") c.run(f"poetry install {args}", hide=hide) if dev: - c.run("poetry run pre-commit install") + c.run("poetry run pre-commit install", hide=hide) def install_ospf_mdr(c: Context, os_info: OsInfo, hide: bool) -> None: @@ -136,7 +137,7 @@ def install_ospf_mdr(c: Context, os_info: OsInfo, hide: bool) -> None: "--localstatedir=/var/run/quagga", hide=hide ) - c.run("make", hide=hide) + c.run("make -j$(nproc)", hide=hide) print("installing ospf mdr...") c.run("sudo make install", hide=hide) From 8a60a4739fe8c4ec3f9658a0be28240d948d3c7b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 12:23:26 -0700 Subject: [PATCH 0471/1131] fixed missing invoke install argument --- tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index 24e2599c..5ef0f938 100644 --- a/tasks.py +++ b/tasks.py @@ -151,7 +151,7 @@ def install(c, dev=False, verbose=False): os_info = get_os() install_system(c, os_info, hide) install_grpcio(c, hide) - build(c, hide) + build(c, os_info, hide) install_core(c, hide) install_poetry(c, dev, hide) install_ospf_mdr(c, os_info, hide) From 75acbf4daef775a4a017846daa2f1296c02ee4b9 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 12:41:39 -0700 Subject: [PATCH 0472/1131] invoke install account for ebtables based on nf_tables --- tasks.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tasks.py b/tasks.py index 5ef0f938..e79f132f 100644 --- a/tasks.py +++ b/tasks.py @@ -76,6 +76,12 @@ def install_system(c: Context, os_info: OsInfo, hide: bool) -> None: "iptables-ebtables iproute python3-devel python3-tkinter tk ethtool", hide=hide ) + r = c.run("ebtables -V", hide=hide) + if "nf_tables" in r.stdout: + c.run( + "sudo update-alternatives --set ebtables /usr/sbin/ebtables-legacy", + hide=hide + ) def install_grpcio(c: Context, hide: bool) -> None: From 980ab1526ddb8ecb75e210de1fd9028b295ebef2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 12:52:12 -0700 Subject: [PATCH 0473/1131] added invoke cleanup task --- tasks.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tasks.py b/tasks.py index e79f132f..dd37cb78 100644 --- a/tasks.py +++ b/tasks.py @@ -191,6 +191,14 @@ def gui(c): c.run("poetry run scripts/core-pygui", pty=True) +@task +def cleanup(c): + """ + run core-cleanup removing leftover core nodes, bridges, directories + """ + c.run(f"sudo daemon/scripts/core-cleanup", pty=True) + + @task def test(c): """ From ece2f1c43f286b39759128ec4a1e9b198c77da88 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 12:53:21 -0700 Subject: [PATCH 0474/1131] added invoke cleanup message --- tasks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tasks.py b/tasks.py index dd37cb78..e24fa415 100644 --- a/tasks.py +++ b/tasks.py @@ -196,6 +196,7 @@ def cleanup(c): """ run core-cleanup removing leftover core nodes, bridges, directories """ + print("running core-cleanup...") c.run(f"sudo daemon/scripts/core-cleanup", pty=True) From 637f7740d63c051203518a7de4924cf085144065 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 13:00:38 -0700 Subject: [PATCH 0475/1131] added git as invoke install dependency for ospf-mdr just in case core was a source tarball --- tasks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tasks.py b/tasks.py index e24fa415..f5ed9c2a 100644 --- a/tasks.py +++ b/tasks.py @@ -125,9 +125,9 @@ def install_ospf_mdr(c: Context, os_info: OsInfo, hide: bool) -> None: return print("installing ospf mdr dependencies...") if os_info.like == OsLike.DEBIAN: - c.run("sudo apt install -y libtool gawk libreadline-dev", hide=hide) + c.run("sudo apt install -y libtool gawk libreadline-dev git", hide=hide) elif os_info.like == OsLike.REDHAT: - c.run("sudo yum install -y libtool gawk readline-devel", hide=hide) + c.run("sudo yum install -y libtool gawk readline-devel git", hide=hide) print("cloning ospf mdr...") clone_dir = "/tmp/ospf-mdr" c.run( From a9ec21c6044704dee586c05b62e3d76150f5a223 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 14:20:13 -0700 Subject: [PATCH 0476/1131] add make dependency to redhat based invoke installs, since centos 8 does not have it by default --- tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index f5ed9c2a..4ca8e3cf 100644 --- a/tasks.py +++ b/tasks.py @@ -73,7 +73,7 @@ def install_system(c: Context, os_info: OsInfo, hide: bool) -> None: elif os_info.like == OsLike.REDHAT: c.run( "sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel " - "iptables-ebtables iproute python3-devel python3-tkinter tk ethtool", + "iptables-ebtables iproute python3-devel python3-tkinter tk ethtool make", hide=hide ) r = c.run("ebtables -V", hide=hide) From d2fe7fcff0321149ec756f1043fdbb2abb946ce8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 16:50:37 -0700 Subject: [PATCH 0477/1131] invoke install account for centos 8 netem not being installed/enabled and add warning for failed ebtables legacy support --- tasks.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tasks.py b/tasks.py index 4ca8e3cf..1b6023d5 100644 --- a/tasks.py +++ b/tasks.py @@ -73,15 +73,23 @@ def install_system(c: Context, os_info: OsInfo, hide: bool) -> None: elif os_info.like == OsLike.REDHAT: c.run( "sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel " - "iptables-ebtables iproute python3-devel python3-tkinter tk ethtool make", + "iptables-ebtables iproute python3-devel python3-tkinter tk ethtool make " + "kernel-modules-extra", hide=hide ) + # centos 8+ does not support netem by default + if os_info.name == OsName.CENTOS and os_info.version >= 8: + c.run("sudo yum install -y kernel-modules-extra", hide=hide) + c.run("sudo modprobe sch_netem", hide=hide) + # attempt to setup legacy ebtables when an nftables based version is found r = c.run("ebtables -V", hide=hide) if "nf_tables" in r.stdout: - c.run( + if not c.run( "sudo update-alternatives --set ebtables /usr/sbin/ebtables-legacy", + warn=True, hide=hide - ) + ): + print("ERROR: unable to setup required ebtables-legacy, WLAN will not work") def install_grpcio(c: Context, hide: bool) -> None: From 737dae1224526a38552b93814395082b5fb090aa Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 17:56:30 -0700 Subject: [PATCH 0478/1131] invoke install, added message for failed kernel netem enable in centos 8 and exit with error --- tasks.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index 1b6023d5..9e4d393f 100644 --- a/tasks.py +++ b/tasks.py @@ -80,7 +80,11 @@ def install_system(c: Context, os_info: OsInfo, hide: bool) -> None: # centos 8+ does not support netem by default if os_info.name == OsName.CENTOS and os_info.version >= 8: c.run("sudo yum install -y kernel-modules-extra", hide=hide) - c.run("sudo modprobe sch_netem", hide=hide) + if not c.run("sudo modprobe sch_netem", warn=True, hide=hide): + print("ERROR: you need to install the latest kernel") + print("run the following, restart, and try again") + print("sudo yum update") + sys.exit(1) # attempt to setup legacy ebtables when an nftables based version is found r = c.run("ebtables -V", hide=hide) if "nf_tables" in r.stdout: From 8cf89fa114e1552eb56a958b5d7be3e448c4d1d2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 17:57:10 -0700 Subject: [PATCH 0479/1131] invoke install, change ebtables-legacy from error to warning, since we dont exit --- tasks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index 9e4d393f..0255ffd9 100644 --- a/tasks.py +++ b/tasks.py @@ -93,7 +93,9 @@ def install_system(c: Context, os_info: OsInfo, hide: bool) -> None: warn=True, hide=hide ): - print("ERROR: unable to setup required ebtables-legacy, WLAN will not work") + print( + "WARNING: unable to setup required ebtables-legacy, WLAN will not work" + ) def install_grpcio(c: Context, hide: bool) -> None: From 80eaa274697470cf428759727681ddd32a8f6f00 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 20:02:41 -0700 Subject: [PATCH 0480/1131] created baseline doc to support poetry based installations --- docs/install2.md | 74 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 19 deletions(-) diff --git a/docs/install2.md b/docs/install2.md index b8f7c099..c877086c 100644 --- a/docs/install2.md +++ b/docs/install2.md @@ -1,27 +1,63 @@ -# Commands Used Ubuntu +# Installing CORE + +## Overview + +CORE provides a script to help automate installing all required software +to build and run, including a python virtual environment to run it all in. + +The following tools will be leveraged during installation: + +|Tool|Description| +|---|---| +|pip|used to install pipx| +|pipx|used to install standalone python tools (invoke, poetry)| +|invoke|used to run provided tasks (install, daemon, gui, tests, etc)| +|poetry|used to install the managed python virtual environment for running CORE| + +## 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 + +## Running Installation ```shell -# get pip -sudo apt install python3-pip python3-venv +# clone CORE repo +git clone https://github.com/coreemu/core.git +cd core +git checkout enhancement/poetry-invoke -# install pipx -python3 -m pip install --user pipx -python3 -m pipx ensurepath +# run install script +./install2.sh +``` -# install invoke -pipx install invoke +## Using Invoke Tasks -# install core -inv install +The invoke tool installed by way of pipx provides conveniences for running +CORE tasks to help ensure usage of the create python virtual environment. -# run daemon +```shell +Available tasks: + + cleanup run core-cleanup removing leftover core nodes, bridges, directories + daemon start core-daemon + gui start core-pygui + install install core + test run core tests + test-emane run core emane tests + test-mock run core tests using mock to avoid running as sudo +``` + +Example running the core-daemon task from the root of the repo: +```shell inv daemon - -# run gui -inv gui -``` - -Commands Used CentOS - -```shell ``` From d0e9cee6503e9c2217558752984d5450c068e21c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 10 Jul 2020 20:29:47 -0700 Subject: [PATCH 0481/1131] added invoke task to help wrap core-cli --- docs/install2.md | 18 ++++++++++++++++++ tasks.py | 9 +++++++++ 2 files changed, 27 insertions(+) diff --git a/docs/install2.md b/docs/install2.md index c877086c..86e3db92 100644 --- a/docs/install2.md +++ b/docs/install2.md @@ -61,3 +61,21 @@ Example running the core-daemon task from the root of the repo: ```shell inv daemon ``` + +Some tasks are wrappers around command line tools and requires running +them with a slight variation for compatibility. You can enter the +poetry shell to run the script natively. + +```shell +# running core-cli as a task requires all options to be provided +# within a string +inv cli "query session -i 1" + +# entering the poetry shell to use core-cli natively +cd $REPO/daemon +poetry shell +core-cli query session -i 1 + +# exit the shell +exit +``` diff --git a/tasks.py b/tasks.py index 0255ffd9..b180688d 100644 --- a/tasks.py +++ b/tasks.py @@ -205,6 +205,15 @@ def gui(c): c.run("poetry run scripts/core-pygui", pty=True) +@task +def cli(c, args): + """ + run core-cli used to query and modify a running session + """ + with c.cd(DAEMON_DIR): + c.run(f"poetry run scripts/core-cli {args}", pty=True) + + @task def cleanup(c): """ From 7398196dcca5473a5e1d3a2a43b51bfb7cc4d631 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 11 Jul 2020 14:06:53 -0700 Subject: [PATCH 0482/1131] pygui: dont show mobility player when joining sessions not in runtime --- daemon/core/gui/coreclient.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 9479cbcb..255192be 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -377,7 +377,8 @@ class CoreClient: # organize canvas self.app.canvas.organize() - self.show_mobility_players() + if self.is_runtime(): + self.show_mobility_players() # update ui to represent current state self.app.after(0, self.app.joined_session_update) From e70448352740393f02973ed784dae297752db0f9 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 11 Jul 2020 14:26:06 -0700 Subject: [PATCH 0483/1131] update install script to avoid issues with recent grpcio-tools and pip binary packages --- install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index a12072f1..e4ac66e8 100755 --- a/install.sh +++ b/install.sh @@ -86,7 +86,7 @@ if [ -z "${reinstall}" ]; then echo "installing core system dependencies" sudo apt install -y automake pkg-config gcc libev-dev ebtables iproute2 \ python${ubuntu_py} python${ubuntu_py}-dev python3-pip python3-tk tk libtk-img ethtool autoconf - python3 -m pip install grpcio-tools + python3 -m pip install grpcio-tools==1.27.2 echo "installing ospf-mdr system dependencies" sudo apt install -y libtool gawk libreadline-dev install_ospf_mdr @@ -108,7 +108,7 @@ if [ -z "${reinstall}" ]; then echo "installing core system dependencies" sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel iptables-ebtables iproute \ python${centos_py} python${centos_py}-devel python3-pip python3-tkinter tk ethtool autoconf - sudo python3 -m pip install grpcio-tools + python3 -m pip install grpcio-tools==1.27.2 echo "installing ospf-mdr system dependencies" sudo yum install -y libtool gawk readline-devel install_ospf_mdr From 68ff7a86c8c00f31fa158041be9a71efed538781 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 11 Jul 2020 15:00:09 -0700 Subject: [PATCH 0484/1131] fixed install script issues with grpcio-tools and updated documentation --- docs/install.md | 4 +++- install.sh | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/install.md b/docs/install.md index 4a39218d..b7804633 100644 --- a/docs/install.md +++ b/docs/install.md @@ -232,9 +232,11 @@ git clone https://github.com/coreemu/core.git ### Install grpcio-tools Python module grpcio-tools is currently needed to generate gRPC protobuf code. +Specifically leveraging 1.27.2 to avoid compatibility issues with older versions +of pip pulling down binary files. ```shell -sudo python3 -m pip install grpcio-tools +python3 -m pip install --user grpcio==1.27.2 grpcio-tools==1.27.2 ``` ### Build and Install diff --git a/install.sh b/install.sh index e4ac66e8..3eb6694e 100755 --- a/install.sh +++ b/install.sh @@ -86,7 +86,7 @@ if [ -z "${reinstall}" ]; then echo "installing core system dependencies" sudo apt install -y automake pkg-config gcc libev-dev ebtables iproute2 \ python${ubuntu_py} python${ubuntu_py}-dev python3-pip python3-tk tk libtk-img ethtool autoconf - python3 -m pip install grpcio-tools==1.27.2 + python3 -m pip install --user grpcio==1.27.2 grpcio-tools==1.27.2 echo "installing ospf-mdr system dependencies" sudo apt install -y libtool gawk libreadline-dev install_ospf_mdr @@ -108,7 +108,7 @@ if [ -z "${reinstall}" ]; then echo "installing core system dependencies" sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel iptables-ebtables iproute \ python${centos_py} python${centos_py}-devel python3-pip python3-tkinter tk ethtool autoconf - python3 -m pip install grpcio-tools==1.27.2 + python3 -m pip install --user grpcio==1.27.2 grpcio-tools==1.27.2 echo "installing ospf-mdr system dependencies" sudo yum install -y libtool gawk readline-devel install_ospf_mdr From 5a35431bcb9a278929c617ab031ff16cc1afadfb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 11 Jul 2020 15:08:04 -0700 Subject: [PATCH 0485/1131] updated grpcio-tools installation to specifically specify binary requirement and updated doc --- docs/install.md | 2 +- install.sh | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/install.md b/docs/install.md index b7804633..227f877c 100644 --- a/docs/install.md +++ b/docs/install.md @@ -236,7 +236,7 @@ Specifically leveraging 1.27.2 to avoid compatibility issues with older versions of pip pulling down binary files. ```shell -python3 -m pip install --user grpcio==1.27.2 grpcio-tools==1.27.2 +python3 -m pip install --only-binary ":all:" --user grpcio-tools ``` ### Build and Install diff --git a/install.sh b/install.sh index 3eb6694e..6cc193ed 100755 --- a/install.sh +++ b/install.sh @@ -11,6 +11,10 @@ function install_python_depencencies() { sudo python3 -m pip install -r daemon/requirements.txt } +function install_grpcio_tools() { + python3 -m pip install --only-binary ":all:" --user grpcio-tools +} + function install_ospf_mdr() { rm -rf /tmp/ospf-mdr git clone https://github.com/USNavalResearchLaboratory/ospf-mdr /tmp/ospf-mdr @@ -86,7 +90,7 @@ if [ -z "${reinstall}" ]; then echo "installing core system dependencies" sudo apt install -y automake pkg-config gcc libev-dev ebtables iproute2 \ python${ubuntu_py} python${ubuntu_py}-dev python3-pip python3-tk tk libtk-img ethtool autoconf - python3 -m pip install --user grpcio==1.27.2 grpcio-tools==1.27.2 + install_grpcio_tools echo "installing ospf-mdr system dependencies" sudo apt install -y libtool gawk libreadline-dev install_ospf_mdr @@ -108,7 +112,7 @@ if [ -z "${reinstall}" ]; then echo "installing core system dependencies" sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel iptables-ebtables iproute \ python${centos_py} python${centos_py}-devel python3-pip python3-tkinter tk ethtool autoconf - python3 -m pip install --user grpcio==1.27.2 grpcio-tools==1.27.2 + install_grpcio_tools echo "installing ospf-mdr system dependencies" sudo yum install -y libtool gawk readline-devel install_ospf_mdr From ec45d7198b682a9a7af67e0eb52234ad1589451b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 11 Jul 2020 16:17:50 -0700 Subject: [PATCH 0486/1131] ci: changes to switch to poetry --- .github/workflows/daemon-checks.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/daemon-checks.yml b/.github/workflows/daemon-checks.yml index d955ee58..9e9f7aa7 100644 --- a/.github/workflows/daemon-checks.yml +++ b/.github/workflows/daemon-checks.yml @@ -11,32 +11,32 @@ jobs: uses: actions/setup-python@v1 with: python-version: 3.6 - - name: Install pipenv + - name: install poetry run: | python -m pip install --upgrade pip - pip install pipenv + pip install poetry cd daemon cp setup.py.in setup.py cp core/constants.py.in core/constants.py sed -i 's/required=True/required=False/g' core/emulator/coreemu.py - pipenv sync --dev + poetry install - name: isort run: | cd daemon - pipenv run isort -c -df + poetry run isort -c -df - name: black run: | cd daemon - pipenv run black --check --exclude ".+_pb2.*.py|doc|build|utm\.py|setup\.py" . + poetry run black --check --exclude ".+_pb2.*.py|doc|build|utm\.py|setup\.py" . - name: flake8 run: | cd daemon - pipenv run flake8 + poetry run flake8 - name: grpc run: | cd daemon/proto - pipenv run python -m grpc_tools.protoc -I . --python_out=.. --grpc_python_out=.. core/api/grpc/*.proto + poetry run python -m grpc_tools.protoc -I . --python_out=.. --grpc_python_out=.. core/api/grpc/*.proto - name: test run: | cd daemon - pipenv run test --mock + poetry run pytest --mock tests From 5c58e99ad41d45e1a70c001d9932321623c626db Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 11 Jul 2020 22:11:23 -0700 Subject: [PATCH 0487/1131] updated pre-commit file to use poetry environment --- daemon/.pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/.pre-commit-config.yaml b/daemon/.pre-commit-config.yaml index 73566c9d..13a6955b 100644 --- a/daemon/.pre-commit-config.yaml +++ b/daemon/.pre-commit-config.yaml @@ -5,19 +5,19 @@ repos: name: isort stages: [commit] language: system - entry: bash -c 'cd daemon && pipenv run isort --atomic -y' + entry: bash -c 'cd daemon && poetry run isort --atomic -y' types: [python] - id: black name: black stages: [commit] language: system - entry: bash -c 'cd daemon && pipenv run black --exclude ".+_pb2.*.py|doc|build|utm\.py" .' + entry: bash -c 'cd daemon && poetry run black --exclude ".+_pb2.*.py|doc|build|utm\.py" .' types: [python] - id: flake8 name: flake8 stages: [commit] language: system - entry: bash -c 'cd daemon && pipenv run flake8' + entry: bash -c 'cd daemon && poetry run flake8' types: [python] From dcf35680984068fca2e3d35dfa9cdad66fe19082 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 13 Jul 2020 08:58:32 -0700 Subject: [PATCH 0488/1131] force grpcio related installations to all use the same version to avoid any version conflicts --- daemon/poetry.lock | 2 +- daemon/pyproject.toml | 2 +- docs/install.md | 5 ++--- tasks.py | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/daemon/poetry.lock b/daemon/poetry.lock index c72bc364..9de19d13 100644 --- a/daemon/poetry.lock +++ b/daemon/poetry.lock @@ -602,7 +602,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools", "func-timeout"] [metadata] -content-hash = "260c6612feb7c884d03b3b98e5fb22ad4d06a58559876f239bd5c677d14a7ba1" +content-hash = "94df87a12a92ccb6512e4c30965e7ba1fe54b4fa3ff75827ca55b3de8472b30e" python-versions = "^3.6" [metadata.files] diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml index 0cb32e91..165fb34c 100644 --- a/daemon/pyproject.toml +++ b/daemon/pyproject.toml @@ -21,7 +21,7 @@ pyyaml = "*" [tool.poetry.dev-dependencies] black = "==19.3b0" flake8 = "*" -grpcio-tools = "*" +grpcio-tools = "1.27.2" isort = "*" mock = "*" pre-commit = "*" diff --git a/docs/install.md b/docs/install.md index 227f877c..99cee9f6 100644 --- a/docs/install.md +++ b/docs/install.md @@ -232,11 +232,10 @@ git clone https://github.com/coreemu/core.git ### Install grpcio-tools Python module grpcio-tools is currently needed to generate gRPC protobuf code. -Specifically leveraging 1.27.2 to avoid compatibility issues with older versions -of pip pulling down binary files. +Specifically leveraging 1.27.2 as that is what will be used during runtime. ```shell -python3 -m pip install --only-binary ":all:" --user grpcio-tools +python3 -m pip install --user grpcio==12.7.2 grpcio-tools==12.7.2 ``` ### Build and Install diff --git a/tasks.py b/tasks.py index b180688d..c512d388 100644 --- a/tasks.py +++ b/tasks.py @@ -101,7 +101,7 @@ def install_system(c: Context, os_info: OsInfo, hide: bool) -> None: def install_grpcio(c: Context, hide: bool) -> None: print("installing grpcio-tools...") c.run( - "python3 -m pip install --only-binary \":all:\" --user grpcio-tools", hide=hide + "python3 -m pip install --user grpcio==12.7.2 grpcio-tools==12.7.2", hide=hide ) From 32c7808cab79d7f12cc6fbefedb4a92feb341395 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 13 Jul 2020 09:01:55 -0700 Subject: [PATCH 0489/1131] fixed bad version for grpcio tools --- docs/install.md | 2 +- tasks.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/install.md b/docs/install.md index 99cee9f6..6abd0945 100644 --- a/docs/install.md +++ b/docs/install.md @@ -235,7 +235,7 @@ Python module grpcio-tools is currently needed to generate gRPC protobuf code. Specifically leveraging 1.27.2 as that is what will be used during runtime. ```shell -python3 -m pip install --user grpcio==12.7.2 grpcio-tools==12.7.2 +python3 -m pip install --user grpcio==1.27.2 grpcio-tools==1.27.2 ``` ### Build and Install diff --git a/tasks.py b/tasks.py index c512d388..cc16b943 100644 --- a/tasks.py +++ b/tasks.py @@ -101,7 +101,7 @@ def install_system(c: Context, os_info: OsInfo, hide: bool) -> None: def install_grpcio(c: Context, hide: bool) -> None: print("installing grpcio-tools...") c.run( - "python3 -m pip install --user grpcio==12.7.2 grpcio-tools==12.7.2", hide=hide + "python3 -m pip install --user grpcio==1.27.2 grpcio-tools==1.27.2", hide=hide ) From 63f09e02543a463b728399f0d39c51e739f41dd9 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 13 Jul 2020 13:14:13 -0700 Subject: [PATCH 0490/1131] added installation of modified scripts and service to invoke task --- tasks.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tasks.py b/tasks.py index cc16b943..c2028f35 100644 --- a/tasks.py +++ b/tasks.py @@ -1,6 +1,9 @@ +import inspect import os import sys from enum import Enum +from pathlib import Path +from tempfile import NamedTemporaryFile from invoke import task, Context @@ -162,6 +165,61 @@ def install_ospf_mdr(c: Context, os_info: OsInfo, hide: bool) -> None: c.run("sudo make install", hide=hide) +def install_files(c: Context, hide: bool, prefix="/usr/local") -> None: + # install all scripts + python = get_python(c) + bin_dir = Path(prefix).joinpath("bin") + for script in Path("daemon/scripts").iterdir(): + dest = bin_dir.joinpath(script.name) + print(f"installing {script} to {dest}") + with open(script, "r") as f: + lines = f.readlines() + first = lines[0].strip() + # modify python scripts to point to virtual environment + if first == "#!/usr/bin/env python3": + lines[0] = f"#!{python}\n" + temp = NamedTemporaryFile("w", delete=False) + for line in lines: + temp.write(line) + temp.close() + c.run(f"sudo cp {temp.name} {dest}", hide=hide) + c.run(f"sudo chmod 755 {dest}", hide=hide) + os.unlink(temp.name) + # copy normal links + else: + c.run(f"sudo cp {script} {dest}", hide=hide) + + # install core configuration file + config_dir = "/etc/core" + print(f"installing core configuration files under {config_dir}") + c.run(f"sudo mkdir -p {config_dir}", hide=hide) + c.run(f"sudo cp -n daemon/data/core.conf {config_dir}", hide=hide) + c.run(f"sudo cp -n daemon/data/logging.conf {config_dir}", hide=hide) + + # install service + systemd_dir = Path("/lib/systemd/system/") + service_file = systemd_dir.joinpath("core-daemon.service") + if systemd_dir.exists(): + print(f"installing core-daemon.service for systemd to {service_file}") + service_data = inspect.cleandoc(f""" + [Unit] + Description=Common Open Research Emulator Service + After=network.target + + [Service] + Type=simple + ExecStart={bin_dir}/core-daemon + TasksMax=infinity + + [Install] + WantedBy=multi-user.target + """) + temp = NamedTemporaryFile("w", delete=False) + temp.write(service_data) + temp.close() + c.run(f"sudo cp {temp.name} {service_file}", hide=hide) + + @task def install(c, dev=False, verbose=False): """ @@ -174,6 +232,7 @@ def install(c, dev=False, verbose=False): build(c, os_info, hide) install_core(c, hide) install_poetry(c, dev, hide) + install_files(c, hide) install_ospf_mdr(c, os_info, hide) print("please open a new terminal or re-login to leverage invoke for running core") print("# run daemon") From 79058810c2b827a482880cb4fa907bf19290fedf Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 13 Jul 2020 13:48:27 -0700 Subject: [PATCH 0491/1131] added uninstall invoke task to uninstall core files added with the invoke install command, beyond system packages --- tasks.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/tasks.py b/tasks.py index c2028f35..2f9bd556 100644 --- a/tasks.py +++ b/tasks.py @@ -29,10 +29,14 @@ class OsInfo: self.version: float = version -def get_python(c: Context) -> str: +def get_python(c: Context, warn: bool = False) -> str: with c.cd(DAEMON_DIR): - venv = c.run("poetry env info -p", hide=True).stdout.strip() - return os.path.join(venv, "bin", "python") + r = c.run("poetry env info -p", warn=warn, hide=True) + if r.ok: + venv = r.stdout.strip() + return os.path.join(venv, "bin", "python") + else: + return "" def get_pytest(c: Context) -> str: @@ -165,7 +169,7 @@ def install_ospf_mdr(c: Context, os_info: OsInfo, hide: bool) -> None: c.run("sudo make install", hide=hide) -def install_files(c: Context, hide: bool, prefix="/usr/local") -> None: +def install_files(c: Context, hide: bool, prefix: str = "/usr/local") -> None: # install all scripts python = get_python(c) bin_dir = Path(prefix).joinpath("bin") @@ -241,6 +245,47 @@ def install(c, dev=False, verbose=False): print("inv gui") +@task +def uninstall(c, dev=False, verbose=False, prefix="/usr/local"): + """ + uninstall core + """ + hide = not verbose + print("uninstalling core-gui") + with c.cd(GUI_DIR): + c.run("sudo make uninstall", hide=hide) + print("uninstalling vcmd") + with c.cd(VCMD_DIR): + c.run("sudo make uninstall", hide=hide) + print("cleaning build directory") + c.run("make clean", hide=hide) + c.run("./bootstrap.sh clean", hide=hide) + python = get_python(c, warn=True) + if python: + with c.cd(DAEMON_DIR): + if dev: + print("uninstalling pre-commit") + c.run("poetry run pre-commit uninstall", hide=hide) + print("uninstalling poetry virtual environment") + c.run(f"poetry env remove {python}", hide=hide) + + # remove installed files + bin_dir = Path(prefix).joinpath("bin") + for script in Path("daemon/scripts").iterdir(): + dest = bin_dir.joinpath(script.name) + print(f"uninstalling {dest}") + c.run(f"sudo rm -f {dest}", hide=hide) + + # install service + systemd_dir = Path("/lib/systemd/system/") + service_name = "core-daemon.service" + service_file = systemd_dir.joinpath(service_name) + if service_file.exists(): + print(f"uninstalling service {service_file}") + c.run(f"sudo systemctl disable {service_name}", hide=hide) + c.run(f"sudo rm -f {service_file}", hide=hide) + + @task def daemon(c): """ From bd87403ae5d968473fe651db4cd0f69b46a1ec00 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 13 Jul 2020 14:13:32 -0700 Subject: [PATCH 0492/1131] add prefix option to install2.sh script --- install2.sh | 9 +++++++-- tasks.py | 10 ++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/install2.sh b/install2.sh index bdfd2f7b..5e5a9b11 100755 --- a/install2.sh +++ b/install2.sh @@ -13,7 +13,8 @@ fi # parse arguments dev="" verbose="" -while getopts "dv" opt; do +prefix="" +while getopts "dvp:" opt; do case ${opt} in d) dev="-d" @@ -21,11 +22,15 @@ while getopts "dv" opt; do v) verbose="-v" ;; + p) + prefix="-p ${OPTARG}" + ;; \?) echo "script usage: $(basename $0) [-d] [-v]" >&2 echo "" >&2 echo "-v enable verbose install" >&2 echo "-d enable developer install" >&2 + echo "-p install prefix, defaults to /usr/local" >&2 exit 1 ;; esac @@ -49,4 +54,4 @@ python3 -m pip install --user pipx python3 -m pipx ensurepath export PATH=$PATH:~/.local/bin pipx install invoke -inv install ${dev} ${verbose} +inv install ${dev} ${verbose} ${prefix} diff --git a/tasks.py b/tasks.py index 2f9bd556..b4f37679 100644 --- a/tasks.py +++ b/tasks.py @@ -10,6 +10,7 @@ from invoke import task, Context DAEMON_DIR: str = "daemon" VCMD_DIR: str = "netns" GUI_DIR: str = "gui" +DEFAULT_PREFIX: str = "/usr/local" class OsName(Enum): @@ -169,7 +170,7 @@ def install_ospf_mdr(c: Context, os_info: OsInfo, hide: bool) -> None: c.run("sudo make install", hide=hide) -def install_files(c: Context, hide: bool, prefix: str = "/usr/local") -> None: +def install_files(c: Context, hide: bool, prefix: str = DEFAULT_PREFIX) -> None: # install all scripts python = get_python(c) bin_dir = Path(prefix).joinpath("bin") @@ -225,10 +226,11 @@ def install_files(c: Context, hide: bool, prefix: str = "/usr/local") -> None: @task -def install(c, dev=False, verbose=False): +def install(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): """ install core """ + print(f"installing core with prefix: {prefix}") hide = not verbose os_info = get_os() install_system(c, os_info, hide) @@ -236,7 +238,7 @@ def install(c, dev=False, verbose=False): build(c, os_info, hide) install_core(c, hide) install_poetry(c, dev, hide) - install_files(c, hide) + install_files(c, hide, prefix) install_ospf_mdr(c, os_info, hide) print("please open a new terminal or re-login to leverage invoke for running core") print("# run daemon") @@ -246,7 +248,7 @@ def install(c, dev=False, verbose=False): @task -def uninstall(c, dev=False, verbose=False, prefix="/usr/local"): +def uninstall(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): """ uninstall core """ From 125d74e7d5287b944200e9f145c0b4bb20a3bb1f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 08:34:02 -0700 Subject: [PATCH 0493/1131] removed pipenv specific files, wont be needed with poetry --- daemon/Pipfile | 23 -- daemon/Pipfile.lock | 732 -------------------------------------------- 2 files changed, 755 deletions(-) delete mode 100644 daemon/Pipfile delete mode 100644 daemon/Pipfile.lock diff --git a/daemon/Pipfile b/daemon/Pipfile deleted file mode 100644 index 8bf52787..00000000 --- a/daemon/Pipfile +++ /dev/null @@ -1,23 +0,0 @@ -[[source]] -name = "pypi" -url = "https://pypi.org/simple" -verify_ssl = true - -[scripts] -core = "python scripts/core-daemon -f data/core.conf -l data/logging.conf" -core-pygui = "python scripts/core-pygui" -test = "pytest -v tests" -test-mock = "pytest -v --mock tests" -test-emane = "pytest -v tests/emane" - -[dev-packages] -grpcio-tools = "*" -isort = "*" -pre-commit = "*" -flake8 = "*" -black = "==19.3b0" -pytest = "*" -mock = "*" - -[packages] -core = {editable = true,path = "."} diff --git a/daemon/Pipfile.lock b/daemon/Pipfile.lock deleted file mode 100644 index 2fb5c3b8..00000000 --- a/daemon/Pipfile.lock +++ /dev/null @@ -1,732 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "199897f713f6f338316b33fcbbe0001e9e55fcd5e5e24b2245a89454ce13321f" - }, - "pipfile-spec": 6, - "requires": {}, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "bcrypt": { - "hashes": [ - "sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89", - "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42", - "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294", - "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161", - "sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752", - "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31", - "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5", - "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c", - "sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0", - "sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de", - "sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e", - "sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052", - "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09", - "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105", - "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133", - "sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1", - "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7", - "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc" - ], - "version": "==3.1.7" - }, - "cffi": { - "hashes": [ - "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff", - "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b", - "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac", - "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0", - "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384", - "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26", - "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6", - "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b", - "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e", - "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd", - "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2", - "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66", - "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc", - "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8", - "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55", - "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4", - "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5", - "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d", - "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78", - "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa", - "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793", - "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f", - "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a", - "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f", - "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30", - "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f", - "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3", - "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c" - ], - "version": "==1.14.0" - }, - "core": { - "editable": true, - "path": "." - }, - "cryptography": { - "hashes": [ - "sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c", - "sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595", - "sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad", - "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651", - "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2", - "sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff", - "sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d", - "sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42", - "sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d", - "sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e", - "sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912", - "sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793", - "sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13", - "sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7", - "sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0", - "sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879", - "sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f", - "sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9", - "sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2", - "sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf", - "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8" - ], - "version": "==2.8" - }, - "dataclasses": { - "hashes": [ - "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836", - "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6" - ], - "index": "pypi", - "markers": "python_version == '3.6'", - "version": "==0.7" - }, - "fabric": { - "hashes": [ - "sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389", - "sha256:24842d7d51556adcabd885ac3cf5e1df73fc622a1708bf3667bf5927576cdfa6" - ], - "version": "==2.5.0" - }, - "grpcio": { - "hashes": [ - "sha256:02aef8ef1a5ac5f0836b543e462eb421df6048a7974211a906148053b8055ea6", - "sha256:07f82aefb4a56c7e1e52b78afb77d446847d27120a838a1a0489260182096045", - "sha256:1cff47297ee614e7ef66243dc34a776883ab6da9ca129ea114a802c5e58af5c1", - "sha256:1ec8fc865d8da6d0713e2092a27eee344cd54628b2c2065a0e77fff94df4ae00", - "sha256:1ef949b15a1f5f30651532a9b54edf3bd7c0b699a10931505fa2c80b2d395942", - "sha256:209927e65395feb449783943d62a3036982f871d7f4045fadb90b2d82b153ea8", - "sha256:25c77692ea8c0929d4ad400ea9c3dcbcc4936cee84e437e0ef80da58fa73d88a", - "sha256:28f27c64dd699b8b10f70da5f9320c1cffcaefca7dd76275b44571bd097f276c", - "sha256:355bd7d7ce5ff2917d217f0e8ddac568cb7403e1ce1639b35a924db7d13a39b6", - "sha256:4a0a33ada3f6f94f855f92460896ef08c798dcc5f17d9364d1735c5adc9d7e4a", - "sha256:4d3b6e66f32528bf43ca2297caca768280a8e068820b1c3dca0fcf9f03c7d6f1", - "sha256:5121fa96c79fc0ec81825091d0be5c16865f834f41b31da40b08ee60552f9961", - "sha256:57949756a3ce1f096fa2b00f812755f5ab2effeccedb19feeb7d0deafa3d1de7", - "sha256:586d931736912865c9790c60ca2db29e8dc4eace160d5a79fec3e58df79a9386", - "sha256:5ae532b93cf9ce5a2a549b74a2c35e3b690b171ece9358519b3039c7b84c887e", - "sha256:5dab393ab96b2ce4012823b2f2ed4ee907150424d2f02b97bd6f8dd8f17cc866", - "sha256:5ebc13451246de82f130e8ee7e723e8d7ae1827f14b7b0218867667b1b12c88d", - "sha256:68a149a0482d0bc697aac702ec6efb9d380e0afebf9484db5b7e634146528371", - "sha256:6db7ded10b82592c472eeeba34b9f12d7b0ab1e2dcad12f081b08ebdea78d7d6", - "sha256:6e545908bcc2ae28e5b190ce3170f92d0438cf26a82b269611390114de0106eb", - "sha256:6f328a3faaf81a2546a3022b3dfc137cc6d50d81082dbc0c94d1678943f05df3", - "sha256:706e2dea3de33b0d8884c4d35ecd5911b4ff04d0697c4138096666ce983671a6", - "sha256:80c3d1ce8820dd819d1c9d6b63b6f445148480a831173b572a9174a55e7abd47", - "sha256:8111b61eee12d7af5c58f82f2c97c2664677a05df9225ef5cbc2f25398c8c454", - "sha256:9713578f187fb1c4d00ac554fe1edcc6b3ddd62f5d4eb578b81261115802df8e", - "sha256:9c0669ba9aebad540fb05a33beb7e659ea6e5ca35833fc5229c20f057db760e8", - "sha256:9e9cfe55dc7ac2aa47e0fd3285ff829685f96803197042c9d2f0fb44e4b39b2c", - "sha256:a22daaf30037b8e59d6968c76fe0f7ff062c976c7a026e92fbefc4c4bf3fc5a4", - "sha256:a25b84e10018875a0f294a7649d07c43e8bc3e6a821714e39e5cd607a36386d7", - "sha256:a71138366d57901597bfcc52af7f076ab61c046f409c7b429011cd68de8f9fe6", - "sha256:b4efde5524579a9ce0459ca35a57a48ca878a4973514b8bb88cb80d7c9d34c85", - "sha256:b78af4d42985ab3143d9882d0006f48d12f1bc4ba88e78f23762777c3ee64571", - "sha256:bb2987eb3af9bcf46019be39b82c120c3d35639a95bc4ee2d08f36ecdf469345", - "sha256:c03ce53690fe492845e14f4ab7e67d5a429a06db99b226b5c7caa23081c1e2bb", - "sha256:c59b9280284b791377b3524c8e39ca7b74ae2881ba1a6c51b36f4f1bb94cee49", - "sha256:d18b4c8cacbb141979bb44355ee5813dd4d307e9d79b3a36d66eca7e0a203df8", - "sha256:d1e5563e3b7f844dbc48d709c9e4a75647e11d0387cc1fa0c861d3e9d34bc844", - "sha256:d22c897b65b1408509099f1c3334bd3704f5e4eb7c0486c57d0e212f71cb8f54", - "sha256:dbec0a3a154dbf2eb85b38abaddf24964fa1c059ee0a4ad55d6f39211b1a4bca", - "sha256:ed123037896a8db6709b8ad5acc0ed435453726ea0b63361d12de369624c2ab5", - "sha256:f3614dabd2cc8741850597b418bcf644d4f60e73615906c3acc407b78ff720b3", - "sha256:f9d632ce9fd485119c968ec6a7a343de698c5e014d17602ae2f110f1b05925ed", - "sha256:fb62996c61eeff56b59ab8abfcaa0859ec2223392c03d6085048b576b567459b" - ], - "version": "==1.27.2" - }, - "invoke": { - "hashes": [ - "sha256:87b3ef9d72a1667e104f89b159eaf8a514dbf2f3576885b2bbdefe74c3fb2132", - "sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134", - "sha256:de3f23bfe669e3db1085789fd859eb8ca8e0c5d9c20811e2407fa042e8a5e15d" - ], - "version": "==1.4.1" - }, - "lxml": { - "hashes": [ - "sha256:06d4e0bbb1d62e38ae6118406d7cdb4693a3fa34ee3762238bcb96c9e36a93cd", - "sha256:0701f7965903a1c3f6f09328c1278ac0eee8f56f244e66af79cb224b7ef3801c", - "sha256:1f2c4ec372bf1c4a2c7e4bb20845e8bcf8050365189d86806bad1e3ae473d081", - "sha256:4235bc124fdcf611d02047d7034164897ade13046bda967768836629bc62784f", - "sha256:5828c7f3e615f3975d48f40d4fe66e8a7b25f16b5e5705ffe1d22e43fb1f6261", - "sha256:585c0869f75577ac7a8ff38d08f7aac9033da2c41c11352ebf86a04652758b7a", - "sha256:5d467ce9c5d35b3bcc7172c06320dddb275fea6ac2037f72f0a4d7472035cea9", - "sha256:63dbc21efd7e822c11d5ddbedbbb08cd11a41e0032e382a0fd59b0b08e405a3a", - "sha256:7bc1b221e7867f2e7ff1933165c0cec7153dce93d0cdba6554b42a8beb687bdb", - "sha256:8620ce80f50d023d414183bf90cc2576c2837b88e00bea3f33ad2630133bbb60", - "sha256:8a0ebda56ebca1a83eb2d1ac266649b80af8dd4b4a3502b2c1e09ac2f88fe128", - "sha256:90ed0e36455a81b25b7034038e40880189169c308a3df360861ad74da7b68c1a", - "sha256:95e67224815ef86924fbc2b71a9dbd1f7262384bca4bc4793645794ac4200717", - "sha256:afdb34b715daf814d1abea0317b6d672476b498472f1e5aacbadc34ebbc26e89", - "sha256:b4b2c63cc7963aedd08a5f5a454c9f67251b1ac9e22fd9d72836206c42dc2a72", - "sha256:d068f55bda3c2c3fcaec24bd083d9e2eede32c583faf084d6e4b9daaea77dde8", - "sha256:d5b3c4b7edd2e770375a01139be11307f04341ec709cf724e0f26ebb1eef12c3", - "sha256:deadf4df349d1dcd7b2853a2c8796593cc346600726eff680ed8ed11812382a7", - "sha256:df533af6f88080419c5a604d0d63b2c33b1c0c4409aba7d0cb6de305147ea8c8", - "sha256:e4aa948eb15018a657702fee0b9db47e908491c64d36b4a90f59a64741516e77", - "sha256:e5d842c73e4ef6ed8c1bd77806bf84a7cb535f9c0cf9b2c74d02ebda310070e1", - "sha256:ebec08091a22c2be870890913bdadd86fcd8e9f0f22bcb398abd3af914690c15", - "sha256:edc15fcfd77395e24543be48871c251f38132bb834d9fdfdad756adb6ea37679", - "sha256:f2b74784ed7e0bc2d02bd53e48ad6ba523c9b36c194260b7a5045071abbb1012", - "sha256:fa071559f14bd1e92077b1b5f6c22cf09756c6de7139370249eb372854ce51e6", - "sha256:fd52e796fee7171c4361d441796b64df1acfceb51f29e545e812f16d023c4bbc", - "sha256:fe976a0f1ef09b3638778024ab9fb8cde3118f203364212c198f71341c0715ca" - ], - "version": "==4.5.0" - }, - "mako": { - "hashes": [ - "sha256:3139c5d64aa5d175dbafb95027057128b5fbd05a40c53999f3905ceb53366d9d", - "sha256:8e8b53c71c7e59f3de716b6832c4e401d903af574f6962edbbbf6ecc2a5fe6c9" - ], - "version": "==1.1.2" - }, - "markupsafe": { - "hashes": [ - "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", - "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", - "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", - "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", - "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", - "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", - "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", - "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", - "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", - "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", - "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", - "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", - "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", - "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", - "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", - "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", - "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", - "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", - "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", - "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", - "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", - "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" - ], - "version": "==1.1.1" - }, - "netaddr": { - "hashes": [ - "sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd", - "sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca" - ], - "version": "==0.7.19" - }, - "paramiko": { - "hashes": [ - "sha256:920492895db8013f6cc0179293147f830b8c7b21fdfc839b6bad760c27459d9f", - "sha256:9c980875fa4d2cb751604664e9a2d0f69096643f5be4db1b99599fe114a97b2f" - ], - "version": "==2.7.1" - }, - "pillow": { - "hashes": [ - "sha256:0a628977ac2e01ca96aaae247ec2bd38e729631ddf2221b4b715446fd45505be", - "sha256:4d9ed9a64095e031435af120d3c910148067087541131e82b3e8db302f4c8946", - "sha256:54ebae163e8412aff0b9df1e88adab65788f5f5b58e625dc5c7f51eaf14a6837", - "sha256:5bfef0b1cdde9f33881c913af14e43db69815c7e8df429ceda4c70a5e529210f", - "sha256:5f3546ceb08089cedb9e8ff7e3f6a7042bb5b37c2a95d392fb027c3e53a2da00", - "sha256:5f7ae9126d16194f114435ebb79cc536b5682002a4fa57fa7bb2cbcde65f2f4d", - "sha256:62a889aeb0a79e50ecf5af272e9e3c164148f4bd9636cc6bcfa182a52c8b0533", - "sha256:7406f5a9b2fd966e79e6abdaf700585a4522e98d6559ce37fc52e5c955fade0a", - "sha256:8453f914f4e5a3d828281a6628cf517832abfa13ff50679a4848926dac7c0358", - "sha256:87269cc6ce1e3dee11f23fa515e4249ae678dbbe2704598a51cee76c52e19cda", - "sha256:875358310ed7abd5320f21dd97351d62de4929b0426cdb1eaa904b64ac36b435", - "sha256:8ac6ce7ff3892e5deaab7abaec763538ffd011f74dc1801d93d3c5fc541feee2", - "sha256:91b710e3353aea6fc758cdb7136d9bbdcb26b53cefe43e2cba953ac3ee1d3313", - "sha256:9d2ba4ed13af381233e2d810ff3bab84ef9f18430a9b336ab69eaf3cd24299ff", - "sha256:a62ec5e13e227399be73303ff301f2865bf68657d15ea50b038d25fc41097317", - "sha256:ab76e5580b0ed647a8d8d2d2daee170e8e9f8aad225ede314f684e297e3643c2", - "sha256:bf4003aa538af3f4205c5fac56eacaa67a6dd81e454ffd9e9f055fff9f1bc614", - "sha256:bf598d2e37cf8edb1a2f26ed3fb255191f5232badea4003c16301cb94ac5bdd0", - "sha256:c18f70dc27cc5d236f10e7834236aff60aadc71346a5bc1f4f83a4b3abee6386", - "sha256:c5ed816632204a2fc9486d784d8e0d0ae754347aba99c811458d69fcdfd2a2f9", - "sha256:dc058b7833184970d1248135b8b0ab702e6daa833be14035179f2acb78ff5636", - "sha256:ff3797f2f16bf9d17d53257612da84dd0758db33935777149b3334c01ff68865" - ], - "version": "==7.0.0" - }, - "pycparser": { - "hashes": [ - "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", - "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" - ], - "version": "==2.20" - }, - "pynacl": { - "hashes": [ - "sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255", - "sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c", - "sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e", - "sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae", - "sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621", - "sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56", - "sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39", - "sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310", - "sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1", - "sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5", - "sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a", - "sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786", - "sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b", - "sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b", - "sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f", - "sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20", - "sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415", - "sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715", - "sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92", - "sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1", - "sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0" - ], - "version": "==1.3.0" - }, - "pyproj": { - "hashes": [ - "sha256:0d8196a5ac75fee2cf71c21066b3344427abfa8ad69b536d3404d5c7c9c0b886", - "sha256:12e378a0a21c73f96177f6cf64520f17e6b7aa02fc9cb27bd5c2d5b06ce170af", - "sha256:17738836128704d8f80b771572d77b8733841f0cb0ca42620549236ea62c4663", - "sha256:1a39175944710b225fd1943cb3b8ea0c8e059d3016360022ca10bbb7a6bfc9ae", - "sha256:2566bffb5395c9fbdb02077a0bc3e3ed0b2e4e3cadf65019e3139a8dfe27dd1d", - "sha256:3f43277f21ddaabed93b9885a4e494b785dca56e31fd37a935519d99b07807f0", - "sha256:424304beca6e0b0bc12aa46fc6d14a481ea47b1a4edec4854bb281656de38948", - "sha256:48128d794c8f52fcff2433a481e3aa2ccb0e0b3ccd51d3ad7cc10cc488c3f547", - "sha256:4a16b650722982cddedd45dfc36435b96e0ba83a2aebd4a4c247e5a68c852442", - "sha256:5161f1b5ece8a5263b64d97a32fbc473a4c6fdca5c95478e58e519ef1e97528e", - "sha256:6839ce14635ebfb01c67e456148f4f1fa04b03ef9645551b89d36593f2a3e57d", - "sha256:80e9f85ab81da75289308f23a62e1426a38411a07b0da738958d65ae8cc6c59c", - "sha256:881b44e94c781d02ecf1d9314fc7f44c09e6d54a8eac281869365999ac4db7a1", - "sha256:977542d2f8cf2981cf3ad72cedfebcd6ac56977c7aa830d9b49fa7888b56e83d", - "sha256:9bba6cbff7e23bb6d9062786d516602681b4414e9e423c138a7360e4d2a193e8", - "sha256:9bf64bba03ddc534ed3c6271ba8f9d31040f40cf8e9e7e458b6b1524a6f59082", - "sha256:9c712ceaa01488ebe6e357e1dfa2434c2304aad8a810e5d4c3d2abe21def6d58", - "sha256:b7da17e5a5c6039f85843e88c2f1ca8606d1a4cc13a87e7b68b9f51a54ef201a", - "sha256:bcdf81b3f13d2cc0354a4c3f7a567b71fcf6fe8098e519aaaee8e61f05c9de10", - "sha256:bebd3f987b7196e9d2ccfe55911b0c76ba9ce309bcabfb629ef205cbaaad37c5", - "sha256:c244e923073cd0bab74ba861ba31724aab90efda35b47a9676603c1a8e80b3ba", - "sha256:dacb94a9d570f4d9fc9369a22d44d7b3071cfe4d57d0ff2f57abd7ef6127fe41" - ], - "version": "==2.6.0" - }, - "pyyaml": { - "hashes": [ - "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", - "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", - "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", - "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", - "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", - "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", - "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", - "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", - "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", - "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", - "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" - ], - "version": "==5.3.1" - }, - "six": { - "hashes": [ - "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", - "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" - ], - "version": "==1.14.0" - } - }, - "develop": { - "appdirs": { - "hashes": [ - "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", - "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" - ], - "version": "==1.4.3" - }, - "attrs": { - "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" - ], - "version": "==19.3.0" - }, - "black": { - "hashes": [ - "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", - "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c" - ], - "index": "pypi", - "version": "==19.3b0" - }, - "cfgv": { - "hashes": [ - "sha256:1ccf53320421aeeb915275a196e23b3b8ae87dea8ac6698b1638001d4a486d53", - "sha256:c8e8f552ffcc6194f4e18dd4f68d9aef0c0d58ae7e7be8c82bee3c5e9edfa513" - ], - "version": "==3.1.0" - }, - "click": { - "hashes": [ - "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc", - "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a" - ], - "version": "==7.1.1" - }, - "distlib": { - "hashes": [ - "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21" - ], - "version": "==0.3.0" - }, - "entrypoints": { - "hashes": [ - "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", - "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" - ], - "version": "==0.3" - }, - "filelock": { - "hashes": [ - "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", - "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836" - ], - "version": "==3.0.12" - }, - "flake8": { - "hashes": [ - "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", - "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" - ], - "index": "pypi", - "version": "==3.7.9" - }, - "grpcio": { - "hashes": [ - "sha256:02aef8ef1a5ac5f0836b543e462eb421df6048a7974211a906148053b8055ea6", - "sha256:07f82aefb4a56c7e1e52b78afb77d446847d27120a838a1a0489260182096045", - "sha256:1cff47297ee614e7ef66243dc34a776883ab6da9ca129ea114a802c5e58af5c1", - "sha256:1ec8fc865d8da6d0713e2092a27eee344cd54628b2c2065a0e77fff94df4ae00", - "sha256:1ef949b15a1f5f30651532a9b54edf3bd7c0b699a10931505fa2c80b2d395942", - "sha256:209927e65395feb449783943d62a3036982f871d7f4045fadb90b2d82b153ea8", - "sha256:25c77692ea8c0929d4ad400ea9c3dcbcc4936cee84e437e0ef80da58fa73d88a", - "sha256:28f27c64dd699b8b10f70da5f9320c1cffcaefca7dd76275b44571bd097f276c", - "sha256:355bd7d7ce5ff2917d217f0e8ddac568cb7403e1ce1639b35a924db7d13a39b6", - "sha256:4a0a33ada3f6f94f855f92460896ef08c798dcc5f17d9364d1735c5adc9d7e4a", - "sha256:4d3b6e66f32528bf43ca2297caca768280a8e068820b1c3dca0fcf9f03c7d6f1", - "sha256:5121fa96c79fc0ec81825091d0be5c16865f834f41b31da40b08ee60552f9961", - "sha256:57949756a3ce1f096fa2b00f812755f5ab2effeccedb19feeb7d0deafa3d1de7", - "sha256:586d931736912865c9790c60ca2db29e8dc4eace160d5a79fec3e58df79a9386", - "sha256:5ae532b93cf9ce5a2a549b74a2c35e3b690b171ece9358519b3039c7b84c887e", - "sha256:5dab393ab96b2ce4012823b2f2ed4ee907150424d2f02b97bd6f8dd8f17cc866", - "sha256:5ebc13451246de82f130e8ee7e723e8d7ae1827f14b7b0218867667b1b12c88d", - "sha256:68a149a0482d0bc697aac702ec6efb9d380e0afebf9484db5b7e634146528371", - "sha256:6db7ded10b82592c472eeeba34b9f12d7b0ab1e2dcad12f081b08ebdea78d7d6", - "sha256:6e545908bcc2ae28e5b190ce3170f92d0438cf26a82b269611390114de0106eb", - "sha256:6f328a3faaf81a2546a3022b3dfc137cc6d50d81082dbc0c94d1678943f05df3", - "sha256:706e2dea3de33b0d8884c4d35ecd5911b4ff04d0697c4138096666ce983671a6", - "sha256:80c3d1ce8820dd819d1c9d6b63b6f445148480a831173b572a9174a55e7abd47", - "sha256:8111b61eee12d7af5c58f82f2c97c2664677a05df9225ef5cbc2f25398c8c454", - "sha256:9713578f187fb1c4d00ac554fe1edcc6b3ddd62f5d4eb578b81261115802df8e", - "sha256:9c0669ba9aebad540fb05a33beb7e659ea6e5ca35833fc5229c20f057db760e8", - "sha256:9e9cfe55dc7ac2aa47e0fd3285ff829685f96803197042c9d2f0fb44e4b39b2c", - "sha256:a22daaf30037b8e59d6968c76fe0f7ff062c976c7a026e92fbefc4c4bf3fc5a4", - "sha256:a25b84e10018875a0f294a7649d07c43e8bc3e6a821714e39e5cd607a36386d7", - "sha256:a71138366d57901597bfcc52af7f076ab61c046f409c7b429011cd68de8f9fe6", - "sha256:b4efde5524579a9ce0459ca35a57a48ca878a4973514b8bb88cb80d7c9d34c85", - "sha256:b78af4d42985ab3143d9882d0006f48d12f1bc4ba88e78f23762777c3ee64571", - "sha256:bb2987eb3af9bcf46019be39b82c120c3d35639a95bc4ee2d08f36ecdf469345", - "sha256:c03ce53690fe492845e14f4ab7e67d5a429a06db99b226b5c7caa23081c1e2bb", - "sha256:c59b9280284b791377b3524c8e39ca7b74ae2881ba1a6c51b36f4f1bb94cee49", - "sha256:d18b4c8cacbb141979bb44355ee5813dd4d307e9d79b3a36d66eca7e0a203df8", - "sha256:d1e5563e3b7f844dbc48d709c9e4a75647e11d0387cc1fa0c861d3e9d34bc844", - "sha256:d22c897b65b1408509099f1c3334bd3704f5e4eb7c0486c57d0e212f71cb8f54", - "sha256:dbec0a3a154dbf2eb85b38abaddf24964fa1c059ee0a4ad55d6f39211b1a4bca", - "sha256:ed123037896a8db6709b8ad5acc0ed435453726ea0b63361d12de369624c2ab5", - "sha256:f3614dabd2cc8741850597b418bcf644d4f60e73615906c3acc407b78ff720b3", - "sha256:f9d632ce9fd485119c968ec6a7a343de698c5e014d17602ae2f110f1b05925ed", - "sha256:fb62996c61eeff56b59ab8abfcaa0859ec2223392c03d6085048b576b567459b" - ], - "version": "==1.27.2" - }, - "grpcio-tools": { - "hashes": [ - "sha256:00c5080cfb197ed20ecf0d0ff2d07f1fc9c42c724cad21c40ff2d048de5712b1", - "sha256:069826dd02ce1886444cf4519c4fe1b05ac9ef41491f26e97400640531db47f6", - "sha256:1266b577abe7c720fd16a83d0a4999a192e87c4a98fc9f97e0b99b106b3e155f", - "sha256:16dc3fad04fe18d50777c56af7b2d9b9984cd1cfc71184646eb431196d1645c6", - "sha256:1de5a273eaffeb3d126a63345e9e848ea7db740762f700eb8b5d84c5e3e7687d", - "sha256:2ca280af2cae1a014a238057bd3c0a254527569a6a9169a01c07f0590081d530", - "sha256:43a1573400527a23e4174d88604fde7a9d9a69bf9473c21936b7f409858f8ebb", - "sha256:4698c6b6a57f73b14d91a542c69ff33a2da8729691b7060a5d7f6383624d045e", - "sha256:520b7dafddd0f82cb7e4f6e9c6ba1049aa804d0e207870def9fe7f94d1e14090", - "sha256:57f8b9e2c7f55cd45f6dd930d6de61deb42d3eb7f9788137fbc7155cf724132a", - "sha256:59fbeb5bb9a7b94eb61642ac2cee1db5233b8094ca76fc56d4e0c6c20b5dd85f", - "sha256:5fd7efc2fd3370bd2c72dc58f31a407a5dff5498befa145da211b2e8c6a52c63", - "sha256:6016c07d6566e3109a3c032cf3861902d66501ecc08a5a84c47e43027302f367", - "sha256:627c91923df75091d8c4d244af38d5ab7ed8d786d480751d6c2b9267fbb92fe0", - "sha256:69c4a63919b9007e845d9f8980becd2f89d808a4a431ca32b9723ee37b521cb1", - "sha256:77e25c241e33b75612f2aa62985f746c6f6803ec4e452da508bb7f8d90a69db4", - "sha256:7a2d5fb558ac153a326e742ebfd7020eb781c43d3ffd920abd42b2e6c6fdfb37", - "sha256:7b54b283ec83190680903a9037376dc915e1f03852a2d574ba4d981b7a1fd3d0", - "sha256:845a51305af9fc7f9e2078edaec9a759153195f6cf1fbb12b1fa6f077e56b260", - "sha256:84724458c86ff9b14c29b49e321f34d80445b379f4cd4d0494c694b49b1d6f88", - "sha256:87e8ca2c2d2d3e09b2a2bed5d740d7b3e64028dafb7d6be543b77eec85590736", - "sha256:8e7738a4b93842bca1158cde81a3587c9b7111823e40a1ddf73292ca9d58e08b", - "sha256:915a695bc112517af48126ee0ecdb6aff05ed33f3eeef28f0d076f1f6b52ef5e", - "sha256:99961156a36aae4a402d6b14c1e7efde642794b3ddbf32c51db0cb3a199e8b11", - "sha256:9ba88c2d99bcaf7b9cb720925e3290d73b2367d238c5779363fd5598b2dc98c7", - "sha256:a140bf853edb2b5e8692fe94869e3e34077d7599170c113d07a58286c604f4fe", - "sha256:a14dc7a36c845991d908a7179502ca47bcba5ae1817c4426ce68cf2c97b20ad9", - "sha256:a3d2aec4b09c8e59fee8b0d1ed668d09e8c48b738f03f5d8401d7eb409111c47", - "sha256:a8f892378b0b02526635b806f59141abbb429d19bec56e869e04f396502c9651", - "sha256:aaa5ae26883c3d58d1a4323981f96b941fa09bb8f0f368d97c6225585280cf04", - "sha256:b56caecc16307b088a431a4038c3b3bb7d0e7f9988cbd0e9fa04ac937455ea38", - "sha256:bd7f59ff1252a3db8a143b13ea1c1e93d4b8cf4b852eb48b22ef1e6942f62a84", - "sha256:c1bb8f47d58e9f7c4825abfe01e6b85eda53c8b31d2267ca4cddf3c4d0829b80", - "sha256:d1a5e5fa47ba9557a7d3b31605631805adc66cdba9d95b5d10dfc52cca1fed53", - "sha256:dcbc06556f3713a9348c4fce02d05d91e678fc320fb2bcf0ddf8e4bb11d17867", - "sha256:e17b2e0936b04ced99769e26111e1e86ba81619d1b2691b1364f795e45560953", - "sha256:e6932518db389ede8bf06b4119bbd3e17f42d4626e72dec2b8955b20ec732cb6", - "sha256:ea4b3ad696d976d5eac74ec8df9a2c692113e455446ee38d5b3bd87f8e034fa6", - "sha256:ee50b0cf0d28748ef9f941894eb50fc464bd61b8e96aaf80c5056bea9b80d580", - "sha256:ef624b6134aef737b3daa4fb7e806cb8c5749efecd0b1fa9ce4f7e060c7a0221", - "sha256:f5450aa904e720f9c6407b59e96a8951ed6a95463f49444b6d2594b067d39588", - "sha256:f8514453411d72cc3cf7d481f2b6057e5b7436736d0cd39ee2b2f72088bbf497", - "sha256:fae91f30dc050a8d0b32d20dc700e6092f0bd2138d83e9570fff3f0372c1b27e" - ], - "index": "pypi", - "version": "==1.27.2" - }, - "identify": { - "hashes": [ - "sha256:a7577a1f55cee1d21953a5cf11a3c839ab87f5ef909a4cba6cf52ed72b4c6059", - "sha256:ab246293e6585a1c6361a505b68d5b501a0409310932b7de2c2ead667b564d89" - ], - "version": "==1.4.13" - }, - "importlib-metadata": { - "hashes": [ - "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", - "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" - ], - "markers": "python_version < '3.8'", - "version": "==1.6.0" - }, - "importlib-resources": { - "hashes": [ - "sha256:4019b6a9082d8ada9def02bece4a76b131518866790d58fdda0b5f8c603b36c2", - "sha256:dd98ceeef3f5ad2ef4cc287b8586da4ebad15877f351e9688987ad663a0a29b8" - ], - "markers": "python_version < '3.7'", - "version": "==1.4.0" - }, - "isort": { - "hashes": [ - "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", - "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" - ], - "index": "pypi", - "version": "==4.3.21" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "mock": { - "hashes": [ - "sha256:3f9b2c0196c60d21838f307f5825a7b86b678cedc58ab9e50a8988187b4d81e0", - "sha256:dd33eb70232b6118298d516bbcecd26704689c386594f0f3c4f13867b2c56f72" - ], - "index": "pypi", - "version": "==4.0.2" - }, - "more-itertools": { - "hashes": [ - "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", - "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507" - ], - "version": "==8.2.0" - }, - "nodeenv": { - "hashes": [ - "sha256:5b2438f2e42af54ca968dd1b374d14a1194848955187b0e5e4be1f73813a5212" - ], - "version": "==1.3.5" - }, - "packaging": { - "hashes": [ - "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", - "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752" - ], - "version": "==20.3" - }, - "pluggy": { - "hashes": [ - "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", - "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" - ], - "version": "==0.13.1" - }, - "pre-commit": { - "hashes": [ - "sha256:487c675916e6f99d355ec5595ad77b325689d423ef4839db1ed2f02f639c9522", - "sha256:c0aa11bce04a7b46c5544723aedf4e81a4d5f64ad1205a30a9ea12d5e81969e1" - ], - "index": "pypi", - "version": "==2.2.0" - }, - "protobuf": { - "hashes": [ - "sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab", - "sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f", - "sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a", - "sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0", - "sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4", - "sha256:7774bbbaac81d3ba86de646c39f154afc8156717972bf0450c9dbfa1dc8dbea2", - "sha256:82d7ac987715d8d1eb4068bf997f3053468e0ce0287e2729c30601feb6602fee", - "sha256:8eb9c93798b904f141d9de36a0ba9f9b73cc382869e67c9e642c0aba53b0fc07", - "sha256:adf0e4d57b33881d0c63bb11e7f9038f98ee0c3e334c221f0858f826e8fb0151", - "sha256:c40973a0aee65422d8cb4e7d7cbded95dfeee0199caab54d5ab25b63bce8135a", - "sha256:c77c974d1dadf246d789f6dad1c24426137c9091e930dbf50e0a29c1fcf00b1f", - "sha256:dd9aa4401c36785ea1b6fff0552c674bdd1b641319cb07ed1fe2392388e9b0d7", - "sha256:e11df1ac6905e81b815ab6fd518e79be0a58b5dc427a2cf7208980f30694b956", - "sha256:e2f8a75261c26b2f5f3442b0525d50fd79a71aeca04b5ec270fc123536188306", - "sha256:e512b7f3a4dd780f59f1bf22c302740e27b10b5c97e858a6061772668cd6f961", - "sha256:ef2c2e56aaf9ee914d3dccc3408d42661aaf7d9bb78eaa8f17b2e6282f214481", - "sha256:fac513a9dc2a74b99abd2e17109b53945e364649ca03d9f7a0b96aa8d1807d0a", - "sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80" - ], - "version": "==3.11.3" - }, - "py": { - "hashes": [ - "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", - "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" - ], - "version": "==1.8.1" - }, - "pycodestyle": { - "hashes": [ - "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", - "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" - ], - "version": "==2.5.0" - }, - "pyflakes": { - "hashes": [ - "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", - "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" - ], - "version": "==2.1.1" - }, - "pyparsing": { - "hashes": [ - "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", - "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" - ], - "version": "==2.4.6" - }, - "pytest": { - "hashes": [ - "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172", - "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970" - ], - "index": "pypi", - "version": "==5.4.1" - }, - "pyyaml": { - "hashes": [ - "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", - "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", - "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", - "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", - "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", - "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", - "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", - "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", - "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", - "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", - "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" - ], - "version": "==5.3.1" - }, - "six": { - "hashes": [ - "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", - "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" - ], - "version": "==1.14.0" - }, - "toml": { - "hashes": [ - "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" - ], - "version": "==0.10.0" - }, - "virtualenv": { - "hashes": [ - "sha256:4e399f48c6b71228bf79f5febd27e3bbb753d9d5905776a86667bc61ab628a25", - "sha256:9e81279f4a9d16d1c0654a127c2c86e5bca2073585341691882c1e66e31ef8a5" - ], - "version": "==20.0.15" - }, - "wcwidth": { - "hashes": [ - "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", - "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" - ], - "version": "==0.1.9" - }, - "zipp": { - "hashes": [ - "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", - "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" - ], - "markers": "python_version < '3.8'", - "version": "==3.1.0" - } - } -} From cb66ba60a6eb8195b2f8136e75aa4e8c891ef83f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 08:54:08 -0700 Subject: [PATCH 0494/1131] removed kernel-modules-extra, so it is only attempted in centos8 --- tasks.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tasks.py b/tasks.py index b4f37679..bdabc837 100644 --- a/tasks.py +++ b/tasks.py @@ -81,8 +81,7 @@ def install_system(c: Context, os_info: OsInfo, hide: bool) -> None: elif os_info.like == OsLike.REDHAT: c.run( "sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel " - "iptables-ebtables iproute python3-devel python3-tkinter tk ethtool make " - "kernel-modules-extra", + "iptables-ebtables iproute python3-devel python3-tkinter tk ethtool make", hide=hide ) # centos 8+ does not support netem by default From e283c5ec7d0624d2931424fefafe1e41f842017d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 09:24:59 -0700 Subject: [PATCH 0495/1131] broke out invoke tasks for installing scripts and service, testing centos not needing prefix --- tasks.py | 69 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/tasks.py b/tasks.py index bdabc837..9168542c 100644 --- a/tasks.py +++ b/tasks.py @@ -115,7 +115,8 @@ def install_grpcio(c: Context, hide: bool) -> None: def build(c: Context, os_info: OsInfo, hide: bool) -> None: print("building core...") c.run("./bootstrap.sh", hide=hide) - prefix = "--prefix=/usr" if os_info.like == OsLike.REDHAT else "" + # prefix = "--prefix=/usr" if os_info.like == OsLike.REDHAT else "" + prefix = "" c.run(f"./configure {prefix}", hide=hide) c.run("make -j$(nproc)", hide=hide) @@ -169,7 +170,43 @@ def install_ospf_mdr(c: Context, os_info: OsInfo, hide: bool) -> None: c.run("sudo make install", hide=hide) -def install_files(c: Context, hide: bool, prefix: str = DEFAULT_PREFIX) -> None: +@task +def install_service(c, hide, prefix=DEFAULT_PREFIX): + """ + install systemd core service + """ + # install service + bin_dir = Path(prefix).joinpath("bin") + systemd_dir = Path("/lib/systemd/system/") + service_file = systemd_dir.joinpath("core-daemon.service") + if systemd_dir.exists(): + print(f"installing core-daemon.service for systemd to {service_file}") + service_data = inspect.cleandoc(f""" + [Unit] + Description=Common Open Research Emulator Service + After=network.target + + [Service] + Type=simple + ExecStart={bin_dir}/core-daemon + TasksMax=infinity + + [Install] + WantedBy=multi-user.target + """) + temp = NamedTemporaryFile("w", delete=False) + temp.write(service_data) + temp.close() + c.run(f"sudo cp {temp.name} {service_file}", hide=hide) + else: + print(f"ERROR: systemd service path not found: {systemd_dir}") + + +@task +def install_scripts(c, hide, prefix=DEFAULT_PREFIX): + """ + install core script files, modified to leverage virtual environment + """ # install all scripts python = get_python(c) bin_dir = Path(prefix).joinpath("bin") @@ -200,34 +237,11 @@ def install_files(c: Context, hide: bool, prefix: str = DEFAULT_PREFIX) -> None: c.run(f"sudo cp -n daemon/data/core.conf {config_dir}", hide=hide) c.run(f"sudo cp -n daemon/data/logging.conf {config_dir}", hide=hide) - # install service - systemd_dir = Path("/lib/systemd/system/") - service_file = systemd_dir.joinpath("core-daemon.service") - if systemd_dir.exists(): - print(f"installing core-daemon.service for systemd to {service_file}") - service_data = inspect.cleandoc(f""" - [Unit] - Description=Common Open Research Emulator Service - After=network.target - - [Service] - Type=simple - ExecStart={bin_dir}/core-daemon - TasksMax=infinity - - [Install] - WantedBy=multi-user.target - """) - temp = NamedTemporaryFile("w", delete=False) - temp.write(service_data) - temp.close() - c.run(f"sudo cp {temp.name} {service_file}", hide=hide) - @task def install(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): """ - install core + install core, poetry, scripts, service, and ospf mdr """ print(f"installing core with prefix: {prefix}") hide = not verbose @@ -237,7 +251,8 @@ def install(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): build(c, os_info, hide) install_core(c, hide) install_poetry(c, dev, hide) - install_files(c, hide, prefix) + install_scripts(c, hide, prefix) + install_service(c, hide, prefix) install_ospf_mdr(c, os_info, hide) print("please open a new terminal or re-login to leverage invoke for running core") print("# run daemon") From 5d23be4a9d26e326d9c3468ac4c4cdd67199231b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 09:30:04 -0700 Subject: [PATCH 0496/1131] updates to install2.md to replace install docs --- docs/install2.md | 130 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 126 insertions(+), 4 deletions(-) diff --git a/docs/install2.md b/docs/install2.md index 86e3db92..921c80f0 100644 --- a/docs/install2.md +++ b/docs/install2.md @@ -1,4 +1,7 @@ -# Installing CORE +# CORE Installation + +* Table of Contents +{:toc} ## Overview @@ -14,6 +17,11 @@ The following tools will be leveraged during installation: |invoke|used to run provided tasks (install, daemon, gui, tests, etc)| |poetry|used to install the managed python virtual environment for running CORE| +## 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. @@ -28,16 +36,130 @@ Verified: > **NOTE:** CentOS 8 does not provide legacy ebtables support, WLAN will not > function properly -## Running Installation +## Utility Requirements + +* iproute2 4.5+ is a requirement for bridge related commands +* ebtables not backed by nftables + +## Automated Installation + +> **NOTE:** installs OSPF MDR +> **NOTE:** sets up script files using the prefix provided +> **NOTE:** install a systemd service file to /lib/systemd/system/core-daemon.service ```shell # clone CORE repo git clone https://github.com/coreemu/core.git cd core -git checkout enhancement/poetry-invoke # run install script -./install2.sh +# script usage: install.sh [-d] [-v] +# +# -v enable verbose install +# -d enable developer install +# -p install prefix, defaults to /usr/local +./install.sh +``` + +## Manual Installation + +> **NOTE:** install OSPF MDR by manual instructions below + +```shell +# clone CORE repo +git clone https://github.com/coreemu/core.git +cd core + +# install python3 and venv support +# ubuntu +sudo apt install -y python3-pip python3-venv +# centos +sudo yum install -y python3-pip + +# install system dependencies +# ubuntu +sudo apt install -y automake pkg-config gcc libev-dev ebtables iproute2 \ + ethtool tk python3-tk +# centos +sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel \ + iptables-ebtables iproute python3-devel python3-tkinter tk ethtool \ + make kernel-modules-extra + +# install grpcio-tools +python3 -m pip install --user grpcio==1.27.2 grpcio-tools==1.27.2 + +# build core +./bootstrap.sh +./configure +make +sudo make install + +# install pipx, may need to restart terminal after ensurepath +python3 -m pip install --user pipx +python3 -m pipx ensurepath + +# install poetry +pipx install poetry + +# install poetry virtual environment +cd daemon +poetry install --no-dev +cd .. + +# install invoke to run helper tasks +pipx install invoke + +# install core scripts leveraging poetry virtual environment +inv install-scripts + +# optionally install systemd service file +inv install-service +``` + +## Manually Install OSPF MDR (Routing Support) + +Virtual networks generally require some form of routing in order to work (e.g. to automatically populate routing +tables for routing packets from one subnet to another.) CORE builds OSPF routing protocol configurations by +default when the blue router node type is used. + +* [OSPF MANET Designated Routers](https://github.com/USNavalResearchLaboratory/ospf-mdr) (MDR) - the Quagga routing +suite with a modified version of OSPFv3, optimized for use with mobile wireless networks. The **mdr** node type +(and the MDR service) requires this variant of Quagga. + +```shell +# system dependencies +# ubuntu +sudo apt install -y libtool gawk libreadline-dev +# centos +sudo yum install -y libtool gawk readline-devel + +# build and install +git clone https://github.com/USNavalResearchLaboratory/ospf-mdr +cd ospf-mdr +./bootstrap.sh +./configure --disable-doc --enable-user=root --enable-group=root --with-cflags=-ggdb \ + --sysconfdir=/usr/local/etc/quagga --enable-vtysh \ + --localstatedir=/var/run/quagga +make +sudo make install +``` + +## Manually Install EMANE + +EMANE can be installed from deb or RPM packages or from source. See the +[EMANE GitHub](https://github.com/adjacentlink/emane) for full details. + +Here are quick instructions for installing all EMANE packages for Ubuntu 18.04: +```shell +# install dependencies +# ubuntu +sudo apt-get install libssl-dev libxml-libxml-perl libxml-simple-perl +wget https://adjacentlink.com/downloads/emane/emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz +tar xzf emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz +# install base emane packages +sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/emane*.deb +# install python3 bindings +sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/python3*.deb ``` ## Using Invoke Tasks From 6c7e760f4ea9e986a26ef785f982b9705358da54 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 09:31:05 -0700 Subject: [PATCH 0497/1131] tweak to install doc --- docs/install2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/install2.md b/docs/install2.md index 921c80f0..9d8d97a4 100644 --- a/docs/install2.md +++ b/docs/install2.md @@ -44,7 +44,9 @@ Verified: ## Automated Installation > **NOTE:** installs OSPF MDR + > **NOTE:** sets up script files using the prefix provided + > **NOTE:** install a systemd service file to /lib/systemd/system/core-daemon.service ```shell From 35b6f5297a95faeb2700b1d1ea382d7df05cb14d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 09:42:14 -0700 Subject: [PATCH 0498/1131] update doc and install to properly set and provide options for OSs like centos who need a different prefix --- docs/install2.md | 3 +++ tasks.py | 11 +++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/install2.md b/docs/install2.md index 9d8d97a4..1eb94695 100644 --- a/docs/install2.md +++ b/docs/install2.md @@ -92,6 +92,7 @@ python3 -m pip install --user grpcio==1.27.2 grpcio-tools==1.27.2 # build core ./bootstrap.sh +# centos requires --prefix=/usr ./configure make sudo make install @@ -112,9 +113,11 @@ cd .. pipx install invoke # install core scripts leveraging poetry virtual environment +# centos requires --prefix=/usr inv install-scripts # optionally install systemd service file +# centos requires --prefix=/usr inv install-service ``` diff --git a/tasks.py b/tasks.py index 9168542c..5c335644 100644 --- a/tasks.py +++ b/tasks.py @@ -115,8 +115,7 @@ def install_grpcio(c: Context, hide: bool) -> None: def build(c: Context, os_info: OsInfo, hide: bool) -> None: print("building core...") c.run("./bootstrap.sh", hide=hide) - # prefix = "--prefix=/usr" if os_info.like == OsLike.REDHAT else "" - prefix = "" + prefix = "--prefix=/usr" if os_info.like == OsLike.REDHAT else "" c.run(f"./configure {prefix}", hide=hide) c.run("make -j$(nproc)", hide=hide) @@ -171,11 +170,11 @@ def install_ospf_mdr(c: Context, os_info: OsInfo, hide: bool) -> None: @task -def install_service(c, hide, prefix=DEFAULT_PREFIX): +def install_service(c, verbose=False, prefix=DEFAULT_PREFIX): """ install systemd core service """ - # install service + hide = not verbose bin_dir = Path(prefix).joinpath("bin") systemd_dir = Path("/lib/systemd/system/") service_file = systemd_dir.joinpath("core-daemon.service") @@ -203,11 +202,11 @@ def install_service(c, hide, prefix=DEFAULT_PREFIX): @task -def install_scripts(c, hide, prefix=DEFAULT_PREFIX): +def install_scripts(c, verbose=False, prefix=DEFAULT_PREFIX): """ install core script files, modified to leverage virtual environment """ - # install all scripts + hide = not verbose python = get_python(c) bin_dir = Path(prefix).joinpath("bin") for script in Path("daemon/scripts").iterdir(): From 08105cf4b3603fd134bdfe46208c87dc0cbde5bc Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 09:44:28 -0700 Subject: [PATCH 0499/1131] updated list of invoke tasks in doc --- docs/install2.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/install2.md b/docs/install2.md index 1eb94695..42716fd3 100644 --- a/docs/install2.md +++ b/docs/install2.md @@ -175,13 +175,17 @@ CORE tasks to help ensure usage of the create python virtual environment. ```shell Available tasks: - cleanup run core-cleanup removing leftover core nodes, bridges, directories - daemon start core-daemon - gui start core-pygui - install install core - test run core tests - test-emane run core emane tests - test-mock run core tests using mock to avoid running as sudo + cleanup run core-cleanup removing leftover core nodes, bridges, directories + cli run core-cli used to query and modify a running session + daemon start core-daemon + gui start core-pygui + install install core, poetry, scripts, service, and ospf mdr + install-scripts install core script files, modified to leverage virtual environment + install-service install systemd core service + test run core tests + test-emane run core emane tests + test-mock run core tests using mock to avoid running as sudo + uninstall uninstall core ``` Example running the core-daemon task from the root of the repo: From 6b5aaa6b19ca09852bacc6da360d3a5df9a16859 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 10:01:57 -0700 Subject: [PATCH 0500/1131] adjust how invoke install prefix is used for core configure --- tasks.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tasks.py b/tasks.py index 5c335644..9da66faa 100644 --- a/tasks.py +++ b/tasks.py @@ -112,11 +112,10 @@ def install_grpcio(c: Context, hide: bool) -> None: ) -def build(c: Context, os_info: OsInfo, hide: bool) -> None: +def build(c: Context, hide: bool, prefix: str = DEFAULT_PREFIX) -> None: print("building core...") c.run("./bootstrap.sh", hide=hide) - prefix = "--prefix=/usr" if os_info.like == OsLike.REDHAT else "" - c.run(f"./configure {prefix}", hide=hide) + c.run(f"./configure --prefix={prefix}", hide=hide) c.run("make -j$(nproc)", hide=hide) @@ -247,7 +246,7 @@ def install(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): os_info = get_os() install_system(c, os_info, hide) install_grpcio(c, hide) - build(c, os_info, hide) + build(c, hide, prefix) install_core(c, hide) install_poetry(c, dev, hide) install_scripts(c, hide, prefix) From dfb3e0c4242c209ed35c4eb6f63f3f284696ab83 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 10:42:24 -0700 Subject: [PATCH 0501/1131] default install docs and script to poetry based installs --- docs/install.md | 377 ++++++++++++++++++----------------------------- docs/install2.md | 212 -------------------------- install.sh | 166 ++++----------------- install2.sh | 57 ------- 4 files changed, 175 insertions(+), 637 deletions(-) delete mode 100644 docs/install2.md delete mode 100755 install2.sh diff --git a/docs/install.md b/docs/install.md index 6abd0945..42716fd3 100644 --- a/docs/install.md +++ b/docs/install.md @@ -5,102 +5,123 @@ ## Overview -This section will describe how to install CORE from source or from a pre-built package. -CORE has been vetted on Ubuntu 18 and CentOS 7.6. Other versions and distributions -can work, assuming you can get the required packages and versions similar to those -noted below for the tested distributions. +CORE provides a script to help automate installing all required software +to build and run, including a python virtual environment to run it all in. -> **NOTE:** iproute2 4.5+ is a requirement for bridge related commands +The following tools will be leveraged during installation: + +|Tool|Description| +|---|---| +|pip|used to install pipx| +|pipx|used to install standalone python tools (invoke, poetry)| +|invoke|used to run provided tasks (install, daemon, gui, tests, etc)| +|poetry|used to install the managed python virtual environment for running CORE| ## 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. -## Operating System +## Supported Linux Distributions -CORE requires a Linux operating system because it uses namespacing provided by the kernel. It does not run on -Windows or Mac OS X operating systems (unless it is running within a virtual machine guest.) The -technology that CORE currently uses is Linux network namespaces. +Plan is to support recent Ubuntu and CentOS LTS releases. -Ubuntu and CentOS Linux are the recommended distributions for running CORE. However, these distributions are -not strictly required. CORE will likely work on other flavors of Linux as well, assuming dependencies are met. +Verified: +* Ubuntu - 18.04, 20.04 +* CentOS - 7.8, 8.0* -> **NOTE:** CORE Services determine what run on each node. You may require other software packages depending on the -services you wish to use. For example, the HTTP service will require the apache2 package. +> **NOTE:** Ubuntu 20.04 requires installing legacy ebtables for WLAN +> functionality -## Installed Files +> **NOTE:** CentOS 8 does not provide legacy ebtables support, WLAN will not +> function properly -CORE files are installed to the following directories by default, when the installation prefix is **/usr**. +## Utility Requirements -Install Path | Description --------------|------------ -/usr/bin/core-gui|GUI startup command -/usr/bin/coretk-gui|BETA Python GUI -/usr/bin/core-daemon|Daemon startup command -/usr/bin/{core-cleanup, coresendmsg, core-manage}|Misc. helper commands/scripts -/usr/lib/core|GUI files -/usr/lib/python{3.6+}/dist-packages/core|Python modules for daemon/scripts -/etc/core/|Daemon and log configuration files -~/.core/|User-specific GUI preferences and scenario files -/usr/share/core/|Example scripts and scenarios -/usr/share/man/man1/|Command man pages -/etc/init.d/core-daemon|SysV startup script for daemon -/usr/lib/systemd/system/core-daemon.service|Systemd startup script for daemon +* iproute2 4.5+ is a requirement for bridge related commands +* ebtables not backed by nftables -## Automated Install +## Automated Installation -There is a helper script in the root of the repository that can help automate -the CORE installation. Some steps require commands be ran as sudo and you -will be prompted for a password. This should work on Ubuntu/CentOS and will -install system dependencies, python dependencies, and CORE. This will target -system installations of python 3.6. +> **NOTE:** installs OSPF MDR + +> **NOTE:** sets up script files using the prefix provided + +> **NOTE:** install a systemd service file to /lib/systemd/system/core-daemon.service ```shell +# clone CORE repo git clone https://github.com/coreemu/core.git cd core + +# run install script +# script usage: install.sh [-d] [-v] +# +# -v enable verbose install +# -d enable developer install +# -p install prefix, defaults to /usr/local ./install.sh ``` -You can target newer system python versions using the **-v** flag. Assuming -these versions are actually available on your system. +## Manual Installation + +> **NOTE:** install OSPF MDR by manual instructions below ```shell -# ubuntu 3.7 -./install.sh -v 3.7 -# centos 3.7 -./install.sh -v 37 +# clone CORE repo +git clone https://github.com/coreemu/core.git +cd core + +# install python3 and venv support +# ubuntu +sudo apt install -y python3-pip python3-venv +# centos +sudo yum install -y python3-pip + +# install system dependencies +# ubuntu +sudo apt install -y automake pkg-config gcc libev-dev ebtables iproute2 \ + ethtool tk python3-tk +# centos +sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel \ + iptables-ebtables iproute python3-devel python3-tkinter tk ethtool \ + make kernel-modules-extra + +# install grpcio-tools +python3 -m pip install --user grpcio==1.27.2 grpcio-tools==1.27.2 + +# build core +./bootstrap.sh +# centos requires --prefix=/usr +./configure +make +sudo make install + +# install pipx, may need to restart terminal after ensurepath +python3 -m pip install --user pipx +python3 -m pipx ensurepath + +# install poetry +pipx install poetry + +# install poetry virtual environment +cd daemon +poetry install --no-dev +cd .. + +# install invoke to run helper tasks +pipx install invoke + +# install core scripts leveraging poetry virtual environment +# centos requires --prefix=/usr +inv install-scripts + +# optionally install systemd service file +# centos requires --prefix=/usr +inv install-service ``` -## Pre-Req Installing Python - -Python 3.6 is the minimum required python version. Newer versions can be used if available. -These steps are needed, since the system packages can not provide all the -dependencies needed by CORE. - -### Ubuntu - -```shell -sudo apt install python3.6 -sudo apt install python3-pip -``` - -### CentOS - -```shell -sudo yum install python36 -sudo yum install python3-pip -``` - -### Dependencies - -Install the current python dependencies. - -```shell -sudo python3 -m pip install -r requirements.txt -``` - -## Pre-Req Installing OSPF MDR +## Manually Install OSPF MDR (Routing Support) Virtual networks generally require some form of routing in order to work (e.g. to automatically populate routing tables for routing packets from one subnet to another.) CORE builds OSPF routing protocol configurations by @@ -110,21 +131,14 @@ default when the blue router node type is used. suite with a modified version of OSPFv3, optimized for use with mobile wireless networks. The **mdr** node type (and the MDR service) requires this variant of Quagga. -### Ubuntu - ```shell -sudo apt install libtool gawk libreadline-dev -``` +# system dependencies +# ubuntu +sudo apt install -y libtool gawk libreadline-dev +# centos +sudo yum install -y libtool gawk readline-devel -### CentOS - -```shell -sudo yum install libtool gawk readline-devel -``` - -### Build and Install - -```shell +# build and install git clone https://github.com/USNavalResearchLaboratory/ospf-mdr cd ospf-mdr ./bootstrap.sh @@ -135,167 +149,64 @@ make sudo make install ``` -Note that the configuration directory */usr/local/etc/quagga* shown for Quagga above could be */etc/quagga*, -if you create a symbolic link from */etc/quagga/Quagga.conf -> /usr/local/etc/quagga/Quagga.conf* on the host. -The *quaggaboot.sh* script in a Linux network namespace will try and do this for you if needed. +## Manually Install EMANE -If you try to run quagga after installing from source and get an error such as: +EMANE can be installed from deb or RPM packages or from source. See the +[EMANE GitHub](https://github.com/adjacentlink/emane) for full details. +Here are quick instructions for installing all EMANE packages for Ubuntu 18.04: ```shell -error while loading shared libraries libzebra.so.0 +# install dependencies +# ubuntu +sudo apt-get install libssl-dev libxml-libxml-perl libxml-simple-perl +wget https://adjacentlink.com/downloads/emane/emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz +tar xzf emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz +# install base emane packages +sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/emane*.deb +# install python3 bindings +sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/python3*.deb ``` -this is usually a sign that you have to run ```sudo ldconfig```` to refresh the cache file. +## Using Invoke Tasks -## Installing from Packages - -The easiest way to install CORE is using the pre-built packages. The package managers on Ubuntu or CentOS -will help in automatically installing most dependencies, except for the python ones described previously. - -You can obtain the CORE packages from [CORE Releases](https://github.com/coreemu/core/releases). - -### Ubuntu - -Ubuntu package defaults to using systemd for running as a service. +The invoke tool installed by way of pipx provides conveniences for running +CORE tasks to help ensure usage of the create python virtual environment. ```shell -sudo apt install ./core_$VERSION_amd64.deb +Available tasks: + + cleanup run core-cleanup removing leftover core nodes, bridges, directories + cli run core-cli used to query and modify a running session + daemon start core-daemon + gui start core-pygui + install install core, poetry, scripts, service, and ospf mdr + install-scripts install core script files, modified to leverage virtual environment + install-service install systemd core service + test run core tests + test-emane run core emane tests + test-mock run core tests using mock to avoid running as sudo + uninstall uninstall core ``` -### CentOS +Example running the core-daemon task from the root of the repo: +```shell +inv daemon +``` -**NOTE: tkimg is not required for the core-gui, but if you get an error message about it you can install the package -on CentOS <= 6, or build from source otherwise** +Some tasks are wrappers around command line tools and requires running +them with a slight variation for compatibility. You can enter the +poetry shell to run the script natively. ```shell -yum install ./core_$VERSION_x86_64.rpm -``` - -Disabling SELINUX: - -```shell -# change the following in /etc/sysconfig/selinux -SELINUX=disabled - -# add the following to the kernel line in /etc/grub.conf -selinux=0 -``` - -Turn off firewalls: - -```shell -systemctl disable firewalld -systemctl disable iptables.service -systemctl disable ip6tables.service -chkconfig iptables off -chkconfig ip6tables off -``` - -You need to reboot after making these changes, or flush the firewall using - -```shell -iptables -F -ip6tables -F -``` - -## Installing from Source - -Steps for building from cloned source code. Python 3.6 is the minimum required version -a newer version can be used below if available. - -### Distro Requirements - -System packages required to build from source. - -#### Ubuntu - -```shell -sudo apt install git automake pkg-config gcc libev-dev ebtables iproute2 \ - python3.6 python3.6-dev python3-pip python3-tk tk libtk-img ethtool autoconf -``` - -#### CentOS - -```shell -sudo yum install git automake pkgconf-pkg-config gcc gcc-c++ libev-devel iptables-ebtables iproute \ - python36 python36-devel python3-pip python3-tkinter tk ethtool autoconf -``` - -### Clone Repository - -Clone the CORE repository for building from source. - -```shell -git clone https://github.com/coreemu/core.git -``` - -### Install grpcio-tools - -Python module grpcio-tools is currently needed to generate gRPC protobuf code. -Specifically leveraging 1.27.2 as that is what will be used during runtime. - -```shell -python3 -m pip install --user grpcio==1.27.2 grpcio-tools==1.27.2 -``` - -### Build and Install - -```shell -./bootstrap.sh -./configure -make -sudo make install -``` - -## Building Documentation - -Building documentation requires python-sphinx not noted above. - -```shell -sudo apt install python3-sphinx -sudo yum install python3-sphinx - -./bootstrap.sh -./configure -make doc -``` - -## Building Packages -Build package commands, DESTDIR is used to make install into and then for packaging by fpm. - -**NOTE: clean the DESTDIR if re-using the same directory** - -* Install [fpm](http://fpm.readthedocs.io/en/latest/installing.html) - -```shell -./bootstrap.sh -./configure -make -mkdir /tmp/core-build -make fpm DESTDIR=/tmp/core-build -``` - -This will produce and RPM and Deb package for the currently configured python version. - -## Running CORE - -Start the CORE daemon. - -```shell -# systemd -sudo systemctl daemon-reload -sudo systemctl start core-daemon - -# sysv -sudo service core-daemon start -``` - -Run the GUI - -```shell -# default gui -core-gui - -# new beta gui -coretk-gui +# running core-cli as a task requires all options to be provided +# within a string +inv cli "query session -i 1" + +# entering the poetry shell to use core-cli natively +cd $REPO/daemon +poetry shell +core-cli query session -i 1 + +# exit the shell +exit ``` diff --git a/docs/install2.md b/docs/install2.md deleted file mode 100644 index 42716fd3..00000000 --- a/docs/install2.md +++ /dev/null @@ -1,212 +0,0 @@ -# CORE Installation - -* Table of Contents -{:toc} - -## Overview - -CORE provides a script to help automate installing all required software -to build and run, including a python virtual environment to run it all in. - -The following tools will be leveraged during installation: - -|Tool|Description| -|---|---| -|pip|used to install pipx| -|pipx|used to install standalone python tools (invoke, poetry)| -|invoke|used to run provided tasks (install, daemon, gui, tests, etc)| -|poetry|used to install the managed python virtual environment for running CORE| - -## 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 - -## Utility Requirements - -* iproute2 4.5+ is a requirement for bridge related commands -* ebtables not backed by nftables - -## Automated Installation - -> **NOTE:** installs OSPF MDR - -> **NOTE:** sets up script files using the prefix provided - -> **NOTE:** install a systemd service file to /lib/systemd/system/core-daemon.service - -```shell -# clone CORE repo -git clone https://github.com/coreemu/core.git -cd core - -# run install script -# script usage: install.sh [-d] [-v] -# -# -v enable verbose install -# -d enable developer install -# -p install prefix, defaults to /usr/local -./install.sh -``` - -## Manual Installation - -> **NOTE:** install OSPF MDR by manual instructions below - -```shell -# clone CORE repo -git clone https://github.com/coreemu/core.git -cd core - -# install python3 and venv support -# ubuntu -sudo apt install -y python3-pip python3-venv -# centos -sudo yum install -y python3-pip - -# install system dependencies -# ubuntu -sudo apt install -y automake pkg-config gcc libev-dev ebtables iproute2 \ - ethtool tk python3-tk -# centos -sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel \ - iptables-ebtables iproute python3-devel python3-tkinter tk ethtool \ - make kernel-modules-extra - -# install grpcio-tools -python3 -m pip install --user grpcio==1.27.2 grpcio-tools==1.27.2 - -# build core -./bootstrap.sh -# centos requires --prefix=/usr -./configure -make -sudo make install - -# install pipx, may need to restart terminal after ensurepath -python3 -m pip install --user pipx -python3 -m pipx ensurepath - -# install poetry -pipx install poetry - -# install poetry virtual environment -cd daemon -poetry install --no-dev -cd .. - -# install invoke to run helper tasks -pipx install invoke - -# install core scripts leveraging poetry virtual environment -# centos requires --prefix=/usr -inv install-scripts - -# optionally install systemd service file -# centos requires --prefix=/usr -inv install-service -``` - -## Manually Install OSPF MDR (Routing Support) - -Virtual networks generally require some form of routing in order to work (e.g. to automatically populate routing -tables for routing packets from one subnet to another.) CORE builds OSPF routing protocol configurations by -default when the blue router node type is used. - -* [OSPF MANET Designated Routers](https://github.com/USNavalResearchLaboratory/ospf-mdr) (MDR) - the Quagga routing -suite with a modified version of OSPFv3, optimized for use with mobile wireless networks. The **mdr** node type -(and the MDR service) requires this variant of Quagga. - -```shell -# system dependencies -# ubuntu -sudo apt install -y libtool gawk libreadline-dev -# centos -sudo yum install -y libtool gawk readline-devel - -# build and install -git clone https://github.com/USNavalResearchLaboratory/ospf-mdr -cd ospf-mdr -./bootstrap.sh -./configure --disable-doc --enable-user=root --enable-group=root --with-cflags=-ggdb \ - --sysconfdir=/usr/local/etc/quagga --enable-vtysh \ - --localstatedir=/var/run/quagga -make -sudo make install -``` - -## Manually Install EMANE - -EMANE can be installed from deb or RPM packages or from source. See the -[EMANE GitHub](https://github.com/adjacentlink/emane) for full details. - -Here are quick instructions for installing all EMANE packages for Ubuntu 18.04: -```shell -# install dependencies -# ubuntu -sudo apt-get install libssl-dev libxml-libxml-perl libxml-simple-perl -wget https://adjacentlink.com/downloads/emane/emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz -tar xzf emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz -# install base emane packages -sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/emane*.deb -# install python3 bindings -sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/python3*.deb -``` - -## Using Invoke Tasks - -The invoke tool installed by way of pipx provides conveniences for running -CORE tasks to help ensure usage of the create python virtual environment. - -```shell -Available tasks: - - cleanup run core-cleanup removing leftover core nodes, bridges, directories - cli run core-cli used to query and modify a running session - daemon start core-daemon - gui start core-pygui - install install core, poetry, scripts, service, and ospf mdr - install-scripts install core script files, modified to leverage virtual environment - install-service install systemd core service - test run core tests - test-emane run core emane tests - test-mock run core tests using mock to avoid running as sudo - uninstall uninstall core -``` - -Example running the core-daemon task from the root of the repo: -```shell -inv daemon -``` - -Some tasks are wrappers around command line tools and requires running -them with a slight variation for compatibility. You can enter the -poetry shell to run the script natively. - -```shell -# running core-cli as a task requires all options to be provided -# within a string -inv cli "query session -i 1" - -# entering the poetry shell to use core-cli natively -cd $REPO/daemon -poetry shell -core-cli query session -i 1 - -# exit the shell -exit -``` diff --git a/install.sh b/install.sh index 6cc193ed..5e5a9b11 100755 --- a/install.sh +++ b/install.sh @@ -3,57 +3,6 @@ # exit on error set -e -ubuntu_py=3.6 -centos_py=36 -reinstall= - -function install_python_depencencies() { - sudo python3 -m pip install -r daemon/requirements.txt -} - -function install_grpcio_tools() { - python3 -m pip install --only-binary ":all:" --user grpcio-tools -} - -function install_ospf_mdr() { - rm -rf /tmp/ospf-mdr - git clone https://github.com/USNavalResearchLaboratory/ospf-mdr /tmp/ospf-mdr - cd /tmp/ospf-mdr - ./bootstrap.sh - ./configure --disable-doc --enable-user=root --enable-group=root --with-cflags=-ggdb \ - --sysconfdir=/usr/local/etc/quagga --enable-vtysh \ - --localstatedir=/var/run/quagga - make -j8 - sudo make install - cd - -} - -function build_core() { - ./bootstrap.sh - ./configure $1 - make -j8 -} - -function install_core() { - sudo make install -} - -function uninstall_core() { - sudo make uninstall - make clean - ./bootstrap.sh clean -} - -function install_dev_core() { - cd gui - sudo make install - cd - - cd netns - sudo make install - cd - - cd daemon -} - # detect os/ver for install type os="" if [[ -f /etc/os-release ]]; then @@ -62,100 +11,47 @@ if [[ -f /etc/os-release ]]; then fi # parse arguments -while getopts "drv:" opt; do +dev="" +verbose="" +prefix="" +while getopts "dvp:" opt; do case ${opt} in d) - dev=1 + dev="-d" ;; v) - ubuntu_py=${OPTARG} - centos_py=${OPTARG} + verbose="-v" ;; - r) - reinstall=1 + p) + prefix="-p ${OPTARG}" ;; \?) - echo "script usage: $(basename $0) [-d] [-r] [-v python version]" >&2 + echo "script usage: $(basename $0) [-d] [-v]" >&2 + echo "" >&2 + echo "-v enable verbose install" >&2 + echo "-d enable developer install" >&2 + echo "-p install prefix, defaults to /usr/local" >&2 exit 1 ;; esac done shift $((OPTIND - 1)) -# check if we are reinstalling or installing -if [ -z "${reinstall}" ]; then - echo "installing CORE for ${os}" - case ${os} in - "ubuntu") - echo "installing core system dependencies" - sudo apt install -y automake pkg-config gcc libev-dev ebtables iproute2 \ - python${ubuntu_py} python${ubuntu_py}-dev python3-pip python3-tk tk libtk-img ethtool autoconf - install_grpcio_tools - echo "installing ospf-mdr system dependencies" - sudo apt install -y libtool gawk libreadline-dev - install_ospf_mdr - if [[ -z ${dev} ]]; then - echo "normal install" - install_python_depencencies - build_core - install_core - else - echo "dev install" - python3 -m pip install pipenv - build_core - install_dev_core - python3 -m pipenv sync --dev - python3 -m pipenv run pre-commit install - fi - ;; - "centos") - echo "installing core system dependencies" - sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel iptables-ebtables iproute \ - python${centos_py} python${centos_py}-devel python3-pip python3-tkinter tk ethtool autoconf - install_grpcio_tools - echo "installing ospf-mdr system dependencies" - sudo yum install -y libtool gawk readline-devel - install_ospf_mdr - if [[ -z ${dev} ]]; then - echo "normal install" - install_python_depencencies - build_core --prefix=/usr - install_core - else - echo "dev install" - sudo python3 -m pip install pipenv - build_core --prefix=/usr - install_dev_core - sudo python3 -m pipenv sync --dev - python3 -m pipenv sync --dev - python3 -m pipenv run pre-commit install - fi - ;; - *) - echo "unknown OS ID ${os} cannot install" - ;; - esac -else - branch=$(git symbolic-ref --short HEAD) - echo "reinstalling CORE on ${os} with latest ${branch}" - echo "uninstalling CORE" - uninstall_core - echo "pulling latest code" - git pull - echo "installing python dependencies" - install_python_depencencies - echo "building CORE" - case ${os} in - "ubuntu") - build_core - ;; - "centos") - build_core --prefix=/usr - ;; - *) - echo "unknown OS ID ${os} cannot reinstall" - ;; - esac - echo "installing CORE" - install_core -fi +echo "installing CORE for ${os}" +case ${os} in +"ubuntu") + sudo apt install -y python3-pip python3-venv + ;; +"centos") + sudo yum install -y python3-pip + ;; +*) + echo "unknown OS ID ${os} cannot install" + ;; +esac + +python3 -m pip install --user pipx +python3 -m pipx ensurepath +export PATH=$PATH:~/.local/bin +pipx install invoke +inv install ${dev} ${verbose} ${prefix} diff --git a/install2.sh b/install2.sh deleted file mode 100755 index 5e5a9b11..00000000 --- a/install2.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash - -# exit on error -set -e - -# detect os/ver for install type -os="" -if [[ -f /etc/os-release ]]; then - . /etc/os-release - os=${ID} -fi - -# parse arguments -dev="" -verbose="" -prefix="" -while getopts "dvp:" opt; do - case ${opt} in - d) - dev="-d" - ;; - v) - verbose="-v" - ;; - p) - prefix="-p ${OPTARG}" - ;; - \?) - echo "script usage: $(basename $0) [-d] [-v]" >&2 - echo "" >&2 - echo "-v enable verbose install" >&2 - echo "-d enable developer install" >&2 - echo "-p install prefix, defaults to /usr/local" >&2 - exit 1 - ;; - esac -done -shift $((OPTIND - 1)) - -echo "installing CORE for ${os}" -case ${os} in -"ubuntu") - sudo apt install -y python3-pip python3-venv - ;; -"centos") - sudo yum install -y python3-pip - ;; -*) - echo "unknown OS ID ${os} cannot install" - ;; -esac - -python3 -m pip install --user pipx -python3 -m pipx ensurepath -export PATH=$PATH:~/.local/bin -pipx install invoke -inv install ${dev} ${verbose} ${prefix} From f00d4aef0b527886b494444b559a8d04f49744d0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 11:02:00 -0700 Subject: [PATCH 0502/1131] update install doc to note centos 8 and netem --- docs/install.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/install.md b/docs/install.md index 42716fd3..2bf1a3b1 100644 --- a/docs/install.md +++ b/docs/install.md @@ -36,6 +36,16 @@ Verified: > **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 * iproute2 4.5+ is a requirement for bridge related commands From 05830c68304a038920309fb2d46d61327659b4b3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 11:25:40 -0700 Subject: [PATCH 0503/1131] removed fpm packaging, as it will not be used anymore, beyond distributed packages --- Makefile.am | 58 ----------------------------------------------------- 1 file changed, 58 deletions(-) diff --git a/Makefile.am b/Makefile.am index 4db05408..fbdf573e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -44,58 +44,6 @@ DISTCLEANFILES = aclocal.m4 \ MAINTAINERCLEANFILES = .version \ .version.date -define fpm-rpm = -fpm -s dir -t rpm -n core \ - -m "$(PACKAGE_MAINTAINERS)" \ - --license "BSD" \ - --description "Common Open Research Emulator" \ - --url https://github.com/coreemu/core \ - --vendor "$(PACKAGE_VENDOR)" \ - -p core_VERSION_ARCH.rpm \ - -v $(PACKAGE_VERSION) \ - --rpm-init scripts/core-daemon \ - --config-files "/etc/core" \ - -d "ethtool" \ - -d "tcl" \ - -d "tk" \ - -d "procps-ng" \ - -d "bash >= 3.0" \ - -d "ebtables" \ - -d "iproute" \ - -d "libev" \ - -d "net-tools" \ - -d "python3 >= 3.6" \ - -d "python3-tkinter" \ - -C $(DESTDIR) -endef - -define fpm-deb = -fpm -s dir -t deb -n core \ - -m "$(PACKAGE_MAINTAINERS)" \ - --license "BSD" \ - --description "Common Open Research Emulator" \ - --url https://github.com/coreemu/core \ - --vendor "$(PACKAGE_VENDOR)" \ - -p core_VERSION_ARCH.deb \ - -v $(PACKAGE_VERSION) \ - --deb-systemd scripts/core-daemon.service \ - --deb-no-default-config-files \ - --config-files "/etc/core" \ - -d "ethtool" \ - -d "tcl" \ - -d "tk" \ - -d "libtk-img" \ - -d "procps" \ - -d "libc6 >= 2.14" \ - -d "bash >= 3.0" \ - -d "ebtables" \ - -d "iproute2" \ - -d "libev4" \ - -d "python3 >= 3.6" \ - -d "python3-tk" \ - -C $(DESTDIR) -endef - define fpm-distributed-deb = fpm -s dir -t deb -n core-distributed \ -m "$(PACKAGE_MAINTAINERS)" \ @@ -138,12 +86,6 @@ fpm -s dir -t rpm -n core-distributed \ -C $(DESTDIR) endef -.PHONY: fpm -fpm: clean-local-fpm - $(MAKE) install DESTDIR=$(DESTDIR) - $(call fpm-deb) - $(call fpm-rpm) - .PHONY: fpm-distributed fpm-distributed: clean-local-fpm $(MAKE) -C netns install DESTDIR=$(DESTDIR) From 50f331d93ef9b80e7fa031830b9797ffb2c39851 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 11:40:20 -0700 Subject: [PATCH 0504/1131] removed references to building and dealing with service files, as that will now be limited to the invoke task --- Makefile.am | 4 +- configure.ac | 14 ----- scripts/.gitignore | 2 - scripts/Makefile.am | 31 --------- scripts/core-daemon.in | 112 --------------------------------- scripts/core-daemon.service.in | 11 ---- 6 files changed, 1 insertion(+), 173 deletions(-) delete mode 100644 scripts/.gitignore delete mode 100644 scripts/Makefile.am delete mode 100644 scripts/core-daemon.in delete mode 100644 scripts/core-daemon.service.in diff --git a/Makefile.am b/Makefile.am index fbdf573e..20191438 100644 --- a/Makefile.am +++ b/Makefile.am @@ -11,7 +11,7 @@ if WANT_GUI endif if WANT_DAEMON - DAEMON = scripts daemon + DAEMON = daemon endif if WANT_NETNS @@ -124,8 +124,6 @@ all: change-files .PHONY: change-files change-files: $(call change-files,gui/core-gui) - $(call change-files,scripts/core-daemon.service) - $(call change-files,scripts/core-daemon) $(call change-files,daemon/core/constants.py) $(call change-files,netns/setup.py) $(call change-files,daemon/setup.py) diff --git a/configure.ac b/configure.ac index 02102760..10d30c20 100644 --- a/configure.ac +++ b/configure.ac @@ -208,22 +208,12 @@ if [test "x$want_python" = "xyes" && test "x$enable_docs" = "xyes"] ; then AS_IF([$PYTHON -c "import sphinx_rtd_theme" &> /dev/null], [], [AC_MSG_ERROR([doc dependency missing, please install python3 -m pip install sphinx-rtd-theme])]) fi -AC_ARG_WITH([startup], - [AS_HELP_STRING([--with-startup=option], - [option=systemd,suse,none to install systemd/SUSE init scripts])], - [with_startup=$with_startup], - [with_startup=initd]) -AC_SUBST(with_startup) -AC_MSG_RESULT([using startup option $with_startup]) - # Variable substitutions AM_CONDITIONAL(WANT_GUI, test x$enable_gui = xyes) AM_CONDITIONAL(WANT_DAEMON, test x$enable_daemon = xyes) AM_CONDITIONAL(WANT_DOCS, test x$want_docs = xyes) AM_CONDITIONAL(WANT_PYTHON, test x$want_python = xyes) AM_CONDITIONAL(WANT_NETNS, test x$want_linux_netns = xyes) -AM_CONDITIONAL(WANT_INITD, test x$with_startup = xinitd) -AM_CONDITIONAL(WANT_SYSTEMD, test x$with_startup = xsystemd) AM_CONDITIONAL(WANT_VNODEDONLY, test x$enable_vnodedonly = xyes) if test $cross_compiling = no; then @@ -237,7 +227,6 @@ AC_CONFIG_FILES([Makefile gui/version.tcl gui/Makefile gui/icons/Makefile - scripts/Makefile man/Makefile docs/Makefile daemon/Makefile @@ -267,9 +256,6 @@ Daemon: Daemon path: ${bindir} Daemon config: ${CORE_CONF_DIR} Python: ${PYTHON} - Logs: ${CORE_STATE_DIR}/log - -Startup: ${with_startup} Features to build: Build GUI: ${enable_gui} diff --git a/scripts/.gitignore b/scripts/.gitignore deleted file mode 100644 index 86129f95..00000000 --- a/scripts/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -core-daemon -core-daemon.service diff --git a/scripts/Makefile.am b/scripts/Makefile.am deleted file mode 100644 index abdef40d..00000000 --- a/scripts/Makefile.am +++ /dev/null @@ -1,31 +0,0 @@ -# CORE -# (c)2011-2013 the Boeing Company. -# See the LICENSE file included in this distribution. -# -# author: Jeff Ahrenholz -# -# Makefile for installing scripts. -# - -CLEANFILES = core-daemon - -DISTCLEANFILES = Makefile.in core-daemon.service core-daemon - -EXTRA_DIST = core-daemon.in core-daemon.service.in - -SUBDIRS = - -# install startup scripts based on --with-startup=option configure option -# init.d (default), systemd -if WANT_INITD -startupdir = /etc/init.d -startup_SCRIPTS = core-daemon -endif -if WANT_SYSTEMD -startupdir = /usr/lib/systemd/system -startup_SCRIPTS = core-daemon.service -endif - -# remove extra scripts and their directories if they are empty -uninstall-hook: - rmdir -p $(startupdir) || true diff --git a/scripts/core-daemon.in b/scripts/core-daemon.in deleted file mode 100644 index 0a988f0f..00000000 --- a/scripts/core-daemon.in +++ /dev/null @@ -1,112 +0,0 @@ -#!/bin/sh -### BEGIN INIT INFO -# Provides: core-daemon -# Required-Start: $network $remote_fs -# Required-Stop: $network $remote_fs -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: Start the core-daemon CORE daemon at boot time -# Description: Starts and stops the core-daemon CORE daemon used to -# provide network emulation services for the CORE GUI -# or scripts. -### END INIT INFO -# -# chkconfig: 35 90 03 -# description: Starts and stops the CORE daemon \ -# used to provide network emulation services. -# -# config: /etc/core/ - -NAME=`basename $0` -PIDFILE="@CORE_STATE_DIR@/run/$NAME.pid" -LOG="@CORE_STATE_DIR@/log/$NAME.log" -CMD="@bindir@/$NAME" - -get_pid() { - cat "$PIDFILE" -} - -is_alive() { - [ -f "$PIDFILE" ] && ps -p `get_pid` > /dev/null 2>&1 -} - -corestart() { - if is_alive; then - echo "$NAME already started" - else - echo "starting $NAME" - $CMD 2>&1 >> "$LOG" & - fi - - echo $! > "$PIDFILE" - if ! is_alive; then - echo "unable to start $NAME, see $LOG" - exit 1 - fi -} - -corestop() { - if is_alive; then - echo -n "stopping $NAME.." - kill `get_pid` - for i in 1 2 3 4 5; do - sleep 1 - if ! is_alive; then - break - fi - echo -n "." - done - echo - - if is_alive; then - echo "not stopped; may still be shutting down" - exit 1 - else - echo "stopped" - if [ -f "$PIDFILE" ]; then - rm -f "$PIDFILE" - fi - fi - else - echo "$NAME not running" - fi -} - -corerestart() { - corestop - corestart -} - -corestatus() { - if is_alive; then - echo "$NAME is running" - else - echo "$NAME is stopped" - exit 1 - fi -} - - -case "$1" in - start) - corestart - ;; - stop) - corestop - ;; - restart) - corerestart - ;; - force-reload) - corerestart - ;; - status) - corestatus - ;; - *) - echo "Usage: $0 {start|stop|restart|status}" - exit 1 -esac - -exit $? - diff --git a/scripts/core-daemon.service.in b/scripts/core-daemon.service.in deleted file mode 100644 index cd53cfad..00000000 --- a/scripts/core-daemon.service.in +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=Common Open Research Emulator Service -After=network.target - -[Service] -Type=simple -ExecStart=@bindir@/core-daemon -TasksMax=infinity - -[Install] -WantedBy=multi-user.target From df01f0444444d51f936c482d802e52a95513e397 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 12:08:05 -0700 Subject: [PATCH 0505/1131] removed python buid/installation from makefiles, poetry will handle --- .gitignore | 4 +--- daemon/.gitignore | 2 -- daemon/Makefile.am | 38 ++++---------------------------------- 3 files changed, 5 insertions(+), 39 deletions(-) delete mode 100644 daemon/.gitignore diff --git a/.gitignore b/.gitignore index bcfbadeb..2012df9d 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ coverage.xml # python files *.egg-info +*.pyc # ignore package files *.rpm @@ -55,8 +56,5 @@ coverage.xml netns/setup.py daemon/setup.py -# ignore corefx build -corefx/target - # python __pycache__ diff --git a/daemon/.gitignore b/daemon/.gitignore deleted file mode 100644 index 27ffc2f1..00000000 --- a/daemon/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.pyc -build diff --git a/daemon/Makefile.am b/daemon/Makefile.am index a5663654..1cf4d233 100644 --- a/daemon/Makefile.am +++ b/daemon/Makefile.am @@ -7,43 +7,12 @@ # Makefile for building netns components. # -SETUPPY = setup.py -SETUPPYFLAGS = -v - if WANT_DOCS DOCS = doc endif SUBDIRS = proto $(DOCS) -SCRIPT_FILES := $(notdir $(wildcard scripts/*)) -MAN_FILES := $(notdir $(wildcard ../man/*.1)) - -# Python package build -noinst_SCRIPTS = build -build: - $(PYTHON) $(SETUPPY) $(SETUPPYFLAGS) build - -# Python package install -install-exec-hook: - $(PYTHON) $(SETUPPY) $(SETUPPYFLAGS) install \ - --root=/$(DESTDIR) \ - --prefix=$(prefix) \ - --single-version-externally-managed - -# Python package uninstall -uninstall-hook: - rm -rf $(DESTDIR)/etc/core - rm -rf $(DESTDIR)/$(datadir)/core - rm -f $(addprefix $(DESTDIR)/$(datarootdir)/man/man1/, $(MAN_FILES)) - rm -f $(addprefix $(DESTDIR)/$(bindir)/,$(SCRIPT_FILES)) - rm -rf $(DESTDIR)/$(pythondir)/core-$(PACKAGE_VERSION)-py$(PYTHON_VERSION).egg-info - rm -rf $(DESTDIR)/$(pythondir)/core - -# Python package cleanup -clean-local: - -rm -rf build - # because we include entire directories with EXTRA_DIST, we need to clean up # the source control files dist-hook: @@ -52,17 +21,18 @@ dist-hook: distclean-local: -rm -rf core.egg-info - DISTCLEANFILES = Makefile.in # files to include with distribution tarball -EXTRA_DIST = $(SETUPPY) \ +EXTRA_DIST = setup.py \ core \ data \ doc/conf.py.in \ examples \ scripts \ tests \ - test.py \ setup.cfg \ + MANIFEST.in \ + poetry.lock \ + pyproject.toml \ requirements.txt From 7b3f934e954691416b7c67700bb318eedc66c40f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 12:13:41 -0700 Subject: [PATCH 0506/1131] updated pyproject.toml to align with setup.py as is --- daemon/pyproject.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml index 165fb34c..d36b341c 100644 --- a/daemon/pyproject.toml +++ b/daemon/pyproject.toml @@ -1,8 +1,11 @@ [tool.poetry] name = "core" version = "6.6.0" -description = "" -authors = [] +description = "CORE Common Open Research Emulator" +authors = ["Boeing Research & Technology"] +license = "BSD-2-Clause" +repository = "https://github.com/coreemu/core" +documentation = "https://coreemu.github.io/core/" [tool.poetry.dependencies] python = "^3.6" From 8c50d08121a01b30bd0b7aeeb8d02cfa146b6a3c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 12:33:13 -0700 Subject: [PATCH 0507/1131] removed setup.py and requirements.txt as poetry will be where this information will live --- Makefile.am | 1 - daemon/Makefile.am | 6 ++--- daemon/pyproject.toml | 2 +- daemon/requirements.txt | 19 ------------- daemon/setup.py.in | 60 ----------------------------------------- 5 files changed, 3 insertions(+), 85 deletions(-) delete mode 100644 daemon/requirements.txt delete mode 100644 daemon/setup.py.in diff --git a/Makefile.am b/Makefile.am index 20191438..7a3799fc 100644 --- a/Makefile.am +++ b/Makefile.am @@ -126,7 +126,6 @@ change-files: $(call change-files,gui/core-gui) $(call change-files,daemon/core/constants.py) $(call change-files,netns/setup.py) - $(call change-files,daemon/setup.py) CORE_DOC_SRC = core-python-$(PACKAGE_VERSION) .PHONY: doc diff --git a/daemon/Makefile.am b/daemon/Makefile.am index 1cf4d233..04f48a92 100644 --- a/daemon/Makefile.am +++ b/daemon/Makefile.am @@ -24,8 +24,7 @@ distclean-local: DISTCLEANFILES = Makefile.in # files to include with distribution tarball -EXTRA_DIST = setup.py \ - core \ +EXTRA_DIST = core \ data \ doc/conf.py.in \ examples \ @@ -34,5 +33,4 @@ EXTRA_DIST = setup.py \ setup.cfg \ MANIFEST.in \ poetry.lock \ - pyproject.toml \ - requirements.txt + pyproject.toml diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml index d36b341c..da22690b 100644 --- a/daemon/pyproject.toml +++ b/daemon/pyproject.toml @@ -2,7 +2,7 @@ name = "core" version = "6.6.0" description = "CORE Common Open Research Emulator" -authors = ["Boeing Research & Technology"] +authors = ["Boeing Research and Technology"] license = "BSD-2-Clause" repository = "https://github.com/coreemu/core" documentation = "https://coreemu.github.io/core/" diff --git a/daemon/requirements.txt b/daemon/requirements.txt deleted file mode 100644 index 19d155e5..00000000 --- a/daemon/requirements.txt +++ /dev/null @@ -1,19 +0,0 @@ -bcrypt==3.1.7 -cffi==1.14.0 -cryptography==2.8 -dataclasses==0.7; python_version == "3.6" -fabric==2.5.0 -grpcio==1.27.2 -invoke==1.4.1 -lxml==4.5.0 -Mako==1.1.1 -MarkupSafe==1.1.1 -netaddr==0.7.19 -paramiko==2.7.1 -Pillow==7.0.0 -protobuf==3.11.3 -pycparser==2.19 -PyNaCl==1.3.0 -pyproj==2.5.0 -PyYAML==5.3 -six==1.14.0 diff --git a/daemon/setup.py.in b/daemon/setup.py.in deleted file mode 100644 index e8c99e67..00000000 --- a/daemon/setup.py.in +++ /dev/null @@ -1,60 +0,0 @@ -""" -Defines how CORE will be built for installation. -""" - -import glob -import os - -from setuptools import find_packages, setup - -_CORE_DIR = "/etc/core" -_MAN_DIR = "share/man/man1" -_EXAMPLES_DIR = "share/core" - - -def recursive_files(data_path, files_path): - all_files = [] - for path, _directories, filenames in os.walk(files_path): - directory = os.path.join(data_path, path) - files = [] - for filename in filenames: - files.append(os.path.join(path, filename)) - all_files.append((directory, files)) - return all_files - - -data_files = [ - (_CORE_DIR, glob.glob("data/*")), - (_MAN_DIR, glob.glob("../man/**.1")), -] -data_files.extend(recursive_files(_EXAMPLES_DIR, "examples")) - -setup( - name="core", - version="@PACKAGE_VERSION@", - packages=find_packages(), - install_requires=[ - 'dataclasses;python_version=="3.6"', - "fabric", - "grpcio", - "invoke", - "lxml", - "mako", - "netaddr", - "pillow", - "protobuf", - "pyproj", - "pyyaml", - ], - tests_require=[ - "pytest", - ], - data_files=data_files, - scripts=glob.glob("scripts/*"), - include_package_data=True, - description="Python components of CORE", - url="https://github.com/coreemu/core", - author="Boeing Research & Technology", - license="BSD", - long_description="Python scripts and modules for building virtual emulated networks.", -) From 0cd3f6115dfb4eca5b262fecfd84b011f2e88edb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 12:37:29 -0700 Subject: [PATCH 0508/1131] remove setup.py reference from github action --- .github/workflows/daemon-checks.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/daemon-checks.yml b/.github/workflows/daemon-checks.yml index 9e9f7aa7..6cb12124 100644 --- a/.github/workflows/daemon-checks.yml +++ b/.github/workflows/daemon-checks.yml @@ -16,7 +16,6 @@ jobs: python -m pip install --upgrade pip pip install poetry cd daemon - cp setup.py.in setup.py cp core/constants.py.in core/constants.py sed -i 's/required=True/required=False/g' core/emulator/coreemu.py poetry install From 873fc0e4683eb23a60a163f8bf0eff5a2f066182 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 12:49:40 -0700 Subject: [PATCH 0509/1131] removed daemon MANIFEST.in, poetry will provide --- daemon/MANIFEST.in | 2 -- daemon/Makefile.am | 1 - daemon/pyproject.toml | 1 + 3 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 daemon/MANIFEST.in diff --git a/daemon/MANIFEST.in b/daemon/MANIFEST.in deleted file mode 100644 index c46dc828..00000000 --- a/daemon/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -graft core/gui/data -graft core/configservices/*/templates diff --git a/daemon/Makefile.am b/daemon/Makefile.am index 04f48a92..7528dc01 100644 --- a/daemon/Makefile.am +++ b/daemon/Makefile.am @@ -31,6 +31,5 @@ EXTRA_DIST = core \ scripts \ tests \ setup.cfg \ - MANIFEST.in \ poetry.lock \ pyproject.toml diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml index da22690b..d9f204b8 100644 --- a/daemon/pyproject.toml +++ b/daemon/pyproject.toml @@ -6,6 +6,7 @@ authors = ["Boeing Research and Technology"] license = "BSD-2-Clause" repository = "https://github.com/coreemu/core" documentation = "https://coreemu.github.io/core/" +include = ["core/gui/data/**/*", "core/configservices/*/templates"] [tool.poetry.dependencies] python = "^3.6" From fdd2e6f1f11f8a55f4e4c7804864b03a3114156f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 12:54:13 -0700 Subject: [PATCH 0510/1131] removed references for excluding utm.py as it is no longer present --- .github/workflows/daemon-checks.yml | 2 +- daemon/.pre-commit-config.yaml | 2 +- daemon/setup.cfg | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/daemon-checks.yml b/.github/workflows/daemon-checks.yml index 6cb12124..5ea8d1c2 100644 --- a/.github/workflows/daemon-checks.yml +++ b/.github/workflows/daemon-checks.yml @@ -26,7 +26,7 @@ jobs: - name: black run: | cd daemon - poetry run black --check --exclude ".+_pb2.*.py|doc|build|utm\.py|setup\.py" . + poetry run black --check --exclude ".+_pb2.*.py|doc|build" . - name: flake8 run: | cd daemon diff --git a/daemon/.pre-commit-config.yaml b/daemon/.pre-commit-config.yaml index 13a6955b..fe810f04 100644 --- a/daemon/.pre-commit-config.yaml +++ b/daemon/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: name: black stages: [commit] language: system - entry: bash -c 'cd daemon && poetry run black --exclude ".+_pb2.*.py|doc|build|utm\.py" .' + entry: bash -c 'cd daemon && poetry run black --exclude ".+_pb2.*.py|doc|build" .' types: [python] - id: flake8 diff --git a/daemon/setup.cfg b/daemon/setup.cfg index a3084b8b..f2c2a3aa 100644 --- a/daemon/setup.cfg +++ b/daemon/setup.cfg @@ -2,7 +2,7 @@ test=pytest [isort] -skip_glob=*_pb2*.py,utm.py,doc,build +skip_glob=*_pb2*.py,doc,build multi_line_output=3 include_trailing_comma=True force_grid_wrap=0 @@ -14,7 +14,7 @@ ignore=E501,W503,E203 max-line-length=88 max-complexity=26 select=B,C,E,F,W,T4 -exclude=*_pb2*.py,utm.py,doc,build +exclude=*_pb2*.py,doc,build [tool:pytest] norecursedirs=distributed emane From f8b0ab6ec3ed88359c4f3384335f8e0337541022 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 13:24:16 -0700 Subject: [PATCH 0511/1131] moved isort config from setup.cfg to pyproject.toml --- daemon/pyproject.toml | 8 ++++++++ daemon/setup.cfg | 11 ----------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml index d9f204b8..f7a9874a 100644 --- a/daemon/pyproject.toml +++ b/daemon/pyproject.toml @@ -31,6 +31,14 @@ mock = "*" pre-commit = "*" pytest = "*" +[tool.isort] +skip_glob = "*_pb2*.py,doc,build" +multi_line_output = 3 +include_trailing_comma = "True" +force_grid_wrap = 0 +use_parentheses = "True" +line_length = 88 + [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" diff --git a/daemon/setup.cfg b/daemon/setup.cfg index f2c2a3aa..89c968b9 100644 --- a/daemon/setup.cfg +++ b/daemon/setup.cfg @@ -1,14 +1,3 @@ -[aliases] -test=pytest - -[isort] -skip_glob=*_pb2*.py,doc,build -multi_line_output=3 -include_trailing_comma=True -force_grid_wrap=0 -use_parentheses=True -line_length=88 - [flake8] ignore=E501,W503,E203 max-line-length=88 From 80194b3e38bc75a3e9de61264db6fc4a5787f354 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 13:33:40 -0700 Subject: [PATCH 0512/1131] moved python black configuration to pyproject.toml and fixed bad exclude --- .github/workflows/daemon-checks.yml | 2 +- daemon/.pre-commit-config.yaml | 2 +- daemon/core/nodes/docker.py | 6 ++---- daemon/pyproject.toml | 5 +++++ 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/daemon-checks.yml b/.github/workflows/daemon-checks.yml index 5ea8d1c2..52440467 100644 --- a/.github/workflows/daemon-checks.yml +++ b/.github/workflows/daemon-checks.yml @@ -26,7 +26,7 @@ jobs: - name: black run: | cd daemon - poetry run black --check --exclude ".+_pb2.*.py|doc|build" . + poetry run black --check . - name: flake8 run: | cd daemon diff --git a/daemon/.pre-commit-config.yaml b/daemon/.pre-commit-config.yaml index fe810f04..bc9ead08 100644 --- a/daemon/.pre-commit-config.yaml +++ b/daemon/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: name: black stages: [commit] language: system - entry: bash -c 'cd daemon && poetry run black --exclude ".+_pb2.*.py|doc|build" .' + entry: bash -c 'cd daemon && poetry run black .' types: [python] - id: flake8 diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index 1ef814ee..ce34bd98 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -78,7 +78,7 @@ class DockerNode(CoreNode): name: str = None, nodedir: str = None, server: DistributedServer = None, - image: str = None + image: str = None, ) -> None: """ Create a DockerNode instance. @@ -209,9 +209,7 @@ class DockerNode(CoreNode): if self.server is not None: self.host_cmd(f"rm -f {temp.name}") os.unlink(temp.name) - logging.debug( - "node(%s) added file: %s; mode: 0%o", self.name, filename, mode - ) + logging.debug("node(%s) added file: %s; mode: 0%o", self.name, filename, mode) def nodefilecopy(self, filename: str, srcfilename: str, mode: int = None) -> None: """ diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml index f7a9874a..3e37e4f9 100644 --- a/daemon/pyproject.toml +++ b/daemon/pyproject.toml @@ -39,6 +39,11 @@ force_grid_wrap = 0 use_parentheses = "True" line_length = 88 +[tool.black] +line_length = 88 +exclude = ".+_pb2.*.py|doc/|build/|__pycache__/" + + [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" From be2f7e1cae302ad608c55433b66308cf9f053d5d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 13:42:59 -0700 Subject: [PATCH 0513/1131] simplified invoke install/uninstall task, since daemon no longer formally installs --- daemon/pyproject.toml | 1 - tasks.py | 16 +++------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml index 3e37e4f9..1fdc9d1a 100644 --- a/daemon/pyproject.toml +++ b/daemon/pyproject.toml @@ -43,7 +43,6 @@ line_length = 88 line_length = 88 exclude = ".+_pb2.*.py|doc/|build/|__pycache__/" - [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" diff --git a/tasks.py b/tasks.py index 9da66faa..6ed956c2 100644 --- a/tasks.py +++ b/tasks.py @@ -8,8 +8,6 @@ from tempfile import NamedTemporaryFile from invoke import task, Context DAEMON_DIR: str = "daemon" -VCMD_DIR: str = "netns" -GUI_DIR: str = "gui" DEFAULT_PREFIX: str = "/usr/local" @@ -121,11 +119,7 @@ def build(c: Context, hide: bool, prefix: str = DEFAULT_PREFIX) -> None: def install_core(c: Context, hide: bool) -> None: print("installing core vcmd...") - with c.cd(VCMD_DIR): - c.run("sudo make install", hide=hide) - print("installing core gui...") - with c.cd(GUI_DIR): - c.run("sudo make install", hide=hide) + c.run("sudo make install", hide=hide) def install_poetry(c: Context, dev: bool, hide: bool) -> None: @@ -265,12 +259,8 @@ def uninstall(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): uninstall core """ hide = not verbose - print("uninstalling core-gui") - with c.cd(GUI_DIR): - c.run("sudo make uninstall", hide=hide) - print("uninstalling vcmd") - with c.cd(VCMD_DIR): - c.run("sudo make uninstall", hide=hide) + print("uninstalling core") + c.run("sudo make uninstall", hide=hide) print("cleaning build directory") c.run("make clean", hide=hide) c.run("./bootstrap.sh clean", hide=hide) From 1cadf8362fbec42379938449c3e21a9061cf0ddf Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 22:09:00 -0700 Subject: [PATCH 0514/1131] added a text spinner while installing/uninstalling --- tasks.py | 177 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 109 insertions(+), 68 deletions(-) diff --git a/tasks.py b/tasks.py index 6ed956c2..0b727f58 100644 --- a/tasks.py +++ b/tasks.py @@ -1,9 +1,14 @@ import inspect +import itertools import os import sys +import threading +import time +from contextlib import contextmanager from enum import Enum from pathlib import Path from tempfile import NamedTemporaryFile +from typing import Optional from invoke import task, Context @@ -11,6 +16,40 @@ DAEMON_DIR: str = "daemon" DEFAULT_PREFIX: str = "/usr/local" +class Progress: + cycles = itertools.cycle(["-", "/", "|", "\\"]) + + def __init__(self, verbose: bool) -> None: + self.verbose: bool = verbose + self.thread: Optional[threading.Thread] = None + self.running: bool = False + + @contextmanager + def start(self, message: str) -> None: + if not self.verbose: + print(f"{message} ... ", end="") + self.running = True + self.thread = threading.Thread(target=self.run, daemon=True) + self.thread.start() + yield + self.stop() + + def run(self) -> None: + while self.running: + sys.stdout.write(next(self.cycles)) + sys.stdout.flush() + sys.stdout.write("\b") + time.sleep(0.1) + + def stop(self) -> None: + if not self.verbose: + print("done") + if self.thread: + self.running = False + self.thread.join() + self.thread = None + + class OsName(Enum): UBUNTU = "ubuntu" CENTOS = "centos" @@ -52,7 +91,7 @@ def get_os() -> OsInfo: if not line: continue key, value = line.split("=") - d[key] = value.strip('"') + d[key] = value.strip("\"") name_value = d["ID"] like_value = d["ID_LIKE"] version_value = d["VERSION_ID"] @@ -69,27 +108,28 @@ def get_os() -> OsInfo: def install_system(c: Context, os_info: OsInfo, hide: bool) -> None: - print("installing system dependencies...") if os_info.like == OsLike.DEBIAN: c.run( - "sudo apt install -y automake pkg-config gcc libev-dev ebtables iproute2 " - "ethtool tk python3-tk", + "sudo apt install -y automake pkg-config gcc libev-dev ebtables " + "iproute2 ethtool tk python3-tk", hide=hide ) elif os_info.like == OsLike.REDHAT: c.run( - "sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel " - "iptables-ebtables iproute python3-devel python3-tkinter tk ethtool make", + "sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ " + "libev-devel iptables-ebtables iproute python3-devel python3-tkinter " + "tk ethtool make", hide=hide ) # centos 8+ does not support netem by default if os_info.name == OsName.CENTOS and os_info.version >= 8: c.run("sudo yum install -y kernel-modules-extra", hide=hide) if not c.run("sudo modprobe sch_netem", warn=True, hide=hide): - print("ERROR: you need to install the latest kernel") + print("\nERROR: you need to install the latest kernel") print("run the following, restart, and try again") print("sudo yum update") sys.exit(1) + # attempt to setup legacy ebtables when an nftables based version is found r = c.run("ebtables -V", hide=hide) if "nf_tables" in r.stdout: @@ -99,35 +139,31 @@ def install_system(c: Context, os_info: OsInfo, hide: bool) -> None: hide=hide ): print( - "WARNING: unable to setup required ebtables-legacy, WLAN will not work" + "\nWARNING: unable to setup ebtables-legacy, WLAN will not work" ) def install_grpcio(c: Context, hide: bool) -> None: - print("installing grpcio-tools...") c.run( - "python3 -m pip install --user grpcio==1.27.2 grpcio-tools==1.27.2", hide=hide + "python3 -m pip install --user grpcio==1.27.2 grpcio-tools==1.27.2", + hide=hide, ) -def build(c: Context, hide: bool, prefix: str = DEFAULT_PREFIX) -> None: - print("building core...") +def build_core(c: Context, hide: bool, prefix: str = DEFAULT_PREFIX) -> None: c.run("./bootstrap.sh", hide=hide) c.run(f"./configure --prefix={prefix}", hide=hide) c.run("make -j$(nproc)", hide=hide) def install_core(c: Context, hide: bool) -> None: - print("installing core vcmd...") c.run("sudo make install", hide=hide) def install_poetry(c: Context, dev: bool, hide: bool) -> None: - print("installing poetry...") c.run("pipx install poetry", hide=hide) args = "" if dev else "--no-dev" with c.cd(DAEMON_DIR): - print("installing core environment using poetry...") c.run(f"poetry install {args}", hide=hide) if dev: c.run("poetry run pre-commit install", hide=hide) @@ -135,31 +171,29 @@ def install_poetry(c: Context, dev: bool, hide: bool) -> None: def install_ospf_mdr(c: Context, os_info: OsInfo, hide: bool) -> None: if c.run("which zebra", warn=True, hide=hide): - print("quagga already installed, skipping ospf mdr") + print("\nquagga already installed, skipping ospf mdr") return - print("installing ospf mdr dependencies...") - if os_info.like == OsLike.DEBIAN: - c.run("sudo apt install -y libtool gawk libreadline-dev git", hide=hide) - elif os_info.like == OsLike.REDHAT: - c.run("sudo yum install -y libtool gawk readline-devel git", hide=hide) - print("cloning ospf mdr...") - clone_dir = "/tmp/ospf-mdr" - c.run( - f"git clone https://github.com/USNavalResearchLaboratory/ospf-mdr {clone_dir}", - hide=hide - ) - with c.cd(clone_dir): - print("building ospf mdr...") - c.run("./bootstrap.sh", hide=hide) + p = Progress(not hide) + with p.start("installing ospf mdr dependencies"): + if os_info.like == OsLike.DEBIAN: + c.run("sudo apt install -y libtool gawk libreadline-dev git", hide=hide) + elif os_info.like == OsLike.REDHAT: + c.run("sudo yum install -y libtool gawk readline-devel git", hide=hide) + clone_dir = "/tmp/ospf-mdr" c.run( - "./configure --disable-doc --enable-user=root --enable-group=root " - "--with-cflags=-ggdb --sysconfdir=/usr/local/etc/quagga --enable-vtysh " - "--localstatedir=/var/run/quagga", + f"git clone https://github.com/USNavalResearchLaboratory/ospf-mdr {clone_dir}", hide=hide ) - c.run("make -j$(nproc)", hide=hide) - print("installing ospf mdr...") - c.run("sudo make install", hide=hide) + with c.cd(clone_dir): + c.run("./bootstrap.sh", hide=hide) + c.run( + "./configure --disable-doc --enable-user=root --enable-group=root " + "--with-cflags=-ggdb --sysconfdir=/usr/local/etc/quagga --enable-vtysh " + "--localstatedir=/var/run/quagga", + hide=hide + ) + c.run("make -j$(nproc)", hide=hide) + c.run("sudo make install", hide=hide) @task @@ -172,7 +206,6 @@ def install_service(c, verbose=False, prefix=DEFAULT_PREFIX): systemd_dir = Path("/lib/systemd/system/") service_file = systemd_dir.joinpath("core-daemon.service") if systemd_dir.exists(): - print(f"installing core-daemon.service for systemd to {service_file}") service_data = inspect.cleandoc(f""" [Unit] Description=Common Open Research Emulator Service @@ -204,7 +237,6 @@ def install_scripts(c, verbose=False, prefix=DEFAULT_PREFIX): bin_dir = Path(prefix).joinpath("bin") for script in Path("daemon/scripts").iterdir(): dest = bin_dir.joinpath(script.name) - print(f"installing {script} to {dest}") with open(script, "r") as f: lines = f.readlines() first = lines[0].strip() @@ -224,7 +256,6 @@ def install_scripts(c, verbose=False, prefix=DEFAULT_PREFIX): # install core configuration file config_dir = "/etc/core" - print(f"installing core configuration files under {config_dir}") c.run(f"sudo mkdir -p {config_dir}", hide=hide) c.run(f"sudo cp -n daemon/data/core.conf {config_dir}", hide=hide) c.run(f"sudo cp -n daemon/data/logging.conf {config_dir}", hide=hide) @@ -235,22 +266,28 @@ def install(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): """ install core, poetry, scripts, service, and ospf mdr """ + c.run("sudo -v", hide=True) print(f"installing core with prefix: {prefix}") + p = Progress(verbose) hide = not verbose os_info = get_os() - install_system(c, os_info, hide) - install_grpcio(c, hide) - build(c, hide, prefix) - install_core(c, hide) - install_poetry(c, dev, hide) - install_scripts(c, hide, prefix) - install_service(c, hide, prefix) - install_ospf_mdr(c, os_info, hide) - print("please open a new terminal or re-login to leverage invoke for running core") - print("# run daemon") - print("inv daemon") - print("# run gui") - print("inv gui") + with p.start("installing system dependencies"): + install_system(c, os_info, hide) + with p.start("installing system grpcio-tools"): + install_grpcio(c, hide) + with p.start("building core"): + build_core(c, hide, prefix) + with p.start("installing vcmd/gui"): + install_core(c, hide) + with p.start("installing poetry virtual environment"): + install_poetry(c, dev, hide) + with p.start("installing scripts and /etc/core"): + install_scripts(c, hide, prefix) + with p.start("installing systemd service"): + install_service(c, hide, prefix) + with p.start("installing ospf mdr"): + install_ospf_mdr(c, os_info, hide) + print("\nyou may need to open a new terminal to leverage invoke for running core") @task @@ -259,35 +296,39 @@ def uninstall(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): uninstall core """ hide = not verbose - print("uninstalling core") - c.run("sudo make uninstall", hide=hide) - print("cleaning build directory") - c.run("make clean", hide=hide) - c.run("./bootstrap.sh clean", hide=hide) + p = Progress(verbose) + c.run("sudo -v", hide=True) + with p.start("uninstalling core"): + c.run("sudo make uninstall", hide=hide) + + with p.start("cleaning build directory"): + c.run("make clean", hide=hide) + c.run("./bootstrap.sh clean", hide=hide) + python = get_python(c, warn=True) if python: with c.cd(DAEMON_DIR): if dev: - print("uninstalling pre-commit") - c.run("poetry run pre-commit uninstall", hide=hide) - print("uninstalling poetry virtual environment") - c.run(f"poetry env remove {python}", hide=hide) + with p.start("uninstalling pre-commit"): + c.run("poetry run pre-commit uninstall", hide=hide) + with p.start("uninstalling poetry virtual environment"): + c.run(f"poetry env remove {python}", hide=hide) # remove installed files bin_dir = Path(prefix).joinpath("bin") - for script in Path("daemon/scripts").iterdir(): - dest = bin_dir.joinpath(script.name) - print(f"uninstalling {dest}") - c.run(f"sudo rm -f {dest}", hide=hide) + with p.start("uninstalling script files"): + for script in Path("daemon/scripts").iterdir(): + dest = bin_dir.joinpath(script.name) + c.run(f"sudo rm -f {dest}", hide=hide) # install service systemd_dir = Path("/lib/systemd/system/") service_name = "core-daemon.service" service_file = systemd_dir.joinpath(service_name) if service_file.exists(): - print(f"uninstalling service {service_file}") - c.run(f"sudo systemctl disable {service_name}", hide=hide) - c.run(f"sudo rm -f {service_file}", hide=hide) + with p.start(f"uninstalling service {service_file}"): + c.run(f"sudo systemctl disable {service_name}", hide=hide) + c.run(f"sudo rm -f {service_file}", hide=hide) @task From 119a3640e4b8a443bb1b21995c277452347c83e4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 14 Jul 2020 22:26:39 -0700 Subject: [PATCH 0515/1131] remove duplicated progress usage when installing ospf mdr --- tasks.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/tasks.py b/tasks.py index 0b727f58..a8e8b371 100644 --- a/tasks.py +++ b/tasks.py @@ -173,27 +173,25 @@ def install_ospf_mdr(c: Context, os_info: OsInfo, hide: bool) -> None: if c.run("which zebra", warn=True, hide=hide): print("\nquagga already installed, skipping ospf mdr") return - p = Progress(not hide) - with p.start("installing ospf mdr dependencies"): - if os_info.like == OsLike.DEBIAN: - c.run("sudo apt install -y libtool gawk libreadline-dev git", hide=hide) - elif os_info.like == OsLike.REDHAT: - c.run("sudo yum install -y libtool gawk readline-devel git", hide=hide) - clone_dir = "/tmp/ospf-mdr" + if os_info.like == OsLike.DEBIAN: + c.run("sudo apt install -y libtool gawk libreadline-dev git", hide=hide) + elif os_info.like == OsLike.REDHAT: + c.run("sudo yum install -y libtool gawk readline-devel git", hide=hide) + clone_dir = "/tmp/ospf-mdr" + c.run( + f"git clone https://github.com/USNavalResearchLaboratory/ospf-mdr {clone_dir}", + hide=hide + ) + with c.cd(clone_dir): + c.run("./bootstrap.sh", hide=hide) c.run( - f"git clone https://github.com/USNavalResearchLaboratory/ospf-mdr {clone_dir}", + "./configure --disable-doc --enable-user=root --enable-group=root " + "--with-cflags=-ggdb --sysconfdir=/usr/local/etc/quagga --enable-vtysh " + "--localstatedir=/var/run/quagga", hide=hide ) - with c.cd(clone_dir): - c.run("./bootstrap.sh", hide=hide) - c.run( - "./configure --disable-doc --enable-user=root --enable-group=root " - "--with-cflags=-ggdb --sysconfdir=/usr/local/etc/quagga --enable-vtysh " - "--localstatedir=/var/run/quagga", - hide=hide - ) - c.run("make -j$(nproc)", hide=hide) - c.run("sudo make install", hide=hide) + c.run("make -j$(nproc)", hide=hide) + c.run("sudo make install", hide=hide) @task From a1ea762b8956488a1a97c5a05396ca0e67423e01 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 15 Jul 2020 00:08:22 -0700 Subject: [PATCH 0516/1131] updates to help provide better install related documentation --- docs/install.md | 65 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/docs/install.md b/docs/install.md index 2bf1a3b1..5e8bddf0 100644 --- a/docs/install.md +++ b/docs/install.md @@ -53,11 +53,14 @@ sudo modprobe sch_netem ## Automated Installation -> **NOTE:** installs OSPF MDR +The automated install will install the various tools needed to help automate +the CORE installation (python3, pip, pipx, invoke, poetry). The script will +also automatically clone, build, and install the latest version of OSPF MDR. +Finally it will install CORE scripts and a systemd service, which have +been modified to use the installed poetry created virtual environment. -> **NOTE:** sets up script files using the prefix provided - -> **NOTE:** install a systemd service file to /lib/systemd/system/core-daemon.service +After installation has completed you should be able to run the various +CORE scripts for running core. ```shell # clone CORE repo @@ -75,6 +78,13 @@ cd core ## Manual Installation +Below is an example of more formal manual steps that can be taken to install +CORE. You can also just install invoke and run `inv install` alone to simulate +what is done using `install.sh`. + +The last two steps help install core scripts modified to leverage the installed +poetry virtual environment and setup a systemd based service, if desired. + > **NOTE:** install OSPF MDR by manual instructions below ```shell @@ -131,6 +141,24 @@ inv install-scripts inv install-service ``` +## Installed Scripts + +These scripts will be installed from the automated `install.sh` script or +using `inv install` manually. + +| Name | Description | +|---|---| +| core-daemon | runs the backed core server providing TLV and gRPC APIs | +| core-gui | runs the legacy tcl/tk based GUI | +| core-pygui | runs the new python/tk based GUI | +| core-cleanup | tool to help removed lingering core created containers, bridges, directories | +| core-imn-to-xml | tool to help automate converting a .imn file to .xml format | +| core-route-monitor | tool to help monitor traffic across nodes and feed that to SDT | +| 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 | +| core-cli | tool to query, open xml files, and send commands using gRPC | +| core-manage | tool to add, remove, or check for services, models, and node types | + ## Manually Install OSPF MDR (Routing Support) Virtual networks generally require some form of routing in order to work (e.g. to automatically populate routing @@ -171,10 +199,10 @@ Here are quick instructions for installing all EMANE packages for Ubuntu 18.04: sudo apt-get install libssl-dev libxml-libxml-perl libxml-simple-perl wget https://adjacentlink.com/downloads/emane/emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz tar xzf emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz -# install base emane packages -sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/emane*.deb -# install python3 bindings -sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/python3*.deb + +# install emane python bindings into the core virtual environment +cd $REPO/daemon +poetry run pip install $EMANE_REPO/src/python ``` ## Using Invoke Tasks @@ -220,3 +248,24 @@ core-cli query session -i 1 # exit the shell exit ``` + +## Running User Scripts + +If you create your own scripts to run CORE directly in python or using gRPC/TLV +APIs you will need to make sure you are running them within context of the +poetry install virtual environment. + +> **NOTE:** the following assumes CORE has been installed successfully + +One way to do this would be to enable to environments shell. +```shell +cd $REPO/daemon +poetry shell +python run /path/to/script.py +``` + +Another way would be to run the script directly by way of poetry. +```shell +cd $REPO/daemon +poetry run python /path/to/script.py +``` From 642af4fe47ad70fff9fa67195c8cf1c6e921c26a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 15 Jul 2020 16:22:03 -0700 Subject: [PATCH 0517/1131] slimmed down install documentation and added links to relevant tools and files --- docs/install.md | 183 ++++++++++++++---------------------------------- 1 file changed, 53 insertions(+), 130 deletions(-) diff --git a/docs/install.md b/docs/install.md index 5e8bddf0..0bff31c9 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,4 +1,4 @@ -# CORE Installation +# Installation * Table of Contents {:toc} @@ -12,10 +12,10 @@ The following tools will be leveraged during installation: |Tool|Description| |---|---| -|pip|used to install pipx| -|pipx|used to install standalone python tools (invoke, poetry)| -|invoke|used to run provided tasks (install, daemon, gui, tests, etc)| -|poetry|used to install the managed python virtual environment for running CORE| +|[pip](https://pip.pypa.io/en/stable/)|used to install pipx| +|[pipx](https://pipxproject.github.io/pipx/)|used to install standalone python tools (invoke, poetry)| +|[invoke](http://www.pyinvoke.org/)|used to run provided tasks (install, daemon, gui, tests, etc)| +|[poetry](https://python-poetry.org/)|used to install the managed python virtual environment for running CORE| ## Required Hardware @@ -51,6 +51,27 @@ sudo modprobe sch_netem * iproute2 4.5+ is a requirement for bridge related commands * ebtables not backed by nftables +## Upgrading + +Please make sure to uninstall the previous installation of CORE cleanly +before proceeding to install. + +Previous install was built from source: +```shell +cd $REPO +sudo make uninstall +make clean +./bootstrap.sh clean +``` + +Installed from previously built packages: +```shell +# centos +sudo yum remove core +# ubuntu +sudo apt remove core +``` + ## Automated Installation The automated install will install the various tools needed to help automate @@ -62,6 +83,9 @@ been modified to use the installed poetry created virtual environment. After installation has completed you should be able to run the various CORE scripts for running core. +> **NOTE:** provide a prefix that will be found on path when running as sudo +> if the default prefix is not valid + ```shell # clone CORE repo git clone https://github.com/coreemu/core.git @@ -76,75 +100,20 @@ cd core ./install.sh ``` -## Manual Installation +### Unsupported Linux Distribution -Below is an example of more formal manual steps that can be taken to install -CORE. You can also just install invoke and run `inv install` alone to simulate -what is done using `install.sh`. +If you are on an unsupported distribution, you can look into the +[install.sh](https://github.com/coreemu/core/blob/master/install.sh) +and +[tasks.py](https://github.com/coreemu/core/blob/master/tasks.py) +files to see the various commands ran to install CORE and translate them to +your use case, assuming it is possible. -The last two steps help install core scripts modified to leverage the installed -poetry virtual environment and setup a systemd based service, if desired. - -> **NOTE:** install OSPF MDR by manual instructions below - -```shell -# clone CORE repo -git clone https://github.com/coreemu/core.git -cd core - -# install python3 and venv support -# ubuntu -sudo apt install -y python3-pip python3-venv -# centos -sudo yum install -y python3-pip - -# install system dependencies -# ubuntu -sudo apt install -y automake pkg-config gcc libev-dev ebtables iproute2 \ - ethtool tk python3-tk -# centos -sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel \ - iptables-ebtables iproute python3-devel python3-tkinter tk ethtool \ - make kernel-modules-extra - -# install grpcio-tools -python3 -m pip install --user grpcio==1.27.2 grpcio-tools==1.27.2 - -# build core -./bootstrap.sh -# centos requires --prefix=/usr -./configure -make -sudo make install - -# install pipx, may need to restart terminal after ensurepath -python3 -m pip install --user pipx -python3 -m pipx ensurepath - -# install poetry -pipx install poetry - -# install poetry virtual environment -cd daemon -poetry install --no-dev -cd .. - -# install invoke to run helper tasks -pipx install invoke - -# install core scripts leveraging poetry virtual environment -# centos requires --prefix=/usr -inv install-scripts - -# optionally install systemd service file -# centos requires --prefix=/usr -inv install-service -``` +If you get install down entirely, feel free to contribute and help others. ## Installed Scripts -These scripts will be installed from the automated `install.sh` script or -using `inv install` manually. +After the installation complete it will have installed the following scripts. | Name | Description | |---|---| @@ -159,32 +128,25 @@ using `inv install` manually. | core-cli | tool to query, open xml files, and send commands using gRPC | | core-manage | tool to add, remove, or check for services, models, and node types | -## Manually Install OSPF MDR (Routing Support) +## Running User Scripts -Virtual networks generally require some form of routing in order to work (e.g. to automatically populate routing -tables for routing packets from one subnet to another.) CORE builds OSPF routing protocol configurations by -default when the blue router node type is used. +If you create your own python scripts to run CORE directly or using the gRPC/TLV +APIs you will need to make sure you are running them within context of the +installed virtual environment. -* [OSPF MANET Designated Routers](https://github.com/USNavalResearchLaboratory/ospf-mdr) (MDR) - the Quagga routing -suite with a modified version of OSPFv3, optimized for use with mobile wireless networks. The **mdr** node type -(and the MDR service) requires this variant of Quagga. +> **NOTE:** the following assumes CORE has been installed successfully +One way to do this would be to enable the core virtual environment shell. ```shell -# system dependencies -# ubuntu -sudo apt install -y libtool gawk libreadline-dev -# centos -sudo yum install -y libtool gawk readline-devel +cd $REPO/daemon +poetry shell +python run /path/to/script.py +``` -# build and install -git clone https://github.com/USNavalResearchLaboratory/ospf-mdr -cd ospf-mdr -./bootstrap.sh -./configure --disable-doc --enable-user=root --enable-group=root --with-cflags=-ggdb \ - --sysconfdir=/usr/local/etc/quagga --enable-vtysh \ - --localstatedir=/var/run/quagga -make -sudo make install +Another way would be to run the script directly by way of poetry. +```shell +cd $REPO/daemon +poetry run python /path/to/script.py ``` ## Manually Install EMANE @@ -217,7 +179,7 @@ Available tasks: cli run core-cli used to query and modify a running session daemon start core-daemon gui start core-pygui - install install core, poetry, scripts, service, and ospf mdr + install install core, scripts, service, and ospf mdr install-scripts install core script files, modified to leverage virtual environment install-service install systemd core service test run core tests @@ -230,42 +192,3 @@ Example running the core-daemon task from the root of the repo: ```shell inv daemon ``` - -Some tasks are wrappers around command line tools and requires running -them with a slight variation for compatibility. You can enter the -poetry shell to run the script natively. - -```shell -# running core-cli as a task requires all options to be provided -# within a string -inv cli "query session -i 1" - -# entering the poetry shell to use core-cli natively -cd $REPO/daemon -poetry shell -core-cli query session -i 1 - -# exit the shell -exit -``` - -## Running User Scripts - -If you create your own scripts to run CORE directly in python or using gRPC/TLV -APIs you will need to make sure you are running them within context of the -poetry install virtual environment. - -> **NOTE:** the following assumes CORE has been installed successfully - -One way to do this would be to enable to environments shell. -```shell -cd $REPO/daemon -poetry shell -python run /path/to/script.py -``` - -Another way would be to run the script directly by way of poetry. -```shell -cd $REPO/daemon -poetry run python /path/to/script.py -``` From 1c876819f19977ae9da5fb220a0e3547197a1b78 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 15 Jul 2020 16:50:04 -0700 Subject: [PATCH 0518/1131] task to automate installing emane --- tasks.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tasks.py b/tasks.py index a8e8b371..4b6f2ca6 100644 --- a/tasks.py +++ b/tasks.py @@ -288,6 +288,49 @@ def install(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): print("\nyou may need to open a new terminal to leverage invoke for running core") +@task +def install_emane(c, verbose=False): + """ + install emane and the python bindings + """ + c.run("sudo -v", hide=True) + p = Progress(verbose) + hide = not verbose + os_info = get_os() + emane_dir = "/tmp/emane" + with p.start("installing system dependencies"): + if os_info.like == OsLike.DEBIAN: + c.run( + "sudo apt install gcc g++ automake libtool libxml2-dev libprotobuf-dev " + "libpcap-dev libpcre3-dev uuid-dev pkg-config protobuf-compiler git " + "python3-protobuf python3-setuptools", + hide=hide, + ) + elif os_info.like == OsLike.REDHAT: + c.run( + "sudo yum install autoconf automake git libtool libxml2-devel " + "libpcap-devel pcre-devel libuuid-devel make gcc-c++ " + "python3-setuptools", + hide=hide, + ) + with p.start("cloning emane"): + c.run( + f"git clone https://github.com/adjacentlink/emane.git {emane_dir}", + hide=hide + ) + with p.start("building emane"): + with c.cd(emane_dir): + c.run("./autogen.sh", hide=hide) + c.run("PYTHON=python3 ./configure --prefix=/usr", hide=hide) + c.run("make -j$(nproc)", hide=hide) + with p.start("installing emane"): + with c.cd(emane_dir): + c.run("sudo make install", hide=hide) + with p.start("installing python binding for core"): + with c.cd(DAEMON_DIR): + c.run(f"poetry run pip install {emane_dir}/src/python", hide=hide) + + @task def uninstall(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): """ From 33d100acffb8716f396e29c069a9dbf155c7d98b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 15 Jul 2020 17:09:32 -0700 Subject: [PATCH 0519/1131] fix bad links in generated docs for grpc to point to latest on master --- docs/grpc.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/grpc.md b/docs/grpc.md index 69cf4aed..ca80256e 100644 --- a/docs/grpc.md +++ b/docs/grpc.md @@ -15,20 +15,23 @@ properly account for this issue or clear out your proxy when running if needed. ## Python Client A python client wrapper is provided at -[CoreGrpcClient](../daemon/core/api/grpc/client.py) to help provide some -conveniences when using the API. +[CoreGrpcClient](https://github.com/coreemu/core/blob/master/daemon/core/api/grpc/client.py) +to help provide some conveniences when using the API. ## Proto Files Proto files are used to define the API and protobuf messages that are used for interfaces with this API. -They can be found [here](../daemon/proto/core/api/grpc) to see the specifics of +They can be found +[here](https://github.com/coreemu/core/tree/master/daemon/proto/core/api/grpc) +to see the specifics of what is going on and response message values that would be returned. ## Examples -Example usage of this API can be found [here](../daemon/examples/grpc). These -examples will create a session using the gRPC API when the core-daemon is running. +Example usage of this API can be found +[here](https://github.com/coreemu/core/tree/master/daemon/examples/grpc). +These examples will create a session using the gRPC API when the core-daemon is running. You can then switch to and attach to these sessions using either of the CORE GUIs. From 897ecc6d356c07041b71f1ba35b28c9f3238cee0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 15 Jul 2020 17:52:34 -0700 Subject: [PATCH 0520/1131] updated install emane task to auto answer yes to installing system packages --- tasks.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tasks.py b/tasks.py index 4b6f2ca6..6f5b7ac3 100644 --- a/tasks.py +++ b/tasks.py @@ -301,14 +301,14 @@ def install_emane(c, verbose=False): with p.start("installing system dependencies"): if os_info.like == OsLike.DEBIAN: c.run( - "sudo apt install gcc g++ automake libtool libxml2-dev libprotobuf-dev " - "libpcap-dev libpcre3-dev uuid-dev pkg-config protobuf-compiler git " - "python3-protobuf python3-setuptools", + "sudo apt install -y gcc g++ automake libtool libxml2-dev " + "libprotobuf-dev libpcap-dev libpcre3-dev uuid-dev pkg-config " + "protobuf-compiler git python3-protobuf python3-setuptools", hide=hide, ) elif os_info.like == OsLike.REDHAT: c.run( - "sudo yum install autoconf automake git libtool libxml2-devel " + "sudo yum install -y autoconf automake git libtool libxml2-devel " "libpcap-devel pcre-devel libuuid-devel make gcc-c++ " "python3-setuptools", hide=hide, From 495fbe5632731168ba02fc233fcc78dfc4536815 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 15 Jul 2020 21:50:35 -0700 Subject: [PATCH 0521/1131] added protobuf-compiler to install emane task --- tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index 6f5b7ac3..d044deb8 100644 --- a/tasks.py +++ b/tasks.py @@ -309,7 +309,7 @@ def install_emane(c, verbose=False): elif os_info.like == OsLike.REDHAT: c.run( "sudo yum install -y autoconf automake git libtool libxml2-devel " - "libpcap-devel pcre-devel libuuid-devel make gcc-c++ " + "libpcap-devel pcre-devel libuuid-devel make gcc-c++ protobuf-compiler " "python3-setuptools", hide=hide, ) From c884ee27cd08b51607e6a694efe257ab93b2ecbe Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 16 Jul 2020 08:42:36 -0700 Subject: [PATCH 0522/1131] removed invoke tasks wrapping scripts, since they can be used directly, added invoke task help strings, add invoke task to run user scripts --- tasks.py | 75 ++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/tasks.py b/tasks.py index d044deb8..1dde1cea 100644 --- a/tasks.py +++ b/tasks.py @@ -194,7 +194,12 @@ def install_ospf_mdr(c: Context, os_info: OsInfo, hide: bool) -> None: c.run("sudo make install", hide=hide) -@task +@task( + help={ + "verbose": "enable verbose", + "prefix": f"prefix where scripts are installed, default is {DEFAULT_PREFIX}" + }, +) def install_service(c, verbose=False, prefix=DEFAULT_PREFIX): """ install systemd core service @@ -225,7 +230,12 @@ def install_service(c, verbose=False, prefix=DEFAULT_PREFIX): print(f"ERROR: systemd service path not found: {systemd_dir}") -@task +@task( + help={ + "verbose": "enable verbose", + "prefix": f"prefix where scripts are installed, default is {DEFAULT_PREFIX}" + }, +) def install_scripts(c, verbose=False, prefix=DEFAULT_PREFIX): """ install core script files, modified to leverage virtual environment @@ -259,7 +269,13 @@ def install_scripts(c, verbose=False, prefix=DEFAULT_PREFIX): c.run(f"sudo cp -n daemon/data/logging.conf {config_dir}", hide=hide) -@task +@task( + help={ + "dev": "install development mode", + "verbose": "enable verbose", + "prefix": f"prefix where scripts are installed, default is {DEFAULT_PREFIX}" + }, +) def install(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): """ install core, poetry, scripts, service, and ospf mdr @@ -288,7 +304,11 @@ def install(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): print("\nyou may need to open a new terminal to leverage invoke for running core") -@task +@task( + help={ + "verbose": "enable verbose", + }, +) def install_emane(c, verbose=False): """ install emane and the python bindings @@ -331,7 +351,13 @@ def install_emane(c, verbose=False): c.run(f"poetry run pip install {emane_dir}/src/python", hide=hide) -@task +@task( + help={ + "dev": "uninstall development mode", + "verbose": "enable verbose", + "prefix": f"prefix where scripts are installed, default is {DEFAULT_PREFIX}" + }, +) def uninstall(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): """ uninstall core @@ -386,31 +412,26 @@ def daemon(c): ) -@task -def gui(c): +@task( + help={ + "sudo": "run script as sudo", + "file": "script file to run in the core virtual environment" + }, +) +def run(c, file, sudo=False): """ - start core-pygui + convenience for running a core related script """ + if not file: + print("no script was provided") + return + python = get_python(c) + path = Path(file).absolute() with c.cd(DAEMON_DIR): - c.run("poetry run scripts/core-pygui", pty=True) - - -@task -def cli(c, args): - """ - run core-cli used to query and modify a running session - """ - with c.cd(DAEMON_DIR): - c.run(f"poetry run scripts/core-cli {args}", pty=True) - - -@task -def cleanup(c): - """ - run core-cleanup removing leftover core nodes, bridges, directories - """ - print("running core-cleanup...") - c.run(f"sudo daemon/scripts/core-cleanup", pty=True) + cmd = f"{python} {path}" + if sudo: + cmd = f"sudo {cmd}" + c.run(cmd, pty=True) @task From d1fd19edc6cd739a845e3722fa9e296a858c125a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 16 Jul 2020 08:47:18 -0700 Subject: [PATCH 0523/1131] updated doc examples for invoke tasks --- docs/install.md | 30 +++++++++++++++++++++++------- tasks.py | 4 ++-- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/docs/install.md b/docs/install.md index 0bff31c9..6a9fb393 100644 --- a/docs/install.md +++ b/docs/install.md @@ -173,22 +173,38 @@ The invoke tool installed by way of pipx provides conveniences for running CORE tasks to help ensure usage of the create python virtual environment. ```shell +inv --list + Available tasks: - cleanup run core-cleanup removing leftover core nodes, bridges, directories - cli run core-cli used to query and modify a running session daemon start core-daemon - gui start core-pygui - install install core, scripts, service, and ospf mdr + install install core, poetry, scripts, service, and ospf mdr + install-emane install emane and the python bindings install-scripts install core script files, modified to leverage virtual environment install-service install systemd core service + run runs a user script in the core virtual environment test run core tests test-emane run core emane tests test-mock run core tests using mock to avoid running as sudo - uninstall uninstall core + uninstall uninstall core, scripts, service, virtual environment, and clean build directory ``` -Example running the core-daemon task from the root of the repo: +Print help for a given task: ```shell -inv daemon +inv -h install + +Usage: inv[oke] [--core-opts] install [--options] [other tasks here ...] + +Docstring: + install core, poetry, scripts, service, and ospf mdr + +Options: + -d, --dev install development mode + -p STRING, --prefix=STRING prefix where scripts are installed, default is /usr/local + -v, --verbose enable verbose +``` + +Example running a core user script: +```shell +inv run /path/to/core/grpc/script.py ``` diff --git a/tasks.py b/tasks.py index 1dde1cea..e1b539a4 100644 --- a/tasks.py +++ b/tasks.py @@ -360,7 +360,7 @@ def install_emane(c, verbose=False): ) def uninstall(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): """ - uninstall core + uninstall core, scripts, service, virtual environment, and clean build directory """ hide = not verbose p = Progress(verbose) @@ -420,7 +420,7 @@ def daemon(c): ) def run(c, file, sudo=False): """ - convenience for running a core related script + runs a user script in the core virtual environment """ if not file: print("no script was provided") From 1212e5ddf8dce4bf797b8fe2be3c3486020e5943 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 16 Jul 2020 08:59:57 -0700 Subject: [PATCH 0524/1131] fix to avoid setting interface data for a mac to the string None, when not present --- daemon/core/nodes/base.py | 3 ++- daemon/core/nodes/network.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 7f444480..cea1e81b 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -1050,8 +1050,9 @@ class CoreNetworkBase(NodeBase): if uni: unidirectional = 1 + mac = str(iface.mac) if iface.mac else None iface2_data = InterfaceData( - id=linked_node.get_iface_id(iface), name=iface.name, mac=str(iface.mac) + id=linked_node.get_iface_id(iface), name=iface.name, mac=mac ) ip4 = iface.get_ip4() if ip4: diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index a55de4cf..58c1e195 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -875,8 +875,9 @@ class PtpNet(CoreNetwork): if iface1.getparams() != iface2.getparams(): unidirectional = 1 + mac = str(iface1.mac) if iface1.mac else None iface1_data = InterfaceData( - id=iface1.node.get_iface_id(iface1), name=iface1.name, mac=str(iface1.mac) + id=iface1.node.get_iface_id(iface1), name=iface1.name, mac=mac ) ip4 = iface1.get_ip4() if ip4: @@ -887,8 +888,9 @@ class PtpNet(CoreNetwork): iface1_data.ip6 = str(ip6.ip) iface1_data.ip6_mask = ip6.prefixlen + mac = str(iface2.mac) if iface2.mac else None iface2_data = InterfaceData( - id=iface2.node.get_iface_id(iface2), name=iface2.name, mac=str(iface2.mac) + id=iface2.node.get_iface_id(iface2), name=iface2.name, mac=mac ) ip4 = iface2.get_ip4() if ip4: From 0be1972a29c7600ffce663348f169b0027deffe7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 16 Jul 2020 09:16:32 -0700 Subject: [PATCH 0525/1131] update to running user scripts in install doc --- docs/install.md | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/install.md b/docs/install.md index 6a9fb393..6129c17f 100644 --- a/docs/install.md +++ b/docs/install.md @@ -136,19 +136,28 @@ installed virtual environment. > **NOTE:** the following assumes CORE has been installed successfully -One way to do this would be to enable the core virtual environment shell. +There is an invoke task to help with this case. +```shell +cd $REPO +inv -h run +Usage: inv[oke] [--core-opts] run [--options] [other tasks here ...] + +Docstring: + runs a user script in the core virtual environment + +Options: + -f STRING, --file=STRING script file to run in the core virtual environment + -s, --sudo run script as sudo +``` + +Another way would be to enable the core virtual environment shell. Which +would allow you to run scripts in a more **normal** way. ```shell cd $REPO/daemon poetry shell python run /path/to/script.py ``` -Another way would be to run the script directly by way of poetry. -```shell -cd $REPO/daemon -poetry run python /path/to/script.py -``` - ## Manually Install EMANE EMANE can be installed from deb or RPM packages or from source. See the @@ -203,8 +212,3 @@ Options: -p STRING, --prefix=STRING prefix where scripts are installed, default is /usr/local -v, --verbose enable verbose ``` - -Example running a core user script: -```shell -inv run /path/to/core/grpc/script.py -``` From b50f05837476e1b524e8cd465bc98bb1865c8ee7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 16 Jul 2020 09:26:08 -0700 Subject: [PATCH 0526/1131] improved emane section in install doc --- docs/install.md | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/install.md b/docs/install.md index 6129c17f..12d47802 100644 --- a/docs/install.md +++ b/docs/install.md @@ -163,15 +163,23 @@ python run /path/to/script.py EMANE can be installed from deb or RPM packages or from source. See the [EMANE GitHub](https://github.com/adjacentlink/emane) for full details. -Here are quick instructions for installing all EMANE packages for Ubuntu 18.04: -```shell -# install dependencies -# ubuntu -sudo apt-get install libssl-dev libxml-libxml-perl libxml-simple-perl -wget https://adjacentlink.com/downloads/emane/emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz -tar xzf emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz +There is an invoke task to help with installing EMANE, but has issues, +which attempts to build EMANE from source, but has issue on systems with + older protobuf-compilers. -# install emane python bindings into the core virtual environment +```shell +cd $REPO +inv install-emane +``` + +Alternatively, you can +[build EMANE](https://github.com/adjacentlink/emane/wiki/Build) +from source and install the python +bindings into the core virtual environment. + +The following would install the EMANE python bindings after being +successfully built. +```shell cd $REPO/daemon poetry run pip install $EMANE_REPO/src/python ``` From db4ef2b42e3582b74d17f01b0f3a9f747b8c97c2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 16 Jul 2020 10:02:56 -0700 Subject: [PATCH 0527/1131] fixed core.conf commented out example path for core-pygui to use .coregui instead of .coretk --- daemon/data/core.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/data/core.conf b/daemon/data/core.conf index 5ff0be7f..20ee5d1f 100644 --- a/daemon/data/core.conf +++ b/daemon/data/core.conf @@ -13,7 +13,7 @@ frr_sbin_search = "/usr/local/sbin /usr/sbin /usr/lib/frr" # this may be a comma-separated list, and directory names should be unique # and not named 'services' #custom_services_dir = /home/username/.core/myservices -#custom_config_services_dir = /home/username/.coretk/custom_services +#custom_config_services_dir = /home/username/.coregui/custom_services # uncomment to establish a standalone control backchannel for accessing nodes # (overriden by the session option of the same name) From 6b550618572fe175ffe9a8178ec93a6cc715c64a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 16 Jul 2020 10:09:26 -0700 Subject: [PATCH 0528/1131] update dev gui doc for new installation --- docs/devguide.md | 59 ++++++++++++------------------------------------ 1 file changed, 15 insertions(+), 44 deletions(-) diff --git a/docs/devguide.md b/docs/devguide.md index c10bb007..9b9d61c8 100644 --- a/docs/devguide.md +++ b/docs/devguide.md @@ -16,7 +16,6 @@ daemon. Here is a brief description of the source directories. |gui|Tcl/Tk GUI| |man|Template files for creating man pages for various CORE command line utilities| |netns|C program for creating CORE containers| -|scripts|Template files used for running CORE as a service| ## Getting started @@ -34,21 +33,11 @@ git checkout develop ## Install the Development Environment This command will automatically install system dependencies, clone and build OSPF-MDR, -build CORE, setup the CORE pipenv environment, and install pre-commit hooks. - -This script is currently compatible with Ubuntu and CentOS, tested on Ubuntu 18.04 and -CentOS 7.6. The script also currently defaults to using python3.6, but a different -version of python can be targeted if python3.6 is not available on your system. +build CORE, setup the CORE poetry environment, and install pre-commit hooks. You can +refer to the [install docs](install.md) for issues related to different distributions. ```shell -# default dev install using python3.6 -./install.sh -d - -# providing a newer python version for ubuntu -./install.sh -d -v 3.7 - -# providing a newer python version for centos -./install.sh -d -v 37 +./install -d ``` ### pre-commit @@ -57,42 +46,24 @@ pre-commit hooks help automate running tools to check modified code. Every time python utilities will be ran to check validity of code, potentially failing and backing out the commit. These changes are currently mandated as part of the current CI, so add the changes and commit again. -### Adding EMANE to Pipenv - -EMANE bindings are not available through pip, you will need to build and install from source. - -[Build EMANE](https://github.com/adjacentlink/emane/wiki/Build#general-build-instructions) - -```shell -# clone emane repo -git clone https://github.com/adjacentlink/emane.git - -# install emane build deps -sudo apt install libxml2-dev libprotobuf-dev uuid-dev libpcap-dev protobuf-compiler - -# build emane -./autogen.sh -./configure --prefix=/usr -make -j8 - -# install emane binding in pipenv -# NOTE: this will mody pipenv Pipfiles and we do not want that, use git checkout -- Pipfile*, to remove changes -python3 -m pipenv pip install $EMANEREPO/src/python -``` - ## Running CORE -Commands below can be used to run the core-daemon, the new core gui, and tests. +You can now run core as you normally would, or leverage some of the invoke tasks to +conveniently run tests, etc. ```shell -# runs for daemon -sudo python3 -m pipenv run core +# run core-daemon +sudo core-daemon -# runs coretk gui -python3 -m pipenv run core-pygui +# run python gui +core-pygui -# runs mocked unit tests -python3 -m pipenv run test-mock +# run tcl gui +core-gui + +# run mocked unit tests +cd $REPO +inv test-mock ``` ## Linux Network Namespace Commands From 1c2d7c6d12be0cf3be9b3822b88d373af933fc36 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 16 Jul 2020 10:35:16 -0700 Subject: [PATCH 0529/1131] added reinstall invoke task, added some simple detections for old core installations in install task --- tasks.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tasks.py b/tasks.py index e1b539a4..931aaf93 100644 --- a/tasks.py +++ b/tasks.py @@ -107,6 +107,15 @@ def get_os() -> OsInfo: return OsInfo(name, like, version) +def check_existing_core(c: Context, hide: bool) -> None: + if c.run("python -c \"import core\"", warn=True, hide=hide): + raise SystemError("existing python2 core installation detected, please remove") + if c.run("python3 -c \"import core\"", warn=True, hide=hide): + raise SystemError("existing python3 core installation detected, please remove") + if c.run("which core-daemon", warn=True, hide=hide): + raise SystemError("core scripts found, please remove old installation") + + def install_system(c: Context, os_info: OsInfo, hide: bool) -> None: if os_info.like == OsLike.DEBIAN: c.run( @@ -285,6 +294,8 @@ def install(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): p = Progress(verbose) hide = not verbose os_info = get_os() + with p.start("checking for old installations"): + check_existing_core(c, hide) with p.start("installing system dependencies"): install_system(c, os_info, hide) with p.start("installing system grpcio-tools"): @@ -398,6 +409,33 @@ def uninstall(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): c.run(f"sudo rm -f {service_file}", hide=hide) +@task( + help={ + "dev": "reinstall development mode", + "verbose": "enable verbose", + "prefix": f"prefix where scripts are installed, default is {DEFAULT_PREFIX}", + "branch": "branch to install latest code from, default is current branch" + }, +) +def reinstall(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX, branch=None): + """ + run the uninstall task, get latest from specified branch, and run install task + """ + uninstall(c, dev, verbose, prefix) + hide = not verbose + p = Progress(verbose) + with p.start("pulling latest code"): + current = c.run("git rev-parse --abbrev-ref HEAD", hide=hide).stdout.strip() + if branch and branch != current: + c.run(f"git checkout {branch}") + else: + branch = current + c.run("git pull", hide=hide) + if not Path("tasks.py").exists(): + raise FileNotFoundError(f"missing tasks.py on branch: {branch}") + install(c, dev, verbose, prefix) + + @task def daemon(c): """ From 6219d08416362e5aa6c2a41b53a967270408888f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 16 Jul 2020 11:04:52 -0700 Subject: [PATCH 0530/1131] enable centos 8 check to enable powertools repo for centos 8 when installing emane --- tasks.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tasks.py b/tasks.py index 931aaf93..5f52f444 100644 --- a/tasks.py +++ b/tasks.py @@ -289,8 +289,8 @@ def install(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): """ install core, poetry, scripts, service, and ospf mdr """ - c.run("sudo -v", hide=True) print(f"installing core with prefix: {prefix}") + c.run("sudo -v", hide=True) p = Progress(verbose) hide = not verbose os_info = get_os() @@ -338,10 +338,12 @@ def install_emane(c, verbose=False): hide=hide, ) elif os_info.like == OsLike.REDHAT: + if os_info.name == OsName.CENTOS and os_info.version >= 8: + c.run("sudo yum config-manager --set-enabled PowerTools", hide=hide) c.run( "sudo yum install -y autoconf automake git libtool libxml2-devel " "libpcap-devel pcre-devel libuuid-devel make gcc-c++ protobuf-compiler " - "python3-setuptools", + "protobuf-devel python3-setuptools", hide=hide, ) with p.start("cloning emane"): @@ -373,6 +375,7 @@ def uninstall(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): """ uninstall core, scripts, service, virtual environment, and clean build directory """ + print(f"uninstalling core with prefix: {prefix}") hide = not verbose p = Progress(verbose) c.run("sudo -v", hide=True) From 35b4c157a097528718e731a575db483e3fb83793 Mon Sep 17 00:00:00 2001 From: Shawn Kelly O'Shea Date: Thu, 16 Jul 2020 15:22:33 -0400 Subject: [PATCH 0531/1131] Increase height of options dialogue in TCL gui We have emane models with a large list of options. Without this modification, a user cannot access all of the options provided by the emane model (some of the options are cutoff and cannot be scrolled-down to). --- gui/plugins.tcl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/plugins.tcl b/gui/plugins.tcl index 95c1a203..fdb5c454 100644 --- a/gui/plugins.tcl +++ b/gui/plugins.tcl @@ -672,11 +672,11 @@ proc popupCapabilityConfig { channel wlan model types values captions bmp possib pack $windowScroll -fill y -side right pack $windowCanvas -expand yes -fill both -side top - frame $windowCanvas.notebookFrame -width 700 -height 1200 + frame $windowCanvas.notebookFrame -width 700 -height 2400 set notebookFrame $windowCanvas.notebookFrame pack $notebookFrame -fill both -expand yes -padx 5 -pady 5 - ttk::notebook $notebookFrame.vals -width 690 -height 1200 + ttk::notebook $notebookFrame.vals -width 690 -height 2400 set configNotebook $notebookFrame.vals ttk::notebook::enableTraversal $configNotebook pack $configNotebook -fill both -expand yes From fdf00cff0e85bb5f5ce47250364849c51e456438 Mon Sep 17 00:00:00 2001 From: apwiggins Date: Thu, 16 Jul 2020 18:00:12 -0300 Subject: [PATCH 0532/1131] Update frr.py to add staticd to daemons list Add staticd to the list of possible daemons to be started. http://docs.frrouting.org/en/latest/setup.html#daemons-configuration-file https://github.com/coreemu/core/issues/397 --- daemon/core/services/frr.py | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py index 9a344339..e3675bc7 100644 --- a/daemon/core/services/frr.py +++ b/daemon/core/services/frr.py @@ -271,6 +271,7 @@ nhrpd=yes eigrpd=yes babeld=yes sharpd=yes +staticd=yes pbrd=yes bfdd=yes fabricd=yes From 36123e7aa545b9fd04cedfa2169a7ea12612e985 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 16 Jul 2020 14:21:06 -0700 Subject: [PATCH 0533/1131] updated frr daemons template file for the config service to align with changes to normal service --- daemon/core/configservices/frrservices/templates/daemons | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon/core/configservices/frrservices/templates/daemons b/daemon/core/configservices/frrservices/templates/daemons index 0f6bda53..dbd42108 100644 --- a/daemon/core/configservices/frrservices/templates/daemons +++ b/daemon/core/configservices/frrservices/templates/daemons @@ -20,6 +20,7 @@ nhrpd=yes eigrpd=yes babeld=yes sharpd=yes +staticd=yes pbrd=yes bfdd=yes fabricd=yes From 6d4434bc1274c5aa7612ff289a97b646f6b477cc Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 16 Jul 2020 22:51:26 -0700 Subject: [PATCH 0534/1131] grpc: added set session user call, updated mobility to look for files within new gui as well, fixed pygui issue when start session has a grpc exceptions, showing and empty error window --- daemon/core/api/grpc/client.py | 14 ++++++++++ daemon/core/api/grpc/server.py | 15 ++++++++++ daemon/core/gui/coreclient.py | 5 ++++ daemon/core/gui/data/xmls/sample1.xml | 2 +- daemon/core/gui/toolbar.py | 2 +- daemon/core/location/mobility.py | 40 +++++++++++++-------------- daemon/proto/core/api/grpc/core.proto | 11 ++++++++ 7 files changed, 66 insertions(+), 23 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 20e193eb..3e974233 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -414,6 +414,20 @@ class CoreGrpcClient: request = core_pb2.SetSessionStateRequest(session_id=session_id, state=state) return self.stub.SetSessionState(request) + def set_session_user( + self, session_id: int, user: str + ) -> core_pb2.SetSessionUserResponse: + """ + Set session user, used for helping to find files without full paths. + + :param session_id: id of session + :param user: user to set for session + :return: response with result of success or failure + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.SetSessionUserRequest(session_id=session_id, user=user) + return self.stub.SetSessionUser(request) + def add_session_server( self, session_id: int, name: str, host: str ) -> core_pb2.AddSessionServerResponse: diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 5bdebac6..da2d53c3 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -448,6 +448,21 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): return core_pb2.SetSessionStateResponse(result=result) + def SetSessionUser( + self, request: core_pb2.SetSessionUserRequest, context: ServicerContext + ) -> core_pb2.SetSessionUserResponse: + """ + Sets the user for a session. + + :param request: set session user request + :param context: context object + :return: set session user response + """ + logging.debug("set session user: %s", request) + session = self.get_session(request.session_id, context) + session.user = request.user + return core_pb2.SetSessionUserResponse(result=True) + def GetSessionOptions( self, request: core_pb2.GetSessionOptionsRequest, context: ServicerContext ) -> core_pb2.GetSessionOptionsResponse: diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 255192be..52023e14 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -1,6 +1,7 @@ """ Incorporate grpc into python tkinter GUI """ +import getpass import json import logging import os @@ -71,6 +72,7 @@ class CoreClient: self.default_services: Dict[NodeType, Set[str]] = {} self.emane_models: List[str] = [] self.observer: Optional[str] = None + self.user = getpass.getuser() # loaded configuration data self.servers: Dict[str, CoreServer] = {} @@ -289,6 +291,9 @@ class CoreClient: self.session_id, self.handle_events ) + # set session user + self.client.set_session_user(self.session_id, self.user) + # get session service defaults response = self.client.get_service_defaults(self.session_id) self.default_services = { diff --git a/daemon/core/gui/data/xmls/sample1.xml b/daemon/core/gui/data/xmls/sample1.xml index afec8874..5055c225 100644 --- a/daemon/core/gui/data/xmls/sample1.xml +++ b/daemon/core/gui/data/xmls/sample1.xml @@ -188,7 +188,7 @@ - + diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index c3e9067f..406a88ca 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -305,7 +305,7 @@ class Toolbar(ttk.Frame): self.set_runtime() self.app.core.set_metadata() self.app.core.show_mobility_players() - else: + elif response.exceptions: enable_buttons(self.design_frame, enabled=True) message = "\n".join(response.exceptions) self.app.show_error("Start Session Error", message) diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index f2e0f470..e982c5c1 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -5,10 +5,10 @@ mobility.py: mobility helpers for moving nodes and calculating wireless range. import heapq import logging import math -import os import threading import time from functools import total_ordering +from pathlib import Path from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple from core import utils @@ -1030,30 +1030,28 @@ class Ns2ScriptedMobility(WayPointMobility): def findfile(self, file_name: str) -> str: """ Locate a script file. If the specified file doesn't exist, look in the - same directory as the scenario file, or in the default - configs directory (~/.core/configs). This allows for sample files without - absolute path names. + same directory as the scenario file, or in gui directories. :param file_name: file name to find :return: absolute path to the file + :raises CoreError: when file is not found """ - if os.path.exists(file_name): - return file_name - - if self.session.file_name is not None: - d = os.path.dirname(self.session.file_name) - sessfn = os.path.join(d, file_name) - if os.path.exists(sessfn): - return sessfn - - if self.session.user is not None: - userfn = os.path.join( - "/home", self.session.user, ".core", "configs", file_name - ) - if os.path.exists(userfn): - return userfn - - return file_name + file_path = Path(file_name).expanduser() + if file_path.exists(): + return str(file_path) + if self.session.file_name: + file_path = Path(self.session.file_name).parent / file_name + if file_path.exists(): + return str(file_path) + if self.session.user: + user_path = Path(f"~{self.session.user}").expanduser() + file_path = user_path / ".core" / "configs" / file_name + if file_path.exists(): + return str(file_path) + file_path = user_path / ".coregui" / "mobility" / file_name + if file_path.exists(): + return str(file_path) + raise CoreError(f"invalid file: {file_name}") def parsemap(self, mapstr: str) -> None: """ diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index f01fca50..5ca4812c 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -39,6 +39,8 @@ service CoreApi { } rpc SetSessionState (SetSessionStateRequest) returns (SetSessionStateResponse) { } + rpc SetSessionUser (SetSessionUserRequest) returns (SetSessionUserResponse) { + } rpc AddSessionServer (AddSessionServerRequest) returns (AddSessionServerResponse) { } @@ -297,6 +299,15 @@ message SetSessionStateResponse { bool result = 1; } +message SetSessionUserRequest { + int32 session_id = 1; + string user = 2; +} + +message SetSessionUserResponse { + bool result = 1; +} + message AddSessionServerRequest { int32 session_id = 1; string name = 2; From 5e2ca0f5497b2b5ed43e104d29860a5a1297af1b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 18 Jul 2020 11:56:48 -0700 Subject: [PATCH 0535/1131] daemon: refactored how to get required commands, added usage of this func for validating distributed servers when added --- daemon/core/emulator/coreemu.py | 9 ++------- daemon/core/emulator/distributed.py | 12 +++++++++++- daemon/core/executables.py | 15 +++++++++++++++ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py index 016f2e5b..c07d8c95 100644 --- a/daemon/core/emulator/coreemu.py +++ b/daemon/core/emulator/coreemu.py @@ -9,7 +9,7 @@ import core.services from core import configservices, utils from core.configservice.manager import ConfigServiceManager from core.emulator.session import Session -from core.executables import COMMON_REQUIREMENTS, OVS_REQUIREMENTS, VCMD_REQUIREMENTS +from core.executables import get_requirements from core.services.coreservices import ServiceManager @@ -79,13 +79,8 @@ class CoreEmu: :return: nothing :raises core.errors.CoreError: when an executable does not exist on path """ - requirements = COMMON_REQUIREMENTS use_ovs = self.config.get("ovs") == "1" - if use_ovs: - requirements += OVS_REQUIREMENTS - else: - requirements += VCMD_REQUIREMENTS - for requirement in requirements: + for requirement in get_requirements(use_ovs): utils.which(requirement, required=True) def load_services(self) -> None: diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index 381eb019..a5e1009f 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -14,7 +14,8 @@ from fabric import Connection from invoke import UnexpectedExit from core import utils -from core.errors import CoreCommandError +from core.errors import CoreCommandError, CoreError +from core.executables import get_requirements from core.nodes.interface import GreTap from core.nodes.network import CoreNetwork, CtrlNet @@ -131,8 +132,17 @@ class DistributedController: :param name: distributed server name :param host: distributed server host address :return: nothing + :raises CoreError: when there is an error validating server """ server = DistributedServer(name, host) + for requirement in get_requirements(self.session.use_ovs()): + try: + server.remote_cmd(f"which {requirement}") + except CoreCommandError: + raise CoreError( + f"server({server.name}) failed validation for " + f"command({requirement})" + ) self.servers[name] = server cmd = f"mkdir -p {self.session.session_dir}" server.remote_cmd(cmd) diff --git a/daemon/core/executables.py b/daemon/core/executables.py index 17aecc1d..6eb0214a 100644 --- a/daemon/core/executables.py +++ b/daemon/core/executables.py @@ -14,3 +14,18 @@ OVS_VSCTL: str = "ovs-vsctl" COMMON_REQUIREMENTS: List[str] = [SYSCTL, IP, ETHTOOL, TC, EBTABLES, MOUNT, UMOUNT] VCMD_REQUIREMENTS: List[str] = [VNODED, VCMD] OVS_REQUIREMENTS: List[str] = [OVS_VSCTL] + + +def get_requirements(use_ovs: bool) -> List[str]: + """ + Retrieve executable requirements needed to run CORE. + + :param use_ovs: True if OVS is being used, False otherwise + :return: list of executable requirements + """ + requirements = COMMON_REQUIREMENTS + if use_ovs: + requirements += OVS_REQUIREMENTS + else: + requirements += VCMD_REQUIREMENTS + return requirements From d5d5da72560c866e2450640e04cb9e7488d9e9b2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 21 Jul 2020 10:08:12 -0700 Subject: [PATCH 0536/1131] bumped version to 7.0.0 --- configure.ac | 2 +- daemon/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index 10d30c20..60f6709e 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, 6.5.0) +AC_INIT(core, 7.0.0) # autoconf and automake initialization AC_CONFIG_SRCDIR([netns/version.h.in]) diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml index 1fdc9d1a..b75f1ee3 100644 --- a/daemon/pyproject.toml +++ b/daemon/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "core" -version = "6.6.0" +version = "7.0.0" description = "CORE Common Open Research Emulator" authors = ["Boeing Research and Technology"] license = "BSD-2-Clause" From 45bfa9fdadf0b95da3d4755c1a682d34be8319ce Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 21 Jul 2020 16:52:17 -0700 Subject: [PATCH 0537/1131] small tweaks to docs --- docs/devguide.md | 2 +- docs/install.md | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/devguide.md b/docs/devguide.md index 9b9d61c8..ba34a211 100644 --- a/docs/devguide.md +++ b/docs/devguide.md @@ -62,7 +62,7 @@ core-pygui core-gui # run mocked unit tests -cd $REPO +cd inv test-mock ``` diff --git a/docs/install.md b/docs/install.md index 12d47802..604ac509 100644 --- a/docs/install.md +++ b/docs/install.md @@ -58,7 +58,7 @@ before proceeding to install. Previous install was built from source: ```shell -cd $REPO +cd sudo make uninstall make clean ./bootstrap.sh clean @@ -138,7 +138,7 @@ installed virtual environment. There is an invoke task to help with this case. ```shell -cd $REPO +cd inv -h run Usage: inv[oke] [--core-opts] run [--options] [other tasks here ...] @@ -153,7 +153,7 @@ Options: Another way would be to enable the core virtual environment shell. Which would allow you to run scripts in a more **normal** way. ```shell -cd $REPO/daemon +cd /daemon poetry shell python run /path/to/script.py ``` @@ -168,7 +168,7 @@ which attempts to build EMANE from source, but has issue on systems with older protobuf-compilers. ```shell -cd $REPO +cd inv install-emane ``` @@ -180,8 +180,8 @@ bindings into the core virtual environment. The following would install the EMANE python bindings after being successfully built. ```shell -cd $REPO/daemon -poetry run pip install $EMANE_REPO/src/python +cd /daemon +poetry run pip install /src/python ``` ## Using Invoke Tasks From 165e404184e92858280b16eb2cd013651ad75abe Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 22 Jul 2020 12:49:11 -0700 Subject: [PATCH 0538/1131] added example dockerfile and build command to readme --- daemon/examples/docker/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/daemon/examples/docker/README.md b/daemon/examples/docker/README.md index 3c2b1372..17c6cb90 100644 --- a/daemon/examples/docker/README.md +++ b/daemon/examples/docker/README.md @@ -44,3 +44,18 @@ newgrp docker This directory provides a few small examples creating Docker nodes and linking them to themselves or with standard CORE nodes. + +Images used by nodes need to have networking tools installed for CORE to automate +setup and configuration of the container. + +Example Dockerfile: +``` +FROM ubuntu:latest +RUN apt-get update +RUN apt-get install -y iproute2 ethtool +``` + +Build image: +```shell +sudo docker build -t . +``` From e34002b851ec51fd74db8e56e6885f98aa8e41d9 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 22 Jul 2020 17:18:35 -0700 Subject: [PATCH 0539/1131] pygui: added option to launch core-pygui into a specific session using an id --- daemon/core/gui/app.py | 4 ++-- daemon/core/gui/coreclient.py | 24 ++++++++++++++++++------ daemon/core/gui/dialogs/error.py | 25 ++++++++++--------------- daemon/examples/grpc/switch.py | 4 ++-- daemon/scripts/core-pygui | 3 ++- 5 files changed, 34 insertions(+), 26 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index e0121d14..176b31e3 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -26,7 +26,7 @@ HEIGHT: int = 800 class Application(ttk.Frame): - def __init__(self, proxy: bool) -> None: + def __init__(self, proxy: bool, session_id: int = None) -> None: super().__init__() # load node icons NodeUtils.setup() @@ -56,7 +56,7 @@ class Application(ttk.Frame): self.core: CoreClient = CoreClient(self, proxy) self.setup_app() self.draw() - self.core.setup() + self.core.setup(session_id) def setup_scaling(self) -> None: self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()} diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 52023e14..26a5a390 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -473,7 +473,7 @@ class CoreClient: except grpc.RpcError as e: self.app.show_grpc_exception("Delete Session Error", e) - def setup(self) -> None: + def setup(self, session_id: int = None) -> None: """ Query sessions, if there exist any, prompt whether to join one """ @@ -494,14 +494,26 @@ class CoreClient: ) group_services.add(service.name) - # if there are no sessions, create a new session, else join a session + # join provided session, create new session, or show dialog to select an + # existing session response = self.client.get_sessions() sessions = response.sessions - if len(sessions) == 0: - self.create_new_session() + if session_id: + session_ids = set(x.id for x in sessions) + if session_id not in session_ids: + dialog = ErrorDialog( + self.app, "Join Session Error", f"{session_id} does not exist" + ) + dialog.show() + self.app.close() + else: + self.join_session(session_id) else: - dialog = SessionsDialog(self.app, True) - dialog.show() + if not sessions: + self.create_new_session() + else: + dialog = SessionsDialog(self.app, True) + dialog.show() except grpc.RpcError as e: logging.exception("core setup error") dialog = ErrorDialog(self.app, "Setup Error", e.details()) diff --git a/daemon/core/gui/dialogs/error.py b/daemon/core/gui/dialogs/error.py index 7fb81077..9d215e82 100644 --- a/daemon/core/gui/dialogs/error.py +++ b/daemon/core/gui/dialogs/error.py @@ -1,9 +1,10 @@ +import tkinter as tk from tkinter import ttk from typing import TYPE_CHECKING, Optional from core.gui.dialogs.dialog import Dialog from core.gui.images import ImageEnum, Images -from core.gui.themes import FRAME_PAD, PADX, PADY +from core.gui.themes import PADY from core.gui.widgets import CodeText if TYPE_CHECKING: @@ -21,21 +22,15 @@ class ErrorDialog(Dialog): def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(1, weight=1) - - frame = ttk.Frame(self.top, padding=FRAME_PAD) - frame.grid(pady=PADY, sticky="ew") - frame.columnconfigure(1, weight=1) - image = Images.get(ImageEnum.ERROR, 36) - label = ttk.Label(frame, image=image) + image = Images.get(ImageEnum.ERROR, 24) + label = ttk.Label( + self.top, text=self.title, image=image, compound=tk.LEFT, anchor=tk.CENTER + ) label.image = image - label.grid(row=0, column=0, padx=PADX) - label = ttk.Label(frame, text=self.title) - label.grid(row=0, column=1, sticky="ew") - + label.grid(sticky=tk.EW, pady=PADY) self.error_message = CodeText(self.top) self.error_message.text.insert("1.0", self.details) - self.error_message.text.config(state="disabled") - self.error_message.grid(sticky="nsew", pady=PADY) - + self.error_message.text.config(state=tk.DISABLED) + self.error_message.grid(sticky=tk.NSEW, pady=PADY) button = ttk.Button(self.top, text="Close", command=lambda: self.destroy()) - button.grid(sticky="ew") + button.grid(sticky=tk.EW) diff --git a/daemon/examples/grpc/switch.py b/daemon/examples/grpc/switch.py index 1ed7c684..79a4e621 100644 --- a/daemon/examples/grpc/switch.py +++ b/daemon/examples/grpc/switch.py @@ -40,14 +40,14 @@ def main(): # create node one position = Position(x=100, y=100) - node1 = Node(type=NodeType.DEFAULT, position=position) + node1 = Node(type=NodeType.DEFAULT, position=position, model="PC") response = core.add_node(session_id, node1) logging.info("created node: %s", response) node1_id = response.node_id # create node two position = Position(x=300, y=100) - node2 = Node(type=NodeType.DEFAULT, position=position) + node2 = Node(type=NodeType.DEFAULT, position=position, model="PC") response = core.add_node(session_id, node2) logging.info("created node: %s", response) node2_id = response.node_id diff --git a/daemon/scripts/core-pygui b/daemon/scripts/core-pygui index 46860ce9..888f4171 100755 --- a/daemon/scripts/core-pygui +++ b/daemon/scripts/core-pygui @@ -13,6 +13,7 @@ if __name__ == "__main__": parser.add_argument("-l", "--level", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], default="INFO", help="logging level") parser.add_argument("-p", "--proxy", action="store_true", help="enable proxy") + parser.add_argument("-s", "--session", type=int, help="session id to join") args = parser.parse_args() # check home directory exists and create if necessary @@ -28,5 +29,5 @@ if __name__ == "__main__": # start app Images.load_all() - app = Application(args.proxy) + app = Application(args.proxy, args.session) app.mainloop() From f8d862a296d995b242e86a1decb25d90587f5f6e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 22 Jul 2020 19:19:22 -0700 Subject: [PATCH 0540/1131] grpc/pygui: added grpc alert api, updated pygui to better handle and display alerts --- daemon/core/api/grpc/client.py | 17 +++++++++++ daemon/core/api/grpc/server.py | 16 ++++++++++- daemon/core/gui/coreclient.py | 2 +- daemon/core/gui/dialogs/alerts.py | 13 +++++---- daemon/core/gui/statusbar.py | 25 ++++++++++++++-- daemon/core/gui/themes.py | 41 +++++++++++++-------------- daemon/proto/core/api/grpc/core.proto | 14 +++++++++ 7 files changed, 97 insertions(+), 31 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 3e974233..aacfa4f6 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -445,6 +445,23 @@ class CoreGrpcClient: ) return self.stub.AddSessionServer(request) + def alert( + self, + session_id: int, + level: core_pb2.ExceptionLevel, + source: str, + text: str, + node_id: int = None, + ) -> core_pb2.SessionAlertResponse: + request = core_pb2.SessionAlertRequest( + session_id=session_id, + level=level, + source=source, + text=text, + node_id=node_id, + ) + return self.stub.SessionAlert(request) + def events( self, session_id: int, diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index da2d53c3..4c204845 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -109,7 +109,12 @@ from core.api.grpc.wlan_pb2 import ( ) from core.emulator.coreemu import CoreEmu from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions -from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags +from core.emulator.enumerations import ( + EventTypes, + ExceptionLevels, + LinkTypes, + MessageFlags, +) from core.emulator.session import NT, Session from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility @@ -584,6 +589,15 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session.distributed.add_server(request.name, request.host) return core_pb2.AddSessionServerResponse(result=True) + def SessionAlert( + self, request: core_pb2.SessionAlertRequest, context: ServicerContext + ) -> core_pb2.SessionAlertResponse: + session = self.get_session(request.session_id, context) + level = ExceptionLevels(request.level) + node_id = request.node_id if request.node_id else None + session.exception(level, request.source, request.text, node_id) + return core_pb2.SessionAlertResponse(result=True) + def Events(self, request: core_pb2.EventsRequest, context: ServicerContext) -> None: session = self.get_session(request.session_id, context) event_types = set(request.events) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 26a5a390..8474b3cb 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -271,7 +271,7 @@ class CoreClient: def handle_exception_event(self, event: ExceptionEvent) -> None: logging.info("exception event: %s", event) - self.app.statusbar.core_alarms.append(event) + self.app.statusbar.add_alert(event) def join_session(self, session_id: int, query_location: bool = True) -> None: logging.info("join session(%s)", session_id) diff --git a/daemon/core/gui/dialogs/alerts.py b/daemon/core/gui/dialogs/alerts.py index 00ef1e8c..8e0aa02e 100644 --- a/daemon/core/gui/dialogs/alerts.py +++ b/daemon/core/gui/dialogs/alerts.py @@ -52,6 +52,7 @@ class AlertsDialog(Dialog): for alarm in self.app.statusbar.core_alarms: exception = alarm.exception_event level_name = ExceptionLevel.Enum.Name(exception.level) + node_id = exception.node_id if exception.node_id else "" insert_id = self.tree.insert( "", tk.END, @@ -60,7 +61,7 @@ class AlertsDialog(Dialog): exception.date, level_name, alarm.session_id, - exception.node_id, + node_id, exception.source, ), tags=(level_name,), @@ -98,15 +99,17 @@ class AlertsDialog(Dialog): button.grid(row=0, column=1, sticky="ew") def reset_alerts(self) -> None: - self.codetext.text.delete("1.0", tk.END) + self.codetext.text.config(state=tk.NORMAL) + self.codetext.text.delete(1.0, tk.END) + self.codetext.text.config(state=tk.DISABLED) for item in self.tree.get_children(): self.tree.delete(item) - self.app.statusbar.core_alarms.clear() + self.app.statusbar.clear_alerts() def click_select(self, event: tk.Event) -> None: current = self.tree.selection()[0] alarm = self.alarm_map[current] self.codetext.text.config(state=tk.NORMAL) - self.codetext.text.delete("1.0", "end") - self.codetext.text.insert("1.0", alarm.exception_event.text) + self.codetext.text.delete(1.0, tk.END) + self.codetext.text.insert(1.0, alarm.exception_event.text) self.codetext.text.config(state=tk.DISABLED) diff --git a/daemon/core/gui/statusbar.py b/daemon/core/gui/statusbar.py index 2b597b63..67da0efa 100644 --- a/daemon/core/gui/statusbar.py +++ b/daemon/core/gui/statusbar.py @@ -5,7 +5,7 @@ import tkinter as tk from tkinter import ttk from typing import TYPE_CHECKING, List, Optional -from core.api.grpc.core_pb2 import ExceptionEvent +from core.api.grpc.core_pb2 import ExceptionEvent, ExceptionLevel from core.gui.dialogs.alerts import AlertsDialog from core.gui.themes import Styles @@ -22,6 +22,7 @@ class StatusBar(ttk.Frame): self.zoom: Optional[ttk.Label] = None self.cpu_usage: Optional[ttk.Label] = None self.alerts_button: Optional[ttk.Button] = None + self.alert_style = Styles.no_alert self.running: bool = False self.core_alarms: List[ExceptionEvent] = [] self.draw() @@ -60,10 +61,30 @@ class StatusBar(ttk.Frame): self.cpu_usage.grid(row=0, column=2, sticky="ew") self.alerts_button = ttk.Button( - self, text="Alerts", command=self.click_alerts, style=Styles.green_alert + self, text="Alerts", command=self.click_alerts, style=self.alert_style ) self.alerts_button.grid(row=0, column=3, sticky="ew") + def add_alert(self, event: ExceptionEvent) -> None: + self.core_alarms.append(event) + level = event.exception_event.level + self._set_alert_style(level) + label = f"Alerts ({len(self.core_alarms)})" + self.alerts_button.config(text=label, style=self.alert_style) + + def _set_alert_style(self, level: ExceptionLevel) -> None: + if level in [ExceptionLevel.FATAL, ExceptionLevel.ERROR]: + self.alert_style = Styles.red_alert + elif level == ExceptionLevel.WARNING and self.alert_style != Styles.red_alert: + self.alert_style = Styles.yellow_alert + elif self.alert_style == Styles.no_alert: + self.alert_style = Styles.green_alert + + def clear_alerts(self): + self.core_alarms.clear() + self.alert_style = Styles.no_alert + self.alerts_button.config(text="Alerts", style=self.alert_style) + def click_alerts(self) -> None: dialog = AlertsDialog(self.app) dialog.show() diff --git a/daemon/core/gui/themes.py b/daemon/core/gui/themes.py index 93a0a599..45b109f0 100644 --- a/daemon/core/gui/themes.py +++ b/daemon/core/gui/themes.py @@ -14,6 +14,7 @@ class Styles: tooltip_frame: str = "Tooltip.TFrame" service_checkbutton: str = "Service.TCheckbutton" picker_button: str = "Picker.TButton" + no_alert: str = "NAlert.TButton" green_alert: str = "GAlert.TButton" red_alert: str = "RAlert.TButton" yellow_alert: str = "YAlert.TButton" @@ -175,33 +176,29 @@ def style_listbox(widget: tk.Widget) -> None: ) +def _alert_style(style: ttk.Style, name: str, background: str): + style.configure( + name, + background=background, + padding=0, + relief=tk.RIDGE, + borderwidth=1, + font="TkDefaultFont", + foreground="black", + highlightbackground="white", + ) + style.map(name, background=[("!active", background), ("active", "white")]) + + def theme_change(event: tk.Event) -> None: style = ttk.Style() style.configure(Styles.picker_button, font="TkSmallCaptionFont") style.configure( - Styles.green_alert, - background="green", - padding=0, - relief=tk.RIDGE, - borderwidth=1, - font="TkDefaultFont", - ) - style.configure( - Styles.yellow_alert, - background="yellow", - padding=0, - relief=tk.RIDGE, - borderwidth=1, - font="TkDefaultFont", - ) - style.configure( - Styles.red_alert, - background="red", - padding=0, - relief=tk.RIDGE, - borderwidth=1, - font="TkDefaultFont", + Styles.no_alert, padding=0, relief=tk.RIDGE, borderwidth=1, font="TkDefaultFont" ) + _alert_style(style, Styles.green_alert, "green") + _alert_style(style, Styles.yellow_alert, "yellow") + _alert_style(style, Styles.red_alert, "red") def scale_fonts(fonts_size: Dict[str, int], scale: float) -> None: diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 5ca4812c..eb889d14 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -43,6 +43,8 @@ service CoreApi { } rpc AddSessionServer (AddSessionServerRequest) returns (AddSessionServerResponse) { } + rpc SessionAlert (SessionAlertRequest) returns (SessionAlertResponse) { + } // streams rpc Events (EventsRequest) returns (stream Event) { @@ -318,6 +320,18 @@ message AddSessionServerResponse { bool result = 1; } +message SessionAlertRequest { + int32 session_id = 1; + ExceptionLevel.Enum level = 2; + string source = 3; + string text = 4; + int32 node_id = 5; +} + +message SessionAlertResponse { + bool result = 1; +} + message EventsRequest { int32 session_id = 1; repeated EventType.Enum events = 2; From 3544d004317b5ddbc5053e8f38c09e005fd582e7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 22 Jul 2020 21:57:05 -0700 Subject: [PATCH 0541/1131] pygui: implemented cpu usage monitor to status bar --- daemon/core/gui/graph/graph.py | 3 +- daemon/core/gui/statusbar.py | 64 ++++++++++++++++++++++++++++------ 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 9cb3b109..56a31c3f 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -590,8 +590,7 @@ class CanvasGraph(tk.Canvas): ) logging.debug("ratio: %s", self.ratio) logging.debug("offset: %s", self.offset) - zoom_label = f"{self.ratio * 100:.0f}%" - self.app.statusbar.zoom.config(text=zoom_label) + self.app.statusbar.set_zoom(self.ratio) if self.wallpaper: self.redraw_wallpaper() diff --git a/daemon/core/gui/statusbar.py b/daemon/core/gui/statusbar.py index 67da0efa..e9fc03b2 100644 --- a/daemon/core/gui/statusbar.py +++ b/daemon/core/gui/statusbar.py @@ -1,7 +1,10 @@ """ status bar """ +import sched import tkinter as tk +from pathlib import Path +from threading import Thread from tkinter import ttk from typing import TYPE_CHECKING, List, Optional @@ -13,6 +16,41 @@ if TYPE_CHECKING: from core.gui.app import Application +class CpuUsage: + def __init__(self, statusbar: "StatusBar") -> None: + self.scheduler: sched.scheduler = sched.scheduler() + self.running: bool = False + self.thread: Optional[Thread] = None + self.prev_idle: int = 0 + self.prev_total: int = 0 + self.stat_file: Path = Path("/proc/stat") + self.statusbar: "StatusBar" = statusbar + + def start(self) -> None: + self.running = True + self.thread = Thread(target=self._start, daemon=True) + self.thread.start() + + def _start(self): + self.scheduler.enter(0, 0, self.run) + self.scheduler.run() + + def run(self) -> None: + lines = self.stat_file.read_text().splitlines()[0] + values = [int(x) for x in lines.split()[1:]] + idle = sum(values[3:5]) + non_idle = sum(values[:3] + values[5:8]) + total = idle + non_idle + total_diff = total - self.prev_total + idle_diff = idle - self.prev_idle + cpu_percent = (total_diff - idle_diff) / total_diff + self.statusbar.after(0, self.statusbar.set_cpu, cpu_percent) + self.prev_idle = idle + self.prev_total = total + if self.running: + self.scheduler.enter(3, 0, self.run) + + class StatusBar(ttk.Frame): def __init__(self, master: tk.Widget, app: "Application") -> None: super().__init__(master) @@ -20,12 +58,14 @@ class StatusBar(ttk.Frame): self.status: Optional[ttk.Label] = None self.statusvar: tk.StringVar = tk.StringVar() self.zoom: Optional[ttk.Label] = None - self.cpu_usage: Optional[ttk.Label] = None + self.cpu_label: Optional[ttk.Label] = None self.alerts_button: Optional[ttk.Button] = None self.alert_style = Styles.no_alert self.running: bool = False self.core_alarms: List[ExceptionEvent] = [] self.draw() + self.cpu_usage: CpuUsage = CpuUsage(self) + self.cpu_usage.start() def draw(self) -> None: self.columnconfigure(0, weight=7) @@ -46,25 +86,27 @@ class StatusBar(ttk.Frame): ) self.status.grid(row=0, column=0, sticky="ew") - self.zoom = ttk.Label( - self, - text="%s" % (int(self.app.canvas.ratio * 100)) + "%", - anchor=tk.CENTER, - borderwidth=1, - relief=tk.RIDGE, - ) + self.zoom = ttk.Label(self, anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE) self.zoom.grid(row=0, column=1, sticky="ew") + self.set_zoom(self.app.canvas.ratio) - self.cpu_usage = ttk.Label( - self, text="CPU TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE + self.cpu_label = ttk.Label( + self, anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE ) - self.cpu_usage.grid(row=0, column=2, sticky="ew") + self.cpu_label.grid(row=0, column=2, sticky="ew") + self.set_cpu(0.0) self.alerts_button = ttk.Button( self, text="Alerts", command=self.click_alerts, style=self.alert_style ) self.alerts_button.grid(row=0, column=3, sticky="ew") + def set_cpu(self, usage: float) -> None: + self.cpu_label.config(text=f"CPU {usage * 100:.2f}%") + + def set_zoom(self, zoom: float) -> None: + self.zoom.config(text=f"ZOOM {zoom * 100:.0f}%") + def add_alert(self, event: ExceptionEvent) -> None: self.core_alarms.append(event) level = event.exception_event.level From fff4bd796358797e3b88babc9f005055d035d949 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 23 Jul 2020 09:41:39 -0700 Subject: [PATCH 0542/1131] moved cpu usage to a grpc call that the gui will listen to, fixed grpc stream typing to be grpc.Future, fixed pygui issue for start callback when a start fails, but there are no exceptions --- daemon/core/api/grpc/client.py | 19 +++++++++++-- daemon/core/api/grpc/grpcutils.py | 20 ++++++++++++++ daemon/core/api/grpc/server.py | 9 ++++++ daemon/core/gui/coreclient.py | 27 ++++++++++++++++-- daemon/core/gui/statusbar.py | 40 --------------------------- daemon/core/gui/toolbar.py | 7 +++-- daemon/proto/core/api/grpc/core.proto | 10 +++++++ 7 files changed, 85 insertions(+), 47 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index aacfa4f6..0674a0eb 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -467,7 +467,7 @@ class CoreGrpcClient: session_id: int, handler: Callable[[core_pb2.Event], None], events: List[core_pb2.Event] = None, - ) -> grpc.Channel: + ) -> grpc.Future: """ Listen for session events. @@ -484,7 +484,7 @@ class CoreGrpcClient: def throughputs( self, session_id: int, handler: Callable[[core_pb2.ThroughputsEvent], None] - ) -> grpc.Channel: + ) -> grpc.Future: """ Listen for throughput events with information for interfaces and bridges. @@ -498,6 +498,21 @@ class CoreGrpcClient: start_streamer(stream, handler) return stream + def cpu_usage( + self, delay: int, handler: Callable[[core_pb2.CpuUsageEvent], None] + ) -> grpc.Future: + """ + Listen for cpu usage events with the given repeat delay. + + :param delay: delay between receiving events + :param handler: handler for every event + :return: stream processing events, can be used to cancel stream + """ + request = core_pb2.CpuUsageRequest(delay=delay) + stream = self.stub.CpuUsage(request) + start_streamer(stream, handler) + return stream + def add_node( self, session_id: int, node: core_pb2.Node, source: str = None ) -> core_pb2.AddNodeResponse: diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index bd3519f7..84b8ee6a 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -1,5 +1,6 @@ import logging import time +from pathlib import Path from typing import Any, Dict, List, Tuple, Type, Union import grpc @@ -20,6 +21,25 @@ from core.services.coreservices import CoreService WORKERS = 10 +class CpuUsage: + def __init__(self) -> None: + self.stat_file: Path = Path("/proc/stat") + self.prev_idle: int = 0 + self.prev_total: int = 0 + + def run(self) -> float: + lines = self.stat_file.read_text().splitlines()[0] + values = [int(x) for x in lines.split()[1:]] + idle = sum(values[3:5]) + non_idle = sum(values[:3] + values[5:8]) + total = idle + non_idle + total_diff = total - self.prev_total + idle_diff = idle - self.prev_idle + self.prev_idle = idle + self.prev_total = total + return (total_diff - idle_diff) / total_diff + + def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOptions]: """ Convert node protobuf message to data for creating a node. diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 4c204845..38100e05 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -681,6 +681,15 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): last_stats = stats time.sleep(delay) + def CpuUsage( + self, request: core_pb2.CpuUsageRequest, context: ServicerContext + ) -> None: + cpu_usage = grpcutils.CpuUsage() + while self._is_running(context): + usage = cpu_usage.run() + yield core_pb2.CpuUsageEvent(usage=usage) + time.sleep(request.delay) + def AddNode( self, request: core_pb2.AddNodeRequest, context: ServicerContext ) -> core_pb2.AddNodeResponse: diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 8474b3cb..fc0bd520 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -16,6 +16,7 @@ from core.api.grpc import client from core.api.grpc.common_pb2 import ConfigOption from core.api.grpc.configservices_pb2 import ConfigService, ConfigServiceConfig from core.api.grpc.core_pb2 import ( + CpuUsageEvent, Event, ExceptionEvent, Hook, @@ -55,6 +56,7 @@ if TYPE_CHECKING: from core.gui.app import Application GUI_SOURCE = "gui" +CPU_USAGE_DELAY = 3 class CoreClient: @@ -92,8 +94,9 @@ class CoreClient: self.hooks: Dict[str, Hook] = {} self.emane_config: Dict[str, ConfigOption] = {} self.mobility_players: Dict[int, MobilityPlayer] = {} - self.handling_throughputs: Optional[grpc.Channel] = None - self.handling_events: Optional[grpc.Channel] = None + self.handling_throughputs: Optional[grpc.Future] = None + self.handling_cpu_usage: Optional[grpc.Future] = None + self.handling_events: Optional[grpc.Future] = None self.xml_dir: Optional[str] = None self.xml_file: Optional[str] = None @@ -111,6 +114,7 @@ class CoreClient: ) if throughputs_enabled: self.enable_throughputs() + self.setup_cpu_usage() return self._client def reset(self) -> None: @@ -258,6 +262,20 @@ class CoreClient: self.handling_events.cancel() self.handling_events = None + def cancel_cpu_usage(self) -> None: + if self.handling_cpu_usage: + self.handling_cpu_usage.cancel() + self.handling_cpu_usage = None + + def setup_cpu_usage(self) -> None: + if self.handling_cpu_usage and self.handling_cpu_usage.running(): + return + if self.handling_cpu_usage: + self.handling_cpu_usage.cancel() + self.handling_cpu_usage = self._client.cpu_usage( + CPU_USAGE_DELAY, self.handle_cpu_event + ) + def handle_throughputs(self, event: ThroughputsEvent) -> None: if event.session_id != self.session_id: logging.warning( @@ -269,6 +287,9 @@ class CoreClient: logging.debug("handling throughputs event: %s", event) self.app.after(0, self.app.canvas.set_throughputs, event) + def handle_cpu_event(self, event: CpuUsageEvent) -> None: + self.app.after(0, self.app.statusbar.set_cpu, event.usage) + def handle_exception_event(self, event: ExceptionEvent) -> None: logging.info("exception event: %s", event) self.app.statusbar.add_alert(event) @@ -479,6 +500,8 @@ class CoreClient: """ try: self.client.connect() + self.setup_cpu_usage() + # get service information response = self.client.get_services() for service in response.services: diff --git a/daemon/core/gui/statusbar.py b/daemon/core/gui/statusbar.py index e9fc03b2..6989593e 100644 --- a/daemon/core/gui/statusbar.py +++ b/daemon/core/gui/statusbar.py @@ -1,10 +1,7 @@ """ status bar """ -import sched import tkinter as tk -from pathlib import Path -from threading import Thread from tkinter import ttk from typing import TYPE_CHECKING, List, Optional @@ -16,41 +13,6 @@ if TYPE_CHECKING: from core.gui.app import Application -class CpuUsage: - def __init__(self, statusbar: "StatusBar") -> None: - self.scheduler: sched.scheduler = sched.scheduler() - self.running: bool = False - self.thread: Optional[Thread] = None - self.prev_idle: int = 0 - self.prev_total: int = 0 - self.stat_file: Path = Path("/proc/stat") - self.statusbar: "StatusBar" = statusbar - - def start(self) -> None: - self.running = True - self.thread = Thread(target=self._start, daemon=True) - self.thread.start() - - def _start(self): - self.scheduler.enter(0, 0, self.run) - self.scheduler.run() - - def run(self) -> None: - lines = self.stat_file.read_text().splitlines()[0] - values = [int(x) for x in lines.split()[1:]] - idle = sum(values[3:5]) - non_idle = sum(values[:3] + values[5:8]) - total = idle + non_idle - total_diff = total - self.prev_total - idle_diff = idle - self.prev_idle - cpu_percent = (total_diff - idle_diff) / total_diff - self.statusbar.after(0, self.statusbar.set_cpu, cpu_percent) - self.prev_idle = idle - self.prev_total = total - if self.running: - self.scheduler.enter(3, 0, self.run) - - class StatusBar(ttk.Frame): def __init__(self, master: tk.Widget, app: "Application") -> None: super().__init__(master) @@ -64,8 +26,6 @@ class StatusBar(ttk.Frame): self.running: bool = False self.core_alarms: List[ExceptionEvent] = [] self.draw() - self.cpu_usage: CpuUsage = CpuUsage(self) - self.cpu_usage.start() def draw(self) -> None: self.columnconfigure(0, weight=7) diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 406a88ca..968b447d 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -305,10 +305,11 @@ class Toolbar(ttk.Frame): self.set_runtime() self.app.core.set_metadata() self.app.core.show_mobility_players() - elif response.exceptions: + else: enable_buttons(self.design_frame, enabled=True) - message = "\n".join(response.exceptions) - self.app.show_error("Start Session Error", message) + if response.exceptions: + message = "\n".join(response.exceptions) + self.app.show_error("Start Session Error", message) def set_runtime(self) -> None: enable_buttons(self.runtime_frame, enabled=True) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index eb889d14..9214ad1b 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -51,6 +51,8 @@ service CoreApi { } rpc Throughputs (ThroughputsRequest) returns (stream ThroughputsEvent) { } + rpc CpuUsage (CpuUsageRequest) returns (stream CpuUsageEvent) { + } // node rpc rpc AddNode (AddNodeRequest) returns (AddNodeResponse) { @@ -347,6 +349,14 @@ message ThroughputsEvent { repeated InterfaceThroughput iface_throughputs = 3; } +message CpuUsageRequest { + int32 delay = 1; +} + +message CpuUsageEvent { + double usage = 1; +} + message InterfaceThroughput { int32 node_id = 1; int32 iface_id = 2; From ba3a2474957394b196b47e79432daf6fc5e77cf1 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 23 Jul 2020 21:21:43 -0700 Subject: [PATCH 0543/1131] updated changelog for 7.0.0 --- CHANGELOG.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96f7b30a..375a7607 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,53 @@ +## 2020-07-23 CORE 7.0.0 + +* Breaking Changes + * core.emudata and core.data combined and cleaned up into core.data + * updates to consistently use mac instead of hwaddr/mac + * \#468 - code related to adding/editing/deleting links cleaned up + * \#469 - usages of per all changed to loss to be consistent + * \#470 - variables with numbered names now use numbers directly + * \#471 - node startup is no longer embedded within its constructor + * \#472 - code updated to refer to interfaces consistently as iface + * \#475 - code updates changing how ip addresses are stored on interfaces + * \#476 - executables to check for moved into own module core.executables + * \#486 - core will now install into its own python virtual environment managed by poetry +* core-daemon + * updates to properly save/load distributed servers to xml + * \#474 - added type hinting to all service files + * \#478 - fixed typo in config service directory + * \#479 - opening an xml file will now cycle through states like a normal session + * \#480 - ovs configuration will now save/load from xml and display in guis + * \#484 - changes to support adding emane links during runtime +* core-pygui + * fixed issue not displaying services for the default group in service dialogs + * fixed issue starting a session when the daemon is not present + * fixed issue attempting to open terminals for invalid nodes + * fixed issue syncing session location + * fixed issue joining a session with mobility, not in runtime + * added cpu usage monitor to status bar + * emane configurations can now be seen during runtime + * rj45 nodes can only have one link + * disabling throughputs will clear labels + * improvements to custom service copy + * link options will now be drawn on as a label + * updates to handle runtime link events + * \#477 - added optional details pane for a quick view of node/link details + * \#485 - pygui fixed observer widget for invalid nodes + * \#496 - improved alert handling +* core-gui + * \#493 - increased frame size to show all emane configuration options +* gRPC API + * added set session user rpc + * added cpu usage stream + * interface objects returned from get_node will now provide node_id, net_id, and net2_id data + * peer to peer nodes will not be included in get_session calls + * pathloss events will now throw an error when nem id not found + * \#481 - link rpc calls will broadcast out + * \#496 - added alert rpc call +* Services + * fixed issue reading files in security services + * \#494 - add staticd to daemons list for frr services + ## 2020-06-11 CORE 6.5.0 * Breaking Changes * CoreNode.newnetif - both parameters are required and now takes an InterfaceData object as its second parameter From 154fa8b77d873ff892937eb12994ce237e2f00c8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 24 Jul 2020 22:00:38 -0700 Subject: [PATCH 0544/1131] pygui: replaced hook with wrapped hook class, fixed hook dialog edit --- daemon/core/gui/coreclient.py | 11 ++- daemon/core/gui/dialogs/hooks.py | 39 ++++++---- daemon/core/gui/wrappers.py | 126 +++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 17 deletions(-) create mode 100644 daemon/core/gui/wrappers.py diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index fc0bd520..97399556 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -19,7 +19,6 @@ from core.api.grpc.core_pb2 import ( CpuUsageEvent, Event, ExceptionEvent, - Hook, Interface, Link, LinkEvent, @@ -51,6 +50,7 @@ from core.gui.graph.shape import AnnotationData, Shape from core.gui.graph.shapeutils import ShapeType from core.gui.interface import InterfaceManager from core.gui.nodeutils import NodeDraw, NodeUtils +from core.gui.wrappers import Hook if TYPE_CHECKING: from core.gui.app import Application @@ -332,7 +332,8 @@ class CoreClient: # get hooks response = self.client.get_hooks(self.session_id) - for hook in response.hooks: + for hook_proto in response.hooks: + hook = Hook.from_proto(hook_proto) self.hooks[hook.file] = hook # get emane config @@ -570,7 +571,7 @@ class CoreClient: wlan_configs = self.get_wlan_configs_proto() mobility_configs = self.get_mobility_configs_proto() emane_model_configs = self.get_emane_model_configs_proto() - hooks = list(self.hooks.values()) + hooks = [x.to_proto() for x in self.hooks.values()] service_configs = self.get_service_configs_proto() file_configs = self.get_service_file_configs_proto() asymmetric_links = [ @@ -823,7 +824,9 @@ class CoreClient: config_proto.data, ) for hook in self.hooks.values(): - self.client.add_hook(self.session_id, hook.state, hook.file, hook.data) + self.client.add_hook( + self.session_id, hook.state.value, hook.file, hook.data + ) for config_proto in self.get_emane_model_configs_proto(): self.client.set_emane_model_config( self.session_id, diff --git a/daemon/core/gui/dialogs/hooks.py b/daemon/core/gui/dialogs/hooks.py index 08d666ba..b004dae2 100644 --- a/daemon/core/gui/dialogs/hooks.py +++ b/daemon/core/gui/dialogs/hooks.py @@ -2,10 +2,10 @@ import tkinter as tk from tkinter import ttk from typing import TYPE_CHECKING, Optional -from core.api.grpc import core_pb2 from core.gui.dialogs.dialog import Dialog from core.gui.themes import PADX, PADY from core.gui.widgets import CodeText, ListboxScroll +from core.gui.wrappers import Hook, SessionState if TYPE_CHECKING: from core.gui.app import Application @@ -16,8 +16,9 @@ class HookDialog(Dialog): super().__init__(app, "Hook", master=master) self.name: tk.StringVar = tk.StringVar() self.codetext: Optional[CodeText] = None - self.hook: core_pb2.Hook = core_pb2.Hook() + self.hook: Optional[Hook] = None self.state: tk.StringVar = tk.StringVar() + self.editing: bool = False self.draw() def draw(self) -> None: @@ -34,8 +35,8 @@ class HookDialog(Dialog): label.grid(row=0, column=0, sticky="ew", padx=PADX) entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=1, sticky="ew", padx=PADX) - values = tuple(x for x in core_pb2.SessionState.Enum.keys() if x != "NONE") - initial_state = core_pb2.SessionState.Enum.Name(core_pb2.SessionState.RUNTIME) + values = tuple(x.name for x in SessionState) + initial_state = SessionState.RUNTIME.name self.state.set(initial_state) self.name.set(f"{initial_state.lower()}_hook.sh") combobox = ttk.Combobox( @@ -67,23 +68,30 @@ class HookDialog(Dialog): button.grid(row=0, column=1, sticky="ew") def state_change(self, event: tk.Event) -> None: + if self.editing: + return state_name = self.state.get() self.name.set(f"{state_name.lower()}_hook.sh") - def set(self, hook: core_pb2.Hook) -> None: + def set(self, hook: Hook) -> None: + self.editing = True self.hook = hook self.name.set(hook.file) self.codetext.text.delete(1.0, tk.END) self.codetext.text.insert(tk.END, hook.data) - state_name = core_pb2.SessionState.Enum.Name(hook.state) + state_name = hook.state.name self.state.set(state_name) def save(self) -> None: data = self.codetext.text.get("1.0", tk.END).strip() - state_value = core_pb2.SessionState.Enum.Value(self.state.get()) - self.hook.file = self.name.get() - self.hook.data = data - self.hook.state = state_value + state = SessionState[self.state.get()] + file_name = self.name.get() + if self.editing: + self.hook.state = state + self.hook.file = file_name + self.hook.data = data + else: + self.hook = Hook(state=state, file=file_name, data=data) self.destroy() @@ -94,6 +102,7 @@ class HooksDialog(Dialog): self.edit_button: Optional[ttk.Button] = None self.delete_button: Optional[ttk.Button] = None self.selected: Optional[str] = None + self.selected_index: Optional[int] = None self.draw() def draw(self) -> None: @@ -133,10 +142,13 @@ class HooksDialog(Dialog): self.listbox.insert(tk.END, hook.file) def click_edit(self) -> None: - hook = self.app.core.hooks[self.selected] + hook = self.app.core.hooks.pop(self.selected) dialog = HookDialog(self, self.app) dialog.set(hook) dialog.show() + self.app.core.hooks[hook.file] = hook + self.listbox.delete(self.selected_index) + self.listbox.insert(self.selected_index, hook.file) def click_delete(self) -> None: del self.app.core.hooks[self.selected] @@ -146,11 +158,12 @@ class HooksDialog(Dialog): def select(self, event: tk.Event) -> None: if self.listbox.curselection(): - index = self.listbox.curselection()[0] - self.selected = self.listbox.get(index) + self.selected_index = self.listbox.curselection()[0] + self.selected = self.listbox.get(self.selected_index) self.edit_button.config(state=tk.NORMAL) self.delete_button.config(state=tk.NORMAL) else: self.selected = None + self.selected_index = None self.edit_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED) diff --git a/daemon/core/gui/wrappers.py b/daemon/core/gui/wrappers.py new file mode 100644 index 00000000..217ab321 --- /dev/null +++ b/daemon/core/gui/wrappers.py @@ -0,0 +1,126 @@ +from dataclasses import dataclass +from enum import Enum +from typing import List + +from core.api.grpc import core_pb2 + + +class SessionState(Enum): + DEFINITION = 1 + CONFIGURATION = 2 + INSTANTIATION = 3 + RUNTIME = 4 + DATACOLLECT = 5 + SHUTDOWN = 6 + + +class NodeType(Enum): + DEFAULT = 0 + PHYSICAL = 1 + SWITCH = 4 + HUB = 5 + WIRELESS_LAN = 6 + RJ45 = 7 + TUNNEL = 8 + EMANE = 10 + TAP_BRIDGE = 11 + PEER_TO_PEER = 12 + CONTROL_NET = 13 + DOCKER = 15 + LXC = 16 + + +@dataclass +class Hook: + state: SessionState + file: str + data: str + + @classmethod + def from_proto(cls, hook: core_pb2.Hook) -> "Hook": + return Hook(state=SessionState(hook.state), file=hook.file, data=hook.data) + + def to_proto(self) -> core_pb2.Hook: + return core_pb2.Hook(state=self.state.value, file=self.file, data=self.data) + + +@dataclass +class Position: + x: float + y: float + + @classmethod + def from_proto(cls, position: core_pb2.Position) -> "Position": + return Position(x=position.x, y=position.y) + + def to_proto(self) -> core_pb2.Position: + return core_pb2.Position(x=self.x, y=self.y) + + +@dataclass +class Geo: + lat: float = None + lon: float = None + alt: float = None + + @classmethod + def from_proto(cls, geo: core_pb2.Geo) -> "Geo": + return Geo(lat=geo.lat, lon=geo.lon, alt=geo.alt) + + def to_proto(self) -> core_pb2.Geo: + return core_pb2.Geo(lat=self.lat, lon=self.lon, alt=self.alt) + + +@dataclass +class Node: + id: int + name: str + type: NodeType + model: str = None + position: Position = None + services: List[str] = None + config_services: List[str] = None + emane: str = None + icon: str = None + image: str = None + server: str = None + geo: Geo = None + dir: str = None + channel: str = None + + @classmethod + def from_proto(cls, node: core_pb2.Node) -> "Node": + return Node( + id=node.id, + name=node.name, + type=NodeType(node.type), + model=node.model, + position=Position.from_proto(node.position), + services=list(node.services), + config_services=list(node.config_services), + emane=node.emane, + icon=node.icon, + image=node.image, + server=node.server, + geo=Geo.from_proto(node.geo), + dir=node.dir, + channel=node.channel, + ) + + def to_proto(self) -> core_pb2.Node: + return core_pb2.Node( + id=self.id, + name=self.name, + type=self.type.value, + model=self.model, + position=self.position.to_proto(), + services=self.services, + config_services=self.config_services, + emane=self.emane, + icon=self.icon, + image=self.image, + server=self.server, + geo=self.geo.to_proto(), + dir=self.dir, + channel=self.channel, + ) From 77f6577bce08437f3dae737143ff8482e0501e06 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 25 Jul 2020 10:30:14 -0700 Subject: [PATCH 0545/1131] pygui: added wrappers for most usages of protobufs within pygui --- daemon/core/gui/coreclient.py | 126 ++++--- daemon/core/gui/dialogs/alerts.py | 23 +- .../core/gui/dialogs/configserviceconfig.py | 4 +- daemon/core/gui/dialogs/emaneconfig.py | 3 +- daemon/core/gui/dialogs/linkconfig.py | 29 +- daemon/core/gui/dialogs/mobilityconfig.py | 3 +- daemon/core/gui/dialogs/mobilityplayer.py | 3 +- daemon/core/gui/dialogs/nodeconfig.py | 2 +- daemon/core/gui/dialogs/sessionoptions.py | 4 +- daemon/core/gui/dialogs/sessions.py | 7 +- daemon/core/gui/dialogs/wlanconfig.py | 3 +- daemon/core/gui/frames/link.py | 4 +- daemon/core/gui/frames/node.py | 2 +- daemon/core/gui/graph/edges.py | 11 +- daemon/core/gui/graph/graph.py | 35 +- daemon/core/gui/graph/node.py | 3 +- daemon/core/gui/images.py | 2 +- daemon/core/gui/interface.py | 14 +- daemon/core/gui/nodeutils.py | 4 +- daemon/core/gui/statusbar.py | 4 +- daemon/core/gui/toolbar.py | 11 +- daemon/core/gui/widgets.py | 14 +- daemon/core/gui/wrappers.py | 337 +++++++++++++++++- 23 files changed, 475 insertions(+), 173 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 97399556..c41caeca 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -13,27 +13,8 @@ from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple import grpc from core.api.grpc import client -from core.api.grpc.common_pb2 import ConfigOption from core.api.grpc.configservices_pb2 import ConfigService, ConfigServiceConfig -from core.api.grpc.core_pb2 import ( - CpuUsageEvent, - Event, - ExceptionEvent, - Interface, - Link, - LinkEvent, - LinkType, - MessageType, - Node, - NodeEvent, - NodeType, - Position, - SessionLocation, - SessionState, - StartSessionResponse, - StopSessionResponse, - ThroughputsEvent, -) +from core.api.grpc.core_pb2 import CpuUsageEvent, Event, ThroughputsEvent from core.api.grpc.emane_pb2 import EmaneModelConfig from core.api.grpc.mobility_pb2 import MobilityConfig from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig, ServiceFileConfig @@ -50,7 +31,22 @@ from core.gui.graph.shape import AnnotationData, Shape from core.gui.graph.shapeutils import ShapeType from core.gui.interface import InterfaceManager from core.gui.nodeutils import NodeDraw, NodeUtils -from core.gui.wrappers import Hook +from core.gui.wrappers import ( + ConfigOption, + ExceptionEvent, + Hook, + Interface, + Link, + LinkEvent, + LinkType, + MessageType, + Node, + NodeEvent, + NodeType, + Position, + SessionLocation, + SessionState, +) if TYPE_CHECKING: from core.gui.app import Application @@ -165,12 +161,13 @@ class CoreClient: return if event.HasField("link_event"): - self.app.after(0, self.handle_link_event, event.link_event) + link_event = LinkEvent.from_proto(event.link_event) + self.app.after(0, self.handle_link_event, link_event) elif event.HasField("session_event"): logging.info("session event: %s", event) session_event = event.session_event - if session_event.event <= SessionState.SHUTDOWN: - self.state = event.session_event.event + if session_event.event <= SessionState.SHUTDOWN.value: + self.state = SessionState(session_event.event) elif session_event.event in {7, 8, 9}: node_id = session_event.node_id dialog = self.mobility_players.get(node_id) @@ -184,10 +181,12 @@ class CoreClient: else: logging.warning("unknown session event: %s", session_event) elif event.HasField("node_event"): - self.app.after(0, self.handle_node_event, event.node_event) + node_event = NodeEvent.from_proto(event.node_event) + self.app.after(0, self.handle_node_event, node_event) elif event.HasField("config_event"): logging.info("config event: %s", event) elif event.HasField("exception_event"): + event = ExceptionEvent.from_proto(event.session_id, event.exception_event) self.handle_exception_event(event) else: logging.info("unhandled event: %s", event) @@ -307,7 +306,7 @@ class CoreClient: try: response = self.client.get_session(self.session_id) session = response.session - self.state = session.state + self.state = SessionState(session.state) self.handling_events = self.client.events( self.session_id, self.handle_events ) @@ -324,7 +323,7 @@ class CoreClient: # get location if query_location: response = self.client.get_session_location(self.session_id) - self.location = response.location + self.location = SessionLocation.from_proto(response.location) # get emane models response = self.client.get_emane_models(self.session_id) @@ -338,20 +337,22 @@ class CoreClient: # get emane config response = self.client.get_emane_config(self.session_id) - self.emane_config = response.config + self.emane_config = ConfigOption.from_dict(response.config) # update interface manager self.ifaces_manager.joined(session.links) # draw session - self.app.canvas.reset_and_redraw(session) + nodes = [Node.from_proto(x) for x in session.nodes] + links = [Link.from_proto(x) for x in session.links] + self.app.canvas.reset_and_redraw(nodes, links) # get mobility configs response = self.client.get_mobility_configs(self.session_id) for node_id in response.configs: config = response.configs[node_id].config canvas_node = self.canvas_nodes[node_id] - canvas_node.mobility_config = dict(config) + canvas_node.mobility_config = ConfigOption.from_dict(config) # get emane model config response = self.client.get_emane_model_configs(self.session_id) @@ -360,16 +361,16 @@ class CoreClient: if config.iface_id != -1: iface_id = config.iface_id canvas_node = self.canvas_nodes[config.node_id] - canvas_node.emane_model_configs[(config.model, iface_id)] = dict( - config.config - ) + canvas_node.emane_model_configs[ + (config.model, iface_id) + ] = ConfigOption.from_dict(config.config) # get wlan configurations response = self.client.get_wlan_configs(self.session_id) for _id in response.configs: mapped_config = response.configs[_id] canvas_node = self.canvas_nodes[_id] - canvas_node.wlan_config = dict(mapped_config.config) + canvas_node.wlan_config = ConfigOption.from_dict(mapped_config.config) # get service configurations response = self.client.get_node_service_configs(self.session_id) @@ -501,7 +502,6 @@ class CoreClient: """ try: self.client.connect() - self.setup_cpu_usage() # get service information response = self.client.get_services() @@ -546,8 +546,9 @@ class CoreClient: def edit_node(self, core_node: Node) -> None: try: + position = core_node.position.to_proto() self.client.edit_node( - self.session_id, core_node.id, core_node.position, source=GUI_SOURCE + self.session_id, core_node.id, position, source=GUI_SOURCE ) except grpc.RpcError as e: self.app.show_grpc_exception("Edit Node Error", e) @@ -556,18 +557,17 @@ class CoreClient: for server in self.servers.values(): self.client.add_session_server(self.session_id, server.name, server.address) - def start_session(self) -> StartSessionResponse: + def start_session(self) -> Tuple[bool, List[str]]: self.ifaces_manager.reset_mac() - nodes = [x.core_node for x in self.canvas_nodes.values()] + nodes = [x.core_node.to_proto() for x in self.canvas_nodes.values()] links = [] for edge in self.links.values(): - link = Link() - link.CopyFrom(edge.link) - if link.HasField("iface1") and not link.iface1.mac: + link = edge.link + if link.iface1 and not link.iface1.mac: link.iface1.mac = self.ifaces_manager.next_mac() - if link.HasField("iface2") and not link.iface2.mac: + if link.iface2 and not link.iface2.mac: link.iface2.mac = self.ifaces_manager.next_mac() - links.append(link) + links.append(link.to_proto()) wlan_configs = self.get_wlan_configs_proto() mobility_configs = self.get_mobility_configs_proto() emane_model_configs = self.get_emane_model_configs_proto() @@ -582,14 +582,15 @@ class CoreClient: emane_config = {x: self.emane_config[x].value for x in self.emane_config} else: emane_config = None - response = StartSessionResponse(result=False) + result = False + exceptions = [] try: self.send_servers() response = self.client.start_session( self.session_id, nodes, links, - self.location, + self.location.to_proto(), hooks, emane_config, emane_model_configs, @@ -605,20 +606,23 @@ class CoreClient: ) if response.result: self.set_metadata() + result = response.result + exceptions = response.exceptions except grpc.RpcError as e: self.app.show_grpc_exception("Start Session Error", e) - return response + return result, exceptions - def stop_session(self, session_id: int = None) -> StopSessionResponse: + def stop_session(self, session_id: int = None) -> bool: if not session_id: session_id = self.session_id - response = StopSessionResponse(result=False) + result = False try: response = self.client.stop_session(session_id) logging.info("stopped session(%s), result: %s", session_id, response) + result = response.result except grpc.RpcError as e: self.app.show_grpc_exception("Stop Session Error", e) - return response + return result def show_mobility_players(self) -> None: for canvas_node in self.canvas_nodes.values(): @@ -920,7 +924,7 @@ class CoreClient: ) return node - def deleted_graph_nodes(self, canvas_nodes: List[Node]) -> None: + def deleted_graph_nodes(self, canvas_nodes: List[CanvasNode]) -> None: """ remove the nodes selected by the user and anything related to that node such as link, configurations, interfaces @@ -951,13 +955,7 @@ class CoreClient: ip6=ip6, ip6_mask=ip6_mask, ) - logging.info( - "create node(%s) interface(%s) IPv4(%s) IPv6(%s)", - node.name, - iface.name, - iface.ip4, - iface.ip6, - ) + logging.info("create node(%s) interface(%s)", node.name, iface) return iface def create_link( @@ -1010,8 +1008,7 @@ class CoreClient: continue if not canvas_node.wlan_config: continue - config = canvas_node.wlan_config - config = {x: config[x].value for x in config} + config = ConfigOption.to_dict(canvas_node.wlan_config) node_id = canvas_node.core_node.id wlan_config = WlanConfig(node_id=node_id, config=config) configs.append(wlan_config) @@ -1024,8 +1021,7 @@ class CoreClient: continue if not canvas_node.mobility_config: continue - config = canvas_node.mobility_config - config = {x: config[x].value for x in config} + config = ConfigOption.to_dict(canvas_node.mobility_config) node_id = canvas_node.core_node.id mobility_config = MobilityConfig(node_id=node_id, config=config) configs.append(mobility_config) @@ -1039,7 +1035,7 @@ class CoreClient: node_id = canvas_node.core_node.id for key, config in canvas_node.emane_model_configs.items(): model, iface_id = key - config = {x: config[x].value for x in config} + config = ConfigOption.to_dict(config) if iface_id is None: iface_id = -1 config_proto = EmaneModelConfig( @@ -1116,7 +1112,7 @@ class CoreClient: node_id, config, ) - return dict(config) + return ConfigOption.from_dict(config) def get_mobility_config(self, node_id: int) -> Dict[str, ConfigOption]: response = self.client.get_mobility_config(self.session_id, node_id) @@ -1126,7 +1122,7 @@ class CoreClient: node_id, config, ) - return dict(config) + return ConfigOption.from_dict(config) def get_emane_model_config( self, node_id: int, model: str, iface_id: int = None @@ -1145,7 +1141,7 @@ class CoreClient: iface_id, config, ) - return dict(config) + return ConfigOption.from_dict(config) def execute_script(self, script) -> None: response = self.client.execute_script(script) diff --git a/daemon/core/gui/dialogs/alerts.py b/daemon/core/gui/dialogs/alerts.py index 8e0aa02e..fd6d342e 100644 --- a/daemon/core/gui/dialogs/alerts.py +++ b/daemon/core/gui/dialogs/alerts.py @@ -5,10 +5,10 @@ import tkinter as tk from tkinter import ttk from typing import TYPE_CHECKING, Dict, Optional -from core.api.grpc.core_pb2 import ExceptionEvent, ExceptionLevel from core.gui.dialogs.dialog import Dialog from core.gui.themes import PADX, PADY from core.gui.widgets import CodeText +from core.gui.wrappers import ExceptionEvent, ExceptionLevel if TYPE_CHECKING: from core.gui.app import Application @@ -49,9 +49,8 @@ class AlertsDialog(Dialog): self.tree.heading("source", text="Source") self.tree.bind("<>", self.click_select) - for alarm in self.app.statusbar.core_alarms: - exception = alarm.exception_event - level_name = ExceptionLevel.Enum.Name(exception.level) + for exception in self.app.statusbar.core_alarms: + level_name = exception.level.name node_id = exception.node_id if exception.node_id else "" insert_id = self.tree.insert( "", @@ -60,21 +59,21 @@ class AlertsDialog(Dialog): values=( exception.date, level_name, - alarm.session_id, + exception.session_id, node_id, exception.source, ), tags=(level_name,), ) - self.alarm_map[insert_id] = alarm + self.alarm_map[insert_id] = exception - error_name = ExceptionLevel.Enum.Name(ExceptionLevel.ERROR) + error_name = ExceptionLevel.ERROR.name self.tree.tag_configure(error_name, background="#ff6666") - fatal_name = ExceptionLevel.Enum.Name(ExceptionLevel.FATAL) + fatal_name = ExceptionLevel.FATAL.name self.tree.tag_configure(fatal_name, background="#d9d9d9") - warning_name = ExceptionLevel.Enum.Name(ExceptionLevel.WARNING) + warning_name = ExceptionLevel.WARNING.name self.tree.tag_configure(warning_name, background="#ffff99") - notice_name = ExceptionLevel.Enum.Name(ExceptionLevel.NOTICE) + notice_name = ExceptionLevel.NOTICE.name self.tree.tag_configure(notice_name, background="#85e085") yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) @@ -108,8 +107,8 @@ class AlertsDialog(Dialog): def click_select(self, event: tk.Event) -> None: current = self.tree.selection()[0] - alarm = self.alarm_map[current] + exception = self.alarm_map[current] self.codetext.text.config(state=tk.NORMAL) self.codetext.text.delete(1.0, tk.END) - self.codetext.text.insert(1.0, alarm.exception_event.text) + self.codetext.text.insert(1.0, exception.text) self.codetext.text.config(state=tk.DISABLED) diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py index c2d42ee4..5a6a89a8 100644 --- a/daemon/core/gui/dialogs/configserviceconfig.py +++ b/daemon/core/gui/dialogs/configserviceconfig.py @@ -8,11 +8,11 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Set import grpc -from core.api.grpc.common_pb2 import ConfigOption from core.api.grpc.services_pb2 import ServiceValidationMode from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll +from core.gui.wrappers import ConfigOption if TYPE_CHECKING: from core.gui.app import Application @@ -99,7 +99,7 @@ class ConfigServiceConfigDialog(Dialog): service_config = self.canvas_node.config_service_configs.get( self.service_name, {} ) - self.config = response.config + self.config = ConfigOption.from_dict(response.config) self.default_config = {x.name: x.value for x in self.config.values()} custom_config = service_config.get("config") if custom_config: diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index bb334757..d87e935a 100644 --- a/daemon/core/gui/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -8,12 +8,11 @@ from typing import TYPE_CHECKING, Dict, List, Optional import grpc -from core.api.grpc.common_pb2 import ConfigOption -from core.api.grpc.core_pb2 import Node from core.gui.dialogs.dialog import Dialog from core.gui.images import ImageEnum, Images from core.gui.themes import PADX, PADY from core.gui.widgets import ConfigFrame +from core.gui.wrappers import ConfigOption, Node if TYPE_CHECKING: from core.gui.app import Application diff --git a/daemon/core/gui/dialogs/linkconfig.py b/daemon/core/gui/dialogs/linkconfig.py index 28798ec1..87f43284 100644 --- a/daemon/core/gui/dialogs/linkconfig.py +++ b/daemon/core/gui/dialogs/linkconfig.py @@ -5,11 +5,11 @@ import tkinter as tk from tkinter import ttk from typing import TYPE_CHECKING, Optional -from core.api.grpc import core_pb2 from core.gui import validation from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.dialog import Dialog from core.gui.themes import PADX, PADY +from core.gui.wrappers import Interface, Link, LinkOptions if TYPE_CHECKING: from core.gui.app import Application @@ -21,7 +21,7 @@ def get_int(var: tk.StringVar) -> Optional[int]: if value != "": return int(value) else: - return None + return 0 def get_float(var: tk.StringVar) -> Optional[float]: @@ -29,14 +29,15 @@ def get_float(var: tk.StringVar) -> Optional[float]: if value != "": return float(value) else: - return None + return 0.0 class LinkConfigurationDialog(Dialog): def __init__(self, app: "Application", edge: "CanvasEdge") -> None: super().__init__(app, "Link Configuration") self.edge: "CanvasEdge" = edge - self.is_symmetric: bool = edge.link.options.unidirectional is False + + self.is_symmetric: bool = edge.link.is_symmetric() if self.is_symmetric: symmetry_var = tk.StringVar(value=">>") else: @@ -223,32 +224,32 @@ class LinkConfigurationDialog(Dialog): delay = get_int(self.delay) duplicate = get_int(self.duplicate) loss = get_float(self.loss) - options = core_pb2.LinkOptions( + options = LinkOptions( bandwidth=bandwidth, jitter=jitter, delay=delay, dup=duplicate, loss=loss ) - link.options.CopyFrom(options) + link.options = options iface1_id = None - if link.HasField("iface1"): + if link.iface1: iface1_id = link.iface1.id iface2_id = None - if link.HasField("iface2"): + if link.iface2: iface2_id = link.iface2.id if not self.is_symmetric: link.options.unidirectional = True asym_iface1 = None if iface1_id: - asym_iface1 = core_pb2.Interface(id=iface1_id) + asym_iface1 = Interface(id=iface1_id) asym_iface2 = None if iface2_id: - asym_iface2 = core_pb2.Interface(id=iface2_id) + asym_iface2 = Interface(id=iface2_id) down_bandwidth = get_int(self.down_bandwidth) down_jitter = get_int(self.down_jitter) down_delay = get_int(self.down_delay) down_duplicate = get_int(self.down_duplicate) down_loss = get_float(self.down_loss) - options = core_pb2.LinkOptions( + options = LinkOptions( bandwidth=down_bandwidth, jitter=down_jitter, delay=down_delay, @@ -256,7 +257,7 @@ class LinkConfigurationDialog(Dialog): loss=down_loss, unidirectional=True, ) - self.edge.asymmetric_link = core_pb2.Link( + self.edge.asymmetric_link = Link( node1_id=link.node2_id, node2_id=link.node1_id, iface1=asym_iface1, @@ -267,7 +268,7 @@ class LinkConfigurationDialog(Dialog): link.options.unidirectional = False self.edge.asymmetric_link = None - if self.app.core.is_runtime() and link.HasField("options"): + if self.app.core.is_runtime() and link.options: session_id = self.app.core.session_id self.app.core.client.edit_link( session_id, @@ -316,7 +317,7 @@ class LinkConfigurationDialog(Dialog): color = self.app.canvas.itemcget(self.edge.id, "fill") self.color.set(color) link = self.edge.link - if link.HasField("options"): + if link.options: self.bandwidth.set(str(link.options.bandwidth)) self.jitter.set(str(link.options.jitter)) self.duplicate.set(str(link.options.dup)) diff --git a/daemon/core/gui/dialogs/mobilityconfig.py b/daemon/core/gui/dialogs/mobilityconfig.py index daaf9ea5..ca9caf43 100644 --- a/daemon/core/gui/dialogs/mobilityconfig.py +++ b/daemon/core/gui/dialogs/mobilityconfig.py @@ -6,11 +6,10 @@ from typing import TYPE_CHECKING, Dict, Optional import grpc -from core.api.grpc.common_pb2 import ConfigOption -from core.api.grpc.core_pb2 import Node from core.gui.dialogs.dialog import Dialog from core.gui.themes import PADX, PADY from core.gui.widgets import ConfigFrame +from core.gui.wrappers import ConfigOption, Node if TYPE_CHECKING: from core.gui.app import Application diff --git a/daemon/core/gui/dialogs/mobilityplayer.py b/daemon/core/gui/dialogs/mobilityplayer.py index e6ef62ea..16aa8ea0 100644 --- a/daemon/core/gui/dialogs/mobilityplayer.py +++ b/daemon/core/gui/dialogs/mobilityplayer.py @@ -4,12 +4,11 @@ from typing import TYPE_CHECKING, Dict, Optional import grpc -from core.api.grpc.common_pb2 import ConfigOption -from core.api.grpc.core_pb2 import Node from core.api.grpc.mobility_pb2 import MobilityAction from core.gui.dialogs.dialog import Dialog from core.gui.images import ImageEnum from core.gui.themes import PADX, PADY +from core.gui.wrappers import ConfigOption, Node if TYPE_CHECKING: from core.gui.app import Application diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index 9e958283..33c8fb32 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Dict, Optional import netaddr from PIL.ImageTk import PhotoImage -from core.api.grpc.core_pb2 import Node from core.gui import nodeutils, validation from core.gui.appconfig import ICONS_PATH from core.gui.dialogs.dialog import Dialog @@ -16,6 +15,7 @@ from core.gui.images import Images from core.gui.nodeutils import NodeUtils from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import ListboxScroll, image_chooser +from core.gui.wrappers import Node if TYPE_CHECKING: from core.gui.app import Application diff --git a/daemon/core/gui/dialogs/sessionoptions.py b/daemon/core/gui/dialogs/sessionoptions.py index fd021fee..24bacb30 100644 --- a/daemon/core/gui/dialogs/sessionoptions.py +++ b/daemon/core/gui/dialogs/sessionoptions.py @@ -5,10 +5,10 @@ from typing import TYPE_CHECKING, Dict, Optional import grpc -from core.api.grpc.common_pb2 import ConfigOption from core.gui.dialogs.dialog import Dialog from core.gui.themes import PADX, PADY from core.gui.widgets import ConfigFrame +from core.gui.wrappers import ConfigOption if TYPE_CHECKING: from core.gui.app import Application @@ -28,7 +28,7 @@ class SessionOptionsDialog(Dialog): try: session_id = self.app.core.session_id response = self.app.core.client.get_session_options(session_id) - return response.config + return ConfigOption.from_dict(response.config) except grpc.RpcError as e: self.app.show_grpc_exception("Get Session Options Error", e) self.has_error = True diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index a7d702eb..75b9dcf4 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -5,12 +5,11 @@ from typing import TYPE_CHECKING, List, Optional import grpc -from core.api.grpc import core_pb2 -from core.api.grpc.core_pb2 import SessionSummary from core.gui.dialogs.dialog import Dialog from core.gui.images import ImageEnum, Images from core.gui.task import ProgressTask from core.gui.themes import PADX, PADY +from core.gui.wrappers import SessionState, SessionSummary if TYPE_CHECKING: from core.gui.app import Application @@ -33,7 +32,7 @@ class SessionsDialog(Dialog): try: response = self.app.core.client.get_sessions() logging.info("sessions: %s", response) - return response.sessions + return [SessionSummary.from_proto(x) for x in response.sessions] except grpc.RpcError as e: self.app.show_grpc_exception("Get Sessions Error", e) self.destroy() @@ -82,7 +81,7 @@ class SessionsDialog(Dialog): self.tree.heading("nodes", text="Node Count") for index, session in enumerate(self.sessions): - state_name = core_pb2.SessionState.Enum.Name(session.state) + state_name = SessionState(session.state).name self.tree.insert( "", tk.END, diff --git a/daemon/core/gui/dialogs/wlanconfig.py b/daemon/core/gui/dialogs/wlanconfig.py index 326b3195..17f62dfb 100644 --- a/daemon/core/gui/dialogs/wlanconfig.py +++ b/daemon/core/gui/dialogs/wlanconfig.py @@ -3,11 +3,10 @@ from typing import TYPE_CHECKING, Dict, Optional import grpc -from core.api.grpc.common_pb2 import ConfigOption -from core.api.grpc.core_pb2 import Node from core.gui.dialogs.dialog import Dialog from core.gui.themes import PADX, PADY from core.gui.widgets import ConfigFrame +from core.gui.wrappers import ConfigOption, Node if TYPE_CHECKING: from core.gui.app import Application diff --git a/daemon/core/gui/frames/link.py b/daemon/core/gui/frames/link.py index 57b1bf66..cbea9982 100644 --- a/daemon/core/gui/frames/link.py +++ b/daemon/core/gui/frames/link.py @@ -1,9 +1,9 @@ import tkinter as tk from typing import TYPE_CHECKING, Optional -from core.api.grpc.core_pb2 import Interface from core.gui.frames.base import DetailsFrame, InfoFrameBase from core.gui.utils import bandwidth_text +from core.gui.wrappers import Interface if TYPE_CHECKING: from core.gui.app import Application @@ -62,7 +62,7 @@ class EdgeInfoFrame(InfoFrameBase): ip6 = f"{iface2.ip6}/{iface2.ip6_mask}" if iface2.ip6 else "" frame.add_detail("IP6", ip6) - if link.HasField("options"): + if link.options: frame.add_separator() bandwidth = bandwidth_text(options.bandwidth) frame.add_detail("Bandwidth", bandwidth) diff --git a/daemon/core/gui/frames/node.py b/daemon/core/gui/frames/node.py index 7480e056..577cc489 100644 --- a/daemon/core/gui/frames/node.py +++ b/daemon/core/gui/frames/node.py @@ -1,8 +1,8 @@ from typing import TYPE_CHECKING -from core.api.grpc.core_pb2 import NodeType from core.gui.frames.base import DetailsFrame, InfoFrameBase from core.gui.nodeutils import NodeUtils +from core.gui.wrappers import NodeType if TYPE_CHECKING: from core.gui.app import Application diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index d9085910..610b6cc0 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -3,14 +3,13 @@ import math import tkinter as tk from typing import TYPE_CHECKING, Optional, Tuple -from core.api.grpc import core_pb2 -from core.api.grpc.core_pb2 import Interface, Link from core.gui import themes from core.gui.dialogs.linkconfig import LinkConfigurationDialog from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame from core.gui.graph import tags from core.gui.nodeutils import NodeUtils from core.gui.utils import bandwidth_text +from core.gui.wrappers import Interface, Link if TYPE_CHECKING: from core.gui.graph.graph import CanvasGraph @@ -305,7 +304,7 @@ class CanvasEdge(Edge): self.link = link self.draw_labels() - def iface_label(self, iface: core_pb2.Interface) -> str: + def iface_label(self, iface: Interface) -> str: label = "" if iface.name and self.canvas.show_iface_names.get(): label = f"{iface.name}" @@ -319,10 +318,10 @@ class CanvasEdge(Edge): def create_node_labels(self) -> Tuple[str, str]: label1 = None - if self.link.HasField("iface1"): + if self.link.iface1: label1 = self.iface_label(self.link.iface1) label2 = None - if self.link.HasField("iface2"): + if self.link.iface2: label2 = self.iface_label(self.link.iface2) return label1, label2 @@ -417,6 +416,8 @@ class CanvasEdge(Edge): dialog.show() def draw_link_options(self): + if not self.link.options: + return options = self.link.options lines = [] bandwidth = options.bandwidth diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 56a31c3f..8b053c78 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -7,14 +7,7 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from PIL import Image from PIL.ImageTk import PhotoImage -from core.api.grpc.core_pb2 import ( - Interface, - Link, - LinkType, - Node, - Session, - ThroughputsEvent, -) +from core.api.grpc.core_pb2 import ThroughputsEvent from core.gui.dialogs.shapemod import ShapeDialog from core.gui.graph import tags from core.gui.graph.edges import ( @@ -30,6 +23,7 @@ from core.gui.graph.shape import Shape from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker from core.gui.images import ImageEnum, TypeToImage from core.gui.nodeutils import NodeDraw, NodeUtils +from core.gui.wrappers import Interface, Link, LinkType, Node if TYPE_CHECKING: from core.gui.app import Application @@ -134,12 +128,7 @@ class CanvasGraph(tk.Canvas): ) self.configure(scrollregion=self.bbox(tk.ALL)) - def reset_and_redraw(self, session: Session) -> None: - """ - Reset the private variables CanvasGraph object, redraw nodes given the new grpc - client. - :param session: session to draw - """ + def reset_and_redraw(self, nodes: List[Node], links: List[Link]) -> None: # reset view options to default state self.show_node_labels.set(True) self.show_link_labels.set(True) @@ -164,7 +153,7 @@ class CanvasGraph(tk.Canvas): self.wireless_edges.clear() self.wireless_network.clear() self.drawing_edge = None - self.draw_session(session) + self.draw_session(nodes, links) def setup_bindings(self) -> None: """ @@ -251,12 +240,12 @@ class CanvasGraph(tk.Canvas): dst.edges.add(edge) self.edges[edge.token] = edge self.core.links[edge.token] = edge - if link.HasField("iface1"): + 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.HasField("iface2"): + if link.iface2: iface2 = link.iface2 self.core.iface_to_edge[(node2.id, iface2.id)] = edge.token dst.ifaces[iface2.id] = iface2 @@ -337,19 +326,19 @@ class CanvasGraph(tk.Canvas): self.nodes[node.id] = node self.core.canvas_nodes[core_node.id] = node - def draw_session(self, session: Session) -> None: + def draw_session(self, nodes: List[Node], links: List[Link]) -> None: """ Draw existing session. """ # draw existing nodes - for core_node in session.nodes: + for core_node in nodes: # peer to peer node is not drawn on the GUI if NodeUtils.is_ignore_node(core_node.type): continue self.add_core_node(core_node) # draw existing links - for link in session.links: + for link in links: logging.debug("drawing link: %s", link) canvas_node1 = self.core.canvas_nodes[link.node1_id] canvas_node2 = self.core.canvas_nodes[link.node2_id] @@ -987,12 +976,12 @@ class CanvasGraph(tk.Canvas): copy_edge = self.edges[token] copy_link = copy_edge.link options = edge.link.options - copy_link.options.CopyFrom(options) + copy_link.options = deepcopy(options) iface1_id = None - if copy_link.HasField("iface1"): + if copy_link.iface1: iface1_id = copy_link.iface1.id iface2_id = None - if copy_link.HasField("iface2"): + if copy_link.iface2: iface2_id = copy_link.iface2.id if not options.unidirectional: copy_edge.asymmetric_link = None diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 7b5cd2f3..df6476c7 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -6,8 +6,6 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple import grpc from PIL.ImageTk import PhotoImage -from core.api.grpc.common_pb2 import ConfigOption -from core.api.grpc.core_pb2 import Interface, Node, NodeType from core.api.grpc.services_pb2 import NodeServiceData from core.gui import themes from core.gui.dialogs.emaneconfig import EmaneConfigDialog @@ -22,6 +20,7 @@ from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge from core.gui.graph.tooltip import CanvasTooltip from core.gui.images import ImageEnum from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils +from core.gui.wrappers import ConfigOption, Interface, Node, NodeType if TYPE_CHECKING: from core.gui.app import Application diff --git a/daemon/core/gui/images.py b/daemon/core/gui/images.py index 22719457..0a2f4d5d 100644 --- a/daemon/core/gui/images.py +++ b/daemon/core/gui/images.py @@ -5,8 +5,8 @@ from typing import Dict, Optional, Tuple from PIL import Image from PIL.ImageTk import PhotoImage -from core.api.grpc.core_pb2 import NodeType from core.gui.appconfig import LOCAL_ICONS_PATH +from core.gui.wrappers import NodeType class Images: diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index f4f2e3cc..f5b1461e 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -4,9 +4,9 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple import netaddr from netaddr import EUI, IPNetwork -from core.api.grpc.core_pb2 import Interface, Link, Node from core.gui.graph.node import CanvasNode from core.gui.nodeutils import NodeUtils +from core.gui.wrappers import Interface, Link, Node if TYPE_CHECKING: from core.gui.app import Application @@ -89,10 +89,10 @@ class InterfaceManager: remaining_subnets = set() for edge in self.app.core.links.values(): link = edge.link - if link.HasField("iface1"): + if link.iface1: subnets = self.get_subnets(link.iface1) remaining_subnets.add(subnets) - if link.HasField("iface2"): + if link.iface2: subnets = self.get_subnets(link.iface2) remaining_subnets.add(subnets) @@ -100,9 +100,9 @@ class InterfaceManager: # or remove used indexes from subnet ifaces = [] for link in links: - if link.HasField("iface1"): + if link.iface1: ifaces.append(link.iface1) - if link.HasField("iface2"): + if link.iface2: ifaces.append(link.iface2) for iface in ifaces: subnets = self.get_subnets(iface) @@ -117,9 +117,9 @@ class InterfaceManager: def joined(self, links: List[Link]) -> None: ifaces = [] for link in links: - if link.HasField("iface1"): + if link.iface1: ifaces.append(link.iface1) - if link.HasField("iface2"): + if link.iface2: ifaces.append(link.iface2) # add to used subnets and mark used indexes diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index 08c8f31c..6c451303 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -3,9 +3,9 @@ from typing import List, Optional, Set from PIL.ImageTk import PhotoImage -from core.api.grpc.core_pb2 import Node, NodeType from core.gui.appconfig import CustomNode, GuiConfig from core.gui.images import ImageEnum, Images, TypeToImage +from core.gui.wrappers import Node, NodeType ICON_SIZE: int = 48 ANTENNA_SIZE: int = 32 @@ -17,7 +17,7 @@ class NodeDraw: self.image: Optional[PhotoImage] = None self.image_enum: Optional[ImageEnum] = None self.image_file: Optional[str] = None - self.node_type: NodeType = None + self.node_type: Optional[NodeType] = None self.model: Optional[str] = None self.services: Set[str] = set() self.label: Optional[str] = None diff --git a/daemon/core/gui/statusbar.py b/daemon/core/gui/statusbar.py index 6989593e..d4304b6e 100644 --- a/daemon/core/gui/statusbar.py +++ b/daemon/core/gui/statusbar.py @@ -5,9 +5,9 @@ import tkinter as tk from tkinter import ttk from typing import TYPE_CHECKING, List, Optional -from core.api.grpc.core_pb2 import ExceptionEvent, ExceptionLevel from core.gui.dialogs.alerts import AlertsDialog from core.gui.themes import Styles +from core.gui.wrappers import ExceptionEvent, ExceptionLevel if TYPE_CHECKING: from core.gui.app import Application @@ -69,7 +69,7 @@ class StatusBar(ttk.Frame): def add_alert(self, event: ExceptionEvent) -> None: self.core_alarms.append(event) - level = event.exception_event.level + level = event.level self._set_alert_style(level) label = f"Alerts ({len(self.core_alarms)})" self.alerts_button.config(text=label, style=self.alert_style) diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 968b447d..b7b67338 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Callable, List, Optional from PIL.ImageTk import PhotoImage -from core.api.grpc import core_pb2 from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.runtool import RunToolDialog from core.gui.graph import tags @@ -300,15 +299,15 @@ class Toolbar(ttk.Frame): ) task.start() - def start_callback(self, response: core_pb2.StartSessionResponse) -> None: - if response.result: + def start_callback(self, result: bool, exceptions: List[str]) -> None: + if result: self.set_runtime() self.app.core.set_metadata() self.app.core.show_mobility_players() else: enable_buttons(self.design_frame, enabled=True) - if response.exceptions: - message = "\n".join(response.exceptions) + if exceptions: + message = "\n".join(exceptions) self.app.show_error("Start Session Error", message) def set_runtime(self) -> None: @@ -405,7 +404,7 @@ class Toolbar(ttk.Frame): ) task.start() - def stop_callback(self, response: core_pb2.StopSessionResponse) -> None: + def stop_callback(self, result: bool) -> None: self.set_design() self.app.canvas.stopped_session() diff --git a/daemon/core/gui/widgets.py b/daemon/core/gui/widgets.py index 81bad0f5..85f3da10 100644 --- a/daemon/core/gui/widgets.py +++ b/daemon/core/gui/widgets.py @@ -5,12 +5,10 @@ from pathlib import Path from tkinter import filedialog, font, ttk from typing import TYPE_CHECKING, Any, Callable, Dict, Set, Type -from core.api.grpc import core_pb2 -from core.api.grpc.common_pb2 import ConfigOption -from core.api.grpc.core_pb2 import ConfigOptionType from core.gui import themes, validation from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY +from core.gui.wrappers import ConfigOption, ConfigOptionType if TYPE_CHECKING: from core.gui.app import Application @@ -110,7 +108,7 @@ class ConfigFrame(ttk.Notebook): label = ttk.Label(tab.frame, text=option.label) label.grid(row=index, pady=PADY, padx=PADX, sticky="w") value = tk.StringVar() - if option.type == core_pb2.ConfigOptionType.BOOL: + if option.type == ConfigOptionType.BOOL: select = ("On", "Off") state = "readonly" if self.enabled else tk.DISABLED combobox = ttk.Combobox( @@ -129,7 +127,7 @@ class ConfigFrame(ttk.Notebook): tab.frame, textvariable=value, values=select, state=state ) combobox.grid(row=index, column=1, sticky="ew") - elif option.type == core_pb2.ConfigOptionType.STRING: + elif option.type == ConfigOptionType.STRING: value.set(option.value) state = tk.NORMAL if self.enabled else tk.DISABLED if "file" in option.label: @@ -153,7 +151,7 @@ class ConfigFrame(ttk.Notebook): tab.frame, textvariable=value, state=state ) entry.grid(row=index, column=1, sticky="ew") - elif option.type == core_pb2.ConfigOptionType.FLOAT: + elif option.type == ConfigOptionType.FLOAT: value.set(option.value) state = tk.NORMAL if self.enabled else tk.DISABLED entry = validation.PositiveFloatEntry( @@ -169,7 +167,7 @@ class ConfigFrame(ttk.Notebook): option = self.config[key] value = self.values[key] config_value = value.get() - if option.type == core_pb2.ConfigOptionType.BOOL: + if option.type == ConfigOptionType.BOOL: if config_value == "On": option.value = "1" else: @@ -182,7 +180,7 @@ class ConfigFrame(ttk.Notebook): for name, data in config.items(): option = self.config[name] value = self.values[name] - if option.type == core_pb2.ConfigOptionType.BOOL: + if option.type == ConfigOptionType.BOOL: if data == "1": data = "On" else: diff --git a/daemon/core/gui/wrappers.py b/daemon/core/gui/wrappers.py index 217ab321..f72cbac4 100644 --- a/daemon/core/gui/wrappers.py +++ b/daemon/core/gui/wrappers.py @@ -1,8 +1,22 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import Enum -from typing import List +from typing import Dict, List -from core.api.grpc import core_pb2 +from core.api.grpc import common_pb2, core_pb2 + + +class ConfigOptionType(Enum): + UINT8 = 1 + UINT16 = 2 + UINT32 = 3 + UINT64 = 4 + INT8 = 5 + INT16 = 6 + INT32 = 7 + INT64 = 8 + FLOAT = 9 + STRING = 10 + BOOL = 11 class SessionState(Enum): @@ -30,6 +44,292 @@ class NodeType(Enum): LXC = 16 +class LinkType(Enum): + WIRELESS = 0 + WIRED = 1 + + +class ExceptionLevel(Enum): + DEFAULT = 0 + FATAL = 1 + ERROR = 2 + WARNING = 3 + NOTICE = 4 + + +class MessageType(Enum): + NONE = 0 + ADD = 1 + DELETE = 2 + CRI = 4 + LOCAL = 8 + STRING = 16 + TEXT = 32 + TTY = 64 + + +@dataclass +class SessionLocation: + x: float + y: float + z: float + lat: float + lon: float + alt: float + scale: float + + @classmethod + def from_proto(cls, location: core_pb2.SessionLocation) -> "SessionLocation": + return SessionLocation( + x=location.x, + y=location.y, + z=location.z, + lat=location.lat, + lon=location.lon, + alt=location.alt, + scale=location.scale, + ) + + def to_proto(self) -> core_pb2.SessionLocation: + return core_pb2.SessionLocation( + x=self.x, + y=self.y, + z=self.z, + lat=self.lat, + lon=self.lon, + alt=self.alt, + scale=self.scale, + ) + + +@dataclass +class ExceptionEvent: + session_id: int + node_id: int + level: ExceptionLevel + source: str + date: str + text: str + opaque: str + + @classmethod + def from_proto( + cls, session_id: int, event: core_pb2.ExceptionEvent + ) -> "ExceptionEvent": + return ExceptionEvent( + session_id=session_id, + node_id=event.node_id, + level=ExceptionLevel(event.level), + source=event.source, + date=event.date, + text=event.text, + opaque=event.opaque, + ) + + +@dataclass +class ConfigOption: + label: str + name: str + value: str + type: ConfigOptionType + group: str + select: List[str] = None + + @classmethod + def from_dict( + cls, config: Dict[str, common_pb2.ConfigOption] + ) -> Dict[str, "ConfigOption"]: + d = {} + for key, value in config.items(): + d[key] = ConfigOption.from_proto(value) + return d + + @classmethod + def to_dict(cls, config: Dict[str, "ConfigOption"]) -> Dict[str, str]: + return {k: v.value for k, v in config.items()} + + @classmethod + def from_proto(cls, option: common_pb2.ConfigOption) -> "ConfigOption": + return ConfigOption( + label=option.label, + name=option.name, + value=option.value, + type=ConfigOptionType(option.type), + group=option.group, + select=option.select, + ) + + +@dataclass +class Interface: + id: int + name: str = None + mac: str = None + ip4: str = None + ip4_mask: int = None + ip6: str = None + ip6_mask: int = None + net_id: int = None + flow_id: int = None + mtu: int = None + node_id: int = None + net2_id: int = None + + @classmethod + def from_proto(cls, iface: core_pb2.Interface) -> "Interface": + return Interface( + id=iface.id, + name=iface.name, + mac=iface.mac, + ip4=iface.ip4, + ip4_mask=iface.ip4_mask, + ip6=iface.ip6, + ip6_mask=iface.ip6_mask, + net_id=iface.net_id, + flow_id=iface.flow_id, + mtu=iface.mtu, + node_id=iface.node_id, + net2_id=iface.net2_id, + ) + + def to_proto(self) -> core_pb2.Interface: + return core_pb2.Interface( + id=self.id, + name=self.name, + mac=self.mac, + ip4=self.ip4, + ip4_mask=self.ip4_mask, + ip6=self.ip6, + ip6_mask=self.ip6_mask, + net_id=self.net_id, + flow_id=self.flow_id, + mtu=self.mtu, + node_id=self.node_id, + net2_id=self.net2_id, + ) + + +@dataclass +class LinkOptions: + jitter: int = 0 + key: int = 0 + mburst: int = 0 + mer: int = 0 + loss: float = 0.0 + bandwidth: int = 0 + burst: int = 0 + delay: int = 0 + dup: int = 0 + unidirectional: bool = False + + @classmethod + def from_proto(cls, options: core_pb2.LinkOptions) -> "LinkOptions": + return LinkOptions( + jitter=options.jitter, + key=options.key, + mburst=options.mburst, + mer=options.mer, + loss=options.loss, + bandwidth=options.bandwidth, + burst=options.burst, + delay=options.delay, + dup=options.dup, + unidirectional=options.unidirectional, + ) + + def to_proto(self) -> core_pb2.LinkOptions: + return core_pb2.LinkOptions( + jitter=self.jitter, + key=self.key, + mburst=self.mburst, + mer=self.mer, + loss=self.loss, + bandwidth=self.bandwidth, + burst=self.burst, + delay=self.delay, + dup=self.dup, + unidirectional=self.unidirectional, + ) + + +@dataclass +class Link: + node1_id: int + node2_id: int + type: LinkType = LinkType.WIRED + iface1: Interface = None + iface2: Interface = None + options: LinkOptions = None + network_id: int = None + label: str = None + color: str = None + + @classmethod + def from_proto(cls, link: core_pb2.Link) -> "Link": + iface1 = None + if link.HasField("iface1"): + iface1 = Interface.from_proto(link.iface1) + iface2 = None + if link.HasField("iface2"): + iface2 = Interface.from_proto(link.iface2) + options = None + if link.HasField("options"): + options = LinkOptions.from_proto(link.options) + return Link( + type=LinkType(link.type), + node1_id=link.node1_id, + node2_id=link.node2_id, + iface1=iface1, + iface2=iface2, + options=options, + network_id=link.network_id, + label=link.label, + color=link.color, + ) + + def to_proto(self) -> core_pb2.Link: + iface1 = self.iface1.to_proto() if self.iface1 else None + iface2 = self.iface2.to_proto() if self.iface2 else None + options = self.options.to_proto() if self.options else None + return core_pb2.Link( + type=self.type.value, + node1_id=self.node1_id, + node2_id=self.node2_id, + iface1=iface1, + iface2=iface2, + options=options, + network_id=self.network_id, + label=self.label, + color=self.color, + ) + + def is_symmetric(self) -> bool: + result = True + if self.options: + result = self.options.unidirectional is False + return result + + +@dataclass +class SessionSummary: + id: int + state: SessionState + nodes: int + file: str + dir: str + + @classmethod + def from_proto(cls, summary: core_pb2.SessionSummary) -> "SessionSummary": + return SessionSummary( + id=summary.id, + state=SessionState(summary.state), + nodes=summary.nodes, + file=summary.file, + dir=summary.dir, + ) + + @dataclass class Hook: state: SessionState @@ -78,8 +378,8 @@ class Node: type: NodeType model: str = None position: Position = None - services: List[str] = None - config_services: List[str] = None + services: List[str] = field(default_factory=list) + config_services: List[str] = field(default_factory=list) emane: str = None icon: str = None image: str = None @@ -120,7 +420,32 @@ class Node: icon=self.icon, image=self.image, server=self.server, - geo=self.geo.to_proto(), dir=self.dir, channel=self.channel, ) + + +@dataclass +class LinkEvent: + message_type: MessageType + link: Link + + @classmethod + def from_proto(cls, event: core_pb2.LinkEvent) -> "LinkEvent": + return LinkEvent( + message_type=MessageType(event.message_type), + link=Link.from_proto(event.link), + ) + + +@dataclass +class NodeEvent: + message_type: MessageType + node: Node + + @classmethod + def from_proto(cls, event: core_pb2.NodeEvent) -> "NodeEvent": + return NodeEvent( + message_type=MessageType(event.message_type), + node=Node.from_proto(event.node), + ) From a9a2fb8e46080770947bb5b0953c38644f54c56b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 25 Jul 2020 19:43:24 -0700 Subject: [PATCH 0546/1131] pygui: added wrapper for throughput events, fixed sending nodes/links for configuration --- daemon/core/gui/coreclient.py | 17 +++++++-------- daemon/core/gui/graph/graph.py | 3 +-- daemon/core/gui/wrappers.py | 40 ++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index c41caeca..099ce043 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -12,9 +12,9 @@ from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple import grpc -from core.api.grpc import client +from core.api.grpc import client, core_pb2 from core.api.grpc.configservices_pb2 import ConfigService, ConfigServiceConfig -from core.api.grpc.core_pb2 import CpuUsageEvent, Event, ThroughputsEvent +from core.api.grpc.core_pb2 import CpuUsageEvent, Event from core.api.grpc.emane_pb2 import EmaneModelConfig from core.api.grpc.mobility_pb2 import MobilityConfig from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig, ServiceFileConfig @@ -46,6 +46,7 @@ from core.gui.wrappers import ( Position, SessionLocation, SessionState, + ThroughputsEvent, ) if TYPE_CHECKING: @@ -275,7 +276,8 @@ class CoreClient: CPU_USAGE_DELAY, self.handle_cpu_event ) - def handle_throughputs(self, event: ThroughputsEvent) -> None: + def handle_throughputs(self, event: core_pb2.ThroughputsEvent) -> None: + event = ThroughputsEvent.from_proto(event) if event.session_id != self.session_id: logging.warning( "ignoring throughput event session(%s) current(%s)", @@ -776,12 +778,9 @@ class CoreClient: """ create nodes and links that have not been created yet """ - node_protos = [x.core_node for x in self.canvas_nodes.values()] - link_protos = [x.link for x in self.links.values()] - if self.state != SessionState.DEFINITION: - self.client.set_session_state(self.session_id, SessionState.DEFINITION) - - self.client.set_session_state(self.session_id, SessionState.DEFINITION) + node_protos = [x.core_node.to_proto() for x in self.canvas_nodes.values()] + link_protos = [x.link.to_proto() for x in self.links.values()] + self.client.set_session_state(self.session_id, SessionState.DEFINITION.value) for node_proto in node_protos: response = self.client.add_node(self.session_id, node_proto) logging.debug("create node: %s", response) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 8b053c78..ae0b00c0 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from PIL import Image from PIL.ImageTk import PhotoImage -from core.api.grpc.core_pb2 import ThroughputsEvent from core.gui.dialogs.shapemod import ShapeDialog from core.gui.graph import tags from core.gui.graph.edges import ( @@ -23,7 +22,7 @@ from core.gui.graph.shape import Shape from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker from core.gui.images import ImageEnum, TypeToImage from core.gui.nodeutils import NodeDraw, NodeUtils -from core.gui.wrappers import Interface, Link, LinkType, Node +from core.gui.wrappers import Interface, Link, LinkType, Node, ThroughputsEvent if TYPE_CHECKING: from core.gui.app import Application diff --git a/daemon/core/gui/wrappers.py b/daemon/core/gui/wrappers.py index f72cbac4..4098a4df 100644 --- a/daemon/core/gui/wrappers.py +++ b/daemon/core/gui/wrappers.py @@ -68,6 +68,46 @@ class MessageType(Enum): TTY = 64 +@dataclass +class BridgeThroughput: + node_id: int + throughput: float + + @classmethod + def from_proto(cls, proto: core_pb2.BridgeThroughput) -> "BridgeThroughput": + return BridgeThroughput(node_id=proto.node_id, throughput=proto.throughput) + + +@dataclass +class InterfaceThroughput: + node_id: int + iface_id: int + throughput: float + + @classmethod + def from_proto(cls, proto: core_pb2.InterfaceThroughput) -> "InterfaceThroughput": + return InterfaceThroughput( + node_id=proto.node_id, iface_id=proto.iface_id, throughput=proto.throughput + ) + + +@dataclass +class ThroughputsEvent: + session_id: int + bridge_throughputs: List[BridgeThroughput] + iface_throughputs: List[InterfaceThroughput] + + @classmethod + def from_proto(cls, proto: core_pb2.ThroughputsEvent) -> "ThroughputsEvent": + bridges = [BridgeThroughput.from_proto(x) for x in proto.bridge_throughputs] + ifaces = [InterfaceThroughput.from_proto(x) for x in proto.iface_throughputs] + return ThroughputsEvent( + session_id=proto.session_id, + bridge_throughputs=bridges, + iface_throughputs=ifaces, + ) + + @dataclass class SessionLocation: x: float From 82a212d1cfc409ba0fa754e6d032ef12550d6743 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 25 Jul 2020 20:27:11 -0700 Subject: [PATCH 0547/1131] pygui: modified usages of protobufs within coreclient to use module namespace to make more obvious, replaced config services and services with wrappers --- daemon/core/gui/coreclient.py | 56 ++-- .../core/gui/dialogs/configserviceconfig.py | 3 +- daemon/core/gui/dialogs/mobilityplayer.py | 9 +- daemon/core/gui/dialogs/serviceconfig.py | 4 +- daemon/core/gui/graph/node.py | 3 +- daemon/core/gui/wrappers.py | 266 ++++++++++++------ 6 files changed, 213 insertions(+), 128 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 099ce043..fd1abc34 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -12,13 +12,15 @@ from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple import grpc -from core.api.grpc import client, core_pb2 -from core.api.grpc.configservices_pb2 import ConfigService, ConfigServiceConfig -from core.api.grpc.core_pb2 import CpuUsageEvent, Event -from core.api.grpc.emane_pb2 import EmaneModelConfig -from core.api.grpc.mobility_pb2 import MobilityConfig -from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig, ServiceFileConfig -from core.api.grpc.wlan_pb2 import WlanConfig +from core.api.grpc import ( + client, + configservices_pb2, + core_pb2, + emane_pb2, + mobility_pb2, + services_pb2, + wlan_pb2, +) from core.gui import appconfig from core.gui.appconfig import CoreServer, Observer from core.gui.dialogs.emaneinstall import EmaneInstallDialog @@ -33,6 +35,7 @@ from core.gui.interface import InterfaceManager from core.gui.nodeutils import NodeDraw, NodeUtils from core.gui.wrappers import ( ConfigOption, + ConfigService, ExceptionEvent, Hook, Interface, @@ -42,6 +45,7 @@ from core.gui.wrappers import ( MessageType, Node, NodeEvent, + NodeServiceData, NodeType, Position, SessionLocation, @@ -150,7 +154,7 @@ class CoreClient: for observer in self.app.guiconfig.observers: self.custom_observers[observer.name] = observer - def handle_events(self, event: Event) -> None: + def handle_events(self, event: core_pb2.Event) -> None: if event.source == GUI_SOURCE: return if event.session_id != self.session_id: @@ -288,7 +292,7 @@ class CoreClient: logging.debug("handling throughputs event: %s", event) self.app.after(0, self.app.canvas.set_throughputs, event) - def handle_cpu_event(self, event: CpuUsageEvent) -> None: + def handle_cpu_event(self, event: core_pb2.CpuUsageEvent) -> None: self.app.after(0, self.app.statusbar.set_cpu, event.usage) def handle_exception_event(self, event: ExceptionEvent) -> None: @@ -514,7 +518,7 @@ class CoreClient: # get config service informations response = self.client.get_config_services() for service in response.services: - self.config_services[service.name] = service + self.config_services[service.name] = ConfigService.from_proto(service) group_services = self.config_services_groups.setdefault( service.group, set() ) @@ -708,7 +712,7 @@ class CoreClient: logging.debug( "get node(%s) %s service, response: %s", node_id, service_name, response ) - return response.service + return NodeServiceData.from_proto(response.service) def set_node_service( self, @@ -742,7 +746,7 @@ class CoreClient: response, ) response = self.client.get_node_service(self.session_id, node_id, service_name) - return response.service + return NodeServiceData.from_proto(response.service) def get_node_service_file( self, node_id: int, service_name: str, file_name: str @@ -1000,7 +1004,7 @@ class CoreClient: self.links[edge.token] = edge logging.info("Add link between %s and %s", src_node.name, dst_node.name) - def get_wlan_configs_proto(self) -> List[WlanConfig]: + def get_wlan_configs_proto(self) -> List[wlan_pb2.WlanConfig]: configs = [] for canvas_node in self.canvas_nodes.values(): if canvas_node.core_node.type != NodeType.WIRELESS_LAN: @@ -1009,11 +1013,11 @@ class CoreClient: continue config = ConfigOption.to_dict(canvas_node.wlan_config) node_id = canvas_node.core_node.id - wlan_config = WlanConfig(node_id=node_id, config=config) + wlan_config = wlan_pb2.WlanConfig(node_id=node_id, config=config) configs.append(wlan_config) return configs - def get_mobility_configs_proto(self) -> List[MobilityConfig]: + def get_mobility_configs_proto(self) -> List[mobility_pb2.MobilityConfig]: configs = [] for canvas_node in self.canvas_nodes.values(): if canvas_node.core_node.type != NodeType.WIRELESS_LAN: @@ -1022,11 +1026,13 @@ class CoreClient: continue config = ConfigOption.to_dict(canvas_node.mobility_config) node_id = canvas_node.core_node.id - mobility_config = MobilityConfig(node_id=node_id, config=config) + mobility_config = mobility_pb2.MobilityConfig( + node_id=node_id, config=config + ) configs.append(mobility_config) return configs - def get_emane_model_configs_proto(self) -> List[EmaneModelConfig]: + def get_emane_model_configs_proto(self) -> List[emane_pb2.EmaneModelConfig]: configs = [] for canvas_node in self.canvas_nodes.values(): if canvas_node.core_node.type != NodeType.EMANE: @@ -1037,13 +1043,13 @@ class CoreClient: config = ConfigOption.to_dict(config) if iface_id is None: iface_id = -1 - config_proto = EmaneModelConfig( + config_proto = emane_pb2.EmaneModelConfig( node_id=node_id, iface_id=iface_id, model=model, config=config ) configs.append(config_proto) return configs - def get_service_configs_proto(self) -> List[ServiceConfig]: + def get_service_configs_proto(self) -> List[services_pb2.ServiceConfig]: configs = [] for canvas_node in self.canvas_nodes.values(): if not NodeUtils.is_container_node(canvas_node.core_node.type): @@ -1052,7 +1058,7 @@ class CoreClient: continue node_id = canvas_node.core_node.id for name, config in canvas_node.service_configs.items(): - config_proto = ServiceConfig( + config_proto = services_pb2.ServiceConfig( node_id=node_id, service=name, directories=config.dirs, @@ -1064,7 +1070,7 @@ class CoreClient: configs.append(config_proto) return configs - def get_service_file_configs_proto(self) -> List[ServiceFileConfig]: + def get_service_file_configs_proto(self) -> List[services_pb2.ServiceFileConfig]: configs = [] for canvas_node in self.canvas_nodes.values(): if not NodeUtils.is_container_node(canvas_node.core_node.type): @@ -1074,13 +1080,15 @@ class CoreClient: node_id = canvas_node.core_node.id for service, file_configs in canvas_node.service_file_configs.items(): for file, data in file_configs.items(): - config_proto = ServiceFileConfig( + config_proto = services_pb2.ServiceFileConfig( node_id=node_id, service=service, file=file, data=data ) configs.append(config_proto) return configs - def get_config_service_configs_proto(self) -> List[ConfigServiceConfig]: + def get_config_service_configs_proto( + self + ) -> List[configservices_pb2.ConfigServiceConfig]: config_service_protos = [] for canvas_node in self.canvas_nodes.values(): if not NodeUtils.is_container_node(canvas_node.core_node.type): @@ -1090,7 +1098,7 @@ class CoreClient: node_id = canvas_node.core_node.id for name, service_config in canvas_node.config_service_configs.items(): config = service_config.get("config", {}) - config_proto = ConfigServiceConfig( + config_proto = configservices_pb2.ConfigServiceConfig( node_id=node_id, name=name, templates=service_config["templates"], diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py index 5a6a89a8..5463d88e 100644 --- a/daemon/core/gui/dialogs/configserviceconfig.py +++ b/daemon/core/gui/dialogs/configserviceconfig.py @@ -8,11 +8,10 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Set import grpc -from core.api.grpc.services_pb2 import ServiceValidationMode from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll -from core.gui.wrappers import ConfigOption +from core.gui.wrappers import ConfigOption, ServiceValidationMode if TYPE_CHECKING: from core.gui.app import Application diff --git a/daemon/core/gui/dialogs/mobilityplayer.py b/daemon/core/gui/dialogs/mobilityplayer.py index 16aa8ea0..66833aff 100644 --- a/daemon/core/gui/dialogs/mobilityplayer.py +++ b/daemon/core/gui/dialogs/mobilityplayer.py @@ -4,11 +4,10 @@ from typing import TYPE_CHECKING, Dict, Optional import grpc -from core.api.grpc.mobility_pb2 import MobilityAction from core.gui.dialogs.dialog import Dialog from core.gui.images import ImageEnum from core.gui.themes import PADX, PADY -from core.gui.wrappers import ConfigOption, Node +from core.gui.wrappers import ConfigOption, MobilityAction, Node if TYPE_CHECKING: from core.gui.app import Application @@ -150,7 +149,7 @@ class MobilityPlayerDialog(Dialog): session_id = self.app.core.session_id try: self.app.core.client.mobility_action( - session_id, self.node.id, MobilityAction.START + session_id, self.node.id, MobilityAction.START.value ) except grpc.RpcError as e: self.app.show_grpc_exception("Mobility Error", e) @@ -160,7 +159,7 @@ class MobilityPlayerDialog(Dialog): session_id = self.app.core.session_id try: self.app.core.client.mobility_action( - session_id, self.node.id, MobilityAction.PAUSE + session_id, self.node.id, MobilityAction.PAUSE.value ) except grpc.RpcError as e: self.app.show_grpc_exception("Mobility Error", e) @@ -170,7 +169,7 @@ class MobilityPlayerDialog(Dialog): session_id = self.app.core.session_id try: self.app.core.client.mobility_action( - session_id, self.node.id, MobilityAction.STOP + session_id, self.node.id, MobilityAction.STOP.value ) except grpc.RpcError as e: self.app.show_grpc_exception("Mobility Error", e) diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index 4e615db0..c033cfdc 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -7,12 +7,12 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple import grpc from PIL.ImageTk import PhotoImage -from core.api.grpc.services_pb2 import NodeServiceData, ServiceValidationMode from core.gui.dialogs.copyserviceconfig import CopyServiceConfigDialog from core.gui.dialogs.dialog import Dialog from core.gui.images import ImageEnum, Images from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CodeText, ListboxScroll +from core.gui.wrappers import NodeServiceData, ServiceValidationMode if TYPE_CHECKING: from core.gui.app import Application @@ -72,7 +72,7 @@ class ServiceConfigDialog(Dialog): self.service_file_data: Optional[CodeText] = None self.validation_period_entry: Optional[ttk.Entry] = None self.original_service_files: Dict[str, str] = {} - self.default_config: NodeServiceData = None + self.default_config: Optional[NodeServiceData] = None self.temp_service_files: Dict[str, str] = {} self.modified_files: Set[str] = set() self.has_error: bool = False diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index df6476c7..217389c0 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -6,7 +6,6 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple import grpc from PIL.ImageTk import PhotoImage -from core.api.grpc.services_pb2 import NodeServiceData from core.gui import themes from core.gui.dialogs.emaneconfig import EmaneConfigDialog from core.gui.dialogs.mobilityconfig import MobilityConfigDialog @@ -20,7 +19,7 @@ from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge from core.gui.graph.tooltip import CanvasTooltip from core.gui.images import ImageEnum from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils -from core.gui.wrappers import ConfigOption, Interface, Node, NodeType +from core.gui.wrappers import ConfigOption, Interface, Node, NodeServiceData, NodeType if TYPE_CHECKING: from core.gui.app import Application diff --git a/daemon/core/gui/wrappers.py b/daemon/core/gui/wrappers.py index 4098a4df..5fb12837 100644 --- a/daemon/core/gui/wrappers.py +++ b/daemon/core/gui/wrappers.py @@ -2,7 +2,25 @@ from dataclasses import dataclass, field from enum import Enum from typing import Dict, List -from core.api.grpc import common_pb2, core_pb2 +from core.api.grpc import common_pb2, configservices_pb2, core_pb2, services_pb2 + + +class ConfigServiceValidationMode(Enum): + BLOCKING = 0 + NON_BLOCKING = 1 + TIMER = 2 + + +class ServiceValidationMode(Enum): + BLOCKING = 0 + NON_BLOCKING = 1 + TIMER = 2 + + +class MobilityAction(Enum): + START = 0 + PAUSE = 1 + STOP = 2 class ConfigOptionType(Enum): @@ -68,6 +86,68 @@ class MessageType(Enum): TTY = 64 +@dataclass +class ConfigService: + group: str + name: str + executables: List[str] + dependencies: List[str] + directories: List[str] + files: List[str] + startup: List[str] + validate: List[str] + shutdown: List[str] + validation_mode: ConfigServiceValidationMode + validation_timer: int + validation_period: float + + @classmethod + def from_proto(cls, proto: configservices_pb2.ConfigService) -> "ConfigService": + return ConfigService( + group=proto.group, + name=proto.name, + executables=proto.executables, + dependencies=proto.dependencies, + directories=proto.directories, + files=proto.files, + startup=proto.startup, + validate=proto.validate, + shutdown=proto.shutdown, + validation_mode=ConfigServiceValidationMode(proto.validation_mode), + validation_timer=proto.validation_timer, + validation_period=proto.validation_period, + ) + + +@dataclass +class NodeServiceData: + executables: List[str] + dependencies: List[str] + dirs: List[str] + configs: List[str] + startup: List[str] + validate: List[str] + validation_mode: ServiceValidationMode + validation_timer: int + shutdown: List[str] + meta: str + + @classmethod + def from_proto(cls, proto: services_pb2.NodeServiceData) -> "NodeServiceData": + return NodeServiceData( + executables=proto.executables, + dependencies=proto.dependencies, + dirs=proto.dirs, + configs=proto.configs, + startup=proto.startup, + validate=proto.validate, + validation_mode=proto.validation_mode, + validation_timer=proto.validation_timer, + shutdown=proto.shutdown, + meta=proto.meta, + ) + + @dataclass class BridgeThroughput: node_id: int @@ -119,15 +199,15 @@ class SessionLocation: scale: float @classmethod - def from_proto(cls, location: core_pb2.SessionLocation) -> "SessionLocation": + def from_proto(cls, proto: core_pb2.SessionLocation) -> "SessionLocation": return SessionLocation( - x=location.x, - y=location.y, - z=location.z, - lat=location.lat, - lon=location.lon, - alt=location.alt, - scale=location.scale, + x=proto.x, + y=proto.y, + z=proto.z, + lat=proto.lat, + lon=proto.lon, + alt=proto.alt, + scale=proto.scale, ) def to_proto(self) -> core_pb2.SessionLocation: @@ -154,16 +234,16 @@ class ExceptionEvent: @classmethod def from_proto( - cls, session_id: int, event: core_pb2.ExceptionEvent + cls, session_id: int, proto: core_pb2.ExceptionEvent ) -> "ExceptionEvent": return ExceptionEvent( session_id=session_id, - node_id=event.node_id, - level=ExceptionLevel(event.level), - source=event.source, - date=event.date, - text=event.text, - opaque=event.opaque, + node_id=proto.node_id, + level=ExceptionLevel(proto.level), + source=proto.source, + date=proto.date, + text=proto.text, + opaque=proto.opaque, ) @@ -190,14 +270,14 @@ class ConfigOption: return {k: v.value for k, v in config.items()} @classmethod - def from_proto(cls, option: common_pb2.ConfigOption) -> "ConfigOption": + def from_proto(cls, proto: common_pb2.ConfigOption) -> "ConfigOption": return ConfigOption( - label=option.label, - name=option.name, - value=option.value, - type=ConfigOptionType(option.type), - group=option.group, - select=option.select, + label=proto.label, + name=proto.name, + value=proto.value, + type=ConfigOptionType(proto.type), + group=proto.group, + select=proto.select, ) @@ -217,20 +297,20 @@ class Interface: net2_id: int = None @classmethod - def from_proto(cls, iface: core_pb2.Interface) -> "Interface": + def from_proto(cls, proto: core_pb2.Interface) -> "Interface": return Interface( - id=iface.id, - name=iface.name, - mac=iface.mac, - ip4=iface.ip4, - ip4_mask=iface.ip4_mask, - ip6=iface.ip6, - ip6_mask=iface.ip6_mask, - net_id=iface.net_id, - flow_id=iface.flow_id, - mtu=iface.mtu, - node_id=iface.node_id, - net2_id=iface.net2_id, + id=proto.id, + name=proto.name, + mac=proto.mac, + ip4=proto.ip4, + ip4_mask=proto.ip4_mask, + ip6=proto.ip6, + ip6_mask=proto.ip6_mask, + net_id=proto.net_id, + flow_id=proto.flow_id, + mtu=proto.mtu, + node_id=proto.node_id, + net2_id=proto.net2_id, ) def to_proto(self) -> core_pb2.Interface: @@ -264,18 +344,18 @@ class LinkOptions: unidirectional: bool = False @classmethod - def from_proto(cls, options: core_pb2.LinkOptions) -> "LinkOptions": + def from_proto(cls, proto: core_pb2.LinkOptions) -> "LinkOptions": return LinkOptions( - jitter=options.jitter, - key=options.key, - mburst=options.mburst, - mer=options.mer, - loss=options.loss, - bandwidth=options.bandwidth, - burst=options.burst, - delay=options.delay, - dup=options.dup, - unidirectional=options.unidirectional, + jitter=proto.jitter, + key=proto.key, + mburst=proto.mburst, + mer=proto.mer, + loss=proto.loss, + bandwidth=proto.bandwidth, + burst=proto.burst, + delay=proto.delay, + dup=proto.dup, + unidirectional=proto.unidirectional, ) def to_proto(self) -> core_pb2.LinkOptions: @@ -306,26 +386,26 @@ class Link: color: str = None @classmethod - def from_proto(cls, link: core_pb2.Link) -> "Link": + def from_proto(cls, proto: core_pb2.Link) -> "Link": iface1 = None - if link.HasField("iface1"): - iface1 = Interface.from_proto(link.iface1) + if proto.HasField("iface1"): + iface1 = Interface.from_proto(proto.iface1) iface2 = None - if link.HasField("iface2"): - iface2 = Interface.from_proto(link.iface2) + if proto.HasField("iface2"): + iface2 = Interface.from_proto(proto.iface2) options = None - if link.HasField("options"): - options = LinkOptions.from_proto(link.options) + if proto.HasField("options"): + options = LinkOptions.from_proto(proto.options) return Link( - type=LinkType(link.type), - node1_id=link.node1_id, - node2_id=link.node2_id, + type=LinkType(proto.type), + node1_id=proto.node1_id, + node2_id=proto.node2_id, iface1=iface1, iface2=iface2, options=options, - network_id=link.network_id, - label=link.label, - color=link.color, + network_id=proto.network_id, + label=proto.label, + color=proto.color, ) def to_proto(self) -> core_pb2.Link: @@ -360,13 +440,13 @@ class SessionSummary: dir: str @classmethod - def from_proto(cls, summary: core_pb2.SessionSummary) -> "SessionSummary": + def from_proto(cls, proto: core_pb2.SessionSummary) -> "SessionSummary": return SessionSummary( - id=summary.id, - state=SessionState(summary.state), - nodes=summary.nodes, - file=summary.file, - dir=summary.dir, + id=proto.id, + state=SessionState(proto.state), + nodes=proto.nodes, + file=proto.file, + dir=proto.dir, ) @@ -377,8 +457,8 @@ class Hook: data: str @classmethod - def from_proto(cls, hook: core_pb2.Hook) -> "Hook": - return Hook(state=SessionState(hook.state), file=hook.file, data=hook.data) + def from_proto(cls, proto: core_pb2.Hook) -> "Hook": + return Hook(state=SessionState(proto.state), file=proto.file, data=proto.data) def to_proto(self) -> core_pb2.Hook: return core_pb2.Hook(state=self.state.value, file=self.file, data=self.data) @@ -390,8 +470,8 @@ class Position: y: float @classmethod - def from_proto(cls, position: core_pb2.Position) -> "Position": - return Position(x=position.x, y=position.y) + def from_proto(cls, proto: core_pb2.Position) -> "Position": + return Position(x=proto.x, y=proto.y) def to_proto(self) -> core_pb2.Position: return core_pb2.Position(x=self.x, y=self.y) @@ -404,8 +484,8 @@ class Geo: alt: float = None @classmethod - def from_proto(cls, geo: core_pb2.Geo) -> "Geo": - return Geo(lat=geo.lat, lon=geo.lon, alt=geo.alt) + def from_proto(cls, proto: core_pb2.Geo) -> "Geo": + return Geo(lat=proto.lat, lon=proto.lon, alt=proto.alt) def to_proto(self) -> core_pb2.Geo: return core_pb2.Geo(lat=self.lat, lon=self.lon, alt=self.alt) @@ -429,22 +509,22 @@ class Node: channel: str = None @classmethod - def from_proto(cls, node: core_pb2.Node) -> "Node": + def from_proto(cls, proto: core_pb2.Node) -> "Node": return Node( - id=node.id, - name=node.name, - type=NodeType(node.type), - model=node.model, - position=Position.from_proto(node.position), - services=list(node.services), - config_services=list(node.config_services), - emane=node.emane, - icon=node.icon, - image=node.image, - server=node.server, - geo=Geo.from_proto(node.geo), - dir=node.dir, - channel=node.channel, + id=proto.id, + name=proto.name, + type=NodeType(proto.type), + model=proto.model, + position=Position.from_proto(proto.position), + services=list(proto.services), + config_services=list(proto.config_services), + emane=proto.emane, + icon=proto.icon, + image=proto.image, + server=proto.server, + geo=Geo.from_proto(proto.geo), + dir=proto.dir, + channel=proto.channel, ) def to_proto(self) -> core_pb2.Node: @@ -471,10 +551,10 @@ class LinkEvent: link: Link @classmethod - def from_proto(cls, event: core_pb2.LinkEvent) -> "LinkEvent": + def from_proto(cls, proto: core_pb2.LinkEvent) -> "LinkEvent": return LinkEvent( - message_type=MessageType(event.message_type), - link=Link.from_proto(event.link), + message_type=MessageType(proto.message_type), + link=Link.from_proto(proto.link), ) @@ -484,8 +564,8 @@ class NodeEvent: node: Node @classmethod - def from_proto(cls, event: core_pb2.NodeEvent) -> "NodeEvent": + def from_proto(cls, proto: core_pb2.NodeEvent) -> "NodeEvent": return NodeEvent( - message_type=MessageType(event.message_type), - node=Node.from_proto(event.node), + message_type=MessageType(proto.message_type), + node=Node.from_proto(proto.node), ) From 41a3c5fd7fa2d2de040b39f93b721cb80b3da9e2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 26 Jul 2020 11:45:40 -0700 Subject: [PATCH 0548/1131] pygui: added wrapper class for sessions returned by grpc GetSession --- daemon/core/gui/coreclient.py | 9 ++++----- daemon/core/gui/graph/graph.py | 13 +++++++------ daemon/core/gui/wrappers.py | 21 +++++++++++++++++++++ 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index fd1abc34..5ddc28d7 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -48,6 +48,7 @@ from core.gui.wrappers import ( NodeServiceData, NodeType, Position, + Session, SessionLocation, SessionState, ThroughputsEvent, @@ -311,8 +312,8 @@ class CoreClient: # get session data try: response = self.client.get_session(self.session_id) - session = response.session - self.state = SessionState(session.state) + session = Session.from_proto(response.session) + self.state = session.state self.handling_events = self.client.events( self.session_id, self.handle_events ) @@ -349,9 +350,7 @@ class CoreClient: self.ifaces_manager.joined(session.links) # draw session - nodes = [Node.from_proto(x) for x in session.nodes] - links = [Link.from_proto(x) for x in session.links] - self.app.canvas.reset_and_redraw(nodes, links) + self.app.canvas.reset_and_redraw(session) # get mobility configs response = self.client.get_mobility_configs(self.session_id) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index ae0b00c0..81e0d1c6 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -22,7 +22,7 @@ from core.gui.graph.shape import Shape from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker from core.gui.images import ImageEnum, TypeToImage from core.gui.nodeutils import NodeDraw, NodeUtils -from core.gui.wrappers import Interface, Link, LinkType, Node, ThroughputsEvent +from core.gui.wrappers import Interface, Link, LinkType, Node, Session, ThroughputsEvent if TYPE_CHECKING: from core.gui.app import Application @@ -127,7 +127,7 @@ class CanvasGraph(tk.Canvas): ) self.configure(scrollregion=self.bbox(tk.ALL)) - def reset_and_redraw(self, nodes: List[Node], links: List[Link]) -> None: + def reset_and_redraw(self, session: Session) -> None: # reset view options to default state self.show_node_labels.set(True) self.show_link_labels.set(True) @@ -152,7 +152,7 @@ class CanvasGraph(tk.Canvas): self.wireless_edges.clear() self.wireless_network.clear() self.drawing_edge = None - self.draw_session(nodes, links) + self.draw_session(session) def setup_bindings(self) -> None: """ @@ -325,19 +325,20 @@ class CanvasGraph(tk.Canvas): self.nodes[node.id] = node self.core.canvas_nodes[core_node.id] = node - def draw_session(self, nodes: List[Node], links: List[Link]) -> None: + def draw_session(self, session: Session) -> None: """ Draw existing session. """ # draw existing nodes - for core_node in nodes: + for core_node in session.nodes: + logging.debug("drawing node: %s", core_node) # peer to peer node is not drawn on the GUI if NodeUtils.is_ignore_node(core_node.type): continue self.add_core_node(core_node) # draw existing links - for link in links: + for link in session.links: logging.debug("drawing link: %s", link) canvas_node1 = self.core.canvas_nodes[link.node1_id] canvas_node2 = self.core.canvas_nodes[link.node2_id] diff --git a/daemon/core/gui/wrappers.py b/daemon/core/gui/wrappers.py index 5fb12837..612c7646 100644 --- a/daemon/core/gui/wrappers.py +++ b/daemon/core/gui/wrappers.py @@ -545,6 +545,27 @@ class Node: ) +@dataclass +class Session: + id: int + state: SessionState + nodes: List[Node] + links: List[Link] + dir: str + + @classmethod + def from_proto(cls, proto: core_pb2.Session) -> "Session": + nodes = [Node.from_proto(x) for x in proto.nodes] + links = [Link.from_proto(x) for x in proto.links] + return Session( + id=proto.id, + state=SessionState(proto.state), + nodes=nodes, + links=links, + dir=proto.dir, + ) + + @dataclass class LinkEvent: message_type: MessageType From 3bdd6292cdc0161d75be89c07180f0e79d71774f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 27 Jul 2020 18:19:51 -0700 Subject: [PATCH 0549/1131] grpc: update GetSession to return all session related information, rather than needing 8 different calls, pygui: updated session protobuf wrapper to handle all new data --- daemon/core/api/grpc/grpcutils.py | 129 +++++++++++++++++++- daemon/core/api/grpc/server.py | 137 +++++++--------------- daemon/core/emulator/session.py | 2 +- daemon/core/gui/graph/graph.py | 2 +- daemon/core/gui/wrappers.py | 70 ++++++++++- daemon/proto/core/api/grpc/core.proto | 12 ++ daemon/proto/core/api/grpc/emane.proto | 8 +- daemon/proto/core/api/grpc/services.proto | 15 +-- 8 files changed, 260 insertions(+), 115 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 84b8ee6a..a024c064 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -8,13 +8,22 @@ from grpc import ServicerContext from core import utils from core.api.grpc import common_pb2, core_pb2 -from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig +from core.api.grpc.common_pb2 import MappedConfig +from core.api.grpc.configservices_pb2 import ConfigServiceConfig +from core.api.grpc.emane_pb2 import EmaneModelConfig +from core.api.grpc.services_pb2 import ( + NodeServiceConfig, + NodeServiceData, + ServiceConfig, + ServiceDefaults, +) from core.config import ConfigurableOptions from core.emane.nodes import EmaneNet from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions from core.emulator.enumerations import LinkTypes, NodeTypes from core.emulator.session import Session -from core.nodes.base import CoreNode, NodeBase +from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility +from core.nodes.base import CoreNode, CoreNodeBase, NodeBase from core.nodes.interface import CoreInterface from core.services.coreservices import CoreService @@ -536,3 +545,119 @@ def get_nem_id( message = f"{node.name} interface {iface_id} nem id does not exist" context.abort(grpc.StatusCode.INVALID_ARGUMENT, message) return nem_id + + +def get_emane_model_configs(session: Session) -> List[EmaneModelConfig]: + configs = [] + for _id in session.emane.node_configurations: + if _id == -1: + continue + model_configs = session.emane.node_configurations[_id] + for model_name in model_configs: + model = session.emane.models[model_name] + current_config = session.emane.get_model_config(_id, model_name) + config = get_config_options(current_config, model) + node_id, iface_id = parse_emane_model_id(_id) + model_config = EmaneModelConfig( + node_id=node_id, model=model_name, iface_id=iface_id, config=config + ) + configs.append(model_config) + return configs + + +def get_wlan_configs(session: Session) -> Dict[int, MappedConfig]: + configs = {} + for node_id in session.mobility.node_configurations: + model_config = session.mobility.node_configurations[node_id] + if node_id == -1: + continue + for model_name in model_config: + if model_name != BasicRangeModel.name: + continue + current_config = session.mobility.get_model_config(node_id, model_name) + config = get_config_options(current_config, BasicRangeModel) + mapped_config = MappedConfig(config=config) + configs[node_id] = mapped_config + return configs + + +def get_mobility_configs(session: Session) -> Dict[int, MappedConfig]: + configs = {} + for node_id in session.mobility.node_configurations: + model_config = session.mobility.node_configurations[node_id] + if node_id == -1: + continue + for model_name in model_config: + if model_name != Ns2ScriptedMobility.name: + continue + current_config = session.mobility.get_model_config(node_id, model_name) + config = get_config_options(current_config, Ns2ScriptedMobility) + mapped_config = MappedConfig(config=config) + configs[node_id] = mapped_config + return configs + + +def get_hooks(session: Session) -> List[core_pb2.Hook]: + hooks = [] + for state in session.hooks: + state_hooks = session.hooks[state] + for file_name, file_data in state_hooks: + hook = core_pb2.Hook(state=state.value, file=file_name, data=file_data) + hooks.append(hook) + return hooks + + +def get_emane_models(session: Session) -> List[str]: + emane_models = [] + for model in session.emane.models.keys(): + if len(model.split("_")) != 2: + continue + emane_models.append(model) + return emane_models + + +def get_default_services(session: Session) -> List[ServiceDefaults]: + default_services = [] + for name, services in session.services.default_services.items(): + default_service = ServiceDefaults(node_type=name, services=services) + default_services.append(default_service) + return default_services + + +def get_node_service_configs(session: Session) -> List[NodeServiceConfig]: + configs = [] + for node_id, service_configs in session.services.custom_services.items(): + for name in service_configs: + service = session.services.get_service(node_id, name) + service_proto = get_service_configuration(service) + config = NodeServiceConfig( + node_id=node_id, + service=name, + data=service_proto, + files=service.config_data, + ) + configs.append(config) + return configs + + +def get_node_config_service_configs(session: Session) -> List[ConfigServiceConfig]: + configs = [] + for node in session.nodes.values(): + if not isinstance(node, CoreNodeBase): + continue + for name, service in node.config_services.items(): + if not service.custom_templates and not service.custom_config: + continue + config_proto = ConfigServiceConfig( + node_id=node.id, + name=name, + templates=service.custom_templates, + config=service.custom_config, + ) + configs.append(config_proto) + return configs + + +def get_emane_config(session: Session) -> Dict[str, common_pb2.ConfigOption]: + current_config = session.emane.get_configs() + return get_config_options(current_config, session.emane.emane_config) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 38100e05..65029e0a 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -19,7 +19,6 @@ from core.api.grpc import ( core_pb2_grpc, grpcutils, ) -from core.api.grpc.common_pb2 import MappedConfig from core.api.grpc.configservices_pb2 import ( ConfigService, GetConfigServiceDefaultsRequest, @@ -89,7 +88,6 @@ from core.api.grpc.services_pb2 import ( ServiceAction, ServiceActionRequest, ServiceActionResponse, - ServiceDefaults, SetNodeServiceFileRequest, SetNodeServiceFileResponse, SetNodeServiceRequest, @@ -118,7 +116,7 @@ from core.emulator.enumerations import ( from core.emulator.session import NT, Session from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility -from core.nodes.base import CoreNode, CoreNodeBase, NodeBase +from core.nodes.base import CoreNode, NodeBase from core.nodes.network import PtpNet, WlanNode from core.services.coreservices import ServiceManager @@ -558,7 +556,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get session: %s", request) session = self.get_session(request.session_id, context) - links = [] nodes = [] for _id in session.nodes: @@ -568,9 +565,37 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): nodes.append(node_proto) node_links = get_links(node) links.extend(node_links) - + default_services = grpcutils.get_default_services(session) + x, y, z = session.location.refxyz + lat, lon, alt = session.location.refgeo + location = core_pb2.SessionLocation( + x=x, y=y, z=z, lat=lat, lon=lon, alt=alt, scale=session.location.refscale + ) + hooks = grpcutils.get_hooks(session) + emane_models = grpcutils.get_emane_models(session) + emane_config = grpcutils.get_emane_config(session) + emane_model_configs = grpcutils.get_emane_model_configs(session) + wlan_configs = grpcutils.get_wlan_configs(session) + mobility_configs = grpcutils.get_mobility_configs(session) + service_configs = grpcutils.get_node_service_configs(session) + config_service_configs = grpcutils.get_node_config_service_configs(session) session_proto = core_pb2.Session( - state=session.state.value, nodes=nodes, links=links, dir=session.session_dir + state=session.state.value, + nodes=nodes, + links=links, + dir=session.session_dir, + user=session.user, + default_services=default_services, + location=location, + hooks=hooks, + emane_models=emane_models, + emane_config=emane_config, + emane_model_configs=emane_model_configs, + wlan_configs=wlan_configs, + service_configs=service_configs, + config_service_configs=config_service_configs, + mobility_configs=mobility_configs, + metadata=session.metadata, ) return core_pb2.GetSessionResponse(session=session_proto) @@ -1012,12 +1037,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get hooks: %s", request) session = self.get_session(request.session_id, context) - hooks = [] - for state in session.hooks: - state_hooks = session.hooks[state] - for file_name, file_data in state_hooks: - hook = core_pb2.Hook(state=state.value, file=file_name, data=file_data) - hooks.append(hook) + hooks = grpcutils.get_hooks(session) return core_pb2.GetHooksResponse(hooks=hooks) def AddHook( @@ -1050,19 +1070,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get mobility configs: %s", request) session = self.get_session(request.session_id, context) - response = GetMobilityConfigsResponse() - for node_id in session.mobility.node_configurations: - model_config = session.mobility.node_configurations[node_id] - if node_id == -1: - continue - for model_name in model_config: - if model_name != Ns2ScriptedMobility.name: - continue - current_config = session.mobility.get_model_config(node_id, model_name) - config = get_config_options(current_config, Ns2ScriptedMobility) - mapped_config = MappedConfig(config=config) - response.configs[node_id].CopyFrom(mapped_config) - return response + configs = grpcutils.get_mobility_configs(session) + return GetMobilityConfigsResponse(configs=configs) def GetMobilityConfig( self, request: GetMobilityConfigRequest, context: ServicerContext @@ -1157,12 +1166,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get service defaults: %s", request) session = self.get_session(request.session_id, context) - all_service_defaults = [] - for node_type in session.services.default_services: - services = session.services.default_services[node_type] - service_defaults = ServiceDefaults(node_type=node_type, services=services) - all_service_defaults.append(service_defaults) - return GetServiceDefaultsResponse(defaults=all_service_defaults) + defaults = grpcutils.get_default_services(session) + return GetServiceDefaultsResponse(defaults=defaults) def SetServiceDefaults( self, request: SetServiceDefaultsRequest, context: ServicerContext @@ -1196,18 +1201,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get node service configs: %s", request) session = self.get_session(request.session_id, context) - configs = [] - for node_id, service_configs in session.services.custom_services.items(): - for name in service_configs: - service = session.services.get_service(node_id, name) - service_proto = grpcutils.get_service_configuration(service) - config = GetNodeServiceConfigsResponse.ServiceConfig( - node_id=node_id, - service=name, - data=service_proto, - files=service.config_data, - ) - configs.append(config) + configs = grpcutils.get_node_service_configs(session) return GetNodeServiceConfigsResponse(configs=configs) def GetNodeService( @@ -1337,19 +1331,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get wlan configs: %s", request) session = self.get_session(request.session_id, context) - response = GetWlanConfigsResponse() - for node_id in session.mobility.node_configurations: - model_config = session.mobility.node_configurations[node_id] - if node_id == -1: - continue - for model_name in model_config: - if model_name != BasicRangeModel.name: - continue - current_config = session.mobility.get_model_config(node_id, model_name) - config = get_config_options(current_config, BasicRangeModel) - mapped_config = MappedConfig(config=config) - response.configs[node_id].CopyFrom(mapped_config) - return response + configs = grpcutils.get_wlan_configs(session) + return GetWlanConfigsResponse(configs=configs) def GetWlanConfig( self, request: GetWlanConfigRequest, context: ServicerContext @@ -1401,8 +1384,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get emane config: %s", request) session = self.get_session(request.session_id, context) - current_config = session.emane.get_configs() - config = get_config_options(current_config, session.emane.emane_config) + config = grpcutils.get_emane_config(session) return GetEmaneConfigResponse(config=config) def SetEmaneConfig( @@ -1433,11 +1415,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get emane models: %s", request) session = self.get_session(request.session_id, context) - models = [] - for model in session.emane.models.keys(): - if len(model.split("_")) != 2: - continue - models.append(model) + models = grpcutils.get_emane_models(session) return GetEmaneModelsResponse(models=models) def GetEmaneModelConfig( @@ -1491,22 +1469,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get emane model configs: %s", request) session = self.get_session(request.session_id, context) - - configs = [] - for _id in session.emane.node_configurations: - if _id == -1: - continue - - model_configs = session.emane.node_configurations[_id] - for model_name in model_configs: - model = session.emane.models[model_name] - current_config = session.emane.get_model_config(_id, model_name) - config = get_config_options(current_config, model) - node_id, iface_id = grpcutils.parse_emane_model_id(_id) - model_config = GetEmaneModelConfigsResponse.ModelConfig( - node_id=node_id, model=model_name, iface_id=iface_id, config=config - ) - configs.append(model_config) + configs = grpcutils.get_emane_model_configs(session) return GetEmaneModelConfigsResponse(configs=configs) def SaveXml( @@ -1713,21 +1676,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :return: get node config service configs response """ session = self.get_session(request.session_id, context) - configs = [] - for node in session.nodes.values(): - if not isinstance(node, CoreNodeBase): - continue - - for name, service in node.config_services.items(): - if not service.custom_templates and not service.custom_config: - continue - config_proto = configservices_pb2.ConfigServiceConfig( - node_id=node.id, - name=name, - templates=service.custom_templates, - config=service.custom_config, - ) - configs.append(config_proto) + configs = grpcutils.get_node_config_service_configs(session) return GetNodeConfigServiceConfigsResponse(configs=configs) def GetNodeConfigServices( diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index cad6ae3c..4127b141 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -119,7 +119,7 @@ class Session: # states and hooks handlers self.state: EventTypes = EventTypes.DEFINITION_STATE self.state_time: float = time.monotonic() - self.hooks: Dict[EventTypes, Tuple[str, str]] = {} + self.hooks: Dict[EventTypes, List[Tuple[str, str]]] = {} self.state_hooks: Dict[EventTypes, List[Callable[[EventTypes], None]]] = {} self.add_state_hook( state=EventTypes.RUNTIME_STATE, hook=self.runtime_state_hook diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 81e0d1c6..f2a27444 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -330,7 +330,7 @@ class CanvasGraph(tk.Canvas): Draw existing session. """ # draw existing nodes - for core_node in session.nodes: + for core_node in session.nodes.values(): logging.debug("drawing node: %s", core_node) # peer to peer node is not drawn on the GUI if NodeUtils.is_ignore_node(core_node.type): diff --git a/daemon/core/gui/wrappers.py b/daemon/core/gui/wrappers.py index 612c7646..835a9d17 100644 --- a/daemon/core/gui/wrappers.py +++ b/daemon/core/gui/wrappers.py @@ -1,6 +1,6 @@ from dataclasses import dataclass, field from enum import Enum -from typing import Dict, List +from typing import Dict, List, Optional, Tuple from core.api.grpc import common_pb2, configservices_pb2, core_pb2, services_pb2 @@ -119,6 +119,12 @@ class ConfigService: ) +@dataclass +class ConfigServiceData: + templates: Dict[str, str] + config: Dict[str, str] + + @dataclass class NodeServiceData: executables: List[str] @@ -508,6 +514,22 @@ class Node: dir: str = None channel: str = None + # configurations + emane_model_configs: Dict[ + Tuple[str, Optional[int]], Dict[str, ConfigOption] + ] = field(default_factory=dict, repr=False) + wlan_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False) + mobility_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False) + service_configs: Dict[str, NodeServiceData] = field( + default_factory=dict, repr=False + ) + service_file_configs: Dict[str, Dict[str, str]] = field( + default_factory=dict, repr=False + ) + config_service_configs: Dict[str, ConfigServiceData] = field( + default_factory=dict, repr=False + ) + @classmethod def from_proto(cls, proto: core_pb2.Node) -> "Node": return Node( @@ -549,20 +571,62 @@ class Node: class Session: id: int state: SessionState - nodes: List[Node] + nodes: Dict[int, Node] links: List[Link] dir: str + user: str + default_services: Dict[str, List[str]] + location: SessionLocation + hooks: List[Hook] + emane_models: List[str] + emane_config: Dict[str, ConfigOption] + metadata: Dict[str, str] @classmethod def from_proto(cls, proto: core_pb2.Session) -> "Session": - nodes = [Node.from_proto(x) for x in proto.nodes] + nodes: Dict[int, Node] = {x.id: Node.from_proto(x) for x in proto.nodes} links = [Link.from_proto(x) for x in proto.links] + default_services = {x.node_type: x.services for x in proto.default_services} + hooks = [Hook.from_proto(x) for x in proto.hooks] + # update nodes with their current configurations + for model in proto.emane_model_configs: + iface_id = None + if model.iface_id != -1: + iface_id = model.iface_id + node = nodes[model.node_id] + key = (model.model, iface_id) + node.emane_model_configs[key] = ConfigOption.from_dict(model.config) + for node_id, mapped_config in proto.wlan_configs.items(): + node = nodes[node_id] + node.wlan_config = ConfigOption.from_dict(mapped_config.config) + for config in proto.service_configs: + service = config.service + node = nodes[config.node_id] + node.service_configs[service] = NodeServiceData.from_proto(config.data) + for file, data in config.files.items(): + files = node.service_file_configs.setdefault(service, {}) + files[file] = data + for config in proto.config_service_configs: + node = nodes[config.node_id] + node.config_service_configs[config.name] = ConfigServiceData( + templates=dict(config.templates), config=dict(config.config) + ) + for node_id, mapped_config in proto.mobility_configs.items(): + node = nodes[node_id] + node.mobility_config = ConfigOption.from_dict(mapped_config.config) return Session( id=proto.id, state=SessionState(proto.state), nodes=nodes, links=links, dir=proto.dir, + user=proto.user, + default_services=default_services, + location=SessionLocation.from_proto(proto.location), + hooks=hooks, + emane_models=list(proto.emane_models), + emane_config=ConfigOption.from_dict(proto.emane_config), + metadata=dict(proto.metadata), ) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 9214ad1b..1b20257c 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -714,6 +714,18 @@ message Session { repeated Node nodes = 3; repeated Link links = 4; string dir = 5; + string user = 6; + repeated services.ServiceDefaults default_services = 7; + SessionLocation location = 8; + repeated Hook hooks = 9; + repeated string emane_models = 10; + map emane_config = 11; + repeated emane.EmaneModelConfig emane_model_configs = 12; + map wlan_configs = 13; + repeated services.NodeServiceConfig service_configs = 14; + repeated configservices.ConfigServiceConfig config_service_configs = 15; + map mobility_configs = 16; + map metadata = 17; } message SessionSummary { diff --git a/daemon/proto/core/api/grpc/emane.proto b/daemon/proto/core/api/grpc/emane.proto index ac5456fd..ce9a4297 100644 --- a/daemon/proto/core/api/grpc/emane.proto +++ b/daemon/proto/core/api/grpc/emane.proto @@ -54,13 +54,7 @@ message GetEmaneModelConfigsRequest { } message GetEmaneModelConfigsResponse { - message ModelConfig { - int32 node_id = 1; - string model = 2; - int32 iface_id = 3; - map config = 4; - } - repeated ModelConfig configs = 1; + repeated EmaneModelConfig configs = 1; } message GetEmaneEventChannelRequest { diff --git a/daemon/proto/core/api/grpc/services.proto b/daemon/proto/core/api/grpc/services.proto index 7e8498a7..cf6d9cbf 100644 --- a/daemon/proto/core/api/grpc/services.proto +++ b/daemon/proto/core/api/grpc/services.proto @@ -59,6 +59,13 @@ message NodeServiceData { string meta = 10; } +message NodeServiceConfig { + int32 node_id = 1; + string service = 2; + NodeServiceData data = 3; + map files = 4; +} + message GetServicesRequest { } @@ -89,13 +96,7 @@ message GetNodeServiceConfigsRequest { } message GetNodeServiceConfigsResponse { - message ServiceConfig { - int32 node_id = 1; - string service = 2; - NodeServiceData data = 3; - map files = 4; - } - repeated ServiceConfig configs = 1; + repeated NodeServiceConfig configs = 1; } message GetNodeServiceRequest { From 588afaad13c8996c790f91bd743bf21cc06b1de6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 28 Jul 2020 00:03:15 -0700 Subject: [PATCH 0550/1131] pygui: changes to make use of wrapped session object and wrapped nodes to maintain and retrieving configurations information --- daemon/core/api/grpc/server.py | 1 + daemon/core/gui/coreclient.py | 379 ++++++------------ daemon/core/gui/dialogs/canvassizeandscale.py | 4 +- .../core/gui/dialogs/configserviceconfig.py | 59 ++- daemon/core/gui/dialogs/copyserviceconfig.py | 21 +- daemon/core/gui/dialogs/emaneconfig.py | 30 +- daemon/core/gui/dialogs/hooks.py | 15 +- daemon/core/gui/dialogs/linkconfig.py | 2 +- daemon/core/gui/dialogs/mobilityconfig.py | 12 +- daemon/core/gui/dialogs/mobilityplayer.py | 45 +-- daemon/core/gui/dialogs/nodeconfigservice.py | 24 +- daemon/core/gui/dialogs/nodeservice.py | 23 +- daemon/core/gui/dialogs/runtool.py | 2 +- daemon/core/gui/dialogs/serviceconfig.py | 42 +- daemon/core/gui/dialogs/sessionoptions.py | 4 +- daemon/core/gui/dialogs/sessions.py | 2 +- daemon/core/gui/dialogs/wlanconfig.py | 6 +- daemon/core/gui/graph/graph.py | 21 +- daemon/core/gui/graph/node.py | 21 +- daemon/core/gui/task.py | 2 - daemon/core/gui/wrappers.py | 24 +- 21 files changed, 284 insertions(+), 455 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 65029e0a..cd9cf714 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -580,6 +580,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): service_configs = grpcutils.get_node_service_configs(session) config_service_configs = grpcutils.get_node_config_service_configs(session) session_proto = core_pb2.Session( + id=session.id, state=session.state.value, nodes=nodes, links=links, diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 5ddc28d7..36adf189 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -37,7 +37,6 @@ from core.gui.wrappers import ( ConfigOption, ConfigService, ExceptionEvent, - Hook, Interface, Link, LinkEvent, @@ -61,6 +60,10 @@ GUI_SOURCE = "gui" CPU_USAGE_DELAY = 3 +def to_dict(config: Dict[str, ConfigOption]) -> Dict[str, str]: + return {x: y.value for x, y in config.items()} + + class CoreClient: def __init__(self, app: "Application", proxy: bool) -> None: """ @@ -69,14 +72,13 @@ class CoreClient: self.app: "Application" = app self.master: tk.Tk = app.master self._client: client.CoreGrpcClient = client.CoreGrpcClient(proxy=proxy) - self.session_id: Optional[int] = None + self.session: Optional[Session] = None + self.user = getpass.getuser() + + # global service settings self.services: Dict[str, Set[str]] = {} self.config_services_groups: Dict[str, Set[str]] = {} self.config_services: Dict[str, ConfigService] = {} - self.default_services: Dict[NodeType, Set[str]] = {} - self.emane_models: List[str] = [] - self.observer: Optional[str] = None - self.user = getpass.getuser() # loaded configuration data self.servers: Dict[str, CoreServer] = {} @@ -87,15 +89,12 @@ class CoreClient: # helpers self.iface_to_edge: Dict[Tuple[int, ...], Tuple[int, ...]] = {} self.ifaces_manager: InterfaceManager = InterfaceManager(self.app) + self.observer: Optional[str] = None # session data - self.state: Optional[SessionState] = None - self.canvas_nodes: Dict[int, CanvasNode] = {} - self.location: Optional[SessionLocation] = None - self.links: Dict[Tuple[int, int], CanvasEdge] = {} - self.hooks: Dict[str, Hook] = {} - self.emane_config: Dict[str, ConfigOption] = {} self.mobility_players: Dict[int, MobilityPlayer] = {} + self.canvas_nodes: Dict[int, CanvasNode] = {} + self.links: Dict[Tuple[int, int], CanvasEdge] = {} self.handling_throughputs: Optional[grpc.Future] = None self.handling_cpu_usage: Optional[grpc.Future] = None self.handling_events: Optional[grpc.Future] = None @@ -104,15 +103,15 @@ class CoreClient: @property def client(self) -> client.CoreGrpcClient: - if self.session_id: - response = self._client.check_session(self.session_id) + if self.session: + response = self._client.check_session(self.session.id) if not response.result: throughputs_enabled = self.handling_throughputs is not None self.cancel_throughputs() self.cancel_events() - self._client.create_session(self.session_id) + self._client.create_session(self.session.id) self.handling_events = self._client.events( - self.session_id, self.handle_events + self.session.id, self.handle_events ) if throughputs_enabled: self.enable_throughputs() @@ -126,8 +125,6 @@ class CoreClient: # session data self.canvas_nodes.clear() self.links.clear() - self.hooks.clear() - self.emane_config = None self.close_mobility_players() self.mobility_players.clear() # clear streams @@ -145,12 +142,10 @@ class CoreClient: # read distributed servers for server in self.app.guiconfig.servers: self.servers[server.name] = server - # read custom nodes for custom_node in self.app.guiconfig.nodes: node_draw = NodeDraw.from_custom(custom_node) self.custom_nodes[custom_node.name] = node_draw - # read observers for observer in self.app.guiconfig.observers: self.custom_observers[observer.name] = observer @@ -158,11 +153,11 @@ class CoreClient: def handle_events(self, event: core_pb2.Event) -> None: if event.source == GUI_SOURCE: return - if event.session_id != self.session_id: + if event.session_id != self.session.id: logging.warning( "ignoring event session(%s) current(%s)", event.session_id, - self.session_id, + self.session.id, ) return @@ -173,7 +168,7 @@ class CoreClient: logging.info("session event: %s", event) session_event = event.session_event if session_event.event <= SessionState.SHUTDOWN.value: - self.state = SessionState(session_event.event) + self.session.state = SessionState(session_event.event) elif session_event.event in {7, 8, 9}: node_id = session_event.node_id dialog = self.mobility_players.get(node_id) @@ -253,7 +248,7 @@ class CoreClient: def enable_throughputs(self) -> None: self.handling_throughputs = self.client.throughputs( - self.session_id, self.handle_throughputs + self.session.id, self.handle_throughputs ) def cancel_throughputs(self) -> None: @@ -283,11 +278,11 @@ class CoreClient: def handle_throughputs(self, event: core_pb2.ThroughputsEvent) -> None: event = ThroughputsEvent.from_proto(event) - if event.session_id != self.session_id: + if event.session_id != self.session.id: logging.warning( "ignoring throughput event session(%s) current(%s)", event.session_id, - self.session_id, + self.session.id, ) return logging.debug("handling throughputs event: %s", event) @@ -300,126 +295,33 @@ class CoreClient: logging.info("exception event: %s", event) self.app.statusbar.add_alert(event) - def join_session(self, session_id: int, query_location: bool = True) -> None: - logging.info("join session(%s)", session_id) - # update session and title - self.session_id = session_id - self.master.title(f"CORE Session({self.session_id})") - - # clear session data + def join_session(self, session_id: int) -> None: + logging.info("joining session(%s)", session_id) self.reset() - - # get session data try: - response = self.client.get_session(self.session_id) - session = Session.from_proto(response.session) - self.state = session.state + response = self.client.get_session(session_id) + self.session = Session.from_proto(response.session) + self.client.set_session_user(self.session.id, self.user) + self.master.title(f"CORE Session({self.session.id})") self.handling_events = self.client.events( - self.session_id, self.handle_events + self.session.id, self.handle_events ) - - # set session user - self.client.set_session_user(self.session_id, self.user) - - # get session service defaults - response = self.client.get_service_defaults(self.session_id) - self.default_services = { - x.node_type: set(x.services) for x in response.defaults - } - - # get location - if query_location: - response = self.client.get_session_location(self.session_id) - self.location = SessionLocation.from_proto(response.location) - - # get emane models - response = self.client.get_emane_models(self.session_id) - self.emane_models = response.models - - # get hooks - response = self.client.get_hooks(self.session_id) - for hook_proto in response.hooks: - hook = Hook.from_proto(hook_proto) - self.hooks[hook.file] = hook - - # get emane config - response = self.client.get_emane_config(self.session_id) - self.emane_config = ConfigOption.from_dict(response.config) - - # update interface manager - self.ifaces_manager.joined(session.links) - - # draw session - self.app.canvas.reset_and_redraw(session) - - # get mobility configs - response = self.client.get_mobility_configs(self.session_id) - for node_id in response.configs: - config = response.configs[node_id].config - canvas_node = self.canvas_nodes[node_id] - canvas_node.mobility_config = ConfigOption.from_dict(config) - - # get emane model config - response = self.client.get_emane_model_configs(self.session_id) - for config in response.configs: - iface_id = None - if config.iface_id != -1: - iface_id = config.iface_id - canvas_node = self.canvas_nodes[config.node_id] - canvas_node.emane_model_configs[ - (config.model, iface_id) - ] = ConfigOption.from_dict(config.config) - - # get wlan configurations - response = self.client.get_wlan_configs(self.session_id) - for _id in response.configs: - mapped_config = response.configs[_id] - canvas_node = self.canvas_nodes[_id] - canvas_node.wlan_config = ConfigOption.from_dict(mapped_config.config) - - # get service configurations - response = self.client.get_node_service_configs(self.session_id) - for config in response.configs: - canvas_node = self.canvas_nodes[config.node_id] - canvas_node.service_configs[config.service] = config.data - logging.debug("service file configs: %s", config.files) - for file_name in config.files: - data = config.files[file_name] - files = canvas_node.service_file_configs.setdefault( - config.service, {} - ) - files[file_name] = data - - # get config service configurations - response = self.client.get_node_config_service_configs(self.session_id) - for config in response.configs: - canvas_node = self.canvas_nodes[config.node_id] - service_config = canvas_node.config_service_configs.setdefault( - config.name, {} - ) - if config.templates: - service_config["templates"] = config.templates - if config.config: - service_config["config"] = config.config - - # get metadata - response = self.client.get_session_metadata(self.session_id) - self.parse_metadata(response.config) + self.ifaces_manager.joined(self.session.links) + self.app.canvas.reset_and_redraw(self.session) + self.parse_metadata() + self.app.canvas.organize() + if self.is_runtime(): + self.show_mobility_players() + self.app.after(0, self.app.joined_session_update) except grpc.RpcError as e: self.app.show_grpc_exception("Join Session Error", e) - # organize canvas - self.app.canvas.organize() - if self.is_runtime(): - self.show_mobility_players() - # update ui to represent current state - self.app.after(0, self.app.joined_session_update) - def is_runtime(self) -> bool: - return self.state == SessionState.RUNTIME + return self.session and self.session.state == SessionState.RUNTIME - def parse_metadata(self, config: Dict[str, str]) -> None: + def parse_metadata(self) -> None: # canvas setting + config = self.session.metadata canvas_config = config.get("canvas") logging.debug("canvas metadata: %s", canvas_config) if canvas_config: @@ -447,7 +349,7 @@ class CoreClient: if shapes_config: shapes_config = json.loads(shapes_config) for shape_config in shapes_config: - logging.info("loading shape: %s", shape_config) + logging.debug("loading shape: %s", shape_config) shape_type = shape_config["type"] try: shape_type = ShapeType(shape_type) @@ -478,8 +380,9 @@ class CoreClient: try: response = self.client.create_session() logging.info("created session: %s", response) + self.join_session(response.session_id) location_config = self.app.guiconfig.location - self.location = SessionLocation( + self.session.location = SessionLocation( x=location_config.x, y=location_config.y, z=location_config.z, @@ -488,13 +391,12 @@ class CoreClient: alt=location_config.alt, scale=location_config.scale, ) - self.join_session(response.session_id, query_location=False) except grpc.RpcError as e: self.app.show_grpc_exception("New Session Error", e) def delete_session(self, session_id: int = None) -> None: if session_id is None: - session_id = self.session_id + session_id = self.session.id try: response = self.client.delete_session(session_id) logging.info("deleted session(%s), Result: %s", session_id, response) @@ -507,13 +409,11 @@ class CoreClient: """ try: self.client.connect() - - # get service information + # get all available services response = self.client.get_services() for service in response.services: group_services = self.services.setdefault(service.group, set()) group_services.add(service.name) - # get config service informations response = self.client.get_config_services() for service in response.services: @@ -522,7 +422,6 @@ class CoreClient: service.group, set() ) group_services.add(service.name) - # join provided session, create new session, or show dialog to select an # existing session response = self.client.get_sessions() @@ -553,14 +452,14 @@ class CoreClient: try: position = core_node.position.to_proto() self.client.edit_node( - self.session_id, core_node.id, position, source=GUI_SOURCE + self.session.id, core_node.id, position, source=GUI_SOURCE ) except grpc.RpcError as e: self.app.show_grpc_exception("Edit Node Error", e) def send_servers(self) -> None: for server in self.servers.values(): - self.client.add_session_server(self.session_id, server.name, server.address) + self.client.add_session_server(self.session.id, server.name, server.address) def start_session(self) -> Tuple[bool, List[str]]: self.ifaces_manager.reset_mac() @@ -576,26 +475,23 @@ class CoreClient: wlan_configs = self.get_wlan_configs_proto() mobility_configs = self.get_mobility_configs_proto() emane_model_configs = self.get_emane_model_configs_proto() - hooks = [x.to_proto() for x in self.hooks.values()] + hooks = [x.to_proto() for x in self.session.hooks.values()] service_configs = self.get_service_configs_proto() file_configs = self.get_service_file_configs_proto() asymmetric_links = [ x.asymmetric_link for x in self.links.values() if x.asymmetric_link ] config_service_configs = self.get_config_service_configs_proto() - if self.emane_config: - emane_config = {x: self.emane_config[x].value for x in self.emane_config} - else: - emane_config = None + emane_config = to_dict(self.session.emane_config) result = False exceptions = [] try: self.send_servers() response = self.client.start_session( - self.session_id, + self.session.id, nodes, links, - self.location.to_proto(), + self.session.location.to_proto(), hooks, emane_config, emane_model_configs, @@ -607,7 +503,7 @@ class CoreClient: config_service_configs, ) logging.info( - "start session(%s), result: %s", self.session_id, response.result + "start session(%s), result: %s", self.session.id, response.result ) if response.result: self.set_metadata() @@ -619,7 +515,7 @@ class CoreClient: def stop_session(self, session_id: int = None) -> bool: if not session_id: - session_id = self.session_id + session_id = self.session.id result = False try: response = self.client.stop_session(session_id) @@ -630,15 +526,12 @@ class CoreClient: return result def show_mobility_players(self) -> None: - for canvas_node in self.canvas_nodes.values(): - if canvas_node.core_node.type != NodeType.WIRELESS_LAN: + for node in self.session.nodes.values(): + if node.type != NodeType.WIRELESS_LAN: continue - if canvas_node.mobility_config: - mobility_player = MobilityPlayer( - self.app, canvas_node, canvas_node.mobility_config - ) - node_id = canvas_node.core_node.id - self.mobility_players[node_id] = mobility_player + if node.mobility_config: + mobility_player = MobilityPlayer(self.app, node) + self.mobility_players[node.id] = mobility_player mobility_player.show() def set_metadata(self) -> None: @@ -662,8 +555,8 @@ class CoreClient: shapes = json.dumps(shapes) metadata = {"canvas": canvas_config, "shapes": shapes} - response = self.client.set_session_metadata(self.session_id, metadata) - logging.info("set session metadata %s, result: %s", metadata, response) + response = self.client.set_session_metadata(self.session.id, metadata) + logging.debug("set session metadata %s, result: %s", metadata, response) def launch_terminal(self, node_id: int) -> None: try: @@ -675,7 +568,7 @@ class CoreClient: parent=self.app, ) return - response = self.client.get_node_terminal(self.session_id, node_id) + response = self.client.get_node_terminal(self.session.id, node_id) cmd = f"{terminal} {response.terminal} &" logging.info("launching terminal %s", cmd) os.system(cmd) @@ -687,10 +580,10 @@ class CoreClient: Save core session as to an xml file """ try: - if self.state != SessionState.RUNTIME: + if not self.is_runtime(): logging.debug("Send session data to the daemon") self.send_data() - response = self.client.save_xml(self.session_id, file_path) + response = self.client.save_xml(self.session.id, file_path) logging.info("saved xml file %s, result: %s", file_path, response) except grpc.RpcError as e: self.app.show_grpc_exception("Save XML Error", e) @@ -707,7 +600,7 @@ class CoreClient: self.app.show_grpc_exception("Open XML Error", e) def get_node_service(self, node_id: int, service_name: str) -> NodeServiceData: - response = self.client.get_node_service(self.session_id, node_id, service_name) + response = self.client.get_node_service(self.session.id, node_id, service_name) logging.debug( "get node(%s) %s service, response: %s", node_id, service_name, response ) @@ -724,7 +617,7 @@ class CoreClient: shutdowns: List[str], ) -> NodeServiceData: response = self.client.set_node_service( - self.session_id, + self.session.id, node_id, service_name, directories=dirs, @@ -744,14 +637,14 @@ class CoreClient: shutdowns, response, ) - response = self.client.get_node_service(self.session_id, node_id, service_name) + response = self.client.get_node_service(self.session.id, node_id, service_name) return NodeServiceData.from_proto(response.service) def get_node_service_file( self, node_id: int, service_name: str, file_name: str ) -> str: response = self.client.get_node_service_file( - self.session_id, node_id, service_name, file_name + self.session.id, node_id, service_name, file_name ) logging.debug( "get service file for node(%s), service: %s, file: %s, result: %s", @@ -766,7 +659,7 @@ class CoreClient: self, node_id: int, service_name: str, file_name: str, data: str ) -> None: response = self.client.set_node_service_file( - self.session_id, node_id, service_name, file_name, data + self.session.id, node_id, service_name, file_name, data ) logging.info( "set node(%s) service file, service: %s, file: %s, data: %s, result: %s", @@ -783,13 +676,13 @@ class CoreClient: """ node_protos = [x.core_node.to_proto() for x in self.canvas_nodes.values()] link_protos = [x.link.to_proto() for x in self.links.values()] - self.client.set_session_state(self.session_id, SessionState.DEFINITION.value) + self.client.set_session_state(self.session.id, SessionState.DEFINITION.value) for node_proto in node_protos: - response = self.client.add_node(self.session_id, node_proto) + response = self.client.add_node(self.session.id, node_proto) logging.debug("create node: %s", response) for link_proto in link_protos: response = self.client.add_link( - self.session_id, + self.session.id, link_proto.node1_id, link_proto.node2_id, link_proto.iface1, @@ -806,15 +699,15 @@ class CoreClient: self.create_nodes_and_links() for config_proto in self.get_wlan_configs_proto(): self.client.set_wlan_config( - self.session_id, config_proto.node_id, config_proto.config + self.session.id, config_proto.node_id, config_proto.config ) for config_proto in self.get_mobility_configs_proto(): self.client.set_mobility_config( - self.session_id, config_proto.node_id, config_proto.config + self.session.id, config_proto.node_id, config_proto.config ) for config_proto in self.get_service_configs_proto(): self.client.set_node_service( - self.session_id, + self.session.id, config_proto.node_id, config_proto.service, startup=config_proto.startup, @@ -823,38 +716,37 @@ class CoreClient: ) for config_proto in self.get_service_file_configs_proto(): self.client.set_node_service_file( - self.session_id, + self.session.id, config_proto.node_id, config_proto.service, config_proto.file, config_proto.data, ) - for hook in self.hooks.values(): + for hook in self.session.hooks.values(): self.client.add_hook( - self.session_id, hook.state.value, hook.file, hook.data + self.session.id, hook.state.value, hook.file, hook.data ) for config_proto in self.get_emane_model_configs_proto(): self.client.set_emane_model_config( - self.session_id, + self.session.id, config_proto.node_id, config_proto.model, config_proto.config, config_proto.iface_id, ) - if self.emane_config: - config = {x: self.emane_config[x].value for x in self.emane_config} - self.client.set_emane_config(self.session_id, config) - if self.location: - self.client.set_session_location( - self.session_id, - self.location.x, - self.location.y, - self.location.z, - self.location.lat, - self.location.lon, - self.location.alt, - self.location.scale, - ) + config = to_dict(self.session.emane_config) + self.client.set_emane_config(self.session.id, config) + location = self.session.location + self.client.set_session_location( + self.session.id, + location.x, + location.y, + location.z, + location.lat, + location.lon, + location.alt, + location.scale, + ) self.set_metadata() def close(self) -> None: @@ -888,16 +780,16 @@ class CoreClient: image = "ubuntu:latest" emane = None if node_type == NodeType.EMANE: - if not self.emane_models: + if not self.session.emane_models: dialog = EmaneInstallDialog(self.app) dialog.show() return - emane = self.emane_models[0] - name = f"EMANE{node_id}" + emane = self.session.emane_models[0] + name = f"emane{node_id}" elif node_type == NodeType.WIRELESS_LAN: - name = f"WLAN{node_id}" + name = f"wlan{node_id}" elif node_type in [NodeType.RJ45, NodeType.TUNNEL]: - name = "UNASSIGNED" + name = "unassigned" else: name = f"n{node_id}" node = Node( @@ -914,13 +806,13 @@ class CoreClient: node.services[:] = services # assign default services to CORE node else: - services = self.default_services.get(model) + services = self.session.default_services.get(model) if services: - node.services[:] = services + node.services = services.copy() logging.info( "add node(%s) to session(%s), coordinates(%s, %s)", node.name, - self.session_id, + self.session.id, x, y, ) @@ -1005,60 +897,56 @@ class CoreClient: def get_wlan_configs_proto(self) -> List[wlan_pb2.WlanConfig]: configs = [] - for canvas_node in self.canvas_nodes.values(): - if canvas_node.core_node.type != NodeType.WIRELESS_LAN: + for node in self.session.nodes.values(): + if node.type != NodeType.WIRELESS_LAN: continue - if not canvas_node.wlan_config: + if not node.wlan_config: continue - config = ConfigOption.to_dict(canvas_node.wlan_config) - node_id = canvas_node.core_node.id - wlan_config = wlan_pb2.WlanConfig(node_id=node_id, config=config) + config = ConfigOption.to_dict(node.wlan_config) + wlan_config = wlan_pb2.WlanConfig(node_id=node.id, config=config) configs.append(wlan_config) return configs def get_mobility_configs_proto(self) -> List[mobility_pb2.MobilityConfig]: configs = [] - for canvas_node in self.canvas_nodes.values(): - if canvas_node.core_node.type != NodeType.WIRELESS_LAN: + for node in self.session.nodes.values(): + if node.type != NodeType.WIRELESS_LAN: continue - if not canvas_node.mobility_config: + if not node.mobility_config: continue - config = ConfigOption.to_dict(canvas_node.mobility_config) - node_id = canvas_node.core_node.id + config = ConfigOption.to_dict(node.mobility_config) mobility_config = mobility_pb2.MobilityConfig( - node_id=node_id, config=config + node_id=node.id, config=config ) configs.append(mobility_config) return configs def get_emane_model_configs_proto(self) -> List[emane_pb2.EmaneModelConfig]: configs = [] - for canvas_node in self.canvas_nodes.values(): - if canvas_node.core_node.type != NodeType.EMANE: + for node in self.session.nodes.values(): + if node.type != NodeType.EMANE: continue - node_id = canvas_node.core_node.id - for key, config in canvas_node.emane_model_configs.items(): + for key, config in node.emane_model_configs.items(): model, iface_id = key config = ConfigOption.to_dict(config) if iface_id is None: iface_id = -1 config_proto = emane_pb2.EmaneModelConfig( - node_id=node_id, iface_id=iface_id, model=model, config=config + node_id=node.id, iface_id=iface_id, model=model, config=config ) configs.append(config_proto) return configs def get_service_configs_proto(self) -> List[services_pb2.ServiceConfig]: configs = [] - for canvas_node in self.canvas_nodes.values(): - if not NodeUtils.is_container_node(canvas_node.core_node.type): + for node in self.session.nodes.values(): + if not NodeUtils.is_container_node(node.type): continue - if not canvas_node.service_configs: + if not node.service_configs: continue - node_id = canvas_node.core_node.id - for name, config in canvas_node.service_configs.items(): + for name, config in node.service_configs.items(): config_proto = services_pb2.ServiceConfig( - node_id=node_id, + node_id=node.id, service=name, directories=config.dirs, files=config.configs, @@ -1071,16 +959,15 @@ class CoreClient: def get_service_file_configs_proto(self) -> List[services_pb2.ServiceFileConfig]: configs = [] - for canvas_node in self.canvas_nodes.values(): - if not NodeUtils.is_container_node(canvas_node.core_node.type): + for node in self.session.nodes.values(): + if not NodeUtils.is_container_node(node.type): continue - if not canvas_node.service_file_configs: + if not node.service_file_configs: continue - node_id = canvas_node.core_node.id - for service, file_configs in canvas_node.service_file_configs.items(): + for service, file_configs in node.service_file_configs.items(): for file, data in file_configs.items(): config_proto = services_pb2.ServiceFileConfig( - node_id=node_id, service=service, file=file, data=data + node_id=node.id, service=service, file=file, data=data ) configs.append(config_proto) return configs @@ -1089,29 +976,27 @@ class CoreClient: self ) -> List[configservices_pb2.ConfigServiceConfig]: config_service_protos = [] - for canvas_node in self.canvas_nodes.values(): - if not NodeUtils.is_container_node(canvas_node.core_node.type): + for node in self.session.nodes.values(): + if not NodeUtils.is_container_node(node.type): continue - if not canvas_node.config_service_configs: + if not node.config_service_configs: continue - node_id = canvas_node.core_node.id - for name, service_config in canvas_node.config_service_configs.items(): - config = service_config.get("config", {}) + for name, service_config in node.config_service_configs.items(): config_proto = configservices_pb2.ConfigServiceConfig( - node_id=node_id, + node_id=node.id, name=name, - templates=service_config["templates"], - config=config, + templates=service_config.templates, + config=service_config.config, ) config_service_protos.append(config_proto) return config_service_protos def run(self, node_id: int) -> str: logging.info("running node(%s) cmd: %s", node_id, self.observer) - return self.client.node_command(self.session_id, node_id, self.observer).output + return self.client.node_command(self.session.id, node_id, self.observer).output def get_wlan_config(self, node_id: int) -> Dict[str, ConfigOption]: - response = self.client.get_wlan_config(self.session_id, node_id) + response = self.client.get_wlan_config(self.session.id, node_id) config = response.config logging.debug( "get wlan configuration from node %s, result configuration: %s", @@ -1121,7 +1006,7 @@ class CoreClient: return ConfigOption.from_dict(config) def get_mobility_config(self, node_id: int) -> Dict[str, ConfigOption]: - response = self.client.get_mobility_config(self.session_id, node_id) + response = self.client.get_mobility_config(self.session.id, node_id) config = response.config logging.debug( "get mobility config from node %s, result configuration: %s", @@ -1136,7 +1021,7 @@ class CoreClient: if iface_id is None: iface_id = -1 response = self.client.get_emane_model_config( - self.session_id, node_id, model, iface_id + self.session.id, node_id, model, iface_id ) config = response.config logging.debug( diff --git a/daemon/core/gui/dialogs/canvassizeandscale.py b/daemon/core/gui/dialogs/canvassizeandscale.py index 38cecc83..e8ad6693 100644 --- a/daemon/core/gui/dialogs/canvassizeandscale.py +++ b/daemon/core/gui/dialogs/canvassizeandscale.py @@ -27,7 +27,7 @@ class SizeAndScaleDialog(Dialog): width, height = self.canvas.current_dimensions self.pixel_width: tk.IntVar = tk.IntVar(value=width) self.pixel_height: tk.IntVar = tk.IntVar(value=height) - location = self.app.core.location + location = self.app.core.session.location self.x: tk.DoubleVar = tk.DoubleVar(value=location.x) self.y: tk.DoubleVar = tk.DoubleVar(value=location.y) self.lat: tk.DoubleVar = tk.DoubleVar(value=location.lat) @@ -192,7 +192,7 @@ class SizeAndScaleDialog(Dialog): self.canvas.redraw_canvas((width, height)) if self.canvas.wallpaper: self.canvas.redraw_wallpaper() - location = self.app.core.location + location = self.app.core.session.location location.x = self.x.get() location.y = self.y.get() location.lat = self.lat.get() diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py index 5463d88e..f778cf15 100644 --- a/daemon/core/gui/dialogs/configserviceconfig.py +++ b/daemon/core/gui/dialogs/configserviceconfig.py @@ -11,28 +11,26 @@ import grpc from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll -from core.gui.wrappers import ConfigOption, ServiceValidationMode +from core.gui.wrappers import ( + ConfigOption, + ConfigServiceData, + Node, + ServiceValidationMode, +) if TYPE_CHECKING: from core.gui.app import Application - from core.gui.graph.node import CanvasNode from core.gui.coreclient import CoreClient class ConfigServiceConfigDialog(Dialog): def __init__( - self, - master: tk.BaseWidget, - app: "Application", - service_name: str, - canvas_node: "CanvasNode", - node_id: int, + self, master: tk.BaseWidget, app: "Application", service_name: str, node: Node ) -> None: title = f"{service_name} Config Service" super().__init__(app, title, master=master) self.core: "CoreClient" = app.core - self.canvas_node: "CanvasNode" = canvas_node - self.node_id: int = node_id + self.node: Node = node self.service_name: str = service_name self.radiovar: tk.IntVar = tk.IntVar() self.radiovar.set(2) @@ -50,7 +48,7 @@ class ConfigServiceConfigDialog(Dialog): self.validation_time: Optional[int] = None self.validation_period: tk.StringVar = tk.StringVar() self.modes: List[str] = [] - self.mode_configs: Dict[str, str] = {} + self.mode_configs: Dict[str, Dict[str, str]] = {} self.notebook: Optional[ttk.Notebook] = None self.templates_combobox: Optional[ttk.Combobox] = None @@ -91,25 +89,18 @@ class ConfigServiceConfigDialog(Dialog): response = self.core.client.get_config_service_defaults(self.service_name) self.original_service_files = response.templates self.temp_service_files = dict(self.original_service_files) - self.modes = sorted(x.name for x in response.modes) self.mode_configs = {x.name: x.config for x in response.modes} - - service_config = self.canvas_node.config_service_configs.get( - self.service_name, {} - ) self.config = ConfigOption.from_dict(response.config) self.default_config = {x.name: x.value for x in self.config.values()} - custom_config = service_config.get("config") - if custom_config: - for key, value in custom_config.items(): + service_config = self.node.config_service_configs.get(self.service_name) + if service_config: + for key, value in service_config.config.items(): self.config[key].value = value - logging.info("default config: %s", self.default_config) - - custom_templates = service_config.get("templates", {}) - for file, data in custom_templates.items(): - self.modified_files.add(file) - self.temp_service_files[file] = data + logging.info("default config: %s", self.default_config) + for file, data in service_config.templates.items(): + self.modified_files.add(file) + self.temp_service_files[file] = data except grpc.RpcError as e: self.app.show_grpc_exception("Get Config Service Error", e) self.has_error = True @@ -313,20 +304,18 @@ class ConfigServiceConfigDialog(Dialog): def click_apply(self) -> None: current_listbox = self.master.current.listbox if not self.is_custom(): - self.canvas_node.config_service_configs.pop(self.service_name, None) + self.node.config_service_configs.pop(self.service_name, None) current_listbox.itemconfig(current_listbox.curselection()[0], bg="") self.destroy() return - - service_config = self.canvas_node.config_service_configs.setdefault( - self.service_name, {} - ) + service_config = self.node.config_service_configs.get(self.service_name) + if not service_config: + service_config = ConfigServiceData() if self.config_frame: self.config_frame.parse_config() - service_config["config"] = {x.name: x.value for x in self.config.values()} - templates_config = service_config.setdefault("templates", {}) + service_config.config = {x.name: x.value for x in self.config.values()} for file in self.modified_files: - templates_config[file] = self.temp_service_files[file] + service_config.templates[file] = self.temp_service_files[file] all_current = current_listbox.get(0, tk.END) current_listbox.itemconfig(all_current.index(self.service_name), bg="green") self.destroy() @@ -360,9 +349,9 @@ class ConfigServiceConfigDialog(Dialog): return has_custom_templates or has_custom_config def click_defaults(self) -> None: - self.canvas_node.config_service_configs.pop(self.service_name, None) + self.node.config_service_configs.pop(self.service_name, None) logging.info( - "cleared config service config: %s", self.canvas_node.config_service_configs + "cleared config service config: %s", self.node.config_service_configs ) self.temp_service_files = dict(self.original_service_files) filename = self.templates_combobox.get() diff --git a/daemon/core/gui/dialogs/copyserviceconfig.py b/daemon/core/gui/dialogs/copyserviceconfig.py index 2a01249d..b60d5a0d 100644 --- a/daemon/core/gui/dialogs/copyserviceconfig.py +++ b/daemon/core/gui/dialogs/copyserviceconfig.py @@ -43,16 +43,15 @@ class CopyServiceConfigDialog(Dialog): listbox_scroll = ListboxScroll(self.top) listbox_scroll.grid(sticky="nsew", pady=PADY) self.listbox = listbox_scroll.listbox - for canvas_node in self.app.canvas.nodes.values(): - file_configs = canvas_node.service_file_configs.get(self.service) + for node in self.app.core.session.nodes.values(): + file_configs = node.service_file_configs.get(self.service) if not file_configs: continue data = file_configs.get(self.file_name) if not data: continue - name = canvas_node.core_node.name - self.nodes[name] = canvas_node.id - self.listbox.insert(tk.END, name) + self.nodes[node.name] = node.id + self.listbox.insert(tk.END, node.name) frame = ttk.Frame(self.top) frame.grid(sticky="ew") @@ -70,9 +69,9 @@ class CopyServiceConfigDialog(Dialog): if not selection: return name = self.listbox.get(selection) - canvas_node_id = self.nodes[name] - canvas_node = self.app.canvas.nodes[canvas_node_id] - data = canvas_node.service_file_configs[self.service][self.file_name] + node_id = self.nodes[name] + node = self.app.core.session.nodes[node_id] + data = node.service_file_configs[self.service][self.file_name] self.dialog.temp_service_files[self.file_name] = data self.dialog.modified_files.add(self.file_name) self.dialog.service_file_data.text.delete(1.0, tk.END) @@ -84,9 +83,9 @@ class CopyServiceConfigDialog(Dialog): if not selection: return name = self.listbox.get(selection) - canvas_node_id = self.nodes[name] - canvas_node = self.app.canvas.nodes[canvas_node_id] - data = canvas_node.service_file_configs[self.service][self.file_name] + node_id = self.nodes[name] + node = self.app.core.session.nodes[node_id] + data = node.service_file_configs[self.service][self.file_name] dialog = ViewConfigDialog( self.app, self, name, self.service, self.file_name, data ) diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index d87e935a..019eeaa9 100644 --- a/daemon/core/gui/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -16,7 +16,6 @@ from core.gui.wrappers import ConfigOption, Node if TYPE_CHECKING: from core.gui.app import Application - from core.gui.graph.node import CanvasNode class GlobalEmaneDialog(Dialog): @@ -29,8 +28,9 @@ class GlobalEmaneDialog(Dialog): def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) + session = self.app.core.session self.config_frame = ConfigFrame( - self.top, self.app, self.app.core.emane_config, self.enabled + self.top, self.app, session.emane_config, self.enabled ) self.config_frame.draw_config() self.config_frame.grid(sticky="nsew", pady=PADY) @@ -58,24 +58,19 @@ class EmaneModelDialog(Dialog): self, master: tk.BaseWidget, app: "Application", - canvas_node: "CanvasNode", + node: Node, model: str, iface_id: int = None, ) -> None: - super().__init__( - app, f"{canvas_node.core_node.name} {model} Configuration", master=master - ) - self.canvas_node: "CanvasNode" = canvas_node - self.node: Node = canvas_node.core_node + super().__init__(app, f"{node.name} {model} Configuration", master=master) + self.node: Node = node self.model: str = f"emane_{model}" self.iface_id: int = iface_id self.config_frame: Optional[ConfigFrame] = None self.enabled: bool = not self.app.core.is_runtime() self.has_error: bool = False try: - config = self.canvas_node.emane_model_configs.get( - (self.model, self.iface_id) - ) + config = self.node.emane_model_configs.get((self.model, self.iface_id)) if not config: config = self.app.core.get_emane_model_config( self.node.id, self.model, self.iface_id @@ -110,19 +105,18 @@ class EmaneModelDialog(Dialog): def click_apply(self) -> None: self.config_frame.parse_config() key = (self.model, self.iface_id) - self.canvas_node.emane_model_configs[key] = self.config + self.node.emane_model_configs[key] = self.config self.destroy() class EmaneConfigDialog(Dialog): - def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None: - super().__init__(app, f"{canvas_node.core_node.name} EMANE Configuration") - self.canvas_node: "CanvasNode" = canvas_node - self.node: Node = canvas_node.core_node + def __init__(self, app: "Application", node: Node) -> None: + super().__init__(app, f"{node.name} EMANE Configuration") + self.node: Node = node self.radiovar: tk.IntVar = tk.IntVar() self.radiovar.set(1) self.emane_models: List[str] = [ - x.split("_")[1] for x in self.app.core.emane_models + x.split("_")[1] for x in self.app.core.session.emane_models ] model = self.node.emane.split("_")[1] self.emane_model: tk.StringVar = tk.StringVar(value=model) @@ -231,7 +225,7 @@ class EmaneConfigDialog(Dialog): draw emane model configuration """ model_name = self.emane_model.get() - dialog = EmaneModelDialog(self, self.app, self.canvas_node, model_name) + dialog = EmaneModelDialog(self, self.app, self.node, model_name) if not dialog.has_error: dialog.show() diff --git a/daemon/core/gui/dialogs/hooks.py b/daemon/core/gui/dialogs/hooks.py index b004dae2..31ef3e15 100644 --- a/daemon/core/gui/dialogs/hooks.py +++ b/daemon/core/gui/dialogs/hooks.py @@ -113,8 +113,9 @@ class HooksDialog(Dialog): listbox_scroll.grid(sticky="nsew", pady=PADY) self.listbox = listbox_scroll.listbox self.listbox.bind("<>", self.select) - for hook_file in self.app.core.hooks: - self.listbox.insert(tk.END, hook_file) + session = self.app.core.session + for file in session.hooks: + self.listbox.insert(tk.END, file) frame = ttk.Frame(self.top) frame.grid(sticky="ew") @@ -138,20 +139,22 @@ class HooksDialog(Dialog): dialog.show() hook = dialog.hook if hook: - self.app.core.hooks[hook.file] = hook + self.app.core.session.hooks[hook.file] = hook self.listbox.insert(tk.END, hook.file) def click_edit(self) -> None: - hook = self.app.core.hooks.pop(self.selected) + session = self.app.core.session + hook = session.hooks.pop(self.selected) dialog = HookDialog(self, self.app) dialog.set(hook) dialog.show() - self.app.core.hooks[hook.file] = hook + session.hooks[hook.file] = hook self.listbox.delete(self.selected_index) self.listbox.insert(self.selected_index, hook.file) def click_delete(self) -> None: - del self.app.core.hooks[self.selected] + session = self.app.core.session + del session.hooks[self.selected] self.listbox.delete(tk.ANCHOR) self.edit_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED) diff --git a/daemon/core/gui/dialogs/linkconfig.py b/daemon/core/gui/dialogs/linkconfig.py index 87f43284..2a91da30 100644 --- a/daemon/core/gui/dialogs/linkconfig.py +++ b/daemon/core/gui/dialogs/linkconfig.py @@ -269,7 +269,7 @@ class LinkConfigurationDialog(Dialog): self.edge.asymmetric_link = None if self.app.core.is_runtime() and link.options: - session_id = self.app.core.session_id + session_id = self.app.core.session.id self.app.core.client.edit_link( session_id, link.node1_id, diff --git a/daemon/core/gui/dialogs/mobilityconfig.py b/daemon/core/gui/dialogs/mobilityconfig.py index ca9caf43..857167be 100644 --- a/daemon/core/gui/dialogs/mobilityconfig.py +++ b/daemon/core/gui/dialogs/mobilityconfig.py @@ -13,18 +13,16 @@ from core.gui.wrappers import ConfigOption, Node if TYPE_CHECKING: from core.gui.app import Application - from core.gui.graph.node import CanvasNode class MobilityConfigDialog(Dialog): - def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None: - super().__init__(app, f"{canvas_node.core_node.name} Mobility Configuration") - self.canvas_node: "CanvasNode" = canvas_node - self.node: Node = canvas_node.core_node + def __init__(self, app: "Application", node: Node) -> None: + super().__init__(app, f"{node.name} Mobility Configuration") + self.node: Node = node self.config_frame: Optional[ConfigFrame] = None self.has_error: bool = False try: - config = self.canvas_node.mobility_config + config = self.node.mobility_config if not config: config = self.app.core.get_mobility_config(self.node.id) self.config: Dict[str, ConfigOption] = config @@ -56,5 +54,5 @@ class MobilityConfigDialog(Dialog): def click_apply(self) -> None: self.config_frame.parse_config() - self.canvas_node.mobility_config = self.config + self.node.mobility_config = self.config self.destroy() diff --git a/daemon/core/gui/dialogs/mobilityplayer.py b/daemon/core/gui/dialogs/mobilityplayer.py index 66833aff..1bee97d2 100644 --- a/daemon/core/gui/dialogs/mobilityplayer.py +++ b/daemon/core/gui/dialogs/mobilityplayer.py @@ -1,38 +1,31 @@ import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING, Dict, Optional +from typing import TYPE_CHECKING, Optional import grpc from core.gui.dialogs.dialog import Dialog from core.gui.images import ImageEnum from core.gui.themes import PADX, PADY -from core.gui.wrappers import ConfigOption, MobilityAction, Node +from core.gui.wrappers import MobilityAction, Node if TYPE_CHECKING: from core.gui.app import Application - from core.gui.graph.node import CanvasNode ICON_SIZE: int = 16 class MobilityPlayer: - def __init__( - self, - app: "Application", - canvas_node: "CanvasNode", - config: Dict[str, ConfigOption], - ) -> None: + def __init__(self, app: "Application", node: Node) -> None: self.app: "Application" = app - self.canvas_node: "CanvasNode" = canvas_node - self.config: Dict[str, ConfigOption] = config + self.node: Node = node self.dialog: Optional[MobilityPlayerDialog] = None self.state: Optional[MobilityAction] = None def show(self) -> None: if self.dialog: self.dialog.destroy() - self.dialog = MobilityPlayerDialog(self.app, self.canvas_node, self.config) + self.dialog = MobilityPlayerDialog(self.app, self.node) self.dialog.protocol("WM_DELETE_WINDOW", self.close) if self.state == MobilityAction.START: self.set_play() @@ -64,20 +57,11 @@ class MobilityPlayer: class MobilityPlayerDialog(Dialog): - def __init__( - self, - app: "Application", - canvas_node: "CanvasNode", - config: Dict[str, ConfigOption], - ) -> None: - super().__init__( - app, f"{canvas_node.core_node.name} Mobility Player", modal=False - ) + def __init__(self, app: "Application", node: Node) -> None: + super().__init__(app, f"{node.name} Mobility Player", modal=False) self.resizable(False, False) self.geometry("") - self.canvas_node: "CanvasNode" = canvas_node - self.node: Node = canvas_node.core_node - self.config: Dict[str, ConfigOption] = config + self.node: Node = node self.play_button: Optional[ttk.Button] = None self.pause_button: Optional[ttk.Button] = None self.stop_button: Optional[ttk.Button] = None @@ -85,9 +69,10 @@ class MobilityPlayerDialog(Dialog): self.draw() def draw(self) -> None: + config = self.node.mobility_config self.top.columnconfigure(0, weight=1) - file_name = self.config["file"].value + file_name = config["file"].value label = ttk.Label(self.top, text=file_name) label.grid(sticky="ew", pady=PADY) @@ -114,13 +99,13 @@ class MobilityPlayerDialog(Dialog): self.stop_button.image = image self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX) - loop = tk.IntVar(value=int(self.config["loop"].value == "1")) + loop = tk.IntVar(value=int(config["loop"].value == "1")) checkbutton = ttk.Checkbutton( frame, text="Loop?", variable=loop, state=tk.DISABLED ) checkbutton.grid(row=0, column=3, padx=PADX) - rate = self.config["refresh_ms"].value + rate = config["refresh_ms"].value label = ttk.Label(frame, text=f"rate {rate} ms") label.grid(row=0, column=4) @@ -146,7 +131,7 @@ class MobilityPlayerDialog(Dialog): def click_play(self) -> None: self.set_play() - session_id = self.app.core.session_id + session_id = self.app.core.session.id try: self.app.core.client.mobility_action( session_id, self.node.id, MobilityAction.START.value @@ -156,7 +141,7 @@ class MobilityPlayerDialog(Dialog): def click_pause(self) -> None: self.set_pause() - session_id = self.app.core.session_id + session_id = self.app.core.session.id try: self.app.core.client.mobility_action( session_id, self.node.id, MobilityAction.PAUSE.value @@ -166,7 +151,7 @@ class MobilityPlayerDialog(Dialog): def click_stop(self) -> None: self.set_stop() - session_id = self.app.core.session_id + session_id = self.app.core.session.id try: self.app.core.client.mobility_action( session_id, self.node.id, MobilityAction.STOP.value diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeconfigservice.py index b9a9a1f5..dee34f71 100644 --- a/daemon/core/gui/dialogs/nodeconfigservice.py +++ b/daemon/core/gui/dialogs/nodeconfigservice.py @@ -10,25 +10,24 @@ from core.gui.dialogs.configserviceconfig import ConfigServiceConfigDialog from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CheckboxList, ListboxScroll +from core.gui.wrappers import Node if TYPE_CHECKING: from core.gui.app import Application - from core.gui.graph.node import CanvasNode class NodeConfigServiceDialog(Dialog): def __init__( - self, app: "Application", canvas_node: "CanvasNode", services: Set[str] = None + self, app: "Application", node: Node, services: Set[str] = None ) -> None: - title = f"{canvas_node.core_node.name} Config Services" + title = f"{node.name} Config Services" super().__init__(app, title) - self.canvas_node: "CanvasNode" = canvas_node - self.node_id: int = canvas_node.core_node.id + self.node: Node = node self.groups: Optional[ListboxScroll] = None self.services: Optional[CheckboxList] = None self.current: Optional[ListboxScroll] = None if services is None: - services = set(canvas_node.core_node.config_services) + services = set(node.config_services) self.current_services: Set[str] = services self.draw() @@ -102,7 +101,7 @@ class NodeConfigServiceDialog(Dialog): elif not var.get() and name in self.current_services: self.current_services.remove(name) self.draw_current_services() - self.canvas_node.core_node.config_services[:] = self.current_services + self.node.config_services[:] = self.current_services def click_configure(self) -> None: current_selection = self.current.listbox.curselection() @@ -111,8 +110,7 @@ class NodeConfigServiceDialog(Dialog): self, self.app, self.current.listbox.get(current_selection[0]), - self.canvas_node, - self.node_id, + self.node, ) if not dialog.has_error: dialog.show() @@ -132,10 +130,8 @@ class NodeConfigServiceDialog(Dialog): self.current.listbox.itemconfig(tk.END, bg="green") def click_save(self) -> None: - self.canvas_node.core_node.config_services[:] = self.current_services - logging.info( - "saved node config services: %s", self.canvas_node.core_node.config_services - ) + self.node.config_services[:] = self.current_services + logging.info("saved node config services: %s", self.node.config_services) self.destroy() def click_cancel(self) -> None: @@ -154,4 +150,4 @@ class NodeConfigServiceDialog(Dialog): return def is_custom_service(self, service: str) -> bool: - return service in self.canvas_node.config_service_configs + return service in self.node.config_service_configs diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index 6fcc2912..a56736d5 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -9,22 +9,21 @@ from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.serviceconfig import ServiceConfigDialog from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CheckboxList, ListboxScroll +from core.gui.wrappers import Node if TYPE_CHECKING: from core.gui.app import Application - from core.gui.graph.node import CanvasNode class NodeServiceDialog(Dialog): - def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None: - title = f"{canvas_node.core_node.name} Services" + def __init__(self, app: "Application", node: Node) -> None: + title = f"{node.name} Services" super().__init__(app, title) - self.canvas_node: "CanvasNode" = canvas_node - self.node_id: int = canvas_node.core_node.id + self.node: Node = node self.groups: Optional[ListboxScroll] = None self.services: Optional[CheckboxList] = None self.current: Optional[ListboxScroll] = None - services = set(canvas_node.core_node.services) + services = set(node.services) self.current_services: Set[str] = services self.draw() @@ -104,7 +103,7 @@ class NodeServiceDialog(Dialog): self.current.listbox.insert(tk.END, name) if self.is_custom_service(name): self.current.listbox.itemconfig(tk.END, bg="green") - self.canvas_node.core_node.services[:] = self.current_services + self.node.services = self.current_services.copy() def click_configure(self) -> None: current_selection = self.current.listbox.curselection() @@ -113,8 +112,7 @@ class NodeServiceDialog(Dialog): self, self.app, self.current.listbox.get(current_selection[0]), - self.canvas_node, - self.node_id, + self.node, ) # if error occurred when creating ServiceConfigDialog, don't show the dialog @@ -128,8 +126,7 @@ class NodeServiceDialog(Dialog): ) def click_save(self) -> None: - core_node = self.canvas_node.core_node - core_node.services[:] = self.current_services + self.node.services[:] = self.current_services self.destroy() def click_remove(self) -> None: @@ -144,6 +141,6 @@ class NodeServiceDialog(Dialog): return def is_custom_service(self, service: str) -> bool: - has_service_config = service in self.canvas_node.service_configs - has_file_config = service in self.canvas_node.service_file_configs + has_service_config = service in self.node.service_configs + has_file_config = service in self.node.service_file_configs return has_service_config or has_file_config diff --git a/daemon/core/gui/dialogs/runtool.py b/daemon/core/gui/dialogs/runtool.py index c66fea8f..a1517593 100644 --- a/daemon/core/gui/dialogs/runtool.py +++ b/daemon/core/gui/dialogs/runtool.py @@ -107,7 +107,7 @@ class RunToolDialog(Dialog): node_name = self.node_list.listbox.get(selection) node_id = self.executable_nodes[node_name] response = self.app.core.client.node_command( - self.app.core.session_id, node_id, command + self.app.core.session.id, node_id, command ) self.result.text.insert( tk.END, f"> {node_name} > {command}:\n{response.output}\n" diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index c033cfdc..13be0bcd 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -12,11 +12,10 @@ from core.gui.dialogs.dialog import Dialog from core.gui.images import ImageEnum, Images from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CodeText, ListboxScroll -from core.gui.wrappers import NodeServiceData, ServiceValidationMode +from core.gui.wrappers import Node, NodeServiceData, ServiceValidationMode if TYPE_CHECKING: from core.gui.app import Application - from core.gui.graph.node import CanvasNode from core.gui.coreclient import CoreClient ICON_SIZE: int = 16 @@ -24,18 +23,12 @@ ICON_SIZE: int = 16 class ServiceConfigDialog(Dialog): def __init__( - self, - master: tk.BaseWidget, - app: "Application", - service_name: str, - canvas_node: "CanvasNode", - node_id: int, + self, master: tk.BaseWidget, app: "Application", service_name: str, node: Node ) -> None: title = f"{service_name} Service" super().__init__(app, title, master=master) self.core: "CoreClient" = app.core - self.canvas_node: "CanvasNode" = canvas_node - self.node_id: int = node_id + self.node: Node = node self.service_name: str = service_name self.radiovar: tk.IntVar = tk.IntVar(value=2) self.metadata: str = "" @@ -84,15 +77,13 @@ class ServiceConfigDialog(Dialog): try: self.app.core.create_nodes_and_links() default_config = self.app.core.get_node_service( - self.node_id, self.service_name + self.node.id, self.service_name ) self.default_startup = default_config.startup[:] self.default_validate = default_config.validate[:] self.default_shutdown = default_config.shutdown[:] self.default_directories = default_config.dirs[:] - custom_service_config = self.canvas_node.service_configs.get( - self.service_name - ) + custom_service_config = self.node.service_configs.get(self.service_name) self.default_config = default_config service_config = ( custom_service_config if custom_service_config else default_config @@ -109,15 +100,13 @@ class ServiceConfigDialog(Dialog): self.temp_directories = service_config.dirs[:] self.original_service_files = { x: self.app.core.get_node_service_file( - self.node_id, self.service_name, x + self.node.id, self.service_name, x ) for x in default_config.configs } self.temp_service_files = dict(self.original_service_files) - file_configs = self.canvas_node.service_file_configs.get( - self.service_name, {} - ) + file_configs = self.node.service_file_configs.get(self.service_name, {}) for file, data in file_configs.items(): self.temp_service_files[file] = data except grpc.RpcError as e: @@ -453,7 +442,7 @@ class ServiceConfigDialog(Dialog): and not self.has_new_files() and not self.is_custom_directory() ): - self.canvas_node.service_configs.pop(self.service_name, None) + self.node.service_configs.pop(self.service_name, None) self.current_service_color("") self.destroy() return @@ -466,7 +455,7 @@ class ServiceConfigDialog(Dialog): ): startup, validate, shutdown = self.get_commands() config = self.core.set_node_service( - self.node_id, + self.node.id, self.service_name, dirs=self.temp_directories, files=list(self.filename_combobox["values"]), @@ -474,15 +463,15 @@ class ServiceConfigDialog(Dialog): validations=validate, shutdowns=shutdown, ) - self.canvas_node.service_configs[self.service_name] = config + self.node.service_configs[self.service_name] = config for file in self.modified_files: - file_configs = self.canvas_node.service_file_configs.setdefault( + file_configs = self.node.service_file_configs.setdefault( self.service_name, {} ) file_configs[file] = self.temp_service_files[file] # TODO: check if this is really needed self.app.core.set_node_service_file( - self.node_id, self.service_name, file, self.temp_service_files[file] + self.node.id, self.service_name, file, self.temp_service_files[file] ) self.current_service_color("green") except grpc.RpcError as e: @@ -526,8 +515,8 @@ class ServiceConfigDialog(Dialog): clears out any custom configuration permanently """ # clear coreclient data - self.canvas_node.service_configs.pop(self.service_name, None) - file_configs = self.canvas_node.service_file_configs.pop(self.service_name, {}) + self.node.service_configs.pop(self.service_name, None) + file_configs = self.node.service_file_configs.pop(self.service_name, {}) file_configs.pop(self.service_name, None) self.temp_service_files = dict(self.original_service_files) self.modified_files.clear() @@ -564,9 +553,8 @@ class ServiceConfigDialog(Dialog): def click_copy(self) -> None: file_name = self.filename_combobox.get() - name = self.canvas_node.core_node.name dialog = CopyServiceConfigDialog( - self.app, self, name, self.service_name, file_name + self.app, self, self.node.name, self.service_name, file_name ) dialog.show() diff --git a/daemon/core/gui/dialogs/sessionoptions.py b/daemon/core/gui/dialogs/sessionoptions.py index 24bacb30..570bfbde 100644 --- a/daemon/core/gui/dialogs/sessionoptions.py +++ b/daemon/core/gui/dialogs/sessionoptions.py @@ -26,7 +26,7 @@ class SessionOptionsDialog(Dialog): def get_config(self) -> Dict[str, ConfigOption]: try: - session_id = self.app.core.session_id + session_id = self.app.core.session.id response = self.app.core.client.get_session_options(session_id) return ConfigOption.from_dict(response.config) except grpc.RpcError as e: @@ -54,7 +54,7 @@ class SessionOptionsDialog(Dialog): def save(self) -> None: config = self.config_frame.parse_config() try: - session_id = self.app.core.session_id + session_id = self.app.core.session.id response = self.app.core.client.set_session_options(session_id, config) logging.info("saved session config: %s", response) except grpc.RpcError as e: diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index 75b9dcf4..d41e2052 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -201,7 +201,7 @@ class SessionsDialog(Dialog): logging.debug("delete session: %s", self.selected_session) self.tree.delete(self.selected_id) self.app.core.delete_session(self.selected_session) - if self.selected_session == self.app.core.session_id: + if self.selected_session == self.app.core.session.id: self.click_new() self.destroy() self.click_select() diff --git a/daemon/core/gui/dialogs/wlanconfig.py b/daemon/core/gui/dialogs/wlanconfig.py index 17f62dfb..d4595556 100644 --- a/daemon/core/gui/dialogs/wlanconfig.py +++ b/daemon/core/gui/dialogs/wlanconfig.py @@ -29,7 +29,7 @@ class WlanConfigDialog(Dialog): self.ranges: Dict[int, int] = {} self.positive_int: int = self.app.master.register(self.validate_and_update) try: - config = self.canvas_node.wlan_config + config = self.node.wlan_config if not config: config = self.app.core.get_wlan_config(self.node.id) self.config: Dict[str, ConfigOption] = config @@ -83,9 +83,9 @@ class WlanConfigDialog(Dialog): retrieve user's wlan configuration and store the new configuration values """ config = self.config_frame.parse_config() - self.canvas_node.wlan_config = self.config + self.node.wlan_config = self.config if self.app.core.is_runtime(): - session_id = self.app.core.session_id + session_id = self.app.core.session.id self.app.core.client.set_wlan_config(session_id, self.node.id, config) self.remove_ranges() self.destroy() diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index f2a27444..54d2cae1 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -940,16 +940,19 @@ class CanvasGraph(tk.Canvas): if not copy: continue node = CanvasNode(self.app, scaled_x, scaled_y, copy, canvas_node.image) - # copy configurations and services - node.core_node.services[:] = canvas_node.core_node.services - node.core_node.config_services[:] = canvas_node.core_node.config_services - node.emane_model_configs = deepcopy(canvas_node.emane_model_configs) - node.wlan_config = deepcopy(canvas_node.wlan_config) - node.mobility_config = deepcopy(canvas_node.mobility_config) - node.service_configs = deepcopy(canvas_node.service_configs) - node.service_file_configs = deepcopy(canvas_node.service_file_configs) - node.config_service_configs = deepcopy(canvas_node.config_service_configs) + node.core_node.services = core_node.services.copy() + node.core_node.config_services = core_node.config_services.copy() + node.core_node.emane_model_configs = deepcopy(core_node.emane_model_configs) + node.core_node.wlan_config = deepcopy(core_node.wlan_config) + node.core_node.mobility_config = deepcopy(core_node.mobility_config) + node.core_node.service_configs = deepcopy(core_node.service_configs) + node.core_node.service_file_configs = deepcopy( + core_node.service_file_configs + ) + node.core_node.config_service_configs = deepcopy( + core_node.config_service_configs + ) copy_map[canvas_node.id] = node.id self.core.canvas_nodes[copy.id] = node diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 217389c0..ffc72fbf 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -1,7 +1,7 @@ import functools import logging import tkinter as tk -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple +from typing import TYPE_CHECKING, Dict, List, Set import grpc from PIL.ImageTk import PhotoImage @@ -19,7 +19,7 @@ from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge from core.gui.graph.tooltip import CanvasTooltip from core.gui.images import ImageEnum from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils -from core.gui.wrappers import ConfigOption, Interface, Node, NodeServiceData, NodeType +from core.gui.wrappers import Interface, Node, NodeType if TYPE_CHECKING: from core.gui.app import Application @@ -56,15 +56,6 @@ class CanvasNode: self.wireless_edges: Set[CanvasWirelessEdge] = set() self.antennas: List[int] = [] self.antenna_images: Dict[int, PhotoImage] = {} - # possible configurations - self.emane_model_configs: Dict[ - Tuple[str, Optional[int]], Dict[str, ConfigOption] - ] = {} - self.wlan_config: Dict[str, ConfigOption] = {} - self.mobility_config: Dict[str, ConfigOption] = {} - self.service_configs: Dict[str, NodeServiceData] = {} - self.service_file_configs: Dict[str, Dict[str, str]] = {} - self.config_service_configs: Dict[str, Any] = {} self.setup_bindings() self.context: tk.Menu = tk.Menu(self.canvas) themes.style_menu(self.context) @@ -299,7 +290,7 @@ class CanvasNode: dialog.show() def show_mobility_config(self) -> None: - dialog = MobilityConfigDialog(self.app, self) + dialog = MobilityConfigDialog(self.app, self.core_node) if not dialog.has_error: dialog.show() @@ -308,15 +299,15 @@ class CanvasNode: mobility_player.show() def show_emane_config(self) -> None: - dialog = EmaneConfigDialog(self.app, self) + dialog = EmaneConfigDialog(self.app, self.core_node) dialog.show() def show_services(self) -> None: - dialog = NodeServiceDialog(self.app, self) + dialog = NodeServiceDialog(self.app, self.core_node) dialog.show() def show_config_services(self) -> None: - dialog = NodeConfigServiceDialog(self.app, self) + dialog = NodeConfigServiceDialog(self.app, self.core_node) dialog.show() def has_emane_link(self, iface_id: int) -> Node: diff --git a/daemon/core/gui/task.py b/daemon/core/gui/task.py index c60350f9..f56fd54b 100644 --- a/daemon/core/gui/task.py +++ b/daemon/core/gui/task.py @@ -33,7 +33,6 @@ class ProgressTask: thread.start() def run(self) -> None: - logging.info("running task") try: values = self.task(*self.args) if values is None: @@ -41,7 +40,6 @@ class ProgressTask: elif values and not isinstance(values, tuple): values = (values,) if self.callback: - logging.info("calling callback") self.app.after(0, self.callback, *values) except Exception as e: logging.exception("progress task exception") diff --git a/daemon/core/gui/wrappers.py b/daemon/core/gui/wrappers.py index 835a9d17..d86e20dd 100644 --- a/daemon/core/gui/wrappers.py +++ b/daemon/core/gui/wrappers.py @@ -1,6 +1,6 @@ from dataclasses import dataclass, field from enum import Enum -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Optional, Set, Tuple from core.api.grpc import common_pb2, configservices_pb2, core_pb2, services_pb2 @@ -121,8 +121,8 @@ class ConfigService: @dataclass class ConfigServiceData: - templates: Dict[str, str] - config: Dict[str, str] + templates: Dict[str, str] = field(default_factory=dict) + config: Dict[str, str] = field(default_factory=dict) @dataclass @@ -504,8 +504,8 @@ class Node: type: NodeType model: str = None position: Position = None - services: List[str] = field(default_factory=list) - config_services: List[str] = field(default_factory=list) + services: Set[str] = field(default_factory=set) + config_services: Set[str] = field(default_factory=set) emane: str = None icon: str = None image: str = None @@ -538,8 +538,8 @@ class Node: type=NodeType(proto.type), model=proto.model, position=Position.from_proto(proto.position), - services=list(proto.services), - config_services=list(proto.config_services), + services=set(proto.services), + config_services=set(proto.config_services), emane=proto.emane, icon=proto.icon, image=proto.image, @@ -575,9 +575,9 @@ class Session: links: List[Link] dir: str user: str - default_services: Dict[str, List[str]] + default_services: Dict[str, Set[str]] location: SessionLocation - hooks: List[Hook] + hooks: Dict[str, Hook] emane_models: List[str] emane_config: Dict[str, ConfigOption] metadata: Dict[str, str] @@ -586,8 +586,10 @@ class Session: def from_proto(cls, proto: core_pb2.Session) -> "Session": nodes: Dict[int, Node] = {x.id: Node.from_proto(x) for x in proto.nodes} links = [Link.from_proto(x) for x in proto.links] - default_services = {x.node_type: x.services for x in proto.default_services} - hooks = [Hook.from_proto(x) for x in proto.hooks] + default_services = { + x.node_type: set(x.services) for x in proto.default_services + } + hooks = {x.file: Hook.from_proto(x) for x in proto.hooks} # update nodes with their current configurations for model in proto.emane_model_configs: iface_id = None From 27495cbda10341fe0a802742cb0aed28d1524316 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 28 Jul 2020 10:24:01 -0700 Subject: [PATCH 0551/1131] pygui: changes around using session.nodes instead of canvas_nodes when possible --- daemon/core/gui/coreclient.py | 68 +++++++++++++++--------------- daemon/core/gui/dialogs/find.py | 17 +++----- daemon/core/gui/dialogs/runtool.py | 6 +-- daemon/core/gui/frames/link.py | 10 ++--- daemon/core/gui/graph/graph.py | 24 +++++------ 5 files changed, 58 insertions(+), 67 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 36adf189..6129031a 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -118,6 +118,12 @@ class CoreClient: self.setup_cpu_usage() return self._client + def set_canvas_node(self, node: Node, canvas_node: CanvasNode) -> None: + self.canvas_nodes[node.id] = canvas_node + + def get_canvas_node(self, node_id: int) -> CanvasNode: + return self.canvas_nodes[node_id] + def reset(self) -> None: # helpers self.ifaces_manager.reset() @@ -231,18 +237,21 @@ class CoreClient: def handle_node_event(self, event: NodeEvent) -> None: logging.debug("node event: %s", event) + node = event.node if event.message_type == MessageType.NONE: - canvas_node = self.canvas_nodes[event.node.id] - x = event.node.position.x - y = event.node.position.y + canvas_node = self.canvas_nodes[node.id] + x = node.position.x + y = node.position.y canvas_node.move(x, y) elif event.message_type == MessageType.DELETE: - canvas_node = self.canvas_nodes[event.node.id] + canvas_node = self.canvas_nodes[node.id] self.app.canvas.clear_selection() self.app.canvas.select_object(canvas_node.id) self.app.canvas.delete_selected_objects() elif event.message_type == MessageType.ADD: - self.app.canvas.add_core_node(event.node) + if node.id in self.session.nodes: + logging.error("core node already exists: %s", node) + self.app.canvas.add_core_node(node) else: logging.warning("unknown node event: %s", event) @@ -463,10 +472,9 @@ class CoreClient: def start_session(self) -> Tuple[bool, List[str]]: self.ifaces_manager.reset_mac() - nodes = [x.core_node.to_proto() for x in self.canvas_nodes.values()] + nodes = [x.to_proto() for x in self.session.nodes.values()] links = [] - for edge in self.links.values(): - link = edge.link + for link in self.session.links: if link.iface1 and not link.iface1.mac: link.iface1.mac = self.ifaces_manager.next_mac() if link.iface2 and not link.iface2.mac: @@ -674,13 +682,12 @@ class CoreClient: """ create nodes and links that have not been created yet """ - node_protos = [x.core_node.to_proto() for x in self.canvas_nodes.values()] - link_protos = [x.link.to_proto() for x in self.links.values()] self.client.set_session_state(self.session.id, SessionState.DEFINITION.value) - for node_proto in node_protos: - response = self.client.add_node(self.session.id, node_proto) - logging.debug("create node: %s", response) - for link_proto in link_protos: + for node in self.session.nodes.values(): + response = self.client.add_node(self.session.id, node.to_proto()) + logging.debug("created node: %s", response) + for link in self.session.links: + link_proto = link.to_proto() response = self.client.add_link( self.session.id, link_proto.node1_id, @@ -689,7 +696,7 @@ class CoreClient: link_proto.iface2, link_proto.options, ) - logging.debug("create link: %s", response) + logging.debug("created link: %s", response) def send_data(self) -> None: """ @@ -762,7 +769,7 @@ class CoreClient: """ i = 1 while True: - if i not in self.canvas_nodes: + if i not in self.session.nodes: break i += 1 return i @@ -816,18 +823,20 @@ class CoreClient: x, y, ) + self.session.nodes[node.id] = node return node - def deleted_graph_nodes(self, canvas_nodes: List[CanvasNode]) -> None: + def deleted_canvas_nodes(self, canvas_nodes: List[CanvasNode]) -> None: """ remove the nodes selected by the user and anything related to that node such as link, configurations, interfaces """ for canvas_node in canvas_nodes: - node_id = canvas_node.core_node.id - del self.canvas_nodes[node_id] + node = canvas_node.core_node + del self.canvas_nodes[node.id] + del self.session.nodes[node.id] - def deleted_graph_edges(self, edges: Iterable[CanvasEdge]) -> None: + def deleted_canvas_edges(self, edges: Iterable[CanvasEdge]) -> None: links = [] for edge in edges: del self.links[edge.token] @@ -861,20 +870,19 @@ class CoreClient: """ src_node = canvas_src_node.core_node dst_node = canvas_dst_node.core_node - - # determine subnet self.ifaces_manager.determine_subnets(canvas_src_node, canvas_dst_node) - 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, @@ -882,17 +890,9 @@ class CoreClient: iface1=src_iface, iface2=dst_iface, ) - # assign after creating link proto, since interfaces are copied - if src_iface: - iface1 = link.iface1 - edge.src_iface = iface1 - canvas_src_node.ifaces[iface1.id] = iface1 - if dst_iface: - iface2 = link.iface2 - edge.dst_iface = iface2 - canvas_dst_node.ifaces[iface2.id] = iface2 edge.set_link(link) self.links[edge.token] = edge + self.session.links.append(link) logging.info("Add link between %s and %s", src_node.name, dst_node.name) def get_wlan_configs_proto(self) -> List[wlan_pb2.WlanConfig]: diff --git a/daemon/core/gui/dialogs/find.py b/daemon/core/gui/dialogs/find.py index 328f673e..a4600847 100644 --- a/daemon/core/gui/dialogs/find.py +++ b/daemon/core/gui/dialogs/find.py @@ -87,22 +87,19 @@ class FindDialog(Dialog): """ node_name = self.find_text.get().strip() self.clear_treeview_items() - for node_id, node in sorted( - self.app.core.canvas_nodes.items(), key=lambda x: x[0] - ): - name = node.core_node.name + for node in self.app.core.session.nodes.values(): + name = node.name if not node_name or node_name == name: - pos_x = round(node.core_node.position.x, 1) - pos_y = round(node.core_node.position.y, 1) + pos_x = round(node.position.x, 1) + pos_y = round(node.position.y, 1) # TODO: I am not sure what to insert for Detail column # leaving it blank for now self.tree.insert( "", tk.END, - text=str(node_id), - values=(node_id, name, f"<{pos_x}, {pos_y}>", ""), + text=str(node.id), + values=(node.id, name, f"<{pos_x}, {pos_y}>", ""), ) - results = self.tree.get_children("") if results: self.tree.selection_set(results[0]) @@ -121,7 +118,7 @@ class FindDialog(Dialog): if item: self.app.canvas.delete("find") node_id = int(self.tree.item(item, "text")) - canvas_node = self.app.core.canvas_nodes[node_id] + canvas_node = self.app.core.get_canvas_node(node_id) x0, y0, x1, y1 = self.app.canvas.bbox(canvas_node.id) dist = 5 * self.app.guiconfig.scale diff --git a/daemon/core/gui/dialogs/runtool.py b/daemon/core/gui/dialogs/runtool.py index a1517593..e36c4c9a 100644 --- a/daemon/core/gui/dialogs/runtool.py +++ b/daemon/core/gui/dialogs/runtool.py @@ -25,9 +25,9 @@ class RunToolDialog(Dialog): """ store all CORE nodes (nodes that execute commands) from all existing nodes """ - for nid, node in self.app.core.canvas_nodes.items(): - if NodeUtils.is_container_node(node.core_node.type): - self.executable_nodes[node.core_node.name] = nid + for node in self.app.core.session.nodes.values(): + if NodeUtils.is_container_node(node.type): + self.executable_nodes[node.name] = node.id def draw(self) -> None: self.top.rowconfigure(0, weight=1) diff --git a/daemon/core/gui/frames/link.py b/daemon/core/gui/frames/link.py index cbea9982..093f39eb 100644 --- a/daemon/core/gui/frames/link.py +++ b/daemon/core/gui/frames/link.py @@ -34,10 +34,8 @@ class EdgeInfoFrame(InfoFrameBase): self.columnconfigure(0, weight=1) link = self.edge.link options = link.options - src_canvas_node = self.app.core.canvas_nodes[link.node1_id] - src_node = src_canvas_node.core_node - dst_canvas_node = self.app.core.canvas_nodes[link.node2_id] - dst_node = dst_canvas_node.core_node + src_node = self.app.core.session.nodes[link.node1_id] + dst_node = self.app.core.session.nodes[link.node2_id] frame = DetailsFrame(self) frame.grid(sticky="ew") @@ -81,9 +79,9 @@ class WirelessEdgeInfoFrame(InfoFrameBase): def draw(self) -> None: link = self.edge.link - src_canvas_node = self.app.core.canvas_nodes[link.node1_id] + src_canvas_node = self.app.canvas.nodes[self.edge.src] src_node = src_canvas_node.core_node - dst_canvas_node = self.app.core.canvas_nodes[link.node2_id] + dst_canvas_node = self.app.canvas.nodes[self.edge.dst] dst_node = dst_canvas_node.core_node # find interface for each node connected to network diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 54d2cae1..69ae87cc 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -311,10 +311,7 @@ class CanvasGraph(tk.Canvas): edge.middle_label_text(link.label) def add_core_node(self, core_node: Node) -> None: - if core_node.id in self.core.canvas_nodes: - logging.error("core node already exists: %s", core_node) - return - logging.debug("adding node %s", core_node) + logging.debug("adding node: %s", core_node) # if the gui can't find node's image, default to the "edit-node" image image = NodeUtils.node_image(core_node, self.app.guiconfig, self.app.app_scale) if not image: @@ -323,7 +320,7 @@ class CanvasGraph(tk.Canvas): y = core_node.position.y node = CanvasNode(self.app, x, y, core_node, image) self.nodes[node.id] = node - self.core.canvas_nodes[core_node.id] = node + self.core.set_canvas_node(core_node, node) def draw_session(self, session: Session) -> None: """ @@ -336,12 +333,11 @@ class CanvasGraph(tk.Canvas): if NodeUtils.is_ignore_node(core_node.type): continue self.add_core_node(core_node) - - # draw existing links + # draw existing links for link in session.links: logging.debug("drawing link: %s", link) - canvas_node1 = self.core.canvas_nodes[link.node1_id] - canvas_node2 = self.core.canvas_nodes[link.node2_id] + canvas_node1 = self.core.get_canvas_node(link.node1_id) + canvas_node2 = self.core.get_canvas_node(link.node2_id) if link.type == LinkType.WIRELESS: self.add_wireless_edge(canvas_node1, canvas_node2, link) else: @@ -544,8 +540,8 @@ class CanvasGraph(tk.Canvas): shape.delete() self.selection.clear() - self.core.deleted_graph_nodes(nodes) - self.core.deleted_graph_edges(edges) + self.core.deleted_canvas_nodes(nodes) + self.core.deleted_canvas_edges(edges) def delete_edge(self, edge: CanvasEdge) -> None: edge.delete() @@ -564,7 +560,7 @@ class CanvasGraph(tk.Canvas): dst_wireless = NodeUtils.is_wireless_node(dst_node.core_node.type) if dst_wireless: src_node.delete_antenna() - self.core.deleted_graph_edges([edge]) + self.core.deleted_canvas_edges([edge]) def zoom(self, event: tk.Event, factor: float = None) -> None: if not factor: @@ -750,8 +746,8 @@ class CanvasGraph(tk.Canvas): image_file = self.node_draw.image_file self.node_draw.image = self.app.get_custom_icon(image_file, ICON_SIZE) node = CanvasNode(self.app, x, y, core_node, self.node_draw.image) - self.core.canvas_nodes[core_node.id] = node self.nodes[node.id] = node + self.core.set_canvas_node(core_node, node) def width_and_height(self) -> Tuple[int, int]: """ @@ -955,8 +951,8 @@ class CanvasGraph(tk.Canvas): ) copy_map[canvas_node.id] = node.id - self.core.canvas_nodes[copy.id] = node self.nodes[node.id] = node + self.core.set_canvas_node(copy, node) for edge in canvas_node.edges: if edge.src not in self.to_copy or edge.dst not in self.to_copy: if canvas_node.id == edge.src: From 0d2dd70727ab6c8ab5b0055539d9587c146dce21 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 28 Jul 2020 16:13:37 -0700 Subject: [PATCH 0552/1131] daemon: changes usage of running scripts using /bin/sh to bash to help provide consistency in what could be ran, added bash as a dependency in installation scripts, added bash as an executable check during startup --- .../configservices/frrservices/services.py | 2 +- .../configservices/nrlservices/services.py | 16 ++++++++-------- .../configservices/quaggaservices/services.py | 2 +- .../securityservices/services.py | 10 +++++----- .../configservices/utilservices/services.py | 18 +++++++++--------- daemon/core/executables.py | 12 +++++++++++- daemon/core/location/mobility.py | 3 ++- daemon/core/nodes/base.py | 2 +- daemon/core/nodes/client.py | 8 +++++--- daemon/core/nodes/interface.py | 4 ++-- daemon/core/nodes/netclient.py | 6 +----- daemon/core/services/emaneservices.py | 2 +- daemon/core/services/frr.py | 2 +- daemon/core/services/nrl.py | 6 +++--- daemon/core/services/quagga.py | 2 +- daemon/core/services/sdn.py | 4 ++-- daemon/core/services/security.py | 10 +++++----- daemon/core/services/ucarp.py | 2 +- daemon/core/services/utility.py | 18 +++++++++--------- tasks.py | 4 ++-- 20 files changed, 71 insertions(+), 62 deletions(-) diff --git a/daemon/core/configservices/frrservices/services.py b/daemon/core/configservices/frrservices/services.py index 72050077..fa6f599a 100644 --- a/daemon/core/configservices/frrservices/services.py +++ b/daemon/core/configservices/frrservices/services.py @@ -65,7 +65,7 @@ class FRRZebra(ConfigService): ] executables: List[str] = ["zebra"] dependencies: List[str] = [] - startup: List[str] = ["sh frrboot.sh zebra"] + startup: List[str] = ["bash frrboot.sh zebra"] validate: List[str] = ["pidof zebra"] shutdown: List[str] = ["killall zebra"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING diff --git a/daemon/core/configservices/nrlservices/services.py b/daemon/core/configservices/nrlservices/services.py index cf9b4c88..3f911aef 100644 --- a/daemon/core/configservices/nrlservices/services.py +++ b/daemon/core/configservices/nrlservices/services.py @@ -14,7 +14,7 @@ class MgenSinkService(ConfigService): files: List[str] = ["mgensink.sh", "sink.mgen"] executables: List[str] = ["mgen"] dependencies: List[str] = [] - startup: List[str] = ["sh mgensink.sh"] + startup: List[str] = ["bash mgensink.sh"] validate: List[str] = ["pidof mgen"] shutdown: List[str] = ["killall mgen"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING @@ -36,7 +36,7 @@ class NrlNhdp(ConfigService): files: List[str] = ["nrlnhdp.sh"] executables: List[str] = ["nrlnhdp"] dependencies: List[str] = [] - startup: List[str] = ["sh nrlnhdp.sh"] + startup: List[str] = ["bash nrlnhdp.sh"] validate: List[str] = ["pidof nrlnhdp"] shutdown: List[str] = ["killall nrlnhdp"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING @@ -58,7 +58,7 @@ class NrlSmf(ConfigService): files: List[str] = ["startsmf.sh"] executables: List[str] = ["nrlsmf", "killall"] dependencies: List[str] = [] - startup: List[str] = ["sh startsmf.sh"] + startup: List[str] = ["bash startsmf.sh"] validate: List[str] = ["pidof nrlsmf"] shutdown: List[str] = ["killall nrlsmf"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING @@ -93,7 +93,7 @@ class NrlOlsr(ConfigService): files: List[str] = ["nrlolsrd.sh"] executables: List[str] = ["nrlolsrd"] dependencies: List[str] = [] - startup: List[str] = ["sh nrlolsrd.sh"] + startup: List[str] = ["bash nrlolsrd.sh"] validate: List[str] = ["pidof nrlolsrd"] shutdown: List[str] = ["killall nrlolsrd"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING @@ -117,7 +117,7 @@ class NrlOlsrv2(ConfigService): files: List[str] = ["nrlolsrv2.sh"] executables: List[str] = ["nrlolsrv2"] dependencies: List[str] = [] - startup: List[str] = ["sh nrlolsrv2.sh"] + startup: List[str] = ["bash nrlolsrv2.sh"] validate: List[str] = ["pidof nrlolsrv2"] shutdown: List[str] = ["killall nrlolsrv2"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING @@ -139,7 +139,7 @@ class OlsrOrg(ConfigService): files: List[str] = ["olsrd.sh", "/etc/olsrd/olsrd.conf"] executables: List[str] = ["olsrd"] dependencies: List[str] = [] - startup: List[str] = ["sh olsrd.sh"] + startup: List[str] = ["bash olsrd.sh"] validate: List[str] = ["pidof olsrd"] shutdown: List[str] = ["killall olsrd"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING @@ -161,7 +161,7 @@ class MgenActor(ConfigService): files: List[str] = ["start_mgen_actor.sh"] executables: List[str] = ["mgen"] dependencies: List[str] = [] - startup: List[str] = ["sh start_mgen_actor.sh"] + startup: List[str] = ["bash start_mgen_actor.sh"] validate: List[str] = ["pidof mgen"] shutdown: List[str] = ["killall mgen"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING @@ -176,7 +176,7 @@ class Arouted(ConfigService): files: List[str] = ["startarouted.sh"] executables: List[str] = ["arouted"] dependencies: List[str] = [] - startup: List[str] = ["sh startarouted.sh"] + startup: List[str] = ["bash startarouted.sh"] validate: List[str] = ["pidof arouted"] shutdown: List[str] = ["pkill arouted"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING diff --git a/daemon/core/configservices/quaggaservices/services.py b/daemon/core/configservices/quaggaservices/services.py index 19430664..bf23e00c 100644 --- a/daemon/core/configservices/quaggaservices/services.py +++ b/daemon/core/configservices/quaggaservices/services.py @@ -65,7 +65,7 @@ class Zebra(ConfigService): ] executables: List[str] = ["zebra"] dependencies: List[str] = [] - startup: List[str] = ["sh quaggaboot.sh zebra"] + startup: List[str] = ["bash quaggaboot.sh zebra"] validate: List[str] = ["pidof zebra"] shutdown: List[str] = ["killall zebra"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING diff --git a/daemon/core/configservices/securityservices/services.py b/daemon/core/configservices/securityservices/services.py index 4a58fd8c..c656f5ca 100644 --- a/daemon/core/configservices/securityservices/services.py +++ b/daemon/core/configservices/securityservices/services.py @@ -14,7 +14,7 @@ class VpnClient(ConfigService): files: List[str] = ["vpnclient.sh"] executables: List[str] = ["openvpn", "ip", "killall"] dependencies: List[str] = [] - startup: List[str] = ["sh vpnclient.sh"] + startup: List[str] = ["bash vpnclient.sh"] validate: List[str] = ["pidof openvpn"] shutdown: List[str] = ["killall openvpn"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING @@ -48,7 +48,7 @@ class VpnServer(ConfigService): files: List[str] = ["vpnserver.sh"] executables: List[str] = ["openvpn", "ip", "killall"] dependencies: List[str] = [] - startup: List[str] = ["sh vpnserver.sh"] + startup: List[str] = ["bash vpnserver.sh"] validate: List[str] = ["pidof openvpn"] shutdown: List[str] = ["killall openvpn"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING @@ -91,7 +91,7 @@ class IPsec(ConfigService): files: List[str] = ["ipsec.sh"] executables: List[str] = ["racoon", "ip", "setkey", "killall"] dependencies: List[str] = [] - startup: List[str] = ["sh ipsec.sh"] + startup: List[str] = ["bash ipsec.sh"] validate: List[str] = ["pidof racoon"] shutdown: List[str] = ["killall racoon"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING @@ -106,7 +106,7 @@ class Firewall(ConfigService): files: List[str] = ["firewall.sh"] executables: List[str] = ["iptables"] dependencies: List[str] = [] - startup: List[str] = ["sh firewall.sh"] + startup: List[str] = ["bash firewall.sh"] validate: List[str] = [] shutdown: List[str] = [] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING @@ -121,7 +121,7 @@ class Nat(ConfigService): files: List[str] = ["nat.sh"] executables: List[str] = ["iptables"] dependencies: List[str] = [] - startup: List[str] = ["sh nat.sh"] + startup: List[str] = ["bash nat.sh"] validate: List[str] = [] shutdown: List[str] = [] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING diff --git a/daemon/core/configservices/utilservices/services.py b/daemon/core/configservices/utilservices/services.py index b6bc0eb5..9b3369db 100644 --- a/daemon/core/configservices/utilservices/services.py +++ b/daemon/core/configservices/utilservices/services.py @@ -16,7 +16,7 @@ class DefaultRouteService(ConfigService): files: List[str] = ["defaultroute.sh"] executables: List[str] = ["ip"] dependencies: List[str] = [] - startup: List[str] = ["sh defaultroute.sh"] + startup: List[str] = ["bash defaultroute.sh"] validate: List[str] = [] shutdown: List[str] = [] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING @@ -44,7 +44,7 @@ class DefaultMulticastRouteService(ConfigService): files: List[str] = ["defaultmroute.sh"] executables: List[str] = [] dependencies: List[str] = [] - startup: List[str] = ["sh defaultmroute.sh"] + startup: List[str] = ["bash defaultmroute.sh"] validate: List[str] = [] shutdown: List[str] = [] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING @@ -66,7 +66,7 @@ class StaticRouteService(ConfigService): files: List[str] = ["staticroute.sh"] executables: List[str] = [] dependencies: List[str] = [] - startup: List[str] = ["sh staticroute.sh"] + startup: List[str] = ["bash staticroute.sh"] validate: List[str] = [] shutdown: List[str] = [] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING @@ -94,7 +94,7 @@ class IpForwardService(ConfigService): files: List[str] = ["ipforward.sh"] executables: List[str] = ["sysctl"] dependencies: List[str] = [] - startup: List[str] = ["sh ipforward.sh"] + startup: List[str] = ["bash ipforward.sh"] validate: List[str] = [] shutdown: List[str] = [] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING @@ -116,7 +116,7 @@ class SshService(ConfigService): files: List[str] = ["startsshd.sh", "/etc/ssh/sshd_config"] executables: List[str] = ["sshd"] dependencies: List[str] = [] - startup: List[str] = ["sh startsshd.sh"] + startup: List[str] = ["bash startsshd.sh"] validate: List[str] = [] shutdown: List[str] = ["killall sshd"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING @@ -164,7 +164,7 @@ class DhcpClientService(ConfigService): files: List[str] = ["startdhcpclient.sh"] executables: List[str] = ["dhclient"] dependencies: List[str] = [] - startup: List[str] = ["sh startdhcpclient.sh"] + startup: List[str] = ["bash startdhcpclient.sh"] validate: List[str] = ["pidof dhclient"] shutdown: List[str] = ["killall dhclient"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING @@ -200,9 +200,9 @@ class PcapService(ConfigService): files: List[str] = ["pcap.sh"] executables: List[str] = ["tcpdump"] dependencies: List[str] = [] - startup: List[str] = ["sh pcap.sh start"] + startup: List[str] = ["bash pcap.sh start"] validate: List[str] = ["pidof tcpdump"] - shutdown: List[str] = ["sh pcap.sh stop"] + shutdown: List[str] = ["bash pcap.sh stop"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING default_configs: List[Configuration] = [] modes: Dict[str, Dict[str, str]] = {} @@ -249,7 +249,7 @@ class AtdService(ConfigService): files: List[str] = ["startatd.sh"] executables: List[str] = ["atd"] dependencies: List[str] = [] - startup: List[str] = ["sh startatd.sh"] + startup: List[str] = ["bash startatd.sh"] validate: List[str] = ["pidof atd"] shutdown: List[str] = ["pkill atd"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING diff --git a/daemon/core/executables.py b/daemon/core/executables.py index 6eb0214a..7b7f80b7 100644 --- a/daemon/core/executables.py +++ b/daemon/core/executables.py @@ -1,5 +1,6 @@ from typing import List +BASH: str = "bash" VNODED: str = "vnoded" VCMD: str = "vcmd" SYSCTL: str = "sysctl" @@ -11,7 +12,16 @@ MOUNT: str = "mount" UMOUNT: str = "umount" OVS_VSCTL: str = "ovs-vsctl" -COMMON_REQUIREMENTS: List[str] = [SYSCTL, IP, ETHTOOL, TC, EBTABLES, MOUNT, UMOUNT] +COMMON_REQUIREMENTS: List[str] = [ + BASH, + EBTABLES, + ETHTOOL, + IP, + MOUNT, + SYSCTL, + TC, + UMOUNT, +] VCMD_REQUIREMENTS: List[str] = [VNODED, VCMD] OVS_REQUIREMENTS: List[str] = [OVS_VSCTL] diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index e982c5c1..0e9b2e32 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -22,6 +22,7 @@ from core.emulator.enumerations import ( RegisterTlvs, ) from core.errors import CoreError +from core.executables import BASH from core.nodes.base import CoreNode from core.nodes.interface import CoreInterface from core.nodes.network import WlanNode @@ -1167,7 +1168,7 @@ class Ns2ScriptedMobility(WayPointMobility): if filename is None or filename == "": return filename = self.findfile(filename) - args = f"/bin/sh {filename} {typestr}" + args = f"{BASH} {filename} {typestr}" utils.cmd( args, cwd=self.session.session_dir, env=self.session.get_environment() ) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index cea1e81b..4cf6ea8d 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -599,7 +599,7 @@ class CoreNode(CoreNodeBase): if self.server is None: return self.client.check_cmd(args, wait=wait, shell=shell) else: - args = self.client.create_cmd(args) + args = self.client.create_cmd(args, shell) return self.server.remote_cmd(args, wait=wait) def termcmdstring(self, sh: str = "/bin/sh") -> str: diff --git a/daemon/core/nodes/client.py b/daemon/core/nodes/client.py index 93e099cf..710724b1 100644 --- a/daemon/core/nodes/client.py +++ b/daemon/core/nodes/client.py @@ -5,7 +5,7 @@ The control channel can be accessed via calls using the vcmd shell. """ from core import utils -from core.executables import VCMD +from core.executables import BASH, VCMD class VnodeClient: @@ -49,7 +49,9 @@ class VnodeClient: """ pass - def create_cmd(self, args: str) -> str: + def create_cmd(self, args: str, shell: bool = False) -> str: + if shell: + args = f'{BASH} -c "{args}"' return f"{VCMD} -c {self.ctrlchnlname} -- {args}" def check_cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: @@ -63,5 +65,5 @@ class VnodeClient: :raises core.CoreCommandError: when there is a non-zero exit status """ self._verify_connection() - args = self.create_cmd(args) + args = self.create_cmd(args, shell) return utils.cmd(args, wait=wait, shell=shell) diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 7f33973e..20dc8fd3 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -383,12 +383,12 @@ class Veth(CoreInterface): try: self.node.node_net_client.device_flush(self.name) except CoreCommandError: - logging.exception("error shutting down interface") + pass if self.localname: try: self.net_client.delete_device(self.localname) except CoreCommandError: - logging.info("link already removed: %s", self.localname) + pass self.up = False diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 96a1f4be..68fbef98 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -121,11 +121,7 @@ class LinuxNetClient: :param device: device to flush :return: nothing """ - self.run( - f"[ -e /sys/class/net/{device} ] && " - f"{IP} address flush dev {device} || true", - shell=True, - ) + self.run(f"{IP} address flush dev {device}") def device_mac(self, device: str, mac: str) -> None: """ diff --git a/daemon/core/services/emaneservices.py b/daemon/core/services/emaneservices.py index d694317a..4fd78ec1 100644 --- a/daemon/core/services/emaneservices.py +++ b/daemon/core/services/emaneservices.py @@ -13,7 +13,7 @@ class EmaneTransportService(CoreService): dependencies: Tuple[str, ...] = () dirs: Tuple[str, ...] = () configs: Tuple[str, ...] = ("emanetransport.sh",) - startup: Tuple[str, ...] = (f"sh {configs[0]}",) + startup: Tuple[str, ...] = (f"bash {configs[0]}",) validate: Tuple[str, ...] = (f"pidof {executables[0]}",) validation_timer: float = 0.5 shutdown: Tuple[str, ...] = (f"killall {executables[0]}",) diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py index b130fd8c..cec9d860 100644 --- a/daemon/core/services/frr.py +++ b/daemon/core/services/frr.py @@ -26,7 +26,7 @@ class FRRZebra(CoreService): "/usr/local/etc/frr/vtysh.conf", "/usr/local/etc/frr/daemons", ) - startup: Tuple[str, ...] = ("sh frrboot.sh zebra",) + startup: Tuple[str, ...] = ("bash frrboot.sh zebra",) shutdown: Tuple[str, ...] = ("killall zebra",) validate: Tuple[str, ...] = ("pidof zebra",) diff --git a/daemon/core/services/nrl.py b/daemon/core/services/nrl.py index 697f4eee..91e053b2 100644 --- a/daemon/core/services/nrl.py +++ b/daemon/core/services/nrl.py @@ -97,7 +97,7 @@ class NrlSmf(NrlService): name: str = "SMF" executables: Tuple[str, ...] = ("nrlsmf",) - startup: Tuple[str, ...] = ("sh startsmf.sh",) + startup: Tuple[str, ...] = ("bash startsmf.sh",) shutdown: Tuple[str, ...] = ("killall nrlsmf",) validate: Tuple[str, ...] = ("pidof nrlsmf",) configs: Tuple[str, ...] = ("startsmf.sh",) @@ -566,7 +566,7 @@ class MgenActor(NrlService): group: str = "ProtoSvc" executables: Tuple[str, ...] = ("mgen",) configs: Tuple[str, ...] = ("start_mgen_actor.sh",) - startup: Tuple[str, ...] = ("sh start_mgen_actor.sh",) + startup: Tuple[str, ...] = ("bash start_mgen_actor.sh",) validate: Tuple[str, ...] = ("pidof mgen",) shutdown: Tuple[str, ...] = ("killall mgen",) @@ -596,7 +596,7 @@ class Arouted(NrlService): name: str = "arouted" executables: Tuple[str, ...] = ("arouted",) configs: Tuple[str, ...] = ("startarouted.sh",) - startup: Tuple[str, ...] = ("sh startarouted.sh",) + startup: Tuple[str, ...] = ("bash startarouted.sh",) shutdown: Tuple[str, ...] = ("pkill arouted",) validate: Tuple[str, ...] = ("pidof arouted",) diff --git a/daemon/core/services/quagga.py b/daemon/core/services/quagga.py index 9e2c7cc0..8c474fd8 100644 --- a/daemon/core/services/quagga.py +++ b/daemon/core/services/quagga.py @@ -25,7 +25,7 @@ class Zebra(CoreService): "quaggaboot.sh", "/usr/local/etc/quagga/vtysh.conf", ) - startup: Tuple[str, ...] = ("sh quaggaboot.sh zebra",) + startup: Tuple[str, ...] = ("bash quaggaboot.sh zebra",) shutdown: Tuple[str, ...] = ("killall zebra",) validate: Tuple[str, ...] = ("pidof zebra",) diff --git a/daemon/core/services/sdn.py b/daemon/core/services/sdn.py index ef077662..e72b5138 100644 --- a/daemon/core/services/sdn.py +++ b/daemon/core/services/sdn.py @@ -31,7 +31,7 @@ class OvsService(SdnService): "/var/log/openvswitch", ) configs: Tuple[str, ...] = ("OvsService.sh",) - startup: Tuple[str, ...] = ("sh OvsService.sh",) + startup: Tuple[str, ...] = ("bash OvsService.sh",) shutdown: Tuple[str, ...] = ("killall ovs-vswitchd", "killall ovsdb-server") @classmethod @@ -119,7 +119,7 @@ class RyuService(SdnService): group: str = "SDN" executables: Tuple[str, ...] = ("ryu-manager",) configs: Tuple[str, ...] = ("ryuService.sh",) - startup: Tuple[str, ...] = ("sh ryuService.sh",) + startup: Tuple[str, ...] = ("bash ryuService.sh",) shutdown: Tuple[str, ...] = ("killall ryu-manager",) @classmethod diff --git a/daemon/core/services/security.py b/daemon/core/services/security.py index b813579e..788988c9 100644 --- a/daemon/core/services/security.py +++ b/daemon/core/services/security.py @@ -16,7 +16,7 @@ class VPNClient(CoreService): name: str = "VPNClient" group: str = "Security" configs: Tuple[str, ...] = ("vpnclient.sh",) - startup: Tuple[str, ...] = ("sh vpnclient.sh",) + startup: Tuple[str, ...] = ("bash vpnclient.sh",) shutdown: Tuple[str, ...] = ("killall openvpn",) validate: Tuple[str, ...] = ("pidof openvpn",) custom_needed: bool = True @@ -43,7 +43,7 @@ class VPNServer(CoreService): name: str = "VPNServer" group: str = "Security" configs: Tuple[str, ...] = ("vpnserver.sh",) - startup: Tuple[str, ...] = ("sh vpnserver.sh",) + startup: Tuple[str, ...] = ("bash vpnserver.sh",) shutdown: Tuple[str, ...] = ("killall openvpn",) validate: Tuple[str, ...] = ("pidof openvpn",) custom_needed: bool = True @@ -71,7 +71,7 @@ class IPsec(CoreService): name: str = "IPsec" group: str = "Security" configs: Tuple[str, ...] = ("ipsec.sh",) - startup: Tuple[str, ...] = ("sh ipsec.sh",) + startup: Tuple[str, ...] = ("bash ipsec.sh",) shutdown: Tuple[str, ...] = ("killall racoon",) custom_needed: bool = True @@ -97,7 +97,7 @@ class Firewall(CoreService): name: str = "Firewall" group: str = "Security" configs: Tuple[str, ...] = ("firewall.sh",) - startup: Tuple[str, ...] = ("sh firewall.sh",) + startup: Tuple[str, ...] = ("bash firewall.sh",) custom_needed: bool = True @classmethod @@ -127,7 +127,7 @@ class Nat(CoreService): group: str = "Security" executables: Tuple[str, ...] = ("iptables",) configs: Tuple[str, ...] = ("nat.sh",) - startup: Tuple[str, ...] = ("sh nat.sh",) + startup: Tuple[str, ...] = ("bash nat.sh",) custom_needed: bool = False @classmethod diff --git a/daemon/core/services/ucarp.py b/daemon/core/services/ucarp.py index 8ac92dd3..522eeaf6 100644 --- a/daemon/core/services/ucarp.py +++ b/daemon/core/services/ucarp.py @@ -19,7 +19,7 @@ class Ucarp(CoreService): UCARP_ETC + "/default-down.sh", "ucarpboot.sh", ) - startup: Tuple[str, ...] = ("sh ucarpboot.sh",) + startup: Tuple[str, ...] = ("bash ucarpboot.sh",) shutdown: Tuple[str, ...] = ("killall ucarp",) validate: Tuple[str, ...] = ("pidof ucarp",) diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index 774c4104..a30d1f62 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -28,7 +28,7 @@ class UtilService(CoreService): class IPForwardService(UtilService): name: str = "IPForward" configs: Tuple[str, ...] = ("ipforward.sh",) - startup: Tuple[str, ...] = ("sh ipforward.sh",) + startup: Tuple[str, ...] = ("bash ipforward.sh",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: @@ -61,7 +61,7 @@ class IPForwardService(UtilService): class DefaultRouteService(UtilService): name: str = "DefaultRoute" configs: Tuple[str, ...] = ("defaultroute.sh",) - startup: Tuple[str, ...] = ("sh defaultroute.sh",) + startup: Tuple[str, ...] = ("bash defaultroute.sh",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: @@ -84,7 +84,7 @@ class DefaultRouteService(UtilService): class DefaultMulticastRouteService(UtilService): name: str = "DefaultMulticastRoute" configs: Tuple[str, ...] = ("defaultmroute.sh",) - startup: Tuple[str, ...] = ("sh defaultmroute.sh",) + startup: Tuple[str, ...] = ("bash defaultmroute.sh",) @classmethod def generate_config(cls, node: CoreNode, filename: str) -> str: @@ -103,7 +103,7 @@ class DefaultMulticastRouteService(UtilService): class StaticRouteService(UtilService): name: str = "StaticRoute" configs: Tuple[str, ...] = ("staticroute.sh",) - startup: Tuple[str, ...] = ("sh staticroute.sh",) + startup: Tuple[str, ...] = ("bash staticroute.sh",) custom_needed: bool = True @classmethod @@ -135,7 +135,7 @@ class SshService(UtilService): name: str = "SSH" configs: Tuple[str, ...] = ("startsshd.sh", "/etc/ssh/sshd_config") dirs: Tuple[str, ...] = ("/etc/ssh", "/var/run/sshd") - startup: Tuple[str, ...] = ("sh startsshd.sh",) + startup: Tuple[str, ...] = ("bash startsshd.sh",) shutdown: Tuple[str, ...] = ("killall sshd",) validation_mode: ServiceMode = ServiceMode.BLOCKING @@ -278,7 +278,7 @@ class DhcpClientService(UtilService): name: str = "DHCPClient" configs: Tuple[str, ...] = ("startdhcpclient.sh",) - startup: Tuple[str, ...] = ("sh startdhcpclient.sh",) + startup: Tuple[str, ...] = ("bash startdhcpclient.sh",) shutdown: Tuple[str, ...] = ("killall dhclient",) validate: Tuple[str, ...] = ("pidof dhclient",) @@ -561,8 +561,8 @@ class PcapService(UtilService): name: str = "pcap" configs: Tuple[str, ...] = ("pcap.sh",) - startup: Tuple[str, ...] = ("sh pcap.sh start",) - shutdown: Tuple[str, ...] = ("sh pcap.sh stop",) + startup: Tuple[str, ...] = ("bash pcap.sh start",) + shutdown: Tuple[str, ...] = ("bash pcap.sh stop",) validate: Tuple[str, ...] = ("pidof tcpdump",) meta: str = "logs network traffic to pcap packet capture files" @@ -671,7 +671,7 @@ class AtdService(UtilService): name: str = "atd" configs: Tuple[str, ...] = ("startatd.sh",) dirs: Tuple[str, ...] = ("/var/spool/cron/atjobs", "/var/spool/cron/atspool") - startup: Tuple[str, ...] = ("sh startatd.sh",) + startup: Tuple[str, ...] = ("bash startatd.sh",) shutdown: Tuple[str, ...] = ("pkill atd",) @classmethod diff --git a/tasks.py b/tasks.py index 5f52f444..c3e6d2bb 100644 --- a/tasks.py +++ b/tasks.py @@ -120,14 +120,14 @@ def install_system(c: Context, os_info: OsInfo, hide: bool) -> None: if os_info.like == OsLike.DEBIAN: c.run( "sudo apt install -y automake pkg-config gcc libev-dev ebtables " - "iproute2 ethtool tk python3-tk", + "iproute2 ethtool tk python3-tk bash", hide=hide ) elif os_info.like == OsLike.REDHAT: c.run( "sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ " "libev-devel iptables-ebtables iproute python3-devel python3-tkinter " - "tk ethtool make", + "tk ethtool make bash", hide=hide ) # centos 8+ does not support netem by default From 9e3e0e0326345b8efb07622f5f383728153ce7bd Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 28 Jul 2020 16:34:05 -0700 Subject: [PATCH 0553/1131] install: fixed issue identifying python versions to install dataclasses for, using ~ should account for any version up to 3.7 properly --- daemon/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml index b75f1ee3..7ce3a125 100644 --- a/daemon/pyproject.toml +++ b/daemon/pyproject.toml @@ -10,7 +10,7 @@ include = ["core/gui/data/**/*", "core/configservices/*/templates"] [tool.poetry.dependencies] python = "^3.6" -dataclasses = { version = "*", python = "3.6" } +dataclasses = { version = "*", python = "~3.6" } fabric = "*" grpcio = "1.27.2" invoke = "*" From 511a3037a8d0cc09b36e7d07849dbe08ea53fd18 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 28 Jul 2020 16:35:46 -0700 Subject: [PATCH 0554/1131] bumped versions for release --- configure.ac | 2 +- daemon/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index 60f6709e..6ed18b69 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.0.0) +AC_INIT(core, 7.0.1) # autoconf and automake initialization AC_CONFIG_SRCDIR([netns/version.h.in]) diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml index 7ce3a125..dec01670 100644 --- a/daemon/pyproject.toml +++ b/daemon/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "core" -version = "7.0.0" +version = "7.0.1" description = "CORE Common Open Research Emulator" authors = ["Boeing Research and Technology"] license = "BSD-2-Clause" From afe434f25cbf2e01a89fb75f4c914c514675a55e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 28 Jul 2020 16:39:16 -0700 Subject: [PATCH 0555/1131] updated changelog for bugfix release 7.0.1 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 375a7607..21eb0a57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2020-07-28 CORE 7.0.1 + +* Bugfixes + * \#500 - fixed issue running node commands with shell=True + * fixed issue for poetry based install not properly vetting requirements for dataclasses dependency + ## 2020-07-23 CORE 7.0.0 * Breaking Changes From 858e771efd8f040a18c7e222e4dd971536afe145 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 28 Jul 2020 21:49:34 -0700 Subject: [PATCH 0556/1131] pygui: fixes for copying links/asymmetric links, fixes for configuring asymmetric links, fixed issues adding nodes/links and editing links from gui due to not being able to identify same source changes --- daemon/core/gui/coreclient.py | 56 +++++++++++++++++++------ daemon/core/gui/dialogs/linkconfig.py | 37 ++++------------- daemon/core/gui/graph/graph.py | 59 ++++++++++++++++++--------- 3 files changed, 91 insertions(+), 61 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 6129031a..9fe8130a 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -474,21 +474,22 @@ class CoreClient: self.ifaces_manager.reset_mac() nodes = [x.to_proto() for x in self.session.nodes.values()] links = [] - for link in self.session.links: + asymmetric_links = [] + for edge in self.links.values(): + link = edge.link if link.iface1 and not link.iface1.mac: link.iface1.mac = self.ifaces_manager.next_mac() if link.iface2 and not link.iface2.mac: link.iface2.mac = self.ifaces_manager.next_mac() links.append(link.to_proto()) + if edge.asymmetric_link: + asymmetric_links.append(edge.asymmetric_link.to_proto()) wlan_configs = self.get_wlan_configs_proto() mobility_configs = self.get_mobility_configs_proto() emane_model_configs = self.get_emane_model_configs_proto() hooks = [x.to_proto() for x in self.session.hooks.values()] service_configs = self.get_service_configs_proto() file_configs = self.get_service_file_configs_proto() - asymmetric_links = [ - x.asymmetric_link for x in self.links.values() if x.asymmetric_link - ] config_service_configs = self.get_config_service_configs_proto() emane_config = to_dict(self.session.emane_config) result = False @@ -686,17 +687,32 @@ class CoreClient: for node in self.session.nodes.values(): response = self.client.add_node(self.session.id, node.to_proto()) logging.debug("created node: %s", response) - for link in self.session.links: - link_proto = link.to_proto() + asymmetric_links = [] + for edge in self.links.values(): + link = edge.link response = self.client.add_link( self.session.id, - link_proto.node1_id, - link_proto.node2_id, - link_proto.iface1, - link_proto.iface2, - link_proto.options, + link.node1_id, + link.node2_id, + link.iface1, + link.iface2, + link.options, + source=GUI_SOURCE, ) logging.debug("created link: %s", response) + if edge.asymmetric_link: + asymmetric_links.append(edge.asymmetric_link) + for link in asymmetric_links: + response = self.client.add_link( + self.session.id, + link.node1_id, + link.node2_id, + link.iface1, + link.iface2, + link.options, + source=GUI_SOURCE, + ) + logging.debug("created asymmetric link: %s", response) def send_data(self) -> None: """ @@ -892,8 +908,7 @@ class CoreClient: ) edge.set_link(link) self.links[edge.token] = edge - self.session.links.append(link) - logging.info("Add link between %s and %s", src_node.name, dst_node.name) + logging.info("added link between %s and %s", src_node.name, dst_node.name) def get_wlan_configs_proto(self) -> List[wlan_pb2.WlanConfig]: configs = [] @@ -1039,3 +1054,18 @@ class CoreClient: logging.info("execute python script %s", response) if response.session_id != -1: self.join_session(response.session_id) + + def edit_link(self, link: Link) -> None: + iface1_id = link.iface1.id if link.iface1 else None + iface2_id = link.iface2.id if link.iface2 else None + response = self.client.edit_link( + self.session.id, + link.node1_id, + link.node2_id, + link.options.to_proto(), + iface1_id, + iface2_id, + source=GUI_SOURCE, + ) + if not response.result: + logging.error("error editing link: %s", link) diff --git a/daemon/core/gui/dialogs/linkconfig.py b/daemon/core/gui/dialogs/linkconfig.py index 2a91da30..914bad1e 100644 --- a/daemon/core/gui/dialogs/linkconfig.py +++ b/daemon/core/gui/dialogs/linkconfig.py @@ -228,21 +228,15 @@ class LinkConfigurationDialog(Dialog): bandwidth=bandwidth, jitter=jitter, delay=delay, dup=duplicate, loss=loss ) link.options = options - - iface1_id = None - if link.iface1: - iface1_id = link.iface1.id - iface2_id = None - if link.iface2: - iface2_id = link.iface2.id - + iface1_id = link.iface1.id if link.iface1 else None + iface2_id = link.iface2.id if link.iface2 else None if not self.is_symmetric: link.options.unidirectional = True asym_iface1 = None - if iface1_id: + if iface1_id is not None: asym_iface1 = Interface(id=iface1_id) asym_iface2 = None - if iface2_id: + if iface2_id is not None: asym_iface2 = Interface(id=iface2_id) down_bandwidth = get_int(self.down_bandwidth) down_jitter = get_int(self.down_jitter) @@ -260,8 +254,8 @@ class LinkConfigurationDialog(Dialog): self.edge.asymmetric_link = Link( node1_id=link.node2_id, node2_id=link.node1_id, - iface1=asym_iface1, - iface2=asym_iface2, + iface1=asym_iface2, + iface2=asym_iface1, options=options, ) else: @@ -269,24 +263,9 @@ class LinkConfigurationDialog(Dialog): self.edge.asymmetric_link = None if self.app.core.is_runtime() and link.options: - session_id = self.app.core.session.id - self.app.core.client.edit_link( - session_id, - link.node1_id, - link.node2_id, - link.options, - iface1_id, - iface2_id, - ) + self.app.core.edit_link(link) if self.edge.asymmetric_link: - self.app.core.client.edit_link( - session_id, - link.node2_id, - link.node1_id, - self.edge.asymmetric_link.options, - iface1_id, - iface2_id, - ) + self.app.core.edit_link(self.edge.asymmetric_link) # update edge label self.edge.draw_link_options() diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 69ae87cc..bb762bb8 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -262,7 +262,7 @@ class CanvasGraph(tk.Canvas): edge = self.edges.get(token) if not edge: return - edge.link.options.CopyFrom(link.options) + edge.link.options = deepcopy(link.options) def add_wireless_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None: network_id = link.network_id if link.network_id else None @@ -924,7 +924,8 @@ class CanvasGraph(tk.Canvas): # maps original node canvas id to copy node canvas id copy_map = {} # the edges that will be copy over - to_copy_edges = [] + to_copy_edges = set() + to_copy_ids = {x.id for x in self.to_copy} for canvas_node in self.to_copy: core_node = canvas_node.core_node actual_x = core_node.position.x + 50 @@ -954,15 +955,39 @@ class CanvasGraph(tk.Canvas): self.nodes[node.id] = node self.core.set_canvas_node(copy, node) for edge in canvas_node.edges: - if edge.src not in self.to_copy or edge.dst not in self.to_copy: + 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) 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_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 + options = edge.link.options + if options: + copy_edge.link.options = deepcopy(options) + if options and options.unidirectional: + asym_iface1 = None + if iface1_id is not None: + asym_iface1 = Interface(id=iface1_id) + asym_iface2 = None + if iface2_id is not None: + asym_iface2 = Interface(id=iface2_id) + copy_edge.asymmetric_link = Link( + node1_id=copy_link.node2_id, + node2_id=copy_link.node1_id, + iface1=asym_iface2, + iface2=asym_iface1, + options=deepcopy(edge.asymmetric_link.options), + ) + copy_edge.redraw() else: - to_copy_edges.append(edge) + to_copy_edges.add(edge) # copy link and link config for edge in to_copy_edges: @@ -974,30 +999,26 @@ class CanvasGraph(tk.Canvas): token = create_edge_token(src_node_copy.id, dst_node_copy.id) copy_edge = self.edges[token] 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 options = edge.link.options - copy_link.options = deepcopy(options) - iface1_id = None - if copy_link.iface1: - iface1_id = copy_link.iface1.id - iface2_id = None - if copy_link.iface2: - iface2_id = copy_link.iface2.id - if not options.unidirectional: - copy_edge.asymmetric_link = None - else: + if options: + copy_link.options = deepcopy(options) + if options and options.unidirectional: asym_iface1 = None - if iface1_id: + if iface1_id is not None: asym_iface1 = Interface(id=iface1_id) asym_iface2 = None - if iface2_id: + if iface2_id is not None: asym_iface2 = Interface(id=iface2_id) copy_edge.asymmetric_link = Link( node1_id=copy_link.node2_id, node2_id=copy_link.node1_id, - iface1=asym_iface1, - iface2=asym_iface2, - options=edge.asymmetric_link.options, + iface1=asym_iface2, + iface2=asym_iface1, + options=deepcopy(edge.asymmetric_link.options), ) + copy_edge.redraw() self.itemconfig( copy_edge.id, width=self.itemcget(edge.id, "width"), From fe36d28522f31559aa27f913db4d4bb23d867fc6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 28 Jul 2020 22:45:42 -0700 Subject: [PATCH 0557/1131] pygui: fixed issue with changes to protobuf files for getting emane model configs on a session --- daemon/core/api/grpc/grpcutils.py | 6 +++--- daemon/core/gui/coreclient.py | 2 ++ daemon/proto/core/api/grpc/core.proto | 2 +- daemon/proto/core/api/grpc/emane.proto | 9 ++++++++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index a024c064..51be85fe 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -10,7 +10,7 @@ from core import utils from core.api.grpc import common_pb2, core_pb2 from core.api.grpc.common_pb2 import MappedConfig from core.api.grpc.configservices_pb2 import ConfigServiceConfig -from core.api.grpc.emane_pb2 import EmaneModelConfig +from core.api.grpc.emane_pb2 import GetEmaneModelConfig from core.api.grpc.services_pb2 import ( NodeServiceConfig, NodeServiceData, @@ -547,7 +547,7 @@ def get_nem_id( return nem_id -def get_emane_model_configs(session: Session) -> List[EmaneModelConfig]: +def get_emane_model_configs(session: Session) -> List[GetEmaneModelConfig]: configs = [] for _id in session.emane.node_configurations: if _id == -1: @@ -558,7 +558,7 @@ def get_emane_model_configs(session: Session) -> List[EmaneModelConfig]: current_config = session.emane.get_model_config(_id, model_name) config = get_config_options(current_config, model) node_id, iface_id = parse_emane_model_id(_id) - model_config = EmaneModelConfig( + model_config = GetEmaneModelConfig( node_id=node_id, model=model_name, iface_id=iface_id, config=config ) configs.append(model_config) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 9fe8130a..b30a265b 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -404,6 +404,8 @@ class CoreClient: self.app.show_grpc_exception("New Session Error", e) def delete_session(self, session_id: int = None) -> None: + if session_id is None and not self.session: + return if session_id is None: session_id = self.session.id try: diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 1b20257c..d5ffda59 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -720,7 +720,7 @@ message Session { repeated Hook hooks = 9; repeated string emane_models = 10; map emane_config = 11; - repeated emane.EmaneModelConfig emane_model_configs = 12; + repeated emane.GetEmaneModelConfig emane_model_configs = 12; map wlan_configs = 13; repeated services.NodeServiceConfig service_configs = 14; repeated configservices.ConfigServiceConfig config_service_configs = 15; diff --git a/daemon/proto/core/api/grpc/emane.proto b/daemon/proto/core/api/grpc/emane.proto index ce9a4297..ad6a22ca 100644 --- a/daemon/proto/core/api/grpc/emane.proto +++ b/daemon/proto/core/api/grpc/emane.proto @@ -53,8 +53,15 @@ message GetEmaneModelConfigsRequest { int32 session_id = 1; } +message GetEmaneModelConfig { + int32 node_id = 1; + string model = 2; + int32 iface_id = 3; + map config = 4; +} + message GetEmaneModelConfigsResponse { - repeated EmaneModelConfig configs = 1; + repeated GetEmaneModelConfig configs = 1; } message GetEmaneEventChannelRequest { From d30778b2382ec9eb480c5c0552c0ecbbabfd26a6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 29 Jul 2020 16:55:42 -0700 Subject: [PATCH 0558/1131] daemon: fixed mobility checks to allow both wlan/emane, pygui: enabled emane nodes to configure mobility --- daemon/core/api/grpc/grpcutils.py | 14 ++++++ daemon/core/api/grpc/server.py | 6 ++- daemon/core/gui/coreclient.py | 4 +- daemon/core/gui/graph/node.py | 4 +- daemon/core/gui/nodeutils.py | 5 +++ daemon/core/location/mobility.py | 74 +++++++++++++++---------------- 6 files changed, 65 insertions(+), 42 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 51be85fe..8f666508 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -22,9 +22,11 @@ from core.emane.nodes import EmaneNet from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions from core.emulator.enumerations import LinkTypes, NodeTypes from core.emulator.session import Session +from core.errors import CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.nodes.base import CoreNode, CoreNodeBase, NodeBase from core.nodes.interface import CoreInterface +from core.nodes.network import WlanNode from core.services.coreservices import CoreService WORKERS = 10 @@ -661,3 +663,15 @@ def get_node_config_service_configs(session: Session) -> List[ConfigServiceConfi def get_emane_config(session: Session) -> Dict[str, common_pb2.ConfigOption]: current_config = session.emane.get_configs() return get_config_options(current_config, session.emane.emane_config) + + +def get_mobility_node( + session: Session, node_id: int, context: ServicerContext +) -> Union[WlanNode, EmaneNet]: + try: + return session.get_node(node_id, WlanNode) + except CoreError: + try: + return session.get_node(node_id, EmaneNet) + except CoreError: + context.abort(grpc.StatusCode.NOT_FOUND, "node id is not for wlan or emane") diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index cd9cf714..81f4335e 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -1125,7 +1125,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("mobility action: %s", request) session = self.get_session(request.session_id, context) - node = self.get_node(session, request.node_id, context, WlanNode) + node = grpcutils.get_mobility_node(session, request.node_id, context) + if not node.mobility: + context.abort( + grpc.StatusCode.NOT_FOUND, f"node({node.name}) does not have mobility" + ) result = True if request.action == MobilityAction.START: node.mobility.start() diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index b30a265b..bb6c4f95 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -538,7 +538,7 @@ class CoreClient: def show_mobility_players(self) -> None: for node in self.session.nodes.values(): - if node.type != NodeType.WIRELESS_LAN: + if not NodeUtils.is_mobility(node): continue if node.mobility_config: mobility_player = MobilityPlayer(self.app, node) @@ -927,7 +927,7 @@ class CoreClient: def get_mobility_configs_proto(self) -> List[mobility_pb2.MobilityConfig]: configs = [] for node in self.session.nodes.values(): - if node.type != NodeType.WIRELESS_LAN: + if not NodeUtils.is_mobility(node): continue if not node.mobility_config: continue diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index ffc72fbf..100404ef 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -206,6 +206,7 @@ class CanvasNode: self.context.delete(0, tk.END) is_wlan = self.core_node.type == NodeType.WIRELESS_LAN is_emane = self.core_node.type == NodeType.EMANE + is_mobility = is_wlan or is_emane if self.app.core.is_runtime(): self.context.add_command(label="Configure", command=self.show_config) if is_emane: @@ -216,7 +217,7 @@ class CanvasNode: self.context.add_command( label="WLAN Config", command=self.show_wlan_config ) - if is_wlan and self.core_node.id in self.app.core.mobility_players: + if is_mobility and self.core_node.id in self.app.core.mobility_players: self.context.add_command( label="Mobility Player", command=self.show_mobility_player ) @@ -235,6 +236,7 @@ class CanvasNode: self.context.add_command( label="WLAN Config", command=self.show_wlan_config ) + if is_mobility: self.context.add_command( label="Mobility Config", command=self.show_mobility_config ) diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index 6c451303..8cba5bf0 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -63,10 +63,15 @@ class NodeUtils: WIRELESS_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE} RJ45_NODES: Set[NodeType] = {NodeType.RJ45} 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_mobility(cls, node: Node) -> bool: + return node.type in cls.MOBILITY_NODES + @classmethod def is_router_node(cls, node: Node) -> bool: return cls.is_model_node(node.type) and node.model in cls.ROUTER_NODES diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 0e9b2e32..ce422277 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -9,10 +9,11 @@ import threading import time from functools import total_ordering from pathlib import Path -from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, Union from core import utils from core.config import ConfigGroup, ConfigurableOptions, Configuration, ModelManager +from core.emane.nodes import EmaneNet from core.emulator.data import EventData, LinkData, LinkOptions from core.emulator.enumerations import ( ConfigDataTypes, @@ -31,6 +32,13 @@ if TYPE_CHECKING: from core.emulator.session import Session +def get_mobility_node(session: "Session", node_id: int) -> Union[WlanNode, EmaneNet]: + try: + return session.get_node(node_id, WlanNode) + except CoreError: + return session.get_node(node_id, EmaneNet) + + class MobilityManager(ModelManager): """ Member of session class for handling configuration data for mobility and @@ -69,30 +77,25 @@ class MobilityManager(ModelManager): """ if node_ids is None: node_ids = self.nodes() - for node_id in node_ids: - logging.debug("checking mobility startup for node: %s", node_id) logging.debug( - "node mobility configurations: %s", self.get_all_configs(node_id) + "node(%s) mobility startup: %s", node_id, self.get_all_configs(node_id) ) - try: - node = self.session.get_node(node_id, WlanNode) + node = get_mobility_node(self.session, node_id) + # TODO: may be an issue if there are multiple mobility models + for model in self.models.values(): + config = self.get_configs(node_id, model.name) + if not config: + continue + self.set_model(node, model, config) + if node.mobility: + self.session.event_loop.add_event(0.0, node.mobility.startup) except CoreError: + logging.exception("mobility startup error") logging.warning( "skipping mobility configuration for unknown node: %s", node_id ) - continue - - for model_name in self.models: - config = self.get_configs(node_id, model_name) - if not config: - continue - model_class = self.models[model_name] - self.set_model(node, model_class, config) - - if node.mobility: - self.session.event_loop.add_event(0.0, node.mobility.startup) def handleevent(self, event_data: EventData) -> None: """ @@ -106,40 +109,35 @@ class MobilityManager(ModelManager): node_id = event_data.node name = event_data.name try: - node = self.session.get_node(node_id, WlanNode) + node = get_mobility_node(self.session, node_id) except CoreError: logging.exception( - "Ignoring event for model '%s', unknown node '%s'", name, node_id + "ignoring event for model(%s), unknown node(%s)", name, node_id ) return # name is e.g. "mobility:ns2script" models = name[9:].split(",") for model in models: - try: - cls = self.models[model] - except KeyError: - logging.warning("Ignoring event for unknown model '%s'", model) + cls = self.models.get(model) + if not cls: + logging.warning("ignoring event for unknown model '%s'", model) continue - if cls.config_type in [RegisterTlvs.WIRELESS, RegisterTlvs.MOBILITY]: model = node.mobility else: continue - if model is None: - logging.warning("Ignoring event, %s has no model", node.name) + logging.warning("ignoring event, %s has no model", node.name) continue - if cls.name != model.name: logging.warning( - "Ignoring event for %s wrong model %s,%s", + "ignoring event for %s wrong model %s,%s", node.name, cls.name, model.name, ) continue - if event_type in [EventTypes.STOP, EventTypes.RESTART]: model.stop(move_initial=True) if event_type in [EventTypes.START, EventTypes.RESTART]: @@ -162,11 +160,9 @@ class MobilityManager(ModelManager): event_type = EventTypes.START elif model.state == model.STATE_PAUSED: event_type = EventTypes.PAUSE - start_time = int(model.lasttime - model.timezero) end_time = int(model.endtime) data = f"start={start_time} end={end_time}" - event_data = EventData( node=model.id, event_type=event_type, @@ -174,7 +170,6 @@ class MobilityManager(ModelManager): data=data, time=str(time.monotonic()), ) - self.session.broadcast_event(event_data) def updatewlans( @@ -593,7 +588,7 @@ class WayPointMobility(WirelessModel): self.lasttime: Optional[float] = None self.endtime: Optional[int] = None self.timezero: float = 0.0 - self.wlan: WlanNode = session.get_node(_id, WlanNode) + self.net: Union[WlanNode, EmaneNet] = get_mobility_node(self.session, self.id) # these are really set in child class via confmatrix self.loop: bool = False self.refresh_ms: int = 50 @@ -601,6 +596,9 @@ class WayPointMobility(WirelessModel): # (ns-3 sets this to False as new waypoints may be added from trace) self.empty_queue_stop: bool = True + def startup(self): + raise NotImplementedError + def runround(self) -> None: """ Advance script time and move nodes. @@ -643,7 +641,7 @@ class WayPointMobility(WirelessModel): # only move interfaces attached to self.wlan, or all nodenum in script? moved = [] moved_ifaces = [] - for iface in self.wlan.get_ifaces(): + for iface in self.net.get_ifaces(): node = iface.node if self.movenode(node, dt): moved.append(node) @@ -723,7 +721,7 @@ class WayPointMobility(WirelessModel): """ moved = [] moved_ifaces = [] - for iface in self.wlan.get_ifaces(): + for iface in self.net.get_ifaces(): node = iface.node if node.id not in self.initial: continue @@ -1094,7 +1092,7 @@ class Ns2ScriptedMobility(WayPointMobility): :return: nothing """ if self.autostart == "": - logging.info("not auto-starting ns-2 script for %s", self.wlan.name) + logging.info("not auto-starting ns-2 script for %s", self.net.name) return try: t = float(self.autostart) @@ -1102,11 +1100,11 @@ class Ns2ScriptedMobility(WayPointMobility): logging.exception( "Invalid auto-start seconds specified '%s' for %s", self.autostart, - self.wlan.name, + self.net.name, ) return self.movenodesinitial() - logging.info("scheduling ns-2 script for %s autostart at %s", self.wlan.name, t) + logging.info("scheduling ns-2 script for %s autostart at %s", self.net.name, t) self.state = self.STATE_RUNNING self.session.event_loop.add_event(t, self.run) From 46f896925c67e5a4bc261270d35c7b82bab389a8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 29 Jul 2020 17:08:20 -0700 Subject: [PATCH 0559/1131] daemon: fixed mobility manager updates to support emane/wlan --- daemon/core/emane/emanemodel.py | 4 ++-- daemon/core/location/mobility.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index 8672163d..0ee9aa40 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -130,8 +130,8 @@ class EmaneModel(WirelessModel): :return: nothing """ try: - wlan = self.session.get_node(self.id, EmaneNet) - wlan.setnempositions(moved_ifaces) + emane_net = self.session.get_node(self.id, EmaneNet) + emane_net.setnempositions(moved_ifaces) except CoreError: logging.exception("error during update") diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index ce422277..a548433c 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -100,7 +100,7 @@ class MobilityManager(ModelManager): def handleevent(self, event_data: EventData) -> None: """ Handle an Event Message used to start, stop, or pause - mobility scripts for a given WlanNode. + mobility scripts for a given mobility network. :param event_data: event data to handle :return: nothing @@ -172,12 +172,12 @@ class MobilityManager(ModelManager): ) self.session.broadcast_event(event_data) - def updatewlans( + 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 WlanNode. This saves range calculations if the model + Update every mobility network. This saves range calculations if the model were to recalculate for each individual node movement. :param moved: moved nodes @@ -186,11 +186,11 @@ class MobilityManager(ModelManager): """ for node_id in self.nodes(): try: - node = self.session.get_node(node_id, WlanNode) + node = get_mobility_node(self.session, node_id) + if node.model: + node.model.update(moved, moved_ifaces) except CoreError: - continue - if node.model: - node.model.update(moved, moved_ifaces) + logging.exception("error updating mobility node") class WirelessModel(ConfigurableOptions): @@ -648,7 +648,7 @@ class WayPointMobility(WirelessModel): moved_ifaces.append(iface) # calculate all ranges after moving nodes; this saves calculations - self.session.mobility.updatewlans(moved, moved_ifaces) + self.session.mobility.update_nets(moved, moved_ifaces) # TODO: check session state self.session.event_loop.add_event(0.001 * self.refresh_ms, self.runround) @@ -729,7 +729,7 @@ class WayPointMobility(WirelessModel): self.setnodeposition(node, x, y, z) moved.append(node) moved_ifaces.append(iface) - self.session.mobility.updatewlans(moved, moved_ifaces) + self.session.mobility.update_nets(moved, moved_ifaces) def addwaypoint( self, From 63103ab25086ffd43e89fb56459f5ab4d429c40f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 31 Jul 2020 23:09:26 -0700 Subject: [PATCH 0560/1131] pygui: removed unused unlimited button from linkconfig dialog --- daemon/core/gui/dialogs/linkconfig.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/daemon/core/gui/dialogs/linkconfig.py b/daemon/core/gui/dialogs/linkconfig.py index 914bad1e..1aa2d7f8 100644 --- a/daemon/core/gui/dialogs/linkconfig.py +++ b/daemon/core/gui/dialogs/linkconfig.py @@ -77,10 +77,7 @@ class LinkConfigurationDialog(Dialog): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) frame.grid(row=1, column=0, sticky="ew", pady=PADY) - button = ttk.Button(frame, text="Unlimited") - button.grid(row=0, column=0, sticky="ew", padx=PADX) if self.is_symmetric: button = ttk.Button( frame, textvariable=self.symmetry_var, command=self.change_symmetry @@ -89,7 +86,7 @@ class LinkConfigurationDialog(Dialog): button = ttk.Button( frame, textvariable=self.symmetry_var, command=self.change_symmetry ) - button.grid(row=0, column=1, sticky="ew") + button.grid(sticky="ew") if self.is_symmetric: self.symmetric_frame = self.get_frame() From eb422f5bab665fcd0b592be6072a5c573766bdba Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 31 Jul 2020 23:13:07 -0700 Subject: [PATCH 0561/1131] pygui: mac editing disabled for nodes during runtime --- daemon/core/gui/dialogs/nodeconfig.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index 33c8fb32..604e933a 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -232,8 +232,10 @@ class NodeConfigDialog(Dialog): label = ttk.Label(tab, text="MAC") label.grid(row=row, column=0, padx=PADX, pady=PADY) auto_set = not iface.mac - mac_state = tk.DISABLED if auto_set else tk.NORMAL is_auto = tk.BooleanVar(value=auto_set) + mac_state = tk.DISABLED if auto_set else tk.NORMAL + if state == tk.DISABLED: + mac_state = tk.DISABLED checkbutton = ttk.Checkbutton( tab, text="Auto?", variable=is_auto, state=state ) From e7a93e7fd6ef885cc6c47c61ccc76975eec11449 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 31 Jul 2020 23:18:11 -0700 Subject: [PATCH 0562/1131] pygui: config dialogs that allow selecting a file default to ~/.coregui --- daemon/core/gui/widgets.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/daemon/core/gui/widgets.py b/daemon/core/gui/widgets.py index 85f3da10..0d5bff22 100644 --- a/daemon/core/gui/widgets.py +++ b/daemon/core/gui/widgets.py @@ -5,7 +5,7 @@ from pathlib import Path from tkinter import filedialog, font, ttk from typing import TYPE_CHECKING, Any, Callable, Dict, Set, Type -from core.gui import themes, validation +from core.gui import appconfig, themes, validation from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.wrappers import ConfigOption, ConfigOptionType @@ -26,7 +26,9 @@ INT_TYPES: Set[ConfigOptionType] = { def file_button_click(value: tk.StringVar, parent: tk.Widget) -> None: - file_path = filedialog.askopenfilename(title="Select File", parent=parent) + file_path = filedialog.askopenfilename( + title="Select File", initialdir=str(appconfig.HOME_PATH), parent=parent + ) if file_path: value.set(file_path) From 04f7bc561b7a59130c2cf080997f56583b0517bf Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 31 Jul 2020 23:23:18 -0700 Subject: [PATCH 0563/1131] pygui: fixed exception from bad check when double clicking in sessions dialog --- daemon/core/gui/dialogs/sessions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index d41e2052..3f9f3c9b 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -190,7 +190,7 @@ class SessionsDialog(Dialog): def double_click_join(self, _event: tk.Event) -> None: item = self.tree.selection() - if item is None: + if not item: return session_id = int(self.tree.item(item, "text")) self.join_session(session_id) From fc44ad6fe84bb52ee35ea1c2fafc95fe6de90501 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 1 Aug 2020 11:00:26 -0700 Subject: [PATCH 0564/1131] pygui: update title to show xml file when one is opened, fixed issue creating nodes/links when not runtime due to refactoring, removed xml_file from coreclient and depend on the grpc GetSession wrapped data, grpc: added opened file information to GetSession call --- daemon/core/api/grpc/grpcutils.py | 2 +- daemon/core/api/grpc/server.py | 1 + daemon/core/gui/coreclient.py | 61 +++++++++++++++------------ daemon/core/gui/dialogs/sessions.py | 2 - daemon/core/gui/menubar.py | 24 +++-------- daemon/core/gui/wrappers.py | 4 ++ daemon/proto/core/api/grpc/core.proto | 1 + 7 files changed, 48 insertions(+), 47 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 8f666508..eaec2359 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -229,7 +229,7 @@ def get_config_options( """ results = {} for configuration in configurable_options.configurations(): - value = config[configuration.id] + value = config.get(configuration.id, configuration.default) config_option = common_pb2.ConfigOption( label=configuration.label, name=configuration.id, diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 81f4335e..55bdc802 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -597,6 +597,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): config_service_configs=config_service_configs, mobility_configs=mobility_configs, metadata=session.metadata, + file=session.file_name, ) return core_pb2.GetSessionResponse(session=session_proto) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index bb6c4f95..8a881945 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -22,7 +22,7 @@ from core.api.grpc import ( wlan_pb2, ) from core.gui import appconfig -from core.gui.appconfig import CoreServer, Observer +from core.gui.appconfig import XMLS_PATH, CoreServer, Observer from core.gui.dialogs.emaneinstall import EmaneInstallDialog from core.gui.dialogs.error import ErrorDialog from core.gui.dialogs.mobilityplayer import MobilityPlayer @@ -98,8 +98,6 @@ class CoreClient: self.handling_throughputs: Optional[grpc.Future] = None self.handling_cpu_usage: Optional[grpc.Future] = None self.handling_events: Optional[grpc.Future] = None - self.xml_dir: Optional[str] = None - self.xml_file: Optional[str] = None @property def client(self) -> client.CoreGrpcClient: @@ -311,7 +309,8 @@ class CoreClient: response = self.client.get_session(session_id) self.session = Session.from_proto(response.session) self.client.set_session_user(self.session.id, self.user) - self.master.title(f"CORE Session({self.session.id})") + title_file = self.session.file.name if self.session.file else "" + self.master.title(f"CORE Session({self.session.id}) {title_file}") self.handling_events = self.client.events( self.session.id, self.handle_events ) @@ -586,10 +585,18 @@ class CoreClient: except grpc.RpcError as e: self.app.show_grpc_exception("Node Terminal Error", e) - def save_xml(self, file_path: str) -> None: + def get_xml_dir(self) -> str: + return str(self.session.file.parent) if self.session.file else str(XMLS_PATH) + + def save_xml(self, file_path: str = None) -> None: """ Save core session as to an xml file """ + if not file_path and not self.session.file: + logging.error("trying to save xml for session with no file") + return + if not file_path: + file_path = str(self.session.file) try: if not self.is_runtime(): logging.debug("Send session data to the daemon") @@ -687,34 +694,17 @@ class CoreClient: """ self.client.set_session_state(self.session.id, SessionState.DEFINITION.value) for node in self.session.nodes.values(): - response = self.client.add_node(self.session.id, node.to_proto()) + response = self.client.add_node( + self.session.id, node.to_proto(), source=GUI_SOURCE + ) logging.debug("created node: %s", response) asymmetric_links = [] for edge in self.links.values(): - link = edge.link - response = self.client.add_link( - self.session.id, - link.node1_id, - link.node2_id, - link.iface1, - link.iface2, - link.options, - source=GUI_SOURCE, - ) - logging.debug("created link: %s", response) + self.add_link(edge.link) if edge.asymmetric_link: asymmetric_links.append(edge.asymmetric_link) for link in asymmetric_links: - response = self.client.add_link( - self.session.id, - link.node1_id, - link.node2_id, - link.iface1, - link.iface2, - link.options, - source=GUI_SOURCE, - ) - logging.debug("created asymmetric link: %s", response) + self.add_link(link) def send_data(self) -> None: """ @@ -1057,6 +1047,23 @@ class CoreClient: if response.session_id != -1: self.join_session(response.session_id) + def add_link(self, link: Link) -> None: + iface1 = link.iface1.to_proto() if link.iface1 else None + iface2 = link.iface2.to_proto() if link.iface2 else None + options = link.options.to_proto() if link.options else None + response = self.client.add_link( + self.session.id, + link.node1_id, + link.node2_id, + iface1, + iface2, + options, + source=GUI_SOURCE, + ) + logging.debug("added link: %s", response) + if not response.result: + logging.error("error adding link: %s", link) + def edit_link(self, link: Link) -> None: iface1_id = link.iface1.id if link.iface1 else None iface2_id = link.iface2.id if link.iface2 else None diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index 3f9f3c9b..83e4001a 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -181,8 +181,6 @@ class SessionsDialog(Dialog): def join_session(self, session_id: int) -> None: self.destroy() - if self.app.core.xml_file: - self.app.core.xml_file = None task = ProgressTask( self.app, "Join", self.app.core.join_session, args=(session_id,) ) diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 3b85ac6f..fd1413b6 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -6,7 +6,6 @@ from functools import partial from tkinter import filedialog, messagebox from typing import TYPE_CHECKING, Optional -from core.gui.appconfig import XMLS_PATH from core.gui.coreclient import CoreClient from core.gui.dialogs.about import AboutDialog from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog @@ -265,16 +264,13 @@ class Menubar(tk.Menu): ) def click_save(self, _event=None) -> None: - xml_file = self.core.xml_file - if xml_file: - self.core.save_xml(xml_file) + if self.core.session.file: + self.core.save_xml() else: self.click_save_xml() def click_save_xml(self, _event: tk.Event = None) -> None: - init_dir = self.core.xml_dir - if not init_dir: - init_dir = str(XMLS_PATH) + init_dir = self.core.get_xml_dir() file_path = filedialog.asksaveasfilename( initialdir=init_dir, title="Save As", @@ -284,12 +280,9 @@ class Menubar(tk.Menu): if file_path: self.add_recent_file_to_gui_config(file_path) self.core.save_xml(file_path) - self.core.xml_file = file_path def click_open_xml(self, _event: tk.Event = None) -> None: - init_dir = self.core.xml_dir - if not init_dir: - init_dir = str(XMLS_PATH) + init_dir = self.core.get_xml_dir() file_path = filedialog.askopenfilename( initialdir=init_dir, title="Open", @@ -298,12 +291,10 @@ class Menubar(tk.Menu): if file_path: self.open_xml_task(file_path) - def open_xml_task(self, filename: str) -> None: - self.add_recent_file_to_gui_config(filename) - self.core.xml_file = filename - self.core.xml_dir = str(os.path.dirname(filename)) + def open_xml_task(self, file_path: str) -> None: + self.add_recent_file_to_gui_config(file_path) self.prompt_save_running_session() - task = ProgressTask(self.app, "Open XML", self.core.open_xml, args=(filename,)) + task = ProgressTask(self.app, "Open XML", self.core.open_xml, args=(file_path,)) task.start() def execute_python(self) -> None: @@ -357,7 +348,6 @@ class Menubar(tk.Menu): def click_new(self) -> None: self.prompt_save_running_session() self.core.create_new_session() - self.core.xml_file = None def click_find(self, _event: tk.Event = None) -> None: dialog = FindDialog(self.app) diff --git a/daemon/core/gui/wrappers.py b/daemon/core/gui/wrappers.py index d86e20dd..52384fe2 100644 --- a/daemon/core/gui/wrappers.py +++ b/daemon/core/gui/wrappers.py @@ -1,5 +1,6 @@ from dataclasses import dataclass, field from enum import Enum +from pathlib import Path from typing import Dict, List, Optional, Set, Tuple from core.api.grpc import common_pb2, configservices_pb2, core_pb2, services_pb2 @@ -581,6 +582,7 @@ class Session: emane_models: List[str] emane_config: Dict[str, ConfigOption] metadata: Dict[str, str] + file: Path @classmethod def from_proto(cls, proto: core_pb2.Session) -> "Session": @@ -616,6 +618,7 @@ class Session: for node_id, mapped_config in proto.mobility_configs.items(): node = nodes[node_id] node.mobility_config = ConfigOption.from_dict(mapped_config.config) + file_path = Path(proto.file) if proto.file else None return Session( id=proto.id, state=SessionState(proto.state), @@ -629,6 +632,7 @@ class Session: emane_models=list(proto.emane_models), emane_config=ConfigOption.from_dict(proto.emane_config), metadata=dict(proto.metadata), + file=file_path, ) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index d5ffda59..4727bbef 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -726,6 +726,7 @@ message Session { repeated configservices.ConfigServiceConfig config_service_configs = 15; map mobility_configs = 16; map metadata = 17; + string file = 18; } message SessionSummary { From 06563d59539cefc9da2bcb082816a8374cda5f74 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 1 Aug 2020 11:07:11 -0700 Subject: [PATCH 0565/1131] pygui: fixed issue editing hook with a new name --- daemon/core/gui/dialogs/hooks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/daemon/core/gui/dialogs/hooks.py b/daemon/core/gui/dialogs/hooks.py index 31ef3e15..ce7caf29 100644 --- a/daemon/core/gui/dialogs/hooks.py +++ b/daemon/core/gui/dialogs/hooks.py @@ -149,13 +149,15 @@ class HooksDialog(Dialog): dialog.set(hook) dialog.show() session.hooks[hook.file] = hook + self.selected = hook.file self.listbox.delete(self.selected_index) self.listbox.insert(self.selected_index, hook.file) + self.listbox.select_set(self.selected_index) def click_delete(self) -> None: session = self.app.core.session del session.hooks[self.selected] - self.listbox.delete(tk.ANCHOR) + self.listbox.delete(self.selected_index) self.edit_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED) From 2aeb119b0455b3fee5b41f134db1cca5da481f5d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 2 Aug 2020 10:03:21 -0700 Subject: [PATCH 0566/1131] pygui: changes to display both link and asym link options on edges in canvas --- daemon/core/gui/graph/edges.py | 46 ++++++++++++++++++++++------------ daemon/core/gui/utils.py | 12 +++++++++ 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 610b6cc0..b313957d 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -8,7 +8,7 @@ from core.gui.dialogs.linkconfig import LinkConfigurationDialog from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame from core.gui.graph import tags from core.gui.nodeutils import NodeUtils -from core.gui.utils import bandwidth_text +from core.gui.utils import bandwidth_text, delay_jitter_text from core.gui.wrappers import Interface, Link if TYPE_CHECKING: @@ -419,21 +419,35 @@ class CanvasEdge(Edge): if not self.link.options: return options = self.link.options + asym_options = None + if self.asymmetric_link and self.asymmetric_link.options: + asym_options = self.asymmetric_link.options lines = [] - bandwidth = options.bandwidth - if bandwidth > 0: - lines.append(bandwidth_text(bandwidth)) - delay = options.delay - jitter = options.jitter - if delay > 0 and jitter > 0: - lines.append(f"{delay} us (\u00B1{jitter} us)") - elif jitter > 0: - lines.append(f"0 us (\u00B1{jitter} us)") - loss = options.loss - if loss > 0: - lines.append(f"loss={loss}%") - dup = options.dup - if dup > 0: - lines.append(f"dup={dup}%") + # bandwidth + if options.bandwidth > 0: + bandwidth_line = bandwidth_text(options.bandwidth) + if asym_options and asym_options.bandwidth > 0: + bandwidth_line += f" / {bandwidth_text(asym_options.bandwidth)}" + lines.append(bandwidth_line) + # delay/jitter + dj_line = delay_jitter_text(options.delay, options.jitter) + if dj_line and asym_options: + asym_dj_line = delay_jitter_text(asym_options.delay, asym_options.jitter) + if asym_dj_line: + dj_line += f" / {asym_dj_line}" + if dj_line: + lines.append(dj_line) + # loss + if options.loss > 0: + loss_line = f"loss={options.loss}%" + if asym_options and asym_options.loss > 0: + loss_line += f" / loss={asym_options.loss}%" + lines.append(loss_line) + # duplicate + if options.dup > 0: + dup_line = f"dup={options.dup}%" + if asym_options and asym_options.dup > 0: + dup_line += f" / dup={asym_options.dup}%" + lines.append(dup_line) label = "\n".join(lines) self.middle_label_text(label) diff --git a/daemon/core/gui/utils.py b/daemon/core/gui/utils.py index ee5ad8cb..59171ae9 100644 --- a/daemon/core/gui/utils.py +++ b/daemon/core/gui/utils.py @@ -1,3 +1,6 @@ +from typing import Optional + + def bandwidth_text(bandwidth: int) -> str: size = {0: "bps", 1: "Kbps", 2: "Mbps", 3: "Gbps"} unit = 1000 @@ -8,3 +11,12 @@ def bandwidth_text(bandwidth: int) -> str: if i == 3: break return f"{bandwidth} {size[i]}" + + +def delay_jitter_text(delay: int, jitter: int) -> Optional[str]: + line = None + if delay > 0 and jitter > 0: + line = f"{delay} us (\u00B1{jitter} us)" + elif jitter > 0: + line = f"0 us (\u00B1{jitter} us)" + return line From f0bc3bbc998b6437fc077529de03181663b0c917 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 2 Aug 2020 10:36:14 -0700 Subject: [PATCH 0567/1131] pygui: updates to leverage tk provided constants for sticky configuration, instead of duplicate strings everywhere --- daemon/core/gui/app.py | 20 ++-- daemon/core/gui/dialogs/about.py | 4 +- daemon/core/gui/dialogs/alerts.py | 16 ++-- daemon/core/gui/dialogs/canvassizeandscale.py | 72 +++++++------- daemon/core/gui/dialogs/canvaswallpaper.py | 28 +++--- daemon/core/gui/dialogs/colorpicker.py | 36 +++---- .../core/gui/dialogs/configserviceconfig.py | 66 ++++++------- daemon/core/gui/dialogs/copyserviceconfig.py | 16 ++-- daemon/core/gui/dialogs/customnodes.py | 46 ++++----- daemon/core/gui/dialogs/dialog.py | 4 +- daemon/core/gui/dialogs/emaneconfig.py | 36 +++---- daemon/core/gui/dialogs/emaneinstall.py | 7 +- daemon/core/gui/dialogs/executepython.py | 16 ++-- daemon/core/gui/dialogs/find.py | 18 ++-- daemon/core/gui/dialogs/hooks.py | 28 +++--- daemon/core/gui/dialogs/ipdialog.py | 26 ++--- daemon/core/gui/dialogs/linkconfig.py | 56 +++++------ daemon/core/gui/dialogs/macdialog.py | 10 +- daemon/core/gui/dialogs/mobilityconfig.py | 9 +- daemon/core/gui/dialogs/mobilityplayer.py | 12 +-- daemon/core/gui/dialogs/nodeconfig.py | 44 ++++----- daemon/core/gui/dialogs/nodeconfigservice.py | 20 ++-- daemon/core/gui/dialogs/nodeservice.py | 20 ++-- daemon/core/gui/dialogs/observers.py | 28 +++--- daemon/core/gui/dialogs/preferences.py | 30 +++--- daemon/core/gui/dialogs/runtool.py | 26 ++--- daemon/core/gui/dialogs/servers.py | 28 +++--- daemon/core/gui/dialogs/serviceconfig.py | 94 +++++++++---------- daemon/core/gui/dialogs/sessionoptions.py | 8 +- daemon/core/gui/dialogs/sessions.py | 18 ++-- daemon/core/gui/dialogs/shapemod.py | 48 +++++----- daemon/core/gui/dialogs/throughput.py | 32 +++---- daemon/core/gui/dialogs/wlanconfig.py | 9 +- daemon/core/gui/frames/link.py | 4 +- daemon/core/gui/frames/node.py | 3 +- daemon/core/gui/statusbar.py | 10 +- daemon/core/gui/task.py | 3 +- daemon/core/gui/toolbar.py | 14 +-- daemon/core/gui/tooltip.py | 2 +- daemon/core/gui/widgets.py | 30 +++--- 40 files changed, 501 insertions(+), 496 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 176b31e3..be744bb4 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -111,13 +111,13 @@ class Application(ttk.Frame): self.master.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.columnconfigure(1, weight=1) - self.grid(sticky="nsew") + self.grid(sticky=tk.NSEW) self.toolbar = Toolbar(self) - self.toolbar.grid(sticky="ns") + self.toolbar.grid(sticky=tk.NS) self.right_frame = ttk.Frame(self) self.right_frame.columnconfigure(0, weight=1) self.right_frame.rowconfigure(0, weight=1) - self.right_frame.grid(row=0, column=1, sticky="nsew") + self.right_frame.grid(row=0, column=1, sticky=tk.NSEW) self.draw_canvas() self.draw_infobar() self.draw_status() @@ -139,21 +139,21 @@ class Application(ttk.Frame): canvas_frame = ttk.Frame(self.right_frame) canvas_frame.rowconfigure(0, weight=1) canvas_frame.columnconfigure(0, weight=1) - canvas_frame.grid(row=0, column=0, sticky="nsew", pady=1) + canvas_frame.grid(row=0, column=0, sticky=tk.NSEW, pady=1) self.canvas = CanvasGraph(canvas_frame, self, self.core) - self.canvas.grid(sticky="nsew") + self.canvas.grid(sticky=tk.NSEW) scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview) - scroll_y.grid(row=0, column=1, sticky="ns") + scroll_y.grid(row=0, column=1, sticky=tk.NS) scroll_x = ttk.Scrollbar( canvas_frame, orient=tk.HORIZONTAL, command=self.canvas.xview ) - scroll_x.grid(row=1, column=0, sticky="ew") + scroll_x.grid(row=1, column=0, sticky=tk.EW) self.canvas.configure(xscrollcommand=scroll_x.set) self.canvas.configure(yscrollcommand=scroll_y.set) def draw_status(self) -> None: self.statusbar = StatusBar(self.right_frame, self) - self.statusbar.grid(sticky="ew", columnspan=2) + self.statusbar.grid(sticky=tk.EW, columnspan=2) def display_info(self, frame_class: Type[InfoFrameBase], **kwargs: Any) -> None: if not self.show_infobar.get(): @@ -161,7 +161,7 @@ class Application(ttk.Frame): self.clear_info() self.info_frame = frame_class(self.infobar, **kwargs) self.info_frame.draw() - self.info_frame.grid(sticky="nsew") + self.info_frame.grid(sticky=tk.NSEW) def clear_info(self) -> None: if self.info_frame: @@ -174,7 +174,7 @@ class Application(ttk.Frame): def show_info(self) -> None: self.default_info() - self.infobar.grid(row=0, column=1, sticky="nsew") + self.infobar.grid(row=0, column=1, sticky=tk.NSEW) def hide_info(self) -> None: self.infobar.grid_forget() diff --git a/daemon/core/gui/dialogs/about.py b/daemon/core/gui/dialogs/about.py index fa96e218..c932807d 100644 --- a/daemon/core/gui/dialogs/about.py +++ b/daemon/core/gui/dialogs/about.py @@ -46,9 +46,9 @@ class AboutDialog(Dialog): codetext = CodeText(self.top) codetext.text.insert("1.0", LICENSE) codetext.text.config(state=tk.DISABLED) - codetext.grid(sticky="nsew") + codetext.grid(sticky=tk.NSEW) label = ttk.Label( self.top, text="Icons from https://icons8.com", anchor=tk.CENTER ) - label.grid(sticky="ew") + label.grid(sticky=tk.EW) diff --git a/daemon/core/gui/dialogs/alerts.py b/daemon/core/gui/dialogs/alerts.py index fd6d342e..a0193727 100644 --- a/daemon/core/gui/dialogs/alerts.py +++ b/daemon/core/gui/dialogs/alerts.py @@ -30,13 +30,13 @@ class AlertsDialog(Dialog): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - frame.grid(sticky="nsew", pady=PADY) + frame.grid(sticky=tk.NSEW, pady=PADY) self.tree = ttk.Treeview( frame, columns=("time", "level", "session_id", "node", "source"), show="headings", ) - self.tree.grid(row=0, column=0, sticky="nsew") + self.tree.grid(row=0, column=0, sticky=tk.NSEW) self.tree.column("time", stretch=tk.YES) self.tree.heading("time", text="Time") self.tree.column("level", stretch=tk.YES, width=100) @@ -77,25 +77,25 @@ class AlertsDialog(Dialog): self.tree.tag_configure(notice_name, background="#85e085") yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) - yscrollbar.grid(row=0, column=1, sticky="ns") + yscrollbar.grid(row=0, column=1, sticky=tk.NS) self.tree.configure(yscrollcommand=yscrollbar.set) xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) - xscrollbar.grid(row=1, sticky="ew") + xscrollbar.grid(row=1, sticky=tk.EW) self.tree.configure(xscrollcommand=xscrollbar.set) self.codetext = CodeText(self.top) self.codetext.text.config(state=tk.DISABLED, height=11) - self.codetext.grid(sticky="nsew", pady=PADY) + self.codetext.grid(sticky=tk.NSEW, pady=PADY) frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Reset", command=self.reset_alerts) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Close", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def reset_alerts(self) -> None: self.codetext.text.config(state=tk.NORMAL) diff --git a/daemon/core/gui/dialogs/canvassizeandscale.py b/daemon/core/gui/dialogs/canvassizeandscale.py index e8ad6693..e50bf986 100644 --- a/daemon/core/gui/dialogs/canvassizeandscale.py +++ b/daemon/core/gui/dialogs/canvassizeandscale.py @@ -23,7 +23,7 @@ class SizeAndScaleDialog(Dialog): """ super().__init__(app, "Canvas Size and Scale") self.canvas: CanvasGraph = self.app.canvas - self.section_font: font.Font = font.Font(weight="bold") + self.section_font: font.Font = font.Font(weight=font.BOLD) width, height = self.canvas.current_dimensions self.pixel_width: tk.IntVar = tk.IntVar(value=width) self.pixel_height: tk.IntVar = tk.IntVar(value=height) @@ -54,68 +54,68 @@ class SizeAndScaleDialog(Dialog): def draw_size(self) -> None: label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD) - label_frame.grid(sticky="ew") + label_frame.grid(sticky=tk.EW) label_frame.columnconfigure(0, weight=1) # draw size row 1 frame = ttk.Frame(label_frame) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky=tk.W, padx=PADX) entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_width) - entry.grid(row=0, column=1, sticky="ew", padx=PADX) + entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX) entry.bind("", self.size_scale_keyup) label = ttk.Label(frame, text="x Height") - label.grid(row=0, column=2, sticky="w", padx=PADX) + label.grid(row=0, column=2, sticky=tk.W, padx=PADX) entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_height) - entry.grid(row=0, column=3, sticky="ew", padx=PADX) + entry.grid(row=0, column=3, sticky=tk.EW, padx=PADX) entry.bind("", self.size_scale_keyup) label = ttk.Label(frame, text="Pixels") - label.grid(row=0, column=4, sticky="w") + label.grid(row=0, column=4, sticky=tk.W) # draw size row 2 frame = ttk.Frame(label_frame) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky=tk.W, padx=PADX) entry = validation.PositiveFloatEntry( frame, textvariable=self.meters_width, state=tk.DISABLED ) - entry.grid(row=0, column=1, sticky="ew", padx=PADX) + entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX) label = ttk.Label(frame, text="x Height") - label.grid(row=0, column=2, sticky="w", padx=PADX) + label.grid(row=0, column=2, sticky=tk.W, padx=PADX) entry = validation.PositiveFloatEntry( frame, textvariable=self.meters_height, state=tk.DISABLED ) - entry.grid(row=0, column=3, sticky="ew", padx=PADX) + entry.grid(row=0, column=3, sticky=tk.EW, padx=PADX) label = ttk.Label(frame, text="Meters") - label.grid(row=0, column=4, sticky="w") + label.grid(row=0, column=4, sticky=tk.W) def draw_scale(self) -> None: label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD) - label_frame.grid(sticky="ew") + label_frame.grid(sticky=tk.EW) label_frame.columnconfigure(0, weight=1) frame = ttk.Frame(label_frame) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text=f"{PIXEL_SCALE} Pixels =") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky=tk.W, padx=PADX) entry = validation.PositiveFloatEntry(frame, textvariable=self.scale) - entry.grid(row=0, column=1, sticky="ew", padx=PADX) + entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX) entry.bind("", self.size_scale_keyup) label = ttk.Label(frame, text="Meters") - label.grid(row=0, column=2, sticky="w") + label.grid(row=0, column=2, sticky=tk.W) def draw_reference_point(self) -> None: label_frame = ttk.Labelframe( self.top, text="Reference Point", padding=FRAME_PAD ) - label_frame.grid(sticky="ew") + label_frame.grid(sticky=tk.EW) label_frame.columnconfigure(0, weight=1) label = ttk.Label( @@ -124,61 +124,61 @@ class SizeAndScaleDialog(Dialog): label.grid() frame = ttk.Frame(label_frame) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="X") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky=tk.W, padx=PADX) entry = validation.PositiveFloatEntry(frame, textvariable=self.x) - entry.grid(row=0, column=1, sticky="ew", padx=PADX) + entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX) label = ttk.Label(frame, text="Y") - label.grid(row=0, column=2, sticky="w", padx=PADX) + label.grid(row=0, column=2, sticky=tk.W, padx=PADX) entry = validation.PositiveFloatEntry(frame, textvariable=self.y) - entry.grid(row=0, column=3, sticky="ew", padx=PADX) + entry.grid(row=0, column=3, sticky=tk.EW, padx=PADX) label = ttk.Label(label_frame, text="Translates To") label.grid() frame = ttk.Frame(label_frame) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) frame.columnconfigure(5, weight=1) label = ttk.Label(frame, text="Lat") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky=tk.W, padx=PADX) entry = validation.FloatEntry(frame, textvariable=self.lat) - entry.grid(row=0, column=1, sticky="ew", padx=PADX) + entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX) label = ttk.Label(frame, text="Lon") - label.grid(row=0, column=2, sticky="w", padx=PADX) + label.grid(row=0, column=2, sticky=tk.W, padx=PADX) entry = validation.FloatEntry(frame, textvariable=self.lon) - entry.grid(row=0, column=3, sticky="ew", padx=PADX) + entry.grid(row=0, column=3, sticky=tk.EW, padx=PADX) label = ttk.Label(frame, text="Alt") - label.grid(row=0, column=4, sticky="w", padx=PADX) + label.grid(row=0, column=4, sticky=tk.W, padx=PADX) entry = validation.FloatEntry(frame, textvariable=self.alt) - entry.grid(row=0, column=5, sticky="ew") + entry.grid(row=0, column=5, sticky=tk.EW) def draw_save_as_default(self) -> None: button = ttk.Checkbutton( self.top, text="Save as default?", variable=self.save_default ) - button.grid(sticky="w", pady=PADY) + button.grid(sticky=tk.W, pady=PADY) def draw_buttons(self) -> None: frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def size_scale_keyup(self, _event: tk.Event) -> None: scale = self.scale.get() diff --git a/daemon/core/gui/dialogs/canvaswallpaper.py b/daemon/core/gui/dialogs/canvaswallpaper.py index 8a1e71d8..629f9f36 100644 --- a/daemon/core/gui/dialogs/canvaswallpaper.py +++ b/daemon/core/gui/dialogs/canvaswallpaper.py @@ -51,7 +51,7 @@ class CanvasWallpaperDialog(Dialog): def draw_image_label(self) -> None: label = ttk.Label(self.top, text="Image filename: ") - label.grid(sticky="ew") + label.grid(sticky=tk.EW) if self.filename.get(): self.draw_preview() @@ -60,17 +60,17 @@ class CanvasWallpaperDialog(Dialog): frame.columnconfigure(0, weight=2) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW, pady=PADY) entry = ttk.Entry(frame, textvariable=self.filename) entry.focus() - entry.grid(row=0, column=0, sticky="ew", padx=PADX) + entry.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="...", command=self.click_open_image) - button.grid(row=0, column=1, sticky="ew", padx=PADX) + button.grid(row=0, column=1, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Clear", command=self.click_clear) - button.grid(row=0, column=2, sticky="ew") + button.grid(row=0, column=2, sticky=tk.EW) def draw_options(self) -> None: frame = ttk.Frame(self.top) @@ -78,30 +78,30 @@ class CanvasWallpaperDialog(Dialog): frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) frame.columnconfigure(3, weight=1) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW, pady=PADY) button = ttk.Radiobutton( frame, text="upper-left", value=1, variable=self.scale_option ) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky=tk.EW) self.options.append(button) button = ttk.Radiobutton( frame, text="centered", value=2, variable=self.scale_option ) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) self.options.append(button) button = ttk.Radiobutton( frame, text="scaled", value=3, variable=self.scale_option ) - button.grid(row=0, column=2, sticky="ew") + button.grid(row=0, column=2, sticky=tk.EW) self.options.append(button) button = ttk.Radiobutton( frame, text="titled", value=4, variable=self.scale_option ) - button.grid(row=0, column=3, sticky="ew") + button.grid(row=0, column=3, sticky=tk.EW) self.options.append(button) def draw_additional_options(self) -> None: @@ -111,19 +111,19 @@ class CanvasWallpaperDialog(Dialog): variable=self.adjust_to_dim, command=self.click_adjust_canvas, ) - checkbutton.grid(sticky="ew", padx=PADX) + checkbutton.grid(sticky=tk.EW, padx=PADX, pady=PADY) def draw_buttons(self) -> None: frame = ttk.Frame(self.top) - frame.grid(pady=PADY, sticky="ew") + frame.grid(sticky=tk.EW) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def click_open_image(self) -> None: filename = image_chooser(self, BACKGROUNDS_PATH) diff --git a/daemon/core/gui/dialogs/colorpicker.py b/daemon/core/gui/dialogs/colorpicker.py index 908b8acb..a2f131d4 100644 --- a/daemon/core/gui/dialogs/colorpicker.py +++ b/daemon/core/gui/dialogs/colorpicker.py @@ -48,13 +48,13 @@ class ColorPickerDialog(Dialog): # rgb frames frame = ttk.Frame(self.top) - frame.grid(row=0, column=0, sticky="ew", pady=PADY) + frame.grid(row=0, column=0, sticky=tk.EW, pady=PADY) frame.columnconfigure(2, weight=3) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="R") label.grid(row=0, column=0, padx=PADX) self.red_entry = validation.RgbEntry(frame, width=3, textvariable=self.red) - self.red_entry.grid(row=0, column=1, sticky="ew", padx=PADX) + self.red_entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX) scale = ttk.Scale( frame, from_=0, @@ -64,20 +64,20 @@ class ColorPickerDialog(Dialog): variable=self.red_scale, command=lambda x: self.scale_callback(self.red_scale, self.red), ) - scale.grid(row=0, column=2, sticky="ew", padx=PADX) + scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX) self.red_label = ttk.Label( frame, background="#%02x%02x%02x" % (self.red.get(), 0, 0), width=5 ) - self.red_label.grid(row=0, column=3, sticky="ew") + self.red_label.grid(row=0, column=3, sticky=tk.EW) frame = ttk.Frame(self.top) - frame.grid(row=1, column=0, sticky="ew", pady=PADY) + frame.grid(row=1, column=0, sticky=tk.EW, pady=PADY) frame.columnconfigure(2, weight=3) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="G") label.grid(row=0, column=0, padx=PADX) self.green_entry = validation.RgbEntry(frame, width=3, textvariable=self.green) - self.green_entry.grid(row=0, column=1, sticky="ew", padx=PADX) + self.green_entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX) scale = ttk.Scale( frame, from_=0, @@ -87,20 +87,20 @@ class ColorPickerDialog(Dialog): variable=self.green_scale, command=lambda x: self.scale_callback(self.green_scale, self.green), ) - scale.grid(row=0, column=2, sticky="ew", padx=PADX) + scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX) self.green_label = ttk.Label( frame, background="#%02x%02x%02x" % (0, self.green.get(), 0), width=5 ) - self.green_label.grid(row=0, column=3, sticky="ew") + self.green_label.grid(row=0, column=3, sticky=tk.EW) frame = ttk.Frame(self.top) - frame.grid(row=2, column=0, sticky="ew", pady=PADY) + frame.grid(row=2, column=0, sticky=tk.EW, pady=PADY) frame.columnconfigure(2, weight=3) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="B") label.grid(row=0, column=0, padx=PADX) self.blue_entry = validation.RgbEntry(frame, width=3, textvariable=self.blue) - self.blue_entry.grid(row=0, column=1, sticky="ew", padx=PADX) + self.blue_entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX) scale = ttk.Scale( frame, from_=0, @@ -110,31 +110,31 @@ class ColorPickerDialog(Dialog): variable=self.blue_scale, command=lambda x: self.scale_callback(self.blue_scale, self.blue), ) - scale.grid(row=0, column=2, sticky="ew", padx=PADX) + scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX) self.blue_label = ttk.Label( frame, background="#%02x%02x%02x" % (0, 0, self.blue.get()), width=5 ) - self.blue_label.grid(row=0, column=3, sticky="ew") + self.blue_label.grid(row=0, column=3, sticky=tk.EW) # hex code and color display frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.rowconfigure(1, weight=1) self.hex_entry = validation.HexEntry(frame, textvariable=self.hex) - self.hex_entry.grid(sticky="ew", pady=PADY) + self.hex_entry.grid(sticky=tk.EW, pady=PADY) self.display = tk.Frame(frame, background=self.color, width=100, height=100) - self.display.grid(sticky="nsew") - frame.grid(row=3, column=0, sticky="nsew", pady=PADY) + self.display.grid(sticky=tk.NSEW) + frame.grid(row=3, column=0, sticky=tk.NSEW, pady=PADY) # button frame frame = ttk.Frame(self.top) - frame.grid(row=4, column=0, sticky="ew") + frame.grid(row=4, column=0, sticky=tk.EW) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="OK", command=self.button_ok) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def set_bindings(self) -> None: self.red_entry.bind("", lambda x: self.current_focus("rgb")) diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py index f778cf15..a085afd1 100644 --- a/daemon/core/gui/dialogs/configserviceconfig.py +++ b/daemon/core/gui/dialogs/configserviceconfig.py @@ -111,7 +111,7 @@ class ConfigServiceConfigDialog(Dialog): # draw notebook self.notebook = ttk.Notebook(self.top) - self.notebook.grid(sticky="nsew", pady=PADY) + self.notebook.grid(sticky=tk.NSEW, pady=PADY) self.draw_tab_files() if self.config: self.draw_tab_config() @@ -121,7 +121,7 @@ class ConfigServiceConfigDialog(Dialog): def draw_tab_files(self) -> None: tab = ttk.Frame(self.notebook, padding=FRAME_PAD) - tab.grid(sticky="nsew") + tab.grid(sticky=tk.NSEW) tab.columnconfigure(0, weight=1) self.notebook.add(tab, text="Directories/Files") @@ -131,29 +131,29 @@ class ConfigServiceConfigDialog(Dialog): label.grid(pady=PADY) frame = ttk.Frame(tab) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Directories") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky=tk.W, padx=PADX) directories_combobox = ttk.Combobox( frame, values=self.directories, state="readonly" ) - directories_combobox.grid(row=0, column=1, sticky="ew", pady=PADY) + directories_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY) if self.directories: directories_combobox.current(0) label = ttk.Label(frame, text="Templates") - label.grid(row=1, column=0, sticky="w", padx=PADX) + label.grid(row=1, column=0, sticky=tk.W, padx=PADX) self.templates_combobox = ttk.Combobox( frame, values=self.templates, state="readonly" ) self.templates_combobox.bind( "<>", self.handle_template_changed ) - self.templates_combobox.grid(row=1, column=1, sticky="ew", pady=PADY) + self.templates_combobox.grid(row=1, column=1, sticky=tk.EW, pady=PADY) self.template_text = CodeText(tab) - self.template_text.grid(sticky="nsew") + self.template_text.grid(sticky=tk.NSEW) tab.rowconfigure(self.template_text.grid_info()["row"], weight=1) if self.templates: self.templates_combobox.current(0) @@ -165,13 +165,13 @@ class ConfigServiceConfigDialog(Dialog): def draw_tab_config(self) -> None: tab = ttk.Frame(self.notebook, padding=FRAME_PAD) - tab.grid(sticky="nsew") + tab.grid(sticky=tk.NSEW) tab.columnconfigure(0, weight=1) self.notebook.add(tab, text="Configuration") if self.modes: frame = ttk.Frame(tab) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Modes") label.grid(row=0, column=0, padx=PADX) @@ -179,17 +179,17 @@ class ConfigServiceConfigDialog(Dialog): frame, values=self.modes, state="readonly" ) self.modes_combobox.bind("<>", self.handle_mode_changed) - self.modes_combobox.grid(row=0, column=1, sticky="ew", pady=PADY) + self.modes_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY) logging.info("config service config: %s", self.config) self.config_frame = ConfigFrame(tab, self.app, self.config) self.config_frame.draw_config() - self.config_frame.grid(sticky="nsew", pady=PADY) + self.config_frame.grid(sticky=tk.NSEW, pady=PADY) tab.rowconfigure(self.config_frame.grid_info()["row"], weight=1) def draw_tab_startstop(self) -> None: tab = ttk.Frame(self.notebook, padding=FRAME_PAD) - tab.grid(sticky="nsew") + tab.grid(sticky=tk.NSEW) tab.columnconfigure(0, weight=1) for i in range(3): tab.rowconfigure(i, weight=1) @@ -215,12 +215,12 @@ class ConfigServiceConfigDialog(Dialog): commands = self.validation_commands label_frame.columnconfigure(0, weight=1) label_frame.rowconfigure(0, weight=1) - label_frame.grid(row=i, column=0, sticky="nsew", pady=PADY) + label_frame.grid(row=i, column=0, sticky=tk.NSEW, pady=PADY) listbox_scroll = ListboxScroll(label_frame) for command in commands: listbox_scroll.listbox.insert("end", command) listbox_scroll.listbox.config(height=4) - listbox_scroll.grid(sticky="nsew") + listbox_scroll.grid(sticky=tk.NSEW) if i == 0: self.startup_commands_listbox = listbox_scroll.listbox elif i == 1: @@ -230,23 +230,23 @@ class ConfigServiceConfigDialog(Dialog): def draw_tab_validation(self) -> None: tab = ttk.Frame(self.notebook, padding=FRAME_PAD) - tab.grid(sticky="ew") + tab.grid(sticky=tk.EW) tab.columnconfigure(0, weight=1) - self.notebook.add(tab, text="Validation", sticky="nsew") + self.notebook.add(tab, text="Validation", sticky=tk.NSEW) frame = ttk.Frame(tab) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Validation Time") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky=tk.W, padx=PADX) self.validation_time_entry = ttk.Entry(frame) self.validation_time_entry.insert("end", self.validation_time) self.validation_time_entry.config(state=tk.DISABLED) - self.validation_time_entry.grid(row=0, column=1, sticky="ew", pady=PADY) + self.validation_time_entry.grid(row=0, column=1, sticky=tk.EW, pady=PADY) label = ttk.Label(frame, text="Validation Mode") - label.grid(row=1, column=0, sticky="w", padx=PADX) + label.grid(row=1, column=0, sticky=tk.W, padx=PADX) if self.validation_mode == ServiceValidationMode.BLOCKING: mode = "BLOCKING" elif self.validation_mode == ServiceValidationMode.NON_BLOCKING: @@ -258,48 +258,48 @@ class ConfigServiceConfigDialog(Dialog): ) self.validation_mode_entry.insert("end", mode) self.validation_mode_entry.config(state=tk.DISABLED) - self.validation_mode_entry.grid(row=1, column=1, sticky="ew", pady=PADY) + self.validation_mode_entry.grid(row=1, column=1, sticky=tk.EW, pady=PADY) label = ttk.Label(frame, text="Validation Period") - label.grid(row=2, column=0, sticky="w", padx=PADX) + label.grid(row=2, column=0, sticky=tk.W, padx=PADX) self.validation_period_entry = ttk.Entry( frame, state=tk.DISABLED, textvariable=self.validation_period ) - self.validation_period_entry.grid(row=2, column=1, sticky="ew", pady=PADY) + self.validation_period_entry.grid(row=2, column=1, sticky=tk.EW, pady=PADY) label_frame = ttk.LabelFrame(tab, text="Executables", padding=FRAME_PAD) - label_frame.grid(sticky="nsew", pady=PADY) + label_frame.grid(sticky=tk.NSEW, pady=PADY) label_frame.columnconfigure(0, weight=1) label_frame.rowconfigure(0, weight=1) listbox_scroll = ListboxScroll(label_frame) - listbox_scroll.grid(sticky="nsew") + listbox_scroll.grid(sticky=tk.NSEW) tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1) for executable in self.executables: listbox_scroll.listbox.insert("end", executable) label_frame = ttk.LabelFrame(tab, text="Dependencies", padding=FRAME_PAD) - label_frame.grid(sticky="nsew", pady=PADY) + label_frame.grid(sticky=tk.NSEW, pady=PADY) label_frame.columnconfigure(0, weight=1) label_frame.rowconfigure(0, weight=1) listbox_scroll = ListboxScroll(label_frame) - listbox_scroll.grid(sticky="nsew") + listbox_scroll.grid(sticky=tk.NSEW) tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1) for dependency in self.dependencies: listbox_scroll.listbox.insert("end", dependency) def draw_buttons(self) -> None: frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) for i in range(4): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Defaults", command=self.click_defaults) - button.grid(row=0, column=1, sticky="ew", padx=PADX) + button.grid(row=0, column=1, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Copy...", command=self.click_copy) - button.grid(row=0, column=2, sticky="ew", padx=PADX) + button.grid(row=0, column=2, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=3, sticky="ew") + button.grid(row=0, column=3, sticky=tk.EW) def click_apply(self) -> None: current_listbox = self.master.current.listbox diff --git a/daemon/core/gui/dialogs/copyserviceconfig.py b/daemon/core/gui/dialogs/copyserviceconfig.py index b60d5a0d..b205e175 100644 --- a/daemon/core/gui/dialogs/copyserviceconfig.py +++ b/daemon/core/gui/dialogs/copyserviceconfig.py @@ -38,10 +38,10 @@ class CopyServiceConfigDialog(Dialog): label = ttk.Label( self.top, text=f"{self.service} - {self.file_name}", anchor=tk.CENTER ) - label.grid(sticky="ew", pady=PADY) + label.grid(sticky=tk.EW, pady=PADY) listbox_scroll = ListboxScroll(self.top) - listbox_scroll.grid(sticky="nsew", pady=PADY) + listbox_scroll.grid(sticky=tk.NSEW, pady=PADY) self.listbox = listbox_scroll.listbox for node in self.app.core.session.nodes.values(): file_configs = node.service_file_configs.get(self.service) @@ -54,15 +54,15 @@ class CopyServiceConfigDialog(Dialog): self.listbox.insert(tk.END, node.name) frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Copy", command=self.click_copy) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="View", command=self.click_view) - button.grid(row=0, column=1, sticky="ew", padx=PADX) + button.grid(row=0, column=1, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=2, sticky="ew") + button.grid(row=0, column=2, sticky=tk.EW) def click_copy(self) -> None: selection = self.listbox.curselection() @@ -112,8 +112,8 @@ class ViewConfigDialog(Dialog): self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) self.service_data = CodeText(self.top) - self.service_data.grid(sticky="nsew", pady=PADY) + self.service_data.grid(sticky=tk.NSEW, pady=PADY) self.service_data.text.insert(tk.END, self.data) self.service_data.text.config(state=tk.DISABLED) button = ttk.Button(self.top, text="Close", command=self.destroy) - button.grid(sticky="ew") + button.grid(sticky=tk.EW) diff --git a/daemon/core/gui/dialogs/customnodes.py b/daemon/core/gui/dialogs/customnodes.py index df3bafa7..53451ab1 100644 --- a/daemon/core/gui/dialogs/customnodes.py +++ b/daemon/core/gui/dialogs/customnodes.py @@ -34,47 +34,47 @@ class ServicesSelectDialog(Dialog): self.top.rowconfigure(0, weight=1) frame = ttk.LabelFrame(self.top) - frame.grid(stick="nsew", pady=PADY) + frame.grid(stick=tk.NSEW, pady=PADY) frame.rowconfigure(0, weight=1) for i in range(3): frame.columnconfigure(i, weight=1) label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD) - label_frame.grid(row=0, column=0, sticky="nsew") + label_frame.grid(row=0, column=0, sticky=tk.NSEW) label_frame.rowconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1) self.groups = ListboxScroll(label_frame) - self.groups.grid(sticky="nsew") + self.groups.grid(sticky=tk.NSEW) for group in sorted(self.app.core.services): self.groups.listbox.insert(tk.END, group) self.groups.listbox.bind("<>", self.handle_group_change) self.groups.listbox.selection_set(0) label_frame = ttk.LabelFrame(frame, text="Services") - label_frame.grid(row=0, column=1, sticky="nsew") + label_frame.grid(row=0, column=1, sticky=tk.NSEW) label_frame.columnconfigure(0, weight=1) label_frame.rowconfigure(0, weight=1) self.services = CheckboxList( label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD ) - self.services.grid(sticky="nsew") + self.services.grid(sticky=tk.NSEW) label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD) - label_frame.grid(row=0, column=2, sticky="nsew") + label_frame.grid(row=0, column=2, sticky=tk.NSEW) label_frame.rowconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1) self.current = ListboxScroll(label_frame) - self.current.grid(sticky="nsew") + self.current.grid(sticky=tk.NSEW) for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) frame = ttk.Frame(self.top) - frame.grid(stick="ew") + frame.grid(stick=tk.EW) for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.destroy) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.click_cancel) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) # trigger group change self.groups.listbox.event_generate("<>") @@ -127,58 +127,58 @@ class CustomNodesDialog(Dialog): def draw_node_config(self) -> None: frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD) - frame.grid(sticky="nsew", pady=PADY) + frame.grid(sticky=tk.NSEW, pady=PADY) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) self.nodes_list = ListboxScroll(frame) - self.nodes_list.grid(row=0, column=0, sticky="nsew", padx=PADX) + self.nodes_list.grid(row=0, column=0, sticky=tk.NSEW, padx=PADX) self.nodes_list.listbox.bind("<>", self.handle_node_select) for name in sorted(self.app.core.custom_nodes): self.nodes_list.listbox.insert(tk.END, name) frame = ttk.Frame(frame) - frame.grid(row=0, column=2, sticky="nsew") + frame.grid(row=0, column=2, sticky=tk.NSEW) frame.columnconfigure(0, weight=1) entry = ttk.Entry(frame, textvariable=self.name) - entry.grid(sticky="ew", pady=PADY) + entry.grid(sticky=tk.EW, pady=PADY) self.image_button = ttk.Button( frame, text="Icon", compound=tk.LEFT, command=self.click_icon ) - self.image_button.grid(sticky="ew", pady=PADY) + self.image_button.grid(sticky=tk.EW, pady=PADY) button = ttk.Button(frame, text="Services", command=self.click_services) - button.grid(sticky="ew") + button.grid(sticky=tk.EW) def draw_node_buttons(self) -> None: frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Create", command=self.click_create) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) self.edit_button = ttk.Button( frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) - self.edit_button.grid(row=0, column=1, sticky="ew", padx=PADX) + self.edit_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX) self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete ) - self.delete_button.grid(row=0, column=2, sticky="ew") + self.delete_button.grid(row=0, column=2, sticky=tk.EW) def draw_buttons(self) -> None: frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.click_save) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def reset_values(self) -> None: self.name.set("") diff --git a/daemon/core/gui/dialogs/dialog.py b/daemon/core/gui/dialogs/dialog.py index 962170e7..ce05a5d5 100644 --- a/daemon/core/gui/dialogs/dialog.py +++ b/daemon/core/gui/dialogs/dialog.py @@ -30,7 +30,7 @@ class Dialog(tk.Toplevel): self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.top: ttk.Frame = ttk.Frame(self, padding=DIALOG_PAD) - self.top.grid(sticky="nsew") + self.top.grid(sticky=tk.NSEW) def show(self) -> None: self.transient(self.master) @@ -44,6 +44,6 @@ class Dialog(tk.Toplevel): def draw_spacer(self, row: int = None) -> None: frame = ttk.Frame(self.top) - frame.grid(row=row, sticky="nsew") + frame.grid(row=row, sticky=tk.NSEW) frame.rowconfigure(0, weight=1) self.top.rowconfigure(frame.grid_info()["row"], weight=1) diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index 019eeaa9..f7925d16 100644 --- a/daemon/core/gui/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -33,20 +33,20 @@ class GlobalEmaneDialog(Dialog): self.top, self.app, session.emane_config, self.enabled ) self.config_frame.draw_config() - self.config_frame.grid(sticky="nsew", pady=PADY) + self.config_frame.grid(sticky=tk.NSEW, pady=PADY) self.draw_spacer() self.draw_buttons() def draw_buttons(self) -> None: frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) for i in range(2): frame.columnconfigure(i, weight=1) state = tk.NORMAL if self.enabled else tk.DISABLED button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def click_apply(self) -> None: self.config_frame.parse_config() @@ -87,20 +87,20 @@ class EmaneModelDialog(Dialog): self.top.rowconfigure(0, weight=1) self.config_frame = ConfigFrame(self.top, self.app, self.config, self.enabled) self.config_frame.draw_config() - self.config_frame.grid(sticky="nsew", pady=PADY) + self.config_frame.grid(sticky=tk.NSEW, pady=PADY) self.draw_spacer() self.draw_buttons() def draw_buttons(self) -> None: frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) for i in range(2): frame.columnconfigure(i, weight=1) state = tk.NORMAL if self.enabled else tk.DISABLED button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def click_apply(self) -> None: self.config_frame.parse_config() @@ -156,30 +156,30 @@ class EmaneConfigDialog(Dialog): ), ) button.image = image - button.grid(sticky="ew", pady=PADY) + button.grid(sticky=tk.EW, pady=PADY) def draw_emane_models(self) -> None: """ create a combobox that has all the known emane models """ frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Model") - label.grid(row=0, column=0, sticky="w") + label.grid(row=0, column=0, sticky=tk.W) # create combo box and its binding state = "readonly" if self.enabled else tk.DISABLED combobox = ttk.Combobox( frame, textvariable=self.emane_model, values=self.emane_models, state=state ) - combobox.grid(row=0, column=1, sticky="ew") + combobox.grid(row=0, column=1, sticky=tk.EW) combobox.bind("<>", self.emane_model_change) def draw_emane_buttons(self) -> None: frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) for i in range(2): frame.columnconfigure(i, weight=1) @@ -192,7 +192,7 @@ class EmaneConfigDialog(Dialog): command=self.click_model_config, ) self.emane_model_button.image = image - self.emane_model_button.grid(row=0, column=0, padx=PADX, sticky="ew") + self.emane_model_button.grid(row=0, column=0, padx=PADX, sticky=tk.EW) image = Images.get(ImageEnum.EDITNODE, 16) button = ttk.Button( @@ -203,18 +203,18 @@ class EmaneConfigDialog(Dialog): command=self.click_emane_config, ) button.image = image - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def draw_apply_and_cancel(self) -> None: frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) for i in range(2): frame.columnconfigure(i, weight=1) state = tk.NORMAL if self.enabled else tk.DISABLED button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state) - button.grid(row=0, column=0, padx=PADX, sticky="ew") + button.grid(row=0, column=0, padx=PADX, sticky=tk.EW) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def click_emane_config(self) -> None: dialog = GlobalEmaneDialog(self, self.app) diff --git a/daemon/core/gui/dialogs/emaneinstall.py b/daemon/core/gui/dialogs/emaneinstall.py index 3ad9396b..9f9f2f5c 100644 --- a/daemon/core/gui/dialogs/emaneinstall.py +++ b/daemon/core/gui/dialogs/emaneinstall.py @@ -1,3 +1,4 @@ +import tkinter as tk import webbrowser from tkinter import ttk @@ -13,13 +14,13 @@ class EmaneInstallDialog(Dialog): def draw(self) -> None: self.top.columnconfigure(0, weight=1) label = ttk.Label(self.top, text="EMANE needs to be installed!") - label.grid(sticky="ew", pady=PADY) + label.grid(sticky=tk.EW, pady=PADY) button = ttk.Button( self.top, text="EMANE Documentation", command=self.click_doc ) - button.grid(sticky="ew", pady=PADY) + button.grid(sticky=tk.EW, pady=PADY) button = ttk.Button(self.top, text="Close", command=self.destroy) - button.grid(sticky="ew") + button.grid(sticky=tk.EW) def click_doc(self) -> None: webbrowser.open_new("https://coreemu.github.io/core/emane.html") diff --git a/daemon/core/gui/dialogs/executepython.py b/daemon/core/gui/dialogs/executepython.py index a4516df1..0bef9dc1 100644 --- a/daemon/core/gui/dialogs/executepython.py +++ b/daemon/core/gui/dialogs/executepython.py @@ -25,13 +25,13 @@ class ExecutePythonDialog(Dialog): frame = ttk.Frame(self.top, padding=FRAME_PAD) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - frame.grid(row=i, column=0, sticky="nsew") + frame.grid(row=i, column=0, sticky=tk.NSEW) i = i + 1 var = tk.StringVar(value="") self.file_entry = ttk.Entry(frame, textvariable=var) - self.file_entry.grid(row=0, column=0, sticky="ew") + self.file_entry.grid(row=0, column=0, sticky=tk.EW) button = ttk.Button(frame, text="...", command=self.select_file) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) self.top.columnconfigure(0, weight=1) button = ttk.Checkbutton( @@ -40,18 +40,18 @@ class ExecutePythonDialog(Dialog): variable=self.with_options, command=self.add_options, ) - button.grid(row=i, column=0, sticky="ew") + button.grid(row=i, column=0, sticky=tk.EW) i = i + 1 label = ttk.Label( self.top, text="Any command-line options for running the Python script" ) - label.grid(row=i, column=0, sticky="ew") + label.grid(row=i, column=0, sticky=tk.EW) i = i + 1 self.option_entry = ttk.Entry( self.top, textvariable=self.options, state="disabled" ) - self.option_entry.grid(row=i, column=0, sticky="ew") + self.option_entry.grid(row=i, column=0, sticky=tk.EW) i = i + 1 frame = ttk.Frame(self.top, padding=FRAME_PAD) @@ -59,9 +59,9 @@ class ExecutePythonDialog(Dialog): frame.columnconfigure(1, weight=1) frame.grid(row=i, column=0) button = ttk.Button(frame, text="Execute", command=self.script_execute) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="ew", padx=PADX) + button.grid(row=0, column=1, sticky=tk.EW, padx=PADX) def add_options(self) -> None: if self.with_options.get(): diff --git a/daemon/core/gui/dialogs/find.py b/daemon/core/gui/dialogs/find.py index a4600847..6bfac47b 100644 --- a/daemon/core/gui/dialogs/find.py +++ b/daemon/core/gui/dialogs/find.py @@ -25,25 +25,25 @@ class FindDialog(Dialog): # Find node frame frame = ttk.Frame(self.top, padding=FRAME_PAD) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Find:") label.grid() entry = ttk.Entry(frame, textvariable=self.find_text) - entry.grid(row=0, column=1, sticky="nsew") + entry.grid(row=0, column=1, sticky=tk.NSEW) # node list frame frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - frame.grid(sticky="nsew", pady=PADY) + frame.grid(sticky=tk.NSEW, pady=PADY) self.tree = ttk.Treeview( frame, columns=("nodeid", "name", "location", "detail"), show="headings", selectmode=tk.BROWSE, ) - self.tree.grid(sticky="nsew", pady=PADY) + self.tree.grid(sticky=tk.NSEW, pady=PADY) style = ttk.Style() heading_size = int(self.app.guiconfig.scale * 10) style.configure("Treeview.Heading", font=(None, heading_size, "bold")) @@ -57,21 +57,21 @@ class FindDialog(Dialog): self.tree.heading("detail", text="Detail") self.tree.bind("<>", self.click_select) yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) - yscrollbar.grid(row=0, column=1, sticky="ns") + yscrollbar.grid(row=0, column=1, sticky=tk.NS) self.tree.configure(yscrollcommand=yscrollbar.set) xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) - xscrollbar.grid(row=1, sticky="ew") + xscrollbar.grid(row=1, sticky=tk.EW) self.tree.configure(xscrollcommand=xscrollbar.set) # button frame frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Find", command=self.find_node) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.close_dialog) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def clear_treeview_items(self) -> None: """ diff --git a/daemon/core/gui/dialogs/hooks.py b/daemon/core/gui/dialogs/hooks.py index ce7caf29..e831b4f9 100644 --- a/daemon/core/gui/dialogs/hooks.py +++ b/daemon/core/gui/dialogs/hooks.py @@ -27,14 +27,14 @@ class HookDialog(Dialog): # name and states frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) frame.columnconfigure(0, weight=2) frame.columnconfigure(1, weight=7) frame.columnconfigure(2, weight=1) label = ttk.Label(frame, text="Name") - label.grid(row=0, column=0, sticky="ew", padx=PADX) + label.grid(row=0, column=0, sticky=tk.EW, padx=PADX) entry = ttk.Entry(frame, textvariable=self.name) - entry.grid(row=0, column=1, sticky="ew", padx=PADX) + entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX) values = tuple(x.name for x in SessionState) initial_state = SessionState.RUNTIME.name self.state.set(initial_state) @@ -42,7 +42,7 @@ class HookDialog(Dialog): combobox = ttk.Combobox( frame, textvariable=self.state, values=values, state="readonly" ) - combobox.grid(row=0, column=2, sticky="ew") + combobox.grid(row=0, column=2, sticky=tk.EW) combobox.bind("<>", self.state_change) # data @@ -55,17 +55,17 @@ class HookDialog(Dialog): "# specified state\n" ), ) - self.codetext.grid(sticky="nsew", pady=PADY) + self.codetext.grid(sticky=tk.NSEW, pady=PADY) # button row frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=lambda: self.save()) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def state_change(self, event: tk.Event) -> None: if self.editing: @@ -110,7 +110,7 @@ class HooksDialog(Dialog): self.top.rowconfigure(0, weight=1) listbox_scroll = ListboxScroll(self.top) - listbox_scroll.grid(sticky="nsew", pady=PADY) + listbox_scroll.grid(sticky=tk.NSEW, pady=PADY) self.listbox = listbox_scroll.listbox self.listbox.bind("<>", self.select) session = self.app.core.session @@ -118,21 +118,21 @@ class HooksDialog(Dialog): self.listbox.insert(tk.END, file) frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) for i in range(4): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Create", command=self.click_create) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) self.edit_button = ttk.Button( frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) - self.edit_button.grid(row=0, column=1, sticky="ew", padx=PADX) + self.edit_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX) self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete ) - self.delete_button.grid(row=0, column=2, sticky="ew", padx=PADX) + self.delete_button.grid(row=0, column=2, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) - button.grid(row=0, column=3, sticky="ew") + button.grid(row=0, column=3, sticky=tk.EW) def click_create(self) -> None: dialog = HookDialog(self, self.app) diff --git a/daemon/core/gui/dialogs/ipdialog.py b/daemon/core/gui/dialogs/ipdialog.py index 351bfffc..a09ca097 100644 --- a/daemon/core/gui/dialogs/ipdialog.py +++ b/daemon/core/gui/dialogs/ipdialog.py @@ -34,7 +34,7 @@ class IpConfigDialog(Dialog): frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.rowconfigure(0, weight=1) - frame.grid(sticky="nsew", pady=PADY) + frame.grid(sticky=tk.NSEW, pady=PADY) ip4_frame = ttk.LabelFrame(frame, text="IPv4", padding=FRAME_PAD) ip4_frame.columnconfigure(0, weight=1) @@ -42,23 +42,23 @@ class IpConfigDialog(Dialog): ip4_frame.grid(row=0, column=0, stick="nsew") self.ip4_listbox = ListboxScroll(ip4_frame) self.ip4_listbox.listbox.bind("<>", self.select_ip4) - self.ip4_listbox.grid(sticky="nsew", pady=PADY) + self.ip4_listbox.grid(sticky=tk.NSEW, pady=PADY) for index, ip4 in enumerate(self.ip4s): self.ip4_listbox.listbox.insert(tk.END, ip4) if self.ip4 == ip4: self.ip4_listbox.listbox.select_set(index) self.ip4_entry = ttk.Entry(ip4_frame) - self.ip4_entry.grid(sticky="ew", pady=PADY) + self.ip4_entry.grid(sticky=tk.EW, pady=PADY) ip4_button_frame = ttk.Frame(ip4_frame) ip4_button_frame.columnconfigure(0, weight=1) ip4_button_frame.columnconfigure(1, weight=1) - ip4_button_frame.grid(sticky="ew") + ip4_button_frame.grid(sticky=tk.EW) ip4_add = ttk.Button(ip4_button_frame, text="Add", command=self.click_add_ip4) - ip4_add.grid(row=0, column=0, sticky="ew") + ip4_add.grid(row=0, column=0, sticky=tk.EW) ip4_del = ttk.Button( ip4_button_frame, text="Delete", command=self.click_del_ip4 ) - ip4_del.grid(row=0, column=1, sticky="ew") + ip4_del.grid(row=0, column=1, sticky=tk.EW) ip6_frame = ttk.LabelFrame(frame, text="IPv6", padding=FRAME_PAD) ip6_frame.columnconfigure(0, weight=1) @@ -66,23 +66,23 @@ class IpConfigDialog(Dialog): ip6_frame.grid(row=0, column=1, stick="nsew") self.ip6_listbox = ListboxScroll(ip6_frame) self.ip6_listbox.listbox.bind("<>", self.select_ip6) - self.ip6_listbox.grid(sticky="nsew", pady=PADY) + self.ip6_listbox.grid(sticky=tk.NSEW, pady=PADY) for index, ip6 in enumerate(self.ip6s): self.ip6_listbox.listbox.insert(tk.END, ip6) if self.ip6 == ip6: self.ip6_listbox.listbox.select_set(index) self.ip6_entry = ttk.Entry(ip6_frame) - self.ip6_entry.grid(sticky="ew", pady=PADY) + self.ip6_entry.grid(sticky=tk.EW, pady=PADY) ip6_button_frame = ttk.Frame(ip6_frame) ip6_button_frame.columnconfigure(0, weight=1) ip6_button_frame.columnconfigure(1, weight=1) - ip6_button_frame.grid(sticky="ew") + ip6_button_frame.grid(sticky=tk.EW) ip6_add = ttk.Button(ip6_button_frame, text="Add", command=self.click_add_ip6) - ip6_add.grid(row=0, column=0, sticky="ew") + ip6_add.grid(row=0, column=0, sticky=tk.EW) ip6_del = ttk.Button( ip6_button_frame, text="Delete", command=self.click_del_ip6 ) - ip6_del.grid(row=0, column=1, sticky="ew") + ip6_del.grid(row=0, column=1, sticky=tk.EW) # draw buttons frame = ttk.Frame(self.top) @@ -90,9 +90,9 @@ class IpConfigDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.click_save) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def click_add_ip4(self) -> None: ip4 = self.ip4_entry.get() diff --git a/daemon/core/gui/dialogs/linkconfig.py b/daemon/core/gui/dialogs/linkconfig.py index 1aa2d7f8..a09cfe7f 100644 --- a/daemon/core/gui/dialogs/linkconfig.py +++ b/daemon/core/gui/dialogs/linkconfig.py @@ -73,11 +73,11 @@ class LinkConfigurationDialog(Dialog): label = ttk.Label( self.top, text=f"Link from {source_name} to {dest_name}", anchor=tk.CENTER ) - label.grid(row=0, column=0, sticky="ew", pady=PADY) + label.grid(row=0, column=0, sticky=tk.EW, pady=PADY) frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) - frame.grid(row=1, column=0, sticky="ew", pady=PADY) + frame.grid(row=1, column=0, sticky=tk.EW, pady=PADY) if self.is_symmetric: button = ttk.Button( frame, textvariable=self.symmetry_var, command=self.change_symmetry @@ -86,25 +86,25 @@ class LinkConfigurationDialog(Dialog): button = ttk.Button( frame, textvariable=self.symmetry_var, command=self.change_symmetry ) - button.grid(sticky="ew") + button.grid(sticky=tk.EW) if self.is_symmetric: self.symmetric_frame = self.get_frame() - self.symmetric_frame.grid(row=2, column=0, sticky="ew", pady=PADY) + self.symmetric_frame.grid(row=2, column=0, sticky=tk.EW, pady=PADY) else: self.asymmetric_frame = self.get_frame() - self.asymmetric_frame.grid(row=2, column=0, sticky="ew", pady=PADY) + self.asymmetric_frame.grid(row=2, column=0, sticky=tk.EW, pady=PADY) self.draw_spacer(row=3) frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - frame.grid(row=4, column=0, sticky="ew") + frame.grid(row=4, column=0, sticky=tk.EW) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def get_frame(self) -> ttk.Frame: frame = ttk.Frame(self.top) @@ -115,76 +115,76 @@ class LinkConfigurationDialog(Dialog): label_name = "Asymmetric Effects: Downstream / Upstream " row = 0 label = ttk.Label(frame, text=label_name, anchor=tk.CENTER) - label.grid(row=row, column=0, columnspan=2, sticky="ew", pady=PADY) + label.grid(row=row, column=0, columnspan=2, sticky=tk.EW, pady=PADY) row = row + 1 label = ttk.Label(frame, text="Bandwidth (bps)") - label.grid(row=row, column=0, sticky="ew") + label.grid(row=row, column=0, sticky=tk.EW) entry = validation.PositiveIntEntry( frame, empty_enabled=False, textvariable=self.bandwidth ) - entry.grid(row=row, column=1, sticky="ew", pady=PADY) + 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_bandwidth ) - entry.grid(row=row, column=2, sticky="ew", pady=PADY) + entry.grid(row=row, column=2, sticky=tk.EW, pady=PADY) row = row + 1 label = ttk.Label(frame, text="Delay (us)") - label.grid(row=row, column=0, sticky="ew") + label.grid(row=row, column=0, sticky=tk.EW) entry = validation.PositiveIntEntry( frame, empty_enabled=False, textvariable=self.delay ) - entry.grid(row=row, column=1, sticky="ew", pady=PADY) + 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_delay ) - entry.grid(row=row, column=2, sticky="ew", pady=PADY) + entry.grid(row=row, column=2, sticky=tk.EW, pady=PADY) row = row + 1 label = ttk.Label(frame, text="Jitter (us)") - label.grid(row=row, column=0, sticky="ew") + label.grid(row=row, column=0, sticky=tk.EW) entry = validation.PositiveIntEntry( frame, empty_enabled=False, textvariable=self.jitter ) - entry.grid(row=row, column=1, sticky="ew", pady=PADY) + 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_jitter ) - entry.grid(row=row, column=2, sticky="ew", pady=PADY) + entry.grid(row=row, column=2, sticky=tk.EW, pady=PADY) row = row + 1 label = ttk.Label(frame, text="Loss (%)") - label.grid(row=row, column=0, sticky="ew") + label.grid(row=row, column=0, sticky=tk.EW) entry = validation.PositiveFloatEntry( frame, empty_enabled=False, textvariable=self.loss ) - entry.grid(row=row, column=1, sticky="ew", pady=PADY) + entry.grid(row=row, column=1, sticky=tk.EW, pady=PADY) if not self.is_symmetric: entry = validation.PositiveFloatEntry( frame, empty_enabled=False, textvariable=self.down_loss ) - entry.grid(row=row, column=2, sticky="ew", pady=PADY) + entry.grid(row=row, column=2, sticky=tk.EW, pady=PADY) row = row + 1 label = ttk.Label(frame, text="Duplicate (%)") - label.grid(row=row, column=0, sticky="ew") + label.grid(row=row, column=0, sticky=tk.EW) entry = validation.PositiveIntEntry( frame, empty_enabled=False, textvariable=self.duplicate ) - entry.grid(row=row, column=1, sticky="ew", pady=PADY) + 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_duplicate ) - entry.grid(row=row, column=2, sticky="ew", pady=PADY) + 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="ew") + label.grid(row=row, column=0, sticky=tk.EW) self.color_button = tk.Button( frame, textvariable=self.color, @@ -194,15 +194,15 @@ class LinkConfigurationDialog(Dialog): highlightthickness=0, command=self.click_color, ) - self.color_button.grid(row=row, column=1, sticky="ew", pady=PADY) + self.color_button.grid(row=row, column=1, sticky=tk.EW, pady=PADY) row = row + 1 label = ttk.Label(frame, text="Width") - label.grid(row=row, column=0, sticky="ew") + label.grid(row=row, column=0, sticky=tk.EW) entry = validation.PositiveFloatEntry( frame, empty_enabled=False, textvariable=self.width ) - entry.grid(row=row, column=1, sticky="ew", pady=PADY) + entry.grid(row=row, column=1, sticky=tk.EW, pady=PADY) return frame diff --git a/daemon/core/gui/dialogs/macdialog.py b/daemon/core/gui/dialogs/macdialog.py index 4d89439b..c8cd7f45 100644 --- a/daemon/core/gui/dialogs/macdialog.py +++ b/daemon/core/gui/dialogs/macdialog.py @@ -28,7 +28,7 @@ class MacConfigDialog(Dialog): "provided value below and increment by value in order." ) label = ttk.Label(self.top, text=text) - label.grid(sticky="ew", pady=PADY) + label.grid(sticky=tk.EW, pady=PADY) # draw input frame = ttk.Frame(self.top) @@ -36,9 +36,9 @@ class MacConfigDialog(Dialog): frame.columnconfigure(1, weight=3) frame.grid(stick="ew", pady=PADY) label = ttk.Label(frame, text="Starting MAC") - label.grid(row=0, column=0, sticky="ew", padx=PADX) + label.grid(row=0, column=0, sticky=tk.EW, padx=PADX) entry = ttk.Entry(frame, textvariable=self.mac_var) - entry.grid(row=0, column=1, sticky="ew") + entry.grid(row=0, column=1, sticky=tk.EW) # draw buttons frame = ttk.Frame(self.top) @@ -46,9 +46,9 @@ class MacConfigDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.click_save) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def click_save(self) -> None: mac = self.mac_var.get() diff --git a/daemon/core/gui/dialogs/mobilityconfig.py b/daemon/core/gui/dialogs/mobilityconfig.py index 857167be..80c3ca22 100644 --- a/daemon/core/gui/dialogs/mobilityconfig.py +++ b/daemon/core/gui/dialogs/mobilityconfig.py @@ -1,6 +1,7 @@ """ mobility configuration """ +import tkinter as tk from tkinter import ttk from typing import TYPE_CHECKING, Dict, Optional @@ -37,20 +38,20 @@ class MobilityConfigDialog(Dialog): self.top.rowconfigure(0, weight=1) self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame.draw_config() - self.config_frame.grid(sticky="nsew", pady=PADY) + self.config_frame.grid(sticky=tk.NSEW, pady=PADY) self.draw_apply_buttons() def draw_apply_buttons(self) -> None: frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, padx=PADX, sticky="ew") + button.grid(row=0, column=0, padx=PADX, sticky=tk.EW) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def click_apply(self) -> None: self.config_frame.parse_config() diff --git a/daemon/core/gui/dialogs/mobilityplayer.py b/daemon/core/gui/dialogs/mobilityplayer.py index 1bee97d2..352a3739 100644 --- a/daemon/core/gui/dialogs/mobilityplayer.py +++ b/daemon/core/gui/dialogs/mobilityplayer.py @@ -74,30 +74,30 @@ class MobilityPlayerDialog(Dialog): file_name = config["file"].value label = ttk.Label(self.top, text=file_name) - label.grid(sticky="ew", pady=PADY) + label.grid(sticky=tk.EW, pady=PADY) self.progressbar = ttk.Progressbar(self.top, mode="indeterminate") - self.progressbar.grid(sticky="ew", pady=PADY) + self.progressbar.grid(sticky=tk.EW, pady=PADY) frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) for i in range(3): frame.columnconfigure(i, weight=1) image = self.app.get_icon(ImageEnum.START, ICON_SIZE) self.play_button = ttk.Button(frame, image=image, command=self.click_play) self.play_button.image = image - self.play_button.grid(row=0, column=0, sticky="ew", padx=PADX) + self.play_button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) image = self.app.get_icon(ImageEnum.PAUSE, ICON_SIZE) self.pause_button = ttk.Button(frame, image=image, command=self.click_pause) self.pause_button.image = image - self.pause_button.grid(row=0, column=1, sticky="ew", padx=PADX) + self.pause_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX) image = self.app.get_icon(ImageEnum.STOP, ICON_SIZE) self.stop_button = ttk.Button(frame, image=image, command=self.click_stop) self.stop_button.image = image - self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX) + self.stop_button.grid(row=0, column=2, sticky=tk.EW, padx=PADX) loop = tk.IntVar(value=int(config["loop"].value == "1")) checkbutton = ttk.Checkbutton( diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index 604e933a..d8103283 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -126,12 +126,12 @@ class NodeConfigDialog(Dialog): # field frame frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) frame.columnconfigure(1, weight=1) # icon field label = ttk.Label(frame, text="Icon") - label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) + label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY) self.image_button = ttk.Button( frame, text="Icon", @@ -139,49 +139,49 @@ class NodeConfigDialog(Dialog): compound=tk.NONE, command=self.click_icon, ) - self.image_button.grid(row=row, column=1, sticky="ew") + self.image_button.grid(row=row, column=1, sticky=tk.EW) row += 1 # name field label = ttk.Label(frame, text="Name") - label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) + label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY) entry = validation.NodeNameEntry(frame, textvariable=self.name, state=state) - entry.grid(row=row, column=1, sticky="ew") + entry.grid(row=row, column=1, sticky=tk.EW) row += 1 # node type field if NodeUtils.is_model_node(self.node.type): label = ttk.Label(frame, text="Type") - label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) + label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY) combobox = ttk.Combobox( frame, textvariable=self.type, values=list(NodeUtils.NODE_MODELS), state=combo_state, ) - combobox.grid(row=row, column=1, sticky="ew") + combobox.grid(row=row, column=1, sticky=tk.EW) row += 1 # container image field if NodeUtils.is_image_node(self.node.type): label = ttk.Label(frame, text="Image") - label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) + label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY) entry = ttk.Entry(frame, textvariable=self.container_image, state=state) - entry.grid(row=row, column=1, sticky="ew") + entry.grid(row=row, column=1, sticky=tk.EW) row += 1 if NodeUtils.is_container_node(self.node.type): # server - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Server") - label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) + label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY) servers = ["localhost"] servers.extend(list(sorted(self.app.core.servers.keys()))) combobox = ttk.Combobox( frame, textvariable=self.server, values=servers, state=combo_state ) - combobox.grid(row=row, column=1, sticky="ew") + combobox.grid(row=row, column=1, sticky=tk.EW) row += 1 if NodeUtils.is_rj45_node(self.node.type): @@ -190,7 +190,7 @@ class NodeConfigDialog(Dialog): ifaces = ListboxScroll(frame) ifaces.listbox.config(state=state) ifaces.grid( - row=row, column=0, columnspan=2, sticky="ew", padx=PADX, pady=PADY + row=row, column=0, columnspan=2, sticky=tk.EW, padx=PADX, pady=PADY ) for inf in sorted(response.ifaces[:]): ifaces.listbox.insert(tk.END, inf) @@ -206,13 +206,13 @@ class NodeConfigDialog(Dialog): def draw_ifaces(self) -> None: notebook = ttk.Notebook(self.top) - notebook.grid(sticky="nsew", pady=PADY) + notebook.grid(sticky=tk.NSEW, pady=PADY) self.top.rowconfigure(notebook.grid_info()["row"], weight=1) state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL for iface_id in sorted(self.canvas_node.ifaces): iface = self.canvas_node.ifaces[iface_id] tab = ttk.Frame(notebook, padding=FRAME_PAD) - tab.grid(sticky="nsew", pady=PADY) + tab.grid(sticky=tk.NSEW, pady=PADY) tab.columnconfigure(1, weight=1) tab.columnconfigure(2, weight=1) notebook.add(tab, text=iface.name) @@ -226,7 +226,7 @@ class NodeConfigDialog(Dialog): text=f"Configure EMANE {emane_model}", command=lambda: self.click_emane_config(emane_model, iface.id), ) - button.grid(row=row, sticky="ew", columnspan=3, pady=PADY) + button.grid(row=row, sticky=tk.EW, columnspan=3, pady=PADY) row += 1 label = ttk.Label(tab, text="MAC") @@ -243,7 +243,7 @@ class NodeConfigDialog(Dialog): checkbutton.grid(row=row, column=1, padx=PADX) mac = tk.StringVar(value=iface.mac) entry = ttk.Entry(tab, textvariable=mac, state=mac_state) - entry.grid(row=row, column=2, sticky="ew") + entry.grid(row=row, column=2, sticky=tk.EW) func = partial(mac_auto, is_auto, entry, mac) checkbutton.config(command=func) row += 1 @@ -255,7 +255,7 @@ class NodeConfigDialog(Dialog): ip4_net = f"{iface.ip4}/{iface.ip4_mask}" ip4 = tk.StringVar(value=ip4_net) entry = ttk.Entry(tab, textvariable=ip4, state=state) - entry.grid(row=row, column=1, columnspan=2, sticky="ew") + entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW) row += 1 label = ttk.Label(tab, text="IPv6") @@ -265,21 +265,21 @@ class NodeConfigDialog(Dialog): ip6_net = f"{iface.ip6}/{iface.ip6_mask}" ip6 = tk.StringVar(value=ip6_net) entry = ttk.Entry(tab, textvariable=ip6, state=state) - entry.grid(row=row, column=1, columnspan=2, sticky="ew") + entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW) self.ifaces[iface.id] = InterfaceData(is_auto, mac, ip4, ip6) def draw_buttons(self) -> None: frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, padx=PADX, sticky="ew") + button.grid(row=0, column=0, padx=PADX, sticky=tk.EW) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def click_emane_config(self, emane_model: str, iface_id: int) -> None: dialog = EmaneModelDialog( diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeconfigservice.py index dee34f71..1c67e4b3 100644 --- a/daemon/core/gui/dialogs/nodeconfigservice.py +++ b/daemon/core/gui/dialogs/nodeconfigservice.py @@ -41,32 +41,32 @@ class NodeConfigServiceDialog(Dialog): for i in range(3): frame.columnconfigure(i, weight=1) label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD) - label_frame.grid(row=0, column=0, sticky="nsew") + label_frame.grid(row=0, column=0, sticky=tk.NSEW) label_frame.rowconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1) self.groups = ListboxScroll(label_frame) - self.groups.grid(sticky="nsew") + self.groups.grid(sticky=tk.NSEW) for group in sorted(self.app.core.config_services_groups): self.groups.listbox.insert(tk.END, group) self.groups.listbox.bind("<>", self.handle_group_change) self.groups.listbox.selection_set(0) label_frame = ttk.LabelFrame(frame, text="Services") - label_frame.grid(row=0, column=1, sticky="nsew") + label_frame.grid(row=0, column=1, sticky=tk.NSEW) label_frame.columnconfigure(0, weight=1) label_frame.rowconfigure(0, weight=1) self.services = CheckboxList( label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD ) - self.services.grid(sticky="nsew") + self.services.grid(sticky=tk.NSEW) label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD) - label_frame.grid(row=0, column=2, sticky="nsew") + label_frame.grid(row=0, column=2, sticky=tk.NSEW) label_frame.rowconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1) self.current = ListboxScroll(label_frame) - self.current.grid(sticky="nsew") + self.current.grid(sticky=tk.NSEW) self.draw_current_services() frame = ttk.Frame(self.top) @@ -74,13 +74,13 @@ class NodeConfigServiceDialog(Dialog): for i in range(4): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Configure", command=self.click_configure) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Save", command=self.click_save) - button.grid(row=0, column=1, sticky="ew", padx=PADX) + button.grid(row=0, column=1, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Remove", command=self.click_remove) - button.grid(row=0, column=2, sticky="ew", padx=PADX) + button.grid(row=0, column=2, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.click_cancel) - button.grid(row=0, column=3, sticky="ew") + button.grid(row=0, column=3, sticky=tk.EW) # trigger group change self.handle_group_change() diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index a56736d5..5ec78a93 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -37,31 +37,31 @@ class NodeServiceDialog(Dialog): for i in range(3): frame.columnconfigure(i, weight=1) label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD) - label_frame.grid(row=0, column=0, sticky="nsew") + label_frame.grid(row=0, column=0, sticky=tk.NSEW) label_frame.rowconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1) self.groups = ListboxScroll(label_frame) - self.groups.grid(sticky="nsew") + self.groups.grid(sticky=tk.NSEW) for group in sorted(self.app.core.services): self.groups.listbox.insert(tk.END, group) self.groups.listbox.bind("<>", self.handle_group_change) self.groups.listbox.selection_set(0) label_frame = ttk.LabelFrame(frame, text="Services") - label_frame.grid(row=0, column=1, sticky="nsew") + label_frame.grid(row=0, column=1, sticky=tk.NSEW) label_frame.columnconfigure(0, weight=1) label_frame.rowconfigure(0, weight=1) self.services = CheckboxList( label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD ) - self.services.grid(sticky="nsew") + self.services.grid(sticky=tk.NSEW) label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD) - label_frame.grid(row=0, column=2, sticky="nsew") + label_frame.grid(row=0, column=2, sticky=tk.NSEW) label_frame.rowconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1) self.current = ListboxScroll(label_frame) - self.current.grid(sticky="nsew") + self.current.grid(sticky=tk.NSEW) for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) if self.is_custom_service(service): @@ -72,13 +72,13 @@ class NodeServiceDialog(Dialog): for i in range(4): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Configure", command=self.click_configure) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Save", command=self.click_save) - button.grid(row=0, column=1, sticky="ew", padx=PADX) + button.grid(row=0, column=1, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Remove", command=self.click_remove) - button.grid(row=0, column=2, sticky="ew", padx=PADX) + button.grid(row=0, column=2, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=3, sticky="ew") + button.grid(row=0, column=3, sticky=tk.EW) # trigger group change self.handle_group_change() diff --git a/daemon/core/gui/dialogs/observers.py b/daemon/core/gui/dialogs/observers.py index 286fc2c9..b815d45b 100644 --- a/daemon/core/gui/dialogs/observers.py +++ b/daemon/core/gui/dialogs/observers.py @@ -33,60 +33,60 @@ class ObserverDialog(Dialog): def draw_listbox(self) -> None: listbox_scroll = ListboxScroll(self.top) - listbox_scroll.grid(sticky="nsew", pady=PADY) + listbox_scroll.grid(sticky=tk.NSEW, pady=PADY) listbox_scroll.columnconfigure(0, weight=1) listbox_scroll.rowconfigure(0, weight=1) self.observers = listbox_scroll.listbox - self.observers.grid(row=0, column=0, sticky="nsew") + self.observers.grid(row=0, column=0, sticky=tk.NSEW) self.observers.bind("<>", self.handle_observer_change) for name in sorted(self.app.core.custom_observers): self.observers.insert(tk.END, name) def draw_form_fields(self) -> None: frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Name") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky=tk.W, padx=PADX) entry = ttk.Entry(frame, textvariable=self.name) - entry.grid(row=0, column=1, sticky="ew") + entry.grid(row=0, column=1, sticky=tk.EW) label = ttk.Label(frame, text="Command") - label.grid(row=1, column=0, sticky="w", padx=PADX) + label.grid(row=1, column=0, sticky=tk.W, padx=PADX) entry = ttk.Entry(frame, textvariable=self.cmd) - entry.grid(row=1, column=1, sticky="ew") + entry.grid(row=1, column=1, sticky=tk.EW) def draw_config_buttons(self) -> None: frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Create", command=self.click_create) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) self.save_button = ttk.Button( frame, text="Save", state=tk.DISABLED, command=self.click_save ) - self.save_button.grid(row=0, column=1, sticky="ew", padx=PADX) + self.save_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX) self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete ) - self.delete_button.grid(row=0, column=2, sticky="ew") + self.delete_button.grid(row=0, column=2, sticky=tk.EW) def draw_apply_buttons(self) -> None: frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.click_save_config) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def click_save_config(self) -> None: self.app.guiconfig.observers.clear() diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index 839ebd3b..d0c58dfa 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -34,42 +34,42 @@ class PreferencesDialog(Dialog): def draw_preferences(self) -> None: frame = ttk.LabelFrame(self.top, text="Preferences", padding=FRAME_PAD) - frame.grid(sticky="nsew", pady=PADY) + frame.grid(sticky=tk.NSEW, pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Theme") - label.grid(row=0, column=0, pady=PADY, padx=PADX, sticky="w") + label.grid(row=0, column=0, pady=PADY, padx=PADX, sticky=tk.W) themes = self.app.style.theme_names() combobox = ttk.Combobox( frame, textvariable=self.theme, values=themes, state="readonly" ) combobox.set(self.theme.get()) - combobox.grid(row=0, column=1, sticky="ew") + combobox.grid(row=0, column=1, sticky=tk.EW) combobox.bind("<>", self.theme_change) label = ttk.Label(frame, text="Editor") - label.grid(row=1, column=0, pady=PADY, padx=PADX, sticky="w") + label.grid(row=1, column=0, pady=PADY, padx=PADX, sticky=tk.W) combobox = ttk.Combobox( frame, textvariable=self.editor, values=appconfig.EDITORS, state="readonly" ) - combobox.grid(row=1, column=1, sticky="ew") + combobox.grid(row=1, column=1, sticky=tk.EW) label = ttk.Label(frame, text="Terminal") - label.grid(row=2, column=0, pady=PADY, padx=PADX, sticky="w") + label.grid(row=2, column=0, pady=PADY, padx=PADX, sticky=tk.W) terminals = sorted(appconfig.TERMINALS.values()) combobox = ttk.Combobox(frame, textvariable=self.terminal, values=terminals) - combobox.grid(row=2, column=1, sticky="ew") + combobox.grid(row=2, column=1, sticky=tk.EW) label = ttk.Label(frame, text="3D GUI") - label.grid(row=3, column=0, pady=PADY, padx=PADX, sticky="w") + label.grid(row=3, column=0, pady=PADY, padx=PADX, sticky=tk.W) entry = ttk.Entry(frame, textvariable=self.gui3d) - entry.grid(row=3, column=1, sticky="ew") + entry.grid(row=3, column=1, sticky=tk.EW) label = ttk.Label(frame, text="Scaling") - label.grid(row=4, column=0, pady=PADY, padx=PADX, sticky="w") + label.grid(row=4, column=0, pady=PADY, padx=PADX, sticky=tk.W) scale_frame = ttk.Frame(frame) - scale_frame.grid(row=4, column=1, sticky="ew") + scale_frame.grid(row=4, column=1, sticky=tk.EW) scale_frame.columnconfigure(0, weight=1) scale = ttk.Scale( scale_frame, @@ -79,7 +79,7 @@ class PreferencesDialog(Dialog): orient=tk.HORIZONTAL, variable=self.gui_scale, ) - scale.grid(row=0, column=0, sticky="ew") + scale.grid(row=0, column=0, sticky=tk.EW) entry = validation.AppScaleEntry( scale_frame, textvariable=self.gui_scale, width=4 ) @@ -90,15 +90,15 @@ class PreferencesDialog(Dialog): def draw_buttons(self) -> None: frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.click_save) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def theme_change(self, event: tk.Event) -> None: theme = self.theme.get() diff --git a/daemon/core/gui/dialogs/runtool.py b/daemon/core/gui/dialogs/runtool.py index e36c4c9a..45e21182 100644 --- a/daemon/core/gui/dialogs/runtool.py +++ b/daemon/core/gui/dialogs/runtool.py @@ -38,56 +38,56 @@ class RunToolDialog(Dialog): def draw_command_frame(self) -> None: # the main frame frame = ttk.Frame(self.top) - frame.grid(row=0, column=0, sticky="nsew", padx=PADX) + frame.grid(row=0, column=0, sticky=tk.NSEW, padx=PADX) frame.columnconfigure(0, weight=1) frame.rowconfigure(1, weight=1) labeled_frame = ttk.LabelFrame(frame, text="Command", padding=FRAME_PAD) - labeled_frame.grid(sticky="ew", pady=PADY) + labeled_frame.grid(sticky=tk.EW, pady=PADY) labeled_frame.rowconfigure(0, weight=1) labeled_frame.columnconfigure(0, weight=1) entry = ttk.Entry(labeled_frame, textvariable=self.cmd) - entry.grid(sticky="ew") + entry.grid(sticky=tk.EW) # results frame labeled_frame = ttk.LabelFrame(frame, text="Output", padding=FRAME_PAD) - labeled_frame.grid(sticky="nsew", pady=PADY) + labeled_frame.grid(sticky=tk.NSEW, pady=PADY) labeled_frame.columnconfigure(0, weight=1) labeled_frame.rowconfigure(0, weight=1) self.result = CodeText(labeled_frame) self.result.text.config(state=tk.DISABLED, height=15) - self.result.grid(sticky="nsew", pady=PADY) + self.result.grid(sticky=tk.NSEW, pady=PADY) button_frame = ttk.Frame(labeled_frame) - button_frame.grid(sticky="nsew") + button_frame.grid(sticky=tk.NSEW) button_frame.columnconfigure(0, weight=1) button_frame.columnconfigure(1, weight=1) button = ttk.Button(button_frame, text="Run", command=self.click_run) - button.grid(sticky="ew", padx=PADX) + button.grid(sticky=tk.EW, padx=PADX) button = ttk.Button(button_frame, text="Close", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def draw_nodes_frame(self) -> None: labeled_frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD) - labeled_frame.grid(row=0, column=1, sticky="nsew") + labeled_frame.grid(row=0, column=1, sticky=tk.NSEW) labeled_frame.columnconfigure(0, weight=1) labeled_frame.rowconfigure(0, weight=1) self.node_list = ListboxScroll(labeled_frame) self.node_list.listbox.config(selectmode=tk.MULTIPLE) - self.node_list.grid(sticky="nsew", pady=PADY) + self.node_list.grid(sticky=tk.NSEW, pady=PADY) for n in sorted(self.executable_nodes.keys()): self.node_list.listbox.insert(tk.END, n) button_frame = ttk.Frame(labeled_frame, padding=FRAME_PAD) - button_frame.grid(sticky="nsew") + button_frame.grid(sticky=tk.NSEW) button_frame.columnconfigure(0, weight=1) button_frame.columnconfigure(1, weight=1) button = ttk.Button(button_frame, text="All", command=self.click_all) - button.grid(sticky="nsew", padx=PADX) + button.grid(sticky=tk.NSEW, padx=PADX) button = ttk.Button(button_frame, text="None", command=self.click_none) - button.grid(row=0, column=1, sticky="nsew") + button.grid(row=0, column=1, sticky=tk.NSEW) def click_all(self) -> None: self.node_list.listbox.selection_set(0, self.node_list.listbox.size() - 1) diff --git a/daemon/core/gui/dialogs/servers.py b/daemon/core/gui/dialogs/servers.py index 45121a20..38efad22 100644 --- a/daemon/core/gui/dialogs/servers.py +++ b/daemon/core/gui/dialogs/servers.py @@ -37,12 +37,12 @@ class ServersDialog(Dialog): def draw_servers(self) -> None: listbox_scroll = ListboxScroll(self.top) - listbox_scroll.grid(pady=PADY, sticky="nsew") + listbox_scroll.grid(pady=PADY, sticky=tk.NSEW) listbox_scroll.columnconfigure(0, weight=1) listbox_scroll.rowconfigure(0, weight=1) self.servers = listbox_scroll.listbox - self.servers.grid(row=0, column=0, sticky="nsew") + self.servers.grid(row=0, column=0, sticky=tk.NSEW) self.servers.bind("<>", self.handle_server_change) for server in self.app.core.servers: @@ -50,52 +50,52 @@ class ServersDialog(Dialog): def draw_server_configuration(self) -> None: frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=FRAME_PAD) - frame.grid(pady=PADY, sticky="ew") + frame.grid(pady=PADY, sticky=tk.EW) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Name") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky=tk.W, padx=PADX) entry = ttk.Entry(frame, textvariable=self.name) - entry.grid(row=0, column=1, sticky="ew") + entry.grid(row=0, column=1, sticky=tk.EW) label = ttk.Label(frame, text="Address") - label.grid(row=0, column=2, sticky="w", padx=PADX) + label.grid(row=0, column=2, sticky=tk.W, padx=PADX) entry = ttk.Entry(frame, textvariable=self.address) - entry.grid(row=0, column=3, sticky="ew") + entry.grid(row=0, column=3, sticky=tk.EW) def draw_servers_buttons(self) -> None: frame = ttk.Frame(self.top) - frame.grid(pady=PADY, sticky="ew") + frame.grid(pady=PADY, sticky=tk.EW) for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Create", command=self.click_create) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) self.save_button = ttk.Button( frame, text="Save", state=tk.DISABLED, command=self.click_save ) - self.save_button.grid(row=0, column=1, sticky="ew", padx=PADX) + self.save_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX) self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete ) - self.delete_button.grid(row=0, column=2, sticky="ew") + self.delete_button.grid(row=0, column=2, sticky=tk.EW) def draw_apply_buttons(self) -> None: frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button( frame, text="Save Configuration", command=self.click_save_configuration ) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def click_save_configuration(self): self.app.guiconfig.servers.clear() diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index 13be0bcd..a22b1afd 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -119,16 +119,16 @@ class ServiceConfigDialog(Dialog): # draw metadata frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Meta-data") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky=tk.W, padx=PADX) self.metadata_entry = ttk.Entry(frame, textvariable=self.metadata) - self.metadata_entry.grid(row=0, column=1, sticky="ew") + self.metadata_entry.grid(row=0, column=1, sticky=tk.EW) # draw notebook self.notebook = ttk.Notebook(self.top) - self.notebook.grid(sticky="nsew", pady=PADY) + self.notebook.grid(sticky=tk.NSEW, pady=PADY) self.draw_tab_files() self.draw_tab_directories() self.draw_tab_startstop() @@ -138,7 +138,7 @@ class ServiceConfigDialog(Dialog): def draw_tab_files(self) -> None: tab = ttk.Frame(self.notebook, padding=FRAME_PAD) - tab.grid(sticky="nsew") + tab.grid(sticky=tk.NSEW) tab.columnconfigure(0, weight=1) self.notebook.add(tab, text="Files") @@ -148,15 +148,15 @@ class ServiceConfigDialog(Dialog): label.grid() frame = ttk.Frame(tab) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="File Name") - label.grid(row=0, column=0, padx=PADX, sticky="w") + label.grid(row=0, column=0, padx=PADX, sticky=tk.W) self.filename_combobox = ttk.Combobox(frame, values=self.filenames) self.filename_combobox.bind( "<>", self.display_service_file_data ) - self.filename_combobox.grid(row=0, column=1, sticky="ew", padx=PADX) + self.filename_combobox.grid(row=0, column=1, sticky=tk.EW, padx=PADX) button = ttk.Button( frame, image=self.documentnew_img, command=self.add_filename ) @@ -167,7 +167,7 @@ class ServiceConfigDialog(Dialog): button.grid(row=0, column=3) frame = ttk.Frame(tab) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) frame.columnconfigure(1, weight=1) button = ttk.Radiobutton( frame, @@ -176,16 +176,16 @@ class ServiceConfigDialog(Dialog): value=1, state=tk.DISABLED, ) - button.grid(row=0, column=0, sticky="w", padx=PADX) + button.grid(row=0, column=0, sticky=tk.W, padx=PADX) entry = ttk.Entry(frame, state=tk.DISABLED) - entry.grid(row=0, column=1, sticky="ew", padx=PADX) + entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX) image = Images.get(ImageEnum.FILEOPEN, 16) button = ttk.Button(frame, image=image) button.image = image button.grid(row=0, column=2) frame = ttk.Frame(tab) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) frame.columnconfigure(0, weight=1) button = ttk.Radiobutton( frame, @@ -193,7 +193,7 @@ class ServiceConfigDialog(Dialog): text="Use text below for file contents", value=2, ) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky=tk.EW) image = Images.get(ImageEnum.FILEOPEN, 16) button = ttk.Button(frame, image=image) button.image = image @@ -204,7 +204,7 @@ class ServiceConfigDialog(Dialog): button.grid(row=0, column=2) self.service_file_data = CodeText(tab) - self.service_file_data.grid(sticky="nsew") + self.service_file_data.grid(sticky=tk.NSEW) tab.rowconfigure(self.service_file_data.grid_info()["row"], weight=1) if len(self.filenames) > 0: self.filename_combobox.current(0) @@ -218,7 +218,7 @@ class ServiceConfigDialog(Dialog): def draw_tab_directories(self) -> None: tab = ttk.Frame(self.notebook, padding=FRAME_PAD) - tab.grid(sticky="nsew") + tab.grid(sticky=tk.NSEW) tab.columnconfigure(0, weight=1) tab.rowconfigure(2, weight=1) self.notebook.add(tab, text="Directories") @@ -227,33 +227,33 @@ class ServiceConfigDialog(Dialog): tab, text="Directories required by this service that are unique for each node.", ) - label.grid(row=0, column=0, sticky="ew") + label.grid(row=0, column=0, sticky=tk.EW) frame = ttk.Frame(tab, padding=FRAME_PAD) frame.columnconfigure(0, weight=1) - frame.grid(row=1, column=0, sticky="nsew") + frame.grid(row=1, column=0, sticky=tk.NSEW) var = tk.StringVar(value="") self.directory_entry = ttk.Entry(frame, textvariable=var) - self.directory_entry.grid(row=0, column=0, sticky="ew", padx=PADX) + self.directory_entry.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="...", command=self.find_directory_button) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) self.dir_list = ListboxScroll(tab) - self.dir_list.grid(row=2, column=0, sticky="nsew", pady=PADY) + self.dir_list.grid(row=2, column=0, sticky=tk.NSEW, pady=PADY) self.dir_list.listbox.bind("<>", self.directory_select) for d in self.temp_directories: self.dir_list.listbox.insert("end", d) frame = ttk.Frame(tab) - frame.grid(row=3, column=0, sticky="nsew") + frame.grid(row=3, column=0, sticky=tk.NSEW) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Add", command=self.add_directory) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Remove", command=self.remove_directory) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def draw_tab_startstop(self) -> None: tab = ttk.Frame(self.notebook, padding=FRAME_PAD) - tab.grid(sticky="nsew") + tab.grid(sticky=tk.NSEW) tab.columnconfigure(0, weight=1) for i in range(3): tab.rowconfigure(i, weight=1) @@ -279,25 +279,25 @@ class ServiceConfigDialog(Dialog): commands = self.validation_commands label_frame.columnconfigure(0, weight=1) label_frame.rowconfigure(1, weight=1) - label_frame.grid(row=i, column=0, sticky="nsew", pady=PADY) + label_frame.grid(row=i, column=0, sticky=tk.NSEW, pady=PADY) frame = ttk.Frame(label_frame) - frame.grid(row=0, column=0, sticky="nsew", pady=PADY) + frame.grid(row=0, column=0, sticky=tk.NSEW, pady=PADY) frame.columnconfigure(0, weight=1) entry = ttk.Entry(frame, textvariable=tk.StringVar()) entry.grid(row=0, column=0, stick="ew", padx=PADX) button = ttk.Button(frame, image=self.documentnew_img) button.bind("", self.add_command) - button.grid(row=0, column=1, sticky="ew", padx=PADX) + button.grid(row=0, column=1, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, image=self.editdelete_img) - button.grid(row=0, column=2, sticky="ew") + button.grid(row=0, column=2, sticky=tk.EW) button.bind("", self.delete_command) listbox_scroll = ListboxScroll(label_frame) listbox_scroll.listbox.bind("<>", self.update_entry) for command in commands: listbox_scroll.listbox.insert("end", command) listbox_scroll.listbox.config(height=4) - listbox_scroll.grid(row=1, column=0, sticky="nsew") + listbox_scroll.grid(row=1, column=0, sticky=tk.NSEW) if i == 0: self.startup_commands_listbox = listbox_scroll.listbox elif i == 1: @@ -307,23 +307,23 @@ class ServiceConfigDialog(Dialog): def draw_tab_configuration(self) -> None: tab = ttk.Frame(self.notebook, padding=FRAME_PAD) - tab.grid(sticky="nsew") + tab.grid(sticky=tk.NSEW) tab.columnconfigure(0, weight=1) - self.notebook.add(tab, text="Configuration", sticky="nsew") + self.notebook.add(tab, text="Configuration", sticky=tk.NSEW) frame = ttk.Frame(tab) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Validation Time") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky=tk.W, padx=PADX) self.validation_time_entry = ttk.Entry(frame) self.validation_time_entry.insert("end", self.validation_time) self.validation_time_entry.config(state=tk.DISABLED) - self.validation_time_entry.grid(row=0, column=1, sticky="ew", pady=PADY) + self.validation_time_entry.grid(row=0, column=1, sticky=tk.EW, pady=PADY) label = ttk.Label(frame, text="Validation Mode") - label.grid(row=1, column=0, sticky="w", padx=PADX) + label.grid(row=1, column=0, sticky=tk.W, padx=PADX) if self.validation_mode == ServiceValidationMode.BLOCKING: mode = "BLOCKING" elif self.validation_mode == ServiceValidationMode.NON_BLOCKING: @@ -335,48 +335,48 @@ class ServiceConfigDialog(Dialog): ) self.validation_mode_entry.insert("end", mode) self.validation_mode_entry.config(state=tk.DISABLED) - self.validation_mode_entry.grid(row=1, column=1, sticky="ew", pady=PADY) + self.validation_mode_entry.grid(row=1, column=1, sticky=tk.EW, pady=PADY) label = ttk.Label(frame, text="Validation Period") - label.grid(row=2, column=0, sticky="w", padx=PADX) + label.grid(row=2, column=0, sticky=tk.W, padx=PADX) self.validation_period_entry = ttk.Entry( frame, state=tk.DISABLED, textvariable=tk.StringVar() ) - self.validation_period_entry.grid(row=2, column=1, sticky="ew", pady=PADY) + self.validation_period_entry.grid(row=2, column=1, sticky=tk.EW, pady=PADY) label_frame = ttk.LabelFrame(tab, text="Executables", padding=FRAME_PAD) - label_frame.grid(sticky="nsew", pady=PADY) + label_frame.grid(sticky=tk.NSEW, pady=PADY) label_frame.columnconfigure(0, weight=1) label_frame.rowconfigure(0, weight=1) listbox_scroll = ListboxScroll(label_frame) - listbox_scroll.grid(sticky="nsew") + listbox_scroll.grid(sticky=tk.NSEW) tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1) for executable in self.executables: listbox_scroll.listbox.insert("end", executable) label_frame = ttk.LabelFrame(tab, text="Dependencies", padding=FRAME_PAD) - label_frame.grid(sticky="nsew", pady=PADY) + label_frame.grid(sticky=tk.NSEW, pady=PADY) label_frame.columnconfigure(0, weight=1) label_frame.rowconfigure(0, weight=1) listbox_scroll = ListboxScroll(label_frame) - listbox_scroll.grid(sticky="nsew") + listbox_scroll.grid(sticky=tk.NSEW) tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1) for dependency in self.dependencies: listbox_scroll.listbox.insert("end", dependency) def draw_buttons(self) -> None: frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) for i in range(4): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Defaults", command=self.click_defaults) - button.grid(row=0, column=1, sticky="ew", padx=PADX) + button.grid(row=0, column=1, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Copy...", command=self.click_copy) - button.grid(row=0, column=2, sticky="ew", padx=PADX) + button.grid(row=0, column=2, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=3, sticky="ew") + button.grid(row=0, column=3, sticky=tk.EW) def add_filename(self) -> None: filename = self.filename_combobox.get() diff --git a/daemon/core/gui/dialogs/sessionoptions.py b/daemon/core/gui/dialogs/sessionoptions.py index 570bfbde..e9b032e0 100644 --- a/daemon/core/gui/dialogs/sessionoptions.py +++ b/daemon/core/gui/dialogs/sessionoptions.py @@ -39,17 +39,17 @@ class SessionOptionsDialog(Dialog): self.top.rowconfigure(0, weight=1) self.config_frame = ConfigFrame(self.top, self.app, self.config, self.enabled) self.config_frame.draw_config() - self.config_frame.grid(sticky="nsew", pady=PADY) + self.config_frame.grid(sticky=tk.NSEW, pady=PADY) frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) for i in range(2): frame.columnconfigure(i, weight=1) state = tk.NORMAL if self.enabled else tk.DISABLED button = ttk.Button(frame, text="Save", command=self.save, state=state) - button.grid(row=0, column=0, padx=PADX, sticky="ew") + button.grid(row=0, column=0, padx=PADX, sticky=tk.EW) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def save(self) -> None: config = self.config_frame.parse_config() diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index 83e4001a..4c9ae0ca 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -62,7 +62,7 @@ class SessionsDialog(Dialog): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - frame.grid(sticky="nsew", pady=PADY) + frame.grid(sticky=tk.NSEW, pady=PADY) self.tree = ttk.Treeview( frame, columns=("id", "state", "nodes"), @@ -72,7 +72,7 @@ class SessionsDialog(Dialog): style = ttk.Style() heading_size = int(self.app.guiconfig.scale * 10) style.configure("Treeview.Heading", font=(None, heading_size, "bold")) - self.tree.grid(sticky="nsew") + self.tree.grid(sticky=tk.NSEW) self.tree.column("id", stretch=tk.YES, anchor="center") self.tree.heading("id", text="ID") self.tree.column("state", stretch=tk.YES, anchor="center") @@ -92,25 +92,25 @@ class SessionsDialog(Dialog): self.tree.bind("<>", self.click_select) yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) - yscrollbar.grid(row=0, column=1, sticky="ns") + yscrollbar.grid(row=0, column=1, sticky=tk.NS) self.tree.configure(yscrollcommand=yscrollbar.set) xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) - xscrollbar.grid(row=1, sticky="ew") + xscrollbar.grid(row=1, sticky=tk.EW) self.tree.configure(xscrollcommand=xscrollbar.set) def draw_buttons(self) -> None: frame = ttk.Frame(self.top) for i in range(4): frame.columnconfigure(i, weight=1) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) image = Images.get(ImageEnum.DOCUMENTNEW, 16) b = ttk.Button( frame, image=image, text="New", compound=tk.LEFT, command=self.click_new ) b.image = image - b.grid(row=0, padx=PADX, sticky="ew") + b.grid(row=0, padx=PADX, sticky=tk.EW) image = Images.get(ImageEnum.FILEOPEN, 16) self.connect_button = ttk.Button( @@ -122,7 +122,7 @@ class SessionsDialog(Dialog): state=tk.DISABLED, ) self.connect_button.image = image - self.connect_button.grid(row=0, column=1, padx=PADX, sticky="ew") + self.connect_button.grid(row=0, column=1, padx=PADX, sticky=tk.EW) image = Images.get(ImageEnum.DELETE, 16) self.delete_button = ttk.Button( @@ -134,7 +134,7 @@ class SessionsDialog(Dialog): state=tk.DISABLED, ) self.delete_button.image = image - self.delete_button.grid(row=0, column=2, padx=PADX, sticky="ew") + self.delete_button.grid(row=0, column=2, padx=PADX, sticky=tk.EW) image = Images.get(ImageEnum.CANCEL, 16) if self.is_start_app: @@ -154,7 +154,7 @@ class SessionsDialog(Dialog): command=self.destroy, ) b.image = image - b.grid(row=0, column=3, sticky="ew") + b.grid(row=0, column=3, sticky=tk.EW) def click_new(self) -> None: self.app.core.create_new_session() diff --git a/daemon/core/gui/dialogs/shapemod.py b/daemon/core/gui/dialogs/shapemod.py index 2ca06772..255092ec 100644 --- a/daemon/core/gui/dialogs/shapemod.py +++ b/daemon/core/gui/dialogs/shapemod.py @@ -57,15 +57,15 @@ class ShapeDialog(Dialog): def draw_label_options(self) -> None: label_frame = ttk.LabelFrame(self.top, text="Label", padding=FRAME_PAD) - label_frame.grid(sticky="ew") + label_frame.grid(sticky=tk.EW) label_frame.columnconfigure(0, weight=1) entry = ttk.Entry(label_frame, textvariable=self.shape_text) - entry.grid(sticky="ew", pady=PADY) + entry.grid(sticky=tk.EW, pady=PADY) # font options frame = ttk.Frame(label_frame) - frame.grid(sticky="nsew", pady=PADY) + frame.grid(sticky=tk.NSEW, pady=PADY) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) @@ -75,70 +75,70 @@ class ShapeDialog(Dialog): values=sorted(font.families()), state="readonly", ) - combobox.grid(row=0, column=0, sticky="nsew") + combobox.grid(row=0, column=0, sticky=tk.NSEW) combobox = ttk.Combobox( frame, textvariable=self.font_size, values=FONT_SIZES, state="readonly" ) - combobox.grid(row=0, column=1, padx=PADX, sticky="nsew") + combobox.grid(row=0, column=1, padx=PADX, sticky=tk.NSEW) button = ttk.Button(frame, text="Color", command=self.choose_text_color) - button.grid(row=0, column=2, sticky="nsew") + button.grid(row=0, column=2, sticky=tk.NSEW) # style options frame = ttk.Frame(label_frame) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Checkbutton(frame, variable=self.bold, text="Bold") - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky=tk.EW) button = ttk.Checkbutton(frame, variable=self.italic, text="Italic") - button.grid(row=0, column=1, padx=PADX, sticky="ew") + button.grid(row=0, column=1, padx=PADX, sticky=tk.EW) button = ttk.Checkbutton(frame, variable=self.underline, text="Underline") - button.grid(row=0, column=2, sticky="ew") + button.grid(row=0, column=2, sticky=tk.EW) def draw_shape_options(self) -> None: label_frame = ttk.LabelFrame(self.top, text="Shape", padding=FRAME_PAD) - label_frame.grid(sticky="ew", pady=PADY) + label_frame.grid(sticky=tk.EW, pady=PADY) label_frame.columnconfigure(0, weight=1) frame = ttk.Frame(label_frame) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) for i in range(1, 3): frame.columnconfigure(i, weight=1) label = ttk.Label(frame, text="Fill Color") - label.grid(row=0, column=0, padx=PADX, sticky="w") + label.grid(row=0, column=0, padx=PADX, sticky=tk.W) self.fill = ttk.Label(frame, text=self.fill_color, background=self.fill_color) - self.fill.grid(row=0, column=1, sticky="ew", padx=PADX) + self.fill.grid(row=0, column=1, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Color", command=self.choose_fill_color) - button.grid(row=0, column=2, sticky="ew") + button.grid(row=0, column=2, sticky=tk.EW) label = ttk.Label(frame, text="Border Color") - label.grid(row=1, column=0, sticky="w", padx=PADX) + label.grid(row=1, column=0, sticky=tk.W, padx=PADX) self.border = ttk.Label( frame, text=self.border_color, background=self.border_color ) - self.border.grid(row=1, column=1, sticky="ew", padx=PADX) + self.border.grid(row=1, column=1, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Color", command=self.choose_border_color) - button.grid(row=1, column=2, sticky="ew") + button.grid(row=1, column=2, sticky=tk.EW) frame = ttk.Frame(label_frame) - frame.grid(sticky="ew", pady=PADY) + frame.grid(sticky=tk.EW, pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Border Width") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky=tk.W, padx=PADX) combobox = ttk.Combobox( frame, textvariable=self.border_width, values=BORDER_WIDTH, state="readonly" ) - combobox.grid(row=0, column=1, sticky="nsew") + combobox.grid(row=0, column=1, sticky=tk.NSEW) def draw_buttons(self) -> None: frame = ttk.Frame(self.top) - frame.grid(sticky="nsew") + frame.grid(sticky=tk.NSEW) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Add shape", command=self.click_add) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.cancel) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def choose_text_color(self) -> None: color_picker = ColorPickerDialog(self, self.app, self.text_color) diff --git a/daemon/core/gui/dialogs/throughput.py b/daemon/core/gui/dialogs/throughput.py index 5b3cc9b3..0b59a6ac 100644 --- a/daemon/core/gui/dialogs/throughput.py +++ b/daemon/core/gui/dialogs/throughput.py @@ -37,25 +37,25 @@ class ThroughputDialog(Dialog): variable=self.show_throughput, text="Show Throughput Level On Every Link", ) - button.grid(sticky="ew") + button.grid(sticky=tk.EW) button = ttk.Checkbutton( self.top, variable=self.exponential_weight, text="Use Exponential Weighted Moving Average", ) - button.grid(sticky="ew") + button.grid(sticky=tk.EW) button = ttk.Checkbutton( self.top, variable=self.transmission, text="Include Transmissions" ) - button.grid(sticky="ew") + button.grid(sticky=tk.EW) button = ttk.Checkbutton( self.top, variable=self.reception, text="Include Receptions" ) - button.grid(sticky="ew") + button.grid(sticky=tk.EW) label_frame = ttk.LabelFrame(self.top, text="Link Highlight", padding=FRAME_PAD) label_frame.columnconfigure(0, weight=1) - label_frame.grid(sticky="ew") + label_frame.grid(sticky=tk.EW) scale = ttk.Scale( label_frame, @@ -65,21 +65,21 @@ class ThroughputDialog(Dialog): orient=tk.HORIZONTAL, variable=self.threshold, ) - scale.grid(sticky="ew", pady=PADY) + scale.grid(sticky=tk.EW, pady=PADY) frame = ttk.Frame(label_frame) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Threshold Kbps (0 disabled)") - label.grid(row=0, column=0, sticky="ew", padx=PADX) + label.grid(row=0, column=0, sticky=tk.EW, padx=PADX) entry = ttk.Entry(frame, textvariable=self.threshold) - entry.grid(row=0, column=1, sticky="ew", pady=PADY) + entry.grid(row=0, column=1, sticky=tk.EW, pady=PADY) label = ttk.Label(frame, text="Width") - label.grid(row=1, column=0, sticky="ew", padx=PADX) + label.grid(row=1, column=0, sticky=tk.EW, padx=PADX) entry = ttk.Entry(frame, textvariable=self.width) - entry.grid(row=1, column=1, sticky="ew", pady=PADY) + entry.grid(row=1, column=1, sticky=tk.EW, pady=PADY) label = ttk.Label(frame, text="Color") - label.grid(row=2, column=0, sticky="ew", padx=PADX) + label.grid(row=2, column=0, sticky=tk.EW, padx=PADX) self.color_button = tk.Button( frame, text=self.color, @@ -87,18 +87,18 @@ class ThroughputDialog(Dialog): bg=self.color, highlightthickness=0, ) - self.color_button.grid(row=2, column=1, sticky="ew") + self.color_button.grid(row=2, column=1, sticky=tk.EW) self.draw_spacer() frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.click_save) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def click_color(self) -> None: color_picker = ColorPickerDialog(self, self.app, self.color) diff --git a/daemon/core/gui/dialogs/wlanconfig.py b/daemon/core/gui/dialogs/wlanconfig.py index d4595556..283a96cd 100644 --- a/daemon/core/gui/dialogs/wlanconfig.py +++ b/daemon/core/gui/dialogs/wlanconfig.py @@ -1,3 +1,4 @@ +import tkinter as tk from tkinter import ttk from typing import TYPE_CHECKING, Dict, Optional @@ -54,7 +55,7 @@ class WlanConfigDialog(Dialog): self.top.rowconfigure(0, weight=1) self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame.draw_config() - self.config_frame.grid(sticky="nsew", pady=PADY) + self.config_frame.grid(sticky=tk.NSEW, pady=PADY) self.draw_apply_buttons() self.top.bind("", self.remove_ranges) @@ -63,7 +64,7 @@ class WlanConfigDialog(Dialog): create node configuration options """ frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) for i in range(2): frame.columnconfigure(i, weight=1) @@ -73,10 +74,10 @@ class WlanConfigDialog(Dialog): self.range_entry.config(validatecommand=(self.positive_int, "%P")) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, padx=PADX, sticky="ew") + button.grid(row=0, column=0, padx=PADX, sticky=tk.EW) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky=tk.EW) def click_apply(self) -> None: """ diff --git a/daemon/core/gui/frames/link.py b/daemon/core/gui/frames/link.py index 093f39eb..339c39f0 100644 --- a/daemon/core/gui/frames/link.py +++ b/daemon/core/gui/frames/link.py @@ -38,7 +38,7 @@ class EdgeInfoFrame(InfoFrameBase): dst_node = self.app.core.session.nodes[link.node2_id] frame = DetailsFrame(self) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) frame.add_detail("Source", src_node.name) iface1 = link.iface1 if iface1: @@ -90,7 +90,7 @@ class WirelessEdgeInfoFrame(InfoFrameBase): iface2 = get_iface(dst_canvas_node, net_id) frame = DetailsFrame(self) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) frame.add_detail("Source", src_node.name) if iface1: mac = iface1.mac if iface1.mac else "auto" diff --git a/daemon/core/gui/frames/node.py b/daemon/core/gui/frames/node.py index 577cc489..394ecd85 100644 --- a/daemon/core/gui/frames/node.py +++ b/daemon/core/gui/frames/node.py @@ -1,3 +1,4 @@ +import tkinter as tk from typing import TYPE_CHECKING from core.gui.frames.base import DetailsFrame, InfoFrameBase @@ -18,7 +19,7 @@ class NodeInfoFrame(InfoFrameBase): self.columnconfigure(0, weight=1) node = self.canvas_node.core_node frame = DetailsFrame(self) - frame.grid(sticky="ew") + frame.grid(sticky=tk.EW) frame.add_detail("ID", node.id) frame.add_detail("Name", node.name) if NodeUtils.is_model_node(node.type): diff --git a/daemon/core/gui/statusbar.py b/daemon/core/gui/statusbar.py index d4304b6e..518a82f9 100644 --- a/daemon/core/gui/statusbar.py +++ b/daemon/core/gui/statusbar.py @@ -34,7 +34,7 @@ class StatusBar(ttk.Frame): self.columnconfigure(3, weight=1) frame = ttk.Frame(self, borderwidth=1, relief=tk.RIDGE) - frame.grid(row=0, column=0, sticky="ew") + frame.grid(row=0, column=0, sticky=tk.EW) frame.columnconfigure(0, weight=1) self.status = ttk.Label( @@ -44,22 +44,22 @@ class StatusBar(ttk.Frame): borderwidth=1, relief=tk.RIDGE, ) - self.status.grid(row=0, column=0, sticky="ew") + self.status.grid(row=0, column=0, sticky=tk.EW) self.zoom = ttk.Label(self, anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE) - self.zoom.grid(row=0, column=1, sticky="ew") + self.zoom.grid(row=0, column=1, sticky=tk.EW) self.set_zoom(self.app.canvas.ratio) self.cpu_label = ttk.Label( self, anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE ) - self.cpu_label.grid(row=0, column=2, sticky="ew") + self.cpu_label.grid(row=0, column=2, sticky=tk.EW) self.set_cpu(0.0) self.alerts_button = ttk.Button( self, text="Alerts", command=self.click_alerts, style=self.alert_style ) - self.alerts_button.grid(row=0, column=3, sticky="ew") + self.alerts_button.grid(row=0, column=3, sticky=tk.EW) def set_cpu(self, usage: float) -> None: self.cpu_label.config(text=f"CPU {usage * 100:.2f}%") diff --git a/daemon/core/gui/task.py b/daemon/core/gui/task.py index f56fd54b..b2ab9765 100644 --- a/daemon/core/gui/task.py +++ b/daemon/core/gui/task.py @@ -1,6 +1,7 @@ import logging import threading import time +import tkinter as tk from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple if TYPE_CHECKING: @@ -26,7 +27,7 @@ class ProgressTask: self.time: Optional[float] = None def start(self) -> None: - self.app.progress.grid(sticky="ew", columnspan=2) + self.app.progress.grid(sticky=tk.EW, columnspan=2) self.app.progress.start() self.time = time.perf_counter() thread = threading.Thread(target=self.run, daemon=True) diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index b7b67338..1f5589ba 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -95,7 +95,7 @@ class ButtonBar(ttk.Frame): image = self.app.get_icon(image_enum, TOOLBAR_SIZE) button = ttk.Button(self, image=image, command=func) button.image = image - button.grid(sticky="ew") + button.grid(sticky=tk.EW) Tooltip(button, tooltip) if radio: self.radio_buttons.append(button) @@ -124,7 +124,7 @@ class MarkerFrame(ttk.Frame): image = self.app.get_icon(ImageEnum.DELETE, 16) button = ttk.Button(self, image=image, width=2, command=self.click_clear) button.image = image - button.grid(sticky="ew", pady=self.PAD) + button.grid(sticky=tk.EW, pady=self.PAD) Tooltip(button, "Delete Marker") sizes = [1, 3, 8, 10] @@ -132,14 +132,14 @@ class MarkerFrame(ttk.Frame): sizes = ttk.Combobox( self, state="readonly", textvariable=self.size, value=sizes, width=2 ) - sizes.grid(sticky="ew", pady=self.PAD) + sizes.grid(sticky=tk.EW, pady=self.PAD) Tooltip(sizes, "Marker Size") frame_size = TOOLBAR_SIZE self.color_frame = tk.Frame( self, background=self.color, height=frame_size, width=frame_size ) - self.color_frame.grid(sticky="ew") + self.color_frame.grid(sticky=tk.EW) self.color_frame.bind("", self.click_color) Tooltip(self.color_frame, "Marker Color") @@ -207,7 +207,7 @@ class Toolbar(ttk.Frame): def draw_design_frame(self) -> None: self.design_frame = ButtonBar(self, self.app) - self.design_frame.grid(row=0, column=0, sticky="nsew") + self.design_frame.grid(row=0, column=0, sticky=tk.NSEW) self.design_frame.columnconfigure(0, weight=1) self.play_button = self.design_frame.create_button( ImageEnum.START, self.click_start, "Start Session" @@ -239,7 +239,7 @@ class Toolbar(ttk.Frame): def draw_runtime_frame(self) -> None: self.runtime_frame = ButtonBar(self, self.app) - self.runtime_frame.grid(row=0, column=0, sticky="nsew") + self.runtime_frame.grid(row=0, column=0, sticky=tk.NSEW) self.runtime_frame.columnconfigure(0, weight=1) self.stop_button = self.runtime_frame.create_button( ImageEnum.STOP, self.click_stop, "Stop Session" @@ -387,7 +387,7 @@ class Toolbar(ttk.Frame): self.runtime_frame, image=image, direction=tk.RIGHT ) menu_button.image = image - menu_button.grid(sticky="ew") + menu_button.grid(sticky=tk.EW) self.observers_menu = ObserversMenu(menu_button, self.app) menu_button["menu"] = self.observers_menu diff --git a/daemon/core/gui/tooltip.py b/daemon/core/gui/tooltip.py index c2978510..84a3178f 100644 --- a/daemon/core/gui/tooltip.py +++ b/daemon/core/gui/tooltip.py @@ -46,7 +46,7 @@ class Tooltip(object): self.tw.rowconfigure(0, weight=1) self.tw.columnconfigure(0, weight=1) frame = ttk.Frame(self.tw, style=Styles.tooltip_frame, padding=3) - frame.grid(sticky="nsew") + frame.grid(sticky=tk.NSEW) label = ttk.Label(frame, text=self.text, style=Styles.tooltip) label.grid() diff --git a/daemon/core/gui/widgets.py b/daemon/core/gui/widgets.py index 0d5bff22..eff1a2a3 100644 --- a/daemon/core/gui/widgets.py +++ b/daemon/core/gui/widgets.py @@ -47,13 +47,13 @@ class FrameScroll(ttk.Frame): self.columnconfigure(0, weight=1) bg = self.app.style.lookup(".", "background") self.canvas: tk.Canvas = tk.Canvas(self, highlightthickness=0, background=bg) - self.canvas.grid(row=0, sticky="nsew", padx=2, pady=2) + self.canvas.grid(row=0, sticky=tk.NSEW, padx=2, pady=2) self.canvas.columnconfigure(0, weight=1) self.canvas.rowconfigure(0, weight=1) self.scrollbar: ttk.Scrollbar = ttk.Scrollbar( self, orient="vertical", command=self.canvas.yview ) - self.scrollbar.grid(row=0, column=1, sticky="ns") + self.scrollbar.grid(row=0, column=1, sticky=tk.NS) self.frame: ttk.Frame = _cls(self.canvas) self.frame_id: int = self.canvas.create_window( 0, 0, anchor="nw", window=self.frame @@ -108,7 +108,7 @@ class ConfigFrame(ttk.Notebook): self.add(tab, text=group_name) for index, option in enumerate(sorted(group, key=lambda x: x.name)): label = ttk.Label(tab.frame, text=option.label) - label.grid(row=index, pady=PADY, padx=PADX, sticky="w") + label.grid(row=index, pady=PADY, padx=PADX, sticky=tk.W) value = tk.StringVar() if option.type == ConfigOptionType.BOOL: select = ("On", "Off") @@ -116,7 +116,7 @@ class ConfigFrame(ttk.Notebook): combobox = ttk.Combobox( tab.frame, textvariable=value, values=select, state=state ) - combobox.grid(row=index, column=1, sticky="ew") + combobox.grid(row=index, column=1, sticky=tk.EW) if option.value == "1": value.set("On") else: @@ -128,16 +128,16 @@ class ConfigFrame(ttk.Notebook): combobox = ttk.Combobox( tab.frame, textvariable=value, values=select, state=state ) - combobox.grid(row=index, column=1, sticky="ew") + combobox.grid(row=index, column=1, sticky=tk.EW) elif option.type == ConfigOptionType.STRING: value.set(option.value) state = tk.NORMAL if self.enabled else tk.DISABLED if "file" in option.label: file_frame = ttk.Frame(tab.frame) - file_frame.grid(row=index, column=1, sticky="ew") + file_frame.grid(row=index, column=1, sticky=tk.EW) file_frame.columnconfigure(0, weight=1) entry = ttk.Entry(file_frame, textvariable=value, state=state) - entry.grid(row=0, column=0, sticky="ew", padx=PADX) + entry.grid(row=0, column=0, sticky=tk.EW, padx=PADX) func = partial(file_button_click, value, self) button = ttk.Button( file_frame, text="...", command=func, state=state @@ -145,21 +145,21 @@ class ConfigFrame(ttk.Notebook): button.grid(row=0, column=1) else: entry = ttk.Entry(tab.frame, textvariable=value, state=state) - entry.grid(row=index, column=1, sticky="ew") + entry.grid(row=index, column=1, sticky=tk.EW) elif option.type in INT_TYPES: value.set(option.value) state = tk.NORMAL if self.enabled else tk.DISABLED entry = validation.PositiveIntEntry( tab.frame, textvariable=value, state=state ) - entry.grid(row=index, column=1, sticky="ew") + entry.grid(row=index, column=1, sticky=tk.EW) elif option.type == ConfigOptionType.FLOAT: value.set(option.value) state = tk.NORMAL if self.enabled else tk.DISABLED entry = validation.PositiveFloatEntry( tab.frame, textvariable=value, state=state ) - entry.grid(row=index, column=1, sticky="ew") + entry.grid(row=index, column=1, sticky=tk.EW) else: logging.error("unhandled config option type: %s", option.type) self.values[option.name] = value @@ -196,7 +196,7 @@ class ListboxScroll(ttk.Frame): self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.scrollbar: ttk.Scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL) - self.scrollbar.grid(row=0, column=1, sticky="ns") + self.scrollbar.grid(row=0, column=1, sticky=tk.NS) self.listbox: tk.Listbox = tk.Listbox( self, selectmode=tk.BROWSE, @@ -204,7 +204,7 @@ class ListboxScroll(ttk.Frame): exportselection=False, ) themes.style_listbox(self.listbox) - self.listbox.grid(row=0, column=0, sticky="nsew") + self.listbox.grid(row=0, column=0, sticky=tk.NSEW) self.scrollbar.config(command=self.listbox.yview) @@ -224,7 +224,7 @@ class CheckboxList(FrameScroll): var = tk.BooleanVar(value=checked) func = partial(self.clicked, name, var) checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func) - checkbox.grid(sticky="w") + checkbox.grid(sticky=tk.W) class CodeFont(font.Font): @@ -250,9 +250,9 @@ class CodeText(ttk.Frame): selectforeground="black", relief=tk.FLAT, ) - self.text.grid(row=0, column=0, sticky="nsew") + self.text.grid(row=0, column=0, sticky=tk.NSEW) yscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.text.yview) - yscrollbar.grid(row=0, column=1, sticky="ns") + yscrollbar.grid(row=0, column=1, sticky=tk.NS) self.text.configure(yscrollcommand=yscrollbar.set) From b7e3d1c8775696da39ff7b30e4032c3ba74677d5 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 2 Aug 2020 10:47:01 -0700 Subject: [PATCH 0568/1131] pygui: fixed emane config dialog and emane model config dialogs to expand tabs the full height of the dialog --- daemon/core/gui/dialogs/emaneconfig.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index f7925d16..d47a3c0d 100644 --- a/daemon/core/gui/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -34,7 +34,6 @@ class GlobalEmaneDialog(Dialog): ) self.config_frame.draw_config() self.config_frame.grid(sticky=tk.NSEW, pady=PADY) - self.draw_spacer() self.draw_buttons() def draw_buttons(self) -> None: @@ -88,7 +87,6 @@ class EmaneModelDialog(Dialog): self.config_frame = ConfigFrame(self.top, self.app, self.config, self.enabled) self.config_frame.draw_config() self.config_frame.grid(sticky=tk.NSEW, pady=PADY) - self.draw_spacer() self.draw_buttons() def draw_buttons(self) -> None: From 06e43f619d5d43f62d54254e299d186225f10133 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 3 Aug 2020 10:40:48 -0700 Subject: [PATCH 0569/1131] install: update install complete message to avoid implying invoke is needed to run core --- tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index c3e6d2bb..38b219e6 100644 --- a/tasks.py +++ b/tasks.py @@ -312,7 +312,7 @@ def install(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): install_service(c, hide, prefix) with p.start("installing ospf mdr"): install_ospf_mdr(c, os_info, hide) - print("\nyou may need to open a new terminal to leverage invoke for running core") + print("\ninstall complete!") @task( From f41ce8e3a67f3bb3b24649dc62aa58096612d5a1 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 3 Aug 2020 16:04:07 -0700 Subject: [PATCH 0570/1131] daemon: add core python environment variable to be able to refer to the virtual environment executable --- daemon/core/emulator/session.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 4127b141..fe0b07bc 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -8,6 +8,7 @@ import os import pwd import shutil import subprocess +import sys import tempfile import threading import time @@ -991,6 +992,7 @@ class Session: :return: environment variables """ env = os.environ.copy() + env["CORE_PYTHON"] = sys.executable env["SESSION"] = str(self.id) env["SESSION_SHORT"] = self.short_session_id() env["SESSION_DIR"] = self.session_dir From 4bcaa32fdbbdc49f1ad29263b7e02ed052605b20 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 3 Aug 2020 16:29:35 -0700 Subject: [PATCH 0571/1131] pygui: fixed issue in task handling a returned boolean value, should be doing a none check --- daemon/core/gui/task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/gui/task.py b/daemon/core/gui/task.py index b2ab9765..02148f5a 100644 --- a/daemon/core/gui/task.py +++ b/daemon/core/gui/task.py @@ -38,7 +38,7 @@ class ProgressTask: values = self.task(*self.args) if values is None: values = () - elif values and not isinstance(values, tuple): + elif values is not None and not isinstance(values, tuple): values = (values,) if self.callback: self.app.after(0, self.callback, *values) From 082677c17bb2cdb8647d9f1e322ca9906f973df3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 3 Aug 2020 16:37:31 -0700 Subject: [PATCH 0572/1131] pygui: fixed issue saving selected background to xml when not located within the ~/.coregui/backgrounds directory --- daemon/core/gui/coreclient.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 8a881945..c3ca2385 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -22,7 +22,7 @@ from core.api.grpc import ( wlan_pb2, ) from core.gui import appconfig -from core.gui.appconfig import XMLS_PATH, CoreServer, Observer +from core.gui.appconfig import BACKGROUNDS_PATH, XMLS_PATH, CoreServer, Observer from core.gui.dialogs.emaneinstall import EmaneInstallDialog from core.gui.dialogs.error import ErrorDialog from core.gui.dialogs.mobilityplayer import MobilityPlayer @@ -546,11 +546,15 @@ class CoreClient: def set_metadata(self) -> None: # create canvas data - wallpaper = None + wallpaper_path = None if self.app.canvas.wallpaper_file: - wallpaper = Path(self.app.canvas.wallpaper_file).name + wallpaper = Path(self.app.canvas.wallpaper_file) + if BACKGROUNDS_PATH == wallpaper.parent: + wallpaper_path = wallpaper.name + else: + wallpaper_path = str(wallpaper) canvas_config = { - "wallpaper": wallpaper, + "wallpaper": wallpaper_path, "wallpaper-style": self.app.canvas.scale_option.get(), "gridlines": self.app.canvas.show_grid.get(), "fit_image": self.app.canvas.adjust_to_dim.get(), From e2b3a2dc6da8b55480fdfe08365528f4a4643491 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 4 Aug 2020 12:29:08 -0700 Subject: [PATCH 0573/1131] pygui: fixed issues with configuring services on nodes due to refactoring changes --- daemon/core/gui/dialogs/nodeconfigservice.py | 4 ++-- daemon/core/gui/dialogs/nodeservice.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeconfigservice.py index 1c67e4b3..fefdc4c5 100644 --- a/daemon/core/gui/dialogs/nodeconfigservice.py +++ b/daemon/core/gui/dialogs/nodeconfigservice.py @@ -101,7 +101,7 @@ class NodeConfigServiceDialog(Dialog): elif not var.get() and name in self.current_services: self.current_services.remove(name) self.draw_current_services() - self.node.config_services[:] = self.current_services + self.node.config_services = self.current_services.copy() def click_configure(self) -> None: current_selection = self.current.listbox.curselection() @@ -130,7 +130,7 @@ class NodeConfigServiceDialog(Dialog): self.current.listbox.itemconfig(tk.END, bg="green") def click_save(self) -> None: - self.node.config_services[:] = self.current_services + self.node.config_services = self.current_services.copy() logging.info("saved node config services: %s", self.node.config_services) self.destroy() diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index 5ec78a93..a35e1d53 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -126,7 +126,7 @@ class NodeServiceDialog(Dialog): ) def click_save(self) -> None: - self.node.services[:] = self.current_services + self.node.services = self.current_services.copy() self.destroy() def click_remove(self) -> None: From cd0351c818a7fe556548fbe2cb51fc66d9c048d5 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 4 Aug 2020 16:20:51 -0700 Subject: [PATCH 0574/1131] pygui: added view option to toggle wireless edges --- daemon/core/gui/graph/edges.py | 9 ++++++--- daemon/core/gui/graph/graph.py | 1 + daemon/core/gui/menubar.py | 5 +++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index b313957d..93749370 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -110,7 +110,9 @@ class Edge: arc_y = (perp_m * arc_x) + b return arc_x, arc_y - def draw(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None: + def draw( + self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float], state: str + ) -> None: arc_pos = self._get_arcpoint(src_pos, dst_pos) self.id = self.canvas.create_line( *src_pos, @@ -120,6 +122,7 @@ class Edge: tags=self.tag, width=self.scaled_width(), fill=self.color, + state=state, ) def redraw(self) -> None: @@ -249,7 +252,7 @@ class CanvasWirelessEdge(Edge): self.width: float = WIRELESS_WIDTH color = link.color if link.color else WIRELESS_COLOR self.color: str = color - self.draw(src_pos, dst_pos) + self.draw(src_pos, dst_pos, self.canvas.show_wireless.state()) if link.label: self.middle_label_text(link.label) self.set_binding() @@ -286,7 +289,7 @@ class CanvasEdge(Edge): self.link: Optional[Link] = None self.asymmetric_link: Optional[Link] = None self.throughput: Optional[float] = None - self.draw(src_pos, dst_pos) + self.draw(src_pos, dst_pos, tk.NORMAL) self.set_binding() self.context: tk.Menu = tk.Menu(self.canvas) self.create_context() diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index bb762bb8..b9dd5dba 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -97,6 +97,7 @@ class CanvasGraph(tk.Canvas): # drawing related self.show_node_labels: ShowVar = ShowVar(self, tags.NODE_LABEL, value=True) self.show_link_labels: ShowVar = ShowVar(self, tags.LINK_LABEL, value=True) + 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_iface_names: BooleanVar = BooleanVar(value=False) diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index fd1413b6..dfe11eca 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -167,6 +167,11 @@ class Menubar(tk.Menu): command=self.canvas.show_link_labels.click_handler, variable=self.canvas.show_link_labels, ) + menu.add_checkbutton( + label="Wireless Links", + command=self.canvas.show_wireless.click_handler, + variable=self.canvas.show_wireless, + ) menu.add_checkbutton( label="Annotations", command=self.canvas.show_annotations.click_handler, From 5976bca34b2e068c2a6ebf0691d7395f8dcac434 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 4 Aug 2020 16:32:39 -0700 Subject: [PATCH 0575/1131] pygui: added view toggle for normal links --- daemon/core/gui/graph/edges.py | 1 + daemon/core/gui/graph/graph.py | 1 + daemon/core/gui/menubar.py | 5 +++++ 3 files changed, 7 insertions(+) diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 93749370..d94d47d9 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -381,6 +381,7 @@ class CanvasEdge(Edge): def check_wireless(self) -> None: if self.is_wireless(): self.canvas.itemconfig(self.id, state=tk.HIDDEN) + self.canvas.dtag(self.id, tags.EDGE) self._check_antenna() def _check_antenna(self) -> None: diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index b9dd5dba..cbf3fbb2 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -97,6 +97,7 @@ class CanvasGraph(tk.Canvas): # drawing related self.show_node_labels: ShowVar = ShowVar(self, tags.NODE_LABEL, value=True) self.show_link_labels: ShowVar = ShowVar(self, tags.LINK_LABEL, value=True) + self.show_links: ShowVar = ShowVar(self, tags.EDGE, value=True) 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) diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index dfe11eca..ebbac677 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -167,6 +167,11 @@ class Menubar(tk.Menu): command=self.canvas.show_link_labels.click_handler, variable=self.canvas.show_link_labels, ) + menu.add_checkbutton( + label="Links", + command=self.canvas.show_links.click_handler, + variable=self.canvas.show_links, + ) menu.add_checkbutton( label="Wireless Links", command=self.canvas.show_wireless.click_handler, From 9352c0eafeb645b2103814e54cfba6886ec43b71 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 4 Aug 2020 21:11:17 -0700 Subject: [PATCH 0576/1131] install: added core-python wrapper script to core virtual environment --- tasks.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tasks.py b/tasks.py index 38b219e6..d76bc01d 100644 --- a/tasks.py +++ b/tasks.py @@ -271,6 +271,18 @@ def install_scripts(c, verbose=False, prefix=DEFAULT_PREFIX): else: c.run(f"sudo cp {script} {dest}", hide=hide) + # setup core python helper + core_python = bin_dir.joinpath("core-python") + temp = NamedTemporaryFile("w", delete=False) + temp.writelines([ + "#!/bin/bash\n", + f'exec "{python}" "$@"\n', + ]) + temp.close() + c.run(f"sudo cp {temp.name} {core_python}", hide=hide) + c.run(f"sudo chmod 755 {core_python}", hide=hide) + os.unlink(temp.name) + # install core configuration file config_dir = "/etc/core" c.run(f"sudo mkdir -p {config_dir}", hide=hide) @@ -402,6 +414,10 @@ def uninstall(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX): dest = bin_dir.joinpath(script.name) c.run(f"sudo rm -f {dest}", hide=hide) + # remove core-python symlink + core_python = bin_dir.joinpath("core-python") + c.run(f"sudo rm -f {core_python}", hide=hide) + # install service systemd_dir = Path("/lib/systemd/system/") service_name = "core-daemon.service" From 8004be6e7c82cec1e807a346eefb69ab9a47e2be Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 5 Aug 2020 09:37:23 -0700 Subject: [PATCH 0577/1131] grpc: update client edit_node doc --- daemon/core/api/grpc/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 0674a0eb..e28233fc 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -552,11 +552,12 @@ class CoreGrpcClient: source: str = None, ) -> core_pb2.EditNodeResponse: """ - Edit a node, currently only changes position. + Edit a node's icon and/or location, can only use position(x,y) or + geo(lon, lat, alt), not both. :param session_id: session id :param node_id: node id - :param position: position to set node to + :param position: x,y location for node :param icon: path to icon for gui to use for node :param geo: lon,lat,alt location for node :param source: application source From b89a19a18e203e0d74e9a212415987a4ab9d5298 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 5 Aug 2020 12:10:27 -0700 Subject: [PATCH 0578/1131] grpc: update node events to include icon, pygui: updated handling node events to update icon when there is a change --- daemon/core/api/grpc/events.py | 1 + daemon/core/gui/coreclient.py | 2 ++ daemon/core/gui/graph/node.py | 13 +++++++++++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index fb6eaff8..aff3c5e5 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -32,6 +32,7 @@ def handle_node_event(node_data: NodeData) -> core_pb2.Event: id=node.id, name=node.name, model=node.type, + icon=node.icon, position=position, geo=geo, services=services, diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index c3ca2385..902f780a 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -241,6 +241,8 @@ class CoreClient: x = node.position.x y = node.position.y canvas_node.move(x, y) + if node.icon and node.icon != canvas_node.core_node.icon: + canvas_node.update_icon(node.icon) elif event.message_type == MessageType.DELETE: canvas_node = self.canvas_nodes[node.id] self.app.canvas.clear_selection() diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 100404ef..e63a8b80 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -1,12 +1,13 @@ import functools import logging import tkinter as tk +from pathlib import Path from typing import TYPE_CHECKING, Dict, List, Set import grpc from PIL.ImageTk import PhotoImage -from core.gui import themes +from core.gui import nodeutils, themes from core.gui.dialogs.emaneconfig import EmaneConfigDialog from core.gui.dialogs.mobilityconfig import MobilityConfigDialog from core.gui.dialogs.nodeconfig import NodeConfigDialog @@ -17,7 +18,7 @@ from core.gui.frames.node import NodeInfoFrame from core.gui.graph import tags from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge from core.gui.graph.tooltip import CanvasTooltip -from core.gui.images import ImageEnum +from core.gui.images import ImageEnum, Images from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils from core.gui.wrappers import Interface, Node, NodeType @@ -347,3 +348,11 @@ class CanvasNode: dx = node_x - 16 + (i * 8 * self.app.app_scale) - x dy = node_y - int(23 * self.app.app_scale) - y self.canvas.move(antenna_id, dx, dy) + + def update_icon(self, icon_path: str) -> None: + if not Path(icon_path).exists(): + logging.error(f"node icon does not exist: {icon_path}") + return + self.core_node.icon = icon_path + self.image = Images.create(icon_path, nodeutils.ICON_SIZE) + self.canvas.itemconfig(self.id, image=self.image) From 6dd7ce731e4d90d3bb6230959bbba1a597029f6f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 7 Aug 2020 22:04:34 -0700 Subject: [PATCH 0579/1131] removed invoke run task, since core-python provides a better means to do the same thing, updated install doc page to reflect this, removed old emane install from emane docs page --- docs/emane.md | 17 ----------------- docs/install.md | 25 ++++--------------------- tasks.py | 22 ---------------------- 3 files changed, 4 insertions(+), 60 deletions(-) diff --git a/docs/emane.md b/docs/emane.md index 716d7059..bbd3b0a4 100644 --- a/docs/emane.md +++ b/docs/emane.md @@ -50,23 +50,6 @@ 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 Installation - -EMANE can be installed from deb or RPM packages or from source. See the -[EMANE GitHub](https://github.com/adjacentlink/emane) for full details. - -Here are quick instructions for installing all EMANE packages for Ubuntu 18.04: -```shell -# install dependencies -sudo apt-get install libssl-dev libxml-libxml-perl libxml-simple-perl -wget https://adjacentlink.com/downloads/emane/emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz -tar xzf emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz -# install base emane packages -sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/emane*.deb -# install python3 bindings -sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/python3*.deb -``` - ## EMANE Configuration The CORE configuration file **/etc/core/core.conf** has options specific to diff --git a/docs/install.md b/docs/install.md index 604ac509..7b5014c4 100644 --- a/docs/install.md +++ b/docs/install.md @@ -132,30 +132,14 @@ After the installation complete it will have installed the following scripts. If you create your own python scripts to run CORE directly or using the gRPC/TLV APIs you will need to make sure you are running them within context of the -installed virtual environment. +installed virtual environment. To help support this CORE provides the `core-python` +executable. This executable will allow you to enter CORE's python virtual +environment interpreter or to run a script within it. > **NOTE:** the following assumes CORE has been installed successfully -There is an invoke task to help with this case. ```shell -cd -inv -h run -Usage: inv[oke] [--core-opts] run [--options] [other tasks here ...] - -Docstring: - runs a user script in the core virtual environment - -Options: - -f STRING, --file=STRING script file to run in the core virtual environment - -s, --sudo run script as sudo -``` - -Another way would be to enable the core virtual environment shell. Which -would allow you to run scripts in a more **normal** way. -```shell -cd /daemon -poetry shell -python run /path/to/script.py +core-python