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 01/71] 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 f60da6522..9c3da76ac 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -10,10 +10,14 @@ 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 @@ def draw_preferences(self): 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 @@ def click_save(self): 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 a56f14234..da3945032 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -53,6 +53,9 @@ def __init__( 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 6de511c4f..7524e3182 100644 --- a/daemon/core/gui/statusbar.py +++ b/daemon/core/gui/statusbar.py @@ -29,7 +29,7 @@ def __init__(self, master: "Application", app: "Application", **kwargs): 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 063c2e5b4..2b40cee71 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 @@ 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 @@ def __init__(self, master: "Application", app: "Application", **kwargs): 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 @@ def __init__(self, master: "Application", app: "Application", **kwargs): # 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 @@ def draw_design_frame(self): 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 @@ def draw_runtime_frame(self): 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 @@ def draw_node_picker(self): # 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 @@ def click_edit_node(self): 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 @@ def create_node_button(self): """ 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 @@ def draw_network_picker(self): 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 @@ def create_network_button(self): 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 @@ def draw_annotation_picker(self): 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 @@ def create_annotation_button(self): 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 @@ def stop_callback(self, response: core_pb2.StopSessionResponse): 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 @@ def click_marker_button(self): 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 02/71] 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 e70b9f52d..c3ab4b6b0 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 @@ def __init__(self, proxy: bool): # 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 9c3da76ac..a10e7e892 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -5,7 +5,7 @@ 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 @@ 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) + 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 26ee53794..482c86d21 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 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 03/71] 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 | 60 ++++++++++---------------- 5 files changed, 51 insertions(+), 57 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index c3ab4b6b0..888c9c62e 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -23,6 +23,7 @@ def __init__(self, proxy: bool): 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 a10e7e892..654f9e327 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -81,7 +81,6 @@ def draw_preferences(self): 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 @@ def click_save(self): 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 de3a92048..ff33d7e5f 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 @@ def __init__( 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 482c86d21..7da0b1dd7 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 2b40cee71..8298cafbc 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -4,7 +4,6 @@ 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 @@ def __init__(self, master: "Application", app: "Application", **kwargs): 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 @@ def draw_node_picker(self): 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 @@ def draw_node_picker(self): 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 @@ def draw_node_picker(self): 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 @@ def draw_network_picker(self): 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 @@ def draw_annotation_picker(self): ] 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 @@ def click_marker_button(self): 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) - - 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 + 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) + 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 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 04/71] 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 654f9e327..415fbe9a9 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -137,3 +137,5 @@ def scale_adjust(self): # 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 da3945032..a4ddd0b70 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -12,7 +12,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, Images +from core.gui.images import ImageEnum, Images, TypeToImage from core.gui.nodeutils import EdgeUtils, NodeUtils if TYPE_CHECKING: @@ -914,3 +914,14 @@ def paste(self): 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 dbd8827bf..b8faef13d 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -106,6 +106,12 @@ def _get_label_y(self): 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 cd472764e..f299c5a5b 100644 --- a/daemon/core/gui/images.py +++ b/daemon/core/gui/images.py @@ -3,6 +3,7 @@ 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 1df249937..10075b748 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -103,21 +103,18 @@ def draw_design_frame(self): 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 @@ def draw_runtime_frame(self): 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 @@ def draw_node_picker(self): 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 @@ def draw_network_picker(self): 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 @@ def draw_annotation_picker(self): (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 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 05/71] 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 888c9c62e..3a19b9ebc 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -24,6 +24,7 @@ def __init__(self, proxy: bool): 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 415fbe9a9..45feef8c1 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -125,6 +125,7 @@ def scale_adjust(self): # 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 1259ffa9f..413d94ac7 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 @@ def __init__( 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 @@ def __init__( 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 @@ def draw_labels(self): 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 @@ def draw_labels(self): y2, text=label_two, justify=tk.CENTER, - font=self.font, + font=self.canvas.app.edge_font, tags=tags.LINK_INFO, ) @@ -146,7 +153,7 @@ def set_throughput(self, throughput: float): 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 a4ddd0b70..d65f8c1cc 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -7,7 +7,7 @@ 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 @@ def draw_session(self, session: core_pb2.Session): # 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 @@ def add_node(self, x: float, y: float) -> CanvasNode: 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 @@ def scale_graph(self): 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 c8ddb8fa8..a9cee9384 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -2,7 +2,7 @@ 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 @@ def node_icon( 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 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 06/71] 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 3a19b9ebc..31651cfa3 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -22,10 +22,6 @@ def __init__(self, proxy: bool): # 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 @@ def __init__(self, proxy: bool): 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 @@ def __init__(self, proxy: bool): 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 @@ def center(self): 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 f73a842c4..97c760659 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 45feef8c1..440fab402 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -17,7 +17,7 @@ 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 @@ def click_save(self): 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 413d94ac7..9516de14c 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -32,7 +32,7 @@ def __init__( 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 @@ def __init__( 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 d65f8c1cc..dd5025fdf 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -53,9 +53,6 @@ def __init__( 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 @@ def draw_session(self, session: core_pb2.Session): # 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 @@ def add_node(self, x: float, y: float) -> CanvasNode: 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 @@ def scale_graph(self): 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 10075b748..d07023864 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -85,10 +85,7 @@ def __init__(self, master: "Application", app: "Application", **kwargs): 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 @@ def create_node_button(self): """ 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 @@ def create_network_button(self): 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 @@ def create_annotation_button(self): """ 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 @@ def click_two_node_button(self): # 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 07/71] 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 dd5025fdf..f705565ff 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -667,9 +667,14 @@ def add_node(self, x: float, y: float) -> CanvasNode: 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 @@ def paste(self): 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 d07023864..eff37257d 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -197,8 +197,12 @@ def draw_node_picker(self): # 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 08/71] 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 | 15 +++----- 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, 53 insertions(+), 41 deletions(-) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 31651cfa3..8b18beebb 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -47,13 +47,7 @@ def __init__(self, proxy: bool): 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 @@ def setup_app(self): 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 7fa31ee30..7d8e832cf 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -794,7 +794,7 @@ def create_node( 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 dd2e9f9a2..bb4d203c1 100644 --- a/daemon/core/gui/dialogs/mobilityplayer.py +++ b/daemon/core/gui/dialogs/mobilityplayer.py @@ -100,17 +100,17 @@ def draw(self): 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 f2fe1db24..9a289aeb7 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -38,7 +38,7 @@ def __init__( 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 440fab402..dd1c1c046 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -129,16 +129,9 @@ def scale_adjust(self): 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 toolbar icons and picker icons - self.app.toolbar.scale() + # scale application window + self.app.center() + # 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 9516de14c..e5d7b5dbd 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -13,6 +13,8 @@ TEXT_DISTANCE = 0.30 EDGE_WIDTH = 3 EDGE_COLOR = "#ff0000" +WIRELESS_WIDTH = 1.5 +WIRELESS_COLOR = "#009933" class CanvasWirelessEdge: @@ -32,8 +34,8 @@ def __init__( 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 f705565ff..0869a0d06 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -927,7 +927,9 @@ def paste(self): 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 @@ def scale_graph(self): 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 b8faef13d..3ed5b1d9b 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -16,7 +16,8 @@ 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 @@ def __init__( 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 @@ def delete(self): 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 @@ def motion(self, x_offset: int, y_offset: int, update: bool = True): 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 @@ def wireless_link_selected(self): 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 a9cee9384..81aa2cba7 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -123,8 +123,8 @@ def node_image( 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 09/71] 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 e6a84e72f..123eb2c39 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -66,6 +66,18 @@ def setup_bindings(self): 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 4689d2175..2b6051a4f 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 10/71] support wireless link deletion during runtime --- daemon/core/gui/graph/node.py | 76 ++++++++++++++++++++++++++++------- daemon/core/gui/nodeutils.py | 4 ++ 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 123eb2c39..8210b4f2e 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.nodeutils import NodeUtils +from core.gui.nodeutils import EdgeUtils, NodeUtils if TYPE_CHECKING: from core.gui.app import Application @@ -66,21 +66,43 @@ def setup_bindings(self): 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 @@ def wireless_link_selected(self): 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 c8ddb8fa8..f0a3a35d4 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -90,6 +90,10 @@ def is_wireless_node(cls, node_type: NodeType) -> bool: 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 11/71] 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 d6da667e9..eb525f6d2 100644 --- a/daemon/core/gui/dialogs/wlanconfig.py +++ b/daemon/core/gui/dialogs/wlanconfig.py @@ -27,6 +27,7 @@ def __init__( 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 @@ def draw_apply_buttons(self): 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 @@ def click_apply(self): 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 1259ffa9f..c00fd2aa8 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -177,6 +177,17 @@ def is_wireless(self) -> [bool, bool]: 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 a56f14234..772340640 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -42,6 +42,10 @@ def __init__( 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 12/71] 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 eb525f6d2..c0c8c845d 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 @@ 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 @@ def __init__( 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 @@ def draw(self): 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 @@ def draw_apply_buttons(self): 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 @@ def click_apply(self): 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 c00fd2aa8..0ca1c8e66 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -14,6 +14,7 @@ TEXT_DISTANCE = 0.30 EDGE_WIDTH = 3 EDGE_COLOR = "#ff0000" +WIRELESS_COLOR = "#009933" class CanvasWirelessEdge: @@ -31,7 +32,7 @@ def __init__( 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 8210b4f2e..ecd95d589 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -97,7 +97,7 @@ def delete(self): 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 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 13/71] 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 dd1c1c046..83f50f078 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -10,9 +10,6 @@ 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 14/71] 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 811bca453..7905ed8cf 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -117,6 +117,7 @@ def reset_and_redraw(self, session: core_pb2.Session): 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 15/71] 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 960039055..7b4ccf31a 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -345,32 +345,6 @@ def wireless_link_selected(self): 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 16/71] 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 b61524892..51c90e351 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 bd7423ee9..2863326fd 100644 --- a/daemon/core/gui/task.py +++ b/daemon/core/gui/task.py @@ -2,6 +2,8 @@ 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 @@ def start(self): 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 17/71] initial geo location conversion using pyproj --- daemon/Pipfile | 1 + daemon/Pipfile.lock | 597 +++++++++++++++------------- 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, 441 insertions(+), 282 deletions(-) create mode 100644 daemon/core/location/geo.py diff --git a/daemon/Pipfile b/daemon/Pipfile index d55b248fb..3653f82f6 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 b3cacedc9..4b720b934 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" - ], - "version": "==1.13.2" + "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, @@ -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" - ], - "version": "==1.26.0" + "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: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" - ], - "version": "==4.4.2" + "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": [ @@ -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" - ], - "version": "==3.11.2" + "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": [ @@ -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" - ], - "version": "==1.26.0" + "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: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" - ], - "version": "==3.11.2" + "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": [ @@ -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 d28776a1c..2da9acfa7 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -1128,7 +1128,6 @@ def handle_config_location(self, message_type, config_data): 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 ebb745094..2d91bab37 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -37,8 +37,8 @@ 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 @@ def __init__( 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 000000000..939858bc8 --- /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 bdef71abb..c4d2ae563 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 18/71] 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 8228b536c..8d47613db 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 9a7b3a0db..0eb3f9d4f 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -698,8 +698,6 @@ def stopeventmonitor(self) -> None: 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 @@ def handlelocationeventtoxyz( 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 19/71] 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 0f6051005..e5a5a5457 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -100,16 +100,9 @@ def handle_node_update(self, node_data: NodeData) -> None: 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 20/71] 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 7b4ccf31a..3ed5b1d9b 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -17,7 +17,7 @@ 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 @@ def setup_bindings(self): 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 870ac8bfa..81aa2cba7 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -90,10 +90,6 @@ def is_wireless_node(cls, node_type: NodeType) -> bool: 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 21/71] 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 7d8e832cf..89ce7ba04 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -1064,3 +1064,6 @@ def copy_node_config(self, _from: int, _to: int): 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 7905ed8cf..d07039d4f 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -656,8 +656,11 @@ def press_delete(self, event: tk.Event): 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 @@ def create_edge(self, source: CanvasNode, dest: CanvasNode): 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 22/71] 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 2863326fd..eb6655f8a 100644 --- a/daemon/core/gui/task.py +++ b/daemon/core/gui/task.py @@ -22,8 +22,8 @@ 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: + 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 23/71] 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 89ce7ba04..7d8e832cf 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -1064,6 +1064,3 @@ def copy_node_config(self, _from: int, _to: int): 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 d07039d4f..5652fa409 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -656,11 +656,11 @@ def press_delete(self, event: tk.Event): 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 @@ def create_edge(self, source: CanvasNode, dest: CanvasNode): 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 935e0b928..f4c12014c 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -25,6 +25,7 @@ def __init__(self, master: tk.Tk, app: "Application", cnf={}, **kwargs): 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 @@ def draw_edit_menu(self): 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 @@ def save(self, event=None): 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 eff37257d..3b4828d0a 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -280,6 +280,7 @@ def click_start(self): 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 @@ def click_stop(self): """ 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 24/71] 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 b9cf33ef7..94cfce568 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 25/71] 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 7d8e832cf..10e0820df 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -521,7 +521,6 @@ def start_session(self) -> core_pb2.StartSessionResponse: 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 @@ def set_node_service( self, node_id: int, service_name: str, + files: List[str], startups: List[str], validations: List[str], shutdowns: List[str], @@ -628,14 +628,16 @@ def set_node_service( 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 b528e32a0..5a2fede34 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 @@ def draw_tab_files(self): 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 @@ def draw_buttons(self): 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 @@ def delete_command(self, event: tk.Event): 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 @@ def click_apply(self): 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 @@ def click_apply(self): 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 @@ def click_apply(self): 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 @@ 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") - 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 @@ def is_custom_service_config(self): 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 26/71] 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 6eb7d16d3..000000000 --- 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 b80a7d6d9..000000000 --- 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 27/71] 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 939858bc8..7d3aea220 100644 --- a/daemon/core/location/geo.py +++ b/daemon/core/location/geo.py @@ -10,6 +10,8 @@ from core.emulator.enumerations import RegisterTlvs SCALE_FACTOR = 100.0 +CRS_WGS84 = 4326 +CRS_PROJ = 3857 class GeoLocation: @@ -25,7 +27,10 @@ def __init__(self) -> None: """ 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 @@ def setrefgeo(self, lat: float, lon: float, alt: float) -> None: :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 @@ def reset(self) -> None: 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 @@ def getxyz(self, lat: float, lon: float, alt: float) -> Tuple[float, float, floa :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 @@ def getgeo(self, x: float, y: float, z: float) -> Tuple[float, float, float]: 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 28/71] 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 10e0820df..bd4e4aade 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -935,6 +935,7 @@ def get_service_configs_proto(self) -> List[core_pb2.ServiceConfig]: 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 5a2fede34..af5a9a37b 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -49,8 +49,8 @@ def __init__( 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 @@ def load(self) -> bool: 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 @@ def draw_tab_files(self): 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 @@ def add_filename(self): 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 @@ def add_command(self, event: tk.Event): 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 @@ def update_entry(self, event: tk.Event): 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 @@ def delete_command(self, event: tk.Event): 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 @@ def click_apply(self): 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 @@ def update_temp_service_file_data(self, event: tk.Event): 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 @@ def append_commands( 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 29/71] 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 af5a9a37b..7f0f12e12 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 @@ def __init__( 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 @@ def load(self) -> bool: 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 @@ def load(self) -> bool: 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 @@ def draw_tab_directories(self): 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 @@ def get_commands(self): 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 30/71] 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 3653f82f6..d55b248fb 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 4b720b934..fe07d856d 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 d0defc6b1..2118b15eb 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 31/71] 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 0eb3f9d4f..b7f142e7a 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -328,7 +328,6 @@ def startup(self) -> int: 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 32/71] 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 733930044..f08087139 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 2da9acfa7..11c985c3e 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -949,8 +949,8 @@ def handle_register_message(self, message): 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 b7f142e7a..af0d2492c 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -682,8 +682,9 @@ def starteventmonitor(self) -> None: ) 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 6e198d485..67955a388 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -70,8 +70,7 @@ def startupdateloop(self, wlan: "CoreNetwork") -> None: 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 33/71] 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 11c985c3e..a5dbb8821 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -952,8 +952,7 @@ def handle_register_message(self, message): 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 34/71] 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 94cfce568..43f20442c 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 bd4e4aade..e20f1506b 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -500,7 +500,6 @@ def start_session(self) -> core_pb2.StartSessionResponse: 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 @@ def set_node_service( self, node_id: int, service_name: str, + dirs: List[str], files: List[str], startups: List[str], validations: List[str], @@ -628,6 +628,7 @@ def set_node_service( self.session_id, node_id, service_name, + directories=dirs, files=files, startup=startups, validate=validations, @@ -935,6 +936,7 @@ def get_service_configs_proto(self) -> List[core_pb2.ServiceConfig]: 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 7f0f12e12..c95e8cd7f 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 @@ def __init__( 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 @@ def load(self) -> bool: 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 @@ def load(self) -> bool: 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 @@ def delete_command(cls, event: tk.Event): 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 @@ def click_apply(self): 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 @@ def has_new_files(self): 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 @@ def click_copy(self): 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 35/71] 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 c95e8cd7f..e610cf944 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -67,6 +67,7 @@ def __init__( 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 @@ def load(self): 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 @@ def draw_tab_files(self): 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 @@ def delete_command(cls, event: tk.Event): 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 @@ def click_apply(self): 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 @@ 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) - 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 @@ def click_defaults(self): 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 @@ def directory_select(self, event): 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 36/71] 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 000000000..65e0e0e10 --- /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 935e0b928..19c89f340 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -6,6 +6,7 @@ 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 @@ def draw_file_menu(self): 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 @@ def save(self, event=None): 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 37/71] 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 | 442 ++++++++++------------------ 3 files changed, 160 insertions(+), 299 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index a5dbb8821..1f3b24e91 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -526,11 +526,6 @@ def handle_message(self, message): 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 @@ def handle(self): 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 @@ def handle(self): 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 2d91bab37..d112eb9c5 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -432,6 +432,7 @@ def add_link( 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 @@ def delete_link( 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 @@ def add_node( 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 @@ def edit_node(self, node_id: int, options: NodeOptions) -> None: :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 @@ def edit_node(self, node_id: int, options: NodeOptions) -> None: 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 @@ def delete_node(self, _id: int) -> bool: if node: node.shutdown() self.check_shutdown() + self.sdt.delete_node(_id) return node is not None @@ -1413,6 +1420,7 @@ def delete_nodes(self) -> None: 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 e5a5a5457..aca349acd 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -4,22 +4,15 @@ 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 @@ 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 @@ def __init__(self, session: "Session") -> None: :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 @@ def seturl(self) -> None: :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 @@ def connect(self, flags: int = 0) -> bool: # 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 @@ def cmd(self, cmdstr: str) -> bool: """ 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 @@ def cmd(self, cmdstr: str) -> bool: 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 @@ def sendobjs(self) -> None: 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, - ) + def get_node_position(self, node: NodeBase) -> Optional[str]: + """ + Convenience to generate an SDT position string, given a node. - 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) + :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 handle_distributed(self, message: CoreMessage) -> None: + def add_node(self, node: NodeBase) -> None: """ - Broker handler for processing CORE API messages as they are - received. This is used to snoop the Node messages and update - node positions. + Handle adding a node in SDT. - :param message: message to handle + :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 edit_node(self, node: NodeBase) -> None: + """ + Handle updating a node in SDT. + + :param node: node to update + :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}") - def handlenodemsg(self, msg: CoreNodeMessage) -> None: + def delete_node(self, node_id: int) -> 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 deleting a node in SDT. - :param msg: node message to handle + :param node_id: node id to delete :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 delete node: %s", node_id) + if not self.connect(): return - # node.(_id, type, icon, name) are used. - nodenum = msg.get_tlv(NodeTlvs.NUMBER.value) - if not nodenum: + 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 - 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) - - nodetype = msg.get_tlv(NodeTlvs.TYPE.value) - model = msg.get_tlv(NodeTlvs.MODEL.value) - icon = msg.get_tlv(NodeTlvs.ICON.value) - - 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 - else: - nodetype = None + # delete node + if node_data.message_type == MessageFlags.DELETE.value: + self.cmd(f"delete node,{node_data.id}") + else: + 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) - - def handlelinkmsg(self, msg: CoreLinkMessage) -> 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. - - :param msg: link message to handle + pass + return result + + def add_link(self, node_one: int, node_two: int, is_wireless: bool) -> 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 :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 38/71] 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 f08087139..15122e678 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -27,6 +27,8 @@ SetNodeConfigServiceResponse, ) from core.api.grpc.core_pb2 import ( + ExecuteScriptRequest, + ExecuteScriptResponse, GetEmaneEventChannelRequest, GetEmaneEventChannelResponse, ) @@ -1148,6 +1150,10 @@ def get_emane_event_channel(self, session_id: int) -> GetEmaneEventChannelRespon 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 f155867d4..ca9924811 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -3,6 +3,7 @@ import os import re import tempfile +import threading import time from concurrent import futures from typing import Type @@ -10,6 +11,7 @@ import grpc from grpc import ServicerContext +from core import utils from core.api.grpc import ( common_pb2, configservices_pb2, @@ -33,6 +35,7 @@ SetNodeConfigServiceResponse, ) from core.api.grpc.core_pb2 import ( + ExecuteScriptResponse, GetEmaneEventChannelRequest, GetEmaneEventChannelResponse, ) @@ -1645,3 +1648,22 @@ def GetEmaneEventChannel( 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 e515ab2e3..b89e5fb1c 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 39/71] 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 7d8e832cf..e6663992f 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -1064,3 +1064,9 @@ def copy_node_config(self, _from: int, _to: int): 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 65e0e0e10..37553277a 100644 --- a/daemon/core/gui/dialogs/executepython.py +++ b/daemon/core/gui/dialogs/executepython.py @@ -79,5 +79,6 @@ def select_file(self): 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 40/71] 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 7da0b1dd7..e9c5cba37 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 41/71] 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 83f50f078..c693e0254 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -81,7 +81,11 @@ def draw_preferences(self): ) 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 78685f9ff..af16dadde 100644 --- a/daemon/core/gui/validation.py +++ b/daemon/core/gui/validation.py @@ -11,12 +11,16 @@ 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 @@ def __init__(self, app: "Application"): 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 @@ def check_canvas_float(cls, s: str) -> bool: 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 42/71] 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 15122e678..e90f6eadb 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -475,9 +475,10 @@ def edit_node( 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 @@ def edit_node( :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 @@ def edit_node( 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 94cfce568..633bc2377 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 ca9924811..ae79cc950 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -688,21 +688,26 @@ def EditNode( 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 d112eb9c5..b0e44cb53 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -782,7 +782,8 @@ def edit_node(self, node_id: int, options: NodeOptions) -> None: 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 @@ def set_node_position(self, node: NodeBase, options: NodeOptions) -> None: # 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 @@ def broadcast_node_location(self, node: NodeBase) -> None: 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 aca349acd..1ccf40a50 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -255,20 +255,28 @@ def add_node(self, node: NodeBase) -> None: 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 b89e5fb1c..53d5c602f 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 43/71] 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 8b18beebb..10975eef6 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 @@ def __init__(self, proxy: bool): 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 c693e0254..afba6fedf 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 @@ def scale_adjust(self): # 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 44/71] 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 afba6fedf..45a3aceeb 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -11,6 +11,8 @@ 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 @@ def draw_preferences(self): ) 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 @@ def scale_adjust(self): # 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 45/71] 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 45a3aceeb..2f728416e 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -7,6 +7,7 @@ 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 @@ def draw_preferences(self): 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 @@ def scale_adjust(self): 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 46/71] 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 97c760659..13c3de303 100644 --- a/daemon/core/gui/appconfig.py +++ b/daemon/core/gui/appconfig.py @@ -16,6 +16,7 @@ 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 37553277a..9adf4f938 100644 --- a/daemon/core/gui/dialogs/executepython.py +++ b/daemon/core/gui/dialogs/executepython.py @@ -2,6 +2,7 @@ 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 @@ def add_options(self): 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 47/71] 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 f299c5a5b..1f43103c2 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 1f402510a..8eab98c23 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -10,6 +10,8 @@ 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 @@ def read_device(self, device_element: etree.Element) -> None: 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 @@ def read_device(self, device_element: etree.Element) -> None: 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 48/71] 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 1db9ca49a..f07376b38 100644 --- a/daemon/core/gui/dialogs/marker.py +++ b/daemon/core/gui/dialogs/marker.py @@ -8,6 +8,7 @@ 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 @@ def draw(self): 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 5652fa409..a78386a09 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -534,7 +534,7 @@ def click_press(self, event: tk.Event): 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 763465b5c..45f6d0ee0 100644 --- a/daemon/core/gui/graph/tags.py +++ b/daemon/core/gui/graph/tags.py @@ -10,6 +10,7 @@ WALLPAPER = "wallpaper" SELECTION = "selectednodes" THROUGHPUT = "throughput" +MARKER = "marker" ABOVE_WALLPAPER_TAGS = [ GRIDLINE, SHAPE, @@ -33,4 +34,5 @@ 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 49/71] 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 e20f1506b..04505a076 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -46,7 +46,7 @@ "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 50/71] 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 7db65dc77..fcca28960 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 @@ 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 @@ def draw_interfaces(self): 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 @@ def draw_interfaces(self): 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 @@ def click_icon(self): 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 @@ def config_apply(self): # 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 0659767d2..fb5f64ebe 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -107,8 +107,7 @@ def get_midpoint(self) -> [float, float]: 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 @@ def draw_labels(self): 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 @@ def draw_labels(self): 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 3ed5b1d9b..7ae64eacb 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -107,6 +107,8 @@ def delete_antennas(self): 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 51/71] 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 2f728416e..fdd613b58 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -57,10 +57,7 @@ def draw_preferences(self): 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 52/71] 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 04505a076..554887924 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -575,7 +575,7 @@ def launch_terminal(self, node_id: int): 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 53/71] 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 554887924..cd247a70d 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -575,7 +575,8 @@ def launch_terminal(self, node_id: int): 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 54/71] 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 f73275df8..8182a9170 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 55/71] 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 95699d4cd..609367b65 100644 --- a/daemon/core/gui/menuaction.py +++ b/daemon/core/gui/menuaction.py @@ -192,3 +192,7 @@ def add_recent_file_to_gui_config(self, file_path): 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 f4c12014c..385e0ca1b 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -49,7 +49,7 @@ def draw_file_menu(self): 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 56/71] 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 97c760659..28cd6c0a2 100644 --- a/daemon/core/gui/appconfig.py +++ b/daemon/core/gui/appconfig.py @@ -25,17 +25,16 @@ 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 04505a076..377144ed6 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -5,6 +5,7 @@ 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 @@ "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 @@ def set_metadata(self): 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 2f728416e..73a4fb3b1 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -56,11 +56,9 @@ def draw_preferences(self): 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 57/71] 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 609367b65..3d7ee1547 100644 --- a/daemon/core/gui/menuaction.py +++ b/daemon/core/gui/menuaction.py @@ -90,6 +90,7 @@ def file_save_as_xml(self, event: tk.Event = None): 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 @@ def add_recent_file_to_gui_config(self, file_path): 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 385e0ca1b..70198fbad 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -57,6 +57,7 @@ def draw_file_menu(self): ) 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 58/71] 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 fcca28960..0f6e56d6d 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -21,10 +21,9 @@ 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 @@ def draw_interfaces(self): 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 @@ def config_apply(self): # 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 fb5f64ebe..37b1a96e6 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -110,18 +110,20 @@ def get_midpoint(self) -> [float, float]: 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 59/71] 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 e90f6eadb..cad8b2f27 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -262,6 +262,16 @@ def get_sessions(self) -> core_pb2.GetSessionsResponse: """ 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 ae79cc950..c71800f4e 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -453,6 +453,19 @@ def SetSessionMetadata( 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 c4d1a128b..f6dfd71a9 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -58,7 +58,7 @@ def __init__(self, app: "Application", proxy: bool): """ 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 @@ def __init__(self, app: "Application", proxy: bool): 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 @@ def reset(self): 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 @@ def enable_throughputs(self): ) 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 53d5c602f..0a751ddfb 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 60/71] 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 10975eef6..195f0e917 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -44,7 +44,7 @@ def __init__(self, proxy: bool): 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 f6dfd71a9..7c7f8378a 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -446,7 +446,7 @@ def delete_session(self, session_id: int = None, parent_frame=None): 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 @@ def set_up(self): 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 61/71] 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 629079bc9..7d57a2b9d 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 62/71] 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 7d57a2b9d..8a6a6cc87 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 63/71] 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 8a6a6cc87..5ba13265a 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 64/71] 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 5ba13265a..c930a4d32 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 65/71] 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 c930a4d32..6bd16a714 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 66/71] updated devguide to note using install.sh and clear up needing to maintain duplicate content --- docs/devguide.md | 83 ++++++++++++------------------------------------ 1 file changed, 21 insertions(+), 62 deletions(-) diff --git a/docs/devguide.md b/docs/devguide.md index a6fe970c2..6e42c7745 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 -```shell -./bootstrap.sh -./configure -make -j8 -``` +This command will automatically install system dependencies, clone and build OSPF-MDR, +build CORE, setup the CORE pipenv environment, and install pre-commit hooks. -### Install netns and GUI - -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+. +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 -# change to daemon directory -cd $REPO/daemon +# default dev install using python3.6 +./install.sh -d -# install pipenv -sudo python3 -m pip install pipenv +# providing a newer python version for ubuntu +./install.sh -d -v 3.7 -# setup a virtual environment and install all required development dependencies -python3 -m pipenv install --dev +# providing a newer python version for centos +./install.sh -d -v 37 ``` -### 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. +### pre-commit -```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 67/71] updates to install doc --- docs/install.md | 159 ++++++++++++++++++++++++------------------------ 1 file changed, 79 insertions(+), 80 deletions(-) diff --git a/docs/install.md b/docs/install.md index 115466989..23d9d851e 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 +### CentOS + +```shell +sudo yum install python36 +sudo yum install python3-pip +``` -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). +### Dependencies + +Install the current python dependencies. ```shell -sudo pip3 install -r requirements.txt +sudo python3 -m pip install -r requirements.txt ``` -# Pre-Req Installing OSPF MDR +## 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 -```shell -# systemd -sudo systemctl daemon-reload -sudo systemctl start core-daemon +Steps for building from cloned source code. Python 3.6 is the minimum required version +a newer version can be used below if available. -# sysv -sudo service core-daemon start -``` +### Distro Requirements + +System packages required to build from source. -Run the CORE GUI as a normal user: +#### Ubuntu ```shell -core-gui +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 ``` -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. - -# 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. +#### CentOS ```shell -sudo pip3 install grpcio-tools +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 ``` -## Distro Requirements +### Clone Repository -### Ubuntu 18.04 Requirements +Clone the CORE repository for building from source. ```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 +git clone https://github.com/coreemu/core.git ``` -### Ubuntu 16.04 Requirements - -```shell -sudo apt-get install automake ebtables python3-dev libev-dev python3-setuptools libtk-img ethtool -``` +### Install grpcio-tools -### CentOS 7 with Gnome Desktop Requirements +Python module grpcio-tools is currently needed to generate gRPC protobuf code. ```shell -sudo yum -y install automake gcc python36 python36-devel libev-devel tk ethtool iptables-ebtables iproute python3-pip python3-tkinter +sudo python3 -m pip install grpcio-tools ``` -## 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 68/71] 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 0f6e56d6d..d77c69c40 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -240,12 +240,17 @@ def draw_interfaces(self): 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 @@ def config_apply(self): 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 e41290edc..8bdbc5397 100644 --- a/daemon/core/gui/dialogs/nodeconfigservice.py +++ b/daemon/core/gui/dialogs/nodeconfigservice.py @@ -128,7 +128,9 @@ def click_configure(self): 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 9a289aeb7..691bd331a 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -148,11 +148,12 @@ def click_configure(self): 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 51c90e351..1f9353d82 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 57e95a4f3..7a8b42b8a 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 69/71] 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 7a8b42b8a..0eb9fef13 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -13,6 +13,7 @@ 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 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 70/71] 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 905fbde01..b9369de6c 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 71/71] 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 67955a388..3e8e7d8c4 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -1067,6 +1067,7 @@ def startup(self) -> None: """ super().startup() self.net_client.disable_mac_learning(self.brname) + ebq.ebchange(self) def attach(self, netif: CoreInterface) -> None: """