From 70ec532703c45e04d7c3f00b3ddcad52b58d5edd Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Sun, 15 Sep 2019 15:20:00 -0700 Subject: [PATCH 001/462] initial commit down a path for a possible core python tkinter gui --- coretk/Pipfile | 15 ++ coretk/Pipfile.lock | 134 ++++++++++++++++++ coretk/coretk/__init__.py | 0 coretk/coretk/graph.py | 286 ++++++++++++++++++++++++++++++++++++++ coretk/coretk/switch.png | Bin 0 -> 5286 bytes coretk/setup.cfg | 15 ++ 6 files changed, 450 insertions(+) create mode 100644 coretk/Pipfile create mode 100644 coretk/Pipfile.lock create mode 100644 coretk/coretk/__init__.py create mode 100644 coretk/coretk/graph.py create mode 100644 coretk/coretk/switch.png create mode 100644 coretk/setup.cfg diff --git a/coretk/Pipfile b/coretk/Pipfile new file mode 100644 index 00000000..ff97aa2f --- /dev/null +++ b/coretk/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +flake8 = "*" +isort = "*" +black = "==19.3b0" + +[packages] +pillow = "*" + +[requires] +python_version = "3.7" diff --git a/coretk/Pipfile.lock b/coretk/Pipfile.lock new file mode 100644 index 00000000..3900d09b --- /dev/null +++ b/coretk/Pipfile.lock @@ -0,0 +1,134 @@ +{ + "_meta": { + "hash": { + "sha256": "c719ca1ace4a33dfe01fe579162d4557e8711819c4e7b4f8e799dd25b9cde596" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "pillow": { + "hashes": [ + "sha256:0804f77cb1e9b6dbd37601cee11283bba39a8d44b9ddb053400c58e0c0d7d9de", + "sha256:0ab7c5b5d04691bcbd570658667dd1e21ca311c62dcfd315ad2255b1cd37f64f", + "sha256:0b3e6cf3ea1f8cecd625f1420b931c83ce74f00c29a0ff1ce4385f99900ac7c4", + "sha256:365c06a45712cd723ec16fa4ceb32ce46ad201eb7bbf6d3c16b063c72b61a3ed", + "sha256:38301fbc0af865baa4752ddae1bb3cbb24b3d8f221bf2850aad96b243306fa03", + "sha256:3aef1af1a91798536bbab35d70d35750bd2884f0832c88aeb2499aa2d1ed4992", + "sha256:3fe0ab49537d9330c9bba7f16a5f8b02da615b5c809cdf7124f356a0f182eccd", + "sha256:45a619d5c1915957449264c81c008934452e3fd3604e36809212300b2a4dab68", + "sha256:49f90f147883a0c3778fd29d3eb169d56416f25758d0f66775db9184debc8010", + "sha256:571b5a758baf1cb6a04233fb23d6cf1ca60b31f9f641b1700bfaab1194020555", + "sha256:5ac381e8b1259925287ccc5a87d9cf6322a2dc88ae28a97fe3e196385288413f", + "sha256:6153db744a743c0c8c91b8e3b9d40e0b13a5d31dbf8a12748c6d9bfd3ddc01ad", + "sha256:6fd63afd14a16f5d6b408f623cc2142917a1f92855f0df997e09a49f0341be8a", + "sha256:70acbcaba2a638923c2d337e0edea210505708d7859b87c2bd81e8f9902ae826", + "sha256:70b1594d56ed32d56ed21a7fbb2a5c6fd7446cdb7b21e749c9791eac3a64d9e4", + "sha256:76638865c83b1bb33bcac2a61ce4d13c17dba2204969dedb9ab60ef62bede686", + "sha256:7b2ec162c87fc496aa568258ac88631a2ce0acfe681a9af40842fc55deaedc99", + "sha256:7cee2cef07c8d76894ebefc54e4bb707dfc7f258ad155bd61d87f6cd487a70ff", + "sha256:7d16d4498f8b374fc625c4037742fbdd7f9ac383fd50b06f4df00c81ef60e829", + "sha256:b50bc1780681b127e28f0075dfb81d6135c3a293e0c1d0211133c75e2179b6c0", + "sha256:bd0582f831ad5bcad6ca001deba4568573a4675437db17c4031939156ff339fa", + "sha256:cfd40d8a4b59f7567620410f966bb1f32dc555b2b19f82a91b147fac296f645c", + "sha256:e3ae410089de680e8f84c68b755b42bc42c0ceb8c03dbea88a5099747091d38e", + "sha256:e9046e559c299b395b39ac7dbf16005308821c2f24a63cae2ab173bd6aa11616", + "sha256:ef6be704ae2bc8ad0ebc5cb850ee9139493b0fc4e81abcc240fb392a63ebc808", + "sha256:f8dc19d92896558f9c4317ee365729ead9d7bbcf2052a9a19a3ef17abbb8ac5b" + ], + "index": "pypi", + "version": "==6.1.0" + } + }, + "develop": { + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "version": "==1.4.3" + }, + "attrs": { + "hashes": [ + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + ], + "version": "==19.1.0" + }, + "black": { + "hashes": [ + "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", + "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c" + ], + "index": "pypi", + "version": "==19.3b0" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "version": "==0.3" + }, + "flake8": { + "hashes": [ + "sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", + "sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696" + ], + "index": "pypi", + "version": "==3.7.8" + }, + "isort": { + "hashes": [ + "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", + "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" + ], + "index": "pypi", + "version": "==4.3.21" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pycodestyle": { + "hashes": [ + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + ], + "version": "==2.5.0" + }, + "pyflakes": { + "hashes": [ + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" + ], + "version": "==2.1.1" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + } + } +} diff --git a/coretk/coretk/__init__.py b/coretk/coretk/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py new file mode 100644 index 00000000..8de6ef81 --- /dev/null +++ b/coretk/coretk/graph.py @@ -0,0 +1,286 @@ +import enum +import tkinter as tk + +from PIL import Image, ImageTk + + +class GraphMode(enum.Enum): + SELECT = 0 + EDGE = 1 + NODE = 2 + + +class CanvasGraph(tk.Canvas): + images = {} + + @classmethod + def load(cls, name, file_path): + image = Image.open(file_path) + tk_image = ImageTk.PhotoImage(image) + cls.images[name] = tk_image + + def __init__(self, master=None, cnf=None, **kwargs): + if cnf is None: + cnf = {} + kwargs["highlightthickness"] = 0 + super().__init__(master, cnf, **kwargs) + self.mode = GraphMode.SELECT + self.selected = None + self.node_context = None + self.nodes = {} + self.edges = {} + self.drawing_edge = None + self.setup_menus() + self.setup_bindings() + + def setup_menus(self): + self.node_context = tk.Menu(self.master) + self.node_context.add_command(label="One") + self.node_context.add_command(label="Two") + self.node_context.add_command(label="Three") + + def setup_bindings(self): + self.bind("", self.click_press) + self.bind("", self.click_release) + self.bind("", self.click_motion) + self.bind("", self.context) + self.bind("e", self.set_mode) + self.bind("s", self.set_mode) + self.bind("n", self.set_mode) + + def canvas_xy(self, event): + x = self.canvasx(event.x) + y = self.canvasy(event.y) + return x, y + + def get_selected(self, event): + overlapping = self.find_overlapping(event.x, event.y, event.x, event.y) + nodes = set(self.find_withtag("node")) + selected = None + for _id in overlapping: + if self.drawing_edge and self.drawing_edge.id == _id: + continue + + if _id in nodes: + selected = _id + break + + if selected is None: + selected = _id + + return selected + + def click_release(self, event): + self.focus_set() + self.selected = self.get_selected(event) + print(f"click release selected: {self.selected}") + if self.mode == GraphMode.EDGE: + self.handle_edge_release(event) + elif self.mode == GraphMode.NODE: + x, y = self.canvas_xy(event) + self.add_node(x, y, "Node", "switch") + + def handle_edge_release(self, event): + edge = self.drawing_edge + self.drawing_edge = None + + # not drawing edge return + if edge is None: + return + + # edge dst must be a node + print(f"current selected: {self.selected}") + print(f"current nodes: {self.find_withtag('node')}") + is_node = self.selected in self.find_withtag("node") + if not is_node: + edge.delete() + return + + # edge dst is same as src, delete edge + if edge.src == self.selected: + edge.delete() + + # set dst node and snap edge to center + x, y = self.coords(self.selected) + edge.complete(self.selected, x, y) + print(f"drawing edge token: {edge.token}") + if edge.token in self.edges: + edge.delete() + else: + self.edges[edge.token] = edge + node_src = self.nodes[edge.src] + node_src.edges.add(edge) + node_dst = self.nodes[edge.dst] + node_dst.edges.add(edge) + + print(f"edges: {self.find_withtag('edge')}") + + def click_press(self, event): + print(f"click press: {event}") + selected = self.get_selected(event) + is_node = selected in self.find_withtag("node") + if self.mode == GraphMode.EDGE and is_node: + x, y = self.coords(selected) + self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) + + def click_motion(self, event): + if self.mode == GraphMode.EDGE and self.drawing_edge is not None: + x2, y2 = self.canvas_xy(event) + x1, y1, _, _ = self.coords(self.drawing_edge.id) + self.coords(self.drawing_edge.id, x1, y1, x2, y2) + + def context(self, event): + selected = self.get_selected(event) + nodes = self.find_withtag("node") + if selected in nodes: + print(f"node context: {selected}") + self.node_context.post(event.x_root, event.y_root) + + def set_mode(self, event): + print(f"mode event: {event}") + if event.char == "e": + self.mode = GraphMode.EDGE + elif event.char == "s": + self.mode = GraphMode.SELECT + elif event.char == "n": + self.mode = GraphMode.NODE + print(f"graph mode: {self.mode}") + + def add_node(self, x, y, name, image_name): + image = self.images[image_name] + node = CanvasNode(x, y, name, image, self) + self.nodes[node.id] = node + return node + + +class CanvasEdge: + width = 3 + + def __init__(self, x1, y1, x2, y2, src, canvas): + self.src = src + self.dst = None + self.canvas = canvas + self.id = self.canvas.create_line(x1, y1, x2, y2, tags="edge", width=self.width) + self.token = None + self.canvas.tag_lower(self.id) + + def complete(self, dst, x, y): + self.dst = dst + self.token = tuple(sorted((self.src, self.dst))) + x1, y1, _, _ = self.canvas.coords(self.id) + self.canvas.coords(self.id, x1, y1, x, y) + + def delete(self): + self.canvas.delete(self.id) + + +class CanvasNode: + def __init__(self, x, y, name, image, canvas): + self.name = name + self.image = image + self.canvas = canvas + self.id = self.canvas.create_image( + x, y, anchor=tk.CENTER, image=self.image, tags="node" + ) + self.text_id = self.canvas.create_text(x, y + 20, text=self.name) + self.canvas.tag_bind(self.id, "", self.click_press) + self.canvas.tag_bind(self.id, "", self.click_release) + self.canvas.tag_bind(self.id, "", self.motion) + self.canvas.tag_bind(self.id, "", self.context) + self.edges = set() + self.moving = None + + def click_press(self, event): + print(f"click press {self.name}: {event}") + self.moving = self.canvas.canvas_xy(event) + + def click_release(self, event): + print(f"click release {self.name}: {event}") + self.moving = None + + def motion(self, event): + if self.canvas.mode == GraphMode.EDGE: + return + x, y = self.canvas.canvas_xy(event) + moving_x, moving_y = self.moving + offset_x, offset_y = x - moving_x, y - moving_y + self.moving = x, y + + old_x, old_y = self.canvas.coords(self.id) + self.canvas.move(self.id, offset_x, offset_y) + self.canvas.move(self.text_id, offset_x, offset_y) + new_x, new_y = self.canvas.coords(self.id) + for edge in self.edges: + x1, y1, x2, y2 = self.canvas.coords(edge.id) + if x1 == old_x and y1 == old_y: + self.canvas.coords(edge.id, new_x, new_y, x2, y2) + else: + self.canvas.coords(edge.id, x1, y1, new_x, new_y) + + def context(self, event): + print(f"context click {self.name}: {event}") + + +class Application(tk.Frame): + def __init__(self, master=None): + super().__init__(master) + self.pack(fill=tk.BOTH, expand=1) + self.images = [] + self.menubar = None + self.create_menu() + self.create_widgets() + + def create_menu(self): + self.master.option_add("*tearOff", tk.FALSE) + self.menubar = tk.Menu(self.master) + file_menu = tk.Menu(self.menubar) + file_menu.add_command(label="Open") + file_menu.add_command(label="Exit", command=root.quit) + self.menubar.add_cascade(label="File", menu=file_menu) + help_menu = tk.Menu(self.menubar) + self.menubar.add_cascade(label="Help", menu=help_menu) + self.master.config(menu=self.menubar) + + def create_widgets(self): + edit_frame = tk.Frame(self) + edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) + b = tk.Button(edit_frame, text="Button 1") + b.pack(side=tk.TOP, pady=1) + b = tk.Button(edit_frame, text="Button 2") + b.pack(side=tk.TOP, pady=1) + b = tk.Button(edit_frame, text="Button 3") + b.pack(side=tk.TOP, pady=1) + + self.canvas = CanvasGraph( + self, background="#cccccc", scrollregion=(0, 0, 1000, 1000) + ) + self.canvas.load("switch", "switch.png") + self.canvas.add_node(50, 50, "Node 1", "switch") + self.canvas.add_node(50, 100, "Node 2", "switch") + self.canvas.pack(fill=tk.BOTH, expand=True) + scroll_x = tk.Scrollbar( + self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview + ) + scroll_x.pack(side=tk.BOTTOM, fill=tk.X) + scroll_y = tk.Scrollbar(self.canvas, command=self.canvas.yview) + scroll_y.pack(side=tk.RIGHT, fill=tk.Y) + self.canvas.configure(xscrollcommand=scroll_x.set) + self.canvas.configure(yscrollcommand=scroll_y.set) + + status_bar = tk.Frame(self) + status_bar.pack(side=tk.BOTTOM, fill=tk.X) + b = tk.Button(status_bar, text="Button 1") + b.pack(side=tk.LEFT, padx=1) + b = tk.Button(status_bar, text="Button 2") + b.pack(side=tk.LEFT, padx=1) + b = tk.Button(status_bar, text="Button 3") + b.pack(side=tk.LEFT, padx=1) + + +if __name__ == "__main__": + root = tk.Tk() + root.title("Graph Canvas") + root.geometry("800x600") + root.state("zoomed") + app = Application(master=root) + app.mainloop() diff --git a/coretk/coretk/switch.png b/coretk/coretk/switch.png new file mode 100644 index 0000000000000000000000000000000000000000..f8c852947639b68084a32c96f156ec1be99829e2 GIT binary patch literal 5286 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 M8hYwA5Ua@l0JEh!?f?J) literal 0 HcmV?d00001 diff --git a/coretk/setup.cfg b/coretk/setup.cfg new file mode 100644 index 00000000..d9228b5f --- /dev/null +++ b/coretk/setup.cfg @@ -0,0 +1,15 @@ +[aliases] +test=pytest + +[isort] +multi_line_output=3 +include_trailing_comma=True +force_grid_wrap=0 +use_parentheses=True +line_length=88 + +[flake8] +ignore=E501,W503,E203 +max-line-length=100 +max-complexity=26 +select=B,C,E,F,W,T4 From 28e1e7d796d45f3e265c1225f7889523500b043f Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Sun, 15 Sep 2019 16:00:01 -0700 Subject: [PATCH 002/462] separated graph module from app for coretk --- coretk/coretk/app.py | 68 ++++++++++++++++++++++++++++++++++++++++++ coretk/coretk/graph.py | 65 ---------------------------------------- 2 files changed, 68 insertions(+), 65 deletions(-) create mode 100644 coretk/coretk/app.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py new file mode 100644 index 00000000..cc664923 --- /dev/null +++ b/coretk/coretk/app.py @@ -0,0 +1,68 @@ +import tkinter as tk + +from coretk.graph import CanvasGraph + + +class Application(tk.Frame): + def __init__(self, master=None): + super().__init__(master) + self.pack(fill=tk.BOTH, expand=1) + self.images = [] + self.menubar = None + self.create_menu() + self.create_widgets() + + def create_menu(self): + self.master.option_add("*tearOff", tk.FALSE) + self.menubar = tk.Menu(self.master) + file_menu = tk.Menu(self.menubar) + file_menu.add_command(label="Open") + file_menu.add_command(label="Exit", command=root.quit) + self.menubar.add_cascade(label="File", menu=file_menu) + help_menu = tk.Menu(self.menubar) + self.menubar.add_cascade(label="Help", menu=help_menu) + self.master.config(menu=self.menubar) + + def create_widgets(self): + edit_frame = tk.Frame(self) + edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) + b = tk.Button(edit_frame, text="Button 1") + b.pack(side=tk.TOP, pady=1) + b = tk.Button(edit_frame, text="Button 2") + b.pack(side=tk.TOP, pady=1) + b = tk.Button(edit_frame, text="Button 3") + b.pack(side=tk.TOP, pady=1) + + self.canvas = CanvasGraph( + self, background="#cccccc", scrollregion=(0, 0, 1000, 1000) + ) + self.canvas.load("switch", "switch.png") + self.canvas.add_node(50, 50, "Node 1", "switch") + self.canvas.add_node(50, 100, "Node 2", "switch") + self.canvas.pack(fill=tk.BOTH, expand=True) + scroll_x = tk.Scrollbar( + self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview + ) + scroll_x.pack(side=tk.BOTTOM, fill=tk.X) + scroll_y = tk.Scrollbar(self.canvas, command=self.canvas.yview) + scroll_y.pack(side=tk.RIGHT, fill=tk.Y) + self.canvas.configure(xscrollcommand=scroll_x.set) + self.canvas.configure(yscrollcommand=scroll_y.set) + + status_bar = tk.Frame(self) + status_bar.pack(side=tk.BOTTOM, fill=tk.X) + b = tk.Button(status_bar, text="Button 1") + b.pack(side=tk.LEFT, padx=1) + b = tk.Button(status_bar, text="Button 2") + b.pack(side=tk.LEFT, padx=1) + b = tk.Button(status_bar, text="Button 3") + b.pack(side=tk.LEFT, padx=1) + + +if __name__ == "__main__": + root = tk.Tk() + root.title("Graph Canvas") + root.geometry("800x600") + root.state("zoomed") + app = Application(master=root) + app.mainloop() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 8de6ef81..d6e73e3c 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -219,68 +219,3 @@ class CanvasNode: def context(self, event): print(f"context click {self.name}: {event}") - - -class Application(tk.Frame): - def __init__(self, master=None): - super().__init__(master) - self.pack(fill=tk.BOTH, expand=1) - self.images = [] - self.menubar = None - self.create_menu() - self.create_widgets() - - def create_menu(self): - self.master.option_add("*tearOff", tk.FALSE) - self.menubar = tk.Menu(self.master) - file_menu = tk.Menu(self.menubar) - file_menu.add_command(label="Open") - file_menu.add_command(label="Exit", command=root.quit) - self.menubar.add_cascade(label="File", menu=file_menu) - help_menu = tk.Menu(self.menubar) - self.menubar.add_cascade(label="Help", menu=help_menu) - self.master.config(menu=self.menubar) - - def create_widgets(self): - edit_frame = tk.Frame(self) - edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) - b = tk.Button(edit_frame, text="Button 1") - b.pack(side=tk.TOP, pady=1) - b = tk.Button(edit_frame, text="Button 2") - b.pack(side=tk.TOP, pady=1) - b = tk.Button(edit_frame, text="Button 3") - b.pack(side=tk.TOP, pady=1) - - self.canvas = CanvasGraph( - self, background="#cccccc", scrollregion=(0, 0, 1000, 1000) - ) - self.canvas.load("switch", "switch.png") - self.canvas.add_node(50, 50, "Node 1", "switch") - self.canvas.add_node(50, 100, "Node 2", "switch") - self.canvas.pack(fill=tk.BOTH, expand=True) - scroll_x = tk.Scrollbar( - self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview - ) - scroll_x.pack(side=tk.BOTTOM, fill=tk.X) - scroll_y = tk.Scrollbar(self.canvas, command=self.canvas.yview) - scroll_y.pack(side=tk.RIGHT, fill=tk.Y) - self.canvas.configure(xscrollcommand=scroll_x.set) - self.canvas.configure(yscrollcommand=scroll_y.set) - - status_bar = tk.Frame(self) - status_bar.pack(side=tk.BOTTOM, fill=tk.X) - b = tk.Button(status_bar, text="Button 1") - b.pack(side=tk.LEFT, padx=1) - b = tk.Button(status_bar, text="Button 2") - b.pack(side=tk.LEFT, padx=1) - b = tk.Button(status_bar, text="Button 3") - b.pack(side=tk.LEFT, padx=1) - - -if __name__ == "__main__": - root = tk.Tk() - root.title("Graph Canvas") - root.geometry("800x600") - root.state("zoomed") - app = Application(master=root) - app.mainloop() From bcb7bf4a10a632032043e9d7805b08fc6835518a Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Sun, 15 Sep 2019 16:38:12 -0700 Subject: [PATCH 003/462] coretk change to set a core-icon --- coretk/coretk/app.py | 18 ++++++++++++------ coretk/coretk/core-icon.png | Bin 0 -> 2931 bytes 2 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 coretk/coretk/core-icon.png diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index cc664923..5957054a 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,4 +1,5 @@ import tkinter as tk +from PIL import Image, ImageTk from coretk.graph import CanvasGraph @@ -6,18 +7,27 @@ from coretk.graph import CanvasGraph class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) + self.master.title("CORE") + self.master.geometry("800x600") + self.master.state("zoomed") + self.set_icon() self.pack(fill=tk.BOTH, expand=1) self.images = [] self.menubar = None self.create_menu() self.create_widgets() + def set_icon(self): + image = Image.open("core-icon.png") + tk_image = ImageTk.PhotoImage(image) + self.master.tk.call("wm", "iconphoto", self.master._w, tk_image) + def create_menu(self): self.master.option_add("*tearOff", tk.FALSE) self.menubar = tk.Menu(self.master) file_menu = tk.Menu(self.menubar) file_menu.add_command(label="Open") - file_menu.add_command(label="Exit", command=root.quit) + file_menu.add_command(label="Exit", command=self.master.quit) self.menubar.add_cascade(label="File", menu=file_menu) help_menu = tk.Menu(self.menubar) self.menubar.add_cascade(label="Help", menu=help_menu) @@ -60,9 +70,5 @@ class Application(tk.Frame): if __name__ == "__main__": - root = tk.Tk() - root.title("Graph Canvas") - root.geometry("800x600") - root.state("zoomed") - app = Application(master=root) + app = Application() app.mainloop() diff --git a/coretk/coretk/core-icon.png b/coretk/coretk/core-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0b0ff5aa3a51f71807691032f516d7a15d7da896 GIT binary patch literal 2931 zcmV-(3yk!MP)O2x6XXC0pBbvv5f}@T(BXrBb?NUYTVXBwzz2H$ZZs<%|)6z z4ZU&qR*F=~RwdN9QQBCRX{9>Gu}zv%w{2qq)F^_-cv1X*=t+6bvXC$Ur;8klJ`u*M@O|rQx$bSL|H7aso7y(*5Q2W#c}u10NaL%lSNvaZ5e2X6z?Bt@^^gUbV`0MdSrcDr+` z+wSgbEw&E)@X$H?Ui-y+9sc&yJGKuG5B<&9==ej!!;SIL;W2Q@!8wOf@EGi&vJfye|hnwdWIhcxZU{%JJqS!}SJy@BjfI zhN9C|HZzP!K$Ak>=t|#s4&PZSQj0Xn(Cu{5>-Ld!Q)E4ZK_~r2mgY}heS5L^xeol+ z(+`xVSFPT2`?@>spI*5dQ51m@F;zG%1B9v#SrNrvTarGGZA`&cI83!N5=yFqSXCgH z0s`QiN0z4uo@Tubn`>j`gT2MX{!9VC|Lkw-72_-Sted%IOTAVHS<7vj$Up%U$Ifx) z;RDloh>A!XC1S+TqO5C(qgt@eBX|NJ-~%DI2DvqGo)~%F49?$~wA1qxXb8YE0LY&f zFuij1?$Ob)yB7V}+U3Q8Z|cEy#(9t=I2k9p3)i_fhL_t@ZzMm+bWD#>DNX6bnQIQ1iyG7=^CJkBe zfdzS|Rb z&IGJCIKLRCTkS*+XGx#RRTOzo3GP@yL>Q^}DF}!`FG@vYq^RFEk@b#W4xD9W2z|{s zoH=~%md74_s5CR(MA|p_?=xpEmR44u#*cm+L33jI!FqLQ>gv*E_)xH@HW7T5U=aEU zgH-w6{3UJ@80%zdv0VwZQI}Misk5r0xQZmpka-gf@Avx4gEC_rs<6@7xdqiZdE=HR z{`7Z6V`v!dR-gX!#9J4yrmeP3(+2?TmjEzQ8{1`Uj?Q2i$^|)y>sAB@+wE)8%4*N0 zG8R_rLe=U;6xCVpozBF^4;h#!=`wg`l@FenBkrWM zp78*1l%hJ~sE+dZa3%&tbKM7;s#1&P*;ALen5bN>RRw+Pxo=PIxNqC|r$75JYLyC( z9zQwQ``y>yMgVy!0x^JdFCxd1Ia ze8^$FM`y898co&h>uzoGg;pB}_P^<-x2?J0(^MRI=Fr;RfAnd+`R-Z3GraJx2X7oa zd3+8i!2y7bz_~_X3^07FWW?8Jc?OX=#VvBjttU|mRCo?&k-FUHwm@Q1FeZ@C_r~tI zZNrRODcPk9SMmJw2L~T~a;nAG{MvmVYUH-S z(ZfH&zkl!ewYxrb-~5};9-LWGsf|DN_q)-k58=wC8}@Hsed%rFW*uysZxy;8y9|2k*Be)vC5`!o}hYi5}y1_GBF0D;JZ zH~=9aqEb}`OGxqr?S30l3gp_Lqzh9UES@gcDmKq~Y4g~|Ti30hDgro!P=YfqhPfyau`mEEF+w1B^LOQH zAiLe}oE14tVc^K3;1)&E7sH`Bz4M{_8<2z}M^0k@(R1PU9UIRbdgePbbuPy~|HPx1 z7;9i*v1g86yMB32j5nQ3%fyQ7Ob8GWjO3mP;SJ{y&hk$x7-$g?12xVq#|5A zf88Iq$!V{(&zF`{Z_R*-65u_UWPmg9&LAeHLa@kSeU>e?xi3_?K4K-QGt%Y$nC24I zXb4HKU%q>4)B0Q1&+yh#8+)I9*(9s7)3wSdJM`?Ut4BtLxKPTSIC>hjp&CU|MBT;Z ziQ2}s*JDx4dntWERUj0IoEHj~d&ZCpX1xmtMBv<8tqG;tI4IALF#!1F=N@^^_RRg; zereO>$V46OZU_JP{L9^9dT?GvF*Sx)qFNqdRUYXy(ndN`o~ytdgo*3ISuazU_lv?Z zmgeeal*}2I1wudw9!1s#0O+WOxH5`zJbLN}U;64}a_7#SvURSt;of_;OcDZ496yCO zkG?stnrMa0N$v_{qmyf(CbY?~oRozVnN*I1AY#(z)VMu|7rOh-SzM{WA0|0sX^107!+qUVhPN#<>$1c!3r{&6+UJ`|q5LHhQBo{9I zkLz{ILO3l%BnoCd@d6@n!H`}mh}oP6&p3m32F_*(fmm1~zPAB?VF(4uEHYJVK$yR*zSbwk7X!uVj#{5LPhH z0t3;_+_f!~43?D!nRGtKfCuC9)-tkAM-cr0K0Ns|sd#aI$*-K6I0%nBoe!(5^KhQv z1HpL*R98+FmC+J+Nhh#Cj)f2hQo>si6@}J$rX5-GVK{OYzQkD{$itM!m_ibru3f`b?ap4^>ya002ovPDHLkV1lYhm(u_M literal 0 HcmV?d00001 From 6f916995d7fe36a892dda237c1b82e4df2b759f6 Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Sun, 15 Sep 2019 23:16:08 -0700 Subject: [PATCH 004/462] setup coretk left side buttons as radio buttons --- .gitignore | 3 +++ coretk/coretk/app.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 038146aa..700cbf17 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,6 @@ ns3/setup.py # ignore corefx build corefx/target + +# python +__pycache__ diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 5957054a..e8b559a5 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -36,11 +36,16 @@ class Application(tk.Frame): def create_widgets(self): edit_frame = tk.Frame(self) edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) - b = tk.Button(edit_frame, text="Button 1") + radio_value = tk.IntVar() + b = tk.Radiobutton(edit_frame, text="Button 1", indicatoron=False, variable=radio_value, value=1) b.pack(side=tk.TOP, pady=1) - b = tk.Button(edit_frame, text="Button 2") + b = tk.Radiobutton(edit_frame, text="Button 2", indicatoron=False, variable=radio_value, value=2) b.pack(side=tk.TOP, pady=1) - b = tk.Button(edit_frame, text="Button 3") + b = tk.Radiobutton(edit_frame, text="Button 3", indicatoron=False, variable=radio_value, value=3) + b.pack(side=tk.TOP, pady=1) + b = tk.Radiobutton(edit_frame, text="Button 4", indicatoron=False, variable=radio_value, value=4) + b.pack(side=tk.TOP, pady=1) + b = tk.Radiobutton(edit_frame, text="Button 5", indicatoron=False, variable=radio_value, value=5) b.pack(side=tk.TOP, pady=1) self.canvas = CanvasGraph( From d88ea50ad232a5f8ab2ed11ddc9801abaf1864c6 Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Sun, 15 Sep 2019 23:45:13 -0700 Subject: [PATCH 005/462] coretk - changes to have app load all images, and some small cleanup --- coretk/coretk/app.py | 49 ++++++++++++++++++++++++++---------------- coretk/coretk/graph.py | 22 ++++++------------- coretk/coretk/icons.py | 15 +++++++++++++ 3 files changed, 52 insertions(+), 34 deletions(-) create mode 100644 coretk/coretk/icons.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index e8b559a5..9294b523 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,26 +1,29 @@ import tkinter as tk -from PIL import Image, ImageTk from coretk.graph import CanvasGraph +from coretk.icons import Images class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) - self.master.title("CORE") - self.master.geometry("800x600") - self.master.state("zoomed") - self.set_icon() - self.pack(fill=tk.BOTH, expand=1) - self.images = [] + self.load_images() + self.setup_app() self.menubar = None self.create_menu() self.create_widgets() - def set_icon(self): - image = Image.open("core-icon.png") - tk_image = ImageTk.PhotoImage(image) - self.master.tk.call("wm", "iconphoto", self.master._w, tk_image) + def load_images(self): + Images.load("switch", "switch.png") + Images.load("core", "core-icon.png") + + def setup_app(self): + self.master.title("CORE") + self.master.geometry("800x600") + self.master.state("zoomed") + image = Images.get("core") + self.master.tk.call("wm", "iconphoto", self.master._w, image) + self.pack(fill=tk.BOTH, expand=True) def create_menu(self): self.master.option_add("*tearOff", tk.FALSE) @@ -34,26 +37,34 @@ class Application(tk.Frame): self.master.config(menu=self.menubar) def create_widgets(self): + image = Images.get("switch") edit_frame = tk.Frame(self) edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) radio_value = tk.IntVar() - b = tk.Radiobutton(edit_frame, text="Button 1", indicatoron=False, variable=radio_value, value=1) + b = tk.Radiobutton( + edit_frame, indicatoron=False, variable=radio_value, value=1, image=image + ) b.pack(side=tk.TOP, pady=1) - b = tk.Radiobutton(edit_frame, text="Button 2", indicatoron=False, variable=radio_value, value=2) + b = tk.Radiobutton( + edit_frame, indicatoron=False, variable=radio_value, value=2, image=image + ) b.pack(side=tk.TOP, pady=1) - b = tk.Radiobutton(edit_frame, text="Button 3", indicatoron=False, variable=radio_value, value=3) + b = tk.Radiobutton( + edit_frame, indicatoron=False, variable=radio_value, value=3, image=image + ) b.pack(side=tk.TOP, pady=1) - b = tk.Radiobutton(edit_frame, text="Button 4", indicatoron=False, variable=radio_value, value=4) + b = tk.Radiobutton( + edit_frame, indicatoron=False, variable=radio_value, value=4, image=image + ) b.pack(side=tk.TOP, pady=1) - b = tk.Radiobutton(edit_frame, text="Button 5", indicatoron=False, variable=radio_value, value=5) + b = tk.Radiobutton( + edit_frame, indicatoron=False, variable=radio_value, value=5, image=image + ) b.pack(side=tk.TOP, pady=1) self.canvas = CanvasGraph( self, background="#cccccc", scrollregion=(0, 0, 1000, 1000) ) - self.canvas.load("switch", "switch.png") - self.canvas.add_node(50, 50, "Node 1", "switch") - self.canvas.add_node(50, 100, "Node 2", "switch") self.canvas.pack(fill=tk.BOTH, expand=True) scroll_x = tk.Scrollbar( self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index d6e73e3c..4e5cef74 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -1,7 +1,7 @@ import enum import tkinter as tk -from PIL import Image, ImageTk +from coretk.icons import Images class GraphMode(enum.Enum): @@ -11,14 +11,6 @@ class GraphMode(enum.Enum): class CanvasGraph(tk.Canvas): - images = {} - - @classmethod - def load(cls, name, file_path): - image = Image.open(file_path) - tk_image = ImageTk.PhotoImage(image) - cls.images[name] = tk_image - def __init__(self, master=None, cnf=None, **kwargs): if cnf is None: cnf = {} @@ -78,7 +70,7 @@ class CanvasGraph(tk.Canvas): self.handle_edge_release(event) elif self.mode == GraphMode.NODE: x, y = self.canvas_xy(event) - self.add_node(x, y, "Node", "switch") + self.add_node(x, y, "switch") def handle_edge_release(self, event): edge = self.drawing_edge @@ -146,9 +138,9 @@ class CanvasGraph(tk.Canvas): self.mode = GraphMode.NODE print(f"graph mode: {self.mode}") - def add_node(self, x, y, name, image_name): - image = self.images[image_name] - node = CanvasNode(x, y, name, image, self) + def add_node(self, x, y, image_name): + image = Images.get(image_name) + node = CanvasNode(x, y, image, self) self.nodes[node.id] = node return node @@ -175,13 +167,13 @@ class CanvasEdge: class CanvasNode: - def __init__(self, x, y, name, image, canvas): - self.name = name + def __init__(self, x, y, image, canvas): self.image = image self.canvas = canvas self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags="node" ) + self.name = f"Node {self.id}" self.text_id = self.canvas.create_text(x, y + 20, text=self.name) self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) diff --git a/coretk/coretk/icons.py b/coretk/coretk/icons.py new file mode 100644 index 00000000..94517a14 --- /dev/null +++ b/coretk/coretk/icons.py @@ -0,0 +1,15 @@ +from PIL import Image, ImageTk + + +class Images: + images = {} + + @classmethod + def load(cls, name, file_path): + image = Image.open(file_path) + tk_image = ImageTk.PhotoImage(image) + cls.images[name] = tk_image + + @classmethod + def get(cls, name): + return cls.images[name] From 00a37cbb6fcd9018916a294e5085460574a4b065 Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Tue, 17 Sep 2019 13:18:07 -0700 Subject: [PATCH 006/462] coretk - removed python3.7 hard requirement --- coretk/Pipfile | 3 --- 1 file changed, 3 deletions(-) diff --git a/coretk/Pipfile b/coretk/Pipfile index ff97aa2f..d5344207 100644 --- a/coretk/Pipfile +++ b/coretk/Pipfile @@ -10,6 +10,3 @@ black = "==19.3b0" [packages] pillow = "*" - -[requires] -python_version = "3.7" From 8682f01fdce59559678f48e4492c16d8161eef1b Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Wed, 18 Sep 2019 11:20:22 -0700 Subject: [PATCH 007/462] coretk - updated pipfile to install as editable, added convenient run script, updated images to load assuming local path --- coretk/Pipfile | 5 ++++- coretk/Pipfile.lock | 11 ++++++----- coretk/coretk/app.py | 1 - coretk/coretk/icons.py | 4 ++++ coretk/setup.py | 14 ++++++++++++++ 5 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 coretk/setup.py diff --git a/coretk/Pipfile b/coretk/Pipfile index d5344207..3f8edcc3 100644 --- a/coretk/Pipfile +++ b/coretk/Pipfile @@ -3,10 +3,13 @@ name = "pypi" url = "https://pypi.org/simple" verify_ssl = true +[scripts] +coretk = "python coretk/app.py" + [dev-packages] flake8 = "*" isort = "*" black = "==19.3b0" [packages] -pillow = "*" +coretk = {editable = true,path = "."} diff --git a/coretk/Pipfile.lock b/coretk/Pipfile.lock index 3900d09b..1ed92ea4 100644 --- a/coretk/Pipfile.lock +++ b/coretk/Pipfile.lock @@ -1,12 +1,10 @@ { "_meta": { "hash": { - "sha256": "c719ca1ace4a33dfe01fe579162d4557e8711819c4e7b4f8e799dd25b9cde596" + "sha256": "f5abf95b09f7c7431b5d2143b43b6873cf9cf7d90dfb2eaad08d2446368a0a05" }, "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, + "requires": {}, "sources": [ { "name": "pypi", @@ -16,6 +14,10 @@ ] }, "default": { + "coretk": { + "editable": true, + "path": "." + }, "pillow": { "hashes": [ "sha256:0804f77cb1e9b6dbd37601cee11283bba39a8d44b9ddb053400c58e0c0d7d9de", @@ -45,7 +47,6 @@ "sha256:ef6be704ae2bc8ad0ebc5cb850ee9139493b0fc4e81abcc240fb392a63ebc808", "sha256:f8dc19d92896558f9c4317ee365729ead9d7bbcf2052a9a19a3ef17abbb8ac5b" ], - "index": "pypi", "version": "==6.1.0" } }, diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 9294b523..3beac831 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -20,7 +20,6 @@ class Application(tk.Frame): def setup_app(self): self.master.title("CORE") self.master.geometry("800x600") - self.master.state("zoomed") image = Images.get("core") self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) diff --git a/coretk/coretk/icons.py b/coretk/coretk/icons.py index 94517a14..8aa0ae28 100644 --- a/coretk/coretk/icons.py +++ b/coretk/coretk/icons.py @@ -1,11 +1,15 @@ +import os from PIL import Image, ImageTk +PATH = os.path.abspath(os.path.dirname(__file__)) + class Images: images = {} @classmethod def load(cls, name, file_path): + file_path = os.path.join(PATH, file_path) image = Image.open(file_path) tk_image = ImageTk.PhotoImage(image) cls.images[name] = tk_image diff --git a/coretk/setup.py b/coretk/setup.py new file mode 100644 index 00000000..d68365ff --- /dev/null +++ b/coretk/setup.py @@ -0,0 +1,14 @@ +from setuptools import find_packages, setup + +setup( + name="coretk", + version="0.1.0", + packages=find_packages(), + install_requires=[ + "pillow", + ], + description="CORE GUI", + url="https://github.com/coreemu/core", + author="Boeing Research & Technology", + license="BSD", +) From 4a9e6febe59cb04d95b10c822153f820e1e37a84 Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Wed, 18 Sep 2019 11:21:41 -0700 Subject: [PATCH 008/462] coretk - changes icons.py to images.py --- coretk/coretk/app.py | 2 +- coretk/coretk/graph.py | 2 +- coretk/coretk/{icons.py => images.py} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename coretk/coretk/{icons.py => images.py} (100%) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 3beac831..5ca0067c 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,7 +1,7 @@ import tkinter as tk from coretk.graph import CanvasGraph -from coretk.icons import Images +from coretk.images import Images class Application(tk.Frame): diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 4e5cef74..23bc41bd 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -1,7 +1,7 @@ import enum import tkinter as tk -from coretk.icons import Images +from coretk.images import Images class GraphMode(enum.Enum): diff --git a/coretk/coretk/icons.py b/coretk/coretk/images.py similarity index 100% rename from coretk/coretk/icons.py rename to coretk/coretk/images.py From 17d1830176bdb7502fe2b359e48749a1deb5a831 Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Wed, 18 Sep 2019 11:25:33 -0700 Subject: [PATCH 009/462] coretk changed prints to use logging --- coretk/coretk/app.py | 2 ++ coretk/coretk/graph.py | 25 +++++++++++++------------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 5ca0067c..163c8a17 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,3 +1,4 @@ +import logging import tkinter as tk from coretk.graph import CanvasGraph @@ -85,5 +86,6 @@ class Application(tk.Frame): if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) app = Application() app.mainloop() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 23bc41bd..9bd599e9 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -1,4 +1,5 @@ import enum +import logging import tkinter as tk from coretk.images import Images @@ -65,7 +66,7 @@ class CanvasGraph(tk.Canvas): def click_release(self, event): self.focus_set() self.selected = self.get_selected(event) - print(f"click release selected: {self.selected}") + logging.debug(f"click release selected: {self.selected}") if self.mode == GraphMode.EDGE: self.handle_edge_release(event) elif self.mode == GraphMode.NODE: @@ -81,8 +82,8 @@ class CanvasGraph(tk.Canvas): return # edge dst must be a node - print(f"current selected: {self.selected}") - print(f"current nodes: {self.find_withtag('node')}") + logging.debug(f"current selected: {self.selected}") + logging.debug(f"current nodes: {self.find_withtag('node')}") is_node = self.selected in self.find_withtag("node") if not is_node: edge.delete() @@ -95,7 +96,7 @@ class CanvasGraph(tk.Canvas): # set dst node and snap edge to center x, y = self.coords(self.selected) edge.complete(self.selected, x, y) - print(f"drawing edge token: {edge.token}") + logging.debug(f"drawing edge token: {edge.token}") if edge.token in self.edges: edge.delete() else: @@ -105,10 +106,10 @@ class CanvasGraph(tk.Canvas): node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) - print(f"edges: {self.find_withtag('edge')}") + logging.debug(f"edges: {self.find_withtag('edge')}") def click_press(self, event): - print(f"click press: {event}") + logging.debug(f"click press: {event}") selected = self.get_selected(event) is_node = selected in self.find_withtag("node") if self.mode == GraphMode.EDGE and is_node: @@ -125,18 +126,18 @@ class CanvasGraph(tk.Canvas): selected = self.get_selected(event) nodes = self.find_withtag("node") if selected in nodes: - print(f"node context: {selected}") + logging.debug(f"node context: {selected}") self.node_context.post(event.x_root, event.y_root) def set_mode(self, event): - print(f"mode event: {event}") + logging.debug(f"mode event: {event}") if event.char == "e": self.mode = GraphMode.EDGE elif event.char == "s": self.mode = GraphMode.SELECT elif event.char == "n": self.mode = GraphMode.NODE - print(f"graph mode: {self.mode}") + logging.debug(f"graph mode: {self.mode}") def add_node(self, x, y, image_name): image = Images.get(image_name) @@ -183,11 +184,11 @@ class CanvasNode: self.moving = None def click_press(self, event): - print(f"click press {self.name}: {event}") + logging.debug(f"click press {self.name}: {event}") self.moving = self.canvas.canvas_xy(event) def click_release(self, event): - print(f"click release {self.name}: {event}") + logging.debug(f"click release {self.name}: {event}") self.moving = None def motion(self, event): @@ -210,4 +211,4 @@ class CanvasNode: self.canvas.coords(edge.id, x1, y1, new_x, new_y) def context(self, event): - print(f"context click {self.name}: {event}") + logging.debug(f"context click {self.name}: {event}") From 2f8935b4c99e9da8c2d035a7367e6bf767c22566 Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Wed, 18 Sep 2019 11:26:26 -0700 Subject: [PATCH 010/462] coretk - formatting cleanup --- coretk/coretk/images.py | 1 + coretk/setup.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 8aa0ae28..4ca2b5ac 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -1,4 +1,5 @@ import os + from PIL import Image, ImageTk PATH = os.path.abspath(os.path.dirname(__file__)) diff --git a/coretk/setup.py b/coretk/setup.py index d68365ff..8b8ce2d3 100644 --- a/coretk/setup.py +++ b/coretk/setup.py @@ -4,9 +4,7 @@ setup( name="coretk", version="0.1.0", packages=find_packages(), - install_requires=[ - "pillow", - ], + install_requires=["pillow"], description="CORE GUI", url="https://github.com/coreemu/core", author="Boeing Research & Technology", From 372a690af95df3ebd51cf90a1b0252b19f1116d3 Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Wed, 18 Sep 2019 11:39:48 -0700 Subject: [PATCH 011/462] coretk - added to pre-commit --- coretk/Pipfile | 1 + coretk/Pipfile.lock | 98 +++++++++++++++++++++++++++++++++- daemon/.pre-commit-config.yaml | 21 ++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) diff --git a/coretk/Pipfile b/coretk/Pipfile index 3f8edcc3..720b1c45 100644 --- a/coretk/Pipfile +++ b/coretk/Pipfile @@ -10,6 +10,7 @@ coretk = "python coretk/app.py" flake8 = "*" isort = "*" black = "==19.3b0" +pre-commit = "*" [packages] coretk = {editable = true,path = "."} diff --git a/coretk/Pipfile.lock b/coretk/Pipfile.lock index 1ed92ea4..a08ee17f 100644 --- a/coretk/Pipfile.lock +++ b/coretk/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f5abf95b09f7c7431b5d2143b43b6873cf9cf7d90dfb2eaad08d2446368a0a05" + "sha256": "52de2a0b7a80abe39564a3943879fe75305b0cb8243c0fce78ef43689bee77c0" }, "pipfile-spec": 6, "requires": {}, @@ -58,6 +58,13 @@ ], "version": "==1.4.3" }, + "aspy.yaml": { + "hashes": [ + "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc", + "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45" + ], + "version": "==1.3.0" + }, "attrs": { "hashes": [ "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", @@ -73,6 +80,13 @@ "index": "pypi", "version": "==19.3b0" }, + "cfgv": { + "hashes": [ + "sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144", + "sha256:fbd93c9ab0a523bf7daec408f3be2ed99a980e20b2d19b50fc184ca6b820d289" + ], + "version": "==2.0.1" + }, "click": { "hashes": [ "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", @@ -95,6 +109,28 @@ "index": "pypi", "version": "==3.7.8" }, + "identify": { + "hashes": [ + "sha256:4f1fe9a59df4e80fcb0213086fcf502bc1765a01ea4fe8be48da3b65afd2a017", + "sha256:d8919589bd2a5f99c66302fec0ef9027b12ae150b0b0213999ad3f695fc7296e" + ], + "version": "==1.4.7" + }, + "importlib-metadata": { + "hashes": [ + "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", + "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" + ], + "version": "==0.23" + }, + "importlib-resources": { + "hashes": [ + "sha256:6e2783b2538bd5a14678284a3962b0660c715e5a0f10243fd5e00a4b5974f50b", + "sha256:d3279fd0f6f847cced9f7acc19bd3e5df54d34f93a2e7bb5f238f81545787078" + ], + "markers": "python_version < '3.7'", + "version": "==1.0.2" + }, "isort": { "hashes": [ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", @@ -110,6 +146,27 @@ ], "version": "==0.6.1" }, + "more-itertools": { + "hashes": [ + "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", + "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" + ], + "version": "==7.2.0" + }, + "nodeenv": { + "hashes": [ + "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a" + ], + "version": "==1.3.3" + }, + "pre-commit": { + "hashes": [ + "sha256:1d3c0587bda7c4e537a46c27f2c84aa006acc18facf9970bf947df596ce91f3f", + "sha256:fa78ff96e8e9ac94c748388597693f18b041a181c94a4f039ad20f45287ba44a" + ], + "index": "pypi", + "version": "==1.18.3" + }, "pycodestyle": { "hashes": [ "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", @@ -124,12 +181,51 @@ ], "version": "==2.1.1" }, + "pyyaml": { + "hashes": [ + "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", + "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", + "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", + "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", + "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", + "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", + "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", + "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", + "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", + "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", + "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", + "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", + "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + ], + "version": "==5.1.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, "toml": { "hashes": [ "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" ], "version": "==0.10.0" + }, + "virtualenv": { + "hashes": [ + "sha256:680af46846662bb38c5504b78bad9ed9e4f3ba2d54f54ba42494fdf94337fe30", + "sha256:f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2" + ], + "version": "==16.7.5" + }, + "zipp": { + "hashes": [ + "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", + "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" + ], + "version": "==0.6.0" } } } diff --git a/daemon/.pre-commit-config.yaml b/daemon/.pre-commit-config.yaml index 73566c9d..ac6bc80b 100644 --- a/daemon/.pre-commit-config.yaml +++ b/daemon/.pre-commit-config.yaml @@ -21,3 +21,24 @@ repos: language: system entry: bash -c 'cd daemon && pipenv run flake8' types: [python] + + - id: isort-tk + name: coretk-isort + stages: [commit] + language: system + entry: bash -c 'cd coretk && pipenv run isort --atomic -y' + types: [python] + + - id: black-tk + name: coretk-black + stages: [commit] + language: system + entry: bash -c 'cd coretk && pipenv run black .' + types: [python] + + - id: flake8-tk + name: coretk-flake8 + stages: [commit] + language: system + entry: bash -c 'cd coretk && pipenv run flake8' + types: [python] From 3268c110cd1bc31a844ae6db390e51409cab814e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 19 Sep 2019 10:35:35 -0700 Subject: [PATCH 012/462] gh action - adding coretk checks --- .github/workflows/coretk-checks.yml | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/coretk-checks.yml diff --git a/.github/workflows/coretk-checks.yml b/.github/workflows/coretk-checks.yml new file mode 100644 index 00000000..324babfd --- /dev/null +++ b/.github/workflows/coretk-checks.yml @@ -0,0 +1,31 @@ +name: CORE Tk Checks + +on: [push] + +jobs: + build: + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v1 + - name: Set up Python 3.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7 + - name: Install pipenv + run: | + python -m pip install --upgrade pip + pip install pipenv + cd coretk + pipenv install --dev + - name: isort + run: | + cd coretk + pipenv run isort -c + - name: black + run: | + cd coretk + pipenv run black --check . + - name: flake8 + run: | + cd coretk + pipenv run flake8 From 5297286b7ad06cc842dbc02c96547952b59436f4 Mon Sep 17 00:00:00 2001 From: Huy Pham Date: Thu, 19 Sep 2019 16:24:21 -0700 Subject: [PATCH 013/462] working on menubar --- coretk/coretk/app.py | 583 +++++++++++++++++++++++++++++++++++- coretk/coretk/menuaction.py | 334 +++++++++++++++++++++ 2 files changed, 911 insertions(+), 6 deletions(-) create mode 100644 coretk/coretk/menuaction.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 163c8a17..8e023cac 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,6 +1,7 @@ import logging import tkinter as tk +import coretk.menuaction as action from coretk.graph import CanvasGraph from coretk.images import Images @@ -25,16 +26,586 @@ class Application(tk.Frame): self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) + def create_file_menu(self): + """ + Create file menu + + :return: nothing + """ + file_menu = tk.Menu(self.master) + file_menu.add_command( + label="New", command=action.file_new, accelerator="Ctrl+N" + ) + file_menu.add_command( + label="Open...", command=action.file_open, accelerator="Ctrl+O" + ) + file_menu.add_command(label="Reload", command=action.file_reload) + file_menu.add_command( + label="Save", command=action.file_save, accelerator="Ctrl+S" + ) + file_menu.add_command(label="Save As XML...", command=action.file_save_as_xml) + file_menu.add_command(label="Save As imn...", command=action.file_save_as_imn) + + file_menu.add_separator() + + file_menu.add_command( + label="Export Python script...", command=action.file_export_python_script + ) + file_menu.add_command( + label="Execute XML or Python script...", + command=action.file_execute_xml_or_python_script, + ) + file_menu.add_command( + label="Execute Python script with options...", + command=action.file_execute_python_script_with_options, + ) + + file_menu.add_separator() + + file_menu.add_command( + label="Open current file in editor...", + command=action.file_open_current_file_in_editor, + ) + file_menu.add_command(label="Print...", command=action.file_print) + file_menu.add_command( + label="Save screenshot...", command=action.file_save_screenshot + ) + + file_menu.add_separator() + + file_menu.add_command( + label="/home/ncs/.core/configs/sample1.imn", + command=action.file_example_link, + ) + + file_menu.add_separator() + + file_menu.add_command(label="Quit", command=self.master.quit) + self.menubar.add_cascade(label="File", menu=file_menu) + + def create_edit_menu(self): + """ + Create edit menu + + :return: nothing + """ + edit_menu = tk.Menu(self.master) + edit_menu.add_command( + label="Undo", command=action.edit_undo, accelerator="Ctrl+Z" + ) + edit_menu.add_command( + label="Redo", command=action.edit_redo, accelerator="Ctrl+Y" + ) + + edit_menu.add_separator() + + edit_menu.add_command( + label="Cut", command=action.edit_cut, accelerator="Ctrl+X" + ) + edit_menu.add_command( + label="Copy", command=action.edit_copy, accelerator="Ctrl+C" + ) + edit_menu.add_command( + label="Paste", command=action.edit_paste, accelerator="Ctrl+V" + ) + + edit_menu.add_separator() + + edit_menu.add_command( + label="Select all", command=action.edit_select_all, accelerator="Ctrl+A" + ) + edit_menu.add_command( + label="Select Adjacent", + command=action.edit_select_adjacent, + accelerator="Ctrl+J", + ) + + edit_menu.add_separator() + + edit_menu.add_command( + label="Find...", command=action.edit_find, accelerator="Ctrl+F" + ) + edit_menu.add_command(label="Clear marker", command=action.edit_clear_marker) + edit_menu.add_command(label="Preferences...", command=action.edit_preferences) + + self.menubar.add_cascade(label="Edit", menu=edit_menu) + + def create_canvas_menu(self): + """ + Create canvas menu + + :return: nothing + """ + canvas_menu = tk.Menu(self.master) + canvas_menu.add_command(label="New", command=action.canvas_new) + canvas_menu.add_command(label="Manage...", command=action.canvas_manage) + canvas_menu.add_command(label="Delete", command=action.canvas_delete) + + canvas_menu.add_separator() + + canvas_menu.add_command(label="Size/scale...", command=action.canvas_size_scale) + canvas_menu.add_command(label="Wallpaper...", command=action.canvas_wallpaper) + + canvas_menu.add_separator() + + canvas_menu.add_command( + label="Previous", command=action.canvas_previous, accelerator="PgUp" + ) + canvas_menu.add_command( + label="Next", command=action.canvas_next, accelerator="PgDown" + ) + canvas_menu.add_command( + label="First", command=action.canvas_first, accelerator="Home" + ) + canvas_menu.add_command( + label="Last", command=action.canvas_last, accelerator="End" + ) + + self.menubar.add_cascade(label="Canvas", menu=canvas_menu) + + def create_show_menu(self, view_menu): + """ + Create the menu items in View/Show + + :param tkinter.Menu view_menu: the view menu + :return: nothing + """ + show_menu = tk.Menu(self.master) + show_menu.add_command(label="All") + show_menu.add_command(label="None") + show_menu.add_separator() + show_menu.add_command(label="Interface Names") + show_menu.add_command(label="IPv4 Addresses") + show_menu.add_command(label="IPv6 Addresses") + show_menu.add_command(label="Node Labels") + show_menu.add_command(label="Annotations") + show_menu.add_command(label="Grid") + show_menu.add_command(label="API Messages") + + view_menu.add_cascade(label="Show", menu=show_menu) + + def create_view_menu(self): + """ + Create view menu + + :return: nothing + """ + view_menu = tk.Menu(self.master) + self.create_show_menu(view_menu) + view_menu.add_command( + label="Show hidden nodes", command=action.view_show_hidden_nodes + ) + view_menu.add_command(label="Locked", command=action.view_locked) + view_menu.add_command(label="3D GUI...", command=action.view_3d_gui) + + view_menu.add_separator() + + view_menu.add_command( + label="Zoom in", command=action.view_zoom_in, accelerator="+" + ) + view_menu.add_command( + label="Zoom out", command=action.view_zoom_out, accelerator="-" + ) + + self.menubar.add_cascade(label="View", menu=view_menu) + + def create_experimental_menu(self, tools_menu): + experimental_menu = tk.Menu(self.master) + experimental_menu.add_command(label="Plugins...") + experimental_menu.add_command(label="ns2immunes converter...") + experimental_menu.add_command(label="Topology partitioning...") + + tools_menu.add_cascade(label="Experimental", menu=experimental_menu) + + def create_random_menu(self, topology_generator_menu): + """ + Create random menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + random_menu = tk.Menu(self.master) + random_menu.add_command(label="R(1)") + random_menu.add_command(label="R(5)") + random_menu.add_command(label="R(10)") + random_menu.add_command(label="R(15)") + random_menu.add_command(label="R(20)") + random_menu.add_command(label="R(30)") + random_menu.add_command(label="R(40)") + random_menu.add_command(label="R(50)") + random_menu.add_command(label="R(75)") + random_menu.add_command(label="R(100)") + + topology_generator_menu.add_cascade(label="Random", menu=random_menu) + + def create_grid_menu(self, topology_generator_menu): + """ + Create grid menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology_generator_menu + :return: nothing + """ + grid_menu = tk.Menu(self.master) + grid_menu.add_command(label="G(1)") + grid_menu.add_command(label="G(5)") + grid_menu.add_command(label="G(10)") + grid_menu.add_command(label="G(15)") + grid_menu.add_command(label="G(20)") + grid_menu.add_command(label="G(25)") + grid_menu.add_command(label="G(30)") + grid_menu.add_command(label="G(35)") + grid_menu.add_command(label="G(40)") + grid_menu.add_command(label="G(50)") + grid_menu.add_command(label="G(60)") + grid_menu.add_command(label="G(70)") + grid_menu.add_command(label="G(80)") + grid_menu.add_command(label="G(90)") + grid_menu.add_command(label="G(100)") + + topology_generator_menu.add_cascade(label="Grid", menu=grid_menu) + + # TODO do later + def create_connected_grid_menu(self, topology_generator_menu): + """ + Create connected grid menu items and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + return + + def create_chain_menu(self, topology_generator_menu): + """ + Create chain menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + chain_menu = tk.Menu(self.master) + chain_menu.add_command(label="P(2)") + chain_menu.add_command(label="P(3)") + chain_menu.add_command(label="P(4)") + chain_menu.add_command(label="P(5)") + chain_menu.add_command(label="P(6)") + chain_menu.add_command(label="P(7)") + chain_menu.add_command(label="P(8)") + chain_menu.add_command(label="P(9)") + chain_menu.add_command(label="P(10)") + chain_menu.add_command(label="P(11)") + chain_menu.add_command(label="P(12)") + chain_menu.add_command(label="P(13)") + chain_menu.add_command(label="P(14)") + chain_menu.add_command(label="P(15)") + chain_menu.add_command(label="P(16)") + chain_menu.add_command(label="P(17)") + chain_menu.add_command(label="P(18)") + chain_menu.add_command(label="P(19)") + chain_menu.add_command(label="P(20)") + chain_menu.add_command(label="P(21)") + chain_menu.add_command(label="P(22)") + chain_menu.add_command(label="P(23)") + chain_menu.add_command(label="P(24)") + chain_menu.add_command(label="P(32)") + chain_menu.add_command(label="P(64)") + chain_menu.add_command(label="P(128)") + topology_generator_menu.add_cascade(label="Chain", menu=chain_menu) + + def create_star_menu(self, topology_generator_menu): + """ + Create star menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + star_menu = tk.Menu(self.master) + for i in range(3, 26, 1): + the_label = "C(" + str(i) + ")" + star_menu.add_command(label=the_label) + + topology_generator_menu.add_cascade(label="Star", menu=star_menu) + + def create_cycle_menu(self, topology_generator_menu): + """ + Create cycle menu item and the sub items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + cycle_menu = tk.Menu(self.master) + for i in range(3, 25, 1): + the_label = "C(" + str(i) + ")" + cycle_menu.add_command(label=the_label) + + topology_generator_menu.add_cascade(label="Cycle", menu=cycle_menu) + + def create_wheel_menu(self, topology_generator_menu): + """ + Create wheel menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + wheel_menu = tk.Menu(self.master) + for i in range(4, 26, 1): + the_label = "W(" + str(i) + ")" + wheel_menu.add_command(label=the_label) + + topology_generator_menu.add_cascade(label="Wheel", menu=wheel_menu) + + def create_cube_menu(self, topology_generator_menu): + """ + Create cube menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + cube_menu = tk.Menu(self.master) + for i in range(2, 7, 1): + the_label = "Q(" + str(i) + ")" + cube_menu.add_command(label=the_label) + + topology_generator_menu.add_cascade(label="Cube", menu=cube_menu) + + def create_clique_menu(self, topology_generator_menu): + """ + Create clique menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + clique_menu = tk.Menu(self.master) + for i in range(3, 25, 1): + the_label = "K(" + str(i) + ")" + clique_menu.add_command(label=the_label) + + topology_generator_menu.add_cascade(label="Clique", menu=clique_menu) + + # TODO do later + def create_bipartite_menu(self, topology_generator_menu): + """ + Create bipartite menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology_generator_menu + :return: nothing + """ + # bipartite_menu = tk.Menu(self.master) + return + + def create_topology_generator_menu(self, tools_menu): + """ + Create topology menu item and its sub menu items + + :param tkinter.Menu tools_menu: tools menu + + :return: nothing + """ + topology_generator_menu = tk.Menu(self.master) + # topology_generator_menu.add_command(label="Random") + self.create_random_menu(topology_generator_menu) + # topology_generator_menu.add_command(label="Grid") + self.create_grid_menu(topology_generator_menu) + topology_generator_menu.add_command(label="Connected Grid") + self.create_chain_menu(topology_generator_menu) + # topology_generator_menu.add_command(label="Chain") + # topology_generator_menu.add_command(label="Star") + self.create_star_menu(topology_generator_menu) + # topology_generator_menu.add_command(label="Cycle") + self.create_cycle_menu(topology_generator_menu) + # topology_generator_menu.add_command(label="Wheel") + self.create_wheel_menu(topology_generator_menu) + # topology_generator_menu.add_command(label="Cube") + self.create_cube_menu(topology_generator_menu) + # topology_generator_menu.add_command(label="Clique") + self.create_clique_menu(topology_generator_menu) + topology_generator_menu.add_command(label="Bipartite") + + tools_menu.add_cascade(label="Topology generator", menu=topology_generator_menu) + + def create_tools_menu(self): + """ + Create tools menu + + :return: nothing + """ + + tools_menu = tk.Menu(self.master) + tools_menu.add_command( + label="Auto rearrange all", command=action.tools_auto_rearrange_all + ) + tools_menu.add_command( + label="Auto rearrange selected", + command=action.tools_auto_rearrange_selected, + ) + tools_menu.add_separator() + + tools_menu.add_command( + label="Align to grid", command=action.tools_align_to_grid + ) + + tools_menu.add_separator() + + tools_menu.add_command(label="Traffic...", command=action.tools_traffic) + tools_menu.add_command( + label="IP addresses...", command=action.tools_ip_addresses + ) + tools_menu.add_command( + label="MAC addresses...", command=action.tools_mac_addresses + ) + tools_menu.add_command( + label="Build hosts file...", command=action.tools_build_hosts_file + ) + tools_menu.add_command( + label="Renumber nodes...", command=action.tools_renumber_nodes + ) + self.create_experimental_menu(tools_menu) + self.create_topology_generator_menu(tools_menu) + tools_menu.add_command(label="Debugger...", command=action.tools_debugger) + + self.menubar.add_cascade(label="Tools", menu=tools_menu) + + def create_observer_widgets_menu(self, widget_menu): + """ + Create observer widget menu item and create the sub menu items inside + + :param tkinter.Menu widget_menu: widget_menu + :return: nothing + """ + observer_widget_menu = tk.Menu(self.master) + observer_widget_menu.add_command(label="None") + observer_widget_menu.add_command(label="processes") + observer_widget_menu.add_command(label="ifconfig") + observer_widget_menu.add_command(label="IPv4 routes") + observer_widget_menu.add_command(label="IPv6 routes") + observer_widget_menu.add_command(label="OSPFv2 neighbors") + observer_widget_menu.add_command(label="OSPFv3 neighbors") + observer_widget_menu.add_command(label="Listening sockets") + observer_widget_menu.add_command(label="IPv4 MFC entries") + observer_widget_menu.add_command(label="IPv6 MFC entries") + observer_widget_menu.add_command(label="firewall rules") + observer_widget_menu.add_command(label="IPsec policies") + observer_widget_menu.add_command(label="docker logs") + observer_widget_menu.add_command(label="OSPFv3 MDR level") + observer_widget_menu.add_command(label="PIM neighbors") + observer_widget_menu.add_command(label="Edit...") + + widget_menu.add_cascade(label="Observer Widgets", menu=observer_widget_menu) + + def create_adjacency_menu(self, widget_menu): + """ + Create adjacency menu item and the sub menu items inside + + :param tkinter.Menu widget_menu: widget menu + :return: nothing + """ + adjacency_menu = tk.Menu(self.master) + adjacency_menu.add_command(label="OSPFv2") + adjacency_menu.add_command(label="OSPFv3") + adjacency_menu.add_command(label="OSLR") + adjacency_menu.add_command(label="OSLRv2") + + widget_menu.add_cascade(label="Adjacency", menu=adjacency_menu) + + def create_widgets_menu(self): + """ + Create widget menu + + :return: nothing + """ + widget_menu = tk.Menu(self.master) + self.create_observer_widgets_menu(widget_menu) + self.create_adjacency_menu(widget_menu) + widget_menu.add_command(label="Throughput", command=action.widgets_throughput) + + widget_menu.add_separator() + + widget_menu.add_command( + label="Configure Adjacency...", command=action.widgets_configure_adjacency + ) + widget_menu.add_command( + label="Configure Throughput...", command=action.widgets_configure_throughput + ) + + self.menubar.add_cascade(label="Widgets", menu=widget_menu) + + def create_session_menu(self): + """ + Create session menu + + :return: nothing + """ + session_menu = tk.Menu(self.master) + session_menu.add_command(label="Start", command=action.session_start) + session_menu.add_command( + label="Change sessions...", command=action.session_change_sessions + ) + + session_menu.add_separator() + + session_menu.add_command( + label="Node types...", command=action.session_node_types + ) + session_menu.add_command(label="Comments...", command=action.session_comments) + session_menu.add_command(label="Hooks...", command=action.session_hooks) + session_menu.add_command( + label="Reset node positions", command=action.session_reset_node_positions + ) + session_menu.add_command( + label="Emulation servers...", command=action.session_emulation_servers + ) + session_menu.add_command(label="Options...", command=action.session_options) + + self.menubar.add_cascade(label="Session", menu=session_menu) + + def create_help_menu(self): + """ + Create help menu + + :return: nothing + """ + help_menu = tk.Menu(self.master) + help_menu.add_command( + label="Core Github (www)", command=action.help_core_github + ) + help_menu.add_command( + label="Core Documentation (www)", command=action.help_core_documentation + ) + help_menu.add_command(label="About", command=action.help_about) + + self.menubar.add_cascade(label="Help", menu=help_menu) + + def bind_menubar_shortcut(self): + self.bind_all("", action.file_new_shortcut) + self.bind_all("", action.file_open_shortcut) + self.bind_all("", action.file_save_shortcut) + self.bind_all("", action.edit_undo_shortcut) + self.bind_all("", action.edit_redo_shortcut) + self.bind_all("", action.edit_cut_shortcut) + self.bind_all("", action.edit_copy_shortcut) + self.bind_all("", action.edit_paste_shortcut) + self.bind_all("", action.edit_select_all_shortcut) + self.bind_all("", action.edit_select_adjacent_shortcut) + self.bind_all("", action.edit_find_shortcut) + self.bind_all("", action.canvas_previous_shortcut) + self.bind_all("", action.canvas_next_shortcut) + self.bind_all("", action.canvas_first_shortcut) + self.bind_all("", action.canvas_last_shortcut) + self.bind_all("", action.view_zoom_in_shortcut) + self.bind_all("", action.view_zoom_out_shortcut) + def create_menu(self): self.master.option_add("*tearOff", tk.FALSE) self.menubar = tk.Menu(self.master) - file_menu = tk.Menu(self.menubar) - file_menu.add_command(label="Open") - file_menu.add_command(label="Exit", command=self.master.quit) - self.menubar.add_cascade(label="File", menu=file_menu) - help_menu = tk.Menu(self.menubar) - self.menubar.add_cascade(label="Help", menu=help_menu) + self.create_file_menu() + self.create_edit_menu() + self.create_canvas_menu() + self.create_view_menu() + self.create_tools_menu() + self.create_widgets_menu() + self.create_session_menu() + self.create_help_menu() + self.master.config(menu=self.menubar) + self.bind_menubar_shortcut() def create_widgets(self): image = Images.get("switch") diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py new file mode 100644 index 00000000..005776f9 --- /dev/null +++ b/coretk/coretk/menuaction.py @@ -0,0 +1,334 @@ +""" +The actions taken when each menubar option is clicked +""" + +import logging +import webbrowser + + +def file_new(): + logging.debug("Click file New") + + +def file_new_shortcut(event): + logging.debug("Shortcut for file new shortcut") + + +def file_open(): + logging.debug("Click file Open") + + +def file_open_shortcut(event): + logging.debug("Shortcut for file open") + + +def file_reload(): + logging.debug("Click file Reload") + + +def file_save(): + logging.debug("Click file save") + + +def file_save_shortcut(event): + logging.debug("Shortcut for file save") + + +def file_save_as_xml(): + logging.debug("Click save as XML") + + +def file_save_as_imn(): + logging.debug("Click save as imn") + + +def file_export_python_script(): + logging.debug("Click file export python script") + + +def file_execute_xml_or_python_script(): + logging.debug("Execute XML or Python script") + + +def file_execute_python_script_with_options(): + logging.debug("Click execute Python script with options") + + +def file_open_current_file_in_editor(): + logging.debug("Click file open current in editor") + + +def file_print(): + logging.debug("Click file Print") + + +def file_save_screenshot(): + logging.debug("Click file save screenshot") + + +def file_example_link(): + logging.debug("Click file example link") + + +def edit_undo(): + logging.debug("Click edit undo") + + +def edit_undo_shortcut(event): + logging.debug("Shortcut for edit undo") + + +def edit_redo(): + logging.debug("Click edit redo") + + +def edit_redo_shortcut(event): + logging.debug("Shortcut for edit redo") + + +def edit_cut(): + logging.debug("Click edit cut") + + +def edit_cut_shortcut(event): + logging.debug("Shortcut for edit cut") + + +def edit_copy(): + logging.debug("Click edit copy") + + +def edit_copy_shortcut(event): + logging.debug("Shortcut for edit copy") + + +def edit_paste(): + logging.debug("Click edit paste") + + +def edit_paste_shortcut(event): + logging.debug("Shortcut for edit paste") + + +def edit_select_all(): + logging.debug("Click edit select all") + + +def edit_select_all_shortcut(event): + logging.debug("Shortcut for edit select all") + + +def edit_select_adjacent(): + logging.debug("Click edit select adjacent") + + +def edit_select_adjacent_shortcut(event): + logging.debug("Shortcut for edit select adjacent") + + +def edit_find(): + logging.debug("CLick edit find") + + +def edit_find_shortcut(event): + logging.debug("Shortcut for edit find") + + +def edit_clear_marker(): + logging.debug("Click edit clear marker") + + +def edit_preferences(): + logging.debug("Click preferences") + + +def canvas_new(): + logging.debug("Click canvas new") + + +def canvas_manage(): + logging.debug("Click canvas manage") + + +def canvas_delete(): + logging.debug("Click canvas delete") + + +def canvas_size_scale(): + logging.debug("Click canvas size/scale") + + +def canvas_wallpaper(): + logging.debug("CLick canvas wallpaper") + + +def canvas_previous(): + logging.debug("Click canvas previous") + + +def canvas_previous_shortcut(event): + logging.debug("Shortcut for canvas previous") + + +def canvas_next(): + logging.debug("Click canvas next") + + +def canvas_next_shortcut(event): + logging.debug("Shortcut for canvas next") + + +def canvas_first(): + logging.debug("CLick canvas first") + + +def canvas_first_shortcut(event): + logging.debug("Shortcut for canvas first") + + +def canvas_last(): + logging.debug("CLick canvas last") + + +def canvas_last_shortcut(event): + logging.debug("Shortcut canvas last") + + +def view_show(): + logging.debug("Click view show") + + +def view_show_hidden_nodes(): + logging.debug("Click view show hidden nodes") + + +def view_locked(): + logging.debug("Click view locked") + + +def view_3d_gui(): + logging.debug("CLick view 3D GUI") + + +def view_zoom_in(): + logging.debug("Click view zoom in") + + +def view_zoom_in_shortcut(event): + logging.debug("Shortcut view zoom in") + + +def view_zoom_out(): + logging.debug("Click view zoom out") + + +def view_zoom_out_shortcut(event): + logging.debug("Shortcut view zoom out") + + +def tools_auto_rearrange_all(): + logging.debug("Click tools, auto rearrange all") + + +def tools_auto_rearrange_selected(): + logging.debug("CLick tools auto rearrange selected") + + +def tools_align_to_grid(): + logging.debug("Click tools align to grid") + + +def tools_traffic(): + logging.debug("Click tools traffic") + + +def tools_ip_addresses(): + logging.debug("Click tools ip addresses") + + +def tools_mac_addresses(): + logging.debug("Click tools mac addresses") + + +def tools_build_hosts_file(): + logging.debug("Click tools build hosts file") + + +def tools_renumber_nodes(): + logging.debug("Click tools renumber nodes") + + +def tools_experimental(): + logging.debug("Click tools experimental") + + +def tools_topology_generator(): + logging.debug("Click tools topology generator") + + +def tools_debugger(): + logging.debug("Click tools debugger") + + +def widgets_observer_widgets(): + logging.debug("Click widgets observer widgets") + + +def widgets_adjacency(): + logging.debug("Click widgets adjacency") + + +def widgets_throughput(): + logging.debug("Click widgets throughput") + + +def widgets_configure_adjacency(): + logging.debug("Click widgets configure adjacency") + + +def widgets_configure_throughput(): + logging.debug("Click widgets configure throughput") + + +def session_start(): + logging.debug("Click session start") + + +def session_change_sessions(): + logging.debug("Click session change sessions") + + +def session_node_types(): + logging.debug("Click session node types") + + +def session_comments(): + logging.debug("Click session comments") + + +def session_hooks(): + logging.debug("Click session hooks") + + +def session_reset_node_positions(): + logging.debug("Click session reset node positions") + + +def session_emulation_servers(): + logging.debug("Click session emulation servers") + + +def session_options(): + logging.debug("Click session options") + + +def help_core_github(): + webbrowser.open_new("https://github.com/coreemu/core") + + +def help_core_documentation(): + webbrowser.open_new("http://coreemu.github.io/core/") + + +def help_about(): + logging.debug("Click help About") From 303e96cdd6c59ddf844bc2aadf29e10e9ac433b0 Mon Sep 17 00:00:00 2001 From: Huy Pham Date: Fri, 20 Sep 2019 10:45:35 -0700 Subject: [PATCH 014/462] finish up menubar --- coretk/coretk/app.py | 266 +++++++++++++++++++----------------- coretk/coretk/menuaction.py | 4 + 2 files changed, 146 insertions(+), 124 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 8e023cac..bd0fd680 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -32,7 +32,7 @@ class Application(tk.Frame): :return: nothing """ - file_menu = tk.Menu(self.master) + file_menu = tk.Menu(self.menubar) file_menu.add_command( label="New", command=action.file_new, accelerator="Ctrl+N" ) @@ -89,7 +89,7 @@ class Application(tk.Frame): :return: nothing """ - edit_menu = tk.Menu(self.master) + edit_menu = tk.Menu(self.menubar) edit_menu.add_command( label="Undo", command=action.edit_undo, accelerator="Ctrl+Z" ) @@ -136,7 +136,7 @@ class Application(tk.Frame): :return: nothing """ - canvas_menu = tk.Menu(self.master) + canvas_menu = tk.Menu(self.menubar) canvas_menu.add_command(label="New", command=action.canvas_new) canvas_menu.add_command(label="Manage...", command=action.canvas_manage) canvas_menu.add_command(label="Delete", command=action.canvas_delete) @@ -170,17 +170,17 @@ class Application(tk.Frame): :param tkinter.Menu view_menu: the view menu :return: nothing """ - show_menu = tk.Menu(self.master) - show_menu.add_command(label="All") - show_menu.add_command(label="None") + show_menu = tk.Menu(view_menu) + show_menu.add_command(label="All", command=action.sub_menu_items) + show_menu.add_command(label="None", command=action.sub_menu_items) show_menu.add_separator() - show_menu.add_command(label="Interface Names") - show_menu.add_command(label="IPv4 Addresses") - show_menu.add_command(label="IPv6 Addresses") - show_menu.add_command(label="Node Labels") - show_menu.add_command(label="Annotations") - show_menu.add_command(label="Grid") - show_menu.add_command(label="API Messages") + show_menu.add_command(label="Interface Names", command=action.sub_menu_items) + show_menu.add_command(label="IPv4 Addresses", command=action.sub_menu_items) + show_menu.add_command(label="IPv6 Addresses", command=action.sub_menu_items) + show_menu.add_command(label="Node Labels", command=action.sub_menu_items) + show_menu.add_command(label="Annotations", command=action.sub_menu_items) + show_menu.add_command(label="Grid", command=action.sub_menu_items) + show_menu.add_command(label="API Messages", command=action.sub_menu_items) view_menu.add_cascade(label="Show", menu=show_menu) @@ -190,7 +190,7 @@ class Application(tk.Frame): :return: nothing """ - view_menu = tk.Menu(self.master) + view_menu = tk.Menu(self.menubar, tearoff=True) self.create_show_menu(view_menu) view_menu.add_command( label="Show hidden nodes", command=action.view_show_hidden_nodes @@ -210,10 +210,20 @@ class Application(tk.Frame): self.menubar.add_cascade(label="View", menu=view_menu) def create_experimental_menu(self, tools_menu): - experimental_menu = tk.Menu(self.master) - experimental_menu.add_command(label="Plugins...") - experimental_menu.add_command(label="ns2immunes converter...") - experimental_menu.add_command(label="Topology partitioning...") + """ + Create experimental menu item and the sub menu items inside + + :param tkinter.Menu tools_menu: tools menu + :return: nothing + """ + experimental_menu = tk.Menu(tools_menu, tearoff=True) + experimental_menu.add_command(label="Plugins...", command=action.sub_menu_items) + experimental_menu.add_command( + label="ns2immunes converter...", command=action.sub_menu_items + ) + experimental_menu.add_command( + label="Topology partitioning...", command=action.sub_menu_items + ) tools_menu.add_cascade(label="Experimental", menu=experimental_menu) @@ -224,17 +234,12 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - random_menu = tk.Menu(self.master) - random_menu.add_command(label="R(1)") - random_menu.add_command(label="R(5)") - random_menu.add_command(label="R(10)") - random_menu.add_command(label="R(15)") - random_menu.add_command(label="R(20)") - random_menu.add_command(label="R(30)") - random_menu.add_command(label="R(40)") - random_menu.add_command(label="R(50)") - random_menu.add_command(label="R(75)") - random_menu.add_command(label="R(100)") + random_menu = tk.Menu(topology_generator_menu) + # list of number of random nodes to create + nums = [1, 5, 10, 15, 20, 30, 40, 50, 75, 100] + for i in nums: + the_label = "R(" + str(i) + ")" + random_menu.add_command(label=the_label, command=action.sub_menu_items) topology_generator_menu.add_cascade(label="Random", menu=random_menu) @@ -245,26 +250,17 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology_generator_menu :return: nothing """ - grid_menu = tk.Menu(self.master) - grid_menu.add_command(label="G(1)") - grid_menu.add_command(label="G(5)") - grid_menu.add_command(label="G(10)") - grid_menu.add_command(label="G(15)") - grid_menu.add_command(label="G(20)") - grid_menu.add_command(label="G(25)") - grid_menu.add_command(label="G(30)") - grid_menu.add_command(label="G(35)") - grid_menu.add_command(label="G(40)") - grid_menu.add_command(label="G(50)") - grid_menu.add_command(label="G(60)") - grid_menu.add_command(label="G(70)") - grid_menu.add_command(label="G(80)") - grid_menu.add_command(label="G(90)") - grid_menu.add_command(label="G(100)") + grid_menu = tk.Menu(topology_generator_menu) + + # list of number of nodes to create + nums = [1, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 100] + + for i in nums: + the_label = "G(" + str(i) + ")" + grid_menu.add_command(label=the_label, command=action.sub_menu_items) topology_generator_menu.add_cascade(label="Grid", menu=grid_menu) - # TODO do later def create_connected_grid_menu(self, topology_generator_menu): """ Create connected grid menu items and the sub menu items inside @@ -272,7 +268,15 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - return + grid_menu = tk.Menu(topology_generator_menu) + for i in range(1, 11, 1): + i_n_menu = tk.Menu(grid_menu) + for j in range(1, 11, 1): + i_j_label = str(i) + " X " + str(j) + i_n_menu.add_command(label=i_j_label, command=action.sub_menu_items) + i_n_label = str(i) + " X N" + grid_menu.add_cascade(label=i_n_label, menu=i_n_menu) + topology_generator_menu.add_cascade(label="Connected Grid", menu=grid_menu) def create_chain_menu(self, topology_generator_menu): """ @@ -281,33 +285,13 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - chain_menu = tk.Menu(self.master) - chain_menu.add_command(label="P(2)") - chain_menu.add_command(label="P(3)") - chain_menu.add_command(label="P(4)") - chain_menu.add_command(label="P(5)") - chain_menu.add_command(label="P(6)") - chain_menu.add_command(label="P(7)") - chain_menu.add_command(label="P(8)") - chain_menu.add_command(label="P(9)") - chain_menu.add_command(label="P(10)") - chain_menu.add_command(label="P(11)") - chain_menu.add_command(label="P(12)") - chain_menu.add_command(label="P(13)") - chain_menu.add_command(label="P(14)") - chain_menu.add_command(label="P(15)") - chain_menu.add_command(label="P(16)") - chain_menu.add_command(label="P(17)") - chain_menu.add_command(label="P(18)") - chain_menu.add_command(label="P(19)") - chain_menu.add_command(label="P(20)") - chain_menu.add_command(label="P(21)") - chain_menu.add_command(label="P(22)") - chain_menu.add_command(label="P(23)") - chain_menu.add_command(label="P(24)") - chain_menu.add_command(label="P(32)") - chain_menu.add_command(label="P(64)") - chain_menu.add_command(label="P(128)") + chain_menu = tk.Menu(topology_generator_menu) + # number of nodes to create + nums = list(range(2, 25, 1)) + [32, 64, 128] + for i in nums: + the_label = "P(" + str(i) + ")" + chain_menu.add_command(label=the_label, command=action.sub_menu_items) + topology_generator_menu.add_cascade(label="Chain", menu=chain_menu) def create_star_menu(self, topology_generator_menu): @@ -317,10 +301,10 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - star_menu = tk.Menu(self.master) + star_menu = tk.Menu(topology_generator_menu) for i in range(3, 26, 1): the_label = "C(" + str(i) + ")" - star_menu.add_command(label=the_label) + star_menu.add_command(label=the_label, command=action.sub_menu_items) topology_generator_menu.add_cascade(label="Star", menu=star_menu) @@ -331,10 +315,10 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - cycle_menu = tk.Menu(self.master) + cycle_menu = tk.Menu(topology_generator_menu) for i in range(3, 25, 1): the_label = "C(" + str(i) + ")" - cycle_menu.add_command(label=the_label) + cycle_menu.add_command(label=the_label, command=action.sub_menu_items) topology_generator_menu.add_cascade(label="Cycle", menu=cycle_menu) @@ -345,10 +329,10 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - wheel_menu = tk.Menu(self.master) + wheel_menu = tk.Menu(topology_generator_menu) for i in range(4, 26, 1): the_label = "W(" + str(i) + ")" - wheel_menu.add_command(label=the_label) + wheel_menu.add_command(label=the_label, command=action.sub_menu_items) topology_generator_menu.add_cascade(label="Wheel", menu=wheel_menu) @@ -359,10 +343,10 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - cube_menu = tk.Menu(self.master) + cube_menu = tk.Menu(topology_generator_menu) for i in range(2, 7, 1): the_label = "Q(" + str(i) + ")" - cube_menu.add_command(label=the_label) + cube_menu.add_command(label=the_label, command=action.sub_menu_items) topology_generator_menu.add_cascade(label="Cube", menu=cube_menu) @@ -373,14 +357,13 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - clique_menu = tk.Menu(self.master) + clique_menu = tk.Menu(topology_generator_menu) for i in range(3, 25, 1): the_label = "K(" + str(i) + ")" - clique_menu.add_command(label=the_label) + clique_menu.add_command(label=the_label, command=action.sub_menu_items) topology_generator_menu.add_cascade(label="Clique", menu=clique_menu) - # TODO do later def create_bipartite_menu(self, topology_generator_menu): """ Create bipartite menu item and the sub menu items inside @@ -388,8 +371,17 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology_generator_menu :return: nothing """ - # bipartite_menu = tk.Menu(self.master) - return + bipartite_menu = tk.Menu(topology_generator_menu) + temp = 24 + for i in range(1, 13, 1): + i_n_menu = tk.Menu(bipartite_menu) + for j in range(i, temp, 1): + i_j_label = "K(" + str(i) + " X " + str(j) + ")" + i_n_menu.add_command(label=i_j_label, command=action.sub_menu_items) + i_n_label = "K(" + str(i) + " X N)" + bipartite_menu.add_cascade(label=i_n_label, menu=i_n_menu) + temp = temp - 1 + topology_generator_menu.add_cascade(label="Bipartite", menu=bipartite_menu) def create_topology_generator_menu(self, tools_menu): """ @@ -399,25 +391,18 @@ class Application(tk.Frame): :return: nothing """ - topology_generator_menu = tk.Menu(self.master) - # topology_generator_menu.add_command(label="Random") + topology_generator_menu = tk.Menu(tools_menu, tearoff=True) + self.create_random_menu(topology_generator_menu) - # topology_generator_menu.add_command(label="Grid") self.create_grid_menu(topology_generator_menu) - topology_generator_menu.add_command(label="Connected Grid") + self.create_connected_grid_menu(topology_generator_menu) self.create_chain_menu(topology_generator_menu) - # topology_generator_menu.add_command(label="Chain") - # topology_generator_menu.add_command(label="Star") self.create_star_menu(topology_generator_menu) - # topology_generator_menu.add_command(label="Cycle") self.create_cycle_menu(topology_generator_menu) - # topology_generator_menu.add_command(label="Wheel") self.create_wheel_menu(topology_generator_menu) - # topology_generator_menu.add_command(label="Cube") self.create_cube_menu(topology_generator_menu) - # topology_generator_menu.add_command(label="Clique") self.create_clique_menu(topology_generator_menu) - topology_generator_menu.add_command(label="Bipartite") + self.create_bipartite_menu(topology_generator_menu) tools_menu.add_cascade(label="Topology generator", menu=topology_generator_menu) @@ -428,7 +413,7 @@ class Application(tk.Frame): :return: nothing """ - tools_menu = tk.Menu(self.master) + tools_menu = tk.Menu(self.menubar) tools_menu.add_command( label="Auto rearrange all", command=action.tools_auto_rearrange_all ) @@ -470,23 +455,51 @@ class Application(tk.Frame): :param tkinter.Menu widget_menu: widget_menu :return: nothing """ - observer_widget_menu = tk.Menu(self.master) - observer_widget_menu.add_command(label="None") - observer_widget_menu.add_command(label="processes") - observer_widget_menu.add_command(label="ifconfig") - observer_widget_menu.add_command(label="IPv4 routes") - observer_widget_menu.add_command(label="IPv6 routes") - observer_widget_menu.add_command(label="OSPFv2 neighbors") - observer_widget_menu.add_command(label="OSPFv3 neighbors") - observer_widget_menu.add_command(label="Listening sockets") - observer_widget_menu.add_command(label="IPv4 MFC entries") - observer_widget_menu.add_command(label="IPv6 MFC entries") - observer_widget_menu.add_command(label="firewall rules") - observer_widget_menu.add_command(label="IPsec policies") - observer_widget_menu.add_command(label="docker logs") - observer_widget_menu.add_command(label="OSPFv3 MDR level") - observer_widget_menu.add_command(label="PIM neighbors") - observer_widget_menu.add_command(label="Edit...") + observer_widget_menu = tk.Menu(widget_menu, tearoff=True) + observer_widget_menu.add_command(label="None", command=action.sub_menu_items) + observer_widget_menu.add_command( + label="processes", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="ifconfig", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPv4 routes", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPv6 routes", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="OSPFv2 neighbors", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="OSPFv3 neighbors", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="Listening sockets", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPv4 MFC entries", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPv6 MFC entries", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="firewall rules", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPsec policies", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="docker logs", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="OSPFv3 MDR level", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="PIM neighbors", command=action.sub_menu_items + ) + observer_widget_menu.add_command(label="Edit...", command=action.sub_menu_items) widget_menu.add_cascade(label="Observer Widgets", menu=observer_widget_menu) @@ -497,11 +510,11 @@ class Application(tk.Frame): :param tkinter.Menu widget_menu: widget menu :return: nothing """ - adjacency_menu = tk.Menu(self.master) - adjacency_menu.add_command(label="OSPFv2") - adjacency_menu.add_command(label="OSPFv3") - adjacency_menu.add_command(label="OSLR") - adjacency_menu.add_command(label="OSLRv2") + adjacency_menu = tk.Menu(widget_menu, tearoff=True) + adjacency_menu.add_command(label="OSPFv2", command=action.sub_menu_items) + adjacency_menu.add_command(label="OSPFv3", command=action.sub_menu_items) + adjacency_menu.add_command(label="OSLR", command=action.sub_menu_items) + adjacency_menu.add_command(label="OSLRv2", command=action.sub_menu_items) widget_menu.add_cascade(label="Adjacency", menu=adjacency_menu) @@ -511,7 +524,7 @@ class Application(tk.Frame): :return: nothing """ - widget_menu = tk.Menu(self.master) + widget_menu = tk.Menu(self.menubar, tearoff=True) self.create_observer_widgets_menu(widget_menu) self.create_adjacency_menu(widget_menu) widget_menu.add_command(label="Throughput", command=action.widgets_throughput) @@ -533,7 +546,7 @@ class Application(tk.Frame): :return: nothing """ - session_menu = tk.Menu(self.master) + session_menu = tk.Menu(self.menubar, tearoff=True) session_menu.add_command(label="Start", command=action.session_start) session_menu.add_command( label="Change sessions...", command=action.session_change_sessions @@ -562,7 +575,7 @@ class Application(tk.Frame): :return: nothing """ - help_menu = tk.Menu(self.master) + help_menu = tk.Menu(self.menubar) help_menu.add_command( label="Core Github (www)", command=action.help_core_github ) @@ -574,6 +587,11 @@ class Application(tk.Frame): self.menubar.add_cascade(label="Help", menu=help_menu) def bind_menubar_shortcut(self): + """ + Bind hot keys to matching command + + :return: nothing + """ self.bind_all("", action.file_new_shortcut) self.bind_all("", action.file_open_shortcut) self.bind_all("", action.file_save_shortcut) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 005776f9..2e1427da 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -6,6 +6,10 @@ import logging import webbrowser +def sub_menu_items(): + logging.debug("Click on sub menu items") + + def file_new(): logging.debug("Click file New") From aa1fb621829b7999b57eb40d4596a0319906546b Mon Sep 17 00:00:00 2001 From: Huy Pham Date: Fri, 20 Sep 2019 11:58:15 -0700 Subject: [PATCH 015/462] start working on sidebar --- coretk/coretk/app.py | 54 +++++++++++++++++++++++++++++++----- coretk/coretk/lanswitch.gif | Bin 0 -> 744 bytes coretk/coretk/link.gif | Bin 0 -> 86 bytes coretk/coretk/marker.gif | Bin 0 -> 375 bytes coretk/coretk/router.gif | Bin 0 -> 1152 bytes coretk/coretk/select.gif | Bin 0 -> 925 bytes coretk/coretk/start.gif | Bin 0 -> 1131 bytes 7 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 coretk/coretk/lanswitch.gif create mode 100644 coretk/coretk/link.gif create mode 100644 coretk/coretk/marker.gif create mode 100644 coretk/coretk/router.gif create mode 100644 coretk/coretk/select.gif create mode 100644 coretk/coretk/start.gif diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 163c8a17..22d927e6 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -15,8 +15,14 @@ class Application(tk.Frame): self.create_widgets() def load_images(self): - Images.load("switch", "switch.png") + # Images.load("switch", "switch.png") Images.load("core", "core-icon.png") + Images.load("start", "start.gif") + Images.load("switch", "lanswitch.gif") + Images.load("marker", "marker.gif") + Images.load("router", "router.gif") + Images.load("select", "select.gif") + Images.load("link", "link.gif") def setup_app(self): self.master.title("CORE") @@ -37,28 +43,62 @@ class Application(tk.Frame): self.master.config(menu=self.menubar) def create_widgets(self): - image = Images.get("switch") + select_image = Images.get("select") + start_image = Images.get("start") + link_image = Images.get("link") + router_image = Images.get("router") + switch_image = Images.get("switch") + marker_image = Images.get("marker") + edit_frame = tk.Frame(self) edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) radio_value = tk.IntVar() b = tk.Radiobutton( - edit_frame, indicatoron=False, variable=radio_value, value=1, image=image + edit_frame, + indicatoron=False, + variable=radio_value, + value=1, + image=select_image, ) b.pack(side=tk.TOP, pady=1) b = tk.Radiobutton( - edit_frame, indicatoron=False, variable=radio_value, value=2, image=image + edit_frame, + indicatoron=False, + variable=radio_value, + value=2, + image=start_image, ) b.pack(side=tk.TOP, pady=1) b = tk.Radiobutton( - edit_frame, indicatoron=False, variable=radio_value, value=3, image=image + edit_frame, + indicatoron=False, + variable=radio_value, + value=3, + image=link_image, ) b.pack(side=tk.TOP, pady=1) b = tk.Radiobutton( - edit_frame, indicatoron=False, variable=radio_value, value=4, image=image + edit_frame, + indicatoron=False, + variable=radio_value, + value=4, + image=router_image, ) b.pack(side=tk.TOP, pady=1) b = tk.Radiobutton( - edit_frame, indicatoron=False, variable=radio_value, value=5, image=image + edit_frame, + indicatoron=False, + variable=radio_value, + value=5, + image=switch_image, + ) + b.pack(side=tk.TOP, pady=1) + b = tk.Radiobutton( + edit_frame, + indicatoron=False, + variable=radio_value, + value=6, + image=marker_image, ) b.pack(side=tk.TOP, pady=1) diff --git a/coretk/coretk/lanswitch.gif b/coretk/coretk/lanswitch.gif new file mode 100644 index 0000000000000000000000000000000000000000..38fcbb2ea684f2cb3724f55bb668d66dd952ee4d GIT binary patch literal 744 zcmVJ9wZvc%wRbpgMV>J9(izdZ9gfp+0+}KYOA*e5OBqqdk75L4Bk` zeWOBtq(FhHMS!M3gR4h^r$~aQNrI?Dg|0+}u0w{dOogdTg{n`7s!xckMvAgji>+0Q zu1SuzRgA7lkGD#Xw@Q$=SB#%r9qZk@Ytp1W|LyjZ2mSf$H#qQ6_I&0DF?cci~~ zrNDcq!F#8|eW=5Ks>Fe;#fPuQh_T6uvdNFN%aggzn!VDVz|)|@)uF=GrpDN($JnUI z*{jOhtjpW3%-prm-?-G_xzypg)Z)9<;=R}7%*@Qp%*@Qp%*@QpA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=dv*jE#jucwYQHYIwy=OOkz4Hj~p>W&4i3Z zU|?EVaBDeOZhEkLdR2@d7~9=~8y-G!bar@qcWq-WUF1-iLJ&x!2p8np`UC026(iCh zJzDS}A-9DYGIZ!5v5|<6A4!l-8|AaEc70|*i*d`R)429F;{oItU%<%@ul z7Q3Zf+45z~nKf_b+}ZP|Np6FNvUCYkCQX|-b@KEHbQ>Y5MXMskx^$~ki(gevHEK0! zR;ET=O%?)6_2^ZlTAP-%V2Cc;v3TMBba2S8*t>7d8bD;&uG_Lr69G_J06~HV5GGUz aLDL40AWED>vBKp`qSL5Tt6ohg5CA)88DCBS literal 0 HcmV?d00001 diff --git a/coretk/coretk/link.gif b/coretk/coretk/link.gif new file mode 100644 index 0000000000000000000000000000000000000000..55532ecf0d14eecb81f57e71ae8f90cd9c8f2fea GIT binary patch literal 86 zcmZ?wbhEHblwgox_`m=Kia%Kx85kHDbU=KN3M?hZsq{JzSM|!8CHm iM7a#L1g zw35rPkmRJB!>gvuyrA2_kkrJV)Xlf=-J;{txy;PW+SI|;*4E(H*5%&Z_wUx>;o<-P z|NsC0A^8LW002J#ECc`q02lxm000J*z@KnPED`}ofN^OAs3wpIl1UYa7>b8-NL-kM<*DutFxg;BP1>~th8?)DLBExWd|U~ Vk^lwFilNX&NxIh8*xA-W06UFctKt9v literal 0 HcmV?d00001 diff --git a/coretk/coretk/router.gif b/coretk/coretk/router.gif new file mode 100644 index 0000000000000000000000000000000000000000..eaf145ebc86c1b81600571705902866550da416d GIT binary patch literal 1152 zcmZ?wbhEHblxGlSc;3Sx<6SA|U!xFIuNd5*6w;s++Nc!Tq#D_(8r7;6)1e;Ip#enk z-J0>;I>~*ysT1_lCK_Z;G0L58oIA}pZ@NkT43mOcrUkRiiszUY&owWZYf(DavUI*h z*+R?m`IhAit;!czR?M@im~U0Fz^Y=QHIS@aU=2bGZ7LU9RV}gxp~W^;3vH_wT30W! zt6peVy~w_Lk!{UV+uCJzwM!jq7dzH2v8!9|ShvKkewjnVN{2=uTIJfX%&}>eN7Hi8 zrWKAYYn)ovcr-8fY+m8gvevt0rAzzzz>YO;-J3mnw*+^u4e4I%(YH0ccfI$-ol*T8 zqbF>PnXt)s%5LAOK(r@r;%5Kpd;MqZOPsPLY1-DHSqD<4Z4I1#D0RlR;JHWAXKoLk zcO+!q(X81!vuE$jnX@x&(ea3-r;8WvjaqRodd1n&Mf*w@?=N4nzjE2Zs^tf(mmjKK zakzff;fB>mTGk!w*m$yU>zT57yZQXRtrza^xcp$xwa0s}KRz=GXchvBKUo;L82&TpFaQB4 zPcU%&WBAW07$KqoEo1|3* z!_CHs4~gB{Wjwl{j8FI;7LI4q)tb`Ld9+cY`_9GB?E#YVRbre*mzCJu8YN_JWf(8# z>@kmBq{2Dr(V_#q;`4JRHfIFNX)2%6h}xoYVahuGKn|4$2Or68>$+sav-qg5yleZT z1F{EQ6&e^t<(@SNFFV=0!e!fzj1|m1QimB?R2B#*X}O8mubAUl{8PT+3>%+Lg#jZo zyP&xDo)w8HoMQUPZqw~_6}C39@hP}WC~!Q?E^1uH;kf7sr-*iB!Ruppejj-M;IMAW zj{{9^{fcMi#yL;-o3$(d!6DU*=kH`|K0m)$Jl(wamesE}w|Cd`^Utq&S^qV5`n{@O zJJsWA7O!;-C}nzd{9fJrUiJBotO^$vRQ=p~{C-7Jz!!ez8;L&_GzNq`RCKykx`Bz$ LhNm#efx#L8|Dlf2 literal 0 HcmV?d00001 diff --git a/coretk/coretk/select.gif b/coretk/coretk/select.gif new file mode 100644 index 0000000000000000000000000000000000000000..bb7e128c878317f6287564514a302e5e3c40c10e GIT binary patch literal 925 zcmX|AT}TvB6#nkcsH?a+J25!wWJw!YSVoy{g=r6ESt%99vPu|jWwavpvRe--w$f%s zYflbuf*4Z;~=R4<~Ip225ShbBKwQc6#Xwq9Tr2mq=851@csnR&8f+ycq>V-cDai3y~@!gr8wL0nBC zU{r)Hb5*5f7u61mZ3t|yIUb`VBe1-QqVleY#wih00Goly!xkwyVZ{1lB8I%Sj2 z2m-M!HhYQ+$jnGNeiv{GnB~nh`yh{T`At9|X4{_DxaFbKW*UGN8*$vq~F1bXlQwoK`C%h5?z2 z^9{6>N|K@L0aob!9DW(7DB*YfpQyG!K!#mAI~x7?(4Lbn6<4Dx zBsmXiOOfS?u)5v0FmUO{muJG>%YKj3_qk`!#gV43zkL%=4mf&UnMa1E`pa7HX6 zwqJ$`n~{uVTRP)1JIzL7vZz_If|{*~lS3(L>1YMNREp5@O@UgWlR;}sSO0_k0ekZL z_4(yVp4S^+tEp{9fME3$6#a%3d6h+8OxB~4_u{fkYWYPR?;%tdh>aI$;&(>#2d(q5 zq|ZS55Pb#FeTca%FjoY_pJqd!1@)Oxp9Ml1`7i-t4cyYgh!#e*aJztjI|SS%;9h|a zB09LQQ-@ea61aXdeO(}jc{0Q^LuMvumWN6CNC71urC^MNC<)^vJSd=Gf`Um(ai38{ z8O1oOm|(THknXlgcgI9TOhi;5A6OtpL5zkt4O28s(=ellSv}0@;UNQ$7?@{Zp@4-& z77{Ex<{-(z6Aqg*V)HyUZ^9BLSmGdMfWHe6JVmgA;2$Hb8db|?STjO~hb+(iC2}E= zy;n>}t!$*2i`p!Ao&2Q35-AghO2uI5=DfEu#zz=H@$_Mb?{Rq z{9+lOEYmC(YgRUCGIq4&M5(gkhc7$gRgRhMj=5@EvbrQ*Q!-U!TdHnN9cfQ>p3I%` zKE32ym!#DJX>CAC`=yLudghn1gHmqL|199o2K>39h3w)&E|JY=*YoT7d>;PKe_lXL z%Qpa=2Iup}^G|?k9cpo5kGH%3s7hbT?CS`gs@|e@IvxFC@zo~jk+^@))dtz&Y*T;l z?A{-~bETvTcUU_8qRZQ!rUz#@r|W!B>HK04c84jYlF?$DP{j?^i+0ZQ2+ZIX4SO-=6I%AGsiKi6a3?s^ Date: Fri, 20 Sep 2019 13:33:42 -0700 Subject: [PATCH 016/462] finish up menubar --- coretk/coretk/app.py | 131 +++++++++++++++++++++++++++---------------- 1 file changed, 82 insertions(+), 49 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index bd0fd680..f1593662 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -34,14 +34,14 @@ class Application(tk.Frame): """ file_menu = tk.Menu(self.menubar) file_menu.add_command( - label="New", command=action.file_new, accelerator="Ctrl+N" + label="New", command=action.file_new, accelerator="Ctrl+N", underline=0 ) file_menu.add_command( - label="Open...", command=action.file_open, accelerator="Ctrl+O" + label="Open...", command=action.file_open, accelerator="Ctrl+O", underline=0 ) - file_menu.add_command(label="Reload", command=action.file_reload) + file_menu.add_command(label="Reload", command=action.file_reload, underline=0) file_menu.add_command( - label="Save", command=action.file_save, accelerator="Ctrl+S" + label="Save", command=action.file_save, accelerator="Ctrl+S", underline=0 ) file_menu.add_command(label="Save As XML...", command=action.file_save_as_xml) file_menu.add_command(label="Save As imn...", command=action.file_save_as_imn) @@ -66,7 +66,7 @@ class Application(tk.Frame): label="Open current file in editor...", command=action.file_open_current_file_in_editor, ) - file_menu.add_command(label="Print...", command=action.file_print) + file_menu.add_command(label="Print...", command=action.file_print, underline=0) file_menu.add_command( label="Save screenshot...", command=action.file_save_screenshot ) @@ -80,8 +80,8 @@ class Application(tk.Frame): file_menu.add_separator() - file_menu.add_command(label="Quit", command=self.master.quit) - self.menubar.add_cascade(label="File", menu=file_menu) + file_menu.add_command(label="Quit", command=self.master.quit, underline=0) + self.menubar.add_cascade(label="File", menu=file_menu, underline=0) def create_edit_menu(self): """ @@ -91,22 +91,22 @@ class Application(tk.Frame): """ edit_menu = tk.Menu(self.menubar) edit_menu.add_command( - label="Undo", command=action.edit_undo, accelerator="Ctrl+Z" + label="Undo", command=action.edit_undo, accelerator="Ctrl+Z", underline=0 ) edit_menu.add_command( - label="Redo", command=action.edit_redo, accelerator="Ctrl+Y" + label="Redo", command=action.edit_redo, accelerator="Ctrl+Y", underline=0 ) edit_menu.add_separator() edit_menu.add_command( - label="Cut", command=action.edit_cut, accelerator="Ctrl+X" + label="Cut", command=action.edit_cut, accelerator="Ctrl+X", underline=0 ) edit_menu.add_command( - label="Copy", command=action.edit_copy, accelerator="Ctrl+C" + label="Copy", command=action.edit_copy, accelerator="Ctrl+C", underline=0 ) edit_menu.add_command( - label="Paste", command=action.edit_paste, accelerator="Ctrl+V" + label="Paste", command=action.edit_paste, accelerator="Ctrl+V", underline=0 ) edit_menu.add_separator() @@ -123,12 +123,12 @@ class Application(tk.Frame): edit_menu.add_separator() edit_menu.add_command( - label="Find...", command=action.edit_find, accelerator="Ctrl+F" + label="Find...", command=action.edit_find, accelerator="Ctrl+F", underline=0 ) edit_menu.add_command(label="Clear marker", command=action.edit_clear_marker) edit_menu.add_command(label="Preferences...", command=action.edit_preferences) - self.menubar.add_cascade(label="Edit", menu=edit_menu) + self.menubar.add_cascade(label="Edit", menu=edit_menu, underline=0) def create_canvas_menu(self): """ @@ -161,7 +161,7 @@ class Application(tk.Frame): label="Last", command=action.canvas_last, accelerator="End" ) - self.menubar.add_cascade(label="Canvas", menu=canvas_menu) + self.menubar.add_cascade(label="Canvas", menu=canvas_menu, underline=0) def create_show_menu(self, view_menu): """ @@ -207,7 +207,7 @@ class Application(tk.Frame): label="Zoom out", command=action.view_zoom_out, accelerator="-" ) - self.menubar.add_cascade(label="View", menu=view_menu) + self.menubar.add_cascade(label="View", menu=view_menu, underline=0) def create_experimental_menu(self, tools_menu): """ @@ -217,15 +217,19 @@ class Application(tk.Frame): :return: nothing """ experimental_menu = tk.Menu(tools_menu, tearoff=True) - experimental_menu.add_command(label="Plugins...", command=action.sub_menu_items) experimental_menu.add_command( - label="ns2immunes converter...", command=action.sub_menu_items + label="Plugins...", command=action.sub_menu_items, underline=0 + ) + experimental_menu.add_command( + label="ns2immunes converter...", command=action.sub_menu_items, underline=0 ) experimental_menu.add_command( label="Topology partitioning...", command=action.sub_menu_items ) - tools_menu.add_cascade(label="Experimental", menu=experimental_menu) + tools_menu.add_cascade( + label="Experimental", menu=experimental_menu, underline=0 + ) def create_random_menu(self, topology_generator_menu): """ @@ -241,7 +245,9 @@ class Application(tk.Frame): the_label = "R(" + str(i) + ")" random_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Random", menu=random_menu) + topology_generator_menu.add_cascade( + label="Random", menu=random_menu, underline=0 + ) def create_grid_menu(self, topology_generator_menu): """ @@ -259,7 +265,7 @@ class Application(tk.Frame): the_label = "G(" + str(i) + ")" grid_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Grid", menu=grid_menu) + topology_generator_menu.add_cascade(label="Grid", menu=grid_menu, underline=0) def create_connected_grid_menu(self, topology_generator_menu): """ @@ -276,7 +282,9 @@ class Application(tk.Frame): i_n_menu.add_command(label=i_j_label, command=action.sub_menu_items) i_n_label = str(i) + " X N" grid_menu.add_cascade(label=i_n_label, menu=i_n_menu) - topology_generator_menu.add_cascade(label="Connected Grid", menu=grid_menu) + topology_generator_menu.add_cascade( + label="Connected Grid", menu=grid_menu, underline=0 + ) def create_chain_menu(self, topology_generator_menu): """ @@ -292,7 +300,7 @@ class Application(tk.Frame): the_label = "P(" + str(i) + ")" chain_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Chain", menu=chain_menu) + topology_generator_menu.add_cascade(label="Chain", menu=chain_menu, underline=0) def create_star_menu(self, topology_generator_menu): """ @@ -306,7 +314,7 @@ class Application(tk.Frame): the_label = "C(" + str(i) + ")" star_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Star", menu=star_menu) + topology_generator_menu.add_cascade(label="Star", menu=star_menu, underline=0) def create_cycle_menu(self, topology_generator_menu): """ @@ -320,7 +328,7 @@ class Application(tk.Frame): the_label = "C(" + str(i) + ")" cycle_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Cycle", menu=cycle_menu) + topology_generator_menu.add_cascade(label="Cycle", menu=cycle_menu, underline=0) def create_wheel_menu(self, topology_generator_menu): """ @@ -334,7 +342,7 @@ class Application(tk.Frame): the_label = "W(" + str(i) + ")" wheel_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Wheel", menu=wheel_menu) + topology_generator_menu.add_cascade(label="Wheel", menu=wheel_menu, underline=0) def create_cube_menu(self, topology_generator_menu): """ @@ -348,7 +356,7 @@ class Application(tk.Frame): the_label = "Q(" + str(i) + ")" cube_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Cube", menu=cube_menu) + topology_generator_menu.add_cascade(label="Cube", menu=cube_menu, underline=0) def create_clique_menu(self, topology_generator_menu): """ @@ -362,7 +370,9 @@ class Application(tk.Frame): the_label = "K(" + str(i) + ")" clique_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Clique", menu=clique_menu) + topology_generator_menu.add_cascade( + label="Clique", menu=clique_menu, underline=0 + ) def create_bipartite_menu(self, topology_generator_menu): """ @@ -381,7 +391,9 @@ class Application(tk.Frame): i_n_label = "K(" + str(i) + " X N)" bipartite_menu.add_cascade(label=i_n_label, menu=i_n_menu) temp = temp - 1 - topology_generator_menu.add_cascade(label="Bipartite", menu=bipartite_menu) + topology_generator_menu.add_cascade( + label="Bipartite", menu=bipartite_menu, underline=0 + ) def create_topology_generator_menu(self, tools_menu): """ @@ -404,7 +416,9 @@ class Application(tk.Frame): self.create_clique_menu(topology_generator_menu) self.create_bipartite_menu(topology_generator_menu) - tools_menu.add_cascade(label="Topology generator", menu=topology_generator_menu) + tools_menu.add_cascade( + label="Topology generator", menu=topology_generator_menu, underline=0 + ) def create_tools_menu(self): """ @@ -415,38 +429,43 @@ class Application(tk.Frame): tools_menu = tk.Menu(self.menubar) tools_menu.add_command( - label="Auto rearrange all", command=action.tools_auto_rearrange_all + label="Auto rearrange all", + command=action.tools_auto_rearrange_all, + underline=0, ) tools_menu.add_command( label="Auto rearrange selected", command=action.tools_auto_rearrange_selected, + underline=0, ) tools_menu.add_separator() tools_menu.add_command( - label="Align to grid", command=action.tools_align_to_grid + label="Align to grid", command=action.tools_align_to_grid, underline=0 ) tools_menu.add_separator() tools_menu.add_command(label="Traffic...", command=action.tools_traffic) tools_menu.add_command( - label="IP addresses...", command=action.tools_ip_addresses + label="IP addresses...", command=action.tools_ip_addresses, underline=0 ) tools_menu.add_command( - label="MAC addresses...", command=action.tools_mac_addresses + label="MAC addresses...", command=action.tools_mac_addresses, underline=0 ) tools_menu.add_command( - label="Build hosts file...", command=action.tools_build_hosts_file + label="Build hosts file...", + command=action.tools_build_hosts_file, + underline=0, ) tools_menu.add_command( - label="Renumber nodes...", command=action.tools_renumber_nodes + label="Renumber nodes...", command=action.tools_renumber_nodes, underline=0 ) self.create_experimental_menu(tools_menu) self.create_topology_generator_menu(tools_menu) tools_menu.add_command(label="Debugger...", command=action.tools_debugger) - self.menubar.add_cascade(label="Tools", menu=tools_menu) + self.menubar.add_cascade(label="Tools", menu=tools_menu, underline=0) def create_observer_widgets_menu(self, widget_menu): """ @@ -538,7 +557,7 @@ class Application(tk.Frame): label="Configure Throughput...", command=action.widgets_configure_throughput ) - self.menubar.add_cascade(label="Widgets", menu=widget_menu) + self.menubar.add_cascade(label="Widgets", menu=widget_menu, underline=0) def create_session_menu(self): """ @@ -547,27 +566,41 @@ class Application(tk.Frame): :return: nothing """ session_menu = tk.Menu(self.menubar, tearoff=True) - session_menu.add_command(label="Start", command=action.session_start) session_menu.add_command( - label="Change sessions...", command=action.session_change_sessions + label="Start", command=action.session_start, underline=0 + ) + session_menu.add_command( + label="Change sessions...", + command=action.session_change_sessions, + underline=0, ) session_menu.add_separator() session_menu.add_command( - label="Node types...", command=action.session_node_types - ) - session_menu.add_command(label="Comments...", command=action.session_comments) - session_menu.add_command(label="Hooks...", command=action.session_hooks) - session_menu.add_command( - label="Reset node positions", command=action.session_reset_node_positions + label="Node types...", command=action.session_node_types, underline=0 ) session_menu.add_command( - label="Emulation servers...", command=action.session_emulation_servers + label="Comments...", command=action.session_comments, underline=0 + ) + session_menu.add_command( + label="Hooks...", command=action.session_hooks, underline=0 + ) + session_menu.add_command( + label="Reset node positions", + command=action.session_reset_node_positions, + underline=0, + ) + session_menu.add_command( + label="Emulation servers...", + command=action.session_emulation_servers, + underline=0, + ) + session_menu.add_command( + label="Options...", command=action.session_options, underline=0 ) - session_menu.add_command(label="Options...", command=action.session_options) - self.menubar.add_cascade(label="Session", menu=session_menu) + self.menubar.add_cascade(label="Session", menu=session_menu, underline=0) def create_help_menu(self): """ From 1ebe33cabbb323bf16f5ca5fdfc92f4a7cffdfa9 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 20 Sep 2019 13:51:01 -0700 Subject: [PATCH 017/462] get rid all the dash lines in menubar --- coretk/coretk/app.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index f1593662..01d79d17 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -190,7 +190,7 @@ class Application(tk.Frame): :return: nothing """ - view_menu = tk.Menu(self.menubar, tearoff=True) + view_menu = tk.Menu(self.menubar) self.create_show_menu(view_menu) view_menu.add_command( label="Show hidden nodes", command=action.view_show_hidden_nodes @@ -216,7 +216,7 @@ class Application(tk.Frame): :param tkinter.Menu tools_menu: tools menu :return: nothing """ - experimental_menu = tk.Menu(tools_menu, tearoff=True) + experimental_menu = tk.Menu(tools_menu) experimental_menu.add_command( label="Plugins...", command=action.sub_menu_items, underline=0 ) @@ -403,7 +403,7 @@ class Application(tk.Frame): :return: nothing """ - topology_generator_menu = tk.Menu(tools_menu, tearoff=True) + topology_generator_menu = tk.Menu(tools_menu) self.create_random_menu(topology_generator_menu) self.create_grid_menu(topology_generator_menu) @@ -474,7 +474,7 @@ class Application(tk.Frame): :param tkinter.Menu widget_menu: widget_menu :return: nothing """ - observer_widget_menu = tk.Menu(widget_menu, tearoff=True) + observer_widget_menu = tk.Menu(widget_menu) observer_widget_menu.add_command(label="None", command=action.sub_menu_items) observer_widget_menu.add_command( label="processes", command=action.sub_menu_items @@ -529,7 +529,7 @@ class Application(tk.Frame): :param tkinter.Menu widget_menu: widget menu :return: nothing """ - adjacency_menu = tk.Menu(widget_menu, tearoff=True) + adjacency_menu = tk.Menu(widget_menu) adjacency_menu.add_command(label="OSPFv2", command=action.sub_menu_items) adjacency_menu.add_command(label="OSPFv3", command=action.sub_menu_items) adjacency_menu.add_command(label="OSLR", command=action.sub_menu_items) @@ -543,7 +543,7 @@ class Application(tk.Frame): :return: nothing """ - widget_menu = tk.Menu(self.menubar, tearoff=True) + widget_menu = tk.Menu(self.menubar) self.create_observer_widgets_menu(widget_menu) self.create_adjacency_menu(widget_menu) widget_menu.add_command(label="Throughput", command=action.widgets_throughput) @@ -565,7 +565,7 @@ class Application(tk.Frame): :return: nothing """ - session_menu = tk.Menu(self.menubar, tearoff=True) + session_menu = tk.Menu(self.menubar) session_menu.add_command( label="Start", command=action.session_start, underline=0 ) From f1698de74b9f19c0d475b98b9a8c51c3c030ae14 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 23 Sep 2019 11:43:13 -0700 Subject: [PATCH 018/462] toolbar --- coretk/coretk/app.py | 43 ++++++++++++++++++++++++++++++++++++++- coretk/coretk/hub.gif | Bin 0 -> 719 bytes coretk/coretk/tooltip.py | 0 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 coretk/coretk/hub.gif create mode 100644 coretk/coretk/tooltip.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 22d927e6..5ed74956 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -23,6 +23,7 @@ class Application(tk.Frame): Images.load("router", "router.gif") Images.load("select", "select.gif") Images.load("link", "link.gif") + Images.load("hub", "hub.gif") def setup_app(self): self.master.title("CORE") @@ -42,11 +43,35 @@ class Application(tk.Frame): self.menubar.add_cascade(label="Help", menu=help_menu) self.master.config(menu=self.menubar) + def create_network_layer_node( + self, edit_frame, radio_value, hub_image, switch_image + ): + menu_button = tk.Menubutton( + edit_frame, + direction=tk.RIGHT, + image=hub_image, + width=32, + height=32, + relief=tk.RAISED, + ) + # menu_button.grid() + menu_button.menu = tk.Menu(menu_button) + menu_button["menu"] = menu_button.menu + + menu_button.menu.add_radiobutton( + image=hub_image, variable=radio_value, value=7, indicatoron=False + ) + menu_button.menu.add_radiobutton( + image=switch_image, variable=radio_value, value=8, indicatoron=False + ) + menu_button.pack(side=tk.TOP, pady=1) + def create_widgets(self): select_image = Images.get("select") start_image = Images.get("start") link_image = Images.get("link") router_image = Images.get("router") + hub_image = Images.get("hub") switch_image = Images.get("switch") marker_image = Images.get("marker") @@ -58,6 +83,8 @@ class Application(tk.Frame): indicatoron=False, variable=radio_value, value=1, + width=32, + height=32, image=select_image, ) b.pack(side=tk.TOP, pady=1) @@ -66,6 +93,8 @@ class Application(tk.Frame): indicatoron=False, variable=radio_value, value=2, + width=32, + height=32, image=start_image, ) b.pack(side=tk.TOP, pady=1) @@ -74,6 +103,8 @@ class Application(tk.Frame): indicatoron=False, variable=radio_value, value=3, + width=32, + height=32, image=link_image, ) b.pack(side=tk.TOP, pady=1) @@ -82,15 +113,21 @@ class Application(tk.Frame): indicatoron=False, variable=radio_value, value=4, + width=32, + height=32, image=router_image, ) + b.pack(side=tk.TOP, pady=1) + b = tk.Radiobutton( edit_frame, indicatoron=False, variable=radio_value, value=5, - image=switch_image, + width=32, + height=32, + image=hub_image, ) b.pack(side=tk.TOP, pady=1) b = tk.Radiobutton( @@ -98,10 +135,14 @@ class Application(tk.Frame): indicatoron=False, variable=radio_value, value=6, + width=32, + height=32, image=marker_image, ) b.pack(side=tk.TOP, pady=1) + self.create_network_layer_node(edit_frame, radio_value, hub_image, switch_image) + self.canvas = CanvasGraph( self, background="#cccccc", scrollregion=(0, 0, 1000, 1000) ) diff --git a/coretk/coretk/hub.gif b/coretk/coretk/hub.gif new file mode 100644 index 0000000000000000000000000000000000000000..17f7c4d3ef726f744da685423057366093df33f8 GIT binary patch literal 719 zcmZ?wbhEHbRA3NeIF`X6<6R-+T`3<>s}$0x8qum2(W)NPsSyiAUD`>#I?27dDHHY5 zChDb6G|ZS{kU7OLYpPMsG?V;UrunnXi|3jb&$BFDXi>JnvTT7>*?jBr1(p@_Eh`pS zRm`^nk&CQB{tt%H;S1z=vTxeUh(6(loea&Lqx}}bFOPuPKIM)NwQisM> zt_{l^n^$`_t#EE#=hC{)xow?q+bXxNO@WAtOJ`nH{!xc%(pooDClzr6I=^<~Fy zEI)o@`=tlFu0Gm*?eU&#kM~}Ge1=Lu@h1x-7ehUR4g(N?;)H?yUqgLUb4zPmdq<0u zTu*Obf4`D_=L9(`C!;~HitzeCPr?FNJxqa zbCy%KUAIBbJ2m~}sncho)np9qjxL(X>fF`|N{-XJqoblAaq)7Kbe~IXuu89G zY>4^!<#n%FX}%(x=oD7Zx(x^D7Cd=>$48HrD@OVHGfFIMB$#qu{aN1LM(d zN%JxW&7zjh$r?;@OmoaOK04YXZC%G>x#`KtDFRD*voZx2yYdE7cm|vGV*3$4sHUKQA;Xt`D2tb#>OIgNj>peGaF!W-$oH tY>1eD&xfTUiIIg<%3(%;VvD Date: Wed, 25 Sep 2019 08:29:34 -0700 Subject: [PATCH 019/462] Create a class for menubar and start working on toolbar --- coretk/coretk/app.py | 710 ++++------------------------------- coretk/coretk/coremenubar.py | 654 ++++++++++++++++++++++++++++++++ coretk/coretk/coretoolbar.py | 421 +++++++++++++++++++++ coretk/coretk/oval.gif | Bin 0 -> 174 bytes coretk/coretk/rectangle.gif | Bin 0 -> 160 bytes coretk/coretk/rj45.gif | Bin 0 -> 755 bytes coretk/coretk/text.gif | Bin 0 -> 127 bytes coretk/coretk/tunnel.gif | Bin 0 -> 799 bytes coretk/coretk/wlan.gif | Bin 0 -> 146 bytes 9 files changed, 1153 insertions(+), 632 deletions(-) create mode 100644 coretk/coretk/coremenubar.py create mode 100644 coretk/coretk/coretoolbar.py create mode 100644 coretk/coretk/oval.gif create mode 100644 coretk/coretk/rectangle.gif create mode 100644 coretk/coretk/rj45.gif create mode 100644 coretk/coretk/text.gif create mode 100644 coretk/coretk/tunnel.gif create mode 100644 coretk/coretk/wlan.gif diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index e0eb138e..37e51aff 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,7 +1,8 @@ import logging import tkinter as tk -import coretk.menuaction as action +from coretk.coremenubar import CoreMenubar +from coretk.coretoolbar import CoreToolbar from coretk.graph import CanvasGraph from coretk.images import Images @@ -33,638 +34,14 @@ class Application(tk.Frame): self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) - def create_file_menu(self): - """ - Create file menu - - :return: nothing - """ - file_menu = tk.Menu(self.menubar) - file_menu.add_command( - label="New", command=action.file_new, accelerator="Ctrl+N", underline=0 - ) - file_menu.add_command( - label="Open...", command=action.file_open, accelerator="Ctrl+O", underline=0 - ) - file_menu.add_command(label="Reload", command=action.file_reload, underline=0) - file_menu.add_command( - label="Save", command=action.file_save, accelerator="Ctrl+S", underline=0 - ) - file_menu.add_command(label="Save As XML...", command=action.file_save_as_xml) - file_menu.add_command(label="Save As imn...", command=action.file_save_as_imn) - - file_menu.add_separator() - - file_menu.add_command( - label="Export Python script...", command=action.file_export_python_script - ) - file_menu.add_command( - label="Execute XML or Python script...", - command=action.file_execute_xml_or_python_script, - ) - file_menu.add_command( - label="Execute Python script with options...", - command=action.file_execute_python_script_with_options, - ) - - file_menu.add_separator() - - file_menu.add_command( - label="Open current file in editor...", - command=action.file_open_current_file_in_editor, - ) - file_menu.add_command(label="Print...", command=action.file_print, underline=0) - file_menu.add_command( - label="Save screenshot...", command=action.file_save_screenshot - ) - - file_menu.add_separator() - - file_menu.add_command( - label="/home/ncs/.core/configs/sample1.imn", - command=action.file_example_link, - ) - - file_menu.add_separator() - - file_menu.add_command(label="Quit", command=self.master.quit, underline=0) - self.menubar.add_cascade(label="File", menu=file_menu, underline=0) - - def create_edit_menu(self): - """ - Create edit menu - - :return: nothing - """ - edit_menu = tk.Menu(self.menubar) - edit_menu.add_command( - label="Undo", command=action.edit_undo, accelerator="Ctrl+Z", underline=0 - ) - edit_menu.add_command( - label="Redo", command=action.edit_redo, accelerator="Ctrl+Y", underline=0 - ) - - edit_menu.add_separator() - - edit_menu.add_command( - label="Cut", command=action.edit_cut, accelerator="Ctrl+X", underline=0 - ) - edit_menu.add_command( - label="Copy", command=action.edit_copy, accelerator="Ctrl+C", underline=0 - ) - edit_menu.add_command( - label="Paste", command=action.edit_paste, accelerator="Ctrl+V", underline=0 - ) - - edit_menu.add_separator() - - edit_menu.add_command( - label="Select all", command=action.edit_select_all, accelerator="Ctrl+A" - ) - edit_menu.add_command( - label="Select Adjacent", - command=action.edit_select_adjacent, - accelerator="Ctrl+J", - ) - - edit_menu.add_separator() - - edit_menu.add_command( - label="Find...", command=action.edit_find, accelerator="Ctrl+F", underline=0 - ) - edit_menu.add_command(label="Clear marker", command=action.edit_clear_marker) - edit_menu.add_command(label="Preferences...", command=action.edit_preferences) - - self.menubar.add_cascade(label="Edit", menu=edit_menu, underline=0) - - def create_canvas_menu(self): - """ - Create canvas menu - - :return: nothing - """ - canvas_menu = tk.Menu(self.menubar) - canvas_menu.add_command(label="New", command=action.canvas_new) - canvas_menu.add_command(label="Manage...", command=action.canvas_manage) - canvas_menu.add_command(label="Delete", command=action.canvas_delete) - - canvas_menu.add_separator() - - canvas_menu.add_command(label="Size/scale...", command=action.canvas_size_scale) - canvas_menu.add_command(label="Wallpaper...", command=action.canvas_wallpaper) - - canvas_menu.add_separator() - - canvas_menu.add_command( - label="Previous", command=action.canvas_previous, accelerator="PgUp" - ) - canvas_menu.add_command( - label="Next", command=action.canvas_next, accelerator="PgDown" - ) - canvas_menu.add_command( - label="First", command=action.canvas_first, accelerator="Home" - ) - canvas_menu.add_command( - label="Last", command=action.canvas_last, accelerator="End" - ) - - self.menubar.add_cascade(label="Canvas", menu=canvas_menu, underline=0) - - def create_show_menu(self, view_menu): - """ - Create the menu items in View/Show - - :param tkinter.Menu view_menu: the view menu - :return: nothing - """ - show_menu = tk.Menu(view_menu) - show_menu.add_command(label="All", command=action.sub_menu_items) - show_menu.add_command(label="None", command=action.sub_menu_items) - show_menu.add_separator() - show_menu.add_command(label="Interface Names", command=action.sub_menu_items) - show_menu.add_command(label="IPv4 Addresses", command=action.sub_menu_items) - show_menu.add_command(label="IPv6 Addresses", command=action.sub_menu_items) - show_menu.add_command(label="Node Labels", command=action.sub_menu_items) - show_menu.add_command(label="Annotations", command=action.sub_menu_items) - show_menu.add_command(label="Grid", command=action.sub_menu_items) - show_menu.add_command(label="API Messages", command=action.sub_menu_items) - - view_menu.add_cascade(label="Show", menu=show_menu) - - def create_view_menu(self): - """ - Create view menu - - :return: nothing - """ - view_menu = tk.Menu(self.menubar) - self.create_show_menu(view_menu) - view_menu.add_command( - label="Show hidden nodes", command=action.view_show_hidden_nodes - ) - view_menu.add_command(label="Locked", command=action.view_locked) - view_menu.add_command(label="3D GUI...", command=action.view_3d_gui) - - view_menu.add_separator() - - view_menu.add_command( - label="Zoom in", command=action.view_zoom_in, accelerator="+" - ) - view_menu.add_command( - label="Zoom out", command=action.view_zoom_out, accelerator="-" - ) - - self.menubar.add_cascade(label="View", menu=view_menu, underline=0) - - def create_experimental_menu(self, tools_menu): - """ - Create experimental menu item and the sub menu items inside - - :param tkinter.Menu tools_menu: tools menu - :return: nothing - """ - experimental_menu = tk.Menu(tools_menu) - experimental_menu.add_command( - label="Plugins...", command=action.sub_menu_items, underline=0 - ) - experimental_menu.add_command( - label="ns2immunes converter...", command=action.sub_menu_items, underline=0 - ) - experimental_menu.add_command( - label="Topology partitioning...", command=action.sub_menu_items - ) - - tools_menu.add_cascade( - label="Experimental", menu=experimental_menu, underline=0 - ) - - def create_random_menu(self, topology_generator_menu): - """ - Create random menu item and the sub menu items inside - - :param tkinter.Menu topology_generator_menu: topology generator menu - :return: nothing - """ - random_menu = tk.Menu(topology_generator_menu) - # list of number of random nodes to create - nums = [1, 5, 10, 15, 20, 30, 40, 50, 75, 100] - for i in nums: - the_label = "R(" + str(i) + ")" - random_menu.add_command(label=the_label, command=action.sub_menu_items) - - topology_generator_menu.add_cascade( - label="Random", menu=random_menu, underline=0 - ) - - def create_grid_menu(self, topology_generator_menu): - """ - Create grid menu item and the sub menu items inside - - :param tkinter.Menu topology_generator_menu: topology_generator_menu - :return: nothing - """ - grid_menu = tk.Menu(topology_generator_menu) - - # list of number of nodes to create - nums = [1, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 100] - - for i in nums: - the_label = "G(" + str(i) + ")" - grid_menu.add_command(label=the_label, command=action.sub_menu_items) - - topology_generator_menu.add_cascade(label="Grid", menu=grid_menu, underline=0) - - def create_connected_grid_menu(self, topology_generator_menu): - """ - Create connected grid menu items and the sub menu items inside - - :param tkinter.Menu topology_generator_menu: topology generator menu - :return: nothing - """ - grid_menu = tk.Menu(topology_generator_menu) - for i in range(1, 11, 1): - i_n_menu = tk.Menu(grid_menu) - for j in range(1, 11, 1): - i_j_label = str(i) + " X " + str(j) - i_n_menu.add_command(label=i_j_label, command=action.sub_menu_items) - i_n_label = str(i) + " X N" - grid_menu.add_cascade(label=i_n_label, menu=i_n_menu) - topology_generator_menu.add_cascade( - label="Connected Grid", menu=grid_menu, underline=0 - ) - - def create_chain_menu(self, topology_generator_menu): - """ - Create chain menu item and the sub menu items inside - - :param tkinter.Menu topology_generator_menu: topology generator menu - :return: nothing - """ - chain_menu = tk.Menu(topology_generator_menu) - # number of nodes to create - nums = list(range(2, 25, 1)) + [32, 64, 128] - for i in nums: - the_label = "P(" + str(i) + ")" - chain_menu.add_command(label=the_label, command=action.sub_menu_items) - - topology_generator_menu.add_cascade(label="Chain", menu=chain_menu, underline=0) - - def create_star_menu(self, topology_generator_menu): - """ - Create star menu item and the sub menu items inside - - :param tkinter.Menu topology_generator_menu: topology generator menu - :return: nothing - """ - star_menu = tk.Menu(topology_generator_menu) - for i in range(3, 26, 1): - the_label = "C(" + str(i) + ")" - star_menu.add_command(label=the_label, command=action.sub_menu_items) - - topology_generator_menu.add_cascade(label="Star", menu=star_menu, underline=0) - - def create_cycle_menu(self, topology_generator_menu): - """ - Create cycle menu item and the sub items inside - - :param tkinter.Menu topology_generator_menu: topology generator menu - :return: nothing - """ - cycle_menu = tk.Menu(topology_generator_menu) - for i in range(3, 25, 1): - the_label = "C(" + str(i) + ")" - cycle_menu.add_command(label=the_label, command=action.sub_menu_items) - - topology_generator_menu.add_cascade(label="Cycle", menu=cycle_menu, underline=0) - - def create_wheel_menu(self, topology_generator_menu): - """ - Create wheel menu item and the sub menu items inside - - :param tkinter.Menu topology_generator_menu: topology generator menu - :return: nothing - """ - wheel_menu = tk.Menu(topology_generator_menu) - for i in range(4, 26, 1): - the_label = "W(" + str(i) + ")" - wheel_menu.add_command(label=the_label, command=action.sub_menu_items) - - topology_generator_menu.add_cascade(label="Wheel", menu=wheel_menu, underline=0) - - def create_cube_menu(self, topology_generator_menu): - """ - Create cube menu item and the sub menu items inside - - :param tkinter.Menu topology_generator_menu: topology generator menu - :return: nothing - """ - cube_menu = tk.Menu(topology_generator_menu) - for i in range(2, 7, 1): - the_label = "Q(" + str(i) + ")" - cube_menu.add_command(label=the_label, command=action.sub_menu_items) - - topology_generator_menu.add_cascade(label="Cube", menu=cube_menu, underline=0) - - def create_clique_menu(self, topology_generator_menu): - """ - Create clique menu item and the sub menu items inside - - :param tkinter.Menu topology_generator_menu: topology generator menu - :return: nothing - """ - clique_menu = tk.Menu(topology_generator_menu) - for i in range(3, 25, 1): - the_label = "K(" + str(i) + ")" - clique_menu.add_command(label=the_label, command=action.sub_menu_items) - - topology_generator_menu.add_cascade( - label="Clique", menu=clique_menu, underline=0 - ) - - def create_bipartite_menu(self, topology_generator_menu): - """ - Create bipartite menu item and the sub menu items inside - - :param tkinter.Menu topology_generator_menu: topology_generator_menu - :return: nothing - """ - bipartite_menu = tk.Menu(topology_generator_menu) - temp = 24 - for i in range(1, 13, 1): - i_n_menu = tk.Menu(bipartite_menu) - for j in range(i, temp, 1): - i_j_label = "K(" + str(i) + " X " + str(j) + ")" - i_n_menu.add_command(label=i_j_label, command=action.sub_menu_items) - i_n_label = "K(" + str(i) + " X N)" - bipartite_menu.add_cascade(label=i_n_label, menu=i_n_menu) - temp = temp - 1 - topology_generator_menu.add_cascade( - label="Bipartite", menu=bipartite_menu, underline=0 - ) - - def create_topology_generator_menu(self, tools_menu): - """ - Create topology menu item and its sub menu items - - :param tkinter.Menu tools_menu: tools menu - - :return: nothing - """ - topology_generator_menu = tk.Menu(tools_menu) - - self.create_random_menu(topology_generator_menu) - self.create_grid_menu(topology_generator_menu) - self.create_connected_grid_menu(topology_generator_menu) - self.create_chain_menu(topology_generator_menu) - self.create_star_menu(topology_generator_menu) - self.create_cycle_menu(topology_generator_menu) - self.create_wheel_menu(topology_generator_menu) - self.create_cube_menu(topology_generator_menu) - self.create_clique_menu(topology_generator_menu) - self.create_bipartite_menu(topology_generator_menu) - - tools_menu.add_cascade( - label="Topology generator", menu=topology_generator_menu, underline=0 - ) - - def create_tools_menu(self): - """ - Create tools menu - - :return: nothing - """ - - tools_menu = tk.Menu(self.menubar) - tools_menu.add_command( - label="Auto rearrange all", - command=action.tools_auto_rearrange_all, - underline=0, - ) - tools_menu.add_command( - label="Auto rearrange selected", - command=action.tools_auto_rearrange_selected, - underline=0, - ) - tools_menu.add_separator() - - tools_menu.add_command( - label="Align to grid", command=action.tools_align_to_grid, underline=0 - ) - - tools_menu.add_separator() - - tools_menu.add_command(label="Traffic...", command=action.tools_traffic) - tools_menu.add_command( - label="IP addresses...", command=action.tools_ip_addresses, underline=0 - ) - tools_menu.add_command( - label="MAC addresses...", command=action.tools_mac_addresses, underline=0 - ) - tools_menu.add_command( - label="Build hosts file...", - command=action.tools_build_hosts_file, - underline=0, - ) - tools_menu.add_command( - label="Renumber nodes...", command=action.tools_renumber_nodes, underline=0 - ) - self.create_experimental_menu(tools_menu) - self.create_topology_generator_menu(tools_menu) - tools_menu.add_command(label="Debugger...", command=action.tools_debugger) - - self.menubar.add_cascade(label="Tools", menu=tools_menu, underline=0) - - def create_observer_widgets_menu(self, widget_menu): - """ - Create observer widget menu item and create the sub menu items inside - - :param tkinter.Menu widget_menu: widget_menu - :return: nothing - """ - observer_widget_menu = tk.Menu(widget_menu) - observer_widget_menu.add_command(label="None", command=action.sub_menu_items) - observer_widget_menu.add_command( - label="processes", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="ifconfig", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPv4 routes", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPv6 routes", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="OSPFv2 neighbors", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="OSPFv3 neighbors", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="Listening sockets", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPv4 MFC entries", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPv6 MFC entries", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="firewall rules", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPsec policies", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="docker logs", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="OSPFv3 MDR level", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="PIM neighbors", command=action.sub_menu_items - ) - observer_widget_menu.add_command(label="Edit...", command=action.sub_menu_items) - - widget_menu.add_cascade(label="Observer Widgets", menu=observer_widget_menu) - - def create_adjacency_menu(self, widget_menu): - """ - Create adjacency menu item and the sub menu items inside - - :param tkinter.Menu widget_menu: widget menu - :return: nothing - """ - adjacency_menu = tk.Menu(widget_menu) - adjacency_menu.add_command(label="OSPFv2", command=action.sub_menu_items) - adjacency_menu.add_command(label="OSPFv3", command=action.sub_menu_items) - adjacency_menu.add_command(label="OSLR", command=action.sub_menu_items) - adjacency_menu.add_command(label="OSLRv2", command=action.sub_menu_items) - - widget_menu.add_cascade(label="Adjacency", menu=adjacency_menu) - - def create_widgets_menu(self): - """ - Create widget menu - - :return: nothing - """ - widget_menu = tk.Menu(self.menubar) - self.create_observer_widgets_menu(widget_menu) - self.create_adjacency_menu(widget_menu) - widget_menu.add_command(label="Throughput", command=action.widgets_throughput) - - widget_menu.add_separator() - - widget_menu.add_command( - label="Configure Adjacency...", command=action.widgets_configure_adjacency - ) - widget_menu.add_command( - label="Configure Throughput...", command=action.widgets_configure_throughput - ) - - self.menubar.add_cascade(label="Widgets", menu=widget_menu, underline=0) - - def create_session_menu(self): - """ - Create session menu - - :return: nothing - """ - session_menu = tk.Menu(self.menubar) - session_menu.add_command( - label="Start", command=action.session_start, underline=0 - ) - session_menu.add_command( - label="Change sessions...", - command=action.session_change_sessions, - underline=0, - ) - - session_menu.add_separator() - - session_menu.add_command( - label="Node types...", command=action.session_node_types, underline=0 - ) - session_menu.add_command( - label="Comments...", command=action.session_comments, underline=0 - ) - session_menu.add_command( - label="Hooks...", command=action.session_hooks, underline=0 - ) - session_menu.add_command( - label="Reset node positions", - command=action.session_reset_node_positions, - underline=0, - ) - session_menu.add_command( - label="Emulation servers...", - command=action.session_emulation_servers, - underline=0, - ) - session_menu.add_command( - label="Options...", command=action.session_options, underline=0 - ) - - self.menubar.add_cascade(label="Session", menu=session_menu, underline=0) - - def create_help_menu(self): - """ - Create help menu - - :return: nothing - """ - help_menu = tk.Menu(self.menubar) - help_menu.add_command( - label="Core Github (www)", command=action.help_core_github - ) - help_menu.add_command( - label="Core Documentation (www)", command=action.help_core_documentation - ) - help_menu.add_command(label="About", command=action.help_about) - - self.menubar.add_cascade(label="Help", menu=help_menu) - - def bind_menubar_shortcut(self): - """ - Bind hot keys to matching command - - :return: nothing - """ - self.bind_all("", action.file_new_shortcut) - self.bind_all("", action.file_open_shortcut) - self.bind_all("", action.file_save_shortcut) - self.bind_all("", action.edit_undo_shortcut) - self.bind_all("", action.edit_redo_shortcut) - self.bind_all("", action.edit_cut_shortcut) - self.bind_all("", action.edit_copy_shortcut) - self.bind_all("", action.edit_paste_shortcut) - self.bind_all("", action.edit_select_all_shortcut) - self.bind_all("", action.edit_select_adjacent_shortcut) - self.bind_all("", action.edit_find_shortcut) - self.bind_all("", action.canvas_previous_shortcut) - self.bind_all("", action.canvas_next_shortcut) - self.bind_all("", action.canvas_first_shortcut) - self.bind_all("", action.canvas_last_shortcut) - self.bind_all("", action.view_zoom_in_shortcut) - self.bind_all("", action.view_zoom_out_shortcut) - def create_menu(self): self.master.option_add("*tearOff", tk.FALSE) self.menubar = tk.Menu(self.master) - self.create_file_menu() - self.create_edit_menu() - self.create_canvas_menu() - self.create_view_menu() - self.create_tools_menu() - self.create_widgets_menu() - self.create_session_menu() - self.create_help_menu() - + core_menu = CoreMenubar(self, self.master, self.menubar) + core_menu.create_core_menubar() self.master.config(menu=self.menubar) - self.bind_menubar_shortcut() + # TODO clean up this code def create_network_layer_node( self, edit_frame, radio_value, hub_image, switch_image ): @@ -687,8 +64,72 @@ class Application(tk.Frame): image=switch_image, variable=radio_value, value=8, indicatoron=False ) menu_button.pack(side=tk.TOP, pady=1) + self.master.update() + print(menu_button.winfo_rootx(), menu_button.winfo_rooty()) + # print(menu_button.winfo_width(), menu_button.winfo_height()) + # print(self.master.winfo_height()) + option_frame = tk.Frame(self.master) + + switch_button = tk.Button(option_frame, image=switch_image, width=32, height=32) + switch_button.pack(side=tk.LEFT, pady=1) + hub_button = tk.Button(option_frame, image=hub_image, width=32, height=32) + hub_button.pack(side=tk.LEFT, pady=1) + print("Place the button") + print(menu_button.winfo_rootx(), menu_button.winfo_rooty()) + option_frame.place( + x=menu_button.winfo_rootx() + 33, y=menu_button.winfo_rooty() - 117 + ) + self.update() + + print("option frame: " + str(option_frame.winfo_rooty())) + print("option frame x: " + str(option_frame.winfo_rootx())) + + print("frame dimension: " + str(option_frame.winfo_height())) + print("button height: " + str(hub_button.winfo_rooty())) + + # TODO switch 177 into the rooty of the selection tool, retrieve image in here + def draw_options(self, main_button, radio_value): + hub_image = Images.get("hub") + switch_image = Images.get("switch") + option_frame = tk.Frame(self.master) + + switch_button = tk.Radiobutton( + option_frame, + image=switch_image, + width=32, + height=32, + variable=radio_value, + value=7, + indicatoron=False, + ) + switch_button.pack(side=tk.LEFT, pady=1) + hub_button = tk.Radiobutton( + option_frame, + image=hub_image, + width=32, + height=32, + variable=radio_value, + value=8, + indicatoron=False, + ) + hub_button.pack(side=tk.LEFT, pady=1) + self.master.update() + option_frame.place( + x=main_button.winfo_rootx() + 35 - self.selection_button.winfo_rootx(), + y=main_button.winfo_rooty() - self.selection_button.winfo_rooty(), + ) + + def create_network_layer_node_attempt2(self, edit_frame, radio_value): + hub_image = Images.get("hub") + main_button = tk.Radiobutton( + edit_frame, image=hub_image, width=32, height=32, indicatoron=False + ) + main_button.pack(side=tk.TOP, pady=1) + self.draw_options(main_button, radio_value) def create_widgets(self): + + """ select_image = Images.get("select") start_image = Images.get("start") link_image = Images.get("link") @@ -696,11 +137,15 @@ class Application(tk.Frame): hub_image = Images.get("hub") switch_image = Images.get("switch") marker_image = Images.get("marker") + """ edit_frame = tk.Frame(self) edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) + core_editbar = CoreToolbar(self.master, edit_frame) + core_editbar.create_toolbar() + """ radio_value = tk.IntVar() - b = tk.Radiobutton( + self.selection_button = tk.Radiobutton( edit_frame, indicatoron=False, variable=radio_value, @@ -709,7 +154,7 @@ class Application(tk.Frame): height=32, image=select_image, ) - b.pack(side=tk.TOP, pady=1) + self.selection_button.pack(side=tk.TOP, pady=1) b = tk.Radiobutton( edit_frame, indicatoron=False, @@ -763,8 +208,9 @@ class Application(tk.Frame): ) b.pack(side=tk.TOP, pady=1) - self.create_network_layer_node(edit_frame, radio_value, hub_image, switch_image) - + #self.create_network_layer_node(edit_frame, radio_value, hub_image, switch_image) + self.create_network_layer_node_attempt2(edit_frame, radio_value) + """ self.canvas = CanvasGraph( self, background="#cccccc", scrollregion=(0, 0, 1000, 1000) ) diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py new file mode 100644 index 00000000..f4ba51f5 --- /dev/null +++ b/coretk/coretk/coremenubar.py @@ -0,0 +1,654 @@ +import tkinter as tk + +import coretk.menuaction as action + + +class CoreMenubar(object): + """ + Core menubar + """ + + def __init__(self, application, master, menubar): + """ + Create a CoreMenubar instance + + :param master: + :param tkinter.Menu menubar: menubar object + :param coretk.app.Application application: application object + """ + self.menubar = menubar + self.master = master + self.application = application + + def create_file_menu(self): + """ + Create file menu + + :return: nothing + """ + file_menu = tk.Menu(self.menubar) + file_menu.add_command( + label="New", command=action.file_new, accelerator="Ctrl+N", underline=0 + ) + file_menu.add_command( + label="Open...", command=action.file_open, accelerator="Ctrl+O", underline=0 + ) + file_menu.add_command(label="Reload", command=action.file_reload, underline=0) + file_menu.add_command( + label="Save", command=action.file_save, accelerator="Ctrl+S", underline=0 + ) + file_menu.add_command(label="Save As XML...", command=action.file_save_as_xml) + file_menu.add_command(label="Save As imn...", command=action.file_save_as_imn) + + file_menu.add_separator() + + file_menu.add_command( + label="Export Python script...", command=action.file_export_python_script + ) + file_menu.add_command( + label="Execute XML or Python script...", + command=action.file_execute_xml_or_python_script, + ) + file_menu.add_command( + label="Execute Python script with options...", + command=action.file_execute_python_script_with_options, + ) + + file_menu.add_separator() + + file_menu.add_command( + label="Open current file in editor...", + command=action.file_open_current_file_in_editor, + ) + file_menu.add_command(label="Print...", command=action.file_print, underline=0) + file_menu.add_command( + label="Save screenshot...", command=action.file_save_screenshot + ) + + file_menu.add_separator() + + file_menu.add_command( + label="/home/ncs/.core/configs/sample1.imn", + command=action.file_example_link, + ) + + file_menu.add_separator() + + file_menu.add_command(label="Quit", command=self.master.quit, underline=0) + self.menubar.add_cascade(label="File", menu=file_menu, underline=0) + + def create_edit_menu(self): + """ + Create edit menu + + :return: nothing + """ + edit_menu = tk.Menu(self.menubar) + edit_menu.add_command( + label="Undo", command=action.edit_undo, accelerator="Ctrl+Z", underline=0 + ) + edit_menu.add_command( + label="Redo", command=action.edit_redo, accelerator="Ctrl+Y", underline=0 + ) + + edit_menu.add_separator() + + edit_menu.add_command( + label="Cut", command=action.edit_cut, accelerator="Ctrl+X", underline=0 + ) + edit_menu.add_command( + label="Copy", command=action.edit_copy, accelerator="Ctrl+C", underline=0 + ) + edit_menu.add_command( + label="Paste", command=action.edit_paste, accelerator="Ctrl+V", underline=0 + ) + + edit_menu.add_separator() + + edit_menu.add_command( + label="Select all", command=action.edit_select_all, accelerator="Ctrl+A" + ) + edit_menu.add_command( + label="Select Adjacent", + command=action.edit_select_adjacent, + accelerator="Ctrl+J", + ) + + edit_menu.add_separator() + + edit_menu.add_command( + label="Find...", command=action.edit_find, accelerator="Ctrl+F", underline=0 + ) + edit_menu.add_command(label="Clear marker", command=action.edit_clear_marker) + edit_menu.add_command(label="Preferences...", command=action.edit_preferences) + + self.menubar.add_cascade(label="Edit", menu=edit_menu, underline=0) + + def create_canvas_menu(self): + """ + Create canvas menu + + :return: nothing + """ + canvas_menu = tk.Menu(self.menubar) + canvas_menu.add_command(label="New", command=action.canvas_new) + canvas_menu.add_command(label="Manage...", command=action.canvas_manage) + canvas_menu.add_command(label="Delete", command=action.canvas_delete) + + canvas_menu.add_separator() + + canvas_menu.add_command(label="Size/scale...", command=action.canvas_size_scale) + canvas_menu.add_command(label="Wallpaper...", command=action.canvas_wallpaper) + + canvas_menu.add_separator() + + canvas_menu.add_command( + label="Previous", command=action.canvas_previous, accelerator="PgUp" + ) + canvas_menu.add_command( + label="Next", command=action.canvas_next, accelerator="PgDown" + ) + canvas_menu.add_command( + label="First", command=action.canvas_first, accelerator="Home" + ) + canvas_menu.add_command( + label="Last", command=action.canvas_last, accelerator="End" + ) + + self.menubar.add_cascade(label="Canvas", menu=canvas_menu, underline=0) + + def create_show_menu(self, view_menu): + """ + Create the menu items in View/Show + + :param tkinter.Menu view_menu: the view menu + :return: nothing + """ + show_menu = tk.Menu(view_menu) + show_menu.add_command(label="All", command=action.sub_menu_items) + show_menu.add_command(label="None", command=action.sub_menu_items) + show_menu.add_separator() + show_menu.add_command(label="Interface Names", command=action.sub_menu_items) + show_menu.add_command(label="IPv4 Addresses", command=action.sub_menu_items) + show_menu.add_command(label="IPv6 Addresses", command=action.sub_menu_items) + show_menu.add_command(label="Node Labels", command=action.sub_menu_items) + show_menu.add_command(label="Annotations", command=action.sub_menu_items) + show_menu.add_command(label="Grid", command=action.sub_menu_items) + show_menu.add_command(label="API Messages", command=action.sub_menu_items) + + view_menu.add_cascade(label="Show", menu=show_menu) + + def create_view_menu(self): + """ + Create view menu + + :return: nothing + """ + view_menu = tk.Menu(self.menubar) + self.create_show_menu(view_menu) + view_menu.add_command( + label="Show hidden nodes", command=action.view_show_hidden_nodes + ) + view_menu.add_command(label="Locked", command=action.view_locked) + view_menu.add_command(label="3D GUI...", command=action.view_3d_gui) + + view_menu.add_separator() + + view_menu.add_command( + label="Zoom in", command=action.view_zoom_in, accelerator="+" + ) + view_menu.add_command( + label="Zoom out", command=action.view_zoom_out, accelerator="-" + ) + + self.menubar.add_cascade(label="View", menu=view_menu, underline=0) + + def create_experimental_menu(self, tools_menu): + """ + Create experimental menu item and the sub menu items inside + + :param tkinter.Menu tools_menu: tools menu + :return: nothing + """ + experimental_menu = tk.Menu(tools_menu) + experimental_menu.add_command( + label="Plugins...", command=action.sub_menu_items, underline=0 + ) + experimental_menu.add_command( + label="ns2immunes converter...", command=action.sub_menu_items, underline=0 + ) + experimental_menu.add_command( + label="Topology partitioning...", command=action.sub_menu_items + ) + + tools_menu.add_cascade( + label="Experimental", menu=experimental_menu, underline=0 + ) + + def create_random_menu(self, topology_generator_menu): + """ + Create random menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + random_menu = tk.Menu(topology_generator_menu) + # list of number of random nodes to create + nums = [1, 5, 10, 15, 20, 30, 40, 50, 75, 100] + for i in nums: + the_label = "R(" + str(i) + ")" + random_menu.add_command(label=the_label, command=action.sub_menu_items) + + topology_generator_menu.add_cascade( + label="Random", menu=random_menu, underline=0 + ) + + def create_grid_menu(self, topology_generator_menu): + """ + Create grid menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology_generator_menu + :return: nothing + """ + grid_menu = tk.Menu(topology_generator_menu) + + # list of number of nodes to create + nums = [1, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 100] + + for i in nums: + the_label = "G(" + str(i) + ")" + grid_menu.add_command(label=the_label, command=action.sub_menu_items) + + topology_generator_menu.add_cascade(label="Grid", menu=grid_menu, underline=0) + + def create_connected_grid_menu(self, topology_generator_menu): + """ + Create connected grid menu items and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + grid_menu = tk.Menu(topology_generator_menu) + for i in range(1, 11, 1): + i_n_menu = tk.Menu(grid_menu) + for j in range(1, 11, 1): + i_j_label = str(i) + " X " + str(j) + i_n_menu.add_command(label=i_j_label, command=action.sub_menu_items) + i_n_label = str(i) + " X N" + grid_menu.add_cascade(label=i_n_label, menu=i_n_menu) + topology_generator_menu.add_cascade( + label="Connected Grid", menu=grid_menu, underline=0 + ) + + def create_chain_menu(self, topology_generator_menu): + """ + Create chain menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + chain_menu = tk.Menu(topology_generator_menu) + # number of nodes to create + nums = list(range(2, 25, 1)) + [32, 64, 128] + for i in nums: + the_label = "P(" + str(i) + ")" + chain_menu.add_command(label=the_label, command=action.sub_menu_items) + + topology_generator_menu.add_cascade(label="Chain", menu=chain_menu, underline=0) + + def create_star_menu(self, topology_generator_menu): + """ + Create star menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + star_menu = tk.Menu(topology_generator_menu) + for i in range(3, 26, 1): + the_label = "C(" + str(i) + ")" + star_menu.add_command(label=the_label, command=action.sub_menu_items) + + topology_generator_menu.add_cascade(label="Star", menu=star_menu, underline=0) + + def create_cycle_menu(self, topology_generator_menu): + """ + Create cycle menu item and the sub items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + cycle_menu = tk.Menu(topology_generator_menu) + for i in range(3, 25, 1): + the_label = "C(" + str(i) + ")" + cycle_menu.add_command(label=the_label, command=action.sub_menu_items) + + topology_generator_menu.add_cascade(label="Cycle", menu=cycle_menu, underline=0) + + def create_wheel_menu(self, topology_generator_menu): + """ + Create wheel menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + wheel_menu = tk.Menu(topology_generator_menu) + for i in range(4, 26, 1): + the_label = "W(" + str(i) + ")" + wheel_menu.add_command(label=the_label, command=action.sub_menu_items) + + topology_generator_menu.add_cascade(label="Wheel", menu=wheel_menu, underline=0) + + def create_cube_menu(self, topology_generator_menu): + """ + Create cube menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + cube_menu = tk.Menu(topology_generator_menu) + for i in range(2, 7, 1): + the_label = "Q(" + str(i) + ")" + cube_menu.add_command(label=the_label, command=action.sub_menu_items) + + topology_generator_menu.add_cascade(label="Cube", menu=cube_menu, underline=0) + + def create_clique_menu(self, topology_generator_menu): + """ + Create clique menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + clique_menu = tk.Menu(topology_generator_menu) + for i in range(3, 25, 1): + the_label = "K(" + str(i) + ")" + clique_menu.add_command(label=the_label, command=action.sub_menu_items) + + topology_generator_menu.add_cascade( + label="Clique", menu=clique_menu, underline=0 + ) + + def create_bipartite_menu(self, topology_generator_menu): + """ + Create bipartite menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology_generator_menu + :return: nothing + """ + bipartite_menu = tk.Menu(topology_generator_menu) + temp = 24 + for i in range(1, 13, 1): + i_n_menu = tk.Menu(bipartite_menu) + for j in range(i, temp, 1): + i_j_label = "K(" + str(i) + " X " + str(j) + ")" + i_n_menu.add_command(label=i_j_label, command=action.sub_menu_items) + i_n_label = "K(" + str(i) + " X N)" + bipartite_menu.add_cascade(label=i_n_label, menu=i_n_menu) + temp = temp - 1 + topology_generator_menu.add_cascade( + label="Bipartite", menu=bipartite_menu, underline=0 + ) + + def create_topology_generator_menu(self, tools_menu): + """ + Create topology menu item and its sub menu items + + :param tkinter.Menu tools_menu: tools menu + + :return: nothing + """ + topology_generator_menu = tk.Menu(tools_menu) + + self.create_random_menu(topology_generator_menu) + self.create_grid_menu(topology_generator_menu) + self.create_connected_grid_menu(topology_generator_menu) + self.create_chain_menu(topology_generator_menu) + self.create_star_menu(topology_generator_menu) + self.create_cycle_menu(topology_generator_menu) + self.create_wheel_menu(topology_generator_menu) + self.create_cube_menu(topology_generator_menu) + self.create_clique_menu(topology_generator_menu) + self.create_bipartite_menu(topology_generator_menu) + + tools_menu.add_cascade( + label="Topology generator", menu=topology_generator_menu, underline=0 + ) + + def create_tools_menu(self): + """ + Create tools menu + + :return: nothing + """ + + tools_menu = tk.Menu(self.menubar) + tools_menu.add_command( + label="Auto rearrange all", + command=action.tools_auto_rearrange_all, + underline=0, + ) + tools_menu.add_command( + label="Auto rearrange selected", + command=action.tools_auto_rearrange_selected, + underline=0, + ) + tools_menu.add_separator() + + tools_menu.add_command( + label="Align to grid", command=action.tools_align_to_grid, underline=0 + ) + + tools_menu.add_separator() + + tools_menu.add_command(label="Traffic...", command=action.tools_traffic) + tools_menu.add_command( + label="IP addresses...", command=action.tools_ip_addresses, underline=0 + ) + tools_menu.add_command( + label="MAC addresses...", command=action.tools_mac_addresses, underline=0 + ) + tools_menu.add_command( + label="Build hosts file...", + command=action.tools_build_hosts_file, + underline=0, + ) + tools_menu.add_command( + label="Renumber nodes...", command=action.tools_renumber_nodes, underline=0 + ) + self.create_experimental_menu(tools_menu) + self.create_topology_generator_menu(tools_menu) + tools_menu.add_command(label="Debugger...", command=action.tools_debugger) + + self.menubar.add_cascade(label="Tools", menu=tools_menu, underline=0) + + def create_observer_widgets_menu(self, widget_menu): + """ + Create observer widget menu item and create the sub menu items inside + + :param tkinter.Menu widget_menu: widget_menu + :return: nothing + """ + observer_widget_menu = tk.Menu(widget_menu) + observer_widget_menu.add_command(label="None", command=action.sub_menu_items) + observer_widget_menu.add_command( + label="processes", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="ifconfig", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPv4 routes", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPv6 routes", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="OSPFv2 neighbors", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="OSPFv3 neighbors", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="Listening sockets", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPv4 MFC entries", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPv6 MFC entries", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="firewall rules", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPsec policies", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="docker logs", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="OSPFv3 MDR level", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="PIM neighbors", command=action.sub_menu_items + ) + observer_widget_menu.add_command(label="Edit...", command=action.sub_menu_items) + + widget_menu.add_cascade(label="Observer Widgets", menu=observer_widget_menu) + + def create_adjacency_menu(self, widget_menu): + """ + Create adjacency menu item and the sub menu items inside + + :param tkinter.Menu widget_menu: widget menu + :return: nothing + """ + adjacency_menu = tk.Menu(widget_menu) + adjacency_menu.add_command(label="OSPFv2", command=action.sub_menu_items) + adjacency_menu.add_command(label="OSPFv3", command=action.sub_menu_items) + adjacency_menu.add_command(label="OSLR", command=action.sub_menu_items) + adjacency_menu.add_command(label="OSLRv2", command=action.sub_menu_items) + + widget_menu.add_cascade(label="Adjacency", menu=adjacency_menu) + + def create_widgets_menu(self): + """ + Create widget menu + + :return: nothing + """ + widget_menu = tk.Menu(self.menubar) + self.create_observer_widgets_menu(widget_menu) + self.create_adjacency_menu(widget_menu) + widget_menu.add_command(label="Throughput", command=action.widgets_throughput) + + widget_menu.add_separator() + + widget_menu.add_command( + label="Configure Adjacency...", command=action.widgets_configure_adjacency + ) + widget_menu.add_command( + label="Configure Throughput...", command=action.widgets_configure_throughput + ) + + self.menubar.add_cascade(label="Widgets", menu=widget_menu, underline=0) + + def create_session_menu(self): + """ + Create session menu + + :return: nothing + """ + session_menu = tk.Menu(self.menubar) + session_menu.add_command( + label="Start", command=action.session_start, underline=0 + ) + session_menu.add_command( + label="Change sessions...", + command=action.session_change_sessions, + underline=0, + ) + + session_menu.add_separator() + + session_menu.add_command( + label="Node types...", command=action.session_node_types, underline=0 + ) + session_menu.add_command( + label="Comments...", command=action.session_comments, underline=0 + ) + session_menu.add_command( + label="Hooks...", command=action.session_hooks, underline=0 + ) + session_menu.add_command( + label="Reset node positions", + command=action.session_reset_node_positions, + underline=0, + ) + session_menu.add_command( + label="Emulation servers...", + command=action.session_emulation_servers, + underline=0, + ) + session_menu.add_command( + label="Options...", command=action.session_options, underline=0 + ) + + self.menubar.add_cascade(label="Session", menu=session_menu, underline=0) + + def create_help_menu(self): + """ + Create help menu + + :return: nothing + """ + help_menu = tk.Menu(self.menubar) + help_menu.add_command( + label="Core Github (www)", command=action.help_core_github + ) + help_menu.add_command( + label="Core Documentation (www)", command=action.help_core_documentation + ) + help_menu.add_command(label="About", command=action.help_about) + + self.menubar.add_cascade(label="Help", menu=help_menu) + + def bind_menubar_shortcut(self): + """ + Bind hot keys to matching command + + :return: nothing + """ + self.application.bind_all("", action.file_new_shortcut) + self.application.bind_all("", action.file_open_shortcut) + self.application.bind_all("", action.file_save_shortcut) + self.application.bind_all("", action.edit_undo_shortcut) + self.application.bind_all("", action.edit_redo_shortcut) + self.application.bind_all("", action.edit_cut_shortcut) + self.application.bind_all("", action.edit_copy_shortcut) + self.application.bind_all("", action.edit_paste_shortcut) + self.application.bind_all("", action.edit_select_all_shortcut) + self.application.bind_all("", action.edit_select_adjacent_shortcut) + self.application.bind_all("", action.edit_find_shortcut) + self.application.bind_all("", action.canvas_previous_shortcut) + self.application.bind_all("", action.canvas_next_shortcut) + self.application.bind_all("", action.canvas_first_shortcut) + self.application.bind_all("", action.canvas_last_shortcut) + self.application.bind_all("", action.view_zoom_in_shortcut) + self.application.bind_all("", action.view_zoom_out_shortcut) + + def create_core_menubar(self): + """ + Create core menubar and bind the hot keys to their matching command + + :return: nothing + """ + self.create_file_menu() + self.create_edit_menu() + self.create_canvas_menu() + self.create_view_menu() + self.create_tools_menu() + self.create_widgets_menu() + self.create_session_menu() + self.create_help_menu() + self.bind_menubar_shortcut() diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py new file mode 100644 index 00000000..e66bbcc6 --- /dev/null +++ b/coretk/coretk/coretoolbar.py @@ -0,0 +1,421 @@ +import logging +import tkinter as tk + +from coretk.images import Images + + +class CoreToolbar(object): + """ + Core toolbar class + """ + + # TODO Temporarily have a radio_value instance here, might have to include the run frame + def __init__(self, master, edit_frame): + """ + Create a CoreToolbar instance + + :param tkinter.Frame edit_frame: edit frame + """ + self.master = master + self.edit_frame = edit_frame + self.radio_value = tk.IntVar() + + # Used for drawing the horizontally displayed menu items for network-layer nodes and link-layer node + self.selection_tool_button = None + # Reference to the option menus + self.link_layer_option_menu = None + self.marker_option_menu = None + self.network_layer_option_menu = None + + def load_toolbar_images(self): + """ + Load the images that appear in core toolbar + + :return: nothing + """ + Images.load("core", "core-icon.png") + Images.load("start", "start.gif") + Images.load("switch", "lanswitch.gif") + Images.load("marker", "marker.gif") + Images.load("router", "router.gif") + Images.load("select", "select.gif") + Images.load("link", "link.gif") + Images.load("hub", "hub.gif") + Images.load("wlan", "wlan.gif") + Images.load("rj45", "rj45.gif") + Images.load("tunnel", "tunnel.gif") + Images.load("oval", "oval.gif") + Images.load("rectangle", "rectangle.gif") + Images.load("text", "text.gif") + + def hide_all_option_menu_frames(self): + """ + Hide any option menu frame that is displayed on screen so that when a new option menu frame is drawn, only + one frame is displayed + + :return: nothing + """ + if self.marker_option_menu: + self.marker_option_menu.place_forget() + if self.link_layer_option_menu: + self.link_layer_option_menu.place_forget() + if self.network_layer_option_menu: + self.network_layer_option_menu.place_forget() + + def click_selection_tool(self): + logging.debug("Click selection tool") + + def create_selection_tool_button(self): + """ + Create selection tool button + + :return: nothing + """ + selection_tool_image = Images.get("select") + self.selection_tool_button = tk.Radiobutton( + self.edit_frame, + indicatoron=False, + variable=self.radio_value, + value=1, + width=32, + height=32, + image=selection_tool_image, + command=self.click_selection_tool, + ) + self.selection_tool_button.pack(side=tk.TOP, pady=1) + + def create_start_stop_session_button(self): + """ + Create start stop session button + + :return: nothing + """ + start_image = Images.get("start") + start_button = tk.Radiobutton( + self.edit_frame, + indicatoron=False, + variable=self.radio_value, + value=2, + width=32, + height=32, + image=start_image, + ) + start_button.pack(side=tk.TOP, pady=1) + + def create_link_tool_button(self): + """ + Create link tool button + + :return: nothing + """ + link_image = Images.get("link") + link_button = tk.Radiobutton( + self.edit_frame, + indicatoron=False, + variable=self.radio_value, + value=3, + width=32, + height=32, + image=link_image, + ) + link_button.pack(side=tk.TOP, pady=1) + + def create_network_layer_button(self): + """ + Create network layer button + + :return: nothing + """ + router_image = Images.get("router") + network_layer_button = tk.Radiobutton( + self.edit_frame, + indicatoron=False, + variable=self.radio_value, + value=4, + width=32, + height=32, + image=router_image, + ) + network_layer_button.pack(side=tk.TOP, pady=1) + + def pick_hub(self, frame, main_button): + frame.place_forget() + main_button.configure(image=Images.get("hub")) + if self.radio_value.get() != 5: + self.radio_value.set(5) + logging.debug("Pick link-layer node HUB") + + def pick_switch(self, frame, main_button): + frame.place_forget() + main_button.configure(image=Images.get("switch")) + if self.radio_value.get() != 5: + self.radio_value.set(5) + logging.debug("Pick link-layer node SWITCH") + + def pick_wlan(self, frame, main_button): + frame.place_forget() + main_button.configure(image=Images.get("wlan")) + if self.radio_value.get() != 5: + self.radio_value.set(5) + logging.debug("Pick link-layer node WLAN") + + def pick_rj45(self, frame, main_button): + frame.place_forget() + main_button.configure(image=Images.get("rj45")) + if self.radio_value.get() != 5: + self.radio_value.set(5) + logging.debug("Pick link-layer node RJ45") + + def pick_tunnel(self, frame, main_button): + frame.place_forget() + main_button.configure(image=Images.get("tunnel")) + if self.radio_value.get() != 5: + self.radio_value.set(5) + logging.debug("Pick link-layer node TUNNEL") + + def draw_link_layer_options(self, link_layer_button): + # TODO if other buttons are press or nothing is pressed or the button is pressed but frame is forgotten/hidden + option_frame = tk.Frame(self.master) + current_choice = self.radio_value.get() + self.hide_all_option_menu_frames() + if ( + current_choice == 0 + or current_choice != 5 + or (current_choice == 5 and not option_frame.winfo_manager()) + ): + hub_image = Images.get("hub") + switch_image = Images.get("switch") + wlan_image = Images.get("wlan") + rj45_image = Images.get("rj45") + tunnel_image = Images.get("tunnel") + + hub_button = tk.Radiobutton( + option_frame, + indicatoron=False, + variable=self.radio_value, + value=7, + width=32, + height=32, + image=hub_image, + command=lambda: self.pick_hub( + frame=option_frame, main_button=link_layer_button + ), + ) + hub_button.pack(side=tk.LEFT, pady=1) + switch_button = tk.Radiobutton( + option_frame, + indicatoron=False, + variable=self.radio_value, + value=8, + width=32, + height=32, + image=switch_image, + command=lambda: self.pick_switch( + frame=option_frame, main_button=link_layer_button + ), + ) + switch_button.pack(side=tk.LEFT, pady=1) + wlan_button = tk.Radiobutton( + option_frame, + indicatoron=False, + variable=self.radio_value, + value=9, + width=32, + height=32, + image=wlan_image, + command=lambda: self.pick_wlan( + frame=option_frame, main_button=link_layer_button + ), + ) + wlan_button.pack(side=tk.LEFT, pady=1) + rj45_button = tk.Radiobutton( + option_frame, + indicatoron=False, + variable=self.radio_value, + value=10, + width=32, + height=32, + image=rj45_image, + command=lambda: self.pick_rj45( + frame=option_frame, main_button=link_layer_button + ), + ) + rj45_button.pack(side=tk.LEFT, pady=1) + tunnel_button = tk.Radiobutton( + option_frame, + indicatoron=False, + variable=self.radio_value, + value=11, + width=32, + height=32, + image=tunnel_image, + command=lambda: self.pick_tunnel( + frame=option_frame, main_button=link_layer_button + ), + ) + tunnel_button.pack(side=tk.LEFT, pady=1) + + _x = ( + link_layer_button.winfo_rootx() + - self.selection_tool_button.winfo_rootx() + + 33 + ) + _y = ( + link_layer_button.winfo_rooty() + - self.selection_tool_button.winfo_rooty() + ) + option_frame.place(x=_x, y=_y) + self.link_layer_option_menu = option_frame + + def create_link_layer_button(self): + """ + Create link-layer node button and the options that represent different link-layer node types + + :return: nothing + """ + hub_image = Images.get("hub") + link_layer_button = tk.Radiobutton( + self.edit_frame, + indicatoron=False, + variable=self.radio_value, + value=5, + width=32, + height=32, + image=hub_image, + command=lambda: self.draw_link_layer_options(link_layer_button), + ) + link_layer_button.pack(side=tk.TOP, pady=1) + + def pick_marker(self, frame, main_button): + frame.place_forget() + main_button.configure(image=Images.get("marker")) + if self.radio_value.get() != 6: + self.radio_value.set(6) + logging.debug("Pick marker") + + def pick_oval(self, frame, main_button): + frame.place_forget() + main_button.configure(image=Images.get("oval")) + if self.radio_value.get() != 6: + self.radio_value.set(6) + logging.debug("Pick frame") + + def pick_rectangle(self, frame, main_button): + frame.place_forget() + main_button.configure(image=Images.get("rectangle")) + if self.radio_value.get() != 6: + self.radio_value.set(6) + logging.debug("Pick rectangle") + + def pick_text(self, frame, main_button): + frame.place_forget() + main_button.configure(image=Images.get("text")) + if self.radio_value.get() != 6: + self.radio_value.set(6) + logging.debug("Pick text") + + def draw_marker_options(self, main_button): + # TODO if no button pressed, or other buttons being pressed, or marker button is being pressed but no frame is drawn + option_frame = tk.Frame(self.master) + current_choice = self.radio_value.get() + self.hide_all_option_menu_frames() + # TODO might need to find better way to write this, or might not + if ( + current_choice == 0 + or current_choice != 6 + or (current_choice == 6 and not option_frame.winfo_manager()) + ): + marker_image = Images.get("marker") + oval_image = Images.get("oval") + rectangle_image = Images.get("rectangle") + text_image = Images.get("text") + + marker_button = tk.Radiobutton( + option_frame, + indicatoron=False, + variable=self.radio_value, + value=12, + width=32, + height=32, + image=marker_image, + command=lambda: self.pick_marker( + frame=option_frame, main_button=main_button + ), + ) + marker_button.pack(side=tk.LEFT, pady=1) + oval_button = tk.Radiobutton( + option_frame, + indicatoron=False, + variable=self.radio_value, + value=13, + width=32, + height=32, + image=oval_image, + command=lambda: self.pick_oval( + frame=option_frame, main_button=main_button + ), + ) + oval_button.pack(side=tk.LEFT, pady=1) + rectangle_button = tk.Radiobutton( + option_frame, + indicatoron=False, + variable=self.radio_value, + value=14, + width=32, + height=32, + image=rectangle_image, + command=lambda: self.pick_rectangle( + frame=option_frame, main_button=main_button + ), + ) + rectangle_button.pack(side=tk.LEFT, pady=1) + text_button = tk.Radiobutton( + option_frame, + indicatoron=False, + variable=self.radio_value, + value=15, + width=32, + height=32, + image=text_image, + command=lambda: self.pick_text( + frame=option_frame, main_button=main_button + ), + ) + text_button.pack(side=tk.LEFT, pady=1) + self.master.update() + _x = ( + main_button.winfo_rootx() + - self.selection_tool_button.winfo_rootx() + + 33 + ) + _y = main_button.winfo_rooty() - self.selection_tool_button.winfo_rooty() + option_frame.place(x=_x, y=_y) + self.marker_option_menu = option_frame + + def create_marker_button(self): + """ + Create marker button and options that represent different marker types + + :return: nothing + """ + marker_image = Images.get("marker") + marker_main_button = tk.Radiobutton( + self.edit_frame, + indicatoron=False, + variable=self.radio_value, + value=6, + width=32, + height=32, + image=marker_image, + command=lambda: self.draw_marker_options(marker_main_button), + ) + marker_main_button.pack(side=tk.TOP, pady=1) + + def create_toolbar(self): + self.load_toolbar_images() + self.create_selection_tool_button() + self.create_start_stop_session_button() + self.create_link_tool_button() + self.create_network_layer_button() + self.create_link_layer_button() + self.create_marker_button() diff --git a/coretk/coretk/oval.gif b/coretk/coretk/oval.gif new file mode 100644 index 0000000000000000000000000000000000000000..4b3124d4c9f45c45764c282af9fa277fd20ccc6a GIT binary patch literal 174 zcmZ?wbhEHbRA5kGSjfoG(9rPl?Fk4{{3orEtf^pRU|_CLl98(5l%JZJm#*NPpIeZa zSIMCGlZBCsfr&wf0SG|a8JH5L^shYqmVdFyf+%Zojy>fQ6lc4$C74ZnCAn1Vq}@Ai zXX99F^M5aG|Nli|1^))!lG!&+qxHk36QOake}^#rBeM zi_xlcpRkb%J(0K*PL82|tP literal 0 HcmV?d00001 diff --git a/coretk/coretk/rj45.gif b/coretk/coretk/rj45.gif new file mode 100644 index 0000000000000000000000000000000000000000..9ab7ac56dba8946e37182cf05ca160683f53e2e9 GIT binary patch literal 755 zcmZ?wbhEHbRA5kGI2O;~>+9?1=NA+d6cQ2=8X6iN?hzjD84=+X8R;Dr6%`ZX5gi>J z6B82`=NcCmmynQT6n4RsEljD$^YhO@cS6FCQP*6}( zQc_=US6^S>(9qD>*x1z6)Z1&^+iTL%(b3u2Icbu?gb8&MCe%-yIC1ji#;H>)Cr_R{ zZCcfoDO09TFP}cWa{Bb?bLJGyom)6-)~q>m=FFWtcj3b9MT@dltWaIDBy06*okfcl zEnd8M#fp@5>y(!)S+Ze+`id1RR<2yRYSpUMt5>gGyLR2WbsIKp*tBWW=FOY8Y}vAP z>(=etx9`}oW6z#F`}glZaNxkXbJB+n9lCT$_}H;y$B!RBdGh3`Q>RX!K7HoQnKy4Z z-n`*^`AY@?w*HGWo+|t_C-qG3B79HZ>KVjme2?5b& zEz#jV-gD;8n?E<)TfQaAf5D3RfsslI&C!uzn>KIRx+U6NVO@Rn-hKNI9E=XywfoTV zL#jvjoH)DJ{ph-LXCo9Z`uX0xb^Fd;ql*gOuIA>3CMJ58n)ce(Iwr;j78_ zwFCvl;?#p%KCCcW)XlD^vg!rURSp6*7r%Vup0Kc?jagL6VZj0h<|bY?J(HRl`SC3b zyuqttEQOK`I|U_lW>_{XJ;lP^w&RLwAk%&hA&wh48VA%T&VBgp=M(jZ2bpHG$bGPu zdfRWu>8&jPYjEB<6*W6va?-5+;v-TUT4p_f*)Gl%!N9?d`v_mp{QI;sYb*91-N d=esbwJ0>tWSn~6fcWtY;{pRgIt;xz@4FI19FuMQ% literal 0 HcmV?d00001 diff --git a/coretk/coretk/tunnel.gif b/coretk/coretk/tunnel.gif new file mode 100644 index 0000000000000000000000000000000000000000..d574147f535122637516f4887d931779c5903ead GIT binary patch literal 799 zcmZ?wbhEHbRA5kGI2Oae!0_MK*VoU_FDNJ|BqSs>G&DTiBRt$QBEl;&(mN_DDkjDw zIyyQgCMGVRu= zo;-Qlw5lmnrc9q+K7D%S^y$;*%qf~Xw{X_1S###hnLBsx!iCw37GTOS zvSsVmt=qS6-?3xIo;`c^@85smz=3n;qz@fBbm@}tv17-MA3uKbEDBDn2z=biC+ZOKAtSKatw%=FXNJe4Me>Sj+A1D9KCrS*RkU3* z;lrne-kjRjO)MM=OCIfUlrZJGkdVO4#LcG^5-~xMv7K2WY}*zN!ma^ zbPjCKZ)C4{X}6U-tSN3!tNM$%udfIkJDRAe;MWte!OBrWPFoND ze%zhu(T2k5n;-J6D%2?Cv=W`H(&hR5YrE{RRjOvEka_>yUw;J#YXB&pnhF2_ literal 0 HcmV?d00001 diff --git a/coretk/coretk/wlan.gif b/coretk/coretk/wlan.gif new file mode 100644 index 0000000000000000000000000000000000000000..d72fe9c3f8db0c7013424313bc826eca7022c19a GIT binary patch literal 146 zcmZ?wbhEHbRA5kGXkcUjg8%>jEB@otNY*qmFfdba%1_PAOJ`90$->CRz{sEjQUOxS zz!cuozw-23{>32+u5qs4D3yPjAx>eEpJZvqkAmdpb(+5?eJb>ho%FhP{o<=WA`fyl xetyz3Q~L84X{Px{Rg%)5F0}|bV|49q%MP!#xfR=XUU=m{gSY?m^L8c%YXB@FJ0t)A literal 0 HcmV?d00001 From b1bac1dda07ff3118dabf105f736b1c180681ea7 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 26 Sep 2019 16:05:57 -0700 Subject: [PATCH 020/462] progress on core toolbar --- coretk/coretk/OVS.gif | Bin 0 -> 744 bytes coretk/coretk/app.py | 176 +------- coretk/coretk/coretoolbar.py | 602 ++++++++++++++++---------- coretk/coretk/document-properties.gif | Bin 0 -> 635 bytes coretk/coretk/host.gif | Bin 0 -> 1189 bytes coretk/coretk/mdr.gif | Bin 0 -> 1276 bytes coretk/coretk/observe.gif | Bin 0 -> 1149 bytes coretk/coretk/pc.gif | Bin 0 -> 1300 bytes coretk/coretk/plot.gif | Bin 0 -> 265 bytes coretk/coretk/router_green.gif | Bin 0 -> 753 bytes coretk/coretk/run.gif | Bin 0 -> 324 bytes coretk/coretk/stop.gif | Bin 0 -> 1204 bytes coretk/coretk/toolbaraction.py | 17 + coretk/coretk/twonode.gif | Bin 0 -> 220 bytes 14 files changed, 393 insertions(+), 402 deletions(-) create mode 100755 coretk/coretk/OVS.gif create mode 100644 coretk/coretk/document-properties.gif create mode 100644 coretk/coretk/host.gif create mode 100644 coretk/coretk/mdr.gif create mode 100644 coretk/coretk/observe.gif create mode 100644 coretk/coretk/pc.gif create mode 100644 coretk/coretk/plot.gif create mode 100644 coretk/coretk/router_green.gif create mode 100644 coretk/coretk/run.gif create mode 100644 coretk/coretk/stop.gif create mode 100644 coretk/coretk/toolbaraction.py create mode 100644 coretk/coretk/twonode.gif diff --git a/coretk/coretk/OVS.gif b/coretk/coretk/OVS.gif new file mode 100755 index 0000000000000000000000000000000000000000..38fcbb2ea684f2cb3724f55bb668d66dd952ee4d GIT binary patch literal 744 zcmVJ9wZvc%wRbpgMV>J9(izdZ9gfp+0+}KYOA*e5OBqqdk75L4Bk` zeWOBtq(FhHMS!M3gR4h^r$~aQNrI?Dg|0+}u0w{dOogdTg{n`7s!xckMvAgji>+0Q zu1SuzRgA7lkGD#Xw@Q$=SB#%r9qZk@Ytp1W|LyjZ2mSf$H#qQ6_I&0DF?cci~~ zrNDcq!F#8|eW=5Ks>Fe;#fPuQh_T6uvdNFN%aggzn!VDVz|)|@)uF=GrpDN($JnUI z*{jOhtjpW3%-prm-?-G_xzypg)Z)9<;=R}7%*@Qp%*@Qp%*@QpA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=dv*jE#jucwYQHYIwy=OOkz4Hj~p>W&4i3Z zU|?EVaBDeOZhEkLdR2@d7~9=~8y-G!bar@qcWq-WUF1-iLJ&x!2p8np`UC026(iCh zJzDS}A-9DYGIZ!5v5|<6A4!l-8|AaEc70|*i*d`R)429F;{oItU%<%@ul z7Q3Zf+45z~nKf_b+}ZP|Np6FNvUCYkCQX|-b@KEHbQ>Y5MXMskx^$~ki(gevHEK0! zR;ET=O%?)6_2^ZlTAP-%V2Cc;v3TMBba2S8*t>7d8bD;&uG_Lr69G_J06~HV5GGUz aLDL40AWED>vBKp`qSL5Tt6ohg5CA)88DCBS literal 0 HcmV?d00001 diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 37e51aff..43273462 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -17,15 +17,7 @@ class Application(tk.Frame): self.create_widgets() def load_images(self): - # Images.load("switch", "switch.png") Images.load("core", "core-icon.png") - Images.load("start", "start.gif") - Images.load("switch", "lanswitch.gif") - Images.load("marker", "marker.gif") - Images.load("router", "router.gif") - Images.load("select", "select.gif") - Images.load("link", "link.gif") - Images.load("hub", "hub.gif") def setup_app(self): self.master.title("CORE") @@ -41,176 +33,14 @@ class Application(tk.Frame): core_menu.create_core_menubar() self.master.config(menu=self.menubar) - # TODO clean up this code - def create_network_layer_node( - self, edit_frame, radio_value, hub_image, switch_image - ): - menu_button = tk.Menubutton( - edit_frame, - direction=tk.RIGHT, - image=hub_image, - width=32, - height=32, - relief=tk.RAISED, - ) - # menu_button.grid() - menu_button.menu = tk.Menu(menu_button) - menu_button["menu"] = menu_button.menu - - menu_button.menu.add_radiobutton( - image=hub_image, variable=radio_value, value=7, indicatoron=False - ) - menu_button.menu.add_radiobutton( - image=switch_image, variable=radio_value, value=8, indicatoron=False - ) - menu_button.pack(side=tk.TOP, pady=1) - self.master.update() - print(menu_button.winfo_rootx(), menu_button.winfo_rooty()) - # print(menu_button.winfo_width(), menu_button.winfo_height()) - # print(self.master.winfo_height()) - option_frame = tk.Frame(self.master) - - switch_button = tk.Button(option_frame, image=switch_image, width=32, height=32) - switch_button.pack(side=tk.LEFT, pady=1) - hub_button = tk.Button(option_frame, image=hub_image, width=32, height=32) - hub_button.pack(side=tk.LEFT, pady=1) - print("Place the button") - print(menu_button.winfo_rootx(), menu_button.winfo_rooty()) - option_frame.place( - x=menu_button.winfo_rootx() + 33, y=menu_button.winfo_rooty() - 117 - ) - self.update() - - print("option frame: " + str(option_frame.winfo_rooty())) - print("option frame x: " + str(option_frame.winfo_rootx())) - - print("frame dimension: " + str(option_frame.winfo_height())) - print("button height: " + str(hub_button.winfo_rooty())) - - # TODO switch 177 into the rooty of the selection tool, retrieve image in here - def draw_options(self, main_button, radio_value): - hub_image = Images.get("hub") - switch_image = Images.get("switch") - option_frame = tk.Frame(self.master) - - switch_button = tk.Radiobutton( - option_frame, - image=switch_image, - width=32, - height=32, - variable=radio_value, - value=7, - indicatoron=False, - ) - switch_button.pack(side=tk.LEFT, pady=1) - hub_button = tk.Radiobutton( - option_frame, - image=hub_image, - width=32, - height=32, - variable=radio_value, - value=8, - indicatoron=False, - ) - hub_button.pack(side=tk.LEFT, pady=1) - self.master.update() - option_frame.place( - x=main_button.winfo_rootx() + 35 - self.selection_button.winfo_rootx(), - y=main_button.winfo_rooty() - self.selection_button.winfo_rooty(), - ) - - def create_network_layer_node_attempt2(self, edit_frame, radio_value): - hub_image = Images.get("hub") - main_button = tk.Radiobutton( - edit_frame, image=hub_image, width=32, height=32, indicatoron=False - ) - main_button.pack(side=tk.TOP, pady=1) - self.draw_options(main_button, radio_value) - def create_widgets(self): - - """ - select_image = Images.get("select") - start_image = Images.get("start") - link_image = Images.get("link") - router_image = Images.get("router") - hub_image = Images.get("hub") - switch_image = Images.get("switch") - marker_image = Images.get("marker") - """ - edit_frame = tk.Frame(self) edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) - core_editbar = CoreToolbar(self.master, edit_frame) + exec_frame = tk.Frame(edit_frame) + exec_frame.pack(side=tk.TOP) + core_editbar = CoreToolbar(self.master, edit_frame, exec_frame, self.menubar) core_editbar.create_toolbar() - """ - radio_value = tk.IntVar() - self.selection_button = tk.Radiobutton( - edit_frame, - indicatoron=False, - variable=radio_value, - value=1, - width=32, - height=32, - image=select_image, - ) - self.selection_button.pack(side=tk.TOP, pady=1) - b = tk.Radiobutton( - edit_frame, - indicatoron=False, - variable=radio_value, - value=2, - width=32, - height=32, - image=start_image, - ) - b.pack(side=tk.TOP, pady=1) - b = tk.Radiobutton( - edit_frame, - indicatoron=False, - variable=radio_value, - value=3, - width=32, - height=32, - image=link_image, - ) - b.pack(side=tk.TOP, pady=1) - b = tk.Radiobutton( - edit_frame, - indicatoron=False, - variable=radio_value, - value=4, - width=32, - height=32, - image=router_image, - ) - b.pack(side=tk.TOP, pady=1) - - b = tk.Radiobutton( - edit_frame, - indicatoron=False, - variable=radio_value, - value=5, - width=32, - height=32, - image=hub_image, - ) - b.pack(side=tk.TOP, pady=1) - b = tk.Radiobutton( - edit_frame, - indicatoron=False, - variable=radio_value, - value=6, - width=32, - height=32, - image=marker_image, - ) - b.pack(side=tk.TOP, pady=1) - - #self.create_network_layer_node(edit_frame, radio_value, hub_image, switch_image) - self.create_network_layer_node_attempt2(edit_frame, radio_value) - """ self.canvas = CanvasGraph( self, background="#cccccc", scrollregion=(0, 0, 1000, 1000) ) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index e66bbcc6..44b4f472 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -1,6 +1,7 @@ import logging import tkinter as tk +import coretk.toolbaraction as tbaction from coretk.images import Images @@ -9,8 +10,7 @@ class CoreToolbar(object): Core toolbar class """ - # TODO Temporarily have a radio_value instance here, might have to include the run frame - def __init__(self, master, edit_frame): + def __init__(self, master, edit_frame, exec_frame, menubar): """ Create a CoreToolbar instance @@ -18,15 +18,25 @@ class CoreToolbar(object): """ self.master = master self.edit_frame = edit_frame + self.execution_frame = exec_frame + self.menubar = menubar self.radio_value = tk.IntVar() + self.exec_radio_value = tk.IntVar() + + # button dimension + self.width = 32 + self.height = 32 # Used for drawing the horizontally displayed menu items for network-layer nodes and link-layer node self.selection_tool_button = None + # Reference to the option menus self.link_layer_option_menu = None self.marker_option_menu = None self.network_layer_option_menu = None + self.execution_frame = None + def load_toolbar_images(self): """ Load the images that appear in core toolbar @@ -47,23 +57,70 @@ class CoreToolbar(object): Images.load("oval", "oval.gif") Images.load("rectangle", "rectangle.gif") Images.load("text", "text.gif") + Images.load("host", "host.gif") + Images.load("pc", "pc.gif") + Images.load("mdr", "mdr.gif") + Images.load("prouter", "router_green.gif") + Images.load("ovs", "OVS.gif") + Images.load("editnode", "document-properties.gif") + Images.load("run", "run.gif") + Images.load("plot", "plot.gif") + Images.load("twonode", "twonode.gif") + Images.load("stop", "stop.gif") + Images.load("observe", "observe.gif") - def hide_all_option_menu_frames(self): + def destroy_previous_frame(self): """ - Hide any option menu frame that is displayed on screen so that when a new option menu frame is drawn, only - one frame is displayed + Destroy any extra frame from previous before drawing a new one :return: nothing """ - if self.marker_option_menu: - self.marker_option_menu.place_forget() - if self.link_layer_option_menu: - self.link_layer_option_menu.place_forget() - if self.network_layer_option_menu: - self.network_layer_option_menu.place_forget() + if ( + self.network_layer_option_menu + and self.network_layer_option_menu.winfo_exists() + ): + self.network_layer_option_menu.destroy() + if self.link_layer_option_menu and self.link_layer_option_menu.winfo_exists(): + self.link_layer_option_menu.destroy() + if self.marker_option_menu and self.marker_option_menu.winfo_exists(): + self.marker_option_menu.destroy() + + def create_buttons(self, img, func, frame, main_button): + """ + Create button and put it on the frame + + :param PIL.Image img: button image + :param func: the command that is executed when button is clicked + :param tkinter.Frame frame: frame that contains the button + :param tkinter.Radiobutton main_button: main button + :return: nothing + """ + button = tk.Button(frame, width=self.width, height=self.height, image=img) + button.pack(side=tk.LEFT, pady=1) + button.bind("", lambda mb: func(main_button)) + + def bind_widgets_before_frame_hide(self, frame): + """ + Bind the widgets to a left click, when any of the widgets is clicked, the menu option frame is destroyed before + any further action is performed + + :param tkinter.Frame frame: the frame to be destroyed + :return: nothing + """ + self.menubar.bind("", lambda e: frame.destroy()) + self.master.bind("", lambda e: frame.destroy()) + + def unbind_widgets_after_frame_hide(self): + """ + Unbind the widgets to make sure everything works normally again after the menu option frame is destroyed + + :return: nothing + """ + self.master.unbind("") + self.menubar.unbind("Button-1>") def click_selection_tool(self): - logging.debug("Click selection tool") + logging.debug("Click SELECTION TOOL") def create_selection_tool_button(self): """ @@ -77,13 +134,19 @@ class CoreToolbar(object): indicatoron=False, variable=self.radio_value, value=1, - width=32, - height=32, + width=self.width, + height=self.height, image=selection_tool_image, - command=self.click_selection_tool, + command=lambda: self.click_selection_tool(), ) self.selection_tool_button.pack(side=tk.TOP, pady=1) + def click_start_stop_session_tool(self): + logging.debug("Click START STOP SESSION button") + for i in self.edit_frame.winfo_children(): + i.destroy() + self.create_runtime_tool_bar() + def create_start_stop_session_button(self): """ Create start stop session button @@ -96,9 +159,10 @@ class CoreToolbar(object): indicatoron=False, variable=self.radio_value, value=2, - width=32, - height=32, + width=self.width, + height=self.height, image=start_image, + command=lambda: self.click_start_stop_session_tool(), ) start_button.pack(side=tk.TOP, pady=1) @@ -114,12 +178,100 @@ class CoreToolbar(object): indicatoron=False, variable=self.radio_value, value=3, - width=32, - height=32, + width=self.width, + height=self.height, image=link_image, + command=lambda: tbaction.click_link_tool(), ) link_button.pack(side=tk.TOP, pady=1) + def pick_router(self, main_button): + self.network_layer_option_menu.destroy() + main_button.configure(image=Images.get("router")) + logging.debug("Pick router option") + + def pick_host(self, main_button): + self.network_layer_option_menu.destroy() + main_button.configure(image=Images.get("host")) + logging.debug("Pick host option") + + def pick_pc(self, main_button): + self.network_layer_option_menu.destroy() + main_button.configure(image=Images.get("pc")) + logging.debug("Pick PC option") + + def pick_mdr(self, main_button): + self.network_layer_option_menu.destroy() + main_button.configure(image=Images.get("mdr")) + logging.debug("Pick MDR option") + + def pick_prouter(self, main_button): + self.network_layer_option_menu.destroy() + main_button.configure(image=Images.get("prouter")) + logging.debug("Pick prouter option") + + def pick_ovs(self, main_button): + self.network_layer_option_menu.destroy() + main_button.configure(image=Images.get("ovs")) + logging.debug("Pick OVS option") + + def pick_editnode(self, main_button): + self.network_layer_option_menu.destroy() + main_button.configure(image=Images.get("editnode")) + logging.debug("Pick editnode option") + + def draw_network_layer_options(self, network_layer_button): + """ + Draw the options for network-layer button + + :param tkinter.Radiobutton network_layer_button: network-layer button + :return: nothing + """ + # create a frame and add buttons to it + self.destroy_previous_frame() + option_frame = tk.Frame(self.master) + img_list = [ + Images.get("router"), + Images.get("host"), + Images.get("pc"), + Images.get("mdr"), + Images.get("prouter"), + Images.get("ovs"), + Images.get("editnode"), + ] + func_list = [ + self.pick_router, + self.pick_host, + self.pick_pc, + self.pick_mdr, + self.pick_prouter, + self.pick_ovs, + self.pick_editnode, + ] + for i in range(len(img_list)): + self.create_buttons( + img_list[i], func_list[i], option_frame, network_layer_button + ) + + # place frame at a calculated position as well as keep a reference of that frame + _x = ( + network_layer_button.winfo_rootx() + - self.selection_tool_button.winfo_rootx() + + 40 + ) + _y = ( + network_layer_button.winfo_rooty() + - self.selection_tool_button.winfo_rooty() + - 1 + ) + option_frame.place(x=_x, y=_y) + self.network_layer_option_menu = option_frame + + # destroy the frame before any further actions on other widgets + self.bind_widgets_before_frame_hide(option_frame) + option_frame.wait_window(option_frame) + self.unbind_widgets_after_frame_hide() + def create_network_layer_button(self): """ Create network layer button @@ -132,140 +284,86 @@ class CoreToolbar(object): indicatoron=False, variable=self.radio_value, value=4, - width=32, - height=32, + width=self.width, + height=self.height, image=router_image, + command=lambda: self.draw_network_layer_options(network_layer_button), ) network_layer_button.pack(side=tk.TOP, pady=1) - def pick_hub(self, frame, main_button): - frame.place_forget() + def pick_hub(self, main_button): + self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("hub")) - if self.radio_value.get() != 5: - self.radio_value.set(5) logging.debug("Pick link-layer node HUB") - def pick_switch(self, frame, main_button): - frame.place_forget() + def pick_switch(self, main_button): + self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("switch")) - if self.radio_value.get() != 5: - self.radio_value.set(5) logging.debug("Pick link-layer node SWITCH") - def pick_wlan(self, frame, main_button): - frame.place_forget() + def pick_wlan(self, main_button): + self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("wlan")) - if self.radio_value.get() != 5: - self.radio_value.set(5) logging.debug("Pick link-layer node WLAN") - def pick_rj45(self, frame, main_button): - frame.place_forget() + def pick_rj45(self, main_button): + self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("rj45")) - if self.radio_value.get() != 5: - self.radio_value.set(5) logging.debug("Pick link-layer node RJ45") - def pick_tunnel(self, frame, main_button): - frame.place_forget() + def pick_tunnel(self, main_button): + self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("tunnel")) - if self.radio_value.get() != 5: - self.radio_value.set(5) logging.debug("Pick link-layer node TUNNEL") def draw_link_layer_options(self, link_layer_button): - # TODO if other buttons are press or nothing is pressed or the button is pressed but frame is forgotten/hidden + """ + Draw the options for link-layer button + + :param tkinter.RadioButton link_layer_button: link-layer button + :return: nothing + """ + # create a frame and add buttons to it + self.destroy_previous_frame() option_frame = tk.Frame(self.master) - current_choice = self.radio_value.get() - self.hide_all_option_menu_frames() - if ( - current_choice == 0 - or current_choice != 5 - or (current_choice == 5 and not option_frame.winfo_manager()) - ): - hub_image = Images.get("hub") - switch_image = Images.get("switch") - wlan_image = Images.get("wlan") - rj45_image = Images.get("rj45") - tunnel_image = Images.get("tunnel") + img_list = [ + Images.get("hub"), + Images.get("switch"), + Images.get("wlan"), + Images.get("rj45"), + Images.get("tunnel"), + ] + func_list = [ + self.pick_hub, + self.pick_switch, + self.pick_wlan, + self.pick_rj45, + self.pick_tunnel, + ] + for i in range(len(img_list)): + self.create_buttons( + img_list[i], func_list[i], option_frame, link_layer_button + ) - hub_button = tk.Radiobutton( - option_frame, - indicatoron=False, - variable=self.radio_value, - value=7, - width=32, - height=32, - image=hub_image, - command=lambda: self.pick_hub( - frame=option_frame, main_button=link_layer_button - ), - ) - hub_button.pack(side=tk.LEFT, pady=1) - switch_button = tk.Radiobutton( - option_frame, - indicatoron=False, - variable=self.radio_value, - value=8, - width=32, - height=32, - image=switch_image, - command=lambda: self.pick_switch( - frame=option_frame, main_button=link_layer_button - ), - ) - switch_button.pack(side=tk.LEFT, pady=1) - wlan_button = tk.Radiobutton( - option_frame, - indicatoron=False, - variable=self.radio_value, - value=9, - width=32, - height=32, - image=wlan_image, - command=lambda: self.pick_wlan( - frame=option_frame, main_button=link_layer_button - ), - ) - wlan_button.pack(side=tk.LEFT, pady=1) - rj45_button = tk.Radiobutton( - option_frame, - indicatoron=False, - variable=self.radio_value, - value=10, - width=32, - height=32, - image=rj45_image, - command=lambda: self.pick_rj45( - frame=option_frame, main_button=link_layer_button - ), - ) - rj45_button.pack(side=tk.LEFT, pady=1) - tunnel_button = tk.Radiobutton( - option_frame, - indicatoron=False, - variable=self.radio_value, - value=11, - width=32, - height=32, - image=tunnel_image, - command=lambda: self.pick_tunnel( - frame=option_frame, main_button=link_layer_button - ), - ) - tunnel_button.pack(side=tk.LEFT, pady=1) + # place frame at a calculated position as well as keep a reference of the frame + _x = ( + link_layer_button.winfo_rootx() + - self.selection_tool_button.winfo_rootx() + + 40 + ) + _y = ( + link_layer_button.winfo_rooty() + - self.selection_tool_button.winfo_rooty() + - 1 + ) + option_frame.place(x=_x, y=_y) + self.master.update() + self.link_layer_option_menu = option_frame - _x = ( - link_layer_button.winfo_rootx() - - self.selection_tool_button.winfo_rootx() - + 33 - ) - _y = ( - link_layer_button.winfo_rooty() - - self.selection_tool_button.winfo_rooty() - ) - option_frame.place(x=_x, y=_y) - self.link_layer_option_menu = option_frame + # destroy the frame before any further actions on other widgets + self.bind_widgets_before_frame_hide(option_frame) + option_frame.wait_window(option_frame) + self.unbind_widgets_after_frame_hide() def create_link_layer_button(self): """ @@ -279,118 +377,70 @@ class CoreToolbar(object): indicatoron=False, variable=self.radio_value, value=5, - width=32, - height=32, + width=self.width, + height=self.height, image=hub_image, command=lambda: self.draw_link_layer_options(link_layer_button), ) link_layer_button.pack(side=tk.TOP, pady=1) - def pick_marker(self, frame, main_button): - frame.place_forget() + def pick_marker(self, main_button): + self.marker_option_menu.destroy() main_button.configure(image=Images.get("marker")) - if self.radio_value.get() != 6: - self.radio_value.set(6) - logging.debug("Pick marker") + logging.debug("Pick MARKER") + return "break" - def pick_oval(self, frame, main_button): - frame.place_forget() + def pick_oval(self, main_button): + self.marker_option_menu.destroy() main_button.configure(image=Images.get("oval")) - if self.radio_value.get() != 6: - self.radio_value.set(6) - logging.debug("Pick frame") + logging.debug("Pick OVAL") - def pick_rectangle(self, frame, main_button): - frame.place_forget() + def pick_rectangle(self, main_button): + self.marker_option_menu.destroy() main_button.configure(image=Images.get("rectangle")) - if self.radio_value.get() != 6: - self.radio_value.set(6) - logging.debug("Pick rectangle") + logging.debug("Pick RECTANGLE") - def pick_text(self, frame, main_button): - frame.place_forget() + def pick_text(self, main_button): + self.marker_option_menu.destroy() main_button.configure(image=Images.get("text")) - if self.radio_value.get() != 6: - self.radio_value.set(6) - logging.debug("Pick text") + logging.debug("Pick TEXT") def draw_marker_options(self, main_button): - # TODO if no button pressed, or other buttons being pressed, or marker button is being pressed but no frame is drawn - option_frame = tk.Frame(self.master) - current_choice = self.radio_value.get() - self.hide_all_option_menu_frames() - # TODO might need to find better way to write this, or might not - if ( - current_choice == 0 - or current_choice != 6 - or (current_choice == 6 and not option_frame.winfo_manager()) - ): - marker_image = Images.get("marker") - oval_image = Images.get("oval") - rectangle_image = Images.get("rectangle") - text_image = Images.get("text") + """ + Draw the options for marker button - marker_button = tk.Radiobutton( - option_frame, - indicatoron=False, - variable=self.radio_value, - value=12, - width=32, - height=32, - image=marker_image, - command=lambda: self.pick_marker( - frame=option_frame, main_button=main_button - ), - ) - marker_button.pack(side=tk.LEFT, pady=1) - oval_button = tk.Radiobutton( - option_frame, - indicatoron=False, - variable=self.radio_value, - value=13, - width=32, - height=32, - image=oval_image, - command=lambda: self.pick_oval( - frame=option_frame, main_button=main_button - ), - ) - oval_button.pack(side=tk.LEFT, pady=1) - rectangle_button = tk.Radiobutton( - option_frame, - indicatoron=False, - variable=self.radio_value, - value=14, - width=32, - height=32, - image=rectangle_image, - command=lambda: self.pick_rectangle( - frame=option_frame, main_button=main_button - ), - ) - rectangle_button.pack(side=tk.LEFT, pady=1) - text_button = tk.Radiobutton( - option_frame, - indicatoron=False, - variable=self.radio_value, - value=15, - width=32, - height=32, - image=text_image, - command=lambda: self.pick_text( - frame=option_frame, main_button=main_button - ), - ) - text_button.pack(side=tk.LEFT, pady=1) - self.master.update() - _x = ( - main_button.winfo_rootx() - - self.selection_tool_button.winfo_rootx() - + 33 - ) - _y = main_button.winfo_rooty() - self.selection_tool_button.winfo_rooty() - option_frame.place(x=_x, y=_y) - self.marker_option_menu = option_frame + :param tkinter.Radiobutton main_button: the main button + :return: nothing + """ + # create a frame and add buttons to it + self.destroy_previous_frame() + option_frame = tk.Frame(self.master) + img_list = [ + Images.get("marker"), + Images.get("oval"), + Images.get("rectangle"), + Images.get("text"), + ] + func_list = [ + self.pick_marker, + self.pick_oval, + self.pick_rectangle, + self.pick_text, + ] + for i in range(len(img_list)): + self.create_buttons(img_list[i], func_list[i], option_frame, main_button) + + # place the frame at a calculated position as well as keep a reference of that frame + _x = main_button.winfo_rootx() - self.selection_tool_button.winfo_rootx() + 40 + _y = main_button.winfo_rooty() - self.selection_tool_button.winfo_rooty() - 1 + option_frame.place(x=_x, y=_y) + self.master.update() + self.marker_option_menu = option_frame + + # destroy the frame before any further actions on other widgets + self.bind_widgets_before_frame_hide(option_frame) + option_frame.wait_window(option_frame) + self.unbind_widgets_after_frame_hide() def create_marker_button(self): """ @@ -404,13 +454,30 @@ class CoreToolbar(object): indicatoron=False, variable=self.radio_value, value=6, - width=32, - height=32, + width=self.width, + height=self.height, image=marker_image, command=lambda: self.draw_marker_options(marker_main_button), ) marker_main_button.pack(side=tk.TOP, pady=1) + def create_selection_tool_button_for_exec(self): + for i in self.edit_frame.winfo_children(): + i.destroy() + selection_tool_image = Images.get("select") + for i in range(7): + button = tk.Radiobutton( + self.edit_frame, + indicatoron=False, + variable=self.radio_value, + value=1, + width=self.width, + height=self.height, + image=selection_tool_image, + command=lambda: tbaction.click_selection_tool(), + ) + button.pack(side=tk.TOP, pady=1) + def create_toolbar(self): self.load_toolbar_images() self.create_selection_tool_button() @@ -419,3 +486,80 @@ class CoreToolbar(object): self.create_network_layer_button() self.create_link_layer_button() self.create_marker_button() + + def create_radio_button(self, frame, image, func, value): + button = tk.Radiobutton( + frame, + indicatoron=False, + width=self.width, + height=self.height, + image=image, + value=value, + variable=self.exec_radio_value, + ) + button.pack(side=tk.TOP, pady=1) + + def create_regular_button(self, frame, image, func): + button = tk.Button( + frame, width=self.width, height=self.height, image=image, command=func + ) + button.pack(side=tk.TOP, pady=1) + + def create_observe_button(self): + menu_button = tk.Menubutton( + self.edit_frame, + image=Images.get("observe"), + width=self.width, + height=self.height, + direction=tk.RIGHT, + ) + menu_button.menu = tk.Menu(menu_button, tearoff=0) + menu_button["menu"] = menu_button.menu + menu_button.pack(side=tk.TOP, pady=1) + + menu_button.menu.add_command(label="None") + menu_button.menu.add_command(label="processes") + menu_button.menu.add_command(label="ifconfig") + menu_button.menu.add_command(label="IPv4 routes") + menu_button.menu.add_command(label="IPv6 routes") + menu_button.menu.add_command(label="OSPFv2 neighbors") + menu_button.menu.add_command(label="OSPFv3 neighbors") + menu_button.menu.add_command(label="Listening sockets") + menu_button.menu.add_command(label="IPv4 MFC entries") + menu_button.menu.add_command(label="IPv6 MFC entries") + menu_button.menu.add_command(label="firewall rules") + menu_button.menu.add_command(label="IPSec policies") + menu_button.menu.add_command(label="docker logs") + menu_button.menu.add_command(label="OSPFv3 MDR level") + menu_button.menu.add_command(label="PIM neighbors") + menu_button.menu.add_command(label="Edit...") + + def click_run_button(self): + logging.debug("Click on RUN button") + + def click_stop_button(self): + logging.debug("Click on STOP button ") + for i in self.edit_frame.winfo_children(): + i.destroy() + self.create_toolbar() + + def create_runtime_tool_bar(self): + self.create_radio_button( + self.edit_frame, Images.get("select"), self.click_selection_tool, 1 + ) + self.create_regular_button( + self.edit_frame, Images.get("stop"), self.click_stop_button + ) + self.create_observe_button() + self.create_radio_button( + self.edit_frame, Images.get("plot"), self.click_selection_tool, 2 + ) + self.create_radio_button( + self.edit_frame, Images.get("marker"), self.click_selection_tool, 3 + ) + self.create_radio_button( + self.edit_frame, Images.get("twonode"), self.click_selection_tool, 4 + ) + self.create_regular_button( + self.edit_frame, Images.get("run"), self.click_run_button + ) diff --git a/coretk/coretk/document-properties.gif b/coretk/coretk/document-properties.gif new file mode 100644 index 0000000000000000000000000000000000000000..732d8436455ba607ff30774ef83faa434789f775 GIT binary patch literal 635 zcmZ?wbhEHb6krfwI9A3`UtizZ-QL^V)6>`4+Ydxt6Z*URCUj4n)H`Wv|CDJHr_Go& zZN}v3v!=|LJ$2^nX|v}}pFMZR-1#%-Eu6hz(VT@#<}O+?Z}GAP%a$!&x_sgCm5Z0J zTCxI&Rxe$-dg-b)D^{&twiXE1tzNrs?b`LL*REf)X2YuWo7QaFym8yMZ98^v+r4}D z-hI3FAKZQ9*uj%$j-Nbx?EKkN7cQN-eEIUFs~4`{xO(Hp^_#bD+`N6`*6o|O?p(Wh z_u4HGx_egDy&`;YEE0HVit9zMSJ`0=BM zPwqW>djIj$2algUeDdt!vuDqqJb(1!`IDzF9=~}0?D@;5FJHcR`SRtfmoHwweE#Ou zi?^>|zJ2}b^_!P(-oASC_Vv5>Z$Eu}|LN1G&!0Yi`}Xzw_wPTxfBW&{`%fVH2}D1B z{`&dr*Uvvd@b}MOAo%z99|-;X_y6C25W)u(f3h%gG1N2YFaQB4P8is4G}PBC$jQn` zOH0W%^)$&PmQ9?P6RzIdBv~967UJ*YYhIu;UA!+K3@N%$caBBCo?vDZnK?>>} z4H6mGRzwyX@-nz_KC4-2cu1l_BwJ^i<gzK44%5h8Cn(&TO1gfn3+!O Kwon&fum%8seSYfz literal 0 HcmV?d00001 diff --git a/coretk/coretk/host.gif b/coretk/coretk/host.gif new file mode 100644 index 0000000000000000000000000000000000000000..5bd60ae3d34d9bcd8be206ba2a8a701b19aea319 GIT binary patch literal 1189 zcmb``30Kkw008hm262dT`d1VcH7ko8t5<8Y=8+H2yze8AmB-d>TJ62nYg4kad?v4& zM~5QL6hp;kPN#X`g=i`SsGx=l2;OM*vP`yphyA|8&&!8?{OcqFKn51n00031BLEHn zNC*Id0!Ap-3M58&meYM}^Nii1^A`;HUW+aDPOb$JlayBa^A~QAaVrt~&^ysVU zu~#pg$<9c~y!cbrrA)@p=L&u~%ef84YDQ6|53A zyOhN)Z>=og)RebX6*gBFKC7*0t$xCN#;)aZI%*#_*0XpGPkXscKDVf^?p}MtlfL?r z{)STi^M``Q$GuIDyP7#eFUp6TS;FS0!nVrczj^H)ZQ`~nQ9F0AF*Tvx5@_UB?C>0{ufgN&9cE}$zZ!w_;Nz{*UVt6WVly6 z(m6TOEfNXjqx|X7mt*2l#aPdzNccwFDU}Q)4o~IY1S8%i#nxVJG-+|;J&gZw=hEE2=`yIo!;s0j01%;ph13(4-_1_5q zu>%;8WN#j;`#OSvdLYVS@EK^2sT%CutgEd(_JPmM)a$tvWO-8!ZZMjb+;`VMC~55O zZ8Nu)t;HMEVqV7i7$TIA?NOFFUVpL&d_}6G;!tO9Hwt5&FH99hSJIbFigv>uofpWs zil%hc_f_o8f&6TSug$RC!irVvj|0^3kD=;Q-S+#9SWcSL6V#M4uup%lA`x$XVv5UD z|6pDlk~f#;>(=$uM(SdIU&W(eXoPydho|^6LAH- z5a8TqvnWKEOTGL2@5uXX*GJ%NS|CpVgB{s(IKY-cv>&v9ThKhI(04%|NGmAs4*3eV zF&DNkP4DBLKw}yiL%OoOERP)rdMHmXC1Xvn?aG|Pp=5wzTGz6H@;FVQb$gC}yVs literal 0 HcmV?d00001 diff --git a/coretk/coretk/mdr.gif b/coretk/coretk/mdr.gif new file mode 100644 index 0000000000000000000000000000000000000000..d6762f6500828ead06fb73c7a9061165ccdef85b GIT binary patch literal 1276 zcmb``jXTo`0KoCzh-N62mQF9`y{bhS8&Di5W%q3E|!j5ZXj{V1dP z7^69zTAohjVrksC>#Y$b%3K=zJhSp5qbmMJTLzOyzDB>y-XS*imde`bawS84 zmw2%6!-V0cL*B2(Jh`PG1(ilOjJz+z~L3DWyE~ zO;xt!+NFMoy%loArqJ-l_Jm(**X)msz`k|c5+7%ztE@5^S_rHsgwwkfx+OT=Z$;za z1?W%IUsZdN*kB!mhEtzXfV>X7ht>Edn=uv}N8`!@xG`X6rD(8m2lfq+q_=}^$LNJe zrch2W6c0TaQtZgz-JcRb=$y)|Wj z-BiLi_^0&q5I9?(v7Hx8apXh0#<03WyT?yj39`SYQ1$*P0GqVK6ELG(0k9Ks?dS+d z3pf%;4@qAVb9%@ro@T;!2e%9u$e;%Wd0%mEb+qw`b4TNcjXCa=mAQUIuzk0S85EZ* zBUtH@1DtHVzSAt$x4=OWCFxq)a-ePq&RPZ!3`gHsVKo}*{{&+f1UUU>gn=xwL|_AW r4z4Sh`#3ytbr^3Q6*$HAfFoFup6oRk3ar@WthBAq6&LC31nl__5JiQ; literal 0 HcmV?d00001 diff --git a/coretk/coretk/observe.gif b/coretk/coretk/observe.gif new file mode 100644 index 0000000000000000000000000000000000000000..6b66e7305f35484095a2182ed828c6507c72a746 GIT binary patch literal 1149 zcmd_pYfn=L0LJlylbbZAPQ->d0;0q*%+PGvh{hCQEG*WwP@oJp1|%#lOP0+=yD9^E zJMC#%P#A&~sl5%f$fdVaENf{gXltbuDYVeS>4mnZW77@Ay;;_Njy?H(f#=2lWZqG%4!O^wyx&OriRAmhAY&|RN9pvX;*32s2z+p0&S8&Q(U~J z`1HDh)T$s}R}x#5q*i6+PpZl`6}e4S)vl^;SJiZ=Yv^hUU0qAp)G{=mF*Nl|O&t@i zXTqPe;4j#41E_80z%3B`6)#3hlP=O(9c(TGgqR=%a(E!f5Ddi(3PnRA>5yz#qJh;~oqAfY1LaE{BhQ%km%&jd zZ)}wU1Re`@`9oJa7`GpO_2 znf6ZT%y*~P#QJrKeqCd6rkk^g3>#8|2exj&Pd3Cen=<33%!K@ELWXCN zky+%P*>~UUlh66(^ZwC!|AUxE{;{~g*yF%>j3qE(2~5TX71p5gNl<0m(mag`!3!a6 zj6F1E4?SEA|F#s-FGUP<7R!Rey5O)mJx^D>3o)zS8Hdlb>yO>g zpS$Rb*U=ZRf&bZZ{@I%V0OLSG{CB(wAmv}6G4)7z54#w5GAY0Qgx$RVR97jNeyk-K z!%n+IJ#Z+6-;>|?jPuv|H^lhNu21mKffLy5bD4QP?c|#|n0Jp8wco#gbTJ1jr?B#o zpwIZ0DDQaz=@LnV&pIoiXMa6!%xH2@H(fh#3yU%-1l+Ma#DMEE?9C(%k6MeqHBjI^ zNfj8@$s_i!;2S6J=`b1%yJ zjzYQTh5_ro()5X;@-#bkwK}khXSw%@jRar$%MOj@_Rb53* zLseZvO+&*#LsMN-TSG%fPg_?@Ti;MmUsumSSJ%+k(AdDh*umJu$k^1_)ZE0}(#*=* z(#pot+Sba}-p06XUk)~1%W z=GOL>_RhA>?vAdW&hFl>u1PZ|Pn|S<=Cnz(7R{J7eaf6Av**s7Id8$7C2Ll!TE1r8 z>UA5|uGz9@%eKuMb{yEe{n+-M+jj2Wv3bvtz5Dml;r#-+uDu;gbhD7>etiD&*~3?# zUc7q#==JB9uU|ZP^Xu`OFR$LbeE9ayleb@AzkBuk{f}4gKfQhb`t^s;?>@YJ`SI7A zk6+$@eE0U#*AJiGfBgL6)0dC$zkdJx_0xxMKfZkb{OSA8&p&)21O&UZ4U+dI^{!gd>D|h#E6iqaFl|zFQsMJyw=OWRyvJa6$Rl!U zR@l1ZMcNNJ3l}mw>9FJ}Z2q#Shs%CVOlO(ttDwnBECCWCiy96tj+>F9s9ci5+v~R4 zt(VKRlY^y$HMgRo`OM^{iN{KBDit4fo9;Yc%kk90LmM147@K$wKlQwB;JwN4(@H0| z)u(1i(xn6iz5D;z&8dl-$AS zrM*MM*<1JUic6;qwoFj+F)Y&%Z06!ucyvOES=E6-%8p~vY3;2Fi@SBUUUBj^*tDgo zN2{iU;jnU_h0_HN#u!6a(QXcBZ~bi#no@LkeR1MeOki2y)~_Pu;>N(uQq(MZ@J~>; z?!kZ+-HH+o4h+K6bT)OUvr9TM2owiQY?jkgTFfED&(h#5pQ+c>=_GGEv0Ye)C&`VO tJw%~hJ~@t1va|g8z`l6e8jtN{jVv^D?$ literal 0 HcmV?d00001 diff --git a/coretk/coretk/plot.gif b/coretk/coretk/plot.gif new file mode 100644 index 0000000000000000000000000000000000000000..3924adbf821d1cfeb34a0b9efc7e9d79041fd1df GIT binary patch literal 265 zcmV+k0rvh!Nk%w1VITk?0OJ7w0094gp6-C3?t-B1gQ4!&*x1wB`seBX=<5FI>i+8M z{_E`i?Ck#S?f&lW|9_tDA^saEV<{jqFflS9bZBKDLT_bhZf77vZ*6d4Zg~J9`2+z9 z01p5x00000AOIi$00fut$w{ldIO|=y|6nLmg=3y*s-^|GzOdO&Wo)CfRNr_0?7Wx{ z7<>(jwj&azT$&g-m(u|NL}|P#3J?fbWzrac;9XF=-EZ@i;HCisbU;vTDio=PTh2!- z4^;pSCRKfgT}KOh00w)9eM1P2UXPDMla-Z2nVfzkot>MYnWCeXpQfiYsj51xheIN= PvmP6^x45~wyAc38TK{^> literal 0 HcmV?d00001 diff --git a/coretk/coretk/router_green.gif b/coretk/coretk/router_green.gif new file mode 100644 index 0000000000000000000000000000000000000000..76e3ecd57c59ec99767f5641e701f908f09f0258 GIT binary patch literal 753 zcmV$ZwARvGsB7!0$gd`+|BqoR^Cx|C0iYY0J zDJqL9D~&5Hku5KgFE5iXF_tkhmNGJzGBlYqG@CRwn>IF_HaDF&IG;EPKS85GJf%E6r9DBULPDfMJ*GWE zq(eTZK0c^EMy5tVsX<4lNJytiLaIVQt3XMpN=&IvL#;zYu0u|$P(`stMzKamu}4#_ zRY$T%RIOG=vqwj?M@hCxSg=}Juw6^JOJB2LU$bIPyiQ`YWKF$IW3*;ZzE5SgXi&dU zP{2@9!BT3tZc@ThYq)P~xo>Q_Z*979RK-+pyK-^7bymn$R>@X$y?1rKd3C>eSj$*< zzk6EFT7krdfyIV|#)^%~l8(xhlg*iw&YG3ao0iX=n9!e@(Vw8yr=!)Xq}8jY*R816 zucz3rtJ<=#+_<#fy|v!Gw%@Eq<#>~vjA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=my(jE#=El$DGfh?9&+W>zhgGe=rwY)+0Jg^nay zZG3!fWM&|jQh9!Ub|jCOf|fFFeYa;FPI7*AUNAtGy_SqBI$bsnKeZk z1rZZ4K!FI3>S252fd&*X6#oDSAb`LD2Ng7J300IRJ9!Q{YVZ)>j z9!jK$fuo0!Bu}QqoGFqd0h>4*Jb*C40)`MMUew63L&%V$NtcEkS@R|XsZ_0E)ymZ? zSfx)g$2x_2c52nDS+{omiWDwWvQN#Ty}MSg+rDB4%@u4{ZQizg!^$i)AaK;ic-iiK ji-ij!x`Q2SKKvv~q(ODlW;XnF$?4M`P`G$4C=dWUiqcBu literal 0 HcmV?d00001 diff --git a/coretk/coretk/run.gif b/coretk/coretk/run.gif new file mode 100644 index 0000000000000000000000000000000000000000..71dcc67eddc7b829b3221ba4823a04e93c317ba9 GIT binary patch literal 324 zcmZ?wbhEHbRA5kGXkh>WMn*;!78X`kRv8%?d3kvQ0|P@tLt|rOQ&ZEBkdW~3@W#f* zzP`R`)22WfAZwX)2B~gy?XWL&6~Gx-+uY>JSuFtAW4$w*aj%1_PAOIL8t&n-yIt7K68$->CRAkUx!(gbo61M8v(>U}Ah^DZGE(+-Pd9n~wOxE_RahlU()Kf}#eGDh z*YY|kSclnhF*_f*ZL2LVvRih;gcu3Vl7L(W;oNF5dkLndh`{&>RXH)u)s9RO_M-ic zeWf|^96S<2Tyxsf7B~v9GqLh5uUZ+O;*@5-)iu^LcxTxBeY@PPj~q2OIdSsT=`&}~ Ko!3%ium%9#D0q7S literal 0 HcmV?d00001 diff --git a/coretk/coretk/stop.gif b/coretk/coretk/stop.gif new file mode 100644 index 0000000000000000000000000000000000000000..02c2866864fb955827b6af4d434a8ea03508b5a6 GIT binary patch literal 1204 zcmb```A?Gv9LMo*>GQOGO8fMnKq);bg%%10X@M4zBI6uI7|O(9f`)8t&J&IEhZ*K% z8p0H3vMt-(>;N^g83y9QrosRx$V4G+SB}zh6w0L>BIQtq|HD3c{rvguotmB!tKG*1 zE-VAE0ptL&09yeP015#11C#)q0w@Kz0PqdK&j3}M2!tvku^OOe6M=9I;5xuH0$ktZ z0aXZc!vk)j#99QYB_Y3{$So3DOF@67l5SyG9hK5R#Tq<48*uD4oq7+)?=tY;nACer z`X3y|eIItSz^mDxWAgMc(LBsFqM1&#;6w|9Xu{EdnM5lKv9ggqFQlJ?40xjhKIq^k zU)07$ZGPwwk7V~J+4&?BlVb6uTR9Y~H`eCk^}vVI%Hwnk{46}Cna4KqIA)QbgHIY3 zl1D`3zs1}xsegxz*BL5&94hP%6%9yzEn==&%crFW;+X`=Xnfd;p0S?9 zTz`wXu$@1jB>69icnv2A%1q*_d#8!8+C0 zTZrvD>>8Vox>4Mdm_Es^ZH+W4(mo!~Z1|$qT$^|IwxO%!+@-oJh{nC8;r*=WWA`-;QAzjtz^Ii7u|z=Wv2fXj#VN>!l;b z?mLBTO~!ZdvdioVmi{Zhp5ZE9)4|Gr^t?m#a)!v5$*HJ3Tk$2o=T;c6)t=K>yv+RC(pN0M-Dd-mC&;N5vlB2PJtvQ!1!r1BjF z+s~Ac1T8?_TUcM_wWIJ%I-4ebdPz@^w*X1zQv_`U~f{}rNg+fV2s)AE~YGz)#f^&Xu zL1JDdgW^vXMlJ>x1|5(AAfp(Vn>=>i`Dbv-A$vka-~r3C5f^&JB{~NwA;X2aQL-=@frKKWwv3( zs%b?M|9mdLmiXy?;63*j?HbL7MqLIbcP^&3j?Qk~UiW^@i7u0sr`k2mm^rJirgNbX HCxbNrD`Hob literal 0 HcmV?d00001 From 5ce340b8b0dab3ac5e7b3a84a08897c29201c5c1 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 27 Sep 2019 14:19:48 -0700 Subject: [PATCH 021/462] progress on tool bar --- coretk/coretk/app.py | 7 +- coretk/coretk/coretoolbar.py | 269 +++++++++++++++-------------------- 2 files changed, 122 insertions(+), 154 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 43273462..a3fa4e34 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -36,15 +36,16 @@ class Application(tk.Frame): def create_widgets(self): edit_frame = tk.Frame(self) edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) - exec_frame = tk.Frame(edit_frame) - exec_frame.pack(side=tk.TOP) - core_editbar = CoreToolbar(self.master, edit_frame, exec_frame, self.menubar) + core_editbar = CoreToolbar(self.master, edit_frame, self.menubar) core_editbar.create_toolbar() self.canvas = CanvasGraph( self, background="#cccccc", scrollregion=(0, 0, 1000, 1000) ) self.canvas.pack(fill=tk.BOTH, expand=True) + + # self.canvas.create_line(0, 0, 10, 10) + scroll_x = tk.Scrollbar( self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview ) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 44b4f472..83debab5 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -1,7 +1,6 @@ import logging import tkinter as tk -import coretk.toolbaraction as tbaction from coretk.images import Images @@ -10,7 +9,7 @@ class CoreToolbar(object): Core toolbar class """ - def __init__(self, master, edit_frame, exec_frame, menubar): + def __init__(self, master, edit_frame, menubar): """ Create a CoreToolbar instance @@ -18,7 +17,6 @@ class CoreToolbar(object): """ self.master = master self.edit_frame = edit_frame - self.execution_frame = exec_frame self.menubar = menubar self.radio_value = tk.IntVar() self.exec_radio_value = tk.IntVar() @@ -35,8 +33,6 @@ class CoreToolbar(object): self.marker_option_menu = None self.network_layer_option_menu = None - self.execution_frame = None - def load_toolbar_images(self): """ Load the images that appear in core toolbar @@ -85,7 +81,19 @@ class CoreToolbar(object): if self.marker_option_menu and self.marker_option_menu.winfo_exists(): self.marker_option_menu.destroy() - def create_buttons(self, img, func, frame, main_button): + def destroy_children_widgets(self, parent): + """ + Destroy all children of a parent widget + + :param tkinter.Frame parent: parent frame + :return: nothing + """ + + for i in parent.winfo_children(): + if i.winfo_name() != "!frame": + i.destroy() + + def create_button(self, img, func, frame, main_button): """ Create button and put it on the frame @@ -99,6 +107,40 @@ class CoreToolbar(object): button.pack(side=tk.LEFT, pady=1) button.bind("", lambda mb: func(main_button)) + def create_radio_button(self, frame, image, func, variable, value): + button = tk.Radiobutton( + frame, + indicatoron=False, + width=self.width, + height=self.height, + image=image, + value=value, + variable=variable, + command=func, + ) + button.pack(side=tk.TOP, pady=1) + + def create_regular_button(self, frame, image, func): + button = tk.Button( + frame, width=self.width, height=self.height, image=image, command=func + ) + button.pack(side=tk.TOP, pady=1) + + def draw_button_menu_frame(self, edit_frame, option_frame, main_button): + """ + Draw option menu frame right next to the main button + + :param tkinter.Frame edit_frame: parent frame of the main button + :param tkinter.Frame option_frame: option frame to draw + :param tkinter.Radiobutton main_button: the main button + :return: nothing + """ + + first_button = edit_frame.winfo_children()[0] + _x = main_button.winfo_rootx() - first_button.winfo_rootx() + 40 + _y = main_button.winfo_rooty() - first_button.winfo_rooty() - 1 + option_frame.place(x=_x, y=_y) + def bind_widgets_before_frame_hide(self, frame): """ Bind the widgets to a left click, when any of the widgets is clicked, the menu option frame is destroyed before @@ -122,68 +164,13 @@ class CoreToolbar(object): def click_selection_tool(self): logging.debug("Click SELECTION TOOL") - def create_selection_tool_button(self): - """ - Create selection tool button - - :return: nothing - """ - selection_tool_image = Images.get("select") - self.selection_tool_button = tk.Radiobutton( - self.edit_frame, - indicatoron=False, - variable=self.radio_value, - value=1, - width=self.width, - height=self.height, - image=selection_tool_image, - command=lambda: self.click_selection_tool(), - ) - self.selection_tool_button.pack(side=tk.TOP, pady=1) - def click_start_stop_session_tool(self): logging.debug("Click START STOP SESSION button") - for i in self.edit_frame.winfo_children(): - i.destroy() - self.create_runtime_tool_bar() + self.destroy_children_widgets(self.edit_frame) + self.create_runtime_toolbar() - def create_start_stop_session_button(self): - """ - Create start stop session button - - :return: nothing - """ - start_image = Images.get("start") - start_button = tk.Radiobutton( - self.edit_frame, - indicatoron=False, - variable=self.radio_value, - value=2, - width=self.width, - height=self.height, - image=start_image, - command=lambda: self.click_start_stop_session_tool(), - ) - start_button.pack(side=tk.TOP, pady=1) - - def create_link_tool_button(self): - """ - Create link tool button - - :return: nothing - """ - link_image = Images.get("link") - link_button = tk.Radiobutton( - self.edit_frame, - indicatoron=False, - variable=self.radio_value, - value=3, - width=self.width, - height=self.height, - image=link_image, - command=lambda: tbaction.click_link_tool(), - ) - link_button.pack(side=tk.TOP, pady=1) + def click_link_tool(self): + logging.debug("Click LINK button") def pick_router(self, main_button): self.network_layer_option_menu.destroy() @@ -229,7 +216,7 @@ class CoreToolbar(object): """ # create a frame and add buttons to it self.destroy_previous_frame() - option_frame = tk.Frame(self.master) + option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ Images.get("router"), Images.get("host"), @@ -249,22 +236,12 @@ class CoreToolbar(object): self.pick_editnode, ] for i in range(len(img_list)): - self.create_buttons( + self.create_button( img_list[i], func_list[i], option_frame, network_layer_button ) # place frame at a calculated position as well as keep a reference of that frame - _x = ( - network_layer_button.winfo_rootx() - - self.selection_tool_button.winfo_rootx() - + 40 - ) - _y = ( - network_layer_button.winfo_rooty() - - self.selection_tool_button.winfo_rooty() - - 1 - ) - option_frame.place(x=_x, y=_y) + self.draw_button_menu_frame(self.edit_frame, option_frame, network_layer_button) self.network_layer_option_menu = option_frame # destroy the frame before any further actions on other widgets @@ -283,7 +260,7 @@ class CoreToolbar(object): self.edit_frame, indicatoron=False, variable=self.radio_value, - value=4, + value=3, width=self.width, height=self.height, image=router_image, @@ -325,7 +302,7 @@ class CoreToolbar(object): """ # create a frame and add buttons to it self.destroy_previous_frame() - option_frame = tk.Frame(self.master) + option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ Images.get("hub"), Images.get("switch"), @@ -341,23 +318,12 @@ class CoreToolbar(object): self.pick_tunnel, ] for i in range(len(img_list)): - self.create_buttons( + self.create_button( img_list[i], func_list[i], option_frame, link_layer_button ) # place frame at a calculated position as well as keep a reference of the frame - _x = ( - link_layer_button.winfo_rootx() - - self.selection_tool_button.winfo_rootx() - + 40 - ) - _y = ( - link_layer_button.winfo_rooty() - - self.selection_tool_button.winfo_rooty() - - 1 - ) - option_frame.place(x=_x, y=_y) - self.master.update() + self.draw_button_menu_frame(self.edit_frame, option_frame, link_layer_button) self.link_layer_option_menu = option_frame # destroy the frame before any further actions on other widgets @@ -376,7 +342,7 @@ class CoreToolbar(object): self.edit_frame, indicatoron=False, variable=self.radio_value, - value=5, + value=4, width=self.width, height=self.height, image=hub_image, @@ -414,7 +380,7 @@ class CoreToolbar(object): """ # create a frame and add buttons to it self.destroy_previous_frame() - option_frame = tk.Frame(self.master) + option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ Images.get("marker"), Images.get("oval"), @@ -428,13 +394,10 @@ class CoreToolbar(object): self.pick_text, ] for i in range(len(img_list)): - self.create_buttons(img_list[i], func_list[i], option_frame, main_button) + self.create_button(img_list[i], func_list[i], option_frame, main_button) # place the frame at a calculated position as well as keep a reference of that frame - _x = main_button.winfo_rootx() - self.selection_tool_button.winfo_rootx() + 40 - _y = main_button.winfo_rooty() - self.selection_tool_button.winfo_rooty() - 1 - option_frame.place(x=_x, y=_y) - self.master.update() + self.draw_button_menu_frame(self.edit_frame, option_frame, main_button) self.marker_option_menu = option_frame # destroy the frame before any further actions on other widgets @@ -453,7 +416,7 @@ class CoreToolbar(object): self.edit_frame, indicatoron=False, variable=self.radio_value, - value=6, + value=5, width=self.width, height=self.height, image=marker_image, @@ -461,50 +424,29 @@ class CoreToolbar(object): ) marker_main_button.pack(side=tk.TOP, pady=1) - def create_selection_tool_button_for_exec(self): - for i in self.edit_frame.winfo_children(): - i.destroy() - selection_tool_image = Images.get("select") - for i in range(7): - button = tk.Radiobutton( - self.edit_frame, - indicatoron=False, - variable=self.radio_value, - value=1, - width=self.width, - height=self.height, - image=selection_tool_image, - command=lambda: tbaction.click_selection_tool(), - ) - button.pack(side=tk.TOP, pady=1) - def create_toolbar(self): self.load_toolbar_images() - self.create_selection_tool_button() - self.create_start_stop_session_button() - self.create_link_tool_button() + self.create_regular_button( + self.edit_frame, Images.get("start"), self.click_start_stop_session_tool + ) + self.create_radio_button( + self.edit_frame, + Images.get("select"), + self.click_selection_tool, + self.radio_value, + 1, + ) + self.create_radio_button( + self.edit_frame, + Images.get("link"), + self.click_link_tool, + self.radio_value, + 2, + ) self.create_network_layer_button() self.create_link_layer_button() self.create_marker_button() - def create_radio_button(self, frame, image, func, value): - button = tk.Radiobutton( - frame, - indicatoron=False, - width=self.width, - height=self.height, - image=image, - value=value, - variable=self.exec_radio_value, - ) - button.pack(side=tk.TOP, pady=1) - - def create_regular_button(self, frame, image, func): - button = tk.Button( - frame, width=self.width, height=self.height, image=image, command=func - ) - button.pack(side=tk.TOP, pady=1) - def create_observe_button(self): menu_button = tk.Menubutton( self.edit_frame, @@ -512,6 +454,7 @@ class CoreToolbar(object): width=self.width, height=self.height, direction=tk.RIGHT, + relief=tk.RAISED, ) menu_button.menu = tk.Menu(menu_button, tearoff=0) menu_button["menu"] = menu_button.menu @@ -534,31 +477,55 @@ class CoreToolbar(object): menu_button.menu.add_command(label="PIM neighbors") menu_button.menu.add_command(label="Edit...") + def click_stop_button(self): + logging.debug("Click on STOP button ") + self.destroy_children_widgets(self.edit_frame) + self.create_toolbar() + def click_run_button(self): logging.debug("Click on RUN button") - def click_stop_button(self): - logging.debug("Click on STOP button ") - for i in self.edit_frame.winfo_children(): - i.destroy() - self.create_toolbar() + def click_plot_button(self): + logging.debug("Click on plot button") - def create_runtime_tool_bar(self): - self.create_radio_button( - self.edit_frame, Images.get("select"), self.click_selection_tool, 1 - ) + def click_marker_button(self): + logging.debug("Click on marker button") + + def click_two_node_button(self): + logging.debug("Click TWONODE button") + + def create_runtime_toolbar(self): self.create_regular_button( self.edit_frame, Images.get("stop"), self.click_stop_button ) + self.create_radio_button( + self.edit_frame, + Images.get("select"), + self.click_selection_tool, + self.exec_radio_value, + 1, + ) self.create_observe_button() self.create_radio_button( - self.edit_frame, Images.get("plot"), self.click_selection_tool, 2 + self.edit_frame, + Images.get("plot"), + self.click_plot_button, + self.exec_radio_value, + 2, ) self.create_radio_button( - self.edit_frame, Images.get("marker"), self.click_selection_tool, 3 + self.edit_frame, + Images.get("marker"), + self.click_marker_button, + self.exec_radio_value, + 3, ) self.create_radio_button( - self.edit_frame, Images.get("twonode"), self.click_selection_tool, 4 + self.edit_frame, + Images.get("twonode"), + self.click_two_node_button, + self.exec_radio_value, + 4, ) self.create_regular_button( self.edit_frame, Images.get("run"), self.click_run_button From a1af60688e6da6f8eeb64fc498bb0f10c6fe530e Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 27 Sep 2019 15:28:51 -0700 Subject: [PATCH 022/462] add tool tip class to draw tool tip box for the buttons --- coretk/coretk/coretoolbar.py | 3 +++ coretk/coretk/tooltip.py | 38 ++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 83debab5..69379aa0 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -3,6 +3,8 @@ import tkinter as tk from coretk.images import Images +# from coretk.tooltip import CreateToolTip + class CoreToolbar(object): """ @@ -267,6 +269,7 @@ class CoreToolbar(object): command=lambda: self.draw_network_layer_options(network_layer_button), ) network_layer_button.pack(side=tk.TOP, pady=1) + # network_layer_btt = CreateToolTip(network_layer_button, "Network-layer virtual nodes") def pick_hub(self, main_button): self.link_layer_option_menu.destroy() diff --git a/coretk/coretk/tooltip.py b/coretk/coretk/tooltip.py index e69de29b..226b0ed8 100644 --- a/coretk/coretk/tooltip.py +++ b/coretk/coretk/tooltip.py @@ -0,0 +1,38 @@ +import tkinter as tk + + +class CreateToolTip(object): + """ + Create tool tip for a given widget + """ + + def __init__(self, widget, text="widget info"): + self.widget = widget + self.text = text + self.widget.bind("", self.enter) + self.widget.bind("", self.close) + self.tw = None + + def enter(self, event=None): + x = 0 + y = 0 + x, y, cx, cy = self.widget.bbox("insert") + x += self.widget.winfo_rootx() + y += self.widget.winfo_rooty() + 32 + + self.tw = tk.Toplevel(self.widget) + self.tw.wm_overrideredirect(True) + self.tw.wm_geometry("+%d+%d" % (x, y)) + label = tk.Label( + self.tw, + text=self.text, + justify=tk.LEFT, + background="yellow", + relief="solid", + borderwidth=1, + ) + label.pack(ipadx=1) + + def close(self, event=None): + if self.tw: + self.tw.destroy() From 2e007935ee77eac7005f8a5b6c5fa005566dc59b Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 27 Sep 2019 16:00:38 -0700 Subject: [PATCH 023/462] toolbar --- coretk/coretk/coretoolbar.py | 16 ++++++++++++---- coretk/coretk/tooltip.py | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 69379aa0..74d7979b 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -2,8 +2,7 @@ import logging import tkinter as tk from coretk.images import Images - -# from coretk.tooltip import CreateToolTip +from coretk.tooltip import CreateToolTip class CoreToolbar(object): @@ -109,7 +108,7 @@ class CoreToolbar(object): button.pack(side=tk.LEFT, pady=1) button.bind("", lambda mb: func(main_button)) - def create_radio_button(self, frame, image, func, variable, value): + def create_radio_button(self, frame, image, func, variable, value, tooltip_msg): button = tk.Radiobutton( frame, indicatoron=False, @@ -121,6 +120,7 @@ class CoreToolbar(object): command=func, ) button.pack(side=tk.TOP, pady=1) + CreateToolTip(button, tooltip_msg) def create_regular_button(self, frame, image, func): button = tk.Button( @@ -269,7 +269,7 @@ class CoreToolbar(object): command=lambda: self.draw_network_layer_options(network_layer_button), ) network_layer_button.pack(side=tk.TOP, pady=1) - # network_layer_btt = CreateToolTip(network_layer_button, "Network-layer virtual nodes") + CreateToolTip(network_layer_button, "Network-layer virtual nodes") def pick_hub(self, main_button): self.link_layer_option_menu.destroy() @@ -352,6 +352,7 @@ class CoreToolbar(object): command=lambda: self.draw_link_layer_options(link_layer_button), ) link_layer_button.pack(side=tk.TOP, pady=1) + CreateToolTip(link_layer_button, "link-layer nodes") def pick_marker(self, main_button): self.marker_option_menu.destroy() @@ -426,6 +427,7 @@ class CoreToolbar(object): command=lambda: self.draw_marker_options(marker_main_button), ) marker_main_button.pack(side=tk.TOP, pady=1) + CreateToolTip(marker_main_button, "background annotation tools") def create_toolbar(self): self.load_toolbar_images() @@ -438,6 +440,7 @@ class CoreToolbar(object): self.click_selection_tool, self.radio_value, 1, + "selection tool", ) self.create_radio_button( self.edit_frame, @@ -445,6 +448,7 @@ class CoreToolbar(object): self.click_link_tool, self.radio_value, 2, + "link tool", ) self.create_network_layer_button() self.create_link_layer_button() @@ -507,6 +511,7 @@ class CoreToolbar(object): self.click_selection_tool, self.exec_radio_value, 1, + "selection tool", ) self.create_observe_button() self.create_radio_button( @@ -515,6 +520,7 @@ class CoreToolbar(object): self.click_plot_button, self.exec_radio_value, 2, + "plot", ) self.create_radio_button( self.edit_frame, @@ -522,6 +528,7 @@ class CoreToolbar(object): self.click_marker_button, self.exec_radio_value, 3, + "marker", ) self.create_radio_button( self.edit_frame, @@ -529,6 +536,7 @@ class CoreToolbar(object): self.click_two_node_button, self.exec_radio_value, 4, + "run command from one node to another", ) self.create_regular_button( self.edit_frame, Images.get("run"), self.click_run_button diff --git a/coretk/coretk/tooltip.py b/coretk/coretk/tooltip.py index 226b0ed8..beb68ba3 100644 --- a/coretk/coretk/tooltip.py +++ b/coretk/coretk/tooltip.py @@ -27,7 +27,7 @@ class CreateToolTip(object): self.tw, text=self.text, justify=tk.LEFT, - background="yellow", + background="#ffffe6", relief="solid", borderwidth=1, ) From 4bf08af88606c84ca470d1af373c4872c8bcc1dd Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 27 Sep 2019 16:18:30 -0700 Subject: [PATCH 024/462] added core to coretk dependencies --- coretk/Pipfile | 1 + coretk/Pipfile.lock | 111 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/coretk/Pipfile b/coretk/Pipfile index 720b1c45..da8c8df8 100644 --- a/coretk/Pipfile +++ b/coretk/Pipfile @@ -14,3 +14,4 @@ pre-commit = "*" [packages] coretk = {editable = true,path = "."} +core = {editable = true,path = "./../daemon"} diff --git a/coretk/Pipfile.lock b/coretk/Pipfile.lock index a08ee17f..d3547813 100644 --- a/coretk/Pipfile.lock +++ b/coretk/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "52de2a0b7a80abe39564a3943879fe75305b0cb8243c0fce78ef43689bee77c0" + "sha256": "2cb6b23bfb8b9bebb5ece1016de468cc57fe46cf4df08a61b1861aee4bed1028" }, "pipfile-spec": 6, "requires": {}, @@ -14,10 +14,91 @@ ] }, "default": { + "configparser": { + "hashes": [ + "sha256:254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c", + "sha256:c7d282687a5308319bf3d2e7706e575c635b0a470342641c93bea0ea3b5331df" + ], + "version": "==4.0.2" + }, + "core": { + "editable": true, + "path": "./../daemon" + }, "coretk": { "editable": true, "path": "." }, + "future": { + "hashes": [ + "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8" + ], + "version": "==0.17.1" + }, + "grpcio": { + "hashes": [ + "sha256:0337debec20fe385bcd49048d6917270efbc17a5119857466559b4db91f8995b", + "sha256:164f82a99e08797ea786283b66b45ebe76772d321577d1674ba6fe0200155892", + "sha256:172dfba8d9621048c2cbc1d1cf7a02244e9a9a8cff5bb79bb30bcb0c13c7fd31", + "sha256:18f4b536d8a9cfa15b3214e0bb628071def94160699e91798f0a954c3b2db88d", + "sha256:2283b56bda49b068b0f08d006fffc7dd46eae72322f1a5dec87fc9c218f1dc2d", + "sha256:26b33f488a955bf49262d2ce3423d3a8174108506d8f819e8150aca21bdd3b99", + "sha256:31cc9b6f70bdd0d9ff53df2d563ea1fb278601d5c625932d8a82d03b08ff3de0", + "sha256:37dd8684fbc2bc00766ae6784bcbd7f874bc96527636a341411db811d04ff650", + "sha256:424c01189ef51a808669f020368b01204e0f1fa0bf2adab7c8d0d13166f92e9e", + "sha256:431c099f20a1f1d97def98f87bb74fa752e8819c2bab23d79089353aed1acc9b", + "sha256:4c2f1d0b27bcef301e5d5c1da05ffd7d174f807f61889c006b8e708b16bc978e", + "sha256:59b8d738867b59c5daaff5df242b5f3f9c58b47862f603c6ee530964b897b69b", + "sha256:8d4f1ee2a67cf8f792d4fc9b8c7bb2148174e838d935f175653aec234752828b", + "sha256:97ab9e35b47bda0441332204960f95c1169c55ec8e989381bedd32bdb9f78b05", + "sha256:9cf93e185507bfdaa7ed45a90049bd3f1ed3f6357ad3772b31e993ff723cf67d", + "sha256:a5a81472c0ca6181492b9291c316ff60c6c94dd3f21c1e8c481f21923d899af0", + "sha256:aaa1feb0fdd094af6db0a16cbd446ed94285a50e320aede5971152d9ea022df8", + "sha256:b36bf4408f8400ee9ab13ff129e71f2e4c72ce2d8886b744aeab77ce50a55cf6", + "sha256:bb345f7e98b38a2c1ef33ff1145687234f78dfeedf308b41b3e41f4b42eba099", + "sha256:c13ae15695e0eb4ba2db920d6a197171d2398c675afa4f27460b6381d20a6884", + "sha256:c4a233a00cc5b64543e97733902151bc6738396931b3c166aad03a3aaadbd479", + "sha256:c521c5f8a95baabba69c68dd0f5e34f37c8adf1a9691f9884dba3eab4ebadc29", + "sha256:c772edd094fe3e54d6d54fdecb90c51cb8d07b55e9c1cda2d33e9615e33d07e8", + "sha256:cebfba6542855403b29e4bc95bbcd5ab444f21137b440f2fb7c7925ca0e55bfd", + "sha256:d7490e013c4bad3e8db804fc6483b47125dc8df0ebcfd6e419bd25df35025301", + "sha256:dfb6063619f297cbd22c67530d7465d98348b35d0424bbc1756b36c5ef9f99d4", + "sha256:e80a15b48a66f35c7c33db2a7df4034a533b362269d0e60e0036e23f14bac7b5", + "sha256:ea444fa1c1ec4f8d2ce965bb01e06148ef9ceb398fb2f627511d50f137eac35b", + "sha256:ec986cbf8837a49f9612cc1cfc2a8ccb54875cfce5355a121279de35124ea1db", + "sha256:fb641df6de8c4a55c784c24d334d53096954d9b30679d3ce5eb6a4d25c1020a3", + "sha256:fb88bd791c8efbcb36de12f0aa519ceec0b7806d3decff16e412e097d4725d44", + "sha256:ffa1be3d566a9cbd21a5f2d95fd9262ec6c337c499291bfeb51547b8de18942e" + ], + "version": "==1.24.0" + }, + "lxml": { + "hashes": [ + "sha256:02ca7bf899da57084041bb0f6095333e4d239948ad3169443f454add9f4e9cb4", + "sha256:096b82c5e0ea27ce9138bcbb205313343ee66a6e132f25c5ed67e2c8d960a1bc", + "sha256:0a920ff98cf1aac310470c644bc23b326402d3ef667ddafecb024e1713d485f1", + "sha256:17cae1730a782858a6e2758fd20dd0ef7567916c47757b694a06ffafdec20046", + "sha256:17e3950add54c882e032527795c625929613adbd2ce5162b94667334458b5a36", + "sha256:1f4f214337f6ee5825bf90a65d04d70aab05526c08191ab888cb5149501923c5", + "sha256:2e8f77db25b0a96af679e64ff9bf9dddb27d379c9900c3272f3041c4d1327c9d", + "sha256:4dffd405390a45ecb95ab5ab1c1b847553c18b0ef8ed01e10c1c8b1a76452916", + "sha256:6b899931a5648862c7b88c795eddff7588fb585e81cecce20f8d9da16eff96e0", + "sha256:726c17f3e0d7a7200718c9a890ccfeab391c9133e363a577a44717c85c71db27", + "sha256:760c12276fee05c36f95f8040180abc7fbebb9e5011447a97cdc289b5d6ab6fc", + "sha256:796685d3969815a633827c818863ee199440696b0961e200b011d79b9394bbe7", + "sha256:891fe897b49abb7db470c55664b198b1095e4943b9f82b7dcab317a19116cd38", + "sha256:a471628e20f03dcdfde00770eeaf9c77811f0c331c8805219ca7b87ac17576c5", + "sha256:a63b4fd3e2cabdcc9d918ed280bdde3e8e9641e04f3c59a2a3109644a07b9832", + "sha256:b0b84408d4eabc6de9dd1e1e0bc63e7731e890c0b378a62443e5741cfd0ae90a", + "sha256:be78485e5d5f3684e875dab60f40cddace2f5b2a8f7fede412358ab3214c3a6f", + "sha256:c27eaed872185f047bb7f7da2d21a7d8913457678c9a100a50db6da890bc28b9", + "sha256:c81cb40bff373ab7a7446d6bbca0190bccc5be3448b47b51d729e37799bb5692", + "sha256:d11874b3c33ee441059464711cd365b89fa1a9cf19ae75b0c189b01fbf735b84", + "sha256:e9c028b5897901361d81a4718d1db217b716424a0283afe9d6735fe0caf70f79", + "sha256:fe489d486cd00b739be826e8c1be188ddb74c7a1ca784d93d06fda882a6a1681" + ], + "version": "==4.4.1" + }, "pillow": { "hashes": [ "sha256:0804f77cb1e9b6dbd37601cee11283bba39a8d44b9ddb053400c58e0c0d7d9de", @@ -48,6 +129,34 @@ "sha256:f8dc19d92896558f9c4317ee365729ead9d7bbcf2052a9a19a3ef17abbb8ac5b" ], "version": "==6.1.0" + }, + "protobuf": { + "hashes": [ + "sha256:26c0d756c7ad6823fccbc3b5f84c619b9cc7ac281496fe0a9d78e32023c45034", + "sha256:3200046e4d4f6c42ed66257dbe15e2e5dc76072c280e9b3d69dc8f3a4fa3fbbc", + "sha256:368f1bae6dd22d04fd2254d30cd301863408a96ff604422e3ddd8ab601f095a4", + "sha256:3902fa1920b4ef9f710797496b309efc5ccd0faeba44dc82ed6a711a244764a0", + "sha256:3a7a8925ba6481b9241cdb5d69cd0b0700f23efed6bb691dc9543faa4aa25d6f", + "sha256:4bc33d49f43c6e9916fb56b7377cb4478cbf25824b4d2bedfb8a4e3df31c12ca", + "sha256:568b434a36e31ed30d60d600b2227666ce150b8b5275948f50411481a4575d6d", + "sha256:5c393cd665d03ce6b29561edd6b0cc4bcb3fb8e2a7843e8f223d693f07f61b40", + "sha256:80072e9ba36c73cf89c01f669c7b123733fc2de1780b428082a850f53cc7865f", + "sha256:843f498e98ad1469ad54ecb4a7ccf48605a1c5d2bd26ae799c7a2cddab4a37ec", + "sha256:aa45443035651cbfae74c8deb53358ba660d8e7a5fbab3fc4beb33fb3e3ca4be", + "sha256:aaab817d9d038dd5f56a6fb2b2e8ae68caf1fd28cc6a963c755fa73268495c13", + "sha256:e6f68b9979dc8f75299293d682f67fecb72d78f98652da2eeb85c85edef1ca94", + "sha256:e7366cabddff3441d583fdc0176ab42eba4ee7090ef857d50c4dd59ad124003a", + "sha256:f0144ad97cd28bfdda0567b9278d25061ada5ad2b545b538cd3577697b32bda3", + "sha256:f655338491481f482042f19016647e50365ab41b75b486e0df56e0dcc425abf4" + ], + "version": "==3.9.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" } }, "develop": { From 269d7f8f9252801d1ed694f6f633a4f9b68e91fd Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 30 Sep 2019 10:11:29 -0700 Subject: [PATCH 025/462] create grids --- coretk/coretk/app.py | 6 +-- coretk/coretk/coretoolbar.py | 76 +++++++++++++++++++++++++---- coretk/coretk/graph.py | 93 +++++++++++++++++++++++++++++++++++- 3 files changed, 162 insertions(+), 13 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index a3fa4e34..1b27587f 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -21,7 +21,7 @@ class Application(tk.Frame): def setup_app(self): self.master.title("CORE") - self.master.geometry("800x600") + self.master.geometry("1000x800") image = Images.get("core") self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) @@ -40,11 +40,11 @@ class Application(tk.Frame): core_editbar.create_toolbar() self.canvas = CanvasGraph( - self, background="#cccccc", scrollregion=(0, 0, 1000, 1000) + master=self, background="#cccccc", scrollregion=(0, 0, 1000, 1000) ) self.canvas.pack(fill=tk.BOTH, expand=True) - # self.canvas.create_line(0, 0, 10, 10) + # self.canvas.create_rectangle(0, 0, 1000, 750, outline="#000000", fill="#ffffff", width=1) scroll_x = tk.Scrollbar( self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 74d7979b..df646488 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -1,6 +1,7 @@ import logging import tkinter as tk +from coretk.graph import GraphMode from coretk.images import Images from coretk.tooltip import CreateToolTip @@ -34,6 +35,9 @@ class CoreToolbar(object): self.marker_option_menu = None self.network_layer_option_menu = None + # variables used by canvas graph + self.mode = GraphMode.SELECT + def load_toolbar_images(self): """ Load the images that appear in core toolbar @@ -66,6 +70,24 @@ class CoreToolbar(object): Images.load("stop", "stop.gif") Images.load("observe", "observe.gif") + def get_graph_mode(self): + """ + Retrieve current graph mode + + :rtype: int + :return: current graph mode + """ + return self.mode + + def set_graph_mode(self, mode): + """ + Set graph mode + + :param int mode: graph mode + :return: nothing + """ + self.mode = mode + def destroy_previous_frame(self): """ Destroy any extra frame from previous before drawing a new one @@ -94,7 +116,7 @@ class CoreToolbar(object): if i.winfo_name() != "!frame": i.destroy() - def create_button(self, img, func, frame, main_button): + def create_button(self, img, func, frame, main_button, btt_message): """ Create button and put it on the frame @@ -106,6 +128,7 @@ class CoreToolbar(object): """ button = tk.Button(frame, width=self.width, height=self.height, image=img) button.pack(side=tk.LEFT, pady=1) + CreateToolTip(button, btt_message) button.bind("", lambda mb: func(main_button)) def create_radio_button(self, frame, image, func, variable, value, tooltip_msg): @@ -122,11 +145,12 @@ class CoreToolbar(object): button.pack(side=tk.TOP, pady=1) CreateToolTip(button, tooltip_msg) - def create_regular_button(self, frame, image, func): + def create_regular_button(self, frame, image, func, btt_message): button = tk.Button( frame, width=self.width, height=self.height, image=image, command=func ) button.pack(side=tk.TOP, pady=1) + CreateToolTip(button, btt_message) def draw_button_menu_frame(self, edit_frame, option_frame, main_button): """ @@ -165,6 +189,7 @@ class CoreToolbar(object): def click_selection_tool(self): logging.debug("Click SELECTION TOOL") + self.set_graph_mode(GraphMode.SELECT) def click_start_stop_session_tool(self): logging.debug("Click START STOP SESSION button") @@ -173,6 +198,7 @@ class CoreToolbar(object): def click_link_tool(self): logging.debug("Click LINK button") + self.set_graph_mode(GraphMode.EDGE) def pick_router(self, main_button): self.network_layer_option_menu.destroy() @@ -237,9 +263,22 @@ class CoreToolbar(object): self.pick_ovs, self.pick_editnode, ] + tooltip_list = [ + "router", + "host", + "PC", + "mdr", + "prouter", + "OVS", + "edit node types", + ] for i in range(len(img_list)): self.create_button( - img_list[i], func_list[i], option_frame, network_layer_button + img_list[i], + func_list[i], + option_frame, + network_layer_button, + tooltip_list[i], ) # place frame at a calculated position as well as keep a reference of that frame @@ -320,9 +359,20 @@ class CoreToolbar(object): self.pick_rj45, self.pick_tunnel, ] + tooltip_list = [ + "ethernet hub", + "ethernet switch", + "wireless LAN", + "rj45 physical interface tool", + "tunnel tool", + ] for i in range(len(img_list)): self.create_button( - img_list[i], func_list[i], option_frame, link_layer_button + img_list[i], + func_list[i], + option_frame, + link_layer_button, + tooltip_list[i], ) # place frame at a calculated position as well as keep a reference of the frame @@ -358,7 +408,6 @@ class CoreToolbar(object): self.marker_option_menu.destroy() main_button.configure(image=Images.get("marker")) logging.debug("Pick MARKER") - return "break" def pick_oval(self, main_button): self.marker_option_menu.destroy() @@ -397,8 +446,11 @@ class CoreToolbar(object): self.pick_rectangle, self.pick_text, ] + tooltip_list = ["marker", "oval", "rectangle", "text"] for i in range(len(img_list)): - self.create_button(img_list[i], func_list[i], option_frame, main_button) + self.create_button( + img_list[i], func_list[i], option_frame, main_button, tooltip_list[i] + ) # place the frame at a calculated position as well as keep a reference of that frame self.draw_button_menu_frame(self.edit_frame, option_frame, main_button) @@ -432,7 +484,10 @@ class CoreToolbar(object): def create_toolbar(self): self.load_toolbar_images() self.create_regular_button( - self.edit_frame, Images.get("start"), self.click_start_stop_session_tool + self.edit_frame, + Images.get("start"), + self.click_start_stop_session_tool, + "start the session", ) self.create_radio_button( self.edit_frame, @@ -503,7 +558,10 @@ class CoreToolbar(object): def create_runtime_toolbar(self): self.create_regular_button( - self.edit_frame, Images.get("stop"), self.click_stop_button + self.edit_frame, + Images.get("stop"), + self.click_stop_button, + "stop the session", ) self.create_radio_button( self.edit_frame, @@ -539,5 +597,5 @@ class CoreToolbar(object): "run command from one node to another", ) self.create_regular_button( - self.edit_frame, Images.get("run"), self.click_run_button + self.edit_frame, Images.get("run"), self.click_run_button, "run" ) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 9bd599e9..bf85b8cb 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -17,14 +17,42 @@ class CanvasGraph(tk.Canvas): cnf = {} kwargs["highlightthickness"] = 0 super().__init__(master, cnf, **kwargs) + self.mode = GraphMode.SELECT self.selected = None self.node_context = None self.nodes = {} self.edges = {} self.drawing_edge = None + self.setup_menus() self.setup_bindings() + self.draw_grid() + + def draw_grid(self, width=1000, height=750): + """ + Create grid + + :param int width: the width + :param int height: the height + + :return: nothing + """ + rectangle_id = self.create_rectangle( + 0, + 0, + width, + height, + outline="#000000", + fill="#ffffff", + width=1, + tags="rectangle", + ) + self.tag_lower(rectangle_id) + for i in range(0, width, 27): + self.create_line(i, 0, i, height, dash=(2, 4), tags="grid line") + for i in range(0, height, 27): + self.create_line(0, i, width, i, dash=(2, 4), tags="grid line") def setup_menus(self): self.node_context = tk.Menu(self.master) @@ -33,6 +61,11 @@ class CanvasGraph(tk.Canvas): self.node_context.add_command(label="Three") def setup_bindings(self): + """ + Bind any mouse events or hot keys to the matching action + + :return: nothing + """ self.bind("", self.click_press) self.bind("", self.click_release) self.bind("", self.click_motion) @@ -42,11 +75,25 @@ class CanvasGraph(tk.Canvas): self.bind("n", self.set_mode) def canvas_xy(self, event): + """ + Convert window coordinate to canvas coordinate + + :param event: + :rtype: (int, int) + :return: x, y canvas coordinate + """ x = self.canvasx(event.x) y = self.canvasy(event.y) return x, y def get_selected(self, event): + """ + Retrieve the item id that is on the mouse position + + :param event: mouse event + :rtype: int + :return: the item that the mouse point to + """ overlapping = self.find_overlapping(event.x, event.y, event.x, event.y) nodes = set(self.find_withtag("node")) selected = None @@ -64,6 +111,12 @@ class CanvasGraph(tk.Canvas): return selected def click_release(self, event): + """ + Draw a node or finish drawing an edge according to the current graph mode + + :param event: mouse event + :return: nothing + """ self.focus_set() self.selected = self.get_selected(event) logging.debug(f"click release selected: {self.selected}") @@ -109,6 +162,12 @@ class CanvasGraph(tk.Canvas): logging.debug(f"edges: {self.find_withtag('edge')}") def click_press(self, event): + """ + Start drawing an edge if mouse click is on a node + + :param event: mouse event + :return: nothing + """ logging.debug(f"click press: {event}") selected = self.get_selected(event) is_node = selected in self.find_withtag("node") @@ -117,6 +176,12 @@ class CanvasGraph(tk.Canvas): self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) def click_motion(self, event): + """ + Redraw drawing edge according to the current position of the mouse + + :param event: mouse event + :return: nothing + """ if self.mode == GraphMode.EDGE and self.drawing_edge is not None: x2, y2 = self.canvas_xy(event) x1, y1, _, _ = self.coords(self.drawing_edge.id) @@ -130,6 +195,12 @@ class CanvasGraph(tk.Canvas): self.node_context.post(event.x_root, event.y_root) def set_mode(self, event): + """ + Set canvas mode according to the hot key that has been pressed + + :param event: key event + :return: nothing + """ logging.debug(f"mode event: {event}") if event.char == "e": self.mode = GraphMode.EDGE @@ -147,21 +218,41 @@ class CanvasGraph(tk.Canvas): class CanvasEdge: + """ + Canvas edge class + """ + width = 3 def __init__(self, x1, y1, x2, y2, src, canvas): + """ + Create an instance of canvas edge object + :param int x1: source x-coord + :param int y1: source y-coord + :param int x2: destination x-coord + :param int y2: destination y-coord + :param int src: source id + :param tkinter.Canvas canvas: canvas object + """ self.src = src self.dst = None self.canvas = canvas self.id = self.canvas.create_line(x1, y1, x2, y2, tags="edge", width=self.width) self.token = None - self.canvas.tag_lower(self.id) + + # TODO resolve this + # self.canvas.tag_lower(self.id) def complete(self, dst, x, y): self.dst = dst self.token = tuple(sorted((self.src, self.dst))) x1, y1, _, _ = self.canvas.coords(self.id) self.canvas.coords(self.id, x1, y1, x, y) + self.canvas.lift(self.src) + self.canvas.lift(self.dst) + # self.canvas.create_line(0,0,10,10) + # print(x1,y1,x,y) + # self.canvas.create_line(x1+1, y1+1, x+1, y+1) def delete(self): self.canvas.delete(self.id) From cbd593eed6034aa72c0b4d73bbfe1d1c60a44a72 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 1 Oct 2019 16:25:26 -0700 Subject: [PATCH 026/462] finish the basics of toolbar and start working on simple grpc --- coretk/coretk/app.py | 17 ++++- coretk/coretk/coregrpc.py | 83 +++++++++++++++++++++++++ coretk/coretk/coretoolbar.py | 116 +++++++++++++++++------------------ coretk/coretk/graph.py | 105 +++++++++++++++++++++---------- coretk/coretk/images.py | 28 +++++++++ 5 files changed, 254 insertions(+), 95 deletions(-) create mode 100644 coretk/coretk/coregrpc.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 1b27587f..9102f4d1 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,6 +1,8 @@ import logging import tkinter as tk +import coretk.images as images +from coretk.coregrpc import CoreGrpc from coretk.coremenubar import CoreMenubar from coretk.coretoolbar import CoreToolbar from coretk.graph import CanvasGraph @@ -13,11 +15,16 @@ class Application(tk.Frame): self.load_images() self.setup_app() self.menubar = None + self.canvas = None + self.core_grpc = CoreGrpc() self.create_menu() self.create_widgets() def load_images(self): - Images.load("core", "core-icon.png") + images.load_core_images(Images) + + def close_grpc(self): + self.core_grpc.close() def setup_app(self): self.master.title("CORE") @@ -40,11 +47,14 @@ class Application(tk.Frame): core_editbar.create_toolbar() self.canvas = CanvasGraph( - master=self, background="#cccccc", scrollregion=(0, 0, 1000, 1000) + grpc=self.core_grpc, + master=self, + background="#cccccc", + scrollregion=(0, 0, 1000, 1000), ) self.canvas.pack(fill=tk.BOTH, expand=True) - # self.canvas.create_rectangle(0, 0, 1000, 750, outline="#000000", fill="#ffffff", width=1) + core_editbar.update_canvas(self.canvas) scroll_x = tk.Scrollbar( self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview @@ -69,3 +79,4 @@ if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) app = Application() app.mainloop() + app.close_grpc() diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py new file mode 100644 index 00000000..268c3a13 --- /dev/null +++ b/coretk/coretk/coregrpc.py @@ -0,0 +1,83 @@ +""" +Incorporate grpc into python tkinter GUI +""" +import logging + +from core.api.grpc import client, core_pb2 + + +class CoreGrpc: + def __init__(self): + """ + Create a CoreGrpc instance + """ + self.core = client.CoreGrpcClient() + self.session_id = None + self.set_up() + + def log_event(self, event): + logging.info("event: %s", event) + + def set_up(self): + """ + Create session, handle events session may broadcast, change session state + + :return: nothing + """ + self.core.connect() + # create session + response = self.core.create_session() + logging.info("created session: %s", response) + + # handle events session may broadcast + self.session_id = response.session_id + self.core.events(self.session_id, self.log_event) + + # change session state + response = self.core.set_session_state( + self.session_id, core_pb2.SessionState.CONFIGURATION + ) + logging.info("set session state: %s", response) + + def get_session_id(self): + return self.session_id + + # TODO add checkings to the function + def add_node(self, x, y, node_name): + link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] + network_layer_nodes = ["default"] + node = None + if node_name in link_layer_nodes: + if node_name == "switch": + node = core_pb2.Node(type=core_pb2.NodeType.SWITCH) + elif node_name == "hub": + node = core_pb2.Node(type=core_pb2.NodeType.HUB) + elif node_name == "wlan": + node = core_pb2.Node(type=core_pb2.NodeType.WIRELESS_LAN) + elif node_name == "rj45": + node = core_pb2.Node(type=core_pb2.NodeType.RJ45) + elif node_name == "tunnel": + node = core_pb2.Node(type=core_pb2.NodeType.TUNNEL) + + elif node_name in network_layer_nodes: + position = core_pb2.Position(x=x, y=y) + node = core_pb2.Node(position=position) + else: + return + response = self.core.add_node(self.session_id, node) + logging.info("created %s: %s", node_name, response) + return response.node_id + + def edit_node(self, session_id, node_id, x, y): + position = core_pb2.Position(x=x, y=y) + response = self.core.edit_node(session_id, node_id, position) + logging.info("updated node id %s: %s", node_id, response) + + def close(self): + """ + Clean ups when done using grpc + + :return: nothing + """ + logging.debug("Close grpc") + self.core.close() diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index df646488..d4009f1b 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -36,57 +36,17 @@ class CoreToolbar(object): self.network_layer_option_menu = None # variables used by canvas graph - self.mode = GraphMode.SELECT + self.image_to_draw = None + self.canvas = None - def load_toolbar_images(self): + def update_canvas(self, canvas): """ - Load the images that appear in core toolbar + Update canvas variable in CoreToolbar class + :param tkinter.Canvas canvas: core canvas :return: nothing """ - Images.load("core", "core-icon.png") - Images.load("start", "start.gif") - Images.load("switch", "lanswitch.gif") - Images.load("marker", "marker.gif") - Images.load("router", "router.gif") - Images.load("select", "select.gif") - Images.load("link", "link.gif") - Images.load("hub", "hub.gif") - Images.load("wlan", "wlan.gif") - Images.load("rj45", "rj45.gif") - Images.load("tunnel", "tunnel.gif") - Images.load("oval", "oval.gif") - Images.load("rectangle", "rectangle.gif") - Images.load("text", "text.gif") - Images.load("host", "host.gif") - Images.load("pc", "pc.gif") - Images.load("mdr", "mdr.gif") - Images.load("prouter", "router_green.gif") - Images.load("ovs", "OVS.gif") - Images.load("editnode", "document-properties.gif") - Images.load("run", "run.gif") - Images.load("plot", "plot.gif") - Images.load("twonode", "twonode.gif") - Images.load("stop", "stop.gif") - Images.load("observe", "observe.gif") - - def get_graph_mode(self): - """ - Retrieve current graph mode - - :rtype: int - :return: current graph mode - """ - return self.mode - - def set_graph_mode(self, mode): - """ - Set graph mode - - :param int mode: graph mode - :return: nothing - """ - self.mode = mode + self.canvas = canvas def destroy_previous_frame(self): """ @@ -189,47 +149,67 @@ class CoreToolbar(object): def click_selection_tool(self): logging.debug("Click SELECTION TOOL") - self.set_graph_mode(GraphMode.SELECT) + self.canvas.mode = GraphMode.SELECT def click_start_stop_session_tool(self): logging.debug("Click START STOP SESSION button") self.destroy_children_widgets(self.edit_frame) + self.canvas.set_canvas_mode(GraphMode.SELECT) self.create_runtime_toolbar() def click_link_tool(self): logging.debug("Click LINK button") - self.set_graph_mode(GraphMode.EDGE) + self.canvas.set_canvas_mode(GraphMode.EDGE) def pick_router(self, main_button): + logging.debug("Pick router option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("router")) - logging.debug("Pick router option") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("router")) + self.canvas.set_drawing_name("default") def pick_host(self, main_button): + logging.debug("Pick host option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("host")) - logging.debug("Pick host option") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("host")) + self.canvas.set_drawing_name("default") def pick_pc(self, main_button): + logging.debug("Pick PC option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("pc")) - logging.debug("Pick PC option") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("pc")) + self.canvas.set_drawing_name("default") def pick_mdr(self, main_button): + logging.debug("Pick MDR option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("mdr")) - logging.debug("Pick MDR option") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("mdr")) + self.canvas.set_drawing_name("default") def pick_prouter(self, main_button): + logging.debug("Pick prouter option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("prouter")) - logging.debug("Pick prouter option") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("prouter")) + self.canvas.set_drawing_name("default") def pick_ovs(self, main_button): + logging.debug("Pick OVS option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("ovs")) - logging.debug("Pick OVS option") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("ovs")) + self.canvas.set_drawing_name("default") + # TODO what graph node is this def pick_editnode(self, main_button): self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("editnode")) @@ -311,29 +291,43 @@ class CoreToolbar(object): CreateToolTip(network_layer_button, "Network-layer virtual nodes") def pick_hub(self, main_button): + logging.debug("Pick link-layer node HUB") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("hub")) - logging.debug("Pick link-layer node HUB") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("hub")) + self.canvas.set_drawing_name("hub") def pick_switch(self, main_button): + logging.debug("Pick link-layer node SWITCH") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("switch")) - logging.debug("Pick link-layer node SWITCH") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("switch")) + self.canvas.set_drawing_name("switch") def pick_wlan(self, main_button): + logging.debug("Pick link-layer node WLAN") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("wlan")) - logging.debug("Pick link-layer node WLAN") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("wlan")) + self.canvas.set_drawing_name("wlan") def pick_rj45(self, main_button): + logging.debug("Pick link-layer node RJ45") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("rj45")) - logging.debug("Pick link-layer node RJ45") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("rj45")) def pick_tunnel(self, main_button): + logging.debug("Pick link-layer node TUNNEL") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("tunnel")) - logging.debug("Pick link-layer node TUNNEL") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("tunnel")) + self.canvas.set_drawing_image(Images.get("tunnel")) def draw_link_layer_options(self, link_layer_button): """ @@ -482,7 +476,7 @@ class CoreToolbar(object): CreateToolTip(marker_main_button, "background annotation tools") def create_toolbar(self): - self.load_toolbar_images() + # self.load_toolbar_images() self.create_regular_button( self.edit_frame, Images.get("start"), @@ -508,6 +502,7 @@ class CoreToolbar(object): self.create_network_layer_button() self.create_link_layer_button() self.create_marker_button() + self.radio_value.set(1) def create_observe_button(self): menu_button = tk.Menubutton( @@ -599,3 +594,4 @@ class CoreToolbar(object): self.create_regular_button( self.edit_frame, Images.get("run"), self.click_run_button, "run" ) + self.exec_radio_value.set(1) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index bf85b8cb..06f16488 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -2,23 +2,25 @@ import enum import logging import tkinter as tk -from coretk.images import Images - class GraphMode(enum.Enum): SELECT = 0 EDGE = 1 - NODE = 2 + PICKNODE = 2 + NODE = 3 + OTHER = 4 class CanvasGraph(tk.Canvas): - def __init__(self, master=None, cnf=None, **kwargs): + def __init__(self, grpc=None, master=None, cnf=None, **kwargs): if cnf is None: cnf = {} kwargs["highlightthickness"] = 0 super().__init__(master, cnf, **kwargs) - + self.core_grpc = grpc self.mode = GraphMode.SELECT + self.draw_node_image = None + self.draw_node_name = None self.selected = None self.node_context = None self.nodes = {} @@ -29,6 +31,15 @@ class CanvasGraph(tk.Canvas): self.setup_bindings() self.draw_grid() + def set_canvas_mode(self, mode): + self.mode = mode + + def set_drawing_image(self, img): + self.draw_node_image = img + + def set_drawing_name(self, name): + self.draw_node_name = name + def draw_grid(self, width=1000, height=750): """ Create grid @@ -70,9 +81,9 @@ class CanvasGraph(tk.Canvas): self.bind("", self.click_release) self.bind("", self.click_motion) self.bind("", self.context) - self.bind("e", self.set_mode) - self.bind("s", self.set_mode) - self.bind("n", self.set_mode) + # self.bind("e", self.set_mode) + # self.bind("s", self.set_mode) + # self.bind("n", self.set_mode) def canvas_xy(self, event): """ @@ -124,7 +135,9 @@ class CanvasGraph(tk.Canvas): self.handle_edge_release(event) elif self.mode == GraphMode.NODE: x, y = self.canvas_xy(event) - self.add_node(x, y, "switch") + self.add_node(x, y, self.draw_node_image, self.draw_node_name) + elif self.mode == GraphMode.PICKNODE: + self.mode = GraphMode.NODE def handle_edge_release(self, event): edge = self.drawing_edge @@ -194,25 +207,39 @@ class CanvasGraph(tk.Canvas): logging.debug(f"node context: {selected}") self.node_context.post(event.x_root, event.y_root) - def set_mode(self, event): - """ - Set canvas mode according to the hot key that has been pressed + # def set_mode(self, event): + # """ + # Set canvas mode according to the hot key that has been pressed + # + # :param event: key event + # :return: nothing + # """ + # logging.debug(f"mode event: {event}") + # if event.char == "e": + # self.mode = GraphMode.EDGE + # elif event.char == "s": + # self.mode = GraphMode.SELECT + # elif event.char == "n": + # self.mode = GraphMode.NODE + # logging.debug(f"graph mode: {self.mode}") - :param event: key event - :return: nothing - """ - logging.debug(f"mode event: {event}") - if event.char == "e": - self.mode = GraphMode.EDGE - elif event.char == "s": - self.mode = GraphMode.SELECT - elif event.char == "n": - self.mode = GraphMode.NODE - logging.debug(f"graph mode: {self.mode}") + # def add_node(self, x, y, image_name): + # image = Images.get(image_name) + # node = CanvasNode(x, y, image, self) + # self.nodes[node.id] = node + # return node - def add_node(self, x, y, image_name): - image = Images.get(image_name) - node = CanvasNode(x, y, image, self) + def add_node(self, x, y, image, node_name): + core_session_id = self.core_grpc.get_session_id() + core_node_id = self.core_grpc.add_node(int(x), int(y), node_name) + node = CanvasNode( + core_session_id=core_session_id, + core_node_id=core_node_id, + x=x, + y=y, + image=image, + canvas=self, + ) self.nodes[node.id] = node return node @@ -250,22 +277,23 @@ class CanvasEdge: self.canvas.coords(self.id, x1, y1, x, y) self.canvas.lift(self.src) self.canvas.lift(self.dst) - # self.canvas.create_line(0,0,10,10) - # print(x1,y1,x,y) - # self.canvas.create_line(x1+1, y1+1, x+1, y+1) def delete(self): self.canvas.delete(self.id) class CanvasNode: - def __init__(self, x, y, image, canvas): + def __init__(self, core_session_id, core_node_id, x, y, image, canvas): self.image = image self.canvas = canvas self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags="node" ) - self.name = f"Node {self.id}" + self.x_coord = x + self.y_coord = y + self.core_session_id = core_session_id + self.core_node_id = core_node_id + self.name = f"Node {self.core_node_id}" self.text_id = self.canvas.create_text(x, y + 20, text=self.name) self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) @@ -274,16 +302,29 @@ class CanvasNode: self.edges = set() self.moving = None + def get_coords(self): + return self.x_coord, self.y_coord + + def update_coords(self): + self.x_coord, self.y_coord = self.canvas.coords(self.id) + def click_press(self, event): logging.debug(f"click press {self.name}: {event}") self.moving = self.canvas.canvas_xy(event) def click_release(self, event): logging.debug(f"click release {self.name}: {event}") + self.update_coords() + self.canvas.core_grpc.edit_node( + self.core_session_id, + self.core_node_id, + int(self.x_coord), + int(self.y_coord), + ) self.moving = None def motion(self, event): - if self.canvas.mode == GraphMode.EDGE: + if self.canvas.mode == GraphMode.EDGE or self.canvas.mode == GraphMode.NODE: return x, y = self.canvas.canvas_xy(event) moving_x, moving_y = self.moving diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 4ca2b5ac..71758cc2 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -18,3 +18,31 @@ class Images: @classmethod def get(cls, name): return cls.images[name] + + +def load_core_images(images): + images.load("core", "core-icon.png") + images.load("start", "start.gif") + images.load("switch", "lanswitch.gif") + images.load("marker", "marker.gif") + images.load("router", "router.gif") + images.load("select", "select.gif") + images.load("link", "link.gif") + images.load("hub", "hub.gif") + images.load("wlan", "wlan.gif") + images.load("rj45", "rj45.gif") + images.load("tunnel", "tunnel.gif") + images.load("oval", "oval.gif") + images.load("rectangle", "rectangle.gif") + images.load("text", "text.gif") + images.load("host", "host.gif") + images.load("pc", "pc.gif") + images.load("mdr", "mdr.gif") + images.load("prouter", "router_green.gif") + images.load("ovs", "OVS.gif") + images.load("editnode", "document-properties.gif") + images.load("run", "run.gif") + images.load("plot", "plot.gif") + images.load("twonode", "twonode.gif") + images.load("stop", "stop.gif") + images.load("observe", "observe.gif") From cb03aa261a5ba5f47ca0c2404ea1210aea6c8dba Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 3 Oct 2019 16:50:49 -0700 Subject: [PATCH 027/462] some work on grpc add nodes and links, some work on query session, redraw nodes --- coretk/coretk/app.py | 13 ++- coretk/coretk/coregrpc.py | 114 +++++++++++++++++------ coretk/coretk/coretoolbar.py | 93 +++++++++++-------- coretk/coretk/graph.py | 109 +++++++++------------- coretk/coretk/grpcmanagement.py | 157 ++++++++++++++++++++++++++++++++ coretk/coretk/images.py | 38 ++++++++ 6 files changed, 387 insertions(+), 137 deletions(-) create mode 100644 coretk/coretk/grpcmanagement.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 9102f4d1..9a9506e2 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -16,16 +16,20 @@ class Application(tk.Frame): self.setup_app() self.menubar = None self.canvas = None + + # start grpc self.core_grpc = CoreGrpc() + self.create_menu() self.create_widgets() def load_images(self): + """ + Load core images + :return: + """ images.load_core_images(Images) - def close_grpc(self): - self.core_grpc.close() - def setup_app(self): self.master.title("CORE") self.master.geometry("1000x800") @@ -47,8 +51,8 @@ class Application(tk.Frame): core_editbar.create_toolbar() self.canvas = CanvasGraph( - grpc=self.core_grpc, master=self, + grpc=self.core_grpc, background="#cccccc", scrollregion=(0, 0, 1000, 1000), ) @@ -79,4 +83,3 @@ if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) app = Application() app.mainloop() - app.close_grpc() diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 268c3a13..8596366c 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -14,18 +14,20 @@ class CoreGrpc: self.core = client.CoreGrpcClient() self.session_id = None self.set_up() + self.interface_helper = None def log_event(self, event): logging.info("event: %s", event) - def set_up(self): + def redraw_canvas(self): + return + + def create_new_session(self): """ - Create session, handle events session may broadcast, change session state + Create a new session :return: nothing """ - self.core.connect() - # create session response = self.core.create_session() logging.info("created session: %s", response) @@ -33,39 +35,67 @@ class CoreGrpc: self.session_id = response.session_id self.core.events(self.session_id, self.log_event) - # change session state + def query_existing_sessions(self, sessions): + """ + Query for existing sessions and prompt to join one + + :param repeated core_pb2.SessionSummary sessions: summaries of all the existing sessions + + :return: nothing + """ + for session in sessions: + logging.info("Session id: %s, Session state: %s", session.id, session.state) + logging.info("Input a session you want to enter from the keyboard:") + usr_input = int(input()) + if usr_input == 0: + self.create_new_session() + else: + response = self.core.get_session(usr_input) + self.session_id = usr_input + # self.core.events(self.session_id, self.log_event) + logging.info("Entering session_id %s.... Result: %s", usr_input, response) + + def set_up(self): + """ + Query sessions, if there exist any, promt whether to join one + + :return: nothing + """ + self.core.connect() + + response = self.core.get_sessions() + logging.info("all sessions: %s", response) + + # if there are no sessions, create a new session, else join a session + sessions = response.sessions + + if len(sessions) == 0: + self.create_new_session() + else: + # self.create_new_session() + self.query_existing_sessions(sessions) + + def set_configuration_state(self): response = self.core.set_session_state( self.session_id, core_pb2.SessionState.CONFIGURATION ) logging.info("set session state: %s", response) + def set_instantiate_state(self): + response = self.core.set_session_state( + self.session_id, core_pb2.SessionState.INSTANTIATION + ) + logging.info("set session state: %s", response) + def get_session_id(self): return self.session_id - # TODO add checkings to the function - def add_node(self, x, y, node_name): - link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] - network_layer_nodes = ["default"] - node = None - if node_name in link_layer_nodes: - if node_name == "switch": - node = core_pb2.Node(type=core_pb2.NodeType.SWITCH) - elif node_name == "hub": - node = core_pb2.Node(type=core_pb2.NodeType.HUB) - elif node_name == "wlan": - node = core_pb2.Node(type=core_pb2.NodeType.WIRELESS_LAN) - elif node_name == "rj45": - node = core_pb2.Node(type=core_pb2.NodeType.RJ45) - elif node_name == "tunnel": - node = core_pb2.Node(type=core_pb2.NodeType.TUNNEL) - - elif node_name in network_layer_nodes: - position = core_pb2.Position(x=x, y=y) - node = core_pb2.Node(position=position) - else: - return + def add_node(self, node_type, model, x, y, name): + logging.info("ADD NODE %s", name) + position = core_pb2.Position(x=x, y=y) + node = core_pb2.Node(type=node_type, position=position, model=model, image=name) response = self.core.add_node(self.session_id, node) - logging.info("created %s: %s", node_name, response) + logging.info("created node: %s", response) return response.node_id def edit_node(self, session_id, node_id, x, y): @@ -73,6 +103,34 @@ class CoreGrpc: response = self.core.edit_node(session_id, node_id, position) logging.info("updated node id %s: %s", node_id, response) + # def create_interface_helper(self): + # self.interface_helper = self.core.InterfaceHelper(ip4_prefix="10.83.0.0/16") + + # TODO case for other core_pb2.NodeType + def add_link(self, id1, id2, type1, type2): + """ + Grpc client request add link + + :param int session_id: session id + :param int id1: node 1 core id + :param core_pb2.NodeType type1: node 1 core node type + :param int id2: node 2 core id + :param core_pb2.NodeType type2: node 2 core node type + :return: nothing + """ + if not self.interface_helper: + logging.debug("INTERFACE HELPER NOT CREATED YET, CREATING ONE...") + self.interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/16") + + interface1 = None + interface2 = None + if type1 == core_pb2.NodeType.DEFAULT: + interface1 = self.interface_helper.create_interface(id1, 0) + if type2 == core_pb2.NodeType.DEFAULT: + interface2 = self.interface_helper.create_interface(id2, 0) + response = self.core.add_link(self.session_id, id1, id2, interface1, interface2) + logging.info("created link: %s", response) + def close(self): """ Clean ups when done using grpc diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index d4009f1b..8af43240 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -27,7 +27,6 @@ class CoreToolbar(object): self.width = 32 self.height = 32 - # Used for drawing the horizontally displayed menu items for network-layer nodes and link-layer node self.selection_tool_button = None # Reference to the option menus @@ -35,8 +34,6 @@ class CoreToolbar(object): self.marker_option_menu = None self.network_layer_option_menu = None - # variables used by canvas graph - self.image_to_draw = None self.canvas = None def update_canvas(self, canvas): @@ -151,63 +148,79 @@ class CoreToolbar(object): logging.debug("Click SELECTION TOOL") self.canvas.mode = GraphMode.SELECT - def click_start_stop_session_tool(self): + def click_start_session_tool(self): logging.debug("Click START STOP SESSION button") self.destroy_children_widgets(self.edit_frame) - self.canvas.set_canvas_mode(GraphMode.SELECT) + self.canvas.mode = GraphMode.SELECT self.create_runtime_toolbar() + # set configuration state + self.canvas.core_grpc.set_configuration_state() + + # grpc client requests creating nodes + for node in self.canvas.grpc_manager.nodes_to_create.values(): + self.canvas.core_grpc.add_node( + node.type, node.model, int(node.x), int(node.y), node.name + ) + self.canvas.grpc_manager.nodes_to_create.clear() + + for edge in self.canvas.grpc_manager.edges_to_create.values(): + self.canvas.core_grpc.add_link(edge.id1, edge.id2, edge.type1, edge.type2) + self.canvas.grpc_manager.edges_to_create.clear() + + self.canvas.core_grpc.set_instantiate_state() + def click_link_tool(self): logging.debug("Click LINK button") - self.canvas.set_canvas_mode(GraphMode.EDGE) + self.canvas.mode = GraphMode.EDGE def pick_router(self, main_button): logging.debug("Pick router option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("router")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("router")) - self.canvas.set_drawing_name("default") + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("router") + self.canvas.draw_node_name = "router" def pick_host(self, main_button): logging.debug("Pick host option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("host")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("host")) - self.canvas.set_drawing_name("default") + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("host") + self.canvas.draw_node_name = "host" def pick_pc(self, main_button): logging.debug("Pick PC option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("pc")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("pc")) - self.canvas.set_drawing_name("default") + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("pc") + self.canvas.draw_node_name = "PC" def pick_mdr(self, main_button): logging.debug("Pick MDR option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("mdr")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("mdr")) - self.canvas.set_drawing_name("default") + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("mdr") + self.canvas.draw_node_name = "mdr" def pick_prouter(self, main_button): logging.debug("Pick prouter option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("prouter")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("prouter")) - self.canvas.set_drawing_name("default") + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("prouter") + self.canvas.draw_node_name = "prouter" def pick_ovs(self, main_button): logging.debug("Pick OVS option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("ovs")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("ovs")) - self.canvas.set_drawing_name("default") + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("ovs") + self.canvas.draw_node_name = "OVS" # TODO what graph node is this def pick_editnode(self, main_button): @@ -294,40 +307,41 @@ class CoreToolbar(object): logging.debug("Pick link-layer node HUB") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("hub")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("hub")) - self.canvas.set_drawing_name("hub") + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("hub") + self.canvas.draw_node_name = "hub" def pick_switch(self, main_button): logging.debug("Pick link-layer node SWITCH") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("switch")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("switch")) - self.canvas.set_drawing_name("switch") + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("switch") + self.canvas.draw_node_name = "switch" def pick_wlan(self, main_button): logging.debug("Pick link-layer node WLAN") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("wlan")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("wlan")) - self.canvas.set_drawing_name("wlan") + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("wlan") + self.canvas.draw_node_name = "wlan" def pick_rj45(self, main_button): logging.debug("Pick link-layer node RJ45") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("rj45")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("rj45")) + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("rj45") + self.canvas.draw_node_name = "rj45" def pick_tunnel(self, main_button): logging.debug("Pick link-layer node TUNNEL") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("tunnel")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("tunnel")) - self.canvas.set_drawing_image(Images.get("tunnel")) + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("tunnel") + self.canvas.draw_node_name = "tunnel" def draw_link_layer_options(self, link_layer_button): """ @@ -476,11 +490,10 @@ class CoreToolbar(object): CreateToolTip(marker_main_button, "background annotation tools") def create_toolbar(self): - # self.load_toolbar_images() self.create_regular_button( self.edit_frame, Images.get("start"), - self.click_start_stop_session_tool, + self.click_start_session_tool, "start the session", ) self.create_radio_button( diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 06f16488..5a25188b 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -2,6 +2,9 @@ import enum import logging import tkinter as tk +from coretk.grpcmanagement import GrpcManager +from coretk.images import Images + class GraphMode(enum.Enum): SELECT = 0 @@ -12,12 +15,11 @@ class GraphMode(enum.Enum): class CanvasGraph(tk.Canvas): - def __init__(self, grpc=None, master=None, cnf=None, **kwargs): + def __init__(self, master=None, grpc=None, cnf=None, **kwargs): if cnf is None: cnf = {} kwargs["highlightthickness"] = 0 super().__init__(master, cnf, **kwargs) - self.core_grpc = grpc self.mode = GraphMode.SELECT self.draw_node_image = None self.draw_node_name = None @@ -26,19 +28,32 @@ class CanvasGraph(tk.Canvas): self.nodes = {} self.edges = {} self.drawing_edge = None - self.setup_menus() self.setup_bindings() self.draw_grid() + self.core_grpc = grpc + self.grpc_manager = GrpcManager() + self.draw_existing_node() - def set_canvas_mode(self, mode): - self.mode = mode + def setup_menus(self): + self.node_context = tk.Menu(self.master) + self.node_context.add_command(label="One") + self.node_context.add_command(label="Two") + self.node_context.add_command(label="Three") - def set_drawing_image(self, img): - self.draw_node_image = img + def setup_bindings(self): + """ + Bind any mouse events or hot keys to the matching action - def set_drawing_name(self, name): - self.draw_node_name = name + :return: nothing + """ + self.bind("", self.click_press) + self.bind("", self.click_release) + self.bind("", self.click_motion) + self.bind("", self.context) + # self.bind("e", self.set_mode) + # self.bind("s", self.set_mode) + # self.bind("n", self.set_mode) def draw_grid(self, width=1000, height=750): """ @@ -65,25 +80,20 @@ class CanvasGraph(tk.Canvas): for i in range(0, height, 27): self.create_line(0, i, width, i, dash=(2, 4), tags="grid line") - def setup_menus(self): - self.node_context = tk.Menu(self.master) - self.node_context.add_command(label="One") - self.node_context.add_command(label="Two") - self.node_context.add_command(label="Three") - - def setup_bindings(self): + def draw_existing_node(self): """ - Bind any mouse events or hot keys to the matching action + Draw existing node while updating the grpc manager based on that :return: nothing """ - self.bind("", self.click_press) - self.bind("", self.click_release) - self.bind("", self.click_motion) - self.bind("", self.context) - # self.bind("e", self.set_mode) - # self.bind("s", self.set_mode) - # self.bind("n", self.set_mode) + session_id = self.core_grpc.session_id + response = self.core_grpc.core.get_session(session_id) + nodes = response.session.nodes + if len(nodes) > 0: + for node in nodes: + image = Images.convert_type_and_model_to_image(node.type, node.model) + return image + # n = CanvasNode(node.position.x, node.position.y, image, self, node.id) def canvas_xy(self, event): """ @@ -172,6 +182,10 @@ class CanvasGraph(tk.Canvas): node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) + self.grpc_manager.add_edge( + self.core_grpc.get_session_id(), edge.token, node_src.id, node_dst.id + ) + logging.debug(f"edges: {self.find_withtag('edge')}") def click_press(self, event): @@ -207,40 +221,14 @@ class CanvasGraph(tk.Canvas): logging.debug(f"node context: {selected}") self.node_context.post(event.x_root, event.y_root) - # def set_mode(self, event): - # """ - # Set canvas mode according to the hot key that has been pressed - # - # :param event: key event - # :return: nothing - # """ - # logging.debug(f"mode event: {event}") - # if event.char == "e": - # self.mode = GraphMode.EDGE - # elif event.char == "s": - # self.mode = GraphMode.SELECT - # elif event.char == "n": - # self.mode = GraphMode.NODE - # logging.debug(f"graph mode: {self.mode}") - - # def add_node(self, x, y, image_name): - # image = Images.get(image_name) - # node = CanvasNode(x, y, image, self) - # self.nodes[node.id] = node - # return node - def add_node(self, x, y, image, node_name): - core_session_id = self.core_grpc.get_session_id() - core_node_id = self.core_grpc.add_node(int(x), int(y), node_name) node = CanvasNode( - core_session_id=core_session_id, - core_node_id=core_node_id, - x=x, - y=y, - image=image, - canvas=self, + x=x, y=y, image=image, canvas=self, core_id=self.grpc_manager.peek_id() ) self.nodes[node.id] = node + self.grpc_manager.add_node( + self.core_grpc.get_session_id(), node.id, x, y, node_name + ) return node @@ -283,17 +271,16 @@ class CanvasEdge: class CanvasNode: - def __init__(self, core_session_id, core_node_id, x, y, image, canvas): + def __init__(self, x, y, image, canvas, core_id): self.image = image self.canvas = canvas self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags="node" ) + self.core_id = core_id self.x_coord = x self.y_coord = y - self.core_session_id = core_session_id - self.core_node_id = core_node_id - self.name = f"Node {self.core_node_id}" + self.name = f"Node {self.core_id}" self.text_id = self.canvas.create_text(x, y + 20, text=self.name) self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) @@ -315,12 +302,6 @@ class CanvasNode: def click_release(self, event): logging.debug(f"click release {self.name}: {event}") self.update_coords() - self.canvas.core_grpc.edit_node( - self.core_session_id, - self.core_node_id, - int(self.x_coord), - int(self.y_coord), - ) self.moving = None def motion(self, event): diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py new file mode 100644 index 00000000..d090f6d6 --- /dev/null +++ b/coretk/coretk/grpcmanagement.py @@ -0,0 +1,157 @@ +""" +Manage useful informations about the nodes, edges and configuration +that can be useful for grpc, acts like a session class +""" +import logging + +from core.api.grpc import core_pb2 + +link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] +network_layer_nodes = ["router", "host", "PC", "mdr", "prouter", "OVS"] + + +class Node: + def __init__(self, session_id, node_id, type, model, x, y, name): + """ + Create an instance of a node + + :param int session_id: session id + :param int node_id: node id + :param core_pb2.NodeType type: node type + :param int x: x coordinate + :param int y: coordinate + """ + self.session_id = session_id + self.node_id = node_id + self.type = type + self.x = x + self.y = y + self.model = model + self.name = name + + +class Edge: + def __init__(self, session_id, node_id_1, node_type_1, node_id_2, node_type_2): + """ + Create an instance of an edge + :param int session_id: session id + :param int node_id_1: node 1 id + :param int node_type_1: node 1 type + :param core_pb2.NodeType node_id_2: node 2 id + :param core_pb2.NodeType node_type_2: node 2 type + """ + self.session_id = session_id + self.id1 = node_id_1 + self.id2 = node_id_2 + self.type1 = node_type_1 + self.type2 = node_type_2 + + +class GrpcManager: + def __init__(self): + self.nodes = {} + self.edges = {} + self.id = 1 + # A list of id for re-use, keep in increasing order + self.reusable = [] + self.nodes_to_create = {} + self.edges_to_create = {} + + def peek_id(self): + """ + Peek the next id to be used + + :return: nothing + """ + if len(self.reusable) == 0: + return self.id + else: + return self.reusable[0] + + def get_id(self): + """ + Get the next node id as well as update id status and reusable ids + + :rtype: int + :return: the next id to be used + """ + if len(self.reusable) == 0: + new_id = self.id + self.id = self.id + 1 + return new_id + else: + return self.reusable.pop(0) + + # TODO figure out the name of ovs node + def add_node(self, session_id, canvas_id, x, y, name): + """ + Add node, with information filled in, to grpc manager + + :param int session_id: session id + :param int canvas_id: node's canvas id + :param int x: x coord + :param int y: y coord + :param str name: node type + :return: nothing + """ + node_type = None + node_model = None + if name in link_layer_nodes: + if name == "switch": + node_type = core_pb2.NodeType.SWITCH + elif name == "hub": + node_type = core_pb2.NodeType.HUB + elif name == "wlan": + node_type = core_pb2.NodeType.WIRELESS_LAN + elif name == "rj45": + node_type = core_pb2.NodeType.RJ45 + elif name == "tunnel": + node_type = core_pb2.NodeType.TUNNEL + elif name in network_layer_nodes: + node_type = core_pb2.NodeType.DEFAULT + # TODO what is the model name for ovs + node_model = name + else: + logging.error("grpcmanagemeny.py INVALID node name") + create_node = Node(session_id, self.get_id(), node_type, node_model, x, y, name) + self.nodes[canvas_id] = create_node + self.nodes_to_create[canvas_id] = create_node + logging.debug( + "Adding node to GrpcManager.. Session id: %s, Coords: (%s, %s), Name: %s", + session_id, + x, + y, + name, + ) + + def add_preexisting_node(self): + return + + def delete_node(self, canvas_id): + """ + Delete a node from the session + + :param int canvas_id: node's id in the canvas + :return: thing + """ + try: + self.nodes.pop(canvas_id) + self.reuseable.append(canvas_id) + self.reuseable.sort() + except KeyError: + logging.error("grpcmanagement.py INVALID NODE CANVAS ID") + + def add_edge(self, session_id, token, canvas_id_1, canvas_id_2): + if canvas_id_1 in self.nodes and canvas_id_2 in self.nodes: + edge = Edge( + session_id, + self.nodes[canvas_id_1].node_id, + self.nodes[canvas_id_1].type, + self.nodes[canvas_id_2].node_id, + self.nodes[canvas_id_2].type, + ) + self.edges[token] = edge + self.edges_to_create[token] = edge + logging.debug("Adding edge to grpc manager...") + else: + logging.error("grpcmanagement.py INVALID CANVAS NODE ID") diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 71758cc2..4911efac 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -1,7 +1,10 @@ +import logging import os from PIL import Image, ImageTk +from core.api.grpc import core_pb2 + PATH = os.path.abspath(os.path.dirname(__file__)) @@ -19,6 +22,41 @@ class Images: def get(cls, name): return cls.images[name] + @classmethod + def convert_type_and_model_to_image(cls, node_type, node_model): + """ + Retrieve image based on type and model + :param core_pb2.NodeType node_type: core node type + :param string node_model: the node model + + :return: the matching image + """ + if node_type == core_pb2.NodeType.SWITCH: + return Images.get("switch") + if node_type == core_pb2.NodeType.HUB: + return Images.get("hub") + if node_type == core_pb2.NodeType.WIRELESS_LAN: + return Images.get("wlan") + if node_type == core_pb2.NodeType.RJ45: + return Images.get("rj45") + if node_type == core_pb2.NodeType.TUNNEL: + return Images.get("tunnel") + if node_type == core_pb2.NodeType.DEFAULT: + if node_model == "router": + return Images.get("router") + if node_model == "host": + return Images.get(("host")) + if node_model == "PC": + return Images.get("pc") + if node_model == "mdr": + return Images.get("mdr") + if node_model == "prouter": + return Images.get("prouter") + if node_model == "OVS": + return Images.get("ovs") + else: + logging.debug("INVALID INPUT OR NOT CONSIDERED YET") + def load_core_images(images): images.load("core", "core-icon.png") From 7aa013d351980918ab4908dcdff524285ae7ab66 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 3 Oct 2019 20:38:32 -0700 Subject: [PATCH 028/462] start to wrapping commands to support remote ssh --- daemon/core/nodes/base.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index ff34984d..b655c70a 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -14,6 +14,8 @@ import threading from builtins import range from socket import AF_INET, AF_INET6 +from fabric import Connection + from core import constants, utils from core.emulator.data import LinkData, NodeData from core.emulator.enumerations import LinkTypes, NodeTypes @@ -82,17 +84,23 @@ class NodeBase(object): """ raise NotImplementedError - def net_cmd(self, args): + def net_cmd(self, args, env=None): """ Runs a command that is used to configure and setup the network on the host system. :param list[str]|str args: command to run + :param dict env: environment to run command with :return: combined stdout and stderr :rtype: str :raises CoreCommandError: when a non-zero exit status occurs """ - return utils.check_cmd(args) + if self.server is None: + return utils.check_cmd(args, env=env) + else: + args = " ".join(args) + result = Connection(self.server, user="root").run(args, hide=True) + return result.stderr def setposition(self, x=None, y=None, z=None): """ @@ -515,7 +523,7 @@ class CoreNode(CoreNodeBase): env["NODE_NUMBER"] = str(self.id) env["NODE_NAME"] = str(self.name) - output = utils.check_cmd(vnoded, env=env) + output = self.net_cmd(vnoded, env=env) self.pid = int(output) # create vnode client @@ -660,8 +668,8 @@ class CoreNode(CoreNodeBase): """ source = os.path.abspath(source) logging.debug("node(%s) mounting: %s at %s", self.name, source, target) - self.client.check_cmd(["mkdir", "-p", target]) - self.client.check_cmd([constants.MOUNT_BIN, "-n", "--bind", source, target]) + self.node_net_cmd(["mkdir", "-p", target]) + self.node_net_cmd([constants.MOUNT_BIN, "-n", "--bind", source, target]) self._mounts.append((source, target)) def newifindex(self): From 031517ba56c9e69cf31c1d3616d78a93c7aaaedc Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 4 Oct 2019 09:29:10 -0700 Subject: [PATCH 029/462] fixed base.py imports with isort --- daemon/core/nodes/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index b655c70a..7efad49e 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -14,14 +14,13 @@ import threading from builtins import range from socket import AF_INET, AF_INET6 -from fabric import Connection - from core import constants, utils from core.emulator.data import LinkData, NodeData from core.emulator.enumerations import LinkTypes, NodeTypes from core.nodes import client, ipaddress from core.nodes.interface import CoreInterface, TunTap, Veth from core.nodes.netclient import LinuxNetClient, OvsNetClient +from fabric import Connection _DEFAULT_MTU = 1500 From 8611106c93bd79b1bcc1923f5d64a4cd1b09d298 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 4 Oct 2019 16:52:07 -0700 Subject: [PATCH 030/462] basics on redraw components from prev section, work more on start/stop session --- coretk/coretk/coregrpc.py | 79 +++++++++++++++++++++++++++------ coretk/coretk/coretoolbar.py | 19 ++++---- coretk/coretk/graph.py | 46 ++++++++++++++----- coretk/coretk/grpcmanagement.py | 53 +++++++++++++++++----- coretk/coretk/images.py | 33 +++++++------- 5 files changed, 170 insertions(+), 60 deletions(-) diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 8596366c..bcd04c96 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -55,6 +55,10 @@ class CoreGrpc: # self.core.events(self.session_id, self.log_event) logging.info("Entering session_id %s.... Result: %s", usr_input, response) + def delete_session(self): + response = self.core.delete_session(self.session_id) + logging.info("Deleted session result: %s", response) + def set_up(self): """ Query sessions, if there exist any, promt whether to join one @@ -75,25 +79,59 @@ class CoreGrpc: # self.create_new_session() self.query_existing_sessions(sessions) - def set_configuration_state(self): - response = self.core.set_session_state( - self.session_id, core_pb2.SessionState.CONFIGURATION - ) - logging.info("set session state: %s", response) + def get_session_state(self): + response = self.core.get_session(self.session_id) + logging.info("get sessio: %s", response) + return response.session.state + + def set_session_state(self, state): + """ + Set session state + + :param str state: session state to set + :return: nothing + """ + response = None + if state == "configuration": + response = self.core.set_session_state( + self.session_id, core_pb2.SessionState.CONFIGURATION + ) + elif state == "instantiation": + response = self.core.set_session_state( + self.session_id, core_pb2.SessionState.INSTANTIATION + ) + elif state == "datacollect": + response = self.core.set_session_state( + self.session_id, core_pb2.SessionState.DATACOLLECT + ) + elif state == "shutdown": + response = self.core.set_session_state( + self.session_id, core_pb2.SessionState.SHUTDOWN + ) + elif state == "runtime": + response = self.core.set_session_state( + self.session_id, core_pb2.SessionState.RUNTIME + ) + elif state == "definition": + response = self.core.set_session_state( + self.session_id, core_pb2.SessionState.DEFINITION + ) + elif state == "none": + response = self.core.set_session_state( + self.session_id, core_pb2.SessionState.NONE + ) + else: + logging.error("coregrpc.py: set_session_state: INVALID STATE") - def set_instantiate_state(self): - response = self.core.set_session_state( - self.session_id, core_pb2.SessionState.INSTANTIATION - ) logging.info("set session state: %s", response) def get_session_id(self): return self.session_id - def add_node(self, node_type, model, x, y, name): - logging.info("ADD NODE %s", name) + def add_node(self, node_type, model, x, y, name, node_id): + logging.info("coregrpc.py ADD NODE %s", name) position = core_pb2.Position(x=x, y=y) - node = core_pb2.Node(type=node_type, position=position, model=model, image=name) + node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) response = self.core.add_node(self.session_id, node) logging.info("created node: %s", response) return response.node_id @@ -103,10 +141,25 @@ class CoreGrpc: response = self.core.edit_node(session_id, node_id, position) logging.info("updated node id %s: %s", node_id, response) + def delete_nodes(self): + for node in self.core.get_session(self.session_id).session.nodes: + response = self.core.delete_node(self.session_id, node.id) + logging.info("delete node %s", response) + # def create_interface_helper(self): # self.interface_helper = self.core.InterfaceHelper(ip4_prefix="10.83.0.0/16") - # TODO case for other core_pb2.NodeType + def delete_links(self): + for link in self.core.get_session(self.session_id).session.links: + response = self.core.delete_link( + self.session_id, + link.node_one_id, + link.node_two_id, + link.interface_one.id, + link.interface_two.id, + ) + logging.info("delete link %s", response) + def add_link(self, id1, id2, type1, type2): """ Grpc client request add link diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 8af43240..c59c4222 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -1,6 +1,7 @@ import logging import tkinter as tk +from core.api.grpc import core_pb2 from coretk.graph import GraphMode from coretk.images import Images from coretk.tooltip import CreateToolTip @@ -155,20 +156,19 @@ class CoreToolbar(object): self.create_runtime_toolbar() # set configuration state - self.canvas.core_grpc.set_configuration_state() + if self.canvas.core_grpc.get_session_state() == core_pb2.SessionState.SHUTDOWN: + self.canvas.core_grpc.set_session_state("definition") + self.canvas.core_grpc.set_session_state("configuration") - # grpc client requests creating nodes - for node in self.canvas.grpc_manager.nodes_to_create.values(): + for node in self.canvas.grpc_manager.nodes.values(): self.canvas.core_grpc.add_node( - node.type, node.model, int(node.x), int(node.y), node.name + node.type, node.model, int(node.x), int(node.y), node.name, node.node_id ) - self.canvas.grpc_manager.nodes_to_create.clear() - for edge in self.canvas.grpc_manager.edges_to_create.values(): + for edge in self.canvas.grpc_manager.edges.values(): self.canvas.core_grpc.add_link(edge.id1, edge.id2, edge.type1, edge.type2) - self.canvas.grpc_manager.edges_to_create.clear() - self.canvas.core_grpc.set_instantiate_state() + self.canvas.core_grpc.set_session_state("instantiation") def click_link_tool(self): logging.debug("Click LINK button") @@ -551,6 +551,9 @@ class CoreToolbar(object): logging.debug("Click on STOP button ") self.destroy_children_widgets(self.edit_frame) self.create_toolbar() + self.canvas.core_grpc.set_session_state("datacollect") + self.canvas.core_grpc.delete_links() + self.canvas.core_grpc.delete_nodes() def click_run_button(self): logging.debug("Click on RUN button") diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 5a25188b..3c984557 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -2,6 +2,7 @@ import enum import logging import tkinter as tk +from core.api.grpc import core_pb2 from coretk.grpcmanagement import GrpcManager from coretk.images import Images @@ -33,7 +34,7 @@ class CanvasGraph(tk.Canvas): self.draw_grid() self.core_grpc = grpc self.grpc_manager = GrpcManager() - self.draw_existing_node() + self.draw_existing_component() def setup_menus(self): self.node_context = tk.Menu(self.master) @@ -80,20 +81,40 @@ class CanvasGraph(tk.Canvas): for i in range(0, height, 27): self.create_line(0, i, width, i, dash=(2, 4), tags="grid line") - def draw_existing_node(self): + def draw_existing_component(self): """ - Draw existing node while updating the grpc manager based on that + Draw existing node and update the information in grpc manager to match :return: nothing """ + core_id_to_canvas_id = {} + session_id = self.core_grpc.session_id - response = self.core_grpc.core.get_session(session_id) - nodes = response.session.nodes - if len(nodes) > 0: - for node in nodes: - image = Images.convert_type_and_model_to_image(node.type, node.model) - return image - # n = CanvasNode(node.position.x, node.position.y, image, self, node.id) + session = self.core_grpc.core.get_session(session_id).session + # nodes = response.session.nodes + # if len(nodes) > 0: + for node in session.nodes: + # peer to peer node is not drawn on the GUI + if node.type != core_pb2.NodeType.PEER_TO_PEER: + image, name = Images.convert_type_and_model_to_image( + node.type, node.model + ) + n = CanvasNode(node.position.x, node.position.y, image, self, node.id) + self.nodes[n.id] = n + core_id_to_canvas_id[node.id] = n.id + self.grpc_manager.add_preexisting_node(n, session_id, node, name) + self.grpc_manager.update_reusable_id() + # draw existing links + # links = response.session.links + for link in session.links: + n1 = self.nodes[core_id_to_canvas_id[link.node_one_id]] + n2 = self.nodes[core_id_to_canvas_id[link.node_two_id]] + e = CanvasEdge(n1.x_coord, n1.y_coord, n2.x_coord, n2.y_coord, n1.id, self) + self.edges[e.token] = e + self.grpc_manager.add_edge(session_id, e.token, n1.id, n2.id) + # lift the nodes so they on top of the links + for i in core_id_to_canvas_id.values(): + self.lift(i) def canvas_xy(self, event): """ @@ -289,8 +310,9 @@ class CanvasNode: self.edges = set() self.moving = None - def get_coords(self): - return self.x_coord, self.y_coord + # + # def get_coords(self): + # return self.x_coord, self.y_coord def update_coords(self): self.x_coord, self.y_coord = self.canvas.coords(self.id) diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index d090f6d6..afbf8175 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -11,19 +11,20 @@ network_layer_nodes = ["router", "host", "PC", "mdr", "prouter", "OVS"] class Node: - def __init__(self, session_id, node_id, type, model, x, y, name): + def __init__(self, session_id, node_id, node_type, model, x, y, name): """ Create an instance of a node :param int session_id: session id :param int node_id: node id - :param core_pb2.NodeType type: node type + :param core_pb2.NodeType node_type: node type :param int x: x coordinate :param int y: coordinate + :param str name: node name """ self.session_id = session_id self.node_id = node_id - self.type = type + self.type = node_type self.x = x self.y = y self.model = model @@ -54,8 +55,8 @@ class GrpcManager: self.id = 1 # A list of id for re-use, keep in increasing order self.reusable = [] - self.nodes_to_create = {} - self.edges_to_create = {} + + self.preexisting = [] def peek_id(self): """ @@ -82,7 +83,6 @@ class GrpcManager: else: return self.reusable.pop(0) - # TODO figure out the name of ovs node def add_node(self, session_id, canvas_id, x, y, name): """ Add node, with information filled in, to grpc manager @@ -109,13 +109,11 @@ class GrpcManager: node_type = core_pb2.NodeType.TUNNEL elif name in network_layer_nodes: node_type = core_pb2.NodeType.DEFAULT - # TODO what is the model name for ovs node_model = name else: logging.error("grpcmanagemeny.py INVALID node name") create_node = Node(session_id, self.get_id(), node_type, node_model, x, y, name) self.nodes[canvas_id] = create_node - self.nodes_to_create[canvas_id] = create_node logging.debug( "Adding node to GrpcManager.. Session id: %s, Coords: (%s, %s), Name: %s", session_id, @@ -124,8 +122,42 @@ class GrpcManager: name, ) - def add_preexisting_node(self): - return + def add_preexisting_node(self, canvas_node, session_id, core_node, name): + """ + Add preexisting nodes to grpc manager + + :param core_pb2.Node core_node: core node grpc message + :param coretk.graph.CanvasNode canvas_node: canvas node + :param int session_id: session id + :return: nothing + """ + core_id = core_node.id + if core_id >= self.id: + self.id = core_id + 1 + self.preexisting.append(core_id) + n = Node( + session_id, + core_id, + core_node.type, + core_node.model, + canvas_node.x_coord, + canvas_node.y_coord, + name, + ) + self.nodes[canvas_node.id] = n + + def update_reusable_id(self): + """ + Update available id for reuse + + :return: nothing + """ + for i in range(1, self.id): + if i not in self.preexisting: + self.reusable.append(i) + + self.preexisting.clear() + logging.debug("Next id: %s, Reusable: %s", self.id, self.reusable) def delete_node(self, canvas_id): """ @@ -151,7 +183,6 @@ class GrpcManager: self.nodes[canvas_id_2].type, ) self.edges[token] = edge - self.edges_to_create[token] = edge logging.debug("Adding edge to grpc manager...") else: logging.error("grpcmanagement.py INVALID CANVAS NODE ID") diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 4911efac..fc04c7cc 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -25,35 +25,36 @@ class Images: @classmethod def convert_type_and_model_to_image(cls, node_type, node_model): """ - Retrieve image based on type and model - :param core_pb2.NodeType node_type: core node type - :param string node_model: the node model + Retrieve image based on type and model + :param core_pb2.NodeType node_type: core node type + :param string node_model: the node model - :return: the matching image - """ + :rtype: tuple(PhotoImage, str) + :return: the matching image and its name + """ if node_type == core_pb2.NodeType.SWITCH: - return Images.get("switch") + return Images.get("switch"), "switch" if node_type == core_pb2.NodeType.HUB: - return Images.get("hub") + return Images.get("hub"), "hub" if node_type == core_pb2.NodeType.WIRELESS_LAN: - return Images.get("wlan") + return Images.get("wlan"), "wlan" if node_type == core_pb2.NodeType.RJ45: - return Images.get("rj45") + return Images.get("rj45"), "rj45" if node_type == core_pb2.NodeType.TUNNEL: - return Images.get("tunnel") + return Images.get("tunnel"), "tunnel" if node_type == core_pb2.NodeType.DEFAULT: if node_model == "router": - return Images.get("router") + return Images.get("router"), "router" if node_model == "host": - return Images.get(("host")) + return Images.get(("host")), "host" if node_model == "PC": - return Images.get("pc") + return Images.get("pc"), "PC" if node_model == "mdr": - return Images.get("mdr") + return Images.get("mdr"), "mdr" if node_model == "prouter": - return Images.get("prouter") + return Images.get("prouter"), "prouter" if node_model == "OVS": - return Images.get("ovs") + return Images.get("ovs"), "ovs" else: logging.debug("INVALID INPUT OR NOT CONSIDERED YET") From f83f98262f3523a76b1fa76c6b1a415adfb65900 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 4 Oct 2019 17:33:44 -0700 Subject: [PATCH 031/462] some initial remote node commands using fabric --- daemon/core/emulator/session.py | 18 ++++++- daemon/core/nodes/base.py | 68 ++++++++++++++++++++++----- daemon/examples/python/distributed.py | 55 ++++++++++++++++++++++ 3 files changed, 127 insertions(+), 14 deletions(-) create mode 100644 daemon/examples/python/distributed.py diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 7b2a03b9..f5625232 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -14,6 +14,8 @@ import threading import time from multiprocessing.pool import ThreadPool +from fabric import Connection + from core import constants, utils from core.api.tlv import coreapi from core.api.tlv.broker import CoreBroker @@ -144,6 +146,9 @@ class Session(object): self.emane = EmaneManager(session=self) self.sdt = Sdt(session=self) + # distributed servers + self.servers = set() + # initialize default node services self.services.default_services = { "mdr": ("zebra", "OSPFv3MDR", "IPForward"), @@ -153,6 +158,11 @@ class Session(object): "host": ("DefaultRoute", "SSH"), } + def init_distributed(self): + for server in self.servers: + cmd = "mkdir -p %s" % self.session_dir + Connection(server, user="root").run(cmd, hide=False) + @classmethod def get_node_class(cls, _type): """ @@ -683,7 +693,13 @@ class Session(object): image=node_options.image, ) else: - node = self.create_node(cls=node_class, _id=_id, name=name, start=start) + node = self.create_node( + cls=node_class, + _id=_id, + name=name, + start=start, + server=node_options.emulation_server, + ) # set node attributes node.icon = node_options.icon diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 7efad49e..b08c7cd4 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -14,13 +14,15 @@ import threading from builtins import range from socket import AF_INET, AF_INET6 +from fabric import Connection + from core import constants, utils from core.emulator.data import LinkData, NodeData from core.emulator.enumerations import LinkTypes, NodeTypes +from core.errors import CoreCommandError from core.nodes import client, ipaddress from core.nodes.interface import CoreInterface, TunTap, Veth from core.nodes.netclient import LinuxNetClient, OvsNetClient -from fabric import Connection _DEFAULT_MTU = 1500 @@ -33,7 +35,7 @@ class NodeBase(object): apitype = None # TODO: appears start has no usage, verify and remove - def __init__(self, session, _id=None, name=None, start=True): + def __init__(self, session, _id=None, name=None, start=True, server=None): """ Creates a PyCoreObj instance. @@ -41,7 +43,7 @@ class NodeBase(object): :param int _id: id :param str name: object name :param bool start: start value - :return: + :param str server: remote server node will run on, default is None for localhost """ self.session = session @@ -51,8 +53,11 @@ class NodeBase(object): if name is None: name = "o%s" % self.id self.name = name + self.server = server + if self.server is not None: + self.server_conn = Connection(self.server, user="root") + self.type = None - self.server = None self.services = None # ifindex is key, CoreInterface instance is value self._netif = {} @@ -94,12 +99,23 @@ class NodeBase(object): :rtype: str :raises CoreCommandError: when a non-zero exit status occurs """ + logging.info("net cmd server(%s): %s", self.server, args) if self.server is None: return utils.check_cmd(args, env=env) else: args = " ".join(args) - result = Connection(self.server, user="root").run(args, hide=True) - return result.stderr + result = self.server_conn.run(args, hide=False) + if result.exited: + raise CoreCommandError( + result.exited, result.command, result.stdout, result.stderr + ) + + logging.info( + "fabric result:\n\tstdout: %s\n\tstderr: %s", + result.stdout.strip(), + result.stderr.strip(), + ) + return result.stdout.strip() def setposition(self, x=None, y=None, z=None): """ @@ -243,7 +259,7 @@ class CoreNodeBase(NodeBase): Base class for CORE nodes. """ - def __init__(self, session, _id=None, name=None, start=True): + def __init__(self, session, _id=None, name=None, start=True, server=None): """ Create a CoreNodeBase instance. @@ -251,8 +267,9 @@ class CoreNodeBase(NodeBase): :param int _id: object id :param str name: object name :param bool start: boolean for starting + :param str server: remote server node will run on, default is None for localhost """ - super(CoreNodeBase, self).__init__(session, _id, name, start=start) + super(CoreNodeBase, self).__init__(session, _id, name, start, server) self.services = [] self.nodedir = None self.tmpnodedir = False @@ -265,7 +282,7 @@ class CoreNodeBase(NodeBase): """ if self.nodedir is None: self.nodedir = os.path.join(self.session.session_dir, self.name + ".conf") - os.makedirs(self.nodedir) + self.net_cmd(["mkdir", "-p", self.nodedir]) self.tmpnodedir = True else: self.tmpnodedir = False @@ -446,7 +463,14 @@ class CoreNode(CoreNodeBase): valid_address_types = {"inet", "inet6", "inet6link"} def __init__( - self, session, _id=None, name=None, nodedir=None, bootsh="boot.sh", start=True + self, + session, + _id=None, + name=None, + nodedir=None, + bootsh="boot.sh", + start=True, + server=None, ): """ Create a CoreNode instance. @@ -457,8 +481,9 @@ class CoreNode(CoreNodeBase): :param str nodedir: node directory :param str bootsh: boot shell to use :param bool start: start flag + :param str server: remote server node will run on, default is None for localhost """ - super(CoreNode, self).__init__(session, _id, name, start) + super(CoreNode, self).__init__(session, _id, name, start, server) self.nodedir = nodedir self.ctrlchnlname = os.path.abspath( os.path.join(self.session.session_dir, self.name) @@ -619,7 +644,24 @@ class CoreNode(CoreNodeBase): :rtype: str :raises CoreCommandError: when a non-zero exit status occurs """ - return self.check_cmd(args) + logging.info("net cmd server(%s): %s", self.server, args) + if self.server is None: + return self.check_cmd(args) + else: + args = self.client._cmd_args() + args + args = " ".join(args) + result = self.server_conn.run(args, hide=False) + if result.exited: + raise CoreCommandError( + result.exited, result.command, result.stdout, result.stderr + ) + + logging.info( + "fabric result:\n\tstdout: %s\n\tstderr: %s", + result.stdout.strip(), + result.stderr.strip(), + ) + return result.stdout.strip() def check_cmd(self, args): """ @@ -653,7 +695,7 @@ class CoreNode(CoreNodeBase): hostpath = os.path.join( self.nodedir, os.path.normpath(path).strip("/").replace("/", ".") ) - os.mkdir(hostpath) + self.net_cmd(["mkdir", "-p", hostpath]) self.mount(hostpath, path) def mount(self, source, target): diff --git a/daemon/examples/python/distributed.py b/daemon/examples/python/distributed.py new file mode 100644 index 00000000..bed75a47 --- /dev/null +++ b/daemon/examples/python/distributed.py @@ -0,0 +1,55 @@ +import logging + +from core.emulator.coreemu import CoreEmu +from core.emulator.emudata import NodeOptions +from core.emulator.enumerations import EventTypes + + +def main(): + # ip generator for example + # prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") + + # create emulator instance for creating sessions and utility methods + coreemu = CoreEmu() + session = coreemu.create_session() + + # initialize distributed + session.servers.add("core2") + session.init_distributed() + + # must be in configuration state for nodes to start, when using "node_add" below + session.set_state(EventTypes.CONFIGURATION_STATE) + + # create switch network node + # switch = session.add_node(_type=NodeTypes.SWITCH) + + # create nodes + options = NodeOptions() + options.emulation_server = "10.10.4.38" + options.emulation_server = "core2" + session.add_node(node_options=options) + # interface = prefixes.create_interface(node_one) + # session.add_link(node_one.id, switch.id, interface_one=interface) + + # node_two = session.add_node() + # interface = prefixes.create_interface(node_two) + # session.add_link(node_two.id, switch.id, interface_one=interface) + + # instantiate session + session.instantiate() + + # print("starting iperf server on node: %s" % node_one.name) + # node_one.cmd(["iperf", "-s", "-D"]) + # node_one_address = prefixes.ip4_address(node_one) + # + # print("node %s connecting to %s" % (node_two.name, node_one_address)) + # node_two.client.icmd(["iperf", "-t", "10", "-c", node_one_address]) + # node_one.cmd(["killall", "-9", "iperf"]) + + # shutdown session + coreemu.shutdown() + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + main() From 931ee65235cea91923c16d00aaa554876f22a165 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 5 Oct 2019 09:48:30 -0700 Subject: [PATCH 032/462] added remote_cmd func for nodes to avoid duplication --- daemon/core/nodes/base.py | 44 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index b08c7cd4..8e4d33d2 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -104,18 +104,29 @@ class NodeBase(object): return utils.check_cmd(args, env=env) else: args = " ".join(args) - result = self.server_conn.run(args, hide=False) - if result.exited: - raise CoreCommandError( - result.exited, result.command, result.stdout, result.stderr - ) + return self.remote_cmd(args) - logging.info( - "fabric result:\n\tstdout: %s\n\tstderr: %s", - result.stdout.strip(), - result.stderr.strip(), + def remote_cmd(self, cmd): + """ + Run command remotely using server connection. + + :param str cmd: command to run + :return: stdout when success + :rtype: str + :raises CoreCommandError: when a non-zero exit status occurs + """ + result = self.server_conn.run(cmd, hide=False) + if result.exited: + raise CoreCommandError( + result.exited, result.command, result.stdout, result.stderr ) - return result.stdout.strip() + + logging.info( + "fabric result:\n\tstdout: %s\n\tstderr: %s", + result.stdout.strip(), + result.stderr.strip(), + ) + return result.stdout.strip() def setposition(self, x=None, y=None, z=None): """ @@ -650,18 +661,7 @@ class CoreNode(CoreNodeBase): else: args = self.client._cmd_args() + args args = " ".join(args) - result = self.server_conn.run(args, hide=False) - if result.exited: - raise CoreCommandError( - result.exited, result.command, result.stdout, result.stderr - ) - - logging.info( - "fabric result:\n\tstdout: %s\n\tstderr: %s", - result.stdout.strip(), - result.stderr.strip(), - ) - return result.stdout.strip() + return self.remote_cmd(args) def check_cmd(self, args): """ From 95296988c5e92f52ed66290b1b5d2893d464cc03 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 5 Oct 2019 11:16:57 -0700 Subject: [PATCH 033/462] updates to Pipefile.lock and for nodes to add server to constructor --- daemon/Pipfile.lock | 414 +++++++++++++++++++++++----------- daemon/core/emane/nodes.py | 4 +- daemon/core/nodes/base.py | 5 +- daemon/core/nodes/network.py | 28 ++- daemon/core/nodes/physical.py | 6 +- ns3/corens3/obj.py | 6 +- 6 files changed, 310 insertions(+), 153 deletions(-) diff --git a/daemon/Pipfile.lock b/daemon/Pipfile.lock index 1e1cf60c..73400b8b 100644 --- a/daemon/Pipfile.lock +++ b/daemon/Pipfile.lock @@ -14,6 +14,67 @@ ] }, "default": { + "asn1crypto": { + "hashes": [ + "sha256:0b199f211ae690df3db4fd6c1c4ff976497fb1da689193e368eedbadc53d9292", + "sha256:bca90060bd995c3f62c4433168eab407e44bdbdb567b3f3a396a676c1a4c4a3f" + ], + "version": "==1.0.1" + }, + "bcrypt": { + "hashes": [ + "sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89", + "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42", + "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294", + "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161", + "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31", + "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5", + "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c", + "sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0", + "sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de", + "sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e", + "sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052", + "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09", + "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105", + "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133", + "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7", + "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc" + ], + "version": "==3.1.7" + }, + "cffi": { + "hashes": [ + "sha256:041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774", + "sha256:046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d", + "sha256:066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90", + "sha256:066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b", + "sha256:2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63", + "sha256:300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45", + "sha256:34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25", + "sha256:46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3", + "sha256:4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b", + "sha256:4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647", + "sha256:4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016", + "sha256:50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4", + "sha256:55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb", + "sha256:5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753", + "sha256:59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7", + "sha256:73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9", + "sha256:a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f", + "sha256:a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8", + "sha256:a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f", + "sha256:a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc", + "sha256:ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42", + "sha256:b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3", + "sha256:d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909", + "sha256:d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45", + "sha256:dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d", + "sha256:e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512", + "sha256:e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff", + "sha256:ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201" + ], + "version": "==1.12.3" + }, "configparser": { "hashes": [ "sha256:254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c", @@ -25,14 +86,33 @@ "editable": true, "path": "." }, - "enum34": { + "cryptography": { "hashes": [ - "sha256:2d81cbbe0e73112bdfe6ef8576f2238f2ba27dd0d55752a776c41d38b7da2850", - "sha256:644837f692e5f550741432dd3f223bbb9852018674981b1664e5dc339387588a", - "sha256:6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79", - "sha256:8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1" + "sha256:24b61e5fcb506424d3ec4e18bca995833839bf13c59fc43e530e488f28d46b8c", + "sha256:25dd1581a183e9e7a806fe0543f485103232f940fcfc301db65e630512cce643", + "sha256:3452bba7c21c69f2df772762be0066c7ed5dc65df494a1d53a58b683a83e1216", + "sha256:41a0be220dd1ed9e998f5891948306eb8c812b512dc398e5a01846d855050799", + "sha256:5751d8a11b956fbfa314f6553d186b94aa70fdb03d8a4d4f1c82dcacf0cbe28a", + "sha256:5f61c7d749048fa6e3322258b4263463bfccefecb0dd731b6561cb617a1d9bb9", + "sha256:72e24c521fa2106f19623a3851e9f89ddfdeb9ac63871c7643790f872a305dfc", + "sha256:7b97ae6ef5cba2e3bb14256625423413d5ce8d1abb91d4f29b6d1a081da765f8", + "sha256:961e886d8a3590fd2c723cf07be14e2a91cf53c25f02435c04d39e90780e3b53", + "sha256:96d8473848e984184b6728e2c9d391482008646276c3ff084a1bd89e15ff53a1", + "sha256:ae536da50c7ad1e002c3eee101871d93abdc90d9c5f651818450a0d3af718609", + "sha256:b0db0cecf396033abb4a93c95d1602f268b3a68bb0a9cc06a7cff587bb9a7292", + "sha256:cfee9164954c186b191b91d4193989ca994703b2fff406f71cf454a2d3c7327e", + "sha256:e6347742ac8f35ded4a46ff835c60e68c22a536a8ae5c4422966d06946b6d4c6", + "sha256:f27d93f0139a3c056172ebb5d4f9056e770fdf0206c2f422ff2ebbad142e09ed", + "sha256:f57b76e46a58b63d1c6375017f4564a28f19a5ca912691fd2e4261b3414b618d" ], - "version": "==1.1.6" + "version": "==2.7" + }, + "fabric": { + "hashes": [ + "sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389", + "sha256:24842d7d51556adcabd885ac3cf5e1df73fc622a1708bf3667bf5927576cdfa6" + ], + "version": "==2.5.0" }, "future": { "hashes": [ @@ -42,40 +122,48 @@ }, "grpcio": { "hashes": [ - "sha256:1303578092f1f6e4bfbc354c04ac422856c393723d3ffa032fff0f7cb5cfd693", - "sha256:229c6b313cd82bec8f979b059d87f03cc1a48939b543fe170b5a9c5cf6a6bc69", - "sha256:3cd3d99a8b5568d0d186f9520c16121a0f2a4bcad8e2b9884b76fb88a85a7774", - "sha256:41cfb222db358227521f9638a6fbc397f310042a4db5539a19dea01547c621cd", - "sha256:43330501660f636fd6547d1e196e395cd1e2c2ae57d62219d6184a668ffebda0", - "sha256:45d7a2bd8b4f25a013296683f4140d636cdbb507d94a382ea5029a21e76b1648", - "sha256:47dc935658a13b25108823dabd010194ddea9610357c5c1ef1ad7b3f5157ebee", - "sha256:480aa7e2b56238badce0b9413a96d5b4c90c3bfbd79eba5a0501e92328d9669e", - "sha256:4a0934c8b0f97e1d8c18e76c45afc0d02d33ab03125258179f2ac6c7a13f3626", - "sha256:5624dab19e950f99e560400c59d87b685809e4cfcb2c724103f1ab14c06071f7", - "sha256:60515b1405bb3dadc55e6ca99429072dad3e736afcf5048db5452df5572231ff", - "sha256:610f97ebae742a57d336a69b09a9c7d7de1f62aa54aaa8adc635b38f55ba4382", - "sha256:64ea189b2b0859d1f7b411a09185028744d494ef09029630200cc892e366f169", - "sha256:686090c6c1e09e4f49585b8508d0a31d58bc3895e4049ea55b197d1381e9f70f", - "sha256:7745c365195bb0605e3d47b480a2a4d1baa8a41a5fd0a20de5fa48900e2c886a", - "sha256:79491e0d2b77a1c438116bf9e5f9e2e04e78b78524615e2ce453eff62db59a09", - "sha256:825177dd4c601c487836b7d6b4ba268db59787157911c623ba59a7c03c8d3adc", - "sha256:8a060e1f72fb94eee8a035ed29f1201ce903ad14cbe27bda56b4a22a8abda045", - "sha256:90168cc6353e2766e47b650c963f21cfff294654b10b3a14c67e26a4e3683634", - "sha256:94b7742734bceeff6d8db5edb31ac844cb68fc7f13617eca859ff1b78bb20ba1", - "sha256:962aebf2dd01bbb2cdb64580e61760f1afc470781f9ecd5fe8f3d8dcd8cf4556", - "sha256:9c8d9eacdce840b72eee7924c752c31b675f8aec74790e08cff184a4ea8aa9c1", - "sha256:af5b929debc336f6bab9b0da6915f9ee5e41444012aed6a79a3c7e80d7662fdf", - "sha256:b9cdb87fc77e9a3eabdc42a512368538d648fa0760ad30cf97788076985c790a", - "sha256:c5e6380b90b389454669dc67d0a39fb4dc166416e01308fcddd694236b8329ef", - "sha256:d60c90fe2bfbee735397bf75a2f2c4e70c5deab51cd40c6e4fa98fae018c8db6", - "sha256:d8582c8b1b1063249da1588854251d8a91df1e210a328aeb0ece39da2b2b763b", - "sha256:ddbf86ba3aa0ad8fed2867910d2913ee237d55920b55f1d619049b3399f04efc", - "sha256:e46bc0664c5c8a0545857aa7a096289f8db148e7f9cca2d0b760113e8994bddc", - "sha256:f6437f70ec7fed0ca3a0eef1146591bb754b418bb6c6b21db74f0333d624e135", - "sha256:f71693c3396530c6b00773b029ea85e59272557e9bd6077195a6593e4229892a", - "sha256:f79f7455f8fbd43e8e9d61914ecf7f48ba1c8e271801996fef8d6a8f3cc9f39f" + "sha256:0302331e014fc4bac028b6ad480b33f7abfe20b9bdcca7be417124dda8f22115", + "sha256:0aa0cce9c5eb1261b32173a20ed42b51308d55ce28ecc2021e868b3cb90d9503", + "sha256:0c83947575300499adbc308e986d754e7f629be0bdd9bea1ffdd5cf76e1f1eff", + "sha256:0ca26ff968d45efd4ef73447c4d4b34322ea8c7d06fbb6907ce9e5db78f1bbcb", + "sha256:0cf80a7955760c2498f8821880242bb657d70998065ff0d2a082de5ffce230a7", + "sha256:0d40706e57d9833fe0e023a08b468f33940e8909affa12547874216d36bba208", + "sha256:11872069156de34c6f3f9a1deb46cc88bc35dfde88262c4c73eb22b39b16fc55", + "sha256:16065227faae0ab0abf1789bfb92a2cd2ab5da87630663f93f8178026da40e0d", + "sha256:1e33778277685f6fabb22539136269c87c029e39b6321ef1a639b756a1c0a408", + "sha256:2b16be15b1ae656bc7a36642b8c7045be2dde2048bb4b67478003e9d9db8022a", + "sha256:3701dfca3ada27ceef0d17f728ce9dfef155ed20c57979c2b05083082258c6c1", + "sha256:41912ecaf482abf2de74c69f509878f99223f5dd6b2de1a09c955afd4de3cf9b", + "sha256:4332cbd20544fe7406910137590f38b5b3a1f6170258e038652cf478c639430f", + "sha256:44068ecbdc6467c2bff4d8198816c8a2701b6dd1ec16078fceb6adc7c1f577d6", + "sha256:53115960e37059420e2d16a4b04b00dd2ab3b6c3c67babd01ffbfdcd7881a69b", + "sha256:6e7027bcd4070414751e2a5e60706facb98a1fc636497c9bac5442fe37b8ae6b", + "sha256:6ff57fb2f07b7226b5bec89e8e921ea9bd220f35f11e094f2ba38f09eecd49c6", + "sha256:73240e244d7644654bbda1f309f4911748b6a1804b7a8897ddbe8a04c90f7407", + "sha256:785234bbc469bc75e26c868789a2080ffb30bd6e93930167797729889ad06b0b", + "sha256:82f9d3c7f91d2d1885631335c003c5d45ae1cd69cc0bc4893f21fef50b8151bc", + "sha256:86bdc2a965510658407a1372eb61f0c92f763fdfb2795e4d038944da4320c950", + "sha256:95e925b56676a55e6282b3de80a1cbad5774072159779c61eac02791dface049", + "sha256:96673bb4f14bd3263613526d1e7e33fdb38a9130e3ce87bf52314965706e1900", + "sha256:970014205e76920484679035b6fb4b16e02fc977e5aac4d22025da849c79dab9", + "sha256:ace5e8bf11a1571f855f5dab38a9bd34109b6c9bc2864abf24a597598c7e3695", + "sha256:ad375f03eb3b9cb75a24d91eab8609e134d34605f199efc41e20dd642bdac855", + "sha256:b819c4c7dcf0de76788ce5f95daad6d4e753d6da2b6a5f84e5bb5b5ce95fddc4", + "sha256:c17943fd340cbd906db49f3f03c7545e5a66b617e8348b2c7a0d2c759d216af1", + "sha256:d21247150dea86dabd3b628d8bc4b563036db3d332b3f4db3c5b1b0b122cb4f6", + "sha256:d4d500a7221116de9767229ff5dd10db91f789448d85befb0adf5a37b0cd83b5", + "sha256:e2a942a3cfccbbca21a90c144867112698ef36486345c285da9e98c466f22b22", + "sha256:e983273dca91cb8a5043bc88322eb48e2b8d4e4998ff441a1ee79ced89db3909" ], - "version": "==1.23.0" + "version": "==1.24.1" + }, + "invoke": { + "hashes": [ + "sha256:c52274d2e8a6d64ef0d61093e1983268ea1fc0cd13facb9448c4ef0c9a7ac7da", + "sha256:f4ec8a134c0122ea042c8912529f87652445d9f4de590b353d23f95bfa1f0efd", + "sha256:fc803a5c9052f15e63310aa81a43498d7c55542beb18564db88a9d75a176fa44" + ], + "version": "==1.3.0" }, "lxml": { "hashes": [ @@ -104,6 +192,64 @@ ], "version": "==4.4.1" }, + "paramiko": { + "hashes": [ + "sha256:99f0179bdc176281d21961a003ffdb2ec369daac1a1007241f53374e376576cf", + "sha256:f4b2edfa0d226b70bd4ca31ea7e389325990283da23465d572ed1f70a7583041" + ], + "version": "==2.6.0" + }, + "protobuf": { + "hashes": [ + "sha256:125713564d8cfed7610e52444c9769b8dcb0b55e25cc7841f2290ee7bc86636f", + "sha256:1accdb7a47e51503be64d9a57543964ba674edac103215576399d2d0e34eac77", + "sha256:27003d12d4f68e3cbea9eb67427cab3bfddd47ff90670cb367fcd7a3a89b9657", + "sha256:3264f3c431a631b0b31e9db2ae8c927b79fc1a7b1b06b31e8e5bcf2af91fe896", + "sha256:3c5ab0f5c71ca5af27143e60613729e3488bb45f6d3f143dc918a20af8bab0bf", + "sha256:45dcf8758873e3f69feab075e5f3177270739f146255225474ee0b90429adef6", + "sha256:56a77d61a91186cc5676d8e11b36a5feb513873e4ae88d2ee5cf530d52bbcd3b", + "sha256:5984e4947bbcef5bd849d6244aec507d31786f2dd3344139adc1489fb403b300", + "sha256:6b0441da73796dd00821763bb4119674eaf252776beb50ae3883bed179a60b2a", + "sha256:6f6677c5ade94d4fe75a912926d6796d5c71a2a90c2aeefe0d6f211d75c74789", + "sha256:84a825a9418d7196e2acc48f8746cf1ee75877ed2f30433ab92a133f3eaf8fbe", + "sha256:b842c34fe043ccf78b4a6cf1019d7b80113707d68c88842d061fa2b8fb6ddedc", + "sha256:ca33d2f09dae149a1dcf942d2d825ebb06343b77b437198c9e2ef115cf5d5bc1", + "sha256:db83b5c12c0cd30150bb568e6feb2435c49ce4e68fe2d7b903113f0e221e58fe", + "sha256:f50f3b1c5c1c1334ca7ce9cad5992f098f460ffd6388a3cabad10b66c2006b09", + "sha256:f99f127909731cafb841c52f9216e447d3e4afb99b17bebfad327a75aee206de" + ], + "version": "==3.10.0" + }, + "pycparser": { + "hashes": [ + "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" + ], + "version": "==2.19" + }, + "pynacl": { + "hashes": [ + "sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255", + "sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c", + "sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e", + "sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae", + "sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621", + "sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56", + "sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39", + "sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310", + "sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1", + "sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a", + "sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786", + "sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b", + "sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b", + "sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f", + "sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20", + "sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415", + "sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715", + "sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1", + "sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0" + ], + "version": "==1.3.0" + }, "six": { "hashes": [ "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", @@ -136,10 +282,10 @@ }, "attrs": { "hashes": [ - "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", - "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + "sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2", + "sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396" ], - "version": "==19.1.0" + "version": "==19.2.0" }, "black": { "hashes": [ @@ -180,78 +326,78 @@ }, "grpcio": { "hashes": [ - "sha256:1303578092f1f6e4bfbc354c04ac422856c393723d3ffa032fff0f7cb5cfd693", - "sha256:229c6b313cd82bec8f979b059d87f03cc1a48939b543fe170b5a9c5cf6a6bc69", - "sha256:3cd3d99a8b5568d0d186f9520c16121a0f2a4bcad8e2b9884b76fb88a85a7774", - "sha256:41cfb222db358227521f9638a6fbc397f310042a4db5539a19dea01547c621cd", - "sha256:43330501660f636fd6547d1e196e395cd1e2c2ae57d62219d6184a668ffebda0", - "sha256:45d7a2bd8b4f25a013296683f4140d636cdbb507d94a382ea5029a21e76b1648", - "sha256:47dc935658a13b25108823dabd010194ddea9610357c5c1ef1ad7b3f5157ebee", - "sha256:480aa7e2b56238badce0b9413a96d5b4c90c3bfbd79eba5a0501e92328d9669e", - "sha256:4a0934c8b0f97e1d8c18e76c45afc0d02d33ab03125258179f2ac6c7a13f3626", - "sha256:5624dab19e950f99e560400c59d87b685809e4cfcb2c724103f1ab14c06071f7", - "sha256:60515b1405bb3dadc55e6ca99429072dad3e736afcf5048db5452df5572231ff", - "sha256:610f97ebae742a57d336a69b09a9c7d7de1f62aa54aaa8adc635b38f55ba4382", - "sha256:64ea189b2b0859d1f7b411a09185028744d494ef09029630200cc892e366f169", - "sha256:686090c6c1e09e4f49585b8508d0a31d58bc3895e4049ea55b197d1381e9f70f", - "sha256:7745c365195bb0605e3d47b480a2a4d1baa8a41a5fd0a20de5fa48900e2c886a", - "sha256:79491e0d2b77a1c438116bf9e5f9e2e04e78b78524615e2ce453eff62db59a09", - "sha256:825177dd4c601c487836b7d6b4ba268db59787157911c623ba59a7c03c8d3adc", - "sha256:8a060e1f72fb94eee8a035ed29f1201ce903ad14cbe27bda56b4a22a8abda045", - "sha256:90168cc6353e2766e47b650c963f21cfff294654b10b3a14c67e26a4e3683634", - "sha256:94b7742734bceeff6d8db5edb31ac844cb68fc7f13617eca859ff1b78bb20ba1", - "sha256:962aebf2dd01bbb2cdb64580e61760f1afc470781f9ecd5fe8f3d8dcd8cf4556", - "sha256:9c8d9eacdce840b72eee7924c752c31b675f8aec74790e08cff184a4ea8aa9c1", - "sha256:af5b929debc336f6bab9b0da6915f9ee5e41444012aed6a79a3c7e80d7662fdf", - "sha256:b9cdb87fc77e9a3eabdc42a512368538d648fa0760ad30cf97788076985c790a", - "sha256:c5e6380b90b389454669dc67d0a39fb4dc166416e01308fcddd694236b8329ef", - "sha256:d60c90fe2bfbee735397bf75a2f2c4e70c5deab51cd40c6e4fa98fae018c8db6", - "sha256:d8582c8b1b1063249da1588854251d8a91df1e210a328aeb0ece39da2b2b763b", - "sha256:ddbf86ba3aa0ad8fed2867910d2913ee237d55920b55f1d619049b3399f04efc", - "sha256:e46bc0664c5c8a0545857aa7a096289f8db148e7f9cca2d0b760113e8994bddc", - "sha256:f6437f70ec7fed0ca3a0eef1146591bb754b418bb6c6b21db74f0333d624e135", - "sha256:f71693c3396530c6b00773b029ea85e59272557e9bd6077195a6593e4229892a", - "sha256:f79f7455f8fbd43e8e9d61914ecf7f48ba1c8e271801996fef8d6a8f3cc9f39f" + "sha256:0302331e014fc4bac028b6ad480b33f7abfe20b9bdcca7be417124dda8f22115", + "sha256:0aa0cce9c5eb1261b32173a20ed42b51308d55ce28ecc2021e868b3cb90d9503", + "sha256:0c83947575300499adbc308e986d754e7f629be0bdd9bea1ffdd5cf76e1f1eff", + "sha256:0ca26ff968d45efd4ef73447c4d4b34322ea8c7d06fbb6907ce9e5db78f1bbcb", + "sha256:0cf80a7955760c2498f8821880242bb657d70998065ff0d2a082de5ffce230a7", + "sha256:0d40706e57d9833fe0e023a08b468f33940e8909affa12547874216d36bba208", + "sha256:11872069156de34c6f3f9a1deb46cc88bc35dfde88262c4c73eb22b39b16fc55", + "sha256:16065227faae0ab0abf1789bfb92a2cd2ab5da87630663f93f8178026da40e0d", + "sha256:1e33778277685f6fabb22539136269c87c029e39b6321ef1a639b756a1c0a408", + "sha256:2b16be15b1ae656bc7a36642b8c7045be2dde2048bb4b67478003e9d9db8022a", + "sha256:3701dfca3ada27ceef0d17f728ce9dfef155ed20c57979c2b05083082258c6c1", + "sha256:41912ecaf482abf2de74c69f509878f99223f5dd6b2de1a09c955afd4de3cf9b", + "sha256:4332cbd20544fe7406910137590f38b5b3a1f6170258e038652cf478c639430f", + "sha256:44068ecbdc6467c2bff4d8198816c8a2701b6dd1ec16078fceb6adc7c1f577d6", + "sha256:53115960e37059420e2d16a4b04b00dd2ab3b6c3c67babd01ffbfdcd7881a69b", + "sha256:6e7027bcd4070414751e2a5e60706facb98a1fc636497c9bac5442fe37b8ae6b", + "sha256:6ff57fb2f07b7226b5bec89e8e921ea9bd220f35f11e094f2ba38f09eecd49c6", + "sha256:73240e244d7644654bbda1f309f4911748b6a1804b7a8897ddbe8a04c90f7407", + "sha256:785234bbc469bc75e26c868789a2080ffb30bd6e93930167797729889ad06b0b", + "sha256:82f9d3c7f91d2d1885631335c003c5d45ae1cd69cc0bc4893f21fef50b8151bc", + "sha256:86bdc2a965510658407a1372eb61f0c92f763fdfb2795e4d038944da4320c950", + "sha256:95e925b56676a55e6282b3de80a1cbad5774072159779c61eac02791dface049", + "sha256:96673bb4f14bd3263613526d1e7e33fdb38a9130e3ce87bf52314965706e1900", + "sha256:970014205e76920484679035b6fb4b16e02fc977e5aac4d22025da849c79dab9", + "sha256:ace5e8bf11a1571f855f5dab38a9bd34109b6c9bc2864abf24a597598c7e3695", + "sha256:ad375f03eb3b9cb75a24d91eab8609e134d34605f199efc41e20dd642bdac855", + "sha256:b819c4c7dcf0de76788ce5f95daad6d4e753d6da2b6a5f84e5bb5b5ce95fddc4", + "sha256:c17943fd340cbd906db49f3f03c7545e5a66b617e8348b2c7a0d2c759d216af1", + "sha256:d21247150dea86dabd3b628d8bc4b563036db3d332b3f4db3c5b1b0b122cb4f6", + "sha256:d4d500a7221116de9767229ff5dd10db91f789448d85befb0adf5a37b0cd83b5", + "sha256:e2a942a3cfccbbca21a90c144867112698ef36486345c285da9e98c466f22b22", + "sha256:e983273dca91cb8a5043bc88322eb48e2b8d4e4998ff441a1ee79ced89db3909" ], - "version": "==1.23.0" + "version": "==1.24.1" }, "grpcio-tools": { "hashes": [ - "sha256:056f2a274edda4315e825ac2e3a9536f5415b43aa51669196860c8de6e76d847", - "sha256:0c953251585fdcd422072e4b7f4243fce215f22e21db94ec83c5970e41db6e18", - "sha256:142a73f5769f37bf2e4a8e4a77ef60f7af5f55635f60428322b49c87bd8f9cc0", - "sha256:1b333e2a068d8ef89a01eb23a098d2a789659c3178de79da9bd3d0ffb944cc6d", - "sha256:2124f19cc51d63405a0204ae38ef355732ab0a235975ab41ff6f6f9701905432", - "sha256:24c3a04adfb6c6f1bc4a2f8498d7661ca296ae352b498e538832c22ddde7bf81", - "sha256:3a2054e9640cbdd0ce8a345afb86be52875c5a8f9f5973a5c64791a8002da2dd", - "sha256:3fd15a09eecef83440ac849dcda2ff522f8ee1603ebfcdbb0e9b320ef2012e41", - "sha256:457e7a7dfa0b6bb608a766edba6f20c9d626a790df802016b930ad242fec4470", - "sha256:49ad5661d54ff0d164e4b441ee5e05191187d497380afa16d36d72eb8ef048de", - "sha256:561078e425d21a6720c3c3828385d949e24c0765e2852a46ecc3ad3fca2706e5", - "sha256:5a4f65ab06b32dc34112ed114dee3b698c8463670474334ece5b0b531073804c", - "sha256:8883e0e34676356d219a4cd37d239c3ead655cc550836236b52068e091416fff", - "sha256:8d2b45b1faf81098780e07d6a1c207b328b07e913160b8baa7e2e8d89723e06c", - "sha256:b0ebddb6ecc4c161369f93bb3a74c6120a498d3ddc759b64679709a885dd6d4f", - "sha256:b786ba4842c50de865dd3885b5570690a743e84a327b7213dd440eb0e6b996f8", - "sha256:be8efa010f5a80f1862ead80c3b19b5eb97dc954a0f59a1e2487078576105e03", - "sha256:c29106eaff0e2e708a9a89628dc0134ef145d0d3631f0ef421c09f380c30e354", - "sha256:c3c71236a056ec961b2b8b3b7c0b3b5a826283bc69c4a1c6415d23b70fea8243", - "sha256:cbc35031ec2b29af36947d085a7fbbcd8b79b84d563adf6156103d82565f78db", - "sha256:d47307c22744918e803c1eec7263a14f36aaf34fe496bff9ccbcae67c02b40ae", - "sha256:db088c98e563c1bb070da5984c8df08b45b61e4d9c6d2a8a1ffeed2af89fd1f3", - "sha256:df4dd1cb670062abdacc1fbce41cae4e08a4a212d28dd94fdbbf90615d027f73", - "sha256:e3adcf1499ca08d1e036ff44aedf55ed78653d946f4c4426b6e72ab757cc4dec", - "sha256:e3b3e32e0cda4dc382ec5bed8599dab644e4b3fc66a9ab54eb58248e207880b9", - "sha256:ed524195b35304c670755efa1eca579e5c290a66981f97004a5b2c0d12d6897d", - "sha256:edb42432790b1f8ec9f08faf9326d7e5dfe6e1d8c8fe4db39abc0a49c1c76537", - "sha256:eff1f995e5aa4cc941b6bbc45b5b57842f8f62bbe1a99427281c2c70cf42312c", - "sha256:f2fcdc2669662d77b400f80e20315a3661466e3cb3df1730f8083f9e49465cbc", - "sha256:f52ec9926daf48f41389d39d01570967b99c7dbc12bffc134cc3a3c5b5540ba2", - "sha256:fd007d67fdfbd2a13bf8a8c8ced8353b42a92ca72dbee54e951d8ddbc6ca12bc", - "sha256:ff9045e928dbb7943ea8559bfabebee95a43a830e00bf52c16202d2d805780fb" + "sha256:0a849994d7d6411ca6147bb1db042b61ba6232eb5c90c69de5380a441bf80a75", + "sha256:0db96ed52816471ceec8807aedf5cb4fd133ca201f614464cb46ca58584edf84", + "sha256:1b98720459204e9afa33928e4fd53aeec6598afb7f704ed497f6926c67f12b9b", + "sha256:200479310cc083c41a5020f6e5e916a99ee0f7c588b6affe317b96a839120bf4", + "sha256:25543b8f2e59ddcc9929d6f6111faa5c474b21580d2996f93347bb55f2ecba84", + "sha256:2d4609996616114c155c1e697a9faf604d81f2508cd9a4168a0bafd53c799e24", + "sha256:2fdb2a1ed2b3e43514d9c29c9de415c953a46caabbc8a9b7de1439a0c1bd3b89", + "sha256:3886a7983d8ae19df0c11a54114d6546fcdf76cf18cdccf25c3b14200fd5478a", + "sha256:408d111b9341f107bdafc523e2345471547ffe8a4104e6f2ce690b7a25c4bae5", + "sha256:60b3dd5e76c1389fc836bf83675985b92d158ff9a8d3d6d3f0a670f0c227ef13", + "sha256:629be7ce8504530b4adbf0425a44dd53007ccb6212344804294888c9662cc38f", + "sha256:6af3dde07b1051e954230e650a6ef74073cf993cf473c2078580f8a73c4fe46a", + "sha256:7a1e77539d28e90517c55561f40f7872f1348d0e23f25a38d68abbfb5b0eff88", + "sha256:87917a18b3b5951b6c9badd7b5ef09f63f61611966b58427b856bdf5c1d68e91", + "sha256:8823d0ebd185a77edb506e286c88d06847f75620a033ad96ef9c0fd7efc1d859", + "sha256:8bd3e12e1969beb813b861a2a65d4f2d4faaa87de0b60bf7f848da2d8ffc4eb2", + "sha256:8f37e9acc46e75ed9786ece89afeacd86182893eacc3f0642d81531b90fbe25f", + "sha256:9b358dd2f4142e89d760a52a7a8f4ec5dbaf955e7ada09f703f3a5d05dddd12e", + "sha256:9cb43007c4a8aa7adaacf896f5109b578028f23d259615e3fa5866e38855b311", + "sha256:9cf594bfbfbf84dcd462b20a4a753362be7ed376d2b5020a083dac24400b7b6c", + "sha256:ab79940e5c5ed949e1f95e7f417dd916b0992d29f45d073dd64501a76d128e2c", + "sha256:ba8aab6c78a82755477bb8c79f3be0824b297422d1edb21b94ae5a45407bf3ba", + "sha256:bcc00b83bf39f6e60a13f0b24ec3951f4d2ae810b01e6e125b7ff238a85da1ac", + "sha256:c1fcf5cbe6a2ecdc587b469156520b9128ccdb7c5908060c7d9712cd97e76db5", + "sha256:c6e640d39b9615388b59036b29970292b15f4519043e43833e28c674f740d1f7", + "sha256:c6ea2c385da620049b17f0135cf9307a4750e9d9c9988e15bfeeaf1f209c4ada", + "sha256:cec4f37120f93fe2ab4ab9a7eab9a877163d74c232c93a275a624971f8557b81", + "sha256:d2dbb42d237bcdecb7284535ec074c85bbf880124c1cbbff362ed3bd81ed7d41", + "sha256:d5c98a41abd4f7de43b256c21bbba2a97c57e25bf6a170927a90638b18f7509c", + "sha256:dcf5965a24179aa7dcfa00b5ff70f4f2f202e663657e0c74a642307beecda053", + "sha256:e11e3aacf0200d6e00a9b74534e0174738768fe1c41e5aa2f4aab881d6b43afd", + "sha256:e550816bdb2e49bba94bcd7f342004a8adbc46e9a25c8c4ed3fd58f2435c655f" ], "index": "pypi", - "version": "==1.23.0" + "version": "==1.24.1" }, "identify": { "hashes": [ @@ -262,11 +408,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:652234b6ab8f2506ae58e528b6fbcc668831d3cc758e1bc01ef438d328b68cdb", - "sha256:6f264986fb88042bc1f0535fa9a557e6a376cfe5679dc77caac7fe8b5d43d05f" + "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", + "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" ], "markers": "python_version < '3.8'", - "version": "==0.22" + "version": "==0.23" }, "importlib-resources": { "hashes": [ @@ -314,10 +460,10 @@ }, "packaging": { "hashes": [ - "sha256:a7ac867b97fdc07ee80a8058fe4435ccd274ecc3b0ed61d852d7d53055528cf9", - "sha256:c491ca87294da7cc01902edbe30a5bc6c4c28172b5138ab4e4aa1b9d7bfaeafe" + "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", + "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108" ], - "version": "==19.1" + "version": "==19.2" }, "pluggy": { "hashes": [ @@ -336,24 +482,24 @@ }, "protobuf": { "hashes": [ - "sha256:00a1b0b352dc7c809749526d1688a64b62ea400c5b05416f93cfb1b11a036295", - "sha256:01acbca2d2c8c3f7f235f1842440adbe01bbc379fa1cbdd80753801432b3fae9", - "sha256:0a795bca65987b62d6b8a2d934aa317fd1a4d06a6dd4df36312f5b0ade44a8d9", - "sha256:0ec035114213b6d6e7713987a759d762dd94e9f82284515b3b7331f34bfaec7f", - "sha256:31b18e1434b4907cb0113e7a372cd4d92c047ce7ba0fa7ea66a404d6388ed2c1", - "sha256:32a3abf79b0bef073c70656e86d5bd68a28a1fbb138429912c4fc07b9d426b07", - "sha256:55f85b7808766e5e3f526818f5e2aeb5ba2edcc45bcccede46a3ccc19b569cb0", - "sha256:64ab9bc971989cbdd648c102a96253fdf0202b0c38f15bd34759a8707bdd5f64", - "sha256:64cf847e843a465b6c1ba90fb6c7f7844d54dbe9eb731e86a60981d03f5b2e6e", - "sha256:917c8662b585470e8fd42f052661fc66d59fccaae450a60044307dcbf82a3335", - "sha256:afed9003d7f2be2c3df20f64220c30faec441073731511728a2cb4cab4cd46a6", - "sha256:bf8e05d638b585d1752c5a84247134a0350d3a8b73d3632489a014a9f6f1e758", - "sha256:d831b047bd69becaf64019a47179eb22118a50dd008340655266a906c69c6417", - "sha256:de2760583ed28749ff885789c1cbc6c9c06d6de92fc825740ab99deb2f25ea4d", - "sha256:eabc4cf1bc19689af8022ba52fd668564a8d96e0d08f3b4732d26a64255216a4", - "sha256:fcff6086c86fb1628d94ea455c7b9de898afc50378042927a59df8065a79a549" + "sha256:125713564d8cfed7610e52444c9769b8dcb0b55e25cc7841f2290ee7bc86636f", + "sha256:1accdb7a47e51503be64d9a57543964ba674edac103215576399d2d0e34eac77", + "sha256:27003d12d4f68e3cbea9eb67427cab3bfddd47ff90670cb367fcd7a3a89b9657", + "sha256:3264f3c431a631b0b31e9db2ae8c927b79fc1a7b1b06b31e8e5bcf2af91fe896", + "sha256:3c5ab0f5c71ca5af27143e60613729e3488bb45f6d3f143dc918a20af8bab0bf", + "sha256:45dcf8758873e3f69feab075e5f3177270739f146255225474ee0b90429adef6", + "sha256:56a77d61a91186cc5676d8e11b36a5feb513873e4ae88d2ee5cf530d52bbcd3b", + "sha256:5984e4947bbcef5bd849d6244aec507d31786f2dd3344139adc1489fb403b300", + "sha256:6b0441da73796dd00821763bb4119674eaf252776beb50ae3883bed179a60b2a", + "sha256:6f6677c5ade94d4fe75a912926d6796d5c71a2a90c2aeefe0d6f211d75c74789", + "sha256:84a825a9418d7196e2acc48f8746cf1ee75877ed2f30433ab92a133f3eaf8fbe", + "sha256:b842c34fe043ccf78b4a6cf1019d7b80113707d68c88842d061fa2b8fb6ddedc", + "sha256:ca33d2f09dae149a1dcf942d2d825ebb06343b77b437198c9e2ef115cf5d5bc1", + "sha256:db83b5c12c0cd30150bb568e6feb2435c49ce4e68fe2d7b903113f0e221e58fe", + "sha256:f50f3b1c5c1c1334ca7ce9cad5992f098f460ffd6388a3cabad10b66c2006b09", + "sha256:f99f127909731cafb841c52f9216e447d3e4afb99b17bebfad327a75aee206de" ], - "version": "==3.9.1" + "version": "==3.10.0" }, "py": { "hashes": [ @@ -385,11 +531,11 @@ }, "pytest": { "hashes": [ - "sha256:95d13143cc14174ca1a01ec68e84d76ba5d9d493ac02716fd9706c949a505210", - "sha256:b78fe2881323bd44fd9bd76e5317173d4316577e7b1cddebae9136a4495ec865" + "sha256:13c1c9b22127a77fc684eee24791efafcef343335d855e3573791c68588fe1a5", + "sha256:d8ba7be9466f55ef96ba203fc0f90d0cf212f2f927e69186e1353e30bc7f62e5" ], "index": "pypi", - "version": "==5.1.2" + "version": "==5.2.0" }, "pyyaml": { "hashes": [ diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 89c97b6b..5451506f 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -29,8 +29,8 @@ class EmaneNet(CoreNetworkBase): type = "wlan" is_emane = True - def __init__(self, session, _id=None, name=None, start=True): - super(EmaneNet, self).__init__(session, _id, name, start) + def __init__(self, session, _id=None, name=None, start=True, server=None): + super(EmaneNet, self).__init__(session, _id, name, start, server) self.conf = "" self.up = False self.nemidmap = {} diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 8e4d33d2..d0f03e6e 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -1045,7 +1045,7 @@ class CoreNetworkBase(NodeBase): linktype = LinkTypes.WIRED.value is_emane = False - def __init__(self, session, _id, name, start=True): + def __init__(self, session, _id, name, start=True, server=None): """ Create a CoreNetworkBase instance. @@ -1053,8 +1053,9 @@ class CoreNetworkBase(NodeBase): :param int _id: object id :param str name: object name :param bool start: should object start + :param str server: remote server node will run on, default is None for localhost """ - super(CoreNetworkBase, self).__init__(session, _id, name, start=start) + super(CoreNetworkBase, self).__init__(session, _id, name, start, server) self._linked = {} self._linked_lock = threading.Lock() diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 6af1ed9e..444ded56 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -281,7 +281,9 @@ class CoreNetwork(CoreNetworkBase): policy = "DROP" - def __init__(self, session, _id=None, name=None, start=True, policy=None): + def __init__( + self, session, _id=None, name=None, start=True, server=None, policy=None + ): """ Creates a LxBrNet instance. @@ -289,9 +291,10 @@ class CoreNetwork(CoreNetworkBase): :param int _id: object id :param str name: object name :param bool start: start flag + :param str server: remote server node will run on, default is None for localhost :param policy: network policy """ - CoreNetworkBase.__init__(self, session, _id, name, start) + CoreNetworkBase.__init__(self, session, _id, name, start, server) if name is None: name = str(self.id) if policy is not None: @@ -649,6 +652,7 @@ class GreTapBridge(CoreNetwork): ttl=255, key=None, start=True, + server=None, ): """ Create a GreTapBridge instance. @@ -663,9 +667,7 @@ class GreTapBridge(CoreNetwork): :param key: gre tap key :param bool start: start flag """ - CoreNetwork.__init__( - self, session=session, _id=_id, name=name, policy=policy, start=False - ) + CoreNetwork.__init__(self, session, _id, name, False, server, policy) self.grekey = key if self.grekey is None: self.grekey = self.session.id ^ self.id @@ -769,6 +771,7 @@ class CtrlNet(CoreNetwork): prefix=None, hostid=None, start=True, + server=None, assign_address=True, updown_script=None, serverintf=None, @@ -782,6 +785,7 @@ class CtrlNet(CoreNetwork): :param prefix: control network ipv4 prefix :param hostid: host id :param bool start: start flag + :param str server: remote server node will run on, default is None for localhost :param str assign_address: assigned address :param str updown_script: updown script :param serverintf: server interface @@ -792,7 +796,7 @@ class CtrlNet(CoreNetwork): self.assign_address = assign_address self.updown_script = updown_script self.serverintf = serverintf - CoreNetwork.__init__(self, session, _id=_id, name=name, start=start) + CoreNetwork.__init__(self, session, _id, name, start, server) def startup(self): """ @@ -1028,7 +1032,7 @@ class HubNode(CoreNetwork): policy = "ACCEPT" type = "hub" - def __init__(self, session, _id=None, name=None, start=True): + def __init__(self, session, _id=None, name=None, start=True, server=None): """ Creates a HubNode instance. @@ -1036,9 +1040,10 @@ class HubNode(CoreNetwork): :param int _id: node id :param str name: node namee :param bool start: start flag + :param str server: remote server node will run on, default is None for localhost :raises CoreCommandError: when there is a command exception """ - CoreNetwork.__init__(self, session, _id, name, start) + CoreNetwork.__init__(self, session, _id, name, start, server) # TODO: move to startup method if start: @@ -1055,7 +1060,9 @@ class WlanNode(CoreNetwork): policy = "DROP" type = "wlan" - def __init__(self, session, _id=None, name=None, start=True, policy=None): + def __init__( + self, session, _id=None, name=None, start=True, server=None, policy=None + ): """ Create a WlanNode instance. @@ -1063,9 +1070,10 @@ class WlanNode(CoreNetwork): :param int _id: node id :param str name: node name :param bool start: start flag + :param str server: remote server node will run on, default is None for localhost :param policy: wlan policy """ - CoreNetwork.__init__(self, session, _id, name, start, policy) + CoreNetwork.__init__(self, session, _id, name, start, server, policy) # wireless model such as basic range self.model = None # mobility model such as scripted diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 0035f97a..ba8cdc25 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -277,7 +277,7 @@ class Rj45Node(CoreNodeBase, CoreInterface): apitype = NodeTypes.RJ45.value type = "rj45" - def __init__(self, session, _id=None, name=None, mtu=1500, start=True): + def __init__(self, session, _id=None, name=None, mtu=1500, start=True, server=None): """ Create an RJ45Node instance. @@ -286,9 +286,9 @@ class Rj45Node(CoreNodeBase, CoreInterface): :param str name: node name :param mtu: rj45 mtu :param bool start: start flag - :return: + :param str server: remote server node will run on, default is None for localhost """ - CoreNodeBase.__init__(self, session, _id, name, start=start) + CoreNodeBase.__init__(self, session, _id, name, start, server) CoreInterface.__init__(self, node=self, name=name, mtu=mtu) self.up = False self.lock = threading.RLock() diff --git a/ns3/corens3/obj.py b/ns3/corens3/obj.py index 70291d3b..c1907f03 100644 --- a/ns3/corens3/obj.py +++ b/ns3/corens3/obj.py @@ -117,8 +117,10 @@ class CoreNs3Net(CoreNetworkBase): # icon used type = "wlan" - def __init__(self, session, _id=None, name=None, start=True, policy=None): - CoreNetworkBase.__init__(self, session, _id, name) + def __init__( + self, session, _id=None, name=None, start=True, server=None, policy=None + ): + CoreNetworkBase.__init__(self, session, _id, name, start, server) self.tapbridge = ns.tap_bridge.TapBridgeHelper() self._ns3devs = {} self._tapdevs = {} From cca57bba47fc8312f85afe2774e0500aaa809f9b Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 5 Oct 2019 16:10:01 -0700 Subject: [PATCH 034/462] updated other node system commands to be ran in such a way that should work if local or remote using shell commands --- daemon/core/nodes/base.py | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index d0f03e6e..848fae34 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -2,12 +2,9 @@ Defines the base logic for nodes used within core. """ -import errno import logging import os import random -import shutil -import signal import socket import string import threading @@ -309,7 +306,7 @@ class CoreNodeBase(NodeBase): return if self.tmpnodedir: - shutil.rmtree(self.nodedir, ignore_errors=True) + self.net_cmd(["rm", "-rf", self.nodedir]) def addnetif(self, netif, ifindex): """ @@ -522,8 +519,8 @@ class CoreNode(CoreNodeBase): :rtype: bool """ try: - os.kill(self.pid, 0) - except OSError: + self.net_cmd(["kill", "-9", str(self.pid)]) + except CoreCommandError: return False return True @@ -560,6 +557,7 @@ class CoreNode(CoreNodeBase): output = self.net_cmd(vnoded, env=env) self.pid = int(output) + logging.debug("node(%s) pid: %s", self.name, self.pid) # create vnode client self.client = client.VnodeClient(self.name, self.ctrlchnlname) @@ -599,21 +597,17 @@ class CoreNode(CoreNodeBase): for netif in self.netifs(): netif.shutdown() - # attempt to kill node process and wait for termination of children + # kill node process if present try: - os.kill(self.pid, signal.SIGTERM) - os.waitpid(self.pid, 0) - except OSError as e: - if e.errno != 10: - logging.exception("error killing process") + self.net_cmd(["kill", "-9", str(self.pid)]) + except CoreCommandError: + logging.exception("error killing process") # remove node directory if present try: - os.unlink(self.ctrlchnlname) - except OSError as e: - # no such file or directory - if e.errno != errno.ENOENT: - logging.exception("error removing node directory") + self.net_cmd(["rm", "-rf", self.ctrlchnlname]) + except CoreCommandError: + logging.exception("error removing node directory") # clear interface data, close client, and mark self and not up self._netif.clear() @@ -1029,9 +1023,9 @@ class CoreNode(CoreNodeBase): :return: nothing """ hostfilename = self.hostfilename(filename) - shutil.copy2(srcfilename, hostfilename) + self.net_cmd(["cp", "-a", srcfilename, hostfilename]) if mode is not None: - os.chmod(hostfilename, mode) + self.net_cmd(["chmod", oct(mode), hostfilename]) logging.info( "node(%s) copied file: %s; mode: %s", self.name, hostfilename, mode ) From 4eacd815d13b9bf4a93054e744d13f2026bd66fc Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 6 Oct 2019 00:06:29 -0700 Subject: [PATCH 035/462] updated to use fabric scp for copying files to remote nodes --- daemon/core/nodes/base.py | 64 ++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 848fae34..901d08a6 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -5,11 +5,13 @@ Defines the base logic for nodes used within core. import logging import os import random +import shutil import socket import string import threading from builtins import range from socket import AF_INET, AF_INET6 +from tempfile import NamedTemporaryFile from fabric import Connection @@ -519,7 +521,7 @@ class CoreNode(CoreNodeBase): :rtype: bool """ try: - self.net_cmd(["kill", "-9", str(self.pid)]) + self.net_cmd(["kill", "-0", str(self.pid)]) except CoreCommandError: return False @@ -961,9 +963,13 @@ class CoreNode(CoreNodeBase): """ logging.info("adding file from %s to %s", srcname, filename) directory = os.path.dirname(filename) - self.client.check_cmd(["mkdir", "-p", directory]) - self.client.check_cmd(["mv", srcname, filename]) - self.client.check_cmd(["sync"]) + if self.server is None: + self.client.check_cmd(["mkdir", "-p", directory]) + self.client.check_cmd(["mv", srcname, filename]) + self.client.check_cmd(["sync"]) + else: + self.net_cmd(["mkdir", "-p", directory]) + self.server_conn.put(srcname, filename) def hostfilename(self, filename): """ @@ -981,21 +987,6 @@ class CoreNode(CoreNodeBase): dirname = os.path.join(self.nodedir, dirname) return os.path.join(dirname, basename) - def opennodefile(self, filename, mode="w"): - """ - Open a node file, within it"s directory. - - :param str filename: file name to open - :param str mode: mode to open file in - :return: open file - :rtype: file - """ - hostfilename = self.hostfilename(filename) - dirname, _basename = os.path.split(hostfilename) - if not os.path.isdir(dirname): - os.makedirs(dirname, mode=0o755) - return open(hostfilename, mode) - def nodefile(self, filename, contents, mode=0o644): """ Create a node file with a given mode. @@ -1005,12 +996,24 @@ class CoreNode(CoreNodeBase): :param int mode: mode for file :return: nothing """ - with self.opennodefile(filename, "w") as open_file: - open_file.write(contents) - os.chmod(open_file.name, mode) - logging.debug( - "node(%s) added file: %s; mode: 0%o", self.name, open_file.name, mode - ) + hostfilename = self.hostfilename(filename) + dirname, _basename = os.path.split(hostfilename) + if self.server is None: + if not os.path.isdir(dirname): + os.makedirs(dirname, mode=0o755) + with open(hostfilename, "w") as open_file: + open_file.write(contents) + os.chmod(open_file.name, mode) + else: + temp = NamedTemporaryFile() + temp.write(contents) + temp.close() + self.net_cmd(["mkdir", "-m", oct(0o755), "-p", dirname]) + self.server_conn.put(temp.name, hostfilename) + self.net_cmd(["chmod", oct(mode), hostfilename]) + logging.debug( + "node(%s) added file: %s; mode: 0%o", self.name, hostfilename, mode + ) def nodefilecopy(self, filename, srcfilename, mode=None): """ @@ -1023,9 +1026,14 @@ class CoreNode(CoreNodeBase): :return: nothing """ hostfilename = self.hostfilename(filename) - self.net_cmd(["cp", "-a", srcfilename, hostfilename]) - if mode is not None: - self.net_cmd(["chmod", oct(mode), hostfilename]) + if self.server is None: + shutil.copy2(srcfilename, hostfilename) + if mode is not None: + os.chmod(hostfilename, mode) + else: + self.server_conn.put(srcfilename, hostfilename) + if mode is not None: + self.net_cmd(["chmod", oct(mode), hostfilename]) logging.info( "node(%s) copied file: %s; mode: %s", self.name, hostfilename, mode ) From 212fec916b77a2d9e594026a5e0caec973a5c25e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 7 Oct 2019 11:58:27 -0700 Subject: [PATCH 036/462] updated how distributed servers are added and connections are created to reduce duplicate connections --- daemon/core/emulator/session.py | 29 +++++++++++++++------ daemon/core/nodes/base.py | 36 ++++++++++++++------------- daemon/examples/python/distributed.py | 8 +++--- 3 files changed, 46 insertions(+), 27 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index f5625232..e0afc53c 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -147,7 +147,7 @@ class Session(object): self.sdt = Sdt(session=self) # distributed servers - self.servers = set() + self.servers = {} # initialize default node services self.services.default_services = { @@ -158,10 +158,21 @@ class Session(object): "host": ("DefaultRoute", "SSH"), } + def add_distributed(self, server): + conn = Connection(server, user="root") + self.servers[server] = conn + def init_distributed(self): for server in self.servers: + conn = self.servers[server] cmd = "mkdir -p %s" % self.session_dir - Connection(server, user="root").run(cmd, hide=False) + conn.run(cmd, hide=False) + + def shutdown_distributed(self): + for server in self.servers: + conn = self.servers[server] + cmd = "rm -rf %s" % self.session_dir + conn.run(cmd, hide=False) @classmethod def get_node_class(cls, _type): @@ -676,6 +687,13 @@ class Session(object): if not name: name = "%s%s" % (node_class.__name__, _id) + # verify distributed server + server = self.servers.get(node_options.emulation_server) + if node_options.emulation_server is not None and server is None: + raise CoreError( + "invalid distributed server: %s" % node_options.emulation_server + ) + # create node logging.info( "creating node(%s) id(%s) name(%s) start(%s)", @@ -694,11 +712,7 @@ class Session(object): ) else: node = self.create_node( - cls=node_class, - _id=_id, - name=name, - start=start, - server=node_options.emulation_server, + cls=node_class, _id=_id, name=name, start=start, server=server ) # set node attributes @@ -972,6 +986,7 @@ class Session(object): preserve = self.options.get_config("preservedir") == "1" if not preserve: shutil.rmtree(self.session_dir, ignore_errors=True) + self.shutdown_distributed() # call session shutdown handlers for handler in self.shutdown_handlers: diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 901d08a6..21324c59 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -13,8 +13,6 @@ from builtins import range from socket import AF_INET, AF_INET6 from tempfile import NamedTemporaryFile -from fabric import Connection - from core import constants, utils from core.emulator.data import LinkData, NodeData from core.emulator.enumerations import LinkTypes, NodeTypes @@ -42,7 +40,8 @@ class NodeBase(object): :param int _id: id :param str name: object name :param bool start: start value - :param str server: remote server node will run on, default is None for localhost + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost """ self.session = session @@ -53,8 +52,6 @@ class NodeBase(object): name = "o%s" % self.id self.name = name self.server = server - if self.server is not None: - self.server_conn = Connection(self.server, user="root") self.type = None self.services = None @@ -103,18 +100,23 @@ class NodeBase(object): return utils.check_cmd(args, env=env) else: args = " ".join(args) - return self.remote_cmd(args) + return self.remote_cmd(args, env=env) - def remote_cmd(self, cmd): + def remote_cmd(self, cmd, env=None): """ Run command remotely using server connection. :param str cmd: command to run + :param dict env: environment for remote command, default is None :return: stdout when success :rtype: str :raises CoreCommandError: when a non-zero exit status occurs """ - result = self.server_conn.run(cmd, hide=False) + if env is None: + result = self.server.run(cmd, hide=False) + else: + logging.info("command env: %s", env) + result = self.server.run(cmd, hide=False, env=env, replace_env=True) if result.exited: raise CoreCommandError( result.exited, result.command, result.stdout, result.stderr @@ -969,7 +971,7 @@ class CoreNode(CoreNodeBase): self.client.check_cmd(["sync"]) else: self.net_cmd(["mkdir", "-p", directory]) - self.server_conn.put(srcname, filename) + self.server.put(srcname, filename) def hostfilename(self, filename): """ @@ -992,7 +994,7 @@ class CoreNode(CoreNodeBase): Create a node file with a given mode. :param str filename: name of file to create - :param contents: contents of file + :param str contents: contents of file :param int mode: mode for file :return: nothing """ @@ -1005,12 +1007,12 @@ class CoreNode(CoreNodeBase): open_file.write(contents) os.chmod(open_file.name, mode) else: - temp = NamedTemporaryFile() - temp.write(contents) + temp = NamedTemporaryFile(delete=False) + temp.write(contents.encode("utf-8")) temp.close() - self.net_cmd(["mkdir", "-m", oct(0o755), "-p", dirname]) - self.server_conn.put(temp.name, hostfilename) - self.net_cmd(["chmod", oct(mode), hostfilename]) + self.net_cmd(["mkdir", "-m", "%o" % 0o755, "-p", dirname]) + self.server.put(temp.name, hostfilename) + self.net_cmd(["chmod", "%o" % mode, hostfilename]) logging.debug( "node(%s) added file: %s; mode: 0%o", self.name, hostfilename, mode ) @@ -1031,9 +1033,9 @@ class CoreNode(CoreNodeBase): if mode is not None: os.chmod(hostfilename, mode) else: - self.server_conn.put(srcfilename, hostfilename) + self.server.put(srcfilename, hostfilename) if mode is not None: - self.net_cmd(["chmod", oct(mode), hostfilename]) + self.net_cmd(["chmod", "%o" % mode, hostfilename]) logging.info( "node(%s) copied file: %s; mode: %s", self.name, hostfilename, mode ) diff --git a/daemon/examples/python/distributed.py b/daemon/examples/python/distributed.py index bed75a47..5b5174f6 100644 --- a/daemon/examples/python/distributed.py +++ b/daemon/examples/python/distributed.py @@ -1,4 +1,5 @@ import logging +import pdb from core.emulator.coreemu import CoreEmu from core.emulator.emudata import NodeOptions @@ -14,7 +15,7 @@ def main(): session = coreemu.create_session() # initialize distributed - session.servers.add("core2") + session.add_distributed("core2") session.init_distributed() # must be in configuration state for nodes to start, when using "node_add" below @@ -25,13 +26,12 @@ def main(): # create nodes options = NodeOptions() - options.emulation_server = "10.10.4.38" options.emulation_server = "core2" session.add_node(node_options=options) # interface = prefixes.create_interface(node_one) # session.add_link(node_one.id, switch.id, interface_one=interface) - # node_two = session.add_node() + session.add_node() # interface = prefixes.create_interface(node_two) # session.add_link(node_two.id, switch.id, interface_one=interface) @@ -46,6 +46,8 @@ def main(): # node_two.client.icmd(["iperf", "-t", "10", "-c", node_one_address]) # node_one.cmd(["killall", "-9", "iperf"]) + pdb.set_trace() + # shutdown session coreemu.shutdown() From b7b0e4222c5ee8312133a3d332528a9fed0dea2d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 8 Oct 2019 15:09:26 -0700 Subject: [PATCH 037/462] updates for basic working distrbuted network using fabric --- daemon/core/emulator/distributed.py | 27 ++++++ daemon/core/emulator/session.py | 83 +++++++++++++++++-- daemon/core/nodes/base.py | 75 ++++++++--------- daemon/core/nodes/interface.py | 46 ++++++---- daemon/core/nodes/network.py | 39 +++++++-- daemon/core/utils.py | 2 +- daemon/examples/python/distributed.py | 43 +++++----- .../examples/python/distributed_switches.py | 42 ++++++++++ 8 files changed, 261 insertions(+), 96 deletions(-) create mode 100644 daemon/core/emulator/distributed.py create mode 100644 daemon/examples/python/distributed_switches.py diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py new file mode 100644 index 00000000..104d939d --- /dev/null +++ b/daemon/core/emulator/distributed.py @@ -0,0 +1,27 @@ +import logging + +from core.errors import CoreCommandError + + +def remote_cmd(server, cmd, env=None): + """ + Run command remotely using server connection. + + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost + :param str cmd: command to run + :param dict env: environment for remote command, default is None + :return: stdout when success + :rtype: str + :raises CoreCommandError: when a non-zero exit status occurs + """ + logging.info("remote cmd server(%s): %s", server, cmd) + if env is None: + result = server.run(cmd, hide=False) + else: + result = server.run(cmd, hide=False, env=env, replace_env=True) + if result.exited: + raise CoreCommandError( + result.exited, result.command, result.stdout, result.stderr + ) + return result.stdout.strip() diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index e0afc53c..9eb02a07 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -37,9 +37,11 @@ from core.location.event import EventLoop from core.location.mobility import MobilityManager from core.nodes.base import CoreNetworkBase, CoreNode, CoreNodeBase from core.nodes.docker import DockerNode -from core.nodes.ipaddress import MacAddress +from core.nodes.interface import GreTap +from core.nodes.ipaddress import IpAddress, MacAddress from core.nodes.lxd import LxcNode from core.nodes.network import ( + CoreNetwork, CtrlNet, GreTapBridge, HubNode, @@ -148,6 +150,8 @@ class Session(object): # distributed servers self.servers = {} + self.tunnels = {} + self.address = None # initialize default node services self.services.default_services = { @@ -161,19 +165,81 @@ class Session(object): def add_distributed(self, server): conn = Connection(server, user="root") self.servers[server] = conn - - def init_distributed(self): - for server in self.servers: - conn = self.servers[server] - cmd = "mkdir -p %s" % self.session_dir - conn.run(cmd, hide=False) + cmd = "mkdir -p %s" % self.session_dir + conn.run(cmd, hide=False) def shutdown_distributed(self): + # shutdown all tunnels + for key in self.tunnels: + tunnels = self.tunnels[key] + for tunnel in tunnels: + tunnel.shutdown() + + # remove all remote session directories for server in self.servers: conn = self.servers[server] cmd = "rm -rf %s" % self.session_dir conn.run(cmd, hide=False) + # clear tunnels + self.tunnels.clear() + + def initialize_distributed(self): + for node_id in self.nodes: + node = self.nodes[node_id] + + if not isinstance(node, CoreNetwork): + continue + + if isinstance(node, CtrlNet) and node.serverintf is not None: + continue + + for server in self.servers: + conn = self.servers[server] + key = self.tunnelkey(node_id, IpAddress.to_int(server)) + + # local to server + logging.info( + "local tunnel node(%s) to remote(%s) key(%s)", + node.name, + server, + key, + ) + local_tap = GreTap(session=self, remoteip=server, key=key) + local_tap.net_client.create_interface(node.brname, local_tap.localname) + + # server to local + logging.info( + "remote tunnel node(%s) to local(%s) key(%s)", + node.name, + self.address, + key, + ) + remote_tap = GreTap( + session=self, remoteip=self.address, key=key, server=conn + ) + remote_tap.net_client.create_interface( + node.brname, remote_tap.localname + ) + + # save tunnels for shutdown + self.tunnels[key] = [local_tap, remote_tap] + + def tunnelkey(self, n1num, n2num): + """ + Compute a 32-bit key used to uniquely identify a GRE tunnel. + The hash(n1num), hash(n2num) values are used, so node numbers may be + None or string values (used for e.g. "ctrlnet"). + + :param int n1num: node one id + :param int n2num: node two id + :return: tunnel key for the node pair + :rtype: int + """ + logging.debug("creating tunnel key for: %s, %s", n1num, n2num) + key = (self.id << 16) ^ utils.hashkey(n1num) ^ (utils.hashkey(n2num) << 8) + return key & 0xFFFFFFFF + @classmethod def get_node_class(cls, _type): """ @@ -1493,6 +1559,9 @@ class Session(object): self.add_remove_control_interface(node=None, remove=False) self.broker.startup() + # initialize distributed tunnels + self.initialize_distributed() + # instantiate will be invoked again upon Emane configure if self.emane.startup() == self.emane.NOT_READY: return diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 21324c59..82915b38 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -14,6 +14,7 @@ from socket import AF_INET, AF_INET6 from tempfile import NamedTemporaryFile from core import constants, utils +from core.emulator import distributed from core.emulator.data import LinkData, NodeData from core.emulator.enumerations import LinkTypes, NodeTypes from core.errors import CoreCommandError @@ -95,39 +96,7 @@ class NodeBase(object): :rtype: str :raises CoreCommandError: when a non-zero exit status occurs """ - logging.info("net cmd server(%s): %s", self.server, args) - if self.server is None: - return utils.check_cmd(args, env=env) - else: - args = " ".join(args) - return self.remote_cmd(args, env=env) - - def remote_cmd(self, cmd, env=None): - """ - Run command remotely using server connection. - - :param str cmd: command to run - :param dict env: environment for remote command, default is None - :return: stdout when success - :rtype: str - :raises CoreCommandError: when a non-zero exit status occurs - """ - if env is None: - result = self.server.run(cmd, hide=False) - else: - logging.info("command env: %s", env) - result = self.server.run(cmd, hide=False, env=env, replace_env=True) - if result.exited: - raise CoreCommandError( - result.exited, result.command, result.stdout, result.stderr - ) - - logging.info( - "fabric result:\n\tstdout: %s\n\tstderr: %s", - result.stdout.strip(), - result.stderr.strip(), - ) - return result.stdout.strip() + raise NotImplementedError def setposition(self, x=None, y=None, z=None): """ @@ -279,7 +248,8 @@ class CoreNodeBase(NodeBase): :param int _id: object id :param str name: object name :param bool start: boolean for starting - :param str server: remote server node will run on, default is None for localhost + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost """ super(CoreNodeBase, self).__init__(session, _id, name, start, server) self.services = [] @@ -412,6 +382,23 @@ class CoreNodeBase(NodeBase): return common + def net_cmd(self, args, env=None): + """ + Runs a command that is used to configure and setup the network on the host + system. + + :param list[str]|str args: command to run + :param dict env: environment to run command with + :return: combined stdout and stderr + :rtype: str + :raises CoreCommandError: when a non-zero exit status occurs + """ + if self.server is None: + return utils.check_cmd(args, env=env) + else: + args = " ".join(args) + return distributed.remote_cmd(self.server, args, env=env) + def node_net_cmd(self, args): """ Runs a command that is used to configure and setup the network within a @@ -493,7 +480,8 @@ class CoreNode(CoreNodeBase): :param str nodedir: node directory :param str bootsh: boot shell to use :param bool start: start flag - :param str server: remote server node will run on, default is None for localhost + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost """ super(CoreNode, self).__init__(session, _id, name, start, server) self.nodedir = nodedir @@ -653,13 +641,13 @@ class CoreNode(CoreNodeBase): :rtype: str :raises CoreCommandError: when a non-zero exit status occurs """ - logging.info("net cmd server(%s): %s", self.server, args) if self.server is None: + logging.info("node(%s) cmd: %s", self.name, args) return self.check_cmd(args) else: args = self.client._cmd_args() + args args = " ".join(args) - return self.remote_cmd(args) + return distributed.remote_cmd(self.server, args) def check_cmd(self, args): """ @@ -753,7 +741,11 @@ class CoreNode(CoreNodeBase): raise ValueError("interface name (%s) too long" % name) veth = Veth( - node=self, name=name, localname=localname, net=net, start=self.up + node=self, + name=name, + localname=localname, + start=self.up, + server=self.server, ) if self.up: @@ -806,9 +798,7 @@ class CoreNode(CoreNodeBase): sessionid = self.session.short_session_id() localname = "tap%s.%s.%s" % (self.id, ifindex, sessionid) name = ifname - tuntap = TunTap( - node=self, name=name, localname=localname, net=net, start=self.up - ) + tuntap = TunTap(node=self, name=name, localname=localname, start=self.up) try: self.addnetif(tuntap, ifindex) @@ -1057,7 +1047,8 @@ class CoreNetworkBase(NodeBase): :param int _id: object id :param str name: object name :param bool start: should object start - :param str server: remote server node will run on, default is None for localhost + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost """ super(CoreNetworkBase, self).__init__(session, _id, name, start, server) self._linked = {} diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 51859e3a..8b73b1b7 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -7,6 +7,7 @@ import time from builtins import int, range from core import utils +from core.emulator import distributed from core.errors import CoreCommandError from core.nodes.netclient import LinuxNetClient @@ -16,13 +17,15 @@ class CoreInterface(object): Base class for network interfaces. """ - def __init__(self, node, name, mtu): + def __init__(self, node, name, mtu, server=None): """ Creates a PyCoreNetIf instance. :param core.nodes.base.CoreNode node: node for interface :param str name: interface name :param mtu: mtu value + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost """ self.node = node @@ -42,7 +45,15 @@ class CoreInterface(object): self.netindex = None # index used to find flow data self.flow_id = None - self.net_client = LinuxNetClient(utils.check_cmd) + self.server = server + self.net_client = LinuxNetClient(self.net_cmd) + + def net_cmd(self, args): + if self.server is None: + return utils.check_cmd(args) + else: + args = " ".join(args) + return distributed.remote_cmd(self.server, args) def startup(self): """ @@ -191,8 +202,7 @@ class Veth(CoreInterface): Provides virtual ethernet functionality for core nodes. """ - # TODO: network is not used, why was it needed? - def __init__(self, node, name, localname, mtu=1500, net=None, start=True): + def __init__(self, node, name, localname, mtu=1500, server=None, start=True): """ Creates a VEth instance. @@ -200,12 +210,13 @@ class Veth(CoreInterface): :param str name: interface name :param str localname: interface local name :param mtu: interface mtu - :param net: network + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost :param bool start: start flag :raises CoreCommandError: when there is a command exception """ # note that net arg is ignored - CoreInterface.__init__(self, node=node, name=name, mtu=mtu) + CoreInterface.__init__(self, node, name, mtu, server) self.localname = localname self.up = False if start: @@ -251,8 +262,7 @@ class TunTap(CoreInterface): TUN/TAP virtual device in TAP mode """ - # TODO: network is not used, why was it needed? - def __init__(self, node, name, localname, mtu=1500, net=None, start=True): + def __init__(self, node, name, localname, mtu=1500, server=None, start=True): """ Create a TunTap instance. @@ -260,10 +270,11 @@ class TunTap(CoreInterface): :param str name: interface name :param str localname: local interface name :param mtu: interface mtu - :param core.nodes.base.CoreNetworkBase net: related network + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost :param bool start: start flag """ - CoreInterface.__init__(self, node=node, name=name, mtu=mtu) + CoreInterface.__init__(self, node, name, mtu, server) self.localname = localname self.up = False self.transport_type = "virtual" @@ -427,6 +438,7 @@ class GreTap(CoreInterface): ttl=255, key=None, start=True, + server=None, ): """ Creates a GreTap instance. @@ -441,9 +453,11 @@ class GreTap(CoreInterface): :param ttl: ttl value :param key: gre tap key :param bool start: start flag + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost :raises CoreCommandError: when there is a command exception """ - CoreInterface.__init__(self, node=node, name=name, mtu=mtu) + CoreInterface.__init__(self, node, name, mtu, server) self.session = session if _id is None: # from PyCoreObj @@ -460,9 +474,13 @@ class GreTap(CoreInterface): if remoteip is None: raise ValueError("missing remote IP required for GRE TAP device") - self.net_client.create_gretap( - self.localname, str(remoteip), str(localip), str(ttl), str(key) - ) + if localip is not None: + localip = str(localip) + if ttl is not None: + ttl = str(ttl) + if key is not None: + key = str(key) + self.net_client.create_gretap(self.localname, str(remoteip), localip, ttl, key) self.net_client.device_up(self.localname) self.up = True diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 444ded56..3ec84282 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -10,6 +10,7 @@ import time from socket import AF_INET, AF_INET6 from core import constants, utils +from core.emulator import distributed from core.emulator.data import LinkData from core.emulator.enumerations import LinkTypes, NodeTypes, RegisterTlvs from core.errors import CoreCommandError, CoreError @@ -291,7 +292,8 @@ class CoreNetwork(CoreNetworkBase): :param int _id: object id :param str name: object name :param bool start: start flag - :param str server: remote server node will run on, default is None for localhost + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost :param policy: network policy """ CoreNetworkBase.__init__(self, session, _id, name, start, server) @@ -307,6 +309,27 @@ class CoreNetwork(CoreNetworkBase): self.startup() ebq.startupdateloop(self) + def net_cmd(self, args, env=None): + """ + Runs a command that is used to configure and setup the network on the host + system. + + :param list[str]|str args: command to run + :param dict env: environment to run command with + :return: combined stdout and stderr + :rtype: str + :raises CoreCommandError: when a non-zero exit status occurs + """ + logging.info("network node(%s) cmd", self.name) + output = utils.check_cmd(args, env=env) + + args = " ".join(args) + for server in self.session.servers: + conn = self.session.servers[server] + distributed.remote_cmd(conn, args, env=env) + + return output + def startup(self): """ Linux bridge starup logic. @@ -381,11 +404,11 @@ class CoreNetwork(CoreNetworkBase): """ Attach a network interface. - :param core.netns.vnode.VEth netif: network interface to attach + :param core.nodes.interface.Veth netif: network interface to attach :return: nothing """ if self.up: - self.net_client.create_interface(self.brname, netif.localname) + netif.net_client.create_interface(self.brname, netif.localname) CoreNetworkBase.attach(self, netif) @@ -397,7 +420,7 @@ class CoreNetwork(CoreNetworkBase): :return: nothing """ if self.up: - self.net_client.delete_interface(self.brname, netif.localname) + netif.net_client.delete_interface(self.brname, netif.localname) CoreNetworkBase.detach(self, netif) @@ -591,13 +614,11 @@ class CoreNetwork(CoreNetworkBase): if len(name) >= 16: raise ValueError("interface name %s too long" % name) - netif = Veth( - node=None, name=name, localname=localname, mtu=1500, net=self, start=self.up - ) + netif = Veth(node=None, name=name, localname=localname, mtu=1500, start=self.up) self.attach(netif) if net.up: # this is similar to net.attach() but uses netif.name instead of localname - self.net_client.create_interface(net.brname, netif.name) + netif.net_client.create_interface(net.brname, netif.name) i = net.newifindex() net._netif[i] = netif with net._linked_lock: @@ -666,6 +687,8 @@ class GreTapBridge(CoreNetwork): :param ttl: ttl value :param key: gre tap key :param bool start: start flag + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost """ CoreNetwork.__init__(self, session, _id, name, False, server, policy) self.grekey = key diff --git a/daemon/core/utils.py b/daemon/core/utils.py index 20d2384e..8e59a050 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -263,7 +263,7 @@ def check_cmd(args, **kwargs): kwargs["stdout"] = subprocess.PIPE kwargs["stderr"] = subprocess.STDOUT args = split_args(args) - logging.debug("command: %s", args) + logging.info("command: %s", args) try: p = subprocess.Popen(args, **kwargs) stdout, _ = p.communicate() diff --git a/daemon/examples/python/distributed.py b/daemon/examples/python/distributed.py index 5b5174f6..feb5e8bb 100644 --- a/daemon/examples/python/distributed.py +++ b/daemon/examples/python/distributed.py @@ -1,51 +1,46 @@ import logging import pdb +import sys from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import NodeOptions -from core.emulator.enumerations import EventTypes +from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.enumerations import EventTypes, NodeTypes def main(): # ip generator for example - # prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") + prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") # create emulator instance for creating sessions and utility methods coreemu = CoreEmu() session = coreemu.create_session() # initialize distributed - session.add_distributed("core2") - session.init_distributed() + address = sys.argv[1] + remote = sys.argv[2] + session.address = address + session.add_distributed(remote) # must be in configuration state for nodes to start, when using "node_add" below session.set_state(EventTypes.CONFIGURATION_STATE) - # create switch network node - # switch = session.add_node(_type=NodeTypes.SWITCH) - - # create nodes + # create local node, switch, and remote nodes + node_one = session.add_node() + switch = session.add_node(_type=NodeTypes.SWITCH) options = NodeOptions() - options.emulation_server = "core2" - session.add_node(node_options=options) - # interface = prefixes.create_interface(node_one) - # session.add_link(node_one.id, switch.id, interface_one=interface) + options.emulation_server = remote + node_two = session.add_node(node_options=options) - session.add_node() - # interface = prefixes.create_interface(node_two) - # session.add_link(node_two.id, switch.id, interface_one=interface) + # create not interfaces and link + interface_one = prefixes.create_interface(node_one) + interface_two = prefixes.create_interface(node_two) + session.add_link(node_one.id, switch.id, interface_one=interface_one) + session.add_link(node_two.id, switch.id, interface_one=interface_two) # instantiate session session.instantiate() - # print("starting iperf server on node: %s" % node_one.name) - # node_one.cmd(["iperf", "-s", "-D"]) - # node_one_address = prefixes.ip4_address(node_one) - # - # print("node %s connecting to %s" % (node_two.name, node_one_address)) - # node_two.client.icmd(["iperf", "-t", "10", "-c", node_one_address]) - # node_one.cmd(["killall", "-9", "iperf"]) - + # pause script for verification pdb.set_trace() # shutdown session diff --git a/daemon/examples/python/distributed_switches.py b/daemon/examples/python/distributed_switches.py new file mode 100644 index 00000000..c6366d5d --- /dev/null +++ b/daemon/examples/python/distributed_switches.py @@ -0,0 +1,42 @@ +import logging +import pdb +import sys + +from core.emulator.coreemu import CoreEmu +from core.emulator.enumerations import EventTypes, NodeTypes + + +def main(): + # create emulator instance for creating sessions and utility methods + coreemu = CoreEmu() + session = coreemu.create_session() + + # initialize distributed + address = sys.argv[1] + remote = sys.argv[2] + session.address = address + session.add_distributed(remote) + + # must be in configuration state for nodes to start, when using "node_add" below + session.set_state(EventTypes.CONFIGURATION_STATE) + + # create local node, switch, and remote nodes + switch_one = session.add_node(_type=NodeTypes.SWITCH) + switch_two = session.add_node(_type=NodeTypes.SWITCH) + + # create not interfaces and link + session.add_link(switch_one.id, switch_two.id) + + # instantiate session + session.instantiate() + + # pause script for verification + pdb.set_trace() + + # shutdown session + coreemu.shutdown() + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + main() From c8d68c332a65704097ae0fd251eb910402b9c808 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 8 Oct 2019 21:06:22 -0700 Subject: [PATCH 038/462] updates for testing using examples --- daemon/core/emulator/session.py | 9 ++++++++- daemon/core/nodes/network.py | 3 ++- daemon/examples/python/distributed.py | 3 +++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 9eb02a07..31e75de2 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -1869,7 +1869,14 @@ class Session(object): assign_address = self.master prefix = prefixes[0] - logging.info("controlnet prefix: %s - %s", type(prefix), prefix) + logging.info( + "controlnet(%s) prefix(%s) assign(%s) updown(%s) serverintf(%s)", + _id, + prefix, + assign_address, + updown_script, + server_interface, + ) control_net = self.create_node( cls=CtrlNet, _id=_id, diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 3ec84282..f7d6af69 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -808,7 +808,8 @@ class CtrlNet(CoreNetwork): :param prefix: control network ipv4 prefix :param hostid: host id :param bool start: start flag - :param str server: remote server node will run on, default is None for localhost + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost :param str assign_address: assigned address :param str updown_script: updown script :param serverintf: server interface diff --git a/daemon/examples/python/distributed.py b/daemon/examples/python/distributed.py index feb5e8bb..3cb3debd 100644 --- a/daemon/examples/python/distributed.py +++ b/daemon/examples/python/distributed.py @@ -15,6 +15,9 @@ def main(): coreemu = CoreEmu() session = coreemu.create_session() + # set controlnet + session.options.set_config("controlnet", "172.16.0.0/24") + # initialize distributed address = sys.argv[1] remote = sys.argv[2] From 7e45168e777a01835589f249b7ca2647ae29b244 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 8 Oct 2019 21:17:15 -0700 Subject: [PATCH 039/462] distributed example for ptp --- daemon/core/nodes/network.py | 4 +- daemon/examples/python/distributed.py | 2 +- daemon/examples/python/distributed_ptp.py | 50 +++++++++++++++++++ .../examples/python/distributed_switches.py | 2 +- 4 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 daemon/examples/python/distributed_ptp.py diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index f7d6af69..c404db5a 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -852,7 +852,7 @@ class CtrlNet(CoreNetwork): self.brname, self.updown_script, ) - utils.check_cmd([self.updown_script, self.brname, "startup"]) + self.net_cmd([self.updown_script, self.brname, "startup"]) if self.serverintf: self.net_client.create_interface(self.brname, self.serverintf) @@ -880,7 +880,7 @@ class CtrlNet(CoreNetwork): self.brname, self.updown_script, ) - utils.check_cmd([self.updown_script, self.brname, "shutdown"]) + self.net_cmd([self.updown_script, self.brname, "shutdown"]) except CoreCommandError: logging.exception("error issuing shutdown script shutdown") diff --git a/daemon/examples/python/distributed.py b/daemon/examples/python/distributed.py index 3cb3debd..ca9ca928 100644 --- a/daemon/examples/python/distributed.py +++ b/daemon/examples/python/distributed.py @@ -34,7 +34,7 @@ def main(): options.emulation_server = remote node_two = session.add_node(node_options=options) - # create not interfaces and link + # create node interfaces and link interface_one = prefixes.create_interface(node_one) interface_two = prefixes.create_interface(node_two) session.add_link(node_one.id, switch.id, interface_one=interface_one) diff --git a/daemon/examples/python/distributed_ptp.py b/daemon/examples/python/distributed_ptp.py new file mode 100644 index 00000000..6ab5b9dc --- /dev/null +++ b/daemon/examples/python/distributed_ptp.py @@ -0,0 +1,50 @@ +import logging +import pdb +import sys + +from core.emulator.coreemu import CoreEmu +from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.enumerations import EventTypes + + +def main(): + # ip generator for example + prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") + + # create emulator instance for creating sessions and utility methods + coreemu = CoreEmu() + session = coreemu.create_session() + + # initialize distributed + address = sys.argv[1] + remote = sys.argv[2] + session.address = address + session.add_distributed(remote) + + # must be in configuration state for nodes to start, when using "node_add" below + session.set_state(EventTypes.CONFIGURATION_STATE) + + # create local node, switch, and remote nodes + node_one = session.add_node() + options = NodeOptions() + options.emulation_server = remote + node_two = session.add_node(node_options=options) + + # create node interfaces and link + interface_one = prefixes.create_interface(node_one) + interface_two = prefixes.create_interface(node_two) + session.add_link(node_one.id, node_two.id, interface_one, interface_two) + + # instantiate session + session.instantiate() + + # pause script for verification + pdb.set_trace() + + # shutdown session + coreemu.shutdown() + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + main() diff --git a/daemon/examples/python/distributed_switches.py b/daemon/examples/python/distributed_switches.py index c6366d5d..b7ed166b 100644 --- a/daemon/examples/python/distributed_switches.py +++ b/daemon/examples/python/distributed_switches.py @@ -24,7 +24,7 @@ def main(): switch_one = session.add_node(_type=NodeTypes.SWITCH) switch_two = session.add_node(_type=NodeTypes.SWITCH) - # create not interfaces and link + # create node interfaces and link session.add_link(switch_one.id, switch_two.id) # instantiate session From 859f473ba9f50b5207a09494f7c7c7fd1f8ad422 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 9 Oct 2019 12:13:26 -0700 Subject: [PATCH 040/462] updated ebtables to use net_cmd --- daemon/core/nodes/network.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index c404db5a..2fab566c 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -3,7 +3,6 @@ Defines network nodes used within core. """ import logging -import os import socket import threading import time @@ -165,20 +164,20 @@ class EbtablesQueue(object): """ # save kernel ebtables snapshot to a file args = self.ebatomiccmd(["--atomic-save"]) - utils.check_cmd(args) + wlan.net_cmd(args) # modify the table file using queued ebtables commands for c in self.cmds: args = self.ebatomiccmd(c) - utils.check_cmd(args) + wlan.net_cmd(args) self.cmds = [] # commit the table file to the kernel args = self.ebatomiccmd(["--atomic-commit"]) - utils.check_cmd(args) + wlan.net_cmd(args) try: - os.unlink(self.atomic_file) + wlan.net_cmd(["rm", "-f", self.atomic_file]) except OSError: logging.exception("error removing atomic file: %s", self.atomic_file) @@ -312,7 +311,7 @@ class CoreNetwork(CoreNetworkBase): def net_cmd(self, args, env=None): """ Runs a command that is used to configure and setup the network on the host - system. + system and all configured distributed servers. :param list[str]|str args: command to run :param dict env: environment to run command with @@ -341,7 +340,7 @@ class CoreNetwork(CoreNetworkBase): # create a new ebtables chain for this bridge ebtablescmds( - utils.check_cmd, + self.net_cmd, [ [constants.EBTABLES_BIN, "-N", self.brname, "-P", self.policy], [ @@ -372,7 +371,7 @@ class CoreNetwork(CoreNetworkBase): try: self.net_client.delete_bridge(self.brname) ebtablescmds( - utils.check_cmd, + self.net_cmd, [ [ constants.EBTABLES_BIN, @@ -844,7 +843,6 @@ class CtrlNet(CoreNetwork): if self.assign_address: addrlist = ["%s/%s" % (addr, self.prefix.prefixlen)] self.addrconfig(addrlist=addrlist) - logging.info("address %s", addr) if self.updown_script: logging.info( From a4b6b8be510e2a0ea04e365abbaf5c52bbe446ed Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 9 Oct 2019 15:44:45 -0700 Subject: [PATCH 041/462] updated link config to work distributed, added crude locking for fabric --- daemon/core/emulator/distributed.py | 17 +++++-- daemon/core/nodes/base.py | 6 +-- daemon/core/nodes/network.py | 8 +-- daemon/examples/python/distributed_ptp.py | 2 +- daemon/examples/python/distributed_wlan.py | 58 ++++++++++++++++++++++ 5 files changed, 79 insertions(+), 12 deletions(-) create mode 100644 daemon/examples/python/distributed_wlan.py diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index 104d939d..2c7d7bbb 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -1,7 +1,10 @@ import logging +import threading from core.errors import CoreCommandError +LOCK = threading.Lock() + def remote_cmd(server, cmd, env=None): """ @@ -16,12 +19,18 @@ def remote_cmd(server, cmd, env=None): :raises CoreCommandError: when a non-zero exit status occurs """ logging.info("remote cmd server(%s): %s", server, cmd) - if env is None: - result = server.run(cmd, hide=False) - else: - result = server.run(cmd, hide=False, env=env, replace_env=True) + with LOCK: + if env is None: + result = server.run(cmd, hide=False) + else: + result = server.run(cmd, hide=False, env=env, replace_env=True) if result.exited: raise CoreCommandError( result.exited, result.command, result.stdout, result.stderr ) return result.stdout.strip() + + +def remote_put(server, source, destination): + with LOCK: + server.put(source, destination) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 82915b38..4f95c56e 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -961,7 +961,7 @@ class CoreNode(CoreNodeBase): self.client.check_cmd(["sync"]) else: self.net_cmd(["mkdir", "-p", directory]) - self.server.put(srcname, filename) + distributed.remote_put(self.server, srcname, filename) def hostfilename(self, filename): """ @@ -1001,7 +1001,7 @@ class CoreNode(CoreNodeBase): temp.write(contents.encode("utf-8")) temp.close() self.net_cmd(["mkdir", "-m", "%o" % 0o755, "-p", dirname]) - self.server.put(temp.name, hostfilename) + distributed.remote_put(self.server, temp.name, hostfilename) self.net_cmd(["chmod", "%o" % mode, hostfilename]) logging.debug( "node(%s) added file: %s; mode: 0%o", self.name, hostfilename, mode @@ -1023,7 +1023,7 @@ class CoreNode(CoreNodeBase): if mode is not None: os.chmod(hostfilename, mode) else: - self.server.put(srcfilename, hostfilename) + distributed.remote_put(self.server, srcfilename, hostfilename) if mode is not None: self.net_cmd(["chmod", "%o" % mode, hostfilename]) logging.info( diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 2fab566c..81dfc34b 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -525,14 +525,14 @@ class CoreNetwork(CoreNetworkBase): logging.debug( "linkconfig: %s" % ([tc + parent + ["handle", "1:"] + tbf],) ) - utils.check_cmd(tc + parent + ["handle", "1:"] + tbf) + netif.net_cmd(tc + parent + ["handle", "1:"] + tbf) netif.setparam("has_tbf", True) changed = True elif netif.getparam("has_tbf") and bw <= 0: tcd = [] + tc tcd[2] = "delete" if self.up: - utils.check_cmd(tcd + parent) + netif.net_cmd(tcd + parent) netif.setparam("has_tbf", False) # removing the parent removes the child netif.setparam("has_netem", False) @@ -575,14 +575,14 @@ class CoreNetwork(CoreNetworkBase): tc[2] = "delete" if self.up: logging.debug("linkconfig: %s" % ([tc + parent + ["handle", "10:"]],)) - utils.check_cmd(tc + parent + ["handle", "10:"]) + netif.net_cmd(tc + parent + ["handle", "10:"]) netif.setparam("has_netem", False) elif len(netem) > 1: if self.up: logging.debug( "linkconfig: %s" % ([tc + parent + ["handle", "10:"] + netem],) ) - utils.check_cmd(tc + parent + ["handle", "10:"] + netem) + netif.net_cmd(tc + parent + ["handle", "10:"] + netem) netif.setparam("has_netem", True) def linknet(self, net): diff --git a/daemon/examples/python/distributed_ptp.py b/daemon/examples/python/distributed_ptp.py index 6ab5b9dc..2b611816 100644 --- a/daemon/examples/python/distributed_ptp.py +++ b/daemon/examples/python/distributed_ptp.py @@ -25,8 +25,8 @@ def main(): session.set_state(EventTypes.CONFIGURATION_STATE) # create local node, switch, and remote nodes - node_one = session.add_node() options = NodeOptions() + node_one = session.add_node(node_options=options) options.emulation_server = remote node_two = session.add_node(node_options=options) diff --git a/daemon/examples/python/distributed_wlan.py b/daemon/examples/python/distributed_wlan.py new file mode 100644 index 00000000..ca64ee01 --- /dev/null +++ b/daemon/examples/python/distributed_wlan.py @@ -0,0 +1,58 @@ +import logging +import pdb +import sys + +from core.emulator.coreemu import CoreEmu +from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.enumerations import EventTypes, NodeTypes +from core.location.mobility import BasicRangeModel + + +def main(): + # ip generator for example + prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") + + # create emulator instance for creating sessions and utility methods + coreemu = CoreEmu() + session = coreemu.create_session() + + # set controlnet + # session.options.set_config("controlnet", "172.16.0.0/24") + + # initialize distributed + address = sys.argv[1] + remote = sys.argv[2] + session.address = address + session.add_distributed(remote) + + # must be in configuration state for nodes to start, when using "node_add" below + session.set_state(EventTypes.CONFIGURATION_STATE) + + # create local node, switch, and remote nodes + options = NodeOptions() + options.set_position(0, 0) + options.emulation_server = remote + node_one = session.add_node(node_options=options) + wlan = session.add_node(_type=NodeTypes.WIRELESS_LAN) + session.mobility.set_model(wlan, BasicRangeModel) + node_two = session.add_node(node_options=options) + + # create node interfaces and link + interface_one = prefixes.create_interface(node_one) + interface_two = prefixes.create_interface(node_two) + session.add_link(node_one.id, wlan.id, interface_one=interface_one) + session.add_link(node_two.id, wlan.id, interface_one=interface_two) + + # instantiate session + session.instantiate() + + # pause script for verification + pdb.set_trace() + + # shutdown session + coreemu.shutdown() + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + main() From bc586933399fe7b227222a26f87cfa496d760fe7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 10 Oct 2019 11:53:52 -0700 Subject: [PATCH 042/462] updated emane config files to be generated for remote servers, fixed services not using node remote server compatible commands --- daemon/core/emane/emanemanager.py | 13 +++- daemon/core/emane/emanemodel.py | 11 ++- daemon/core/emulator/distributed.py | 41 ++++++++--- daemon/core/nodes/base.py | 6 +- daemon/core/services/coreservices.py | 10 +-- daemon/core/xml/emanexml.py | 77 +++++++++++++++++---- daemon/examples/python/distributed_emane.py | 65 +++++++++++++++++ 7 files changed, 184 insertions(+), 39 deletions(-) create mode 100644 daemon/examples/python/distributed_emane.py diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 746016f9..2902c47c 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -18,6 +18,7 @@ from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emane.nodes import EmaneNet from core.emane.rfpipe import EmaneRfPipeModel from core.emane.tdma import EmaneTdmaModel +from core.emulator import distributed from core.emulator.enumerations import ( ConfigDataTypes, ConfigFlags, @@ -679,8 +680,12 @@ class EmaneManager(ModelManager): return dev = self.get_config("eventservicedevice") - emanexml.create_event_service_xml(group, port, dev, self.session.session_dir) + for server in self.session.servers: + conn = self.session.servers[server] + emanexml.create_event_service_xml( + group, port, dev, self.session.session_dir, conn + ) def startdaemons(self): """ @@ -745,7 +750,7 @@ class EmaneManager(ModelManager): os.path.join(path, "emane%d.log" % n), os.path.join(path, "platform%d.xml" % n), ] - output = node.check_cmd(args) + output = node.node_net_cmd(args) logging.info("node(%s) emane daemon running: %s", node.name, args) logging.info("node(%s) emane daemon output: %s", node.name, output) @@ -756,6 +761,10 @@ class EmaneManager(ModelManager): emanecmd += ["-f", os.path.join(path, "emane.log")] args = emanecmd + [os.path.join(path, "platform.xml")] utils.check_cmd(args, cwd=path) + args = " ".join(args) + for server in self.session.servers: + conn = self.session.servers[server] + distributed.remote_cmd(conn, args, cwd=path) logging.info("host emane daemon running: %s", args) def stopdaemons(self): diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index 56eee289..3ca2a18f 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -102,6 +102,11 @@ class EmaneModel(WirelessModel): mac_name = emanexml.mac_file_name(self, interface) phy_name = emanexml.phy_file_name(self, interface) + # remote server for file + server = None + if interface is not None: + server = interface.node.server + # check if this is external transport_type = "virtual" if interface and interface.transport_type == "raw": @@ -111,16 +116,16 @@ class EmaneModel(WirelessModel): # create nem xml file nem_file = os.path.join(self.session.session_dir, nem_name) emanexml.create_nem_xml( - self, config, nem_file, transport_name, mac_name, phy_name + self, config, nem_file, transport_name, mac_name, phy_name, server ) # create mac xml file mac_file = os.path.join(self.session.session_dir, mac_name) - emanexml.create_mac_xml(self, config, mac_file) + emanexml.create_mac_xml(self, config, mac_file, server) # create phy xml file phy_file = os.path.join(self.session.session_dir, phy_name) - emanexml.create_phy_xml(self, config, phy_file) + emanexml.create_phy_xml(self, config, phy_file, server) def post_startup(self): """ diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index 2c7d7bbb..35cbf208 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -1,12 +1,16 @@ import logging +import os import threading +from tempfile import NamedTemporaryFile + +from invoke import UnexpectedExit from core.errors import CoreCommandError LOCK = threading.Lock() -def remote_cmd(server, cmd, env=None): +def remote_cmd(server, cmd, env=None, cwd=None): """ Run command remotely using server connection. @@ -14,23 +18,38 @@ def remote_cmd(server, cmd, env=None): default is None for localhost :param str cmd: command to run :param dict env: environment for remote command, default is None + :param str cwd: directory to run command in, defaults to None, which is the user's + home directory :return: stdout when success :rtype: str :raises CoreCommandError: when a non-zero exit status occurs """ logging.info("remote cmd server(%s): %s", server, cmd) - with LOCK: - if env is None: - result = server.run(cmd, hide=False) - else: - result = server.run(cmd, hide=False, env=env, replace_env=True) - if result.exited: - raise CoreCommandError( - result.exited, result.command, result.stdout, result.stderr - ) - return result.stdout.strip() + replace_env = env is not None + try: + with LOCK: + if cwd is None: + result = server.run(cmd, hide=False, env=env, replace_env=replace_env) + else: + with server.cd(cwd): + result = server.run( + cmd, hide=False, env=env, replace_env=replace_env + ) + return result.stdout.strip() + except UnexpectedExit as e: + stdout, stderr = e.streams_for_display() + raise CoreCommandError(e.result.exited, cmd, stdout, stderr) def remote_put(server, source, destination): with LOCK: server.put(source, destination) + + +def remote_put_temp(server, destination, data): + with LOCK: + temp = NamedTemporaryFile(delete=False) + temp.write(data.encode("utf-8")) + temp.close() + server.put(temp.name, destination) + os.unlink(temp.name) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 4f95c56e..d8cac8c5 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -11,7 +11,6 @@ import string import threading from builtins import range from socket import AF_INET, AF_INET6 -from tempfile import NamedTemporaryFile from core import constants, utils from core.emulator import distributed @@ -997,11 +996,8 @@ class CoreNode(CoreNodeBase): open_file.write(contents) os.chmod(open_file.name, mode) else: - temp = NamedTemporaryFile(delete=False) - temp.write(contents.encode("utf-8")) - temp.close() self.net_cmd(["mkdir", "-m", "%o" % 0o755, "-p", dirname]) - distributed.remote_put(self.server, temp.name, hostfilename) + distributed.remote_put_temp(self.server, hostfilename, contents) self.net_cmd(["chmod", "%o" % mode, hostfilename]) logging.debug( "node(%s) added file: %s; mode: 0%o", self.name, hostfilename, mode diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index 4563d4b7..b34daa73 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -9,6 +9,7 @@ services. import enum import logging +import shlex import time from multiprocessing.pool import ThreadPool @@ -597,8 +598,9 @@ class CoreServices(object): status = 0 for cmd in cmds: logging.debug("validating service(%s) using: %s", service.name, cmd) + cmd = shlex.split(cmd) try: - node.check_cmd(cmd) + node.node_net_cmd(cmd) except CoreCommandError as e: logging.debug( "node(%s) service(%s) validate failed", node.name, service.name @@ -728,11 +730,11 @@ class CoreServices(object): status = 0 for cmd in cmds: + cmd = shlex.split(cmd) try: if wait: - node.check_cmd(cmd) - else: - node.cmd(cmd, wait=False) + cmd.append("&") + node.node_net_cmd(cmd) except CoreCommandError: logging.exception("error starting command") status = -1 diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index d73f3d5b..3b4fafef 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -1,9 +1,11 @@ import logging import os +from tempfile import NamedTemporaryFile from lxml import etree from core import utils +from core.emulator import distributed from core.nodes.ipaddress import MacAddress from core.xml import corexml @@ -44,20 +46,29 @@ def _value_to_params(value): return None -def create_file(xml_element, doc_name, file_path): +def create_file(xml_element, doc_name, file_path, server=None): """ Create xml file. :param lxml.etree.Element xml_element: root element to write to file :param str doc_name: name to use in the emane doctype :param str file_path: file path to write xml file to + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost :return: nothing """ doctype = ( '' % {"doc_name": doc_name} ) - corexml.write_xml_file(xml_element, file_path, doctype=doctype) + if server is not None: + temp = NamedTemporaryFile(delete=False) + create_file(xml_element, doc_name, temp.name) + temp.close() + distributed.remote_put(server, temp.name, file_path) + os.unlink(temp.name) + else: + corexml.write_xml_file(xml_element, file_path, doctype=doctype) def add_param(xml_element, name, value): @@ -204,17 +215,18 @@ def build_node_platform_xml(emane_manager, control_net, node, nem_id, platform_x # increment nem id nem_id += 1 + doc_name = "platform" for key in sorted(platform_xmls.keys()): + platform_element = platform_xmls[key] if key == "host": file_name = "platform.xml" + file_path = os.path.join(emane_manager.session.session_dir, file_name) + create_file(platform_element, doc_name, file_path) else: file_name = "platform%d.xml" % key - - platform_element = platform_xmls[key] - - doc_name = "platform" - file_path = os.path.join(emane_manager.session.session_dir, file_name) - create_file(platform_element, doc_name, file_path) + file_path = os.path.join(emane_manager.session.session_dir, file_name) + linked_node = emane_manager.session.nodes[key] + create_file(platform_element, doc_name, file_path, linked_node.server) return nem_id @@ -303,15 +315,20 @@ def build_transport_xml(emane_manager, node, transport_type): file_name = transport_file_name(node.id, transport_type) file_path = os.path.join(emane_manager.session.session_dir, file_name) create_file(transport_element, doc_name, file_path) + for server in emane_manager.session.servers: + conn = emane_manager.session.servers[server] + create_file(transport_element, doc_name, file_path, conn) -def create_phy_xml(emane_model, config, file_path): +def create_phy_xml(emane_model, config, file_path, server): """ Create the phy xml document. :param core.emane.emanemodel.EmaneModel emane_model: emane model to create xml :param dict config: all current configuration values :param str file_path: path to write file to + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost :return: nothing """ phy_element = etree.Element("phy", name="%s PHY" % emane_model.name) @@ -322,15 +339,24 @@ def create_phy_xml(emane_model, config, file_path): phy_element, emane_model.phy_config, config, emane_model.config_ignore ) create_file(phy_element, "phy", file_path) + if server is not None: + create_file(phy_element, "phy", file_path, server) + else: + create_file(phy_element, "phy", file_path) + for server in emane_model.session.servers: + conn = emane_model.session.servers[server] + create_file(phy_element, "phy", file_path, conn) -def create_mac_xml(emane_model, config, file_path): +def create_mac_xml(emane_model, config, file_path, server): """ Create the mac xml document. :param core.emane.emanemodel.EmaneModel emane_model: emane model to create xml :param dict config: all current configuration values :param str file_path: path to write file to + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost :return: nothing """ if not emane_model.mac_library: @@ -343,10 +369,23 @@ def create_mac_xml(emane_model, config, file_path): mac_element, emane_model.mac_config, config, emane_model.config_ignore ) create_file(mac_element, "mac", file_path) + if server is not None: + create_file(mac_element, "mac", file_path, server) + else: + create_file(mac_element, "mac", file_path) + for server in emane_model.session.servers: + conn = emane_model.session.servers[server] + create_file(mac_element, "mac", file_path, conn) def create_nem_xml( - emane_model, config, nem_file, transport_definition, mac_definition, phy_definition + emane_model, + config, + nem_file, + transport_definition, + mac_definition, + phy_definition, + server, ): """ Create the nem xml document. @@ -357,6 +396,8 @@ def create_nem_xml( :param str transport_definition: transport file definition path :param str mac_definition: mac file definition path :param str phy_definition: phy file definition path + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost :return: nothing """ nem_element = etree.Element("nem", name="%s NEM" % emane_model.name) @@ -366,10 +407,16 @@ def create_nem_xml( etree.SubElement(nem_element, "transport", definition=transport_definition) etree.SubElement(nem_element, "mac", definition=mac_definition) etree.SubElement(nem_element, "phy", definition=phy_definition) - create_file(nem_element, "nem", nem_file) + if server is not None: + create_file(nem_element, "nem", nem_file, server) + else: + create_file(nem_element, "nem", nem_file) + for server in emane_model.session.servers: + conn = emane_model.session.servers[server] + create_file(nem_element, "nem", nem_file, conn) -def create_event_service_xml(group, port, device, file_directory): +def create_event_service_xml(group, port, device, file_directory, server=None): """ Create a emane event service xml file. @@ -377,6 +424,8 @@ def create_event_service_xml(group, port, device, file_directory): :param str port: event port :param str device: event device :param str file_directory: directory to create file in + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost :return: nothing """ event_element = etree.Element("emaneeventmsgsvc") @@ -391,7 +440,7 @@ def create_event_service_xml(group, port, device, file_directory): sub_element.text = value file_name = "libemaneeventservice.xml" file_path = os.path.join(file_directory, file_name) - create_file(event_element, "emaneeventmsgsvc", file_path) + create_file(event_element, "emaneeventmsgsvc", file_path, server) def transport_file_name(node_id, transport_type): diff --git a/daemon/examples/python/distributed_emane.py b/daemon/examples/python/distributed_emane.py new file mode 100644 index 00000000..1ffe5795 --- /dev/null +++ b/daemon/examples/python/distributed_emane.py @@ -0,0 +1,65 @@ +import logging +import pdb +import sys + +from core.emane.ieee80211abg import EmaneIeee80211abgModel +from core.emulator.coreemu import CoreEmu +from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.enumerations import EventTypes, NodeTypes + + +def main(): + # ip generator for example + prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") + + # create emulator instance for creating sessions and utility methods + coreemu = CoreEmu() + session = coreemu.create_session() + + # set controlnet + session.options.set_config( + "controlnet", + "core1:172.16.1.0/24 core2:172.16.2.0/24 core3:172.16.3.0/24 " + "core4:172.16.4.0/24 core5:172.16.5.0/24", + ) + + # initialize distributed + address = sys.argv[1] + remote = sys.argv[2] + session.address = address + session.add_distributed(remote) + + # must be in configuration state for nodes to start, when using "node_add" below + session.set_state(EventTypes.CONFIGURATION_STATE) + + # create local node, switch, and remote nodes + options = NodeOptions(model="mdr") + options.set_position(0, 0) + node_one = session.add_node(node_options=options) + emane_net = session.add_node(_type=NodeTypes.EMANE) + session.emane.set_model(emane_net, EmaneIeee80211abgModel) + options.emulation_server = remote + node_two = session.add_node(node_options=options) + + # create node interfaces and link + interface_one = prefixes.create_interface(node_one) + interface_two = prefixes.create_interface(node_two) + session.add_link(node_one.id, emane_net.id, interface_one=interface_one) + session.add_link(node_two.id, emane_net.id, interface_one=interface_two) + + # instantiate session + try: + session.instantiate() + except Exception: + logging.exception("error during instantiate") + + # pause script for verification + pdb.set_trace() + + # shutdown session + coreemu.shutdown() + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + main() From f6cdeb23de01aba78089c7c8192883ab9ece365b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 10 Oct 2019 15:25:12 -0700 Subject: [PATCH 043/462] changes to update commands to leverage either node_net_cmd/net_cmd --- daemon/core/api/tlv/corehandlers.py | 13 +++++++++---- daemon/core/emane/emanemanager.py | 24 ++++++++++++++++++------ daemon/core/emulator/distributed.py | 10 ++++++++-- daemon/core/emulator/session.py | 3 ++- daemon/core/errors.py | 7 ++++++- daemon/core/nodes/base.py | 10 ++++++---- daemon/core/nodes/client.py | 5 +++-- daemon/core/nodes/physical.py | 2 +- daemon/core/services/coreservices.py | 9 +++------ daemon/examples/python/switch.py | 9 ++++++--- daemon/examples/python/wlan.py | 6 +++--- daemon/tests/emane/test_emane.py | 2 +- daemon/tests/test_core.py | 4 +--- 13 files changed, 67 insertions(+), 37 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index e520ce95..a531efe2 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -37,7 +37,7 @@ from core.emulator.enumerations import ( RegisterTlvs, SessionTlvs, ) -from core.errors import CoreError +from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel from core.nodes.network import WlanNode from core.services.coreservices import ServiceManager, ServiceShim @@ -882,16 +882,21 @@ class CoreHandler(socketserver.BaseRequestHandler): return (reply,) else: logging.info("execute message with cmd=%s", command) + command = utils.split_args(command) # execute command and send a response if ( message.flags & MessageFlags.STRING.value or message.flags & MessageFlags.TEXT.value ): - # shlex.split() handles quotes within the string if message.flags & MessageFlags.LOCAL.value: status, res = utils.cmd_output(command) else: - status, res = node.cmd_output(command) + try: + res = node.node_net_cmd(command) + status = 0 + except CoreCommandError as e: + res = e.stderr + status = e.returncode logging.info( "done exec cmd=%s with status=%d res=(%d bytes)", command, @@ -913,7 +918,7 @@ class CoreHandler(socketserver.BaseRequestHandler): if message.flags & MessageFlags.LOCAL.value: utils.mute_detach(command) else: - node.cmd(command, wait=False) + node.node_net_cmd(command, wait=False) except CoreError: logging.exception("error getting object: %s", node_num) # XXX wait and queue this message to try again later diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 2902c47c..49cf4f24 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -772,7 +772,8 @@ class EmaneManager(ModelManager): Kill the appropriate EMANE daemons. """ # TODO: we may want to improve this if we had the PIDs from the specific EMANE daemons that we"ve started - args = ["killall", "-q", "emane"] + kill_emaned = ["killall", "-q", "emane"] + kill_transortd = ["killall", "-q", "emanetransportd"] stop_emane_on_host = False for node in self.getnodes(): if hasattr(node, "transport_type") and node.transport_type == "raw": @@ -780,13 +781,19 @@ class EmaneManager(ModelManager): continue if node.up: - node.cmd(args, wait=False) + node.node_net_cmd(kill_emaned, wait=False) # TODO: RJ45 node if stop_emane_on_host: try: - utils.check_cmd(args) - utils.check_cmd(["killall", "-q", "emanetransportd"]) + utils.check_cmd(kill_emaned) + utils.check_cmd(kill_transortd) + kill_emaned = " ".join(kill_emaned) + kill_transortd = " ".join(kill_transortd) + for server in self.session.servers: + conn = self.session[server] + distributed.remote_cmd(conn, kill_emaned) + distributed.remote_cmd(conn, kill_transortd) except CoreCommandError: logging.exception("error shutting down emane daemons") @@ -976,8 +983,13 @@ class EmaneManager(ModelManager): Return True if an EMANE process associated with the given node is running, False otherwise. """ args = ["pkill", "-0", "-x", "emane"] - status = node.cmd(args) - return status == 0 + try: + node.node_net_cmd(args) + result = True + except CoreCommandError: + result = False + + return result class EmaneGlobalModel(EmaneModel): diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index 35cbf208..abec0a57 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -10,7 +10,7 @@ from core.errors import CoreCommandError LOCK = threading.Lock() -def remote_cmd(server, cmd, env=None, cwd=None): +def remote_cmd(server, cmd, env=None, cwd=None, wait=True): """ Run command remotely using server connection. @@ -20,12 +20,18 @@ def remote_cmd(server, cmd, env=None, cwd=None): :param dict env: environment for remote command, default is None :param str cwd: directory to run command in, defaults to None, which is the user's home directory + :param bool wait: True to wait for status, False to background process :return: stdout when success :rtype: str :raises CoreCommandError: when a non-zero exit status occurs """ - logging.info("remote cmd server(%s): %s", server, cmd) + replace_env = env is not None + if not wait: + cmd += " &" + logging.info( + "remote cmd server(%s) cwd(%s) wait(%s): %s", server.host, cwd, wait, cmd + ) try: with LOCK: if cwd is None: diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 31e75de2..9bc2ab80 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -2044,4 +2044,5 @@ class Session(object): utils.mute_detach(data) else: node = self.get_node(node_id) - node.cmd(data, wait=False) + data = utils.split_args(data) + node.node_net_cmd(data, wait=False) diff --git a/daemon/core/errors.py b/daemon/core/errors.py index bb124434..5b76abb3 100644 --- a/daemon/core/errors.py +++ b/daemon/core/errors.py @@ -10,7 +10,12 @@ class CoreCommandError(subprocess.CalledProcessError): """ def __str__(self): - return "Command(%s), Status(%s):\n%s" % (self.cmd, self.returncode, self.output) + return "Command(%s), Status(%s):\nstdout: %s\nstderr: %s" % ( + self.cmd, + self.returncode, + self.output, + self.stderr, + ) class CoreError(Exception): diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index d8cac8c5..147989d7 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -630,29 +630,31 @@ class CoreNode(CoreNodeBase): """ return self.client.cmd_output(args) - def node_net_cmd(self, args): + def node_net_cmd(self, args, wait=True): """ Runs a command that is used to configure and setup the network within a node. - :param list[str]|str args: command to run + :param list[str] args: command to run + :param bool wait: True to wait for status, False otherwise :return: combined stdout and stderr :rtype: str :raises CoreCommandError: when a non-zero exit status occurs """ if self.server is None: logging.info("node(%s) cmd: %s", self.name, args) - return self.check_cmd(args) + return self.client.check_cmd(args, wait=wait) else: args = self.client._cmd_args() + args args = " ".join(args) - return distributed.remote_cmd(self.server, args) + return distributed.remote_cmd(self.server, args, wait=wait) def check_cmd(self, args): """ Runs shell command on node. :param list[str]|str args: command to run + :param bool wait: True to wait for status, False otherwise :return: combined stdout and stderr :rtype: str :raises CoreCommandError: when a non-zero exit status occurs diff --git a/daemon/core/nodes/client.py b/daemon/core/nodes/client.py index 6b7fa44c..6c72547b 100644 --- a/daemon/core/nodes/client.py +++ b/daemon/core/nodes/client.py @@ -97,17 +97,18 @@ class VnodeClient(object): status = p.wait() return status, output.decode("utf-8").strip() - def check_cmd(self, args): + def check_cmd(self, args, wait=True): """ Run command and return exit status and combined stdout and stderr. :param list[str]|str args: command to run + :param bool wait: True to wait for command status, False otherwise :return: combined stdout and stderr :rtype: str :raises core.CoreCommandError: when there is a non-zero exit status """ status, output = self.cmd_output(args) - if status != 0: + if wait and status != 0: raise CoreCommandError(status, args, output) return output.strip() diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index ba8cdc25..48bd5a5a 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -94,7 +94,7 @@ class PhysicalNode(CoreNodeBase): return output.strip() def shcmd(self, cmdstr, sh="/bin/sh"): - return self.cmd([sh, "-c", cmdstr]) + return self.node_net_cmd([sh, "-c", cmdstr]) def sethwaddr(self, ifindex, addr): """ diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index b34daa73..d6eeb1b5 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -9,7 +9,6 @@ services. import enum import logging -import shlex import time from multiprocessing.pool import ThreadPool @@ -598,7 +597,7 @@ class CoreServices(object): status = 0 for cmd in cmds: logging.debug("validating service(%s) using: %s", service.name, cmd) - cmd = shlex.split(cmd) + cmd = utils.split_args(cmd) try: node.node_net_cmd(cmd) except CoreCommandError as e: @@ -730,11 +729,9 @@ class CoreServices(object): status = 0 for cmd in cmds: - cmd = shlex.split(cmd) + cmd = utils.split_args(cmd) try: - if wait: - cmd.append("&") - node.node_net_cmd(cmd) + node.node_net_cmd(cmd, wait) except CoreCommandError: logging.exception("error starting command") status = -1 diff --git a/daemon/examples/python/switch.py b/daemon/examples/python/switch.py index d669478d..3c6ec383 100644 --- a/daemon/examples/python/switch.py +++ b/daemon/examples/python/switch.py @@ -43,11 +43,14 @@ def example(options): last_node = session.get_node(options.nodes + 1) print("starting iperf server on node: %s" % first_node.name) - first_node.cmd(["iperf", "-s", "-D"]) + first_node.node_net_cmd(["iperf", "-s", "-D"]) first_node_address = prefixes.ip4_address(first_node) print("node %s connecting to %s" % (last_node.name, first_node_address)) - last_node.client.icmd(["iperf", "-t", str(options.time), "-c", first_node_address]) - first_node.cmd(["killall", "-9", "iperf"]) + output = last_node.node_net_cmd( + ["iperf", "-t", str(options.time), "-c", first_node_address] + ) + print(output) + first_node.node_net_cmd(["killall", "-9", "iperf"]) # shutdown session coreemu.shutdown() diff --git a/daemon/examples/python/wlan.py b/daemon/examples/python/wlan.py index 376f34d0..3d5171c2 100644 --- a/daemon/examples/python/wlan.py +++ b/daemon/examples/python/wlan.py @@ -47,11 +47,11 @@ def example(options): last_node = session.get_node(options.nodes + 1) print("starting iperf server on node: %s" % first_node.name) - first_node.cmd(["iperf", "-s", "-D"]) + first_node.node_net_cmd(["iperf", "-s", "-D"]) address = prefixes.ip4_address(first_node) print("node %s connecting to %s" % (last_node.name, address)) - last_node.client.icmd(["iperf", "-t", str(options.time), "-c", address]) - first_node.cmd(["killall", "-9", "iperf"]) + last_node.node_net_cmd(["iperf", "-t", str(options.time), "-c", address]) + first_node.node_net_cmd(["killall", "-9", "iperf"]) # shutdown session coreemu.shutdown() diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py index 85836605..d9001065 100644 --- a/daemon/tests/emane/test_emane.py +++ b/daemon/tests/emane/test_emane.py @@ -26,7 +26,7 @@ _DIR = os.path.dirname(os.path.abspath(__file__)) def ping(from_node, to_node, ip_prefixes, count=3): address = ip_prefixes.ip4_address(to_node) - return from_node.cmd(["ping", "-c", str(count), address]) + return from_node.node_net_cmd(["ping", "-c", str(count), address]) class TestEmane: diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index e120dcc8..7d64ae69 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -38,7 +38,7 @@ def createclients(sessiondir, clientcls=VnodeClient, cmdchnlfilterfunc=None): def ping(from_node, to_node, ip_prefixes): address = ip_prefixes.ip4_address(to_node) - return from_node.cmd(["ping", "-c", "3", address]) + return from_node.node_net_cmd(["ping", "-c", "3", address]) class TestCore: @@ -102,7 +102,6 @@ class TestCore: # check various command using vcmd module command = ["ls"] - assert not client.cmd(command) status, output = client.cmd_output(command) assert not status p, stdin, stdout, stderr = client.popen(command) @@ -110,7 +109,6 @@ class TestCore: assert not client.icmd(command) # check various command using command line - assert not client.cmd(command) status, output = client.cmd_output(command) assert not status p, stdin, stdout, stderr = client.popen(command) From ec2e959bda3950a18be97ea4d0bfa5fc7f5c2e9c Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 10 Oct 2019 17:02:28 -0700 Subject: [PATCH 044/462] move images files, create ip and mac for links, progress on node command and node terminal, progress on save and open xml --- coretk/coretk/app.py | 42 ++++- coretk/coretk/coregrpc.py | 151 +++++++++++---- coretk/coretk/coremenubar.py | 56 ++++-- coretk/coretk/coretoolbar.py | 126 +++++++------ coretk/coretk/graph.py | 177 +++++++++++++++--- coretk/coretk/grpcmanagement.py | 71 +++++++ coretk/coretk/{ => icons}/OVS.gif | Bin coretk/coretk/{ => icons}/core-icon.png | Bin .../{ => icons}/document-properties.gif | Bin coretk/coretk/{ => icons}/host.gif | Bin coretk/coretk/{ => icons}/hub.gif | Bin coretk/coretk/{ => icons}/lanswitch.gif | Bin coretk/coretk/{ => icons}/link.gif | Bin coretk/coretk/{ => icons}/marker.gif | Bin coretk/coretk/{ => icons}/mdr.gif | Bin coretk/coretk/{ => icons}/observe.gif | Bin coretk/coretk/{ => icons}/oval.gif | Bin coretk/coretk/{ => icons}/pc.gif | Bin coretk/coretk/{ => icons}/plot.gif | Bin coretk/coretk/{ => icons}/rectangle.gif | Bin coretk/coretk/{ => icons}/rj45.gif | Bin coretk/coretk/{ => icons}/router.gif | Bin coretk/coretk/{ => icons}/router_green.gif | Bin coretk/coretk/{ => icons}/run.gif | Bin coretk/coretk/{ => icons}/select.gif | Bin coretk/coretk/{ => icons}/start.gif | Bin coretk/coretk/{ => icons}/stop.gif | Bin coretk/coretk/{ => icons}/text.gif | Bin coretk/coretk/{ => icons}/tunnel.gif | Bin coretk/coretk/{ => icons}/twonode.gif | Bin coretk/coretk/{ => icons}/wlan.gif | Bin coretk/coretk/images.py | 83 ++++---- coretk/coretk/interface.py | 43 +++++ coretk/coretk/menuaction.py | 129 ++++++++++--- coretk/coretk/oldimage/OVS.gif | Bin 0 -> 744 bytes coretk/coretk/oldimage/core-icon.png | Bin 0 -> 2931 bytes .../coretk/oldimage/document-properties.gif | Bin 0 -> 635 bytes coretk/coretk/oldimage/host.gif | Bin 0 -> 1189 bytes coretk/coretk/oldimage/hub.gif | Bin 0 -> 719 bytes coretk/coretk/oldimage/lanswitch.gif | Bin 0 -> 744 bytes coretk/coretk/oldimage/link.gif | Bin 0 -> 86 bytes coretk/coretk/oldimage/marker.gif | Bin 0 -> 375 bytes coretk/coretk/oldimage/mdr.gif | Bin 0 -> 1276 bytes coretk/coretk/oldimage/observe.gif | Bin 0 -> 1149 bytes coretk/coretk/oldimage/oval.gif | Bin 0 -> 174 bytes coretk/coretk/oldimage/pc.gif | Bin 0 -> 1300 bytes coretk/coretk/oldimage/plot.gif | Bin 0 -> 265 bytes coretk/coretk/oldimage/rectangle.gif | Bin 0 -> 160 bytes coretk/coretk/oldimage/rj45.gif | Bin 0 -> 755 bytes coretk/coretk/oldimage/router.gif | Bin 0 -> 1152 bytes coretk/coretk/oldimage/router_green.gif | Bin 0 -> 753 bytes coretk/coretk/oldimage/run.gif | Bin 0 -> 324 bytes coretk/coretk/oldimage/select.gif | Bin 0 -> 925 bytes coretk/coretk/oldimage/start.gif | Bin 0 -> 1131 bytes coretk/coretk/oldimage/stop.gif | Bin 0 -> 1204 bytes coretk/coretk/oldimage/text.gif | Bin 0 -> 127 bytes coretk/coretk/oldimage/tunnel.gif | Bin 0 -> 799 bytes coretk/coretk/oldimage/twonode.gif | Bin 0 -> 220 bytes coretk/coretk/oldimage/wlan.gif | Bin 0 -> 146 bytes coretk/coretk/prev_saved_xml.txt | 5 + output.txt | 23 +++ 61 files changed, 703 insertions(+), 203 deletions(-) rename coretk/coretk/{ => icons}/OVS.gif (100%) rename coretk/coretk/{ => icons}/core-icon.png (100%) rename coretk/coretk/{ => icons}/document-properties.gif (100%) rename coretk/coretk/{ => icons}/host.gif (100%) rename coretk/coretk/{ => icons}/hub.gif (100%) rename coretk/coretk/{ => icons}/lanswitch.gif (100%) rename coretk/coretk/{ => icons}/link.gif (100%) rename coretk/coretk/{ => icons}/marker.gif (100%) rename coretk/coretk/{ => icons}/mdr.gif (100%) rename coretk/coretk/{ => icons}/observe.gif (100%) rename coretk/coretk/{ => icons}/oval.gif (100%) rename coretk/coretk/{ => icons}/pc.gif (100%) rename coretk/coretk/{ => icons}/plot.gif (100%) rename coretk/coretk/{ => icons}/rectangle.gif (100%) rename coretk/coretk/{ => icons}/rj45.gif (100%) rename coretk/coretk/{ => icons}/router.gif (100%) rename coretk/coretk/{ => icons}/router_green.gif (100%) rename coretk/coretk/{ => icons}/run.gif (100%) rename coretk/coretk/{ => icons}/select.gif (100%) rename coretk/coretk/{ => icons}/start.gif (100%) rename coretk/coretk/{ => icons}/stop.gif (100%) rename coretk/coretk/{ => icons}/text.gif (100%) rename coretk/coretk/{ => icons}/tunnel.gif (100%) rename coretk/coretk/{ => icons}/twonode.gif (100%) rename coretk/coretk/{ => icons}/wlan.gif (100%) create mode 100644 coretk/coretk/interface.py create mode 100755 coretk/coretk/oldimage/OVS.gif create mode 100644 coretk/coretk/oldimage/core-icon.png create mode 100644 coretk/coretk/oldimage/document-properties.gif create mode 100644 coretk/coretk/oldimage/host.gif create mode 100644 coretk/coretk/oldimage/hub.gif create mode 100644 coretk/coretk/oldimage/lanswitch.gif create mode 100644 coretk/coretk/oldimage/link.gif create mode 100644 coretk/coretk/oldimage/marker.gif create mode 100644 coretk/coretk/oldimage/mdr.gif create mode 100644 coretk/coretk/oldimage/observe.gif create mode 100644 coretk/coretk/oldimage/oval.gif create mode 100644 coretk/coretk/oldimage/pc.gif create mode 100644 coretk/coretk/oldimage/plot.gif create mode 100644 coretk/coretk/oldimage/rectangle.gif create mode 100644 coretk/coretk/oldimage/rj45.gif create mode 100644 coretk/coretk/oldimage/router.gif create mode 100644 coretk/coretk/oldimage/router_green.gif create mode 100644 coretk/coretk/oldimage/run.gif create mode 100644 coretk/coretk/oldimage/select.gif create mode 100644 coretk/coretk/oldimage/start.gif create mode 100644 coretk/coretk/oldimage/stop.gif create mode 100644 coretk/coretk/oldimage/text.gif create mode 100644 coretk/coretk/oldimage/tunnel.gif create mode 100644 coretk/coretk/oldimage/twonode.gif create mode 100644 coretk/coretk/oldimage/wlan.gif create mode 100644 coretk/coretk/prev_saved_xml.txt create mode 100644 output.txt diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 9a9506e2..6328b3bc 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -6,7 +6,8 @@ from coretk.coregrpc import CoreGrpc from coretk.coremenubar import CoreMenubar from coretk.coretoolbar import CoreToolbar from coretk.graph import CanvasGraph -from coretk.images import Images +from coretk.images import ImageEnum, Images +from coretk.menuaction import MenuAction class Application(tk.Frame): @@ -15,13 +16,15 @@ class Application(tk.Frame): self.load_images() self.setup_app() self.menubar = None + self.core_menu = None self.canvas = None - - # start grpc - self.core_grpc = CoreGrpc() + self.core_editbar = None + self.core_grpc = None self.create_menu() self.create_widgets() + self.draw_canvas() + self.start_grpc() def load_images(self): """ @@ -33,23 +36,24 @@ class Application(tk.Frame): def setup_app(self): self.master.title("CORE") self.master.geometry("1000x800") - image = Images.get("core") + image = Images.get(ImageEnum.CORE.value) self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) def create_menu(self): self.master.option_add("*tearOff", tk.FALSE) self.menubar = tk.Menu(self.master) - core_menu = CoreMenubar(self, self.master, self.menubar) - core_menu.create_core_menubar() + self.core_menu = CoreMenubar(self, self.master, self.menubar) + self.core_menu.create_core_menubar() self.master.config(menu=self.menubar) def create_widgets(self): edit_frame = tk.Frame(self) edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) - core_editbar = CoreToolbar(self.master, edit_frame, self.menubar) - core_editbar.create_toolbar() + self.core_editbar = CoreToolbar(self.master, edit_frame, self.menubar) + self.core_editbar.create_toolbar() + def draw_canvas(self): self.canvas = CanvasGraph( master=self, grpc=self.core_grpc, @@ -58,7 +62,7 @@ class Application(tk.Frame): ) self.canvas.pack(fill=tk.BOTH, expand=True) - core_editbar.update_canvas(self.canvas) + self.core_editbar.update_canvas(self.canvas) scroll_x = tk.Scrollbar( self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview @@ -78,8 +82,26 @@ class Application(tk.Frame): b = tk.Button(status_bar, text="Button 3") b.pack(side=tk.LEFT, padx=1) + def start_grpc(self): + """ + Conect client to grpc, query sessions and prompt use to choose an existing session if there exist any + + :return: nothing + """ + self.master.update() + self.core_grpc = CoreGrpc(self.master) + self.core_grpc.set_up() + self.canvas.core_grpc = self.core_grpc + self.canvas.draw_existing_component() + + def on_closing(self): + menu_action = MenuAction(self, self.master) + menu_action.on_quit() + # self.quit() + if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) app = Application() + app.master.protocol("WM_DELETE_WINDOW", app.on_closing) app.mainloop() diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index bcd04c96..abcad792 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -2,26 +2,28 @@ Incorporate grpc into python tkinter GUI """ import logging +import os +import tkinter as tk from core.api.grpc import client, core_pb2 class CoreGrpc: - def __init__(self): + def __init__(self, master): """ Create a CoreGrpc instance """ + print("Create grpc instance") self.core = client.CoreGrpcClient() self.session_id = None - self.set_up() + self.master = master + + # self.set_up() self.interface_helper = None def log_event(self, event): logging.info("event: %s", event) - def redraw_canvas(self): - return - def create_new_session(self): """ Create a new session @@ -35,6 +37,29 @@ class CoreGrpc: self.session_id = response.session_id self.core.events(self.session_id, self.log_event) + def _enter_session(self, session_id, dialog): + """ + enter an existing session + + :return: + """ + dialog.destroy() + response = self.core.get_session(session_id) + self.session_id = session_id + print("set session id: %s", session_id) + logging.info("Entering session_id %s.... Result: %s", session_id, response) + # self.master.canvas.draw_existing_component() + + def _create_session(self, dialog): + """ + create a new session + + :param tkinter.Toplevel dialog: save core session prompt dialog + :return: nothing + """ + dialog.destroy() + self.create_new_session() + def query_existing_sessions(self, sessions): """ Query for existing sessions and prompt to join one @@ -43,17 +68,30 @@ class CoreGrpc: :return: nothing """ + dialog = tk.Toplevel() + dialog.title("CORE sessions") for session in sessions: - logging.info("Session id: %s, Session state: %s", session.id, session.state) - logging.info("Input a session you want to enter from the keyboard:") - usr_input = int(input()) - if usr_input == 0: - self.create_new_session() - else: - response = self.core.get_session(usr_input) - self.session_id = usr_input - # self.core.events(self.session_id, self.log_event) - logging.info("Entering session_id %s.... Result: %s", usr_input, response) + b = tk.Button( + dialog, + text="Session " + str(session.id), + command=lambda sid=session.id: self._enter_session(sid, dialog), + ) + b.pack(side=tk.TOP) + b = tk.Button( + dialog, text="create new", command=lambda: self._create_session(dialog) + ) + b.pack(side=tk.TOP) + dialog.update() + x = ( + self.master.winfo_x() + + (self.master.winfo_width() - dialog.winfo_width()) / 2 + ) + y = ( + self.master.winfo_y() + + (self.master.winfo_height() / 2 - dialog.winfo_height()) / 2 + ) + dialog.geometry(f"+{int(x)}+{int(y)}") + dialog.wait_window() def delete_session(self): response = self.core.delete_session(self.session_id) @@ -61,9 +99,9 @@ class CoreGrpc: def set_up(self): """ - Query sessions, if there exist any, promt whether to join one + Query sessions, if there exist any, prompt whether to join one - :return: nothing + :return: existing sessions """ self.core.connect() @@ -76,12 +114,12 @@ class CoreGrpc: if len(sessions) == 0: self.create_new_session() else: - # self.create_new_session() + self.query_existing_sessions(sessions) def get_session_state(self): response = self.core.get_session(self.session_id) - logging.info("get sessio: %s", response) + logging.info("get session: %s", response) return response.session.state def set_session_state(self, state): @@ -125,9 +163,6 @@ class CoreGrpc: logging.info("set session state: %s", response) - def get_session_id(self): - return self.session_id - def add_node(self, node_type, model, x, y, name, node_id): logging.info("coregrpc.py ADD NODE %s", name) position = core_pb2.Position(x=x, y=y) @@ -136,9 +171,9 @@ class CoreGrpc: logging.info("created node: %s", response) return response.node_id - def edit_node(self, session_id, node_id, x, y): + def edit_node(self, node_id, x, y): position = core_pb2.Position(x=x, y=y) - response = self.core.edit_node(session_id, node_id, position) + response = self.core.edit_node(self.session_id, node_id, position) logging.info("updated node id %s: %s", node_id, response) def delete_nodes(self): @@ -146,9 +181,6 @@ class CoreGrpc: response = self.core.delete_node(self.session_id, node.id) logging.info("delete node %s", response) - # def create_interface_helper(self): - # self.interface_helper = self.core.InterfaceHelper(ip4_prefix="10.83.0.0/16") - def delete_links(self): for link in self.core.get_session(self.session_id).session.links: response = self.core.delete_link( @@ -160,7 +192,7 @@ class CoreGrpc: ) logging.info("delete link %s", response) - def add_link(self, id1, id2, type1, type2): + def add_link(self, id1, id2, type1, type2, edge): """ Grpc client request add link @@ -171,19 +203,64 @@ class CoreGrpc: :param core_pb2.NodeType type2: node 2 core node type :return: nothing """ - if not self.interface_helper: - logging.debug("INTERFACE HELPER NOT CREATED YET, CREATING ONE...") - self.interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/16") - - interface1 = None - interface2 = None + if1 = None + if2 = None if type1 == core_pb2.NodeType.DEFAULT: - interface1 = self.interface_helper.create_interface(id1, 0) + interface = edge.interface_1 + if1 = core_pb2.Interface( + id=interface.id, + name=interface.name, + mac=interface.mac, + ip4=interface.ipv4, + ip4mask=interface.ip4prefix, + ) + # if1 = core_pb2.Interface(id=id1, name=edge.interface_1.name, ip4=edge.interface_1.ipv4, ip4mask=edge.interface_1.ip4prefix) + logging.debug("create interface 1 %s", if1) + # interface1 = self.interface_helper.create_interface(id1, 0) + if type2 == core_pb2.NodeType.DEFAULT: - interface2 = self.interface_helper.create_interface(id2, 0) - response = self.core.add_link(self.session_id, id1, id2, interface1, interface2) + interface = edge.interface_2 + if2 = core_pb2.Interface( + id=interface.id, + name=interface.name, + mac=interface.mac, + ip4=interface.ipv4, + ip4mask=interface.ip4prefix, + ) + # if2 = core_pb2.Interface(id=id2, name=edge.interface_2.name, ip4=edge.interface_2.ipv4, ip4mask=edge.interface_2.ip4prefix) + logging.debug("create interface 2: %s", if2) + # interface2 = self.interface_helper.create_interface(id2, 0) + + # response = self.core.add_link(self.session_id, id1, id2, interface1, interface2) + response = self.core.add_link(self.session_id, id1, id2, if1, if2) logging.info("created link: %s", response) + def launch_terminal(self, node_id): + response = self.core.get_node_terminal(self.session_id, node_id) + logging.info("get terminal %s", response.terminal) + os.system("xterm -e %s &" % response.terminal) + + def save_xml(self, file_path): + """ + Save core session as to an xml file + + :param str file_path: file path that user pick + :return: nothing + """ + response = self.core.save_xml(self.session_id, file_path) + logging.info("coregrpc.py save xml %s", response) + + def open_xml(self, file_path): + """ + Open core xml + + :param str file_path: file to open + :return: session id + """ + response = self.core.open_xml(file_path) + return response.session_id + # logging.info("coregrpc.py open_xml()", type(response)) + def close(self): """ Clean ups when done using grpc diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index f4ba51f5..f08d18ed 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -1,6 +1,7 @@ import tkinter as tk import coretk.menuaction as action +from coretk.menuaction import MenuAction class CoreMenubar(object): @@ -19,6 +20,33 @@ class CoreMenubar(object): self.menubar = menubar self.master = master self.application = application + self.menuaction = action.MenuAction(application, master) + self.menu_action = MenuAction(self.application, self.master) + + # def on_quit(self): + # """ + # Prompt use to stop running session before application is closed + # + # :return: nothing + # """ + # state = self.application.core_grpc.get_session_state() + # + # if state == core_pb2.SessionState.SHUTDOWN or state == core_pb2.SessionState.DEFINITION: + # self.application.core_grpc.delete_session() + # self.application.core_grpc.core.close() + # # self.application.quit() + # else: + # msgbox = tk.messagebox.askyesnocancel("stop", "Stop the running session?") + # + # if msgbox or msgbox == False: + # if msgbox: + # self.application.core_grpc.set_session_state("datacollect") + # self.application.core_grpc.delete_links() + # self.application.core_grpc.delete_nodes() + # self.application.core_grpc.delete_session() + # + # self.application.core_grpc.core.close() + # # self.application.quit() def create_file_menu(self): """ @@ -27,18 +55,24 @@ class CoreMenubar(object): :return: nothing """ file_menu = tk.Menu(self.menubar) + # menu_action = MenuAction(self.application, self.master) file_menu.add_command( label="New", command=action.file_new, accelerator="Ctrl+N", underline=0 ) file_menu.add_command( - label="Open...", command=action.file_open, accelerator="Ctrl+O", underline=0 + label="Open...", + command=self.menu_action.file_open_xml, + accelerator="Ctrl+O", + underline=0, ) file_menu.add_command(label="Reload", command=action.file_reload, underline=0) file_menu.add_command( label="Save", command=action.file_save, accelerator="Ctrl+S", underline=0 ) - file_menu.add_command(label="Save As XML...", command=action.file_save_as_xml) - file_menu.add_command(label="Save As imn...", command=action.file_save_as_imn) + # file_menu.add_command(label="Save As XML...", command=action.file_save_as_xml) + file_menu.add_command( + label="Save As XML...", command=self.menu_action.file_save_as_xml + ) file_menu.add_separator() @@ -68,13 +102,8 @@ class CoreMenubar(object): file_menu.add_separator() file_menu.add_command( - label="/home/ncs/.core/configs/sample1.imn", - command=action.file_example_link, + label="Quit", command=self.menuaction.on_quit, underline=0 ) - - file_menu.add_separator() - - file_menu.add_command(label="Quit", command=self.master.quit, underline=0) self.menubar.add_cascade(label="File", menu=file_menu, underline=0) def create_edit_menu(self): @@ -560,9 +589,7 @@ class CoreMenubar(object): :return: nothing """ session_menu = tk.Menu(self.menubar) - session_menu.add_command( - label="Start", command=action.session_start, underline=0 - ) + session_menu.add_command( label="Change sessions...", command=action.session_change_sessions, @@ -604,10 +631,11 @@ class CoreMenubar(object): """ help_menu = tk.Menu(self.menubar) help_menu.add_command( - label="Core Github (www)", command=action.help_core_github + label="Core Github (www)", command=self.menu_action.help_core_github ) help_menu.add_command( - label="Core Documentation (www)", command=action.help_core_documentation + label="Core Documentation (www)", + command=self.menu_action.help_core_documentation, ) help_menu.add_command(label="About", command=action.help_about) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index c59c4222..6911ebaf 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -3,7 +3,7 @@ import tkinter as tk from core.api.grpc import core_pb2 from coretk.graph import GraphMode -from coretk.images import Images +from coretk.images import ImageEnum, Images from coretk.tooltip import CreateToolTip @@ -153,7 +153,6 @@ class CoreToolbar(object): logging.debug("Click START STOP SESSION button") self.destroy_children_widgets(self.edit_frame) self.canvas.mode = GraphMode.SELECT - self.create_runtime_toolbar() # set configuration state if self.canvas.core_grpc.get_session_state() == core_pb2.SessionState.SHUTDOWN: @@ -166,10 +165,14 @@ class CoreToolbar(object): ) for edge in self.canvas.grpc_manager.edges.values(): - self.canvas.core_grpc.add_link(edge.id1, edge.id2, edge.type1, edge.type2) + self.canvas.core_grpc.add_link( + edge.id1, edge.id2, edge.type1, edge.type2, edge + ) self.canvas.core_grpc.set_session_state("instantiation") + self.create_runtime_toolbar() + def click_link_tool(self): logging.debug("Click LINK button") self.canvas.mode = GraphMode.EDGE @@ -177,55 +180,55 @@ class CoreToolbar(object): def pick_router(self, main_button): logging.debug("Pick router option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get("router")) + main_button.configure(image=Images.get(ImageEnum.ROUTER.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("router") + self.canvas.draw_node_image = Images.get(ImageEnum.ROUTER.value) self.canvas.draw_node_name = "router" def pick_host(self, main_button): logging.debug("Pick host option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get("host")) + main_button.configure(image=Images.get(ImageEnum.HOST.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("host") + self.canvas.draw_node_image = Images.get(ImageEnum.HOST.value) self.canvas.draw_node_name = "host" def pick_pc(self, main_button): logging.debug("Pick PC option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get("pc")) + main_button.configure(image=Images.get(ImageEnum.PC.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("pc") + self.canvas.draw_node_image = Images.get(ImageEnum.PC.value) self.canvas.draw_node_name = "PC" def pick_mdr(self, main_button): logging.debug("Pick MDR option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get("mdr")) + main_button.configure(image=Images.get(ImageEnum.MDR.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("mdr") + self.canvas.draw_node_image = Images.get(ImageEnum.MD.value) self.canvas.draw_node_name = "mdr" def pick_prouter(self, main_button): logging.debug("Pick prouter option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get("prouter")) + main_button.configure(image=Images.get(ImageEnum.PROUTER.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("prouter") + self.canvas.draw_node_image = Images.get(ImageEnum.PROUTER.value) self.canvas.draw_node_name = "prouter" def pick_ovs(self, main_button): logging.debug("Pick OVS option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get("ovs")) + main_button.configure(image=Images.get(ImageEnum.OVS.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("ovs") + self.canvas.draw_node_image = Images.get(ImageEnum.OVS.value) self.canvas.draw_node_name = "OVS" # TODO what graph node is this def pick_editnode(self, main_button): self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get("editnode")) + main_button.configure(image=Images.get(ImageEnum.EDITNODE.value)) logging.debug("Pick editnode option") def draw_network_layer_options(self, network_layer_button): @@ -239,13 +242,13 @@ class CoreToolbar(object): self.destroy_previous_frame() option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ - Images.get("router"), - Images.get("host"), - Images.get("pc"), - Images.get("mdr"), - Images.get("prouter"), - Images.get("ovs"), - Images.get("editnode"), + Images.get(ImageEnum.ROUTER.value), + Images.get(ImageEnum.HOST.value), + Images.get(ImageEnum.PC.value), + Images.get(ImageEnum.MDR.value), + Images.get(ImageEnum.PROUTER.value), + Images.get(ImageEnum.OVS.value), + Images.get(ImageEnum.EDITNODE.value), ] func_list = [ self.pick_router, @@ -289,7 +292,7 @@ class CoreToolbar(object): :return: nothing """ - router_image = Images.get("router") + router_image = Images.get(ImageEnum.ROUTER.value) network_layer_button = tk.Radiobutton( self.edit_frame, indicatoron=False, @@ -306,41 +309,41 @@ class CoreToolbar(object): def pick_hub(self, main_button): logging.debug("Pick link-layer node HUB") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get("hub")) + main_button.configure(image=Images.get(ImageEnum.HUB.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("hub") + self.canvas.draw_node_image = Images.get(ImageEnum.HUB.value) self.canvas.draw_node_name = "hub" def pick_switch(self, main_button): logging.debug("Pick link-layer node SWITCH") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get("switch")) + main_button.configure(image=Images.get(ImageEnum.SWITCH.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("switch") + self.canvas.draw_node_image = Images.get(ImageEnum.SWITCH.value) self.canvas.draw_node_name = "switch" def pick_wlan(self, main_button): logging.debug("Pick link-layer node WLAN") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get("wlan")) + main_button.configure(image=Images.get(ImageEnum.WLAN.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("wlan") + self.canvas.draw_node_image = Images.get(ImageEnum.WLAN.value) self.canvas.draw_node_name = "wlan" def pick_rj45(self, main_button): logging.debug("Pick link-layer node RJ45") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get("rj45")) + main_button.configure(image=Images.get(ImageEnum.RJ45.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("rj45") + self.canvas.draw_node_image = Images.get(ImageEnum.RJ45.value) self.canvas.draw_node_name = "rj45" def pick_tunnel(self, main_button): logging.debug("Pick link-layer node TUNNEL") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get("tunnel")) + main_button.configure(image=Images.get(ImageEnum.TUNNEL.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("tunnel") + self.canvas.draw_node_image = Images.get(ImageEnum.TUNNEL.value) self.canvas.draw_node_name = "tunnel" def draw_link_layer_options(self, link_layer_button): @@ -354,11 +357,11 @@ class CoreToolbar(object): self.destroy_previous_frame() option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ - Images.get("hub"), - Images.get("switch"), - Images.get("wlan"), - Images.get("rj45"), - Images.get("tunnel"), + Images.get(ImageEnum.HUB.value), + Images.get(ImageEnum.SWITCH.value), + Images.get(ImageEnum.WLAN.value), + Images.get(ImageEnum.RJ45.value), + Images.get(ImageEnum.TUNNEL.value), ] func_list = [ self.pick_hub, @@ -398,7 +401,7 @@ class CoreToolbar(object): :return: nothing """ - hub_image = Images.get("hub") + hub_image = Images.get(ImageEnum.HUB.value) link_layer_button = tk.Radiobutton( self.edit_frame, indicatoron=False, @@ -414,22 +417,22 @@ class CoreToolbar(object): def pick_marker(self, main_button): self.marker_option_menu.destroy() - main_button.configure(image=Images.get("marker")) + main_button.configure(image=Images.get(ImageEnum.MARKER.value)) logging.debug("Pick MARKER") def pick_oval(self, main_button): self.marker_option_menu.destroy() - main_button.configure(image=Images.get("oval")) + main_button.configure(image=Images.get(ImageEnum.OVAL.value)) logging.debug("Pick OVAL") def pick_rectangle(self, main_button): self.marker_option_menu.destroy() - main_button.configure(image=Images.get("rectangle")) + main_button.configure(image=Images.get(ImageEnum.RECTANGLE.value)) logging.debug("Pick RECTANGLE") def pick_text(self, main_button): self.marker_option_menu.destroy() - main_button.configure(image=Images.get("text")) + main_button.configure(image=Images.get(ImageEnum.TEXT.value)) logging.debug("Pick TEXT") def draw_marker_options(self, main_button): @@ -443,10 +446,10 @@ class CoreToolbar(object): self.destroy_previous_frame() option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ - Images.get("marker"), - Images.get("oval"), - Images.get("rectangle"), - Images.get("text"), + Images.get(ImageEnum.MARKER.value), + Images.get(ImageEnum.OVAL.value), + Images.get(ImageEnum.RECTANGLE.value), + Images.get(ImageEnum.TEXT.value), ] func_list = [ self.pick_marker, @@ -475,7 +478,7 @@ class CoreToolbar(object): :return: nothing """ - marker_image = Images.get("marker") + marker_image = Images.get(ImageEnum.MARKER.value) marker_main_button = tk.Radiobutton( self.edit_frame, indicatoron=False, @@ -492,13 +495,13 @@ class CoreToolbar(object): def create_toolbar(self): self.create_regular_button( self.edit_frame, - Images.get("start"), + Images.get(ImageEnum.START.value), self.click_start_session_tool, "start the session", ) self.create_radio_button( self.edit_frame, - Images.get("select"), + Images.get(ImageEnum.SELECT.value), self.click_selection_tool, self.radio_value, 1, @@ -506,7 +509,7 @@ class CoreToolbar(object): ) self.create_radio_button( self.edit_frame, - Images.get("link"), + Images.get(ImageEnum.LINK.value), self.click_link_tool, self.radio_value, 2, @@ -520,7 +523,7 @@ class CoreToolbar(object): def create_observe_button(self): menu_button = tk.Menubutton( self.edit_frame, - image=Images.get("observe"), + image=Images.get(ImageEnum.OBSERVE.value), width=self.width, height=self.height, direction=tk.RIGHT, @@ -550,10 +553,10 @@ class CoreToolbar(object): def click_stop_button(self): logging.debug("Click on STOP button ") self.destroy_children_widgets(self.edit_frame) - self.create_toolbar() self.canvas.core_grpc.set_session_state("datacollect") self.canvas.core_grpc.delete_links() self.canvas.core_grpc.delete_nodes() + self.create_toolbar() def click_run_button(self): logging.debug("Click on RUN button") @@ -570,13 +573,13 @@ class CoreToolbar(object): def create_runtime_toolbar(self): self.create_regular_button( self.edit_frame, - Images.get("stop"), + Images.get(ImageEnum.STOP.value), self.click_stop_button, "stop the session", ) self.create_radio_button( self.edit_frame, - Images.get("select"), + Images.get(ImageEnum.SELECT.value), self.click_selection_tool, self.exec_radio_value, 1, @@ -585,7 +588,7 @@ class CoreToolbar(object): self.create_observe_button() self.create_radio_button( self.edit_frame, - Images.get("plot"), + Images.get(ImageEnum.PLOT.value), self.click_plot_button, self.exec_radio_value, 2, @@ -593,7 +596,7 @@ class CoreToolbar(object): ) self.create_radio_button( self.edit_frame, - Images.get("marker"), + Images.get(ImageEnum.MARKER.value), self.click_marker_button, self.exec_radio_value, 3, @@ -601,13 +604,16 @@ class CoreToolbar(object): ) self.create_radio_button( self.edit_frame, - Images.get("twonode"), + Images.get(ImageEnum.TWONODE.value), self.click_two_node_button, self.exec_radio_value, 4, "run command from one node to another", ) self.create_regular_button( - self.edit_frame, Images.get("run"), self.click_run_button, "run" + self.edit_frame, + Images.get(ImageEnum.RUN.value), + self.click_run_button, + "run", ) self.exec_radio_value.set(1) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 3c984557..7e69b224 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -1,10 +1,12 @@ import enum import logging +import math import tkinter as tk from core.api.grpc import core_pb2 from coretk.grpcmanagement import GrpcManager from coretk.images import Images +from coretk.interface import Interface class GraphMode(enum.Enum): @@ -34,7 +36,7 @@ class CanvasGraph(tk.Canvas): self.draw_grid() self.core_grpc = grpc self.grpc_manager = GrpcManager() - self.draw_existing_component() + # self.draw_existing_component() def setup_menus(self): self.node_context = tk.Menu(self.master) @@ -52,9 +54,6 @@ class CanvasGraph(tk.Canvas): self.bind("", self.click_release) self.bind("", self.click_motion) self.bind("", self.context) - # self.bind("e", self.set_mode) - # self.bind("s", self.set_mode) - # self.bind("n", self.set_mode) def draw_grid(self, width=1000, height=750): """ @@ -91,8 +90,7 @@ class CanvasGraph(tk.Canvas): session_id = self.core_grpc.session_id session = self.core_grpc.core.get_session(session_id).session - # nodes = response.session.nodes - # if len(nodes) > 0: + # redraw existing nodes for node in session.nodes: # peer to peer node is not drawn on the GUI if node.type != core_pb2.NodeType.PEER_TO_PEER: @@ -104,18 +102,62 @@ class CanvasGraph(tk.Canvas): core_id_to_canvas_id[node.id] = n.id self.grpc_manager.add_preexisting_node(n, session_id, node, name) self.grpc_manager.update_reusable_id() + # draw existing links - # links = response.session.links for link in session.links: n1 = self.nodes[core_id_to_canvas_id[link.node_one_id]] n2 = self.nodes[core_id_to_canvas_id[link.node_two_id]] e = CanvasEdge(n1.x_coord, n1.y_coord, n2.x_coord, n2.y_coord, n1.id, self) + n1.edges.add(e) + n2.edges.add(e) self.edges[e.token] = e self.grpc_manager.add_edge(session_id, e.token, n1.id, n2.id) + + # TODO add back the link info to grpc manager also redraw + grpc_if1 = link.interface_one + grpc_if2 = link.interface_two + ip4_src = None + ip4_dst = None + ip6_src = None + ip6_dst = None + if grpc_if1 is not None: + ip4_src = grpc_if1.ip4 + ip6_src = grpc_if1.ip6 + if grpc_if2 is not None: + ip4_dst = grpc_if2.ip4 + ip6_dst = grpc_if2.ip6 + e.link_info = LinkInfo( + canvas=self, + edge_id=e.id, + ip4_src=ip4_src, + ip6_src=ip6_src, + ip4_dst=ip4_dst, + ip6_dst=ip6_dst, + throughput=None, + ) + + # TODO will include throughput and ipv6 in the future + if1 = Interface(grpc_if1.name, grpc_if1.ip4) + if2 = Interface(grpc_if2.name, grpc_if2.ip4) + self.grpc_manager.edges[e.token].interface_1 = if1 + self.grpc_manager.edges[e.token].interface_2 = if2 + self.grpc_manager.nodes[ + core_id_to_canvas_id[link.node_one_id] + ].interfaces.append(if1) + self.grpc_manager.nodes[ + core_id_to_canvas_id[link.node_two_id] + ].interfaces.append(if2) + # lift the nodes so they on top of the links for i in core_id_to_canvas_id.values(): self.lift(i) + def delete_components(self): + tags = ["node", "edge", "linkinfo", "nodename"] + for i in tags: + for id in self.find_withtag(i): + self.delete(id) + def canvas_xy(self, event): """ Convert window coordinate to canvas coordinate @@ -171,6 +213,7 @@ class CanvasGraph(tk.Canvas): self.mode = GraphMode.NODE def handle_edge_release(self, event): + print("Calling edge release") edge = self.drawing_edge self.drawing_edge = None @@ -204,7 +247,20 @@ class CanvasGraph(tk.Canvas): node_dst.edges.add(edge) self.grpc_manager.add_edge( - self.core_grpc.get_session_id(), edge.token, node_src.id, node_dst.id + self.core_grpc.session_id, edge.token, node_src.id, node_dst.id + ) + + # draw link info on the edge + if1 = self.grpc_manager.edges[edge.token].interface_1 + if2 = self.grpc_manager.edges[edge.token].interface_2 + edge.link_info = LinkInfo( + self, + edge.id, + ip4_src=if1.ip4_and_prefix, + ip6_src=None, + ip4_dst=if2.ip4_and_prefix, + ip6_dst=None, + throughput=None, ) logging.debug(f"edges: {self.find_withtag('edge')}") @@ -216,6 +272,7 @@ class CanvasGraph(tk.Canvas): :param event: mouse event :return: nothing """ + print("click on the canvas") logging.debug(f"click press: {event}") selected = self.get_selected(event) is_node = selected in self.find_withtag("node") @@ -243,14 +300,15 @@ class CanvasGraph(tk.Canvas): self.node_context.post(event.x_root, event.y_root) def add_node(self, x, y, image, node_name): - node = CanvasNode( - x=x, y=y, image=image, canvas=self, core_id=self.grpc_manager.peek_id() - ) - self.nodes[node.id] = node - self.grpc_manager.add_node( - self.core_grpc.get_session_id(), node.id, x, y, node_name - ) - return node + if self.selected == 1: + node = CanvasNode( + x=x, y=y, image=image, canvas=self, core_id=self.grpc_manager.peek_id() + ) + self.nodes[node.id] = node + self.grpc_manager.add_node( + self.core_grpc.session_id, node.id, x, y, node_name + ) + return node class CanvasEdge: @@ -258,7 +316,7 @@ class CanvasEdge: Canvas edge class """ - width = 3 + width = 1.3 def __init__(self, x1, y1, x2, y2, src, canvas): """ @@ -273,9 +331,13 @@ class CanvasEdge: self.src = src self.dst = None self.canvas = canvas - self.id = self.canvas.create_line(x1, y1, x2, y2, tags="edge", width=self.width) + self.id = self.canvas.create_line( + x1, y1, x2, y2, tags="edge", width=self.width, fill="#ff0000" + ) self.token = None + # link info object + self.link_info = None # TODO resolve this # self.canvas.tag_lower(self.id) @@ -301,29 +363,40 @@ class CanvasNode: self.core_id = core_id self.x_coord = x self.y_coord = y - self.name = f"Node {self.core_id}" - self.text_id = self.canvas.create_text(x, y + 20, text=self.name) + self.name = f"N{self.core_id}" + self.text_id = self.canvas.create_text( + x, y + 20, text=self.name, tags="nodename" + ) self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) self.canvas.tag_bind(self.id, "", self.motion) self.canvas.tag_bind(self.id, "", self.context) + self.canvas.tag_bind(self.id, "", self.double_click) + self.edges = set() self.moving = None - # - # def get_coords(self): - # return self.x_coord, self.y_coord + def double_click(self, event): + node_id = self.canvas.grpc_manager.nodes[self.id].node_id + state = self.canvas.core_grpc.get_session_state() + if state == core_pb2.SessionState.RUNTIME: + self.canvas.core_grpc.launch_terminal(node_id) def update_coords(self): self.x_coord, self.y_coord = self.canvas.coords(self.id) def click_press(self, event): + print("click on the node") logging.debug(f"click press {self.name}: {event}") self.moving = self.canvas.canvas_xy(event) + # return "break" def click_release(self, event): logging.debug(f"click release {self.name}: {event}") self.update_coords() + self.canvas.grpc_manager.update_node_location( + self.id, self.x_coord, self.y_coord + ) self.moving = None def motion(self, event): @@ -344,6 +417,64 @@ class CanvasNode: self.canvas.coords(edge.id, new_x, new_y, x2, y2) else: self.canvas.coords(edge.id, x1, y1, new_x, new_y) + edge.link_info.recalculate_info() def context(self, event): logging.debug(f"context click {self.name}: {event}") + + +class LinkInfo: + def __init__(self, canvas, edge_id, ip4_src, ip6_src, ip4_dst, ip6_dst, throughput): + self.canvas = canvas + self.edge_id = edge_id + self.radius = 37 + + self.ip4_address_1 = ip4_src + self.ip6_address_1 = ip6_src + self.ip4_address_2 = ip4_dst + self.ip6_address_2 = ip6_dst + self.throughput = throughput + self.id1 = self.create_edge_src_info() + self.id2 = self.create_edge_dst_info() + + def slope_src_dst(self): + x1, y1, x2, y2 = self.canvas.coords(self.edge_id) + return (y2 - y1) / (x2 - x1) + + def create_edge_src_info(self): + x1, y1, x2, _ = self.canvas.coords(self.edge_id) + m = self.slope_src_dst() + distance = math.cos(math.atan(m)) * self.radius + if x1 > x2: + distance = -distance + # id1 = self.canvas.create_text(x1, y1, text=self.ip4_address_1) + print(self.ip4_address_1) + id1 = self.canvas.create_text( + x1 + distance, y1 + distance * m, text=self.ip4_address_1, tags="linkinfo" + ) + return id1 + + def create_edge_dst_info(self): + x1, _, x2, y2 = self.canvas.coords(self.edge_id) + m = self.slope_src_dst() + distance = math.cos(math.atan(m)) * self.radius + if x1 > x2: + distance = -distance + # id2 = self.canvas.create_text(x2, y2, text=self.ip4_address_2) + id2 = self.canvas.create_text( + x2 - distance, y2 - distance * m, text=self.ip4_address_2, tags="linkinfo" + ) + return id2 + + def recalculate_info(self): + x1, y1, x2, y2 = self.canvas.coords(self.edge_id) + m = self.slope_src_dst() + distance = math.cos(math.atan(m)) * self.radius + if x1 > x2: + distance = -distance + new_x1 = x1 + distance + new_y1 = y1 + distance * m + new_x2 = x2 - distance + new_y2 = y2 - distance * m + self.canvas.coords(self.id1, new_x1, new_y1) + self.canvas.coords(self.id2, new_x2, new_y2) diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index afbf8175..ea1ee947 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -5,6 +5,7 @@ that can be useful for grpc, acts like a session class import logging from core.api.grpc import core_pb2 +from coretk.interface import Interface, InterfaceManager link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] network_layer_nodes = ["router", "host", "PC", "mdr", "prouter", "OVS"] @@ -29,6 +30,7 @@ class Node: self.y = y self.model = model self.name = name + self.interfaces = [] class Edge: @@ -46,6 +48,8 @@ class Edge: self.id2 = node_id_2 self.type1 = node_type_1 self.type2 = node_type_2 + self.interface_1 = None + self.interface_2 = None class GrpcManager: @@ -57,6 +61,7 @@ class GrpcManager: self.reusable = [] self.preexisting = [] + self.interfaces_manager = InterfaceManager() def peek_id(self): """ @@ -146,6 +151,18 @@ class GrpcManager: ) self.nodes[canvas_node.id] = n + def update_node_location(self, canvas_id, new_x, new_y): + """ + update node + + :param int canvas_id: canvas id of that node + :param int new_x: new x coord + :param int new_y: new y coord + :return: nothing + """ + self.nodes[canvas_id].x = new_x + self.nodes[canvas_id].y = new_y + def update_reusable_id(self): """ Update available id for reuse @@ -173,7 +190,60 @@ class GrpcManager: except KeyError: logging.error("grpcmanagement.py INVALID NODE CANVAS ID") + def create_interface(self, edge, src_canvas_id, dst_canvas_id): + """ + Create the interface for the two end of an edge, add a copy to node's interfaces + + :param coretk.grpcmanagement.Edge edge: edge to add interfaces to + :param int src_canvas_id: canvas id for the source node + :param int dst_canvas_id: canvas id for the destination node + :return: nothing + """ + src_interface = None + dst_interface = None + + src_node = self.nodes[src_canvas_id] + if src_node.model in network_layer_nodes: + ifid = len(src_node.interfaces) + name = "eth" + str(ifid) + src_interface = Interface( + name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) + ) + self.nodes[src_canvas_id].interfaces.append(src_interface) + logging.debug( + "Create source interface 1... IP: %s, name: %s", + src_interface.ipv4, + src_interface.name, + ) + + dst_node = self.nodes[dst_canvas_id] + if dst_node.model in network_layer_nodes: + ifid = len(dst_node.interfaces) + name = "veth" + str(ifid) + dst_interface = Interface( + name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) + ) + self.nodes[dst_canvas_id].interfaces.append(dst_interface) + logging.debug( + "Create destination interface... IP: %s, name: %s", + dst_interface.ipv4, + dst_interface.name, + ) + + edge.interface_1 = src_interface + edge.interface_2 = dst_interface + def add_edge(self, session_id, token, canvas_id_1, canvas_id_2): + """ + Add an edge to grpc manager + + :param int session_id: core session id + :param tuple(int, int) token: edge's identification in the canvas + :param int canvas_id_1: canvas id of source node + :param int canvas_id_2: canvas_id of destination node + + :return: nothing + """ if canvas_id_1 in self.nodes and canvas_id_2 in self.nodes: edge = Edge( session_id, @@ -183,6 +253,7 @@ class GrpcManager: self.nodes[canvas_id_2].type, ) self.edges[token] = edge + self.create_interface(edge, canvas_id_1, canvas_id_2) logging.debug("Adding edge to grpc manager...") else: logging.error("grpcmanagement.py INVALID CANVAS NODE ID") diff --git a/coretk/coretk/OVS.gif b/coretk/coretk/icons/OVS.gif similarity index 100% rename from coretk/coretk/OVS.gif rename to coretk/coretk/icons/OVS.gif diff --git a/coretk/coretk/core-icon.png b/coretk/coretk/icons/core-icon.png similarity index 100% rename from coretk/coretk/core-icon.png rename to coretk/coretk/icons/core-icon.png diff --git a/coretk/coretk/document-properties.gif b/coretk/coretk/icons/document-properties.gif similarity index 100% rename from coretk/coretk/document-properties.gif rename to coretk/coretk/icons/document-properties.gif diff --git a/coretk/coretk/host.gif b/coretk/coretk/icons/host.gif similarity index 100% rename from coretk/coretk/host.gif rename to coretk/coretk/icons/host.gif diff --git a/coretk/coretk/hub.gif b/coretk/coretk/icons/hub.gif similarity index 100% rename from coretk/coretk/hub.gif rename to coretk/coretk/icons/hub.gif diff --git a/coretk/coretk/lanswitch.gif b/coretk/coretk/icons/lanswitch.gif similarity index 100% rename from coretk/coretk/lanswitch.gif rename to coretk/coretk/icons/lanswitch.gif diff --git a/coretk/coretk/link.gif b/coretk/coretk/icons/link.gif similarity index 100% rename from coretk/coretk/link.gif rename to coretk/coretk/icons/link.gif diff --git a/coretk/coretk/marker.gif b/coretk/coretk/icons/marker.gif similarity index 100% rename from coretk/coretk/marker.gif rename to coretk/coretk/icons/marker.gif diff --git a/coretk/coretk/mdr.gif b/coretk/coretk/icons/mdr.gif similarity index 100% rename from coretk/coretk/mdr.gif rename to coretk/coretk/icons/mdr.gif diff --git a/coretk/coretk/observe.gif b/coretk/coretk/icons/observe.gif similarity index 100% rename from coretk/coretk/observe.gif rename to coretk/coretk/icons/observe.gif diff --git a/coretk/coretk/oval.gif b/coretk/coretk/icons/oval.gif similarity index 100% rename from coretk/coretk/oval.gif rename to coretk/coretk/icons/oval.gif diff --git a/coretk/coretk/pc.gif b/coretk/coretk/icons/pc.gif similarity index 100% rename from coretk/coretk/pc.gif rename to coretk/coretk/icons/pc.gif diff --git a/coretk/coretk/plot.gif b/coretk/coretk/icons/plot.gif similarity index 100% rename from coretk/coretk/plot.gif rename to coretk/coretk/icons/plot.gif diff --git a/coretk/coretk/rectangle.gif b/coretk/coretk/icons/rectangle.gif similarity index 100% rename from coretk/coretk/rectangle.gif rename to coretk/coretk/icons/rectangle.gif diff --git a/coretk/coretk/rj45.gif b/coretk/coretk/icons/rj45.gif similarity index 100% rename from coretk/coretk/rj45.gif rename to coretk/coretk/icons/rj45.gif diff --git a/coretk/coretk/router.gif b/coretk/coretk/icons/router.gif similarity index 100% rename from coretk/coretk/router.gif rename to coretk/coretk/icons/router.gif diff --git a/coretk/coretk/router_green.gif b/coretk/coretk/icons/router_green.gif similarity index 100% rename from coretk/coretk/router_green.gif rename to coretk/coretk/icons/router_green.gif diff --git a/coretk/coretk/run.gif b/coretk/coretk/icons/run.gif similarity index 100% rename from coretk/coretk/run.gif rename to coretk/coretk/icons/run.gif diff --git a/coretk/coretk/select.gif b/coretk/coretk/icons/select.gif similarity index 100% rename from coretk/coretk/select.gif rename to coretk/coretk/icons/select.gif diff --git a/coretk/coretk/start.gif b/coretk/coretk/icons/start.gif similarity index 100% rename from coretk/coretk/start.gif rename to coretk/coretk/icons/start.gif diff --git a/coretk/coretk/stop.gif b/coretk/coretk/icons/stop.gif similarity index 100% rename from coretk/coretk/stop.gif rename to coretk/coretk/icons/stop.gif diff --git a/coretk/coretk/text.gif b/coretk/coretk/icons/text.gif similarity index 100% rename from coretk/coretk/text.gif rename to coretk/coretk/icons/text.gif diff --git a/coretk/coretk/tunnel.gif b/coretk/coretk/icons/tunnel.gif similarity index 100% rename from coretk/coretk/tunnel.gif rename to coretk/coretk/icons/tunnel.gif diff --git a/coretk/coretk/twonode.gif b/coretk/coretk/icons/twonode.gif similarity index 100% rename from coretk/coretk/twonode.gif rename to coretk/coretk/icons/twonode.gif diff --git a/coretk/coretk/wlan.gif b/coretk/coretk/icons/wlan.gif similarity index 100% rename from coretk/coretk/wlan.gif rename to coretk/coretk/icons/wlan.gif diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index fc04c7cc..c5df15bb 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -1,11 +1,13 @@ import logging import os +from enum import Enum from PIL import Image, ImageTk from core.api.grpc import core_pb2 PATH = os.path.abspath(os.path.dirname(__file__)) +ICONS_DIR = os.path.join(PATH, "icons") class Images: @@ -13,7 +15,7 @@ class Images: @classmethod def load(cls, name, file_path): - file_path = os.path.join(PATH, file_path) + # file_path = os.path.join(PATH, file_path) image = Image.open(file_path) tk_image = ImageTk.PhotoImage(image) cls.images[name] = tk_image @@ -33,55 +35,62 @@ class Images: :return: the matching image and its name """ if node_type == core_pb2.NodeType.SWITCH: - return Images.get("switch"), "switch" + return Images.get(ImageEnum.SWITCH.value), "switch" if node_type == core_pb2.NodeType.HUB: - return Images.get("hub"), "hub" + return Images.get(ImageEnum.HUB.value), "hub" if node_type == core_pb2.NodeType.WIRELESS_LAN: - return Images.get("wlan"), "wlan" + return Images.get(ImageEnum.WLAN.value), "wlan" if node_type == core_pb2.NodeType.RJ45: - return Images.get("rj45"), "rj45" + return Images.get(ImageEnum.RJ45.value), "rj45" if node_type == core_pb2.NodeType.TUNNEL: - return Images.get("tunnel"), "tunnel" + return Images.get(ImageEnum.TUNNEL.value), "tunnel" if node_type == core_pb2.NodeType.DEFAULT: if node_model == "router": - return Images.get("router"), "router" + return Images.get(ImageEnum.ROUTER.value), "router" if node_model == "host": - return Images.get(("host")), "host" + return Images.get((ImageEnum.HOST.value)), "host" if node_model == "PC": - return Images.get("pc"), "PC" + return Images.get(ImageEnum.PC.value), "PC" if node_model == "mdr": - return Images.get("mdr"), "mdr" + return Images.get(ImageEnum.MDR.value), "mdr" if node_model == "prouter": - return Images.get("prouter"), "prouter" + return Images.get(ImageEnum.PROUTER.value), "prouter" if node_model == "OVS": - return Images.get("ovs"), "ovs" + return Images.get(ImageEnum.OVS.value), "ovs" else: logging.debug("INVALID INPUT OR NOT CONSIDERED YET") +class ImageEnum(Enum): + SWITCH = "lanswitch" + CORE = "core-icon" + START = "start" + MARKER = "marker" + ROUTER = "router" + SELECT = "select" + LINK = "link" + HUB = "hub" + WLAN = "wlan" + RJ45 = "rj45" + TUNNEL = "tunnel" + OVAL = "oval" + RECTANGLE = "rectangle" + TEXT = "text" + HOST = "host" + PC = "pc" + MDR = "mdr" + PROUTER = "router_green" + OVS = "OVS" + EDITNODE = "document-properties" + PLOT = "plot" + TWONODE = "twonode" + STOP = "stop" + OBSERVE = "observe" + RUN = "run" + + def load_core_images(images): - images.load("core", "core-icon.png") - images.load("start", "start.gif") - images.load("switch", "lanswitch.gif") - images.load("marker", "marker.gif") - images.load("router", "router.gif") - images.load("select", "select.gif") - images.load("link", "link.gif") - images.load("hub", "hub.gif") - images.load("wlan", "wlan.gif") - images.load("rj45", "rj45.gif") - images.load("tunnel", "tunnel.gif") - images.load("oval", "oval.gif") - images.load("rectangle", "rectangle.gif") - images.load("text", "text.gif") - images.load("host", "host.gif") - images.load("pc", "pc.gif") - images.load("mdr", "mdr.gif") - images.load("prouter", "router_green.gif") - images.load("ovs", "OVS.gif") - images.load("editnode", "document-properties.gif") - images.load("run", "run.gif") - images.load("plot", "plot.gif") - images.load("twonode", "twonode.gif") - images.load("stop", "stop.gif") - images.load("observe", "observe.gif") + for file_name in os.listdir(ICONS_DIR): + file_path = os.path.join(ICONS_DIR, file_name) + name = file_name.split(".")[0] + images.load(name, file_path) diff --git a/coretk/coretk/interface.py b/coretk/coretk/interface.py new file mode 100644 index 00000000..8538ab1f --- /dev/null +++ b/coretk/coretk/interface.py @@ -0,0 +1,43 @@ +import ipaddress +import random + + +class Interface: + def __init__(self, name, ipv4, ifid=None): + """ + Create an interface instance + + :param str name: interface name + :param str ip4: IPv4 + :param str mac: MAC address + :param int ifid: interface id + """ + self.name = name + self.ipv4 = ipv4 + self.ip4prefix = 24 + self.ip4_and_prefix = ipv4 + "/" + str(self.ip4prefix) + self.mac = self.random_mac_address() + self.id = ifid + + def random_mac_address(self): + return "02:00:00:%02x:%02x:%02x" % ( + random.randint(0, 255), + random.randint(0, 255), + random.randint(0, 225), + ) + + +class InterfaceManager: + def __init__(self): + self.addresses = list(ipaddress.ip_network("10.0.0.0/24").hosts()) + self.index = 0 + + def get_address(self): + """ + Retrieve a new ipv4 address + + :return: + """ + i = self.index + self.index = self.index + 1 + return self.addresses[i] diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 2e1427da..6618e532 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -3,7 +3,14 @@ The actions taken when each menubar option is clicked """ import logging +import tkinter as tk import webbrowser +from tkinter import messagebox + +from core.api.grpc import core_pb2 +from coretk.coregrpc import CoreGrpc + +SAVEDIR = "/home/ncs/Desktop/" def sub_menu_items(): @@ -38,14 +45,6 @@ def file_save_shortcut(event): logging.debug("Shortcut for file save") -def file_save_as_xml(): - logging.debug("Click save as XML") - - -def file_save_as_imn(): - logging.debug("Click save as imn") - - def file_export_python_script(): logging.debug("Click file export python script") @@ -70,10 +69,6 @@ def file_save_screenshot(): logging.debug("Click file save screenshot") -def file_example_link(): - logging.debug("Click file example link") - - def edit_undo(): logging.debug("Click edit undo") @@ -294,10 +289,6 @@ def widgets_configure_throughput(): logging.debug("Click widgets configure throughput") -def session_start(): - logging.debug("Click session start") - - def session_change_sessions(): logging.debug("Click session change sessions") @@ -326,13 +317,107 @@ def session_options(): logging.debug("Click session options") -def help_core_github(): - webbrowser.open_new("https://github.com/coreemu/core") - - -def help_core_documentation(): - webbrowser.open_new("http://coreemu.github.io/core/") +# def help_core_github(): +# webbrowser.open_new("https://github.com/coreemu/core") +# +# +# def help_core_documentation(): +# webbrowser.open_new("http://coreemu.github.io/core/") def help_about(): logging.debug("Click help About") + + +class MenuAction: + """ + Actions performed when choosing menu items + """ + + def __init__(self, application, master): + self.master = master + self.application = application + self.core_grpc = application.core_grpc + + def clean_nodes_links_and_set_configuarations(self): + """ + Prompt use to stop running session before application is closed + + :return: nothing + """ + logging.info( + "menuaction.py: clean_nodes_links_and_set_configuration() Exiting the program" + ) + grpc = self.application.core_grpc + state = grpc.get_session_state() + + if ( + state == core_pb2.SessionState.SHUTDOWN + or state == core_pb2.SessionState.DEFINITION + ): + grpc.delete_session() + grpc.core.close() + # self.application.quit() + else: + msgbox = messagebox.askyesnocancel("stop", "Stop the running session?") + + if msgbox or msgbox is False: + if msgbox: + grpc.set_session_state("datacollect") + grpc.delete_links() + grpc.delete_nodes() + grpc.delete_session() + + grpc.core.close() + # self.application.quit() + + def on_quit(self): + """ + Prompt user whether so save running session, and then close the application + + :return: nothing + """ + self.clean_nodes_links_and_set_configuarations() + # self.application.core_grpc.close() + self.application.quit() + + def file_save_as_xml(self): + logging.info("menuaction.py file_save_as_xml()") + grpc = self.application.core_grpc + file_path = tk.filedialog.asksaveasfilename( + initialdir=SAVEDIR, + title="Save As", + filetypes=(("EmulationScript XML files", "*.xml"), ("All files", "*")), + defaultextension=".xml", + ) + with open("prev_saved_xml.txt", "a") as file: + file.write(file_path + "\n") + grpc.save_xml(file_path) + + def file_open_xml(self): + logging.info("menuaction.py file_open_xml()") + # grpc = self.application.core_grpc + file_path = tk.filedialog.askopenfilename( + initialdir=SAVEDIR, + title="Open", + filetypes=(("EmulationScript XML File", "*.xml"), ("All Files", "*")), + ) + # clean up before openning a new session + # t0 = time.clock() + self.clean_nodes_links_and_set_configuarations() + grpc = CoreGrpc(self.application.master) + grpc.core.connect() + session_id = grpc.open_xml(file_path) + grpc.session_id = session_id + self.application.core_grpc = grpc + self.application.canvas.core_grpc = grpc + self.application.canvas.delete_components() + self.application.canvas.draw_existing_component() + # t1 = time.clock() + # print(t1 - t0) + + def help_core_github(self): + webbrowser.open_new("https://github.com/coreemu/core") + + def help_core_documentation(self): + webbrowser.open_new("http://coreemu.github.io/core/") diff --git a/coretk/coretk/oldimage/OVS.gif b/coretk/coretk/oldimage/OVS.gif new file mode 100755 index 0000000000000000000000000000000000000000..38fcbb2ea684f2cb3724f55bb668d66dd952ee4d GIT binary patch literal 744 zcmVJ9wZvc%wRbpgMV>J9(izdZ9gfp+0+}KYOA*e5OBqqdk75L4Bk` zeWOBtq(FhHMS!M3gR4h^r$~aQNrI?Dg|0+}u0w{dOogdTg{n`7s!xckMvAgji>+0Q zu1SuzRgA7lkGD#Xw@Q$=SB#%r9qZk@Ytp1W|LyjZ2mSf$H#qQ6_I&0DF?cci~~ zrNDcq!F#8|eW=5Ks>Fe;#fPuQh_T6uvdNFN%aggzn!VDVz|)|@)uF=GrpDN($JnUI z*{jOhtjpW3%-prm-?-G_xzypg)Z)9<;=R}7%*@Qp%*@Qp%*@QpA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=dv*jE#jucwYQHYIwy=OOkz4Hj~p>W&4i3Z zU|?EVaBDeOZhEkLdR2@d7~9=~8y-G!bar@qcWq-WUF1-iLJ&x!2p8np`UC026(iCh zJzDS}A-9DYGIZ!5v5|<6A4!l-8|AaEc70|*i*d`R)429F;{oItU%<%@ul z7Q3Zf+45z~nKf_b+}ZP|Np6FNvUCYkCQX|-b@KEHbQ>Y5MXMskx^$~ki(gevHEK0! zR;ET=O%?)6_2^ZlTAP-%V2Cc;v3TMBba2S8*t>7d8bD;&uG_Lr69G_J06~HV5GGUz aLDL40AWED>vBKp`qSL5Tt6ohg5CA)88DCBS literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/core-icon.png b/coretk/coretk/oldimage/core-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0b0ff5aa3a51f71807691032f516d7a15d7da896 GIT binary patch literal 2931 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 literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/document-properties.gif b/coretk/coretk/oldimage/document-properties.gif new file mode 100644 index 0000000000000000000000000000000000000000..732d8436455ba607ff30774ef83faa434789f775 GIT binary patch literal 635 zcmZ?wbhEHb6krfwI9A3`UtizZ-QL^V)6>`4+Ydxt6Z*URCUj4n)H`Wv|CDJHr_Go& zZN}v3v!=|LJ$2^nX|v}}pFMZR-1#%-Eu6hz(VT@#<}O+?Z}GAP%a$!&x_sgCm5Z0J zTCxI&Rxe$-dg-b)D^{&twiXE1tzNrs?b`LL*REf)X2YuWo7QaFym8yMZ98^v+r4}D z-hI3FAKZQ9*uj%$j-Nbx?EKkN7cQN-eEIUFs~4`{xO(Hp^_#bD+`N6`*6o|O?p(Wh z_u4HGx_egDy&`;YEE0HVit9zMSJ`0=BM zPwqW>djIj$2algUeDdt!vuDqqJb(1!`IDzF9=~}0?D@;5FJHcR`SRtfmoHwweE#Ou zi?^>|zJ2}b^_!P(-oASC_Vv5>Z$Eu}|LN1G&!0Yi`}Xzw_wPTxfBW&{`%fVH2}D1B z{`&dr*Uvvd@b}MOAo%z99|-;X_y6C25W)u(f3h%gG1N2YFaQB4P8is4G}PBC$jQn` zOH0W%^)$&PmQ9?P6RzIdBv~967UJ*YYhIu;UA!+K3@N%$caBBCo?vDZnK?>>} z4H6mGRzwyX@-nz_KC4-2cu1l_BwJ^i<gzK44%5h8Cn(&TO1gfn3+!O Kwon&fum%8seSYfz literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/host.gif b/coretk/coretk/oldimage/host.gif new file mode 100644 index 0000000000000000000000000000000000000000..5bd60ae3d34d9bcd8be206ba2a8a701b19aea319 GIT binary patch literal 1189 zcmb``30Kkw008hm262dT`d1VcH7ko8t5<8Y=8+H2yze8AmB-d>TJ62nYg4kad?v4& zM~5QL6hp;kPN#X`g=i`SsGx=l2;OM*vP`yphyA|8&&!8?{OcqFKn51n00031BLEHn zNC*Id0!Ap-3M58&meYM}^Nii1^A`;HUW+aDPOb$JlayBa^A~QAaVrt~&^ysVU zu~#pg$<9c~y!cbrrA)@p=L&u~%ef84YDQ6|53A zyOhN)Z>=og)RebX6*gBFKC7*0t$xCN#;)aZI%*#_*0XpGPkXscKDVf^?p}MtlfL?r z{)STi^M``Q$GuIDyP7#eFUp6TS;FS0!nVrczj^H)ZQ`~nQ9F0AF*Tvx5@_UB?C>0{ufgN&9cE}$zZ!w_;Nz{*UVt6WVly6 z(m6TOEfNXjqx|X7mt*2l#aPdzNccwFDU}Q)4o~IY1S8%i#nxVJG-+|;J&gZw=hEE2=`yIo!;s0j01%;ph13(4-_1_5q zu>%;8WN#j;`#OSvdLYVS@EK^2sT%CutgEd(_JPmM)a$tvWO-8!ZZMjb+;`VMC~55O zZ8Nu)t;HMEVqV7i7$TIA?NOFFUVpL&d_}6G;!tO9Hwt5&FH99hSJIbFigv>uofpWs zil%hc_f_o8f&6TSug$RC!irVvj|0^3kD=;Q-S+#9SWcSL6V#M4uup%lA`x$XVv5UD z|6pDlk~f#;>(=$uM(SdIU&W(eXoPydho|^6LAH- z5a8TqvnWKEOTGL2@5uXX*GJ%NS|CpVgB{s(IKY-cv>&v9ThKhI(04%|NGmAs4*3eV zF&DNkP4DBLKw}yiL%OoOERP)rdMHmXC1Xvn?aG|Pp=5wzTGz6H@;FVQb$gC}yVs literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/hub.gif b/coretk/coretk/oldimage/hub.gif new file mode 100644 index 0000000000000000000000000000000000000000..17f7c4d3ef726f744da685423057366093df33f8 GIT binary patch literal 719 zcmZ?wbhEHbRA3NeIF`X6<6R-+T`3<>s}$0x8qum2(W)NPsSyiAUD`>#I?27dDHHY5 zChDb6G|ZS{kU7OLYpPMsG?V;UrunnXi|3jb&$BFDXi>JnvTT7>*?jBr1(p@_Eh`pS zRm`^nk&CQB{tt%H;S1z=vTxeUh(6(loea&Lqx}}bFOPuPKIM)NwQisM> zt_{l^n^$`_t#EE#=hC{)xow?q+bXxNO@WAtOJ`nH{!xc%(pooDClzr6I=^<~Fy zEI)o@`=tlFu0Gm*?eU&#kM~}Ge1=Lu@h1x-7ehUR4g(N?;)H?yUqgLUb4zPmdq<0u zTu*Obf4`D_=L9(`C!;~HitzeCPr?FNJxqa zbCy%KUAIBbJ2m~}sncho)np9qjxL(X>fF`|N{-XJqoblAaq)7Kbe~IXuu89G zY>4^!<#n%FX}%(x=oD7Zx(x^D7Cd=>$48HrD@OVHGfFIMB$#qu{aN1LM(d zN%JxW&7zjh$r?;@OmoaOK04YXZC%G>x#`KtDFRD*voZx2yYdE7cm|vGV*3$4sHUKQA;Xt`D2tb#>OIgNj>peGaF!W-$oH tY>1eD&xfTUiIIg<%3(%;VvDJ9wZvc%wRbpgMV>J9(izdZ9gfp+0+}KYOA*e5OBqqdk75L4Bk` zeWOBtq(FhHMS!M3gR4h^r$~aQNrI?Dg|0+}u0w{dOogdTg{n`7s!xckMvAgji>+0Q zu1SuzRgA7lkGD#Xw@Q$=SB#%r9qZk@Ytp1W|LyjZ2mSf$H#qQ6_I&0DF?cci~~ zrNDcq!F#8|eW=5Ks>Fe;#fPuQh_T6uvdNFN%aggzn!VDVz|)|@)uF=GrpDN($JnUI z*{jOhtjpW3%-prm-?-G_xzypg)Z)9<;=R}7%*@Qp%*@Qp%*@QpA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=dv*jE#jucwYQHYIwy=OOkz4Hj~p>W&4i3Z zU|?EVaBDeOZhEkLdR2@d7~9=~8y-G!bar@qcWq-WUF1-iLJ&x!2p8np`UC026(iCh zJzDS}A-9DYGIZ!5v5|<6A4!l-8|AaEc70|*i*d`R)429F;{oItU%<%@ul z7Q3Zf+45z~nKf_b+}ZP|Np6FNvUCYkCQX|-b@KEHbQ>Y5MXMskx^$~ki(gevHEK0! zR;ET=O%?)6_2^ZlTAP-%V2Cc;v3TMBba2S8*t>7d8bD;&uG_Lr69G_J06~HV5GGUz aLDL40AWED>vBKp`qSL5Tt6ohg5CA)88DCBS literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/link.gif b/coretk/coretk/oldimage/link.gif new file mode 100644 index 0000000000000000000000000000000000000000..55532ecf0d14eecb81f57e71ae8f90cd9c8f2fea GIT binary patch literal 86 zcmZ?wbhEHblwgox_`m=Kia%Kx85kHDbU=KN3M?hZsq{JzSM|!8CHm iM7a#L1g zw35rPkmRJB!>gvuyrA2_kkrJV)Xlf=-J;{txy;PW+SI|;*4E(H*5%&Z_wUx>;o<-P z|NsC0A^8LW002J#ECc`q02lxm000J*z@KnPED`}ofN^OAs3wpIl1UYa7>b8-NL-kM<*DutFxg;BP1>~th8?)DLBExWd|U~ Vk^lwFilNX&NxIh8*xA-W06UFctKt9v literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/mdr.gif b/coretk/coretk/oldimage/mdr.gif new file mode 100644 index 0000000000000000000000000000000000000000..d6762f6500828ead06fb73c7a9061165ccdef85b GIT binary patch literal 1276 zcmb``jXTo`0KoCzh-N62mQF9`y{bhS8&Di5W%q3E|!j5ZXj{V1dP z7^69zTAohjVrksC>#Y$b%3K=zJhSp5qbmMJTLzOyzDB>y-XS*imde`bawS84 zmw2%6!-V0cL*B2(Jh`PG1(ilOjJz+z~L3DWyE~ zO;xt!+NFMoy%loArqJ-l_Jm(**X)msz`k|c5+7%ztE@5^S_rHsgwwkfx+OT=Z$;za z1?W%IUsZdN*kB!mhEtzXfV>X7ht>Edn=uv}N8`!@xG`X6rD(8m2lfq+q_=}^$LNJe zrch2W6c0TaQtZgz-JcRb=$y)|Wj z-BiLi_^0&q5I9?(v7Hx8apXh0#<03WyT?yj39`SYQ1$*P0GqVK6ELG(0k9Ks?dS+d z3pf%;4@qAVb9%@ro@T;!2e%9u$e;%Wd0%mEb+qw`b4TNcjXCa=mAQUIuzk0S85EZ* zBUtH@1DtHVzSAt$x4=OWCFxq)a-ePq&RPZ!3`gHsVKo}*{{&+f1UUU>gn=xwL|_AW r4z4Sh`#3ytbr^3Q6*$HAfFoFup6oRk3ar@WthBAq6&LC31nl__5JiQ; literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/observe.gif b/coretk/coretk/oldimage/observe.gif new file mode 100644 index 0000000000000000000000000000000000000000..6b66e7305f35484095a2182ed828c6507c72a746 GIT binary patch literal 1149 zcmd_pYfn=L0LJlylbbZAPQ->d0;0q*%+PGvh{hCQEG*WwP@oJp1|%#lOP0+=yD9^E zJMC#%P#A&~sl5%f$fdVaENf{gXltbuDYVeS>4mnZW77@Ay;;_Njy?H(f#=2lWZqG%4!O^wyx&OriRAmhAY&|RN9pvX;*32s2z+p0&S8&Q(U~J z`1HDh)T$s}R}x#5q*i6+PpZl`6}e4S)vl^;SJiZ=Yv^hUU0qAp)G{=mF*Nl|O&t@i zXTqPe;4j#41E_80z%3B`6)#3hlP=O(9c(TGgqR=%a(E!f5Ddi(3PnRA>5yz#qJh;~oqAfY1LaE{BhQ%km%&jd zZ)}wU1Re`@`9oJa7`GpO_2 znf6ZT%y*~P#QJrKeqCd6rkk^g3>#8|2exj&Pd3Cen=<33%!K@ELWXCN zky+%P*>~UUlh66(^ZwC!|AUxE{;{~g*yF%>j3qE(2~5TX71p5gNl<0m(mag`!3!a6 zj6F1E4?SEA|F#s-FGUP<7R!Rey5O)mJx^D>3o)zS8Hdlb>yO>g zpS$Rb*U=ZRf&bZZ{@I%V0OLSG{CB(wAmv}6G4)7z54#w5GAY0Qgx$RVR97jNeyk-K z!%n+IJ#Z+6-;>|?jPuv|H^lhNu21mKffLy5bD4QP?c|#|n0Jp8wco#gbTJ1jr?B#o zpwIZ0DDQaz=@LnV&pIoiXMa6!%xH2@H(fh#3yU%-1l+Ma#DMEE?9C(%k6MeqHBjI^ zNfj8@$s_i!;2S6J=`b1%yJ zjzYQTh5_ro()5X;@-#bfQ6lc4$C74ZnCAn1Vq}@Ai zXX99F^M5aGkwK}khXSw%@jRar$%MOj@_Rb53* zLseZvO+&*#LsMN-TSG%fPg_?@Ti;MmUsumSSJ%+k(AdDh*umJu$k^1_)ZE0}(#*=* z(#pot+Sba}-p06XUk)~1%W z=GOL>_RhA>?vAdW&hFl>u1PZ|Pn|S<=Cnz(7R{J7eaf6Av**s7Id8$7C2Ll!TE1r8 z>UA5|uGz9@%eKuMb{yEe{n+-M+jj2Wv3bvtz5Dml;r#-+uDu;gbhD7>etiD&*~3?# zUc7q#==JB9uU|ZP^Xu`OFR$LbeE9ayleb@AzkBuk{f}4gKfQhb`t^s;?>@YJ`SI7A zk6+$@eE0U#*AJiGfBgL6)0dC$zkdJx_0xxMKfZkb{OSA8&p&)21O&UZ4U+dI^{!gd>D|h#E6iqaFl|zFQsMJyw=OWRyvJa6$Rl!U zR@l1ZMcNNJ3l}mw>9FJ}Z2q#Shs%CVOlO(ttDwnBECCWCiy96tj+>F9s9ci5+v~R4 zt(VKRlY^y$HMgRo`OM^{iN{KBDit4fo9;Yc%kk90LmM147@K$wKlQwB;JwN4(@H0| z)u(1i(xn6iz5D;z&8dl-$AS zrM*MM*<1JUic6;qwoFj+F)Y&%Z06!ucyvOES=E6-%8p~vY3;2Fi@SBUUUBj^*tDgo zN2{iU;jnU_h0_HN#u!6a(QXcBZ~bi#no@LkeR1MeOki2y)~_Pu;>N(uQq(MZ@J~>; z?!kZ+-HH+o4h+K6bT)OUvr9TM2owiQY?jkgTFfED&(h#5pQ+c>=_GGEv0Ye)C&`VO tJw%~hJ~@t1va|g8z`l6e8jtN{jVv^D?$ literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/plot.gif b/coretk/coretk/oldimage/plot.gif new file mode 100644 index 0000000000000000000000000000000000000000..3924adbf821d1cfeb34a0b9efc7e9d79041fd1df GIT binary patch literal 265 zcmV+k0rvh!Nk%w1VITk?0OJ7w0094gp6-C3?t-B1gQ4!&*x1wB`seBX=<5FI>i+8M z{_E`i?Ck#S?f&lW|9_tDA^saEV<{jqFflS9bZBKDLT_bhZf77vZ*6d4Zg~J9`2+z9 z01p5x00000AOIi$00fut$w{ldIO|=y|6nLmg=3y*s-^|GzOdO&Wo)CfRNr_0?7Wx{ z7<>(jwj&azT$&g-m(u|NL}|P#3J?fbWzrac;9XF=-EZ@i;HCisbU;vTDio=PTh2!- z4^;pSCRKfgT}KOh00w)9eM1P2UXPDMla-Z2nVfzkot>MYnWCeXpQfiYsj51xheIN= PvmP6^x45~wyAc38TK{^> literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/rectangle.gif b/coretk/coretk/oldimage/rectangle.gif new file mode 100644 index 0000000000000000000000000000000000000000..ed271f5737d0e8c2e35e9f03735de27509996893 GIT binary patch literal 160 zcmZ?wbhEHbRA5kGSjfcC(9rPV&GC|Nli|1^))!lG!&+qxHk36QOake}^#rBeM zi_xlcpRkb%J(0K*PL82|tP literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/rj45.gif b/coretk/coretk/oldimage/rj45.gif new file mode 100644 index 0000000000000000000000000000000000000000..9ab7ac56dba8946e37182cf05ca160683f53e2e9 GIT binary patch literal 755 zcmZ?wbhEHbRA5kGI2O;~>+9?1=NA+d6cQ2=8X6iN?hzjD84=+X8R;Dr6%`ZX5gi>J z6B82`=NcCmmynQT6n4RsEljD$^YhO@cS6FCQP*6}( zQc_=US6^S>(9qD>*x1z6)Z1&^+iTL%(b3u2Icbu?gb8&MCe%-yIC1ji#;H>)Cr_R{ zZCcfoDO09TFP}cWa{Bb?bLJGyom)6-)~q>m=FFWtcj3b9MT@dltWaIDBy06*okfcl zEnd8M#fp@5>y(!)S+Ze+`id1RR<2yRYSpUMt5>gGyLR2WbsIKp*tBWW=FOY8Y}vAP z>(=etx9`}oW6z#F`}glZaNxkXbJB+n9lCT$_}H;y$B!RBdGh3`Q>RX!K7HoQnKy4Z z-n`*^`AY@?w*HGWo+|t_C-qG3B79HZ>KVjme2?5b& zEz#jV-gD;8n?E<)TfQaAf5D3RfsslI&C!uzn>KIRx+U6NVO@Rn-hKNI9E=XywfoTV zL#jvjoH)DJ{ph-LXCo9Z`uX0xb^Fd;ql*gOuIA>3CMJ58n)ce(Iwr;j78_ zwFCvl;?#p%KCCcW)XlD^vg!rURSp6*7r%Vup0Kc?jagL6VZj0h<|bY?J(HRl`SC3b zyuqttEQOK`I|U_lW>_{XJ;lP^w&RLwAk%&hA&wh48VA%T&VBgp=M(jZ2bpHG$bGPu zdfRWu>8&jPY;I>~*ysT1_lCK_Z;G0L58oIA}pZ@NkT43mOcrUkRiiszUY&owWZYf(DavUI*h z*+R?m`IhAit;!czR?M@im~U0Fz^Y=QHIS@aU=2bGZ7LU9RV}gxp~W^;3vH_wT30W! zt6peVy~w_Lk!{UV+uCJzwM!jq7dzH2v8!9|ShvKkewjnVN{2=uTIJfX%&}>eN7Hi8 zrWKAYYn)ovcr-8fY+m8gvevt0rAzzzz>YO;-J3mnw*+^u4e4I%(YH0ccfI$-ol*T8 zqbF>PnXt)s%5LAOK(r@r;%5Kpd;MqZOPsPLY1-DHSqD<4Z4I1#D0RlR;JHWAXKoLk zcO+!q(X81!vuE$jnX@x&(ea3-r;8WvjaqRodd1n&Mf*w@?=N4nzjE2Zs^tf(mmjKK zakzff;fB>mTGk!w*m$yU>zT57yZQXRtrza^xcp$xwa0s}KRz=GXchvBKUo;L82&TpFaQB4 zPcU%&WBAW07$KqoEo1|3* z!_CHs4~gB{Wjwl{j8FI;7LI4q)tb`Ld9+cY`_9GB?E#YVRbre*mzCJu8YN_JWf(8# z>@kmBq{2Dr(V_#q;`4JRHfIFNX)2%6h}xoYVahuGKn|4$2Or68>$+sav-qg5yleZT z1F{EQ6&e^t<(@SNFFV=0!e!fzj1|m1QimB?R2B#*X}O8mubAUl{8PT+3>%+Lg#jZo zyP&xDo)w8HoMQUPZqw~_6}C39@hP}WC~!Q?E^1uH;kf7sr-*iB!Ruppejj-M;IMAW zj{{9^{fcMi#yL;-o3$(d!6DU*=kH`|K0m)$Jl(wamesE}w|Cd`^Utq&S^qV5`n{@O zJJsWA7O!;-C}nzd{9fJrUiJBotO^$vRQ=p~{C-7Jz!!ez8;L&_GzNq`RCKykx`Bz$ LhNm#efx#L8|Dlf2 literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/router_green.gif b/coretk/coretk/oldimage/router_green.gif new file mode 100644 index 0000000000000000000000000000000000000000..76e3ecd57c59ec99767f5641e701f908f09f0258 GIT binary patch literal 753 zcmV$ZwARvGsB7!0$gd`+|BqoR^Cx|C0iYY0J zDJqL9D~&5Hku5KgFE5iXF_tkhmNGJzGBlYqG@CRwn>IF_HaDF&IG;EPKS85GJf%E6r9DBULPDfMJ*GWE zq(eTZK0c^EMy5tVsX<4lNJytiLaIVQt3XMpN=&IvL#;zYu0u|$P(`stMzKamu}4#_ zRY$T%RIOG=vqwj?M@hCxSg=}Juw6^JOJB2LU$bIPyiQ`YWKF$IW3*;ZzE5SgXi&dU zP{2@9!BT3tZc@ThYq)P~xo>Q_Z*979RK-+pyK-^7bymn$R>@X$y?1rKd3C>eSj$*< zzk6EFT7krdfyIV|#)^%~l8(xhlg*iw&YG3ao0iX=n9!e@(Vw8yr=!)Xq}8jY*R816 zucz3rtJ<=#+_<#fy|v!Gw%@Eq<#>~vjA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=my(jE#=El$DGfh?9&+W>zhgGe=rwY)+0Jg^nay zZG3!fWM&|jQh9!Ub|jCOf|fFFeYa;FPI7*AUNAtGy_SqBI$bsnKeZk z1rZZ4K!FI3>S252fd&*X6#oDSAb`LD2Ng7J300IRJ9!Q{YVZ)>j z9!jK$fuo0!Bu}QqoGFqd0h>4*Jb*C40)`MMUew63L&%V$NtcEkS@R|XsZ_0E)ymZ? zSfx)g$2x_2c52nDS+{omiWDwWvQN#Ty}MSg+rDB4%@u4{ZQizg!^$i)AaK;ic-iiK ji-ij!x`Q2SKKvv~q(ODlW;XnF$?4M`P`G$4C=dWUiqcBu literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/run.gif b/coretk/coretk/oldimage/run.gif new file mode 100644 index 0000000000000000000000000000000000000000..71dcc67eddc7b829b3221ba4823a04e93c317ba9 GIT binary patch literal 324 zcmZ?wbhEHbRA5kGXkh>WMn*;!78X`kRv8%?d3kvQ0|P@tLt|rOQ&ZEBkdW~3@W#f* zzP`R`)22WfAZwX)2B~gy?XWL&6~Gx-+uY>JSuFtAW4$w*aj%1_PAOIL8t&n-yIt7K68$->CRAkUx!(gbo61M8v(>U}Ah^DZGE(+-Pd9n~wOxE_RahlU()Kf}#eGDh z*YY|kSclnhF*_f*ZL2LVvRih;gcu3Vl7L(W;oNF5dkLndh`{&>RXH)u)s9RO_M-ic zeWf|^96S<2Tyxsf7B~v9GqLh5uUZ+O;*@5-)iu^LcxTxBeY@PPj~q2OIdSsT=`&}~ Ko!3%ium%9#D0q7S literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/select.gif b/coretk/coretk/oldimage/select.gif new file mode 100644 index 0000000000000000000000000000000000000000..bb7e128c878317f6287564514a302e5e3c40c10e GIT binary patch literal 925 zcmX|AT}TvB6#nkcsH?a+J25!wWJw!YSVoy{g=r6ESt%99vPu|jWwavpvRe--w$f%s zYflbuf*4Z;~=R4<~Ip225ShbBKwQc6#Xwq9Tr2mq=851@csnR&8f+ycq>V-cDai3y~@!gr8wL0nBC zU{r)Hb5*5f7u61mZ3t|yIUb`VBe1-QqVleY#wih00Goly!xkwyVZ{1lB8I%Sj2 z2m-M!HhYQ+$jnGNeiv{GnB~nh`yh{T`At9|X4{_DxaFbKW*UGN8*$vq~F1bXlQwoK`C%h5?z2 z^9{6>N|K@L0aob!9DW(7DB*YfpQyG!K!#mAI~x7?(4Lbn6<4Dx zBsmXiOOfS?u)5v0FmUO{muJG>%YKj3_qk`!#gV43zkL%=4mf&UnMa1E`pa7HX6 zwqJ$`n~{uVTRP)1JIzL7vZz_If|{*~lS3(L>1YMNREp5@O@UgWlR;}sSO0_k0ekZL z_4(yVp4S^+tEp{9fME3$6#a%3d6h+8OxB~4_u{fkYWYPR?;%tdh>aI$;&(>#2d(q5 zq|ZS55Pb#FeTca%FjoY_pJqd!1@)Oxp9Ml1`7i-t4cyYgh!#e*aJztjI|SS%;9h|a zB09LQQ-@ea61aXdeO(}jc{0Q^LuMvumWN6CNC71urC^MNC<)^vJSd=Gf`Um(ai38{ z8O1oOm|(THknXlgcgI9TOhi;5A6OtpL5zkt4O28s(=ellSv}0@;UNQ$7?@{Zp@4-& z77{Ex<{-(z6Aqg*V)HyUZ^9BLSmGdMfWHe6JVmgA;2$Hb8db|?STjO~hb+(iC2}E= zy;n>}t!$*2i`p!Ao&2Q35-AghO2uI5=DfEu#zz=H@$_Mb?{Rq z{9+lOEYmC(YgRUCGIq4&M5(gkhc7$gRgRhMj=5@EvbrQ*Q!-U!TdHnN9cfQ>p3I%` zKE32ym!#DJX>CAC`=yLudghn1gHmqL|199o2K>39h3w)&E|JY=*YoT7d>;PKe_lXL z%Qpa=2Iup}^G|?k9cpo5kGH%3s7hbT?CS`gs@|e@IvxFC@zo~jk+^@))dtz&Y*T;l z?A{-~bETvTcUU_8qRZQ!rUz#@r|W!B>HK04c84jYlF?$DP{j?^i+0ZQ2+ZIX4SO-=6I%AGsiKi6a3?s^GQOGO8fMnKq);bg%%10X@M4zBI6uI7|O(9f`)8t&J&IEhZ*K% z8p0H3vMt-(>;N^g83y9QrosRx$V4G+SB}zh6w0L>BIQtq|HD3c{rvguotmB!tKG*1 zE-VAE0ptL&09yeP015#11C#)q0w@Kz0PqdK&j3}M2!tvku^OOe6M=9I;5xuH0$ktZ z0aXZc!vk)j#99QYB_Y3{$So3DOF@67l5SyG9hK5R#Tq<48*uD4oq7+)?=tY;nACer z`X3y|eIItSz^mDxWAgMc(LBsFqM1&#;6w|9Xu{EdnM5lKv9ggqFQlJ?40xjhKIq^k zU)07$ZGPwwk7V~J+4&?BlVb6uTR9Y~H`eCk^}vVI%Hwnk{46}Cna4KqIA)QbgHIY3 zl1D`3zs1}xsegxz*BL5&94hP%6%9yzEn==&%crFW;+X`=Xnfd;p0S?9 zTz`wXu$@1jB>69icnv2A%1q*_d#8!8+C0 zTZrvD>>8Vox>4Mdm_Es^ZH+W4(mo!~Z1|$qT$^|IwxO%!+@-oJh{nC8;r*=WWA`-;QAzjtz^Ii7u|z=Wv2fXj#VN>!l;b z?mLBTO~!ZdvdioVmi{Zhp5ZE9)4|Gr^t?m#a)!v5$*HJ3Tk$2o=T;c6)t=K>yv+RC(pN0M-Dd-mC&;N5vlB2PJtvQ!1!r1BjF z+s~Ac1T8?_TUcM_wWIJ%I-4ebdPz@^w*X1jEB<6*W6va?-5+;v-TUT4p_f*)Gl%!N9?d`v_mp{QI;sYb*91-N d=esbwJ0>tWSn~6fcWtY;{pRgIt;xz@4FI19FuMQ% literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/tunnel.gif b/coretk/coretk/oldimage/tunnel.gif new file mode 100644 index 0000000000000000000000000000000000000000..d574147f535122637516f4887d931779c5903ead GIT binary patch literal 799 zcmZ?wbhEHbRA5kGI2Oae!0_MK*VoU_FDNJ|BqSs>G&DTiBRt$QBEl;&(mN_DDkjDw zIyyQgCMGVRu= zo;-Qlw5lmnrc9q+K7D%S^y$;*%qf~Xw{X_1S###hnLBsx!iCw37GTOS zvSsVmt=qS6-?3xIo;`c^@85smz=3n;qz@fBbm@}tv17-MA3uKbEDBDn2z=biC+ZOKAtSKatw%=FXNJe4Me>Sj+A1D9KCrS*RkU3* z;lrne-kjRjO)MM=OCIfUlrZJGkdVO4#LcG^5-~xMv7K2WY}*zN!ma^ zbPjCKZ)C4{X}6U-tSN3!tNM$%udfIkJDRAe;MWte!OBrWPFoND ze%zhu(T2k5n;-J6D%2?Cv=W`H(&hR5YrE{RRjOvEka_>yUw;J#YXB&pnhF2_ literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/twonode.gif b/coretk/coretk/oldimage/twonode.gif new file mode 100644 index 0000000000000000000000000000000000000000..28e75fac3aadc9286cdb65b1269f3f1355f055d4 GIT binary patch literal 220 zcmZ?wbhEHbRA5kGIK;vL1pmSK$$5tVNI>zQv_`U~f{}rNg+fV2s)AE~YGz)#f^&Xu zL1JDdgW^vXMlJ>x1|5(AAfp(Vn>=>i`Dbv-A$vka-~r3C5f^&JB{~NwA;X2aQL-=@frKKWwv3( zs%b?M|9mdLmiXy?;63*j?HbL7MqLIbcP^&3j?Qk~UiW^@i7u0sr`k2mm^rJirgNbX HCxbNrD`Hob literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/wlan.gif b/coretk/coretk/oldimage/wlan.gif new file mode 100644 index 0000000000000000000000000000000000000000..d72fe9c3f8db0c7013424313bc826eca7022c19a GIT binary patch literal 146 zcmZ?wbhEHbRA5kGXkcUjg8%>jEB@otNY*qmFfdba%1_PAOJ`90$->CRz{sEjQUOxS zz!cuozw-23{>32+u5qs4D3yPjAx>eEpJZvqkAmdpb(+5?eJb>ho%FhP{o<=WA`fyl xetyz3Q~L84X{Px{Rg%)5F0}|bV|49q%MP!#xfR=XUU=m{gSY?m^L8c%YXB@FJ0t)A literal 0 HcmV?d00001 diff --git a/coretk/coretk/prev_saved_xml.txt b/coretk/coretk/prev_saved_xml.txt new file mode 100644 index 00000000..3d6d4d91 --- /dev/null +++ b/coretk/coretk/prev_saved_xml.txt @@ -0,0 +1,5 @@ +/home/ncs/Desktop/random.xml/home/ncs/Desktop/untitled.xml/home/ncs/Desktop/test.xml +/home/ncs/Desktop/test1.xml + +/home/ncs/Desktop/untitled.xml +/home/ncs/Desktop/test1.xml diff --git a/output.txt b/output.txt new file mode 100644 index 00000000..ee11dc37 --- /dev/null +++ b/output.txt @@ -0,0 +1,23 @@ +./netns/vcmdmodule.c:static PyObject *VCmd_popen(VCmd *self, PyObject *args, PyObject *kwds) +./netns/vcmdmodule.c: {"popen", (PyCFunction)VCmd_popen, METH_VARARGS | METH_KEYWORDS, +./netns/vcmdmodule.c: "popen(args...) -> (VCmdWait, cmdin, cmdout, cmderr)\n\n" +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: def popen(self, args): +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: Execute a popen command against the node. +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: :return: popen object, stdin, stdout, and stderr +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: logging.debug("popen: %s", cmd) +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) +./daemon/tests/test_core.py: p, stdin, stdout, stderr = client.popen(command) +./daemon/tests/test_core.py: p, stdin, stdout, stderr = client.popen(command) +./daemon/examples/netns/ospfmanetmdrtest.py: """ Exceute call to node.popen(). """ +./daemon/examples/netns/ospfmanetmdrtest.py: self.id, self.stdin, self.out, self.err = self.node.client.popen(self.args) +./daemon/examples/netns/ospfmanetmdrtest.py: self.id, self.stdin, self.out, self.err = self.node.client.popen(args) +./daemon/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) +./daemon/core/nodes/client.py: def popen(self, args): +./daemon/core/nodes/client.py: Execute a popen command against the node. +./daemon/core/nodes/client.py: :return: popen object, stdin, stdout, and stderr +./daemon/core/nodes/client.py: logging.debug("popen: %s", cmd) +./daemon/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) +./daemon/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) +./corefx/src/main/resources/js/leaflet.js:!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i(t.L={})}(this,function(t){"use strict";function i(t){var i,e,n,o;for(e=1,n=arguments.length;e=0}function B(t,i,e,n){return"touchstart"===i?O(t,e,n):"touchmove"===i?W(t,e,n):"touchend"===i&&H(t,e,n),this}function I(t,i,e){var n=t["_leaflet_"+i+e];return"touchstart"===i?t.removeEventListener(te,n,!1):"touchmove"===i?t.removeEventListener(ie,n,!1):"touchend"===i&&(t.removeEventListener(ee,n,!1),t.removeEventListener(ne,n,!1)),this}function O(t,i,n){var o=e(function(t){if("mouse"!==t.pointerType&&t.MSPOINTER_TYPE_MOUSE&&t.pointerType!==t.MSPOINTER_TYPE_MOUSE){if(!(oe.indexOf(t.target.tagName)<0))return;Pt(t)}j(t,i)});t["_leaflet_touchstart"+n]=o,t.addEventListener(te,o,!1),re||(document.documentElement.addEventListener(te,R,!0),document.documentElement.addEventListener(ie,N,!0),document.documentElement.addEventListener(ee,D,!0),document.documentElement.addEventListener(ne,D,!0),re=!0)}function R(t){se[t.pointerId]=t,ae++}function N(t){se[t.pointerId]&&(se[t.pointerId]=t)}function D(t){delete se[t.pointerId],ae--}function j(t,i){t.touches=[];for(var e in se)t.touches.push(se[e]);t.changedTouches=[t],i(t)}function W(t,i,e){var n=function(t){(t.pointerType!==t.MSPOINTER_TYPE_MOUSE&&"mouse"!==t.pointerType||0!==t.buttons)&&j(t,i)};t["_leaflet_touchmove"+e]=n,t.addEventListener(ie,n,!1)}function H(t,i,e){var n=function(t){j(t,i)};t["_leaflet_touchend"+e]=n,t.addEventListener(ee,n,!1),t.addEventListener(ne,n,!1)}function F(t,i,e){function n(t){var i;if(Vi){if(!bi||"mouse"===t.pointerType)return;i=ae}else i=t.touches.length;if(!(i>1)){var e=Date.now(),n=e-(s||e);r=t.touches?t.touches[0]:t,a=n>0&&n<=h,s=e}}function o(t){if(a&&!r.cancelBubble){if(Vi){if(!bi||"mouse"===t.pointerType)return;var e,n,o={};for(n in r)e=r[n],o[n]=e&&e.bind?e.bind(r):e;r=o}r.type="dblclick",i(r),s=null}}var s,r,a=!1,h=250;return t[le+he+e]=n,t[le+ue+e]=o,t[le+"dblclick"+e]=i,t.addEventListener(he,n,!1),t.addEventListener(ue,o,!1),t.addEventListener("dblclick",i,!1),this}function U(t,i){var e=t[le+he+i],n=t[le+ue+i],o=t[le+"dblclick"+i];return t.removeEventListener(he,e,!1),t.removeEventListener(ue,n,!1),bi||t.removeEventListener("dblclick",o,!1),this}function V(t){return"string"==typeof t?document.getElementById(t):t}function q(t,i){var e=t.style[i]||t.currentStyle&&t.currentStyle[i];if((!e||"auto"===e)&&document.defaultView){var n=document.defaultView.getComputedStyle(t,null);e=n?n[i]:null}return"auto"===e?null:e}function G(t,i,e){var n=document.createElement(t);return n.className=i||"",e&&e.appendChild(n),n}function K(t){var i=t.parentNode;i&&i.removeChild(t)}function Y(t){for(;t.firstChild;)t.removeChild(t.firstChild)}function X(t){var i=t.parentNode;i.lastChild!==t&&i.appendChild(t)}function J(t){var i=t.parentNode;i.firstChild!==t&&i.insertBefore(t,i.firstChild)}function $(t,i){if(void 0!==t.classList)return t.classList.contains(i);var e=et(t);return e.length>0&&new RegExp("(^|\\s)"+i+"(\\s|$)").test(e)}function Q(t,i){if(void 0!==t.classList)for(var e=u(i),n=0,o=e.length;n100&&n<500||t.target._simulatedClick&&!t._simulated?Lt(t):(ge=e,i(t))}function Zt(t,i){if(!i||!t.length)return t.slice();var e=i*i;return t=Bt(t,e),t=kt(t,e)}function Et(t,i,e){return Math.sqrt(Dt(t,i,e,!0))}function kt(t,i){var e=t.length,n=new(typeof Uint8Array!=void 0+""?Uint8Array:Array)(e);n[0]=n[e-1]=1,At(t,n,i,0,e-1);var o,s=[];for(o=0;oh&&(s=r,h=a);h>e&&(i[s]=1,At(t,i,e,n,s),At(t,i,e,s,o))}function Bt(t,i){for(var e=[t[0]],n=1,o=0,s=t.length;ni&&(e.push(t[n]),o=n);return oi.max.x&&(e|=2),t.yi.max.y&&(e|=8),e}function Nt(t,i){var e=i.x-t.x,n=i.y-t.y;return e*e+n*n}function Dt(t,i,e,n){var o,s=i.x,r=i.y,a=e.x-s,h=e.y-r,u=a*a+h*h;return u>0&&((o=((t.x-s)*a+(t.y-r)*h)/u)>1?(s=e.x,r=e.y):o>0&&(s+=a*o,r+=h*o)),a=t.x-s,h=t.y-r,n?a*a+h*h:new x(s,r)}function jt(t){return!oi(t[0])||"object"!=typeof t[0][0]&&void 0!==t[0][0]}function Wt(t){return console.warn("Deprecated use of _flat, please use L.LineUtil.isFlat instead."),jt(t)}function Ht(t,i,e){var n,o,s,r,a,h,u,l,c,_=[1,4,2,8];for(o=0,u=t.length;o0?Math.floor(t):Math.ceil(t)};x.prototype={clone:function(){return new x(this.x,this.y)},add:function(t){return this.clone()._add(w(t))},_add:function(t){return this.x+=t.x,this.y+=t.y,this},subtract:function(t){return this.clone()._subtract(w(t))},_subtract:function(t){return this.x-=t.x,this.y-=t.y,this},divideBy:function(t){return this.clone()._divideBy(t)},_divideBy:function(t){return this.x/=t,this.y/=t,this},multiplyBy:function(t){return this.clone()._multiplyBy(t)},_multiplyBy:function(t){return this.x*=t,this.y*=t,this},scaleBy:function(t){return new x(this.x*t.x,this.y*t.y)},unscaleBy:function(t){return new x(this.x/t.x,this.y/t.y)},round:function(){return this.clone()._round()},_round:function(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this},floor:function(){return this.clone()._floor()},_floor:function(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this},ceil:function(){return this.clone()._ceil()},_ceil:function(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this},trunc:function(){return this.clone()._trunc()},_trunc:function(){return this.x=_i(this.x),this.y=_i(this.y),this},distanceTo:function(t){var i=(t=w(t)).x-this.x,e=t.y-this.y;return Math.sqrt(i*i+e*e)},equals:function(t){return(t=w(t)).x===this.x&&t.y===this.y},contains:function(t){return t=w(t),Math.abs(t.x)<=Math.abs(this.x)&&Math.abs(t.y)<=Math.abs(this.y)},toString:function(){return"Point("+a(this.x)+", "+a(this.y)+")"}},P.prototype={extend:function(t){return t=w(t),this.min||this.max?(this.min.x=Math.min(t.x,this.min.x),this.max.x=Math.max(t.x,this.max.x),this.min.y=Math.min(t.y,this.min.y),this.max.y=Math.max(t.y,this.max.y)):(this.min=t.clone(),this.max=t.clone()),this},getCenter:function(t){return new x((this.min.x+this.max.x)/2,(this.min.y+this.max.y)/2,t)},getBottomLeft:function(){return new x(this.min.x,this.max.y)},getTopRight:function(){return new x(this.max.x,this.min.y)},getTopLeft:function(){return this.min},getBottomRight:function(){return this.max},getSize:function(){return this.max.subtract(this.min)},contains:function(t){var i,e;return(t="number"==typeof t[0]||t instanceof x?w(t):b(t))instanceof P?(i=t.min,e=t.max):i=e=t,i.x>=this.min.x&&e.x<=this.max.x&&i.y>=this.min.y&&e.y<=this.max.y},intersects:function(t){t=b(t);var i=this.min,e=this.max,n=t.min,o=t.max,s=o.x>=i.x&&n.x<=e.x,r=o.y>=i.y&&n.y<=e.y;return s&&r},overlaps:function(t){t=b(t);var i=this.min,e=this.max,n=t.min,o=t.max,s=o.x>i.x&&n.xi.y&&n.y=n.lat&&e.lat<=o.lat&&i.lng>=n.lng&&e.lng<=o.lng},intersects:function(t){t=z(t);var i=this._southWest,e=this._northEast,n=t.getSouthWest(),o=t.getNorthEast(),s=o.lat>=i.lat&&n.lat<=e.lat,r=o.lng>=i.lng&&n.lng<=e.lng;return s&&r},overlaps:function(t){t=z(t);var i=this._southWest,e=this._northEast,n=t.getSouthWest(),o=t.getNorthEast(),s=o.lat>i.lat&&n.lati.lng&&n.lng1,Xi=!!document.createElement("canvas").getContext,Ji=!(!document.createElementNS||!E("svg").createSVGRect),$i=!Ji&&function(){try{var t=document.createElement("div");t.innerHTML='';var i=t.firstChild;return i.style.behavior="url(#default#VML)",i&&"object"==typeof i.adj}catch(t){return!1}}(),Qi=(Object.freeze||Object)({ie:Pi,ielt9:Li,edge:bi,webkit:Ti,android:zi,android23:Mi,androidStock:Si,opera:Zi,chrome:Ei,gecko:ki,safari:Ai,phantom:Bi,opera12:Ii,win:Oi,ie3d:Ri,webkit3d:Ni,gecko3d:Di,any3d:ji,mobile:Wi,mobileWebkit:Hi,mobileWebkit3d:Fi,msPointer:Ui,pointer:Vi,touch:qi,mobileOpera:Gi,mobileGecko:Ki,retina:Yi,canvas:Xi,svg:Ji,vml:$i}),te=Ui?"MSPointerDown":"pointerdown",ie=Ui?"MSPointerMove":"pointermove",ee=Ui?"MSPointerUp":"pointerup",ne=Ui?"MSPointerCancel":"pointercancel",oe=["INPUT","SELECT","OPTION"],se={},re=!1,ae=0,he=Ui?"MSPointerDown":Vi?"pointerdown":"touchstart",ue=Ui?"MSPointerUp":Vi?"pointerup":"touchend",le="_leaflet_",ce=st(["transform","webkitTransform","OTransform","MozTransform","msTransform"]),_e=st(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),de="webkitTransition"===_e||"OTransition"===_e?_e+"End":"transitionend";if("onselectstart"in document)fi=function(){mt(window,"selectstart",Pt)},gi=function(){ft(window,"selectstart",Pt)};else{var pe=st(["userSelect","WebkitUserSelect","OUserSelect","MozUserSelect","msUserSelect"]);fi=function(){if(pe){var t=document.documentElement.style;vi=t[pe],t[pe]="none"}},gi=function(){pe&&(document.documentElement.style[pe]=vi,vi=void 0)}}var me,fe,ge,ve=(Object.freeze||Object)({TRANSFORM:ce,TRANSITION:_e,TRANSITION_END:de,get:V,getStyle:q,create:G,remove:K,empty:Y,toFront:X,toBack:J,hasClass:$,addClass:Q,removeClass:tt,setClass:it,getClass:et,setOpacity:nt,testProp:st,setTransform:rt,setPosition:at,getPosition:ht,disableTextSelection:fi,enableTextSelection:gi,disableImageDrag:ut,enableImageDrag:lt,preventOutline:ct,restoreOutline:_t,getSizedParentNode:dt,getScale:pt}),ye="_leaflet_events",xe=Oi&&Ei?2*window.devicePixelRatio:ki?window.devicePixelRatio:1,we={},Pe=(Object.freeze||Object)({on:mt,off:ft,stopPropagation:yt,disableScrollPropagation:xt,disableClickPropagation:wt,preventDefault:Pt,stop:Lt,getMousePosition:bt,getWheelDelta:Tt,fakeStop:zt,skipped:Mt,isExternalTarget:Ct,addListener:mt,removeListener:ft}),Le=ci.extend({run:function(t,i,e,n){this.stop(),this._el=t,this._inProgress=!0,this._duration=e||.25,this._easeOutPower=1/Math.max(n||.5,.2),this._startPos=ht(t),this._offset=i.subtract(this._startPos),this._startTime=+new Date,this.fire("start"),this._animate()},stop:function(){this._inProgress&&(this._step(!0),this._complete())},_animate:function(){this._animId=f(this._animate,this),this._step()},_step:function(t){var i=+new Date-this._startTime,e=1e3*this._duration;ithis.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,i){this._enforcingBounds=!0;var e=this.getCenter(),n=this._limitCenter(e,this._zoom,z(t));return e.equals(n)||this.panTo(n,i),this._enforcingBounds=!1,this},invalidateSize:function(t){if(!this._loaded)return this;t=i({animate:!1,pan:!0},!0===t?{animate:!0}:t);var n=this.getSize();this._sizeChanged=!0,this._lastCenter=null;var o=this.getSize(),s=n.divideBy(2).round(),r=o.divideBy(2).round(),a=s.subtract(r);return a.x||a.y?(t.animate&&t.pan?this.panBy(a):(t.pan&&this._rawPanBy(a),this.fire("move"),t.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(e(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:n,newSize:o})):this},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(t){if(t=this._locateOptions=i({timeout:1e4,watch:!1},t),!("geolocation"in navigator))return this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this;var n=e(this._handleGeolocationResponse,this),o=e(this._handleGeolocationError,this);return t.watch?this._locationWatchId=navigator.geolocation.watchPosition(n,o,t):navigator.geolocation.getCurrentPosition(n,o,t),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var i=t.code,e=t.message||(1===i?"permission denied":2===i?"position unavailable":"timeout");this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:i,message:"Geolocation error: "+e+"."})},_handleGeolocationResponse:function(t){var i=new M(t.coords.latitude,t.coords.longitude),e=i.toBounds(2*t.coords.accuracy),n=this._locateOptions;if(n.setView){var o=this.getBoundsZoom(e);this.setView(i,n.maxZoom?Math.min(o,n.maxZoom):o)}var s={latlng:i,bounds:e,timestamp:t.timestamp};for(var r in t.coords)"number"==typeof t.coords[r]&&(s[r]=t.coords[r]);this.fire("locationfound",s)},addHandler:function(t,i){if(!i)return this;var e=this[t]=new i(this);return this._handlers.push(e),this.options[t]&&e.enable(),this},remove:function(){if(this._initEvents(!0),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch(t){this._container._leaflet_id=void 0,this._containerId=void 0}void 0!==this._locationWatchId&&this.stopLocate(),this._stop(),K(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._resizeRequest&&(g(this._resizeRequest),this._resizeRequest=null),this._clearHandlers(),this._loaded&&this.fire("unload");var t;for(t in this._layers)this._layers[t].remove();for(t in this._panes)K(this._panes[t]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(t,i){var e=G("div","leaflet-pane"+(t?" leaflet-"+t.replace("Pane","")+"-pane":""),i||this._mapPane);return t&&(this._panes[t]=e),e},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter:this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var t=this.getPixelBounds();return new T(this.unproject(t.getBottomLeft()),this.unproject(t.getTopRight()))},getMinZoom:function(){return void 0===this.options.minZoom?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return void 0===this.options.maxZoom?void 0===this._layersMaxZoom?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(t,i,e){t=z(t),e=w(e||[0,0]);var n=this.getZoom()||0,o=this.getMinZoom(),s=this.getMaxZoom(),r=t.getNorthWest(),a=t.getSouthEast(),h=this.getSize().subtract(e),u=b(this.project(a,n),this.project(r,n)).getSize(),l=ji?this.options.zoomSnap:1,c=h.x/u.x,_=h.y/u.y,d=i?Math.max(c,_):Math.min(c,_);return n=this.getScaleZoom(d,n),l&&(n=Math.round(n/(l/100))*(l/100),n=i?Math.ceil(n/l)*l:Math.floor(n/l)*l),Math.max(o,Math.min(s,n))},getSize:function(){return this._size&&!this._sizeChanged||(this._size=new x(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(t,i){var e=this._getTopLeftPoint(t,i);return new P(e,e.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(t){return this.options.crs.getProjectedBounds(void 0===t?this.getZoom():t)},getPane:function(t){return"string"==typeof t?this._panes[t]:t},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(t,i){var e=this.options.crs;return i=void 0===i?this._zoom:i,e.scale(t)/e.scale(i)},getScaleZoom:function(t,i){var e=this.options.crs;i=void 0===i?this._zoom:i;var n=e.zoom(t*e.scale(i));return isNaN(n)?1/0:n},project:function(t,i){return i=void 0===i?this._zoom:i,this.options.crs.latLngToPoint(C(t),i)},unproject:function(t,i){return i=void 0===i?this._zoom:i,this.options.crs.pointToLatLng(w(t),i)},layerPointToLatLng:function(t){var i=w(t).add(this.getPixelOrigin());return this.unproject(i)},latLngToLayerPoint:function(t){return this.project(C(t))._round()._subtract(this.getPixelOrigin())},wrapLatLng:function(t){return this.options.crs.wrapLatLng(C(t))},wrapLatLngBounds:function(t){return this.options.crs.wrapLatLngBounds(z(t))},distance:function(t,i){return this.options.crs.distance(C(t),C(i))},containerPointToLayerPoint:function(t){return w(t).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(t){return w(t).add(this._getMapPanePos())},containerPointToLatLng:function(t){var i=this.containerPointToLayerPoint(w(t));return this.layerPointToLatLng(i)},latLngToContainerPoint:function(t){return this.layerPointToContainerPoint(this.latLngToLayerPoint(C(t)))},mouseEventToContainerPoint:function(t){return bt(t,this._container)},mouseEventToLayerPoint:function(t){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t))},mouseEventToLatLng:function(t){return this.layerPointToLatLng(this.mouseEventToLayerPoint(t))},_initContainer:function(t){var i=this._container=V(t);if(!i)throw new Error("Map container not found.");if(i._leaflet_id)throw new Error("Map container is already initialized.");mt(i,"scroll",this._onScroll,this),this._containerId=n(i)},_initLayout:function(){var t=this._container;this._fadeAnimated=this.options.fadeAnimation&&ji,Q(t,"leaflet-container"+(qi?" leaflet-touch":"")+(Yi?" leaflet-retina":"")+(Li?" leaflet-oldie":"")+(Ai?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":""));var i=q(t,"position");"absolute"!==i&&"relative"!==i&&"fixed"!==i&&(t.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var t=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),at(this._mapPane,new x(0,0)),this.createPane("tilePane"),this.createPane("shadowPane"),this.createPane("overlayPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(Q(t.markerPane,"leaflet-zoom-hide"),Q(t.shadowPane,"leaflet-zoom-hide"))},_resetView:function(t,i){at(this._mapPane,new x(0,0));var e=!this._loaded;this._loaded=!0,i=this._limitZoom(i),this.fire("viewprereset");var n=this._zoom!==i;this._moveStart(n,!1)._move(t,i)._moveEnd(n),this.fire("viewreset"),e&&this.fire("load")},_moveStart:function(t,i){return t&&this.fire("zoomstart"),i||this.fire("movestart"),this},_move:function(t,i,e){void 0===i&&(i=this._zoom);var n=this._zoom!==i;return this._zoom=i,this._lastCenter=t,this._pixelOrigin=this._getNewPixelOrigin(t),(n||e&&e.pinch)&&this.fire("zoom",e),this.fire("move",e)},_moveEnd:function(t){return t&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return g(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(t){at(this._mapPane,this._getMapPanePos().subtract(t))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(t){this._targets={},this._targets[n(this._container)]=this;var i=t?ft:mt;i(this._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress",this._handleDOMEvent,this),this.options.trackResize&&i(window,"resize",this._onResize,this),ji&&this.options.transform3DLimit&&(t?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){g(this._resizeRequest),this._resizeRequest=f(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var t=this._getMapPanePos();Math.max(Math.abs(t.x),Math.abs(t.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,i){for(var e,o=[],s="mouseout"===i||"mouseover"===i,r=t.target||t.srcElement,a=!1;r;){if((e=this._targets[n(r)])&&("click"===i||"preclick"===i)&&!t._simulated&&this._draggableMoved(e)){a=!0;break}if(e&&e.listens(i,!0)){if(s&&!Ct(r,t))break;if(o.push(e),s)break}if(r===this._container)break;r=r.parentNode}return o.length||a||s||!Ct(r,t)||(o=[this]),o},_handleDOMEvent:function(t){if(this._loaded&&!Mt(t)){var i=t.type;"mousedown"!==i&&"keypress"!==i||ct(t.target||t.srcElement),this._fireDOMEvent(t,i)}},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,e,n){if("click"===t.type){var o=i({},t);o.type="preclick",this._fireDOMEvent(o,o.type,n)}if(!t._stopped&&(n=(n||[]).concat(this._findEventTargets(t,e))).length){var s=n[0];"contextmenu"===e&&s.listens(e,!0)&&Pt(t);var r={originalEvent:t};if("keypress"!==t.type){var a=s.getLatLng&&(!s._radius||s._radius<=10);r.containerPoint=a?this.latLngToContainerPoint(s.getLatLng()):this.mouseEventToContainerPoint(t),r.layerPoint=this.containerPointToLayerPoint(r.containerPoint),r.latlng=a?s.getLatLng():this.layerPointToLatLng(r.layerPoint)}for(var h=0;h0?Math.round(t-i)/2:Math.max(0,Math.ceil(t))-Math.max(0,Math.floor(i))},_limitZoom:function(t){var i=this.getMinZoom(),e=this.getMaxZoom(),n=ji?this.options.zoomSnap:1;return n&&(t=Math.round(t/n)*n),Math.max(i,Math.min(e,t))},_onPanTransitionStep:function(){this.fire("move")},_onPanTransitionEnd:function(){tt(this._mapPane,"leaflet-pan-anim"),this.fire("moveend")},_tryAnimatedPan:function(t,i){var e=this._getCenterOffset(t)._trunc();return!(!0!==(i&&i.animate)&&!this.getSize().contains(e))&&(this.panBy(e,i),!0)},_createAnimProxy:function(){var t=this._proxy=G("div","leaflet-proxy leaflet-zoom-animated");this._panes.mapPane.appendChild(t),this.on("zoomanim",function(t){var i=ce,e=this._proxy.style[i];rt(this._proxy,this.project(t.center,t.zoom),this.getZoomScale(t.zoom,1)),e===this._proxy.style[i]&&this._animatingZoom&&this._onZoomTransitionEnd()},this),this.on("load moveend",function(){var t=this.getCenter(),i=this.getZoom();rt(this._proxy,this.project(t,i),this.getZoomScale(i,1))},this),this._on("unload",this._destroyAnimProxy,this)},_destroyAnimProxy:function(){K(this._proxy),delete this._proxy},_catchTransitionEnd:function(t){this._animatingZoom&&t.propertyName.indexOf("transform")>=0&&this._onZoomTransitionEnd()},_nothingToAnimate:function(){return!this._container.getElementsByClassName("leaflet-zoom-animated").length},_tryAnimatedZoom:function(t,i,e){if(this._animatingZoom)return!0;if(e=e||{},!this._zoomAnimated||!1===e.animate||this._nothingToAnimate()||Math.abs(i-this._zoom)>this.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(i),o=this._getCenterOffset(t)._divideBy(1-1/n);return!(!0!==e.animate&&!this.getSize().contains(o))&&(f(function(){this._moveStart(!0,!1)._animateZoom(t,i,!0)},this),!0)},_animateZoom:function(t,i,n,o){this._mapPane&&(n&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=i,Q(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:i,noUpdate:o}),setTimeout(e(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&tt(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom),f(function(){this._moveEnd(!0)},this))}}),Te=v.extend({options:{position:"topright"},initialize:function(t){l(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var i=this._map;return i&&i.removeControl(this),this.options.position=t,i&&i.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var i=this._container=this.onAdd(t),e=this.getPosition(),n=t._controlCorners[e];return Q(i,"leaflet-control"),-1!==e.indexOf("bottom")?n.insertBefore(i,n.firstChild):n.appendChild(i),this},remove:function(){return this._map?(K(this._container),this.onRemove&&this.onRemove(this._map),this._map=null,this):this},_refocusOnMap:function(t){this._map&&t&&t.screenX>0&&t.screenY>0&&this._map.getContainer().focus()}}),ze=function(t){return new Te(t)};be.include({addControl:function(t){return t.addTo(this),this},removeControl:function(t){return t.remove(),this},_initControlPos:function(){function t(t,o){var s=e+t+" "+e+o;i[t+o]=G("div",s,n)}var i=this._controlCorners={},e="leaflet-",n=this._controlContainer=G("div",e+"control-container",this._container);t("top","left"),t("top","right"),t("bottom","left"),t("bottom","right")},_clearControlPos:function(){for(var t in this._controlCorners)K(this._controlCorners[t]);K(this._controlContainer),delete this._controlCorners,delete this._controlContainer}});var Me=Te.extend({options:{collapsed:!0,position:"topright",autoZIndex:!0,hideSingleBase:!1,sortLayers:!1,sortFunction:function(t,i,e,n){return e1,this._baseLayersList.style.display=t?"":"none"),this._separator.style.display=i&&t?"":"none",this},_onLayerChange:function(t){this._handlingClick||this._update();var i=this._getLayer(n(t.target)),e=i.overlay?"add"===t.type?"overlayadd":"overlayremove":"add"===t.type?"baselayerchange":null;e&&this._map.fire(e,i)},_createRadioElement:function(t,i){var e='",n=document.createElement("div");return n.innerHTML=e,n.firstChild},_addItem:function(t){var i,e=document.createElement("label"),o=this._map.hasLayer(t.layer);t.overlay?((i=document.createElement("input")).type="checkbox",i.className="leaflet-control-layers-selector",i.defaultChecked=o):i=this._createRadioElement("leaflet-base-layers",o),this._layerControlInputs.push(i),i.layerId=n(t.layer),mt(i,"click",this._onInputClick,this);var s=document.createElement("span");s.innerHTML=" "+t.name;var r=document.createElement("div");return e.appendChild(r),r.appendChild(i),r.appendChild(s),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(e),this._checkDisabledLayers(),e},_onInputClick:function(){var t,i,e=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=e.length-1;s>=0;s--)t=e[s],i=this._getLayer(t.layerId).layer,t.checked?n.push(i):t.checked||o.push(i);for(s=0;s=0;o--)t=e[o],i=this._getLayer(t.layerId).layer,t.disabled=void 0!==i.options.minZoom&&ni.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expand:function(){return this.expand()},_collapse:function(){return this.collapse()}}),Ce=Te.extend({options:{position:"topleft",zoomInText:"+",zoomInTitle:"Zoom in",zoomOutText:"−",zoomOutTitle:"Zoom out"},onAdd:function(t){var i="leaflet-control-zoom",e=G("div",i+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,i+"-in",e,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,i+"-out",e,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),e},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,i,e,n,o){var s=G("a",e,n);return s.innerHTML=t,s.href="#",s.title=i,s.setAttribute("role","button"),s.setAttribute("aria-label",i),wt(s),mt(s,"click",Lt),mt(s,"click",o,this),mt(s,"click",this._refocusOnMap,this),s},_updateDisabled:function(){var t=this._map,i="leaflet-disabled";tt(this._zoomInButton,i),tt(this._zoomOutButton,i),(this._disabled||t._zoom===t.getMinZoom())&&Q(this._zoomOutButton,i),(this._disabled||t._zoom===t.getMaxZoom())&&Q(this._zoomInButton,i)}});be.mergeOptions({zoomControl:!0}),be.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new Ce,this.addControl(this.zoomControl))});var Se=Te.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var i=G("div","leaflet-control-scale"),e=this.options;return this._addScales(e,"leaflet-control-scale-line",i),t.on(e.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,i,e){t.metric&&(this._mScale=G("div",i,e)),t.imperial&&(this._iScale=G("div",i,e))},_update:function(){var t=this._map,i=t.getSize().y/2,e=t.distance(t.containerPointToLatLng([0,i]),t.containerPointToLatLng([this.options.maxWidth,i]));this._updateScales(e)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var i=this._getRoundNum(t),e=i<1e3?i+" m":i/1e3+" km";this._updateScale(this._mScale,e,i/t)},_updateImperial:function(t){var i,e,n,o=3.2808399*t;o>5280?(i=o/5280,e=this._getRoundNum(i),this._updateScale(this._iScale,e+" mi",e/i)):(n=this._getRoundNum(o),this._updateScale(this._iScale,n+" ft",n/o))},_updateScale:function(t,i,e){t.style.width=Math.round(this.options.maxWidth*e)+"px",t.innerHTML=i},_getRoundNum:function(t){var i=Math.pow(10,(Math.floor(t)+"").length-1),e=t/i;return e=e>=10?10:e>=5?5:e>=3?3:e>=2?2:1,i*e}}),Ze=Te.extend({options:{position:"bottomright",prefix:'Leaflet'},initialize:function(t){l(this,t),this._attributions={}},onAdd:function(t){t.attributionControl=this,this._container=G("div","leaflet-control-attribution"),wt(this._container);for(var i in t._layers)t._layers[i].getAttribution&&this.addAttribution(t._layers[i].getAttribution());return this._update(),this._container},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t?(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update(),this):this},removeAttribution:function(t){return t?(this._attributions[t]&&(this._attributions[t]--,this._update()),this):this},_update:function(){if(this._map){var t=[];for(var i in this._attributions)this._attributions[i]&&t.push(i);var e=[];this.options.prefix&&e.push(this.options.prefix),t.length&&e.push(t.join(", ")),this._container.innerHTML=e.join(" | ")}}});be.mergeOptions({attributionControl:!0}),be.addInitHook(function(){this.options.attributionControl&&(new Ze).addTo(this)});Te.Layers=Me,Te.Zoom=Ce,Te.Scale=Se,Te.Attribution=Ze,ze.layers=function(t,i,e){return new Me(t,i,e)},ze.zoom=function(t){return new Ce(t)},ze.scale=function(t){return new Se(t)},ze.attribution=function(t){return new Ze(t)};var Ee=v.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled?this:(this._enabled=!0,this.addHooks(),this)},disable:function(){return this._enabled?(this._enabled=!1,this.removeHooks(),this):this},enabled:function(){return!!this._enabled}});Ee.addTo=function(t,i){return t.addHandler(i,this),this};var ke,Ae={Events:li},Be=qi?"touchstart mousedown":"mousedown",Ie={mousedown:"mouseup",touchstart:"touchend",pointerdown:"touchend",MSPointerDown:"touchend"},Oe={mousedown:"mousemove",touchstart:"touchmove",pointerdown:"touchmove",MSPointerDown:"touchmove"},Re=ci.extend({options:{clickTolerance:3},initialize:function(t,i,e,n){l(this,n),this._element=t,this._dragStartTarget=i||t,this._preventOutline=e},enable:function(){this._enabled||(mt(this._dragStartTarget,Be,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(Re._dragging===this&&this.finishDrag(),ft(this._dragStartTarget,Be,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){if(!t._simulated&&this._enabled&&(this._moved=!1,!$(this._element,"leaflet-zoom-anim")&&!(Re._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||(Re._dragging=this,this._preventOutline&&ct(this._element),ut(),fi(),this._moving)))){this.fire("down");var i=t.touches?t.touches[0]:t,e=dt(this._element);this._startPoint=new x(i.clientX,i.clientY),this._parentScale=pt(e),mt(document,Oe[t.type],this._onMove,this),mt(document,Ie[t.type],this._onUp,this)}},_onMove:function(t){if(!t._simulated&&this._enabled)if(t.touches&&t.touches.length>1)this._moved=!0;else{var i=t.touches&&1===t.touches.length?t.touches[0]:t,e=new x(i.clientX,i.clientY)._subtract(this._startPoint);(e.x||e.y)&&(Math.abs(e.x)+Math.abs(e.y)1e-7;h++)i=s*Math.sin(a),i=Math.pow((1-i)/(1+i),s/2),a+=u=Math.PI/2-2*Math.atan(r*i)-a;return new M(a*e,t.x*e/n)}},He=(Object.freeze||Object)({LonLat:je,Mercator:We,SphericalMercator:mi}),Fe=i({},pi,{code:"EPSG:3395",projection:We,transformation:function(){var t=.5/(Math.PI*We.R);return Z(t,.5,-t,.5)}()}),Ue=i({},pi,{code:"EPSG:4326",projection:je,transformation:Z(1/180,1,-1/180,.5)}),Ve=i({},di,{projection:je,transformation:Z(1,0,-1,0),scale:function(t){return Math.pow(2,t)},zoom:function(t){return Math.log(t)/Math.LN2},distance:function(t,i){var e=i.lng-t.lng,n=i.lat-t.lat;return Math.sqrt(e*e+n*n)},infinite:!0});di.Earth=pi,di.EPSG3395=Fe,di.EPSG3857=yi,di.EPSG900913=xi,di.EPSG4326=Ue,di.Simple=Ve;var qe=ci.extend({options:{pane:"overlayPane",attribution:null,bubblingMouseEvents:!0},addTo:function(t){return t.addLayer(this),this},remove:function(){return this.removeFrom(this._map||this._mapToAdd)},removeFrom:function(t){return t&&t.removeLayer(this),this},getPane:function(t){return this._map.getPane(t?this.options[t]||t:this.options.pane)},addInteractiveTarget:function(t){return this._map._targets[n(t)]=this,this},removeInteractiveTarget:function(t){return delete this._map._targets[n(t)],this},getAttribution:function(){return this.options.attribution},_layerAdd:function(t){var i=t.target;if(i.hasLayer(this)){if(this._map=i,this._zoomAnimated=i._zoomAnimated,this.getEvents){var e=this.getEvents();i.on(e,this),this.once("remove",function(){i.off(e,this)},this)}this.onAdd(i),this.getAttribution&&i.attributionControl&&i.attributionControl.addAttribution(this.getAttribution()),this.fire("add"),i.fire("layeradd",{layer:this})}}});be.include({addLayer:function(t){if(!t._layerAdd)throw new Error("The provided object is not a Layer.");var i=n(t);return this._layers[i]?this:(this._layers[i]=t,t._mapToAdd=this,t.beforeAdd&&t.beforeAdd(this),this.whenReady(t._layerAdd,t),this)},removeLayer:function(t){var i=n(t);return this._layers[i]?(this._loaded&&t.onRemove(this),t.getAttribution&&this.attributionControl&&this.attributionControl.removeAttribution(t.getAttribution()),delete this._layers[i],this._loaded&&(this.fire("layerremove",{layer:t}),t.fire("remove")),t._map=t._mapToAdd=null,this):this},hasLayer:function(t){return!!t&&n(t)in this._layers},eachLayer:function(t,i){for(var e in this._layers)t.call(i,this._layers[e]);return this},_addLayers:function(t){for(var i=0,e=(t=t?oi(t)?t:[t]:[]).length;ithis._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()i)return r=(n-i)/e,this._map.layerPointToLatLng([s.x-r*(s.x-o.x),s.y-r*(s.y-o.y)])},getBounds:function(){return this._bounds},addLatLng:function(t,i){return i=i||this._defaultShape(),t=C(t),i.push(t),this._bounds.extend(t),this.redraw()},_setLatLngs:function(t){this._bounds=new T,this._latlngs=this._convertLatLngs(t)},_defaultShape:function(){return jt(this._latlngs)?this._latlngs:this._latlngs[0]},_convertLatLngs:function(t){for(var i=[],e=jt(t),n=0,o=t.length;n=2&&i[0]instanceof M&&i[0].equals(i[e-1])&&i.pop(),i},_setLatLngs:function(t){nn.prototype._setLatLngs.call(this,t),jt(this._latlngs)&&(this._latlngs=[this._latlngs])},_defaultShape:function(){return jt(this._latlngs[0])?this._latlngs[0]:this._latlngs[0][0]},_clipPoints:function(){var t=this._renderer._bounds,i=this.options.weight,e=new x(i,i);if(t=new P(t.min.subtract(e),t.max.add(e)),this._parts=[],this._pxBounds&&this._pxBounds.intersects(t))if(this.options.noClip)this._parts=this._rings;else for(var n,o=0,s=this._rings.length;ot.y!=n.y>t.y&&t.x<(n.x-e.x)*(t.y-e.y)/(n.y-e.y)+e.x&&(u=!u);return u||nn.prototype._containsPoint.call(this,t,!0)}}),sn=Ke.extend({initialize:function(t,i){l(this,i),this._layers={},t&&this.addData(t)},addData:function(t){var i,e,n,o=oi(t)?t:t.features;if(o){for(i=0,e=o.length;i0?o:[i.src]}else{oi(this._url)||(this._url=[this._url]),i.autoplay=!!this.options.autoplay,i.loop=!!this.options.loop;for(var a=0;ao?(i.height=o+"px",Q(t,"leaflet-popup-scrolled")):tt(t,"leaflet-popup-scrolled"),this._containerWidth=this._container.offsetWidth},_animateZoom:function(t){var i=this._map._latLngToNewLayerPoint(this._latlng,t.zoom,t.center),e=this._getAnchor();at(this._container,i.add(e))},_adjustPan:function(){if(!(!this.options.autoPan||this._map._panAnim&&this._map._panAnim._inProgress)){var t=this._map,i=parseInt(q(this._container,"marginBottom"),10)||0,e=this._container.offsetHeight+i,n=this._containerWidth,o=new x(this._containerLeft,-e-this._containerBottom);o._add(ht(this._container));var s=t.layerPointToContainerPoint(o),r=w(this.options.autoPanPadding),a=w(this.options.autoPanPaddingTopLeft||r),h=w(this.options.autoPanPaddingBottomRight||r),u=t.getSize(),l=0,c=0;s.x+n+h.x>u.x&&(l=s.x+n-u.x+h.x),s.x-l-a.x<0&&(l=s.x-a.x),s.y+e+h.y>u.y&&(c=s.y+e-u.y+h.y),s.y-c-a.y<0&&(c=s.y-a.y),(l||c)&&t.fire("autopanstart").panBy([l,c])}},_onCloseButtonClick:function(t){this._close(),Lt(t)},_getAnchor:function(){return w(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}});be.mergeOptions({closePopupOnClick:!0}),be.include({openPopup:function(t,i,e){return t instanceof cn||(t=new cn(e).setContent(t)),i&&t.setLatLng(i),this.hasLayer(t)?this:(this._popup&&this._popup.options.autoClose&&this.closePopup(),this._popup=t,this.addLayer(t))},closePopup:function(t){return t&&t!==this._popup||(t=this._popup,this._popup=null),t&&this.removeLayer(t),this}}),qe.include({bindPopup:function(t,i){return t instanceof cn?(l(t,i),this._popup=t,t._source=this):(this._popup&&!i||(this._popup=new cn(i,this)),this._popup.setContent(t)),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t,i){if(t instanceof qe||(i=t,t=this),t instanceof Ke)for(var e in this._layers){t=this._layers[e];break}return i||(i=t.getCenter?t.getCenter():t.getLatLng()),this._popup&&this._map&&(this._popup._source=t,this._popup.update(),this._map.openPopup(this._popup,i)),this},closePopup:function(){return this._popup&&this._popup._close(),this},togglePopup:function(t){return this._popup&&(this._popup._map?this.closePopup():this.openPopup(t)),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var i=t.layer||t.target;this._popup&&this._map&&(Lt(t),i instanceof Qe?this.openPopup(t.layer||t.target,t.latlng):this._map.hasLayer(this._popup)&&this._popup._source===i?this.closePopup():this.openPopup(i,t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}});var _n=ln.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,interactive:!1,opacity:.9},onAdd:function(t){ln.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&this._source.fire("tooltipopen",{tooltip:this},!0)},onRemove:function(t){ln.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&this._source.fire("tooltipclose",{tooltip:this},!0)},getEvents:function(){var t=ln.prototype.getEvents.call(this);return qi&&!this.options.permanent&&(t.preclick=this._close),t},_close:function(){this._map&&this._map.closeTooltip(this)},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=G("div",t)},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var i=this._map,e=this._container,n=i.latLngToContainerPoint(i.getCenter()),o=i.layerPointToContainerPoint(t),s=this.options.direction,r=e.offsetWidth,a=e.offsetHeight,h=w(this.options.offset),u=this._getAnchor();"top"===s?t=t.add(w(-r/2+h.x,-a+h.y+u.y,!0)):"bottom"===s?t=t.subtract(w(r/2-h.x,-h.y,!0)):"center"===s?t=t.subtract(w(r/2+h.x,a/2-u.y+h.y,!0)):"right"===s||"auto"===s&&o.xthis.options.maxZoom||en&&this._retainParent(o,s,r,n))},_retainChildren:function(t,i,e,n){for(var o=2*t;o<2*t+2;o++)for(var s=2*i;s<2*i+2;s++){var r=new x(o,s);r.z=e+1;var a=this._tileCoordsToKey(r),h=this._tiles[a];h&&h.active?h.retain=!0:(h&&h.loaded&&(h.retain=!0),e+1this.options.maxZoom||void 0!==this.options.minZoom&&o1)this._setView(t,e);else{for(var c=o.min.y;c<=o.max.y;c++)for(var _=o.min.x;_<=o.max.x;_++){var d=new x(_,c);if(d.z=this._tileZoom,this._isValidTile(d)){var p=this._tiles[this._tileCoordsToKey(d)];p?p.current=!0:r.push(d)}}if(r.sort(function(t,i){return t.distanceTo(s)-i.distanceTo(s)}),0!==r.length){this._loading||(this._loading=!0,this.fire("loading"));var m=document.createDocumentFragment();for(_=0;_e.max.x)||!i.wrapLat&&(t.ye.max.y))return!1}if(!this.options.bounds)return!0;var n=this._tileCoordsToBounds(t);return z(this.options.bounds).overlaps(n)},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var i=this._map,e=this.getTileSize(),n=t.scaleBy(e),o=n.add(e);return[i.unproject(n,t.z),i.unproject(o,t.z)]},_tileCoordsToBounds:function(t){var i=this._tileCoordsToNwSe(t),e=new T(i[0],i[1]);return this.options.noWrap||(e=this._map.wrapLatLngBounds(e)),e},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var i=t.split(":"),e=new x(+i[0],+i[1]);return e.z=+i[2],e},_removeTile:function(t){var i=this._tiles[t];i&&(K(i.el),delete this._tiles[t],this.fire("tileunload",{tile:i.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){Q(t,"leaflet-tile");var i=this.getTileSize();t.style.width=i.x+"px",t.style.height=i.y+"px",t.onselectstart=r,t.onmousemove=r,Li&&this.options.opacity<1&&nt(t,this.options.opacity),zi&&!Mi&&(t.style.WebkitBackfaceVisibility="hidden")},_addTile:function(t,i){var n=this._getTilePos(t),o=this._tileCoordsToKey(t),s=this.createTile(this._wrapCoords(t),e(this._tileReady,this,t));this._initTile(s),this.createTile.length<2&&f(e(this._tileReady,this,t,null,s)),at(s,n),this._tiles[o]={el:s,coords:t,current:!0},i.appendChild(s),this.fire("tileloadstart",{tile:s,coords:t})},_tileReady:function(t,i,n){i&&this.fire("tileerror",{error:i,tile:n,coords:t});var o=this._tileCoordsToKey(t);(n=this._tiles[o])&&(n.loaded=+new Date,this._map._fadeAnimated?(nt(n.el,0),g(this._fadeFrame),this._fadeFrame=f(this._updateOpacity,this)):(n.active=!0,this._pruneTiles()),i||(Q(n.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:n.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),Li||!this._map._fadeAnimated?f(this._pruneTiles,this):setTimeout(e(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var i=new x(this._wrapX?s(t.x,this._wrapX):t.x,this._wrapY?s(t.y,this._wrapY):t.y);return i.z=t.z,i},_pxBoundsToTileRange:function(t){var i=this.getTileSize();return new P(t.min.unscaleBy(i).floor(),t.max.unscaleBy(i).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}}),mn=pn.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1},initialize:function(t,i){this._url=t,(i=l(this,i)).detectRetina&&Yi&&i.maxZoom>0&&(i.tileSize=Math.floor(i.tileSize/2),i.zoomReverse?(i.zoomOffset--,i.minZoom++):(i.zoomOffset++,i.maxZoom--),i.minZoom=Math.max(0,i.minZoom)),"string"==typeof i.subdomains&&(i.subdomains=i.subdomains.split("")),zi||this.on("tileunload",this._onTileRemove)},setUrl:function(t,i){return this._url=t,i||this.redraw(),this},createTile:function(t,i){var n=document.createElement("img");return mt(n,"load",e(this._tileOnLoad,this,i,n)),mt(n,"error",e(this._tileOnError,this,i,n)),(this.options.crossOrigin||""===this.options.crossOrigin)&&(n.crossOrigin=!0===this.options.crossOrigin?"":this.options.crossOrigin),n.alt="",n.setAttribute("role","presentation"),n.src=this.getTileUrl(t),n},getTileUrl:function(t){var e={r:Yi?"@2x":"",s:this._getSubdomain(t),x:t.x,y:t.y,z:this._getZoomForUrl()};if(this._map&&!this._map.options.crs.infinite){var n=this._globalTileRange.max.y-t.y;this.options.tms&&(e.y=n),e["-y"]=n}return _(this._url,i(e,this.options))},_tileOnLoad:function(t,i){Li?setTimeout(e(t,this,null,i),0):t(null,i)},_tileOnError:function(t,i,e){var n=this.options.errorTileUrl;n&&i.getAttribute("src")!==n&&(i.src=n),t(e,i)},_onTileRemove:function(t){t.tile.onload=null},_getZoomForUrl:function(){var t=this._tileZoom,i=this.options.maxZoom,e=this.options.zoomReverse,n=this.options.zoomOffset;return e&&(t=i-t),t+n},_getSubdomain:function(t){var i=Math.abs(t.x+t.y)%this.options.subdomains.length;return this.options.subdomains[i]},_abortLoading:function(){var t,i;for(t in this._tiles)this._tiles[t].coords.z!==this._tileZoom&&((i=this._tiles[t].el).onload=r,i.onerror=r,i.complete||(i.src=si,K(i),delete this._tiles[t]))},_removeTile:function(t){var i=this._tiles[t];if(i)return Si||i.el.setAttribute("src",si),pn.prototype._removeTile.call(this,t)},_tileReady:function(t,i,e){if(this._map&&(!e||e.getAttribute("src")!==si))return pn.prototype._tileReady.call(this,t,i,e)}}),fn=mn.extend({defaultWmsParams:{service:"WMS",request:"GetMap",layers:"",styles:"",format:"image/jpeg",transparent:!1,version:"1.1.1"},options:{crs:null,uppercase:!1},initialize:function(t,e){this._url=t;var n=i({},this.defaultWmsParams);for(var o in e)o in this.options||(n[o]=e[o]);var s=(e=l(this,e)).detectRetina&&Yi?2:1,r=this.getTileSize();n.width=r.x*s,n.height=r.y*s,this.wmsParams=n},onAdd:function(t){this._crs=this.options.crs||t.options.crs,this._wmsVersion=parseFloat(this.wmsParams.version);var i=this._wmsVersion>=1.3?"crs":"srs";this.wmsParams[i]=this._crs.code,mn.prototype.onAdd.call(this,t)},getTileUrl:function(t){var i=this._tileCoordsToNwSe(t),e=this._crs,n=b(e.project(i[0]),e.project(i[1])),o=n.min,s=n.max,r=(this._wmsVersion>=1.3&&this._crs===Ue?[o.y,o.x,s.y,s.x]:[o.x,o.y,s.x,s.y]).join(","),a=mn.prototype.getTileUrl.call(this,t);return a+c(this.wmsParams,a,this.options.uppercase)+(this.options.uppercase?"&BBOX=":"&bbox=")+r},setParams:function(t,e){return i(this.wmsParams,t),e||this.redraw(),this}});mn.WMS=fn,Jt.wms=function(t,i){return new fn(t,i)};var gn=qe.extend({options:{padding:.1,tolerance:0},initialize:function(t){l(this,t),n(this),this._layers=this._layers||{}},onAdd:function(){this._container||(this._initContainer(),this._zoomAnimated&&Q(this._container,"leaflet-zoom-animated")),this.getPane().appendChild(this._container),this._update(),this.on("update",this._updatePaths,this)},onRemove:function(){this.off("update",this._updatePaths,this),this._destroyContainer()},getEvents:function(){var t={viewreset:this._reset,zoom:this._onZoom,moveend:this._update,zoomend:this._onZoomEnd};return this._zoomAnimated&&(t.zoomanim=this._onAnimZoom),t},_onAnimZoom:function(t){this._updateTransform(t.center,t.zoom)},_onZoom:function(){this._updateTransform(this._map.getCenter(),this._map.getZoom())},_updateTransform:function(t,i){var e=this._map.getZoomScale(i,this._zoom),n=ht(this._container),o=this._map.getSize().multiplyBy(.5+this.options.padding),s=this._map.project(this._center,i),r=this._map.project(t,i).subtract(s),a=o.multiplyBy(-e).add(n).add(o).subtract(r);ji?rt(this._container,a,e):at(this._container,a)},_reset:function(){this._update(),this._updateTransform(this._center,this._zoom);for(var t in this._layers)this._layers[t]._reset()},_onZoomEnd:function(){for(var t in this._layers)this._layers[t]._project()},_updatePaths:function(){for(var t in this._layers)this._layers[t]._update()},_update:function(){var t=this.options.padding,i=this._map.getSize(),e=this._map.containerPointToLayerPoint(i.multiplyBy(-t)).round();this._bounds=new P(e,e.add(i.multiplyBy(1+2*t)).round()),this._center=this._map.getCenter(),this._zoom=this._map.getZoom()}}),vn=gn.extend({getEvents:function(){var t=gn.prototype.getEvents.call(this);return t.viewprereset=this._onViewPreReset,t},_onViewPreReset:function(){this._postponeUpdatePaths=!0},onAdd:function(){gn.prototype.onAdd.call(this),this._draw()},_initContainer:function(){var t=this._container=document.createElement("canvas");mt(t,"mousemove",o(this._onMouseMove,32,this),this),mt(t,"click dblclick mousedown mouseup contextmenu",this._onClick,this),mt(t,"mouseout",this._handleMouseOut,this),this._ctx=t.getContext("2d")},_destroyContainer:function(){g(this._redrawRequest),delete this._ctx,K(this._container),ft(this._container),delete this._container},_updatePaths:function(){if(!this._postponeUpdatePaths){this._redrawBounds=null;for(var t in this._layers)this._layers[t]._update();this._redraw()}},_update:function(){if(!this._map._animatingZoom||!this._bounds){this._drawnLayers={},gn.prototype._update.call(this);var t=this._bounds,i=this._container,e=t.getSize(),n=Yi?2:1;at(i,t.min),i.width=n*e.x,i.height=n*e.y,i.style.width=e.x+"px",i.style.height=e.y+"px",Yi&&this._ctx.scale(2,2),this._ctx.translate(-t.min.x,-t.min.y),this.fire("update")}},_reset:function(){gn.prototype._reset.call(this),this._postponeUpdatePaths&&(this._postponeUpdatePaths=!1,this._updatePaths())},_initPath:function(t){this._updateDashArray(t),this._layers[n(t)]=t;var i=t._order={layer:t,prev:this._drawLast,next:null};this._drawLast&&(this._drawLast.next=i),this._drawLast=i,this._drawFirst=this._drawFirst||this._drawLast},_addPath:function(t){this._requestRedraw(t)},_removePath:function(t){var i=t._order,e=i.next,o=i.prev;e?e.prev=o:this._drawLast=o,o?o.next=e:this._drawFirst=e,delete this._drawnLayers[t._leaflet_id],delete t._order,delete this._layers[n(t)],this._requestRedraw(t)},_updatePath:function(t){this._extendRedrawBounds(t),t._project(),t._update(),this._requestRedraw(t)},_updateStyle:function(t){this._updateDashArray(t),this._requestRedraw(t)},_updateDashArray:function(t){if("string"==typeof t.options.dashArray){var i,e=t.options.dashArray.split(/[, ]+/),n=[];for(i=0;i')}}catch(t){return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}}(),xn={_initContainer:function(){this._container=G("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(gn.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var i=t._container=yn("shape");Q(i,"leaflet-vml-shape "+(this.options.className||"")),i.coordsize="1 1",t._path=yn("path"),i.appendChild(t._path),this._updateStyle(t),this._layers[n(t)]=t},_addPath:function(t){var i=t._container;this._container.appendChild(i),t.options.interactive&&t.addInteractiveTarget(i)},_removePath:function(t){var i=t._container;K(i),t.removeInteractiveTarget(i),delete this._layers[n(t)]},_updateStyle:function(t){var i=t._stroke,e=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(i||(i=t._stroke=yn("stroke")),o.appendChild(i),i.weight=n.weight+"px",i.color=n.color,i.opacity=n.opacity,n.dashArray?i.dashStyle=oi(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):i.dashStyle="",i.endcap=n.lineCap.replace("butt","flat"),i.joinstyle=n.lineJoin):i&&(o.removeChild(i),t._stroke=null),n.fill?(e||(e=t._fill=yn("fill")),o.appendChild(e),e.color=n.fillColor||n.color,e.opacity=n.fillOpacity):e&&(o.removeChild(e),t._fill=null)},_updateCircle:function(t){var i=t._point.round(),e=Math.round(t._radius),n=Math.round(t._radiusY||e);this._setPath(t,t._empty()?"M0 0":"AL "+i.x+","+i.y+" "+e+","+n+" 0,23592600")},_setPath:function(t,i){t._path.v=i},_bringToFront:function(t){X(t._container)},_bringToBack:function(t){J(t._container)}},wn=$i?yn:E,Pn=gn.extend({getEvents:function(){var t=gn.prototype.getEvents.call(this);return t.zoomstart=this._onZoomStart,t},_initContainer:function(){this._container=wn("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=wn("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){K(this._container),ft(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_onZoomStart:function(){this._update()},_update:function(){if(!this._map._animatingZoom||!this._bounds){gn.prototype._update.call(this);var t=this._bounds,i=t.getSize(),e=this._container;this._svgSize&&this._svgSize.equals(i)||(this._svgSize=i,e.setAttribute("width",i.x),e.setAttribute("height",i.y)),at(e,t.min),e.setAttribute("viewBox",[t.min.x,t.min.y,i.x,i.y].join(" ")),this.fire("update")}},_initPath:function(t){var i=t._path=wn("path");t.options.className&&Q(i,t.options.className),t.options.interactive&&Q(i,"leaflet-interactive"),this._updateStyle(t),this._layers[n(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){K(t._path),t.removeInteractiveTarget(t._path),delete this._layers[n(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var i=t._path,e=t.options;i&&(e.stroke?(i.setAttribute("stroke",e.color),i.setAttribute("stroke-opacity",e.opacity),i.setAttribute("stroke-width",e.weight),i.setAttribute("stroke-linecap",e.lineCap),i.setAttribute("stroke-linejoin",e.lineJoin),e.dashArray?i.setAttribute("stroke-dasharray",e.dashArray):i.removeAttribute("stroke-dasharray"),e.dashOffset?i.setAttribute("stroke-dashoffset",e.dashOffset):i.removeAttribute("stroke-dashoffset")):i.setAttribute("stroke","none"),e.fill?(i.setAttribute("fill",e.fillColor||e.color),i.setAttribute("fill-opacity",e.fillOpacity),i.setAttribute("fill-rule",e.fillRule||"evenodd")):i.setAttribute("fill","none"))},_updatePoly:function(t,i){this._setPath(t,k(t._parts,i))},_updateCircle:function(t){var i=t._point,e=Math.max(Math.round(t._radius),1),n="a"+e+","+(Math.max(Math.round(t._radiusY),1)||e)+" 0 1,0 ",o=t._empty()?"M0 0":"M"+(i.x-e)+","+i.y+n+2*e+",0 "+n+2*-e+",0 ";this._setPath(t,o)},_setPath:function(t,i){t._path.setAttribute("d",i)},_bringToFront:function(t){X(t._path)},_bringToBack:function(t){J(t._path)}});$i&&Pn.include(xn),be.include({getRenderer:function(t){var i=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer;return i||(i=this._renderer=this._createRenderer()),this.hasLayer(i)||this.addLayer(i),i},_getPaneRenderer:function(t){if("overlayPane"===t||void 0===t)return!1;var i=this._paneRenderers[t];return void 0===i&&(i=this._createRenderer({pane:t}),this._paneRenderers[t]=i),i},_createRenderer:function(t){return this.options.preferCanvas&&$t(t)||Qt(t)}});var Ln=on.extend({initialize:function(t,i){on.prototype.initialize.call(this,this._boundsToLatLngs(t),i)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return t=z(t),[t.getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});Pn.create=wn,Pn.pointsToPath=k,sn.geometryToLayer=Ft,sn.coordsToLatLng=Ut,sn.coordsToLatLngs=Vt,sn.latLngToCoords=qt,sn.latLngsToCoords=Gt,sn.getFeature=Kt,sn.asFeature=Yt,be.mergeOptions({boxZoom:!0});var bn=Ee.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){mt(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){ft(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){K(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),fi(),ut(),this._startPoint=this._map.mouseEventToContainerPoint(t),mt(document,{contextmenu:Lt,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=G("div","leaflet-zoom-box",this._container),Q(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var i=new P(this._point,this._startPoint),e=i.getSize();at(this._box,i.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(K(this._box),tt(this._container,"leaflet-crosshair")),gi(),lt(),ft(document,{contextmenu:Lt,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){if((1===t.which||1===t.button)&&(this._finish(),this._moved)){this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(e(this._resetState,this),0);var i=new T(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point));this._map.fitBounds(i).fire("boxzoomend",{boxZoomBounds:i})}},_onKeyDown:function(t){27===t.keyCode&&this._finish()}});be.addInitHook("addHandler","boxZoom",bn),be.mergeOptions({doubleClickZoom:!0});var Tn=Ee.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var i=this._map,e=i.getZoom(),n=i.options.zoomDelta,o=t.originalEvent.shiftKey?e-n:e+n;"center"===i.options.doubleClickZoom?i.setZoom(o):i.setZoomAround(t.containerPoint,o)}});be.addInitHook("addHandler","doubleClickZoom",Tn),be.mergeOptions({dragging:!0,inertia:!Mi,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0});var zn=Ee.extend({addHooks:function(){if(!this._draggable){var t=this._map;this._draggable=new Re(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))}Q(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){tt(this._map._container,"leaflet-grab"),tt(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t=this._map;if(t._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity){var i=z(this._map.options.maxBounds);this._offsetLimit=b(this._map.latLngToContainerPoint(i.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(i.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))}else this._offsetLimit=null;t.fire("movestart").fire("dragstart"),t.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){if(this._map.options.inertia){var i=this._lastTime=+new Date,e=this._lastPos=this._draggable._absPos||this._draggable._newPos;this._positions.push(e),this._times.push(i),this._prunePositions(i)}this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;this._positions.length>1&&t-this._times[0]>50;)this._positions.shift(),this._times.shift()},_onZoomEnd:function(){var t=this._map.getSize().divideBy(2),i=this._map.latLngToLayerPoint([0,0]);this._initialWorldOffset=i.subtract(t).x,this._worldWidth=this._map.getPixelWorldBounds().getSize().x},_viscousLimit:function(t,i){return t-(t-i)*this._viscosity},_onPreDragLimit:function(){if(this._viscosity&&this._offsetLimit){var t=this._draggable._newPos.subtract(this._draggable._startPos),i=this._offsetLimit;t.xi.max.x&&(t.x=this._viscousLimit(t.x,i.max.x)),t.y>i.max.y&&(t.y=this._viscousLimit(t.y,i.max.y)),this._draggable._newPos=this._draggable._startPos.add(t)}},_onPreDragWrap:function(){var t=this._worldWidth,i=Math.round(t/2),e=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-i+e)%t+i-e,s=(n+i+e)%t-i-e,r=Math.abs(o+e)0?s:-s))-i;this._delta=0,this._startTime=null,r&&("center"===t.options.scrollWheelZoom?t.setZoom(i+r):t.setZoomAround(this._lastMousePos,i+r))}});be.addInitHook("addHandler","scrollWheelZoom",Cn),be.mergeOptions({tap:!0,tapTolerance:15});var Sn=Ee.extend({addHooks:function(){mt(this._map._container,"touchstart",this._onDown,this)},removeHooks:function(){ft(this._map._container,"touchstart",this._onDown,this)},_onDown:function(t){if(t.touches){if(Pt(t),this._fireClick=!0,t.touches.length>1)return this._fireClick=!1,void clearTimeout(this._holdTimeout);var i=t.touches[0],n=i.target;this._startPos=this._newPos=new x(i.clientX,i.clientY),n.tagName&&"a"===n.tagName.toLowerCase()&&Q(n,"leaflet-active"),this._holdTimeout=setTimeout(e(function(){this._isTapValid()&&(this._fireClick=!1,this._onUp(),this._simulateEvent("contextmenu",i))},this),1e3),this._simulateEvent("mousedown",i),mt(document,{touchmove:this._onMove,touchend:this._onUp},this)}},_onUp:function(t){if(clearTimeout(this._holdTimeout),ft(document,{touchmove:this._onMove,touchend:this._onUp},this),this._fireClick&&t&&t.changedTouches){var i=t.changedTouches[0],e=i.target;e&&e.tagName&&"a"===e.tagName.toLowerCase()&&tt(e,"leaflet-active"),this._simulateEvent("mouseup",i),this._isTapValid()&&this._simulateEvent("click",i)}},_isTapValid:function(){return this._newPos.distanceTo(this._startPos)<=this._map.options.tapTolerance},_onMove:function(t){var i=t.touches[0];this._newPos=new x(i.clientX,i.clientY),this._simulateEvent("mousemove",i)},_simulateEvent:function(t,i){var e=document.createEvent("MouseEvents");e._simulated=!0,i.target._simulatedClick=!0,e.initMouseEvent(t,!0,!0,window,1,i.screenX,i.screenY,i.clientX,i.clientY,!1,!1,!1,!1,0,null),i.target.dispatchEvent(e)}});qi&&!Vi&&be.addInitHook("addHandler","tap",Sn),be.mergeOptions({touchZoom:qi&&!Mi,bounceAtZoomLimits:!0});var Zn=Ee.extend({addHooks:function(){Q(this._map._container,"leaflet-touch-zoom"),mt(this._map._container,"touchstart",this._onTouchStart,this)},removeHooks:function(){tt(this._map._container,"leaflet-touch-zoom"),ft(this._map._container,"touchstart",this._onTouchStart,this)},_onTouchStart:function(t){var i=this._map;if(t.touches&&2===t.touches.length&&!i._animatingZoom&&!this._zooming){var e=i.mouseEventToContainerPoint(t.touches[0]),n=i.mouseEventToContainerPoint(t.touches[1]);this._centerPoint=i.getSize()._divideBy(2),this._startLatLng=i.containerPointToLatLng(this._centerPoint),"center"!==i.options.touchZoom&&(this._pinchStartLatLng=i.containerPointToLatLng(e.add(n)._divideBy(2))),this._startDist=e.distanceTo(n),this._startZoom=i.getZoom(),this._moved=!1,this._zooming=!0,i._stop(),mt(document,"touchmove",this._onTouchMove,this),mt(document,"touchend",this._onTouchEnd,this),Pt(t)}},_onTouchMove:function(t){if(t.touches&&2===t.touches.length&&this._zooming){var i=this._map,n=i.mouseEventToContainerPoint(t.touches[0]),o=i.mouseEventToContainerPoint(t.touches[1]),s=n.distanceTo(o)/this._startDist;if(this._zoom=i.getScaleZoom(s,this._startZoom),!i.options.bounceAtZoomLimits&&(this._zoomi.getMaxZoom()&&s>1)&&(this._zoom=i._limitZoom(this._zoom)),"center"===i.options.touchZoom){if(this._center=this._startLatLng,1===s)return}else{var r=n._add(o)._divideBy(2)._subtract(this._centerPoint);if(1===s&&0===r.x&&0===r.y)return;this._center=i.unproject(i.project(this._pinchStartLatLng,this._zoom).subtract(r),this._zoom)}this._moved||(i._moveStart(!0,!1),this._moved=!0),g(this._animRequest);var a=e(i._move,i,this._center,this._zoom,{pinch:!0,round:!1});this._animRequest=f(a,this,!0),Pt(t)}},_onTouchEnd:function(){this._moved&&this._zooming?(this._zooming=!1,g(this._animRequest),ft(document,"touchmove",this._onTouchMove),ft(document,"touchend",this._onTouchEnd),this._map.options.zoomAnimation?this._map._animateZoom(this._center,this._map._limitZoom(this._zoom),!0,this._map.options.zoomSnap):this._map._resetView(this._center,this._map._limitZoom(this._zoom))):this._zooming=!1}});be.addInitHook("addHandler","touchZoom",Zn),be.BoxZoom=bn,be.DoubleClickZoom=Tn,be.Drag=zn,be.Keyboard=Mn,be.ScrollWheelZoom=Cn,be.Tap=Sn,be.TouchZoom=Zn,Object.freeze=ti,t.version="1.3.4+HEAD.0e566b2",t.Control=Te,t.control=ze,t.Browser=Qi,t.Evented=ci,t.Mixin=Ae,t.Util=ui,t.Class=v,t.Handler=Ee,t.extend=i,t.bind=e,t.stamp=n,t.setOptions=l,t.DomEvent=Pe,t.DomUtil=ve,t.PosAnimation=Le,t.Draggable=Re,t.LineUtil=Ne,t.PolyUtil=De,t.Point=x,t.point=w,t.Bounds=P,t.bounds=b,t.Transformation=S,t.transformation=Z,t.Projection=He,t.LatLng=M,t.latLng=C,t.LatLngBounds=T,t.latLngBounds=z,t.CRS=di,t.GeoJSON=sn,t.geoJSON=Xt,t.geoJson=an,t.Layer=qe,t.LayerGroup=Ge,t.layerGroup=function(t,i){return new Ge(t,i)},t.FeatureGroup=Ke,t.featureGroup=function(t){return new Ke(t)},t.ImageOverlay=hn,t.imageOverlay=function(t,i,e){return new hn(t,i,e)},t.VideoOverlay=un,t.videoOverlay=function(t,i,e){return new un(t,i,e)},t.DivOverlay=ln,t.Popup=cn,t.popup=function(t,i){return new cn(t,i)},t.Tooltip=_n,t.tooltip=function(t,i){return new _n(t,i)},t.Icon=Ye,t.icon=function(t){return new Ye(t)},t.DivIcon=dn,t.divIcon=function(t){return new dn(t)},t.Marker=$e,t.marker=function(t,i){return new $e(t,i)},t.TileLayer=mn,t.tileLayer=Jt,t.GridLayer=pn,t.gridLayer=function(t){return new pn(t)},t.SVG=Pn,t.svg=Qt,t.Renderer=gn,t.Canvas=vn,t.canvas=$t,t.Path=Qe,t.CircleMarker=tn,t.circleMarker=function(t,i){return new tn(t,i)},t.Circle=en,t.circle=function(t,i,e){return new en(t,i,e)},t.Polyline=nn,t.polyline=function(t,i){return new nn(t,i)},t.Polygon=on,t.polygon=function(t,i){return new on(t,i)},t.Rectangle=Ln,t.rectangle=function(t,i){return new Ln(t,i)},t.Map=be,t.map=function(t,i){return new be(t,i)};var En=window.L;t.noConflict=function(){return window.L=En,this},window.L=t}); From c3d27eb8a53b380d19e7656e6b865aef4cb8adde Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 10 Oct 2019 23:01:16 -0700 Subject: [PATCH 045/462] removed utils.cmd and related node functions --- daemon/core/nodes/base.py | 22 ---------------------- daemon/core/nodes/docker.py | 20 -------------------- daemon/core/nodes/lxd.py | 25 ------------------------- daemon/core/nodes/physical.py | 13 ------------- daemon/core/utils.py | 20 -------------------- daemon/tests/emane/test_emane.py | 9 +++++++-- daemon/tests/test_core.py | 8 +++++++- 7 files changed, 14 insertions(+), 103 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 147989d7..d2d49fb4 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -421,17 +421,6 @@ class CoreNodeBase(NodeBase): """ raise NotImplementedError - def cmd(self, args, wait=True): - """ - Runs shell command on node, with option to not wait for a result. - - :param list[str]|str args: command to run - :param bool wait: wait for command to exit, defaults to True - :return: exit status for command - :rtype: int - """ - raise NotImplementedError - def cmd_output(self, args): """ Runs shell command on node and get exit status and output. @@ -609,17 +598,6 @@ class CoreNode(CoreNodeBase): finally: self.rmnodedir() - def cmd(self, args, wait=True): - """ - Runs shell command on node, with option to not wait for a result. - - :param list[str]|str args: command to run - :param bool wait: wait for command to exit, defaults to True - :return: exit status for command - :rtype: int - """ - return self.client.cmd(args, wait) - def cmd_output(self, args): """ Runs shell command on node and get exit status and output. diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index ad7deff2..69bf376f 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -47,15 +47,6 @@ class DockerClient(object): name=self.name )) - def cmd(self, cmd, wait=True): - if isinstance(cmd, list): - cmd = " ".join(cmd) - logging.info("docker cmd wait(%s): %s", wait, cmd) - return utils.cmd("docker exec {name} {cmd}".format( - name=self.name, - cmd=cmd - ), wait) - def cmd_output(self, cmd): if isinstance(cmd, list): cmd = " ".join(cmd) @@ -155,17 +146,6 @@ class DockerNode(CoreNode): self.client.stop_container() self.up = False - def cmd(self, args, wait=True): - """ - Runs shell command on node, with option to not wait for a result. - - :param list[str]|str args: command to run - :param bool wait: wait for command to exit, defaults to True - :return: exit status for command - :rtype: int - """ - return self.client.cmd(args, wait) - def cmd_output(self, args): """ Runs shell command on node and get exit status and output. diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index 86ca192e..54a66182 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -55,13 +55,6 @@ class LxdClient(object): logging.info("lxc cmd output: %s", args) return utils.cmd_output(args) - def cmd(self, cmd, wait=True): - if isinstance(cmd, list): - cmd = " ".join(cmd) - args = self._cmd_args(cmd) - logging.info("lxc cmd: %s", args) - return utils.cmd(args, wait) - def _ns_args(self, cmd): return "nsenter -t {pid} -m -u -i -p -n {cmd}".format(pid=self.pid, cmd=cmd) @@ -72,13 +65,6 @@ class LxdClient(object): logging.info("ns cmd: %s", args) return utils.cmd_output(args) - def ns_cmd(self, cmd, wait=True): - if isinstance(cmd, list): - cmd = " ".join(cmd) - args = self._ns_args(cmd) - logging.info("ns cmd: %s", args) - return utils.cmd(args, wait) - def copy_file(self, source, destination): if destination[0] != "/": destination = os.path.join("/root/", destination) @@ -158,17 +144,6 @@ class LxcNode(CoreNode): self.client.stop_container() self.up = False - def cmd(self, args, wait=True): - """ - Runs shell command on node, with option to not wait for a result. - - :param list[str]|str args: command to run - :param bool wait: wait for command to exit, defaults to True - :return: exit status for command - :rtype: int - """ - return self.client.cmd(args, wait) - def cmd_output(self, args): """ Runs shell command on node and get exit status and output. diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 48bd5a5a..159c746d 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -52,19 +52,6 @@ class PhysicalNode(CoreNodeBase): """ return sh - def cmd(self, args, wait=True): - """ - Runs shell command on node, with option to not wait for a result. - - :param list[str]|str args: command to run - :param bool wait: wait for command to exit, defaults to True - :return: exit status for command - :rtype: int - """ - os.chdir(self.nodedir) - status = utils.cmd(args, wait) - return status - def cmd_output(self, args): """ Runs shell command on node and get exit status and output. diff --git a/daemon/core/utils.py b/daemon/core/utils.py index 8e59a050..83a18c6a 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -207,26 +207,6 @@ def mute_detach(args, **kwargs): return subprocess.Popen(args, **kwargs).pid -def cmd(args, wait=True): - """ - Runs a command on and returns the exit status. - - :param list[str]|str args: command arguments - :param bool wait: wait for command to end or not - :return: command status - :rtype: int - """ - args = split_args(args) - logging.debug("command: %s", args) - try: - p = subprocess.Popen(args) - if not wait: - return 0 - return p.wait() - except OSError: - raise CoreCommandError(-1, args) - - def cmd_output(args): """ Execute a command on the host and return a tuple containing the exit status and diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py index d9001065..3d9b9eb2 100644 --- a/daemon/tests/emane/test_emane.py +++ b/daemon/tests/emane/test_emane.py @@ -12,7 +12,7 @@ from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emane.rfpipe import EmaneRfPipeModel from core.emane.tdma import EmaneTdmaModel from core.emulator.emudata import NodeOptions -from core.errors import CoreError +from core.errors import CoreCommandError, CoreError _EMANE_MODELS = [ EmaneIeee80211abgModel, @@ -26,7 +26,12 @@ _DIR = os.path.dirname(os.path.abspath(__file__)) def ping(from_node, to_node, ip_prefixes, count=3): address = ip_prefixes.ip4_address(to_node) - return from_node.node_net_cmd(["ping", "-c", str(count), address]) + try: + from_node.node_net_cmd(["ping", "-c", str(count), address]) + status = 0 + except CoreCommandError as e: + status = e.returncode + return status class TestEmane: diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 7d64ae69..9a59b4ce 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -10,6 +10,7 @@ import pytest from core.emulator.emudata import NodeOptions from core.emulator.enumerations import MessageFlags, NodeTypes +from core.errors import CoreCommandError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.nodes.client import VnodeClient @@ -38,7 +39,12 @@ def createclients(sessiondir, clientcls=VnodeClient, cmdchnlfilterfunc=None): def ping(from_node, to_node, ip_prefixes): address = ip_prefixes.ip4_address(to_node) - return from_node.node_net_cmd(["ping", "-c", "3", address]) + try: + from_node.node_net_cmd(["ping", "-c", "3", address]) + status = 0 + except CoreCommandError as e: + status = e.returncode + return status class TestCore: From 4a6d69bb09eb27724e7b10342a666f1fd4725487 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 11 Oct 2019 09:34:49 -0700 Subject: [PATCH 046/462] removing cmd_output function from utils and nodes --- daemon/core/api/grpc/server.py | 9 ++++-- daemon/core/api/tlv/corehandlers.py | 7 +++- daemon/core/nodes/base.py | 20 ------------ daemon/core/nodes/client.py | 50 +++++------------------------ daemon/core/nodes/docker.py | 46 ++++++++------------------ daemon/core/nodes/lxd.py | 37 ++++++--------------- daemon/core/nodes/physical.py | 42 ++---------------------- daemon/core/services/utility.py | 8 +++-- daemon/core/utils.py | 21 ------------ daemon/tests/test_core.py | 4 --- 10 files changed, 51 insertions(+), 193 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 7fb6ed31..3d589fab 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -10,6 +10,7 @@ from queue import Empty, Queue import grpc +from core import utils from core.api.grpc import core_pb2, core_pb2_grpc from core.emane.nodes import EmaneNet from core.emulator.data import ( @@ -22,7 +23,7 @@ from core.emulator.data import ( ) from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.enumerations import EventTypes, LinkTypes, NodeTypes -from core.errors import CoreError +from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.nodes.base import CoreNetworkBase from core.nodes.docker import DockerNode @@ -882,7 +883,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("sending node command: %s", request) session = self.get_session(request.session_id, context) node = self.get_node(session, request.node_id, context) - _, output = node.cmd_output(request.command) + try: + args = utils.split_args(request.command) + output = node.node_net_cmd(args) + except CoreCommandError as e: + output = e.stderr return core_pb2.NodeCommandResponse(output=output) def GetNodeTerminal(self, request, context): diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index a531efe2..e1a32e5f 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -889,7 +889,12 @@ class CoreHandler(socketserver.BaseRequestHandler): or message.flags & MessageFlags.TEXT.value ): if message.flags & MessageFlags.LOCAL.value: - status, res = utils.cmd_output(command) + try: + res = utils.check_cmd(command) + status = 0 + except CoreCommandError as e: + res = e.stderr + status = e.returncode else: try: res = node.node_net_cmd(command) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index d2d49fb4..78279b5e 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -421,16 +421,6 @@ class CoreNodeBase(NodeBase): """ raise NotImplementedError - def cmd_output(self, args): - """ - Runs shell command on node and get exit status and output. - - :param list[str]|str args: command to run - :return: exit status and combined stdout and stderr - :rtype: tuple[int, str] - """ - raise NotImplementedError - def termcmdstring(self, sh): """ Create a terminal command string. @@ -598,16 +588,6 @@ class CoreNode(CoreNodeBase): finally: self.rmnodedir() - def cmd_output(self, args): - """ - Runs shell command on node and get exit status and output. - - :param list[str]|str args: command to run - :return: exit status and combined stdout and stderr - :rtype: tuple[int, str] - """ - return self.client.cmd_output(args) - def node_net_cmd(self, args, wait=True): """ Runs a command that is used to configure and setup the network within a diff --git a/daemon/core/nodes/client.py b/daemon/core/nodes/client.py index 6c72547b..3b0b2843 100644 --- a/daemon/core/nodes/client.py +++ b/daemon/core/nodes/client.py @@ -57,46 +57,6 @@ class VnodeClient(object): def _cmd_args(self): return [constants.VCMD_BIN, "-c", self.ctrlchnlname, "--"] - def cmd(self, args, wait=True): - """ - Execute a command on a node and return the status (return code). - - :param list[str]|str args: command arguments - :param bool wait: wait for command to end or not - :return: command status - :rtype: int - """ - self._verify_connection() - args = utils.split_args(args) - - # run command, return process when not waiting - cmd = self._cmd_args() + args - logging.debug("cmd wait(%s): %s", wait, cmd) - p = Popen(cmd, stdout=PIPE, stderr=PIPE) - if not wait: - return 0 - - # wait for and return exit status - return p.wait() - - def cmd_output(self, args): - """ - Execute a command on a node and return a tuple containing the - exit status and result string. stderr output - is folded into the stdout result string. - - :param list[str]|str args: command to run - :return: command status and combined stdout and stderr output - :rtype: tuple[int, str] - """ - p, stdin, stdout, stderr = self.popen(args) - stdin.close() - output = stdout.read() + stderr.read() - stdout.close() - stderr.close() - status = p.wait() - return status, output.decode("utf-8").strip() - def check_cmd(self, args, wait=True): """ Run command and return exit status and combined stdout and stderr. @@ -107,10 +67,16 @@ class VnodeClient(object): :rtype: str :raises core.CoreCommandError: when there is a non-zero exit status """ - status, output = self.cmd_output(args) + p, stdin, stdout, stderr = self.popen(args) + stdin.close() + output = stdout.read() + stderr.read() + output = output.decode("utf-8").strip() + stdout.close() + stderr.close() + status = p.wait() if wait and status != 0: raise CoreCommandError(status, args, output) - return output.strip() + return output def popen(self, args): """ diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index 69bf376f..028b3e0b 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -27,12 +27,12 @@ class DockerClient(object): def get_info(self): args = "docker inspect {name}".format(name=self.name) - status, output = utils.cmd_output(args) - if status: - raise CoreCommandError(status, args, output) + output = utils.check_cmd(args) data = json.loads(output) if not data: - raise CoreCommandError(status, args, "docker({name}) not present".format(name=self.name)) + raise CoreCommandError( + -1, args, "docker({name}) not present".format(name=self.name) + ) return data[0] def is_alive(self): @@ -47,11 +47,11 @@ class DockerClient(object): name=self.name )) - def cmd_output(self, cmd): + def check_cmd(self, cmd): if isinstance(cmd, list): cmd = " ".join(cmd) logging.info("docker cmd output: %s", cmd) - return utils.cmd_output("docker exec {name} {cmd}".format( + return utils.check_cmd("docker exec {name} {cmd}".format( name=self.name, cmd=cmd )) @@ -64,13 +64,11 @@ class DockerClient(object): cmd=cmd ) logging.info("ns cmd: %s", args) - return utils.cmd_output(args) + return utils.check_cmd(args) def get_pid(self): args = "docker inspect -f '{{{{.State.Pid}}}}' {name}".format(name=self.name) - status, output = utils.cmd_output(args) - if status: - raise CoreCommandError(status, args, output) + output = utils.check_cmd(args) self.pid = output logging.debug("node(%s) pid: %s", self.name, self.pid) return output @@ -81,9 +79,7 @@ class DockerClient(object): name=self.name, destination=destination ) - status, output = utils.cmd_output(args) - if status: - raise CoreCommandError(status, args, output) + return utils.check_cmd(args) class DockerNode(CoreNode): @@ -146,16 +142,6 @@ class DockerNode(CoreNode): self.client.stop_container() self.up = False - def cmd_output(self, args): - """ - Runs shell command on node and get exit status and output. - - :param list[str]|str args: command to run - :return: exit status and combined stdout and stderr - :rtype: tuple[int, str] - """ - return self.client.cmd_output(args) - def check_cmd(self, args): """ Runs shell command on node. @@ -165,20 +151,14 @@ class DockerNode(CoreNode): :rtype: str :raises CoreCommandError: when a non-zero exit status occurs """ - status, output = self.client.cmd_output(args) - if status: - raise CoreCommandError(status, args, output) - return output + return self.client.check_cmd(args) - def node_net_cmd(self, args): + def node_net_cmd(self, args, wait=True): if not self.up: logging.debug("node down, not running network command: %s", args) - return 0 + return "" - status, output = self.client.ns_cmd(args) - if status: - raise CoreCommandError(status, args, output) - return output + return self.client.ns_cmd(args) def termcmdstring(self, sh="/bin/sh"): """ diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index 54a66182..c28306b6 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -25,13 +25,11 @@ class LxdClient(object): def get_info(self): args = "lxc list {name} --format json".format(name=self.name) - status, output = utils.cmd_output(args) - if status: - raise CoreCommandError(status, args, output) + output = utils.check_cmd(args) data = json.loads(output) if not data: raise CoreCommandError( - status, args, "LXC({name}) not present".format(name=self.name) + -1, args, "LXC({name}) not present".format(name=self.name) ) return data[0] @@ -48,22 +46,22 @@ class LxdClient(object): def _cmd_args(self, cmd): return "lxc exec -nT {name} -- {cmd}".format(name=self.name, cmd=cmd) - def cmd_output(self, cmd): + def check_cmd(self, cmd): if isinstance(cmd, list): cmd = " ".join(cmd) args = self._cmd_args(cmd) logging.info("lxc cmd output: %s", args) - return utils.cmd_output(args) + return utils.check_cmd(args) def _ns_args(self, cmd): return "nsenter -t {pid} -m -u -i -p -n {cmd}".format(pid=self.pid, cmd=cmd) - def ns_cmd_output(self, cmd): + def ns_check_cmd(self, cmd): if isinstance(cmd, list): cmd = " ".join(cmd) args = self._ns_args(cmd) logging.info("ns cmd: %s", args) - return utils.cmd_output(args) + return utils.check_cmd(args) def copy_file(self, source, destination): if destination[0] != "/": @@ -72,9 +70,7 @@ class LxdClient(object): args = "lxc file push {source} {name}/{destination}".format( source=source, name=self.name, destination=destination ) - status, output = utils.cmd_output(args) - if status: - raise CoreCommandError(status, args, output) + utils.check_cmd(args) class LxcNode(CoreNode): @@ -144,16 +140,6 @@ class LxcNode(CoreNode): self.client.stop_container() self.up = False - def cmd_output(self, args): - """ - Runs shell command on node and get exit status and output. - - :param list[str]|str args: command to run - :return: exit status and combined stdout and stderr - :rtype: tuple[int, str] - """ - return self.client.cmd_output(args) - def check_cmd(self, args): """ Runs shell command on node. @@ -163,15 +149,12 @@ class LxcNode(CoreNode): :rtype: str :raises CoreCommandError: when a non-zero exit status occurs """ - status, output = self.client.cmd_output(args) - if status: - raise CoreCommandError(status, args, output) - return output + return self.client.check_cmd(args) - def node_net_cmd(self, args): + def node_net_cmd(self, args, wait=True): if not self.up: logging.debug("node down, not running network command: %s", args) - return 0 + return "" return self.check_cmd(args) def termcmdstring(self, sh="/bin/sh"): diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 159c746d..60e445ea 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -4,7 +4,6 @@ PhysicalNode class for including real systems in the emulated network. import logging import os -import subprocess import threading from core import constants, utils @@ -52,20 +51,6 @@ class PhysicalNode(CoreNodeBase): """ return sh - def cmd_output(self, args): - """ - Runs shell command on node and get exit status and output. - - :param list[str]|str args: command to run - :return: exit status and combined stdout and stderr - :rtype: tuple[int, str] - """ - os.chdir(self.nodedir) - p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - stdout, _ = p.communicate() - status = p.wait() - return status, stdout.strip() - def check_cmd(self, args): """ Runs shell command on node. @@ -75,10 +60,8 @@ class PhysicalNode(CoreNodeBase): :rtype: str :raises CoreCommandError: when a non-zero exit status occurs """ - status, output = self.cmd_output(args) - if status: - raise CoreCommandError(status, args, output) - return output.strip() + os.chdir(self.nodedir) + return utils.check_cmd(args) def shcmd(self, cmdstr, sh="/bin/sh"): return self.node_net_cmd([sh, "-c", cmdstr]) @@ -526,27 +509,6 @@ class Rj45Node(CoreNodeBase, CoreInterface): """ raise NotImplementedError - def cmd(self, args, wait=True): - """ - Runs shell command on node, with option to not wait for a result. - - :param list[str]|str args: command to run - :param bool wait: wait for command to exit, defaults to True - :return: exit status for command - :rtype: int - """ - raise NotImplementedError - - def cmd_output(self, args): - """ - Runs shell command on node and get exit status and output. - - :param list[str]|str args: command to run - :return: exit status and combined stdout and stderr - :rtype: tuple[int, str] - """ - raise NotImplementedError - def termcmdstring(self, sh): """ Create a terminal command string. diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index 8088b149..66a84dd6 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -415,9 +415,11 @@ class HttpService(UtilService): Detect the apache2 version using the 'a2query' command. """ try: - status, result = utils.cmd_output(["a2query", "-v"]) - except CoreCommandError: - status = -1 + result = utils.check_cmd(["a2query", "-v"]) + status = 0 + except CoreCommandError as e: + status = e.returncode + result = e.stderr if status == 0 and result[:3] == "2.4": return cls.APACHEVER24 diff --git a/daemon/core/utils.py b/daemon/core/utils.py index 83a18c6a..ead65eeb 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -207,27 +207,6 @@ def mute_detach(args, **kwargs): return subprocess.Popen(args, **kwargs).pid -def cmd_output(args): - """ - Execute a command on the host and return a tuple containing the exit status and - result string. stderr output is folded into the stdout result string. - - :param list[str]|str args: command arguments - :return: command status and stdout - :rtype: tuple[int, str] - :raises CoreCommandError: when the file to execute is not found - """ - args = split_args(args) - logging.debug("command: %s", args) - try: - p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - stdout, _ = p.communicate() - status = p.wait() - return status, stdout.decode("utf-8").strip() - except OSError: - raise CoreCommandError(-1, args) - - def check_cmd(args, **kwargs): """ Execute a command on the host and return a tuple containing the exit status and diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 9a59b4ce..7432ee58 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -108,15 +108,11 @@ class TestCore: # check various command using vcmd module command = ["ls"] - status, output = client.cmd_output(command) - assert not status p, stdin, stdout, stderr = client.popen(command) assert not p.wait() assert not client.icmd(command) # check various command using command line - status, output = client.cmd_output(command) - assert not status p, stdin, stdout, stderr = client.popen(command) assert not p.wait() assert not client.icmd(command) From d326f246a7adbc2cb49a5ded4f568f5e8f326902 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 11 Oct 2019 12:57:37 -0700 Subject: [PATCH 047/462] removed node based check_cmd, updated to use appropriate function --- daemon/core/emane/emanemanager.py | 7 +++- daemon/core/emulator/session.py | 10 +++--- daemon/core/nodes/base.py | 53 ++++++---------------------- daemon/core/nodes/docker.py | 13 +------ daemon/core/nodes/interface.py | 17 +++++++-- daemon/core/nodes/lxd.py | 15 ++------ daemon/core/nodes/netclient.py | 3 +- daemon/core/nodes/network.py | 8 +++-- daemon/core/nodes/physical.py | 33 +++-------------- daemon/core/services/coreservices.py | 3 +- daemon/tests/test_nodes.py | 2 +- 11 files changed, 51 insertions(+), 113 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 49cf4f24..90ca9dfe 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -152,8 +152,13 @@ class EmaneManager(ModelManager): """ try: # check for emane - emane_version = utils.check_cmd(["emane", "--version"]) + args = ["emane", "--version"] + emane_version = utils.check_cmd(args) logging.info("using EMANE: %s", emane_version) + args = " ".join(args) + for server in self.session.servers: + conn = self.session.servers[server] + distributed.remote_cmd(conn, args) # load default emane models self.load_models(EMANE_MODELS) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 9bc2ab80..e3a2389b 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -140,6 +140,11 @@ class Session(object): self.options.set_config(key, value) self.metadata = SessionMetaData() + # distributed servers + self.servers = {} + self.tunnels = {} + self.address = None + # initialize session feature helpers self.broker = CoreBroker(session=self) self.location = CoreLocation() @@ -148,11 +153,6 @@ class Session(object): self.emane = EmaneManager(session=self) self.sdt = Sdt(session=self) - # distributed servers - self.servers = {} - self.tunnels = {} - self.address = None - # initialize default node services self.services.default_services = { "mdr": ("zebra", "OSPFv3MDR", "IPForward"), diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 78279b5e..2b9e08eb 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -84,18 +84,24 @@ class NodeBase(object): """ raise NotImplementedError - def net_cmd(self, args, env=None): + def net_cmd(self, args, env=None, cwd=None, wait=True): """ Runs a command that is used to configure and setup the network on the host system. :param list[str]|str args: command to run :param dict env: environment to run command with + :param str cwd: directory to run command in + :param bool wait: True to wait for status, False otherwise :return: combined stdout and stderr :rtype: str :raises CoreCommandError: when a non-zero exit status occurs """ - raise NotImplementedError + if self.server is None: + return utils.check_cmd(args, env=env, cwd=cwd) + else: + args = " ".join(args) + return distributed.remote_cmd(self.server, args, env, cwd, wait) def setposition(self, x=None, y=None, z=None): """ @@ -381,40 +387,13 @@ class CoreNodeBase(NodeBase): return common - def net_cmd(self, args, env=None): - """ - Runs a command that is used to configure and setup the network on the host - system. - - :param list[str]|str args: command to run - :param dict env: environment to run command with - :return: combined stdout and stderr - :rtype: str - :raises CoreCommandError: when a non-zero exit status occurs - """ - if self.server is None: - return utils.check_cmd(args, env=env) - else: - args = " ".join(args) - return distributed.remote_cmd(self.server, args, env=env) - - def node_net_cmd(self, args): + def node_net_cmd(self, args, wait=True): """ Runs a command that is used to configure and setup the network within a node. :param list[str]|str args: command to run - :return: combined stdout and stderr - :rtype: str - :raises CoreCommandError: when a non-zero exit status occurs - """ - raise NotImplementedError - - def check_cmd(self, args): - """ - Runs shell command on node. - - :param list[str]|str args: command to run + :param bool wait: True to wait for status, False otherwise :return: combined stdout and stderr :rtype: str :raises CoreCommandError: when a non-zero exit status occurs @@ -607,18 +586,6 @@ class CoreNode(CoreNodeBase): args = " ".join(args) return distributed.remote_cmd(self.server, args, wait=wait) - def check_cmd(self, args): - """ - Runs shell command on node. - - :param list[str]|str args: command to run - :param bool wait: True to wait for status, False otherwise - :return: combined stdout and stderr - :rtype: str - :raises CoreCommandError: when a non-zero exit status occurs - """ - return self.client.check_cmd(args) - def termcmdstring(self, sh="/bin/sh"): """ Create a terminal command string. diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index 028b3e0b..28a92607 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -142,17 +142,6 @@ class DockerNode(CoreNode): self.client.stop_container() self.up = False - def check_cmd(self, args): - """ - Runs shell command on node. - - :param list[str]|str args: command to run - :return: combined stdout and stderr - :rtype: str - :raises CoreCommandError: when a non-zero exit status occurs - """ - return self.client.check_cmd(args) - def node_net_cmd(self, args, wait=True): if not self.up: logging.debug("node down, not running network command: %s", args) @@ -178,7 +167,7 @@ class DockerNode(CoreNode): """ logging.debug("creating node dir: %s", path) args = "mkdir -p {path}".format(path=path) - self.check_cmd(args) + self.client.check_cmd(args) def mount(self, source, target): """ diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 8b73b1b7..08f1d662 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -48,12 +48,23 @@ class CoreInterface(object): self.server = server self.net_client = LinuxNetClient(self.net_cmd) - def net_cmd(self, args): + def net_cmd(self, args, env=None, cwd=None, wait=True): + """ + Runs a command on the host system or distributed servers. + + :param list[str]|str args: command to run + :param dict env: environment to run command with + :param str cwd: directory to run command in + :param bool wait: True to wait for status, False otherwise + :return: combined stdout and stderr + :rtype: str + :raises CoreCommandError: when a non-zero exit status occurs + """ if self.server is None: - return utils.check_cmd(args) + return utils.check_cmd(args, env=env, cwd=cwd) else: args = " ".join(args) - return distributed.remote_cmd(self.server, args) + return distributed.remote_cmd(self.server, args, env, cwd, wait) def startup(self): """ diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index c28306b6..f16df715 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -140,22 +140,11 @@ class LxcNode(CoreNode): self.client.stop_container() self.up = False - def check_cmd(self, args): - """ - Runs shell command on node. - - :param list[str]|str args: command to run - :return: combined stdout and stderr - :rtype: str - :raises CoreCommandError: when a non-zero exit status occurs - """ - return self.client.check_cmd(args) - def node_net_cmd(self, args, wait=True): if not self.up: logging.debug("node down, not running network command: %s", args) return "" - return self.check_cmd(args) + return self.client.check_cmd(args) def termcmdstring(self, sh="/bin/sh"): """ @@ -175,7 +164,7 @@ class LxcNode(CoreNode): """ logging.info("creating node dir: %s", path) args = "mkdir -p {path}".format(path=path) - self.check_cmd(args) + return self.client.check_cmd(args) def mount(self, source, target): """ diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 34fee343..a689dde9 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -5,7 +5,6 @@ Clients for dealing with bridge/interface commands. import os from core.constants import BRCTL_BIN, ETHTOOL_BIN, IP_BIN, OVS_BIN, TC_BIN -from core.utils import check_cmd class LinuxNetClient(object): @@ -275,7 +274,7 @@ class LinuxNetClient(object): :param str name: bridge name :return: nothing """ - check_cmd([BRCTL_BIN, "setageing", name, "0"]) + self.run([BRCTL_BIN, "setageing", name, "0"]) class OvsNetClient(LinuxNetClient): diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 81dfc34b..b236b96b 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -308,24 +308,26 @@ class CoreNetwork(CoreNetworkBase): self.startup() ebq.startupdateloop(self) - def net_cmd(self, args, env=None): + def net_cmd(self, args, env=None, cwd=None, wait=True): """ Runs a command that is used to configure and setup the network on the host system and all configured distributed servers. :param list[str]|str args: command to run :param dict env: environment to run command with + :param str cwd: directory to run command in + :param bool wait: True to wait for status, False otherwise :return: combined stdout and stderr :rtype: str :raises CoreCommandError: when a non-zero exit status occurs """ logging.info("network node(%s) cmd", self.name) - output = utils.check_cmd(args, env=env) + output = utils.check_cmd(args, env=env, cwd=cwd) args = " ".join(args) for server in self.session.servers: conn = self.session.servers[server] - distributed.remote_cmd(conn, args, env=env) + distributed.remote_cmd(conn, args, env, cwd, wait) return output diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 60e445ea..4c219258 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -51,21 +51,6 @@ class PhysicalNode(CoreNodeBase): """ return sh - def check_cmd(self, args): - """ - Runs shell command on node. - - :param list[str]|str args: command to run - :return: combined stdout and stderr - :rtype: str - :raises CoreCommandError: when a non-zero exit status occurs - """ - os.chdir(self.nodedir) - return utils.check_cmd(args) - - def shcmd(self, cmdstr, sh="/bin/sh"): - return self.node_net_cmd([sh, "-c", cmdstr]) - def sethwaddr(self, ifindex, addr): """ Set hardware address for an interface. @@ -205,13 +190,13 @@ class PhysicalNode(CoreNodeBase): source = os.path.abspath(source) logging.info("mounting %s at %s", source, target) os.makedirs(target) - self.check_cmd([constants.MOUNT_BIN, "--bind", source, target]) + self.net_cmd([constants.MOUNT_BIN, "--bind", source, target], cwd=self.nodedir) self._mounts.append((source, target)) def umount(self, target): logging.info("unmounting '%s'" % target) try: - self.check_cmd([constants.UMOUNT_BIN, "-l", target]) + self.net_cmd([constants.UMOUNT_BIN, "-l", target], cwd=self.nodedir) except CoreCommandError: logging.exception("unmounting failed for %s", target) @@ -256,7 +241,8 @@ class Rj45Node(CoreNodeBase, CoreInterface): :param str name: node name :param mtu: rj45 mtu :param bool start: start flag - :param str server: remote server node will run on, default is None for localhost + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost """ CoreNodeBase.__init__(self, session, _id, name, start, server) CoreInterface.__init__(self, node=self, name=name, mtu=mtu) @@ -498,17 +484,6 @@ class Rj45Node(CoreNodeBase, CoreInterface): CoreInterface.setposition(self, x, y, z) return result - def check_cmd(self, args): - """ - Runs shell command on node. - - :param list[str]|str args: command to run - :return: exist status and combined stdout and stderr - :rtype: tuple[int, str] - :raises CoreCommandError: when a non-zero exit status occurs - """ - raise NotImplementedError - def termcmdstring(self, sh): """ Create a terminal command string. diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index d6eeb1b5..dc45fa33 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -631,8 +631,9 @@ class CoreServices(object): """ status = 0 for args in service.shutdown: + args = utils.split_args(args) try: - node.check_cmd(args) + node.node_net_cmd(args) except CoreCommandError: logging.exception("error running stop command %s", args) status = -1 diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index baf0c20c..ad5476d4 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -30,7 +30,7 @@ class TestNodes: assert os.path.exists(node.nodedir) assert node.alive() assert node.up - assert node.check_cmd(["ip", "addr", "show", "lo"]) + assert node.node_net_cmd(["ip", "addr", "show", "lo"]) def test_node_update(self, session): # given From fc7a161221b44fffcebba3bca2a6189efa7661b8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 11 Oct 2019 13:15:57 -0700 Subject: [PATCH 048/462] updated utils.check_cmd to accept the same parameters as other commands and be leveraged for node cmds --- daemon/core/nodes/base.py | 2 +- daemon/core/nodes/docker.py | 6 +++--- daemon/core/nodes/interface.py | 2 +- daemon/core/nodes/lxd.py | 6 +++--- daemon/core/nodes/network.py | 2 +- daemon/core/utils.py | 29 ++++++++++++++++------------- 6 files changed, 25 insertions(+), 22 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 2b9e08eb..6a3bebf0 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -98,7 +98,7 @@ class NodeBase(object): :raises CoreCommandError: when a non-zero exit status occurs """ if self.server is None: - return utils.check_cmd(args, env=env, cwd=cwd) + return utils.check_cmd(args, env, cwd, wait) else: args = " ".join(args) return distributed.remote_cmd(self.server, args, env, cwd, wait) diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index 28a92607..1b8322ae 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -56,7 +56,7 @@ class DockerClient(object): cmd=cmd )) - def ns_cmd(self, cmd): + def ns_cmd(self, cmd, wait): if isinstance(cmd, list): cmd = " ".join(cmd) args = "nsenter -t {pid} -u -i -p -n {cmd}".format( @@ -64,7 +64,7 @@ class DockerClient(object): cmd=cmd ) logging.info("ns cmd: %s", args) - return utils.check_cmd(args) + return utils.check_cmd(args, wait=wait) def get_pid(self): args = "docker inspect -f '{{{{.State.Pid}}}}' {name}".format(name=self.name) @@ -147,7 +147,7 @@ class DockerNode(CoreNode): logging.debug("node down, not running network command: %s", args) return "" - return self.client.ns_cmd(args) + return self.client.ns_cmd(args, wait) def termcmdstring(self, sh="/bin/sh"): """ diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 08f1d662..d749bbde 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -61,7 +61,7 @@ class CoreInterface(object): :raises CoreCommandError: when a non-zero exit status occurs """ if self.server is None: - return utils.check_cmd(args, env=env, cwd=cwd) + return utils.check_cmd(args, env, cwd, wait) else: args = " ".join(args) return distributed.remote_cmd(self.server, args, env, cwd, wait) diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index f16df715..df01f4ad 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -46,12 +46,12 @@ class LxdClient(object): def _cmd_args(self, cmd): return "lxc exec -nT {name} -- {cmd}".format(name=self.name, cmd=cmd) - def check_cmd(self, cmd): + def check_cmd(self, cmd, wait): if isinstance(cmd, list): cmd = " ".join(cmd) args = self._cmd_args(cmd) logging.info("lxc cmd output: %s", args) - return utils.check_cmd(args) + return utils.check_cmd(args, wait=wait) def _ns_args(self, cmd): return "nsenter -t {pid} -m -u -i -p -n {cmd}".format(pid=self.pid, cmd=cmd) @@ -144,7 +144,7 @@ class LxcNode(CoreNode): if not self.up: logging.debug("node down, not running network command: %s", args) return "" - return self.client.check_cmd(args) + return self.client.check_cmd(args, wait) def termcmdstring(self, sh="/bin/sh"): """ diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index b236b96b..7197c77f 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -322,7 +322,7 @@ class CoreNetwork(CoreNetworkBase): :raises CoreCommandError: when a non-zero exit status occurs """ logging.info("network node(%s) cmd", self.name) - output = utils.check_cmd(args, env=env, cwd=cwd) + output = utils.check_cmd(args, env, cwd, wait) args = " ".join(args) for server in self.session.servers: diff --git a/daemon/core/utils.py b/daemon/core/utils.py index ead65eeb..df59f9ea 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -11,8 +11,8 @@ import logging import logging.config import os import shlex -import subprocess import sys +from subprocess import PIPE, STDOUT, Popen from past.builtins import basestring @@ -203,33 +203,36 @@ def mute_detach(args, **kwargs): args = split_args(args) kwargs["preexec_fn"] = _detach_init kwargs["stdout"] = DEVNULL - kwargs["stderr"] = subprocess.STDOUT - return subprocess.Popen(args, **kwargs).pid + kwargs["stderr"] = STDOUT + return Popen(args, **kwargs).pid -def check_cmd(args, **kwargs): +def check_cmd(args, env=None, cwd=None, wait=True): """ Execute a command on the host and return a tuple containing the exit status and result string. stderr output is folded into the stdout result string. :param list[str]|str args: command arguments - :param dict kwargs: keyword arguments to pass to subprocess.Popen + :param dict env: environment to run command with + :param str cwd: directory to run command in + :param bool wait: True to wait for status, False otherwise :return: combined stdout and stderr :rtype: str :raises CoreCommandError: when there is a non-zero exit status or the file to execute is not found """ - kwargs["stdout"] = subprocess.PIPE - kwargs["stderr"] = subprocess.STDOUT args = split_args(args) logging.info("command: %s", args) try: - p = subprocess.Popen(args, **kwargs) - stdout, _ = p.communicate() - status = p.wait() - if status != 0: - raise CoreCommandError(status, args, stdout) - return stdout.decode("utf-8").strip() + p = Popen(args, stdout=PIPE, stderr=PIPE, env=env, cwd=cwd) + if wait: + stdout, stderr = p.communicate() + status = p.wait() + if status != 0: + raise CoreCommandError(status, args, stdout, stderr) + return stdout.decode("utf-8").strip() + else: + return "" except OSError: raise CoreCommandError(-1, args) From b5d71bab8243ad8700b397530cf2d889cf0158ca Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 11 Oct 2019 13:36:00 -0700 Subject: [PATCH 049/462] removed VnodeClient.popen --- daemon/core/nodes/base.py | 1 - daemon/core/nodes/client.py | 28 ++-------------------------- daemon/core/utils.py | 2 +- daemon/tests/test_core.py | 4 ---- 4 files changed, 3 insertions(+), 32 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 6a3bebf0..120c53ca 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -579,7 +579,6 @@ class CoreNode(CoreNodeBase): :raises CoreCommandError: when a non-zero exit status occurs """ if self.server is None: - logging.info("node(%s) cmd: %s", self.name, args) return self.client.check_cmd(args, wait=wait) else: args = self.client._cmd_args() + args diff --git a/daemon/core/nodes/client.py b/daemon/core/nodes/client.py index 3b0b2843..13072c05 100644 --- a/daemon/core/nodes/client.py +++ b/daemon/core/nodes/client.py @@ -4,12 +4,9 @@ over a control channel to the vnoded process running in a network namespace. The control channel can be accessed via calls using the vcmd shell. """ -import logging import os -from subprocess import PIPE, Popen from core import constants, utils -from core.errors import CoreCommandError class VnodeClient(object): @@ -67,31 +64,10 @@ class VnodeClient(object): :rtype: str :raises core.CoreCommandError: when there is a non-zero exit status """ - p, stdin, stdout, stderr = self.popen(args) - stdin.close() - output = stdout.read() + stderr.read() - output = output.decode("utf-8").strip() - stdout.close() - stderr.close() - status = p.wait() - if wait and status != 0: - raise CoreCommandError(status, args, output) - return output - - def popen(self, args): - """ - Execute a popen command against the node. - - :param list[str]|str args: command arguments - :return: popen object, stdin, stdout, and stderr - :rtype: tuple - """ self._verify_connection() args = utils.split_args(args) - cmd = self._cmd_args() + args - logging.debug("popen: %s", cmd) - p = Popen(cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE) - return p, p.stdin, p.stdout, p.stderr + args = self._cmd_args() + args + return utils.check_cmd(args, wait=wait) def icmd(self, args): """ diff --git a/daemon/core/utils.py b/daemon/core/utils.py index df59f9ea..003c7134 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -222,7 +222,7 @@ def check_cmd(args, env=None, cwd=None, wait=True): execute is not found """ args = split_args(args) - logging.info("command: %s", args) + logging.info("command cwd(%s) wait(%s): %s", cwd, wait, args) try: p = Popen(args, stdout=PIPE, stderr=PIPE, env=env, cwd=cwd) if wait: diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 7432ee58..4360ba06 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -108,13 +108,9 @@ class TestCore: # check various command using vcmd module command = ["ls"] - p, stdin, stdout, stderr = client.popen(command) - assert not p.wait() assert not client.icmd(command) # check various command using command line - p, stdin, stdout, stderr = client.popen(command) - assert not p.wait() assert not client.icmd(command) # check module methods From 69772f993c47b90357f93c7c8a1a5157ac864600 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 11 Oct 2019 13:55:06 -0700 Subject: [PATCH 050/462] removed VnodeClient.icmd and VnodeClient.term --- daemon/core/nodes/client.py | 52 ---------------------------- daemon/examples/python/emane80211.py | 4 --- daemon/tests/test_core.py | 31 ++--------------- 3 files changed, 2 insertions(+), 85 deletions(-) diff --git a/daemon/core/nodes/client.py b/daemon/core/nodes/client.py index 13072c05..81297cf5 100644 --- a/daemon/core/nodes/client.py +++ b/daemon/core/nodes/client.py @@ -4,8 +4,6 @@ over a control channel to the vnoded process running in a network namespace. The control channel can be accessed via calls using the vcmd shell. """ -import os - from core import constants, utils @@ -69,56 +67,6 @@ class VnodeClient(object): args = self._cmd_args() + args return utils.check_cmd(args, wait=wait) - def icmd(self, args): - """ - Execute an icmd against a node. - - :param list[str]|str args: command arguments - :return: command result - :rtype: int - """ - args = utils.split_args(args) - return os.spawnlp( - os.P_WAIT, - constants.VCMD_BIN, - constants.VCMD_BIN, - "-c", - self.ctrlchnlname, - "--", - *args - ) - - def term(self, sh="/bin/sh"): - """ - Open a terminal on a node. - - :param str sh: shell to open terminal with - :return: terminal command result - :rtype: int - """ - args = ( - "xterm", - "-ut", - "-title", - self.name, - "-e", - constants.VCMD_BIN, - "-c", - self.ctrlchnlname, - "--", - sh, - ) - if "SUDO_USER" in os.environ: - args = ( - "su", - "-s", - "/bin/sh", - "-c", - "exec " + " ".join(map(lambda x: "'%s'" % x, args)), - os.environ["SUDO_USER"], - ) - return os.spawnvp(os.P_NOWAIT, args[0], args) - def termcmdstring(self, sh="/bin/sh"): """ Create a terminal command string. diff --git a/daemon/examples/python/emane80211.py b/daemon/examples/python/emane80211.py index 0e42be95..adf6959b 100644 --- a/daemon/examples/python/emane80211.py +++ b/daemon/examples/python/emane80211.py @@ -40,10 +40,6 @@ def example(options): # instantiate session session.instantiate() - # start a shell on the first node - node = session.get_node(2) - node.client.term("bash") - # shutdown session input("press enter to exit...") coreemu.shutdown() diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 4360ba06..392794d0 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -3,7 +3,6 @@ Unit tests for testing basic CORE networks. """ import os -import stat import threading import pytest @@ -12,31 +11,12 @@ from core.emulator.emudata import NodeOptions from core.emulator.enumerations import MessageFlags, NodeTypes from core.errors import CoreCommandError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility -from core.nodes.client import VnodeClient _PATH = os.path.abspath(os.path.dirname(__file__)) _MOBILITY_FILE = os.path.join(_PATH, "mobility.scen") _WIRED = [NodeTypes.PEER_TO_PEER, NodeTypes.HUB, NodeTypes.SWITCH] -def createclients(sessiondir, clientcls=VnodeClient, cmdchnlfilterfunc=None): - """ - Create clients - - :param str sessiondir: session directory to create clients - :param class clientcls: class to create clients from - :param func cmdchnlfilterfunc: command channel filter function - :return: list of created clients - :rtype: list - """ - direntries = map(lambda x: os.path.join(sessiondir, x), os.listdir(sessiondir)) - cmdchnls = list(filter(lambda x: stat.S_ISSOCK(os.stat(x).st_mode), direntries)) - if cmdchnlfilterfunc: - cmdchnls = list(filter(cmdchnlfilterfunc, cmdchnls)) - cmdchnls.sort() - return map(lambda x: clientcls(os.path.basename(x), x), cmdchnls) - - def ping(from_node, to_node, ip_prefixes): address = ip_prefixes.ip4_address(to_node) try: @@ -106,15 +86,8 @@ class TestCore: # check we are connected assert client.connected() - # check various command using vcmd module - command = ["ls"] - assert not client.icmd(command) - - # check various command using command line - assert not client.icmd(command) - - # check module methods - assert createclients(session.session_dir) + # validate command + assert client.check_cmd("echo hello") == "hello" def test_netif(self, session, ip_prefixes): """ From 02ef91242eb3c9b33354021bfed01983854e4889 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 11 Oct 2019 16:36:57 -0700 Subject: [PATCH 051/462] initial changes to convert all commands to be string based for consistency --- daemon/core/api/grpc/server.py | 4 +- daemon/core/api/tlv/corehandlers.py | 1 - daemon/core/emane/emanemanager.py | 41 ++++----- daemon/core/emane/tdma.py | 3 +- daemon/core/emulator/session.py | 1 - daemon/core/location/mobility.py | 2 +- daemon/core/nodes/base.py | 60 ++++++------- daemon/core/nodes/client.py | 10 +-- daemon/core/nodes/docker.py | 1 - daemon/core/nodes/interface.py | 3 +- daemon/core/nodes/ipaddress.py | 6 +- daemon/core/nodes/lxd.py | 20 ++--- daemon/core/nodes/netclient.py | 86 +++++++++---------- daemon/core/nodes/network.py | 123 ++++++++------------------- daemon/core/services/coreservices.py | 3 - daemon/core/services/utility.py | 2 +- daemon/core/utils.py | 22 +---- daemon/core/xml/corexmldeployment.py | 5 +- daemon/tests/emane/test_emane.py | 2 +- daemon/tests/test_core.py | 2 +- daemon/tests/test_nodes.py | 4 +- 21 files changed, 145 insertions(+), 256 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 3d589fab..84d019e3 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -10,7 +10,6 @@ from queue import Empty, Queue import grpc -from core import utils from core.api.grpc import core_pb2, core_pb2_grpc from core.emane.nodes import EmaneNet from core.emulator.data import ( @@ -884,8 +883,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session = self.get_session(request.session_id, context) node = self.get_node(session, request.node_id, context) try: - args = utils.split_args(request.command) - output = node.node_net_cmd(args) + output = node.node_net_cmd(request.command) except CoreCommandError as e: output = e.stderr return core_pb2.NodeCommandResponse(output=output) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index e1a32e5f..444ae5a5 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -882,7 +882,6 @@ class CoreHandler(socketserver.BaseRequestHandler): return (reply,) else: logging.info("execute message with cmd=%s", command) - command = utils.split_args(command) # execute command and send a response if ( message.flags & MessageFlags.STRING.value diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 90ca9dfe..65fed8bd 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -152,10 +152,9 @@ class EmaneManager(ModelManager): """ try: # check for emane - args = ["emane", "--version"] + args = "emane --version" emane_version = utils.check_cmd(args) logging.info("using EMANE: %s", emane_version) - args = " ".join(args) for server in self.session.servers: conn = self.session.servers[server] distributed.remote_cmd(conn, args) @@ -652,14 +651,6 @@ class EmaneManager(ModelManager): emane_net = self._emane_nets[key] emanexml.build_xml_files(self, emane_net) - def buildtransportxml(self): - """ - Calls emanegentransportxml using a platform.xml file to build the transportdaemon*.xml. - """ - utils.check_cmd( - ["emanegentransportxml", "platform.xml"], cwd=self.session.session_dir - ) - def buildeventservicexml(self): """ Build the libemaneeventservice.xml file if event service options @@ -705,9 +696,9 @@ class EmaneManager(ModelManager): logging.info("setting user-defined EMANE log level: %d", cfgloglevel) loglevel = str(cfgloglevel) - emanecmd = ["emane", "-d", "-l", loglevel] + emanecmd = "emane -d -l %s" % loglevel if realtime: - emanecmd += ("-r",) + emanecmd += " -r" otagroup, _otaport = self.get_config("otamanagergroup").split(":") otadev = self.get_config("otamanagerdevice") @@ -750,11 +741,11 @@ class EmaneManager(ModelManager): node.node_net_client.create_route(eventgroup, eventdev) # start emane - args = emanecmd + [ - "-f", + args = "%s -f %s %s" % ( + emanecmd, os.path.join(path, "emane%d.log" % n), os.path.join(path, "platform%d.xml" % n), - ] + ) output = node.node_net_cmd(args) logging.info("node(%s) emane daemon running: %s", node.name, args) logging.info("node(%s) emane daemon output: %s", node.name, output) @@ -763,22 +754,22 @@ class EmaneManager(ModelManager): return path = self.session.session_dir - emanecmd += ["-f", os.path.join(path, "emane.log")] - args = emanecmd + [os.path.join(path, "platform.xml")] - utils.check_cmd(args, cwd=path) - args = " ".join(args) + emanecmd += " -f %s" % os.path.join(path, "emane.log") + emanecmd += " %s" % os.path.join(path, "platform.xml") + utils.check_cmd(emanecmd, cwd=path) for server in self.session.servers: conn = self.session.servers[server] - distributed.remote_cmd(conn, args, cwd=path) - logging.info("host emane daemon running: %s", args) + distributed.remote_cmd(conn, emanecmd, cwd=path) + logging.info("host emane daemon running: %s", emanecmd) def stopdaemons(self): """ Kill the appropriate EMANE daemons. """ - # TODO: we may want to improve this if we had the PIDs from the specific EMANE daemons that we"ve started - kill_emaned = ["killall", "-q", "emane"] - kill_transortd = ["killall", "-q", "emanetransportd"] + # TODO: we may want to improve this if we had the PIDs from the specific EMANE + # daemons that we"ve started + kill_emaned = "killall -q emane" + kill_transortd = "killall -q emanetransportd" stop_emane_on_host = False for node in self.getnodes(): if hasattr(node, "transport_type") and node.transport_type == "raw": @@ -793,8 +784,6 @@ class EmaneManager(ModelManager): try: utils.check_cmd(kill_emaned) utils.check_cmd(kill_transortd) - kill_emaned = " ".join(kill_emaned) - kill_transortd = " ".join(kill_transortd) for server in self.session.servers: conn = self.session[server] distributed.remote_cmd(conn, kill_emaned) diff --git a/daemon/core/emane/tdma.py b/daemon/core/emane/tdma.py index dfd36b5d..249e81b6 100644 --- a/daemon/core/emane/tdma.py +++ b/daemon/core/emane/tdma.py @@ -62,4 +62,5 @@ class EmaneTdmaModel(emanemodel.EmaneModel): logging.info( "setting up tdma schedule: schedule(%s) device(%s)", schedule, event_device ) - utils.check_cmd(["emaneevent-tdmaschedule", "-i", event_device, schedule]) + args = "emaneevent-tdmaschedule -i %s %s" % (event_device, schedule) + utils.check_cmd(args) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index e3a2389b..7c4cd651 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -2044,5 +2044,4 @@ class Session(object): utils.mute_detach(data) else: node = self.get_node(node_id) - data = utils.split_args(data) node.node_net_cmd(data, wait=False) diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index f6ce60ca..3522a3f7 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -1281,7 +1281,7 @@ class Ns2ScriptedMobility(WayPointMobility): if filename is None or filename == "": return filename = self.findfile(filename) - args = ["/bin/sh", filename, typestr] + args = "/bin/sh %s %s" % (filename, typestr) utils.check_cmd( args, cwd=self.session.session_dir, env=self.session.get_environment() ) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 120c53ca..85e25074 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -12,7 +12,8 @@ import threading from builtins import range from socket import AF_INET, AF_INET6 -from core import constants, utils +from core import utils +from core.constants import MOUNT_BIN, VNODED_BIN from core.emulator import distributed from core.emulator.data import LinkData, NodeData from core.emulator.enumerations import LinkTypes, NodeTypes @@ -89,7 +90,7 @@ class NodeBase(object): Runs a command that is used to configure and setup the network on the host system. - :param list[str]|str args: command to run + :param str args: command to run :param dict env: environment to run command with :param str cwd: directory to run command in :param bool wait: True to wait for status, False otherwise @@ -100,7 +101,6 @@ class NodeBase(object): if self.server is None: return utils.check_cmd(args, env, cwd, wait) else: - args = " ".join(args) return distributed.remote_cmd(self.server, args, env, cwd, wait) def setposition(self, x=None, y=None, z=None): @@ -269,7 +269,7 @@ class CoreNodeBase(NodeBase): """ if self.nodedir is None: self.nodedir = os.path.join(self.session.session_dir, self.name + ".conf") - self.net_cmd(["mkdir", "-p", self.nodedir]) + self.net_cmd("mkdir -p %s" % self.nodedir) self.tmpnodedir = True else: self.tmpnodedir = False @@ -285,7 +285,7 @@ class CoreNodeBase(NodeBase): return if self.tmpnodedir: - self.net_cmd(["rm", "-rf", self.nodedir]) + self.net_cmd("rm -rf %s" % self.nodedir) def addnetif(self, netif, ifindex): """ @@ -334,7 +334,7 @@ class CoreNodeBase(NodeBase): Attach a network. :param int ifindex: interface of index to attach - :param core.nodes.interface.CoreInterface net: network to attach + :param core.nodes.base.CoreNetworkBase net: network to attach :return: nothing """ if ifindex not in self._netif: @@ -392,7 +392,7 @@ class CoreNodeBase(NodeBase): Runs a command that is used to configure and setup the network within a node. - :param list[str]|str args: command to run + :param str args: command to run :param bool wait: True to wait for status, False otherwise :return: combined stdout and stderr :rtype: str @@ -468,7 +468,7 @@ class CoreNode(CoreNodeBase): :rtype: bool """ try: - self.net_cmd(["kill", "-0", str(self.pid)]) + self.net_cmd("kill -0 %s" % self.pid) except CoreCommandError: return False @@ -488,18 +488,11 @@ class CoreNode(CoreNodeBase): raise ValueError("starting a node that is already up") # create a new namespace for this node using vnoded - vnoded = [ - constants.VNODED_BIN, - "-v", - "-c", - self.ctrlchnlname, - "-l", - self.ctrlchnlname + ".log", - "-p", - self.ctrlchnlname + ".pid", - ] + vnoded = "{cmd} -v -c {name} -l {name}.log -p {name}.pid".format( + cmd=VNODED_BIN, name=self.ctrlchnlname + ) if self.nodedir: - vnoded += ["-C", self.nodedir] + vnoded += " -C %s" % self.nodedir env = self.session.get_environment(state=False) env["NODE_NUMBER"] = str(self.id) env["NODE_NAME"] = str(self.name) @@ -548,13 +541,13 @@ class CoreNode(CoreNodeBase): # kill node process if present try: - self.net_cmd(["kill", "-9", str(self.pid)]) + self.net_cmd("kill -9 %s" % self.pid) except CoreCommandError: logging.exception("error killing process") # remove node directory if present try: - self.net_cmd(["rm", "-rf", self.ctrlchnlname]) + self.net_cmd("rm -rf %s" % self.ctrlchnlname) except CoreCommandError: logging.exception("error removing node directory") @@ -572,7 +565,7 @@ class CoreNode(CoreNodeBase): Runs a command that is used to configure and setup the network within a node. - :param list[str] args: command to run + :param str args: command to run :param bool wait: True to wait for status, False otherwise :return: combined stdout and stderr :rtype: str @@ -581,8 +574,7 @@ class CoreNode(CoreNodeBase): if self.server is None: return self.client.check_cmd(args, wait=wait) else: - args = self.client._cmd_args() + args - args = " ".join(args) + args = self.client.create_cmd(args) return distributed.remote_cmd(self.server, args, wait=wait) def termcmdstring(self, sh="/bin/sh"): @@ -606,7 +598,7 @@ class CoreNode(CoreNodeBase): hostpath = os.path.join( self.nodedir, os.path.normpath(path).strip("/").replace("/", ".") ) - self.net_cmd(["mkdir", "-p", hostpath]) + self.net_cmd("mkdir -p %s" % hostpath) self.mount(hostpath, path) def mount(self, source, target): @@ -620,8 +612,8 @@ class CoreNode(CoreNodeBase): """ source = os.path.abspath(source) logging.debug("node(%s) mounting: %s at %s", self.name, source, target) - self.node_net_cmd(["mkdir", "-p", target]) - self.node_net_cmd([constants.MOUNT_BIN, "-n", "--bind", source, target]) + self.node_net_cmd("mkdir -p %s" % target) + self.node_net_cmd("%s -n --bind %s %s" % (MOUNT_BIN, source, target)) self._mounts.append((source, target)) def newifindex(self): @@ -881,11 +873,11 @@ class CoreNode(CoreNodeBase): logging.info("adding file from %s to %s", srcname, filename) directory = os.path.dirname(filename) if self.server is None: - self.client.check_cmd(["mkdir", "-p", directory]) - self.client.check_cmd(["mv", srcname, filename]) - self.client.check_cmd(["sync"]) + self.client.check_cmd("mkdir -p %s" % directory) + self.client.check_cmd("mv %s %s" % (srcname, filename)) + self.client.check_cmd("sync") else: - self.net_cmd(["mkdir", "-p", directory]) + self.net_cmd("mkdir -p %s" % directory) distributed.remote_put(self.server, srcname, filename) def hostfilename(self, filename): @@ -922,9 +914,9 @@ class CoreNode(CoreNodeBase): open_file.write(contents) os.chmod(open_file.name, mode) else: - self.net_cmd(["mkdir", "-m", "%o" % 0o755, "-p", dirname]) + self.net_cmd("mkdir -m %o -p %s" % (0o755, dirname)) distributed.remote_put_temp(self.server, hostfilename, contents) - self.net_cmd(["chmod", "%o" % mode, hostfilename]) + self.net_cmd("chmod %o %s" % (mode, hostfilename)) logging.debug( "node(%s) added file: %s; mode: 0%o", self.name, hostfilename, mode ) @@ -947,7 +939,7 @@ class CoreNode(CoreNodeBase): else: distributed.remote_put(self.server, srcfilename, hostfilename) if mode is not None: - self.net_cmd(["chmod", "%o" % mode, hostfilename]) + self.net_cmd("chmod %o %s" % (mode, hostfilename)) logging.info( "node(%s) copied file: %s; mode: %s", self.name, hostfilename, mode ) diff --git a/daemon/core/nodes/client.py b/daemon/core/nodes/client.py index 81297cf5..e09c72fa 100644 --- a/daemon/core/nodes/client.py +++ b/daemon/core/nodes/client.py @@ -5,6 +5,7 @@ The control channel can be accessed via calls using the vcmd shell. """ from core import constants, utils +from core.constants import VCMD_BIN class VnodeClient(object): @@ -49,22 +50,21 @@ class VnodeClient(object): """ pass - def _cmd_args(self): - return [constants.VCMD_BIN, "-c", self.ctrlchnlname, "--"] + def create_cmd(self, args): + return "%s -c %s -- %s" % (VCMD_BIN, self.ctrlchnlname, args) def check_cmd(self, args, wait=True): """ Run command and return exit status and combined stdout and stderr. - :param list[str]|str args: command to run + :param str args: command to run :param bool wait: True to wait for command status, False otherwise :return: combined stdout and stderr :rtype: str :raises core.CoreCommandError: when there is a non-zero exit status """ self._verify_connection() - args = utils.split_args(args) - args = self._cmd_args() + args + args = self.create_cmd(args) return utils.check_cmd(args, wait=wait) def termcmdstring(self, sh="/bin/sh"): diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index 1b8322ae..416b31d1 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -63,7 +63,6 @@ class DockerClient(object): pid=self.pid, cmd=cmd ) - logging.info("ns cmd: %s", args) return utils.check_cmd(args, wait=wait) def get_pid(self): diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index d749bbde..4e834fd3 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -52,7 +52,7 @@ class CoreInterface(object): """ Runs a command on the host system or distributed servers. - :param list[str]|str args: command to run + :param str args: command to run :param dict env: environment to run command with :param str cwd: directory to run command in :param bool wait: True to wait for status, False otherwise @@ -63,7 +63,6 @@ class CoreInterface(object): if self.server is None: return utils.check_cmd(args, env, cwd, wait) else: - args = " ".join(args) return distributed.remote_cmd(self.server, args, env, cwd, wait) def startup(self): diff --git a/daemon/core/nodes/ipaddress.py b/daemon/core/nodes/ipaddress.py index 00aed74c..c7860dbc 100644 --- a/daemon/core/nodes/ipaddress.py +++ b/daemon/core/nodes/ipaddress.py @@ -19,7 +19,7 @@ class MacAddress(object): """ Creates a MacAddress instance. - :param str address: mac address + :param bytes address: mac address """ self.addr = address @@ -42,7 +42,7 @@ class MacAddress(object): """ if not self.addr: return IpAddress.from_string("::") - tmp = struct.unpack("!Q", "\x00\x00" + self.addr)[0] + tmp = struct.unpack("!Q", b"\x00\x00" + self.addr)[0] nic = int(tmp) & 0x000000FFFFFF oui = int(tmp) & 0xFFFFFF000000 # toggle U/L bit @@ -88,7 +88,7 @@ class IpAddress(object): Create a IpAddress instance. :param int af: address family - :param str address: ip address + :param bytes address: ip address :return: """ # check if (af, addr) is valid diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index df01f4ad..96b107f8 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -43,25 +43,19 @@ class LxdClient(object): def stop_container(self): utils.check_cmd("lxc delete --force {name}".format(name=self.name)) - def _cmd_args(self, cmd): + def create_cmd(self, cmd): return "lxc exec -nT {name} -- {cmd}".format(name=self.name, cmd=cmd) - def check_cmd(self, cmd, wait): - if isinstance(cmd, list): - cmd = " ".join(cmd) - args = self._cmd_args(cmd) - logging.info("lxc cmd output: %s", args) + def check_cmd(self, cmd, wait=True): + args = self.create_cmd(cmd) return utils.check_cmd(args, wait=wait) - def _ns_args(self, cmd): + def create_ns_cmd(self, cmd): return "nsenter -t {pid} -m -u -i -p -n {cmd}".format(pid=self.pid, cmd=cmd) - def ns_check_cmd(self, cmd): - if isinstance(cmd, list): - cmd = " ".join(cmd) - args = self._ns_args(cmd) - logging.info("ns cmd: %s", args) - return utils.check_cmd(args) + def ns_check_cmd(self, cmd, wait=True): + args = self.create_ns_cmd(cmd) + return utils.check_cmd(args, wait=wait) def copy_file(self, source, destination): if destination[0] != "/": diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index a689dde9..930e13b4 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -27,7 +27,7 @@ class LinuxNetClient(object): :param str name: name for hostname :return: nothing """ - self.run(["hostname", name]) + self.run("hostname %s" % name) def create_route(self, route, device): """ @@ -37,7 +37,7 @@ class LinuxNetClient(object): :param str device: device to add route to :return: nothing """ - self.run([IP_BIN, "route", "add", route, "dev", device]) + self.run("%s route add %s dev %s" % (IP_BIN, route, device)) def device_up(self, device): """ @@ -46,7 +46,7 @@ class LinuxNetClient(object): :param str device: device to bring up :return: nothing """ - self.run([IP_BIN, "link", "set", device, "up"]) + self.run("%s link set %s up" % (IP_BIN, device)) def device_down(self, device): """ @@ -55,7 +55,7 @@ class LinuxNetClient(object): :param str device: device to bring down :return: nothing """ - self.run([IP_BIN, "link", "set", device, "down"]) + self.run("%s link set %s down" % (IP_BIN, device)) def device_name(self, device, name): """ @@ -65,7 +65,7 @@ class LinuxNetClient(object): :param str name: name to set :return: nothing """ - self.run([IP_BIN, "link", "set", device, "name", name]) + self.run("%s link set %s name %s" % (IP_BIN, device, name)) def device_show(self, device): """ @@ -75,7 +75,7 @@ class LinuxNetClient(object): :return: device information :rtype: str """ - return self.run([IP_BIN, "link", "show", device]) + return self.run("%s link show %s" % (IP_BIN, device)) def device_ns(self, device, namespace): """ @@ -85,7 +85,7 @@ class LinuxNetClient(object): :param str namespace: namespace to set device to :return: nothing """ - self.run([IP_BIN, "link", "set", device, "netns", namespace]) + self.run("%s link set %s netns %s" % (IP_BIN, device, namespace)) def device_flush(self, device): """ @@ -94,7 +94,7 @@ class LinuxNetClient(object): :param str device: device to flush :return: nothing """ - self.run([IP_BIN, "-6", "address", "flush", "dev", device]) + self.run("%s -6 address flush dev %s" % (IP_BIN, device)) def device_mac(self, device, mac): """ @@ -104,7 +104,7 @@ class LinuxNetClient(object): :param str mac: mac to set :return: nothing """ - self.run([IP_BIN, "link", "set", "dev", device, "address", mac]) + self.run("%s link set dev %s address %s" % (IP_BIN, device, mac)) def delete_device(self, device): """ @@ -113,7 +113,7 @@ class LinuxNetClient(object): :param str device: device to delete :return: nothing """ - self.run([IP_BIN, "link", "delete", device]) + self.run("%s link delete %s" % (IP_BIN, device)) def delete_tc(self, device): """ @@ -122,7 +122,7 @@ class LinuxNetClient(object): :param str device: device to remove tc :return: nothing """ - self.run([TC_BIN, "qdisc", "del", "dev", device, "root"]) + self.run("%s qdisc del dev %s root" % (TC_BIN, device)) def checksums_off(self, interface_name): """ @@ -131,7 +131,7 @@ class LinuxNetClient(object): :param str interface_name: interface to update :return: nothing """ - self.run([ETHTOOL_BIN, "-K", interface_name, "rx", "off", "tx", "off"]) + self.run("%s -K %s rx off tx off" % (ETHTOOL_BIN, interface_name)) def create_address(self, device, address, broadcast=None): """ @@ -144,19 +144,11 @@ class LinuxNetClient(object): """ if broadcast is not None: self.run( - [ - IP_BIN, - "address", - "add", - address, - "broadcast", - broadcast, - "dev", - device, - ] + "%s address add %s broadcast %s dev %s" + % (IP_BIN, address, broadcast, device) ) else: - self.run([IP_BIN, "address", "add", address, "dev", device]) + self.run("%s address add %s dev %s" % (IP_BIN, address, device)) def delete_address(self, device, address): """ @@ -166,7 +158,7 @@ class LinuxNetClient(object): :param str address: address to remove :return: nothing """ - self.run([IP_BIN, "address", "delete", address, "dev", device]) + self.run("%s address delete %s dev %s" % (IP_BIN, address, device)) def create_veth(self, name, peer): """ @@ -176,9 +168,7 @@ class LinuxNetClient(object): :param str peer: peer name :return: nothing """ - self.run( - [IP_BIN, "link", "add", "name", name, "type", "veth", "peer", "name", peer] - ) + self.run("%s link add name %s type veth peer name %s" % (IP_BIN, name, peer)) def create_gretap(self, device, address, local, ttl, key): """ @@ -191,13 +181,13 @@ class LinuxNetClient(object): :param str key: key for tap :return: nothing """ - cmd = [IP_BIN, "link", "add", device, "type", "gretap", "remote", address] + cmd = "%s link add %s type gretap remote %s" % (IP_BIN, device, address) if local is not None: - cmd.extend(["local", local]) + cmd += " local %s" % local if ttl is not None: - cmd.extend(["ttl", ttl]) + cmd += " ttl %s" % ttl if key is not None: - cmd.extend(["key", key]) + cmd += " key %s" % key self.run(cmd) def create_bridge(self, name): @@ -207,9 +197,9 @@ class LinuxNetClient(object): :param str name: bridge name :return: nothing """ - self.run([BRCTL_BIN, "addbr", name]) - self.run([BRCTL_BIN, "stp", name, "off"]) - self.run([BRCTL_BIN, "setfd", name, "0"]) + self.run("%s addbr %s" % (BRCTL_BIN, name)) + self.run("%s stp %s off" % (BRCTL_BIN, name)) + self.run("%s setfd %s 0" % (BRCTL_BIN, name)) self.device_up(name) # turn off multicast snooping so forwarding occurs w/o IGMP joins @@ -226,7 +216,7 @@ class LinuxNetClient(object): :return: nothing """ self.device_down(name) - self.run([BRCTL_BIN, "delbr", name]) + self.run("%s delbr %s" % (BRCTL_BIN, name)) def create_interface(self, bridge_name, interface_name): """ @@ -236,7 +226,7 @@ class LinuxNetClient(object): :param str interface_name: interface name :return: nothing """ - self.run([BRCTL_BIN, "addif", bridge_name, interface_name]) + self.run("%s addif %s %s" % (BRCTL_BIN, bridge_name, interface_name)) self.device_up(interface_name) def delete_interface(self, bridge_name, interface_name): @@ -247,7 +237,7 @@ class LinuxNetClient(object): :param str interface_name: interface name :return: nothing """ - self.run([BRCTL_BIN, "delif", bridge_name, interface_name]) + self.run("%s delif %s %s" % (BRCTL_BIN, bridge_name, interface_name)) def existing_bridges(self, _id): """ @@ -255,7 +245,7 @@ class LinuxNetClient(object): :param _id: node id to check bridges for """ - output = self.run([BRCTL_BIN, "show"]) + output = self.run("%s show" % BRCTL_BIN) lines = output.split("\n") for line in lines[1:]: columns = line.split() @@ -274,7 +264,7 @@ class LinuxNetClient(object): :param str name: bridge name :return: nothing """ - self.run([BRCTL_BIN, "setageing", name, "0"]) + self.run("%s setageing %s 0" % (BRCTL_BIN, name)) class OvsNetClient(LinuxNetClient): @@ -289,10 +279,10 @@ class OvsNetClient(LinuxNetClient): :param str name: bridge name :return: nothing """ - self.run([OVS_BIN, "add-br", name]) - self.run([OVS_BIN, "set", "bridge", name, "stp_enable=false"]) - self.run([OVS_BIN, "set", "bridge", name, "other_config:stp-max-age=6"]) - self.run([OVS_BIN, "set", "bridge", name, "other_config:stp-forward-delay=4"]) + self.run("%s add-br %s" % (OVS_BIN, name)) + self.run("%s set bridge %s stp_enable=false" % (OVS_BIN, name)) + self.run("%s set bridge %s other_config:stp-max-age=6" % (OVS_BIN, name)) + self.run("%s set bridge %s other_config:stp-forward-delay=4" % (OVS_BIN, name)) self.device_up(name) def delete_bridge(self, name): @@ -303,7 +293,7 @@ class OvsNetClient(LinuxNetClient): :return: nothing """ self.device_down(name) - self.run([OVS_BIN, "del-br", name]) + self.run("%s del-br %s" % (OVS_BIN, name)) def create_interface(self, bridge_name, interface_name): """ @@ -313,7 +303,7 @@ class OvsNetClient(LinuxNetClient): :param str interface_name: interface name :return: nothing """ - self.run([OVS_BIN, "add-port", bridge_name, interface_name]) + self.run("%s add-port %s %s" % (OVS_BIN, bridge_name, interface_name)) self.device_up(interface_name) def delete_interface(self, bridge_name, interface_name): @@ -324,7 +314,7 @@ class OvsNetClient(LinuxNetClient): :param str interface_name: interface name :return: nothing """ - self.run([OVS_BIN, "del-port", bridge_name, interface_name]) + self.run("%s del-port %s %s" % (OVS_BIN, bridge_name, interface_name)) def existing_bridges(self, _id): """ @@ -332,7 +322,7 @@ class OvsNetClient(LinuxNetClient): :param _id: node id to check bridges for """ - output = self.run([OVS_BIN, "list-br"]) + output = self.run("%s list-br" % OVS_BIN) if output: for line in output.split("\n"): fields = line.split(".") @@ -347,4 +337,4 @@ class OvsNetClient(LinuxNetClient): :param str name: bridge name :return: nothing """ - self.run([OVS_BIN, "set", "bridge", name, "other_config:mac-aging-time=0"]) + self.run("%s set bridge %s other_config:mac-aging-time=0" % (OVS_BIN, name)) diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 7197c77f..e0fcd839 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -9,6 +9,7 @@ import time from socket import AF_INET, AF_INET6 from core import constants, utils +from core.constants import EBTABLES_BIN from core.emulator import distributed from core.emulator.data import LinkData from core.emulator.enumerations import LinkTypes, NodeTypes, RegisterTlvs @@ -92,14 +93,11 @@ class EbtablesQueue(object): """ Helper for building ebtables atomic file command list. - :param list[str] cmd: ebtable command + :param str cmd: ebtable command :return: ebtable atomic command :rtype: list[str] """ - r = [constants.EBTABLES_BIN, "--atomic-file", self.atomic_file] - if cmd: - r.extend(cmd) - return r + return "%s --atomic-file %s %s" % (EBTABLES_BIN, self.atomic_file, cmd) def lastupdate(self, wlan): """ @@ -163,7 +161,7 @@ class EbtablesQueue(object): :return: nothing """ # save kernel ebtables snapshot to a file - args = self.ebatomiccmd(["--atomic-save"]) + args = self.ebatomiccmd("--atomic-save") wlan.net_cmd(args) # modify the table file using queued ebtables commands @@ -173,12 +171,12 @@ class EbtablesQueue(object): self.cmds = [] # commit the table file to the kernel - args = self.ebatomiccmd(["--atomic-commit"]) + args = self.ebatomiccmd("--atomic-commit") wlan.net_cmd(args) try: - wlan.net_cmd(["rm", "-f", self.atomic_file]) - except OSError: + wlan.net_cmd("rm -f %s" % self.atomic_file) + except CoreCommandError: logging.exception("error removing atomic file: %s", self.atomic_file) def ebchange(self, wlan): @@ -200,58 +198,26 @@ class EbtablesQueue(object): """ with wlan._linked_lock: # flush the chain - self.cmds.extend([["-F", wlan.brname]]) + self.cmds.append("-F %s" % wlan.brname) # rebuild the chain for netif1, v in wlan._linked.items(): for netif2, linked in v.items(): if wlan.policy == "DROP" and linked: self.cmds.extend( [ - [ - "-A", - wlan.brname, - "-i", - netif1.localname, - "-o", - netif2.localname, - "-j", - "ACCEPT", - ], - [ - "-A", - wlan.brname, - "-o", - netif1.localname, - "-i", - netif2.localname, - "-j", - "ACCEPT", - ], + "-A %s -i %s -o %s -j ACCEPT" + % (wlan.brname, netif1.localname, netif2.localname), + "-A %s -o %s -i %s -j ACCEPT" + % (wlan.brname, netif1.localname, netif2.localname), ] ) elif wlan.policy == "ACCEPT" and not linked: self.cmds.extend( [ - [ - "-A", - wlan.brname, - "-i", - netif1.localname, - "-o", - netif2.localname, - "-j", - "DROP", - ], - [ - "-A", - wlan.brname, - "-o", - netif1.localname, - "-i", - netif2.localname, - "-j", - "DROP", - ], + "-A %s -i %s -o %s -j DROP" + % (wlan.brname, netif1.localname, netif2.localname), + "-A %s -o %s -i %s -j DROP" + % (wlan.brname, netif1.localname, netif2.localname), ] ) @@ -313,7 +279,7 @@ class CoreNetwork(CoreNetworkBase): Runs a command that is used to configure and setup the network on the host system and all configured distributed servers. - :param list[str]|str args: command to run + :param str args: command to run :param dict env: environment to run command with :param str cwd: directory to run command in :param bool wait: True to wait for status, False otherwise @@ -323,12 +289,9 @@ class CoreNetwork(CoreNetworkBase): """ logging.info("network node(%s) cmd", self.name) output = utils.check_cmd(args, env, cwd, wait) - - args = " ".join(args) for server in self.session.servers: conn = self.session.servers[server] distributed.remote_cmd(conn, args, env, cwd, wait) - return output def startup(self): @@ -341,21 +304,12 @@ class CoreNetwork(CoreNetworkBase): self.net_client.create_bridge(self.brname) # create a new ebtables chain for this bridge - ebtablescmds( - self.net_cmd, - [ - [constants.EBTABLES_BIN, "-N", self.brname, "-P", self.policy], - [ - constants.EBTABLES_BIN, - "-A", - "FORWARD", - "--logical-in", - self.brname, - "-j", - self.brname, - ], - ], - ) + cmds = [ + "%s -N %s -P %s" % (EBTABLES_BIN, self.brname, self.policy), + "%s -A FORWARD --logical-in %s -j %s" + % (EBTABLES_BIN, self.brname, self.brname), + ] + ebtablescmds(self.net_cmd, cmds) self.up = True @@ -372,21 +326,12 @@ class CoreNetwork(CoreNetworkBase): try: self.net_client.delete_bridge(self.brname) - ebtablescmds( - self.net_cmd, - [ - [ - constants.EBTABLES_BIN, - "-D", - "FORWARD", - "--logical-in", - self.brname, - "-j", - self.brname, - ], - [constants.EBTABLES_BIN, "-X", self.brname], - ], - ) + cmds = [ + "%s -D FORWARD --logical-in %s -j %s" + % (EBTABLES_BIN, self.brname, self.brname), + "%s -X %s" % (EBTABLES_BIN, self.brname), + ] + ebtablescmds(self.net_cmd, cmds) except CoreCommandError: logging.exception("error during shutdown") @@ -852,7 +797,7 @@ class CtrlNet(CoreNetwork): self.brname, self.updown_script, ) - self.net_cmd([self.updown_script, self.brname, "startup"]) + self.net_cmd("%s %s startup" % (self.updown_script, self.brname)) if self.serverintf: self.net_client.create_interface(self.brname, self.serverintf) @@ -880,7 +825,7 @@ class CtrlNet(CoreNetwork): self.brname, self.updown_script, ) - self.net_cmd([self.updown_script, self.brname, "shutdown"]) + self.net_cmd("%s %s shutdown" % (self.updown_script, self.brname)) except CoreCommandError: logging.exception("error issuing shutdown script shutdown") @@ -1064,7 +1009,8 @@ class HubNode(CoreNetwork): :param int _id: node id :param str name: node namee :param bool start: start flag - :param str server: remote server node will run on, default is None for localhost + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost :raises CoreCommandError: when there is a command exception """ CoreNetwork.__init__(self, session, _id, name, start, server) @@ -1094,7 +1040,8 @@ class WlanNode(CoreNetwork): :param int _id: node id :param str name: node name :param bool start: start flag - :param str server: remote server node will run on, default is None for localhost + :param fabric.connection.Connection server: remote server node will run on, + default is None for localhost :param policy: wlan policy """ CoreNetwork.__init__(self, session, _id, name, start, server, policy) diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index dc45fa33..6db2d8ed 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -597,7 +597,6 @@ class CoreServices(object): status = 0 for cmd in cmds: logging.debug("validating service(%s) using: %s", service.name, cmd) - cmd = utils.split_args(cmd) try: node.node_net_cmd(cmd) except CoreCommandError as e: @@ -631,7 +630,6 @@ class CoreServices(object): """ status = 0 for args in service.shutdown: - args = utils.split_args(args) try: node.node_net_cmd(args) except CoreCommandError: @@ -730,7 +728,6 @@ class CoreServices(object): status = 0 for cmd in cmds: - cmd = utils.split_args(cmd) try: node.node_net_cmd(cmd, wait) except CoreCommandError: diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index 66a84dd6..14bd5a90 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -415,7 +415,7 @@ class HttpService(UtilService): Detect the apache2 version using the 'a2query' command. """ try: - result = utils.check_cmd(["a2query", "-v"]) + result = utils.check_cmd("a2query -v") status = 0 except CoreCommandError as e: status = e.returncode diff --git a/daemon/core/utils.py b/daemon/core/utils.py index 003c7134..8f8da19c 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -14,8 +14,6 @@ import shlex import sys from subprocess import PIPE, STDOUT, Popen -from past.builtins import basestring - from core.errors import CoreCommandError DEVNULL = open(os.devnull, "wb") @@ -177,20 +175,6 @@ def make_tuple_fromstr(s, value_type): return tuple(value_type(i) for i in values) -def split_args(args): - """ - Convenience method for splitting potential string commands into a shell-like - syntax list. - - :param list/str args: command list or string - :return: shell-like syntax list - :rtype: list - """ - if isinstance(args, basestring): - args = shlex.split(args) - return args - - def mute_detach(args, **kwargs): """ Run a muted detached process by forking it. @@ -200,7 +184,7 @@ def mute_detach(args, **kwargs): :return: process id of the command :rtype: int """ - args = split_args(args) + args = shlex.split(args) kwargs["preexec_fn"] = _detach_init kwargs["stdout"] = DEVNULL kwargs["stderr"] = STDOUT @@ -212,7 +196,7 @@ def check_cmd(args, env=None, cwd=None, wait=True): Execute a command on the host and return a tuple containing the exit status and result string. stderr output is folded into the stdout result string. - :param list[str]|str args: command arguments + :param str args: command arguments :param dict env: environment to run command with :param str cwd: directory to run command in :param bool wait: True to wait for status, False otherwise @@ -221,8 +205,8 @@ def check_cmd(args, env=None, cwd=None, wait=True): :raises CoreCommandError: when there is a non-zero exit status or the file to execute is not found """ - args = split_args(args) logging.info("command cwd(%s) wait(%s): %s", cwd, wait, args) + args = shlex.split(args) try: p = Popen(args, stdout=PIPE, stderr=PIPE, env=env, cwd=cwd) if wait: diff --git a/daemon/core/xml/corexmldeployment.py b/daemon/core/xml/corexmldeployment.py index c410ef5f..ee316ffc 100644 --- a/daemon/core/xml/corexmldeployment.py +++ b/daemon/core/xml/corexmldeployment.py @@ -3,7 +3,8 @@ import socket from lxml import etree -from core import constants, utils +from core import utils +from core.constants import IP_BIN from core.emane.nodes import EmaneNet from core.nodes import ipaddress from core.nodes.base import CoreNodeBase @@ -67,7 +68,7 @@ def get_address_type(address): def get_ipv4_addresses(hostname): if hostname == "localhost": addresses = [] - args = [constants.IP_BIN, "-o", "-f", "inet", "addr", "show"] + args = "%s -o -f inet address show" % IP_BIN output = utils.check_cmd(args) for line in output.split(os.linesep): split = line.split() diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py index 3d9b9eb2..65065665 100644 --- a/daemon/tests/emane/test_emane.py +++ b/daemon/tests/emane/test_emane.py @@ -27,7 +27,7 @@ _DIR = os.path.dirname(os.path.abspath(__file__)) def ping(from_node, to_node, ip_prefixes, count=3): address = ip_prefixes.ip4_address(to_node) try: - from_node.node_net_cmd(["ping", "-c", str(count), address]) + from_node.node_net_cmd("ping -c %s %s" % (count, address)) status = 0 except CoreCommandError as e: status = e.returncode diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 392794d0..3fc90da8 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -20,7 +20,7 @@ _WIRED = [NodeTypes.PEER_TO_PEER, NodeTypes.HUB, NodeTypes.SWITCH] def ping(from_node, to_node, ip_prefixes): address = ip_prefixes.ip4_address(to_node) try: - from_node.node_net_cmd(["ping", "-c", "3", address]) + from_node.node_net_cmd("ping -c 3 %s" % address) status = 0 except CoreCommandError as e: status = e.returncode diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index ad5476d4..1f18c87e 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -30,7 +30,7 @@ class TestNodes: assert os.path.exists(node.nodedir) assert node.alive() assert node.up - assert node.node_net_cmd(["ip", "addr", "show", "lo"]) + assert node.node_net_cmd("ip address show lo") def test_node_update(self, session): # given @@ -67,4 +67,4 @@ class TestNodes: # then assert node assert node.up - assert utils.check_cmd(["brctl", "show", node.brname]) + assert utils.check_cmd("brctl show %s" % node.brname) From 5b3308a23159553621b51e5b79c48277afe5d44d Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 11 Oct 2019 22:27:04 -0700 Subject: [PATCH 052/462] updated linkconfig to use string commands, fixed issues for wlan configuration --- daemon/core/api/tlv/corehandlers.py | 2 +- daemon/core/location/mobility.py | 2 +- daemon/core/nodes/netclient.py | 2 +- daemon/core/nodes/network.py | 49 ++++++++++++++--------------- 4 files changed, 27 insertions(+), 28 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 444ae5a5..6ff7f55b 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -1425,7 +1425,7 @@ class CoreHandler(socketserver.BaseRequestHandler): parsed_config = ConfigShim.str_to_dict(values_str) self.session.mobility.set_model_config(node_id, object_name, parsed_config) - if self.session.state == EventTypes.RUNTIME_STATE.value: + if self.session.state == EventTypes.RUNTIME_STATE.value and parsed_config: try: node = self.session.get_node(node_id) if object_name == BasicRangeModel.name: diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 3522a3f7..2f323783 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -426,7 +426,7 @@ class BasicRangeModel(WirelessModel): self.delay = int(config["delay"]) if self.delay == 0: self.delay = None - self.loss = int(config["error"]) + self.loss = int(float(config["error"])) if self.loss == 0: self.loss = None self.jitter = int(config["jitter"]) diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 930e13b4..43d21ead 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -122,7 +122,7 @@ class LinuxNetClient(object): :param str device: device to remove tc :return: nothing """ - self.run("%s qdisc del dev %s root" % (TC_BIN, device)) + self.run("%s qdisc delete dev %s root" % (TC_BIN, device)) def checksums_off(self, interface_name): """ diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index e0fcd839..4df7b28a 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -8,8 +8,8 @@ import threading import time from socket import AF_INET, AF_INET6 -from core import constants, utils -from core.constants import EBTABLES_BIN +from core import utils +from core.constants import EBTABLES_BIN, TC_BIN from core.emulator import distributed from core.emulator.data import LinkData from core.emulator.enumerations import LinkTypes, NodeTypes, RegisterTlvs @@ -457,8 +457,8 @@ class CoreNetwork(CoreNetworkBase): """ if devname is None: devname = netif.localname - tc = [constants.TC_BIN, "qdisc", "replace", "dev", devname] - parent = ["root"] + tc = "%s qdisc replace dev %s" % (TC_BIN, devname) + parent = "root" changed = False if netif.setparam("bw", bw): # from tc-tbf(8): minimum value for burst is rate / kernel_hz @@ -466,27 +466,24 @@ class CoreNetwork(CoreNetworkBase): burst = max(2 * netif.mtu, bw / 1000) # max IP payload limit = 0xFFFF - tbf = ["tbf", "rate", str(bw), "burst", str(burst), "limit", str(limit)] + tbf = "tbf rate %s burst %s limit %s" % (bw, burst, limit) if bw > 0: if self.up: - logging.debug( - "linkconfig: %s" % ([tc + parent + ["handle", "1:"] + tbf],) - ) - netif.net_cmd(tc + parent + ["handle", "1:"] + tbf) + cmd = "%s %s handle 1: %s" % (tc, parent, tbf) + netif.net_cmd(cmd) netif.setparam("has_tbf", True) changed = True elif netif.getparam("has_tbf") and bw <= 0: - tcd = [] + tc - tcd[2] = "delete" if self.up: - netif.net_cmd(tcd + parent) + cmd = "%s qdisc delete dev %s %s" % (TC_BIN, devname, parent) + netif.net_cmd(cmd) netif.setparam("has_tbf", False) # removing the parent removes the child netif.setparam("has_netem", False) changed = True if netif.getparam("has_tbf"): - parent = ["parent", "1:1"] - netem = ["netem"] + parent = "parent 1:1" + netem = "netem" changed = max(changed, netif.setparam("delay", delay)) if loss is not None: loss = float(loss) @@ -499,17 +496,17 @@ class CoreNetwork(CoreNetworkBase): return # jitter and delay use the same delay statement if delay is not None: - netem += ["delay", "%sus" % delay] + netem += " delay %sus" % delay if jitter is not None: if delay is None: - netem += ["delay", "0us", "%sus" % jitter, "25%"] + netem += " delay 0us %sus 25%%" % jitter else: - netem += ["%sus" % jitter, "25%"] + netem += " %sus 25%%" % jitter if loss is not None and loss > 0: - netem += ["loss", "%s%%" % min(loss, 100)] + netem += " loss %s%%" % min(loss, 100) if duplicate is not None and duplicate > 0: - netem += ["duplicate", "%s%%" % min(duplicate, 100)] + netem += " duplicate %s%%" % min(duplicate, 100) delay_check = delay is None or delay <= 0 jitter_check = jitter is None or jitter <= 0 @@ -519,17 +516,19 @@ class CoreNetwork(CoreNetworkBase): # possibly remove netem if it exists and parent queue wasn't removed if not netif.getparam("has_netem"): return - tc[2] = "delete" if self.up: - logging.debug("linkconfig: %s" % ([tc + parent + ["handle", "10:"]],)) - netif.net_cmd(tc + parent + ["handle", "10:"]) + cmd = "%s qdisc delete dev %s %s handle 10:" % (TC_BIN, devname, parent) + netif.net_cmd(cmd) netif.setparam("has_netem", False) elif len(netem) > 1: if self.up: - logging.debug( - "linkconfig: %s" % ([tc + parent + ["handle", "10:"] + netem],) + cmd = "%s qdisc replace dev %s %s handle 10: %s" % ( + TC_BIN, + devname, + parent, + netem, ) - netif.net_cmd(tc + parent + ["handle", "10:"] + netem) + netif.net_cmd(cmd) netif.setparam("has_netem", True) def linknet(self, net): From 2bfd05088094ed8d93d6f9737843793e4f66e0a4 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 11 Oct 2019 22:37:33 -0700 Subject: [PATCH 053/462] updated missed commands to be string based --- daemon/core/emane/emanemanager.py | 5 +++-- daemon/core/emulator/session.py | 3 ++- daemon/core/nodes/physical.py | 7 ++++--- daemon/examples/python/switch.py | 6 +++--- daemon/examples/python/wlan.py | 6 +++--- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 65fed8bd..35cc7ca9 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -974,9 +974,10 @@ class EmaneManager(ModelManager): def emanerunning(self, node): """ - Return True if an EMANE process associated with the given node is running, False otherwise. + Return True if an EMANE process associated with the given node is running, + False otherwise. """ - args = ["pkill", "-0", "-x", "emane"] + args = "pkill -0 -x emane" try: node.node_net_cmd(args) result = True diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 7c4cd651..7d3928b2 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -2025,7 +2025,8 @@ class Session(object): data, ) - # TODO: if data is None, this blows up, but this ties into how event functions are ran, need to clean that up + # TODO: if data is None, this blows up, but this ties into how event functions + # are ran, need to clean that up def run_event(self, node_id=None, name=None, data=None): """ Run a scheduled event, executing commands in the data string. diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 4c219258..9bd04c53 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -6,7 +6,8 @@ import logging import os import threading -from core import constants, utils +from core import utils +from core.constants import MOUNT_BIN, UMOUNT_BIN from core.emulator.enumerations import NodeTypes from core.errors import CoreCommandError from core.nodes.base import CoreNodeBase @@ -190,13 +191,13 @@ class PhysicalNode(CoreNodeBase): source = os.path.abspath(source) logging.info("mounting %s at %s", source, target) os.makedirs(target) - self.net_cmd([constants.MOUNT_BIN, "--bind", source, target], cwd=self.nodedir) + self.net_cmd("%s --bind %s %s" % (MOUNT_BIN, source, target), cwd=self.nodedir) self._mounts.append((source, target)) def umount(self, target): logging.info("unmounting '%s'" % target) try: - self.net_cmd([constants.UMOUNT_BIN, "-l", target], cwd=self.nodedir) + self.net_cmd("%s -l %s" % (UMOUNT_BIN, target), cwd=self.nodedir) except CoreCommandError: logging.exception("unmounting failed for %s", target) diff --git a/daemon/examples/python/switch.py b/daemon/examples/python/switch.py index 3c6ec383..e4d0fd02 100644 --- a/daemon/examples/python/switch.py +++ b/daemon/examples/python/switch.py @@ -43,14 +43,14 @@ def example(options): last_node = session.get_node(options.nodes + 1) print("starting iperf server on node: %s" % first_node.name) - first_node.node_net_cmd(["iperf", "-s", "-D"]) + first_node.node_net_cmd("iperf -s -D") first_node_address = prefixes.ip4_address(first_node) print("node %s connecting to %s" % (last_node.name, first_node_address)) output = last_node.node_net_cmd( - ["iperf", "-t", str(options.time), "-c", first_node_address] + "iperf -t %s -c %s" % (options.time, first_node_address) ) print(output) - first_node.node_net_cmd(["killall", "-9", "iperf"]) + first_node.node_net_cmd("killall -9 iperf") # shutdown session coreemu.shutdown() diff --git a/daemon/examples/python/wlan.py b/daemon/examples/python/wlan.py index 3d5171c2..b16af7cd 100644 --- a/daemon/examples/python/wlan.py +++ b/daemon/examples/python/wlan.py @@ -47,11 +47,11 @@ def example(options): last_node = session.get_node(options.nodes + 1) print("starting iperf server on node: %s" % first_node.name) - first_node.node_net_cmd(["iperf", "-s", "-D"]) + first_node.node_net_cmd("iperf -s -D") address = prefixes.ip4_address(first_node) print("node %s connecting to %s" % (last_node.name, address)) - last_node.node_net_cmd(["iperf", "-t", str(options.time), "-c", address]) - first_node.node_net_cmd(["killall", "-9", "iperf"]) + last_node.node_net_cmd("iperf -t %s -c %s" % (options.time, address)) + first_node.node_net_cmd("killall -9 iperf") # shutdown session coreemu.shutdown() From 82bdbd776b3d36b88f45c9397097735cf5ad1f93 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 14 Oct 2019 12:31:41 -0700 Subject: [PATCH 054/462] removed parameter conversion for creating GreTap commands --- daemon/core/nodes/interface.py | 14 ++++---------- daemon/core/nodes/netclient.py | 4 ++-- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 4e834fd3..2f42fdfe 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -456,12 +456,12 @@ class GreTap(CoreInterface): :param core.nodes.base.CoreNode node: related core node :param str name: interface name :param core.emulator.session.Session session: core session instance - :param mtu: interface mtu + :param int mtu: interface mtu :param str remoteip: remote address :param int _id: object id :param str localip: local address - :param ttl: ttl value - :param key: gre tap key + :param int ttl: ttl value + :param int key: gre tap key :param bool start: start flag :param fabric.connection.Connection server: remote server node will run on, default is None for localhost @@ -484,13 +484,7 @@ class GreTap(CoreInterface): if remoteip is None: raise ValueError("missing remote IP required for GRE TAP device") - if localip is not None: - localip = str(localip) - if ttl is not None: - ttl = str(ttl) - if key is not None: - key = str(key) - self.net_client.create_gretap(self.localname, str(remoteip), localip, ttl, key) + self.net_client.create_gretap(self.localname, remoteip, localip, ttl, key) self.net_client.device_up(self.localname) self.up = True diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 43d21ead..6de5d698 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -177,8 +177,8 @@ class LinuxNetClient(object): :param str device: device to add tap to :param str address: address to add tap for :param str local: local address to tie to - :param str ttl: time to live value - :param str key: key for tap + :param int ttl: time to live value + :param int key: key for tap :return: nothing """ cmd = "%s link add %s type gretap remote %s" % (IP_BIN, device, address) From 5f282bb6950e575c8d6c9f1c920c0072dbf376a1 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 14 Oct 2019 14:28:18 -0700 Subject: [PATCH 055/462] updates to lxd/docker to work with net_cmd/node_net_cmd --- daemon/core/nodes/base.py | 20 ++++++-- daemon/core/nodes/client.py | 11 +---- daemon/core/nodes/docker.py | 89 ++++++++++++++++++++++++---------- daemon/core/nodes/lxd.py | 70 +++++++++++++++----------- daemon/examples/lxd/lxd2lxd.py | 4 +- 5 files changed, 122 insertions(+), 72 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 85e25074..f263d0f3 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -452,14 +452,24 @@ class CoreNode(CoreNodeBase): self._mounts = [] self.bootsh = bootsh - if session.options.get_config("ovs") == "True": - self.node_net_client = OvsNetClient(self.node_net_cmd) - else: - self.node_net_client = LinuxNetClient(self.node_net_cmd) + use_ovs = session.options.get_config("ovs") == "True" + self.node_net_client = self.create_node_net_client(use_ovs) if start: self.startup() + def create_node_net_client(self, use_ovs): + """ + Create a client for running network orchestration commands. + + :param bool use_ovs: True to use OVS bridges, False for Linux bridge + :return: network client + """ + if use_ovs: + return OvsNetClient(self.node_net_cmd) + else: + return LinuxNetClient(self.node_net_cmd) + def alive(self): """ Check if the node is alive. @@ -584,7 +594,7 @@ class CoreNode(CoreNodeBase): :param str sh: shell to execute command in :return: str """ - return self.client.termcmdstring(sh) + return self.client.create_cmd(sh) def privatedir(self, path): """ diff --git a/daemon/core/nodes/client.py b/daemon/core/nodes/client.py index e09c72fa..632e12bc 100644 --- a/daemon/core/nodes/client.py +++ b/daemon/core/nodes/client.py @@ -4,7 +4,7 @@ over a control channel to the vnoded process running in a network namespace. The control channel can be accessed via calls using the vcmd shell. """ -from core import constants, utils +from core import utils from core.constants import VCMD_BIN @@ -66,12 +66,3 @@ class VnodeClient(object): self._verify_connection() args = self.create_cmd(args) return utils.check_cmd(args, wait=wait) - - def termcmdstring(self, sh="/bin/sh"): - """ - Create a terminal command string. - - :param str sh: shell to execute command in - :return: str - """ - return "%s -c %s -- %s" % (constants.VCMD_BIN, self.ctrlchnlname, sh) diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index 416b31d1..e465a768 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -1,21 +1,25 @@ import json import logging import os +from tempfile import NamedTemporaryFile from core import utils +from core.emulator import distributed from core.emulator.enumerations import NodeTypes from core.errors import CoreCommandError from core.nodes.base import CoreNode +from core.nodes.netclient import LinuxNetClient, OvsNetClient class DockerClient(object): - def __init__(self, name, image): + def __init__(self, name, image, run): self.name = name self.image = image + self.run = run self.pid = None def create_container(self): - utils.check_cmd( + self.run( "docker run -td --init --net=none --hostname {name} --name {name} " "--sysctl net.ipv6.conf.all.disable_ipv6=0 " "{image} /bin/bash".format( @@ -27,7 +31,7 @@ class DockerClient(object): def get_info(self): args = "docker inspect {name}".format(name=self.name) - output = utils.check_cmd(args) + output = self.run(args) data = json.loads(output) if not data: raise CoreCommandError( @@ -43,22 +47,24 @@ class DockerClient(object): return False def stop_container(self): - utils.check_cmd("docker rm -f {name}".format( + self.run("docker rm -f {name}".format( name=self.name )) def check_cmd(self, cmd): - if isinstance(cmd, list): - cmd = " ".join(cmd) logging.info("docker cmd output: %s", cmd) return utils.check_cmd("docker exec {name} {cmd}".format( name=self.name, cmd=cmd )) + def create_ns_cmd(self, cmd): + return "nsenter -t {pid} -u -i -p -n {cmd}".format( + pid=self.pid, + cmd=cmd + ) + def ns_cmd(self, cmd, wait): - if isinstance(cmd, list): - cmd = " ".join(cmd) args = "nsenter -t {pid} -u -i -p -n {cmd}".format( pid=self.pid, cmd=cmd @@ -67,7 +73,7 @@ class DockerClient(object): def get_pid(self): args = "docker inspect -f '{{{{.State.Pid}}}}' {name}".format(name=self.name) - output = utils.check_cmd(args) + output = self.run(args) self.pid = output logging.debug("node(%s) pid: %s", self.name, self.pid) return output @@ -78,7 +84,7 @@ class DockerClient(object): name=self.name, destination=destination ) - return utils.check_cmd(args) + return self.run(args) class DockerNode(CoreNode): @@ -101,6 +107,12 @@ class DockerNode(CoreNode): self.image = image super(DockerNode, self).__init__(session, _id, name, nodedir, bootsh, start) + def create_node_net_client(self, use_ovs): + if use_ovs: + return OvsNetClient(self.nsenter_cmd) + else: + return LinuxNetClient(self.nsenter_cmd) + def alive(self): """ Check if the node is alive. @@ -122,7 +134,7 @@ class DockerNode(CoreNode): if self.up: raise ValueError("starting a node that is already up") self.makenodedir() - self.client = DockerClient(self.name, self.image) + self.client = DockerClient(self.name, self.image, self.net_cmd) self.pid = self.client.create_container() self.up = True @@ -141,12 +153,13 @@ class DockerNode(CoreNode): self.client.stop_container() self.up = False - def node_net_cmd(self, args, wait=True): - if not self.up: - logging.debug("node down, not running network command: %s", args) - return "" - - return self.client.ns_cmd(args, wait) + def nsenter_cmd(self, args, wait=True): + if self.server is None: + args = self.client.create_ns_cmd(args) + return utils.check_cmd(args, wait=wait) + else: + args = self.client.create_ns_cmd(args) + return distributed.remote_cmd(self.server, args, wait=wait) def termcmdstring(self, sh="/bin/sh"): """ @@ -166,7 +179,7 @@ class DockerNode(CoreNode): """ logging.debug("creating node dir: %s", path) args = "mkdir -p {path}".format(path=path) - self.client.check_cmd(args) + self.node_net_cmd(args) def mount(self, source, target): """ @@ -189,13 +202,24 @@ class DockerNode(CoreNode): :param int mode: mode for file :return: nothing """ - logging.debug("node dir(%s) ctrlchannel(%s)", self.nodedir, self.ctrlchnlname) logging.debug("nodefile filename(%s) mode(%s)", filename, mode) - file_path = os.path.join(self.nodedir, filename) - with open(file_path, "w") as f: - os.chmod(f.name, mode) - f.write(contents) - self.client.copy_file(file_path, filename) + directory = os.path.dirname(filename) + temp = NamedTemporaryFile(delete=False) + temp.write(contents.encode("utf-8")) + temp.close() + + if directory: + self.node_net_cmd("mkdir -m %o -p %s" % (0o755, directory)) + if self.server is not None: + distributed.remote_put(self.server, temp.name, temp.name) + self.client.copy_file(temp.name, filename) + self.node_net_cmd("chmod %o %s" % (mode, filename)) + if self.server is not None: + self.net_cmd("rm -f %s" % temp.name) + os.unlink(temp.name) + logging.debug( + "node(%s) added file: %s; mode: 0%o", self.name, filename, mode + ) def nodefilecopy(self, filename, srcfilename, mode=None): """ @@ -207,5 +231,18 @@ class DockerNode(CoreNode): :param int mode: mode to copy to :return: nothing """ - logging.info("node file copy file(%s) source(%s) mode(%s)", filename, srcfilename, mode) - raise Exception("not supported") + logging.info( + "node file copy file(%s) source(%s) mode(%s)", filename, srcfilename, mode + ) + directory = os.path.dirname(filename) + self.node_net_cmd("mkdir -p %s" % directory) + + if self.server is None: + source = srcfilename + else: + temp = NamedTemporaryFile(delete=False) + source = temp.name + distributed.remote_put(self.server, source, temp.name) + + self.client.copy_file(source, filename) + self.node_net_cmd("chmod %o %s" % (mode, filename)) diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index 96b107f8..afd36db2 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -2,30 +2,31 @@ import json import logging import os import time +from tempfile import NamedTemporaryFile from core import utils +from core.emulator import distributed from core.emulator.enumerations import NodeTypes from core.errors import CoreCommandError from core.nodes.base import CoreNode class LxdClient(object): - def __init__(self, name, image): + def __init__(self, name, image, run): self.name = name self.image = image + self.run = run self.pid = None def create_container(self): - utils.check_cmd( - "lxc launch {image} {name}".format(name=self.name, image=self.image) - ) + self.run("lxc launch {image} {name}".format(name=self.name, image=self.image)) data = self.get_info() self.pid = data["state"]["pid"] return self.pid def get_info(self): args = "lxc list {name} --format json".format(name=self.name) - output = utils.check_cmd(args) + output = self.run(args) data = json.loads(output) if not data: raise CoreCommandError( @@ -41,20 +42,16 @@ class LxdClient(object): return False def stop_container(self): - utils.check_cmd("lxc delete --force {name}".format(name=self.name)) + self.run("lxc delete --force {name}".format(name=self.name)) def create_cmd(self, cmd): return "lxc exec -nT {name} -- {cmd}".format(name=self.name, cmd=cmd) - def check_cmd(self, cmd, wait=True): - args = self.create_cmd(cmd) - return utils.check_cmd(args, wait=wait) - def create_ns_cmd(self, cmd): return "nsenter -t {pid} -m -u -i -p -n {cmd}".format(pid=self.pid, cmd=cmd) - def ns_check_cmd(self, cmd, wait=True): - args = self.create_ns_cmd(cmd) + def check_cmd(self, cmd, wait=True): + args = self.create_cmd(cmd) return utils.check_cmd(args, wait=wait) def copy_file(self, source, destination): @@ -64,7 +61,7 @@ class LxdClient(object): args = "lxc file push {source} {name}/{destination}".format( source=source, name=self.name, destination=destination ) - utils.check_cmd(args) + self.run(args) class LxcNode(CoreNode): @@ -115,7 +112,7 @@ class LxcNode(CoreNode): if self.up: raise ValueError("starting a node that is already up") self.makenodedir() - self.client = LxdClient(self.name, self.image) + self.client = LxdClient(self.name, self.image, self.net_cmd) self.pid = self.client.create_container() self.up = True @@ -134,12 +131,6 @@ class LxcNode(CoreNode): self.client.stop_container() self.up = False - def node_net_cmd(self, args, wait=True): - if not self.up: - logging.debug("node down, not running network command: %s", args) - return "" - return self.client.check_cmd(args, wait) - def termcmdstring(self, sh="/bin/sh"): """ Create a terminal command string. @@ -147,7 +138,7 @@ class LxcNode(CoreNode): :param str sh: shell to execute command in :return: str """ - return "lxc exec {name} -- bash".format(name=self.name) + return "lxc exec {name} -- {sh}".format(name=self.name, sh=sh) def privatedir(self, path): """ @@ -158,7 +149,7 @@ class LxcNode(CoreNode): """ logging.info("creating node dir: %s", path) args = "mkdir -p {path}".format(path=path) - return self.client.check_cmd(args) + return self.node_net_cmd(args) def mount(self, source, target): """ @@ -181,13 +172,23 @@ class LxcNode(CoreNode): :param int mode: mode for file :return: nothing """ - logging.debug("node dir(%s) ctrlchannel(%s)", self.nodedir, self.ctrlchnlname) logging.debug("nodefile filename(%s) mode(%s)", filename, mode) - file_path = os.path.join(self.nodedir, filename) - with open(file_path, "w") as f: - os.chmod(f.name, mode) - f.write(contents) - self.client.copy_file(file_path, filename) + + directory = os.path.dirname(filename) + temp = NamedTemporaryFile(delete=False) + temp.write(contents.encode("utf-8")) + temp.close() + + if directory: + self.node_net_cmd("mkdir -m %o -p %s" % (0o755, directory)) + if self.server is not None: + distributed.remote_put(self.server, temp.name, temp.name) + self.client.copy_file(temp.name, filename) + self.node_net_cmd("chmod %o %s" % (mode, filename)) + if self.server is not None: + self.net_cmd("rm -f %s" % temp.name) + os.unlink(temp.name) + logging.debug("node(%s) added file: %s; mode: 0%o", self.name, filename, mode) def nodefilecopy(self, filename, srcfilename, mode=None): """ @@ -202,7 +203,18 @@ class LxcNode(CoreNode): logging.info( "node file copy file(%s) source(%s) mode(%s)", filename, srcfilename, mode ) - raise Exception("not supported") + directory = os.path.dirname(filename) + self.node_net_cmd("mkdir -p %s" % directory) + + if self.server is None: + source = srcfilename + else: + temp = NamedTemporaryFile(delete=False) + source = temp.name + distributed.remote_put(self.server, source, temp.name) + + self.client.copy_file(source, filename) + self.node_net_cmd("chmod %o %s" % (mode, filename)) def addnetif(self, netif, ifindex): super(LxcNode, self).addnetif(netif, ifindex) diff --git a/daemon/examples/lxd/lxd2lxd.py b/daemon/examples/lxd/lxd2lxd.py index e0ff13a3..4f27de95 100644 --- a/daemon/examples/lxd/lxd2lxd.py +++ b/daemon/examples/lxd/lxd2lxd.py @@ -5,7 +5,7 @@ from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes, NodeTypes if __name__ == "__main__": - logging.basicConfig(level=logging.DEBUG) + logging.basicConfig(level=logging.INFO) coreemu = CoreEmu() session = coreemu.create_session() @@ -14,7 +14,7 @@ if __name__ == "__main__": # create nodes and interfaces try: prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") - options = NodeOptions(image="ubuntu") + options = NodeOptions(image="ubuntu:18.04") # create node one node_one = session.add_node(_type=NodeTypes.LXC, node_options=options) From 6570f22ccf1151df3120f87428805f0edd3d060a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 14 Oct 2019 15:43:57 -0700 Subject: [PATCH 056/462] refactor fabric distributed to use a class and update sessions to create and provide these to nodes --- daemon/core/emane/emanemanager.py | 21 +++--- daemon/core/emulator/distributed.py | 113 ++++++++++++++++++---------- daemon/core/emulator/session.py | 32 ++++---- daemon/core/nodes/base.py | 35 ++++----- daemon/core/nodes/docker.py | 7 +- daemon/core/nodes/interface.py | 19 +++-- daemon/core/nodes/lxd.py | 5 +- daemon/core/nodes/network.py | 27 ++++--- daemon/core/nodes/physical.py | 4 +- daemon/core/xml/emanexml.py | 23 +++--- 10 files changed, 153 insertions(+), 133 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 35cc7ca9..f48d2e2e 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -18,7 +18,6 @@ from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emane.nodes import EmaneNet from core.emane.rfpipe import EmaneRfPipeModel from core.emane.tdma import EmaneTdmaModel -from core.emulator import distributed from core.emulator.enumerations import ( ConfigDataTypes, ConfigFlags, @@ -155,9 +154,9 @@ class EmaneManager(ModelManager): args = "emane --version" emane_version = utils.check_cmd(args) logging.info("using EMANE: %s", emane_version) - for server in self.session.servers: - conn = self.session.servers[server] - distributed.remote_cmd(conn, args) + for host in self.session.servers: + server = self.session.servers[host] + server.remote_cmd(args) # load default emane models self.load_models(EMANE_MODELS) @@ -757,9 +756,9 @@ class EmaneManager(ModelManager): emanecmd += " -f %s" % os.path.join(path, "emane.log") emanecmd += " %s" % os.path.join(path, "platform.xml") utils.check_cmd(emanecmd, cwd=path) - for server in self.session.servers: - conn = self.session.servers[server] - distributed.remote_cmd(conn, emanecmd, cwd=path) + for host in self.session.servers: + server = self.session.servers[host] + server.remote_cmd(emanecmd, cwd=path) logging.info("host emane daemon running: %s", emanecmd) def stopdaemons(self): @@ -784,10 +783,10 @@ class EmaneManager(ModelManager): try: utils.check_cmd(kill_emaned) utils.check_cmd(kill_transortd) - for server in self.session.servers: - conn = self.session[server] - distributed.remote_cmd(conn, kill_emaned) - distributed.remote_cmd(conn, kill_transortd) + for host in self.session.servers: + server = self.session[host] + server.remote_cmd(kill_emaned) + server.remote_cmd(kill_transortd) except CoreCommandError: logging.exception("error shutting down emane daemons") diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index abec0a57..19594ae1 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -1,8 +1,13 @@ +""" +Defines distributed server functionality. +""" + import logging import os import threading from tempfile import NamedTemporaryFile +from fabric import Connection from invoke import UnexpectedExit from core.errors import CoreCommandError @@ -10,52 +15,80 @@ from core.errors import CoreCommandError LOCK = threading.Lock() -def remote_cmd(server, cmd, env=None, cwd=None, wait=True): +class DistributedServer(object): """ - Run command remotely using server connection. - - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost - :param str cmd: command to run - :param dict env: environment for remote command, default is None - :param str cwd: directory to run command in, defaults to None, which is the user's - home directory - :param bool wait: True to wait for status, False to background process - :return: stdout when success - :rtype: str - :raises CoreCommandError: when a non-zero exit status occurs + Provides distributed server interactions. """ - replace_env = env is not None - if not wait: - cmd += " &" - logging.info( - "remote cmd server(%s) cwd(%s) wait(%s): %s", server.host, cwd, wait, cmd - ) - try: - with LOCK: - if cwd is None: - result = server.run(cmd, hide=False, env=env, replace_env=replace_env) - else: - with server.cd(cwd): - result = server.run( + def __init__(self, host): + """ + Create a DistributedServer instance. + + :param str host: host to connect to + """ + self.host = host + self.conn = Connection(host, user="root") + self.lock = threading.Lock() + + def remote_cmd(self, cmd, env=None, cwd=None, wait=True): + """ + Run command remotely using server connection. + + :param str cmd: command to run + :param dict env: environment for remote command, default is None + :param str cwd: directory to run command in, defaults to None, which is the user's + home directory + :param bool wait: True to wait for status, False to background process + :return: stdout when success + :rtype: str + :raises CoreCommandError: when a non-zero exit status occurs + """ + + replace_env = env is not None + if not wait: + cmd += " &" + logging.info( + "remote cmd server(%s) cwd(%s) wait(%s): %s", self.host, cwd, wait, cmd + ) + try: + with self.lock: + if cwd is None: + result = self.conn.run( cmd, hide=False, env=env, replace_env=replace_env ) - return result.stdout.strip() - except UnexpectedExit as e: - stdout, stderr = e.streams_for_display() - raise CoreCommandError(e.result.exited, cmd, stdout, stderr) + else: + with self.conn.cd(cwd): + result = self.conn.run( + cmd, hide=False, env=env, replace_env=replace_env + ) + return result.stdout.strip() + except UnexpectedExit as e: + stdout, stderr = e.streams_for_display() + raise CoreCommandError(e.result.exited, cmd, stdout, stderr) + def remote_put(self, source, destination): + """ + Push file to remote server. -def remote_put(server, source, destination): - with LOCK: - server.put(source, destination) + :param str source: source file to push + :param str destination: destination file location + :return: nothing + """ + with self.lock: + self.conn.put(source, destination) + def remote_put_temp(self, destination, data): + """ + Remote push file contents to a remote server, using a temp file as an + intermediate step. -def remote_put_temp(server, destination, data): - with LOCK: - temp = NamedTemporaryFile(delete=False) - temp.write(data.encode("utf-8")) - temp.close() - server.put(temp.name, destination) - os.unlink(temp.name) + :param str destination: file destination for data + :param str data: data to store in remote file + :return: nothing + """ + with self.lock: + temp = NamedTemporaryFile(delete=False) + temp.write(data.encode("utf-8")) + temp.close() + self.conn.put(temp.name, destination) + os.unlink(temp.name) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 7d3928b2..a0b4ec38 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -14,14 +14,13 @@ import threading import time from multiprocessing.pool import ThreadPool -from fabric import Connection - from core import constants, utils from core.api.tlv import coreapi from core.api.tlv.broker import CoreBroker from core.emane.emanemanager import EmaneManager from core.emane.nodes import EmaneNet from core.emulator.data import EventData, ExceptionData, NodeData +from core.emulator.distributed import DistributedServer from core.emulator.emudata import ( IdGen, LinkOptions, @@ -162,11 +161,11 @@ class Session(object): "host": ("DefaultRoute", "SSH"), } - def add_distributed(self, server): - conn = Connection(server, user="root") - self.servers[server] = conn + def add_distributed(self, host): + server = DistributedServer(host) + self.servers[host] = server cmd = "mkdir -p %s" % self.session_dir - conn.run(cmd, hide=False) + server.remote_cmd(cmd) def shutdown_distributed(self): # shutdown all tunnels @@ -176,10 +175,10 @@ class Session(object): tunnel.shutdown() # remove all remote session directories - for server in self.servers: - conn = self.servers[server] + for host in self.servers: + server = self.servers[host] cmd = "rm -rf %s" % self.session_dir - conn.run(cmd, hide=False) + server.remote_cmd(cmd) # clear tunnels self.tunnels.clear() @@ -194,18 +193,15 @@ class Session(object): if isinstance(node, CtrlNet) and node.serverintf is not None: continue - for server in self.servers: - conn = self.servers[server] - key = self.tunnelkey(node_id, IpAddress.to_int(server)) + for host in self.servers: + server = self.servers[host] + key = self.tunnelkey(node_id, IpAddress.to_int(host)) # local to server logging.info( - "local tunnel node(%s) to remote(%s) key(%s)", - node.name, - server, - key, + "local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key ) - local_tap = GreTap(session=self, remoteip=server, key=key) + local_tap = GreTap(session=self, remoteip=host, key=key) local_tap.net_client.create_interface(node.brname, local_tap.localname) # server to local @@ -216,7 +212,7 @@ class Session(object): key, ) remote_tap = GreTap( - session=self, remoteip=self.address, key=key, server=conn + session=self, remoteip=self.address, key=key, server=server ) remote_tap.net_client.create_interface( node.brname, remote_tap.localname diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index f263d0f3..7758e4af 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -14,7 +14,6 @@ from socket import AF_INET, AF_INET6 from core import utils from core.constants import MOUNT_BIN, VNODED_BIN -from core.emulator import distributed from core.emulator.data import LinkData, NodeData from core.emulator.enumerations import LinkTypes, NodeTypes from core.errors import CoreCommandError @@ -41,8 +40,8 @@ class NodeBase(object): :param int _id: id :param str name: object name :param bool start: start value - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost """ self.session = session @@ -101,7 +100,7 @@ class NodeBase(object): if self.server is None: return utils.check_cmd(args, env, cwd, wait) else: - return distributed.remote_cmd(self.server, args, env, cwd, wait) + return self.server.remote_cmd(args, env, cwd, wait) def setposition(self, x=None, y=None, z=None): """ @@ -200,7 +199,7 @@ class NodeBase(object): x, y, _ = self.getposition() model = self.type - emulation_server = self.server + emulation_server = self.server.host services = self.services if services is not None: @@ -253,8 +252,8 @@ class CoreNodeBase(NodeBase): :param int _id: object id :param str name: object name :param bool start: boolean for starting - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost """ super(CoreNodeBase, self).__init__(session, _id, name, start, server) self.services = [] @@ -437,8 +436,8 @@ class CoreNode(CoreNodeBase): :param str nodedir: node directory :param str bootsh: boot shell to use :param bool start: start flag - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost """ super(CoreNode, self).__init__(session, _id, name, start, server) self.nodedir = nodedir @@ -585,7 +584,7 @@ class CoreNode(CoreNodeBase): return self.client.check_cmd(args, wait=wait) else: args = self.client.create_cmd(args) - return distributed.remote_cmd(self.server, args, wait=wait) + return self.server.remote_cmd(args, wait=wait) def termcmdstring(self, sh="/bin/sh"): """ @@ -888,7 +887,7 @@ class CoreNode(CoreNodeBase): self.client.check_cmd("sync") else: self.net_cmd("mkdir -p %s" % directory) - distributed.remote_put(self.server, srcname, filename) + self.server.remote_put(srcname, filename) def hostfilename(self, filename): """ @@ -925,7 +924,7 @@ class CoreNode(CoreNodeBase): os.chmod(open_file.name, mode) else: self.net_cmd("mkdir -m %o -p %s" % (0o755, dirname)) - distributed.remote_put_temp(self.server, hostfilename, contents) + self.server.remote_put_temp(hostfilename, contents) self.net_cmd("chmod %o %s" % (mode, hostfilename)) logging.debug( "node(%s) added file: %s; mode: 0%o", self.name, hostfilename, mode @@ -944,12 +943,10 @@ class CoreNode(CoreNodeBase): hostfilename = self.hostfilename(filename) if self.server is None: shutil.copy2(srcfilename, hostfilename) - if mode is not None: - os.chmod(hostfilename, mode) else: - distributed.remote_put(self.server, srcfilename, hostfilename) - if mode is not None: - self.net_cmd("chmod %o %s" % (mode, hostfilename)) + self.server.remote_put(srcfilename, hostfilename) + if mode is not None: + self.net_cmd("chmod %o %s" % (mode, hostfilename)) logging.info( "node(%s) copied file: %s; mode: %s", self.name, hostfilename, mode ) @@ -971,8 +968,8 @@ class CoreNetworkBase(NodeBase): :param int _id: object id :param str name: object name :param bool start: should object start - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost """ super(CoreNetworkBase, self).__init__(session, _id, name, start, server) self._linked = {} diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index e465a768..2679704f 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -4,7 +4,6 @@ import os from tempfile import NamedTemporaryFile from core import utils -from core.emulator import distributed from core.emulator.enumerations import NodeTypes from core.errors import CoreCommandError from core.nodes.base import CoreNode @@ -159,7 +158,7 @@ class DockerNode(CoreNode): return utils.check_cmd(args, wait=wait) else: args = self.client.create_ns_cmd(args) - return distributed.remote_cmd(self.server, args, wait=wait) + return self.server.remote_cmd(args, wait=wait) def termcmdstring(self, sh="/bin/sh"): """ @@ -211,7 +210,7 @@ class DockerNode(CoreNode): if directory: self.node_net_cmd("mkdir -m %o -p %s" % (0o755, directory)) if self.server is not None: - distributed.remote_put(self.server, temp.name, temp.name) + self.server.remote_put(temp.name, temp.name) self.client.copy_file(temp.name, filename) self.node_net_cmd("chmod %o %s" % (mode, filename)) if self.server is not None: @@ -242,7 +241,7 @@ class DockerNode(CoreNode): else: temp = NamedTemporaryFile(delete=False) source = temp.name - distributed.remote_put(self.server, source, temp.name) + self.server.remote_put(source, temp.name) self.client.copy_file(source, filename) self.node_net_cmd("chmod %o %s" % (mode, filename)) diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 2f42fdfe..bfcb8583 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -7,7 +7,6 @@ import time from builtins import int, range from core import utils -from core.emulator import distributed from core.errors import CoreCommandError from core.nodes.netclient import LinuxNetClient @@ -24,8 +23,8 @@ class CoreInterface(object): :param core.nodes.base.CoreNode node: node for interface :param str name: interface name :param mtu: mtu value - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost """ self.node = node @@ -63,7 +62,7 @@ class CoreInterface(object): if self.server is None: return utils.check_cmd(args, env, cwd, wait) else: - return distributed.remote_cmd(self.server, args, env, cwd, wait) + return self.server.remote_cmd(args, env, cwd, wait) def startup(self): """ @@ -220,8 +219,8 @@ class Veth(CoreInterface): :param str name: interface name :param str localname: interface local name :param mtu: interface mtu - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost :param bool start: start flag :raises CoreCommandError: when there is a command exception """ @@ -280,8 +279,8 @@ class TunTap(CoreInterface): :param str name: interface name :param str localname: local interface name :param mtu: interface mtu - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost :param bool start: start flag """ CoreInterface.__init__(self, node, name, mtu, server) @@ -463,8 +462,8 @@ class GreTap(CoreInterface): :param int ttl: ttl value :param int key: gre tap key :param bool start: start flag - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost :raises CoreCommandError: when there is a command exception """ CoreInterface.__init__(self, node, name, mtu, server) diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index afd36db2..eef3dc8f 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -5,7 +5,6 @@ import time from tempfile import NamedTemporaryFile from core import utils -from core.emulator import distributed from core.emulator.enumerations import NodeTypes from core.errors import CoreCommandError from core.nodes.base import CoreNode @@ -182,7 +181,7 @@ class LxcNode(CoreNode): if directory: self.node_net_cmd("mkdir -m %o -p %s" % (0o755, directory)) if self.server is not None: - distributed.remote_put(self.server, temp.name, temp.name) + self.server.remote_put(temp.name, temp.name) self.client.copy_file(temp.name, filename) self.node_net_cmd("chmod %o %s" % (mode, filename)) if self.server is not None: @@ -211,7 +210,7 @@ class LxcNode(CoreNode): else: temp = NamedTemporaryFile(delete=False) source = temp.name - distributed.remote_put(self.server, source, temp.name) + self.server.remote_put(source, temp.name) self.client.copy_file(source, filename) self.node_net_cmd("chmod %o %s" % (mode, filename)) diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 4df7b28a..4d68ccaf 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -10,7 +10,6 @@ from socket import AF_INET, AF_INET6 from core import utils from core.constants import EBTABLES_BIN, TC_BIN -from core.emulator import distributed from core.emulator.data import LinkData from core.emulator.enumerations import LinkTypes, NodeTypes, RegisterTlvs from core.errors import CoreCommandError, CoreError @@ -257,8 +256,8 @@ class CoreNetwork(CoreNetworkBase): :param int _id: object id :param str name: object name :param bool start: start flag - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost :param policy: network policy """ CoreNetworkBase.__init__(self, session, _id, name, start, server) @@ -289,9 +288,9 @@ class CoreNetwork(CoreNetworkBase): """ logging.info("network node(%s) cmd", self.name) output = utils.check_cmd(args, env, cwd, wait) - for server in self.session.servers: - conn = self.session.servers[server] - distributed.remote_cmd(conn, args, env, cwd, wait) + for host in self.session.servers: + server = self.session.servers[host] + server.remote_cmd(args, env, cwd, wait) return output def startup(self): @@ -632,8 +631,8 @@ class GreTapBridge(CoreNetwork): :param ttl: ttl value :param key: gre tap key :param bool start: start flag - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost """ CoreNetwork.__init__(self, session, _id, name, False, server, policy) self.grekey = key @@ -753,8 +752,8 @@ class CtrlNet(CoreNetwork): :param prefix: control network ipv4 prefix :param hostid: host id :param bool start: start flag - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost :param str assign_address: assigned address :param str updown_script: updown script :param serverintf: server interface @@ -1008,8 +1007,8 @@ class HubNode(CoreNetwork): :param int _id: node id :param str name: node namee :param bool start: start flag - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost :raises CoreCommandError: when there is a command exception """ CoreNetwork.__init__(self, session, _id, name, start, server) @@ -1039,8 +1038,8 @@ class WlanNode(CoreNetwork): :param int _id: node id :param str name: node name :param bool start: start flag - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost :param policy: wlan policy """ CoreNetwork.__init__(self, session, _id, name, start, server, policy) diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 9bd04c53..9daf4f35 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -242,8 +242,8 @@ class Rj45Node(CoreNodeBase, CoreInterface): :param str name: node name :param mtu: rj45 mtu :param bool start: start flag - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost """ CoreNodeBase.__init__(self, session, _id, name, start, server) CoreInterface.__init__(self, node=self, name=name, mtu=mtu) diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index 3b4fafef..0005c378 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -5,7 +5,6 @@ from tempfile import NamedTemporaryFile from lxml import etree from core import utils -from core.emulator import distributed from core.nodes.ipaddress import MacAddress from core.xml import corexml @@ -53,8 +52,8 @@ def create_file(xml_element, doc_name, file_path, server=None): :param lxml.etree.Element xml_element: root element to write to file :param str doc_name: name to use in the emane doctype :param str file_path: file path to write xml file to - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost :return: nothing """ doctype = ( @@ -65,7 +64,7 @@ def create_file(xml_element, doc_name, file_path, server=None): temp = NamedTemporaryFile(delete=False) create_file(xml_element, doc_name, temp.name) temp.close() - distributed.remote_put(server, temp.name, file_path) + server.remote_put(temp.name, file_path) os.unlink(temp.name) else: corexml.write_xml_file(xml_element, file_path, doctype=doctype) @@ -327,8 +326,8 @@ def create_phy_xml(emane_model, config, file_path, server): :param core.emane.emanemodel.EmaneModel emane_model: emane model to create xml :param dict config: all current configuration values :param str file_path: path to write file to - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost :return: nothing """ phy_element = etree.Element("phy", name="%s PHY" % emane_model.name) @@ -355,8 +354,8 @@ def create_mac_xml(emane_model, config, file_path, server): :param core.emane.emanemodel.EmaneModel emane_model: emane model to create xml :param dict config: all current configuration values :param str file_path: path to write file to - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost :return: nothing """ if not emane_model.mac_library: @@ -396,8 +395,8 @@ def create_nem_xml( :param str transport_definition: transport file definition path :param str mac_definition: mac file definition path :param str phy_definition: phy file definition path - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost :return: nothing """ nem_element = etree.Element("nem", name="%s NEM" % emane_model.name) @@ -424,8 +423,8 @@ def create_event_service_xml(group, port, device, file_directory, server=None): :param str port: event port :param str device: event device :param str file_directory: directory to create file in - :param fabric.connection.Connection server: remote server node will run on, - default is None for localhost + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost :return: nothing """ event_element = etree.Element("emaneeventmsgsvc") From b2d2705849cb2f547586214790495ccb4bc19996 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 15 Oct 2019 14:13:42 -0700 Subject: [PATCH 057/462] removed broker from session, updated most places using broker to use alternative logic to compensate where needed --- daemon/core/__init__.py | 3 + daemon/core/api/tlv/coreapi.py | 18 -- daemon/core/api/tlv/corehandlers.py | 72 +++---- daemon/core/emane/emanemanager.py | 182 ++---------------- daemon/core/emulator/distributed.py | 8 +- daemon/core/emulator/session.py | 111 ++++------- daemon/core/location/mobility.py | 93 --------- daemon/core/nodes/base.py | 12 +- daemon/core/nodes/network.py | 4 +- daemon/core/plugins/sdt.py | 2 +- daemon/core/xml/corexmldeployment.py | 4 - daemon/core/xml/emanexml.py | 24 +-- daemon/examples/python/distributed.py | 16 +- daemon/examples/python/distributed_emane.py | 26 +-- daemon/examples/python/distributed_ptp.py | 13 +- .../examples/python/distributed_switches.py | 11 +- daemon/examples/python/distributed_wlan.py | 16 +- daemon/tests/conftest.py | 1 - daemon/tests/test_gui.py | 6 +- 19 files changed, 151 insertions(+), 471 deletions(-) diff --git a/daemon/core/__init__.py b/daemon/core/__init__.py index c847c8dc..40ca3604 100644 --- a/daemon/core/__init__.py +++ b/daemon/core/__init__.py @@ -2,3 +2,6 @@ import logging.config # setup default null handler logging.getLogger(__name__).addHandler(logging.NullHandler()) + +# disable paramiko logging +logging.getLogger("paramiko").setLevel(logging.WARNING) diff --git a/daemon/core/api/tlv/coreapi.py b/daemon/core/api/tlv/coreapi.py index 63747642..1e1de8be 100644 --- a/daemon/core/api/tlv/coreapi.py +++ b/daemon/core/api/tlv/coreapi.py @@ -15,7 +15,6 @@ from core.api.tlv import structutils from core.emulator.enumerations import ( ConfigTlvs, EventTlvs, - EventTypes, ExceptionTlvs, ExecuteTlvs, FileTlvs, @@ -1017,20 +1016,3 @@ def str_to_list(value): return None return value.split("|") - - -def state_name(value): - """ - Helper to convert state number into state name using event types. - - :param int value: state value to derive name from - :return: state name - :rtype: str - """ - - try: - value = EventTypes(value).name - except ValueError: - value = "unknown" - - return value diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 6ff7f55b..4b4e7c1e 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -86,6 +86,7 @@ class CoreHandler(socketserver.BaseRequestHandler): self.master = False self.session = None + self.session_clients = {} # core emulator self.coreemu = server.coreemu @@ -138,8 +139,9 @@ class CoreHandler(socketserver.BaseRequestHandler): if self.session: # remove client from session broker and shutdown if there are no clients self.remove_session_handlers() - self.session.broker.session_clients.remove(self) - if not self.session.broker.session_clients and not self.session.is_active(): + clients = self.session_clients[self.session.id] + clients.remove(self) + if not clients and not self.session.is_active(): logging.info( "no session clients left and not active, initiating shutdown" ) @@ -407,9 +409,7 @@ class CoreHandler(socketserver.BaseRequestHandler): tlv_data += coreapi.CoreRegisterTlv.pack( RegisterTlvs.EMULATION_SERVER.value, "core-daemon" ) - tlv_data += coreapi.CoreRegisterTlv.pack( - self.session.broker.config_type, self.session.broker.name - ) + tlv_data += coreapi.CoreRegisterTlv.pack(RegisterTlvs.UTILITY.value, "broker") tlv_data += coreapi.CoreRegisterTlv.pack( self.session.location.config_type, self.session.location.name ) @@ -533,10 +533,6 @@ class CoreHandler(socketserver.BaseRequestHandler): :param message: message to handle :return: nothing """ - if self.session and self.session.broker.handle_message(message): - logging.debug("message not being handled locally") - return - logging.debug( "%s handling message:\n%s", threading.currentThread().getName(), message ) @@ -606,12 +602,11 @@ class CoreHandler(socketserver.BaseRequestHandler): self.session = self.coreemu.create_session(port, master=False) logging.debug("created new session for client: %s", self.session.id) - # TODO: hack to associate this handler with this sessions broker for broadcasting - # TODO: broker needs to be pulled out of session to the server/handler level if self.master: logging.debug("session set to master") self.session.master = True - self.session.broker.session_clients.append(self) + clients = self.session_clients.setdefault(self.session.id, []) + clients.append(self) # add handlers for various data self.add_session_handlers() @@ -643,7 +638,8 @@ class CoreHandler(socketserver.BaseRequestHandler): ]: continue - for client in self.session.broker.session_clients: + clients = self.session_clients[self.session.id] + for client in clients: if client == self: continue @@ -734,6 +730,7 @@ class CoreHandler(socketserver.BaseRequestHandler): node_options.icon = message.get_tlv(NodeTlvs.ICON.value) node_options.canvas = message.get_tlv(NodeTlvs.CANVAS.value) node_options.opaque = message.get_tlv(NodeTlvs.OPAQUE.value) + node_options.emulation_server = message.get_tlv(NodeTlvs.EMULATION_SERVER.value) services = message.get_tlv(NodeTlvs.SERVICES.value) if services: @@ -1027,8 +1024,9 @@ class CoreHandler(socketserver.BaseRequestHandler): # find the session containing this client and set the session to master for _id in self.coreemu.sessions: - session = self.coreemu.sessions[_id] - if self in session.broker.session_clients: + clients = self.session_clients[_id] + if self in clients: + session = self.coreemu.sessions[_id] logging.debug("setting session to master: %s", session.id) session.master = True break @@ -1077,7 +1075,7 @@ class CoreHandler(socketserver.BaseRequestHandler): self.handle_config_location(message_type, config_data) elif config_data.object == self.session.metadata.name: replies = self.handle_config_metadata(message_type, config_data) - elif config_data.object == self.session.broker.name: + elif config_data.object == "broker": self.handle_config_broker(message_type, config_data) elif config_data.object == self.session.services.name: replies = self.handle_config_services(message_type, config_data) @@ -1182,7 +1180,6 @@ class CoreHandler(socketserver.BaseRequestHandler): def handle_config_broker(self, message_type, config_data): if message_type not in [ConfigFlags.REQUEST, ConfigFlags.RESET]: - session_id = config_data.session if not config_data.data_values: logging.info("emulation server data missing") else: @@ -1194,29 +1191,10 @@ class CoreHandler(socketserver.BaseRequestHandler): for server in server_list: server_items = server.split(":") - name, host, port = server_items[:3] - - if host == "": - host = None - - if port == "": - port = None - else: - port = int(port) - - if session_id is not None: - # receive session ID and my IP from master - self.session.broker.session_id_master = int( - session_id.split("|")[0] - ) - self.session.broker.myip = host - host = None - port = None - - # this connects to the server immediately; maybe we should wait - # or spin off a new "client" thread here - self.session.broker.addserver(name, host, port) - self.session.broker.setupserver(name) + name, host, _ = server_items[:3] + self.session.add_distributed(name, host) + elif message_type == ConfigFlags.RESET: + self.session.shutdown_distributed() def handle_config_services(self, message_type, config_data): replies = [] @@ -1842,11 +1820,9 @@ class CoreHandler(socketserver.BaseRequestHandler): # remove client from session broker and shutdown if needed self.remove_session_handlers() - self.session.broker.session_clients.remove(self) - if ( - not self.session.broker.session_clients - and not self.session.is_active() - ): + clients = self.session_clients[self.session.id] + clients.remove(self) + if not clients and not self.session.is_active(): self.coreemu.delete_session(self.session.id) # set session to join @@ -1855,7 +1831,8 @@ class CoreHandler(socketserver.BaseRequestHandler): # add client to session broker and set master if needed if self.master: self.session.master = True - self.session.broker.session_clients.append(self) + clients = self.session_clients.setdefault(self.session.id, []) + clients.append(self) # add broadcast handlers logging.info("adding session broadcast handlers") @@ -2139,7 +2116,8 @@ class CoreUdpHandler(CoreHandler): if not isinstance(message, (coreapi.CoreNodeMessage, coreapi.CoreLinkMessage)): return - for client in self.session.broker.session_clients: + clients = self.session_clients[self.session.id] + for client in clients: try: client.sendall(message.raw_message) except IOError: diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index f48d2e2e..e4208189 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -2,14 +2,12 @@ emane.py: definition of an Emane class for implementing configuration control of an EMANE emulation. """ -import copy import logging import os import threading from core import utils -from core.api.tlv import coreapi, dataconversion -from core.config import ConfigGroup, ConfigShim, Configuration, ModelManager +from core.config import ConfigGroup, Configuration, ModelManager from core.emane import emanemanifest from core.emane.bypass import EmaneBypassModel from core.emane.commeffect import EmaneCommEffectModel @@ -18,14 +16,7 @@ from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emane.nodes import EmaneNet from core.emane.rfpipe import EmaneRfPipeModel from core.emane.tdma import EmaneTdmaModel -from core.emulator.enumerations import ( - ConfigDataTypes, - ConfigFlags, - ConfigTlvs, - MessageFlags, - MessageTypes, - RegisterTlvs, -) +from core.emulator.enumerations import ConfigDataTypes, RegisterTlvs from core.errors import CoreCommandError, CoreError from core.xml import emanexml @@ -75,8 +66,6 @@ class EmaneManager(ModelManager): self.session = session self._emane_nets = {} self._emane_node_lock = threading.Lock() - self._ifccounts = {} - self._ifccountslock = threading.Lock() # port numbers are allocated from these counters self.platformport = self.session.options.get_config_int( "emane_platform_port", 8100 @@ -91,7 +80,6 @@ class EmaneManager(ModelManager): self.emane_config = EmaneGlobalModel(session) self.set_configs(self.emane_config.default_values()) - session.broker.handlers.add(self.handledistributed) self.service = None self.event_device = None self.emane_check() @@ -154,8 +142,8 @@ class EmaneManager(ModelManager): args = "emane --version" emane_version = utils.check_cmd(args) logging.info("using EMANE: %s", emane_version) - for host in self.session.servers: - server = self.session.servers[host] + for name in self.session.servers: + server = self.session.servers[name] server.remote_cmd(args) # load default emane models @@ -282,7 +270,6 @@ class EmaneManager(ModelManager): return EmaneManager.NOT_NEEDED # control network bridge required for EMANE 0.9.2 - # - needs to be configured before checkdistributed() for distributed # - needs to exist when eventservice binds to it (initeventservice) if self.session.master: otadev = self.get_config("otamanagerdevice") @@ -297,10 +284,9 @@ class EmaneManager(ModelManager): ) return EmaneManager.NOT_READY - ctrlnet = self.session.add_remove_control_net( + self.session.add_remove_control_net( net_index=netidx, remove=False, conf_required=False ) - self.distributedctrlnet(ctrlnet) eventdev = self.get_config("eventservicedevice") logging.debug("emane event service device: eventdev(%s)", eventdev) if eventdev != otadev: @@ -313,18 +299,9 @@ class EmaneManager(ModelManager): ) return EmaneManager.NOT_READY - ctrlnet = self.session.add_remove_control_net( + self.session.add_remove_control_net( net_index=netidx, remove=False, conf_required=False ) - self.distributedctrlnet(ctrlnet) - - if self.checkdistributed(): - # we are slave, but haven't received a platformid yet - platform_id_start = "platform_id_start" - default_values = self.emane_config.default_values() - value = self.get_config(platform_id_start) - if value == default_values[platform_id_start]: - return EmaneManager.NOT_READY self.check_node_models() return EmaneManager.SUCCESS @@ -413,9 +390,6 @@ class EmaneManager(ModelManager): """ stop all EMANE daemons """ - with self._ifccountslock: - self._ifccounts.clear() - with self._emane_node_lock: if not self._emane_nets: return @@ -424,92 +398,6 @@ class EmaneManager(ModelManager): self.stopdaemons() self.stopeventmonitor() - def handledistributed(self, message): - """ - Broker handler for processing CORE API messages as they are - received. This is used to snoop the Link add messages to get NEM - counts of NEMs that exist on other servers. - """ - if ( - message.message_type == MessageTypes.LINK.value - and message.flags & MessageFlags.ADD.value - ): - nn = message.node_numbers() - # first node is always link layer node in Link add message - if nn[0] in self.session.broker.network_nodes: - serverlist = self.session.broker.getserversbynode(nn[1]) - for server in serverlist: - with self._ifccountslock: - if server not in self._ifccounts: - self._ifccounts[server] = 1 - else: - self._ifccounts[server] += 1 - - def checkdistributed(self): - """ - Check for EMANE nodes that exist on multiple emulation servers and - coordinate the NEM id and port number space. - If we are the master EMANE node, return False so initialization will - proceed as normal; otherwise slaves return True here and - initialization is deferred. - """ - # check with the session if we are the "master" Emane object? - master = False - - with self._emane_node_lock: - if self._emane_nets: - master = self.session.master - logging.info("emane check distributed as master: %s.", master) - - # we are not the master Emane object, wait for nem id and ports - if not master: - return True - - nemcount = 0 - with self._emane_node_lock: - for key in self._emane_nets: - emane_node = self._emane_nets[key] - nemcount += emane_node.numnetif() - - nemid = int(self.get_config("nem_id_start")) - nemid += nemcount - - platformid = int(self.get_config("platform_id_start")) - - # build an ordered list of servers so platform ID is deterministic - servers = [] - for key in sorted(self._emane_nets): - for server in self.session.broker.getserversbynode(key): - if server not in servers: - servers.append(server) - - servers.sort(key=lambda x: x.name) - for server in servers: - if server.name == "localhost": - continue - - if server.sock is None: - continue - - platformid += 1 - - # create temporary config for updating distributed nodes - typeflags = ConfigFlags.UPDATE.value - config = copy.deepcopy(self.get_configs()) - config["platform_id_start"] = str(platformid) - config["nem_id_start"] = str(nemid) - config_data = ConfigShim.config_data( - 0, None, typeflags, self.emane_config, config - ) - message = dataconversion.convert_config(config_data) - server.sock.send(message) - # increment nemid for next server by number of interfaces - with self._ifccountslock: - if server in self._ifccounts: - nemid += self._ifccounts[server] - - return False - def buildxml(self): """ Build XML files required to run EMANE on each node. @@ -526,52 +414,6 @@ class EmaneManager(ModelManager): self.buildnemxml() self.buildeventservicexml() - # TODO: remove need for tlv messaging - def distributedctrlnet(self, ctrlnet): - """ - Distributed EMANE requires multiple control network prefixes to - be configured. This generates configuration for slave control nets - using the default list of prefixes. - """ - # slave server - session = self.session - if not session.master: - return - - # not distributed - servers = session.broker.getservernames() - if len(servers) < 2: - return - - # normal Config messaging will distribute controlnets - prefix = session.options.get_config("controlnet", default="") - prefixes = prefix.split() - if len(prefixes) < len(servers): - logging.info( - "setting up default controlnet prefixes for distributed (%d configured)", - len(prefixes), - ) - prefix = ctrlnet.DEFAULT_PREFIX_LIST[0] - prefixes = prefix.split() - servers.remove("localhost") - servers.insert(0, "localhost") - prefix = " ".join("%s:%s" % (s, prefixes[i]) for i, s in enumerate(servers)) - - # this generates a config message having controlnet prefix assignments - logging.info("setting up controlnet prefixes for distributed: %s", prefix) - vals = "controlnet=%s" % prefix - tlvdata = b"" - tlvdata += coreapi.CoreConfigTlv.pack(ConfigTlvs.OBJECT.value, "session") - tlvdata += coreapi.CoreConfigTlv.pack(ConfigTlvs.TYPE.value, 0) - tlvdata += coreapi.CoreConfigTlv.pack(ConfigTlvs.VALUES.value, vals) - rawmsg = coreapi.CoreConfMessage.pack(0, tlvdata) - msghdr = rawmsg[: coreapi.CoreMessage.header_len] - msg = coreapi.CoreConfMessage( - flags=0, hdr=msghdr, data=rawmsg[coreapi.CoreMessage.header_len :] - ) - logging.debug("sending controlnet message:\n%s", msg) - self.session.broker.handle_message(msg) - def check_node_models(self): """ Associate EMANE model classes with EMANE network nodes. @@ -676,8 +518,8 @@ class EmaneManager(ModelManager): dev = self.get_config("eventservicedevice") emanexml.create_event_service_xml(group, port, dev, self.session.session_dir) - for server in self.session.servers: - conn = self.session.servers[server] + for name in self.session.servers: + conn = self.session.servers[name] emanexml.create_event_service_xml( group, port, dev, self.session.session_dir, conn ) @@ -756,8 +598,8 @@ class EmaneManager(ModelManager): emanecmd += " -f %s" % os.path.join(path, "emane.log") emanecmd += " %s" % os.path.join(path, "platform.xml") utils.check_cmd(emanecmd, cwd=path) - for host in self.session.servers: - server = self.session.servers[host] + for name in self.session.servers: + server = self.session.servers[name] server.remote_cmd(emanecmd, cwd=path) logging.info("host emane daemon running: %s", emanecmd) @@ -783,8 +625,8 @@ class EmaneManager(ModelManager): try: utils.check_cmd(kill_emaned) utils.check_cmd(kill_transortd) - for host in self.session.servers: - server = self.session[host] + for name in self.session.servers: + server = self.session[name] server.remote_cmd(kill_emaned) server.remote_cmd(kill_transortd) except CoreCommandError: diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index 19594ae1..4e258937 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -20,12 +20,14 @@ class DistributedServer(object): Provides distributed server interactions. """ - def __init__(self, host): + def __init__(self, name, host): """ Create a DistributedServer instance. + :param str name: convenience name to associate with host :param str host: host to connect to """ + self.name = name self.host = host self.conn = Connection(host, user="root") self.lock = threading.Lock() @@ -36,8 +38,8 @@ class DistributedServer(object): :param str cmd: command to run :param dict env: environment for remote command, default is None - :param str cwd: directory to run command in, defaults to None, which is the user's - home directory + :param str cwd: directory to run command in, defaults to None, which is the + user's home directory :param bool wait: True to wait for status, False to background process :return: stdout when success :rtype: str diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index a0b4ec38..1445cb8f 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -15,8 +15,6 @@ import time from multiprocessing.pool import ThreadPool from core import constants, utils -from core.api.tlv import coreapi -from core.api.tlv.broker import CoreBroker from core.emane.emanemanager import EmaneManager from core.emane.nodes import EmaneNet from core.emulator.data import EventData, ExceptionData, NodeData @@ -142,10 +140,9 @@ class Session(object): # distributed servers self.servers = {} self.tunnels = {} - self.address = None + self.address = self.options.get_config("distributed_address", default=None) # initialize session feature helpers - self.broker = CoreBroker(session=self) self.location = CoreLocation() self.mobility = MobilityManager(session=self) self.services = CoreServices(session=self) @@ -161,9 +158,9 @@ class Session(object): "host": ("DefaultRoute", "SSH"), } - def add_distributed(self, host): - server = DistributedServer(host) - self.servers[host] = server + def add_distributed(self, name, host): + server = DistributedServer(name, host) + self.servers[name] = server cmd = "mkdir -p %s" % self.session_dir server.remote_cmd(cmd) @@ -175,8 +172,8 @@ class Session(object): tunnel.shutdown() # remove all remote session directories - for host in self.servers: - server = self.servers[host] + for name in self.servers: + server = self.servers[name] cmd = "rm -rf %s" % self.session_dir server.remote_cmd(cmd) @@ -193,8 +190,9 @@ class Session(object): if isinstance(node, CtrlNet) and node.serverintf is not None: continue - for host in self.servers: - server = self.servers[host] + for name in self.servers: + server = self.servers[name] + host = server.host key = self.tunnelkey(node_id, IpAddress.to_int(host)) # local to server @@ -219,23 +217,35 @@ class Session(object): ) # save tunnels for shutdown - self.tunnels[key] = [local_tap, remote_tap] + self.tunnels[key] = (local_tap, remote_tap) - def tunnelkey(self, n1num, n2num): + def tunnelkey(self, n1_id, n2_id): """ Compute a 32-bit key used to uniquely identify a GRE tunnel. The hash(n1num), hash(n2num) values are used, so node numbers may be None or string values (used for e.g. "ctrlnet"). - :param int n1num: node one id - :param int n2num: node two id + :param int n1_id: node one id + :param int n2_id: node two id :return: tunnel key for the node pair :rtype: int """ - logging.debug("creating tunnel key for: %s, %s", n1num, n2num) - key = (self.id << 16) ^ utils.hashkey(n1num) ^ (utils.hashkey(n2num) << 8) + logging.debug("creating tunnel key for: %s, %s", n1_id, n2_id) + key = (self.id << 16) ^ utils.hashkey(n1_id) ^ (utils.hashkey(n2_id) << 8) return key & 0xFFFFFFFF + def gettunnel(self, n1_id, n2_id): + """ + Return the GreTap between two nodes if it exists. + + :param int n1_id: node one id + :param int n2_id: node two id + :return: gre tap between nodes or None + """ + key = self.tunnelkey(n1_id, n2_id) + logging.debug("checking for tunnel key(%s) in: %s", key, self.tunnels) + return self.tunnels.get(key) + @classmethod def get_node_class(cls, _type): """ @@ -285,7 +295,7 @@ class Session(object): node_two = self.get_node(node_two_id) # both node ids are provided - tunnel = self.broker.gettunnel(node_one_id, node_two_id) + tunnel = self.gettunnel(node_one_id, node_two_id) logging.debug("tunnel between nodes: %s", tunnel) if isinstance(tunnel, GreTapBridge): net_one = tunnel @@ -958,13 +968,13 @@ class Session(object): def clear(self): """ - Clear all CORE session data. (objects, hooks, broker) + Clear all CORE session data. (nodes, hooks, etc) :return: nothing """ self.delete_nodes() + self.shutdown_distributed() self.del_hooks() - self.broker.reset() self.emane.reset() def start_events(self): @@ -1038,17 +1048,16 @@ class Session(object): # shutdown/cleanup feature helpers self.emane.shutdown() - self.broker.shutdown() self.sdt.shutdown() - # delete all current nodes + # remove and shutdown all nodes and tunnels self.delete_nodes() + self.shutdown_distributed() # remove this sessions working directory preserve = self.options.get_config("preservedir") == "1" if not preserve: shutil.rmtree(self.session_dir, ignore_errors=True) - self.shutdown_distributed() # call session shutdown handlers for handler in self.shutdown_handlers: @@ -1160,7 +1169,7 @@ class Session(object): """ try: state_file = open(self._state_file, "w") - state_file.write("%d %s\n" % (state, coreapi.state_name(state))) + state_file.write("%d %s\n" % (state, EventTypes(self.state).name)) state_file.close() except IOError: logging.exception("error writing state file: %s", state) @@ -1278,7 +1287,7 @@ class Session(object): hook(state) except Exception: message = "exception occured when running %s state hook: %s" % ( - coreapi.state_name(state), + EventTypes(self.state).name, hook, ) logging.exception(message) @@ -1549,11 +1558,10 @@ class Session(object): # write current nodes out to session directory file self.write_nodes() - # create control net interfaces and broker network tunnels + # create control net interfaces and network tunnels # which need to exist for emane to sync on location events # in distributed scenarios self.add_remove_control_interface(node=None, remove=False) - self.broker.startup() # initialize distributed tunnels self.initialize_distributed() @@ -1566,9 +1574,6 @@ class Session(object): self.boot_nodes() self.mobility.startup() - # set broker local instantiation to complete - self.broker.local_instantiation_complete() - # notify listeners that instantiation is complete event = EventData(event_type=EventTypes.INSTANTIATION_COMPLETE.value) self.broadcast_event(event) @@ -1606,21 +1611,16 @@ class Session(object): have entered runtime (time=0). """ # this is called from instantiate() after receiving an event message - # for the instantiation state, and from the broker when distributed - # nodes have been started + # for the instantiation state logging.debug( "session(%s) checking if not in runtime state, current state: %s", self.id, - coreapi.state_name(self.state), + EventTypes(self.state).name, ) if self.state == EventTypes.RUNTIME_STATE.value: logging.info("valid runtime state found, returning") return - # check to verify that all nodes and networks are running - if not self.broker.instantiation_complete(): - return - # start event loop and set to runtime self.event_loop.run() self.set_state(EventTypes.RUNTIME_STATE, send_event=True) @@ -1830,37 +1830,11 @@ class Session(object): except IndexError: # no server name. possibly only one server prefix = prefixes[0] - else: - # slave servers have their name and localhost in the serverlist - servers = self.broker.getservernames() - servers.remove("localhost") - prefix = None - for server_prefix in prefixes: - try: - # split each entry into server and prefix - server, p = server_prefix.split(":") - except ValueError: - server = "" - p = None - - if server == servers[0]: - # the server name in the list matches this server - prefix = p - break - - if not prefix: - logging.error( - "control network prefix not found for server: %s", servers[0] - ) - assign_address = False - try: - prefix = prefixes[0].split(":", 1)[1] - except IndexError: - prefix = prefixes[0] # len(prefixes) == 1 else: - # TODO: can we get the server name from the servers.conf or from the node assignments? + # TODO: can we get the server name from the servers.conf or from the node + # assignments?o # with one prefix, only master gets a ctrlnet address assign_address = self.master prefix = prefixes[0] @@ -1882,13 +1856,6 @@ class Session(object): serverintf=server_interface, ) - # tunnels between controlnets will be built with Broker.addnettunnels() - # TODO: potentially remove documentation saying node ids are ints - # TODO: need to move broker code out of the session object - self.broker.addnet(_id) - for server in self.broker.getservers(): - self.broker.addnodemap(server, _id) - return control_net def add_remove_control_interface( diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 2f323783..eae46ce4 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -19,13 +19,9 @@ from core.emulator.enumerations import ( EventTypes, LinkTypes, MessageFlags, - MessageTypes, - NodeTlvs, RegisterTlvs, ) from core.errors import CoreError -from core.nodes.base import CoreNodeBase -from core.nodes.ipaddress import IpAddress class MobilityManager(ModelManager): @@ -48,11 +44,6 @@ class MobilityManager(ModelManager): self.models[BasicRangeModel.name] = BasicRangeModel self.models[Ns2ScriptedMobility.name] = Ns2ScriptedMobility - # dummy node objects for tracking position of nodes on other servers - self.phys = {} - self.physnets = {} - self.session.broker.handlers.add(self.physnodehandlelink) - def reset(self): """ Clear out all current configurations. @@ -93,9 +84,6 @@ class MobilityManager(ModelManager): model_class = self.models[model_name] self.set_model(node, model_class, config) - if self.session.master: - self.installphysnodes(node) - if node.mobility: self.session.event_loop.add_event(0.0, node.mobility.startup) @@ -209,87 +197,6 @@ class MobilityManager(ModelManager): if node.model: node.model.update(moved, moved_netifs) - def addphys(self, netnum, node): - """ - Keep track of PhysicalNodes and which network they belong to. - - :param int netnum: network number - :param core.coreobj.PyCoreNode node: node to add physical network to - :return: nothing - """ - node_id = node.id - self.phys[node_id] = node - if netnum not in self.physnets: - self.physnets[netnum] = [node_id] - else: - self.physnets[netnum].append(node_id) - - # TODO: remove need for handling old style message - - def physnodehandlelink(self, message): - """ - Broker handler. Snoop Link add messages to get - node numbers of PhyiscalNodes and their nets. - Physical nodes exist only on other servers, but a shadow object is - created here for tracking node position. - - :param message: link message to handle - :return: nothing - """ - if ( - message.message_type == MessageTypes.LINK.value - and message.flags & MessageFlags.ADD.value - ): - nn = message.node_numbers() - # first node is always link layer node in Link add message - if nn[0] not in self.session.broker.network_nodes: - return - if nn[1] in self.session.broker.physical_nodes: - # record the fact that this PhysicalNode is linked to a net - dummy = CoreNodeBase( - session=self.session, _id=nn[1], name="n%d" % nn[1], start=False - ) - self.addphys(nn[0], dummy) - - # TODO: remove need to handling old style messages - def physnodeupdateposition(self, message): - """ - Snoop node messages belonging to physical nodes. The dummy object - in self.phys[] records the node position. - - :param message: message to handle - :return: nothing - """ - nodenum = message.node_numbers()[0] - try: - dummy = self.phys[nodenum] - nodexpos = message.get_tlv(NodeTlvs.X_POSITION.value) - nodeypos = message.get_tlv(NodeTlvs.Y_POSITION.value) - dummy.setposition(nodexpos, nodeypos, None) - except KeyError: - logging.exception("error retrieving physical node: %s", nodenum) - - def installphysnodes(self, net): - """ - After installing a mobility model on a net, include any physical - nodes that we have recorded. Use the GreTap tunnel to the physical node - as the node's interface. - - :param net: network to install - :return: nothing - """ - node_ids = self.physnets.get(net.id, []) - for node_id in node_ids: - node = self.phys[node_id] - # TODO: fix this bad logic, relating to depending on a break to get a valid server - for server in self.session.broker.getserversbynode(node_id): - break - netif = self.session.broker.gettunnel(net.id, IpAddress.to_int(server.host)) - node.addnetif(netif, 0) - netif.node = node - x, y, z = netif.node.position.get() - netif.poshook(netif, x, y, z) - class WirelessModel(ConfigurableOptions): """ diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 7758e4af..a9e8dc43 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -199,7 +199,9 @@ class NodeBase(object): x, y, _ = self.getposition() model = self.type - emulation_server = self.server.host + emulation_server = None + if self.server is not None: + emulation_server = self.server.host services = self.services if services is not None: @@ -593,7 +595,13 @@ class CoreNode(CoreNodeBase): :param str sh: shell to execute command in :return: str """ - return self.client.create_cmd(sh) + terminal = self.client.create_cmd(sh) + if self.server is None: + return terminal + else: + return "ssh -X -f {host} xterm -e {terminal}".format( + host=self.server.host, terminal=terminal + ) def privatedir(self, path): """ diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 4d68ccaf..f25c800c 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -288,8 +288,8 @@ class CoreNetwork(CoreNetworkBase): """ logging.info("network node(%s) cmd", self.name) output = utils.check_cmd(args, env, cwd, wait) - for host in self.session.servers: - server = self.session.servers[host] + for name in self.session.servers: + server = self.session.servers[name] server.remote_cmd(args, env, cwd, wait) return output diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 32800eea..fd674b45 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -76,7 +76,7 @@ class Sdt(object): # node information for remote nodes not in session._objs # local nodes also appear here since their obj may not exist yet self.remotes = {} - session.broker.handlers.add(self.handle_distributed) + # session.broker.handlers.add(self.handle_distributed) # add handler for node updates self.session.node_handlers.append(self.handle_node_update) diff --git a/daemon/core/xml/corexmldeployment.py b/daemon/core/xml/corexmldeployment.py index ee316ffc..0a81b75e 100644 --- a/daemon/core/xml/corexmldeployment.py +++ b/daemon/core/xml/corexmldeployment.py @@ -107,10 +107,6 @@ class CoreXmlDeployment(object): def add_deployment(self): physical_host = self.add_physical_host(socket.gethostname()) - # TODO: handle other servers - # servers = self.session.broker.getservernames() - # servers.remove("localhost") - for node_id in self.session.nodes: node = self.session.nodes[node_id] if isinstance(node, CoreNodeBase): diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index 0005c378..881ff373 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -314,9 +314,9 @@ def build_transport_xml(emane_manager, node, transport_type): file_name = transport_file_name(node.id, transport_type) file_path = os.path.join(emane_manager.session.session_dir, file_name) create_file(transport_element, doc_name, file_path) - for server in emane_manager.session.servers: - conn = emane_manager.session.servers[server] - create_file(transport_element, doc_name, file_path, conn) + for name in emane_manager.session.servers: + server = emane_manager.session.servers[name] + create_file(transport_element, doc_name, file_path, server) def create_phy_xml(emane_model, config, file_path, server): @@ -342,9 +342,9 @@ def create_phy_xml(emane_model, config, file_path, server): create_file(phy_element, "phy", file_path, server) else: create_file(phy_element, "phy", file_path) - for server in emane_model.session.servers: - conn = emane_model.session.servers[server] - create_file(phy_element, "phy", file_path, conn) + for name in emane_model.session.servers: + server = emane_model.session.servers[name] + create_file(phy_element, "phy", file_path, server) def create_mac_xml(emane_model, config, file_path, server): @@ -372,9 +372,9 @@ def create_mac_xml(emane_model, config, file_path, server): create_file(mac_element, "mac", file_path, server) else: create_file(mac_element, "mac", file_path) - for server in emane_model.session.servers: - conn = emane_model.session.servers[server] - create_file(mac_element, "mac", file_path, conn) + for name in emane_model.session.servers: + server = emane_model.session.servers[name] + create_file(mac_element, "mac", file_path, server) def create_nem_xml( @@ -410,9 +410,9 @@ def create_nem_xml( create_file(nem_element, "nem", nem_file, server) else: create_file(nem_element, "nem", nem_file) - for server in emane_model.session.servers: - conn = emane_model.session.servers[server] - create_file(nem_element, "nem", nem_file, conn) + for name in emane_model.session.servers: + server = emane_model.session.servers[name] + create_file(nem_element, "nem", nem_file, server) def create_event_service_xml(group, port, device, file_directory, server=None): diff --git a/daemon/examples/python/distributed.py b/daemon/examples/python/distributed.py index ca9ca928..8bcf2972 100644 --- a/daemon/examples/python/distributed.py +++ b/daemon/examples/python/distributed.py @@ -8,21 +8,19 @@ from core.emulator.enumerations import EventTypes, NodeTypes def main(): + address = sys.argv[1] + remote = sys.argv[2] + # ip generator for example prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") # create emulator instance for creating sessions and utility methods - coreemu = CoreEmu() + coreemu = CoreEmu({"controlnet": "172.16.0.0/24", "distributed_address": address}) session = coreemu.create_session() - # set controlnet - session.options.set_config("controlnet", "172.16.0.0/24") - # initialize distributed - address = sys.argv[1] - remote = sys.argv[2] - session.address = address - session.add_distributed(remote) + server_name = "core2" + session.add_distributed(server_name, remote) # must be in configuration state for nodes to start, when using "node_add" below session.set_state(EventTypes.CONFIGURATION_STATE) @@ -31,7 +29,7 @@ def main(): node_one = session.add_node() switch = session.add_node(_type=NodeTypes.SWITCH) options = NodeOptions() - options.emulation_server = remote + options.emulation_server = server_name node_two = session.add_node(node_options=options) # create node interfaces and link diff --git a/daemon/examples/python/distributed_emane.py b/daemon/examples/python/distributed_emane.py index 1ffe5795..c64d1f0c 100644 --- a/daemon/examples/python/distributed_emane.py +++ b/daemon/examples/python/distributed_emane.py @@ -9,25 +9,25 @@ from core.emulator.enumerations import EventTypes, NodeTypes def main(): + address = sys.argv[1] + remote = sys.argv[2] + # ip generator for example prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") # create emulator instance for creating sessions and utility methods - coreemu = CoreEmu() + coreemu = CoreEmu( + { + "controlnet": "core1:172.16.1.0/24 core2:172.16.2.0/24 core3:172.16.3.0/24 " + "core4:172.16.4.0/24 core5:172.16.5.0/24", + "distributed_address": address, + } + ) session = coreemu.create_session() - # set controlnet - session.options.set_config( - "controlnet", - "core1:172.16.1.0/24 core2:172.16.2.0/24 core3:172.16.3.0/24 " - "core4:172.16.4.0/24 core5:172.16.5.0/24", - ) - # initialize distributed - address = sys.argv[1] - remote = sys.argv[2] - session.address = address - session.add_distributed(remote) + server_name = "core2" + session.add_distributed(server_name, remote) # must be in configuration state for nodes to start, when using "node_add" below session.set_state(EventTypes.CONFIGURATION_STATE) @@ -38,7 +38,7 @@ def main(): node_one = session.add_node(node_options=options) emane_net = session.add_node(_type=NodeTypes.EMANE) session.emane.set_model(emane_net, EmaneIeee80211abgModel) - options.emulation_server = remote + options.emulation_server = server_name node_two = session.add_node(node_options=options) # create node interfaces and link diff --git a/daemon/examples/python/distributed_ptp.py b/daemon/examples/python/distributed_ptp.py index 2b611816..b0f27c28 100644 --- a/daemon/examples/python/distributed_ptp.py +++ b/daemon/examples/python/distributed_ptp.py @@ -8,18 +8,19 @@ from core.emulator.enumerations import EventTypes def main(): + address = sys.argv[1] + remote = sys.argv[2] + # ip generator for example prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") # create emulator instance for creating sessions and utility methods - coreemu = CoreEmu() + coreemu = CoreEmu({"distributed_address": address}) session = coreemu.create_session() # initialize distributed - address = sys.argv[1] - remote = sys.argv[2] - session.address = address - session.add_distributed(remote) + server_name = "core2" + session.add_distributed(server_name, remote) # must be in configuration state for nodes to start, when using "node_add" below session.set_state(EventTypes.CONFIGURATION_STATE) @@ -27,7 +28,7 @@ def main(): # create local node, switch, and remote nodes options = NodeOptions() node_one = session.add_node(node_options=options) - options.emulation_server = remote + options.emulation_server = server_name node_two = session.add_node(node_options=options) # create node interfaces and link diff --git a/daemon/examples/python/distributed_switches.py b/daemon/examples/python/distributed_switches.py index b7ed166b..bc13bf2c 100644 --- a/daemon/examples/python/distributed_switches.py +++ b/daemon/examples/python/distributed_switches.py @@ -7,15 +7,16 @@ from core.emulator.enumerations import EventTypes, NodeTypes def main(): + address = sys.argv[1] + remote = sys.argv[2] + # create emulator instance for creating sessions and utility methods - coreemu = CoreEmu() + coreemu = CoreEmu({"distributed_address": address}) session = coreemu.create_session() # initialize distributed - address = sys.argv[1] - remote = sys.argv[2] - session.address = address - session.add_distributed(remote) + server_name = "core2" + session.add_distributed(server_name, remote) # must be in configuration state for nodes to start, when using "node_add" below session.set_state(EventTypes.CONFIGURATION_STATE) diff --git a/daemon/examples/python/distributed_wlan.py b/daemon/examples/python/distributed_wlan.py index ca64ee01..f8af1f5f 100644 --- a/daemon/examples/python/distributed_wlan.py +++ b/daemon/examples/python/distributed_wlan.py @@ -9,21 +9,19 @@ from core.location.mobility import BasicRangeModel def main(): + address = sys.argv[1] + remote = sys.argv[2] + # ip generator for example prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") # create emulator instance for creating sessions and utility methods - coreemu = CoreEmu() + coreemu = CoreEmu({"distributed_address": address}) session = coreemu.create_session() - # set controlnet - # session.options.set_config("controlnet", "172.16.0.0/24") - # initialize distributed - address = sys.argv[1] - remote = sys.argv[2] - session.address = address - session.add_distributed(remote) + server_name = "core2" + session.add_distributed(server_name, remote) # must be in configuration state for nodes to start, when using "node_add" below session.set_state(EventTypes.CONFIGURATION_STATE) @@ -31,7 +29,7 @@ def main(): # create local node, switch, and remote nodes options = NodeOptions() options.set_position(0, 0) - options.emulation_server = remote + options.emulation_server = server_name node_one = session.add_node(node_options=options) wlan = session.add_node(_type=NodeTypes.WIRELESS_LAN) session.mobility.set_model(wlan, BasicRangeModel) diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index 001233bb..ead3c2b4 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -58,7 +58,6 @@ class CoreServerTest(object): self.request_handler = CoreHandler(request_mock, "", self.server) self.request_handler.session = self.session self.request_handler.add_session_handlers() - self.session.broker.session_clients.append(self.request_handler) # have broker handle a configuration state change self.session.set_state(EventTypes.DEFINITION_STATE) diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index caff15fe..02e634be 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -763,13 +763,11 @@ class TestGui: (ConfigTlvs.VALUES, "%s:%s:%s" % (server, host, port)), ], ) - coreserver.session.broker.addserver = mock.MagicMock() - coreserver.session.broker.setupserver = mock.MagicMock() + coreserver.session.add_distributed = mock.MagicMock() coreserver.request_handler.handle_message(message) - coreserver.session.broker.addserver.assert_called_once_with(server, host, port) - coreserver.session.broker.setupserver.assert_called_once_with(server) + coreserver.session.add_distributed.assert_called_once_with(server, host) def test_config_services_request_all(self, coreserver): message = coreapi.CoreConfMessage.create( From 0b8bc7bd1362e96d3089eb28b239afb2d0198b7e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 15 Oct 2019 15:02:38 -0700 Subject: [PATCH 058/462] updated corehandlers to allow sdt snooping to help mimic previous behavior --- daemon/core/api/tlv/corehandlers.py | 2 ++ daemon/core/plugins/sdt.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 4b4e7c1e..60ddfcca 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -2082,6 +2082,7 @@ class CoreUdpHandler(CoreHandler): logging.debug("session handling message: %s", session.session_id) self.session = session self.handle_message(message) + self.session.sdt.handle_distributed(message) self.broadcast(message) else: logging.error( @@ -2106,6 +2107,7 @@ class CoreUdpHandler(CoreHandler): if session or message.message_type == MessageTypes.REGISTER.value: self.session = session self.handle_message(message) + self.session.sdt.handle_distributed(message) self.broadcast(message) else: logging.error( diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index fd674b45..52635da3 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -76,7 +76,6 @@ class Sdt(object): # node information for remote nodes not in session._objs # local nodes also appear here since their obj may not exist yet self.remotes = {} - # session.broker.handlers.add(self.handle_distributed) # add handler for node updates self.session.node_handlers.append(self.handle_node_update) From 61a4e228a158ec9ffa5b4ce1b5df111f985c93e9 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 16 Oct 2019 10:14:36 -0700 Subject: [PATCH 059/462] updated ctrlnets to assign unique addresses per server, fixed ovs command issue for interface specific commands --- daemon/core/nodes/base.py | 62 ++++++---------------------------- daemon/core/nodes/docker.py | 14 +++++--- daemon/core/nodes/interface.py | 33 +++++++++++------- daemon/core/nodes/netclient.py | 14 ++++++++ daemon/core/nodes/network.py | 35 ++++++++++++++----- daemon/core/nodes/physical.py | 2 +- 6 files changed, 81 insertions(+), 79 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index a9e8dc43..4e9de039 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -4,12 +4,9 @@ Defines the base logic for nodes used within core. import logging import os -import random import shutil import socket -import string import threading -from builtins import range from socket import AF_INET, AF_INET6 from core import utils @@ -18,8 +15,8 @@ from core.emulator.data import LinkData, NodeData from core.emulator.enumerations import LinkTypes, NodeTypes from core.errors import CoreCommandError from core.nodes import client, ipaddress -from core.nodes.interface import CoreInterface, TunTap, Veth -from core.nodes.netclient import LinuxNetClient, OvsNetClient +from core.nodes.interface import TunTap, Veth +from core.nodes.netclient import get_net_client _DEFAULT_MTU = 1500 @@ -63,10 +60,8 @@ class NodeBase(object): self.opaque = None self.position = Position() - if session.options.get_config("ovs") == "True": - self.net_client = OvsNetClient(self.net_cmd) - else: - self.net_client = LinuxNetClient(self.net_cmd) + use_ovs = session.options.get_config("ovs") == "True" + self.net_client = get_net_client(use_ovs, self.net_cmd) def startup(self): """ @@ -461,15 +456,13 @@ class CoreNode(CoreNodeBase): def create_node_net_client(self, use_ovs): """ - Create a client for running network orchestration commands. + Create node network client for running network commands within the nodes + container. - :param bool use_ovs: True to use OVS bridges, False for Linux bridge - :return: network client + :param bool use_ovs: True for OVS bridges, False for Linux bridges + :return:node network client """ - if use_ovs: - return OvsNetClient(self.node_net_cmd) - else: - return LinuxNetClient(self.node_net_cmd) + return get_net_client(use_ovs, self.node_net_cmd) def alive(self): """ @@ -675,11 +668,7 @@ class CoreNode(CoreNodeBase): raise ValueError("interface name (%s) too long" % name) veth = Veth( - node=self, - name=name, - localname=localname, - start=self.up, - server=self.server, + self.session, self, name, localname, start=self.up, server=self.server ) if self.up: @@ -732,7 +721,7 @@ class CoreNode(CoreNodeBase): sessionid = self.session.short_session_id() localname = "tap%s.%s.%s" % (self.id, ifindex, sessionid) name = ifname - tuntap = TunTap(node=self, name=name, localname=localname, start=self.up) + tuntap = TunTap(self.session, self, name, localname, start=self.up) try: self.addnetif(tuntap, ifindex) @@ -849,35 +838,6 @@ class CoreNode(CoreNodeBase): self.ifup(ifindex) return ifindex - def connectnode(self, ifname, othernode, otherifname): - """ - Connect a node. - - :param str ifname: name of interface to connect - :param core.nodes.base.CoreNode othernode: node to connect to - :param str otherifname: interface name to connect to - :return: nothing - """ - tmplen = 8 - tmp1 = "tmp." + "".join( - [random.choice(string.ascii_lowercase) for _ in range(tmplen)] - ) - tmp2 = "tmp." + "".join( - [random.choice(string.ascii_lowercase) for _ in range(tmplen)] - ) - self.net_client.create_veth(tmp1, tmp2) - self.net_client.device_ns(tmp1, str(self.pid)) - self.node_net_client.device_name(tmp1, ifname) - interface = CoreInterface(node=self, name=ifname, mtu=_DEFAULT_MTU) - self.addnetif(interface, self.newifindex()) - - self.net_client.device_ns(tmp2, str(othernode.pid)) - othernode.node_net_client.device_name(tmp2, otherifname) - other_interface = CoreInterface( - node=othernode, name=otherifname, mtu=_DEFAULT_MTU - ) - othernode.addnetif(other_interface, othernode.newifindex()) - def addfile(self, srcname, filename): """ Add a file. diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index 2679704f..b91e987e 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -7,7 +7,7 @@ from core import utils from core.emulator.enumerations import NodeTypes from core.errors import CoreCommandError from core.nodes.base import CoreNode -from core.nodes.netclient import LinuxNetClient, OvsNetClient +from core.nodes.netclient import get_net_client class DockerClient(object): @@ -107,10 +107,14 @@ class DockerNode(CoreNode): super(DockerNode, self).__init__(session, _id, name, nodedir, bootsh, start) def create_node_net_client(self, use_ovs): - if use_ovs: - return OvsNetClient(self.nsenter_cmd) - else: - return LinuxNetClient(self.nsenter_cmd) + """ + Create node network client for running network commands within the nodes + container. + + :param bool use_ovs: True for OVS bridges, False for Linux bridges + :return:node network client + """ + return get_net_client(use_ovs, self.nsenter_cmd) def alive(self): """ diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index bfcb8583..a6e04eb5 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -8,7 +8,7 @@ from builtins import int, range from core import utils from core.errors import CoreCommandError -from core.nodes.netclient import LinuxNetClient +from core.nodes.netclient import get_net_client class CoreInterface(object): @@ -16,17 +16,18 @@ class CoreInterface(object): Base class for network interfaces. """ - def __init__(self, node, name, mtu, server=None): + def __init__(self, session, node, name, mtu, server=None): """ Creates a PyCoreNetIf instance. + :param core.emulator.session.Session session: core session instance :param core.nodes.base.CoreNode node: node for interface :param str name: interface name - :param mtu: mtu value + :param int mtu: mtu value :param core.emulator.distributed.DistributedServer server: remote server node will run on, default is None for localhost """ - + self.session = session self.node = node self.name = name if not isinstance(mtu, int): @@ -45,7 +46,8 @@ class CoreInterface(object): # index used to find flow data self.flow_id = None self.server = server - self.net_client = LinuxNetClient(self.net_cmd) + use_ovs = session.options.get_config("ovs") == "True" + self.net_client = get_net_client(use_ovs, self.net_cmd) def net_cmd(self, args, env=None, cwd=None, wait=True): """ @@ -211,21 +213,24 @@ class Veth(CoreInterface): Provides virtual ethernet functionality for core nodes. """ - def __init__(self, node, name, localname, mtu=1500, server=None, start=True): + def __init__( + self, session, node, name, localname, mtu=1500, server=None, start=True + ): """ Creates a VEth instance. + :param core.emulator.session.Session session: core session instance :param core.nodes.base.CoreNode node: related core node :param str name: interface name :param str localname: interface local name - :param mtu: interface mtu + :param int mtu: interface mtu :param core.emulator.distributed.DistributedServer server: remote server node will run on, default is None for localhost :param bool start: start flag :raises CoreCommandError: when there is a command exception """ # note that net arg is ignored - CoreInterface.__init__(self, node, name, mtu, server) + CoreInterface.__init__(self, session, node, name, mtu, server) self.localname = localname self.up = False if start: @@ -271,19 +276,22 @@ class TunTap(CoreInterface): TUN/TAP virtual device in TAP mode """ - def __init__(self, node, name, localname, mtu=1500, server=None, start=True): + def __init__( + self, session, node, name, localname, mtu=1500, server=None, start=True + ): """ Create a TunTap instance. + :param core.emulator.session.Session session: core session instance :param core.nodes.base.CoreNode node: related core node :param str name: interface name :param str localname: local interface name - :param mtu: interface mtu + :param int mtu: interface mtu :param core.emulator.distributed.DistributedServer server: remote server node will run on, default is None for localhost :param bool start: start flag """ - CoreInterface.__init__(self, node, name, mtu, server) + CoreInterface.__init__(self, session, node, name, mtu, server) self.localname = localname self.up = False self.transport_type = "virtual" @@ -466,8 +474,7 @@ class GreTap(CoreInterface): will run on, default is None for localhost :raises CoreCommandError: when there is a command exception """ - CoreInterface.__init__(self, node, name, mtu, server) - self.session = session + CoreInterface.__init__(self, session, node, name, mtu, server) if _id is None: # from PyCoreObj _id = ((id(self) >> 16) ^ (id(self) & 0xFFFF)) & 0xFFFF diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 6de5d698..9234bef5 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -7,6 +7,20 @@ import os from core.constants import BRCTL_BIN, ETHTOOL_BIN, IP_BIN, OVS_BIN, TC_BIN +def get_net_client(use_ovs, run): + """ + Retrieve desired net client for running network commands. + + :param bool use_ovs: True for OVS bridges, False for Linux bridges + :param func run: function used to run net client commands + :return: net client class + """ + if use_ovs: + return OvsNetClient(run) + else: + return LinuxNetClient(run) + + class LinuxNetClient(object): """ Client for creating Linux bridges and ip interfaces for nodes. diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index f25c800c..931622bb 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -16,6 +16,7 @@ from core.errors import CoreCommandError, CoreError from core.nodes import ipaddress from core.nodes.base import CoreNetworkBase from core.nodes.interface import GreTap, Veth +from core.nodes.netclient import get_net_client ebtables_lock = threading.Lock() @@ -558,7 +559,7 @@ class CoreNetwork(CoreNetworkBase): if len(name) >= 16: raise ValueError("interface name %s too long" % name) - netif = Veth(node=None, name=name, localname=localname, mtu=1500, start=self.up) + netif = Veth(self.session, None, name, localname, start=self.up) self.attach(netif) if net.up: # this is similar to net.attach() but uses netif.name instead of localname @@ -766,6 +767,24 @@ class CtrlNet(CoreNetwork): self.serverintf = serverintf CoreNetwork.__init__(self, session, _id, name, start, server) + def add_addresses(self, address): + """ + Add addresses used for created control networks, + + :param core.nodes.interfaces.IpAddress address: starting address to use + :return: + """ + use_ovs = self.session.options.get_config("ovs") == "True" + current = "%s/%s" % (address, self.prefix.prefixlen) + net_client = get_net_client(use_ovs, utils.check_cmd) + net_client.create_address(self.brname, current) + for name in self.session.servers: + server = self.session.servers[name] + address -= 1 + current = "%s/%s" % (address, self.prefix.prefixlen) + net_client = get_net_client(use_ovs, server.remote_cmd) + net_client.create_address(self.brname, current) + def startup(self): """ Startup functionality for the control network. @@ -778,16 +797,14 @@ class CtrlNet(CoreNetwork): CoreNetwork.startup(self) - if self.hostid: - addr = self.prefix.addr(self.hostid) - else: - addr = self.prefix.max_addr() - logging.info("added control network bridge: %s %s", self.brname, self.prefix) - if self.assign_address: - addrlist = ["%s/%s" % (addr, self.prefix.prefixlen)] - self.addrconfig(addrlist=addrlist) + if self.hostid and self.assign_address: + address = self.prefix.addr(self.hostid) + self.add_addresses(address) + elif self.assign_address: + address = self.prefix.max_addr() + self.add_addresses(address) if self.updown_script: logging.info( diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 9daf4f35..93e04c5e 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -246,7 +246,7 @@ class Rj45Node(CoreNodeBase, CoreInterface): will run on, default is None for localhost """ CoreNodeBase.__init__(self, session, _id, name, start, server) - CoreInterface.__init__(self, node=self, name=name, mtu=mtu) + CoreInterface.__init__(self, session, self, name, mtu, server) self.up = False self.lock = threading.RLock() self.ifindex = None From 8aef9f273feb57da549b2246b7cb880676079e63 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 16 Oct 2019 17:11:21 -0700 Subject: [PATCH 060/462] updates to clear broker from physical node --- daemon/core/emulator/session.py | 49 +++++++++++++++++---------------- daemon/core/nodes/physical.py | 43 +++++++++++++++-------------- gui/nodes.tcl | 2 +- 3 files changed, 48 insertions(+), 46 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 1445cb8f..e864ec8b 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -153,7 +153,7 @@ class Session(object): self.services.default_services = { "mdr": ("zebra", "OSPFv3MDR", "IPForward"), "PC": ("DefaultRoute",), - "prouter": ("zebra", "OSPFv2", "OSPFv3", "IPForward"), + "prouter": (), "router": ("zebra", "OSPFv2", "OSPFv3", "IPForward"), "host": ("DefaultRoute", "SSH"), } @@ -192,32 +192,33 @@ class Session(object): for name in self.servers: server = self.servers[name] - host = server.host - key = self.tunnelkey(node_id, IpAddress.to_int(host)) + self.create_gre_tunnel(node, server) - # local to server - logging.info( - "local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key - ) - local_tap = GreTap(session=self, remoteip=host, key=key) - local_tap.net_client.create_interface(node.brname, local_tap.localname) + def create_gre_tunnel(self, node, server): + host = server.host + key = self.tunnelkey(node.id, IpAddress.to_int(host)) + tunnel = self.tunnels.get(key) + if tunnel is not None: + return tunnel - # server to local - logging.info( - "remote tunnel node(%s) to local(%s) key(%s)", - node.name, - self.address, - key, - ) - remote_tap = GreTap( - session=self, remoteip=self.address, key=key, server=server - ) - remote_tap.net_client.create_interface( - node.brname, remote_tap.localname - ) + # local to server + logging.info( + "local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key + ) + local_tap = GreTap(session=self, remoteip=host, key=key) + local_tap.net_client.create_interface(node.brname, local_tap.localname) - # save tunnels for shutdown - self.tunnels[key] = (local_tap, remote_tap) + # server to local + logging.info( + "remote tunnel node(%s) to local(%s) key(%s)", node.name, self.address, key + ) + remote_tap = GreTap(session=self, remoteip=self.address, key=key, server=server) + remote_tap.net_client.create_interface(node.brname, remote_tap.localname) + + # save tunnels for shutdown + tunnel = (local_tap, remote_tap) + self.tunnels[key] = tunnel + return tunnel def tunnelkey(self, n1_id, n2_id): """ diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 93e04c5e..ecbcf368 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -9,15 +9,19 @@ import threading from core import utils from core.constants import MOUNT_BIN, UMOUNT_BIN from core.emulator.enumerations import NodeTypes -from core.errors import CoreCommandError +from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNodeBase from core.nodes.interface import CoreInterface from core.nodes.network import CoreNetwork, GreTap class PhysicalNode(CoreNodeBase): - def __init__(self, session, _id=None, name=None, nodedir=None, start=True): - CoreNodeBase.__init__(self, session, _id, name, start=start) + def __init__( + self, session, _id=None, name=None, nodedir=None, start=True, server=None + ): + CoreNodeBase.__init__(self, session, _id, name, start, server) + if not self.server: + raise CoreError("physical nodes must be assigned to a remote server") self.nodedir = nodedir self.up = start self.lock = threading.RLock() @@ -86,7 +90,6 @@ class PhysicalNode(CoreNodeBase): def adoptnetif(self, netif, ifindex, hwaddr, addrlist): """ - The broker builds a GreTap tunnel device to this physical node. When a link message is received linking this node to another part of the emulation, no new interface is created; instead, adopt the GreTap netif as the node interface. @@ -157,26 +160,21 @@ class PhysicalNode(CoreNodeBase): if ifindex is None: ifindex = self.newifindex() - if self.up: - # this is reached when this node is linked to a network node - # tunnel to net not built yet, so build it now and adopt it - gt = self.session.broker.addnettunnel(net.id) - if gt is None or len(gt) != 1: - raise ValueError( - "error building tunnel from adding a new network interface: %s" % gt - ) - gt = gt[0] - net.detach(gt) - self.adoptnetif(gt, ifindex, hwaddr, addrlist) - return ifindex - - # this is reached when configuring services (self.up=False) if ifname is None: ifname = "gt%d" % ifindex - netif = GreTap(node=self, name=ifname, session=self.session, start=False) - self.adoptnetif(netif, ifindex, hwaddr, addrlist) - return ifindex + if self.up: + # this is reached when this node is linked to a network node + # tunnel to net not built yet, so build it now and adopt it + _, remote_tap = self.session.create_gre_tunnel(net, self.server) + # net.detach(remote_tap) + self.adoptnetif(remote_tap, ifindex, hwaddr, addrlist) + return ifindex + else: + # this is reached when configuring services (self.up=False) + netif = GreTap(node=self, name=ifname, session=self.session, start=False) + self.adoptnetif(netif, ifindex, hwaddr, addrlist) + return ifindex def privatedir(self, path): if path[0] != "/": @@ -223,6 +221,9 @@ class PhysicalNode(CoreNodeBase): os.chmod(node_file.name, mode) logging.info("created nodefile: '%s'; mode: 0%o", node_file.name, mode) + def node_net_cmd(self, args, wait=True): + return self.net_cmd(args, wait=wait) + class Rj45Node(CoreNodeBase, CoreInterface): """ diff --git a/gui/nodes.tcl b/gui/nodes.tcl index 00e52c5d..c8645f03 100644 --- a/gui/nodes.tcl +++ b/gui/nodes.tcl @@ -19,7 +19,7 @@ array set g_node_types_default { 4 {mdr mdr.gif mdr.gif {zebra OSPFv3MDR IPForward} \ netns {built-in type for wireless routers}} 5 {prouter router_green.gif router_green.gif \ - {zebra OSPFv2 OSPFv3 IPForward} \ + {} \ physical {built-in type for physical nodes}} } From 009ce8143efec337cb6afd22b27afd5217d12a11 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 16 Oct 2019 20:19:51 -0700 Subject: [PATCH 061/462] removed lock for distributed commands and limited usage to uploads --- daemon/core/emulator/distributed.py | 13 ++++++------- daemon/core/nodes/physical.py | 1 - 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index 4e258937..2df33541 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -53,16 +53,15 @@ class DistributedServer(object): "remote cmd server(%s) cwd(%s) wait(%s): %s", self.host, cwd, wait, cmd ) try: - with self.lock: - if cwd is None: + if cwd is None: + result = self.conn.run( + cmd, hide=False, env=env, replace_env=replace_env + ) + else: + with self.conn.cd(cwd): result = self.conn.run( cmd, hide=False, env=env, replace_env=replace_env ) - else: - with self.conn.cd(cwd): - result = self.conn.run( - cmd, hide=False, env=env, replace_env=replace_env - ) return result.stdout.strip() except UnexpectedExit as e: stdout, stderr = e.streams_for_display() diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index ecbcf368..37a2eb54 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -167,7 +167,6 @@ class PhysicalNode(CoreNodeBase): # this is reached when this node is linked to a network node # tunnel to net not built yet, so build it now and adopt it _, remote_tap = self.session.create_gre_tunnel(net, self.server) - # net.detach(remote_tap) self.adoptnetif(remote_tap, ifindex, hwaddr, addrlist) return ifindex else: From 774dd8330cd81c0a72ecdc7cbafddfe70427b1a6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 16 Oct 2019 20:26:14 -0700 Subject: [PATCH 062/462] removed broker.py --- daemon/core/api/tlv/broker.py | 1147 --------------------------------- 1 file changed, 1147 deletions(-) delete mode 100644 daemon/core/api/tlv/broker.py diff --git a/daemon/core/api/tlv/broker.py b/daemon/core/api/tlv/broker.py deleted file mode 100644 index d71d050c..00000000 --- a/daemon/core/api/tlv/broker.py +++ /dev/null @@ -1,1147 +0,0 @@ -""" -Broker class that is part of the session object. Handles distributing parts of the emulation out to -other emulation servers. The broker is consulted when handling messages to determine if messages -should be handled locally or forwarded on to another emulation server. -""" - -import logging -import os -import select -import socket -import threading - -from core import utils -from core.api.tlv import coreapi -from core.emane.nodes import EmaneNet -from core.emulator.enumerations import ( - ConfigDataTypes, - ConfigFlags, - ConfigTlvs, - EventTlvs, - EventTypes, - ExecuteTlvs, - FileTlvs, - LinkTlvs, - MessageFlags, - MessageTypes, - NodeTlvs, - NodeTypes, - RegisterTlvs, -) -from core.nodes.base import CoreNetworkBase, CoreNodeBase -from core.nodes.interface import GreTap -from core.nodes.ipaddress import IpAddress -from core.nodes.network import CtrlNet, GreTapBridge -from core.nodes.physical import PhysicalNode - - -class CoreDistributedServer(object): - """ - Represents CORE daemon servers for communication. - """ - - def __init__(self, name, host, port): - """ - Creates a CoreServer instance. - - :param str name: name of the CORE server - :param str host: server address - :param int port: server port - """ - self.name = name - self.host = host - self.port = port - self.sock = None - self.instantiation_complete = False - - def connect(self): - """ - Connect to CORE server and save connection. - - :return: nothing - """ - if self.sock: - raise ValueError("socket already connected") - - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - try: - sock.connect((self.host, self.port)) - except IOError as e: - sock.close() - raise e - - self.sock = sock - - def close(self): - """ - Close connection with CORE server. - - :return: nothing - """ - if self.sock is not None: - self.sock.close() - self.sock = None - - -class CoreBroker(object): - """ - Helps with brokering messages between CORE daemon servers. - """ - - # configurable manager name - name = "broker" - - # configurable manager type - config_type = RegisterTlvs.UTILITY.value - - def __init__(self, session): - """ - Creates a CoreBroker instance. - - :param core.emulator.session.Session session: session this manager is tied to - :return: nothing - """ - - # ConfigurableManager.__init__(self) - self.session = session - self.session_clients = [] - self.session_id_master = None - self.myip = None - # dict containing tuples of (host, port, sock) - self.servers = {} - self.servers_lock = threading.Lock() - self.addserver("localhost", None, None) - # dict containing node number to server name mapping - self.nodemap = {} - # this lock also protects self.nodecounts - self.nodemap_lock = threading.Lock() - # reference counts of nodes on servers - self.nodecounts = {} - # set of node numbers that are link-layer nodes (networks) - self.network_nodes = set() - # set of node numbers that are PhysicalNode nodes - self.physical_nodes = set() - # allows for other message handlers to process API messages (e.g. EMANE) - self.handlers = set() - # dict with tunnel key to tunnel device mapping - self.tunnels = {} - self.dorecvloop = False - self.recvthread = None - self.bootcount = 0 - - def startup(self): - """ - Build tunnels between network-layer nodes now that all node - and link information has been received; called when session - enters the instantation state. - """ - self.addnettunnels() - self.writeservers() - - def shutdown(self): - """ - Close all active sockets; called when the session enters the - data collect state - """ - self.reset() - with self.servers_lock: - while len(self.servers) > 0: - name, server = self.servers.popitem() - if server.sock is not None: - logging.info( - "closing connection with %s: %s:%s", - name, - server.host, - server.port, - ) - server.close() - self.dorecvloop = False - if self.recvthread is not None: - self.recvthread.join() - - def reset(self): - """ - Reset to initial state. - """ - logging.debug("broker reset") - self.nodemap_lock.acquire() - self.nodemap.clear() - for server in self.nodecounts: - count = self.nodecounts[server] - if count < 1: - self.delserver(server) - self.nodecounts.clear() - self.bootcount = 0 - self.nodemap_lock.release() - self.network_nodes.clear() - self.physical_nodes.clear() - while len(self.tunnels) > 0: - _key, gt = self.tunnels.popitem() - gt.shutdown() - - def startrecvloop(self): - """ - Spawn the receive loop for receiving messages. - """ - if self.recvthread is not None: - logging.info("server receive loop already started") - if self.recvthread.isAlive(): - return - else: - self.recvthread.join() - # start reading data from connected sockets - logging.info("starting server receive loop") - self.dorecvloop = True - self.recvthread = threading.Thread(target=self.recvloop) - self.recvthread.daemon = True - self.recvthread.start() - - def recvloop(self): - """ - Receive loop for receiving messages from server sockets. - """ - self.dorecvloop = True - # note: this loop continues after emulation is stopped, - # even with 0 servers - while self.dorecvloop: - rlist = [] - with self.servers_lock: - # build a socket list for select call - for name in self.servers: - server = self.servers[name] - if server.sock is not None: - rlist.append(server.sock) - r, _w, _x = select.select(rlist, [], [], 1.0) - for sock in r: - server = self.getserverbysock(sock) - logging.info( - "attempting to receive from server: peer:%s remote:%s", - server.sock.getpeername(), - server.sock.getsockname(), - ) - if server is None: - # servers may have changed; loop again - continue - rcvlen = self.recv(server) - if rcvlen == 0: - logging.info( - "connection with server(%s) closed: %s:%s", - server.name, - server.host, - server.port, - ) - - def recv(self, server): - """ - Receive data on an emulation server socket and broadcast it to - all connected session handlers. Returns the length of data recevied - and forwarded. Return value of zero indicates the socket has closed - and should be removed from the self.servers dict. - - :param CoreDistributedServer server: server to receive from - :return: message length - :rtype: int - """ - msghdr = server.sock.recv(coreapi.CoreMessage.header_len) - if len(msghdr) == 0: - # server disconnected - logging.info("server disconnected, closing server") - server.close() - return 0 - - if len(msghdr) != coreapi.CoreMessage.header_len: - logging.warning( - "warning: broker received not enough data len=%s", len(msghdr) - ) - return len(msghdr) - - msgtype, msgflags, msglen = coreapi.CoreMessage.unpack_header(msghdr) - msgdata = server.sock.recv(msglen) - data = msghdr + msgdata - count = None - logging.debug("received message type: %s", MessageTypes(msgtype)) - # snoop exec response for remote interactive TTYs - if msgtype == MessageTypes.EXECUTE.value and msgflags & MessageFlags.TTY.value: - data = self.fixupremotetty(msghdr, msgdata, server.host) - logging.debug("created remote tty message: %s", data) - elif msgtype == MessageTypes.NODE.value: - # snoop node delete response to decrement node counts - if msgflags & MessageFlags.DELETE.value: - msg = coreapi.CoreNodeMessage(msgflags, msghdr, msgdata) - nodenum = msg.get_tlv(NodeTlvs.NUMBER.value) - if nodenum is not None: - count = self.delnodemap(server, nodenum) - elif msgtype == MessageTypes.LINK.value: - # this allows green link lines for remote WLANs - msg = coreapi.CoreLinkMessage(msgflags, msghdr, msgdata) - self.session.sdt.handle_distributed(msg) - elif msgtype == MessageTypes.EVENT.value: - msg = coreapi.CoreEventMessage(msgflags, msghdr, msgdata) - eventtype = msg.get_tlv(EventTlvs.TYPE.value) - if eventtype == EventTypes.INSTANTIATION_COMPLETE.value: - server.instantiation_complete = True - if self.instantiation_complete(): - self.session.check_runtime() - else: - logging.error("unknown message type received: %s", msgtype) - - try: - for session_client in self.session_clients: - session_client.sendall(data) - except IOError: - logging.exception("error sending message") - - if count is not None and count < 1: - return 0 - else: - return len(data) - - def addserver(self, name, host, port): - """ - Add a new server, and try to connect to it. If we"re already connected to this - (host, port), then leave it alone. When host,port is None, do not try to connect. - - :param str name: name of server - :param str host: server address - :param int port: server port - :return: nothing - """ - with self.servers_lock: - server = self.servers.get(name) - if server is not None: - if ( - host == server.host - and port == server.port - and server.sock is not None - ): - # leave this socket connected - return - - logging.debug( - "closing connection with %s @ %s:%s", name, server.host, server.port - ) - server.close() - del self.servers[name] - - logging.debug("adding broker server(%s): %s:%s", name, host, port) - server = CoreDistributedServer(name, host, port) - if host is not None and port is not None: - try: - server.connect() - except IOError: - logging.exception( - "error connecting to server(%s): %s:%s", name, host, port - ) - if server.sock is not None: - self.startrecvloop() - self.servers[name] = server - - def delserver(self, server): - """ - Remove a server and hang up any connection. - - :param CoreDistributedServer server: server to delete - :return: nothing - """ - with self.servers_lock: - try: - s = self.servers.pop(server.name) - if s != server: - raise ValueError("server removed was not the server provided") - except KeyError: - logging.exception("error deleting server") - - if server.sock is not None: - logging.info( - "closing connection with %s @ %s:%s", - server.name, - server.host, - server.port, - ) - server.close() - - def getserverbyname(self, name): - """ - Return the server object having the given name, or None. - - :param str name: name of server to retrieve - :return: server for given name - :rtype: CoreDistributedServer - """ - with self.servers_lock: - return self.servers.get(name) - - def getserverbysock(self, sock): - """ - Return the server object corresponding to the given socket, or None. - - :param sock: socket associated with a server - :return: core server associated wit the socket - :rtype: CoreDistributedServer - """ - with self.servers_lock: - for name in self.servers: - server = self.servers[name] - if server.sock == sock: - return server - return None - - def getservers(self): - """ - Return a list of servers sorted by name. - - :return: sorted server list - :rtype: list - """ - with self.servers_lock: - return sorted(self.servers.values(), key=lambda x: x.name) - - def getservernames(self): - """ - Return a sorted list of server names (keys from self.servers). - - :return: sorted server names - :rtype: list - """ - with self.servers_lock: - return sorted(self.servers.keys()) - - def tunnelkey(self, n1num, n2num): - """ - Compute a 32-bit key used to uniquely identify a GRE tunnel. - The hash(n1num), hash(n2num) values are used, so node numbers may be - None or string values (used for e.g. "ctrlnet"). - - :param int n1num: node one id - :param int n2num: node two id - :return: tunnel key for the node pair - :rtype: int - """ - logging.debug("creating tunnel key for: %s, %s", n1num, n2num) - sid = self.session_id_master - if sid is None: - # this is the master session - sid = self.session.id - - key = (sid << 16) ^ utils.hashkey(n1num) ^ (utils.hashkey(n2num) << 8) - return key & 0xFFFFFFFF - - def addtunnel(self, remoteip, n1num, n2num, localnum): - """ - Adds a new GreTapBridge between nodes on two different machines. - - :param str remoteip: remote address for tunnel - :param int n1num: node one id - :param int n2num: node two id - :param int localnum: local id - :return: nothing - """ - key = self.tunnelkey(n1num, n2num) - if localnum == n2num: - remotenum = n1num - else: - remotenum = n2num - - if key in self.tunnels.keys(): - logging.warning( - "tunnel with key %s (%s-%s) already exists!", key, n1num, n2num - ) - else: - _id = key & ((1 << 16) - 1) - logging.info( - "adding tunnel for %s-%s to %s with key %s", n1num, n2num, remoteip, key - ) - if localnum in self.physical_nodes: - # no bridge is needed on physical nodes; use the GreTap directly - gt = GreTap( - node=None, - name=None, - session=self.session, - remoteip=remoteip, - key=key, - ) - else: - gt = self.session.create_node( - cls=GreTapBridge, - _id=_id, - policy="ACCEPT", - remoteip=remoteip, - key=key, - ) - gt.localnum = localnum - gt.remotenum = remotenum - self.tunnels[key] = gt - - def addnettunnels(self): - """ - Add GreTaps between network devices on different machines. - The GreTapBridge is not used since that would add an extra bridge. - """ - logging.debug("adding network tunnels for nodes: %s", self.network_nodes) - for n in self.network_nodes: - self.addnettunnel(n) - - def addnettunnel(self, node_id): - """ - Add network tunnel between node and broker. - - :param int node_id: node id of network to add tunnel to - :return: list of gre taps - :rtype: list - :raises core.CoreError: when node to add net tunnel to does not exist - """ - net = self.session.get_node(node_id) - logging.debug("adding net tunnel for: id(%s) %s", node_id, net.name) - - # add other nets here that do not require tunnels - if isinstance(net, EmaneNet): - logging.debug("emane network does not require a tunnel") - return None - - server_interface = getattr(net, "serverintf", None) - if isinstance(net, CtrlNet) and server_interface is not None: - logging.debug( - "control networks with server interfaces do not need a tunnel" - ) - return None - - servers = self.getserversbynode(node_id) - if len(servers) < 2: - logging.debug("not enough servers to create a tunnel for node: %s", node_id) - return None - - hosts = [] - for server in servers: - if server.host is None: - continue - logging.debug("adding server host for net tunnel: %s", server.host) - hosts.append(server.host) - - if len(hosts) == 0: - for session_client in self.session_clients: - # get IP address from API message sender (master) - if session_client.client_address != "": - address = session_client.client_address[0] - logging.debug("adding session_client host: %s", address) - hosts.append(address) - - r = [] - for host in hosts: - if self.myip: - # we are the remote emulation server - myip = self.myip - else: - # we are the session master - myip = host - key = self.tunnelkey(node_id, IpAddress.to_int(myip)) - if key in self.tunnels.keys(): - logging.debug( - "tunnel already exists, returning existing tunnel: %s", key - ) - gt = self.tunnels[key] - r.append(gt) - continue - logging.info( - "adding tunnel for net %s to %s with key %s", node_id, host, key - ) - gt = GreTap( - node=None, name=None, session=self.session, remoteip=host, key=key - ) - self.tunnels[key] = gt - r.append(gt) - # attaching to net will later allow gt to be destroyed - # during net.shutdown() - net.attach(gt) - - return r - - def deltunnel(self, n1num, n2num): - """ - Delete tunnel between nodes. - - :param int n1num: node one id - :param int n2num: node two id - :return: nothing - """ - key = self.tunnelkey(n1num, n2num) - try: - logging.info( - "deleting tunnel between %s - %s with key: %s", n1num, n2num, key - ) - gt = self.tunnels.pop(key) - except KeyError: - gt = None - if gt: - self.session.delete_node(gt.id) - del gt - - def gettunnel(self, n1num, n2num): - """ - Return the GreTap between two nodes if it exists. - - :param int n1num: node one id - :param int n2num: node two id - :return: gre tap between nodes or none - """ - key = self.tunnelkey(n1num, n2num) - logging.debug("checking for tunnel(%s) in: %s", key, self.tunnels.keys()) - if key in self.tunnels.keys(): - return self.tunnels[key] - else: - return None - - def addnodemap(self, server, nodenum): - """ - Record a node number to emulation server mapping. - - :param CoreDistributedServer server: core server to associate node with - :param int nodenum: node id - :return: nothing - """ - with self.nodemap_lock: - if nodenum in self.nodemap: - if server in self.nodemap[nodenum]: - return - self.nodemap[nodenum].add(server) - else: - self.nodemap[nodenum] = {server} - - if server in self.nodecounts: - self.nodecounts[server] += 1 - else: - self.nodecounts[server] = 1 - - def delnodemap(self, server, nodenum): - """ - Remove a node number to emulation server mapping. - Return the number of nodes left on this server. - - :param CoreDistributedServer server: server to remove from node map - :param int nodenum: node id - :return: number of nodes left on server - :rtype: int - """ - count = None - with self.nodemap_lock: - if nodenum not in self.nodemap: - return count - - self.nodemap[nodenum].remove(server) - if server in self.nodecounts: - count = self.nodecounts[server] - count -= 1 - self.nodecounts[server] = count - - return count - - def getserversbynode(self, nodenum): - """ - Retrieve a set of emulation servers given a node number. - - :param int nodenum: node id - :return: core server associated with node - :rtype: set - """ - with self.nodemap_lock: - if nodenum not in self.nodemap: - return set() - return self.nodemap[nodenum] - - def addnet(self, nodenum): - """ - Add a node number to the list of link-layer nodes. - - :param int nodenum: node id to add - :return: nothing - """ - logging.debug("adding net to broker: %s", nodenum) - self.network_nodes.add(nodenum) - logging.debug("broker network nodes: %s", self.network_nodes) - - def addphys(self, nodenum): - """ - Add a node number to the list of physical nodes. - - :param int nodenum: node id to add - :return: nothing - """ - self.physical_nodes.add(nodenum) - - def handle_message(self, message): - """ - Handle an API message. Determine whether this needs to be handled - by the local server or forwarded on to another one. - Returns True when message does not need to be handled locally, - and performs forwarding if required. - Returning False indicates this message should be handled locally. - - :param core.api.coreapi.CoreMessage message: message to handle - :return: true or false for handling locally - :rtype: bool - """ - servers = set() - handle_locally = False - # Do not forward messages when in definition state - # (for e.g. configuring services) - if self.session.state == EventTypes.DEFINITION_STATE.value: - return False - - # Decide whether message should be handled locally or forwarded, or both - if message.message_type == MessageTypes.NODE.value: - handle_locally, servers = self.handlenodemsg(message) - elif message.message_type == MessageTypes.EVENT.value: - # broadcast events everywhere - servers = self.getservers() - elif message.message_type == MessageTypes.CONFIG.value: - # broadcast location and services configuration everywhere - confobj = message.get_tlv(ConfigTlvs.OBJECT.value) - if ( - confobj == "location" - or confobj == "services" - or confobj == "session" - or confobj == "all" - ): - servers = self.getservers() - elif message.message_type == MessageTypes.FILE.value: - # broadcast hook scripts and custom service files everywhere - filetype = message.get_tlv(FileTlvs.TYPE.value) - if filetype is not None and ( - filetype[:5] == "hook:" or filetype[:8] == "service:" - ): - servers = self.getservers() - if message.message_type == MessageTypes.LINK.value: - # prepare a server list from two node numbers in link message - handle_locally, servers, message = self.handlelinkmsg(message) - elif len(servers) == 0: - # check for servers based on node numbers in all messages but link - nn = message.node_numbers() - if len(nn) == 0: - return False - servers = self.getserversbynode(nn[0]) - - # allow other handlers to process this message (this is used - # by e.g. EMANE to use the link add message to keep counts of - # interfaces on other servers) - for handler in self.handlers: - handler(message) - - # perform any message forwarding - handle_locally |= self.forwardmsg(message, servers) - return not handle_locally - - def setupserver(self, servername): - """ - Send the appropriate API messages for configuring the specified emulation server. - - :param str servername: name of server to configure - :return: nothing - """ - server = self.getserverbyname(servername) - if server is None: - logging.warning("ignoring unknown server: %s", servername) - return - - if server.sock is None or server.host is None or server.port is None: - logging.info("ignoring disconnected server: %s", servername) - return - - # communicate this session"s current state to the server - tlvdata = coreapi.CoreEventTlv.pack(EventTlvs.TYPE.value, self.session.state) - msg = coreapi.CoreEventMessage.pack(0, tlvdata) - server.sock.send(msg) - - # send a Configuration message for the broker object and inform the - # server of its local name - tlvdata = b"" - tlvdata += coreapi.CoreConfigTlv.pack(ConfigTlvs.OBJECT.value, "broker") - tlvdata += coreapi.CoreConfigTlv.pack( - ConfigTlvs.TYPE.value, ConfigFlags.UPDATE.value - ) - tlvdata += coreapi.CoreConfigTlv.pack( - ConfigTlvs.DATA_TYPES.value, (ConfigDataTypes.STRING.value,) - ) - tlvdata += coreapi.CoreConfigTlv.pack( - ConfigTlvs.VALUES.value, - "%s:%s:%s" % (server.name, server.host, server.port), - ) - tlvdata += coreapi.CoreConfigTlv.pack( - ConfigTlvs.SESSION.value, "%s" % self.session.id - ) - msg = coreapi.CoreConfMessage.pack(0, tlvdata) - server.sock.send(msg) - - @staticmethod - def fixupremotetty(msghdr, msgdata, host): - """ - When an interactive TTY request comes from the GUI, snoop the reply - and add an SSH command to the appropriate remote server. - - :param msghdr: message header - :param msgdata: message data - :param str host: host address - :return: packed core execute tlv data - """ - msgtype, msgflags, _msglen = coreapi.CoreMessage.unpack_header(msghdr) - msgcls = coreapi.CLASS_MAP[msgtype] - msg = msgcls(msgflags, msghdr, msgdata) - - nodenum = msg.get_tlv(ExecuteTlvs.NODE.value) - execnum = msg.get_tlv(ExecuteTlvs.NUMBER.value) - cmd = msg.get_tlv(ExecuteTlvs.COMMAND.value) - res = msg.get_tlv(ExecuteTlvs.RESULT.value) - - tlvdata = b"" - tlvdata += coreapi.CoreExecuteTlv.pack(ExecuteTlvs.NODE.value, nodenum) - tlvdata += coreapi.CoreExecuteTlv.pack(ExecuteTlvs.NUMBER.value, execnum) - tlvdata += coreapi.CoreExecuteTlv.pack(ExecuteTlvs.COMMAND.value, cmd) - res = "ssh -X -f " + host + " xterm -e " + res - tlvdata += coreapi.CoreExecuteTlv.pack(ExecuteTlvs.RESULT.value, res) - - return coreapi.CoreExecMessage.pack(msgflags, tlvdata) - - def handlenodemsg(self, message): - """ - Determine and return the servers to which this node message should - be forwarded. Also keep track of link-layer nodes and the mapping of - nodes to servers. - - :param core.api.coreapi.CoreMessage message: message to handle - :return: boolean for handling locally and set of servers - :rtype: tuple - """ - servers = set() - handle_locally = False - serverfiletxt = None - - # snoop Node Message for emulation server TLV and record mapping - n = message.tlv_data[NodeTlvs.NUMBER.value] - - # replicate link-layer nodes on all servers - nodetype = message.get_tlv(NodeTlvs.TYPE.value) - if nodetype is not None: - try: - nodetype = NodeTypes(nodetype) - nodecls = self.session.get_node_class(nodetype) - except KeyError: - logging.warning("broker invalid node type %s", nodetype) - return handle_locally, servers - if nodecls is None: - logging.warning("broker unimplemented node type %s", nodetype) - return handle_locally, servers - if ( - issubclass(nodecls, CoreNetworkBase) - and nodetype != NodeTypes.WIRELESS_LAN.value - ): - # network node replicated on all servers; could be optimized - # don"t replicate WLANs, because ebtables rules won"t work - servers = self.getservers() - handle_locally = True - self.addnet(n) - for server in servers: - self.addnodemap(server, n) - # do not record server name for networks since network - # nodes are replicated across all server - return handle_locally, servers - elif issubclass(nodecls, CoreNodeBase): - name = message.get_tlv(NodeTlvs.NAME.value) - if name: - serverfiletxt = "%s %s %s" % (n, name, nodecls) - if issubclass(nodecls, PhysicalNode): - # remember physical nodes - self.addphys(n) - - # emulation server TLV specifies server - servername = message.get_tlv(NodeTlvs.EMULATION_SERVER.value) - server = self.getserverbyname(servername) - if server is not None: - self.addnodemap(server, n) - if server not in servers: - servers.add(server) - if serverfiletxt and self.session.master: - self.writenodeserver(serverfiletxt, server) - - # hook to update coordinates of physical nodes - if n in self.physical_nodes: - self.session.mobility.physnodeupdateposition(message) - - return handle_locally, servers - - def handlelinkmsg(self, message): - """ - Determine and return the servers to which this link message should - be forwarded. Also build tunnels between different servers or add - opaque data to the link message before forwarding. - - :param core.api.coreapi.CoreMessage message: message to handle - :return: boolean to handle locally, a set of server, and message - :rtype: tuple - """ - servers = set() - handle_locally = False - - # determine link message destination using non-network nodes - nn = message.node_numbers() - logging.debug( - "checking link nodes (%s) with network nodes (%s)", nn, self.network_nodes - ) - if nn[0] in self.network_nodes: - if nn[1] in self.network_nodes: - # two network nodes linked together - prevent loops caused by - # the automatic tunnelling - handle_locally = True - else: - servers = self.getserversbynode(nn[1]) - elif nn[1] in self.network_nodes: - servers = self.getserversbynode(nn[0]) - else: - logging.debug("link nodes are not network nodes") - servers1 = self.getserversbynode(nn[0]) - logging.debug("servers for node(%s): %s", nn[0], servers1) - servers2 = self.getserversbynode(nn[1]) - logging.debug("servers for node(%s): %s", nn[1], servers2) - # nodes are on two different servers, build tunnels as needed - if servers1 != servers2: - localn = None - if len(servers1) == 0 or len(servers2) == 0: - handle_locally = True - servers = servers1.union(servers2) - host = None - # get the IP of remote server and decide which node number - # is for a local node - for server in servers: - host = server.host - if host is None: - # server is local - handle_locally = True - if server in servers1: - localn = nn[0] - else: - localn = nn[1] - if handle_locally and localn is None: - # having no local node at this point indicates local node is - # the one with the empty server set - if len(servers1) == 0: - localn = nn[0] - elif len(servers2) == 0: - localn = nn[1] - if host is None: - host = self.getlinkendpoint(message, localn == nn[0]) - - logging.debug( - "handle locally(%s) and local node(%s)", handle_locally, localn - ) - if localn is None: - message = self.addlinkendpoints(message, servers1, servers2) - elif message.flags & MessageFlags.ADD.value: - self.addtunnel(host, nn[0], nn[1], localn) - elif message.flags & MessageFlags.DELETE.value: - self.deltunnel(nn[0], nn[1]) - handle_locally = False - else: - servers = servers1.union(servers2) - - return handle_locally, servers, message - - def addlinkendpoints(self, message, servers1, servers2): - """ - For a link message that is not handled locally, inform the remote - servers of the IP addresses used as tunnel endpoints by adding - opaque data to the link message. - - :param core.api.coreapi.CoreMessage message: message to link end points - :param servers1: - :param servers2: - :return: core link message - :rtype: coreapi.CoreLinkMessage - """ - ip1 = "" - for server in servers1: - if server.host is not None: - ip1 = server.host - break - ip2 = "" - for server in servers2: - if server.host is not None: - ip2 = server.host - break - tlvdata = message.raw_message[coreapi.CoreMessage.header_len :] - tlvdata += coreapi.CoreLinkTlv.pack(LinkTlvs.OPAQUE.value, "%s:%s" % (ip1, ip2)) - newraw = coreapi.CoreLinkMessage.pack(message.flags, tlvdata) - msghdr = newraw[: coreapi.CoreMessage.header_len] - return coreapi.CoreLinkMessage(message.flags, msghdr, tlvdata) - - def getlinkendpoint(self, msg, first_is_local): - """ - A link message between two different servers has been received, - and we need to determine the tunnel endpoint. First look for - opaque data in the link message, otherwise use the IP of the message - sender (the master server). - - :param core.api.tlv.coreapi.CoreLinkMessage msg: link message - :param bool first_is_local: is first local - :return: host address - :rtype: str - """ - host = None - opaque = msg.get_tlv(LinkTlvs.OPAQUE.value) - if opaque is not None: - if first_is_local: - host = opaque.split(":")[1] - else: - host = opaque.split(":")[0] - if host == "": - host = None - - if host is None: - for session_client in self.session_clients: - # get IP address from API message sender (master) - if session_client.client_address != "": - host = session_client.client_address[0] - break - - return host - - def handlerawmsg(self, msg): - """ - Helper to invoke message handler, using raw (packed) message bytes. - - :param msg: raw message butes - :return: should handle locally or not - :rtype: bool - """ - hdr = msg[: coreapi.CoreMessage.header_len] - msgtype, flags, _msglen = coreapi.CoreMessage.unpack_header(hdr) - msgcls = coreapi.CLASS_MAP[msgtype] - return self.handle_message( - msgcls(flags, hdr, msg[coreapi.CoreMessage.header_len :]) - ) - - def forwardmsg(self, message, servers): - """ - Forward API message to all given servers. - - Return True if an empty host/port is encountered, indicating - the message should be handled locally. - - :param core.api.coreapi.CoreMessage message: message to forward - :param list servers: server to forward message to - :return: handle locally value - :rtype: bool - """ - handle_locally = len(servers) == 0 - for server in servers: - if server.host is None and server.port is None: - # local emulation server, handle this locally - handle_locally = True - elif server.sock is None: - logging.info( - "server %s @ %s:%s is disconnected", - server.name, - server.host, - server.port, - ) - else: - logging.info( - "forwarding message to server(%s): %s:%s", - server.name, - server.host, - server.port, - ) - logging.debug("message being forwarded:\n%s", message) - server.sock.send(message.raw_message) - return handle_locally - - def writeservers(self): - """ - Write the server list to a text file in the session directory upon - startup: /tmp/pycore.nnnnn/servers - - :return: nothing - """ - servers = self.getservers() - filename = os.path.join(self.session.session_dir, "servers") - master = self.session_id_master - if master is None: - master = self.session.id - try: - with open(filename, "w") as f: - f.write("master=%s\n" % master) - for server in servers: - if server.name == "localhost": - continue - - lhost, lport = None, None - if server.sock: - lhost, lport = server.sock.getsockname() - f.write( - "%s %s %s %s %s\n" - % (server.name, server.host, server.port, lhost, lport) - ) - except IOError: - logging.exception("error writing server list to the file: %s", filename) - - def writenodeserver(self, nodestr, server): - """ - Creates a /tmp/pycore.nnnnn/nX.conf/server file having the node - and server info. This may be used by scripts for accessing nodes on - other machines, much like local nodes may be accessed via the - VnodeClient class. - - :param str nodestr: node string - :param CoreDistributedServer server: core server - :return: nothing - """ - serverstr = "%s %s %s" % (server.name, server.host, server.port) - name = nodestr.split()[1] - dirname = os.path.join(self.session.session_dir, name + ".conf") - filename = os.path.join(dirname, "server") - try: - os.makedirs(dirname) - except OSError: - # directory may already exist from previous distributed run - logging.exception("error creating directory: %s", dirname) - - try: - with open(filename, "w") as f: - f.write("%s\n%s\n" % (serverstr, nodestr)) - except IOError: - logging.exception( - "error writing server file %s for node %s", filename, name - ) - - def local_instantiation_complete(self): - """ - Set the local server"s instantiation-complete status to True. - - :return: nothing - """ - # TODO: do we really want to allow a localhost to not exist? - with self.servers_lock: - server = self.servers.get("localhost") - if server is not None: - server.instantiation_complete = True - - # broadcast out instantiate complete - tlvdata = b"" - tlvdata += coreapi.CoreEventTlv.pack( - EventTlvs.TYPE.value, EventTypes.INSTANTIATION_COMPLETE.value - ) - message = coreapi.CoreEventMessage.pack(0, tlvdata) - for session_client in self.session_clients: - session_client.sendall(message) - - def instantiation_complete(self): - """ - Return True if all servers have completed instantiation, False - otherwise. - - :return: have all server completed instantiation - :rtype: bool - """ - with self.servers_lock: - for name in self.servers: - server = self.servers[name] - if not server.instantiation_complete: - return False - return True From 7afaff8cbb28c9d7eb7b59bee780ee4af26b3d41 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 17 Oct 2019 08:41:48 -0700 Subject: [PATCH 063/462] updated requirements and setup.py to include fabric/invoke --- daemon/requirements.txt | 2 ++ daemon/setup.py.in | 2 ++ 2 files changed, 4 insertions(+) diff --git a/daemon/requirements.txt b/daemon/requirements.txt index a32f12de..d9029923 100644 --- a/daemon/requirements.txt +++ b/daemon/requirements.txt @@ -1,7 +1,9 @@ configparser==4.0.2 +fabric==2.5.0 future==0.17.1 grpcio==1.23.0 grpcio-tools==1.21.1 +invoke==1.3.0 lxml==4.4.1 protobuf==3.9.1 six==1.12.0 diff --git a/daemon/setup.py.in b/daemon/setup.py.in index 49af9cfe..3a451fb4 100644 --- a/daemon/setup.py.in +++ b/daemon/setup.py.in @@ -35,8 +35,10 @@ setup( packages=find_packages(), install_requires=[ "configparser", + "fabric", "future", "grpcio", + "invoke", "lxml", "protobuf", ], From b7dd8ddb6670933bd9f3374ffc2a3353ca8602aa Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 17 Oct 2019 09:09:03 -0700 Subject: [PATCH 064/462] fix for docker/lxd based nodes to use remote servers and example for lxd --- daemon/core/emulator/session.py | 1 + daemon/core/nodes/docker.py | 18 +++++++- daemon/core/nodes/lxd.py | 7 +++- daemon/examples/python/distributed_lxd.py | 51 +++++++++++++++++++++++ 4 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 daemon/examples/python/distributed_lxd.py diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index e864ec8b..5e3997eb 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -782,6 +782,7 @@ class Session(object): name=name, start=start, image=node_options.image, + server=server, ) else: node = self.create_node( diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index b91e987e..17d7578a 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -89,7 +89,17 @@ class DockerClient(object): class DockerNode(CoreNode): apitype = NodeTypes.DOCKER.value - def __init__(self, session, _id=None, name=None, nodedir=None, bootsh="boot.sh", start=True, image=None): + def __init__( + self, + session, + _id=None, + name=None, + nodedir=None, + bootsh="boot.sh", + start=True, + server=None, + image=None + ): """ Create a DockerNode instance. @@ -99,12 +109,16 @@ class DockerNode(CoreNode): :param str nodedir: node directory :param str bootsh: boot shell to use :param bool start: start flag + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost :param str image: image to start container with """ if image is None: image = "ubuntu" self.image = image - super(DockerNode, self).__init__(session, _id, name, nodedir, bootsh, start) + super(DockerNode, self).__init__( + session, _id, name, nodedir, bootsh, start, server + ) def create_node_net_client(self, use_ovs): """ diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index eef3dc8f..b11086e7 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -74,6 +74,7 @@ class LxcNode(CoreNode): nodedir=None, bootsh="boot.sh", start=True, + server=None, image=None, ): """ @@ -85,12 +86,16 @@ class LxcNode(CoreNode): :param str nodedir: node directory :param str bootsh: boot shell to use :param bool start: start flag + :param core.emulator.distributed.DistributedServer server: remote server node + will run on, default is None for localhost :param str image: image to start container with """ if image is None: image = "ubuntu" self.image = image - super(LxcNode, self).__init__(session, _id, name, nodedir, bootsh, start) + super(LxcNode, self).__init__( + session, _id, name, nodedir, bootsh, start, server + ) def alive(self): """ diff --git a/daemon/examples/python/distributed_lxd.py b/daemon/examples/python/distributed_lxd.py new file mode 100644 index 00000000..8bafeb7a --- /dev/null +++ b/daemon/examples/python/distributed_lxd.py @@ -0,0 +1,51 @@ +import logging +import pdb +import sys + +from core.emulator.coreemu import CoreEmu +from core.emulator.emudata import IpPrefixes, NodeOptions +from core.emulator.enumerations import EventTypes, NodeTypes + + +def main(): + address = sys.argv[1] + remote = sys.argv[2] + + # ip generator for example + prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") + + # create emulator instance for creating sessions and utility methods + coreemu = CoreEmu({"distributed_address": address}) + session = coreemu.create_session() + + # initialize distributed + server_name = "core2" + session.add_distributed(server_name, remote) + + # must be in configuration state for nodes to start, when using "node_add" below + session.set_state(EventTypes.CONFIGURATION_STATE) + + # create local node, switch, and remote nodes + options = NodeOptions(image="ubuntu:18.04") + node_one = session.add_node(_type=NodeTypes.LXC, node_options=options) + options.emulation_server = server_name + node_two = session.add_node(_type=NodeTypes.LXC, node_options=options) + + # create node interfaces and link + interface_one = prefixes.create_interface(node_one) + interface_two = prefixes.create_interface(node_two) + session.add_link(node_one.id, node_two.id, interface_one, interface_two) + + # instantiate session + session.instantiate() + + # pause script for verification + pdb.set_trace() + + # shutdown session + coreemu.shutdown() + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + main() From 0ef06a0167a43092ae25090b871eb1ad9f162a31 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 17 Oct 2019 09:32:32 -0700 Subject: [PATCH 065/462] added docs for session distributed commands --- daemon/core/emulator/session.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 5e3997eb..fe371c44 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -159,12 +159,25 @@ class Session(object): } def add_distributed(self, name, host): + """ + Add distributed server configuration. + + :param str name: distributed server name + :param str host: distributed server host address + :return: nothing + """ server = DistributedServer(name, host) self.servers[name] = server cmd = "mkdir -p %s" % self.session_dir server.remote_cmd(cmd) def shutdown_distributed(self): + """ + Shutdown logic for dealing with distributed tunnels and server session + directories. + + :return: nothing + """ # shutdown all tunnels for key in self.tunnels: tunnels = self.tunnels[key] @@ -180,7 +193,12 @@ class Session(object): # clear tunnels self.tunnels.clear() - def initialize_distributed(self): + def start_distributed(self): + """ + Start distributed network tunnels. + + :return: nothing + """ for node_id in self.nodes: node = self.nodes[node_id] @@ -195,6 +213,16 @@ class Session(object): self.create_gre_tunnel(node, server) def create_gre_tunnel(self, node, server): + """ + Create gre tunnel using a pair of gre taps between the local and remote server. + + + :param core.nodes.network.CoreNetwork node: node to create gre tunnel for + :param core.emulator.distributed.DistributedServer server: server to create + tunnel for + :return: local and remote gre taps created for tunnel + :rtype: tuple + """ host = server.host key = self.tunnelkey(node.id, IpAddress.to_int(host)) tunnel = self.tunnels.get(key) @@ -1566,7 +1594,7 @@ class Session(object): self.add_remove_control_interface(node=None, remove=False) # initialize distributed tunnels - self.initialize_distributed() + self.start_distributed() # instantiate will be invoked again upon Emane configure if self.emane.startup() == self.emane.NOT_READY: From e94a6d1afa7ec4014faa2287f79f0f2437374795 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 17 Oct 2019 11:10:59 -0700 Subject: [PATCH 066/462] separated distributed session logic into its own class to help reduce session.py size as it is already too big --- daemon/core/api/tlv/corehandlers.py | 4 +- daemon/core/emane/emanemanager.py | 22 +-- daemon/core/emulator/distributed.py | 152 ++++++++++++++++++ daemon/core/emulator/session.py | 139 ++-------------- daemon/core/nodes/network.py | 9 +- daemon/core/nodes/physical.py | 2 +- daemon/core/xml/emanexml.py | 24 +-- daemon/examples/python/distributed.py | 2 +- daemon/examples/python/distributed_emane.py | 2 +- daemon/examples/python/distributed_lxd.py | 2 +- daemon/examples/python/distributed_ptp.py | 2 +- .../examples/python/distributed_switches.py | 2 +- daemon/examples/python/distributed_wlan.py | 2 +- daemon/tests/test_gui.py | 4 +- 14 files changed, 196 insertions(+), 172 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 60ddfcca..8f995920 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -1192,9 +1192,9 @@ class CoreHandler(socketserver.BaseRequestHandler): for server in server_list: server_items = server.split(":") name, host, _ = server_items[:3] - self.session.add_distributed(name, host) + self.session.distributed.add_server(name, host) elif message_type == ConfigFlags.RESET: - self.session.shutdown_distributed() + self.session.distributed.shutdown() def handle_config_services(self, message_type, config_data): replies = [] diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index e4208189..91553b5a 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -142,9 +142,7 @@ class EmaneManager(ModelManager): args = "emane --version" emane_version = utils.check_cmd(args) logging.info("using EMANE: %s", emane_version) - for name in self.session.servers: - server = self.session.servers[name] - server.remote_cmd(args) + self.session.distributed.execute(lambda x: x.remote_cmd(args)) # load default emane models self.load_models(EMANE_MODELS) @@ -518,11 +516,11 @@ class EmaneManager(ModelManager): dev = self.get_config("eventservicedevice") emanexml.create_event_service_xml(group, port, dev, self.session.session_dir) - for name in self.session.servers: - conn = self.session.servers[name] - emanexml.create_event_service_xml( - group, port, dev, self.session.session_dir, conn + self.session.distributed.execute( + lambda x: emanexml.create_event_service_xml( + group, port, dev, self.session.session_dir, x ) + ) def startdaemons(self): """ @@ -598,9 +596,7 @@ class EmaneManager(ModelManager): emanecmd += " -f %s" % os.path.join(path, "emane.log") emanecmd += " %s" % os.path.join(path, "platform.xml") utils.check_cmd(emanecmd, cwd=path) - for name in self.session.servers: - server = self.session.servers[name] - server.remote_cmd(emanecmd, cwd=path) + self.session.distributed.execute(lambda x: x.remote_cmd(emanecmd, cwd=path)) logging.info("host emane daemon running: %s", emanecmd) def stopdaemons(self): @@ -625,10 +621,8 @@ class EmaneManager(ModelManager): try: utils.check_cmd(kill_emaned) utils.check_cmd(kill_transortd) - for name in self.session.servers: - server = self.session[name] - server.remote_cmd(kill_emaned) - server.remote_cmd(kill_transortd) + self.session.distributed.execute(lambda x: x.remote_cmd(kill_emaned)) + self.session.distributed.execute(lambda x: x.remote_cmd(kill_transortd)) except CoreCommandError: logging.exception("error shutting down emane daemons") diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index 2df33541..c6218441 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -5,12 +5,17 @@ Defines distributed server functionality. import logging import os import threading +from collections import OrderedDict from tempfile import NamedTemporaryFile from fabric import Connection from invoke import UnexpectedExit +from core import utils from core.errors import CoreCommandError +from core.nodes.interface import GreTap +from core.nodes.ipaddress import IpAddress +from core.nodes.network import CoreNetwork, CtrlNet LOCK = threading.Lock() @@ -93,3 +98,150 @@ class DistributedServer(object): temp.close() self.conn.put(temp.name, destination) os.unlink(temp.name) + + +class DistributedController(object): + def __init__(self, session): + """ + Create + + :param session: + """ + self.session = session + self.servers = OrderedDict() + self.tunnels = {} + self.address = self.session.options.get_config( + "distributed_address", default=None + ) + + def add_server(self, name, host): + """ + Add distributed server configuration. + + :param str name: distributed server name + :param str host: distributed server host address + :return: nothing + """ + server = DistributedServer(name, host) + self.servers[name] = server + cmd = "mkdir -p %s" % self.session.session_dir + server.remote_cmd(cmd) + + def execute(self, func): + """ + Convenience for executing logic against all distributed servers. + + :param func: function to run, that takes a DistributedServer as a parameter + :return: nothing + """ + for name in self.servers: + server = self.servers[name] + func(server) + + def shutdown(self): + """ + Shutdown logic for dealing with distributed tunnels and server session + directories. + + :return: nothing + """ + # shutdown all tunnels + for key in self.tunnels: + tunnels = self.tunnels[key] + for tunnel in tunnels: + tunnel.shutdown() + + # remove all remote session directories + for name in self.servers: + server = self.servers[name] + cmd = "rm -rf %s" % self.session.session_dir + server.remote_cmd(cmd) + + # clear tunnels + self.tunnels.clear() + + def start(self): + """ + Start distributed network tunnels. + + :return: nothing + """ + for node_id in self.session.nodes: + node = self.session.nodes[node_id] + + if not isinstance(node, CoreNetwork): + continue + + if isinstance(node, CtrlNet) and node.serverintf is not None: + continue + + for name in self.servers: + server = self.servers[name] + self.create_gre_tunnel(node, server) + + def create_gre_tunnel(self, node, server): + """ + Create gre tunnel using a pair of gre taps between the local and remote server. + + + :param core.nodes.network.CoreNetwork node: node to create gre tunnel for + :param core.emulator.distributed.DistributedServer server: server to create + tunnel for + :return: local and remote gre taps created for tunnel + :rtype: tuple + """ + host = server.host + key = self.tunnel_key(node.id, IpAddress.to_int(host)) + tunnel = self.tunnels.get(key) + if tunnel is not None: + return tunnel + + # local to server + logging.info( + "local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key + ) + local_tap = GreTap(session=self.session, remoteip=host, key=key) + local_tap.net_client.create_interface(node.brname, local_tap.localname) + + # server to local + logging.info( + "remote tunnel node(%s) to local(%s) key(%s)", node.name, self.address, key + ) + remote_tap = GreTap( + session=self.session, remoteip=self.address, key=key, server=server + ) + remote_tap.net_client.create_interface(node.brname, remote_tap.localname) + + # save tunnels for shutdown + tunnel = (local_tap, remote_tap) + self.tunnels[key] = tunnel + return tunnel + + def tunnel_key(self, n1_id, n2_id): + """ + Compute a 32-bit key used to uniquely identify a GRE tunnel. + The hash(n1num), hash(n2num) values are used, so node numbers may be + None or string values (used for e.g. "ctrlnet"). + + :param int n1_id: node one id + :param int n2_id: node two id + :return: tunnel key for the node pair + :rtype: int + """ + logging.debug("creating tunnel key for: %s, %s", n1_id, n2_id) + key = ( + (self.session.id << 16) ^ utils.hashkey(n1_id) ^ (utils.hashkey(n2_id) << 8) + ) + return key & 0xFFFFFFFF + + def get_tunnel(self, n1_id, n2_id): + """ + Return the GreTap between two nodes if it exists. + + :param int n1_id: node one id + :param int n2_id: node two id + :return: gre tap between nodes or None + """ + key = self.tunnel_key(n1_id, n2_id) + logging.debug("checking for tunnel key(%s) in: %s", key, self.tunnels) + return self.tunnels.get(key) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index fe371c44..d962da28 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -18,7 +18,7 @@ from core import constants, utils from core.emane.emanemanager import EmaneManager from core.emane.nodes import EmaneNet from core.emulator.data import EventData, ExceptionData, NodeData -from core.emulator.distributed import DistributedServer +from core.emulator.distributed import DistributedController from core.emulator.emudata import ( IdGen, LinkOptions, @@ -34,11 +34,9 @@ from core.location.event import EventLoop from core.location.mobility import MobilityManager from core.nodes.base import CoreNetworkBase, CoreNode, CoreNodeBase from core.nodes.docker import DockerNode -from core.nodes.interface import GreTap -from core.nodes.ipaddress import IpAddress, MacAddress +from core.nodes.ipaddress import MacAddress from core.nodes.lxd import LxcNode from core.nodes.network import ( - CoreNetwork, CtrlNet, GreTapBridge, HubNode, @@ -137,10 +135,8 @@ class Session(object): self.options.set_config(key, value) self.metadata = SessionMetaData() - # distributed servers - self.servers = {} - self.tunnels = {} - self.address = self.options.get_config("distributed_address", default=None) + # distributed support and logic + self.distributed = DistributedController(self) # initialize session feature helpers self.location = CoreLocation() @@ -158,123 +154,6 @@ class Session(object): "host": ("DefaultRoute", "SSH"), } - def add_distributed(self, name, host): - """ - Add distributed server configuration. - - :param str name: distributed server name - :param str host: distributed server host address - :return: nothing - """ - server = DistributedServer(name, host) - self.servers[name] = server - cmd = "mkdir -p %s" % self.session_dir - server.remote_cmd(cmd) - - def shutdown_distributed(self): - """ - Shutdown logic for dealing with distributed tunnels and server session - directories. - - :return: nothing - """ - # shutdown all tunnels - for key in self.tunnels: - tunnels = self.tunnels[key] - for tunnel in tunnels: - tunnel.shutdown() - - # remove all remote session directories - for name in self.servers: - server = self.servers[name] - cmd = "rm -rf %s" % self.session_dir - server.remote_cmd(cmd) - - # clear tunnels - self.tunnels.clear() - - def start_distributed(self): - """ - Start distributed network tunnels. - - :return: nothing - """ - for node_id in self.nodes: - node = self.nodes[node_id] - - if not isinstance(node, CoreNetwork): - continue - - if isinstance(node, CtrlNet) and node.serverintf is not None: - continue - - for name in self.servers: - server = self.servers[name] - self.create_gre_tunnel(node, server) - - def create_gre_tunnel(self, node, server): - """ - Create gre tunnel using a pair of gre taps between the local and remote server. - - - :param core.nodes.network.CoreNetwork node: node to create gre tunnel for - :param core.emulator.distributed.DistributedServer server: server to create - tunnel for - :return: local and remote gre taps created for tunnel - :rtype: tuple - """ - host = server.host - key = self.tunnelkey(node.id, IpAddress.to_int(host)) - tunnel = self.tunnels.get(key) - if tunnel is not None: - return tunnel - - # local to server - logging.info( - "local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key - ) - local_tap = GreTap(session=self, remoteip=host, key=key) - local_tap.net_client.create_interface(node.brname, local_tap.localname) - - # server to local - logging.info( - "remote tunnel node(%s) to local(%s) key(%s)", node.name, self.address, key - ) - remote_tap = GreTap(session=self, remoteip=self.address, key=key, server=server) - remote_tap.net_client.create_interface(node.brname, remote_tap.localname) - - # save tunnels for shutdown - tunnel = (local_tap, remote_tap) - self.tunnels[key] = tunnel - return tunnel - - def tunnelkey(self, n1_id, n2_id): - """ - Compute a 32-bit key used to uniquely identify a GRE tunnel. - The hash(n1num), hash(n2num) values are used, so node numbers may be - None or string values (used for e.g. "ctrlnet"). - - :param int n1_id: node one id - :param int n2_id: node two id - :return: tunnel key for the node pair - :rtype: int - """ - logging.debug("creating tunnel key for: %s, %s", n1_id, n2_id) - key = (self.id << 16) ^ utils.hashkey(n1_id) ^ (utils.hashkey(n2_id) << 8) - return key & 0xFFFFFFFF - - def gettunnel(self, n1_id, n2_id): - """ - Return the GreTap between two nodes if it exists. - - :param int n1_id: node one id - :param int n2_id: node two id - :return: gre tap between nodes or None - """ - key = self.tunnelkey(n1_id, n2_id) - logging.debug("checking for tunnel key(%s) in: %s", key, self.tunnels) - return self.tunnels.get(key) - @classmethod def get_node_class(cls, _type): """ @@ -324,7 +203,7 @@ class Session(object): node_two = self.get_node(node_two_id) # both node ids are provided - tunnel = self.gettunnel(node_one_id, node_two_id) + tunnel = self.distributed.get_tunnel(node_one_id, node_two_id) logging.debug("tunnel between nodes: %s", tunnel) if isinstance(tunnel, GreTapBridge): net_one = tunnel @@ -789,7 +668,7 @@ class Session(object): name = "%s%s" % (node_class.__name__, _id) # verify distributed server - server = self.servers.get(node_options.emulation_server) + server = self.distributed.servers.get(node_options.emulation_server) if node_options.emulation_server is not None and server is None: raise CoreError( "invalid distributed server: %s" % node_options.emulation_server @@ -1003,7 +882,7 @@ class Session(object): :return: nothing """ self.delete_nodes() - self.shutdown_distributed() + self.distributed.shutdown() self.del_hooks() self.emane.reset() @@ -1082,7 +961,7 @@ class Session(object): # remove and shutdown all nodes and tunnels self.delete_nodes() - self.shutdown_distributed() + self.distributed.shutdown() # remove this sessions working directory preserve = self.options.get_config("preservedir") == "1" @@ -1594,7 +1473,7 @@ class Session(object): self.add_remove_control_interface(node=None, remove=False) # initialize distributed tunnels - self.start_distributed() + self.distributed.start() # instantiate will be invoked again upon Emane configure if self.emane.startup() == self.emane.NOT_READY: diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 931622bb..98bec198 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -289,9 +289,7 @@ class CoreNetwork(CoreNetworkBase): """ logging.info("network node(%s) cmd", self.name) output = utils.check_cmd(args, env, cwd, wait) - for name in self.session.servers: - server = self.session.servers[name] - server.remote_cmd(args, env, cwd, wait) + self.session.distributed.execute(lambda x: x.remote_cmd(args, env, cwd, wait)) return output def startup(self): @@ -778,8 +776,9 @@ class CtrlNet(CoreNetwork): current = "%s/%s" % (address, self.prefix.prefixlen) net_client = get_net_client(use_ovs, utils.check_cmd) net_client.create_address(self.brname, current) - for name in self.session.servers: - server = self.session.servers[name] + servers = self.session.distributed.servers + for name in servers: + server = servers[name] address -= 1 current = "%s/%s" % (address, self.prefix.prefixlen) net_client = get_net_client(use_ovs, server.remote_cmd) diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 37a2eb54..0f9e0217 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -166,7 +166,7 @@ class PhysicalNode(CoreNodeBase): if self.up: # this is reached when this node is linked to a network node # tunnel to net not built yet, so build it now and adopt it - _, remote_tap = self.session.create_gre_tunnel(net, self.server) + _, remote_tap = self.session.distributed.create_gre_tunnel(net, self.server) self.adoptnetif(remote_tap, ifindex, hwaddr, addrlist) return ifindex else: diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index 881ff373..41319ea4 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -314,9 +314,9 @@ def build_transport_xml(emane_manager, node, transport_type): file_name = transport_file_name(node.id, transport_type) file_path = os.path.join(emane_manager.session.session_dir, file_name) create_file(transport_element, doc_name, file_path) - for name in emane_manager.session.servers: - server = emane_manager.session.servers[name] - create_file(transport_element, doc_name, file_path, server) + emane_manager.session.distributed.execute( + lambda x: create_file(transport_element, doc_name, file_path, x) + ) def create_phy_xml(emane_model, config, file_path, server): @@ -342,9 +342,9 @@ def create_phy_xml(emane_model, config, file_path, server): create_file(phy_element, "phy", file_path, server) else: create_file(phy_element, "phy", file_path) - for name in emane_model.session.servers: - server = emane_model.session.servers[name] - create_file(phy_element, "phy", file_path, server) + emane_model.session.distributed.execute( + lambda x: create_file(phy_element, "phy", file_path, x) + ) def create_mac_xml(emane_model, config, file_path, server): @@ -372,9 +372,9 @@ def create_mac_xml(emane_model, config, file_path, server): create_file(mac_element, "mac", file_path, server) else: create_file(mac_element, "mac", file_path) - for name in emane_model.session.servers: - server = emane_model.session.servers[name] - create_file(mac_element, "mac", file_path, server) + emane_model.session.distributed.execute( + lambda x: create_file(mac_element, "mac", file_path, x) + ) def create_nem_xml( @@ -410,9 +410,9 @@ def create_nem_xml( create_file(nem_element, "nem", nem_file, server) else: create_file(nem_element, "nem", nem_file) - for name in emane_model.session.servers: - server = emane_model.session.servers[name] - create_file(nem_element, "nem", nem_file, server) + emane_model.session.distributed.execute( + lambda x: create_file(nem_element, "nem", nem_file, x) + ) def create_event_service_xml(group, port, device, file_directory, server=None): diff --git a/daemon/examples/python/distributed.py b/daemon/examples/python/distributed.py index 8bcf2972..8eb23b2c 100644 --- a/daemon/examples/python/distributed.py +++ b/daemon/examples/python/distributed.py @@ -20,7 +20,7 @@ def main(): # initialize distributed server_name = "core2" - session.add_distributed(server_name, remote) + session.distributed.add_server(server_name, remote) # must be in configuration state for nodes to start, when using "node_add" below session.set_state(EventTypes.CONFIGURATION_STATE) diff --git a/daemon/examples/python/distributed_emane.py b/daemon/examples/python/distributed_emane.py index c64d1f0c..4ef50ccb 100644 --- a/daemon/examples/python/distributed_emane.py +++ b/daemon/examples/python/distributed_emane.py @@ -27,7 +27,7 @@ def main(): # initialize distributed server_name = "core2" - session.add_distributed(server_name, remote) + session.distributed.add_server(server_name, remote) # must be in configuration state for nodes to start, when using "node_add" below session.set_state(EventTypes.CONFIGURATION_STATE) diff --git a/daemon/examples/python/distributed_lxd.py b/daemon/examples/python/distributed_lxd.py index 8bafeb7a..130942ea 100644 --- a/daemon/examples/python/distributed_lxd.py +++ b/daemon/examples/python/distributed_lxd.py @@ -20,7 +20,7 @@ def main(): # initialize distributed server_name = "core2" - session.add_distributed(server_name, remote) + session.distributed.add_server(server_name, remote) # must be in configuration state for nodes to start, when using "node_add" below session.set_state(EventTypes.CONFIGURATION_STATE) diff --git a/daemon/examples/python/distributed_ptp.py b/daemon/examples/python/distributed_ptp.py index b0f27c28..62e7df64 100644 --- a/daemon/examples/python/distributed_ptp.py +++ b/daemon/examples/python/distributed_ptp.py @@ -20,7 +20,7 @@ def main(): # initialize distributed server_name = "core2" - session.add_distributed(server_name, remote) + session.distributed.add_server(server_name, remote) # must be in configuration state for nodes to start, when using "node_add" below session.set_state(EventTypes.CONFIGURATION_STATE) diff --git a/daemon/examples/python/distributed_switches.py b/daemon/examples/python/distributed_switches.py index bc13bf2c..f9b69757 100644 --- a/daemon/examples/python/distributed_switches.py +++ b/daemon/examples/python/distributed_switches.py @@ -16,7 +16,7 @@ def main(): # initialize distributed server_name = "core2" - session.add_distributed(server_name, remote) + session.distributed.add_server(server_name, remote) # must be in configuration state for nodes to start, when using "node_add" below session.set_state(EventTypes.CONFIGURATION_STATE) diff --git a/daemon/examples/python/distributed_wlan.py b/daemon/examples/python/distributed_wlan.py index f8af1f5f..10f25aa8 100644 --- a/daemon/examples/python/distributed_wlan.py +++ b/daemon/examples/python/distributed_wlan.py @@ -21,7 +21,7 @@ def main(): # initialize distributed server_name = "core2" - session.add_distributed(server_name, remote) + session.distributed.add_server(server_name, remote) # must be in configuration state for nodes to start, when using "node_add" below session.set_state(EventTypes.CONFIGURATION_STATE) diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index 02e634be..c07e2bd3 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -763,11 +763,11 @@ class TestGui: (ConfigTlvs.VALUES, "%s:%s:%s" % (server, host, port)), ], ) - coreserver.session.add_distributed = mock.MagicMock() + coreserver.session.distributed.add_server = mock.MagicMock() coreserver.request_handler.handle_message(message) - coreserver.session.add_distributed.assert_called_once_with(server, host) + coreserver.session.distributed.add_server.assert_called_once_with(server, host) def test_config_services_request_all(self, coreserver): message = coreapi.CoreConfMessage.create( From 4746fe67ef78adde410ff00e0c07f7af63e783eb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 17 Oct 2019 11:35:48 -0700 Subject: [PATCH 067/462] added docs for distributed.py --- daemon/core/emulator/distributed.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index c6218441..83576434 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -101,6 +101,10 @@ class DistributedServer(object): class DistributedController(object): + """ + Provides logic for dealing with remote tunnels and distributed servers. + """ + def __init__(self, session): """ Create From 5d5ffb70c2be8fd4d0e20d257034cf6a37c0d616 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 17 Oct 2019 13:00:20 -0700 Subject: [PATCH 068/462] update to grpc edit_node to allow editing icon and broadcasting a node update for all to listen to a change --- daemon/core/api/grpc/client.py | 5 +++-- daemon/core/api/grpc/server.py | 7 +++++-- daemon/proto/core/api/grpc/core.proto | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 2d767993..54f77fc6 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -326,19 +326,20 @@ class CoreGrpcClient(object): request = core_pb2.GetNodeRequest(session_id=session_id, node_id=node_id) return self.stub.GetNode(request) - def edit_node(self, session_id, node_id, position): + def edit_node(self, session_id, node_id, position, icon=None): """ Edit a node, currently only changes position. :param int session_id: session id :param int node_id: node id :param core_pb2.Position position: position to set node to + :param str icon: path to icon for gui to use for node :return: response with result of success or failure :rtype: core_pb2.EditNodeResponse :raises grpc.RpcError: when session or node doesn't exist """ request = core_pb2.EditNodeRequest( - session_id=session_id, node_id=node_id, position=position + session_id=session_id, node_id=node_id, position=position, icon=icon ) return self.stub.EditNode(request) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index f61b6204..3c70ed09 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -842,8 +842,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("edit node: %s", request) session = self.get_session(request.session_id, context) - node_id = request.node_id + node = self.get_node(session, request.node_id, context) node_options = NodeOptions() + node_options.icon = request.icon x = request.position.x y = request.position.y node_options.set_position(x, y) @@ -853,7 +854,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): node_options.set_location(lat, lon, alt) result = True try: - session.update_node(node_id, node_options) + session.update_node(node.id, node_options) + node_data = node.data(0) + session.broadcast_node(node_data) except CoreError: result = False return core_pb2.EditNodeResponse(result=result) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 68c83ad2..1e17b327 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -316,6 +316,7 @@ message EditNodeRequest { int32 session_id = 1; int32 node_id = 2; Position position = 3; + string icon = 4; } message EditNodeResponse { From 6edd6a7fdb43d424a23c6d91ba5c079fd57a63a2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 17 Oct 2019 14:52:31 -0700 Subject: [PATCH 069/462] first pass at removing all python2 specific dependencies, updating python requirements.txt/setup.py/Pipfiles/Makefiles, and removing python2 compat imports --- Makefile.am | 13 +-- configure.ac | 8 +- daemon/Makefile.am | 8 +- daemon/Pipfile.lock | 141 +++++++++++++---------------- daemon/core/api/tlv/coreapi.py | 4 +- daemon/core/api/tlv/structutils.py | 4 +- daemon/core/emane/commeffect.py | 3 +- daemon/core/location/event.py | 21 ++--- daemon/core/plugins/sdt.py | 3 +- daemon/core/services/nrl.py | 10 +- daemon/requirements.txt | 3 - daemon/setup.py.in | 6 -- ns3/Makefile.am | 8 +- 13 files changed, 89 insertions(+), 143 deletions(-) diff --git a/Makefile.am b/Makefile.am index ff31a5a4..c51c3707 100644 --- a/Makefile.am +++ b/Makefile.am @@ -44,15 +44,6 @@ DISTCLEANFILES = aclocal.m4 \ MAINTAINERCLEANFILES = .version \ .version.date - -if PYTHON3 -PYTHON_DEB_DEP = python3 >= 3.6 -PYTHON_RPM_DEP = python3 >= 3.6 -else -PYTHON_DEB_DEP = python (>= 2.7), python (<< 3.0) -PYTHON_RPM_DEP = python >= 2.7, python < 3.0 -endif - define fpm-rpm = fpm -s dir -t rpm -n core \ -m "$(PACKAGE_MAINTAINERS)" \ @@ -74,7 +65,7 @@ fpm -s dir -t rpm -n core \ -d "iproute" \ -d "libev" \ -d "net-tools" \ - -d "$(PYTHON_RPM_DEP)" \ + -d "python3 >= 3.6" \ -C $(DESTDIR) endef @@ -101,7 +92,7 @@ fpm -s dir -t deb -n core \ -d "ebtables" \ -d "iproute2" \ -d "libev4" \ - -d "$(PYTHON_DEB_DEP)" \ + -d "python3 >= 3.6" \ -C $(DESTDIR) endef diff --git a/configure.ac b/configure.ac index fec902a3..5d04356e 100644 --- a/configure.ac +++ b/configure.ac @@ -55,12 +55,6 @@ else want_python=no fi -AC_ARG_ENABLE([python3], - [AS_HELP_STRING([--enable-python3], - [sets python3 flag for building packages])], - [enable_python3=yes], [enable_python3=no]) -AM_CONDITIONAL([PYTHON3], [test "x$enable_python3" == "xyes"]) - AC_ARG_ENABLE([daemon], [AS_HELP_STRING([--enable-daemon[=ARG]], [build and install the daemon with Python modules @@ -116,7 +110,7 @@ if test "x$enable_daemon" = "xyes"; then AC_FUNC_REALLOC AC_CHECK_FUNCS([atexit dup2 gettimeofday memset socket strerror uname]) - AM_PATH_PYTHON(2.7) + AM_PATH_PYTHON(3.6) AS_IF([$PYTHON -m grpc_tools.protoc -h &> /dev/null], [], [AC_MSG_ERROR([please install python grpcio-tools])]) AC_CHECK_PROG(brctl_path, brctl, $as_dir, no, $SEARCHPATH) diff --git a/daemon/Makefile.am b/daemon/Makefile.am index a6503cc0..80483387 100644 --- a/daemon/Makefile.am +++ b/daemon/Makefile.am @@ -14,8 +14,6 @@ if WANT_DOCS DOCS = doc endif -PYTHONLIBDIR=$(subst site-packages,dist-packages,$(pythondir)) - SUBDIRS = proto $(DOCS) SCRIPT_FILES := $(notdir $(wildcard scripts/*)) @@ -31,7 +29,7 @@ install-exec-hook: $(PYTHON) $(SETUPPY) $(SETUPPYFLAGS) install \ --root=/$(DESTDIR) \ --prefix=$(prefix) \ - --install-lib=$(PYTHONLIBDIR) \ + --install-lib=$(pythondir) \ --single-version-externally-managed # Python package uninstall @@ -40,8 +38,8 @@ uninstall-hook: rm -rf $(DESTDIR)/$(datadir)/core rm -f $(addprefix $(DESTDIR)/$(datarootdir)/man/man1/, $(MAN_FILES)) rm -f $(addprefix $(DESTDIR)/$(bindir)/,$(SCRIPT_FILES)) - rm -rf $(DESTDIR)/$(PYTHONLIBDIR)/core-$(PACKAGE_VERSION)-py$(PYTHON_VERSION).egg-info - rm -rf $(DESTDIR)/$(PYTHONLIBDIR)/core + rm -rf $(DESTDIR)/$(pythondir)/core-$(PACKAGE_VERSION)-py$(PYTHON_VERSION).egg-info + rm -rf $(DESTDIR)/$(pythondir)/core # Python package cleanup clean-local: diff --git a/daemon/Pipfile.lock b/daemon/Pipfile.lock index 73400b8b..4bdaea3f 100644 --- a/daemon/Pipfile.lock +++ b/daemon/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d702e6eed5a1362bf261543572bbffd2e8a87140b8d8cb07b99fb0d25220a2b5" + "sha256": "6195c89ec6e2e449fcbd7f3fa41cbab79c02d952984a913e0f80114e1904bf11" }, "pipfile-spec": 6, "requires": {}, @@ -14,13 +14,6 @@ ] }, "default": { - "asn1crypto": { - "hashes": [ - "sha256:0b199f211ae690df3db4fd6c1c4ff976497fb1da689193e368eedbadc53d9292", - "sha256:bca90060bd995c3f62c4433168eab407e44bdbdb567b3f3a396a676c1a4c4a3f" - ], - "version": "==1.0.1" - }, "bcrypt": { "hashes": [ "sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89", @@ -44,43 +37,40 @@ }, "cffi": { "hashes": [ - "sha256:041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774", - "sha256:046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d", - "sha256:066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90", - "sha256:066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b", - "sha256:2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63", - "sha256:300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45", - "sha256:34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25", - "sha256:46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3", - "sha256:4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b", - "sha256:4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647", - "sha256:4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016", - "sha256:50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4", - "sha256:55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb", - "sha256:5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753", - "sha256:59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7", - "sha256:73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9", - "sha256:a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f", - "sha256:a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8", - "sha256:a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f", - "sha256:a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc", - "sha256:ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42", - "sha256:b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3", - "sha256:d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909", - "sha256:d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45", - "sha256:dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d", - "sha256:e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512", - "sha256:e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff", - "sha256:ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201" + "sha256:08f99e8b38d5134d504aa7e486af8e4fde66a2f388bbecc270cdd1e00fa09ff8", + "sha256:1112d2fc92a867a6103bce6740a549e74b1d320cf28875609f6e93857eee4f2d", + "sha256:1b9ab50c74e075bd2ae489853c5f7f592160b379df53b7f72befcbe145475a36", + "sha256:24eff2997436b6156c2f30bed215c782b1d8fd8c6a704206053c79af95962e45", + "sha256:2eff642fbc9877a6449026ad66bf37c73bf4232505fb557168ba5c502f95999b", + "sha256:362e896cea1249ed5c2a81cf6477fabd9e1a5088aa7ea08358a4c6b0998294d2", + "sha256:40eddb3589f382cb950f2dcf1c39c9b8d7bd5af20665ce273815b0d24635008b", + "sha256:5ed40760976f6b8613d4a0db5e423673ca162d4ed6c9ed92d1f4e58a47ee01b5", + "sha256:632c6112c1e914c486f06cfe3f0cc507f44aa1e00ebf732cedb5719e6aa0466a", + "sha256:64d84f0145e181f4e6cc942088603c8db3ae23485c37eeda71cb3900b5e67cb4", + "sha256:6cb4edcf87d0e7f5bdc7e5c1a0756fbb37081b2181293c5fdf203347df1cd2a2", + "sha256:6f19c9df4785305669335b934c852133faed913c0faa63056248168966f7a7d5", + "sha256:719537b4c5cd5218f0f47826dd705fb7a21d83824920088c4214794457113f3f", + "sha256:7b0e337a70e58f1a36fb483fd63880c9e74f1db5c532b4082bceac83df1523fa", + "sha256:853376efeeb8a4ae49a737d5d30f5db8cdf01d9319695719c4af126488df5a6a", + "sha256:85bbf77ffd12985d76a69d2feb449e35ecdcb4fc54a5f087d2bd54158ae5bb0c", + "sha256:8978115c6f0b0ce5880bc21c967c65058be8a15f1b81aa5fdbdcbea0e03952d1", + "sha256:8f7eec920bc83692231d7306b3e311586c2e340db2dc734c43c37fbf9c981d24", + "sha256:8fe230f612c18af1df6f348d02d682fe2c28ca0a6c3856c99599cdacae7cf226", + "sha256:92068ebc494b5f9826b822cec6569f1f47b9a446a3fef477e1d11d7fac9ea895", + "sha256:b57e1c8bcdd7340e9c9d09613b5e7fdd0c600be142f04e2cc1cc8cb7c0b43529", + "sha256:ba956c9b44646bc1852db715b4a252e52a8f5a4009b57f1dac48ba3203a7bde1", + "sha256:ca42034c11eb447497ea0e7b855d87ccc2aebc1e253c22e7d276b8599c112a27", + "sha256:dc9b2003e9a62bbe0c84a04c61b0329e86fccd85134a78d7aca373bbbf788165", + "sha256:dd308802beb4b2961af8f037becbdf01a1e85009fdfc14088614c1b3c383fae5", + "sha256:e77cd105b19b8cd721d101687fcf665fd1553eb7b57556a1ef0d453b6fc42faa", + "sha256:f56dff1bd81022f1c980754ec721fb8da56192b026f17f0f99b965da5ab4fbd2", + "sha256:fa4cc13c03ea1d0d37ce8528e0ecc988d2365e8ac64d8d86cafab4038cb4ce89", + "sha256:fa8cf1cb974a9f5911d2a0303f6adc40625c05578d8e7ff5d313e1e27850bd59", + "sha256:fb003019f06d5fc0aa4738492ad8df1fa343b8a37cbcf634018ad78575d185df", + "sha256:fd409b7778167c3bcc836484a8f49c0e0b93d3e745d975749f83aa5d18a5822f", + "sha256:fe5d65a3ee38122003245a82303d11ac05ff36531a8f5ce4bc7d4bbc012797e1" ], - "version": "==1.12.3" - }, - "configparser": { - "hashes": [ - "sha256:254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c", - "sha256:c7d282687a5308319bf3d2e7706e575c635b0a470342641c93bea0ea3b5331df" - ], - "version": "==4.0.2" + "version": "==1.13.0" }, "core": { "editable": true, @@ -88,24 +78,29 @@ }, "cryptography": { "hashes": [ - "sha256:24b61e5fcb506424d3ec4e18bca995833839bf13c59fc43e530e488f28d46b8c", - "sha256:25dd1581a183e9e7a806fe0543f485103232f940fcfc301db65e630512cce643", - "sha256:3452bba7c21c69f2df772762be0066c7ed5dc65df494a1d53a58b683a83e1216", - "sha256:41a0be220dd1ed9e998f5891948306eb8c812b512dc398e5a01846d855050799", - "sha256:5751d8a11b956fbfa314f6553d186b94aa70fdb03d8a4d4f1c82dcacf0cbe28a", - "sha256:5f61c7d749048fa6e3322258b4263463bfccefecb0dd731b6561cb617a1d9bb9", - "sha256:72e24c521fa2106f19623a3851e9f89ddfdeb9ac63871c7643790f872a305dfc", - "sha256:7b97ae6ef5cba2e3bb14256625423413d5ce8d1abb91d4f29b6d1a081da765f8", - "sha256:961e886d8a3590fd2c723cf07be14e2a91cf53c25f02435c04d39e90780e3b53", - "sha256:96d8473848e984184b6728e2c9d391482008646276c3ff084a1bd89e15ff53a1", - "sha256:ae536da50c7ad1e002c3eee101871d93abdc90d9c5f651818450a0d3af718609", - "sha256:b0db0cecf396033abb4a93c95d1602f268b3a68bb0a9cc06a7cff587bb9a7292", - "sha256:cfee9164954c186b191b91d4193989ca994703b2fff406f71cf454a2d3c7327e", - "sha256:e6347742ac8f35ded4a46ff835c60e68c22a536a8ae5c4422966d06946b6d4c6", - "sha256:f27d93f0139a3c056172ebb5d4f9056e770fdf0206c2f422ff2ebbad142e09ed", - "sha256:f57b76e46a58b63d1c6375017f4564a28f19a5ca912691fd2e4261b3414b618d" + "sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c", + "sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595", + "sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad", + "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651", + "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2", + "sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff", + "sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d", + "sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42", + "sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d", + "sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e", + "sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912", + "sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793", + "sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13", + "sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7", + "sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0", + "sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879", + "sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f", + "sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9", + "sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2", + "sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf", + "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8" ], - "version": "==2.7" + "version": "==2.8" }, "fabric": { "hashes": [ @@ -114,12 +109,6 @@ ], "version": "==2.5.0" }, - "future": { - "hashes": [ - "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8" - ], - "version": "==0.17.1" - }, "grpcio": { "hashes": [ "sha256:0302331e014fc4bac028b6ad480b33f7abfe20b9bdcca7be417124dda8f22115", @@ -282,10 +271,10 @@ }, "attrs": { "hashes": [ - "sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2", - "sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396" + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], - "version": "==19.2.0" + "version": "==19.3.0" }, "black": { "hashes": [ @@ -531,11 +520,11 @@ }, "pytest": { "hashes": [ - "sha256:13c1c9b22127a77fc684eee24791efafcef343335d855e3573791c68588fe1a5", - "sha256:d8ba7be9466f55ef96ba203fc0f90d0cf212f2f927e69186e1353e30bc7f62e5" + "sha256:7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8", + "sha256:ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0" ], "index": "pypi", - "version": "==5.2.0" + "version": "==5.2.1" }, "pyyaml": { "hashes": [ @@ -571,10 +560,10 @@ }, "virtualenv": { "hashes": [ - "sha256:680af46846662bb38c5504b78bad9ed9e4f3ba2d54f54ba42494fdf94337fe30", - "sha256:f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2" + "sha256:3e3597e89c73df9313f5566e8fc582bd7037938d15b05329c232ec57a11a7ad5", + "sha256:5d370508bf32e522d79096e8cbea3499d47e624ac7e11e9089f9397a0b3318df" ], - "version": "==16.7.5" + "version": "==16.7.6" }, "wcwidth": { "hashes": [ diff --git a/daemon/core/api/tlv/coreapi.py b/daemon/core/api/tlv/coreapi.py index 1e1de8be..ba737fd4 100644 --- a/daemon/core/api/tlv/coreapi.py +++ b/daemon/core/api/tlv/coreapi.py @@ -9,8 +9,6 @@ import socket import struct from enum import Enum -from past.builtins import basestring - from core.api.tlv import structutils from core.emulator.enumerations import ( ConfigTlvs, @@ -181,7 +179,7 @@ class CoreTlvDataString(CoreTlvData): :return: length of data packed and the packed data :rtype: tuple """ - if not isinstance(value, basestring): + if not isinstance(value, str): raise ValueError("value not a string: %s" % type(value)) value = value.encode("utf-8") diff --git a/daemon/core/api/tlv/structutils.py b/daemon/core/api/tlv/structutils.py index 28e22a27..41358848 100644 --- a/daemon/core/api/tlv/structutils.py +++ b/daemon/core/api/tlv/structutils.py @@ -4,8 +4,6 @@ Utilities for working with python struct data. import logging -from past.builtins import basestring - def pack_values(clazz, packers): """ @@ -31,7 +29,7 @@ def pack_values(clazz, packers): # only pack actual values and avoid packing empty strings # protobuf defaults to empty strings and does no imply a value to set - if value is None or (isinstance(value, basestring) and not value): + if value is None or (isinstance(value, str) and not value): continue # transform values as needed diff --git a/daemon/core/emane/commeffect.py b/daemon/core/emane/commeffect.py index 1f829b18..4ae20107 100644 --- a/daemon/core/emane/commeffect.py +++ b/daemon/core/emane/commeffect.py @@ -7,7 +7,6 @@ import os from builtins import int from lxml import etree -from past.builtins import basestring from core.config import ConfigGroup from core.emane import emanemanifest, emanemodel @@ -26,7 +25,7 @@ def convert_none(x): """ Helper to use 0 for None values. """ - if isinstance(x, basestring): + if isinstance(x, str): x = float(x) if x is None: return 0 diff --git a/daemon/core/location/event.py b/daemon/core/location/event.py index b3170083..1872ac18 100644 --- a/daemon/core/location/event.py +++ b/daemon/core/location/event.py @@ -5,8 +5,7 @@ event.py: event loop implementation using a heap queue and threads. import heapq import threading import time - -from past.builtins import cmp +from functools import total_ordering class Timer(threading.Thread): @@ -70,6 +69,7 @@ class Timer(threading.Thread): self.finished.set() +@total_ordering class Event(object): """ Provides event objects that can be used within the EventLoop class. @@ -92,18 +92,11 @@ class Event(object): self.kwds = kwds self.canceled = False - def __cmp__(self, other): - """ - Comparison function. - - :param Event other: event to compare with - :return: comparison result - :rtype: int - """ - tmp = cmp(self.time, other.time) - if tmp == 0: - tmp = cmp(self.eventnum, other.eventnum) - return tmp + def __lt__(self, other): + result = self.time < other.time + if result: + result = self.eventnum < other.eventnum + return result def run(self): """ diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 52635da3..934233e9 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -4,8 +4,7 @@ sdt.py: Scripted Display Tool (SDT3D) helper import logging import socket - -from future.moves.urllib.parse import urlparse +from urllib.parse import urlparse from core import constants from core.emane.nodes import EmaneNet diff --git a/daemon/core/services/nrl.py b/daemon/core/services/nrl.py index 23484459..a610f1cc 100644 --- a/daemon/core/services/nrl.py +++ b/daemon/core/services/nrl.py @@ -3,8 +3,6 @@ nrl.py: defines services provided by NRL protolib tools hosted here: http://www.nrl.navy.mil/itd/ncs/products """ -from past.builtins import filter - from core import utils from core.nodes.ipaddress import Ipv4Prefix from core.services.coreservices import CoreService @@ -94,7 +92,7 @@ class NrlNhdp(NrlService): cmd += " -flooding ecds" cmd += " -smfClient %s_smf" % node.name - netifs = filter(lambda x: not getattr(x, "control", False), node.netifs()) + netifs = list(filter(lambda x: not getattr(x, "control", False), node.netifs())) if len(netifs) > 0: interfacenames = map(lambda x: x.name, netifs) cmd += " -i " @@ -128,7 +126,7 @@ class NrlSmf(NrlService): cmd = "nrlsmf instance %s_smf" % node.name servicenames = map(lambda x: x.name, node.services) - netifs = filter(lambda x: not getattr(x, "control", False), node.netifs()) + netifs = list(filter(lambda x: not getattr(x, "control", False), node.netifs())) if len(netifs) == 0: return "" @@ -218,7 +216,7 @@ class NrlOlsrv2(NrlService): cmd += " -p olsr" - netifs = filter(lambda x: not getattr(x, "control", False), node.netifs()) + netifs = list(filter(lambda x: not getattr(x, "control", False), node.netifs())) if len(netifs) > 0: interfacenames = map(lambda x: x.name, netifs) cmd += " -i " @@ -246,7 +244,7 @@ class OlsrOrg(NrlService): Generate the appropriate command-line based on node interfaces. """ cmd = cls.startup[0] - netifs = filter(lambda x: not getattr(x, "control", False), node.netifs()) + netifs = list(filter(lambda x: not getattr(x, "control", False), node.netifs())) if len(netifs) > 0: interfacenames = map(lambda x: x.name, netifs) cmd += " -i " diff --git a/daemon/requirements.txt b/daemon/requirements.txt index d9029923..96fe83ca 100644 --- a/daemon/requirements.txt +++ b/daemon/requirements.txt @@ -1,9 +1,6 @@ -configparser==4.0.2 fabric==2.5.0 -future==0.17.1 grpcio==1.23.0 grpcio-tools==1.21.1 invoke==1.3.0 lxml==4.4.1 protobuf==3.9.1 -six==1.12.0 diff --git a/daemon/setup.py.in b/daemon/setup.py.in index 3a451fb4..378912d3 100644 --- a/daemon/setup.py.in +++ b/daemon/setup.py.in @@ -34,18 +34,12 @@ setup( version="@PACKAGE_VERSION@", packages=find_packages(), install_requires=[ - "configparser", "fabric", - "future", "grpcio", "invoke", "lxml", "protobuf", ], - extra_require={ - ":python_version<'3.2'": ["futures"], - ":python_version<'3.4'": ["enum34"], - }, tests_require=[ "pytest", "mock", diff --git a/ns3/Makefile.am b/ns3/Makefile.am index 115a7008..62086e82 100644 --- a/ns3/Makefile.am +++ b/ns3/Makefile.am @@ -9,8 +9,6 @@ if WANT_PYTHON -PYTHONLIBDIR=$(subst site-packages,dist-packages,$(pythondir)) - SETUPPY = setup.py SETUPPYFLAGS = -v @@ -24,15 +22,15 @@ install-exec-hook: $(PYTHON) $(SETUPPY) $(SETUPPYFLAGS) install \ --root=/$(DESTDIR) \ --prefix=$(prefix) \ - --install-lib=$(PYTHONLIBDIR) \ + --install-lib=$(pythondir) \ --single-version-externally-managed \ --no-compile # Python package uninstall uninstall-hook: -rm -rf core_ns3.egg-info - -rm -rf $(DESTDIR)/$(PYTHONLIBDIR)/core_ns3-$(PACKAGE_VERSION)-py$(PYTHON_VERSION).egg-info - -rm -rf $(DESTDIR)/$(PYTHONLIBDIR)/corens3 + -rm -rf $(DESTDIR)/$(pythondir)/core_ns3-$(PACKAGE_VERSION)-py$(PYTHON_VERSION).egg-info + -rm -rf $(DESTDIR)/$(pythondir)/corens3 -rm -rf $(DESTDIR)/$(datadir)/corens3 # Python package cleanup From da946f1f56cebb81eadcf1852290af33afd24d20 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 17 Oct 2019 15:02:24 -0700 Subject: [PATCH 070/462] removing builtins imports --- daemon/core/api/grpc/server.py | 1 - daemon/core/api/tlv/corehandlers.py | 1 - daemon/core/emane/commeffect.py | 1 - daemon/core/location/mobility.py | 1 - daemon/core/nodes/interface.py | 1 - daemon/core/nodes/ipaddress.py | 1 - daemon/examples/grpc/switch.py | 1 - daemon/examples/python/emane80211.py | 1 - daemon/examples/python/switch.py | 1 - daemon/examples/python/switch_inject.py | 1 - daemon/examples/python/wlan.py | 1 - daemon/tests/test_grpc.py | 1 - 12 files changed, 12 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 3c70ed09..dcea5fb5 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -4,7 +4,6 @@ import os import re import tempfile import time -from builtins import int from concurrent import futures from queue import Empty, Queue diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 9a5e487a..f3966ba1 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -10,7 +10,6 @@ import socketserver import sys import threading import time -from builtins import range from itertools import repeat from queue import Empty, Queue diff --git a/daemon/core/emane/commeffect.py b/daemon/core/emane/commeffect.py index 4ae20107..13831291 100644 --- a/daemon/core/emane/commeffect.py +++ b/daemon/core/emane/commeffect.py @@ -4,7 +4,6 @@ commeffect.py: EMANE CommEffect model for CORE import logging import os -from builtins import int from lxml import etree diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index eae46ce4..eb2f244f 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -8,7 +8,6 @@ import math import os import threading import time -from builtins import int from functools import total_ordering from core import utils diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index a6e04eb5..3e4f73ef 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -4,7 +4,6 @@ virtual ethernet classes that implement the interfaces available under Linux. import logging import time -from builtins import int, range from core import utils from core.errors import CoreCommandError diff --git a/daemon/core/nodes/ipaddress.py b/daemon/core/nodes/ipaddress.py index c7860dbc..be2ec36d 100644 --- a/daemon/core/nodes/ipaddress.py +++ b/daemon/core/nodes/ipaddress.py @@ -6,7 +6,6 @@ import logging import random import socket import struct -from builtins import bytes, int, range from socket import AF_INET, AF_INET6 diff --git a/daemon/examples/grpc/switch.py b/daemon/examples/grpc/switch.py index 89dc371d..48aa63bc 100644 --- a/daemon/examples/grpc/switch.py +++ b/daemon/examples/grpc/switch.py @@ -1,5 +1,4 @@ import logging -from builtins import range from core.api.grpc import client, core_pb2 diff --git a/daemon/examples/python/emane80211.py b/daemon/examples/python/emane80211.py index adf6959b..93222f9e 100644 --- a/daemon/examples/python/emane80211.py +++ b/daemon/examples/python/emane80211.py @@ -5,7 +5,6 @@ import datetime import logging import parser -from builtins import range from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emulator.coreemu import CoreEmu diff --git a/daemon/examples/python/switch.py b/daemon/examples/python/switch.py index e4d0fd02..6702802e 100644 --- a/daemon/examples/python/switch.py +++ b/daemon/examples/python/switch.py @@ -8,7 +8,6 @@ import datetime import logging import parser -from builtins import range from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes diff --git a/daemon/examples/python/switch_inject.py b/daemon/examples/python/switch_inject.py index ff1ff84b..1b7b634c 100644 --- a/daemon/examples/python/switch_inject.py +++ b/daemon/examples/python/switch_inject.py @@ -5,7 +5,6 @@ # and repeat for minnodes <= n <= maxnodes with a step size of # nodestep import logging -from builtins import range from core.emulator.emudata import IpPrefixes from core.emulator.enumerations import EventTypes, NodeTypes diff --git a/daemon/examples/python/wlan.py b/daemon/examples/python/wlan.py index b16af7cd..b3b4544e 100644 --- a/daemon/examples/python/wlan.py +++ b/daemon/examples/python/wlan.py @@ -8,7 +8,6 @@ import datetime import logging import parser -from builtins import range from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index f9604229..37d8e7ae 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -1,5 +1,4 @@ import time -from builtins import int from queue import Queue import grpc From c9326b6a97a1f9fbb04a2d9903d8bba0b5391c7e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 17 Oct 2019 18:59:50 -0700 Subject: [PATCH 071/462] initial changes to use f strings --- daemon/core/errors.py | 8 ++-- daemon/core/nodes/client.py | 2 +- daemon/core/nodes/docker.py | 10 ++--- daemon/core/nodes/interface.py | 4 +- daemon/core/nodes/ipaddress.py | 15 +++---- daemon/core/nodes/lxd.py | 10 ++--- daemon/core/nodes/netclient.py | 77 +++++++++++++++++----------------- daemon/core/nodes/physical.py | 16 +++---- 8 files changed, 70 insertions(+), 72 deletions(-) diff --git a/daemon/core/errors.py b/daemon/core/errors.py index 5b76abb3..f5c38b5b 100644 --- a/daemon/core/errors.py +++ b/daemon/core/errors.py @@ -10,11 +10,9 @@ class CoreCommandError(subprocess.CalledProcessError): """ def __str__(self): - return "Command(%s), Status(%s):\nstdout: %s\nstderr: %s" % ( - self.cmd, - self.returncode, - self.output, - self.stderr, + return ( + f"Command({self.cmd}), Status({self.returncode}):\n" + f"stdout: {self.output}\nstderr: {self.stderr}" ) diff --git a/daemon/core/nodes/client.py b/daemon/core/nodes/client.py index 632e12bc..299b8135 100644 --- a/daemon/core/nodes/client.py +++ b/daemon/core/nodes/client.py @@ -51,7 +51,7 @@ class VnodeClient(object): pass def create_cmd(self, args): - return "%s -c %s -- %s" % (VCMD_BIN, self.ctrlchnlname, args) + return f"{VCMD_BIN} -c {self.ctrlchnlname} -- {args}" def check_cmd(self, args, wait=True): """ diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index 17d7578a..df8422af 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -226,13 +226,13 @@ class DockerNode(CoreNode): temp.close() if directory: - self.node_net_cmd("mkdir -m %o -p %s" % (0o755, directory)) + self.node_net_cmd(f"mkdir -m {0o755:o} -p {directory}") if self.server is not None: self.server.remote_put(temp.name, temp.name) self.client.copy_file(temp.name, filename) - self.node_net_cmd("chmod %o %s" % (mode, filename)) + self.node_net_cmd(f"chmod {mode:o} {filename}") if self.server is not None: - self.net_cmd("rm -f %s" % temp.name) + self.net_cmd(f"rm -f {temp.name}") os.unlink(temp.name) logging.debug( "node(%s) added file: %s; mode: 0%o", self.name, filename, mode @@ -252,7 +252,7 @@ class DockerNode(CoreNode): "node file copy file(%s) source(%s) mode(%s)", filename, srcfilename, mode ) directory = os.path.dirname(filename) - self.node_net_cmd("mkdir -p %s" % directory) + self.node_net_cmd(f"mkdir -p {directory}") if self.server is None: source = srcfilename @@ -262,4 +262,4 @@ class DockerNode(CoreNode): self.server.remote_put(source, temp.name) self.client.copy_file(source, filename) - self.node_net_cmd("chmod %o %s" % (mode, filename)) + self.node_net_cmd(f"chmod {mode:o} {filename}") diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 3e4f73ef..c8841432 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -344,7 +344,7 @@ class TunTap(CoreInterface): if r == 0: result = True break - msg = "attempt %s failed with nonzero exit status %s" % (i, r) + msg = f"attempt {i} failed with nonzero exit status {r}" if i < attempts + 1: msg += ", retrying..." logging.info(msg) @@ -480,7 +480,7 @@ class GreTap(CoreInterface): self.id = _id sessionid = self.session.short_session_id() # interface name on the local host machine - self.localname = "gt.%s.%s" % (self.id, sessionid) + self.localname = f"gt.{self.id}.{sessionid}" self.transport_type = "raw" if not start: self.up = False diff --git a/daemon/core/nodes/ipaddress.py b/daemon/core/nodes/ipaddress.py index be2ec36d..df2309ab 100644 --- a/daemon/core/nodes/ipaddress.py +++ b/daemon/core/nodes/ipaddress.py @@ -29,7 +29,7 @@ class MacAddress(object): :return: string representation :rtype: str """ - return ":".join("%02x" % x for x in bytearray(self.addr)) + return ":".join(f"{x:02x}" for x in bytearray(self.addr)) def to_link_local(self): """ @@ -217,14 +217,14 @@ class IpPrefix(object): # prefixstr format: address/prefixlen tmp = prefixstr.split("/") if len(tmp) > 2: - raise ValueError("invalid prefix: %s" % prefixstr) + raise ValueError(f"invalid prefix: {prefixstr}") self.af = af if self.af == AF_INET: self.addrlen = 32 elif self.af == AF_INET6: self.addrlen = 128 else: - raise ValueError("invalid address family: %s" % self.af) + raise ValueError(f"invalid address family: {self.af}") if len(tmp) == 2: self.prefixlen = int(tmp[1]) else: @@ -247,7 +247,8 @@ class IpPrefix(object): :return: string representation :rtype: str """ - return "%s/%s" % (socket.inet_ntop(self.af, self.prefix), self.prefixlen) + address = socket.inet_ntop(self.af, self.prefix) + return f"{address}/{self.prefixlen}" def __eq__(self, other): """ @@ -283,7 +284,7 @@ class IpPrefix(object): return NotImplemented a = IpAddress(self.af, self.prefix) + (tmp << (self.addrlen - self.prefixlen)) - prefixstr = "%s/%s" % (a, self.prefixlen) + prefixstr = f"{a}/{self.prefixlen}" if self.__class__ == IpPrefix: return self.__class__(self.af, prefixstr) else: @@ -324,7 +325,7 @@ class IpPrefix(object): self.af == AF_INET and tmp == (1 << (self.addrlen - self.prefixlen)) - 1 ) ): - raise ValueError("invalid hostid for prefix %s: %s" % (self, hostid)) + raise ValueError(f"invalid hostid for prefix {self}: {hostid}") addr = bytes(b"") prefix_endpoint = -1 @@ -374,7 +375,7 @@ class IpPrefix(object): :return: prefix string :rtype: str """ - return "%s" % socket.inet_ntop(self.af, self.prefix) + return socket.inet_ntop(self.af, self.prefix) def netmask_str(self): """ diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index b11086e7..50588fb2 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -184,13 +184,13 @@ class LxcNode(CoreNode): temp.close() if directory: - self.node_net_cmd("mkdir -m %o -p %s" % (0o755, directory)) + self.node_net_cmd(f"mkdir -m {0o755:o} -p {directory}") if self.server is not None: self.server.remote_put(temp.name, temp.name) self.client.copy_file(temp.name, filename) - self.node_net_cmd("chmod %o %s" % (mode, filename)) + self.node_net_cmd(f"chmod {mode:o} {filename}") if self.server is not None: - self.net_cmd("rm -f %s" % temp.name) + self.net_cmd(f"rm -f {temp.name}") os.unlink(temp.name) logging.debug("node(%s) added file: %s; mode: 0%o", self.name, filename, mode) @@ -208,7 +208,7 @@ class LxcNode(CoreNode): "node file copy file(%s) source(%s) mode(%s)", filename, srcfilename, mode ) directory = os.path.dirname(filename) - self.node_net_cmd("mkdir -p %s" % directory) + self.node_net_cmd(f"mkdir -p {directory}") if self.server is None: source = srcfilename @@ -218,7 +218,7 @@ class LxcNode(CoreNode): self.server.remote_put(source, temp.name) self.client.copy_file(source, filename) - self.node_net_cmd("chmod %o %s" % (mode, filename)) + self.node_net_cmd(f"chmod {mode:o} {filename}") def addnetif(self, netif, ifindex): super(LxcNode, self).addnetif(netif, ifindex) diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 9234bef5..94e73e7f 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -41,7 +41,7 @@ class LinuxNetClient(object): :param str name: name for hostname :return: nothing """ - self.run("hostname %s" % name) + self.run(f"hostname {name}") def create_route(self, route, device): """ @@ -51,7 +51,7 @@ class LinuxNetClient(object): :param str device: device to add route to :return: nothing """ - self.run("%s route add %s dev %s" % (IP_BIN, route, device)) + self.run(f"{IP_BIN} route add {route} dev {device}") def device_up(self, device): """ @@ -60,7 +60,7 @@ class LinuxNetClient(object): :param str device: device to bring up :return: nothing """ - self.run("%s link set %s up" % (IP_BIN, device)) + self.run(f"{IP_BIN} link set {device} up") def device_down(self, device): """ @@ -69,7 +69,7 @@ class LinuxNetClient(object): :param str device: device to bring down :return: nothing """ - self.run("%s link set %s down" % (IP_BIN, device)) + self.run(f"{IP_BIN} link set {device} down") def device_name(self, device, name): """ @@ -79,7 +79,7 @@ class LinuxNetClient(object): :param str name: name to set :return: nothing """ - self.run("%s link set %s name %s" % (IP_BIN, device, name)) + self.run(f"{IP_BIN} link set {device} name {name}") def device_show(self, device): """ @@ -89,7 +89,7 @@ class LinuxNetClient(object): :return: device information :rtype: str """ - return self.run("%s link show %s" % (IP_BIN, device)) + return self.run(f"{IP_BIN} link show {device}") def device_ns(self, device, namespace): """ @@ -99,7 +99,7 @@ class LinuxNetClient(object): :param str namespace: namespace to set device to :return: nothing """ - self.run("%s link set %s netns %s" % (IP_BIN, device, namespace)) + self.run(f"{IP_BIN} link set {device} netns {namespace}") def device_flush(self, device): """ @@ -108,7 +108,7 @@ class LinuxNetClient(object): :param str device: device to flush :return: nothing """ - self.run("%s -6 address flush dev %s" % (IP_BIN, device)) + self.run(f"{IP_BIN} -6 address flush dev {device}") def device_mac(self, device, mac): """ @@ -118,7 +118,7 @@ class LinuxNetClient(object): :param str mac: mac to set :return: nothing """ - self.run("%s link set dev %s address %s" % (IP_BIN, device, mac)) + self.run(f"{IP_BIN} link set dev {device} address {mac}") def delete_device(self, device): """ @@ -127,7 +127,7 @@ class LinuxNetClient(object): :param str device: device to delete :return: nothing """ - self.run("%s link delete %s" % (IP_BIN, device)) + self.run(f"{IP_BIN} link delete {device}") def delete_tc(self, device): """ @@ -136,7 +136,7 @@ class LinuxNetClient(object): :param str device: device to remove tc :return: nothing """ - self.run("%s qdisc delete dev %s root" % (TC_BIN, device)) + self.run(f"{TC_BIN} qdisc delete dev {device} root") def checksums_off(self, interface_name): """ @@ -145,7 +145,7 @@ class LinuxNetClient(object): :param str interface_name: interface to update :return: nothing """ - self.run("%s -K %s rx off tx off" % (ETHTOOL_BIN, interface_name)) + self.run(f"{ETHTOOL_BIN} -K {interface_name} rx off tx off") def create_address(self, device, address, broadcast=None): """ @@ -158,11 +158,10 @@ class LinuxNetClient(object): """ if broadcast is not None: self.run( - "%s address add %s broadcast %s dev %s" - % (IP_BIN, address, broadcast, device) + f"{IP_BIN} address add {address} broadcast {broadcast} dev {device}" ) else: - self.run("%s address add %s dev %s" % (IP_BIN, address, device)) + self.run(f"{IP_BIN} address add {address} dev {device}") def delete_address(self, device, address): """ @@ -172,7 +171,7 @@ class LinuxNetClient(object): :param str address: address to remove :return: nothing """ - self.run("%s address delete %s dev %s" % (IP_BIN, address, device)) + self.run(f"{IP_BIN} address delete {address} dev {device}") def create_veth(self, name, peer): """ @@ -182,7 +181,7 @@ class LinuxNetClient(object): :param str peer: peer name :return: nothing """ - self.run("%s link add name %s type veth peer name %s" % (IP_BIN, name, peer)) + self.run(f"{IP_BIN} link add name {name} type veth peer name {peer}") def create_gretap(self, device, address, local, ttl, key): """ @@ -195,13 +194,13 @@ class LinuxNetClient(object): :param int key: key for tap :return: nothing """ - cmd = "%s link add %s type gretap remote %s" % (IP_BIN, device, address) + cmd = f"{IP_BIN} link add {device} type gretap remote {address}" if local is not None: - cmd += " local %s" % local + cmd += f" local {local}" if ttl is not None: - cmd += " ttl %s" % ttl + cmd += f" ttl {ttl}" if key is not None: - cmd += " key %s" % key + cmd += f" key {key}" self.run(cmd) def create_bridge(self, name): @@ -211,13 +210,13 @@ class LinuxNetClient(object): :param str name: bridge name :return: nothing """ - self.run("%s addbr %s" % (BRCTL_BIN, name)) - self.run("%s stp %s off" % (BRCTL_BIN, name)) - self.run("%s setfd %s 0" % (BRCTL_BIN, name)) + self.run(f"{BRCTL_BIN} addbr {name}") + self.run(f"{BRCTL_BIN} stp {name} off") + self.run(f"{BRCTL_BIN} setfd {name} 0") self.device_up(name) # turn off multicast snooping so forwarding occurs w/o IGMP joins - snoop = "/sys/devices/virtual/net/%s/bridge/multicast_snooping" % name + snoop = f"/sys/devices/virtual/net/{name}/bridge/multicast_snooping" if os.path.exists(snoop): with open(snoop, "w") as f: f.write("0") @@ -230,7 +229,7 @@ class LinuxNetClient(object): :return: nothing """ self.device_down(name) - self.run("%s delbr %s" % (BRCTL_BIN, name)) + self.run(f"{BRCTL_BIN} delbr {name}") def create_interface(self, bridge_name, interface_name): """ @@ -240,7 +239,7 @@ class LinuxNetClient(object): :param str interface_name: interface name :return: nothing """ - self.run("%s addif %s %s" % (BRCTL_BIN, bridge_name, interface_name)) + self.run(f"{BRCTL_BIN} addif {bridge_name} {interface_name}") self.device_up(interface_name) def delete_interface(self, bridge_name, interface_name): @@ -251,7 +250,7 @@ class LinuxNetClient(object): :param str interface_name: interface name :return: nothing """ - self.run("%s delif %s %s" % (BRCTL_BIN, bridge_name, interface_name)) + self.run(f"{BRCTL_BIN} delif {bridge_name} {interface_name}") def existing_bridges(self, _id): """ @@ -259,7 +258,7 @@ class LinuxNetClient(object): :param _id: node id to check bridges for """ - output = self.run("%s show" % BRCTL_BIN) + output = self.run(f"{BRCTL_BIN} show") lines = output.split("\n") for line in lines[1:]: columns = line.split() @@ -278,7 +277,7 @@ class LinuxNetClient(object): :param str name: bridge name :return: nothing """ - self.run("%s setageing %s 0" % (BRCTL_BIN, name)) + self.run(f"{BRCTL_BIN} setageing {name} 0") class OvsNetClient(LinuxNetClient): @@ -293,10 +292,10 @@ class OvsNetClient(LinuxNetClient): :param str name: bridge name :return: nothing """ - self.run("%s add-br %s" % (OVS_BIN, name)) - self.run("%s set bridge %s stp_enable=false" % (OVS_BIN, name)) - self.run("%s set bridge %s other_config:stp-max-age=6" % (OVS_BIN, name)) - self.run("%s set bridge %s other_config:stp-forward-delay=4" % (OVS_BIN, name)) + self.run(f"{OVS_BIN} add-br {name}") + self.run(f"{OVS_BIN} set bridge {name} stp_enable=false") + self.run(f"{OVS_BIN} set bridge {name} other_config:stp-max-age=6") + self.run(f"{OVS_BIN} set bridge {name} other_config:stp-forward-delay=4") self.device_up(name) def delete_bridge(self, name): @@ -307,7 +306,7 @@ class OvsNetClient(LinuxNetClient): :return: nothing """ self.device_down(name) - self.run("%s del-br %s" % (OVS_BIN, name)) + self.run(f"{OVS_BIN} del-br {name}") def create_interface(self, bridge_name, interface_name): """ @@ -317,7 +316,7 @@ class OvsNetClient(LinuxNetClient): :param str interface_name: interface name :return: nothing """ - self.run("%s add-port %s %s" % (OVS_BIN, bridge_name, interface_name)) + self.run(f"{OVS_BIN} add-port {bridge_name} {interface_name}") self.device_up(interface_name) def delete_interface(self, bridge_name, interface_name): @@ -328,7 +327,7 @@ class OvsNetClient(LinuxNetClient): :param str interface_name: interface name :return: nothing """ - self.run("%s del-port %s %s" % (OVS_BIN, bridge_name, interface_name)) + self.run(f"{OVS_BIN} del-port {bridge_name} {interface_name}") def existing_bridges(self, _id): """ @@ -336,7 +335,7 @@ class OvsNetClient(LinuxNetClient): :param _id: node id to check bridges for """ - output = self.run("%s list-br" % OVS_BIN) + output = self.run(f"{OVS_BIN} list-br") if output: for line in output.split("\n"): fields = line.split(".") @@ -351,4 +350,4 @@ class OvsNetClient(LinuxNetClient): :param str name: bridge name :return: nothing """ - self.run("%s set bridge %s other_config:mac-aging-time=0" % (OVS_BIN, name)) + self.run(f"{OVS_BIN} set bridge {name} other_config:mac-aging-time=0") diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 0f9e0217..cae3f298 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -94,7 +94,7 @@ class PhysicalNode(CoreNodeBase): the emulation, no new interface is created; instead, adopt the GreTap netif as the node interface. """ - netif.name = "gt%d" % ifindex + netif.name = f"gt{ifindex}" netif.node = self self.addnetif(netif, ifindex) @@ -161,7 +161,7 @@ class PhysicalNode(CoreNodeBase): ifindex = self.newifindex() if ifname is None: - ifname = "gt%d" % ifindex + ifname = f"gt{ifindex}" if self.up: # this is reached when this node is linked to a network node @@ -177,7 +177,7 @@ class PhysicalNode(CoreNodeBase): def privatedir(self, path): if path[0] != "/": - raise ValueError("path not fully qualified: %s" % path) + raise ValueError(f"path not fully qualified: {path}") hostpath = os.path.join( self.nodedir, os.path.normpath(path).strip("/").replace("/", ".") ) @@ -188,13 +188,13 @@ class PhysicalNode(CoreNodeBase): source = os.path.abspath(source) logging.info("mounting %s at %s", source, target) os.makedirs(target) - self.net_cmd("%s --bind %s %s" % (MOUNT_BIN, source, target), cwd=self.nodedir) + self.net_cmd(f"{MOUNT_BIN} --bind {source} {target}", cwd=self.nodedir) self._mounts.append((source, target)) def umount(self, target): - logging.info("unmounting '%s'" % target) + logging.info("unmounting '%s'", target) try: - self.net_cmd("%s -l %s" % (UMOUNT_BIN, target), cwd=self.nodedir) + self.net_cmd(f"{UMOUNT_BIN} -l {target}", cwd=self.nodedir) except CoreCommandError: logging.exception("unmounting failed for %s", target) @@ -363,7 +363,7 @@ class Rj45Node(CoreNodeBase, CoreInterface): if ifindex == self.ifindex: self.shutdown() else: - raise ValueError("ifindex %s does not exist" % ifindex) + raise ValueError(f"ifindex {ifindex} does not exist") def netif(self, ifindex, net=None): """ @@ -442,7 +442,7 @@ class Rj45Node(CoreNodeBase, CoreInterface): if len(items) < 2: continue - if items[1] == "%s:" % self.localname: + if items[1] == f"{self.localname}:": flags = items[2][1:-1].split(",") if "UP" in flags: self.old_up = True From 79cde8cd59441dc71d2cf2ee6b3796069eaf8571 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 17 Oct 2019 19:25:52 -0700 Subject: [PATCH 072/462] further f string updates --- daemon/core/location/corelocation.py | 2 -- daemon/core/services/xorp.py | 7 ------- daemon/examples/myservices/sample.py | 4 ++-- daemon/examples/python/parser.py | 19 ------------------- daemon/tests/conftest.py | 6 +----- daemon/tests/distributed/test_distributed.py | 6 +++--- daemon/tests/emane/test_emane.py | 2 +- daemon/tests/test_core.py | 2 +- daemon/tests/test_grpc.py | 16 +++++++--------- daemon/tests/test_gui.py | 10 +++++----- daemon/tests/test_nodes.py | 2 +- 11 files changed, 21 insertions(+), 55 deletions(-) diff --git a/daemon/core/location/corelocation.py b/daemon/core/location/corelocation.py index b3e62153..5f9e11e3 100644 --- a/daemon/core/location/corelocation.py +++ b/daemon/core/location/corelocation.py @@ -128,8 +128,6 @@ class CoreLocation(object): z, ) lat, lon = self.refgeo[:2] - # self.info("getgeo(%s,%s,%s) e=%s n=%s zone=%s lat,lon,alt=" \ - # "%.3f,%.3f,%.3f" % (x, y, z, e, n, zone, lat, lon, alt)) return lat, lon, alt def getxyz(self, lat, lon, alt): diff --git a/daemon/core/services/xorp.py b/daemon/core/services/xorp.py index 812fcf9f..1cd62620 100644 --- a/daemon/core/services/xorp.py +++ b/daemon/core/services/xorp.py @@ -312,13 +312,6 @@ class XorpRipng(XorpService): continue cfg += "\tinterface %s {\n" % ifc.name cfg += "\t vif %s {\n" % ifc.name - # for a in ifc.addrlist: - # if a.find(":") < 0: - # continue - # addr = a.split("/")[0] - # cfg += "\t\taddress %s {\n" % addr - # cfg += "\t\t disable: false\n" - # cfg += "\t\t}\n" cfg += "\t\taddress %s {\n" % ifc.hwaddr.tolinklocal() cfg += "\t\t disable: false\n" cfg += "\t\t}\n" diff --git a/daemon/examples/myservices/sample.py b/daemon/examples/myservices/sample.py index d6c111ab..8c6dbe06 100644 --- a/daemon/examples/myservices/sample.py +++ b/daemon/examples/myservices/sample.py @@ -37,7 +37,7 @@ class MyService(CoreService): dependencies = () dirs = () configs = ("myservice1.sh", "myservice2.sh") - startup = ("sh %s" % configs[0], "sh %s" % configs[1]) + startup = tuple(f"sh {x}" for x in configs) validate = () validation_mode = ServiceMode.NON_BLOCKING validation_timer = 5 @@ -81,7 +81,7 @@ class MyService(CoreService): if filename == cls.configs[0]: cfg += "# auto-generated by MyService (sample.py)\n" for ifc in node.netifs(): - cfg += 'echo "Node %s has interface %s"\n' % (node.name, ifc.name) + cfg += f'echo "Node {node.name} has interface {ifc.name}"\n' elif filename == cls.configs[1]: cfg += "echo hello" diff --git a/daemon/examples/python/parser.py b/daemon/examples/python/parser.py index 7bbb0fbc..fdc2591a 100644 --- a/daemon/examples/python/parser.py +++ b/daemon/examples/python/parser.py @@ -24,25 +24,6 @@ def parse_options(name): options = parser.parse_args() - # usagestr = "usage: %prog [-h] [options] [args]" - # parser = optparse.OptionParser(usage=usagestr) - # - # parser.add_option("-n", "--nodes", dest="nodes", type=int, default=DEFAULT_NODES, - # help="number of nodes to create in this example") - # - # parser.add_option("-t", "--time", dest="time", type=int, default=DEFAULT_TIME, - # help="example iperf run time in seconds") - - # def usage(msg=None, err=0): - # print - # if msg: - # print "%s\n" % msg - # parser.print_help() - # sys.exit(err) - - # parse command line options - # options, args = parser.parse_args() - if options.nodes < 2: parser.error("invalid min number of nodes: %s" % options.nodes) if options.time < 1: diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index ead3c2b4..521a2432 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -67,11 +67,7 @@ class CoreServerTest(object): self.request_handler.handle_message(message) # add broker server for distributed core - distributed = "%s:%s:%s" % ( - self.distributed_server, - distributed_address, - self.port, - ) + distributed = f"{self.distributed_server}:{distributed_address}:{self.port}" message = CoreConfMessage.create( 0, [ diff --git a/daemon/tests/distributed/test_distributed.py b/daemon/tests/distributed/test_distributed.py index 49271d64..7078d6ed 100644 --- a/daemon/tests/distributed/test_distributed.py +++ b/daemon/tests/distributed/test_distributed.py @@ -206,7 +206,7 @@ class TestDistributed: # test a ping command node_one = cored.session.get_node(1) - message = command_message(node_one, "ping -c 5 %s" % ip4_address) + message = command_message(node_one, f"ping -c 5 {ip4_address}") cored.request_handler.dispatch_replies = validate_response cored.request_handler.handle_message(message) @@ -259,7 +259,7 @@ class TestDistributed: # test a ping command node_one = cored.session.get_node(1) - message = command_message(node_one, "ping -c 5 %s" % ip4_address) + message = command_message(node_one, f"ping -c 5 {ip4_address}") cored.request_handler.dispatch_replies = validate_response cored.request_handler.handle_message(message) @@ -307,7 +307,7 @@ class TestDistributed: # test a ping command node_one = cored.session.get_node(1) - message = command_message(node_one, "ping -c 5 %s" % ip4_address) + message = command_message(node_one, f"ping -c 5 {ip4_address}") cored.request_handler.dispatch_replies = validate_response cored.request_handler.handle_message(message) cored.request_handler.handle_message(message) diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py index 65065665..a0ae05d1 100644 --- a/daemon/tests/emane/test_emane.py +++ b/daemon/tests/emane/test_emane.py @@ -27,7 +27,7 @@ _DIR = os.path.dirname(os.path.abspath(__file__)) def ping(from_node, to_node, ip_prefixes, count=3): address = ip_prefixes.ip4_address(to_node) try: - from_node.node_net_cmd("ping -c %s %s" % (count, address)) + from_node.node_net_cmd(f"ping -c {count} {address}") status = 0 except CoreCommandError as e: status = e.returncode diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 3fc90da8..fa2adc6e 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -20,7 +20,7 @@ _WIRED = [NodeTypes.PEER_TO_PEER, NodeTypes.HUB, NodeTypes.SWITCH] def ping(from_node, to_node, ip_prefixes): address = ip_prefixes.ip4_address(to_node) try: - from_node.node_net_cmd("ping -c 3 %s" % address) + from_node.node_net_cmd(f"ping -c 3 {address}") status = 0 except CoreCommandError as e: status = e.returncode diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 37d8e7ae..b2ea73ca 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -206,8 +206,7 @@ class TestGrpc: # then assert response.node.id == node.id - @pytest.mark.parametrize("node_id, expected", [(1, True), (2, False)]) - def test_edit_node(self, grpc_server, node_id, expected): + def test_edit_node(self, grpc_server): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -217,13 +216,12 @@ class TestGrpc: x, y = 10, 10 with client.context_connect(): position = core_pb2.Position(x=x, y=y) - response = client.edit_node(session.id, node_id, position) + response = client.edit_node(session.id, node.id, position) # then - assert response.result is expected - if expected is True: - assert node.position.x == x - assert node.position.y == y + assert response.result is True + assert node.position.x == x + assert node.position.y == y @pytest.mark.parametrize("node_id, expected", [(1, True), (2, False)]) def test_delete_node(self, grpc_server, node_id, expected): @@ -253,7 +251,7 @@ class TestGrpc: output = "hello world" # then - command = "echo %s" % output + command = f"echo {output}" with client.context_connect(): response = client.node_command(session.id, node.id, command) @@ -863,7 +861,7 @@ class TestGrpc: client.events(session.id, handle_event) time.sleep(0.1) event = EventData( - event_type=EventTypes.RUNTIME_STATE.value, time="%s" % time.time() + event_type=EventTypes.RUNTIME_STATE.value, time=str(time.time()) ) session.broadcast_event(event) diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index c07e2bd3..40c025ee 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -29,7 +29,7 @@ from core.nodes.ipaddress import Ipv4Prefix def dict_to_str(values): - return "|".join("%s=%s" % (x, values[x]) for x in values) + return "|".join(f"{x}={values[x]}" for x in values) class TestGui: @@ -383,7 +383,7 @@ class TestGui: message = coreapi.CoreFileMessage.create( MessageFlags.ADD.value, [ - (FileTlvs.TYPE, "hook:%s" % state), + (FileTlvs.TYPE, f"hook:{state}"), (FileTlvs.NAME, file_name), (FileTlvs.DATA, file_data), ], @@ -406,7 +406,7 @@ class TestGui: MessageFlags.ADD.value, [ (FileTlvs.NODE, node.id), - (FileTlvs.TYPE, "service:%s" % service), + (FileTlvs.TYPE, f"service:{service}"), (FileTlvs.NAME, file_name), (FileTlvs.DATA, file_data), ], @@ -760,7 +760,7 @@ class TestGui: [ (ConfigTlvs.OBJECT, "broker"), (ConfigTlvs.TYPE, ConfigFlags.UPDATE.value), - (ConfigTlvs.VALUES, "%s:%s:%s" % (server, host, port)), + (ConfigTlvs.VALUES, f"{server}:{host}:{port}"), ], ) coreserver.session.distributed.add_server = mock.MagicMock() @@ -844,7 +844,7 @@ class TestGui: (ConfigTlvs.NODE, node.id), (ConfigTlvs.OBJECT, "services"), (ConfigTlvs.TYPE, ConfigFlags.UPDATE.value), - (ConfigTlvs.OPAQUE, "service:%s" % service), + (ConfigTlvs.OPAQUE, f"service:{service}"), (ConfigTlvs.VALUES, dict_to_str(values)), ], ) diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index 1f18c87e..34c426c5 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -67,4 +67,4 @@ class TestNodes: # then assert node assert node.up - assert utils.check_cmd("brctl show %s" % node.brname) + assert utils.check_cmd(f"brctl show {node.brname}") From 7d2a6157167c866a5af3895986f23c1fc15e4826 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 18 Oct 2019 10:33:31 -0700 Subject: [PATCH 073/462] more updates to using f string --- daemon/core/config.py | 24 +++------ daemon/core/emane/commeffect.py | 6 +-- daemon/core/emane/emanemanager.py | 21 ++++---- daemon/core/emane/emanemanifest.py | 2 +- daemon/core/emane/nodes.py | 2 +- daemon/core/emane/tdma.py | 2 +- daemon/core/emulator/distributed.py | 4 +- daemon/core/emulator/emudata.py | 4 +- daemon/core/emulator/session.py | 74 ++++++++++++++-------------- daemon/core/location/mobility.py | 19 ++++--- daemon/core/nodes/base.py | 63 +++++++++++------------ daemon/core/plugins/sdt.py | 34 ++++++------- daemon/core/utils.py | 23 ++++----- daemon/core/xml/corexml.py | 2 +- daemon/core/xml/corexmldeployment.py | 23 ++++----- daemon/core/xml/emanexml.py | 34 +++++++------ daemon/examples/python/emane80211.py | 7 ++- daemon/examples/python/parser.py | 6 +-- daemon/examples/python/switch.py | 16 +++--- daemon/examples/python/wlan.py | 12 +++-- daemon/scripts/core-daemon | 38 ++++++++------ daemon/scripts/core-manage | 29 ++++++----- daemon/scripts/coresendmsg | 54 ++++++++++---------- 23 files changed, 248 insertions(+), 251 deletions(-) diff --git a/daemon/core/config.py b/daemon/core/config.py index f63ad59a..e55d5f17 100644 --- a/daemon/core/config.py +++ b/daemon/core/config.py @@ -40,10 +40,8 @@ class ConfigShim(object): """ group_strings = [] for config_group in config_groups: - group_string = "%s:%s-%s" % ( - config_group.name, - config_group.start, - config_group.stop, + group_string = ( + f"{config_group.name}:{config_group.start}-{config_group.stop}" ) group_strings.append(group_string) return "|".join(group_strings) @@ -74,7 +72,7 @@ class ConfigShim(object): if not captions: captions = configuration.label else: - captions += "|%s" % configuration.label + captions += f"|{configuration.label}" data_types.append(configuration.type.value) @@ -83,11 +81,11 @@ class ConfigShim(object): _id = configuration.id config_value = config.get(_id, configuration.default) - key_value = "%s=%s" % (_id, config_value) + key_value = f"{_id}={config_value}" if not key_values: key_values = key_value else: - key_values += "|%s" % key_value + key_values += f"|{key_value}" groups_str = cls.groups_to_str(configurable_options.config_groups()) return ConfigData( @@ -130,13 +128,7 @@ class Configuration(object): self.label = label def __str__(self): - return "%s(id=%s, type=%s, default=%s, options=%s)" % ( - self.__class__.__name__, - self.id, - self.type, - self.default, - self.options, - ) + return f"{self.__class__.__name__}(id={self.id}, type={self.type}, default={self.default}, options={self.options})" class ConfigurableManager(object): @@ -333,7 +325,7 @@ class ModelManager(ConfigurableManager): # get model class to configure model_class = self.models.get(model_name) if not model_class: - raise ValueError("%s is an invalid model" % model_name) + raise ValueError(f"{model_name} is an invalid model") # retrieve default values model_config = self.get_model_config(node_id, model_name) @@ -361,7 +353,7 @@ class ModelManager(ConfigurableManager): # get model class to configure model_class = self.models.get(model_name) if not model_class: - raise ValueError("%s is an invalid model" % model_name) + raise ValueError(f"{model_name} is an invalid model") config = self.get_configs(node_id=node_id, config_type=model_name) if not config: diff --git a/daemon/core/emane/commeffect.py b/daemon/core/emane/commeffect.py index 13831291..33edc342 100644 --- a/daemon/core/emane/commeffect.py +++ b/daemon/core/emane/commeffect.py @@ -73,9 +73,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): shim_name = emanexml.shim_file_name(self, interface) # create and write nem document - nem_element = etree.Element( - "nem", name="%s NEM" % self.name, type="unstructured" - ) + nem_element = etree.Element("nem", name=f"{self.name} NEM", type="unstructured") transport_type = "virtual" if interface and interface.transport_type == "raw": transport_type = "raw" @@ -90,7 +88,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): # create and write shim document shim_element = etree.Element( - "shim", name="%s SHIM" % self.name, library=self.shim_library + "shim", name=f"{self.name} SHIM", library=self.shim_library ) # append all shim options (except filterfile) to shimdoc diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 91553b5a..743f90b2 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -227,7 +227,7 @@ class EmaneManager(ModelManager): with self._emane_node_lock: if emane_net.id in self._emane_nets: raise KeyError( - "non-unique EMANE object id %s for %s" % (emane_net.id, emane_net) + f"non-unique EMANE object id {emane_net.id} for {emane_net}" ) self._emane_nets[emane_net.id] = emane_net @@ -342,7 +342,7 @@ class EmaneManager(ModelManager): try: with open(emane_nems_filename, "w") as f: for nodename, ifname, nemid in nems: - f.write("%s %s %s\n" % (nodename, ifname, nemid)) + f.write(f"{nodename} {ifname} {nemid}\n") except IOError: logging.exception("Error writing EMANE NEMs file: %s") @@ -535,7 +535,7 @@ class EmaneManager(ModelManager): logging.info("setting user-defined EMANE log level: %d", cfgloglevel) loglevel = str(cfgloglevel) - emanecmd = "emane -d -l %s" % loglevel + emanecmd = f"emane -d -l {loglevel}" if realtime: emanecmd += " -r" @@ -580,11 +580,9 @@ class EmaneManager(ModelManager): node.node_net_client.create_route(eventgroup, eventdev) # start emane - args = "%s -f %s %s" % ( - emanecmd, - os.path.join(path, "emane%d.log" % n), - os.path.join(path, "platform%d.xml" % n), - ) + log_file = os.path.join(path, f"emane{n}.log") + platform_xml = os.path.join(path, f"platform{n}.xml") + args = f"{emanecmd} -f {log_file} {platform_xml}" output = node.node_net_cmd(args) logging.info("node(%s) emane daemon running: %s", node.name, args) logging.info("node(%s) emane daemon output: %s", node.name, output) @@ -593,8 +591,9 @@ class EmaneManager(ModelManager): return path = self.session.session_dir - emanecmd += " -f %s" % os.path.join(path, "emane.log") - emanecmd += " %s" % os.path.join(path, "platform.xml") + log_file = os.path.join(path, "emane.log") + platform_xml = os.path.join(path, "platform.xml") + emanecmd += f" -f {log_file} {platform_xml}" utils.check_cmd(emanecmd, cwd=path) self.session.distributed.execute(lambda x: x.remote_cmd(emanecmd, cwd=path)) logging.info("host emane daemon running: %s", emanecmd) @@ -797,7 +796,7 @@ class EmaneManager(ModelManager): node = self.session.get_node(n) except CoreError: logging.exception( - "location event NEM %s has no corresponding node %s" % (nemid, n) + "location event NEM %s has no corresponding node %s", nemid, n ) return False diff --git a/daemon/core/emane/emanemanifest.py b/daemon/core/emane/emanemanifest.py index 13cff8f2..a6583b9e 100644 --- a/daemon/core/emane/emanemanifest.py +++ b/daemon/core/emane/emanemanifest.py @@ -115,7 +115,7 @@ def parse(manifest_path, defaults): # define description and account for gui quirks config_descriptions = config_name if config_name.endswith("uri"): - config_descriptions = "%s file" % config_descriptions + config_descriptions = f"{config_descriptions} file" configuration = Configuration( _id=config_name, diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 5451506f..e0ceee2f 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -210,7 +210,7 @@ class EmaneNet(CoreNetworkBase): nemid = self.getnemid(netif) ifname = netif.localname if nemid is None: - logging.info("nemid for %s is unknown" % ifname) + logging.info("nemid for %s is unknown", ifname) continue x, y, z = netif.node.getposition() lat, lon, alt = self.session.location.getgeo(x, y, z) diff --git a/daemon/core/emane/tdma.py b/daemon/core/emane/tdma.py index 249e81b6..91e662ea 100644 --- a/daemon/core/emane/tdma.py +++ b/daemon/core/emane/tdma.py @@ -62,5 +62,5 @@ class EmaneTdmaModel(emanemodel.EmaneModel): logging.info( "setting up tdma schedule: schedule(%s) device(%s)", schedule, event_device ) - args = "emaneevent-tdmaschedule -i %s %s" % (event_device, schedule) + args = f"emaneevent-tdmaschedule -i {event_device} {schedule}" utils.check_cmd(args) diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index 83576434..03e043eb 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -128,7 +128,7 @@ class DistributedController(object): """ server = DistributedServer(name, host) self.servers[name] = server - cmd = "mkdir -p %s" % self.session.session_dir + cmd = f"mkdir -p {self.session.session_dir}" server.remote_cmd(cmd) def execute(self, func): @@ -158,7 +158,7 @@ class DistributedController(object): # remove all remote session directories for name in self.servers: server = self.servers[name] - cmd = "rm -rf %s" % self.session.session_dir + cmd = f"rm -rf {self.session.session_dir}" server.remote_cmd(cmd) # clear tunnels diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index d11d1e0e..5a38c69c 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -284,7 +284,7 @@ class InterfaceData(object): :return: ip4 string or None """ if self.has_ip4(): - return "%s/%s" % (self.ip4, self.ip4_mask) + return f"{self.ip4}/{self.ip4_mask}" else: return None @@ -295,7 +295,7 @@ class InterfaceData(object): :return: ip4 string or None """ if self.has_ip6(): - return "%s/%s" % (self.ip6, self.ip6_mask) + return f"{self.ip6}/{self.ip6_mask}" else: return None diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index d962da28..d80e5e25 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -90,7 +90,7 @@ class Session(object): self.master = False # define and create session directory when desired - self.session_dir = os.path.join(tempfile.gettempdir(), "pycore.%s" % self.id) + self.session_dir = os.path.join(tempfile.gettempdir(), f"pycore.{self.id}") if mkdir: os.mkdir(self.session_dir) @@ -164,7 +164,7 @@ class Session(object): """ node_class = NODES.get(_type) if node_class is None: - raise CoreError("invalid node type: %s" % _type) + raise CoreError(f"invalid node type: {_type}") return node_class @classmethod @@ -178,7 +178,7 @@ class Session(object): """ node_type = NODES_TYPE.get(_class) if node_type is None: - raise CoreError("invalid node class: %s" % _class) + raise CoreError(f"invalid node class: {_class}") return node_type def _link_nodes(self, node_one_id, node_two_id): @@ -254,7 +254,7 @@ class Session(object): """ objects = [x for x in objects if x] if len(objects) < 2: - raise CoreError("wireless link failure: %s" % objects) + raise CoreError(f"wireless link failure: {objects}") logging.debug( "handling wireless linking objects(%s) connect(%s)", objects, connect ) @@ -665,13 +665,13 @@ class Session(object): node_options = NodeOptions() name = node_options.name if not name: - name = "%s%s" % (node_class.__name__, _id) + name = f"{node_class.__name__}{_id}" # verify distributed server server = self.distributed.servers.get(node_options.emulation_server) if node_options.emulation_server is not None and server is None: raise CoreError( - "invalid distributed server: %s" % node_options.emulation_server + f"invalid distributed server: {node_options.emulation_server}" ) # create node @@ -854,7 +854,7 @@ class Session(object): :return: nothing """ # hack to conform with old logic until updated - state = ":%s" % state + state = f":{state}" self.set_hook(state, file_name, source_name, data) def add_node_file(self, node_id, source_name, file_name, data): @@ -1066,7 +1066,7 @@ class Session(object): self.run_state_hooks(state_value) if send_event: - event_data = EventData(event_type=state_value, time="%s" % time.time()) + event_data = EventData(event_type=state_value, time=str(time.time())) self.broadcast_event(event_data) def write_state(self, state): @@ -1078,7 +1078,7 @@ class Session(object): """ try: state_file = open(self._state_file, "w") - state_file.write("%d %s\n" % (state, EventTypes(self.state).name)) + state_file.write(f"{state} {EventTypes(self.state).name}\n") state_file.close() except IOError: logging.exception("error writing state file: %s", state) @@ -1195,9 +1195,9 @@ class Session(object): try: hook(state) except Exception: - message = "exception occured when running %s state hook: %s" % ( - EventTypes(self.state).name, - hook, + state_name = EventTypes(self.state).name + message = ( + f"exception occured when running {state_name} state hook: {hook}" ) logging.exception(message) self.exception( @@ -1258,16 +1258,16 @@ class Session(object): :rtype: dict """ env = os.environ.copy() - env["SESSION"] = "%s" % self.id - env["SESSION_SHORT"] = "%s" % self.short_session_id() - env["SESSION_DIR"] = "%s" % self.session_dir - env["SESSION_NAME"] = "%s" % self.name - env["SESSION_FILENAME"] = "%s" % self.file_name - env["SESSION_USER"] = "%s" % self.user - env["SESSION_NODE_COUNT"] = "%s" % self.get_node_count() + env["SESSION"] = str(self.id) + env["SESSION_SHORT"] = self.short_session_id() + env["SESSION_DIR"] = self.session_dir + env["SESSION_NAME"] = str(self.name) + env["SESSION_FILENAME"] = str(self.file_name) + env["SESSION_USER"] = str(self.user) + env["SESSION_NODE_COUNT"] = str(self.get_node_count()) if state: - env["SESSION_STATE"] = "%s" % self.state + env["SESSION_STATE"] = str(self.state) # attempt to read and add environment config file environment_config_file = os.path.join(constants.CORE_CONF_DIR, "environment") @@ -1356,7 +1356,7 @@ class Session(object): with self._nodes_lock: if node.id in self.nodes: node.shutdown() - raise CoreError("duplicate node id %s for %s" % (node.id, node.name)) + raise CoreError(f"duplicate node id {node.id} for {node.name}") self.nodes[node.id] = node return node @@ -1371,7 +1371,7 @@ class Session(object): :raises core.CoreError: when node does not exist """ if _id not in self.nodes: - raise CoreError("unknown node id %s" % _id) + raise CoreError(f"unknown node id {_id}") return self.nodes[_id] def delete_node(self, _id): @@ -1416,9 +1416,7 @@ class Session(object): with open(file_path, "w") as f: for _id in self.nodes.keys(): node = self.nodes[_id] - f.write( - "%s %s %s %s\n" % (_id, node.name, node.apitype, type(node)) - ) + f.write(f"{_id} {node.name} {node.apitype} {type(node)}\n") except IOError: logging.exception("error writing nodes file") @@ -1585,7 +1583,7 @@ class Session(object): interface names, where length may be limited. """ ssid = (self.id >> 8) ^ (self.id & ((1 << 8) - 1)) - return "%x" % ssid + return f"{ssid:x}" def boot_nodes(self): """ @@ -1670,7 +1668,7 @@ class Session(object): def get_control_net(self, net_index): # TODO: all nodes use an integer id and now this wants to use a string - _id = "ctrl%dnet" % net_index + _id = f"ctrl{net_index}net" return self.get_node(_id) def add_remove_control_net(self, net_index, remove=False, conf_required=True): @@ -1718,7 +1716,7 @@ class Session(object): return None # build a new controlnet bridge - _id = "ctrl%dnet" % net_index + _id = f"ctrl{net_index}net" # use the updown script for control net 0 only. updown_script = None @@ -1797,13 +1795,12 @@ class Session(object): control_ip = node.id try: - addrlist = [ - "%s/%s" - % (control_net.prefix.addr(control_ip), control_net.prefix.prefixlen) - ] + address = control_net.prefix.addr(control_ip) + prefix = control_net.prefix.prefixlen + addrlist = [f"{address}/{prefix}"] except ValueError: - msg = "Control interface not added to node %s. " % node.id - msg += "Invalid control network prefix (%s). " % control_net.prefix + msg = f"Control interface not added to node {node.id}. " + msg += f"Invalid control network prefix ({control_net.prefix}). " msg += "A longer prefix length may be required for this many nodes." logging.exception(msg) return @@ -1811,7 +1808,7 @@ class Session(object): interface1 = node.newnetif( net=control_net, ifindex=control_net.CTRLIF_IDX_BASE + net_index, - ifname="ctrl%d" % net_index, + ifname=f"ctrl{net_index}", hwaddr=MacAddress.random(), addrlist=addrlist, ) @@ -1834,7 +1831,7 @@ class Session(object): logging.exception("error retrieving control net node") return - header = "CORE session %s host entries" % self.id + header = f"CORE session {self.id} host entries" if remove: logging.info("Removing /etc/hosts file entries.") utils.file_demunge("/etc/hosts", header) @@ -1844,9 +1841,10 @@ class Session(object): for interface in control_net.netifs(): name = interface.node.name for address in interface.addrlist: - entries.append("%s %s" % (address.split("/")[0], name)) + address = address.split("/")[0] + entries.append(f"{address} {name}") - logging.info("Adding %d /etc/hosts file entries." % len(entries)) + logging.info("Adding %d /etc/hosts file entries.", len(entries)) utils.file_munge("/etc/hosts", header, "\n".join(entries) + "\n") diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index eb2f244f..f2b49818 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -165,15 +165,16 @@ class MobilityManager(ModelManager): elif model.state == model.STATE_PAUSED: event_type = EventTypes.PAUSE.value - data = "start=%d" % int(model.lasttime - model.timezero) - data += " end=%d" % int(model.endtime) + start_time = int(model.lasttime - model.timezero) + end_time = int(model.endtime) + data = f"start={start_time} end={end_time}" event_data = EventData( node=model.id, event_type=event_type, - name="mobility:%s" % model.name, + name=f"mobility:{model.name}", data=data, - time="%s" % time.time(), + time=str(time.time()), ) self.session.broadcast_event(event_data) @@ -991,7 +992,7 @@ class Ns2ScriptedMobility(WayPointMobility): "ns-2 scripted mobility failed to load file: %s", self.file ) return - logging.info("reading ns-2 script file: %s" % filename) + logging.info("reading ns-2 script file: %s", filename) ln = 0 ix = iy = iz = None inodenum = None @@ -1112,7 +1113,7 @@ class Ns2ScriptedMobility(WayPointMobility): :return: nothing """ if self.autostart == "": - logging.info("not auto-starting ns-2 script for %s" % self.wlan.name) + logging.info("not auto-starting ns-2 script for %s", self.wlan.name) return try: t = float(self.autostart) @@ -1124,9 +1125,7 @@ class Ns2ScriptedMobility(WayPointMobility): ) return self.movenodesinitial() - logging.info( - "scheduling ns-2 script for %s autostart at %s" % (self.wlan.name, t) - ) + logging.info("scheduling ns-2 script for %s autostart at %s", self.wlan.name, t) self.state = self.STATE_RUNNING self.session.event_loop.add_event(t, self.run) @@ -1187,7 +1186,7 @@ class Ns2ScriptedMobility(WayPointMobility): if filename is None or filename == "": return filename = self.findfile(filename) - args = "/bin/sh %s %s" % (filename, typestr) + args = f"/bin/sh {filename} {typestr}" utils.check_cmd( args, cwd=self.session.session_dir, env=self.session.get_environment() ) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 4e9de039..0b3bf04b 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -46,7 +46,7 @@ class NodeBase(object): _id = session.get_node_id() self.id = _id if name is None: - name = "o%s" % self.id + name = f"o{self.id}" self.name = name self.server = server @@ -265,7 +265,7 @@ class CoreNodeBase(NodeBase): """ if self.nodedir is None: self.nodedir = os.path.join(self.session.session_dir, self.name + ".conf") - self.net_cmd("mkdir -p %s" % self.nodedir) + self.net_cmd(f"mkdir -p {self.nodedir}") self.tmpnodedir = True else: self.tmpnodedir = False @@ -281,7 +281,7 @@ class CoreNodeBase(NodeBase): return if self.tmpnodedir: - self.net_cmd("rm -rf %s" % self.nodedir) + self.net_cmd(f"rm -rf {self.nodedir}") def addnetif(self, netif, ifindex): """ @@ -292,7 +292,7 @@ class CoreNodeBase(NodeBase): :return: nothing """ if ifindex in self._netif: - raise ValueError("ifindex %s already exists" % ifindex) + raise ValueError(f"ifindex {ifindex} already exists") self._netif[ifindex] = netif # TODO: this should have probably been set ahead, seems bad to me, check for failure and fix netif.netindex = ifindex @@ -305,7 +305,7 @@ class CoreNodeBase(NodeBase): :return: nothing """ if ifindex not in self._netif: - raise ValueError("ifindex %s does not exist" % ifindex) + raise ValueError(f"ifindex {ifindex} does not exist") netif = self._netif.pop(ifindex) netif.shutdown() del netif @@ -334,7 +334,7 @@ class CoreNodeBase(NodeBase): :return: nothing """ if ifindex not in self._netif: - raise ValueError("ifindex %s does not exist" % ifindex) + raise ValueError(f"ifindex {ifindex} does not exist") self._netif[ifindex].attachnet(net) def detachnet(self, ifindex): @@ -345,7 +345,7 @@ class CoreNodeBase(NodeBase): :return: nothing """ if ifindex not in self._netif: - raise ValueError("ifindex %s does not exist" % ifindex) + raise ValueError(f"ifindex {ifindex} does not exist") self._netif[ifindex].detachnet() def setposition(self, x=None, y=None, z=None): @@ -472,7 +472,7 @@ class CoreNode(CoreNodeBase): :rtype: bool """ try: - self.net_cmd("kill -0 %s" % self.pid) + self.net_cmd(f"kill -0 {self.pid}") except CoreCommandError: return False @@ -496,10 +496,11 @@ class CoreNode(CoreNodeBase): cmd=VNODED_BIN, name=self.ctrlchnlname ) if self.nodedir: - vnoded += " -C %s" % self.nodedir + vnoded += f" -C {self.nodedir}" env = self.session.get_environment(state=False) env["NODE_NUMBER"] = str(self.id) env["NODE_NAME"] = str(self.name) + logging.info("env: %s", env) output = self.net_cmd(vnoded, env=env) self.pid = int(output) @@ -545,13 +546,13 @@ class CoreNode(CoreNodeBase): # kill node process if present try: - self.net_cmd("kill -9 %s" % self.pid) + self.net_cmd(f"kill -9 {self.pid}") except CoreCommandError: logging.exception("error killing process") # remove node directory if present try: - self.net_cmd("rm -rf %s" % self.ctrlchnlname) + self.net_cmd(f"rm -rf {self.ctrlchnlname}") except CoreCommandError: logging.exception("error removing node directory") @@ -604,11 +605,11 @@ class CoreNode(CoreNodeBase): :return: nothing """ if path[0] != "/": - raise ValueError("path not fully qualified: %s" % path) + raise ValueError(f"path not fully qualified: {path}") hostpath = os.path.join( self.nodedir, os.path.normpath(path).strip("/").replace("/", ".") ) - self.net_cmd("mkdir -p %s" % hostpath) + self.net_cmd(f"mkdir -p {hostpath}") self.mount(hostpath, path) def mount(self, source, target): @@ -622,8 +623,8 @@ class CoreNode(CoreNodeBase): """ source = os.path.abspath(source) logging.debug("node(%s) mounting: %s at %s", self.name, source, target) - self.node_net_cmd("mkdir -p %s" % target) - self.node_net_cmd("%s -n --bind %s %s" % (MOUNT_BIN, source, target)) + self.node_net_cmd(f"mkdir -p {target}") + self.node_net_cmd(f"{MOUNT_BIN} -n --bind {source} {target}") self._mounts.append((source, target)) def newifindex(self): @@ -650,22 +651,22 @@ class CoreNode(CoreNodeBase): ifindex = self.newifindex() if ifname is None: - ifname = "eth%d" % ifindex + ifname = f"eth{ifindex}" sessionid = self.session.short_session_id() try: - suffix = "%x.%s.%s" % (self.id, ifindex, sessionid) + suffix = f"{self.id:x}.{ifindex}.{sessionid}" except TypeError: - suffix = "%s.%s.%s" % (self.id, ifindex, sessionid) + suffix = f"{self.id}.{ifindex}.{sessionid}" - localname = "veth" + suffix + localname = f"veth{suffix}" if len(localname) >= 16: - raise ValueError("interface local name (%s) too long" % localname) + raise ValueError(f"interface local name ({localname}) too long") name = localname + "p" if len(name) >= 16: - raise ValueError("interface name (%s) too long" % name) + raise ValueError(f"interface name ({name}) too long") veth = Veth( self.session, self, name, localname, start=self.up, server=self.server @@ -716,10 +717,10 @@ class CoreNode(CoreNodeBase): ifindex = self.newifindex() if ifname is None: - ifname = "eth%d" % ifindex + ifname = f"eth{ifindex}" sessionid = self.session.short_session_id() - localname = "tap%s.%s.%s" % (self.id, ifindex, sessionid) + localname = f"tap{self.id}.{ifindex}.{sessionid}" name = ifname tuntap = TunTap(self.session, self, name, localname, start=self.up) @@ -778,7 +779,7 @@ class CoreNode(CoreNodeBase): try: interface.deladdr(addr) except ValueError: - logging.exception("trying to delete unknown address: %s" % addr) + logging.exception("trying to delete unknown address: %s", addr) if self.up: self.node_net_client.delete_address(interface.name, str(addr)) @@ -850,11 +851,11 @@ class CoreNode(CoreNodeBase): logging.info("adding file from %s to %s", srcname, filename) directory = os.path.dirname(filename) if self.server is None: - self.client.check_cmd("mkdir -p %s" % directory) - self.client.check_cmd("mv %s %s" % (srcname, filename)) + self.client.check_cmd(f"mkdir -p {directory}") + self.client.check_cmd(f"mv {srcname} {filename}") self.client.check_cmd("sync") else: - self.net_cmd("mkdir -p %s" % directory) + self.net_cmd(f"mkdir -p {directory}") self.server.remote_put(srcname, filename) def hostfilename(self, filename): @@ -866,7 +867,7 @@ class CoreNode(CoreNodeBase): """ dirname, basename = os.path.split(filename) if not basename: - raise ValueError("no basename for filename: %s" % filename) + raise ValueError(f"no basename for filename: {filename}") if dirname and dirname[0] == "/": dirname = dirname[1:] dirname = dirname.replace("/", ".") @@ -891,9 +892,9 @@ class CoreNode(CoreNodeBase): open_file.write(contents) os.chmod(open_file.name, mode) else: - self.net_cmd("mkdir -m %o -p %s" % (0o755, dirname)) + self.net_cmd(f"mkdir -m {0o755:o} -p {dirname}") self.server.remote_put_temp(hostfilename, contents) - self.net_cmd("chmod %o %s" % (mode, hostfilename)) + self.net_cmd(f"chmod {mode:o} {hostfilename}") logging.debug( "node(%s) added file: %s; mode: 0%o", self.name, hostfilename, mode ) @@ -914,7 +915,7 @@ class CoreNode(CoreNodeBase): else: self.server.remote_put(srcfilename, hostfilename) if mode is not None: - self.net_cmd("chmod %o %s" % (mode, hostfilename)) + self.net_cmd(f"chmod {mode:o} {hostfilename}") logging.info( "node(%s) copied file: %s; mode: %s", self.name, hostfilename, mode ) diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 934233e9..280a2bc2 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -7,6 +7,7 @@ import socket from urllib.parse import urlparse from core import constants +from core.constants import CORE_DATA_DIR from core.emane.nodes import EmaneNet from core.emulator.enumerations import ( EventTypes, @@ -161,7 +162,7 @@ class Sdt(object): return False self.seturl() - logging.info("connecting to SDT at %s://%s" % (self.protocol, self.address)) + logging.info("connecting to SDT at %s://%s", self.protocol, self.address) if self.sock is None: try: if self.protocol.lower() == "udp": @@ -192,14 +193,14 @@ class Sdt(object): :return: initialize command status :rtype: bool """ - if not self.cmd('path "%s/icons/normal"' % constants.CORE_DATA_DIR): + if not self.cmd(f'path "{CORE_DATA_DIR}/icons/normal"'): return False # send node type to icon mappings for node_type, icon in self.DEFAULT_SPRITES: - if not self.cmd("sprite %s image %s" % (node_type, icon)): + if not self.cmd(f"sprite {node_type} image {icon}"): return False lat, long = self.session.location.refgeo[:2] - return self.cmd("flyto %.6f,%.6f,%d" % (long, lat, self.DEFAULT_ALT)) + return self.cmd(f"flyto {long:.6f},{lat:.6f},{self.DEFAULT_ALT}") def disconnect(self): """ @@ -240,8 +241,8 @@ class Sdt(object): if self.sock is None: return False try: - logging.info("sdt: %s" % cmdstr) - self.sock.sendall("%s\n" % cmdstr) + logging.info("sdt: %s", cmdstr) + self.sock.sendall(f"{cmdstr}\n") return True except IOError: logging.exception("SDT connection error") @@ -266,23 +267,21 @@ class Sdt(object): if not self.connect(): return if flags & MessageFlags.DELETE.value: - self.cmd("delete node,%d" % nodenum) + 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 = "pos %.6f,%.6f,%.6f" % (lon, lat, alt) + 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("sprite %s image %s" % (type, icon)) - self.cmd( - 'node %d type %s label on,"%s" %s' % (nodenum, node_type, name, pos) - ) + self.cmd(f"sprite {node_type} image {icon}") + self.cmd(f'node {nodenum} type {node_type} label on,"{name}" {pos}') else: - self.cmd("node %d %s" % (nodenum, pos)) + self.cmd(f"node {nodenum} {pos}") def updatenodegeo(self, nodenum, lat, long, alt): """ @@ -298,8 +297,8 @@ class Sdt(object): # TODO: received Node Message with lat/long/alt. if not self.connect(): return - pos = "pos %.6f,%.6f,%.6f" % (long, lat, alt) - self.cmd("node %d %s" % (nodenum, pos)) + pos = f"pos {long:.6f},{lat:.6f},{alt:.6f}" + self.cmd(f"node {nodenum} {pos}") def updatelink(self, node1num, node2num, flags, wireless=False): """ @@ -316,14 +315,13 @@ class Sdt(object): if not self.connect(): return if flags & MessageFlags.DELETE.value: - self.cmd("delete link,%s,%s" % (node1num, node2num)) + self.cmd(f"delete link,{node1num},{node2num}") elif flags & MessageFlags.ADD.value: - attr = "" if wireless: attr = " line green,2" else: attr = " line red,2" - self.cmd("link %s,%s%s" % (node1num, node2num, attr)) + self.cmd(f"link {node1num},{node2num}{attr}") def sendobjs(self): """ diff --git a/daemon/core/utils.py b/daemon/core/utils.py index 8f8da19c..7d88e355 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -137,7 +137,7 @@ def which(command, required): break if found_path is None and required: - raise ValueError("failed to find required executable(%s) in path" % command) + raise ValueError(f"failed to find required executable({command}) in path") return found_path @@ -238,12 +238,13 @@ def hex_dump(s, bytes_per_word=2, words_per_line=8): line = s[:total_bytes] s = s[total_bytes:] tmp = map( - lambda x: ("%02x" * bytes_per_word) % x, + lambda x: (f"{bytes_per_word:02x}" * bytes_per_word) % x, zip(*[iter(map(ord, line))] * bytes_per_word), ) if len(line) % 2: - tmp.append("%x" % ord(line[-1])) - dump += "0x%08x: %s\n" % (count, " ".join(tmp)) + tmp.append(f"{ord(line[-1]):x}") + tmp = " ".join(tmp) + dump += f"0x{count:08x}: {tmp}\n" count += len(line) return dump[:-1] @@ -261,9 +262,9 @@ def file_munge(pathname, header, text): file_demunge(pathname, header) with open(pathname, "a") as append_file: - append_file.write("# BEGIN %s\n" % header) + append_file.write(f"# BEGIN {header}\n") append_file.write(text) - append_file.write("# END %s\n" % header) + append_file.write(f"# END {header}\n") def file_demunge(pathname, header): @@ -281,9 +282,9 @@ def file_demunge(pathname, header): end = None for i, line in enumerate(lines): - if line == "# BEGIN %s\n" % header: + if line == f"# BEGIN {header}\n": start = i - elif line == "# END %s\n" % header: + elif line == f"# END {header}\n": end = i + 1 if start is None or end is None: @@ -305,7 +306,7 @@ def expand_corepath(pathname, session=None, node=None): :rtype: str """ if session is not None: - pathname = pathname.replace("~", "/home/%s" % session.user) + pathname = pathname.replace("~", f"/home/{session.user}") pathname = pathname.replace("%SESSION%", str(session.id)) pathname = pathname.replace("%SESSION_DIR%", session.session_dir) pathname = pathname.replace("%SESSION_USER%", session.user) @@ -364,7 +365,7 @@ def load_classes(path, clazz): # validate path exists logging.debug("attempting to load modules from path: %s", path) if not os.path.isdir(path): - logging.warning("invalid custom module directory specified" ": %s" % path) + logging.warning("invalid custom module directory specified" ": %s", path) # check if path is in sys.path parent_path = os.path.dirname(path) if parent_path not in sys.path: @@ -380,7 +381,7 @@ def load_classes(path, clazz): # import and add all service modules in the path classes = [] for module_name in module_names: - import_statement = "%s.%s" % (base_module, module_name) + import_statement = f"{base_module}.{module_name}" logging.debug("importing custom module: %s", import_statement) try: module = importlib.import_module(import_statement) diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 78d1c488..f4a360c6 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -600,7 +600,7 @@ class CoreXmlReader(object): name = hook.get("name") state = hook.get("state") data = hook.text - hook_type = "hook:%s" % state + hook_type = f"hook:{state}" logging.info("reading hook: state(%s) name(%s)", state, name) self.session.set_hook( hook_type, file_name=name, source_name=None, data=data diff --git a/daemon/core/xml/corexmldeployment.py b/daemon/core/xml/corexmldeployment.py index 0a81b75e..239bace2 100644 --- a/daemon/core/xml/corexmldeployment.py +++ b/daemon/core/xml/corexmldeployment.py @@ -31,20 +31,20 @@ def add_emane_interface(host_element, netif, platform_name="p1", transport_name= host_id = host_element.get("id") # platform data - platform_id = "%s/%s" % (host_id, platform_name) + platform_id = f"{host_id}/{platform_name}" platform_element = etree.SubElement( host_element, "emanePlatform", id=platform_id, name=platform_name ) # transport data - transport_id = "%s/%s" % (host_id, transport_name) + transport_id = f"{host_id}/{transport_name}" etree.SubElement( platform_element, "transport", id=transport_id, name=transport_name ) # nem data - nem_name = "nem%s" % nem_id - nem_element_id = "%s/%s" % (host_id, nem_name) + nem_name = f"nem{nem_id}" + nem_element_id = f"{host_id}/{nem_name}" nem_element = etree.SubElement( platform_element, "nem", id=nem_element_id, name=nem_name ) @@ -68,7 +68,7 @@ def get_address_type(address): def get_ipv4_addresses(hostname): if hostname == "localhost": addresses = [] - args = "%s -o -f inet address show" % IP_BIN + args = f"{IP_BIN} -o -f inet address show" output = utils.check_cmd(args) for line in output.split(os.linesep): split = line.split() @@ -94,13 +94,12 @@ class CoreXmlDeployment(object): self.add_deployment() def find_device(self, name): - device = self.scenario.find("devices/device[@name='%s']" % name) + device = self.scenario.find(f"devices/device[@name='{name}']") return device def find_interface(self, device, name): interface = self.scenario.find( - "devices/device[@name='%s']/interfaces/interface[@name='%s']" - % (device.name, name) + f"devices/device[@name='{device.name}']/interfaces/interface[@name='{name}']" ) return interface @@ -114,7 +113,8 @@ class CoreXmlDeployment(object): def add_physical_host(self, name): # add host - host_id = "%s/%s" % (self.root.get("id"), name) + root_id = self.root.get("id") + host_id = f"{root_id}/{name}" host_element = etree.SubElement(self.root, "testHost", id=host_id, name=name) # add type element @@ -128,10 +128,11 @@ class CoreXmlDeployment(object): def add_virtual_host(self, physical_host, node): if not isinstance(node, CoreNodeBase): - raise TypeError("invalid node type: %s" % node) + raise TypeError(f"invalid node type: {node}") # create virtual host element - host_id = "%s/%s" % (physical_host.get("id"), node.name) + phys_id = physical_host.get("id") + host_id = f"{phys_id}/{node.name}" host_element = etree.SubElement( physical_host, "testHost", id=host_id, name=node.name ) diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index 41319ea4..c97c176f 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -57,8 +57,7 @@ def create_file(xml_element, doc_name, file_path, server=None): :return: nothing """ doctype = ( - '' - % {"doc_name": doc_name} + f'' ) if server is not None: temp = NamedTemporaryFile(delete=False) @@ -208,7 +207,7 @@ def build_node_platform_xml(emane_manager, control_net, node, nem_id, platform_x node.setnemid(netif, nem_id) macstr = _hwaddr_prefix + ":00:00:" - macstr += "%02X:%02X" % ((nem_id >> 8) & 0xFF, nem_id & 0xFF) + macstr += f"{(nem_id >> 8) & 0xFF:02X}:{nem_id & 0xFF:02X}" netif.sethwaddr(MacAddress.from_string(macstr)) # increment nem id @@ -222,7 +221,7 @@ def build_node_platform_xml(emane_manager, control_net, node, nem_id, platform_x file_path = os.path.join(emane_manager.session.session_dir, file_name) create_file(platform_element, doc_name, file_path) else: - file_name = "platform%d.xml" % key + file_name = f"platform{key}.xml" file_path = os.path.join(emane_manager.session.session_dir, file_name) linked_node = emane_manager.session.nodes[key] create_file(platform_element, doc_name, file_path, linked_node.server) @@ -290,8 +289,8 @@ def build_transport_xml(emane_manager, node, transport_type): """ transport_element = etree.Element( "transport", - name="%s Transport" % transport_type.capitalize(), - library="trans%s" % transport_type.lower(), + name=f"{transport_type.capitalize()} Transport", + library=f"trans{transport_type.lower()}", ) # add bitrate @@ -330,7 +329,7 @@ def create_phy_xml(emane_model, config, file_path, server): will run on, default is None for localhost :return: nothing """ - phy_element = etree.Element("phy", name="%s PHY" % emane_model.name) + phy_element = etree.Element("phy", name=f"{emane_model.name} PHY") if emane_model.phy_library: phy_element.set("library", emane_model.phy_library) @@ -362,7 +361,7 @@ def create_mac_xml(emane_model, config, file_path, server): raise ValueError("must define emane model library") mac_element = etree.Element( - "mac", name="%s MAC" % emane_model.name, library=emane_model.mac_library + "mac", name=f"{emane_model.name} MAC", library=emane_model.mac_library ) add_configurations( mac_element, emane_model.mac_config, config, emane_model.config_ignore @@ -399,7 +398,7 @@ def create_nem_xml( will run on, default is None for localhost :return: nothing """ - nem_element = etree.Element("nem", name="%s NEM" % emane_model.name) + nem_element = etree.Element("nem", name=f"{emane_model.name} NEM") if is_external(config): nem_element.set("type", "unstructured") else: @@ -450,7 +449,7 @@ def transport_file_name(node_id, transport_type): :param str transport_type: transport type to generate transport file :return: """ - return "n%strans%s.xml" % (node_id, transport_type) + return f"n{node_id}trans{transport_type}.xml" def _basename(emane_model, interface=None): @@ -461,14 +460,14 @@ def _basename(emane_model, interface=None): :return: basename used for file creation :rtype: str """ - name = "n%s" % emane_model.id + name = f"n{emane_model.id}" if interface: node_id = interface.node.id if emane_model.session.emane.getifcconfig(node_id, interface, emane_model.name): name = interface.localname.replace(".", "_") - return "%s%s" % (name, emane_model.name) + return f"{name}{emane_model.name}" def nem_file_name(emane_model, interface=None): @@ -484,7 +483,7 @@ def nem_file_name(emane_model, interface=None): append = "" if interface and interface.transport_type == "raw": append = "_raw" - return "%snem%s.xml" % (basename, append) + return f"{basename}nem{append}.xml" def shim_file_name(emane_model, interface=None): @@ -496,7 +495,8 @@ def shim_file_name(emane_model, interface=None): :return: shim xml filename :rtype: str """ - return "%sshim.xml" % _basename(emane_model, interface) + name = _basename(emane_model, interface) + return f"{name}shim.xml" def mac_file_name(emane_model, interface=None): @@ -508,7 +508,8 @@ def mac_file_name(emane_model, interface=None): :return: mac xml filename :rtype: str """ - return "%smac.xml" % _basename(emane_model, interface) + name = _basename(emane_model, interface) + return f"{name}mac.xml" def phy_file_name(emane_model, interface=None): @@ -520,4 +521,5 @@ def phy_file_name(emane_model, interface=None): :return: phy xml filename :rtype: str """ - return "%sphy.xml" % _basename(emane_model, interface) + name = _basename(emane_model, interface) + return f"{name}phy.xml" diff --git a/daemon/examples/python/emane80211.py b/daemon/examples/python/emane80211.py index 93222f9e..d77252da 100644 --- a/daemon/examples/python/emane80211.py +++ b/daemon/examples/python/emane80211.py @@ -48,12 +48,11 @@ def main(): logging.basicConfig(level=logging.INFO) options = parser.parse_options("emane80211") start = datetime.datetime.now() - print( - "running emane 80211 example: nodes(%s) time(%s)" - % (options.nodes, options.time) + logging.info( + "running emane 80211 example: nodes(%s) time(%s)", options.nodes, options.time ) example(options) - print("elapsed time: %s" % (datetime.datetime.now() - start)) + logging.info("elapsed time: %s", datetime.datetime.now() - start) if __name__ == "__main__" or __name__ == "__builtin__": diff --git a/daemon/examples/python/parser.py b/daemon/examples/python/parser.py index fdc2591a..d9efdab6 100644 --- a/daemon/examples/python/parser.py +++ b/daemon/examples/python/parser.py @@ -6,7 +6,7 @@ DEFAULT_STEP = 1 def parse_options(name): - parser = argparse.ArgumentParser(description="Run %s example" % name) + parser = argparse.ArgumentParser(description=f"Run {name} example") parser.add_argument( "-n", "--nodes", @@ -25,8 +25,8 @@ def parse_options(name): options = parser.parse_args() if options.nodes < 2: - parser.error("invalid min number of nodes: %s" % options.nodes) + parser.error(f"invalid min number of nodes: {options.nodes}") if options.time < 1: - parser.error("invalid test time: %s" % options.time) + parser.error(f"invalid test time: {options.time}") return options diff --git a/daemon/examples/python/switch.py b/daemon/examples/python/switch.py index 6702802e..80257a4a 100644 --- a/daemon/examples/python/switch.py +++ b/daemon/examples/python/switch.py @@ -41,14 +41,12 @@ def example(options): first_node = session.get_node(2) last_node = session.get_node(options.nodes + 1) - print("starting iperf server on node: %s" % first_node.name) + logging.info("starting iperf server on node: %s", first_node.name) first_node.node_net_cmd("iperf -s -D") first_node_address = prefixes.ip4_address(first_node) - print("node %s connecting to %s" % (last_node.name, first_node_address)) - output = last_node.node_net_cmd( - "iperf -t %s -c %s" % (options.time, first_node_address) - ) - print(output) + logging.info("node %s connecting to %s", last_node.name, first_node_address) + output = last_node.node_net_cmd(f"iperf -t {options.time} -c {first_node_address}") + logging.info(output) first_node.node_net_cmd("killall -9 iperf") # shutdown session @@ -59,9 +57,11 @@ def main(): logging.basicConfig(level=logging.INFO) options = parser.parse_options("switch") start = datetime.datetime.now() - print("running switch example: nodes(%s) time(%s)" % (options.nodes, options.time)) + logging.info( + "running switch example: nodes(%s) time(%s)", options.nodes, options.time + ) example(options) - print("elapsed time: %s" % (datetime.datetime.now() - start)) + logging.info("elapsed time: %s", datetime.datetime.now() - start) if __name__ == "__main__": diff --git a/daemon/examples/python/wlan.py b/daemon/examples/python/wlan.py index b3b4544e..9506a35c 100644 --- a/daemon/examples/python/wlan.py +++ b/daemon/examples/python/wlan.py @@ -45,11 +45,11 @@ def example(options): first_node = session.get_node(2) last_node = session.get_node(options.nodes + 1) - print("starting iperf server on node: %s" % first_node.name) + logging.info("starting iperf server on node: %s", first_node.name) first_node.node_net_cmd("iperf -s -D") address = prefixes.ip4_address(first_node) - print("node %s connecting to %s" % (last_node.name, address)) - last_node.node_net_cmd("iperf -t %s -c %s" % (options.time, address)) + logging.info("node %s connecting to %s", last_node.name, address) + last_node.node_net_cmd(f"iperf -t {options.time} -c {address}") first_node.node_net_cmd("killall -9 iperf") # shutdown session @@ -61,9 +61,11 @@ def main(): options = parser.parse_options("wlan") start = datetime.datetime.now() - print("running wlan example: nodes(%s) time(%s)" % (options.nodes, options.time)) + logging.info( + "running wlan example: nodes(%s) time(%s)", options.nodes, options.time + ) example(options) - print("elapsed time: %s" % (datetime.datetime.now() - start)) + logging.info("elapsed time: %s", datetime.datetime.now() - start) if __name__ == "__main__": diff --git a/daemon/scripts/core-daemon b/daemon/scripts/core-daemon index 07c6a9a7..49aae2d2 100755 --- a/daemon/scripts/core-daemon +++ b/daemon/scripts/core-daemon @@ -17,7 +17,9 @@ from core import constants from core.api.grpc.server import CoreGrpcServer from core.api.tlv.corehandlers import CoreHandler, CoreUdpHandler from core.api.tlv.coreserver import CoreServer, CoreUdpServer +from core.constants import CORE_CONF_DIR, COREDPY_VERSION from core.emulator import enumerations +from core.emulator.enumerations import CORE_API_PORT from core.utils import close_onexec, load_logging_config @@ -67,7 +69,9 @@ def cored(cfg): # initialize grpc api if cfg["grpc"] == "True": grpc_server = CoreGrpcServer(server.coreemu) - grpc_address = "%s:%s" % (cfg["grpcaddress"], cfg["grpcport"]) + address_config = cfg["grpcaddress"] + port_config = cfg["grpcport"] + grpc_address = f"{address_config}:{port_config}" grpc_thread = threading.Thread(target=grpc_server.listen, args=(grpc_address,)) grpc_thread.daemon = True grpc_thread.start() @@ -91,30 +95,34 @@ def get_merged_config(filename): :rtype: dict """ # these are the defaults used in the config file + default_log = os.path.join(constants.CORE_CONF_DIR, "logging.conf") + default_grpc_port = "50051" + default_threads = "1" + default_address = "localhost" defaults = { - "port": "%d" % enumerations.CORE_API_PORT, - "listenaddr": "localhost", - "numthreads": "1", - "grpcport": "50051", - "grpcaddress": "localhost", - "logfile": os.path.join(constants.CORE_CONF_DIR, "logging.conf") + "port": str(CORE_API_PORT), + "listenaddr": default_address, + "numthreads": default_threads, + "grpcport": default_grpc_port, + "grpcaddress": default_address, + "logfile": default_log } parser = argparse.ArgumentParser( - description="CORE daemon v.%s instantiates Linux network namespace nodes." % constants.COREDPY_VERSION) + description=f"CORE daemon v.{COREDPY_VERSION} instantiates Linux network namespace nodes.") parser.add_argument("-f", "--configfile", dest="configfile", - help="read config from specified file; default = %s" % filename) + help=f"read config from specified file; default = {filename}") parser.add_argument("-p", "--port", dest="port", type=int, - help="port number to listen on; default = %s" % defaults["port"]) + help=f"port number to listen on; default = {CORE_API_PORT}") parser.add_argument("-n", "--numthreads", dest="numthreads", type=int, - help="number of server threads; default = %s" % defaults["numthreads"]) + help=f"number of server threads; default = {default_threads}") parser.add_argument("--ovs", action="store_true", help="enable experimental ovs mode, default is false") parser.add_argument("--grpc", action="store_true", help="enable grpc api, default is false") parser.add_argument("--grpc-port", dest="grpcport", - help="grpc port to listen on; default %s" % defaults["grpcport"]) + help=f"grpc port to listen on; default {default_grpc_port}") parser.add_argument("--grpc-address", dest="grpcaddress", - help="grpc address to listen on; default %s" % defaults["grpcaddress"]) - parser.add_argument("-l", "--logfile", help="core logging configuration; default %s" % defaults["logfile"]) + help=f"grpc address to listen on; default {default_address}") + parser.add_argument("-l", "--logfile", help=f"core logging configuration; default {default_log}") # parse command line options args = parser.parse_args() @@ -146,7 +154,7 @@ def main(): :return: nothing """ # get a configuration merged from config file and command-line arguments - cfg = get_merged_config("%s/core.conf" % constants.CORE_CONF_DIR) + cfg = get_merged_config(f"{CORE_CONF_DIR}/core.conf") # load logging configuration load_logging_config(cfg["logfile"]) diff --git a/daemon/scripts/core-manage b/daemon/scripts/core-manage index 93247f6f..d9b9de08 100755 --- a/daemon/scripts/core-manage +++ b/daemon/scripts/core-manage @@ -38,7 +38,7 @@ class FileUpdater(object): txt = "Updating" if self.action == "check": txt = "Checking" - sys.stdout.write("%s file: %s\n" % (txt, self.filename)) + sys.stdout.write(f"{txt} file: {self.filename}\n") if self.target == "service": r = self.update_file(fn=self.update_services) @@ -52,9 +52,9 @@ class FileUpdater(object): if not r: txt = "NOT " if self.action == "check": - sys.stdout.write("String %sfound.\n" % txt) + sys.stdout.write(f"String {txt} found.\n") else: - sys.stdout.write("File %supdated.\n" % txt) + sys.stdout.write(f"File {txt} updated.\n") return r @@ -70,7 +70,7 @@ class FileUpdater(object): r = self.update_keyvals(key, vals) if self.action == "check": return r - valstr = "%s" % r + valstr = str(r) return "= ".join([key, valstr]) + "\n" def update_emane_models(self, line): @@ -125,7 +125,7 @@ class FileUpdater(object): else: raise ValueError("unknown target") if not os.path.exists(filename): - raise ValueError("file %s does not exist" % filename) + raise ValueError(f"file {filename} does not exist") return search, filename def update_file(self, fn=None): @@ -187,18 +187,17 @@ class FileUpdater(object): def main(): + actions = ", ".join(FileUpdater.actions) + targets = ", ".join(FileUpdater.targets) usagestr = "usage: %prog [-h] [options] \n" usagestr += "\nHelper tool to add, remove, or check for " usagestr += "services, models, and node types\nin a CORE installation.\n" usagestr += "\nExamples:\n %prog add service newrouting" usagestr += "\n %prog -v check model RfPipe" usagestr += "\n %prog --userpath=\"$HOME/.core\" add nodetype \"{ftp ftp.gif ftp.gif {DefaultRoute FTP} netns {FTP server} }\" \n" - usagestr += "\nArguments:\n should be one of: %s" % \ - ", ".join(FileUpdater.actions) - usagestr += "\n should be one of: %s" % \ - ", ".join(FileUpdater.targets) - usagestr += "\n is the text to %s" % \ - ", ".join(FileUpdater.actions) + usagestr += f"\nArguments:\n should be one of: {actions}" + usagestr += f"\n should be one of: {targets}" + usagestr += f"\n is the text to {actions}" parser = optparse.OptionParser(usage=usagestr) parser.set_defaults(userpath=None, verbose=False, ) @@ -222,14 +221,14 @@ def main(): action = args[0] if action not in FileUpdater.actions: - usage("invalid action %s" % action, 1) + usage(f"invalid action {action}", 1) target = args[1] if target not in FileUpdater.targets: - usage("invalid target %s" % target, 1) + usage(f"invalid target {target}", 1) if target == "nodetype" and not options.userpath: - usage("user path option required for this target (%s)" % target) + usage(f"user path option required for this target ({target})") data = args[2] @@ -237,7 +236,7 @@ def main(): up = FileUpdater(action, target, data, options) r = up.process() except Exception as e: - sys.stderr.write("Exception: %s\n" % e) + sys.stderr.write(f"Exception: {e}\n") sys.exit(1) if not r: sys.exit(1) diff --git a/daemon/scripts/coresendmsg b/daemon/scripts/coresendmsg index c8813f21..c8dccfd7 100755 --- a/daemon/scripts/coresendmsg +++ b/daemon/scripts/coresendmsg @@ -21,9 +21,9 @@ def print_available_tlvs(t, tlv_class): """ Print a TLV list. """ - print("TLVs available for %s message:" % t) + print(f"TLVs available for {t} message:") for tlv in sorted([tlv for tlv in tlv_class.tlv_type_map], key=lambda x: x.name): - print("%s:%s" % (tlv.value, tlv.name)) + print(f"{tlv.value}:{tlv.name}") def print_examples(name): @@ -54,9 +54,9 @@ def print_examples(name): "srcname=\"./test.log\"", "move a test.log file from host to node 2"), ] - print("Example %s invocations:" % name) + print(f"Example {name} invocations:") for cmd, descr in examples: - print(" %s %s\n\t\t%s" % (name, cmd, descr)) + print(f" {name} {cmd}\n\t\t{descr}") def receive_message(sock): @@ -86,11 +86,11 @@ def receive_message(sock): except KeyError: msg = coreapi.CoreMessage(msgflags, msghdr, msgdata) msg.message_type = msgtype - print("unimplemented CORE message type: %s" % msg.type_str()) + print(f"unimplemented CORE message type: {msg.type_str()}") return msg if len(data) > msglen + coreapi.CoreMessage.header_len: - print("received a message of type %d, dropping %d bytes of extra data" \ - % (msgtype, len(data) - (msglen + coreapi.CoreMessage.header_len))) + data_size = len(data) - (msglen + coreapi.CoreMessage.header_len) + print(f"received a message of type {msgtype}, dropping {data_size} bytes of extra data") return msgcls(msgflags, msghdr, msgdata) @@ -132,7 +132,7 @@ def connect_to_session(sock, requested): print("requested session not found!") return False - print("joining session: %s" % session) + print(f"joining session: {session}") tlvdata = coreapi.CoreSessionTlv.pack(SessionTlvs.NUMBER.value, session) flags = MessageFlags.ADD.value smsg = coreapi.CoreSessionMessage.pack(flags, tlvdata) @@ -147,9 +147,9 @@ def receive_response(sock, opt): print("waiting for response...") msg = receive_message(sock) if msg is None: - print("disconnected from %s:%s" % (opt.address, opt.port)) + print(f"disconnected from {opt.address}:{opt.port}") sys.exit(0) - print("received message: %s" % msg) + print(f"received message: {msg}") def main(): @@ -160,36 +160,36 @@ def main(): flags = [flag.name for flag in MessageFlags] usagestr = "usage: %prog [-h|-H] [options] [message-type] [flags=flags] " usagestr += "[message-TLVs]\n\n" - usagestr += "Supported message types:\n %s\n" % types - usagestr += "Supported message flags (flags=f1,f2,...):\n %s" % flags + usagestr += f"Supported message types:\n {types}\n" + usagestr += f"Supported message flags (flags=f1,f2,...):\n {flags}" parser = optparse.OptionParser(usage=usagestr) + default_address = "localhost" + default_session = None + default_tcp = False parser.set_defaults( port=CORE_API_PORT, - address="localhost", - session=None, + address=default_address, + session=default_session, listen=False, examples=False, tlvs=False, - tcp=False + tcp=default_tcp ) parser.add_option("-H", dest="examples", action="store_true", help="show example usage help message and exit") parser.add_option("-p", "--port", dest="port", type=int, - help="TCP port to connect to, default: %d" % \ - parser.defaults["port"]) + help=f"TCP port to connect to, default: {CORE_API_PORT}") parser.add_option("-a", "--address", dest="address", type=str, - help="Address to connect to, default: %s" % \ - parser.defaults["address"]) + help=f"Address to connect to, default: {default_address}") parser.add_option("-s", "--session", dest="session", type=str, - help="Session to join, default: %s" % \ - parser.defaults["session"]) + help=f"Session to join, default: {default_session}") parser.add_option("-l", "--listen", dest="listen", action="store_true", help="Listen for a response message and print it.") parser.add_option("-t", "--list-tlvs", dest="tlvs", action="store_true", help="List TLVs for the specified message type.") parser.add_option("--tcp", dest="tcp", action="store_true", - help="Use TCP instead of UDP and connect to a session default: %s" % parser.defaults["tcp"]) + help=f"Use TCP instead of UDP and connect to a session default: {default_tcp}") def usage(msg=None, err=0): sys.stdout.write("\n") @@ -209,7 +209,7 @@ def main(): # given a message type t, determine the message and TLV classes t = args.pop(0) if t not in types: - usage("Unknown message type requested: %s" % t) + usage(f"Unknown message type requested: {t}") message_type = MessageTypes[t] msg_cls = coreapi.CLASS_MAP[message_type.value] tlv_cls = msg_cls.tlv_class @@ -225,7 +225,7 @@ def main(): for a in args: typevalue = a.split("=") if len(typevalue) < 2: - usage("Use \"type=value\" syntax instead of \"%s\"." % a) + usage(f"Use \"type=value\" syntax instead of \"{a}\".") tlv_typestr = typevalue[0] tlv_valstr = "=".join(typevalue[1:]) if tlv_typestr == "flags": @@ -237,7 +237,7 @@ def main(): tlv_type = tlv_cls.tlv_type_map[tlv_name] tlvdata += tlv_cls.pack_string(tlv_type.value, tlv_valstr) except KeyError: - usage("Unknown TLV: \"%s\"" % tlv_name) + usage(f"Unknown TLV: \"{tlv_name}\"") flags = 0 for f in flagstr.split(","): @@ -249,7 +249,7 @@ def main(): n = flag_enum.value flags |= n except KeyError: - usage("Invalid flag \"%s\"." % f) + usage(f"Invalid flag \"{f}\".") msg = msg_cls.pack(flags, tlvdata) @@ -264,7 +264,7 @@ def main(): try: sock.connect((opt.address, opt.port)) except Exception as e: - print("Error connecting to %s:%s:\n\t%s" % (opt.address, opt.port, e)) + print(f"Error connecting to {opt.address}:{opt.port}:\n\t{e}") sys.exit(1) if opt.tcp and not connect_to_session(sock, opt.session): From 07b44080763dade1c3f0b0420da3d95a835e0cdf Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 18 Oct 2019 12:44:13 -0700 Subject: [PATCH 074/462] f string updates to all but services complete --- daemon/core/api/tlv/coreapi.py | 34 +++++------ daemon/core/api/tlv/corehandlers.py | 47 +++++++-------- daemon/core/nodes/network.py | 91 +++++++++++++---------------- 3 files changed, 75 insertions(+), 97 deletions(-) diff --git a/daemon/core/api/tlv/coreapi.py b/daemon/core/api/tlv/coreapi.py index ba737fd4..0fd16bf5 100644 --- a/daemon/core/api/tlv/coreapi.py +++ b/daemon/core/api/tlv/coreapi.py @@ -180,7 +180,7 @@ class CoreTlvDataString(CoreTlvData): :rtype: tuple """ if not isinstance(value, str): - raise ValueError("value not a string: %s" % type(value)) + raise ValueError(f"value not a string: {type(value)}") value = value.encode("utf-8") if len(value) < 256: @@ -220,7 +220,7 @@ class CoreTlvDataUint16List(CoreTlvData): :rtype: tuple """ if not isinstance(values, tuple): - raise ValueError("value not a tuple: %s" % values) + raise ValueError(f"value not a tuple: {values}") data = b"" for value in values: @@ -237,7 +237,8 @@ class CoreTlvDataUint16List(CoreTlvData): :param data: data to unpack :return: unpacked data """ - data_format = "!%dH" % (len(data) / 2) + size = int(len(data) / 2) + data_format = f"!{size}H" return struct.unpack(data_format, data) @classmethod @@ -435,7 +436,7 @@ class CoreTlv(object): try: return self.tlv_type_map(self.tlv_type).name except ValueError: - return "unknown tlv type: %s" % str(self.tlv_type) + return f"unknown tlv type: {self.tlv_type}" def __str__(self): """ @@ -444,11 +445,7 @@ class CoreTlv(object): :return: string representation :rtype: str """ - return "%s " % ( - self.__class__.__name__, - self.type_str(), - self.value, - ) + return f"{self.__class__.__name__} " class CoreNodeTlv(CoreTlv): @@ -734,7 +731,7 @@ class CoreMessage(object): :return: nothing """ if key in self.tlv_data: - raise KeyError("key already exists: %s (val=%s)" % (key, value)) + raise KeyError(f"key already exists: {key} (val={value})") self.tlv_data[key] = value @@ -793,7 +790,7 @@ class CoreMessage(object): try: return MessageTypes(self.message_type).name except ValueError: - return "unknown message type: %s" % str(self.message_type) + return f"unknown message type: {self.message_type}" def flag_str(self): """ @@ -810,12 +807,13 @@ class CoreMessage(object): try: message_flags.append(self.flag_map(flag).name) except ValueError: - message_flags.append("0x%x" % flag) + message_flags.append(f"0x{flag:x}") flag <<= 1 if not (self.flags & ~(flag - 1)): break - return "0x%x <%s>" % (self.flags, " | ".join(message_flags)) + message_flags = " | ".join(message_flags) + return f"0x{self.flags:x} <{message_flags}>" def __str__(self): """ @@ -824,20 +822,16 @@ class CoreMessage(object): :return: string representation :rtype: str """ - result = "%s " % ( - self.__class__.__name__, - self.type_str(), - self.flag_str(), - ) + result = f"{self.__class__.__name__} " for key in self.tlv_data: value = self.tlv_data[key] try: tlv_type = self.tlv_class.tlv_type_map(key).name except ValueError: - tlv_type = "tlv type %s" % key + tlv_type = f"tlv type {key}" - result += "\n %s: %s" % (tlv_type, value) + result += f"\n {tlv_type}: {value}" return result diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index f3966ba1..85b7b831 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -75,7 +75,7 @@ class CoreHandler(socketserver.BaseRequestHandler): self.handler_threads = [] num_threads = int(server.config["numthreads"]) if num_threads < 1: - raise ValueError("invalid number of threads: %s" % num_threads) + raise ValueError(f"invalid number of threads: {num_threads}") logging.debug("launching core server handler threads: %s", num_threads) for _ in range(num_threads): @@ -460,7 +460,7 @@ class CoreHandler(socketserver.BaseRequestHandler): try: header = self.request.recv(coreapi.CoreMessage.header_len) except IOError as e: - raise IOError("error receiving header (%s)" % e) + raise IOError(f"error receiving header ({e})") if len(header) != coreapi.CoreMessage.header_len: if len(header) == 0: @@ -478,10 +478,7 @@ class CoreHandler(socketserver.BaseRequestHandler): while len(data) < message_len: data += self.request.recv(message_len - len(data)) if len(data) > message_len: - error_message = ( - "received message length does not match received data (%s != %s)" - % (len(data), message_len) - ) + error_message = f"received message length does not match received data ({len(data)} != {message_len})" logging.error(error_message) raise IOError(error_message) @@ -573,11 +570,7 @@ class CoreHandler(socketserver.BaseRequestHandler): ) except KeyError: # multiple TLVs of same type cause KeyError exception - reply_message = "CoreMessage (type %d flags %d length %d)" % ( - message_type, - message_flags, - message_length, - ) + reply_message = f"CoreMessage (type {message_type} flags {message_flags} length {message_length})" logging.debug("sending reply:\n%s", reply_message) @@ -999,7 +992,7 @@ class CoreHandler(socketserver.BaseRequestHandler): RegisterTlvs.EXECUTE_SERVER.value, execute_server ) tlv_data += coreapi.CoreRegisterTlv.pack( - RegisterTlvs.SESSION.value, "%s" % sid + RegisterTlvs.SESSION.value, str(sid) ) message = coreapi.CoreRegMessage.pack(0, tlv_data) replies.append(message) @@ -1104,7 +1097,7 @@ class CoreHandler(socketserver.BaseRequestHandler): self.session.mobility.config_reset(node_id) self.session.emane.config_reset(node_id) else: - raise Exception("cant handle config all: %s" % message_type) + raise Exception(f"cant handle config all: {message_type}") return replies @@ -1158,7 +1151,7 @@ class CoreHandler(socketserver.BaseRequestHandler): if metadata_configs is None: metadata_configs = {} data_values = "|".join( - ["%s=%s" % (x, metadata_configs[x]) for x in metadata_configs] + [f"{x}={metadata_configs[x]}" for x in metadata_configs] ) data_types = tuple(ConfigDataTypes.STRING.value for _ in metadata_configs) config_response = ConfigData( @@ -1239,7 +1232,7 @@ class CoreHandler(socketserver.BaseRequestHandler): services = sorted(group_map[group], key=lambda x: x.name.lower()) logging.debug("sorted services for group(%s): %s", group, services) end_index = start_index + len(services) - 1 - group_strings.append("%s:%s-%s" % (group, start_index, end_index)) + group_strings.append(f"{group}:{start_index}-{end_index}") start_index += len(services) for service_name in services: captions.append(service_name.name) @@ -1714,24 +1707,24 @@ class CoreHandler(socketserver.BaseRequestHandler): ): status = self.session.services.stop_service(node, service) if status: - fail += "Stop %s," % service.name + fail += f"Stop {service.name}," if ( event_type == EventTypes.START.value or event_type == EventTypes.RESTART.value ): status = self.session.services.startup_service(node, service) if status: - fail += "Start %s(%s)," % service.name + fail += f"Start ({service.name})," if event_type == EventTypes.PAUSE.value: status = self.session.services.validate_service(node, service) if status: - fail += "%s," % service.name + fail += f"{service.name}," if event_type == EventTypes.RECONFIGURE.value: self.session.services.service_reconfigure(node, service) fail_data = "" if len(fail) > 0: - fail_data += "Fail:" + fail + fail_data += f"Fail:{fail}" unknown_data = "" num = len(unknown) if num > 0: @@ -1741,14 +1734,14 @@ class CoreHandler(socketserver.BaseRequestHandler): unknown_data += ", " num -= 1 logging.warning("Event requested for unknown service(s): %s", unknown_data) - unknown_data = "Unknown:" + unknown_data + unknown_data = f"Unknown:{unknown_data}" event_data = EventData( node=node_id, event_type=event_type, name=name, data=fail_data + ";" + unknown_data, - time="%s" % time.time(), + time=str(time.time()), ) self.session.broadcast_event(event_data) @@ -1769,7 +1762,7 @@ class CoreHandler(socketserver.BaseRequestHandler): thumb = message.get_tlv(SessionTlvs.THUMB.value) user = message.get_tlv(SessionTlvs.USER.value) logging.debug( - "SESSION message flags=0x%x sessions=%s" % (message.flags, session_id_str) + "SESSION message flags=0x%x sessions=%s", message.flags, session_id_str ) if message.flags == 0: @@ -1940,7 +1933,7 @@ class CoreHandler(socketserver.BaseRequestHandler): # service customizations service_configs = self.session.services.all_configs() for node_id, service in service_configs: - opaque = "service:%s" % service.name + opaque = f"service:{service.name}" data_types = tuple( repeat(ConfigDataTypes.STRING.value, len(ServiceShim.keys)) ) @@ -1976,7 +1969,7 @@ class CoreHandler(socketserver.BaseRequestHandler): file_data = FileData( message_type=MessageFlags.ADD.value, name=str(file_name), - type="hook:%s" % state, + type=f"hook:{state}", data=str(config_data), ) self.session.broadcast_file(file_data) @@ -1992,7 +1985,7 @@ class CoreHandler(socketserver.BaseRequestHandler): metadata_configs = self.session.metadata.get_configs() if metadata_configs: data_values = "|".join( - ["%s=%s" % (x, metadata_configs[x]) for x in metadata_configs] + [f"{x}={metadata_configs[x]}" for x in metadata_configs] ) data_types = tuple( ConfigDataTypes.STRING.value @@ -2041,7 +2034,7 @@ class CoreUdpHandler(CoreHandler): data = self.request[0] header = data[: coreapi.CoreMessage.header_len] if len(header) < coreapi.CoreMessage.header_len: - raise IOError("error receiving header (received %d bytes)" % len(header)) + raise IOError(f"error receiving header (received {len(header)} bytes)") message_type, message_flags, message_len = coreapi.CoreMessage.unpack_header( header @@ -2136,7 +2129,7 @@ class CoreUdpHandler(CoreHandler): :return: """ raise Exception( - "Unable to queue %s message for later processing using UDP!" % msg + f"Unable to queue {msg} message for later processing using UDP!" ) def sendall(self, data): diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 98bec198..229005c4 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -97,7 +97,7 @@ class EbtablesQueue(object): :return: ebtable atomic command :rtype: list[str] """ - return "%s --atomic-file %s %s" % (EBTABLES_BIN, self.atomic_file, cmd) + return f"{EBTABLES_BIN} --atomic-file {self.atomic_file} {cmd}" def lastupdate(self, wlan): """ @@ -175,7 +175,7 @@ class EbtablesQueue(object): wlan.net_cmd(args) try: - wlan.net_cmd("rm -f %s" % self.atomic_file) + wlan.net_cmd(f"rm -f {self.atomic_file}") except CoreCommandError: logging.exception("error removing atomic file: %s", self.atomic_file) @@ -198,26 +198,22 @@ class EbtablesQueue(object): """ with wlan._linked_lock: # flush the chain - self.cmds.append("-F %s" % wlan.brname) + self.cmds.append(f"-F {wlan.brname}") # rebuild the chain for netif1, v in wlan._linked.items(): for netif2, linked in v.items(): if wlan.policy == "DROP" and linked: self.cmds.extend( [ - "-A %s -i %s -o %s -j ACCEPT" - % (wlan.brname, netif1.localname, netif2.localname), - "-A %s -o %s -i %s -j ACCEPT" - % (wlan.brname, netif1.localname, netif2.localname), + f"-A {wlan.brname} -i {netif1.localname} -o {netif2.localname} -j ACCEPT", + f"-A {wlan.brname} -o {netif1.localname} -i {netif2.localname} -j ACCEPT", ] ) elif wlan.policy == "ACCEPT" and not linked: self.cmds.extend( [ - "-A %s -i %s -o %s -j DROP" - % (wlan.brname, netif1.localname, netif2.localname), - "-A %s -o %s -i %s -j DROP" - % (wlan.brname, netif1.localname, netif2.localname), + f"-A {wlan.brname} -i {netif1.localname} -o {netif2.localname} -j DROP", + f"-A {wlan.brname} -o {netif1.localname} -i {netif2.localname} -j DROP", ] ) @@ -268,7 +264,7 @@ class CoreNetwork(CoreNetworkBase): self.policy = policy self.name = name sessionid = self.session.short_session_id() - self.brname = "b.%s.%s" % (str(self.id), sessionid) + self.brname = f"b.{self.id}.{sessionid}" self.up = False if start: self.startup() @@ -303,9 +299,8 @@ class CoreNetwork(CoreNetworkBase): # create a new ebtables chain for this bridge cmds = [ - "%s -N %s -P %s" % (EBTABLES_BIN, self.brname, self.policy), - "%s -A FORWARD --logical-in %s -j %s" - % (EBTABLES_BIN, self.brname, self.brname), + f"{EBTABLES_BIN} -N {self.brname} -P {self.policy}", + f"{EBTABLES_BIN} -A FORWARD --logical-in {self.brname} -j {self.brname}", ] ebtablescmds(self.net_cmd, cmds) @@ -325,9 +320,8 @@ class CoreNetwork(CoreNetworkBase): try: self.net_client.delete_bridge(self.brname) cmds = [ - "%s -D FORWARD --logical-in %s -j %s" - % (EBTABLES_BIN, self.brname, self.brname), - "%s -X %s" % (EBTABLES_BIN, self.brname), + f"{EBTABLES_BIN} -D FORWARD --logical-in {self.brname} -j {self.brname}", + f"{EBTABLES_BIN} -X {self.brname}", ] ebtablescmds(self.net_cmd, cmds) except CoreCommandError: @@ -379,10 +373,10 @@ class CoreNetwork(CoreNetworkBase): """ # check if the network interfaces are attached to this network if self._netif[netif1.netifi] != netif1: - raise ValueError("inconsistency for netif %s" % netif1.name) + raise ValueError(f"inconsistency for netif {netif1.name}") if self._netif[netif2.netifi] != netif2: - raise ValueError("inconsistency for netif %s" % netif2.name) + raise ValueError(f"inconsistency for netif {netif2.name}") try: linked = self._linked[netif1][netif2] @@ -392,7 +386,7 @@ class CoreNetwork(CoreNetworkBase): elif self.policy == "DROP": linked = False else: - raise Exception("unknown policy: %s" % self.policy) + raise Exception(f"unknown policy: {self.policy}") self._linked[netif1][netif2] = linked return linked @@ -455,7 +449,7 @@ class CoreNetwork(CoreNetworkBase): """ if devname is None: devname = netif.localname - tc = "%s qdisc replace dev %s" % (TC_BIN, devname) + tc = f"{TC_BIN} qdisc replace dev {devname}" parent = "root" changed = False if netif.setparam("bw", bw): @@ -464,16 +458,16 @@ class CoreNetwork(CoreNetworkBase): burst = max(2 * netif.mtu, bw / 1000) # max IP payload limit = 0xFFFF - tbf = "tbf rate %s burst %s limit %s" % (bw, burst, limit) + tbf = f"tbf rate {bw} burst {burst} limit {limit}" if bw > 0: if self.up: - cmd = "%s %s handle 1: %s" % (tc, parent, tbf) + cmd = f"{tc} {parent} handle 1: {tbf}" netif.net_cmd(cmd) netif.setparam("has_tbf", True) changed = True elif netif.getparam("has_tbf") and bw <= 0: if self.up: - cmd = "%s qdisc delete dev %s %s" % (TC_BIN, devname, parent) + cmd = f"{TC_BIN} qdisc delete dev {devname} {parent}" netif.net_cmd(cmd) netif.setparam("has_tbf", False) # removing the parent removes the child @@ -494,17 +488,17 @@ class CoreNetwork(CoreNetworkBase): return # jitter and delay use the same delay statement if delay is not None: - netem += " delay %sus" % delay + netem += f" delay {delay}us" if jitter is not None: if delay is None: - netem += " delay 0us %sus 25%%" % jitter + netem += f" delay 0us {jitter}us 25%" else: - netem += " %sus 25%%" % jitter + netem += f" {jitter}us 25%" if loss is not None and loss > 0: - netem += " loss %s%%" % min(loss, 100) + netem += f" loss {min(loss, 100)}%" if duplicate is not None and duplicate > 0: - netem += " duplicate %s%%" % min(duplicate, 100) + netem += f" duplicate {min(duplicate, 100)}%" delay_check = delay is None or delay <= 0 jitter_check = jitter is None or jitter <= 0 @@ -515,16 +509,13 @@ class CoreNetwork(CoreNetworkBase): if not netif.getparam("has_netem"): return if self.up: - cmd = "%s qdisc delete dev %s %s handle 10:" % (TC_BIN, devname, parent) + cmd = f"{TC_BIN} qdisc delete dev {devname} {parent} handle 10:" netif.net_cmd(cmd) netif.setparam("has_netem", False) elif len(netem) > 1: if self.up: - cmd = "%s qdisc replace dev %s %s handle 10: %s" % ( - TC_BIN, - devname, - parent, - netem, + cmd = ( + f"{TC_BIN} qdisc replace dev {devname} {parent} handle 10: {netem}" ) netif.net_cmd(cmd) netif.setparam("has_netem", True) @@ -540,22 +531,22 @@ class CoreNetwork(CoreNetworkBase): """ sessionid = self.session.short_session_id() try: - _id = "%x" % self.id + _id = f"{self.id:x}" except TypeError: - _id = "%s" % self.id + _id = str(self.id) try: - net_id = "%x" % net.id + net_id = f"{net.id:x}" except TypeError: - net_id = "%s" % net.id + net_id = str(net.id) - localname = "veth%s.%s.%s" % (_id, net_id, sessionid) + localname = f"veth{_id}.{net_id}.{sessionid}" if len(localname) >= 16: - raise ValueError("interface local name %s too long" % localname) + raise ValueError(f"interface local name {localname} too long") - name = "veth%s.%s.%s" % (net_id, _id, sessionid) + name = f"veth{net_id}.{_id}.{sessionid}" if len(name) >= 16: - raise ValueError("interface name %s too long" % name) + raise ValueError(f"interface name {name} too long") netif = Veth(self.session, None, name, localname, start=self.up) self.attach(netif) @@ -689,7 +680,7 @@ class GreTapBridge(CoreNetwork): :return: nothing """ if self.gretap: - raise ValueError("gretap already exists for %s" % self.name) + raise ValueError(f"gretap already exists for {self.name}") remoteip = addrlist[0].split("/")[0] localip = None if len(addrlist) > 1: @@ -773,14 +764,14 @@ class CtrlNet(CoreNetwork): :return: """ use_ovs = self.session.options.get_config("ovs") == "True" - current = "%s/%s" % (address, self.prefix.prefixlen) + current = f"{address}/{self.prefix.prefixlen}" net_client = get_net_client(use_ovs, utils.check_cmd) net_client.create_address(self.brname, current) servers = self.session.distributed.servers for name in servers: server = servers[name] address -= 1 - current = "%s/%s" % (address, self.prefix.prefixlen) + current = f"{address}/{self.prefix.prefixlen}" net_client = get_net_client(use_ovs, server.remote_cmd) net_client.create_address(self.brname, current) @@ -792,7 +783,7 @@ class CtrlNet(CoreNetwork): :raises CoreCommandError: when there is a command exception """ if self.net_client.existing_bridges(self.id): - raise CoreError("old bridges exist for node: %s" % self.id) + raise CoreError(f"old bridges exist for node: {self.id}") CoreNetwork.startup(self) @@ -811,7 +802,7 @@ class CtrlNet(CoreNetwork): self.brname, self.updown_script, ) - self.net_cmd("%s %s startup" % (self.updown_script, self.brname)) + self.net_cmd(f"{self.updown_script} {self.brname} startup") if self.serverintf: self.net_client.create_interface(self.brname, self.serverintf) @@ -839,7 +830,7 @@ class CtrlNet(CoreNetwork): self.brname, self.updown_script, ) - self.net_cmd("%s %s shutdown" % (self.updown_script, self.brname)) + self.net_cmd(f"{self.updown_script} {self.brname} shutdown") except CoreCommandError: logging.exception("error issuing shutdown script shutdown") From 5633d4d18ba7e4b2f21dfa8855074532f71c5db9 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 18 Oct 2019 12:55:35 -0700 Subject: [PATCH 075/462] converted format strings to f strings --- daemon/core/api/grpc/server.py | 16 +++--------- daemon/core/nodes/base.py | 8 ++---- daemon/core/nodes/docker.py | 46 ++++++++++------------------------ daemon/core/nodes/lxd.py | 22 +++++++--------- 4 files changed, 28 insertions(+), 64 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index dcea5fb5..11bfcd6b 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -247,9 +247,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ session = self.coreemu.sessions.get(session_id) if not session: - context.abort( - grpc.StatusCode.NOT_FOUND, "session {} not found".format(session_id) - ) + context.abort(grpc.StatusCode.NOT_FOUND, f"session {session_id} not found") return session def get_node(self, session, node_id, context): @@ -265,9 +263,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): try: return session.get_node(node_id) except CoreError: - context.abort( - grpc.StatusCode.NOT_FOUND, "node {} not found".format(node_id) - ) + context.abort(grpc.StatusCode.NOT_FOUND, f"node {node_id} not found") def CreateSession(self, request, context): """ @@ -1577,17 +1573,13 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): nem_one = request.nem_one emane_one, netif = session.emane.nemlookup(nem_one) if not emane_one or not netif: - context.abort( - grpc.StatusCode.NOT_FOUND, "nem one {} not found".format(nem_one) - ) + context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem_one} not found") node_one = netif.node nem_two = request.nem_two emane_two, netif = session.emane.nemlookup(nem_two) if not emane_two or not netif: - context.abort( - grpc.StatusCode.NOT_FOUND, "nem two {} not found".format(nem_two) - ) + context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem_two} not found") node_two = netif.node if emane_one.id == emane_two.id: diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 0b3bf04b..03147738 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -492,9 +492,7 @@ class CoreNode(CoreNodeBase): raise ValueError("starting a node that is already up") # create a new namespace for this node using vnoded - vnoded = "{cmd} -v -c {name} -l {name}.log -p {name}.pid".format( - cmd=VNODED_BIN, name=self.ctrlchnlname - ) + vnoded = f"{VNODED_BIN} -v -c {self.ctrlchnlname} -l {self.ctrlchnlname}.log -p {self.ctrlchnlname}.pid" if self.nodedir: vnoded += f" -C {self.nodedir}" env = self.session.get_environment(state=False) @@ -593,9 +591,7 @@ class CoreNode(CoreNodeBase): if self.server is None: return terminal else: - return "ssh -X -f {host} xterm -e {terminal}".format( - host=self.server.host, terminal=terminal - ) + return f"ssh -X -f {self.server.host} xterm -e {terminal}" def privatedir(self, path): """ diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index df8422af..369f462b 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -19,23 +19,18 @@ class DockerClient(object): def create_container(self): self.run( - "docker run -td --init --net=none --hostname {name} --name {name} " - "--sysctl net.ipv6.conf.all.disable_ipv6=0 " - "{image} /bin/bash".format( - name=self.name, - image=self.image - )) + f"docker run -td --init --net=none --hostname {self.name} --name {self.name} " + f"--sysctl net.ipv6.conf.all.disable_ipv6=0 {self.image} /bin/bash" + ) self.pid = self.get_pid() return self.pid def get_info(self): - args = "docker inspect {name}".format(name=self.name) + args = f"docker inspect {self.name}" output = self.run(args) data = json.loads(output) if not data: - raise CoreCommandError( - -1, args, "docker({name}) not present".format(name=self.name) - ) + raise CoreCommandError(-1, args, f"docker({self.name}) not present") return data[0] def is_alive(self): @@ -46,43 +41,28 @@ class DockerClient(object): return False def stop_container(self): - self.run("docker rm -f {name}".format( - name=self.name - )) + self.run(f"docker rm -f {self.name}") def check_cmd(self, cmd): logging.info("docker cmd output: %s", cmd) - return utils.check_cmd("docker exec {name} {cmd}".format( - name=self.name, - cmd=cmd - )) + return utils.check_cmd(f"docker exec {self.name} {cmd}") def create_ns_cmd(self, cmd): - return "nsenter -t {pid} -u -i -p -n {cmd}".format( - pid=self.pid, - cmd=cmd - ) + return f"nsenter -t {self.pid} -u -i -p -n {cmd}" def ns_cmd(self, cmd, wait): - args = "nsenter -t {pid} -u -i -p -n {cmd}".format( - pid=self.pid, - cmd=cmd - ) + args = f"nsenter -t {self.pid} -u -i -p -n {cmd}" return utils.check_cmd(args, wait=wait) def get_pid(self): - args = "docker inspect -f '{{{{.State.Pid}}}}' {name}".format(name=self.name) + args = f"docker inspect -f '{{{{.State.Pid}}}}' {self.name}" output = self.run(args) self.pid = output logging.debug("node(%s) pid: %s", self.name, self.pid) return output def copy_file(self, source, destination): - args = "docker cp {source} {name}:{destination}".format( - source=source, - name=self.name, - destination=destination - ) + args = f"docker cp {source} {self.name}:{destination}" return self.run(args) @@ -185,7 +165,7 @@ class DockerNode(CoreNode): :param str sh: shell to execute command in :return: str """ - return "docker exec -it {name} bash".format(name=self.name) + return f"docker exec -it {self.name} bash" def privatedir(self, path): """ @@ -195,7 +175,7 @@ class DockerNode(CoreNode): :return: nothing """ logging.debug("creating node dir: %s", path) - args = "mkdir -p {path}".format(path=path) + args = f"mkdir -p {path}" self.node_net_cmd(args) def mount(self, source, target): diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index 50588fb2..323b20a9 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -18,19 +18,17 @@ class LxdClient(object): self.pid = None def create_container(self): - self.run("lxc launch {image} {name}".format(name=self.name, image=self.image)) + self.run(f"lxc launch {self.image} {self.name}") data = self.get_info() self.pid = data["state"]["pid"] return self.pid def get_info(self): - args = "lxc list {name} --format json".format(name=self.name) + args = f"lxc list {self.name} --format json" output = self.run(args) data = json.loads(output) if not data: - raise CoreCommandError( - -1, args, "LXC({name}) not present".format(name=self.name) - ) + raise CoreCommandError(-1, args, f"LXC({self.name}) not present") return data[0] def is_alive(self): @@ -41,13 +39,13 @@ class LxdClient(object): return False def stop_container(self): - self.run("lxc delete --force {name}".format(name=self.name)) + self.run(f"lxc delete --force {self.name}") def create_cmd(self, cmd): - return "lxc exec -nT {name} -- {cmd}".format(name=self.name, cmd=cmd) + return f"lxc exec -nT {self.name} -- {cmd}" def create_ns_cmd(self, cmd): - return "nsenter -t {pid} -m -u -i -p -n {cmd}".format(pid=self.pid, cmd=cmd) + return f"nsenter -t {self.pid} -m -u -i -p -n {cmd}" def check_cmd(self, cmd, wait=True): args = self.create_cmd(cmd) @@ -57,9 +55,7 @@ class LxdClient(object): if destination[0] != "/": destination = os.path.join("/root/", destination) - args = "lxc file push {source} {name}/{destination}".format( - source=source, name=self.name, destination=destination - ) + args = f"lxc file push {source} {self.name}/{destination}" self.run(args) @@ -142,7 +138,7 @@ class LxcNode(CoreNode): :param str sh: shell to execute command in :return: str """ - return "lxc exec {name} -- {sh}".format(name=self.name, sh=sh) + return f"lxc exec {self.name} -- {sh}" def privatedir(self, path): """ @@ -152,7 +148,7 @@ class LxcNode(CoreNode): :return: nothing """ logging.info("creating node dir: %s", path) - args = "mkdir -p {path}".format(path=path) + args = f"mkdir -p {path}" return self.node_net_cmd(args) def mount(self, source, target): From bab5c75cb9f8184e6e82c245eef0fc5dcdbb5dfa Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 18 Oct 2019 13:20:05 -0700 Subject: [PATCH 076/462] removed unwanted logging of container env --- daemon/core/nodes/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 03147738..d8eaae76 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -492,13 +492,15 @@ class CoreNode(CoreNodeBase): raise ValueError("starting a node that is already up") # create a new namespace for this node using vnoded - vnoded = f"{VNODED_BIN} -v -c {self.ctrlchnlname} -l {self.ctrlchnlname}.log -p {self.ctrlchnlname}.pid" + vnoded = ( + f"{VNODED_BIN} -v -c {self.ctrlchnlname} -l {self.ctrlchnlname}.log " + f"-p {self.ctrlchnlname}.pid" + ) if self.nodedir: vnoded += f" -C {self.nodedir}" env = self.session.get_environment(state=False) env["NODE_NUMBER"] = str(self.id) env["NODE_NAME"] = str(self.name) - logging.info("env: %s", env) output = self.net_cmd(vnoded, env=env) self.pid = int(output) From c5ce85b2356b69654a2175bb2d981b3970ac9d52 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 18 Oct 2019 14:22:37 -0700 Subject: [PATCH 077/462] added net client get ifindex and mac functions --- daemon/core/nodes/base.py | 13 +++---------- daemon/core/nodes/netclient.py | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index d8eaae76..fcd75505 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -678,16 +678,9 @@ class CoreNode(CoreNodeBase): veth.name = ifname if self.up: - # TODO: potentially find better way to query interface ID - # retrieve interface information - output = self.node_net_client.device_show(veth.name) - logging.debug("interface command output: %s", output) - output = output.split("\n") - veth.flow_id = int(output[0].strip().split(":")[0]) + 1 - logging.debug("interface flow index: %s - %s", veth.name, veth.flow_id) - # TODO: mimic packed hwaddr - # veth.hwaddr = MacAddress.from_string(output[1].strip().split()[1]) - logging.debug("interface mac: %s - %s", veth.name, veth.hwaddr) + flow_id = self.node_net_client.get_ifindex(veth.name) + veth.flow_id = int(flow_id) + logging.info("interface flow index: %s - %s", veth.name, veth.flow_id) try: # add network interface to the node. If unsuccessful, destroy the diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 94e73e7f..beff4e8e 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -91,6 +91,26 @@ class LinuxNetClient(object): """ return self.run(f"{IP_BIN} link show {device}") + def get_mac(self, device): + """ + Retrieve MAC address for a given device. + + :param str device: device to get mac for + :return: MAC address + :rtype: str + """ + return self.run(f"cat /sys/class/net/{device}/address") + + def get_ifindex(self, device): + """ + Retrieve ifindex for a given device. + + :param str device: device to get ifindex for + :return: ifindex + :rtype: str + """ + return self.run(f"cat /sys/class/net/{device}/ifindex") + def device_ns(self, device, namespace): """ Set netns for a device. From e298a2a5c11282f01a8a354f94c04f03e07fb704 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 18 Oct 2019 14:28:50 -0700 Subject: [PATCH 078/462] grpc will now always be ran, but can be configured through command line or core.conf --- daemon/Pipfile | 2 +- daemon/scripts/core-daemon | 17 +++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/daemon/Pipfile b/daemon/Pipfile index a1e33a29..a689f4dc 100644 --- a/daemon/Pipfile +++ b/daemon/Pipfile @@ -4,7 +4,7 @@ url = "https://pypi.org/simple" verify_ssl = true [scripts] -core = "python scripts/core-daemon -f data/core.conf -l data/logging.conf --grpc" +core = "python scripts/core-daemon -f data/core.conf -l data/logging.conf" test = "pytest -v tests" test_emane = "pytest -v tests/emane" diff --git a/daemon/scripts/core-daemon b/daemon/scripts/core-daemon index 49aae2d2..6b55c14f 100755 --- a/daemon/scripts/core-daemon +++ b/daemon/scripts/core-daemon @@ -18,7 +18,6 @@ from core.api.grpc.server import CoreGrpcServer from core.api.tlv.corehandlers import CoreHandler, CoreUdpHandler from core.api.tlv.coreserver import CoreServer, CoreUdpServer from core.constants import CORE_CONF_DIR, COREDPY_VERSION -from core.emulator import enumerations from core.emulator.enumerations import CORE_API_PORT from core.utils import close_onexec, load_logging_config @@ -67,14 +66,13 @@ def cored(cfg): sys.exit(1) # initialize grpc api - if cfg["grpc"] == "True": - grpc_server = CoreGrpcServer(server.coreemu) - address_config = cfg["grpcaddress"] - port_config = cfg["grpcport"] - grpc_address = f"{address_config}:{port_config}" - grpc_thread = threading.Thread(target=grpc_server.listen, args=(grpc_address,)) - grpc_thread.daemon = True - grpc_thread.start() + grpc_server = CoreGrpcServer(server.coreemu) + address_config = cfg["grpcaddress"] + port_config = cfg["grpcport"] + grpc_address = f"{address_config}:{port_config}" + grpc_thread = threading.Thread(target=grpc_server.listen, args=(grpc_address,)) + grpc_thread.daemon = True + grpc_thread.start() # start udp server start_udp(server, address) @@ -117,7 +115,6 @@ def get_merged_config(filename): parser.add_argument("-n", "--numthreads", dest="numthreads", type=int, help=f"number of server threads; default = {default_threads}") parser.add_argument("--ovs", action="store_true", help="enable experimental ovs mode, default is false") - parser.add_argument("--grpc", action="store_true", help="enable grpc api, default is false") parser.add_argument("--grpc-port", dest="grpcport", help=f"grpc port to listen on; default {default_grpc_port}") parser.add_argument("--grpc-address", dest="grpcaddress", From 2012105df08830e81e34a5824bbaaea823477a0e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 18 Oct 2019 14:43:36 -0700 Subject: [PATCH 079/462] updated core.conf to contain distributed address and grpc configurations --- daemon/data/core.conf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/daemon/data/core.conf b/daemon/data/core.conf index 3a111ba0..aa1238d5 100644 --- a/daemon/data/core.conf +++ b/daemon/data/core.conf @@ -1,6 +1,9 @@ [core-daemon] +#distributed_address = 127.0.0.1 listenaddr = localhost port = 4038 +grpcaddress = localhost +grpcport = 50051 numthreads = 1 quagga_bin_search = "/usr/local/bin /usr/bin /usr/lib/quagga" quagga_sbin_search = "/usr/local/sbin /usr/sbin /usr/lib/quagga" From d1e9223d522f9a4f2d4138a02b11efc28956068d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 18 Oct 2019 14:52:08 -0700 Subject: [PATCH 080/462] updates to install docs to remove python2 references --- docs/install.md | 59 ++++++++++--------------------------------------- 1 file changed, 12 insertions(+), 47 deletions(-) diff --git a/docs/install.md b/docs/install.md index 822642fa..12242b4b 100644 --- a/docs/install.md +++ b/docs/install.md @@ -34,7 +34,7 @@ Install Path | Description /usr/bin/core-daemon|Daemon startup command /usr/bin/{core-cleanup, coresendmsg, core-manage}|Misc. helper commands/scripts /usr/lib/core|GUI files -/usr/lib/python{2.7,3}/dist-packages/core|Python modules for daemon/scripts +/usr/lib/python{3.6+}/dist-packages/core|Python modules for daemon/scripts /etc/core/|Daemon and log configuration files ~/.core/|User-specific GUI preferences and scenario files /usr/share/core/|Example scripts and scenarios @@ -48,11 +48,6 @@ 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. ```shell -# python 2 -sudo apt install python -sudo apt install python-pip - -# python 3 sudo apt install python3 sudo apt install python3-pip ``` @@ -64,23 +59,9 @@ To account for this it would be recommended to install the python dependencies u the latest [CORE Release](https://github.com/coreemu/core/releases). ```shell -# for python 2 -sudo python -m pip install -r requirements.txt -# for python 3 sudo python3 -m pip install -r requirements.txt ``` -## Ubuntu 19.04 - -Ubuntu 19.04 can provide all the packages needed at the system level and can be installed as follows: - -```shell -# python 2 -sudo apt install python-configparser python-enum34 python-future python-grpcio python-lxml -# python 3 -sudo apt install python3-configparser python3-enum34 python3-future python3-grpcio python3-lxml -``` - # Pre-Req Installing OSPF MDR Virtual networks generally require some form of routing in order to work (e.g. to automatically populate routing @@ -134,7 +115,7 @@ this is usually a sign that you have to run ```sudo ldconfig```` to refresh the # Installing from Packages The easiest way to install CORE is using the pre-built packages. The package managers on Ubuntu or Fedora/CentOS -will help in automatically installing most dependencies for you. +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). @@ -143,10 +124,9 @@ You can obtain the CORE packages from [CORE Releases](https://github.com/coreemu Ubuntu package defaults to using systemd for running as a service. ```shell -# python2 -sudo apt install ./core_python_$VERSION_amd64.deb -# python3 -sudo apt install ./core_python3_$VERSION_amd64.deb +# $PYTHON and $VERSION represent the python and CORE +# versions the package was built for +sudo apt install ./core_$PYTHON_$VERSION_amd64.deb ``` Run the CORE GUI as a normal user: @@ -164,9 +144,6 @@ Messages will print out on the console about connecting to the CORE daemon. on CentOS <= 6, or build from source otherwise** ```shell -# python2 -yum install ./core_python_$VERSION_x86_64.rpm -# python3 yum install ./core_python3_$VERSION_x86_64.rpm ``` @@ -234,10 +211,7 @@ You can obtain the CORE source from the [CORE GitHub](https://github.com/coreemu Python module grpcio-tools is currently needed to generate code from the CORE protobuf file during the build. ```shell -# python2 -pip2 install grpcio-tools -# python3 -pip3 install grpcio-tools +python3 -m pip install grpcio-tools ``` ## Distro Requirements @@ -245,27 +219,26 @@ pip3 install grpcio-tools ### Ubuntu 18.04 Requirements ```shell -sudo apt install automake pkg-config gcc libev-dev bridge-utils ebtables python-dev python-setuptools tk libtk-img ethtool +sudo apt install automake pkg-config gcc libev-dev bridge-utils ebtables python3-dev python3-setuptools tk libtk-img ethtool ``` ### Ubuntu 16.04 Requirements ```shell -sudo apt-get install automake bridge-utils ebtables python-dev libev-dev python-setuptools libtk-img ethtool +sudo apt-get install automake bridge-utils ebtables python3-dev libev-dev python3-setuptools libtk-img ethtool ``` ### CentOS 7 with Gnome Desktop Requirements ```shell -sudo yum -y install automake gcc python-devel libev-devel tk ethtool +sudo yum -y install automake gcc python3-devel python3-devel libev-devel tk ethtool ``` ## Build and Install ```shell ./bootstrap.sh -# $VERSION should be path to python2/3 -PYTHON=$VERSION ./configure +PYTHON=python3 ./configure make sudo make install ``` @@ -275,16 +248,11 @@ sudo make install Building documentation requires python-sphinx not noted above. ```shell -# install python2 sphinx -sudo apt install python-sphinx -sudo yum install python-sphinx -# install python3 sphinx sudo apt install python3-sphinx sudo yum install python3-sphinx ./bootstrap.sh -# $VERSION should be path to python2/3 -PYTHON=$VERSION ./configure +PYTHON=python3 ./configure make doc ``` @@ -297,10 +265,7 @@ Build package commands, DESTDIR is used to make install into and then for packag ```shell ./bootstrap.sh -# for python2 -PYTHON=python2 ./configure -# for python3 -PYTHON=python3 ./configure --enable-python3 +PYTHON=python3 ./configure make mkdir /tmp/core-build make fpm DESTDIR=/tmp/core-build From 83c408359a64b63dc6e820bcf32f9ef83716201a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 18 Oct 2019 14:56:21 -0700 Subject: [PATCH 081/462] set flow id logging to debug --- daemon/core/nodes/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index fcd75505..fd6e0d81 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -680,7 +680,7 @@ class CoreNode(CoreNodeBase): if self.up: flow_id = self.node_net_client.get_ifindex(veth.name) veth.flow_id = int(flow_id) - logging.info("interface flow index: %s - %s", veth.name, veth.flow_id) + logging.debug("interface flow index: %s - %s", veth.name, veth.flow_id) try: # add network interface to the node. If unsuccessful, destroy the From 73b2eff312f4ae3ef6c6b3816d7e1e1ce8f11a28 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 18 Oct 2019 16:25:38 -0700 Subject: [PATCH 082/462] fix for corehandlers.py session_clients access --- daemon/core/api/tlv/corehandlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 85b7b831..c7827e1f 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -1016,7 +1016,7 @@ class CoreHandler(socketserver.BaseRequestHandler): # find the session containing this client and set the session to master for _id in self.coreemu.sessions: - clients = self.session_clients[_id] + clients = self.session_clients.get(_id, []) if self in clients: session = self.coreemu.sessions[_id] logging.debug("setting session to master: %s", session.id) From 18e5598203c61e5cfa2ea03d2faade7127c07bf2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 18 Oct 2019 16:28:13 -0700 Subject: [PATCH 083/462] fixed node data reporting emulation server host instead of name --- daemon/core/nodes/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index fd6e0d81..e963683b 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -196,7 +196,7 @@ class NodeBase(object): model = self.type emulation_server = None if self.server is not None: - emulation_server = self.server.host + emulation_server = self.server.name services = self.services if services is not None: From 38683cb0d0da27e990621c8f658f0169cd7a0db5 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 18 Oct 2019 16:42:00 -0700 Subject: [PATCH 084/462] more work on coretk --- coretk/coretk/app.py | 10 +- coretk/coretk/coregrpc.py | 169 ++++++++++++----------- coretk/coretk/coretocanvas.py | 44 ++++++ coretk/coretk/coretoolbar.py | 55 ++++++-- coretk/coretk/graph.py | 189 ++++++++++++++----------- coretk/coretk/graph_helper.py | 198 +++++++++++++++++++++++++++ coretk/coretk/grpcmanagement.py | 46 ++++++- coretk/coretk/icons/antenna.gif | Bin 0 -> 230 bytes coretk/coretk/icons/document-new.gif | Bin 0 -> 1054 bytes coretk/coretk/icons/edit-delete.gif | Bin 0 -> 1006 bytes coretk/coretk/icons/fileopen.gif | Bin 0 -> 1095 bytes coretk/coretk/images.py | 4 + coretk/coretk/interface.py | 5 + coretk/coretk/linkinfo.py | 134 ++++++++++++++++++ coretk/coretk/menuaction.py | 53 ++++--- coretk/coretk/prev_saved_xml.txt | 7 +- coretk/coretk/querysessiondrawing.py | 174 +++++++++++++++++++++++ coretk/coretk/wirelessconnection.py | 53 +++++++ 18 files changed, 939 insertions(+), 202 deletions(-) create mode 100644 coretk/coretk/coretocanvas.py create mode 100644 coretk/coretk/graph_helper.py create mode 100644 coretk/coretk/icons/antenna.gif create mode 100644 coretk/coretk/icons/document-new.gif create mode 100644 coretk/coretk/icons/edit-delete.gif create mode 100644 coretk/coretk/icons/fileopen.gif create mode 100644 coretk/coretk/linkinfo.py create mode 100644 coretk/coretk/querysessiondrawing.py create mode 100644 coretk/coretk/wirelessconnection.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 6328b3bc..e4f632a7 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -25,6 +25,7 @@ class Application(tk.Frame): self.create_widgets() self.draw_canvas() self.start_grpc() + # self.try_make_table() def load_images(self): """ @@ -89,7 +90,7 @@ class Application(tk.Frame): :return: nothing """ self.master.update() - self.core_grpc = CoreGrpc(self.master) + self.core_grpc = CoreGrpc(self) self.core_grpc.set_up() self.canvas.core_grpc = self.core_grpc self.canvas.draw_existing_component() @@ -99,6 +100,13 @@ class Application(tk.Frame): menu_action.on_quit() # self.quit() + def try_make_table(self): + f = tk.Frame(self.master) + for i in range(3): + e = tk.Entry(f) + e.grid(row=0, column=1, stick="nsew") + f.pack(side=tk.TOP) + if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index abcad792..095169ed 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -3,26 +3,37 @@ Incorporate grpc into python tkinter GUI """ import logging import os -import tkinter as tk +from collections import OrderedDict from core.api.grpc import client, core_pb2 +from coretk.linkinfo import Throughput +from coretk.querysessiondrawing import SessionTable +from coretk.wirelessconnection import WirelessConnection class CoreGrpc: - def __init__(self, master): + def __init__(self, app, sid=None): """ Create a CoreGrpc instance """ - print("Create grpc instance") self.core = client.CoreGrpcClient() - self.session_id = None - self.master = master + self.session_id = sid + self.master = app.master # self.set_up() self.interface_helper = None + self.throughput_draw = Throughput(app.canvas, self) + self.wireless_draw = WirelessConnection(app.canvas, self) def log_event(self, event): logging.info("event: %s", event) + if event.link_event is not None: + self.wireless_draw.hangle_link_event(event.link_event) + + def log_throughput(self, event): + interface_throughputs = event.interface_throughputs + # bridge_throughputs = event.bridge_throughputs + self.throughput_draw.process_grpc_throughput_event(interface_throughputs) def create_new_session(self): """ @@ -36,29 +47,7 @@ class CoreGrpc: # handle events session may broadcast self.session_id = response.session_id self.core.events(self.session_id, self.log_event) - - def _enter_session(self, session_id, dialog): - """ - enter an existing session - - :return: - """ - dialog.destroy() - response = self.core.get_session(session_id) - self.session_id = session_id - print("set session id: %s", session_id) - logging.info("Entering session_id %s.... Result: %s", session_id, response) - # self.master.canvas.draw_existing_component() - - def _create_session(self, dialog): - """ - create a new session - - :param tkinter.Toplevel dialog: save core session prompt dialog - :return: nothing - """ - dialog.destroy() - self.create_new_session() + self.core.throughputs(self.log_throughput) def query_existing_sessions(self, sessions): """ @@ -68,35 +57,29 @@ class CoreGrpc: :return: nothing """ - dialog = tk.Toplevel() - dialog.title("CORE sessions") - for session in sessions: - b = tk.Button( - dialog, - text="Session " + str(session.id), - command=lambda sid=session.id: self._enter_session(sid, dialog), - ) - b.pack(side=tk.TOP) - b = tk.Button( - dialog, text="create new", command=lambda: self._create_session(dialog) - ) - b.pack(side=tk.TOP) - dialog.update() - x = ( - self.master.winfo_x() - + (self.master.winfo_width() - dialog.winfo_width()) / 2 - ) - y = ( - self.master.winfo_y() - + (self.master.winfo_height() / 2 - dialog.winfo_height()) / 2 - ) - dialog.geometry(f"+{int(x)}+{int(y)}") - dialog.wait_window() + SessionTable(self, self.master) - def delete_session(self): - response = self.core.delete_session(self.session_id) + def delete_session(self, custom_sid=None): + if custom_sid is None: + sid = self.session_id + else: + sid = custom_sid + response = self.core.delete_session(sid) logging.info("Deleted session result: %s", response) + def terminate_session(self, custom_sid=None): + if custom_sid is None: + sid = self.session_id + else: + sid = custom_sid + s = self.core.get_session(sid).session + # delete links and nodes from running session + if s.state == core_pb2.SessionState.RUNTIME: + self.set_session_state("datacollect", sid) + self.delete_links(sid) + self.delete_nodes(sid) + self.delete_session(sid) + def set_up(self): """ Query sessions, if there exist any, prompt whether to join one @@ -106,7 +89,7 @@ class CoreGrpc: self.core.connect() response = self.core.get_sessions() - logging.info("all sessions: %s", response) + # logging.info("coregrpc.py: all sessions: %s", response) # if there are no sessions, create a new session, else join a session sessions = response.sessions @@ -119,70 +102,87 @@ class CoreGrpc: def get_session_state(self): response = self.core.get_session(self.session_id) - logging.info("get session: %s", response) + # logging.info("get session: %s", response) return response.session.state - def set_session_state(self, state): + def set_session_state(self, state, custom_session_id=None): """ Set session state :param str state: session state to set :return: nothing """ - response = None + if custom_session_id is None: + sid = self.session_id + else: + sid = custom_session_id + if state == "configuration": response = self.core.set_session_state( - self.session_id, core_pb2.SessionState.CONFIGURATION + sid, core_pb2.SessionState.CONFIGURATION ) elif state == "instantiation": response = self.core.set_session_state( - self.session_id, core_pb2.SessionState.INSTANTIATION + sid, core_pb2.SessionState.INSTANTIATION ) elif state == "datacollect": response = self.core.set_session_state( - self.session_id, core_pb2.SessionState.DATACOLLECT + sid, core_pb2.SessionState.DATACOLLECT ) elif state == "shutdown": - response = self.core.set_session_state( - self.session_id, core_pb2.SessionState.SHUTDOWN - ) + response = self.core.set_session_state(sid, core_pb2.SessionState.SHUTDOWN) elif state == "runtime": - response = self.core.set_session_state( - self.session_id, core_pb2.SessionState.RUNTIME - ) + response = self.core.set_session_state(sid, core_pb2.SessionState.RUNTIME) elif state == "definition": response = self.core.set_session_state( - self.session_id, core_pb2.SessionState.DEFINITION + sid, core_pb2.SessionState.DEFINITION ) elif state == "none": - response = self.core.set_session_state( - self.session_id, core_pb2.SessionState.NONE - ) + response = self.core.set_session_state(sid, core_pb2.SessionState.NONE) else: logging.error("coregrpc.py: set_session_state: INVALID STATE") logging.info("set session state: %s", response) def add_node(self, node_type, model, x, y, name, node_id): - logging.info("coregrpc.py ADD NODE %s", name) position = core_pb2.Position(x=x, y=y) node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) response = self.core.add_node(self.session_id, node) logging.info("created node: %s", response) + if node_type == core_pb2.NodeType.WIRELESS_LAN: + d = OrderedDict() + d["basic_range"] = "275" + d["bandwidth"] = "54000000" + d["jitter"] = "0" + d["delay"] = "20000" + d["error"] = "0" + r = self.core.set_wlan_config(self.session_id, node_id, d) + logging.debug("set wlan config %s", r) return response.node_id def edit_node(self, node_id, x, y): position = core_pb2.Position(x=x, y=y) response = self.core.edit_node(self.session_id, node_id, position) logging.info("updated node id %s: %s", node_id, response) + # self.core.events(self.session_id, self.log_event) - def delete_nodes(self): - for node in self.core.get_session(self.session_id).session.nodes: + def delete_nodes(self, delete_session=None): + if delete_session is None: + sid = self.session_id + else: + sid = delete_session + for node in self.core.get_session(sid).session.nodes: response = self.core.delete_node(self.session_id, node.id) - logging.info("delete node %s", response) + logging.info("delete nodes %s", response) - def delete_links(self): - for link in self.core.get_session(self.session_id).session.links: + def delete_links(self, delete_session=None): + sid = None + if delete_session is None: + sid = self.session_id + else: + sid = delete_session + + for link in self.core.get_session(sid).session.links: response = self.core.delete_link( self.session_id, link.node_one_id, @@ -190,7 +190,7 @@ class CoreGrpc: link.interface_one.id, link.interface_two.id, ) - logging.info("delete link %s", response) + logging.info("delete links %s", response) def add_link(self, id1, id2, type1, type2, edge): """ @@ -235,6 +235,13 @@ class CoreGrpc: response = self.core.add_link(self.session_id, id1, id2, if1, if2) logging.info("created link: %s", response) + # def get_session(self): + # response = self.core.get_session(self.session_id) + # nodes = response.session.nodes + # for node in nodes: + # r = self.core.get_node_links(self.session_id, node.id) + # logging.info(r) + def launch_terminal(self, node_id): response = self.core.get_node_terminal(self.session_id, node_id) logging.info("get terminal %s", response.terminal) @@ -249,6 +256,7 @@ class CoreGrpc: """ response = self.core.save_xml(self.session_id, file_path) logging.info("coregrpc.py save xml %s", response) + self.core.events(self.session_id, self.log_event) def open_xml(self, file_path): """ @@ -258,7 +266,10 @@ class CoreGrpc: :return: session id """ response = self.core.open_xml(file_path) - return response.session_id + self.session_id = response.session_id + # print("Sessionz") + # self.core.events(self.session_id, self.log_event) + # return response.session_id # logging.info("coregrpc.py open_xml()", type(response)) def close(self): diff --git a/coretk/coretk/coretocanvas.py b/coretk/coretk/coretocanvas.py new file mode 100644 index 00000000..f16c5306 --- /dev/null +++ b/coretk/coretk/coretocanvas.py @@ -0,0 +1,44 @@ +""" +provide mapping from core to canvas +""" +import logging + + +class CoreToCanvasMapping: + def __init__(self): + self.core_id_to_canvas_id = {} + self.core_node_and_interface_to_canvas_edge = {} + + def map_node_and_interface_to_canvas_edge(self, nid, iid, edge_token): + self.core_node_and_interface_to_canvas_edge[tuple([nid, iid])] = edge_token + + def get_token_from_node_and_interface(self, nid, iid): + key = tuple([nid, iid]) + if key in self.core_node_and_interface_to_canvas_edge: + return self.core_node_and_interface_to_canvas_edge[key] + else: + logging.error("invalid key") + return None + + def map_core_id_to_canvas_id(self, core_nid, canvas_nid): + if core_nid not in self.core_id_to_canvas_id: + self.core_id_to_canvas_id[core_nid] = canvas_nid + else: + logging.debug("key already existed") + + def get_canvas_id_from_core_id(self, core_id): + if core_id in self.core_id_to_canvas_id: + return self.core_id_to_canvas_id[core_id] + else: + logging.debug("invalid key") + return None + + # def add_mapping(self, core_id, canvas_id): + # if core_id not in self.core_id_to_canvas_id: + # self.core_id_to_canvas_id[core_id] = canvas_id + # else: + # logging.error("key already mapped") + # + # def delete_mapping(self, core_id): + # result = self.core_id_to_canvas_id.pop(core_id, None) + # return result diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 6911ebaf..4e263d28 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -1,11 +1,24 @@ import logging import tkinter as tk +from enum import Enum from core.api.grpc import core_pb2 from coretk.graph import GraphMode from coretk.images import ImageEnum, Images from coretk.tooltip import CreateToolTip +# from coretk.graph_helper import WlanConnection + + +class SessionStateEnum(Enum): + NONE = "none" + DEFINITION = "definition" + CONFIGURATION = "configuration" + RUNTIME = "runtime" + DATACOLLECT = "datacollect" + SHUTDOWN = "shutdown" + INSTANTIATION = "instantiation" + class CoreToolbar(object): """ @@ -62,7 +75,7 @@ class CoreToolbar(object): if self.marker_option_menu and self.marker_option_menu.winfo_exists(): self.marker_option_menu.destroy() - def destroy_children_widgets(self, parent): + def destroy_children_widgets(self): """ Destroy all children of a parent widget @@ -70,7 +83,7 @@ class CoreToolbar(object): :return: nothing """ - for i in parent.winfo_children(): + for i in self.edit_frame.winfo_children(): if i.winfo_name() != "!frame": i.destroy() @@ -150,14 +163,23 @@ class CoreToolbar(object): self.canvas.mode = GraphMode.SELECT def click_start_session_tool(self): + """ + Start session handler: redraw buttons, send node and link messages to grpc server + + :return: nothing + """ logging.debug("Click START STOP SESSION button") - self.destroy_children_widgets(self.edit_frame) + # self.destroy_children_widgets(self.edit_frame) + self.destroy_children_widgets() self.canvas.mode = GraphMode.SELECT # set configuration state - if self.canvas.core_grpc.get_session_state() == core_pb2.SessionState.SHUTDOWN: - self.canvas.core_grpc.set_session_state("definition") - self.canvas.core_grpc.set_session_state("configuration") + state = self.canvas.core_grpc.get_session_state() + + if state == core_pb2.SessionState.SHUTDOWN: + self.canvas.core_grpc.set_session_state(SessionStateEnum.DEFINITION.value) + + self.canvas.core_grpc.set_session_state(SessionStateEnum.CONFIGURATION.value) for node in self.canvas.grpc_manager.nodes.values(): self.canvas.core_grpc.add_node( @@ -169,8 +191,9 @@ class CoreToolbar(object): edge.id1, edge.id2, edge.type1, edge.type2, edge ) - self.canvas.core_grpc.set_session_state("instantiation") + self.canvas.core_grpc.set_session_state(SessionStateEnum.INSTANTIATION.value) + # self.canvas.core_grpc.get_session() self.create_runtime_toolbar() def click_link_tool(self): @@ -206,7 +229,7 @@ class CoreToolbar(object): self.network_layer_option_menu.destroy() main_button.configure(image=Images.get(ImageEnum.MDR.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.MD.value) + self.canvas.draw_node_image = Images.get(ImageEnum.MDR.value) self.canvas.draw_node_name = "mdr" def pick_prouter(self, main_button): @@ -493,6 +516,11 @@ class CoreToolbar(object): CreateToolTip(marker_main_button, "background annotation tools") def create_toolbar(self): + """ + Create buttons for toolbar in edit mode + + :return: nothing + """ self.create_regular_button( self.edit_frame, Images.get(ImageEnum.START.value), @@ -551,9 +579,16 @@ class CoreToolbar(object): menu_button.menu.add_command(label="Edit...") def click_stop_button(self): + """ + redraw buttons on the toolbar, send node and link messages to grpc server + + :return: nothing + """ logging.debug("Click on STOP button ") - self.destroy_children_widgets(self.edit_frame) - self.canvas.core_grpc.set_session_state("datacollect") + # self.destroy_children_widgets(self.edit_frame) + self.destroy_children_widgets() + + self.canvas.core_grpc.set_session_state(SessionStateEnum.DATACOLLECT.value) self.canvas.core_grpc.delete_links() self.canvas.core_grpc.delete_nodes() self.create_toolbar() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 7e69b224..7793880f 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -1,12 +1,13 @@ import enum import logging -import math import tkinter as tk from core.api.grpc import core_pb2 +from coretk.graph_helper import GraphHelper, WlanAntennaManager from coretk.grpcmanagement import GrpcManager from coretk.images import Images from coretk.interface import Interface +from coretk.linkinfo import LinkInfo class GraphMode(enum.Enum): @@ -31,11 +32,17 @@ class CanvasGraph(tk.Canvas): self.nodes = {} self.edges = {} self.drawing_edge = None + self.setup_menus() self.setup_bindings() self.draw_grid() - self.core_grpc = grpc + self.grpc_manager = GrpcManager() + self.core_grpc = grpc + + self.helper = GraphHelper(self) + # self.core_id_to_canvas_id = {} + # self.core_map = CoreToCanvasMapping() # self.draw_existing_component() def setup_menus(self): @@ -44,6 +51,31 @@ class CanvasGraph(tk.Canvas): self.node_context.add_command(label="Two") self.node_context.add_command(label="Three") + def canvas_reset_and_redraw(self, new_grpc): + """ + Reset the private variables CanvasGraph object, redraw nodes given the new grpc client + :param new_grpc: + :return: + """ + # delete any existing drawn items + self.delete_components() + + # set the private variables to default value + self.mode = GraphMode.SELECT + self.draw_node_image = None + self.draw_node_name = None + self.selected = None + self.node_context = None + self.nodes = {} + self.edges = {} + self.drawing_edge = None + self.grpc_manager = GrpcManager() + + # new grpc + self.core_grpc = new_grpc + + self.draw_existing_component() + def setup_bindings(self): """ Bind any mouse events or hot keys to the matching action @@ -97,7 +129,9 @@ class CanvasGraph(tk.Canvas): image, name = Images.convert_type_and_model_to_image( node.type, node.model ) - n = CanvasNode(node.position.x, node.position.y, image, self, node.id) + n = CanvasNode( + node.position.x, node.position.y, image, name, self, node.id + ) self.nodes[n.id] = n core_id_to_canvas_id[node.id] = n.id self.grpc_manager.add_preexisting_node(n, session_id, node, name) @@ -107,12 +141,33 @@ class CanvasGraph(tk.Canvas): for link in session.links: n1 = self.nodes[core_id_to_canvas_id[link.node_one_id]] n2 = self.nodes[core_id_to_canvas_id[link.node_two_id]] - e = CanvasEdge(n1.x_coord, n1.y_coord, n2.x_coord, n2.y_coord, n1.id, self) + if link.type == core_pb2.LinkType.WIRED: + e = CanvasEdge( + n1.x_coord, + n1.y_coord, + n2.x_coord, + n2.y_coord, + n1.id, + self, + is_wired=True, + ) + elif link.type == core_pb2.LinkType.WIRELESS: + e = CanvasEdge( + n1.x_coord, + n1.y_coord, + n2.x_coord, + n2.y_coord, + n1.id, + self, + is_wired=False, + ) n1.edges.add(e) n2.edges.add(e) self.edges[e.token] = e self.grpc_manager.add_edge(session_id, e.token, n1.id, n2.id) + self.helper.redraw_antenna(link, n1, n2) + # TODO add back the link info to grpc manager also redraw grpc_if1 = link.interface_one grpc_if2 = link.interface_two @@ -128,12 +183,11 @@ class CanvasGraph(tk.Canvas): ip6_dst = grpc_if2.ip6 e.link_info = LinkInfo( canvas=self, - edge_id=e.id, + edge=e, ip4_src=ip4_src, ip6_src=ip6_src, ip4_dst=ip4_dst, ip6_dst=ip6_dst, - throughput=None, ) # TODO will include throughput and ipv6 in the future @@ -213,7 +267,6 @@ class CanvasGraph(tk.Canvas): self.mode = GraphMode.NODE def handle_edge_release(self, event): - print("Calling edge release") edge = self.drawing_edge self.drawing_edge = None @@ -253,14 +306,19 @@ class CanvasGraph(tk.Canvas): # draw link info on the edge if1 = self.grpc_manager.edges[edge.token].interface_1 if2 = self.grpc_manager.edges[edge.token].interface_2 + ip4_and_prefix_1 = None + ip4_and_prefix_2 = None + if if1 is not None: + ip4_and_prefix_1 = if1.ip4_and_prefix + if if2 is not None: + ip4_and_prefix_2 = if2.ip4_and_prefix edge.link_info = LinkInfo( self, - edge.id, - ip4_src=if1.ip4_and_prefix, + edge, + ip4_src=ip4_and_prefix_1, ip6_src=None, - ip4_dst=if2.ip4_and_prefix, + ip4_dst=ip4_and_prefix_2, ip6_dst=None, - throughput=None, ) logging.debug(f"edges: {self.find_withtag('edge')}") @@ -272,7 +330,6 @@ class CanvasGraph(tk.Canvas): :param event: mouse event :return: nothing """ - print("click on the canvas") logging.debug(f"click press: {event}") selected = self.get_selected(event) is_node = selected in self.find_withtag("node") @@ -302,7 +359,12 @@ class CanvasGraph(tk.Canvas): def add_node(self, x, y, image, node_name): if self.selected == 1: node = CanvasNode( - x=x, y=y, image=image, canvas=self, core_id=self.grpc_manager.peek_id() + x=x, + y=y, + image=image, + node_type=node_name, + canvas=self, + core_id=self.grpc_manager.peek_id(), ) self.nodes[node.id] = node self.grpc_manager.add_node( @@ -316,9 +378,9 @@ class CanvasEdge: Canvas edge class """ - width = 1.3 + width = 1.4 - def __init__(self, x1, y1, x2, y2, src, canvas): + def __init__(self, x1, y1, x2, y2, src, canvas, is_wired=None): """ Create an instance of canvas edge object :param int x1: source x-coord @@ -331,13 +393,28 @@ class CanvasEdge: self.src = src self.dst = None self.canvas = canvas - self.id = self.canvas.create_line( - x1, y1, x2, y2, tags="edge", width=self.width, fill="#ff0000" - ) + + if is_wired is None or is_wired is True: + self.id = self.canvas.create_line( + x1, y1, x2, y2, tags="edge", width=self.width, fill="#ff0000" + ) + else: + self.id = self.canvas.create_line( + x1, + y1, + x2, + y2, + tags="edge", + width=self.width, + fill="#ff0000", + state=tk.HIDDEN, + ) self.token = None # link info object self.link_info = None + self.throughput = None + self.wired = is_wired # TODO resolve this # self.canvas.tag_lower(self.id) @@ -346,6 +423,7 @@ class CanvasEdge: self.token = tuple(sorted((self.src, self.dst))) x1, y1, _, _ = self.canvas.coords(self.id) self.canvas.coords(self.id, x1, y1, x, y) + self.canvas.helper.draw_wireless_case(self.src, self.dst, self) self.canvas.lift(self.src) self.canvas.lift(self.dst) @@ -354,8 +432,9 @@ class CanvasEdge: class CanvasNode: - def __init__(self, x, y, image, canvas, core_id): + def __init__(self, x, y, image, node_type, canvas, core_id): self.image = image + self.node_type = node_type self.canvas = canvas self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags="node" @@ -367,6 +446,8 @@ class CanvasNode: self.text_id = self.canvas.create_text( x, y + 20, text=self.name, tags="nodename" ) + self.antenna_draw = WlanAntennaManager(self.canvas, self.id) + self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) self.canvas.tag_bind(self.id, "", self.motion) @@ -374,6 +455,7 @@ class CanvasNode: self.canvas.tag_bind(self.id, "", self.double_click) self.edges = set() + self.wlans = [] self.moving = None def double_click(self, event): @@ -386,7 +468,6 @@ class CanvasNode: self.x_coord, self.y_coord = self.canvas.coords(self.id) def click_press(self, event): - print("click on the node") logging.debug(f"click press {self.name}: {event}") self.moving = self.canvas.canvas_xy(event) # return "break" @@ -410,7 +491,13 @@ class CanvasNode: old_x, old_y = self.canvas.coords(self.id) self.canvas.move(self.id, offset_x, offset_y) self.canvas.move(self.text_id, offset_x, offset_y) + self.antenna_draw.update_antennas_position(offset_x, offset_y) + new_x, new_y = self.canvas.coords(self.id) + + if self.canvas.core_grpc.get_session_state() == core_pb2.SessionState.RUNTIME: + self.canvas.core_grpc.edit_node(self.core_id, int(new_x), int(new_y)) + for edge in self.edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) if x1 == old_x and y1 == old_y: @@ -418,63 +505,11 @@ class CanvasNode: else: self.canvas.coords(edge.id, x1, y1, new_x, new_y) edge.link_info.recalculate_info() + self.canvas.core_grpc.throughput_draw.update_throughtput_location(edge) + + self.canvas.helper.update_wlan_connection( + old_x, old_y, new_x, new_y, self.wlans + ) def context(self, event): logging.debug(f"context click {self.name}: {event}") - - -class LinkInfo: - def __init__(self, canvas, edge_id, ip4_src, ip6_src, ip4_dst, ip6_dst, throughput): - self.canvas = canvas - self.edge_id = edge_id - self.radius = 37 - - self.ip4_address_1 = ip4_src - self.ip6_address_1 = ip6_src - self.ip4_address_2 = ip4_dst - self.ip6_address_2 = ip6_dst - self.throughput = throughput - self.id1 = self.create_edge_src_info() - self.id2 = self.create_edge_dst_info() - - def slope_src_dst(self): - x1, y1, x2, y2 = self.canvas.coords(self.edge_id) - return (y2 - y1) / (x2 - x1) - - def create_edge_src_info(self): - x1, y1, x2, _ = self.canvas.coords(self.edge_id) - m = self.slope_src_dst() - distance = math.cos(math.atan(m)) * self.radius - if x1 > x2: - distance = -distance - # id1 = self.canvas.create_text(x1, y1, text=self.ip4_address_1) - print(self.ip4_address_1) - id1 = self.canvas.create_text( - x1 + distance, y1 + distance * m, text=self.ip4_address_1, tags="linkinfo" - ) - return id1 - - def create_edge_dst_info(self): - x1, _, x2, y2 = self.canvas.coords(self.edge_id) - m = self.slope_src_dst() - distance = math.cos(math.atan(m)) * self.radius - if x1 > x2: - distance = -distance - # id2 = self.canvas.create_text(x2, y2, text=self.ip4_address_2) - id2 = self.canvas.create_text( - x2 - distance, y2 - distance * m, text=self.ip4_address_2, tags="linkinfo" - ) - return id2 - - def recalculate_info(self): - x1, y1, x2, y2 = self.canvas.coords(self.edge_id) - m = self.slope_src_dst() - distance = math.cos(math.atan(m)) * self.radius - if x1 > x2: - distance = -distance - new_x1 = x1 + distance - new_y1 = y1 + distance * m - new_x2 = x2 - distance - new_y2 = y2 - distance * m - self.canvas.coords(self.id1, new_x1, new_y1) - self.canvas.coords(self.id2, new_x2, new_y2) diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py new file mode 100644 index 00000000..2fc6e011 --- /dev/null +++ b/coretk/coretk/graph_helper.py @@ -0,0 +1,198 @@ +""" +Some graph helper functions +""" +import logging +import tkinter as tk + +from core.api.grpc import core_pb2 +from coretk.images import ImageEnum, Images + + +class GraphHelper: + def __init__(self, canvas): + """ + create an instance of GraphHelper object + """ + self.canvas = canvas + + def draw_wireless_case(self, src_id, dst_id, edge): + src_node_name = self.canvas.nodes[src_id].node_type + dst_node_name = self.canvas.nodes[dst_id].node_type + + if src_node_name == "wlan" or dst_node_name == "wlan": + self.canvas.itemconfig(edge.id, state=tk.HIDDEN) + edge.wired = False + if edge.token not in self.canvas.edges: + if src_node_name == "wlan" and dst_node_name == "wlan": + self.canvas.nodes[src_id].antenna_draw.add_antenna() + elif src_node_name == "wlan": + self.canvas.nodes[dst_id].antenna_draw.add_antenna() + else: + self.canvas.nodes[src_id].antenna_draw.add_antenna() + + edge.wired = True + + def redraw_antenna(self, link, node_one, node_two): + if link.type == core_pb2.LinkType.WIRELESS: + if node_one.node_type == "wlan" and node_two.node_type == "wlan": + node_one.antenna_draw.add_antenna() + elif node_one.node_type == "wlan" and node_two.node_type != "wlan": + node_two.antenna_draw.add_antenna() + elif node_one.node_type != "wlan" and node_two.node_type == "wlan": + node_one.antenna_draw.add_antenna() + else: + logging.error( + "graph_helper.py WIRELESS link but both nodes are non-wireless node" + ) + + def update_wlan_connection(self, old_x, old_y, new_x, new_y, edge_ids): + for eid in edge_ids: + x1, y1, x2, y2 = self.canvas.coords(eid) + if x1 == old_x and y1 == old_y: + self.canvas.coords(eid, new_x, new_y, x2, y2) + else: + self.canvas.coords(eid, x1, y1, new_x, new_y) + + +class WlanAntennaManager: + def __init__(self, canvas, node_id): + """ + crate an instance for AntennaManager + """ + self.canvas = canvas + self.node_id = node_id + self.quantity = 0 + self._max = 5 + self.antennas = [] + + # distance between each antenna + self.offset = 0 + + def add_antenna(self): + """ + add an antenna to a node + + :return: nothing + """ + if self.quantity < 5: + x, y = self.canvas.coords(self.node_id) + self.antennas.append( + self.canvas.create_image( + x - 16 + self.offset, + y - 16, + anchor=tk.CENTER, + image=Images.get(ImageEnum.ANTENNA.value), + tags="antenna", + ) + ) + self.quantity = self.quantity + 1 + self.offset = self.offset + 8 + + def update_antennas_position(self, offset_x, offset_y): + """ + redraw antennas of a node according to the new node position + + :return: nothing + """ + for i in self.antennas: + self.canvas.move(i, offset_x, offset_y) + + def delete_antenna(self, canvas_id): + return + + def delete_antennas(self): + """ + Delete all the antennas of a node + + :return: nothing + """ + for i in self.antennas: + self.canvas.delete(i) + + +# class WlanConnection: +# def __init__(self, canvas, grpc): +# """ +# create in +# :param canvas: +# """ +# self.canvas = canvas +# self.core_grpc = grpc +# self.throughput_on = False +# self.map_node_link = {} +# self.links = [] +# +# def wireless_nodes(self): +# """ +# retrieve all the wireless clouds in the canvas +# +# :return: list(coretk.graph.CanvasNode) +# """ +# wireless_nodes = [] +# for n in self.canvas.nodes.values(): +# if n.node_type == "wlan": +# wireless_nodes.append(n) +# return wireless_nodes +# +# def draw_wireless_link(self, src, dst): +# """ +# draw a line between 2 nodes that are connected to the same wireless cloud +# +# :param coretk.graph.CanvasNode src: source node +# :param coretk.graph.CanvasNode dst: destination node +# :return: nothing +# """ +# cid = self.canvas.create_line(src.x_coord, src.y_coord, dst.x_coord, dst.y_coord, tags="wlanconnection") +# if src.id not in self.map_node_link: +# self.map_node_link[src.id] = [] +# if dst.id not in self.map_node_link: +# self.map_node_link[dst.id] = [] +# self.map_node_link[src.id].append(cid) +# self.map_node_link[dst.id].append(cid) +# self.links.append(cid) +# +# def subnet_wireless_connection(self, wlan_node): +# """ +# retrieve all the non-wireless nodes connected to wireless_node and create line (represent wireless connection) between each pair of nodes +# :param coretk.grpah.CanvasNode wlan_node: wireless node +# +# :return: nothing +# """ +# non_wlan_nodes = [] +# for e in wlan_node.edges: +# src = self.canvas.nodes[e.src] +# dst = self.canvas.nodes[e.dst] +# if src.node_type == "wlan" and dst.node_type != "wlan": +# non_wlan_nodes.append(dst) +# elif src.node_type != "wlan" and dst.node_type == "wlan": +# non_wlan_nodes.append(src) +# +# size = len(non_wlan_nodes) +# for i in range(size): +# for j in range(i+1, size): +# self.draw_wireless_link(non_wlan_nodes[i], non_wlan_nodes[j]) +# +# def session_wireless_connection(self): +# """ +# draw all the wireless connection in the canvas +# +# :return: nothing +# """ +# wlan_nodes = self.wireless_nodes() +# for n in wlan_nodes: +# self.subnet_wireless_connection(n) +# +# def show_links(self): +# """ +# show all the links +# """ +# for l in self.links: +# self.canvas.itemconfig(l, state=tk.NORMAL) +# +# def hide_links(self): +# """ +# hide all the links +# :return: +# """ +# for l in self.links: +# self.canvas.itemconfig(l, state=tk.HIDDEN) diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index ea1ee947..20f1a74a 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -5,6 +5,7 @@ that can be useful for grpc, acts like a session class import logging from core.api.grpc import core_pb2 +from coretk.coretocanvas import CoreToCanvasMapping from coretk.interface import Interface, InterfaceManager link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] @@ -61,8 +62,13 @@ class GrpcManager: self.reusable = [] self.preexisting = [] + # self.core_id_to_canvas_id = {} self.interfaces_manager = InterfaceManager() + # map tuple(core_node_id, interface_id) to and edge + # self.node_id_and_interface_to_edge_token = {} + self.core_mapping = CoreToCanvasMapping() + def peek_id(self): """ Peek the next id to be used @@ -117,8 +123,11 @@ class GrpcManager: node_model = name else: logging.error("grpcmanagemeny.py INVALID node name") - create_node = Node(session_id, self.get_id(), node_type, node_model, x, y, name) + nid = self.get_id() + create_node = Node(session_id, nid, node_type, node_model, x, y, name) self.nodes[canvas_id] = create_node + self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) + # self.core_id_to_canvas_id[nid] = canvas_id logging.debug( "Adding node to GrpcManager.. Session id: %s, Coords: (%s, %s), Name: %s", session_id, @@ -219,7 +228,7 @@ class GrpcManager: dst_node = self.nodes[dst_canvas_id] if dst_node.model in network_layer_nodes: ifid = len(dst_node.interfaces) - name = "veth" + str(ifid) + name = "eth" + str(ifid) dst_interface = Interface( name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) ) @@ -232,6 +241,7 @@ class GrpcManager: edge.interface_1 = src_interface edge.interface_2 = dst_interface + return src_interface, dst_interface def add_edge(self, session_id, token, canvas_id_1, canvas_id_2): """ @@ -253,7 +263,37 @@ class GrpcManager: self.nodes[canvas_id_2].type, ) self.edges[token] = edge - self.create_interface(edge, canvas_id_1, canvas_id_2) + src_interface, dst_interface = self.create_interface( + edge, canvas_id_1, canvas_id_2 + ) + node_one_id = self.nodes[canvas_id_1].node_id + node_two_id = self.nodes[canvas_id_2].node_id + + # provide a way to get an edge from a core node and an interface id + if src_interface is not None: + # self.node_id_and_interface_to_edge_token[tuple([node_one_id, src_interface.id])] = token + self.core_mapping.map_node_and_interface_to_canvas_edge( + node_one_id, src_interface.id, token + ) + logging.debug( + "map node id %s, interface_id %s to edge token %s", + node_one_id, + src_interface.id, + token, + ) + + if dst_interface is not None: + # self.node_id_and_interface_to_edge_token[tuple([node_two_id, dst_interface.id])] = token + self.core_mapping.map_node_and_interface_to_canvas_edge( + node_two_id, dst_interface.id, token + ) + logging.debug( + "map node id %s, interface_id %s to edge token %s", + node_two_id, + dst_interface.id, + token, + ) + logging.debug("Adding edge to grpc manager...") else: logging.error("grpcmanagement.py INVALID CANVAS NODE ID") diff --git a/coretk/coretk/icons/antenna.gif b/coretk/coretk/icons/antenna.gif new file mode 100644 index 0000000000000000000000000000000000000000..55814324be26242f061339e6cd85d0ebfa67c8b1 GIT binary patch literal 230 zcmZ?wbhEHb)Me0RIK;{T1Zin$?H&D_H*bFS?Ad=9Q2fWIk*sNCU|^=;l%JZJm(HO0 zlZBCsft^7Iqz9y%fq7=dt~>t>PI<20Yw>z*%LIA$wu$OaDpThjU$rSV+3545te3L4 z4c2bo87$x;EGEftbE0tw!@SeeF1l5zS^GY_bn3O=?wHJw67KH-e4D?=Sg*L9zEOBr z-}0AbT~_ipKg-wE^ENhdx3uO}bToH$b2Cop7bHOy`C3Coz bFfU_Yv1-jSmJO>mux#46e%q>aP6lfLpq*ob literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/document-new.gif b/coretk/coretk/icons/document-new.gif new file mode 100644 index 0000000000000000000000000000000000000000..570b45e690fa2f57a8320af5433809beed01d541 GIT binary patch literal 1054 zcmeIx>q}Ds7{~FmF->Kani;e#Z&XAOg>+ew_Zuc8i1?;bEX!U9zNjpUw3a!uU36|< z(sa|glxjv4lpvU-Xa(l9ZSJ^pKik>PHgBanJ6rRA=w)9%Jb%Csp4|ML(`U*ffk=Rq z0nL)eW{LQ@G^j};dnT1P%UUGzR!K{nw6#sDP|B4msj|IA+1{$^RCIJHySi1~FFShr zx_Z?w)crjgZSSy7G%(mVIHcAM_3MTQUJVZnz1F^Ys~Y?Cz%+C5*SAXA0MG^wZQ#-d z9&HGr=Xvz}HhMmso{w-XL{JMyJjO`3>A=e3zEv=B6--)%&i_G9E?T}Xfum;2f(e{k zK}s>J+6*pGa3ckatxHpjOU5;<#ECrF#Iq6TGiWtftR@E{rx3+De%VXpSRnH{eg%X~ zAY}HExhOn|z|))L1uJYJ(T|L+fWowltq?*?R>b1Kv=9Q?un!Pwflv@eK@9teU{(}| zP#neyBStKu1dI_@jDQK!h?5{j!Z?W%wjhiQvIpU|Ad0Y4B;~MCPP@ZtchZ!TcF-<| z%k6ZzX}84CTk2yI%@(8`A0pD`}lKbW&+-VurqU z*^`kZtp0H|{dnR;UuHs9dt_<4T#$09NfDN;)5fPAl=w6<9e>ZE)I8wb-8o+C!(Gu; zMdO*d%~6kU`lAvur&GhlGOWEG4HakC0U;+iqUo_Paf3mhUNIS-k(?JJT+WIYWfk1x zM@5VBnv!#h>uNXRXJZeS=!>hOuTAexjomNIOZhsZI#xE-7+w)N`{*e@W+Y!zxm|Fo z+B}-Cfcm&OHPD1S>H3a1;jO)KwT-<+-NHK_%=Be&L&M!EFxITuW+zD5+WXsuz1}(w Nm&*gnpz<`}>|gl)4y^zH literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/edit-delete.gif b/coretk/coretk/icons/edit-delete.gif new file mode 100644 index 0000000000000000000000000000000000000000..d23f758c7ddf6f31eb685d8d30930ae6017d500f GIT binary patch literal 1006 zcmZ?wbhEHb6krfwc;3#SqOPv4qo<*(uc>FCZD^!zXsl;qqGw`eXkuh+ZenC%VQOh< zZe?Ll3!6;)m&HC-rC;Y-q_XA*4fqE)7jqL z(bCh|+S}FH+uPj_ME#wUCiG6}pEPyq;;P#E?lx?!SW@GRxDVwV)3f=%U5q$v3C82^&2;=+q`!D*3BEXZP~nI z!{!~Ex9-}yZTIHwyLa#0y?fXG?RyUF-+y5LzJvSsA3S*I=#k^6j+{Ps{Pelgr_Y@| zeg5>B^XJc8IDh`qg^O1%UAlVq($&kCu3frv{p!^l*RI~Ue(mO^Yd0@nyLtWQ?MpXr zU%7Gn`i&~^i_io<3f9L*#8xI~mdi?ak_?%XjbJeE9JC{fBoSKD_<%`Qzs=AHIBf|K-ca|D%9jA)xq^g^`QlKZ6bf5PpV3N^QRbX&vc*H5LFT%M>QaGKnot4Mr zibW8c*9qoImY*Ay)4iuC`K?fR`DxR+c}~qdf=ox$efcB}b=G$6JKk1V5QxKE#`RT&>|v=%>_EGRItEZYX>VOJtMJI25sS(Icmq NtJ&5n@o+F$0|0Y`xn=+W literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/fileopen.gif b/coretk/coretk/icons/fileopen.gif new file mode 100644 index 0000000000000000000000000000000000000000..fb74420765af2473e13624376b818542d7c92349 GIT binary patch literal 1095 zcmZ?wbhEHb6krfw_|C`x1pi3{4Iq#JN1>sicnm11NGhvLEw4(0qRKQNx2PlqmzL6s zP?p&Ld6P1`8wErMb0Z?|^!Gl}2?_9Nd?VRN+rY&1BZOO7JOO{Sqyma!y#gi5; znz(uEio~R#@CdKNM|bSlwR-2SRX}q4j+NVXtlYkR<(6&BK{390$%c)K*Kb_3X59iH z1E@GC$aVJYjx}p$tzA2N-MTsJ*U#IqVgAOA3)ioozjp1MRjXz$Up{^5(#eY!P3Y;V z3Ji4i^K?e0?1hf3koxfet7qfbs+b$3F%}76F3>2S#Rg0hNFa4UWw0 z{0c4;3>uwSxy21qE^KIYX6Ki8Xb~`Ma^VzK4_FegvB{NNTtDH+1jA-GUTO1!Cl@w0 zyYtK2H?RmAwRi|ByH8LF+|=SJtnRfNoSC% z_jH5QOItdF)&1u?vvNv7Rly3yMUo?hD89j+h0zkyZQ ztS7=S`S=9Y;B7sT#_8u5xK1|fjWW%?z9IGUw%%y-{QCz~TZPU0Vl0cFUsxKvy)V|f d{QZNYlg<0%Y^%S2czStzf4qJDe-;)7YXHy9P`m&D literal 0 HcmV?d00001 diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index c5df15bb..f865c191 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -87,6 +87,10 @@ class ImageEnum(Enum): STOP = "stop" OBSERVE = "observe" RUN = "run" + DOCUMENTNEW = "document-new" + FILEOPEN = "fileopen" + EDITDELETE = "edit-delete" + ANTENNA = "antenna" def load_core_images(images): diff --git a/coretk/coretk/interface.py b/coretk/coretk/interface.py index 8538ab1f..4bad6e99 100644 --- a/coretk/coretk/interface.py +++ b/coretk/coretk/interface.py @@ -20,6 +20,11 @@ class Interface: self.id = ifid def random_mac_address(self): + """ + create a random MAC address for an interface + + :return: nothing + """ return "02:00:00:%02x:%02x:%02x" % ( random.randint(0, 255), random.randint(0, 255), diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py new file mode 100644 index 00000000..7480e004 --- /dev/null +++ b/coretk/coretk/linkinfo.py @@ -0,0 +1,134 @@ +""" +Link information, such as IPv4, IPv6 and throughput drawn in the canvas +""" +import math + + +class LinkInfo: + def __init__(self, canvas, edge, ip4_src, ip6_src, ip4_dst, ip6_dst): + self.canvas = canvas + self.edge = edge + # self.edge_id = edge.id + self.radius = 37 + self.core_grpc = self.canvas.core_grpc + + self.ip4_address_1 = ip4_src + self.ip6_address_1 = ip6_src + self.ip4_address_2 = ip4_dst + self.ip6_address_2 = ip6_dst + self.id1 = self.create_edge_src_info() + self.id2 = self.create_edge_dst_info() + + def slope_src_dst(self): + x1, y1, x2, y2 = self.canvas.coords(self.edge.id) + if x2 - x1 == 0: + return 9999 + else: + return (y2 - y1) / (x2 - x1) + + def create_edge_src_info(self): + x1, y1, x2, _ = self.canvas.coords(self.edge.id) + m = self.slope_src_dst() + distance = math.cos(math.atan(m)) * self.radius + if x1 > x2: + distance = -distance + # id1 = self.canvas.create_text(x1, y1, text=self.ip4_address_1) + id1 = self.canvas.create_text( + x1 + distance, y1 + distance * m, text=self.ip4_address_1, tags="linkinfo" + ) + return id1 + + def create_edge_dst_info(self): + x1, _, x2, y2 = self.canvas.coords(self.edge.id) + m = self.slope_src_dst() + distance = math.cos(math.atan(m)) * self.radius + if x1 > x2: + distance = -distance + # id2 = self.canvas.create_text(x2, y2, text=self.ip4_address_2) + id2 = self.canvas.create_text( + x2 - distance, y2 - distance * m, text=self.ip4_address_2, tags="linkinfo" + ) + return id2 + + def recalculate_info(self): + x1, y1, x2, y2 = self.canvas.coords(self.edge.id) + m = self.slope_src_dst() + distance = math.cos(math.atan(m)) * self.radius + if x1 > x2: + distance = -distance + new_x1 = x1 + distance + new_y1 = y1 + distance * m + new_x2 = x2 - distance + new_y2 = y2 - distance * m + self.canvas.coords(self.id1, new_x1, new_y1) + self.canvas.coords(self.id2, new_x2, new_y2) + + # def link_througput(self): + # x1, y1, x2, y2 = self.canvas.coords(self.edge.id) + # x = (x1 + x2) / 2 + # y = (y1 + y2) / 2 + # tid = self.canvas.create_text(x, y, text="place text here") + # return tid + + +class Throughput: + def __init__(self, canvas, grpc): + self.canvas = canvas + self.core_grpc = grpc + self.grpc_manager = canvas.grpc_manager + + # edge canvas id mapped to throughput value + self.tracker = {} + + # map an edge canvas id to a throughput canvas id + self.map = {} + + def load_throughput_info(self, interface_throughputs): + """ + load all interface throughouts from an event + + :param repeated core_bp2.InterfaceThroughputinterface_throughputs: interface throughputs + :return: nothing + """ + for t in interface_throughputs: + nid = t.node_id + iid = t.interface_id + tp = t.throughput + # token = self.grpc_manager.node_id_and_interface_to_edge_token[nid, iid] + token = self.grpc_manager.core_mapping.get_token_from_node_and_interface( + nid, iid + ) + edge_id = self.canvas.edges[token].id + if edge_id not in self.tracker: + self.tracker[edge_id] = tp + else: + temp = self.tracker[edge_id] + self.tracker[edge_id] = (temp + tp) / 2 + + def draw_throughputs(self): + for edge_id in self.tracker: + x1, y1, x2, y2 = self.canvas.coords(edge_id) + x = (x1 + x2) / 2 + y = (y1 + y2) / 2 + if edge_id not in self.map: + tp_id = self.canvas.create_text( + x, y, text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]) + ) + self.map[edge_id] = tp_id + else: + self.canvas.itemconfig( + self.map[edge_id], + text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), + ) + + def process_grpc_throughput_event(self, interface_throughputs): + self.load_throughput_info(interface_throughputs) + self.draw_throughputs() + + def update_throughtput_location(self, edge): + tp_id = self.map[edge.id] + x1, y1 = self.canvas.coords(edge.src) + x2, y2 = self.canvas.coords(edge.dst) + x = (x1 + x2) / 2 + y = (y1 + y2) / 2 + self.canvas.coords(tp_id, x, y) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 6618e532..207a1f3a 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -3,12 +3,10 @@ The actions taken when each menubar option is clicked """ import logging -import tkinter as tk import webbrowser -from tkinter import messagebox +from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 -from coretk.coregrpc import CoreGrpc SAVEDIR = "/home/ncs/Desktop/" @@ -317,14 +315,6 @@ def session_options(): logging.debug("Click session options") -# def help_core_github(): -# webbrowser.open_new("https://github.com/coreemu/core") -# -# -# def help_core_documentation(): -# webbrowser.open_new("http://coreemu.github.io/core/") - - def help_about(): logging.debug("Click help About") @@ -367,7 +357,8 @@ class MenuAction: grpc.delete_links() grpc.delete_nodes() grpc.delete_session() - + # else: + # grpc.set_session_state("definition") grpc.core.close() # self.application.quit() @@ -384,35 +375,43 @@ class MenuAction: def file_save_as_xml(self): logging.info("menuaction.py file_save_as_xml()") grpc = self.application.core_grpc - file_path = tk.filedialog.asksaveasfilename( + file_path = filedialog.asksaveasfilename( initialdir=SAVEDIR, title="Save As", filetypes=(("EmulationScript XML files", "*.xml"), ("All files", "*")), defaultextension=".xml", ) - with open("prev_saved_xml.txt", "a") as file: - file.write(file_path + "\n") + # with open("prev_saved_xml.txt", "a") as file: + # file.write(file_path + "\n") grpc.save_xml(file_path) def file_open_xml(self): logging.info("menuaction.py file_open_xml()") - # grpc = self.application.core_grpc - file_path = tk.filedialog.askopenfilename( + file_path = filedialog.askopenfilename( initialdir=SAVEDIR, title="Open", filetypes=(("EmulationScript XML File", "*.xml"), ("All Files", "*")), ) - # clean up before openning a new session - # t0 = time.clock() + # clean up before opening a new session self.clean_nodes_links_and_set_configuarations() - grpc = CoreGrpc(self.application.master) - grpc.core.connect() - session_id = grpc.open_xml(file_path) - grpc.session_id = session_id - self.application.core_grpc = grpc - self.application.canvas.core_grpc = grpc - self.application.canvas.delete_components() - self.application.canvas.draw_existing_component() + # grpc = CoreGrpc(self.application.master) + # grpc.core.connect() + core_grpc = self.application.core_grpc + core_grpc.core.connect() + # session_id = core_grpc.open_xml(file_path) + # core_grpc.session_id = session_id + + core_grpc.open_xml(file_path) + # print("Print session state") + # print(grpc.get_session_state()) + self.application.canvas.canvas_reset_and_redraw(core_grpc) + + # Todo might not need + self.application.core_grpc = core_grpc + + self.application.core_editbar.destroy_children_widgets() + self.application.core_editbar.create_runtime_toolbar() + # self.application.canvas.draw_existing_component() # t1 = time.clock() # print(t1 - t0) diff --git a/coretk/coretk/prev_saved_xml.txt b/coretk/coretk/prev_saved_xml.txt index 3d6d4d91..bfb9d15a 100644 --- a/coretk/coretk/prev_saved_xml.txt +++ b/coretk/coretk/prev_saved_xml.txt @@ -1,5 +1,2 @@ -/home/ncs/Desktop/random.xml/home/ncs/Desktop/untitled.xml/home/ncs/Desktop/test.xml -/home/ncs/Desktop/test1.xml - -/home/ncs/Desktop/untitled.xml -/home/ncs/Desktop/test1.xml +/home/ncs/Desktop/runningtest.xml +/home/ncs/Desktop/notrunning.xml diff --git a/coretk/coretk/querysessiondrawing.py b/coretk/coretk/querysessiondrawing.py new file mode 100644 index 00000000..639b4ba5 --- /dev/null +++ b/coretk/coretk/querysessiondrawing.py @@ -0,0 +1,174 @@ +import logging +import tkinter as tk +from tkinter.ttk import Scrollbar, Treeview + +from coretk.images import ImageEnum, Images + + +class SessionTable: + def __init__(self, grpc, master): + self.grpc = grpc + self.selected = False + self.selected_sid = None + self.master = master + self.top = tk.Toplevel(self.master) + self.description_definition() + self.top.title("CORE sessions") + + self.tree = Treeview(self.top) + # self.tree.pack(side=tk.TOP) + self.tree.grid(row=1, column=0, columnspan=2) + self.draw_scrollbar() + self.draw() + + def description_definition(self): + lable = tk.Label( + self.top, + text="Below is a list of active CORE sessions. Double-click to " + "\nconnect to an existing session. Usually, only sessions in " + "\nthe RUNTIME state persist in the daemon, except for the " + "\none you might be concurrently editting.", + ) + lable.grid(sticky=tk.W) + + def column_definition(self): + # self.tree["columns"] = ("name", "nodecount", "filename", "date") + self.tree["columns"] = "nodecount" + self.tree.column("#0", width=300, minwidth=30) + # self.tree.column("name", width=72, miwidth=30) + self.tree.column("nodecount", width=300, minwidth=30) + # self.tree.column("filename", width=92, minwidth=30) + # self.tree.column("date", width=170, minwidth=30) + + def draw_scrollbar(self): + yscrollbar = Scrollbar(self.top, orient="vertical", command=self.tree.yview) + yscrollbar.grid(row=1, column=3, sticky=tk.N + tk.S + tk.W) + self.tree.configure(yscrollcommand=yscrollbar.set) + + xscrollbar = Scrollbar(self.top, orient="horizontal", command=self.tree.xview) + xscrollbar.grid(row=2, columnspan=2, sticky=tk.E + tk.W + tk.S) + self.tree.configure(xscrollcommand=xscrollbar.set) + + def heading_definition(self): + self.tree.heading("#0", text="ID", anchor=tk.W) + # self.tree.heading("name", text="Name", anchor=tk.CENTER) + self.tree.heading("nodecount", text="Node Count", anchor=tk.W) + # self.tree.heading("filename", text="Filename", anchor=tk.CENTER) + # self.tree.heading("date", text="Date", anchor=tk.CENTER) + + def enter_session(self, sid): + self.top.destroy() + response = self.grpc.core.get_session(sid) + self.grpc.session_id = sid + self.grpc.core.events(sid, self.grpc.log_event) + logging.info("Entering session_id %s.... Result: %s", sid, response) + + def new_session(self): + self.top.destroy() + self.grpc.create_new_session() + + def on_selected(self, event): + item = self.tree.selection() + sid = int(self.tree.item(item, "text")) + self.enter_session(sid) + + def click_select(self, event): + # logging.debug("Click on %s ", event) + item = self.tree.selection() + sid = int(self.tree.item(item, "text")) + self.selected = True + self.selected_sid = sid + + def session_definition(self): + response = self.grpc.core.get_sessions() + # logging.info("querysessiondrawing.py Get all sessions %s", response) + index = 1 + for session in response.sessions: + self.tree.insert( + "", index, None, text=str(session.id), values=(str(session.nodes)) + ) + index = index + 1 + self.tree.bind("", self.on_selected) + self.tree.bind("<>", self.click_select) + + def click_connect(self): + """ + if no session is selected yet, create a new one else join that session + + :return: nothing + """ + if self.selected and self.selected_sid is not None: + self.enter_session(self.selected_sid) + elif not self.selected and self.selected_sid is None: + self.new_session() + else: + logging.error("querysessiondrawing.py invalid state") + + def shutdown_session(self, sid): + self.grpc.terminate_session(sid) + self.new_session() + self.top.destroy() + + def click_shutdown(self): + """ + if no session is currently selected create a new session else shut the selected session down + + :return: nothing + """ + if self.selected and self.selected_sid is not None: + self.shutdown_session(self.selected_sid) + elif not self.selected and self.selected_sid is None: + self.new_session() + else: + logging.error("querysessiondrawing.py invalid state") + # if self.selected and self.selected_sid is not None: + + def draw_buttons(self): + f = tk.Frame(self.top) + f.grid(row=3, sticky=tk.W) + + b = tk.Button( + f, + image=Images.get(ImageEnum.DOCUMENTNEW.value), + text="New", + compound=tk.LEFT, + command=self.new_session, + ) + b.pack(side=tk.LEFT, padx=3, pady=4) + b = tk.Button( + f, + image=Images.get(ImageEnum.FILEOPEN.value), + text="Connect", + compound=tk.LEFT, + command=self.click_connect, + ) + b.pack(side=tk.LEFT, padx=3, pady=4) + b = tk.Button( + f, + image=Images.get(ImageEnum.EDITDELETE.value), + text="Shutdown", + compound=tk.LEFT, + command=self.click_shutdown, + ) + b.pack(side=tk.LEFT, padx=3, pady=4) + b = tk.Button(f, text="Cancel", command=self.new_session) + b.pack(side=tk.LEFT, padx=3, pady=4) + + def center(self): + window_width = self.master.winfo_width() + window_height = self.master.winfo_height() + self.top.update() + top_level_width = self.top.winfo_width() + top_level_height = self.top.winfo_height() + x = window_width / 2 - top_level_width / 2 + y = window_height / 2 - top_level_height / 2 + + self.top.geometry("+%d+%d" % (x, y)) + + def draw(self): + self.column_definition() + self.heading_definition() + self.session_definition() + self.draw_buttons() + self.center() + self.top.wait_window() diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py new file mode 100644 index 00000000..63042e19 --- /dev/null +++ b/coretk/coretk/wirelessconnection.py @@ -0,0 +1,53 @@ +""" +Wireless connection handler +""" +from core.api.grpc import core_pb2 + + +class WirelessConnection: + def __init__(self, canvas, grpc): + self.canvas = canvas + self.core_grpc = grpc + self.core_mapping = canvas.grpc_manager.core_mapping + + # map a (node_one_id, node_two_id) to a wlan canvas id + self.map = {} + + def add_wlan_connection(self, node_one_id, node_two_id): + canvas_id_one = self.core_mapping.get_canvas_id_from_core_id(node_one_id) + canvas_id_two = self.core_mapping.get_canvas_id_from_core_id(node_two_id) + key = tuple(sorted((node_one_id, node_two_id))) + + if key not in self.map: + x1, y1 = self.canvas.coords(canvas_id_one) + x2, y2 = self.canvas.coords(canvas_id_two) + wlan_canvas_id = self.canvas.create_line( + x1, y1, x2, y2, fill="#009933", tags="wlan", width=1.5 + ) + self.map[key] = wlan_canvas_id + self.canvas.nodes[canvas_id_one].wlans.append(wlan_canvas_id) + self.canvas.nodes[canvas_id_two].wlans.append(wlan_canvas_id) + + def delete_wlan_connection(self, node_one_id, node_two_id): + canvas_id_one = self.core_mapping.get_canvas_id_from_core_id(node_one_id) + canvas_id_two = self.core_mapping.get_canvas_id_from_core_id(node_two_id) + + key = tuple(sorted((node_one_id, node_two_id))) + wlan_canvas_id = self.map[key] + + self.canvas.nodes[canvas_id_one].wlans.remove(wlan_canvas_id) + self.canvas.nodes[canvas_id_two].wlans.remove(wlan_canvas_id) + + self.canvas.delete(wlan_canvas_id) + self.map.pop(key, None) + + def hangle_link_event(self, link_event): + if link_event.message_type == core_pb2.MessageType.ADD: + self.add_wlan_connection( + link_event.link.node_one_id, link_event.link.node_two_id + ) + + if link_event.message_type == core_pb2.MessageType.DELETE: + self.delete_wlan_connection( + link_event.link.node_one_id, link_event.link.node_two_id + ) From c0ab9ea4ccfa01742bb4c7e404c3e6d53b6ee4e9 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 18 Oct 2019 21:51:15 -0700 Subject: [PATCH 085/462] small update to grpc docs --- docs/grpc.md | 53 ++-------------------------------------------------- 1 file changed, 2 insertions(+), 51 deletions(-) diff --git a/docs/grpc.md b/docs/grpc.md index d9743506..46149c54 100644 --- a/docs/grpc.md +++ b/docs/grpc.md @@ -1,60 +1,11 @@ # Using the gRPC API -By default the gRPC API is currently not turned on by default. There are a couple ways that this can be enabled -to use. +gRPC is the main API for interfacing with CORE. -## Enabling gRPC - -### HTTP Proxy +## HTTP Proxy Since gRPC is HTTP2 based, proxy configurations can cause issue. Clear out your proxy when running if needed. -### Daemon Options - -The gRPC API is enabled through options provided to the **core-daemon**. - -```shell -usage: core-daemon [-h] [-f CONFIGFILE] [-p PORT] [-n NUMTHREADS] [--ovs] - [--grpc] [--grpc-port GRPCPORT] - [--grpc-address GRPCADDRESS] - -CORE daemon v.5.3.0 instantiates Linux network namespace nodes. - -optional arguments: - -h, --help show this help message and exit - -f CONFIGFILE, --configfile CONFIGFILE - read config from specified file; default = - /etc/core/core.conf - -p PORT, --port PORT port number to listen on; default = 4038 - -n NUMTHREADS, --numthreads NUMTHREADS - number of server threads; default = 1 - --ovs enable experimental ovs mode, default is false - --grpc enable grpc api, default is false - --grpc-port GRPCPORT grpc port to listen on; default 50051 - --grpc-address GRPCADDRESS - grpc address to listen on; default localhost -``` - -### Enabling in Service Files - -Modify service files to append the --grpc options as desired. - -For sysv services /etc/init.d/core-daemon -```shell -CMD="PYTHONPATH=/usr/lib/python3.6/site-packages python3 /usr/bin/$NAME --grpc" -``` - -For systemd service /lib/systemd/system/core-daemon.service -```shell -ExecStart=@PYTHON@ @bindir@/core-daemon --grpc -``` - -### Enabling from Command Line - -```shell -sudo core-daemon --grpc -``` - ## Python Client A python client wrapper is provided at **core.api.grpc.client.CoreGrpcClient**. From d4af459653344d78d855bf5824a34b07f79c8bcd Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 18 Oct 2019 22:56:10 -0700 Subject: [PATCH 086/462] update to distributed core doc --- docs/distributed.md | 109 +++++++++++++++++++------------------------- 1 file changed, 48 insertions(+), 61 deletions(-) diff --git a/docs/distributed.md b/docs/distributed.md index 076476db..4c5894f8 100644 --- a/docs/distributed.md +++ b/docs/distributed.md @@ -9,39 +9,17 @@ A large emulation scenario can be deployed on multiple emulation servers and controlled by a single GUI. The GUI, representing the entire topology, can be run on one of the emulation servers or on a separate machine. -Each machine that will act as an emulation server would ideally have the - same version of CORE installed. It is not important to have the GUI component - but the CORE Python daemon **core-daemon** needs to be installed. - -**NOTE: The server that the GUI connects with is referred to as -the master server.** +Each machine that will act as an emulation will require the installation of a distributed CORE package and +some configuration to allow SSH as root. -## Configuring Listen Address +## Configuring SSH -First we need to configure the **core-daemon** on all servers to listen on an -interface over the network. The simplest way would be updating the core -configuration file to listen on all interfaces. Alternatively, configure it to -listen to the specific interface you desire by supplying the correct address. +Distributed CORE works using the python fabric library to run commands on remote servers over SSH. -The **listenaddr** configuration should be set to the address of the interface -that should receive CORE API control commands from the other servers; -setting **listenaddr = 0.0.0.0** causes the Python daemon to listen on all -interfaces. CORE uses TCP port **4038** by default to communicate from the -controlling machine (with GUI) to the emulation servers. Make sure that -firewall rules are configured as necessary to allow this traffic. +### Remote GUI Terminals -```shell -# open configuration file -vi /etc/core/core.conf - -# within core.conf -[core-daemon] -listenaddr = 0.0.0.0 -``` - -## Enabling Remote SSH Shells - -### Update GUI Terminal Program +You need to have the same user defined on each server, since the user used +for these remote shells is the same user that is running the CORE GUI. **Edit -> Preferences... -> Terminal program:** @@ -54,31 +32,51 @@ May need to install xterm if, not already installed. sudo apt install xterm ``` -### Setup SSH +### Distributed Server SSH Configuration -In order to easily open shells on the emulation servers, the servers should be -running an SSH server, and public key login should be enabled. This is -accomplished by generating an SSH key for your user on all servers being used -for distributed emulation, if you do not already have one. Then copying your -master server public key to the authorized_keys file on all other servers that -will be used to help drive the distributed emulation. When double-clicking on a -node during runtime, instead of opening a local shell, the GUI will attempt to -SSH to the emulation server to run an interactive shell. +First the distributed servers must be configured to allow passwordless root login over SSH. -You need to have the same user defined on each server, since the user used -for these remote shells is the same user that is running the CORE GUI. - -```shell +On distributed server: +```shelll # install openssh-server sudo apt install openssh-server -# generate ssh if needed -ssh-keygen -o -t rsa -b 4096 +# open sshd config +vi /etc/ssh/sshd_config + +# verify these configurations in file +PermitRootLogin yes +PasswordAuthentication yes + +# restart sshd +sudo systemctl restart sshd +``` + +On master server: +```shell +# install package if needed +sudo apt install openssh-client + +# generate ssh key if needed +ssh-keygen -o -t rsa -b 4096 -f ~/.ssh/core # copy public key to authorized_keys file -ssh-copy-id user@server -# or -scp ~/.ssh/id_rsa.pub username@server:~/.ssh/authorized_keys +ssh-copy-id -i ~/.ssh/core root@server + +# configure fabric to use the core ssh key +sudo vi /etc/fabric.yml +``` + +On distributed server: +```shell +# open sshd config +vi /etc/ssh/sshd_config + +# change configuration for root login to without password +PermitRootLogin without-password + +# restart sshd +sudo systemctl restart sshd ``` ## Add Emulation Servers in GUI @@ -155,27 +153,16 @@ The names before the addresses need to match the servers configured in controlnet = core1:172.16.1.0/24 core2:172.16.2.0/24 core3:172.16.3.0/24 core4:172.16.4.0/24 core5:172.16.5.0/24 ``` -EMANE appears to require location events for nodes to be sync'ed across -all EMANE instances for nodes to find each other. Using an EMANE eel file -for your scenario can help clear this up, which might be desired anyway. - -* https://github.com/adjacentlink/emane/wiki/EEL-Generator - -You can also move nodes within the GUI to help trigger location events from -CORE when the **core.conf** settings below is used. Assuming the nodes -did not find each other by default and you are not using an eel file. - ```shell emane_event_generate = True ``` ## Distributed Checklist -1. Install the same version of the CORE daemon on all servers. -1. Set **listenaddr** configuration in all of the server's core.conf files, -then start (or restart) the daemon. +1. Install CORE on master server +1. Install distributed CORE package on all servers needed 1. Installed and configure public-key SSH access on all servers (if you want to use -double-click shells or Widgets.) +double-click shells or Widgets.) for both the GUI user (for terminals) and root for running CORE commands 1. Choose the servers that participate in distributed emulation. 1. Assign nodes to desired servers, empty for master server. 1. Press the **Start** button to launch the distributed emulation. From 6006710c32f559542bd588e015b2389fda49ce58 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 18 Oct 2019 23:28:09 -0700 Subject: [PATCH 087/462] changed net_cmd to host_cmd and node_net_cmd to cmd, for simpler more logical naming --- daemon/core/api/grpc/server.py | 2 +- daemon/core/api/tlv/corehandlers.py | 4 +-- daemon/core/emane/emanemanager.py | 6 ++-- daemon/core/emulator/session.py | 2 +- daemon/core/nodes/base.py | 42 +++++++++++++--------------- daemon/core/nodes/docker.py | 14 +++++----- daemon/core/nodes/interface.py | 8 +++--- daemon/core/nodes/lxd.py | 14 +++++----- daemon/core/nodes/network.py | 26 ++++++++--------- daemon/core/nodes/physical.py | 8 +++--- daemon/core/services/coreservices.py | 6 ++-- daemon/examples/python/switch.py | 6 ++-- daemon/examples/python/wlan.py | 6 ++-- daemon/tests/emane/test_emane.py | 2 +- daemon/tests/test_core.py | 2 +- daemon/tests/test_nodes.py | 2 +- 16 files changed, 74 insertions(+), 76 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 11bfcd6b..08713f71 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -881,7 +881,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session = self.get_session(request.session_id, context) node = self.get_node(session, request.node_id, context) try: - output = node.node_net_cmd(request.command) + output = node.cmd(request.command) except CoreCommandError as e: output = e.stderr return core_pb2.NodeCommandResponse(output=output) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index c7827e1f..1cb3a45d 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -885,7 +885,7 @@ class CoreHandler(socketserver.BaseRequestHandler): status = e.returncode else: try: - res = node.node_net_cmd(command) + res = node.cmd(command) status = 0 except CoreCommandError as e: res = e.stderr @@ -911,7 +911,7 @@ class CoreHandler(socketserver.BaseRequestHandler): if message.flags & MessageFlags.LOCAL.value: utils.mute_detach(command) else: - node.node_net_cmd(command, wait=False) + node.cmd(command, wait=False) except CoreError: logging.exception("error getting object: %s", node_num) # XXX wait and queue this message to try again later diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 743f90b2..a03e8e2a 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -583,7 +583,7 @@ class EmaneManager(ModelManager): log_file = os.path.join(path, f"emane{n}.log") platform_xml = os.path.join(path, f"platform{n}.xml") args = f"{emanecmd} -f {log_file} {platform_xml}" - output = node.node_net_cmd(args) + output = node.cmd(args) logging.info("node(%s) emane daemon running: %s", node.name, args) logging.info("node(%s) emane daemon output: %s", node.name, output) @@ -613,7 +613,7 @@ class EmaneManager(ModelManager): continue if node.up: - node.node_net_cmd(kill_emaned, wait=False) + node.cmd(kill_emaned, wait=False) # TODO: RJ45 node if stop_emane_on_host: @@ -813,7 +813,7 @@ class EmaneManager(ModelManager): """ args = "pkill -0 -x emane" try: - node.node_net_cmd(args) + node.cmd(args) result = True except CoreCommandError: result = False diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index d80e5e25..076b79b3 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -1915,4 +1915,4 @@ class Session(object): utils.mute_detach(data) else: node = self.get_node(node_id) - node.node_net_cmd(data, wait=False) + node.cmd(data, wait=False) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index e963683b..9a91c5ce 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -61,7 +61,7 @@ class NodeBase(object): self.position = Position() use_ovs = session.options.get_config("ovs") == "True" - self.net_client = get_net_client(use_ovs, self.net_cmd) + self.net_client = get_net_client(use_ovs, self.host_cmd) def startup(self): """ @@ -79,10 +79,9 @@ class NodeBase(object): """ raise NotImplementedError - def net_cmd(self, args, env=None, cwd=None, wait=True): + def host_cmd(self, args, env=None, cwd=None, wait=True): """ - Runs a command that is used to configure and setup the network on the host - system. + Runs a command on the host system or distributed server. :param str args: command to run :param dict env: environment to run command with @@ -265,7 +264,7 @@ class CoreNodeBase(NodeBase): """ if self.nodedir is None: self.nodedir = os.path.join(self.session.session_dir, self.name + ".conf") - self.net_cmd(f"mkdir -p {self.nodedir}") + self.host_cmd(f"mkdir -p {self.nodedir}") self.tmpnodedir = True else: self.tmpnodedir = False @@ -281,7 +280,7 @@ class CoreNodeBase(NodeBase): return if self.tmpnodedir: - self.net_cmd(f"rm -rf {self.nodedir}") + self.host_cmd(f"rm -rf {self.nodedir}") def addnetif(self, netif, ifindex): """ @@ -383,10 +382,9 @@ class CoreNodeBase(NodeBase): return common - def node_net_cmd(self, args, wait=True): + def cmd(self, args, wait=True): """ - Runs a command that is used to configure and setup the network within a - node. + Runs a command within a node container. :param str args: command to run :param bool wait: True to wait for status, False otherwise @@ -462,7 +460,7 @@ class CoreNode(CoreNodeBase): :param bool use_ovs: True for OVS bridges, False for Linux bridges :return:node network client """ - return get_net_client(use_ovs, self.node_net_cmd) + return get_net_client(use_ovs, self.cmd) def alive(self): """ @@ -472,7 +470,7 @@ class CoreNode(CoreNodeBase): :rtype: bool """ try: - self.net_cmd(f"kill -0 {self.pid}") + self.host_cmd(f"kill -0 {self.pid}") except CoreCommandError: return False @@ -502,7 +500,7 @@ class CoreNode(CoreNodeBase): env["NODE_NUMBER"] = str(self.id) env["NODE_NAME"] = str(self.name) - output = self.net_cmd(vnoded, env=env) + output = self.host_cmd(vnoded, env=env) self.pid = int(output) logging.debug("node(%s) pid: %s", self.name, self.pid) @@ -546,13 +544,13 @@ class CoreNode(CoreNodeBase): # kill node process if present try: - self.net_cmd(f"kill -9 {self.pid}") + self.host_cmd(f"kill -9 {self.pid}") except CoreCommandError: logging.exception("error killing process") # remove node directory if present try: - self.net_cmd(f"rm -rf {self.ctrlchnlname}") + self.host_cmd(f"rm -rf {self.ctrlchnlname}") except CoreCommandError: logging.exception("error removing node directory") @@ -565,7 +563,7 @@ class CoreNode(CoreNodeBase): finally: self.rmnodedir() - def node_net_cmd(self, args, wait=True): + def cmd(self, args, wait=True): """ Runs a command that is used to configure and setup the network within a node. @@ -607,7 +605,7 @@ class CoreNode(CoreNodeBase): hostpath = os.path.join( self.nodedir, os.path.normpath(path).strip("/").replace("/", ".") ) - self.net_cmd(f"mkdir -p {hostpath}") + self.host_cmd(f"mkdir -p {hostpath}") self.mount(hostpath, path) def mount(self, source, target): @@ -621,8 +619,8 @@ class CoreNode(CoreNodeBase): """ source = os.path.abspath(source) logging.debug("node(%s) mounting: %s at %s", self.name, source, target) - self.node_net_cmd(f"mkdir -p {target}") - self.node_net_cmd(f"{MOUNT_BIN} -n --bind {source} {target}") + self.cmd(f"mkdir -p {target}") + self.cmd(f"{MOUNT_BIN} -n --bind {source} {target}") self._mounts.append((source, target)) def newifindex(self): @@ -846,7 +844,7 @@ class CoreNode(CoreNodeBase): self.client.check_cmd(f"mv {srcname} {filename}") self.client.check_cmd("sync") else: - self.net_cmd(f"mkdir -p {directory}") + self.host_cmd(f"mkdir -p {directory}") self.server.remote_put(srcname, filename) def hostfilename(self, filename): @@ -883,9 +881,9 @@ class CoreNode(CoreNodeBase): open_file.write(contents) os.chmod(open_file.name, mode) else: - self.net_cmd(f"mkdir -m {0o755:o} -p {dirname}") + self.host_cmd(f"mkdir -m {0o755:o} -p {dirname}") self.server.remote_put_temp(hostfilename, contents) - self.net_cmd(f"chmod {mode:o} {hostfilename}") + self.host_cmd(f"chmod {mode:o} {hostfilename}") logging.debug( "node(%s) added file: %s; mode: 0%o", self.name, hostfilename, mode ) @@ -906,7 +904,7 @@ class CoreNode(CoreNodeBase): else: self.server.remote_put(srcfilename, hostfilename) if mode is not None: - self.net_cmd(f"chmod {mode:o} {hostfilename}") + self.host_cmd(f"chmod {mode:o} {hostfilename}") logging.info( "node(%s) copied file: %s; mode: %s", self.name, hostfilename, mode ) diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index 369f462b..d4850d8a 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -131,7 +131,7 @@ class DockerNode(CoreNode): if self.up: raise ValueError("starting a node that is already up") self.makenodedir() - self.client = DockerClient(self.name, self.image, self.net_cmd) + self.client = DockerClient(self.name, self.image, self.host_cmd) self.pid = self.client.create_container() self.up = True @@ -176,7 +176,7 @@ class DockerNode(CoreNode): """ logging.debug("creating node dir: %s", path) args = f"mkdir -p {path}" - self.node_net_cmd(args) + self.cmd(args) def mount(self, source, target): """ @@ -206,13 +206,13 @@ class DockerNode(CoreNode): temp.close() if directory: - self.node_net_cmd(f"mkdir -m {0o755:o} -p {directory}") + self.cmd(f"mkdir -m {0o755:o} -p {directory}") if self.server is not None: self.server.remote_put(temp.name, temp.name) self.client.copy_file(temp.name, filename) - self.node_net_cmd(f"chmod {mode:o} {filename}") + self.cmd(f"chmod {mode:o} {filename}") if self.server is not None: - self.net_cmd(f"rm -f {temp.name}") + self.host_cmd(f"rm -f {temp.name}") os.unlink(temp.name) logging.debug( "node(%s) added file: %s; mode: 0%o", self.name, filename, mode @@ -232,7 +232,7 @@ class DockerNode(CoreNode): "node file copy file(%s) source(%s) mode(%s)", filename, srcfilename, mode ) directory = os.path.dirname(filename) - self.node_net_cmd(f"mkdir -p {directory}") + self.cmd(f"mkdir -p {directory}") if self.server is None: source = srcfilename @@ -242,4 +242,4 @@ class DockerNode(CoreNode): self.server.remote_put(source, temp.name) self.client.copy_file(source, filename) - self.node_net_cmd(f"chmod {mode:o} {filename}") + self.cmd(f"chmod {mode:o} {filename}") diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index c8841432..cf215e5b 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -17,7 +17,7 @@ class CoreInterface(object): def __init__(self, session, node, name, mtu, server=None): """ - Creates a PyCoreNetIf instance. + Creates a CoreInterface instance. :param core.emulator.session.Session session: core session instance :param core.nodes.base.CoreNode node: node for interface @@ -46,11 +46,11 @@ class CoreInterface(object): self.flow_id = None self.server = server use_ovs = session.options.get_config("ovs") == "True" - self.net_client = get_net_client(use_ovs, self.net_cmd) + self.net_client = get_net_client(use_ovs, self.host_cmd) - def net_cmd(self, args, env=None, cwd=None, wait=True): + def host_cmd(self, args, env=None, cwd=None, wait=True): """ - Runs a command on the host system or distributed servers. + Runs a command on the host system or distributed server. :param str args: command to run :param dict env: environment to run command with diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index 323b20a9..77078047 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -112,7 +112,7 @@ class LxcNode(CoreNode): if self.up: raise ValueError("starting a node that is already up") self.makenodedir() - self.client = LxdClient(self.name, self.image, self.net_cmd) + self.client = LxdClient(self.name, self.image, self.host_cmd) self.pid = self.client.create_container() self.up = True @@ -149,7 +149,7 @@ class LxcNode(CoreNode): """ logging.info("creating node dir: %s", path) args = f"mkdir -p {path}" - return self.node_net_cmd(args) + return self.cmd(args) def mount(self, source, target): """ @@ -180,13 +180,13 @@ class LxcNode(CoreNode): temp.close() if directory: - self.node_net_cmd(f"mkdir -m {0o755:o} -p {directory}") + self.cmd(f"mkdir -m {0o755:o} -p {directory}") if self.server is not None: self.server.remote_put(temp.name, temp.name) self.client.copy_file(temp.name, filename) - self.node_net_cmd(f"chmod {mode:o} {filename}") + self.cmd(f"chmod {mode:o} {filename}") if self.server is not None: - self.net_cmd(f"rm -f {temp.name}") + self.host_cmd(f"rm -f {temp.name}") os.unlink(temp.name) logging.debug("node(%s) added file: %s; mode: 0%o", self.name, filename, mode) @@ -204,7 +204,7 @@ class LxcNode(CoreNode): "node file copy file(%s) source(%s) mode(%s)", filename, srcfilename, mode ) directory = os.path.dirname(filename) - self.node_net_cmd(f"mkdir -p {directory}") + self.cmd(f"mkdir -p {directory}") if self.server is None: source = srcfilename @@ -214,7 +214,7 @@ class LxcNode(CoreNode): self.server.remote_put(source, temp.name) self.client.copy_file(source, filename) - self.node_net_cmd(f"chmod {mode:o} {filename}") + self.cmd(f"chmod {mode:o} {filename}") def addnetif(self, netif, ifindex): super(LxcNode, self).addnetif(netif, ifindex) diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 229005c4..9b122650 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -162,20 +162,20 @@ class EbtablesQueue(object): """ # save kernel ebtables snapshot to a file args = self.ebatomiccmd("--atomic-save") - wlan.net_cmd(args) + wlan.host_cmd(args) # modify the table file using queued ebtables commands for c in self.cmds: args = self.ebatomiccmd(c) - wlan.net_cmd(args) + wlan.host_cmd(args) self.cmds = [] # commit the table file to the kernel args = self.ebatomiccmd("--atomic-commit") - wlan.net_cmd(args) + wlan.host_cmd(args) try: - wlan.net_cmd(f"rm -f {self.atomic_file}") + wlan.host_cmd(f"rm -f {self.atomic_file}") except CoreCommandError: logging.exception("error removing atomic file: %s", self.atomic_file) @@ -270,7 +270,7 @@ class CoreNetwork(CoreNetworkBase): self.startup() ebq.startupdateloop(self) - def net_cmd(self, args, env=None, cwd=None, wait=True): + def host_cmd(self, args, env=None, cwd=None, wait=True): """ Runs a command that is used to configure and setup the network on the host system and all configured distributed servers. @@ -302,7 +302,7 @@ class CoreNetwork(CoreNetworkBase): f"{EBTABLES_BIN} -N {self.brname} -P {self.policy}", f"{EBTABLES_BIN} -A FORWARD --logical-in {self.brname} -j {self.brname}", ] - ebtablescmds(self.net_cmd, cmds) + ebtablescmds(self.host_cmd, cmds) self.up = True @@ -323,7 +323,7 @@ class CoreNetwork(CoreNetworkBase): f"{EBTABLES_BIN} -D FORWARD --logical-in {self.brname} -j {self.brname}", f"{EBTABLES_BIN} -X {self.brname}", ] - ebtablescmds(self.net_cmd, cmds) + ebtablescmds(self.host_cmd, cmds) except CoreCommandError: logging.exception("error during shutdown") @@ -462,13 +462,13 @@ class CoreNetwork(CoreNetworkBase): if bw > 0: if self.up: cmd = f"{tc} {parent} handle 1: {tbf}" - netif.net_cmd(cmd) + netif.host_cmd(cmd) netif.setparam("has_tbf", True) changed = True elif netif.getparam("has_tbf") and bw <= 0: if self.up: cmd = f"{TC_BIN} qdisc delete dev {devname} {parent}" - netif.net_cmd(cmd) + netif.host_cmd(cmd) netif.setparam("has_tbf", False) # removing the parent removes the child netif.setparam("has_netem", False) @@ -510,14 +510,14 @@ class CoreNetwork(CoreNetworkBase): return if self.up: cmd = f"{TC_BIN} qdisc delete dev {devname} {parent} handle 10:" - netif.net_cmd(cmd) + netif.host_cmd(cmd) netif.setparam("has_netem", False) elif len(netem) > 1: if self.up: cmd = ( f"{TC_BIN} qdisc replace dev {devname} {parent} handle 10: {netem}" ) - netif.net_cmd(cmd) + netif.host_cmd(cmd) netif.setparam("has_netem", True) def linknet(self, net): @@ -802,7 +802,7 @@ class CtrlNet(CoreNetwork): self.brname, self.updown_script, ) - self.net_cmd(f"{self.updown_script} {self.brname} startup") + self.host_cmd(f"{self.updown_script} {self.brname} startup") if self.serverintf: self.net_client.create_interface(self.brname, self.serverintf) @@ -830,7 +830,7 @@ class CtrlNet(CoreNetwork): self.brname, self.updown_script, ) - self.net_cmd(f"{self.updown_script} {self.brname} shutdown") + self.host_cmd(f"{self.updown_script} {self.brname} shutdown") except CoreCommandError: logging.exception("error issuing shutdown script shutdown") diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index cae3f298..c1d6328b 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -188,13 +188,13 @@ class PhysicalNode(CoreNodeBase): source = os.path.abspath(source) logging.info("mounting %s at %s", source, target) os.makedirs(target) - self.net_cmd(f"{MOUNT_BIN} --bind {source} {target}", cwd=self.nodedir) + self.host_cmd(f"{MOUNT_BIN} --bind {source} {target}", cwd=self.nodedir) self._mounts.append((source, target)) def umount(self, target): logging.info("unmounting '%s'", target) try: - self.net_cmd(f"{UMOUNT_BIN} -l {target}", cwd=self.nodedir) + self.host_cmd(f"{UMOUNT_BIN} -l {target}", cwd=self.nodedir) except CoreCommandError: logging.exception("unmounting failed for %s", target) @@ -220,8 +220,8 @@ class PhysicalNode(CoreNodeBase): os.chmod(node_file.name, mode) logging.info("created nodefile: '%s'; mode: 0%o", node_file.name, mode) - def node_net_cmd(self, args, wait=True): - return self.net_cmd(args, wait=wait) + def cmd(self, args, wait=True): + return self.host_cmd(args, wait=wait) class Rj45Node(CoreNodeBase, CoreInterface): diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index 6db2d8ed..20553eb1 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -598,7 +598,7 @@ class CoreServices(object): for cmd in cmds: logging.debug("validating service(%s) using: %s", service.name, cmd) try: - node.node_net_cmd(cmd) + node.cmd(cmd) except CoreCommandError as e: logging.debug( "node(%s) service(%s) validate failed", node.name, service.name @@ -631,7 +631,7 @@ class CoreServices(object): status = 0 for args in service.shutdown: try: - node.node_net_cmd(args) + node.cmd(args) except CoreCommandError: logging.exception("error running stop command %s", args) status = -1 @@ -729,7 +729,7 @@ class CoreServices(object): status = 0 for cmd in cmds: try: - node.node_net_cmd(cmd, wait) + node.cmd(cmd, wait) except CoreCommandError: logging.exception("error starting command") status = -1 diff --git a/daemon/examples/python/switch.py b/daemon/examples/python/switch.py index 80257a4a..252253f8 100644 --- a/daemon/examples/python/switch.py +++ b/daemon/examples/python/switch.py @@ -42,12 +42,12 @@ def example(options): last_node = session.get_node(options.nodes + 1) logging.info("starting iperf server on node: %s", first_node.name) - first_node.node_net_cmd("iperf -s -D") + first_node.cmd("iperf -s -D") first_node_address = prefixes.ip4_address(first_node) logging.info("node %s connecting to %s", last_node.name, first_node_address) - output = last_node.node_net_cmd(f"iperf -t {options.time} -c {first_node_address}") + output = last_node.cmd(f"iperf -t {options.time} -c {first_node_address}") logging.info(output) - first_node.node_net_cmd("killall -9 iperf") + first_node.cmd("killall -9 iperf") # shutdown session coreemu.shutdown() diff --git a/daemon/examples/python/wlan.py b/daemon/examples/python/wlan.py index 9506a35c..4b4bc724 100644 --- a/daemon/examples/python/wlan.py +++ b/daemon/examples/python/wlan.py @@ -46,11 +46,11 @@ def example(options): last_node = session.get_node(options.nodes + 1) logging.info("starting iperf server on node: %s", first_node.name) - first_node.node_net_cmd("iperf -s -D") + first_node.cmd("iperf -s -D") address = prefixes.ip4_address(first_node) logging.info("node %s connecting to %s", last_node.name, address) - last_node.node_net_cmd(f"iperf -t {options.time} -c {address}") - first_node.node_net_cmd("killall -9 iperf") + last_node.cmd(f"iperf -t {options.time} -c {address}") + first_node.cmd("killall -9 iperf") # shutdown session coreemu.shutdown() diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py index a0ae05d1..3eb87596 100644 --- a/daemon/tests/emane/test_emane.py +++ b/daemon/tests/emane/test_emane.py @@ -27,7 +27,7 @@ _DIR = os.path.dirname(os.path.abspath(__file__)) def ping(from_node, to_node, ip_prefixes, count=3): address = ip_prefixes.ip4_address(to_node) try: - from_node.node_net_cmd(f"ping -c {count} {address}") + from_node.cmd(f"ping -c {count} {address}") status = 0 except CoreCommandError as e: status = e.returncode diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index fa2adc6e..47368740 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -20,7 +20,7 @@ _WIRED = [NodeTypes.PEER_TO_PEER, NodeTypes.HUB, NodeTypes.SWITCH] def ping(from_node, to_node, ip_prefixes): address = ip_prefixes.ip4_address(to_node) try: - from_node.node_net_cmd(f"ping -c 3 {address}") + from_node.cmd(f"ping -c 3 {address}") status = 0 except CoreCommandError as e: status = e.returncode diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index 34c426c5..e495dea9 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -30,7 +30,7 @@ class TestNodes: assert os.path.exists(node.nodedir) assert node.alive() assert node.up - assert node.node_net_cmd("ip address show lo") + assert node.cmd("ip address show lo") def test_node_update(self, session): # given From d056578e9d6d888766d472e5ede15b7c3a17b9c3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 21 Oct 2019 09:36:07 -0700 Subject: [PATCH 088/462] modified ctrlnets to use an id starting at 9001, to avoid string based ids --- daemon/core/emulator/session.py | 7 +++---- daemon/core/nodes/network.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index d80e5e25..a2f72647 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -71,6 +71,7 @@ NODES = { NodeTypes.LXC: LxcNode, } NODES_TYPE = {NODES[x]: x for x in NODES} +CTRL_NET_ID = 9001 class Session(object): @@ -1667,9 +1668,7 @@ class Session(object): return -1 def get_control_net(self, net_index): - # TODO: all nodes use an integer id and now this wants to use a string - _id = f"ctrl{net_index}net" - return self.get_node(_id) + return self.get_node(CTRL_NET_ID + net_index) def add_remove_control_net(self, net_index, remove=False, conf_required=True): """ @@ -1716,7 +1715,7 @@ class Session(object): return None # build a new controlnet bridge - _id = f"ctrl{net_index}net" + _id = CTRL_NET_ID + net_index # use the updown script for control net 0 only. updown_script = None diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 229005c4..46730212 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -723,7 +723,7 @@ class CtrlNet(CoreNetwork): def __init__( self, session, - _id="ctrlnet", + _id=None, name=None, prefix=None, hostid=None, From 3fc0ca5cec5c388250bdd6cefb771ed5a57708ac Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 21 Oct 2019 09:51:52 -0700 Subject: [PATCH 089/462] fix to get_node over grpc to avoid issues with nodes that dont have services --- daemon/core/api/grpc/server.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 08713f71..0ab4806a 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -807,7 +807,10 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): if isinstance(node, EmaneNet): emane_model = node.model.name - services = [x.name for x in getattr(node, "services", [])] + services = [] + if node.services: + services = [x.name for x in node.services] + position = core_pb2.Position( x=node.position.x, y=node.position.y, z=node.position.z ) From 16b7e70c33acd97160f1a7d4a601f03eacd6e470 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 21 Oct 2019 10:08:41 -0700 Subject: [PATCH 090/462] update to add config example for fabric.yml --- docs/distributed.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/distributed.md b/docs/distributed.md index 4c5894f8..b6ef9f77 100644 --- a/docs/distributed.md +++ b/docs/distributed.md @@ -48,6 +48,10 @@ vi /etc/ssh/sshd_config PermitRootLogin yes PasswordAuthentication yes +# if desired add/modify the following line to allow SSH to +# accept all env variables +AcceptEnv * + # restart sshd sudo systemctl restart sshd ``` @@ -65,6 +69,9 @@ ssh-copy-id -i ~/.ssh/core root@server # configure fabric to use the core ssh key sudo vi /etc/fabric.yml + +# set configuration +connect_kwargs: {"key_filename": "/home/user/.ssh/core"} ``` On distributed server: From 78f981463d5dd03bc0f784c1236c9d365e127fa0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 21 Oct 2019 10:32:42 -0700 Subject: [PATCH 091/462] renamed utils.check_cmd to utils.cmd, updated host_cmd to allow for shell commands for output redirection --- daemon/core/api/tlv/corehandlers.py | 2 +- daemon/core/emane/emanemanager.py | 8 ++++---- daemon/core/emane/tdma.py | 2 +- daemon/core/location/mobility.py | 2 +- daemon/core/nodes/base.py | 5 +++-- daemon/core/nodes/client.py | 2 +- daemon/core/nodes/docker.py | 6 +++--- daemon/core/nodes/interface.py | 5 +++-- daemon/core/nodes/lxd.py | 2 +- daemon/core/nodes/netclient.py | 10 ++++------ daemon/core/nodes/network.py | 7 ++++--- daemon/core/services/utility.py | 2 +- daemon/core/utils.py | 8 +++++--- daemon/core/xml/corexmldeployment.py | 2 +- daemon/tests/test_nodes.py | 2 +- 15 files changed, 34 insertions(+), 31 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 1cb3a45d..41bac314 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -878,7 +878,7 @@ class CoreHandler(socketserver.BaseRequestHandler): ): if message.flags & MessageFlags.LOCAL.value: try: - res = utils.check_cmd(command) + res = utils.cmd(command) status = 0 except CoreCommandError as e: res = e.stderr diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index a03e8e2a..b40ed118 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -140,7 +140,7 @@ class EmaneManager(ModelManager): try: # check for emane args = "emane --version" - emane_version = utils.check_cmd(args) + emane_version = utils.cmd(args) logging.info("using EMANE: %s", emane_version) self.session.distributed.execute(lambda x: x.remote_cmd(args)) @@ -594,7 +594,7 @@ class EmaneManager(ModelManager): log_file = os.path.join(path, "emane.log") platform_xml = os.path.join(path, "platform.xml") emanecmd += f" -f {log_file} {platform_xml}" - utils.check_cmd(emanecmd, cwd=path) + utils.cmd(emanecmd, cwd=path) self.session.distributed.execute(lambda x: x.remote_cmd(emanecmd, cwd=path)) logging.info("host emane daemon running: %s", emanecmd) @@ -618,8 +618,8 @@ class EmaneManager(ModelManager): if stop_emane_on_host: try: - utils.check_cmd(kill_emaned) - utils.check_cmd(kill_transortd) + utils.cmd(kill_emaned) + utils.cmd(kill_transortd) self.session.distributed.execute(lambda x: x.remote_cmd(kill_emaned)) self.session.distributed.execute(lambda x: x.remote_cmd(kill_transortd)) except CoreCommandError: diff --git a/daemon/core/emane/tdma.py b/daemon/core/emane/tdma.py index 91e662ea..afad9d10 100644 --- a/daemon/core/emane/tdma.py +++ b/daemon/core/emane/tdma.py @@ -63,4 +63,4 @@ class EmaneTdmaModel(emanemodel.EmaneModel): "setting up tdma schedule: schedule(%s) device(%s)", schedule, event_device ) args = f"emaneevent-tdmaschedule -i {event_device} {schedule}" - utils.check_cmd(args) + utils.cmd(args) diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index f2b49818..92bd1b4b 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -1187,6 +1187,6 @@ class Ns2ScriptedMobility(WayPointMobility): return filename = self.findfile(filename) args = f"/bin/sh {filename} {typestr}" - utils.check_cmd( + utils.cmd( args, cwd=self.session.session_dir, env=self.session.get_environment() ) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 9a91c5ce..8b35410d 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -79,7 +79,7 @@ class NodeBase(object): """ raise NotImplementedError - def host_cmd(self, args, env=None, cwd=None, wait=True): + def host_cmd(self, args, env=None, cwd=None, wait=True, shell=False): """ Runs a command on the host system or distributed server. @@ -87,12 +87,13 @@ class NodeBase(object): :param dict env: environment to run command with :param str cwd: directory to run command in :param bool wait: True to wait for status, False otherwise + :param bool shell: True to use shell, False otherwise :return: combined stdout and stderr :rtype: str :raises CoreCommandError: when a non-zero exit status occurs """ if self.server is None: - return utils.check_cmd(args, env, cwd, wait) + return utils.cmd(args, env, cwd, wait, shell) else: return self.server.remote_cmd(args, env, cwd, wait) diff --git a/daemon/core/nodes/client.py b/daemon/core/nodes/client.py index 299b8135..3596bfa7 100644 --- a/daemon/core/nodes/client.py +++ b/daemon/core/nodes/client.py @@ -65,4 +65,4 @@ class VnodeClient(object): """ self._verify_connection() args = self.create_cmd(args) - return utils.check_cmd(args, wait=wait) + return utils.cmd(args, wait=wait) diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index d4850d8a..a70cfb39 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -45,14 +45,14 @@ class DockerClient(object): def check_cmd(self, cmd): logging.info("docker cmd output: %s", cmd) - return utils.check_cmd(f"docker exec {self.name} {cmd}") + return utils.cmd(f"docker exec {self.name} {cmd}") def create_ns_cmd(self, cmd): return f"nsenter -t {self.pid} -u -i -p -n {cmd}" def ns_cmd(self, cmd, wait): args = f"nsenter -t {self.pid} -u -i -p -n {cmd}" - return utils.check_cmd(args, wait=wait) + return utils.cmd(args, wait=wait) def get_pid(self): args = f"docker inspect -f '{{{{.State.Pid}}}}' {self.name}" @@ -153,7 +153,7 @@ class DockerNode(CoreNode): def nsenter_cmd(self, args, wait=True): if self.server is None: args = self.client.create_ns_cmd(args) - return utils.check_cmd(args, wait=wait) + return utils.cmd(args, wait=wait) else: args = self.client.create_ns_cmd(args) return self.server.remote_cmd(args, wait=wait) diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index cf215e5b..a32103b8 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -48,7 +48,7 @@ class CoreInterface(object): use_ovs = session.options.get_config("ovs") == "True" self.net_client = get_net_client(use_ovs, self.host_cmd) - def host_cmd(self, args, env=None, cwd=None, wait=True): + def host_cmd(self, args, env=None, cwd=None, wait=True, shell=False): """ Runs a command on the host system or distributed server. @@ -56,12 +56,13 @@ class CoreInterface(object): :param dict env: environment to run command with :param str cwd: directory to run command in :param bool wait: True to wait for status, False otherwise + :param bool shell: True to use shell, False otherwise :return: combined stdout and stderr :rtype: str :raises CoreCommandError: when a non-zero exit status occurs """ if self.server is None: - return utils.check_cmd(args, env, cwd, wait) + return utils.cmd(args, env, cwd, wait, shell) else: return self.server.remote_cmd(args, env, cwd, wait) diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index 77078047..9d5dedc4 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -49,7 +49,7 @@ class LxdClient(object): def check_cmd(self, cmd, wait=True): args = self.create_cmd(cmd) - return utils.check_cmd(args, wait=wait) + return utils.cmd(args, wait=wait) def copy_file(self, source, destination): if destination[0] != "/": diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index beff4e8e..8bd58be7 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -2,8 +2,6 @@ Clients for dealing with bridge/interface commands. """ -import os - from core.constants import BRCTL_BIN, ETHTOOL_BIN, IP_BIN, OVS_BIN, TC_BIN @@ -236,10 +234,10 @@ class LinuxNetClient(object): self.device_up(name) # turn off multicast snooping so forwarding occurs w/o IGMP joins - snoop = f"/sys/devices/virtual/net/{name}/bridge/multicast_snooping" - if os.path.exists(snoop): - with open(snoop, "w") as f: - f.write("0") + snoop_file = "multicast_snooping" + snoop = f"/sys/devices/virtual/net/{name}/bridge/{snoop_file}" + self.run(f"echo 0 > /tmp/{snoop_file}", shell=True) + self.run(f"cp /tmp/{snoop_file} {snoop}") def delete_bridge(self, name): """ diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 892460fd..f85235f1 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -270,7 +270,7 @@ class CoreNetwork(CoreNetworkBase): self.startup() ebq.startupdateloop(self) - def host_cmd(self, args, env=None, cwd=None, wait=True): + def host_cmd(self, args, env=None, cwd=None, wait=True, shell=False): """ Runs a command that is used to configure and setup the network on the host system and all configured distributed servers. @@ -279,12 +279,13 @@ class CoreNetwork(CoreNetworkBase): :param dict env: environment to run command with :param str cwd: directory to run command in :param bool wait: True to wait for status, False otherwise + :param bool shell: True to use shell, False otherwise :return: combined stdout and stderr :rtype: str :raises CoreCommandError: when a non-zero exit status occurs """ logging.info("network node(%s) cmd", self.name) - output = utils.check_cmd(args, env, cwd, wait) + output = utils.cmd(args, env, cwd, wait, shell) self.session.distributed.execute(lambda x: x.remote_cmd(args, env, cwd, wait)) return output @@ -765,7 +766,7 @@ class CtrlNet(CoreNetwork): """ use_ovs = self.session.options.get_config("ovs") == "True" current = f"{address}/{self.prefix.prefixlen}" - net_client = get_net_client(use_ovs, utils.check_cmd) + net_client = get_net_client(use_ovs, utils.cmd) net_client.create_address(self.brname, current) servers = self.session.distributed.servers for name in servers: diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index 14bd5a90..e408b182 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -415,7 +415,7 @@ class HttpService(UtilService): Detect the apache2 version using the 'a2query' command. """ try: - result = utils.check_cmd("a2query -v") + result = utils.cmd("a2query -v") status = 0 except CoreCommandError as e: status = e.returncode diff --git a/daemon/core/utils.py b/daemon/core/utils.py index 7d88e355..2e2296c0 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -191,7 +191,7 @@ def mute_detach(args, **kwargs): return Popen(args, **kwargs).pid -def check_cmd(args, env=None, cwd=None, wait=True): +def cmd(args, env=None, cwd=None, wait=True, shell=False): """ Execute a command on the host and return a tuple containing the exit status and result string. stderr output is folded into the stdout result string. @@ -200,15 +200,17 @@ def check_cmd(args, env=None, cwd=None, wait=True): :param dict env: environment to run command with :param str cwd: directory to run command in :param bool wait: True to wait for status, False otherwise + :param bool shell: True to use shell, False otherwise :return: combined stdout and stderr :rtype: str :raises CoreCommandError: when there is a non-zero exit status or the file to execute is not found """ logging.info("command cwd(%s) wait(%s): %s", cwd, wait, args) - args = shlex.split(args) + if shell is False: + args = shlex.split(args) try: - p = Popen(args, stdout=PIPE, stderr=PIPE, env=env, cwd=cwd) + p = Popen(args, stdout=PIPE, stderr=PIPE, env=env, cwd=cwd, shell=shell) if wait: stdout, stderr = p.communicate() status = p.wait() diff --git a/daemon/core/xml/corexmldeployment.py b/daemon/core/xml/corexmldeployment.py index 239bace2..83bf3333 100644 --- a/daemon/core/xml/corexmldeployment.py +++ b/daemon/core/xml/corexmldeployment.py @@ -69,7 +69,7 @@ def get_ipv4_addresses(hostname): if hostname == "localhost": addresses = [] args = f"{IP_BIN} -o -f inet address show" - output = utils.check_cmd(args) + output = utils.cmd(args) for line in output.split(os.linesep): split = line.split() if not split: diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index e495dea9..70525f70 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -67,4 +67,4 @@ class TestNodes: # then assert node assert node.up - assert utils.check_cmd(f"brctl show {node.brname}") + assert utils.cmd(f"brctl show {node.brname}") From 630b44627c8e5282828aff4777d37887e4472541 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 21 Oct 2019 11:36:59 -0700 Subject: [PATCH 092/462] updated distributed python examples a bit to clean things up --- daemon/examples/python/distributed_emane.py | 22 +++----- daemon/examples/python/distributed_lxd.py | 17 +++--- daemon/examples/python/distributed_parser.py | 15 +++++ daemon/examples/python/distributed_ptp.py | 17 +++--- .../{distributed.py => distributed_switch.py} | 19 +++---- .../examples/python/distributed_switches.py | 43 -------------- daemon/examples/python/distributed_wlan.py | 56 ------------------- daemon/examples/python/emane80211.py | 4 -- daemon/examples/python/switch.py | 7 --- daemon/examples/python/switch_inject.py | 6 -- daemon/examples/python/wlan.py | 7 --- 11 files changed, 46 insertions(+), 167 deletions(-) create mode 100644 daemon/examples/python/distributed_parser.py rename daemon/examples/python/{distributed.py => distributed_switch.py} (81%) delete mode 100644 daemon/examples/python/distributed_switches.py delete mode 100644 daemon/examples/python/distributed_wlan.py diff --git a/daemon/examples/python/distributed_emane.py b/daemon/examples/python/distributed_emane.py index 4ef50ccb..74c7c93b 100644 --- a/daemon/examples/python/distributed_emane.py +++ b/daemon/examples/python/distributed_emane.py @@ -1,17 +1,13 @@ import logging -import pdb -import sys +import distributed_parser from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes, NodeTypes -def main(): - address = sys.argv[1] - remote = sys.argv[2] - +def main(args): # ip generator for example prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") @@ -20,14 +16,14 @@ def main(): { "controlnet": "core1:172.16.1.0/24 core2:172.16.2.0/24 core3:172.16.3.0/24 " "core4:172.16.4.0/24 core5:172.16.5.0/24", - "distributed_address": address, + "distributed_address": args.address, } ) session = coreemu.create_session() # initialize distributed server_name = "core2" - session.distributed.add_server(server_name, remote) + session.distributed.add_server(server_name, args.server) # must be in configuration state for nodes to start, when using "node_add" below session.set_state(EventTypes.CONFIGURATION_STATE) @@ -48,13 +44,10 @@ def main(): session.add_link(node_two.id, emane_net.id, interface_one=interface_two) # instantiate session - try: - session.instantiate() - except Exception: - logging.exception("error during instantiate") + session.instantiate() # pause script for verification - pdb.set_trace() + input("press enter for shutdown") # shutdown session coreemu.shutdown() @@ -62,4 +55,5 @@ def main(): if __name__ == "__main__": logging.basicConfig(level=logging.INFO) - main() + args = distributed_parser.parse(__file__) + main(args) diff --git a/daemon/examples/python/distributed_lxd.py b/daemon/examples/python/distributed_lxd.py index 130942ea..80366a14 100644 --- a/daemon/examples/python/distributed_lxd.py +++ b/daemon/examples/python/distributed_lxd.py @@ -1,26 +1,22 @@ import logging -import pdb -import sys +import distributed_parser from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes, NodeTypes -def main(): - address = sys.argv[1] - remote = sys.argv[2] - +def main(args): # ip generator for example prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") # create emulator instance for creating sessions and utility methods - coreemu = CoreEmu({"distributed_address": address}) + coreemu = CoreEmu({"distributed_address": args.address}) session = coreemu.create_session() # initialize distributed server_name = "core2" - session.distributed.add_server(server_name, remote) + session.distributed.add_server(server_name, args.server) # must be in configuration state for nodes to start, when using "node_add" below session.set_state(EventTypes.CONFIGURATION_STATE) @@ -40,7 +36,7 @@ def main(): session.instantiate() # pause script for verification - pdb.set_trace() + input("press enter for shutdown") # shutdown session coreemu.shutdown() @@ -48,4 +44,5 @@ def main(): if __name__ == "__main__": logging.basicConfig(level=logging.INFO) - main() + args = distributed_parser.parse(__file__) + main(args) diff --git a/daemon/examples/python/distributed_parser.py b/daemon/examples/python/distributed_parser.py new file mode 100644 index 00000000..5557efd8 --- /dev/null +++ b/daemon/examples/python/distributed_parser.py @@ -0,0 +1,15 @@ +import argparse + + +def parse(name): + parser = argparse.ArgumentParser(description=f"Run {name} example") + parser.add_argument( + "-a", + "--address", + help="local address that distributed servers will use for gre tunneling", + ) + parser.add_argument( + "-s", "--server", help="distributed server to use for creating nodes" + ) + options = parser.parse_args() + return options diff --git a/daemon/examples/python/distributed_ptp.py b/daemon/examples/python/distributed_ptp.py index 62e7df64..887fdae4 100644 --- a/daemon/examples/python/distributed_ptp.py +++ b/daemon/examples/python/distributed_ptp.py @@ -1,26 +1,22 @@ import logging -import pdb -import sys +import distributed_parser from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes -def main(): - address = sys.argv[1] - remote = sys.argv[2] - +def main(args): # ip generator for example prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") # create emulator instance for creating sessions and utility methods - coreemu = CoreEmu({"distributed_address": address}) + coreemu = CoreEmu({"distributed_address": args.address}) session = coreemu.create_session() # initialize distributed server_name = "core2" - session.distributed.add_server(server_name, remote) + session.distributed.add_server(server_name, args.server) # must be in configuration state for nodes to start, when using "node_add" below session.set_state(EventTypes.CONFIGURATION_STATE) @@ -40,7 +36,7 @@ def main(): session.instantiate() # pause script for verification - pdb.set_trace() + input("press enter for shutdown") # shutdown session coreemu.shutdown() @@ -48,4 +44,5 @@ def main(): if __name__ == "__main__": logging.basicConfig(level=logging.INFO) - main() + args = distributed_parser.parse(__file__) + main(args) diff --git a/daemon/examples/python/distributed.py b/daemon/examples/python/distributed_switch.py similarity index 81% rename from daemon/examples/python/distributed.py rename to daemon/examples/python/distributed_switch.py index 8eb23b2c..e87cd2c9 100644 --- a/daemon/examples/python/distributed.py +++ b/daemon/examples/python/distributed_switch.py @@ -1,26 +1,24 @@ import logging -import pdb -import sys +import distributed_parser from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes, NodeTypes -def main(): - address = sys.argv[1] - remote = sys.argv[2] - +def main(args): # ip generator for example prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") # create emulator instance for creating sessions and utility methods - coreemu = CoreEmu({"controlnet": "172.16.0.0/24", "distributed_address": address}) + coreemu = CoreEmu( + {"controlnet": "172.16.0.0/24", "distributed_address": args.address} + ) session = coreemu.create_session() # initialize distributed server_name = "core2" - session.distributed.add_server(server_name, remote) + session.distributed.add_server(server_name, args.server) # must be in configuration state for nodes to start, when using "node_add" below session.set_state(EventTypes.CONFIGURATION_STATE) @@ -42,7 +40,7 @@ def main(): session.instantiate() # pause script for verification - pdb.set_trace() + input("press enter for shutdown") # shutdown session coreemu.shutdown() @@ -50,4 +48,5 @@ def main(): if __name__ == "__main__": logging.basicConfig(level=logging.INFO) - main() + args = distributed_parser.parse(__file__) + main(args) diff --git a/daemon/examples/python/distributed_switches.py b/daemon/examples/python/distributed_switches.py deleted file mode 100644 index f9b69757..00000000 --- a/daemon/examples/python/distributed_switches.py +++ /dev/null @@ -1,43 +0,0 @@ -import logging -import pdb -import sys - -from core.emulator.coreemu import CoreEmu -from core.emulator.enumerations import EventTypes, NodeTypes - - -def main(): - address = sys.argv[1] - remote = sys.argv[2] - - # create emulator instance for creating sessions and utility methods - coreemu = CoreEmu({"distributed_address": address}) - session = coreemu.create_session() - - # initialize distributed - server_name = "core2" - session.distributed.add_server(server_name, remote) - - # must be in configuration state for nodes to start, when using "node_add" below - session.set_state(EventTypes.CONFIGURATION_STATE) - - # create local node, switch, and remote nodes - switch_one = session.add_node(_type=NodeTypes.SWITCH) - switch_two = session.add_node(_type=NodeTypes.SWITCH) - - # create node interfaces and link - session.add_link(switch_one.id, switch_two.id) - - # instantiate session - session.instantiate() - - # pause script for verification - pdb.set_trace() - - # shutdown session - coreemu.shutdown() - - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - main() diff --git a/daemon/examples/python/distributed_wlan.py b/daemon/examples/python/distributed_wlan.py deleted file mode 100644 index 10f25aa8..00000000 --- a/daemon/examples/python/distributed_wlan.py +++ /dev/null @@ -1,56 +0,0 @@ -import logging -import pdb -import sys - -from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes, NodeOptions -from core.emulator.enumerations import EventTypes, NodeTypes -from core.location.mobility import BasicRangeModel - - -def main(): - address = sys.argv[1] - remote = sys.argv[2] - - # ip generator for example - prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") - - # create emulator instance for creating sessions and utility methods - coreemu = CoreEmu({"distributed_address": address}) - session = coreemu.create_session() - - # initialize distributed - server_name = "core2" - session.distributed.add_server(server_name, remote) - - # must be in configuration state for nodes to start, when using "node_add" below - session.set_state(EventTypes.CONFIGURATION_STATE) - - # create local node, switch, and remote nodes - options = NodeOptions() - options.set_position(0, 0) - options.emulation_server = server_name - node_one = session.add_node(node_options=options) - wlan = session.add_node(_type=NodeTypes.WIRELESS_LAN) - session.mobility.set_model(wlan, BasicRangeModel) - node_two = session.add_node(node_options=options) - - # create node interfaces and link - interface_one = prefixes.create_interface(node_one) - interface_two = prefixes.create_interface(node_two) - session.add_link(node_one.id, wlan.id, interface_one=interface_one) - session.add_link(node_two.id, wlan.id, interface_one=interface_two) - - # instantiate session - session.instantiate() - - # pause script for verification - pdb.set_trace() - - # shutdown session - coreemu.shutdown() - - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - main() diff --git a/daemon/examples/python/emane80211.py b/daemon/examples/python/emane80211.py index d77252da..75098398 100644 --- a/daemon/examples/python/emane80211.py +++ b/daemon/examples/python/emane80211.py @@ -1,7 +1,3 @@ -#!/usr/bin/python -i -# -# Example CORE Python script that attaches N nodes to an EMANE 802.11abg network. - import datetime import logging import parser diff --git a/daemon/examples/python/switch.py b/daemon/examples/python/switch.py index 252253f8..0d952fda 100644 --- a/daemon/examples/python/switch.py +++ b/daemon/examples/python/switch.py @@ -1,10 +1,3 @@ -#!/usr/bin/python -# -# run iperf to measure the effective throughput between two nodes when -# n nodes are connected to a virtual wlan; run test for testsec -# and repeat for minnodes <= n <= maxnodes with a step size of -# nodestep - import datetime import logging import parser diff --git a/daemon/examples/python/switch_inject.py b/daemon/examples/python/switch_inject.py index 1b7b634c..0a87afd2 100644 --- a/daemon/examples/python/switch_inject.py +++ b/daemon/examples/python/switch_inject.py @@ -1,9 +1,3 @@ -#!/usr/bin/python -# -# run iperf to measure the effective throughput between two nodes when -# n nodes are connected to a virtual wlan; run test for testsec -# and repeat for minnodes <= n <= maxnodes with a step size of -# nodestep import logging from core.emulator.emudata import IpPrefixes diff --git a/daemon/examples/python/wlan.py b/daemon/examples/python/wlan.py index 4b4bc724..1ef1a5d1 100644 --- a/daemon/examples/python/wlan.py +++ b/daemon/examples/python/wlan.py @@ -1,10 +1,3 @@ -#!/usr/bin/python -# -# run iperf to measure the effective throughput between two nodes when -# n nodes are connected to a virtual wlan; run test for testsec -# and repeat for minnodes <= n <= maxnodes with a step size of -# nodestep - import datetime import logging import parser From 233ca92fd2cdebbabe0896ae4dac81a0d1e8cb24 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 21 Oct 2019 12:51:38 -0700 Subject: [PATCH 093/462] update grpc to allow for configuring and created distributed nodes --- daemon/Pipfile.lock | 70 +++++++++--------- daemon/core/api/grpc/client.py | 16 ++++ daemon/core/api/grpc/server.py | 16 ++++ daemon/examples/grpc/distributed_switch.py | 86 ++++++++++++++++++++++ daemon/proto/core/api/grpc/core.proto | 13 ++++ 5 files changed, 167 insertions(+), 34 deletions(-) create mode 100644 daemon/examples/grpc/distributed_switch.py diff --git a/daemon/Pipfile.lock b/daemon/Pipfile.lock index 4bdaea3f..5a19aae7 100644 --- a/daemon/Pipfile.lock +++ b/daemon/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "6195c89ec6e2e449fcbd7f3fa41cbab79c02d952984a913e0f80114e1904bf11" + "sha256": "d702e6eed5a1362bf261543572bbffd2e8a87140b8d8cb07b99fb0d25220a2b5" }, "pipfile-spec": 6, "requires": {}, @@ -20,6 +20,7 @@ "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42", "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294", "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161", + "sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752", "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31", "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5", "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c", @@ -30,6 +31,7 @@ "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09", "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105", "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133", + "sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1", "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7", "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc" ], @@ -37,40 +39,38 @@ }, "cffi": { "hashes": [ - "sha256:08f99e8b38d5134d504aa7e486af8e4fde66a2f388bbecc270cdd1e00fa09ff8", - "sha256:1112d2fc92a867a6103bce6740a549e74b1d320cf28875609f6e93857eee4f2d", - "sha256:1b9ab50c74e075bd2ae489853c5f7f592160b379df53b7f72befcbe145475a36", - "sha256:24eff2997436b6156c2f30bed215c782b1d8fd8c6a704206053c79af95962e45", - "sha256:2eff642fbc9877a6449026ad66bf37c73bf4232505fb557168ba5c502f95999b", - "sha256:362e896cea1249ed5c2a81cf6477fabd9e1a5088aa7ea08358a4c6b0998294d2", - "sha256:40eddb3589f382cb950f2dcf1c39c9b8d7bd5af20665ce273815b0d24635008b", - "sha256:5ed40760976f6b8613d4a0db5e423673ca162d4ed6c9ed92d1f4e58a47ee01b5", - "sha256:632c6112c1e914c486f06cfe3f0cc507f44aa1e00ebf732cedb5719e6aa0466a", - "sha256:64d84f0145e181f4e6cc942088603c8db3ae23485c37eeda71cb3900b5e67cb4", - "sha256:6cb4edcf87d0e7f5bdc7e5c1a0756fbb37081b2181293c5fdf203347df1cd2a2", - "sha256:6f19c9df4785305669335b934c852133faed913c0faa63056248168966f7a7d5", - "sha256:719537b4c5cd5218f0f47826dd705fb7a21d83824920088c4214794457113f3f", - "sha256:7b0e337a70e58f1a36fb483fd63880c9e74f1db5c532b4082bceac83df1523fa", - "sha256:853376efeeb8a4ae49a737d5d30f5db8cdf01d9319695719c4af126488df5a6a", - "sha256:85bbf77ffd12985d76a69d2feb449e35ecdcb4fc54a5f087d2bd54158ae5bb0c", - "sha256:8978115c6f0b0ce5880bc21c967c65058be8a15f1b81aa5fdbdcbea0e03952d1", - "sha256:8f7eec920bc83692231d7306b3e311586c2e340db2dc734c43c37fbf9c981d24", - "sha256:8fe230f612c18af1df6f348d02d682fe2c28ca0a6c3856c99599cdacae7cf226", - "sha256:92068ebc494b5f9826b822cec6569f1f47b9a446a3fef477e1d11d7fac9ea895", - "sha256:b57e1c8bcdd7340e9c9d09613b5e7fdd0c600be142f04e2cc1cc8cb7c0b43529", - "sha256:ba956c9b44646bc1852db715b4a252e52a8f5a4009b57f1dac48ba3203a7bde1", - "sha256:ca42034c11eb447497ea0e7b855d87ccc2aebc1e253c22e7d276b8599c112a27", - "sha256:dc9b2003e9a62bbe0c84a04c61b0329e86fccd85134a78d7aca373bbbf788165", - "sha256:dd308802beb4b2961af8f037becbdf01a1e85009fdfc14088614c1b3c383fae5", - "sha256:e77cd105b19b8cd721d101687fcf665fd1553eb7b57556a1ef0d453b6fc42faa", - "sha256:f56dff1bd81022f1c980754ec721fb8da56192b026f17f0f99b965da5ab4fbd2", - "sha256:fa4cc13c03ea1d0d37ce8528e0ecc988d2365e8ac64d8d86cafab4038cb4ce89", - "sha256:fa8cf1cb974a9f5911d2a0303f6adc40625c05578d8e7ff5d313e1e27850bd59", - "sha256:fb003019f06d5fc0aa4738492ad8df1fa343b8a37cbcf634018ad78575d185df", - "sha256:fd409b7778167c3bcc836484a8f49c0e0b93d3e745d975749f83aa5d18a5822f", - "sha256:fe5d65a3ee38122003245a82303d11ac05ff36531a8f5ce4bc7d4bbc012797e1" + "sha256:00d890313797d9fe4420506613384b43099ad7d2b905c0752dbcc3a6f14d80fa", + "sha256:0cf9e550ac6c5e57b713437e2f4ac2d7fd0cd10336525a27224f5fc1ec2ee59a", + "sha256:0ea23c9c0cdd6778146a50d867d6405693ac3b80a68829966c98dd5e1bbae400", + "sha256:193697c2918ecdb3865acf6557cddf5076bb39f1f654975e087b67efdff83365", + "sha256:1ae14b542bf3b35e5229439c35653d2ef7d8316c1fffb980f9b7647e544baa98", + "sha256:1e389e069450609c6ffa37f21f40cce36f9be7643bbe5051ab1de99d5a779526", + "sha256:263242b6ace7f9cd4ea401428d2d45066b49a700852334fd55311bde36dcda14", + "sha256:33142ae9807665fa6511cfa9857132b2c3ee6ddffb012b3f0933fc11e1e830d5", + "sha256:364f8404034ae1b232335d8c7f7b57deac566f148f7222cef78cf8ae28ef764e", + "sha256:47368f69fe6529f8f49a5d146ddee713fc9057e31d61e8b6dc86a6a5e38cecc1", + "sha256:4895640844f17bec32943995dc8c96989226974dfeb9dd121cc45d36e0d0c434", + "sha256:558b3afef987cf4b17abd849e7bedf64ee12b28175d564d05b628a0f9355599b", + "sha256:5ba86e1d80d458b338bda676fd9f9d68cb4e7a03819632969cf6d46b01a26730", + "sha256:63424daa6955e6b4c70dc2755897f5be1d719eabe71b2625948b222775ed5c43", + "sha256:6381a7d8b1ebd0bc27c3bc85bc1bfadbb6e6f756b4d4db0aa1425c3719ba26b4", + "sha256:6381ab708158c4e1639da1f2a7679a9bbe3e5a776fc6d1fd808076f0e3145331", + "sha256:6fd58366747debfa5e6163ada468a90788411f10c92597d3b0a912d07e580c36", + "sha256:728ec653964655d65408949b07f9b2219df78badd601d6c49e28d604efe40599", + "sha256:7cfcfda59ef1f95b9f729c56fe8a4041899f96b72685d36ef16a3440a0f85da8", + "sha256:819f8d5197c2684524637f940445c06e003c4a541f9983fd30d6deaa2a5487d8", + "sha256:825ecffd9574557590e3225560a8a9d751f6ffe4a49e3c40918c9969b93395fa", + "sha256:9009e917d8f5ef780c2626e29b6bc126f4cb2a4d43ca67aa2b40f2a5d6385e78", + "sha256:9c77564a51d4d914ed5af096cd9843d90c45b784b511723bd46a8a9d09cf16fc", + "sha256:a19089fa74ed19c4fe96502a291cfdb89223a9705b1d73b3005df4256976142e", + "sha256:a40ed527bffa2b7ebe07acc5a3f782da072e262ca994b4f2085100b5a444bbb2", + "sha256:bb75ba21d5716abc41af16eac1145ab2e471deedde1f22c6f99bd9f995504df0", + "sha256:e22a00c0c81ffcecaf07c2bfb3672fa372c50e2bd1024ffee0da191c1b27fc71", + "sha256:e55b5a746fb77f10c83e8af081979351722f6ea48facea79d470b3731c7b2891", + "sha256:ec2fa3ee81707a5232bf2dfbd6623fdb278e070d596effc7e2d788f2ada71a05", + "sha256:fd82eb4694be712fcae03c717ca2e0fc720657ac226b80bbb597e971fc6928c2" ], - "version": "==1.13.0" + "version": "==1.13.1" }, "core": { "editable": true, @@ -226,6 +226,7 @@ "sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39", "sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310", "sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1", + "sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5", "sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a", "sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786", "sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b", @@ -234,6 +235,7 @@ "sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20", "sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415", "sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715", + "sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92", "sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1", "sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0" ], diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 54f77fc6..f14bd064 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -276,6 +276,22 @@ class CoreGrpcClient(object): request = core_pb2.SetSessionStateRequest(session_id=session_id, state=state) return self.stub.SetSessionState(request) + def add_session_server(self, session_id, name, host): + """ + Add distributed session server. + + :param int session_id: id of session + :param str name: name of server to add + :param str host: host address to connect to + :return: response with result of success or failure + :rtype: core_pb2.AddSessionServerResponse + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.AddSessionServerRequest( + session_id=session_id, name=name, host=host + ) + return self.stub.AddSessionServer(request) + def events(self, session_id, handler): """ Listen for session events. diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 0ab4806a..a92a72e5 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -473,6 +473,20 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session_proto = core_pb2.Session(state=session.state, nodes=nodes, links=links) return core_pb2.GetSessionResponse(session=session_proto) + def AddSessionServer(self, request, context): + """ + Add distributed server to a session. + + :param core.api.grpc.core_pb2.AddSessionServerRequest request: get-session + request + :param grpc.ServicerContext context: context object + :return: add session server response + :rtype: core.api.grpc.core_bp2.AddSessionServerResponse + """ + session = self.get_session(request.session_id, context) + session.distributed.add_server(request.name, request.host) + return core_pb2.AddSessionServerResponse(result=True) + def Events(self, request, context): session = self.get_session(request.session_id, context) queue = Queue() @@ -761,6 +775,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): node_options.opaque = node_proto.opaque node_options.image = node_proto.image node_options.services = node_proto.services + if node_proto.server: + node_options.emulation_server = node_proto.server position = node_proto.position node_options.set_position(position.x, position.y) diff --git a/daemon/examples/grpc/distributed_switch.py b/daemon/examples/grpc/distributed_switch.py new file mode 100644 index 00000000..9cc35f72 --- /dev/null +++ b/daemon/examples/grpc/distributed_switch.py @@ -0,0 +1,86 @@ +import argparse +import logging + +from core.api.grpc import client, core_pb2 + + +def log_event(event): + logging.info("event: %s", event) + + +def main(args): + core = client.CoreGrpcClient() + + with core.context_connect(): + # create session + response = core.create_session() + session_id = response.session_id + logging.info("created session: %s", response) + + # add distributed server + server_name = "core2" + response = core.add_session_server(session_id, server_name, args.server) + logging.info("added session server: %s", response) + + # handle events session may broadcast + core.events(session_id, log_event) + + # change session state + response = core.set_session_state( + session_id, core_pb2.SessionState.CONFIGURATION + ) + logging.info("set session state: %s", response) + + # create switch node + switch = core_pb2.Node(type=core_pb2.NodeType.SWITCH) + response = core.add_node(session_id, switch) + logging.info("created switch: %s", response) + switch_id = response.node_id + + # helper to create interfaces + interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/16") + + # create node one + position = core_pb2.Position(x=100, y=50) + node = core_pb2.Node(position=position) + response = core.add_node(session_id, node) + logging.info("created node one: %s", response) + node_one_id = response.node_id + + # create link + interface_one = interface_helper.create_interface(node_one_id, 0) + response = core.add_link(session_id, node_one_id, switch_id, interface_one) + logging.info("created link from node one to switch: %s", response) + + # create node two + position = core_pb2.Position(x=200, y=50) + node = core_pb2.Node(position=position, server=server_name) + response = core.add_node(session_id, node) + logging.info("created node two: %s", response) + node_two_id = response.node_id + + # create link + interface_one = interface_helper.create_interface(node_two_id, 0) + response = core.add_link(session_id, node_two_id, switch_id, interface_one) + logging.info("created link from node two to switch: %s", response) + + # change session state + response = core.set_session_state( + session_id, core_pb2.SessionState.INSTANTIATION + ) + logging.info("set session state: %s", response) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + parser = argparse.ArgumentParser(description="Run distributed_switch example") + parser.add_argument( + "-a", + "--address", + help="local address that distributed servers will use for gre tunneling", + ) + parser.add_argument( + "-s", "--server", help="distributed server to use for creating nodes" + ) + args = parser.parse_args() + main(args) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 1e17b327..d5528b52 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -25,6 +25,8 @@ service CoreApi { } rpc SetSessionState (SetSessionStateRequest) returns (SetSessionStateResponse) { } + rpc AddSessionServer (AddSessionServerRequest) returns (AddSessionServerResponse) { + } // streams rpc Events (EventsRequest) returns (stream Event) { @@ -201,6 +203,16 @@ message SetSessionStateResponse { bool result = 1; } +message AddSessionServerRequest { + int32 session_id = 1; + string name = 2; + string host = 3; +} + +message AddSessionServerResponse { + bool result = 1; +} + message EventsRequest { int32 session_id = 1; } @@ -802,6 +814,7 @@ message Node { string icon = 8; string opaque = 9; string image = 10; + string server = 11; } message Link { From f18d07985ad1abcc55a1d06e77e6d5d4aec8a5b9 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 21 Oct 2019 16:33:18 -0700 Subject: [PATCH 094/462] more work on coretk --- coretk/coretk/app.py | 2 + coretk/coretk/coregrpc.py | 12 ++- coretk/coretk/graph.py | 9 +- coretk/coretk/grpcmanagement.py | 26 ++++- coretk/coretk/interface.py | 57 +++++++++- coretk/coretk/nodeconfigtable.py | 151 +++++++++++++++++++++++++++ coretk/coretk/querysessiondrawing.py | 9 ++ 7 files changed, 256 insertions(+), 10 deletions(-) create mode 100644 coretk/coretk/nodeconfigtable.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index e4f632a7..ec22f2fd 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -93,6 +93,8 @@ class Application(tk.Frame): self.core_grpc = CoreGrpc(self) self.core_grpc.set_up() self.canvas.core_grpc = self.core_grpc + self.canvas.grpc_manager.core_grpc = self.core_grpc + self.canvas.grpc_manager.update_preexisting_ids() self.canvas.draw_existing_component() def on_closing(self): diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 095169ed..3d2955dc 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -18,6 +18,9 @@ class CoreGrpc: """ self.core = client.CoreGrpcClient() self.session_id = sid + + self.node_ids = [] + self.master = app.master # self.set_up() @@ -32,8 +35,14 @@ class CoreGrpc: def log_throughput(self, event): interface_throughputs = event.interface_throughputs + throughputs_belong_to_session = [] + for if_tp in interface_throughputs: + if if_tp.node_id in self.node_ids: + throughputs_belong_to_session.append(if_tp) # bridge_throughputs = event.bridge_throughputs - self.throughput_draw.process_grpc_throughput_event(interface_throughputs) + self.throughput_draw.process_grpc_throughput_event( + throughputs_belong_to_session + ) def create_new_session(self): """ @@ -147,6 +156,7 @@ class CoreGrpc: def add_node(self, node_type, model, x, y, name, node_id): position = core_pb2.Position(x=x, y=y) node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) + self.node_ids.append(node_id) response = self.core.add_node(self.session_id, node) logging.info("created node: %s", response) if node_type == core_pb2.NodeType.WIRELESS_LAN: diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 7793880f..d88524bc 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -8,6 +8,7 @@ from coretk.grpcmanagement import GrpcManager from coretk.images import Images from coretk.interface import Interface from coretk.linkinfo import LinkInfo +from coretk.nodeconfigtable import NodeConfig class GraphMode(enum.Enum): @@ -37,8 +38,8 @@ class CanvasGraph(tk.Canvas): self.setup_bindings() self.draw_grid() - self.grpc_manager = GrpcManager() self.core_grpc = grpc + self.grpc_manager = GrpcManager(grpc) self.helper = GraphHelper(self) # self.core_id_to_canvas_id = {} @@ -69,7 +70,8 @@ class CanvasGraph(tk.Canvas): self.nodes = {} self.edges = {} self.drawing_edge = None - self.grpc_manager = GrpcManager() + + self.grpc_manager = GrpcManager(new_grpc) # new grpc self.core_grpc = new_grpc @@ -463,6 +465,9 @@ class CanvasNode: state = self.canvas.core_grpc.get_session_state() if state == core_pb2.SessionState.RUNTIME: self.canvas.core_grpc.launch_terminal(node_id) + else: + print("config table show up") + NodeConfig(self, self.image, self.node_type, self.name) def update_coords(self): self.x_coord, self.y_coord = self.canvas.coords(self.id) diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index 20f1a74a..2d5a9027 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -54,14 +54,17 @@ class Edge: class GrpcManager: - def __init__(self): + def __init__(self, grpc): self.nodes = {} self.edges = {} - self.id = 1 + self.id = None # A list of id for re-use, keep in increasing order self.reusable = [] self.preexisting = [] + self.core_grpc = None + + # self.update_preexisting_ids() # self.core_id_to_canvas_id = {} self.interfaces_manager = InterfaceManager() @@ -69,6 +72,23 @@ class GrpcManager: # self.node_id_and_interface_to_edge_token = {} self.core_mapping = CoreToCanvasMapping() + def update_preexisting_ids(self): + """ + get preexisting node ids + :return: + """ + max_id = 0 + client = self.core_grpc.core + sessions = client.get_sessions().sessions + for session_summary in sessions: + session = client.get_session(session_summary.id).session + for node in session.nodes: + if node.id > max_id: + max_id = node.id + self.preexisting.append(node.id) + self.id = max_id + 1 + self.update_reusable_id() + def peek_id(self): """ Peek the next id to be used @@ -211,6 +231,8 @@ class GrpcManager: src_interface = None dst_interface = None + self.interfaces_manager.new_subnet() + src_node = self.nodes[src_canvas_id] if src_node.model in network_layer_nodes: ifid = len(src_node.interfaces) diff --git a/coretk/coretk/interface.py b/coretk/coretk/interface.py index 4bad6e99..94911745 100644 --- a/coretk/coretk/interface.py +++ b/coretk/coretk/interface.py @@ -32,10 +32,39 @@ class Interface: ) +class SubnetAddresses: + def __init__(self, network, addresses): + self.network = network + self.address = addresses + self.address_index = 0 + + def get_new_ip_address(self): + ipaddr = self.address[self.address_index] + self.address_index = self.address_index + 1 + return ipaddr + + class InterfaceManager: def __init__(self): - self.addresses = list(ipaddress.ip_network("10.0.0.0/24").hosts()) - self.index = 0 + # self.prefix = None + self.core_subnets = list( + ipaddress.ip_network("10.0.0.0/12").subnets(prefixlen_diff=12) + ) + self.subnet_index = 0 + self.address_index = None + + # self.network = ipaddress.ip_network("10.0.0.0/24") + # self.addresses = list(self.network.hosts()) + self.network = None + self.addresses = None + # self.start_interface_manager() + + def start_interface_manager(self): + self.subnet_index = 0 + self.network = self.core_subnets[self.subnet_index] + self.subnet_index = self.subnet_index + 1 + self.addresses = list(self.network.hosts()) + self.address_index = 0 def get_address(self): """ @@ -43,6 +72,24 @@ class InterfaceManager: :return: """ - i = self.index - self.index = self.index + 1 - return self.addresses[i] + # i = self.index + # self.address_index = self.index + 1 + # return self.addresses[i] + ipaddr = self.addresses[self.address_index] + self.address_index = self.address_index + 1 + return ipaddr + + def new_subnet(self): + self.network = self.core_subnets[self.subnet_index] + self.subnet_index = self.subnet_index + 1 + self.addresses = list(self.network.hosts()) + self.address_index = 0 + + # def new_subnet(self): + # """ + # retrieve a new subnet + # :return: + # """ + # if self.prefix is None: + # self.prefix = + # self.addresses = list(ipaddress.ip_network("10.0.0.0/24").hosts()) diff --git a/coretk/coretk/nodeconfigtable.py b/coretk/coretk/nodeconfigtable.py new file mode 100644 index 00000000..51b8314d --- /dev/null +++ b/coretk/coretk/nodeconfigtable.py @@ -0,0 +1,151 @@ +""" +Create toplevel for node configuration +""" +import logging +import os +import tkinter as tk +from tkinter import filedialog + +from PIL import Image, ImageTk + +PATH = os.path.abspath(os.path.dirname(__file__)) +ICONS_DIR = os.path.join(PATH, "icons") + +NETWORKNODETYPES = ["switch", "hub", "wlan", "rj45", "tunnel"] +DEFAULTNODES = ["router", "host", "PC"] + + +class NodeConfig: + def __init__(self, canvas_node, image, node_type, name): + self.image = image + self.node_type = node_type + self.name = name + self.canvas_node = canvas_node + + self.top = tk.Toplevel() + self.top.title(node_type + " configuration") + self.namevar = tk.StringVar(self.top, value="default name") + self.name_and_image_definition() + self.type_and_service_definition() + self.select_definition() + + def open_icon_dir(self, toplevel, entry_text): + imgfile = filedialog.askopenfilename( + initialdir=ICONS_DIR, + title="Open", + filetypes=( + ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), + ("All Files", "*"), + ), + ) + if len(imgfile) > 0: + img = Image.open(imgfile) + tk_img = ImageTk.PhotoImage(img) + lb = toplevel.grid_slaves(1, 0)[0] + lb.configure(image=tk_img) + lb.image = tk_img + entry_text.set(imgfile) + + def click_apply(self, toplevel, entry_text): + print("click apply") + imgfile = entry_text.get() + if imgfile: + img = Image.open(imgfile) + tk_img = ImageTk.PhotoImage(img) + lb = self.top.grid_slaves(row=0, column=3)[0] + lb.configure(image=tk_img) + lb.image = tk_img + self.image = tk_img + toplevel.destroy() + + def img_modification(self): + print("image modification") + t = tk.Toplevel() + t.title(self.name + " image") + + f = tk.Frame(t) + entry_text = tk.StringVar() + image_file_label = tk.Label(f, text="Image file: ") + image_file_label.pack(side=tk.LEFT, padx=2, pady=2) + image_file_entry = tk.Entry(f, textvariable=entry_text, width=60) + image_file_entry.pack(side=tk.LEFT, padx=2, pady=2) + image_file_button = tk.Button( + f, text="...", command=lambda: self.open_icon_dir(t, entry_text) + ) + image_file_button.pack(side=tk.LEFT, padx=2, pady=2) + f.grid(sticky=tk.W + tk.E) + + img = tk.Label(t, image=self.image) + img.grid(sticky=tk.W + tk.E) + + f = tk.Frame(t) + apply_button = tk.Button( + f, text="Apply", command=lambda: self.click_apply(t, entry_text) + ) + apply_button.pack(side=tk.LEFT, padx=2, pady=2) + apply_to_multiple_button = tk.Button(f, text="Apply to multiple...") + apply_to_multiple_button.pack(side=tk.LEFT, padx=2, pady=2) + cancel_button = tk.Button(f, text="Cancel", command=t.destroy) + cancel_button.pack(side=tk.LEFT, padx=2, pady=2) + f.grid(sticky=tk.E + tk.W) + + def name_and_image_definition(self): + name_label = tk.Label(self.top, text="Node name: ") + name_label.grid() + name_entry = tk.Entry(self.top, textvariable=self.namevar) + name_entry.grid(row=0, column=1) + + core_button = tk.Button(self.top, text="None") + core_button.grid(row=0, column=2) + img_button = tk.Button( + self.top, + image=self.image, + width=40, + height=40, + command=self.img_modification, + ) + img_button.grid(row=0, column=3) + + def type_and_service_definition(self): + f = tk.Frame(self.top) + type_label = tk.Label(f, text="Type: ") + type_label.pack(side=tk.LEFT) + + type_button = tk.Button(f, text="None") + type_button.pack(side=tk.LEFT) + + service_button = tk.Button(f, text="Services...") + service_button.pack(side=tk.LEFT) + + f.grid(row=1, column=1, columnspan=2, sticky=tk.W) + + def config_apply(self): + """ + modify image of the canvas node + :return: nothing + """ + logging.debug("nodeconfigtable.py configuration apply") + self.canvas_node.image = self.image + self.canvas_node.canvas.itemconfig(self.canvas_node.id, image=self.image) + self.top.destroy() + + def config_cancel(self): + """ + save chosen image but not modify canvas node + :return: nothing + """ + logging.debug("nodeconfigtable.py configuration cancel") + self.canvas_node.image = self.image + self.top.destroy() + + def select_definition(self): + f = tk.Frame(self.top) + apply_button = tk.Button(f, text="Apply", command=self.config_apply) + apply_button.pack(side=tk.LEFT) + cancel_button = tk.Button(f, text="Cancel", command=self.config_cancel) + cancel_button.pack(side=tk.LEFT) + f.grid(row=3, column=1, sticky=tk.W) + + def network_node_config(self): + self.name_and_image_definition() + self.select_definition() diff --git a/coretk/coretk/querysessiondrawing.py b/coretk/coretk/querysessiondrawing.py index 639b4ba5..84786ba5 100644 --- a/coretk/coretk/querysessiondrawing.py +++ b/coretk/coretk/querysessiondrawing.py @@ -7,6 +7,11 @@ from coretk.images import ImageEnum, Images class SessionTable: def __init__(self, grpc, master): + """ + create session table instance + :param coretk.coregrpc.CoreGrpc grpc: coregrpc + :param root.master master: + """ self.grpc = grpc self.selected = False self.selected_sid = None @@ -22,6 +27,10 @@ class SessionTable: self.draw() def description_definition(self): + """ + write a short description + :return: nothing + """ lable = tk.Label( self.top, text="Below is a list of active CORE sessions. Double-click to " From 0a689a3e9650051bf4b729401cad0f311168b06b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 22 Oct 2019 09:57:41 -0700 Subject: [PATCH 095/462] updates to grpc to provide a consistent config response, mapping config ids to ConfigOptions --- daemon/core/api/grpc/client.py | 14 ++- daemon/core/api/grpc/server.py | 141 ++++++++++++++------------ daemon/proto/core/api/grpc/core.proto | 23 ++--- daemon/tests/test_grpc.py | 15 ++- 4 files changed, 109 insertions(+), 84 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index f14bd064..cb24d979 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -194,7 +194,7 @@ class CoreGrpcClient(object): def get_session_options(self, session_id): """ - Retrieve session options. + Retrieve session options as a dict with id mapping. :param int session_id: id of session :return: response with a list of configuration groups @@ -204,6 +204,18 @@ class CoreGrpcClient(object): request = core_pb2.GetSessionOptionsRequest(session_id=session_id) return self.stub.GetSessionOptions(request) + def get_session_options_group(self, session_id): + """ + Retrieve session options in a group list. + + :param int session_id: id of session + :return: response with a list of configuration groups + :rtype: core_pb2.GetSessionOptionsGroupResponse + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.GetSessionOptionsGroupRequest(session_id=session_id) + return self.stub.GetSessionOptionsGroup(request) + def set_session_options(self, session_id, config): """ Set options for a session. diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index a92a72e5..85838827 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -46,38 +46,33 @@ def convert_value(value): return value -def get_config_groups(config, configurable_options): +def get_config_options(config, configurable_options): """ - Retrieve configuration groups in a form that is used by the grpc server + Retrieve configuration options in a form that is used by the grpc server. - :param core.config.Configuration config: configuration + :param dict config: configuration :param core.config.ConfigurableOptions configurable_options: configurable options - :return: list of configuration groups - :rtype: [core.api.grpc.core_pb2.ConfigGroup] + :return: mapping of configuration ids to configuration options + :rtype: dict[str,core.api.grpc.core_pb2.ConfigOption] """ - groups = [] - config_options = [] - + results = {} for configuration in configurable_options.configurations(): value = config[configuration.id] - config_option = core_pb2.ConfigOption() - config_option.label = configuration.label - config_option.name = configuration.id - config_option.value = value - config_option.type = configuration.type.value - config_option.select.extend(configuration.options) - config_options.append(config_option) - + config_option = core_pb2.ConfigOption( + label=configuration.label, + name=configuration.id, + value=value, + type=configuration.type.value, + select=configuration.options, + ) + results[configuration.id] = config_option for config_group in configurable_options.config_groups(): start = config_group.start - 1 stop = config_group.stop - options = config_options[start:stop] - config_group_proto = core_pb2.ConfigGroup( - name=config_group.name, options=options - ) - groups.append(config_group_proto) - - return groups + options = list(results.values())[start:stop] + for option in options: + option.group = config_group.name + return results def get_links(session, node): @@ -390,20 +385,21 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): def GetSessionOptions(self, request, context): """ - Retrieve session options + Retrieve session options. - :param core.api.grpc.core_pb2.GetSessionOptions request: get-session-options request + :param core.api.grpc.core_pb2.GetSessionOptions request: + get-session-options request :param grpc.ServicerContext context: context object :return: get-session-options response about all session's options :rtype: core.api.grpc.core_pb2.GetSessionOptions """ logging.debug("get session options: %s", request) session = self.get_session(request.session_id, context) - config = session.options.get_configs() - defaults = session.options.default_values() - defaults.update(config) - groups = get_config_groups(defaults, session.options) - return core_pb2.GetSessionOptionsResponse(groups=groups) + current_config = session.options.get_configs() + default_config = session.options.default_values() + default_config.update(current_config) + config = get_config_options(default_config, session.options) + return core_pb2.GetSessionOptionsResponse(config=config) def SetSessionOptions(self, request, context): """ @@ -1115,7 +1111,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ Retrieve all mobility configurations from a session - :param core.api.grpc.core_pb2.GetMobilityConfigsRequest request: get-mobility-configurations request + :param core.api.grpc.core_pb2.GetMobilityConfigsRequest request: + get-mobility-configurations request :param grpc.ServicerContext context: context object :return: get-mobility-configurations response that has a list of configurations :rtype: core.api.grpc.core_pb2.GetMobilityConfigsResponse @@ -1130,33 +1127,36 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): for model_name in model_config: if model_name != Ns2ScriptedMobility.name: continue - config = session.mobility.get_model_config(node_id, model_name) - groups = get_config_groups(config, Ns2ScriptedMobility) - response.configs[node_id].groups.extend(groups) + current_config = session.mobility.get_model_config(node_id, model_name) + config = get_config_options(current_config, Ns2ScriptedMobility) + mapped_config = core_pb2.MappedConfig(config=config) + response.configs[node_id].CopyFrom(mapped_config) return response def GetMobilityConfig(self, request, context): """ Retrieve mobility configuration of a node - :param core.api.grpc.core_pb2.GetMobilityConfigRequest request: get-mobility-configuration request + :param core.api.grpc.core_pb2.GetMobilityConfigRequest request: + get-mobility-configuration request :param grpc.ServicerContext context: context object :return: get-mobility-configuration response :rtype: core.api.grpc.core_pb2.GetMobilityConfigResponse """ logging.debug("get mobility config: %s", request) session = self.get_session(request.session_id, context) - config = session.mobility.get_model_config( + current_config = session.mobility.get_model_config( request.node_id, Ns2ScriptedMobility.name ) - groups = get_config_groups(config, Ns2ScriptedMobility) - return core_pb2.GetMobilityConfigResponse(groups=groups) + config = get_config_options(current_config, Ns2ScriptedMobility) + return core_pb2.GetMobilityConfigResponse(config=config) def SetMobilityConfig(self, request, context): """ Set mobility configuration of a node - :param core.api.grpc.core_pb2.SetMobilityConfigRequest request: set-mobility-configuration request + :param core.api.grpc.core_pb2.SetMobilityConfigRequest request: + set-mobility-configuration request :param grpc.ServicerContext context: context object :return: set-mobility-configuration response "rtype" core.api.grpc.SetMobilityConfigResponse @@ -1172,7 +1172,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ Take mobility action whether to start, pause, stop or none of those - :param core.api.grpc.core_pb2.MobilityActionRequest request: mobility-action request + :param core.api.grpc.core_pb2.MobilityActionRequest request: mobility-action + request :param grpc.ServicerContext context: context object :return: mobility-action response :rtype: core.api.grpc.core_pb2.MobilityActionResponse @@ -1212,7 +1213,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ Retrieve all the default services of all node types in a session - :param core.api.grpc.core_pb2.GetServiceDefaultsRequest request: get-default-service request + :param core.api.grpc.core_pb2.GetServiceDefaultsRequest request: + get-default-service request :param grpc.ServicerContext context: context object :return: get-service-defaults response about all the available default services :rtype: core.api.grpc.core_pb2.GetServiceDefaultsResponse @@ -1231,7 +1233,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): def SetServiceDefaults(self, request, context): """ Set new default services to the session after whipping out the old ones - :param core.api.grpc.core_pb2.SetServiceDefaults request: set-service-defaults request + :param core.api.grpc.core_pb2.SetServiceDefaults request: set-service-defaults + request :param grpc.ServicerContext context: context object :return: set-service-defaults response :rtype: core.api.grpc.core_pb2 SetServiceDefaultsResponse @@ -1249,7 +1252,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ Retrieve a requested service from a node - :param core.api.grpc.core_pb2.GetNodeServiceRequest request: get-node-service request + :param core.api.grpc.core_pb2.GetNodeServiceRequest request: get-node-service + request :param grpc.ServicerContext context: context object :return: get-node-service response about the requested service :rtype: core.api.grpc.core_pb2.GetNodeServiceResponse @@ -1277,7 +1281,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ Retrieve a requested service file from a node - :param core.api.grpc.core_pb2.GetNodeServiceFileRequest request: get-node-service request + :param core.api.grpc.core_pb2.GetNodeServiceFileRequest request: + get-node-service request :param grpc.ServicerContext context: context object :return: get-node-service response about the requested service :rtype: core.api.grpc.core_pb2.GetNodeServiceFileResponse @@ -1301,8 +1306,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ Set a node service for a node - :param core.api.grpc.core_pb2.SetNodeServiceRequest request: set-node-service request - that has info to set a node service + :param core.api.grpc.core_pb2.SetNodeServiceRequest request: set-node-service + request that has info to set a node service :param grpc.ServicerContext context: context object :return: set-node-service response :rtype: core.api.grpc.core_pb2.SetNodeServiceResponse @@ -1320,7 +1325,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ Store the customized service file in the service config - :param core.api.grpc.core_pb2.SetNodeServiceFileRequest request: set-node-service-file request + :param core.api.grpc.core_pb2.SetNodeServiceFileRequest request: + set-node-service-file request :param grpc.ServicerContext context: context object :return: set-node-service-file response :rtype: core.api.grpc.core_pb2.SetNodeServiceFileResponse @@ -1382,11 +1388,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get wlan config: %s", request) session = self.get_session(request.session_id, context) - config = session.mobility.get_model_config( + current_config = session.mobility.get_model_config( request.node_id, BasicRangeModel.name ) - groups = get_config_groups(config, BasicRangeModel) - return core_pb2.GetWlanConfigResponse(groups=groups) + config = get_config_options(current_config, BasicRangeModel) + return core_pb2.GetWlanConfigResponse(config=config) def SetWlanConfig(self, request, context): """ @@ -1418,9 +1424,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get emane config: %s", request) session = self.get_session(request.session_id, context) - config = session.emane.get_configs() - groups = get_config_groups(config, session.emane.emane_config) - return core_pb2.GetEmaneConfigResponse(groups=groups) + current_config = session.emane.get_configs() + config = get_config_options(current_config, session.emane.emane_config) + return core_pb2.GetEmaneConfigResponse(config=config) def SetEmaneConfig(self, request, context): """ @@ -1459,7 +1465,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ Retrieve EMANE model configuration of a node - :param core.api.grpc.core_pb2.GetEmaneModelConfigRequest request: get-EMANE-model-configuration request + :param core.api.grpc.core_pb2.GetEmaneModelConfigRequest request: + get-EMANE-model-configuration request :param grpc.ServicerContext context: context object :return: get-EMANE-model-configuration response :rtype: core.api.grpc.core_pb2.GetEmaneModelConfigResponse @@ -1468,15 +1475,16 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session = self.get_session(request.session_id, context) model = session.emane.models[request.model] _id = get_emane_model_id(request.node_id, request.interface) - config = session.emane.get_model_config(_id, request.model) - groups = get_config_groups(config, model) - return core_pb2.GetEmaneModelConfigResponse(groups=groups) + current_config = session.emane.get_model_config(_id, request.model) + config = get_config_options(current_config, model) + return core_pb2.GetEmaneModelConfigResponse(config=config) def SetEmaneModelConfig(self, request, context): """ Set EMANE model configuration of a node - :param core.api.grpc.core_pb2.SetEmaneModelConfigRequest request: set-EMANE-model-configuration request + :param core.api.grpc.core_pb2.SetEmaneModelConfigRequest request: + set-EMANE-model-configuration request :param grpc.ServicerContext context: context object :return: set-EMANE-model-configuration response :rtype: core.api.grpc.core_pb2.SetEmaneModelConfigResponse @@ -1491,9 +1499,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ Retrieve all EMANE model configurations of a session - :param core.api.grpc.core_pb2.GetEmaneModelConfigsRequest request: get-EMANE-model-configurations request + :param core.api.grpc.core_pb2.GetEmaneModelConfigsRequest request: + get-EMANE-model-configurations request :param grpc.ServicerContext context: context object - :return: get-EMANE-model-configurations response that has all the EMANE configurations + :return: get-EMANE-model-configurations response that has all the EMANE + configurations :rtype: core.api.grpc.core_pb2.GetEmaneModelConfigsResponse """ logging.debug("get emane model configs: %s", request) @@ -1506,11 +1516,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): for model_name in model_config: model = session.emane.models[model_name] - config = session.emane.get_model_config(node_id, model_name) - config_groups = get_config_groups(config, model) - node_configurations = response.configs[node_id] - node_configurations.model = model_name - node_configurations.groups.extend(config_groups) + current_config = session.emane.get_model_config(node_id, model_name) + config = get_config_options(current_config, model) + model_config = core_pb2.GetEmaneModelConfigsResponse.ModelConfig( + model=model_name, config=config + ) + response.configs[node_id].CopyFrom(model_config) return response def SaveXml(self, request, context): diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index d5528b52..db43dec6 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -163,7 +163,7 @@ message GetSessionOptionsRequest { } message GetSessionOptionsResponse { - repeated ConfigGroup groups = 1; + map config = 2; } message SetSessionOptionsRequest { @@ -428,10 +428,7 @@ message GetMobilityConfigsRequest { } message GetMobilityConfigsResponse { - message MobilityConfig { - repeated ConfigGroup groups = 1; - } - map configs = 1; + map configs = 1; } message GetMobilityConfigRequest { @@ -440,7 +437,7 @@ message GetMobilityConfigRequest { } message GetMobilityConfigResponse { - repeated ConfigGroup groups = 1; + map config = 1; } message SetMobilityConfigRequest { @@ -551,7 +548,7 @@ message GetWlanConfigRequest { } message GetWlanConfigResponse { - repeated ConfigGroup groups = 1; + map config = 1; } message SetWlanConfigRequest { @@ -569,7 +566,7 @@ message GetEmaneConfigRequest { } message GetEmaneConfigResponse { - repeated ConfigGroup groups = 1; + map config = 1; } message SetEmaneConfigRequest { @@ -597,7 +594,7 @@ message GetEmaneModelConfigRequest { } message GetEmaneModelConfigResponse { - repeated ConfigGroup groups = 1; + map config = 1; } message SetEmaneModelConfigRequest { @@ -619,7 +616,7 @@ message GetEmaneModelConfigsRequest { message GetEmaneModelConfigsResponse { message ModelConfig { string model = 1; - repeated ConfigGroup groups = 2; + map config = 2; } map configs = 1; } @@ -777,9 +774,8 @@ message NodeServiceData { string meta = 10; } -message ConfigGroup { - string name = 1; - repeated ConfigOption options = 2; +message MappedConfig { + map config = 1; } message ConfigOption { @@ -788,6 +784,7 @@ message ConfigOption { string value = 3; int32 type = 4; repeated string select = 5; + string group = 6; } message Session { diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index b2ea73ca..cb837957 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -100,7 +100,7 @@ class TestGrpc: response = client.get_session_options(session.id) # then - assert len(response.groups) > 0 + assert len(response.config) > 0 def test_get_session_location(self, grpc_server): # given @@ -457,7 +457,7 @@ class TestGrpc: response = client.get_wlan_config(session.id, wlan.id) # then - assert len(response.groups) > 0 + assert len(response.config) > 0 def test_set_wlan_config(self, grpc_server): # given @@ -501,7 +501,7 @@ class TestGrpc: response = client.get_emane_config(session.id) # then - assert len(response.groups) > 0 + assert len(response.config) > 0 def test_set_emane_config(self, grpc_server): # given @@ -540,6 +540,9 @@ class TestGrpc: # then assert len(response.configs) == 1 assert emane_network.id in response.configs + model_config = response.configs[emane_network.id] + assert model_config.model == EmaneIeee80211abgModel.name + assert len(model_config.config) > 0 def test_set_emane_model_config(self, grpc_server): # given @@ -582,7 +585,7 @@ class TestGrpc: ) # then - assert len(response.groups) > 0 + assert len(response.config) > 0 def test_get_emane_models(self, grpc_server): # given @@ -610,6 +613,8 @@ class TestGrpc: # then assert len(response.configs) > 0 assert wlan.id in response.configs + mapped_config = response.configs[wlan.id] + assert len(mapped_config.config) > 0 def test_get_mobility_config(self, grpc_server): # given @@ -623,7 +628,7 @@ class TestGrpc: response = client.get_mobility_config(session.id, wlan.id) # then - assert len(response.groups) > 0 + assert len(response.config) > 0 def test_set_mobility_config(self, grpc_server): # given From f39b7e9f96487ea05788ee20c58ec7f9ea375b7f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 22 Oct 2019 12:08:55 -0700 Subject: [PATCH 096/462] updated open_xml functionality, grpc open_xml can optionally start now, added opened files to grpc get_sessions --- daemon/core/api/grpc/client.py | 5 +++-- daemon/core/api/grpc/server.py | 18 ++++++++++++------ daemon/core/api/tlv/corehandlers.py | 2 +- daemon/core/emulator/session.py | 11 ++++++++--- daemon/core/xml/corexml.py | 6 ++---- daemon/proto/core/api/grpc/core.proto | 3 +++ 6 files changed, 29 insertions(+), 16 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index cb24d979..496c722f 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -880,17 +880,18 @@ class CoreGrpcClient(object): with open(file_path, "w") as xml_file: xml_file.write(response.data) - def open_xml(self, file_path): + def open_xml(self, file_path, start=False): """ Load a local scenario XML file to open as a new session. :param str file_path: path of scenario XML file + :param bool start: True to start session, False otherwise :return: response with opened session id :rtype: core_pb2.OpenXmlResponse """ with open(file_path, "r") as xml_file: data = xml_file.read() - request = core_pb2.OpenXmlRequest(data=data) + request = core_pb2.OpenXmlRequest(data=data, start=start, file=file_path) return self.stub.OpenXml(request) def emane_link(self, session_id, nem_one, nem_two, linked): diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 85838827..de73dca1 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -305,7 +305,10 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): for session_id in self.coreemu.sessions: session = self.coreemu.sessions[session_id] session_summary = core_pb2.SessionSummary( - id=session_id, state=session.state, nodes=session.get_node_count() + id=session_id, + state=session.state, + nodes=session.get_node_count(), + file=session.file_name, ) sessions.append(session_summary) return core_pb2.GetSessionsResponse(sessions=sessions) @@ -1555,19 +1558,22 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("open xml: %s", request) session = self.coreemu.create_session() - session.set_state(EventTypes.CONFIGURATION_STATE) - _, temp_path = tempfile.mkstemp() - with open(temp_path, "w") as xml_file: - xml_file.write(request.data) + temp = tempfile.NamedTemporaryFile(delete=False) + temp.write(request.data.encode("utf-8")) + temp.close() try: - session.open_xml(temp_path, start=True) + session.open_xml(temp.name, request.start) + session.name = os.path.basename(request.file) + session.file_name = request.file return core_pb2.OpenXmlResponse(session_id=session.id, result=True) except IOError: logging.exception("error opening session file") self.coreemu.delete_session(session.id) context.abort(grpc.StatusCode.INVALID_ARGUMENT, "invalid xml file") + finally: + os.unlink(temp.name) def GetInterfaces(self, request, context): """ diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 41bac314..1e15377a 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -944,7 +944,7 @@ class CoreHandler(socketserver.BaseRequestHandler): if os.path.splitext(file_name)[1].lower() == ".xml": session = self.coreemu.create_session(master=False) try: - session.open_xml(file_name, start=True) + session.open_xml(file_name) except Exception: self.coreemu.delete_session(session.id) raise diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 179c580d..ee0c5d1f 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -820,19 +820,24 @@ class Session(object): :param bool start: instantiate session if true, false otherwise :return: nothing """ + logging.info("opening xml: %s", file_name) + # clear out existing session self.clear() if start: - self.set_state(EventTypes.CONFIGURATION_STATE) + state = EventTypes.CONFIGURATION_STATE + else: + state = EventTypes.DEFINITION_STATE + self.set_state(state) + self.name = os.path.basename(file_name) + self.file_name = file_name # write out xml file CoreXmlReader(self).read(file_name) # start session if needed if start: - self.name = os.path.basename(file_name) - self.file_name = file_name self.instantiate() def save_xml(self, file_name): diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index f4a360c6..220175bc 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -304,10 +304,8 @@ class CoreXmlWriter(object): default_options = self.session.options.default_values() for _id in default_options: default_value = default_options[_id] - # TODO: should we just save the current config regardless, since it may change? - value = options_config[_id] - if value != default_value: - add_configuration(option_elements, _id, value) + value = options_config.get(_id, default_value) + add_configuration(option_elements, _id, value) if option_elements.getchildren(): self.scenario.append(option_elements) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index db43dec6..af1768ce 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -631,6 +631,8 @@ message SaveXmlResponse { message OpenXmlRequest { string data = 1; + bool start = 2; + string file = 3; } message OpenXmlResponse { @@ -798,6 +800,7 @@ message SessionSummary { int32 id = 1; SessionState.Enum state = 2; int32 nodes = 3; + string file = 4; } message Node { From b703ad11c6320c821fb4ca87728d3d0feb40672f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 22 Oct 2019 12:19:37 -0700 Subject: [PATCH 097/462] updating command logging back to debug --- daemon/core/emulator/distributed.py | 2 +- daemon/core/nodes/network.py | 2 +- daemon/core/utils.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index 03e043eb..24ffbae6 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -54,7 +54,7 @@ class DistributedServer(object): replace_env = env is not None if not wait: cmd += " &" - logging.info( + logging.debug( "remote cmd server(%s) cwd(%s) wait(%s): %s", self.host, cwd, wait, cmd ) try: diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index f85235f1..e57a0a95 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -284,7 +284,7 @@ class CoreNetwork(CoreNetworkBase): :rtype: str :raises CoreCommandError: when a non-zero exit status occurs """ - logging.info("network node(%s) cmd", self.name) + logging.debug("network node(%s) cmd", self.name) output = utils.cmd(args, env, cwd, wait, shell) self.session.distributed.execute(lambda x: x.remote_cmd(args, env, cwd, wait)) return output diff --git a/daemon/core/utils.py b/daemon/core/utils.py index 2e2296c0..407258be 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -206,7 +206,7 @@ def cmd(args, env=None, cwd=None, wait=True, shell=False): :raises CoreCommandError: when there is a non-zero exit status or the file to execute is not found """ - logging.info("command cwd(%s) wait(%s): %s", cwd, wait, args) + logging.debug("command cwd(%s) wait(%s): %s", cwd, wait, args) if shell is False: args = shlex.split(args) try: From 9e7b5abeb9c6d658c905e5738bffb0ed912606ad Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 22 Oct 2019 12:36:47 -0700 Subject: [PATCH 098/462] updated fabric commands to be hide output --- daemon/core/emulator/distributed.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index 24ffbae6..5abb5bef 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -18,6 +18,7 @@ from core.nodes.ipaddress import IpAddress from core.nodes.network import CoreNetwork, CtrlNet LOCK = threading.Lock() +CMD_HIDE = True class DistributedServer(object): @@ -60,12 +61,12 @@ class DistributedServer(object): try: if cwd is None: result = self.conn.run( - cmd, hide=False, env=env, replace_env=replace_env + cmd, hide=CMD_HIDE, env=env, replace_env=replace_env ) else: with self.conn.cd(cwd): result = self.conn.run( - cmd, hide=False, env=env, replace_env=replace_env + cmd, hide=CMD_HIDE, env=env, replace_env=replace_env ) return result.stdout.strip() except UnexpectedExit as e: From c1bb9ed5d8cde051c9ee941b8178962eb8de033e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:15:12 -0700 Subject: [PATCH 099/462] added optional custom class param for session.add_node --- daemon/core/emulator/session.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index ee0c5d1f..725accf8 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -634,19 +634,23 @@ class Session(object): if node_two: node_two.lock.release() - def add_node(self, _type=NodeTypes.DEFAULT, _id=None, node_options=None): + def add_node(self, _type=NodeTypes.DEFAULT, _id=None, node_options=None, _cls=None): """ Add a node to the session, based on the provided node data. :param core.emulator.enumerations.NodeTypes _type: type of node to create :param int _id: id for node, defaults to None for generated id :param core.emulator.emudata.NodeOptions node_options: data to create node with + :param class _cls: optional custom class to use for a created node :return: created node :raises core.CoreError: when an invalid node type is given """ # validate node type, get class, or throw error - node_class = self.get_node_class(_type) + if _cls is None: + node_class = self.get_node_class(_type) + else: + node_class = _cls # set node start based on current session state, override and check when rj45 start = self.state > EventTypes.DEFINITION_STATE.value @@ -705,13 +709,8 @@ class Session(object): # set node position and broadcast it self.set_node_position(node, node_options) - # add services to default and physical nodes only - if _type in [ - NodeTypes.DEFAULT, - NodeTypes.PHYSICAL, - NodeTypes.DOCKER, - NodeTypes.LXC, - ]: + # add services to needed nodes + if isinstance(node, (CoreNode, PhysicalNode, DockerNode, LxcNode)): node.type = node_options.model logging.debug("set node type: %s", node.type) self.services.add_services(node, node.type, node_options.services) From c1ed7f54d8980cea4a576c88ca68a95a308ca36a Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:17:47 -0700 Subject: [PATCH 100/462] coretk --- coretk/coretk/coregrpc.py | 5 + coretk/coretk/coremenubar.py | 4 +- coretk/coretk/coretocanvas.py | 1 + coretk/coretk/grpcmanagement.py | 4 +- coretk/coretk/linkinfo.py | 174 ++++++++++++++++++++++++++++---- coretk/coretk/menuaction.py | 5 + coretk/coretk/sizeandscale.py | 86 ++++++++++++++++ 7 files changed, 259 insertions(+), 20 deletions(-) create mode 100644 coretk/coretk/sizeandscale.py diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 3d2955dc..e3e98edd 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -35,6 +35,9 @@ class CoreGrpc: def log_throughput(self, event): interface_throughputs = event.interface_throughputs + # for i in interface_throughputs: + # print(i) + # return throughputs_belong_to_session = [] for if_tp in interface_throughputs: if if_tp.node_id in self.node_ids: @@ -245,6 +248,8 @@ class CoreGrpc: response = self.core.add_link(self.session_id, id1, id2, if1, if2) logging.info("created link: %s", response) + self.core.get_node_links(self.session_id, id1) + # def get_session(self): # response = self.core.get_session(self.session_id) # nodes = response.session.nodes diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index f08d18ed..353e0c8a 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -166,7 +166,9 @@ class CoreMenubar(object): canvas_menu.add_separator() - canvas_menu.add_command(label="Size/scale...", command=action.canvas_size_scale) + canvas_menu.add_command( + label="Size/scale...", command=self.menu_action.canvas_size_and_scale + ) canvas_menu.add_command(label="Wallpaper...", command=action.canvas_wallpaper) canvas_menu.add_separator() diff --git a/coretk/coretk/coretocanvas.py b/coretk/coretk/coretocanvas.py index f16c5306..e808735f 100644 --- a/coretk/coretk/coretocanvas.py +++ b/coretk/coretk/coretocanvas.py @@ -8,6 +8,7 @@ class CoreToCanvasMapping: def __init__(self): self.core_id_to_canvas_id = {} self.core_node_and_interface_to_canvas_edge = {} + # self.edge_id_to_canvas_token = {} def map_node_and_interface_to_canvas_edge(self, nid, iid, edge_token): self.core_node_and_interface_to_canvas_edge[tuple([nid, iid])] = edge_token diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index 2d5a9027..ebd34f6c 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -230,7 +230,7 @@ class GrpcManager: """ src_interface = None dst_interface = None - + print("create interface") self.interfaces_manager.new_subnet() src_node = self.nodes[src_canvas_id] @@ -263,6 +263,8 @@ class GrpcManager: edge.interface_1 = src_interface edge.interface_2 = dst_interface + print(src_interface) + print(dst_interface) return src_interface, dst_interface def add_edge(self, session_id, token, canvas_id_1, canvas_id_2): diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index 7480e004..c21a56b0 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -1,11 +1,23 @@ """ Link information, such as IPv4, IPv6 and throughput drawn in the canvas """ +import logging import math +WIRELESS_DEF = ["mdr", "wlan"] + class LinkInfo: def __init__(self, canvas, edge, ip4_src, ip6_src, ip4_dst, ip6_dst): + """ + create an instance of LinkInfo object + :param tkinter.Canvas canvas: canvas object + :param coretk.graph.CanvasEdge edge: canvas edge onject + :param ip4_src: + :param ip6_src: + :param ip4_dst: + :param ip6_dst: + """ self.canvas = canvas self.edge = edge # self.edge_id = edge.id @@ -20,13 +32,23 @@ class LinkInfo: self.id2 = self.create_edge_dst_info() def slope_src_dst(self): + """ + calculate slope of the line connecting source node to destination node + :rtype: float + :return: slope of line + """ x1, y1, x2, y2 = self.canvas.coords(self.edge.id) if x2 - x1 == 0: - return 9999 + return 9999.0 else: return (y2 - y1) / (x2 - x1) def create_edge_src_info(self): + """ + draw the ip address for source node + + :return: nothing + """ x1, y1, x2, _ = self.canvas.coords(self.edge.id) m = self.slope_src_dst() distance = math.cos(math.atan(m)) * self.radius @@ -39,6 +61,11 @@ class LinkInfo: return id1 def create_edge_dst_info(self): + """ + draw the ip address for destination node + + :return: nothing + """ x1, _, x2, y2 = self.canvas.coords(self.edge.id) m = self.slope_src_dst() distance = math.cos(math.atan(m)) * self.radius @@ -51,6 +78,11 @@ class LinkInfo: return id2 def recalculate_info(self): + """ + move the node info when the canvas node move + + :return: nothing + """ x1, y1, x2, y2 = self.canvas.coords(self.edge.id) m = self.slope_src_dst() distance = math.cos(math.atan(m)) * self.radius @@ -73,6 +105,11 @@ class LinkInfo: class Throughput: def __init__(self, canvas, grpc): + """ + create an instance of Throughput object + :param tkinter.Canvas canvas: canvas object + :param coretk.coregrpc,CoreGrpc grpc: grpc object + """ self.canvas = canvas self.core_grpc = grpc self.grpc_manager = canvas.grpc_manager @@ -83,6 +120,8 @@ class Throughput: # map an edge canvas id to a throughput canvas id self.map = {} + self.edge_id_to_token = {} + def load_throughput_info(self, interface_throughputs): """ load all interface throughouts from an event @@ -98,28 +137,119 @@ class Throughput: token = self.grpc_manager.core_mapping.get_token_from_node_and_interface( nid, iid ) + print(token) edge_id = self.canvas.edges[token].id + + self.edge_id_to_token[edge_id] = token + if edge_id not in self.tracker: self.tracker[edge_id] = tp else: temp = self.tracker[edge_id] self.tracker[edge_id] = (temp + tp) / 2 + def edge_is_wired(self, token): + """ + determine whether link is a WIRED link + + :param token: + :return: + """ + canvas_edge = self.canvas.edges[token] + canvas_src_id = canvas_edge.src + canvas_dst_id = canvas_edge.dst + src_node = self.canvas.nodes[canvas_src_id] + dst_node = self.canvas.nodes[canvas_dst_id] + + if src_node.node_type == "wlan": + if dst_node.node_type == "mdr": + return False + else: + logging.debug("linkinfo.py is_wired WARNING wlan only connected to mdr") + return True + if dst_node.node_type == "wlan": + if src_node.node_type == "mdr": + return False + else: + logging.debug("linkinfo.py is_wired WARNING wlan only connected to mdr") + return True + return True + + def draw_wired_throughput(self, edge_id): + + x1, y1, x2, y2 = self.canvas.coords(edge_id) + x = (x1 + x2) / 2 + y = (y1 + y2) / 2 + + if edge_id not in self.map: + tp_id = self.canvas.create_text( + x, y, text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]) + ) + self.map[edge_id] = tp_id + + # redraw throughput + else: + self.canvas.itemconfig( + self.map[edge_id], + text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), + ) + + def draw_wireless_throughput(self, edge_id): + token = self.edge_id_to_token[edge_id] + canvas_edge = self.canvas.edges[token] + canvas_src_id = canvas_edge.src + canvas_dst_id = canvas_edge.dst + src_node = self.canvas.nodes[canvas_src_id] + dst_node = self.canvas.nodes[canvas_dst_id] + + # non_wlan_node = None + if src_node.node_type == "wlan": + non_wlan_node = dst_node + else: + non_wlan_node = src_node + + x, y = self.canvas.coords(non_wlan_node.id) + if edge_id not in self.map: + tp_id = self.canvas.create_text( + x + 50, + y + 25, + text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), + ) + self.map[edge_id] = tp_id + + # redraw throughput + else: + self.canvas.itemconfig( + self.map[edge_id], + text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), + ) + def draw_throughputs(self): for edge_id in self.tracker: - x1, y1, x2, y2 = self.canvas.coords(edge_id) - x = (x1 + x2) / 2 - y = (y1 + y2) / 2 - if edge_id not in self.map: - tp_id = self.canvas.create_text( - x, y, text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]) - ) - self.map[edge_id] = tp_id + if self.edge_is_wired(self.edge_id_to_token[edge_id]): + self.draw_wired_throughput(edge_id) else: - self.canvas.itemconfig( - self.map[edge_id], - text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), - ) + self.draw_wireless_throughput(edge_id) + # draw wireless throughput + + # x1, y1, x2, y2 = self.canvas.coords(edge_id) + # x = (x1 + x2) / 2 + # y = (y1 + y2) / 2 + # + # print(self.is_wired(self.edge_id_to_token[edge_id])) + # # new throughput + # if edge_id not in self.map: + # tp_id = self.canvas.create_text( + # x, y, text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]) + # ) + # self.map[edge_id] = tp_id + # + # # redraw throughput + # else: + # self.canvas.itemconfig( + # self.map[edge_id], + # text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), + # ) def process_grpc_throughput_event(self, interface_throughputs): self.load_throughput_info(interface_throughputs) @@ -127,8 +257,16 @@ class Throughput: def update_throughtput_location(self, edge): tp_id = self.map[edge.id] - x1, y1 = self.canvas.coords(edge.src) - x2, y2 = self.canvas.coords(edge.dst) - x = (x1 + x2) / 2 - y = (y1 + y2) / 2 - self.canvas.coords(tp_id, x, y) + if self.edge_is_wired(self.edge_id_to_token[edge.id]): + x1, y1 = self.canvas.coords(edge.src) + x2, y2 = self.canvas.coords(edge.dst) + x = (x1 + x2) / 2 + y = (y1 + y2) / 2 + self.canvas.coords(tp_id, x, y) + else: + if self.canvas.nodes[edge.src].node_type == "wlan": + x, y = self.canvas.coords(edge.dst) + self.canvas.coords(tp_id, x + 50, y + 20) + else: + x, y = self.canvas.coords(edge.src) + self.canvas.coords(tp_id, x + 50, y + 25) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 207a1f3a..e7e1aafc 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -7,6 +7,7 @@ import webbrowser from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 +from coretk.sizeandscale import SizeAndScale SAVEDIR = "/home/ncs/Desktop/" @@ -153,6 +154,7 @@ def canvas_delete(): def canvas_size_scale(): logging.debug("Click canvas size/scale") + SizeAndScale() def canvas_wallpaper(): @@ -415,6 +417,9 @@ class MenuAction: # t1 = time.clock() # print(t1 - t0) + def canvas_size_and_scale(self): + SizeAndScale() + def help_core_github(self): webbrowser.open_new("https://github.com/coreemu/core") diff --git a/coretk/coretk/sizeandscale.py b/coretk/coretk/sizeandscale.py new file mode 100644 index 00000000..e9485892 --- /dev/null +++ b/coretk/coretk/sizeandscale.py @@ -0,0 +1,86 @@ +""" +size and scale +""" + +import tkinter as tk + + +class SizeAndScale: + def __init__(self): + self.top = tk.Toplevel() + self.top.title("Canvas Size and Scale") + self.size_chart() + + self.pixel_width_text = None + + def click_scrollbar(self, e1, e2, e3): + print(e1, e2, e3) + + def create_text_label(self, frame, text, row, column): + text_label = tk.Label(frame, text=text) + text_label.grid(row=row, column=column) + + def size_chart(self): + f = tk.Frame(self.top) + t = tk.Label(f, text="Size") + t.grid(row=0, column=0, sticky=tk.W) + + scrollbar = tk.Scrollbar(f, orient=tk.VERTICAL) + scrollbar.grid(row=1, column=1) + e = tk.Entry(f, text="1000", xscrollcommand=scrollbar.set) + e.focus() + e.grid(row=1, column=0) + scrollbar.config(command=self.click_scrollbar) + + # l = tk.Label(f, text="W") + # l.grid(row=1, column=2) + # l = tk.Label(f, text=" X ") + # l.grid(row=1, column=3) + self.create_text_label(f, "W", 1, 2) + self.create_text_label(f, " X ", 1, 3) + + hpixel_scrollbar = tk.Scrollbar(f, orient=tk.VERTICAL) + hpixel_scrollbar.grid(row=1, column=5) + + hpixel_entry = tk.Entry(f, text="750", xscrollcommand=hpixel_scrollbar.set) + hpixel_entry.focus() + hpixel_entry.grid(row=1, column=4) + + h_label = tk.Label(f, text="H") + h_label.grid(row=1, column=6) + + self.create_text_label(f, "pixels", 1, 7) + # pixel_label = tk.Label(f, text="pixels") + # pixel_label.grid(row=1, column=7) + + wmeter_scrollbar = tk.Scrollbar(f, orient=tk.VERTICAL) + wmeter_scrollbar.grid(row=2, column=2) + + wmeter_entry = tk.Entry(f, text="1500.0", xscrollcommand=wmeter_scrollbar.set) + wmeter_entry.focus() + wmeter_entry.grid(row=2, column=0, columnspan=2, sticky=tk.W + tk.E) + + # l = tk.Label(f, text=" X ") + # l.grid(row=2, column=3) + self.create_text_label(f, " X ", row=2, column=3) + + # f1 = tk.Frame(f) + hmeter_scrollbar = tk.Scrollbar(f, orient=tk.VERTICAL) + hmeter_scrollbar.grid(row=2, column=6) + + hmeter_entry = tk.Entry(f, text="1125.0", xscrollcommand=hmeter_scrollbar.set) + hmeter_entry.focus() + hmeter_entry.grid(row=2, column=4, columnspan=2, sticky=tk.W + tk.E) + + self.create_text_label(f, "pixels", 2, 7) + # pixel_label = tk.Label(f, text="pixels") + # pixel_label.grid(row=2, column=7) + # hmeter_entry.pack(side=tk.LEFT) + # + # hmeter_scrollbar = tk.Scrollbar(hmeter_entry, orient=tk.VERTICAL) + # hmeter_scrollbar.pack(side=tk.LEFT) + # f1.grid(row=2, column=4) + + f.grid() + + # def scale_chart(self): From ce411a07d79d498f93f207d2d1a38d77bff5ecc1 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 22 Oct 2019 15:13:28 -0700 Subject: [PATCH 101/462] removed session.create_wireless_node, can be achieved simply without needing this function --- daemon/core/emane/emanemanager.py | 2 +- daemon/core/emulator/session.py | 16 ---------------- daemon/examples/python/emane80211.py | 15 ++++++++------- daemon/examples/python/parser.py | 14 +++++++------- daemon/examples/python/switch.py | 16 +++++++--------- daemon/examples/python/wlan.py | 23 +++++++++++------------ daemon/tests/emane/test_emane.py | 20 ++++++++++---------- daemon/tests/test_core.py | 16 ++++++++-------- daemon/tests/test_xml.py | 8 ++++---- 9 files changed, 56 insertions(+), 74 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index b40ed118..ec3691a8 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -585,7 +585,7 @@ class EmaneManager(ModelManager): args = f"{emanecmd} -f {log_file} {platform_xml}" output = node.cmd(args) logging.info("node(%s) emane daemon running: %s", node.name, args) - logging.info("node(%s) emane daemon output: %s", node.name, output) + logging.debug("node(%s) emane daemon output: %s", node.name, output) if not run_emane_on_host: return diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 725accf8..85b02882 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -908,22 +908,6 @@ class Session(object): """ self.mobility.handleevent(event_data) - def create_wireless_node(self, _id=None, node_options=None): - """ - Create a wireless node for use within an wireless/EMANE networks. - - :param int _id: int for node, defaults to None and will be generated - :param core.emulator.emudata.NodeOptions node_options: options for emane node, model will always be "mdr" - :return: new emane node - :rtype: core.nodes.network.WlanNode - """ - if not node_options: - node_options = NodeOptions() - node_options.model = "mdr" - return self.add_node( - _type=NodeTypes.DEFAULT, _id=_id, node_options=node_options - ) - def create_emane_network( self, model, diff --git a/daemon/examples/python/emane80211.py b/daemon/examples/python/emane80211.py index 75098398..f3682fdf 100644 --- a/daemon/examples/python/emane80211.py +++ b/daemon/examples/python/emane80211.py @@ -4,11 +4,11 @@ import parser from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emulator.coreemu import CoreEmu -from core.emulator.emudata import IpPrefixes +from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.enumerations import EventTypes -def example(options): +def example(args): # ip generator for example prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") @@ -26,8 +26,9 @@ def example(options): emane_network.setposition(x=80, y=50) # create nodes - for i in range(options.nodes): - node = session.create_wireless_node() + options = NodeOptions(model="mdr") + for i in range(args.nodes): + node = session.add_node(node_options=options) node.setposition(x=150 * (i + 1), y=150) interface = prefixes.create_interface(node) session.add_link(node.id, emane_network.id, interface_one=interface) @@ -42,12 +43,12 @@ def example(options): def main(): logging.basicConfig(level=logging.INFO) - options = parser.parse_options("emane80211") + args = parser.parse("emane80211") start = datetime.datetime.now() logging.info( - "running emane 80211 example: nodes(%s) time(%s)", options.nodes, options.time + "running emane 80211 example: nodes(%s) time(%s)", args.nodes, args.time ) - example(options) + example(args) logging.info("elapsed time: %s", datetime.datetime.now() - start) diff --git a/daemon/examples/python/parser.py b/daemon/examples/python/parser.py index d9efdab6..28c81343 100644 --- a/daemon/examples/python/parser.py +++ b/daemon/examples/python/parser.py @@ -5,7 +5,7 @@ DEFAULT_TIME = 10 DEFAULT_STEP = 1 -def parse_options(name): +def parse(name): parser = argparse.ArgumentParser(description=f"Run {name} example") parser.add_argument( "-n", @@ -22,11 +22,11 @@ def parse_options(name): help="example iperf run time in seconds", ) - options = parser.parse_args() + args = parser.parse_args() - if options.nodes < 2: - parser.error(f"invalid min number of nodes: {options.nodes}") - if options.time < 1: - parser.error(f"invalid test time: {options.time}") + if args.nodes < 2: + parser.error(f"invalid min number of nodes: {args.nodes}") + if args.time < 1: + parser.error(f"invalid test time: {args.time}") - return options + return args diff --git a/daemon/examples/python/switch.py b/daemon/examples/python/switch.py index 0d952fda..6277149c 100644 --- a/daemon/examples/python/switch.py +++ b/daemon/examples/python/switch.py @@ -7,7 +7,7 @@ from core.emulator.emudata import IpPrefixes from core.emulator.enumerations import EventTypes, NodeTypes -def example(options): +def example(args): # ip generator for example prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") @@ -22,7 +22,7 @@ def example(options): switch = session.add_node(_type=NodeTypes.SWITCH) # create nodes - for _ in range(options.nodes): + for _ in range(args.nodes): node = session.add_node() interface = prefixes.create_interface(node) session.add_link(node.id, switch.id, interface_one=interface) @@ -32,13 +32,13 @@ def example(options): # get nodes to run example first_node = session.get_node(2) - last_node = session.get_node(options.nodes + 1) + last_node = session.get_node(args.nodes + 1) logging.info("starting iperf server on node: %s", first_node.name) first_node.cmd("iperf -s -D") first_node_address = prefixes.ip4_address(first_node) logging.info("node %s connecting to %s", last_node.name, first_node_address) - output = last_node.cmd(f"iperf -t {options.time} -c {first_node_address}") + output = last_node.cmd(f"iperf -t {args.time} -c {first_node_address}") logging.info(output) first_node.cmd("killall -9 iperf") @@ -48,12 +48,10 @@ def example(options): def main(): logging.basicConfig(level=logging.INFO) - options = parser.parse_options("switch") + args = parser.parse("switch") start = datetime.datetime.now() - logging.info( - "running switch example: nodes(%s) time(%s)", options.nodes, options.time - ) - example(options) + logging.info("running switch example: nodes(%s) time(%s)", args.nodes, args.time) + example(args) logging.info("elapsed time: %s", datetime.datetime.now() - start) diff --git a/daemon/examples/python/wlan.py b/daemon/examples/python/wlan.py index 1ef1a5d1..31e1030c 100644 --- a/daemon/examples/python/wlan.py +++ b/daemon/examples/python/wlan.py @@ -8,7 +8,7 @@ from core.emulator.enumerations import EventTypes, NodeTypes from core.location.mobility import BasicRangeModel -def example(options): +def example(args): # ip generator for example prefixes = IpPrefixes("10.83.0.0/16") @@ -24,10 +24,10 @@ def example(options): session.mobility.set_model(wlan, BasicRangeModel) # create nodes, must set a position for wlan basic range model - node_options = NodeOptions() - node_options.set_position(0, 0) - for _ in range(options.nodes): - node = session.add_node(node_options=node_options) + options = NodeOptions(model="mdr") + options.set_position(0, 0) + for _ in range(args.nodes): + node = session.add_node(node_options=options) interface = prefixes.create_interface(node) session.add_link(node.id, wlan.id, interface_one=interface) @@ -36,13 +36,14 @@ def example(options): # get nodes for example run first_node = session.get_node(2) - last_node = session.get_node(options.nodes + 1) + last_node = session.get_node(args.nodes + 1) logging.info("starting iperf server on node: %s", first_node.name) first_node.cmd("iperf -s -D") address = prefixes.ip4_address(first_node) logging.info("node %s connecting to %s", last_node.name, address) - last_node.cmd(f"iperf -t {options.time} -c {address}") + output = last_node.cmd(f"iperf -t {args.time} -c {address}") + logging.info(output) first_node.cmd("killall -9 iperf") # shutdown session @@ -51,13 +52,11 @@ def example(options): def main(): logging.basicConfig(level=logging.INFO) - options = parser.parse_options("wlan") + args = parser.parse("wlan") start = datetime.datetime.now() - logging.info( - "running wlan example: nodes(%s) time(%s)", options.nodes, options.time - ) - example(options) + logging.info("running wlan example: nodes(%s) time(%s)", args.nodes, args.time) + example(args) logging.info("elapsed time: %s", datetime.datetime.now() - start) diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py index 3eb87596..26fd6dc7 100644 --- a/daemon/tests/emane/test_emane.py +++ b/daemon/tests/emane/test_emane.py @@ -60,11 +60,11 @@ class TestEmane: ) # create nodes - node_options = NodeOptions() - node_options.set_position(150, 150) - node_one = session.create_wireless_node(node_options=node_options) - node_options.set_position(300, 150) - node_two = session.create_wireless_node(node_options=node_options) + options = NodeOptions(model="mdr") + options.set_position(150, 150) + node_one = session.add_node(node_options=options) + options.set_position(300, 150) + node_two = session.add_node(node_options=options) for i, node in enumerate([node_one, node_two]): node.setposition(x=150 * (i + 1), y=150) @@ -95,11 +95,11 @@ class TestEmane: emane_network.setposition(x=80, y=50) # create nodes - node_options = NodeOptions() - node_options.set_position(150, 150) - node_one = session.create_wireless_node(node_options=node_options) - node_options.set_position(300, 150) - node_two = session.create_wireless_node(node_options=node_options) + options = NodeOptions(model="mdr") + options.set_position(150, 150) + node_one = session.add_node(node_options=options) + options.set_position(300, 150) + node_two = session.add_node(node_options=options) for i, node in enumerate([node_one, node_two]): node.setposition(x=150 * (i + 1), y=150) diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 47368740..48a2b025 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -146,10 +146,10 @@ class TestCore: session.mobility.set_model(wlan_node, BasicRangeModel) # create nodes - node_options = NodeOptions() - node_options.set_position(0, 0) - node_one = session.create_wireless_node(node_options=node_options) - node_two = session.create_wireless_node(node_options=node_options) + options = NodeOptions(model="mdr") + options.set_position(0, 0) + node_one = session.add_node(node_options=options) + node_two = session.add_node(node_options=options) # link nodes for node in [node_one, node_two]: @@ -176,10 +176,10 @@ class TestCore: session.mobility.set_model(wlan_node, BasicRangeModel) # create nodes - node_options = NodeOptions() - node_options.set_position(0, 0) - node_one = session.create_wireless_node(node_options=node_options) - node_two = session.create_wireless_node(node_options=node_options) + options = NodeOptions(model="mdr") + options.set_position(0, 0) + node_one = session.add_node(node_options=options) + node_two = session.add_node(node_options=options) # link nodes for node in [node_one, node_two]: diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index dc01c09d..f0ab92d8 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -174,10 +174,10 @@ class TestXml: session.mobility.set_model(wlan_node, BasicRangeModel, {"test": "1"}) # create nodes - node_options = NodeOptions() - node_options.set_position(0, 0) - node_one = session.create_wireless_node(node_options=node_options) - node_two = session.create_wireless_node(node_options=node_options) + options = NodeOptions(model="mdr") + options.set_position(0, 0) + node_one = session.add_node(node_options=options) + node_two = session.add_node(node_options=options) # link nodes for node in [node_one, node_two]: From ab0abd65aa28abba7a0398bacc8133e09d4e2971 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 22 Oct 2019 15:31:50 -0700 Subject: [PATCH 102/462] modified session.add_node parameter node_options, to just be options --- daemon/core/api/grpc/server.py | 28 ++++---- daemon/core/api/tlv/corehandlers.py | 20 +++--- daemon/core/emulator/session.py | 67 +++++++++----------- daemon/core/xml/corexml.py | 20 +++--- daemon/examples/docker/docker2core.py | 2 +- daemon/examples/docker/docker2docker.py | 4 +- daemon/examples/docker/switch.py | 4 +- daemon/examples/lxd/lxd2core.py | 2 +- daemon/examples/lxd/lxd2lxd.py | 4 +- daemon/examples/lxd/switch.py | 4 +- daemon/examples/python/distributed_emane.py | 4 +- daemon/examples/python/distributed_lxd.py | 4 +- daemon/examples/python/distributed_ptp.py | 4 +- daemon/examples/python/distributed_switch.py | 2 +- daemon/examples/python/emane80211.py | 2 +- daemon/examples/python/wlan.py | 2 +- daemon/tests/emane/test_emane.py | 8 +-- daemon/tests/test_core.py | 8 +-- daemon/tests/test_grpc.py | 8 +-- daemon/tests/test_nodes.py | 4 +- daemon/tests/test_xml.py | 8 +-- 21 files changed, 100 insertions(+), 109 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index de73dca1..a54892c2 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -769,18 +769,18 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): node_type = NodeTypes.DEFAULT.value node_type = NodeTypes(node_type) - node_options = NodeOptions(name=node_proto.name, model=node_proto.model) - node_options.icon = node_proto.icon - node_options.opaque = node_proto.opaque - node_options.image = node_proto.image - node_options.services = node_proto.services + options = NodeOptions(name=node_proto.name, model=node_proto.model) + options.icon = node_proto.icon + options.opaque = node_proto.opaque + options.image = node_proto.image + options.services = node_proto.services if node_proto.server: - node_options.emulation_server = node_proto.server + options.emulation_server = node_proto.server position = node_proto.position - node_options.set_position(position.x, position.y) - node_options.set_location(position.lat, position.lon, position.alt) - node = session.add_node(_type=node_type, _id=node_id, node_options=node_options) + options.set_position(position.x, position.y) + options.set_location(position.lat, position.lon, position.alt) + node = session.add_node(_type=node_type, _id=node_id, options=options) # configure emane if provided emane_model = node_proto.emane @@ -856,18 +856,18 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("edit node: %s", request) session = self.get_session(request.session_id, context) node = self.get_node(session, request.node_id, context) - node_options = NodeOptions() - node_options.icon = request.icon + options = NodeOptions() + options.icon = request.icon x = request.position.x y = request.position.y - node_options.set_position(x, y) + options.set_position(x, y) lat = request.position.lat lon = request.position.lon alt = request.position.alt - node_options.set_location(lat, lon, alt) + options.set_location(lat, lon, alt) result = True try: - session.update_node(node.id, node_options) + session.update_node(node.id, options) node_data = node.data(0) session.broadcast_node(node_data) except CoreError: diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 1e15377a..153cd555 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -698,12 +698,12 @@ class CoreHandler(socketserver.BaseRequestHandler): node_id = message.get_tlv(NodeTlvs.NUMBER.value) - node_options = NodeOptions( + options = NodeOptions( name=message.get_tlv(NodeTlvs.NAME.value), model=message.get_tlv(NodeTlvs.MODEL.value), ) - node_options.set_position( + options.set_position( x=message.get_tlv(NodeTlvs.X_POSITION.value), y=message.get_tlv(NodeTlvs.Y_POSITION.value), ) @@ -717,19 +717,19 @@ class CoreHandler(socketserver.BaseRequestHandler): alt = message.get_tlv(NodeTlvs.ALTITUDE.value) if alt is not None: alt = float(alt) - node_options.set_location(lat=lat, lon=lon, alt=alt) + options.set_location(lat=lat, lon=lon, alt=alt) - node_options.icon = message.get_tlv(NodeTlvs.ICON.value) - node_options.canvas = message.get_tlv(NodeTlvs.CANVAS.value) - node_options.opaque = message.get_tlv(NodeTlvs.OPAQUE.value) - node_options.emulation_server = message.get_tlv(NodeTlvs.EMULATION_SERVER.value) + options.icon = message.get_tlv(NodeTlvs.ICON.value) + options.canvas = message.get_tlv(NodeTlvs.CANVAS.value) + options.opaque = message.get_tlv(NodeTlvs.OPAQUE.value) + options.emulation_server = message.get_tlv(NodeTlvs.EMULATION_SERVER.value) services = message.get_tlv(NodeTlvs.SERVICES.value) if services: - node_options.services = services.split("|") + options.services = services.split("|") if message.flags & MessageFlags.ADD.value: - node = self.session.add_node(node_type, node_id, node_options) + node = self.session.add_node(node_type, node_id, options) if node: if message.flags & MessageFlags.STRING.value: self.node_status_request[node.id] = True @@ -748,7 +748,7 @@ class CoreHandler(socketserver.BaseRequestHandler): replies.append(coreapi.CoreNodeMessage.pack(flags, tlvdata)) # node update else: - self.session.update_node(node_id, node_options) + self.session.update_node(node_id, options) return replies diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 85b02882..71149e4b 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -634,13 +634,13 @@ class Session(object): if node_two: node_two.lock.release() - def add_node(self, _type=NodeTypes.DEFAULT, _id=None, node_options=None, _cls=None): + def add_node(self, _type=NodeTypes.DEFAULT, _id=None, options=None, _cls=None): """ Add a node to the session, based on the provided node data. :param core.emulator.enumerations.NodeTypes _type: type of node to create :param int _id: id for node, defaults to None for generated id - :param core.emulator.emudata.NodeOptions node_options: data to create node with + :param core.emulator.emudata.NodeOptions options: data to create node with :param class _cls: optional custom class to use for a created node :return: created node :raises core.CoreError: when an invalid node type is given @@ -666,18 +666,16 @@ class Session(object): break # generate name if not provided - if not node_options: - node_options = NodeOptions() - name = node_options.name + if not options: + options = NodeOptions() + name = options.name if not name: name = f"{node_class.__name__}{_id}" # verify distributed server - server = self.distributed.servers.get(node_options.emulation_server) - if node_options.emulation_server is not None and server is None: - raise CoreError( - f"invalid distributed server: {node_options.emulation_server}" - ) + server = self.distributed.servers.get(options.emulation_server) + if options.emulation_server is not None and server is None: + raise CoreError(f"invalid distributed server: {options.emulation_server}") # create node logging.info( @@ -693,7 +691,7 @@ class Session(object): _id=_id, name=name, start=start, - image=node_options.image, + image=options.image, server=server, ) else: @@ -702,18 +700,18 @@ class Session(object): ) # set node attributes - node.icon = node_options.icon - node.canvas = node_options.canvas - node.opaque = node_options.opaque + node.icon = options.icon + node.canvas = options.canvas + node.opaque = options.opaque # set node position and broadcast it - self.set_node_position(node, node_options) + self.set_node_position(node, options) # add services to needed nodes if isinstance(node, (CoreNode, PhysicalNode, DockerNode, LxcNode)): - node.type = node_options.model + node.type = options.model logging.debug("set node type: %s", node.type) - self.services.add_services(node, node.type, node_options.services) + self.services.add_services(node, node.type, options.services) # boot nodes if created after runtime, CoreNodes, Physical, and RJ45 are all nodes is_boot_node = isinstance(node, CoreNodeBase) and not isinstance(node, Rj45Node) @@ -724,12 +722,12 @@ class Session(object): return node - def update_node(self, node_id, node_options): + def update_node(self, node_id, options): """ Update node information. :param int node_id: id of node to update - :param core.emulator.emudata.NodeOptions node_options: data to update node with + :param core.emulator.emudata.NodeOptions options: data to update node with :return: True if node updated, False otherwise :rtype: bool :raises core.CoreError: when node to update does not exist @@ -738,26 +736,26 @@ class Session(object): node = self.get_node(node_id) # set node position and broadcast it - self.set_node_position(node, node_options) + self.set_node_position(node, options) # update attributes - node.canvas = node_options.canvas - node.icon = node_options.icon + node.canvas = options.canvas + node.icon = options.icon - def set_node_position(self, node, node_options): + def set_node_position(self, node, options): """ Set position for a node, use lat/lon/alt if needed. :param node: node to set position for - :param core.emulator.emudata.NodeOptions node_options: data for node + :param core.emulator.emudata.NodeOptions options: data for node :return: nothing """ # extract location values - x = node_options.x - y = node_options.y - lat = node_options.lat - lon = node_options.lon - alt = node_options.alt + x = options.x + y = options.y + lat = options.lat + lon = options.lon + alt = options.alt # check if we need to generate position from lat/lon/alt has_empty_position = all(i is None for i in [x, y]) @@ -909,12 +907,7 @@ class Session(object): self.mobility.handleevent(event_data) def create_emane_network( - self, - model, - geo_reference, - geo_scale=None, - node_options=NodeOptions(), - config=None, + self, model, geo_reference, geo_scale=None, options=NodeOptions(), config=None ): """ Convenience method for creating an emane network. @@ -922,7 +915,7 @@ class Session(object): :param model: emane model to use for emane network :param geo_reference: geo reference point to use for emane node locations :param geo_scale: geo scale to use for emane node locations, defaults to 1.0 - :param core.emulator.emudata.NodeOptions node_options: options for emane node being created + :param core.emulator.emudata.NodeOptions options: options for emane node being created :param dict config: emane model configuration :return: create emane network """ @@ -932,7 +925,7 @@ class Session(object): self.location.refscale = geo_scale # create and return network - emane_network = self.add_node(_type=NodeTypes.EMANE, node_options=node_options) + emane_network = self.add_node(_type=NodeTypes.EMANE, options=options) self.emane.set_model(emane_network, model, config) return emane_network diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 220175bc..8b9df329 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -737,53 +737,51 @@ class CoreXmlReader(object): node_id = get_int(device_element, "id") name = device_element.get("name") model = device_element.get("type") - node_options = NodeOptions(name, model) + options = NodeOptions(name, model) service_elements = device_element.find("services") if service_elements is not None: - node_options.services = [ - x.get("name") for x in service_elements.iterchildren() - ] + options.services = [x.get("name") for x in service_elements.iterchildren()] position_element = device_element.find("position") if position_element is not None: x = get_int(position_element, "x") y = get_int(position_element, "y") if all([x, y]): - node_options.set_position(x, y) + options.set_position(x, y) lat = get_float(position_element, "lat") lon = get_float(position_element, "lon") alt = get_float(position_element, "alt") if all([lat, lon, alt]): - node_options.set_location(lat, lon, alt) + 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, node_options=node_options) + self.session.add_node(_id=node_id, options=options) def read_network(self, network_element): node_id = get_int(network_element, "id") name = network_element.get("name") node_type = NodeTypes[network_element.get("type")] - node_options = NodeOptions(name) + options = NodeOptions(name) position_element = network_element.find("position") if position_element is not None: x = get_int(position_element, "x") y = get_int(position_element, "y") if all([x, y]): - node_options.set_position(x, y) + options.set_position(x, y) lat = get_float(position_element, "lat") lon = get_float(position_element, "lon") alt = get_float(position_element, "alt") if all([lat, lon, alt]): - node_options.set_location(lat, lon, alt) + options.set_location(lat, lon, alt) logging.info( "reading node id(%s) node_type(%s) name(%s)", node_id, node_type, name ) - self.session.add_node(_type=node_type, _id=node_id, node_options=node_options) + self.session.add_node(_type=node_type, _id=node_id, options=options) def read_links(self): link_elements = self.scenario.find("links") diff --git a/daemon/examples/docker/docker2core.py b/daemon/examples/docker/docker2core.py index 1359f2d0..86cf3dfe 100644 --- a/daemon/examples/docker/docker2core.py +++ b/daemon/examples/docker/docker2core.py @@ -15,7 +15,7 @@ if __name__ == "__main__": options = NodeOptions(model=None, image="ubuntu") # create node one - node_one = session.add_node(_type=NodeTypes.DOCKER, node_options=options) + node_one = session.add_node(_type=NodeTypes.DOCKER, options=options) interface_one = prefixes.create_interface(node_one) # create node two diff --git a/daemon/examples/docker/docker2docker.py b/daemon/examples/docker/docker2docker.py index 7f3a3fbb..261a8f67 100644 --- a/daemon/examples/docker/docker2docker.py +++ b/daemon/examples/docker/docker2docker.py @@ -17,11 +17,11 @@ if __name__ == "__main__": options = NodeOptions(model=None, image="ubuntu") # create node one - node_one = session.add_node(_type=NodeTypes.DOCKER, node_options=options) + node_one = session.add_node(_type=NodeTypes.DOCKER, options=options) interface_one = prefixes.create_interface(node_one) # create node two - node_two = session.add_node(_type=NodeTypes.DOCKER, node_options=options) + node_two = session.add_node(_type=NodeTypes.DOCKER, options=options) interface_two = prefixes.create_interface(node_two) # add link diff --git a/daemon/examples/docker/switch.py b/daemon/examples/docker/switch.py index 154878bc..f66863e5 100644 --- a/daemon/examples/docker/switch.py +++ b/daemon/examples/docker/switch.py @@ -19,11 +19,11 @@ if __name__ == "__main__": switch = session.add_node(_type=NodeTypes.SWITCH) # node one - node_one = session.add_node(_type=NodeTypes.DOCKER, node_options=options) + node_one = session.add_node(_type=NodeTypes.DOCKER, options=options) interface_one = prefixes.create_interface(node_one) # node two - node_two = session.add_node(_type=NodeTypes.DOCKER, node_options=options) + node_two = session.add_node(_type=NodeTypes.DOCKER, options=options) interface_two = prefixes.create_interface(node_two) # node three diff --git a/daemon/examples/lxd/lxd2core.py b/daemon/examples/lxd/lxd2core.py index e4304145..06b2b6ba 100644 --- a/daemon/examples/lxd/lxd2core.py +++ b/daemon/examples/lxd/lxd2core.py @@ -15,7 +15,7 @@ if __name__ == "__main__": options = NodeOptions(image="ubuntu") # create node one - node_one = session.add_node(_type=NodeTypes.LXC, node_options=options) + node_one = session.add_node(_type=NodeTypes.LXC, options=options) interface_one = prefixes.create_interface(node_one) # create node two diff --git a/daemon/examples/lxd/lxd2lxd.py b/daemon/examples/lxd/lxd2lxd.py index 4f27de95..2449a223 100644 --- a/daemon/examples/lxd/lxd2lxd.py +++ b/daemon/examples/lxd/lxd2lxd.py @@ -17,11 +17,11 @@ if __name__ == "__main__": options = NodeOptions(image="ubuntu:18.04") # create node one - node_one = session.add_node(_type=NodeTypes.LXC, node_options=options) + node_one = session.add_node(_type=NodeTypes.LXC, options=options) interface_one = prefixes.create_interface(node_one) # create node two - node_two = session.add_node(_type=NodeTypes.LXC, node_options=options) + node_two = session.add_node(_type=NodeTypes.LXC, options=options) interface_two = prefixes.create_interface(node_two) # add link diff --git a/daemon/examples/lxd/switch.py b/daemon/examples/lxd/switch.py index 6056326f..7deaae5f 100644 --- a/daemon/examples/lxd/switch.py +++ b/daemon/examples/lxd/switch.py @@ -19,11 +19,11 @@ if __name__ == "__main__": switch = session.add_node(_type=NodeTypes.SWITCH) # node one - node_one = session.add_node(_type=NodeTypes.LXC, node_options=options) + node_one = session.add_node(_type=NodeTypes.LXC, options=options) interface_one = prefixes.create_interface(node_one) # node two - node_two = session.add_node(_type=NodeTypes.LXC, node_options=options) + node_two = session.add_node(_type=NodeTypes.LXC, options=options) interface_two = prefixes.create_interface(node_two) # node three diff --git a/daemon/examples/python/distributed_emane.py b/daemon/examples/python/distributed_emane.py index 74c7c93b..e2143886 100644 --- a/daemon/examples/python/distributed_emane.py +++ b/daemon/examples/python/distributed_emane.py @@ -31,11 +31,11 @@ def main(args): # create local node, switch, and remote nodes options = NodeOptions(model="mdr") options.set_position(0, 0) - node_one = session.add_node(node_options=options) + node_one = session.add_node(options=options) emane_net = session.add_node(_type=NodeTypes.EMANE) session.emane.set_model(emane_net, EmaneIeee80211abgModel) options.emulation_server = server_name - node_two = session.add_node(node_options=options) + node_two = session.add_node(options=options) # create node interfaces and link interface_one = prefixes.create_interface(node_one) diff --git a/daemon/examples/python/distributed_lxd.py b/daemon/examples/python/distributed_lxd.py index 80366a14..2cb69718 100644 --- a/daemon/examples/python/distributed_lxd.py +++ b/daemon/examples/python/distributed_lxd.py @@ -23,9 +23,9 @@ def main(args): # create local node, switch, and remote nodes options = NodeOptions(image="ubuntu:18.04") - node_one = session.add_node(_type=NodeTypes.LXC, node_options=options) + node_one = session.add_node(_type=NodeTypes.LXC, options=options) options.emulation_server = server_name - node_two = session.add_node(_type=NodeTypes.LXC, node_options=options) + node_two = session.add_node(_type=NodeTypes.LXC, options=options) # create node interfaces and link interface_one = prefixes.create_interface(node_one) diff --git a/daemon/examples/python/distributed_ptp.py b/daemon/examples/python/distributed_ptp.py index 887fdae4..35877be6 100644 --- a/daemon/examples/python/distributed_ptp.py +++ b/daemon/examples/python/distributed_ptp.py @@ -23,9 +23,9 @@ def main(args): # create local node, switch, and remote nodes options = NodeOptions() - node_one = session.add_node(node_options=options) + node_one = session.add_node(options=options) options.emulation_server = server_name - node_two = session.add_node(node_options=options) + node_two = session.add_node(options=options) # create node interfaces and link interface_one = prefixes.create_interface(node_one) diff --git a/daemon/examples/python/distributed_switch.py b/daemon/examples/python/distributed_switch.py index e87cd2c9..c9f5e1a4 100644 --- a/daemon/examples/python/distributed_switch.py +++ b/daemon/examples/python/distributed_switch.py @@ -28,7 +28,7 @@ def main(args): switch = session.add_node(_type=NodeTypes.SWITCH) options = NodeOptions() options.emulation_server = server_name - node_two = session.add_node(node_options=options) + node_two = session.add_node(options=options) # create node interfaces and link interface_one = prefixes.create_interface(node_one) diff --git a/daemon/examples/python/emane80211.py b/daemon/examples/python/emane80211.py index f3682fdf..a72a915d 100644 --- a/daemon/examples/python/emane80211.py +++ b/daemon/examples/python/emane80211.py @@ -28,7 +28,7 @@ def example(args): # create nodes options = NodeOptions(model="mdr") for i in range(args.nodes): - node = session.add_node(node_options=options) + node = session.add_node(options=options) node.setposition(x=150 * (i + 1), y=150) interface = prefixes.create_interface(node) session.add_link(node.id, emane_network.id, interface_one=interface) diff --git a/daemon/examples/python/wlan.py b/daemon/examples/python/wlan.py index 31e1030c..7c23d411 100644 --- a/daemon/examples/python/wlan.py +++ b/daemon/examples/python/wlan.py @@ -27,7 +27,7 @@ def example(args): options = NodeOptions(model="mdr") options.set_position(0, 0) for _ in range(args.nodes): - node = session.add_node(node_options=options) + node = session.add_node(options=options) interface = prefixes.create_interface(node) session.add_link(node.id, wlan.id, interface_one=interface) diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py index 26fd6dc7..c6255227 100644 --- a/daemon/tests/emane/test_emane.py +++ b/daemon/tests/emane/test_emane.py @@ -62,9 +62,9 @@ class TestEmane: # create nodes options = NodeOptions(model="mdr") options.set_position(150, 150) - node_one = session.add_node(node_options=options) + node_one = session.add_node(options=options) options.set_position(300, 150) - node_two = session.add_node(node_options=options) + node_two = session.add_node(options=options) for i, node in enumerate([node_one, node_two]): node.setposition(x=150 * (i + 1), y=150) @@ -97,9 +97,9 @@ class TestEmane: # create nodes options = NodeOptions(model="mdr") options.set_position(150, 150) - node_one = session.add_node(node_options=options) + node_one = session.add_node(options=options) options.set_position(300, 150) - node_two = session.add_node(node_options=options) + node_two = session.add_node(options=options) for i, node in enumerate([node_one, node_two]): node.setposition(x=150 * (i + 1), y=150) diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 48a2b025..321bca7b 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -148,8 +148,8 @@ class TestCore: # create nodes options = NodeOptions(model="mdr") options.set_position(0, 0) - node_one = session.add_node(node_options=options) - node_two = session.add_node(node_options=options) + node_one = session.add_node(options=options) + node_two = session.add_node(options=options) # link nodes for node in [node_one, node_two]: @@ -178,8 +178,8 @@ class TestCore: # create nodes options = NodeOptions(model="mdr") options.set_position(0, 0) - node_one = session.add_node(node_options=options) - node_two = session.add_node(node_options=options) + node_one = session.add_node(options=options) + node_two = session.add_node(options=options) # link nodes for node in [node_one, node_two]: diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index cb837957..6af78912 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -245,8 +245,8 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() session.set_state(EventTypes.CONFIGURATION_STATE) - node_options = NodeOptions(model="Host") - node = session.add_node(node_options=node_options) + options = NodeOptions(model="Host") + node = session.add_node(options=options) session.instantiate() output = "hello world" @@ -263,8 +263,8 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() session.set_state(EventTypes.CONFIGURATION_STATE) - node_options = NodeOptions(model="Host") - node = session.add_node(node_options=node_options) + options = NodeOptions(model="Host") + node = session.add_node(options=options) session.instantiate() # then diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index 70525f70..01e8c112 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -17,10 +17,10 @@ class TestNodes: @pytest.mark.parametrize("model", MODELS) def test_node_add(self, session, model): # given - node_options = NodeOptions(model=model) + options = NodeOptions(model=model) # when - node = session.add_node(node_options=node_options) + node = session.add_node(options=options) # give time for node services to boot time.sleep(1) diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index f0ab92d8..496623a6 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -108,8 +108,8 @@ class TestXml: ptp_node = session.add_node(_type=NodeTypes.PEER_TO_PEER) # create nodes - node_options = NodeOptions(model="host") - node_one = session.add_node(node_options=node_options) + options = NodeOptions(model="host") + node_one = session.add_node(options=options) node_two = session.add_node() # link nodes to ptp net @@ -176,8 +176,8 @@ class TestXml: # create nodes options = NodeOptions(model="mdr") options.set_position(0, 0) - node_one = session.add_node(node_options=options) - node_two = session.add_node(node_options=options) + node_one = session.add_node(options=options) + node_two = session.add_node(options=options) # link nodes for node in [node_one, node_two]: From cb81095b646de607044e644918a21ffa19232d5b Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 22 Oct 2019 20:50:01 -0700 Subject: [PATCH 103/462] refactored NodeData and NodeOptions to use server instead of emulation_server --- daemon/core/api/grpc/server.py | 2 +- daemon/core/api/tlv/corehandlers.py | 2 +- daemon/core/api/tlv/dataconversion.py | 2 +- daemon/core/emulator/data.py | 2 +- daemon/core/emulator/emudata.py | 2 +- daemon/core/emulator/session.py | 8 ++++---- daemon/core/nodes/base.py | 6 +++--- daemon/examples/python/distributed_emane.py | 2 +- daemon/examples/python/distributed_lxd.py | 2 +- daemon/examples/python/distributed_ptp.py | 2 +- daemon/examples/python/distributed_switch.py | 2 +- daemon/tests/distributed/test_distributed.py | 16 +++++++--------- 12 files changed, 23 insertions(+), 25 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index a54892c2..19ef40d0 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -775,7 +775,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): options.image = node_proto.image options.services = node_proto.services if node_proto.server: - options.emulation_server = node_proto.server + options.server = node_proto.server position = node_proto.position options.set_position(position.x, position.y) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 153cd555..3f5f6475 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -722,7 +722,7 @@ class CoreHandler(socketserver.BaseRequestHandler): options.icon = message.get_tlv(NodeTlvs.ICON.value) options.canvas = message.get_tlv(NodeTlvs.CANVAS.value) options.opaque = message.get_tlv(NodeTlvs.OPAQUE.value) - options.emulation_server = message.get_tlv(NodeTlvs.EMULATION_SERVER.value) + options.server = message.get_tlv(NodeTlvs.EMULATION_SERVER.value) services = message.get_tlv(NodeTlvs.SERVICES.value) if services: diff --git a/daemon/core/api/tlv/dataconversion.py b/daemon/core/api/tlv/dataconversion.py index a8525a05..8e1c270d 100644 --- a/daemon/core/api/tlv/dataconversion.py +++ b/daemon/core/api/tlv/dataconversion.py @@ -24,7 +24,7 @@ def convert_node(node_data): (NodeTlvs.IP6_ADDRESS, node_data.ip6_address), (NodeTlvs.MODEL, node_data.model), (NodeTlvs.EMULATION_ID, node_data.emulation_id), - (NodeTlvs.EMULATION_SERVER, node_data.emulation_server), + (NodeTlvs.EMULATION_SERVER, node_data.server), (NodeTlvs.SESSION, node_data.session), (NodeTlvs.X_POSITION, node_data.x_position), (NodeTlvs.Y_POSITION, node_data.y_position), diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index d5cb3f57..ba0dd457 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -64,7 +64,7 @@ NodeData = collections.namedtuple( "ip6_address", "model", "emulation_id", - "emulation_server", + "server", "session", "x_position", "y_position", diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index 5a38c69c..e739800e 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -87,7 +87,7 @@ class NodeOptions(object): self.lon = None self.alt = None self.emulation_id = None - self.emulation_server = None + self.server = None self.image = image def set_position(self, x, y): diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 71149e4b..118f695e 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -673,9 +673,9 @@ class Session(object): name = f"{node_class.__name__}{_id}" # verify distributed server - server = self.distributed.servers.get(options.emulation_server) - if options.emulation_server is not None and server is None: - raise CoreError(f"invalid distributed server: {options.emulation_server}") + server = self.distributed.servers.get(options.server) + if options.server is not None and server is None: + raise CoreError(f"invalid distributed server: {options.server}") # create node logging.info( @@ -713,7 +713,7 @@ class Session(object): logging.debug("set node type: %s", node.type) self.services.add_services(node, node.type, options.services) - # boot nodes if created after runtime, CoreNodes, Physical, and RJ45 are all nodes + # boot nodes after runtime, CoreNodes, Physical, and RJ45 are all nodes is_boot_node = isinstance(node, CoreNodeBase) and not isinstance(node, Rj45Node) if self.state == EventTypes.RUNTIME_STATE.value and is_boot_node: self.write_nodes() diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 8b35410d..65ed5dc7 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -194,9 +194,9 @@ class NodeBase(object): x, y, _ = self.getposition() model = self.type - emulation_server = None + server = None if self.server is not None: - emulation_server = self.server.name + server = self.server.name services = self.services if services is not None: @@ -217,7 +217,7 @@ class NodeBase(object): longitude=lon, altitude=alt, model=model, - emulation_server=emulation_server, + server=server, services=services, ) diff --git a/daemon/examples/python/distributed_emane.py b/daemon/examples/python/distributed_emane.py index e2143886..6b61e505 100644 --- a/daemon/examples/python/distributed_emane.py +++ b/daemon/examples/python/distributed_emane.py @@ -34,7 +34,7 @@ def main(args): node_one = session.add_node(options=options) emane_net = session.add_node(_type=NodeTypes.EMANE) session.emane.set_model(emane_net, EmaneIeee80211abgModel) - options.emulation_server = server_name + options.server = server_name node_two = session.add_node(options=options) # create node interfaces and link diff --git a/daemon/examples/python/distributed_lxd.py b/daemon/examples/python/distributed_lxd.py index 2cb69718..73d24b5a 100644 --- a/daemon/examples/python/distributed_lxd.py +++ b/daemon/examples/python/distributed_lxd.py @@ -24,7 +24,7 @@ def main(args): # create local node, switch, and remote nodes options = NodeOptions(image="ubuntu:18.04") node_one = session.add_node(_type=NodeTypes.LXC, options=options) - options.emulation_server = server_name + options.server = server_name node_two = session.add_node(_type=NodeTypes.LXC, options=options) # create node interfaces and link diff --git a/daemon/examples/python/distributed_ptp.py b/daemon/examples/python/distributed_ptp.py index 35877be6..0138dab3 100644 --- a/daemon/examples/python/distributed_ptp.py +++ b/daemon/examples/python/distributed_ptp.py @@ -24,7 +24,7 @@ def main(args): # create local node, switch, and remote nodes options = NodeOptions() node_one = session.add_node(options=options) - options.emulation_server = server_name + options.server = server_name node_two = session.add_node(options=options) # create node interfaces and link diff --git a/daemon/examples/python/distributed_switch.py b/daemon/examples/python/distributed_switch.py index c9f5e1a4..f659abd2 100644 --- a/daemon/examples/python/distributed_switch.py +++ b/daemon/examples/python/distributed_switch.py @@ -27,7 +27,7 @@ def main(args): node_one = session.add_node() switch = session.add_node(_type=NodeTypes.SWITCH) options = NodeOptions() - options.emulation_server = server_name + options.server = server_name node_two = session.add_node(options=options) # create node interfaces and link diff --git a/daemon/tests/distributed/test_distributed.py b/daemon/tests/distributed/test_distributed.py index 7078d6ed..c3d74365 100644 --- a/daemon/tests/distributed/test_distributed.py +++ b/daemon/tests/distributed/test_distributed.py @@ -35,15 +35,13 @@ def set_emane_model(node_id, model): ) -def node_message( - _id, name, emulation_server=None, node_type=NodeTypes.DEFAULT, model=None -): +def node_message(_id, name, server=None, node_type=NodeTypes.DEFAULT, model=None): """ Convenience method for creating a node TLV messages. :param int _id: node id :param str name: node name - :param str emulation_server: distributed server name, if desired + :param str server: distributed server name, if desired :param core.emulator.enumerations.NodeTypes node_type: node type :param str model: model for node :return: tlv message @@ -53,7 +51,7 @@ def node_message( (NodeTlvs.NUMBER, _id), (NodeTlvs.TYPE, node_type.value), (NodeTlvs.NAME, name), - (NodeTlvs.EMULATION_SERVER, emulation_server), + (NodeTlvs.EMULATION_SERVER, server), (NodeTlvs.X_POSITION, 0), (NodeTlvs.Y_POSITION, 0), ] @@ -182,7 +180,7 @@ class TestDistributed: # create distributed node and assign to distributed server message = node_message( - _id=2, name="n2", emulation_server=cored.distributed_server, model="host" + _id=2, name="n2", server=cored.distributed_server, model="host" ) cored.request_handler.handle_message(message) @@ -231,7 +229,7 @@ class TestDistributed: # create distributed node and assign to distributed server message = node_message( - _id=2, name="n2", emulation_server=cored.distributed_server, model="mdr" + _id=2, name="n2", server=cored.distributed_server, model="mdr" ) cored.request_handler.handle_message(message) @@ -281,7 +279,7 @@ class TestDistributed: message = node_message( _id=2, name="n2", - emulation_server=cored.distributed_server, + server=cored.distributed_server, node_type=NodeTypes.PHYSICAL, model="prouter", ) @@ -330,7 +328,7 @@ class TestDistributed: message = node_message( _id=2, name=distributed_address, - emulation_server=cored.distributed_server, + server=cored.distributed_server, node_type=NodeTypes.TUNNEL, ) cored.request_handler.handle_message(message) From 14d759667cdd9b4b8b093223aa8adcd7ff4305ec Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 22 Oct 2019 20:55:06 -0700 Subject: [PATCH 104/462] refactored session.update_node to session.edit_node to match grpc call --- daemon/core/api/grpc/server.py | 2 +- daemon/core/api/tlv/corehandlers.py | 2 +- daemon/core/emulator/session.py | 4 ++-- daemon/tests/test_nodes.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 19ef40d0..daf84236 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -867,7 +867,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): options.set_location(lat, lon, alt) result = True try: - session.update_node(node.id, options) + session.edit_node(node.id, options) node_data = node.data(0) session.broadcast_node(node_data) except CoreError: diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 3f5f6475..ffb28073 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -748,7 +748,7 @@ class CoreHandler(socketserver.BaseRequestHandler): replies.append(coreapi.CoreNodeMessage.pack(flags, tlvdata)) # node update else: - self.session.update_node(node_id, options) + self.session.edit_node(node_id, options) return replies diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 118f695e..55f2f078 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -722,9 +722,9 @@ class Session(object): return node - def update_node(self, node_id, options): + def edit_node(self, node_id, options): """ - Update node information. + Edit node information. :param int node_id: id of node to update :param core.emulator.emudata.NodeOptions options: data to update node with diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index 01e8c112..76b5b85d 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -40,7 +40,7 @@ class TestNodes: update_options.set_position(x=position_value, y=position_value) # when - session.update_node(node.id, update_options) + session.edit_node(node.id, update_options) # then assert node.position.x == position_value From 945f3cce5b85ee7184b208f73098052639fd0576 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 22 Oct 2019 21:27:31 -0700 Subject: [PATCH 105/462] removed session.create_emane_network, removed unused node types --- daemon/core/emulator/enumerations.py | 3 --- daemon/core/emulator/session.py | 31 ++++++++------------------- daemon/examples/python/emane80211.py | 11 +++++----- daemon/proto/core/api/grpc/core.proto | 3 --- daemon/tests/emane/test_emane.py | 21 +++++++++--------- daemon/tests/test_grpc.py | 18 ++++++++-------- docs/scripting.md | 11 +++++----- 7 files changed, 40 insertions(+), 58 deletions(-) diff --git a/daemon/core/emulator/enumerations.py b/daemon/core/emulator/enumerations.py index 9f5abece..f426774e 100644 --- a/daemon/core/emulator/enumerations.py +++ b/daemon/core/emulator/enumerations.py @@ -73,18 +73,15 @@ class NodeTypes(Enum): DEFAULT = 0 PHYSICAL = 1 - TBD = 3 SWITCH = 4 HUB = 5 WIRELESS_LAN = 6 RJ45 = 7 TUNNEL = 8 - KTUNNEL = 9 EMANE = 10 TAP_BRIDGE = 11 PEER_TO_PEER = 12 CONTROL_NET = 13 - EMANE_NET = 14 DOCKER = 15 LXC = 16 diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 55f2f078..e3b00695 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -55,15 +55,12 @@ from core.xml.corexml import CoreXmlReader, CoreXmlWriter NODES = { NodeTypes.DEFAULT: CoreNode, NodeTypes.PHYSICAL: PhysicalNode, - NodeTypes.TBD: None, NodeTypes.SWITCH: SwitchNode, NodeTypes.HUB: HubNode, NodeTypes.WIRELESS_LAN: WlanNode, NodeTypes.RJ45: Rj45Node, NodeTypes.TUNNEL: TunnelNode, - NodeTypes.KTUNNEL: None, NodeTypes.EMANE: EmaneNet, - NodeTypes.EMANE_NET: None, NodeTypes.TAP_BRIDGE: GreTapBridge, NodeTypes.PEER_TO_PEER: PtpNet, NodeTypes.CONTROL_NET: CtrlNet, @@ -906,28 +903,18 @@ class Session(object): """ self.mobility.handleevent(event_data) - def create_emane_network( - self, model, geo_reference, geo_scale=None, options=NodeOptions(), config=None - ): + def set_location(self, lat, lon, alt, scale): """ - Convenience method for creating an emane network. + Set session geospatial location. - :param model: emane model to use for emane network - :param geo_reference: geo reference point to use for emane node locations - :param geo_scale: geo scale to use for emane node locations, defaults to 1.0 - :param core.emulator.emudata.NodeOptions options: options for emane node being created - :param dict config: emane model configuration - :return: create emane network + :param float lat: latitude + :param float lon: longitude + :param float alt: altitude + :param float scale: reference scale + :return: nothing """ - # required to be set for emane to function properly - self.location.setrefgeo(*geo_reference) - if geo_scale: - self.location.refscale = geo_scale - - # create and return network - emane_network = self.add_node(_type=NodeTypes.EMANE, options=options) - self.emane.set_model(emane_network, model, config) - return emane_network + self.location.setrefgeo(lat, lon, alt) + self.location.refscale = scale def shutdown(self): """ diff --git a/daemon/examples/python/emane80211.py b/daemon/examples/python/emane80211.py index a72a915d..3a10321b 100644 --- a/daemon/examples/python/emane80211.py +++ b/daemon/examples/python/emane80211.py @@ -5,7 +5,7 @@ import parser from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes, NodeOptions -from core.emulator.enumerations import EventTypes +from core.emulator.enumerations import EventTypes, NodeTypes def example(args): @@ -20,10 +20,11 @@ def example(args): session.set_state(EventTypes.CONFIGURATION_STATE) # create emane network node - emane_network = session.create_emane_network( - model=EmaneIeee80211abgModel, geo_reference=(47.57917, -122.13232, 2.00000) - ) - emane_network.setposition(x=80, y=50) + session.set_location(47.57917, -122.13232, 2.00000, 1.0) + options = NodeOptions() + options.set_position(80, 50) + emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) + session.emane.set_model(emane_network, EmaneIeee80211abgModel) # create nodes options = NodeOptions(model="mdr") diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index af1768ce..325c436f 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -695,18 +695,15 @@ message NodeType { enum Enum { DEFAULT = 0; PHYSICAL = 1; - TBD = 3; SWITCH = 4; HUB = 5; WIRELESS_LAN = 6; RJ45 = 7; TUNNEL = 8; - KTUNNEL = 9; EMANE = 10; TAP_BRIDGE = 11; PEER_TO_PEER = 12; CONTROL_NET = 13; - EMANE_NET = 14; DOCKER = 15; LXC = 16; } diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py index c6255227..a27e8d83 100644 --- a/daemon/tests/emane/test_emane.py +++ b/daemon/tests/emane/test_emane.py @@ -12,6 +12,7 @@ from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emane.rfpipe import EmaneRfPipeModel from core.emane.tdma import EmaneTdmaModel from core.emulator.emudata import NodeOptions +from core.emulator.enumerations import NodeTypes from core.errors import CoreCommandError, CoreError _EMANE_MODELS = [ @@ -46,10 +47,11 @@ class TestEmane: """ # create emane node for networking the core nodes - emane_network = session.create_emane_network( - model, geo_reference=(47.57917, -122.13232, 2.00000) - ) - emane_network.setposition(x=80, y=50) + session.set_location(47.57917, -122.13232, 2.00000, 1.0) + options = NodeOptions() + options.set_position(80, 50) + emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) + session.emane.set_model(emane_network, model) # configure tdma if model == EmaneTdmaModel: @@ -87,12 +89,11 @@ class TestEmane: :param ip_prefixes: generates ip addresses for nodes """ # create emane node for networking the core nodes - emane_network = session.create_emane_network( - EmaneIeee80211abgModel, - geo_reference=(47.57917, -122.13232, 2.00000), - config={"test": "1"}, - ) - emane_network.setposition(x=80, y=50) + session.set_location(47.57917, -122.13232, 2.00000, 1.0) + options = NodeOptions() + options.set_position(80, 50) + emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) + session.emane.set_model(emane_network, EmaneIeee80211abgModel, {"test": "1"}) # create nodes options = NodeOptions(model="mdr") diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 6af78912..30e2daea 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -524,9 +524,9 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - emane_network = session.create_emane_network( - model=EmaneIeee80211abgModel, geo_reference=(47.57917, -122.13232, 2.00000) - ) + session.set_location(47.57917, -122.13232, 2.00000, 1.0) + emane_network = session.add_node(_type=NodeTypes.EMANE) + session.emane.set_model(emane_network, EmaneIeee80211abgModel) config_key = "platform_id_start" config_value = "2" session.emane.set_model_config( @@ -548,9 +548,9 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - emane_network = session.create_emane_network( - model=EmaneIeee80211abgModel, geo_reference=(47.57917, -122.13232, 2.00000) - ) + session.set_location(47.57917, -122.13232, 2.00000, 1.0) + emane_network = session.add_node(_type=NodeTypes.EMANE) + session.emane.set_model(emane_network, EmaneIeee80211abgModel) config_key = "bandwidth" config_value = "900000" @@ -574,9 +574,9 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - emane_network = session.create_emane_network( - model=EmaneIeee80211abgModel, geo_reference=(47.57917, -122.13232, 2.00000) - ) + session.set_location(47.57917, -122.13232, 2.00000, 1.0) + emane_network = session.add_node(_type=NodeTypes.EMANE) + session.emane.set_model(emane_network, EmaneIeee80211abgModel) # then with client.context_connect(): diff --git a/docs/scripting.md b/docs/scripting.md index 6985b7d8..38eaa901 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -108,13 +108,12 @@ Examples for configuring custom emane model settings. # create session and emane network coreemu = CoreEmu() session = coreemu.create_session() -emane_network = session.create_emane_network( - model=EmaneIeee80211abgModel, - geo_reference=(47.57917, -122.13232, 2.00000) -) -emane_network.setposition(x=80, y=50) +session.set_location(47.57917, -122.13232, 2.00000, 1.0) +options = NodeOptions() +options.set_position(80, 50) +emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) # set custom emane model config config = {} -session.emane.set_model_config(emane_network.id, EmaneIeee80211abgModel.name, config) +session.emane.set_model(emane_network, EmaneIeee80211abgModel, config) ``` From 28d1803af6e12efcd754e4393a8b814dd23ac4e0 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 22 Oct 2019 23:03:03 -0700 Subject: [PATCH 106/462] added netifi to CoreInterface, so it is defined up front --- daemon/core/nodes/interface.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index a32103b8..8dd3ac76 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -40,8 +40,10 @@ class CoreInterface(object): self.poshook = lambda a, b, c, d: None # used with EMANE self.transport_type = None - # interface index on the network + # node interface index self.netindex = None + # net interface index + self.netifi = None # index used to find flow data self.flow_id = None self.server = server From 3dccd073f234b3bd563a67d0603c0ecf2536f2c3 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 23 Oct 2019 09:02:24 -0700 Subject: [PATCH 107/462] updated newveth and newtuntap function to remove the net parameter, since it was not being used --- daemon/core/nodes/base.py | 10 ++++------ ns3/corens3/obj.py | 10 +++++----- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 65ed5dc7..d0ada65c 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -634,13 +634,12 @@ class CoreNode(CoreNodeBase): with self.lock: return super(CoreNode, self).newifindex() - def newveth(self, ifindex=None, ifname=None, net=None): + def newveth(self, ifindex=None, ifname=None): """ Create a new interface. :param int ifindex: index for the new interface :param str ifname: name for the new interface - :param core.nodes.base.CoreNetworkBase net: network to associate interface with :return: nothing """ with self.lock: @@ -692,13 +691,12 @@ class CoreNode(CoreNodeBase): return ifindex - def newtuntap(self, ifindex=None, ifname=None, net=None): + def newtuntap(self, ifindex=None, ifname=None): """ Create a new tunnel tap. :param int ifindex: interface index :param str ifname: interface name - :param net: network to associate with :return: interface index :rtype: int """ @@ -803,7 +801,7 @@ class CoreNode(CoreNodeBase): with self.lock: # TODO: emane specific code if net.is_emane is True: - ifindex = self.newtuntap(ifindex=ifindex, ifname=ifname, net=net) + ifindex = self.newtuntap(ifindex, ifname) # TUN/TAP is not ready for addressing yet; the device may # take some time to appear, and installing it into a # namespace after it has been bound removes addressing; @@ -815,7 +813,7 @@ class CoreNode(CoreNodeBase): netif.addaddr(address) return ifindex else: - ifindex = self.newveth(ifindex=ifindex, ifname=ifname, net=net) + ifindex = self.newveth(ifindex, ifname) if net is not None: self.attachnet(ifindex, net) diff --git a/ns3/corens3/obj.py b/ns3/corens3/obj.py index c1907f03..750dc7a5 100644 --- a/ns3/corens3/obj.py +++ b/ns3/corens3/obj.py @@ -60,7 +60,7 @@ class CoreNs3Node(CoreNode, ns.network.Node): if not isinstance(net, CoreNs3Net): return CoreNode.newnetif(self, net, addrlist, hwaddr, ifindex, ifname) - ifindex = self.newtuntap(ifindex=ifindex, ifname=ifname, net=net) + ifindex = self.newtuntap(ifindex, ifname) self.attachnet(ifindex, net) netif = self.netif(ifindex) netif.sethwaddr(hwaddr) @@ -68,7 +68,7 @@ class CoreNs3Node(CoreNode, ns.network.Node): netif.addaddr(addr) addrstr = netif.addrlist[0] - (addr, mask) = addrstr.split('/') + addr, mask = addrstr.split('/') tap = net._tapdevs[netif] tap.SetAttribute( "IpAddress", @@ -76,9 +76,9 @@ class CoreNs3Node(CoreNode, ns.network.Node): ) tap.SetAttribute( "Netmask", - ns.network.Ipv4MaskValue(ns.network.Ipv4Mask("/" + mask)) + ns.network.Ipv4MaskValue(ns.network.Ipv4Mask(f"/{mask}")) ) - ns.core.Simulator.Schedule(ns.core.Time('0'), netif.install) + ns.core.Simulator.Schedule(ns.core.Time("0"), netif.install) return ifindex def getns3position(self): @@ -118,7 +118,7 @@ class CoreNs3Net(CoreNetworkBase): type = "wlan" def __init__( - self, session, _id=None, name=None, start=True, server=None, policy=None + self, session, _id=None, name=None, start=True, server=None ): CoreNetworkBase.__init__(self, session, _id, name, start, server) self.tapbridge = ns.tap_bridge.TapBridgeHelper() From 39c40d2a8c1f7f23d8180b05ad0d97f92551c58f Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 23 Oct 2019 09:15:27 -0700 Subject: [PATCH 108/462] updated netif function to remove net parameter, since it was not used --- daemon/core/emulator/emudata.py | 2 +- daemon/core/emulator/session.py | 4 ++-- daemon/core/nodes/base.py | 5 +---- daemon/core/nodes/network.py | 4 +--- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index e739800e..792a896f 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -29,7 +29,7 @@ def create_interface(node, network, interface_data): ifindex=interface_data.id, ifname=interface_data.name, ) - return node.netif(interface_data.id, network) + return node.netif(interface_data.id) def link_config(network, interface, link_options, devname=None, interface_two=None): diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index e3b00695..6d74f0cd 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -594,11 +594,11 @@ class Session(object): raise CoreError("modify link for unknown nodes") elif not node_one: # node1 = layer 2node, node2 = layer3 node - interface = node_two.netif(interface_two_id, net_one) + interface = node_two.netif(interface_two_id) link_config(net_one, interface, link_options) elif not node_two: # node2 = layer 2node, node1 = layer3 node - interface = node_one.netif(interface_one_id, net_one) + interface = node_one.netif(interface_one_id) link_config(net_one, interface, link_options) else: common_networks = node_one.commonnets(node_two) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index d0ada65c..aadd2348 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -294,7 +294,6 @@ class CoreNodeBase(NodeBase): if ifindex in self._netif: raise ValueError(f"ifindex {ifindex} already exists") self._netif[ifindex] = netif - # TODO: this should have probably been set ahead, seems bad to me, check for failure and fix netif.netindex = ifindex def delnetif(self, ifindex): @@ -310,13 +309,11 @@ class CoreNodeBase(NodeBase): netif.shutdown() del netif - # TODO: net parameter is not used, remove - def netif(self, ifindex, net=None): + def netif(self, ifindex): """ Retrieve network interface. :param int ifindex: index of interface to retrieve - :param core.nodes.interface.CoreInterface net: network node :return: network interface, or None if not found :rtype: core.nodes.interface.CoreInterface """ diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index e57a0a95..0a5a0b7b 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -337,8 +337,6 @@ class CoreNetwork(CoreNetworkBase): del self.session self.up = False - # TODO: this depends on a subtype with localname defined, seems like the - # wrong place for this to live def attach(self, netif): """ Attach a network interface. @@ -1064,7 +1062,7 @@ class WlanNode(CoreNetwork): """ Attach a network interface. - :param core.nodes.interface.CoreInterface netif: network interface + :param core.nodes.interface.Veth netif: network interface :return: nothing """ CoreNetwork.attach(self, netif) From 6a0a9e7698e2d92f2303f55e601394e2f6029073 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 23 Oct 2019 09:31:07 -0700 Subject: [PATCH 109/462] updated all classes to be created without using (object), in python3 all classes are new style classes --- daemon/core/api/grpc/client.py | 4 ++-- daemon/core/api/tlv/coreapi.py | 6 +++--- daemon/core/config.py | 10 +++++----- daemon/core/emulator/coreemu.py | 2 +- daemon/core/emulator/distributed.py | 4 ++-- daemon/core/emulator/emudata.py | 10 +++++----- daemon/core/emulator/session.py | 2 +- daemon/core/location/corelocation.py | 2 +- daemon/core/location/event.py | 4 ++-- daemon/core/location/mobility.py | 2 +- daemon/core/nodes/base.py | 4 ++-- daemon/core/nodes/client.py | 2 +- daemon/core/nodes/docker.py | 2 +- daemon/core/nodes/interface.py | 2 +- daemon/core/nodes/ipaddress.py | 6 +++--- daemon/core/nodes/lxd.py | 2 +- daemon/core/nodes/netclient.py | 2 +- daemon/core/nodes/network.py | 2 +- daemon/core/plugins/sdt.py | 4 ++-- daemon/core/services/coreservices.py | 10 +++++----- daemon/core/xml/corexml.py | 8 ++++---- daemon/core/xml/corexmldeployment.py | 2 +- daemon/scripts/core-manage | 2 +- daemon/tests/conftest.py | 2 +- 24 files changed, 48 insertions(+), 48 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 496c722f..1f98c985 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -14,7 +14,7 @@ from core.api.grpc import core_pb2, core_pb2_grpc from core.nodes.ipaddress import Ipv4Prefix, Ipv6Prefix, MacAddress -class InterfaceHelper(object): +class InterfaceHelper: """ Convenience class to help generate IP4 and IP6 addresses for gRPC clients. """ @@ -133,7 +133,7 @@ def start_streamer(stream, handler): thread.start() -class CoreGrpcClient(object): +class CoreGrpcClient: """ Provides convenience methods for interfacing with the CORE grpc server. """ diff --git a/daemon/core/api/tlv/coreapi.py b/daemon/core/api/tlv/coreapi.py index 0fd16bf5..95818598 100644 --- a/daemon/core/api/tlv/coreapi.py +++ b/daemon/core/api/tlv/coreapi.py @@ -27,7 +27,7 @@ from core.emulator.enumerations import ( from core.nodes.ipaddress import IpAddress, MacAddress -class CoreTlvData(object): +class CoreTlvData: """ Helper base class used for packing and unpacking values using struct. """ @@ -348,7 +348,7 @@ class CoreTlvDataMacAddr(CoreTlvDataObj): return MacAddress(address=value[2:]) -class CoreTlv(object): +class CoreTlv: """ Base class for representing CORE TLVs. """ @@ -670,7 +670,7 @@ class CoreExceptionTlv(CoreTlv): } -class CoreMessage(object): +class CoreMessage: """ Base class for representing CORE messages. """ diff --git a/daemon/core/config.py b/daemon/core/config.py index e55d5f17..05ab5183 100644 --- a/daemon/core/config.py +++ b/daemon/core/config.py @@ -8,7 +8,7 @@ from collections import OrderedDict from core.emulator.data import ConfigData -class ConfigShim(object): +class ConfigShim: """ Provides helper methods for converting newer configuration values into TLV compatible formats. """ @@ -102,7 +102,7 @@ class ConfigShim(object): ) -class Configuration(object): +class Configuration: """ Represents a configuration options. """ @@ -131,7 +131,7 @@ class Configuration(object): return f"{self.__class__.__name__}(id={self.id}, type={self.type}, default={self.default}, options={self.options})" -class ConfigurableManager(object): +class ConfigurableManager: """ Provides convenience methods for storing and retrieving configuration options for nodes. """ @@ -240,7 +240,7 @@ class ConfigurableManager(object): return self.node_configurations.get(node_id) -class ConfigGroup(object): +class ConfigGroup: """ Defines configuration group tabs used for display by ConfigurationOptions. """ @@ -258,7 +258,7 @@ class ConfigGroup(object): self.stop = stop -class ConfigurableOptions(object): +class ConfigurableOptions: """ Provides a base for defining configuration options within CORE. """ diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py index 9c8b35ee..237000c7 100644 --- a/daemon/core/emulator/coreemu.py +++ b/daemon/core/emulator/coreemu.py @@ -29,7 +29,7 @@ signal.signal(signal.SIGUSR1, signal_handler) signal.signal(signal.SIGUSR2, signal_handler) -class CoreEmu(object): +class CoreEmu: """ Provides logic for creating and configuring CORE sessions and the nodes within them. """ diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index 5abb5bef..c2e90d6b 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -21,7 +21,7 @@ LOCK = threading.Lock() CMD_HIDE = True -class DistributedServer(object): +class DistributedServer: """ Provides distributed server interactions. """ @@ -101,7 +101,7 @@ class DistributedServer(object): os.unlink(temp.name) -class DistributedController(object): +class DistributedController: """ Provides logic for dealing with remote tunnels and distributed servers. """ diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index 792a896f..5e59eaae 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -4,7 +4,7 @@ from core.nodes.ipaddress import Ipv4Prefix, Ipv6Prefix, MacAddress from core.nodes.physical import PhysicalNode -class IdGen(object): +class IdGen: def __init__(self, _id=0): self.id = _id @@ -61,7 +61,7 @@ def link_config(network, interface, link_options, devname=None, interface_two=No network.linkconfig(**config) -class NodeOptions(object): +class NodeOptions: """ Options for creating and updating nodes within core. """ @@ -115,7 +115,7 @@ class NodeOptions(object): self.alt = alt -class LinkOptions(object): +class LinkOptions: """ Options for creating and updating links within core. """ @@ -145,7 +145,7 @@ class LinkOptions(object): self.opaque = None -class IpPrefixes(object): +class IpPrefixes: """ Convenience class to help generate IP4 and IP6 addresses for nodes within CORE. """ @@ -236,7 +236,7 @@ class IpPrefixes(object): ) -class InterfaceData(object): +class InterfaceData: """ Convenience class for storing interface data. """ diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 6d74f0cd..287e4b3c 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -71,7 +71,7 @@ NODES_TYPE = {NODES[x]: x for x in NODES} CTRL_NET_ID = 9001 -class Session(object): +class Session: """ CORE session manager. """ diff --git a/daemon/core/location/corelocation.py b/daemon/core/location/corelocation.py index 5f9e11e3..aeab8896 100644 --- a/daemon/core/location/corelocation.py +++ b/daemon/core/location/corelocation.py @@ -11,7 +11,7 @@ from core.emulator.enumerations import RegisterTlvs from core.location import utm -class CoreLocation(object): +class CoreLocation: """ Member of session class for handling global location data. This keeps track of a latitude/longitude/altitude reference point and scale in diff --git a/daemon/core/location/event.py b/daemon/core/location/event.py index 1872ac18..146062d1 100644 --- a/daemon/core/location/event.py +++ b/daemon/core/location/event.py @@ -70,7 +70,7 @@ class Timer(threading.Thread): @total_ordering -class Event(object): +class Event: """ Provides event objects that can be used within the EventLoop class. """ @@ -118,7 +118,7 @@ class Event(object): self.canceled = True -class EventLoop(object): +class EventLoop: """ Provides an event loop for running events. """ diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 92bd1b4b..fd07f0ef 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -535,7 +535,7 @@ class BasicRangeModel(WirelessModel): @total_ordering -class WayPoint(object): +class WayPoint: """ Maintains information regarding waypoints. """ diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index aadd2348..0f0840a1 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -21,7 +21,7 @@ from core.nodes.netclient import get_net_client _DEFAULT_MTU = 1500 -class NodeBase(object): +class NodeBase: """ Base class for CORE nodes (nodes and networks) """ @@ -1067,7 +1067,7 @@ class CoreNetworkBase(NodeBase): return all_links -class Position(object): +class Position: """ Helper class for Cartesian coordinate position """ diff --git a/daemon/core/nodes/client.py b/daemon/core/nodes/client.py index 3596bfa7..1e949c04 100644 --- a/daemon/core/nodes/client.py +++ b/daemon/core/nodes/client.py @@ -8,7 +8,7 @@ from core import utils from core.constants import VCMD_BIN -class VnodeClient(object): +class VnodeClient: """ Provides client functionality for interacting with a virtual node. """ diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index a70cfb39..d3d76319 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -10,7 +10,7 @@ from core.nodes.base import CoreNode from core.nodes.netclient import get_net_client -class DockerClient(object): +class DockerClient: def __init__(self, name, image, run): self.name = name self.image = image diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 8dd3ac76..4dde287a 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -10,7 +10,7 @@ from core.errors import CoreCommandError from core.nodes.netclient import get_net_client -class CoreInterface(object): +class CoreInterface: """ Base class for network interfaces. """ diff --git a/daemon/core/nodes/ipaddress.py b/daemon/core/nodes/ipaddress.py index df2309ab..c0b4e209 100644 --- a/daemon/core/nodes/ipaddress.py +++ b/daemon/core/nodes/ipaddress.py @@ -9,7 +9,7 @@ import struct from socket import AF_INET, AF_INET6 -class MacAddress(object): +class MacAddress: """ Provides mac address utilities for use within core. """ @@ -77,7 +77,7 @@ class MacAddress(object): return cls(tmpbytes[2:]) -class IpAddress(object): +class IpAddress: """ Provides ip utilities and functionality for use within core. """ @@ -202,7 +202,7 @@ class IpAddress(object): return struct.unpack("!I", value)[0] -class IpPrefix(object): +class IpPrefix: """ Provides ip address generation and prefix utilities. """ diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index 9d5dedc4..164fe5f7 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -10,7 +10,7 @@ from core.errors import CoreCommandError from core.nodes.base import CoreNode -class LxdClient(object): +class LxdClient: def __init__(self, name, image, run): self.name = name self.image = image diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 8bd58be7..b201493f 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -19,7 +19,7 @@ def get_net_client(use_ovs, run): return LinuxNetClient(run) -class LinuxNetClient(object): +class LinuxNetClient: """ Client for creating Linux bridges and ip interfaces for nodes. """ diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 0a5a0b7b..1654a4a6 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -21,7 +21,7 @@ from core.nodes.netclient import get_net_client ebtables_lock = threading.Lock() -class EbtablesQueue(object): +class EbtablesQueue: """ Helper class for queuing up ebtables commands into rate-limited atomic commits. This improves performance and reliability when there are diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 280a2bc2..410bba9d 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -24,7 +24,7 @@ from core.nodes.network import WlanNode # TODO: A named tuple may be more appropriate, than abusing a class dict like this -class Bunch(object): +class Bunch: """ Helper class for recording a collection of attributes. """ @@ -38,7 +38,7 @@ class Bunch(object): self.__dict__.update(kwargs) -class Sdt(object): +class Sdt: """ Helper class for exporting session objects to NRL"s SDT3D. The connect() method initializes the display, and can be invoked diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index 20553eb1..2a4cb178 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -29,7 +29,7 @@ class ServiceMode(enum.Enum): TIMER = 2 -class ServiceDependencies(object): +class ServiceDependencies: """ Can generate boot paths for services, based on their dependencies. Will validate that all services will be booted and that all dependencies exist within the services provided. @@ -127,7 +127,7 @@ class ServiceDependencies(object): return self.path -class ServiceShim(object): +class ServiceShim: keys = [ "dirs", "files", @@ -235,7 +235,7 @@ class ServiceShim(object): return servicesstring[1].split(",") -class ServiceManager(object): +class ServiceManager: """ Manages services available for CORE nodes to use. """ @@ -306,7 +306,7 @@ class ServiceManager(object): return service_errors -class CoreServices(object): +class CoreServices: """ Class for interacting with a list of available startup services for nodes. Mostly used to convert a CoreService into a Config API @@ -791,7 +791,7 @@ class CoreServices(object): node.nodefile(file_name, cfg) -class CoreService(object): +class CoreService: """ Parent class used for defining services. """ diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 8b9df329..b813c0d8 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -104,7 +104,7 @@ def add_configuration(parent, name, value): add_attribute(config_element, "value", value) -class NodeElement(object): +class NodeElement: def __init__(self, session, node, element_name): self.session = session self.node = node @@ -131,7 +131,7 @@ class NodeElement(object): add_attribute(position, "alt", alt) -class ServiceElement(object): +class ServiceElement: def __init__(self, service): self.service = service self.element = etree.Element("service") @@ -232,7 +232,7 @@ class NetworkElement(NodeElement): add_attribute(self.element, "type", node_type) -class CoreXmlWriter(object): +class CoreXmlWriter: def __init__(self, session): self.session = session self.scenario = etree.Element("scenario") @@ -527,7 +527,7 @@ class CoreXmlWriter(object): return link_element -class CoreXmlReader(object): +class CoreXmlReader: def __init__(self, session): self.session = session self.scenario = None diff --git a/daemon/core/xml/corexmldeployment.py b/daemon/core/xml/corexmldeployment.py index 83bf3333..ec6759e8 100644 --- a/daemon/core/xml/corexmldeployment.py +++ b/daemon/core/xml/corexmldeployment.py @@ -84,7 +84,7 @@ def get_ipv4_addresses(hostname): raise NotImplementedError -class CoreXmlDeployment(object): +class CoreXmlDeployment: def __init__(self, session, scenario): self.session = session self.scenario = scenario diff --git a/daemon/scripts/core-manage b/daemon/scripts/core-manage index d9b9de08..14e10e5b 100755 --- a/daemon/scripts/core-manage +++ b/daemon/scripts/core-manage @@ -14,7 +14,7 @@ from core import services from core.constants import CORE_CONF_DIR -class FileUpdater(object): +class FileUpdater: """ Helper class for changing configuration files. """ diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index 521a2432..f8949299 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -23,7 +23,7 @@ from core.services.coreservices import ServiceManager EMANE_SERVICES = "zebra|OSPFv3MDR|IPForward" -class CoreServerTest(object): +class CoreServerTest: def __init__(self, port=CORE_API_PORT): self.host = "localhost" self.port = port From 68be311c7ade488ef5c4ec981afe486f39ebec37 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 23 Oct 2019 09:51:52 -0700 Subject: [PATCH 110/462] updated usages of super to use python3 variation --- daemon/core/api/grpc/server.py | 2 +- daemon/core/api/tlv/coreapi.py | 6 +++--- daemon/core/config.py | 2 +- daemon/core/emane/emanemanager.py | 6 +++--- daemon/core/emane/ieee80211abg.py | 2 +- daemon/core/emane/nodes.py | 2 +- daemon/core/emane/rfpipe.py | 2 +- daemon/core/emane/tdma.py | 2 +- daemon/core/emulator/sessionconfig.py | 6 ++---- daemon/core/location/event.py | 2 +- daemon/core/location/mobility.py | 17 ++++++++--------- daemon/core/nodes/base.py | 10 +++++----- daemon/core/nodes/docker.py | 4 +--- daemon/core/nodes/lxd.py | 6 ++---- daemon/core/xml/corexml.py | 4 ++-- 15 files changed, 33 insertions(+), 40 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index daf84236..4381184d 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -202,7 +202,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ def __init__(self, coreemu): - super(CoreGrpcServer, self).__init__() + super().__init__() self.coreemu = coreemu self.running = True self.server = None diff --git a/daemon/core/api/tlv/coreapi.py b/daemon/core/api/tlv/coreapi.py index 95818598..5cb013d8 100644 --- a/daemon/core/api/tlv/coreapi.py +++ b/daemon/core/api/tlv/coreapi.py @@ -94,12 +94,12 @@ class CoreTlvDataObj(CoreTlvData): """ Convenience method for packing custom object data. - :param obj: custom object to pack + :param value: custom object to pack :return: length of data and the packed data itself :rtype: tuple """ value = cls.get_value(value) - return super(CoreTlvDataObj, cls).pack(value) + return super().pack(value) @classmethod def unpack(cls, data): @@ -109,7 +109,7 @@ class CoreTlvDataObj(CoreTlvData): :param data: data to unpack custom object from :return: unpacked custom object """ - data = super(CoreTlvDataObj, cls).unpack(data) + data = super().unpack(data) return cls.new_obj(data) @staticmethod diff --git a/daemon/core/config.py b/daemon/core/config.py index 05ab5183..e8e73300 100644 --- a/daemon/core/config.py +++ b/daemon/core/config.py @@ -309,7 +309,7 @@ class ModelManager(ConfigurableManager): """ Creates a ModelManager object. """ - super(ModelManager, self).__init__() + super().__init__() self.models = {} self.node_models = {} diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index ec3691a8..a66f9de7 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -62,7 +62,7 @@ class EmaneManager(ModelManager): :param core.session.Session session: session this manager is tied to :return: nothing """ - super(EmaneManager, self).__init__() + super().__init__() self.session = session self._emane_nets = {} self._emane_node_lock = threading.Lock() @@ -128,7 +128,7 @@ class EmaneManager(ModelManager): return config def config_reset(self, node_id=None): - super(EmaneManager, self).config_reset(node_id) + super().config_reset(node_id) self.set_configs(self.emane_config.default_values()) def emane_check(self): @@ -871,7 +871,7 @@ class EmaneGlobalModel(EmaneModel): ] def __init__(self, session, _id=None): - super(EmaneGlobalModel, self).__init__(session, _id) + super().__init__(session, _id) def build_xml_files(self, config, interface=None): raise NotImplementedError diff --git a/daemon/core/emane/ieee80211abg.py b/daemon/core/emane/ieee80211abg.py index d4df4282..e7a4d0d7 100644 --- a/daemon/core/emane/ieee80211abg.py +++ b/daemon/core/emane/ieee80211abg.py @@ -19,4 +19,4 @@ class EmaneIeee80211abgModel(emanemodel.EmaneModel): cls.mac_defaults["pcrcurveuri"] = os.path.join( emane_prefix, "share/emane/xml/models/mac/ieee80211abg/ieee80211pcr.xml" ) - super(EmaneIeee80211abgModel, cls).load(emane_prefix) + super().load(emane_prefix) diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index e0ceee2f..c7817b41 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -30,7 +30,7 @@ class EmaneNet(CoreNetworkBase): is_emane = True def __init__(self, session, _id=None, name=None, start=True, server=None): - super(EmaneNet, self).__init__(session, _id, name, start, server) + super().__init__(session, _id, name, start, server) self.conf = "" self.up = False self.nemidmap = {} diff --git a/daemon/core/emane/rfpipe.py b/daemon/core/emane/rfpipe.py index 312a5bb2..51820b7d 100644 --- a/daemon/core/emane/rfpipe.py +++ b/daemon/core/emane/rfpipe.py @@ -19,4 +19,4 @@ class EmaneRfPipeModel(emanemodel.EmaneModel): cls.mac_defaults["pcrcurveuri"] = os.path.join( emane_prefix, "share/emane/xml/models/mac/rfpipe/rfpipepcr.xml" ) - super(EmaneRfPipeModel, cls).load(emane_prefix) + super().load(emane_prefix) diff --git a/daemon/core/emane/tdma.py b/daemon/core/emane/tdma.py index afad9d10..59ed9e04 100644 --- a/daemon/core/emane/tdma.py +++ b/daemon/core/emane/tdma.py @@ -32,7 +32,7 @@ class EmaneTdmaModel(emanemodel.EmaneModel): emane_prefix, "share/emane/xml/models/mac/tdmaeventscheduler/tdmabasemodelpcr.xml", ) - super(EmaneTdmaModel, cls).load(emane_prefix) + super().load(emane_prefix) cls.mac_config.insert( 0, Configuration( diff --git a/daemon/core/emulator/sessionconfig.py b/daemon/core/emulator/sessionconfig.py index dc5a5133..38373696 100644 --- a/daemon/core/emulator/sessionconfig.py +++ b/daemon/core/emulator/sessionconfig.py @@ -61,7 +61,7 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions): config_type = RegisterTlvs.UTILITY.value def __init__(self): - super(SessionConfig, self).__init__() + super().__init__() self.set_configs(self.default_values()) def get_config( @@ -71,9 +71,7 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions): config_type=ConfigurableManager._default_type, default=None, ): - value = super(SessionConfig, self).get_config( - _id, node_id, config_type, default - ) + value = super().get_config(_id, node_id, config_type, default) if value == "": value = default return value diff --git a/daemon/core/location/event.py b/daemon/core/location/event.py index 146062d1..11e535d3 100644 --- a/daemon/core/location/event.py +++ b/daemon/core/location/event.py @@ -23,7 +23,7 @@ class Timer(threading.Thread): :param args: function arguments :param kwargs: function keyword arguments """ - super(Timer, self).__init__() + super().__init__() self.interval = interval self.function = function diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index fd07f0ef..470b2a84 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -38,7 +38,7 @@ class MobilityManager(ModelManager): :param core.emulator.session.Session session: session this manager is tied to """ - super(MobilityManager, self).__init__() + super().__init__() self.session = session self.models[BasicRangeModel.name] = BasicRangeModel self.models[Ns2ScriptedMobility.name] = Ns2ScriptedMobility @@ -302,7 +302,7 @@ class BasicRangeModel(WirelessModel): :param core.session.Session session: related core session :param int _id: object id """ - super(BasicRangeModel, self).__init__(session=session, _id=_id) + super().__init__(session=session, _id=_id) self.session = session self.wlan = session.get_node(_id) self._netifs = {} @@ -587,8 +587,7 @@ class WayPointMobility(WirelessModel): :param int _id: object id :return: """ - super(WayPointMobility, self).__init__(session=session, _id=_id) - + super().__init__(session=session, _id=_id) self.state = self.STATE_STOPPED self.queue = [] self.queue_copy = [] @@ -945,7 +944,7 @@ class Ns2ScriptedMobility(WayPointMobility): :param core.emulator.session.Session session: CORE session instance :param int _id: object id """ - super(Ns2ScriptedMobility, self).__init__(session=session, _id=_id) + super().__init__(session, _id) self._netifs = {} self._netifslock = threading.Lock() @@ -1137,7 +1136,7 @@ class Ns2ScriptedMobility(WayPointMobility): """ logging.info("starting script") laststate = self.state - super(Ns2ScriptedMobility, self).start() + super().start() if laststate == self.STATE_PAUSED: self.statescript("unpause") @@ -1147,7 +1146,7 @@ class Ns2ScriptedMobility(WayPointMobility): :return: nothing """ - super(Ns2ScriptedMobility, self).run() + super().run() self.statescript("run") def pause(self): @@ -1156,7 +1155,7 @@ class Ns2ScriptedMobility(WayPointMobility): :return: nothing """ - super(Ns2ScriptedMobility, self).pause() + super().pause() self.statescript("pause") def stop(self, move_initial=True): @@ -1166,7 +1165,7 @@ class Ns2ScriptedMobility(WayPointMobility): :param bool move_initial: flag to check if we should move node to initial position :return: nothing """ - super(Ns2ScriptedMobility, self).stop(move_initial=move_initial) + super().stop(move_initial=move_initial) self.statescript("stop") def statescript(self, typestr): diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 0f0840a1..ac67e8de 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -252,7 +252,7 @@ class CoreNodeBase(NodeBase): :param core.emulator.distributed.DistributedServer server: remote server node will run on, default is None for localhost """ - super(CoreNodeBase, self).__init__(session, _id, name, start, server) + super().__init__(session, _id, name, start, server) self.services = [] self.nodedir = None self.tmpnodedir = False @@ -354,7 +354,7 @@ class CoreNodeBase(NodeBase): :param z: z position :return: nothing """ - changed = super(CoreNodeBase, self).setposition(x, y, z) + changed = super().setposition(x, y, z) if changed: for netif in self.netifs(sort=True): netif.setposition(x, y, z) @@ -432,7 +432,7 @@ class CoreNode(CoreNodeBase): :param core.emulator.distributed.DistributedServer server: remote server node will run on, default is None for localhost """ - super(CoreNode, self).__init__(session, _id, name, start, server) + super().__init__(session, _id, name, start, server) self.nodedir = nodedir self.ctrlchnlname = os.path.abspath( os.path.join(self.session.session_dir, self.name) @@ -629,7 +629,7 @@ class CoreNode(CoreNodeBase): :rtype: int """ with self.lock: - return super(CoreNode, self).newifindex() + return super().newifindex() def newveth(self, ifindex=None, ifname=None): """ @@ -925,7 +925,7 @@ class CoreNetworkBase(NodeBase): :param core.emulator.distributed.DistributedServer server: remote server node will run on, default is None for localhost """ - super(CoreNetworkBase, self).__init__(session, _id, name, start, server) + super().__init__(session, _id, name, start, server) self._linked = {} self._linked_lock = threading.Lock() diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index d3d76319..20d6ec20 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -96,9 +96,7 @@ class DockerNode(CoreNode): if image is None: image = "ubuntu" self.image = image - super(DockerNode, self).__init__( - session, _id, name, nodedir, bootsh, start, server - ) + super().__init__(session, _id, name, nodedir, bootsh, start, server) def create_node_net_client(self, use_ovs): """ diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index 164fe5f7..fc2ee5cc 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -89,9 +89,7 @@ class LxcNode(CoreNode): if image is None: image = "ubuntu" self.image = image - super(LxcNode, self).__init__( - session, _id, name, nodedir, bootsh, start, server - ) + super().__init__(session, _id, name, nodedir, bootsh, start, server) def alive(self): """ @@ -217,6 +215,6 @@ class LxcNode(CoreNode): self.cmd(f"chmod {mode:o} {filename}") def addnetif(self, netif, ifindex): - super(LxcNode, self).addnetif(netif, ifindex) + super().addnetif(netif, ifindex) # adding small delay to allow time for adding addresses to work correctly time.sleep(0.5) diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index b813c0d8..96d0feff 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -197,7 +197,7 @@ class ServiceElement: class DeviceElement(NodeElement): def __init__(self, session, node): - super(DeviceElement, self).__init__(session, node, "device") + super().__init__(session, node, "device") add_attribute(self.element, "type", node.type) self.add_services() @@ -212,7 +212,7 @@ class DeviceElement(NodeElement): class NetworkElement(NodeElement): def __init__(self, session, node): - super(NetworkElement, self).__init__(session, node, "network") + super().__init__(session, node, "network") model = getattr(self.node, "model", None) if model: add_attribute(self.element, "model", model.name) From b185c3c679f9ead72dc062de251232627cd8e618 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 23 Oct 2019 10:16:35 -0700 Subject: [PATCH 111/462] updated network.py to leverage super() --- daemon/core/nodes/base.py | 4 +--- daemon/core/nodes/network.py | 36 +++++++++++++++--------------------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index ac67e8de..72fc0fe1 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -139,7 +139,7 @@ class NodeBase: if sort: return [self._netif[x] for x in sorted(self._netif)] else: - return self._netif.values() + return list(self._netif.values()) def numnetif(self): """ @@ -158,11 +158,9 @@ class NodeBase: :return: interface index if found, -1 otherwise :rtype: int """ - for ifindex in self._netif: if self._netif[ifindex] is netif: return ifindex - return -1 def newifindex(self): diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 1654a4a6..756de0e2 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -257,7 +257,7 @@ class CoreNetwork(CoreNetworkBase): will run on, default is None for localhost :param policy: network policy """ - CoreNetworkBase.__init__(self, session, _id, name, start, server) + super().__init__(session, _id, name, start, server) if name is None: name = str(self.id) if policy is not None: @@ -346,8 +346,7 @@ class CoreNetwork(CoreNetworkBase): """ if self.up: netif.net_client.create_interface(self.brname, netif.localname) - - CoreNetworkBase.attach(self, netif) + super().attach(netif) def detach(self, netif): """ @@ -358,8 +357,7 @@ class CoreNetwork(CoreNetworkBase): """ if self.up: netif.net_client.delete_interface(self.brname, netif.localname) - - CoreNetworkBase.detach(self, netif) + super().detach(netif) def linked(self, netif1, netif2): """ @@ -652,7 +650,7 @@ class GreTapBridge(CoreNetwork): :return: nothing """ - CoreNetwork.startup(self) + super().startup() if self.gretap: self.attach(self.gretap) @@ -666,7 +664,7 @@ class GreTapBridge(CoreNetwork): self.detach(self.gretap) self.gretap.shutdown() self.gretap = None - CoreNetwork.shutdown(self) + super().shutdown() def addrconfig(self, addrlist): """ @@ -753,7 +751,7 @@ class CtrlNet(CoreNetwork): self.assign_address = assign_address self.updown_script = updown_script self.serverintf = serverintf - CoreNetwork.__init__(self, session, _id, name, start, server) + super().__init__(session, _id, name, start, server) def add_addresses(self, address): """ @@ -784,8 +782,7 @@ class CtrlNet(CoreNetwork): if self.net_client.existing_bridges(self.id): raise CoreError(f"old bridges exist for node: {self.id}") - CoreNetwork.startup(self) - + super().startup() logging.info("added control network bridge: %s %s", self.brname, self.prefix) if self.hostid and self.assign_address: @@ -833,7 +830,7 @@ class CtrlNet(CoreNetwork): except CoreCommandError: logging.exception("error issuing shutdown script shutdown") - CoreNetwork.shutdown(self) + super().shutdown() def all_link_data(self, flags): """ @@ -864,8 +861,7 @@ class PtpNet(CoreNetwork): raise ValueError( "Point-to-point links support at most 2 network interfaces" ) - - CoreNetwork.attach(self, netif) + super().attach(netif) def data(self, message_type, lat=None, lon=None, alt=None): """ @@ -1017,7 +1013,7 @@ class HubNode(CoreNetwork): will run on, default is None for localhost :raises CoreCommandError: when there is a command exception """ - CoreNetwork.__init__(self, session, _id, name, start, server) + super().__init__(session, _id, name, start, server) # TODO: move to startup method if start: @@ -1048,7 +1044,7 @@ class WlanNode(CoreNetwork): will run on, default is None for localhost :param policy: wlan policy """ - CoreNetwork.__init__(self, session, _id, name, start, server, policy) + super().__init__(session, _id, name, start, server, policy) # wireless model such as basic range self.model = None # mobility model such as scripted @@ -1065,7 +1061,7 @@ class WlanNode(CoreNetwork): :param core.nodes.interface.Veth netif: network interface :return: nothing """ - CoreNetwork.attach(self, netif) + super().attach(netif) if self.model: netif.poshook = self.model.position_callback if netif.node is None: @@ -1097,12 +1093,12 @@ class WlanNode(CoreNetwork): def update_mobility(self, config): if not self.mobility: - raise ValueError("no mobility set to update for node(%s)", self.id) + raise ValueError(f"no mobility set to update for node({self.id})") self.mobility.update_config(config) def updatemodel(self, config): if not self.model: - raise ValueError("no model set to update for node(%s)", self.id) + raise ValueError(f"no model set to update for node({self.id})") logging.debug( "node(%s) updating model(%s): %s", self.id, self.model.name, config ) @@ -1120,11 +1116,9 @@ class WlanNode(CoreNetwork): :return: list of link data :rtype: list[core.emulator.data.LinkData] """ - all_links = CoreNetwork.all_link_data(self, flags) - + all_links = super().all_link_data(flags) if self.model: all_links.extend(self.model.all_link_data(flags)) - return all_links From 440c8ed565683eb34fed10bb492cb68a05f6a454 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 23 Oct 2019 10:40:40 -0700 Subject: [PATCH 112/462] updated interface.py to use python3 super() --- daemon/core/location/mobility.py | 2 +- daemon/core/nodes/interface.py | 6 +++--- daemon/core/nodes/ipaddress.py | 4 ++-- daemon/core/nodes/physical.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 470b2a84..085962ff 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -302,7 +302,7 @@ class BasicRangeModel(WirelessModel): :param core.session.Session session: related core session :param int _id: object id """ - super().__init__(session=session, _id=_id) + super().__init__(session, _id) self.session = session self.wlan = session.get_node(_id) self._netifs = {} diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 4dde287a..84e8f399 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -232,7 +232,7 @@ class Veth(CoreInterface): :raises CoreCommandError: when there is a command exception """ # note that net arg is ignored - CoreInterface.__init__(self, session, node, name, mtu, server) + super().__init__(session, node, name, mtu, server) self.localname = localname self.up = False if start: @@ -293,7 +293,7 @@ class TunTap(CoreInterface): will run on, default is None for localhost :param bool start: start flag """ - CoreInterface.__init__(self, session, node, name, mtu, server) + super().__init__(session, node, name, mtu, server) self.localname = localname self.up = False self.transport_type = "virtual" @@ -476,7 +476,7 @@ class GreTap(CoreInterface): will run on, default is None for localhost :raises CoreCommandError: when there is a command exception """ - CoreInterface.__init__(self, session, node, name, mtu, server) + super().__init__(session, node, name, mtu, server) if _id is None: # from PyCoreObj _id = ((id(self) >> 16) ^ (id(self) & 0xFFFF)) & 0xFFFF diff --git a/daemon/core/nodes/ipaddress.py b/daemon/core/nodes/ipaddress.py index c0b4e209..d77b3a94 100644 --- a/daemon/core/nodes/ipaddress.py +++ b/daemon/core/nodes/ipaddress.py @@ -401,7 +401,7 @@ class Ipv4Prefix(IpPrefix): :param str prefixstr: ip prefix """ - IpPrefix.__init__(self, AF_INET, prefixstr) + super().__init__(AF_INET, prefixstr) class Ipv6Prefix(IpPrefix): @@ -415,7 +415,7 @@ class Ipv6Prefix(IpPrefix): :param str prefixstr: ip prefix """ - IpPrefix.__init__(self, AF_INET6, prefixstr) + super().__init__(AF_INET6, prefixstr) def is_ip_address(af, addrstr): diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index c1d6328b..4ed38470 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -19,7 +19,7 @@ class PhysicalNode(CoreNodeBase): def __init__( self, session, _id=None, name=None, nodedir=None, start=True, server=None ): - CoreNodeBase.__init__(self, session, _id, name, start, server) + super().__init__(session, _id, name, start, server) if not self.server: raise CoreError("physical nodes must be assigned to a remote server") self.nodedir = nodedir From 711104df64ce7c1b6976416896fcc32a25c366c5 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 23 Oct 2019 10:56:01 -0700 Subject: [PATCH 113/462] update to move mac learning disable into wlan and hub node startup --- daemon/core/nodes/network.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 756de0e2..80a730e2 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -1001,23 +1001,14 @@ class HubNode(CoreNetwork): policy = "ACCEPT" type = "hub" - def __init__(self, session, _id=None, name=None, start=True, server=None): + def startup(self): """ - Creates a HubNode instance. + Startup for a hub node, that disables mac learning after normal startup. - :param core.session.Session session: core session instance - :param int _id: node id - :param str name: node namee - :param bool start: start flag - :param core.emulator.distributed.DistributedServer server: remote server node - will run on, default is None for localhost - :raises CoreCommandError: when there is a command exception + :return: nothing """ - super().__init__(session, _id, name, start, server) - - # TODO: move to startup method - if start: - self.net_client.disable_mac_learning(self.brname) + super().startup() + self.net_client.disable_mac_learning(self.brname) class WlanNode(CoreNetwork): @@ -1045,14 +1036,18 @@ class WlanNode(CoreNetwork): :param policy: wlan policy """ super().__init__(session, _id, name, start, server, policy) - # wireless model such as basic range + # wireless and mobility models (BasicRangeModel, Ns2WaypointMobility) self.model = None - # mobility model such as scripted self.mobility = None - # TODO: move to startup method - if start: - self.net_client.disable_mac_learning(self.brname) + def startup(self): + """ + Startup for a wlan node, that disables mac learning after normal startup. + + :return: nothing + """ + super().startup() + self.net_client.disable_mac_learning(self.brname) def attach(self, netif): """ From 053d2a0b10693bb6052d5d00b21fba98a806089d Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 23 Oct 2019 11:01:25 -0700 Subject: [PATCH 114/462] removed unused utils.hex_dump --- daemon/core/utils.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/daemon/core/utils.py b/daemon/core/utils.py index 407258be..e16efd1f 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -223,34 +223,6 @@ def cmd(args, env=None, cwd=None, wait=True, shell=False): raise CoreCommandError(-1, args) -def hex_dump(s, bytes_per_word=2, words_per_line=8): - """ - Hex dump of a string. - - :param str s: string to hex dump - :param bytes_per_word: number of bytes per word - :param words_per_line: number of words per line - :return: hex dump of string - """ - dump = "" - count = 0 - total_bytes = bytes_per_word * words_per_line - - while s: - line = s[:total_bytes] - s = s[total_bytes:] - tmp = map( - lambda x: (f"{bytes_per_word:02x}" * bytes_per_word) % x, - zip(*[iter(map(ord, line))] * bytes_per_word), - ) - if len(line) % 2: - tmp.append(f"{ord(line[-1]):x}") - tmp = " ".join(tmp) - dump += f"0x{count:08x}: {tmp}\n" - count += len(line) - return dump[:-1] - - def file_munge(pathname, header, text): """ Insert text at the end of a file, surrounded by header comments. From 7366738023b73f4e5578711ed16727571ed809de Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 23 Oct 2019 11:24:50 -0700 Subject: [PATCH 115/462] updated network to network link to not look for Rj45, since that wont happen, removed unwanted grpc client function --- daemon/core/api/grpc/client.py | 12 ------------ daemon/core/emulator/session.py | 6 +----- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 1f98c985..6b5343d8 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -204,18 +204,6 @@ class CoreGrpcClient: request = core_pb2.GetSessionOptionsRequest(session_id=session_id) return self.stub.GetSessionOptions(request) - def get_session_options_group(self, session_id): - """ - Retrieve session options in a group list. - - :param int session_id: id of session - :return: response with a list of configuration groups - :rtype: core_pb2.GetSessionOptionsGroupResponse - :raises grpc.RpcError: when session doesn't exist - """ - request = core_pb2.GetSessionOptionsGroupRequest(session_id=session_id) - return self.stub.GetSessionOptionsGroup(request) - def set_session_options(self, session_id, config): """ Set options for a session. diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 287e4b3c..9d8dd77f 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -355,11 +355,7 @@ class Session: net_one.name, net_two.name, ) - if isinstance(net_two, Rj45Node): - interface = net_two.linknet(net_one) - else: - interface = net_one.linknet(net_two) - + interface = net_one.linknet(net_two) link_config(net_one, interface, link_options) if not link_options.unidirectional: From d28a64b53c416a0c00755702f153792d0635f64b Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 23 Oct 2019 12:31:13 -0700 Subject: [PATCH 116/462] removed todo for wireless links, since they may be used by an API, until know for sure, leaving in --- daemon/core/emulator/session.py | 1 - 1 file changed, 1 deletion(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 9d8dd77f..93a1e7d6 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -240,7 +240,6 @@ class Session: ) return node_one, node_two, net_one, net_two, tunnel - # TODO: this doesn't appear to ever be used, EMANE or basic wireless range def _link_wireless(self, objects, connect): """ Objects to deal with when connecting/disconnecting wireless links. From ade1d980a8e52d963ef162a087b2340835e6b4d4 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 23 Oct 2019 23:15:19 -0700 Subject: [PATCH 117/462] updates to speed up tests --- daemon/tests/conftest.py | 122 +++++---- daemon/tests/test_core.py | 10 +- daemon/tests/test_grpc.py | 10 +- daemon/tests/test_gui.py | 485 +++++++++++++++++----------------- daemon/tests/test_nodes.py | 11 - daemon/tests/test_services.py | 21 +- 6 files changed, 338 insertions(+), 321 deletions(-) diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index f8949299..2d5911a9 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -2,10 +2,10 @@ Unit test fixture module. """ -import os import threading import time +import mock import pytest from mock.mock import MagicMock @@ -18,7 +18,7 @@ from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes from core.emulator.enumerations import CORE_API_PORT, ConfigTlvs, EventTlvs, EventTypes from core.nodes import ipaddress -from core.services.coreservices import ServiceManager +from core.nodes.base import CoreNode EMANE_SERVICES = "zebra|OSPFv3MDR|IPForward" @@ -86,7 +86,7 @@ class CoreServerTest: (ConfigTlvs.OBJECT, "location"), (ConfigTlvs.TYPE, 0), (ConfigTlvs.DATA_TYPES, (9, 9, 9, 9, 9, 9)), - (ConfigTlvs.VALUES, "0|0| 47.5766974863|-122.125920191|0.0|150.0"), + (ConfigTlvs.VALUES, "0|0|47.5766974863|-122.125920191|0.0|150.0"), ], ) self.request_handler.handle_message(message) @@ -109,82 +109,108 @@ class CoreServerTest: self.server.server_close() -@pytest.fixture -def grpc_server(): - coremu = CoreEmu() - grpc_server = CoreGrpcServer(coremu) +class PatchManager: + def __init__(self): + self.patches = [] + + def patch_obj(self, _cls, attribute): + p = mock.patch.object(_cls, attribute) + p.start() + self.patches.append(p) + + def patch(self, func): + p = mock.patch(func) + p.start() + self.patches.append(p) + + def shutdown(self): + for p in self.patches: + p.stop() + + +@pytest.fixture(scope="module") +def module_grpc(global_coreemu): + grpc_server = CoreGrpcServer(global_coreemu) thread = threading.Thread(target=grpc_server.listen, args=("localhost:50051",)) thread.daemon = True thread.start() time.sleep(0.1) yield grpc_server - coremu.shutdown() grpc_server.server.stop(None) @pytest.fixture -def session(): - # use coreemu and create a session +def grpc_server(module_grpc): + yield module_grpc + module_grpc.coreemu.shutdown() + + +@pytest.fixture(scope="session") +def global_coreemu(): coreemu = CoreEmu(config={"emane_prefix": "/usr"}) - session_fixture = coreemu.create_session() - session_fixture.set_state(EventTypes.CONFIGURATION_STATE) - assert os.path.exists(session_fixture.session_dir) - - # return created session - yield session_fixture - - # clear session configurations - session_fixture.location.reset() - session_fixture.services.reset() - session_fixture.mobility.config_reset() - session_fixture.emane.config_reset() - - # shutdown coreemu + yield coreemu coreemu.shutdown() - # clear services, since they will be reloaded - ServiceManager.services.clear() + +@pytest.fixture(scope="session") +def global_session(request, global_coreemu): + patch_manager = PatchManager() + if request.config.getoption("mock"): + patch_manager.patch("os.mkdir") + patch_manager.patch("core.utils.cmd") + patch_manager.patch("core.nodes.netclient.get_net_client") + patch_manager.patch_obj(CoreNode, "nodefile") + session = global_coreemu.create_session(_id=1000) + yield session + patch_manager.shutdown() + session.shutdown() -@pytest.fixture(scope="module") +@pytest.fixture +def session(global_session): + global_session.write_state = MagicMock() + global_session.set_state(EventTypes.CONFIGURATION_STATE) + yield global_session + global_session.clear() + global_session.location.reset() + global_session.services.reset() + global_session.mobility.config_reset() + global_session.emane.config_reset() + + +@pytest.fixture(scope="session") def ip_prefixes(): return IpPrefixes(ip4_prefix="10.83.0.0/16") -@pytest.fixture(scope="module") +@pytest.fixture(scope="session") def interface_helper(): return InterfaceHelper(ip4_prefix="10.83.0.0/16") -@pytest.fixture() -def cored(): - # create and return server - server = CoreServerTest() - yield server - - # cleanup - server.shutdown() - - # cleanup services - ServiceManager.services.clear() - - -@pytest.fixture() -def coreserver(): - # create and return server +@pytest.fixture(scope="module") +def module_cored(): server = CoreServerTest() server.setup_handler() yield server - - # cleanup server.shutdown() - # cleanup services - ServiceManager.services.clear() + +@pytest.fixture +def cored(module_cored): + session = module_cored.session + module_cored.server.coreemu.sessions[session.id] = session + yield module_cored + session.clear() + session.location.reset() + session.services.reset() + session.mobility.config_reset() + session.emane.config_reset() def pytest_addoption(parser): parser.addoption("--distributed", help="distributed server address") + parser.addoption("--mock", action="store_true", help="run without mocking") def pytest_generate_tests(metafunc): diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 321bca7b..adad4cbf 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -20,7 +20,7 @@ _WIRED = [NodeTypes.PEER_TO_PEER, NodeTypes.HUB, NodeTypes.SWITCH] def ping(from_node, to_node, ip_prefixes): address = ip_prefixes.ip4_address(to_node) try: - from_node.cmd(f"ping -c 3 {address}") + from_node.cmd(f"ping -c 1 {address}") status = 0 except CoreCommandError as e: status = e.returncode @@ -57,14 +57,14 @@ class TestCore: status = ping(node_one, node_two, ip_prefixes) assert not status - def test_vnode_client(self, session, ip_prefixes): + def test_vnode_client(self, request, session, ip_prefixes): """ Test vnode client methods. + :param request: pytest request :param session: session for test :param ip_prefixes: generates ip addresses for nodes """ - # create ptp ptp_node = session.add_node(_type=NodeTypes.PEER_TO_PEER) @@ -87,7 +87,8 @@ class TestCore: assert client.connected() # validate command - assert client.check_cmd("echo hello") == "hello" + if not request.config.getoption("mock"): + assert client.check_cmd("echo hello") == "hello" def test_netif(self, session, ip_prefixes): """ @@ -163,6 +164,7 @@ class TestCore: status = ping(node_one, node_two, ip_prefixes) assert not status + @pytest.mark.skip def test_mobility(self, session, ip_prefixes): """ Test basic wlan network. diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 30e2daea..2415176a 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -240,7 +240,10 @@ class TestGrpc: with pytest.raises(CoreError): assert session.get_node(node.id) - def test_node_command(self, grpc_server): + def test_node_command(self, request, grpc_server): + if request.config.getoption("mock"): + pytest.skip("mocking calls") + # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() @@ -834,7 +837,10 @@ class TestGrpc: # then queue.get(timeout=5) - def test_throughputs(self, grpc_server): + def test_throughputs(self, request, grpc_server): + if request.config.getoption("mock"): + pytest.skip("mocking calls") + # given client = CoreGrpcClient() grpc_server.coreemu.create_session() diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index 40c025ee..58a6d257 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -6,6 +6,7 @@ import time import mock import pytest +from mock import MagicMock from core.api.tlv import coreapi from core.emane.ieee80211abg import EmaneIeee80211abgModel @@ -45,7 +46,7 @@ class TestGui: (NodeTypes.RJ45, None), ], ) - def test_node_add(self, coreserver, node_type, model): + def test_node_add(self, cored, node_type, model): node_id = 1 message = coreapi.CoreNodeMessage.create( MessageFlags.ADD.value, @@ -59,13 +60,13 @@ class TestGui: ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - assert coreserver.session.get_node(node_id) is not None + assert cored.session.get_node(node_id) is not None - def test_node_update(self, coreserver): + def test_node_update(self, cored): node_id = 1 - coreserver.session.add_node(_id=node_id) + cored.session.add_node(_id=node_id) x = 50 y = 100 message = coreapi.CoreNodeMessage.create( @@ -77,30 +78,30 @@ class TestGui: ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - node = coreserver.session.get_node(node_id) + node = cored.session.get_node(node_id) assert node is not None assert node.position.x == x assert node.position.y == y - def test_node_delete(self, coreserver): + def test_node_delete(self, cored): node_id = 1 - coreserver.session.add_node(_id=node_id) + cored.session.add_node(_id=node_id) message = coreapi.CoreNodeMessage.create( MessageFlags.DELETE.value, [(NodeTlvs.NUMBER, node_id)] ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) with pytest.raises(CoreError): - coreserver.session.get_node(node_id) + cored.session.get_node(node_id) - def test_link_add_node_to_net(self, coreserver): + def test_link_add_node_to_net(self, cored): node_one = 1 - coreserver.session.add_node(_id=node_one) + cored.session.add_node(_id=node_one) switch = 2 - coreserver.session.add_node(_id=switch, _type=NodeTypes.SWITCH) + cored.session.add_node(_id=switch, _type=NodeTypes.SWITCH) ip_prefix = Ipv4Prefix("10.0.0.0/24") interface_one = ip_prefix.addr(node_one) message = coreapi.CoreLinkMessage.create( @@ -114,17 +115,17 @@ class TestGui: ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - switch_node = coreserver.session.get_node(switch) + switch_node = cored.session.get_node(switch) all_links = switch_node.all_link_data(0) assert len(all_links) == 1 - def test_link_add_net_to_node(self, coreserver): + def test_link_add_net_to_node(self, cored): node_one = 1 - coreserver.session.add_node(_id=node_one) + cored.session.add_node(_id=node_one) switch = 2 - coreserver.session.add_node(_id=switch, _type=NodeTypes.SWITCH) + cored.session.add_node(_id=switch, _type=NodeTypes.SWITCH) ip_prefix = Ipv4Prefix("10.0.0.0/24") interface_one = ip_prefix.addr(node_one) message = coreapi.CoreLinkMessage.create( @@ -138,17 +139,17 @@ class TestGui: ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - switch_node = coreserver.session.get_node(switch) + switch_node = cored.session.get_node(switch) all_links = switch_node.all_link_data(0) assert len(all_links) == 1 - def test_link_add_node_to_node(self, coreserver): + def test_link_add_node_to_node(self, cored): node_one = 1 - coreserver.session.add_node(_id=node_one) + cored.session.add_node(_id=node_one) node_two = 2 - coreserver.session.add_node(_id=node_two) + cored.session.add_node(_id=node_two) ip_prefix = Ipv4Prefix("10.0.0.0/24") interface_one = ip_prefix.addr(node_one) interface_two = ip_prefix.addr(node_two) @@ -166,19 +167,19 @@ class TestGui: ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) all_links = [] - for node_id in coreserver.session.nodes: - node = coreserver.session.nodes[node_id] + for node_id in cored.session.nodes: + node = cored.session.nodes[node_id] all_links += node.all_link_data(0) assert len(all_links) == 1 - def test_link_update(self, coreserver): + def test_link_update(self, cored): node_one = 1 - coreserver.session.add_node(_id=node_one) + cored.session.add_node(_id=node_one) switch = 2 - coreserver.session.add_node(_id=switch, _type=NodeTypes.SWITCH) + cored.session.add_node(_id=switch, _type=NodeTypes.SWITCH) ip_prefix = Ipv4Prefix("10.0.0.0/24") interface_one = ip_prefix.addr(node_one) message = coreapi.CoreLinkMessage.create( @@ -191,8 +192,8 @@ class TestGui: (LinkTlvs.INTERFACE1_IP4_MASK, 24), ], ) - coreserver.request_handler.handle_message(message) - switch_node = coreserver.session.get_node(switch) + cored.request_handler.handle_message(message) + switch_node = cored.session.get_node(switch) all_links = switch_node.all_link_data(0) assert len(all_links) == 1 link = all_links[0] @@ -208,19 +209,19 @@ class TestGui: (LinkTlvs.BANDWIDTH, bandwidth), ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - switch_node = coreserver.session.get_node(switch) + switch_node = cored.session.get_node(switch) all_links = switch_node.all_link_data(0) assert len(all_links) == 1 link = all_links[0] assert link.bandwidth == bandwidth - def test_link_delete_node_to_node(self, coreserver): + def test_link_delete_node_to_node(self, cored): node_one = 1 - coreserver.session.add_node(_id=node_one) + cored.session.add_node(_id=node_one) node_two = 2 - coreserver.session.add_node(_id=node_two) + cored.session.add_node(_id=node_two) ip_prefix = Ipv4Prefix("10.0.0.0/24") interface_one = ip_prefix.addr(node_one) interface_two = ip_prefix.addr(node_two) @@ -236,10 +237,10 @@ class TestGui: (LinkTlvs.INTERFACE2_IP4_MASK, 24), ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) all_links = [] - for node_id in coreserver.session.nodes: - node = coreserver.session.nodes[node_id] + for node_id in cored.session.nodes: + node = cored.session.nodes[node_id] all_links += node.all_link_data(0) assert len(all_links) == 1 @@ -252,19 +253,19 @@ class TestGui: (LinkTlvs.INTERFACE2_NUMBER, 0), ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) all_links = [] - for node_id in coreserver.session.nodes: - node = coreserver.session.nodes[node_id] + for node_id in cored.session.nodes: + node = cored.session.nodes[node_id] all_links += node.all_link_data(0) assert len(all_links) == 0 - def test_link_delete_node_to_net(self, coreserver): + def test_link_delete_node_to_net(self, cored): node_one = 1 - coreserver.session.add_node(_id=node_one) + cored.session.add_node(_id=node_one) switch = 2 - coreserver.session.add_node(_id=switch, _type=NodeTypes.SWITCH) + cored.session.add_node(_id=switch, _type=NodeTypes.SWITCH) ip_prefix = Ipv4Prefix("10.0.0.0/24") interface_one = ip_prefix.addr(node_one) message = coreapi.CoreLinkMessage.create( @@ -277,8 +278,8 @@ class TestGui: (LinkTlvs.INTERFACE1_IP4_MASK, 24), ], ) - coreserver.request_handler.handle_message(message) - switch_node = coreserver.session.get_node(switch) + cored.request_handler.handle_message(message) + switch_node = cored.session.get_node(switch) all_links = switch_node.all_link_data(0) assert len(all_links) == 1 @@ -290,17 +291,17 @@ class TestGui: (LinkTlvs.INTERFACE1_NUMBER, 0), ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - switch_node = coreserver.session.get_node(switch) + switch_node = cored.session.get_node(switch) all_links = switch_node.all_link_data(0) assert len(all_links) == 0 - def test_link_delete_net_to_node(self, coreserver): + def test_link_delete_net_to_node(self, cored): node_one = 1 - coreserver.session.add_node(_id=node_one) + cored.session.add_node(_id=node_one) switch = 2 - coreserver.session.add_node(_id=switch, _type=NodeTypes.SWITCH) + cored.session.add_node(_id=switch, _type=NodeTypes.SWITCH) ip_prefix = Ipv4Prefix("10.0.0.0/24") interface_one = ip_prefix.addr(node_one) message = coreapi.CoreLinkMessage.create( @@ -313,8 +314,8 @@ class TestGui: (LinkTlvs.INTERFACE1_IP4_MASK, 24), ], ) - coreserver.request_handler.handle_message(message) - switch_node = coreserver.session.get_node(switch) + cored.request_handler.handle_message(message) + switch_node = cored.session.get_node(switch) all_links = switch_node.all_link_data(0) assert len(all_links) == 1 @@ -326,58 +327,58 @@ class TestGui: (LinkTlvs.INTERFACE2_NUMBER, 0), ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - switch_node = coreserver.session.get_node(switch) + switch_node = cored.session.get_node(switch) all_links = switch_node.all_link_data(0) assert len(all_links) == 0 - def test_session_update(self, coreserver): - session_id = coreserver.session.id + def test_session_update(self, cored): + session_id = cored.session.id name = "test" message = coreapi.CoreSessionMessage.create( 0, [(SessionTlvs.NUMBER, str(session_id)), (SessionTlvs.NAME, name)] ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - assert coreserver.session.name == name + assert cored.session.name == name - def test_session_query(self, coreserver): - coreserver.request_handler.dispatch_replies = mock.MagicMock() + def test_session_query(self, cored): + cored.request_handler.dispatch_replies = mock.MagicMock() message = coreapi.CoreSessionMessage.create(MessageFlags.STRING.value, []) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - args, _ = coreserver.request_handler.dispatch_replies.call_args + args, _ = cored.request_handler.dispatch_replies.call_args replies = args[0] assert len(replies) == 1 - def test_session_join(self, coreserver): - coreserver.request_handler.dispatch_replies = mock.MagicMock() - session_id = coreserver.session.id + def test_session_join(self, cored): + cored.request_handler.dispatch_replies = mock.MagicMock() + session_id = cored.session.id message = coreapi.CoreSessionMessage.create( MessageFlags.ADD.value, [(SessionTlvs.NUMBER, str(session_id))] ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - assert coreserver.request_handler.session.id == session_id + assert cored.request_handler.session.id == session_id - def test_session_delete(self, coreserver): - assert len(coreserver.server.coreemu.sessions) == 1 - session_id = coreserver.session.id + def test_session_delete(self, cored): + assert len(cored.server.coreemu.sessions) == 1 + session_id = cored.session.id message = coreapi.CoreSessionMessage.create( MessageFlags.DELETE.value, [(SessionTlvs.NUMBER, str(session_id))] ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - assert len(coreserver.server.coreemu.sessions) == 0 + assert len(cored.server.coreemu.sessions) == 0 - def test_file_hook_add(self, coreserver): + def test_file_hook_add(self, cored): state = EventTypes.DATACOLLECT_STATE.value - assert coreserver.session._hooks.get(state) is None + assert cored.session._hooks.get(state) is None file_name = "test.sh" file_data = "echo hello" message = coreapi.CoreFileMessage.create( @@ -389,16 +390,16 @@ class TestGui: ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - hooks = coreserver.session._hooks.get(state) + hooks = cored.session._hooks.get(state) assert len(hooks) == 1 name, data = hooks[0] assert file_name == name assert file_data == data - def test_file_service_file_set(self, coreserver): - node = coreserver.session.add_node() + def test_file_service_file_set(self, cored): + node = cored.session.add_node() service = "DefaultRoute" file_name = "defaultroute.sh" file_data = "echo hello" @@ -412,16 +413,14 @@ class TestGui: ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - service_file = coreserver.session.services.get_service_file( - node, service, file_name - ) + service_file = cored.session.services.get_service_file(node, service, file_name) assert file_data == service_file.data - def test_file_node_file_copy(self, coreserver): + def test_file_node_file_copy(self, request, cored): file_name = "/var/log/test/node.log" - node = coreserver.session.add_node() + node = cored.session.add_node() node.makenodedir() file_data = "echo hello" message = coreapi.CoreFileMessage.create( @@ -433,17 +432,17 @@ class TestGui: ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - directory, basename = os.path.split(file_name) - created_directory = directory[1:].replace("/", ".") - create_path = os.path.join(node.nodedir, created_directory, basename) - assert os.path.exists(create_path) + if not request.config.getoption("mock"): + directory, basename = os.path.split(file_name) + created_directory = directory[1:].replace("/", ".") + create_path = os.path.join(node.nodedir, created_directory, basename) + assert os.path.exists(create_path) - def test_exec_node_tty(self, coreserver): - coreserver.request_handler.dispatch_replies = mock.MagicMock() - node = coreserver.session.add_node() - node.startup() + def test_exec_node_tty(self, cored): + cored.request_handler.dispatch_replies = mock.MagicMock() + node = cored.session.add_node() message = coreapi.CoreExecMessage.create( MessageFlags.TTY.value, [ @@ -453,49 +452,51 @@ class TestGui: ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - args, _ = coreserver.request_handler.dispatch_replies.call_args + args, _ = cored.request_handler.dispatch_replies.call_args replies = args[0] assert len(replies) == 1 - def test_exec_local_command(self, coreserver): - coreserver.request_handler.dispatch_replies = mock.MagicMock() - node = coreserver.session.add_node() - node.startup() + def test_exec_local_command(self, request, cored): + if request.config.getoption("mock"): + pytest.skip("mocking calls") + + cored.request_handler.dispatch_replies = mock.MagicMock() + node = cored.session.add_node() + cmd = "echo hello" message = coreapi.CoreExecMessage.create( MessageFlags.TEXT.value | MessageFlags.LOCAL.value, [ (ExecuteTlvs.NODE, node.id), (ExecuteTlvs.NUMBER, 1), - (ExecuteTlvs.COMMAND, "echo hello"), + (ExecuteTlvs.COMMAND, cmd), ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - args, _ = coreserver.request_handler.dispatch_replies.call_args + args, _ = cored.request_handler.dispatch_replies.call_args replies = args[0] assert len(replies) == 1 - def test_exec_node_command(self, coreserver): - coreserver.request_handler.dispatch_replies = mock.MagicMock() - node = coreserver.session.add_node() - node.startup() + def test_exec_node_command(self, cored): + cored.request_handler.dispatch_replies = mock.MagicMock() + node = cored.session.add_node() + cmd = "echo hello" message = coreapi.CoreExecMessage.create( MessageFlags.TEXT.value, [ (ExecuteTlvs.NODE, node.id), (ExecuteTlvs.NUMBER, 1), - (ExecuteTlvs.COMMAND, "echo hello"), + (ExecuteTlvs.COMMAND, cmd), ], ) + node.cmd = MagicMock(return_value="hello") - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - args, _ = coreserver.request_handler.dispatch_replies.call_args - replies = args[0] - assert len(replies) == 1 + node.cmd.assert_called_with(cmd) @pytest.mark.parametrize( "state", @@ -507,16 +508,16 @@ class TestGui: EventTypes.DEFINITION_STATE, ], ) - def test_event_state(self, coreserver, state): + def test_event_state(self, cored, state): message = coreapi.CoreEventMessage.create(0, [(EventTlvs.TYPE, state.value)]) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - assert coreserver.session.state == state.value + assert cored.session.state == state.value - def test_event_schedule(self, coreserver): - coreserver.session.add_event = mock.MagicMock() - node = coreserver.session.add_node() + def test_event_schedule(self, cored): + cored.session.add_event = mock.MagicMock() + node = cored.session.add_node() message = coreapi.CoreEventMessage.create( MessageFlags.ADD.value, [ @@ -528,37 +529,37 @@ class TestGui: ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - coreserver.session.add_event.assert_called_once() + cored.session.add_event.assert_called_once() - def test_event_save_xml(self, coreserver, tmpdir): + def test_event_save_xml(self, cored, tmpdir): xml_file = tmpdir.join("session.xml") file_path = xml_file.strpath - coreserver.session.add_node() + cored.session.add_node() message = coreapi.CoreEventMessage.create( 0, [(EventTlvs.TYPE, EventTypes.FILE_SAVE.value), (EventTlvs.NAME, file_path)], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) assert os.path.exists(file_path) - def test_event_open_xml(self, coreserver, tmpdir): + def test_event_open_xml(self, cored, tmpdir): xml_file = tmpdir.join("session.xml") file_path = xml_file.strpath - node = coreserver.session.add_node() - coreserver.session.save_xml(file_path) - coreserver.session.delete_node(node.id) + node = cored.session.add_node() + cored.session.save_xml(file_path) + cored.session.delete_node(node.id) message = coreapi.CoreEventMessage.create( 0, [(EventTlvs.TYPE, EventTypes.FILE_OPEN.value), (EventTlvs.NAME, file_path)], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - assert coreserver.session.get_node(node.id) + assert cored.session.get_node(node.id) @pytest.mark.parametrize( "state", @@ -570,9 +571,9 @@ class TestGui: EventTypes.RECONFIGURE, ], ) - def test_event_service(self, coreserver, state): - coreserver.session.broadcast_event = mock.MagicMock() - node = coreserver.session.add_node() + def test_event_service(self, cored, state): + cored.session.broadcast_event = mock.MagicMock() + node = cored.session.add_node() node.startup() message = coreapi.CoreEventMessage.create( 0, @@ -583,9 +584,9 @@ class TestGui: ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - coreserver.session.broadcast_event.assert_called_once() + cored.session.broadcast_event.assert_called_once() @pytest.mark.parametrize( "state", @@ -597,37 +598,37 @@ class TestGui: EventTypes.RECONFIGURE, ], ) - def test_event_mobility(self, coreserver, state): + def test_event_mobility(self, cored, state): message = coreapi.CoreEventMessage.create( 0, [(EventTlvs.TYPE, state.value), (EventTlvs.NAME, "mobility:ns2script")] ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - def test_register_gui(self, coreserver): - coreserver.request_handler.master = False + def test_register_gui(self, cored): + cored.request_handler.master = False message = coreapi.CoreRegMessage.create(0, [(RegisterTlvs.GUI, "gui")]) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - assert coreserver.request_handler.master is True + assert cored.request_handler.master is True - def test_register_xml(self, coreserver, tmpdir): + def test_register_xml(self, cored, tmpdir): xml_file = tmpdir.join("session.xml") file_path = xml_file.strpath - node = coreserver.session.add_node() - coreserver.session.save_xml(file_path) - coreserver.session.delete_node(node.id) + node = cored.session.add_node() + cored.session.save_xml(file_path) + cored.session.delete_node(node.id) message = coreapi.CoreRegMessage.create( 0, [(RegisterTlvs.EXECUTE_SERVER, file_path)] ) - coreserver.session.instantiate() + cored.session.instantiate() - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - assert coreserver.server.coreemu.sessions[2].get_node(node.id) + assert cored.server.coreemu.sessions[2].get_node(node.id) - def test_register_python(self, coreserver, tmpdir): + def test_register_python(self, cored, tmpdir): xml_file = tmpdir.join("test.py") file_path = xml_file.strpath with open(file_path, "w") as f: @@ -637,14 +638,14 @@ class TestGui: message = coreapi.CoreRegMessage.create( 0, [(RegisterTlvs.EXECUTE_SERVER, file_path)] ) - coreserver.session.instantiate() + cored.session.instantiate() - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - assert len(coreserver.session.nodes) == 1 + assert len(cored.session.nodes) == 1 - def test_config_all(self, coreserver): - node = coreserver.session.add_node() + def test_config_all(self, cored): + node = cored.session.add_node() message = coreapi.CoreConfMessage.create( MessageFlags.ADD.value, [ @@ -653,13 +654,13 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.RESET.value), ], ) - coreserver.session.location.reset = mock.MagicMock() + cored.session.location.refxyz = (10, 10, 10) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - coreserver.session.location.reset.assert_called_once() + assert cored.session.location.refxyz == (0, 0, 0) - def test_config_options_request(self, coreserver): + def test_config_options_request(self, cored): message = coreapi.CoreConfMessage.create( 0, [ @@ -667,13 +668,13 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.REQUEST.value), ], ) - coreserver.request_handler.handle_broadcast_config = mock.MagicMock() + cored.request_handler.handle_broadcast_config = mock.MagicMock() - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - coreserver.request_handler.handle_broadcast_config.assert_called_once() + cored.request_handler.handle_broadcast_config.assert_called_once() - def test_config_options_update(self, coreserver): + def test_config_options_update(self, cored): test_key = "test" test_value = "test" values = {test_key: test_value} @@ -686,11 +687,11 @@ class TestGui: ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - assert coreserver.session.options.get_config(test_key) == test_value + assert cored.session.options.get_config(test_key) == test_value - def test_config_location_reset(self, coreserver): + def test_config_location_reset(self, cored): message = coreapi.CoreConfMessage.create( 0, [ @@ -698,13 +699,13 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.RESET.value), ], ) - coreserver.session.location.refxyz = (10, 10, 10) + cored.session.location.refxyz = (10, 10, 10) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - assert coreserver.session.location.refxyz == (0, 0, 0) + assert cored.session.location.refxyz == (0, 0, 0) - def test_config_location_update(self, coreserver): + def test_config_location_update(self, cored): message = coreapi.CoreConfMessage.create( 0, [ @@ -714,13 +715,13 @@ class TestGui: ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - assert coreserver.session.location.refxyz == (10, 10, 0.0) - assert coreserver.session.location.refgeo == (70, 50, 0) - assert coreserver.session.location.refscale == 0.5 + assert cored.session.location.refxyz == (10, 10, 0.0) + assert cored.session.location.refgeo == (70, 50, 0) + assert cored.session.location.refscale == 0.5 - def test_config_metadata_request(self, coreserver): + def test_config_metadata_request(self, cored): message = coreapi.CoreConfMessage.create( 0, [ @@ -728,13 +729,13 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.REQUEST.value), ], ) - coreserver.request_handler.handle_broadcast_config = mock.MagicMock() + cored.request_handler.handle_broadcast_config = mock.MagicMock() - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - coreserver.request_handler.handle_broadcast_config.assert_called_once() + cored.request_handler.handle_broadcast_config.assert_called_once() - def test_config_metadata_update(self, coreserver): + def test_config_metadata_update(self, cored): test_key = "test" test_value = "test" values = {test_key: test_value} @@ -747,11 +748,11 @@ class TestGui: ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - assert coreserver.session.metadata.get_config(test_key) == test_value + assert cored.session.metadata.get_config(test_key) == test_value - def test_config_broker_request(self, coreserver): + def test_config_broker_request(self, cored): server = "test" host = "10.0.0.1" port = 50000 @@ -763,13 +764,13 @@ class TestGui: (ConfigTlvs.VALUES, f"{server}:{host}:{port}"), ], ) - coreserver.session.distributed.add_server = mock.MagicMock() + cored.session.distributed.add_server = mock.MagicMock() - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - coreserver.session.distributed.add_server.assert_called_once_with(server, host) + cored.session.distributed.add_server.assert_called_once_with(server, host) - def test_config_services_request_all(self, coreserver): + def test_config_services_request_all(self, cored): message = coreapi.CoreConfMessage.create( 0, [ @@ -777,14 +778,14 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.REQUEST.value), ], ) - coreserver.request_handler.handle_broadcast_config = mock.MagicMock() + cored.request_handler.handle_broadcast_config = mock.MagicMock() - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - coreserver.request_handler.handle_broadcast_config.assert_called_once() + cored.request_handler.handle_broadcast_config.assert_called_once() - def test_config_services_request_specific(self, coreserver): - node = coreserver.session.add_node() + def test_config_services_request_specific(self, cored): + node = cored.session.add_node() message = coreapi.CoreConfMessage.create( 0, [ @@ -794,14 +795,14 @@ class TestGui: (ConfigTlvs.OPAQUE, "service:DefaultRoute"), ], ) - coreserver.request_handler.handle_broadcast_config = mock.MagicMock() + cored.request_handler.handle_broadcast_config = mock.MagicMock() - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - coreserver.request_handler.handle_broadcast_config.assert_called_once() + cored.request_handler.handle_broadcast_config.assert_called_once() - def test_config_services_request_specific_file(self, coreserver): - node = coreserver.session.add_node() + def test_config_services_request_specific_file(self, cored): + node = cored.session.add_node() message = coreapi.CoreConfMessage.create( 0, [ @@ -811,16 +812,16 @@ class TestGui: (ConfigTlvs.OPAQUE, "service:DefaultRoute:defaultroute.sh"), ], ) - coreserver.session.broadcast_file = mock.MagicMock() + cored.session.broadcast_file = mock.MagicMock() - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - coreserver.session.broadcast_file.assert_called_once() + cored.session.broadcast_file.assert_called_once() - def test_config_services_reset(self, coreserver): - node = coreserver.session.add_node() + def test_config_services_reset(self, cored): + node = cored.session.add_node() service = "DefaultRoute" - coreserver.session.services.set_service(node.id, service) + cored.session.services.set_service(node.id, service) message = coreapi.CoreConfMessage.create( 0, [ @@ -828,14 +829,14 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.RESET.value), ], ) - assert coreserver.session.services.get_service(node.id, service) is not None + assert cored.session.services.get_service(node.id, service) is not None - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - assert coreserver.session.services.get_service(node.id, service) is None + assert cored.session.services.get_service(node.id, service) is None - def test_config_services_set(self, coreserver): - node = coreserver.session.add_node() + def test_config_services_set(self, cored): + node = cored.session.add_node() service = "DefaultRoute" values = {"meta": "metadata"} message = coreapi.CoreConfMessage.create( @@ -848,14 +849,14 @@ class TestGui: (ConfigTlvs.VALUES, dict_to_str(values)), ], ) - assert coreserver.session.services.get_service(node.id, service) is None + assert cored.session.services.get_service(node.id, service) is None - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - assert coreserver.session.services.get_service(node.id, service) is not None + assert cored.session.services.get_service(node.id, service) is not None - def test_config_mobility_reset(self, coreserver): - wlan = coreserver.session.add_node(_type=NodeTypes.WIRELESS_LAN) + def test_config_mobility_reset(self, cored): + wlan = cored.session.add_node(_type=NodeTypes.WIRELESS_LAN) message = coreapi.CoreConfMessage.create( 0, [ @@ -863,15 +864,15 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.RESET.value), ], ) - coreserver.session.mobility.set_model_config(wlan.id, BasicRangeModel.name, {}) - assert len(coreserver.session.mobility.node_configurations) == 1 + cored.session.mobility.set_model_config(wlan.id, BasicRangeModel.name, {}) + assert len(cored.session.mobility.node_configurations) == 1 - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - assert len(coreserver.session.mobility.node_configurations) == 0 + assert len(cored.session.mobility.node_configurations) == 0 - def test_config_mobility_model_request(self, coreserver): - wlan = coreserver.session.add_node(_type=NodeTypes.WIRELESS_LAN) + def test_config_mobility_model_request(self, cored): + wlan = cored.session.add_node(_type=NodeTypes.WIRELESS_LAN) message = coreapi.CoreConfMessage.create( 0, [ @@ -880,14 +881,14 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.REQUEST.value), ], ) - coreserver.request_handler.handle_broadcast_config = mock.MagicMock() + cored.request_handler.handle_broadcast_config = mock.MagicMock() - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - coreserver.request_handler.handle_broadcast_config.assert_called_once() + cored.request_handler.handle_broadcast_config.assert_called_once() - def test_config_mobility_model_update(self, coreserver): - wlan = coreserver.session.add_node(_type=NodeTypes.WIRELESS_LAN) + def test_config_mobility_model_update(self, cored): + wlan = cored.session.add_node(_type=NodeTypes.WIRELESS_LAN) config_key = "range" config_value = "1000" values = {config_key: config_value} @@ -901,15 +902,13 @@ class TestGui: ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - config = coreserver.session.mobility.get_model_config( - wlan.id, BasicRangeModel.name - ) + config = cored.session.mobility.get_model_config(wlan.id, BasicRangeModel.name) assert config[config_key] == config_value - def test_config_emane_model_request(self, coreserver): - wlan = coreserver.session.add_node(_type=NodeTypes.WIRELESS_LAN) + def test_config_emane_model_request(self, cored): + wlan = cored.session.add_node(_type=NodeTypes.WIRELESS_LAN) message = coreapi.CoreConfMessage.create( 0, [ @@ -918,14 +917,14 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.REQUEST.value), ], ) - coreserver.request_handler.handle_broadcast_config = mock.MagicMock() + cored.request_handler.handle_broadcast_config = mock.MagicMock() - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - coreserver.request_handler.handle_broadcast_config.assert_called_once() + cored.request_handler.handle_broadcast_config.assert_called_once() - def test_config_emane_model_update(self, coreserver): - wlan = coreserver.session.add_node(_type=NodeTypes.WIRELESS_LAN) + def test_config_emane_model_update(self, cored): + wlan = cored.session.add_node(_type=NodeTypes.WIRELESS_LAN) config_key = "distance" config_value = "50051" values = {config_key: config_value} @@ -939,14 +938,14 @@ class TestGui: ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - config = coreserver.session.emane.get_model_config( + config = cored.session.emane.get_model_config( wlan.id, EmaneIeee80211abgModel.name ) assert config[config_key] == config_value - def test_config_emane_request(self, coreserver): + def test_config_emane_request(self, cored): message = coreapi.CoreConfMessage.create( 0, [ @@ -954,13 +953,13 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.REQUEST.value), ], ) - coreserver.request_handler.handle_broadcast_config = mock.MagicMock() + cored.request_handler.handle_broadcast_config = mock.MagicMock() - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - coreserver.request_handler.handle_broadcast_config.assert_called_once() + cored.request_handler.handle_broadcast_config.assert_called_once() - def test_config_emane_update(self, coreserver): + def test_config_emane_update(self, cored): config_key = "eventservicedevice" config_value = "eth4" values = {config_key: config_value} @@ -973,7 +972,7 @@ class TestGui: ], ) - coreserver.request_handler.handle_message(message) + cored.request_handler.handle_message(message) - config = coreserver.session.emane.get_configs() + config = cored.session.emane.get_configs() assert config[config_key] == config_value diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index 76b5b85d..71d1a157 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -1,15 +1,10 @@ -import os -import time - import pytest -from core import utils from core.emulator.emudata import NodeOptions from core.emulator.enumerations import NodeTypes from core.errors import CoreError MODELS = ["router", "host", "PC", "mdr"] - NET_TYPES = [NodeTypes.SWITCH, NodeTypes.HUB, NodeTypes.WIRELESS_LAN] @@ -22,15 +17,10 @@ class TestNodes: # when node = session.add_node(options=options) - # give time for node services to boot - time.sleep(1) - # then assert node - assert os.path.exists(node.nodedir) assert node.alive() assert node.up - assert node.cmd("ip address show lo") def test_node_update(self, session): # given @@ -67,4 +57,3 @@ class TestNodes: # then assert node assert node.up - assert utils.cmd(f"brctl show {node.brname}") diff --git a/daemon/tests/test_services.py b/daemon/tests/test_services.py index e2a3f32a..489a9ab7 100644 --- a/daemon/tests/test_services.py +++ b/daemon/tests/test_services.py @@ -1,7 +1,9 @@ import os import pytest +from mock import MagicMock +from core.errors import CoreCommandError from core.services.coreservices import CoreService, ServiceDependencies, ServiceManager _PATH = os.path.abspath(os.path.dirname(__file__)) @@ -88,7 +90,7 @@ class TestServices: assert node.services assert len(node.services) == total_service + 2 - def test_service_file(self, session): + def test_service_file(self, request, session): # given ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_ONE) @@ -100,7 +102,8 @@ class TestServices: session.services.create_service_files(node, my_service) # then - assert os.path.exists(file_path) + if not request.config.getoption("mock"): + assert os.path.exists(file_path) def test_service_validate(self, session): # given @@ -121,6 +124,7 @@ class TestServices: my_service = ServiceManager.get(SERVICE_TWO) node = session.add_node() session.services.create_service_files(node, my_service) + node.cmd = MagicMock(side_effect=CoreCommandError(-1, "invalid")) # when status = session.services.validate_service(node, my_service) @@ -147,6 +151,7 @@ class TestServices: my_service = ServiceManager.get(SERVICE_TWO) node = session.add_node() session.services.create_service_files(node, my_service) + node.cmd = MagicMock(side_effect=CoreCommandError(-1, "invalid")) # when status = session.services.startup_service(node, my_service, wait=True) @@ -173,6 +178,7 @@ class TestServices: my_service = ServiceManager.get(SERVICE_TWO) node = session.add_node() session.services.create_service_files(node, my_service) + node.cmd = MagicMock(side_effect=CoreCommandError(-1, "invalid")) # when status = session.services.stop_service(node, my_service) @@ -216,17 +222,6 @@ class TestServices: custom_service_two = session.services.get_service(node_two.id, my_service.name) session.services.create_service_files(node_two, custom_service_two) - # then - file_path_one = node_one.hostfilename(file_name) - assert os.path.exists(file_path_one) - with open(file_path_one, "r") as custom_file: - assert custom_file.read() == file_data_one - - file_path_two = node_two.hostfilename(file_name) - assert os.path.exists(file_path_two) - with open(file_path_two, "r") as custom_file: - assert custom_file.read() == file_data_two - def test_service_import(self): """ Test importing a custom service. From 4a6a87b93145572b2c4c3f7c0c7d44d7389034ee Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 24 Oct 2019 09:06:14 -0700 Subject: [PATCH 118/462] updates to how test fixtures are created --- daemon/tests/conftest.py | 90 ++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 41 deletions(-) diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index 2d5911a9..e28037b7 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -17,6 +17,7 @@ from core.api.tlv.coreserver import CoreServer from core.emulator.coreemu import CoreEmu from core.emulator.emudata import IpPrefixes from core.emulator.enumerations import CORE_API_PORT, ConfigTlvs, EventTlvs, EventTypes +from core.emulator.session import Session from core.nodes import ipaddress from core.nodes.base import CoreNode @@ -128,56 +129,34 @@ class PatchManager: p.stop() -@pytest.fixture(scope="module") -def module_grpc(global_coreemu): - grpc_server = CoreGrpcServer(global_coreemu) - thread = threading.Thread(target=grpc_server.listen, args=("localhost:50051",)) - thread.daemon = True - thread.start() - time.sleep(0.1) - yield grpc_server - grpc_server.server.stop(None) - - -@pytest.fixture -def grpc_server(module_grpc): - yield module_grpc - module_grpc.coreemu.shutdown() - - @pytest.fixture(scope="session") -def global_coreemu(): - coreemu = CoreEmu(config={"emane_prefix": "/usr"}) - yield coreemu - coreemu.shutdown() - - -@pytest.fixture(scope="session") -def global_session(request, global_coreemu): +def patcher(request): patch_manager = PatchManager() if request.config.getoption("mock"): patch_manager.patch("os.mkdir") patch_manager.patch("core.utils.cmd") patch_manager.patch("core.nodes.netclient.get_net_client") patch_manager.patch_obj(CoreNode, "nodefile") - session = global_coreemu.create_session(_id=1000) - yield session + patch_manager.patch_obj(Session, "write_state") + yield patch_manager patch_manager.shutdown() + + +@pytest.fixture(scope="session") +def global_coreemu(patcher): + coreemu = CoreEmu(config={"emane_prefix": "/usr"}) + yield coreemu + coreemu.shutdown() + + +@pytest.fixture(scope="session") +def global_session(request, patcher, global_coreemu): + mkdir = not request.config.getoption("mock") + session = Session(1000, {"emane_prefix": "/usr"}, mkdir) + yield session session.shutdown() -@pytest.fixture -def session(global_session): - global_session.write_state = MagicMock() - global_session.set_state(EventTypes.CONFIGURATION_STATE) - yield global_session - global_session.clear() - global_session.location.reset() - global_session.services.reset() - global_session.mobility.config_reset() - global_session.emane.config_reset() - - @pytest.fixture(scope="session") def ip_prefixes(): return IpPrefixes(ip4_prefix="10.83.0.0/16") @@ -189,13 +168,42 @@ def interface_helper(): @pytest.fixture(scope="module") -def module_cored(): - server = CoreServerTest() +def module_grpc(global_coreemu): + grpc_server = CoreGrpcServer(global_coreemu) + thread = threading.Thread(target=grpc_server.listen, args=("localhost:50051",)) + thread.daemon = True + thread.start() + time.sleep(0.1) + yield grpc_server + grpc_server.server.stop(None) + + +@pytest.fixture(scope="module") +def module_cored(request, patcher): + mkdir = not request.config.getoption("mock") + server = CoreServerTest(mkdir) server.setup_handler() yield server server.shutdown() +@pytest.fixture +def grpc_server(module_grpc): + yield module_grpc + module_grpc.coreemu.shutdown() + + +@pytest.fixture +def session(global_session): + global_session.set_state(EventTypes.CONFIGURATION_STATE) + yield global_session + global_session.clear() + global_session.location.reset() + global_session.services.reset() + global_session.mobility.config_reset() + global_session.emane.config_reset() + + @pytest.fixture def cored(module_cored): session = module_cored.session From 27be86f1757ff2ebd6c2f295e742b3fa3a6971a3 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 24 Oct 2019 09:26:28 -0700 Subject: [PATCH 119/462] fixed unwanted patcher in test fixture --- daemon/tests/conftest.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index e28037b7..b57c1d6c 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -179,9 +179,8 @@ def module_grpc(global_coreemu): @pytest.fixture(scope="module") -def module_cored(request, patcher): - mkdir = not request.config.getoption("mock") - server = CoreServerTest(mkdir) +def module_cored(patcher): + server = CoreServerTest() server.setup_handler() yield server server.shutdown() From 6045908a144606e51ea3324c44c22c7205cfac67 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 24 Oct 2019 10:58:26 -0700 Subject: [PATCH 120/462] cleanup for test fixtures to help simplify test_gui fixture setup --- daemon/core/emulator/coreemu.py | 3 +- daemon/core/emulator/session.py | 1 + daemon/tests/conftest.py | 30 +- daemon/tests/test_gui.py | 468 ++++++++++++++++---------------- 4 files changed, 259 insertions(+), 243 deletions(-) diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py index 237000c7..754ab771 100644 --- a/daemon/core/emulator/coreemu.py +++ b/daemon/core/emulator/coreemu.py @@ -49,7 +49,7 @@ class CoreEmu: self.config = config # session management - self.session_id_gen = IdGen(_id=0) + self.session_id_gen = IdGen() self.sessions = {} # load services @@ -79,6 +79,7 @@ class CoreEmu: :return: nothing """ logging.info("shutting down all sessions") + self.session_id_gen.id = 0 sessions = self.sessions.copy() self.sessions.clear() for _id in sessions: diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 93a1e7d6..2a2c4f11 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -1368,6 +1368,7 @@ class Session: while self.nodes: _, node = self.nodes.popitem() node.shutdown() + self.node_id_gen.id = 0 def write_nodes(self): """ diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index b57c1d6c..99d641da 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -129,6 +129,12 @@ class PatchManager: p.stop() +class MockServer: + def __init__(self, config, coreemu): + self.config = config + self.coreemu = coreemu + + @pytest.fixture(scope="session") def patcher(request): patch_manager = PatchManager() @@ -138,6 +144,7 @@ def patcher(request): patch_manager.patch("core.nodes.netclient.get_net_client") patch_manager.patch_obj(CoreNode, "nodefile") patch_manager.patch_obj(Session, "write_state") + patch_manager.patch_obj(Session, "write_nodes") yield patch_manager patch_manager.shutdown() @@ -179,11 +186,14 @@ def module_grpc(global_coreemu): @pytest.fixture(scope="module") -def module_cored(patcher): - server = CoreServerTest() - server.setup_handler() - yield server - server.shutdown() +def module_coretlv(patcher, global_coreemu, global_session): + request_mock = MagicMock() + request_mock.fileno = MagicMock(return_value=1) + server = MockServer({"numthreads": "1"}, global_coreemu) + request_handler = CoreHandler(request_mock, "", server) + request_handler.session = global_session + request_handler.add_session_handlers() + yield request_handler @pytest.fixture @@ -204,10 +214,12 @@ def session(global_session): @pytest.fixture -def cored(module_cored): - session = module_cored.session - module_cored.server.coreemu.sessions[session.id] = session - yield module_cored +def coretlv(module_coretlv): + session = module_coretlv.session + coreemu = module_coretlv.coreemu + coreemu.sessions[session.id] = session + yield module_coretlv + coreemu.shutdown() session.clear() session.location.reset() session.services.reset() diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index 58a6d257..21c14c2c 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -43,10 +43,9 @@ class TestGui: (NodeTypes.SWITCH, None), (NodeTypes.WIRELESS_LAN, None), (NodeTypes.TUNNEL, None), - (NodeTypes.RJ45, None), ], ) - def test_node_add(self, cored, node_type, model): + def test_node_add(self, coretlv, node_type, model): node_id = 1 message = coreapi.CoreNodeMessage.create( MessageFlags.ADD.value, @@ -60,13 +59,13 @@ class TestGui: ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - assert cored.session.get_node(node_id) is not None + assert coretlv.session.get_node(node_id) is not None - def test_node_update(self, cored): + def test_node_update(self, coretlv): node_id = 1 - cored.session.add_node(_id=node_id) + coretlv.session.add_node(_id=node_id) x = 50 y = 100 message = coreapi.CoreNodeMessage.create( @@ -78,30 +77,30 @@ class TestGui: ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - node = cored.session.get_node(node_id) + node = coretlv.session.get_node(node_id) assert node is not None assert node.position.x == x assert node.position.y == y - def test_node_delete(self, cored): + def test_node_delete(self, coretlv): node_id = 1 - cored.session.add_node(_id=node_id) + coretlv.session.add_node(_id=node_id) message = coreapi.CoreNodeMessage.create( MessageFlags.DELETE.value, [(NodeTlvs.NUMBER, node_id)] ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) with pytest.raises(CoreError): - cored.session.get_node(node_id) + coretlv.session.get_node(node_id) - def test_link_add_node_to_net(self, cored): + def test_link_add_node_to_net(self, coretlv): node_one = 1 - cored.session.add_node(_id=node_one) + coretlv.session.add_node(_id=node_one) switch = 2 - cored.session.add_node(_id=switch, _type=NodeTypes.SWITCH) + coretlv.session.add_node(_id=switch, _type=NodeTypes.SWITCH) ip_prefix = Ipv4Prefix("10.0.0.0/24") interface_one = ip_prefix.addr(node_one) message = coreapi.CoreLinkMessage.create( @@ -115,17 +114,17 @@ class TestGui: ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - switch_node = cored.session.get_node(switch) + switch_node = coretlv.session.get_node(switch) all_links = switch_node.all_link_data(0) assert len(all_links) == 1 - def test_link_add_net_to_node(self, cored): + def test_link_add_net_to_node(self, coretlv): node_one = 1 - cored.session.add_node(_id=node_one) + coretlv.session.add_node(_id=node_one) switch = 2 - cored.session.add_node(_id=switch, _type=NodeTypes.SWITCH) + coretlv.session.add_node(_id=switch, _type=NodeTypes.SWITCH) ip_prefix = Ipv4Prefix("10.0.0.0/24") interface_one = ip_prefix.addr(node_one) message = coreapi.CoreLinkMessage.create( @@ -139,17 +138,17 @@ class TestGui: ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - switch_node = cored.session.get_node(switch) + switch_node = coretlv.session.get_node(switch) all_links = switch_node.all_link_data(0) assert len(all_links) == 1 - def test_link_add_node_to_node(self, cored): + def test_link_add_node_to_node(self, coretlv): node_one = 1 - cored.session.add_node(_id=node_one) + coretlv.session.add_node(_id=node_one) node_two = 2 - cored.session.add_node(_id=node_two) + coretlv.session.add_node(_id=node_two) ip_prefix = Ipv4Prefix("10.0.0.0/24") interface_one = ip_prefix.addr(node_one) interface_two = ip_prefix.addr(node_two) @@ -167,19 +166,19 @@ class TestGui: ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) all_links = [] - for node_id in cored.session.nodes: - node = cored.session.nodes[node_id] + for node_id in coretlv.session.nodes: + node = coretlv.session.nodes[node_id] all_links += node.all_link_data(0) assert len(all_links) == 1 - def test_link_update(self, cored): + def test_link_update(self, coretlv): node_one = 1 - cored.session.add_node(_id=node_one) + coretlv.session.add_node(_id=node_one) switch = 2 - cored.session.add_node(_id=switch, _type=NodeTypes.SWITCH) + coretlv.session.add_node(_id=switch, _type=NodeTypes.SWITCH) ip_prefix = Ipv4Prefix("10.0.0.0/24") interface_one = ip_prefix.addr(node_one) message = coreapi.CoreLinkMessage.create( @@ -192,8 +191,8 @@ class TestGui: (LinkTlvs.INTERFACE1_IP4_MASK, 24), ], ) - cored.request_handler.handle_message(message) - switch_node = cored.session.get_node(switch) + coretlv.handle_message(message) + switch_node = coretlv.session.get_node(switch) all_links = switch_node.all_link_data(0) assert len(all_links) == 1 link = all_links[0] @@ -209,19 +208,19 @@ class TestGui: (LinkTlvs.BANDWIDTH, bandwidth), ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - switch_node = cored.session.get_node(switch) + switch_node = coretlv.session.get_node(switch) all_links = switch_node.all_link_data(0) assert len(all_links) == 1 link = all_links[0] assert link.bandwidth == bandwidth - def test_link_delete_node_to_node(self, cored): + def test_link_delete_node_to_node(self, coretlv): node_one = 1 - cored.session.add_node(_id=node_one) + coretlv.session.add_node(_id=node_one) node_two = 2 - cored.session.add_node(_id=node_two) + coretlv.session.add_node(_id=node_two) ip_prefix = Ipv4Prefix("10.0.0.0/24") interface_one = ip_prefix.addr(node_one) interface_two = ip_prefix.addr(node_two) @@ -237,10 +236,10 @@ class TestGui: (LinkTlvs.INTERFACE2_IP4_MASK, 24), ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) all_links = [] - for node_id in cored.session.nodes: - node = cored.session.nodes[node_id] + for node_id in coretlv.session.nodes: + node = coretlv.session.nodes[node_id] all_links += node.all_link_data(0) assert len(all_links) == 1 @@ -253,19 +252,19 @@ class TestGui: (LinkTlvs.INTERFACE2_NUMBER, 0), ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) all_links = [] - for node_id in cored.session.nodes: - node = cored.session.nodes[node_id] + for node_id in coretlv.session.nodes: + node = coretlv.session.nodes[node_id] all_links += node.all_link_data(0) assert len(all_links) == 0 - def test_link_delete_node_to_net(self, cored): + def test_link_delete_node_to_net(self, coretlv): node_one = 1 - cored.session.add_node(_id=node_one) + coretlv.session.add_node(_id=node_one) switch = 2 - cored.session.add_node(_id=switch, _type=NodeTypes.SWITCH) + coretlv.session.add_node(_id=switch, _type=NodeTypes.SWITCH) ip_prefix = Ipv4Prefix("10.0.0.0/24") interface_one = ip_prefix.addr(node_one) message = coreapi.CoreLinkMessage.create( @@ -278,8 +277,8 @@ class TestGui: (LinkTlvs.INTERFACE1_IP4_MASK, 24), ], ) - cored.request_handler.handle_message(message) - switch_node = cored.session.get_node(switch) + coretlv.handle_message(message) + switch_node = coretlv.session.get_node(switch) all_links = switch_node.all_link_data(0) assert len(all_links) == 1 @@ -291,17 +290,17 @@ class TestGui: (LinkTlvs.INTERFACE1_NUMBER, 0), ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - switch_node = cored.session.get_node(switch) + switch_node = coretlv.session.get_node(switch) all_links = switch_node.all_link_data(0) assert len(all_links) == 0 - def test_link_delete_net_to_node(self, cored): + def test_link_delete_net_to_node(self, coretlv): node_one = 1 - cored.session.add_node(_id=node_one) + coretlv.session.add_node(_id=node_one) switch = 2 - cored.session.add_node(_id=switch, _type=NodeTypes.SWITCH) + coretlv.session.add_node(_id=switch, _type=NodeTypes.SWITCH) ip_prefix = Ipv4Prefix("10.0.0.0/24") interface_one = ip_prefix.addr(node_one) message = coreapi.CoreLinkMessage.create( @@ -314,8 +313,8 @@ class TestGui: (LinkTlvs.INTERFACE1_IP4_MASK, 24), ], ) - cored.request_handler.handle_message(message) - switch_node = cored.session.get_node(switch) + coretlv.handle_message(message) + switch_node = coretlv.session.get_node(switch) all_links = switch_node.all_link_data(0) assert len(all_links) == 1 @@ -327,58 +326,58 @@ class TestGui: (LinkTlvs.INTERFACE2_NUMBER, 0), ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - switch_node = cored.session.get_node(switch) + switch_node = coretlv.session.get_node(switch) all_links = switch_node.all_link_data(0) assert len(all_links) == 0 - def test_session_update(self, cored): - session_id = cored.session.id + def test_session_update(self, coretlv): + session_id = coretlv.session.id name = "test" message = coreapi.CoreSessionMessage.create( 0, [(SessionTlvs.NUMBER, str(session_id)), (SessionTlvs.NAME, name)] ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - assert cored.session.name == name + assert coretlv.session.name == name - def test_session_query(self, cored): - cored.request_handler.dispatch_replies = mock.MagicMock() + def test_session_query(self, coretlv): + coretlv.dispatch_replies = mock.MagicMock() message = coreapi.CoreSessionMessage.create(MessageFlags.STRING.value, []) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - args, _ = cored.request_handler.dispatch_replies.call_args + args, _ = coretlv.dispatch_replies.call_args replies = args[0] assert len(replies) == 1 - def test_session_join(self, cored): - cored.request_handler.dispatch_replies = mock.MagicMock() - session_id = cored.session.id + def test_session_join(self, coretlv): + coretlv.dispatch_replies = mock.MagicMock() + session_id = coretlv.session.id message = coreapi.CoreSessionMessage.create( MessageFlags.ADD.value, [(SessionTlvs.NUMBER, str(session_id))] ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - assert cored.request_handler.session.id == session_id + assert coretlv.session.id == session_id - def test_session_delete(self, cored): - assert len(cored.server.coreemu.sessions) == 1 - session_id = cored.session.id + def test_session_delete(self, coretlv): + assert len(coretlv.coreemu.sessions) == 1 + session_id = coretlv.session.id message = coreapi.CoreSessionMessage.create( MessageFlags.DELETE.value, [(SessionTlvs.NUMBER, str(session_id))] ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - assert len(cored.server.coreemu.sessions) == 0 + assert len(coretlv.coreemu.sessions) == 0 - def test_file_hook_add(self, cored): + def test_file_hook_add(self, coretlv): state = EventTypes.DATACOLLECT_STATE.value - assert cored.session._hooks.get(state) is None + assert coretlv.session._hooks.get(state) is None file_name = "test.sh" file_data = "echo hello" message = coreapi.CoreFileMessage.create( @@ -390,16 +389,16 @@ class TestGui: ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - hooks = cored.session._hooks.get(state) + hooks = coretlv.session._hooks.get(state) assert len(hooks) == 1 name, data = hooks[0] assert file_name == name assert file_data == data - def test_file_service_file_set(self, cored): - node = cored.session.add_node() + def test_file_service_file_set(self, coretlv): + node = coretlv.session.add_node() service = "DefaultRoute" file_name = "defaultroute.sh" file_data = "echo hello" @@ -413,14 +412,16 @@ class TestGui: ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - service_file = cored.session.services.get_service_file(node, service, file_name) + service_file = coretlv.session.services.get_service_file( + node, service, file_name + ) assert file_data == service_file.data - def test_file_node_file_copy(self, request, cored): + def test_file_node_file_copy(self, request, coretlv): file_name = "/var/log/test/node.log" - node = cored.session.add_node() + node = coretlv.session.add_node() node.makenodedir() file_data = "echo hello" message = coreapi.CoreFileMessage.create( @@ -432,7 +433,7 @@ class TestGui: ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) if not request.config.getoption("mock"): directory, basename = os.path.split(file_name) @@ -440,9 +441,9 @@ class TestGui: create_path = os.path.join(node.nodedir, created_directory, basename) assert os.path.exists(create_path) - def test_exec_node_tty(self, cored): - cored.request_handler.dispatch_replies = mock.MagicMock() - node = cored.session.add_node() + def test_exec_node_tty(self, coretlv): + coretlv.dispatch_replies = mock.MagicMock() + node = coretlv.session.add_node() message = coreapi.CoreExecMessage.create( MessageFlags.TTY.value, [ @@ -452,18 +453,18 @@ class TestGui: ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - args, _ = cored.request_handler.dispatch_replies.call_args + args, _ = coretlv.dispatch_replies.call_args replies = args[0] assert len(replies) == 1 - def test_exec_local_command(self, request, cored): + def test_exec_local_command(self, request, coretlv): if request.config.getoption("mock"): pytest.skip("mocking calls") - cored.request_handler.dispatch_replies = mock.MagicMock() - node = cored.session.add_node() + coretlv.dispatch_replies = mock.MagicMock() + node = coretlv.session.add_node() cmd = "echo hello" message = coreapi.CoreExecMessage.create( MessageFlags.TEXT.value | MessageFlags.LOCAL.value, @@ -474,15 +475,15 @@ class TestGui: ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - args, _ = cored.request_handler.dispatch_replies.call_args + args, _ = coretlv.dispatch_replies.call_args replies = args[0] assert len(replies) == 1 - def test_exec_node_command(self, cored): - cored.request_handler.dispatch_replies = mock.MagicMock() - node = cored.session.add_node() + def test_exec_node_command(self, coretlv): + coretlv.dispatch_replies = mock.MagicMock() + node = coretlv.session.add_node() cmd = "echo hello" message = coreapi.CoreExecMessage.create( MessageFlags.TEXT.value, @@ -494,7 +495,7 @@ class TestGui: ) node.cmd = MagicMock(return_value="hello") - cored.request_handler.handle_message(message) + coretlv.handle_message(message) node.cmd.assert_called_with(cmd) @@ -508,16 +509,16 @@ class TestGui: EventTypes.DEFINITION_STATE, ], ) - def test_event_state(self, cored, state): + def test_event_state(self, coretlv, state): message = coreapi.CoreEventMessage.create(0, [(EventTlvs.TYPE, state.value)]) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - assert cored.session.state == state.value + assert coretlv.session.state == state.value - def test_event_schedule(self, cored): - cored.session.add_event = mock.MagicMock() - node = cored.session.add_node() + def test_event_schedule(self, coretlv): + coretlv.session.add_event = mock.MagicMock() + node = coretlv.session.add_node() message = coreapi.CoreEventMessage.create( MessageFlags.ADD.value, [ @@ -529,37 +530,37 @@ class TestGui: ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - cored.session.add_event.assert_called_once() + coretlv.session.add_event.assert_called_once() - def test_event_save_xml(self, cored, tmpdir): - xml_file = tmpdir.join("session.xml") + def test_event_save_xml(self, coretlv, tmpdir): + xml_file = tmpdir.join("coretlv.session.xml") file_path = xml_file.strpath - cored.session.add_node() + coretlv.session.add_node() message = coreapi.CoreEventMessage.create( 0, [(EventTlvs.TYPE, EventTypes.FILE_SAVE.value), (EventTlvs.NAME, file_path)], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) assert os.path.exists(file_path) - def test_event_open_xml(self, cored, tmpdir): - xml_file = tmpdir.join("session.xml") + def test_event_open_xml(self, coretlv, tmpdir): + xml_file = tmpdir.join("coretlv.session.xml") file_path = xml_file.strpath - node = cored.session.add_node() - cored.session.save_xml(file_path) - cored.session.delete_node(node.id) + node = coretlv.session.add_node() + coretlv.session.save_xml(file_path) + coretlv.session.delete_node(node.id) message = coreapi.CoreEventMessage.create( 0, [(EventTlvs.TYPE, EventTypes.FILE_OPEN.value), (EventTlvs.NAME, file_path)], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - assert cored.session.get_node(node.id) + assert coretlv.session.get_node(node.id) @pytest.mark.parametrize( "state", @@ -571,10 +572,9 @@ class TestGui: EventTypes.RECONFIGURE, ], ) - def test_event_service(self, cored, state): - cored.session.broadcast_event = mock.MagicMock() - node = cored.session.add_node() - node.startup() + def test_event_service(self, coretlv, state): + coretlv.session.broadcast_event = mock.MagicMock() + node = coretlv.session.add_node() message = coreapi.CoreEventMessage.create( 0, [ @@ -584,9 +584,9 @@ class TestGui: ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - cored.session.broadcast_event.assert_called_once() + coretlv.session.broadcast_event.assert_called_once() @pytest.mark.parametrize( "state", @@ -598,54 +598,54 @@ class TestGui: EventTypes.RECONFIGURE, ], ) - def test_event_mobility(self, cored, state): + def test_event_mobility(self, coretlv, state): message = coreapi.CoreEventMessage.create( 0, [(EventTlvs.TYPE, state.value), (EventTlvs.NAME, "mobility:ns2script")] ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - def test_register_gui(self, cored): - cored.request_handler.master = False + def test_register_gui(self, coretlv): + coretlv.master = False message = coreapi.CoreRegMessage.create(0, [(RegisterTlvs.GUI, "gui")]) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - assert cored.request_handler.master is True + assert coretlv.master is True - def test_register_xml(self, cored, tmpdir): - xml_file = tmpdir.join("session.xml") + def test_register_xml(self, coretlv, tmpdir): + xml_file = tmpdir.join("coretlv.session.xml") file_path = xml_file.strpath - node = cored.session.add_node() - cored.session.save_xml(file_path) - cored.session.delete_node(node.id) + node = coretlv.session.add_node() + coretlv.session.save_xml(file_path) + coretlv.session.delete_node(node.id) message = coreapi.CoreRegMessage.create( 0, [(RegisterTlvs.EXECUTE_SERVER, file_path)] ) - cored.session.instantiate() + coretlv.session.instantiate() - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - assert cored.server.coreemu.sessions[2].get_node(node.id) + assert coretlv.coreemu.sessions[1].get_node(node.id) - def test_register_python(self, cored, tmpdir): + def test_register_python(self, coretlv, tmpdir): xml_file = tmpdir.join("test.py") file_path = xml_file.strpath with open(file_path, "w") as f: f.write("coreemu = globals()['coreemu']\n") - f.write("session = coreemu.sessions[1]\n") + f.write(f"session = coreemu.sessions[{coretlv.session.id}]\n") f.write("session.add_node()\n") message = coreapi.CoreRegMessage.create( 0, [(RegisterTlvs.EXECUTE_SERVER, file_path)] ) - cored.session.instantiate() + coretlv.session.instantiate() - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - assert len(cored.session.nodes) == 1 + assert len(coretlv.session.nodes) == 1 - def test_config_all(self, cored): - node = cored.session.add_node() + def test_config_all(self, coretlv): + node = coretlv.session.add_node() message = coreapi.CoreConfMessage.create( MessageFlags.ADD.value, [ @@ -654,13 +654,13 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.RESET.value), ], ) - cored.session.location.refxyz = (10, 10, 10) + coretlv.session.location.refxyz = (10, 10, 10) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - assert cored.session.location.refxyz == (0, 0, 0) + assert coretlv.session.location.refxyz == (0, 0, 0) - def test_config_options_request(self, cored): + def test_config_options_request(self, coretlv): message = coreapi.CoreConfMessage.create( 0, [ @@ -668,13 +668,13 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.REQUEST.value), ], ) - cored.request_handler.handle_broadcast_config = mock.MagicMock() + coretlv.handle_broadcast_config = mock.MagicMock() - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - cored.request_handler.handle_broadcast_config.assert_called_once() + coretlv.handle_broadcast_config.assert_called_once() - def test_config_options_update(self, cored): + def test_config_options_update(self, coretlv): test_key = "test" test_value = "test" values = {test_key: test_value} @@ -687,11 +687,11 @@ class TestGui: ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - assert cored.session.options.get_config(test_key) == test_value + assert coretlv.session.options.get_config(test_key) == test_value - def test_config_location_reset(self, cored): + def test_config_location_reset(self, coretlv): message = coreapi.CoreConfMessage.create( 0, [ @@ -699,13 +699,13 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.RESET.value), ], ) - cored.session.location.refxyz = (10, 10, 10) + coretlv.session.location.refxyz = (10, 10, 10) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - assert cored.session.location.refxyz == (0, 0, 0) + assert coretlv.session.location.refxyz == (0, 0, 0) - def test_config_location_update(self, cored): + def test_config_location_update(self, coretlv): message = coreapi.CoreConfMessage.create( 0, [ @@ -715,13 +715,13 @@ class TestGui: ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - assert cored.session.location.refxyz == (10, 10, 0.0) - assert cored.session.location.refgeo == (70, 50, 0) - assert cored.session.location.refscale == 0.5 + assert coretlv.session.location.refxyz == (10, 10, 0.0) + assert coretlv.session.location.refgeo == (70, 50, 0) + assert coretlv.session.location.refscale == 0.5 - def test_config_metadata_request(self, cored): + def test_config_metadata_request(self, coretlv): message = coreapi.CoreConfMessage.create( 0, [ @@ -729,13 +729,13 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.REQUEST.value), ], ) - cored.request_handler.handle_broadcast_config = mock.MagicMock() + coretlv.handle_broadcast_config = mock.MagicMock() - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - cored.request_handler.handle_broadcast_config.assert_called_once() + coretlv.handle_broadcast_config.assert_called_once() - def test_config_metadata_update(self, cored): + def test_config_metadata_update(self, coretlv): test_key = "test" test_value = "test" values = {test_key: test_value} @@ -748,11 +748,11 @@ class TestGui: ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - assert cored.session.metadata.get_config(test_key) == test_value + assert coretlv.session.metadata.get_config(test_key) == test_value - def test_config_broker_request(self, cored): + def test_config_broker_request(self, coretlv): server = "test" host = "10.0.0.1" port = 50000 @@ -764,13 +764,13 @@ class TestGui: (ConfigTlvs.VALUES, f"{server}:{host}:{port}"), ], ) - cored.session.distributed.add_server = mock.MagicMock() + coretlv.session.distributed.add_server = mock.MagicMock() - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - cored.session.distributed.add_server.assert_called_once_with(server, host) + coretlv.session.distributed.add_server.assert_called_once_with(server, host) - def test_config_services_request_all(self, cored): + def test_config_services_request_all(self, coretlv): message = coreapi.CoreConfMessage.create( 0, [ @@ -778,14 +778,14 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.REQUEST.value), ], ) - cored.request_handler.handle_broadcast_config = mock.MagicMock() + coretlv.handle_broadcast_config = mock.MagicMock() - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - cored.request_handler.handle_broadcast_config.assert_called_once() + coretlv.handle_broadcast_config.assert_called_once() - def test_config_services_request_specific(self, cored): - node = cored.session.add_node() + def test_config_services_request_specific(self, coretlv): + node = coretlv.session.add_node() message = coreapi.CoreConfMessage.create( 0, [ @@ -795,14 +795,14 @@ class TestGui: (ConfigTlvs.OPAQUE, "service:DefaultRoute"), ], ) - cored.request_handler.handle_broadcast_config = mock.MagicMock() + coretlv.handle_broadcast_config = mock.MagicMock() - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - cored.request_handler.handle_broadcast_config.assert_called_once() + coretlv.handle_broadcast_config.assert_called_once() - def test_config_services_request_specific_file(self, cored): - node = cored.session.add_node() + def test_config_services_request_specific_file(self, coretlv): + node = coretlv.session.add_node() message = coreapi.CoreConfMessage.create( 0, [ @@ -812,16 +812,16 @@ class TestGui: (ConfigTlvs.OPAQUE, "service:DefaultRoute:defaultroute.sh"), ], ) - cored.session.broadcast_file = mock.MagicMock() + coretlv.session.broadcast_file = mock.MagicMock() - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - cored.session.broadcast_file.assert_called_once() + coretlv.session.broadcast_file.assert_called_once() - def test_config_services_reset(self, cored): - node = cored.session.add_node() + def test_config_services_reset(self, coretlv): + node = coretlv.session.add_node() service = "DefaultRoute" - cored.session.services.set_service(node.id, service) + coretlv.session.services.set_service(node.id, service) message = coreapi.CoreConfMessage.create( 0, [ @@ -829,14 +829,14 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.RESET.value), ], ) - assert cored.session.services.get_service(node.id, service) is not None + assert coretlv.session.services.get_service(node.id, service) is not None - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - assert cored.session.services.get_service(node.id, service) is None + assert coretlv.session.services.get_service(node.id, service) is None - def test_config_services_set(self, cored): - node = cored.session.add_node() + def test_config_services_set(self, coretlv): + node = coretlv.session.add_node() service = "DefaultRoute" values = {"meta": "metadata"} message = coreapi.CoreConfMessage.create( @@ -849,14 +849,14 @@ class TestGui: (ConfigTlvs.VALUES, dict_to_str(values)), ], ) - assert cored.session.services.get_service(node.id, service) is None + assert coretlv.session.services.get_service(node.id, service) is None - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - assert cored.session.services.get_service(node.id, service) is not None + assert coretlv.session.services.get_service(node.id, service) is not None - def test_config_mobility_reset(self, cored): - wlan = cored.session.add_node(_type=NodeTypes.WIRELESS_LAN) + def test_config_mobility_reset(self, coretlv): + wlan = coretlv.session.add_node(_type=NodeTypes.WIRELESS_LAN) message = coreapi.CoreConfMessage.create( 0, [ @@ -864,15 +864,15 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.RESET.value), ], ) - cored.session.mobility.set_model_config(wlan.id, BasicRangeModel.name, {}) - assert len(cored.session.mobility.node_configurations) == 1 + coretlv.session.mobility.set_model_config(wlan.id, BasicRangeModel.name, {}) + assert len(coretlv.session.mobility.node_configurations) == 1 - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - assert len(cored.session.mobility.node_configurations) == 0 + assert len(coretlv.session.mobility.node_configurations) == 0 - def test_config_mobility_model_request(self, cored): - wlan = cored.session.add_node(_type=NodeTypes.WIRELESS_LAN) + def test_config_mobility_model_request(self, coretlv): + wlan = coretlv.session.add_node(_type=NodeTypes.WIRELESS_LAN) message = coreapi.CoreConfMessage.create( 0, [ @@ -881,14 +881,14 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.REQUEST.value), ], ) - cored.request_handler.handle_broadcast_config = mock.MagicMock() + coretlv.handle_broadcast_config = mock.MagicMock() - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - cored.request_handler.handle_broadcast_config.assert_called_once() + coretlv.handle_broadcast_config.assert_called_once() - def test_config_mobility_model_update(self, cored): - wlan = cored.session.add_node(_type=NodeTypes.WIRELESS_LAN) + def test_config_mobility_model_update(self, coretlv): + wlan = coretlv.session.add_node(_type=NodeTypes.WIRELESS_LAN) config_key = "range" config_value = "1000" values = {config_key: config_value} @@ -902,13 +902,15 @@ class TestGui: ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - config = cored.session.mobility.get_model_config(wlan.id, BasicRangeModel.name) + config = coretlv.session.mobility.get_model_config( + wlan.id, BasicRangeModel.name + ) assert config[config_key] == config_value - def test_config_emane_model_request(self, cored): - wlan = cored.session.add_node(_type=NodeTypes.WIRELESS_LAN) + def test_config_emane_model_request(self, coretlv): + wlan = coretlv.session.add_node(_type=NodeTypes.WIRELESS_LAN) message = coreapi.CoreConfMessage.create( 0, [ @@ -917,14 +919,14 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.REQUEST.value), ], ) - cored.request_handler.handle_broadcast_config = mock.MagicMock() + coretlv.handle_broadcast_config = mock.MagicMock() - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - cored.request_handler.handle_broadcast_config.assert_called_once() + coretlv.handle_broadcast_config.assert_called_once() - def test_config_emane_model_update(self, cored): - wlan = cored.session.add_node(_type=NodeTypes.WIRELESS_LAN) + def test_config_emane_model_update(self, coretlv): + wlan = coretlv.session.add_node(_type=NodeTypes.WIRELESS_LAN) config_key = "distance" config_value = "50051" values = {config_key: config_value} @@ -938,14 +940,14 @@ class TestGui: ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - config = cored.session.emane.get_model_config( + config = coretlv.session.emane.get_model_config( wlan.id, EmaneIeee80211abgModel.name ) assert config[config_key] == config_value - def test_config_emane_request(self, cored): + def test_config_emane_request(self, coretlv): message = coreapi.CoreConfMessage.create( 0, [ @@ -953,13 +955,13 @@ class TestGui: (ConfigTlvs.TYPE, ConfigFlags.REQUEST.value), ], ) - cored.request_handler.handle_broadcast_config = mock.MagicMock() + coretlv.handle_broadcast_config = mock.MagicMock() - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - cored.request_handler.handle_broadcast_config.assert_called_once() + coretlv.handle_broadcast_config.assert_called_once() - def test_config_emane_update(self, cored): + def test_config_emane_update(self, coretlv): config_key = "eventservicedevice" config_value = "eth4" values = {config_key: config_value} @@ -972,7 +974,7 @@ class TestGui: ], ) - cored.request_handler.handle_message(message) + coretlv.handle_message(message) - config = cored.session.emane.get_configs() + config = coretlv.session.emane.get_configs() assert config[config_key] == config_value From c255625d2fadf930ef816502f1572b8faeed274b Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 24 Oct 2019 11:20:38 -0700 Subject: [PATCH 121/462] removed skipping TestCore:test_mobility --- daemon/tests/test_core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index adad4cbf..80cf6787 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -164,7 +164,6 @@ class TestCore: status = ping(node_one, node_two, ip_prefixes) assert not status - @pytest.mark.skip def test_mobility(self, session, ip_prefixes): """ Test basic wlan network. From dc27fadb10f510f6c62d83a765fe4a8ec2a2b14c Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 24 Oct 2019 11:52:25 -0700 Subject: [PATCH 122/462] added distributed tests based on new distributed, removed old distributed tests and fixture to support it --- daemon/tests/conftest.py | 93 +---- daemon/tests/distributed/test_distributed.py | 352 ------------------- daemon/tests/test_distributed.py | 39 ++ 3 files changed, 42 insertions(+), 442 deletions(-) delete mode 100644 daemon/tests/distributed/test_distributed.py create mode 100644 daemon/tests/test_distributed.py diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index 99d641da..adfe3e24 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -11,105 +11,17 @@ from mock.mock import MagicMock from core.api.grpc.client import InterfaceHelper from core.api.grpc.server import CoreGrpcServer -from core.api.tlv.coreapi import CoreConfMessage, CoreEventMessage from core.api.tlv.corehandlers import CoreHandler -from core.api.tlv.coreserver import CoreServer from core.emulator.coreemu import CoreEmu +from core.emulator.distributed import DistributedServer from core.emulator.emudata import IpPrefixes -from core.emulator.enumerations import CORE_API_PORT, ConfigTlvs, EventTlvs, EventTypes +from core.emulator.enumerations import EventTypes from core.emulator.session import Session -from core.nodes import ipaddress from core.nodes.base import CoreNode EMANE_SERVICES = "zebra|OSPFv3MDR|IPForward" -class CoreServerTest: - def __init__(self, port=CORE_API_PORT): - self.host = "localhost" - self.port = port - address = (self.host, self.port) - self.server = CoreServer( - address, CoreHandler, {"numthreads": 1, "daemonize": False} - ) - - self.distributed_server = "core2" - self.prefix = ipaddress.Ipv4Prefix("10.83.0.0/16") - self.session = None - self.request_handler = None - - def setup_handler(self): - self.session = self.server.coreemu.create_session(1) - request_mock = MagicMock() - request_mock.fileno = MagicMock(return_value=1) - self.request_handler = CoreHandler(request_mock, "", self.server) - self.request_handler.session = self.session - self.request_handler.add_session_handlers() - - def setup(self, distributed_address): - # validate address - assert distributed_address, "distributed server address was not provided" - - # create session - self.session = self.server.coreemu.create_session(1) - - # create request handler - request_mock = MagicMock() - request_mock.fileno = MagicMock(return_value=1) - self.request_handler = CoreHandler(request_mock, "", self.server) - self.request_handler.session = self.session - self.request_handler.add_session_handlers() - - # have broker handle a configuration state change - self.session.set_state(EventTypes.DEFINITION_STATE) - message = CoreEventMessage.create( - 0, [(EventTlvs.TYPE, EventTypes.CONFIGURATION_STATE.value)] - ) - self.request_handler.handle_message(message) - - # add broker server for distributed core - distributed = f"{self.distributed_server}:{distributed_address}:{self.port}" - message = CoreConfMessage.create( - 0, - [ - (ConfigTlvs.OBJECT, "broker"), - (ConfigTlvs.TYPE, 0), - (ConfigTlvs.DATA_TYPES, (10,)), - (ConfigTlvs.VALUES, distributed), - ], - ) - self.request_handler.handle_message(message) - - # set session location - message = CoreConfMessage.create( - 0, - [ - (ConfigTlvs.OBJECT, "location"), - (ConfigTlvs.TYPE, 0), - (ConfigTlvs.DATA_TYPES, (9, 9, 9, 9, 9, 9)), - (ConfigTlvs.VALUES, "0|0|47.5766974863|-122.125920191|0.0|150.0"), - ], - ) - self.request_handler.handle_message(message) - - # set services for host nodes - message = CoreConfMessage.create( - 0, - [ - (ConfigTlvs.SESSION, str(self.session.id)), - (ConfigTlvs.OBJECT, "services"), - (ConfigTlvs.TYPE, 0), - (ConfigTlvs.DATA_TYPES, (10, 10, 10)), - (ConfigTlvs.VALUES, "host|DefaultRoute|SSH"), - ], - ) - self.request_handler.handle_message(message) - - def shutdown(self): - self.server.coreemu.shutdown() - self.server.server_close() - - class PatchManager: def __init__(self): self.patches = [] @@ -138,6 +50,7 @@ class MockServer: @pytest.fixture(scope="session") def patcher(request): patch_manager = PatchManager() + patch_manager.patch_obj(DistributedServer, "remote_cmd") if request.config.getoption("mock"): patch_manager.patch("os.mkdir") patch_manager.patch("core.utils.cmd") diff --git a/daemon/tests/distributed/test_distributed.py b/daemon/tests/distributed/test_distributed.py deleted file mode 100644 index c3d74365..00000000 --- a/daemon/tests/distributed/test_distributed.py +++ /dev/null @@ -1,352 +0,0 @@ -""" -Unit tests for testing CORE with distributed networks. -""" -from core.api.tlv.coreapi import ( - CoreConfMessage, - CoreEventMessage, - CoreExecMessage, - CoreLinkMessage, - CoreNodeMessage, -) -from core.emane.ieee80211abg import EmaneIeee80211abgModel -from core.emulator.enumerations import ( - ConfigFlags, - ConfigTlvs, - EventTlvs, - EventTypes, - ExecuteTlvs, - LinkTlvs, - LinkTypes, - MessageFlags, - NodeTlvs, - NodeTypes, -) -from core.nodes.ipaddress import IpAddress, MacAddress - - -def set_emane_model(node_id, model): - return CoreConfMessage.create( - 0, - [ - (ConfigTlvs.NODE, node_id), - (ConfigTlvs.OBJECT, model), - (ConfigTlvs.TYPE, ConfigFlags.UPDATE.value), - ], - ) - - -def node_message(_id, name, server=None, node_type=NodeTypes.DEFAULT, model=None): - """ - Convenience method for creating a node TLV messages. - - :param int _id: node id - :param str name: node name - :param str server: distributed server name, if desired - :param core.emulator.enumerations.NodeTypes node_type: node type - :param str model: model for node - :return: tlv message - :rtype: core.api.tlv.coreapi.CoreNodeMessage - """ - values = [ - (NodeTlvs.NUMBER, _id), - (NodeTlvs.TYPE, node_type.value), - (NodeTlvs.NAME, name), - (NodeTlvs.EMULATION_SERVER, server), - (NodeTlvs.X_POSITION, 0), - (NodeTlvs.Y_POSITION, 0), - ] - - if model: - values.append((NodeTlvs.MODEL, model)) - - return CoreNodeMessage.create(MessageFlags.ADD.value, values) - - -def link_message( - n1, - n2, - intf_one=None, - address_one=None, - intf_two=None, - address_two=None, - key=None, - mask=24, -): - """ - Convenience method for creating link TLV messages. - - :param int n1: node one id - :param int n2: node two id - :param int intf_one: node one interface id - :param core.nodes.ipaddress.IpAddress address_one: node one ip4 address - :param int intf_two: node two interface id - :param core.nodes.ipaddress.IpAddress address_two: node two ip4 address - :param int key: tunnel key for link if needed - :param int mask: ip4 mask to use for link - :return: tlv mesage - :rtype: core.api.tlv.coreapi.CoreLinkMessage - """ - mac_one, mac_two = None, None - if address_one: - mac_one = MacAddress.random() - if address_two: - mac_two = MacAddress.random() - - values = [ - (LinkTlvs.N1_NUMBER, n1), - (LinkTlvs.N2_NUMBER, n2), - (LinkTlvs.DELAY, 0), - (LinkTlvs.BANDWIDTH, 0), - (LinkTlvs.PER, "0"), - (LinkTlvs.DUP, "0"), - (LinkTlvs.JITTER, 0), - (LinkTlvs.TYPE, LinkTypes.WIRED.value), - (LinkTlvs.INTERFACE1_NUMBER, intf_one), - (LinkTlvs.INTERFACE1_IP4, address_one), - (LinkTlvs.INTERFACE1_IP4_MASK, mask), - (LinkTlvs.INTERFACE1_MAC, mac_one), - (LinkTlvs.INTERFACE2_NUMBER, intf_two), - (LinkTlvs.INTERFACE2_IP4, address_two), - (LinkTlvs.INTERFACE2_IP4_MASK, mask), - (LinkTlvs.INTERFACE2_MAC, mac_two), - ] - - if key: - values.append((LinkTlvs.KEY, key)) - - return CoreLinkMessage.create(MessageFlags.ADD.value, values) - - -def command_message(node, command): - """ - Create an execute command TLV message. - - :param node: node to execute command for - :param command: command to execute - :return: tlv message - :rtype: core.api.tlv.coreapi.CoreExecMessage - """ - flags = MessageFlags.STRING.value | MessageFlags.TEXT.value - return CoreExecMessage.create( - flags, - [ - (ExecuteTlvs.NODE, node.id), - (ExecuteTlvs.NUMBER, 1), - (ExecuteTlvs.COMMAND, command), - ], - ) - - -def state_message(state): - """ - Create a event TLV message for a new state. - - :param core.enumerations.EventTypes state: state to create message for - :return: tlv message - :rtype: core.api.tlv.coreapi.CoreEventMessage - """ - return CoreEventMessage.create(0, [(EventTlvs.TYPE, state.value)]) - - -def validate_response(replies, _): - """ - Patch method for handling dispatch replies within a CoreRequestHandler to validate a response. - - :param tuple replies: replies to handle - :param _: nothing - :return: nothing - """ - response = replies[0] - header = response[: CoreExecMessage.header_len] - tlv_data = response[CoreExecMessage.header_len :] - response = CoreExecMessage(MessageFlags.TEXT, header, tlv_data) - assert not response.get_tlv(ExecuteTlvs.STATUS.value) - - -class TestDistributed: - def test_switch(self, cored, distributed_address): - """ - Test creating a distributed switch network. - - :param core.api.tlv.coreserver.CoreServer conftest.Core cored: core daemon server to test with - :param str distributed_address: distributed server to test against - """ - # initialize server for testing - cored.setup(distributed_address) - - # create local node - message = node_message(_id=1, name="n1", model="host") - cored.request_handler.handle_message(message) - - # create distributed node and assign to distributed server - message = node_message( - _id=2, name="n2", server=cored.distributed_server, model="host" - ) - cored.request_handler.handle_message(message) - - # create distributed switch and assign to distributed server - message = node_message(_id=3, name="n3", node_type=NodeTypes.SWITCH) - cored.request_handler.handle_message(message) - - # link message one - ip4_address = cored.prefix.addr(1) - message = link_message(n1=1, n2=3, intf_one=0, address_one=ip4_address) - cored.request_handler.handle_message(message) - - # link message two - ip4_address = cored.prefix.addr(2) - message = link_message(n1=3, n2=2, intf_two=0, address_two=ip4_address) - cored.request_handler.handle_message(message) - - # change session to instantiation state - message = state_message(EventTypes.INSTANTIATION_STATE) - cored.request_handler.handle_message(message) - - # test a ping command - node_one = cored.session.get_node(1) - message = command_message(node_one, f"ping -c 5 {ip4_address}") - cored.request_handler.dispatch_replies = validate_response - cored.request_handler.handle_message(message) - - def test_emane(self, cored, distributed_address): - """ - Test creating a distributed emane network. - - :param core.api.tlv.coreserver.CoreServer conftest.Core cored: core daemon server to test with - :param str distributed_address: distributed server to test against - """ - # initialize server for testing - cored.setup(distributed_address) - - # configure required controlnet - cored.session.options.set_config( - "controlnet", "core1:172.16.1.0/24 core2:172.16.2.0/24" - ) - - # create local node - message = node_message(_id=1, name="n1", model="mdr") - cored.request_handler.handle_message(message) - - # create distributed node and assign to distributed server - message = node_message( - _id=2, name="n2", server=cored.distributed_server, model="mdr" - ) - cored.request_handler.handle_message(message) - - # create distributed switch and assign to distributed server - message = node_message(_id=3, name="n3", node_type=NodeTypes.EMANE) - cored.request_handler.handle_message(message) - - # set emane model - message = set_emane_model(3, EmaneIeee80211abgModel.name) - cored.request_handler.handle_message(message) - - # link message one - ip4_address = cored.prefix.addr(1) - message = link_message(n1=1, n2=3, intf_one=0, address_one=ip4_address, mask=32) - cored.request_handler.handle_message(message) - - # link message two - ip4_address = cored.prefix.addr(2) - message = link_message(n1=2, n2=3, intf_one=0, address_one=ip4_address, mask=32) - cored.request_handler.handle_message(message) - - # change session to instantiation state - message = state_message(EventTypes.INSTANTIATION_STATE) - cored.request_handler.handle_message(message) - - # test a ping command - node_one = cored.session.get_node(1) - message = command_message(node_one, f"ping -c 5 {ip4_address}") - cored.request_handler.dispatch_replies = validate_response - cored.request_handler.handle_message(message) - - def test_prouter(self, cored, distributed_address): - """ - Test creating a distributed prouter node. - - :param core.coreserver.CoreServer Core cored: core daemon server to test with - :param str distributed_address: distributed server to test against - """ - # initialize server for testing - cored.setup(distributed_address) - - # create local node - message = node_message(_id=1, name="n1", model="host") - cored.request_handler.handle_message(message) - - # create distributed node and assign to distributed server - message = node_message( - _id=2, - name="n2", - server=cored.distributed_server, - node_type=NodeTypes.PHYSICAL, - model="prouter", - ) - cored.request_handler.handle_message(message) - - # create distributed switch and assign to distributed server - message = node_message(_id=3, name="n3", node_type=NodeTypes.SWITCH) - cored.request_handler.handle_message(message) - - # link message one - ip4_address = cored.prefix.addr(1) - message = link_message(n1=1, n2=3, intf_one=0, address_one=ip4_address) - cored.request_handler.handle_message(message) - - # link message two - ip4_address = cored.prefix.addr(2) - message = link_message(n1=3, n2=2, intf_two=0, address_two=ip4_address) - cored.request_handler.handle_message(message) - - # change session to instantiation state - message = state_message(EventTypes.INSTANTIATION_STATE) - cored.request_handler.handle_message(message) - - # test a ping command - node_one = cored.session.get_node(1) - message = command_message(node_one, f"ping -c 5 {ip4_address}") - cored.request_handler.dispatch_replies = validate_response - cored.request_handler.handle_message(message) - cored.request_handler.handle_message(message) - - def test_tunnel(self, cored, distributed_address): - """ - Test session broker creation. - - :param core.coreserver.CoreServer Core cored: core daemon server to test with - :param str distributed_address: distributed server to test against - """ - # initialize server for testing - cored.setup(distributed_address) - - # create local node - message = node_message(_id=1, name="n1", model="host") - cored.request_handler.handle_message(message) - - # create distributed node and assign to distributed server - message = node_message( - _id=2, - name=distributed_address, - server=cored.distributed_server, - node_type=NodeTypes.TUNNEL, - ) - cored.request_handler.handle_message(message) - - # link message one - ip4_address = cored.prefix.addr(1) - address_two = IpAddress.from_string(distributed_address) - message = link_message( - n1=1, - n2=2, - intf_one=0, - address_one=ip4_address, - intf_two=0, - address_two=address_two, - key=1, - ) - cored.request_handler.handle_message(message) - - # change session to instantiation state - message = state_message(EventTypes.INSTANTIATION_STATE) - cored.request_handler.handle_message(message) diff --git a/daemon/tests/test_distributed.py b/daemon/tests/test_distributed.py new file mode 100644 index 00000000..d6b251e0 --- /dev/null +++ b/daemon/tests/test_distributed.py @@ -0,0 +1,39 @@ +from core.emulator.emudata import NodeOptions +from core.emulator.enumerations import NodeTypes + + +class TestDistributed: + def test_remote_node(self, session): + # given + server_name = "core2" + host = "127.0.0.1" + + # when + session.distributed.add_server(server_name, host) + options = NodeOptions() + options.server = server_name + node = session.add_node(options=options) + + # then + assert node.server is not None + assert node.server.name == server_name + assert node.server.host == host + + def test_remote_bridge(self, session): + # given + server_name = "core2" + host = "127.0.0.1" + session.distributed.address = host + + # when + session.distributed.add_server(server_name, host) + options = NodeOptions() + options.server = server_name + node = session.add_node(_type=NodeTypes.HUB, options=options) + session.instantiate() + + # then + assert node.server is not None + assert node.server.name == server_name + assert node.server.host == host + assert len(session.distributed.tunnels) > 0 From 5c12651e4ea53a2e55bda754b870c983fe003945 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 24 Oct 2019 13:05:02 -0700 Subject: [PATCH 123/462] updates to session.clear to clear out all configuration data as well, updated session.shutdown to use clear, updated tests to account for this --- daemon/core/api/tlv/corehandlers.py | 12 ++++++++---- daemon/core/emane/emanemanager.py | 1 - daemon/core/emulator/session.py | 15 +++++++++------ daemon/tests/conftest.py | 9 --------- daemon/tests/test_gui.py | 7 +------ 5 files changed, 18 insertions(+), 26 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index ffb28073..1e7be162 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -1092,10 +1092,14 @@ class CoreHandler(socketserver.BaseRequestHandler): if message_type == ConfigFlags.RESET: node_id = config_data.node - self.session.location.reset() - self.session.services.reset() - self.session.mobility.config_reset(node_id) - self.session.emane.config_reset(node_id) + if node_id is not None: + self.session.mobility.config_reset(node_id) + self.session.emane.config_reset(node_id) + else: + self.session.location.reset() + self.session.services.reset() + self.session.mobility.config_reset() + self.session.emane.config_reset() else: raise Exception(f"cant handle config all: {message_type}") diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index a66f9de7..f097b7ad 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -376,7 +376,6 @@ class EmaneManager(ModelManager): with self._emane_node_lock: self._emane_nets.clear() - # don't clear self._ifccounts here; NEM counts are needed for buildxml self.platformport = self.session.options.get_config_int( "emane_platform_port", 8100 ) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 2a2c4f11..c950fd4e 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -876,10 +876,15 @@ class Session: :return: nothing """ + self.emane.shutdown() self.delete_nodes() self.distributed.shutdown() self.del_hooks() self.emane.reset() + self.emane.config_reset() + self.location.reset() + self.services.reset() + self.mobility.config_reset() def start_events(self): """ @@ -919,13 +924,11 @@ class Session: self.set_state(EventTypes.DATACOLLECT_STATE, send_event=True) self.set_state(EventTypes.SHUTDOWN_STATE, send_event=True) - # shutdown/cleanup feature helpers - self.emane.shutdown() - self.sdt.shutdown() + # clear out current core session + self.clear() - # remove and shutdown all nodes and tunnels - self.delete_nodes() - self.distributed.shutdown() + # shutdown sdt + self.sdt.shutdown() # remove this sessions working directory preserve = self.options.get_config("preservedir") == "1" diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index adfe3e24..0c60bb2f 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -120,10 +120,6 @@ def session(global_session): global_session.set_state(EventTypes.CONFIGURATION_STATE) yield global_session global_session.clear() - global_session.location.reset() - global_session.services.reset() - global_session.mobility.config_reset() - global_session.emane.config_reset() @pytest.fixture @@ -133,11 +129,6 @@ def coretlv(module_coretlv): coreemu.sessions[session.id] = session yield module_coretlv coreemu.shutdown() - session.clear() - session.location.reset() - session.services.reset() - session.mobility.config_reset() - session.emane.config_reset() def pytest_addoption(parser): diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index 21c14c2c..fc9d183e 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -645,14 +645,9 @@ class TestGui: assert len(coretlv.session.nodes) == 1 def test_config_all(self, coretlv): - node = coretlv.session.add_node() message = coreapi.CoreConfMessage.create( MessageFlags.ADD.value, - [ - (ConfigTlvs.OBJECT, "all"), - (ConfigTlvs.NODE, node.id), - (ConfigTlvs.TYPE, ConfigFlags.RESET.value), - ], + [(ConfigTlvs.OBJECT, "all"), (ConfigTlvs.TYPE, ConfigFlags.RESET.value)], ) coretlv.session.location.refxyz = (10, 10, 10) From e4a2c18d175fc1499b0cfac8cb5cb4b988701673 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 24 Oct 2019 15:28:30 -0700 Subject: [PATCH 124/462] adding mocked tests to github actions --- .github/workflows/daemon-checks.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/daemon-checks.yml b/.github/workflows/daemon-checks.yml index 023f5165..93dd5526 100644 --- a/.github/workflows/daemon-checks.yml +++ b/.github/workflows/daemon-checks.yml @@ -30,3 +30,7 @@ jobs: run: | cd daemon pipenv run flake8 + - name: tests + run: | + cd daemon + pipenv run pytest -v --mock From 6f58a82a750aae5a7e94bf6a4fd44909d78a9566 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 24 Oct 2019 15:31:17 -0700 Subject: [PATCH 125/462] removing mocked tests, until grpc and constants.py generation are determined --- .github/workflows/daemon-checks.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/daemon-checks.yml b/.github/workflows/daemon-checks.yml index 93dd5526..023f5165 100644 --- a/.github/workflows/daemon-checks.yml +++ b/.github/workflows/daemon-checks.yml @@ -30,7 +30,3 @@ jobs: run: | cd daemon pipenv run flake8 - - name: tests - run: | - cd daemon - pipenv run pytest -v --mock From 5829e3ae429ef295c28b368304862d2ef46fdc95 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 24 Oct 2019 16:50:24 -0700 Subject: [PATCH 126/462] more work on coretk --- coretk/coretk/app.py | 2 +- coretk/coretk/coregrpc.py | 9 +- coretk/coretk/coremenubar.py | 4 +- coretk/coretk/graph.py | 19 +- coretk/coretk/menuaction.py | 6 +- coretk/coretk/nodeconfigtable.py | 9 +- coretk/coretk/setwallpaper.py | 294 +++++++++++++++++++++++++ coretk/coretk/sizeandscale.py | 279 ++++++++++++++++++----- coretk/coretk/wallpaper/sample1-bg.gif | Bin 0 -> 319126 bytes coretk/coretk/wallpaper/sample4-bg.jpg | Bin 0 -> 201030 bytes 10 files changed, 548 insertions(+), 74 deletions(-) create mode 100644 coretk/coretk/setwallpaper.py create mode 100644 coretk/coretk/wallpaper/sample1-bg.gif create mode 100644 coretk/coretk/wallpaper/sample4-bg.jpg diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index ec22f2fd..1f85fb9f 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -59,7 +59,7 @@ class Application(tk.Frame): master=self, grpc=self.core_grpc, background="#cccccc", - scrollregion=(0, 0, 1000, 1000), + scrollregion=(0, 0, 1200, 1000), ) self.canvas.pack(fill=tk.BOTH, expand=True) diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index e3e98edd..047e7c92 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -35,9 +35,9 @@ class CoreGrpc: def log_throughput(self, event): interface_throughputs = event.interface_throughputs - # for i in interface_throughputs: - # print(i) - # return + for i in interface_throughputs: + print("") + return throughputs_belong_to_session = [] for if_tp in interface_throughputs: if if_tp.node_id in self.node_ids: @@ -58,8 +58,9 @@ class CoreGrpc: # handle events session may broadcast self.session_id = response.session_id + self.master.title("CORE Session ID " + str(self.session_id)) self.core.events(self.session_id, self.log_event) - self.core.throughputs(self.log_throughput) + # self.core.throughputs(self.log_throughput) def query_existing_sessions(self, sessions): """ diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index 353e0c8a..a9780072 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -169,7 +169,9 @@ class CoreMenubar(object): canvas_menu.add_command( label="Size/scale...", command=self.menu_action.canvas_size_and_scale ) - canvas_menu.add_command(label="Wallpaper...", command=action.canvas_wallpaper) + canvas_menu.add_command( + label="Wallpaper...", command=self.menu_action.canvas_set_wallpaper + ) canvas_menu.add_separator() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index d88524bc..9f4b87db 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -34,6 +34,8 @@ class CanvasGraph(tk.Canvas): self.edges = {} self.drawing_edge = None + self.grid = None + self.meters_per_pixel = 1.5 self.setup_menus() self.setup_bindings() self.draw_grid() @@ -89,7 +91,7 @@ class CanvasGraph(tk.Canvas): self.bind("", self.click_motion) self.bind("", self.context) - def draw_grid(self, width=1000, height=750): + def draw_grid(self, width=1000, height=800): """ Create grid @@ -98,7 +100,7 @@ class CanvasGraph(tk.Canvas): :return: nothing """ - rectangle_id = self.create_rectangle( + self.grid = self.create_rectangle( 0, 0, width, @@ -108,11 +110,11 @@ class CanvasGraph(tk.Canvas): width=1, tags="rectangle", ) - self.tag_lower(rectangle_id) + self.tag_lower(self.grid) for i in range(0, width, 27): - self.create_line(i, 0, i, height, dash=(2, 4), tags="grid line") + self.create_line(i, 0, i, height, dash=(2, 4), tags="gridline") for i in range(0, height, 27): - self.create_line(0, i, width, i, dash=(2, 4), tags="grid line") + self.create_line(0, i, width, i, dash=(2, 4), tags="gridline") def draw_existing_component(self): """ @@ -235,6 +237,7 @@ class CanvasGraph(tk.Canvas): :return: the item that the mouse point to """ overlapping = self.find_overlapping(event.x, event.y, event.x, event.y) + print(overlapping) nodes = set(self.find_withtag("node")) selected = None for _id in overlapping: @@ -260,6 +263,7 @@ class CanvasGraph(tk.Canvas): self.focus_set() self.selected = self.get_selected(event) logging.debug(f"click release selected: {self.selected}") + print(self.mode) if self.mode == GraphMode.EDGE: self.handle_edge_release(event) elif self.mode == GraphMode.NODE: @@ -359,7 +363,8 @@ class CanvasGraph(tk.Canvas): self.node_context.post(event.x_root, event.y_root) def add_node(self, x, y, image, node_name): - if self.selected == 1: + plot_id = self.find_all()[0] + if self.selected == plot_id: node = CanvasNode( x=x, y=y, @@ -510,7 +515,7 @@ class CanvasNode: else: self.canvas.coords(edge.id, x1, y1, new_x, new_y) edge.link_info.recalculate_info() - self.canvas.core_grpc.throughput_draw.update_throughtput_location(edge) + # self.canvas.core_grpc.throughput_draw.update_throughtput_location(edge) self.canvas.helper.update_wlan_connection( old_x, old_y, new_x, new_y, self.wlans diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index e7e1aafc..4a1332d2 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -7,6 +7,7 @@ import webbrowser from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 +from coretk.setwallpaper import CanvasWallpaper from coretk.sizeandscale import SizeAndScale SAVEDIR = "/home/ncs/Desktop/" @@ -418,7 +419,10 @@ class MenuAction: # print(t1 - t0) def canvas_size_and_scale(self): - SizeAndScale() + SizeAndScale(self.application) + + def canvas_set_wallpaper(self): + CanvasWallpaper(self.application) def help_core_github(self): webbrowser.open_new("https://github.com/coreemu/core") diff --git a/coretk/coretk/nodeconfigtable.py b/coretk/coretk/nodeconfigtable.py index 51b8314d..76550289 100644 --- a/coretk/coretk/nodeconfigtable.py +++ b/coretk/coretk/nodeconfigtable.py @@ -30,7 +30,7 @@ class NodeConfig: self.select_definition() def open_icon_dir(self, toplevel, entry_text): - imgfile = filedialog.askopenfilename( + filename = filedialog.askopenfilename( initialdir=ICONS_DIR, title="Open", filetypes=( @@ -38,16 +38,15 @@ class NodeConfig: ("All Files", "*"), ), ) - if len(imgfile) > 0: - img = Image.open(imgfile) + if len(filename) > 0: + img = Image.open(filename) tk_img = ImageTk.PhotoImage(img) lb = toplevel.grid_slaves(1, 0)[0] lb.configure(image=tk_img) lb.image = tk_img - entry_text.set(imgfile) + entry_text.set(filename) def click_apply(self, toplevel, entry_text): - print("click apply") imgfile = entry_text.get() if imgfile: img = Image.open(imgfile) diff --git a/coretk/coretk/setwallpaper.py b/coretk/coretk/setwallpaper.py new file mode 100644 index 00000000..a4a8c908 --- /dev/null +++ b/coretk/coretk/setwallpaper.py @@ -0,0 +1,294 @@ +""" +set wallpaper +""" +import enum +import logging +import os +import tkinter as tk +from tkinter import filedialog + +from PIL import Image, ImageTk + +PATH = os.path.abspath(os.path.dirname(__file__)) +WALLPAPER_DIR = os.path.join(PATH, "wallpaper") + + +class ScaleOption(enum.Enum): + UPPER_LEFT = 1 + CENTERED = 2 + SCALED = 3 + TILED = 4 + + +class CanvasWallpaper: + def __init__(self, application): + self.application = application + self.canvas = self.application.canvas + + self.top = tk.Toplevel() + self.top.title("Set Canvas Wallpaper") + self.radiovar = tk.IntVar() + self.show_grid_var = tk.IntVar() + self.adjust_to_dim_var = tk.IntVar() + self.wallpaper = None + + self.create_image_label() + self.create_text_label() + self.open_image() + self.display_options() + self.additional_options() + self.apply_cancel() + + def create_image_label(self): + image_label = tk.Label( + self.top, text="(image preview)", height=8, width=32, bg="white" + ) + image_label.grid(pady=5) + + def create_text_label(self): + text_label = tk.Label(self.top, text="Image filename: ") + text_label.grid() + + def open_image_link(self): + filename = filedialog.askopenfilename( + initialdir=WALLPAPER_DIR, + title="Open", + filetypes=( + ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), + ("All Files", "*"), + ), + ) + + # fill the file name into the file name entry + img_open_frame = self.top.grid_slaves(2, 0)[0] + filename_entry = img_open_frame.grid_slaves(0, 0)[0] + filename_entry.delete(0, tk.END) + filename_entry.insert(tk.END, filename) + + # display that onto the label + img_label = self.top.grid_slaves(0, 0)[0] + if filename: + img = Image.open(filename) + img = img.resize((250, 135), Image.ANTIALIAS) + tk_img = ImageTk.PhotoImage(img) + img_label.config(image=tk_img, width=250, height=135) + img_label.image = tk_img + + def clear_link(self): + # delete entry + img_open_frame = self.top.grid_slaves(2, 0)[0] + filename_entry = img_open_frame.grid_slaves(0, 0)[0] + filename_entry.delete(0, tk.END) + + # delete display image + img_label = self.top.grid_slaves(0, 0)[0] + img_label.config(image="", width=32, height=8) + + def open_image(self): + f = tk.Frame(self.top) + + var = tk.StringVar(f, value="") + e = tk.Entry(f, textvariable=var) + e.focus() + e.grid() + + b = tk.Button(f, text="...", command=self.open_image_link) + b.grid(row=0, column=1) + + b = tk.Button(f, text="Clear", command=self.clear_link) + b.grid(row=0, column=2) + + f.grid() + + def display_options(self): + f = tk.Frame(self.top) + + b1 = tk.Radiobutton(f, text="upper-left", value=1, variable=self.radiovar) + b1.grid(row=0, column=0) + + b2 = tk.Radiobutton(f, text="centered", value=2, variable=self.radiovar) + b2.grid(row=0, column=1) + + b3 = tk.Radiobutton(f, text="scaled", value=3, variable=self.radiovar) + b3.grid(row=0, column=2) + + b4 = tk.Radiobutton(f, text="titled", value=4, variable=self.radiovar) + b4.grid(row=0, column=3) + + self.radiovar.set(1) + + f.grid() + + def additional_options(self): + b = tk.Checkbutton(self.top, text="Show grid", variable=self.show_grid_var) + b.grid(sticky=tk.W, padx=5) + b = tk.Checkbutton( + self.top, + text="Adjust canvas size to image dimensions", + variable=self.adjust_to_dim_var, + ) + b.grid(sticky=tk.W, padx=5) + self.show_grid_var.set(1) + self.adjust_to_dim_var.set(0) + + def delete_previous_wallpaper(self): + prev_wallpaper = self.canvas.find_withtag("wallpaper") + if prev_wallpaper: + for i in prev_wallpaper: + self.canvas.delete(i) + + def get_canvas_width_and_height(self): + """ + retrieve canvas width and height in pixels + + :return: nothing + """ + canvas = self.application.canvas + grid = canvas.find_withtag("rectangle")[0] + x0, y0, x1, y1 = canvas.coords(grid) + canvas_w = abs(x0 - x1) + canvas_h = abs(y0 - y1) + return canvas_w, canvas_h + + def determine_cropped_image_dimension(self): + """ + determine the dimension of the image after being cropped + + :return: nothing + """ + return + + def upper_left(self, img): + tk_img = ImageTk.PhotoImage(img) + + # crop image if it is bigger than canvas + canvas_w, canvas_h = self.get_canvas_width_and_height() + + cropx = img_w = tk_img.width() + cropy = img_h = tk_img.height() + + if img_w > canvas_w: + + cropx -= img_w - canvas_w + if img_h > canvas_h: + cropy -= img_h - canvas_h + cropped = img.crop((0, 0, cropx, cropy)) + cropped_tk = ImageTk.PhotoImage(cropped) + + # place left corner of image to the left corner of the canvas + self.application.croppedwallpaper = cropped_tk + + self.delete_previous_wallpaper() + + wid = self.canvas.create_image( + (cropx / 2, cropy / 2), image=cropped_tk, tags="wallpaper" + ) + self.application.wallpaper_id = wid + + def center(self, img): + """ + place the image at the center of canvas + + :param Image img: image object + :return: nothing + """ + tk_img = ImageTk.PhotoImage(img) + canvas_w, canvas_h = self.get_canvas_width_and_height() + + cropx = img_w = tk_img.width() + cropy = img_h = tk_img.height() + + # dimension of the cropped image + if img_w > canvas_w: + cropx -= img_w - canvas_w + if img_h > canvas_h: + cropy -= img_h - canvas_h + + x0 = (img_w - cropx) / 2 + y0 = (img_h - cropy) / 2 + x1 = x0 + cropx + y1 = y0 + cropy + cropped = img.crop((x0, y0, x1, y1)) + cropped_tk = ImageTk.PhotoImage(cropped) + + # place the center of the image at the center of the canvas + self.application.croppedwallpaper = cropped_tk + self.delete_previous_wallpaper() + wid = self.canvas.create_image( + (canvas_w / 2, canvas_h / 2), image=cropped_tk, tags="wallpaper" + ) + self.application.wallpaper_id = wid + + def scaled(self, img): + """ + scale image based on canvas dimension + + :param Image img: image object + :return: nothing + """ + canvas_w, canvas_h = self.get_canvas_width_and_height() + resized_image = img.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS) + image_tk = ImageTk.PhotoImage(resized_image) + self.application.croppedwallpaper = image_tk + + self.delete_previous_wallpaper() + + wid = self.canvas.create_image( + (canvas_w / 2, canvas_h / 2), image=image_tk, tags="wallpaper" + ) + self.application.wallpaper_id = wid + + def tiled(self, img): + return + + def show_grid(self): + """ + + :return: nothing + """ + if self.show_grid_var.get() == 0: + for i in self.canvas.find_withtag("gridline"): + self.canvas.itemconfig(i, state=tk.HIDDEN) + elif self.show_grid_var.get() == 1: + for i in self.canvas.find_withtag("gridline"): + self.canvas.itemconfig(i, state=tk.NORMAL) + self.canvas.lift(i) + else: + logging.error("setwallpaper.py show_grid invalid value") + + def click_apply(self): + img_link_frame = self.top.grid_slaves(2, 0)[0] + filename = img_link_frame.grid_slaves(0, 0)[0].get() + if not filename: + self.top.destroy() + return + try: + img = Image.open(filename) + except FileNotFoundError: + print("invalid filename, draw original white plot") + if self.application.wallpaper_id: + self.canvas.delete(self.application.wallpaper_id) + self.top.destroy() + return + if self.radiovar.get() == ScaleOption.UPPER_LEFT.value: + self.upper_left(img) + elif self.radiovar.get() == ScaleOption.CENTERED.value: + self.center(img) + elif self.radiovar.get() == ScaleOption.SCALED.value: + self.scaled(img) + elif self.radiovar.get() == ScaleOption.TILED.value: + print("not implemented yet") + + self.show_grid() + self.top.destroy() + + def apply_cancel(self): + f = tk.Frame(self.top) + + b = tk.Button(f, text="Apply", command=self.click_apply) + b.grid(row=0, column=0) + + b = tk.Button(f, text="Cancel", command=self.top.destroy) + b.grid(row=0, column=1) + + f.grid(pady=5) diff --git a/coretk/coretk/sizeandscale.py b/coretk/coretk/sizeandscale.py index e9485892..44d3adbc 100644 --- a/coretk/coretk/sizeandscale.py +++ b/coretk/coretk/sizeandscale.py @@ -1,86 +1,255 @@ """ size and scale """ - import tkinter as tk +from functools import partial + +DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] class SizeAndScale: - def __init__(self): + def __init__(self, application): + """ + create an instance for size and scale object + + :param application: main application + """ + self.application = application self.top = tk.Toplevel() self.top.title("Canvas Size and Scale") + self.meter_per_pixel = self.application.canvas.meters_per_pixel + self.size_chart() + self.scale_chart() + self.reference_point_chart() + self.save_as_default() + self.apply_cancel() - self.pixel_width_text = None + def pixel_scrollbar_command(self, size_frame, entry_row, entry_column, event): + """ + change the value shown based on scrollbar action - def click_scrollbar(self, e1, e2, e3): - print(e1, e2, e3) + :param tkinter.Frame frame: pixel dimension frame + :param int entry_row: row number of entry of the frame + :param int entry_column: column number of entry of the frame + :param event: scrollbar event + :return: nothing + """ + pixel_frame = size_frame.grid_slaves(0, 0)[0] + pixel_entry = pixel_frame.grid_slaves(entry_row, entry_column)[0] + val = int(pixel_entry.get()) - def create_text_label(self, frame, text, row, column): + if event == "-1": + new_val = val + 2 + elif event == "1": + new_val = val - 2 + + pixel_entry.delete(0, tk.END) + pixel_entry.insert(tk.END, str(new_val)) + + # change meter dimension + meter_frame = size_frame.grid_slaves(1, 0)[0] + meter_entry = meter_frame.grid_slaves(entry_row, entry_column)[0] + meter_entry.delete(0, tk.END) + meter_entry.insert(tk.END, str(new_val * self.meter_per_pixel)) + + def meter_scrollbar_command(self, size_frame, entry_row, entry_column, event): + """ + change the value shown based on scrollbar action + + :param tkinter.Frame size_frame: size frame + :param int entry_row: row number of entry in the frame it is contained in + :param int entry_column: column number of entry in the frame in is contained in + :param event: scroolbar event + :return: nothing + """ + meter_frame = size_frame.grid_slaves(1, 0)[0] + meter_entry = meter_frame.grid_slaves(entry_row, entry_column)[0] + val = float(meter_entry.get()) + + if event == "-1": + val += 100.0 + elif event == "1": + val -= 100.0 + meter_entry.delete(0, tk.END) + meter_entry.insert(tk.END, str(val)) + + # change pixel dimension + pixel_frame = size_frame.grid_slaves(0, 0)[0] + pixel_entry = pixel_frame.grid_slaves(entry_row, entry_column)[0] + pixel_entry.delete(0, tk.END) + pixel_entry.insert(tk.END, str(int(val / self.meter_per_pixel))) + + def create_text_label(self, frame, text, row, column, sticky=None): + """ + create text label + :param tkinter.Frame frame: parent frame + :param str text: label text + :param int row: row number + :param int column: column number + :param sticky: sticky value + + :return: nothing + """ text_label = tk.Label(frame, text=text) - text_label.grid(row=row, column=column) + text_label.grid(row=row, column=column, sticky=sticky, padx=3, pady=3) + + def create_entry(self, frame, default_value, row, column, width): + text_var = tk.StringVar(frame, value=str(default_value)) + entry = tk.Entry( + frame, textvariable=text_var, width=width, bg="white", state=tk.NORMAL + ) + entry.focus() + entry.grid(row=row, column=column, padx=3, pady=3) def size_chart(self): - f = tk.Frame(self.top) - t = tk.Label(f, text="Size") - t.grid(row=0, column=0, sticky=tk.W) + label = tk.Label(self.top, text="Size") + label.grid(sticky=tk.W, padx=5) - scrollbar = tk.Scrollbar(f, orient=tk.VERTICAL) - scrollbar.grid(row=1, column=1) - e = tk.Entry(f, text="1000", xscrollcommand=scrollbar.set) - e.focus() - e.grid(row=1, column=0) - scrollbar.config(command=self.click_scrollbar) + canvas = self.application.canvas + plot = canvas.find_withtag("rectangle") + x0, y0, x1, y1 = canvas.bbox(plot[0]) + w = abs(x0 - x1) - 2 + h = abs(y0 - y1) - 2 - # l = tk.Label(f, text="W") - # l.grid(row=1, column=2) - # l = tk.Label(f, text=" X ") - # l.grid(row=1, column=3) - self.create_text_label(f, "W", 1, 2) - self.create_text_label(f, " X ", 1, 3) + f = tk.Frame( + self.top, + highlightbackground="#b3b3b3", + highlightcolor="#b3b3b3", + highlightthickness=0.5, + bd=0, + ) - hpixel_scrollbar = tk.Scrollbar(f, orient=tk.VERTICAL) - hpixel_scrollbar.grid(row=1, column=5) + f1 = tk.Frame(f) + pw_scrollbar = tk.Scrollbar(f1, orient=tk.VERTICAL) + pw_scrollbar.grid(row=0, column=1) + self.create_entry(f1, w, 0, 0, 6) + pw_scrollbar.config(command=partial(self.pixel_scrollbar_command, f, 0, 0)) - hpixel_entry = tk.Entry(f, text="750", xscrollcommand=hpixel_scrollbar.set) - hpixel_entry.focus() - hpixel_entry.grid(row=1, column=4) + self.create_text_label(f1, " W x ", 0, 2) - h_label = tk.Label(f, text="H") - h_label.grid(row=1, column=6) + scrollbar = tk.Scrollbar(f1, orient=tk.VERTICAL) + scrollbar.grid(row=0, column=4) + self.create_entry(f1, h, 0, 3, 6) + scrollbar.config(command=partial(self.pixel_scrollbar_command, f, 0, 3)) + self.create_text_label(f1, " H pixels ", 0, 7) + f1.grid(sticky=tk.W, pady=3) - self.create_text_label(f, "pixels", 1, 7) - # pixel_label = tk.Label(f, text="pixels") - # pixel_label.grid(row=1, column=7) + f2 = tk.Frame(f) + scrollbar = tk.Scrollbar(f2, orient=tk.VERTICAL) + scrollbar.grid(row=0, column=1) + self.create_entry(f2, w * self.meter_per_pixel, 0, 0, 8) + scrollbar.config(command=partial(self.meter_scrollbar_command, f, 0, 0)) + self.create_text_label(f2, " x ", 0, 2) - wmeter_scrollbar = tk.Scrollbar(f, orient=tk.VERTICAL) - wmeter_scrollbar.grid(row=2, column=2) + scrollbar = tk.Scrollbar(f2, orient=tk.VERTICAL) + scrollbar.grid(row=0, column=4) + self.create_entry(f2, h * self.meter_per_pixel, 0, 3, 8) + scrollbar.config(command=partial(self.meter_scrollbar_command, f, 0, 3)) + self.create_text_label(f2, " meters ", 0, 5) - wmeter_entry = tk.Entry(f, text="1500.0", xscrollcommand=wmeter_scrollbar.set) - wmeter_entry.focus() - wmeter_entry.grid(row=2, column=0, columnspan=2, sticky=tk.W + tk.E) + f2.grid(sticky=tk.W, pady=3) - # l = tk.Label(f, text=" X ") - # l.grid(row=2, column=3) - self.create_text_label(f, " X ", row=2, column=3) + f.grid(sticky=tk.W + tk.E, padx=5, pady=5, columnspan=2) + def scale_chart(self): + label = tk.Label(self.top, text="Scale") + label.grid(padx=5, sticky=tk.W) + f = tk.Frame( + self.top, + highlightbackground="#b3b3b3", + highlightcolor="#b3b3b3", + highlightthickness=0.5, + bd=0, + ) + # self.create_text_label(f, "Scale", 0, 0, tk.W) # f1 = tk.Frame(f) - hmeter_scrollbar = tk.Scrollbar(f, orient=tk.VERTICAL) - hmeter_scrollbar.grid(row=2, column=6) + self.create_text_label(f, "100 pixels = ", 0, 0) + self.create_entry(f, self.meter_per_pixel * 100, 0, 1, 10) + self.create_text_label(f, "meters", 0, 2) + # f1.grid(sticky=tk.W, pady=3) + f.grid(sticky=tk.W + tk.E, padx=5, pady=5, columnspan=2) - hmeter_entry = tk.Entry(f, text="1125.0", xscrollcommand=hmeter_scrollbar.set) - hmeter_entry.focus() - hmeter_entry.grid(row=2, column=4, columnspan=2, sticky=tk.W + tk.E) + def reference_point_chart(self): + label = tk.Label(self.top, text="Reference point") + label.grid(padx=5, sticky=tk.W) - self.create_text_label(f, "pixels", 2, 7) - # pixel_label = tk.Label(f, text="pixels") - # pixel_label.grid(row=2, column=7) - # hmeter_entry.pack(side=tk.LEFT) - # - # hmeter_scrollbar = tk.Scrollbar(hmeter_entry, orient=tk.VERTICAL) - # hmeter_scrollbar.pack(side=tk.LEFT) - # f1.grid(row=2, column=4) + f = tk.Frame( + self.top, + highlightbackground="#b3b3b3", + highlightcolor="#b3b3b3", + highlightthickness=0.5, + bd=0, + ) + self.create_text_label( + f, + "The default reference point is (0, 0), the upper left corner of the canvas.", + 1, + 0, + tk.W, + ) + f1 = tk.Frame(f) + self.create_entry(f1, 0, 0, 0, 4) + self.create_text_label(f1, " X, ", 0, 1) + self.create_entry(f1, 0, 0, 2, 4) + self.create_text_label(f1, "Y = ", 0, 3) + self.create_entry(f1, 47.5791667, 0, 4, 13) + self.create_text_label(f1, " lat, ", 0, 5) + self.create_entry(f1, -122.132322, 0, 6, 13) + self.create_text_label(f1, "long", 0, 7) + f1.grid(row=2, column=0, sticky=tk.W, pady=3) - f.grid() + f2 = tk.Frame(f) + self.create_text_label(f2, "Altitude: ", 0, 0) + self.create_entry(f2, 2.0, 0, 1, 11) + self.create_text_label(f2, " meters ", 0, 2) + f2.grid(row=3, column=0, sticky=tk.W, pady=3) - # def scale_chart(self): + f.grid(sticky=tk.W, padx=5, pady=5, columnspan=2) + + def save_as_default(self): + var = tk.IntVar() + button = tk.Checkbutton(self.top, text="Save as default", variable=var) + button.grid(sticky=tk.W, padx=5, pady=5, columnspan=2) + + def redraw_grid(self, pixel_width, pixel_height): + """ + redraw grid with new dimension + + :param int pixel_width: width in pixel + :param int pixel_height: height in pixel + :return: nothing + """ + canvas = self.application.canvas + canvas.config(scrollregion=(0, 0, pixel_width + 200, pixel_height + 200)) + + # delete old plot and redraw + for i in canvas.find_withtag("gridline"): + canvas.delete(i) + for i in canvas.find_withtag("rectangle"): + canvas.delete(i) + + canvas.draw_grid(width=pixel_width, height=pixel_height) + # lift anything that is drawn on the plot before + for tag in DRAW_OBJECT_TAGS: + for i in canvas.find_withtag(tag): + canvas.lift(i) + + def click_apply(self): + size_frame = self.top.grid_slaves(1, 0)[0] + pixel_size_frame = size_frame.grid_slaves(0, 0)[0] + + pixel_width = int(pixel_size_frame.grid_slaves(0, 0)[0].get()) + pixel_height = int(pixel_size_frame.grid_slaves(0, 3)[0].get()) + + scale_frame = self.top.grid_slaves(3, 0)[0] + meter_per_pixel = float(scale_frame.grid_slaves(0, 1)[0].get()) / 100 + self.application.canvas.meters_per_pixel = meter_per_pixel + self.redraw_grid(pixel_width, pixel_height) + self.top.destroy() + + def apply_cancel(self): + apply_button = tk.Button(self.top, text="Apply", command=self.click_apply) + apply_button.grid(row=7, column=0, pady=5) + cancel_button = tk.Button(self.top, text="Cancel", command=self.top.destroy) + cancel_button.grid(row=7, column=1, pady=5) diff --git a/coretk/coretk/wallpaper/sample1-bg.gif b/coretk/coretk/wallpaper/sample1-bg.gif new file mode 100644 index 0000000000000000000000000000000000000000..98344744698658e0fdbc19a8be28f9bf065018c5 GIT binary patch literal 319126 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

NU9e(u@he7O? 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

uvBR5V{f}J-cJ3y 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�p?wYX4M{|;z0QetGSy4BB6qLhTB>RQV~-Ph#j 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 v&=+@&p7_)!bWV9_&7i6OSe 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 HKmY(cCWiQ$ literal 0 HcmV?d00001 diff --git a/coretk/coretk/wallpaper/sample4-bg.jpg b/coretk/coretk/wallpaper/sample4-bg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b3d29dc60c3c76c56a31a1dde3c280ce1c23fea GIT binary patch literal 201030 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 Date: Thu, 24 Oct 2019 21:17:15 -0700 Subject: [PATCH 127/462] changes to fix lock issues with multiple threads when stopping from gui --- daemon/core/emulator/session.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index c950fd4e..bef7f048 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -1351,17 +1351,16 @@ class Session: """ # delete node and check for session shutdown if a node was removed logging.info("deleting node(%s)", _id) - result = False + node = None with self._nodes_lock: if _id in self.nodes: node = self.nodes.pop(_id) - node.shutdown() - result = True - if result: + if node: + node.shutdown() self.check_shutdown() - return result + return node is not None def delete_nodes(self): """ From 77c7bf798ecdd1ec07b8d0ec2ada99e76abd26ce Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 25 Oct 2019 15:32:12 -0700 Subject: [PATCH 128/462] changes to tests to fix session fixture not being master and updated emane xml config test to use a valid value --- daemon/tests/conftest.py | 3 +++ daemon/tests/emane/test_emane.py | 10 +++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index 0c60bb2f..e1f04f66 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -12,6 +12,7 @@ from mock.mock import MagicMock from core.api.grpc.client import InterfaceHelper from core.api.grpc.server import CoreGrpcServer from core.api.tlv.corehandlers import CoreHandler +from core.emane.emanemanager import EmaneManager from core.emulator.coreemu import CoreEmu from core.emulator.distributed import DistributedServer from core.emulator.emudata import IpPrefixes @@ -58,6 +59,7 @@ def patcher(request): patch_manager.patch_obj(CoreNode, "nodefile") patch_manager.patch_obj(Session, "write_state") patch_manager.patch_obj(Session, "write_nodes") + patch_manager.patch_obj(EmaneManager, "buildxml") yield patch_manager patch_manager.shutdown() @@ -73,6 +75,7 @@ def global_coreemu(patcher): def global_session(request, patcher, global_coreemu): mkdir = not request.config.getoption("mock") session = Session(1000, {"emane_prefix": "/usr"}, mkdir) + session.master = True yield session session.shutdown() diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py index a27e8d83..4c507eee 100644 --- a/daemon/tests/emane/test_emane.py +++ b/daemon/tests/emane/test_emane.py @@ -93,7 +93,11 @@ class TestEmane: options = NodeOptions() options.set_position(80, 50) emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) - session.emane.set_model(emane_network, EmaneIeee80211abgModel, {"test": "1"}) + config_key = "txpower" + config_value = "10" + session.emane.set_model( + emane_network, EmaneIeee80211abgModel, {config_key: config_value} + ) # create nodes options = NodeOptions(model="mdr") @@ -138,11 +142,11 @@ class TestEmane: # retrieve configuration we set originally value = str( - session.emane.get_config("test", emane_id, EmaneIeee80211abgModel.name) + session.emane.get_config(config_key, emane_id, EmaneIeee80211abgModel.name) ) # verify nodes and configuration were restored assert session.get_node(n1_id) assert session.get_node(n2_id) assert session.get_node(emane_id) - assert value == "1" + assert value == config_value From fff281a4529c22f5d721d9a90036802b6778b698 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 25 Oct 2019 22:06:30 -0700 Subject: [PATCH 129/462] removed master from corehandlers and session, since it will not be needed any more --- daemon/core/api/tlv/corehandlers.py | 46 ++++------------------------- daemon/core/emane/emanemanager.py | 41 ++++++++++++------------- daemon/core/emulator/coreemu.py | 8 ++--- daemon/core/emulator/session.py | 26 +++++----------- daemon/tests/conftest.py | 1 - daemon/tests/test_gui.py | 4 --- 6 files changed, 34 insertions(+), 92 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 1e7be162..a6eae965 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -83,13 +83,9 @@ class CoreHandler(socketserver.BaseRequestHandler): self.handler_threads.append(thread) thread.start() - self.master = False self.session = None self.session_clients = {} - - # core emulator self.coreemu = server.coreemu - utils.close_onexec(request.fileno()) socketserver.BaseRequestHandler.__init__(self, request, client_address, server) @@ -591,12 +587,8 @@ class CoreHandler(socketserver.BaseRequestHandler): port = self.request.getpeername()[1] # TODO: add shutdown handler for session - self.session = self.coreemu.create_session(port, master=False) + self.session = self.coreemu.create_session(port) logging.debug("created new session for client: %s", self.session.id) - - if self.master: - logging.debug("session set to master") - self.session.master = True clients = self.session_clients.setdefault(self.session.id, []) clients.append(self) @@ -942,7 +934,7 @@ class CoreHandler(socketserver.BaseRequestHandler): file_name = sys.argv[0] if os.path.splitext(file_name)[1].lower() == ".xml": - session = self.coreemu.create_session(master=False) + session = self.coreemu.create_session() try: session.open_xml(file_name) except Exception: @@ -1012,17 +1004,6 @@ class CoreHandler(socketserver.BaseRequestHandler): logging.debug("ignoring Register message") else: # register capabilities with the GUI - self.master = True - - # find the session containing this client and set the session to master - for _id in self.coreemu.sessions: - clients = self.session_clients.get(_id, []) - if self in clients: - session = self.coreemu.sessions[_id] - logging.debug("setting session to master: %s", session.id) - session.master = True - break - replies.append(self.register()) replies.append(self.session_message()) @@ -1441,11 +1422,6 @@ class CoreHandler(socketserver.BaseRequestHandler): config = ConfigShim.str_to_dict(values_str) self.session.emane.set_configs(config) - # extra logic to start slave Emane object after nemid has been configured from the master - if message_type == ConfigFlags.UPDATE and self.session.master is False: - # instantiation was previously delayed by setup returning Emane.NOT_READY - self.session.instantiate() - return replies def handle_config_emane_models(self, message_type, config_data): @@ -1613,20 +1589,11 @@ class CoreHandler(socketserver.BaseRequestHandler): for _id in self.session.nodes: self.send_node_emulation_id(_id) elif event_type == EventTypes.RUNTIME_STATE: - if self.session.master: - logging.warning( - "Unexpected event message: RUNTIME state received at session master" - ) - else: - # master event queue is started in session.checkruntime() - self.session.start_events() + logging.warning("Unexpected event message: RUNTIME state received") elif event_type == EventTypes.DATACOLLECT_STATE: self.session.data_collect() elif event_type == EventTypes.SHUTDOWN_STATE: - if self.session.master: - logging.warning( - "Unexpected event message: SHUTDOWN state received at session master" - ) + logging.warning("Unexpected event message: SHUTDOWN state received") elif event_type in { EventTypes.START, EventTypes.STOP, @@ -1824,9 +1791,7 @@ class CoreHandler(socketserver.BaseRequestHandler): # set session to join self.session = session - # add client to session broker and set master if needed - if self.master: - self.session.master = True + # add client to session broker clients = self.session_clients.setdefault(self.session.id, []) clients.append(self) @@ -2022,7 +1987,6 @@ class CoreUdpHandler(CoreHandler): MessageTypes.EVENT.value: self.handle_event_message, MessageTypes.SESSION.value: self.handle_session_message, } - self.master = False self.session = None self.coreemu = server.mainserver.coreemu socketserver.BaseRequestHandler.__init__(self, request, client_address, server) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index f097b7ad..dc3e2acf 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -269,37 +269,34 @@ class EmaneManager(ModelManager): # control network bridge required for EMANE 0.9.2 # - needs to exist when eventservice binds to it (initeventservice) - if self.session.master: - otadev = self.get_config("otamanagerdevice") - netidx = self.session.get_control_net_index(otadev) - logging.debug( - "emane ota manager device: index(%s) otadev(%s)", netidx, otadev + otadev = self.get_config("otamanagerdevice") + netidx = self.session.get_control_net_index(otadev) + logging.debug("emane ota manager device: index(%s) otadev(%s)", netidx, otadev) + if netidx < 0: + logging.error( + "EMANE cannot start, check core config. invalid OTA device provided: %s", + otadev, ) + return EmaneManager.NOT_READY + + self.session.add_remove_control_net( + net_index=netidx, remove=False, conf_required=False + ) + eventdev = self.get_config("eventservicedevice") + logging.debug("emane event service device: eventdev(%s)", eventdev) + if eventdev != otadev: + netidx = self.session.get_control_net_index(eventdev) + logging.debug("emane event service device index: %s", netidx) if netidx < 0: logging.error( - "EMANE cannot start, check core config. invalid OTA device provided: %s", - otadev, + "EMANE cannot start, check core config. invalid event service device: %s", + eventdev, ) return EmaneManager.NOT_READY self.session.add_remove_control_net( net_index=netidx, remove=False, conf_required=False ) - eventdev = self.get_config("eventservicedevice") - logging.debug("emane event service device: eventdev(%s)", eventdev) - if eventdev != otadev: - netidx = self.session.get_control_net_index(eventdev) - logging.debug("emane event service device index: %s", netidx) - if netidx < 0: - logging.error( - "EMANE cannot start, check core config. invalid event service device: %s", - eventdev, - ) - return EmaneManager.NOT_READY - - self.session.add_remove_control_net( - net_index=netidx, remove=False, conf_required=False - ) self.check_node_models() return EmaneManager.SUCCESS diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py index 754ab771..cdba4e44 100644 --- a/daemon/core/emulator/coreemu.py +++ b/daemon/core/emulator/coreemu.py @@ -86,12 +86,11 @@ class CoreEmu: session = sessions[_id] session.shutdown() - def create_session(self, _id=None, master=True, _cls=Session): + def create_session(self, _id=None, _cls=Session): """ - Create a new CORE session, set to master if running standalone. + Create a new CORE session. :param int _id: session id for new session - :param bool master: sets session to master :param class _cls: Session class to use :return: created session :rtype: EmuSession @@ -104,9 +103,6 @@ class CoreEmu: session = _cls(_id, config=self.config) logging.info("created session: %s", _id) - if master: - session.master = True - self.sessions[_id] = session return session diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index bef7f048..fcebcd9e 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -85,7 +85,6 @@ class Session: :param bool mkdir: flag to determine if a directory should be made """ self.id = _id - self.master = False # define and create session directory when desired self.session_dir = os.path.join(tempfile.gettempdir(), f"pycore.{self.id}") @@ -1694,28 +1693,19 @@ class Session: prefixes = prefix_spec.split() if len(prefixes) > 1: # a list of per-host prefixes is provided - assign_address = True - if self.master: - try: - # split first (master) entry into server and prefix - prefix = prefixes[0].split(":", 1)[1] - except IndexError: - # no server name. possibly only one server - prefix = prefixes[0] - - # len(prefixes) == 1 + try: + # split first (master) entry into server and prefix + prefix = prefixes[0].split(":", 1)[1] + except IndexError: + # no server name. possibly only one server + prefix = prefixes[0] else: - # TODO: can we get the server name from the servers.conf or from the node - # assignments?o - # with one prefix, only master gets a ctrlnet address - assign_address = self.master prefix = prefixes[0] logging.info( - "controlnet(%s) prefix(%s) assign(%s) updown(%s) serverintf(%s)", + "controlnet(%s) prefix(%s) updown(%s) serverintf(%s)", _id, prefix, - assign_address, updown_script, server_interface, ) @@ -1723,7 +1713,7 @@ class Session: cls=CtrlNet, _id=_id, prefix=prefix, - assign_address=assign_address, + assign_address=True, updown_script=updown_script, serverintf=server_interface, ) diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index e1f04f66..2055820c 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -75,7 +75,6 @@ def global_coreemu(patcher): def global_session(request, patcher, global_coreemu): mkdir = not request.config.getoption("mock") session = Session(1000, {"emane_prefix": "/usr"}, mkdir) - session.master = True yield session session.shutdown() diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index fc9d183e..b4025a0c 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -606,13 +606,9 @@ class TestGui: coretlv.handle_message(message) def test_register_gui(self, coretlv): - coretlv.master = False message = coreapi.CoreRegMessage.create(0, [(RegisterTlvs.GUI, "gui")]) - coretlv.handle_message(message) - assert coretlv.master is True - def test_register_xml(self, coretlv, tmpdir): xml_file = tmpdir.join("coretlv.session.xml") file_path = xml_file.strpath From 934ea96558489c763ca87098b63f0cf73a390d04 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 28 Oct 2019 15:18:57 -0700 Subject: [PATCH 130/462] changes to support a simpler start/stop session API --- daemon/core/api/grpc/client.py | 25 +++++++++ daemon/core/api/grpc/grpcutils.py | 55 ++++++++++++++++++++ daemon/core/api/grpc/server.py | 49 +++++++++++++++++- daemon/examples/grpc/large.py | 74 +++++++++++++++++++++++++++ daemon/proto/core/api/grpc/core.proto | 22 ++++++++ 5 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 daemon/core/api/grpc/grpcutils.py create mode 100644 daemon/examples/grpc/large.py diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 6b5343d8..ea32ffb4 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -148,6 +148,31 @@ class CoreGrpcClient: self.stub = None self.channel = None + def start_session(self, session_id, nodes, links): + """ + Start a session. + + :param int session_id: id of session + :param list nodes: list of nodes to create + :param list links: list of links to create + :return: + """ + request = core_pb2.StartSessionRequest( + session_id=session_id, nodes=nodes, links=links + ) + return self.stub.StartSession(request) + + def stop_session(self, session_id): + """ + Stop a running session. + + :param int session_id: id of session + :return: stop session response + :rtype: core_pb2.StopSessionResponse + """ + request = core_pb2.StopSessionRequest(session_id=session_id) + return self.stub.StopSession(request) + def create_session(self, session_id=None): """ Create a session. diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py new file mode 100644 index 00000000..b729e584 --- /dev/null +++ b/daemon/core/api/grpc/grpcutils.py @@ -0,0 +1,55 @@ +import asyncio +import logging +import time + +from core.emulator.emudata import NodeOptions +from core.emulator.enumerations import NodeTypes + + +def add_node_data(node_proto): + _id = node_proto.id + _type = node_proto.type + if _type is None: + _type = NodeTypes.DEFAULT.value + _type = NodeTypes(_type) + + options = NodeOptions(name=node_proto.name, model=node_proto.model) + options.icon = node_proto.icon + options.opaque = node_proto.opaque + options.image = node_proto.image + options.services = node_proto.services + if node_proto.server: + options.server = node_proto.server + + position = node_proto.position + options.set_position(position.x, position.y) + options.set_location(position.lat, position.lon, position.alt) + return _type, _id, options + + +async def async_add_node(session, node_proto): + _type, _id, options = add_node_data(node_proto) + session.add_node(_type=_type, _id=_id, options=options) + + +async def create_nodes(loop, session, node_protos): + tasks = [] + for node_proto in node_protos: + task = loop.create_task(async_add_node(session, node_proto)) + tasks.append(task) + + start = time.monotonic() + results = await asyncio.gather(*tasks, return_exceptions=True) + total = time.monotonic() - start + + logging.info(f"created nodes time: {total}") + return results + + +def sync_create_nodes(session, node_protos): + start = time.monotonic() + for node_proto in node_protos: + _type, _id, options = add_node_data(node_proto) + session.add_node(_type=_type, _id=_id, options=options) + total = time.monotonic() - start + logging.info(f"created nodes time: {total}") diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 4381184d..ff23b43e 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -9,7 +9,7 @@ from queue import Empty, Queue import grpc -from core.api.grpc import core_pb2, core_pb2_grpc +from core.api.grpc import core_pb2, core_pb2_grpc, grpcutils from core.emane.nodes import EmaneNet from core.emulator.data import ( ConfigData, @@ -260,6 +260,53 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): except CoreError: context.abort(grpc.StatusCode.NOT_FOUND, f"node {node_id} not found") + def StartSession(self, request, context): + """ + Start a session. + + :param core.api.grpc.core_pb2.StartSessionRequest request: start session request + :param context: grcp context + :return: start session response + :rtype: core.api.grpc.core_pb2.StartSessionResponse + """ + logging.debug("start session: %s", request) + session = self.get_session(request.session_id, context) + + # clear previous state and setup for creation + session.clear() + session.set_state(EventTypes.CONFIGURATION_STATE) + if not os.path.exists(session.session_dir): + os.mkdir(session.session_dir) + + # create nodes + # loop = asyncio.new_event_loop() + # asyncio.set_event_loop(loop) + # results = loop.run_until_complete( + # grpcutils.create_nodes(loop, session, request.nodes) + # ) + grpcutils.sync_create_nodes(session, request.nodes) + + # set to instantiation and start + session.set_state(EventTypes.INSTANTIATION_STATE) + session.instantiate() + + return core_pb2.StartSessionResponse(result=True) + + def StopSession(self, request, context): + """ + Stop a running session. + + :param core.api.grpc.core_pb2.StopSessionRequest request: stop session request + :param context: grcp context + :return: stop session response + :rtype: core.api.grpc.core_pb2.StopSessionResponse + """ + logging.debug("stop session: %s", request) + session = self.coreemu.create_session(request.session_id) + session.set_state(EventTypes.DATACOLLECT_STATE) + session.clear() + return core_pb2.StopSessionResponse(result=True) + def CreateSession(self, request, context): """ Create a session diff --git a/daemon/examples/grpc/large.py b/daemon/examples/grpc/large.py new file mode 100644 index 00000000..c2a4d0b6 --- /dev/null +++ b/daemon/examples/grpc/large.py @@ -0,0 +1,74 @@ +import logging + +from core.api.grpc import client, core_pb2 + + +def log_event(event): + logging.info("event: %s", event) + + +def main(): + core = client.CoreGrpcClient() + + with core.context_connect(): + # create session + response = core.create_session() + session_id = response.session_id + logging.info("created session: %s", response) + + # create nodes for session + nodes = [] + position = core_pb2.Position(x=50, y=100) + switch = core_pb2.Node(id=1, type=core_pb2.NodeType.SWITCH, position=position) + nodes.append(switch) + for i in range(2, 50): + position = core_pb2.Position(x=50 + 50 * i, y=50) + node = core_pb2.Node(id=i, position=position, model="PC") + nodes.append(node) + + # start session + links = [] + response = core.start_session(session_id, nodes, links) + logging.info("started session: %s", response) + + # handle events session may broadcast + # core.events(session_id, log_event) + + # change session state + # response = core.set_session_state( + # session_id, core_pb2.SessionState.CONFIGURATION + # ) + # logging.info("set session state: %s", response) + + # create switch node + # switch = core_pb2.Node(type=core_pb2.NodeType.SWITCH) + # response = core.add_node(session_id, switch) + # logging.info("created switch: %s", response) + # switch_id = response.node_id + + # helper to create interfaces + # interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/16") + # + # for i in range(2): + # # create node + # position = core_pb2.Position(x=50 + 50 * i, y=50) + # node = core_pb2.Node(position=position) + # response = core.add_node(session_id, node) + # logging.info("created node: %s", response) + # node_id = response.node_id + # + # # create link + # interface_one = interface_helper.create_interface(node_id, 0) + # response = core.add_link(session_id, node_id, switch_id, interface_one) + # logging.info("created link: %s", response) + + # change session state + # response = core.set_session_state( + # session_id, core_pb2.SessionState.INSTANTIATION + # ) + # logging.info("set session state: %s", response) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + main() diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 325c436f..2a766fc3 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -7,6 +7,10 @@ option java_outer_classname = "CoreProto"; service CoreApi { // session rpc + rpc StartSession (StartSessionRequest) returns (StartSessionResponse) { + } + rpc StopSession (StopSessionRequest) returns (StopSessionResponse) { + } rpc CreateSession (CreateSessionRequest) returns (CreateSessionResponse) { } rpc DeleteSession (DeleteSessionRequest) returns (DeleteSessionResponse) { @@ -126,6 +130,24 @@ service CoreApi { } // rpc request/response messages +message StartSessionRequest { + int32 session_id = 1; + repeated Node nodes = 2; + repeated Link links = 3; +} + +message StartSessionResponse { + bool result = 1; +} + +message StopSessionRequest { + int32 session_id = 1; +} + +message StopSessionResponse { + bool result = 1; +} + message CreateSessionRequest { int32 session_id = 1; } From 236ac7919a7b408d0263043075308312ccc8c42a Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 28 Oct 2019 23:11:15 -0700 Subject: [PATCH 131/462] moved grpc utility functions into grpcutils, updated StartSession to threadpool node and link creation --- daemon/core/api/grpc/grpcutils.py | 316 ++++++++++++++++++++++++++++-- daemon/core/api/grpc/server.py | 289 +++------------------------ daemon/examples/grpc/large.py | 52 ++--- 3 files changed, 335 insertions(+), 322 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index b729e584..aec094d3 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -1,12 +1,24 @@ -import asyncio +import concurrent.futures import logging import time -from core.emulator.emudata import NodeOptions -from core.emulator.enumerations import NodeTypes +from core.api.grpc import core_pb2 +from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions +from core.emulator.enumerations import LinkTypes, NodeTypes +from core.nodes.base import CoreNetworkBase +from core.nodes.ipaddress import MacAddress + +WORKERS = 10 def add_node_data(node_proto): + """ + Convert node protobuf message to data for creating a node. + + :param core_pb2.Node node_proto: node proto message + :return: node type, id, and options + :rtype: tuple + """ _id = node_proto.id _type = node_proto.type if _type is None: @@ -27,29 +39,293 @@ def add_node_data(node_proto): return _type, _id, options -async def async_add_node(session, node_proto): - _type, _id, options = add_node_data(node_proto) - session.add_node(_type=_type, _id=_id, options=options) +def link_interface(interface_proto): + """ + Create interface data from interface proto. + + :param core_pb2.Interface interface_proto: interface proto + :return: interface data + :rtype: InterfaceData + """ + interface = None + if interface_proto: + name = interface_proto.name + if name == "": + name = None + mac = interface_proto.mac + if mac == "": + mac = None + else: + mac = MacAddress.from_string(mac) + interface = InterfaceData( + _id=interface_proto.id, + name=name, + mac=mac, + ip4=interface_proto.ip4, + ip4_mask=interface_proto.ip4mask, + ip6=interface_proto.ip6, + ip6_mask=interface_proto.ip6mask, + ) + return interface -async def create_nodes(loop, session, node_protos): - tasks = [] - for node_proto in node_protos: - task = loop.create_task(async_add_node(session, node_proto)) - tasks.append(task) +def add_link_data(link_proto): + """ + Convert link proto to link interfaces and options data. + :param core_pb2.Link link_proto: link proto + :return: link interfaces and options + :rtype: tuple + """ + interface_one = link_interface(link_proto.interface_one) + interface_two = link_interface(link_proto.interface_two) + + link_type = None + link_type_value = link_proto.type + if link_type_value is not None: + link_type = LinkTypes(link_type_value) + + options = LinkOptions(_type=link_type) + options_data = link_proto.options + if options_data: + options.delay = options_data.delay + options.bandwidth = options_data.bandwidth + options.per = options_data.per + options.dup = options_data.dup + options.jitter = options_data.jitter + options.mer = options_data.mer + options.burst = options_data.burst + options.mburst = options_data.mburst + options.unidirectional = options_data.unidirectional + options.key = options_data.key + options.opaque = options_data.opaque + + return interface_one, interface_two, options + + +def create_nodes(session, node_protos): + """ + Create nodes using a thread pool and wait for completion. + + :param core.emulator.session.Session session: session to create nodes in + :param list[core_pb2.Node] node_protos: node proto messages + :return: results and exceptions for created nodes + :rtype: tuple + """ start = time.monotonic() - results = await asyncio.gather(*tasks, return_exceptions=True) + with concurrent.futures.ThreadPoolExecutor(max_workers=WORKERS) as executor: + futures = [] + for node_proto in node_protos: + _type, _id, options = add_node_data(node_proto) + future = executor.submit(session.add_node, _type, _id, options) + futures.append(future) + results = [] + exceptions = [] + for future in concurrent.futures.as_completed(futures): + try: + result = future.result() + results.append(result) + except Exception as executor: + exceptions.append(executor) total = time.monotonic() - start + logging.info("created nodes time: %s", total) + return results, exceptions - logging.info(f"created nodes time: {total}") + +def create_links(session, link_protos): + """ + Create nodes using a thread pool and wait for completion. + + :param core.emulator.session.Session session: session to create nodes in + :param list[core_pb2.Link] link_protos: link proto messages + :return: results and exceptions for created links + :rtype: tuple + """ + start = time.monotonic() + with concurrent.futures.ThreadPoolExecutor(max_workers=WORKERS) as executor: + futures = [] + for link_proto in link_protos: + node_one_id = link_proto.node_one_id + node_two_id = link_proto.node_two_id + interface_one, interface_two, options = add_link_data(link_proto) + future = executor.submit( + session.add_link, + node_one_id, + node_two_id, + interface_one, + interface_two, + options, + ) + futures.append(future) + results = [] + exceptions = [] + for future in concurrent.futures.as_completed(futures): + try: + result = future.result() + results.append(result) + except Exception as executor: + exceptions.append(executor) + total = time.monotonic() - start + logging.info("created links time: %s", total) + return results, exceptions + + +def convert_value(value): + """ + Convert value into string. + + :param value: value + :return: string conversion of the value + :rtype: str + """ + if value is not None: + value = str(value) + return value + + +def get_config_options(config, configurable_options): + """ + Retrieve configuration options in a form that is used by the grpc server. + + :param dict config: configuration + :param core.config.ConfigurableOptions configurable_options: configurable options + :return: mapping of configuration ids to configuration options + :rtype: dict[str,core.api.grpc.core_pb2.ConfigOption] + """ + results = {} + for configuration in configurable_options.configurations(): + value = config[configuration.id] + config_option = core_pb2.ConfigOption( + label=configuration.label, + name=configuration.id, + value=value, + type=configuration.type.value, + select=configuration.options, + ) + results[configuration.id] = config_option + for config_group in configurable_options.config_groups(): + start = config_group.start - 1 + stop = config_group.stop + options = list(results.values())[start:stop] + for option in options: + option.group = config_group.name return results -def sync_create_nodes(session, node_protos): - start = time.monotonic() - for node_proto in node_protos: - _type, _id, options = add_node_data(node_proto) - session.add_node(_type=_type, _id=_id, options=options) - total = time.monotonic() - start - logging.info(f"created nodes time: {total}") +def get_links(session, node): + """ + Retrieve a list of links for grpc to use + + :param core.emulator.Session session: node's section + :param core.nodes.base.CoreNode node: node to get links from + :return: [core.api.grpc.core_pb2.Link] + """ + links = [] + for link_data in node.all_link_data(0): + link = convert_link(session, link_data) + links.append(link) + return links + + +def get_emane_model_id(node_id, interface_id): + """ + Get EMANE model id + + :param int node_id: node id + :param int interface_id: interface id + :return: EMANE model id + :rtype: int + """ + if interface_id >= 0: + return node_id * 1000 + interface_id + else: + return node_id + + +def convert_link(session, link_data): + """ + Convert link_data into core protobuf Link + + :param core.emulator.session.Session session: + :param core.emulator.data.LinkData link_data: + :return: core protobuf Link + :rtype: core.api.grpc.core_pb2.Link + """ + interface_one = None + if link_data.interface1_id is not None: + node = session.get_node(link_data.node1_id) + interface_name = None + if not isinstance(node, CoreNetworkBase): + interface = node.netif(link_data.interface1_id) + interface_name = interface.name + interface_one = core_pb2.Interface( + id=link_data.interface1_id, + name=interface_name, + mac=convert_value(link_data.interface1_mac), + ip4=convert_value(link_data.interface1_ip4), + ip4mask=link_data.interface1_ip4_mask, + ip6=convert_value(link_data.interface1_ip6), + ip6mask=link_data.interface1_ip6_mask, + ) + + interface_two = None + if link_data.interface2_id is not None: + node = session.get_node(link_data.node2_id) + interface_name = None + if not isinstance(node, CoreNetworkBase): + interface = node.netif(link_data.interface2_id) + interface_name = interface.name + interface_two = core_pb2.Interface( + id=link_data.interface2_id, + name=interface_name, + mac=convert_value(link_data.interface2_mac), + ip4=convert_value(link_data.interface2_ip4), + ip4mask=link_data.interface2_ip4_mask, + ip6=convert_value(link_data.interface2_ip6), + ip6mask=link_data.interface2_ip6_mask, + ) + + options = core_pb2.LinkOptions( + opaque=link_data.opaque, + jitter=link_data.jitter, + key=link_data.key, + mburst=link_data.mburst, + mer=link_data.mer, + per=link_data.per, + bandwidth=link_data.bandwidth, + burst=link_data.burst, + delay=link_data.delay, + dup=link_data.dup, + unidirectional=link_data.unidirectional, + ) + + return core_pb2.Link( + type=link_data.link_type, + node_one_id=link_data.node1_id, + node_two_id=link_data.node2_id, + interface_one=interface_one, + interface_two=interface_two, + options=options, + ) + + +def get_net_stats(): + """ + Retrieve status about the current interfaces in the system + + :return: send and receive status of the interfaces in the system + :rtype: dict + """ + with open("/proc/net/dev", "r") as f: + data = f.readlines()[2:] + + stats = {} + for line in data: + line = line.strip() + if not line: + continue + line = line.split() + line[0] = line[0].strip(":") + stats[line[0]] = {"rx": float(line[1]), "tx": float(line[9])} + + return stats diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index ff23b43e..c516ea45 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -10,6 +10,13 @@ from queue import Empty, Queue import grpc from core.api.grpc import core_pb2, core_pb2_grpc, grpcutils +from core.api.grpc.grpcutils import ( + convert_value, + get_config_options, + get_emane_model_id, + get_links, + get_net_stats, +) from core.emane.nodes import EmaneNet from core.emulator.data import ( ConfigData, @@ -19,13 +26,11 @@ from core.emulator.data import ( LinkData, NodeData, ) -from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions -from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags, NodeTypes +from core.emulator.emudata import LinkOptions, NodeOptions +from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility -from core.nodes.base import CoreNetworkBase from core.nodes.docker import DockerNode -from core.nodes.ipaddress import MacAddress from core.nodes.lxd import LxcNode from core.services.coreservices import ServiceManager @@ -33,167 +38,6 @@ _ONE_DAY_IN_SECONDS = 60 * 60 * 24 _INTERFACE_REGEX = re.compile(r"\d+") -def convert_value(value): - """ - Convert value into string. - - :param value: value - :return: string conversion of the value - :rtype: str - """ - if value is not None: - value = str(value) - return value - - -def get_config_options(config, configurable_options): - """ - Retrieve configuration options in a form that is used by the grpc server. - - :param dict config: configuration - :param core.config.ConfigurableOptions configurable_options: configurable options - :return: mapping of configuration ids to configuration options - :rtype: dict[str,core.api.grpc.core_pb2.ConfigOption] - """ - results = {} - for configuration in configurable_options.configurations(): - value = config[configuration.id] - config_option = core_pb2.ConfigOption( - label=configuration.label, - name=configuration.id, - value=value, - type=configuration.type.value, - select=configuration.options, - ) - results[configuration.id] = config_option - for config_group in configurable_options.config_groups(): - start = config_group.start - 1 - stop = config_group.stop - options = list(results.values())[start:stop] - for option in options: - option.group = config_group.name - return results - - -def get_links(session, node): - """ - Retrieve a list of links for grpc to use - - :param core.emulator.Session session: node's section - :param core.nodes.base.CoreNode node: node to get links from - :return: [core.api.grpc.core_pb2.Link] - """ - links = [] - for link_data in node.all_link_data(0): - link = convert_link(session, link_data) - links.append(link) - return links - - -def get_emane_model_id(node_id, interface_id): - """ - Get EMANE model id - - :param int node_id: node id - :param int interface_id: interface id - :return: EMANE model id - :rtype: int - """ - if interface_id >= 0: - return node_id * 1000 + interface_id - else: - return node_id - - -def convert_link(session, link_data): - """ - Convert link_data into core protobuf Link - - :param core.emulator.session.Session session: - :param core.emulator.data.LinkData link_data: - :return: core protobuf Link - :rtype: core.api.grpc.core_pb2.Link - """ - interface_one = None - if link_data.interface1_id is not None: - node = session.get_node(link_data.node1_id) - interface_name = None - if not isinstance(node, CoreNetworkBase): - interface = node.netif(link_data.interface1_id) - interface_name = interface.name - interface_one = core_pb2.Interface( - id=link_data.interface1_id, - name=interface_name, - mac=convert_value(link_data.interface1_mac), - ip4=convert_value(link_data.interface1_ip4), - ip4mask=link_data.interface1_ip4_mask, - ip6=convert_value(link_data.interface1_ip6), - ip6mask=link_data.interface1_ip6_mask, - ) - - interface_two = None - if link_data.interface2_id is not None: - node = session.get_node(link_data.node2_id) - interface_name = None - if not isinstance(node, CoreNetworkBase): - interface = node.netif(link_data.interface2_id) - interface_name = interface.name - interface_two = core_pb2.Interface( - id=link_data.interface2_id, - name=interface_name, - mac=convert_value(link_data.interface2_mac), - ip4=convert_value(link_data.interface2_ip4), - ip4mask=link_data.interface2_ip4_mask, - ip6=convert_value(link_data.interface2_ip6), - ip6mask=link_data.interface2_ip6_mask, - ) - - options = core_pb2.LinkOptions( - opaque=link_data.opaque, - jitter=link_data.jitter, - key=link_data.key, - mburst=link_data.mburst, - mer=link_data.mer, - per=link_data.per, - bandwidth=link_data.bandwidth, - burst=link_data.burst, - delay=link_data.delay, - dup=link_data.dup, - unidirectional=link_data.unidirectional, - ) - - return core_pb2.Link( - type=link_data.link_type, - node_one_id=link_data.node1_id, - node_two_id=link_data.node2_id, - interface_one=interface_one, - interface_two=interface_two, - options=options, - ) - - -def get_net_stats(): - """ - Retrieve status about the current interfaces in the system - - :return: send and receive status of the interfaces in the system - :rtype: dict - """ - with open("/proc/net/dev", "r") as f: - data = f.readlines()[2:] - - stats = {} - for line in data: - line = line.strip() - if not line: - continue - line = line.split() - line[0] = line[0].strip(":") - stats[line[0]] = {"rx": float(line[1]), "tx": float(line[9])} - - return stats - - class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ Create CoreGrpcServer instance @@ -279,12 +123,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): os.mkdir(session.session_dir) # create nodes - # loop = asyncio.new_event_loop() - # asyncio.set_event_loop(loop) - # results = loop.run_until_complete( - # grpcutils.create_nodes(loop, session, request.nodes) - # ) - grpcutils.sync_create_nodes(session, request.nodes) + grpcutils.create_nodes(session, request.nodes) + + # create links + logging.info("links: %s", request.links) + grpcutils.create_links(session, request.links) # set to instantiation and start session.set_state(EventTypes.INSTANTIATION_STATE) @@ -302,7 +145,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :rtype: core.api.grpc.core_pb2.StopSessionResponse """ logging.debug("stop session: %s", request) - session = self.coreemu.create_session(request.session_id) + session = self.get_session(request.session_id, context) session.set_state(EventTypes.DATACOLLECT_STATE) session.clear() return core_pb2.StopSessionResponse(result=True) @@ -808,32 +651,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("add node: %s", request) session = self.get_session(request.session_id, context) - - node_proto = request.node - node_id = node_proto.id - node_type = node_proto.type - if node_type is None: - node_type = NodeTypes.DEFAULT.value - node_type = NodeTypes(node_type) - - options = NodeOptions(name=node_proto.name, model=node_proto.model) - options.icon = node_proto.icon - options.opaque = node_proto.opaque - options.image = node_proto.image - options.services = node_proto.services - if node_proto.server: - options.server = node_proto.server - - position = node_proto.position - options.set_position(position.x, position.y) - options.set_location(position.lat, position.lon, position.alt) - node = session.add_node(_type=node_type, _id=node_id, options=options) - + _type, _id, options = grpcutils.add_node_data(request.node) + node = session.add_node(_type=_type, _id=_id, options=options) # configure emane if provided - emane_model = node_proto.emane + emane_model = request.node.emane if emane_model: - session.emane.set_model_config(node_id, emane_model) - + session.emane.set_model_config(id, emane_model) return core_pb2.AddNodeResponse(node_id=node.id) def GetNode(self, request, context): @@ -991,82 +814,16 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :rtype: core.api.grpc.AddLinkResponse """ logging.debug("add link: %s", request) + # validate session and nodes session = self.get_session(request.session_id, context) - - # validate node exist self.get_node(session, request.link.node_one_id, context) self.get_node(session, request.link.node_two_id, context) + node_one_id = request.link.node_one_id node_two_id = request.link.node_two_id - - interface_one = None - interface_one_data = request.link.interface_one - if interface_one_data: - name = interface_one_data.name - if name == "": - name = None - mac = interface_one_data.mac - if mac == "": - mac = None - else: - mac = MacAddress.from_string(mac) - interface_one = InterfaceData( - _id=interface_one_data.id, - name=name, - mac=mac, - ip4=interface_one_data.ip4, - ip4_mask=interface_one_data.ip4mask, - ip6=interface_one_data.ip6, - ip6_mask=interface_one_data.ip6mask, - ) - - interface_two = None - interface_two_data = request.link.interface_two - if interface_two_data: - name = interface_two_data.name - if name == "": - name = None - mac = interface_two_data.mac - if mac == "": - mac = None - else: - mac = MacAddress.from_string(mac) - interface_two = InterfaceData( - _id=interface_two_data.id, - name=name, - mac=mac, - ip4=interface_two_data.ip4, - ip4_mask=interface_two_data.ip4mask, - ip6=interface_two_data.ip6, - ip6_mask=interface_two_data.ip6mask, - ) - - link_type = None - link_type_value = request.link.type - if link_type_value is not None: - link_type = LinkTypes(link_type_value) - - options_data = request.link.options - link_options = LinkOptions(_type=link_type) - if options_data: - link_options.delay = options_data.delay - link_options.bandwidth = options_data.bandwidth - link_options.per = options_data.per - link_options.dup = options_data.dup - link_options.jitter = options_data.jitter - link_options.mer = options_data.mer - link_options.burst = options_data.burst - link_options.mburst = options_data.mburst - link_options.unidirectional = options_data.unidirectional - link_options.key = options_data.key - link_options.opaque = options_data.opaque - + interface_one, interface_two, options = grpcutils.add_link_data(request.link) session.add_link( - node_one_id, - node_two_id, - interface_one, - interface_two, - link_options=link_options, + node_one_id, node_two_id, interface_one, interface_two, link_options=options ) return core_pb2.AddLinkResponse(result=True) diff --git a/daemon/examples/grpc/large.py b/daemon/examples/grpc/large.py index c2a4d0b6..ef1e6cc4 100644 --- a/daemon/examples/grpc/large.py +++ b/daemon/examples/grpc/large.py @@ -26,47 +26,27 @@ def main(): node = core_pb2.Node(id=i, position=position, model="PC") nodes.append(node) - # start session + # create links + interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/16") links = [] + for node in nodes: + interface_one = interface_helper.create_interface(node.id, 0) + link = core_pb2.Link( + type=core_pb2.LinkType.WIRED, + node_one_id=node.id, + node_two_id=switch.id, + interface_one=interface_one, + ) + links.append(link) + + # start session response = core.start_session(session_id, nodes, links) logging.info("started session: %s", response) - # handle events session may broadcast - # core.events(session_id, log_event) + input("press enter to shutdown session") - # change session state - # response = core.set_session_state( - # session_id, core_pb2.SessionState.CONFIGURATION - # ) - # logging.info("set session state: %s", response) - - # create switch node - # switch = core_pb2.Node(type=core_pb2.NodeType.SWITCH) - # response = core.add_node(session_id, switch) - # logging.info("created switch: %s", response) - # switch_id = response.node_id - - # helper to create interfaces - # interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/16") - # - # for i in range(2): - # # create node - # position = core_pb2.Position(x=50 + 50 * i, y=50) - # node = core_pb2.Node(position=position) - # response = core.add_node(session_id, node) - # logging.info("created node: %s", response) - # node_id = response.node_id - # - # # create link - # interface_one = interface_helper.create_interface(node_id, 0) - # response = core.add_link(session_id, node_id, switch_id, interface_one) - # logging.info("created link: %s", response) - - # change session state - # response = core.set_session_state( - # session_id, core_pb2.SessionState.INSTANTIATION - # ) - # logging.info("set session state: %s", response) + response = core.stop_session(session_id) + logging.info("stop sessionL %s", response) if __name__ == "__main__": From 2a32a5b1a2c37cfd7356302c57359e108192922b Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 29 Oct 2019 09:04:16 -0700 Subject: [PATCH 132/462] adjust open xml --- coretk/coretk/app.py | 7 +- coretk/coretk/appcache.py | 33 +++++++++ coretk/coretk/coregrpc.py | 13 +--- coretk/coretk/coremenubar.py | 10 ++- coretk/coretk/coretoolbar.py | 45 ++++++------ coretk/coretk/coretoolbarhelp.py | 31 ++++++++ coretk/coretk/graph.py | 37 ++++++---- coretk/coretk/graph_helper.py | 15 +++- coretk/coretk/grpcmanagement.py | 19 ++--- coretk/coretk/menuaction.py | 14 ++-- coretk/coretk/prev_saved_xml.txt | 2 - coretk/coretk/setwallpaper.py | 117 ++++++++++++++++++++++++++----- coretk/coretk/sizeandscale.py | 22 ++++++ 13 files changed, 277 insertions(+), 88 deletions(-) create mode 100644 coretk/coretk/appcache.py create mode 100644 coretk/coretk/coretoolbarhelp.py delete mode 100644 coretk/coretk/prev_saved_xml.txt diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 1f85fb9f..644f1eb2 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,6 +1,7 @@ import logging import tkinter as tk +import coretk.appcache as appcache import coretk.images as images from coretk.coregrpc import CoreGrpc from coretk.coremenubar import CoreMenubar @@ -13,6 +14,8 @@ from coretk.menuaction import MenuAction class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) + appcache.cache_variable(self) + print(self.is_open_xml) self.load_images() self.setup_app() self.menubar = None @@ -51,7 +54,7 @@ class Application(tk.Frame): def create_widgets(self): edit_frame = tk.Frame(self) edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) - self.core_editbar = CoreToolbar(self.master, edit_frame, self.menubar) + self.core_editbar = CoreToolbar(self, edit_frame, self.menubar) self.core_editbar.create_toolbar() def draw_canvas(self): @@ -63,7 +66,7 @@ class Application(tk.Frame): ) self.canvas.pack(fill=tk.BOTH, expand=True) - self.core_editbar.update_canvas(self.canvas) + self.core_editbar.canvas = self.canvas scroll_x = tk.Scrollbar( self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview diff --git a/coretk/coretk/appcache.py b/coretk/coretk/appcache.py new file mode 100644 index 00000000..b2f87f71 --- /dev/null +++ b/coretk/coretk/appcache.py @@ -0,0 +1,33 @@ +""" +stores some information helpful for setting starting values for some tables +like size and scale, set wallpaper, etc +""" +import tkinter as tk + + +def cache_variable(application): + # for menubar + application.is_open_xml = False + + application.size_and_scale = None + application.set_wallpaper = None + + # set wallpaper variables + + # canvas id of the wallpaper + application.wallpaper_id = None + + # current image for wallpaper + application.current_wallpaper = None + + # wallpaper option + application.radiovar = tk.IntVar() + application.radiovar.set(1) + + # show grid option + application.show_grid_var = tk.IntVar() + application.show_grid_var.set(1) + + # adjust canvas to image dimension variable + application.adjust_to_dim_var = tk.IntVar() + application.adjust_to_dim_var.set(0) diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 047e7c92..9b51116a 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -190,7 +190,7 @@ class CoreGrpc: logging.info("delete nodes %s", response) def delete_links(self, delete_session=None): - sid = None + # sid = None if delete_session is None: sid = self.session_id else: @@ -228,7 +228,6 @@ class CoreGrpc: ip4=interface.ipv4, ip4mask=interface.ip4prefix, ) - # if1 = core_pb2.Interface(id=id1, name=edge.interface_1.name, ip4=edge.interface_1.ipv4, ip4mask=edge.interface_1.ip4prefix) logging.debug("create interface 1 %s", if1) # interface1 = self.interface_helper.create_interface(id1, 0) @@ -241,15 +240,12 @@ class CoreGrpc: ip4=interface.ipv4, ip4mask=interface.ip4prefix, ) - # if2 = core_pb2.Interface(id=id2, name=edge.interface_2.name, ip4=edge.interface_2.ipv4, ip4mask=edge.interface_2.ip4prefix) logging.debug("create interface 2: %s", if2) - # interface2 = self.interface_helper.create_interface(id2, 0) - # response = self.core.add_link(self.session_id, id1, id2, interface1, interface2) response = self.core.add_link(self.session_id, id1, id2, if1, if2) logging.info("created link: %s", response) - self.core.get_node_links(self.session_id, id1) + # self.core.get_node_links(self.session_id, id1) # def get_session(self): # response = self.core.get_session(self.session_id) @@ -283,10 +279,7 @@ class CoreGrpc: """ response = self.core.open_xml(file_path) self.session_id = response.session_id - # print("Sessionz") - # self.core.events(self.session_id, self.log_event) - # return response.session_id - # logging.info("coregrpc.py open_xml()", type(response)) + logging.debug("coreprgc.py open_xml(): %s", response.result) def close(self): """ diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index a9780072..1129e8b7 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -66,13 +66,11 @@ class CoreMenubar(object): underline=0, ) file_menu.add_command(label="Reload", command=action.file_reload, underline=0) - file_menu.add_command( - label="Save", command=action.file_save, accelerator="Ctrl+S", underline=0 - ) + # file_menu.add_command( + # label="Save", command=action.file_save, accelerator="Ctrl+S", underline=0 + # ) # file_menu.add_command(label="Save As XML...", command=action.file_save_as_xml) - file_menu.add_command( - label="Save As XML...", command=self.menu_action.file_save_as_xml - ) + file_menu.add_command(label="Save", command=self.menu_action.file_save_as_xml) file_menu.add_separator() diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 4e263d28..d46a9791 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -3,12 +3,11 @@ import tkinter as tk from enum import Enum from core.api.grpc import core_pb2 +from coretk.coretoolbarhelp import CoreToolbarHelp from coretk.graph import GraphMode from coretk.images import ImageEnum, Images from coretk.tooltip import CreateToolTip -# from coretk.graph_helper import WlanConnection - class SessionStateEnum(Enum): NONE = "none" @@ -25,13 +24,14 @@ class CoreToolbar(object): Core toolbar class """ - def __init__(self, master, edit_frame, menubar): + def __init__(self, application, edit_frame, menubar): """ Create a CoreToolbar instance :param tkinter.Frame edit_frame: edit frame """ - self.master = master + self.application = application + self.master = application.master self.edit_frame = edit_frame self.menubar = menubar self.radio_value = tk.IntVar() @@ -50,15 +50,6 @@ class CoreToolbar(object): self.canvas = None - def update_canvas(self, canvas): - """ - Update canvas variable in CoreToolbar class - - :param tkinter.Canvas canvas: core canvas - :return: nothing - """ - self.canvas = canvas - def destroy_previous_frame(self): """ Destroy any extra frame from previous before drawing a new one @@ -169,6 +160,7 @@ class CoreToolbar(object): :return: nothing """ logging.debug("Click START STOP SESSION button") + helper = CoreToolbarHelp(self.application) # self.destroy_children_widgets(self.edit_frame) self.destroy_children_widgets() self.canvas.mode = GraphMode.SELECT @@ -176,24 +168,29 @@ class CoreToolbar(object): # set configuration state state = self.canvas.core_grpc.get_session_state() - if state == core_pb2.SessionState.SHUTDOWN: + if state == core_pb2.SessionState.SHUTDOWN or self.application.is_open_xml: self.canvas.core_grpc.set_session_state(SessionStateEnum.DEFINITION.value) + self.application.is_open_xml = False self.canvas.core_grpc.set_session_state(SessionStateEnum.CONFIGURATION.value) - for node in self.canvas.grpc_manager.nodes.values(): - self.canvas.core_grpc.add_node( - node.type, node.model, int(node.x), int(node.y), node.name, node.node_id - ) - - for edge in self.canvas.grpc_manager.edges.values(): - self.canvas.core_grpc.add_link( - edge.id1, edge.id2, edge.type1, edge.type2, edge - ) + helper.add_nodes() + helper.add_edges() + # for node in self.canvas.grpc_manager.nodes.values(): + # print(node.type, node.model, int(node.x), int(node.y), node.name, node.node_id) + # self.canvas.core_grpc.add_node( + # node.type, node.model, int(node.x), int(node.y), node.name, node.node_id + # ) + # print(len(self.canvas.grpc_manager.edges)) + # for edge in self.canvas.grpc_manager.edges.values(): + # print(edge.id1, edge.id2, edge.type1, edge.type2) + # self.canvas.core_grpc.add_link( + # edge.id1, edge.id2, edge.type1, edge.type2, edge + # ) self.canvas.core_grpc.set_session_state(SessionStateEnum.INSTANTIATION.value) - # self.canvas.core_grpc.get_session() + # self.application.is_open_xml = False self.create_runtime_toolbar() def click_link_tool(self): diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py new file mode 100644 index 00000000..a02c7ae9 --- /dev/null +++ b/coretk/coretk/coretoolbarhelp.py @@ -0,0 +1,31 @@ +""" +CoreToolbar help to draw on canvas, and make grpc client call +""" + + +class CoreToolbarHelp: + def __init__(self, application): + self.application = application + self.core_grpc = application.core_grpc + + def add_nodes(self): + """ + add the nodes stored in grpc manager + :return: nothing + """ + grpc_manager = self.application.canvas.grpc_manager + for node in grpc_manager.nodes.values(): + self.application.core_grpc.add_node( + node.type, node.model, int(node.x), int(node.y), node.name, node.node_id + ) + + def add_edges(self): + """ + add the edges stored in grpc manager + :return: + """ + grpc_manager = self.application.canvas.grpc_manager + for edge in grpc_manager.edges.values(): + self.application.core_grpc.add_link( + edge.id1, edge.id2, edge.type1, edge.type2, edge + ) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 9f4b87db..4107401b 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -43,7 +43,7 @@ class CanvasGraph(tk.Canvas): self.core_grpc = grpc self.grpc_manager = GrpcManager(grpc) - self.helper = GraphHelper(self) + self.helper = GraphHelper(self, grpc) # self.core_id_to_canvas_id = {} # self.core_map = CoreToCanvasMapping() # self.draw_existing_component() @@ -61,7 +61,8 @@ class CanvasGraph(tk.Canvas): :return: """ # delete any existing drawn items - self.delete_components() + # self.delete_components() + self.helper.delete_canvas_components() # set the private variables to default value self.mode = GraphMode.SELECT @@ -73,12 +74,14 @@ class CanvasGraph(tk.Canvas): self.edges = {} self.drawing_edge = None + print("graph.py create a new grpc manager") self.grpc_manager = GrpcManager(new_grpc) # new grpc self.core_grpc = new_grpc - + print("grpah.py draw existing component") self.draw_existing_component() + print(self.grpc_manager.edges) def setup_bindings(self): """ @@ -130,6 +133,8 @@ class CanvasGraph(tk.Canvas): for node in session.nodes: # peer to peer node is not drawn on the GUI if node.type != core_pb2.NodeType.PEER_TO_PEER: + + # draw nodes on the canvas image, name = Images.convert_type_and_model_to_image( node.type, node.model ) @@ -138,7 +143,10 @@ class CanvasGraph(tk.Canvas): ) self.nodes[n.id] = n core_id_to_canvas_id[node.id] = n.id + + # store the node in grpc manager self.grpc_manager.add_preexisting_node(n, session_id, node, name) + self.grpc_manager.update_reusable_id() # draw existing links @@ -165,6 +173,9 @@ class CanvasGraph(tk.Canvas): self, is_wired=False, ) + edge_token = tuple(sorted((n1.id, n2.id))) + e.token = edge_token + e.dst = n2.id n1.edges.add(e) n2.edges.add(e) self.edges[e.token] = e @@ -195,8 +206,8 @@ class CanvasGraph(tk.Canvas): ) # TODO will include throughput and ipv6 in the future - if1 = Interface(grpc_if1.name, grpc_if1.ip4) - if2 = Interface(grpc_if2.name, grpc_if2.ip4) + if1 = Interface(grpc_if1.name, grpc_if1.ip4, ifid=grpc_if1.id) + if2 = Interface(grpc_if2.name, grpc_if2.ip4, ifid=grpc_if2.id) self.grpc_manager.edges[e.token].interface_1 = if1 self.grpc_manager.edges[e.token].interface_2 = if2 self.grpc_manager.nodes[ @@ -207,14 +218,16 @@ class CanvasGraph(tk.Canvas): ].interfaces.append(if2) # lift the nodes so they on top of the links - for i in core_id_to_canvas_id.values(): + # for i in core_id_to_canvas_id.values(): + # self.lift(i) + for i in self.find_withtag("node"): self.lift(i) - def delete_components(self): - tags = ["node", "edge", "linkinfo", "nodename"] - for i in tags: - for id in self.find_withtag(i): - self.delete(id) + # def delete_components(self): + # tags = ["node", "edge", "linkinfo", "nodename"] + # for i in tags: + # for id in self.find_withtag(i): + # self.delete(id) def canvas_xy(self, event): """ @@ -237,7 +250,6 @@ class CanvasGraph(tk.Canvas): :return: the item that the mouse point to """ overlapping = self.find_overlapping(event.x, event.y, event.x, event.y) - print(overlapping) nodes = set(self.find_withtag("node")) selected = None for _id in overlapping: @@ -263,7 +275,6 @@ class CanvasGraph(tk.Canvas): self.focus_set() self.selected = self.get_selected(event) logging.debug(f"click release selected: {self.selected}") - print(self.mode) if self.mode == GraphMode.EDGE: self.handle_edge_release(event) elif self.mode == GraphMode.NODE: diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py index 2fc6e011..64f38acf 100644 --- a/coretk/coretk/graph_helper.py +++ b/coretk/coretk/graph_helper.py @@ -7,13 +7,26 @@ import tkinter as tk from core.api.grpc import core_pb2 from coretk.images import ImageEnum, Images +CANVAS_COMPONENT_TAGS = ["edge", "node", "nodename", "wallpaper", "linkinfo"] + class GraphHelper: - def __init__(self, canvas): + def __init__(self, canvas, grpc): """ create an instance of GraphHelper object """ self.canvas = canvas + self.core_grpc = grpc + + def delete_canvas_components(self): + """ + delete the components of the graph leaving only the blank canvas + + :return: nothing + """ + for tag in CANVAS_COMPONENT_TAGS: + for i in self.canvas.find_withtag(tag): + self.canvas.delete(i) def draw_wireless_case(self, src_id, dst_id, edge): src_node_name = self.canvas.nodes[src_id].node_type diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index ebd34f6c..9638c7c5 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -160,13 +160,17 @@ class GrpcManager: """ Add preexisting nodes to grpc manager + :param str name: node_type :param core_pb2.Node core_node: core node grpc message :param coretk.graph.CanvasNode canvas_node: canvas node :param int session_id: session id :return: nothing """ + + # update the next available id core_id = core_node.id - if core_id >= self.id: + print(core_id) + if self.id is None or core_id >= self.id: self.id = core_id + 1 self.preexisting.append(core_id) n = Node( @@ -198,12 +202,13 @@ class GrpcManager: :return: nothing """ - for i in range(1, self.id): - if i not in self.preexisting: - self.reusable.append(i) + if len(self.preexisting) > 0: + for i in range(1, self.id): + if i not in self.preexisting: + self.reusable.append(i) - self.preexisting.clear() - logging.debug("Next id: %s, Reusable: %s", self.id, self.reusable) + self.preexisting.clear() + logging.debug("Next id: %s, Reusable: %s", self.id, self.reusable) def delete_node(self, canvas_id): """ @@ -263,8 +268,6 @@ class GrpcManager: edge.interface_1 = src_interface edge.interface_2 = dst_interface - print(src_interface) - print(dst_interface) return src_interface, dst_interface def add_edge(self, session_id, token, canvas_id_1, canvas_id_2): diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 4a1332d2..999801c2 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -37,8 +37,8 @@ def file_reload(): logging.debug("Click file Reload") -def file_save(): - logging.debug("Click file save") +# def file_save(): +# logging.debug("Click file save") def file_save_shortcut(event): @@ -390,6 +390,7 @@ class MenuAction: def file_open_xml(self): logging.info("menuaction.py file_open_xml()") + self.application.is_open_xml = True file_path = filedialog.askopenfilename( initialdir=SAVEDIR, title="Open", @@ -413,16 +414,19 @@ class MenuAction: self.application.core_grpc = core_grpc self.application.core_editbar.destroy_children_widgets() - self.application.core_editbar.create_runtime_toolbar() + self.application.core_editbar.create_toolbar() + # self.application.is_open_xml = False + + # self.application.core_editbar.create_runtime_toolbar() # self.application.canvas.draw_existing_component() # t1 = time.clock() # print(t1 - t0) def canvas_size_and_scale(self): - SizeAndScale(self.application) + self.application.size_and_scale = SizeAndScale(self.application) def canvas_set_wallpaper(self): - CanvasWallpaper(self.application) + self.application.set_wallpaper = CanvasWallpaper(self.application) def help_core_github(self): webbrowser.open_new("https://github.com/coreemu/core") diff --git a/coretk/coretk/prev_saved_xml.txt b/coretk/coretk/prev_saved_xml.txt deleted file mode 100644 index bfb9d15a..00000000 --- a/coretk/coretk/prev_saved_xml.txt +++ /dev/null @@ -1,2 +0,0 @@ -/home/ncs/Desktop/runningtest.xml -/home/ncs/Desktop/notrunning.xml diff --git a/coretk/coretk/setwallpaper.py b/coretk/coretk/setwallpaper.py index a4a8c908..beec0162 100644 --- a/coretk/coretk/setwallpaper.py +++ b/coretk/coretk/setwallpaper.py @@ -14,6 +14,7 @@ WALLPAPER_DIR = os.path.join(PATH, "wallpaper") class ScaleOption(enum.Enum): + NONE = 0 UPPER_LEFT = 1 CENTERED = 2 SCALED = 3 @@ -22,15 +23,23 @@ class ScaleOption(enum.Enum): class CanvasWallpaper: def __init__(self, application): + """ + create an instance of CanvasWallpaper object + + :param coretk.app.Application application: root application + """ self.application = application self.canvas = self.application.canvas self.top = tk.Toplevel() self.top.title("Set Canvas Wallpaper") self.radiovar = tk.IntVar() + print(self.application.radiovar.get()) + self.radiovar.set(self.application.radiovar.get()) self.show_grid_var = tk.IntVar() + self.show_grid_var.set(self.application.show_grid_var.get()) self.adjust_to_dim_var = tk.IntVar() - self.wallpaper = None + self.adjust_to_dim_var.set(self.application.adjust_to_dim_var.get()) self.create_image_label() self.create_text_label() @@ -75,6 +84,11 @@ class CanvasWallpaper: img_label.image = tk_img def clear_link(self): + """ + delete like shown in image link entry if there is any + + :return: nothing + """ # delete entry img_open_frame = self.top.grid_slaves(2, 0)[0] filename_entry = img_open_frame.grid_slaves(0, 0)[0] @@ -115,10 +129,28 @@ class CanvasWallpaper: b4 = tk.Radiobutton(f, text="titled", value=4, variable=self.radiovar) b4.grid(row=0, column=3) - self.radiovar.set(1) + # self.radiovar.set(1) f.grid() + def adjust_canvas_size(self): + + # deselect all radio buttons and grey them out + if self.adjust_to_dim_var.get() == 1: + self.radiovar.set(0) + option_frame = self.top.grid_slaves(3, 0)[0] + for i in option_frame.grid_slaves(): + i.config(state=tk.DISABLED) + + # turn back the radio button to active state so that user can choose again + elif self.adjust_to_dim_var.get() == 0: + option_frame = self.top.grid_slaves(3, 0)[0] + for i in option_frame.grid_slaves(): + i.config(state=tk.NORMAL) + self.radiovar.set(1) + else: + logging.error("setwallpaper.py adjust_canvas_size invalid value") + def additional_options(self): b = tk.Checkbutton(self.top, text="Show grid", variable=self.show_grid_var) b.grid(sticky=tk.W, padx=5) @@ -126,15 +158,21 @@ class CanvasWallpaper: self.top, text="Adjust canvas size to image dimensions", variable=self.adjust_to_dim_var, + command=self.adjust_canvas_size, ) b.grid(sticky=tk.W, padx=5) self.show_grid_var.set(1) self.adjust_to_dim_var.set(0) - def delete_previous_wallpaper(self): - prev_wallpaper = self.canvas.find_withtag("wallpaper") - if prev_wallpaper: - for i in prev_wallpaper: + def delete_canvas_components(self, tag_list): + """ + delete canvas items whose tag is in the tag list + + :param list[string] tag_list: list of tags + :return: nothing + """ + for tag in tag_list: + for i in self.canvas.find_withtag(tag): self.canvas.delete(i) def get_canvas_width_and_height(self): @@ -159,6 +197,7 @@ class CanvasWallpaper: return def upper_left(self, img): + print("upperleft") tk_img = ImageTk.PhotoImage(img) # crop image if it is bigger than canvas @@ -178,7 +217,8 @@ class CanvasWallpaper: # place left corner of image to the left corner of the canvas self.application.croppedwallpaper = cropped_tk - self.delete_previous_wallpaper() + self.delete_canvas_components(["wallpaper"]) + # self.delete_previous_wallpaper() wid = self.canvas.create_image( (cropx / 2, cropy / 2), image=cropped_tk, tags="wallpaper" @@ -213,7 +253,8 @@ class CanvasWallpaper: # place the center of the image at the center of the canvas self.application.croppedwallpaper = cropped_tk - self.delete_previous_wallpaper() + self.delete_canvas_components(["wallpaper"]) + # self.delete_previous_wallpaper() wid = self.canvas.create_image( (canvas_w / 2, canvas_h / 2), image=cropped_tk, tags="wallpaper" ) @@ -231,7 +272,8 @@ class CanvasWallpaper: image_tk = ImageTk.PhotoImage(resized_image) self.application.croppedwallpaper = image_tk - self.delete_previous_wallpaper() + self.delete_canvas_components(["wallpaper"]) + # self.delete_previous_wallpaper() wid = self.canvas.create_image( (canvas_w / 2, canvas_h / 2), image=image_tk, tags="wallpaper" @@ -241,11 +283,34 @@ class CanvasWallpaper: def tiled(self, img): return + def draw_new_canvas(self, canvas_width, canvas_height): + """ + delete the old canvas and draw a new one + + :param int canvas_width: canvas width in pixel + :param int canvas_height: canvas height in pixel + :return: + """ + self.delete_canvas_components(["rectangle", "gridline"]) + self.canvas.draw_grid(canvas_width, canvas_height) + + def canvas_to_image_dimension(self, img): + image_tk = ImageTk.PhotoImage(img) + img_w = image_tk.width() + img_h = image_tk.height() + self.delete_canvas_components(["wallpaper"]) + self.draw_new_canvas(img_w, img_h) + wid = self.canvas.create_image((img_w / 2, img_h / 2), image=image_tk) + self.application.croppedwallpaper = image_tk + self.application.wallpaper_id = wid + def show_grid(self): """ :return: nothing """ + self.application.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) + if self.show_grid_var.get() == 0: for i in self.canvas.find_withtag("gridline"): self.canvas.itemconfig(i, state=tk.HIDDEN) @@ -256,28 +321,46 @@ class CanvasWallpaper: else: logging.error("setwallpaper.py show_grid invalid value") + def save_wallpaper_options(self): + self.application.radiovar.set(self.radiovar.get()) + self.application.show_grid_var.set(self.show_grid_var.get()) + self.application.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) + def click_apply(self): img_link_frame = self.top.grid_slaves(2, 0)[0] filename = img_link_frame.grid_slaves(0, 0)[0].get() if not filename: + self.delete_canvas_components(["wallpaper"]) self.top.destroy() + self.application.current_wallpaper = None + self.save_wallpaper_options() return try: img = Image.open(filename) + self.application.current_wallpaper = img except FileNotFoundError: print("invalid filename, draw original white plot") if self.application.wallpaper_id: self.canvas.delete(self.application.wallpaper_id) self.top.destroy() return - if self.radiovar.get() == ScaleOption.UPPER_LEFT.value: - self.upper_left(img) - elif self.radiovar.get() == ScaleOption.CENTERED.value: - self.center(img) - elif self.radiovar.get() == ScaleOption.SCALED.value: - self.scaled(img) - elif self.radiovar.get() == ScaleOption.TILED.value: - print("not implemented yet") + + self.application.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) + if self.adjust_to_dim_var.get() == 0: + + self.application.radiovar.set(self.radiovar.get()) + + if self.radiovar.get() == ScaleOption.UPPER_LEFT.value: + self.upper_left(img) + elif self.radiovar.get() == ScaleOption.CENTERED.value: + self.center(img) + elif self.radiovar.get() == ScaleOption.SCALED.value: + self.scaled(img) + elif self.radiovar.get() == ScaleOption.TILED.value: + print("not implemented yet") + + elif self.adjust_to_dim_var.get() == 1: + self.canvas_to_image_dimension(img) self.show_grid() self.top.destroy() diff --git a/coretk/coretk/sizeandscale.py b/coretk/coretk/sizeandscale.py index 44d3adbc..3552c2e5 100644 --- a/coretk/coretk/sizeandscale.py +++ b/coretk/coretk/sizeandscale.py @@ -4,6 +4,8 @@ size and scale import tkinter as tk from functools import partial +from coretk.setwallpaper import ScaleOption + DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] @@ -246,6 +248,26 @@ class SizeAndScale: meter_per_pixel = float(scale_frame.grid_slaves(0, 1)[0].get()) / 100 self.application.canvas.meters_per_pixel = meter_per_pixel self.redraw_grid(pixel_width, pixel_height) + print(self.application.current_wallpaper) + print(self.application.radiovar) + # if there is a current wallpaper showing, redraw it based on current wallpaper options + wallpaper_tool = self.application.set_wallpaper + current_wallpaper = self.application.current_wallpaper + if current_wallpaper: + if self.application.adjust_to_dim_var.get() == 0: + if self.application.radiovar.get() == ScaleOption.UPPER_LEFT.value: + wallpaper_tool.upper_left(current_wallpaper) + elif self.application.radiovar.get() == ScaleOption.CENTERED.value: + wallpaper_tool.center(current_wallpaper) + elif self.application.radiovar.get() == ScaleOption.SCALED.value: + wallpaper_tool.scaled(current_wallpaper) + elif self.application.radiovar.get() == ScaleOption.TILED.value: + print("not implemented") + elif self.application.adjust_to_dim_var.get() == 1: + wallpaper_tool.canvas_to_image_dimension(current_wallpaper) + + wallpaper_tool.show_grid() + self.top.destroy() def apply_cancel(self): From 4e03dc6888808f58263fc1f12fe30f323562c41b Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 29 Oct 2019 10:25:39 -0700 Subject: [PATCH 133/462] updates to grpc StartSession, added utility threadpool function to help improve speed when running certain tasks, made use of utility threadpool function where needed --- daemon/core/api/grpc/grpcutils.py | 57 +++++++++------------------- daemon/core/api/grpc/server.py | 3 +- daemon/core/emulator/session.py | 56 +++++++++++++++------------ daemon/core/services/coreservices.py | 17 +++------ daemon/core/utils.py | 27 +++++++++++++ 5 files changed, 83 insertions(+), 77 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index aec094d3..ea166328 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -1,7 +1,7 @@ -import concurrent.futures import logging import time +from core import utils from core.api.grpc import core_pb2 from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.enumerations import LinkTypes, NodeTypes @@ -112,23 +112,15 @@ def create_nodes(session, node_protos): :return: results and exceptions for created nodes :rtype: tuple """ + funcs = [] + for node_proto in node_protos: + _type, _id, options = add_node_data(node_proto) + args = (_type, _id, options) + funcs.append((session.add_node, args, {})) start = time.monotonic() - with concurrent.futures.ThreadPoolExecutor(max_workers=WORKERS) as executor: - futures = [] - for node_proto in node_protos: - _type, _id, options = add_node_data(node_proto) - future = executor.submit(session.add_node, _type, _id, options) - futures.append(future) - results = [] - exceptions = [] - for future in concurrent.futures.as_completed(futures): - try: - result = future.result() - results.append(result) - except Exception as executor: - exceptions.append(executor) + results, exceptions = utils.threadpool(funcs) total = time.monotonic() - start - logging.info("created nodes time: %s", total) + logging.debug("grpc created nodes time: %s", total) return results, exceptions @@ -141,32 +133,17 @@ def create_links(session, link_protos): :return: results and exceptions for created links :rtype: tuple """ + funcs = [] + for link_proto in link_protos: + node_one_id = link_proto.node_one_id + node_two_id = link_proto.node_two_id + interface_one, interface_two, options = add_link_data(link_proto) + args = (node_one_id, node_two_id, interface_one, interface_two, options) + funcs.append((session.add_link, args, {})) start = time.monotonic() - with concurrent.futures.ThreadPoolExecutor(max_workers=WORKERS) as executor: - futures = [] - for link_proto in link_protos: - node_one_id = link_proto.node_one_id - node_two_id = link_proto.node_two_id - interface_one, interface_two, options = add_link_data(link_proto) - future = executor.submit( - session.add_link, - node_one_id, - node_two_id, - interface_one, - interface_two, - options, - ) - futures.append(future) - results = [] - exceptions = [] - for future in concurrent.futures.as_completed(futures): - try: - result = future.result() - results.append(result) - except Exception as executor: - exceptions.append(executor) + results, exceptions = utils.threadpool(funcs) total = time.monotonic() - start - logging.info("created links time: %s", total) + logging.debug("grpc created links time: %s", total) return results, exceptions diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index c516ea45..d58d9b9d 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -126,7 +126,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): grpcutils.create_nodes(session, request.nodes) # create links - logging.info("links: %s", request.links) grpcutils.create_links(session, request.links) # set to instantiation and start @@ -146,8 +145,10 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("stop session: %s", request) session = self.get_session(request.session_id, context) + session.data_collect() session.set_state(EventTypes.DATACOLLECT_STATE) session.clear() + session.set_state(EventTypes.SHUTDOWN_STATE) return core_pb2.StopSessionResponse(result=True) def CreateSession(self, request, context): diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index fcebcd9e..768ea524 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -12,7 +12,6 @@ import subprocess import tempfile import threading import time -from multiprocessing.pool import ThreadPool from core import constants, utils from core.emane.emanemanager import EmaneManager @@ -1366,9 +1365,11 @@ class Session: Clear the nodes dictionary, and call shutdown for each node. """ with self._nodes_lock: + funcs = [] while self.nodes: _, node = self.nodes.popitem() - node.shutdown() + funcs.append((node.shutdown, [], {})) + utils.threadpool(funcs) self.node_id_gen.id = 0 def write_nodes(self): @@ -1508,11 +1509,14 @@ class Session: # stop node services with self._nodes_lock: + funcs = [] for node_id in self.nodes: node = self.nodes[node_id] - # TODO: determine if checking for CoreNode alone is ok if isinstance(node, CoreNodeBase): self.services.stop_services(node) + args = (node,) + funcs.append((self.services.stop_services, args, {})) + utils.threadpool(funcs) # shutdown emane self.emane.shutdown() @@ -1520,7 +1524,8 @@ class Session: # update control interface hosts self.update_control_interface_hosts(remove=True) - # remove all four possible control networks. Does nothing if ctrlnet is not installed. + # remove all four possible control networks. Does nothing if ctrlnet is not + # installed. self.add_remove_control_interface(node=None, net_index=0, remove=True) self.add_remove_control_interface(node=None, net_index=1, remove=True) self.add_remove_control_interface(node=None, net_index=2, remove=True) @@ -1551,6 +1556,18 @@ class Session: ssid = (self.id >> 8) ^ (self.id & ((1 << 8) - 1)) return f"{ssid:x}" + def boot_node(self, node): + """ + Boot node by adding a control interface when necessary and starting + node services. + + :param core.nodes.base.CoreNodeBase node: node to boot + :return: nothing + """ + logging.info("booting node(%s): %s", node.name, [x.name for x in node.services]) + self.add_remove_control_interface(node=node, remove=False) + self.services.boot_services(node) + def boot_nodes(self): """ Invoke the boot() procedure for all nodes and send back node @@ -1558,29 +1575,18 @@ class Session: request flag. """ with self._nodes_lock: - pool = ThreadPool() - results = [] - - start = time.time() + funcs = [] + start = time.monotonic() for _id in self.nodes: node = self.nodes[_id] if isinstance(node, CoreNodeBase) and not isinstance(node, Rj45Node): - # add a control interface if configured - logging.info( - "booting node(%s): %s", - node.name, - [x.name for x in node.services], - ) - self.add_remove_control_interface(node=node, remove=False) - result = pool.apply_async(self.services.boot_services, (node,)) - results.append(result) - - pool.close() - pool.join() - for result in results: - result.get() - logging.debug("boot run time: %s", time.time() - start) - + args = (node,) + funcs.append((self.boot_node, args, {})) + results, exceptions = utils.threadpool(funcs) + total = time.monotonic() - start + logging.debug("boot run time: %s", total) + if exceptions: + raise CoreError(exceptions) self.update_control_interface_hosts() def get_control_net_prefixes(self): @@ -1730,7 +1736,7 @@ class Session: If conf_reqd is False, the control network may be built even when the user has not configured one (e.g. for EMANE.) - :param core.nodes.base.CoreNode node: node to add or remove control interface + :param core.nodes.base.CoreNodeBase node: node to add or remove control interface :param int net_index: network index :param bool remove: flag to check if it should be removed :param bool conf_required: flag to check if conf is required diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index 2a4cb178..80168425 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -10,7 +10,6 @@ services. import enum import logging import time -from multiprocessing.pool import ThreadPool from core import utils from core.constants import which @@ -462,18 +461,14 @@ class CoreServices: :param core.netns.vnode.LxcNode node: node to start services on :return: nothing """ - pool = ThreadPool() - results = [] - + funcs = [] boot_paths = ServiceDependencies(node.services).boot_paths() for boot_path in boot_paths: - result = pool.apply_async(self._start_boot_paths, (node, boot_path)) - results.append(result) - - pool.close() - pool.join() - for result in results: - result.get() + args = (node, boot_path) + funcs.append((self._start_boot_paths, args, {})) + result, exceptions = utils.threadpool(funcs) + if exceptions: + raise ServiceBootError(exceptions) def _start_boot_paths(self, node, boot_path): """ diff --git a/daemon/core/utils.py b/daemon/core/utils.py index e16efd1f..413df156 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -2,6 +2,7 @@ Miscellaneous utility functions, wrappers around some subprocess procedures. """ +import concurrent.futures import fcntl import hashlib import importlib @@ -381,3 +382,29 @@ def load_logging_config(config_path): with open(config_path, "r") as log_config_file: log_config = json.load(log_config_file) logging.config.dictConfig(log_config) + + +def threadpool(funcs, workers=10): + """ + Run provided functions, arguments, and keywords within a threadpool + collecting results and exceptions. + + :param iter funcs: iterable that provides a func, args, kwargs + :param int workers: number of workers for the threadpool + :return: results and exceptions from running functions with args and kwargs + :rtype: tuple + """ + with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor: + futures = [] + for func, args, kwargs in funcs: + future = executor.submit(func, *args, **kwargs) + futures.append(future) + results = [] + exceptions = [] + for future in concurrent.futures.as_completed(futures): + try: + result = future.result() + results.append(result) + except Exception as e: + exceptions.append(e) + return results, exceptions From de936ea31568b70ca569276826f0094573f5812a Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 29 Oct 2019 12:35:07 -0700 Subject: [PATCH 134/462] added hook creation and set location to grpc.StartSession --- daemon/core/api/grpc/client.py | 16 ++++-- daemon/core/api/grpc/grpcutils.py | 13 +++++ daemon/core/api/grpc/server.py | 29 ++++++---- daemon/proto/core/api/grpc/core.proto | 11 ++-- daemon/tests/test_grpc.py | 79 ++++++++++++++++++++++++--- 5 files changed, 119 insertions(+), 29 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index ea32ffb4..c10e3d4e 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -148,17 +148,23 @@ class CoreGrpcClient: self.stub = None self.channel = None - def start_session(self, session_id, nodes, links): + def start_session(self, session_id, nodes, links, location=None, hooks=None): """ Start a session. :param int session_id: id of session :param list nodes: list of nodes to create :param list links: list of links to create + :param core_pb2.SessionLocation location: location to set + :param list[core_pb2.Hook] hooks: session hooks to set :return: """ request = core_pb2.StartSessionRequest( - session_id=session_id, nodes=nodes, links=links + session_id=session_id, + nodes=nodes, + links=links, + location=location, + hooks=hooks, ) return self.stub.StartSession(request) @@ -282,9 +288,11 @@ class CoreGrpcClient: :rtype: core_pb2.SetSessionLocationResponse :raises grpc.RpcError: when session doesn't exist """ - position = core_pb2.SessionPosition(x=x, y=y, z=z, lat=lat, lon=lon, alt=alt) + location = core_pb2.SessionLocation( + x=x, y=y, z=z, lat=lat, lon=lon, alt=alt, scale=scale + ) request = core_pb2.SetSessionLocationRequest( - session_id=session_id, position=position, scale=scale + session_id=session_id, location=location ) return self.stub.SetSessionLocation(request) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index ea166328..166807d0 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -306,3 +306,16 @@ def get_net_stats(): stats[line[0]] = {"rx": float(line[1]), "tx": float(line[9])} return stats + + +def session_location(session, location): + """ + Set session location based on location proto. + + :param core.emulator.session.Session session: session for location + :param core_pb2.SessionLocation location: location to set + :return: nothing + """ + session.location.refxyz = (location.x, location.y, location.z) + session.location.setrefgeo(location.lat, location.lon, location.alt) + session.location.refscale = location.scale diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index d58d9b9d..f78f5ead 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -122,9 +122,21 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): if not os.path.exists(session.session_dir): os.mkdir(session.session_dir) + # location + if request.HasField("location"): + grpcutils.session_location(session, request.location) + + # add all hooks + for hook in request.hooks: + session.add_hook(hook.state, hook.file, None, hook.data) + # create nodes grpcutils.create_nodes(session, request.nodes) + # emane configs + # wlan configs + # mobility configs + # create links grpcutils.create_links(session, request.links) @@ -217,10 +229,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session = self.get_session(request.session_id, context) x, y, z = session.location.refxyz lat, lon, alt = session.location.refgeo - position = core_pb2.SessionPosition(x=x, y=y, z=z, lat=lat, lon=lon, alt=alt) - return core_pb2.GetSessionLocationResponse( - position=position, scale=session.location.refscale + scale = session.location.refscale + location = core_pb2.SessionLocation( + x=x, y=y, z=z, lat=lat, lon=lon, alt=alt, scale=scale ) + return core_pb2.GetSessionLocationResponse(location=location) def SetSessionLocation(self, request, context): """ @@ -233,15 +246,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("set session location: %s", request) session = self.get_session(request.session_id, context) - session.location.refxyz = ( - request.position.x, - request.position.y, - request.position.z, - ) - session.location.setrefgeo( - request.position.lat, request.position.lon, request.position.alt - ) - session.location.refscale = request.scale + grpcutils.session_location(session, request.location) return core_pb2.SetSessionLocationResponse(result=True) def SetSessionState(self, request, context): diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 2a766fc3..e8722b94 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -134,6 +134,8 @@ message StartSessionRequest { int32 session_id = 1; repeated Node nodes = 2; repeated Link links = 3; + repeated Hook hooks = 4; + SessionLocation location = 5; } message StartSessionResponse { @@ -202,14 +204,12 @@ message GetSessionLocationRequest { } message GetSessionLocationResponse { - SessionPosition position = 1; - float scale = 2; + SessionLocation location = 1; } message SetSessionLocationRequest { int32 session_id = 1; - SessionPosition position = 2; - float scale = 3; + SessionLocation location = 2; } message SetSessionLocationResponse { @@ -872,13 +872,14 @@ message Interface { int32 mtu = 10; } -message SessionPosition { +message SessionLocation { float x = 1; float y = 2; float z = 3; float lat = 4; float lon = 5; float alt = 6; + float scale = 7; } message Position { diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 2415176a..a86e2b89 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -3,9 +3,10 @@ from queue import Queue import grpc import pytest +from mock import patch from core.api.grpc import core_pb2 -from core.api.grpc.client import CoreGrpcClient +from core.api.grpc.client import CoreGrpcClient, InterfaceHelper from core.config import ConfigShim from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emulator.data import EventData @@ -18,9 +19,71 @@ from core.emulator.enumerations import ( ) from core.errors import CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility +from core.xml.corexml import CoreXmlWriter class TestGrpc: + def test_start_session(self, grpc_server): + # given + client = CoreGrpcClient() + session = grpc_server.coreemu.create_session() + nodes = [] + position = core_pb2.Position(x=50, y=100) + node_one = core_pb2.Node(id=1, position=position, model="PC") + position = core_pb2.Position(x=100, y=100) + node_two = core_pb2.Node(id=2, position=position, model="PC") + nodes.extend([node_one, node_two]) + links = [] + interface_helper = InterfaceHelper(ip4_prefix="10.83.0.0/16") + interface_one = interface_helper.create_interface(node_one.id, 0) + interface_two = interface_helper.create_interface(node_two.id, 0) + link = core_pb2.Link( + type=core_pb2.LinkType.WIRED, + node_one_id=node_one.id, + node_two_id=node_two.id, + interface_one=interface_one, + interface_two=interface_two, + ) + links.append(link) + hooks = [] + hook = core_pb2.Hook( + state=core_pb2.SessionState.RUNTIME, file="echo.sh", data="echo hello" + ) + hooks.append(hook) + location_x = 5 + location_y = 10 + location_z = 15 + location_lat = 20 + location_lon = 30 + location_alt = 40 + location_scale = 5 + location = core_pb2.SessionLocation( + x=location_x, + y=location_y, + z=location_z, + lat=location_lat, + lon=location_lon, + alt=location_alt, + scale=location_scale, + ) + + # when + with patch.object(CoreXmlWriter, "write"): + with client.context_connect(): + client.start_session(session.id, nodes, links, location, hooks) + + # then + assert node_one.id in session.nodes + assert node_two.id in session.nodes + assert session.nodes[node_one.id].netif(0) is not None + assert session.nodes[node_two.id].netif(0) is not None + hook_file, hook_data = session._hooks[core_pb2.SessionState.RUNTIME][0] + assert hook_file == hook.file + assert hook_data == hook.data + assert session.location.refxyz == (location_x, location_y, location_z) + assert session.location.refgeo == (location_lat, location_lon, location_alt) + assert session.location.refscale == location_scale + @pytest.mark.parametrize("session_id", [None, 6013]) def test_create_session(self, grpc_server, session_id): # given @@ -112,13 +175,13 @@ class TestGrpc: response = client.get_session_location(session.id) # then - assert response.scale == 1.0 - assert response.position.x == 0 - assert response.position.y == 0 - assert response.position.z == 0 - assert response.position.lat == 0 - assert response.position.lon == 0 - assert response.position.alt == 0 + assert response.location.scale == 1.0 + assert response.location.x == 0 + assert response.location.y == 0 + assert response.location.z == 0 + assert response.location.lat == 0 + assert response.location.lon == 0 + assert response.location.alt == 0 def test_set_session_location(self, grpc_server): # given From adbab066c9ef16c4232bc9d4d7b9b7925404b86b Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 29 Oct 2019 13:37:37 -0700 Subject: [PATCH 135/462] added wlan configs to grpc.StartSession --- daemon/core/api/grpc/client.py | 18 ++++++++++++++++-- daemon/core/api/grpc/server.py | 15 ++++++++++++--- daemon/proto/core/api/grpc/core.proto | 10 ++++++++-- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index c10e3d4e..5c5ee72c 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -148,7 +148,16 @@ class CoreGrpcClient: self.stub = None self.channel = None - def start_session(self, session_id, nodes, links, location=None, hooks=None): + def start_session( + self, + session_id, + nodes, + links, + location=None, + hooks=None, + emane_config=None, + wlan_configs=None, + ): """ Start a session. @@ -157,6 +166,8 @@ class CoreGrpcClient: :param list links: list of links to create :param core_pb2.SessionLocation location: location to set :param list[core_pb2.Hook] hooks: session hooks to set + :param dict emane_config: emane configuration to set + :param list wlan_configs: wlan configurations to set :return: """ request = core_pb2.StartSessionRequest( @@ -165,6 +176,8 @@ class CoreGrpcClient: links=links, location=location, hooks=hooks, + emane_config=emane_config, + wlan_configs=wlan_configs, ) return self.stub.StartSession(request) @@ -793,8 +806,9 @@ class CoreGrpcClient: :rtype: core_pb2.SetWlanConfigResponse :raises grpc.RpcError: when session doesn't exist """ + wlan_config = core_pb2.WlanConfig(node_id=node_id, config=config) request = core_pb2.SetWlanConfigRequest( - session_id=session_id, node_id=node_id, config=config + session_id=session_id, wlan_config=wlan_config ) return self.stub.SetWlanConfig(request) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index f78f5ead..ac7aba1e 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -134,7 +134,15 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): grpcutils.create_nodes(session, request.nodes) # emane configs + config = session.emane.get_configs() + config.update(request.emane_config) + # wlan configs + for wlan_config in request.wlan_configs: + session.mobility.set_model_config( + wlan_config.node_id, BasicRangeModel.name, wlan_config.config + ) + # mobility configs # create links @@ -1218,12 +1226,13 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("set wlan config: %s", request) session = self.get_session(request.session_id, context) + wlan_config = request.wlan_config session.mobility.set_model_config( - request.node_id, BasicRangeModel.name, request.config + wlan_config.node_id, BasicRangeModel.name, wlan_config.config ) if session.state == EventTypes.RUNTIME_STATE.value: - node = self.get_node(session, request.node_id, context) - node.updatemodel(request.config) + node = self.get_node(session, wlan_config.node_id, context) + node.updatemodel(wlan_config.config) return core_pb2.SetWlanConfigResponse(result=True) def GetEmaneConfig(self, request, context): diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index e8722b94..fa65442f 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -136,6 +136,8 @@ message StartSessionRequest { repeated Link links = 3; repeated Hook hooks = 4; SessionLocation location = 5; + map emane_config = 6; + repeated WlanConfig wlan_configs = 7; } message StartSessionResponse { @@ -575,8 +577,7 @@ message GetWlanConfigResponse { message SetWlanConfigRequest { int32 session_id = 1; - int32 node_id = 2; - map config = 3; + WlanConfig wlan_config = 2; } message SetWlanConfigResponse { @@ -681,6 +682,11 @@ message EmaneLinkResponse { } // data structures for messages below +message WlanConfig { + int32 node_id = 1; + map config = 2; +} + message MessageType { enum Enum { NONE = 0; From c0516255f2fa4f2edcdd108f336597d8dc090485 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 29 Oct 2019 14:40:37 -0700 Subject: [PATCH 136/462] added emane model configs and mobility configs to grpc.StartSession --- daemon/core/api/grpc/client.py | 21 ++++++---- daemon/core/api/grpc/server.py | 22 +++++++--- daemon/proto/core/api/grpc/core.proto | 22 +++++++--- daemon/tests/test_grpc.py | 60 ++++++++++++++++++++++++++- 4 files changed, 104 insertions(+), 21 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 5c5ee72c..b904d9f4 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -156,7 +156,9 @@ class CoreGrpcClient: location=None, hooks=None, emane_config=None, + emane_model_configs=None, wlan_configs=None, + mobility_configs=None, ): """ Start a session. @@ -167,8 +169,11 @@ class CoreGrpcClient: :param core_pb2.SessionLocation location: location to set :param list[core_pb2.Hook] hooks: session hooks to set :param dict emane_config: emane configuration to set + :param list emane_model_configs: emane model configurations to set :param list wlan_configs: wlan configurations to set - :return: + :param list mobility_configs: mobility configurations to set + :return: start session response + :rtype: core_pb2.StartSessionResponse """ request = core_pb2.StartSessionRequest( session_id=session_id, @@ -177,7 +182,9 @@ class CoreGrpcClient: location=location, hooks=hooks, emane_config=emane_config, + emane_model_configs=emane_model_configs, wlan_configs=wlan_configs, + mobility_configs=mobility_configs, ) return self.stub.StartSession(request) @@ -621,8 +628,9 @@ class CoreGrpcClient: :rtype: core_pb2.SetMobilityConfigResponse :raises grpc.RpcError: when session or node doesn't exist """ + mobility_config = core_pb2.MobilityConfig(node_id=node_id, config=config) request = core_pb2.SetMobilityConfigRequest( - session_id=session_id, node_id=node_id, config=config + session_id=session_id, mobility_config=mobility_config ) return self.stub.SetMobilityConfig(request) @@ -881,12 +889,11 @@ class CoreGrpcClient: :rtype: core_pb2.SetEmaneModelConfigResponse :raises grpc.RpcError: when session doesn't exist """ + model_config = core_pb2.EmaneModelConfig( + node_id=node_id, model=model, config=config, interface_id=interface_id + ) request = core_pb2.SetEmaneModelConfigRequest( - session_id=session_id, - node_id=node_id, - model=model, - config=config, - interface_id=interface_id, + session_id=session_id, emane_model_config=model_config ) return self.stub.SetEmaneModelConfig(request) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index ac7aba1e..3a3fd2f2 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -81,7 +81,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param int session_id: session id :param grpc.ServicerContext context: - :return: session object that satisfies. If session not found then raise an exception. + :return: session object that satisfies, if session not found then raise an + exception :rtype: core.emulator.session.Session """ session = self.coreemu.sessions.get(session_id) @@ -136,14 +137,21 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): # emane configs config = session.emane.get_configs() config.update(request.emane_config) + for config in request.emane_model_configs: + _id = get_emane_model_id(config.node_id, config.interface_id) + session.emane.set_model_config(_id, config.model, config.config) # wlan configs - for wlan_config in request.wlan_configs: + for config in request.wlan_configs: session.mobility.set_model_config( - wlan_config.node_id, BasicRangeModel.name, wlan_config.config + config.node_id, BasicRangeModel.name, config.config ) # mobility configs + for config in request.mobility_configs: + session.mobility.set_model_config( + config.node_id, Ns2ScriptedMobility.name, config.config + ) # create links grpcutils.create_links(session, request.links) @@ -984,8 +992,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("set mobility config: %s", request) session = self.get_session(request.session_id, context) + mobility_config = request.mobility_config session.mobility.set_model_config( - request.node_id, Ns2ScriptedMobility.name, request.config + mobility_config.node_id, Ns2ScriptedMobility.name, mobility_config.config ) return core_pb2.SetMobilityConfigResponse(result=True) @@ -1313,8 +1322,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("set emane model config: %s", request) session = self.get_session(request.session_id, context) - _id = get_emane_model_id(request.node_id, request.interface_id) - session.emane.set_model_config(_id, request.model, request.config) + model_config = request.emane_model_config + _id = get_emane_model_id(model_config.node_id, model_config.interface_id) + session.emane.set_model_config(_id, model_config.model, model_config.config) return core_pb2.SetEmaneModelConfigResponse(result=True) def GetEmaneModelConfigs(self, request, context): diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index fa65442f..03843efa 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -138,6 +138,8 @@ message StartSessionRequest { SessionLocation location = 5; map emane_config = 6; repeated WlanConfig wlan_configs = 7; + repeated EmaneModelConfig emane_model_configs = 8; + repeated MobilityConfig mobility_configs = 9; } message StartSessionResponse { @@ -466,8 +468,7 @@ message GetMobilityConfigResponse { message SetMobilityConfigRequest { int32 session_id = 1; - int32 node_id = 2; - map config = 3; + MobilityConfig mobility_config = 2; } message SetMobilityConfigResponse { @@ -622,10 +623,7 @@ message GetEmaneModelConfigResponse { message SetEmaneModelConfigRequest { int32 session_id = 1; - int32 node_id = 2; - int32 interface_id = 3; - string model = 4; - map config = 5; + EmaneModelConfig emane_model_config = 2; } message SetEmaneModelConfigResponse { @@ -687,6 +685,18 @@ message WlanConfig { map config = 2; } +message MobilityConfig { + int32 node_id = 1; + map config = 2; +} + +message EmaneModelConfig { + int32 node_id = 1; + int32 interface_id = 2; + string model = 3; + map config = 4; +} + message MessageType { enum Enum { NONE = 0; diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index a86e2b89..63125eb5 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -32,7 +32,11 @@ class TestGrpc: node_one = core_pb2.Node(id=1, position=position, model="PC") position = core_pb2.Position(x=100, y=100) node_two = core_pb2.Node(id=2, position=position, model="PC") - nodes.extend([node_one, node_two]) + position = core_pb2.Position(x=200, y=200) + wlan_node = core_pb2.Node( + id=3, type=NodeTypes.WIRELESS_LAN.value, position=position + ) + nodes.extend([node_one, node_two, wlan_node]) links = [] interface_helper = InterfaceHelper(ip4_prefix="10.83.0.0/16") interface_one = interface_helper.create_interface(node_one.id, 0) @@ -66,15 +70,54 @@ class TestGrpc: alt=location_alt, scale=location_scale, ) + emane_config_key = "platform_id_start" + emane_config_value = "2" + emane_config = {emane_config_key: emane_config_value} + model_configs = [] + model_node_id = 20 + model_config_key = "bandwidth" + model_config_value = "500000" + model_config = core_pb2.EmaneModelConfig( + node_id=model_node_id, + interface_id=-1, + model=EmaneIeee80211abgModel.name, + config={model_config_key: model_config_value}, + ) + model_configs.append(model_config) + wlan_configs = [] + wlan_config_key = "range" + wlan_config_value = "333" + wlan_config = core_pb2.WlanConfig( + node_id=wlan_node.id, config={wlan_config_key: wlan_config_value} + ) + wlan_configs.append(wlan_config) + mobility_config_key = "refresh_ms" + mobility_config_value = "60" + mobility_configs = [] + mobility_config = core_pb2.MobilityConfig( + node_id=wlan_node.id, config={mobility_config_key: mobility_config_value} + ) + mobility_configs.append(mobility_config) # when with patch.object(CoreXmlWriter, "write"): with client.context_connect(): - client.start_session(session.id, nodes, links, location, hooks) + client.start_session( + session.id, + nodes, + links, + location, + hooks, + emane_config, + model_configs, + wlan_configs, + mobility_configs, + ) # then assert node_one.id in session.nodes assert node_two.id in session.nodes + assert wlan_node.id in session.nodes assert session.nodes[node_one.id].netif(0) is not None assert session.nodes[node_two.id].netif(0) is not None hook_file, hook_data = session._hooks[core_pb2.SessionState.RUNTIME][0] @@ -83,6 +126,19 @@ class TestGrpc: assert session.location.refxyz == (location_x, location_y, location_z) assert session.location.refgeo == (location_lat, location_lon, location_alt) assert session.location.refscale == location_scale + assert session.emane.get_config(emane_config_key) == emane_config_value + set_wlan_config = session.mobility.get_model_config( + wlan_node.id, BasicRangeModel.name + ) + assert set_wlan_config[wlan_config_key] == wlan_config_value + set_mobility_config = session.mobility.get_model_config( + wlan_node.id, Ns2ScriptedMobility.name + ) + assert set_mobility_config[mobility_config_key] == mobility_config_value + set_model_config = session.emane.get_model_config( + model_node_id, EmaneIeee80211abgModel.name + ) + assert set_model_config[model_config_key] == model_config_value @pytest.mark.parametrize("session_id", [None, 6013]) def test_create_session(self, grpc_server, session_id): From fe95f246d470830b7a7537d71482b4b5c5867a37 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 30 Oct 2019 12:01:01 -0700 Subject: [PATCH 137/462] added grpc get/set session metadata --- daemon/core/api/grpc/client.py | 27 ++++++++++++++++++++++ daemon/core/api/grpc/server.py | 29 ++++++++++++++++++++++++ daemon/proto/core/api/grpc/core.proto | 21 ++++++++++++++++++ daemon/tests/test_grpc.py | 32 +++++++++++++++++++++++++++ 4 files changed, 109 insertions(+) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index b904d9f4..01a7b1f6 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -270,6 +270,33 @@ class CoreGrpcClient: ) return self.stub.SetSessionOptions(request) + def get_session_metadata(self, session_id): + """ + Retrieve session metadata as a dict with id mapping. + + :param int session_id: id of session + :return: response with metadata dict + :rtype: core_pb2.GetSessionMetadataResponse + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.GetSessionMetadataRequest(session_id=session_id) + return self.stub.GetSessionMetadata(request) + + def set_session_metadata(self, session_id, config): + """ + Set metadata for a session. + + :param int session_id: id of session + :param dict[str, str] config: configuration values to set + :return: response with result of success or failure + :rtype: core_pb2.SetSessionMetadataResponse + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.SetSessionMetadataRequest( + session_id=session_id, config=config + ) + return self.stub.SetSessionMetadata(request) + def get_session_location(self, session_id): """ Get session location. diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 3a3fd2f2..33d1ebfd 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -331,6 +331,35 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): config.update(request.config) return core_pb2.SetSessionOptionsResponse(result=True) + def GetSessionMetadata(self, request, context): + """ + Retrieve session metadata. + + :param core.api.grpc.core_pb2.GetSessionMetadata request: get session metadata + request + :param grpc.ServicerContext context: context object + :return: get session metadata response + :rtype: core.api.grpc.core_pb2.GetSessionMetadata + """ + logging.debug("get session metadata: %s", request) + session = self.get_session(request.session_id, context) + config = session.metadata.get_configs() + return core_pb2.GetSessionMetadataResponse(config=config) + + def SetSessionMetadata(self, request, context): + """ + Update a session's metadata. + + :param core.api.grpc.core_pb2.SetSessionMetadata request: set metadata request + :param grpc.ServicerContext context: context object + :return: set metadata response + :rtype: core.api.grpc.core_pb2.SetSessionMetadataResponse + """ + logging.debug("set session metadata: %s", request) + session = self.get_session(request.session_id, context) + session.metadata.set_configs(request.config) + return core_pb2.SetSessionMetadataResponse(result=True) + def GetSession(self, request, context): """ Retrieve requested session diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 03843efa..19a1ff97 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -23,6 +23,10 @@ service CoreApi { } rpc SetSessionOptions (SetSessionOptionsRequest) returns (SetSessionOptionsResponse) { } + rpc SetSessionMetadata (SetSessionMetadataRequest) returns (SetSessionMetadataResponse) { + } + rpc GetSessionMetadata (GetSessionMetadataRequest) returns (GetSessionMetadataResponse) { + } rpc GetSessionLocation (GetSessionLocationRequest) returns (GetSessionLocationResponse) { } rpc SetSessionLocation (SetSessionLocationRequest) returns (SetSessionLocationResponse) { @@ -203,6 +207,23 @@ message SetSessionOptionsResponse { bool result = 1; } +message SetSessionMetadataRequest { + int32 session_id = 1; + map config = 2; +} + +message SetSessionMetadataResponse { + bool result = 1; +} + +message GetSessionMetadataRequest { + int32 session_id = 1; +} + +message GetSessionMetadataResponse { + map config = 1; +} + message GetSessionLocationRequest { int32 session_id = 1; } diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 63125eb5..c091691e 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -283,6 +283,38 @@ class TestGrpc: config = session.options.get_configs() assert len(config) > 0 + def test_set_session_metadata(self, grpc_server): + # given + client = CoreGrpcClient() + session = grpc_server.coreemu.create_session() + + # then + key = "meta1" + value = "value1" + with client.context_connect(): + response = client.set_session_metadata(session.id, {key: value}) + + # then + assert response.result is True + assert session.metadata.get_config(key) == value + config = session.metadata.get_configs() + assert len(config) > 0 + + def test_get_session_metadata(self, grpc_server): + # given + client = CoreGrpcClient() + session = grpc_server.coreemu.create_session() + key = "meta1" + value = "value1" + session.metadata.set_config(key, value) + + # then + with client.context_connect(): + response = client.get_session_metadata(session.id) + + # then + assert response.config[key] == value + def test_set_session_state(self, grpc_server): # given client = CoreGrpcClient() From 79dfbaa3ec83345db95586b936b157eca0c16562 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 30 Oct 2019 13:27:12 -0700 Subject: [PATCH 138/462] switched session.metadata to just be a standard dict, fixed shutdown service issue during session.data_collect --- daemon/core/api/grpc/server.py | 5 ++--- daemon/core/api/tlv/corehandlers.py | 19 ++++++++----------- daemon/core/emulator/session.py | 9 ++++----- daemon/core/emulator/sessionconfig.py | 11 ----------- daemon/core/xml/corexml.py | 10 +++++----- daemon/tests/test_grpc.py | 6 ++---- daemon/tests/test_gui.py | 2 +- 7 files changed, 22 insertions(+), 40 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 33d1ebfd..e1e1f24d 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -343,8 +343,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get session metadata: %s", request) session = self.get_session(request.session_id, context) - config = session.metadata.get_configs() - return core_pb2.GetSessionMetadataResponse(config=config) + return core_pb2.GetSessionMetadataResponse(config=session.metadata) def SetSessionMetadata(self, request, context): """ @@ -357,7 +356,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("set session metadata: %s", request) session = self.get_session(request.session_id, context) - session.metadata.set_configs(request.config) + session.metadata = dict(request.config) return core_pb2.SetSessionMetadataResponse(result=True) def GetSession(self, request, context): diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index a6eae965..e66d6875 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -430,9 +430,7 @@ class CoreHandler(socketserver.BaseRequestHandler): tlv_data += coreapi.CoreRegisterTlv.pack( self.session.options.config_type, self.session.options.name ) - tlv_data += coreapi.CoreRegisterTlv.pack( - self.session.metadata.config_type, self.session.metadata.name - ) + tlv_data += coreapi.CoreRegisterTlv.pack(RegisterTlvs.UTILITY.value, "metadata") return coreapi.CoreRegMessage.pack(MessageFlags.ADD.value, tlv_data) @@ -1046,7 +1044,7 @@ class CoreHandler(socketserver.BaseRequestHandler): replies = self.handle_config_session(message_type, config_data) elif config_data.object == self.session.location.name: self.handle_config_location(message_type, config_data) - elif config_data.object == self.session.metadata.name: + elif config_data.object == "metadata": replies = self.handle_config_metadata(message_type, config_data) elif config_data.object == "broker": self.handle_config_broker(message_type, config_data) @@ -1132,7 +1130,7 @@ class CoreHandler(socketserver.BaseRequestHandler): replies = [] if message_type == ConfigFlags.REQUEST: node_id = config_data.node - metadata_configs = self.session.metadata.get_configs() + metadata_configs = self.session.metadata if metadata_configs is None: metadata_configs = {} data_values = "|".join( @@ -1142,7 +1140,7 @@ class CoreHandler(socketserver.BaseRequestHandler): config_response = ConfigData( message_type=0, node=node_id, - object=self.session.metadata.name, + object="metadata", type=ConfigFlags.NONE.value, data_types=data_types, data_values=data_values, @@ -1152,7 +1150,7 @@ class CoreHandler(socketserver.BaseRequestHandler): values = ConfigShim.str_to_dict(config_data.data_values) for key in values: value = values[key] - self.session.metadata.set_config(key, value) + self.session.metadata[key] = value return replies def handle_config_broker(self, message_type, config_data): @@ -1951,18 +1949,17 @@ class CoreHandler(socketserver.BaseRequestHandler): self.session.broadcast_config(config_data) # send session metadata - metadata_configs = self.session.metadata.get_configs() + metadata_configs = self.session.metadata if metadata_configs: data_values = "|".join( [f"{x}={metadata_configs[x]}" for x in metadata_configs] ) data_types = tuple( - ConfigDataTypes.STRING.value - for _ in self.session.metadata.get_configs() + ConfigDataTypes.STRING.value for _ in self.session.metadata ) config_data = ConfigData( message_type=0, - object=self.session.metadata.name, + object="metadata", type=ConfigFlags.NONE.value, data_types=data_types, data_values=data_values, diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 768ea524..cba99e8e 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -26,7 +26,7 @@ from core.emulator.emudata import ( link_config, ) from core.emulator.enumerations import EventTypes, ExceptionLevels, LinkTypes, NodeTypes -from core.emulator.sessionconfig import SessionConfig, SessionMetaData +from core.emulator.sessionconfig import SessionConfig from core.errors import CoreError from core.location.corelocation import CoreLocation from core.location.event import EventLoop @@ -129,7 +129,7 @@ class Session: for key in config: value = config[key] self.options.set_config(key, value) - self.metadata = SessionMetaData() + self.metadata = {} # distributed support and logic self.distributed = DistributedController(self) @@ -1513,9 +1513,8 @@ class Session: for node_id in self.nodes: node = self.nodes[node_id] if isinstance(node, CoreNodeBase): - self.services.stop_services(node) - args = (node,) - funcs.append((self.services.stop_services, args, {})) + args = (node,) + funcs.append((self.services.stop_services, args, {})) utils.threadpool(funcs) # shutdown emane diff --git a/daemon/core/emulator/sessionconfig.py b/daemon/core/emulator/sessionconfig.py index 38373696..eb38474b 100644 --- a/daemon/core/emulator/sessionconfig.py +++ b/daemon/core/emulator/sessionconfig.py @@ -87,14 +87,3 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions): if value is not None: value = int(value) return value - - -class SessionMetaData(ConfigurableManager): - """ - Metadata is simply stored in a configs[] dict. Key=value pairs are - passed in from configure messages destined to the "metadata" object. - The data is not otherwise interpreted or processed. - """ - - name = "metadata" - config_type = RegisterTlvs.UTILITY.value diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 96d0feff..dcca6b80 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -313,13 +313,13 @@ class CoreXmlWriter: def write_session_metadata(self): # metadata metadata_elements = etree.Element("session_metadata") - config = self.session.metadata.get_configs() + config = self.session.metadata if not config: return - for _id in config: - value = config[_id] - add_configuration(metadata_elements, _id, value) + for key in config: + value = config[key] + add_configuration(metadata_elements, key, value) if metadata_elements.getchildren(): self.scenario.append(metadata_elements) @@ -574,7 +574,7 @@ class CoreXmlReader: value = data.get("value") configs[name] = value logging.info("reading session metadata: %s", configs) - self.session.metadata.set_configs(configs) + self.session.metadata = configs def read_session_options(self): session_options = self.scenario.find("session_options") diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index c091691e..62ff3a22 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -296,9 +296,7 @@ class TestGrpc: # then assert response.result is True - assert session.metadata.get_config(key) == value - config = session.metadata.get_configs() - assert len(config) > 0 + assert session.metadata[key] == value def test_get_session_metadata(self, grpc_server): # given @@ -306,7 +304,7 @@ class TestGrpc: session = grpc_server.coreemu.create_session() key = "meta1" value = "value1" - session.metadata.set_config(key, value) + session.metadata[key] = value # then with client.context_connect(): diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index b4025a0c..c2a8c9fc 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -741,7 +741,7 @@ class TestGui: coretlv.handle_message(message) - assert coretlv.session.metadata.get_config(test_key) == test_value + assert coretlv.session.metadata[test_key] == test_value def test_config_broker_request(self, coretlv): server = "test" From 46127b44f9c58da991b9618d6dc5b62144cfd21f Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 30 Oct 2019 13:33:22 -0700 Subject: [PATCH 139/462] more work on configurations --- coretk/coretk/canvasaction.py | 38 ++++++ coretk/coretk/graph.py | 88 +++++++++---- coretk/coretk/grpcmanagement.py | 1 - coretk/coretk/imagemodification.py | 91 +++++++++++++ coretk/coretk/nodeconfigtable.py | 69 +++++----- coretk/coretk/serviceconfiguration.py | 10 ++ coretk/coretk/wlanconfiguration.py | 181 ++++++++++++++++++++++++++ 7 files changed, 419 insertions(+), 59 deletions(-) create mode 100644 coretk/coretk/canvasaction.py create mode 100644 coretk/coretk/imagemodification.py create mode 100644 coretk/coretk/serviceconfiguration.py create mode 100644 coretk/coretk/wlanconfiguration.py diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py new file mode 100644 index 00000000..addd2cd8 --- /dev/null +++ b/coretk/coretk/canvasaction.py @@ -0,0 +1,38 @@ +""" +canvas graph action +""" + +# import tkinter as tk + +from core.api.grpc import core_pb2 +from coretk.nodeconfigtable import NodeConfig +from coretk.wlanconfiguration import WlanConfiguration + +NODE_TO_TYPE = { + "router": core_pb2.NodeType.DEFAULT, + "wlan": core_pb2.NodeType.WIRELESS_LAN, +} + + +class CanvasAction: + def __init__(self, master, canvas): + self.master = master + + self.canvas = canvas + self.node_to_show_config = None + + def display_configuration(self, canvas_node): + pb_type = NODE_TO_TYPE[canvas_node.node_type] + self.node_to_show_config = canvas_node + if pb_type == core_pb2.NodeType.DEFAULT: + self.display_node_configuration() + elif pb_type == core_pb2.NodeType.WIRELESS_LAN: + self.display_wlan_configuration() + + def display_node_configuration(self): + NodeConfig(self.canvas, self.node_to_show_config) + self.node_to_show_config = None + + def display_wlan_configuration(self): + WlanConfiguration(self.canvas, self.node_to_show_config) + self.node_to_show_config = None diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 4107401b..dd3bbc1c 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -3,12 +3,14 @@ import logging import tkinter as tk from core.api.grpc import core_pb2 +from coretk.canvasaction import CanvasAction from coretk.graph_helper import GraphHelper, WlanAntennaManager from coretk.grpcmanagement import GrpcManager from coretk.images import Images from coretk.interface import Interface from coretk.linkinfo import LinkInfo -from coretk.nodeconfigtable import NodeConfig + +# from coretk.nodeconfigtable import NodeConfig class GraphMode(enum.Enum): @@ -19,6 +21,12 @@ class GraphMode(enum.Enum): OTHER = 4 +CORE_NODES = ["router"] +CORE_WIRED_NETWORK_NODES = [] +CORE_WIRELESS_NODE = ["wlan"] +CORE_EMANE = ["emane"] + + class CanvasGraph(tk.Canvas): def __init__(self, master=None, grpc=None, cnf=None, **kwargs): if cnf is None: @@ -36,6 +44,7 @@ class CanvasGraph(tk.Canvas): self.grid = None self.meters_per_pixel = 1.5 + self.canvas_action = CanvasAction(master, self) self.setup_menus() self.setup_bindings() self.draw_grid() @@ -44,15 +53,30 @@ class CanvasGraph(tk.Canvas): self.grpc_manager = GrpcManager(grpc) self.helper = GraphHelper(self, grpc) + self.is_node_context_opened = False # self.core_id_to_canvas_id = {} # self.core_map = CoreToCanvasMapping() # self.draw_existing_component() + def test(self): + print("testing the button") + print(self.node_context.winfo_rootx()) + def setup_menus(self): self.node_context = tk.Menu(self.master) - self.node_context.add_command(label="One") - self.node_context.add_command(label="Two") - self.node_context.add_command(label="Three") + self.node_context.add_command( + label="Configure", command=self.canvas_action.display_node_configuration + ) + self.node_context.add_command(label="Select adjacent") + self.node_context.add_command(label="Create link to") + self.node_context.add_command(label="Assign to") + self.node_context.add_command(label="Move to") + self.node_context.add_command(label="Cut") + self.node_context.add_command(label="Copy") + self.node_context.add_command(label="Paste") + self.node_context.add_command(label="Delete") + self.node_context.add_command(label="Hide") + self.node_context.add_command(label="Services") def canvas_reset_and_redraw(self, new_grpc): """ @@ -74,14 +98,11 @@ class CanvasGraph(tk.Canvas): self.edges = {} self.drawing_edge = None - print("graph.py create a new grpc manager") self.grpc_manager = GrpcManager(new_grpc) # new grpc self.core_grpc = new_grpc - print("grpah.py draw existing component") self.draw_existing_component() - print(self.grpc_manager.edges) def setup_bindings(self): """ @@ -218,8 +239,6 @@ class CanvasGraph(tk.Canvas): ].interfaces.append(if2) # lift the nodes so they on top of the links - # for i in core_id_to_canvas_id.values(): - # self.lift(i) for i in self.find_withtag("node"): self.lift(i) @@ -272,16 +291,20 @@ class CanvasGraph(tk.Canvas): :param event: mouse event :return: nothing """ - self.focus_set() - self.selected = self.get_selected(event) - logging.debug(f"click release selected: {self.selected}") - if self.mode == GraphMode.EDGE: - self.handle_edge_release(event) - elif self.mode == GraphMode.NODE: - x, y = self.canvas_xy(event) - self.add_node(x, y, self.draw_node_image, self.draw_node_name) - elif self.mode == GraphMode.PICKNODE: - self.mode = GraphMode.NODE + if self.is_node_context_opened: + self.node_context.unpost() + self.is_node_context_opened = False + else: + self.focus_set() + self.selected = self.get_selected(event) + logging.debug(f"click release selected: {self.selected}") + if self.mode == GraphMode.EDGE: + self.handle_edge_release(event) + elif self.mode == GraphMode.NODE: + x, y = self.canvas_xy(event) + self.add_node(x, y, self.draw_node_image, self.draw_node_name) + elif self.mode == GraphMode.PICKNODE: + self.mode = GraphMode.NODE def handle_edge_release(self, event): edge = self.drawing_edge @@ -367,11 +390,17 @@ class CanvasGraph(tk.Canvas): self.coords(self.drawing_edge.id, x1, y1, x2, y2) def context(self, event): - selected = self.get_selected(event) - nodes = self.find_withtag("node") - if selected in nodes: - logging.debug(f"node context: {selected}") - self.node_context.post(event.x_root, event.y_root) + if not self.is_node_context_opened: + selected = self.get_selected(event) + nodes = self.find_withtag("node") + if selected in nodes: + logging.debug(f"node context: {selected}") + self.node_context.post(event.x_root, event.y_root) + self.canvas_action.node_to_show_config = self.nodes[selected] + self.is_node_context_opened = True + else: + self.node_context.unpost() + self.is_node_context_opened = False def add_node(self, x, y, image, node_name): plot_id = self.find_all()[0] @@ -482,8 +511,15 @@ class CanvasNode: if state == core_pb2.SessionState.RUNTIME: self.canvas.core_grpc.launch_terminal(node_id) else: - print("config table show up") - NodeConfig(self, self.image, self.node_type, self.name) + self.canvas.canvas_action.display_configuration(self) + # if self.node_type in CORE_NODES: + # self.canvas.canvas_action.node_to_show_config = self + # self.canvas.canvas_action.display_node_configuration() + # elif self.node_type in CORE_WIRED_NETWORK_NODES: + # return + # elif self.node_type in CORE_WIRELESS_NODE: + # return + # elif self def update_coords(self): self.x_coord, self.y_coord = self.canvas.coords(self.id) diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index 9638c7c5..4cfc6a16 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -169,7 +169,6 @@ class GrpcManager: # update the next available id core_id = core_node.id - print(core_id) if self.id is None or core_id >= self.id: self.id = core_id + 1 self.preexisting.append(core_id) diff --git a/coretk/coretk/imagemodification.py b/coretk/coretk/imagemodification.py new file mode 100644 index 00000000..646f31b7 --- /dev/null +++ b/coretk/coretk/imagemodification.py @@ -0,0 +1,91 @@ +""" +node image modification +""" + + +import os +import tkinter as tk +from tkinter import filedialog + +from PIL import Image, ImageTk + +PATH = os.path.abspath(os.path.dirname(__file__)) +ICONS_DIR = os.path.join(PATH, "icons") + + +class ImageModification: + def __init__(self, canvas, canvas_node, node_config): + """ + create an instance of ImageModification + :param coretk.graph.CanvasGraph canvas: canvas object + :param coretk.graph.CanvasNode canvas_node: node object + :param coretk.nodeconfigtable.NodeConfig node_config: node configuration object + """ + self.canvas = canvas + self.image = canvas_node.image + self.node_type = canvas_node.node_type + self.name = canvas_node.name + self.canvas_node = canvas_node + self.node_configuration = node_config + self.p_top = node_config.top + + self.top = tk.Toplevel() + self.top.title(self.name + " image") + self.image_modification() + + def open_icon_dir(self, toplevel, entry_text): + filename = filedialog.askopenfilename( + initialdir=ICONS_DIR, + title="Open", + filetypes=( + ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), + ("All Files", "*"), + ), + ) + if len(filename) > 0: + img = Image.open(filename) + tk_img = ImageTk.PhotoImage(img) + lb = toplevel.grid_slaves(1, 0)[0] + lb.configure(image=tk_img) + lb.image = tk_img + entry_text.set(filename) + + def click_apply(self, toplevel, entry_text): + imgfile = entry_text.get() + if imgfile: + img = Image.open(imgfile) + tk_img = ImageTk.PhotoImage(img) + f = self.p_top.grid_slaves(row=0, column=0)[0] + lb = f.grid_slaves(row=0, column=3)[0] + lb.configure(image=tk_img) + lb.image = tk_img + self.image = tk_img + self.node_configuration.image = tk_img + toplevel.destroy() + + def image_modification(self): + f = tk.Frame(self.top) + entry_text = tk.StringVar() + image_file_label = tk.Label(f, text="Image file: ") + image_file_label.grid(row=0, column=0) + image_file_entry = tk.Entry(f, textvariable=entry_text, width=32, bg="white") + image_file_entry.grid(row=0, column=1) + image_file_button = tk.Button( + f, text="...", command=lambda: self.open_icon_dir(self.top, entry_text) + ) + image_file_button.grid(row=0, column=2) + f.grid() + + img = tk.Label(self.top, image=self.image) + img.grid() + + f = tk.Frame(self.top) + apply_button = tk.Button( + f, text="Apply", command=lambda: self.click_apply(self.top, entry_text) + ) + apply_button.grid(row=0, column=0) + apply_to_multiple_button = tk.Button(f, text="Apply to multiple...") + apply_to_multiple_button.grid(row=0, column=1) + cancel_button = tk.Button(f, text="Cancel", command=self.top.destroy) + cancel_button.grid(row=0, column=2) + f.grid() diff --git a/coretk/coretk/nodeconfigtable.py b/coretk/coretk/nodeconfigtable.py index 76550289..6d03f52f 100644 --- a/coretk/coretk/nodeconfigtable.py +++ b/coretk/coretk/nodeconfigtable.py @@ -8,6 +8,8 @@ from tkinter import filedialog from PIL import Image, ImageTk +from coretk.imagemodification import ImageModification + PATH = os.path.abspath(os.path.dirname(__file__)) ICONS_DIR = os.path.join(PATH, "icons") @@ -16,14 +18,15 @@ DEFAULTNODES = ["router", "host", "PC"] class NodeConfig: - def __init__(self, canvas_node, image, node_type, name): - self.image = image - self.node_type = node_type - self.name = name + def __init__(self, canvas, canvas_node): + self.canvas = canvas + self.image = canvas_node.image + self.node_type = canvas_node.node_type + self.name = canvas_node.name self.canvas_node = canvas_node self.top = tk.Toplevel() - self.top.title(node_type + " configuration") + self.top.title(canvas_node.node_type + " configuration") self.namevar = tk.StringVar(self.top, value="default name") self.name_and_image_definition() self.type_and_service_definition() @@ -58,65 +61,67 @@ class NodeConfig: toplevel.destroy() def img_modification(self): - print("image modification") t = tk.Toplevel() t.title(self.name + " image") f = tk.Frame(t) entry_text = tk.StringVar() image_file_label = tk.Label(f, text="Image file: ") - image_file_label.pack(side=tk.LEFT, padx=2, pady=2) - image_file_entry = tk.Entry(f, textvariable=entry_text, width=60) - image_file_entry.pack(side=tk.LEFT, padx=2, pady=2) + image_file_label.grid(row=0, column=0) + image_file_entry = tk.Entry(f, textvariable=entry_text, width=32, bg="white") + image_file_entry.grid(row=0, column=1) image_file_button = tk.Button( f, text="...", command=lambda: self.open_icon_dir(t, entry_text) ) - image_file_button.pack(side=tk.LEFT, padx=2, pady=2) - f.grid(sticky=tk.W + tk.E) + image_file_button.grid(row=0, column=2) + f.grid() img = tk.Label(t, image=self.image) - img.grid(sticky=tk.W + tk.E) + img.grid() f = tk.Frame(t) apply_button = tk.Button( f, text="Apply", command=lambda: self.click_apply(t, entry_text) ) - apply_button.pack(side=tk.LEFT, padx=2, pady=2) + apply_button.grid(row=0, column=0) apply_to_multiple_button = tk.Button(f, text="Apply to multiple...") - apply_to_multiple_button.pack(side=tk.LEFT, padx=2, pady=2) + apply_to_multiple_button.grid(row=0, column=1) cancel_button = tk.Button(f, text="Cancel", command=t.destroy) - cancel_button.pack(side=tk.LEFT, padx=2, pady=2) - f.grid(sticky=tk.E + tk.W) + cancel_button.grid(row=0, column=2) + f.grid() def name_and_image_definition(self): - name_label = tk.Label(self.top, text="Node name: ") - name_label.grid() - name_entry = tk.Entry(self.top, textvariable=self.namevar) - name_entry.grid(row=0, column=1) + f = tk.Frame(self.top, bg="#d9d9d9") + name_label = tk.Label(f, text="Node name: ", bg="#d9d9d9") + name_label.grid(padx=2, pady=2) + name_entry = tk.Entry(f, textvariable=self.namevar) + name_entry.grid(row=0, column=1, padx=2, pady=2) - core_button = tk.Button(self.top, text="None") - core_button.grid(row=0, column=2) + core_button = tk.Button(f, text="None") + core_button.grid(row=0, column=2, padx=2, pady=2) img_button = tk.Button( - self.top, + f, image=self.image, width=40, height=40, - command=self.img_modification, + command=lambda: ImageModification(self.canvas, self.canvas_node, self), + bg="#d9d9d9", ) - img_button.grid(row=0, column=3) + img_button.grid(row=0, column=3, padx=4, pady=4) + f.grid(padx=4, pady=4) def type_and_service_definition(self): f = tk.Frame(self.top) type_label = tk.Label(f, text="Type: ") - type_label.pack(side=tk.LEFT) + type_label.grid(row=0, column=0) type_button = tk.Button(f, text="None") - type_button.pack(side=tk.LEFT) + type_button.grid(row=0, column=1) service_button = tk.Button(f, text="Services...") - service_button.pack(side=tk.LEFT) + service_button.grid(row=0, column=2) - f.grid(row=1, column=1, columnspan=2, sticky=tk.W) + f.grid(padx=2, pady=2) def config_apply(self): """ @@ -140,10 +145,10 @@ class NodeConfig: def select_definition(self): f = tk.Frame(self.top) apply_button = tk.Button(f, text="Apply", command=self.config_apply) - apply_button.pack(side=tk.LEFT) + apply_button.grid(row=0, column=0) cancel_button = tk.Button(f, text="Cancel", command=self.config_cancel) - cancel_button.pack(side=tk.LEFT) - f.grid(row=3, column=1, sticky=tk.W) + cancel_button.grid(row=0, column=1) + f.grid() def network_node_config(self): self.name_and_image_definition() diff --git a/coretk/coretk/serviceconfiguration.py b/coretk/coretk/serviceconfiguration.py new file mode 100644 index 00000000..a539f1fb --- /dev/null +++ b/coretk/coretk/serviceconfiguration.py @@ -0,0 +1,10 @@ +""" +service configuration +""" + +# import tkinter as tk + + +class ServiceConfiguration: + def __init__(self): + return diff --git a/coretk/coretk/wlanconfiguration.py b/coretk/coretk/wlanconfiguration.py new file mode 100644 index 00000000..53e98ea5 --- /dev/null +++ b/coretk/coretk/wlanconfiguration.py @@ -0,0 +1,181 @@ +""" +wlan configuration +""" + +import ast +import tkinter as tk +from functools import partial + + +class WlanConfiguration: + def __init__(self, canvas, canvas_node): + + self.canvas = canvas + self.image = canvas_node.image + self.node_type = canvas_node.node_type + self.name = canvas_node.name + self.canvas_node = canvas_node + + self.top = tk.Toplevel() + self.top.title("wlan configuration") + self.node_name = tk.StringVar() + + self.range_var = tk.DoubleVar() + self.range_var.set(275.0) + self.bandwidth_var = tk.IntVar() + self.bandwidth_var.set(54000000) + + self.delay_var = tk.StringVar() + + self.image_modification() + self.wlan_configuration() + self.subnet() + self.wlan_options() + self.config_option() + + def image_modification(self): + f = tk.Frame(self.top, bg="#d9d9d9") + lbl = tk.Label(f, text="Node name: ", bg="#d9d9d9") + lbl.grid(row=0, column=0, padx=3, pady=3) + e = tk.Entry(f, textvariable=self.node_name, bg="white") + e.grid(row=0, column=1, padx=3, pady=3) + b = tk.Button(f, text="None") + b.grid(row=0, column=2, padx=3, pady=3) + b = tk.Button(f, text="not implemented") + b.grid(row=0, column=3, padx=3, pady=3) + f.grid(padx=2, pady=2, ipadx=2, ipady=2) + + def create_string_var(self, val): + v = tk.StringVar() + v.set(val) + return v + + def scrollbar_command(self, entry_widget, delta, event): + try: + value = int(entry_widget.get()) + except ValueError: + value = ast.literal_eval(entry_widget.get()) + entry_widget.delete(0, tk.END) + if event == "-1": + entry_widget.insert(tk.END, str(round(value + delta, 1))) + elif event == "1": + entry_widget.insert(tk.END, str(round(value - delta, 1))) + + def wlan_configuration(self): + lbl = tk.Label(self.top, text="Wireless") + lbl.grid(sticky=tk.W, padx=3, pady=3) + + f = tk.Frame( + self.top, + highlightbackground="#b3b3b3", + highlightcolor="#b3b3b3", + highlightthickness=0.5, + bd=0, + bg="#d9d9d9", + ) + + lbl = tk.Label( + f, + text="The basic range model calculates on/off connectivity based on pixel distance between nodes.", + bg="#d9d9d9", + ) + lbl.grid(padx=4, pady=4) + + f1 = tk.Frame(f, bg="#d9d9d9") + + lbl = tk.Label(f1, text="Range: ", bg="#d9d9d9") + lbl.grid(row=0, column=0) + + e = tk.Entry(f1, textvariable=self.range_var, width=5, bg="white") + e.grid(row=0, column=1) + + lbl = tk.Label(f1, text="Bandwidth (bps): ", bg="#d9d9d9") + lbl.grid(row=0, column=2) + + f11 = tk.Frame(f1, bg="#d9d9d9") + sb = tk.Scrollbar(f11, orient=tk.VERTICAL) + e = tk.Entry(f11, textvariable=self.bandwidth_var, width=10, bg="white") + sb.config(command=partial(self.scrollbar_command, e, 1000000)) + e.grid() + sb.grid(row=0, column=1) + f11.grid(row=0, column=3) + + # e = tk.Entry(f1, textvariable=self.bandwidth_var, width=10) + # e.grid(row=0, column=4) + f1.grid(sticky=tk.W, padx=4, pady=4) + + f2 = tk.Frame(f, bg="#d9d9d9") + lbl = tk.Label(f2, text="Delay (us): ", bg="#d9d9d9") + lbl.grid(row=0, column=0) + + f21 = tk.Frame(f2, bg="#d9d9d9") + sb = tk.Scrollbar(f21, orient=tk.VERTICAL) + e = tk.Entry(f21, textvariable=self.create_string_var(20000), bg="white") + sb.config(command=partial(self.scrollbar_command, e, 5000)) + e.grid() + sb.grid(row=0, column=1) + f21.grid(row=0, column=1) + + lbl = tk.Label(f2, text="Loss (%): ", bg="#d9d9d9") + lbl.grid(row=0, column=2) + + f22 = tk.Frame(f2, bg="#d9d9d9") + sb = tk.Scrollbar(f22, orient=tk.VERTICAL) + e = tk.Entry(f22, textvariable=self.create_string_var(0), bg="white") + sb.config(command=partial(self.scrollbar_command, e, 0.1)) + e.grid() + sb.grid(row=0, column=1) + f22.grid(row=0, column=3) + + # e = tk.Entry(f2, textvariable=self.create_string_var(0)) + # e.grid(row=0, column=3) + f2.grid(sticky=tk.W, padx=4, pady=4) + + f3 = tk.Frame(f, bg="#d9d9d9") + lbl = tk.Label(f3, text="Jitter (us): ", bg="#d9d9d9") + lbl.grid() + f31 = tk.Frame(f3, bg="#d9d9d9") + sb = tk.Scrollbar(f31, orient=tk.VERTICAL) + e = tk.Entry(f31, textvariable=self.create_string_var(0), bg="white") + sb.config(command=partial(self.scrollbar_command, e, 5000)) + e.grid() + sb.grid(row=0, column=1) + f31.grid(row=0, column=1) + + f3.grid(sticky=tk.W, padx=4, pady=4) + f.grid(padx=3, pady=3) + + def subnet(self): + f = tk.Frame(self.top) + f1 = tk.Frame(f) + lbl = tk.Label(f1, text="IPv4 subnet") + lbl.grid() + e = tk.Entry(f1, width=30, bg="white") + e.grid(row=0, column=1) + f1.grid() + + f2 = tk.Frame(f) + lbl = tk.Label(f2, text="IPv6 subnet") + lbl.grid() + e = tk.Entry(f2, width=30, bg="white") + e.grid(row=0, column=1) + f2.grid() + f.grid(sticky=tk.W, padx=3, pady=3) + + def wlan_options(self): + f = tk.Frame(self.top) + b = tk.Button(f, text="ns-2 mobility script...") + b.pack(side=tk.LEFT, padx=1) + b = tk.Button(f, text="Link to all routers") + b.pack(side=tk.LEFT, padx=1) + b = tk.Button(f, text="Choose WLAN members") + b.pack(side=tk.LEFT, padx=1) + f.grid(sticky=tk.W) + + def config_option(self): + f = tk.Frame(self.top, bg="#d9d9d9") + b = tk.Button(f, text="Apply", bg="#d9d9d9") + b.grid(padx=2, pady=2) + b = tk.Button(f, text="Cancel", bg="#d9d9d9", command=self.top.destroy) + b.grid(row=0, column=1, padx=2, pady=2) + f.grid(padx=4, pady=4) From a33c8046f515cfa256a1fdda05b4356d901e2f90 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 30 Oct 2019 15:44:57 -0700 Subject: [PATCH 140/462] changes to support running mock tests --- .github/workflows/daemon-checks.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/daemon-checks.yml b/.github/workflows/daemon-checks.yml index 023f5165..21dd95dc 100644 --- a/.github/workflows/daemon-checks.yml +++ b/.github/workflows/daemon-checks.yml @@ -17,6 +17,8 @@ jobs: pip install pipenv cd daemon cp setup.py.in setup.py + cp core/constants.py.in core/constants.py + sed -i 's/True/False/g' core/constants.py pipenv install --dev - name: isort run: | @@ -30,3 +32,11 @@ jobs: run: | cd daemon pipenv run flake8 + - name: grpc + run: | + cd daemon/proto + pipenv run python -m grpc_tools.protoc -I . --python_out=.. --grpc_python_out=.. core/api/grpc/core.proto + - name: test + run: | + cd daemon + pipenv run test --mock From ea39f8fc6fb7113d48832c2f871cfe7e1a491d07 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 30 Oct 2019 15:49:08 -0700 Subject: [PATCH 141/462] updated corehandlers.py to no longer use threading.isAlive, which is pending deprecation --- daemon/core/api/tlv/corehandlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index e66d6875..321306a2 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -123,7 +123,7 @@ class CoreHandler(socketserver.BaseRequestHandler): for thread in self.handler_threads: logging.info("waiting for thread: %s", thread.getName()) thread.join(timeout) - if thread.isAlive(): + if thread.is_alive(): logging.warning( "joining %s failed: still alive after %s sec", thread.getName(), From 85c926ff47201e486ea0cddcdb5153d3288ac5c3 Mon Sep 17 00:00:00 2001 From: Jeff Ahrenholz Date: Thu, 31 Oct 2019 13:00:46 -0700 Subject: [PATCH 142/462] create ebtables chains as needed * otherwise every switch gets a chain, causing problems with simultaneous running Python scripts --- daemon/core/nodes/network.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 80a730e2..6d88d453 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -181,7 +181,7 @@ class EbtablesQueue: def ebchange(self, wlan): """ - Flag a change to the given WLAN"s _linked dict, so the ebtables + Flag a change to the given WLAN's _linked dict, so the ebtables chain will be rebuilt at the next interval. :return: nothing @@ -197,8 +197,13 @@ class EbtablesQueue: :return: nothing """ with wlan._linked_lock: - # flush the chain - self.cmds.append(f"-F {wlan.brname}") + if wlan.has_ebtables_chain: + # flush the chain + self.cmds.append(f"-F {wlan.brname}") + else: + wlan.has_ebtables_chain = True + self.cmds.extend([f"-N {wlan.brname} -P {wlan.policy}", + f"-A FORWARD --logical-in {wlan.brname} -j {wlan.brname}"]) # rebuild the chain for netif1, v in wlan._linked.items(): for netif2, linked in v.items(): @@ -297,14 +302,7 @@ class CoreNetwork(CoreNetworkBase): :raises CoreCommandError: when there is a command exception """ self.net_client.create_bridge(self.brname) - - # create a new ebtables chain for this bridge - cmds = [ - f"{EBTABLES_BIN} -N {self.brname} -P {self.policy}", - f"{EBTABLES_BIN} -A FORWARD --logical-in {self.brname} -j {self.brname}", - ] - ebtablescmds(self.host_cmd, cmds) - + self.has_ebtables_chain = False self.up = True def shutdown(self): @@ -320,11 +318,12 @@ class CoreNetwork(CoreNetworkBase): try: self.net_client.delete_bridge(self.brname) - cmds = [ - f"{EBTABLES_BIN} -D FORWARD --logical-in {self.brname} -j {self.brname}", - f"{EBTABLES_BIN} -X {self.brname}", - ] - ebtablescmds(self.host_cmd, cmds) + if self.has_ebtables_chain: + cmds = [ + f"{EBTABLES_BIN} -D FORWARD --logical-in {self.brname} -j {self.brname}", + f"{EBTABLES_BIN} -X {self.brname}", + ] + ebtablescmds(self.host_cmd, cmds) except CoreCommandError: logging.exception("error during shutdown") From 140fe3c7fe405933dbc1a6554f3960adf0290c4f Mon Sep 17 00:00:00 2001 From: Jeff Ahrenholz Date: Thu, 31 Oct 2019 13:11:09 -0700 Subject: [PATCH 143/462] remove sudo from init script, since it is already running as root --- scripts/core-daemon.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/core-daemon.in b/scripts/core-daemon.in index 263d980d..0a988f0f 100644 --- a/scripts/core-daemon.in +++ b/scripts/core-daemon.in @@ -35,7 +35,7 @@ corestart() { echo "$NAME already started" else echo "starting $NAME" - sudo $CMD 2>&1 >> "$LOG" & + $CMD 2>&1 >> "$LOG" & fi echo $! > "$PIDFILE" From 6be1e19d982ca87bee234e15ae159447d61013d5 Mon Sep 17 00:00:00 2001 From: Jeff Ahrenholz Date: Thu, 31 Oct 2019 13:20:28 -0700 Subject: [PATCH 144/462] don't flush IPv6 address if interface is absent --- daemon/core/nodes/netclient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index b201493f..7a9cc2d8 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -126,7 +126,7 @@ class LinuxNetClient: :param str device: device to flush :return: nothing """ - self.run(f"{IP_BIN} -6 address flush dev {device}") + self.run(f"[ -e /sys/class/net/{device} ] && {IP_BIN} -6 address flush dev {device} || echo") def device_mac(self, device, mac): """ From 891e9aef9af1befefc2cdc1f8aaeb72b3b9614a7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 31 Oct 2019 14:06:50 -0700 Subject: [PATCH 145/462] initial add with a common dialog class and leveraging it for a session options dialog --- coretk/coretk/configutils.py | 52 +++++++++++++++++++++++++ coretk/coretk/coremenubar.py | 2 +- coretk/coretk/dialogs/__init__.py | 0 coretk/coretk/dialogs/dialog.py | 23 +++++++++++ coretk/coretk/dialogs/sessionoptions.py | 35 +++++++++++++++++ coretk/coretk/menuaction.py | 10 +++-- 6 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 coretk/coretk/configutils.py create mode 100644 coretk/coretk/dialogs/__init__.py create mode 100644 coretk/coretk/dialogs/dialog.py create mode 100644 coretk/coretk/dialogs/sessionoptions.py diff --git a/coretk/coretk/configutils.py b/coretk/coretk/configutils.py new file mode 100644 index 00000000..97230561 --- /dev/null +++ b/coretk/coretk/configutils.py @@ -0,0 +1,52 @@ +import enum +import logging +import tkinter as tk +from tkinter import ttk + + +class ConfigType(enum.Enum): + STRING = 10 + BOOL = 11 + + +def create_config(master, config, pad_x=2, pad_y=2): + master.columnconfigure(0, weight=1) + master.columnconfigure(1, weight=3) + values = {} + for index, key in enumerate(sorted(config)): + option = config[key] + label = tk.Label(master, text=option.label) + label.grid(row=index, pady=pad_y, padx=pad_x, sticky="ew") + value = tk.StringVar() + config_type = ConfigType(option.type) + if config_type == ConfigType.BOOL: + select = tuple(option.select) + combobox = ttk.Combobox(master, textvariable=value, values=select) + combobox.grid(row=index, column=1, sticky="ew", pady=pad_y) + if option.value == "1": + value.set("On") + else: + value.set("Off") + elif config_type == ConfigType.STRING: + entry = tk.Entry(master, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pad_y) + else: + logging.error("unhandled config option type: %s", config_type) + values[key] = value + return values + + +def parse_config(options, values): + config = {} + for key in options: + option = options[key] + value = values[key] + config_type = ConfigType(option.type) + config_value = value.get() + if config_type == ConfigType.BOOL: + if config_value == "On": + config_value = "1" + else: + config_value = "0" + config[key] = config_value + return config diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index 1129e8b7..1a558684 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -620,7 +620,7 @@ class CoreMenubar(object): underline=0, ) session_menu.add_command( - label="Options...", command=action.session_options, underline=0 + label="Options...", command=self.menu_action.session_options, underline=0 ) self.menubar.add_cascade(label="Session", menu=session_menu, underline=0) diff --git a/coretk/coretk/dialogs/__init__.py b/coretk/coretk/dialogs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py new file mode 100644 index 00000000..9ce421b9 --- /dev/null +++ b/coretk/coretk/dialogs/dialog.py @@ -0,0 +1,23 @@ +import tkinter as tk + +from coretk.images import ImageEnum, Images + + +class Dialog(tk.Toplevel): + def __init__(self, master, title, modal=False): + super().__init__(master, padx=5, pady=5) + image = Images.get(ImageEnum.CORE.value) + self.tk.call("wm", "iconphoto", self._w, image) + self.title(title) + self.protocol("WM_DELETE_WINDOW", self.destroy) + self.withdraw() + self.modal = modal + + def show(self): + self.transient(self.master) + self.focus_force() + if self.modal: + self.grab_set() + self.update() + self.deiconify() + self.wait_window() diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py new file mode 100644 index 00000000..3612dab5 --- /dev/null +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -0,0 +1,35 @@ +import logging +import tkinter as tk + +from coretk import configutils +from coretk.dialogs.dialog import Dialog + +PAD_X = 2 +PAD_Y = 2 + + +class SessionOptionsDialog(Dialog): + def __init__(self, master): + super().__init__(master, "Session Options", modal=True) + self.options = None + self.values = None + self.save_button = tk.Button(self, text="Save", command=self.save) + self.cancel_button = tk.Button(self, text="Cancel", command=self.destroy) + self.draw() + + def draw(self): + session_id = self.master.core_grpc.session_id + response = self.master.core_grpc.core.get_session_options(session_id) + logging.info("session options: %s", response) + self.options = response.config + self.values = configutils.create_config(self, self.options, PAD_X, PAD_Y) + row = len(response.config) + self.save_button.grid(row=row, pady=PAD_Y, padx=PAD_X, sticky="ew") + self.cancel_button.grid(row=row, column=1, pady=PAD_Y, padx=PAD_X, sticky="ew") + + def save(self): + config = configutils.parse_config(self.options, self.values) + session_id = self.master.core_grpc.session_id + response = self.master.core_grpc.core.set_session_options(session_id, config) + logging.info("saved session config: %s", response) + self.destroy() diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 999801c2..22329516 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -7,6 +7,7 @@ import webbrowser from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 +from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.setwallpaper import CanvasWallpaper from coretk.sizeandscale import SizeAndScale @@ -314,10 +315,6 @@ def session_emulation_servers(): logging.debug("Click session emulation servers") -def session_options(): - logging.debug("Click session options") - - def help_about(): logging.debug("Click help About") @@ -433,3 +430,8 @@ class MenuAction: def help_core_documentation(self): webbrowser.open_new("http://coreemu.github.io/core/") + + def session_options(self): + logging.debug("Click session options") + dialog = SessionOptionsDialog(self.application) + dialog.show() From c65c846638041214cc8cb77652b59f909ee8f83e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 31 Oct 2019 15:18:49 -0700 Subject: [PATCH 146/462] updated configutils to generate a scrollable view for configurations --- coretk/coretk/configutils.py | 59 +++++++++++++++++++++---- coretk/coretk/dialogs/sessionoptions.py | 5 +-- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/coretk/coretk/configutils.py b/coretk/coretk/configutils.py index 97230561..fd5263f3 100644 --- a/coretk/coretk/configutils.py +++ b/coretk/coretk/configutils.py @@ -9,34 +9,77 @@ class ConfigType(enum.Enum): BOOL = 11 -def create_config(master, config, pad_x=2, pad_y=2): +def create_config(master, config, padx=2, pady=2): + """ + Creates a scrollable canvas with an embedded window for displaying configuration + options. Will use grid layout to consume row 0 and columns 0-2. + + :param master: master to add scrollable canvas to + :param dict config: config option mapping keys to config options + :param int padx: x padding for widgets + :param int pady: y padding for widgets + :return: widget value mapping + """ + master.rowconfigure(0, weight=1) master.columnconfigure(0, weight=1) - master.columnconfigure(1, weight=3) + master.columnconfigure(1, weight=1) + + canvas = tk.Canvas(master) + canvas.grid(row=0, columnspan=2, sticky="nsew", padx=padx, pady=pady) + canvas.columnconfigure(0, weight=1) + canvas.rowconfigure(0, weight=1) + + scroll_y = tk.Scrollbar(master, orient="vertical", command=canvas.yview) + scroll_y.grid(row=0, column=2, sticky="ns") + + frame = tk.Frame(canvas, padx=padx, pady=pady) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=3) + values = {} for index, key in enumerate(sorted(config)): option = config[key] - label = tk.Label(master, text=option.label) - label.grid(row=index, pady=pad_y, padx=pad_x, sticky="ew") + label = tk.Label(frame, text=option.label) + label.grid(row=index, pady=pady, padx=padx, sticky="ew") value = tk.StringVar() config_type = ConfigType(option.type) if config_type == ConfigType.BOOL: select = tuple(option.select) - combobox = ttk.Combobox(master, textvariable=value, values=select) - combobox.grid(row=index, column=1, sticky="ew", pady=pad_y) + combobox = ttk.Combobox(frame, textvariable=value, values=select) + combobox.grid(row=index, column=1, sticky="ew", pady=pady) if option.value == "1": value.set("On") else: value.set("Off") elif config_type == ConfigType.STRING: - entry = tk.Entry(master, textvariable=value) - entry.grid(row=index, column=1, sticky="ew", pady=pad_y) + entry = tk.Entry(frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pady) else: logging.error("unhandled config option type: %s", config_type) values[key] = value + + frame_id = canvas.create_window(0, 0, anchor="nw", window=frame) + canvas.update_idletasks() + canvas.configure(scrollregion=canvas.bbox("all"), yscrollcommand=scroll_y.set) + + frame.bind( + "", lambda event: canvas.configure(scrollregion=canvas.bbox("all")) + ) + canvas.bind( + "", lambda event: canvas.itemconfig(frame_id, width=event.width) + ) + return values def parse_config(options, values): + """ + Given a set of configurations, parse out values and transform them when needed. + + :param dict options: option key mapping to configuration options + :param dict values: option key mapping to widget values + :return: + """ config = {} for key in options: option = options[key] diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index 3612dab5..65faae81 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -23,9 +23,8 @@ class SessionOptionsDialog(Dialog): logging.info("session options: %s", response) self.options = response.config self.values = configutils.create_config(self, self.options, PAD_X, PAD_Y) - row = len(response.config) - self.save_button.grid(row=row, pady=PAD_Y, padx=PAD_X, sticky="ew") - self.cancel_button.grid(row=row, column=1, pady=PAD_Y, padx=PAD_X, sticky="ew") + self.save_button.grid(row=1, pady=PAD_Y, padx=PAD_X, sticky="ew") + self.cancel_button.grid(row=1, column=1, pady=PAD_Y, padx=PAD_X, sticky="ew") def save(self): config = configutils.parse_config(self.options, self.values) From f0c32304db4d3662b09dac4db70052c30e63d935 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 31 Oct 2019 15:54:38 -0700 Subject: [PATCH 147/462] removed canvas border for configuration options --- coretk/coretk/configutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coretk/coretk/configutils.py b/coretk/coretk/configutils.py index fd5263f3..7c87f9dd 100644 --- a/coretk/coretk/configutils.py +++ b/coretk/coretk/configutils.py @@ -24,7 +24,7 @@ def create_config(master, config, padx=2, pady=2): master.columnconfigure(0, weight=1) master.columnconfigure(1, weight=1) - canvas = tk.Canvas(master) + canvas = tk.Canvas(master, highlightthickness=0) canvas.grid(row=0, columnspan=2, sticky="nsew", padx=padx, pady=pady) canvas.columnconfigure(0, weight=1) canvas.rowconfigure(0, weight=1) From 01d3a3158a5e1d192d622eba70770960d6a7002f Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 31 Oct 2019 23:17:26 -0700 Subject: [PATCH 148/462] updated sessions dialog to use common dialog base, cleaned up code and made widgets expandable --- coretk/coretk/app.py | 2 +- coretk/coretk/coregrpc.py | 24 +--- coretk/coretk/coremenubar.py | 2 +- coretk/coretk/coretoolbar.py | 115 ++++++++------- coretk/coretk/dialogs/dialog.py | 11 +- coretk/coretk/dialogs/sessionoptions.py | 4 +- coretk/coretk/dialogs/sessions.py | 159 ++++++++++++++++++++ coretk/coretk/graph_helper.py | 2 +- coretk/coretk/images.py | 26 ++-- coretk/coretk/menuaction.py | 12 +- coretk/coretk/querysessiondrawing.py | 183 ------------------------ 11 files changed, 250 insertions(+), 290 deletions(-) create mode 100644 coretk/coretk/dialogs/sessions.py delete mode 100644 coretk/coretk/querysessiondrawing.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 644f1eb2..f253fd01 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -40,7 +40,7 @@ class Application(tk.Frame): def setup_app(self): self.master.title("CORE") self.master.geometry("1000x800") - image = Images.get(ImageEnum.CORE.value) + image = Images.get(ImageEnum.CORE) self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 9b51116a..13fe28b3 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -6,8 +6,8 @@ import os from collections import OrderedDict from core.api.grpc import client, core_pb2 +from coretk.dialogs.sessions import SessionsDialog from coretk.linkinfo import Throughput -from coretk.querysessiondrawing import SessionTable from coretk.wirelessconnection import WirelessConnection @@ -18,12 +18,9 @@ class CoreGrpc: """ self.core = client.CoreGrpcClient() self.session_id = sid - self.node_ids = [] - + self.app = app self.master = app.master - - # self.set_up() self.interface_helper = None self.throughput_draw = Throughput(app.canvas, self) self.wireless_draw = WirelessConnection(app.canvas, self) @@ -62,16 +59,6 @@ class CoreGrpc: self.core.events(self.session_id, self.log_event) # self.core.throughputs(self.log_throughput) - def query_existing_sessions(self, sessions): - """ - Query for existing sessions and prompt to join one - - :param repeated core_pb2.SessionSummary sessions: summaries of all the existing sessions - - :return: nothing - """ - SessionTable(self, self.master) - def delete_session(self, custom_sid=None): if custom_sid is None: sid = self.session_id @@ -100,18 +87,15 @@ class CoreGrpc: :return: existing sessions """ self.core.connect() - response = self.core.get_sessions() - # logging.info("coregrpc.py: all sessions: %s", response) # if there are no sessions, create a new session, else join a session sessions = response.sessions - if len(sessions) == 0: self.create_new_session() else: - - self.query_existing_sessions(sessions) + dialog = SessionsDialog(self.app, self.app) + dialog.show() def get_session_state(self): response = self.core.get_session(self.session_id) diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index 1a558684..aa00a927 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -594,7 +594,7 @@ class CoreMenubar(object): session_menu.add_command( label="Change sessions...", - command=action.session_change_sessions, + command=self.menu_action.session_change_sessions, underline=0, ) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index d46a9791..83831a8a 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -200,55 +200,55 @@ class CoreToolbar(object): def pick_router(self, main_button): logging.debug("Pick router option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.ROUTER.value)) + main_button.configure(image=Images.get(ImageEnum.ROUTER)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.ROUTER.value) + self.canvas.draw_node_image = Images.get(ImageEnum.ROUTER) self.canvas.draw_node_name = "router" def pick_host(self, main_button): logging.debug("Pick host option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.HOST.value)) + main_button.configure(image=Images.get(ImageEnum.HOST)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.HOST.value) + self.canvas.draw_node_image = Images.get(ImageEnum.HOST) self.canvas.draw_node_name = "host" def pick_pc(self, main_button): logging.debug("Pick PC option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.PC.value)) + main_button.configure(image=Images.get(ImageEnum.PC)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.PC.value) + self.canvas.draw_node_image = Images.get(ImageEnum.PC) self.canvas.draw_node_name = "PC" def pick_mdr(self, main_button): logging.debug("Pick MDR option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.MDR.value)) + main_button.configure(image=Images.get(ImageEnum.MDR)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.MDR.value) + self.canvas.draw_node_image = Images.get(ImageEnum.MDR) self.canvas.draw_node_name = "mdr" def pick_prouter(self, main_button): logging.debug("Pick prouter option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.PROUTER.value)) + main_button.configure(image=Images.get(ImageEnum.PROUTER)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.PROUTER.value) + self.canvas.draw_node_image = Images.get(ImageEnum.PROUTER) self.canvas.draw_node_name = "prouter" def pick_ovs(self, main_button): logging.debug("Pick OVS option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.OVS.value)) + main_button.configure(image=Images.get(ImageEnum.OVS)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.OVS.value) + self.canvas.draw_node_image = Images.get(ImageEnum.OVS) self.canvas.draw_node_name = "OVS" # TODO what graph node is this def pick_editnode(self, main_button): self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.EDITNODE.value)) + main_button.configure(image=Images.get(ImageEnum.EDITNODE)) logging.debug("Pick editnode option") def draw_network_layer_options(self, network_layer_button): @@ -262,13 +262,13 @@ class CoreToolbar(object): self.destroy_previous_frame() option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ - Images.get(ImageEnum.ROUTER.value), - Images.get(ImageEnum.HOST.value), - Images.get(ImageEnum.PC.value), - Images.get(ImageEnum.MDR.value), - Images.get(ImageEnum.PROUTER.value), - Images.get(ImageEnum.OVS.value), - Images.get(ImageEnum.EDITNODE.value), + Images.get(ImageEnum.ROUTER), + Images.get(ImageEnum.HOST), + Images.get(ImageEnum.PC), + Images.get(ImageEnum.MDR), + Images.get(ImageEnum.PROUTER), + Images.get(ImageEnum.OVS), + Images.get(ImageEnum.EDITNODE), ] func_list = [ self.pick_router, @@ -312,7 +312,7 @@ class CoreToolbar(object): :return: nothing """ - router_image = Images.get(ImageEnum.ROUTER.value) + router_image = Images.get(ImageEnum.ROUTER) network_layer_button = tk.Radiobutton( self.edit_frame, indicatoron=False, @@ -329,41 +329,41 @@ class CoreToolbar(object): def pick_hub(self, main_button): logging.debug("Pick link-layer node HUB") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.HUB.value)) + main_button.configure(image=Images.get(ImageEnum.HUB)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.HUB.value) + self.canvas.draw_node_image = Images.get(ImageEnum.HUB) self.canvas.draw_node_name = "hub" def pick_switch(self, main_button): logging.debug("Pick link-layer node SWITCH") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.SWITCH.value)) + main_button.configure(image=Images.get(ImageEnum.SWITCH)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.SWITCH.value) + self.canvas.draw_node_image = Images.get(ImageEnum.SWITCH) self.canvas.draw_node_name = "switch" def pick_wlan(self, main_button): logging.debug("Pick link-layer node WLAN") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.WLAN.value)) + main_button.configure(image=Images.get(ImageEnum.WLAN)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.WLAN.value) + self.canvas.draw_node_image = Images.get(ImageEnum.WLAN) self.canvas.draw_node_name = "wlan" def pick_rj45(self, main_button): logging.debug("Pick link-layer node RJ45") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.RJ45.value)) + main_button.configure(image=Images.get(ImageEnum.RJ45)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.RJ45.value) + self.canvas.draw_node_image = Images.get(ImageEnum.RJ45) self.canvas.draw_node_name = "rj45" def pick_tunnel(self, main_button): logging.debug("Pick link-layer node TUNNEL") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.TUNNEL.value)) + main_button.configure(image=Images.get(ImageEnum.TUNNEL)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.TUNNEL.value) + self.canvas.draw_node_image = Images.get(ImageEnum.TUNNEL) self.canvas.draw_node_name = "tunnel" def draw_link_layer_options(self, link_layer_button): @@ -377,11 +377,11 @@ class CoreToolbar(object): self.destroy_previous_frame() option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ - Images.get(ImageEnum.HUB.value), - Images.get(ImageEnum.SWITCH.value), - Images.get(ImageEnum.WLAN.value), - Images.get(ImageEnum.RJ45.value), - Images.get(ImageEnum.TUNNEL.value), + Images.get(ImageEnum.HUB), + Images.get(ImageEnum.SWITCH), + Images.get(ImageEnum.WLAN), + Images.get(ImageEnum.RJ45), + Images.get(ImageEnum.TUNNEL), ] func_list = [ self.pick_hub, @@ -421,7 +421,7 @@ class CoreToolbar(object): :return: nothing """ - hub_image = Images.get(ImageEnum.HUB.value) + hub_image = Images.get(ImageEnum.HUB) link_layer_button = tk.Radiobutton( self.edit_frame, indicatoron=False, @@ -437,22 +437,22 @@ class CoreToolbar(object): def pick_marker(self, main_button): self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.MARKER.value)) + main_button.configure(image=Images.get(ImageEnum.MARKER)) logging.debug("Pick MARKER") def pick_oval(self, main_button): self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.OVAL.value)) + main_button.configure(image=Images.get(ImageEnum.OVAL)) logging.debug("Pick OVAL") def pick_rectangle(self, main_button): self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.RECTANGLE.value)) + main_button.configure(image=Images.get(ImageEnum.RECTANGLE)) logging.debug("Pick RECTANGLE") def pick_text(self, main_button): self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.TEXT.value)) + main_button.configure(image=Images.get(ImageEnum.TEXT)) logging.debug("Pick TEXT") def draw_marker_options(self, main_button): @@ -466,10 +466,10 @@ class CoreToolbar(object): self.destroy_previous_frame() option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ - Images.get(ImageEnum.MARKER.value), - Images.get(ImageEnum.OVAL.value), - Images.get(ImageEnum.RECTANGLE.value), - Images.get(ImageEnum.TEXT.value), + Images.get(ImageEnum.MARKER), + Images.get(ImageEnum.OVAL), + Images.get(ImageEnum.RECTANGLE), + Images.get(ImageEnum.TEXT), ] func_list = [ self.pick_marker, @@ -498,7 +498,7 @@ class CoreToolbar(object): :return: nothing """ - marker_image = Images.get(ImageEnum.MARKER.value) + marker_image = Images.get(ImageEnum.MARKER) marker_main_button = tk.Radiobutton( self.edit_frame, indicatoron=False, @@ -520,13 +520,13 @@ class CoreToolbar(object): """ self.create_regular_button( self.edit_frame, - Images.get(ImageEnum.START.value), + Images.get(ImageEnum.START), self.click_start_session_tool, "start the session", ) self.create_radio_button( self.edit_frame, - Images.get(ImageEnum.SELECT.value), + Images.get(ImageEnum.SELECT), self.click_selection_tool, self.radio_value, 1, @@ -534,7 +534,7 @@ class CoreToolbar(object): ) self.create_radio_button( self.edit_frame, - Images.get(ImageEnum.LINK.value), + Images.get(ImageEnum.LINK), self.click_link_tool, self.radio_value, 2, @@ -548,7 +548,7 @@ class CoreToolbar(object): def create_observe_button(self): menu_button = tk.Menubutton( self.edit_frame, - image=Images.get(ImageEnum.OBSERVE.value), + image=Images.get(ImageEnum.OBSERVE), width=self.width, height=self.height, direction=tk.RIGHT, @@ -605,13 +605,13 @@ class CoreToolbar(object): def create_runtime_toolbar(self): self.create_regular_button( self.edit_frame, - Images.get(ImageEnum.STOP.value), + Images.get(ImageEnum.STOP), self.click_stop_button, "stop the session", ) self.create_radio_button( self.edit_frame, - Images.get(ImageEnum.SELECT.value), + Images.get(ImageEnum.SELECT), self.click_selection_tool, self.exec_radio_value, 1, @@ -620,7 +620,7 @@ class CoreToolbar(object): self.create_observe_button() self.create_radio_button( self.edit_frame, - Images.get(ImageEnum.PLOT.value), + Images.get(ImageEnum.PLOT), self.click_plot_button, self.exec_radio_value, 2, @@ -628,7 +628,7 @@ class CoreToolbar(object): ) self.create_radio_button( self.edit_frame, - Images.get(ImageEnum.MARKER.value), + Images.get(ImageEnum.MARKER), self.click_marker_button, self.exec_radio_value, 3, @@ -636,16 +636,13 @@ class CoreToolbar(object): ) self.create_radio_button( self.edit_frame, - Images.get(ImageEnum.TWONODE.value), + Images.get(ImageEnum.TWONODE), self.click_two_node_button, self.exec_radio_value, 4, "run command from one node to another", ) self.create_regular_button( - self.edit_frame, - Images.get(ImageEnum.RUN.value), - self.click_run_button, - "run", + self.edit_frame, Images.get(ImageEnum.RUN), self.click_run_button, "run" ) self.exec_radio_value.set(1) diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index 9ce421b9..b218d8c7 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -4,14 +4,15 @@ from coretk.images import ImageEnum, Images class Dialog(tk.Toplevel): - def __init__(self, master, title, modal=False): + def __init__(self, master, app, title, modal=False): super().__init__(master, padx=5, pady=5) - image = Images.get(ImageEnum.CORE.value) - self.tk.call("wm", "iconphoto", self._w, image) + self.withdraw() + self.app = app + self.modal = modal self.title(title) self.protocol("WM_DELETE_WINDOW", self.destroy) - self.withdraw() - self.modal = modal + image = Images.get(ImageEnum.CORE) + self.tk.call("wm", "iconphoto", self._w, image) def show(self): self.transient(self.master) diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index 65faae81..a34c6658 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -9,8 +9,8 @@ PAD_Y = 2 class SessionOptionsDialog(Dialog): - def __init__(self, master): - super().__init__(master, "Session Options", modal=True) + def __init__(self, master, app): + super().__init__(master, app, "Session Options", modal=True) self.options = None self.values = None self.save_button = tk.Button(self, text="Save", command=self.save) diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py new file mode 100644 index 00000000..10165e2e --- /dev/null +++ b/coretk/coretk/dialogs/sessions.py @@ -0,0 +1,159 @@ +import logging +import tkinter as tk +from tkinter.ttk import Scrollbar, Treeview + +from core.api.grpc import core_pb2 +from coretk.dialogs.dialog import Dialog +from coretk.images import ImageEnum, Images + + +class SessionsDialog(Dialog): + def __init__(self, master, app): + """ + create session table instance + + :param coretk.coregrpc.CoreGrpc grpc: coregrpc + :param root.master master: + """ + super().__init__(master, app, "Sessions", modal=True) + self.selected = False + self.selected_id = None + self.tree = None + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.draw_description() + self.draw_tree() + self.draw_buttons() + + def draw_description(self): + """ + write a short description + :return: nothing + """ + label = tk.Label( + self, + text="Below is a list of active CORE sessions. Double-click to \n" + "connect to an existing session. Usually, only sessions in \n" + "the RUNTIME state persist in the daemon, except for the \n" + "one you might be concurrently editting.", + ) + label.grid(row=0, sticky="ew", pady=5) + + def draw_tree(self): + self.tree = Treeview(self, columns=("id", "state", "nodes"), show="headings") + self.tree.grid(row=1, sticky="nsew") + self.tree.column("id", stretch=tk.YES) + self.tree.heading("id", text="ID") + self.tree.column("state", stretch=tk.YES) + self.tree.heading("state", text="State") + self.tree.column("nodes", stretch=tk.YES) + self.tree.heading("nodes", text="Node Count") + + response = self.app.core_grpc.core.get_sessions() + logging.info("sessions: %s", response) + for index, session in enumerate(response.sessions): + state_name = core_pb2.SessionState.Enum.Name(session.state) + self.tree.insert( + "", + tk.END, + text=str(session.id), + values=(session.id, state_name, session.nodes), + ) + self.tree.bind("", self.on_selected) + self.tree.bind("<>", self.click_select) + + yscrollbar = Scrollbar(self, orient="vertical", command=self.tree.yview) + yscrollbar.grid(row=1, column=1, sticky="ns") + self.tree.configure(yscrollcommand=yscrollbar.set) + + xscrollbar = Scrollbar(self, orient="horizontal", command=self.tree.xview) + xscrollbar.grid(row=2, sticky="ew", pady=5) + self.tree.configure(xscrollcommand=xscrollbar.set) + + def draw_buttons(self): + frame = tk.Frame(self) + for i in range(4): + frame.columnconfigure(i, weight=1) + frame.grid(row=3, sticky="ew") + b = tk.Button( + frame, + image=Images.get(ImageEnum.DOCUMENTNEW), + text="New", + compound=tk.LEFT, + command=self.click_new, + ) + b.grid(row=0, padx=2, sticky="ew") + b = tk.Button( + frame, + image=Images.get(ImageEnum.FILEOPEN), + text="Connect", + compound=tk.LEFT, + command=self.click_connect, + ) + b.grid(row=0, column=1, padx=2, sticky="ew") + b = tk.Button( + frame, + image=Images.get(ImageEnum.EDITDELETE), + text="Shutdown", + compound=tk.LEFT, + command=self.click_shutdown, + ) + b.grid(row=0, column=2, padx=2, sticky="ew") + b = tk.Button(frame, text="Cancel", command=self.click_new) + b.grid(row=0, column=3, padx=2, sticky="ew") + + def click_new(self): + self.app.core_grpc.create_new_session() + self.destroy() + + def click_select(self, event): + item = self.tree.selection() + session_id = int(self.tree.item(item, "text")) + self.selected = True + self.selected_id = session_id + + def click_connect(self): + """ + if no session is selected yet, create a new one else join that session + + :return: nothing + """ + if self.selected and self.selected_id is not None: + self.join_session(self.selected_id) + elif not self.selected and self.selected_id is None: + self.click_new() + else: + logging.error("sessions invalid state") + + def click_shutdown(self): + """ + if no session is currently selected create a new session else shut the selected + session down. + + :return: nothing + """ + if self.selected and self.selected_id is not None: + self.shutdown_session(self.selected_id) + elif not self.selected and self.selected_id is None: + self.click_new() + else: + logging.error("querysessiondrawing.py invalid state") + + def join_session(self, session_id): + response = self.app.core_grpc.core.get_session(session_id) + self.app.core_grpc.session_id = session_id + self.app.core_grpc.core.events(session_id, self.app.core_grpc.log_event) + logging.info("entering session_id %s.... Result: %s", session_id, response) + self.destroy() + + def on_selected(self, event): + item = self.tree.selection() + sid = int(self.tree.item(item, "text")) + self.join_session(sid) + + def shutdown_session(self, sid): + self.app.core_grpc.terminate_session(sid) + self.click_new() + self.destroy() diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py index 64f38acf..ba58a435 100644 --- a/coretk/coretk/graph_helper.py +++ b/coretk/coretk/graph_helper.py @@ -94,7 +94,7 @@ class WlanAntennaManager: x - 16 + self.offset, y - 16, anchor=tk.CENTER, - image=Images.get(ImageEnum.ANTENNA.value), + image=Images.get(ImageEnum.ANTENNA), tags="antenna", ) ) diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index f865c191..6a817b6f 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -21,8 +21,8 @@ class Images: cls.images[name] = tk_image @classmethod - def get(cls, name): - return cls.images[name] + def get(cls, image): + return cls.images[image.value] @classmethod def convert_type_and_model_to_image(cls, node_type, node_model): @@ -35,28 +35,28 @@ class Images: :return: the matching image and its name """ if node_type == core_pb2.NodeType.SWITCH: - return Images.get(ImageEnum.SWITCH.value), "switch" + return Images.get(ImageEnum.SWITCH), "switch" if node_type == core_pb2.NodeType.HUB: - return Images.get(ImageEnum.HUB.value), "hub" + return Images.get(ImageEnum.HUB), "hub" if node_type == core_pb2.NodeType.WIRELESS_LAN: - return Images.get(ImageEnum.WLAN.value), "wlan" + return Images.get(ImageEnum.WLAN), "wlan" if node_type == core_pb2.NodeType.RJ45: - return Images.get(ImageEnum.RJ45.value), "rj45" + return Images.get(ImageEnum.RJ45), "rj45" if node_type == core_pb2.NodeType.TUNNEL: - return Images.get(ImageEnum.TUNNEL.value), "tunnel" + return Images.get(ImageEnum.TUNNEL), "tunnel" if node_type == core_pb2.NodeType.DEFAULT: if node_model == "router": - return Images.get(ImageEnum.ROUTER.value), "router" + return Images.get(ImageEnum.ROUTER), "router" if node_model == "host": - return Images.get((ImageEnum.HOST.value)), "host" + return Images.get(ImageEnum.HOST), "host" if node_model == "PC": - return Images.get(ImageEnum.PC.value), "PC" + return Images.get(ImageEnum.PC), "PC" if node_model == "mdr": - return Images.get(ImageEnum.MDR.value), "mdr" + return Images.get(ImageEnum.MDR), "mdr" if node_model == "prouter": - return Images.get(ImageEnum.PROUTER.value), "prouter" + return Images.get(ImageEnum.PROUTER), "prouter" if node_model == "OVS": - return Images.get(ImageEnum.OVS.value), "ovs" + return Images.get(ImageEnum.OVS), "ovs" else: logging.debug("INVALID INPUT OR NOT CONSIDERED YET") diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 22329516..97ba9029 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -8,6 +8,7 @@ from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 from coretk.dialogs.sessionoptions import SessionOptionsDialog +from coretk.dialogs.sessions import SessionsDialog from coretk.setwallpaper import CanvasWallpaper from coretk.sizeandscale import SizeAndScale @@ -291,10 +292,6 @@ def widgets_configure_throughput(): logging.debug("Click widgets configure throughput") -def session_change_sessions(): - logging.debug("Click session change sessions") - - def session_node_types(): logging.debug("Click session node types") @@ -433,5 +430,10 @@ class MenuAction: def session_options(self): logging.debug("Click session options") - dialog = SessionOptionsDialog(self.application) + dialog = SessionOptionsDialog(self.application, self.application) + dialog.show() + + def session_change_sessions(self): + logging.debug("Click session change sessions") + dialog = SessionsDialog(self.application, self.application) dialog.show() diff --git a/coretk/coretk/querysessiondrawing.py b/coretk/coretk/querysessiondrawing.py deleted file mode 100644 index 84786ba5..00000000 --- a/coretk/coretk/querysessiondrawing.py +++ /dev/null @@ -1,183 +0,0 @@ -import logging -import tkinter as tk -from tkinter.ttk import Scrollbar, Treeview - -from coretk.images import ImageEnum, Images - - -class SessionTable: - def __init__(self, grpc, master): - """ - create session table instance - :param coretk.coregrpc.CoreGrpc grpc: coregrpc - :param root.master master: - """ - self.grpc = grpc - self.selected = False - self.selected_sid = None - self.master = master - self.top = tk.Toplevel(self.master) - self.description_definition() - self.top.title("CORE sessions") - - self.tree = Treeview(self.top) - # self.tree.pack(side=tk.TOP) - self.tree.grid(row=1, column=0, columnspan=2) - self.draw_scrollbar() - self.draw() - - def description_definition(self): - """ - write a short description - :return: nothing - """ - lable = tk.Label( - self.top, - text="Below is a list of active CORE sessions. Double-click to " - "\nconnect to an existing session. Usually, only sessions in " - "\nthe RUNTIME state persist in the daemon, except for the " - "\none you might be concurrently editting.", - ) - lable.grid(sticky=tk.W) - - def column_definition(self): - # self.tree["columns"] = ("name", "nodecount", "filename", "date") - self.tree["columns"] = "nodecount" - self.tree.column("#0", width=300, minwidth=30) - # self.tree.column("name", width=72, miwidth=30) - self.tree.column("nodecount", width=300, minwidth=30) - # self.tree.column("filename", width=92, minwidth=30) - # self.tree.column("date", width=170, minwidth=30) - - def draw_scrollbar(self): - yscrollbar = Scrollbar(self.top, orient="vertical", command=self.tree.yview) - yscrollbar.grid(row=1, column=3, sticky=tk.N + tk.S + tk.W) - self.tree.configure(yscrollcommand=yscrollbar.set) - - xscrollbar = Scrollbar(self.top, orient="horizontal", command=self.tree.xview) - xscrollbar.grid(row=2, columnspan=2, sticky=tk.E + tk.W + tk.S) - self.tree.configure(xscrollcommand=xscrollbar.set) - - def heading_definition(self): - self.tree.heading("#0", text="ID", anchor=tk.W) - # self.tree.heading("name", text="Name", anchor=tk.CENTER) - self.tree.heading("nodecount", text="Node Count", anchor=tk.W) - # self.tree.heading("filename", text="Filename", anchor=tk.CENTER) - # self.tree.heading("date", text="Date", anchor=tk.CENTER) - - def enter_session(self, sid): - self.top.destroy() - response = self.grpc.core.get_session(sid) - self.grpc.session_id = sid - self.grpc.core.events(sid, self.grpc.log_event) - logging.info("Entering session_id %s.... Result: %s", sid, response) - - def new_session(self): - self.top.destroy() - self.grpc.create_new_session() - - def on_selected(self, event): - item = self.tree.selection() - sid = int(self.tree.item(item, "text")) - self.enter_session(sid) - - def click_select(self, event): - # logging.debug("Click on %s ", event) - item = self.tree.selection() - sid = int(self.tree.item(item, "text")) - self.selected = True - self.selected_sid = sid - - def session_definition(self): - response = self.grpc.core.get_sessions() - # logging.info("querysessiondrawing.py Get all sessions %s", response) - index = 1 - for session in response.sessions: - self.tree.insert( - "", index, None, text=str(session.id), values=(str(session.nodes)) - ) - index = index + 1 - self.tree.bind("", self.on_selected) - self.tree.bind("<>", self.click_select) - - def click_connect(self): - """ - if no session is selected yet, create a new one else join that session - - :return: nothing - """ - if self.selected and self.selected_sid is not None: - self.enter_session(self.selected_sid) - elif not self.selected and self.selected_sid is None: - self.new_session() - else: - logging.error("querysessiondrawing.py invalid state") - - def shutdown_session(self, sid): - self.grpc.terminate_session(sid) - self.new_session() - self.top.destroy() - - def click_shutdown(self): - """ - if no session is currently selected create a new session else shut the selected session down - - :return: nothing - """ - if self.selected and self.selected_sid is not None: - self.shutdown_session(self.selected_sid) - elif not self.selected and self.selected_sid is None: - self.new_session() - else: - logging.error("querysessiondrawing.py invalid state") - # if self.selected and self.selected_sid is not None: - - def draw_buttons(self): - f = tk.Frame(self.top) - f.grid(row=3, sticky=tk.W) - - b = tk.Button( - f, - image=Images.get(ImageEnum.DOCUMENTNEW.value), - text="New", - compound=tk.LEFT, - command=self.new_session, - ) - b.pack(side=tk.LEFT, padx=3, pady=4) - b = tk.Button( - f, - image=Images.get(ImageEnum.FILEOPEN.value), - text="Connect", - compound=tk.LEFT, - command=self.click_connect, - ) - b.pack(side=tk.LEFT, padx=3, pady=4) - b = tk.Button( - f, - image=Images.get(ImageEnum.EDITDELETE.value), - text="Shutdown", - compound=tk.LEFT, - command=self.click_shutdown, - ) - b.pack(side=tk.LEFT, padx=3, pady=4) - b = tk.Button(f, text="Cancel", command=self.new_session) - b.pack(side=tk.LEFT, padx=3, pady=4) - - def center(self): - window_width = self.master.winfo_width() - window_height = self.master.winfo_height() - self.top.update() - top_level_width = self.top.winfo_width() - top_level_height = self.top.winfo_height() - x = window_width / 2 - top_level_width / 2 - y = window_height / 2 - top_level_height / 2 - - self.top.geometry("+%d+%d" % (x, y)) - - def draw(self): - self.column_definition() - self.heading_definition() - self.session_definition() - self.draw_buttons() - self.center() - self.top.wait_window() From 60fff5491802b7bd55ef6fd50925b91bdd56c8d8 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 1 Nov 2019 08:35:14 -0700 Subject: [PATCH 149/462] work on wlan config --- coretk/coretk/canvasaction.py | 11 +- coretk/coretk/coregrpc.py | 100 +++++++++---- coretk/coretk/coretoolbar.py | 62 ++++---- coretk/coretk/coretoolbarhelp.py | 113 +++++++++++++-- coretk/coretk/graph.py | 4 - coretk/coretk/grpcmanagement.py | 7 + coretk/coretk/icons/emane.gif | Bin 0 -> 1111 bytes coretk/coretk/images.py | 3 + coretk/coretk/interface.py | 6 +- coretk/coretk/menuaction.py | 12 +- coretk/coretk/nodeconfigtable.py | 11 +- coretk/coretk/nodeservice.py | 195 ++++++++++++++++++++++++++ coretk/coretk/serviceconfiguration.py | 10 -- coretk/coretk/wlanconfiguration.py | 150 +++++++++++++++++--- coretk/coretk/wlannodeconfig.py | 29 ++++ 15 files changed, 606 insertions(+), 107 deletions(-) create mode 100644 coretk/coretk/icons/emane.gif create mode 100644 coretk/coretk/nodeservice.py delete mode 100644 coretk/coretk/serviceconfiguration.py create mode 100644 coretk/coretk/wlannodeconfig.py diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py index addd2cd8..0c56d4ba 100644 --- a/coretk/coretk/canvasaction.py +++ b/coretk/coretk/canvasaction.py @@ -8,6 +8,7 @@ from core.api.grpc import core_pb2 from coretk.nodeconfigtable import NodeConfig from coretk.wlanconfiguration import WlanConfiguration +# TODO, finish classifying node types NODE_TO_TYPE = { "router": core_pb2.NodeType.DEFAULT, "wlan": core_pb2.NodeType.WIRELESS_LAN, @@ -27,12 +28,16 @@ class CanvasAction: if pb_type == core_pb2.NodeType.DEFAULT: self.display_node_configuration() elif pb_type == core_pb2.NodeType.WIRELESS_LAN: - self.display_wlan_configuration() + self.display_wlan_configuration(canvas_node) def display_node_configuration(self): NodeConfig(self.canvas, self.node_to_show_config) self.node_to_show_config = None - def display_wlan_configuration(self): - WlanConfiguration(self.canvas, self.node_to_show_config) + def display_wlan_configuration(self, canvas_node): + # print(self.canvas.grpc_manager.wlanconfig_management.configurations) + wlan_config = self.canvas.grpc_manager.wlanconfig_management.configurations[ + canvas_node.core_id + ] + WlanConfiguration(self.canvas, self.node_to_show_config, wlan_config) self.node_to_show_config = None diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 9b51116a..3a1b1719 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -29,8 +29,10 @@ class CoreGrpc: self.wireless_draw = WirelessConnection(app.canvas, self) def log_event(self, event): - logging.info("event: %s", event) + # logging.info("event: %s", event) if event.link_event is not None: + logging.info("event: %s", event) + self.wireless_draw.hangle_link_event(event.link_event) def log_throughput(self, event): @@ -206,6 +208,54 @@ class CoreGrpc: ) logging.info("delete links %s", response) + def create_interface(self, node_type, gui_interface): + """ + create a protobuf interface given the interface object stored by the programmer + + :param core_bp2.NodeType type: node type + :param coretk.interface.Interface gui_interface: the programmer's interface object + :rtype: core_bp2.Interface + :return: protobuf interface object + """ + if node_type != core_pb2.NodeType.DEFAULT: + return None + else: + interface = core_pb2.Interface( + id=gui_interface.id, + name=gui_interface.name, + mac=gui_interface.mac, + ip4=gui_interface.ipv4, + ip4mask=gui_interface.ip4prefix, + ) + logging.debug("create interface 1 %s", interface) + + return interface + + # TODO add location, hooks, emane_config, etc... + def start_session( + self, + nodes, + links, + location=None, + hooks=None, + emane_config=None, + emane_model_configs=None, + wlan_configs=None, + mobility_configs=None, + ): + response = self.core.start_session( + session_id=self.session_id, + nodes=nodes, + links=links, + wlan_configs=wlan_configs, + ) + logging.debug("Start session %s, result: %s", self.session_id, response.result) + + def stop_session(self): + response = self.core.stop_session(session_id=self.session_id) + logging.debug("coregrpc.py Stop session, result: %s", response.result) + + # TODO no need, might get rid of this def add_link(self, id1, id2, type1, type2, edge): """ Grpc client request add link @@ -217,30 +267,30 @@ class CoreGrpc: :param core_pb2.NodeType type2: node 2 core node type :return: nothing """ - if1 = None - if2 = None - if type1 == core_pb2.NodeType.DEFAULT: - interface = edge.interface_1 - if1 = core_pb2.Interface( - id=interface.id, - name=interface.name, - mac=interface.mac, - ip4=interface.ipv4, - ip4mask=interface.ip4prefix, - ) - logging.debug("create interface 1 %s", if1) - # interface1 = self.interface_helper.create_interface(id1, 0) - - if type2 == core_pb2.NodeType.DEFAULT: - interface = edge.interface_2 - if2 = core_pb2.Interface( - id=interface.id, - name=interface.name, - mac=interface.mac, - ip4=interface.ipv4, - ip4mask=interface.ip4prefix, - ) - logging.debug("create interface 2: %s", if2) + if1 = self.create_interface(type1, edge.interface_1) + if2 = self.create_interface(type2, edge.interface_2) + # if type1 == core_pb2.NodeType.DEFAULT: + # interface = edge.interface_1 + # if1 = core_pb2.Interface( + # id=interface.id, + # name=interface.name, + # mac=interface.mac, + # ip4=interface.ipv4, + # ip4mask=interface.ip4prefix, + # ) + # logging.debug("create interface 1 %s", if1) + # # interface1 = self.interface_helper.create_interface(id1, 0) + # + # if type2 == core_pb2.NodeType.DEFAULT: + # interface = edge.interface_2 + # if2 = core_pb2.Interface( + # id=interface.id, + # name=interface.name, + # mac=interface.mac, + # ip4=interface.ipv4, + # ip4mask=interface.ip4prefix, + # ) + # logging.debug("create interface 2: %s", if2) response = self.core.add_link(self.session_id, id1, id2, if1, if2) logging.info("created link: %s", response) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index d46a9791..58c25c77 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -1,22 +1,23 @@ import logging import tkinter as tk -from enum import Enum -from core.api.grpc import core_pb2 +# from core.api.grpc import core_pb2 from coretk.coretoolbarhelp import CoreToolbarHelp from coretk.graph import GraphMode from coretk.images import ImageEnum, Images from coretk.tooltip import CreateToolTip +# from enum import Enum -class SessionStateEnum(Enum): - NONE = "none" - DEFINITION = "definition" - CONFIGURATION = "configuration" - RUNTIME = "runtime" - DATACOLLECT = "datacollect" - SHUTDOWN = "shutdown" - INSTANTIATION = "instantiation" + +# class SessionStateEnum(Enum): +# NONE = "none" +# DEFINITION = "definition" +# CONFIGURATION = "configuration" +# RUNTIME = "runtime" +# DATACOLLECT = "datacollect" +# SHUTDOWN = "shutdown" +# INSTANTIATION = "instantiation" class CoreToolbar(object): @@ -161,21 +162,22 @@ class CoreToolbar(object): """ logging.debug("Click START STOP SESSION button") helper = CoreToolbarHelp(self.application) - # self.destroy_children_widgets(self.edit_frame) self.destroy_children_widgets() self.canvas.mode = GraphMode.SELECT # set configuration state - state = self.canvas.core_grpc.get_session_state() + # state = self.canvas.core_grpc.get_session_state() + # if state == core_pb2.SessionState.SHUTDOWN or self.application.is_open_xml: + # self.canvas.core_grpc.set_session_state(SessionStateEnum.DEFINITION.value) + # self.application.is_open_xml = False + # + # self.canvas.core_grpc.set_session_state(SessionStateEnum.CONFIGURATION.value) + # helper.add_nodes() + # helper.add_edges() + # self.canvas.core_grpc.set_session_state(SessionStateEnum.INSTANTIATION.value) + helper.gui_start_session() + self.create_runtime_toolbar() - if state == core_pb2.SessionState.SHUTDOWN or self.application.is_open_xml: - self.canvas.core_grpc.set_session_state(SessionStateEnum.DEFINITION.value) - self.application.is_open_xml = False - - self.canvas.core_grpc.set_session_state(SessionStateEnum.CONFIGURATION.value) - - helper.add_nodes() - helper.add_edges() # for node in self.canvas.grpc_manager.nodes.values(): # print(node.type, node.model, int(node.x), int(node.y), node.name, node.node_id) # self.canvas.core_grpc.add_node( @@ -188,10 +190,8 @@ class CoreToolbar(object): # self.canvas.core_grpc.add_link( # edge.id1, edge.id2, edge.type1, edge.type2, edge # ) - self.canvas.core_grpc.set_session_state(SessionStateEnum.INSTANTIATION.value) # self.canvas.core_grpc.get_session() # self.application.is_open_xml = False - self.create_runtime_toolbar() def click_link_tool(self): logging.debug("Click LINK button") @@ -366,6 +366,13 @@ class CoreToolbar(object): self.canvas.draw_node_image = Images.get(ImageEnum.TUNNEL.value) self.canvas.draw_node_name = "tunnel" + def pick_emane(self, main_button): + self.link_layer_option_menu.destroy() + main_button.configure(image=Images.get(ImageEnum.EMANE.value)) + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get(ImageEnum.EMANE.value) + self.canvas.draw_node_name = "emane" + def draw_link_layer_options(self, link_layer_button): """ Draw the options for link-layer button @@ -380,6 +387,7 @@ class CoreToolbar(object): Images.get(ImageEnum.HUB.value), Images.get(ImageEnum.SWITCH.value), Images.get(ImageEnum.WLAN.value), + Images.get(ImageEnum.EMANE.value), Images.get(ImageEnum.RJ45.value), Images.get(ImageEnum.TUNNEL.value), ] @@ -387,6 +395,7 @@ class CoreToolbar(object): self.pick_hub, self.pick_switch, self.pick_wlan, + self.pick_emane, self.pick_rj45, self.pick_tunnel, ] @@ -394,6 +403,7 @@ class CoreToolbar(object): "ethernet hub", "ethernet switch", "wireless LAN", + "emane", "rj45 physical interface tool", "tunnel tool", ] @@ -582,12 +592,12 @@ class CoreToolbar(object): :return: nothing """ logging.debug("Click on STOP button ") - # self.destroy_children_widgets(self.edit_frame) self.destroy_children_widgets() - self.canvas.core_grpc.set_session_state(SessionStateEnum.DATACOLLECT.value) - self.canvas.core_grpc.delete_links() - self.canvas.core_grpc.delete_nodes() + # self.canvas.core_grpc.set_session_state(SessionStateEnum.DATACOLLECT.value) + # self.canvas.core_grpc.delete_links() + # self.canvas.core_grpc.delete_nodes() + self.canvas.core_grpc.stop_session() self.create_toolbar() def click_run_button(self): diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index a02c7ae9..b4b6fe28 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -1,6 +1,7 @@ """ CoreToolbar help to draw on canvas, and make grpc client call """ +from core.api.grpc.client import core_pb2 class CoreToolbarHelp: @@ -8,24 +9,114 @@ class CoreToolbarHelp: self.application = application self.core_grpc = application.core_grpc - def add_nodes(self): + def get_node_list(self): """ - add the nodes stored in grpc manager + form a list node protobuf nodes to pass in start_session in grpc + :return: nothing """ grpc_manager = self.application.canvas.grpc_manager - for node in grpc_manager.nodes.values(): - self.application.core_grpc.add_node( - node.type, node.model, int(node.x), int(node.y), node.name, node.node_id - ) - def add_edges(self): + # list(core_pb2.Node) + nodes = [] + + for node in grpc_manager.nodes.values(): + pos = core_pb2.Position(x=int(node.x), y=int(node.y)) + n = core_pb2.Node( + id=node.node_id, type=node.type, position=pos, model=node.model + ) + nodes.append(n) + return nodes + + def get_link_list(self): """ - add the edges stored in grpc manager - :return: + form a list of links to pass into grpc start session + + :rtype: list(core_pb2.Link) + :return: list of protobuf links """ grpc_manager = self.application.canvas.grpc_manager + + # list(core_bp2.Link) + links = [] for edge in grpc_manager.edges.values(): - self.application.core_grpc.add_link( - edge.id1, edge.id2, edge.type1, edge.type2, edge + interface_one = self.application.core_grpc.create_interface( + edge.type1, edge.interface_1 ) + interface_two = self.application.core_grpc.create_interface( + edge.type2, edge.interface_2 + ) + # TODO for now only consider the basic cases + if ( + edge.type1 == core_pb2.NodeType.WIRELESS_LAN + or edge.type2 == core_pb2.NodeType.WIRELESS_LAN + ): + link_type = core_pb2.LinkType.WIRELESS + else: + link_type = core_pb2.LinkType.WIRED + link = core_pb2.Link( + node_one_id=edge.id1, + node_two_id=edge.id2, + type=link_type, + interface_one=interface_one, + interface_two=interface_two, + ) + links.append(link) + # self.id1 = edge.id1 + # self.id2 = edge.id2 + # self.type = link_type + # self.if1 = interface_one + # self.if2 = interface_two + + return links + + def get_wlan_configuration_list(self): + configs = [] + grpc_manager = self.application.canvas.grpc_manager + manager_configs = grpc_manager.wlanconfig_management.configurations + for key in manager_configs: + cnf = core_pb2.WlanConfig(node_id=key, config=manager_configs[key]) + configs.append(cnf) + return configs + + def gui_start_session(self): + # list(core_pb2.Node) + nodes = self.get_node_list() + + # list(core_bp2.Link) + links = self.get_link_list() + + # print(links[0]) + wlan_configs = self.get_wlan_configuration_list() + # print(wlan_configs) + self.core_grpc.start_session(nodes, links, wlan_configs=wlan_configs) + # self.core_grpc.core.add_link(self.core_grpc.session_id, self.id1, self.id2, self.if1, self.if2) + # res = self.core_grpc.core.get_wlan_config(self.core_grpc.session_id, 1) + + # res = self.core_grpc.core.get_session(self.core_grpc.session_id).session + # print(res) + # res = self.core_grpc.core.get_wlan_config(self.core_grpc.session_id, 1) + + # print(res) + + # def add_nodes(self): + # """ + # add the nodes stored in grpc manager + # :return: nothing + # """ + # grpc_manager = self.application.canvas.grpc_manager + # for node in grpc_manager.nodes.values(): + # self.application.core_grpc.add_node( + # node.type, node.model, int(node.x), int(node.y), node.name, node.node_id + # ) + # + # def add_edges(self): + # """ + # add the edges stored in grpc manager + # :return: + # """ + # grpc_manager = self.application.canvas.grpc_manager + # for edge in grpc_manager.edges.values(): + # self.application.core_grpc.add_link( + # edge.id1, edge.id2, edge.type1, edge.type2, edge + # ) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index dd3bbc1c..ef0f064f 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -58,10 +58,6 @@ class CanvasGraph(tk.Canvas): # self.core_map = CoreToCanvasMapping() # self.draw_existing_component() - def test(self): - print("testing the button") - print(self.node_context.winfo_rootx()) - def setup_menus(self): self.node_context = tk.Menu(self.master) self.node_context.add_command( diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index 4cfc6a16..e7373dc6 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -7,6 +7,7 @@ import logging from core.api.grpc import core_pb2 from coretk.coretocanvas import CoreToCanvasMapping from coretk.interface import Interface, InterfaceManager +from coretk.wlannodeconfig import WlanNodeConfig link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] network_layer_nodes = ["router", "host", "PC", "mdr", "prouter", "OVS"] @@ -72,6 +73,8 @@ class GrpcManager: # self.node_id_and_interface_to_edge_token = {} self.core_mapping = CoreToCanvasMapping() + self.wlanconfig_management = WlanNodeConfig() + def update_preexisting_ids(self): """ get preexisting node ids @@ -145,6 +148,10 @@ class GrpcManager: logging.error("grpcmanagemeny.py INVALID node name") nid = self.get_id() create_node = Node(session_id, nid, node_type, node_model, x, y, name) + + # set default configuration for wireless node + self.wlanconfig_management.set_default_config(node_type, nid) + self.nodes[canvas_id] = create_node self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) # self.core_id_to_canvas_id[nid] = canvas_id diff --git a/coretk/coretk/icons/emane.gif b/coretk/coretk/icons/emane.gif new file mode 100644 index 0000000000000000000000000000000000000000..8a3d3850d4c01d35e3322c97b8cd0ab00251ecd8 GIT binary patch literal 1111 zcmW+!Uue)(7(D}p89_BM)XmtxBE44IKCNjFQrT)0L!wKI3bD;^%+_DXryeeCA)}Qv zqWBE4bQrr}ZY?5waS`Q$uZ-BG@MV#(psWa4Qc771P(TF@bg;Tq+#*nc3B1B2 zK!Qk62`0fMP$DFXM3ra~T_Pnxl1NfXCdnmHG9-&+m28q-GNnL@NKq*!#idXPp$JuI zLKjv#sxO zw1^hfVp?1al~9UOm8NuMWkKb*7}Q_}uN<)e3t~Ynm<6{$i?Apb)uLH+i?jqwVo5ET zCAUP&uq>9w?ZR~VpO9U-B`I_g)I(sn8T|uJivo^P!Hz8JGnl_iy~ z3aqLSz#t6DU<}ScMlcGaG8&^Zk_k-0q)f)-Ok@VLFe|e$J2P3pA}q>cEY7k_Ru!#E zyRy8RdxdK$1^z}{u4nEZX z&53(&(@U*+B98YSUj5?1%Z=$|%fORwc6Tg3piPIL-c~!jIQ{Ctxs&TQ*DYRn`If;W zpa1>ssk!l6f3I8h!yTs{o9sUN^RM&kj?3&_KOHFbkM?(+9%=8n{z%#K;M|Fg7gt~V z_J{pnZhvj`w@=o8FnDHP`_7B4y*Cbh-@5Cav4)wCXW!~s7+)Ie%@;>", self.group_select) + + def group_service_select(self, event): + print("select group service") + listbox = event.widget + cur_selection = listbox.curselection() + if cur_selection: + s = listbox.get(listbox.curselection()) + self.service_to_config = s + else: + self.service_to_config = None + + def group_services(self): + f = tk.Frame(self.config_frame) + lbl = tk.Label(f, text="Group services") + lbl.grid() + + sb = tk.Scrollbar(f, orient=tk.VERTICAL) + sb.grid(row=1, column=1, sticky=tk.S + tk.N) + + listbox = tk.Listbox( + f, + selectmode=tk.SINGLE, + yscrollcommand=sb.set, + relief=tk.FLAT, + highlightbackground="#b3b3b3", + highlightcolor="#b3b3b3", + highlightthickness=0.5, + bd=0, + ) + listbox.grid(row=1, column=0) + sb.config(command=listbox.yview) + f.grid(padx=3, pady=3, row=0, column=1) + + listbox.bind("<>", self.group_service_select) + + def current_services(self): + f = tk.Frame(self.config_frame) + lbl = tk.Label(f, text="Current services") + lbl.grid() + + sb = tk.Scrollbar(f, orient=tk.VERTICAL) + sb.grid(row=1, column=1, sticky=tk.S + tk.N) + + listbox = tk.Listbox( + f, + selectmode=tk.MULTIPLE, + yscrollcommand=sb.set, + relief=tk.FLAT, + highlightbackground="#b3b3b3", + highlightcolor="#b3b3b3", + highlightthickness=0.5, + bd=0, + ) + listbox.grid(row=1, column=0) + sb.config(command=listbox.yview) + f.grid(padx=3, pady=3, row=0, column=2) + + def config_service(self): + if self.service_to_config is None: + messagebox.showinfo("CORE info", "Choose a service to configure.") + else: + print(self.service_to_config) + + def node_service_options(self): + f = tk.Frame(self.top) + b = tk.Button(f, text="Connfigure", command=self.config_service) + b.grid(row=0, column=0) + b = tk.Button(f, text="Apply") + b.grid(row=0, column=1) + b = tk.Button(f, text="Cancel", command=self.top.destroy) + b.grid(row=0, column=2) + f.grid(sticky=tk.E) diff --git a/coretk/coretk/serviceconfiguration.py b/coretk/coretk/serviceconfiguration.py deleted file mode 100644 index a539f1fb..00000000 --- a/coretk/coretk/serviceconfiguration.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -service configuration -""" - -# import tkinter as tk - - -class ServiceConfiguration: - def __init__(self): - return diff --git a/coretk/coretk/wlanconfiguration.py b/coretk/coretk/wlanconfiguration.py index 53e98ea5..32bb56e2 100644 --- a/coretk/coretk/wlanconfiguration.py +++ b/coretk/coretk/wlanconfiguration.py @@ -2,13 +2,20 @@ wlan configuration """ -import ast import tkinter as tk from functools import partial +from coretk.imagemodification import ImageModification + class WlanConfiguration: - def __init__(self, canvas, canvas_node): + def __init__(self, canvas, canvas_node, config): + """ + create an instance of WlanConfiguration + + :param coretk.grpah.CanvasGraph canvas: canvas object + :param coretk.graph.CanvasNode canvas_node: canvas node object + """ self.canvas = canvas self.image = canvas_node.image @@ -20,10 +27,14 @@ class WlanConfiguration: self.top.title("wlan configuration") self.node_name = tk.StringVar() - self.range_var = tk.DoubleVar() - self.range_var.set(275.0) - self.bandwidth_var = tk.IntVar() - self.bandwidth_var.set(54000000) + # self.range_var = tk.DoubleVar() + # self.range_var.set(275.0) + self.config = config + self.range_var = tk.StringVar() + self.range_var.set(config["basic_range"]) + # self.bandwidth_var = tk.IntVar() + self.bandwidth_var = tk.StringVar() + self.bandwidth_var.set(config["bandwidth"]) self.delay_var = tk.StringVar() @@ -34,6 +45,11 @@ class WlanConfiguration: self.config_option() def image_modification(self): + """ + draw image modification part + + :return: nothing + """ f = tk.Frame(self.top, bg="#d9d9d9") lbl = tk.Label(f, text="Node name: ", bg="#d9d9d9") lbl.grid(row=0, column=0, padx=3, pady=3) @@ -41,20 +57,40 @@ class WlanConfiguration: e.grid(row=0, column=1, padx=3, pady=3) b = tk.Button(f, text="None") b.grid(row=0, column=2, padx=3, pady=3) - b = tk.Button(f, text="not implemented") + b = tk.Button( + f, + image=self.image, + command=lambda: ImageModification( + canvas=self.canvas, canvas_node=self.canvas_node, node_config=self + ), + ) b.grid(row=0, column=3, padx=3, pady=3) f.grid(padx=2, pady=2, ipadx=2, ipady=2) def create_string_var(self, val): + """ + create string variable for convenience + + :param str val: text value + :return: nothing + """ v = tk.StringVar() v.set(val) return v def scrollbar_command(self, entry_widget, delta, event): + """ + change text in entry based on scrollbar action (click up or down) + + :param tkinter.Entry entry_widget: entry needed for changing text + :param int or float delta: the amount to change + :param event: scrollbar event + :return: nothing + """ try: value = int(entry_widget.get()) except ValueError: - value = ast.literal_eval(entry_widget.get()) + value = float(entry_widget.get()) entry_widget.delete(0, tk.END) if event == "-1": entry_widget.insert(tk.END, str(round(value + delta, 1))) @@ -62,6 +98,11 @@ class WlanConfiguration: entry_widget.insert(tk.END, str(round(value - delta, 1))) def wlan_configuration(self): + """ + create wireless configuration table + + :return: nothing + """ lbl = tk.Label(self.top, text="Wireless") lbl.grid(sticky=tk.W, padx=3, pady=3) @@ -86,7 +127,12 @@ class WlanConfiguration: lbl = tk.Label(f1, text="Range: ", bg="#d9d9d9") lbl.grid(row=0, column=0) - e = tk.Entry(f1, textvariable=self.range_var, width=5, bg="white") + e = tk.Entry( + f1, + textvariable=self.create_string_var(self.config["basic_range"]), + width=5, + bg="white", + ) e.grid(row=0, column=1) lbl = tk.Label(f1, text="Bandwidth (bps): ", bg="#d9d9d9") @@ -94,7 +140,12 @@ class WlanConfiguration: f11 = tk.Frame(f1, bg="#d9d9d9") sb = tk.Scrollbar(f11, orient=tk.VERTICAL) - e = tk.Entry(f11, textvariable=self.bandwidth_var, width=10, bg="white") + e = tk.Entry( + f11, + textvariable=self.create_string_var(self.config["bandwidth"]), + width=10, + bg="white", + ) sb.config(command=partial(self.scrollbar_command, e, 1000000)) e.grid() sb.grid(row=0, column=1) @@ -110,7 +161,9 @@ class WlanConfiguration: f21 = tk.Frame(f2, bg="#d9d9d9") sb = tk.Scrollbar(f21, orient=tk.VERTICAL) - e = tk.Entry(f21, textvariable=self.create_string_var(20000), bg="white") + e = tk.Entry( + f21, textvariable=self.create_string_var(self.config["delay"]), bg="white" + ) sb.config(command=partial(self.scrollbar_command, e, 5000)) e.grid() sb.grid(row=0, column=1) @@ -121,7 +174,9 @@ class WlanConfiguration: f22 = tk.Frame(f2, bg="#d9d9d9") sb = tk.Scrollbar(f22, orient=tk.VERTICAL) - e = tk.Entry(f22, textvariable=self.create_string_var(0), bg="white") + e = tk.Entry( + f22, textvariable=self.create_string_var(self.config["error"]), bg="white" + ) sb.config(command=partial(self.scrollbar_command, e, 0.1)) e.grid() sb.grid(row=0, column=1) @@ -136,7 +191,9 @@ class WlanConfiguration: lbl.grid() f31 = tk.Frame(f3, bg="#d9d9d9") sb = tk.Scrollbar(f31, orient=tk.VERTICAL) - e = tk.Entry(f31, textvariable=self.create_string_var(0), bg="white") + e = tk.Entry( + f31, textvariable=self.create_string_var(self.config["jitter"]), bg="white" + ) sb.config(command=partial(self.scrollbar_command, e, 5000)) e.grid() sb.grid(row=0, column=1) @@ -146,23 +203,33 @@ class WlanConfiguration: f.grid(padx=3, pady=3) def subnet(self): + """ + create the entries for ipv4 subnet and ipv6 subnet + + :return: nothing + """ f = tk.Frame(self.top) f1 = tk.Frame(f) lbl = tk.Label(f1, text="IPv4 subnet") lbl.grid() - e = tk.Entry(f1, width=30, bg="white") + e = tk.Entry(f1, width=30, bg="white", textvariable=self.create_string_var("")) e.grid(row=0, column=1) f1.grid() f2 = tk.Frame(f) lbl = tk.Label(f2, text="IPv6 subnet") lbl.grid() - e = tk.Entry(f2, width=30, bg="white") + e = tk.Entry(f2, width=30, bg="white", textvariable=self.create_string_var("")) e.grid(row=0, column=1) f2.grid() f.grid(sticky=tk.W, padx=3, pady=3) def wlan_options(self): + """ + create wireless node options + + :return: + """ f = tk.Frame(self.top) b = tk.Button(f, text="ns-2 mobility script...") b.pack(side=tk.LEFT, padx=1) @@ -172,9 +239,60 @@ class WlanConfiguration: b.pack(side=tk.LEFT, padx=1) f.grid(sticky=tk.W) + def wlan_config_apply(self): + """ + retrieve user's wlan configuration and store the new configuration values + + :return: nothing + """ + config_frame = self.top.grid_slaves(row=2, column=0)[0] + range_and_bandwidth_frame = config_frame.grid_slaves(row=1, column=0)[0] + range_val = range_and_bandwidth_frame.grid_slaves(row=0, column=1)[0].get() + bandwidth = ( + range_and_bandwidth_frame.grid_slaves(row=0, column=3)[0] + .grid_slaves(row=0, column=0)[0] + .get() + ) + + delay_and_loss_frame = config_frame.grid_slaves(row=2, column=0)[0] + delay = ( + delay_and_loss_frame.grid_slaves(row=0, column=1)[0] + .grid_slaves(row=0, column=0)[0] + .get() + ) + loss = ( + delay_and_loss_frame.grid_slaves(row=0, column=3)[0] + .grid_slaves(row=0, column=0)[0] + .get() + ) + + jitter_frame = config_frame.grid_slaves(row=3, column=0)[0] + jitter_val = ( + jitter_frame.grid_slaves(row=0, column=1)[0] + .grid_slaves(row=0, column=0)[0] + .get() + ) + + # set wireless node configuration here + wlanconfig_manager = self.canvas.grpc_manager.wlanconfig_management + wlanconfig_manager.set_custom_config( + node_id=self.canvas_node.core_id, + range=range_val, + bandwidth=bandwidth, + jitter=jitter_val, + delay=delay, + error=loss, + ) + self.top.destroy() + def config_option(self): + """ + create node configuration options + + :return: nothing + """ f = tk.Frame(self.top, bg="#d9d9d9") - b = tk.Button(f, text="Apply", bg="#d9d9d9") + b = tk.Button(f, text="Apply", bg="#d9d9d9", command=self.wlan_config_apply) b.grid(padx=2, pady=2) b = tk.Button(f, text="Cancel", bg="#d9d9d9", command=self.top.destroy) b.grid(row=0, column=1, padx=2, pady=2) diff --git a/coretk/coretk/wlannodeconfig.py b/coretk/coretk/wlannodeconfig.py new file mode 100644 index 00000000..647cd7cf --- /dev/null +++ b/coretk/coretk/wlannodeconfig.py @@ -0,0 +1,29 @@ +""" +wireless node configuration for all the wireless node +""" +from collections import OrderedDict + +from core.api.grpc import core_pb2 + + +class WlanNodeConfig: + def __init__(self): + # maps node id to wlan configuration + self.configurations = {} + + def set_default_config(self, node_type, node_id): + if node_type == core_pb2.NodeType.WIRELESS_LAN: + config = OrderedDict() + config["basic_range"] = "275" + config["bandwidth"] = "54000000" + config["jitter"] = "0" + config["delay"] = "20000" + config["error"] = "0" + self.configurations[node_id] = config + + def set_custom_config(self, node_id, range, bandwidth, jitter, delay, error): + self.configurations[node_id]["basic_range"] = range + self.configurations[node_id]["bandwidth"] = bandwidth + self.configurations[node_id]["jitter"] = jitter + self.configurations[node_id]["delay"] = delay + self.configurations[node_id]["error"] = error From 96d020a53df7ccea6547799c283e06b1d32db925 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 1 Nov 2019 09:01:56 -0700 Subject: [PATCH 150/462] coretk --- coretk/coretk/coretoolbar.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 628ddc35..78688725 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -384,12 +384,12 @@ class CoreToolbar(object): self.destroy_previous_frame() option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ - Images.get(ImageEnum.HUB.value), - Images.get(ImageEnum.SWITCH.value), - Images.get(ImageEnum.WLAN.value), - Images.get(ImageEnum.EMANE.value), - Images.get(ImageEnum.RJ45.value), - Images.get(ImageEnum.TUNNEL.value), + Images.get(ImageEnum.HUB), + Images.get(ImageEnum.SWITCH), + Images.get(ImageEnum.WLAN), + Images.get(ImageEnum.EMANE), + Images.get(ImageEnum.RJ45), + Images.get(ImageEnum.TUNNEL), ] func_list = [ self.pick_hub, From 0e5d7a6f801cf30c6969351a0e3fbec8b0779a56 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 Nov 2019 10:45:47 -0700 Subject: [PATCH 151/462] cleanup for organizing how some parameters are passed around, merging logic into central places, avoiding things like needing to requery a session multiple times for joining --- coretk/coretk/app.py | 56 ++++++-------------------- coretk/coretk/appcache.py | 33 --------------- coretk/coretk/coregrpc.py | 50 ++++++++++++++--------- coretk/coretk/coretoolbarhelp.py | 17 ++++---- coretk/coretk/dialogs/sessions.py | 5 +-- coretk/coretk/graph.py | 62 ++++++++++++----------------- coretk/coretk/grpcmanagement.py | 15 +++---- coretk/coretk/images.py | 14 +++---- coretk/coretk/linkinfo.py | 18 ++++----- coretk/coretk/wirelessconnection.py | 3 +- 10 files changed, 100 insertions(+), 173 deletions(-) delete mode 100644 coretk/coretk/appcache.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index f253fd01..bcc43e43 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,8 +1,6 @@ import logging import tkinter as tk -import coretk.appcache as appcache -import coretk.images as images from coretk.coregrpc import CoreGrpc from coretk.coremenubar import CoreMenubar from coretk.coretoolbar import CoreToolbar @@ -14,28 +12,25 @@ from coretk.menuaction import MenuAction class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) - appcache.cache_variable(self) - print(self.is_open_xml) - self.load_images() - self.setup_app() + Images.load_all() self.menubar = None self.core_menu = None self.canvas = None self.core_editbar = None - self.core_grpc = None - + self.is_open_xml = False + self.size_and_scale = None + self.set_wallpaper = None + self.wallpaper_id = None + self.current_wallpaper = None + self.radiovar = tk.IntVar(value=1) + self.show_grid_var = tk.IntVar(value=1) + self.adjust_to_dim_var = tk.IntVar(value=0) + self.core_grpc = CoreGrpc(self) + self.setup_app() self.create_menu() self.create_widgets() self.draw_canvas() - self.start_grpc() - # self.try_make_table() - - def load_images(self): - """ - Load core images - :return: - """ - images.load_core_images(Images) + self.core_grpc.set_up() def setup_app(self): self.master.title("CORE") @@ -59,10 +54,7 @@ class Application(tk.Frame): def draw_canvas(self): self.canvas = CanvasGraph( - master=self, - grpc=self.core_grpc, - background="#cccccc", - scrollregion=(0, 0, 1200, 1000), + self, self.core_grpc, background="#cccccc", scrollregion=(0, 0, 1200, 1000) ) self.canvas.pack(fill=tk.BOTH, expand=True) @@ -86,31 +78,9 @@ class Application(tk.Frame): b = tk.Button(status_bar, text="Button 3") b.pack(side=tk.LEFT, padx=1) - def start_grpc(self): - """ - Conect client to grpc, query sessions and prompt use to choose an existing session if there exist any - - :return: nothing - """ - self.master.update() - self.core_grpc = CoreGrpc(self) - self.core_grpc.set_up() - self.canvas.core_grpc = self.core_grpc - self.canvas.grpc_manager.core_grpc = self.core_grpc - self.canvas.grpc_manager.update_preexisting_ids() - self.canvas.draw_existing_component() - def on_closing(self): menu_action = MenuAction(self, self.master) menu_action.on_quit() - # self.quit() - - def try_make_table(self): - f = tk.Frame(self.master) - for i in range(3): - e = tk.Entry(f) - e.grid(row=0, column=1, stick="nsew") - f.pack(side=tk.TOP) if __name__ == "__main__": diff --git a/coretk/coretk/appcache.py b/coretk/coretk/appcache.py deleted file mode 100644 index b2f87f71..00000000 --- a/coretk/coretk/appcache.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -stores some information helpful for setting starting values for some tables -like size and scale, set wallpaper, etc -""" -import tkinter as tk - - -def cache_variable(application): - # for menubar - application.is_open_xml = False - - application.size_and_scale = None - application.set_wallpaper = None - - # set wallpaper variables - - # canvas id of the wallpaper - application.wallpaper_id = None - - # current image for wallpaper - application.current_wallpaper = None - - # wallpaper option - application.radiovar = tk.IntVar() - application.radiovar.set(1) - - # show grid option - application.show_grid_var = tk.IntVar() - application.show_grid_var.set(1) - - # adjust canvas to image dimension variable - application.adjust_to_dim_var = tk.IntVar() - application.adjust_to_dim_var.set(0) diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 13fe28b3..e38a0940 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -7,8 +7,7 @@ from collections import OrderedDict from core.api.grpc import client, core_pb2 from coretk.dialogs.sessions import SessionsDialog -from coretk.linkinfo import Throughput -from coretk.wirelessconnection import WirelessConnection +from coretk.grpcmanagement import GrpcManager class CoreGrpc: @@ -22,15 +21,14 @@ class CoreGrpc: self.app = app self.master = app.master self.interface_helper = None - self.throughput_draw = Throughput(app.canvas, self) - self.wireless_draw = WirelessConnection(app.canvas, self) + self.manager = GrpcManager(self) - def log_event(self, event): + def handle_events(self, event): logging.info("event: %s", event) if event.link_event is not None: - self.wireless_draw.hangle_link_event(event.link_event) + self.app.canvas.wireless_draw.hangle_link_event(event.link_event) - def log_throughput(self, event): + def handle_throughputs(self, event): interface_throughputs = event.interface_throughputs for i in interface_throughputs: print("") @@ -44,6 +42,30 @@ class CoreGrpc: throughputs_belong_to_session ) + def join_session(self, session_id): + # query session and set as current session + self.session_id = session_id + response = self.core.get_session(self.session_id) + logging.info("joining session(%s): %s", self.session_id, response) + self.core.events(self.session_id, self.app.core_grpc.handle_events) + + # determine next node id and reusable nodes + session = response.session + self.manager.reusable.clear() + self.manager.preexisting.clear() + max_id = 1 + for node in session.nodes: + if node.id > max_id: + max_id = node.id + self.manager.preexisting.add(node.id) + self.manager.id = max_id + for i in range(1, self.manager.id): + if i not in self.manager.preexisting: + self.manager.reusable.append(i) + + # draw session + self.app.canvas.draw_existing_component(session) + def create_new_session(self): """ Create a new session @@ -56,7 +78,7 @@ class CoreGrpc: # handle events session may broadcast self.session_id = response.session_id self.master.title("CORE Session ID " + str(self.session_id)) - self.core.events(self.session_id, self.log_event) + self.core.events(self.session_id, self.handle_events) # self.core.throughputs(self.log_throughput) def delete_session(self, custom_sid=None): @@ -114,6 +136,7 @@ class CoreGrpc: else: sid = custom_session_id + response = None if state == "configuration": response = self.core.set_session_state( sid, core_pb2.SessionState.CONFIGURATION @@ -229,15 +252,6 @@ class CoreGrpc: response = self.core.add_link(self.session_id, id1, id2, if1, if2) logging.info("created link: %s", response) - # self.core.get_node_links(self.session_id, id1) - - # def get_session(self): - # response = self.core.get_session(self.session_id) - # nodes = response.session.nodes - # for node in nodes: - # r = self.core.get_node_links(self.session_id, node.id) - # logging.info(r) - def launch_terminal(self, node_id): response = self.core.get_node_terminal(self.session_id, node_id) logging.info("get terminal %s", response.terminal) @@ -252,7 +266,7 @@ class CoreGrpc: """ response = self.core.save_xml(self.session_id, file_path) logging.info("coregrpc.py save xml %s", response) - self.core.events(self.session_id, self.log_event) + self.core.events(self.session_id, self.handle_events) def open_xml(self, file_path): """ diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index a02c7ae9..883f293a 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -4,18 +4,17 @@ CoreToolbar help to draw on canvas, and make grpc client call class CoreToolbarHelp: - def __init__(self, application): - self.application = application - self.core_grpc = application.core_grpc + def __init__(self, app): + self.app = app def add_nodes(self): """ add the nodes stored in grpc manager :return: nothing """ - grpc_manager = self.application.canvas.grpc_manager - for node in grpc_manager.nodes.values(): - self.application.core_grpc.add_node( + manager = self.app.core_grpc.manager + for node in manager.nodes.values(): + self.app.core_grpc.add_node( node.type, node.model, int(node.x), int(node.y), node.name, node.node_id ) @@ -24,8 +23,8 @@ class CoreToolbarHelp: add the edges stored in grpc manager :return: """ - grpc_manager = self.application.canvas.grpc_manager - for edge in grpc_manager.edges.values(): - self.application.core_grpc.add_link( + manager = self.app.core_grpc.manager + for edge in manager.edges.values(): + self.app.core_grpc.add_link( edge.id1, edge.id2, edge.type1, edge.type2, edge ) diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index 10165e2e..b2bd90f0 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -142,10 +142,7 @@ class SessionsDialog(Dialog): logging.error("querysessiondrawing.py invalid state") def join_session(self, session_id): - response = self.app.core_grpc.core.get_session(session_id) - self.app.core_grpc.session_id = session_id - self.app.core_grpc.core.events(session_id, self.app.core_grpc.log_event) - logging.info("entering session_id %s.... Result: %s", session_id, response) + self.app.core_grpc.join_session(session_id) self.destroy() def on_selected(self, event): diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 4107401b..e474934f 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -4,11 +4,11 @@ import tkinter as tk from core.api.grpc import core_pb2 from coretk.graph_helper import GraphHelper, WlanAntennaManager -from coretk.grpcmanagement import GrpcManager from coretk.images import Images from coretk.interface import Interface -from coretk.linkinfo import LinkInfo +from coretk.linkinfo import LinkInfo, Throughput from coretk.nodeconfigtable import NodeConfig +from coretk.wirelessconnection import WirelessConnection class GraphMode(enum.Enum): @@ -20,7 +20,7 @@ class GraphMode(enum.Enum): class CanvasGraph(tk.Canvas): - def __init__(self, master=None, grpc=None, cnf=None, **kwargs): + def __init__(self, master, core_grpc, cnf=None, **kwargs): if cnf is None: cnf = {} kwargs["highlightthickness"] = 0 @@ -33,20 +33,15 @@ class CanvasGraph(tk.Canvas): self.nodes = {} self.edges = {} self.drawing_edge = None - self.grid = None self.meters_per_pixel = 1.5 self.setup_menus() self.setup_bindings() self.draw_grid() - - self.core_grpc = grpc - self.grpc_manager = GrpcManager(grpc) - - self.helper = GraphHelper(self, grpc) - # self.core_id_to_canvas_id = {} - # self.core_map = CoreToCanvasMapping() - # self.draw_existing_component() + self.core_grpc = core_grpc + self.helper = GraphHelper(self, core_grpc) + self.throughput_draw = Throughput(self, core_grpc) + self.wireless_draw = WirelessConnection(self, core_grpc) def setup_menus(self): self.node_context = tk.Menu(self.master) @@ -56,7 +51,9 @@ class CanvasGraph(tk.Canvas): def canvas_reset_and_redraw(self, new_grpc): """ - Reset the private variables CanvasGraph object, redraw nodes given the new grpc client + Reset the private variables CanvasGraph object, redraw nodes given the new grpc + client. + :param new_grpc: :return: """ @@ -73,15 +70,11 @@ class CanvasGraph(tk.Canvas): self.nodes = {} self.edges = {} self.drawing_edge = None - - print("graph.py create a new grpc manager") - self.grpc_manager = GrpcManager(new_grpc) - # new grpc self.core_grpc = new_grpc print("grpah.py draw existing component") self.draw_existing_component() - print(self.grpc_manager.edges) + print(self.core_grpc.manager.edges) def setup_bindings(self): """ @@ -119,16 +112,13 @@ class CanvasGraph(tk.Canvas): for i in range(0, height, 27): self.create_line(0, i, width, i, dash=(2, 4), tags="gridline") - def draw_existing_component(self): + def draw_existing_component(self, session): """ Draw existing node and update the information in grpc manager to match :return: nothing """ core_id_to_canvas_id = {} - - session_id = self.core_grpc.session_id - session = self.core_grpc.core.get_session(session_id).session # redraw existing nodes for node in session.nodes: # peer to peer node is not drawn on the GUI @@ -145,9 +135,7 @@ class CanvasGraph(tk.Canvas): core_id_to_canvas_id[node.id] = n.id # store the node in grpc manager - self.grpc_manager.add_preexisting_node(n, session_id, node, name) - - self.grpc_manager.update_reusable_id() + self.core_grpc.manager.add_preexisting_node(n, session.id, node, name) # draw existing links for link in session.links: @@ -179,7 +167,7 @@ class CanvasGraph(tk.Canvas): n1.edges.add(e) n2.edges.add(e) self.edges[e.token] = e - self.grpc_manager.add_edge(session_id, e.token, n1.id, n2.id) + self.core_grpc.manager.add_edge(session.id, e.token, n1.id, n2.id) self.helper.redraw_antenna(link, n1, n2) @@ -208,12 +196,12 @@ class CanvasGraph(tk.Canvas): # TODO will include throughput and ipv6 in the future if1 = Interface(grpc_if1.name, grpc_if1.ip4, ifid=grpc_if1.id) if2 = Interface(grpc_if2.name, grpc_if2.ip4, ifid=grpc_if2.id) - self.grpc_manager.edges[e.token].interface_1 = if1 - self.grpc_manager.edges[e.token].interface_2 = if2 - self.grpc_manager.nodes[ + self.core_grpc.manager.edges[e.token].interface_1 = if1 + self.core_grpc.manager.edges[e.token].interface_2 = if2 + self.core_grpc.manager.nodes[ core_id_to_canvas_id[link.node_one_id] ].interfaces.append(if1) - self.grpc_manager.nodes[ + self.core_grpc.manager.nodes[ core_id_to_canvas_id[link.node_two_id] ].interfaces.append(if2) @@ -316,13 +304,13 @@ class CanvasGraph(tk.Canvas): node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) - self.grpc_manager.add_edge( + self.core_grpc.manager.add_edge( self.core_grpc.session_id, edge.token, node_src.id, node_dst.id ) # draw link info on the edge - if1 = self.grpc_manager.edges[edge.token].interface_1 - if2 = self.grpc_manager.edges[edge.token].interface_2 + if1 = self.core_grpc.manager.edges[edge.token].interface_1 + if2 = self.core_grpc.manager.edges[edge.token].interface_2 ip4_and_prefix_1 = None ip4_and_prefix_2 = None if if1 is not None: @@ -382,10 +370,10 @@ class CanvasGraph(tk.Canvas): image=image, node_type=node_name, canvas=self, - core_id=self.grpc_manager.peek_id(), + core_id=self.core_grpc.manager.peek_id(), ) self.nodes[node.id] = node - self.grpc_manager.add_node( + self.core_grpc.manager.add_node( self.core_grpc.session_id, node.id, x, y, node_name ) return node @@ -477,7 +465,7 @@ class CanvasNode: self.moving = None def double_click(self, event): - node_id = self.canvas.grpc_manager.nodes[self.id].node_id + node_id = self.canvas.core_grpc.manager.nodes[self.id].node_id state = self.canvas.core_grpc.get_session_state() if state == core_pb2.SessionState.RUNTIME: self.canvas.core_grpc.launch_terminal(node_id) @@ -496,7 +484,7 @@ class CanvasNode: def click_release(self, event): logging.debug(f"click release {self.name}: {event}") self.update_coords() - self.canvas.grpc_manager.update_node_location( + self.canvas.core_grpc.manager.update_node_location( self.id, self.x_coord, self.y_coord ) self.moving = None diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index 9638c7c5..5f10433b 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -57,17 +57,14 @@ class GrpcManager: def __init__(self, grpc): self.nodes = {} self.edges = {} - self.id = None + self.id = 1 # A list of id for re-use, keep in increasing order self.reusable = [] - - self.preexisting = [] - self.core_grpc = None - + self.preexisting = set() + self.core_grpc = grpc # self.update_preexisting_ids() # self.core_id_to_canvas_id = {} self.interfaces_manager = InterfaceManager() - # map tuple(core_node_id, interface_id) to and edge # self.node_id_and_interface_to_edge_token = {} self.core_mapping = CoreToCanvasMapping() @@ -172,7 +169,7 @@ class GrpcManager: print(core_id) if self.id is None or core_id >= self.id: self.id = core_id + 1 - self.preexisting.append(core_id) + self.preexisting.add(core_id) n = Node( session_id, core_id, @@ -219,8 +216,8 @@ class GrpcManager: """ try: self.nodes.pop(canvas_id) - self.reuseable.append(canvas_id) - self.reuseable.sort() + self.reusable.append(canvas_id) + self.reusable.sort() except KeyError: logging.error("grpcmanagement.py INVALID NODE CANVAS ID") diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 6a817b6f..241a6997 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -13,6 +13,13 @@ ICONS_DIR = os.path.join(PATH, "icons") class Images: images = {} + @classmethod + def load_all(cls): + for file_name in os.listdir(ICONS_DIR): + file_path = os.path.join(ICONS_DIR, file_name) + name = file_name.split(".")[0] + cls.load(name, file_path) + @classmethod def load(cls, name, file_path): # file_path = os.path.join(PATH, file_path) @@ -91,10 +98,3 @@ class ImageEnum(Enum): FILEOPEN = "fileopen" EDITDELETE = "edit-delete" ANTENNA = "antenna" - - -def load_core_images(images): - for file_name in os.listdir(ICONS_DIR): - file_path = os.path.join(ICONS_DIR, file_name) - name = file_name.split(".")[0] - images.load(name, file_path) diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index c21a56b0..17bc99c8 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -11,7 +11,7 @@ class LinkInfo: def __init__(self, canvas, edge, ip4_src, ip6_src, ip4_dst, ip6_dst): """ create an instance of LinkInfo object - :param tkinter.Canvas canvas: canvas object + :param coretk.graph.Graph canvas: canvas object :param coretk.graph.CanvasEdge edge: canvas edge onject :param ip4_src: :param ip6_src: @@ -104,29 +104,25 @@ class LinkInfo: class Throughput: - def __init__(self, canvas, grpc): + def __init__(self, canvas, core_grpc): """ create an instance of Throughput object - :param tkinter.Canvas canvas: canvas object - :param coretk.coregrpc,CoreGrpc grpc: grpc object + :param coretk.app.Application app: application """ self.canvas = canvas - self.core_grpc = grpc - self.grpc_manager = canvas.grpc_manager - + self.core_grpc = core_grpc # edge canvas id mapped to throughput value self.tracker = {} - # map an edge canvas id to a throughput canvas id self.map = {} - self.edge_id_to_token = {} def load_throughput_info(self, interface_throughputs): """ load all interface throughouts from an event - :param repeated core_bp2.InterfaceThroughputinterface_throughputs: interface throughputs + :param repeated core_bp2.InterfaceThroughputinterface_throughputs: interface + throughputs :return: nothing """ for t in interface_throughputs: @@ -134,7 +130,7 @@ class Throughput: iid = t.interface_id tp = t.throughput # token = self.grpc_manager.node_id_and_interface_to_edge_token[nid, iid] - token = self.grpc_manager.core_mapping.get_token_from_node_and_interface( + token = self.core_grpc.manager.core_mapping.get_token_from_node_and_interface( nid, iid ) print(token) diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py index 63042e19..3e4a98f8 100644 --- a/coretk/coretk/wirelessconnection.py +++ b/coretk/coretk/wirelessconnection.py @@ -8,8 +8,7 @@ class WirelessConnection: def __init__(self, canvas, grpc): self.canvas = canvas self.core_grpc = grpc - self.core_mapping = canvas.grpc_manager.core_mapping - + self.core_mapping = grpc.manager.core_mapping # map a (node_one_id, node_two_id) to a wlan canvas id self.map = {} From 1f230146a67df157298088f64f12f89c94dc793d Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 Nov 2019 13:15:45 -0700 Subject: [PATCH 152/462] merged grpcmanager with coregrpc --- coretk/coretk/coregrpc.py | 426 ++++++++++++++++++++++------ coretk/coretk/coretocanvas.py | 11 - coretk/coretk/coretoolbarhelp.py | 53 +--- coretk/coretk/graph.py | 28 +- coretk/coretk/grpcmanagement.py | 328 --------------------- coretk/coretk/linkinfo.py | 2 +- coretk/coretk/wirelessconnection.py | 6 +- 7 files changed, 368 insertions(+), 486 deletions(-) delete mode 100644 coretk/coretk/grpcmanagement.py diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 029a2d16..94f5ca7b 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -6,8 +6,54 @@ import os from collections import OrderedDict from core.api.grpc import client, core_pb2 +from coretk.coretocanvas import CoreToCanvasMapping from coretk.dialogs.sessions import SessionsDialog -from coretk.grpcmanagement import GrpcManager +from coretk.interface import Interface, InterfaceManager +from coretk.wlannodeconfig import WlanNodeConfig + +link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] +network_layer_nodes = ["router", "host", "PC", "mdr", "prouter", "OVS"] + + +class Node: + def __init__(self, session_id, node_id, node_type, model, x, y, name): + """ + Create an instance of a node + + :param int session_id: session id + :param int node_id: node id + :param core_pb2.NodeType node_type: node type + :param int x: x coordinate + :param int y: coordinate + :param str name: node name + """ + self.session_id = session_id + self.node_id = node_id + self.type = node_type + self.x = x + self.y = y + self.model = model + self.name = name + self.interfaces = [] + + +class Edge: + def __init__(self, session_id, node_id_1, node_type_1, node_id_2, node_type_2): + """ + Create an instance of an edge + :param int session_id: session id + :param int node_id_1: node 1 id + :param int node_type_1: node 1 type + :param core_pb2.NodeType node_id_2: node 2 id + :param core_pb2.NodeType node_type_2: node 2 type + """ + self.session_id = session_id + self.id1 = node_id_1 + self.id2 = node_id_2 + self.type1 = node_type_1 + self.type2 = node_type_2 + self.interface_1 = None + self.interface_2 = None class CoreGrpc: @@ -21,7 +67,16 @@ class CoreGrpc: self.app = app self.master = app.master self.interface_helper = None - self.manager = GrpcManager(self) + + # data for managing the current session + self.nodes = {} + self.edges = {} + self.id = 1 + self.reusable = [] + self.preexisting = set() + self.interfaces_manager = InterfaceManager() + self.core_mapping = CoreToCanvasMapping() + self.wlanconfig_management = WlanNodeConfig() def handle_events(self, event): logging.info("event: %s", event) @@ -37,7 +92,6 @@ class CoreGrpc: for if_tp in interface_throughputs: if if_tp.node_id in self.node_ids: throughputs_belong_to_session.append(if_tp) - # bridge_throughputs = event.bridge_throughputs self.throughput_draw.process_grpc_throughput_event( throughputs_belong_to_session ) @@ -47,21 +101,24 @@ class CoreGrpc: self.session_id = session_id response = self.core.get_session(self.session_id) logging.info("joining session(%s): %s", self.session_id, response) - self.core.events(self.session_id, self.app.core_grpc.handle_events) + self.core.events(self.session_id, self.handle_events) + + # set title to session + self.master.title(f"CORE Session({self.session_id})") # determine next node id and reusable nodes session = response.session - self.manager.reusable.clear() - self.manager.preexisting.clear() + self.reusable.clear() + self.preexisting.clear() max_id = 1 for node in session.nodes: if node.id > max_id: max_id = node.id - self.manager.preexisting.add(node.id) - self.manager.id = max_id - for i in range(1, self.manager.id): - if i not in self.manager.preexisting: - self.manager.reusable.append(i) + self.preexisting.add(node.id) + self.id = max_id + for i in range(1, self.id): + if i not in self.preexisting: + self.reusable.append(i) # draw session self.app.canvas.canvas_reset_and_redraw(session) @@ -74,12 +131,7 @@ class CoreGrpc: """ response = self.core.create_session() logging.info("created session: %s", response) - - # handle events session may broadcast - self.session_id = response.session_id - self.master.title("CORE Session ID " + str(self.session_id)) - self.core.events(self.session_id, self.handle_events) - # self.core.throughputs(self.log_throughput) + self.join_session(response.session_id) def delete_session(self, custom_sid=None): if custom_sid is None: @@ -164,28 +216,10 @@ class CoreGrpc: logging.info("set session state: %s", response) - def add_node(self, node_type, model, x, y, name, node_id): - position = core_pb2.Position(x=x, y=y) - node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) - self.node_ids.append(node_id) - response = self.core.add_node(self.session_id, node) - logging.info("created node: %s", response) - if node_type == core_pb2.NodeType.WIRELESS_LAN: - d = OrderedDict() - d["basic_range"] = "275" - d["bandwidth"] = "54000000" - d["jitter"] = "0" - d["delay"] = "20000" - d["error"] = "0" - r = self.core.set_wlan_config(self.session_id, node_id, d) - logging.debug("set wlan config %s", r) - return response.node_id - def edit_node(self, node_id, x, y): position = core_pb2.Position(x=x, y=y) response = self.core.edit_node(self.session_id, node_id, position) logging.info("updated node id %s: %s", node_id, response) - # self.core.events(self.session_id, self.log_event) def delete_nodes(self, delete_session=None): if delete_session is None: @@ -213,29 +247,6 @@ class CoreGrpc: ) logging.info("delete links %s", response) - def create_interface(self, node_type, gui_interface): - """ - create a protobuf interface given the interface object stored by the programmer - - :param core_bp2.NodeType type: node type - :param coretk.interface.Interface gui_interface: the programmer's interface object - :rtype: core_bp2.Interface - :return: protobuf interface object - """ - if node_type != core_pb2.NodeType.DEFAULT: - return None - else: - interface = core_pb2.Interface( - id=gui_interface.id, - name=gui_interface.name, - mac=gui_interface.mac, - ip4=gui_interface.ipv4, - ip4mask=gui_interface.ip4prefix, - ) - logging.debug("create interface 1 %s", interface) - - return interface - # TODO add location, hooks, emane_config, etc... def start_session( self, @@ -274,29 +285,6 @@ class CoreGrpc: """ if1 = self.create_interface(type1, edge.interface_1) if2 = self.create_interface(type2, edge.interface_2) - # if type1 == core_pb2.NodeType.DEFAULT: - # interface = edge.interface_1 - # if1 = core_pb2.Interface( - # id=interface.id, - # name=interface.name, - # mac=interface.mac, - # ip4=interface.ipv4, - # ip4mask=interface.ip4prefix, - # ) - # logging.debug("create interface 1 %s", if1) - # # interface1 = self.interface_helper.create_interface(id1, 0) - # - # if type2 == core_pb2.NodeType.DEFAULT: - # interface = edge.interface_2 - # if2 = core_pb2.Interface( - # id=interface.id, - # name=interface.name, - # mac=interface.mac, - # ip4=interface.ipv4, - # ip4mask=interface.ip4prefix, - # ) - # logging.debug("create interface 2: %s", if2) - response = self.core.add_link(self.session_id, id1, id2, if1, if2) logging.info("created link: %s", response) @@ -335,3 +323,279 @@ class CoreGrpc: """ logging.debug("Close grpc") self.core.close() + + def peek_id(self): + """ + Peek the next id to be used + + :return: nothing + """ + if len(self.reusable) == 0: + return self.id + else: + return self.reusable[0] + + def get_id(self): + """ + Get the next node id as well as update id status and reusable ids + + :rtype: int + :return: the next id to be used + """ + if len(self.reusable) == 0: + new_id = self.id + self.id = self.id + 1 + return new_id + else: + return self.reusable.pop(0) + + def add_node(self, node_type, model, x, y, name, node_id): + position = core_pb2.Position(x=x, y=y) + node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) + self.node_ids.append(node_id) + response = self.core.add_node(self.session_id, node) + logging.info("created node: %s", response) + if node_type == core_pb2.NodeType.WIRELESS_LAN: + d = OrderedDict() + d["basic_range"] = "275" + d["bandwidth"] = "54000000" + d["jitter"] = "0" + d["delay"] = "20000" + d["error"] = "0" + r = self.core.set_wlan_config(self.session_id, node_id, d) + logging.debug("set wlan config %s", r) + return response.node_id + + def add_graph_node(self, session_id, canvas_id, x, y, name): + """ + Add node, with information filled in, to grpc manager + + :param int session_id: session id + :param int canvas_id: node's canvas id + :param int x: x coord + :param int y: y coord + :param str name: node type + :return: nothing + """ + node_type = None + node_model = None + if name in link_layer_nodes: + if name == "switch": + node_type = core_pb2.NodeType.SWITCH + elif name == "hub": + node_type = core_pb2.NodeType.HUB + elif name == "wlan": + node_type = core_pb2.NodeType.WIRELESS_LAN + elif name == "rj45": + node_type = core_pb2.NodeType.RJ45 + elif name == "tunnel": + node_type = core_pb2.NodeType.TUNNEL + elif name in network_layer_nodes: + node_type = core_pb2.NodeType.DEFAULT + node_model = name + else: + logging.error("grpcmanagemeny.py INVALID node name") + nid = self.get_id() + create_node = Node(session_id, nid, node_type, node_model, x, y, name) + + # set default configuration for wireless node + self.wlanconfig_management.set_default_config(node_type, nid) + + self.nodes[canvas_id] = create_node + self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) + # self.core_id_to_canvas_id[nid] = canvas_id + logging.debug( + "Adding node to GrpcManager.. Session id: %s, Coords: (%s, %s), Name: %s", + session_id, + x, + y, + name, + ) + + def add_preexisting_node(self, canvas_node, session_id, core_node, name): + """ + Add preexisting nodes to grpc manager + + :param str name: node_type + :param core_pb2.Node core_node: core node grpc message + :param coretk.graph.CanvasNode canvas_node: canvas node + :param int session_id: session id + :return: nothing + """ + + # update the next available id + core_id = core_node.id + if self.id is None or core_id >= self.id: + self.id = core_id + 1 + self.preexisting.add(core_id) + n = Node( + session_id, + core_id, + core_node.type, + core_node.model, + canvas_node.x_coord, + canvas_node.y_coord, + name, + ) + self.nodes[canvas_node.id] = n + + def update_node_location(self, canvas_id, new_x, new_y): + """ + update node + + :param int canvas_id: canvas id of that node + :param int new_x: new x coord + :param int new_y: new y coord + :return: nothing + """ + self.nodes[canvas_id].x = new_x + self.nodes[canvas_id].y = new_y + + def update_reusable_id(self): + """ + Update available id for reuse + + :return: nothing + """ + if len(self.preexisting) > 0: + for i in range(1, self.id): + if i not in self.preexisting: + self.reusable.append(i) + + self.preexisting.clear() + logging.debug("Next id: %s, Reusable: %s", self.id, self.reusable) + + def delete_node(self, canvas_id): + """ + Delete a node from the session + + :param int canvas_id: node's id in the canvas + :return: thing + """ + try: + self.nodes.pop(canvas_id) + self.reusable.append(canvas_id) + self.reusable.sort() + except KeyError: + logging.error("grpcmanagement.py INVALID NODE CANVAS ID") + + def create_interface(self, node_type, gui_interface): + """ + create a protobuf interface given the interface object stored by the programmer + + :param core_bp2.NodeType type: node type + :param coretk.interface.Interface gui_interface: the programmer's interface object + :rtype: core_bp2.Interface + :return: protobuf interface object + """ + if node_type != core_pb2.NodeType.DEFAULT: + return None + else: + interface = core_pb2.Interface( + id=gui_interface.id, + name=gui_interface.name, + mac=gui_interface.mac, + ip4=gui_interface.ipv4, + ip4mask=gui_interface.ip4prefix, + ) + logging.debug("create interface: %s", interface) + return interface + + def create_edge_interface(self, edge, src_canvas_id, dst_canvas_id): + """ + Create the interface for the two end of an edge, add a copy to node's interfaces + + :param coretk.grpcmanagement.Edge edge: edge to add interfaces to + :param int src_canvas_id: canvas id for the source node + :param int dst_canvas_id: canvas id for the destination node + :return: nothing + """ + src_interface = None + dst_interface = None + print("create interface") + self.interfaces_manager.new_subnet() + + src_node = self.nodes[src_canvas_id] + if src_node.model in network_layer_nodes: + ifid = len(src_node.interfaces) + name = "eth" + str(ifid) + src_interface = Interface( + name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) + ) + self.nodes[src_canvas_id].interfaces.append(src_interface) + logging.debug( + "Create source interface 1... IP: %s, name: %s", + src_interface.ipv4, + src_interface.name, + ) + + dst_node = self.nodes[dst_canvas_id] + if dst_node.model in network_layer_nodes: + ifid = len(dst_node.interfaces) + name = "eth" + str(ifid) + dst_interface = Interface( + name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) + ) + self.nodes[dst_canvas_id].interfaces.append(dst_interface) + logging.debug( + "Create destination interface... IP: %s, name: %s", + dst_interface.ipv4, + dst_interface.name, + ) + + edge.interface_1 = src_interface + edge.interface_2 = dst_interface + return src_interface, dst_interface + + def add_edge(self, session_id, token, canvas_id_1, canvas_id_2): + """ + Add an edge to grpc manager + + :param int session_id: core session id + :param tuple(int, int) token: edge's identification in the canvas + :param int canvas_id_1: canvas id of source node + :param int canvas_id_2: canvas_id of destination node + + :return: nothing + """ + if canvas_id_1 in self.nodes and canvas_id_2 in self.nodes: + edge = Edge( + session_id, + self.nodes[canvas_id_1].node_id, + self.nodes[canvas_id_1].type, + self.nodes[canvas_id_2].node_id, + self.nodes[canvas_id_2].type, + ) + self.edges[token] = edge + src_interface, dst_interface = self.create_edge_interface( + edge, canvas_id_1, canvas_id_2 + ) + node_one_id = self.nodes[canvas_id_1].node_id + node_two_id = self.nodes[canvas_id_2].node_id + + # provide a way to get an edge from a core node and an interface id + if src_interface is not None: + self.core_mapping.map_node_and_interface_to_canvas_edge( + node_one_id, src_interface.id, token + ) + logging.debug( + "map node id %s, interface_id %s to edge token %s", + node_one_id, + src_interface.id, + token, + ) + + if dst_interface is not None: + self.core_mapping.map_node_and_interface_to_canvas_edge( + node_two_id, dst_interface.id, token + ) + logging.debug( + "map node id %s, interface_id %s to edge token %s", + node_two_id, + dst_interface.id, + token, + ) + + logging.debug("Adding edge to grpc manager...") + else: + logging.error("grpcmanagement.py INVALID CANVAS NODE ID") diff --git a/coretk/coretk/coretocanvas.py b/coretk/coretk/coretocanvas.py index e808735f..6e767db3 100644 --- a/coretk/coretk/coretocanvas.py +++ b/coretk/coretk/coretocanvas.py @@ -8,7 +8,6 @@ class CoreToCanvasMapping: def __init__(self): self.core_id_to_canvas_id = {} self.core_node_and_interface_to_canvas_edge = {} - # self.edge_id_to_canvas_token = {} def map_node_and_interface_to_canvas_edge(self, nid, iid, edge_token): self.core_node_and_interface_to_canvas_edge[tuple([nid, iid])] = edge_token @@ -33,13 +32,3 @@ class CoreToCanvasMapping: else: logging.debug("invalid key") return None - - # def add_mapping(self, core_id, canvas_id): - # if core_id not in self.core_id_to_canvas_id: - # self.core_id_to_canvas_id[core_id] = canvas_id - # else: - # logging.error("key already mapped") - # - # def delete_mapping(self, core_id): - # result = self.core_id_to_canvas_id.pop(core_id, None) - # return result diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index 3a0b8623..b26269db 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -15,8 +15,8 @@ class CoreToolbarHelp: :return: nothing """ nodes = [] - manager = self.app.core_grpc.manager - for node in manager.nodes.values(): + core = self.app.core_grpc + for node in core.nodes.values(): pos = core_pb2.Position(x=int(node.x), y=int(node.y)) n = core_pb2.Node( id=node.node_id, type=node.type, position=pos, model=node.model @@ -32,8 +32,8 @@ class CoreToolbarHelp: :return: list of protobuf links """ links = [] - manager = self.app.core_grpc.manager - for edge in manager.edges.values(): + core = self.app.core_grpc + for edge in core.edges.values(): interface_one = self.app.core_grpc.create_interface( edge.type1, edge.interface_1 ) @@ -56,61 +56,20 @@ class CoreToolbarHelp: interface_two=interface_two, ) links.append(link) - # self.id1 = edge.id1 - # self.id2 = edge.id2 - # self.type = link_type - # self.if1 = interface_one - # self.if2 = interface_two return links def get_wlan_configuration_list(self): configs = [] - manager = self.app.core_grpc.manager - manager_configs = manager.wlanconfig_management.configurations + core = self.app.core_grpc + manager_configs = core.wlanconfig_management.configurations for key in manager_configs: cnf = core_pb2.WlanConfig(node_id=key, config=manager_configs[key]) configs.append(cnf) return configs def gui_start_session(self): - # list(core_pb2.Node) nodes = self.get_node_list() - - # list(core_bp2.Link) links = self.get_link_list() - - # print(links[0]) wlan_configs = self.get_wlan_configuration_list() - # print(wlan_configs) self.app.core_grpc.start_session(nodes, links, wlan_configs=wlan_configs) - # self.core_grpc.core.add_link(self.core_grpc.session_id, self.id1, self.id2, self.if1, self.if2) - # res = self.core_grpc.core.get_wlan_config(self.core_grpc.session_id, 1) - - # res = self.core_grpc.core.get_session(self.core_grpc.session_id).session - # print(res) - # res = self.core_grpc.core.get_wlan_config(self.core_grpc.session_id, 1) - - # print(res) - - # def add_nodes(self): - # """ - # add the nodes stored in grpc manager - # :return: nothing - # """ - # grpc_manager = self.application.canvas.grpc_manager - # for node in grpc_manager.nodes.values(): - # self.application.core_grpc.add_node( - # node.type, node.model, int(node.x), int(node.y), node.name, node.node_id - # ) - # - # def add_edges(self): - # """ - # add the edges stored in grpc manager - # :return: - # """ - # grpc_manager = self.application.canvas.grpc_manager - # for edge in grpc_manager.edges.values(): - # self.application.core_grpc.add_link( - # edge.id1, edge.id2, edge.type1, edge.type2, edge - # ) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 5b76cdcb..7859a8e6 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -147,7 +147,7 @@ class CanvasGraph(tk.Canvas): core_id_to_canvas_id[node.id] = n.id # store the node in grpc manager - self.core_grpc.manager.add_preexisting_node(n, session.id, node, name) + self.core_grpc.add_preexisting_node(n, session.id, node, name) # draw existing links for link in session.links: @@ -179,7 +179,7 @@ class CanvasGraph(tk.Canvas): n1.edges.add(e) n2.edges.add(e) self.edges[e.token] = e - self.core_grpc.manager.add_edge(session.id, e.token, n1.id, n2.id) + self.core_grpc.add_edge(session.id, e.token, n1.id, n2.id) self.helper.redraw_antenna(link, n1, n2) @@ -208,12 +208,12 @@ class CanvasGraph(tk.Canvas): # TODO will include throughput and ipv6 in the future if1 = Interface(grpc_if1.name, grpc_if1.ip4, ifid=grpc_if1.id) if2 = Interface(grpc_if2.name, grpc_if2.ip4, ifid=grpc_if2.id) - self.core_grpc.manager.edges[e.token].interface_1 = if1 - self.core_grpc.manager.edges[e.token].interface_2 = if2 - self.core_grpc.manager.nodes[ + self.core_grpc.edges[e.token].interface_1 = if1 + self.core_grpc.edges[e.token].interface_2 = if2 + self.core_grpc.nodes[ core_id_to_canvas_id[link.node_one_id] ].interfaces.append(if1) - self.core_grpc.manager.nodes[ + self.core_grpc.nodes[ core_id_to_canvas_id[link.node_two_id] ].interfaces.append(if2) @@ -318,13 +318,13 @@ class CanvasGraph(tk.Canvas): node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) - self.core_grpc.manager.add_edge( + self.core_grpc.add_edge( self.core_grpc.session_id, edge.token, node_src.id, node_dst.id ) # draw link info on the edge - if1 = self.core_grpc.manager.edges[edge.token].interface_1 - if2 = self.core_grpc.manager.edges[edge.token].interface_2 + if1 = self.core_grpc.edges[edge.token].interface_1 + if2 = self.core_grpc.edges[edge.token].interface_2 ip4_and_prefix_1 = None ip4_and_prefix_2 = None if if1 is not None: @@ -390,10 +390,10 @@ class CanvasGraph(tk.Canvas): image=image, node_type=node_name, canvas=self, - core_id=self.core_grpc.manager.peek_id(), + core_id=self.core_grpc.peek_id(), ) self.nodes[node.id] = node - self.core_grpc.manager.add_node( + self.core_grpc.add_graph_node( self.core_grpc.session_id, node.id, x, y, node_name ) return node @@ -485,7 +485,7 @@ class CanvasNode: self.moving = None def double_click(self, event): - node_id = self.canvas.core_grpc.manager.nodes[self.id].node_id + node_id = self.canvas.core_grpc.nodes[self.id].node_id state = self.canvas.core_grpc.get_session_state() if state == core_pb2.SessionState.RUNTIME: self.canvas.core_grpc.launch_terminal(node_id) @@ -511,9 +511,7 @@ class CanvasNode: def click_release(self, event): logging.debug(f"click release {self.name}: {event}") self.update_coords() - self.canvas.core_grpc.manager.update_node_location( - self.id, self.x_coord, self.y_coord - ) + self.canvas.core_grpc.update_node_location(self.id, self.x_coord, self.y_coord) self.moving = None def motion(self, event): diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py deleted file mode 100644 index 85d8c8f5..00000000 --- a/coretk/coretk/grpcmanagement.py +++ /dev/null @@ -1,328 +0,0 @@ -""" -Manage useful informations about the nodes, edges and configuration -that can be useful for grpc, acts like a session class -""" -import logging - -from core.api.grpc import core_pb2 -from coretk.coretocanvas import CoreToCanvasMapping -from coretk.interface import Interface, InterfaceManager -from coretk.wlannodeconfig import WlanNodeConfig - -link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] -network_layer_nodes = ["router", "host", "PC", "mdr", "prouter", "OVS"] - - -class Node: - def __init__(self, session_id, node_id, node_type, model, x, y, name): - """ - Create an instance of a node - - :param int session_id: session id - :param int node_id: node id - :param core_pb2.NodeType node_type: node type - :param int x: x coordinate - :param int y: coordinate - :param str name: node name - """ - self.session_id = session_id - self.node_id = node_id - self.type = node_type - self.x = x - self.y = y - self.model = model - self.name = name - self.interfaces = [] - - -class Edge: - def __init__(self, session_id, node_id_1, node_type_1, node_id_2, node_type_2): - """ - Create an instance of an edge - :param int session_id: session id - :param int node_id_1: node 1 id - :param int node_type_1: node 1 type - :param core_pb2.NodeType node_id_2: node 2 id - :param core_pb2.NodeType node_type_2: node 2 type - """ - self.session_id = session_id - self.id1 = node_id_1 - self.id2 = node_id_2 - self.type1 = node_type_1 - self.type2 = node_type_2 - self.interface_1 = None - self.interface_2 = None - - -class GrpcManager: - def __init__(self, grpc): - self.nodes = {} - self.edges = {} - self.id = 1 - # A list of id for re-use, keep in increasing order - self.reusable = [] - self.preexisting = set() - self.core_grpc = grpc - # self.update_preexisting_ids() - # self.core_id_to_canvas_id = {} - self.interfaces_manager = InterfaceManager() - # map tuple(core_node_id, interface_id) to and edge - # self.node_id_and_interface_to_edge_token = {} - self.core_mapping = CoreToCanvasMapping() - self.wlanconfig_management = WlanNodeConfig() - - def update_preexisting_ids(self): - """ - get preexisting node ids - :return: - """ - max_id = 0 - client = self.core_grpc.core - sessions = client.get_sessions().sessions - for session_summary in sessions: - session = client.get_session(session_summary.id).session - for node in session.nodes: - if node.id > max_id: - max_id = node.id - self.preexisting.append(node.id) - self.id = max_id + 1 - self.update_reusable_id() - - def peek_id(self): - """ - Peek the next id to be used - - :return: nothing - """ - if len(self.reusable) == 0: - return self.id - else: - return self.reusable[0] - - def get_id(self): - """ - Get the next node id as well as update id status and reusable ids - - :rtype: int - :return: the next id to be used - """ - if len(self.reusable) == 0: - new_id = self.id - self.id = self.id + 1 - return new_id - else: - return self.reusable.pop(0) - - def add_node(self, session_id, canvas_id, x, y, name): - """ - Add node, with information filled in, to grpc manager - - :param int session_id: session id - :param int canvas_id: node's canvas id - :param int x: x coord - :param int y: y coord - :param str name: node type - :return: nothing - """ - node_type = None - node_model = None - if name in link_layer_nodes: - if name == "switch": - node_type = core_pb2.NodeType.SWITCH - elif name == "hub": - node_type = core_pb2.NodeType.HUB - elif name == "wlan": - node_type = core_pb2.NodeType.WIRELESS_LAN - elif name == "rj45": - node_type = core_pb2.NodeType.RJ45 - elif name == "tunnel": - node_type = core_pb2.NodeType.TUNNEL - elif name in network_layer_nodes: - node_type = core_pb2.NodeType.DEFAULT - node_model = name - else: - logging.error("grpcmanagemeny.py INVALID node name") - nid = self.get_id() - create_node = Node(session_id, nid, node_type, node_model, x, y, name) - - # set default configuration for wireless node - self.wlanconfig_management.set_default_config(node_type, nid) - - self.nodes[canvas_id] = create_node - self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) - # self.core_id_to_canvas_id[nid] = canvas_id - logging.debug( - "Adding node to GrpcManager.. Session id: %s, Coords: (%s, %s), Name: %s", - session_id, - x, - y, - name, - ) - - def add_preexisting_node(self, canvas_node, session_id, core_node, name): - """ - Add preexisting nodes to grpc manager - - :param str name: node_type - :param core_pb2.Node core_node: core node grpc message - :param coretk.graph.CanvasNode canvas_node: canvas node - :param int session_id: session id - :return: nothing - """ - - # update the next available id - core_id = core_node.id - if self.id is None or core_id >= self.id: - self.id = core_id + 1 - self.preexisting.add(core_id) - n = Node( - session_id, - core_id, - core_node.type, - core_node.model, - canvas_node.x_coord, - canvas_node.y_coord, - name, - ) - self.nodes[canvas_node.id] = n - - def update_node_location(self, canvas_id, new_x, new_y): - """ - update node - - :param int canvas_id: canvas id of that node - :param int new_x: new x coord - :param int new_y: new y coord - :return: nothing - """ - self.nodes[canvas_id].x = new_x - self.nodes[canvas_id].y = new_y - - def update_reusable_id(self): - """ - Update available id for reuse - - :return: nothing - """ - if len(self.preexisting) > 0: - for i in range(1, self.id): - if i not in self.preexisting: - self.reusable.append(i) - - self.preexisting.clear() - logging.debug("Next id: %s, Reusable: %s", self.id, self.reusable) - - def delete_node(self, canvas_id): - """ - Delete a node from the session - - :param int canvas_id: node's id in the canvas - :return: thing - """ - try: - self.nodes.pop(canvas_id) - self.reusable.append(canvas_id) - self.reusable.sort() - except KeyError: - logging.error("grpcmanagement.py INVALID NODE CANVAS ID") - - def create_interface(self, edge, src_canvas_id, dst_canvas_id): - """ - Create the interface for the two end of an edge, add a copy to node's interfaces - - :param coretk.grpcmanagement.Edge edge: edge to add interfaces to - :param int src_canvas_id: canvas id for the source node - :param int dst_canvas_id: canvas id for the destination node - :return: nothing - """ - src_interface = None - dst_interface = None - print("create interface") - self.interfaces_manager.new_subnet() - - src_node = self.nodes[src_canvas_id] - if src_node.model in network_layer_nodes: - ifid = len(src_node.interfaces) - name = "eth" + str(ifid) - src_interface = Interface( - name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) - ) - self.nodes[src_canvas_id].interfaces.append(src_interface) - logging.debug( - "Create source interface 1... IP: %s, name: %s", - src_interface.ipv4, - src_interface.name, - ) - - dst_node = self.nodes[dst_canvas_id] - if dst_node.model in network_layer_nodes: - ifid = len(dst_node.interfaces) - name = "eth" + str(ifid) - dst_interface = Interface( - name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) - ) - self.nodes[dst_canvas_id].interfaces.append(dst_interface) - logging.debug( - "Create destination interface... IP: %s, name: %s", - dst_interface.ipv4, - dst_interface.name, - ) - - edge.interface_1 = src_interface - edge.interface_2 = dst_interface - return src_interface, dst_interface - - def add_edge(self, session_id, token, canvas_id_1, canvas_id_2): - """ - Add an edge to grpc manager - - :param int session_id: core session id - :param tuple(int, int) token: edge's identification in the canvas - :param int canvas_id_1: canvas id of source node - :param int canvas_id_2: canvas_id of destination node - - :return: nothing - """ - if canvas_id_1 in self.nodes and canvas_id_2 in self.nodes: - edge = Edge( - session_id, - self.nodes[canvas_id_1].node_id, - self.nodes[canvas_id_1].type, - self.nodes[canvas_id_2].node_id, - self.nodes[canvas_id_2].type, - ) - self.edges[token] = edge - src_interface, dst_interface = self.create_interface( - edge, canvas_id_1, canvas_id_2 - ) - node_one_id = self.nodes[canvas_id_1].node_id - node_two_id = self.nodes[canvas_id_2].node_id - - # provide a way to get an edge from a core node and an interface id - if src_interface is not None: - # self.node_id_and_interface_to_edge_token[tuple([node_one_id, src_interface.id])] = token - self.core_mapping.map_node_and_interface_to_canvas_edge( - node_one_id, src_interface.id, token - ) - logging.debug( - "map node id %s, interface_id %s to edge token %s", - node_one_id, - src_interface.id, - token, - ) - - if dst_interface is not None: - # self.node_id_and_interface_to_edge_token[tuple([node_two_id, dst_interface.id])] = token - self.core_mapping.map_node_and_interface_to_canvas_edge( - node_two_id, dst_interface.id, token - ) - logging.debug( - "map node id %s, interface_id %s to edge token %s", - node_two_id, - dst_interface.id, - token, - ) - - logging.debug("Adding edge to grpc manager...") - else: - logging.error("grpcmanagement.py INVALID CANVAS NODE ID") diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index 17bc99c8..a306bfdc 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -130,7 +130,7 @@ class Throughput: iid = t.interface_id tp = t.throughput # token = self.grpc_manager.node_id_and_interface_to_edge_token[nid, iid] - token = self.core_grpc.manager.core_mapping.get_token_from_node_and_interface( + token = self.core_grpc.core_mapping.get_token_from_node_and_interface( nid, iid ) print(token) diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py index 3e4a98f8..0c45e896 100644 --- a/coretk/coretk/wirelessconnection.py +++ b/coretk/coretk/wirelessconnection.py @@ -5,10 +5,10 @@ from core.api.grpc import core_pb2 class WirelessConnection: - def __init__(self, canvas, grpc): + def __init__(self, canvas, core_grpc): self.canvas = canvas - self.core_grpc = grpc - self.core_mapping = grpc.manager.core_mapping + self.core_grpc = core_grpc + self.core_mapping = core_grpc.core_mapping # map a (node_one_id, node_two_id) to a wlan canvas id self.map = {} From 6fa3beb1c1442871dbaa08a7d33edf9753946c38 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 Nov 2019 13:42:49 -0700 Subject: [PATCH 153/462] refactoring to change core_grpc to core, and update CoreGrpc to be CoreClient and have core now be client, also refactored usages of application to be just app to keep it short --- coretk/coretk/app.py | 8 +-- coretk/coretk/{coregrpc.py => coreclient.py} | 72 ++++++++++---------- coretk/coretk/coremenubar.py | 44 ++++++------ coretk/coretk/coretoolbar.py | 14 ++-- coretk/coretk/coretoolbarhelp.py | 19 ++---- coretk/coretk/dialogs/sessionoptions.py | 8 +-- coretk/coretk/dialogs/sessions.py | 10 +-- coretk/coretk/graph.py | 56 ++++++++------- coretk/coretk/graph_helper.py | 4 +- coretk/coretk/linkinfo.py | 10 ++- coretk/coretk/menuaction.py | 32 ++++----- coretk/coretk/setwallpaper.py | 54 +++++++-------- coretk/coretk/sizeandscale.py | 34 ++++----- coretk/coretk/wirelessconnection.py | 5 +- 14 files changed, 176 insertions(+), 194 deletions(-) rename coretk/coretk/{coregrpc.py => coreclient.py} (90%) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index bcc43e43..72749524 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,7 +1,7 @@ import logging import tkinter as tk -from coretk.coregrpc import CoreGrpc +from coretk.coreclient import CoreClient from coretk.coremenubar import CoreMenubar from coretk.coretoolbar import CoreToolbar from coretk.graph import CanvasGraph @@ -25,12 +25,12 @@ class Application(tk.Frame): self.radiovar = tk.IntVar(value=1) self.show_grid_var = tk.IntVar(value=1) self.adjust_to_dim_var = tk.IntVar(value=0) - self.core_grpc = CoreGrpc(self) + self.core = CoreClient(self) self.setup_app() self.create_menu() self.create_widgets() self.draw_canvas() - self.core_grpc.set_up() + self.core.set_up() def setup_app(self): self.master.title("CORE") @@ -54,7 +54,7 @@ class Application(tk.Frame): def draw_canvas(self): self.canvas = CanvasGraph( - self, self.core_grpc, background="#cccccc", scrollregion=(0, 0, 1200, 1000) + self, self.core, background="#cccccc", scrollregion=(0, 0, 1200, 1000) ) self.canvas.pack(fill=tk.BOTH, expand=True) diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coreclient.py similarity index 90% rename from coretk/coretk/coregrpc.py rename to coretk/coretk/coreclient.py index 94f5ca7b..e5ba838e 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coreclient.py @@ -56,13 +56,13 @@ class Edge: self.interface_2 = None -class CoreGrpc: - def __init__(self, app, sid=None): +class CoreClient: + def __init__(self, app): """ Create a CoreGrpc instance """ - self.core = client.CoreGrpcClient() - self.session_id = sid + self.client = client.CoreGrpcClient() + self.session_id = None self.node_ids = [] self.app = app self.master = app.master @@ -99,9 +99,9 @@ class CoreGrpc: def join_session(self, session_id): # query session and set as current session self.session_id = session_id - response = self.core.get_session(self.session_id) + response = self.client.get_session(self.session_id) logging.info("joining session(%s): %s", self.session_id, response) - self.core.events(self.session_id, self.handle_events) + self.client.events(self.session_id, self.handle_events) # set title to session self.master.title(f"CORE Session({self.session_id})") @@ -129,7 +129,7 @@ class CoreGrpc: :return: nothing """ - response = self.core.create_session() + response = self.client.create_session() logging.info("created session: %s", response) self.join_session(response.session_id) @@ -138,15 +138,15 @@ class CoreGrpc: sid = self.session_id else: sid = custom_sid - response = self.core.delete_session(sid) + response = self.client.delete_session(sid) logging.info("Deleted session result: %s", response) - def terminate_session(self, custom_sid=None): + def shutdown_session(self, custom_sid=None): if custom_sid is None: sid = self.session_id else: sid = custom_sid - s = self.core.get_session(sid).session + s = self.client.get_session(sid).session # delete links and nodes from running session if s.state == core_pb2.SessionState.RUNTIME: self.set_session_state("datacollect", sid) @@ -160,8 +160,8 @@ class CoreGrpc: :return: existing sessions """ - self.core.connect() - response = self.core.get_sessions() + self.client.connect() + response = self.client.get_sessions() # if there are no sessions, create a new session, else join a session sessions = response.sessions @@ -172,7 +172,7 @@ class CoreGrpc: dialog.show() def get_session_state(self): - response = self.core.get_session(self.session_id) + response = self.client.get_session(self.session_id) # logging.info("get session: %s", response) return response.session.state @@ -190,27 +190,29 @@ class CoreGrpc: response = None if state == "configuration": - response = self.core.set_session_state( + response = self.client.set_session_state( sid, core_pb2.SessionState.CONFIGURATION ) elif state == "instantiation": - response = self.core.set_session_state( + response = self.client.set_session_state( sid, core_pb2.SessionState.INSTANTIATION ) elif state == "datacollect": - response = self.core.set_session_state( + response = self.client.set_session_state( sid, core_pb2.SessionState.DATACOLLECT ) elif state == "shutdown": - response = self.core.set_session_state(sid, core_pb2.SessionState.SHUTDOWN) + response = self.client.set_session_state( + sid, core_pb2.SessionState.SHUTDOWN + ) elif state == "runtime": - response = self.core.set_session_state(sid, core_pb2.SessionState.RUNTIME) + response = self.client.set_session_state(sid, core_pb2.SessionState.RUNTIME) elif state == "definition": - response = self.core.set_session_state( + response = self.client.set_session_state( sid, core_pb2.SessionState.DEFINITION ) elif state == "none": - response = self.core.set_session_state(sid, core_pb2.SessionState.NONE) + response = self.client.set_session_state(sid, core_pb2.SessionState.NONE) else: logging.error("coregrpc.py: set_session_state: INVALID STATE") @@ -218,7 +220,7 @@ class CoreGrpc: def edit_node(self, node_id, x, y): position = core_pb2.Position(x=x, y=y) - response = self.core.edit_node(self.session_id, node_id, position) + response = self.client.edit_node(self.session_id, node_id, position) logging.info("updated node id %s: %s", node_id, response) def delete_nodes(self, delete_session=None): @@ -226,8 +228,8 @@ class CoreGrpc: sid = self.session_id else: sid = delete_session - for node in self.core.get_session(sid).session.nodes: - response = self.core.delete_node(self.session_id, node.id) + for node in self.client.get_session(sid).session.nodes: + response = self.client.delete_node(self.session_id, node.id) logging.info("delete nodes %s", response) def delete_links(self, delete_session=None): @@ -237,8 +239,8 @@ class CoreGrpc: else: sid = delete_session - for link in self.core.get_session(sid).session.links: - response = self.core.delete_link( + for link in self.client.get_session(sid).session.links: + response = self.client.delete_link( self.session_id, link.node_one_id, link.node_two_id, @@ -259,7 +261,7 @@ class CoreGrpc: wlan_configs=None, mobility_configs=None, ): - response = self.core.start_session( + response = self.client.start_session( session_id=self.session_id, nodes=nodes, links=links, @@ -268,7 +270,7 @@ class CoreGrpc: logging.debug("Start session %s, result: %s", self.session_id, response.result) def stop_session(self): - response = self.core.stop_session(session_id=self.session_id) + response = self.client.stop_session(session_id=self.session_id) logging.debug("coregrpc.py Stop session, result: %s", response.result) # TODO no need, might get rid of this @@ -285,11 +287,11 @@ class CoreGrpc: """ if1 = self.create_interface(type1, edge.interface_1) if2 = self.create_interface(type2, edge.interface_2) - response = self.core.add_link(self.session_id, id1, id2, if1, if2) + response = self.client.add_link(self.session_id, id1, id2, if1, if2) logging.info("created link: %s", response) def launch_terminal(self, node_id): - response = self.core.get_node_terminal(self.session_id, node_id) + response = self.client.get_node_terminal(self.session_id, node_id) logging.info("get terminal %s", response.terminal) os.system("xterm -e %s &" % response.terminal) @@ -300,9 +302,9 @@ class CoreGrpc: :param str file_path: file path that user pick :return: nothing """ - response = self.core.save_xml(self.session_id, file_path) + response = self.client.save_xml(self.session_id, file_path) logging.info("coregrpc.py save xml %s", response) - self.core.events(self.session_id, self.handle_events) + self.client.events(self.session_id, self.handle_events) def open_xml(self, file_path): """ @@ -311,7 +313,7 @@ class CoreGrpc: :param str file_path: file to open :return: session id """ - response = self.core.open_xml(file_path) + response = self.client.open_xml(file_path) logging.debug("open xml: %s", response) self.join_session(response.session_id) @@ -322,7 +324,7 @@ class CoreGrpc: :return: nothing """ logging.debug("Close grpc") - self.core.close() + self.client.close() def peek_id(self): """ @@ -353,7 +355,7 @@ class CoreGrpc: position = core_pb2.Position(x=x, y=y) node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) self.node_ids.append(node_id) - response = self.core.add_node(self.session_id, node) + response = self.client.add_node(self.session_id, node) logging.info("created node: %s", response) if node_type == core_pb2.NodeType.WIRELESS_LAN: d = OrderedDict() @@ -362,7 +364,7 @@ class CoreGrpc: d["jitter"] = "0" d["delay"] = "20000" d["error"] = "0" - r = self.core.set_wlan_config(self.session_id, node_id, d) + r = self.client.set_wlan_config(self.session_id, node_id, d) logging.debug("set wlan config %s", r) return response.node_id diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index aa00a927..7b3f2633 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -9,19 +9,19 @@ class CoreMenubar(object): Core menubar """ - def __init__(self, application, master, menubar): + def __init__(self, app, master, menubar): """ Create a CoreMenubar instance :param master: :param tkinter.Menu menubar: menubar object - :param coretk.app.Application application: application object + :param coretk.app.Application app: application object """ self.menubar = menubar self.master = master - self.application = application - self.menuaction = action.MenuAction(application, master) - self.menu_action = MenuAction(self.application, self.master) + self.app = app + self.menuaction = action.MenuAction(app, master) + self.menu_action = MenuAction(self.app, self.master) # def on_quit(self): # """ @@ -649,23 +649,23 @@ class CoreMenubar(object): :return: nothing """ - self.application.bind_all("", action.file_new_shortcut) - self.application.bind_all("", action.file_open_shortcut) - self.application.bind_all("", action.file_save_shortcut) - self.application.bind_all("", action.edit_undo_shortcut) - self.application.bind_all("", action.edit_redo_shortcut) - self.application.bind_all("", action.edit_cut_shortcut) - self.application.bind_all("", action.edit_copy_shortcut) - self.application.bind_all("", action.edit_paste_shortcut) - self.application.bind_all("", action.edit_select_all_shortcut) - self.application.bind_all("", action.edit_select_adjacent_shortcut) - self.application.bind_all("", action.edit_find_shortcut) - self.application.bind_all("", action.canvas_previous_shortcut) - self.application.bind_all("", action.canvas_next_shortcut) - self.application.bind_all("", action.canvas_first_shortcut) - self.application.bind_all("", action.canvas_last_shortcut) - self.application.bind_all("", action.view_zoom_in_shortcut) - self.application.bind_all("", action.view_zoom_out_shortcut) + self.app.bind_all("", action.file_new_shortcut) + self.app.bind_all("", action.file_open_shortcut) + self.app.bind_all("", action.file_save_shortcut) + self.app.bind_all("", action.edit_undo_shortcut) + self.app.bind_all("", action.edit_redo_shortcut) + self.app.bind_all("", action.edit_cut_shortcut) + self.app.bind_all("", action.edit_copy_shortcut) + self.app.bind_all("", action.edit_paste_shortcut) + self.app.bind_all("", action.edit_select_all_shortcut) + self.app.bind_all("", action.edit_select_adjacent_shortcut) + self.app.bind_all("", action.edit_find_shortcut) + self.app.bind_all("", action.canvas_previous_shortcut) + self.app.bind_all("", action.canvas_next_shortcut) + self.app.bind_all("", action.canvas_first_shortcut) + self.app.bind_all("", action.canvas_last_shortcut) + self.app.bind_all("", action.view_zoom_in_shortcut) + self.app.bind_all("", action.view_zoom_out_shortcut) def create_core_menubar(self): """ diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 78688725..5d7c05ae 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -25,14 +25,14 @@ class CoreToolbar(object): Core toolbar class """ - def __init__(self, application, edit_frame, menubar): + def __init__(self, app, edit_frame, menubar): """ Create a CoreToolbar instance :param tkinter.Frame edit_frame: edit frame """ - self.application = application - self.master = application.master + self.app = app + self.master = app.master self.edit_frame = edit_frame self.menubar = menubar self.radio_value = tk.IntVar() @@ -161,7 +161,7 @@ class CoreToolbar(object): :return: nothing """ logging.debug("Click START STOP SESSION button") - helper = CoreToolbarHelp(self.application) + helper = CoreToolbarHelp(self.app) self.destroy_children_widgets() self.canvas.mode = GraphMode.SELECT @@ -593,11 +593,7 @@ class CoreToolbar(object): """ logging.debug("Click on STOP button ") self.destroy_children_widgets() - - # self.canvas.core_grpc.set_session_state(SessionStateEnum.DATACOLLECT.value) - # self.canvas.core_grpc.delete_links() - # self.canvas.core_grpc.delete_nodes() - self.canvas.core_grpc.stop_session() + self.app.core.stop_session() self.create_toolbar() def click_run_button(self): diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index b26269db..ca30141d 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -15,8 +15,7 @@ class CoreToolbarHelp: :return: nothing """ nodes = [] - core = self.app.core_grpc - for node in core.nodes.values(): + for node in self.app.core.nodes.values(): pos = core_pb2.Position(x=int(node.x), y=int(node.y)) n = core_pb2.Node( id=node.node_id, type=node.type, position=pos, model=node.model @@ -32,14 +31,9 @@ class CoreToolbarHelp: :return: list of protobuf links """ links = [] - core = self.app.core_grpc - for edge in core.edges.values(): - interface_one = self.app.core_grpc.create_interface( - edge.type1, edge.interface_1 - ) - interface_two = self.app.core_grpc.create_interface( - edge.type2, edge.interface_2 - ) + for edge in self.app.core.edges.values(): + interface_one = self.app.core.create_interface(edge.type1, edge.interface_1) + interface_two = self.app.core.create_interface(edge.type2, edge.interface_2) # TODO for now only consider the basic cases if ( edge.type1 == core_pb2.NodeType.WIRELESS_LAN @@ -61,8 +55,7 @@ class CoreToolbarHelp: def get_wlan_configuration_list(self): configs = [] - core = self.app.core_grpc - manager_configs = core.wlanconfig_management.configurations + manager_configs = self.app.core.wlanconfig_management.configurations for key in manager_configs: cnf = core_pb2.WlanConfig(node_id=key, config=manager_configs[key]) configs.append(cnf) @@ -72,4 +65,4 @@ class CoreToolbarHelp: nodes = self.get_node_list() links = self.get_link_list() wlan_configs = self.get_wlan_configuration_list() - self.app.core_grpc.start_session(nodes, links, wlan_configs=wlan_configs) + self.app.core.start_session(nodes, links, wlan_configs=wlan_configs) diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index a34c6658..6279d844 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -18,8 +18,8 @@ class SessionOptionsDialog(Dialog): self.draw() def draw(self): - session_id = self.master.core_grpc.session_id - response = self.master.core_grpc.core.get_session_options(session_id) + session_id = self.app.core.session_id + response = self.app.core.client.get_session_options(session_id) logging.info("session options: %s", response) self.options = response.config self.values = configutils.create_config(self, self.options, PAD_X, PAD_Y) @@ -28,7 +28,7 @@ class SessionOptionsDialog(Dialog): def save(self): config = configutils.parse_config(self.options, self.values) - session_id = self.master.core_grpc.session_id - response = self.master.core_grpc.core.set_session_options(session_id, config) + session_id = self.app.core.session_id + response = self.app.core.client.set_session_options(session_id, config) logging.info("saved session config: %s", response) self.destroy() diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index b2bd90f0..b7c4d128 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -12,7 +12,7 @@ class SessionsDialog(Dialog): """ create session table instance - :param coretk.coregrpc.CoreGrpc grpc: coregrpc + :param coretk.coreclient.CoreClient grpc: coregrpc :param root.master master: """ super().__init__(master, app, "Sessions", modal=True) @@ -51,7 +51,7 @@ class SessionsDialog(Dialog): self.tree.column("nodes", stretch=tk.YES) self.tree.heading("nodes", text="Node Count") - response = self.app.core_grpc.core.get_sessions() + response = self.app.core.client.get_sessions() logging.info("sessions: %s", response) for index, session in enumerate(response.sessions): state_name = core_pb2.SessionState.Enum.Name(session.state) @@ -105,7 +105,7 @@ class SessionsDialog(Dialog): b.grid(row=0, column=3, padx=2, sticky="ew") def click_new(self): - self.app.core_grpc.create_new_session() + self.app.core.create_new_session() self.destroy() def click_select(self, event): @@ -142,7 +142,7 @@ class SessionsDialog(Dialog): logging.error("querysessiondrawing.py invalid state") def join_session(self, session_id): - self.app.core_grpc.join_session(session_id) + self.app.core.join_session(session_id) self.destroy() def on_selected(self, event): @@ -151,6 +151,6 @@ class SessionsDialog(Dialog): self.join_session(sid) def shutdown_session(self, sid): - self.app.core_grpc.terminate_session(sid) + self.app.core.shutdown_session(sid) self.click_new() self.destroy() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 7859a8e6..e53a5274 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -26,7 +26,7 @@ CORE_EMANE = ["emane"] class CanvasGraph(tk.Canvas): - def __init__(self, master, core_grpc, cnf=None, **kwargs): + def __init__(self, master, core, cnf=None, **kwargs): if cnf is None: cnf = {} kwargs["highlightthickness"] = 0 @@ -45,10 +45,10 @@ class CanvasGraph(tk.Canvas): self.setup_menus() self.setup_bindings() self.draw_grid() - self.core_grpc = core_grpc - self.helper = GraphHelper(self, core_grpc) - self.throughput_draw = Throughput(self, core_grpc) - self.wireless_draw = WirelessConnection(self, core_grpc) + self.core = core + self.helper = GraphHelper(self, core) + self.throughput_draw = Throughput(self, core) + self.wireless_draw = WirelessConnection(self, core) self.is_node_context_opened = False def setup_menus(self): @@ -147,7 +147,7 @@ class CanvasGraph(tk.Canvas): core_id_to_canvas_id[node.id] = n.id # store the node in grpc manager - self.core_grpc.add_preexisting_node(n, session.id, node, name) + self.core.add_preexisting_node(n, session.id, node, name) # draw existing links for link in session.links: @@ -179,7 +179,7 @@ class CanvasGraph(tk.Canvas): n1.edges.add(e) n2.edges.add(e) self.edges[e.token] = e - self.core_grpc.add_edge(session.id, e.token, n1.id, n2.id) + self.core.add_edge(session.id, e.token, n1.id, n2.id) self.helper.redraw_antenna(link, n1, n2) @@ -208,14 +208,14 @@ class CanvasGraph(tk.Canvas): # TODO will include throughput and ipv6 in the future if1 = Interface(grpc_if1.name, grpc_if1.ip4, ifid=grpc_if1.id) if2 = Interface(grpc_if2.name, grpc_if2.ip4, ifid=grpc_if2.id) - self.core_grpc.edges[e.token].interface_1 = if1 - self.core_grpc.edges[e.token].interface_2 = if2 - self.core_grpc.nodes[ - core_id_to_canvas_id[link.node_one_id] - ].interfaces.append(if1) - self.core_grpc.nodes[ - core_id_to_canvas_id[link.node_two_id] - ].interfaces.append(if2) + self.core.edges[e.token].interface_1 = if1 + self.core.edges[e.token].interface_2 = if2 + self.core.nodes[core_id_to_canvas_id[link.node_one_id]].interfaces.append( + if1 + ) + self.core.nodes[core_id_to_canvas_id[link.node_two_id]].interfaces.append( + if2 + ) # lift the nodes so they on top of the links for i in self.find_withtag("node"): @@ -318,13 +318,13 @@ class CanvasGraph(tk.Canvas): node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) - self.core_grpc.add_edge( - self.core_grpc.session_id, edge.token, node_src.id, node_dst.id + self.core.add_edge( + self.core.session_id, edge.token, node_src.id, node_dst.id ) # draw link info on the edge - if1 = self.core_grpc.edges[edge.token].interface_1 - if2 = self.core_grpc.edges[edge.token].interface_2 + if1 = self.core.edges[edge.token].interface_1 + if2 = self.core.edges[edge.token].interface_2 ip4_and_prefix_1 = None ip4_and_prefix_2 = None if if1 is not None: @@ -390,12 +390,10 @@ class CanvasGraph(tk.Canvas): image=image, node_type=node_name, canvas=self, - core_id=self.core_grpc.peek_id(), + core_id=self.core.peek_id(), ) self.nodes[node.id] = node - self.core_grpc.add_graph_node( - self.core_grpc.session_id, node.id, x, y, node_name - ) + self.core.add_graph_node(self.core.session_id, node.id, x, y, node_name) return node @@ -485,10 +483,10 @@ class CanvasNode: self.moving = None def double_click(self, event): - node_id = self.canvas.core_grpc.nodes[self.id].node_id - state = self.canvas.core_grpc.get_session_state() + node_id = self.canvas.core.nodes[self.id].node_id + state = self.canvas.core.get_session_state() if state == core_pb2.SessionState.RUNTIME: - self.canvas.core_grpc.launch_terminal(node_id) + self.canvas.core.launch_terminal(node_id) else: self.canvas.canvas_action.display_configuration(self) # if self.node_type in CORE_NODES: @@ -511,7 +509,7 @@ class CanvasNode: def click_release(self, event): logging.debug(f"click release {self.name}: {event}") self.update_coords() - self.canvas.core_grpc.update_node_location(self.id, self.x_coord, self.y_coord) + self.canvas.core.update_node_location(self.id, self.x_coord, self.y_coord) self.moving = None def motion(self, event): @@ -529,8 +527,8 @@ class CanvasNode: new_x, new_y = self.canvas.coords(self.id) - if self.canvas.core_grpc.get_session_state() == core_pb2.SessionState.RUNTIME: - self.canvas.core_grpc.edit_node(self.core_id, int(new_x), int(new_y)) + if self.canvas.core.get_session_state() == core_pb2.SessionState.RUNTIME: + self.canvas.core.edit_node(self.core_id, int(new_x), int(new_y)) for edge in self.edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py index ba58a435..bdedd65e 100644 --- a/coretk/coretk/graph_helper.py +++ b/coretk/coretk/graph_helper.py @@ -11,12 +11,12 @@ CANVAS_COMPONENT_TAGS = ["edge", "node", "nodename", "wallpaper", "linkinfo"] class GraphHelper: - def __init__(self, canvas, grpc): + def __init__(self, canvas, core): """ create an instance of GraphHelper object """ self.canvas = canvas - self.core_grpc = grpc + self.core = core def delete_canvas_components(self): """ diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index a306bfdc..916e8dfb 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -22,7 +22,7 @@ class LinkInfo: self.edge = edge # self.edge_id = edge.id self.radius = 37 - self.core_grpc = self.canvas.core_grpc + self.core = self.canvas.core self.ip4_address_1 = ip4_src self.ip6_address_1 = ip6_src @@ -104,13 +104,13 @@ class LinkInfo: class Throughput: - def __init__(self, canvas, core_grpc): + def __init__(self, canvas, core): """ create an instance of Throughput object :param coretk.app.Application app: application """ self.canvas = canvas - self.core_grpc = core_grpc + self.core = core # edge canvas id mapped to throughput value self.tracker = {} # map an edge canvas id to a throughput canvas id @@ -130,9 +130,7 @@ class Throughput: iid = t.interface_id tp = t.throughput # token = self.grpc_manager.node_id_and_interface_to_edge_token[nid, iid] - token = self.core_grpc.core_mapping.get_token_from_node_and_interface( - nid, iid - ) + token = self.core.core_mapping.get_token_from_node_and_interface(nid, iid) print(token) edge_id = self.canvas.edges[token].id diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 4ed5e529..7598f210 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -321,10 +321,9 @@ class MenuAction: Actions performed when choosing menu items """ - def __init__(self, application, master): + def __init__(self, app, master): self.master = master - self.application = application - self.core_grpc = application.core_grpc + self.app = app def prompt_save_running_session(self): """ @@ -335,21 +334,20 @@ class MenuAction: logging.info( "menuaction.py: clean_nodes_links_and_set_configuration() Exiting the program" ) - grpc = self.application.core_grpc - state = grpc.get_session_state() + state = self.app.core.get_session_state() if ( state == core_pb2.SessionState.SHUTDOWN or state == core_pb2.SessionState.DEFINITION ): - grpc.delete_session() + self.app.core.delete_session() else: msgbox = messagebox.askyesnocancel("stop", "Stop the running session?") if msgbox or msgbox is False: if msgbox: - grpc.stop_session() - grpc.delete_session() + self.app.core.stop_session() + self.app.core.delete_session() def on_quit(self): """ @@ -358,23 +356,21 @@ class MenuAction: :return: nothing """ self.prompt_save_running_session() - # self.application.core_grpc.close() - self.application.quit() + self.app.quit() def file_save_as_xml(self): logging.info("menuaction.py file_save_as_xml()") - grpc = self.application.core_grpc file_path = filedialog.asksaveasfilename( initialdir=SAVEDIR, title="Save As", filetypes=(("EmulationScript XML files", "*.xml"), ("All files", "*")), defaultextension=".xml", ) - grpc.save_xml(file_path) + self.app.core.save_xml(file_path) def file_open_xml(self): logging.info("menuaction.py file_open_xml()") - self.application.is_open_xml = True + self.app.is_open_xml = True file_path = filedialog.askopenfilename( initialdir=SAVEDIR, title="Open", @@ -382,17 +378,17 @@ class MenuAction: ) # clean up before opening a new session self.prompt_save_running_session() - self.application.core_grpc.open_xml(file_path) + self.app.core.open_xml(file_path) # Todo might not need # self.application.core_editbar.destroy_children_widgets() # self.application.core_editbar.create_toolbar() def canvas_size_and_scale(self): - self.application.size_and_scale = SizeAndScale(self.application) + self.app.size_and_scale = SizeAndScale(self.app) def canvas_set_wallpaper(self): - self.application.set_wallpaper = CanvasWallpaper(self.application) + self.app.set_wallpaper = CanvasWallpaper(self.app) def help_core_github(self): webbrowser.open_new("https://github.com/coreemu/core") @@ -402,10 +398,10 @@ class MenuAction: def session_options(self): logging.debug("Click session options") - dialog = SessionOptionsDialog(self.application, self.application) + dialog = SessionOptionsDialog(self.app, self.app) dialog.show() def session_change_sessions(self): logging.debug("Click session change sessions") - dialog = SessionsDialog(self.application, self.application) + dialog = SessionsDialog(self.app, self.app) dialog.show() diff --git a/coretk/coretk/setwallpaper.py b/coretk/coretk/setwallpaper.py index beec0162..edace83c 100644 --- a/coretk/coretk/setwallpaper.py +++ b/coretk/coretk/setwallpaper.py @@ -22,24 +22,24 @@ class ScaleOption(enum.Enum): class CanvasWallpaper: - def __init__(self, application): + def __init__(self, app): """ create an instance of CanvasWallpaper object - :param coretk.app.Application application: root application + :param coretk.app.Application app: root application """ - self.application = application - self.canvas = self.application.canvas + self.app = app + self.canvas = self.app.canvas self.top = tk.Toplevel() self.top.title("Set Canvas Wallpaper") self.radiovar = tk.IntVar() - print(self.application.radiovar.get()) - self.radiovar.set(self.application.radiovar.get()) + print(self.app.radiovar.get()) + self.radiovar.set(self.app.radiovar.get()) self.show_grid_var = tk.IntVar() - self.show_grid_var.set(self.application.show_grid_var.get()) + self.show_grid_var.set(self.app.show_grid_var.get()) self.adjust_to_dim_var = tk.IntVar() - self.adjust_to_dim_var.set(self.application.adjust_to_dim_var.get()) + self.adjust_to_dim_var.set(self.app.adjust_to_dim_var.get()) self.create_image_label() self.create_text_label() @@ -181,7 +181,7 @@ class CanvasWallpaper: :return: nothing """ - canvas = self.application.canvas + canvas = self.app.canvas grid = canvas.find_withtag("rectangle")[0] x0, y0, x1, y1 = canvas.coords(grid) canvas_w = abs(x0 - x1) @@ -215,7 +215,7 @@ class CanvasWallpaper: cropped_tk = ImageTk.PhotoImage(cropped) # place left corner of image to the left corner of the canvas - self.application.croppedwallpaper = cropped_tk + self.app.croppedwallpaper = cropped_tk self.delete_canvas_components(["wallpaper"]) # self.delete_previous_wallpaper() @@ -223,7 +223,7 @@ class CanvasWallpaper: wid = self.canvas.create_image( (cropx / 2, cropy / 2), image=cropped_tk, tags="wallpaper" ) - self.application.wallpaper_id = wid + self.app.wallpaper_id = wid def center(self, img): """ @@ -252,13 +252,13 @@ class CanvasWallpaper: cropped_tk = ImageTk.PhotoImage(cropped) # place the center of the image at the center of the canvas - self.application.croppedwallpaper = cropped_tk + self.app.croppedwallpaper = cropped_tk self.delete_canvas_components(["wallpaper"]) # self.delete_previous_wallpaper() wid = self.canvas.create_image( (canvas_w / 2, canvas_h / 2), image=cropped_tk, tags="wallpaper" ) - self.application.wallpaper_id = wid + self.app.wallpaper_id = wid def scaled(self, img): """ @@ -270,7 +270,7 @@ class CanvasWallpaper: canvas_w, canvas_h = self.get_canvas_width_and_height() resized_image = img.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS) image_tk = ImageTk.PhotoImage(resized_image) - self.application.croppedwallpaper = image_tk + self.app.croppedwallpaper = image_tk self.delete_canvas_components(["wallpaper"]) # self.delete_previous_wallpaper() @@ -278,7 +278,7 @@ class CanvasWallpaper: wid = self.canvas.create_image( (canvas_w / 2, canvas_h / 2), image=image_tk, tags="wallpaper" ) - self.application.wallpaper_id = wid + self.app.wallpaper_id = wid def tiled(self, img): return @@ -301,15 +301,15 @@ class CanvasWallpaper: self.delete_canvas_components(["wallpaper"]) self.draw_new_canvas(img_w, img_h) wid = self.canvas.create_image((img_w / 2, img_h / 2), image=image_tk) - self.application.croppedwallpaper = image_tk - self.application.wallpaper_id = wid + self.app.croppedwallpaper = image_tk + self.app.wallpaper_id = wid def show_grid(self): """ :return: nothing """ - self.application.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) + self.app.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) if self.show_grid_var.get() == 0: for i in self.canvas.find_withtag("gridline"): @@ -322,9 +322,9 @@ class CanvasWallpaper: logging.error("setwallpaper.py show_grid invalid value") def save_wallpaper_options(self): - self.application.radiovar.set(self.radiovar.get()) - self.application.show_grid_var.set(self.show_grid_var.get()) - self.application.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) + self.app.radiovar.set(self.radiovar.get()) + self.app.show_grid_var.set(self.show_grid_var.get()) + self.app.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) def click_apply(self): img_link_frame = self.top.grid_slaves(2, 0)[0] @@ -332,23 +332,23 @@ class CanvasWallpaper: if not filename: self.delete_canvas_components(["wallpaper"]) self.top.destroy() - self.application.current_wallpaper = None + self.app.current_wallpaper = None self.save_wallpaper_options() return try: img = Image.open(filename) - self.application.current_wallpaper = img + self.app.current_wallpaper = img except FileNotFoundError: print("invalid filename, draw original white plot") - if self.application.wallpaper_id: - self.canvas.delete(self.application.wallpaper_id) + if self.app.wallpaper_id: + self.canvas.delete(self.app.wallpaper_id) self.top.destroy() return - self.application.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) + self.app.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) if self.adjust_to_dim_var.get() == 0: - self.application.radiovar.set(self.radiovar.get()) + self.app.radiovar.set(self.radiovar.get()) if self.radiovar.get() == ScaleOption.UPPER_LEFT.value: self.upper_left(img) diff --git a/coretk/coretk/sizeandscale.py b/coretk/coretk/sizeandscale.py index 3552c2e5..5c746c39 100644 --- a/coretk/coretk/sizeandscale.py +++ b/coretk/coretk/sizeandscale.py @@ -10,16 +10,16 @@ DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] class SizeAndScale: - def __init__(self, application): + def __init__(self, app): """ create an instance for size and scale object - :param application: main application + :param app: main application """ - self.application = application + self.app = app self.top = tk.Toplevel() self.top.title("Canvas Size and Scale") - self.meter_per_pixel = self.application.canvas.meters_per_pixel + self.meter_per_pixel = self.app.canvas.meters_per_pixel self.size_chart() self.scale_chart() @@ -108,7 +108,7 @@ class SizeAndScale: label = tk.Label(self.top, text="Size") label.grid(sticky=tk.W, padx=5) - canvas = self.application.canvas + canvas = self.app.canvas plot = canvas.find_withtag("rectangle") x0, y0, x1, y1 = canvas.bbox(plot[0]) w = abs(x0 - x1) - 2 @@ -222,7 +222,7 @@ class SizeAndScale: :param int pixel_height: height in pixel :return: nothing """ - canvas = self.application.canvas + canvas = self.app.canvas canvas.config(scrollregion=(0, 0, pixel_width + 200, pixel_height + 200)) # delete old plot and redraw @@ -246,24 +246,24 @@ class SizeAndScale: scale_frame = self.top.grid_slaves(3, 0)[0] meter_per_pixel = float(scale_frame.grid_slaves(0, 1)[0].get()) / 100 - self.application.canvas.meters_per_pixel = meter_per_pixel + self.app.canvas.meters_per_pixel = meter_per_pixel self.redraw_grid(pixel_width, pixel_height) - print(self.application.current_wallpaper) - print(self.application.radiovar) + print(self.app.current_wallpaper) + print(self.app.radiovar) # if there is a current wallpaper showing, redraw it based on current wallpaper options - wallpaper_tool = self.application.set_wallpaper - current_wallpaper = self.application.current_wallpaper + wallpaper_tool = self.app.set_wallpaper + current_wallpaper = self.app.current_wallpaper if current_wallpaper: - if self.application.adjust_to_dim_var.get() == 0: - if self.application.radiovar.get() == ScaleOption.UPPER_LEFT.value: + if self.app.adjust_to_dim_var.get() == 0: + if self.app.radiovar.get() == ScaleOption.UPPER_LEFT.value: wallpaper_tool.upper_left(current_wallpaper) - elif self.application.radiovar.get() == ScaleOption.CENTERED.value: + elif self.app.radiovar.get() == ScaleOption.CENTERED.value: wallpaper_tool.center(current_wallpaper) - elif self.application.radiovar.get() == ScaleOption.SCALED.value: + elif self.app.radiovar.get() == ScaleOption.SCALED.value: wallpaper_tool.scaled(current_wallpaper) - elif self.application.radiovar.get() == ScaleOption.TILED.value: + elif self.app.radiovar.get() == ScaleOption.TILED.value: print("not implemented") - elif self.application.adjust_to_dim_var.get() == 1: + elif self.app.adjust_to_dim_var.get() == 1: wallpaper_tool.canvas_to_image_dimension(current_wallpaper) wallpaper_tool.show_grid() diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py index 0c45e896..d1e17bc5 100644 --- a/coretk/coretk/wirelessconnection.py +++ b/coretk/coretk/wirelessconnection.py @@ -5,10 +5,9 @@ from core.api.grpc import core_pb2 class WirelessConnection: - def __init__(self, canvas, core_grpc): + def __init__(self, canvas, core): self.canvas = canvas - self.core_grpc = core_grpc - self.core_mapping = core_grpc.core_mapping + self.core_mapping = core.core_mapping # map a (node_one_id, node_two_id) to a wlan canvas id self.map = {} From 6c49a73e3883e34f885c577ef4c5ff3d5966ea27 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 Nov 2019 13:45:42 -0700 Subject: [PATCH 154/462] removed old image, fixed get image calls in images.py --- coretk/coretk/images.py | 4 ++-- coretk/coretk/switch.png | Bin 5286 -> 0 bytes 2 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 coretk/coretk/switch.png diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 3f230fd8..7a7c0331 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -46,9 +46,9 @@ class Images: if node_type == core_pb2.NodeType.HUB: return Images.get(ImageEnum.HUB), "hub" if node_type == core_pb2.NodeType.WIRELESS_LAN: - return Images.get(ImageEnum.WLAN.value), "wlan" + return Images.get(ImageEnum.WLAN), "wlan" if node_type == core_pb2.NodeType.EMANE: - return Images.get(ImageEnum.EMANE.value), "emane" + return Images.get(ImageEnum.EMANE), "emane" if node_type == core_pb2.NodeType.RJ45: return Images.get(ImageEnum.RJ45), "rj45" diff --git a/coretk/coretk/switch.png b/coretk/coretk/switch.png deleted file mode 100644 index f8c852947639b68084a32c96f156ec1be99829e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5286 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 M8hYwA5Ua@l0JEh!?f?J) From 0f78acaa0ce4c55bd99a753b93ec37fc3ccb6bc8 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 Nov 2019 13:57:41 -0700 Subject: [PATCH 155/462] update method names for drawing main app, moved delete window call into app --- coretk/coretk/app.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 72749524..73cbcfbd 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -27,26 +27,27 @@ class Application(tk.Frame): self.adjust_to_dim_var = tk.IntVar(value=0) self.core = CoreClient(self) self.setup_app() - self.create_menu() - self.create_widgets() + self.draw_menu() + self.draw_toolbar() self.draw_canvas() self.core.set_up() def setup_app(self): self.master.title("CORE") self.master.geometry("1000x800") + self.master.protocol("WM_DELETE_WINDOW", self.on_closing) image = Images.get(ImageEnum.CORE) self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) - def create_menu(self): + def draw_menu(self): self.master.option_add("*tearOff", tk.FALSE) self.menubar = tk.Menu(self.master) self.core_menu = CoreMenubar(self, self.master, self.menubar) self.core_menu.create_core_menubar() self.master.config(menu=self.menubar) - def create_widgets(self): + def draw_toolbar(self): edit_frame = tk.Frame(self) edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) self.core_editbar = CoreToolbar(self, edit_frame, self.menubar) @@ -86,5 +87,4 @@ class Application(tk.Frame): if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) app = Application() - app.master.protocol("WM_DELETE_WINDOW", app.on_closing) app.mainloop() From f10acbc8d97d096c8ea99e31cc9eaf118180aabc Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 Nov 2019 16:31:40 -0700 Subject: [PATCH 156/462] added gui directory check and creation, created module for referencing gui directories --- coretk/coretk/app.py | 2 + coretk/coretk/appdirs.py | 39 ++++++++++++++++++ .../{wallpaper => backgrounds}/sample1-bg.gif | Bin .../{wallpaper => backgrounds}/sample4-bg.jpg | Bin coretk/coretk/images.py | 12 ++---- coretk/coretk/setwallpaper.py | 7 +--- 6 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 coretk/coretk/appdirs.py rename coretk/coretk/{wallpaper => backgrounds}/sample1-bg.gif (100%) rename coretk/coretk/{wallpaper => backgrounds}/sample4-bg.jpg (100%) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 73cbcfbd..671504a8 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,6 +1,7 @@ import logging import tkinter as tk +from coretk import appdirs from coretk.coreclient import CoreClient from coretk.coremenubar import CoreMenubar from coretk.coretoolbar import CoreToolbar @@ -86,5 +87,6 @@ class Application(tk.Frame): if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) + appdirs.check_directory() app = Application() app.mainloop() diff --git a/coretk/coretk/appdirs.py b/coretk/coretk/appdirs.py new file mode 100644 index 00000000..7dae4bf3 --- /dev/null +++ b/coretk/coretk/appdirs.py @@ -0,0 +1,39 @@ +import logging +import shutil +from pathlib import Path + +# gui home paths +HOME_PATH = Path.home().joinpath(".coretk") +BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds") +CUSTOM_EMANE_PATH = HOME_PATH.joinpath("custom_emane") +CUSTOM_SERVICE_PATH = HOME_PATH.joinpath("custom_services") +ICONS_PATH = HOME_PATH.joinpath("icons") +MOBILITY_PATH = HOME_PATH.joinpath("mobility") +XML_PATH = HOME_PATH.joinpath("xml") +CONFIG_PATH = HOME_PATH.joinpath("gui.yaml") + +# local paths +LOCAL_ICONS_PATH = Path(__file__).parent.joinpath("icons").absolute() +LOCAL_BACKGROUND_PATH = Path(__file__).parent.joinpath("backgrounds").absolute() + + +def check_directory(): + if HOME_PATH.exists(): + logging.info("~/.coretk exists") + return + logging.info("creating ~/.coretk") + HOME_PATH.mkdir() + BACKGROUNDS_PATH.mkdir() + CUSTOM_EMANE_PATH.mkdir() + CUSTOM_SERVICE_PATH.mkdir() + ICONS_PATH.mkdir() + MOBILITY_PATH.mkdir() + XML_PATH.mkdir() + for image in LOCAL_ICONS_PATH.glob("*"): + new_image = ICONS_PATH.joinpath(image.name) + shutil.copy(image, new_image) + for background in LOCAL_BACKGROUND_PATH.glob("*"): + new_background = BACKGROUNDS_PATH.joinpath(background.name) + shutil.copy(background, new_background) + with CONFIG_PATH.open("w") as f: + f.write("# gui config") diff --git a/coretk/coretk/wallpaper/sample1-bg.gif b/coretk/coretk/backgrounds/sample1-bg.gif similarity index 100% rename from coretk/coretk/wallpaper/sample1-bg.gif rename to coretk/coretk/backgrounds/sample1-bg.gif diff --git a/coretk/coretk/wallpaper/sample4-bg.jpg b/coretk/coretk/backgrounds/sample4-bg.jpg similarity index 100% rename from coretk/coretk/wallpaper/sample4-bg.jpg rename to coretk/coretk/backgrounds/sample4-bg.jpg diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 7a7c0331..f9b5f870 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -1,13 +1,10 @@ import logging -import os from enum import Enum from PIL import Image, ImageTk from core.api.grpc import core_pb2 - -PATH = os.path.abspath(os.path.dirname(__file__)) -ICONS_DIR = os.path.join(PATH, "icons") +from coretk.appdirs import LOCAL_ICONS_PATH class Images: @@ -15,14 +12,11 @@ class Images: @classmethod def load_all(cls): - for file_name in os.listdir(ICONS_DIR): - file_path = os.path.join(ICONS_DIR, file_name) - name = file_name.split(".")[0] - cls.load(name, file_path) + for image in LOCAL_ICONS_PATH.glob("*"): + cls.load(image.stem, str(image)) @classmethod def load(cls, name, file_path): - # file_path = os.path.join(PATH, file_path) image = Image.open(file_path) tk_image = ImageTk.PhotoImage(image) cls.images[name] = tk_image diff --git a/coretk/coretk/setwallpaper.py b/coretk/coretk/setwallpaper.py index edace83c..81932208 100644 --- a/coretk/coretk/setwallpaper.py +++ b/coretk/coretk/setwallpaper.py @@ -3,14 +3,12 @@ set wallpaper """ import enum import logging -import os import tkinter as tk from tkinter import filedialog from PIL import Image, ImageTk -PATH = os.path.abspath(os.path.dirname(__file__)) -WALLPAPER_DIR = os.path.join(PATH, "wallpaper") +from coretk.appdirs import BACKGROUNDS_PATH class ScaleOption(enum.Enum): @@ -60,7 +58,7 @@ class CanvasWallpaper: def open_image_link(self): filename = filedialog.askopenfilename( - initialdir=WALLPAPER_DIR, + initialdir=str(BACKGROUNDS_PATH), title="Open", filetypes=( ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), @@ -207,7 +205,6 @@ class CanvasWallpaper: cropy = img_h = tk_img.height() if img_w > canvas_w: - cropx -= img_w - canvas_w if img_h > canvas_h: cropy -= img_h - canvas_h From b4f4ecd93d56157dbb29026a45e0bdf10f81a23c Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 Nov 2019 16:47:30 -0700 Subject: [PATCH 157/462] avoid issues when open/save xml provides no value --- coretk/coretk/menuaction.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 7598f210..bfbb5c5d 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -7,13 +7,12 @@ import webbrowser from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 +from coretk.appdirs import XML_PATH from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog from coretk.setwallpaper import CanvasWallpaper from coretk.sizeandscale import SizeAndScale -SAVEDIR = "/home/ncs/Desktop/" - def sub_menu_items(): logging.debug("Click on sub menu items") @@ -361,24 +360,26 @@ class MenuAction: def file_save_as_xml(self): logging.info("menuaction.py file_save_as_xml()") file_path = filedialog.asksaveasfilename( - initialdir=SAVEDIR, + initialdir=str(XML_PATH), title="Save As", filetypes=(("EmulationScript XML files", "*.xml"), ("All files", "*")), defaultextension=".xml", ) - self.app.core.save_xml(file_path) + if file_path: + self.app.core.save_xml(file_path) def file_open_xml(self): logging.info("menuaction.py file_open_xml()") self.app.is_open_xml = True file_path = filedialog.askopenfilename( - initialdir=SAVEDIR, + initialdir=str(XML_PATH), title="Open", filetypes=(("EmulationScript XML File", "*.xml"), ("All Files", "*")), ) - # clean up before opening a new session - self.prompt_save_running_session() - self.app.core.open_xml(file_path) + if file_path: + logging.info("opening xml: %s", file_path) + self.prompt_save_running_session() + self.app.core.open_xml(file_path) # Todo might not need # self.application.core_editbar.destroy_children_widgets() From 09e18889b0605b4c0ded372ffa766bdb0c3cfb8c Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 Nov 2019 18:14:36 -0700 Subject: [PATCH 158/462] start to hooks dialog and hook dialog create/edit --- coretk/coretk/coremenubar.py | 2 +- coretk/coretk/dialogs/hooks.py | 106 +++++++++++++++++++++++++++++++++ coretk/coretk/menuaction.py | 10 ++-- 3 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 coretk/coretk/dialogs/hooks.py diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index 7b3f2633..0751ee06 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -607,7 +607,7 @@ class CoreMenubar(object): label="Comments...", command=action.session_comments, underline=0 ) session_menu.add_command( - label="Hooks...", command=action.session_hooks, underline=0 + label="Hooks...", command=self.menu_action.session_hooks, underline=0 ) session_menu.add_command( label="Reset node positions", diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py new file mode 100644 index 00000000..bdf96d33 --- /dev/null +++ b/coretk/coretk/dialogs/hooks.py @@ -0,0 +1,106 @@ +import tkinter as tk +from tkinter import ttk + +from core.api.grpc import core_pb2 +from coretk.dialogs.dialog import Dialog + + +class HookDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Hook", modal=True) + self.name = tk.StringVar() + self.data = None + self.hook = None + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + + # name and states + frame = tk.Frame(self) + frame.grid(row=0, sticky="ew", pady=2) + frame.columnconfigure(0, weight=2) + frame.columnconfigure(1, weight=7) + frame.columnconfigure(2, weight=1) + label = tk.Label(frame, text="Name") + label.grid(row=0, column=0, sticky="ew") + entry = tk.Entry(frame, textvariable=self.name) + entry.grid(row=0, column=1, sticky="ew") + combobox = ttk.Combobox(frame, values=("DEFINITION", "CONFIGURATION")) + combobox.grid(row=0, column=2, sticky="ew") + + # data + self.data = tk.Text(self) + self.data.grid(row=1, sticky="nsew", pady=2) + + # button row + frame = tk.Frame(self) + frame.grid(row=2, sticky="ew", pady=2) + for i in range(2): + frame.columnconfigure(i, weight=1) + button = tk.Button(frame, text="Save", command=lambda: self.save()) + button.grid(row=0, column=0, sticky="ew") + button = tk.Button(frame, text="Cancel", command=lambda: self.destroy()) + button.grid(row=0, column=1, sticky="ew") + + def set(self, hook): + self.hook = hook + self.name.set(hook.file) + self.data.delete(1.0, tk.END) + self.data.insert(tk.END, hook.data) + + def save(self): + data = self.data.get("1.0", tk.END).strip() + self.hook = core_pb2.Hook(file=self.name.get(), data=data) + self.destroy() + + +class HooksDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Hooks", modal=True) + self.listbox = None + self.edit = None + self.delete = None + self.selected = None + self.hooks = {} + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.listbox = tk.Listbox(self) + self.listbox.grid(row=0, sticky="ew") + self.listbox.bind("<>", self.select) + frame = tk.Frame(self) + frame.grid(row=1, sticky="ew") + for i in range(4): + frame.columnconfigure(i, weight=1) + button = tk.Button(frame, text="Create", command=self.click_create) + button.grid(row=0, column=0, sticky="ew") + self.edit = tk.Button( + frame, text="Edit", state=tk.DISABLED, command=self.click_edit + ) + self.edit.grid(row=0, column=1, sticky="ew") + self.delete = tk.Button(frame, text="Delete", state=tk.DISABLED) + self.delete.grid(row=0, column=2, sticky="ew") + button = tk.Button(frame, text="Cancel", command=lambda: self.destroy()) + button.grid(row=0, column=3, sticky="ew") + + def click_create(self): + dialog = HookDialog(self, self.app) + dialog.show() + hook = dialog.hook + if hook: + self.hooks[hook.file] = hook + self.listbox.insert(tk.END, hook.file) + + def click_edit(self): + hook = self.hooks[self.selected] + dialog = HookDialog(self, self.app) + dialog.set(hook) + dialog.show() + + def select(self, event): + self.edit.config(state=tk.NORMAL) + self.delete.config(state=tk.NORMAL) + index = self.listbox.curselection()[0] + self.selected = self.listbox.get(index) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index bfbb5c5d..64caf8ae 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -8,6 +8,7 @@ from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 from coretk.appdirs import XML_PATH +from coretk.dialogs.hooks import HooksDialog from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog from coretk.setwallpaper import CanvasWallpaper @@ -299,10 +300,6 @@ def session_comments(): logging.debug("Click session comments") -def session_hooks(): - logging.debug("Click session hooks") - - def session_reset_node_positions(): logging.debug("Click session reset node positions") @@ -406,3 +403,8 @@ class MenuAction: logging.debug("Click session change sessions") dialog = SessionsDialog(self.app, self.app) dialog.show() + + def session_hooks(self): + logging.debug("Click session hooks") + dialog = HooksDialog(self.app, self.app) + dialog.show() From c947d8c6c240f6329cc9d9e1a8f3642ad05d68e1 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 2 Nov 2019 10:29:16 -0700 Subject: [PATCH 159/462] update to hooks dialog to leverage grpc, allows for getting hooks and creation --- coretk/coretk/dialogs/hooks.py | 67 +++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index bdf96d33..a4195246 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -1,3 +1,4 @@ +import logging import tkinter as tk from tkinter import ttk @@ -11,10 +12,12 @@ class HookDialog(Dialog): self.name = tk.StringVar() self.data = None self.hook = None + self.state = tk.StringVar() self.draw() def draw(self): self.columnconfigure(0, weight=1) + self.rowconfigure(1, weight=1) # name and states frame = tk.Frame(self) @@ -26,12 +29,33 @@ class HookDialog(Dialog): label.grid(row=0, column=0, sticky="ew") entry = tk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=1, sticky="ew") - combobox = ttk.Combobox(frame, values=("DEFINITION", "CONFIGURATION")) + values = tuple(x for x in core_pb2.SessionState.Enum.keys() if x != "NONE") + initial_state = core_pb2.SessionState.Enum.Name(core_pb2.SessionState.RUNTIME) + self.state.set(initial_state) + self.name.set(f"{initial_state.lower()}_hook.sh") + combobox = ttk.Combobox(frame, textvariable=self.state, values=values) combobox.grid(row=0, column=2, sticky="ew") + combobox.bind("<>", self.state_change) # data - self.data = tk.Text(self) - self.data.grid(row=1, sticky="nsew", pady=2) + frame = tk.Frame(self) + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + frame.grid(row=1, sticky="nsew", pady=2) + self.data = tk.Text(frame) + self.data.insert( + 1.0, + ( + "#!/bin/sh\n" + "# session hook script; write commands here to execute on the host at the\n" + "# specified state\n" + ), + ) + self.data.grid(row=0, column=0, sticky="nsew") + scrollbar = tk.Scrollbar(frame) + scrollbar.grid(row=0, column=1, sticky="ns") + self.data.config(yscrollcommand=scrollbar.set) + scrollbar.config(command=self.data.yview) # button row frame = tk.Frame(self) @@ -43,15 +67,26 @@ class HookDialog(Dialog): button = tk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=1, sticky="ew") + def state_change(self, event): + state_name = self.state.get() + self.name.set(f"{state_name.lower()}_hook.sh") + def set(self, hook): self.hook = hook self.name.set(hook.file) self.data.delete(1.0, tk.END) self.data.insert(tk.END, hook.data) + state_name = core_pb2.SessionState.Enum.Name(hook.state) + self.state.set(state_name) def save(self): data = self.data.get("1.0", tk.END).strip() - self.hook = core_pb2.Hook(file=self.name.get(), data=data) + state_value = core_pb2.SessionState.Enum.Value(self.state.get()) + self.hook = core_pb2.Hook(state=state_value, file=self.name.get(), data=data) + response = self.app.core.client.add_hook( + self.app.core.session_id, self.hook.state, self.hook.file, self.hook.data + ) + logging.info("add hook: %s", response) self.destroy() @@ -59,29 +94,35 @@ class HooksDialog(Dialog): def __init__(self, master, app): super().__init__(master, app, "Hooks", modal=True) self.listbox = None - self.edit = None - self.delete = None + self.edit_button = None + self.delete_button = None self.selected = None self.hooks = {} self.draw() def draw(self): + response = self.app.core.client.get_hooks(self.app.core.session_id) + logging.info("get hooks: %s", response) self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) self.listbox = tk.Listbox(self) - self.listbox.grid(row=0, sticky="ew") + self.listbox.grid(row=0, sticky="nsew") self.listbox.bind("<>", self.select) + for hook in response.hooks: + self.hooks[hook.file] = hook + self.listbox.insert(tk.END, hook.file) frame = tk.Frame(self) frame.grid(row=1, sticky="ew") for i in range(4): frame.columnconfigure(i, weight=1) button = tk.Button(frame, text="Create", command=self.click_create) button.grid(row=0, column=0, sticky="ew") - self.edit = tk.Button( + self.edit_button = tk.Button( frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) - self.edit.grid(row=0, column=1, sticky="ew") - self.delete = tk.Button(frame, text="Delete", state=tk.DISABLED) - self.delete.grid(row=0, column=2, sticky="ew") + self.edit_button.grid(row=0, column=1, sticky="ew") + self.delete_button = tk.Button(frame, text="Delete", state=tk.DISABLED) + self.delete_button.grid(row=0, column=2, sticky="ew") button = tk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=3, sticky="ew") @@ -100,7 +141,7 @@ class HooksDialog(Dialog): dialog.show() def select(self, event): - self.edit.config(state=tk.NORMAL) - self.delete.config(state=tk.NORMAL) + self.edit_button.config(state=tk.NORMAL) + self.delete_button.config(state=tk.NORMAL) index = self.listbox.curselection()[0] self.selected = self.listbox.get(index) From 8c1b70822e1fb0ff987b3c2db102593eb1d604e4 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 2 Nov 2019 14:34:00 -0700 Subject: [PATCH 160/462] fixed how hooks get created and sent to grpc StartSession, also query hooks when joining a session --- coretk/coretk/coreclient.py | 35 +++++++++++++++++++++++----------- coretk/coretk/dialogs/hooks.py | 23 ++++++++-------------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index e5ba838e..ad912422 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -71,6 +71,7 @@ class CoreClient: # data for managing the current session self.nodes = {} self.edges = {} + self.hooks = {} self.id = 1 self.reusable = [] self.preexisting = set() @@ -97,19 +98,30 @@ class CoreClient: ) def join_session(self, session_id): - # query session and set as current session + # update session and title self.session_id = session_id - response = self.client.get_session(self.session_id) - logging.info("joining session(%s): %s", self.session_id, response) - self.client.events(self.session_id, self.handle_events) - - # set title to session self.master.title(f"CORE Session({self.session_id})") - # determine next node id and reusable nodes - session = response.session + # clear session data self.reusable.clear() self.preexisting.clear() + self.nodes.clear() + self.edges.clear() + self.hooks.clear() + + # get session data + response = self.client.get_session(self.session_id) + logging.info("joining session(%s): %s", self.session_id, response) + session = response.session + self.client.events(self.session_id, self.handle_events) + + # get hooks + response = self.client.get_hooks(self.session_id) + logging.info("joined session hooks: %s", response) + for hook in response.hooks: + self.hooks[hook.file] = hook + + # determine next node id and reusable nodes max_id = 1 for node in session.nodes: if node.id > max_id: @@ -262,9 +274,10 @@ class CoreClient: mobility_configs=None, ): response = self.client.start_session( - session_id=self.session_id, - nodes=nodes, - links=links, + self.session_id, + nodes, + links, + hooks=list(self.hooks.values()), wlan_configs=wlan_configs, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index a4195246..7e1338ad 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -1,4 +1,3 @@ -import logging import tkinter as tk from tkinter import ttk @@ -11,7 +10,7 @@ class HookDialog(Dialog): super().__init__(master, app, "Hook", modal=True) self.name = tk.StringVar() self.data = None - self.hook = None + self.hook = core_pb2.Hook() self.state = tk.StringVar() self.draw() @@ -82,11 +81,9 @@ class HookDialog(Dialog): def save(self): data = self.data.get("1.0", tk.END).strip() state_value = core_pb2.SessionState.Enum.Value(self.state.get()) - self.hook = core_pb2.Hook(state=state_value, file=self.name.get(), data=data) - response = self.app.core.client.add_hook( - self.app.core.session_id, self.hook.state, self.hook.file, self.hook.data - ) - logging.info("add hook: %s", response) + self.hook.file = self.name.get() + self.hook.data = data + self.hook.state = state_value self.destroy() @@ -97,20 +94,16 @@ class HooksDialog(Dialog): self.edit_button = None self.delete_button = None self.selected = None - self.hooks = {} self.draw() def draw(self): - response = self.app.core.client.get_hooks(self.app.core.session_id) - logging.info("get hooks: %s", response) self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.listbox = tk.Listbox(self) self.listbox.grid(row=0, sticky="nsew") self.listbox.bind("<>", self.select) - for hook in response.hooks: - self.hooks[hook.file] = hook - self.listbox.insert(tk.END, hook.file) + for hook_file in self.app.core.hooks: + self.listbox.insert(tk.END, hook_file) frame = tk.Frame(self) frame.grid(row=1, sticky="ew") for i in range(4): @@ -131,11 +124,11 @@ class HooksDialog(Dialog): dialog.show() hook = dialog.hook if hook: - self.hooks[hook.file] = hook + self.app.core.hooks[hook.file] = hook self.listbox.insert(tk.END, hook.file) def click_edit(self): - hook = self.hooks[self.selected] + hook = self.app.core.hooks[self.selected] dialog = HookDialog(self, self.app) dialog.set(hook) dialog.show() From b991dc0242387b4b9f17b519777983f3e3a3641f Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 2 Nov 2019 14:46:30 -0700 Subject: [PATCH 161/462] updates to handle delete hook and button states --- coretk/coretk/dialogs/hooks.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index 7e1338ad..95ca8af7 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -114,7 +114,9 @@ class HooksDialog(Dialog): frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) self.edit_button.grid(row=0, column=1, sticky="ew") - self.delete_button = tk.Button(frame, text="Delete", state=tk.DISABLED) + self.delete_button = tk.Button( + frame, text="Delete", state=tk.DISABLED, command=self.click_delete + ) self.delete_button.grid(row=0, column=2, sticky="ew") button = tk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=3, sticky="ew") @@ -133,8 +135,19 @@ class HooksDialog(Dialog): dialog.set(hook) dialog.show() + def click_delete(self): + del self.app.core.hooks[self.selected] + self.listbox.delete(tk.ANCHOR) + self.edit_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) + def select(self, event): - self.edit_button.config(state=tk.NORMAL) - self.delete_button.config(state=tk.NORMAL) - index = self.listbox.curselection()[0] - self.selected = self.listbox.get(index) + if self.listbox.curselection(): + index = self.listbox.curselection()[0] + self.selected = self.listbox.get(index) + self.edit_button.config(state=tk.NORMAL) + self.delete_button.config(state=tk.NORMAL) + else: + self.selected = None + self.edit_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) From eabca7dfcf10fbc0e4f2db22bf0d023674779171 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Sat, 2 Nov 2019 21:59:29 -0700 Subject: [PATCH 162/462] work on wlan configuration --- coretk/coretk/custom_node.py | 19 +++ coretk/coretk/dialogs/mobilityconfig.py | 210 ++++++++++++++++++++++++ coretk/coretk/images.py | 4 +- coretk/coretk/wlanconfiguration.py | 13 +- 4 files changed, 243 insertions(+), 3 deletions(-) create mode 100644 coretk/coretk/custom_node.py create mode 100644 coretk/coretk/dialogs/mobilityconfig.py diff --git a/coretk/coretk/custom_node.py b/coretk/coretk/custom_node.py new file mode 100644 index 00000000..5cc8ea75 --- /dev/null +++ b/coretk/coretk/custom_node.py @@ -0,0 +1,19 @@ +""" +edit node types +""" + +import tkinter as tk + + +class EditNodeTypes: + def __init__(self): + self.top = tk.Toplevel + self.top.title("CORE Node Types") + + def node_types(self): + """ + list box of node types + :return: + """ + lbl = tk.Label(self.top, text="Node types") + lbl.grid() diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/coretk/coretk/dialogs/mobilityconfig.py new file mode 100644 index 00000000..25339406 --- /dev/null +++ b/coretk/coretk/dialogs/mobilityconfig.py @@ -0,0 +1,210 @@ +""" +mobility configuration +""" + +import os +import tkinter as tk +from pathlib import Path +from tkinter import filedialog + +from coretk.dialogs.dialog import Dialog + + +class MobilityConfiguration(Dialog): + def __init__(self, master, app, canvas_node): + """ + create an instance of mobility configuration + + :param app: core application + :param root.master master: + """ + super().__init__(master, app, "ns2script configuration", modal=True) + self.canvas_node = canvas_node + self.mobility_script_parameters() + self.ns2script_options() + self.loop = "On" + + def create_string_var(self, val): + """ + create string variable for entry widget + + :return: nothing + """ + var = tk.StringVar() + var.set(val) + return var + + def open_file(self, entry): + configs_dir = os.path.join(Path.home(), ".core/configs") + if os.path.isdir(configs_dir): + filename = filedialog.askopenfilename(initialdir=configs_dir, title="Open") + if filename: + entry.delete(0, tk.END) + entry.insert(0, filename) + + def set_loop_value(self, value): + """ + set loop value when user changes the option + :param value: + :return: + """ + self.loop = value + + def create_label_entry_filebrowser( + self, parent_frame, text_label, filebrowser=False + ): + f = tk.Frame(parent_frame, bg="#d9d9d9") + lbl = tk.Label(f, text=text_label, bg="#d9d9d9") + lbl.grid(padx=3, pady=3) + # f.grid() + e = tk.Entry(f, textvariable=self.create_string_var(""), bg="#ffffff") + e.grid(row=0, column=1, padx=3, pady=3) + if filebrowser: + b = tk.Button(f, text="...", command=lambda: self.open_file(e)) + b.grid(row=0, column=2, padx=3, pady=3) + f.grid(sticky=tk.E) + + def mobility_script_parameters(self): + lbl = tk.Label(self, text="node ns2script") + lbl.grid(sticky=tk.W + tk.E) + + sb = tk.Scrollbar(self, orient=tk.VERTICAL) + sb.grid(row=1, column=1, sticky=tk.N + tk.S + tk.E) + + f = tk.Frame(self, bg="#d9d9d9") + lbl = tk.Label( + f, text="ns-2 Mobility Scripts Parameters", bg="#d9d9d9", relief=tk.RAISED + ) + lbl.grid(row=0, column=0, sticky=tk.W) + + f1 = tk.Canvas( + f, + yscrollcommand=sb.set, + bg="#d9d9d9", + relief=tk.RAISED, + highlightbackground="#b3b3b3", + highlightcolor="#b3b3b3", + highlightthickness=0.5, + bd=0, + ) + self.create_label_entry_filebrowser( + f1, "mobility script file", filebrowser=True + ) + self.create_label_entry_filebrowser(f1, "Refresh time (ms)") + + # f12 = tk.Frame(f1) + # + # lbl = tk.Label(f12, text="Refresh time (ms)") + # lbl.grid() + # + # e = tk.Entry(f12, textvariable=self.create_string_var("50")) + # e.grid(row=0, column=1) + # f12.grid() + + f13 = tk.Frame(f1) + + lbl = tk.Label(f13, text="loop") + lbl.grid() + + om = tk.OptionMenu( + f13, + self.create_string_var("On"), + "On", + "Off", + command=self.set_loop_value, + indicatoron=True, + ) + om.grid(row=0, column=1) + + f13.grid(sticky=tk.E) + + self.create_label_entry_filebrowser(f1, "auto-start seconds (0.0 for runtime)") + # f14 = tk.Frame(f1) + # + # lbl = tk.Label(f14, text="auto-start seconds (0.0 for runtime)") + # lbl.grid() + # + # e = tk.Entry(f14, textvariable=self.create_string_var("")) + # e.grid(row=0, column=1) + # + # f14.grid() + self.create_label_entry_filebrowser( + f1, "node mapping (optional, e.g. 0:1, 1:2, 2:3)" + ) + # f15 = tk.Frame(f1) + # + # lbl = tk.Label(f15, text="node mapping (optional, e.g. 0:1, 1:2, 2:3)") + # lbl.grid() + # + # e = tk.Entry(f15, textvariable=self.create_string_var("")) + # e.grid(row=0, column=1) + # + # f15.grid() + + self.create_label_entry_filebrowser( + f1, "script file to run upon start", filebrowser=True + ) + self.create_label_entry_filebrowser( + f1, "script file to run upon pause", filebrowser=True + ) + self.create_label_entry_filebrowser( + f1, "script file to run upon stop", filebrowser=True + ) + f1.grid() + sb.config(command=f1.yview) + f.grid(row=1, column=0) + + def ns2script_apply(self): + """ + + :return: + """ + config_frame = self.grid_slaves(row=1, column=0)[0] + canvas = config_frame.grid_slaves(row=1, column=0)[0] + file = ( + canvas.grid_slaves(row=0, column=0)[0].grid_slaves(row=0, column=1)[0].get() + ) + + refresh_time = ( + canvas.grid_slaves(row=1, column=0)[0].grid_slaves(row=0, column=1)[0].get() + ) + auto_start_seconds = ( + canvas.grid_slaves(row=3, column=0)[0].grid_slaves(row=0, column=1)[0].get() + ) + + node_mapping = ( + canvas.grid_slaves(row=4, column=0)[0].grid_slaves(row=0, column=1)[0].get() + ) + + file_upon_start = ( + canvas.grid_slaves(row=5, column=0)[0].grid_slaves(row=0, column=1)[0].get() + ) + file_upon_pause = ( + canvas.grid_slaves(row=6, column=0)[0].grid_slaves(row=0, column=1)[0].get() + ) + file_upon_stop = ( + canvas.grid_slaves(row=7, column=0)[0].grid_slaves(row=0, column=1)[0].get() + ) + + print("mobility script file: ", file) + print("refresh time: ", refresh_time) + print("auto start seconds: ", auto_start_seconds) + print("node mapping: ", node_mapping) + print("script file to run upon start: ", file_upon_start) + print("file upon pause: ", file_upon_pause) + print("file upon stop: ", file_upon_stop) + + self.destroy() + + def ns2script_options(self): + """ + create the options for ns2script configuration + + :return: nothing + """ + f = tk.Frame(self) + b = tk.Button(f, text="Apply", command=self.ns2script_apply) + b.grid() + b = tk.Button(f, text="Cancel", command=self.destroy) + b.grid(row=0, column=1) + f.grid() diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 1042042d..b22afa49 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -39,9 +39,9 @@ class Images: if node_type == core_pb2.NodeType.HUB: return Images.get(ImageEnum.HUB), "hub" if node_type == core_pb2.NodeType.WIRELESS_LAN: - return Images.get(ImageEnum.WLAN.value), "wlan" + return Images.get(ImageEnum.WLAN), "wlan" if node_type == core_pb2.NodeType.EMANE: - return Images.get(ImageEnum.EMANE.value), "emane" + return Images.get(ImageEnum.EMANE), "emane" if node_type == core_pb2.NodeType.RJ45: return Images.get(ImageEnum.RJ45), "rj45" diff --git a/coretk/coretk/wlanconfiguration.py b/coretk/coretk/wlanconfiguration.py index 32bb56e2..6e028084 100644 --- a/coretk/coretk/wlanconfiguration.py +++ b/coretk/coretk/wlanconfiguration.py @@ -5,6 +5,7 @@ wlan configuration import tkinter as tk from functools import partial +from coretk.dialogs.mobilityconfig import MobilityConfiguration from coretk.imagemodification import ImageModification @@ -224,6 +225,12 @@ class WlanConfiguration: f2.grid() f.grid(sticky=tk.W, padx=3, pady=3) + def click_ns2_mobility_script(self): + dialog = MobilityConfiguration( + self.top, self.canvas.core_grpc.app, self.canvas_node + ) + dialog.show() + def wlan_options(self): """ create wireless node options @@ -231,7 +238,11 @@ class WlanConfiguration: :return: """ f = tk.Frame(self.top) - b = tk.Button(f, text="ns-2 mobility script...") + b = tk.Button( + f, + text="ns-2 mobility script...", + command=lambda: self.click_ns2_mobility_script(), + ) b.pack(side=tk.LEFT, padx=1) b = tk.Button(f, text="Link to all routers") b.pack(side=tk.LEFT, padx=1) From d4f77a01e30c559f765a29fc98648a85701ef8f0 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 2 Nov 2019 23:47:43 -0700 Subject: [PATCH 163/462] moved all modules creating dialogs under dialogs package, updated node config and node icon dialogs to use common dialog class, fixed common dialog class show order to fix delayed cases with grab_set --- coretk/coretk/canvasaction.py | 8 +- coretk/coretk/dialogs/dialog.py | 5 +- coretk/coretk/dialogs/nodeconfig.py | 94 ++++++++++ coretk/coretk/dialogs/nodeicon.py | 69 ++++++++ coretk/coretk/{ => dialogs}/nodeservice.py | 0 coretk/coretk/{ => dialogs}/setwallpaper.py | 0 coretk/coretk/{ => dialogs}/sizeandscale.py | 2 +- .../wlanconfig.py} | 13 +- coretk/coretk/imagemodification.py | 91 ---------- coretk/coretk/images.py | 8 +- coretk/coretk/menuaction.py | 4 +- coretk/coretk/nodeconfigtable.py | 164 ------------------ 12 files changed, 184 insertions(+), 274 deletions(-) create mode 100644 coretk/coretk/dialogs/nodeconfig.py create mode 100644 coretk/coretk/dialogs/nodeicon.py rename coretk/coretk/{ => dialogs}/nodeservice.py (100%) rename coretk/coretk/{ => dialogs}/setwallpaper.py (100%) rename coretk/coretk/{ => dialogs}/sizeandscale.py (99%) rename coretk/coretk/{wlanconfiguration.py => dialogs/wlanconfig.py} (97%) delete mode 100644 coretk/coretk/imagemodification.py delete mode 100644 coretk/coretk/nodeconfigtable.py diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py index 0c56d4ba..b5f09333 100644 --- a/coretk/coretk/canvasaction.py +++ b/coretk/coretk/canvasaction.py @@ -5,8 +5,8 @@ canvas graph action # import tkinter as tk from core.api.grpc import core_pb2 -from coretk.nodeconfigtable import NodeConfig -from coretk.wlanconfiguration import WlanConfiguration +from coretk.dialogs.nodeconfig import NodeConfigDialog +from coretk.dialogs.wlanconfig import WlanConfiguration # TODO, finish classifying node types NODE_TO_TYPE = { @@ -18,7 +18,6 @@ NODE_TO_TYPE = { class CanvasAction: def __init__(self, master, canvas): self.master = master - self.canvas = canvas self.node_to_show_config = None @@ -31,7 +30,8 @@ class CanvasAction: self.display_wlan_configuration(canvas_node) def display_node_configuration(self): - NodeConfig(self.canvas, self.node_to_show_config) + dialog = NodeConfigDialog(self.master, self.master, self.node_to_show_config) + dialog.show() self.node_to_show_config = None def display_wlan_configuration(self, canvas_node): diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index b218d8c7..f9cfcabe 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -17,8 +17,9 @@ class Dialog(tk.Toplevel): def show(self): self.transient(self.master) self.focus_force() - if self.modal: - self.grab_set() self.update() self.deiconify() + if self.modal: + self.wait_visibility() + self.grab_set() self.wait_window() diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py new file mode 100644 index 00000000..d670853c --- /dev/null +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -0,0 +1,94 @@ +import tkinter as tk +from tkinter import ttk + +from coretk.dialogs.dialog import Dialog +from coretk.dialogs.nodeicon import NodeIconDialog +from coretk.dialogs.nodeservice import NodeServices + +NETWORKNODETYPES = ["switch", "hub", "wlan", "rj45", "tunnel"] +DEFAULTNODES = ["router", "host", "PC"] + + +class NodeConfigDialog(Dialog): + def __init__(self, master, app, canvas_node): + """ + create an instance of node configuration + + :param master: dialog master + :param coretk.app.Application: main app + :param coretk.graph.CanvasNode canvas_node: canvas node object + """ + super().__init__(master, app, f"{canvas_node.name} Configuration", modal=True) + self.canvas_node = canvas_node + self.image = canvas_node.image + self.image_button = None + self.name = tk.StringVar(value=canvas_node.name) + self.type = tk.StringVar(value=canvas_node.node_type) + self.server = tk.StringVar() + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.draw_first_row() + self.draw_second_row() + self.draw_third_row() + + def draw_first_row(self): + frame = tk.Frame(self) + frame.grid(row=0, column=0, pady=2, sticky="ew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + + entry = tk.Entry(frame, textvariable=self.name) + entry.grid(row=0, column=0, padx=2, sticky="ew") + + combobox = ttk.Combobox(frame, textvariable=self.type, values=DEFAULTNODES) + combobox.grid(row=0, column=1, padx=2, sticky="ew") + + combobox = ttk.Combobox(frame, textvariable=self.server, values=["localhost"]) + combobox.current(0) + combobox.grid(row=0, column=2, sticky="ew") + + def draw_second_row(self): + frame = tk.Frame(self) + frame.grid(row=1, column=0, pady=2, sticky="ew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + + button = tk.Button(frame, text="Services", command=lambda: NodeServices()) + button.grid(row=0, column=0, padx=2, sticky="ew") + + self.image_button = tk.Button( + frame, + text="Icon", + image=self.image, + compound=tk.LEFT, + command=self.click_icon, + ) + self.image_button.grid(row=0, column=1, sticky="ew") + + def draw_third_row(self): + frame = tk.Frame(self) + frame.grid(row=2, column=0, sticky="ew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + + button = tk.Button(frame, text="Apply", command=self.config_apply) + button.grid(row=0, column=0, padx=2, sticky="ew") + + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_icon(self): + dialog = NodeIconDialog(self, self.app, self.canvas_node) + dialog.show() + if dialog.image: + self.image = dialog.image + self.image_button.config(image=self.image) + + def config_apply(self): + self.canvas_node.name = self.name.get() + self.canvas_node.image = self.image + self.canvas_node.canvas.itemconfig(self.canvas_node.id, image=self.image) + self.destroy() diff --git a/coretk/coretk/dialogs/nodeicon.py b/coretk/coretk/dialogs/nodeicon.py new file mode 100644 index 00000000..d82c0756 --- /dev/null +++ b/coretk/coretk/dialogs/nodeicon.py @@ -0,0 +1,69 @@ +import tkinter as tk +from tkinter import filedialog + +from coretk.appdirs import ICONS_PATH +from coretk.dialogs.dialog import Dialog +from coretk.images import Images + + +class NodeIconDialog(Dialog): + def __init__(self, master, app, canvas_node): + """ + create an instance of ImageModification + :param master: dialog master + :param coretk.app.Application: main app + :param coretk.graph.CanvasNode canvas_node: node object + """ + super().__init__(master, app, f"{canvas_node.name} Icon", modal=True) + self.file_path = tk.StringVar() + self.image_label = None + self.image = canvas_node.image + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + + # row one + frame = tk.Frame(self) + frame.grid(row=0, column=0, pady=2, sticky="ew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=3) + label = tk.Label(frame, text="Image") + label.grid(row=0, column=0, sticky="ew") + entry = tk.Entry(frame, textvariable=self.file_path) + entry.grid(row=0, column=1, sticky="ew") + button = tk.Button(frame, text="...", command=self.click_file) + button.grid(row=0, column=2) + + # row two + self.image_label = tk.Label(self, image=self.image) + self.image_label.grid(row=1, column=0, pady=2, sticky="ew") + + # row three + frame = tk.Frame(self) + frame.grid(row=2, column=0, sticky="ew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + button = tk.Button(frame, text="Apply", command=self.destroy) + button.grid(row=0, column=0, sticky="ew") + + button = tk.Button(frame, text="Cancel", command=self.click_cancel) + button.grid(row=0, column=1, sticky="ew") + + def click_file(self): + file_path = filedialog.askopenfilename( + initialdir=str(ICONS_PATH), + title="Open", + filetypes=( + ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), + ("All Files", "*"), + ), + ) + if file_path: + self.image = Images.create(file_path) + self.image_label.config(image=self.image) + self.file_path.set(file_path) + + def click_cancel(self): + self.image = None + self.destroy() diff --git a/coretk/coretk/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py similarity index 100% rename from coretk/coretk/nodeservice.py rename to coretk/coretk/dialogs/nodeservice.py diff --git a/coretk/coretk/setwallpaper.py b/coretk/coretk/dialogs/setwallpaper.py similarity index 100% rename from coretk/coretk/setwallpaper.py rename to coretk/coretk/dialogs/setwallpaper.py diff --git a/coretk/coretk/sizeandscale.py b/coretk/coretk/dialogs/sizeandscale.py similarity index 99% rename from coretk/coretk/sizeandscale.py rename to coretk/coretk/dialogs/sizeandscale.py index 5c746c39..0267a300 100644 --- a/coretk/coretk/sizeandscale.py +++ b/coretk/coretk/dialogs/sizeandscale.py @@ -4,7 +4,7 @@ size and scale import tkinter as tk from functools import partial -from coretk.setwallpaper import ScaleOption +from coretk.dialogs.setwallpaper import ScaleOption DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] diff --git a/coretk/coretk/wlanconfiguration.py b/coretk/coretk/dialogs/wlanconfig.py similarity index 97% rename from coretk/coretk/wlanconfiguration.py rename to coretk/coretk/dialogs/wlanconfig.py index 32bb56e2..8065a5fa 100644 --- a/coretk/coretk/wlanconfiguration.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -5,7 +5,7 @@ wlan configuration import tkinter as tk from functools import partial -from coretk.imagemodification import ImageModification +from coretk.dialogs.nodeicon import NodeIconDialog class WlanConfiguration: @@ -57,16 +57,13 @@ class WlanConfiguration: e.grid(row=0, column=1, padx=3, pady=3) b = tk.Button(f, text="None") b.grid(row=0, column=2, padx=3, pady=3) - b = tk.Button( - f, - image=self.image, - command=lambda: ImageModification( - canvas=self.canvas, canvas_node=self.canvas_node, node_config=self - ), - ) + b = tk.Button(f, image=self.image, command=lambda: self.click_image) b.grid(row=0, column=3, padx=3, pady=3) f.grid(padx=2, pady=2, ipadx=2, ipady=2) + def click_image(self): + NodeIconDialog(self.app, canvas_node=self.canvas_node, node_config=self) + def create_string_var(self, val): """ create string variable for convenience diff --git a/coretk/coretk/imagemodification.py b/coretk/coretk/imagemodification.py deleted file mode 100644 index 646f31b7..00000000 --- a/coretk/coretk/imagemodification.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -node image modification -""" - - -import os -import tkinter as tk -from tkinter import filedialog - -from PIL import Image, ImageTk - -PATH = os.path.abspath(os.path.dirname(__file__)) -ICONS_DIR = os.path.join(PATH, "icons") - - -class ImageModification: - def __init__(self, canvas, canvas_node, node_config): - """ - create an instance of ImageModification - :param coretk.graph.CanvasGraph canvas: canvas object - :param coretk.graph.CanvasNode canvas_node: node object - :param coretk.nodeconfigtable.NodeConfig node_config: node configuration object - """ - self.canvas = canvas - self.image = canvas_node.image - self.node_type = canvas_node.node_type - self.name = canvas_node.name - self.canvas_node = canvas_node - self.node_configuration = node_config - self.p_top = node_config.top - - self.top = tk.Toplevel() - self.top.title(self.name + " image") - self.image_modification() - - def open_icon_dir(self, toplevel, entry_text): - filename = filedialog.askopenfilename( - initialdir=ICONS_DIR, - title="Open", - filetypes=( - ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), - ("All Files", "*"), - ), - ) - if len(filename) > 0: - img = Image.open(filename) - tk_img = ImageTk.PhotoImage(img) - lb = toplevel.grid_slaves(1, 0)[0] - lb.configure(image=tk_img) - lb.image = tk_img - entry_text.set(filename) - - def click_apply(self, toplevel, entry_text): - imgfile = entry_text.get() - if imgfile: - img = Image.open(imgfile) - tk_img = ImageTk.PhotoImage(img) - f = self.p_top.grid_slaves(row=0, column=0)[0] - lb = f.grid_slaves(row=0, column=3)[0] - lb.configure(image=tk_img) - lb.image = tk_img - self.image = tk_img - self.node_configuration.image = tk_img - toplevel.destroy() - - def image_modification(self): - f = tk.Frame(self.top) - entry_text = tk.StringVar() - image_file_label = tk.Label(f, text="Image file: ") - image_file_label.grid(row=0, column=0) - image_file_entry = tk.Entry(f, textvariable=entry_text, width=32, bg="white") - image_file_entry.grid(row=0, column=1) - image_file_button = tk.Button( - f, text="...", command=lambda: self.open_icon_dir(self.top, entry_text) - ) - image_file_button.grid(row=0, column=2) - f.grid() - - img = tk.Label(self.top, image=self.image) - img.grid() - - f = tk.Frame(self.top) - apply_button = tk.Button( - f, text="Apply", command=lambda: self.click_apply(self.top, entry_text) - ) - apply_button.grid(row=0, column=0) - apply_to_multiple_button = tk.Button(f, text="Apply to multiple...") - apply_to_multiple_button.grid(row=0, column=1) - cancel_button = tk.Button(f, text="Cancel", command=self.top.destroy) - cancel_button.grid(row=0, column=2) - f.grid() diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index f9b5f870..768b33ba 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -10,6 +10,11 @@ from coretk.appdirs import LOCAL_ICONS_PATH class Images: images = {} + @classmethod + def create(cls, file_path): + image = Image.open(file_path) + return ImageTk.PhotoImage(image) + @classmethod def load_all(cls): for image in LOCAL_ICONS_PATH.glob("*"): @@ -17,8 +22,7 @@ class Images: @classmethod def load(cls, name, file_path): - image = Image.open(file_path) - tk_image = ImageTk.PhotoImage(image) + tk_image = cls.create(file_path) cls.images[name] = tk_image @classmethod diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 64caf8ae..1d9adca1 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -11,8 +11,8 @@ from coretk.appdirs import XML_PATH from coretk.dialogs.hooks import HooksDialog from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog -from coretk.setwallpaper import CanvasWallpaper -from coretk.sizeandscale import SizeAndScale +from coretk.dialogs.setwallpaper import CanvasWallpaper +from coretk.dialogs.sizeandscale import SizeAndScale def sub_menu_items(): diff --git a/coretk/coretk/nodeconfigtable.py b/coretk/coretk/nodeconfigtable.py deleted file mode 100644 index a1a7c1d2..00000000 --- a/coretk/coretk/nodeconfigtable.py +++ /dev/null @@ -1,164 +0,0 @@ -""" -Create toplevel for node configuration -""" -import logging -import os -import tkinter as tk -from tkinter import filedialog - -from PIL import Image, ImageTk - -from coretk.imagemodification import ImageModification -from coretk.nodeservice import NodeServices - -PATH = os.path.abspath(os.path.dirname(__file__)) -ICONS_DIR = os.path.join(PATH, "icons") - -NETWORKNODETYPES = ["switch", "hub", "wlan", "rj45", "tunnel"] -DEFAULTNODES = ["router", "host", "PC"] - - -class NodeConfig: - def __init__(self, canvas, canvas_node): - """ - create an instance of node configuration - - :param coretk.graph.CanvasGraph canvas: canvas object - :param coretk.graph.CanvasNode canvas_node: canvas node object - """ - self.canvas = canvas - self.image = canvas_node.image - self.node_type = canvas_node.node_type - self.name = canvas_node.name - self.canvas_node = canvas_node - - self.top = tk.Toplevel() - self.top.title(canvas_node.node_type + " configuration") - self.namevar = tk.StringVar(self.top, value="default name") - self.name_and_image_definition() - self.type_and_service_definition() - self.select_definition() - - def open_icon_dir(self, toplevel, entry_text): - filename = filedialog.askopenfilename( - initialdir=ICONS_DIR, - title="Open", - filetypes=( - ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), - ("All Files", "*"), - ), - ) - if len(filename) > 0: - img = Image.open(filename) - tk_img = ImageTk.PhotoImage(img) - lb = toplevel.grid_slaves(1, 0)[0] - lb.configure(image=tk_img) - lb.image = tk_img - entry_text.set(filename) - - def click_apply(self, toplevel, entry_text): - imgfile = entry_text.get() - if imgfile: - img = Image.open(imgfile) - tk_img = ImageTk.PhotoImage(img) - lb = self.top.grid_slaves(row=0, column=3)[0] - lb.configure(image=tk_img) - lb.image = tk_img - self.image = tk_img - toplevel.destroy() - - def img_modification(self): - t = tk.Toplevel() - t.title(self.name + " image") - - f = tk.Frame(t) - entry_text = tk.StringVar() - image_file_label = tk.Label(f, text="Image file: ") - image_file_label.grid(row=0, column=0) - image_file_entry = tk.Entry(f, textvariable=entry_text, width=32, bg="white") - image_file_entry.grid(row=0, column=1) - image_file_button = tk.Button( - f, text="...", command=lambda: self.open_icon_dir(t, entry_text) - ) - image_file_button.grid(row=0, column=2) - f.grid() - - img = tk.Label(t, image=self.image) - img.grid() - - f = tk.Frame(t) - apply_button = tk.Button( - f, text="Apply", command=lambda: self.click_apply(t, entry_text) - ) - apply_button.grid(row=0, column=0) - apply_to_multiple_button = tk.Button(f, text="Apply to multiple...") - apply_to_multiple_button.grid(row=0, column=1) - cancel_button = tk.Button(f, text="Cancel", command=t.destroy) - cancel_button.grid(row=0, column=2) - f.grid() - - def name_and_image_definition(self): - f = tk.Frame(self.top, bg="#d9d9d9") - name_label = tk.Label(f, text="Node name: ", bg="#d9d9d9") - name_label.grid(padx=2, pady=2) - name_entry = tk.Entry(f, textvariable=self.namevar) - name_entry.grid(row=0, column=1, padx=2, pady=2) - - core_button = tk.Button(f, text="None") - core_button.grid(row=0, column=2, padx=2, pady=2) - img_button = tk.Button( - f, - image=self.image, - width=40, - height=40, - command=lambda: ImageModification(self.canvas, self.canvas_node, self), - bg="#d9d9d9", - ) - img_button.grid(row=0, column=3, padx=4, pady=4) - f.grid(padx=4, pady=4) - - def type_and_service_definition(self): - f = tk.Frame(self.top) - type_label = tk.Label(f, text="Type: ") - type_label.grid(row=0, column=0) - - type_button = tk.Button(f, text="None") - type_button.grid(row=0, column=1) - - service_button = tk.Button( - f, text="Services...", command=lambda: NodeServices() - ) - service_button.grid(row=0, column=2) - - f.grid(padx=2, pady=2) - - def config_apply(self): - """ - modify image of the canvas node - :return: nothing - """ - logging.debug("nodeconfigtable.py configuration apply") - self.canvas_node.image = self.image - self.canvas_node.canvas.itemconfig(self.canvas_node.id, image=self.image) - self.top.destroy() - - def config_cancel(self): - """ - save chosen image but not modify canvas node - :return: nothing - """ - logging.debug("nodeconfigtable.py configuration cancel") - self.canvas_node.image = self.image - self.top.destroy() - - def select_definition(self): - f = tk.Frame(self.top) - apply_button = tk.Button(f, text="Apply", command=self.config_apply) - apply_button.grid(row=0, column=0) - cancel_button = tk.Button(f, text="Cancel", command=self.config_cancel) - cancel_button.grid(row=0, column=1) - f.grid() - - def network_node_config(self): - self.name_and_image_definition() - self.select_definition() From 91ab1b0ee6f1810e3f2d387c266576c074df3783 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Sun, 3 Nov 2019 22:58:45 -0800 Subject: [PATCH 164/462] mobility config, start emane config --- coretk/coretk/app.py | 5 +- coretk/coretk/canvasaction.py | 10 +++ coretk/coretk/coregrpc.py | 56 +++++----------- coretk/coretk/coretoolbar.py | 4 +- coretk/coretk/coretoolbarhelp.py | 35 +++++++--- coretk/coretk/dialogs/emaneconfig.py | 47 +++++++++++++ coretk/coretk/dialogs/mobilityconfig.py | 71 ++++++++++++++------ coretk/coretk/graph.py | 6 +- coretk/coretk/grpcmanagement.py | 5 ++ coretk/coretk/menuaction.py | 8 +++ coretk/coretk/mobilitynodeconfig.py | 89 +++++++++++++++++++++++++ coretk/coretk/wlanconfiguration.py | 6 +- coretk/coretk/wlannodeconfig.py | 35 +++++++++- 13 files changed, 297 insertions(+), 80 deletions(-) create mode 100644 coretk/coretk/dialogs/emaneconfig.py create mode 100644 coretk/coretk/mobilitynodeconfig.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index f253fd01..64eabd5d 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -15,7 +15,6 @@ class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) appcache.cache_variable(self) - print(self.is_open_xml) self.load_images() self.setup_app() self.menubar = None @@ -100,6 +99,10 @@ class Application(tk.Frame): self.canvas.grpc_manager.update_preexisting_ids() self.canvas.draw_existing_component() + self.canvas.grpc_manager.wlanconfig_management.load_wlan_configurations( + self.core_grpc + ) + def on_closing(self): menu_action = MenuAction(self, self.master) menu_action.on_quit() diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py index 0c56d4ba..353c40c9 100644 --- a/coretk/coretk/canvasaction.py +++ b/coretk/coretk/canvasaction.py @@ -5,6 +5,7 @@ canvas graph action # import tkinter as tk from core.api.grpc import core_pb2 +from coretk.dialogs.emaneconfig import EmaneConfiguration from coretk.nodeconfigtable import NodeConfig from coretk.wlanconfiguration import WlanConfiguration @@ -12,6 +13,7 @@ from coretk.wlanconfiguration import WlanConfiguration NODE_TO_TYPE = { "router": core_pb2.NodeType.DEFAULT, "wlan": core_pb2.NodeType.WIRELESS_LAN, + "emane": core_pb2.NodeType.EMANE, } @@ -29,6 +31,8 @@ class CanvasAction: self.display_node_configuration() elif pb_type == core_pb2.NodeType.WIRELESS_LAN: self.display_wlan_configuration(canvas_node) + elif pb_type == core_pb2.NodeType.EMANE: + self.display_emane_configuration() def display_node_configuration(self): NodeConfig(self.canvas, self.node_to_show_config) @@ -41,3 +45,9 @@ class CanvasAction: ] WlanConfiguration(self.canvas, self.node_to_show_config, wlan_config) self.node_to_show_config = None + + def display_emane_configuration(self): + app = self.canvas.core_grpc.app + dialog = EmaneConfiguration(self.master, app, self.node_to_show_config) + print(dialog) + dialog.show() diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 5cb6b0b1..5db0cc6d 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -3,7 +3,6 @@ Incorporate grpc into python tkinter GUI """ import logging import os -from collections import OrderedDict from core.api.grpc import client, core_pb2 from coretk.dialogs.sessions import SessionsDialog @@ -143,22 +142,22 @@ class CoreGrpc: logging.info("set session state: %s", response) - def add_node(self, node_type, model, x, y, name, node_id): - position = core_pb2.Position(x=x, y=y) - node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) - self.node_ids.append(node_id) - response = self.core.add_node(self.session_id, node) - logging.info("created node: %s", response) - if node_type == core_pb2.NodeType.WIRELESS_LAN: - d = OrderedDict() - d["basic_range"] = "275" - d["bandwidth"] = "54000000" - d["jitter"] = "0" - d["delay"] = "20000" - d["error"] = "0" - r = self.core.set_wlan_config(self.session_id, node_id, d) - logging.debug("set wlan config %s", r) - return response.node_id + # def add_node(self, node_type, model, x, y, name, node_id): + # position = core_pb2.Position(x=x, y=y) + # node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) + # self.node_ids.append(node_id) + # response = self.core.add_node(self.session_id, node) + # logging.info("created node: %s", response) + # if node_type == core_pb2.NodeType.WIRELESS_LAN: + # d = OrderedDict() + # d["basic_range"] = "275" + # d["bandwidth"] = "54000000" + # d["jitter"] = "0" + # d["delay"] = "20000" + # d["error"] = "0" + # r = self.core.set_wlan_config(self.session_id, node_id, d) + # logging.debug("set wlan config %s", r) + # return response.node_id def edit_node(self, node_id, x, y): position = core_pb2.Position(x=x, y=y) @@ -232,6 +231,7 @@ class CoreGrpc: nodes=nodes, links=links, wlan_configs=wlan_configs, + mobility_configs=mobility_configs, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) @@ -253,28 +253,6 @@ class CoreGrpc: """ if1 = self.create_interface(type1, edge.interface_1) if2 = self.create_interface(type2, edge.interface_2) - # if type1 == core_pb2.NodeType.DEFAULT: - # interface = edge.interface_1 - # if1 = core_pb2.Interface( - # id=interface.id, - # name=interface.name, - # mac=interface.mac, - # ip4=interface.ipv4, - # ip4mask=interface.ip4prefix, - # ) - # logging.debug("create interface 1 %s", if1) - # # interface1 = self.interface_helper.create_interface(id1, 0) - # - # if type2 == core_pb2.NodeType.DEFAULT: - # interface = edge.interface_2 - # if2 = core_pb2.Interface( - # id=interface.id, - # name=interface.name, - # mac=interface.mac, - # ip4=interface.ipv4, - # ip4mask=interface.ip4prefix, - # ) - # logging.debug("create interface 2: %s", if2) response = self.core.add_link(self.session_id, id1, id2, if1, if2) logging.info("created link: %s", response) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 78688725..a9809c2c 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -368,9 +368,9 @@ class CoreToolbar(object): def pick_emane(self, main_button): self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.EMANE.value)) + main_button.configure(image=Images.get(ImageEnum.EMANE)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.EMANE.value) + self.canvas.draw_node_image = Images.get(ImageEnum.EMANE) self.canvas.draw_node_name = "emane" def draw_link_layer_options(self, link_layer_button): diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index b4b6fe28..bbc25f2b 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -71,6 +71,11 @@ class CoreToolbarHelp: return links def get_wlan_configuration_list(self): + """ + form a list of wlan configuration to pass to start_session + + :return: nothing + """ configs = [] grpc_manager = self.application.canvas.grpc_manager manager_configs = grpc_manager.wlanconfig_management.configurations @@ -79,23 +84,33 @@ class CoreToolbarHelp: configs.append(cnf) return configs + def get_mobility_configuration_list(self): + """ + form a list of mobility configuration to pass to start_session + + :return: nothing + """ + configs = [] + grpc_manager = self.application.canvas.grpc_manager + manager_configs = grpc_manager.mobilityconfig_management.configurations + for key in manager_configs: + cnf = core_pb2.MobilityConfig(node_id=key, config=manager_configs[key]) + configs.append(cnf) + return configs + def gui_start_session(self): - # list(core_pb2.Node) nodes = self.get_node_list() - - # list(core_bp2.Link) links = self.get_link_list() - - # print(links[0]) wlan_configs = self.get_wlan_configuration_list() - # print(wlan_configs) - self.core_grpc.start_session(nodes, links, wlan_configs=wlan_configs) + mobility_configs = self.get_mobility_configuration_list() + + self.core_grpc.start_session( + nodes, links, wlan_configs=wlan_configs, mobility_configs=mobility_configs + ) # self.core_grpc.core.add_link(self.core_grpc.session_id, self.id1, self.id2, self.if1, self.if2) # res = self.core_grpc.core.get_wlan_config(self.core_grpc.session_id, 1) - # res = self.core_grpc.core.get_session(self.core_grpc.session_id).session - # print(res) - # res = self.core_grpc.core.get_wlan_config(self.core_grpc.session_id, 1) + # res = self.core_grpc.core.get_wlan_config(self.core_grpc.session_id, 2) # print(res) diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py new file mode 100644 index 00000000..5ab2f545 --- /dev/null +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -0,0 +1,47 @@ +""" +emane configuration +""" + +import tkinter as tk + +from coretk.dialogs.dialog import Dialog + + +class EmaneConfiguration(Dialog): + def __init__(self, master, app, canvas_node): + super().__init__(master, app, "emane configuration", modal=False) + self.canvas_node = canvas_node + + def create_text_variable(self, val): + """ + create a string variable for convenience + + :param str val: entry text + :return: nothing + """ + var = tk.StringVar() + var.set(val) + return var + + def choose_core(self): + print("not implemented") + + def node_name_and_image(self): + f = tk.Frame(self) + + lbl = tk.Label(f, text="Node name:") + lbl.grid(row=0, column=0) + e = tk.Entry(f, textvariable=self.create_text_variable(""), bg="white") + e.grid(row=0, column=1) + + om = tk.OptionMenu( + f, + self.create_text_variable("None"), + "(none)", + "core1", + "core2", + command=self.choose_core, + ) + om.grid(row=0, column=2) + + # b = tk.Button(f,) diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/coretk/coretk/dialogs/mobilityconfig.py index 25339406..258d2f9c 100644 --- a/coretk/coretk/dialogs/mobilityconfig.py +++ b/coretk/coretk/dialogs/mobilityconfig.py @@ -20,6 +20,10 @@ class MobilityConfiguration(Dialog): """ super().__init__(master, app, "ns2script configuration", modal=True) self.canvas_node = canvas_node + self.node_config = app.canvas.grpc_manager.mobilityconfig_management.configurations[ + canvas_node.core_id + ] + self.mobility_script_parameters() self.ns2script_options() self.loop = "On" @@ -51,13 +55,13 @@ class MobilityConfiguration(Dialog): self.loop = value def create_label_entry_filebrowser( - self, parent_frame, text_label, filebrowser=False + self, parent_frame, text_label, entry_text, filebrowser=False ): f = tk.Frame(parent_frame, bg="#d9d9d9") lbl = tk.Label(f, text=text_label, bg="#d9d9d9") lbl.grid(padx=3, pady=3) # f.grid() - e = tk.Entry(f, textvariable=self.create_string_var(""), bg="#ffffff") + e = tk.Entry(f, textvariable=self.create_string_var(entry_text), bg="#ffffff") e.grid(row=0, column=1, padx=3, pady=3) if filebrowser: b = tk.Button(f, text="...", command=lambda: self.open_file(e)) @@ -88,9 +92,11 @@ class MobilityConfiguration(Dialog): bd=0, ) self.create_label_entry_filebrowser( - f1, "mobility script file", filebrowser=True + f1, "mobility script file", self.node_config["file"], filebrowser=True + ) + self.create_label_entry_filebrowser( + f1, "Refresh time (ms)", self.node_config["refresh_ms"] ) - self.create_label_entry_filebrowser(f1, "Refresh time (ms)") # f12 = tk.Frame(f1) # @@ -107,18 +113,15 @@ class MobilityConfiguration(Dialog): lbl.grid() om = tk.OptionMenu( - f13, - self.create_string_var("On"), - "On", - "Off", - command=self.set_loop_value, - indicatoron=True, + f13, self.create_string_var("On"), "On", "Off", command=self.set_loop_value ) om.grid(row=0, column=1) f13.grid(sticky=tk.E) - self.create_label_entry_filebrowser(f1, "auto-start seconds (0.0 for runtime)") + self.create_label_entry_filebrowser( + f1, "auto-start seconds (0.0 for runtime)", self.node_config["autostart"] + ) # f14 = tk.Frame(f1) # # lbl = tk.Label(f14, text="auto-start seconds (0.0 for runtime)") @@ -129,7 +132,7 @@ class MobilityConfiguration(Dialog): # # f14.grid() self.create_label_entry_filebrowser( - f1, "node mapping (optional, e.g. 0:1, 1:2, 2:3)" + f1, "node mapping (optional, e.g. 0:1, 1:2, 2:3)", self.node_config["map"] ) # f15 = tk.Frame(f1) # @@ -142,13 +145,22 @@ class MobilityConfiguration(Dialog): # f15.grid() self.create_label_entry_filebrowser( - f1, "script file to run upon start", filebrowser=True + f1, + "script file to run upon start", + self.node_config["script_start"], + filebrowser=True, ) self.create_label_entry_filebrowser( - f1, "script file to run upon pause", filebrowser=True + f1, + "script file to run upon pause", + self.node_config["script_pause"], + filebrowser=True, ) self.create_label_entry_filebrowser( - f1, "script file to run upon stop", filebrowser=True + f1, + "script file to run upon stop", + self.node_config["script_stop"], + filebrowser=True, ) f1.grid() sb.config(command=f1.yview) @@ -186,13 +198,28 @@ class MobilityConfiguration(Dialog): canvas.grid_slaves(row=7, column=0)[0].grid_slaves(row=0, column=1)[0].get() ) - print("mobility script file: ", file) - print("refresh time: ", refresh_time) - print("auto start seconds: ", auto_start_seconds) - print("node mapping: ", node_mapping) - print("script file to run upon start: ", file_upon_start) - print("file upon pause: ", file_upon_pause) - print("file upon stop: ", file_upon_stop) + # print("mobility script file: ", file) + # print("refresh time: ", refresh_time) + # print("auto start seconds: ", auto_start_seconds) + # print("node mapping: ", node_mapping) + # print("script file to run upon start: ", file_upon_start) + # print("file upon pause: ", file_upon_pause) + # print("file upon stop: ", file_upon_stop) + if self.loop == "On": + loop = "1" + else: + loop = "0" + self.app.canvas.grpc_manager.mobilityconfig_management.set_custom_configuration( + node_id=self.canvas_node.core_id, + file=file, + refresh_ms=refresh_time, + loop=loop, + autostart=auto_start_seconds, + node_mapping=node_mapping, + script_start=file_upon_start, + script_pause=file_upon_pause, + script_stop=file_upon_stop, + ) self.destroy() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index ef0f064f..d9d1460c 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -90,8 +90,8 @@ class CanvasGraph(tk.Canvas): self.draw_node_name = None self.selected = None self.node_context = None - self.nodes = {} - self.edges = {} + self.nodes.clear() + self.edges.clear() self.drawing_edge = None self.grpc_manager = GrpcManager(new_grpc) @@ -100,6 +100,8 @@ class CanvasGraph(tk.Canvas): self.core_grpc = new_grpc self.draw_existing_component() + # self.grpc_manager.wlanconfig_management.load_wlan_configurations(self.core_grpc) + def setup_bindings(self): """ Bind any mouse events or hot keys to the matching action diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index e7373dc6..ffc1f0f9 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -7,6 +7,7 @@ import logging from core.api.grpc import core_pb2 from coretk.coretocanvas import CoreToCanvasMapping from coretk.interface import Interface, InterfaceManager +from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.wlannodeconfig import WlanNodeConfig link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] @@ -74,6 +75,7 @@ class GrpcManager: self.core_mapping = CoreToCanvasMapping() self.wlanconfig_management = WlanNodeConfig() + self.mobilityconfig_management = MobilityNodeConfig() def update_preexisting_ids(self): """ @@ -152,6 +154,9 @@ class GrpcManager: # set default configuration for wireless node self.wlanconfig_management.set_default_config(node_type, nid) + # set default mobility configuration for wireless node + self.mobilityconfig_management.set_default_configuration(node_type, nid) + self.nodes[canvas_id] = create_node self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) # self.core_id_to_canvas_id[nid] = canvas_id diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 6a1ef0e7..bf30b713 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -400,6 +400,14 @@ class MenuAction: # print(grpc.get_session_state()) self.application.canvas.canvas_reset_and_redraw(core_grpc) + self.application.canvas.grpc_manager.wlanconfig_management.load_wlan_configurations( + core_grpc + ) + + self.application.canvas.grpc_manager.mobilityconfig_management.load_mobility_configurations( + core_grpc + ) + # Todo might not need self.application.core_grpc = core_grpc diff --git a/coretk/coretk/mobilitynodeconfig.py b/coretk/coretk/mobilitynodeconfig.py new file mode 100644 index 00000000..7bcb58a1 --- /dev/null +++ b/coretk/coretk/mobilitynodeconfig.py @@ -0,0 +1,89 @@ +""" +mobility configurations for all the nodes +""" + +import logging +from collections import OrderedDict + +from core.api.grpc import core_pb2 + + +class MobilityNodeConfig: + def __init__(self): + self.configurations = {} + + def set_default_configuration(self, node_type, node_id): + """ + set default mobility configuration for a node + + :param core_pb2.NodeType node_type: protobuf node type + :param int node_id: node id + :return: nothing + """ + if node_type == core_pb2.NodeType.WIRELESS_LAN: + config = OrderedDict() + config["autostart"] = "" + config["file"] = "" + config["loop"] = "1" + config["map"] = "" + config["refresh_ms"] = "50" + config["script_pause"] = "" + config["script_start"] = "" + config["script_stop"] = "" + self.configurations[node_id] = config + + def set_custom_configuration( + self, + node_id, + file, + refresh_ms, + loop, + autostart, + node_mapping, + script_start, + script_pause, + script_stop, + ): + """ + set custom mobility configuration for a node + + :param int node_id: node id + :param str file: path to mobility script file + :param str refresh_ms: refresh time + :param str loop: loop option + :param str autostart: auto-start seconds value + :param str node_mapping: node mapping + :param str script_start: path to script to run upon start + :param str script_pause: path to script to run upon pause + :param str script_stop: path to script to run upon stop + :return: nothing + """ + if node_id in self.configurations: + self.configurations[node_id]["autostart"] = autostart + self.configurations[node_id]["file"] = file + self.configurations[node_id]["loop"] = loop + self.configurations[node_id]["map"] = node_mapping + self.configurations[node_id]["refresh_ms"] = refresh_ms + self.configurations[node_id]["script_pause"] = script_pause + self.configurations[node_id]["script_start"] = script_start + self.configurations[node_id]["script_stop"] = script_stop + else: + logging.error("mobilitynodeconfig.py invalid node_id") + + def load_mobility_configurations(self, core_grpc): + """ + load mobility configuration from the daemon into memory + + :param coretk.coregrpc.CoreGrpc core_grpc: CoreGrpc object + :return: nothing + """ + self.configurations.clear() + sid = core_grpc.session_id + client = core_grpc.core + configs = client.get_mobility_configs(sid).configs + for nid in configs: + node_config = configs[nid].config + cnf = OrderedDict() + for key in node_config: + cnf[key] = node_config[key].value + self.configurations[nid] = cnf diff --git a/coretk/coretk/wlanconfiguration.py b/coretk/coretk/wlanconfiguration.py index 6e028084..cd9b3a35 100644 --- a/coretk/coretk/wlanconfiguration.py +++ b/coretk/coretk/wlanconfiguration.py @@ -32,7 +32,8 @@ class WlanConfiguration: # self.range_var.set(275.0) self.config = config self.range_var = tk.StringVar() - self.range_var.set(config["basic_range"]) + # self.range_var.set(config["basic_range"]) + self.range_var.set(config["range"]) # self.bandwidth_var = tk.IntVar() self.bandwidth_var = tk.StringVar() self.bandwidth_var.set(config["bandwidth"]) @@ -130,7 +131,8 @@ class WlanConfiguration: e = tk.Entry( f1, - textvariable=self.create_string_var(self.config["basic_range"]), + # textvariable=self.create_string_var(self.config["basic_range"]), + textvariable=self.create_string_var(self.config["range"]), width=5, bg="white", ) diff --git a/coretk/coretk/wlannodeconfig.py b/coretk/coretk/wlannodeconfig.py index 647cd7cf..d8bb2996 100644 --- a/coretk/coretk/wlannodeconfig.py +++ b/coretk/coretk/wlannodeconfig.py @@ -14,7 +14,7 @@ class WlanNodeConfig: def set_default_config(self, node_type, node_id): if node_type == core_pb2.NodeType.WIRELESS_LAN: config = OrderedDict() - config["basic_range"] = "275" + config["range"] = "275" config["bandwidth"] = "54000000" config["jitter"] = "0" config["delay"] = "20000" @@ -22,8 +22,39 @@ class WlanNodeConfig: self.configurations[node_id] = config def set_custom_config(self, node_id, range, bandwidth, jitter, delay, error): - self.configurations[node_id]["basic_range"] = range + self.configurations[node_id]["range"] = range self.configurations[node_id]["bandwidth"] = bandwidth self.configurations[node_id]["jitter"] = jitter self.configurations[node_id]["delay"] = delay self.configurations[node_id]["error"] = error + + def delete_node_config(self, node_id): + """ + not implemented + :param node_id: + :return: + """ + return + + def load_wlan_configurations(self, core_grpc): + """ + load wlan configuration from the daemon + + :param coretk.coregrpc.CoreGrpc core_grpc: CoreGrpc object + :return: nothing + """ + self.configurations.clear() + sid = core_grpc.session_id + client = core_grpc.core + for node in client.get_session(sid).session.nodes: + if node.type == core_pb2.NodeType.WIRELESS_LAN: + wlan_config = client.get_wlan_config(sid, node.id).config + config = OrderedDict() + for key in wlan_config: + config[key] = wlan_config[key].value + + # config[key] = wlan_config[key]["value"] + # print(config) + # for k, v in wlan_config.config: + + self.configurations[node.id] = config From eb862dd9daa2c0b51b0ae4639bbc9d5e5517e2fe Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 4 Nov 2019 10:48:22 -0800 Subject: [PATCH 165/462] emane config --- coretk/coretk/dialogs/emaneconfig.py | 116 +++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 6 deletions(-) diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index 5ab2f545..75cbca0e 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -3,14 +3,26 @@ emane configuration """ import tkinter as tk +import webbrowser from coretk.dialogs.dialog import Dialog +from coretk.images import ImageEnum, Images class EmaneConfiguration(Dialog): def __init__(self, master, app, canvas_node): super().__init__(master, app, "emane configuration", modal=False) self.canvas_node = canvas_node + self.radiovar = tk.IntVar() + self.radiovar.set(1) + self.columnconfigure(0, weight=1) + + # draw + self.node_name_and_image() + self.emane_configuration() + + def browse_emane_wiki(self): + webbrowser.open_new("https://github.com/adjacentlink/emane/wiki") def create_text_variable(self, val): """ @@ -27,12 +39,12 @@ class EmaneConfiguration(Dialog): print("not implemented") def node_name_and_image(self): - f = tk.Frame(self) + f = tk.Frame(self, bg="#d9d9d9") - lbl = tk.Label(f, text="Node name:") - lbl.grid(row=0, column=0) + lbl = tk.Label(f, text="Node name:", bg="#d9d9d9") + lbl.grid(row=0, column=0, padx=2, pady=2) e = tk.Entry(f, textvariable=self.create_text_variable(""), bg="white") - e.grid(row=0, column=1) + e.grid(row=0, column=1, padx=2, pady=2) om = tk.OptionMenu( f, @@ -42,6 +54,98 @@ class EmaneConfiguration(Dialog): "core2", command=self.choose_core, ) - om.grid(row=0, column=2) + om.grid(row=0, column=2, padx=2, pady=2) - # b = tk.Button(f,) + b = tk.Button(f, image=self.canvas_node.image) + b.grid(row=0, column=3, padx=2, pady=2) + + f.grid(row=0, column=0) + + def draw_option_buttons(self, parent): + f = tk.Frame(parent, bg="#d9d9d9") + b = tk.Button( + f, + text="model options", + image=Images.get(ImageEnum.EDITNODE), + compound=tk.RIGHT, + bg="#d9d9d9", + state=tk.DISABLED, + ) + b.grid(row=0, column=0, padx=2, pady=2) + b = tk.Button( + f, + text="EMANE options", + image=Images.get(ImageEnum.EDITNODE), + compound=tk.RIGHT, + bg="#d9d9d9", + ) + b.grid(row=0, column=1, padx=2, pady=2) + f.grid(row=4, column=0) + + def click_radio_button(self): + print(self.radiovar.get()) + + def draw_emane_models(self, parent): + models = ["none", "rfpipe", "ieee80211abg", "commeffect", "bypass", "tdma"] + f = tk.Frame( + parent, + bg="#d9d9d9", + highlightbackground="#b3b3b3", + highlightcolor="#b3b3b3", + highlightthickness=0.5, + bd=0, + ) + value = 1 + for m in models: + b = tk.Radiobutton( + f, + text=m, + variable=self.radiovar, + indicatoron=True, + value=value, + bg="#d9d9d9", + highlightthickness=0, + command=self.click_radio_button, + ) + b.grid(sticky=tk.W) + value = value + 1 + f.grid(row=3, column=0, sticky=tk.W + tk.E) + + def emane_configuration(self): + lbl = tk.Label(self, text="Emane") + lbl.grid(row=1, column=0) + f = tk.Frame( + self, + bg="#d9d9d9", + highlightbackground="#b3b3b3", + highlightcolor="#b3b3b3", + highlightthickness=0.5, + bd=0, + relief=tk.RAISED, + ) + f.columnconfigure(0, weight=1) + + b = tk.Button( + f, + image=Images.get(ImageEnum.EDITNODE), + text="EMANE Wiki", + compound=tk.RIGHT, + relief=tk.RAISED, + bg="#d9d9d9", + command=self.browse_emane_wiki, + ) + b.grid(row=0, column=0, sticky=tk.W) + + lbl = tk.Label( + f, + text="The EMANE emulation system provides more complex wireless radio emulation " + "\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details", + bg="#d9d9d9", + ) + lbl.grid(row=1, column=0, sticky=tk.W) + + lbl = tk.Label(f, text="EMANE Models", bg="#d9d9d9") + lbl.grid(row=2, column=0, sticky=tk.W) + self.draw_option_buttons(f) + self.draw_emane_models(f) + f.grid(row=2, column=0) From 22a55b69d3231cbb72bf372b3007c2fbc4a98ed0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 4 Nov 2019 11:34:28 -0800 Subject: [PATCH 166/462] update node service dialog to use common dialog class --- coretk/coretk/dialogs/nodeconfig.py | 8 +- coretk/coretk/dialogs/nodeservice.py | 216 ++++++++++++++------------- 2 files changed, 122 insertions(+), 102 deletions(-) diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index d670853c..f1de2e50 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -3,7 +3,7 @@ from tkinter import ttk from coretk.dialogs.dialog import Dialog from coretk.dialogs.nodeicon import NodeIconDialog -from coretk.dialogs.nodeservice import NodeServices +from coretk.dialogs.nodeservice import NodeServicesDialog NETWORKNODETYPES = ["switch", "hub", "wlan", "rj45", "tunnel"] DEFAULTNODES = ["router", "host", "PC"] @@ -56,7 +56,7 @@ class NodeConfigDialog(Dialog): frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - button = tk.Button(frame, text="Services", command=lambda: NodeServices()) + button = tk.Button(frame, text="Services", command=self.click_services) button.grid(row=0, column=0, padx=2, sticky="ew") self.image_button = tk.Button( @@ -80,6 +80,10 @@ class NodeConfigDialog(Dialog): button = tk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") + def click_services(self): + dialog = NodeServicesDialog(self, self.app, self.canvas_node) + dialog.show() + def click_icon(self): dialog = NodeIconDialog(self, self.app, self.canvas_node) dialog.show() diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 3bcedfb7..82d36cca 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -4,6 +4,8 @@ core node services import tkinter as tk from tkinter import messagebox +from coretk.dialogs.dialog import Dialog + CORE_DEFAULT_GROUPS = ["EMANE", "FRR", "ProtoSvc", "Quagga", "Security", "Utility"] DEFAULT_GROUP_RADIO_VALUE = { "EMANE": 1, @@ -57,35 +59,28 @@ DEFAULT_GROUP_SERVICES = { } -class NodeServices: - def __init__(self): +class NodeServicesDialog(Dialog): + def __init__(self, master, app, canvas_node): + super().__init__(master, app, "Node Services", modal=True) + self.canvas_node = canvas_node self.core_groups = [] self.service_to_config = None + self.config_frame = None + self.draw() - self.top = tk.Toplevel() - self.top.title("Node services") - self.config_frame = tk.Frame(self.top) - self.config_frame.grid() + def draw(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.config_frame = tk.Frame(self) + self.config_frame.columnconfigure(0, weight=1) + self.config_frame.columnconfigure(1, weight=1) + self.config_frame.columnconfigure(2, weight=1) + self.config_frame.rowconfigure(0, weight=1) + self.config_frame.grid(row=0, column=0, sticky="nsew") self.draw_group() - self.group_services() - self.current_services() - self.node_service_options() - - def display_group_services(self, group_name): - group_services_frame = self.config_frame.grid_slaves(row=0, column=1)[0] - listbox = group_services_frame.grid_slaves(row=1, column=0)[0] - listbox.delete(0, tk.END) - for s in DEFAULT_GROUP_SERVICES[group_name]: - listbox.insert(tk.END, s) - for i in range(listbox.size()): - listbox.itemconfig(i, selectbackground="white") - - def group_select(self, event): - listbox = event.widget - cur_selection = listbox.curselection() - if cur_selection: - s = listbox.get(listbox.curselection()) - self.display_group_services(s) + self.draw_services() + self.draw_current_services() + self.draw_buttons() def draw_group(self): """ @@ -93,36 +88,113 @@ class NodeServices: :return: nothing """ - f = tk.Frame(self.config_frame) + frame = tk.Frame(self.config_frame) + frame.columnconfigure(0, weight=1) + frame.rowconfigure(1, weight=1) + frame.grid(row=0, column=0, padx=3, pady=3, sticky="nsew") - lbl = tk.Label(f, text="Group") - lbl.grid() + label = tk.Label(frame, text="Group") + label.grid(row=0, column=0, sticky="ew") - sb = tk.Scrollbar(f, orient=tk.VERTICAL) - sb.grid(row=1, column=1, sticky=tk.S + tk.N) + scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar.grid(row=1, column=1, sticky="ns") listbox = tk.Listbox( - f, + frame, selectmode=tk.SINGLE, - yscrollcommand=sb.set, + yscrollcommand=scrollbar.set, relief=tk.FLAT, - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", highlightthickness=0.5, bd=0, ) + listbox.grid(row=1, column=0, sticky="nsew") + listbox.bind("<>", self.handle_group_change) - for grp in CORE_DEFAULT_GROUPS: - listbox.insert(tk.END, grp) - for i in range(0, listbox.size()): - listbox.itemconfig(i, selectbackground="white") - listbox.grid(row=1, column=0) + for group in CORE_DEFAULT_GROUPS: + listbox.insert(tk.END, group) - sb.config(command=listbox.yview) - f.grid(padx=3, pady=3) - listbox.bind("<>", self.group_select) + scrollbar.config(command=listbox.yview) - def group_service_select(self, event): + def draw_services(self): + frame = tk.Frame(self.config_frame) + frame.columnconfigure(0, weight=1) + frame.rowconfigure(1, weight=1) + frame.grid(row=0, column=1, padx=3, pady=3, sticky="nsew") + + label = tk.Label(frame, text="Group services") + label.grid(row=0, column=0, sticky="ew") + + scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar.grid(row=1, column=1, sticky="ns") + + listbox = tk.Listbox( + frame, + selectmode=tk.SINGLE, + yscrollcommand=scrollbar.set, + relief=tk.FLAT, + highlightthickness=0.5, + bd=0, + ) + listbox.grid(row=1, column=0, sticky="nsew") + listbox.bind("<>", self.handle_service_change) + + scrollbar.config(command=listbox.yview) + + def draw_current_services(self): + frame = tk.Frame(self.config_frame) + frame.columnconfigure(0, weight=1) + frame.rowconfigure(1, weight=1) + frame.grid(row=0, column=2, padx=3, pady=3, sticky="nsew") + + label = tk.Label(frame, text="Current services") + label.grid(row=0, column=0, sticky="ew") + + scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar.grid(row=1, column=1, sticky="ns") + + listbox = tk.Listbox( + frame, + selectmode=tk.MULTIPLE, + yscrollcommand=scrollbar.set, + relief=tk.FLAT, + highlightthickness=0.5, + bd=0, + ) + listbox.grid(row=1, column=0, sticky="nsew") + + scrollbar.config(command=listbox.yview) + + def draw_buttons(self): + frame = tk.Frame(self) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + frame.grid(row=1, column=0, sticky="ew") + + button = tk.Button(frame, text="Configure", command=self.click_configure) + button.grid(row=0, column=0, sticky="ew") + + button = tk.Button(frame, text="Apply") + button.grid(row=0, column=1, sticky="ew") + + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=2, sticky="ew") + + def handle_group_change(self, event): + listbox = event.widget + cur_selection = listbox.curselection() + if cur_selection: + s = listbox.get(listbox.curselection()) + self.display_group_services(s) + + def display_group_services(self, group_name): + group_services_frame = self.config_frame.grid_slaves(row=0, column=1)[0] + listbox = group_services_frame.grid_slaves(row=1, column=0)[0] + listbox.delete(0, tk.END) + for s in DEFAULT_GROUP_SERVICES[group_name]: + listbox.insert(tk.END, s) + + def handle_service_change(self, event): print("select group service") listbox = event.widget cur_selection = listbox.curselection() @@ -132,64 +204,8 @@ class NodeServices: else: self.service_to_config = None - def group_services(self): - f = tk.Frame(self.config_frame) - lbl = tk.Label(f, text="Group services") - lbl.grid() - - sb = tk.Scrollbar(f, orient=tk.VERTICAL) - sb.grid(row=1, column=1, sticky=tk.S + tk.N) - - listbox = tk.Listbox( - f, - selectmode=tk.SINGLE, - yscrollcommand=sb.set, - relief=tk.FLAT, - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", - highlightthickness=0.5, - bd=0, - ) - listbox.grid(row=1, column=0) - sb.config(command=listbox.yview) - f.grid(padx=3, pady=3, row=0, column=1) - - listbox.bind("<>", self.group_service_select) - - def current_services(self): - f = tk.Frame(self.config_frame) - lbl = tk.Label(f, text="Current services") - lbl.grid() - - sb = tk.Scrollbar(f, orient=tk.VERTICAL) - sb.grid(row=1, column=1, sticky=tk.S + tk.N) - - listbox = tk.Listbox( - f, - selectmode=tk.MULTIPLE, - yscrollcommand=sb.set, - relief=tk.FLAT, - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", - highlightthickness=0.5, - bd=0, - ) - listbox.grid(row=1, column=0) - sb.config(command=listbox.yview) - f.grid(padx=3, pady=3, row=0, column=2) - - def config_service(self): + def click_configure(self): if self.service_to_config is None: messagebox.showinfo("CORE info", "Choose a service to configure.") else: print(self.service_to_config) - - def node_service_options(self): - f = tk.Frame(self.top) - b = tk.Button(f, text="Connfigure", command=self.config_service) - b.grid(row=0, column=0) - b = tk.Button(f, text="Apply") - b.grid(row=0, column=1) - b = tk.Button(f, text="Cancel", command=self.top.destroy) - b.grid(row=0, column=2) - f.grid(sticky=tk.E) From 2334f0f688d5dfb0c118497bae6f2d97ecad66e3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 4 Nov 2019 12:29:01 -0800 Subject: [PATCH 167/462] updated canvas background dialog to use common dialog class --- .../{setwallpaper.py => canvasbackground.py} | 256 +++++++++--------- coretk/coretk/dialogs/sizeandscale.py | 2 +- coretk/coretk/menuaction.py | 5 +- 3 files changed, 131 insertions(+), 132 deletions(-) rename coretk/coretk/dialogs/{setwallpaper.py => canvasbackground.py} (60%) diff --git a/coretk/coretk/dialogs/setwallpaper.py b/coretk/coretk/dialogs/canvasbackground.py similarity index 60% rename from coretk/coretk/dialogs/setwallpaper.py rename to coretk/coretk/dialogs/canvasbackground.py index 81932208..fe9b5f6a 100644 --- a/coretk/coretk/dialogs/setwallpaper.py +++ b/coretk/coretk/dialogs/canvasbackground.py @@ -9,6 +9,7 @@ from tkinter import filedialog from PIL import Image, ImageTk from coretk.appdirs import BACKGROUNDS_PATH +from coretk.dialogs.dialog import Dialog class ScaleOption(enum.Enum): @@ -19,44 +20,116 @@ class ScaleOption(enum.Enum): TILED = 4 -class CanvasWallpaper: - def __init__(self, app): +class CanvasBackgroundDialog(Dialog): + def __init__(self, master, app): """ create an instance of CanvasWallpaper object :param coretk.app.Application app: root application """ - self.app = app + super().__init__(master, app, "Canvas Background", modal=True) self.canvas = self.app.canvas + self.radiovar = tk.IntVar(value=self.app.radiovar.get()) + self.show_grid_var = tk.IntVar(value=self.app.show_grid_var.get()) + self.adjust_to_dim_var = tk.IntVar(value=self.app.adjust_to_dim_var.get()) + self.image_label = None + self.file_name = tk.StringVar() + self.options = [] + self.draw() - self.top = tk.Toplevel() - self.top.title("Set Canvas Wallpaper") - self.radiovar = tk.IntVar() - print(self.app.radiovar.get()) - self.radiovar.set(self.app.radiovar.get()) - self.show_grid_var = tk.IntVar() - self.show_grid_var.set(self.app.show_grid_var.get()) - self.adjust_to_dim_var = tk.IntVar() - self.adjust_to_dim_var.set(self.app.adjust_to_dim_var.get()) + def draw(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.draw_image() + self.draw_image_label() + self.draw_image_selection() + self.draw_options() + self.draw_additional_options() + self.draw_buttons() - self.create_image_label() - self.create_text_label() - self.open_image() - self.display_options() - self.additional_options() - self.apply_cancel() - - def create_image_label(self): - image_label = tk.Label( - self.top, text="(image preview)", height=8, width=32, bg="white" + def draw_image(self): + self.image_label = tk.Label( + self, text="(image preview)", height=8, width=32, bg="white" ) - image_label.grid(pady=5) + self.image_label.grid(row=0, column=0, pady=5, sticky="nsew") - def create_text_label(self): - text_label = tk.Label(self.top, text="Image filename: ") - text_label.grid() + def draw_image_label(self): + label = tk.Label(self, text="Image filename: ") + label.grid(row=1, column=0, sticky="ew") - def open_image_link(self): + def draw_image_selection(self): + frame = tk.Frame(self) + frame.columnconfigure(0, weight=2) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + frame.grid(row=2, column=0, sticky="ew") + + entry = tk.Entry(frame, textvariable=self.file_name) + entry.focus() + entry.grid(row=0, column=0, sticky="ew") + + button = tk.Button(frame, text="...", command=self.click_open_image) + button.grid(row=0, column=1, sticky="ew") + + button = tk.Button(frame, text="Clear", command=self.click_clear) + button.grid(row=0, column=2, sticky="ew") + + def draw_options(self): + frame = tk.Frame(self) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + frame.columnconfigure(3, weight=1) + frame.grid(row=3, column=0, sticky="ew") + + button = tk.Radiobutton( + frame, text="upper-left", value=1, variable=self.radiovar + ) + button.grid(row=0, column=0, sticky="ew") + self.options.append(button) + + button = tk.Radiobutton(frame, text="centered", value=2, variable=self.radiovar) + button.grid(row=0, column=1, sticky="ew") + self.options.append(button) + + button = tk.Radiobutton(frame, text="scaled", value=3, variable=self.radiovar) + button.grid(row=0, column=2, sticky="ew") + self.options.append(button) + + button = tk.Radiobutton(frame, text="titled", value=4, variable=self.radiovar) + button.grid(row=0, column=3, sticky="ew") + self.options.append(button) + + def draw_additional_options(self): + checkbutton = tk.Checkbutton( + self, text="Show grid", variable=self.show_grid_var + ) + checkbutton.grid(row=4, column=0, sticky="ew", padx=5) + + checkbutton = tk.Checkbutton( + self, + text="Adjust canvas size to image dimensions", + variable=self.adjust_to_dim_var, + command=self.click_adjust_canvas, + ) + checkbutton.grid(row=5, column=0, sticky="ew", padx=5) + + self.show_grid_var.set(1) + self.adjust_to_dim_var.set(0) + + def draw_buttons(self): + frame = tk.Frame(self) + frame.grid(row=6, column=0, pady=5, sticky="ew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + + button = tk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, sticky="ew") + + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_open_image(self): filename = filedialog.askopenfilename( initialdir=str(BACKGROUNDS_PATH), title="Open", @@ -65,102 +138,39 @@ class CanvasWallpaper: ("All Files", "*"), ), ) - - # fill the file name into the file name entry - img_open_frame = self.top.grid_slaves(2, 0)[0] - filename_entry = img_open_frame.grid_slaves(0, 0)[0] - filename_entry.delete(0, tk.END) - filename_entry.insert(tk.END, filename) - - # display that onto the label - img_label = self.top.grid_slaves(0, 0)[0] if filename: + self.file_name.set(filename) + width, height = 250, 135 img = Image.open(filename) - img = img.resize((250, 135), Image.ANTIALIAS) + img = img.resize((width, height), Image.ANTIALIAS) tk_img = ImageTk.PhotoImage(img) - img_label.config(image=tk_img, width=250, height=135) - img_label.image = tk_img + self.image_label.config(image=tk_img, width=width, height=height) + self.image_label.image = tk_img - def clear_link(self): + def click_clear(self): """ delete like shown in image link entry if there is any :return: nothing """ # delete entry - img_open_frame = self.top.grid_slaves(2, 0)[0] - filename_entry = img_open_frame.grid_slaves(0, 0)[0] - filename_entry.delete(0, tk.END) - + self.file_name.set("") # delete display image - img_label = self.top.grid_slaves(0, 0)[0] - img_label.config(image="", width=32, height=8) - - def open_image(self): - f = tk.Frame(self.top) - - var = tk.StringVar(f, value="") - e = tk.Entry(f, textvariable=var) - e.focus() - e.grid() - - b = tk.Button(f, text="...", command=self.open_image_link) - b.grid(row=0, column=1) - - b = tk.Button(f, text="Clear", command=self.clear_link) - b.grid(row=0, column=2) - - f.grid() - - def display_options(self): - f = tk.Frame(self.top) - - b1 = tk.Radiobutton(f, text="upper-left", value=1, variable=self.radiovar) - b1.grid(row=0, column=0) - - b2 = tk.Radiobutton(f, text="centered", value=2, variable=self.radiovar) - b2.grid(row=0, column=1) - - b3 = tk.Radiobutton(f, text="scaled", value=3, variable=self.radiovar) - b3.grid(row=0, column=2) - - b4 = tk.Radiobutton(f, text="titled", value=4, variable=self.radiovar) - b4.grid(row=0, column=3) - - # self.radiovar.set(1) - - f.grid() - - def adjust_canvas_size(self): + self.image_label.config(image="", width=32, height=8) + def click_adjust_canvas(self): # deselect all radio buttons and grey them out if self.adjust_to_dim_var.get() == 1: self.radiovar.set(0) - option_frame = self.top.grid_slaves(3, 0)[0] - for i in option_frame.grid_slaves(): - i.config(state=tk.DISABLED) - + for option in self.options: + option.config(state=tk.DISABLED) # turn back the radio button to active state so that user can choose again elif self.adjust_to_dim_var.get() == 0: - option_frame = self.top.grid_slaves(3, 0)[0] - for i in option_frame.grid_slaves(): - i.config(state=tk.NORMAL) - self.radiovar.set(1) + self.radiovar.set(1) + for option in self.options: + option.config(state=tk.NORMAL) else: - logging.error("setwallpaper.py adjust_canvas_size invalid value") - - def additional_options(self): - b = tk.Checkbutton(self.top, text="Show grid", variable=self.show_grid_var) - b.grid(sticky=tk.W, padx=5) - b = tk.Checkbutton( - self.top, - text="Adjust canvas size to image dimensions", - variable=self.adjust_to_dim_var, - command=self.adjust_canvas_size, - ) - b.grid(sticky=tk.W, padx=5) - self.show_grid_var.set(1) - self.adjust_to_dim_var.set(0) + logging.error("canvasbackground.py adjust_canvas_size invalid value") def delete_canvas_components(self, tag_list): """ @@ -316,7 +326,7 @@ class CanvasWallpaper: self.canvas.itemconfig(i, state=tk.NORMAL) self.canvas.lift(i) else: - logging.error("setwallpaper.py show_grid invalid value") + logging.error("canvasbackground.py show_grid invalid value") def save_wallpaper_options(self): self.app.radiovar.set(self.radiovar.get()) @@ -324,51 +334,39 @@ class CanvasWallpaper: self.app.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) def click_apply(self): - img_link_frame = self.top.grid_slaves(2, 0)[0] - filename = img_link_frame.grid_slaves(0, 0)[0].get() + filename = self.file_name.get() if not filename: self.delete_canvas_components(["wallpaper"]) - self.top.destroy() + self.destroy() self.app.current_wallpaper = None self.save_wallpaper_options() return + try: img = Image.open(filename) self.app.current_wallpaper = img except FileNotFoundError: - print("invalid filename, draw original white plot") + logging.error("invalid background: %s", filename) if self.app.wallpaper_id: self.canvas.delete(self.app.wallpaper_id) - self.top.destroy() + self.destroy() return self.app.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) if self.adjust_to_dim_var.get() == 0: - self.app.radiovar.set(self.radiovar.get()) - - if self.radiovar.get() == ScaleOption.UPPER_LEFT.value: + option = ScaleOption(self.radiovar.get()) + if option == ScaleOption.UPPER_LEFT: self.upper_left(img) - elif self.radiovar.get() == ScaleOption.CENTERED.value: + elif option == ScaleOption.CENTERED: self.center(img) - elif self.radiovar.get() == ScaleOption.SCALED.value: + elif option == ScaleOption.SCALED: self.scaled(img) - elif self.radiovar.get() == ScaleOption.TILED.value: + elif option == ScaleOption.TILED: print("not implemented yet") elif self.adjust_to_dim_var.get() == 1: self.canvas_to_image_dimension(img) self.show_grid() - self.top.destroy() - - def apply_cancel(self): - f = tk.Frame(self.top) - - b = tk.Button(f, text="Apply", command=self.click_apply) - b.grid(row=0, column=0) - - b = tk.Button(f, text="Cancel", command=self.top.destroy) - b.grid(row=0, column=1) - - f.grid(pady=5) + self.destroy() diff --git a/coretk/coretk/dialogs/sizeandscale.py b/coretk/coretk/dialogs/sizeandscale.py index 0267a300..dbc5a29c 100644 --- a/coretk/coretk/dialogs/sizeandscale.py +++ b/coretk/coretk/dialogs/sizeandscale.py @@ -4,7 +4,7 @@ size and scale import tkinter as tk from functools import partial -from coretk.dialogs.setwallpaper import ScaleOption +from coretk.dialogs.canvasbackground import ScaleOption DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 1d9adca1..bbb72895 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -8,10 +8,10 @@ from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 from coretk.appdirs import XML_PATH +from coretk.dialogs.canvasbackground import CanvasBackgroundDialog from coretk.dialogs.hooks import HooksDialog from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog -from coretk.dialogs.setwallpaper import CanvasWallpaper from coretk.dialogs.sizeandscale import SizeAndScale @@ -386,7 +386,8 @@ class MenuAction: self.app.size_and_scale = SizeAndScale(self.app) def canvas_set_wallpaper(self): - self.app.set_wallpaper = CanvasWallpaper(self.app) + dialog = CanvasBackgroundDialog(self.app, self.app) + dialog.show() def help_core_github(self): webbrowser.open_new("https://github.com/coreemu/core") From 8c4ed15629ce455d85145dddcebee0fc1245e36c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 4 Nov 2019 14:09:59 -0800 Subject: [PATCH 168/462] converted canvas size and scale dialog to use common dialog class --- coretk/coretk/dialogs/canvassizeandscale.py | 211 +++++++++++++++ coretk/coretk/dialogs/sizeandscale.py | 277 -------------------- coretk/coretk/menuaction.py | 14 +- 3 files changed, 214 insertions(+), 288 deletions(-) create mode 100644 coretk/coretk/dialogs/canvassizeandscale.py delete mode 100644 coretk/coretk/dialogs/sizeandscale.py diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py new file mode 100644 index 00000000..20b464d0 --- /dev/null +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -0,0 +1,211 @@ +""" +size and scale +""" +import tkinter as tk +from tkinter import font + +from coretk.dialogs.canvasbackground import ScaleOption +from coretk.dialogs.dialog import Dialog + +DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] + + +class SizeAndScaleDialog(Dialog): + def __init__(self, master, app): + """ + create an instance for size and scale object + + :param app: main application + """ + super().__init__(master, app, "Canvas Size and Scale", modal=True) + self.meter_per_pixel = self.app.canvas.meters_per_pixel + self.section_font = font.Font(weight="bold") + + # get current canvas dimensions + canvas = self.app.canvas + plot = canvas.find_withtag("rectangle") + x0, y0, x1, y1 = canvas.bbox(plot[0]) + width = abs(x0 - x1) - 2 + height = abs(y0 - y1) - 2 + self.pixel_width = tk.IntVar(value=width) + self.pixel_height = tk.IntVar(value=height) + self.meters_width = tk.IntVar(value=width * self.meter_per_pixel) + self.meters_height = tk.IntVar(value=height * self.meter_per_pixel) + self.scale = tk.IntVar(value=self.meter_per_pixel * 100) + self.x = tk.IntVar(value=0) + self.y = tk.IntVar(value=0) + self.lat = tk.DoubleVar(value=47.5791667) + self.lon = tk.DoubleVar(value=-122.132322) + self.alt = tk.DoubleVar(value=2.0) + self.save_default = tk.BooleanVar(value=False) + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.draw_size() + self.draw_scale() + self.draw_reference_point() + self.draw_save_as_default() + self.draw_buttons() + + def draw_size(self): + label = tk.Label(self, text="Size", font=self.section_font) + label.grid(sticky="w") + + # draw size row 1 + frame = tk.Frame(self) + frame.grid(sticky="ew", pady=3) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(3, weight=1) + label = tk.Label(frame, text="Width") + label.grid(row=0, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.pixel_width) + entry.grid(row=0, column=1, sticky="ew") + label = tk.Label(frame, text="x Height") + label.grid(row=0, column=2, sticky="w") + entry = tk.Entry(frame, textvariable=self.pixel_height) + entry.grid(row=0, column=3, sticky="ew") + label = tk.Label(frame, text="Pixels") + label.grid(row=0, column=4, sticky="w") + + # draw size row 2 + frame = tk.Frame(self) + frame.grid(sticky="ew", pady=3) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(3, weight=1) + label = tk.Label(frame, text="Width") + label.grid(row=0, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.meters_width) + entry.grid(row=0, column=1, sticky="ew") + label = tk.Label(frame, text="x Height") + label.grid(row=0, column=2, sticky="w") + entry = tk.Entry(frame, textvariable=self.meters_height) + entry.grid(row=0, column=3, sticky="ew") + label = tk.Label(frame, text="Meters") + label.grid(row=0, column=4, sticky="w") + + def draw_scale(self): + label = tk.Label(self, text="Scale", font=self.section_font) + label.grid(sticky="w") + + frame = tk.Frame(self) + frame.grid(sticky="ew") + frame.columnconfigure(1, weight=1) + label = tk.Label(frame, text="100 Pixels =") + label.grid(row=0, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.scale) + entry.grid(row=0, column=1, sticky="ew") + label = tk.Label(frame, text="Meters") + label.grid(row=0, column=2, sticky="w") + + def draw_reference_point(self): + label = tk.Label(self, text="Reference point", font=self.section_font) + label.grid(sticky="w") + label = tk.Label( + self, text="Default is (0, 0), the upper left corner of the canvas" + ) + label.grid(sticky="w") + + frame = tk.Frame(self) + frame.grid(sticky="ew", pady=3) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(3, weight=1) + + label = tk.Label(frame, text="X") + label.grid(row=0, column=0, sticky="w") + x_var = tk.StringVar(value=0) + entry = tk.Entry(frame, textvariable=x_var) + entry.grid(row=0, column=1, sticky="ew") + + label = tk.Label(frame, text="Y") + label.grid(row=0, column=2, sticky="w") + y_var = tk.StringVar(value=0) + entry = tk.Entry(frame, textvariable=y_var) + entry.grid(row=0, column=3, sticky="ew") + + label = tk.Label(self, text="Translates To") + label.grid(sticky="w") + + frame = tk.Frame(self) + frame.grid(sticky="ew", pady=3) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(3, weight=1) + frame.columnconfigure(5, weight=1) + + label = tk.Label(frame, text="Lat") + label.grid(row=0, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.lat) + entry.grid(row=0, column=1, sticky="ew") + + label = tk.Label(frame, text="Lon") + label.grid(row=0, column=2, sticky="w") + entry = tk.Entry(frame, textvariable=self.lon) + entry.grid(row=0, column=3, sticky="ew") + + label = tk.Label(frame, text="Alt") + label.grid(row=0, column=4, sticky="w") + entry = tk.Entry(frame, textvariable=self.alt) + entry.grid(row=0, column=5, sticky="ew") + + def draw_save_as_default(self): + button = tk.Checkbutton( + self, text="Save as default?", variable=self.save_default + ) + button.grid(sticky="w", pady=3) + + def draw_buttons(self): + frame = tk.Frame(self) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.grid(sticky="ew") + + button = tk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, pady=5, sticky="ew") + + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, pady=5, sticky="ew") + + def redraw_grid(self): + """ + redraw grid with new dimension + + :return: nothing + """ + width, height = self.pixel_width.get(), self.pixel_height.get() + + canvas = self.app.canvas + canvas.config(scrollregion=(0, 0, width + 200, height + 200)) + + # delete old plot and redraw + for i in canvas.find_withtag("gridline"): + canvas.delete(i) + for i in canvas.find_withtag("rectangle"): + canvas.delete(i) + + canvas.draw_grid(width=width, height=height) + # lift anything that is drawn on the plot before + for tag in DRAW_OBJECT_TAGS: + for i in canvas.find_withtag(tag): + canvas.lift(i) + + def click_apply(self): + meter_per_pixel = float(self.scale.get()) / 100 + self.app.canvas.meters_per_pixel = meter_per_pixel + self.redraw_grid() + # if there is a current wallpaper showing, redraw it based on current wallpaper options + wallpaper_tool = self.app.set_wallpaper + current_wallpaper = self.app.current_wallpaper + if current_wallpaper: + if self.app.adjust_to_dim_var.get() == 0: + if self.app.radiovar.get() == ScaleOption.UPPER_LEFT.value: + wallpaper_tool.upper_left(current_wallpaper) + elif self.app.radiovar.get() == ScaleOption.CENTERED.value: + wallpaper_tool.center(current_wallpaper) + elif self.app.radiovar.get() == ScaleOption.SCALED.value: + wallpaper_tool.scaled(current_wallpaper) + elif self.app.radiovar.get() == ScaleOption.TILED.value: + print("not implemented") + elif self.app.adjust_to_dim_var.get() == 1: + wallpaper_tool.canvas_to_image_dimension(current_wallpaper) + wallpaper_tool.show_grid() + self.destroy() diff --git a/coretk/coretk/dialogs/sizeandscale.py b/coretk/coretk/dialogs/sizeandscale.py deleted file mode 100644 index dbc5a29c..00000000 --- a/coretk/coretk/dialogs/sizeandscale.py +++ /dev/null @@ -1,277 +0,0 @@ -""" -size and scale -""" -import tkinter as tk -from functools import partial - -from coretk.dialogs.canvasbackground import ScaleOption - -DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] - - -class SizeAndScale: - def __init__(self, app): - """ - create an instance for size and scale object - - :param app: main application - """ - self.app = app - self.top = tk.Toplevel() - self.top.title("Canvas Size and Scale") - self.meter_per_pixel = self.app.canvas.meters_per_pixel - - self.size_chart() - self.scale_chart() - self.reference_point_chart() - self.save_as_default() - self.apply_cancel() - - def pixel_scrollbar_command(self, size_frame, entry_row, entry_column, event): - """ - change the value shown based on scrollbar action - - :param tkinter.Frame frame: pixel dimension frame - :param int entry_row: row number of entry of the frame - :param int entry_column: column number of entry of the frame - :param event: scrollbar event - :return: nothing - """ - pixel_frame = size_frame.grid_slaves(0, 0)[0] - pixel_entry = pixel_frame.grid_slaves(entry_row, entry_column)[0] - val = int(pixel_entry.get()) - - if event == "-1": - new_val = val + 2 - elif event == "1": - new_val = val - 2 - - pixel_entry.delete(0, tk.END) - pixel_entry.insert(tk.END, str(new_val)) - - # change meter dimension - meter_frame = size_frame.grid_slaves(1, 0)[0] - meter_entry = meter_frame.grid_slaves(entry_row, entry_column)[0] - meter_entry.delete(0, tk.END) - meter_entry.insert(tk.END, str(new_val * self.meter_per_pixel)) - - def meter_scrollbar_command(self, size_frame, entry_row, entry_column, event): - """ - change the value shown based on scrollbar action - - :param tkinter.Frame size_frame: size frame - :param int entry_row: row number of entry in the frame it is contained in - :param int entry_column: column number of entry in the frame in is contained in - :param event: scroolbar event - :return: nothing - """ - meter_frame = size_frame.grid_slaves(1, 0)[0] - meter_entry = meter_frame.grid_slaves(entry_row, entry_column)[0] - val = float(meter_entry.get()) - - if event == "-1": - val += 100.0 - elif event == "1": - val -= 100.0 - meter_entry.delete(0, tk.END) - meter_entry.insert(tk.END, str(val)) - - # change pixel dimension - pixel_frame = size_frame.grid_slaves(0, 0)[0] - pixel_entry = pixel_frame.grid_slaves(entry_row, entry_column)[0] - pixel_entry.delete(0, tk.END) - pixel_entry.insert(tk.END, str(int(val / self.meter_per_pixel))) - - def create_text_label(self, frame, text, row, column, sticky=None): - """ - create text label - :param tkinter.Frame frame: parent frame - :param str text: label text - :param int row: row number - :param int column: column number - :param sticky: sticky value - - :return: nothing - """ - text_label = tk.Label(frame, text=text) - text_label.grid(row=row, column=column, sticky=sticky, padx=3, pady=3) - - def create_entry(self, frame, default_value, row, column, width): - text_var = tk.StringVar(frame, value=str(default_value)) - entry = tk.Entry( - frame, textvariable=text_var, width=width, bg="white", state=tk.NORMAL - ) - entry.focus() - entry.grid(row=row, column=column, padx=3, pady=3) - - def size_chart(self): - label = tk.Label(self.top, text="Size") - label.grid(sticky=tk.W, padx=5) - - canvas = self.app.canvas - plot = canvas.find_withtag("rectangle") - x0, y0, x1, y1 = canvas.bbox(plot[0]) - w = abs(x0 - x1) - 2 - h = abs(y0 - y1) - 2 - - f = tk.Frame( - self.top, - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", - highlightthickness=0.5, - bd=0, - ) - - f1 = tk.Frame(f) - pw_scrollbar = tk.Scrollbar(f1, orient=tk.VERTICAL) - pw_scrollbar.grid(row=0, column=1) - self.create_entry(f1, w, 0, 0, 6) - pw_scrollbar.config(command=partial(self.pixel_scrollbar_command, f, 0, 0)) - - self.create_text_label(f1, " W x ", 0, 2) - - scrollbar = tk.Scrollbar(f1, orient=tk.VERTICAL) - scrollbar.grid(row=0, column=4) - self.create_entry(f1, h, 0, 3, 6) - scrollbar.config(command=partial(self.pixel_scrollbar_command, f, 0, 3)) - self.create_text_label(f1, " H pixels ", 0, 7) - f1.grid(sticky=tk.W, pady=3) - - f2 = tk.Frame(f) - scrollbar = tk.Scrollbar(f2, orient=tk.VERTICAL) - scrollbar.grid(row=0, column=1) - self.create_entry(f2, w * self.meter_per_pixel, 0, 0, 8) - scrollbar.config(command=partial(self.meter_scrollbar_command, f, 0, 0)) - self.create_text_label(f2, " x ", 0, 2) - - scrollbar = tk.Scrollbar(f2, orient=tk.VERTICAL) - scrollbar.grid(row=0, column=4) - self.create_entry(f2, h * self.meter_per_pixel, 0, 3, 8) - scrollbar.config(command=partial(self.meter_scrollbar_command, f, 0, 3)) - self.create_text_label(f2, " meters ", 0, 5) - - f2.grid(sticky=tk.W, pady=3) - - f.grid(sticky=tk.W + tk.E, padx=5, pady=5, columnspan=2) - - def scale_chart(self): - label = tk.Label(self.top, text="Scale") - label.grid(padx=5, sticky=tk.W) - f = tk.Frame( - self.top, - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", - highlightthickness=0.5, - bd=0, - ) - # self.create_text_label(f, "Scale", 0, 0, tk.W) - # f1 = tk.Frame(f) - self.create_text_label(f, "100 pixels = ", 0, 0) - self.create_entry(f, self.meter_per_pixel * 100, 0, 1, 10) - self.create_text_label(f, "meters", 0, 2) - # f1.grid(sticky=tk.W, pady=3) - f.grid(sticky=tk.W + tk.E, padx=5, pady=5, columnspan=2) - - def reference_point_chart(self): - label = tk.Label(self.top, text="Reference point") - label.grid(padx=5, sticky=tk.W) - - f = tk.Frame( - self.top, - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", - highlightthickness=0.5, - bd=0, - ) - self.create_text_label( - f, - "The default reference point is (0, 0), the upper left corner of the canvas.", - 1, - 0, - tk.W, - ) - f1 = tk.Frame(f) - self.create_entry(f1, 0, 0, 0, 4) - self.create_text_label(f1, " X, ", 0, 1) - self.create_entry(f1, 0, 0, 2, 4) - self.create_text_label(f1, "Y = ", 0, 3) - self.create_entry(f1, 47.5791667, 0, 4, 13) - self.create_text_label(f1, " lat, ", 0, 5) - self.create_entry(f1, -122.132322, 0, 6, 13) - self.create_text_label(f1, "long", 0, 7) - f1.grid(row=2, column=0, sticky=tk.W, pady=3) - - f2 = tk.Frame(f) - self.create_text_label(f2, "Altitude: ", 0, 0) - self.create_entry(f2, 2.0, 0, 1, 11) - self.create_text_label(f2, " meters ", 0, 2) - f2.grid(row=3, column=0, sticky=tk.W, pady=3) - - f.grid(sticky=tk.W, padx=5, pady=5, columnspan=2) - - def save_as_default(self): - var = tk.IntVar() - button = tk.Checkbutton(self.top, text="Save as default", variable=var) - button.grid(sticky=tk.W, padx=5, pady=5, columnspan=2) - - def redraw_grid(self, pixel_width, pixel_height): - """ - redraw grid with new dimension - - :param int pixel_width: width in pixel - :param int pixel_height: height in pixel - :return: nothing - """ - canvas = self.app.canvas - canvas.config(scrollregion=(0, 0, pixel_width + 200, pixel_height + 200)) - - # delete old plot and redraw - for i in canvas.find_withtag("gridline"): - canvas.delete(i) - for i in canvas.find_withtag("rectangle"): - canvas.delete(i) - - canvas.draw_grid(width=pixel_width, height=pixel_height) - # lift anything that is drawn on the plot before - for tag in DRAW_OBJECT_TAGS: - for i in canvas.find_withtag(tag): - canvas.lift(i) - - def click_apply(self): - size_frame = self.top.grid_slaves(1, 0)[0] - pixel_size_frame = size_frame.grid_slaves(0, 0)[0] - - pixel_width = int(pixel_size_frame.grid_slaves(0, 0)[0].get()) - pixel_height = int(pixel_size_frame.grid_slaves(0, 3)[0].get()) - - scale_frame = self.top.grid_slaves(3, 0)[0] - meter_per_pixel = float(scale_frame.grid_slaves(0, 1)[0].get()) / 100 - self.app.canvas.meters_per_pixel = meter_per_pixel - self.redraw_grid(pixel_width, pixel_height) - print(self.app.current_wallpaper) - print(self.app.radiovar) - # if there is a current wallpaper showing, redraw it based on current wallpaper options - wallpaper_tool = self.app.set_wallpaper - current_wallpaper = self.app.current_wallpaper - if current_wallpaper: - if self.app.adjust_to_dim_var.get() == 0: - if self.app.radiovar.get() == ScaleOption.UPPER_LEFT.value: - wallpaper_tool.upper_left(current_wallpaper) - elif self.app.radiovar.get() == ScaleOption.CENTERED.value: - wallpaper_tool.center(current_wallpaper) - elif self.app.radiovar.get() == ScaleOption.SCALED.value: - wallpaper_tool.scaled(current_wallpaper) - elif self.app.radiovar.get() == ScaleOption.TILED.value: - print("not implemented") - elif self.app.adjust_to_dim_var.get() == 1: - wallpaper_tool.canvas_to_image_dimension(current_wallpaper) - - wallpaper_tool.show_grid() - - self.top.destroy() - - def apply_cancel(self): - apply_button = tk.Button(self.top, text="Apply", command=self.click_apply) - apply_button.grid(row=7, column=0, pady=5) - cancel_button = tk.Button(self.top, text="Cancel", command=self.top.destroy) - cancel_button.grid(row=7, column=1, pady=5) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index bbb72895..7971553d 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -9,10 +9,10 @@ from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 from coretk.appdirs import XML_PATH from coretk.dialogs.canvasbackground import CanvasBackgroundDialog +from coretk.dialogs.canvassizeandscale import SizeAndScaleDialog from coretk.dialogs.hooks import HooksDialog from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog -from coretk.dialogs.sizeandscale import SizeAndScale def sub_menu_items(): @@ -155,15 +155,6 @@ def canvas_delete(): logging.debug("Click canvas delete") -def canvas_size_scale(): - logging.debug("Click canvas size/scale") - SizeAndScale() - - -def canvas_wallpaper(): - logging.debug("CLick canvas wallpaper") - - def canvas_previous(): logging.debug("Click canvas previous") @@ -383,7 +374,8 @@ class MenuAction: # self.application.core_editbar.create_toolbar() def canvas_size_and_scale(self): - self.app.size_and_scale = SizeAndScale(self.app) + dialog = SizeAndScaleDialog(self.app, self.app) + dialog.show() def canvas_set_wallpaper(self): dialog = CanvasBackgroundDialog(self.app, self.app) From bd7055a87c81f60f30e9e20e561675ad21fb04c0 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 4 Nov 2019 15:43:05 -0800 Subject: [PATCH 169/462] cleared out old files, leftovers from merge --- coretk/coretk/coreclient.py | 2 +- coretk/coretk/coregrpc.py | 303 ---------------- coretk/coretk/custom_node.py | 19 - coretk/coretk/grpcmanagement.py | 337 ------------------ coretk/coretk/oldimage/OVS.gif | Bin 744 -> 0 bytes coretk/coretk/oldimage/core-icon.png | Bin 2931 -> 0 bytes .../coretk/oldimage/document-properties.gif | Bin 635 -> 0 bytes coretk/coretk/oldimage/host.gif | Bin 1189 -> 0 bytes coretk/coretk/oldimage/hub.gif | Bin 719 -> 0 bytes coretk/coretk/oldimage/lanswitch.gif | Bin 744 -> 0 bytes coretk/coretk/oldimage/link.gif | Bin 86 -> 0 bytes coretk/coretk/oldimage/marker.gif | Bin 375 -> 0 bytes coretk/coretk/oldimage/mdr.gif | Bin 1276 -> 0 bytes coretk/coretk/oldimage/observe.gif | Bin 1149 -> 0 bytes coretk/coretk/oldimage/oval.gif | Bin 174 -> 0 bytes coretk/coretk/oldimage/pc.gif | Bin 1300 -> 0 bytes coretk/coretk/oldimage/plot.gif | Bin 265 -> 0 bytes coretk/coretk/oldimage/rectangle.gif | Bin 160 -> 0 bytes coretk/coretk/oldimage/rj45.gif | Bin 755 -> 0 bytes coretk/coretk/oldimage/router.gif | Bin 1152 -> 0 bytes coretk/coretk/oldimage/router_green.gif | Bin 753 -> 0 bytes coretk/coretk/oldimage/run.gif | Bin 324 -> 0 bytes coretk/coretk/oldimage/select.gif | Bin 925 -> 0 bytes coretk/coretk/oldimage/start.gif | Bin 1131 -> 0 bytes coretk/coretk/oldimage/stop.gif | Bin 1204 -> 0 bytes coretk/coretk/oldimage/text.gif | Bin 127 -> 0 bytes coretk/coretk/oldimage/tunnel.gif | Bin 799 -> 0 bytes coretk/coretk/oldimage/twonode.gif | Bin 220 -> 0 bytes coretk/coretk/oldimage/wlan.gif | Bin 146 -> 0 bytes 29 files changed, 1 insertion(+), 660 deletions(-) delete mode 100644 coretk/coretk/coregrpc.py delete mode 100644 coretk/coretk/custom_node.py delete mode 100644 coretk/coretk/grpcmanagement.py delete mode 100755 coretk/coretk/oldimage/OVS.gif delete mode 100644 coretk/coretk/oldimage/core-icon.png delete mode 100644 coretk/coretk/oldimage/document-properties.gif delete mode 100644 coretk/coretk/oldimage/host.gif delete mode 100644 coretk/coretk/oldimage/hub.gif delete mode 100644 coretk/coretk/oldimage/lanswitch.gif delete mode 100644 coretk/coretk/oldimage/link.gif delete mode 100644 coretk/coretk/oldimage/marker.gif delete mode 100644 coretk/coretk/oldimage/mdr.gif delete mode 100644 coretk/coretk/oldimage/observe.gif delete mode 100644 coretk/coretk/oldimage/oval.gif delete mode 100644 coretk/coretk/oldimage/pc.gif delete mode 100644 coretk/coretk/oldimage/plot.gif delete mode 100644 coretk/coretk/oldimage/rectangle.gif delete mode 100644 coretk/coretk/oldimage/rj45.gif delete mode 100644 coretk/coretk/oldimage/router.gif delete mode 100644 coretk/coretk/oldimage/router_green.gif delete mode 100644 coretk/coretk/oldimage/run.gif delete mode 100644 coretk/coretk/oldimage/select.gif delete mode 100644 coretk/coretk/oldimage/start.gif delete mode 100644 coretk/coretk/oldimage/stop.gif delete mode 100644 coretk/coretk/oldimage/text.gif delete mode 100644 coretk/coretk/oldimage/tunnel.gif delete mode 100644 coretk/coretk/oldimage/twonode.gif delete mode 100644 coretk/coretk/oldimage/wlan.gif diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 08753781..0063a0c8 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -539,7 +539,7 @@ class CoreClient: """ Create the interface for the two end of an edge, add a copy to node's interfaces - :param coretk.grpcmanagement.Edge edge: edge to add interfaces to + :param coretk.coreclient.Edge edge: edge to add interfaces to :param int src_canvas_id: canvas id for the source node :param int dst_canvas_id: canvas id for the destination node :return: nothing diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py deleted file mode 100644 index 5db0cc6d..00000000 --- a/coretk/coretk/coregrpc.py +++ /dev/null @@ -1,303 +0,0 @@ -""" -Incorporate grpc into python tkinter GUI -""" -import logging -import os - -from core.api.grpc import client, core_pb2 -from coretk.dialogs.sessions import SessionsDialog -from coretk.linkinfo import Throughput -from coretk.wirelessconnection import WirelessConnection - - -class CoreGrpc: - def __init__(self, app, sid=None): - """ - Create a CoreGrpc instance - """ - self.core = client.CoreGrpcClient() - self.session_id = sid - self.node_ids = [] - self.app = app - self.master = app.master - self.interface_helper = None - self.throughput_draw = Throughput(app.canvas, self) - self.wireless_draw = WirelessConnection(app.canvas, self) - - def log_event(self, event): - # logging.info("event: %s", event) - if event.link_event is not None: - logging.info("event: %s", event) - - self.wireless_draw.hangle_link_event(event.link_event) - - def log_throughput(self, event): - interface_throughputs = event.interface_throughputs - for i in interface_throughputs: - print("") - return - throughputs_belong_to_session = [] - for if_tp in interface_throughputs: - if if_tp.node_id in self.node_ids: - throughputs_belong_to_session.append(if_tp) - # bridge_throughputs = event.bridge_throughputs - self.throughput_draw.process_grpc_throughput_event( - throughputs_belong_to_session - ) - - def create_new_session(self): - """ - Create a new session - - :return: nothing - """ - response = self.core.create_session() - logging.info("created session: %s", response) - - # handle events session may broadcast - self.session_id = response.session_id - self.master.title("CORE Session ID " + str(self.session_id)) - self.core.events(self.session_id, self.log_event) - # self.core.throughputs(self.log_throughput) - - def delete_session(self, custom_sid=None): - if custom_sid is None: - sid = self.session_id - else: - sid = custom_sid - response = self.core.delete_session(sid) - logging.info("Deleted session result: %s", response) - - def terminate_session(self, custom_sid=None): - if custom_sid is None: - sid = self.session_id - else: - sid = custom_sid - s = self.core.get_session(sid).session - # delete links and nodes from running session - if s.state == core_pb2.SessionState.RUNTIME: - self.set_session_state("datacollect", sid) - self.delete_links(sid) - self.delete_nodes(sid) - self.delete_session(sid) - - def set_up(self): - """ - Query sessions, if there exist any, prompt whether to join one - - :return: existing sessions - """ - self.core.connect() - response = self.core.get_sessions() - - # if there are no sessions, create a new session, else join a session - sessions = response.sessions - if len(sessions) == 0: - self.create_new_session() - else: - dialog = SessionsDialog(self.app, self.app) - dialog.show() - - def get_session_state(self): - response = self.core.get_session(self.session_id) - # logging.info("get session: %s", response) - return response.session.state - - def set_session_state(self, state, custom_session_id=None): - """ - Set session state - - :param str state: session state to set - :return: nothing - """ - if custom_session_id is None: - sid = self.session_id - else: - sid = custom_session_id - - if state == "configuration": - response = self.core.set_session_state( - sid, core_pb2.SessionState.CONFIGURATION - ) - elif state == "instantiation": - response = self.core.set_session_state( - sid, core_pb2.SessionState.INSTANTIATION - ) - elif state == "datacollect": - response = self.core.set_session_state( - sid, core_pb2.SessionState.DATACOLLECT - ) - elif state == "shutdown": - response = self.core.set_session_state(sid, core_pb2.SessionState.SHUTDOWN) - elif state == "runtime": - response = self.core.set_session_state(sid, core_pb2.SessionState.RUNTIME) - elif state == "definition": - response = self.core.set_session_state( - sid, core_pb2.SessionState.DEFINITION - ) - elif state == "none": - response = self.core.set_session_state(sid, core_pb2.SessionState.NONE) - else: - logging.error("coregrpc.py: set_session_state: INVALID STATE") - - logging.info("set session state: %s", response) - - # def add_node(self, node_type, model, x, y, name, node_id): - # position = core_pb2.Position(x=x, y=y) - # node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) - # self.node_ids.append(node_id) - # response = self.core.add_node(self.session_id, node) - # logging.info("created node: %s", response) - # if node_type == core_pb2.NodeType.WIRELESS_LAN: - # d = OrderedDict() - # d["basic_range"] = "275" - # d["bandwidth"] = "54000000" - # d["jitter"] = "0" - # d["delay"] = "20000" - # d["error"] = "0" - # r = self.core.set_wlan_config(self.session_id, node_id, d) - # logging.debug("set wlan config %s", r) - # return response.node_id - - def edit_node(self, node_id, x, y): - position = core_pb2.Position(x=x, y=y) - response = self.core.edit_node(self.session_id, node_id, position) - logging.info("updated node id %s: %s", node_id, response) - # self.core.events(self.session_id, self.log_event) - - def delete_nodes(self, delete_session=None): - if delete_session is None: - sid = self.session_id - else: - sid = delete_session - for node in self.core.get_session(sid).session.nodes: - response = self.core.delete_node(self.session_id, node.id) - logging.info("delete nodes %s", response) - - def delete_links(self, delete_session=None): - # sid = None - if delete_session is None: - sid = self.session_id - else: - sid = delete_session - - for link in self.core.get_session(sid).session.links: - response = self.core.delete_link( - self.session_id, - link.node_one_id, - link.node_two_id, - link.interface_one.id, - link.interface_two.id, - ) - logging.info("delete links %s", response) - - def create_interface(self, node_type, gui_interface): - """ - create a protobuf interface given the interface object stored by the programmer - - :param core_bp2.NodeType type: node type - :param coretk.interface.Interface gui_interface: the programmer's interface object - :rtype: core_bp2.Interface - :return: protobuf interface object - """ - if node_type != core_pb2.NodeType.DEFAULT: - return None - else: - interface = core_pb2.Interface( - id=gui_interface.id, - name=gui_interface.name, - mac=gui_interface.mac, - ip4=gui_interface.ipv4, - ip4mask=gui_interface.ip4prefix, - ) - logging.debug("create interface 1 %s", interface) - - return interface - - # TODO add location, hooks, emane_config, etc... - def start_session( - self, - nodes, - links, - location=None, - hooks=None, - emane_config=None, - emane_model_configs=None, - wlan_configs=None, - mobility_configs=None, - ): - response = self.core.start_session( - session_id=self.session_id, - nodes=nodes, - links=links, - wlan_configs=wlan_configs, - mobility_configs=mobility_configs, - ) - logging.debug("Start session %s, result: %s", self.session_id, response.result) - - def stop_session(self): - response = self.core.stop_session(session_id=self.session_id) - logging.debug("coregrpc.py Stop session, result: %s", response.result) - - # TODO no need, might get rid of this - def add_link(self, id1, id2, type1, type2, edge): - """ - Grpc client request add link - - :param int session_id: session id - :param int id1: node 1 core id - :param core_pb2.NodeType type1: node 1 core node type - :param int id2: node 2 core id - :param core_pb2.NodeType type2: node 2 core node type - :return: nothing - """ - if1 = self.create_interface(type1, edge.interface_1) - if2 = self.create_interface(type2, edge.interface_2) - - response = self.core.add_link(self.session_id, id1, id2, if1, if2) - logging.info("created link: %s", response) - - # self.core.get_node_links(self.session_id, id1) - - # def get_session(self): - # response = self.core.get_session(self.session_id) - # nodes = response.session.nodes - # for node in nodes: - # r = self.core.get_node_links(self.session_id, node.id) - # logging.info(r) - - def launch_terminal(self, node_id): - response = self.core.get_node_terminal(self.session_id, node_id) - logging.info("get terminal %s", response.terminal) - os.system("xterm -e %s &" % response.terminal) - - def save_xml(self, file_path): - """ - Save core session as to an xml file - - :param str file_path: file path that user pick - :return: nothing - """ - response = self.core.save_xml(self.session_id, file_path) - logging.info("coregrpc.py save xml %s", response) - self.core.events(self.session_id, self.log_event) - - def open_xml(self, file_path): - """ - Open core xml - - :param str file_path: file to open - :return: session id - """ - response = self.core.open_xml(file_path) - self.session_id = response.session_id - logging.debug("coreprgc.py open_xml(): %s", response.result) - - def close(self): - """ - Clean ups when done using grpc - - :return: nothing - """ - logging.debug("Close grpc") - self.core.close() diff --git a/coretk/coretk/custom_node.py b/coretk/coretk/custom_node.py deleted file mode 100644 index 5cc8ea75..00000000 --- a/coretk/coretk/custom_node.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -edit node types -""" - -import tkinter as tk - - -class EditNodeTypes: - def __init__(self): - self.top = tk.Toplevel - self.top.title("CORE Node Types") - - def node_types(self): - """ - list box of node types - :return: - """ - lbl = tk.Label(self.top, text="Node types") - lbl.grid() diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py deleted file mode 100644 index ffc1f0f9..00000000 --- a/coretk/coretk/grpcmanagement.py +++ /dev/null @@ -1,337 +0,0 @@ -""" -Manage useful informations about the nodes, edges and configuration -that can be useful for grpc, acts like a session class -""" -import logging - -from core.api.grpc import core_pb2 -from coretk.coretocanvas import CoreToCanvasMapping -from coretk.interface import Interface, InterfaceManager -from coretk.mobilitynodeconfig import MobilityNodeConfig -from coretk.wlannodeconfig import WlanNodeConfig - -link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] -network_layer_nodes = ["router", "host", "PC", "mdr", "prouter", "OVS"] - - -class Node: - def __init__(self, session_id, node_id, node_type, model, x, y, name): - """ - Create an instance of a node - - :param int session_id: session id - :param int node_id: node id - :param core_pb2.NodeType node_type: node type - :param int x: x coordinate - :param int y: coordinate - :param str name: node name - """ - self.session_id = session_id - self.node_id = node_id - self.type = node_type - self.x = x - self.y = y - self.model = model - self.name = name - self.interfaces = [] - - -class Edge: - def __init__(self, session_id, node_id_1, node_type_1, node_id_2, node_type_2): - """ - Create an instance of an edge - :param int session_id: session id - :param int node_id_1: node 1 id - :param int node_type_1: node 1 type - :param core_pb2.NodeType node_id_2: node 2 id - :param core_pb2.NodeType node_type_2: node 2 type - """ - self.session_id = session_id - self.id1 = node_id_1 - self.id2 = node_id_2 - self.type1 = node_type_1 - self.type2 = node_type_2 - self.interface_1 = None - self.interface_2 = None - - -class GrpcManager: - def __init__(self, grpc): - self.nodes = {} - self.edges = {} - self.id = None - # A list of id for re-use, keep in increasing order - self.reusable = [] - - self.preexisting = [] - self.core_grpc = None - - # self.update_preexisting_ids() - # self.core_id_to_canvas_id = {} - self.interfaces_manager = InterfaceManager() - - # map tuple(core_node_id, interface_id) to and edge - # self.node_id_and_interface_to_edge_token = {} - self.core_mapping = CoreToCanvasMapping() - - self.wlanconfig_management = WlanNodeConfig() - self.mobilityconfig_management = MobilityNodeConfig() - - def update_preexisting_ids(self): - """ - get preexisting node ids - :return: - """ - max_id = 0 - client = self.core_grpc.core - sessions = client.get_sessions().sessions - for session_summary in sessions: - session = client.get_session(session_summary.id).session - for node in session.nodes: - if node.id > max_id: - max_id = node.id - self.preexisting.append(node.id) - self.id = max_id + 1 - self.update_reusable_id() - - def peek_id(self): - """ - Peek the next id to be used - - :return: nothing - """ - if len(self.reusable) == 0: - return self.id - else: - return self.reusable[0] - - def get_id(self): - """ - Get the next node id as well as update id status and reusable ids - - :rtype: int - :return: the next id to be used - """ - if len(self.reusable) == 0: - new_id = self.id - self.id = self.id + 1 - return new_id - else: - return self.reusable.pop(0) - - def add_node(self, session_id, canvas_id, x, y, name): - """ - Add node, with information filled in, to grpc manager - - :param int session_id: session id - :param int canvas_id: node's canvas id - :param int x: x coord - :param int y: y coord - :param str name: node type - :return: nothing - """ - node_type = None - node_model = None - if name in link_layer_nodes: - if name == "switch": - node_type = core_pb2.NodeType.SWITCH - elif name == "hub": - node_type = core_pb2.NodeType.HUB - elif name == "wlan": - node_type = core_pb2.NodeType.WIRELESS_LAN - elif name == "rj45": - node_type = core_pb2.NodeType.RJ45 - elif name == "tunnel": - node_type = core_pb2.NodeType.TUNNEL - elif name in network_layer_nodes: - node_type = core_pb2.NodeType.DEFAULT - node_model = name - else: - logging.error("grpcmanagemeny.py INVALID node name") - nid = self.get_id() - create_node = Node(session_id, nid, node_type, node_model, x, y, name) - - # set default configuration for wireless node - self.wlanconfig_management.set_default_config(node_type, nid) - - # set default mobility configuration for wireless node - self.mobilityconfig_management.set_default_configuration(node_type, nid) - - self.nodes[canvas_id] = create_node - self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) - # self.core_id_to_canvas_id[nid] = canvas_id - logging.debug( - "Adding node to GrpcManager.. Session id: %s, Coords: (%s, %s), Name: %s", - session_id, - x, - y, - name, - ) - - def add_preexisting_node(self, canvas_node, session_id, core_node, name): - """ - Add preexisting nodes to grpc manager - - :param str name: node_type - :param core_pb2.Node core_node: core node grpc message - :param coretk.graph.CanvasNode canvas_node: canvas node - :param int session_id: session id - :return: nothing - """ - - # update the next available id - core_id = core_node.id - if self.id is None or core_id >= self.id: - self.id = core_id + 1 - self.preexisting.append(core_id) - n = Node( - session_id, - core_id, - core_node.type, - core_node.model, - canvas_node.x_coord, - canvas_node.y_coord, - name, - ) - self.nodes[canvas_node.id] = n - - def update_node_location(self, canvas_id, new_x, new_y): - """ - update node - - :param int canvas_id: canvas id of that node - :param int new_x: new x coord - :param int new_y: new y coord - :return: nothing - """ - self.nodes[canvas_id].x = new_x - self.nodes[canvas_id].y = new_y - - def update_reusable_id(self): - """ - Update available id for reuse - - :return: nothing - """ - if len(self.preexisting) > 0: - for i in range(1, self.id): - if i not in self.preexisting: - self.reusable.append(i) - - self.preexisting.clear() - logging.debug("Next id: %s, Reusable: %s", self.id, self.reusable) - - def delete_node(self, canvas_id): - """ - Delete a node from the session - - :param int canvas_id: node's id in the canvas - :return: thing - """ - try: - self.nodes.pop(canvas_id) - self.reuseable.append(canvas_id) - self.reuseable.sort() - except KeyError: - logging.error("grpcmanagement.py INVALID NODE CANVAS ID") - - def create_interface(self, edge, src_canvas_id, dst_canvas_id): - """ - Create the interface for the two end of an edge, add a copy to node's interfaces - - :param coretk.grpcmanagement.Edge edge: edge to add interfaces to - :param int src_canvas_id: canvas id for the source node - :param int dst_canvas_id: canvas id for the destination node - :return: nothing - """ - src_interface = None - dst_interface = None - print("create interface") - self.interfaces_manager.new_subnet() - - src_node = self.nodes[src_canvas_id] - if src_node.model in network_layer_nodes: - ifid = len(src_node.interfaces) - name = "eth" + str(ifid) - src_interface = Interface( - name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) - ) - self.nodes[src_canvas_id].interfaces.append(src_interface) - logging.debug( - "Create source interface 1... IP: %s, name: %s", - src_interface.ipv4, - src_interface.name, - ) - - dst_node = self.nodes[dst_canvas_id] - if dst_node.model in network_layer_nodes: - ifid = len(dst_node.interfaces) - name = "eth" + str(ifid) - dst_interface = Interface( - name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) - ) - self.nodes[dst_canvas_id].interfaces.append(dst_interface) - logging.debug( - "Create destination interface... IP: %s, name: %s", - dst_interface.ipv4, - dst_interface.name, - ) - - edge.interface_1 = src_interface - edge.interface_2 = dst_interface - return src_interface, dst_interface - - def add_edge(self, session_id, token, canvas_id_1, canvas_id_2): - """ - Add an edge to grpc manager - - :param int session_id: core session id - :param tuple(int, int) token: edge's identification in the canvas - :param int canvas_id_1: canvas id of source node - :param int canvas_id_2: canvas_id of destination node - - :return: nothing - """ - if canvas_id_1 in self.nodes and canvas_id_2 in self.nodes: - edge = Edge( - session_id, - self.nodes[canvas_id_1].node_id, - self.nodes[canvas_id_1].type, - self.nodes[canvas_id_2].node_id, - self.nodes[canvas_id_2].type, - ) - self.edges[token] = edge - src_interface, dst_interface = self.create_interface( - edge, canvas_id_1, canvas_id_2 - ) - node_one_id = self.nodes[canvas_id_1].node_id - node_two_id = self.nodes[canvas_id_2].node_id - - # provide a way to get an edge from a core node and an interface id - if src_interface is not None: - # self.node_id_and_interface_to_edge_token[tuple([node_one_id, src_interface.id])] = token - self.core_mapping.map_node_and_interface_to_canvas_edge( - node_one_id, src_interface.id, token - ) - logging.debug( - "map node id %s, interface_id %s to edge token %s", - node_one_id, - src_interface.id, - token, - ) - - if dst_interface is not None: - # self.node_id_and_interface_to_edge_token[tuple([node_two_id, dst_interface.id])] = token - self.core_mapping.map_node_and_interface_to_canvas_edge( - node_two_id, dst_interface.id, token - ) - logging.debug( - "map node id %s, interface_id %s to edge token %s", - node_two_id, - dst_interface.id, - token, - ) - - logging.debug("Adding edge to grpc manager...") - else: - logging.error("grpcmanagement.py INVALID CANVAS NODE ID") diff --git a/coretk/coretk/oldimage/OVS.gif b/coretk/coretk/oldimage/OVS.gif deleted file mode 100755 index 38fcbb2ea684f2cb3724f55bb668d66dd952ee4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 744 zcmVJ9wZvc%wRbpgMV>J9(izdZ9gfp+0+}KYOA*e5OBqqdk75L4Bk` zeWOBtq(FhHMS!M3gR4h^r$~aQNrI?Dg|0+}u0w{dOogdTg{n`7s!xckMvAgji>+0Q zu1SuzRgA7lkGD#Xw@Q$=SB#%r9qZk@Ytp1W|LyjZ2mSf$H#qQ6_I&0DF?cci~~ zrNDcq!F#8|eW=5Ks>Fe;#fPuQh_T6uvdNFN%aggzn!VDVz|)|@)uF=GrpDN($JnUI z*{jOhtjpW3%-prm-?-G_xzypg)Z)9<;=R}7%*@Qp%*@Qp%*@QpA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=dv*jE#jucwYQHYIwy=OOkz4Hj~p>W&4i3Z zU|?EVaBDeOZhEkLdR2@d7~9=~8y-G!bar@qcWq-WUF1-iLJ&x!2p8np`UC026(iCh zJzDS}A-9DYGIZ!5v5|<6A4!l-8|AaEc70|*i*d`R)429F;{oItU%<%@ul z7Q3Zf+45z~nKf_b+}ZP|Np6FNvUCYkCQX|-b@KEHbQ>Y5MXMskx^$~ki(gevHEK0! zR;ET=O%?)6_2^ZlTAP-%V2Cc;v3TMBba2S8*t>7d8bD;&uG_Lr69G_J06~HV5GGUz aLDL40AWED>vBKp`qSL5Tt6ohg5CA)88DCBS diff --git a/coretk/coretk/oldimage/core-icon.png b/coretk/coretk/oldimage/core-icon.png deleted file mode 100644 index 0b0ff5aa3a51f71807691032f516d7a15d7da896..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2931 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 diff --git a/coretk/coretk/oldimage/document-properties.gif b/coretk/coretk/oldimage/document-properties.gif deleted file mode 100644 index 732d8436455ba607ff30774ef83faa434789f775..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 635 zcmZ?wbhEHb6krfwI9A3`UtizZ-QL^V)6>`4+Ydxt6Z*URCUj4n)H`Wv|CDJHr_Go& zZN}v3v!=|LJ$2^nX|v}}pFMZR-1#%-Eu6hz(VT@#<}O+?Z}GAP%a$!&x_sgCm5Z0J zTCxI&Rxe$-dg-b)D^{&twiXE1tzNrs?b`LL*REf)X2YuWo7QaFym8yMZ98^v+r4}D z-hI3FAKZQ9*uj%$j-Nbx?EKkN7cQN-eEIUFs~4`{xO(Hp^_#bD+`N6`*6o|O?p(Wh z_u4HGx_egDy&`;YEE0HVit9zMSJ`0=BM zPwqW>djIj$2algUeDdt!vuDqqJb(1!`IDzF9=~}0?D@;5FJHcR`SRtfmoHwweE#Ou zi?^>|zJ2}b^_!P(-oASC_Vv5>Z$Eu}|LN1G&!0Yi`}Xzw_wPTxfBW&{`%fVH2}D1B z{`&dr*Uvvd@b}MOAo%z99|-;X_y6C25W)u(f3h%gG1N2YFaQB4P8is4G}PBC$jQn` zOH0W%^)$&PmQ9?P6RzIdBv~967UJ*YYhIu;UA!+K3@N%$caBBCo?vDZnK?>>} z4H6mGRzwyX@-nz_KC4-2cu1l_BwJ^i<gzK44%5h8Cn(&TO1gfn3+!O Kwon&fum%8seSYfz diff --git a/coretk/coretk/oldimage/host.gif b/coretk/coretk/oldimage/host.gif deleted file mode 100644 index 5bd60ae3d34d9bcd8be206ba2a8a701b19aea319..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1189 zcmb``30Kkw008hm262dT`d1VcH7ko8t5<8Y=8+H2yze8AmB-d>TJ62nYg4kad?v4& zM~5QL6hp;kPN#X`g=i`SsGx=l2;OM*vP`yphyA|8&&!8?{OcqFKn51n00031BLEHn zNC*Id0!Ap-3M58&meYM}^Nii1^A`;HUW+aDPOb$JlayBa^A~QAaVrt~&^ysVU zu~#pg$<9c~y!cbrrA)@p=L&u~%ef84YDQ6|53A zyOhN)Z>=og)RebX6*gBFKC7*0t$xCN#;)aZI%*#_*0XpGPkXscKDVf^?p}MtlfL?r z{)STi^M``Q$GuIDyP7#eFUp6TS;FS0!nVrczj^H)ZQ`~nQ9F0AF*Tvx5@_UB?C>0{ufgN&9cE}$zZ!w_;Nz{*UVt6WVly6 z(m6TOEfNXjqx|X7mt*2l#aPdzNccwFDU}Q)4o~IY1S8%i#nxVJG-+|;J&gZw=hEE2=`yIo!;s0j01%;ph13(4-_1_5q zu>%;8WN#j;`#OSvdLYVS@EK^2sT%CutgEd(_JPmM)a$tvWO-8!ZZMjb+;`VMC~55O zZ8Nu)t;HMEVqV7i7$TIA?NOFFUVpL&d_}6G;!tO9Hwt5&FH99hSJIbFigv>uofpWs zil%hc_f_o8f&6TSug$RC!irVvj|0^3kD=;Q-S+#9SWcSL6V#M4uup%lA`x$XVv5UD z|6pDlk~f#;>(=$uM(SdIU&W(eXoPydho|^6LAH- z5a8TqvnWKEOTGL2@5uXX*GJ%NS|CpVgB{s(IKY-cv>&v9ThKhI(04%|NGmAs4*3eV zF&DNkP4DBLKw}yiL%OoOERP)rdMHmXC1Xvn?aG|Pp=5wzTGz6H@;FVQb$gC}yVs diff --git a/coretk/coretk/oldimage/hub.gif b/coretk/coretk/oldimage/hub.gif deleted file mode 100644 index 17f7c4d3ef726f744da685423057366093df33f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 719 zcmZ?wbhEHbRA3NeIF`X6<6R-+T`3<>s}$0x8qum2(W)NPsSyiAUD`>#I?27dDHHY5 zChDb6G|ZS{kU7OLYpPMsG?V;UrunnXi|3jb&$BFDXi>JnvTT7>*?jBr1(p@_Eh`pS zRm`^nk&CQB{tt%H;S1z=vTxeUh(6(loea&Lqx}}bFOPuPKIM)NwQisM> zt_{l^n^$`_t#EE#=hC{)xow?q+bXxNO@WAtOJ`nH{!xc%(pooDClzr6I=^<~Fy zEI)o@`=tlFu0Gm*?eU&#kM~}Ge1=Lu@h1x-7ehUR4g(N?;)H?yUqgLUb4zPmdq<0u zTu*Obf4`D_=L9(`C!;~HitzeCPr?FNJxqa zbCy%KUAIBbJ2m~}sncho)np9qjxL(X>fF`|N{-XJqoblAaq)7Kbe~IXuu89G zY>4^!<#n%FX}%(x=oD7Zx(x^D7Cd=>$48HrD@OVHGfFIMB$#qu{aN1LM(d zN%JxW&7zjh$r?;@OmoaOK04YXZC%G>x#`KtDFRD*voZx2yYdE7cm|vGV*3$4sHUKQA;Xt`D2tb#>OIgNj>peGaF!W-$oH tY>1eD&xfTUiIIg<%3(%;VvDJ9wZvc%wRbpgMV>J9(izdZ9gfp+0+}KYOA*e5OBqqdk75L4Bk` zeWOBtq(FhHMS!M3gR4h^r$~aQNrI?Dg|0+}u0w{dOogdTg{n`7s!xckMvAgji>+0Q zu1SuzRgA7lkGD#Xw@Q$=SB#%r9qZk@Ytp1W|LyjZ2mSf$H#qQ6_I&0DF?cci~~ zrNDcq!F#8|eW=5Ks>Fe;#fPuQh_T6uvdNFN%aggzn!VDVz|)|@)uF=GrpDN($JnUI z*{jOhtjpW3%-prm-?-G_xzypg)Z)9<;=R}7%*@Qp%*@Qp%*@QpA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=dv*jE#jucwYQHYIwy=OOkz4Hj~p>W&4i3Z zU|?EVaBDeOZhEkLdR2@d7~9=~8y-G!bar@qcWq-WUF1-iLJ&x!2p8np`UC026(iCh zJzDS}A-9DYGIZ!5v5|<6A4!l-8|AaEc70|*i*d`R)429F;{oItU%<%@ul z7Q3Zf+45z~nKf_b+}ZP|Np6FNvUCYkCQX|-b@KEHbQ>Y5MXMskx^$~ki(gevHEK0! zR;ET=O%?)6_2^ZlTAP-%V2Cc;v3TMBba2S8*t>7d8bD;&uG_Lr69G_J06~HV5GGUz aLDL40AWED>vBKp`qSL5Tt6ohg5CA)88DCBS diff --git a/coretk/coretk/oldimage/link.gif b/coretk/coretk/oldimage/link.gif deleted file mode 100644 index 55532ecf0d14eecb81f57e71ae8f90cd9c8f2fea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86 zcmZ?wbhEHblwgox_`m=Kia%Kx85kHDbU=KN3M?hZsq{JzSM|!8CHm iM7a#L1g zw35rPkmRJB!>gvuyrA2_kkrJV)Xlf=-J;{txy;PW+SI|;*4E(H*5%&Z_wUx>;o<-P z|NsC0A^8LW002J#ECc`q02lxm000J*z@KnPED`}ofN^OAs3wpIl1UYa7>b8-NL-kM<*DutFxg;BP1>~th8?)DLBExWd|U~ Vk^lwFilNX&NxIh8*xA-W06UFctKt9v diff --git a/coretk/coretk/oldimage/mdr.gif b/coretk/coretk/oldimage/mdr.gif deleted file mode 100644 index d6762f6500828ead06fb73c7a9061165ccdef85b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1276 zcmb``jXTo`0KoCzh-N62mQF9`y{bhS8&Di5W%q3E|!j5ZXj{V1dP z7^69zTAohjVrksC>#Y$b%3K=zJhSp5qbmMJTLzOyzDB>y-XS*imde`bawS84 zmw2%6!-V0cL*B2(Jh`PG1(ilOjJz+z~L3DWyE~ zO;xt!+NFMoy%loArqJ-l_Jm(**X)msz`k|c5+7%ztE@5^S_rHsgwwkfx+OT=Z$;za z1?W%IUsZdN*kB!mhEtzXfV>X7ht>Edn=uv}N8`!@xG`X6rD(8m2lfq+q_=}^$LNJe zrch2W6c0TaQtZgz-JcRb=$y)|Wj z-BiLi_^0&q5I9?(v7Hx8apXh0#<03WyT?yj39`SYQ1$*P0GqVK6ELG(0k9Ks?dS+d z3pf%;4@qAVb9%@ro@T;!2e%9u$e;%Wd0%mEb+qw`b4TNcjXCa=mAQUIuzk0S85EZ* zBUtH@1DtHVzSAt$x4=OWCFxq)a-ePq&RPZ!3`gHsVKo}*{{&+f1UUU>gn=xwL|_AW r4z4Sh`#3ytbr^3Q6*$HAfFoFup6oRk3ar@WthBAq6&LC31nl__5JiQ; diff --git a/coretk/coretk/oldimage/observe.gif b/coretk/coretk/oldimage/observe.gif deleted file mode 100644 index 6b66e7305f35484095a2182ed828c6507c72a746..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1149 zcmd_pYfn=L0LJlylbbZAPQ->d0;0q*%+PGvh{hCQEG*WwP@oJp1|%#lOP0+=yD9^E zJMC#%P#A&~sl5%f$fdVaENf{gXltbuDYVeS>4mnZW77@Ay;;_Njy?H(f#=2lWZqG%4!O^wyx&OriRAmhAY&|RN9pvX;*32s2z+p0&S8&Q(U~J z`1HDh)T$s}R}x#5q*i6+PpZl`6}e4S)vl^;SJiZ=Yv^hUU0qAp)G{=mF*Nl|O&t@i zXTqPe;4j#41E_80z%3B`6)#3hlP=O(9c(TGgqR=%a(E!f5Ddi(3PnRA>5yz#qJh;~oqAfY1LaE{BhQ%km%&jd zZ)}wU1Re`@`9oJa7`GpO_2 znf6ZT%y*~P#QJrKeqCd6rkk^g3>#8|2exj&Pd3Cen=<33%!K@ELWXCN zky+%P*>~UUlh66(^ZwC!|AUxE{;{~g*yF%>j3qE(2~5TX71p5gNl<0m(mag`!3!a6 zj6F1E4?SEA|F#s-FGUP<7R!Rey5O)mJx^D>3o)zS8Hdlb>yO>g zpS$Rb*U=ZRf&bZZ{@I%V0OLSG{CB(wAmv}6G4)7z54#w5GAY0Qgx$RVR97jNeyk-K z!%n+IJ#Z+6-;>|?jPuv|H^lhNu21mKffLy5bD4QP?c|#|n0Jp8wco#gbTJ1jr?B#o zpwIZ0DDQaz=@LnV&pIoiXMa6!%xH2@H(fh#3yU%-1l+Ma#DMEE?9C(%k6MeqHBjI^ zNfj8@$s_i!;2S6J=`b1%yJ zjzYQTh5_ro()5X;@-#bfQ6lc4$C74ZnCAn1Vq}@Ai zXX99F^M5aGkwK}khXSw%@jRar$%MOj@_Rb53* zLseZvO+&*#LsMN-TSG%fPg_?@Ti;MmUsumSSJ%+k(AdDh*umJu$k^1_)ZE0}(#*=* z(#pot+Sba}-p06XUk)~1%W z=GOL>_RhA>?vAdW&hFl>u1PZ|Pn|S<=Cnz(7R{J7eaf6Av**s7Id8$7C2Ll!TE1r8 z>UA5|uGz9@%eKuMb{yEe{n+-M+jj2Wv3bvtz5Dml;r#-+uDu;gbhD7>etiD&*~3?# zUc7q#==JB9uU|ZP^Xu`OFR$LbeE9ayleb@AzkBuk{f}4gKfQhb`t^s;?>@YJ`SI7A zk6+$@eE0U#*AJiGfBgL6)0dC$zkdJx_0xxMKfZkb{OSA8&p&)21O&UZ4U+dI^{!gd>D|h#E6iqaFl|zFQsMJyw=OWRyvJa6$Rl!U zR@l1ZMcNNJ3l}mw>9FJ}Z2q#Shs%CVOlO(ttDwnBECCWCiy96tj+>F9s9ci5+v~R4 zt(VKRlY^y$HMgRo`OM^{iN{KBDit4fo9;Yc%kk90LmM147@K$wKlQwB;JwN4(@H0| z)u(1i(xn6iz5D;z&8dl-$AS zrM*MM*<1JUic6;qwoFj+F)Y&%Z06!ucyvOES=E6-%8p~vY3;2Fi@SBUUUBj^*tDgo zN2{iU;jnU_h0_HN#u!6a(QXcBZ~bi#no@LkeR1MeOki2y)~_Pu;>N(uQq(MZ@J~>; z?!kZ+-HH+o4h+K6bT)OUvr9TM2owiQY?jkgTFfED&(h#5pQ+c>=_GGEv0Ye)C&`VO tJw%~hJ~@t1va|g8z`l6e8jtN{jVv^D?$ diff --git a/coretk/coretk/oldimage/plot.gif b/coretk/coretk/oldimage/plot.gif deleted file mode 100644 index 3924adbf821d1cfeb34a0b9efc7e9d79041fd1df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 265 zcmV+k0rvh!Nk%w1VITk?0OJ7w0094gp6-C3?t-B1gQ4!&*x1wB`seBX=<5FI>i+8M z{_E`i?Ck#S?f&lW|9_tDA^saEV<{jqFflS9bZBKDLT_bhZf77vZ*6d4Zg~J9`2+z9 z01p5x00000AOIi$00fut$w{ldIO|=y|6nLmg=3y*s-^|GzOdO&Wo)CfRNr_0?7Wx{ z7<>(jwj&azT$&g-m(u|NL}|P#3J?fbWzrac;9XF=-EZ@i;HCisbU;vTDio=PTh2!- z4^;pSCRKfgT}KOh00w)9eM1P2UXPDMla-Z2nVfzkot>MYnWCeXpQfiYsj51xheIN= PvmP6^x45~wyAc38TK{^> diff --git a/coretk/coretk/oldimage/rectangle.gif b/coretk/coretk/oldimage/rectangle.gif deleted file mode 100644 index ed271f5737d0e8c2e35e9f03735de27509996893..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 160 zcmZ?wbhEHbRA5kGSjfcC(9rPV&GC|Nli|1^))!lG!&+qxHk36QOake}^#rBeM zi_xlcpRkb%J(0K*PL82|tP diff --git a/coretk/coretk/oldimage/rj45.gif b/coretk/coretk/oldimage/rj45.gif deleted file mode 100644 index 9ab7ac56dba8946e37182cf05ca160683f53e2e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 755 zcmZ?wbhEHbRA5kGI2O;~>+9?1=NA+d6cQ2=8X6iN?hzjD84=+X8R;Dr6%`ZX5gi>J z6B82`=NcCmmynQT6n4RsEljD$^YhO@cS6FCQP*6}( zQc_=US6^S>(9qD>*x1z6)Z1&^+iTL%(b3u2Icbu?gb8&MCe%-yIC1ji#;H>)Cr_R{ zZCcfoDO09TFP}cWa{Bb?bLJGyom)6-)~q>m=FFWtcj3b9MT@dltWaIDBy06*okfcl zEnd8M#fp@5>y(!)S+Ze+`id1RR<2yRYSpUMt5>gGyLR2WbsIKp*tBWW=FOY8Y}vAP z>(=etx9`}oW6z#F`}glZaNxkXbJB+n9lCT$_}H;y$B!RBdGh3`Q>RX!K7HoQnKy4Z z-n`*^`AY@?w*HGWo+|t_C-qG3B79HZ>KVjme2?5b& zEz#jV-gD;8n?E<)TfQaAf5D3RfsslI&C!uzn>KIRx+U6NVO@Rn-hKNI9E=XywfoTV zL#jvjoH)DJ{ph-LXCo9Z`uX0xb^Fd;ql*gOuIA>3CMJ58n)ce(Iwr;j78_ zwFCvl;?#p%KCCcW)XlD^vg!rURSp6*7r%Vup0Kc?jagL6VZj0h<|bY?J(HRl`SC3b zyuqttEQOK`I|U_lW>_{XJ;lP^w&RLwAk%&hA&wh48VA%T&VBgp=M(jZ2bpHG$bGPu zdfRWu>8&jPY;I>~*ysT1_lCK_Z;G0L58oIA}pZ@NkT43mOcrUkRiiszUY&owWZYf(DavUI*h z*+R?m`IhAit;!czR?M@im~U0Fz^Y=QHIS@aU=2bGZ7LU9RV}gxp~W^;3vH_wT30W! zt6peVy~w_Lk!{UV+uCJzwM!jq7dzH2v8!9|ShvKkewjnVN{2=uTIJfX%&}>eN7Hi8 zrWKAYYn)ovcr-8fY+m8gvevt0rAzzzz>YO;-J3mnw*+^u4e4I%(YH0ccfI$-ol*T8 zqbF>PnXt)s%5LAOK(r@r;%5Kpd;MqZOPsPLY1-DHSqD<4Z4I1#D0RlR;JHWAXKoLk zcO+!q(X81!vuE$jnX@x&(ea3-r;8WvjaqRodd1n&Mf*w@?=N4nzjE2Zs^tf(mmjKK zakzff;fB>mTGk!w*m$yU>zT57yZQXRtrza^xcp$xwa0s}KRz=GXchvBKUo;L82&TpFaQB4 zPcU%&WBAW07$KqoEo1|3* z!_CHs4~gB{Wjwl{j8FI;7LI4q)tb`Ld9+cY`_9GB?E#YVRbre*mzCJu8YN_JWf(8# z>@kmBq{2Dr(V_#q;`4JRHfIFNX)2%6h}xoYVahuGKn|4$2Or68>$+sav-qg5yleZT z1F{EQ6&e^t<(@SNFFV=0!e!fzj1|m1QimB?R2B#*X}O8mubAUl{8PT+3>%+Lg#jZo zyP&xDo)w8HoMQUPZqw~_6}C39@hP}WC~!Q?E^1uH;kf7sr-*iB!Ruppejj-M;IMAW zj{{9^{fcMi#yL;-o3$(d!6DU*=kH`|K0m)$Jl(wamesE}w|Cd`^Utq&S^qV5`n{@O zJJsWA7O!;-C}nzd{9fJrUiJBotO^$vRQ=p~{C-7Jz!!ez8;L&_GzNq`RCKykx`Bz$ LhNm#efx#L8|Dlf2 diff --git a/coretk/coretk/oldimage/router_green.gif b/coretk/coretk/oldimage/router_green.gif deleted file mode 100644 index 76e3ecd57c59ec99767f5641e701f908f09f0258..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 753 zcmV$ZwARvGsB7!0$gd`+|BqoR^Cx|C0iYY0J zDJqL9D~&5Hku5KgFE5iXF_tkhmNGJzGBlYqG@CRwn>IF_HaDF&IG;EPKS85GJf%E6r9DBULPDfMJ*GWE zq(eTZK0c^EMy5tVsX<4lNJytiLaIVQt3XMpN=&IvL#;zYu0u|$P(`stMzKamu}4#_ zRY$T%RIOG=vqwj?M@hCxSg=}Juw6^JOJB2LU$bIPyiQ`YWKF$IW3*;ZzE5SgXi&dU zP{2@9!BT3tZc@ThYq)P~xo>Q_Z*979RK-+pyK-^7bymn$R>@X$y?1rKd3C>eSj$*< zzk6EFT7krdfyIV|#)^%~l8(xhlg*iw&YG3ao0iX=n9!e@(Vw8yr=!)Xq}8jY*R816 zucz3rtJ<=#+_<#fy|v!Gw%@Eq<#>~vjA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=my(jE#=El$DGfh?9&+W>zhgGe=rwY)+0Jg^nay zZG3!fWM&|jQh9!Ub|jCOf|fFFeYa;FPI7*AUNAtGy_SqBI$bsnKeZk z1rZZ4K!FI3>S252fd&*X6#oDSAb`LD2Ng7J300IRJ9!Q{YVZ)>j z9!jK$fuo0!Bu}QqoGFqd0h>4*Jb*C40)`MMUew63L&%V$NtcEkS@R|XsZ_0E)ymZ? zSfx)g$2x_2c52nDS+{omiWDwWvQN#Ty}MSg+rDB4%@u4{ZQizg!^$i)AaK;ic-iiK ji-ij!x`Q2SKKvv~q(ODlW;XnF$?4M`P`G$4C=dWUiqcBu diff --git a/coretk/coretk/oldimage/run.gif b/coretk/coretk/oldimage/run.gif deleted file mode 100644 index 71dcc67eddc7b829b3221ba4823a04e93c317ba9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 324 zcmZ?wbhEHbRA5kGXkh>WMn*;!78X`kRv8%?d3kvQ0|P@tLt|rOQ&ZEBkdW~3@W#f* zzP`R`)22WfAZwX)2B~gy?XWL&6~Gx-+uY>JSuFtAW4$w*aj%1_PAOIL8t&n-yIt7K68$->CRAkUx!(gbo61M8v(>U}Ah^DZGE(+-Pd9n~wOxE_RahlU()Kf}#eGDh z*YY|kSclnhF*_f*ZL2LVvRih;gcu3Vl7L(W;oNF5dkLndh`{&>RXH)u)s9RO_M-ic zeWf|^96S<2Tyxsf7B~v9GqLh5uUZ+O;*@5-)iu^LcxTxBeY@PPj~q2OIdSsT=`&}~ Ko!3%ium%9#D0q7S diff --git a/coretk/coretk/oldimage/select.gif b/coretk/coretk/oldimage/select.gif deleted file mode 100644 index bb7e128c878317f6287564514a302e5e3c40c10e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 925 zcmX|AT}TvB6#nkcsH?a+J25!wWJw!YSVoy{g=r6ESt%99vPu|jWwavpvRe--w$f%s zYflbuf*4Z;~=R4<~Ip225ShbBKwQc6#Xwq9Tr2mq=851@csnR&8f+ycq>V-cDai3y~@!gr8wL0nBC zU{r)Hb5*5f7u61mZ3t|yIUb`VBe1-QqVleY#wih00Goly!xkwyVZ{1lB8I%Sj2 z2m-M!HhYQ+$jnGNeiv{GnB~nh`yh{T`At9|X4{_DxaFbKW*UGN8*$vq~F1bXlQwoK`C%h5?z2 z^9{6>N|K@L0aob!9DW(7DB*YfpQyG!K!#mAI~x7?(4Lbn6<4Dx zBsmXiOOfS?u)5v0FmUO{muJG>%YKj3_qk`!#gV43zkL%=4mf&UnMa1E`pa7HX6 zwqJ$`n~{uVTRP)1JIzL7vZz_If|{*~lS3(L>1YMNREp5@O@UgWlR;}sSO0_k0ekZL z_4(yVp4S^+tEp{9fME3$6#a%3d6h+8OxB~4_u{fkYWYPR?;%tdh>aI$;&(>#2d(q5 zq|ZS55Pb#FeTca%FjoY_pJqd!1@)Oxp9Ml1`7i-t4cyYgh!#e*aJztjI|SS%;9h|a zB09LQQ-@ea61aXdeO(}jc{0Q^LuMvumWN6CNC71urC^MNC<)^vJSd=Gf`Um(ai38{ z8O1oOm|(THknXlgcgI9TOhi;5A6OtpL5zkt4O28s(=ellSv}0@;UNQ$7?@{Zp@4-& z77{Ex<{-(z6Aqg*V)HyUZ^9BLSmGdMfWHe6JVmgA;2$Hb8db|?STjO~hb+(iC2}E= zy;n>}t!$*2i`p!Ao&2Q35-AghO2uI5=DfEu#zz=H@$_Mb?{Rq z{9+lOEYmC(YgRUCGIq4&M5(gkhc7$gRgRhMj=5@EvbrQ*Q!-U!TdHnN9cfQ>p3I%` zKE32ym!#DJX>CAC`=yLudghn1gHmqL|199o2K>39h3w)&E|JY=*YoT7d>;PKe_lXL z%Qpa=2Iup}^G|?k9cpo5kGH%3s7hbT?CS`gs@|e@IvxFC@zo~jk+^@))dtz&Y*T;l z?A{-~bETvTcUU_8qRZQ!rUz#@r|W!B>HK04c84jYlF?$DP{j?^i+0ZQ2+ZIX4SO-=6I%AGsiKi6a3?s^GQOGO8fMnKq);bg%%10X@M4zBI6uI7|O(9f`)8t&J&IEhZ*K% z8p0H3vMt-(>;N^g83y9QrosRx$V4G+SB}zh6w0L>BIQtq|HD3c{rvguotmB!tKG*1 zE-VAE0ptL&09yeP015#11C#)q0w@Kz0PqdK&j3}M2!tvku^OOe6M=9I;5xuH0$ktZ z0aXZc!vk)j#99QYB_Y3{$So3DOF@67l5SyG9hK5R#Tq<48*uD4oq7+)?=tY;nACer z`X3y|eIItSz^mDxWAgMc(LBsFqM1&#;6w|9Xu{EdnM5lKv9ggqFQlJ?40xjhKIq^k zU)07$ZGPwwk7V~J+4&?BlVb6uTR9Y~H`eCk^}vVI%Hwnk{46}Cna4KqIA)QbgHIY3 zl1D`3zs1}xsegxz*BL5&94hP%6%9yzEn==&%crFW;+X`=Xnfd;p0S?9 zTz`wXu$@1jB>69icnv2A%1q*_d#8!8+C0 zTZrvD>>8Vox>4Mdm_Es^ZH+W4(mo!~Z1|$qT$^|IwxO%!+@-oJh{nC8;r*=WWA`-;QAzjtz^Ii7u|z=Wv2fXj#VN>!l;b z?mLBTO~!ZdvdioVmi{Zhp5ZE9)4|Gr^t?m#a)!v5$*HJ3Tk$2o=T;c6)t=K>yv+RC(pN0M-Dd-mC&;N5vlB2PJtvQ!1!r1BjF z+s~Ac1T8?_TUcM_wWIJ%I-4ebdPz@^w*X1jEB<6*W6va?-5+;v-TUT4p_f*)Gl%!N9?d`v_mp{QI;sYb*91-N d=esbwJ0>tWSn~6fcWtY;{pRgIt;xz@4FI19FuMQ% diff --git a/coretk/coretk/oldimage/tunnel.gif b/coretk/coretk/oldimage/tunnel.gif deleted file mode 100644 index d574147f535122637516f4887d931779c5903ead..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 799 zcmZ?wbhEHbRA5kGI2Oae!0_MK*VoU_FDNJ|BqSs>G&DTiBRt$QBEl;&(mN_DDkjDw zIyyQgCMGVRu= zo;-Qlw5lmnrc9q+K7D%S^y$;*%qf~Xw{X_1S###hnLBsx!iCw37GTOS zvSsVmt=qS6-?3xIo;`c^@85smz=3n;qz@fBbm@}tv17-MA3uKbEDBDn2z=biC+ZOKAtSKatw%=FXNJe4Me>Sj+A1D9KCrS*RkU3* z;lrne-kjRjO)MM=OCIfUlrZJGkdVO4#LcG^5-~xMv7K2WY}*zN!ma^ zbPjCKZ)C4{X}6U-tSN3!tNM$%udfIkJDRAe;MWte!OBrWPFoND ze%zhu(T2k5n;-J6D%2?Cv=W`H(&hR5YrE{RRjOvEka_>yUw;J#YXB&pnhF2_ diff --git a/coretk/coretk/oldimage/twonode.gif b/coretk/coretk/oldimage/twonode.gif deleted file mode 100644 index 28e75fac3aadc9286cdb65b1269f3f1355f055d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 220 zcmZ?wbhEHbRA5kGIK;vL1pmSK$$5tVNI>zQv_`U~f{}rNg+fV2s)AE~YGz)#f^&Xu zL1JDdgW^vXMlJ>x1|5(AAfp(Vn>=>i`Dbv-A$vka-~r3C5f^&JB{~NwA;X2aQL-=@frKKWwv3( zs%b?M|9mdLmiXy?;63*j?HbL7MqLIbcP^&3j?Qk~UiW^@i7u0sr`k2mm^rJirgNbX HCxbNrD`Hob diff --git a/coretk/coretk/oldimage/wlan.gif b/coretk/coretk/oldimage/wlan.gif deleted file mode 100644 index d72fe9c3f8db0c7013424313bc826eca7022c19a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 146 zcmZ?wbhEHbRA5kGXkcUjg8%>jEB@otNY*qmFfdba%1_PAOJ`90$->CRz{sEjQUOxS zz!cuozw-23{>32+u5qs4D3yPjAx>eEpJZvqkAmdpb(+5?eJb>ho%FhP{o<=WA`fyl xetyz3Q~L84X{Px{Rg%)5F0}|bV|49q%MP!#xfR=XUU=m{gSY?m^L8c%YXB@FJ0t)A From 81e9bda65dc91aac48cc309870b09ae40f5d5b57 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 4 Nov 2019 15:51:37 -0800 Subject: [PATCH 170/462] fixed broken get_mobility_configs --- coretk/coretk/coreclient.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 0063a0c8..d36932df 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -127,13 +127,15 @@ class CoreClient: for node in session.nodes: if node.type == core_pb2.NodeType.WIRELESS_LAN: response = self.client.get_wlan_config(self.session_id, node.id) + logging.info("wlan config(%s): %s", node.id, response) node_config = response.config config = {x: node_config[x].value for x in node_config} self.wlanconfig_management.configurations[node.id] = config # get mobility configs response = self.client.get_mobility_configs(self.session_id) - for node_id in response.config: + logging.info("mobility configs: %s", response) + for node_id in response.configs: node_config = response.config[node_id].config config = {x: node_config[x].value for x in node_config} self.mobilityconfig_management.configurations[node_id] = config From c5d5226384b6ae7e0f93a2255d8954279fa31f8a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 4 Nov 2019 16:20:35 -0800 Subject: [PATCH 171/462] updated configutils parser to save values within configoptions, allowing them to be reused to redraw a config --- coretk/coretk/configutils.py | 11 +++++------ coretk/coretk/dialogs/sessionoptions.py | 3 ++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/coretk/coretk/configutils.py b/coretk/coretk/configutils.py index 7c87f9dd..bb3e793e 100644 --- a/coretk/coretk/configutils.py +++ b/coretk/coretk/configutils.py @@ -78,9 +78,8 @@ def parse_config(options, values): :param dict options: option key mapping to configuration options :param dict values: option key mapping to widget values - :return: + :return: nothing """ - config = {} for key in options: option = options[key] value = values[key] @@ -88,8 +87,8 @@ def parse_config(options, values): config_value = value.get() if config_type == ConfigType.BOOL: if config_value == "On": - config_value = "1" + option.value = "1" else: - config_value = "0" - config[key] = config_value - return config + option.value = "0" + else: + option.value = config_value diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index 6279d844..e887c2a1 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -27,8 +27,9 @@ class SessionOptionsDialog(Dialog): self.cancel_button.grid(row=1, column=1, pady=PAD_Y, padx=PAD_X, sticky="ew") def save(self): - config = configutils.parse_config(self.options, self.values) + configutils.parse_config(self.options, self.values) session_id = self.app.core.session_id + config = {x: self.options[x].value for x in self.options} response = self.app.core.client.set_session_options(session_id, config) logging.info("saved session config: %s", response) self.destroy() From d7280a3f6dd303b4b43f39a988c490aec5f3322e Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 4 Nov 2019 16:24:35 -0800 Subject: [PATCH 172/462] work on emane config --- coretk/coretk/canvasaction.py | 1 - coretk/coretk/coreclient.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py index f45086ab..60df71b0 100644 --- a/coretk/coretk/canvasaction.py +++ b/coretk/coretk/canvasaction.py @@ -45,5 +45,4 @@ class CanvasAction: def display_emane_configuration(self): app = self.canvas.core.app dialog = EmaneConfiguration(self.master, app, self.node_to_show_config) - print(dialog) dialog.show() diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index d36932df..a6d4f19c 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -136,7 +136,7 @@ class CoreClient: response = self.client.get_mobility_configs(self.session_id) logging.info("mobility configs: %s", response) for node_id in response.configs: - node_config = response.config[node_id].config + node_config = response.configs[node_id].config config = {x: node_config[x].value for x in node_config} self.mobilityconfig_management.configurations[node_id] = config From 22601a4580971dd15e5844aa395342bc7c90fbcd Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 4 Nov 2019 17:18:59 -0800 Subject: [PATCH 173/462] updated wlan config dialog to use common dialog class --- coretk/coretk/canvasaction.py | 9 +- coretk/coretk/dialogs/wlanconfig.py | 367 ++++++++++------------------ 2 files changed, 139 insertions(+), 237 deletions(-) diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py index b5f09333..6e1492ed 100644 --- a/coretk/coretk/canvasaction.py +++ b/coretk/coretk/canvasaction.py @@ -6,7 +6,7 @@ canvas graph action from core.api.grpc import core_pb2 from coretk.dialogs.nodeconfig import NodeConfigDialog -from coretk.dialogs.wlanconfig import WlanConfiguration +from coretk.dialogs.wlanconfig import WlanConfigDialog # TODO, finish classifying node types NODE_TO_TYPE = { @@ -36,8 +36,11 @@ class CanvasAction: def display_wlan_configuration(self, canvas_node): # print(self.canvas.grpc_manager.wlanconfig_management.configurations) - wlan_config = self.canvas.grpc_manager.wlanconfig_management.configurations[ + wlan_config = self.master.core.wlanconfig_management.configurations[ canvas_node.core_id ] - WlanConfiguration(self.canvas, self.node_to_show_config, wlan_config) + dialog = WlanConfigDialog( + self.master, self.master, self.node_to_show_config, wlan_config + ) + dialog.show() self.node_to_show_config = None diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index 8065a5fa..e6e1cfd4 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -3,294 +3,193 @@ wlan configuration """ import tkinter as tk -from functools import partial +from coretk.dialogs.dialog import Dialog from coretk.dialogs.nodeicon import NodeIconDialog -class WlanConfiguration: - def __init__(self, canvas, canvas_node, config): +class WlanConfigDialog(Dialog): + def __init__(self, master, app, canvas_node, config): """ create an instance of WlanConfiguration :param coretk.grpah.CanvasGraph canvas: canvas object :param coretk.graph.CanvasNode canvas_node: canvas node object """ - - self.canvas = canvas + super().__init__( + master, app, f"{canvas_node.name} Wlan Configuration", modal=True + ) self.image = canvas_node.image - self.node_type = canvas_node.node_type - self.name = canvas_node.name self.canvas_node = canvas_node - - self.top = tk.Toplevel() - self.top.title("wlan configuration") - self.node_name = tk.StringVar() - - # self.range_var = tk.DoubleVar() - # self.range_var.set(275.0) self.config = config - self.range_var = tk.StringVar() - self.range_var.set(config["basic_range"]) - # self.bandwidth_var = tk.IntVar() - self.bandwidth_var = tk.StringVar() - self.bandwidth_var.set(config["bandwidth"]) + self.name = tk.StringVar(value=canvas_node.name) + self.range_var = tk.StringVar(value=config["basic_range"]) + self.bandwidth_var = tk.StringVar(value=config["bandwidth"]) + self.delay_var = tk.StringVar(value=config["delay"]) + self.loss_var = tk.StringVar(value=config["error"]) + self.jitter_var = tk.StringVar(value=config["jitter"]) + self.ip4_subnet = tk.StringVar() + self.ip6_subnet = tk.StringVar() + self.image_button = None + self.draw() - self.delay_var = tk.StringVar() + def draw(self): + self.columnconfigure(0, weight=1) + self.draw_name_config() + self.draw_wlan_config() + self.draw_subnet() + self.draw_wlan_buttons() + self.draw_apply_buttons() - self.image_modification() - self.wlan_configuration() - self.subnet() - self.wlan_options() - self.config_option() - - def image_modification(self): + def draw_name_config(self): """ draw image modification part :return: nothing """ - f = tk.Frame(self.top, bg="#d9d9d9") - lbl = tk.Label(f, text="Node name: ", bg="#d9d9d9") - lbl.grid(row=0, column=0, padx=3, pady=3) - e = tk.Entry(f, textvariable=self.node_name, bg="white") - e.grid(row=0, column=1, padx=3, pady=3) - b = tk.Button(f, text="None") - b.grid(row=0, column=2, padx=3, pady=3) - b = tk.Button(f, image=self.image, command=lambda: self.click_image) - b.grid(row=0, column=3, padx=3, pady=3) - f.grid(padx=2, pady=2, ipadx=2, ipady=2) + frame = tk.Frame(self) + frame.grid(pady=2, sticky="ew") + frame.columnconfigure(0, weight=1) - def click_image(self): - NodeIconDialog(self.app, canvas_node=self.canvas_node, node_config=self) + entry = tk.Entry(frame, textvariable=self.name, bg="white") + entry.grid(row=0, column=0, padx=2, sticky="ew") - def create_string_var(self, val): - """ - create string variable for convenience + self.image_button = tk.Button(frame, image=self.image, command=self.click_icon) + self.image_button.grid(row=0, column=1, padx=3) - :param str val: text value - :return: nothing - """ - v = tk.StringVar() - v.set(val) - return v - - def scrollbar_command(self, entry_widget, delta, event): - """ - change text in entry based on scrollbar action (click up or down) - - :param tkinter.Entry entry_widget: entry needed for changing text - :param int or float delta: the amount to change - :param event: scrollbar event - :return: nothing - """ - try: - value = int(entry_widget.get()) - except ValueError: - value = float(entry_widget.get()) - entry_widget.delete(0, tk.END) - if event == "-1": - entry_widget.insert(tk.END, str(round(value + delta, 1))) - elif event == "1": - entry_widget.insert(tk.END, str(round(value - delta, 1))) - - def wlan_configuration(self): + def draw_wlan_config(self): """ create wireless configuration table :return: nothing """ - lbl = tk.Label(self.top, text="Wireless") - lbl.grid(sticky=tk.W, padx=3, pady=3) + label = tk.Label(self, text="Wireless") + label.grid(sticky="w", pady=2) - f = tk.Frame( - self.top, - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", - highlightthickness=0.5, - bd=0, - bg="#d9d9d9", + frame = tk.Frame(self) + frame.grid(pady=2, sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + + label = tk.Label( + frame, + text=( + "The basic range model calculates on/off " + "connectivity based on pixel distance between nodes." + ), ) + label.grid(row=0, columnspan=2, pady=2, sticky="ew") - lbl = tk.Label( - f, - text="The basic range model calculates on/off connectivity based on pixel distance between nodes.", - bg="#d9d9d9", - ) - lbl.grid(padx=4, pady=4) + label = tk.Label(frame, text="Range") + label.grid(row=1, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.range_var) + entry.grid(row=1, column=1, sticky="ew") - f1 = tk.Frame(f, bg="#d9d9d9") + label = tk.Label(frame, text="Bandwidth (bps)") + label.grid(row=2, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.bandwidth_var) + entry.grid(row=2, column=1, sticky="ew") - lbl = tk.Label(f1, text="Range: ", bg="#d9d9d9") - lbl.grid(row=0, column=0) + label = tk.Label(frame, text="Delay (us)") + label.grid(row=3, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.delay_var) + entry.grid(row=3, column=1, sticky="ew") - e = tk.Entry( - f1, - textvariable=self.create_string_var(self.config["basic_range"]), - width=5, - bg="white", - ) - e.grid(row=0, column=1) + label = tk.Label(frame, text="Loss (%)") + label.grid(row=4, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.loss_var) + entry.grid(row=4, column=1, sticky="ew") - lbl = tk.Label(f1, text="Bandwidth (bps): ", bg="#d9d9d9") - lbl.grid(row=0, column=2) + label = tk.Label(frame, text="Jitter (us)") + label.grid(row=5, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.jitter_var) + entry.grid(row=5, column=1, sticky="ew") - f11 = tk.Frame(f1, bg="#d9d9d9") - sb = tk.Scrollbar(f11, orient=tk.VERTICAL) - e = tk.Entry( - f11, - textvariable=self.create_string_var(self.config["bandwidth"]), - width=10, - bg="white", - ) - sb.config(command=partial(self.scrollbar_command, e, 1000000)) - e.grid() - sb.grid(row=0, column=1) - f11.grid(row=0, column=3) - - # e = tk.Entry(f1, textvariable=self.bandwidth_var, width=10) - # e.grid(row=0, column=4) - f1.grid(sticky=tk.W, padx=4, pady=4) - - f2 = tk.Frame(f, bg="#d9d9d9") - lbl = tk.Label(f2, text="Delay (us): ", bg="#d9d9d9") - lbl.grid(row=0, column=0) - - f21 = tk.Frame(f2, bg="#d9d9d9") - sb = tk.Scrollbar(f21, orient=tk.VERTICAL) - e = tk.Entry( - f21, textvariable=self.create_string_var(self.config["delay"]), bg="white" - ) - sb.config(command=partial(self.scrollbar_command, e, 5000)) - e.grid() - sb.grid(row=0, column=1) - f21.grid(row=0, column=1) - - lbl = tk.Label(f2, text="Loss (%): ", bg="#d9d9d9") - lbl.grid(row=0, column=2) - - f22 = tk.Frame(f2, bg="#d9d9d9") - sb = tk.Scrollbar(f22, orient=tk.VERTICAL) - e = tk.Entry( - f22, textvariable=self.create_string_var(self.config["error"]), bg="white" - ) - sb.config(command=partial(self.scrollbar_command, e, 0.1)) - e.grid() - sb.grid(row=0, column=1) - f22.grid(row=0, column=3) - - # e = tk.Entry(f2, textvariable=self.create_string_var(0)) - # e.grid(row=0, column=3) - f2.grid(sticky=tk.W, padx=4, pady=4) - - f3 = tk.Frame(f, bg="#d9d9d9") - lbl = tk.Label(f3, text="Jitter (us): ", bg="#d9d9d9") - lbl.grid() - f31 = tk.Frame(f3, bg="#d9d9d9") - sb = tk.Scrollbar(f31, orient=tk.VERTICAL) - e = tk.Entry( - f31, textvariable=self.create_string_var(self.config["jitter"]), bg="white" - ) - sb.config(command=partial(self.scrollbar_command, e, 5000)) - e.grid() - sb.grid(row=0, column=1) - f31.grid(row=0, column=1) - - f3.grid(sticky=tk.W, padx=4, pady=4) - f.grid(padx=3, pady=3) - - def subnet(self): + def draw_subnet(self): """ create the entries for ipv4 subnet and ipv6 subnet :return: nothing """ - f = tk.Frame(self.top) - f1 = tk.Frame(f) - lbl = tk.Label(f1, text="IPv4 subnet") - lbl.grid() - e = tk.Entry(f1, width=30, bg="white", textvariable=self.create_string_var("")) - e.grid(row=0, column=1) - f1.grid() + frame = tk.Frame(self) + frame.grid(pady=3, sticky="ew") + frame.columnconfigure(1, weight=1) + frame.columnconfigure(3, weight=1) - f2 = tk.Frame(f) - lbl = tk.Label(f2, text="IPv6 subnet") - lbl.grid() - e = tk.Entry(f2, width=30, bg="white", textvariable=self.create_string_var("")) - e.grid(row=0, column=1) - f2.grid() - f.grid(sticky=tk.W, padx=3, pady=3) + label = tk.Label(frame, text="IPv4 Subnet") + label.grid(row=0, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.ip4_subnet) + entry.grid(row=0, column=1, sticky="ew") - def wlan_options(self): + label = tk.Label(frame, text="IPv6 Subnet") + label.grid(row=0, column=2, sticky="w") + entry = tk.Entry(frame, textvariable=self.ip6_subnet) + entry.grid(row=0, column=3, sticky="ew") + + def draw_wlan_buttons(self): """ create wireless node options :return: """ - f = tk.Frame(self.top) - b = tk.Button(f, text="ns-2 mobility script...") - b.pack(side=tk.LEFT, padx=1) - b = tk.Button(f, text="Link to all routers") - b.pack(side=tk.LEFT, padx=1) - b = tk.Button(f, text="Choose WLAN members") - b.pack(side=tk.LEFT, padx=1) - f.grid(sticky=tk.W) + frame = tk.Frame(self) + frame.grid(pady=2, sticky="ew") + for i in range(3): + frame.columnconfigure(i, weight=1) - def wlan_config_apply(self): - """ - retrieve user's wlan configuration and store the new configuration values + button = tk.Button(frame, text="ns-2 mobility script...") + button.grid(row=0, column=0, padx=2, sticky="ew") - :return: nothing - """ - config_frame = self.top.grid_slaves(row=2, column=0)[0] - range_and_bandwidth_frame = config_frame.grid_slaves(row=1, column=0)[0] - range_val = range_and_bandwidth_frame.grid_slaves(row=0, column=1)[0].get() - bandwidth = ( - range_and_bandwidth_frame.grid_slaves(row=0, column=3)[0] - .grid_slaves(row=0, column=0)[0] - .get() - ) + button = tk.Button(frame, text="Link to all routers") + button.grid(row=0, column=1, padx=2, sticky="ew") - delay_and_loss_frame = config_frame.grid_slaves(row=2, column=0)[0] - delay = ( - delay_and_loss_frame.grid_slaves(row=0, column=1)[0] - .grid_slaves(row=0, column=0)[0] - .get() - ) - loss = ( - delay_and_loss_frame.grid_slaves(row=0, column=3)[0] - .grid_slaves(row=0, column=0)[0] - .get() - ) + button = tk.Button(frame, text="Choose WLAN members") + button.grid(row=0, column=2, padx=2, sticky="ew") - jitter_frame = config_frame.grid_slaves(row=3, column=0)[0] - jitter_val = ( - jitter_frame.grid_slaves(row=0, column=1)[0] - .grid_slaves(row=0, column=0)[0] - .get() - ) - - # set wireless node configuration here - wlanconfig_manager = self.canvas.grpc_manager.wlanconfig_management - wlanconfig_manager.set_custom_config( - node_id=self.canvas_node.core_id, - range=range_val, - bandwidth=bandwidth, - jitter=jitter_val, - delay=delay, - error=loss, - ) - self.top.destroy() - - def config_option(self): + def draw_apply_buttons(self): """ create node configuration options :return: nothing """ - f = tk.Frame(self.top, bg="#d9d9d9") - b = tk.Button(f, text="Apply", bg="#d9d9d9", command=self.wlan_config_apply) - b.grid(padx=2, pady=2) - b = tk.Button(f, text="Cancel", bg="#d9d9d9", command=self.top.destroy) - b.grid(row=0, column=1, padx=2, pady=2) - f.grid(padx=4, pady=4) + frame = tk.Frame(self) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + + button = tk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, padx=2, sticky="ew") + + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, padx=2, sticky="ew") + + def click_icon(self): + dialog = NodeIconDialog(self, self.app, self.canvas_node) + dialog.show() + if dialog.image: + self.image = dialog.image + self.image_button.config(image=self.image) + + def click_apply(self): + """ + retrieve user's wlan configuration and store the new configuration values + + :return: nothing + """ + basic_range = self.range_var.get() + bandwidth = self.bandwidth_var.get() + delay = self.delay_var.get() + loss = self.loss_var.get() + jitter = self.jitter_var.get() + + # set wireless node configuration here + wlanconfig_manager = self.app.core.wlanconfig_management + wlanconfig_manager.set_custom_config( + node_id=self.canvas_node.core_id, + range=basic_range, + bandwidth=bandwidth, + jitter=jitter, + delay=delay, + error=loss, + ) + self.destroy() From 68a5468ffba89fefc138bd637a440497a274a0bd Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 5 Nov 2019 12:37:47 -0800 Subject: [PATCH 174/462] added servers dialog for distributed core, updated usages of ttk.Combobox to readonly --- coretk/coretk/coreclient.py | 10 ++ coretk/coretk/coremenubar.py | 2 +- coretk/coretk/dialogs/hooks.py | 4 +- coretk/coretk/dialogs/nodeconfig.py | 10 +- coretk/coretk/dialogs/servers.py | 169 ++++++++++++++++++++++++++++ coretk/coretk/menuaction.py | 10 +- 6 files changed, 197 insertions(+), 8 deletions(-) create mode 100644 coretk/coretk/dialogs/servers.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index ad912422..38e0ec2c 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -56,6 +56,13 @@ class Edge: self.interface_2 = None +class CoreServer: + def __init__(self, name, address, port): + self.name = name + self.address = address + self.port = port + + class CoreClient: def __init__(self, app): """ @@ -68,6 +75,9 @@ class CoreClient: self.master = app.master self.interface_helper = None + # distributed server data + self.servers = {} + # data for managing the current session self.nodes = {} self.edges = {} diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index 0751ee06..5add9790 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -616,7 +616,7 @@ class CoreMenubar(object): ) session_menu.add_command( label="Emulation servers...", - command=action.session_emulation_servers, + command=self.menu_action.session_servers, underline=0, ) session_menu.add_command( diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index 95ca8af7..99647635 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -32,7 +32,9 @@ class HookDialog(Dialog): initial_state = core_pb2.SessionState.Enum.Name(core_pb2.SessionState.RUNTIME) self.state.set(initial_state) self.name.set(f"{initial_state.lower()}_hook.sh") - combobox = ttk.Combobox(frame, textvariable=self.state, values=values) + combobox = ttk.Combobox( + frame, textvariable=self.state, values=values, state="readonly" + ) combobox.grid(row=0, column=2, sticky="ew") combobox.bind("<>", self.state_change) diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index f1de2e50..b8548c51 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -43,10 +43,16 @@ class NodeConfigDialog(Dialog): entry = tk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=0, padx=2, sticky="ew") - combobox = ttk.Combobox(frame, textvariable=self.type, values=DEFAULTNODES) + combobox = ttk.Combobox( + frame, textvariable=self.type, values=DEFAULTNODES, state="readonly" + ) combobox.grid(row=0, column=1, padx=2, sticky="ew") - combobox = ttk.Combobox(frame, textvariable=self.server, values=["localhost"]) + servers = [""] + servers.extend(list(sorted(self.app.core.servers.keys()))) + combobox = ttk.Combobox( + frame, textvariable=self.server, values=servers, state="readonly" + ) combobox.current(0) combobox.grid(row=0, column=2, sticky="ew") diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py new file mode 100644 index 00000000..dbc31267 --- /dev/null +++ b/coretk/coretk/dialogs/servers.py @@ -0,0 +1,169 @@ +import tkinter as tk + +from coretk.coreclient import CoreServer +from coretk.dialogs.dialog import Dialog + +DEFAULT_NAME = "example" +DEFAULT_ADDRESS = "127.0.0.1" +DEFAULT_PORT = 50051 + + +class ServersDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "CORE Servers", modal=True) + self.name = tk.StringVar(value=DEFAULT_NAME) + self.address = tk.StringVar(value=DEFAULT_ADDRESS) + self.port = tk.IntVar(value=DEFAULT_PORT) + self.servers = None + self.selected_index = None + self.selected = None + self.save_button = None + self.delete_button = None + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.draw_servers() + self.draw_server_configuration() + self.draw_servers_buttons() + self.draw_apply_buttons() + + def draw_servers(self): + frame = tk.Frame(self) + frame.grid(pady=2, sticky="nsew") + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + + scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar.grid(row=0, column=1, sticky="ns") + + self.servers = tk.Listbox( + frame, + selectmode=tk.SINGLE, + yscrollcommand=scrollbar.set, + relief=tk.FLAT, + highlightthickness=0.5, + bd=0, + ) + self.servers.grid(row=0, column=0, sticky="nsew") + self.servers.bind("<>", self.handle_server_change) + + for server in self.app.core.servers: + self.servers.insert(tk.END, server) + + scrollbar.config(command=self.servers.yview) + + def draw_server_configuration(self): + label = tk.Label(self, text="Server Configuration") + label.grid(pady=2, sticky="ew") + + frame = tk.Frame(self) + frame.grid(pady=2, sticky="ew") + frame.columnconfigure(1, weight=1) + frame.columnconfigure(3, weight=1) + frame.columnconfigure(5, weight=1) + + label = tk.Label(frame, text="Name") + label.grid(row=0, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.name) + entry.grid(row=0, column=1, sticky="ew") + + label = tk.Label(frame, text="Address") + label.grid(row=0, column=2, sticky="w") + entry = tk.Entry(frame, textvariable=self.address) + entry.grid(row=0, column=3, sticky="ew") + + label = tk.Label(frame, text="Port") + label.grid(row=0, column=4, sticky="w") + entry = tk.Entry(frame, textvariable=self.port) + entry.grid(row=0, column=5, sticky="ew") + + def draw_servers_buttons(self): + frame = tk.Frame(self) + frame.grid(pady=2, sticky="ew") + for i in range(3): + frame.columnconfigure(i, weight=1) + + button = tk.Button(frame, text="Create", command=self.click_create) + button.grid(row=0, column=0, sticky="ew") + + self.save_button = tk.Button( + frame, text="Save", state=tk.DISABLED, command=self.click_save + ) + self.save_button.grid(row=0, column=1, sticky="ew") + + self.delete_button = tk.Button( + frame, text="Delete", state=tk.DISABLED, command=self.click_delete + ) + self.delete_button.grid(row=0, column=2, sticky="ew") + + def draw_apply_buttons(self): + frame = tk.Frame(self) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + + button = tk.Button( + frame, text="Save Configuration", command=self.click_save_configuration + ) + button.grid(row=0, column=0, sticky="ew") + + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_save_configuration(self): + pass + + def click_create(self): + name = self.name.get() + if name not in self.app.core.servers: + address = self.address.get() + port = self.port.get() + server = CoreServer(name, address, port) + self.app.core.servers[name] = server + self.servers.insert(tk.END, name) + + def click_save(self): + name = self.name.get() + if self.selected and name not in self.app.core.servers: + previous_name = self.selected + self.selected = name + server = self.app.core.servers.pop(previous_name) + server.name = name + server.address = self.address.get() + server.port = self.port.get() + self.app.core.servers[name] = server + self.servers.delete(self.selected_index) + self.servers.insert(self.selected_index, name) + self.servers.selection_set(self.selected_index) + + def click_delete(self): + if self.selected: + self.servers.delete(self.selected_index) + del self.app.core.servers[self.selected] + self.selected = None + self.selected_index = None + self.name.set(DEFAULT_NAME) + self.address.set(DEFAULT_ADDRESS) + self.port.set(DEFAULT_PORT) + self.servers.selection_clear(0, tk.END) + self.save_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) + + def handle_server_change(self, event): + selection = self.servers.curselection() + if selection: + self.selected_index = selection[0] + self.selected = self.servers.get(self.selected_index) + server = self.app.core.servers[self.selected] + self.name.set(server.name) + self.address.set(server.address) + self.port.set(server.port) + self.save_button.config(state=tk.NORMAL) + self.delete_button.config(state=tk.NORMAL) + else: + self.selected_index = None + self.selected = None + self.save_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 7971553d..a60f7458 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -11,6 +11,7 @@ from coretk.appdirs import XML_PATH from coretk.dialogs.canvasbackground import CanvasBackgroundDialog from coretk.dialogs.canvassizeandscale import SizeAndScaleDialog from coretk.dialogs.hooks import HooksDialog +from coretk.dialogs.servers import ServersDialog from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog @@ -295,10 +296,6 @@ def session_reset_node_positions(): logging.debug("Click session reset node positions") -def session_emulation_servers(): - logging.debug("Click session emulation servers") - - def help_about(): logging.debug("Click help About") @@ -401,3 +398,8 @@ class MenuAction: logging.debug("Click session hooks") dialog = HooksDialog(self.app, self.app) dialog.show() + + def session_servers(self): + logging.debug("Click session emulation servers") + dialog = ServersDialog(self.app, self.app) + dialog.show() From 2d1f5edf79f15dbfe9f82b130577064712ee48f8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 5 Nov 2019 13:10:42 -0800 Subject: [PATCH 175/462] started initial data for gui config file and added example server to it when initially running gui --- coretk/Pipfile | 1 + coretk/Pipfile.lock | 378 ++++++++++++++++++++++++++---------- coretk/coretk/app.py | 1 + coretk/coretk/appdirs.py | 19 +- coretk/coretk/coreclient.py | 5 + 5 files changed, 300 insertions(+), 104 deletions(-) diff --git a/coretk/Pipfile b/coretk/Pipfile index da8c8df8..dfeb664c 100644 --- a/coretk/Pipfile +++ b/coretk/Pipfile @@ -15,3 +15,4 @@ pre-commit = "*" [packages] coretk = {editable = true,path = "."} core = {editable = true,path = "./../daemon"} +pyyaml = "*" diff --git a/coretk/Pipfile.lock b/coretk/Pipfile.lock index d3547813..07906a87 100644 --- a/coretk/Pipfile.lock +++ b/coretk/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2cb6b23bfb8b9bebb5ece1016de468cc57fe46cf4df08a61b1861aee4bed1028" + "sha256": "eb59f8233d6608de2d67743d8d8afe51c929f837cf6cf4d991ffc79ab5134910" }, "pipfile-spec": 6, "requires": {}, @@ -14,12 +14,65 @@ ] }, "default": { - "configparser": { + "bcrypt": { "hashes": [ - "sha256:254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c", - "sha256:c7d282687a5308319bf3d2e7706e575c635b0a470342641c93bea0ea3b5331df" + "sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89", + "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42", + "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294", + "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161", + "sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752", + "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31", + "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5", + "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c", + "sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0", + "sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de", + "sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e", + "sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052", + "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09", + "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105", + "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133", + "sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1", + "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7", + "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc" ], - "version": "==4.0.2" + "version": "==3.1.7" + }, + "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: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" }, "core": { "editable": true, @@ -29,54 +82,106 @@ "editable": true, "path": "." }, - "future": { + "cryptography": { "hashes": [ - "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8" + "sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c", + "sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595", + "sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad", + "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651", + "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2", + "sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff", + "sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d", + "sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42", + "sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d", + "sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e", + "sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912", + "sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793", + "sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13", + "sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7", + "sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0", + "sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879", + "sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f", + "sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9", + "sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2", + "sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf", + "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8" ], - "version": "==0.17.1" + "version": "==2.8" + }, + "fabric": { + "hashes": [ + "sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389", + "sha256:24842d7d51556adcabd885ac3cf5e1df73fc622a1708bf3667bf5927576cdfa6" + ], + "version": "==2.5.0" }, "grpcio": { "hashes": [ - "sha256:0337debec20fe385bcd49048d6917270efbc17a5119857466559b4db91f8995b", - "sha256:164f82a99e08797ea786283b66b45ebe76772d321577d1674ba6fe0200155892", - "sha256:172dfba8d9621048c2cbc1d1cf7a02244e9a9a8cff5bb79bb30bcb0c13c7fd31", - "sha256:18f4b536d8a9cfa15b3214e0bb628071def94160699e91798f0a954c3b2db88d", - "sha256:2283b56bda49b068b0f08d006fffc7dd46eae72322f1a5dec87fc9c218f1dc2d", - "sha256:26b33f488a955bf49262d2ce3423d3a8174108506d8f819e8150aca21bdd3b99", - "sha256:31cc9b6f70bdd0d9ff53df2d563ea1fb278601d5c625932d8a82d03b08ff3de0", - "sha256:37dd8684fbc2bc00766ae6784bcbd7f874bc96527636a341411db811d04ff650", - "sha256:424c01189ef51a808669f020368b01204e0f1fa0bf2adab7c8d0d13166f92e9e", - "sha256:431c099f20a1f1d97def98f87bb74fa752e8819c2bab23d79089353aed1acc9b", - "sha256:4c2f1d0b27bcef301e5d5c1da05ffd7d174f807f61889c006b8e708b16bc978e", - "sha256:59b8d738867b59c5daaff5df242b5f3f9c58b47862f603c6ee530964b897b69b", - "sha256:8d4f1ee2a67cf8f792d4fc9b8c7bb2148174e838d935f175653aec234752828b", - "sha256:97ab9e35b47bda0441332204960f95c1169c55ec8e989381bedd32bdb9f78b05", - "sha256:9cf93e185507bfdaa7ed45a90049bd3f1ed3f6357ad3772b31e993ff723cf67d", - "sha256:a5a81472c0ca6181492b9291c316ff60c6c94dd3f21c1e8c481f21923d899af0", - "sha256:aaa1feb0fdd094af6db0a16cbd446ed94285a50e320aede5971152d9ea022df8", - "sha256:b36bf4408f8400ee9ab13ff129e71f2e4c72ce2d8886b744aeab77ce50a55cf6", - "sha256:bb345f7e98b38a2c1ef33ff1145687234f78dfeedf308b41b3e41f4b42eba099", - "sha256:c13ae15695e0eb4ba2db920d6a197171d2398c675afa4f27460b6381d20a6884", - "sha256:c4a233a00cc5b64543e97733902151bc6738396931b3c166aad03a3aaadbd479", - "sha256:c521c5f8a95baabba69c68dd0f5e34f37c8adf1a9691f9884dba3eab4ebadc29", - "sha256:c772edd094fe3e54d6d54fdecb90c51cb8d07b55e9c1cda2d33e9615e33d07e8", - "sha256:cebfba6542855403b29e4bc95bbcd5ab444f21137b440f2fb7c7925ca0e55bfd", - "sha256:d7490e013c4bad3e8db804fc6483b47125dc8df0ebcfd6e419bd25df35025301", - "sha256:dfb6063619f297cbd22c67530d7465d98348b35d0424bbc1756b36c5ef9f99d4", - "sha256:e80a15b48a66f35c7c33db2a7df4034a533b362269d0e60e0036e23f14bac7b5", - "sha256:ea444fa1c1ec4f8d2ce965bb01e06148ef9ceb398fb2f627511d50f137eac35b", - "sha256:ec986cbf8837a49f9612cc1cfc2a8ccb54875cfce5355a121279de35124ea1db", - "sha256:fb641df6de8c4a55c784c24d334d53096954d9b30679d3ce5eb6a4d25c1020a3", - "sha256:fb88bd791c8efbcb36de12f0aa519ceec0b7806d3decff16e412e097d4725d44", - "sha256:ffa1be3d566a9cbd21a5f2d95fd9262ec6c337c499291bfeb51547b8de18942e" + "sha256:01cb705eafba1108e2a947ba0457da4f6a1e8142c729fc61702b5fdd11009eb1", + "sha256:0b5a79e29f167d3cd06faad6b15babbc2661066daaacf79373c3a8e67ca1fca1", + "sha256:1097a61a0e97b3580642e6e1460a3a1f1ba1815e2a70d6057173bcc495417076", + "sha256:13970e665a4ec4cec7d067d7d3504a0398c657d91d26c581144ad9044e429c9a", + "sha256:1557817cea6e0b87fad2a3e20da385170efb03a313db164e8078955add2dfa1b", + "sha256:1b0fb036a2f9dd93d9a35c57c26420eeb4b571fcb14b51cddf5b1e73ea5d882b", + "sha256:24d9e58d08e8cd545d8a3247a18654aff0e5e60414701696a8098fbb0d792b75", + "sha256:2c38b586163d2b91567fe5e6d9e7798f792012365adc838a64b66b22dce3f4d4", + "sha256:2df3ab4348507de60e1cbf75196403df1b9b4c4d4dc5bd11ac4eb63c46f691c7", + "sha256:32f70f7c90454ea568b868af2e96616743718d9233d23f62407e98caed81dfbf", + "sha256:3af2a49d576820045c9c880ff29a5a96d020fe31b35d248519bfc6ccb8be4eac", + "sha256:4ff7d63800a63db031ebac6a6f581ae84877c959401c24c28f2cc51fd36c47ad", + "sha256:502aaa8be56f0ae69cda66bc27e1fb5531ceaa27ca515ec3c34f6178b1297180", + "sha256:55358ce3ec283222e435f7dbc6603521438458f3c65f7c1cb33b8dabf56d70d8", + "sha256:5583b01c67f85fa64a2c3fb085e5517c88b9c1500a2cce12d473cd99d0ed2e49", + "sha256:58d9a5557d3eb7b734a3cea8b16c891099a522b3953a45a30bd4c034f75fc913", + "sha256:5911f042c4ab177757eec5bcb4e2e9a2e823d888835d24577321bf55f02938fa", + "sha256:5e16ea922f4e5017c04fd94e2639b1006e03097e9dd0cbb7a1c852af3ea8bf2e", + "sha256:656e19d3f1b9050ee01b457f92838a9679d7cf84c995f708780f44484048705e", + "sha256:6a1435449a82008c451c7e1a82a834387b9108f9a8d27910f86e7c482f5568e9", + "sha256:6ff02ca6cbed0ddb76e93ba0f8beb6a8c77d83a84eb7cafe2ae3399a8b9d69ea", + "sha256:76de68f60102f333bf4817f38e81ecbee68b850f5a5da9f355235e948ac40981", + "sha256:7c6d7ddd50fc6548ea1dfe09c62509c4f95b8b40082287747be05aa8feb15ee2", + "sha256:836b9d29507de729129e363276fe7c7d6a34c7961e0f155787025552b15d22c0", + "sha256:869242b2baf8a888a4fe0548f86abc47cb4b48bdfd76ae62d6456e939c202e65", + "sha256:8954b24bd08641d906ee50b2d638efc76df893fbd0913149b80484fd0eac40c9", + "sha256:8cdea65d1abb2e698420db8daf20c8d272fbd9d96a51b26a713c1c76f237d181", + "sha256:90161840b4fe9636f91ed0d3ea1e7e615e488cbea4e77594c889e5f3d7a776db", + "sha256:90fb6316b4d7d36700c40db4335902b78dcae13b5466673c21fd3b08a3c1b0c6", + "sha256:91b34f58db2611c9a93ecf751028f97fba1f06e65f49b38f272f6aa5d2977331", + "sha256:9474944a96a33eb8734fa8dc5805403d57973a3526204a5e1c1780d02e0572b6", + "sha256:9a36275db2a4774ac16c6822e7af816ee048071d5030b4c035fd53942b361935", + "sha256:9cbe26e2976b994c5f7c2d35a63354674d6ca0ce62f5b513f078bf63c1745229", + "sha256:9eaeabb3c0eecd6ddd0c16767fd12d130e2cebb8c2618f959a278b1ff336ddc3", + "sha256:a2bc7e10ebcf4be503ae427f9887e75c0cc24e88ce467a8e6eaca6bd2862406e", + "sha256:a5b42e6292ba51b8e67e09fc256963ba4ca9c04026de004d2fe59cc17e3c3776", + "sha256:bd6ec1233c86c0b9bb5d03ec30dbe3ffbfa53335790320d99a7ae9018c5450f2", + "sha256:bef57530816af54d66b1f4c70a8f851f320cb6f84d4b5a0b422b0e9811ea4e59", + "sha256:c146a63eaadc6589b732780061f3c94cd0574388d372baccbb3c1597a9ebdb7a", + "sha256:c2efd3b130dc639d615b6f58980e1bfd1b177ad821f30827afa5001aa30ddd48", + "sha256:c888b18f7392e6cc79a33a803e7ebd7890ac3318f571fca6b356526f35b53b12", + "sha256:ca30721fda297ae22f16bc37aa7ed244970ddfdcb98247570cdd26daaad4665e", + "sha256:cf5f5340dd682ab034baa52f423a0f91326489c262ac9617fa06309ec05880e9", + "sha256:d0726aa0d9b57c56985db5952e90fb1033a317074f2877db5307cdd6eede1564", + "sha256:df442945b2dd6f8ae0e20b403e0fd4548cd5c2aad69200047cc3251257b78f65", + "sha256:e08e758c31919d167c0867539bd3b2441629ef00aa595e3ea2b635273659f40a", + "sha256:e4864339deeeaefaad34dd3a432ee618a039fca28efb292949c855e00878203c", + "sha256:f4cd049cb94d9f517b1cab5668a3b345968beba093bc79a637e671000b3540ec" ], - "version": "==1.24.0" + "version": "==1.24.3" + }, + "invoke": { + "hashes": [ + "sha256:c52274d2e8a6d64ef0d61093e1983268ea1fc0cd13facb9448c4ef0c9a7ac7da", + "sha256:f4ec8a134c0122ea042c8912529f87652445d9f4de590b353d23f95bfa1f0efd", + "sha256:fc803a5c9052f15e63310aa81a43498d7c55542beb18564db88a9d75a176fa44" + ], + "version": "==1.3.0" }, "lxml": { "hashes": [ "sha256:02ca7bf899da57084041bb0f6095333e4d239948ad3169443f454add9f4e9cb4", "sha256:096b82c5e0ea27ce9138bcbb205313343ee66a6e132f25c5ed67e2c8d960a1bc", "sha256:0a920ff98cf1aac310470c644bc23b326402d3ef667ddafecb024e1713d485f1", + "sha256:1409b14bf83a7d729f92e2a7fbfe7ec929d4883ca071b06e95c539ceedb6497c", "sha256:17cae1730a782858a6e2758fd20dd0ef7567916c47757b694a06ffafdec20046", "sha256:17e3950add54c882e032527795c625929613adbd2ce5162b94667334458b5a36", "sha256:1f4f214337f6ee5825bf90a65d04d70aab05526c08191ab888cb5149501923c5", @@ -87,11 +192,14 @@ "sha256:760c12276fee05c36f95f8040180abc7fbebb9e5011447a97cdc289b5d6ab6fc", "sha256:796685d3969815a633827c818863ee199440696b0961e200b011d79b9394bbe7", "sha256:891fe897b49abb7db470c55664b198b1095e4943b9f82b7dcab317a19116cd38", + "sha256:9277562f175d2334744ad297568677056861070399cec56ff06abbe2564d1232", "sha256:a471628e20f03dcdfde00770eeaf9c77811f0c331c8805219ca7b87ac17576c5", "sha256:a63b4fd3e2cabdcc9d918ed280bdde3e8e9641e04f3c59a2a3109644a07b9832", + "sha256:ae88588d687bd476be588010cbbe551e9c2872b816f2da8f01f6f1fda74e1ef0", "sha256:b0b84408d4eabc6de9dd1e1e0bc63e7731e890c0b378a62443e5741cfd0ae90a", "sha256:be78485e5d5f3684e875dab60f40cddace2f5b2a8f7fede412358ab3214c3a6f", "sha256:c27eaed872185f047bb7f7da2d21a7d8913457678c9a100a50db6da890bc28b9", + "sha256:c7fccd08b14aa437fe096c71c645c0f9be0655a9b1a4b7cffc77bcb23b3d61d2", "sha256:c81cb40bff373ab7a7446d6bbca0190bccc5be3448b47b51d729e37799bb5692", "sha256:d11874b3c33ee441059464711cd365b89fa1a9cf19ae75b0c189b01fbf735b84", "sha256:e9c028b5897901361d81a4718d1db217b716424a0283afe9d6735fe0caf70f79", @@ -99,64 +207,126 @@ ], "version": "==4.4.1" }, + "paramiko": { + "hashes": [ + "sha256:99f0179bdc176281d21961a003ffdb2ec369daac1a1007241f53374e376576cf", + "sha256:f4b2edfa0d226b70bd4ca31ea7e389325990283da23465d572ed1f70a7583041" + ], + "version": "==2.6.0" + }, "pillow": { "hashes": [ - "sha256:0804f77cb1e9b6dbd37601cee11283bba39a8d44b9ddb053400c58e0c0d7d9de", - "sha256:0ab7c5b5d04691bcbd570658667dd1e21ca311c62dcfd315ad2255b1cd37f64f", - "sha256:0b3e6cf3ea1f8cecd625f1420b931c83ce74f00c29a0ff1ce4385f99900ac7c4", - "sha256:365c06a45712cd723ec16fa4ceb32ce46ad201eb7bbf6d3c16b063c72b61a3ed", - "sha256:38301fbc0af865baa4752ddae1bb3cbb24b3d8f221bf2850aad96b243306fa03", - "sha256:3aef1af1a91798536bbab35d70d35750bd2884f0832c88aeb2499aa2d1ed4992", - "sha256:3fe0ab49537d9330c9bba7f16a5f8b02da615b5c809cdf7124f356a0f182eccd", - "sha256:45a619d5c1915957449264c81c008934452e3fd3604e36809212300b2a4dab68", - "sha256:49f90f147883a0c3778fd29d3eb169d56416f25758d0f66775db9184debc8010", - "sha256:571b5a758baf1cb6a04233fb23d6cf1ca60b31f9f641b1700bfaab1194020555", - "sha256:5ac381e8b1259925287ccc5a87d9cf6322a2dc88ae28a97fe3e196385288413f", - "sha256:6153db744a743c0c8c91b8e3b9d40e0b13a5d31dbf8a12748c6d9bfd3ddc01ad", - "sha256:6fd63afd14a16f5d6b408f623cc2142917a1f92855f0df997e09a49f0341be8a", - "sha256:70acbcaba2a638923c2d337e0edea210505708d7859b87c2bd81e8f9902ae826", - "sha256:70b1594d56ed32d56ed21a7fbb2a5c6fd7446cdb7b21e749c9791eac3a64d9e4", - "sha256:76638865c83b1bb33bcac2a61ce4d13c17dba2204969dedb9ab60ef62bede686", - "sha256:7b2ec162c87fc496aa568258ac88631a2ce0acfe681a9af40842fc55deaedc99", - "sha256:7cee2cef07c8d76894ebefc54e4bb707dfc7f258ad155bd61d87f6cd487a70ff", - "sha256:7d16d4498f8b374fc625c4037742fbdd7f9ac383fd50b06f4df00c81ef60e829", - "sha256:b50bc1780681b127e28f0075dfb81d6135c3a293e0c1d0211133c75e2179b6c0", - "sha256:bd0582f831ad5bcad6ca001deba4568573a4675437db17c4031939156ff339fa", - "sha256:cfd40d8a4b59f7567620410f966bb1f32dc555b2b19f82a91b147fac296f645c", - "sha256:e3ae410089de680e8f84c68b755b42bc42c0ceb8c03dbea88a5099747091d38e", - "sha256:e9046e559c299b395b39ac7dbf16005308821c2f24a63cae2ab173bd6aa11616", - "sha256:ef6be704ae2bc8ad0ebc5cb850ee9139493b0fc4e81abcc240fb392a63ebc808", - "sha256:f8dc19d92896558f9c4317ee365729ead9d7bbcf2052a9a19a3ef17abbb8ac5b" + "sha256:047d9473cf68af50ac85f8ee5d5f21a60f849bc17d348da7fc85711287a75031", + "sha256:0f66dc6c8a3cc319561a633b6aa82c44107f12594643efa37210d8c924fc1c71", + "sha256:12c9169c4e8fe0a7329e8658c7e488001f6b4c8e88740e76292c2b857af2e94c", + "sha256:248cffc168896982f125f5c13e9317c059f74fffdb4152893339f3be62a01340", + "sha256:27faf0552bf8c260a5cee21a76e031acaea68babb64daf7e8f2e2540745082aa", + "sha256:285edafad9bc60d96978ed24d77cdc0b91dace88e5da8c548ba5937c425bca8b", + "sha256:384b12c9aa8ef95558abdcb50aada56d74bc7cc131dd62d28c2d0e4d3aadd573", + "sha256:38950b3a707f6cef09cd3cbb142474357ad1a985ceb44d921bdf7b4647b3e13e", + "sha256:4aad1b88933fd6dc2846552b89ad0c74ddbba2f0884e2c162aa368374bf5abab", + "sha256:4ac6148008c169603070c092e81f88738f1a0c511e07bd2bb0f9ef542d375da9", + "sha256:4deb1d2a45861ae6f0b12ea0a786a03d19d29edcc7e05775b85ec2877cb54c5e", + "sha256:59aa2c124df72cc75ed72c8d6005c442d4685691a30c55321e00ed915ad1a291", + "sha256:5a47d2123a9ec86660fe0e8d0ebf0aa6bc6a17edc63f338b73ea20ba11713f12", + "sha256:5cc901c2ab9409b4b7ac7b5bcc3e86ac14548627062463da0af3b6b7c555a871", + "sha256:6c1db03e8dff7b9f955a0fb9907eb9ca5da75b5ce056c0c93d33100a35050281", + "sha256:7ce80c0a65a6ea90ef9c1f63c8593fcd2929448613fc8da0adf3e6bfad669d08", + "sha256:809c19241c14433c5d6135e1b6c72da4e3b56d5c865ad5736ab99af8896b8f41", + "sha256:83792cb4e0b5af480588601467c0764242b9a483caea71ef12d22a0d0d6bdce2", + "sha256:846fa202bd7ee0f6215c897a1d33238ef071b50766339186687bd9b7a6d26ac5", + "sha256:9f5529fc02009f96ba95bea48870173426879dc19eec49ca8e08cd63ecd82ddb", + "sha256:a423c2ea001c6265ed28700df056f75e26215fd28c001e93ef4380b0f05f9547", + "sha256:ac4428094b42907aba5879c7c000d01c8278d451a3b7cccd2103e21f6397ea75", + "sha256:b1ae48d87f10d1384e5beecd169c77502fcc04a2c00a4c02b85f0a94b419e5f9", + "sha256:bf4e972a88f8841d8fdc6db1a75e0f8d763e66e3754b03006cbc3854d89f1cb1", + "sha256:c6414f6aad598364aaf81068cabb077894eb88fed99c6a65e6e8217bab62ae7a", + "sha256:c710fcb7ee32f67baf25aa9ffede4795fd5d93b163ce95fdc724383e38c9df96", + "sha256:c7be4b8a09852291c3c48d3c25d1b876d2494a0a674980089ac9d5e0d78bd132", + "sha256:c9e5ffb910b14f090ac9c38599063e354887a5f6d7e6d26795e916b4514f2c1a", + "sha256:e0697b826da6c2472bb6488db4c0a7fa8af0d52fa08833ceb3681358914b14e5", + "sha256:e9a3edd5f714229d41057d56ac0f39ad9bdba6767e8c888c951869f0bdd129b0" ], - "version": "==6.1.0" + "version": "==6.2.1" }, "protobuf": { "hashes": [ - "sha256:26c0d756c7ad6823fccbc3b5f84c619b9cc7ac281496fe0a9d78e32023c45034", - "sha256:3200046e4d4f6c42ed66257dbe15e2e5dc76072c280e9b3d69dc8f3a4fa3fbbc", - "sha256:368f1bae6dd22d04fd2254d30cd301863408a96ff604422e3ddd8ab601f095a4", - "sha256:3902fa1920b4ef9f710797496b309efc5ccd0faeba44dc82ed6a711a244764a0", - "sha256:3a7a8925ba6481b9241cdb5d69cd0b0700f23efed6bb691dc9543faa4aa25d6f", - "sha256:4bc33d49f43c6e9916fb56b7377cb4478cbf25824b4d2bedfb8a4e3df31c12ca", - "sha256:568b434a36e31ed30d60d600b2227666ce150b8b5275948f50411481a4575d6d", - "sha256:5c393cd665d03ce6b29561edd6b0cc4bcb3fb8e2a7843e8f223d693f07f61b40", - "sha256:80072e9ba36c73cf89c01f669c7b123733fc2de1780b428082a850f53cc7865f", - "sha256:843f498e98ad1469ad54ecb4a7ccf48605a1c5d2bd26ae799c7a2cddab4a37ec", - "sha256:aa45443035651cbfae74c8deb53358ba660d8e7a5fbab3fc4beb33fb3e3ca4be", - "sha256:aaab817d9d038dd5f56a6fb2b2e8ae68caf1fd28cc6a963c755fa73268495c13", - "sha256:e6f68b9979dc8f75299293d682f67fecb72d78f98652da2eeb85c85edef1ca94", - "sha256:e7366cabddff3441d583fdc0176ab42eba4ee7090ef857d50c4dd59ad124003a", - "sha256:f0144ad97cd28bfdda0567b9278d25061ada5ad2b545b538cd3577697b32bda3", - "sha256:f655338491481f482042f19016647e50365ab41b75b486e0df56e0dcc425abf4" + "sha256:125713564d8cfed7610e52444c9769b8dcb0b55e25cc7841f2290ee7bc86636f", + "sha256:1accdb7a47e51503be64d9a57543964ba674edac103215576399d2d0e34eac77", + "sha256:27003d12d4f68e3cbea9eb67427cab3bfddd47ff90670cb367fcd7a3a89b9657", + "sha256:3264f3c431a631b0b31e9db2ae8c927b79fc1a7b1b06b31e8e5bcf2af91fe896", + "sha256:3c5ab0f5c71ca5af27143e60613729e3488bb45f6d3f143dc918a20af8bab0bf", + "sha256:45dcf8758873e3f69feab075e5f3177270739f146255225474ee0b90429adef6", + "sha256:56a77d61a91186cc5676d8e11b36a5feb513873e4ae88d2ee5cf530d52bbcd3b", + "sha256:5984e4947bbcef5bd849d6244aec507d31786f2dd3344139adc1489fb403b300", + "sha256:6b0441da73796dd00821763bb4119674eaf252776beb50ae3883bed179a60b2a", + "sha256:6f6677c5ade94d4fe75a912926d6796d5c71a2a90c2aeefe0d6f211d75c74789", + "sha256:84a825a9418d7196e2acc48f8746cf1ee75877ed2f30433ab92a133f3eaf8fbe", + "sha256:b842c34fe043ccf78b4a6cf1019d7b80113707d68c88842d061fa2b8fb6ddedc", + "sha256:ca33d2f09dae149a1dcf942d2d825ebb06343b77b437198c9e2ef115cf5d5bc1", + "sha256:db83b5c12c0cd30150bb568e6feb2435c49ce4e68fe2d7b903113f0e221e58fe", + "sha256:f50f3b1c5c1c1334ca7ce9cad5992f098f460ffd6388a3cabad10b66c2006b09", + "sha256:f99f127909731cafb841c52f9216e447d3e4afb99b17bebfad327a75aee206de" ], - "version": "==3.9.2" + "version": "==3.10.0" + }, + "pycparser": { + "hashes": [ + "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" + ], + "version": "==2.19" + }, + "pynacl": { + "hashes": [ + "sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255", + "sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c", + "sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e", + "sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae", + "sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621", + "sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56", + "sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39", + "sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310", + "sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1", + "sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5", + "sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a", + "sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786", + "sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b", + "sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b", + "sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f", + "sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20", + "sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415", + "sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715", + "sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92", + "sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1", + "sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0" + ], + "version": "==1.3.0" + }, + "pyyaml": { + "hashes": [ + "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", + "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", + "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", + "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", + "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", + "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", + "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", + "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", + "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", + "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", + "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", + "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", + "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + ], + "index": "pypi", + "version": "==5.1.2" }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" ], - "version": "==1.12.0" + "version": "==1.13.0" } }, "develop": { @@ -176,10 +346,10 @@ }, "attrs": { "hashes": [ - "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", - "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], - "version": "==19.1.0" + "version": "==19.3.0" }, "black": { "hashes": [ @@ -212,11 +382,11 @@ }, "flake8": { "hashes": [ - "sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", - "sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696" + "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", + "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" ], "index": "pypi", - "version": "==3.7.8" + "version": "==3.7.9" }, "identify": { "hashes": [ @@ -230,6 +400,7 @@ "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" ], + "markers": "python_version < '3.8'", "version": "==0.23" }, "importlib-resources": { @@ -270,11 +441,11 @@ }, "pre-commit": { "hashes": [ - "sha256:1d3c0587bda7c4e537a46c27f2c84aa006acc18facf9970bf947df596ce91f3f", - "sha256:fa78ff96e8e9ac94c748388597693f18b041a181c94a4f039ad20f45287ba44a" + "sha256:9f152687127ec90642a2cc3e4d9e1e6240c4eb153615cb02aa1ad41d331cbb6e", + "sha256:c2e4810d2d3102d354947907514a78c5d30424d299dc0fe48f5aa049826e9b50" ], "index": "pypi", - "version": "==1.18.3" + "version": "==1.20.0" }, "pycodestyle": { "hashes": [ @@ -306,14 +477,15 @@ "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" ], + "index": "pypi", "version": "==5.1.2" }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" ], - "version": "==1.12.0" + "version": "==1.13.0" }, "toml": { "hashes": [ @@ -324,10 +496,10 @@ }, "virtualenv": { "hashes": [ - "sha256:680af46846662bb38c5504b78bad9ed9e4f3ba2d54f54ba42494fdf94337fe30", - "sha256:f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2" + "sha256:11cb4608930d5fd3afb545ecf8db83fa50e1f96fc4fca80c94b07d2c83146589", + "sha256:d257bb3773e48cac60e475a19b608996c73f4d333b3ba2e4e57d5ac6134e0136" ], - "version": "==16.7.5" + "version": "==16.7.7" }, "zipp": { "hashes": [ diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 671504a8..9c9c0d2f 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -26,6 +26,7 @@ class Application(tk.Frame): self.radiovar = tk.IntVar(value=1) self.show_grid_var = tk.IntVar(value=1) self.adjust_to_dim_var = tk.IntVar(value=0) + self.config = appdirs.read_config() self.core = CoreClient(self) self.setup_app() self.draw_menu() diff --git a/coretk/coretk/appdirs.py b/coretk/coretk/appdirs.py index 7dae4bf3..80710920 100644 --- a/coretk/coretk/appdirs.py +++ b/coretk/coretk/appdirs.py @@ -2,6 +2,8 @@ import logging import shutil from pathlib import Path +import yaml + # gui home paths HOME_PATH = Path.home().joinpath(".coretk") BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds") @@ -17,6 +19,11 @@ LOCAL_ICONS_PATH = Path(__file__).parent.joinpath("icons").absolute() LOCAL_BACKGROUND_PATH = Path(__file__).parent.joinpath("backgrounds").absolute() +class IndentDumper(yaml.Dumper): + def increase_indent(self, flow=False, indentless=False): + return super().increase_indent(flow, False) + + def check_directory(): if HOME_PATH.exists(): logging.info("~/.coretk exists") @@ -36,4 +43,14 @@ def check_directory(): new_background = BACKGROUNDS_PATH.joinpath(background.name) shutil.copy(background, new_background) with CONFIG_PATH.open("w") as f: - f.write("# gui config") + yaml.dump( + {"servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}]}, + f, + Dumper=IndentDumper, + default_flow_style=False, + ) + + +def read_config(): + with CONFIG_PATH.open("r") as f: + return yaml.load(f, Loader=yaml.SafeLoader) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 38e0ec2c..ccb4c7ee 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -77,6 +77,11 @@ class CoreClient: # distributed server data self.servers = {} + for server_config in self.app.config["servers"]: + server = CoreServer( + server_config["name"], server_config["address"], server_config["port"] + ) + self.servers[server.name] = server # data for managing the current session self.nodes = {} From 20637da14020dd8b3903972f9b22ada9d84e3d46 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 5 Nov 2019 14:25:25 -0800 Subject: [PATCH 176/462] change to appropriate toolbar when join session, emane config, emane model config --- coretk/coretk/configutils.py | 7 +- coretk/coretk/coreclient.py | 19 ++ coretk/coretk/coretoolbar.py | 12 -- coretk/coretk/coretoolbarhelp.py | 13 +- coretk/coretk/dialogs/emaneconfig.py | 268 ++++++++++++++++++--------- coretk/coretk/graph.py | 6 - daemon/data/logging.conf | 2 +- 7 files changed, 217 insertions(+), 110 deletions(-) diff --git a/coretk/coretk/configutils.py b/coretk/coretk/configutils.py index bb3e793e..267feb73 100644 --- a/coretk/coretk/configutils.py +++ b/coretk/coretk/configutils.py @@ -7,6 +7,7 @@ from tkinter import ttk class ConfigType(enum.Enum): STRING = 10 BOOL = 11 + EMANECONFIG = 7 def create_config(master, config, padx=2, pady=2): @@ -52,8 +53,13 @@ def create_config(master, config, padx=2, pady=2): else: value.set("Off") elif config_type == ConfigType.STRING: + value.set(option.value) entry = tk.Entry(frame, textvariable=value) entry.grid(row=index, column=1, sticky="ew", pady=pady) + elif config_type == ConfigType.EMANECONFIG: + value.set(option.value) + entry = tk.Entry(frame, textvariable=value, bg="white") + entry.grid(row=index, column=1, sticky="ew", pady=pady) else: logging.error("unhandled config option type: %s", config_type) values[key] = value @@ -68,7 +74,6 @@ def create_config(master, config, padx=2, pady=2): canvas.bind( "", lambda event: canvas.itemconfig(frame_id, width=event.width) ) - return values diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index a6d4f19c..67385809 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -80,6 +80,7 @@ class CoreClient: self.core_mapping = CoreToCanvasMapping() self.wlanconfig_management = WlanNodeConfig() self.mobilityconfig_management = MobilityNodeConfig() + self.emane_config = None def handle_events(self, event): logging.info("event: %s", event) @@ -110,11 +111,15 @@ class CoreClient: self.nodes.clear() self.edges.clear() self.hooks.clear() + self.wlanconfig_management.configurations.clear() + self.mobilityconfig_management.configurations.clear() + self.emane_config = None # get session data response = self.client.get_session(self.session_id) logging.info("joining session(%s): %s", self.session_id, response) session = response.session + session_state = session.state self.client.events(self.session_id, self.handle_events) # get hooks @@ -140,6 +145,11 @@ class CoreClient: config = {x: node_config[x].value for x in node_config} self.mobilityconfig_management.configurations[node_id] = config + # get emane config + response = self.client.get_emane_config(self.session_id) + logging.info("emane config: %s", response) + self.emane_config = response.config + # determine next node id and reusable nodes max_id = 1 for node in session.nodes: @@ -154,6 +164,14 @@ class CoreClient: # draw session self.app.canvas.canvas_reset_and_redraw(session) + # draw tool bar appropritate with session state + if session_state == core_pb2.SessionState.RUNTIME: + self.app.core_editbar.destroy_children_widgets() + self.app.core_editbar.create_runtime_toolbar() + else: + self.app.core_editbar.destroy_children_widgets() + self.app.core_editbar.create_toolbar() + def create_new_session(self): """ Create a new session @@ -298,6 +316,7 @@ class CoreClient: links, hooks=list(self.hooks.values()), wlan_configs=wlan_configs, + emane_config=emane_config, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index f9d0a461..41c18eed 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -7,18 +7,6 @@ from coretk.graph import GraphMode from coretk.images import ImageEnum, Images from coretk.tooltip import CreateToolTip -# from enum import Enum - - -# class SessionStateEnum(Enum): -# NONE = "none" -# DEFINITION = "definition" -# CONFIGURATION = "configuration" -# RUNTIME = "runtime" -# DATACOLLECT = "datacollect" -# SHUTDOWN = "shutdown" -# INSTANTIATION = "instantiation" - class CoreToolbar(object): """ diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index aa53fbbc..d9c7e0bf 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -3,6 +3,8 @@ CoreToolbar help to draw on canvas, and make grpc client call """ from core.api.grpc.client import core_pb2 +# from coretk import configutils + class CoreToolbarHelp: def __init__(self, app): @@ -85,6 +87,15 @@ class CoreToolbarHelp: links = self.get_link_list() wlan_configs = self.get_wlan_configuration_list() mobility_configs = self.get_mobility_configuration_list() + + # get emane config + pb_emane_config = self.app.core.emane_config + emane_config = {x: pb_emane_config[x].value for x in pb_emane_config} + self.app.core.start_session( - nodes, links, wlan_configs=wlan_configs, mobility_configs=mobility_configs + nodes, + links, + wlan_configs=wlan_configs, + mobility_configs=mobility_configs, + emane_config=emane_config, ) diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index 256ff69d..d6436deb 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -5,11 +5,15 @@ emane configuration import logging import tkinter as tk import webbrowser +from tkinter import ttk +from coretk import configutils from coretk.dialogs.dialog import Dialog -from coretk.dialogs.mobilityconfig import MobilityConfiguration from coretk.images import ImageEnum, Images +PAD_X = 2 +PAD_Y = 2 + class EmaneConfiguration(Dialog): def __init__(self, master, app, canvas_node): @@ -20,6 +24,13 @@ class EmaneConfiguration(Dialog): self.radiovar.set(1) self.columnconfigure(0, weight=1) + # list(string) of emane models + self.emane_models = None + + self.emane_dialog = Dialog(self, app, "emane configuration", modal=False) + self.emane_model_dialog = None + self.emane_model_combobox = None + # draw self.node_name_and_image() self.emane_configuration() @@ -27,8 +38,10 @@ class EmaneConfiguration(Dialog): self.emane_options() self.draw_apply_and_cancel() - def browse_emane_wiki(self): - webbrowser.open_new("https://github.com/adjacentlink/emane/wiki") + self.values = None + self.options = app.core.emane_config + self.model_options = None + self.model_values = None def create_text_variable(self, val): """ @@ -52,20 +65,109 @@ class EmaneConfiguration(Dialog): e = tk.Entry(f, textvariable=self.create_text_variable(""), bg="white") e.grid(row=0, column=1, padx=2, pady=2) - om = tk.OptionMenu( - f, - self.create_text_variable("None"), - "(none)", - "core1", - "core2", - command=self.choose_core, - ) - om.grid(row=0, column=2, padx=2, pady=2) + cbb = ttk.Combobox(f, values=["(none)", "core1", "core2"], state="readonly") + cbb.current(0) + cbb.grid(row=0, column=2, padx=2, pady=2) b = tk.Button(f, image=self.canvas_node.image) b.grid(row=0, column=3, padx=2, pady=2) - f.grid(row=0, column=0, sticky=tk.N + tk.S + tk.E + tk.W) + f.grid(row=0, column=0, sticky="nsew") + + def save_emane_option(self): + configutils.parse_config(self.options, self.values) + self.emane_dialog.destroy() + + def draw_emane_options(self): + if not self.emane_dialog.winfo_exists(): + self.emane_dialog = Dialog( + self, self.app, "emane configuration", modal=False + ) + b1 = tk.Button(self.emane_dialog, text="Appy", command=self.save_emane_option) + b2 = tk.Button( + self.emane_dialog, text="Cancel", command=self.emane_dialog.destroy + ) + + if self.options is None: + session_id = self.app.core.session_id + response = self.app.core.client.get_emane_config(session_id) + logging.info("emane config: %s", response) + self.options = response.config + self.values = configutils.create_config( + self.emane_dialog, self.options, PAD_X, PAD_Y + ) + b1.grid(row=1, column=0) + b2.grid(row=1, column=1) + self.emane_dialog.show() + + def save_emane_model_options(self): + """ + configure the node's emane model on the fly + + :return: nothing + """ + # get model name + model_name = self.emane_models[self.emane_model_combobox.current()] + + # parse configuration + configutils.parse_config(self.model_options, self.model_values) + config = {x: self.model_options[x].value for x in self.model_options} + + # add string emane_ infront for grpc call + response = self.app.core.client.set_emane_model_config( + self.app.core.session_id, + self.canvas_node.core_id, + "emane_" + model_name, + config, + ) + logging.info( + "emaneconfig.py config emane model (%s), result: %s", + self.canvas_node.core_id, + response, + ) + + self.emane_model_dialog.destroy() + + def draw_model_options(self): + """ + draw emane model configuration + + :return: nothing + """ + # get model name + model_name = self.emane_models[self.emane_model_combobox.current()] + + # create the dialog and the necessry widget + if not self.emane_model_dialog or not self.emane_model_dialog.winfo_exists(): + self.emane_model_dialog = Dialog( + self, self.app, model_name + " configuration", modal=False + ) + + b1 = tk.Button( + self.emane_model_dialog, text="Apply", command=self.save_emane_model_options + ) + b2 = tk.Button( + self.emane_model_dialog, + text="Cancel", + command=self.emane_model_dialog.destroy, + ) + + # query for configurations + session_id = self.app.core.session_id + # add string emane_ before model name for grpc call + response = self.app.core.client.get_emane_model_config( + session_id, self.canvas_node.core_id, "emane_" + model_name + ) + logging.info("emane model config %s", response) + + self.model_options = response.config + self.model_values = configutils.create_config( + self.emane_model_dialog, self.model_options, PAD_X, PAD_Y + ) + + b1.grid(row=1, column=0, sticky="nsew") + b2.grid(row=1, column=1, sticky="nsew") + self.emane_model_dialog.show() def draw_option_buttons(self, parent): f = tk.Frame(parent, bg="#d9d9d9") @@ -73,60 +175,53 @@ class EmaneConfiguration(Dialog): f.columnconfigure(1, weight=1) b = tk.Button( f, - text="model options", + text=self.emane_models[0] + " options", image=Images.get(ImageEnum.EDITNODE), compound=tk.RIGHT, bg="#d9d9d9", - state=tk.DISABLED, + command=self.draw_model_options, ) - b.grid(row=0, column=0, padx=10, pady=2, sticky=tk.N + tk.S + tk.E + tk.W) + b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") b = tk.Button( f, text="EMANE options", image=Images.get(ImageEnum.EDITNODE), compound=tk.RIGHT, bg="#d9d9d9", + command=self.draw_emane_options, ) - b.grid(row=0, column=1, padx=10, pady=2, sticky=tk.N + tk.S + tk.E + tk.W) - f.grid(row=4, column=0, sticky=tk.N + tk.S + tk.E + tk.W) + b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") + f.grid(row=4, column=0, sticky="nsew") - def radiobutton_text(self, val): + def combobox_select(self, event): """ - get appropriate text based on radio value + update emane model options button - :return: the text value to configure button + :param event: + :return: nothing """ - if val == 1: - return "none" - elif val == 2: - return "rfpipe options" - elif val == 3: - return "ieee80211abg options" - elif val == 4: - return "commeffect options" - elif val == 5: - return "bypass options" - elif val == 6: - return "tdma options" - else: - logging.debug("emaneconfig.py invalid radio value") - return "" + # get model name + model_name = self.emane_models[self.emane_model_combobox.current()] - def click_radio_button(self): - print(type(self.radiovar.get())) + # get the button and configure button text config_frame = self.grid_slaves(row=2, column=0)[0] option_button_frame = config_frame.grid_slaves(row=4, column=0)[0] b = option_button_frame.grid_slaves(row=0, column=0)[0] - text = self.radiobutton_text(self.radiovar.get()) - if text == "none": - state = tk.DISABLED - else: - state = tk.NORMAL - b.config(text=text, state=state) - # b.config(text=) + b.config(text=model_name + " options") def draw_emane_models(self, parent): - models = ["none", "rfpipe", "ieee80211abg", "commeffect", "bypass", "tdma"] + """ + create a combobox that has all the known emane models + + :param parent: parent + :return: nothing + """ + # query for all the known model names + session_id = self.app.core.session_id + response = self.app.core.client.get_emane_models(session_id) + self.emane_models = [x.split("_")[1] for x in response.models] + + # create combo box and its binding f = tk.Frame( parent, bg="#d9d9d9", @@ -135,20 +230,12 @@ class EmaneConfiguration(Dialog): highlightthickness=0.5, bd=0, ) - value = 1 - for m in models: - b = tk.Radiobutton( - f, - text=m, - variable=self.radiovar, - indicatoron=True, - value=value, - bg="#d9d9d9", - highlightthickness=0, - command=self.click_radio_button, - ) - b.grid(sticky=tk.W) - value = value + 1 + self.emane_model_combobox = ttk.Combobox( + f, values=self.emane_models, state="readonly" + ) + self.emane_model_combobox.grid() + self.emane_model_combobox.current(0) + self.emane_model_combobox.bind("<>", self.combobox_select) f.grid(row=3, column=0, sticky=tk.W + tk.E) def draw_text_label_and_entry(self, parent, label_text, entry_text): @@ -164,11 +251,19 @@ class EmaneConfiguration(Dialog): lbl.grid(row=0, column=0) e = tk.Entry(f, textvariable=var, bg="white") e.grid(row=0, column=1) - f.grid(stick=tk.W) + f.grid(stick=tk.W, padx=2, pady=2) def emane_configuration(self): + """ + draw the main frame for emane configuration + + :return: nothing + """ + # draw label lbl = tk.Label(self, text="Emane") lbl.grid(row=1, column=0) + + # main frame that has emane wiki, a short description, emane models and the configure buttons f = tk.Frame( self, bg="#d9d9d9", @@ -187,7 +282,9 @@ class EmaneConfiguration(Dialog): compound=tk.RIGHT, relief=tk.RAISED, bg="#d9d9d9", - command=self.browse_emane_wiki, + command=lambda: webbrowser.open_new( + "https://github.com/adjacentlink/emane/wiki" + ), ) b.grid(row=0, column=0, sticky=tk.W) @@ -197,54 +294,47 @@ class EmaneConfiguration(Dialog): "\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details", bg="#d9d9d9", ) - lbl.grid(row=1, column=0, sticky=tk.N + tk.S + tk.E + tk.W) + lbl.grid(row=1, column=0, sticky="nsew") lbl = tk.Label(f, text="EMANE Models", bg="#d9d9d9") lbl.grid(row=2, column=0, sticky=tk.W) - self.draw_option_buttons(f) + self.draw_emane_models(f) - f.grid(row=2, column=0, sticky=tk.N + tk.S + tk.E + tk.W) + self.draw_option_buttons(f) + + f.grid(row=2, column=0, sticky="nsew") def draw_ip_subnets(self): self.draw_text_label_and_entry(self, "IPv4 subnet", "") self.draw_text_label_and_entry(self, "IPv6 subnet", "") - def click_ns2_mobility_script(self): - dialog = MobilityConfiguration(self, self.app, self.canvas_node) - dialog.show() - def emane_options(self): """ create wireless node options :return: """ - f = tk.Frame(self) + f = tk.Frame(self, bg="#d9d9d9") f.columnconfigure(0, weight=1) f.columnconfigure(1, weight=1) - f.columnconfigure(2, weight=1) - b = tk.Button( - f, - text="ns-2 mobility script...", - command=lambda: self.click_ns2_mobility_script(), - ) - # b.pack(side=tk.LEFT, padx=1) - b.grid(row=0, column=0, padx=10, pady=2, sticky=tk.N + tk.S + tk.E + tk.W) - b = tk.Button(f, text="Link to all routers") - b.grid(row=0, column=1, padx=10, pady=2, sticky=tk.N + tk.S + tk.E + tk.W) - # b.pack(side=tk.LEFT, padx=1) - b = tk.Button(f, text="Choose WLAN members") - b.grid(row=0, column=2, padx=10, pady=2, sticky=tk.N + tk.S + tk.E + tk.W) - # b.pack(side=tk.LEFT, padx=1) - f.grid(row=5, column=0, sticky=tk.N + tk.S + tk.E + tk.W) + b = tk.Button(f, text="Link to all routers", bg="#d9d9d9") + b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") + b = tk.Button(f, text="Choose WLAN members", bg="#d9d9d9") + b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") + f.grid(row=5, column=0, sticky="nsew") + + def apply(self): + # save emane configuration + self.app.core.emane_config = self.options + self.destroy() def draw_apply_and_cancel(self): f = tk.Frame(self, bg="#d9d9d9") f.columnconfigure(0, weight=1) f.columnconfigure(1, weight=1) - b = tk.Button(f, text="Apply", bg="#d9d9d9") - b.grid(row=0, column=0, padx=10, pady=2, sticky=tk.N + tk.S + tk.E + tk.W) + b = tk.Button(f, text="Apply", bg="#d9d9d9", command=self.apply) + b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") b = tk.Button(f, text="Cancel", bg="#d9d9d9", command=self.destroy) - b.grid(row=0, column=1, padx=10, pady=2, sticky=tk.N + tk.S + tk.E + tk.W) + b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") - f.grid(sticky=tk.N + tk.S + tk.E + tk.W) + f.grid(sticky="nsew") diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 2bf02854..448a6f1d 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -223,12 +223,6 @@ class CanvasGraph(tk.Canvas): for i in self.find_withtag("node"): self.lift(i) - # def delete_components(self): - # tags = ["node", "edge", "linkinfo", "nodename"] - # for i in tags: - # for id in self.find_withtag(i): - # self.delete(id) - def canvas_xy(self, event): """ Convert window coordinate to canvas coordinate diff --git a/daemon/data/logging.conf b/daemon/data/logging.conf index 7f3d496f..46de6e92 100644 --- a/daemon/data/logging.conf +++ b/daemon/data/logging.conf @@ -14,7 +14,7 @@ } }, "root": { - "level": "INFO", + "level": "DEBUG", "handlers": ["console"] } } From 6405af24293ff2eca18fae18b798f15da5400297 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 5 Nov 2019 15:15:22 -0800 Subject: [PATCH 177/462] more work on coretk --- coretk/coretk/coretoolbarhelp.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index d9c7e0bf..915d3b80 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -3,8 +3,6 @@ CoreToolbar help to draw on canvas, and make grpc client call """ from core.api.grpc.client import core_pb2 -# from coretk import configutils - class CoreToolbarHelp: def __init__(self, app): From 1c36c5e2915542821227fb6f0f53b315bc125f91 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 5 Nov 2019 16:16:46 -0800 Subject: [PATCH 178/462] startred custom node dialog, update node services dialog to use service data retrieved from grpc --- coretk/coretk/appdirs.py | 14 ++--- coretk/coretk/coreclient.py | 10 +++- coretk/coretk/coretoolbar.py | 17 +----- coretk/coretk/dialogs/customnodes.py | 82 ++++++++++++++++++++++++++++ coretk/coretk/dialogs/nodeservice.py | 71 +++--------------------- 5 files changed, 110 insertions(+), 84 deletions(-) create mode 100644 coretk/coretk/dialogs/customnodes.py diff --git a/coretk/coretk/appdirs.py b/coretk/coretk/appdirs.py index 80710920..51a78f76 100644 --- a/coretk/coretk/appdirs.py +++ b/coretk/coretk/appdirs.py @@ -42,15 +42,15 @@ def check_directory(): for background in LOCAL_BACKGROUND_PATH.glob("*"): new_background = BACKGROUNDS_PATH.joinpath(background.name) shutil.copy(background, new_background) - with CONFIG_PATH.open("w") as f: - yaml.dump( - {"servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}]}, - f, - Dumper=IndentDumper, - default_flow_style=False, - ) + config = {"servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}]} + save_config(config) def read_config(): with CONFIG_PATH.open("r") as f: return yaml.load(f, Loader=yaml.SafeLoader) + + +def save_config(config): + with CONFIG_PATH.open("w") as f: + yaml.dump(config, f, Dumper=IndentDumper, default_flow_style=False) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index ccb4c7ee..a5ccc14a 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -74,6 +74,7 @@ class CoreClient: self.app = app self.master = app.master self.interface_helper = None + self.services = {} # distributed server data self.servers = {} @@ -188,9 +189,16 @@ class CoreClient: :return: existing sessions """ self.client.connect() - response = self.client.get_sessions() + + # get service information + response = self.client.get_services() + for service in response.services: + group_services = self.services.setdefault(service.group, []) + group_services.append(service) # if there are no sessions, create a new session, else join a session + response = self.client.get_sessions() + logging.info("current sessions: %s", response) sessions = response.sessions if len(sessions) == 0: self.create_new_session() diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 5d7c05ae..f73cc384 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -1,24 +1,12 @@ import logging import tkinter as tk -# from core.api.grpc import core_pb2 from coretk.coretoolbarhelp import CoreToolbarHelp +from coretk.dialogs.customnodes import CustomNodesDialog from coretk.graph import GraphMode from coretk.images import ImageEnum, Images from coretk.tooltip import CreateToolTip -# from enum import Enum - - -# class SessionStateEnum(Enum): -# NONE = "none" -# DEFINITION = "definition" -# CONFIGURATION = "configuration" -# RUNTIME = "runtime" -# DATACOLLECT = "datacollect" -# SHUTDOWN = "shutdown" -# INSTANTIATION = "instantiation" - class CoreToolbar(object): """ @@ -245,11 +233,12 @@ class CoreToolbar(object): self.canvas.draw_node_image = Images.get(ImageEnum.OVS) self.canvas.draw_node_name = "OVS" - # TODO what graph node is this def pick_editnode(self, main_button): self.network_layer_option_menu.destroy() main_button.configure(image=Images.get(ImageEnum.EDITNODE)) logging.debug("Pick editnode option") + dialog = CustomNodesDialog(self.app, self.app) + dialog.show() def draw_network_layer_options(self, network_layer_button): """ diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py new file mode 100644 index 00000000..b38196fe --- /dev/null +++ b/coretk/coretk/dialogs/customnodes.py @@ -0,0 +1,82 @@ +import tkinter as tk + +from coretk.dialogs.dialog import Dialog + + +class CustomNodesDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Custom Nodes", modal=True) + self.save_button = None + self.delete_button = None + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.draw_node_config() + self.draw_node_buttons() + self.draw_buttons() + + def draw_node_config(self): + frame = tk.Frame(self) + frame.grid(sticky="nsew") + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + + scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar.grid(row=0, column=1, sticky="ns") + + listbox = tk.Listbox(frame) + listbox.grid( + row=0, + column=0, + selectmode=tk.SINGLE, + yscrollcommand=scrollbar.set, + sticky="nsew", + ) + + scrollbar.config(command=listbox.yview) + + frame = tk.Frame(frame) + frame.grid(row=0, column=2, sticky="nsew") + frame.columnconfigure(0, weight=1) + + def draw_node_buttons(self): + frame = tk.Frame(self) + frame.grid(pady=2, sticky="ew") + for i in range(3): + frame.columnconfigure(i, weight=1) + + button = tk.Button(frame, text="Create", command=self.click_create) + button.grid(row=0, column=0, sticky="ew") + + self.save_button = tk.Button( + frame, text="Save", state=tk.DISABLED, command=self.click_save + ) + self.save_button.grid(row=0, column=1, sticky="ew") + + self.delete_button = tk.Button( + frame, text="Delete", state=tk.DISABLED, command=self.click_delete + ) + self.delete_button.grid(row=0, column=2, sticky="ew") + + def draw_buttons(self): + frame = tk.Frame(self) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + + button = tk.Button(frame, text="Save Configuration") + button.grid(row=0, column=0, sticky="ew") + + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_create(self): + pass + + def click_save(self): + pass + + def click_delete(self): + pass diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 82d36cca..6d92b98e 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -6,58 +6,6 @@ from tkinter import messagebox from coretk.dialogs.dialog import Dialog -CORE_DEFAULT_GROUPS = ["EMANE", "FRR", "ProtoSvc", "Quagga", "Security", "Utility"] -DEFAULT_GROUP_RADIO_VALUE = { - "EMANE": 1, - "FRR": 2, - "ProtoSvc": 3, - "Quagga": 4, - "Security": 5, - "Utility": 6, -} -DEFAULT_GROUP_SERVICES = { - "EMANE": ["transportd"], - "FRR": [ - "FRRBable", - "FRRBGP", - "FRROSPFv2", - "FRROSPFv3", - "FRRpimd", - "FRRRIP", - "FRRRIPNG", - "FRRzebra", - ], - "ProtoSvc": ["MGEN_Sink", "MgenActor", "SMF"], - "Quagga": [ - "Babel", - "BGP", - "OSPFv2", - "OSPFv3", - "OSPFv3MDR", - "RIP", - "RIPNG", - "Xpimd", - "zebra", - ], - "Security": ["Firewall", "IPsec", "NAT", "VPNClient", "VPNServer"], - "Utility": [ - "atd", - "DefaultMulticastRoute", - "DefaultRoute", - "DHCP", - "DHCPClient", - "FTP", - "HTTP", - "IPForward ", - "pcap", - "radvd", - "SSH", - "StaticRoute", - "ucarp", - "UserDefined", - ], -} - class NodeServicesDialog(Dialog): def __init__(self, master, app, canvas_node): @@ -66,6 +14,7 @@ class NodeServicesDialog(Dialog): self.core_groups = [] self.service_to_config = None self.config_frame = None + self.services_list = None self.draw() def draw(self): @@ -110,7 +59,7 @@ class NodeServicesDialog(Dialog): listbox.grid(row=1, column=0, sticky="nsew") listbox.bind("<>", self.handle_group_change) - for group in CORE_DEFAULT_GROUPS: + for group in sorted(self.app.core.services): listbox.insert(tk.END, group) scrollbar.config(command=listbox.yview) @@ -127,7 +76,7 @@ class NodeServicesDialog(Dialog): scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) scrollbar.grid(row=1, column=1, sticky="ns") - listbox = tk.Listbox( + self.services_list = tk.Listbox( frame, selectmode=tk.SINGLE, yscrollcommand=scrollbar.set, @@ -135,10 +84,10 @@ class NodeServicesDialog(Dialog): highlightthickness=0.5, bd=0, ) - listbox.grid(row=1, column=0, sticky="nsew") - listbox.bind("<>", self.handle_service_change) + self.services_list.grid(row=1, column=0, sticky="nsew") + self.services_list.bind("<>", self.handle_service_change) - scrollbar.config(command=listbox.yview) + scrollbar.config(command=self.services_list.yview) def draw_current_services(self): frame = tk.Frame(self.config_frame) @@ -188,11 +137,9 @@ class NodeServicesDialog(Dialog): self.display_group_services(s) def display_group_services(self, group_name): - group_services_frame = self.config_frame.grid_slaves(row=0, column=1)[0] - listbox = group_services_frame.grid_slaves(row=1, column=0)[0] - listbox.delete(0, tk.END) - for s in DEFAULT_GROUP_SERVICES[group_name]: - listbox.insert(tk.END, s) + self.services_list.delete(0, tk.END) + for service in sorted(self.app.core.services[group_name], key=lambda x: x.name): + self.services_list.insert(tk.END, service.name) def handle_service_change(self, event): print("select group service") From cb72a70d850b9bb8aa06ee070ee56867b406fc21 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 5 Nov 2019 16:23:47 -0800 Subject: [PATCH 179/462] change to fix coretk action --- .github/workflows/coretk-checks.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coretk-checks.yml b/.github/workflows/coretk-checks.yml index 324babfd..e99ed214 100644 --- a/.github/workflows/coretk-checks.yml +++ b/.github/workflows/coretk-checks.yml @@ -15,7 +15,11 @@ jobs: run: | python -m pip install --upgrade pip pip install pipenv - cd coretk + cd daemon + cp setup.py.in setup.py + cp core/constants.py.in core/constants.py + sed -i 's/True/False/g' core/constants.py + cd ../coretk pipenv install --dev - name: isort run: | From 73b147b1520a2eed0f73d3411575932af1129299 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 5 Nov 2019 16:56:47 -0800 Subject: [PATCH 180/462] updated tk servers dialog to save config --- coretk/coretk/dialogs/servers.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index dbc31267..9ab6a694 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -1,5 +1,6 @@ import tkinter as tk +from coretk import appdirs from coretk.coreclient import CoreServer from coretk.dialogs.dialog import Dialog @@ -113,7 +114,14 @@ class ServersDialog(Dialog): button.grid(row=0, column=1, sticky="ew") def click_save_configuration(self): - pass + servers = [] + for name in sorted(self.app.core.servers): + server = self.app.core.servers[name] + servers.append( + {"name": server.name, "address": server.address, "port": server.port} + ) + self.app.config["servers"] = servers + appdirs.save_config(self.app.config) def click_create(self): name = self.name.get() From 275a03b9e72791473d97a44a75ea45456d39a44a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 5 Nov 2019 17:32:48 -0800 Subject: [PATCH 181/462] added observer widget dialog, still needs to be hooked to where widgets will be kept --- coretk/coretk/coremenubar.py | 4 +- coretk/coretk/dialogs/observerwidgets.py | 148 +++++++++++++++++++++++ coretk/coretk/dialogs/servers.py | 9 +- coretk/coretk/menuaction.py | 5 + 4 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 coretk/coretk/dialogs/observerwidgets.py diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index 5add9790..f720b093 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -543,7 +543,9 @@ class CoreMenubar(object): observer_widget_menu.add_command( label="PIM neighbors", command=action.sub_menu_items ) - observer_widget_menu.add_command(label="Edit...", command=action.sub_menu_items) + observer_widget_menu.add_command( + label="Edit...", command=self.menu_action.edit_observer_widgets + ) widget_menu.add_cascade(label="Observer Widgets", menu=observer_widget_menu) diff --git a/coretk/coretk/dialogs/observerwidgets.py b/coretk/coretk/dialogs/observerwidgets.py new file mode 100644 index 00000000..c1c4d170 --- /dev/null +++ b/coretk/coretk/dialogs/observerwidgets.py @@ -0,0 +1,148 @@ +import tkinter as tk + +from coretk.dialogs.dialog import Dialog + + +class Widget: + def __init__(self, name, command): + self.name = name + self.command = command + + +class ObserverWidgetsDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Observer Widgets", modal=True) + self.config_widgets = {} + self.widgets = None + self.save_button = None + self.delete_button = None + self.selected = None + self.selected_index = None + self.name = tk.StringVar() + self.command = tk.StringVar() + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.draw_widgets() + self.draw_widget_fields() + self.draw_widget_buttons() + self.draw_apply_buttons() + + def draw_widgets(self): + frame = tk.Frame(self) + frame.grid(sticky="nsew") + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + + scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar.grid(row=0, column=1, sticky="ns") + + self.widgets = tk.Listbox( + frame, selectmode=tk.SINGLE, yscrollcommand=scrollbar.set + ) + self.widgets.grid(row=0, column=0, sticky="nsew") + self.widgets.bind("<>", self.handle_widget_change) + + scrollbar.config(command=self.widgets.yview) + + def draw_widget_fields(self): + frame = tk.Frame(self) + frame.grid(sticky="ew") + frame.columnconfigure(1, weight=1) + + label = tk.Label(frame, text="Name") + label.grid(row=0, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.name) + entry.grid(row=0, column=1, sticky="ew") + + label = tk.Label(frame, text="Command") + label.grid(row=1, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.command) + entry.grid(row=1, column=1, sticky="ew") + + def draw_widget_buttons(self): + frame = tk.Frame(self) + frame.grid(pady=2, sticky="ew") + for i in range(3): + frame.columnconfigure(i, weight=1) + + button = tk.Button(frame, text="Create", command=self.click_create) + button.grid(row=0, column=0, sticky="ew") + + self.save_button = tk.Button( + frame, text="Save", state=tk.DISABLED, command=self.click_save + ) + self.save_button.grid(row=0, column=1, sticky="ew") + + self.delete_button = tk.Button( + frame, text="Delete", state=tk.DISABLED, command=self.click_delete + ) + self.delete_button.grid(row=0, column=2, sticky="ew") + + def draw_apply_buttons(self): + frame = tk.Frame(self) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + + button = tk.Button( + frame, text="Save Configuration", command=self.click_save_configuration + ) + button.grid(row=0, column=0, sticky="ew") + + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_save_configuration(self): + pass + + def click_create(self): + name = self.name.get() + if name not in self.config_widgets: + command = self.command.get() + widget = Widget(name, command) + self.config_widgets[name] = widget + self.widgets.insert(tk.END, name) + + def click_save(self): + name = self.name.get() + if self.selected: + previous_name = self.selected + self.selected = name + widget = self.config_widgets.pop(previous_name) + widget.name = name + widget.command = self.command.get() + self.config_widgets[name] = widget + self.widgets.delete(self.selected_index) + self.widgets.insert(self.selected_index, name) + self.widgets.selection_set(self.selected_index) + + def click_delete(self): + if self.selected: + self.widgets.delete(self.selected_index) + del self.config_widgets[self.selected] + self.selected = None + self.selected_index = None + self.name.set("") + self.command.set("") + self.widgets.selection_clear(0, tk.END) + self.save_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) + + def handle_widget_change(self, event): + selection = self.widgets.curselection() + if selection: + self.selected_index = selection[0] + self.selected = self.widgets.get(self.selected_index) + widget = self.config_widgets[self.selected] + self.name.set(widget.name) + self.command.set(widget.command) + self.save_button.config(state=tk.NORMAL) + self.delete_button.config(state=tk.NORMAL) + else: + self.selected_index = None + self.selected = None + self.save_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index 9ab6a694..3ff95cde 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -40,12 +40,7 @@ class ServersDialog(Dialog): scrollbar.grid(row=0, column=1, sticky="ns") self.servers = tk.Listbox( - frame, - selectmode=tk.SINGLE, - yscrollcommand=scrollbar.set, - relief=tk.FLAT, - highlightthickness=0.5, - bd=0, + frame, selectmode=tk.SINGLE, yscrollcommand=scrollbar.set ) self.servers.grid(row=0, column=0, sticky="nsew") self.servers.bind("<>", self.handle_server_change) @@ -134,7 +129,7 @@ class ServersDialog(Dialog): def click_save(self): name = self.name.get() - if self.selected and name not in self.app.core.servers: + if self.selected: previous_name = self.selected self.selected = name server = self.app.core.servers.pop(previous_name) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index a60f7458..391cf765 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -11,6 +11,7 @@ from coretk.appdirs import XML_PATH from coretk.dialogs.canvasbackground import CanvasBackgroundDialog from coretk.dialogs.canvassizeandscale import SizeAndScaleDialog from coretk.dialogs.hooks import HooksDialog +from coretk.dialogs.observerwidgets import ObserverWidgetsDialog from coretk.dialogs.servers import ServersDialog from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog @@ -403,3 +404,7 @@ class MenuAction: logging.debug("Click session emulation servers") dialog = ServersDialog(self.app, self.app) dialog.show() + + def edit_observer_widgets(self): + dialog = ObserverWidgetsDialog(self.app, self.app) + dialog.show() From 2b3e071045f4aa3c4ad1199210c87ec27c7bd915 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 5 Nov 2019 22:44:50 -0800 Subject: [PATCH 182/462] updated nodeicondialog to just icondialog, added custom widgets for convenience, listboxscroll and checkboxlist --- coretk/coretk/dialogs/customnodes.py | 94 +++++++++++++++++++++++++--- coretk/coretk/dialogs/nodeconfig.py | 6 +- coretk/coretk/dialogs/nodeicon.py | 14 ++--- coretk/coretk/dialogs/wlanconfig.py | 6 +- coretk/coretk/widgets.py | 58 +++++++++++++++++ 5 files changed, 155 insertions(+), 23 deletions(-) create mode 100644 coretk/coretk/widgets.py diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index b38196fe..841b2dcf 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -1,6 +1,68 @@ import tkinter as tk from coretk.dialogs.dialog import Dialog +from coretk.dialogs.nodeicon import IconDialog +from coretk.widgets import CheckboxList, ListboxScroll + + +class ServicesSelectDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Node Services", modal=True) + self.groups = None + self.services = None + self.current = None + self.current_services = set() + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + + frame = tk.Frame(self) + frame.grid(stick="nsew") + frame.rowconfigure(0, weight=1) + for i in range(3): + frame.columnconfigure(i, weight=1) + self.groups = ListboxScroll(frame, text="Groups") + self.groups.grid(row=0, column=0, sticky="nsew") + for group in sorted(self.app.core.services): + self.groups.listbox.insert(tk.END, group) + self.groups.listbox.bind("<>", self.handle_group_change) + + self.services = CheckboxList( + frame, text="Services", clicked=self.service_clicked + ) + self.services.grid(row=0, column=1, sticky="nsew") + + self.current = ListboxScroll(frame, text="Selected") + self.current.grid(row=0, column=2, sticky="nsew") + + frame = tk.Frame(self) + frame.grid(stick="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + button = tk.Button(frame, text="Save") + button.grid(row=0, column=0, sticky="ew") + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def handle_group_change(self, event): + selection = self.groups.listbox.curselection() + if selection: + index = selection[0] + group = self.groups.listbox.get(index) + self.services.clear() + for service in sorted(self.app.core.services[group], key=lambda x: x.name): + self.services.add(service.name) + + def service_clicked(self, name, var): + if var.get() and name not in self.current_services: + self.current_services.add(name) + elif not var.get() and name in self.current_services: + self.current_services.remove(name) + self.current.listbox.delete(0, tk.END) + for name in sorted(self.current_services): + self.current.listbox.insert(tk.END, name) class CustomNodesDialog(Dialog): @@ -8,6 +70,9 @@ class CustomNodesDialog(Dialog): super().__init__(master, app, "Custom Nodes", modal=True) self.save_button = None self.delete_button = None + self.name = tk.StringVar() + self.image_button = None + self.image = None self.draw() def draw(self): @@ -26,20 +91,20 @@ class CustomNodesDialog(Dialog): scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) scrollbar.grid(row=0, column=1, sticky="ns") - listbox = tk.Listbox(frame) - listbox.grid( - row=0, - column=0, - selectmode=tk.SINGLE, - yscrollcommand=scrollbar.set, - sticky="nsew", - ) + listbox = tk.Listbox(frame, selectmode=tk.SINGLE, yscrollcommand=scrollbar.set) + listbox.grid(row=0, column=0, sticky="nsew") scrollbar.config(command=listbox.yview) frame = tk.Frame(frame) frame.grid(row=0, column=2, sticky="nsew") frame.columnconfigure(0, weight=1) + entry = tk.Entry(frame, textvariable=self.name) + entry.grid(sticky="ew") + self.image_button = tk.Button(frame, text="Icon", command=self.click_icon) + self.image_button.grid(sticky="ew") + button = tk.Button(frame, text="Services", command=self.click_services) + button.grid(sticky="ew") def draw_node_buttons(self): frame = tk.Frame(self) @@ -66,12 +131,23 @@ class CustomNodesDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save Configuration") + button = tk.Button(frame, text="Save", command=self.click_save) button.grid(row=0, column=0, sticky="ew") button = tk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") + def click_icon(self): + dialog = IconDialog(self, self.app, self.name.get(), self.image) + dialog.show() + if dialog.image: + self.image = dialog.image + self.image_button.config(image=self.image) + + def click_services(self): + dialog = ServicesSelectDialog(self, self.app) + dialog.show() + def click_create(self): pass diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index b8548c51..bc03dc51 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -2,7 +2,7 @@ import tkinter as tk from tkinter import ttk from coretk.dialogs.dialog import Dialog -from coretk.dialogs.nodeicon import NodeIconDialog +from coretk.dialogs.nodeicon import IconDialog from coretk.dialogs.nodeservice import NodeServicesDialog NETWORKNODETYPES = ["switch", "hub", "wlan", "rj45", "tunnel"] @@ -91,7 +91,9 @@ class NodeConfigDialog(Dialog): dialog.show() def click_icon(self): - dialog = NodeIconDialog(self, self.app, self.canvas_node) + dialog = IconDialog( + self, self.app, self.canvas_node.name, self.canvas_node.image + ) dialog.show() if dialog.image: self.image = dialog.image diff --git a/coretk/coretk/dialogs/nodeicon.py b/coretk/coretk/dialogs/nodeicon.py index d82c0756..4d26e29f 100644 --- a/coretk/coretk/dialogs/nodeicon.py +++ b/coretk/coretk/dialogs/nodeicon.py @@ -6,18 +6,12 @@ from coretk.dialogs.dialog import Dialog from coretk.images import Images -class NodeIconDialog(Dialog): - def __init__(self, master, app, canvas_node): - """ - create an instance of ImageModification - :param master: dialog master - :param coretk.app.Application: main app - :param coretk.graph.CanvasNode canvas_node: node object - """ - super().__init__(master, app, f"{canvas_node.name} Icon", modal=True) +class IconDialog(Dialog): + def __init__(self, master, app, name, image): + super().__init__(master, app, f"{name} Icon", modal=True) self.file_path = tk.StringVar() self.image_label = None - self.image = canvas_node.image + self.image = image self.draw() def draw(self): diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index ad10bf40..d57c8935 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -5,7 +5,7 @@ wlan configuration import tkinter as tk from coretk.dialogs.dialog import Dialog -from coretk.dialogs.nodeicon import NodeIconDialog +from coretk.dialogs.nodeicon import IconDialog class WlanConfigDialog(Dialog): @@ -167,7 +167,9 @@ class WlanConfigDialog(Dialog): button.grid(row=0, column=1, padx=2, sticky="ew") def click_icon(self): - dialog = NodeIconDialog(self, self.app, self.canvas_node) + dialog = IconDialog( + self, self.app, self.canvas_node.name, self.canvas_node.image + ) dialog.show() if dialog.image: self.image = dialog.image diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py new file mode 100644 index 00000000..80347e73 --- /dev/null +++ b/coretk/coretk/widgets.py @@ -0,0 +1,58 @@ +import tkinter as tk +from functools import partial + + +class ListboxScroll(tk.LabelFrame): + def __init__(self, master=None, cnf={}, **kw): + super().__init__(master, cnf, **kw) + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.scrollbar = tk.Scrollbar(self, orient=tk.VERTICAL) + self.scrollbar.grid(row=0, column=1, sticky="ns") + self.listbox = tk.Listbox( + self, selectmode=tk.SINGLE, yscrollcommand=self.scrollbar.set + ) + self.listbox.grid(row=0, column=0, sticky="nsew") + self.scrollbar.config(command=self.listbox.yview) + + +class CheckboxList(tk.LabelFrame): + def __init__(self, master=None, cnf={}, clicked=None, **kw): + super().__init__(master, cnf, **kw) + self.clicked = clicked + self.rowconfigure(0, weight=1) + self.columnconfigure(0, weight=1) + self.columnconfigure(1, weight=1) + self.canvas = tk.Canvas(self, highlightthickness=0) + self.canvas.grid(row=0, columnspan=2, sticky="nsew", padx=2, pady=2) + self.canvas.columnconfigure(0, weight=1) + self.canvas.rowconfigure(0, weight=1) + self.scrollbar = tk.Scrollbar( + self, orient="vertical", command=self.canvas.yview + ) + self.scrollbar.grid(row=0, column=2, sticky="ns") + self.frame = tk.Frame(self.canvas, padx=2, pady=2) + self.frame.columnconfigure(0, weight=1) + self.frame_id = self.canvas.create_window(0, 0, anchor="nw", window=self.frame) + self.canvas.update_idletasks() + self.canvas.configure( + scrollregion=self.canvas.bbox("all"), yscrollcommand=self.scrollbar.set + ) + self.frame.bind( + "", + lambda event: self.canvas.configure(scrollregion=self.canvas.bbox("all")), + ) + self.canvas.bind( + "", + lambda event: self.canvas.itemconfig(self.frame_id, width=event.width), + ) + + def clear(self): + for widget in self.frame.winfo_children(): + widget.destroy() + + def add(self, name): + var = tk.BooleanVar() + func = partial(self.clicked, name, var) + checkbox = tk.Checkbutton(self.frame, text=name, variable=var, command=func) + checkbox.grid(sticky="w") From b71f93e60669cbd90b7c47c500401287982a7bd6 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 6 Nov 2019 07:22:40 -0800 Subject: [PATCH 183/462] added scrollable frame widget which can re-use code to be the basis for other scrollable frame widgets --- coretk/coretk/widgets.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 80347e73..606f7662 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -2,6 +2,41 @@ import tkinter as tk from functools import partial +class FrameScroll(tk.LabelFrame): + def __init__(self, master=None, cnf={}, **kw): + super().__init__(master, cnf, **kw) + self.rowconfigure(0, weight=1) + self.columnconfigure(0, weight=1) + self.columnconfigure(1, weight=1) + self.canvas = tk.Canvas(self, highlightthickness=0) + self.canvas.grid(row=0, columnspan=2, sticky="nsew", padx=2, pady=2) + self.canvas.columnconfigure(0, weight=1) + self.canvas.rowconfigure(0, weight=1) + self.scrollbar = tk.Scrollbar( + self, orient="vertical", command=self.canvas.yview + ) + self.scrollbar.grid(row=0, column=2, sticky="ns") + self.frame = tk.Frame(self.canvas, padx=2, pady=2) + self.frame.columnconfigure(0, weight=1) + self.frame_id = self.canvas.create_window(0, 0, anchor="nw", window=self.frame) + self.canvas.update_idletasks() + self.canvas.configure( + scrollregion=self.canvas.bbox("all"), yscrollcommand=self.scrollbar.set + ) + self.frame.bind( + "", + lambda event: self.canvas.configure(scrollregion=self.canvas.bbox("all")), + ) + self.canvas.bind( + "", + lambda event: self.canvas.itemconfig(self.frame_id, width=event.width), + ) + + def clear(self): + for widget in self.frame.winfo_children(): + widget.destroy() + + class ListboxScroll(tk.LabelFrame): def __init__(self, master=None, cnf={}, **kw): super().__init__(master, cnf, **kw) From 40e1bb0374163ff718b2ab3cf127572ee192c475 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 6 Nov 2019 14:34:41 -0800 Subject: [PATCH 184/462] add emane configurations to my start_session() --- coretk/coretk/coreclient.py | 124 +++++++++++++++------------ coretk/coretk/coretoolbarhelp.py | 31 +++++-- coretk/coretk/dialogs/emaneconfig.py | 5 ++ coretk/coretk/emaneodelnodeconfig.py | 78 +++++++++++++++++ 4 files changed, 175 insertions(+), 63 deletions(-) create mode 100644 coretk/coretk/emaneodelnodeconfig.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 71e37447..ecc35395 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -3,16 +3,16 @@ Incorporate grpc into python tkinter GUI """ import logging import os -from collections import OrderedDict from core.api.grpc import client, core_pb2 from coretk.coretocanvas import CoreToCanvasMapping from coretk.dialogs.sessions import SessionsDialog +from coretk.emaneodelnodeconfig import EmaneModelNodeConfig from coretk.interface import Interface, InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.wlannodeconfig import WlanNodeConfig -link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] +link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel", "emane"] network_layer_nodes = ["router", "host", "PC", "mdr", "prouter", "OVS"] @@ -95,6 +95,7 @@ class CoreClient: self.core_mapping = CoreToCanvasMapping() self.wlanconfig_management = WlanNodeConfig() self.mobilityconfig_management = MobilityNodeConfig() + self.emaneconfig_management = EmaneModelNodeConfig(app) self.emane_config = None def handle_events(self, event): @@ -165,6 +166,8 @@ class CoreClient: logging.info("emane config: %s", response) self.emane_config = response.config + # get emane model config + # determine next node id and reusable nodes max_id = 1 for node in session.nodes: @@ -332,6 +335,8 @@ class CoreClient: hooks=list(self.hooks.values()), wlan_configs=wlan_configs, emane_config=emane_config, + emane_model_configs=emane_model_configs, + mobility_configs=mobility_configs, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) @@ -339,22 +344,22 @@ class CoreClient: response = self.client.stop_session(session_id=self.session_id) logging.debug("coregrpc.py Stop session, result: %s", response.result) - # TODO no need, might get rid of this - def add_link(self, id1, id2, type1, type2, edge): - """ - Grpc client request add link - - :param int session_id: session id - :param int id1: node 1 core id - :param core_pb2.NodeType type1: node 1 core node type - :param int id2: node 2 core id - :param core_pb2.NodeType type2: node 2 core node type - :return: nothing - """ - if1 = self.create_interface(type1, edge.interface_1) - if2 = self.create_interface(type2, edge.interface_2) - response = self.client.add_link(self.session_id, id1, id2, if1, if2) - logging.info("created link: %s", response) + # # TODO no need, might get rid of this + # def add_link(self, id1, id2, type1, type2, edge): + # """ + # Grpc client request add link + # + # :param int session_id: session id + # :param int id1: node 1 core id + # :param core_pb2.NodeType type1: node 1 core node type + # :param int id2: node 2 core id + # :param core_pb2.NodeType type2: node 2 core node type + # :return: nothing + # """ + # if1 = self.create_interface(type1, edge.interface_1) + # if2 = self.create_interface(type2, edge.interface_2) + # response = self.client.add_link(self.session_id, id1, id2, if1, if2) + # logging.info("created link: %s", response) def launch_terminal(self, node_id): response = self.client.get_node_terminal(self.session_id, node_id) @@ -417,22 +422,22 @@ class CoreClient: else: return self.reusable.pop(0) - def add_node(self, node_type, model, x, y, name, node_id): - position = core_pb2.Position(x=x, y=y) - node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) - self.node_ids.append(node_id) - response = self.client.add_node(self.session_id, node) - logging.info("created node: %s", response) - if node_type == core_pb2.NodeType.WIRELESS_LAN: - d = OrderedDict() - d["basic_range"] = "275" - d["bandwidth"] = "54000000" - d["jitter"] = "0" - d["delay"] = "20000" - d["error"] = "0" - r = self.client.set_wlan_config(self.session_id, node_id, d) - logging.debug("set wlan config %s", r) - return response.node_id + # def add_node(self, node_type, model, x, y, name, node_id): + # position = core_pb2.Position(x=x, y=y) + # node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) + # self.node_ids.append(node_id) + # response = self.client.add_node(self.session_id, node) + # logging.info("created node: %s", response) + # if node_type == core_pb2.NodeType.WIRELESS_LAN: + # d = OrderedDict() + # d["basic_range"] = "275" + # d["bandwidth"] = "54000000" + # d["jitter"] = "0" + # d["delay"] = "20000" + # d["error"] = "0" + # r = self.client.set_wlan_config(self.session_id, node_id, d) + # logging.debug("set wlan config %s", r) + # return response.node_id def add_graph_node(self, session_id, canvas_id, x, y, name): """ @@ -458,6 +463,8 @@ class CoreClient: node_type = core_pb2.NodeType.RJ45 elif name == "tunnel": node_type = core_pb2.NodeType.TUNNEL + elif name == "emane": + node_type = core_pb2.NodeType.EMANE elif name in network_layer_nodes: node_type = core_pb2.NodeType.DEFAULT node_model = name @@ -468,9 +475,12 @@ class CoreClient: # set default configuration for wireless node self.wlanconfig_management.set_default_config(node_type, nid) - self.mobilityconfig_management.set_default_configuration(node_type, nid) + # set default emane configuration for emane node + if node_type == core_pb2.NodeType.EMANE: + self.emaneconfig_management.set_default_config(nid) + self.nodes[canvas_id] = create_node self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) # self.core_id_to_canvas_id[nid] = canvas_id @@ -628,44 +638,50 @@ class CoreClient: :return: nothing """ + node_one = self.nodes[canvas_id_1] + node_two = self.nodes[canvas_id_2] if canvas_id_1 in self.nodes and canvas_id_2 in self.nodes: edge = Edge( session_id, - self.nodes[canvas_id_1].node_id, - self.nodes[canvas_id_1].type, - self.nodes[canvas_id_2].node_id, - self.nodes[canvas_id_2].type, + node_one.node_id, + node_one.type, + node_two.node_id, + node_two.type, ) self.edges[token] = edge src_interface, dst_interface = self.create_edge_interface( edge, canvas_id_1, canvas_id_2 ) - node_one_id = self.nodes[canvas_id_1].node_id - node_two_id = self.nodes[canvas_id_2].node_id + node_one_id = node_one.node_id + node_two_id = node_two.node_id # provide a way to get an edge from a core node and an interface id if src_interface is not None: self.core_mapping.map_node_and_interface_to_canvas_edge( node_one_id, src_interface.id, token ) - logging.debug( - "map node id %s, interface_id %s to edge token %s", - node_one_id, - src_interface.id, - token, - ) if dst_interface is not None: self.core_mapping.map_node_and_interface_to_canvas_edge( node_two_id, dst_interface.id, token ) - logging.debug( - "map node id %s, interface_id %s to edge token %s", - node_two_id, - dst_interface.id, - token, - ) - logging.debug("Adding edge to grpc manager...") + if ( + node_one.type == core_pb2.NodeType.EMANE + and node_two.type == core_pb2.NodeType.DEFAULT + ): + if node_two.model == "mdr": + self.emaneconfig_management.set_default_for_mdr( + node_one.node_id, node_two.node_id, dst_interface.id + ) + elif ( + node_two.type == core_pb2.NodeType.EMANE + and node_one.type == core_pb2.NodeType.DEFAULT + ): + if node_one.model == "mdr": + self.emaneconfig_management.set_default_for_mdr( + node_two.node_id, node_one.node_id, src_interface.id + ) + else: logging.error("grpcmanagement.py INVALID CANVAS NODE ID") diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index 915d3b80..c650a5bc 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -34,14 +34,6 @@ class CoreToolbarHelp: for edge in self.app.core.edges.values(): interface_one = self.app.core.create_interface(edge.type1, edge.interface_1) interface_two = self.app.core.create_interface(edge.type2, edge.interface_2) - # TODO for now only consider the basic cases - # if ( - # edge.type1 == core_pb2.NodeType.WIRELESS_LAN - # or edge.type2 == core_pb2.NodeType.WIRELESS_LAN - # ): - # link_type = core_pb2.LinkType.WIRELESS - # else: - # link_type = core_pb2.LinkType.WIRED link = core_pb2.Link( node_one_id=edge.id1, node_two_id=edge.id2, @@ -80,20 +72,41 @@ class CoreToolbarHelp: configs.append(cnf) return configs + def get_emane_configuration_list(self): + """ + form a list of emane configuration for the nodes + + :return: nothing + """ + configs = [] + manager_configs = self.app.core.emaneconfig_management.configurations + for key, value in manager_configs.items(): + config = {x: value[1][x].value for x in value[1]} + configs.append( + core_pb2.EmaneModelConfig( + node_id=key[0], interface_id=key[1], model=value[0], config=config + ) + ) + return configs + def gui_start_session(self): nodes = self.get_node_list() links = self.get_link_list() wlan_configs = self.get_wlan_configuration_list() mobility_configs = self.get_mobility_configuration_list() - # get emane config + # get emane config (global configuration) pb_emane_config = self.app.core.emane_config emane_config = {x: pb_emane_config[x].value for x in pb_emane_config} + # get emane configuration list + emane_model_configs = self.get_emane_configuration_list() + self.app.core.start_session( nodes, links, wlan_configs=wlan_configs, mobility_configs=mobility_configs, emane_config=emane_config, + emane_model_configs=emane_model_configs, ) diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index d6436deb..0521d2cf 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -126,6 +126,11 @@ class EmaneConfiguration(Dialog): response, ) + # store the change locally + self.app.core.emaneconfig_management.set_custom_emane_cloud_config( + self.canvas_node.core_id, "emane_" + model_name + ) + self.emane_model_dialog.destroy() def draw_model_options(self): diff --git a/coretk/coretk/emaneodelnodeconfig.py b/coretk/coretk/emaneodelnodeconfig.py new file mode 100644 index 00000000..e3a9cc33 --- /dev/null +++ b/coretk/coretk/emaneodelnodeconfig.py @@ -0,0 +1,78 @@ +""" +emane model configurations +""" +import logging + + +class EmaneModelNodeConfig: + def __init__(self, app): + """ + create an instance for EmaneModelNodeConfig + + :param app: application + """ + # dict(tuple(node_id, interface_id, model) : config) + self.configurations = {} + + # dict(int, list(int)) stores emane node maps to mdr nodes that are linked to that emane node + self.links = {} + + self.app = app + + def set_default_config(self, node_id): + """ + set a default emane configuration for a newly created emane + + :param int node_id: node id + :return: nothing + """ + session_id = self.app.core.session_id + client = self.app.core.client + default_emane_model = client.get_emane_models(session_id).models[0] + response = client.get_emane_model_config( + session_id, node_id, default_emane_model + ) + logging.info( + "emanemodelnodeconfig.py get emane model config (%s), result: %s", + node_id, + response, + ) + self.configurations[tuple([node_id, None])] = tuple( + [default_emane_model, response.config] + ) + self.links[node_id] = [] + + def set_default_for_mdr(self, emane_node_id, mdr_node_id, interface_id): + """ + set emane configuration of an mdr node on the correct interface + + :param int emane_node_id: emane node id + :param int mdr_node_id: mdr node id + :param int interface_id: interface id + :return: nothing + """ + self.configurations[tuple([mdr_node_id, interface_id])] = self.configurations[ + tuple([emane_node_id, None]) + ] + self.links[emane_node_id].append(tuple([mdr_node_id, interface_id])) + + def set_custom_emane_cloud_config(self, emane_node_id, model_name): + """ + set custom configuration for an emane node, if model is changed, update the nodes connected to that emane node + + :param int emane_node_id: emane node id + :param str model_name: model name + :return: nothing + """ + prev_model_name = self.configurations[tuple([emane_node_id, None])][0] + session_id = self.app.core.session_id + response = self.app.core.client.get_emane_model_config( + session_id, emane_node_id, model_name + ) + self.configurations[tuple([emane_node_id, None])] = tuple( + [model_name, response.config] + ) + + if prev_model_name != model_name: + for k in self.links[emane_node_id]: + self.configurations[k] = tuple([model_name, response.config]) From 99876375647701c5508c8c6d1a26ba01e288da44 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 6 Nov 2019 14:36:36 -0800 Subject: [PATCH 185/462] updated and used common frame scroll class where needed, created common config widget to use where all configurations get drawn --- coretk/coretk/dialogs/customnodes.py | 7 +- coretk/coretk/dialogs/emaneconfig.py | 1 + coretk/coretk/dialogs/sessionoptions.py | 30 +++--- coretk/coretk/widgets.py | 126 ++++++++++++++++-------- 4 files changed, 109 insertions(+), 55 deletions(-) diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 841b2dcf..38d59c83 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -28,6 +28,7 @@ class ServicesSelectDialog(Dialog): for group in sorted(self.app.core.services): self.groups.listbox.insert(tk.END, group) self.groups.listbox.bind("<>", self.handle_group_change) + self.groups.listbox.selection_set(0) self.services = CheckboxList( frame, text="Services", clicked=self.service_clicked @@ -46,6 +47,9 @@ class ServicesSelectDialog(Dialog): button = tk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") + # trigger group change + self.groups.listbox.event_generate("<>") + def handle_group_change(self, event): selection = self.groups.listbox.curselection() if selection: @@ -53,7 +57,8 @@ class ServicesSelectDialog(Dialog): group = self.groups.listbox.get(index) self.services.clear() for service in sorted(self.app.core.services[group], key=lambda x: x.name): - self.services.add(service.name) + checked = service.name in self.current_services + self.services.add(service.name, checked) def service_clicked(self, name, var): if var.get() and name not in self.current_services: diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index d6436deb..b3bca2fd 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -93,6 +93,7 @@ class EmaneConfiguration(Dialog): response = self.app.core.client.get_emane_config(session_id) logging.info("emane config: %s", response) self.options = response.config + self.values = configutils.create_config( self.emane_dialog, self.options, PAD_X, PAD_Y ) diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index e887c2a1..8702d581 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -1,8 +1,8 @@ import logging import tkinter as tk -from coretk import configutils from coretk.dialogs.dialog import Dialog +from coretk.widgets import ConfigFrame PAD_X = 2 PAD_Y = 2 @@ -11,25 +11,33 @@ PAD_Y = 2 class SessionOptionsDialog(Dialog): def __init__(self, master, app): super().__init__(master, app, "Session Options", modal=True) - self.options = None - self.values = None - self.save_button = tk.Button(self, text="Save", command=self.save) - self.cancel_button = tk.Button(self, text="Cancel", command=self.destroy) + self.config_frame = None self.draw() def draw(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + session_id = self.app.core.session_id response = self.app.core.client.get_session_options(session_id) logging.info("session options: %s", response) - self.options = response.config - self.values = configutils.create_config(self, self.options, PAD_X, PAD_Y) - self.save_button.grid(row=1, pady=PAD_Y, padx=PAD_X, sticky="ew") - self.cancel_button.grid(row=1, column=1, pady=PAD_Y, padx=PAD_X, sticky="ew") + + self.config_frame = ConfigFrame(self, config=response.config) + self.config_frame.draw_config() + self.config_frame.grid(sticky="nsew") + + frame = tk.Frame(self) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + button = tk.Button(frame, text="Save", command=self.save) + button.grid(row=0, column=0, pady=PAD_Y, padx=PAD_X, sticky="ew") + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, pady=PAD_Y, padx=PAD_X, sticky="ew") def save(self): - configutils.parse_config(self.options, self.values) + config = self.config_frame.parse_config() session_id = self.app.core.session_id - config = {x: self.options[x].value for x in self.options} response = self.app.core.client.set_session_options(session_id, config) logging.info("saved session config: %s", response) self.destroy() diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 606f7662..cb377dcc 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -1,5 +1,9 @@ +import logging import tkinter as tk from functools import partial +from tkinter import ttk + +from coretk.configutils import ConfigType class FrameScroll(tk.LabelFrame): @@ -7,29 +11,39 @@ class FrameScroll(tk.LabelFrame): super().__init__(master, cnf, **kw) self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) - self.columnconfigure(1, weight=1) self.canvas = tk.Canvas(self, highlightthickness=0) - self.canvas.grid(row=0, columnspan=2, sticky="nsew", padx=2, pady=2) + self.canvas.grid(row=0, sticky="nsew", padx=2, pady=2) self.canvas.columnconfigure(0, weight=1) self.canvas.rowconfigure(0, weight=1) self.scrollbar = tk.Scrollbar( self, orient="vertical", command=self.canvas.yview ) - self.scrollbar.grid(row=0, column=2, sticky="ns") + self.scrollbar.grid(row=0, column=1, sticky="ns") self.frame = tk.Frame(self.canvas, padx=2, pady=2) - self.frame.columnconfigure(0, weight=1) self.frame_id = self.canvas.create_window(0, 0, anchor="nw", window=self.frame) self.canvas.update_idletasks() self.canvas.configure( scrollregion=self.canvas.bbox("all"), yscrollcommand=self.scrollbar.set ) - self.frame.bind( - "", - lambda event: self.canvas.configure(scrollregion=self.canvas.bbox("all")), - ) - self.canvas.bind( - "", - lambda event: self.canvas.itemconfig(self.frame_id, width=event.width), + self.frame.bind("", self._configure_frame) + self.canvas.bind("", self._configure_canvas) + + def _configure_frame(self, event): + req_width = self.frame.winfo_reqwidth() + req_height = self.frame.winfo_reqheight() + if req_width != self.canvas.winfo_reqwidth(): + self.canvas.configure(width=req_width) + if req_height != self.canvas.winfo_reqheight(): + self.canvas.configure(height=req_height) + self.canvas.configure(scrollregion=self.canvas.bbox("all")) + + def _configure_canvas(self, event): + self.canvas.itemconfig(self.frame_id, width=event.width) + + def update_canvas(self): + self.canvas.update_idletasks() + self.canvas.configure( + scrollregion=self.canvas.bbox("all"), yscrollcommand=self.scrollbar.set ) def clear(self): @@ -37,6 +51,61 @@ class FrameScroll(tk.LabelFrame): widget.destroy() +class ConfigFrame(FrameScroll): + def __init__(self, master=None, cnf={}, config=None, **kw): + super().__init__(master, cnf, **kw) + self.frame.columnconfigure(1, weight=1) + if not config: + config = {} + self.config = config + self.values = {} + + def draw_config(self): + padx = 2 + pady = 2 + for index, key in enumerate(sorted(self.config)): + option = self.config[key] + label = tk.Label(self.frame, text=option.label) + label.grid(row=index, pady=pady, padx=padx, sticky="w") + value = tk.StringVar() + config_type = ConfigType(option.type) + if config_type == ConfigType.BOOL: + select = tuple(option.select) + combobox = ttk.Combobox(self.frame, textvariable=value, values=select) + combobox.grid(row=index, column=1, sticky="ew", pady=pady) + if option.value == "1": + value.set("On") + else: + value.set("Off") + elif config_type == ConfigType.STRING: + value.set(option.value) + entry = tk.Entry(self.frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pady) + elif config_type == ConfigType.EMANECONFIG: + value.set(option.value) + entry = tk.Entry(self.frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pady) + else: + logging.error("unhandled config option type: %s", config_type) + self.values[key] = value + + def parse_config(self): + for key in self.config: + option = self.config[key] + value = self.values[key] + config_type = ConfigType(option.type) + config_value = value.get() + if config_type == ConfigType.BOOL: + if config_value == "On": + option.value = "1" + else: + option.value = "0" + else: + option.value = config_value + + return {x: self.config[x].value for x in self.config} + + class ListboxScroll(tk.LabelFrame): def __init__(self, master=None, cnf={}, **kw): super().__init__(master, cnf, **kw) @@ -51,43 +120,14 @@ class ListboxScroll(tk.LabelFrame): self.scrollbar.config(command=self.listbox.yview) -class CheckboxList(tk.LabelFrame): +class CheckboxList(FrameScroll): def __init__(self, master=None, cnf={}, clicked=None, **kw): super().__init__(master, cnf, **kw) self.clicked = clicked - self.rowconfigure(0, weight=1) - self.columnconfigure(0, weight=1) - self.columnconfigure(1, weight=1) - self.canvas = tk.Canvas(self, highlightthickness=0) - self.canvas.grid(row=0, columnspan=2, sticky="nsew", padx=2, pady=2) - self.canvas.columnconfigure(0, weight=1) - self.canvas.rowconfigure(0, weight=1) - self.scrollbar = tk.Scrollbar( - self, orient="vertical", command=self.canvas.yview - ) - self.scrollbar.grid(row=0, column=2, sticky="ns") - self.frame = tk.Frame(self.canvas, padx=2, pady=2) self.frame.columnconfigure(0, weight=1) - self.frame_id = self.canvas.create_window(0, 0, anchor="nw", window=self.frame) - self.canvas.update_idletasks() - self.canvas.configure( - scrollregion=self.canvas.bbox("all"), yscrollcommand=self.scrollbar.set - ) - self.frame.bind( - "", - lambda event: self.canvas.configure(scrollregion=self.canvas.bbox("all")), - ) - self.canvas.bind( - "", - lambda event: self.canvas.itemconfig(self.frame_id, width=event.width), - ) - def clear(self): - for widget in self.frame.winfo_children(): - widget.destroy() - - def add(self, name): - var = tk.BooleanVar() + def add(self, name, checked): + var = tk.BooleanVar(value=checked) func = partial(self.clicked, name, var) checkbox = tk.Checkbutton(self.frame, text=name, variable=var, command=func) checkbox.grid(sticky="w") From 0147bb9988a27e2c750cf6df9222ed5c83070fdc Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 6 Nov 2019 20:49:09 -0800 Subject: [PATCH 186/462] updated config generation to use config frame where possible --- coretk/coretk/configutils.py | 99 ---------------------------- coretk/coretk/dialogs/emaneconfig.py | 63 ++++++++++-------- coretk/coretk/widgets.py | 43 ++++++++---- 3 files changed, 65 insertions(+), 140 deletions(-) delete mode 100644 coretk/coretk/configutils.py diff --git a/coretk/coretk/configutils.py b/coretk/coretk/configutils.py deleted file mode 100644 index 267feb73..00000000 --- a/coretk/coretk/configutils.py +++ /dev/null @@ -1,99 +0,0 @@ -import enum -import logging -import tkinter as tk -from tkinter import ttk - - -class ConfigType(enum.Enum): - STRING = 10 - BOOL = 11 - EMANECONFIG = 7 - - -def create_config(master, config, padx=2, pady=2): - """ - Creates a scrollable canvas with an embedded window for displaying configuration - options. Will use grid layout to consume row 0 and columns 0-2. - - :param master: master to add scrollable canvas to - :param dict config: config option mapping keys to config options - :param int padx: x padding for widgets - :param int pady: y padding for widgets - :return: widget value mapping - """ - master.rowconfigure(0, weight=1) - master.columnconfigure(0, weight=1) - master.columnconfigure(1, weight=1) - - canvas = tk.Canvas(master, highlightthickness=0) - canvas.grid(row=0, columnspan=2, sticky="nsew", padx=padx, pady=pady) - canvas.columnconfigure(0, weight=1) - canvas.rowconfigure(0, weight=1) - - scroll_y = tk.Scrollbar(master, orient="vertical", command=canvas.yview) - scroll_y.grid(row=0, column=2, sticky="ns") - - frame = tk.Frame(canvas, padx=padx, pady=pady) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=3) - - values = {} - for index, key in enumerate(sorted(config)): - option = config[key] - label = tk.Label(frame, text=option.label) - label.grid(row=index, pady=pady, padx=padx, sticky="ew") - value = tk.StringVar() - config_type = ConfigType(option.type) - if config_type == ConfigType.BOOL: - select = tuple(option.select) - combobox = ttk.Combobox(frame, textvariable=value, values=select) - combobox.grid(row=index, column=1, sticky="ew", pady=pady) - if option.value == "1": - value.set("On") - else: - value.set("Off") - elif config_type == ConfigType.STRING: - value.set(option.value) - entry = tk.Entry(frame, textvariable=value) - entry.grid(row=index, column=1, sticky="ew", pady=pady) - elif config_type == ConfigType.EMANECONFIG: - value.set(option.value) - entry = tk.Entry(frame, textvariable=value, bg="white") - entry.grid(row=index, column=1, sticky="ew", pady=pady) - else: - logging.error("unhandled config option type: %s", config_type) - values[key] = value - - frame_id = canvas.create_window(0, 0, anchor="nw", window=frame) - canvas.update_idletasks() - canvas.configure(scrollregion=canvas.bbox("all"), yscrollcommand=scroll_y.set) - - frame.bind( - "", lambda event: canvas.configure(scrollregion=canvas.bbox("all")) - ) - canvas.bind( - "", lambda event: canvas.itemconfig(frame_id, width=event.width) - ) - return values - - -def parse_config(options, values): - """ - Given a set of configurations, parse out values and transform them when needed. - - :param dict options: option key mapping to configuration options - :param dict values: option key mapping to widget values - :return: nothing - """ - for key in options: - option = options[key] - value = values[key] - config_type = ConfigType(option.type) - config_value = value.get() - if config_type == ConfigType.BOOL: - if config_value == "On": - option.value = "1" - else: - option.value = "0" - else: - option.value = config_value diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index b3bca2fd..d90a970c 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -7,9 +7,9 @@ import tkinter as tk import webbrowser from tkinter import ttk -from coretk import configutils from coretk.dialogs.dialog import Dialog from coretk.images import ImageEnum, Images +from coretk.widgets import ConfigFrame PAD_X = 2 PAD_Y = 2 @@ -38,10 +38,10 @@ class EmaneConfiguration(Dialog): self.emane_options() self.draw_apply_and_cancel() - self.values = None + self.emane_config_frame = None self.options = app.core.emane_config self.model_options = None - self.model_values = None + self.model_config_frame = None def create_text_variable(self, val): """ @@ -75,7 +75,7 @@ class EmaneConfiguration(Dialog): f.grid(row=0, column=0, sticky="nsew") def save_emane_option(self): - configutils.parse_config(self.options, self.values) + self.emane_config_frame.parse_config() self.emane_dialog.destroy() def draw_emane_options(self): @@ -83,10 +83,6 @@ class EmaneConfiguration(Dialog): self.emane_dialog = Dialog( self, self.app, "emane configuration", modal=False ) - b1 = tk.Button(self.emane_dialog, text="Appy", command=self.save_emane_option) - b2 = tk.Button( - self.emane_dialog, text="Cancel", command=self.emane_dialog.destroy - ) if self.options is None: session_id = self.app.core.session_id @@ -94,11 +90,20 @@ class EmaneConfiguration(Dialog): logging.info("emane config: %s", response) self.options = response.config - self.values = configutils.create_config( - self.emane_dialog, self.options, PAD_X, PAD_Y - ) - b1.grid(row=1, column=0) - b2.grid(row=1, column=1) + self.emane_dialog.columnconfigure(0, weight=1) + self.emane_dialog.rowconfigure(0, weight=1) + self.emane_config_frame = ConfigFrame(self.emane_dialog, config=self.options) + self.emane_config_frame.draw_config() + self.emane_config_frame.grid(sticky="nsew") + + frame = tk.Frame(self.emane_dialog) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + b1 = tk.Button(frame, text="Appy", command=self.save_emane_option) + b1.grid(row=0, column=0, sticky="ew") + b2 = tk.Button(frame, text="Cancel", command=self.emane_dialog.destroy) + b2.grid(row=0, column=1, sticky="ew") self.emane_dialog.show() def save_emane_model_options(self): @@ -111,8 +116,7 @@ class EmaneConfiguration(Dialog): model_name = self.emane_models[self.emane_model_combobox.current()] # parse configuration - configutils.parse_config(self.model_options, self.model_values) - config = {x: self.model_options[x].value for x in self.model_options} + config = self.model_config_frame.parse_config() # add string emane_ infront for grpc call response = self.app.core.client.set_emane_model_config( @@ -141,17 +145,10 @@ class EmaneConfiguration(Dialog): # create the dialog and the necessry widget if not self.emane_model_dialog or not self.emane_model_dialog.winfo_exists(): self.emane_model_dialog = Dialog( - self, self.app, model_name + " configuration", modal=False + self, self.app, f"{model_name} configuration", modal=False ) - - b1 = tk.Button( - self.emane_model_dialog, text="Apply", command=self.save_emane_model_options - ) - b2 = tk.Button( - self.emane_model_dialog, - text="Cancel", - command=self.emane_model_dialog.destroy, - ) + self.emane_model_dialog.columnconfigure(0, weight=1) + self.emane_model_dialog.rowconfigure(0, weight=1) # query for configurations session_id = self.app.core.session_id @@ -162,12 +159,20 @@ class EmaneConfiguration(Dialog): logging.info("emane model config %s", response) self.model_options = response.config - self.model_values = configutils.create_config( - self.emane_model_dialog, self.model_options, PAD_X, PAD_Y + self.model_config_frame = ConfigFrame( + self.emane_model_dialog, config=self.model_options ) + self.model_config_frame.grid(sticky="nsew") + self.model_config_frame.draw_config() - b1.grid(row=1, column=0, sticky="nsew") - b2.grid(row=1, column=1, sticky="nsew") + frame = tk.Frame(self.emane_model_dialog) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + b1 = tk.Button(frame, text="Apply", command=self.save_emane_model_options) + b1.grid(row=0, column=0, sticky="ew") + b2 = tk.Button(frame, text="Cancel", command=self.emane_model_dialog.destroy) + b2.grid(row=0, column=1, sticky="ew") self.emane_model_dialog.show() def draw_option_buttons(self, parent): diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index cb377dcc..9625bc67 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -3,7 +3,18 @@ import tkinter as tk from functools import partial from tkinter import ttk -from coretk.configutils import ConfigType +from core.api.grpc import core_pb2 + +INT_TYPES = { + core_pb2.ConfigOptionType.UINT8, + core_pb2.ConfigOptionType.UINT16, + core_pb2.ConfigOptionType.UINT32, + core_pb2.ConfigOptionType.UINT64, + core_pb2.ConfigOptionType.INT8, + core_pb2.ConfigOptionType.INT16, + core_pb2.ConfigOptionType.INT32, + core_pb2.ConfigOptionType.INT64, +} class FrameScroll(tk.LabelFrame): @@ -30,11 +41,8 @@ class FrameScroll(tk.LabelFrame): def _configure_frame(self, event): req_width = self.frame.winfo_reqwidth() - req_height = self.frame.winfo_reqheight() if req_width != self.canvas.winfo_reqwidth(): self.canvas.configure(width=req_width) - if req_height != self.canvas.winfo_reqheight(): - self.canvas.configure(height=req_height) self.canvas.configure(scrollregion=self.canvas.bbox("all")) def _configure_canvas(self, event): @@ -68,34 +76,45 @@ class ConfigFrame(FrameScroll): label = tk.Label(self.frame, text=option.label) label.grid(row=index, pady=pady, padx=padx, sticky="w") value = tk.StringVar() - config_type = ConfigType(option.type) - if config_type == ConfigType.BOOL: + if option.type == core_pb2.ConfigOptionType.BOOL: select = tuple(option.select) - combobox = ttk.Combobox(self.frame, textvariable=value, values=select) + combobox = ttk.Combobox( + self.frame, textvariable=value, values=select, state="readonly" + ) combobox.grid(row=index, column=1, sticky="ew", pady=pady) if option.value == "1": value.set("On") else: value.set("Off") - elif config_type == ConfigType.STRING: + elif option.select: + value.set(option.value) + select = tuple(option.select) + combobox = ttk.Combobox( + self.frame, textvariable=value, values=select, state="readonly" + ) + combobox.grid(row=index, column=1, sticky="ew", pady=pady) + elif option.type == core_pb2.ConfigOptionType.STRING: value.set(option.value) entry = tk.Entry(self.frame, textvariable=value) entry.grid(row=index, column=1, sticky="ew", pady=pady) - elif config_type == ConfigType.EMANECONFIG: + elif option.type in INT_TYPES: + value.set(option.value) + entry = tk.Entry(self.frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pady) + elif option.type == core_pb2.ConfigOptionType.FLOAT: value.set(option.value) entry = tk.Entry(self.frame, textvariable=value) entry.grid(row=index, column=1, sticky="ew", pady=pady) else: - logging.error("unhandled config option type: %s", config_type) + logging.error("unhandled config option type: %s", option.type) self.values[key] = value def parse_config(self): for key in self.config: option = self.config[key] value = self.values[key] - config_type = ConfigType(option.type) config_value = value.get() - if config_type == ConfigType.BOOL: + if option.type == core_pb2.ConfigOptionType.BOOL: if config_value == "On": option.value = "1" else: From 5d6d22c6eb14e857495dd5cc4214365b9f727fab Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 6 Nov 2019 22:41:05 -0800 Subject: [PATCH 187/462] updated config frame widget to draw tabs for each config group --- coretk/coretk/widgets.py | 95 ++++++++++++++------------- daemon/proto/core/api/grpc/core.proto | 17 +++++ 2 files changed, 65 insertions(+), 47 deletions(-) diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 9625bc67..236dbc24 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -18,7 +18,7 @@ INT_TYPES = { class FrameScroll(tk.LabelFrame): - def __init__(self, master=None, cnf={}, **kw): + def __init__(self, master=None, cnf={}, _cls=tk.Frame, **kw): super().__init__(master, cnf, **kw) self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) @@ -30,7 +30,7 @@ class FrameScroll(tk.LabelFrame): self, orient="vertical", command=self.canvas.yview ) self.scrollbar.grid(row=0, column=1, sticky="ns") - self.frame = tk.Frame(self.canvas, padx=2, pady=2) + self.frame = _cls(self.canvas) self.frame_id = self.canvas.create_window(0, 0, anchor="nw", window=self.frame) self.canvas.update_idletasks() self.canvas.configure( @@ -48,12 +48,6 @@ class FrameScroll(tk.LabelFrame): def _configure_canvas(self, event): self.canvas.itemconfig(self.frame_id, width=event.width) - def update_canvas(self): - self.canvas.update_idletasks() - self.canvas.configure( - scrollregion=self.canvas.bbox("all"), yscrollcommand=self.scrollbar.set - ) - def clear(self): for widget in self.frame.winfo_children(): widget.destroy() @@ -61,53 +55,60 @@ class FrameScroll(tk.LabelFrame): class ConfigFrame(FrameScroll): def __init__(self, master=None, cnf={}, config=None, **kw): - super().__init__(master, cnf, **kw) - self.frame.columnconfigure(1, weight=1) - if not config: - config = {} + super().__init__(master, cnf, ttk.Notebook, **kw) self.config = config self.values = {} def draw_config(self): padx = 2 pady = 2 - for index, key in enumerate(sorted(self.config)): + group_mapping = {} + for key in self.config: option = self.config[key] - label = tk.Label(self.frame, text=option.label) - label.grid(row=index, pady=pady, padx=padx, sticky="w") - value = tk.StringVar() - if option.type == core_pb2.ConfigOptionType.BOOL: - select = tuple(option.select) - combobox = ttk.Combobox( - self.frame, textvariable=value, values=select, state="readonly" - ) - combobox.grid(row=index, column=1, sticky="ew", pady=pady) - if option.value == "1": - value.set("On") + group = group_mapping.setdefault(option.group, []) + group.append(option) + + for group_name in sorted(group_mapping): + group = group_mapping[group_name] + frame = tk.Frame(self.frame) + frame.columnconfigure(1, weight=1) + self.frame.add(frame, text=group_name) + for index, option in enumerate(sorted(group, key=lambda x: x.name)): + label = tk.Label(frame, text=option.label) + label.grid(row=index, pady=pady, padx=padx, sticky="w") + value = tk.StringVar() + if option.type == core_pb2.ConfigOptionType.BOOL: + select = tuple(option.select) + combobox = ttk.Combobox( + frame, textvariable=value, values=select, state="readonly" + ) + combobox.grid(row=index, column=1, sticky="ew", pady=pady) + if option.value == "1": + value.set("On") + else: + value.set("Off") + elif option.select: + value.set(option.value) + select = tuple(option.select) + combobox = ttk.Combobox( + frame, textvariable=value, values=select, state="readonly" + ) + combobox.grid(row=index, column=1, sticky="ew", pady=pady) + elif option.type == core_pb2.ConfigOptionType.STRING: + value.set(option.value) + entry = tk.Entry(frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pady) + elif option.type in INT_TYPES: + value.set(option.value) + entry = tk.Entry(frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pady) + elif option.type == core_pb2.ConfigOptionType.FLOAT: + value.set(option.value) + entry = tk.Entry(frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pady) else: - value.set("Off") - elif option.select: - value.set(option.value) - select = tuple(option.select) - combobox = ttk.Combobox( - self.frame, textvariable=value, values=select, state="readonly" - ) - combobox.grid(row=index, column=1, sticky="ew", pady=pady) - elif option.type == core_pb2.ConfigOptionType.STRING: - value.set(option.value) - entry = tk.Entry(self.frame, textvariable=value) - entry.grid(row=index, column=1, sticky="ew", pady=pady) - elif option.type in INT_TYPES: - value.set(option.value) - entry = tk.Entry(self.frame, textvariable=value) - entry.grid(row=index, column=1, sticky="ew", pady=pady) - elif option.type == core_pb2.ConfigOptionType.FLOAT: - value.set(option.value) - entry = tk.Entry(self.frame, textvariable=value) - entry.grid(row=index, column=1, sticky="ew", pady=pady) - else: - logging.error("unhandled config option type: %s", option.type) - self.values[key] = value + logging.error("unhandled config option type: %s", option.type) + self.values[option.name] = value def parse_config(self): for key in self.config: diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 19a1ff97..a53a11bb 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -768,6 +768,23 @@ message NodeType { } } +message ConfigOptionType { + enum Enum { + NONE = 0; + UINT8 = 1; + UINT16 = 2; + UINT32 = 3; + UINT64 = 4; + INT8 = 5; + INT16 = 6; + INT32 = 7; + INT64 = 8; + FLOAT = 9; + STRING = 10; + BOOL = 11; + } +} + message ServiceValidationMode { enum Enum { BLOCKING = 0; From 1cf484783567e387a83d11f667ab16a4d7fa5ba0 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 6 Nov 2019 23:16:49 -0800 Subject: [PATCH 188/462] removed coreclient set session state --- coretk/coretk/coreclient.py | 46 +++---------------------------------- 1 file changed, 3 insertions(+), 43 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 903d81c0..3fc67254 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -214,7 +214,9 @@ class CoreClient: s = self.client.get_session(sid).session # delete links and nodes from running session if s.state == core_pb2.SessionState.RUNTIME: - self.set_session_state("datacollect", sid) + self.client.set_session_state( + self.session_id, core_pb2.SessionState.DATACOLLECT + ) self.delete_links(sid) self.delete_nodes(sid) self.delete_session(sid) @@ -248,48 +250,6 @@ class CoreClient: # logging.info("get session: %s", response) return response.session.state - def set_session_state(self, state, custom_session_id=None): - """ - Set session state - - :param str state: session state to set - :return: nothing - """ - if custom_session_id is None: - sid = self.session_id - else: - sid = custom_session_id - - response = None - if state == "configuration": - response = self.client.set_session_state( - sid, core_pb2.SessionState.CONFIGURATION - ) - elif state == "instantiation": - response = self.client.set_session_state( - sid, core_pb2.SessionState.INSTANTIATION - ) - elif state == "datacollect": - response = self.client.set_session_state( - sid, core_pb2.SessionState.DATACOLLECT - ) - elif state == "shutdown": - response = self.client.set_session_state( - sid, core_pb2.SessionState.SHUTDOWN - ) - elif state == "runtime": - response = self.client.set_session_state(sid, core_pb2.SessionState.RUNTIME) - elif state == "definition": - response = self.client.set_session_state( - sid, core_pb2.SessionState.DEFINITION - ) - elif state == "none": - response = self.client.set_session_state(sid, core_pb2.SessionState.NONE) - else: - logging.error("coregrpc.py: set_session_state: INVALID STATE") - - logging.info("set session state: %s", response) - def edit_node(self, node_id, x, y): position = core_pb2.Position(x=x, y=y) response = self.client.edit_node(self.session_id, node_id, position) From 613568ca28a517c5e66ed748e421dff7493f1df5 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 6 Nov 2019 23:58:02 -0800 Subject: [PATCH 189/462] updates to get custom nodes dialog to a working state --- coretk/coretk/coreclient.py | 8 +++ coretk/coretk/dialogs/customnodes.py | 85 +++++++++++++++++++++------- 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 3fc67254..b5114462 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -64,6 +64,13 @@ class CoreServer: self.port = port +class CustomNode: + def __init__(self, name, image, services): + self.name = name + self.image = image + self.services = services + + class CoreClient: def __init__(self, app): """ @@ -76,6 +83,7 @@ class CoreClient: self.master = app.master self.interface_helper = None self.services = {} + self.custom_nodes = {} # distributed server data self.servers = {} diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 38d59c83..04931034 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -1,17 +1,18 @@ import tkinter as tk +from coretk.coreclient import CustomNode from coretk.dialogs.dialog import Dialog from coretk.dialogs.nodeicon import IconDialog from coretk.widgets import CheckboxList, ListboxScroll class ServicesSelectDialog(Dialog): - def __init__(self, master, app): + def __init__(self, master, app, current_services): super().__init__(master, app, "Node Services", modal=True) self.groups = None self.services = None self.current = None - self.current_services = set() + self.current_services = current_services self.draw() def draw(self): @@ -37,14 +38,16 @@ class ServicesSelectDialog(Dialog): self.current = ListboxScroll(frame, text="Selected") self.current.grid(row=0, column=2, sticky="nsew") + for service in sorted(self.current_services): + self.current.listbox.insert(tk.END, service) frame = tk.Frame(self) frame.grid(stick="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save") + button = tk.Button(frame, text="Save", command=self.click_cancel) button.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = tk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=1, sticky="ew") # trigger group change @@ -69,15 +72,23 @@ class ServicesSelectDialog(Dialog): for name in sorted(self.current_services): self.current.listbox.insert(tk.END, name) + def click_cancel(self): + self.current_services = None + self.destroy() + class CustomNodesDialog(Dialog): def __init__(self, master, app): super().__init__(master, app, "Custom Nodes", modal=True) - self.save_button = None + self.edit_button = None self.delete_button = None + self.nodes_list = None self.name = tk.StringVar() self.image_button = None self.image = None + self.services = set() + self.selected = None + self.selected_index = None self.draw() def draw(self): @@ -93,13 +104,11 @@ class CustomNodesDialog(Dialog): frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) - scrollbar.grid(row=0, column=1, sticky="ns") - - listbox = tk.Listbox(frame, selectmode=tk.SINGLE, yscrollcommand=scrollbar.set) - listbox.grid(row=0, column=0, sticky="nsew") - - scrollbar.config(command=listbox.yview) + self.nodes_list = ListboxScroll(frame) + self.nodes_list.grid(row=0, column=0, sticky="nsew") + self.nodes_list.listbox.bind("<>", self.handle_node_select) + for name in sorted(self.app.core.custom_nodes): + self.nodes_list.listbox.insert(tk.END, name) frame = tk.Frame(frame) frame.grid(row=0, column=2, sticky="nsew") @@ -120,10 +129,10 @@ class CustomNodesDialog(Dialog): button = tk.Button(frame, text="Create", command=self.click_create) button.grid(row=0, column=0, sticky="ew") - self.save_button = tk.Button( - frame, text="Save", state=tk.DISABLED, command=self.click_save + self.edit_button = tk.Button( + frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) - self.save_button.grid(row=0, column=1, sticky="ew") + self.edit_button.grid(row=0, column=1, sticky="ew") self.delete_button = tk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete @@ -136,7 +145,7 @@ class CustomNodesDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save", command=self.click_save) + button = tk.Button(frame, text="Save", command=self.click_edit) button.grid(row=0, column=0, sticky="ew") button = tk.Button(frame, text="Cancel", command=self.destroy) @@ -150,14 +159,50 @@ class CustomNodesDialog(Dialog): self.image_button.config(image=self.image) def click_services(self): - dialog = ServicesSelectDialog(self, self.app) + dialog = ServicesSelectDialog(self, self.app, self.services) dialog.show() + if dialog.current_services is not None: + self.services = dialog.current_services def click_create(self): - pass + name = self.name.get() + if name not in self.app.core.custom_nodes: + custom_node = CustomNode(name, self.image, self.services) + self.app.core.custom_nodes[name] = custom_node + self.nodes_list.listbox.insert(tk.END, name) + self.reset_values() - def click_save(self): + def reset_values(self): + self.name.set("") + self.image = None + self.services = set() + self.image_button.config(image="") + + def click_edit(self): pass def click_delete(self): - pass + if self.selected and self.selected in self.app.core.custom_nodes: + self.nodes_list.listbox.delete(self.selected_index) + del self.app.core.custom_nodes[self.selected] + self.reset_values() + self.nodes_list.listbox.selection_clear(0, tk.END) + self.nodes_list.listbox.event_generate("<>") + + def handle_node_select(self, event): + selection = self.nodes_list.listbox.curselection() + if selection: + self.selected_index = selection[0] + self.selected = self.nodes_list.listbox.get(self.selected_index) + custom_node = self.app.core.custom_nodes[self.selected] + self.name.set(custom_node.name) + self.services = custom_node.services + self.image = custom_node.image + self.image_button.config(image=self.image) + self.edit_button.config(state=tk.NORMAL) + self.delete_button.config(state=tk.NORMAL) + else: + self.selected = None + self.selected_index = None + self.edit_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) From a789498f5c13f6cf7d557975a95dcdba3e199156 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 7 Nov 2019 08:30:49 -0800 Subject: [PATCH 190/462] updates --- daemon/data/logging.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/data/logging.conf b/daemon/data/logging.conf index 46de6e92..7f3d496f 100644 --- a/daemon/data/logging.conf +++ b/daemon/data/logging.conf @@ -14,7 +14,7 @@ } }, "root": { - "level": "DEBUG", + "level": "INFO", "handlers": ["console"] } } From 5db05aad1301d38fd9524c8f0d949c50f0fc1020 Mon Sep 17 00:00:00 2001 From: Jeff Ahrenholz Date: Thu, 7 Nov 2019 08:57:46 -0800 Subject: [PATCH 191/462] fix black pre-commit errors (formatting) --- daemon/core/nodes/network.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 6d88d453..45f37c57 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -202,8 +202,12 @@ class EbtablesQueue: self.cmds.append(f"-F {wlan.brname}") else: wlan.has_ebtables_chain = True - self.cmds.extend([f"-N {wlan.brname} -P {wlan.policy}", - f"-A FORWARD --logical-in {wlan.brname} -j {wlan.brname}"]) + self.cmds.extend( + [ + f"-N {wlan.brname} -P {wlan.policy}", + f"-A FORWARD --logical-in {wlan.brname} -j {wlan.brname}", + ] + ) # rebuild the chain for netif1, v in wlan._linked.items(): for netif2, linked in v.items(): From ddcce82af4b5023651b9ac9736f56cf6e4f5a99f Mon Sep 17 00:00:00 2001 From: Jeff Ahrenholz Date: Thu, 7 Nov 2019 09:01:01 -0800 Subject: [PATCH 192/462] address PR comments and fix pre-commit --- daemon/core/nodes/netclient.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 7a9cc2d8..4a7250f0 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -126,7 +126,10 @@ class LinuxNetClient: :param str device: device to flush :return: nothing """ - self.run(f"[ -e /sys/class/net/{device} ] && {IP_BIN} -6 address flush dev {device} || echo") + self.run( + f"[ -e /sys/class/net/{device} ] && {IP_BIN} -6 address flush dev {device} || true", + shell=True, + ) def device_mac(self, device, mac): """ From 2873c32c23148c03c3cda2714f616a6be7c13e2a Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 7 Nov 2019 11:33:40 -0800 Subject: [PATCH 193/462] custom nodes dialog works for creating, editing, and saving to config in basic case --- coretk/coretk/appdirs.py | 5 +- coretk/coretk/coreclient.py | 31 ++++++++--- coretk/coretk/dialogs/customnodes.py | 55 +++++++++++++++---- .../dialogs/{nodeicon.py => icondialog.py} | 0 coretk/coretk/dialogs/nodeconfig.py | 2 +- coretk/coretk/dialogs/wlanconfig.py | 2 +- coretk/coretk/images.py | 4 ++ 7 files changed, 78 insertions(+), 21 deletions(-) rename coretk/coretk/dialogs/{nodeicon.py => icondialog.py} (100%) diff --git a/coretk/coretk/appdirs.py b/coretk/coretk/appdirs.py index 51a78f76..553b0949 100644 --- a/coretk/coretk/appdirs.py +++ b/coretk/coretk/appdirs.py @@ -42,7 +42,10 @@ def check_directory(): for background in LOCAL_BACKGROUND_PATH.glob("*"): new_background = BACKGROUNDS_PATH.joinpath(background.name) shutil.copy(background, new_background) - config = {"servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}]} + config = { + "servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}], + "nodes": [], + } save_config(config) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index b5114462..f4135269 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -8,6 +8,7 @@ from collections import OrderedDict from core.api.grpc import client, core_pb2 from coretk.coretocanvas import CoreToCanvasMapping from coretk.dialogs.sessions import SessionsDialog +from coretk.images import Images from coretk.interface import Interface, InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.wlannodeconfig import WlanNodeConfig @@ -65,9 +66,10 @@ class CoreServer: class CustomNode: - def __init__(self, name, image, services): + def __init__(self, name, image, image_file, services): self.name = name self.image = image + self.image_file = image_file self.services = services @@ -83,15 +85,11 @@ class CoreClient: self.master = app.master self.interface_helper = None self.services = {} - self.custom_nodes = {} - # distributed server data + # loaded configuration data self.servers = {} - for server_config in self.app.config["servers"]: - server = CoreServer( - server_config["name"], server_config["address"], server_config["port"] - ) - self.servers[server.name] = server + self.custom_nodes = {} + self.read_config() # data for managing the current session self.nodes = {} @@ -106,6 +104,23 @@ class CoreClient: self.mobilityconfig_management = MobilityNodeConfig() self.emane_config = None + def read_config(self): + # read distributed server + for server_config in self.app.config["servers"]: + server = CoreServer( + server_config["name"], server_config["address"], server_config["port"] + ) + self.servers[server.name] = server + + # read custom nodes + for node in self.app.config["nodes"]: + image_file = node["image"] + image = Images.get_custom(image_file) + custom_node = CustomNode( + node["name"], image, image_file, set(node["services"]) + ) + self.custom_nodes[custom_node.name] = custom_node + def handle_events(self, event): logging.info("event: %s", event) if event.link_event is not None: diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 04931034..f2423bf0 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -1,8 +1,11 @@ +import logging import tkinter as tk +from pathlib import Path +from coretk import appdirs from coretk.coreclient import CustomNode from coretk.dialogs.dialog import Dialog -from coretk.dialogs.nodeicon import IconDialog +from coretk.dialogs.icondialog import IconDialog from coretk.widgets import CheckboxList, ListboxScroll @@ -86,6 +89,7 @@ class CustomNodesDialog(Dialog): self.name = tk.StringVar() self.image_button = None self.image = None + self.image_file = None self.services = set() self.selected = None self.selected_index = None @@ -145,17 +149,25 @@ class CustomNodesDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save", command=self.click_edit) + button = tk.Button(frame, text="Save", command=self.click_save) button.grid(row=0, column=0, sticky="ew") button = tk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") + def reset_values(self): + self.name.set("") + self.image = None + self.image_file = None + self.services = set() + self.image_button.config(image="") + def click_icon(self): dialog = IconDialog(self, self.app, self.name.get(), self.image) dialog.show() if dialog.image: self.image = dialog.image + self.image_file = dialog.file_path.get() self.image_button.config(image=self.image) def click_services(self): @@ -164,22 +176,44 @@ class CustomNodesDialog(Dialog): if dialog.current_services is not None: self.services = dialog.current_services + def click_save(self): + self.app.config["nodes"].clear() + for name in sorted(self.app.core.custom_nodes): + custom_node = self.app.core.custom_nodes[name] + self.app.config["nodes"].append( + { + "name": custom_node.name, + "image": custom_node.image_file, + "services": list(custom_node.services), + } + ) + logging.info("saving custom nodes: %s", self.app.config["nodes"]) + appdirs.save_config(self.app.config) + def click_create(self): name = self.name.get() if name not in self.app.core.custom_nodes: - custom_node = CustomNode(name, self.image, self.services) + custom_node = CustomNode( + name, self.image, Path(self.image_file).name, set(self.services) + ) self.app.core.custom_nodes[name] = custom_node self.nodes_list.listbox.insert(tk.END, name) self.reset_values() - def reset_values(self): - self.name.set("") - self.image = None - self.services = set() - self.image_button.config(image="") - def click_edit(self): - pass + name = self.name.get() + if self.selected: + previous_name = self.selected + self.selected = name + custom_node = self.app.core.custom_nodes.pop(previous_name) + custom_node.name = name + custom_node.image = self.image + custom_node.image_file = Path(self.image_file).name + custom_node.services = self.services + self.app.core.custom_nodes[name] = custom_node + self.nodes_list.listbox.delete(self.selected_index) + self.nodes_list.listbox.insert(self.selected_index, name) + self.nodes_list.listbox.selection_set(self.selected_index) def click_delete(self): if self.selected and self.selected in self.app.core.custom_nodes: @@ -198,6 +232,7 @@ class CustomNodesDialog(Dialog): self.name.set(custom_node.name) self.services = custom_node.services self.image = custom_node.image + self.image_file = custom_node.image_file self.image_button.config(image=self.image) self.edit_button.config(state=tk.NORMAL) self.delete_button.config(state=tk.NORMAL) diff --git a/coretk/coretk/dialogs/nodeicon.py b/coretk/coretk/dialogs/icondialog.py similarity index 100% rename from coretk/coretk/dialogs/nodeicon.py rename to coretk/coretk/dialogs/icondialog.py diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index bc03dc51..3f13488a 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -2,7 +2,7 @@ import tkinter as tk from tkinter import ttk from coretk.dialogs.dialog import Dialog -from coretk.dialogs.nodeicon import IconDialog +from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeServicesDialog NETWORKNODETYPES = ["switch", "hub", "wlan", "rj45", "tunnel"] diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index d57c8935..dc40e6c7 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -5,7 +5,7 @@ wlan configuration import tkinter as tk from coretk.dialogs.dialog import Dialog -from coretk.dialogs.nodeicon import IconDialog +from coretk.dialogs.icondialog import IconDialog class WlanConfigDialog(Dialog): diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 768b33ba..f25b0eb1 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -29,6 +29,10 @@ class Images: def get(cls, image): return cls.images[image.value] + @classmethod + def get_custom(cls, name): + return cls.images[name] + @classmethod def convert_type_and_model_to_image(cls, node_type, node_model): """ From 2d9cf81d0b980cf5788e6adeecff20f8a1b82234 Mon Sep 17 00:00:00 2001 From: Jeff Ahrenholz Date: Thu, 7 Nov 2019 11:38:31 -0800 Subject: [PATCH 194/462] remove shell=True from run command --- daemon/core/nodes/netclient.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 4a7250f0..4d568a64 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -127,8 +127,7 @@ class LinuxNetClient: :return: nothing """ self.run( - f"[ -e /sys/class/net/{device} ] && {IP_BIN} -6 address flush dev {device} || true", - shell=True, + f"[ -e /sys/class/net/{device} ] && {IP_BIN} -6 address flush dev {device} || true" ) def device_mac(self, device, mac): From dcfd7f879535a331a795c58c7b8fc76ea5c8eded Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 7 Nov 2019 13:23:02 -0800 Subject: [PATCH 195/462] working on delete node --- coretk/coretk/coreclient.py | 49 ++++++++++++++++------- coretk/coretk/coretoolbarhelp.py | 3 ++ coretk/coretk/graph.py | 36 ++++++++++++++++- coretk/coretk/mobilitynodeconfig.py | 4 ++ coretk/coretk/nodedelete.py | 61 +++++++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 15 deletions(-) create mode 100644 coretk/coretk/nodedelete.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 2f6e0b88..ae03922b 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -500,6 +500,41 @@ class CoreClient: name, ) + def delete_wanted_graph_nodes(self, canvas_ids, tokens): + """ + remove the nodes selected by the user and anything related to that node + such as link, configurations, interfaces + + :param list(int) canvas_ids: list of canvas node ids + :return: nothing + """ + # keep reference to the core ids + core_node_ids = [self.nodes[x].node_id for x in canvas_ids] + + # delete the nodes + for i in canvas_ids: + try: + self.nodes.pop(i) + self.reusable.append(i) + except KeyError: + logging.error("coreclient.py INVALID NODE CANVAS ID") + + self.reusable.sort() + + # delete the edges and interfaces + for i in tokens: + try: + self.edges.pop(i) + except KeyError: + logging.error("coreclient.py invalid edge token ") + + # delete any configurations + for i in core_node_ids: + if i in self.mobilityconfig_management.configurations: + self.mobilityconfig_management.pop(i) + if i in self.wlanconfig_management.configurations: + self.wlanconfig_management.pop(i) + def add_preexisting_node(self, canvas_node, session_id, core_node, name): """ Add preexisting nodes to grpc manager @@ -553,20 +588,6 @@ class CoreClient: self.preexisting.clear() logging.debug("Next id: %s, Reusable: %s", self.id, self.reusable) - def delete_node(self, canvas_id): - """ - Delete a node from the session - - :param int canvas_id: node's id in the canvas - :return: thing - """ - try: - self.nodes.pop(canvas_id) - self.reusable.append(canvas_id) - self.reusable.sort() - except KeyError: - logging.error("grpcmanagement.py INVALID NODE CANVAS ID") - def create_interface(self, node_type, gui_interface): """ create a protobuf interface given the interface object stored by the programmer diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index c650a5bc..5192f2c5 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -110,3 +110,6 @@ class CoreToolbarHelp: emane_config=emane_config, emane_model_configs=emane_model_configs, ) + + response = self.app.core.client.get_session(self.app.core.session_id) + print(response) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 448a6f1d..9ab6b956 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -8,6 +8,7 @@ from coretk.graph_helper import GraphHelper, WlanAntennaManager from coretk.images import Images from coretk.interface import Interface from coretk.linkinfo import LinkInfo, Throughput +from coretk.nodedelete import CanvasComponentManagement from coretk.wirelessconnection import WirelessConnection @@ -41,7 +42,11 @@ class CanvasGraph(tk.Canvas): self.drawing_edge = None self.grid = None self.meters_per_pixel = 1.5 + + self.canvas_management = CanvasComponentManagement(self, core) + self.canvas_action = CanvasAction(master, self) + self.setup_menus() self.setup_bindings() self.draw_grid() @@ -101,6 +106,7 @@ class CanvasGraph(tk.Canvas): self.bind("", self.click_release) self.bind("", self.click_motion) self.bind("", self.context) + self.bind("", self.press_delete) def draw_grid(self, width=1000, height=800): """ @@ -377,6 +383,25 @@ class CanvasGraph(tk.Canvas): self.node_context.unpost() self.is_node_context_opened = False + def press_delete(self, event): + # hide nodes, links, link information that shows on the GUI + to_delete_nodes, to_delete_edge_tokens = ( + self.canvas_management.delete_selected_nodes() + ) + + # delete nodes and link info stored in CanvasGraph object + for nid in to_delete_nodes: + self.nodes.pop(nid) + for token in to_delete_edge_tokens: + self.edges.pop(token) + + self.core.delete_wanted_graph_nodes(to_delete_nodes, to_delete_edge_tokens) + # delete any configuration related to the links and nodes + + # delete selected node + + # delete links connected to the selected nodes + def add_node(self, x, y, image, node_name): plot_id = self.find_all()[0] if self.selected == plot_id: @@ -473,11 +498,15 @@ class CanvasNode: self.canvas.tag_bind(self.id, "", self.motion) self.canvas.tag_bind(self.id, "", self.context) self.canvas.tag_bind(self.id, "", self.double_click) + self.canvas.tag_bind(self.id, "", self.select_multiple) self.edges = set() self.wlans = [] self.moving = None + def click(self, event): + print("click") + def double_click(self, event): node_id = self.canvas.core.nodes[self.id].node_id state = self.canvas.core.get_session_state() @@ -500,7 +529,8 @@ class CanvasNode: def click_press(self, event): logging.debug(f"click press {self.name}: {event}") self.moving = self.canvas.canvas_xy(event) - # return "break" + + self.canvas.canvas_management.node_select(self) def click_release(self, event): logging.debug(f"click release {self.name}: {event}") @@ -520,6 +550,7 @@ class CanvasNode: self.canvas.move(self.id, offset_x, offset_y) self.canvas.move(self.text_id, offset_x, offset_y) self.antenna_draw.update_antennas_position(offset_x, offset_y) + self.canvas.canvas_management.node_drag(self, offset_x, offset_y) new_x, new_y = self.canvas.coords(self.id) @@ -539,5 +570,8 @@ class CanvasNode: old_x, old_y, new_x, new_y, self.wlans ) + def select_multiple(self, event): + self.canvas.canvas_management.node_select(self, True) + def context(self, event): logging.debug(f"context click {self.name}: {event}") diff --git a/coretk/coretk/mobilitynodeconfig.py b/coretk/coretk/mobilitynodeconfig.py index e79e58ac..4a94f573 100644 --- a/coretk/coretk/mobilitynodeconfig.py +++ b/coretk/coretk/mobilitynodeconfig.py @@ -10,6 +10,10 @@ from core.api.grpc import core_pb2 class MobilityNodeConfig: def __init__(self): + """ + create an instance of MobilityConfig object + """ + # dict that maps node id to mobility configuration self.configurations = {} def set_default_configuration(self, node_type, node_id): diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py new file mode 100644 index 00000000..6ce3a6f9 --- /dev/null +++ b/coretk/coretk/nodedelete.py @@ -0,0 +1,61 @@ +""" +manage deletion +""" + + +class CanvasComponentManagement: + def __init__(self, canvas, core): + self.app = core + self.canvas = canvas + + # dictionary that maps node to box + self.selected = {} + + def node_select(self, canvas_node, choose_multiple=False): + """ + create a bounding box when a node is selected + + :param coretk.graph.CanvasNode canvas_node: canvas node object + :return: nothing + """ + + if not choose_multiple: + self.delete_current_bbox() + + # draw a bounding box if node hasn't been selected yet + if canvas_node.id not in self.selected: + x0, y0, x1, y1 = self.canvas.bbox(canvas_node.id) + bbox_id = self.canvas.create_rectangle( + (x0 - 6, y0 - 6, x1 + 6, y1 + 6), activedash=True, dash="-" + ) + self.selected[canvas_node.id] = bbox_id + + def node_drag(self, canvas_node, offset_x, offset_y): + self.canvas.move(self.selected[canvas_node.id], offset_x, offset_y) + + def delete_current_bbox(self): + for bbid in self.selected.values(): + self.canvas.delete(bbid) + self.selected.clear() + + def delete_selected_nodes(self): + selected_nodes = list(self.selected.keys()) + edges = set() + for n in selected_nodes: + edges = edges.union(self.canvas.nodes[n].edges) + edge_canvas_ids = [x.id for x in edges] + edge_tokens = [x.token for x in edges] + link_infos = [x.link_info.id1 for x in edges] + [x.link_info.id2 for x in edges] + + for i in edge_canvas_ids: + self.canvas.itemconfig(i, state="hidden") + + for i in link_infos: + self.canvas.itemconfig(i, state="hidden") + + for cnid, bbid in self.selected.items(): + self.canvas.itemconfig(cnid, state="hidden") + self.canvas.itemconfig(bbid, state="hidden") + self.canvas.itemconfig(self.canvas.nodes[cnid].text_id, state="hidden") + self.selected.clear() + return selected_nodes, edge_tokens From 4970fb0d5534540d62255d2d03a0053421e285d2 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 7 Nov 2019 14:37:08 -0800 Subject: [PATCH 196/462] cleanup for app toolbar, updated toolbar to use grid layout --- coretk/coretk/app.py | 6 +- coretk/coretk/coreclient.py | 18 +- coretk/coretk/coretoolbar.py | 665 ++++++++++++----------------------- coretk/coretk/graph.py | 7 +- 4 files changed, 234 insertions(+), 462 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 9c9c0d2f..faaf1c20 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -50,10 +50,8 @@ class Application(tk.Frame): self.master.config(menu=self.menubar) def draw_toolbar(self): - edit_frame = tk.Frame(self) - edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) - self.core_editbar = CoreToolbar(self, edit_frame, self.menubar) - self.core_editbar.create_toolbar() + self.core_editbar = CoreToolbar(self, self) + self.core_editbar.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) def draw_canvas(self): self.canvas = CanvasGraph( diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index f4135269..5369b829 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -13,8 +13,8 @@ from coretk.interface import Interface, InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.wlannodeconfig import WlanNodeConfig -link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] -network_layer_nodes = ["router", "host", "PC", "mdr", "prouter", "OVS"] +link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel", "emane"] +network_layer_nodes = ["router", "host", "PC", "mdr", "prouter"] class Node: @@ -205,11 +205,9 @@ class CoreClient: # draw tool bar appropritate with session state if session_state == core_pb2.SessionState.RUNTIME: - self.app.core_editbar.destroy_children_widgets() - self.app.core_editbar.create_runtime_toolbar() + self.app.core_editbar.runtime_frame.tkraise() else: - self.app.core_editbar.destroy_children_widgets() - self.app.core_editbar.create_toolbar() + self.app.core_editbar.design_frame.tkraise() def create_new_session(self): """ @@ -447,26 +445,26 @@ class CoreClient: node_type = core_pb2.NodeType.WIRELESS_LAN elif name == "rj45": node_type = core_pb2.NodeType.RJ45 + elif name == "emane": + node_type = core_pb2.NodeType.EMANE elif name == "tunnel": node_type = core_pb2.NodeType.TUNNEL elif name in network_layer_nodes: node_type = core_pb2.NodeType.DEFAULT node_model = name else: - logging.error("grpcmanagemeny.py INVALID node name") + logging.error("invalid node name: %s", name) nid = self.get_id() create_node = Node(session_id, nid, node_type, node_model, x, y, name) # set default configuration for wireless node self.wlanconfig_management.set_default_config(node_type, nid) - self.mobilityconfig_management.set_default_configuration(node_type, nid) self.nodes[canvas_id] = create_node self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) - # self.core_id_to_canvas_id[nid] = canvas_id logging.debug( - "Adding node to GrpcManager.. Session id: %s, Coords: (%s, %s), Name: %s", + "Adding node to core.. session id: %s, coords: (%s, %s), name: %s", session_id, x, y, diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index aeccb54f..aaed37a9 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -1,5 +1,6 @@ import logging import tkinter as tk +from functools import partial from coretk.coretoolbarhelp import CoreToolbarHelp from coretk.dialogs.customnodes import CustomNodesDialog @@ -8,21 +9,20 @@ from coretk.images import ImageEnum, Images from coretk.tooltip import CreateToolTip -class CoreToolbar(object): +class CoreToolbar(tk.Frame): """ Core toolbar class """ - def __init__(self, app, edit_frame, menubar): + def __init__(self, master, app, cnf={}, **kwargs): """ Create a CoreToolbar instance :param tkinter.Frame edit_frame: edit frame """ + super().__init__(master, cnf, **kwargs) self.app = app self.master = app.master - self.edit_frame = edit_frame - self.menubar = menubar self.radio_value = tk.IntVar() self.exec_radio_value = tk.IntVar() @@ -30,44 +30,144 @@ class CoreToolbar(object): self.width = 32 self.height = 32 - self.selection_tool_button = None - # Reference to the option menus + self.selection_tool_button = None self.link_layer_option_menu = None self.marker_option_menu = None self.network_layer_option_menu = None - self.canvas = None + self.node_button = None + self.network_button = None + self.annotation_button = None - def destroy_previous_frame(self): - """ - Destroy any extra frame from previous before drawing a new one + # frames + self.design_frame = None + self.runtime_frame = None + self.node_picker = None + self.network_picker = None + self.annotation_picker = None - :return: nothing - """ - if ( - self.network_layer_option_menu - and self.network_layer_option_menu.winfo_exists() - ): - self.network_layer_option_menu.destroy() - if self.link_layer_option_menu and self.link_layer_option_menu.winfo_exists(): - self.link_layer_option_menu.destroy() - if self.marker_option_menu and self.marker_option_menu.winfo_exists(): - self.marker_option_menu.destroy() + # draw components + self.draw() - def destroy_children_widgets(self): - """ - Destroy all children of a parent widget + def draw(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.draw_design_frame() + self.draw_runtime_frame() + self.design_frame.tkraise() - :param tkinter.Frame parent: parent frame - :return: nothing - """ + def draw_design_frame(self): + self.design_frame = tk.Frame(self) + self.design_frame.grid(row=0, column=0, sticky="nsew") + self.design_frame.columnconfigure(0, weight=1) - for i in self.edit_frame.winfo_children(): - if i.winfo_name() != "!frame": - i.destroy() + self.create_regular_button( + self.design_frame, + Images.get(ImageEnum.START), + self.click_start_session_tool, + "start the session", + ) + self.create_radio_button( + self.design_frame, + Images.get(ImageEnum.SELECT), + self.click_selection_tool, + self.radio_value, + 1, + "selection tool", + ) + self.create_radio_button( + self.design_frame, + Images.get(ImageEnum.LINK), + self.click_link_tool, + self.radio_value, + 2, + "link tool", + ) + self.create_node_button() + self.create_link_layer_button() + self.create_marker_button() + self.radio_value.set(1) - def create_button(self, img, func, frame, main_button, btt_message): + def draw_runtime_frame(self): + self.runtime_frame = tk.Frame(self) + self.runtime_frame.grid(row=0, column=0, sticky="nsew") + self.runtime_frame.columnconfigure(0, weight=1) + + self.create_regular_button( + self.runtime_frame, + Images.get(ImageEnum.STOP), + self.click_stop_button, + "stop the session", + ) + self.create_radio_button( + self.runtime_frame, + Images.get(ImageEnum.SELECT), + self.click_selection_tool, + self.exec_radio_value, + 1, + "selection tool", + ) + self.create_observe_button() + self.create_radio_button( + self.runtime_frame, + Images.get(ImageEnum.PLOT), + self.click_plot_button, + self.exec_radio_value, + 2, + "plot", + ) + self.create_radio_button( + self.runtime_frame, + Images.get(ImageEnum.MARKER), + self.click_marker_button, + self.exec_radio_value, + 3, + "marker", + ) + self.create_radio_button( + self.runtime_frame, + Images.get(ImageEnum.TWONODE), + self.click_two_node_button, + self.exec_radio_value, + 4, + "run command from one node to another", + ) + self.create_regular_button( + self.runtime_frame, Images.get(ImageEnum.RUN), self.click_run_button, "run" + ) + self.exec_radio_value.set(1) + + def draw_node_picker(self): + self.hide_pickers() + self.node_picker = tk.Frame(self.master, padx=1, pady=1) + nodes = [ + (ImageEnum.ROUTER, "router"), + (ImageEnum.HOST, "host"), + (ImageEnum.PC, "PC"), + (ImageEnum.MDR, "mdr"), + (ImageEnum.PROUTER, "prouter"), + (ImageEnum.EDITNODE, "custom node types"), + ] + for image_enum, tooltip in nodes: + self.create_button( + Images.get(image_enum), + partial(self.update_button, self.node_button, image_enum, tooltip), + self.node_picker, + tooltip, + ) + self.show_picker(self.node_button, self.node_picker) + + def show_picker(self, button, picker): + first_button = self.winfo_children()[0] + x = button.winfo_rootx() - first_button.winfo_rootx() + 40 + y = button.winfo_rooty() - first_button.winfo_rooty() - 1 + picker.place(x=x, y=y) + self.app.bind_all("", lambda e: self.hide_pickers()) + self.wait_window(picker) + self.app.unbind_all("") + + def create_button(self, img, func, frame, tooltip): """ Create button and put it on the frame @@ -78,9 +178,9 @@ class CoreToolbar(object): :return: nothing """ button = tk.Button(frame, width=self.width, height=self.height, image=img) + button.bind("", lambda e: func()) button.pack(side=tk.LEFT, pady=1) - CreateToolTip(button, btt_message) - button.bind("", lambda mb: func(main_button)) + CreateToolTip(button, tooltip) def create_radio_button(self, frame, image, func, variable, value, tooltip_msg): button = tk.Radiobutton( @@ -93,326 +193,108 @@ class CoreToolbar(object): variable=variable, command=func, ) - button.pack(side=tk.TOP, pady=1) + button.grid() CreateToolTip(button, tooltip_msg) - def create_regular_button(self, frame, image, func, btt_message): + def create_regular_button(self, frame, image, func, tooltip): button = tk.Button( frame, width=self.width, height=self.height, image=image, command=func ) - button.pack(side=tk.TOP, pady=1) - CreateToolTip(button, btt_message) - - def draw_button_menu_frame(self, edit_frame, option_frame, main_button): - """ - Draw option menu frame right next to the main button - - :param tkinter.Frame edit_frame: parent frame of the main button - :param tkinter.Frame option_frame: option frame to draw - :param tkinter.Radiobutton main_button: the main button - :return: nothing - """ - - first_button = edit_frame.winfo_children()[0] - _x = main_button.winfo_rootx() - first_button.winfo_rootx() + 40 - _y = main_button.winfo_rooty() - first_button.winfo_rooty() - 1 - option_frame.place(x=_x, y=_y) - - def bind_widgets_before_frame_hide(self, frame): - """ - Bind the widgets to a left click, when any of the widgets is clicked, the menu option frame is destroyed before - any further action is performed - - :param tkinter.Frame frame: the frame to be destroyed - :return: nothing - """ - self.menubar.bind("", lambda e: frame.destroy()) - self.master.bind("", lambda e: frame.destroy()) - - def unbind_widgets_after_frame_hide(self): - """ - Unbind the widgets to make sure everything works normally again after the menu option frame is destroyed - - :return: nothing - """ - self.master.unbind("") - self.menubar.unbind("Button-1>") + button.grid() + CreateToolTip(button, tooltip) def click_selection_tool(self): - logging.debug("Click SELECTION TOOL") + logging.debug("clicked selection tool") self.canvas.mode = GraphMode.SELECT def click_start_session_tool(self): """ - Start session handler: redraw buttons, send node and link messages to grpc server + Start session handler redraw buttons, send node and link messages to grpc + server. :return: nothing """ - logging.debug("Click START STOP SESSION button") + logging.debug("clicked start button") helper = CoreToolbarHelp(self.app) - self.destroy_children_widgets() self.canvas.mode = GraphMode.SELECT - - # set configuration state - # state = self.canvas.core_grpc.get_session_state() - # if state == core_pb2.SessionState.SHUTDOWN or self.application.is_open_xml: - # self.canvas.core_grpc.set_session_state(SessionStateEnum.DEFINITION.value) - # self.application.is_open_xml = False - # - # self.canvas.core_grpc.set_session_state(SessionStateEnum.CONFIGURATION.value) - # helper.add_nodes() - # helper.add_edges() - # self.canvas.core_grpc.set_session_state(SessionStateEnum.INSTANTIATION.value) helper.gui_start_session() - self.create_runtime_toolbar() - - # for node in self.canvas.grpc_manager.nodes.values(): - # print(node.type, node.model, int(node.x), int(node.y), node.name, node.node_id) - # self.canvas.core_grpc.add_node( - # node.type, node.model, int(node.x), int(node.y), node.name, node.node_id - # ) - - # print(len(self.canvas.grpc_manager.edges)) - # for edge in self.canvas.grpc_manager.edges.values(): - # print(edge.id1, edge.id2, edge.type1, edge.type2) - # self.canvas.core_grpc.add_link( - # edge.id1, edge.id2, edge.type1, edge.type2, edge - # ) - # self.canvas.core_grpc.get_session() - # self.application.is_open_xml = False + self.runtime_frame.tkraise() def click_link_tool(self): logging.debug("Click LINK button") self.canvas.mode = GraphMode.EDGE - def pick_router(self, main_button): - logging.debug("Pick router option") - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.ROUTER)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.ROUTER) - self.canvas.draw_node_name = "router" + def update_button(self, button, image_enum, name): + logging.info("update button(%s): %s, %s", button, image_enum, name) + self.hide_pickers() + if image_enum == ImageEnum.EDITNODE: + dialog = CustomNodesDialog(self.app, self.app) + dialog.show() + else: + image = Images.get(image_enum) + logging.info("updating button(%s): %s", button, name) + button.configure(image=image) + self.canvas.mode = GraphMode.NODE + self.canvas.draw_node_image = image + self.canvas.draw_node_name = name - def pick_host(self, main_button): - logging.debug("Pick host option") - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.HOST)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.HOST) - self.canvas.draw_node_name = "host" + def hide_pickers(self): + logging.info("hiding pickers") + if self.node_picker: + self.node_picker.destroy() + self.node_picker = None + if self.network_picker: + self.network_picker.destroy() + self.network_picker = None + if self.annotation_picker: + self.annotation_picker.destroy() + self.annotation_picker = None - def pick_pc(self, main_button): - logging.debug("Pick PC option") - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.PC)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.PC) - self.canvas.draw_node_name = "PC" - - def pick_mdr(self, main_button): - logging.debug("Pick MDR option") - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.MDR)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.MDR) - self.canvas.draw_node_name = "mdr" - - def pick_prouter(self, main_button): - logging.debug("Pick prouter option") - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.PROUTER)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.PROUTER) - self.canvas.draw_node_name = "prouter" - - def pick_ovs(self, main_button): - logging.debug("Pick OVS option") - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.OVS)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.OVS) - self.canvas.draw_node_name = "OVS" - - def pick_editnode(self, main_button): - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.EDITNODE)) - logging.debug("Pick editnode option") - dialog = CustomNodesDialog(self.app, self.app) - dialog.show() - - def draw_network_layer_options(self, network_layer_button): - """ - Draw the options for network-layer button - - :param tkinter.Radiobutton network_layer_button: network-layer button - :return: nothing - """ - # create a frame and add buttons to it - self.destroy_previous_frame() - option_frame = tk.Frame(self.master, padx=1, pady=1) - img_list = [ - Images.get(ImageEnum.ROUTER), - Images.get(ImageEnum.HOST), - Images.get(ImageEnum.PC), - Images.get(ImageEnum.MDR), - Images.get(ImageEnum.PROUTER), - Images.get(ImageEnum.OVS), - Images.get(ImageEnum.EDITNODE), - ] - func_list = [ - self.pick_router, - self.pick_host, - self.pick_pc, - self.pick_mdr, - self.pick_prouter, - self.pick_ovs, - self.pick_editnode, - ] - tooltip_list = [ - "router", - "host", - "PC", - "mdr", - "prouter", - "OVS", - "edit node types", - ] - for i in range(len(img_list)): - self.create_button( - img_list[i], - func_list[i], - option_frame, - network_layer_button, - tooltip_list[i], - ) - - # place frame at a calculated position as well as keep a reference of that frame - self.draw_button_menu_frame(self.edit_frame, option_frame, network_layer_button) - self.network_layer_option_menu = option_frame - - # destroy the frame before any further actions on other widgets - self.bind_widgets_before_frame_hide(option_frame) - option_frame.wait_window(option_frame) - self.unbind_widgets_after_frame_hide() - - def create_network_layer_button(self): + def create_node_button(self): """ Create network layer button :return: nothing """ router_image = Images.get(ImageEnum.ROUTER) - network_layer_button = tk.Radiobutton( - self.edit_frame, + self.node_button = tk.Radiobutton( + self.design_frame, indicatoron=False, variable=self.radio_value, value=3, width=self.width, height=self.height, image=router_image, - command=lambda: self.draw_network_layer_options(network_layer_button), + command=self.draw_node_picker, ) - network_layer_button.pack(side=tk.TOP, pady=1) - CreateToolTip(network_layer_button, "Network-layer virtual nodes") + self.node_button.grid() + CreateToolTip(self.node_button, "Network-layer virtual nodes") - def pick_hub(self, main_button): - logging.debug("Pick link-layer node HUB") - self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.HUB)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.HUB) - self.canvas.draw_node_name = "hub" - - def pick_switch(self, main_button): - logging.debug("Pick link-layer node SWITCH") - self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.SWITCH)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.SWITCH) - self.canvas.draw_node_name = "switch" - - def pick_wlan(self, main_button): - logging.debug("Pick link-layer node WLAN") - self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.WLAN)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.WLAN) - self.canvas.draw_node_name = "wlan" - - def pick_rj45(self, main_button): - logging.debug("Pick link-layer node RJ45") - self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.RJ45)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.RJ45) - self.canvas.draw_node_name = "rj45" - - def pick_tunnel(self, main_button): - logging.debug("Pick link-layer node TUNNEL") - self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.TUNNEL)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.TUNNEL) - self.canvas.draw_node_name = "tunnel" - - def pick_emane(self, main_button): - self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.EMANE)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.EMANE) - self.canvas.draw_node_name = "emane" - - def draw_link_layer_options(self, link_layer_button): + def draw_network_picker(self): """ Draw the options for link-layer button :param tkinter.RadioButton link_layer_button: link-layer button :return: nothing """ - # create a frame and add buttons to it - self.destroy_previous_frame() - option_frame = tk.Frame(self.master, padx=1, pady=1) - img_list = [ - Images.get(ImageEnum.HUB), - Images.get(ImageEnum.SWITCH), - Images.get(ImageEnum.WLAN), - Images.get(ImageEnum.EMANE), - Images.get(ImageEnum.RJ45), - Images.get(ImageEnum.TUNNEL), + self.hide_pickers() + self.network_picker = tk.Frame(self.master, padx=1, pady=1) + nodes = [ + (ImageEnum.HUB, "hub", "ethernet hub"), + (ImageEnum.SWITCH, "switch", "ethernet switch"), + (ImageEnum.WLAN, "wlan", "wireless LAN"), + (ImageEnum.EMANE, "emane", "EMANE"), + (ImageEnum.RJ45, "rj45", "rj45 physical interface tool"), + (ImageEnum.TUNNEL, "tunnel", "tunnel tool"), ] - func_list = [ - self.pick_hub, - self.pick_switch, - self.pick_wlan, - self.pick_emane, - self.pick_rj45, - self.pick_tunnel, - ] - tooltip_list = [ - "ethernet hub", - "ethernet switch", - "wireless LAN", - "emane", - "rj45 physical interface tool", - "tunnel tool", - ] - for i in range(len(img_list)): + for image_enum, name, tooltip in nodes: self.create_button( - img_list[i], - func_list[i], - option_frame, - link_layer_button, - tooltip_list[i], + Images.get(image_enum), + partial(self.update_button, self.network_button, image_enum, name), + self.network_picker, + tooltip, ) - - # place frame at a calculated position as well as keep a reference of the frame - self.draw_button_menu_frame(self.edit_frame, option_frame, link_layer_button) - self.link_layer_option_menu = option_frame - - # destroy the frame before any further actions on other widgets - self.bind_widgets_before_frame_hide(option_frame) - option_frame.wait_window(option_frame) - self.unbind_widgets_after_frame_hide() + self.show_picker(self.network_button, self.network_picker) def create_link_layer_button(self): """ @@ -421,75 +303,42 @@ class CoreToolbar(object): :return: nothing """ hub_image = Images.get(ImageEnum.HUB) - link_layer_button = tk.Radiobutton( - self.edit_frame, + self.network_button = tk.Radiobutton( + self.design_frame, indicatoron=False, variable=self.radio_value, value=4, width=self.width, height=self.height, image=hub_image, - command=lambda: self.draw_link_layer_options(link_layer_button), + command=self.draw_network_picker, ) - link_layer_button.pack(side=tk.TOP, pady=1) - CreateToolTip(link_layer_button, "link-layer nodes") + self.network_button.grid() + CreateToolTip(self.network_button, "link-layer nodes") - def pick_marker(self, main_button): - self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.MARKER)) - logging.debug("Pick MARKER") - - def pick_oval(self, main_button): - self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.OVAL)) - logging.debug("Pick OVAL") - - def pick_rectangle(self, main_button): - self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.RECTANGLE)) - logging.debug("Pick RECTANGLE") - - def pick_text(self, main_button): - self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.TEXT)) - logging.debug("Pick TEXT") - - def draw_marker_options(self, main_button): + def draw_annotation_picker(self): """ Draw the options for marker button :param tkinter.Radiobutton main_button: the main button :return: nothing """ - # create a frame and add buttons to it - self.destroy_previous_frame() - option_frame = tk.Frame(self.master, padx=1, pady=1) - img_list = [ - Images.get(ImageEnum.MARKER), - Images.get(ImageEnum.OVAL), - Images.get(ImageEnum.RECTANGLE), - Images.get(ImageEnum.TEXT), + self.hide_pickers() + self.annotation_picker = tk.Frame(self.master, padx=1, pady=1) + nodes = [ + (ImageEnum.MARKER, "marker"), + (ImageEnum.OVAL, "oval"), + (ImageEnum.RECTANGLE, "rectangle"), + (ImageEnum.TEXT, "text"), ] - func_list = [ - self.pick_marker, - self.pick_oval, - self.pick_rectangle, - self.pick_text, - ] - tooltip_list = ["marker", "oval", "rectangle", "text"] - for i in range(len(img_list)): + for image_enum, tooltip in nodes: self.create_button( - img_list[i], func_list[i], option_frame, main_button, tooltip_list[i] + Images.get(image_enum), + partial(self.update_annotation, image_enum), + self.annotation_picker, + tooltip, ) - - # place the frame at a calculated position as well as keep a reference of that frame - self.draw_button_menu_frame(self.edit_frame, option_frame, main_button) - self.marker_option_menu = option_frame - - # destroy the frame before any further actions on other widgets - self.bind_widgets_before_frame_hide(option_frame) - option_frame.wait_window(option_frame) - self.unbind_widgets_after_frame_hide() + self.show_picker(self.annotation_button, self.annotation_picker) def create_marker_button(self): """ @@ -498,55 +347,22 @@ class CoreToolbar(object): :return: nothing """ marker_image = Images.get(ImageEnum.MARKER) - marker_main_button = tk.Radiobutton( - self.edit_frame, + self.annotation_button = tk.Radiobutton( + self.design_frame, indicatoron=False, variable=self.radio_value, value=5, width=self.width, height=self.height, image=marker_image, - command=lambda: self.draw_marker_options(marker_main_button), + command=self.draw_annotation_picker, ) - marker_main_button.pack(side=tk.TOP, pady=1) - CreateToolTip(marker_main_button, "background annotation tools") - - def create_toolbar(self): - """ - Create buttons for toolbar in edit mode - - :return: nothing - """ - self.create_regular_button( - self.edit_frame, - Images.get(ImageEnum.START), - self.click_start_session_tool, - "start the session", - ) - self.create_radio_button( - self.edit_frame, - Images.get(ImageEnum.SELECT), - self.click_selection_tool, - self.radio_value, - 1, - "selection tool", - ) - self.create_radio_button( - self.edit_frame, - Images.get(ImageEnum.LINK), - self.click_link_tool, - self.radio_value, - 2, - "link tool", - ) - self.create_network_layer_button() - self.create_link_layer_button() - self.create_marker_button() - self.radio_value.set(1) + self.annotation_button.grid() + CreateToolTip(self.annotation_button, "background annotation tools") def create_observe_button(self): menu_button = tk.Menubutton( - self.edit_frame, + self.runtime_frame, image=Images.get(ImageEnum.OBSERVE), width=self.width, height=self.height, @@ -555,7 +371,7 @@ class CoreToolbar(object): ) menu_button.menu = tk.Menu(menu_button, tearoff=0) menu_button["menu"] = menu_button.menu - menu_button.pack(side=tk.TOP, pady=1) + menu_button.grid() menu_button.menu.add_command(label="None") menu_button.menu.add_command(label="processes") @@ -581,9 +397,13 @@ class CoreToolbar(object): :return: nothing """ logging.debug("Click on STOP button ") - self.destroy_children_widgets() self.app.core.stop_session() - self.create_toolbar() + self.design_frame.tkraise() + + def update_annotation(self, image_enum): + logging.info("clicked annotation: ") + self.hide_pickers() + self.annotation_button.configure(image=Images.get(image_enum)) def click_run_button(self): logging.debug("Click on RUN button") @@ -596,48 +416,3 @@ class CoreToolbar(object): def click_two_node_button(self): logging.debug("Click TWONODE button") - - def create_runtime_toolbar(self): - self.create_regular_button( - self.edit_frame, - Images.get(ImageEnum.STOP), - self.click_stop_button, - "stop the session", - ) - self.create_radio_button( - self.edit_frame, - Images.get(ImageEnum.SELECT), - self.click_selection_tool, - self.exec_radio_value, - 1, - "selection tool", - ) - self.create_observe_button() - self.create_radio_button( - self.edit_frame, - Images.get(ImageEnum.PLOT), - self.click_plot_button, - self.exec_radio_value, - 2, - "plot", - ) - self.create_radio_button( - self.edit_frame, - Images.get(ImageEnum.MARKER), - self.click_marker_button, - self.exec_radio_value, - 3, - "marker", - ) - self.create_radio_button( - self.edit_frame, - Images.get(ImageEnum.TWONODE), - self.click_two_node_button, - self.exec_radio_value, - 4, - "run command from one node to another", - ) - self.create_regular_button( - self.edit_frame, Images.get(ImageEnum.RUN), self.click_run_button, "run" - ) - self.exec_radio_value.set(1) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 448a6f1d..eb2362c6 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -272,7 +272,7 @@ class CanvasGraph(tk.Canvas): else: self.focus_set() self.selected = self.get_selected(event) - logging.debug(f"click release selected: {self.selected}") + logging.debug(f"click release selected({self.selected}) mode({self.mode})") if self.mode == GraphMode.EDGE: self.handle_edge_release(event) elif self.mode == GraphMode.NODE: @@ -379,6 +379,7 @@ class CanvasGraph(tk.Canvas): def add_node(self, x, y, image, node_name): plot_id = self.find_all()[0] + logging.info("add node event: %s - %s", plot_id, self.selected) if self.selected == plot_id: node = CanvasNode( x=x, @@ -498,12 +499,12 @@ class CanvasNode: self.x_coord, self.y_coord = self.canvas.coords(self.id) def click_press(self, event): - logging.debug(f"click press {self.name}: {event}") + logging.debug(f"node click press {self.name}: {event}") self.moving = self.canvas.canvas_xy(event) # return "break" def click_release(self, event): - logging.debug(f"click release {self.name}: {event}") + logging.debug(f"node click release {self.name}: {event}") self.update_coords() self.canvas.core.update_node_location(self.id, self.x_coord, self.y_coord) self.moving = None From 707201ce54f791a2093ffb7aa1d4af4ac707feff Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 7 Nov 2019 15:19:01 -0800 Subject: [PATCH 197/462] work on node deletion --- coretk/coretk/coreclient.py | 48 ++++++++++++++--------------- coretk/coretk/coretoolbarhelp.py | 5 ++- coretk/coretk/dialogs/wlanconfig.py | 2 +- coretk/coretk/graph.py | 22 ++++++++++--- 4 files changed, 46 insertions(+), 31 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index ae03922b..318ccb99 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -308,7 +308,6 @@ class CoreClient: logging.info("delete nodes %s", response) def delete_links(self, delete_session=None): - # sid = None if delete_session is None: sid = self.session_id else: @@ -430,23 +429,6 @@ class CoreClient: else: return self.reusable.pop(0) - # def add_node(self, node_type, model, x, y, name, node_id): - # position = core_pb2.Position(x=x, y=y) - # node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) - # self.node_ids.append(node_id) - # response = self.client.add_node(self.session_id, node) - # logging.info("created node: %s", response) - # if node_type == core_pb2.NodeType.WIRELESS_LAN: - # d = OrderedDict() - # d["basic_range"] = "275" - # d["bandwidth"] = "54000000" - # d["jitter"] = "0" - # d["delay"] = "20000" - # d["error"] = "0" - # r = self.client.set_wlan_config(self.session_id, node_id, d) - # logging.debug("set wlan config %s", r) - # return response.node_id - def add_graph_node(self, session_id, canvas_id, x, y, name): """ Add node, with information filled in, to grpc manager @@ -510,12 +492,13 @@ class CoreClient: """ # keep reference to the core ids core_node_ids = [self.nodes[x].node_id for x in canvas_ids] + node_interface_pairs = [] # delete the nodes for i in canvas_ids: try: - self.nodes.pop(i) - self.reusable.append(i) + n = self.nodes.pop(i) + self.reusable.append(n.node_id) except KeyError: logging.error("coreclient.py INVALID NODE CANVAS ID") @@ -524,16 +507,33 @@ class CoreClient: # delete the edges and interfaces for i in tokens: try: - self.edges.pop(i) + e = self.edges.pop(i) + if e.interface_1 is not None: + node_interface_pairs.append(tuple([e.id1, e.interface_1.id])) + if e.interface_2 is not None: + node_interface_pairs.append(tuple([e.id2, e.interface_2.id])) + except KeyError: logging.error("coreclient.py invalid edge token ") - # delete any configurations + # delete global emane config if there no longer exist any emane cloud + if core_pb2.NodeType.EMANE not in [x.type for x in self.nodes.values()]: + self.emane_config = None + + # delete any mobility configuration, wlan configuration for i in core_node_ids: if i in self.mobilityconfig_management.configurations: - self.mobilityconfig_management.pop(i) + self.mobilityconfig_management.configurations.pop(i) if i in self.wlanconfig_management.configurations: - self.wlanconfig_management.pop(i) + self.wlanconfig_management.configurations.pop(i) + + # delete emane configurations + for i in node_interface_pairs: + if i in self.emaneconfig_management.configurations: + self.emaneconfig_management.configurations.pop(i) + for i in core_node_ids: + if tuple([i, None]) in self.emaneconfig_management.configurations: + self.emaneconfig_management.configurations.pop(tuple([i, None])) def add_preexisting_node(self, canvas_node, session_id, core_node, name): """ diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index 5192f2c5..c43a3c14 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -97,7 +97,10 @@ class CoreToolbarHelp: # get emane config (global configuration) pb_emane_config = self.app.core.emane_config - emane_config = {x: pb_emane_config[x].value for x in pb_emane_config} + if pb_emane_config is not None: + emane_config = {x: pb_emane_config[x].value for x in pb_emane_config} + else: + emane_config = None # get emane configuration list emane_model_configs = self.get_emane_configuration_list() diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index d57c8935..ecd2610b 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -24,7 +24,7 @@ class WlanConfigDialog(Dialog): self.config = config self.name = tk.StringVar(value=canvas_node.name) - self.range_var = tk.StringVar(value=config["basic_range"]) + self.range_var = tk.StringVar(value=config["range"]) self.bandwidth_var = tk.StringVar(value=config["bandwidth"]) self.delay_var = tk.StringVar(value=config["delay"]) self.loss_var = tk.StringVar(value=config["error"]) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 9ab6b956..af769766 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -383,7 +383,14 @@ class CanvasGraph(tk.Canvas): self.node_context.unpost() self.is_node_context_opened = False + # TODO rather than delete, might move the data to somewhere else in order to reuse + # TODO when the user undo def press_delete(self, event): + """ + delete selected nodes and any data that relates to it + :param event: + :return: + """ # hide nodes, links, link information that shows on the GUI to_delete_nodes, to_delete_edge_tokens = ( self.canvas_management.delete_selected_nodes() @@ -395,12 +402,17 @@ class CanvasGraph(tk.Canvas): for token in to_delete_edge_tokens: self.edges.pop(token) + # delete the edge data inside of canvas node + canvas_node_link_to_delete = [] + for canvas_id, node in self.nodes.items(): + for e in node.edges: + if e.token in to_delete_edge_tokens: + canvas_node_link_to_delete.append(tuple([canvas_id, e])) + for nid, edge in canvas_node_link_to_delete: + self.nodes[nid].edges.remove(edge) + + # delete the related data from core self.core.delete_wanted_graph_nodes(to_delete_nodes, to_delete_edge_tokens) - # delete any configuration related to the links and nodes - - # delete selected node - - # delete links connected to the selected nodes def add_node(self, x, y, image, node_name): plot_id = self.find_all()[0] From 173747fd138c9caf90a20a338679ca456303b1d8 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 7 Nov 2019 15:58:02 -0800 Subject: [PATCH 198/462] renamed coretoolbar to just toolbar --- coretk/coretk/app.py | 19 +++++++++++-------- coretk/coretk/coreclient.py | 4 ++-- coretk/coretk/menuaction.py | 4 ---- coretk/coretk/{coretoolbar.py => toolbar.py} | 6 +++--- .../{coretoolbarhelp.py => toolbarhelper.py} | 2 +- 5 files changed, 17 insertions(+), 18 deletions(-) rename coretk/coretk/{coretoolbar.py => toolbar.py} (99%) rename coretk/coretk/{coretoolbarhelp.py => toolbarhelper.py} (99%) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index faaf1c20..a53b0f8b 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -4,10 +4,10 @@ import tkinter as tk from coretk import appdirs from coretk.coreclient import CoreClient from coretk.coremenubar import CoreMenubar -from coretk.coretoolbar import CoreToolbar from coretk.graph import CanvasGraph from coretk.images import ImageEnum, Images from coretk.menuaction import MenuAction +from coretk.toolbar import Toolbar class Application(tk.Frame): @@ -17,7 +17,7 @@ class Application(tk.Frame): self.menubar = None self.core_menu = None self.canvas = None - self.core_editbar = None + self.toolbar = None self.is_open_xml = False self.size_and_scale = None self.set_wallpaper = None @@ -29,9 +29,7 @@ class Application(tk.Frame): self.config = appdirs.read_config() self.core = CoreClient(self) self.setup_app() - self.draw_menu() - self.draw_toolbar() - self.draw_canvas() + self.draw() self.core.set_up() def setup_app(self): @@ -42,6 +40,11 @@ class Application(tk.Frame): self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) + def draw(self): + self.draw_menu() + self.draw_toolbar() + self.draw_canvas() + def draw_menu(self): self.master.option_add("*tearOff", tk.FALSE) self.menubar = tk.Menu(self.master) @@ -50,8 +53,8 @@ class Application(tk.Frame): self.master.config(menu=self.menubar) def draw_toolbar(self): - self.core_editbar = CoreToolbar(self, self) - self.core_editbar.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) + self.toolbar = Toolbar(self, self) + self.toolbar.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) def draw_canvas(self): self.canvas = CanvasGraph( @@ -59,7 +62,7 @@ class Application(tk.Frame): ) self.canvas.pack(fill=tk.BOTH, expand=True) - self.core_editbar.canvas = self.canvas + self.toolbar.canvas = self.canvas scroll_x = tk.Scrollbar( self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 564c13fe..63816cb4 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -208,9 +208,9 @@ class CoreClient: # draw tool bar appropritate with session state if session_state == core_pb2.SessionState.RUNTIME: - self.app.core_editbar.runtime_frame.tkraise() + self.app.toolbar.runtime_frame.tkraise() else: - self.app.core_editbar.design_frame.tkraise() + self.app.toolbar.design_frame.tkraise() def create_new_session(self): """ diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 391cf765..12f81559 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -367,10 +367,6 @@ class MenuAction: self.prompt_save_running_session() self.app.core.open_xml(file_path) - # Todo might not need - # self.application.core_editbar.destroy_children_widgets() - # self.application.core_editbar.create_toolbar() - def canvas_size_and_scale(self): dialog = SizeAndScaleDialog(self.app, self.app) dialog.show() diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/toolbar.py similarity index 99% rename from coretk/coretk/coretoolbar.py rename to coretk/coretk/toolbar.py index aaed37a9..c97a326f 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/toolbar.py @@ -2,14 +2,14 @@ import logging import tkinter as tk from functools import partial -from coretk.coretoolbarhelp import CoreToolbarHelp from coretk.dialogs.customnodes import CustomNodesDialog from coretk.graph import GraphMode from coretk.images import ImageEnum, Images +from coretk.toolbarhelper import ToolbarHelper from coretk.tooltip import CreateToolTip -class CoreToolbar(tk.Frame): +class Toolbar(tk.Frame): """ Core toolbar class """ @@ -215,8 +215,8 @@ class CoreToolbar(tk.Frame): :return: nothing """ logging.debug("clicked start button") - helper = CoreToolbarHelp(self.app) self.canvas.mode = GraphMode.SELECT + helper = ToolbarHelper(self.app) helper.gui_start_session() self.runtime_frame.tkraise() diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/toolbarhelper.py similarity index 99% rename from coretk/coretk/coretoolbarhelp.py rename to coretk/coretk/toolbarhelper.py index c650a5bc..80b31506 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/toolbarhelper.py @@ -4,7 +4,7 @@ CoreToolbar help to draw on canvas, and make grpc client call from core.api.grpc.client import core_pb2 -class CoreToolbarHelp: +class ToolbarHelper: def __init__(self, app): self.app = app From c4d2ae599bea53535872ab7d103d63a494de0351 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 7 Nov 2019 16:15:29 -0800 Subject: [PATCH 199/462] removed toolbarhelper, moved logic into coreclient, updated start session click --- coretk/coretk/coreclient.py | 78 +++++++++++++++++++---- coretk/coretk/toolbar.py | 4 +- coretk/coretk/toolbaraction.py | 17 ----- coretk/coretk/toolbarhelper.py | 112 --------------------------------- 4 files changed, 66 insertions(+), 145 deletions(-) delete mode 100644 coretk/coretk/toolbaraction.py delete mode 100644 coretk/coretk/toolbarhelper.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 63816cb4..da75d3bd 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -305,23 +305,19 @@ class CoreClient: ) logging.info("delete links %s", response) - # TODO add location, hooks, emane_config, etc... - def start_session( - self, - nodes, - links, - location=None, - hooks=None, - emane_config=None, - emane_model_configs=None, - wlan_configs=None, - mobility_configs=None, - ): + def start_session(self): + nodes = self.get_nodes_proto() + links = self.get_links_proto() + wlan_configs = self.get_wlan_configs_proto() + mobility_configs = self.get_mobility_configs_proto() + emane_model_configs = self.get_emane_model_configs_proto() + hooks = list(self.hooks.values()) + emane_config = {x: self.emane_config[x].value for x in self.emane_config} response = self.client.start_session( self.session_id, nodes, links, - hooks=list(self.hooks.values()), + hooks=hooks, wlan_configs=wlan_configs, emane_config=emane_config, emane_model_configs=emane_model_configs, @@ -675,3 +671,59 @@ class CoreClient: else: logging.error("grpcmanagement.py INVALID CANVAS NODE ID") + + def get_nodes_proto(self): + nodes = [] + for node in self.nodes.values(): + pos = core_pb2.Position(x=int(node.x), y=int(node.y)) + proto_node = core_pb2.Node( + id=node.node_id, type=node.type, position=pos, model=node.model + ) + nodes.append(proto_node) + return nodes + + def get_links_proto(self): + links = [] + for edge in self.edges.values(): + interface_one = self.create_interface(edge.type1, edge.interface_1) + interface_two = self.create_interface(edge.type2, edge.interface_2) + link = core_pb2.Link( + node_one_id=edge.id1, + node_two_id=edge.id2, + type=core_pb2.LinkType.WIRED, + interface_one=interface_one, + interface_two=interface_two, + ) + links.append(link) + return links + + def get_wlan_configs_proto(self): + configs = [] + wlan_configs = self.wlanconfig_management.configurations + for node_id in wlan_configs: + config = wlan_configs[node_id] + config_proto = core_pb2.WlanConfig(node_id=node_id, config=config) + configs.append(config_proto) + return configs + + def get_mobility_configs_proto(self): + configs = [] + mobility_configs = self.mobilityconfig_management.configurations + for node_id in mobility_configs: + config = mobility_configs[node_id] + config_proto = core_pb2.MobilityConfig(node_id=node_id, config=config) + configs.append(config_proto) + return configs + + def get_emane_model_configs_proto(self): + configs = [] + emane_configs = self.emaneconfig_management.configurations + for key, value in emane_configs.items(): + node_id, interface_id = key + model, options = value + config = {x: options[x].value for x in options} + config_proto = core_pb2.EmaneModelConfig( + node_id=node_id, interface_id=interface_id, model=model, config=config + ) + configs.append(config_proto) + return configs diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index c97a326f..ace49796 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -5,7 +5,6 @@ from functools import partial from coretk.dialogs.customnodes import CustomNodesDialog from coretk.graph import GraphMode from coretk.images import ImageEnum, Images -from coretk.toolbarhelper import ToolbarHelper from coretk.tooltip import CreateToolTip @@ -216,8 +215,7 @@ class Toolbar(tk.Frame): """ logging.debug("clicked start button") self.canvas.mode = GraphMode.SELECT - helper = ToolbarHelper(self.app) - helper.gui_start_session() + self.app.core.start_session() self.runtime_frame.tkraise() def click_link_tool(self): diff --git a/coretk/coretk/toolbaraction.py b/coretk/coretk/toolbaraction.py deleted file mode 100644 index bbbc689b..00000000 --- a/coretk/coretk/toolbaraction.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Python file that store button actions -""" - -import logging - - -def click_selection_tool(): - logging.debug("Click SELECTION TOOL") - - -def click_start_stop_session_tool(): - logging.debug("Click START STOP SELECTION TOOL") - - -def click_link_tool(): - logging.debug("Click LINK TOOL") diff --git a/coretk/coretk/toolbarhelper.py b/coretk/coretk/toolbarhelper.py deleted file mode 100644 index 80b31506..00000000 --- a/coretk/coretk/toolbarhelper.py +++ /dev/null @@ -1,112 +0,0 @@ -""" -CoreToolbar help to draw on canvas, and make grpc client call -""" -from core.api.grpc.client import core_pb2 - - -class ToolbarHelper: - def __init__(self, app): - self.app = app - - def get_node_list(self): - """ - form a list node protobuf nodes to pass in start_session in grpc - - :return: nothing - """ - nodes = [] - for node in self.app.core.nodes.values(): - pos = core_pb2.Position(x=int(node.x), y=int(node.y)) - n = core_pb2.Node( - id=node.node_id, type=node.type, position=pos, model=node.model - ) - nodes.append(n) - return nodes - - def get_link_list(self): - """ - form a list of links to pass into grpc start session - - :rtype: list(core_pb2.Link) - :return: list of protobuf links - """ - links = [] - for edge in self.app.core.edges.values(): - interface_one = self.app.core.create_interface(edge.type1, edge.interface_1) - interface_two = self.app.core.create_interface(edge.type2, edge.interface_2) - link = core_pb2.Link( - node_one_id=edge.id1, - node_two_id=edge.id2, - type=core_pb2.LinkType.WIRED, - interface_one=interface_one, - interface_two=interface_two, - ) - links.append(link) - - return links - - def get_wlan_configuration_list(self): - """ - form a list of wlan configuration to pass to start_session - - :return: nothing - """ - configs = [] - manager_configs = self.app.core.wlanconfig_management.configurations - for key in manager_configs: - cnf = core_pb2.WlanConfig(node_id=key, config=manager_configs[key]) - configs.append(cnf) - return configs - - def get_mobility_configuration_list(self): - """ - form a list of mobility configuration to pass to start_session - - :return: nothing - """ - configs = [] - core = self.app.canvas.core - manager_configs = core.mobilityconfig_management.configurations - for key in manager_configs: - cnf = core_pb2.MobilityConfig(node_id=key, config=manager_configs[key]) - configs.append(cnf) - return configs - - def get_emane_configuration_list(self): - """ - form a list of emane configuration for the nodes - - :return: nothing - """ - configs = [] - manager_configs = self.app.core.emaneconfig_management.configurations - for key, value in manager_configs.items(): - config = {x: value[1][x].value for x in value[1]} - configs.append( - core_pb2.EmaneModelConfig( - node_id=key[0], interface_id=key[1], model=value[0], config=config - ) - ) - return configs - - def gui_start_session(self): - nodes = self.get_node_list() - links = self.get_link_list() - wlan_configs = self.get_wlan_configuration_list() - mobility_configs = self.get_mobility_configuration_list() - - # get emane config (global configuration) - pb_emane_config = self.app.core.emane_config - emane_config = {x: pb_emane_config[x].value for x in pb_emane_config} - - # get emane configuration list - emane_model_configs = self.get_emane_configuration_list() - - self.app.core.start_session( - nodes, - links, - wlan_configs=wlan_configs, - mobility_configs=mobility_configs, - emane_config=emane_config, - emane_model_configs=emane_model_configs, - ) From 99678499cec107b41aad7945db8961ea03fd69a2 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 7 Nov 2019 21:46:40 -0800 Subject: [PATCH 200/462] some refactoring for menubar code --- coretk/coretk/app.py | 40 ++-- coretk/coretk/menuaction.py | 110 ++------- coretk/coretk/{coremenubar.py => menubar.py} | 238 ++++++------------- coretk/coretk/toolbar.py | 13 +- 4 files changed, 115 insertions(+), 286 deletions(-) rename coretk/coretk/{coremenubar.py => menubar.py} (79%) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index a53b0f8b..4bb23bc0 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -3,10 +3,10 @@ import tkinter as tk from coretk import appdirs from coretk.coreclient import CoreClient -from coretk.coremenubar import CoreMenubar from coretk.graph import CanvasGraph from coretk.images import ImageEnum, Images from coretk.menuaction import MenuAction +from coretk.menubar import Menubar from coretk.toolbar import Toolbar @@ -15,9 +15,9 @@ class Application(tk.Frame): super().__init__(master) Images.load_all() self.menubar = None - self.core_menu = None - self.canvas = None self.toolbar = None + self.canvas = None + self.statusbar = None self.is_open_xml = False self.size_and_scale = None self.set_wallpaper = None @@ -41,29 +41,18 @@ class Application(tk.Frame): self.pack(fill=tk.BOTH, expand=True) def draw(self): - self.draw_menu() - self.draw_toolbar() - self.draw_canvas() - - def draw_menu(self): self.master.option_add("*tearOff", tk.FALSE) - self.menubar = tk.Menu(self.master) - self.core_menu = CoreMenubar(self, self.master, self.menubar) - self.core_menu.create_core_menubar() - self.master.config(menu=self.menubar) - - def draw_toolbar(self): + self.menubar = Menubar(self.master, self) self.toolbar = Toolbar(self, self) self.toolbar.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) + self.draw_canvas() + self.draw_status() def draw_canvas(self): self.canvas = CanvasGraph( self, self.core, background="#cccccc", scrollregion=(0, 0, 1200, 1000) ) self.canvas.pack(fill=tk.BOTH, expand=True) - - self.toolbar.canvas = self.canvas - scroll_x = tk.Scrollbar( self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview ) @@ -73,14 +62,15 @@ class Application(tk.Frame): self.canvas.configure(xscrollcommand=scroll_x.set) self.canvas.configure(yscrollcommand=scroll_y.set) - status_bar = tk.Frame(self) - status_bar.pack(side=tk.BOTTOM, fill=tk.X) - b = tk.Button(status_bar, text="Button 1") - b.pack(side=tk.LEFT, padx=1) - b = tk.Button(status_bar, text="Button 2") - b.pack(side=tk.LEFT, padx=1) - b = tk.Button(status_bar, text="Button 3") - b.pack(side=tk.LEFT, padx=1) + def draw_status(self): + self.statusbar = tk.Frame(self) + self.statusbar.pack(side=tk.BOTTOM, fill=tk.X) + button = tk.Button(self.statusbar, text="Button 1") + button.pack(side=tk.LEFT, padx=1) + button = tk.Button(self.statusbar, text="Button 2") + button.pack(side=tk.LEFT, padx=1) + button = tk.Button(self.statusbar, text="Button 3") + button.pack(side=tk.LEFT, padx=1) def on_closing(self): menu_action = MenuAction(self, self.master) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 12f81559..88f32bf4 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -21,34 +21,14 @@ def sub_menu_items(): logging.debug("Click on sub menu items") -def file_new(): +def file_new(event=None): logging.debug("Click file New") -def file_new_shortcut(event): - logging.debug("Shortcut for file new shortcut") - - -def file_open(): - logging.debug("Click file Open") - - -def file_open_shortcut(event): - logging.debug("Shortcut for file open") - - def file_reload(): logging.debug("Click file Reload") -# def file_save(): -# logging.debug("Click file save") - - -def file_save_shortcut(event): - logging.debug("Shortcut for file save") - - def file_export_python_script(): logging.debug("Click file export python script") @@ -73,70 +53,38 @@ def file_save_screenshot(): logging.debug("Click file save screenshot") -def edit_undo(): +def edit_undo(event=None): logging.debug("Click edit undo") -def edit_undo_shortcut(event): - logging.debug("Shortcut for edit undo") - - -def edit_redo(): +def edit_redo(event=None): logging.debug("Click edit redo") -def edit_redo_shortcut(event): - logging.debug("Shortcut for edit redo") - - -def edit_cut(): +def edit_cut(event=None): logging.debug("Click edit cut") -def edit_cut_shortcut(event): - logging.debug("Shortcut for edit cut") - - -def edit_copy(): +def edit_copy(event=None): logging.debug("Click edit copy") -def edit_copy_shortcut(event): - logging.debug("Shortcut for edit copy") - - -def edit_paste(): +def edit_paste(event=None): logging.debug("Click edit paste") -def edit_paste_shortcut(event): - logging.debug("Shortcut for edit paste") - - -def edit_select_all(): +def edit_select_all(event=None): logging.debug("Click edit select all") -def edit_select_all_shortcut(event): - logging.debug("Shortcut for edit select all") - - -def edit_select_adjacent(): +def edit_select_adjacent(event=None): logging.debug("Click edit select adjacent") -def edit_select_adjacent_shortcut(event): - logging.debug("Shortcut for edit select adjacent") - - -def edit_find(): +def edit_find(event=None): logging.debug("CLick edit find") -def edit_find_shortcut(event): - logging.debug("Shortcut for edit find") - - def edit_clear_marker(): logging.debug("Click edit clear marker") @@ -157,38 +105,22 @@ def canvas_delete(): logging.debug("Click canvas delete") -def canvas_previous(): +def canvas_previous(event=None): logging.debug("Click canvas previous") -def canvas_previous_shortcut(event): - logging.debug("Shortcut for canvas previous") - - -def canvas_next(): +def canvas_next(event=None): logging.debug("Click canvas next") -def canvas_next_shortcut(event): - logging.debug("Shortcut for canvas next") - - -def canvas_first(): +def canvas_first(event=None): logging.debug("CLick canvas first") -def canvas_first_shortcut(event): - logging.debug("Shortcut for canvas first") - - -def canvas_last(): +def canvas_last(event=None): logging.debug("CLick canvas last") -def canvas_last_shortcut(event): - logging.debug("Shortcut canvas last") - - def view_show(): logging.debug("Click view show") @@ -205,22 +137,14 @@ def view_3d_gui(): logging.debug("CLick view 3D GUI") -def view_zoom_in(): +def view_zoom_in(event=None): logging.debug("Click view zoom in") -def view_zoom_in_shortcut(event): - logging.debug("Shortcut view zoom in") - - -def view_zoom_out(): +def view_zoom_out(event=None): logging.debug("Click view zoom out") -def view_zoom_out_shortcut(event): - logging.debug("Shortcut view zoom out") - - def tools_auto_rearrange_all(): logging.debug("Click tools, auto rearrange all") @@ -343,7 +267,7 @@ class MenuAction: self.prompt_save_running_session() self.app.quit() - def file_save_as_xml(self): + def file_save_as_xml(self, event=None): logging.info("menuaction.py file_save_as_xml()") file_path = filedialog.asksaveasfilename( initialdir=str(XML_PATH), @@ -354,7 +278,7 @@ class MenuAction: if file_path: self.app.core.save_xml(file_path) - def file_open_xml(self): + def file_open_xml(self, event=None): logging.info("menuaction.py file_open_xml()") self.app.is_open_xml = True file_path = filedialog.askopenfilename( diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/menubar.py similarity index 79% rename from coretk/coretk/coremenubar.py rename to coretk/coretk/menubar.py index f720b093..143833b7 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/menubar.py @@ -1,15 +1,14 @@ import tkinter as tk import coretk.menuaction as action -from coretk.menuaction import MenuAction -class CoreMenubar(object): +class Menubar(tk.Menu): """ Core menubar """ - def __init__(self, app, master, menubar): + def __init__(self, master, app, cnf={}, **kwargs): """ Create a CoreMenubar instance @@ -17,63 +16,54 @@ class CoreMenubar(object): :param tkinter.Menu menubar: menubar object :param coretk.app.Application app: application object """ - self.menubar = menubar - self.master = master + super().__init__(master, cnf, **kwargs) + self.master.config(menu=self) self.app = app self.menuaction = action.MenuAction(app, master) - self.menu_action = MenuAction(self.app, self.master) + self.draw() - # def on_quit(self): - # """ - # Prompt use to stop running session before application is closed - # - # :return: nothing - # """ - # state = self.application.core_grpc.get_session_state() - # - # if state == core_pb2.SessionState.SHUTDOWN or state == core_pb2.SessionState.DEFINITION: - # self.application.core_grpc.delete_session() - # self.application.core_grpc.core.close() - # # self.application.quit() - # else: - # msgbox = tk.messagebox.askyesnocancel("stop", "Stop the running session?") - # - # if msgbox or msgbox == False: - # if msgbox: - # self.application.core_grpc.set_session_state("datacollect") - # self.application.core_grpc.delete_links() - # self.application.core_grpc.delete_nodes() - # self.application.core_grpc.delete_session() - # - # self.application.core_grpc.core.close() - # # self.application.quit() + def draw(self): + """ + Create core menubar and bind the hot keys to their matching command - def create_file_menu(self): + :return: nothing + """ + self.draw_file_menu() + self.draw_edit_menu() + self.draw_canvas_menu() + self.draw_view_menu() + self.draw_tools_menu() + self.draw_widgets_menu() + self.draw_session_menu() + self.draw_help_menu() + + def draw_file_menu(self): """ Create file menu :return: nothing """ - file_menu = tk.Menu(self.menubar) - # menu_action = MenuAction(self.application, self.master) + file_menu = tk.Menu(self) file_menu.add_command( - label="New", command=action.file_new, accelerator="Ctrl+N", underline=0 + label="New Session", + command=action.file_new, + accelerator="Ctrl+N", + underline=0, ) + self.app.bind_all("", action.file_new) file_menu.add_command( label="Open...", - command=self.menu_action.file_open_xml, + command=self.menuaction.file_open_xml, accelerator="Ctrl+O", underline=0, ) + self.app.bind_all("", self.menuaction.file_open_xml) file_menu.add_command(label="Reload", command=action.file_reload, underline=0) - # file_menu.add_command( - # label="Save", command=action.file_save, accelerator="Ctrl+S", underline=0 - # ) - # file_menu.add_command(label="Save As XML...", command=action.file_save_as_xml) - file_menu.add_command(label="Save", command=self.menu_action.file_save_as_xml) - + file_menu.add_command( + label="Save", accelerator="Ctrl+S", command=self.menuaction.file_save_as_xml + ) + self.app.bind_all("", self.menuaction.file_save_as_xml) file_menu.add_separator() - file_menu.add_command( label="Export Python script...", command=action.file_export_python_script ) @@ -85,9 +75,7 @@ class CoreMenubar(object): label="Execute Python script with options...", command=action.file_execute_python_script_with_options, ) - file_menu.add_separator() - file_menu.add_command( label="Open current file in editor...", command=action.file_open_current_file_in_editor, @@ -96,97 +84,95 @@ class CoreMenubar(object): file_menu.add_command( label="Save screenshot...", command=action.file_save_screenshot ) - file_menu.add_separator() - file_menu.add_command( label="Quit", command=self.menuaction.on_quit, underline=0 ) - self.menubar.add_cascade(label="File", menu=file_menu, underline=0) + self.add_cascade(label="File", menu=file_menu, underline=0) - def create_edit_menu(self): + def draw_edit_menu(self): """ Create edit menu :return: nothing """ - edit_menu = tk.Menu(self.menubar) + edit_menu = tk.Menu(self) edit_menu.add_command( label="Undo", command=action.edit_undo, accelerator="Ctrl+Z", underline=0 ) + self.app.bind_all("", action.edit_undo) edit_menu.add_command( label="Redo", command=action.edit_redo, accelerator="Ctrl+Y", underline=0 ) - + self.app.bind_all("", action.edit_redo) edit_menu.add_separator() - edit_menu.add_command( label="Cut", command=action.edit_cut, accelerator="Ctrl+X", underline=0 ) + self.app.bind_all("", action.edit_cut) edit_menu.add_command( label="Copy", command=action.edit_copy, accelerator="Ctrl+C", underline=0 ) + self.app.bind_all("", action.edit_copy) edit_menu.add_command( label="Paste", command=action.edit_paste, accelerator="Ctrl+V", underline=0 ) - + self.app.bind_all("", action.edit_paste) edit_menu.add_separator() - edit_menu.add_command( label="Select all", command=action.edit_select_all, accelerator="Ctrl+A" ) + self.app.bind_all("", action.edit_select_all) edit_menu.add_command( label="Select Adjacent", command=action.edit_select_adjacent, accelerator="Ctrl+J", ) - + self.app.bind_all("", action.edit_select_adjacent) edit_menu.add_separator() - edit_menu.add_command( label="Find...", command=action.edit_find, accelerator="Ctrl+F", underline=0 ) + self.app.bind_all("", action.edit_find) edit_menu.add_command(label="Clear marker", command=action.edit_clear_marker) edit_menu.add_command(label="Preferences...", command=action.edit_preferences) + self.add_cascade(label="Edit", menu=edit_menu, underline=0) - self.menubar.add_cascade(label="Edit", menu=edit_menu, underline=0) - - def create_canvas_menu(self): + def draw_canvas_menu(self): """ Create canvas menu :return: nothing """ - canvas_menu = tk.Menu(self.menubar) + canvas_menu = tk.Menu(self) canvas_menu.add_command(label="New", command=action.canvas_new) canvas_menu.add_command(label="Manage...", command=action.canvas_manage) canvas_menu.add_command(label="Delete", command=action.canvas_delete) - canvas_menu.add_separator() - canvas_menu.add_command( - label="Size/scale...", command=self.menu_action.canvas_size_and_scale + label="Size/scale...", command=self.menuaction.canvas_size_and_scale ) canvas_menu.add_command( - label="Wallpaper...", command=self.menu_action.canvas_set_wallpaper + label="Wallpaper...", command=self.menuaction.canvas_set_wallpaper ) - canvas_menu.add_separator() - canvas_menu.add_command( label="Previous", command=action.canvas_previous, accelerator="PgUp" ) + self.app.bind_all("", action.canvas_previous) canvas_menu.add_command( label="Next", command=action.canvas_next, accelerator="PgDown" ) + self.app.bind_all("", action.canvas_next) canvas_menu.add_command( label="First", command=action.canvas_first, accelerator="Home" ) + self.app.bind_all("", action.canvas_first) canvas_menu.add_command( label="Last", command=action.canvas_last, accelerator="End" ) - - self.menubar.add_cascade(label="Canvas", menu=canvas_menu, underline=0) + self.app.bind_all("", action.canvas_last) + self.add_cascade(label="Canvas", menu=canvas_menu, underline=0) def create_show_menu(self, view_menu): """ @@ -206,33 +192,31 @@ class CoreMenubar(object): show_menu.add_command(label="Annotations", command=action.sub_menu_items) show_menu.add_command(label="Grid", command=action.sub_menu_items) show_menu.add_command(label="API Messages", command=action.sub_menu_items) - view_menu.add_cascade(label="Show", menu=show_menu) - def create_view_menu(self): + def draw_view_menu(self): """ Create view menu :return: nothing """ - view_menu = tk.Menu(self.menubar) + view_menu = tk.Menu(self) self.create_show_menu(view_menu) view_menu.add_command( label="Show hidden nodes", command=action.view_show_hidden_nodes ) view_menu.add_command(label="Locked", command=action.view_locked) view_menu.add_command(label="3D GUI...", command=action.view_3d_gui) - view_menu.add_separator() - view_menu.add_command( label="Zoom in", command=action.view_zoom_in, accelerator="+" ) + self.app.bind_all("", action.view_zoom_in) view_menu.add_command( label="Zoom out", command=action.view_zoom_out, accelerator="-" ) - - self.menubar.add_cascade(label="View", menu=view_menu, underline=0) + self.app.bind_all("", action.view_zoom_out) + self.add_cascade(label="View", menu=view_menu, underline=0) def create_experimental_menu(self, tools_menu): """ @@ -251,7 +235,6 @@ class CoreMenubar(object): experimental_menu.add_command( label="Topology partitioning...", command=action.sub_menu_items ) - tools_menu.add_cascade( label="Experimental", menu=experimental_menu, underline=0 ) @@ -269,7 +252,6 @@ class CoreMenubar(object): for i in nums: the_label = "R(" + str(i) + ")" random_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade( label="Random", menu=random_menu, underline=0 ) @@ -282,14 +264,11 @@ class CoreMenubar(object): :return: nothing """ grid_menu = tk.Menu(topology_generator_menu) - # list of number of nodes to create nums = [1, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 100] - for i in nums: the_label = "G(" + str(i) + ")" grid_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Grid", menu=grid_menu, underline=0) def create_connected_grid_menu(self, topology_generator_menu): @@ -324,7 +303,6 @@ class CoreMenubar(object): for i in nums: the_label = "P(" + str(i) + ")" chain_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Chain", menu=chain_menu, underline=0) def create_star_menu(self, topology_generator_menu): @@ -338,7 +316,6 @@ class CoreMenubar(object): for i in range(3, 26, 1): the_label = "C(" + str(i) + ")" star_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Star", menu=star_menu, underline=0) def create_cycle_menu(self, topology_generator_menu): @@ -352,7 +329,6 @@ class CoreMenubar(object): for i in range(3, 25, 1): the_label = "C(" + str(i) + ")" cycle_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Cycle", menu=cycle_menu, underline=0) def create_wheel_menu(self, topology_generator_menu): @@ -366,7 +342,6 @@ class CoreMenubar(object): for i in range(4, 26, 1): the_label = "W(" + str(i) + ")" wheel_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Wheel", menu=wheel_menu, underline=0) def create_cube_menu(self, topology_generator_menu): @@ -380,7 +355,6 @@ class CoreMenubar(object): for i in range(2, 7, 1): the_label = "Q(" + str(i) + ")" cube_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Cube", menu=cube_menu, underline=0) def create_clique_menu(self, topology_generator_menu): @@ -394,7 +368,6 @@ class CoreMenubar(object): for i in range(3, 25, 1): the_label = "K(" + str(i) + ")" clique_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade( label="Clique", menu=clique_menu, underline=0 ) @@ -429,7 +402,6 @@ class CoreMenubar(object): :return: nothing """ topology_generator_menu = tk.Menu(tools_menu) - self.create_random_menu(topology_generator_menu) self.create_grid_menu(topology_generator_menu) self.create_connected_grid_menu(topology_generator_menu) @@ -440,19 +412,17 @@ class CoreMenubar(object): self.create_cube_menu(topology_generator_menu) self.create_clique_menu(topology_generator_menu) self.create_bipartite_menu(topology_generator_menu) - tools_menu.add_cascade( label="Topology generator", menu=topology_generator_menu, underline=0 ) - def create_tools_menu(self): + def draw_tools_menu(self): """ Create tools menu :return: nothing """ - - tools_menu = tk.Menu(self.menubar) + tools_menu = tk.Menu(self) tools_menu.add_command( label="Auto rearrange all", command=action.tools_auto_rearrange_all, @@ -464,13 +434,10 @@ class CoreMenubar(object): underline=0, ) tools_menu.add_separator() - tools_menu.add_command( label="Align to grid", command=action.tools_align_to_grid, underline=0 ) - tools_menu.add_separator() - tools_menu.add_command(label="Traffic...", command=action.tools_traffic) tools_menu.add_command( label="IP addresses...", command=action.tools_ip_addresses, underline=0 @@ -489,8 +456,7 @@ class CoreMenubar(object): self.create_experimental_menu(tools_menu) self.create_topology_generator_menu(tools_menu) tools_menu.add_command(label="Debugger...", command=action.tools_debugger) - - self.menubar.add_cascade(label="Tools", menu=tools_menu, underline=0) + self.add_cascade(label="Tools", menu=tools_menu, underline=0) def create_observer_widgets_menu(self, widget_menu): """ @@ -544,9 +510,8 @@ class CoreMenubar(object): label="PIM neighbors", command=action.sub_menu_items ) observer_widget_menu.add_command( - label="Edit...", command=self.menu_action.edit_observer_widgets + label="Edit...", command=self.menuaction.edit_observer_widgets ) - widget_menu.add_cascade(label="Observer Widgets", menu=observer_widget_menu) def create_adjacency_menu(self, widget_menu): @@ -561,47 +526,40 @@ class CoreMenubar(object): adjacency_menu.add_command(label="OSPFv3", command=action.sub_menu_items) adjacency_menu.add_command(label="OSLR", command=action.sub_menu_items) adjacency_menu.add_command(label="OSLRv2", command=action.sub_menu_items) - widget_menu.add_cascade(label="Adjacency", menu=adjacency_menu) - def create_widgets_menu(self): + def draw_widgets_menu(self): """ Create widget menu :return: nothing """ - widget_menu = tk.Menu(self.menubar) + widget_menu = tk.Menu(self) self.create_observer_widgets_menu(widget_menu) self.create_adjacency_menu(widget_menu) widget_menu.add_command(label="Throughput", command=action.widgets_throughput) - widget_menu.add_separator() - widget_menu.add_command( label="Configure Adjacency...", command=action.widgets_configure_adjacency ) widget_menu.add_command( label="Configure Throughput...", command=action.widgets_configure_throughput ) + self.add_cascade(label="Widgets", menu=widget_menu, underline=0) - self.menubar.add_cascade(label="Widgets", menu=widget_menu, underline=0) - - def create_session_menu(self): + def draw_session_menu(self): """ Create session menu :return: nothing """ - session_menu = tk.Menu(self.menubar) - + session_menu = tk.Menu(self) session_menu.add_command( label="Change sessions...", - command=self.menu_action.session_change_sessions, + command=self.menuaction.session_change_sessions, underline=0, ) - session_menu.add_separator() - session_menu.add_command( label="Node types...", command=action.session_node_types, underline=0 ) @@ -609,7 +567,7 @@ class CoreMenubar(object): label="Comments...", command=action.session_comments, underline=0 ) session_menu.add_command( - label="Hooks...", command=self.menu_action.session_hooks, underline=0 + label="Hooks...", command=self.menuaction.session_hooks, underline=0 ) session_menu.add_command( label="Reset node positions", @@ -618,69 +576,27 @@ class CoreMenubar(object): ) session_menu.add_command( label="Emulation servers...", - command=self.menu_action.session_servers, + command=self.menuaction.session_servers, underline=0, ) session_menu.add_command( - label="Options...", command=self.menu_action.session_options, underline=0 + label="Options...", command=self.menuaction.session_options, underline=0 ) + self.add_cascade(label="Session", menu=session_menu, underline=0) - self.menubar.add_cascade(label="Session", menu=session_menu, underline=0) - - def create_help_menu(self): + def draw_help_menu(self): """ Create help menu :return: nothing """ - help_menu = tk.Menu(self.menubar) + help_menu = tk.Menu(self) help_menu.add_command( - label="Core Github (www)", command=self.menu_action.help_core_github + label="Core Github (www)", command=self.menuaction.help_core_github ) help_menu.add_command( label="Core Documentation (www)", - command=self.menu_action.help_core_documentation, + command=self.menuaction.help_core_documentation, ) help_menu.add_command(label="About", command=action.help_about) - - self.menubar.add_cascade(label="Help", menu=help_menu) - - def bind_menubar_shortcut(self): - """ - Bind hot keys to matching command - - :return: nothing - """ - self.app.bind_all("", action.file_new_shortcut) - self.app.bind_all("", action.file_open_shortcut) - self.app.bind_all("", action.file_save_shortcut) - self.app.bind_all("", action.edit_undo_shortcut) - self.app.bind_all("", action.edit_redo_shortcut) - self.app.bind_all("", action.edit_cut_shortcut) - self.app.bind_all("", action.edit_copy_shortcut) - self.app.bind_all("", action.edit_paste_shortcut) - self.app.bind_all("", action.edit_select_all_shortcut) - self.app.bind_all("", action.edit_select_adjacent_shortcut) - self.app.bind_all("", action.edit_find_shortcut) - self.app.bind_all("", action.canvas_previous_shortcut) - self.app.bind_all("", action.canvas_next_shortcut) - self.app.bind_all("", action.canvas_first_shortcut) - self.app.bind_all("", action.canvas_last_shortcut) - self.app.bind_all("", action.view_zoom_in_shortcut) - self.app.bind_all("", action.view_zoom_out_shortcut) - - def create_core_menubar(self): - """ - Create core menubar and bind the hot keys to their matching command - - :return: nothing - """ - self.create_file_menu() - self.create_edit_menu() - self.create_canvas_menu() - self.create_view_menu() - self.create_tools_menu() - self.create_widgets_menu() - self.create_session_menu() - self.create_help_menu() - self.bind_menubar_shortcut() + self.add_cascade(label="Help", menu=help_menu) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index ace49796..91e09508 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -34,7 +34,6 @@ class Toolbar(tk.Frame): self.link_layer_option_menu = None self.marker_option_menu = None self.network_layer_option_menu = None - self.canvas = None self.node_button = None self.network_button = None self.annotation_button = None @@ -204,7 +203,7 @@ class Toolbar(tk.Frame): def click_selection_tool(self): logging.debug("clicked selection tool") - self.canvas.mode = GraphMode.SELECT + self.app.canvas.mode = GraphMode.SELECT def click_start_session_tool(self): """ @@ -214,13 +213,13 @@ class Toolbar(tk.Frame): :return: nothing """ logging.debug("clicked start button") - self.canvas.mode = GraphMode.SELECT + self.app.canvas.mode = GraphMode.SELECT self.app.core.start_session() self.runtime_frame.tkraise() def click_link_tool(self): logging.debug("Click LINK button") - self.canvas.mode = GraphMode.EDGE + self.app.canvas.mode = GraphMode.EDGE def update_button(self, button, image_enum, name): logging.info("update button(%s): %s, %s", button, image_enum, name) @@ -232,9 +231,9 @@ class Toolbar(tk.Frame): image = Images.get(image_enum) logging.info("updating button(%s): %s", button, name) button.configure(image=image) - self.canvas.mode = GraphMode.NODE - self.canvas.draw_node_image = image - self.canvas.draw_node_name = name + self.app.canvas.mode = GraphMode.NODE + self.app.canvas.draw_node_image = image + self.app.canvas.draw_node_name = name def hide_pickers(self): logging.info("hiding pickers") From 6357062fecbdadd77dabc702018edf035e000c15 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 7 Nov 2019 22:00:46 -0800 Subject: [PATCH 201/462] removed packing from toolbar --- coretk/coretk/toolbar.py | 4 ++-- coretk/coretk/tooltip.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 91e09508..298cc24a 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -172,12 +172,12 @@ class Toolbar(tk.Frame): :param PIL.Image img: button image :param func: the command that is executed when button is clicked :param tkinter.Frame frame: frame that contains the button - :param tkinter.Radiobutton main_button: main button + :param str tooltip: tooltip text :return: nothing """ button = tk.Button(frame, width=self.width, height=self.height, image=img) button.bind("", lambda e: func()) - button.pack(side=tk.LEFT, pady=1) + button.grid(pady=1) CreateToolTip(button, tooltip) def create_radio_button(self, frame, image, func, variable, value, tooltip_msg): diff --git a/coretk/coretk/tooltip.py b/coretk/coretk/tooltip.py index beb68ba3..7ccb505b 100644 --- a/coretk/coretk/tooltip.py +++ b/coretk/coretk/tooltip.py @@ -14,8 +14,6 @@ class CreateToolTip(object): self.tw = None def enter(self, event=None): - x = 0 - y = 0 x, y, cx, cy = self.widget.bbox("insert") x += self.widget.winfo_rootx() y += self.widget.winfo_rooty() + 32 From dbaf5dad9177ed1a43ed64c4f51b40c4101ceb38 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 7 Nov 2019 22:11:27 -0800 Subject: [PATCH 202/462] changed tooltip from pack to grid --- coretk/coretk/tooltip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coretk/coretk/tooltip.py b/coretk/coretk/tooltip.py index 7ccb505b..6fbbc3c9 100644 --- a/coretk/coretk/tooltip.py +++ b/coretk/coretk/tooltip.py @@ -29,7 +29,7 @@ class CreateToolTip(object): relief="solid", borderwidth=1, ) - label.pack(ipadx=1) + label.grid(padx=1) def close(self, event=None): if self.tw: From 22177def1c3440055da8f0fb43de938d1750edc4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 8 Nov 2019 10:07:23 -0800 Subject: [PATCH 203/462] updates to disable all unimplemented menu options --- coretk/coretk/app.py | 6 - coretk/coretk/menuaction.py | 210 +------------- coretk/coretk/menubar.py | 550 +++++++++++++----------------------- 3 files changed, 201 insertions(+), 565 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 4bb23bc0..1297c622 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -65,12 +65,6 @@ class Application(tk.Frame): def draw_status(self): self.statusbar = tk.Frame(self) self.statusbar.pack(side=tk.BOTTOM, fill=tk.X) - button = tk.Button(self.statusbar, text="Button 1") - button.pack(side=tk.LEFT, padx=1) - button = tk.Button(self.statusbar, text="Button 2") - button.pack(side=tk.LEFT, padx=1) - button = tk.Button(self.statusbar, text="Button 3") - button.pack(side=tk.LEFT, padx=1) def on_closing(self): menu_action = MenuAction(self, self.master) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 88f32bf4..3b33377c 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -17,214 +17,6 @@ from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog -def sub_menu_items(): - logging.debug("Click on sub menu items") - - -def file_new(event=None): - logging.debug("Click file New") - - -def file_reload(): - logging.debug("Click file Reload") - - -def file_export_python_script(): - logging.debug("Click file export python script") - - -def file_execute_xml_or_python_script(): - logging.debug("Execute XML or Python script") - - -def file_execute_python_script_with_options(): - logging.debug("Click execute Python script with options") - - -def file_open_current_file_in_editor(): - logging.debug("Click file open current in editor") - - -def file_print(): - logging.debug("Click file Print") - - -def file_save_screenshot(): - logging.debug("Click file save screenshot") - - -def edit_undo(event=None): - logging.debug("Click edit undo") - - -def edit_redo(event=None): - logging.debug("Click edit redo") - - -def edit_cut(event=None): - logging.debug("Click edit cut") - - -def edit_copy(event=None): - logging.debug("Click edit copy") - - -def edit_paste(event=None): - logging.debug("Click edit paste") - - -def edit_select_all(event=None): - logging.debug("Click edit select all") - - -def edit_select_adjacent(event=None): - logging.debug("Click edit select adjacent") - - -def edit_find(event=None): - logging.debug("CLick edit find") - - -def edit_clear_marker(): - logging.debug("Click edit clear marker") - - -def edit_preferences(): - logging.debug("Click preferences") - - -def canvas_new(): - logging.debug("Click canvas new") - - -def canvas_manage(): - logging.debug("Click canvas manage") - - -def canvas_delete(): - logging.debug("Click canvas delete") - - -def canvas_previous(event=None): - logging.debug("Click canvas previous") - - -def canvas_next(event=None): - logging.debug("Click canvas next") - - -def canvas_first(event=None): - logging.debug("CLick canvas first") - - -def canvas_last(event=None): - logging.debug("CLick canvas last") - - -def view_show(): - logging.debug("Click view show") - - -def view_show_hidden_nodes(): - logging.debug("Click view show hidden nodes") - - -def view_locked(): - logging.debug("Click view locked") - - -def view_3d_gui(): - logging.debug("CLick view 3D GUI") - - -def view_zoom_in(event=None): - logging.debug("Click view zoom in") - - -def view_zoom_out(event=None): - logging.debug("Click view zoom out") - - -def tools_auto_rearrange_all(): - logging.debug("Click tools, auto rearrange all") - - -def tools_auto_rearrange_selected(): - logging.debug("CLick tools auto rearrange selected") - - -def tools_align_to_grid(): - logging.debug("Click tools align to grid") - - -def tools_traffic(): - logging.debug("Click tools traffic") - - -def tools_ip_addresses(): - logging.debug("Click tools ip addresses") - - -def tools_mac_addresses(): - logging.debug("Click tools mac addresses") - - -def tools_build_hosts_file(): - logging.debug("Click tools build hosts file") - - -def tools_renumber_nodes(): - logging.debug("Click tools renumber nodes") - - -def tools_experimental(): - logging.debug("Click tools experimental") - - -def tools_topology_generator(): - logging.debug("Click tools topology generator") - - -def tools_debugger(): - logging.debug("Click tools debugger") - - -def widgets_observer_widgets(): - logging.debug("Click widgets observer widgets") - - -def widgets_adjacency(): - logging.debug("Click widgets adjacency") - - -def widgets_throughput(): - logging.debug("Click widgets throughput") - - -def widgets_configure_adjacency(): - logging.debug("Click widgets configure adjacency") - - -def widgets_configure_throughput(): - logging.debug("Click widgets configure throughput") - - -def session_node_types(): - logging.debug("Click session node types") - - -def session_comments(): - logging.debug("Click session comments") - - -def session_reset_node_positions(): - logging.debug("Click session reset node positions") - - -def help_about(): - logging.debug("Click help About") - - class MenuAction: """ Actions performed when choosing menu items @@ -258,7 +50,7 @@ class MenuAction: self.app.core.stop_session() self.app.core.delete_session() - def on_quit(self): + def on_quit(self, event=None): """ Prompt user whether so save running session, and then close the application diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index 143833b7..3ef38d73 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -43,52 +43,33 @@ class Menubar(tk.Menu): :return: nothing """ - file_menu = tk.Menu(self) - file_menu.add_command( - label="New Session", - command=action.file_new, - accelerator="Ctrl+N", - underline=0, - ) - self.app.bind_all("", action.file_new) - file_menu.add_command( - label="Open...", - command=self.menuaction.file_open_xml, - accelerator="Ctrl+O", - underline=0, + menu = tk.Menu(self) + menu.add_command(label="New Session", accelerator="Ctrl+N", state=tk.DISABLED) + menu.add_command( + label="Open...", command=self.menuaction.file_open_xml, accelerator="Ctrl+O" ) self.app.bind_all("", self.menuaction.file_open_xml) - file_menu.add_command(label="Reload", command=action.file_reload, underline=0) - file_menu.add_command( + menu.add_command(label="Reload", underline=0, state=tk.DISABLED) + menu.add_command( label="Save", accelerator="Ctrl+S", command=self.menuaction.file_save_as_xml ) self.app.bind_all("", self.menuaction.file_save_as_xml) - file_menu.add_separator() - file_menu.add_command( - label="Export Python script...", command=action.file_export_python_script + 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 with options...", state=tk.DISABLED ) - file_menu.add_command( - label="Execute XML or Python script...", - command=action.file_execute_xml_or_python_script, + menu.add_separator() + menu.add_command(label="Open current file in editor...", state=tk.DISABLED) + menu.add_command(label="Print...", underline=0, state=tk.DISABLED) + menu.add_command(label="Save screenshot...", state=tk.DISABLED) + menu.add_separator() + menu.add_command( + label="Quit", accelerator="Ctrl+Q", command=self.menuaction.on_quit ) - file_menu.add_command( - label="Execute Python script with options...", - command=action.file_execute_python_script_with_options, - ) - file_menu.add_separator() - file_menu.add_command( - label="Open current file in editor...", - command=action.file_open_current_file_in_editor, - ) - file_menu.add_command(label="Print...", command=action.file_print, underline=0) - file_menu.add_command( - label="Save screenshot...", command=action.file_save_screenshot - ) - file_menu.add_separator() - file_menu.add_command( - label="Quit", command=self.menuaction.on_quit, underline=0 - ) - self.add_cascade(label="File", menu=file_menu, underline=0) + self.app.bind_all("", self.menuaction.on_quit) + self.add_cascade(label="File", menu=menu) def draw_edit_menu(self): """ @@ -96,47 +77,23 @@ class Menubar(tk.Menu): :return: nothing """ - edit_menu = tk.Menu(self) - edit_menu.add_command( - label="Undo", command=action.edit_undo, accelerator="Ctrl+Z", underline=0 + menu = tk.Menu(self) + menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED) + menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED) + menu.add_separator() + menu.add_command(label="Cut", accelerator="Ctrl+X", state=tk.DISABLED) + menu.add_command(label="Copy", accelerator="Ctrl+C", state=tk.DISABLED) + menu.add_command(label="Paste", accelerator="Ctrl+V", state=tk.DISABLED) + menu.add_separator() + menu.add_command(label="Select all", accelerator="Ctrl+A", state=tk.DISABLED) + menu.add_command( + label="Select Adjacent", accelerator="Ctrl+J", state=tk.DISABLED ) - self.app.bind_all("", action.edit_undo) - edit_menu.add_command( - label="Redo", command=action.edit_redo, accelerator="Ctrl+Y", underline=0 - ) - self.app.bind_all("", action.edit_redo) - edit_menu.add_separator() - edit_menu.add_command( - label="Cut", command=action.edit_cut, accelerator="Ctrl+X", underline=0 - ) - self.app.bind_all("", action.edit_cut) - edit_menu.add_command( - label="Copy", command=action.edit_copy, accelerator="Ctrl+C", underline=0 - ) - self.app.bind_all("", action.edit_copy) - edit_menu.add_command( - label="Paste", command=action.edit_paste, accelerator="Ctrl+V", underline=0 - ) - self.app.bind_all("", action.edit_paste) - edit_menu.add_separator() - edit_menu.add_command( - label="Select all", command=action.edit_select_all, accelerator="Ctrl+A" - ) - self.app.bind_all("", action.edit_select_all) - edit_menu.add_command( - label="Select Adjacent", - command=action.edit_select_adjacent, - accelerator="Ctrl+J", - ) - self.app.bind_all("", action.edit_select_adjacent) - edit_menu.add_separator() - edit_menu.add_command( - label="Find...", command=action.edit_find, accelerator="Ctrl+F", underline=0 - ) - self.app.bind_all("", action.edit_find) - edit_menu.add_command(label="Clear marker", command=action.edit_clear_marker) - edit_menu.add_command(label="Preferences...", command=action.edit_preferences) - self.add_cascade(label="Edit", menu=edit_menu, underline=0) + menu.add_separator() + menu.add_command(label="Find...", accelerator="Ctrl+F", state=tk.DISABLED) + menu.add_command(label="Clear marker", state=tk.DISABLED) + menu.add_command(label="Preferences...", state=tk.DISABLED) + self.add_cascade(label="Edit", menu=menu) def draw_canvas_menu(self): """ @@ -144,55 +101,23 @@ class Menubar(tk.Menu): :return: nothing """ - canvas_menu = tk.Menu(self) - canvas_menu.add_command(label="New", command=action.canvas_new) - canvas_menu.add_command(label="Manage...", command=action.canvas_manage) - canvas_menu.add_command(label="Delete", command=action.canvas_delete) - canvas_menu.add_separator() - canvas_menu.add_command( + menu = tk.Menu(self) + menu.add_command(label="New", state=tk.DISABLED) + menu.add_command(label="Manage...", state=tk.DISABLED) + menu.add_command(label="Delete", state=tk.DISABLED) + menu.add_separator() + menu.add_command( label="Size/scale...", command=self.menuaction.canvas_size_and_scale ) - canvas_menu.add_command( + menu.add_command( label="Wallpaper...", command=self.menuaction.canvas_set_wallpaper ) - canvas_menu.add_separator() - canvas_menu.add_command( - label="Previous", command=action.canvas_previous, accelerator="PgUp" - ) - self.app.bind_all("", action.canvas_previous) - canvas_menu.add_command( - label="Next", command=action.canvas_next, accelerator="PgDown" - ) - self.app.bind_all("", action.canvas_next) - canvas_menu.add_command( - label="First", command=action.canvas_first, accelerator="Home" - ) - self.app.bind_all("", action.canvas_first) - canvas_menu.add_command( - label="Last", command=action.canvas_last, accelerator="End" - ) - self.app.bind_all("", action.canvas_last) - self.add_cascade(label="Canvas", menu=canvas_menu, underline=0) - - def create_show_menu(self, view_menu): - """ - Create the menu items in View/Show - - :param tkinter.Menu view_menu: the view menu - :return: nothing - """ - show_menu = tk.Menu(view_menu) - show_menu.add_command(label="All", command=action.sub_menu_items) - show_menu.add_command(label="None", command=action.sub_menu_items) - show_menu.add_separator() - show_menu.add_command(label="Interface Names", command=action.sub_menu_items) - show_menu.add_command(label="IPv4 Addresses", command=action.sub_menu_items) - show_menu.add_command(label="IPv6 Addresses", command=action.sub_menu_items) - show_menu.add_command(label="Node Labels", command=action.sub_menu_items) - show_menu.add_command(label="Annotations", command=action.sub_menu_items) - show_menu.add_command(label="Grid", command=action.sub_menu_items) - show_menu.add_command(label="API Messages", command=action.sub_menu_items) - view_menu.add_cascade(label="Show", menu=show_menu) + menu.add_separator() + menu.add_command(label="Previous", accelerator="PgUp", state=tk.DISABLED) + menu.add_command(label="Next", accelerator="PgDown", state=tk.DISABLED) + menu.add_command(label="First", accelerator="Home", state=tk.DISABLED) + menu.add_command(label="Last", accelerator="End", state=tk.DISABLED) + self.add_cascade(label="Canvas", menu=menu) def draw_view_menu(self): """ @@ -202,21 +127,33 @@ class Menubar(tk.Menu): """ view_menu = tk.Menu(self) self.create_show_menu(view_menu) - view_menu.add_command( - label="Show hidden nodes", command=action.view_show_hidden_nodes - ) - view_menu.add_command(label="Locked", command=action.view_locked) - view_menu.add_command(label="3D GUI...", command=action.view_3d_gui) + view_menu.add_command(label="Show hidden nodes", state=tk.DISABLED) + view_menu.add_command(label="Locked", state=tk.DISABLED) + view_menu.add_command(label="3D GUI...", state=tk.DISABLED) view_menu.add_separator() - view_menu.add_command( - label="Zoom in", command=action.view_zoom_in, accelerator="+" - ) - self.app.bind_all("", action.view_zoom_in) - view_menu.add_command( - label="Zoom out", command=action.view_zoom_out, accelerator="-" - ) - self.app.bind_all("", action.view_zoom_out) - self.add_cascade(label="View", menu=view_menu, underline=0) + view_menu.add_command(label="Zoom in", accelerator="+", state=tk.DISABLED) + view_menu.add_command(label="Zoom out", accelerator="-", state=tk.DISABLED) + self.add_cascade(label="View", menu=view_menu) + + def create_show_menu(self, view_menu): + """ + Create the menu items in View/Show + + :param tkinter.Menu view_menu: the view menu + :return: nothing + """ + menu = tk.Menu(view_menu) + menu.add_command(label="All", state=tk.DISABLED) + menu.add_command(label="None", state=tk.DISABLED) + menu.add_separator() + menu.add_command(label="Interface Names", state=tk.DISABLED) + menu.add_command(label="IPv4 Addresses", state=tk.DISABLED) + menu.add_command(label="IPv6 Addresses", state=tk.DISABLED) + menu.add_command(label="Node Labels", state=tk.DISABLED) + menu.add_command(label="Annotations", state=tk.DISABLED) + menu.add_command(label="Grid", state=tk.DISABLED) + menu.add_command(label="API Messages", state=tk.DISABLED) + view_menu.add_cascade(label="Show", menu=menu) def create_experimental_menu(self, tools_menu): """ @@ -225,19 +162,11 @@ class Menubar(tk.Menu): :param tkinter.Menu tools_menu: tools menu :return: nothing """ - experimental_menu = tk.Menu(tools_menu) - experimental_menu.add_command( - label="Plugins...", command=action.sub_menu_items, underline=0 - ) - experimental_menu.add_command( - label="ns2immunes converter...", command=action.sub_menu_items, underline=0 - ) - experimental_menu.add_command( - label="Topology partitioning...", command=action.sub_menu_items - ) - tools_menu.add_cascade( - label="Experimental", menu=experimental_menu, underline=0 - ) + menu = tk.Menu(tools_menu) + menu.add_command(label="Plugins...", state=tk.DISABLED) + menu.add_command(label="ns2immunes converter...", state=tk.DISABLED) + menu.add_command(label="Topology partitioning...", state=tk.DISABLED) + tools_menu.add_cascade(label="Experimental", menu=menu) def create_random_menu(self, topology_generator_menu): """ @@ -246,15 +175,13 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - random_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) # list of number of random nodes to create nums = [1, 5, 10, 15, 20, 30, 40, 50, 75, 100] for i in nums: - the_label = "R(" + str(i) + ")" - random_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade( - label="Random", menu=random_menu, underline=0 - ) + label = f"R({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Random", menu=menu) def create_grid_menu(self, topology_generator_menu): """ @@ -263,13 +190,13 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology_generator_menu :return: nothing """ - grid_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) # list of number of nodes to create nums = [1, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 100] for i in nums: - the_label = "G(" + str(i) + ")" - grid_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Grid", menu=grid_menu, underline=0) + label = f"G({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Grid", menu=menu) def create_connected_grid_menu(self, topology_generator_menu): """ @@ -278,17 +205,15 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - grid_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) for i in range(1, 11, 1): - i_n_menu = tk.Menu(grid_menu) + submenu = tk.Menu(menu) for j in range(1, 11, 1): - i_j_label = str(i) + " X " + str(j) - i_n_menu.add_command(label=i_j_label, command=action.sub_menu_items) - i_n_label = str(i) + " X N" - grid_menu.add_cascade(label=i_n_label, menu=i_n_menu) - topology_generator_menu.add_cascade( - label="Connected Grid", menu=grid_menu, underline=0 - ) + label = f"{i} X {j}" + submenu.add_command(label=label, state=tk.DISABLED) + label = str(i) + " X N" + menu.add_cascade(label=label, menu=submenu) + topology_generator_menu.add_cascade(label="Connected Grid", menu=menu) def create_chain_menu(self, topology_generator_menu): """ @@ -297,13 +222,13 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - chain_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) # number of nodes to create nums = list(range(2, 25, 1)) + [32, 64, 128] for i in nums: - the_label = "P(" + str(i) + ")" - chain_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Chain", menu=chain_menu, underline=0) + label = f"P({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Chain", menu=menu) def create_star_menu(self, topology_generator_menu): """ @@ -312,11 +237,11 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - star_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) for i in range(3, 26, 1): - the_label = "C(" + str(i) + ")" - star_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Star", menu=star_menu, underline=0) + label = f"C({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Star", menu=menu) def create_cycle_menu(self, topology_generator_menu): """ @@ -325,11 +250,11 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - cycle_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) for i in range(3, 25, 1): - the_label = "C(" + str(i) + ")" - cycle_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Cycle", menu=cycle_menu, underline=0) + label = f"C({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Cycle", menu=menu) def create_wheel_menu(self, topology_generator_menu): """ @@ -338,11 +263,11 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - wheel_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) for i in range(4, 26, 1): - the_label = "W(" + str(i) + ")" - wheel_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Wheel", menu=wheel_menu, underline=0) + label = f"W({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Wheel", menu=menu) def create_cube_menu(self, topology_generator_menu): """ @@ -351,11 +276,11 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - cube_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) for i in range(2, 7, 1): - the_label = "Q(" + str(i) + ")" - cube_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Cube", menu=cube_menu, underline=0) + label = f"Q({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Cube", menu=menu) def create_clique_menu(self, topology_generator_menu): """ @@ -364,13 +289,11 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - clique_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) for i in range(3, 25, 1): - the_label = "K(" + str(i) + ")" - clique_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade( - label="Clique", menu=clique_menu, underline=0 - ) + label = f"K({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Clique", menu=menu) def create_bipartite_menu(self, topology_generator_menu): """ @@ -379,19 +302,17 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology_generator_menu :return: nothing """ - bipartite_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) temp = 24 for i in range(1, 13, 1): - i_n_menu = tk.Menu(bipartite_menu) + submenu = tk.Menu(menu) for j in range(i, temp, 1): - i_j_label = "K(" + str(i) + " X " + str(j) + ")" - i_n_menu.add_command(label=i_j_label, command=action.sub_menu_items) - i_n_label = "K(" + str(i) + " X N)" - bipartite_menu.add_cascade(label=i_n_label, menu=i_n_menu) + label = f"K({i} X {j})" + submenu.add_command(label=label, state=tk.DISABLED) + label = f"K({i})" + menu.add_cascade(label=label, menu=submenu) temp = temp - 1 - topology_generator_menu.add_cascade( - label="Bipartite", menu=bipartite_menu, underline=0 - ) + topology_generator_menu.add_cascade(label="Bipartite", menu=menu) def create_topology_generator_menu(self, tools_menu): """ @@ -401,20 +322,18 @@ class Menubar(tk.Menu): :return: nothing """ - topology_generator_menu = tk.Menu(tools_menu) - self.create_random_menu(topology_generator_menu) - self.create_grid_menu(topology_generator_menu) - self.create_connected_grid_menu(topology_generator_menu) - self.create_chain_menu(topology_generator_menu) - self.create_star_menu(topology_generator_menu) - self.create_cycle_menu(topology_generator_menu) - self.create_wheel_menu(topology_generator_menu) - self.create_cube_menu(topology_generator_menu) - self.create_clique_menu(topology_generator_menu) - self.create_bipartite_menu(topology_generator_menu) - tools_menu.add_cascade( - label="Topology generator", menu=topology_generator_menu, underline=0 - ) + menu = tk.Menu(tools_menu) + self.create_random_menu(menu) + self.create_grid_menu(menu) + self.create_connected_grid_menu(menu) + self.create_chain_menu(menu) + self.create_star_menu(menu) + self.create_cycle_menu(menu) + self.create_wheel_menu(menu) + self.create_cube_menu(menu) + self.create_clique_menu(menu) + self.create_bipartite_menu(menu) + tools_menu.add_cascade(label="Topology generator", menu=menu) def draw_tools_menu(self): """ @@ -422,41 +341,21 @@ class Menubar(tk.Menu): :return: nothing """ - tools_menu = tk.Menu(self) - tools_menu.add_command( - label="Auto rearrange all", - command=action.tools_auto_rearrange_all, - underline=0, - ) - tools_menu.add_command( - label="Auto rearrange selected", - command=action.tools_auto_rearrange_selected, - underline=0, - ) - tools_menu.add_separator() - tools_menu.add_command( - label="Align to grid", command=action.tools_align_to_grid, underline=0 - ) - tools_menu.add_separator() - tools_menu.add_command(label="Traffic...", command=action.tools_traffic) - tools_menu.add_command( - label="IP addresses...", command=action.tools_ip_addresses, underline=0 - ) - tools_menu.add_command( - label="MAC addresses...", command=action.tools_mac_addresses, underline=0 - ) - tools_menu.add_command( - label="Build hosts file...", - command=action.tools_build_hosts_file, - underline=0, - ) - tools_menu.add_command( - label="Renumber nodes...", command=action.tools_renumber_nodes, underline=0 - ) - self.create_experimental_menu(tools_menu) - self.create_topology_generator_menu(tools_menu) - tools_menu.add_command(label="Debugger...", command=action.tools_debugger) - self.add_cascade(label="Tools", menu=tools_menu, underline=0) + menu = tk.Menu(self) + menu.add_command(label="Auto rearrange all", state=tk.DISABLED) + menu.add_command(label="Auto rearrange selected", state=tk.DISABLED) + menu.add_separator() + menu.add_command(label="Align to grid", state=tk.DISABLED) + menu.add_separator() + menu.add_command(label="Traffic...", state=tk.DISABLED) + menu.add_command(label="IP addresses...", state=tk.DISABLED) + menu.add_command(label="MAC addresses...", state=tk.DISABLED) + menu.add_command(label="Build hosts file...", state=tk.DISABLED) + menu.add_command(label="Renumber nodes...", state=tk.DISABLED) + self.create_experimental_menu(menu) + self.create_topology_generator_menu(menu) + menu.add_command(label="Debugger...", state=tk.DISABLED) + self.add_cascade(label="Tools", menu=menu) def create_observer_widgets_menu(self, widget_menu): """ @@ -465,54 +364,24 @@ class Menubar(tk.Menu): :param tkinter.Menu widget_menu: widget_menu :return: nothing """ - observer_widget_menu = tk.Menu(widget_menu) - observer_widget_menu.add_command(label="None", command=action.sub_menu_items) - observer_widget_menu.add_command( - label="processes", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="ifconfig", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPv4 routes", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPv6 routes", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="OSPFv2 neighbors", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="OSPFv3 neighbors", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="Listening sockets", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPv4 MFC entries", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPv6 MFC entries", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="firewall rules", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPsec policies", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="docker logs", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="OSPFv3 MDR level", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="PIM neighbors", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="Edit...", command=self.menuaction.edit_observer_widgets - ) - widget_menu.add_cascade(label="Observer Widgets", menu=observer_widget_menu) + menu = tk.Menu(widget_menu) + menu.add_command(label="None", state=tk.DISABLED) + menu.add_command(label="processes", state=tk.DISABLED) + menu.add_command(label="ifconfig", state=tk.DISABLED) + menu.add_command(label="IPv4 routes", state=tk.DISABLED) + menu.add_command(label="IPv6 routes", state=tk.DISABLED) + menu.add_command(label="OSPFv2 neighbors", state=tk.DISABLED) + menu.add_command(label="OSPFv3 neighbors", state=tk.DISABLED) + menu.add_command(label="Listening sockets", state=tk.DISABLED) + menu.add_command(label="IPv4 MFC entries", state=tk.DISABLED) + menu.add_command(label="IPv6 MFC entries", state=tk.DISABLED) + menu.add_command(label="firewall rules", state=tk.DISABLED) + menu.add_command(label="IPsec policies", state=tk.DISABLED) + menu.add_command(label="docker logs", state=tk.DISABLED) + menu.add_command(label="OSPFv3 MDR level", state=tk.DISABLED) + menu.add_command(label="PIM neighbors", state=tk.DISABLED) + menu.add_command(label="Edit...", command=self.menuaction.edit_observer_widgets) + widget_menu.add_cascade(label="Observer Widgets", menu=menu) def create_adjacency_menu(self, widget_menu): """ @@ -521,12 +390,12 @@ class Menubar(tk.Menu): :param tkinter.Menu widget_menu: widget menu :return: nothing """ - adjacency_menu = tk.Menu(widget_menu) - adjacency_menu.add_command(label="OSPFv2", command=action.sub_menu_items) - adjacency_menu.add_command(label="OSPFv3", command=action.sub_menu_items) - adjacency_menu.add_command(label="OSLR", command=action.sub_menu_items) - adjacency_menu.add_command(label="OSLRv2", command=action.sub_menu_items) - widget_menu.add_cascade(label="Adjacency", menu=adjacency_menu) + menu = tk.Menu(widget_menu) + menu.add_command(label="OSPFv2", state=tk.DISABLED) + menu.add_command(label="OSPFv3", state=tk.DISABLED) + menu.add_command(label="OSLR", state=tk.DISABLED) + menu.add_command(label="OSLRv2", state=tk.DISABLED) + widget_menu.add_cascade(label="Adjacency", menu=menu) def draw_widgets_menu(self): """ @@ -534,18 +403,14 @@ class Menubar(tk.Menu): :return: nothing """ - widget_menu = tk.Menu(self) - self.create_observer_widgets_menu(widget_menu) - self.create_adjacency_menu(widget_menu) - widget_menu.add_command(label="Throughput", command=action.widgets_throughput) - widget_menu.add_separator() - widget_menu.add_command( - label="Configure Adjacency...", command=action.widgets_configure_adjacency - ) - widget_menu.add_command( - label="Configure Throughput...", command=action.widgets_configure_throughput - ) - self.add_cascade(label="Widgets", menu=widget_menu, underline=0) + menu = tk.Menu(self) + self.create_observer_widgets_menu(menu) + self.create_adjacency_menu(menu) + menu.add_command(label="Throughput", state=tk.DISABLED) + menu.add_separator() + menu.add_command(label="Configure Adjacency...", state=tk.DISABLED) + menu.add_command(label="Configure Throughput...", state=tk.DISABLED) + self.add_cascade(label="Widgets", menu=menu) def draw_session_menu(self): """ @@ -553,36 +418,21 @@ class Menubar(tk.Menu): :return: nothing """ - session_menu = tk.Menu(self) - session_menu.add_command( + menu = tk.Menu(self) + menu.add_command( label="Change sessions...", command=self.menuaction.session_change_sessions, underline=0, ) - session_menu.add_separator() - session_menu.add_command( - label="Node types...", command=action.session_node_types, underline=0 + menu.add_separator() + menu.add_command(label="Comments...", state=tk.DISABLED) + menu.add_command(label="Hooks...", command=self.menuaction.session_hooks) + menu.add_command(label="Reset node positions", state=tk.DISABLED) + menu.add_command( + label="Emulation servers...", command=self.menuaction.session_servers ) - session_menu.add_command( - label="Comments...", command=action.session_comments, underline=0 - ) - session_menu.add_command( - label="Hooks...", command=self.menuaction.session_hooks, underline=0 - ) - session_menu.add_command( - label="Reset node positions", - command=action.session_reset_node_positions, - underline=0, - ) - session_menu.add_command( - label="Emulation servers...", - command=self.menuaction.session_servers, - underline=0, - ) - session_menu.add_command( - label="Options...", command=self.menuaction.session_options, underline=0 - ) - self.add_cascade(label="Session", menu=session_menu, underline=0) + menu.add_command(label="Options...", command=self.menuaction.session_options) + self.add_cascade(label="Session", menu=menu) def draw_help_menu(self): """ @@ -590,13 +440,13 @@ class Menubar(tk.Menu): :return: nothing """ - help_menu = tk.Menu(self) - help_menu.add_command( + menu = tk.Menu(self) + menu.add_command( label="Core Github (www)", command=self.menuaction.help_core_github ) - help_menu.add_command( + menu.add_command( label="Core Documentation (www)", command=self.menuaction.help_core_documentation, ) - help_menu.add_command(label="About", command=action.help_about) - self.add_cascade(label="Help", menu=help_menu) + menu.add_command(label="About", state=tk.DISABLED) + self.add_cascade(label="Help", menu=menu) From b0fe5660bd8b607c0fd0925a9cd0a07b33a39ccc Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 8 Nov 2019 11:00:22 -0800 Subject: [PATCH 204/462] updates to draw custom nodes on the node picker frame --- coretk/coretk/app.py | 3 +- coretk/coretk/coreclient.py | 12 +++---- coretk/coretk/dialogs/customnodes.py | 2 +- coretk/coretk/toolbar.py | 50 ++++++++++++++++------------ 4 files changed, 37 insertions(+), 30 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 1297c622..7330f21a 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -72,7 +72,8 @@ class Application(tk.Frame): if __name__ == "__main__": - logging.basicConfig(level=logging.DEBUG) + log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s" + logging.basicConfig(level=logging.DEBUG, format=log_format) appdirs.check_directory() app = Application() app.mainloop() diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index f018a348..ccb47176 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -13,8 +13,8 @@ from coretk.interface import Interface, InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.wlannodeconfig import WlanNodeConfig -link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel", "emane"] -network_layer_nodes = ["router", "host", "PC", "mdr", "prouter"] +NETWORK_NODES = ["switch", "hub", "wlan", "rj45", "tunnel", "emane"] +DEFAULT_NODES = ["router", "host", "PC", "mdr", "prouter"] class Node: @@ -422,7 +422,7 @@ class CoreClient: """ node_type = None node_model = None - if name in link_layer_nodes: + if name in NETWORK_NODES: if name == "switch": node_type = core_pb2.NodeType.SWITCH elif name == "hub": @@ -437,7 +437,7 @@ class CoreClient: node_type = core_pb2.NodeType.TUNNEL elif name == "emane": node_type = core_pb2.NodeType.EMANE - elif name in network_layer_nodes: + elif name in DEFAULT_NODES or name in self.custom_nodes: node_type = core_pb2.NodeType.DEFAULT node_model = name else: @@ -606,7 +606,7 @@ class CoreClient: self.interfaces_manager.new_subnet() src_node = self.nodes[src_canvas_id] - if src_node.model in network_layer_nodes: + if src_node.model in DEFAULT_NODES: ifid = len(src_node.interfaces) name = "eth" + str(ifid) src_interface = Interface( @@ -620,7 +620,7 @@ class CoreClient: ) dst_node = self.nodes[dst_canvas_id] - if dst_node.model in network_layer_nodes: + if dst_node.model in DEFAULT_NODES: ifid = len(dst_node.interfaces) name = "eth" + str(ifid) dst_interface = Interface( diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index f2423bf0..e98b8c77 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -208,7 +208,7 @@ class CustomNodesDialog(Dialog): custom_node = self.app.core.custom_nodes.pop(previous_name) custom_node.name = name custom_node.image = self.image - custom_node.image_file = Path(self.image_file).name + custom_node.image_file = Path(self.image_file).stem custom_node.services = self.services self.app.core.custom_nodes[name] = custom_node self.nodes_list.listbox.delete(self.selected_index) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 298cc24a..ebdb4d39 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -145,15 +145,23 @@ class Toolbar(tk.Frame): (ImageEnum.PC, "PC"), (ImageEnum.MDR, "mdr"), (ImageEnum.PROUTER, "prouter"), - (ImageEnum.EDITNODE, "custom node types"), ] + # draw default nodes for image_enum, tooltip in nodes: - self.create_button( - Images.get(image_enum), - partial(self.update_button, self.node_button, image_enum, tooltip), - self.node_picker, - tooltip, - ) + image = Images.get(image_enum) + func = partial(self.update_button, self.node_button, image, tooltip) + self.create_button(image, func, self.node_picker, tooltip) + # draw custom nodes + for name in sorted(self.app.core.custom_nodes): + custom_node = self.app.core.custom_nodes[name] + image = custom_node.image + func = partial(self.update_button, self.node_button, image, name) + self.create_button(image, func, self.node_picker, name) + # draw edit node + image = Images.get(ImageEnum.EDITNODE) + self.create_button( + image, self.click_edit_node, self.node_picker, "custom nodes" + ) self.show_picker(self.node_button, self.node_picker) def show_picker(self, button, picker): @@ -165,17 +173,17 @@ class Toolbar(tk.Frame): self.wait_window(picker) self.app.unbind_all("") - def create_button(self, img, func, frame, tooltip): + def create_button(self, image, func, frame, tooltip): """ Create button and put it on the frame - :param PIL.Image img: button image + :param PIL.Image image: button image :param func: the command that is executed when button is clicked :param tkinter.Frame frame: frame that contains the button :param str tooltip: tooltip text :return: nothing """ - button = tk.Button(frame, width=self.width, height=self.height, image=img) + button = tk.Button(frame, width=self.width, height=self.height, image=image) button.bind("", lambda e: func()) button.grid(pady=1) CreateToolTip(button, tooltip) @@ -221,19 +229,17 @@ class Toolbar(tk.Frame): logging.debug("Click LINK button") self.app.canvas.mode = GraphMode.EDGE - def update_button(self, button, image_enum, name): - logging.info("update button(%s): %s, %s", button, image_enum, name) + def click_edit_node(self): + dialog = CustomNodesDialog(self.app, self.app) + dialog.show() + + def update_button(self, button, image, name): + logging.info("update button(%s): %s", button, name) self.hide_pickers() - if image_enum == ImageEnum.EDITNODE: - dialog = CustomNodesDialog(self.app, self.app) - dialog.show() - else: - image = Images.get(image_enum) - logging.info("updating button(%s): %s", button, name) - button.configure(image=image) - self.app.canvas.mode = GraphMode.NODE - self.app.canvas.draw_node_image = image - self.app.canvas.draw_node_name = name + button.configure(image=image) + self.app.canvas.mode = GraphMode.NODE + self.app.canvas.draw_node_image = image + self.app.canvas.draw_node_name = name def hide_pickers(self): logging.info("hiding pickers") From 3306dbbfae4a66e4bafaedbf10b4c2dc3c302d59 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 8 Nov 2019 15:43:58 -0800 Subject: [PATCH 205/462] updates to allow custom nodes to be linked with interfaces --- coretk/coretk/coreclient.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index ccb47176..54e6a75b 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -13,8 +13,8 @@ from coretk.interface import Interface, InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.wlannodeconfig import WlanNodeConfig -NETWORK_NODES = ["switch", "hub", "wlan", "rj45", "tunnel", "emane"] -DEFAULT_NODES = ["router", "host", "PC", "mdr", "prouter"] +NETWORK_NODES = {"switch", "hub", "wlan", "rj45", "tunnel", "emane"} +DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"} class Node: @@ -409,6 +409,9 @@ class CoreClient: else: return self.reusable.pop(0) + def is_model_node(self, name): + return name in DEFAULT_NODES or name in self.custom_nodes + def add_graph_node(self, session_id, canvas_id, x, y, name): """ Add node, with information filled in, to grpc manager @@ -437,7 +440,7 @@ class CoreClient: node_type = core_pb2.NodeType.TUNNEL elif name == "emane": node_type = core_pb2.NodeType.EMANE - elif name in DEFAULT_NODES or name in self.custom_nodes: + elif self.is_model_node(name): node_type = core_pb2.NodeType.DEFAULT node_model = name else: @@ -606,7 +609,7 @@ class CoreClient: self.interfaces_manager.new_subnet() src_node = self.nodes[src_canvas_id] - if src_node.model in DEFAULT_NODES: + if self.is_model_node(src_node.model): ifid = len(src_node.interfaces) name = "eth" + str(ifid) src_interface = Interface( @@ -620,7 +623,7 @@ class CoreClient: ) dst_node = self.nodes[dst_canvas_id] - if dst_node.model in DEFAULT_NODES: + if self.is_model_node(dst_node.model): ifid = len(dst_node.interfaces) name = "eth" + str(ifid) dst_interface = Interface( From 78f44e81109c388c0ce759fc3a0f451ab00893e6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 8 Nov 2019 16:21:36 -0800 Subject: [PATCH 206/462] updates to toolbar event handling to eliminate events being sent to other components when displaying pickers --- coretk/coretk/toolbar.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index ebdb4d39..015d2ad6 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -83,8 +83,8 @@ class Toolbar(tk.Frame): "link tool", ) self.create_node_button() - self.create_link_layer_button() - self.create_marker_button() + self.create_network_button() + self.create_annotation_button() self.radio_value.set(1) def draw_runtime_frame(self): @@ -169,9 +169,11 @@ class Toolbar(tk.Frame): x = button.winfo_rootx() - first_button.winfo_rootx() + 40 y = button.winfo_rooty() - first_button.winfo_rooty() - 1 picker.place(x=x, y=y) - self.app.bind_all("", lambda e: self.hide_pickers()) + self.app.bind_all("", lambda e: self.hide_pickers()) + picker.wait_visibility() + picker.grab_set() self.wait_window(picker) - self.app.unbind_all("") + self.app.unbind_all("") def create_button(self, image, func, frame, tooltip): """ @@ -184,7 +186,7 @@ class Toolbar(tk.Frame): :return: nothing """ button = tk.Button(frame, width=self.width, height=self.height, image=image) - button.bind("", lambda e: func()) + button.bind("", lambda e: func()) button.grid(pady=1) CreateToolTip(button, tooltip) @@ -268,8 +270,8 @@ class Toolbar(tk.Frame): width=self.width, height=self.height, image=router_image, - command=self.draw_node_picker, ) + self.node_button.bind("", lambda e: self.draw_node_picker()) self.node_button.grid() CreateToolTip(self.node_button, "Network-layer virtual nodes") @@ -291,15 +293,16 @@ class Toolbar(tk.Frame): (ImageEnum.TUNNEL, "tunnel", "tunnel tool"), ] for image_enum, name, tooltip in nodes: + image = Images.get(image_enum) self.create_button( - Images.get(image_enum), - partial(self.update_button, self.network_button, image_enum, name), + image, + partial(self.update_button, self.network_button, image, name), self.network_picker, tooltip, ) self.show_picker(self.network_button, self.network_picker) - def create_link_layer_button(self): + def create_network_button(self): """ Create link-layer node button and the options that represent different link-layer node types @@ -314,7 +317,9 @@ class Toolbar(tk.Frame): width=self.width, height=self.height, image=hub_image, - command=self.draw_network_picker, + ) + self.network_button.bind( + "", lambda e: self.draw_network_picker() ) self.network_button.grid() CreateToolTip(self.network_button, "link-layer nodes") @@ -343,7 +348,7 @@ class Toolbar(tk.Frame): ) self.show_picker(self.annotation_button, self.annotation_picker) - def create_marker_button(self): + def create_annotation_button(self): """ Create marker button and options that represent different marker types @@ -358,7 +363,9 @@ class Toolbar(tk.Frame): width=self.width, height=self.height, image=marker_image, - command=self.draw_annotation_picker, + ) + self.annotation_button.bind( + "", lambda e: self.draw_annotation_picker() ) self.annotation_button.grid() CreateToolTip(self.annotation_button, "background annotation tools") From 5bf0a2ac05bb0958e48270d1262b769caf31963b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 8 Nov 2019 16:37:00 -0800 Subject: [PATCH 207/462] fixed service editing for custom nodes, fixed hiding picker when clicking edit custom nodes --- coretk/coretk/dialogs/customnodes.py | 7 ++++--- coretk/coretk/toolbar.py | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index e98b8c77..20c5ae95 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -15,7 +15,7 @@ class ServicesSelectDialog(Dialog): self.groups = None self.services = None self.current = None - self.current_services = current_services + self.current_services = set(current_services) self.draw() def draw(self): @@ -48,7 +48,7 @@ class ServicesSelectDialog(Dialog): frame.grid(stick="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save", command=self.click_cancel) + button = tk.Button(frame, text="Save", command=self.destroy) button.grid(row=0, column=0, sticky="ew") button = tk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=1, sticky="ew") @@ -174,7 +174,8 @@ class CustomNodesDialog(Dialog): dialog = ServicesSelectDialog(self, self.app, self.services) dialog.show() if dialog.current_services is not None: - self.services = dialog.current_services + self.services.clear() + self.services.update(dialog.current_services) def click_save(self): self.app.config["nodes"].clear() diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 015d2ad6..5b2c83bd 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -232,6 +232,7 @@ class Toolbar(tk.Frame): self.app.canvas.mode = GraphMode.EDGE def click_edit_node(self): + self.hide_pickers() dialog = CustomNodesDialog(self.app, self.app) dialog.show() From b88abd0f74281b846cdedbd2c35a1a5c54d2e22b Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 10 Nov 2019 17:26:38 -0800 Subject: [PATCH 208/462] added initial code that can help support canvas tooltips for observer widgets --- coretk/coretk/canvastooltip.py | 134 +++++++++++++++++++++++++++++++++ coretk/coretk/graph.py | 2 + 2 files changed, 136 insertions(+) create mode 100644 coretk/coretk/canvastooltip.py diff --git a/coretk/coretk/canvastooltip.py b/coretk/coretk/canvastooltip.py new file mode 100644 index 00000000..42270809 --- /dev/null +++ b/coretk/coretk/canvastooltip.py @@ -0,0 +1,134 @@ +import tkinter as tk +from tkinter import ttk + + +class CanvasTooltip: + """ + It creates a tooltip for a given canvas tag or id as the mouse is + above it. + + This class has been derived from the original Tooltip class updated + and posted back to StackOverflow at the following link: + + https://stackoverflow.com/questions/3221956/ + what-is-the-simplest-way-to-make-tooltips-in-tkinter/ + 41079350#41079350 + + Alberto Vassena on 2016.12.10. + """ + + def __init__( + self, + canvas, + tag_or_id, + *, + bg="#FFFFEA", + pad=(5, 3, 5, 3), + text="canvas info", + waittime=400, + wraplength=250 + ): + # in miliseconds, originally 500 + self.waittime = waittime + # in pixels, originally 180 + self.wraplength = wraplength + self.canvas = canvas + self.text = text + self.canvas.tag_bind(tag_or_id, "", self.on_enter) + self.canvas.tag_bind(tag_or_id, "", self.on_leave) + self.canvas.tag_bind(tag_or_id, "", self.on_leave) + self.bg = bg + self.pad = pad + self.id = None + self.tw = None + + def on_enter(self, event=None): + self.schedule() + + def on_leave(self, event=None): + self.unschedule() + self.hide() + + def schedule(self): + self.unschedule() + self.id = self.canvas.after(self.waittime, self.show) + + def unschedule(self): + id_ = self.id + self.id = None + if id_: + self.canvas.after_cancel(id_) + + def show(self, event=None): + def tip_pos_calculator(canvas, label, *, tip_delta=(10, 5), pad=(5, 3, 5, 3)): + + c = canvas + + s_width, s_height = c.winfo_screenwidth(), c.winfo_screenheight() + + width, height = ( + pad[0] + label.winfo_reqwidth() + pad[2], + pad[1] + label.winfo_reqheight() + pad[3], + ) + + mouse_x, mouse_y = c.winfo_pointerxy() + + x1, y1 = mouse_x + tip_delta[0], mouse_y + tip_delta[1] + x2, y2 = x1 + width, y1 + height + + x_delta = x2 - s_width + if x_delta < 0: + x_delta = 0 + y_delta = y2 - s_height + if y_delta < 0: + y_delta = 0 + + offscreen = (x_delta, y_delta) != (0, 0) + + if offscreen: + + if x_delta: + x1 = mouse_x - tip_delta[0] - width + + if y_delta: + y1 = mouse_y - tip_delta[1] - height + + offscreen_again = y1 < 0 # out on the top + + if offscreen_again: + y1 = 0 + + return x1, y1 + + bg = self.bg + pad = self.pad + canvas = self.canvas + + # creates a toplevel window + self.tw = tk.Toplevel(canvas.master) + + # Leaves only the label and removes the app window + self.tw.wm_overrideredirect(True) + + win = tk.Frame(self.tw, background=bg, borderwidth=0) + label = ttk.Label( + win, + text=self.text, + justify=tk.LEFT, + background=bg, + relief=tk.SOLID, + borderwidth=0, + wraplength=self.wraplength, + ) + + label.grid(padx=(pad[0], pad[2]), pady=(pad[1], pad[3]), sticky=tk.NSEW) + win.grid() + + x, y = tip_pos_calculator(canvas, label) + + self.tw.wm_geometry("+%d+%d" % (x, y)) + + def hide(self): + if self.tw: + self.tw.destroy() + self.tw = None diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 19491180..80360c44 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -4,6 +4,7 @@ import tkinter as tk from core.api.grpc import core_pb2 from coretk.canvasaction import CanvasAction +from coretk.canvastooltip import CanvasTooltip from coretk.graph_helper import GraphHelper, WlanAntennaManager from coretk.images import Images from coretk.interface import Interface @@ -512,6 +513,7 @@ class CanvasNode: self.canvas.tag_bind(self.id, "", self.context) self.canvas.tag_bind(self.id, "", self.double_click) self.canvas.tag_bind(self.id, "", self.select_multiple) + self.tooltip = CanvasTooltip(self.canvas, self.id, text=self.name) self.edges = set() self.wlans = [] From 18c9904d58652491cbb3d30b7fae6ca87f2ad930 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 Nov 2019 10:19:30 -0800 Subject: [PATCH 209/462] modified grpc set node service and node service file to use messages for their config, updated start session to leverage these messages to set them when starting a session --- daemon/core/api/grpc/client.py | 25 ++++++++++------- daemon/core/api/grpc/grpcutils.py | 15 +++++++++++ daemon/core/api/grpc/server.py | 20 +++++++++----- daemon/proto/core/api/grpc/core.proto | 28 ++++++++++++------- daemon/tests/test_grpc.py | 39 ++++++++++++++++++--------- 5 files changed, 90 insertions(+), 37 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 01a7b1f6..ceec0448 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -159,6 +159,8 @@ class CoreGrpcClient: emane_model_configs=None, wlan_configs=None, mobility_configs=None, + service_configs=None, + service_file_configs=None, ): """ Start a session. @@ -169,9 +171,11 @@ class CoreGrpcClient: :param core_pb2.SessionLocation location: location to set :param list[core_pb2.Hook] hooks: session hooks to set :param dict emane_config: emane configuration to set - :param list emane_model_configs: emane model configurations to set - :param list wlan_configs: wlan configurations to set - :param list mobility_configs: mobility configurations to set + :param list emane_model_configs: node emane model configurations + :param list wlan_configs: node wlan configurations + :param list mobility_configs: node mobility configurations + :param list service_configs: node service configurations + :param list service_file_configs: node service file configurations :return: start session response :rtype: core_pb2.StartSessionResponse """ @@ -185,6 +189,8 @@ class CoreGrpcClient: emane_model_configs=emane_model_configs, wlan_configs=wlan_configs, mobility_configs=mobility_configs, + service_configs=service_configs, + service_file_configs=service_file_configs, ) return self.stub.StartSession(request) @@ -768,14 +774,14 @@ class CoreGrpcClient: :rtype: core_pb2.SetNodeServiceResponse :raises grpc.RpcError: when session or node doesn't exist """ - request = core_pb2.SetNodeServiceRequest( - session_id=session_id, + config = core_pb2.ServiceConfig( node_id=node_id, service=service, startup=startup, validate=validate, shutdown=shutdown, ) + request = core_pb2.SetNodeServiceRequest(session_id=session_id, config=config) return self.stub.SetNodeService(request) def set_node_service_file(self, session_id, node_id, service, file_name, data): @@ -791,12 +797,11 @@ class CoreGrpcClient: :rtype: core_pb2.SetNodeServiceFileResponse :raises grpc.RpcError: when session or node doesn't exist """ + config = core_pb2.ServiceFileConfig( + node_id=node_id, service=service, file=file_name, data=data + ) request = core_pb2.SetNodeServiceFileRequest( - session_id=session_id, - node_id=node_id, - service=service, - file=file_name, - data=data, + session_id=session_id, config=config ) return self.stub.SetNodeServiceFile(request) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 166807d0..c6b625fb 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -319,3 +319,18 @@ def session_location(session, location): session.location.refxyz = (location.x, location.y, location.z) session.location.setrefgeo(location.lat, location.lon, location.alt) session.location.refscale = location.scale + + +def service_configuration(session, config): + """ + Convenience method for setting a node service configuration. + + :param core.emulator.session.Session session: session for service configuration + :param core_pb2.ServiceConfig config: service configuration + :return: + """ + session.services.set_service(config.node_id, config.service) + service = session.services.get_service(config.node_id, config.service) + service.startup = tuple(config.startup) + service.validate = tuple(config.validate) + service.shutdown = tuple(config.shutdown) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index e1e1f24d..c00cc3af 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -153,6 +153,16 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): config.node_id, Ns2ScriptedMobility.name, config.config ) + # service configs + for config in request.service_configs: + grpcutils.service_configuration(session, config) + + # service file configs + for config in request.service_file_configs: + session.services.set_service_file( + config.node_id, config.service, config.file, config.data + ) + # create links grpcutils.create_links(session, request.links) @@ -1172,11 +1182,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("set node service: %s", request) session = self.get_session(request.session_id, context) - session.services.set_service(request.node_id, request.service) - service = session.services.get_service(request.node_id, request.service) - service.startup = tuple(request.startup) - service.validate = tuple(request.validate) - service.shutdown = tuple(request.shutdown) + config = request.config + grpcutils.service_configuration(session, config) return core_pb2.SetNodeServiceResponse(result=True) def SetNodeServiceFile(self, request, context): @@ -1191,8 +1198,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("set node service file: %s", request) session = self.get_session(request.session_id, context) + config = request.config session.services.set_service_file( - request.node_id, request.service, request.file, request.data + config.node_id, config.service, config.file, config.data ) return core_pb2.SetNodeServiceFileResponse(result=True) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index a53a11bb..ac7cc2ed 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -144,6 +144,8 @@ message StartSessionRequest { repeated WlanConfig wlan_configs = 7; repeated EmaneModelConfig emane_model_configs = 8; repeated MobilityConfig mobility_configs = 9; + repeated ServiceConfig service_configs = 10; + repeated ServiceFileConfig service_file_configs = 11; } message StartSessionResponse { @@ -554,11 +556,7 @@ message GetNodeServiceFileResponse { message SetNodeServiceRequest { int32 session_id = 1; - int32 node_id = 2; - string service = 3; - repeated string startup = 4; - repeated string validate = 5; - repeated string shutdown = 6; + ServiceConfig config = 2; } message SetNodeServiceResponse { @@ -567,10 +565,7 @@ message SetNodeServiceResponse { message SetNodeServiceFileRequest { int32 session_id = 1; - int32 node_id = 2; - string service = 3; - string file = 4; - string data = 5; + ServiceFileConfig config = 2; } message SetNodeServiceFileResponse { @@ -718,6 +713,21 @@ message EmaneModelConfig { map config = 4; } +message ServiceConfig { + int32 node_id = 1; + string service = 2; + repeated string startup = 3; + repeated string validate = 4; + repeated string shutdown = 5; +} + +message ServiceFileConfig { + int32 node_id = 1; + string service = 2; + string file = 3; + string data = 4; +} + message MessageType { enum Enum { NONE = 0; diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 62ff3a22..5f934b64 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -27,7 +27,6 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - nodes = [] position = core_pb2.Position(x=50, y=100) node_one = core_pb2.Node(id=1, position=position, model="PC") position = core_pb2.Position(x=100, y=100) @@ -36,8 +35,7 @@ class TestGrpc: wlan_node = core_pb2.Node( id=3, type=NodeTypes.WIRELESS_LAN.value, position=position ) - nodes.extend([node_one, node_two, wlan_node]) - links = [] + nodes = [node_one, node_two, wlan_node] interface_helper = InterfaceHelper(ip4_prefix="10.83.0.0/16") interface_one = interface_helper.create_interface(node_one.id, 0) interface_two = interface_helper.create_interface(node_two.id, 0) @@ -48,12 +46,11 @@ class TestGrpc: interface_one=interface_one, interface_two=interface_two, ) - links.append(link) - hooks = [] + links = [link] hook = core_pb2.Hook( state=core_pb2.SessionState.RUNTIME, file="echo.sh", data="echo hello" ) - hooks.append(hook) + hooks = [hook] location_x = 5 location_y = 10 location_z = 15 @@ -73,7 +70,6 @@ class TestGrpc: emane_config_key = "platform_id_start" emane_config_value = "2" emane_config = {emane_config_key: emane_config_value} - model_configs = [] model_node_id = 20 model_config_key = "bandwidth" model_config_value = "500000" @@ -83,21 +79,30 @@ class TestGrpc: model=EmaneIeee80211abgModel.name, config={model_config_key: model_config_value}, ) - model_configs.append(model_config) - wlan_configs = [] + model_configs = [model_config] wlan_config_key = "range" wlan_config_value = "333" wlan_config = core_pb2.WlanConfig( node_id=wlan_node.id, config={wlan_config_key: wlan_config_value} ) - wlan_configs.append(wlan_config) + wlan_configs = [wlan_config] mobility_config_key = "refresh_ms" mobility_config_value = "60" - mobility_configs = [] mobility_config = core_pb2.MobilityConfig( node_id=wlan_node.id, config={mobility_config_key: mobility_config_value} ) - mobility_configs.append(mobility_config) + mobility_configs = [mobility_config] + service_config = core_pb2.ServiceConfig( + node_id=node_one.id, service="DefaultRoute", validate=["echo hello"] + ) + service_configs = [service_config] + service_file_config = core_pb2.ServiceFileConfig( + node_id=node_one.id, + service="DefaultRoute", + file="defaultroute.sh", + data="echo hello", + ) + service_file_configs = [service_file_config] # when with patch.object(CoreXmlWriter, "write"): @@ -112,6 +117,8 @@ class TestGrpc: model_configs, wlan_configs, mobility_configs, + service_configs, + service_file_configs, ) # then @@ -139,6 +146,14 @@ class TestGrpc: model_node_id, EmaneIeee80211abgModel.name ) assert set_model_config[model_config_key] == model_config_value + service = session.services.get_service( + node_one.id, service_config.service, default_service=True + ) + assert service.validate == tuple(service_config.validate) + service_file = session.services.get_service_file( + node_one, service_file_config.service, service_file_config.file + ) + assert service_file.data == service_file_config.data @pytest.mark.parametrize("session_id", [None, 6013]) def test_create_session(self, grpc_server, session_id): From 13a7aa73a1197ff18927b8d427683cd2d64436fb Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 11 Nov 2019 10:57:26 -0800 Subject: [PATCH 210/462] draw node service configurations --- coretk/coretk/coreclient.py | 15 ++ coretk/coretk/dialogs/nodeconfig.py | 4 +- coretk/coretk/dialogs/nodeservice.py | 202 +++++++----------- coretk/coretk/dialogs/serviceconfiguration.py | 185 ++++++++++++++++ coretk/coretk/servicenodeconfig.py | 37 ++++ 5 files changed, 312 insertions(+), 131 deletions(-) create mode 100644 coretk/coretk/dialogs/serviceconfiguration.py create mode 100644 coretk/coretk/servicenodeconfig.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index f018a348..05a2b1e9 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -11,6 +11,7 @@ from coretk.emaneodelnodeconfig import EmaneModelNodeConfig from coretk.images import Images from coretk.interface import Interface, InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig +from coretk.servicenodeconfig import ServiceNodeConfig from coretk.wlannodeconfig import WlanNodeConfig link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel", "emane"] @@ -104,6 +105,7 @@ class CoreClient: self.mobilityconfig_management = MobilityNodeConfig() self.emaneconfig_management = EmaneModelNodeConfig(app) self.emane_config = None + self.serviceconfig_manager = ServiceNodeConfig(app) def read_config(self): # read distributed server @@ -327,6 +329,13 @@ class CoreClient: ) logging.debug("Start session %s, result: %s", self.session_id, response.result) + response = self.client.get_service_defaults(self.session_id) + for default in response.defaults: + print(default.node_type) + print(default.services) + response = self.client.get_node_service(self.session_id, 5, "FTP") + print(response) + def stop_session(self): response = self.client.stop_session(session_id=self.session_id) logging.debug("coregrpc.py Stop session, result: %s", response.result) @@ -453,6 +462,12 @@ class CoreClient: if node_type == core_pb2.NodeType.EMANE: self.emaneconfig_management.set_default_config(nid) + # set default service configurations + if node_type == core_pb2.NodeType.DEFAULT: + self.serviceconfig_manager.node_default_services_configuration( + node_id=nid, node_model=node_model + ) + self.nodes[canvas_id] = create_node self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) logging.debug( diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 3f13488a..89dd7d11 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -3,7 +3,7 @@ from tkinter import ttk from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog -from coretk.dialogs.nodeservice import NodeServicesDialog +from coretk.dialogs.nodeservice import NodeService NETWORKNODETYPES = ["switch", "hub", "wlan", "rj45", "tunnel"] DEFAULTNODES = ["router", "host", "PC"] @@ -87,7 +87,7 @@ class NodeConfigDialog(Dialog): button.grid(row=0, column=1, sticky="ew") def click_services(self): - dialog = NodeServicesDialog(self, self.app, self.canvas_node) + dialog = NodeService(self, self.app, self.canvas_node) dialog.show() def click_icon(self): diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 6d92b98e..52421ca0 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -5,154 +5,98 @@ import tkinter as tk from tkinter import messagebox from coretk.dialogs.dialog import Dialog +from coretk.dialogs.serviceconfiguration import ServiceConfiguration +from coretk.widgets import CheckboxList, ListboxScroll -class NodeServicesDialog(Dialog): - def __init__(self, master, app, canvas_node): +class NodeService(Dialog): + def __init__(self, master, app, canvas_node, current_services=set()): super().__init__(master, app, "Node Services", modal=True) self.canvas_node = canvas_node - self.core_groups = [] - self.service_to_config = None - self.config_frame = None - self.services_list = None + self.groups = None + self.services = None + self.current = None + self.current_services = current_services self.draw() def draw(self): self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) - self.config_frame = tk.Frame(self) - self.config_frame.columnconfigure(0, weight=1) - self.config_frame.columnconfigure(1, weight=1) - self.config_frame.columnconfigure(2, weight=1) - self.config_frame.rowconfigure(0, weight=1) - self.config_frame.grid(row=0, column=0, sticky="nsew") - self.draw_group() - self.draw_services() - self.draw_current_services() - self.draw_buttons() - def draw_group(self): - """ - draw the group tab - - :return: nothing - """ - frame = tk.Frame(self.config_frame) - frame.columnconfigure(0, weight=1) - frame.rowconfigure(1, weight=1) - frame.grid(row=0, column=0, padx=3, pady=3, sticky="nsew") - - label = tk.Label(frame, text="Group") - label.grid(row=0, column=0, sticky="ew") - - scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) - scrollbar.grid(row=1, column=1, sticky="ns") - - listbox = tk.Listbox( - frame, - selectmode=tk.SINGLE, - yscrollcommand=scrollbar.set, - relief=tk.FLAT, - highlightthickness=0.5, - bd=0, - ) - listbox.grid(row=1, column=0, sticky="nsew") - listbox.bind("<>", self.handle_group_change) - - for group in sorted(self.app.core.services): - listbox.insert(tk.END, group) - - scrollbar.config(command=listbox.yview) - - def draw_services(self): - frame = tk.Frame(self.config_frame) - frame.columnconfigure(0, weight=1) - frame.rowconfigure(1, weight=1) - frame.grid(row=0, column=1, padx=3, pady=3, sticky="nsew") - - label = tk.Label(frame, text="Group services") - label.grid(row=0, column=0, sticky="ew") - - scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) - scrollbar.grid(row=1, column=1, sticky="ns") - - self.services_list = tk.Listbox( - frame, - selectmode=tk.SINGLE, - yscrollcommand=scrollbar.set, - relief=tk.FLAT, - highlightthickness=0.5, - bd=0, - ) - self.services_list.grid(row=1, column=0, sticky="nsew") - self.services_list.bind("<>", self.handle_service_change) - - scrollbar.config(command=self.services_list.yview) - - def draw_current_services(self): - frame = tk.Frame(self.config_frame) - frame.columnconfigure(0, weight=1) - frame.rowconfigure(1, weight=1) - frame.grid(row=0, column=2, padx=3, pady=3, sticky="nsew") - - label = tk.Label(frame, text="Current services") - label.grid(row=0, column=0, sticky="ew") - - scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) - scrollbar.grid(row=1, column=1, sticky="ns") - - listbox = tk.Listbox( - frame, - selectmode=tk.MULTIPLE, - yscrollcommand=scrollbar.set, - relief=tk.FLAT, - highlightthickness=0.5, - bd=0, - ) - listbox.grid(row=1, column=0, sticky="nsew") - - scrollbar.config(command=listbox.yview) - - def draw_buttons(self): frame = tk.Frame(self) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=1) - frame.grid(row=1, column=0, sticky="ew") + frame.grid(stick="nsew") + frame.rowconfigure(0, weight=1) + for i in range(3): + frame.columnconfigure(i, weight=1) + self.groups = ListboxScroll(frame, text="Groups") + self.groups.grid(row=0, column=0, sticky="nsew") + for group in sorted(self.app.core.services): + self.groups.listbox.insert(tk.END, group) + self.groups.listbox.bind("<>", self.handle_group_change) + self.groups.listbox.selection_set(0) + self.services = CheckboxList( + frame, text="Services", clicked=self.service_clicked + ) + self.services.grid(row=0, column=1, sticky="nsew") + + self.current = ListboxScroll(frame, text="Selected") + self.current.grid(row=0, column=2, sticky="nsew") + for service in sorted(self.current_services): + self.current.listbox.insert(tk.END, service) + + frame = tk.Frame(self) + frame.grid(stick="ew") + for i in range(3): + frame.columnconfigure(i, weight=1) button = tk.Button(frame, text="Configure", command=self.click_configure) button.grid(row=0, column=0, sticky="ew") - - button = tk.Button(frame, text="Apply") + button = tk.Button(frame, text="Save", command=self.click_save) button.grid(row=0, column=1, sticky="ew") - - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = tk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=2, sticky="ew") + # trigger group change + self.groups.listbox.event_generate("<>") + def handle_group_change(self, event): - listbox = event.widget - cur_selection = listbox.curselection() - if cur_selection: - s = listbox.get(listbox.curselection()) - self.display_group_services(s) + selection = self.groups.listbox.curselection() + if selection: + index = selection[0] + group = self.groups.listbox.get(index) + self.services.clear() + for service in sorted(self.app.core.services[group], key=lambda x: x.name): + checked = service.name in self.current_services + self.services.add(service.name, checked) - def display_group_services(self, group_name): - self.services_list.delete(0, tk.END) - for service in sorted(self.app.core.services[group_name], key=lambda x: x.name): - self.services_list.insert(tk.END, service.name) - - def handle_service_change(self, event): - print("select group service") - listbox = event.widget - cur_selection = listbox.curselection() - if cur_selection: - s = listbox.get(listbox.curselection()) - self.service_to_config = s - else: - self.service_to_config = None + def service_clicked(self, name, var): + if var.get() and name not in self.current_services: + self.current_services.add(name) + elif not var.get() and name in self.current_services: + self.current_services.remove(name) + self.current.listbox.delete(0, tk.END) + for name in sorted(self.current_services): + self.current.listbox.insert(tk.END, name) def click_configure(self): - if self.service_to_config is None: - messagebox.showinfo("CORE info", "Choose a service to configure.") + current_selection = self.current.listbox.curselection() + if len(current_selection): + dialog = ServiceConfiguration( + master=self, + app=self.app, + service_name=self.current.listbox.get(current_selection[0]), + canvas_node=self.canvas_node, + ) + dialog.show() else: - print(self.service_to_config) + messagebox.showinfo( + "Node service configuration", "Select a service to configure" + ) + + def click_save(self): + print("not implemented") + print(self.current_services) + + def click_cancel(self): + self.current_services = None + self.destroy() diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py new file mode 100644 index 00000000..e352a2e8 --- /dev/null +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -0,0 +1,185 @@ +"Service configuration dialog" + +import tkinter as tk +from tkinter import ttk + +from coretk.dialogs.dialog import Dialog +from coretk.images import ImageEnum, Images +from coretk.widgets import ListboxScroll + + +class ServiceConfiguration(Dialog): + def __init__(self, master, app, service_name, canvas_node): + super().__init__(master, app, service_name + " service", modal=True) + self.app = app + self.service_name = service_name + self.metadata = tk.StringVar() + self.filename = tk.StringVar() + self.radiovar = tk.IntVar() + self.radiovar.set(1) + self.startup_index = tk.IntVar() + self.start_time = tk.IntVar() + self.documentnew_img = Images.get(ImageEnum.DOCUMENTNEW) + self.editdelete_img = Images.get(ImageEnum.EDITDELETE) + self.draw() + + def draw(self): + # self.columnconfigure(1, weight=1) + frame = tk.Frame(self) + frame1 = tk.Frame(frame) + label = tk.Label(frame1, text=self.service_name) + label.grid(row=0, column=0, sticky="ew") + frame1.grid(row=0, column=0) + frame2 = tk.Frame(frame) + # frame2.columnconfigure(0, weight=1) + # frame2.columnconfigure(1, weight=4) + label = tk.Label(frame2, text="Meta-data") + label.grid(row=0, column=0) + entry = tk.Entry(frame2, textvariable=self.metadata) + entry.grid(row=0, column=1) + frame2.grid(row=1, column=0) + frame.grid(row=0, column=0) + + frame = tk.Frame(self) + tab_parent = ttk.Notebook(frame) + tab1 = ttk.Frame(tab_parent) + tab2 = ttk.Frame(tab_parent) + tab3 = ttk.Frame(tab_parent) + tab1.columnconfigure(0, weight=1) + tab2.columnconfigure(0, weight=1) + tab3.columnconfigure(0, weight=1) + + tab_parent.add(tab1, text="Files", sticky="nsew") + tab_parent.add(tab2, text="Directories", sticky="nsew") + tab_parent.add(tab3, text="Startup/shutdown", sticky="nsew") + tab_parent.grid(row=0, column=0, sticky="nsew") + frame.grid(row=1, column=0, sticky="nsew") + + # tab 1 + label = tk.Label( + tab1, text="Config files and scripts that are generated for this service." + ) + label.grid(row=0, column=0, sticky="nsew") + + frame = tk.Frame(tab1) + label = tk.Label(frame, text="File name: ") + label.grid(row=0, column=0) + entry = tk.Entry(frame, textvariable=self.filename) + entry.grid(row=0, column=1) + button = tk.Button(frame, image=self.documentnew_img) + button.grid(row=0, column=2) + button = tk.Button(frame, image=self.editdelete_img) + button.grid(row=0, column=3) + frame.grid(row=1, column=0, sticky="nsew") + + frame = tk.Frame(tab1) + button = tk.Radiobutton( + frame, + variable=self.radiovar, + text="Copy this source file:", + indicatoron=True, + ) + button.grid(row=0, column=0) + entry = tk.Entry(frame, state=tk.DISABLED) + entry.grid(row=0, column=1) + button = tk.Button(frame, text="not implemented") + button.grid(row=0, column=2) + frame.grid(row=2, column=0, sticky="nsew") + + frame = tk.Frame(tab1) + button = tk.Radiobutton( + frame, + variable=self.radiovar, + text="Use text below for file contents:", + indicatoron=True, + ) + button.grid(row=0, column=0) + button = tk.Button(frame, text="not implemented") + button.grid(row=0, column=1) + button = tk.Button(frame, text="not implemented") + button.grid(row=0, column=2) + frame.grid(row=3, column=0, sticky="nsew") + + # tab 2 + label = tk.Label( + tab2, + text="Directories required by this service that are unique for each node.", + ) + label.grid(row=0, column=0, sticky="nsew") + + # tab 3 + label_frame = tk.LabelFrame(tab3, text="Startup commands") + label_frame.columnconfigure(0, weight=1) + frame = tk.Frame(label_frame) + frame.columnconfigure(0, weight=1) + entry = tk.Entry(frame, textvariable=tk.StringVar()) + entry.grid(row=0, column=0, stick="nsew") + button = tk.Button(frame, image=self.documentnew_img) + button.grid(row=0, column=1, sticky="nsew") + button = tk.Button(frame, image=self.editdelete_img) + button.grid(row=0, column=2, sticky="nsew") + frame.grid(row=0, column=0, sticky="nsew") + listbox_scroll = ListboxScroll(label_frame) + listbox_scroll.listbox.config(height=4) + listbox_scroll.grid(row=1, column=0, sticky="nsew") + label_frame.grid(row=2, column=0, sticky="nsew") + + label_frame = tk.LabelFrame(tab3, text="Shutdown commands") + label_frame.columnconfigure(0, weight=1) + frame = tk.Frame(label_frame) + frame.columnconfigure(0, weight=1) + entry = tk.Entry(frame, textvariable=tk.StringVar()) + entry.grid(row=0, column=0, sticky="nsew") + button = tk.Button(frame, image=self.documentnew_img) + button.grid(row=0, column=1, sticky="nsew") + button = tk.Button(frame, image=self.editdelete_img) + button.grid(row=0, column=2, sticky="nsew") + frame.grid(row=0, column=0, sticky="nsew") + listbox_scroll = ListboxScroll(label_frame) + listbox_scroll.listbox.config(height=4) + listbox_scroll.grid(row=1, column=0, sticky="nsew") + label_frame.grid(row=3, column=0, sticky="nsew") + + label_frame = tk.LabelFrame(tab3, text="Validate commands") + label_frame.columnconfigure(0, weight=1) + frame = tk.Frame(label_frame) + frame.columnconfigure(0, weight=1) + entry = tk.Entry(frame, textvariable=tk.StringVar()) + entry.grid(row=0, column=0, sticky="nsew") + button = tk.Button(frame, image=self.documentnew_img) + button.grid(row=0, column=1, sticky="nsew") + button = tk.Button(frame, image=self.editdelete_img) + button.grid(row=0, column=2, sticky="nsew") + frame.grid(row=0, column=0, sticky="nsew") + listbox_scroll = ListboxScroll(label_frame) + listbox_scroll.listbox.config(height=4) + listbox_scroll.grid(row=1, column=0, sticky="nsew") + label_frame.grid(row=4, column=0, sticky="nsew") + + button = tk.Button( + self, text="onle store values that have changed from their defaults" + ) + button.grid(row=2, column=0) + + frame = tk.Frame(self) + button = tk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, sticky="nsew") + button = tk.Button(frame, text="Dafults", command=self.click_defaults) + button.grid(row=0, column=1, sticky="nsew") + button = tk.Button(frame, text="Copy...", command=self.click_copy) + button.grid(row=0, column=2, sticky="nsew") + button = tk.Button(frame, text="Cancel", command=self.click_cancel) + button.grid(row=0, column=3, sticky="nsew") + frame.grid(row=3, column=0) + + def click_apply(self, event): + print("not implemented") + + def click_defaults(self, event): + print("not implemented") + + def click_copy(self, event): + print("not implemented") + + def click_cancel(self, event): + print("not implemented") diff --git a/coretk/coretk/servicenodeconfig.py b/coretk/coretk/servicenodeconfig.py new file mode 100644 index 00000000..486e646d --- /dev/null +++ b/coretk/coretk/servicenodeconfig.py @@ -0,0 +1,37 @@ +""" +service node configuration +""" +import logging + + +class ServiceNodeConfig: + def __init__(self, app): + self.app = app + self.configurations = {} + self.default_services = {} + + def node_default_services_configuration(self, node_id, node_model): + """ + set the default configurations for the default services of a node + + :param coretk.graph.CanvasNode canvas_node: canvas node object + :return: nothing + """ + session_id = self.app.core.session_id + client = self.app.core.client + if len(self.default_services) == 0: + response = client.get_service_defaults(session_id) + logging.info("session default services: %s", response) + for default in response.defaults: + self.default_services[default.node_type] = default.services + + self.configurations[node_id] = {} + + for default in self.default_services[node_model]: + response = client.get_node_service(session_id, node_id, default) + logging.info( + "servicenodeconfig.py get node service (%s), result: %s", + node_id, + response, + ) + self.configurations[node_id][default] = response.service From aa718817d08c09c396d7b1ab60b9fe112620206e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 Nov 2019 13:23:02 -0800 Subject: [PATCH 211/462] updates to implement working observer widgets --- coretk/coretk/canvastooltip.py | 33 ++++-------------------------- coretk/coretk/coreclient.py | 32 ++++++++++++++++++++++++++--- coretk/coretk/graph.py | 31 ++++++++++++++++------------ coretk/coretk/menubar.py | 37 +++++++++++++++++++--------------- daemon/core/api/grpc/server.py | 4 ++-- 5 files changed, 74 insertions(+), 63 deletions(-) diff --git a/coretk/coretk/canvastooltip.py b/coretk/coretk/canvastooltip.py index 42270809..8ace41d0 100644 --- a/coretk/coretk/canvastooltip.py +++ b/coretk/coretk/canvastooltip.py @@ -18,25 +18,14 @@ class CanvasTooltip: """ def __init__( - self, - canvas, - tag_or_id, - *, - bg="#FFFFEA", - pad=(5, 3, 5, 3), - text="canvas info", - waittime=400, - wraplength=250 + self, canvas, *, bg="#FFFFEA", pad=(5, 3, 5, 3), waittime=400, wraplength=600 ): # in miliseconds, originally 500 self.waittime = waittime # in pixels, originally 180 self.wraplength = wraplength self.canvas = canvas - self.text = text - self.canvas.tag_bind(tag_or_id, "", self.on_enter) - self.canvas.tag_bind(tag_or_id, "", self.on_leave) - self.canvas.tag_bind(tag_or_id, "", self.on_leave) + self.text = tk.StringVar() self.bg = bg self.pad = pad self.id = None @@ -61,18 +50,13 @@ class CanvasTooltip: def show(self, event=None): def tip_pos_calculator(canvas, label, *, tip_delta=(10, 5), pad=(5, 3, 5, 3)): - c = canvas - s_width, s_height = c.winfo_screenwidth(), c.winfo_screenheight() - width, height = ( pad[0] + label.winfo_reqwidth() + pad[2], pad[1] + label.winfo_reqheight() + pad[3], ) - mouse_x, mouse_y = c.winfo_pointerxy() - x1, y1 = mouse_x + tip_delta[0], mouse_y + tip_delta[1] x2, y2 = x1 + width, y1 + height @@ -84,20 +68,14 @@ class CanvasTooltip: y_delta = 0 offscreen = (x_delta, y_delta) != (0, 0) - if offscreen: - if x_delta: x1 = mouse_x - tip_delta[0] - width - if y_delta: y1 = mouse_y - tip_delta[1] - height - offscreen_again = y1 < 0 # out on the top - if offscreen_again: y1 = 0 - return x1, y1 bg = self.bg @@ -111,21 +89,18 @@ class CanvasTooltip: self.tw.wm_overrideredirect(True) win = tk.Frame(self.tw, background=bg, borderwidth=0) + win.grid() label = ttk.Label( win, - text=self.text, + textvariable=self.text, justify=tk.LEFT, background=bg, relief=tk.SOLID, borderwidth=0, wraplength=self.wraplength, ) - label.grid(padx=(pad[0], pad[2]), pady=(pad[1], pad[3]), sticky=tk.NSEW) - win.grid() - x, y = tip_pos_calculator(canvas, label) - self.tw.wm_geometry("+%d+%d" % (x, y)) def hide(self): diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 54e6a75b..73a9dd15 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -15,6 +15,17 @@ from coretk.wlannodeconfig import WlanNodeConfig NETWORK_NODES = {"switch", "hub", "wlan", "rj45", "tunnel", "emane"} DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"} +OBSERVER_WIDGETS = { + "processes": "ps", + "ifconfig": "ifconfig", + "IPV4 Routes": "ip -4 ro", + "IPV6 Routes": "ip -6 ro", + "Listening sockets": "netstat -tuwnl", + "IPv4 MFC entries": "ip -4 mroute show", + "IPv6 MFC entries": "ip -6 mroute show", + "firewall rules": "iptables -L", + "IPSec policies": "setkey -DP", +} class Node: @@ -85,6 +96,7 @@ class CoreClient: self.master = app.master self.interface_helper = None self.services = {} + self.observer = None # loaded configuration data self.servers = {} @@ -92,6 +104,7 @@ class CoreClient: self.read_config() # data for managing the current session + self.state = None self.nodes = {} self.edges = {} self.hooks = {} @@ -105,6 +118,9 @@ class CoreClient: self.emaneconfig_management = EmaneModelNodeConfig(app) self.emane_config = None + def set_observer(self, value): + self.observer = value + def read_config(self): # read distributed server for server_config in self.app.config["servers"]: @@ -124,8 +140,11 @@ class CoreClient: def handle_events(self, event): logging.info("event: %s", event) - if event.link_event is not None: + if event.HasField("link_event"): self.app.canvas.wireless_draw.hangle_link_event(event.link_event) + elif event.HasField("session_event"): + if event.session_event.event <= core_pb2.SessionState.SHUTDOWN: + self.state = event.session_event.event def handle_throughputs(self, event): interface_throughputs = event.interface_throughputs @@ -159,7 +178,7 @@ class CoreClient: response = self.client.get_session(self.session_id) logging.info("joining session(%s): %s", self.session_id, response) session = response.session - session_state = session.state + self.state = session.state self.client.events(self.session_id, self.handle_events) # get hooks @@ -207,11 +226,14 @@ class CoreClient: self.app.canvas.canvas_reset_and_redraw(session) # draw tool bar appropritate with session state - if session_state == core_pb2.SessionState.RUNTIME: + if self.is_runtime(): self.app.toolbar.runtime_frame.tkraise() else: self.app.toolbar.design_frame.tkraise() + def is_runtime(self): + return self.state == core_pb2.SessionState.RUNTIME + def create_new_session(self): """ Create a new session @@ -754,3 +776,7 @@ class CoreClient: ) configs.append(config_proto) return configs + + def run(self, node_id): + logging.info("running node(%s) cmd: %s", node_id, self.observer) + return self.client.node_command(self.session_id, node_id, self.observer).output diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 80360c44..e813ae48 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -150,7 +150,7 @@ class CanvasGraph(tk.Canvas): node.type, node.model ) n = CanvasNode( - node.position.x, node.position.y, image, name, self, node.id + node.position.x, node.position.y, image, name, self.master, node.id ) self.nodes[n.id] = n core_id_to_canvas_id[node.id] = n.id @@ -419,14 +419,7 @@ class CanvasGraph(tk.Canvas): plot_id = self.find_all()[0] logging.info("add node event: %s - %s", plot_id, self.selected) if self.selected == plot_id: - node = CanvasNode( - x=x, - y=y, - image=image, - node_type=node_name, - canvas=self, - core_id=self.core.peek_id(), - ) + node = CanvasNode(x, y, image, node_name, self.master, self.core.peek_id()) self.nodes[node.id] = node self.core.add_graph_node(self.core.session_id, node.id, x, y, node_name) return node @@ -491,10 +484,11 @@ class CanvasEdge: class CanvasNode: - def __init__(self, x, y, image, node_type, canvas, core_id): + def __init__(self, x, y, image, node_type, app, core_id): self.image = image self.node_type = node_type - self.canvas = canvas + self.app = app + self.canvas = app.canvas self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags="node" ) @@ -506,19 +500,30 @@ class CanvasNode: x, y + 20, text=self.name, tags="nodename" ) self.antenna_draw = WlanAntennaManager(self.canvas, self.id) - + self.tooltip = CanvasTooltip(self.canvas) self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) self.canvas.tag_bind(self.id, "", self.motion) self.canvas.tag_bind(self.id, "", self.context) self.canvas.tag_bind(self.id, "", self.double_click) self.canvas.tag_bind(self.id, "", self.select_multiple) - self.tooltip = CanvasTooltip(self.canvas, self.id, text=self.name) + self.canvas.tag_bind(self.id, "", self.on_enter) + self.canvas.tag_bind(self.id, "", self.on_leave) self.edges = set() self.wlans = [] self.moving = None + def on_enter(self, event): + if self.app.core.is_runtime() and self.app.core.observer: + self.tooltip.text.set("waiting...") + self.tooltip.on_enter(event) + output = self.app.core.run(self.core_id) + self.tooltip.text.set(output) + + def on_leave(self, event): + self.tooltip.on_leave(event) + def click(self, event): print("click") diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index 3ef38d73..d590c091 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -1,6 +1,8 @@ import tkinter as tk +from functools import partial import coretk.menuaction as action +from coretk.coreclient import OBSERVER_WIDGETS class Menubar(tk.Menu): @@ -364,23 +366,26 @@ class Menubar(tk.Menu): :param tkinter.Menu widget_menu: widget_menu :return: nothing """ + var = tk.StringVar(value="none") menu = tk.Menu(widget_menu) - menu.add_command(label="None", state=tk.DISABLED) - menu.add_command(label="processes", state=tk.DISABLED) - menu.add_command(label="ifconfig", state=tk.DISABLED) - menu.add_command(label="IPv4 routes", state=tk.DISABLED) - menu.add_command(label="IPv6 routes", state=tk.DISABLED) - menu.add_command(label="OSPFv2 neighbors", state=tk.DISABLED) - menu.add_command(label="OSPFv3 neighbors", state=tk.DISABLED) - menu.add_command(label="Listening sockets", state=tk.DISABLED) - menu.add_command(label="IPv4 MFC entries", state=tk.DISABLED) - menu.add_command(label="IPv6 MFC entries", state=tk.DISABLED) - menu.add_command(label="firewall rules", state=tk.DISABLED) - menu.add_command(label="IPsec policies", state=tk.DISABLED) - menu.add_command(label="docker logs", state=tk.DISABLED) - menu.add_command(label="OSPFv3 MDR level", state=tk.DISABLED) - menu.add_command(label="PIM neighbors", state=tk.DISABLED) - menu.add_command(label="Edit...", command=self.menuaction.edit_observer_widgets) + menu.var = var + menu.add_radiobutton( + label="None", + variable=var, + value="none", + command=lambda: self.app.core.set_observer(None), + ) + for name in sorted(OBSERVER_WIDGETS): + cmd = OBSERVER_WIDGETS[name] + menu.add_radiobutton( + label=name, + variable=var, + value=name, + command=partial(self.app.core.set_observer, cmd), + ) + menu.add_radiobutton( + label="Edit...", command=self.menuaction.edit_observer_widgets + ) widget_menu.add_cascade(label="Observer Widgets", menu=menu) def create_adjacency_menu(self, widget_menu): diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index c00cc3af..8c987371 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -184,9 +184,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("stop session: %s", request) session = self.get_session(request.session_id, context) session.data_collect() - session.set_state(EventTypes.DATACOLLECT_STATE) + session.set_state(EventTypes.DATACOLLECT_STATE, send_event=True) session.clear() - session.set_state(EventTypes.SHUTDOWN_STATE) + session.set_state(EventTypes.SHUTDOWN_STATE, send_event=True) return core_pb2.StopSessionResponse(result=True) def CreateSession(self, request, context): From dd73c968301f448b9fe7a81c477c898b13f8d86f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 Nov 2019 13:33:37 -0800 Subject: [PATCH 212/462] added observers to gui config and display them within menu --- coretk/coretk/appdirs.py | 1 + coretk/coretk/coreclient.py | 9 +++++++-- coretk/coretk/menubar.py | 15 ++++++++++++--- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/coretk/coretk/appdirs.py b/coretk/coretk/appdirs.py index 553b0949..9eebd3a4 100644 --- a/coretk/coretk/appdirs.py +++ b/coretk/coretk/appdirs.py @@ -45,6 +45,7 @@ def check_directory(): config = { "servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}], "nodes": [], + "observers": [], } save_config(config) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 73a9dd15..8ae310c9 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -101,6 +101,7 @@ class CoreClient: # loaded configuration data self.servers = {} self.custom_nodes = {} + self.custom_observers = {} self.read_config() # data for managing the current session @@ -123,14 +124,14 @@ class CoreClient: def read_config(self): # read distributed server - for server_config in self.app.config["servers"]: + for server_config in self.app.config.get("servers", []): server = CoreServer( server_config["name"], server_config["address"], server_config["port"] ) self.servers[server.name] = server # read custom nodes - for node in self.app.config["nodes"]: + for node in self.app.config.get("nodes", []): image_file = node["image"] image = Images.get_custom(image_file) custom_node = CustomNode( @@ -138,6 +139,10 @@ class CoreClient: ) self.custom_nodes[custom_node.name] = custom_node + # read observers + for observer in self.app.config.get("observers", []): + self.custom_observers[observer["name"]] = observer["cmd"] + def handle_events(self, event): logging.info("event: %s", event) if event.HasField("link_event"): diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index d590c091..c39bd3f8 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -369,6 +369,10 @@ class Menubar(tk.Menu): var = tk.StringVar(value="none") menu = tk.Menu(widget_menu) menu.var = var + menu.add_command( + label="Edit Observers", command=self.menuaction.edit_observer_widgets + ) + menu.add_separator() menu.add_radiobutton( label="None", variable=var, @@ -383,9 +387,14 @@ class Menubar(tk.Menu): value=name, command=partial(self.app.core.set_observer, cmd), ) - menu.add_radiobutton( - label="Edit...", command=self.menuaction.edit_observer_widgets - ) + for name in sorted(self.app.core.custom_observers): + cmd = self.app.core.custom_observers[name] + menu.add_radiobutton( + label=name, + variable=var, + value=name, + command=partial(self.app.core.set_observer, cmd), + ) widget_menu.add_cascade(label="Observer Widgets", menu=menu) def create_adjacency_menu(self, widget_menu): From f5480c8e352cb3b224d1c45628090cdd8ae5f706 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 Nov 2019 14:04:50 -0800 Subject: [PATCH 213/462] updated observers dialog to create/edit/delete custom observers and save configuration --- coretk/coretk/coreclient.py | 23 +++-- .../{observerwidgets.py => observers.py} | 97 ++++++++++--------- coretk/coretk/dialogs/servers.py | 1 + coretk/coretk/menuaction.py | 4 +- coretk/coretk/menubar.py | 4 +- 5 files changed, 68 insertions(+), 61 deletions(-) rename coretk/coretk/dialogs/{observerwidgets.py => observers.py} (57%) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 8ae310c9..0285d60b 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -84,6 +84,12 @@ class CustomNode: self.services = services +class Observer: + def __init__(self, name, cmd): + self.name = name + self.cmd = cmd + + class CoreClient: def __init__(self, app): """ @@ -124,24 +130,23 @@ class CoreClient: def read_config(self): # read distributed server - for server_config in self.app.config.get("servers", []): - server = CoreServer( - server_config["name"], server_config["address"], server_config["port"] - ) + for config in self.app.config.get("servers", []): + server = CoreServer(config["name"], config["address"], config["port"]) self.servers[server.name] = server # read custom nodes - for node in self.app.config.get("nodes", []): - image_file = node["image"] + for config in self.app.config.get("nodes", []): + image_file = config["image"] image = Images.get_custom(image_file) custom_node = CustomNode( - node["name"], image, image_file, set(node["services"]) + config["name"], image, image_file, set(config["services"]) ) self.custom_nodes[custom_node.name] = custom_node # read observers - for observer in self.app.config.get("observers", []): - self.custom_observers[observer["name"]] = observer["cmd"] + for config in self.app.config.get("observers", []): + observer = Observer(config["name"], config["cmd"]) + self.custom_observers[observer.name] = observer def handle_events(self, event): logging.info("event: %s", event) diff --git a/coretk/coretk/dialogs/observerwidgets.py b/coretk/coretk/dialogs/observers.py similarity index 57% rename from coretk/coretk/dialogs/observerwidgets.py rename to coretk/coretk/dialogs/observers.py index c1c4d170..406f62be 100644 --- a/coretk/coretk/dialogs/observerwidgets.py +++ b/coretk/coretk/dialogs/observers.py @@ -1,36 +1,31 @@ import tkinter as tk +from coretk import appdirs +from coretk.coreclient import Observer from coretk.dialogs.dialog import Dialog -class Widget: - def __init__(self, name, command): - self.name = name - self.command = command - - -class ObserverWidgetsDialog(Dialog): +class ObserverDialog(Dialog): def __init__(self, master, app): super().__init__(master, app, "Observer Widgets", modal=True) - self.config_widgets = {} - self.widgets = None + self.observers = None self.save_button = None self.delete_button = None self.selected = None self.selected_index = None self.name = tk.StringVar() - self.command = tk.StringVar() + self.cmd = tk.StringVar() self.draw() def draw(self): self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) - self.draw_widgets() - self.draw_widget_fields() - self.draw_widget_buttons() + self.draw_listbox() + self.draw_form_fields() + self.draw_config_buttons() self.draw_apply_buttons() - def draw_widgets(self): + def draw_listbox(self): frame = tk.Frame(self) frame.grid(sticky="nsew") frame.columnconfigure(0, weight=1) @@ -39,15 +34,17 @@ class ObserverWidgetsDialog(Dialog): scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) scrollbar.grid(row=0, column=1, sticky="ns") - self.widgets = tk.Listbox( + self.observers = tk.Listbox( frame, selectmode=tk.SINGLE, yscrollcommand=scrollbar.set ) - self.widgets.grid(row=0, column=0, sticky="nsew") - self.widgets.bind("<>", self.handle_widget_change) + self.observers.grid(row=0, column=0, sticky="nsew") + self.observers.bind("<>", self.handle_observer_change) + for name in sorted(self.app.core.custom_observers): + self.observers.insert(tk.END, name) - scrollbar.config(command=self.widgets.yview) + scrollbar.config(command=self.observers.yview) - def draw_widget_fields(self): + def draw_form_fields(self): frame = tk.Frame(self) frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) @@ -59,10 +56,10 @@ class ObserverWidgetsDialog(Dialog): label = tk.Label(frame, text="Command") label.grid(row=1, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.command) + entry = tk.Entry(frame, textvariable=self.cmd) entry.grid(row=1, column=1, sticky="ew") - def draw_widget_buttons(self): + def draw_config_buttons(self): frame = tk.Frame(self) frame.grid(pady=2, sticky="ew") for i in range(3): @@ -87,58 +84,62 @@ class ObserverWidgetsDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button( - frame, text="Save Configuration", command=self.click_save_configuration - ) + button = tk.Button(frame, text="Save", command=self.click_save_config) button.grid(row=0, column=0, sticky="ew") button = tk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def click_save_configuration(self): - pass + def click_save_config(self): + observers = [] + for name in sorted(self.app.core.custom_observers): + observer = self.app.core.custom_observers[name] + observers.append({"name": observer.name, "cmd": observer.cmd}) + self.app.config["observers"] = observers + appdirs.save_config(self.app.config) + self.destroy() def click_create(self): name = self.name.get() - if name not in self.config_widgets: - command = self.command.get() - widget = Widget(name, command) - self.config_widgets[name] = widget - self.widgets.insert(tk.END, name) + if name not in self.app.core.custom_observers: + cmd = self.cmd.get() + observer = Observer(name, cmd) + self.app.core.custom_observers[name] = observer + self.observers.insert(tk.END, name) def click_save(self): name = self.name.get() if self.selected: previous_name = self.selected self.selected = name - widget = self.config_widgets.pop(previous_name) - widget.name = name - widget.command = self.command.get() - self.config_widgets[name] = widget - self.widgets.delete(self.selected_index) - self.widgets.insert(self.selected_index, name) - self.widgets.selection_set(self.selected_index) + observer = self.app.core.custom_observers.pop(previous_name) + observer.name = name + observer.cmd = self.cmd.get() + self.app.core.custom_observers[name] = observer + self.observers.delete(self.selected_index) + self.observers.insert(self.selected_index, name) + self.observers.selection_set(self.selected_index) def click_delete(self): if self.selected: - self.widgets.delete(self.selected_index) - del self.config_widgets[self.selected] + self.observers.delete(self.selected_index) + del self.app.core.custom_observers[self.selected] self.selected = None self.selected_index = None self.name.set("") - self.command.set("") - self.widgets.selection_clear(0, tk.END) + self.cmd.set("") + self.observers.selection_clear(0, tk.END) self.save_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED) - def handle_widget_change(self, event): - selection = self.widgets.curselection() + def handle_observer_change(self, event): + selection = self.observers.curselection() if selection: self.selected_index = selection[0] - self.selected = self.widgets.get(self.selected_index) - widget = self.config_widgets[self.selected] - self.name.set(widget.name) - self.command.set(widget.command) + self.selected = self.observers.get(self.selected_index) + observer = self.app.core.custom_observers[self.selected] + self.name.set(observer.name) + self.cmd.set(observer.cmd) self.save_button.config(state=tk.NORMAL) self.delete_button.config(state=tk.NORMAL) else: diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index 3ff95cde..ce6b9698 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -117,6 +117,7 @@ class ServersDialog(Dialog): ) self.app.config["servers"] = servers appdirs.save_config(self.app.config) + self.destroy() def click_create(self): name = self.name.get() diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 3b33377c..f1eb51e3 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -11,7 +11,7 @@ from coretk.appdirs import XML_PATH from coretk.dialogs.canvasbackground import CanvasBackgroundDialog from coretk.dialogs.canvassizeandscale import SizeAndScaleDialog from coretk.dialogs.hooks import HooksDialog -from coretk.dialogs.observerwidgets import ObserverWidgetsDialog +from coretk.dialogs.observers import ObserverDialog from coretk.dialogs.servers import ServersDialog from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog @@ -118,5 +118,5 @@ class MenuAction: dialog.show() def edit_observer_widgets(self): - dialog = ObserverWidgetsDialog(self.app, self.app) + dialog = ObserverDialog(self.app, self.app) dialog.show() diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index c39bd3f8..d482045a 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -388,12 +388,12 @@ class Menubar(tk.Menu): command=partial(self.app.core.set_observer, cmd), ) for name in sorted(self.app.core.custom_observers): - cmd = self.app.core.custom_observers[name] + observer = self.app.core.custom_observers[name] menu.add_radiobutton( label=name, variable=var, value=name, - command=partial(self.app.core.set_observer, cmd), + command=partial(self.app.core.set_observer, observer.cmd), ) widget_menu.add_cascade(label="Observer Widgets", menu=menu) From 691013eeb844db6644c837736fd581a8e2bdc611 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 Nov 2019 15:35:48 -0800 Subject: [PATCH 214/462] added preferences dialog, updates to try and simplify config saving a bit, adding default preferences configurations --- coretk/coretk/app.py | 9 ++- coretk/coretk/{appdirs.py => appconfig.py} | 37 ++++++++++-- coretk/coretk/coreclient.py | 2 +- coretk/coretk/dialogs/canvasbackground.py | 2 +- coretk/coretk/dialogs/customnodes.py | 4 +- coretk/coretk/dialogs/icondialog.py | 2 +- coretk/coretk/dialogs/observers.py | 3 +- coretk/coretk/dialogs/preferences.py | 67 ++++++++++++++++++++++ coretk/coretk/dialogs/servers.py | 3 +- coretk/coretk/images.py | 2 +- coretk/coretk/menuaction.py | 7 ++- coretk/coretk/menubar.py | 10 ++-- 12 files changed, 126 insertions(+), 22 deletions(-) rename coretk/coretk/{appdirs.py => appconfig.py} (70%) create mode 100644 coretk/coretk/dialogs/preferences.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 7330f21a..276dbd6d 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,7 +1,7 @@ import logging import tkinter as tk -from coretk import appdirs +from coretk import appconfig from coretk.coreclient import CoreClient from coretk.graph import CanvasGraph from coretk.images import ImageEnum, Images @@ -26,7 +26,7 @@ class Application(tk.Frame): self.radiovar = tk.IntVar(value=1) self.show_grid_var = tk.IntVar(value=1) self.adjust_to_dim_var = tk.IntVar(value=0) - self.config = appdirs.read_config() + self.config = appconfig.read() self.core = CoreClient(self) self.setup_app() self.draw() @@ -70,10 +70,13 @@ class Application(tk.Frame): menu_action = MenuAction(self, self.master) menu_action.on_quit() + def save_config(self): + appconfig.save(self.config) + if __name__ == "__main__": log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=log_format) - appdirs.check_directory() + appconfig.check_directory() app = Application() app.mainloop() diff --git a/coretk/coretk/appdirs.py b/coretk/coretk/appconfig.py similarity index 70% rename from coretk/coretk/appdirs.py rename to coretk/coretk/appconfig.py index 9eebd3a4..67f97181 100644 --- a/coretk/coretk/appdirs.py +++ b/coretk/coretk/appconfig.py @@ -1,4 +1,5 @@ import logging +import os import shutil from pathlib import Path @@ -18,6 +19,20 @@ CONFIG_PATH = HOME_PATH.joinpath("gui.yaml") LOCAL_ICONS_PATH = Path(__file__).parent.joinpath("icons").absolute() LOCAL_BACKGROUND_PATH = Path(__file__).parent.joinpath("backgrounds").absolute() +# configuration data +TERMINALS = [ + "$TERM", + "gnome-terminal --window --", + "lxterminal -e", + "konsole -e", + "xterm -e", + "aterm -e", + "eterm -e", + "rxvt -e", + "xfce4-terminal -x", +] +EDITORS = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"] + class IndentDumper(yaml.Dumper): def increase_indent(self, flow=False, indentless=False): @@ -42,19 +57,33 @@ def check_directory(): for background in LOCAL_BACKGROUND_PATH.glob("*"): new_background = BACKGROUNDS_PATH.joinpath(background.name) shutil.copy(background, new_background) + + if "TERM" in os.environ: + terminal = TERMINALS[0] + else: + terminal = TERMINALS[1] + if "EDITOR" in os.environ: + editor = EDITORS[0] + else: + editor = EDITORS[1] config = { + "preferences": { + "editor": editor, + "terminal": terminal, + "gui3d": "/usr/local/bin/std3d.sh", + }, "servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}], "nodes": [], - "observers": [], + "observers": [{"name": "hello", "cmd": "echo hello"}], } - save_config(config) + save(config) -def read_config(): +def read(): with CONFIG_PATH.open("r") as f: return yaml.load(f, Loader=yaml.SafeLoader) -def save_config(config): +def save(config): with CONFIG_PATH.open("w") as f: yaml.dump(config, f, Dumper=IndentDumper, default_flow_style=False) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 0285d60b..9e18a413 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -15,7 +15,7 @@ from coretk.wlannodeconfig import WlanNodeConfig NETWORK_NODES = {"switch", "hub", "wlan", "rj45", "tunnel", "emane"} DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"} -OBSERVER_WIDGETS = { +OBSERVERS = { "processes": "ps", "ifconfig": "ifconfig", "IPV4 Routes": "ip -4 ro", diff --git a/coretk/coretk/dialogs/canvasbackground.py b/coretk/coretk/dialogs/canvasbackground.py index fe9b5f6a..5f3b253f 100644 --- a/coretk/coretk/dialogs/canvasbackground.py +++ b/coretk/coretk/dialogs/canvasbackground.py @@ -8,7 +8,7 @@ from tkinter import filedialog from PIL import Image, ImageTk -from coretk.appdirs import BACKGROUNDS_PATH +from coretk.appconfig import BACKGROUNDS_PATH from coretk.dialogs.dialog import Dialog diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 20c5ae95..5caa19ff 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -2,7 +2,6 @@ import logging import tkinter as tk from pathlib import Path -from coretk import appdirs from coretk.coreclient import CustomNode from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog @@ -189,7 +188,8 @@ class CustomNodesDialog(Dialog): } ) logging.info("saving custom nodes: %s", self.app.config["nodes"]) - appdirs.save_config(self.app.config) + self.app.save_config() + self.destroy() def click_create(self): name = self.name.get() diff --git a/coretk/coretk/dialogs/icondialog.py b/coretk/coretk/dialogs/icondialog.py index 4d26e29f..bf7b08cf 100644 --- a/coretk/coretk/dialogs/icondialog.py +++ b/coretk/coretk/dialogs/icondialog.py @@ -1,7 +1,7 @@ import tkinter as tk from tkinter import filedialog -from coretk.appdirs import ICONS_PATH +from coretk.appconfig import ICONS_PATH from coretk.dialogs.dialog import Dialog from coretk.images import Images diff --git a/coretk/coretk/dialogs/observers.py b/coretk/coretk/dialogs/observers.py index 406f62be..6546493d 100644 --- a/coretk/coretk/dialogs/observers.py +++ b/coretk/coretk/dialogs/observers.py @@ -1,6 +1,5 @@ import tkinter as tk -from coretk import appdirs from coretk.coreclient import Observer from coretk.dialogs.dialog import Dialog @@ -96,7 +95,7 @@ class ObserverDialog(Dialog): observer = self.app.core.custom_observers[name] observers.append({"name": observer.name, "cmd": observer.cmd}) self.app.config["observers"] = observers - appdirs.save_config(self.app.config) + self.app.save_config() self.destroy() def click_create(self): diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py new file mode 100644 index 00000000..74293a30 --- /dev/null +++ b/coretk/coretk/dialogs/preferences.py @@ -0,0 +1,67 @@ +import tkinter as tk +from tkinter import ttk + +from coretk import appconfig +from coretk.dialogs.dialog import Dialog + + +class PreferencesDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Preferences", modal=True) + preferences = self.app.config["preferences"] + self.editor = tk.StringVar(value=preferences["editor"]) + self.terminal = tk.StringVar(value=preferences["terminal"]) + self.gui3d = tk.StringVar(value=preferences["gui3d"]) + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.draw_programs() + self.draw_buttons() + + def draw_programs(self): + frame = ttk.LabelFrame(self, text="Programs") + frame.grid(sticky="ew", pady=2) + frame.columnconfigure(1, weight=1) + + label = ttk.Label(frame, text="Editor") + label.grid(row=0, column=0, pady=2, padx=2, sticky="w") + combobox = ttk.Combobox( + frame, textvariable=self.editor, values=appconfig.EDITORS, state="readonly" + ) + combobox.grid(row=0, column=1, sticky="ew") + + label = ttk.Label(frame, text="Terminal") + label.grid(row=1, column=0, pady=2, padx=2, sticky="w") + combobox = ttk.Combobox( + frame, + textvariable=self.terminal, + values=appconfig.TERMINALS, + state="readonly", + ) + combobox.grid(row=1, column=1, sticky="ew") + + label = ttk.Label(frame, text="3D GUI") + label.grid(row=2, column=0, pady=2, padx=2, sticky="w") + entry = ttk.Entry(frame, textvariable=self.gui3d) + entry.grid(row=2, column=1, sticky="ew") + + def draw_buttons(self): + frame = ttk.Frame(self) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + + button = ttk.Button(frame, text="Save", command=self.click_save) + button.grid(row=0, column=0, sticky="ew") + + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_save(self): + preferences = self.app.config["preferences"] + preferences["terminal"] = self.terminal.get() + preferences["editor"] = self.editor.get() + preferences["gui3d"] = self.gui3d.get() + self.app.save_config() + self.destroy() diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index ce6b9698..c917ddf2 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -1,6 +1,5 @@ import tkinter as tk -from coretk import appdirs from coretk.coreclient import CoreServer from coretk.dialogs.dialog import Dialog @@ -116,7 +115,7 @@ class ServersDialog(Dialog): {"name": server.name, "address": server.address, "port": server.port} ) self.app.config["servers"] = servers - appdirs.save_config(self.app.config) + self.app.save_config() self.destroy() def click_create(self): diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index f25b0eb1..d064f77e 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -4,7 +4,7 @@ from enum import Enum from PIL import Image, ImageTk from core.api.grpc import core_pb2 -from coretk.appdirs import LOCAL_ICONS_PATH +from coretk.appconfig import LOCAL_ICONS_PATH class Images: diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index f1eb51e3..98b3d254 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -7,11 +7,12 @@ import webbrowser from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 -from coretk.appdirs import XML_PATH +from coretk.appconfig import XML_PATH from coretk.dialogs.canvasbackground import CanvasBackgroundDialog from coretk.dialogs.canvassizeandscale import SizeAndScaleDialog from coretk.dialogs.hooks import HooksDialog from coretk.dialogs.observers import ObserverDialog +from coretk.dialogs.preferences import PreferencesDialog from coretk.dialogs.servers import ServersDialog from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog @@ -83,6 +84,10 @@ class MenuAction: self.prompt_save_running_session() self.app.core.open_xml(file_path) + def gui_preferences(self): + dialog = PreferencesDialog(self.app, self.app) + dialog.show() + def canvas_size_and_scale(self): dialog = SizeAndScaleDialog(self.app, self.app) dialog.show() diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index d482045a..621510e2 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -2,7 +2,7 @@ import tkinter as tk from functools import partial import coretk.menuaction as action -from coretk.coreclient import OBSERVER_WIDGETS +from coretk.coreclient import OBSERVERS class Menubar(tk.Menu): @@ -94,7 +94,9 @@ class Menubar(tk.Menu): menu.add_separator() menu.add_command(label="Find...", accelerator="Ctrl+F", state=tk.DISABLED) menu.add_command(label="Clear marker", state=tk.DISABLED) - menu.add_command(label="Preferences...", state=tk.DISABLED) + menu.add_command( + label="Preferences...", command=self.menuaction.gui_preferences + ) self.add_cascade(label="Edit", menu=menu) def draw_canvas_menu(self): @@ -379,8 +381,8 @@ class Menubar(tk.Menu): value="none", command=lambda: self.app.core.set_observer(None), ) - for name in sorted(OBSERVER_WIDGETS): - cmd = OBSERVER_WIDGETS[name] + for name in sorted(OBSERVERS): + cmd = OBSERVERS[name] menu.add_radiobutton( label=name, variable=var, From a8f06da338466083ab76c94a0786999b19901294 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 Nov 2019 15:55:39 -0800 Subject: [PATCH 215/462] converted canvas dialogs to use ttk widgets --- coretk/coretk/dialogs/canvasbackground.py | 45 ++++++------ coretk/coretk/dialogs/canvassizeandscale.py | 76 ++++++++++----------- 2 files changed, 61 insertions(+), 60 deletions(-) diff --git a/coretk/coretk/dialogs/canvasbackground.py b/coretk/coretk/dialogs/canvasbackground.py index 5f3b253f..ed7e44f6 100644 --- a/coretk/coretk/dialogs/canvasbackground.py +++ b/coretk/coretk/dialogs/canvasbackground.py @@ -4,7 +4,7 @@ set wallpaper import enum import logging import tkinter as tk -from tkinter import filedialog +from tkinter import filedialog, ttk from PIL import Image, ImageTk @@ -39,7 +39,6 @@ class CanvasBackgroundDialog(Dialog): def draw(self): self.columnconfigure(0, weight=1) - self.rowconfigure(0, weight=1) self.draw_image() self.draw_image_label() self.draw_image_selection() @@ -48,65 +47,67 @@ class CanvasBackgroundDialog(Dialog): self.draw_buttons() def draw_image(self): - self.image_label = tk.Label( - self, text="(image preview)", height=8, width=32, bg="white" + self.image_label = ttk.Label( + self, text="(image preview)", width=32, anchor=tk.CENTER ) - self.image_label.grid(row=0, column=0, pady=5, sticky="nsew") + self.image_label.grid(row=0, column=0, pady=5) def draw_image_label(self): - label = tk.Label(self, text="Image filename: ") + label = ttk.Label(self, text="Image filename: ") label.grid(row=1, column=0, sticky="ew") def draw_image_selection(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.columnconfigure(0, weight=2) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) frame.grid(row=2, column=0, sticky="ew") - entry = tk.Entry(frame, textvariable=self.file_name) + entry = ttk.Entry(frame, textvariable=self.file_name) entry.focus() entry.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="...", command=self.click_open_image) + button = ttk.Button(frame, text="...", command=self.click_open_image) button.grid(row=0, column=1, sticky="ew") - button = tk.Button(frame, text="Clear", command=self.click_clear) + button = ttk.Button(frame, text="Clear", command=self.click_clear) button.grid(row=0, column=2, sticky="ew") def draw_options(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) frame.columnconfigure(3, weight=1) frame.grid(row=3, column=0, sticky="ew") - button = tk.Radiobutton( + button = ttk.Radiobutton( frame, text="upper-left", value=1, variable=self.radiovar ) button.grid(row=0, column=0, sticky="ew") self.options.append(button) - button = tk.Radiobutton(frame, text="centered", value=2, variable=self.radiovar) + button = ttk.Radiobutton( + frame, text="centered", value=2, variable=self.radiovar + ) button.grid(row=0, column=1, sticky="ew") self.options.append(button) - button = tk.Radiobutton(frame, text="scaled", value=3, variable=self.radiovar) + button = ttk.Radiobutton(frame, text="scaled", value=3, variable=self.radiovar) button.grid(row=0, column=2, sticky="ew") self.options.append(button) - button = tk.Radiobutton(frame, text="titled", value=4, variable=self.radiovar) + button = ttk.Radiobutton(frame, text="titled", value=4, variable=self.radiovar) button.grid(row=0, column=3, sticky="ew") self.options.append(button) def draw_additional_options(self): - checkbutton = tk.Checkbutton( + checkbutton = ttk.Checkbutton( self, text="Show grid", variable=self.show_grid_var ) checkbutton.grid(row=4, column=0, sticky="ew", padx=5) - checkbutton = tk.Checkbutton( + checkbutton = ttk.Checkbutton( self, text="Adjust canvas size to image dimensions", variable=self.adjust_to_dim_var, @@ -118,15 +119,15 @@ class CanvasBackgroundDialog(Dialog): self.adjust_to_dim_var.set(0) def draw_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(row=6, column=0, pady=5, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - button = tk.Button(frame, text="Apply", command=self.click_apply) + button = ttk.Button(frame, text="Apply", command=self.click_apply) button.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") def click_open_image(self): @@ -144,7 +145,7 @@ class CanvasBackgroundDialog(Dialog): img = Image.open(filename) img = img.resize((width, height), Image.ANTIALIAS) tk_img = ImageTk.PhotoImage(img) - self.image_label.config(image=tk_img, width=width, height=height) + self.image_label.config(image=tk_img, width=width) self.image_label.image = tk_img def click_clear(self): @@ -156,7 +157,7 @@ class CanvasBackgroundDialog(Dialog): # delete entry self.file_name.set("") # delete display image - self.image_label.config(image="", width=32, height=8) + self.image_label.config(image="", width=32) def click_adjust_canvas(self): # deselect all radio buttons and grey them out diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 20b464d0..1d880639 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -2,7 +2,7 @@ size and scale """ import tkinter as tk -from tkinter import font +from tkinter import font, ttk from coretk.dialogs.canvasbackground import ScaleOption from coretk.dialogs.dialog import Dialog @@ -49,120 +49,120 @@ class SizeAndScaleDialog(Dialog): self.draw_buttons() def draw_size(self): - label = tk.Label(self, text="Size", font=self.section_font) + label = ttk.Label(self, text="Size", font=self.section_font) label.grid(sticky="w") # draw size row 1 - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew", pady=3) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) - label = tk.Label(frame, text="Width") + label = ttk.Label(frame, text="Width") label.grid(row=0, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.pixel_width) + entry = ttk.Entry(frame, textvariable=self.pixel_width) entry.grid(row=0, column=1, sticky="ew") - label = tk.Label(frame, text="x Height") + label = ttk.Label(frame, text="x Height") label.grid(row=0, column=2, sticky="w") - entry = tk.Entry(frame, textvariable=self.pixel_height) + entry = ttk.Entry(frame, textvariable=self.pixel_height) entry.grid(row=0, column=3, sticky="ew") - label = tk.Label(frame, text="Pixels") + label = ttk.Label(frame, text="Pixels") label.grid(row=0, column=4, sticky="w") # draw size row 2 - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew", pady=3) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) - label = tk.Label(frame, text="Width") + label = ttk.Label(frame, text="Width") label.grid(row=0, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.meters_width) + entry = ttk.Entry(frame, textvariable=self.meters_width) entry.grid(row=0, column=1, sticky="ew") - label = tk.Label(frame, text="x Height") + label = ttk.Label(frame, text="x Height") label.grid(row=0, column=2, sticky="w") - entry = tk.Entry(frame, textvariable=self.meters_height) + entry = ttk.Entry(frame, textvariable=self.meters_height) entry.grid(row=0, column=3, sticky="ew") - label = tk.Label(frame, text="Meters") + label = ttk.Label(frame, text="Meters") label.grid(row=0, column=4, sticky="w") def draw_scale(self): - label = tk.Label(self, text="Scale", font=self.section_font) + label = ttk.Label(self, text="Scale", font=self.section_font) label.grid(sticky="w") - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) - label = tk.Label(frame, text="100 Pixels =") + label = ttk.Label(frame, text="100 Pixels =") label.grid(row=0, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.scale) + entry = ttk.Entry(frame, textvariable=self.scale) entry.grid(row=0, column=1, sticky="ew") - label = tk.Label(frame, text="Meters") + label = ttk.Label(frame, text="Meters") label.grid(row=0, column=2, sticky="w") def draw_reference_point(self): - label = tk.Label(self, text="Reference point", font=self.section_font) + label = ttk.Label(self, text="Reference point", font=self.section_font) label.grid(sticky="w") - label = tk.Label( + label = ttk.Label( self, text="Default is (0, 0), the upper left corner of the canvas" ) label.grid(sticky="w") - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew", pady=3) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) - label = tk.Label(frame, text="X") + label = ttk.Label(frame, text="X") label.grid(row=0, column=0, sticky="w") x_var = tk.StringVar(value=0) - entry = tk.Entry(frame, textvariable=x_var) + entry = ttk.Entry(frame, textvariable=x_var) entry.grid(row=0, column=1, sticky="ew") - label = tk.Label(frame, text="Y") + label = ttk.Label(frame, text="Y") label.grid(row=0, column=2, sticky="w") y_var = tk.StringVar(value=0) - entry = tk.Entry(frame, textvariable=y_var) + entry = ttk.Entry(frame, textvariable=y_var) entry.grid(row=0, column=3, sticky="ew") - label = tk.Label(self, text="Translates To") + label = ttk.Label(self, text="Translates To") label.grid(sticky="w") - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew", pady=3) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) frame.columnconfigure(5, weight=1) - label = tk.Label(frame, text="Lat") + label = ttk.Label(frame, text="Lat") label.grid(row=0, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.lat) + entry = ttk.Entry(frame, textvariable=self.lat) entry.grid(row=0, column=1, sticky="ew") - label = tk.Label(frame, text="Lon") + label = ttk.Label(frame, text="Lon") label.grid(row=0, column=2, sticky="w") - entry = tk.Entry(frame, textvariable=self.lon) + entry = ttk.Entry(frame, textvariable=self.lon) entry.grid(row=0, column=3, sticky="ew") - label = tk.Label(frame, text="Alt") + label = ttk.Label(frame, text="Alt") label.grid(row=0, column=4, sticky="w") - entry = tk.Entry(frame, textvariable=self.alt) + entry = ttk.Entry(frame, textvariable=self.alt) entry.grid(row=0, column=5, sticky="ew") def draw_save_as_default(self): - button = tk.Checkbutton( + button = ttk.Checkbutton( self, text="Save as default?", variable=self.save_default ) button.grid(sticky="w", pady=3) def draw_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.grid(sticky="ew") - button = tk.Button(frame, text="Apply", command=self.click_apply) + button = ttk.Button(frame, text="Apply", command=self.click_apply) button.grid(row=0, column=0, pady=5, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, pady=5, sticky="ew") def redraw_grid(self): From 58a4db6050ed8d852c043261bf693f6fc41eb056 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 Nov 2019 16:33:51 -0800 Subject: [PATCH 216/462] updates to convert more dialogs to use ttk widgets when possible --- coretk/coretk/dialogs/customnodes.py | 33 +++++----- coretk/coretk/dialogs/emaneconfig.py | 82 ++++++++++--------------- coretk/coretk/dialogs/hooks.py | 26 ++++---- coretk/coretk/dialogs/icondialog.py | 18 +++--- coretk/coretk/dialogs/observers.py | 29 ++++----- coretk/coretk/dialogs/servers.py | 35 ++++++----- coretk/coretk/dialogs/sessionoptions.py | 8 +-- coretk/coretk/dialogs/sessions.py | 28 ++++----- coretk/coretk/widgets.py | 16 ++--- 9 files changed, 127 insertions(+), 148 deletions(-) diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 5caa19ff..f5c1ab78 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -1,6 +1,7 @@ import logging import tkinter as tk from pathlib import Path +from tkinter import ttk from coretk.coreclient import CustomNode from coretk.dialogs.dialog import Dialog @@ -21,7 +22,7 @@ class ServicesSelectDialog(Dialog): self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(stick="nsew") frame.rowconfigure(0, weight=1) for i in range(3): @@ -43,13 +44,13 @@ class ServicesSelectDialog(Dialog): for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(stick="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save", command=self.destroy) + button = ttk.Button(frame, text="Save", command=self.destroy) button.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.click_cancel) + button = ttk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=1, sticky="ew") # trigger group change @@ -102,7 +103,7 @@ class CustomNodesDialog(Dialog): self.draw_buttons() def draw_node_config(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="nsew") frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) @@ -113,45 +114,45 @@ class CustomNodesDialog(Dialog): for name in sorted(self.app.core.custom_nodes): self.nodes_list.listbox.insert(tk.END, name) - frame = tk.Frame(frame) + frame = ttk.Frame(frame) frame.grid(row=0, column=2, sticky="nsew") frame.columnconfigure(0, weight=1) - entry = tk.Entry(frame, textvariable=self.name) + entry = ttk.Entry(frame, textvariable=self.name) entry.grid(sticky="ew") - self.image_button = tk.Button(frame, text="Icon", command=self.click_icon) + self.image_button = ttk.Button(frame, text="Icon", command=self.click_icon) self.image_button.grid(sticky="ew") - button = tk.Button(frame, text="Services", command=self.click_services) + button = ttk.Button(frame, text="Services", command=self.click_services) button.grid(sticky="ew") def draw_node_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(pady=2, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Create", command=self.click_create) + button = ttk.Button(frame, text="Create", command=self.click_create) button.grid(row=0, column=0, sticky="ew") - self.edit_button = tk.Button( + self.edit_button = ttk.Button( frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) self.edit_button.grid(row=0, column=1, sticky="ew") - self.delete_button = tk.Button( + self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete ) self.delete_button.grid(row=0, column=2, sticky="ew") def draw_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save", command=self.click_save) + button = ttk.Button(frame, text="Save", command=self.click_save) button.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") def reset_values(self): diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index 6950dc10..7fd577e2 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -58,18 +58,18 @@ class EmaneConfiguration(Dialog): print("not implemented") def node_name_and_image(self): - f = tk.Frame(self, bg="#d9d9d9") + f = ttk.Frame(self) - lbl = tk.Label(f, text="Node name:", bg="#d9d9d9") + lbl = ttk.Label(f, text="Node name:") lbl.grid(row=0, column=0, padx=2, pady=2) - e = tk.Entry(f, textvariable=self.create_text_variable(""), bg="white") + e = ttk.Entry(f, textvariable=self.create_text_variable("")) e.grid(row=0, column=1, padx=2, pady=2) cbb = ttk.Combobox(f, values=["(none)", "core1", "core2"], state="readonly") cbb.current(0) cbb.grid(row=0, column=2, padx=2, pady=2) - b = tk.Button(f, image=self.canvas_node.image) + b = ttk.Button(f, image=self.canvas_node.image) b.grid(row=0, column=3, padx=2, pady=2) f.grid(row=0, column=0, sticky="nsew") @@ -96,13 +96,13 @@ class EmaneConfiguration(Dialog): self.emane_config_frame.draw_config() self.emane_config_frame.grid(sticky="nsew") - frame = tk.Frame(self.emane_dialog) + frame = ttk.Frame(self.emane_dialog) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - b1 = tk.Button(frame, text="Appy", command=self.save_emane_option) + b1 = ttk.Button(frame, text="Appy", command=self.save_emane_option) b1.grid(row=0, column=0, sticky="ew") - b2 = tk.Button(frame, text="Cancel", command=self.emane_dialog.destroy) + b2 = ttk.Button(frame, text="Cancel", command=self.emane_dialog.destroy) b2.grid(row=0, column=1, sticky="ew") self.emane_dialog.show() @@ -170,35 +170,33 @@ class EmaneConfiguration(Dialog): self.model_config_frame.grid(sticky="nsew") self.model_config_frame.draw_config() - frame = tk.Frame(self.emane_model_dialog) + frame = ttk.Frame(self.emane_model_dialog) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - b1 = tk.Button(frame, text="Apply", command=self.save_emane_model_options) + b1 = ttk.Button(frame, text="Apply", command=self.save_emane_model_options) b1.grid(row=0, column=0, sticky="ew") - b2 = tk.Button(frame, text="Cancel", command=self.emane_model_dialog.destroy) + b2 = ttk.Button(frame, text="Cancel", command=self.emane_model_dialog.destroy) b2.grid(row=0, column=1, sticky="ew") self.emane_model_dialog.show() def draw_option_buttons(self, parent): - f = tk.Frame(parent, bg="#d9d9d9") + f = ttk.Frame(parent) f.columnconfigure(0, weight=1) f.columnconfigure(1, weight=1) - b = tk.Button( + b = ttk.Button( f, text=self.emane_models[0] + " options", image=Images.get(ImageEnum.EDITNODE), compound=tk.RIGHT, - bg="#d9d9d9", command=self.draw_model_options, ) b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") - b = tk.Button( + b = ttk.Button( f, text="EMANE options", image=Images.get(ImageEnum.EDITNODE), compound=tk.RIGHT, - bg="#d9d9d9", command=self.draw_emane_options, ) b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") @@ -233,21 +231,14 @@ class EmaneConfiguration(Dialog): self.emane_models = [x.split("_")[1] for x in response.models] # create combo box and its binding - f = tk.Frame( - parent, - bg="#d9d9d9", - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", - highlightthickness=0.5, - bd=0, - ) + f = ttk.Frame(parent) self.emane_model_combobox = ttk.Combobox( f, values=self.emane_models, state="readonly" ) self.emane_model_combobox.grid() self.emane_model_combobox.current(0) self.emane_model_combobox.bind("<>", self.combobox_select) - f.grid(row=3, column=0, sticky=tk.W + tk.E) + f.grid(row=3, column=0, sticky="ew") def draw_text_label_and_entry(self, parent, label_text, entry_text): """ @@ -257,10 +248,10 @@ class EmaneConfiguration(Dialog): """ var = tk.StringVar() var.set(entry_text) - f = tk.Frame(parent) - lbl = tk.Label(f, text=label_text) + f = ttk.Frame(parent) + lbl = ttk.Label(f, text=label_text) lbl.grid(row=0, column=0) - e = tk.Entry(f, textvariable=var, bg="white") + e = ttk.Entry(f, textvariable=var) e.grid(row=0, column=1) f.grid(stick=tk.W, padx=2, pady=2) @@ -271,44 +262,33 @@ class EmaneConfiguration(Dialog): :return: nothing """ # draw label - lbl = tk.Label(self, text="Emane") + lbl = ttk.Label(self, text="Emane") lbl.grid(row=1, column=0) # main frame that has emane wiki, a short description, emane models and the configure buttons - f = tk.Frame( - self, - bg="#d9d9d9", - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", - highlightthickness=0.5, - bd=0, - relief=tk.RAISED, - ) + f = ttk.Frame(self) f.columnconfigure(0, weight=1) - b = tk.Button( + b = ttk.Button( f, image=Images.get(ImageEnum.EDITNODE), text="EMANE Wiki", compound=tk.RIGHT, - relief=tk.RAISED, - bg="#d9d9d9", command=lambda: webbrowser.open_new( "https://github.com/adjacentlink/emane/wiki" ), ) - b.grid(row=0, column=0, sticky=tk.W) + b.grid(row=0, column=0, sticky="w") - lbl = tk.Label( + lbl = ttk.Label( f, text="The EMANE emulation system provides more complex wireless radio emulation " "\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details", - bg="#d9d9d9", ) lbl.grid(row=1, column=0, sticky="nsew") - lbl = tk.Label(f, text="EMANE Models", bg="#d9d9d9") - lbl.grid(row=2, column=0, sticky=tk.W) + lbl = ttk.Label(f, text="EMANE Models") + lbl.grid(row=2, column=0, sticky="w") self.draw_emane_models(f) self.draw_option_buttons(f) @@ -325,12 +305,12 @@ class EmaneConfiguration(Dialog): :return: """ - f = tk.Frame(self, bg="#d9d9d9") + f = ttk.Frame(self) f.columnconfigure(0, weight=1) f.columnconfigure(1, weight=1) - b = tk.Button(f, text="Link to all routers", bg="#d9d9d9") + b = ttk.Button(f, text="Link to all routers") b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") - b = tk.Button(f, text="Choose WLAN members", bg="#d9d9d9") + b = ttk.Button(f, text="Choose WLAN members") b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") f.grid(row=5, column=0, sticky="nsew") @@ -340,12 +320,12 @@ class EmaneConfiguration(Dialog): self.destroy() def draw_apply_and_cancel(self): - f = tk.Frame(self, bg="#d9d9d9") + f = ttk.Frame(self) f.columnconfigure(0, weight=1) f.columnconfigure(1, weight=1) - b = tk.Button(f, text="Apply", bg="#d9d9d9", command=self.apply) + b = ttk.Button(f, text="Apply", command=self.apply) b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") - b = tk.Button(f, text="Cancel", bg="#d9d9d9", command=self.destroy) + b = ttk.Button(f, text="Cancel", command=self.destroy) b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") f.grid(sticky="nsew") diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index 99647635..5ae9da8a 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -19,14 +19,14 @@ class HookDialog(Dialog): self.rowconfigure(1, weight=1) # name and states - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(row=0, sticky="ew", pady=2) frame.columnconfigure(0, weight=2) frame.columnconfigure(1, weight=7) frame.columnconfigure(2, weight=1) - label = tk.Label(frame, text="Name") + label = ttk.Label(frame, text="Name") label.grid(row=0, column=0, sticky="ew") - entry = tk.Entry(frame, textvariable=self.name) + entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=1, sticky="ew") values = tuple(x for x in core_pb2.SessionState.Enum.keys() if x != "NONE") initial_state = core_pb2.SessionState.Enum.Name(core_pb2.SessionState.RUNTIME) @@ -39,7 +39,7 @@ class HookDialog(Dialog): combobox.bind("<>", self.state_change) # data - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) frame.grid(row=1, sticky="nsew", pady=2) @@ -53,19 +53,19 @@ class HookDialog(Dialog): ), ) self.data.grid(row=0, column=0, sticky="nsew") - scrollbar = tk.Scrollbar(frame) + scrollbar = ttk.Scrollbar(frame) scrollbar.grid(row=0, column=1, sticky="ns") self.data.config(yscrollcommand=scrollbar.set) scrollbar.config(command=self.data.yview) # button row - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(row=2, sticky="ew", pady=2) for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save", command=lambda: self.save()) + button = ttk.Button(frame, text="Save", command=lambda: self.save()) button.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="Cancel", command=lambda: self.destroy()) + button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=1, sticky="ew") def state_change(self, event): @@ -106,21 +106,21 @@ class HooksDialog(Dialog): self.listbox.bind("<>", self.select) for hook_file in self.app.core.hooks: self.listbox.insert(tk.END, hook_file) - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(row=1, sticky="ew") for i in range(4): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Create", command=self.click_create) + button = ttk.Button(frame, text="Create", command=self.click_create) button.grid(row=0, column=0, sticky="ew") - self.edit_button = tk.Button( + self.edit_button = ttk.Button( frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) self.edit_button.grid(row=0, column=1, sticky="ew") - self.delete_button = tk.Button( + self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete ) self.delete_button.grid(row=0, column=2, sticky="ew") - button = tk.Button(frame, text="Cancel", command=lambda: self.destroy()) + button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=3, sticky="ew") def click_create(self): diff --git a/coretk/coretk/dialogs/icondialog.py b/coretk/coretk/dialogs/icondialog.py index bf7b08cf..fb6fb6bb 100644 --- a/coretk/coretk/dialogs/icondialog.py +++ b/coretk/coretk/dialogs/icondialog.py @@ -1,5 +1,5 @@ import tkinter as tk -from tkinter import filedialog +from tkinter import filedialog, ttk from coretk.appconfig import ICONS_PATH from coretk.dialogs.dialog import Dialog @@ -18,30 +18,30 @@ class IconDialog(Dialog): self.columnconfigure(0, weight=1) # row one - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(row=0, column=0, pady=2, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=3) - label = tk.Label(frame, text="Image") + label = ttk.Label(frame, text="Image") label.grid(row=0, column=0, sticky="ew") - entry = tk.Entry(frame, textvariable=self.file_path) + entry = ttk.Entry(frame, textvariable=self.file_path) entry.grid(row=0, column=1, sticky="ew") - button = tk.Button(frame, text="...", command=self.click_file) + button = ttk.Button(frame, text="...", command=self.click_file) button.grid(row=0, column=2) # row two - self.image_label = tk.Label(self, image=self.image) + self.image_label = ttk.Label(self, image=self.image, anchor=tk.CENTER) self.image_label.grid(row=1, column=0, pady=2, sticky="ew") # row three - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(row=2, column=0, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - button = tk.Button(frame, text="Apply", command=self.destroy) + button = ttk.Button(frame, text="Apply", command=self.destroy) button.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.click_cancel) + button = ttk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=1, sticky="ew") def click_file(self): diff --git a/coretk/coretk/dialogs/observers.py b/coretk/coretk/dialogs/observers.py index 6546493d..6c57f08e 100644 --- a/coretk/coretk/dialogs/observers.py +++ b/coretk/coretk/dialogs/observers.py @@ -1,4 +1,5 @@ import tkinter as tk +from tkinter import ttk from coretk.coreclient import Observer from coretk.dialogs.dialog import Dialog @@ -25,12 +26,12 @@ class ObserverDialog(Dialog): self.draw_apply_buttons() def draw_listbox(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="nsew") frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL) scrollbar.grid(row=0, column=1, sticky="ns") self.observers = tk.Listbox( @@ -44,49 +45,49 @@ class ObserverDialog(Dialog): scrollbar.config(command=self.observers.yview) def draw_form_fields(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) - label = tk.Label(frame, text="Name") + label = ttk.Label(frame, text="Name") label.grid(row=0, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.name) + entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=1, sticky="ew") - label = tk.Label(frame, text="Command") + label = ttk.Label(frame, text="Command") label.grid(row=1, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.cmd) + entry = ttk.Entry(frame, textvariable=self.cmd) entry.grid(row=1, column=1, sticky="ew") def draw_config_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(pady=2, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Create", command=self.click_create) + button = ttk.Button(frame, text="Create", command=self.click_create) button.grid(row=0, column=0, sticky="ew") - self.save_button = tk.Button( + self.save_button = ttk.Button( frame, text="Save", state=tk.DISABLED, command=self.click_save ) self.save_button.grid(row=0, column=1, sticky="ew") - self.delete_button = tk.Button( + self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete ) self.delete_button.grid(row=0, column=2, sticky="ew") def draw_apply_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save", command=self.click_save_config) + button = ttk.Button(frame, text="Save", command=self.click_save_config) button.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") def click_save_config(self): diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index c917ddf2..25f3e826 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -1,4 +1,5 @@ import tkinter as tk +from tkinter import ttk from coretk.coreclient import CoreServer from coretk.dialogs.dialog import Dialog @@ -30,12 +31,12 @@ class ServersDialog(Dialog): self.draw_apply_buttons() def draw_servers(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(pady=2, sticky="nsew") frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL) scrollbar.grid(row=0, column=1, sticky="ns") self.servers = tk.Listbox( @@ -50,61 +51,61 @@ class ServersDialog(Dialog): scrollbar.config(command=self.servers.yview) def draw_server_configuration(self): - label = tk.Label(self, text="Server Configuration") + label = ttk.Label(self, text="Server Configuration") label.grid(pady=2, sticky="ew") - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(pady=2, sticky="ew") frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) frame.columnconfigure(5, weight=1) - label = tk.Label(frame, text="Name") + label = ttk.Label(frame, text="Name") label.grid(row=0, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.name) + entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=1, sticky="ew") - label = tk.Label(frame, text="Address") + label = ttk.Label(frame, text="Address") label.grid(row=0, column=2, sticky="w") - entry = tk.Entry(frame, textvariable=self.address) + entry = ttk.Entry(frame, textvariable=self.address) entry.grid(row=0, column=3, sticky="ew") - label = tk.Label(frame, text="Port") + label = ttk.Label(frame, text="Port") label.grid(row=0, column=4, sticky="w") - entry = tk.Entry(frame, textvariable=self.port) + entry = ttk.Entry(frame, textvariable=self.port) entry.grid(row=0, column=5, sticky="ew") def draw_servers_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(pady=2, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Create", command=self.click_create) + button = ttk.Button(frame, text="Create", command=self.click_create) button.grid(row=0, column=0, sticky="ew") - self.save_button = tk.Button( + self.save_button = ttk.Button( frame, text="Save", state=tk.DISABLED, command=self.click_save ) self.save_button.grid(row=0, column=1, sticky="ew") - self.delete_button = tk.Button( + self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete ) self.delete_button.grid(row=0, column=2, sticky="ew") def draw_apply_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button( + button = ttk.Button( frame, text="Save Configuration", command=self.click_save_configuration ) button.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") def click_save_configuration(self): diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index 8702d581..45f26a58 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -1,5 +1,5 @@ import logging -import tkinter as tk +from tkinter import ttk from coretk.dialogs.dialog import Dialog from coretk.widgets import ConfigFrame @@ -26,13 +26,13 @@ class SessionOptionsDialog(Dialog): self.config_frame.draw_config() self.config_frame.grid(sticky="nsew") - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save", command=self.save) + button = ttk.Button(frame, text="Save", command=self.save) button.grid(row=0, column=0, pady=PAD_Y, padx=PAD_X, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, pady=PAD_Y, padx=PAD_X, sticky="ew") def save(self): diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index b7c4d128..40d09c44 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -1,6 +1,6 @@ import logging import tkinter as tk -from tkinter.ttk import Scrollbar, Treeview +from tkinter import ttk from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog @@ -9,12 +9,6 @@ from coretk.images import ImageEnum, Images class SessionsDialog(Dialog): def __init__(self, master, app): - """ - create session table instance - - :param coretk.coreclient.CoreClient grpc: coregrpc - :param root.master master: - """ super().__init__(master, app, "Sessions", modal=True) self.selected = False self.selected_id = None @@ -32,7 +26,7 @@ class SessionsDialog(Dialog): write a short description :return: nothing """ - label = tk.Label( + label = ttk.Label( self, text="Below is a list of active CORE sessions. Double-click to \n" "connect to an existing session. Usually, only sessions in \n" @@ -42,7 +36,9 @@ class SessionsDialog(Dialog): label.grid(row=0, sticky="ew", pady=5) def draw_tree(self): - self.tree = Treeview(self, columns=("id", "state", "nodes"), show="headings") + self.tree = ttk.Treeview( + self, columns=("id", "state", "nodes"), show="headings" + ) self.tree.grid(row=1, sticky="nsew") self.tree.column("id", stretch=tk.YES) self.tree.heading("id", text="ID") @@ -64,20 +60,20 @@ class SessionsDialog(Dialog): self.tree.bind("", self.on_selected) self.tree.bind("<>", self.click_select) - yscrollbar = Scrollbar(self, orient="vertical", command=self.tree.yview) + yscrollbar = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview) yscrollbar.grid(row=1, column=1, sticky="ns") self.tree.configure(yscrollcommand=yscrollbar.set) - xscrollbar = Scrollbar(self, orient="horizontal", command=self.tree.xview) + xscrollbar = ttk.Scrollbar(self, orient="horizontal", command=self.tree.xview) xscrollbar.grid(row=2, sticky="ew", pady=5) self.tree.configure(xscrollcommand=xscrollbar.set) def draw_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) for i in range(4): frame.columnconfigure(i, weight=1) frame.grid(row=3, sticky="ew") - b = tk.Button( + b = ttk.Button( frame, image=Images.get(ImageEnum.DOCUMENTNEW), text="New", @@ -85,7 +81,7 @@ class SessionsDialog(Dialog): command=self.click_new, ) b.grid(row=0, padx=2, sticky="ew") - b = tk.Button( + b = ttk.Button( frame, image=Images.get(ImageEnum.FILEOPEN), text="Connect", @@ -93,7 +89,7 @@ class SessionsDialog(Dialog): command=self.click_connect, ) b.grid(row=0, column=1, padx=2, sticky="ew") - b = tk.Button( + b = ttk.Button( frame, image=Images.get(ImageEnum.EDITDELETE), text="Shutdown", @@ -101,7 +97,7 @@ class SessionsDialog(Dialog): command=self.click_shutdown, ) b.grid(row=0, column=2, padx=2, sticky="ew") - b = tk.Button(frame, text="Cancel", command=self.click_new) + b = ttk.Button(frame, text="Cancel", command=self.click_new) b.grid(row=0, column=3, padx=2, sticky="ew") def click_new(self): diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 236dbc24..9ae9e851 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -26,7 +26,7 @@ class FrameScroll(tk.LabelFrame): self.canvas.grid(row=0, sticky="nsew", padx=2, pady=2) self.canvas.columnconfigure(0, weight=1) self.canvas.rowconfigure(0, weight=1) - self.scrollbar = tk.Scrollbar( + self.scrollbar = ttk.Scrollbar( self, orient="vertical", command=self.canvas.yview ) self.scrollbar.grid(row=0, column=1, sticky="ns") @@ -70,11 +70,11 @@ class ConfigFrame(FrameScroll): for group_name in sorted(group_mapping): group = group_mapping[group_name] - frame = tk.Frame(self.frame) + frame = ttk.Frame(self.frame) frame.columnconfigure(1, weight=1) self.frame.add(frame, text=group_name) for index, option in enumerate(sorted(group, key=lambda x: x.name)): - label = tk.Label(frame, text=option.label) + label = ttk.Label(frame, text=option.label) label.grid(row=index, pady=pady, padx=padx, sticky="w") value = tk.StringVar() if option.type == core_pb2.ConfigOptionType.BOOL: @@ -96,15 +96,15 @@ class ConfigFrame(FrameScroll): combobox.grid(row=index, column=1, sticky="ew", pady=pady) elif option.type == core_pb2.ConfigOptionType.STRING: value.set(option.value) - entry = tk.Entry(frame, textvariable=value) + entry = ttk.Entry(frame, textvariable=value) entry.grid(row=index, column=1, sticky="ew", pady=pady) elif option.type in INT_TYPES: value.set(option.value) - entry = tk.Entry(frame, textvariable=value) + entry = ttk.Entry(frame, textvariable=value) entry.grid(row=index, column=1, sticky="ew", pady=pady) elif option.type == core_pb2.ConfigOptionType.FLOAT: value.set(option.value) - entry = tk.Entry(frame, textvariable=value) + entry = ttk.Entry(frame, textvariable=value) entry.grid(row=index, column=1, sticky="ew", pady=pady) else: logging.error("unhandled config option type: %s", option.type) @@ -131,7 +131,7 @@ class ListboxScroll(tk.LabelFrame): super().__init__(master, cnf, **kw) self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) - self.scrollbar = tk.Scrollbar(self, orient=tk.VERTICAL) + self.scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL) self.scrollbar.grid(row=0, column=1, sticky="ns") self.listbox = tk.Listbox( self, selectmode=tk.SINGLE, yscrollcommand=self.scrollbar.set @@ -149,5 +149,5 @@ class CheckboxList(FrameScroll): def add(self, name, checked): var = tk.BooleanVar(value=checked) func = partial(self.clicked, name, var) - checkbox = tk.Checkbutton(self.frame, text=name, variable=var, command=func) + checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func) checkbox.grid(sticky="w") From 3dd3d928dd6bf8a6fd203a77785c5e49f0eb21b6 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 11 Nov 2019 16:34:41 -0800 Subject: [PATCH 217/462] more work on designing node service configuration --- coretk/coretk/dialogs/serviceconfiguration.py | 227 ++++++++++++------ coretk/coretk/icons/document-save.gif | Bin 0 -> 1049 bytes coretk/coretk/images.py | 1 + 3 files changed, 158 insertions(+), 70 deletions(-) create mode 100644 coretk/coretk/icons/document-save.gif diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index e352a2e8..016373c8 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -16,11 +16,20 @@ class ServiceConfiguration(Dialog): self.metadata = tk.StringVar() self.filename = tk.StringVar() self.radiovar = tk.IntVar() - self.radiovar.set(1) + self.radiovar.set(2) self.startup_index = tk.IntVar() self.start_time = tk.IntVar() self.documentnew_img = Images.get(ImageEnum.DOCUMENTNEW) self.editdelete_img = Images.get(ImageEnum.EDITDELETE) + self.tab_parent = None + self.filenames = ["test1", "test2", "test3"] + + self.metadata_entry = None + self.filename_combobox = None + self.startup_commands_listbox = None + self.shutdown_commands_listbox = None + self.validate_commands_listbox = None + self.draw() def draw(self): @@ -35,24 +44,27 @@ class ServiceConfiguration(Dialog): # frame2.columnconfigure(1, weight=4) label = tk.Label(frame2, text="Meta-data") label.grid(row=0, column=0) - entry = tk.Entry(frame2, textvariable=self.metadata) - entry.grid(row=0, column=1) + self.metadata_entry = tk.Entry(frame2, textvariable=self.metadata) + self.metadata_entry.grid(row=0, column=1) frame2.grid(row=1, column=0) frame.grid(row=0, column=0) frame = tk.Frame(self) - tab_parent = ttk.Notebook(frame) - tab1 = ttk.Frame(tab_parent) - tab2 = ttk.Frame(tab_parent) - tab3 = ttk.Frame(tab_parent) + self.tab_parent = ttk.Notebook(frame) + tab1 = ttk.Frame(self.tab_parent) + tab2 = ttk.Frame(self.tab_parent) + tab3 = ttk.Frame(self.tab_parent) + tab4 = ttk.Frame(self.tab_parent) tab1.columnconfigure(0, weight=1) tab2.columnconfigure(0, weight=1) tab3.columnconfigure(0, weight=1) + tab4.columnconfigure(0, weight=1) - tab_parent.add(tab1, text="Files", sticky="nsew") - tab_parent.add(tab2, text="Directories", sticky="nsew") - tab_parent.add(tab3, text="Startup/shutdown", sticky="nsew") - tab_parent.grid(row=0, column=0, sticky="nsew") + self.tab_parent.add(tab1, text="Files", sticky="nsew") + self.tab_parent.add(tab2, text="Directories", sticky="nsew") + self.tab_parent.add(tab3, text="Startup/shutdown", sticky="nsew") + self.tab_parent.add(tab4, text="Configuration", sticky="nsew") + self.tab_parent.grid(row=0, column=0, sticky="nsew") frame.grid(row=1, column=0, sticky="nsew") # tab 1 @@ -64,11 +76,14 @@ class ServiceConfiguration(Dialog): frame = tk.Frame(tab1) label = tk.Label(frame, text="File name: ") label.grid(row=0, column=0) - entry = tk.Entry(frame, textvariable=self.filename) - entry.grid(row=0, column=1) + self.filename_combobox = ttk.Combobox(frame, values=self.filenames) + self.filename_combobox.grid(row=0, column=1) + self.filename_combobox.current(0) button = tk.Button(frame, image=self.documentnew_img) + button.bind("", self.add_filename) button.grid(row=0, column=2) button = tk.Button(frame, image=self.editdelete_img) + button.bind("", self.delete_filename) button.grid(row=0, column=3) frame.grid(row=1, column=0, sticky="nsew") @@ -78,11 +93,13 @@ class ServiceConfiguration(Dialog): variable=self.radiovar, text="Copy this source file:", indicatoron=True, + value=1, + state="disabled", ) button.grid(row=0, column=0) entry = tk.Entry(frame, state=tk.DISABLED) entry.grid(row=0, column=1) - button = tk.Button(frame, text="not implemented") + button = tk.Button(frame, image=Images.get(ImageEnum.FILEOPEN)) button.grid(row=0, column=2) frame.grid(row=2, column=0, sticky="nsew") @@ -92,11 +109,12 @@ class ServiceConfiguration(Dialog): variable=self.radiovar, text="Use text below for file contents:", indicatoron=True, + value=2, ) button.grid(row=0, column=0) - button = tk.Button(frame, text="not implemented") + button = tk.Button(frame, image=Images.get(ImageEnum.FILEOPEN)) button.grid(row=0, column=1) - button = tk.Button(frame, text="not implemented") + button = tk.Button(frame, image=Images.get(ImageEnum.DOCUMENTSAVE)) button.grid(row=0, column=2) frame.grid(row=3, column=0, sticky="nsew") @@ -108,53 +126,64 @@ class ServiceConfiguration(Dialog): label.grid(row=0, column=0, sticky="nsew") # tab 3 - label_frame = tk.LabelFrame(tab3, text="Startup commands") - label_frame.columnconfigure(0, weight=1) - frame = tk.Frame(label_frame) - frame.columnconfigure(0, weight=1) - entry = tk.Entry(frame, textvariable=tk.StringVar()) - entry.grid(row=0, column=0, stick="nsew") - button = tk.Button(frame, image=self.documentnew_img) - button.grid(row=0, column=1, sticky="nsew") - button = tk.Button(frame, image=self.editdelete_img) - button.grid(row=0, column=2, sticky="nsew") - frame.grid(row=0, column=0, sticky="nsew") - listbox_scroll = ListboxScroll(label_frame) - listbox_scroll.listbox.config(height=4) - listbox_scroll.grid(row=1, column=0, sticky="nsew") - label_frame.grid(row=2, column=0, sticky="nsew") + for i in range(3): + label_frame = None + if i == 0: + label_frame = tk.LabelFrame(tab3, text="Startup commands") + elif i == 1: + label_frame = tk.LabelFrame(tab3, text="Shutdown commands") + elif i == 2: + label_frame = tk.LabelFrame(tab3, text="Validation commands") + label_frame.columnconfigure(0, weight=1) + frame = tk.Frame(label_frame) + frame.columnconfigure(0, weight=1) + entry = tk.Entry(frame, textvariable=tk.StringVar()) + entry.grid(row=0, column=0, stick="nsew") + button = tk.Button(frame, image=self.documentnew_img) + button.bind("", self.add_command) + button.grid(row=0, column=1, sticky="nsew") + button = tk.Button(frame, image=self.editdelete_img) + button.grid(row=0, column=2, sticky="nsew") + button.bind("", self.delete_command) + frame.grid(row=0, column=0, sticky="nsew") + listbox_scroll = ListboxScroll(label_frame) + listbox_scroll.listbox.bind("<>", self.update_entry) + listbox_scroll.listbox.config(height=4) + listbox_scroll.grid(row=1, column=0, sticky="nsew") + if i == 0: + self.startup_commands_listbox = listbox_scroll.listbox + elif i == 1: + self.shutdown_commands_listbox = listbox_scroll.listbox + elif i == 2: + self.validate_commands_listbox = listbox_scroll.listbox + label_frame.grid(row=i, column=0, sticky="nsew") - label_frame = tk.LabelFrame(tab3, text="Shutdown commands") - label_frame.columnconfigure(0, weight=1) - frame = tk.Frame(label_frame) - frame.columnconfigure(0, weight=1) - entry = tk.Entry(frame, textvariable=tk.StringVar()) - entry.grid(row=0, column=0, sticky="nsew") - button = tk.Button(frame, image=self.documentnew_img) - button.grid(row=0, column=1, sticky="nsew") - button = tk.Button(frame, image=self.editdelete_img) - button.grid(row=0, column=2, sticky="nsew") - frame.grid(row=0, column=0, sticky="nsew") - listbox_scroll = ListboxScroll(label_frame) - listbox_scroll.listbox.config(height=4) - listbox_scroll.grid(row=1, column=0, sticky="nsew") - label_frame.grid(row=3, column=0, sticky="nsew") + # tab 4 + for i in range(2): + if i == 0: + label_frame = tk.LabelFrame(tab4, text="Executables") + elif i == 1: + label_frame = tk.LabelFrame(tab4, text="Dependencies") - label_frame = tk.LabelFrame(tab3, text="Validate commands") - label_frame.columnconfigure(0, weight=1) - frame = tk.Frame(label_frame) - frame.columnconfigure(0, weight=1) - entry = tk.Entry(frame, textvariable=tk.StringVar()) - entry.grid(row=0, column=0, sticky="nsew") - button = tk.Button(frame, image=self.documentnew_img) - button.grid(row=0, column=1, sticky="nsew") - button = tk.Button(frame, image=self.editdelete_img) - button.grid(row=0, column=2, sticky="nsew") - frame.grid(row=0, column=0, sticky="nsew") - listbox_scroll = ListboxScroll(label_frame) - listbox_scroll.listbox.config(height=4) - listbox_scroll.grid(row=1, column=0, sticky="nsew") - label_frame.grid(row=4, column=0, sticky="nsew") + label_frame.columnconfigure(0, weight=1) + listbox_scroll = ListboxScroll(label_frame) + listbox_scroll.listbox.config(height=4, state="disabled") + listbox_scroll.grid(row=0, column=0, sticky="nsew") + label_frame.grid(row=i, column=0, sticky="nsew") + + for i in range(3): + frame = tk.Frame(tab4) + frame.columnconfigure(0, weight=1) + if i == 0: + label = tk.Label(frame, text="Validation time:") + elif i == 1: + label = tk.Label(frame, text="Validation mode:") + elif i == 2: + label = tk.Label(frame, text="Validation period:") + label.grid(row=i, column=0) + entry = tk.Entry(frame, state="disabled", textvariable=tk.StringVar()) + entry.grid(row=i, column=1) + frame.grid(row=2 + i, column=0, sticky="nsew") button = tk.Button( self, text="onle store values that have changed from their defaults" @@ -164,22 +193,80 @@ class ServiceConfiguration(Dialog): frame = tk.Frame(self) button = tk.Button(frame, text="Apply", command=self.click_apply) button.grid(row=0, column=0, sticky="nsew") - button = tk.Button(frame, text="Dafults", command=self.click_defaults) + button = tk.Button( + frame, text="Dafults", command=self.click_defaults, state="disabled" + ) button.grid(row=0, column=1, sticky="nsew") - button = tk.Button(frame, text="Copy...", command=self.click_copy) + button = tk.Button( + frame, text="Copy...", command=self.click_copy, state="disabled" + ) button.grid(row=0, column=2, sticky="nsew") - button = tk.Button(frame, text="Cancel", command=self.click_cancel) + button = tk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=3, sticky="nsew") frame.grid(row=3, column=0) - def click_apply(self, event): + def add_filename(self, event): + 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 delete_filename(self, event): + 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 add_command(self, 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() + if command_to_add == "": + return + for cmd in listbox.get(0, tk.END): + if cmd == command_to_add: + return + listbox.insert(tk.END, command_to_add) + + def update_entry(self, event): + listbox = event.widget + current_selection = listbox.curselection() + if len(current_selection) > 0: + cmd = listbox.get(current_selection[0]) + entry = listbox.master.master.grid_slaves(row=0, column=0)[0].grid_slaves( + row=0, column=0 + )[0] + entry.delete(0, "end") + entry.insert(0, cmd) + + def delete_command(self, event): + button = event.widget + frame_contains_button = button.master + listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox + current_selection = listbox.curselection() + if len(current_selection) > 0: + listbox.delete(current_selection[0]) + entry = frame_contains_button.grid_slaves(row=0, column=0)[0] + entry.delete(0, tk.END) + + def click_apply(self): + metadata = self.metadata_entry.get() + filenames = list(self.filename_combobox["values"]) + 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") + print( + metadata, filenames, startup_commands, shutdown_commands, validate_commands + ) + + def click_defaults(self): print("not implemented") - def click_defaults(self, event): + def click_copy(self): print("not implemented") - def click_copy(self, event): - print("not implemented") - - def click_cancel(self, event): + def click_cancel(self): print("not implemented") diff --git a/coretk/coretk/icons/document-save.gif b/coretk/coretk/icons/document-save.gif new file mode 100644 index 0000000000000000000000000000000000000000..165bcb908bbcf12dfb5d0c06a933898ed8f3944c GIT binary patch literal 1049 zcmZ?wbhEHb6krfw_}@YinU=Z|&e<14NFF4ld44ZmzCg z9-cm4UOwJlzTVz`KE46IegS^|fqwo${sBS$K0$$j!2!Xcfx)3cp)oNjndzz7*?C3z zMP~yN%>eKber00=&?_cmGkk~>bNa@fGjG?NeY^JT=M^U&Y&`d1?~!8%PM$h?>ddk8*N$Dd za{9`Zi`TASzIprlov%mkeLME($C-x@?>~Na|H+SwPk&x|{`1<4pVuBfe(>n=gU62_ zK7R7>$Hy=K} z|M>C!r_Ue1e*OIQ+n2B3zJB}m^~aANzyJLH144iP{{8#^|9^&2K;sZl{K>+|z|h5@ z1F{E{Cm1-sF^F=Ccsy!8$gjkvB_r{uXI8tKnApdQix+hmo2Jb%Ol)TFmv^iC!Pvqf zz}X|^|3g!G#rb}@hBp$Mij5v0GmpRb7dnSnV*-j_uJ)us;Cr9J~LHYW5xwW<>NUDdbt97Hy(V% z$)(wGNaN(?!^}$Z!Q1vIwxqgFGm4t@LgC3m=0&Gx2#Oj_k-W&p?c^l?(@>>DK-r^7 z#8T-<69WeWk4D0U1x##QykZIr7Y;NuvhgYeEGT%$)W&^S(;-8_p_!5K0y9g4-(LnM KMj0k125SJ$J7sYI literal 0 HcmV?d00001 diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index f25b0eb1..03e7fc01 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -101,6 +101,7 @@ class ImageEnum(Enum): OBSERVE = "observe" RUN = "run" DOCUMENTNEW = "document-new" + DOCUMENTSAVE = "document-save" FILEOPEN = "fileopen" EDITDELETE = "edit-delete" ANTENNA = "antenna" From 469e32b8909053bb4e262f0c892c4edf34447505 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 12 Nov 2019 12:13:53 -0800 Subject: [PATCH 218/462] finished converting dialogs to use ttk --- coretk/coretk/dialogs/mobilityconfig.py | 67 ++++++++++++------------- coretk/coretk/dialogs/nodeconfig.py | 22 ++++---- coretk/coretk/dialogs/nodeservice.py | 30 +++++------ coretk/coretk/dialogs/wlanconfig.py | 64 +++++++++++------------ 4 files changed, 87 insertions(+), 96 deletions(-) diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/coretk/coretk/dialogs/mobilityconfig.py index 7f99df18..68c557ee 100644 --- a/coretk/coretk/dialogs/mobilityconfig.py +++ b/coretk/coretk/dialogs/mobilityconfig.py @@ -2,11 +2,10 @@ mobility configuration """ -import os import tkinter as tk -from pathlib import Path -from tkinter import filedialog +from tkinter import filedialog, ttk +from coretk import appconfig from coretk.dialogs.dialog import Dialog @@ -40,12 +39,12 @@ class MobilityConfiguration(Dialog): return var def open_file(self, entry): - configs_dir = os.path.join(Path.home(), ".core/configs") - if os.path.isdir(configs_dir): - filename = filedialog.askopenfilename(initialdir=configs_dir, title="Open") - if filename: - entry.delete(0, tk.END) - entry.insert(0, filename) + filename = filedialog.askopenfilename( + initialdir=str(appconfig.MOBILITY_PATH), title="Open" + ) + if filename: + entry.delete(0, tk.END) + entry.insert(0, filename) def set_loop_value(self, value): """ @@ -58,26 +57,26 @@ class MobilityConfiguration(Dialog): def create_label_entry_filebrowser( self, parent_frame, text_label, entry_text, filebrowser=False ): - f = tk.Frame(parent_frame, bg="#d9d9d9") - lbl = tk.Label(f, text=text_label, bg="#d9d9d9") + f = ttk.Frame(parent_frame, bg="#d9d9d9") + lbl = ttk.Label(f, text=text_label, bg="#d9d9d9") lbl.grid(padx=3, pady=3) # f.grid() - e = tk.Entry(f, textvariable=self.create_string_var(entry_text), bg="#ffffff") + e = ttk.Entry(f, textvariable=self.create_string_var(entry_text), bg="#ffffff") e.grid(row=0, column=1, padx=3, pady=3) if filebrowser: - b = tk.Button(f, text="...", command=lambda: self.open_file(e)) + b = ttk.Button(f, text="...", command=lambda: self.open_file(e)) b.grid(row=0, column=2, padx=3, pady=3) f.grid(sticky=tk.E) def mobility_script_parameters(self): - lbl = tk.Label(self, text="node ns2script") - lbl.grid(sticky=tk.W + tk.E) + lbl = ttk.Label(self, text="node ns2script") + lbl.grid(sticky="ew") - sb = tk.Scrollbar(self, orient=tk.VERTICAL) - sb.grid(row=1, column=1, sticky=tk.N + tk.S + tk.E) + sb = ttk.Scrollbar(self, orient=tk.VERTICAL) + sb.grid(row=1, column=1, sticky="ns") - f = tk.Frame(self, bg="#d9d9d9") - lbl = tk.Label( + f = ttk.Frame(self, bg="#d9d9d9") + lbl = ttk.Label( f, text="ns-2 Mobility Scripts Parameters", bg="#d9d9d9", relief=tk.RAISED ) lbl.grid(row=0, column=0, sticky=tk.W) @@ -99,21 +98,21 @@ class MobilityConfiguration(Dialog): f1, "Refresh time (ms)", self.node_config["refresh_ms"] ) - # f12 = tk.Frame(f1) + # f12 = ttk.Frame(f1) # - # lbl = tk.Label(f12, text="Refresh time (ms)") + # lbl = ttk.Label(f12, text="Refresh time (ms)") # lbl.grid() # - # e = tk.Entry(f12, textvariable=self.create_string_var("50")) + # e = ttk.Entry(f12, textvariable=self.create_string_var("50")) # e.grid(row=0, column=1) # f12.grid() - f13 = tk.Frame(f1) + f13 = ttk.Frame(f1) - lbl = tk.Label(f13, text="loop") + lbl = ttk.Label(f13, text="loop") lbl.grid() - om = tk.OptionMenu( + om = ttk.OptionMenu( f13, self.create_string_var("On"), "On", "Off", command=self.set_loop_value ) om.grid(row=0, column=1) @@ -123,24 +122,24 @@ class MobilityConfiguration(Dialog): self.create_label_entry_filebrowser( f1, "auto-start seconds (0.0 for runtime)", self.node_config["autostart"] ) - # f14 = tk.Frame(f1) + # f14 = ttk.Frame(f1) # - # lbl = tk.Label(f14, text="auto-start seconds (0.0 for runtime)") + # lbl = ttk.Label(f14, text="auto-start seconds (0.0 for runtime)") # lbl.grid() # - # e = tk.Entry(f14, textvariable=self.create_string_var("")) + # e = ttk.Entry(f14, textvariable=self.create_string_var("")) # e.grid(row=0, column=1) # # f14.grid() self.create_label_entry_filebrowser( f1, "node mapping (optional, e.g. 0:1, 1:2, 2:3)", self.node_config["map"] ) - # f15 = tk.Frame(f1) + # f15 = ttk.Frame(f1) # - # lbl = tk.Label(f15, text="node mapping (optional, e.g. 0:1, 1:2, 2:3)") + # lbl = ttk.Label(f15, text="node mapping (optional, e.g. 0:1, 1:2, 2:3)") # lbl.grid() # - # e = tk.Entry(f15, textvariable=self.create_string_var("")) + # e = ttk.Entry(f15, textvariable=self.create_string_var("")) # e.grid(row=0, column=1) # # f15.grid() @@ -230,9 +229,9 @@ class MobilityConfiguration(Dialog): :return: nothing """ - f = tk.Frame(self) - b = tk.Button(f, text="Apply", command=self.ns2script_apply) + f = ttk.Frame(self) + b = ttk.Button(f, text="Apply", command=self.ns2script_apply) b.grid() - b = tk.Button(f, text="Cancel", command=self.destroy) + b = ttk.Button(f, text="Cancel", command=self.destroy) b.grid(row=0, column=1) f.grid() diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 3f13488a..be41afb5 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -1,13 +1,11 @@ import tkinter as tk from tkinter import ttk +from coretk.coreclient import DEFAULT_NODES from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeServicesDialog -NETWORKNODETYPES = ["switch", "hub", "wlan", "rj45", "tunnel"] -DEFAULTNODES = ["router", "host", "PC"] - class NodeConfigDialog(Dialog): def __init__(self, master, app, canvas_node): @@ -34,17 +32,17 @@ class NodeConfigDialog(Dialog): self.draw_third_row() def draw_first_row(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(row=0, column=0, pady=2, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) - entry = tk.Entry(frame, textvariable=self.name) + entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=0, padx=2, sticky="ew") combobox = ttk.Combobox( - frame, textvariable=self.type, values=DEFAULTNODES, state="readonly" + frame, textvariable=self.type, values=DEFAULT_NODES, state="readonly" ) combobox.grid(row=0, column=1, padx=2, sticky="ew") @@ -57,15 +55,15 @@ class NodeConfigDialog(Dialog): combobox.grid(row=0, column=2, sticky="ew") def draw_second_row(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(row=1, column=0, pady=2, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - button = tk.Button(frame, text="Services", command=self.click_services) + button = ttk.Button(frame, text="Services", command=self.click_services) button.grid(row=0, column=0, padx=2, sticky="ew") - self.image_button = tk.Button( + self.image_button = ttk.Button( frame, text="Icon", image=self.image, @@ -75,15 +73,15 @@ class NodeConfigDialog(Dialog): self.image_button.grid(row=0, column=1, sticky="ew") def draw_third_row(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(row=2, column=0, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - button = tk.Button(frame, text="Apply", command=self.config_apply) + button = ttk.Button(frame, text="Apply", command=self.config_apply) button.grid(row=0, column=0, padx=2, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") def click_services(self): diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 6d92b98e..fe31457c 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -2,7 +2,7 @@ core node services """ import tkinter as tk -from tkinter import messagebox +from tkinter import messagebox, ttk from coretk.dialogs.dialog import Dialog @@ -20,7 +20,7 @@ class NodeServicesDialog(Dialog): def draw(self): self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) - self.config_frame = tk.Frame(self) + self.config_frame = ttk.Frame(self) self.config_frame.columnconfigure(0, weight=1) self.config_frame.columnconfigure(1, weight=1) self.config_frame.columnconfigure(2, weight=1) @@ -37,15 +37,15 @@ class NodeServicesDialog(Dialog): :return: nothing """ - frame = tk.Frame(self.config_frame) + frame = ttk.Frame(self.config_frame) frame.columnconfigure(0, weight=1) frame.rowconfigure(1, weight=1) frame.grid(row=0, column=0, padx=3, pady=3, sticky="nsew") - label = tk.Label(frame, text="Group") + label = ttk.Label(frame, text="Group") label.grid(row=0, column=0, sticky="ew") - scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL) scrollbar.grid(row=1, column=1, sticky="ns") listbox = tk.Listbox( @@ -65,15 +65,15 @@ class NodeServicesDialog(Dialog): scrollbar.config(command=listbox.yview) def draw_services(self): - frame = tk.Frame(self.config_frame) + frame = ttk.Frame(self.config_frame) frame.columnconfigure(0, weight=1) frame.rowconfigure(1, weight=1) frame.grid(row=0, column=1, padx=3, pady=3, sticky="nsew") - label = tk.Label(frame, text="Group services") + label = ttk.Label(frame, text="Group services") label.grid(row=0, column=0, sticky="ew") - scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL) scrollbar.grid(row=1, column=1, sticky="ns") self.services_list = tk.Listbox( @@ -90,15 +90,15 @@ class NodeServicesDialog(Dialog): scrollbar.config(command=self.services_list.yview) def draw_current_services(self): - frame = tk.Frame(self.config_frame) + frame = ttk.Frame(self.config_frame) frame.columnconfigure(0, weight=1) frame.rowconfigure(1, weight=1) frame.grid(row=0, column=2, padx=3, pady=3, sticky="nsew") - label = tk.Label(frame, text="Current services") + label = ttk.Label(frame, text="Current services") label.grid(row=0, column=0, sticky="ew") - scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL) scrollbar.grid(row=1, column=1, sticky="ns") listbox = tk.Listbox( @@ -114,19 +114,19 @@ class NodeServicesDialog(Dialog): scrollbar.config(command=listbox.yview) def draw_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) frame.grid(row=1, column=0, sticky="ew") - button = tk.Button(frame, text="Configure", command=self.click_configure) + button = ttk.Button(frame, text="Configure", command=self.click_configure) button.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="Apply") + button = ttk.Button(frame, text="Apply") button.grid(row=0, column=1, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=2, sticky="ew") def handle_group_change(self, event): diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index 78dc2c22..d33f7ca0 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -3,6 +3,7 @@ wlan configuration """ import tkinter as tk +from tkinter import ttk from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog @@ -10,12 +11,6 @@ from coretk.dialogs.icondialog import IconDialog class WlanConfigDialog(Dialog): def __init__(self, master, app, canvas_node, config): - """ - create an instance of WlanConfiguration - - :param coretk.grpah.CanvasGraph canvas: canvas object - :param coretk.graph.CanvasNode canvas_node: canvas node object - """ super().__init__( master, app, f"{canvas_node.name} Wlan Configuration", modal=True ) @@ -48,14 +43,14 @@ class WlanConfigDialog(Dialog): :return: nothing """ - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(pady=2, sticky="ew") frame.columnconfigure(0, weight=1) - entry = tk.Entry(frame, textvariable=self.name, bg="white") + entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=0, padx=2, sticky="ew") - self.image_button = tk.Button(frame, image=self.image, command=self.click_icon) + self.image_button = ttk.Button(frame, image=self.image, command=self.click_icon) self.image_button.grid(row=0, column=1, padx=3) def draw_wlan_config(self): @@ -64,15 +59,15 @@ class WlanConfigDialog(Dialog): :return: nothing """ - label = tk.Label(self, text="Wireless") + label = ttk.Label(self, text="Wireless") label.grid(sticky="w", pady=2) - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(pady=2, sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - label = tk.Label( + label = ttk.Label( frame, text=( "The basic range model calculates on/off " @@ -81,29 +76,29 @@ class WlanConfigDialog(Dialog): ) label.grid(row=0, columnspan=2, pady=2, sticky="ew") - label = tk.Label(frame, text="Range") + label = ttk.Label(frame, text="Range") label.grid(row=1, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.range_var) + entry = ttk.Entry(frame, textvariable=self.range_var) entry.grid(row=1, column=1, sticky="ew") - label = tk.Label(frame, text="Bandwidth (bps)") + label = ttk.Label(frame, text="Bandwidth (bps)") label.grid(row=2, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.bandwidth_var) + entry = ttk.Entry(frame, textvariable=self.bandwidth_var) entry.grid(row=2, column=1, sticky="ew") - label = tk.Label(frame, text="Delay (us)") + label = ttk.Label(frame, text="Delay (us)") label.grid(row=3, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.delay_var) + entry = ttk.Entry(frame, textvariable=self.delay_var) entry.grid(row=3, column=1, sticky="ew") - label = tk.Label(frame, text="Loss (%)") + label = ttk.Label(frame, text="Loss (%)") label.grid(row=4, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.loss_var) + entry = ttk.Entry(frame, textvariable=self.loss_var) entry.grid(row=4, column=1, sticky="ew") - label = tk.Label(frame, text="Jitter (us)") + label = ttk.Label(frame, text="Jitter (us)") label.grid(row=5, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.jitter_var) + entry = ttk.Entry(frame, textvariable=self.jitter_var) entry.grid(row=5, column=1, sticky="ew") def draw_subnet(self): @@ -113,19 +108,19 @@ class WlanConfigDialog(Dialog): :return: nothing """ - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(pady=3, sticky="ew") frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) - label = tk.Label(frame, text="IPv4 Subnet") + label = ttk.Label(frame, text="IPv4 Subnet") label.grid(row=0, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.ip4_subnet) + entry = ttk.Entry(frame, textvariable=self.ip4_subnet) entry.grid(row=0, column=1, sticky="ew") - label = tk.Label(frame, text="IPv6 Subnet") + label = ttk.Label(frame, text="IPv6 Subnet") label.grid(row=0, column=2, sticky="w") - entry = tk.Entry(frame, textvariable=self.ip6_subnet) + entry = ttk.Entry(frame, textvariable=self.ip6_subnet) entry.grid(row=0, column=3, sticky="ew") def draw_wlan_buttons(self): @@ -135,18 +130,18 @@ class WlanConfigDialog(Dialog): :return: """ - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(pady=2, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="ns-2 mobility script...") + button = ttk.Button(frame, text="ns-2 mobility script...") button.grid(row=0, column=0, padx=2, sticky="ew") - button = tk.Button(frame, text="Link to all routers") + button = ttk.Button(frame, text="Link to all routers") button.grid(row=0, column=1, padx=2, sticky="ew") - button = tk.Button(frame, text="Choose WLAN members") + button = ttk.Button(frame, text="Choose WLAN members") button.grid(row=0, column=2, padx=2, sticky="ew") def draw_apply_buttons(self): @@ -155,15 +150,15 @@ class WlanConfigDialog(Dialog): :return: nothing """ - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Apply", command=self.click_apply) + button = ttk.Button(frame, text="Apply", command=self.click_apply) button.grid(row=0, column=0, padx=2, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, padx=2, sticky="ew") def click_icon(self): @@ -188,7 +183,6 @@ class WlanConfigDialog(Dialog): jitter = self.jitter_var.get() # set wireless node configuration here - wlanconfig_manager = self.app.core.wlanconfig_management wlanconfig_manager.set_custom_config( node_id=self.canvas_node.core_id, From 96abea311f32601a7771cb35ac3478a6a03b7dfb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 12 Nov 2019 12:47:29 -0800 Subject: [PATCH 219/462] cleanup for padding to canvas dialogs --- coretk/coretk/dialogs/canvasbackground.py | 13 ++-- coretk/coretk/dialogs/canvassizeandscale.py | 83 +++++++++++---------- coretk/coretk/dialogs/dialog.py | 4 +- 3 files changed, 55 insertions(+), 45 deletions(-) diff --git a/coretk/coretk/dialogs/canvasbackground.py b/coretk/coretk/dialogs/canvasbackground.py index ed7e44f6..8b1c4cf4 100644 --- a/coretk/coretk/dialogs/canvasbackground.py +++ b/coretk/coretk/dialogs/canvasbackground.py @@ -11,6 +11,8 @@ from PIL import Image, ImageTk from coretk.appconfig import BACKGROUNDS_PATH from coretk.dialogs.dialog import Dialog +PADX = 5 + class ScaleOption(enum.Enum): NONE = 0 @@ -65,10 +67,10 @@ class CanvasBackgroundDialog(Dialog): entry = ttk.Entry(frame, textvariable=self.file_name) entry.focus() - entry.grid(row=0, column=0, sticky="ew") + entry.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="...", command=self.click_open_image) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Clear", command=self.click_clear) button.grid(row=0, column=2, sticky="ew") @@ -105,7 +107,7 @@ class CanvasBackgroundDialog(Dialog): checkbutton = ttk.Checkbutton( self, text="Show grid", variable=self.show_grid_var ) - checkbutton.grid(row=4, column=0, sticky="ew", padx=5) + checkbutton.grid(row=4, column=0, sticky="ew", padx=PADX) checkbutton = ttk.Checkbutton( self, @@ -113,7 +115,7 @@ class CanvasBackgroundDialog(Dialog): variable=self.adjust_to_dim_var, command=self.click_adjust_canvas, ) - checkbutton.grid(row=5, column=0, sticky="ew", padx=5) + checkbutton.grid(row=5, column=0, sticky="ew", padx=PADX) self.show_grid_var.set(1) self.adjust_to_dim_var.set(0) @@ -125,7 +127,7 @@ class CanvasBackgroundDialog(Dialog): frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="ew") + 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") @@ -206,7 +208,6 @@ class CanvasBackgroundDialog(Dialog): return def upper_left(self, img): - print("upperleft") tk_img = ImageTk.PhotoImage(img) # crop image if it is bigger than canvas diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 1d880639..1af71c93 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -8,6 +8,9 @@ from coretk.dialogs.canvasbackground import ScaleOption from coretk.dialogs.dialog import Dialog DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] +FRAME_BAD = 5 +PAD = (0, 0, 5, 0) +PADX = 5 class SizeAndScaleDialog(Dialog): @@ -49,101 +52,105 @@ class SizeAndScaleDialog(Dialog): self.draw_buttons() def draw_size(self): - label = ttk.Label(self, text="Size", font=self.section_font) - label.grid(sticky="w") + label_frame = ttk.Labelframe(self, text="Size", padding=FRAME_BAD) + label_frame.grid(sticky="ew") + label_frame.columnconfigure(0, weight=1) # draw size row 1 - frame = ttk.Frame(self) + frame = ttk.Frame(label_frame) frame.grid(sticky="ew", pady=3) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") - label.grid(row=0, column=0, sticky="w") + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.pixel_width) - entry.grid(row=0, column=1, sticky="ew") + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="x Height") - label.grid(row=0, column=2, sticky="w") + label.grid(row=0, column=2, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.pixel_height) - entry.grid(row=0, column=3, sticky="ew") + entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Pixels") label.grid(row=0, column=4, sticky="w") # draw size row 2 - frame = ttk.Frame(self) + frame = ttk.Frame(label_frame) frame.grid(sticky="ew", pady=3) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") - label.grid(row=0, column=0, sticky="w") + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.meters_width) - entry.grid(row=0, column=1, sticky="ew") + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="x Height") - label.grid(row=0, column=2, sticky="w") + label.grid(row=0, column=2, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.meters_height) - entry.grid(row=0, column=3, sticky="ew") + entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=4, sticky="w") def draw_scale(self): - label = ttk.Label(self, text="Scale", font=self.section_font) - label.grid(sticky="w") + label_frame = ttk.Labelframe(self, text="Scale", padding=FRAME_BAD) + label_frame.grid(sticky="ew") + label_frame.columnconfigure(0, weight=1) - frame = ttk.Frame(self) + frame = ttk.Frame(label_frame) frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="100 Pixels =") - label.grid(row=0, column=0, sticky="w") + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.scale) - entry.grid(row=0, column=1, sticky="ew") + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=2, sticky="w") def draw_reference_point(self): - label = ttk.Label(self, text="Reference point", font=self.section_font) - label.grid(sticky="w") - label = ttk.Label( - self, text="Default is (0, 0), the upper left corner of the canvas" - ) - label.grid(sticky="w") + label_frame = ttk.Labelframe(self, text="Reference Point", padding=FRAME_BAD) + label_frame.grid(sticky="ew") + label_frame.columnconfigure(0, weight=1) - frame = ttk.Frame(self) + label = ttk.Label( + label_frame, text="Default is (0, 0), the upper left corner of the canvas" + ) + label.grid() + + frame = ttk.Frame(label_frame) frame.grid(sticky="ew", pady=3) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="X") - label.grid(row=0, column=0, sticky="w") + label.grid(row=0, column=0, sticky="w", padx=PADX) x_var = tk.StringVar(value=0) entry = ttk.Entry(frame, textvariable=x_var) - entry.grid(row=0, column=1, sticky="ew") + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Y") - label.grid(row=0, column=2, sticky="w") + label.grid(row=0, column=2, sticky="w", padx=PADX) y_var = tk.StringVar(value=0) entry = ttk.Entry(frame, textvariable=y_var) - entry.grid(row=0, column=3, sticky="ew") + entry.grid(row=0, column=3, sticky="ew", padx=PADX) - label = ttk.Label(self, text="Translates To") - label.grid(sticky="w") + label = ttk.Label(label_frame, text="Translates To") + label.grid() - frame = ttk.Frame(self) + frame = ttk.Frame(label_frame) frame.grid(sticky="ew", pady=3) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) frame.columnconfigure(5, weight=1) label = ttk.Label(frame, text="Lat") - label.grid(row=0, column=0, sticky="w") + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.lat) - entry.grid(row=0, column=1, sticky="ew") + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Lon") - label.grid(row=0, column=2, sticky="w") + label.grid(row=0, column=2, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.lon) - entry.grid(row=0, column=3, sticky="ew") + entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Alt") - label.grid(row=0, column=4, sticky="w") + label.grid(row=0, column=4, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.alt) entry.grid(row=0, column=5, sticky="ew") @@ -160,10 +167,10 @@ class SizeAndScaleDialog(Dialog): frame.grid(sticky="ew") button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, pady=5, sticky="ew") + 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, pady=5, sticky="ew") + button.grid(row=0, column=1, sticky="ew") def redraw_grid(self): """ diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index f9cfcabe..908523f2 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -2,10 +2,12 @@ import tkinter as tk from coretk.images import ImageEnum, Images +DIALOG_PAD = 5 + class Dialog(tk.Toplevel): def __init__(self, master, app, title, modal=False): - super().__init__(master, padx=5, pady=5) + super().__init__(master, padx=DIALOG_PAD, pady=DIALOG_PAD) self.withdraw() self.app = app self.modal = modal From b96f8ff999f0fc9194d5cd7e78b010e30d722652 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 12 Nov 2019 13:01:58 -0800 Subject: [PATCH 220/462] updated standard tooltip to use delays and have the same style of the canvas tooltip --- coretk/coretk/toolbar.py | 14 +++++++------- coretk/coretk/tooltip.py | 34 +++++++++++++++++++++++++++------- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 5b2c83bd..752b1000 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -5,7 +5,7 @@ from functools import partial from coretk.dialogs.customnodes import CustomNodesDialog from coretk.graph import GraphMode from coretk.images import ImageEnum, Images -from coretk.tooltip import CreateToolTip +from coretk.tooltip import Tooltip class Toolbar(tk.Frame): @@ -188,7 +188,7 @@ class Toolbar(tk.Frame): button = tk.Button(frame, width=self.width, height=self.height, image=image) button.bind("", lambda e: func()) button.grid(pady=1) - CreateToolTip(button, tooltip) + Tooltip(button, tooltip) def create_radio_button(self, frame, image, func, variable, value, tooltip_msg): button = tk.Radiobutton( @@ -202,14 +202,14 @@ class Toolbar(tk.Frame): command=func, ) button.grid() - CreateToolTip(button, tooltip_msg) + Tooltip(button, tooltip_msg) def create_regular_button(self, frame, image, func, tooltip): button = tk.Button( frame, width=self.width, height=self.height, image=image, command=func ) button.grid() - CreateToolTip(button, tooltip) + Tooltip(button, tooltip) def click_selection_tool(self): logging.debug("clicked selection tool") @@ -274,7 +274,7 @@ class Toolbar(tk.Frame): ) self.node_button.bind("", lambda e: self.draw_node_picker()) self.node_button.grid() - CreateToolTip(self.node_button, "Network-layer virtual nodes") + Tooltip(self.node_button, "Network-layer virtual nodes") def draw_network_picker(self): """ @@ -323,7 +323,7 @@ class Toolbar(tk.Frame): "", lambda e: self.draw_network_picker() ) self.network_button.grid() - CreateToolTip(self.network_button, "link-layer nodes") + Tooltip(self.network_button, "link-layer nodes") def draw_annotation_picker(self): """ @@ -369,7 +369,7 @@ class Toolbar(tk.Frame): "", lambda e: self.draw_annotation_picker() ) self.annotation_button.grid() - CreateToolTip(self.annotation_button, "background annotation tools") + Tooltip(self.annotation_button, "background annotation tools") def create_observe_button(self): menu_button = tk.Menubutton( diff --git a/coretk/coretk/tooltip.py b/coretk/coretk/tooltip.py index 6fbbc3c9..1877fd24 100644 --- a/coretk/coretk/tooltip.py +++ b/coretk/coretk/tooltip.py @@ -1,7 +1,8 @@ import tkinter as tk +from tkinter import ttk -class CreateToolTip(object): +class Tooltip(object): """ Create tool tip for a given widget """ @@ -9,10 +10,29 @@ class CreateToolTip(object): def __init__(self, widget, text="widget info"): self.widget = widget self.text = text - self.widget.bind("", self.enter) - self.widget.bind("", self.close) + self.widget.bind("", self.on_enter) + self.widget.bind("", self.on_leave) + self.waittime = 400 + self.id = None self.tw = None + def on_enter(self, event=None): + self.schedule() + + def on_leave(self, event=None): + self.unschedule() + self.close(event) + + def schedule(self): + self.unschedule() + self.id = self.widget.after(self.waittime, self.enter) + + def unschedule(self): + id_ = self.id + self.id = None + if id_: + self.widget.after_cancel(id_) + def enter(self, event=None): x, y, cx, cy = self.widget.bbox("insert") x += self.widget.winfo_rootx() @@ -21,13 +41,13 @@ class CreateToolTip(object): self.tw = tk.Toplevel(self.widget) self.tw.wm_overrideredirect(True) self.tw.wm_geometry("+%d+%d" % (x, y)) - label = tk.Label( + label = ttk.Label( self.tw, text=self.text, justify=tk.LEFT, - background="#ffffe6", - relief="solid", - borderwidth=1, + background="#FFFFEA", + relief=tk.SOLID, + borderwidth=0, ) label.grid(padx=1) From 14187ba79c4471302a53660e248242668e139268 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 12 Nov 2019 17:32:34 -0800 Subject: [PATCH 221/462] tweaks to retrieving images/icons, updates to toolbar to use ttk widgets --- coretk/coretk/app.py | 9 +- coretk/coretk/coreclient.py | 4 +- coretk/coretk/dialogs/dialog.py | 2 +- coretk/coretk/dialogs/emaneconfig.py | 14 +- coretk/coretk/dialogs/icondialog.py | 2 +- coretk/coretk/dialogs/sessions.py | 20 +- coretk/coretk/graph.py | 4 +- coretk/coretk/graph_helper.py | 3 +- coretk/coretk/images.py | 90 ++++---- coretk/coretk/toolbar.py | 307 ++++++++++++--------------- 10 files changed, 221 insertions(+), 234 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 276dbd6d..452d5397 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,5 +1,6 @@ import logging import tkinter as tk +from tkinter import ttk from coretk import appconfig from coretk.coreclient import CoreClient @@ -36,7 +37,7 @@ class Application(tk.Frame): self.master.title("CORE") self.master.geometry("1000x800") self.master.protocol("WM_DELETE_WINDOW", self.on_closing) - image = Images.get(ImageEnum.CORE) + image = Images.get(ImageEnum.CORE, 16) self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) @@ -53,17 +54,17 @@ class Application(tk.Frame): self, self.core, background="#cccccc", scrollregion=(0, 0, 1200, 1000) ) self.canvas.pack(fill=tk.BOTH, expand=True) - scroll_x = tk.Scrollbar( + scroll_x = ttk.Scrollbar( self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview ) scroll_x.pack(side=tk.BOTTOM, fill=tk.X) - scroll_y = tk.Scrollbar(self.canvas, command=self.canvas.yview) + scroll_y = ttk.Scrollbar(self.canvas, command=self.canvas.yview) scroll_y.pack(side=tk.RIGHT, fill=tk.Y) self.canvas.configure(xscrollcommand=scroll_x.set) self.canvas.configure(yscrollcommand=scroll_y.set) def draw_status(self): - self.statusbar = tk.Frame(self) + self.statusbar = ttk.Frame(self) self.statusbar.pack(side=tk.BOTTOM, fill=tk.X) def on_closing(self): diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 9e18a413..d28ad49e 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -8,7 +8,7 @@ from core.api.grpc import client, core_pb2 from coretk.coretocanvas import CoreToCanvasMapping from coretk.dialogs.sessions import SessionsDialog from coretk.emaneodelnodeconfig import EmaneModelNodeConfig -from coretk.images import Images +from coretk.images import NODE_WIDTH, Images from coretk.interface import Interface, InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.wlannodeconfig import WlanNodeConfig @@ -137,7 +137,7 @@ class CoreClient: # read custom nodes for config in self.app.config.get("nodes", []): image_file = config["image"] - image = Images.get_custom(image_file) + image = Images.get_custom(image_file, NODE_WIDTH) custom_node = CustomNode( config["name"], image, image_file, set(config["services"]) ) diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index 908523f2..c043a47a 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -13,7 +13,7 @@ class Dialog(tk.Toplevel): self.modal = modal self.title(title) self.protocol("WM_DELETE_WINDOW", self.destroy) - image = Images.get(ImageEnum.CORE) + image = Images.get(ImageEnum.CORE, 16) self.tk.call("wm", "iconphoto", self._w, image) def show(self): diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index 7fd577e2..8673d3fc 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -182,25 +182,31 @@ class EmaneConfiguration(Dialog): def draw_option_buttons(self, parent): f = ttk.Frame(parent) + f.grid(row=4, column=0, sticky="nsew") f.columnconfigure(0, weight=1) f.columnconfigure(1, weight=1) + + image = Images.get(ImageEnum.EDITNODE, 16) b = ttk.Button( f, text=self.emane_models[0] + " options", - image=Images.get(ImageEnum.EDITNODE), + image=image, compound=tk.RIGHT, command=self.draw_model_options, ) + b.image = image b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") + + image = Images.get(ImageEnum.EDITNODE, 16) b = ttk.Button( f, text="EMANE options", - image=Images.get(ImageEnum.EDITNODE), + image=image, compound=tk.RIGHT, command=self.draw_emane_options, ) + b.image = image b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") - f.grid(row=4, column=0, sticky="nsew") def combobox_select(self, event): """ @@ -271,7 +277,7 @@ class EmaneConfiguration(Dialog): b = ttk.Button( f, - image=Images.get(ImageEnum.EDITNODE), + image=Images.get(ImageEnum.EDITNODE, 8), text="EMANE Wiki", compound=tk.RIGHT, command=lambda: webbrowser.open_new( diff --git a/coretk/coretk/dialogs/icondialog.py b/coretk/coretk/dialogs/icondialog.py index fb6fb6bb..15d8000b 100644 --- a/coretk/coretk/dialogs/icondialog.py +++ b/coretk/coretk/dialogs/icondialog.py @@ -54,7 +54,7 @@ class IconDialog(Dialog): ), ) if file_path: - self.image = Images.create(file_path) + self.image = Images.create(file_path, 32, 32) self.image_label.config(image=self.image) self.file_path.set(file_path) diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index 40d09c44..b9b7d77b 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -73,30 +73,36 @@ class SessionsDialog(Dialog): for i in range(4): frame.columnconfigure(i, weight=1) frame.grid(row=3, sticky="ew") + + image = Images.get(ImageEnum.DOCUMENTNEW, 16) b = ttk.Button( - frame, - image=Images.get(ImageEnum.DOCUMENTNEW), - text="New", - compound=tk.LEFT, - command=self.click_new, + frame, image=image, text="New", compound=tk.LEFT, command=self.click_new ) + b.image = image b.grid(row=0, padx=2, sticky="ew") + + image = Images.get(ImageEnum.FILEOPEN, 16) b = ttk.Button( frame, - image=Images.get(ImageEnum.FILEOPEN), + image=image, text="Connect", compound=tk.LEFT, command=self.click_connect, ) + b.image = image b.grid(row=0, column=1, padx=2, sticky="ew") + + image = Images.get(ImageEnum.EDITDELETE, 16) b = ttk.Button( frame, - image=Images.get(ImageEnum.EDITDELETE), + image=image, text="Shutdown", compound=tk.LEFT, command=self.click_shutdown, ) + b.image = image b.grid(row=0, column=2, padx=2, sticky="ew") + b = ttk.Button(frame, text="Cancel", command=self.click_new) b.grid(row=0, column=3, padx=2, sticky="ew") diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index e813ae48..5cdd2eec 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -146,9 +146,7 @@ class CanvasGraph(tk.Canvas): # peer to peer node is not drawn on the GUI if node.type != core_pb2.NodeType.PEER_TO_PEER: # draw nodes on the canvas - image, name = Images.convert_type_and_model_to_image( - node.type, node.model - ) + image, name = Images.node_icon(node.type, node.model) n = CanvasNode( node.position.x, node.position.y, image, name, self.master, node.id ) diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py index bdedd65e..c1f59815 100644 --- a/coretk/coretk/graph_helper.py +++ b/coretk/coretk/graph_helper.py @@ -77,6 +77,7 @@ class WlanAntennaManager: self.quantity = 0 self._max = 5 self.antennas = [] + self.image = Images.get(ImageEnum.ANTENNA, 32) # distance between each antenna self.offset = 0 @@ -94,7 +95,7 @@ class WlanAntennaManager: x - 16 + self.offset, y - 16, anchor=tk.CENTER, - image=Images.get(ImageEnum.ANTENNA), + image=self.image, tags="antenna", ) ) diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index d064f77e..cebcb49c 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -6,35 +6,37 @@ from PIL import Image, ImageTk from core.api.grpc import core_pb2 from coretk.appconfig import LOCAL_ICONS_PATH +NODE_WIDTH = 32 + class Images: images = {} @classmethod - def create(cls, file_path): + def create(cls, file_path, width, height=None): + if height is None: + height = width image = Image.open(file_path) + image = image.resize((width, height), Image.ANTIALIAS) return ImageTk.PhotoImage(image) @classmethod def load_all(cls): for image in LOCAL_ICONS_PATH.glob("*"): - cls.load(image.stem, str(image)) + cls.images[image.stem] = str(image) @classmethod - def load(cls, name, file_path): - tk_image = cls.create(file_path) - cls.images[name] = tk_image + def get(cls, image_enum, width, height=None): + file_path = cls.images[image_enum.value] + return cls.create(file_path, width, height) @classmethod - def get(cls, image): - return cls.images[image.value] + def get_custom(cls, name, width, height): + file_path = cls.images[name] + return cls.create(file_path, width, height) @classmethod - def get_custom(cls, name): - return cls.images[name] - - @classmethod - def convert_type_and_model_to_image(cls, node_type, node_model): + def node_icon(cls, node_type, node_model): """ Retrieve image based on type and model :param core_pb2.NodeType node_type: core node type @@ -43,34 +45,48 @@ class Images: :rtype: tuple(PhotoImage, str) :return: the matching image and its name """ + image_enum = ImageEnum.ROUTER + name = "unknown" if node_type == core_pb2.NodeType.SWITCH: - return Images.get(ImageEnum.SWITCH), "switch" - if node_type == core_pb2.NodeType.HUB: - return Images.get(ImageEnum.HUB), "hub" - if node_type == core_pb2.NodeType.WIRELESS_LAN: - return Images.get(ImageEnum.WLAN), "wlan" - if node_type == core_pb2.NodeType.EMANE: - return Images.get(ImageEnum.EMANE), "emane" - - if node_type == core_pb2.NodeType.RJ45: - return Images.get(ImageEnum.RJ45), "rj45" - if node_type == core_pb2.NodeType.TUNNEL: - return Images.get(ImageEnum.TUNNEL), "tunnel" - if node_type == core_pb2.NodeType.DEFAULT: + image_enum = ImageEnum.SWITCH + name = "switch" + elif node_type == core_pb2.NodeType.HUB: + image_enum = ImageEnum.HUB + name = "hub" + elif node_type == core_pb2.NodeType.WIRELESS_LAN: + image_enum = ImageEnum.WLAN + name = "wlan" + elif node_type == core_pb2.NodeType.EMANE: + image_enum = ImageEnum.EMANE + name = "emane" + elif node_type == core_pb2.NodeType.RJ45: + image_enum = ImageEnum.RJ45 + name = "rj45" + elif node_type == core_pb2.NodeType.TUNNEL: + image_enum = ImageEnum.TUNNEL + name = "tunnel" + elif node_type == core_pb2.NodeType.DEFAULT: if node_model == "router": - return Images.get(ImageEnum.ROUTER), "router" - if node_model == "host": - return Images.get(ImageEnum.HOST), "host" - if node_model == "PC": - return Images.get(ImageEnum.PC), "PC" - if node_model == "mdr": - return Images.get(ImageEnum.MDR), "mdr" - if node_model == "prouter": - return Images.get(ImageEnum.PROUTER), "prouter" - if node_model == "OVS": - return Images.get(ImageEnum.OVS), "ovs" + image_enum = ImageEnum.ROUTER + name = "router" + elif node_model == "host": + image_enum = ImageEnum.HOST + name = "host" + elif node_model == "PC": + image_enum = ImageEnum.PC + name = "PC" + elif node_model == "mdr": + image_enum = ImageEnum.MDR + name = "mdr" + elif node_model == "prouter": + image_enum = ImageEnum.PROUTER + name = "prouter" + else: + logging.error("invalid node model: %s", node_model) else: - logging.debug("INVALID INPUT OR NOT CONSIDERED YET") + logging.error("invalid node type: %s", node_type) + + return Images.get(image_enum, NODE_WIDTH), name class ImageEnum(Enum): diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 752b1000..89393c3e 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -1,43 +1,44 @@ import logging import tkinter as tk from functools import partial +from tkinter import ttk from coretk.dialogs.customnodes import CustomNodesDialog from coretk.graph import GraphMode from coretk.images import ImageEnum, Images from coretk.tooltip import Tooltip +WIDTH = 32 -class Toolbar(tk.Frame): + +def icon(image_enum): + return Images.get(image_enum, WIDTH) + + +class Toolbar(ttk.Frame): """ Core toolbar class """ - def __init__(self, master, app, cnf={}, **kwargs): + def __init__(self, master, app, **kwargs): """ Create a CoreToolbar instance :param tkinter.Frame edit_frame: edit frame """ - super().__init__(master, cnf, **kwargs) + super().__init__(master, **kwargs) self.app = app self.master = app.master - self.radio_value = tk.IntVar() - self.exec_radio_value = tk.IntVar() - # button dimension - self.width = 32 - self.height = 32 - - # Reference to the option menus - self.selection_tool_button = None - self.link_layer_option_menu = None - self.marker_option_menu = None - self.network_layer_option_menu = None + # design buttons + self.select_button = None + self.link_button = None self.node_button = None self.network_button = None self.annotation_button = None + # runtime buttons + # frames self.design_frame = None self.runtime_frame = None @@ -56,89 +57,77 @@ class Toolbar(tk.Frame): self.design_frame.tkraise() def draw_design_frame(self): - self.design_frame = tk.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_regular_button( + self.create_button( self.design_frame, - Images.get(ImageEnum.START), - self.click_start_session_tool, + icon(ImageEnum.START), + self.click_start, "start the session", ) - self.create_radio_button( + self.select_button = self.create_button( self.design_frame, - Images.get(ImageEnum.SELECT), - self.click_selection_tool, - self.radio_value, - 1, + icon(ImageEnum.SELECT), + self.click_selection, "selection tool", ) - self.create_radio_button( - self.design_frame, - Images.get(ImageEnum.LINK), - self.click_link_tool, - self.radio_value, - 2, - "link tool", + self.link_button = self.create_button( + self.design_frame, icon(ImageEnum.LINK), self.click_link, "link tool" ) self.create_node_button() self.create_network_button() self.create_annotation_button() - self.radio_value.set(1) + + def design_select(self, button): + logging.info("selecting design button: %s", button) + self.select_button.state(["!pressed"]) + self.link_button.state(["!pressed"]) + self.node_button.state(["!pressed"]) + self.network_button.state(["!pressed"]) + self.annotation_button.state(["!pressed"]) + button.state(["pressed"]) def draw_runtime_frame(self): - self.runtime_frame = tk.Frame(self) + self.runtime_frame = ttk.Frame(self) self.runtime_frame.grid(row=0, column=0, sticky="nsew") self.runtime_frame.columnconfigure(0, weight=1) - self.create_regular_button( + self.create_button( self.runtime_frame, - Images.get(ImageEnum.STOP), - self.click_stop_button, + icon(ImageEnum.STOP), + self.click_stop, "stop the session", ) - self.create_radio_button( + self.create_button( self.runtime_frame, - Images.get(ImageEnum.SELECT), - self.click_selection_tool, - self.exec_radio_value, - 1, + icon(ImageEnum.SELECT), + self.click_selection, "selection tool", ) - self.create_observe_button() - self.create_radio_button( - self.runtime_frame, - Images.get(ImageEnum.PLOT), - self.click_plot_button, - self.exec_radio_value, - 2, - "plot", + # self.create_observe_button() + self.create_button( + self.runtime_frame, icon(ImageEnum.PLOT), self.click_plot_button, "plot" ) - self.create_radio_button( + self.create_button( self.runtime_frame, - Images.get(ImageEnum.MARKER), + icon(ImageEnum.MARKER), self.click_marker_button, - self.exec_radio_value, - 3, "marker", ) - self.create_radio_button( + self.create_button( self.runtime_frame, - Images.get(ImageEnum.TWONODE), + icon(ImageEnum.TWONODE), self.click_two_node_button, - self.exec_radio_value, - 4, "run command from one node to another", ) - self.create_regular_button( - self.runtime_frame, Images.get(ImageEnum.RUN), self.click_run_button, "run" + self.create_button( + self.runtime_frame, icon(ImageEnum.RUN), self.click_run_button, "run" ) - self.exec_radio_value.set(1) def draw_node_picker(self): self.hide_pickers() - self.node_picker = tk.Frame(self.master, padx=1, pady=1) + self.node_picker = ttk.Frame(self.master) nodes = [ (ImageEnum.ROUTER, "router"), (ImageEnum.HOST, "host"), @@ -148,26 +137,28 @@ class Toolbar(tk.Frame): ] # draw default nodes for image_enum, tooltip in nodes: - image = Images.get(image_enum) + image = icon(image_enum) func = partial(self.update_button, self.node_button, image, tooltip) - self.create_button(image, func, self.node_picker, tooltip) + self.create_picker_button(image, func, self.node_picker, tooltip) # draw custom nodes for name in sorted(self.app.core.custom_nodes): custom_node = self.app.core.custom_nodes[name] image = custom_node.image func = partial(self.update_button, self.node_button, image, name) - self.create_button(image, func, self.node_picker, name) + self.create_picker_button(image, func, self.node_picker, name) # draw edit node - image = Images.get(ImageEnum.EDITNODE) - self.create_button( + image = icon(ImageEnum.EDITNODE) + self.create_picker_button( image, self.click_edit_node, self.node_picker, "custom nodes" ) - self.show_picker(self.node_button, self.node_picker) + self.design_select(self.node_button) + self.node_button.after( + 0, lambda: self.show_picker(self.node_button, self.node_picker) + ) def show_picker(self, button, picker): - first_button = self.winfo_children()[0] - x = button.winfo_rootx() - first_button.winfo_rootx() + 40 - y = button.winfo_rooty() - first_button.winfo_rooty() - 1 + x = self.winfo_width() + 1 + y = button.winfo_rooty() - picker.master.winfo_rooty() - 1 picker.place(x=x, y=y) self.app.bind_all("", lambda e: self.hide_pickers()) picker.wait_visibility() @@ -175,7 +166,7 @@ class Toolbar(tk.Frame): self.wait_window(picker) self.app.unbind_all("") - def create_button(self, image, func, frame, tooltip): + def create_picker_button(self, image, func, frame, tooltip): """ Create button and put it on the frame @@ -185,37 +176,25 @@ class Toolbar(tk.Frame): :param str tooltip: tooltip text :return: nothing """ - button = tk.Button(frame, width=self.width, height=self.height, image=image) + button = ttk.Button(frame, image=image) + button.image = image button.bind("", lambda e: func()) button.grid(pady=1) Tooltip(button, tooltip) - def create_radio_button(self, frame, image, func, variable, value, tooltip_msg): - button = tk.Radiobutton( - frame, - indicatoron=False, - width=self.width, - height=self.height, - image=image, - value=value, - variable=variable, - command=func, - ) - button.grid() - Tooltip(button, tooltip_msg) - - def create_regular_button(self, frame, image, func, tooltip): - button = tk.Button( - frame, width=self.width, height=self.height, image=image, command=func - ) - button.grid() + def create_button(self, frame, image, func, tooltip): + button = ttk.Button(frame, image=image, command=func) + button.image = image + button.grid(sticky="ew") Tooltip(button, tooltip) + return button - def click_selection_tool(self): + def click_selection(self): logging.debug("clicked selection tool") + self.design_select(self.select_button) self.app.canvas.mode = GraphMode.SELECT - def click_start_session_tool(self): + def click_start(self): """ Start session handler redraw buttons, send node and link messages to grpc server. @@ -227,8 +206,9 @@ class Toolbar(tk.Frame): self.app.core.start_session() self.runtime_frame.tkraise() - def click_link_tool(self): + def click_link(self): logging.debug("Click LINK button") + self.design_select(self.link_button) self.app.canvas.mode = GraphMode.EDGE def click_edit_node(self): @@ -240,6 +220,7 @@ class Toolbar(tk.Frame): logging.info("update button(%s): %s", button, name) self.hide_pickers() button.configure(image=image) + button.image = image self.app.canvas.mode = GraphMode.NODE self.app.canvas.draw_node_image = image self.app.canvas.draw_node_name = name @@ -262,29 +243,22 @@ class Toolbar(tk.Frame): :return: nothing """ - router_image = Images.get(ImageEnum.ROUTER) - self.node_button = tk.Radiobutton( - self.design_frame, - indicatoron=False, - variable=self.radio_value, - value=3, - width=self.width, - height=self.height, - image=router_image, + image = icon(ImageEnum.ROUTER) + self.node_button = ttk.Button( + self.design_frame, image=image, command=self.draw_node_picker ) - self.node_button.bind("", lambda e: self.draw_node_picker()) - self.node_button.grid() + self.node_button.image = image + self.node_button.grid(sticky="ew") Tooltip(self.node_button, "Network-layer virtual nodes") def draw_network_picker(self): """ - Draw the options for link-layer button + Draw the options for link-layer button. - :param tkinter.RadioButton link_layer_button: link-layer button :return: nothing """ self.hide_pickers() - self.network_picker = tk.Frame(self.master, padx=1, pady=1) + self.network_picker = ttk.Frame(self.master) nodes = [ (ImageEnum.HUB, "hub", "ethernet hub"), (ImageEnum.SWITCH, "switch", "ethernet switch"), @@ -294,14 +268,17 @@ class Toolbar(tk.Frame): (ImageEnum.TUNNEL, "tunnel", "tunnel tool"), ] for image_enum, name, tooltip in nodes: - image = Images.get(image_enum) - self.create_button( + image = icon(image_enum) + self.create_picker_button( image, partial(self.update_button, self.network_button, image, name), self.network_picker, tooltip, ) - self.show_picker(self.network_button, self.network_picker) + self.design_select(self.network_button) + self.network_button.after( + 0, lambda: self.show_picker(self.network_button, self.network_picker) + ) def create_network_button(self): """ @@ -309,31 +286,22 @@ class Toolbar(tk.Frame): :return: nothing """ - hub_image = Images.get(ImageEnum.HUB) - self.network_button = tk.Radiobutton( - self.design_frame, - indicatoron=False, - variable=self.radio_value, - value=4, - width=self.width, - height=self.height, - image=hub_image, + image = icon(ImageEnum.HUB) + self.network_button = ttk.Button( + self.design_frame, image=image, command=self.draw_network_picker ) - self.network_button.bind( - "", lambda e: self.draw_network_picker() - ) - self.network_button.grid() + self.network_button.image = image + self.network_button.grid(sticky="ew") Tooltip(self.network_button, "link-layer nodes") def draw_annotation_picker(self): """ - Draw the options for marker button + Draw the options for marker button. - :param tkinter.Radiobutton main_button: the main button :return: nothing """ self.hide_pickers() - self.annotation_picker = tk.Frame(self.master, padx=1, pady=1) + self.annotation_picker = ttk.Frame(self.master) nodes = [ (ImageEnum.MARKER, "marker"), (ImageEnum.OVAL, "oval"), @@ -341,13 +309,17 @@ class Toolbar(tk.Frame): (ImageEnum.TEXT, "text"), ] for image_enum, tooltip in nodes: - self.create_button( - Images.get(image_enum), - partial(self.update_annotation, image_enum), + image = icon(image_enum) + self.create_picker_button( + image, + partial(self.update_annotation, image), self.annotation_picker, tooltip, ) - self.show_picker(self.annotation_button, self.annotation_picker) + self.design_select(self.annotation_button) + self.annotation_button.after( + 0, lambda: self.show_picker(self.annotation_button, self.annotation_picker) + ) def create_annotation_button(self): """ @@ -355,53 +327,39 @@ class Toolbar(tk.Frame): :return: nothing """ - marker_image = Images.get(ImageEnum.MARKER) - self.annotation_button = tk.Radiobutton( - self.design_frame, - indicatoron=False, - variable=self.radio_value, - value=5, - width=self.width, - height=self.height, - image=marker_image, + image = icon(ImageEnum.MARKER) + self.annotation_button = ttk.Button( + self.design_frame, image=image, command=self.draw_annotation_picker ) - self.annotation_button.bind( - "", lambda e: self.draw_annotation_picker() - ) - self.annotation_button.grid() + self.annotation_button.image = image + self.annotation_button.grid(sticky="ew") Tooltip(self.annotation_button, "background annotation tools") def create_observe_button(self): - menu_button = tk.Menubutton( - self.runtime_frame, - image=Images.get(ImageEnum.OBSERVE), - width=self.width, - height=self.height, - direction=tk.RIGHT, - relief=tk.RAISED, + menu_button = ttk.Menubutton( + self.runtime_frame, image=icon(ImageEnum.OBSERVE), direction=tk.RIGHT ) - menu_button.menu = tk.Menu(menu_button, tearoff=0) - menu_button["menu"] = menu_button.menu - menu_button.grid() + menu_button.grid(sticky="ew") + menu = tk.Menu(menu_button, tearoff=0) + menu_button["menu"] = menu + menu.add_command(label="None") + menu.add_command(label="processes") + menu.add_command(label="ifconfig") + menu.add_command(label="IPv4 routes") + menu.add_command(label="IPv6 routes") + menu.add_command(label="OSPFv2 neighbors") + menu.add_command(label="OSPFv3 neighbors") + menu.add_command(label="Listening sockets") + menu.add_command(label="IPv4 MFC entries") + menu.add_command(label="IPv6 MFC entries") + menu.add_command(label="firewall rules") + menu.add_command(label="IPSec policies") + menu.add_command(label="docker logs") + menu.add_command(label="OSPFv3 MDR level") + menu.add_command(label="PIM neighbors") + menu.add_command(label="Edit...") - menu_button.menu.add_command(label="None") - menu_button.menu.add_command(label="processes") - menu_button.menu.add_command(label="ifconfig") - menu_button.menu.add_command(label="IPv4 routes") - menu_button.menu.add_command(label="IPv6 routes") - menu_button.menu.add_command(label="OSPFv2 neighbors") - menu_button.menu.add_command(label="OSPFv3 neighbors") - menu_button.menu.add_command(label="Listening sockets") - menu_button.menu.add_command(label="IPv4 MFC entries") - menu_button.menu.add_command(label="IPv6 MFC entries") - menu_button.menu.add_command(label="firewall rules") - menu_button.menu.add_command(label="IPSec policies") - menu_button.menu.add_command(label="docker logs") - menu_button.menu.add_command(label="OSPFv3 MDR level") - menu_button.menu.add_command(label="PIM neighbors") - menu_button.menu.add_command(label="Edit...") - - def click_stop_button(self): + def click_stop(self): """ redraw buttons on the toolbar, send node and link messages to grpc server @@ -411,10 +369,11 @@ class Toolbar(tk.Frame): self.app.core.stop_session() self.design_frame.tkraise() - def update_annotation(self, image_enum): + def update_annotation(self, image): logging.info("clicked annotation: ") self.hide_pickers() - self.annotation_button.configure(image=Images.get(image_enum)) + self.annotation_button.configure(image=image) + self.annotation_button.image = image def click_run_button(self): logging.debug("Click on RUN button") From 1693c5942c8818922b9a8ce9070853bb288bc351 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 12 Nov 2019 22:42:55 -0800 Subject: [PATCH 222/462] added ttk theme based on opensource dark theme with minor tweaks for button presses and menu coloring --- coretk/coretk/theme.py | 162 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 coretk/coretk/theme.py diff --git a/coretk/coretk/theme.py b/coretk/coretk/theme.py new file mode 100644 index 00000000..9525d35e --- /dev/null +++ b/coretk/coretk/theme.py @@ -0,0 +1,162 @@ +import tkinter as tk +from tkinter import ttk + + +class Colors: + disabledfg = "DarkGrey" + frame = "#424242" + dark = "#222222" + darker = "#121212" + darkest = "black" + lighter = "#626262" + lightest = "#ffffff" + selectbg = "#4a6984" + selectfg = "#ffffff" + white = "white" + black = "black" + + +style = ttk.Style() +style.theme_create( + "black", + "clam", + { + ".": { + "configure": { + "background": Colors.frame, + "foreground": Colors.white, + "bordercolor": Colors.darkest, + "darkcolor": Colors.dark, + "lightcolor": Colors.lighter, + "troughcolor": Colors.darker, + "selectbackground": Colors.selectbg, + "selectforeground": Colors.selectfg, + "selectborderwidth": 0, + "font": "TkDefaultFont", + }, + "map": { + "background": [("disabled", Colors.frame), ("active", Colors.lighter)], + "foreground": [("disabled", Colors.disabledfg)], + "selectbackground": [("!focus", Colors.darkest)], + "selectforeground": [("!focus", Colors.white)], + }, + }, + "TButton": { + "configure": {"width": 8, "padding": (5, 1), "relief": tk.RAISED}, + "map": { + "relief": [("pressed", tk.SUNKEN)], + "shiftrelief": [("pressed", 1)], + }, + }, + "TMenubutton": { + "configure": {"width": 11, "padding": (5, 1), "relief": tk.RAISED} + }, + "TCheckbutton": { + "configure": { + "indicatorbackground": Colors.white, + "indicatormargin": (1, 1, 4, 1), + } + }, + "TRadiobutton": { + "configure": { + "indicatorbackground": Colors.white, + "indicatormargin": (1, 1, 4, 1), + } + }, + "TEntry": { + "configure": { + "fieldbackground": Colors.white, + "foreground": Colors.black, + "padding": (2, 0), + } + }, + "TCombobox": { + "configure": { + "fieldbackground": Colors.white, + "foreground": Colors.black, + "padding": (2, 0), + } + }, + "TNotebook.Tab": { + "configure": {"padding": (6, 2, 6, 2)}, + "map": {"background": [("selected", Colors.lighter)]}, + }, + "Treeview": { + "configure": { + "fieldbackground": Colors.white, + "background": Colors.white, + "foreground": Colors.black, + }, + "map": { + "background": [("selected", Colors.selectbg)], + "foreground": [("selected", Colors.selectfg)], + }, + }, + }, +) +style.theme_use("black") + + +def update_menu(event): + bg = style.lookup(".", "background") + fg = style.lookup(".", "foreground") + abg = style.lookup(".", "lightcolor") + event.widget.config( + background=bg, foreground=fg, activebackground=abg, activeforeground=fg + ) + + +class Application(ttk.Frame): + def __init__(self, master=None): + super().__init__(master) + self.master.bind_class("Menu", "<>", update_menu) + self.master.geometry("800x600") + menu = tk.Menu(self.master) + menu.add_command(label="Command1") + menu.add_command(label="Command2") + submenu = tk.Menu(menu, tearoff=False) + submenu.add_command(label="Command1") + submenu.add_command(label="Command2") + menu.add_cascade(label="Submenu", menu=submenu) + self.master.config(menu=menu) + self.master.columnconfigure(0, weight=1) + self.master.rowconfigure(0, weight=1) + notebook = ttk.Notebook(self.master) + notebook.grid(sticky="nsew") + frame = ttk.Frame(notebook) + frame.grid(sticky="nsew") + ttk.Label(frame, text="Label").grid() + ttk.Entry(frame).grid() + ttk.Button(frame, text="Button").grid() + ttk.Combobox(frame, values=("one", "two", "three")).grid() + menubutton = ttk.Menubutton(frame, text="MenuButton") + menubutton.grid() + mbmenu = tk.Menu(menubutton, tearoff=False) + menubutton.config(menu=mbmenu) + mbmenu.add_command(label="Menu1") + mbmenu.add_command(label="Menu2") + submenu = tk.Menu(mbmenu, tearoff=False) + submenu.add_command(label="Command1") + submenu.add_command(label="Command2") + mbmenu.add_cascade(label="Submenu", menu=submenu) + ttk.Radiobutton(frame, text="Radio Button").grid() + ttk.Checkbutton(frame, text="Check Button").grid() + tv = ttk.Treeview(frame, columns=("one", "two", "three"), show="headings") + tv.grid() + tv.column("one", stretch=tk.YES) + tv.heading("one", text="ID") + tv.column("two", stretch=tk.YES) + tv.heading("two", text="State") + tv.column("three", stretch=tk.YES) + tv.heading("three", text="Node Count") + tv.insert("", tk.END, text="1", values=("v1", "v2", "v3")) + tv.insert("", tk.END, text="2", values=("v1", "v2", "v3")) + notebook.add(frame, text="Tab1") + frame = ttk.Frame(notebook) + frame.grid(sticky="nsew") + notebook.add(frame, text="Tab2") + + +if __name__ == "__main__": + app = Application() + app.mainloop() From b69b24d9fc8e912aa8b868128e39717bbd137c95 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 13 Nov 2019 09:09:53 -0800 Subject: [PATCH 223/462] continued work on node service configuration --- coretk/coretk/coreclient.py | 29 +++++ coretk/coretk/dialogs/serviceconfiguration.py | 109 +++++++++++++++--- coretk/coretk/servicenodeconfig.py | 12 ++ 3 files changed, 134 insertions(+), 16 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index d924e305..13b1c1cf 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -416,6 +416,35 @@ class CoreClient: logging.debug("open xml: %s", response) self.join_session(response.session_id) + def get_node_service(self, node_id, service_name): + response = self.client.get_node_service(self.session_id, node_id, service_name) + logging.debug("get node service %s", response) + return response.service + + def set_node_service(self, node_id, service_name, startups, validations, shutdowns): + response = self.client.set_node_service( + self.session_id, node_id, service_name, startups, validations, shutdowns + ) + logging.debug("set node service %s", response) + + def create_nodes_and_links(self): + node_protos = self.get_nodes_proto() + link_protos = self.get_links_proto() + self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION) + for node_proto in node_protos: + response = self.client.add_node(self.session_id, node_proto) + logging.debug("create node: %s", response) + for link_proto in link_protos: + response = self.client.add_link( + self.session_id, + link_proto.node_one_id, + link_proto.node_two_id, + link_proto.interface_one, + link_proto.interface_two, + link_proto.options, + ) + logging.debug("create link: %s", response) + def close(self): """ Clean ups when done using grpc diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 016373c8..7cd8e501 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -3,6 +3,7 @@ import tkinter as tk from tkinter import ttk +from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog from coretk.images import ImageEnum, Images from coretk.widgets import ListboxScroll @@ -12,26 +13,51 @@ class ServiceConfiguration(Dialog): def __init__(self, master, app, service_name, canvas_node): super().__init__(master, app, service_name + " service", modal=True) self.app = app + self.canvas_node = canvas_node self.service_name = service_name - self.metadata = tk.StringVar() - self.filename = tk.StringVar() self.radiovar = tk.IntVar() self.radiovar.set(2) - self.startup_index = tk.IntVar() - self.start_time = tk.IntVar() self.documentnew_img = Images.get(ImageEnum.DOCUMENTNEW) self.editdelete_img = Images.get(ImageEnum.EDITDELETE) - self.tab_parent = None - self.filenames = ["test1", "test2", "test3"] + self.metadata = "" + self.filenames = [] + self.dependencies = [] + self.executables = [] + self.startup_commands = [] + self.validation_commands = [] + self.shutdown_commands = [] + self.validation_mode = None + self.validation_time = None + self.validation_period = None + self.tab_parent = None self.metadata_entry = None self.filename_combobox = None self.startup_commands_listbox = None self.shutdown_commands_listbox = None self.validate_commands_listbox = None - + self.validation_time_entry = None + self.validation_mode_entry = None + self.load() self.draw() + def load(self): + # create nodes and links in definition state for getting and setting service file + self.app.core.create_nodes_and_links() + # load data from local memory + service_config = self.app.core.serviceconfig_manager.configurations[ + self.canvas_node.core_id + ][self.service_name] + self.dependencies = [x for x in service_config.dependencies] + self.executables = [x for x in service_config.executables] + self.metadata = service_config.meta + self.filenames = [x for x in service_config.configs] + self.startup_commands = [x for x in service_config.startup] + self.validation_commands = [x for x in service_config.validate] + self.shutdown_commands = [x for x in service_config.shutdown] + self.validation_mode = service_config.validation_mode + self.validation_time = service_config.validation_timer + def draw(self): # self.columnconfigure(1, weight=1) frame = tk.Frame(self) @@ -44,7 +70,9 @@ class ServiceConfiguration(Dialog): # frame2.columnconfigure(1, weight=4) label = tk.Label(frame2, text="Meta-data") label.grid(row=0, column=0) - self.metadata_entry = tk.Entry(frame2, textvariable=self.metadata) + self.metadata_entry = tk.Entry( + frame2, textvariable=tk.StringVar(value=self.metadata) + ) self.metadata_entry.grid(row=0, column=1) frame2.grid(row=1, column=0) frame.grid(row=0, column=0) @@ -78,7 +106,12 @@ class ServiceConfiguration(Dialog): label.grid(row=0, column=0) self.filename_combobox = ttk.Combobox(frame, values=self.filenames) self.filename_combobox.grid(row=0, column=1) - self.filename_combobox.current(0) + if len(self.filenames) > 0: + self.filename_combobox.current(0) + self.filename_combobox.bind( + "<>", self.display_service_file_data + ) + button = tk.Button(frame, image=self.documentnew_img) button.bind("", self.add_filename) button.grid(row=0, column=2) @@ -130,10 +163,13 @@ class ServiceConfiguration(Dialog): label_frame = None if i == 0: label_frame = tk.LabelFrame(tab3, text="Startup commands") + commands = self.startup_commands elif i == 1: label_frame = tk.LabelFrame(tab3, text="Shutdown commands") + commands = self.shutdown_commands elif i == 2: label_frame = tk.LabelFrame(tab3, text="Validation commands") + commands = self.validation_commands label_frame.columnconfigure(0, weight=1) frame = tk.Frame(label_frame) frame.columnconfigure(0, weight=1) @@ -148,6 +184,8 @@ class ServiceConfiguration(Dialog): frame.grid(row=0, column=0, sticky="nsew") listbox_scroll = ListboxScroll(label_frame) listbox_scroll.listbox.bind("<>", self.update_entry) + for command in commands: + listbox_scroll.listbox.insert("end", command) listbox_scroll.listbox.config(height=4) listbox_scroll.grid(row=1, column=0, sticky="nsew") if i == 0: @@ -160,33 +198,60 @@ class ServiceConfiguration(Dialog): # tab 4 for i in range(2): + label_frame = None if i == 0: label_frame = tk.LabelFrame(tab4, text="Executables") elif i == 1: label_frame = tk.LabelFrame(tab4, text="Dependencies") - label_frame.columnconfigure(0, weight=1) listbox_scroll = ListboxScroll(label_frame) - listbox_scroll.listbox.config(height=4, state="disabled") + listbox_scroll.listbox.config(height=4) listbox_scroll.grid(row=0, column=0, sticky="nsew") label_frame.grid(row=i, column=0, sticky="nsew") + if i == 0: + for executable in self.executables: + print(executable) + listbox_scroll.listbox.insert("end", executable) + if i == 1: + for dependency in self.dependencies: + listbox_scroll.listbox.insert("end", dependency) for i in range(3): frame = tk.Frame(tab4) frame.columnconfigure(0, weight=1) if i == 0: label = tk.Label(frame, text="Validation time:") + self.validation_time_entry = tk.Entry( + frame, + state="disabled", + textvariable=tk.StringVar(value=self.validation_time), + ) + self.validation_time_entry.grid(row=i, column=1) elif i == 1: label = tk.Label(frame, text="Validation mode:") + if self.validation_mode == core_pb2.ServiceValidationMode.BLOCKING: + mode = "BLOCKING" + elif ( + self.validation_mode == core_pb2.ServiceValidationMode.NON_BLOCKING + ): + mode = "NON_BLOCKING" + elif self.validation_mode == core_pb2.ServiceValidationMode.TIMER: + mode = "TIMER" + self.validation_mode_entry = tk.Entry( + frame, state="disabled", textvariable=tk.StringVar(value=mode) + ) + self.validation_mode_entry.grid(row=i, column=1) elif i == 2: label = tk.Label(frame, text="Validation period:") + self.validation_period_entry = tk.Entry( + frame, state="disabled", textvariable=tk.StringVar() + ) + self.validation_period_entry.grid(row=i, column=1) label.grid(row=i, column=0) - entry = tk.Entry(frame, state="disabled", textvariable=tk.StringVar()) - entry.grid(row=i, column=1) frame.grid(row=2 + i, column=0, sticky="nsew") button = tk.Button( - self, text="onle store values that have changed from their defaults" + self, text="only store values that have changed from their defaults" ) button.grid(row=2, column=0) @@ -258,9 +323,21 @@ class ServiceConfiguration(Dialog): 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") - print( - metadata, filenames, startup_commands, shutdown_commands, validate_commands + self.app.core.serviceconfig_manager.node_service_custom_configuration( + self.canvas_node.core_id, + self.service_name, + startup_commands, + validate_commands, + shutdown_commands, ) + # wipe nodes and links when finished by setting to DEFINITION state + self.app.core.client.set_session_state( + self.app.core.session_id, core_pb2.SessionState.DEFINITION + ) + print(metadata, filenames) + + def display_service_file_data(self, event): + print("not implemented") def click_defaults(self): print("not implemented") diff --git a/coretk/coretk/servicenodeconfig.py b/coretk/coretk/servicenodeconfig.py index 486e646d..f9c2ee88 100644 --- a/coretk/coretk/servicenodeconfig.py +++ b/coretk/coretk/servicenodeconfig.py @@ -35,3 +35,15 @@ class ServiceNodeConfig: response, ) self.configurations[node_id][default] = response.service + + def node_custom_service_configuration(self, node_id, service_name): + return + + def node_service_custom_configuration( + self, node_id, service_name, startups, validates, shutdowns + ): + self.app.core.set_node_service( + node_id, service_name, startups, validates, shutdowns + ) + config = self.app.core.get_node_service(node_id, service_name) + self.configurations[node_id][service_name] = config From ca798215e4ea66dcf1fdc0adc8072d18a4f351a3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 13 Nov 2019 10:45:43 -0800 Subject: [PATCH 224/462] updates to leverage theming, initially using a dark theme, updated all dialogs to be contained within a frame to help provide consistent theming --- coretk/coretk/app.py | 15 +- coretk/coretk/canvastooltip.py | 18 +- coretk/coretk/dialogs/canvasbackground.py | 16 +- coretk/coretk/dialogs/canvassizeandscale.py | 14 +- coretk/coretk/dialogs/customnodes.py | 20 +-- coretk/coretk/dialogs/dialog.py | 7 +- coretk/coretk/dialogs/emaneconfig.py | 38 ++-- coretk/coretk/dialogs/hooks.py | 20 ++- coretk/coretk/dialogs/icondialog.py | 8 +- coretk/coretk/dialogs/mobilityconfig.py | 24 ++- coretk/coretk/dialogs/nodeconfig.py | 8 +- coretk/coretk/dialogs/nodeservice.py | 8 +- coretk/coretk/dialogs/observers.py | 12 +- coretk/coretk/dialogs/preferences.py | 6 +- coretk/coretk/dialogs/servers.py | 14 +- coretk/coretk/dialogs/serviceconfiguration.py | 8 +- coretk/coretk/dialogs/sessionoptions.py | 8 +- coretk/coretk/dialogs/sessions.py | 14 +- coretk/coretk/dialogs/wlanconfig.py | 23 ++- coretk/coretk/theme.py | 162 ------------------ coretk/coretk/themes.py | 127 ++++++++++++++ coretk/coretk/tooltip.py | 17 +- coretk/coretk/widgets.py | 20 +-- 23 files changed, 298 insertions(+), 309 deletions(-) delete mode 100644 coretk/coretk/theme.py create mode 100644 coretk/coretk/themes.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 452d5397..bc3ac9dd 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,8 +1,9 @@ import logging import tkinter as tk +from functools import partial from tkinter import ttk -from coretk import appconfig +from coretk import appconfig, themes from coretk.coreclient import CoreClient from coretk.graph import CanvasGraph from coretk.images import ImageEnum, Images @@ -14,7 +15,8 @@ from coretk.toolbar import Toolbar class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) - Images.load_all() + self.style = ttk.Style() + self.setup_theme() self.menubar = None self.toolbar = None self.canvas = None @@ -33,6 +35,14 @@ class Application(tk.Frame): self.draw() self.core.set_up() + def setup_theme(self): + themes.load(self.style) + self.style.theme_use(themes.DARK) + func = partial(themes.update_menu, self.style) + self.master.bind_class("Menu", "<>", func) + func = partial(themes.update_toplevel, self.style) + self.master.bind_class("Toplevel", "<>", func) + def setup_app(self): self.master.title("CORE") self.master.geometry("1000x800") @@ -78,6 +88,7 @@ class Application(tk.Frame): if __name__ == "__main__": log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=log_format) + Images.load_all() appconfig.check_directory() app = Application() app.mainloop() diff --git a/coretk/coretk/canvastooltip.py b/coretk/coretk/canvastooltip.py index 8ace41d0..8ae528d8 100644 --- a/coretk/coretk/canvastooltip.py +++ b/coretk/coretk/canvastooltip.py @@ -1,6 +1,8 @@ import tkinter as tk from tkinter import ttk +from coretk.themes import Styles + class CanvasTooltip: """ @@ -17,16 +19,13 @@ class CanvasTooltip: Alberto Vassena on 2016.12.10. """ - def __init__( - self, canvas, *, bg="#FFFFEA", pad=(5, 3, 5, 3), waittime=400, wraplength=600 - ): + def __init__(self, canvas, *, pad=(5, 3, 5, 3), waittime=400, wraplength=600): # in miliseconds, originally 500 self.waittime = waittime # in pixels, originally 180 self.wraplength = wraplength self.canvas = canvas self.text = tk.StringVar() - self.bg = bg self.pad = pad self.id = None self.tw = None @@ -78,7 +77,6 @@ class CanvasTooltip: y1 = 0 return x1, y1 - bg = self.bg pad = self.pad canvas = self.canvas @@ -87,20 +85,16 @@ class CanvasTooltip: # Leaves only the label and removes the app window self.tw.wm_overrideredirect(True) - - win = tk.Frame(self.tw, background=bg, borderwidth=0) + win = ttk.Frame(self.tw, style=Styles.tooltip_frame, padding=3) win.grid() label = ttk.Label( win, textvariable=self.text, - justify=tk.LEFT, - background=bg, - relief=tk.SOLID, - borderwidth=0, wraplength=self.wraplength, + style=Styles.tooltip, ) label.grid(padx=(pad[0], pad[2]), pady=(pad[1], pad[3]), sticky=tk.NSEW) - x, y = tip_pos_calculator(canvas, label) + x, y = tip_pos_calculator(canvas, label, pad=pad) self.tw.wm_geometry("+%d+%d" % (x, y)) def hide(self): diff --git a/coretk/coretk/dialogs/canvasbackground.py b/coretk/coretk/dialogs/canvasbackground.py index 8b1c4cf4..7b1dd186 100644 --- a/coretk/coretk/dialogs/canvasbackground.py +++ b/coretk/coretk/dialogs/canvasbackground.py @@ -40,7 +40,7 @@ class CanvasBackgroundDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) self.draw_image() self.draw_image_label() self.draw_image_selection() @@ -50,16 +50,16 @@ class CanvasBackgroundDialog(Dialog): def draw_image(self): self.image_label = ttk.Label( - self, text="(image preview)", width=32, anchor=tk.CENTER + self.top, text="(image preview)", width=32, anchor=tk.CENTER ) self.image_label.grid(row=0, column=0, pady=5) def draw_image_label(self): - label = ttk.Label(self, text="Image filename: ") + label = ttk.Label(self.top, text="Image filename: ") label.grid(row=1, column=0, sticky="ew") def draw_image_selection(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=2) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) @@ -76,7 +76,7 @@ class CanvasBackgroundDialog(Dialog): button.grid(row=0, column=2, sticky="ew") def draw_options(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) @@ -105,12 +105,12 @@ class CanvasBackgroundDialog(Dialog): def draw_additional_options(self): checkbutton = ttk.Checkbutton( - self, text="Show grid", variable=self.show_grid_var + self.top, text="Show grid", variable=self.show_grid_var ) checkbutton.grid(row=4, column=0, sticky="ew", padx=PADX) checkbutton = ttk.Checkbutton( - self, + self.top, text="Adjust canvas size to image dimensions", variable=self.adjust_to_dim_var, command=self.click_adjust_canvas, @@ -121,7 +121,7 @@ class CanvasBackgroundDialog(Dialog): self.adjust_to_dim_var.set(0) def draw_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(row=6, column=0, pady=5, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 1af71c93..09132aef 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -44,7 +44,7 @@ class SizeAndScaleDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) self.draw_size() self.draw_scale() self.draw_reference_point() @@ -52,7 +52,7 @@ class SizeAndScaleDialog(Dialog): self.draw_buttons() def draw_size(self): - label_frame = ttk.Labelframe(self, text="Size", padding=FRAME_BAD) + label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_BAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -89,7 +89,7 @@ class SizeAndScaleDialog(Dialog): label.grid(row=0, column=4, sticky="w") def draw_scale(self): - label_frame = ttk.Labelframe(self, text="Scale", padding=FRAME_BAD) + label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_BAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -104,7 +104,9 @@ class SizeAndScaleDialog(Dialog): label.grid(row=0, column=2, sticky="w") def draw_reference_point(self): - label_frame = ttk.Labelframe(self, text="Reference Point", padding=FRAME_BAD) + label_frame = ttk.Labelframe( + self.top, text="Reference Point", padding=FRAME_BAD + ) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -156,12 +158,12 @@ class SizeAndScaleDialog(Dialog): def draw_save_as_default(self): button = ttk.Checkbutton( - self, text="Save as default?", variable=self.save_default + self.top, text="Save as default?", variable=self.save_default ) button.grid(sticky="w", pady=3) def draw_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.grid(sticky="ew") diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index f5c1ab78..154b7868 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -19,10 +19,10 @@ class ServicesSelectDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) - self.rowconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(stick="nsew") frame.rowconfigure(0, weight=1) for i in range(3): @@ -44,7 +44,7 @@ class ServicesSelectDialog(Dialog): for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(stick="ew") for i in range(2): frame.columnconfigure(i, weight=1) @@ -96,19 +96,19 @@ class CustomNodesDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) - self.rowconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) self.draw_node_config() self.draw_node_buttons() self.draw_buttons() def draw_node_config(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(sticky="nsew") frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - self.nodes_list = ListboxScroll(frame) + self.nodes_list = ListboxScroll(frame, text="Nodes") self.nodes_list.grid(row=0, column=0, sticky="nsew") self.nodes_list.listbox.bind("<>", self.handle_node_select) for name in sorted(self.app.core.custom_nodes): @@ -125,7 +125,7 @@ class CustomNodesDialog(Dialog): button.grid(sticky="ew") def draw_node_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(pady=2, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) @@ -144,7 +144,7 @@ class CustomNodesDialog(Dialog): self.delete_button.grid(row=0, column=2, sticky="ew") def draw_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index c043a47a..769fd176 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -1,4 +1,5 @@ import tkinter as tk +from tkinter import ttk from coretk.images import ImageEnum, Images @@ -7,7 +8,7 @@ DIALOG_PAD = 5 class Dialog(tk.Toplevel): def __init__(self, master, app, title, modal=False): - super().__init__(master, padx=DIALOG_PAD, pady=DIALOG_PAD) + super().__init__(master) self.withdraw() self.app = app self.modal = modal @@ -15,6 +16,10 @@ class Dialog(tk.Toplevel): self.protocol("WM_DELETE_WINDOW", self.destroy) image = Images.get(ImageEnum.CORE, 16) self.tk.call("wm", "iconphoto", self._w, image) + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.top = ttk.Frame(self, padding=DIALOG_PAD) + self.top.grid(sticky="nsew") def show(self): self.transient(self.master) diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index 8673d3fc..181e1264 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -55,10 +55,10 @@ class EmaneConfiguration(Dialog): return var def choose_core(self): - print("not implemented") + logging.info("not implemented") def node_name_and_image(self): - f = ttk.Frame(self) + f = ttk.Frame(self.top) lbl = ttk.Label(f, text="Node name:") lbl.grid(row=0, column=0, padx=2, pady=2) @@ -90,13 +90,15 @@ class EmaneConfiguration(Dialog): logging.info("emane config: %s", response) self.options = response.config - self.emane_dialog.columnconfigure(0, weight=1) - self.emane_dialog.rowconfigure(0, weight=1) - self.emane_config_frame = ConfigFrame(self.emane_dialog, config=self.options) + self.emane_dialog.top.columnconfigure(0, weight=1) + self.emane_dialog.top.rowconfigure(0, weight=1) + self.emane_config_frame = ConfigFrame( + self.emane_dialog.top, config=self.options + ) self.emane_config_frame.draw_config() self.emane_config_frame.grid(sticky="nsew") - frame = ttk.Frame(self.emane_dialog) + frame = ttk.Frame(self.emane_dialog.top) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) @@ -152,8 +154,8 @@ class EmaneConfiguration(Dialog): self.emane_model_dialog = Dialog( self, self.app, f"{model_name} configuration", modal=False ) - self.emane_model_dialog.columnconfigure(0, weight=1) - self.emane_model_dialog.rowconfigure(0, weight=1) + self.emane_model_dialog.top.columnconfigure(0, weight=1) + self.emane_model_dialog.top.rowconfigure(0, weight=1) # query for configurations session_id = self.app.core.session_id @@ -165,12 +167,12 @@ class EmaneConfiguration(Dialog): self.model_options = response.config self.model_config_frame = ConfigFrame( - self.emane_model_dialog, config=self.model_options + self.emane_model_dialog.top, config=self.model_options ) self.model_config_frame.grid(sticky="nsew") self.model_config_frame.draw_config() - frame = ttk.Frame(self.emane_model_dialog) + frame = ttk.Frame(self.emane_model_dialog.top) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) @@ -268,22 +270,24 @@ class EmaneConfiguration(Dialog): :return: nothing """ # draw label - lbl = ttk.Label(self, text="Emane") + lbl = ttk.Label(self.top, text="Emane") lbl.grid(row=1, column=0) # main frame that has emane wiki, a short description, emane models and the configure buttons - f = ttk.Frame(self) + f = ttk.Frame(self.top) f.columnconfigure(0, weight=1) + image = Images.get(ImageEnum.EDITNODE, 16) b = ttk.Button( f, - image=Images.get(ImageEnum.EDITNODE, 8), + image=image, text="EMANE Wiki", compound=tk.RIGHT, command=lambda: webbrowser.open_new( "https://github.com/adjacentlink/emane/wiki" ), ) + b.image = image b.grid(row=0, column=0, sticky="w") lbl = ttk.Label( @@ -302,8 +306,8 @@ class EmaneConfiguration(Dialog): f.grid(row=2, column=0, sticky="nsew") def draw_ip_subnets(self): - self.draw_text_label_and_entry(self, "IPv4 subnet", "") - self.draw_text_label_and_entry(self, "IPv6 subnet", "") + self.draw_text_label_and_entry(self.top, "IPv4 subnet", "") + self.draw_text_label_and_entry(self.top, "IPv6 subnet", "") def emane_options(self): """ @@ -311,7 +315,7 @@ class EmaneConfiguration(Dialog): :return: """ - f = ttk.Frame(self) + f = ttk.Frame(self.top) f.columnconfigure(0, weight=1) f.columnconfigure(1, weight=1) b = ttk.Button(f, text="Link to all routers") @@ -326,7 +330,7 @@ class EmaneConfiguration(Dialog): self.destroy() def draw_apply_and_cancel(self): - f = ttk.Frame(self) + f = ttk.Frame(self.top) f.columnconfigure(0, weight=1) f.columnconfigure(1, weight=1) b = ttk.Button(f, text="Apply", command=self.apply) diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index 5ae9da8a..dc677e12 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -15,11 +15,11 @@ class HookDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) - self.rowconfigure(1, weight=1) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(1, weight=1) # name and states - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(row=0, sticky="ew", pady=2) frame.columnconfigure(0, weight=2) frame.columnconfigure(1, weight=7) @@ -39,7 +39,7 @@ class HookDialog(Dialog): combobox.bind("<>", self.state_change) # data - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) frame.grid(row=1, sticky="nsew", pady=2) @@ -59,7 +59,7 @@ class HookDialog(Dialog): scrollbar.config(command=self.data.yview) # button row - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(row=2, sticky="ew", pady=2) for i in range(2): frame.columnconfigure(i, weight=1) @@ -99,14 +99,16 @@ class HooksDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) - self.rowconfigure(0, weight=1) - self.listbox = tk.Listbox(self) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + + self.listbox = tk.Listbox(self.top) self.listbox.grid(row=0, sticky="nsew") self.listbox.bind("<>", self.select) for hook_file in self.app.core.hooks: self.listbox.insert(tk.END, hook_file) - frame = ttk.Frame(self) + + frame = ttk.Frame(self.top) frame.grid(row=1, sticky="ew") for i in range(4): frame.columnconfigure(i, weight=1) diff --git a/coretk/coretk/dialogs/icondialog.py b/coretk/coretk/dialogs/icondialog.py index 15d8000b..c5518f28 100644 --- a/coretk/coretk/dialogs/icondialog.py +++ b/coretk/coretk/dialogs/icondialog.py @@ -15,10 +15,10 @@ class IconDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) # row one - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(row=0, column=0, pady=2, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=3) @@ -30,11 +30,11 @@ class IconDialog(Dialog): button.grid(row=0, column=2) # row two - self.image_label = ttk.Label(self, image=self.image, anchor=tk.CENTER) + self.image_label = ttk.Label(self.top, image=self.image, anchor=tk.CENTER) self.image_label.grid(row=1, column=0, pady=2, sticky="ew") # row three - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(row=2, column=0, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/coretk/coretk/dialogs/mobilityconfig.py index 68c557ee..1f53b4a0 100644 --- a/coretk/coretk/dialogs/mobilityconfig.py +++ b/coretk/coretk/dialogs/mobilityconfig.py @@ -1,7 +1,7 @@ """ mobility configuration """ - +import logging import tkinter as tk from tkinter import filedialog, ttk @@ -9,7 +9,7 @@ from coretk import appconfig from coretk.dialogs.dialog import Dialog -class MobilityConfiguration(Dialog): +class MobilityConfigDialog(Dialog): def __init__(self, master, app, canvas_node): """ create an instance of mobility configuration @@ -19,7 +19,7 @@ class MobilityConfiguration(Dialog): """ super().__init__(master, app, "ns2script configuration", modal=True) self.canvas_node = canvas_node - print(app.canvas.core.mobilityconfig_management.configurations) + logging.info(app.canvas.core.mobilityconfig_management.configurations) self.node_config = app.canvas.core.mobilityconfig_management.configurations[ canvas_node.core_id ] @@ -57,11 +57,11 @@ class MobilityConfiguration(Dialog): def create_label_entry_filebrowser( self, parent_frame, text_label, entry_text, filebrowser=False ): - f = ttk.Frame(parent_frame, bg="#d9d9d9") - lbl = ttk.Label(f, text=text_label, bg="#d9d9d9") + f = ttk.Frame(parent_frame) + lbl = ttk.Label(f, text=text_label) lbl.grid(padx=3, pady=3) # f.grid() - e = ttk.Entry(f, textvariable=self.create_string_var(entry_text), bg="#ffffff") + e = ttk.Entry(f, textvariable=self.create_string_var(entry_text)) e.grid(row=0, column=1, padx=3, pady=3) if filebrowser: b = ttk.Button(f, text="...", command=lambda: self.open_file(e)) @@ -69,16 +69,14 @@ class MobilityConfiguration(Dialog): f.grid(sticky=tk.E) def mobility_script_parameters(self): - lbl = ttk.Label(self, text="node ns2script") + lbl = ttk.Label(self.top, text="node ns2script") lbl.grid(sticky="ew") - sb = ttk.Scrollbar(self, orient=tk.VERTICAL) + sb = ttk.Scrollbar(self.top, orient=tk.VERTICAL) sb.grid(row=1, column=1, sticky="ns") - f = ttk.Frame(self, bg="#d9d9d9") - lbl = ttk.Label( - f, text="ns-2 Mobility Scripts Parameters", bg="#d9d9d9", relief=tk.RAISED - ) + f = ttk.Frame(self.top) + lbl = ttk.Label(f, text="ns-2 Mobility Scripts Parameters") lbl.grid(row=0, column=0, sticky=tk.W) f1 = tk.Canvas( @@ -229,7 +227,7 @@ class MobilityConfiguration(Dialog): :return: nothing """ - f = ttk.Frame(self) + f = ttk.Frame(self.top) b = ttk.Button(f, text="Apply", command=self.ns2script_apply) b.grid() b = ttk.Button(f, text="Cancel", command=self.destroy) diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index ce7261e2..48233762 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -26,13 +26,13 @@ class NodeConfigDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) self.draw_first_row() self.draw_second_row() self.draw_third_row() def draw_first_row(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(row=0, column=0, pady=2, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) @@ -55,7 +55,7 @@ class NodeConfigDialog(Dialog): combobox.grid(row=0, column=2, sticky="ew") def draw_second_row(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(row=1, column=0, pady=2, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) @@ -73,7 +73,7 @@ class NodeConfigDialog(Dialog): self.image_button.grid(row=0, column=1, sticky="ew") def draw_third_row(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(row=2, column=0, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 49cc56d8..ce5a4715 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -22,10 +22,10 @@ class NodeService(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) - self.rowconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(stick="nsew") frame.rowconfigure(0, weight=1) for i in range(3): @@ -47,7 +47,7 @@ class NodeService(Dialog): for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(stick="ew") for i in range(3): frame.columnconfigure(i, weight=1) diff --git a/coretk/coretk/dialogs/observers.py b/coretk/coretk/dialogs/observers.py index 6c57f08e..78c5811f 100644 --- a/coretk/coretk/dialogs/observers.py +++ b/coretk/coretk/dialogs/observers.py @@ -18,15 +18,15 @@ class ObserverDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) - self.rowconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) self.draw_listbox() self.draw_form_fields() self.draw_config_buttons() self.draw_apply_buttons() def draw_listbox(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(sticky="nsew") frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) @@ -45,7 +45,7 @@ class ObserverDialog(Dialog): scrollbar.config(command=self.observers.yview) def draw_form_fields(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) @@ -60,7 +60,7 @@ class ObserverDialog(Dialog): entry.grid(row=1, column=1, sticky="ew") def draw_config_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(pady=2, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) @@ -79,7 +79,7 @@ class ObserverDialog(Dialog): self.delete_button.grid(row=0, column=2, sticky="ew") def draw_apply_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py index 74293a30..0c426d3c 100644 --- a/coretk/coretk/dialogs/preferences.py +++ b/coretk/coretk/dialogs/preferences.py @@ -15,12 +15,12 @@ class PreferencesDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) self.draw_programs() self.draw_buttons() def draw_programs(self): - frame = ttk.LabelFrame(self, text="Programs") + frame = ttk.LabelFrame(self.top, text="Programs") frame.grid(sticky="ew", pady=2) frame.columnconfigure(1, weight=1) @@ -47,7 +47,7 @@ class PreferencesDialog(Dialog): entry.grid(row=2, column=1, sticky="ew") def draw_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index 25f3e826..d3db22e7 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -23,15 +23,15 @@ class ServersDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) - self.rowconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) self.draw_servers() self.draw_server_configuration() self.draw_servers_buttons() self.draw_apply_buttons() def draw_servers(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(pady=2, sticky="nsew") frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) @@ -51,10 +51,10 @@ class ServersDialog(Dialog): scrollbar.config(command=self.servers.yview) def draw_server_configuration(self): - label = ttk.Label(self, text="Server Configuration") + label = ttk.Label(self.top, text="Server Configuration") label.grid(pady=2, sticky="ew") - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(pady=2, sticky="ew") frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) @@ -76,7 +76,7 @@ class ServersDialog(Dialog): entry.grid(row=0, column=5, sticky="ew") def draw_servers_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(pady=2, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) @@ -95,7 +95,7 @@ class ServersDialog(Dialog): self.delete_button.grid(row=0, column=2, sticky="ew") def draw_apply_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index b0b35db9..29f844b5 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -34,7 +34,7 @@ class ServiceConfiguration(Dialog): def draw(self): # self.columnconfigure(1, weight=1) - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame1 = ttk.Frame(frame) label = ttk.Label(frame1, text=self.service_name) label.grid(row=0, column=0, sticky="ew") @@ -49,7 +49,7 @@ class ServiceConfiguration(Dialog): frame2.grid(row=1, column=0) frame.grid(row=0, column=0) - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) self.tab_parent = ttk.Notebook(frame) tab1 = ttk.Frame(self.tab_parent) tab2 = ttk.Frame(self.tab_parent) @@ -190,11 +190,11 @@ class ServiceConfiguration(Dialog): frame.grid(row=2 + i, column=0, sticky="nsew") button = ttk.Button( - self, text="onle store values that have changed from their defaults" + self.top, text="onle store values that have changed from their defaults" ) button.grid(row=2, column=0) - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) button = ttk.Button(frame, text="Apply", command=self.click_apply) button.grid(row=0, column=0, sticky="nsew") button = ttk.Button( diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index 45f26a58..8cd7ad68 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -15,18 +15,18 @@ class SessionOptionsDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) - self.rowconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) session_id = self.app.core.session_id response = self.app.core.client.get_session_options(session_id) logging.info("session options: %s", response) - self.config_frame = ConfigFrame(self, config=response.config) + self.config_frame = ConfigFrame(self.top, config=response.config) self.config_frame.draw_config() self.config_frame.grid(sticky="nsew") - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index b9b7d77b..6ea66973 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -16,7 +16,7 @@ class SessionsDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) self.draw_description() self.draw_tree() self.draw_buttons() @@ -27,7 +27,7 @@ class SessionsDialog(Dialog): :return: nothing """ label = ttk.Label( - self, + self.top, text="Below is a list of active CORE sessions. Double-click to \n" "connect to an existing session. Usually, only sessions in \n" "the RUNTIME state persist in the daemon, except for the \n" @@ -37,7 +37,7 @@ class SessionsDialog(Dialog): def draw_tree(self): self.tree = ttk.Treeview( - self, columns=("id", "state", "nodes"), show="headings" + self.top, columns=("id", "state", "nodes"), show="headings" ) self.tree.grid(row=1, sticky="nsew") self.tree.column("id", stretch=tk.YES) @@ -60,16 +60,18 @@ class SessionsDialog(Dialog): self.tree.bind("", self.on_selected) self.tree.bind("<>", self.click_select) - yscrollbar = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview) + yscrollbar = ttk.Scrollbar(self.top, orient="vertical", command=self.tree.yview) yscrollbar.grid(row=1, column=1, sticky="ns") self.tree.configure(yscrollcommand=yscrollbar.set) - xscrollbar = ttk.Scrollbar(self, orient="horizontal", command=self.tree.xview) + xscrollbar = ttk.Scrollbar( + self.top, orient="horizontal", command=self.tree.xview + ) xscrollbar.grid(row=2, sticky="ew", pady=5) self.tree.configure(xscrollcommand=xscrollbar.set) def draw_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) for i in range(4): frame.columnconfigure(i, weight=1) frame.grid(row=3, sticky="ew") diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index d33f7ca0..85a2a18f 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -7,6 +7,7 @@ from tkinter import ttk from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog +from coretk.dialogs.mobilityconfig import MobilityConfigDialog class WlanConfigDialog(Dialog): @@ -30,7 +31,7 @@ class WlanConfigDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) self.draw_name_config() self.draw_wlan_config() self.draw_subnet() @@ -43,7 +44,7 @@ class WlanConfigDialog(Dialog): :return: nothing """ - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(pady=2, sticky="ew") frame.columnconfigure(0, weight=1) @@ -59,10 +60,10 @@ class WlanConfigDialog(Dialog): :return: nothing """ - label = ttk.Label(self, text="Wireless") + label = ttk.Label(self.top, text="Wireless") label.grid(sticky="w", pady=2) - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(pady=2, sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) @@ -108,7 +109,7 @@ class WlanConfigDialog(Dialog): :return: nothing """ - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(pady=3, sticky="ew") frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) @@ -130,12 +131,14 @@ class WlanConfigDialog(Dialog): :return: """ - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(pady=2, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) - button = ttk.Button(frame, text="ns-2 mobility script...") + button = ttk.Button( + frame, text="ns-2 mobility script...", command=self.click_mobility + ) button.grid(row=0, column=0, padx=2, sticky="ew") button = ttk.Button(frame, text="Link to all routers") @@ -150,7 +153,7 @@ class WlanConfigDialog(Dialog): :return: nothing """ - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) @@ -161,6 +164,10 @@ class WlanConfigDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, padx=2, sticky="ew") + def click_mobility(self): + dialog = MobilityConfigDialog(self, self.app, self.canvas_node) + dialog.show() + def click_icon(self): dialog = IconDialog( self, self.app, self.canvas_node.name, self.canvas_node.image diff --git a/coretk/coretk/theme.py b/coretk/coretk/theme.py deleted file mode 100644 index 9525d35e..00000000 --- a/coretk/coretk/theme.py +++ /dev/null @@ -1,162 +0,0 @@ -import tkinter as tk -from tkinter import ttk - - -class Colors: - disabledfg = "DarkGrey" - frame = "#424242" - dark = "#222222" - darker = "#121212" - darkest = "black" - lighter = "#626262" - lightest = "#ffffff" - selectbg = "#4a6984" - selectfg = "#ffffff" - white = "white" - black = "black" - - -style = ttk.Style() -style.theme_create( - "black", - "clam", - { - ".": { - "configure": { - "background": Colors.frame, - "foreground": Colors.white, - "bordercolor": Colors.darkest, - "darkcolor": Colors.dark, - "lightcolor": Colors.lighter, - "troughcolor": Colors.darker, - "selectbackground": Colors.selectbg, - "selectforeground": Colors.selectfg, - "selectborderwidth": 0, - "font": "TkDefaultFont", - }, - "map": { - "background": [("disabled", Colors.frame), ("active", Colors.lighter)], - "foreground": [("disabled", Colors.disabledfg)], - "selectbackground": [("!focus", Colors.darkest)], - "selectforeground": [("!focus", Colors.white)], - }, - }, - "TButton": { - "configure": {"width": 8, "padding": (5, 1), "relief": tk.RAISED}, - "map": { - "relief": [("pressed", tk.SUNKEN)], - "shiftrelief": [("pressed", 1)], - }, - }, - "TMenubutton": { - "configure": {"width": 11, "padding": (5, 1), "relief": tk.RAISED} - }, - "TCheckbutton": { - "configure": { - "indicatorbackground": Colors.white, - "indicatormargin": (1, 1, 4, 1), - } - }, - "TRadiobutton": { - "configure": { - "indicatorbackground": Colors.white, - "indicatormargin": (1, 1, 4, 1), - } - }, - "TEntry": { - "configure": { - "fieldbackground": Colors.white, - "foreground": Colors.black, - "padding": (2, 0), - } - }, - "TCombobox": { - "configure": { - "fieldbackground": Colors.white, - "foreground": Colors.black, - "padding": (2, 0), - } - }, - "TNotebook.Tab": { - "configure": {"padding": (6, 2, 6, 2)}, - "map": {"background": [("selected", Colors.lighter)]}, - }, - "Treeview": { - "configure": { - "fieldbackground": Colors.white, - "background": Colors.white, - "foreground": Colors.black, - }, - "map": { - "background": [("selected", Colors.selectbg)], - "foreground": [("selected", Colors.selectfg)], - }, - }, - }, -) -style.theme_use("black") - - -def update_menu(event): - bg = style.lookup(".", "background") - fg = style.lookup(".", "foreground") - abg = style.lookup(".", "lightcolor") - event.widget.config( - background=bg, foreground=fg, activebackground=abg, activeforeground=fg - ) - - -class Application(ttk.Frame): - def __init__(self, master=None): - super().__init__(master) - self.master.bind_class("Menu", "<>", update_menu) - self.master.geometry("800x600") - menu = tk.Menu(self.master) - menu.add_command(label="Command1") - menu.add_command(label="Command2") - submenu = tk.Menu(menu, tearoff=False) - submenu.add_command(label="Command1") - submenu.add_command(label="Command2") - menu.add_cascade(label="Submenu", menu=submenu) - self.master.config(menu=menu) - self.master.columnconfigure(0, weight=1) - self.master.rowconfigure(0, weight=1) - notebook = ttk.Notebook(self.master) - notebook.grid(sticky="nsew") - frame = ttk.Frame(notebook) - frame.grid(sticky="nsew") - ttk.Label(frame, text="Label").grid() - ttk.Entry(frame).grid() - ttk.Button(frame, text="Button").grid() - ttk.Combobox(frame, values=("one", "two", "three")).grid() - menubutton = ttk.Menubutton(frame, text="MenuButton") - menubutton.grid() - mbmenu = tk.Menu(menubutton, tearoff=False) - menubutton.config(menu=mbmenu) - mbmenu.add_command(label="Menu1") - mbmenu.add_command(label="Menu2") - submenu = tk.Menu(mbmenu, tearoff=False) - submenu.add_command(label="Command1") - submenu.add_command(label="Command2") - mbmenu.add_cascade(label="Submenu", menu=submenu) - ttk.Radiobutton(frame, text="Radio Button").grid() - ttk.Checkbutton(frame, text="Check Button").grid() - tv = ttk.Treeview(frame, columns=("one", "two", "three"), show="headings") - tv.grid() - tv.column("one", stretch=tk.YES) - tv.heading("one", text="ID") - tv.column("two", stretch=tk.YES) - tv.heading("two", text="State") - tv.column("three", stretch=tk.YES) - tv.heading("three", text="Node Count") - tv.insert("", tk.END, text="1", values=("v1", "v2", "v3")) - tv.insert("", tk.END, text="2", values=("v1", "v2", "v3")) - notebook.add(frame, text="Tab1") - frame = ttk.Frame(notebook) - frame.grid(sticky="nsew") - notebook.add(frame, text="Tab2") - - -if __name__ == "__main__": - app = Application() - app.mainloop() diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py new file mode 100644 index 00000000..b661b644 --- /dev/null +++ b/coretk/coretk/themes.py @@ -0,0 +1,127 @@ +import tkinter as tk + +DARK = "black" + + +class Styles: + tooltip = "Tooltip.TLabel" + tooltip_frame = "Tooltip.TFrame" + + +class Colors: + disabledfg = "DarkGrey" + frame = "#424242" + dark = "#222222" + darker = "#121212" + darkest = "black" + lighter = "#626262" + lightest = "#ffffff" + selectbg = "#4a6984" + selectfg = "#ffffff" + white = "white" + black = "black" + + +def load(style): + style.theme_create( + DARK, + "clam", + { + ".": { + "configure": { + "background": Colors.frame, + "foreground": Colors.white, + "bordercolor": Colors.darkest, + "darkcolor": Colors.dark, + "lightcolor": Colors.lighter, + "troughcolor": Colors.darker, + "selectbackground": Colors.selectbg, + "selectforeground": Colors.selectfg, + "selectborderwidth": 0, + "font": "TkDefaultFont", + }, + "map": { + "background": [ + ("disabled", Colors.frame), + ("active", Colors.lighter), + ], + "foreground": [("disabled", Colors.disabledfg)], + "selectbackground": [("!focus", Colors.darkest)], + "selectforeground": [("!focus", Colors.white)], + }, + }, + "TButton": { + "configure": {"width": 8, "padding": (5, 1), "relief": tk.RAISED}, + "map": { + "relief": [("pressed", tk.SUNKEN)], + "shiftrelief": [("pressed", 1)], + }, + }, + "TMenubutton": { + "configure": {"width": 11, "padding": (5, 1), "relief": tk.RAISED} + }, + "TCheckbutton": { + "configure": { + "indicatorbackground": Colors.white, + "indicatormargin": (1, 1, 4, 1), + } + }, + "TRadiobutton": { + "configure": { + "indicatorbackground": Colors.white, + "indicatormargin": (1, 1, 4, 1), + } + }, + "TEntry": { + "configure": { + "fieldbackground": Colors.white, + "foreground": Colors.black, + "padding": (2, 0), + } + }, + "TCombobox": { + "configure": { + "fieldbackground": Colors.white, + "foreground": Colors.black, + "padding": (2, 0), + } + }, + "TNotebook.Tab": { + "configure": {"padding": (6, 2, 6, 2)}, + "map": {"background": [("selected", Colors.lighter)]}, + }, + "Treeview": { + "configure": { + "fieldbackground": Colors.white, + "background": Colors.white, + "foreground": Colors.black, + }, + "map": { + "background": [("selected", Colors.selectbg)], + "foreground": [("selected", Colors.selectfg)], + }, + }, + Styles.tooltip: { + "configure": {"justify": tk.LEFT, "relief": tk.SOLID, "borderwidth": 0} + }, + Styles.tooltip_frame: {"configure": {}}, + }, + ) + + +def update_toplevel(style, event): + if not isinstance(event.widget, tk.Toplevel): + return + bg = style.lookup(".", "background") + event.widget.config(background=bg) + + +def update_menu(style, event): + if not isinstance(event.widget, tk.Menu): + return + bg = style.lookup(".", "background") + fg = style.lookup(".", "foreground") + abg = style.lookup(".", "lightcolor") + event.widget.config( + background=bg, foreground=fg, activebackground=abg, activeforeground=fg + ) diff --git a/coretk/coretk/tooltip.py b/coretk/coretk/tooltip.py index 1877fd24..9a3f7ade 100644 --- a/coretk/coretk/tooltip.py +++ b/coretk/coretk/tooltip.py @@ -1,6 +1,8 @@ import tkinter as tk from tkinter import ttk +from coretk.themes import Styles + class Tooltip(object): """ @@ -41,15 +43,12 @@ class Tooltip(object): self.tw = tk.Toplevel(self.widget) self.tw.wm_overrideredirect(True) self.tw.wm_geometry("+%d+%d" % (x, y)) - label = ttk.Label( - self.tw, - text=self.text, - justify=tk.LEFT, - background="#FFFFEA", - relief=tk.SOLID, - borderwidth=0, - ) - label.grid(padx=1) + self.tw.rowconfigure(0, weight=1) + self.tw.columnconfigure(0, weight=1) + frame = ttk.Frame(self.tw, style=Styles.tooltip_frame, padding=3) + frame.grid(sticky="nsew") + label = ttk.Label(frame, text=self.text, style=Styles.tooltip) + label.grid() def close(self, event=None): if self.tw: diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 9ae9e851..87651701 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -17,9 +17,9 @@ INT_TYPES = { } -class FrameScroll(tk.LabelFrame): - def __init__(self, master=None, cnf={}, _cls=tk.Frame, **kw): - super().__init__(master, cnf, **kw) +class FrameScroll(ttk.LabelFrame): + def __init__(self, master=None, _cls=tk.Frame, **kw): + super().__init__(master, **kw) self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) self.canvas = tk.Canvas(self, highlightthickness=0) @@ -54,8 +54,8 @@ class FrameScroll(tk.LabelFrame): class ConfigFrame(FrameScroll): - def __init__(self, master=None, cnf={}, config=None, **kw): - super().__init__(master, cnf, ttk.Notebook, **kw) + def __init__(self, master=None, config=None, **kw): + super().__init__(master, ttk.Notebook, **kw) self.config = config self.values = {} @@ -126,9 +126,9 @@ class ConfigFrame(FrameScroll): return {x: self.config[x].value for x in self.config} -class ListboxScroll(tk.LabelFrame): - def __init__(self, master=None, cnf={}, **kw): - super().__init__(master, cnf, **kw) +class ListboxScroll(ttk.LabelFrame): + def __init__(self, master=None, **kw): + super().__init__(master, **kw) self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL) @@ -141,8 +141,8 @@ class ListboxScroll(tk.LabelFrame): class CheckboxList(FrameScroll): - def __init__(self, master=None, cnf={}, clicked=None, **kw): - super().__init__(master, cnf, **kw) + def __init__(self, master=None, clicked=None, **kw): + super().__init__(master, **kw) self.clicked = clicked self.frame.columnconfigure(0, weight=1) From f01a8a4cb2789b9fa8aef8da90a08828da83c5ed Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 13 Nov 2019 10:51:16 -0800 Subject: [PATCH 225/462] node service config --- coretk/coretk/coreclient.py | 7 +++++++ coretk/coretk/dialogs/serviceconfiguration.py | 17 ++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 0d562f8a..51aa8e46 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -427,6 +427,13 @@ class CoreClient: ) logging.debug("set node service %s", response) + def get_node_service_file(self, node_id, service_name, file_name): + response = self.client.get_node_service_file( + self.session_id, node_id, service_name, file_name + ) + logging.debug("get service file %s", response) + return response.data + def create_nodes_and_links(self): node_protos = self.get_nodes_proto() link_protos = self.get_links_proto() diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 5cc41116..c1687cd8 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -2,6 +2,7 @@ import logging import tkinter as tk from tkinter import ttk +from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog @@ -38,6 +39,7 @@ class ServiceConfiguration(Dialog): self.validate_commands_listbox = None self.validation_time_entry = None self.validation_mode_entry = None + self.service_file_data = None self.load() self.draw() @@ -153,6 +155,9 @@ class ServiceConfiguration(Dialog): button.grid(row=0, column=2) frame.grid(row=3, column=0, sticky="nsew") + self.service_file_data = ScrolledText(tab1) + self.service_file_data.grid(row=4, column=0, sticky="nsew") + # tab 2 label = ttk.Label( tab2, @@ -333,6 +338,9 @@ class ServiceConfiguration(Dialog): validate_commands, shutdown_commands, ) + filename = self.filename_combobox.get() + file_data = self.service_file_data.get() + print(filename, file_data) logging.info( "%s, %s, %s, %s, %s", metadata, @@ -347,7 +355,14 @@ class ServiceConfiguration(Dialog): ) def display_service_file_data(self, event): - print("not implemented") + combobox = event.widget + filename = combobox.get() + print(filename) + file_data = self.app.core.get_node_service_file( + self.canvas_node.core_id, self.service_name, filename + ) + self.service_file_data.delete(1.0, "end") + self.service_file_data.insert("end", file_data) def click_defaults(self): logging.info("not implemented") From 40b2c270e49684bae2692ffc2b9fc4e318b09ca6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 13 Nov 2019 12:11:37 -0800 Subject: [PATCH 226/462] fixed typo in canvas dialog variable, updated theming to make label frame have a relief groove border --- coretk/coretk/dialogs/canvassizeandscale.py | 8 ++++---- coretk/coretk/themes.py | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 09132aef..5bbd2ec2 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -8,7 +8,7 @@ from coretk.dialogs.canvasbackground import ScaleOption from coretk.dialogs.dialog import Dialog DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] -FRAME_BAD = 5 +FRAME_PAD = 5 PAD = (0, 0, 5, 0) PADX = 5 @@ -52,7 +52,7 @@ class SizeAndScaleDialog(Dialog): self.draw_buttons() def draw_size(self): - label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_BAD) + label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -89,7 +89,7 @@ class SizeAndScaleDialog(Dialog): label.grid(row=0, column=4, sticky="w") def draw_scale(self): - label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_BAD) + label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -105,7 +105,7 @@ class SizeAndScaleDialog(Dialog): def draw_reference_point(self): label_frame = ttk.Labelframe( - self.top, text="Reference Point", padding=FRAME_BAD + self.top, text="Reference Point", padding=FRAME_PAD ) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index b661b644..8eefa35e 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -86,6 +86,7 @@ def load(style): "padding": (2, 0), } }, + "TLabelframe": {"configure": {"relief": tk.GROOVE}}, "TNotebook.Tab": { "configure": {"padding": (6, 2, 6, 2)}, "map": {"background": [("selected", Colors.lighter)]}, From 08927b180a37488868050eaa7fa1cff1e06bbe6b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 13 Nov 2019 12:42:16 -0800 Subject: [PATCH 227/462] changed service checklist style --- coretk/coretk/themes.py | 8 ++++++++ coretk/coretk/widgets.py | 10 +++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index 8eefa35e..8bbdb545 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -6,6 +6,7 @@ DARK = "black" class Styles: tooltip = "Tooltip.TLabel" tooltip_frame = "Tooltip.TFrame" + service_checkbutton = "Service.TCheckbutton" class Colors: @@ -20,6 +21,7 @@ class Colors: selectfg = "#ffffff" white = "white" black = "black" + listboxbg = "#f2f1f0" def load(style): @@ -106,6 +108,12 @@ def load(style): "configure": {"justify": tk.LEFT, "relief": tk.SOLID, "borderwidth": 0} }, Styles.tooltip_frame: {"configure": {}}, + Styles.service_checkbutton: { + "configure": { + "background": Colors.listboxbg, + "foreground": Colors.black, + } + }, }, ) diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 87651701..d91f7f8a 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -4,6 +4,7 @@ from functools import partial from tkinter import ttk from core.api.grpc import core_pb2 +from coretk.themes import Styles INT_TYPES = { core_pb2.ConfigOptionType.UINT8, @@ -136,6 +137,7 @@ class ListboxScroll(ttk.LabelFrame): self.listbox = tk.Listbox( self, selectmode=tk.SINGLE, yscrollcommand=self.scrollbar.set ) + logging.info("listbox background: %s", self.listbox.cget("background")) self.listbox.grid(row=0, column=0, sticky="nsew") self.scrollbar.config(command=self.listbox.yview) @@ -149,5 +151,11 @@ class CheckboxList(FrameScroll): def add(self, name, checked): var = tk.BooleanVar(value=checked) func = partial(self.clicked, name, var) - checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func) + checkbox = ttk.Checkbutton( + self.frame, + text=name, + variable=var, + command=func, + style=Styles.service_checkbutton, + ) checkbox.grid(sticky="w") From d63da73581174b93e979aecf5199ca5e6d8b4d67 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 13 Nov 2019 13:19:18 -0800 Subject: [PATCH 228/462] updated framescroll to dynamically set bg based on current style --- coretk/coretk/app.py | 2 -- coretk/coretk/dialogs/customnodes.py | 2 +- coretk/coretk/dialogs/emaneconfig.py | 4 ++-- coretk/coretk/dialogs/nodeservice.py | 2 +- coretk/coretk/dialogs/sessionoptions.py | 2 +- coretk/coretk/themes.py | 6 +++--- coretk/coretk/widgets.py | 23 +++++++++-------------- 7 files changed, 17 insertions(+), 24 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index bc3ac9dd..b66f1388 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -40,8 +40,6 @@ class Application(tk.Frame): self.style.theme_use(themes.DARK) func = partial(themes.update_menu, self.style) self.master.bind_class("Menu", "<>", func) - func = partial(themes.update_toplevel, self.style) - self.master.bind_class("Toplevel", "<>", func) def setup_app(self): self.master.title("CORE") diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 154b7868..f427f08b 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -35,7 +35,7 @@ class ServicesSelectDialog(Dialog): self.groups.listbox.selection_set(0) self.services = CheckboxList( - frame, text="Services", clicked=self.service_clicked + frame, self.app, text="Services", clicked=self.service_clicked ) self.services.grid(row=0, column=1, sticky="nsew") diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index 181e1264..d64e9089 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -93,7 +93,7 @@ class EmaneConfiguration(Dialog): self.emane_dialog.top.columnconfigure(0, weight=1) self.emane_dialog.top.rowconfigure(0, weight=1) self.emane_config_frame = ConfigFrame( - self.emane_dialog.top, config=self.options + self.emane_dialog.top, self.app, config=self.options ) self.emane_config_frame.draw_config() self.emane_config_frame.grid(sticky="nsew") @@ -167,7 +167,7 @@ class EmaneConfiguration(Dialog): self.model_options = response.config self.model_config_frame = ConfigFrame( - self.emane_model_dialog.top, config=self.model_options + self.emane_model_dialog.top, self.app, config=self.model_options ) self.model_config_frame.grid(sticky="nsew") self.model_config_frame.draw_config() diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index ce5a4715..9ced9205 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -38,7 +38,7 @@ class NodeService(Dialog): self.groups.listbox.selection_set(0) self.services = CheckboxList( - frame, text="Services", clicked=self.service_clicked + frame, self.app, text="Services", clicked=self.service_clicked ) self.services.grid(row=0, column=1, sticky="nsew") diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index 8cd7ad68..b2666015 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -22,7 +22,7 @@ class SessionOptionsDialog(Dialog): response = self.app.core.client.get_session_options(session_id) logging.info("session options: %s", response) - self.config_frame = ConfigFrame(self.top, config=response.config) + self.config_frame = ConfigFrame(self.top, self.app, config=response.config) self.config_frame.draw_config() self.config_frame.grid(sticky="nsew") diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index 8bbdb545..f6dced19 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -1,3 +1,4 @@ +import logging import tkinter as tk DARK = "black" @@ -118,9 +119,8 @@ def load(style): ) -def update_toplevel(style, event): - if not isinstance(event.widget, tk.Toplevel): - return +def update_bg(style, event): + logging.info("updating background: %s", event.widget) bg = style.lookup(".", "background") event.widget.config(background=bg) diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index d91f7f8a..e825fdb4 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -4,7 +4,6 @@ from functools import partial from tkinter import ttk from core.api.grpc import core_pb2 -from coretk.themes import Styles INT_TYPES = { core_pb2.ConfigOptionType.UINT8, @@ -19,11 +18,13 @@ INT_TYPES = { class FrameScroll(ttk.LabelFrame): - def __init__(self, master=None, _cls=tk.Frame, **kw): + def __init__(self, master, app, _cls=ttk.Frame, **kw): super().__init__(master, **kw) + self.app = app self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) - self.canvas = tk.Canvas(self, highlightthickness=0) + bg = self.app.style.lookup(".", "background") + self.canvas = tk.Canvas(self, highlightthickness=0, background=bg) self.canvas.grid(row=0, sticky="nsew", padx=2, pady=2) self.canvas.columnconfigure(0, weight=1) self.canvas.rowconfigure(0, weight=1) @@ -55,8 +56,8 @@ class FrameScroll(ttk.LabelFrame): class ConfigFrame(FrameScroll): - def __init__(self, master=None, config=None, **kw): - super().__init__(master, ttk.Notebook, **kw) + def __init__(self, master, app, config, **kw): + super().__init__(master, app, ttk.Notebook, **kw) self.config = config self.values = {} @@ -143,19 +144,13 @@ class ListboxScroll(ttk.LabelFrame): class CheckboxList(FrameScroll): - def __init__(self, master=None, clicked=None, **kw): - super().__init__(master, **kw) + def __init__(self, master, app, clicked=None, **kw): + super().__init__(master, app, **kw) self.clicked = clicked self.frame.columnconfigure(0, weight=1) def add(self, name, checked): var = tk.BooleanVar(value=checked) func = partial(self.clicked, name, var) - checkbox = ttk.Checkbutton( - self.frame, - text=name, - variable=var, - command=func, - style=Styles.service_checkbutton, - ) + checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func) checkbox.grid(sticky="w") From 145abca8632dcf1f858b8182683a44567b5dedb4 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 13 Nov 2019 22:49:32 -0800 Subject: [PATCH 229/462] added theme configuration to preferences dialog --- coretk/coretk/app.py | 4 ++-- coretk/coretk/appconfig.py | 3 +++ coretk/coretk/dialogs/preferences.py | 36 +++++++++++++++++++++------- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index b66f1388..5481ad19 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -15,6 +15,7 @@ from coretk.toolbar import Toolbar class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) + self.config = appconfig.read() self.style = ttk.Style() self.setup_theme() self.menubar = None @@ -29,7 +30,6 @@ class Application(tk.Frame): self.radiovar = tk.IntVar(value=1) self.show_grid_var = tk.IntVar(value=1) self.adjust_to_dim_var = tk.IntVar(value=0) - self.config = appconfig.read() self.core = CoreClient(self) self.setup_app() self.draw() @@ -37,7 +37,7 @@ class Application(tk.Frame): def setup_theme(self): themes.load(self.style) - self.style.theme_use(themes.DARK) + self.style.theme_use(self.config["preferences"]["theme"]) func = partial(themes.update_menu, self.style) self.master.bind_class("Menu", "<>", func) diff --git a/coretk/coretk/appconfig.py b/coretk/coretk/appconfig.py index 67f97181..e8f5db6e 100644 --- a/coretk/coretk/appconfig.py +++ b/coretk/coretk/appconfig.py @@ -6,6 +6,8 @@ from pathlib import Path import yaml # gui home paths +from coretk import themes + HOME_PATH = Path.home().joinpath(".coretk") BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds") CUSTOM_EMANE_PATH = HOME_PATH.joinpath("custom_emane") @@ -68,6 +70,7 @@ def check_directory(): editor = EDITORS[1] config = { "preferences": { + "theme": themes.DARK, "editor": editor, "terminal": terminal, "gui3d": "/usr/local/bin/std3d.sh", diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py index 0c426d3c..148cb6bb 100644 --- a/coretk/coretk/dialogs/preferences.py +++ b/coretk/coretk/dialogs/preferences.py @@ -1,3 +1,4 @@ +import logging import tkinter as tk from tkinter import ttk @@ -10,41 +11,52 @@ class PreferencesDialog(Dialog): super().__init__(master, app, "Preferences", modal=True) preferences = self.app.config["preferences"] self.editor = tk.StringVar(value=preferences["editor"]) + self.theme = tk.StringVar(value=preferences["theme"]) self.terminal = tk.StringVar(value=preferences["terminal"]) self.gui3d = tk.StringVar(value=preferences["gui3d"]) self.draw() def draw(self): self.top.columnconfigure(0, weight=1) - self.draw_programs() + self.draw_preferences() self.draw_buttons() - def draw_programs(self): - frame = ttk.LabelFrame(self.top, text="Programs") + def draw_preferences(self): + frame = ttk.LabelFrame(self.top, text="Preferences") frame.grid(sticky="ew", pady=2) frame.columnconfigure(1, weight=1) - label = ttk.Label(frame, text="Editor") + label = ttk.Label(frame, text="Theme") label.grid(row=0, column=0, pady=2, padx=2, sticky="w") + themes = self.app.style.theme_names() + combobox = ttk.Combobox( + frame, textvariable=self.theme, values=themes, state="readonly" + ) + combobox.set(self.theme.get()) + combobox.grid(row=0, column=1, sticky="ew") + combobox.bind("<>", self.theme_change) + + label = ttk.Label(frame, text="Editor") + label.grid(row=1, column=0, pady=2, padx=2, sticky="w") combobox = ttk.Combobox( frame, textvariable=self.editor, values=appconfig.EDITORS, state="readonly" ) - combobox.grid(row=0, column=1, sticky="ew") + combobox.grid(row=1, column=1, sticky="ew") label = ttk.Label(frame, text="Terminal") - label.grid(row=1, column=0, pady=2, padx=2, sticky="w") + label.grid(row=2, column=0, pady=2, padx=2, sticky="w") combobox = ttk.Combobox( frame, textvariable=self.terminal, values=appconfig.TERMINALS, state="readonly", ) - combobox.grid(row=1, column=1, sticky="ew") + combobox.grid(row=2, column=1, sticky="ew") label = ttk.Label(frame, text="3D GUI") - label.grid(row=2, column=0, pady=2, padx=2, sticky="w") + label.grid(row=3, column=0, pady=2, padx=2, sticky="w") entry = ttk.Entry(frame, textvariable=self.gui3d) - entry.grid(row=2, column=1, sticky="ew") + entry.grid(row=3, column=1, sticky="ew") def draw_buttons(self): frame = ttk.Frame(self.top) @@ -58,10 +70,16 @@ class PreferencesDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") + def theme_change(self, event): + theme = self.theme.get() + logging.info("changing theme: %s", theme) + self.app.style.theme_use(theme) + def click_save(self): preferences = self.app.config["preferences"] preferences["terminal"] = self.terminal.get() preferences["editor"] = self.editor.get() preferences["gui3d"] = self.gui3d.get() + preferences["theme"] = self.theme.get() self.app.save_config() self.destroy() From 31d87810086f1482715cea1d619c6499d97b130e Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 13 Nov 2019 23:03:12 -0800 Subject: [PATCH 230/462] fixed emane icon background to alpha, updated wlan icon to have a white cloud instead of alpha --- coretk/coretk/icons/emane.gif | Bin 1111 -> 337 bytes coretk/coretk/icons/wlan.gif | Bin 146 -> 173 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/coretk/coretk/icons/emane.gif b/coretk/coretk/icons/emane.gif index 8a3d3850d4c01d35e3322c97b8cd0ab00251ecd8..0531a932532f4573b3f581364a01a059a7a54e26 100644 GIT binary patch literal 337 zcmZ?wbhEHbRA5kGc+A591jgD7#-R)|uL6w9SoqqGI`S+J)Z}3~{55m3>-wv~T?H#< zW@b2-6z*ZzwOP^2gZrP5;o(z43Q~UM#g#@z4%MQzE@c_^bw;Lk?IH|)m32vxaa}CA z3fvO{o3pwW8foTD(O4XmndY#7V{QjS_&47}7hO2# Z!QgRoI`a`OK~`a{m=hZowsSC80|0bwhHwA? literal 1111 zcmW+!Uue)(7(D}p89_BM)XmtxBE44IKCNjFQrT)0L!wKI3bD;^%+_DXryeeCA)}Qv zqWBE4bQrr}ZY?5waS`Q$uZ-BG@MV#(psWa4Qc771P(TF@bg;Tq+#*nc3B1B2 zK!Qk62`0fMP$DFXM3ra~T_Pnxl1NfXCdnmHG9-&+m28q-GNnL@NKq*!#idXPp$JuI zLKjv#sxO zw1^hfVp?1al~9UOm8NuMWkKb*7}Q_}uN<)e3t~Ynm<6{$i?Apb)uLH+i?jqwVo5ET zCAUP&uq>9w?ZR~VpO9U-B`I_g)I(sn8T|uJivo^P!Hz8JGnl_iy~ z3aqLSz#t6DU<}ScMlcGaG8&^Zk_k-0q)f)-Ok@VLFe|e$J2P3pA}q>cEY7k_Ru!#E zyRy8RdxdK$1^z}{u4nEZX z&53(&(@U*+B98YSUj5?1%Z=$|%fORwc6Tg3piPIL-c~!jIQ{Ctxs&TQ*DYRn`If;W zpa1>ssk!l6f3I8h!yTs{o9sUN^RM&kj?3&_KOHFbkM?(+9%=8n{z%#K;M|Fg7gt~V z_J{pnZhvj`w@=o8FnDHP`_7B4y*Cbh-@5Cav4)wCXW!~s7+)Ie%@;jEB@otNY*qmFfdba%1_PAOJ`90$->CRz{sEjQUOxS zz!cuozw-23{>32+u5qs4D3yPjAx>eEpJZvqkAmdpb(+5?eJb>ho%FhP{o<=WA`fyl xetyz3Q~L84X{Px{Rg%)5F0}|bV|49q%MP!#xfR=XUU=m{gSY?m^L8c%YXB@FJ0t)A From 4d2b84b107cff0fce3e943ca8dfd8d9bb36b420c Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 14 Nov 2019 11:26:20 -0800 Subject: [PATCH 231/462] updated wallpaper dialog to now save and redisplay current wallpaper, updated app to display in center of screen on launch --- coretk/coretk/app.py | 29 +++-- coretk/coretk/dialogs/canvasbackground.py | 132 ++++++++++---------- coretk/coretk/dialogs/canvassizeandscale.py | 66 +++++----- coretk/coretk/graph.py | 14 ++- coretk/coretk/menuaction.py | 1 - 5 files changed, 124 insertions(+), 118 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 5481ad19..11fad378 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -15,21 +15,19 @@ from coretk.toolbar import Toolbar class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) - self.config = appconfig.read() - self.style = ttk.Style() - self.setup_theme() + # widgets self.menubar = None self.toolbar = None self.canvas = None self.statusbar = None - self.is_open_xml = False - self.size_and_scale = None + + # variables self.set_wallpaper = None - self.wallpaper_id = None - self.current_wallpaper = None - self.radiovar = tk.IntVar(value=1) - self.show_grid_var = tk.IntVar(value=1) - self.adjust_to_dim_var = tk.IntVar(value=0) + + # setup + self.config = appconfig.read() + self.style = ttk.Style() + self.setup_theme() self.core = CoreClient(self) self.setup_app() self.draw() @@ -43,12 +41,21 @@ class Application(tk.Frame): def setup_app(self): self.master.title("CORE") - self.master.geometry("1000x800") + self.center() self.master.protocol("WM_DELETE_WINDOW", self.on_closing) image = Images.get(ImageEnum.CORE, 16) self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) + def center(self): + width = 1000 + height = 800 + 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)) + self.master.geometry(f"{width}x{height}+{x}+{y}") + def draw(self): self.master.option_add("*tearOff", tk.FALSE) self.menubar = Menubar(self.master, self) diff --git a/coretk/coretk/dialogs/canvasbackground.py b/coretk/coretk/dialogs/canvasbackground.py index 7b1dd186..80b1ae5f 100644 --- a/coretk/coretk/dialogs/canvasbackground.py +++ b/coretk/coretk/dialogs/canvasbackground.py @@ -10,6 +10,7 @@ from PIL import Image, ImageTk from coretk.appconfig import BACKGROUNDS_PATH from coretk.dialogs.dialog import Dialog +from coretk.images import Images PADX = 5 @@ -31,11 +32,11 @@ class CanvasBackgroundDialog(Dialog): """ super().__init__(master, app, "Canvas Background", modal=True) self.canvas = self.app.canvas - self.radiovar = tk.IntVar(value=self.app.radiovar.get()) - self.show_grid_var = tk.IntVar(value=self.app.show_grid_var.get()) - self.adjust_to_dim_var = tk.IntVar(value=self.app.adjust_to_dim_var.get()) + self.scale_option = tk.IntVar(value=self.canvas.scale_option.get()) + self.show_grid = tk.IntVar(value=self.canvas.show_grid.get()) + self.adjust_to_dim = tk.IntVar(value=self.canvas.adjust_to_dim.get()) + self.filename = tk.StringVar(value=self.canvas.wallpaper_file) self.image_label = None - self.file_name = tk.StringVar() self.options = [] self.draw() @@ -57,6 +58,8 @@ class CanvasBackgroundDialog(Dialog): def draw_image_label(self): label = ttk.Label(self.top, text="Image filename: ") label.grid(row=1, column=0, sticky="ew") + if self.filename.get(): + self.draw_preview() def draw_image_selection(self): frame = ttk.Frame(self.top) @@ -65,7 +68,7 @@ class CanvasBackgroundDialog(Dialog): frame.columnconfigure(2, weight=1) frame.grid(row=2, column=0, sticky="ew") - entry = ttk.Entry(frame, textvariable=self.file_name) + entry = ttk.Entry(frame, textvariable=self.filename) entry.focus() entry.grid(row=0, column=0, sticky="ew", padx=PADX) @@ -84,41 +87,45 @@ class CanvasBackgroundDialog(Dialog): frame.grid(row=3, column=0, sticky="ew") button = ttk.Radiobutton( - frame, text="upper-left", value=1, variable=self.radiovar + frame, text="upper-left", value=1, variable=self.scale_option ) button.grid(row=0, column=0, sticky="ew") self.options.append(button) button = ttk.Radiobutton( - frame, text="centered", value=2, variable=self.radiovar + frame, text="centered", value=2, variable=self.scale_option ) button.grid(row=0, column=1, sticky="ew") self.options.append(button) - button = ttk.Radiobutton(frame, text="scaled", value=3, variable=self.radiovar) + button = ttk.Radiobutton( + frame, text="scaled", value=3, variable=self.scale_option + ) button.grid(row=0, column=2, sticky="ew") self.options.append(button) - button = ttk.Radiobutton(frame, text="titled", value=4, variable=self.radiovar) + button = ttk.Radiobutton( + frame, text="titled", value=4, variable=self.scale_option + ) button.grid(row=0, column=3, sticky="ew") self.options.append(button) def draw_additional_options(self): checkbutton = ttk.Checkbutton( - self.top, text="Show grid", variable=self.show_grid_var + self.top, text="Show grid", variable=self.show_grid ) checkbutton.grid(row=4, column=0, sticky="ew", padx=PADX) checkbutton = ttk.Checkbutton( self.top, text="Adjust canvas size to image dimensions", - variable=self.adjust_to_dim_var, + variable=self.adjust_to_dim, command=self.click_adjust_canvas, ) checkbutton.grid(row=5, column=0, sticky="ew", padx=PADX) - self.show_grid_var.set(1) - self.adjust_to_dim_var.set(0) + self.show_grid.set(1) + self.adjust_to_dim.set(0) def draw_buttons(self): frame = ttk.Frame(self.top) @@ -142,13 +149,13 @@ class CanvasBackgroundDialog(Dialog): ), ) if filename: - self.file_name.set(filename) - width, height = 250, 135 - img = Image.open(filename) - img = img.resize((width, height), Image.ANTIALIAS) - tk_img = ImageTk.PhotoImage(img) - self.image_label.config(image=tk_img, width=width) - self.image_label.image = tk_img + self.filename.set(filename) + self.draw_preview() + + def draw_preview(self): + image = Images.create(self.filename.get(), 250, 135) + self.image_label.config(image=image) + self.image_label.image = image def click_clear(self): """ @@ -157,19 +164,20 @@ class CanvasBackgroundDialog(Dialog): :return: nothing """ # delete entry - self.file_name.set("") + self.filename.set("") # delete display image self.image_label.config(image="", width=32) + self.image_label.image = None def click_adjust_canvas(self): # deselect all radio buttons and grey them out - if self.adjust_to_dim_var.get() == 1: - self.radiovar.set(0) + if self.adjust_to_dim.get() == 1: + self.scale_option.set(0) for option in self.options: option.config(state=tk.DISABLED) # turn back the radio button to active state so that user can choose again - elif self.adjust_to_dim_var.get() == 0: - self.radiovar.set(1) + elif self.adjust_to_dim.get() == 0: + self.scale_option.set(1) for option in self.options: option.config(state=tk.NORMAL) else: @@ -192,9 +200,8 @@ class CanvasBackgroundDialog(Dialog): :return: nothing """ - canvas = self.app.canvas - grid = canvas.find_withtag("rectangle")[0] - x0, y0, x1, y1 = canvas.coords(grid) + grid = self.canvas.find_withtag("rectangle")[0] + x0, y0, x1, y1 = self.canvas.coords(grid) canvas_w = abs(x0 - x1) canvas_h = abs(y0 - y1) return canvas_w, canvas_h @@ -224,15 +231,12 @@ class CanvasBackgroundDialog(Dialog): cropped_tk = ImageTk.PhotoImage(cropped) # place left corner of image to the left corner of the canvas - self.app.croppedwallpaper = cropped_tk - + self.canvas.wallpaper_drawn = cropped_tk self.delete_canvas_components(["wallpaper"]) - # self.delete_previous_wallpaper() - wid = self.canvas.create_image( (cropx / 2, cropy / 2), image=cropped_tk, tags="wallpaper" ) - self.app.wallpaper_id = wid + self.canvas.wallpaper_id = wid def center(self, img): """ @@ -261,13 +265,13 @@ class CanvasBackgroundDialog(Dialog): cropped_tk = ImageTk.PhotoImage(cropped) # place the center of the image at the center of the canvas - self.app.croppedwallpaper = cropped_tk self.delete_canvas_components(["wallpaper"]) # self.delete_previous_wallpaper() wid = self.canvas.create_image( (canvas_w / 2, canvas_h / 2), image=cropped_tk, tags="wallpaper" ) - self.app.wallpaper_id = wid + self.canvas.wallpaper_id = wid + self.canvas.wallpaper_drawn = cropped_tk def scaled(self, img): """ @@ -279,15 +283,12 @@ class CanvasBackgroundDialog(Dialog): canvas_w, canvas_h = self.get_canvas_width_and_height() resized_image = img.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS) image_tk = ImageTk.PhotoImage(resized_image) - self.app.croppedwallpaper = image_tk - self.delete_canvas_components(["wallpaper"]) - # self.delete_previous_wallpaper() - wid = self.canvas.create_image( (canvas_w / 2, canvas_h / 2), image=image_tk, tags="wallpaper" ) - self.app.wallpaper_id = wid + self.canvas.wallpaper_id = wid + self.canvas.wallpaper_drawn = image_tk def tiled(self, img): return @@ -310,20 +311,15 @@ class CanvasBackgroundDialog(Dialog): self.delete_canvas_components(["wallpaper"]) self.draw_new_canvas(img_w, img_h) wid = self.canvas.create_image((img_w / 2, img_h / 2), image=image_tk) - self.app.croppedwallpaper = image_tk - self.app.wallpaper_id = wid + self.canvas.wallpaper_id = wid + self.canvas.wallpaper_drawn = image_tk - def show_grid(self): - """ - - :return: nothing - """ - self.app.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) - - if self.show_grid_var.get() == 0: + def draw_grid(self): + self.canvas.adjust_to_dim.set(self.adjust_to_dim.get()) + if self.show_grid.get() == 0: for i in self.canvas.find_withtag("gridline"): self.canvas.itemconfig(i, state=tk.HIDDEN) - elif self.show_grid_var.get() == 1: + elif self.show_grid.get() == 1: for i in self.canvas.find_withtag("gridline"): self.canvas.itemconfig(i, state=tk.NORMAL) self.canvas.lift(i) @@ -331,33 +327,36 @@ class CanvasBackgroundDialog(Dialog): logging.error("canvasbackground.py show_grid invalid value") def save_wallpaper_options(self): - self.app.radiovar.set(self.radiovar.get()) - self.app.show_grid_var.set(self.show_grid_var.get()) - self.app.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) + self.canvas.scale_option.set(self.scale_option.get()) + self.canvas.show_grid.set(self.show_grid.get()) + self.canvas.adjust_to_dim.set(self.adjust_to_dim.get()) def click_apply(self): - filename = self.file_name.get() + filename = self.filename.get() if not filename: self.delete_canvas_components(["wallpaper"]) self.destroy() - self.app.current_wallpaper = None + self.canvas.wallpaper = None + self.canvas.wallpaper_file = None self.save_wallpaper_options() return try: img = Image.open(filename) - self.app.current_wallpaper = img + self.canvas.wallpaper = img + self.canvas.wallpaper_file = filename except FileNotFoundError: logging.error("invalid background: %s", filename) - if self.app.wallpaper_id: - self.canvas.delete(self.app.wallpaper_id) + if self.canvas.wallpaper_id: + self.canvas.delete(self.canvas.wallpaper_id) + self.canvas.wallpaper_id = None self.destroy() return - self.app.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) - if self.adjust_to_dim_var.get() == 0: - self.app.radiovar.set(self.radiovar.get()) - option = ScaleOption(self.radiovar.get()) + self.canvas.adjust_to_dim.set(self.adjust_to_dim.get()) + if self.adjust_to_dim.get() == 0: + self.canvas.scale_option.set(self.scale_option.get()) + option = ScaleOption(self.scale_option.get()) if option == ScaleOption.UPPER_LEFT: self.upper_left(img) elif option == ScaleOption.CENTERED: @@ -365,10 +364,9 @@ class CanvasBackgroundDialog(Dialog): elif option == ScaleOption.SCALED: self.scaled(img) elif option == ScaleOption.TILED: - print("not implemented yet") - - elif self.adjust_to_dim_var.get() == 1: + logging.warning("tiled background not implemented yet") + elif self.adjust_to_dim.get() == 1: self.canvas_to_image_dimension(img) - self.show_grid() + self.draw_grid() self.destroy() diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 5bbd2ec2..598d8d37 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -1,6 +1,7 @@ """ size and scale """ +import logging import tkinter as tk from tkinter import font, ttk @@ -9,7 +10,6 @@ from coretk.dialogs.dialog import Dialog DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] FRAME_PAD = 5 -PAD = (0, 0, 5, 0) PADX = 5 @@ -21,13 +21,13 @@ class SizeAndScaleDialog(Dialog): :param app: main application """ super().__init__(master, app, "Canvas Size and Scale", modal=True) - self.meter_per_pixel = self.app.canvas.meters_per_pixel + self.canvas = self.app.canvas + self.meter_per_pixel = self.canvas.meters_per_pixel self.section_font = font.Font(weight="bold") # get current canvas dimensions - canvas = self.app.canvas - plot = canvas.find_withtag("rectangle") - x0, y0, x1, y1 = canvas.bbox(plot[0]) + plot = self.canvas.find_withtag("rectangle") + x0, y0, x1, y1 = self.canvas.bbox(plot[0]) width = abs(x0 - x1) - 2 height = abs(y0 - y1) - 2 self.pixel_width = tk.IntVar(value=width) @@ -122,14 +122,12 @@ class SizeAndScaleDialog(Dialog): label = ttk.Label(frame, text="X") label.grid(row=0, column=0, sticky="w", padx=PADX) - x_var = tk.StringVar(value=0) - entry = ttk.Entry(frame, textvariable=x_var) + entry = ttk.Entry(frame, textvariable=self.x) entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Y") label.grid(row=0, column=2, sticky="w", padx=PADX) - y_var = tk.StringVar(value=0) - entry = ttk.Entry(frame, textvariable=y_var) + entry = ttk.Entry(frame, textvariable=self.y) entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(label_frame, text="Translates To") @@ -181,40 +179,40 @@ class SizeAndScaleDialog(Dialog): :return: nothing """ width, height = self.pixel_width.get(), self.pixel_height.get() - - canvas = self.app.canvas - canvas.config(scrollregion=(0, 0, width + 200, height + 200)) + self.canvas.config(scrollregion=(0, 0, width + 200, height + 200)) # delete old plot and redraw - for i in canvas.find_withtag("gridline"): - canvas.delete(i) - for i in canvas.find_withtag("rectangle"): - canvas.delete(i) + for i in self.canvas.find_withtag("gridline"): + self.canvas.delete(i) + for i in self.canvas.find_withtag("rectangle"): + self.canvas.delete(i) - canvas.draw_grid(width=width, height=height) + self.canvas.draw_grid(width=width, height=height) # lift anything that is drawn on the plot before for tag in DRAW_OBJECT_TAGS: - for i in canvas.find_withtag(tag): - canvas.lift(i) + for i in self.canvas.find_withtag(tag): + self.canvas.lift(i) def click_apply(self): meter_per_pixel = float(self.scale.get()) / 100 - self.app.canvas.meters_per_pixel = meter_per_pixel + self.canvas.meters_per_pixel = meter_per_pixel self.redraw_grid() - # if there is a current wallpaper showing, redraw it based on current wallpaper options + # if there is a current wallpaper showing, redraw it based on current + # wallpaper options wallpaper_tool = self.app.set_wallpaper - current_wallpaper = self.app.current_wallpaper - if current_wallpaper: - if self.app.adjust_to_dim_var.get() == 0: - if self.app.radiovar.get() == ScaleOption.UPPER_LEFT.value: - wallpaper_tool.upper_left(current_wallpaper) - elif self.app.radiovar.get() == ScaleOption.CENTERED.value: - wallpaper_tool.center(current_wallpaper) - elif self.app.radiovar.get() == ScaleOption.SCALED.value: - wallpaper_tool.scaled(current_wallpaper) - elif self.app.radiovar.get() == ScaleOption.TILED.value: - print("not implemented") - elif self.app.adjust_to_dim_var.get() == 1: - wallpaper_tool.canvas_to_image_dimension(current_wallpaper) + wallpaper = self.canvas.wallpaper + if wallpaper: + if self.canvas.adjust_to_dim.get() == 0: + scale_option = ScaleOption(self.canvas.scale_option.get()) + if scale_option == ScaleOption.UPPER_LEFT: + wallpaper_tool.upper_left(wallpaper) + elif scale_option == ScaleOption.CENTERED: + wallpaper_tool.center(wallpaper) + elif scale_option == ScaleOption.SCALED: + wallpaper_tool.scaled(wallpaper) + elif scale_option == ScaleOption.TILED: + logging.warning("tiled background not implemented") + elif self.canvas.adjust_to_dim.get() == 1: + wallpaper_tool.canvas_to_image_dimension(wallpaper) wallpaper_tool.show_grid() self.destroy() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 5cdd2eec..1bec481b 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -43,11 +43,8 @@ class CanvasGraph(tk.Canvas): self.drawing_edge = None self.grid = None self.meters_per_pixel = 1.5 - self.canvas_management = CanvasComponentManagement(self, core) - self.canvas_action = CanvasAction(master, self) - self.setup_menus() self.setup_bindings() self.draw_grid() @@ -57,6 +54,15 @@ class CanvasGraph(tk.Canvas): self.wireless_draw = WirelessConnection(self, core) self.is_node_context_opened = False + # background related + self.wallpaper_id = None + self.wallpaper = None + self.wallpaper_drawn = None + self.wallpaper_file = "" + self.scale_option = tk.IntVar(value=1) + self.show_grid = tk.IntVar(value=1) + self.adjust_to_dim = tk.IntVar(value=0) + def setup_menus(self): self.node_context = tk.Menu(self.master) self.node_context.add_command( @@ -95,8 +101,6 @@ class CanvasGraph(tk.Canvas): self.drawing_edge = None self.draw_existing_component(session) - # self.grpc_manager.wlanconfig_management.load_wlan_configurations(self.core_grpc) - def setup_bindings(self): """ Bind any mouse events or hot keys to the matching action diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 98b3d254..b47c6ef2 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -73,7 +73,6 @@ class MenuAction: def file_open_xml(self, event=None): logging.info("menuaction.py file_open_xml()") - self.app.is_open_xml = True file_path = filedialog.askopenfilename( initialdir=str(XML_PATH), title="Open", From aec1126a14cca6e1eaaf8394f498cadee13e142d Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 14 Nov 2019 12:58:27 -0800 Subject: [PATCH 232/462] updated canvas dialogs to make use of common canvas redraw/wallpaper logic --- coretk/coretk/app.py | 3 - coretk/coretk/dialogs/canvasbackground.py | 196 ++------------------ coretk/coretk/dialogs/canvassizeandscale.py | 47 +---- coretk/coretk/graph.py | 154 ++++++++++++++- 4 files changed, 159 insertions(+), 241 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 11fad378..185e5968 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -21,9 +21,6 @@ class Application(tk.Frame): self.canvas = None self.statusbar = None - # variables - self.set_wallpaper = None - # setup self.config = appconfig.read() self.style = ttk.Style() diff --git a/coretk/coretk/dialogs/canvasbackground.py b/coretk/coretk/dialogs/canvasbackground.py index 80b1ae5f..8fb92c8b 100644 --- a/coretk/coretk/dialogs/canvasbackground.py +++ b/coretk/coretk/dialogs/canvasbackground.py @@ -1,12 +1,11 @@ """ set wallpaper """ -import enum import logging import tkinter as tk from tkinter import filedialog, ttk -from PIL import Image, ImageTk +from PIL import Image from coretk.appconfig import BACKGROUNDS_PATH from coretk.dialogs.dialog import Dialog @@ -15,14 +14,6 @@ from coretk.images import Images PADX = 5 -class ScaleOption(enum.Enum): - NONE = 0 - UPPER_LEFT = 1 - CENTERED = 2 - SCALED = 3 - TILED = 4 - - class CanvasBackgroundDialog(Dialog): def __init__(self, master, app): """ @@ -33,8 +24,8 @@ class CanvasBackgroundDialog(Dialog): super().__init__(master, app, "Canvas Background", modal=True) self.canvas = self.app.canvas self.scale_option = tk.IntVar(value=self.canvas.scale_option.get()) - self.show_grid = tk.IntVar(value=self.canvas.show_grid.get()) - self.adjust_to_dim = tk.IntVar(value=self.canvas.adjust_to_dim.get()) + self.show_grid = tk.BooleanVar(value=self.canvas.show_grid.get()) + self.adjust_to_dim = tk.BooleanVar(value=self.canvas.adjust_to_dim.get()) self.filename = tk.StringVar(value=self.canvas.wallpaper_file) self.image_label = None self.options = [] @@ -124,9 +115,6 @@ class CanvasBackgroundDialog(Dialog): ) checkbutton.grid(row=5, column=0, sticky="ew", padx=PADX) - self.show_grid.set(1) - self.adjust_to_dim.set(0) - def draw_buttons(self): frame = ttk.Frame(self.top) frame.grid(row=6, column=0, pady=5, sticky="ew") @@ -171,202 +159,40 @@ class CanvasBackgroundDialog(Dialog): def click_adjust_canvas(self): # deselect all radio buttons and grey them out - if self.adjust_to_dim.get() == 1: + if self.adjust_to_dim.get(): self.scale_option.set(0) for option in self.options: option.config(state=tk.DISABLED) # turn back the radio button to active state so that user can choose again - elif self.adjust_to_dim.get() == 0: + else: self.scale_option.set(1) for option in self.options: option.config(state=tk.NORMAL) - else: - logging.error("canvasbackground.py adjust_canvas_size invalid value") - def delete_canvas_components(self, tag_list): - """ - delete canvas items whose tag is in the tag list - - :param list[string] tag_list: list of tags - :return: nothing - """ - for tag in tag_list: - for i in self.canvas.find_withtag(tag): - self.canvas.delete(i) - - def get_canvas_width_and_height(self): - """ - retrieve canvas width and height in pixels - - :return: nothing - """ - grid = self.canvas.find_withtag("rectangle")[0] - x0, y0, x1, y1 = self.canvas.coords(grid) - canvas_w = abs(x0 - x1) - canvas_h = abs(y0 - y1) - return canvas_w, canvas_h - - def determine_cropped_image_dimension(self): - """ - determine the dimension of the image after being cropped - - :return: nothing - """ - return - - def upper_left(self, img): - tk_img = ImageTk.PhotoImage(img) - - # crop image if it is bigger than canvas - canvas_w, canvas_h = self.get_canvas_width_and_height() - - cropx = img_w = tk_img.width() - cropy = img_h = tk_img.height() - - if img_w > canvas_w: - cropx -= img_w - canvas_w - if img_h > canvas_h: - cropy -= img_h - canvas_h - cropped = img.crop((0, 0, cropx, cropy)) - cropped_tk = ImageTk.PhotoImage(cropped) - - # place left corner of image to the left corner of the canvas - self.canvas.wallpaper_drawn = cropped_tk - self.delete_canvas_components(["wallpaper"]) - wid = self.canvas.create_image( - (cropx / 2, cropy / 2), image=cropped_tk, tags="wallpaper" - ) - self.canvas.wallpaper_id = wid - - def center(self, img): - """ - place the image at the center of canvas - - :param Image img: image object - :return: nothing - """ - tk_img = ImageTk.PhotoImage(img) - canvas_w, canvas_h = self.get_canvas_width_and_height() - - cropx = img_w = tk_img.width() - cropy = img_h = tk_img.height() - - # dimension of the cropped image - if img_w > canvas_w: - cropx -= img_w - canvas_w - if img_h > canvas_h: - cropy -= img_h - canvas_h - - x0 = (img_w - cropx) / 2 - y0 = (img_h - cropy) / 2 - x1 = x0 + cropx - y1 = y0 + cropy - cropped = img.crop((x0, y0, x1, y1)) - cropped_tk = ImageTk.PhotoImage(cropped) - - # place the center of the image at the center of the canvas - self.delete_canvas_components(["wallpaper"]) - # self.delete_previous_wallpaper() - wid = self.canvas.create_image( - (canvas_w / 2, canvas_h / 2), image=cropped_tk, tags="wallpaper" - ) - self.canvas.wallpaper_id = wid - self.canvas.wallpaper_drawn = cropped_tk - - def scaled(self, img): - """ - scale image based on canvas dimension - - :param Image img: image object - :return: nothing - """ - canvas_w, canvas_h = self.get_canvas_width_and_height() - resized_image = img.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS) - image_tk = ImageTk.PhotoImage(resized_image) - self.delete_canvas_components(["wallpaper"]) - wid = self.canvas.create_image( - (canvas_w / 2, canvas_h / 2), image=image_tk, tags="wallpaper" - ) - self.canvas.wallpaper_id = wid - self.canvas.wallpaper_drawn = image_tk - - def tiled(self, img): - return - - def draw_new_canvas(self, canvas_width, canvas_height): - """ - delete the old canvas and draw a new one - - :param int canvas_width: canvas width in pixel - :param int canvas_height: canvas height in pixel - :return: - """ - self.delete_canvas_components(["rectangle", "gridline"]) - self.canvas.draw_grid(canvas_width, canvas_height) - - def canvas_to_image_dimension(self, img): - image_tk = ImageTk.PhotoImage(img) - img_w = image_tk.width() - img_h = image_tk.height() - self.delete_canvas_components(["wallpaper"]) - self.draw_new_canvas(img_w, img_h) - wid = self.canvas.create_image((img_w / 2, img_h / 2), image=image_tk) - self.canvas.wallpaper_id = wid - self.canvas.wallpaper_drawn = image_tk - - def draw_grid(self): - self.canvas.adjust_to_dim.set(self.adjust_to_dim.get()) - if self.show_grid.get() == 0: - for i in self.canvas.find_withtag("gridline"): - self.canvas.itemconfig(i, state=tk.HIDDEN) - elif self.show_grid.get() == 1: - for i in self.canvas.find_withtag("gridline"): - self.canvas.itemconfig(i, state=tk.NORMAL) - self.canvas.lift(i) - else: - logging.error("canvasbackground.py show_grid invalid value") - - def save_wallpaper_options(self): + def click_apply(self): self.canvas.scale_option.set(self.scale_option.get()) self.canvas.show_grid.set(self.show_grid.get()) self.canvas.adjust_to_dim.set(self.adjust_to_dim.get()) + self.canvas.update_grid() - def click_apply(self): filename = self.filename.get() if not filename: - self.delete_canvas_components(["wallpaper"]) - self.destroy() + self.canvas.delete(self.canvas.wallpaper_id) self.canvas.wallpaper = None self.canvas.wallpaper_file = None - self.save_wallpaper_options() + self.destroy() return try: img = Image.open(filename) self.canvas.wallpaper = img self.canvas.wallpaper_file = filename + self.canvas.redraw() except FileNotFoundError: logging.error("invalid background: %s", filename) if self.canvas.wallpaper_id: self.canvas.delete(self.canvas.wallpaper_id) self.canvas.wallpaper_id = None - self.destroy() - return + self.canvas.wallpaper_file = None - self.canvas.adjust_to_dim.set(self.adjust_to_dim.get()) - if self.adjust_to_dim.get() == 0: - self.canvas.scale_option.set(self.scale_option.get()) - option = ScaleOption(self.scale_option.get()) - if option == ScaleOption.UPPER_LEFT: - self.upper_left(img) - elif option == ScaleOption.CENTERED: - self.center(img) - elif option == ScaleOption.SCALED: - self.scaled(img) - elif option == ScaleOption.TILED: - logging.warning("tiled background not implemented yet") - elif self.adjust_to_dim.get() == 1: - self.canvas_to_image_dimension(img) - - self.draw_grid() self.destroy() diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 598d8d37..3a72389d 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -1,14 +1,11 @@ """ size and scale """ -import logging import tkinter as tk from tkinter import font, ttk -from coretk.dialogs.canvasbackground import ScaleOption from coretk.dialogs.dialog import Dialog -DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] FRAME_PAD = 5 PADX = 5 @@ -172,47 +169,11 @@ class SizeAndScaleDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def redraw_grid(self): - """ - redraw grid with new dimension - - :return: nothing - """ - width, height = self.pixel_width.get(), self.pixel_height.get() - self.canvas.config(scrollregion=(0, 0, width + 200, height + 200)) - - # delete old plot and redraw - for i in self.canvas.find_withtag("gridline"): - self.canvas.delete(i) - for i in self.canvas.find_withtag("rectangle"): - self.canvas.delete(i) - - self.canvas.draw_grid(width=width, height=height) - # lift anything that is drawn on the plot before - for tag in DRAW_OBJECT_TAGS: - for i in self.canvas.find_withtag(tag): - self.canvas.lift(i) - def click_apply(self): meter_per_pixel = float(self.scale.get()) / 100 + width, height = self.pixel_width.get(), self.pixel_height.get() self.canvas.meters_per_pixel = meter_per_pixel - self.redraw_grid() - # if there is a current wallpaper showing, redraw it based on current - # wallpaper options - wallpaper_tool = self.app.set_wallpaper - wallpaper = self.canvas.wallpaper - if wallpaper: - if self.canvas.adjust_to_dim.get() == 0: - scale_option = ScaleOption(self.canvas.scale_option.get()) - if scale_option == ScaleOption.UPPER_LEFT: - wallpaper_tool.upper_left(wallpaper) - elif scale_option == ScaleOption.CENTERED: - wallpaper_tool.center(wallpaper) - elif scale_option == ScaleOption.SCALED: - wallpaper_tool.scaled(wallpaper) - elif scale_option == ScaleOption.TILED: - logging.warning("tiled background not implemented") - elif self.canvas.adjust_to_dim.get() == 1: - wallpaper_tool.canvas_to_image_dimension(wallpaper) - wallpaper_tool.show_grid() + self.canvas.redraw_grid(width, height) + if self.canvas.wallpaper: + self.canvas.redraw() self.destroy() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 1bec481b..e5bab65b 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -2,6 +2,8 @@ import enum import logging import tkinter as tk +from PIL import ImageTk + from core.api.grpc import core_pb2 from coretk.canvasaction import CanvasAction from coretk.canvastooltip import CanvasTooltip @@ -21,6 +23,14 @@ class GraphMode(enum.Enum): OTHER = 4 +class ScaleOption(enum.Enum): + NONE = 0 + UPPER_LEFT = 1 + CENTERED = 2 + SCALED = 3 + TILED = 4 + + CORE_NODES = ["router"] CORE_WIRED_NETWORK_NODES = [] CORE_WIRELESS_NODE = ["wlan"] @@ -60,8 +70,8 @@ class CanvasGraph(tk.Canvas): self.wallpaper_drawn = None self.wallpaper_file = "" self.scale_option = tk.IntVar(value=1) - self.show_grid = tk.IntVar(value=1) - self.adjust_to_dim = tk.IntVar(value=0) + self.show_grid = tk.BooleanVar(value=True) + self.adjust_to_dim = tk.BooleanVar(value=False) def setup_menus(self): self.node_context = tk.Menu(self.master) @@ -132,11 +142,12 @@ class CanvasGraph(tk.Canvas): width=1, tags="rectangle", ) - self.tag_lower(self.grid) for i in range(0, width, 27): self.create_line(i, 0, i, height, dash=(2, 4), tags="gridline") for i in range(0, height, 27): self.create_line(0, i, width, i, dash=(2, 4), tags="gridline") + self.tag_lower("gridline") + self.tag_lower(self.grid) def draw_existing_component(self, session): """ @@ -228,9 +239,8 @@ class CanvasGraph(tk.Canvas): if2 ) - # lift the nodes so they on top of the links - for i in self.find_withtag("node"): - self.lift(i) + # raise the nodes so they on top of the links + self.tag_raise("node") def canvas_xy(self, event): """ @@ -426,6 +436,132 @@ class CanvasGraph(tk.Canvas): self.core.add_graph_node(self.core.session_id, node.id, x, y, node_name) return node + def width_and_height(self): + """ + retrieve canvas width and height in pixels + + :return: nothing + """ + grid = self.find_withtag("rectangle")[0] + x0, y0, x1, y1 = self.coords(grid) + canvas_w = abs(x0 - x1) + canvas_h = abs(y0 - y1) + return canvas_w, canvas_h + + def wallpaper_upper_left(self): + tk_img = ImageTk.PhotoImage(self.wallpaper) + # crop image if it is bigger than canvas + canvas_w, canvas_h = self.width_and_height() + cropx = img_w = tk_img.width() + cropy = img_h = tk_img.height() + if img_w > canvas_w: + cropx -= img_w - canvas_w + if img_h > canvas_h: + cropy -= img_h - canvas_h + cropped = self.wallpaper.crop((0, 0, cropx, cropy)) + cropped_tk = ImageTk.PhotoImage(cropped) + self.delete(self.wallpaper_id) + # place left corner of image to the left corner of the canvas + self.wallpaper_id = self.create_image( + (cropx / 2, cropy / 2), image=cropped_tk, tags="wallpaper" + ) + self.wallpaper_drawn = cropped_tk + + def wallpaper_center(self): + """ + place the image at the center of canvas + + :param Image img: image object + :return: nothing + """ + tk_img = ImageTk.PhotoImage(self.wallpaper) + canvas_w, canvas_h = self.width_and_height() + cropx = img_w = tk_img.width() + cropy = img_h = tk_img.height() + # dimension of the cropped image + if img_w > canvas_w: + cropx -= img_w - canvas_w + if img_h > canvas_h: + cropy -= img_h - canvas_h + x0 = (img_w - cropx) / 2 + y0 = (img_h - cropy) / 2 + x1 = x0 + cropx + y1 = y0 + cropy + cropped = self.wallpaper.crop((x0, y0, x1, y1)) + cropped_tk = ImageTk.PhotoImage(cropped) + # place the center of the image at the center of the canvas + self.delete(self.wallpaper_id) + self.wallpaper_id = self.create_image( + (canvas_w / 2, canvas_h / 2), image=cropped_tk, tags="wallpaper" + ) + self.wallpaper_drawn = cropped_tk + + def wallpaper_scaled(self): + """ + scale image based on canvas dimension + + :param Image img: image object + :return: nothing + """ + canvas_w, canvas_h = self.width_and_height() + image = Images.create(self.wallpaper_file, int(canvas_w), int(canvas_h)) + self.delete(self.wallpaper_id) + self.wallpaper_id = self.create_image( + (canvas_w / 2, canvas_h / 2), image=image, tags="wallpaper" + ) + self.wallpaper_drawn = image + + def resize_to_wallpaper(self): + image_tk = ImageTk.PhotoImage(self.wallpaper) + img_w = image_tk.width() + img_h = image_tk.height() + self.delete(self.wallpaper_id) + self.delete("rectangle") + self.delete("gridline") + self.draw_grid(img_w, img_h) + self.wallpaper_id = self.create_image((img_w / 2, img_h / 2), image=image_tk) + self.wallpaper_drawn = image_tk + + def redraw_grid(self, width, height): + """ + redraw grid with new dimension + + :return: nothing + """ + self.config(scrollregion=(0, 0, width + 200, height + 200)) + + # delete previous grid + self.delete("rectangle") + self.delete("gridline") + + # redraw + self.draw_grid(width=width, height=height) + + # hide/show grid + self.update_grid() + + def redraw(self): + if self.adjust_to_dim.get(): + self.resize_to_wallpaper() + else: + option = ScaleOption(self.scale_option.get()) + if option == ScaleOption.UPPER_LEFT: + self.wallpaper_upper_left() + elif option == ScaleOption.CENTERED: + self.wallpaper_center() + elif option == ScaleOption.SCALED: + self.wallpaper_scaled() + elif option == ScaleOption.TILED: + logging.warning("tiled background not implemented yet") + + def update_grid(self): + logging.info("updating grid show: %s", self.show_grid.get()) + if self.show_grid.get(): + self.itemconfig("gridline", state=tk.NORMAL) + self.tag_raise("gridline") + else: + self.itemconfig("gridline", state=tk.HIDDEN) + class CanvasEdge: """ @@ -469,8 +605,6 @@ class CanvasEdge: self.link_info = None self.throughput = None self.wired = is_wired - # TODO resolve this - # self.canvas.tag_lower(self.id) def complete(self, dst, x, y): self.dst = dst @@ -478,8 +612,8 @@ class CanvasEdge: x1, y1, _, _ = self.canvas.coords(self.id) self.canvas.coords(self.id, x1, y1, x, y) self.canvas.helper.draw_wireless_case(self.src, self.dst, self) - self.canvas.lift(self.src) - self.canvas.lift(self.dst) + self.canvas.tag_raise(self.src) + self.canvas.tag_raise(self.dst) def delete(self): self.canvas.delete(self.id) From 940281a3eef564aad4896c6ad6454e1931b9c184 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 14 Nov 2019 15:20:07 -0800 Subject: [PATCH 233/462] save work --- coretk/coretk/dialogs/serviceconfiguration.py | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index ceb3ece1..360e9561 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -40,6 +40,8 @@ class ServiceConfiguration(Dialog): self.validation_time_entry = None self.validation_mode_entry = None self.service_file_data = None + self.service_files = {} + self.temp_service_files = {} self.load() self.draw() @@ -59,6 +61,13 @@ class ServiceConfiguration(Dialog): self.shutdown_commands = [x for x in service_config.shutdown] self.validation_mode = service_config.validation_mode self.validation_time = service_config.validation_timer + self.service_files = { + x: self.app.core.get_node_service_file( + self.canvas_node.core_id, self.service_name, x + ) + for x in self.filenames + } + self.temp_service_files = self.service_files def draw(self): # self.columnconfigure(1, weight=1) @@ -105,17 +114,17 @@ class ServiceConfiguration(Dialog): frame = ttk.Frame(tab1) label = ttk.Label(frame, text="File name: ") label.grid(row=0, column=0) - self.filename_combobox = ttk.Combobox(frame, values=self.filenames) + self.filename_combobox = ttk.Combobox( + frame, values=self.filenames, state="readonly" + ) self.filename_combobox.grid(row=0, column=1) - if len(self.filenames) > 0: - self.filename_combobox.current(0) self.filename_combobox.bind( "<>", self.display_service_file_data ) - button = ttk.Button(frame, image=self.documentnew_img) + button = ttk.Button(frame, image=self.documentnew_img, state="disabled") button.bind("", self.add_filename) button.grid(row=0, column=2) - button = ttk.Button(frame, image=self.editdelete_img) + button = ttk.Button(frame, image=self.editdelete_img, state="disabled") button.bind("", self.delete_filename) button.grid(row=0, column=3) frame.grid(row=1, column=0, sticky="nsew") @@ -157,6 +166,10 @@ class ServiceConfiguration(Dialog): self.service_file_data = ScrolledText(tab1) self.service_file_data.grid(row=4, column=0, sticky="nsew") + if len(self.filenames) > 0: + self.filename_combobox.current(0) + self.service_file_data.delete(1.0, "end") + self.service_file_data.insert("end", self.service_files[self.filenames[0]]) # tab 2 label = ttk.Label( @@ -245,9 +258,13 @@ class ServiceConfiguration(Dialog): mode = "NON_BLOCKING" elif self.validation_mode == core_pb2.ServiceValidationMode.TIMER: mode = "TIMER" + print("the mode is", mode) self.validation_mode_entry = ttk.Entry( - frame, state="disabled", textvariable=tk.StringVar(value=mode) + frame, textvariable=tk.StringVar(value=mode) ) + self.validation_mode_entry.insert("end", mode) + print("get mode") + print(self.validation_mode_entry.get()) self.validation_mode_entry.grid(row=i, column=1) elif i == 2: label = ttk.Label(frame, text="Validation period:") @@ -279,6 +296,8 @@ class ServiceConfiguration(Dialog): frame.grid(row=3, column=0) def add_filename(self, 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() @@ -286,6 +305,8 @@ class ServiceConfiguration(Dialog): combobox["values"] += (filename,) def delete_filename(self, 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() @@ -357,12 +378,8 @@ class ServiceConfiguration(Dialog): def display_service_file_data(self, event): combobox = event.widget filename = combobox.get() - print(filename) - file_data = self.app.core.get_node_service_file( - self.canvas_node.core_id, self.service_name, filename - ) self.service_file_data.delete(1.0, "end") - self.service_file_data.insert("end", file_data) + self.service_file_data.insert("end", self.service_files[filename]) def click_defaults(self): logging.info("not implemented") From fa76bbf01b55bc9a1e28c4e3fe098e288600cf9c Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 14 Nov 2019 21:39:44 -0800 Subject: [PATCH 234/462] removed unused code in graph and coreclient --- coretk/coretk/coreclient.py | 17 ----------------- coretk/coretk/graph.py | 14 -------------- 2 files changed, 31 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 0d562f8a..2bf047d7 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -372,23 +372,6 @@ class CoreClient: response = self.client.stop_session(session_id=self.session_id) logging.debug("coregrpc.py Stop session, result: %s", response.result) - # # TODO no need, might get rid of this - # def add_link(self, id1, id2, type1, type2, edge): - # """ - # Grpc client request add link - # - # :param int session_id: session id - # :param int id1: node 1 core id - # :param core_pb2.NodeType type1: node 1 core node type - # :param int id2: node 2 core id - # :param core_pb2.NodeType type2: node 2 core node type - # :return: nothing - # """ - # if1 = self.create_interface(type1, edge.interface_1) - # if2 = self.create_interface(type2, edge.interface_2) - # response = self.client.add_link(self.session_id, id1, id2, if1, if2) - # logging.info("created link: %s", response) - def launch_terminal(self, node_id): response = self.client.get_node_terminal(self.session_id, node_id) logging.info("get terminal %s", response.terminal) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index e5bab65b..f7c45e63 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -31,12 +31,6 @@ class ScaleOption(enum.Enum): TILED = 4 -CORE_NODES = ["router"] -CORE_WIRED_NETWORK_NODES = [] -CORE_WIRELESS_NODE = ["wlan"] -CORE_EMANE = ["emane"] - - class CanvasGraph(tk.Canvas): def __init__(self, master, core, cnf=None, **kwargs): if cnf is None: @@ -670,14 +664,6 @@ class CanvasNode: self.canvas.core.launch_terminal(node_id) else: self.canvas.canvas_action.display_configuration(self) - # if self.node_type in CORE_NODES: - # self.canvas.canvas_action.node_to_show_config = self - # self.canvas.canvas_action.display_node_configuration() - # elif self.node_type in CORE_WIRED_NETWORK_NODES: - # return - # elif self.node_type in CORE_WIRELESS_NODE: - # return - # elif self def update_coords(self): self.x_coord, self.y_coord = self.canvas.coords(self.id) From 6d38058887b9f8d9af47850d6fb1f8c019766c64 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 15 Nov 2019 10:22:30 -0800 Subject: [PATCH 235/462] update node creation and storage to leverage protobufs directly --- coretk/coretk/canvasaction.py | 2 - coretk/coretk/coreclient.py | 343 ++++++++-------------------- coretk/coretk/coretocanvas.py | 34 --- coretk/coretk/dialogs/nodeconfig.py | 2 +- coretk/coretk/graph.py | 154 ++++++------- coretk/coretk/graph_helper.py | 110 +-------- coretk/coretk/images.py | 22 +- coretk/coretk/linkinfo.py | 3 +- coretk/coretk/toolbar.py | 36 +-- coretk/coretk/wirelessconnection.py | 26 +-- 10 files changed, 216 insertions(+), 516 deletions(-) delete mode 100644 coretk/coretk/coretocanvas.py diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py index decf43f3..8f217833 100644 --- a/coretk/coretk/canvasaction.py +++ b/coretk/coretk/canvasaction.py @@ -36,8 +36,6 @@ class CanvasAction: self.node_to_show_config = None def display_wlan_configuration(self, canvas_node): - - # print(self.canvas.grpc_manager.wlanconfig_management.configurations) wlan_config = self.master.core.wlanconfig_management.configurations[ canvas_node.core_id ] diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 2bf047d7..d80f6e5c 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -5,7 +5,6 @@ import logging import os from core.api.grpc import client, core_pb2 -from coretk.coretocanvas import CoreToCanvasMapping from coretk.dialogs.sessions import SessionsDialog from coretk.emaneodelnodeconfig import EmaneModelNodeConfig from coretk.images import NODE_WIDTH, Images @@ -29,28 +28,6 @@ OBSERVERS = { } -class Node: - def __init__(self, session_id, node_id, node_type, model, x, y, name): - """ - Create an instance of a node - - :param int session_id: session id - :param int node_id: node id - :param core_pb2.NodeType node_type: node type - :param int x: x coordinate - :param int y: coordinate - :param str name: node name - """ - self.session_id = session_id - self.node_id = node_id - self.type = node_type - self.x = x - self.y = y - self.model = model - self.name = name - self.interfaces = [] - - class Edge: def __init__(self, session_id, node_id_1, node_type_1, node_id_2, node_type_2): """ @@ -112,15 +89,15 @@ class CoreClient: self.read_config() # data for managing the current session + self.canvas_nodes = {} + self.interface_to_edge = {} self.state = None - self.nodes = {} self.edges = {} self.hooks = {} self.id = 1 self.reusable = [] self.preexisting = set() self.interfaces_manager = InterfaceManager() - self.core_mapping = CoreToCanvasMapping() self.wlanconfig_management = WlanNodeConfig() self.mobilityconfig_management = MobilityNodeConfig() self.emaneconfig_management = EmaneModelNodeConfig(app) @@ -179,7 +156,7 @@ class CoreClient: # clear session data self.reusable.clear() self.preexisting.clear() - self.nodes.clear() + self.canvas_nodes.clear() self.edges.clear() self.hooks.clear() self.wlanconfig_management.configurations.clear() @@ -203,14 +180,14 @@ class CoreClient: for node in session.nodes: if node.type == core_pb2.NodeType.WIRELESS_LAN: response = self.client.get_wlan_config(self.session_id, node.id) - logging.info("wlan config(%s): %s", node.id, response) + logging.debug("wlan config(%s): %s", node.id, response) node_config = response.config config = {x: node_config[x].value for x in node_config} self.wlanconfig_management.configurations[node.id] = config # get mobility configs response = self.client.get_mobility_configs(self.session_id) - logging.info("mobility configs: %s", response) + logging.debug("mobility configs: %s", response) for node_id in response.configs: node_config = response.configs[node_id].config config = {x: node_config[x].value for x in node_config} @@ -218,7 +195,7 @@ class CoreClient: # get emane config response = self.client.get_emane_config(self.session_id) - logging.info("emane config: %s", response) + logging.debug("emane config: %s", response) self.emane_config = response.config # get emane model config @@ -339,7 +316,7 @@ class CoreClient: logging.info("delete links %s", response) def start_session(self): - nodes = self.get_nodes_proto() + nodes = [x.core_node for x in self.canvas_nodes.values()] links = self.get_links_proto() wlan_configs = self.get_wlan_configs_proto() mobility_configs = self.get_mobility_configs_proto() @@ -411,7 +388,7 @@ class CoreClient: logging.debug("set node service %s", response) def create_nodes_and_links(self): - node_protos = self.get_nodes_proto() + node_protos = [x.core_node for x in self.canvas_nodes.values()] link_protos = self.get_links_proto() self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION) for node_proto in node_protos: @@ -437,17 +414,6 @@ class CoreClient: logging.debug("Close grpc") self.client.close() - def peek_id(self): - """ - Peek the next id to be used - - :return: nothing - """ - if len(self.reusable) == 0: - return self.id - else: - return self.reusable[0] - def get_id(self): """ Get the next node id as well as update id status and reusable ids @@ -465,106 +431,87 @@ class CoreClient: def is_model_node(self, name): return name in DEFAULT_NODES or name in self.custom_nodes - def add_graph_node(self, session_id, canvas_id, x, y, name): + def create_node(self, x, y, node_type, model): """ Add node, with information filled in, to grpc manager - :param int session_id: session id - :param int canvas_id: node's canvas id :param int x: x coord :param int y: y coord - :param str name: node type + :param core_pb2.NodeType node_type: node type + :param str model: node model :return: nothing """ - node_type = None - node_model = None - if name in NETWORK_NODES: - if name == "switch": - node_type = core_pb2.NodeType.SWITCH - elif name == "hub": - node_type = core_pb2.NodeType.HUB - elif name == "wlan": - node_type = core_pb2.NodeType.WIRELESS_LAN - elif name == "rj45": - node_type = core_pb2.NodeType.RJ45 - elif name == "emane": - node_type = core_pb2.NodeType.EMANE - elif name == "tunnel": - node_type = core_pb2.NodeType.TUNNEL - elif name == "emane": - node_type = core_pb2.NodeType.EMANE - elif self.is_model_node(name): - node_type = core_pb2.NodeType.DEFAULT - node_model = name - else: - logging.error("invalid node name: %s", name) - nid = self.get_id() - create_node = Node(session_id, nid, node_type, node_model, x, y, name) + node_id = self.get_id() + position = core_pb2.Position(x=x, y=y) + node = core_pb2.Node( + id=node_id, + type=node_type, + name=f"n{node_id}", + model=model, + position=position, + ) # set default configuration for wireless node - self.wlanconfig_management.set_default_config(node_type, nid) - self.mobilityconfig_management.set_default_configuration(node_type, nid) + self.wlanconfig_management.set_default_config(node_type, node_id) + self.mobilityconfig_management.set_default_configuration(node_type, node_id) # set default emane configuration for emane node if node_type == core_pb2.NodeType.EMANE: - self.emaneconfig_management.set_default_config(nid) + self.emaneconfig_management.set_default_config(node_id) # set default service configurations if node_type == core_pb2.NodeType.DEFAULT: self.serviceconfig_manager.node_default_services_configuration( - node_id=nid, node_model=node_model + node_id=node_id, node_model=model ) - self.nodes[canvas_id] = create_node - self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) logging.debug( - "Adding node to core.. session id: %s, coords: (%s, %s), name: %s", - session_id, + "adding node to core session: %s, coords: (%s, %s), name: %s", + self.session_id, x, y, - name, + node.name, ) + return node - def delete_wanted_graph_nodes(self, canvas_ids, tokens): + def delete_wanted_graph_nodes(self, node_ids, edge_tokens): """ remove the nodes selected by the user and anything related to that node such as link, configurations, interfaces - :param list(int) canvas_ids: list of canvas node ids + :param list[int] node_ids: list of nodes to delete + :param list edge_tokens: list of edges to delete :return: nothing """ - # keep reference to the core ids - core_node_ids = [self.nodes[x].node_id for x in canvas_ids] - node_interface_pairs = [] - # delete the nodes - for i in canvas_ids: + for node_id in node_ids: try: - n = self.nodes.pop(i) - self.reusable.append(n.node_id) + del self.canvas_nodes[node_id] + self.reusable.append(node_id) except KeyError: - logging.error("coreclient.py INVALID NODE CANVAS ID") - + logging.error("invalid canvas id: %s", node_id) self.reusable.sort() # delete the edges and interfaces - for i in tokens: + node_interface_pairs = [] + for i in edge_tokens: try: e = self.edges.pop(i) if e.interface_1 is not None: node_interface_pairs.append(tuple([e.id1, e.interface_1.id])) if e.interface_2 is not None: node_interface_pairs.append(tuple([e.id2, e.interface_2.id])) - except KeyError: logging.error("coreclient.py invalid edge token ") # delete global emane config if there no longer exist any emane cloud - if core_pb2.NodeType.EMANE not in [x.type for x in self.nodes.values()]: + # TODO: should not need to worry about this + node_types = [x.core_node.type for x in self.canvas_nodes.values()] + if core_pb2.NodeType.EMANE not in node_types: self.emane_config = None # delete any mobility configuration, wlan configuration - for i in core_node_ids: + for i in node_ids: if i in self.mobilityconfig_management.configurations: self.mobilityconfig_management.configurations.pop(i) if i in self.wlanconfig_management.configurations: @@ -574,69 +521,16 @@ class CoreClient: for i in node_interface_pairs: if i in self.emaneconfig_management.configurations: self.emaneconfig_management.configurations.pop(i) - for i in core_node_ids: + for i in node_ids: if tuple([i, None]) in self.emaneconfig_management.configurations: self.emaneconfig_management.configurations.pop(tuple([i, None])) - def add_preexisting_node(self, canvas_node, session_id, core_node, name): - """ - Add preexisting nodes to grpc manager - - :param str name: node_type - :param core_pb2.Node core_node: core node grpc message - :param coretk.graph.CanvasNode canvas_node: canvas node - :param int session_id: session id - :return: nothing - """ - - # update the next available id - core_id = core_node.id - if self.id is None or core_id >= self.id: - self.id = core_id + 1 - self.preexisting.add(core_id) - n = Node( - session_id, - core_id, - core_node.type, - core_node.model, - canvas_node.x_coord, - canvas_node.y_coord, - name, - ) - self.nodes[canvas_node.id] = n - - def update_node_location(self, canvas_id, new_x, new_y): - """ - update node - - :param int canvas_id: canvas id of that node - :param int new_x: new x coord - :param int new_y: new y coord - :return: nothing - """ - self.nodes[canvas_id].x = new_x - self.nodes[canvas_id].y = new_y - - def update_reusable_id(self): - """ - Update available id for reuse - - :return: nothing - """ - if len(self.preexisting) > 0: - for i in range(1, self.id): - if i not in self.preexisting: - self.reusable.append(i) - - self.preexisting.clear() - logging.debug("Next id: %s, Reusable: %s", self.id, self.reusable) - def create_interface(self, node_type, gui_interface): """ create a protobuf interface given the interface object stored by the programmer - :param core_bp2.NodeType type: node type - :param coretk.interface.Interface gui_interface: the programmer's interface object + :param core_bp2.NodeType node_type: node type + :param coretk.interface.Interface gui_interface: the programmer's interface :rtype: core_bp2.Interface :return: protobuf interface object """ @@ -653,120 +547,71 @@ class CoreClient: logging.debug("create interface: %s", interface) return interface - def create_edge_interface(self, edge, src_canvas_id, dst_canvas_id): - """ - Create the interface for the two end of an edge, add a copy to node's interfaces - - :param coretk.coreclient.Edge edge: edge to add interfaces to - :param int src_canvas_id: canvas id for the source node - :param int dst_canvas_id: canvas id for the destination node - :return: nothing - """ - src_interface = None - dst_interface = None - print("create interface") - self.interfaces_manager.new_subnet() - - src_node = self.nodes[src_canvas_id] - if self.is_model_node(src_node.model): - ifid = len(src_node.interfaces) - name = "eth" + str(ifid) - src_interface = Interface( + def create_edge_interface(self, canvas_node): + interface = None + core_node = canvas_node.core_node + if self.is_model_node(core_node.model): + ifid = len(canvas_node.interfaces) + name = f"eth{ifid}" + interface = Interface( name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) ) - self.nodes[src_canvas_id].interfaces.append(src_interface) + canvas_node.interfaces.append(interface) logging.debug( - "Create source interface 1... IP: %s, name: %s", - src_interface.ipv4, - src_interface.name, + "create node(%s) interface IP: %s, name: %s", + core_node.name, + interface.ipv4, + interface.name, ) + return interface - dst_node = self.nodes[dst_canvas_id] - if self.is_model_node(dst_node.model): - ifid = len(dst_node.interfaces) - name = "eth" + str(ifid) - dst_interface = Interface( - name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) - ) - self.nodes[dst_canvas_id].interfaces.append(dst_interface) - logging.debug( - "Create destination interface... IP: %s, name: %s", - dst_interface.ipv4, - dst_interface.name, - ) - - edge.interface_1 = src_interface - edge.interface_2 = dst_interface - return src_interface, dst_interface - - def add_edge(self, session_id, token, canvas_id_1, canvas_id_2): + def create_edge(self, token, canvas_node_one, canvas_node_two): """ Add an edge to grpc manager - :param int session_id: core session id :param tuple(int, int) token: edge's identification in the canvas - :param int canvas_id_1: canvas id of source node - :param int canvas_id_2: canvas_id of destination node + :param canvas_node_one: canvas node one + :param canvas_node_two: canvas node two :return: nothing """ - node_one = self.nodes[canvas_id_1] - node_two = self.nodes[canvas_id_2] - if canvas_id_1 in self.nodes and canvas_id_2 in self.nodes: - edge = Edge( - session_id, - node_one.node_id, - node_one.type, - node_two.node_id, - node_two.type, - ) - self.edges[token] = edge - src_interface, dst_interface = self.create_edge_interface( - edge, canvas_id_1, canvas_id_2 - ) - node_one_id = node_one.node_id - node_two_id = node_two.node_id + node_one = canvas_node_one.core_node + node_two = canvas_node_two.core_node - # provide a way to get an edge from a core node and an interface id - if src_interface is not None: - self.core_mapping.map_node_and_interface_to_canvas_edge( - node_one_id, src_interface.id, token + # create interfaces + self.interfaces_manager.new_subnet() + interface_one = self.create_edge_interface(canvas_node_one) + if interface_one is not None: + self.interface_to_edge[(node_one.id, interface_one.id)] = token + interface_two = self.create_edge_interface(canvas_node_two) + if interface_two is not None: + self.interface_to_edge[(node_two.id, interface_two.id)] = token + + # emane setup + if ( + node_one.type == core_pb2.NodeType.EMANE + and node_two.type == core_pb2.NodeType.DEFAULT + ): + if node_two.model == "mdr": + self.emaneconfig_management.set_default_for_mdr( + node_one.node_id, node_two.node_id, interface_two.id + ) + elif ( + node_two.type == core_pb2.NodeType.EMANE + and node_one.type == core_pb2.NodeType.DEFAULT + ): + if node_one.model == "mdr": + self.emaneconfig_management.set_default_for_mdr( + node_two.node_id, node_one.node_id, interface_one.id ) - if dst_interface is not None: - self.core_mapping.map_node_and_interface_to_canvas_edge( - node_two_id, dst_interface.id, token - ) - - if ( - node_one.type == core_pb2.NodeType.EMANE - and node_two.type == core_pb2.NodeType.DEFAULT - ): - if node_two.model == "mdr": - self.emaneconfig_management.set_default_for_mdr( - node_one.node_id, node_two.node_id, dst_interface.id - ) - elif ( - node_two.type == core_pb2.NodeType.EMANE - and node_one.type == core_pb2.NodeType.DEFAULT - ): - if node_one.model == "mdr": - self.emaneconfig_management.set_default_for_mdr( - node_two.node_id, node_one.node_id, src_interface.id - ) - - else: - logging.error("grpcmanagement.py INVALID CANVAS NODE ID") - - def get_nodes_proto(self): - nodes = [] - for node in self.nodes.values(): - pos = core_pb2.Position(x=int(node.x), y=int(node.y)) - proto_node = core_pb2.Node( - id=node.node_id, type=node.type, position=pos, model=node.model - ) - nodes.append(proto_node) - return nodes + edge = Edge( + self.session_id, node_one.id, node_one.type, node_two.id, node_two.type + ) + edge.interface_1 = interface_one + edge.interface_2 = interface_two + self.edges[token] = edge + return edge def get_links_proto(self): links = [] diff --git a/coretk/coretk/coretocanvas.py b/coretk/coretk/coretocanvas.py deleted file mode 100644 index 6e767db3..00000000 --- a/coretk/coretk/coretocanvas.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -provide mapping from core to canvas -""" -import logging - - -class CoreToCanvasMapping: - def __init__(self): - self.core_id_to_canvas_id = {} - self.core_node_and_interface_to_canvas_edge = {} - - def map_node_and_interface_to_canvas_edge(self, nid, iid, edge_token): - self.core_node_and_interface_to_canvas_edge[tuple([nid, iid])] = edge_token - - def get_token_from_node_and_interface(self, nid, iid): - key = tuple([nid, iid]) - if key in self.core_node_and_interface_to_canvas_edge: - return self.core_node_and_interface_to_canvas_edge[key] - else: - logging.error("invalid key") - return None - - def map_core_id_to_canvas_id(self, core_nid, canvas_nid): - if core_nid not in self.core_id_to_canvas_id: - self.core_id_to_canvas_id[core_nid] = canvas_nid - else: - logging.debug("key already existed") - - def get_canvas_id_from_core_id(self, core_id): - if core_id in self.core_id_to_canvas_id: - return self.core_id_to_canvas_id[core_id] - else: - logging.debug("invalid key") - return None diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 48233762..14e9de03 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -21,7 +21,7 @@ class NodeConfigDialog(Dialog): self.image = canvas_node.image self.image_button = None self.name = tk.StringVar(value=canvas_node.name) - self.type = tk.StringVar(value=canvas_node.node_type) + self.type = tk.StringVar(value=canvas_node.core_node.model) self.server = tk.StringVar() self.draw() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index f7c45e63..1fcafac1 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -39,7 +39,8 @@ class CanvasGraph(tk.Canvas): super().__init__(master, cnf, **kwargs) self.mode = GraphMode.SELECT self.draw_node_image = None - self.draw_node_name = None + self.draw_node_type = None + self.draw_node_model = None self.selected = None self.node_context = None self.nodes = {} @@ -97,13 +98,14 @@ class CanvasGraph(tk.Canvas): # set the private variables to default value self.mode = GraphMode.SELECT self.draw_node_image = None - self.draw_node_name = None + self.draw_node_type = None + self.draw_node_model = None self.selected = None self.node_context = None self.nodes.clear() self.edges.clear() self.drawing_edge = None - self.draw_existing_component(session) + self.draw_session(session) def setup_bindings(self): """ @@ -143,61 +145,46 @@ class CanvasGraph(tk.Canvas): self.tag_lower("gridline") self.tag_lower(self.grid) - def draw_existing_component(self, session): + def draw_session(self, session): """ - Draw existing node and update the information in grpc manager to match + Draw existing session. :return: nothing """ - core_id_to_canvas_id = {} - # redraw existing nodes - for node in session.nodes: + # draw existing nodes + for core_node in session.nodes: # peer to peer node is not drawn on the GUI - if node.type != core_pb2.NodeType.PEER_TO_PEER: - # draw nodes on the canvas - image, name = Images.node_icon(node.type, node.model) - n = CanvasNode( - node.position.x, node.position.y, image, name, self.master, node.id - ) - self.nodes[n.id] = n - core_id_to_canvas_id[node.id] = n.id + if core_node.type == core_pb2.NodeType.PEER_TO_PEER: + continue - # store the node in grpc manager - self.core.add_preexisting_node(n, session.id, node, name) + # draw nodes on the canvas + image = Images.node_icon(core_node.type, core_node.model) + position = core_node.position + node = CanvasNode(position.x, position.y, image, self.master, core_node) + self.nodes[node.id] = node + self.core.canvas_nodes[core_node.id] = node # draw existing links for link in session.links: - n1 = self.nodes[core_id_to_canvas_id[link.node_one_id]] - n2 = self.nodes[core_id_to_canvas_id[link.node_two_id]] - if link.type == core_pb2.LinkType.WIRED: - e = CanvasEdge( - n1.x_coord, - n1.y_coord, - n2.x_coord, - n2.y_coord, - n1.id, - self, - is_wired=True, - ) - elif link.type == core_pb2.LinkType.WIRELESS: - e = CanvasEdge( - n1.x_coord, - n1.y_coord, - n2.x_coord, - n2.y_coord, - n1.id, - self, - is_wired=False, - ) - edge_token = tuple(sorted((n1.id, n2.id))) - e.token = edge_token - e.dst = n2.id - n1.edges.add(e) - n2.edges.add(e) - self.edges[e.token] = e - self.core.add_edge(session.id, e.token, n1.id, n2.id) - - self.helper.redraw_antenna(link, n1, n2) + canvas_node_one = self.core.canvas_nodes[link.node_one_id] + canvas_node_two = self.core.canvas_nodes[link.node_two_id] + is_wired = link.type == core_pb2.LinkType.WIRED + edge = CanvasEdge( + canvas_node_one.x_coord, + canvas_node_one.y_coord, + canvas_node_two.x_coord, + canvas_node_two.y_coord, + canvas_node_one.id, + self, + is_wired=is_wired, + ) + edge.token = tuple(sorted((canvas_node_one.id, canvas_node_two.id))) + edge.dst = canvas_node_two.id + canvas_node_one.edges.add(edge) + canvas_node_two.edges.add(edge) + self.edges[edge.token] = edge + self.core.create_edge(edge.token, canvas_node_one, canvas_node_two) + self.helper.redraw_antenna(link, canvas_node_one, canvas_node_two) # TODO add back the link info to grpc manager also redraw grpc_if1 = link.interface_one @@ -212,9 +199,9 @@ class CanvasGraph(tk.Canvas): if grpc_if2 is not None: ip4_dst = grpc_if2.ip4 ip6_dst = grpc_if2.ip6 - e.link_info = LinkInfo( + edge.link_info = LinkInfo( canvas=self, - edge=e, + edge=edge, ip4_src=ip4_src, ip6_src=ip6_src, ip4_dst=ip4_dst, @@ -224,14 +211,10 @@ class CanvasGraph(tk.Canvas): # TODO will include throughput and ipv6 in the future if1 = Interface(grpc_if1.name, grpc_if1.ip4, ifid=grpc_if1.id) if2 = Interface(grpc_if2.name, grpc_if2.ip4, ifid=grpc_if2.id) - self.core.edges[e.token].interface_1 = if1 - self.core.edges[e.token].interface_2 = if2 - self.core.nodes[core_id_to_canvas_id[link.node_one_id]].interfaces.append( - if1 - ) - self.core.nodes[core_id_to_canvas_id[link.node_two_id]].interfaces.append( - if2 - ) + self.core.edges[edge.token].interface_1 = if1 + self.core.edges[edge.token].interface_2 = if2 + canvas_node_one.interfaces.append(if1) + canvas_node_two.interfaces.append(if2) # raise the nodes so they on top of the links self.tag_raise("node") @@ -290,7 +273,13 @@ class CanvasGraph(tk.Canvas): self.handle_edge_release(event) elif self.mode == GraphMode.NODE: x, y = self.canvas_xy(event) - self.add_node(x, y, self.draw_node_image, self.draw_node_name) + self.add_node( + x, + y, + self.draw_node_image, + self.draw_node_type, + self.draw_node_model, + ) elif self.mode == GraphMode.PICKNODE: self.mode = GraphMode.NODE @@ -313,6 +302,7 @@ class CanvasGraph(tk.Canvas): # edge dst is same as src, delete edge if edge.src == self.selected: edge.delete() + return # set dst node and snap edge to center x, y = self.coords(self.selected) @@ -326,14 +316,11 @@ class CanvasGraph(tk.Canvas): node_src.edges.add(edge) node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) - - self.core.add_edge( - self.core.session_id, edge.token, node_src.id, node_dst.id - ) + core_edge = self.core.create_edge(edge.token, node_src, node_dst) # draw link info on the edge - if1 = self.core.edges[edge.token].interface_1 - if2 = self.core.edges[edge.token].interface_2 + if1 = core_edge.interface_1 + if2 = core_edge.interface_2 ip4_and_prefix_1 = None ip4_and_prefix_2 = None if if1 is not None: @@ -404,8 +391,10 @@ class CanvasGraph(tk.Canvas): ) # delete nodes and link info stored in CanvasGraph object + node_ids = [] for nid in to_delete_nodes: - self.nodes.pop(nid) + canvas_node = self.nodes.pop(nid) + node_ids.append(canvas_node.core_node.id) for token in to_delete_edge_tokens: self.edges.pop(token) @@ -419,15 +408,16 @@ class CanvasGraph(tk.Canvas): self.nodes[nid].edges.remove(edge) # delete the related data from core - self.core.delete_wanted_graph_nodes(to_delete_nodes, to_delete_edge_tokens) + self.core.delete_wanted_graph_nodes(node_ids, to_delete_edge_tokens) - def add_node(self, x, y, image, node_name): + def add_node(self, x, y, image, node_type, model): plot_id = self.find_all()[0] logging.info("add node event: %s - %s", plot_id, self.selected) if self.selected == plot_id: - node = CanvasNode(x, y, image, node_name, self.master, self.core.peek_id()) + core_node = self.core.create_node(int(x), int(y), node_type, model) + node = CanvasNode(x, y, image, self.master, core_node) + self.core.canvas_nodes[core_node.id] = node self.nodes[node.id] = node - self.core.add_graph_node(self.core.session_id, node.id, x, y, node_name) return node def width_and_height(self): @@ -494,7 +484,6 @@ class CanvasGraph(tk.Canvas): """ scale image based on canvas dimension - :param Image img: image object :return: nothing """ canvas_w, canvas_h = self.width_and_height() @@ -614,18 +603,17 @@ class CanvasEdge: class CanvasNode: - def __init__(self, x, y, image, node_type, app, core_id): + def __init__(self, x, y, image, app, core_node): self.image = image - self.node_type = node_type self.app = app self.canvas = app.canvas self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags="node" ) - self.core_id = core_id + self.core_node = core_node + self.name = core_node.name self.x_coord = x self.y_coord = y - self.name = f"N{self.core_id}" self.text_id = self.canvas.create_text( x, y + 20, text=self.name, tags="nodename" ) @@ -641,6 +629,7 @@ class CanvasNode: self.canvas.tag_bind(self.id, "", self.on_leave) self.edges = set() + self.interfaces = [] self.wlans = [] self.moving = None @@ -648,7 +637,7 @@ class CanvasNode: if self.app.core.is_runtime() and self.app.core.observer: self.tooltip.text.set("waiting...") self.tooltip.on_enter(event) - output = self.app.core.run(self.core_id) + output = self.app.core.run(self.core_node.id) self.tooltip.text.set(output) def on_leave(self, event): @@ -658,15 +647,15 @@ class CanvasNode: print("click") def double_click(self, event): - node_id = self.canvas.core.nodes[self.id].node_id - state = self.canvas.core.get_session_state() - if state == core_pb2.SessionState.RUNTIME: - self.canvas.core.launch_terminal(node_id) + if self.app.core.is_runtime(): + self.canvas.core.launch_terminal(self.core_node.id) else: self.canvas.canvas_action.display_configuration(self) def update_coords(self): self.x_coord, self.y_coord = self.canvas.coords(self.id) + self.core_node.position.x = int(self.x_coord) + self.core_node.position.y = int(self.y_coord) def click_press(self, event): logging.debug(f"node click press {self.name}: {event}") @@ -677,7 +666,6 @@ class CanvasNode: def click_release(self, event): logging.debug(f"node click release {self.name}: {event}") self.update_coords() - self.canvas.core.update_node_location(self.id, self.x_coord, self.y_coord) self.moving = None def motion(self, event): @@ -697,7 +685,7 @@ class CanvasNode: new_x, new_y = self.canvas.coords(self.id) if self.canvas.core.get_session_state() == core_pb2.SessionState.RUNTIME: - self.canvas.core.edit_node(self.core_id, int(new_x), int(new_y)) + self.canvas.core.edit_node(self.core_node.id, int(new_x), int(new_y)) for edge in self.edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py index c1f59815..476c0182 100644 --- a/coretk/coretk/graph_helper.py +++ b/coretk/coretk/graph_helper.py @@ -29,29 +29,31 @@ class GraphHelper: self.canvas.delete(i) def draw_wireless_case(self, src_id, dst_id, edge): - src_node_name = self.canvas.nodes[src_id].node_type - dst_node_name = self.canvas.nodes[dst_id].node_type - - if src_node_name == "wlan" or dst_node_name == "wlan": + src_node_type = self.canvas.nodes[src_id].core_node.type + dst_node_type = self.canvas.nodes[dst_id].core_node.type + is_src_wlan = src_node_type == core_pb2.NodeType.WIRELESS_LAN + is_dst_wlan = dst_node_type == core_pb2.NodeType.WIRELESS_LAN + if is_src_wlan or is_dst_wlan: self.canvas.itemconfig(edge.id, state=tk.HIDDEN) edge.wired = False if edge.token not in self.canvas.edges: - if src_node_name == "wlan" and dst_node_name == "wlan": + if is_src_wlan and is_dst_wlan: self.canvas.nodes[src_id].antenna_draw.add_antenna() - elif src_node_name == "wlan": + elif is_src_wlan: self.canvas.nodes[dst_id].antenna_draw.add_antenna() else: self.canvas.nodes[src_id].antenna_draw.add_antenna() - edge.wired = True def redraw_antenna(self, link, node_one, node_two): + is_node_one_wlan = node_one.core_node.type == core_pb2.NodeType.WIRELESS_LAN + is_node_two_wlan = node_two.core_node.type == core_pb2.NodeType.WIRELESS_LAN if link.type == core_pb2.LinkType.WIRELESS: - if node_one.node_type == "wlan" and node_two.node_type == "wlan": + if is_node_one_wlan and is_node_two_wlan: node_one.antenna_draw.add_antenna() - elif node_one.node_type == "wlan" and node_two.node_type != "wlan": + elif is_node_one_wlan and not is_node_two_wlan: node_two.antenna_draw.add_antenna() - elif node_one.node_type != "wlan" and node_two.node_type == "wlan": + elif not is_node_one_wlan and is_node_two_wlan: node_one.antenna_draw.add_antenna() else: logging.error( @@ -122,91 +124,3 @@ class WlanAntennaManager: """ for i in self.antennas: self.canvas.delete(i) - - -# class WlanConnection: -# def __init__(self, canvas, grpc): -# """ -# create in -# :param canvas: -# """ -# self.canvas = canvas -# self.core_grpc = grpc -# self.throughput_on = False -# self.map_node_link = {} -# self.links = [] -# -# def wireless_nodes(self): -# """ -# retrieve all the wireless clouds in the canvas -# -# :return: list(coretk.graph.CanvasNode) -# """ -# wireless_nodes = [] -# for n in self.canvas.nodes.values(): -# if n.node_type == "wlan": -# wireless_nodes.append(n) -# return wireless_nodes -# -# def draw_wireless_link(self, src, dst): -# """ -# draw a line between 2 nodes that are connected to the same wireless cloud -# -# :param coretk.graph.CanvasNode src: source node -# :param coretk.graph.CanvasNode dst: destination node -# :return: nothing -# """ -# cid = self.canvas.create_line(src.x_coord, src.y_coord, dst.x_coord, dst.y_coord, tags="wlanconnection") -# if src.id not in self.map_node_link: -# self.map_node_link[src.id] = [] -# if dst.id not in self.map_node_link: -# self.map_node_link[dst.id] = [] -# self.map_node_link[src.id].append(cid) -# self.map_node_link[dst.id].append(cid) -# self.links.append(cid) -# -# def subnet_wireless_connection(self, wlan_node): -# """ -# retrieve all the non-wireless nodes connected to wireless_node and create line (represent wireless connection) between each pair of nodes -# :param coretk.grpah.CanvasNode wlan_node: wireless node -# -# :return: nothing -# """ -# non_wlan_nodes = [] -# for e in wlan_node.edges: -# src = self.canvas.nodes[e.src] -# dst = self.canvas.nodes[e.dst] -# if src.node_type == "wlan" and dst.node_type != "wlan": -# non_wlan_nodes.append(dst) -# elif src.node_type != "wlan" and dst.node_type == "wlan": -# non_wlan_nodes.append(src) -# -# size = len(non_wlan_nodes) -# for i in range(size): -# for j in range(i+1, size): -# self.draw_wireless_link(non_wlan_nodes[i], non_wlan_nodes[j]) -# -# def session_wireless_connection(self): -# """ -# draw all the wireless connection in the canvas -# -# :return: nothing -# """ -# wlan_nodes = self.wireless_nodes() -# for n in wlan_nodes: -# self.subnet_wireless_connection(n) -# -# def show_links(self): -# """ -# show all the links -# """ -# for l in self.links: -# self.canvas.itemconfig(l, state=tk.NORMAL) -# -# def hide_links(self): -# """ -# hide all the links -# :return: -# """ -# for l in self.links: -# self.canvas.itemconfig(l, state=tk.HIDDEN) diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 7335e14e..6ce4732f 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -31,7 +31,7 @@ class Images: return cls.create(file_path, width, height) @classmethod - def get_custom(cls, name, width, height): + def get_custom(cls, name, width, height=None): file_path = cls.images[name] return cls.create(file_path, width, height) @@ -41,52 +41,38 @@ class Images: Retrieve image based on type and model :param core_pb2.NodeType node_type: core node type :param string node_model: the node model - - :rtype: tuple(PhotoImage, str) - :return: the matching image and its name + :return: core node icon + :rtype: PhotoImage """ image_enum = ImageEnum.ROUTER - name = "unknown" if node_type == core_pb2.NodeType.SWITCH: image_enum = ImageEnum.SWITCH - name = "switch" elif node_type == core_pb2.NodeType.HUB: image_enum = ImageEnum.HUB - name = "hub" elif node_type == core_pb2.NodeType.WIRELESS_LAN: image_enum = ImageEnum.WLAN - name = "wlan" elif node_type == core_pb2.NodeType.EMANE: image_enum = ImageEnum.EMANE - name = "emane" elif node_type == core_pb2.NodeType.RJ45: image_enum = ImageEnum.RJ45 - name = "rj45" elif node_type == core_pb2.NodeType.TUNNEL: image_enum = ImageEnum.TUNNEL - name = "tunnel" elif node_type == core_pb2.NodeType.DEFAULT: if node_model == "router": image_enum = ImageEnum.ROUTER - name = "router" elif node_model == "host": image_enum = ImageEnum.HOST - name = "host" elif node_model == "PC": image_enum = ImageEnum.PC - name = "PC" elif node_model == "mdr": image_enum = ImageEnum.MDR - name = "mdr" elif node_model == "prouter": image_enum = ImageEnum.PROUTER - name = "prouter" else: logging.error("invalid node model: %s", node_model) else: logging.error("invalid node type: %s", node_type) - - return Images.get(image_enum, NODE_WIDTH), name + return Images.get(image_enum, NODE_WIDTH) class ImageEnum(Enum): diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index 916e8dfb..29430deb 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -129,8 +129,7 @@ class Throughput: nid = t.node_id iid = t.interface_id tp = t.throughput - # token = self.grpc_manager.node_id_and_interface_to_edge_token[nid, iid] - token = self.core.core_mapping.get_token_from_node_and_interface(nid, iid) + token = self.core.interface_to_edge[(nid, iid)] print(token) edge_id = self.canvas.edges[token].id diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 89393c3e..a049c321 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -3,6 +3,7 @@ import tkinter as tk from functools import partial from tkinter import ttk +from core.api.grpc import core_pb2 from coretk.dialogs.customnodes import CustomNodesDialog from coretk.graph import GraphMode from coretk.images import ImageEnum, Images @@ -136,10 +137,16 @@ class Toolbar(ttk.Frame): (ImageEnum.PROUTER, "prouter"), ] # draw default nodes - for image_enum, tooltip in nodes: + for image_enum, model in nodes: image = icon(image_enum) - func = partial(self.update_button, self.node_button, image, tooltip) - self.create_picker_button(image, func, self.node_picker, tooltip) + func = partial( + self.update_button, + self.node_button, + image, + core_pb2.NodeType.DEFAULT, + model, + ) + self.create_picker_button(image, func, self.node_picker, model) # draw custom nodes for name in sorted(self.app.core.custom_nodes): custom_node = self.app.core.custom_nodes[name] @@ -216,14 +223,15 @@ class Toolbar(ttk.Frame): dialog = CustomNodesDialog(self.app, self.app) dialog.show() - def update_button(self, button, image, name): - logging.info("update button(%s): %s", button, name) + def update_button(self, button, image, node_type, model=None): + logging.info("update button(%s): %s", button, node_type) self.hide_pickers() button.configure(image=image) button.image = image self.app.canvas.mode = GraphMode.NODE self.app.canvas.draw_node_image = image - self.app.canvas.draw_node_name = name + self.app.canvas.draw_node_type = node_type + self.app.canvas.draw_node_model = model def hide_pickers(self): logging.info("hiding pickers") @@ -260,18 +268,18 @@ class Toolbar(ttk.Frame): self.hide_pickers() self.network_picker = ttk.Frame(self.master) nodes = [ - (ImageEnum.HUB, "hub", "ethernet hub"), - (ImageEnum.SWITCH, "switch", "ethernet switch"), - (ImageEnum.WLAN, "wlan", "wireless LAN"), - (ImageEnum.EMANE, "emane", "EMANE"), - (ImageEnum.RJ45, "rj45", "rj45 physical interface tool"), - (ImageEnum.TUNNEL, "tunnel", "tunnel tool"), + (ImageEnum.HUB, core_pb2.NodeType.HUB, "ethernet hub"), + (ImageEnum.SWITCH, core_pb2.NodeType.SWITCH, "ethernet switch"), + (ImageEnum.WLAN, core_pb2.NodeType.WIRELESS_LAN, "wireless LAN"), + (ImageEnum.EMANE, core_pb2.NodeType.EMANE, "EMANE"), + (ImageEnum.RJ45, core_pb2.NodeType.RJ45, "rj45 physical interface tool"), + (ImageEnum.TUNNEL, core_pb2.NodeType.TUNNEL, "tunnel tool"), ] - for image_enum, name, tooltip in nodes: + for image_enum, node_type, tooltip in nodes: image = icon(image_enum) self.create_picker_button( image, - partial(self.update_button, self.network_button, image, name), + partial(self.update_button, self.network_button, image, node_type), self.network_picker, tooltip, ) diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py index d1e17bc5..b864ce38 100644 --- a/coretk/coretk/wirelessconnection.py +++ b/coretk/coretk/wirelessconnection.py @@ -7,35 +7,31 @@ from core.api.grpc import core_pb2 class WirelessConnection: def __init__(self, canvas, core): self.canvas = canvas - self.core_mapping = core.core_mapping + self.core = core # map a (node_one_id, node_two_id) to a wlan canvas id self.map = {} def add_wlan_connection(self, node_one_id, node_two_id): - canvas_id_one = self.core_mapping.get_canvas_id_from_core_id(node_one_id) - canvas_id_two = self.core_mapping.get_canvas_id_from_core_id(node_two_id) + canvas_node_one = self.core.canvas_nodes[node_one_id] + canvas_node_two = self.core.canvas_nodes[node_two_id] key = tuple(sorted((node_one_id, node_two_id))) - if key not in self.map: - x1, y1 = self.canvas.coords(canvas_id_one) - x2, y2 = self.canvas.coords(canvas_id_two) + x1, y1 = self.canvas.coords(canvas_node_one.id) + x2, y2 = self.canvas.coords(canvas_node_two.id) wlan_canvas_id = self.canvas.create_line( x1, y1, x2, y2, fill="#009933", tags="wlan", width=1.5 ) self.map[key] = wlan_canvas_id - self.canvas.nodes[canvas_id_one].wlans.append(wlan_canvas_id) - self.canvas.nodes[canvas_id_two].wlans.append(wlan_canvas_id) + canvas_node_one.wlans.append(wlan_canvas_id) + canvas_node_two.wlans.append(wlan_canvas_id) def delete_wlan_connection(self, node_one_id, node_two_id): - canvas_id_one = self.core_mapping.get_canvas_id_from_core_id(node_one_id) - canvas_id_two = self.core_mapping.get_canvas_id_from_core_id(node_two_id) - + canvas_node_one = self.core.canvas_nodes[node_one_id] + canvas_node_two = self.core.canvas_nodes[node_two_id] key = tuple(sorted((node_one_id, node_two_id))) wlan_canvas_id = self.map[key] - - self.canvas.nodes[canvas_id_one].wlans.remove(wlan_canvas_id) - self.canvas.nodes[canvas_id_two].wlans.remove(wlan_canvas_id) - + canvas_node_one.wlans.remove(wlan_canvas_id) + canvas_node_two.wlans.remove(wlan_canvas_id) self.canvas.delete(wlan_canvas_id) self.map.pop(key, None) From 814d35d7dda1e2a774ab45663d4a503dec93d148 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 15 Nov 2019 12:39:08 -0800 Subject: [PATCH 236/462] removed coreclient edge and updated code to save and use link protobuf data structures --- coretk/coretk/canvasaction.py | 15 +--- coretk/coretk/coreclient.py | 119 +++++++++------------------- coretk/coretk/graph.py | 46 +++++------ coretk/coretk/interface.py | 50 ------------ coretk/coretk/linkinfo.py | 2 - coretk/coretk/toolbar.py | 1 + coretk/coretk/wirelessconnection.py | 2 +- 7 files changed, 63 insertions(+), 172 deletions(-) diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py index 8f217833..5b9c08c3 100644 --- a/coretk/coretk/canvasaction.py +++ b/coretk/coretk/canvasaction.py @@ -6,13 +6,6 @@ from coretk.dialogs.emaneconfig import EmaneConfiguration from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog -# TODO, finish classifying node types -NODE_TO_TYPE = { - "router": core_pb2.NodeType.DEFAULT, - "wlan": core_pb2.NodeType.WIRELESS_LAN, - "emane": core_pb2.NodeType.EMANE, -} - class CanvasAction: def __init__(self, master, canvas): @@ -21,13 +14,13 @@ class CanvasAction: self.node_to_show_config = None def display_configuration(self, canvas_node): - pb_type = NODE_TO_TYPE[canvas_node.node_type] + node_type = canvas_node.core_node.type self.node_to_show_config = canvas_node - if pb_type == core_pb2.NodeType.DEFAULT: + if node_type == core_pb2.NodeType.DEFAULT: self.display_node_configuration() - elif pb_type == core_pb2.NodeType.WIRELESS_LAN: + elif node_type == core_pb2.NodeType.WIRELESS_LAN: self.display_wlan_configuration(canvas_node) - elif pb_type == core_pb2.NodeType.EMANE: + elif node_type == core_pb2.NodeType.EMANE: self.display_emane_configuration() def display_node_configuration(self): diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index d80f6e5c..5dd0a621 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -8,7 +8,7 @@ from core.api.grpc import client, core_pb2 from coretk.dialogs.sessions import SessionsDialog from coretk.emaneodelnodeconfig import EmaneModelNodeConfig from coretk.images import NODE_WIDTH, Images -from coretk.interface import Interface, InterfaceManager +from coretk.interface import InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.servicenodeconfig import ServiceNodeConfig from coretk.wlannodeconfig import WlanNodeConfig @@ -28,25 +28,6 @@ OBSERVERS = { } -class Edge: - def __init__(self, session_id, node_id_1, node_type_1, node_id_2, node_type_2): - """ - Create an instance of an edge - :param int session_id: session id - :param int node_id_1: node 1 id - :param int node_type_1: node 1 type - :param core_pb2.NodeType node_id_2: node 2 id - :param core_pb2.NodeType node_type_2: node 2 type - """ - self.session_id = session_id - self.id1 = node_id_1 - self.id2 = node_id_2 - self.type1 = node_type_1 - self.type2 = node_type_2 - self.interface_1 = None - self.interface_2 = None - - class CoreServer: def __init__(self, name, address, port): self.name = name @@ -92,7 +73,7 @@ class CoreClient: self.canvas_nodes = {} self.interface_to_edge = {} self.state = None - self.edges = {} + self.links = {} self.hooks = {} self.id = 1 self.reusable = [] @@ -157,7 +138,7 @@ class CoreClient: self.reusable.clear() self.preexisting.clear() self.canvas_nodes.clear() - self.edges.clear() + self.links.clear() self.hooks.clear() self.wlanconfig_management.configurations.clear() self.mobilityconfig_management.configurations.clear() @@ -212,7 +193,7 @@ class CoreClient: self.reusable.append(i) # draw session - self.app.canvas.canvas_reset_and_redraw(session) + self.app.canvas.reset_and_redraw(session) # draw tool bar appropritate with session state if self.is_runtime(): @@ -317,7 +298,7 @@ class CoreClient: def start_session(self): nodes = [x.core_node for x in self.canvas_nodes.values()] - links = self.get_links_proto() + links = list(self.links.values()) wlan_configs = self.get_wlan_configs_proto() mobility_configs = self.get_mobility_configs_proto() emane_model_configs = self.get_emane_model_configs_proto() @@ -389,7 +370,7 @@ class CoreClient: def create_nodes_and_links(self): node_protos = [x.core_node for x in self.canvas_nodes.values()] - link_protos = self.get_links_proto() + link_protos = list(self.links.values()) self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION) for node_proto in node_protos: response = self.client.add_node(self.session_id, node_proto) @@ -496,11 +477,15 @@ class CoreClient: node_interface_pairs = [] for i in edge_tokens: try: - e = self.edges.pop(i) - if e.interface_1 is not None: - node_interface_pairs.append(tuple([e.id1, e.interface_1.id])) - if e.interface_2 is not None: - node_interface_pairs.append(tuple([e.id2, e.interface_2.id])) + link = self.links.pop(i) + if link.interface_one is not None: + node_interface_pairs.append( + (link.node_one_id, link.interface_one.id) + ) + if link.interface_two is not None: + node_interface_pairs.append( + (link.node_two_id, link.interface_two.id) + ) except KeyError: logging.error("coreclient.py invalid edge token ") @@ -525,49 +510,31 @@ class CoreClient: if tuple([i, None]) in self.emaneconfig_management.configurations: self.emaneconfig_management.configurations.pop(tuple([i, None])) - def create_interface(self, node_type, gui_interface): - """ - create a protobuf interface given the interface object stored by the programmer - - :param core_bp2.NodeType node_type: node type - :param coretk.interface.Interface gui_interface: the programmer's interface - :rtype: core_bp2.Interface - :return: protobuf interface object - """ - if node_type != core_pb2.NodeType.DEFAULT: - return None - else: - interface = core_pb2.Interface( - id=gui_interface.id, - name=gui_interface.name, - mac=gui_interface.mac, - ip4=gui_interface.ipv4, - ip4mask=gui_interface.ip4prefix, - ) - logging.debug("create interface: %s", interface) - return interface - - def create_edge_interface(self, canvas_node): + def create_interface(self, canvas_node): interface = None core_node = canvas_node.core_node if self.is_model_node(core_node.model): ifid = len(canvas_node.interfaces) name = f"eth{ifid}" - interface = Interface( - name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) + interface = core_pb2.Interface( + id=ifid, + name=name, + ip4=str(self.interfaces_manager.get_address()), + ip4mask=24, ) canvas_node.interfaces.append(interface) logging.debug( - "create node(%s) interface IP: %s, name: %s", + "create node(%s) interface IPv4: %s, name: %s", core_node.name, - interface.ipv4, + interface.ip4, interface.name, ) return interface - def create_edge(self, token, canvas_node_one, canvas_node_two): + def create_link(self, token, canvas_node_one, canvas_node_two): """ - Add an edge to grpc manager + Create core link for a pair of canvas nodes, with token referencing + the canvas edge. :param tuple(int, int) token: edge's identification in the canvas :param canvas_node_one: canvas node one @@ -580,14 +547,15 @@ class CoreClient: # create interfaces self.interfaces_manager.new_subnet() - interface_one = self.create_edge_interface(canvas_node_one) + interface_one = self.create_interface(canvas_node_one) if interface_one is not None: self.interface_to_edge[(node_one.id, interface_one.id)] = token - interface_two = self.create_edge_interface(canvas_node_two) + interface_two = self.create_interface(canvas_node_two) if interface_two is not None: self.interface_to_edge[(node_two.id, interface_two.id)] = token # emane setup + # TODO: determine if this is needed if ( node_one.type == core_pb2.NodeType.EMANE and node_two.type == core_pb2.NodeType.DEFAULT @@ -605,28 +573,15 @@ class CoreClient: node_two.node_id, node_one.node_id, interface_one.id ) - edge = Edge( - self.session_id, node_one.id, node_one.type, node_two.id, node_two.type + link = core_pb2.Link( + type=core_pb2.LinkType.WIRED, + node_one_id=node_one.id, + node_two_id=node_two.id, + interface_one=interface_one, + interface_two=interface_two, ) - edge.interface_1 = interface_one - edge.interface_2 = interface_two - self.edges[token] = edge - return edge - - def get_links_proto(self): - links = [] - for edge in self.edges.values(): - interface_one = self.create_interface(edge.type1, edge.interface_1) - interface_two = self.create_interface(edge.type2, edge.interface_2) - link = core_pb2.Link( - node_one_id=edge.id1, - node_two_id=edge.id2, - type=core_pb2.LinkType.WIRED, - interface_one=interface_one, - interface_two=interface_two, - ) - links.append(link) - return links + self.links[token] = link + return link def get_wlan_configs_proto(self): configs = [] diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 1fcafac1..c1be859d 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -9,7 +9,6 @@ from coretk.canvasaction import CanvasAction from coretk.canvastooltip import CanvasTooltip from coretk.graph_helper import GraphHelper, WlanAntennaManager from coretk.images import Images -from coretk.interface import Interface from coretk.linkinfo import LinkInfo, Throughput from coretk.nodedelete import CanvasComponentManagement from coretk.wirelessconnection import WirelessConnection @@ -84,7 +83,7 @@ class CanvasGraph(tk.Canvas): self.node_context.add_command(label="Hide") self.node_context.add_command(label="Services") - def canvas_reset_and_redraw(self, session): + def reset_and_redraw(self, session): """ Reset the private variables CanvasGraph object, redraw nodes given the new grpc client. @@ -183,22 +182,23 @@ class CanvasGraph(tk.Canvas): canvas_node_one.edges.add(edge) canvas_node_two.edges.add(edge) self.edges[edge.token] = edge - self.core.create_edge(edge.token, canvas_node_one, canvas_node_two) + self.core.links[edge.token] = link self.helper.redraw_antenna(link, canvas_node_one, canvas_node_two) # TODO add back the link info to grpc manager also redraw - grpc_if1 = link.interface_one - grpc_if2 = link.interface_two + # TODO will include throughput and ipv6 in the future + interface_one = link.interface_one + interface_two = link.interface_two ip4_src = None ip4_dst = None ip6_src = None ip6_dst = None - if grpc_if1 is not None: - ip4_src = grpc_if1.ip4 - ip6_src = grpc_if1.ip6 - if grpc_if2 is not None: - ip4_dst = grpc_if2.ip4 - ip6_dst = grpc_if2.ip6 + if interface_one is not None: + ip4_src = interface_one.ip4 + ip6_src = interface_one.ip6 + if interface_two is not None: + ip4_dst = interface_two.ip4 + ip6_dst = interface_two.ip6 edge.link_info = LinkInfo( canvas=self, edge=edge, @@ -207,14 +207,8 @@ class CanvasGraph(tk.Canvas): ip4_dst=ip4_dst, ip6_dst=ip6_dst, ) - - # TODO will include throughput and ipv6 in the future - if1 = Interface(grpc_if1.name, grpc_if1.ip4, ifid=grpc_if1.id) - if2 = Interface(grpc_if2.name, grpc_if2.ip4, ifid=grpc_if2.id) - self.core.edges[edge.token].interface_1 = if1 - self.core.edges[edge.token].interface_2 = if2 - canvas_node_one.interfaces.append(if1) - canvas_node_two.interfaces.append(if2) + canvas_node_one.interfaces.append(interface_one) + canvas_node_two.interfaces.append(interface_two) # raise the nodes so they on top of the links self.tag_raise("node") @@ -316,17 +310,17 @@ class CanvasGraph(tk.Canvas): node_src.edges.add(edge) node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) - core_edge = self.core.create_edge(edge.token, node_src, node_dst) + link = self.core.create_link(edge.token, node_src, node_dst) # draw link info on the edge - if1 = core_edge.interface_1 - if2 = core_edge.interface_2 ip4_and_prefix_1 = None ip4_and_prefix_2 = None - if if1 is not None: - ip4_and_prefix_1 = if1.ip4_and_prefix - if if2 is not None: - ip4_and_prefix_2 = if2.ip4_and_prefix + if link.HasField("interface_one"): + if1 = link.interface_one + ip4_and_prefix_1 = f"{if1.ip4}/{if1.ip4mask}" + if link.HasField("interface_two"): + if2 = link.interface_two + ip4_and_prefix_2 = f"{if2.ip4}/{if2.ip4mask}" edge.link_info = LinkInfo( self, edge, diff --git a/coretk/coretk/interface.py b/coretk/coretk/interface.py index 5df26e26..b0fcd346 100644 --- a/coretk/coretk/interface.py +++ b/coretk/coretk/interface.py @@ -1,35 +1,4 @@ import ipaddress -import random - - -class Interface: - def __init__(self, name, ipv4, ifid=None): - """ - Create an interface instance - - :param str name: interface name - :param str ip4: IPv4 - :param str mac: MAC address - :param int ifid: interface id - """ - self.name = name - self.ipv4 = ipv4 - self.ip4prefix = 24 - self.ip4_and_prefix = ipv4 + "/" + str(self.ip4prefix) - self.mac = self.random_mac_address() - self.id = ifid - - def random_mac_address(self): - """ - create a random MAC address for an interface - - :return: nothing - """ - return "02:00:00:%02x:%02x:%02x" % ( - random.randint(0, 255), - random.randint(0, 255), - random.randint(0, 225), - ) class SubnetAddresses: @@ -46,18 +15,13 @@ class SubnetAddresses: class InterfaceManager: def __init__(self): - # self.prefix = None self.core_subnets = list( ipaddress.ip_network("10.0.0.0/12").subnets(prefixlen_diff=12) ) self.subnet_index = 0 self.address_index = 0 - - # self.network = ipaddress.ip_network("10.0.0.0/24") - # self.addresses = list(self.network.hosts()) self.network = None self.addresses = None - # self.start_interface_manager() def start_interface_manager(self): self.subnet_index = 0 @@ -72,24 +36,10 @@ class InterfaceManager: :return: """ - # i = self.index - # self.address_index = self.index + 1 - # return self.addresses[i] ipaddr = self.addresses[self.address_index] self.address_index = self.address_index + 1 return ipaddr def new_subnet(self): self.network = self.core_subnets[self.subnet_index] - # self.subnet_index = self.subnet_index + 1 self.addresses = list(self.network.hosts()) - # self.address_index = 0 - - # def new_subnet(self): - # """ - # retrieve a new subnet - # :return: - # """ - # if self.prefix is None: - # self.prefix = - # self.addresses = list(ipaddress.ip_network("10.0.0.0/24").hosts()) diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index 29430deb..1e0363cf 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -4,8 +4,6 @@ Link information, such as IPv4, IPv6 and throughput drawn in the canvas import logging import math -WIRELESS_DEF = ["mdr", "wlan"] - class LinkInfo: def __init__(self, canvas, edge, ip4_src, ip6_src, ip4_dst, ip6_dst): diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index a049c321..6017cbb1 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -375,6 +375,7 @@ class Toolbar(ttk.Frame): """ logging.debug("Click on STOP button ") self.app.core.stop_session() + self.app.canvas.delete("wireless") self.design_frame.tkraise() def update_annotation(self, image): diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py index b864ce38..e8d03126 100644 --- a/coretk/coretk/wirelessconnection.py +++ b/coretk/coretk/wirelessconnection.py @@ -19,7 +19,7 @@ class WirelessConnection: x1, y1 = self.canvas.coords(canvas_node_one.id) x2, y2 = self.canvas.coords(canvas_node_two.id) wlan_canvas_id = self.canvas.create_line( - x1, y1, x2, y2, fill="#009933", tags="wlan", width=1.5 + x1, y1, x2, y2, fill="#009933", tags="wireless", width=1.5 ) self.map[key] = wlan_canvas_id canvas_node_one.wlans.append(wlan_canvas_id) From 7981340b13f85c37ccfc90dfe20359d209d03ddc Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 15 Nov 2019 13:09:53 -0800 Subject: [PATCH 237/462] added path for creating non core container based nodes --- coretk/coretk/icons/docker.gif | Bin 0 -> 719 bytes coretk/coretk/icons/lxc.gif | Bin 0 -> 724 bytes coretk/coretk/images.py | 2 ++ coretk/coretk/toolbar.py | 28 ++++++++++++++++------------ 4 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 coretk/coretk/icons/docker.gif create mode 100644 coretk/coretk/icons/lxc.gif diff --git a/coretk/coretk/icons/docker.gif b/coretk/coretk/icons/docker.gif new file mode 100644 index 0000000000000000000000000000000000000000..dde25750d31c0f3ce9329dd5dd26f33ec3ebd0bf GIT binary patch literal 719 zcmZ?wbhEHblxGlSIOfgZtdQWYnC7jV<)f10tCAb2Rve;H5w2Mit=$x>)10K&ovh!R zYA_+qU}C1>v@E0P*+w&SjA!MW%q=jTUu3qZ#C&O)#fox^m6eujsw~&lSZ}Pg-qdKj zz1eP8i{0*4`@QWB`@38YcDo(y@i^4ud8p6l@Wj9)lY)*;4mma@^w`w!+e$?woLI z*Th?Ur{38&@hcMr_DcW~~#!wc>oS@huevWF*EK03YT@!9oHE^K{var^WCgBvLR zWMSlDsAte&00K~)FtGn?sBda+X>DuoXcp@3>Fw)m>6&ZbT_)L+Qh)j+bfo|;6p&-%r>DD zDiID3o8lQ5*(7wL%@~_IM0KS8F@Bifz{t!gX4A2u$BC6!)@_DIW1zA-qa3e{!vu$e zOziwBGP@@vGPm<9xJ`(-c}ek1zj5}xGnqooZ1Wvjh0bP}qy^2kC=Dy%GJO#+EyTRc z#Y>7;s=2E^o{yNRn>sz~>8dcT8{6vp4u#Iznt5wQyrI-4sqOl=)|Dy#-Klk5 SKj~;kf!l(FgPKnq7_0#ge~iHZ literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/lxc.gif b/coretk/coretk/icons/lxc.gif new file mode 100644 index 0000000000000000000000000000000000000000..e9f57aa30d3e2092b7de3f074146985272accd7b GIT binary patch literal 724 zcmZ?wbhEHblxGlSI2OW?Y^9KCr=07kQsAsw=&V-ere5K$QSGTw@2%D9uhS8z+ZUog zG0b3cxWTkY!2~R9E8Z-jp+asb?o-oSm3;Zc@(qDf#E87F?K7 zd~s&Ur8$+C=ha+W(sF%y$E~#!ZmpYmd&87F8>imgGV9*fIrp~Dy}x7r{hbRQ?pgA1 z@3KexS3W+t`pJ>?&yH_-erm^yGrRr|0-*Slg^`P)o2=Jx;6p9&Kkl-LBYXZ8aFmIu}pS- z7G&0Ipkv`46kvTrw~2w7uh-~BgCi5OF~6>s&MAh+Zu=RS_)HRx^)R)Im}FV}VP;Tj zWMUUFXi#X3WaW`_TT>vY{PFMDa3YoW# ztlWHvl~1mYO*Sgzao|j^{AYK5nrH Date: Fri, 15 Nov 2019 17:05:03 -0800 Subject: [PATCH 238/462] save service file data change locally and use that for display and for start session --- coretk/coretk/coreclient.py | 90 ++++++++++++++---- coretk/coretk/dialogs/nodeservice.py | 6 +- coretk/coretk/dialogs/serviceconfiguration.py | 94 ++++++++++++------- coretk/coretk/servicefileconfig.py | 37 ++++++++ coretk/coretk/servicenodeconfig.py | 5 +- 5 files changed, 177 insertions(+), 55 deletions(-) create mode 100644 coretk/coretk/servicefileconfig.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 51aa8e46..52a45140 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -11,6 +11,7 @@ from coretk.emaneodelnodeconfig import EmaneModelNodeConfig from coretk.images import NODE_WIDTH, Images from coretk.interface import Interface, InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig +from coretk.servicefileconfig import ServiceFileConfig from coretk.servicenodeconfig import ServiceNodeConfig from coretk.wlannodeconfig import WlanNodeConfig @@ -126,6 +127,9 @@ class CoreClient: self.emaneconfig_management = EmaneModelNodeConfig(app) self.emane_config = None self.serviceconfig_manager = ServiceNodeConfig(app) + self.servicefileconfig_manager = ServiceFileConfig() + self.created_nodes = set() + self.created_links = set() def set_observer(self, value): self.observer = value @@ -345,6 +349,10 @@ class CoreClient: mobility_configs = self.get_mobility_configs_proto() emane_model_configs = self.get_emane_model_configs_proto() hooks = list(self.hooks.values()) + # service_configs = self.get_service_config_proto() + # service_file_configs = self.get_service_file_config_proto() + self.created_links.clear() + self.created_nodes.clear() if self.emane_config: emane_config = {x: self.emane_config[x].value for x in self.emane_config} else: @@ -358,16 +366,10 @@ class CoreClient: emane_config=emane_config, emane_model_configs=emane_model_configs, mobility_configs=mobility_configs, + # service_configs=service_configs ) logging.debug("Start session %s, result: %s", self.session_id, response.result) - response = self.client.get_service_defaults(self.session_id) - for default in response.defaults: - print(default.node_type) - print(default.services) - response = self.client.get_node_service(self.session_id, 5, "FTP") - print(response) - def stop_session(self): response = self.client.stop_session(session_id=self.session_id) logging.debug("coregrpc.py Stop session, result: %s", response.result) @@ -434,23 +436,42 @@ class CoreClient: logging.debug("get service file %s", response) return response.data + def set_node_service_file(self, node_id, service_name, file_name, data): + response = self.client.set_node_service_file( + self.session_id, node_id, service_name, file_name, data + ) + logging.debug("set node service file %s", response) + def create_nodes_and_links(self): + """ + create nodes and links that have not been created yet + + :return: nothing + """ node_protos = self.get_nodes_proto() link_protos = self.get_links_proto() - self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION) for node_proto in node_protos: - response = self.client.add_node(self.session_id, node_proto) - logging.debug("create node: %s", response) + if node_proto.id not in self.created_nodes: + response = self.client.add_node(self.session_id, node_proto) + logging.debug("create node: %s", response) + self.created_nodes.add(node_proto.id) for link_proto in link_protos: - response = self.client.add_link( - self.session_id, - link_proto.node_one_id, - link_proto.node_two_id, - link_proto.interface_one, - link_proto.interface_two, - link_proto.options, - ) - logging.debug("create link: %s", response) + if ( + tuple([link_proto.node_one_id, link_proto.node_two_id]) + not in self.created_links + ): + response = self.client.add_link( + self.session_id, + link_proto.node_one_id, + link_proto.node_two_id, + link_proto.interface_one, + link_proto.interface_two, + link_proto.options, + ) + logging.debug("create link: %s", response) + self.created_links.add( + tuple([link_proto.node_one_id, link_proto.node_two_id]) + ) def close(self): """ @@ -838,6 +859,37 @@ class CoreClient: configs.append(config_proto) return configs + def get_service_config_proto(self): + configs = [] + for ( + node_id, + service_configs, + ) in self.serviceconfig_manager.configurations.items(): + for service, config in service_configs.items(): + config = core_pb2.ServiceConfig( + node_id=node_id, + service=service, + startup=config.startup, + validate=config.validate, + shutdown=config.shutdown, + ) + configs.append(config) + return configs + + def get_service_file_config_proto(self): + configs = [] + for ( + node_id, + service_file_configs, + ) in self.servicefileconfig_manager.configurations.items(): + for service, file_configs in service_file_configs.items(): + for file, data in file_configs.items(): + config = core_pb2.ServiceFileConfig( + node_id=node_id, service=service, file=file, data=data + ) + configs.append(config) + return configs + def run(self, node_id): logging.info("running node(%s) cmd: %s", node_id, self.observer) return self.client.node_command(self.session_id, node_id, self.observer).output diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 9ced9205..d791cc0e 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -17,7 +17,11 @@ class NodeService(Dialog): self.services = None self.current = None if services is None: - services = set() + services = set( + app.core.serviceconfig_manager.configurations[ + canvas_node.core_id + ].keys() + ) self.current_services = services self.draw() diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 360e9561..da0c0736 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -15,6 +15,7 @@ class ServiceConfiguration(Dialog): super().__init__(master, app, f"{service_name} service", modal=True) self.app = app self.canvas_node = canvas_node + self.node_id = canvas_node.core_id self.service_name = service_name self.radiovar = tk.IntVar() self.radiovar.set(2) @@ -40,8 +41,9 @@ class ServiceConfiguration(Dialog): self.validation_time_entry = None self.validation_mode_entry = None self.service_file_data = None - self.service_files = {} + self.original_service_files = {} self.temp_service_files = {} + self.modified_files = set() self.load() self.draw() @@ -49,9 +51,18 @@ class ServiceConfiguration(Dialog): # create nodes and links in definition state for getting and setting service file self.app.core.create_nodes_and_links() # load data from local memory - service_config = self.app.core.serviceconfig_manager.configurations[ - self.canvas_node.core_id - ][self.service_name] + serviceconfig_manager = self.app.core.serviceconfig_manager + if self.service_name in serviceconfig_manager.configurations[self.node_id]: + service_config = serviceconfig_manager.configurations[self.node_id][ + self.service_name + ] + else: + serviceconfig_manager.node_custom_service_configuration( + self.node_id, self.service_name + ) + service_config = serviceconfig_manager.configurations[self.node_id][ + self.service_name + ] self.dependencies = [x for x in service_config.dependencies] self.executables = [x for x in service_config.executables] self.metadata = service_config.meta @@ -61,13 +72,24 @@ class ServiceConfiguration(Dialog): self.shutdown_commands = [x for x in service_config.shutdown] self.validation_mode = service_config.validation_mode self.validation_time = service_config.validation_timer - self.service_files = { + self.original_service_files = { x: self.app.core.get_node_service_file( self.canvas_node.core_id, self.service_name, x ) for x in self.filenames } - self.temp_service_files = self.service_files + self.temp_service_files = { + x: self.original_service_files[x] for x in self.original_service_files + } + configs = self.app.core.servicefileconfig_manager.configurations + if ( + self.canvas_node.core_id in configs + and self.service_name in configs[self.canvas_node.core_id] + ): + for file, data in configs[self.canvas_node.core_id][ + self.service_name + ].items(): + self.temp_service_files[file] = data def draw(self): # self.columnconfigure(1, weight=1) @@ -169,7 +191,10 @@ class ServiceConfiguration(Dialog): if len(self.filenames) > 0: self.filename_combobox.current(0) self.service_file_data.delete(1.0, "end") - self.service_file_data.insert("end", self.service_files[self.filenames[0]]) + self.service_file_data.insert( + "end", self.temp_service_files[self.filenames[0]] + ) + self.service_file_data.bind("", self.update_temp_service_file_data) # tab 2 label = ttk.Label( @@ -184,7 +209,6 @@ class ServiceConfiguration(Dialog): if i == 0: label_frame = ttk.LabelFrame(tab3, text="Startup commands") commands = self.startup_commands - elif i == 1: label_frame = ttk.LabelFrame(tab3, text="Shutdown commands") commands = self.shutdown_commands @@ -242,11 +266,9 @@ class ServiceConfiguration(Dialog): frame.columnconfigure(0, weight=1) if i == 0: label = ttk.Label(frame, text="Validation time:") - self.validation_time_entry = ttk.Entry( - frame, - state="disabled", - textvariable=tk.StringVar(value=self.validation_time), - ) + self.validation_time_entry = ttk.Entry(frame) + self.validation_time_entry.insert("end", self.validation_time) + self.validation_time_entry.config(state="disabled") self.validation_time_entry.grid(row=i, column=1) elif i == 1: label = ttk.Label(frame, text="Validation mode:") @@ -258,13 +280,11 @@ class ServiceConfiguration(Dialog): mode = "NON_BLOCKING" elif self.validation_mode == core_pb2.ServiceValidationMode.TIMER: mode = "TIMER" - print("the mode is", mode) self.validation_mode_entry = ttk.Entry( frame, textvariable=tk.StringVar(value=mode) ) self.validation_mode_entry.insert("end", mode) - print("get mode") - print(self.validation_mode_entry.get()) + self.validation_mode_entry.config(state="disabled") self.validation_mode_entry.grid(row=i, column=1) elif i == 2: label = ttk.Label(frame, text="Validation period:") @@ -347,8 +367,6 @@ class ServiceConfiguration(Dialog): entry.delete(0, tk.END) def click_apply(self): - metadata = self.metadata_entry.get() - filenames = list(self.filename_combobox["values"]) 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") @@ -359,27 +377,35 @@ class ServiceConfiguration(Dialog): validate_commands, shutdown_commands, ) - filename = self.filename_combobox.get() - file_data = self.service_file_data.get() - print(filename, file_data) - logging.info( - "%s, %s, %s, %s, %s", - metadata, - filenames, - startup_commands, - shutdown_commands, - validate_commands, - ) - # wipe nodes and links when finished by setting to DEFINITION state - self.app.core.client.set_session_state( - self.app.core.session_id, core_pb2.SessionState.DEFINITION - ) + for file in self.modified_files: + self.app.core.servicefileconfig_manager.set_custom_service_file_config( + self.canvas_node.core_id, + self.service_name, + file, + self.temp_service_files[file], + ) + self.app.core.set_node_service_file( + self.canvas_node.core_id, + self.service_name, + file, + self.temp_service_files[file], + ) + self.destroy() def display_service_file_data(self, event): combobox = event.widget filename = combobox.get() self.service_file_data.delete(1.0, "end") - self.service_file_data.insert("end", self.service_files[filename]) + self.service_file_data.insert("end", self.temp_service_files[filename]) + + def update_temp_service_file_data(self, 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]: + self.modified_files.add(filename) + else: + self.modified_files.discard(filename) def click_defaults(self): logging.info("not implemented") diff --git a/coretk/coretk/servicefileconfig.py b/coretk/coretk/servicefileconfig.py new file mode 100644 index 00000000..06a134cd --- /dev/null +++ b/coretk/coretk/servicefileconfig.py @@ -0,0 +1,37 @@ +""" +service file configuration +""" + + +class ServiceFileConfig: + def __init__(self): + # dict(node_id:dict(service:dict(filename, data))) + self.configurations = {} + + # def set_service_configs(self, node_id, service_name, file_configs): + # """ + # store file configs + # + # :param int node_id: node id + # :param str service_name: service name + # :param dict(str, str) file_configs: map of service file to its data + # :return: nothing + # """ + # for key, value in file_configs.items(): + # self.configurations[node_id][service_name][key] = value + + def set_custom_service_file_config(self, node_id, service_name, file_name, data): + """ + store file config + + :param int node_id: node id + :param str service_name: service name + :param str file_name: file name + :param str data: data + :return: nothing + """ + if node_id not in self.configurations: + self.configurations[node_id] = {} + if service_name not in self.configurations[node_id]: + self.configurations[node_id][service_name] = {} + self.configurations[node_id][service_name][file_name] = data diff --git a/coretk/coretk/servicenodeconfig.py b/coretk/coretk/servicenodeconfig.py index f9c2ee88..98de9034 100644 --- a/coretk/coretk/servicenodeconfig.py +++ b/coretk/coretk/servicenodeconfig.py @@ -7,6 +7,7 @@ import logging class ServiceNodeConfig: def __init__(self, app): self.app = app + # dict(node_id:dict(service:node_service_config_proto)) self.configurations = {} self.default_services = {} @@ -37,7 +38,9 @@ class ServiceNodeConfig: self.configurations[node_id][default] = response.service def node_custom_service_configuration(self, node_id, service_name): - return + self.configurations[node_id][service_name] = self.app.core.get_node_service( + node_id, service_name + ) def node_service_custom_configuration( self, node_id, service_name, startups, validates, shutdowns From 8ad9b7d7286bd17e4899ecd331c9ff56dce6713b Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 15 Nov 2019 23:31:41 -0800 Subject: [PATCH 239/462] removed CustomNode class, added nodeutils and NodeDraw to support defining the current node type to draw and reuse for custom nodes as well --- coretk/coretk/app.py | 4 ++ coretk/coretk/coreclient.py | 21 +++----- coretk/coretk/dialogs/customnodes.py | 43 +++++++-------- coretk/coretk/dialogs/icondialog.py | 3 +- coretk/coretk/graph.py | 27 ++++------ coretk/coretk/images.py | 41 --------------- coretk/coretk/nodeutils.py | 79 ++++++++++++++++++++++++++++ coretk/coretk/themes.py | 7 ++- coretk/coretk/toolbar.py | 60 ++++++--------------- 9 files changed, 146 insertions(+), 139 deletions(-) create mode 100644 coretk/coretk/nodeutils.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 185e5968..fbce02e1 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -9,12 +9,16 @@ from coretk.graph import CanvasGraph from coretk.images import ImageEnum, Images from coretk.menuaction import MenuAction from coretk.menubar import Menubar +from coretk.nodeutils import NodeUtils from coretk.toolbar import Toolbar class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) + # load node icons + NodeUtils.setup() + # widgets self.menubar = None self.toolbar = None diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 5ed35b57..c57a0b42 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -7,13 +7,12 @@ import os from core.api.grpc import client, core_pb2 from coretk.dialogs.sessions import SessionsDialog from coretk.emaneodelnodeconfig import EmaneModelNodeConfig -from coretk.images import NODE_WIDTH, Images from coretk.interface import InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig +from coretk.nodeutils import NodeDraw from coretk.servicenodeconfig import ServiceNodeConfig from coretk.wlannodeconfig import WlanNodeConfig -NETWORK_NODES = {"switch", "hub", "wlan", "rj45", "tunnel", "emane"} DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"} OBSERVERS = { "processes": "ps", @@ -35,14 +34,6 @@ class CoreServer: self.port = port -class CustomNode: - def __init__(self, name, image, image_file, services): - self.name = name - self.image = image - self.image_file = image_file - self.services = services - - class Observer: def __init__(self, name, cmd): self.name = name @@ -96,12 +87,11 @@ class CoreClient: # read custom nodes for config in self.app.config.get("nodes", []): + name = config["name"] image_file = config["image"] - image = Images.get_custom(image_file, NODE_WIDTH) - custom_node = CustomNode( - config["name"], image, image_file, set(config["services"]) - ) - self.custom_nodes[custom_node.name] = custom_node + services = set(config["services"]) + node_draw = NodeDraw.from_custom(name, image_file, services) + self.custom_nodes[name] = node_draw # read observers for config in self.app.config.get("observers", []): @@ -448,6 +438,7 @@ class CoreClient: self.emaneconfig_management.set_default_config(node_id) # set default service configurations + # TODO: need to deal with this and custom node cases if node_type == core_pb2.NodeType.DEFAULT: self.serviceconfig_manager.node_default_services_configuration( node_id=node_id, node_model=model diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index f427f08b..da7a1fb0 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -3,9 +3,9 @@ import tkinter as tk from pathlib import Path from tkinter import ttk -from coretk.coreclient import CustomNode from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog +from coretk.nodeutils import NodeDraw from coretk.widgets import CheckboxList, ListboxScroll @@ -119,7 +119,9 @@ class CustomNodesDialog(Dialog): frame.columnconfigure(0, weight=1) entry = ttk.Entry(frame, textvariable=self.name) entry.grid(sticky="ew") - self.image_button = ttk.Button(frame, text="Icon", command=self.click_icon) + self.image_button = ttk.Button( + frame, text="Icon", compound=tk.LEFT, command=self.click_icon + ) self.image_button.grid(sticky="ew") button = ttk.Button(frame, text="Services", command=self.click_services) button.grid(sticky="ew") @@ -180,12 +182,12 @@ class CustomNodesDialog(Dialog): def click_save(self): self.app.config["nodes"].clear() for name in sorted(self.app.core.custom_nodes): - custom_node = self.app.core.custom_nodes[name] + node_draw = self.app.core.custom_nodes[name] self.app.config["nodes"].append( { - "name": custom_node.name, - "image": custom_node.image_file, - "services": list(custom_node.services), + "name": name, + "image": node_draw.image_file, + "services": list(node_draw.services), } ) logging.info("saving custom nodes: %s", self.app.config["nodes"]) @@ -195,10 +197,9 @@ class CustomNodesDialog(Dialog): def click_create(self): name = self.name.get() if name not in self.app.core.custom_nodes: - custom_node = CustomNode( - name, self.image, Path(self.image_file).name, set(self.services) - ) - self.app.core.custom_nodes[name] = custom_node + image_file = Path(self.image_file).stem + node_draw = NodeDraw.from_custom(name, image_file, set(self.services)) + self.app.core.custom_nodes[name] = node_draw self.nodes_list.listbox.insert(tk.END, name) self.reset_values() @@ -207,12 +208,12 @@ class CustomNodesDialog(Dialog): if self.selected: previous_name = self.selected self.selected = name - custom_node = self.app.core.custom_nodes.pop(previous_name) - custom_node.name = name - custom_node.image = self.image - custom_node.image_file = Path(self.image_file).stem - custom_node.services = self.services - self.app.core.custom_nodes[name] = custom_node + node_draw = self.app.core.custom_nodes.pop(previous_name) + node_draw.model = name + node_draw.image_file = Path(self.image_file).stem + node_draw.image = self.image + node_draw.services = self.services + self.app.core.custom_nodes[name] = node_draw self.nodes_list.listbox.delete(self.selected_index) self.nodes_list.listbox.insert(self.selected_index, name) self.nodes_list.listbox.selection_set(self.selected_index) @@ -230,11 +231,11 @@ class CustomNodesDialog(Dialog): if selection: self.selected_index = selection[0] self.selected = self.nodes_list.listbox.get(self.selected_index) - custom_node = self.app.core.custom_nodes[self.selected] - self.name.set(custom_node.name) - self.services = custom_node.services - self.image = custom_node.image - self.image_file = custom_node.image_file + node_draw = self.app.core.custom_nodes[self.selected] + self.name.set(node_draw.model) + self.services = node_draw.services + self.image = node_draw.image + self.image_file = node_draw.image_file self.image_button.config(image=self.image) self.edit_button.config(state=tk.NORMAL) self.delete_button.config(state=tk.NORMAL) diff --git a/coretk/coretk/dialogs/icondialog.py b/coretk/coretk/dialogs/icondialog.py index c5518f28..98a4ed55 100644 --- a/coretk/coretk/dialogs/icondialog.py +++ b/coretk/coretk/dialogs/icondialog.py @@ -1,6 +1,7 @@ import tkinter as tk from tkinter import filedialog, ttk +from coretk import nodeutils from coretk.appconfig import ICONS_PATH from coretk.dialogs.dialog import Dialog from coretk.images import Images @@ -54,7 +55,7 @@ class IconDialog(Dialog): ), ) if file_path: - self.image = Images.create(file_path, 32, 32) + self.image = Images.create(file_path, nodeutils.ICON_SIZE) self.image_label.config(image=self.image) self.file_path.set(file_path) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index c1be859d..38d67079 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -11,6 +11,7 @@ from coretk.graph_helper import GraphHelper, WlanAntennaManager from coretk.images import Images from coretk.linkinfo import LinkInfo, Throughput from coretk.nodedelete import CanvasComponentManagement +from coretk.nodeutils import NodeUtils from coretk.wirelessconnection import WirelessConnection @@ -37,9 +38,7 @@ class CanvasGraph(tk.Canvas): kwargs["highlightthickness"] = 0 super().__init__(master, cnf, **kwargs) self.mode = GraphMode.SELECT - self.draw_node_image = None - self.draw_node_type = None - self.draw_node_model = None + self.node_draw = None self.selected = None self.node_context = None self.nodes = {} @@ -96,9 +95,7 @@ class CanvasGraph(tk.Canvas): # set the private variables to default value self.mode = GraphMode.SELECT - self.draw_node_image = None - self.draw_node_type = None - self.draw_node_model = None + self.node_draw = None self.selected = None self.node_context = None self.nodes.clear() @@ -157,7 +154,7 @@ class CanvasGraph(tk.Canvas): continue # draw nodes on the canvas - image = Images.node_icon(core_node.type, core_node.model) + image = NodeUtils.node_icon(core_node.type, core_node.model) position = core_node.position node = CanvasNode(position.x, position.y, image, self.master, core_node) self.nodes[node.id] = node @@ -267,13 +264,7 @@ class CanvasGraph(tk.Canvas): self.handle_edge_release(event) elif self.mode == GraphMode.NODE: x, y = self.canvas_xy(event) - self.add_node( - x, - y, - self.draw_node_image, - self.draw_node_type, - self.draw_node_model, - ) + self.add_node(x, y) elif self.mode == GraphMode.PICKNODE: self.mode = GraphMode.NODE @@ -404,12 +395,14 @@ class CanvasGraph(tk.Canvas): # delete the related data from core self.core.delete_wanted_graph_nodes(node_ids, to_delete_edge_tokens) - def add_node(self, x, y, image, node_type, model): + def add_node(self, x, y): plot_id = self.find_all()[0] logging.info("add node event: %s - %s", plot_id, self.selected) if self.selected == plot_id: - core_node = self.core.create_node(int(x), int(y), node_type, model) - node = CanvasNode(x, y, image, self.master, core_node) + core_node = self.core.create_node( + int(x), int(y), self.node_draw.node_type, self.node_draw.model + ) + node = CanvasNode(x, y, self.node_draw.image, self.master, core_node) self.core.canvas_nodes[core_node.id] = node self.nodes[node.id] = node return node diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 1a377253..8f13c593 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -1,9 +1,7 @@ -import logging from enum import Enum from PIL import Image, ImageTk -from core.api.grpc import core_pb2 from coretk.appconfig import LOCAL_ICONS_PATH NODE_WIDTH = 32 @@ -35,45 +33,6 @@ class Images: file_path = cls.images[name] return cls.create(file_path, width, height) - @classmethod - def node_icon(cls, node_type, node_model): - """ - Retrieve image based on type and model - :param core_pb2.NodeType node_type: core node type - :param string node_model: the node model - :return: core node icon - :rtype: PhotoImage - """ - image_enum = ImageEnum.ROUTER - if node_type == core_pb2.NodeType.SWITCH: - image_enum = ImageEnum.SWITCH - elif node_type == core_pb2.NodeType.HUB: - image_enum = ImageEnum.HUB - elif node_type == core_pb2.NodeType.WIRELESS_LAN: - image_enum = ImageEnum.WLAN - elif node_type == core_pb2.NodeType.EMANE: - image_enum = ImageEnum.EMANE - elif node_type == core_pb2.NodeType.RJ45: - image_enum = ImageEnum.RJ45 - elif node_type == core_pb2.NodeType.TUNNEL: - image_enum = ImageEnum.TUNNEL - elif node_type == core_pb2.NodeType.DEFAULT: - if node_model == "router": - image_enum = ImageEnum.ROUTER - elif node_model == "host": - image_enum = ImageEnum.HOST - elif node_model == "PC": - image_enum = ImageEnum.PC - elif node_model == "mdr": - image_enum = ImageEnum.MDR - elif node_model == "prouter": - image_enum = ImageEnum.PROUTER - else: - logging.error("invalid node model: %s", node_model) - else: - logging.error("invalid node type: %s", node_type) - return Images.get(image_enum, NODE_WIDTH) - class ImageEnum(Enum): SWITCH = "lanswitch" diff --git a/coretk/coretk/nodeutils.py b/coretk/coretk/nodeutils.py new file mode 100644 index 00000000..df483ba6 --- /dev/null +++ b/coretk/coretk/nodeutils.py @@ -0,0 +1,79 @@ +from core.api.grpc.core_pb2 import NodeType +from coretk.images import ImageEnum, Images + +ICON_SIZE = 32 + + +class NodeDraw: + def __init__(self): + self.custom = False + self.image = None + self.image_enum = None + self.image_file = None + self.node_type = None + self.model = None + self.tooltip = None + self.services = set() + + @classmethod + def from_setup(cls, image_enum, node_type, model=None, tooltip=None): + node_draw = NodeDraw() + node_draw.image_enum = image_enum + node_draw.image = Images.get(image_enum, ICON_SIZE) + node_draw.node_type = node_type + node_draw.model = model + if tooltip is None: + tooltip = model + node_draw.tooltip = tooltip + return node_draw + + @classmethod + def from_custom(cls, name, image_file, services): + node_draw = NodeDraw() + node_draw.custom = True + node_draw.image_file = image_file + node_draw.image = Images.get_custom(image_file, ICON_SIZE) + node_draw.node_type = NodeType.DEFAULT + node_draw.services = services + node_draw.model = name + node_draw.tooltip = name + return node_draw + + +class NodeUtils: + NODES = [] + NETWORK_NODES = [] + NODE_ICONS = {} + + @classmethod + def node_icon(cls, node_type, model): + return cls.NODE_ICONS[(node_type, model)] + + @classmethod + def setup(cls): + nodes = [ + (ImageEnum.ROUTER, NodeType.DEFAULT, "router"), + (ImageEnum.HOST, NodeType.DEFAULT, "host"), + (ImageEnum.PC, NodeType.DEFAULT, "PC"), + (ImageEnum.MDR, NodeType.DEFAULT, "mdr"), + (ImageEnum.PROUTER, NodeType.DEFAULT, "prouter"), + (ImageEnum.DOCKER, NodeType.DOCKER, "Docker"), + (ImageEnum.LXC, NodeType.LXC, "LXC"), + ] + for image_enum, node_type, model in nodes: + node_draw = NodeDraw.from_setup(image_enum, node_type, model) + cls.NODES.append(node_draw) + cls.NODE_ICONS[(node_type, model)] = node_draw.image + + network_nodes = [ + (ImageEnum.HUB, NodeType.HUB, "ethernet hub"), + (ImageEnum.SWITCH, NodeType.SWITCH, "ethernet switch"), + (ImageEnum.WLAN, NodeType.WIRELESS_LAN, "wireless LAN"), + (ImageEnum.EMANE, NodeType.EMANE, "EMANE"), + (ImageEnum.RJ45, NodeType.RJ45, "rj45 physical interface tool"), + (ImageEnum.TUNNEL, NodeType.TUNNEL, "tunnel tool"), + ] + for image_enum, node_type, tooltip in network_nodes: + node_draw = NodeDraw.from_setup(image_enum, node_type, tooltip=tooltip) + cls.NETWORK_NODES.append(node_draw) + cls.NODE_ICONS[(node_type, None)] = node_draw.image diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index f6dced19..0cf54eb8 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -54,7 +54,12 @@ def load(style): }, }, "TButton": { - "configure": {"width": 8, "padding": (5, 1), "relief": tk.RAISED}, + "configure": { + "width": 8, + "padding": (5, 1), + "relief": tk.RAISED, + "anchor": tk.CENTER, + }, "map": { "relief": [("pressed", tk.SUNKEN)], "shiftrelief": [("pressed", 1)], diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 7fe675a4..bb1803ea 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -3,10 +3,10 @@ import tkinter as tk from functools import partial from tkinter import ttk -from core.api.grpc import core_pb2 from coretk.dialogs.customnodes import CustomNodesDialog from coretk.graph import GraphMode from coretk.images import ImageEnum, Images +from coretk.nodeutils import NodeUtils from coretk.tooltip import Tooltip WIDTH = 32 @@ -129,33 +129,16 @@ class Toolbar(ttk.Frame): def draw_node_picker(self): self.hide_pickers() self.node_picker = ttk.Frame(self.master) - nodes = [ - (ImageEnum.ROUTER, core_pb2.NodeType.DEFAULT, "router"), - (ImageEnum.HOST, core_pb2.NodeType.DEFAULT, "host"), - (ImageEnum.PC, core_pb2.NodeType.DEFAULT, "PC"), - (ImageEnum.MDR, core_pb2.NodeType.DEFAULT, "mdr"), - (ImageEnum.PROUTER, core_pb2.NodeType.DEFAULT, "prouter"), - (ImageEnum.DOCKER, core_pb2.NodeType.DOCKER, "Docker"), - (ImageEnum.LXC, core_pb2.NodeType.LXC, "LXC"), - ] # draw default nodes - for image_enum, node_type, model in nodes: - image = icon(image_enum) - func = partial( - self.update_button, self.node_button, image, node_type, model - ) - self.create_picker_button(image, func, self.node_picker, model) + for node_draw in NodeUtils.NODES: + image = icon(node_draw.image_enum) + func = partial(self.update_button, self.node_button, image, node_draw) + self.create_picker_button(image, func, self.node_picker, node_draw.tooltip) # draw custom nodes for name in sorted(self.app.core.custom_nodes): - custom_node = self.app.core.custom_nodes[name] - image = custom_node.image - func = partial( - self.update_button, - self.node_button, - image, - core_pb2.NodeType.DEFAULT, - name, - ) + node_draw = self.app.core.custom_nodes[name] + image = Images.get_custom(node_draw.image_file, WIDTH) + func = partial(self.update_button, self.node_button, image, node_draw) self.create_picker_button(image, func, self.node_picker, name) # draw edit node image = icon(ImageEnum.EDITNODE) @@ -227,15 +210,13 @@ class Toolbar(ttk.Frame): dialog = CustomNodesDialog(self.app, self.app) dialog.show() - def update_button(self, button, image, node_type, model=None): - logging.info("update button(%s): %s", button, node_type) + def update_button(self, button, image, node_draw): + logging.info("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.draw_node_image = image - self.app.canvas.draw_node_type = node_type - self.app.canvas.draw_node_model = model + self.app.canvas.node_draw = node_draw def hide_pickers(self): logging.info("hiding pickers") @@ -271,21 +252,13 @@ class Toolbar(ttk.Frame): """ self.hide_pickers() self.network_picker = ttk.Frame(self.master) - nodes = [ - (ImageEnum.HUB, core_pb2.NodeType.HUB, "ethernet hub"), - (ImageEnum.SWITCH, core_pb2.NodeType.SWITCH, "ethernet switch"), - (ImageEnum.WLAN, core_pb2.NodeType.WIRELESS_LAN, "wireless LAN"), - (ImageEnum.EMANE, core_pb2.NodeType.EMANE, "EMANE"), - (ImageEnum.RJ45, core_pb2.NodeType.RJ45, "rj45 physical interface tool"), - (ImageEnum.TUNNEL, core_pb2.NodeType.TUNNEL, "tunnel tool"), - ] - for image_enum, node_type, tooltip in nodes: - image = icon(image_enum) + for node_draw in NodeUtils.NETWORK_NODES: + image = icon(node_draw.image_enum) self.create_picker_button( image, - partial(self.update_button, self.network_button, image, node_type), + partial(self.update_button, self.network_button, image, node_draw), self.network_picker, - tooltip, + node_draw.tooltip, ) self.design_select(self.network_button) self.network_button.after( @@ -294,7 +267,8 @@ class Toolbar(ttk.Frame): def create_network_button(self): """ - Create link-layer node button and the options that represent different link-layer node types + Create link-layer node button and the options that represent different + link-layer node types. :return: nothing """ From 95cd7926756b7edb6b86df4f69dd2458985b1e77 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 15 Nov 2019 23:45:01 -0800 Subject: [PATCH 240/462] updated check for creating interfaces on nodes --- coretk/coretk/coreclient.py | 8 ++------ coretk/coretk/dialogs/nodeconfig.py | 3 ++- coretk/coretk/nodeutils.py | 5 +++++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index c57a0b42..3c63e6bc 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -9,11 +9,10 @@ from coretk.dialogs.sessions import SessionsDialog from coretk.emaneodelnodeconfig import EmaneModelNodeConfig from coretk.interface import InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig -from coretk.nodeutils import NodeDraw +from coretk.nodeutils import NodeDraw, NodeUtils from coretk.servicenodeconfig import ServiceNodeConfig from coretk.wlannodeconfig import WlanNodeConfig -DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"} OBSERVERS = { "processes": "ps", "ifconfig": "ifconfig", @@ -406,9 +405,6 @@ class CoreClient: else: return self.reusable.pop(0) - def is_model_node(self, name): - return name in DEFAULT_NODES or name in self.custom_nodes - def create_node(self, x, y, node_type, model): """ Add node, with information filled in, to grpc manager @@ -511,7 +507,7 @@ class CoreClient: def create_interface(self, canvas_node): interface = None core_node = canvas_node.core_node - if self.is_model_node(core_node.model): + if NodeUtils.is_interface_node(core_node.type): ifid = len(canvas_node.interfaces) name = f"eth{ifid}" interface = core_pb2.Interface( diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 14e9de03..57c2fcfb 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -1,11 +1,12 @@ import tkinter as tk from tkinter import ttk -from coretk.coreclient import DEFAULT_NODES from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeService +DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"} + class NodeConfigDialog(Dialog): def __init__(self, master, app, canvas_node): diff --git a/coretk/coretk/nodeutils.py b/coretk/coretk/nodeutils.py index df483ba6..6777718a 100644 --- a/coretk/coretk/nodeutils.py +++ b/coretk/coretk/nodeutils.py @@ -44,6 +44,11 @@ class NodeUtils: NODES = [] NETWORK_NODES = [] NODE_ICONS = {} + INTERFACE_NODE = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} + + @classmethod + def is_interface_node(cls, node_type): + return node_type in cls.INTERFACE_NODE @classmethod def node_icon(cls, node_type, model): From 695f5c3e66557eb3e497b7ddcf2224df613f2eba Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 16 Nov 2019 08:54:15 -0800 Subject: [PATCH 241/462] updated node config layout --- coretk/coretk/dialogs/nodeconfig.py | 79 ++++++++++++++++------------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 57c2fcfb..a8252336 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -6,6 +6,7 @@ from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeService DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"} +PAD = 5 class NodeConfigDialog(Dialog): @@ -28,54 +29,64 @@ class NodeConfigDialog(Dialog): def draw(self): self.top.columnconfigure(0, weight=1) - self.draw_first_row() - self.draw_second_row() - self.draw_third_row() + row = 0 - def draw_first_row(self): + # field frame frame = ttk.Frame(self.top) - frame.grid(row=0, column=0, pady=2, sticky="ew") - frame.columnconfigure(0, weight=1) + frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=1) + # name field + label = ttk.Label(frame, text="Name") + label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) entry = ttk.Entry(frame, textvariable=self.name) - entry.grid(row=0, column=0, padx=2, sticky="ew") - - combobox = ttk.Combobox( - frame, textvariable=self.type, values=DEFAULT_NODES, state="readonly" - ) - combobox.grid(row=0, column=1, padx=2, sticky="ew") - - servers = [""] - servers.extend(list(sorted(self.app.core.servers.keys()))) - combobox = ttk.Combobox( - frame, textvariable=self.server, values=servers, state="readonly" - ) - combobox.current(0) - combobox.grid(row=0, column=2, sticky="ew") - - def draw_second_row(self): - frame = ttk.Frame(self.top) - frame.grid(row=1, column=0, pady=2, sticky="ew") - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - - button = ttk.Button(frame, text="Services", command=self.click_services) - button.grid(row=0, column=0, padx=2, sticky="ew") + entry.grid(row=row, column=1, sticky="ew") + row += 1 + # icon field + label = ttk.Label(frame, text="Icon") + label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) self.image_button = ttk.Button( frame, text="Icon", image=self.image, - compound=tk.LEFT, + compound=tk.NONE, command=self.click_icon, ) - self.image_button.grid(row=0, column=1, sticky="ew") + self.image_button.grid(row=row, column=1, sticky="ew") + row += 1 - def draw_third_row(self): + # node type field + label = ttk.Label(frame, text="Type") + label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + combobox = ttk.Combobox( + frame, textvariable=self.type, values=list(DEFAULT_NODES), state="readonly" + ) + combobox.grid(row=row, column=1, sticky="ew") + row += 1 + + # server + frame.grid(sticky="ew") + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="Server") + label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + servers = ["localhost"] + servers.extend(list(sorted(self.app.core.servers.keys()))) + combobox = ttk.Combobox( + frame, textvariable=self.server, values=servers, state="readonly" + ) + combobox.grid(row=row, column=1, sticky="ew") + row += 1 + + # services + button = ttk.Button(self.top, text="Services", command=self.click_services) + button.grid(sticky="ew", pady=PAD) + + self.draw_buttons() + + def draw_buttons(self): frame = ttk.Frame(self.top) - frame.grid(row=2, column=0, sticky="ew") + frame.grid(sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) From c43e5f999ca27948a2dee8b1b6e22e2dcd9895a9 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 20 Nov 2019 10:59:30 -0800 Subject: [PATCH 242/462] added node config interface display and updated canvas nodes to use core node porotbuf directly for display and saving data --- coretk/coretk/canvasaction.py | 2 +- coretk/coretk/dialogs/emaneconfig.py | 14 ++-- coretk/coretk/dialogs/mobilityconfig.py | 5 +- coretk/coretk/dialogs/nodeconfig.py | 93 ++++++++++++++++++++++--- coretk/coretk/dialogs/wlanconfig.py | 11 ++- coretk/coretk/graph.py | 45 ++++++------ coretk/coretk/themes.py | 3 +- 7 files changed, 124 insertions(+), 49 deletions(-) diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py index 5b9c08c3..08aa8c3d 100644 --- a/coretk/coretk/canvasaction.py +++ b/coretk/coretk/canvasaction.py @@ -30,7 +30,7 @@ class CanvasAction: def display_wlan_configuration(self, canvas_node): wlan_config = self.master.core.wlanconfig_management.configurations[ - canvas_node.core_id + canvas_node.core_node.id ] dialog = WlanConfigDialog( self.master, self.master, self.node_to_show_config, wlan_config diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index d64e9089..2c885511 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -20,6 +20,7 @@ class EmaneConfiguration(Dialog): super().__init__(master, app, "emane configuration", modal=False) self.app = app self.canvas_node = canvas_node + self.node = canvas_node.core_node self.radiovar = tk.IntVar() self.radiovar.set(1) self.columnconfigure(0, weight=1) @@ -122,20 +123,15 @@ class EmaneConfiguration(Dialog): # add string emane_ infront for grpc call response = self.app.core.client.set_emane_model_config( - self.app.core.session_id, - self.canvas_node.core_id, - "emane_" + model_name, - config, + self.app.core.session_id, self.node.id, f"emane_{model_name}", config ) logging.info( - "emaneconfig.py config emane model (%s), result: %s", - self.canvas_node.core_id, - response, + "emaneconfig.py config emane model (%s), result: %s", self.node.id, response ) # store the change locally self.app.core.emaneconfig_management.set_custom_emane_cloud_config( - self.canvas_node.core_id, "emane_" + model_name + self.node.id, f"emane_{model_name}" ) self.emane_model_dialog.destroy() @@ -161,7 +157,7 @@ class EmaneConfiguration(Dialog): session_id = self.app.core.session_id # add string emane_ before model name for grpc call response = self.app.core.client.get_emane_model_config( - session_id, self.canvas_node.core_id, "emane_" + model_name + session_id, self.node.id, f"emane_{model_name}" ) logging.info("emane model config %s", response) diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/coretk/coretk/dialogs/mobilityconfig.py index 1f53b4a0..2c20229a 100644 --- a/coretk/coretk/dialogs/mobilityconfig.py +++ b/coretk/coretk/dialogs/mobilityconfig.py @@ -19,9 +19,10 @@ class MobilityConfigDialog(Dialog): """ super().__init__(master, app, "ns2script configuration", modal=True) self.canvas_node = canvas_node + self.node = canvas_node.core_node logging.info(app.canvas.core.mobilityconfig_management.configurations) self.node_config = app.canvas.core.mobilityconfig_management.configurations[ - canvas_node.core_id + self.node.id ] self.mobility_script_parameters() @@ -208,7 +209,7 @@ class MobilityConfigDialog(Dialog): else: loop = "0" self.app.canvas.core.mobilityconfig_management.set_custom_configuration( - node_id=self.canvas_node.core_id, + node_id=self.node.id, file=file, refresh_ms=refresh_time, loop=loop, diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index a8252336..8a0d4522 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -1,14 +1,36 @@ +import logging import tkinter as tk +from functools import partial from tkinter import ttk from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeService +from coretk.widgets import FrameScroll DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"} PAD = 5 +def mac_auto(is_auto, entry): + logging.info("mac auto clicked") + if is_auto.get(): + logging.info("disabling mac") + entry.var.set("") + entry.config(state=tk.DISABLED) + else: + entry.var.set("00:00:00:00:00:00") + entry.config(state=tk.NORMAL) + + +class InterfaceData: + def __init__(self, is_auto, mac, ip4, ip6): + self.is_auto = is_auto + self.mac = mac + self.ip4 = ip4 + self.ip6 = ip6 + + class NodeConfigDialog(Dialog): def __init__(self, master, app, canvas_node): """ @@ -18,13 +40,20 @@ class NodeConfigDialog(Dialog): :param coretk.app.Application: main app :param coretk.graph.CanvasNode canvas_node: canvas node object """ - super().__init__(master, app, f"{canvas_node.name} Configuration", modal=True) + super().__init__( + master, app, f"{canvas_node.core_node.name} Configuration", modal=True + ) self.canvas_node = canvas_node + self.node = canvas_node.core_node self.image = canvas_node.image self.image_button = None - self.name = tk.StringVar(value=canvas_node.name) - self.type = tk.StringVar(value=canvas_node.core_node.model) - self.server = tk.StringVar() + self.name = tk.StringVar(value=self.node.name) + self.type = tk.StringVar(value=self.node.model) + server = "localhost" + if self.node.server: + server = self.node.server + self.server = tk.StringVar(value=server) + self.interfaces = {} self.draw() def draw(self): @@ -82,8 +111,50 @@ class NodeConfigDialog(Dialog): button = ttk.Button(self.top, text="Services", command=self.click_services) button.grid(sticky="ew", pady=PAD) + # interfaces + if self.canvas_node.interfaces: + self.draw_interfaces() + self.draw_buttons() + def draw_interfaces(self): + scroll = FrameScroll(self.top, self.app, text="Interfaces") + scroll.grid(sticky="nsew") + scroll.frame.columnconfigure(0, weight=1) + scroll.frame.rowconfigure(0, weight=1) + for interface in self.canvas_node.interfaces: + logging.info("interface: %s", interface) + frame = ttk.LabelFrame(scroll.frame, text=interface.name, padding=PAD) + frame.grid(sticky="ew", pady=PAD) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + + label = ttk.Label(frame, text="MAC") + label.grid(row=0, column=0, padx=PAD, pady=PAD) + is_auto = tk.BooleanVar(value=True) + checkbutton = ttk.Checkbutton(frame, text="Auto?", variable=is_auto) + checkbutton.var = is_auto + checkbutton.grid(row=0, column=1, padx=PAD) + mac = tk.StringVar(value=interface.mac) + entry = ttk.Entry(frame, textvariable=mac, state=tk.DISABLED) + entry.grid(row=0, column=2, sticky="ew") + func = partial(mac_auto, is_auto, entry) + checkbutton.config(command=func) + + label = ttk.Label(frame, text="IPv4") + label.grid(row=1, column=0, padx=PAD, pady=PAD) + ip4 = tk.StringVar(value=f"{interface.ip4}/{interface.ip4mask}") + entry = ttk.Entry(frame, textvariable=ip4) + entry.grid(row=1, column=1, columnspan=2, sticky="ew") + + label = ttk.Label(frame, text="IPv6") + label.grid(row=2, column=0, padx=PAD, pady=PAD) + ip6 = tk.StringVar(value=f"{interface.ip6}/{interface.ip6mask}") + entry = ttk.Entry(frame, textvariable=ip6) + entry.grid(row=2, column=1, columnspan=2, sticky="ew") + + self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6) + def draw_buttons(self): frame = ttk.Frame(self.top) frame.grid(sticky="ew") @@ -101,16 +172,20 @@ class NodeConfigDialog(Dialog): dialog.show() def click_icon(self): - dialog = IconDialog( - self, self.app, self.canvas_node.name, self.canvas_node.image - ) + dialog = IconDialog(self, self.app, self.node.name, self.canvas_node.image) dialog.show() if dialog.image: self.image = dialog.image self.image_button.config(image=self.image) def config_apply(self): - self.canvas_node.name = self.name.get() + # update core node + self.node.name = self.name.get() + + # update canvas node self.canvas_node.image = self.image - self.canvas_node.canvas.itemconfig(self.canvas_node.id, image=self.image) + + # redraw + self.canvas_node.redraw() + self.destroy() diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index 85a2a18f..4f213e68 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -13,13 +13,14 @@ from coretk.dialogs.mobilityconfig import MobilityConfigDialog class WlanConfigDialog(Dialog): def __init__(self, master, app, canvas_node, config): super().__init__( - master, app, f"{canvas_node.name} Wlan Configuration", modal=True + master, app, f"{canvas_node.core_node.name} Wlan Configuration", modal=True ) self.image = canvas_node.image self.canvas_node = canvas_node + self.node = canvas_node.core_node self.config = config - self.name = tk.StringVar(value=canvas_node.name) + self.name = tk.StringVar(value=self.node.name) self.range_var = tk.StringVar(value=config["range"]) self.bandwidth_var = tk.StringVar(value=config["bandwidth"]) self.delay_var = tk.StringVar(value=config["delay"]) @@ -169,9 +170,7 @@ class WlanConfigDialog(Dialog): dialog.show() def click_icon(self): - dialog = IconDialog( - self, self.app, self.canvas_node.name, self.canvas_node.image - ) + dialog = IconDialog(self, self.app, self.node.name, self.canvas_node.image) dialog.show() if dialog.image: self.image = dialog.image @@ -192,7 +191,7 @@ class WlanConfigDialog(Dialog): # set wireless node configuration here wlanconfig_manager = self.app.core.wlanconfig_management wlanconfig_manager.set_custom_config( - node_id=self.canvas_node.core_id, + node_id=self.node.id, range=basic_range, bandwidth=bandwidth, jitter=jitter, diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 38d67079..3eb002e6 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -155,21 +155,22 @@ class CanvasGraph(tk.Canvas): # draw nodes on the canvas image = NodeUtils.node_icon(core_node.type, core_node.model) - position = core_node.position - node = CanvasNode(position.x, position.y, image, self.master, core_node) + node = CanvasNode(self.master, core_node, image) self.nodes[node.id] = node self.core.canvas_nodes[core_node.id] = node # draw existing links for link in session.links: canvas_node_one = self.core.canvas_nodes[link.node_one_id] + node_one = canvas_node_one.core_node canvas_node_two = self.core.canvas_nodes[link.node_two_id] + node_two = canvas_node_two.core_node is_wired = link.type == core_pb2.LinkType.WIRED edge = CanvasEdge( - canvas_node_one.x_coord, - canvas_node_one.y_coord, - canvas_node_two.x_coord, - canvas_node_two.y_coord, + node_one.position.x, + node_one.position.y, + node_two.position.x, + node_two.position.y, canvas_node_one.id, self, is_wired=is_wired, @@ -402,7 +403,7 @@ class CanvasGraph(tk.Canvas): core_node = self.core.create_node( int(x), int(y), self.node_draw.node_type, self.node_draw.model ) - node = CanvasNode(x, y, self.node_draw.image, self.master, core_node) + node = CanvasNode(self.master, core_node, self.node_draw.image) self.core.canvas_nodes[core_node.id] = node self.nodes[node.id] = node return node @@ -590,19 +591,18 @@ class CanvasEdge: class CanvasNode: - def __init__(self, x, y, image, app, core_node): - self.image = image + def __init__(self, app, core_node, image): self.app = app self.canvas = app.canvas + self.image = image + self.core_node = core_node + x = self.core_node.position.x + y = self.core_node.position.y self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags="node" ) - self.core_node = core_node - self.name = core_node.name - self.x_coord = x - self.y_coord = y self.text_id = self.canvas.create_text( - x, y + 20, text=self.name, tags="nodename" + x, y + 20, text=self.core_node.name, tags="nodename" ) self.antenna_draw = WlanAntennaManager(self.canvas, self.id) self.tooltip = CanvasTooltip(self.canvas) @@ -620,6 +620,10 @@ class CanvasNode: self.wlans = [] self.moving = None + def redraw(self): + self.canvas.itemconfig(self.id, image=self.image) + self.canvas.itemconfig(self.text_id, text=self.core_node.name) + def on_enter(self, event): if self.app.core.is_runtime() and self.app.core.observer: self.tooltip.text.set("waiting...") @@ -640,18 +644,18 @@ class CanvasNode: self.canvas.canvas_action.display_configuration(self) def update_coords(self): - self.x_coord, self.y_coord = self.canvas.coords(self.id) - self.core_node.position.x = int(self.x_coord) - self.core_node.position.y = int(self.y_coord) + x, y = self.canvas.coords(self.id) + self.core_node.position.x = int(x) + self.core_node.position.y = int(y) def click_press(self, event): - logging.debug(f"node click press {self.name}: {event}") + logging.debug(f"node click press {self.core_node.name}: {event}") self.moving = self.canvas.canvas_xy(event) self.canvas.canvas_management.node_select(self) def click_release(self, event): - logging.debug(f"node click release {self.name}: {event}") + logging.debug(f"node click release {self.core_node.name}: {event}") self.update_coords() self.moving = None @@ -681,7 +685,6 @@ class CanvasNode: else: self.canvas.coords(edge.id, x1, y1, new_x, new_y) edge.link_info.recalculate_info() - # self.canvas.core_grpc.throughput_draw.update_throughtput_location(edge) self.canvas.helper.update_wlan_connection( old_x, old_y, new_x, new_y, self.wlans @@ -691,4 +694,4 @@ class CanvasNode: self.canvas.canvas_management.node_select(self, True) def context(self, event): - logging.debug(f"context click {self.name}: {event}") + logging.debug(f"context click {self.core_node.name}: {event}") diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index 0cf54eb8..267c3a3c 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -85,7 +85,8 @@ def load(style): "fieldbackground": Colors.white, "foreground": Colors.black, "padding": (2, 0), - } + }, + "map": {"fieldbackground": [("disabled", Colors.frame)]}, }, "TCombobox": { "configure": { From 07c07da0996eb911e14e5c9ff2e6b783630b9bb0 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 20 Nov 2019 11:20:08 -0800 Subject: [PATCH 243/462] update node config dialog to display fields based on node type, added field for nodes with images --- coretk/coretk/canvasaction.py | 3 +- coretk/coretk/coreclient.py | 6 +++- coretk/coretk/dialogs/nodeconfig.py | 56 +++++++++++++++++++---------- coretk/coretk/nodeutils.py | 16 +++++++-- 4 files changed, 57 insertions(+), 24 deletions(-) diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py index 08aa8c3d..509b9723 100644 --- a/coretk/coretk/canvasaction.py +++ b/coretk/coretk/canvasaction.py @@ -5,6 +5,7 @@ from core.api.grpc import core_pb2 from coretk.dialogs.emaneconfig import EmaneConfiguration from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog +from coretk.nodeutils import NodeUtils class CanvasAction: @@ -16,7 +17,7 @@ class CanvasAction: def display_configuration(self, canvas_node): node_type = canvas_node.core_node.type self.node_to_show_config = canvas_node - if node_type == core_pb2.NodeType.DEFAULT: + if NodeUtils.is_container_node(node_type): self.display_node_configuration() elif node_type == core_pb2.NodeType.WIRELESS_LAN: self.display_wlan_configuration(canvas_node) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 14cc3a11..d9bbd659 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -440,12 +440,16 @@ class CoreClient: """ node_id = self.get_id() position = core_pb2.Position(x=x, y=y) + image = None + if NodeUtils.is_image_node(node_type): + image = "ubuntu:latest" node = core_pb2.Node( id=node_id, type=node_type, name=f"n{node_id}", model=model, position=position, + image=image, ) # set default configuration for wireless node @@ -530,7 +534,7 @@ class CoreClient: def create_interface(self, canvas_node): interface = None core_node = canvas_node.core_node - if NodeUtils.is_interface_node(core_node.type): + if NodeUtils.is_container_node(core_node.type): ifid = len(canvas_node.interfaces) name = f"eth{ifid}" interface = core_pb2.Interface( diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 8a0d4522..4ed4baa6 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -6,9 +6,9 @@ from tkinter import ttk from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeService +from coretk.nodeutils import NodeUtils from coretk.widgets import FrameScroll -DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"} PAD = 5 @@ -49,6 +49,7 @@ class NodeConfigDialog(Dialog): self.image_button = None self.name = tk.StringVar(value=self.node.name) self.type = tk.StringVar(value=self.node.model) + self.container_image = tk.StringVar(value=self.node.image) server = "localhost" if self.node.server: server = self.node.server @@ -86,26 +87,39 @@ class NodeConfigDialog(Dialog): row += 1 # node type field - label = ttk.Label(frame, text="Type") - label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) - combobox = ttk.Combobox( - frame, textvariable=self.type, values=list(DEFAULT_NODES), state="readonly" - ) - combobox.grid(row=row, column=1, sticky="ew") - row += 1 + if NodeUtils.is_model_node(self.node.type): + label = ttk.Label(frame, text="Type") + label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + combobox = ttk.Combobox( + frame, + textvariable=self.type, + values=list(NodeUtils.NODE_MODELS), + state="readonly", + ) + combobox.grid(row=row, column=1, sticky="ew") + row += 1 + + # container image field + if NodeUtils.is_image_node(self.node.type): + label = ttk.Label(frame, text="Image") + label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + entry = ttk.Entry(frame, textvariable=self.container_image) + entry.grid(row=row, column=1, sticky="ew") + row += 1 # server - frame.grid(sticky="ew") - frame.columnconfigure(1, weight=1) - label = ttk.Label(frame, text="Server") - label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) - servers = ["localhost"] - servers.extend(list(sorted(self.app.core.servers.keys()))) - combobox = ttk.Combobox( - frame, textvariable=self.server, values=servers, state="readonly" - ) - combobox.grid(row=row, column=1, sticky="ew") - row += 1 + if NodeUtils.is_container_node(self.node.type): + frame.grid(sticky="ew") + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="Server") + label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + servers = ["localhost"] + servers.extend(list(sorted(self.app.core.servers.keys()))) + combobox = ttk.Combobox( + frame, textvariable=self.server, values=servers, state="readonly" + ) + combobox.grid(row=row, column=1, sticky="ew") + row += 1 # services button = ttk.Button(self.top, text="Services", command=self.click_services) @@ -181,6 +195,10 @@ class NodeConfigDialog(Dialog): def config_apply(self): # update core node self.node.name = self.name.get() + if NodeUtils.is_image_node(self.node.type): + self.node.image = self.container_image.get() + if NodeUtils.is_container_node(self.node.type): + self.node.server = self.server.get() # update canvas node self.canvas_node.image = self.image diff --git a/coretk/coretk/nodeutils.py b/coretk/coretk/nodeutils.py index 6777718a..380c9a0b 100644 --- a/coretk/coretk/nodeutils.py +++ b/coretk/coretk/nodeutils.py @@ -44,11 +44,21 @@ class NodeUtils: NODES = [] NETWORK_NODES = [] NODE_ICONS = {} - INTERFACE_NODE = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} + CONTAINER_NODES = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} + IMAGE_NODES = {NodeType.DOCKER, NodeType.LXC} + NODE_MODELS = {"router", "host", "PC", "mdr", "prouter"} @classmethod - def is_interface_node(cls, node_type): - return node_type in cls.INTERFACE_NODE + def is_container_node(cls, node_type): + return node_type in cls.CONTAINER_NODES + + @classmethod + def is_model_node(cls, node_type): + return node_type == NodeType.DEFAULT + + @classmethod + def is_image_node(cls, node_type): + return node_type in cls.IMAGE_NODES @classmethod def node_icon(cls, node_type, model): From 2e121a334412ec5c69178fb95a50996ed3e0e889 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 20 Nov 2019 16:52:02 -0800 Subject: [PATCH 244/462] service config, trying out some new icons --- coretk/coretk/coreclient.py | 26 ++++++++------ coretk/coretk/dialogs/nodeservice.py | 15 ++++++-- coretk/coretk/dialogs/serviceconfiguration.py | 17 +++++---- coretk/coretk/icons/host.png | Bin 0 -> 642 bytes coretk/coretk/icons/hub.png | Bin 0 -> 1108 bytes coretk/coretk/icons/lanswitch.png | Bin 0 -> 669 bytes coretk/coretk/icons/marker.png | Bin 0 -> 667 bytes coretk/coretk/icons/oval.png | Bin 0 -> 1236 bytes coretk/coretk/icons/pc.png | Bin 0 -> 517 bytes coretk/coretk/icons/rectangle.png | Bin 0 -> 157 bytes coretk/coretk/icons/rj45.png | Bin 0 -> 696 bytes coretk/coretk/icons/router.png | Bin 0 -> 1462 bytes coretk/coretk/icons/select.png | Bin 0 -> 348 bytes coretk/coretk/icons/start.png | Bin 0 -> 588 bytes coretk/coretk/icons/stop.png | Bin 0 -> 145 bytes coretk/coretk/icons/text.png | Bin 0 -> 146 bytes coretk/coretk/{icons => oldicons}/host.gif | Bin coretk/coretk/{icons => oldicons}/hub.gif | Bin .../coretk/{icons => oldicons}/lanswitch.gif | Bin coretk/coretk/{icons => oldicons}/oval.gif | Bin coretk/coretk/{icons => oldicons}/pc.gif | Bin .../coretk/{icons => oldicons}/rectangle.gif | Bin coretk/coretk/{icons => oldicons}/rj45.gif | Bin coretk/coretk/{icons => oldicons}/router.gif | Bin coretk/coretk/{icons => oldicons}/select.gif | Bin coretk/coretk/{icons => oldicons}/start.gif | Bin coretk/coretk/{icons => oldicons}/stop.gif | Bin coretk/coretk/{icons => oldicons}/text.gif | Bin coretk/coretk/servicenodeconfig.py | 33 ++++++++++++++++++ coretk/coretk/toolbar.py | 3 ++ 30 files changed, 72 insertions(+), 22 deletions(-) create mode 100644 coretk/coretk/icons/host.png create mode 100644 coretk/coretk/icons/hub.png create mode 100644 coretk/coretk/icons/lanswitch.png create mode 100644 coretk/coretk/icons/marker.png create mode 100644 coretk/coretk/icons/oval.png create mode 100644 coretk/coretk/icons/pc.png create mode 100644 coretk/coretk/icons/rectangle.png create mode 100644 coretk/coretk/icons/rj45.png create mode 100644 coretk/coretk/icons/router.png create mode 100644 coretk/coretk/icons/select.png create mode 100644 coretk/coretk/icons/start.png create mode 100644 coretk/coretk/icons/stop.png create mode 100644 coretk/coretk/icons/text.png rename coretk/coretk/{icons => oldicons}/host.gif (100%) rename coretk/coretk/{icons => oldicons}/hub.gif (100%) rename coretk/coretk/{icons => oldicons}/lanswitch.gif (100%) rename coretk/coretk/{icons => oldicons}/oval.gif (100%) rename coretk/coretk/{icons => oldicons}/pc.gif (100%) rename coretk/coretk/{icons => oldicons}/rectangle.gif (100%) rename coretk/coretk/{icons => oldicons}/rj45.gif (100%) rename coretk/coretk/{icons => oldicons}/router.gif (100%) rename coretk/coretk/{icons => oldicons}/select.gif (100%) rename coretk/coretk/{icons => oldicons}/start.gif (100%) rename coretk/coretk/{icons => oldicons}/stop.gif (100%) rename coretk/coretk/{icons => oldicons}/text.gif (100%) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 14cc3a11..9b486ccf 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -123,6 +123,9 @@ class CoreClient: ) def join_session(self, session_id): + self.master.config(cursor="watch") + self.master.update() + # update session and title self.session_id = session_id self.master.title(f"CORE Session({self.session_id})") @@ -193,6 +196,7 @@ class CoreClient: self.app.toolbar.runtime_frame.tkraise() else: self.app.toolbar.design_frame.tkraise() + self.master.config(cursor="") def is_runtime(self): return self.state == core_pb2.SessionState.RUNTIME @@ -296,7 +300,8 @@ class CoreClient: mobility_configs = self.get_mobility_configs_proto() emane_model_configs = self.get_emane_model_configs_proto() hooks = list(self.hooks.values()) - # service_configs = self.get_service_config_proto() + service_configs = self.get_service_config_proto() + print(service_configs) # service_file_configs = self.get_service_file_config_proto() self.created_links.clear() self.created_nodes.clear() @@ -313,7 +318,7 @@ class CoreClient: emane_config=emane_config, emane_model_configs=emane_model_configs, mobility_configs=mobility_configs, - # service_configs=service_configs + service_configs=service_configs, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) @@ -638,14 +643,15 @@ class CoreClient: service_configs, ) in self.serviceconfig_manager.configurations.items(): for service, config in service_configs.items(): - config = core_pb2.ServiceConfig( - node_id=node_id, - service=service, - startup=config.startup, - validate=config.validate, - shutdown=config.shutdown, - ) - configs.append(config) + if service in self.serviceconfig_manager.current_services[node_id]: + config = core_pb2.ServiceConfig( + node_id=node_id, + service=service, + startup=config.startup, + validate=config.validate, + shutdown=config.shutdown, + ) + configs.append(config) return configs def get_service_file_config_proto(self): diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 67d9a0b3..35c55b75 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -19,9 +19,11 @@ class NodeService(Dialog): self.current = None if services is None: services = set( - app.core.serviceconfig_manager.configurations[self.node_id].keys() + app.core.serviceconfig_manager.current_services[self.node_id] ) self.current_services = services + self.service_manager = self.app.core.serviceconfig_manager + self.service_file_manager = self.app.core.servicefileconfig_manager self.draw() def draw(self): @@ -76,9 +78,16 @@ class NodeService(Dialog): def service_clicked(self, name, var): if var.get() and name not in self.current_services: - self.current_services.add(name) + if self.service_manager.node_new_service_configuration(self.node_id, name): + self.current_services.add(name) + else: + for checkbutton in self.services.frame.winfo_children(): + if name == checkbutton.cget("text"): + checkbutton.config(variable=tk.BooleanVar(value=False)) + elif not var.get() and name in self.current_services: self.current_services.remove(name) + self.service_manager.current_services[self.node_id].remove(name) self.current.listbox.delete(0, tk.END) for name in sorted(self.current_services): self.current.listbox.insert(tk.END, name) @@ -90,7 +99,7 @@ class NodeService(Dialog): master=self, app=self.app, service_name=self.current.listbox.get(current_selection[0]), - canvas_node=self.canvas_node, + node_id=self.node_id, ) dialog.show() else: diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 195dc913..bb1fd858 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -11,11 +11,11 @@ from coretk.widgets import ListboxScroll class ServiceConfiguration(Dialog): - def __init__(self, master, app, service_name, canvas_node): + def __init__(self, master, app, service_name, node_id): super().__init__(master, app, f"{service_name} service", modal=True) self.app = app - self.canvas_node = canvas_node - self.node_id = canvas_node.core_node.id + self.service_manager = app.core.serviceconfig_manager + self.node_id = node_id self.service_name = service_name self.radiovar = tk.IntVar() self.radiovar.set(2) @@ -51,16 +51,15 @@ class ServiceConfiguration(Dialog): # create nodes and links in definition state for getting and setting service file self.app.core.create_nodes_and_links() # load data from local memory - serviceconfig_manager = self.app.core.serviceconfig_manager - if self.service_name in serviceconfig_manager.configurations[self.node_id]: - service_config = serviceconfig_manager.configurations[self.node_id][ + if self.service_name in self.service_manager.configurations[self.node_id]: + service_config = self.service_manager.configurations[self.node_id][ self.service_name ] else: - serviceconfig_manager.node_custom_service_configuration( + self.service_manager.node_custom_service_configuration( self.node_id, self.service_name ) - service_config = serviceconfig_manager.configurations[self.node_id][ + service_config = self.service_manager.configurations[self.node_id][ self.service_name ] self.dependencies = [x for x in service_config.dependencies] @@ -363,7 +362,7 @@ class ServiceConfiguration(Dialog): 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") - self.app.core.serviceconfig_manager.node_service_custom_configuration( + self.service_manager.node_service_custom_configuration( self.node_id, self.service_name, startup_commands, diff --git a/coretk/coretk/icons/host.png b/coretk/coretk/icons/host.png new file mode 100644 index 0000000000000000000000000000000000000000..e6efda088f82d791ba5e3122f707581e05644165 GIT binary patch literal 642 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 literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/rectangle.png b/coretk/coretk/icons/rectangle.png new file mode 100644 index 0000000000000000000000000000000000000000..f8a9581ee1df0f3e5795b74658e731af2f23f45b GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCwj^(N7l!{JxM1({$v}}HPZ!6K zjK;U;H*z*OFt8lFek|vZQSPBr_k0XQT6pK2)Y~n~%U-Q9ov({XW@ gD>xtnLwy-z)n8@{hR1x1fTlBey85}Sb4q9e09ofZqyPW_ literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/rj45.png b/coretk/coretk/icons/rj45.png new file mode 100644 index 0000000000000000000000000000000000000000..baf4f9e80fb8632f29c5951d6ed6dbc32a1c5a98 GIT binary patch literal 696 zcmV;p0!RIcP)CJt(WY!s>`r^`w0ld50S0zSU5Da)CZ$p8R)W!aUq{-7&ru_QNO*_@KJhFH{MNp7G+xwy+Y z+QpxM$x&?5aA)xi5P*10FqgGhk{ig#$P*x1{0T5*Vw1$O7EAgG82OlFf#VfRdIQYj zE@wv8VmX8(B=0gG!V!x4E3xrv|5jDoU7t4jx(CxA-@I`rFJFO`wX0sO*IU{8@^bZ) zwwI_0aJ_ z29g#uK(c00&KW{&VDoi#)*%&=9x?z<)|}(w#+!q4+J4jq3^mUeB3>3KF0Dwf{+#b{hh*eq+R@WET@BR))-LqDhClCVvixQJ z;u+tv`L4*3{Eh5?v-Aekzr;Rn4pk41q%8deS_4N-w1J)NA?$1qlYWlj_)cI^Xbl`S et$~)7CFc*V=k<6^q$m~u0000b3C7hBeV5RHriLh{c1wR?Td-(=S4m4SE3w;XYOxmiS;|kf%kIt`AKF6Q znVq?_Z3&6}-u9ex&-u>Yx%ZrVFEEEW{J%jn;UpLd%43eqD=cXnBqpB*R1i=`zyf+W#3xi$K619cZ75$3 zc_mO436?sP^dSaz190XWd;%!~8XZg@wAZyK><+e_Ky^pWc4j&Zpv1~{&EnNyCiLFu z6>*&a@&3-*Q_Xhx2q)k>wofT5z10BXy(0FWA`iAzga80deGv@Y9}r~3M@OHz_wL0F z7c)ZUB1YBG>Uo@zPXeeCv3I*luxHg@A$vYy$LBw5opIO<-W=elsUKh4Jt03^tv zRz$0#w~drJ=QBh63GyUA!*H`Ig>xwl6R2vfTQX6ko(?eW^+)}NmEJG`2k`++zrxNW z3XpN^H&W;l2#3QG6YrRPPBy``D;NptHXU68J(WML1F+4jI(J@~!39n(etm!`^eGeDZO#wy_IXJ5uvZ%DYZ zn!zQzu)Sgn)-JXT3kGyEV6=#riv;!*d9Y{IZY(Nvi*#fc7>M4d%yE+Pi)7B*N`GhXtyn>PBJp{e=a7v2sH~fJSV20`I-ii>AH^l4?>cunCfC5>0&(p@Y8$ zJvZGNkW~3ROXl}YXMaCu8O+XkKx0kFr=-f~b+T-MFIIU8z#o?0o;FO_n`T&tI4E-C(zV`RW+xGMi9}7RNkrl;NNMRuvXquaB;I1D zaz_i(N=W2}+VpC168%m6>F3`mixebXr2y7i*^gz$Fg$u4FD_aE2BSHC62pIA7YPLd z4v#Lp`}P_C8KG_5{sTvQZHR#*5V*CQT+o6K95*K(I849!Y~=OC^m-&%qBv6@gJ~~- zf_!{DX23~e`a)CEA9ne>Mr;qCPHcbFUp{eTwiCcoo5Qa$@z*kO!|9sS!})TUk!RL$ zI4t$NbY%@OZDhbl#9ju<@%Vf_3Z@|d{VZuck}99~#VUJe;+b_0bNIjGU*+KI{#Xi` Q*8l(j07*qoM6N<$g0Q%$(EtDd literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/select.png b/coretk/coretk/icons/select.png new file mode 100644 index 0000000000000000000000000000000000000000..9063d308a02c5693c78f9cf680d8a153bbcf7d36 GIT binary patch literal 348 zcmV-i0i*tjP)# z55NoPH5!SIN<`u(k+5s+Rm{jtWM+0Jv-egv=T6RdPUdFLP`=t|Rh&0o0q2;A2?TU7 z9RnETkW0+O1Ol!w7ZV7$#^Shup8_D@7Rxb#fP1XP1Ogth-UR3m0RmpI5fcdbz*bBk z;0rr-f$thXNDJEgpJb{62tx|!;aT#spVIvb7VtVs2fd1FJEJ713!LFD-FLCn6zJxX zpQN$FT&z6~V1VOXY!+`R@Q(Q=z$f+#;-|7Mj+y{v_j*(C7+uJ^*o^^f=YSg# uehylax$uWVziWwSc2W_bcR+r-7<`5MpOLY(tM5>z<9W>Gwf-lK?pMy9Ex!B0vJuUZJ|9SuW ze(=3}1OkCTAWKxYurX8~6fISOQ^0mlGyP&+iK?|VmrJ#!_og2i1sX%;!Bop@V8F?E z8>b$LKD_nm;!3A5GCd_)s$@Srz(7heX$s8(KiB5pK$&MT3gCa|yHPR)-30y%f; zfvmAEuw`@twawoJW?dk6;v~ge^F#*^*{<%0lND0C_fV|P&^LSml-n(J*RD1B;qw$K zQ>dN0oE_u*CDuoeQJlSpE+2Q&@8uR!rG5%mC+WL*337R>-TtmorO2N>M{#P7XzxC2 zwcfu2(f$JzZr&w#`iz}ED=mLRGqzHbv(nN7>GC_`Ygb5DK3eJXCm=%W;R4N>X=t~s z)p`+-)o;X=agwFy*1MexB(Gi)Umrv2U!3&26G#$T_wUf0zYUT)JI0BCeE!7uiBZzG zZ(P*>hIN5UWEYw0000#8 b_z*AuI)zzdTjAtdpot8gu6{1-oD!M<$_gx= literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/text.png b/coretk/coretk/icons/text.png new file mode 100644 index 0000000000000000000000000000000000000000..4933dabbc40d0694ad46369a884685bb463f1f04 GIT binary patch literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjY)RhkE)4%caKYZ?lYt`co-U3d z9-YYv609r?%3>S;Uon~7>2#QbNzA6`M~(ZI^O^6MgAUfS2|ez3W+$;V;Yw2=cUv>- p4KLRmhVY-x0sj|h`?@PIFf7}|rg+lx(=(uv44$rjF6*2UngH*%FU0@= literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/host.gif b/coretk/coretk/oldicons/host.gif similarity index 100% rename from coretk/coretk/icons/host.gif rename to coretk/coretk/oldicons/host.gif diff --git a/coretk/coretk/icons/hub.gif b/coretk/coretk/oldicons/hub.gif similarity index 100% rename from coretk/coretk/icons/hub.gif rename to coretk/coretk/oldicons/hub.gif diff --git a/coretk/coretk/icons/lanswitch.gif b/coretk/coretk/oldicons/lanswitch.gif similarity index 100% rename from coretk/coretk/icons/lanswitch.gif rename to coretk/coretk/oldicons/lanswitch.gif diff --git a/coretk/coretk/icons/oval.gif b/coretk/coretk/oldicons/oval.gif similarity index 100% rename from coretk/coretk/icons/oval.gif rename to coretk/coretk/oldicons/oval.gif diff --git a/coretk/coretk/icons/pc.gif b/coretk/coretk/oldicons/pc.gif similarity index 100% rename from coretk/coretk/icons/pc.gif rename to coretk/coretk/oldicons/pc.gif diff --git a/coretk/coretk/icons/rectangle.gif b/coretk/coretk/oldicons/rectangle.gif similarity index 100% rename from coretk/coretk/icons/rectangle.gif rename to coretk/coretk/oldicons/rectangle.gif diff --git a/coretk/coretk/icons/rj45.gif b/coretk/coretk/oldicons/rj45.gif similarity index 100% rename from coretk/coretk/icons/rj45.gif rename to coretk/coretk/oldicons/rj45.gif diff --git a/coretk/coretk/icons/router.gif b/coretk/coretk/oldicons/router.gif similarity index 100% rename from coretk/coretk/icons/router.gif rename to coretk/coretk/oldicons/router.gif diff --git a/coretk/coretk/icons/select.gif b/coretk/coretk/oldicons/select.gif similarity index 100% rename from coretk/coretk/icons/select.gif rename to coretk/coretk/oldicons/select.gif diff --git a/coretk/coretk/icons/start.gif b/coretk/coretk/oldicons/start.gif similarity index 100% rename from coretk/coretk/icons/start.gif rename to coretk/coretk/oldicons/start.gif diff --git a/coretk/coretk/icons/stop.gif b/coretk/coretk/oldicons/stop.gif similarity index 100% rename from coretk/coretk/icons/stop.gif rename to coretk/coretk/oldicons/stop.gif diff --git a/coretk/coretk/icons/text.gif b/coretk/coretk/oldicons/text.gif similarity index 100% rename from coretk/coretk/icons/text.gif rename to coretk/coretk/oldicons/text.gif diff --git a/coretk/coretk/servicenodeconfig.py b/coretk/coretk/servicenodeconfig.py index 98de9034..3473e1d8 100644 --- a/coretk/coretk/servicenodeconfig.py +++ b/coretk/coretk/servicenodeconfig.py @@ -2,15 +2,23 @@ service node configuration """ import logging +from tkinter import messagebox + +import grpc class ServiceNodeConfig: def __init__(self, app): self.app = app # dict(node_id:dict(service:node_service_config_proto)) + # maps node to all of its service configuration self.configurations = {} + # dict(node_id:set(str)) + # maps node to current configurations + self.current_services = {} self.default_services = {} + # todo rewrite, no need self.default services def node_default_services_configuration(self, node_id, node_model): """ set the default configurations for the default services of a node @@ -20,6 +28,7 @@ class ServiceNodeConfig: """ session_id = self.app.core.session_id client = self.app.core.client + if len(self.default_services) == 0: response = client.get_service_defaults(session_id) logging.info("session default services: %s", response) @@ -28,6 +37,7 @@ class ServiceNodeConfig: self.configurations[node_id] = {} + self.current_services[node_id] = set() for default in self.default_services[node_model]: response = client.get_node_service(session_id, node_id, default) logging.info( @@ -36,6 +46,29 @@ class ServiceNodeConfig: response, ) self.configurations[node_id][default] = response.service + self.current_services[node_id].add(default) + + def node_new_service_configuration(self, node_id, service_name): + """ + store node's configuration if a new service is added from the GUI + + :param int node_id: node id + :param str service_name: service name + :return: nothing + """ + try: + config = self.app.core.get_node_service(node_id, service_name) + except grpc.RpcError: + messagebox.showerror("Service problem", "Service not found") + return False + if node_id not in self.configurations: + self.configurations[node_id] = {} + if node_id not in self.current_services: + self.current_services[node_id] = set() + if service_name not in self.configurations[node_id]: + self.configurations[node_id][service_name] = config + self.current_services[node_id].add(service_name) + return True def node_custom_service_configuration(self, node_id, service_name): self.configurations[node_id][service_name] = self.app.core.get_node_service( diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index bb1803ea..834f7b39 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -196,9 +196,12 @@ class Toolbar(ttk.Frame): :return: nothing """ logging.debug("clicked start button") + self.master.config(cursor="watch") + self.master.update() self.app.canvas.mode = GraphMode.SELECT self.app.core.start_session() self.runtime_frame.tkraise() + self.master.config(cursor="") def click_link(self): logging.debug("Click LINK button") From 6035032e960617ce73143a66d5a0bf754342c9be Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 20 Nov 2019 23:16:04 -0800 Subject: [PATCH 245/462] removed wlan config class and canvas action class --- coretk/coretk/canvasaction.py | 45 -------- coretk/coretk/coreclient.py | 31 +++--- coretk/coretk/dialogs/nodeconfig.py | 8 +- coretk/coretk/dialogs/wlanconfig.py | 166 +++------------------------- coretk/coretk/graph.py | 96 +++++++++------- coretk/coretk/widgets.py | 3 +- coretk/coretk/wlannodeconfig.py | 37 ------- 7 files changed, 92 insertions(+), 294 deletions(-) delete mode 100644 coretk/coretk/canvasaction.py delete mode 100644 coretk/coretk/wlannodeconfig.py diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py deleted file mode 100644 index 509b9723..00000000 --- a/coretk/coretk/canvasaction.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -canvas graph action -""" -from core.api.grpc import core_pb2 -from coretk.dialogs.emaneconfig import EmaneConfiguration -from coretk.dialogs.nodeconfig import NodeConfigDialog -from coretk.dialogs.wlanconfig import WlanConfigDialog -from coretk.nodeutils import NodeUtils - - -class CanvasAction: - def __init__(self, master, canvas): - self.master = master - self.canvas = canvas - self.node_to_show_config = None - - def display_configuration(self, canvas_node): - node_type = canvas_node.core_node.type - self.node_to_show_config = canvas_node - if NodeUtils.is_container_node(node_type): - self.display_node_configuration() - elif node_type == core_pb2.NodeType.WIRELESS_LAN: - self.display_wlan_configuration(canvas_node) - elif node_type == core_pb2.NodeType.EMANE: - self.display_emane_configuration() - - def display_node_configuration(self): - dialog = NodeConfigDialog(self.master, self.master, self.node_to_show_config) - dialog.show() - self.node_to_show_config = None - - def display_wlan_configuration(self, canvas_node): - wlan_config = self.master.core.wlanconfig_management.configurations[ - canvas_node.core_node.id - ] - dialog = WlanConfigDialog( - self.master, self.master, self.node_to_show_config, wlan_config - ) - dialog.show() - self.node_to_show_config = None - - def display_emane_configuration(self): - app = self.canvas.core.app - dialog = EmaneConfiguration(self.master, app, self.node_to_show_config) - dialog.show() diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index d9bbd659..44b8e38a 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -12,7 +12,6 @@ from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.nodeutils import NodeDraw, NodeUtils from coretk.servicefileconfig import ServiceFileConfig from coretk.servicenodeconfig import ServiceNodeConfig -from coretk.wlannodeconfig import WlanNodeConfig OBSERVERS = { "processes": "ps", @@ -70,7 +69,7 @@ class CoreClient: self.reusable = [] self.preexisting = set() self.interfaces_manager = InterfaceManager() - self.wlanconfig_management = WlanNodeConfig() + self.wlan_configs = {} self.mobilityconfig_management = MobilityNodeConfig() self.emaneconfig_management = EmaneModelNodeConfig(app) self.emane_config = None @@ -133,7 +132,7 @@ class CoreClient: self.canvas_nodes.clear() self.links.clear() self.hooks.clear() - self.wlanconfig_management.configurations.clear() + self.wlan_configs.clear() self.mobilityconfig_management.configurations.clear() self.emane_config = None @@ -155,9 +154,7 @@ class CoreClient: if node.type == core_pb2.NodeType.WIRELESS_LAN: response = self.client.get_wlan_config(self.session_id, node.id) logging.debug("wlan config(%s): %s", node.id, response) - node_config = response.config - config = {x: node_config[x].value for x in node_config} - self.wlanconfig_management.configurations[node.id] = config + self.wlan_configs[node.id] = response.config # get mobility configs response = self.client.get_mobility_configs(self.session_id) @@ -453,7 +450,6 @@ class CoreClient: ) # set default configuration for wireless node - self.wlanconfig_management.set_default_config(node_type, node_id) self.mobilityconfig_management.set_default_configuration(node_type, node_id) # set default emane configuration for emane node @@ -520,8 +516,8 @@ class CoreClient: for i in node_ids: if i in self.mobilityconfig_management.configurations: self.mobilityconfig_management.configurations.pop(i) - if i in self.wlanconfig_management.configurations: - self.wlanconfig_management.configurations.pop(i) + if i in self.wlan_configs: + del self.wlan_configs[i] # delete emane configurations for i in node_interface_pairs: @@ -606,11 +602,10 @@ class CoreClient: def get_wlan_configs_proto(self): configs = [] - wlan_configs = self.wlanconfig_management.configurations - for node_id in wlan_configs: - config = wlan_configs[node_id] - config_proto = core_pb2.WlanConfig(node_id=node_id, config=config) - configs.append(config_proto) + for node_id, config in self.wlan_configs.items(): + config = {x: config[x].value for x in config} + wlan_config = core_pb2.WlanConfig(node_id=node_id, config=config) + configs.append(wlan_config) return configs def get_mobility_configs_proto(self): @@ -669,3 +664,11 @@ class CoreClient: def run(self, node_id): logging.info("running node(%s) cmd: %s", node_id, self.observer) return self.client.node_command(self.session_id, node_id, self.observer).output + + def get_wlan_config(self, node_id): + config = self.wlan_configs.get(node_id) + if not config: + response = self.client.get_wlan_config(self.session_id, node_id) + config = response.config + self.wlan_configs[node_id] = config + return config diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 4ed4baa6..8b9fcfab 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -107,8 +107,8 @@ class NodeConfigDialog(Dialog): entry.grid(row=row, column=1, sticky="ew") row += 1 - # server if NodeUtils.is_container_node(self.node.type): + # server frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Server") @@ -121,9 +121,9 @@ class NodeConfigDialog(Dialog): combobox.grid(row=row, column=1, sticky="ew") row += 1 - # services - button = ttk.Button(self.top, text="Services", command=self.click_services) - button.grid(sticky="ew", pady=PAD) + # services + button = ttk.Button(self.top, text="Services", command=self.click_services) + button.grid(sticky="ew", pady=PAD) # interfaces if self.canvas_node.interfaces: diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index 4f213e68..3973efb5 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -2,152 +2,33 @@ wlan configuration """ -import tkinter as tk from tkinter import ttk from coretk.dialogs.dialog import Dialog -from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog +from coretk.widgets import ConfigFrame + +PAD = 5 class WlanConfigDialog(Dialog): - def __init__(self, master, app, canvas_node, config): + def __init__(self, master, app, canvas_node): super().__init__( master, app, f"{canvas_node.core_node.name} Wlan Configuration", modal=True ) - self.image = canvas_node.image self.canvas_node = canvas_node self.node = canvas_node.core_node - self.config = config - - self.name = tk.StringVar(value=self.node.name) - self.range_var = tk.StringVar(value=config["range"]) - self.bandwidth_var = tk.StringVar(value=config["bandwidth"]) - self.delay_var = tk.StringVar(value=config["delay"]) - self.loss_var = tk.StringVar(value=config["error"]) - self.jitter_var = tk.StringVar(value=config["jitter"]) - self.ip4_subnet = tk.StringVar() - self.ip6_subnet = tk.StringVar() - self.image_button = None + self.config_frame = None + self.config = self.app.core.get_wlan_config(self.node.id) self.draw() def draw(self): self.top.columnconfigure(0, weight=1) - self.draw_name_config() - self.draw_wlan_config() - self.draw_subnet() - self.draw_wlan_buttons() + self.config_frame = ConfigFrame(self.top, self.app, self.config, borderwidth=0) + self.config_frame.draw_config() + self.config_frame.grid(sticky="nsew", pady=PAD) self.draw_apply_buttons() - def draw_name_config(self): - """ - draw image modification part - - :return: nothing - """ - frame = ttk.Frame(self.top) - frame.grid(pady=2, sticky="ew") - frame.columnconfigure(0, weight=1) - - entry = ttk.Entry(frame, textvariable=self.name) - entry.grid(row=0, column=0, padx=2, sticky="ew") - - self.image_button = ttk.Button(frame, image=self.image, command=self.click_icon) - self.image_button.grid(row=0, column=1, padx=3) - - def draw_wlan_config(self): - """ - create wireless configuration table - - :return: nothing - """ - label = ttk.Label(self.top, text="Wireless") - label.grid(sticky="w", pady=2) - - frame = ttk.Frame(self.top) - frame.grid(pady=2, sticky="ew") - for i in range(2): - frame.columnconfigure(i, weight=1) - - label = ttk.Label( - frame, - text=( - "The basic range model calculates on/off " - "connectivity based on pixel distance between nodes." - ), - ) - label.grid(row=0, columnspan=2, pady=2, sticky="ew") - - label = ttk.Label(frame, text="Range") - label.grid(row=1, column=0, sticky="w") - entry = ttk.Entry(frame, textvariable=self.range_var) - entry.grid(row=1, column=1, sticky="ew") - - label = ttk.Label(frame, text="Bandwidth (bps)") - label.grid(row=2, column=0, sticky="w") - entry = ttk.Entry(frame, textvariable=self.bandwidth_var) - entry.grid(row=2, column=1, sticky="ew") - - label = ttk.Label(frame, text="Delay (us)") - label.grid(row=3, column=0, sticky="w") - entry = ttk.Entry(frame, textvariable=self.delay_var) - entry.grid(row=3, column=1, sticky="ew") - - label = ttk.Label(frame, text="Loss (%)") - label.grid(row=4, column=0, sticky="w") - entry = ttk.Entry(frame, textvariable=self.loss_var) - entry.grid(row=4, column=1, sticky="ew") - - label = ttk.Label(frame, text="Jitter (us)") - label.grid(row=5, column=0, sticky="w") - entry = ttk.Entry(frame, textvariable=self.jitter_var) - entry.grid(row=5, column=1, sticky="ew") - - def draw_subnet(self): - """ - create the entries for ipv4 subnet and ipv6 subnet - - :return: nothing - """ - - frame = ttk.Frame(self.top) - frame.grid(pady=3, sticky="ew") - frame.columnconfigure(1, weight=1) - frame.columnconfigure(3, weight=1) - - label = ttk.Label(frame, text="IPv4 Subnet") - label.grid(row=0, column=0, sticky="w") - entry = ttk.Entry(frame, textvariable=self.ip4_subnet) - entry.grid(row=0, column=1, sticky="ew") - - label = ttk.Label(frame, text="IPv6 Subnet") - label.grid(row=0, column=2, sticky="w") - entry = ttk.Entry(frame, textvariable=self.ip6_subnet) - entry.grid(row=0, column=3, sticky="ew") - - def draw_wlan_buttons(self): - """ - create wireless node options - - :return: - """ - - frame = ttk.Frame(self.top) - frame.grid(pady=2, sticky="ew") - for i in range(3): - frame.columnconfigure(i, weight=1) - - button = ttk.Button( - frame, text="ns-2 mobility script...", command=self.click_mobility - ) - button.grid(row=0, column=0, padx=2, sticky="ew") - - button = ttk.Button(frame, text="Link to all routers") - button.grid(row=0, column=1, padx=2, sticky="ew") - - button = ttk.Button(frame, text="Choose WLAN members") - button.grid(row=0, column=2, padx=2, sticky="ew") - def draw_apply_buttons(self): """ create node configuration options @@ -160,42 +41,21 @@ class WlanConfigDialog(Dialog): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, padx=2, sticky="ew") + button.grid(row=0, column=0, padx=PAD, sticky="ew") button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, padx=2, sticky="ew") + button.grid(row=0, column=1, sticky="ew") def click_mobility(self): dialog = MobilityConfigDialog(self, self.app, self.canvas_node) dialog.show() - def click_icon(self): - dialog = IconDialog(self, self.app, self.node.name, self.canvas_node.image) - dialog.show() - if dialog.image: - self.image = dialog.image - self.image_button.config(image=self.image) - def click_apply(self): """ retrieve user's wlan configuration and store the new configuration values :return: nothing """ - basic_range = self.range_var.get() - bandwidth = self.bandwidth_var.get() - delay = self.delay_var.get() - loss = self.loss_var.get() - jitter = self.jitter_var.get() - - # set wireless node configuration here - wlanconfig_manager = self.app.core.wlanconfig_management - wlanconfig_manager.set_custom_config( - node_id=self.node.id, - range=basic_range, - bandwidth=bandwidth, - jitter=jitter, - delay=delay, - error=loss, - ) + self.config_frame.parse_config() + self.app.core.wlan_configs[self.node.id] = self.config self.destroy() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 3eb002e6..86ce617e 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -5,8 +5,11 @@ import tkinter as tk from PIL import ImageTk from core.api.grpc import core_pb2 -from coretk.canvasaction import CanvasAction +from core.api.grpc.core_pb2 import NodeType from coretk.canvastooltip import CanvasTooltip +from coretk.dialogs.mobilityconfig import MobilityConfigDialog +from coretk.dialogs.nodeconfig import NodeConfigDialog +from coretk.dialogs.wlanconfig import WlanConfigDialog from coretk.graph_helper import GraphHelper, WlanAntennaManager from coretk.images import Images from coretk.linkinfo import LinkInfo, Throughput @@ -38,24 +41,21 @@ class CanvasGraph(tk.Canvas): kwargs["highlightthickness"] = 0 super().__init__(master, cnf, **kwargs) self.mode = GraphMode.SELECT - self.node_draw = None self.selected = None - self.node_context = None + self.node_draw = None + self.context = None self.nodes = {} self.edges = {} self.drawing_edge = None self.grid = None self.meters_per_pixel = 1.5 self.canvas_management = CanvasComponentManagement(self, core) - self.canvas_action = CanvasAction(master, self) - self.setup_menus() self.setup_bindings() self.draw_grid() self.core = core self.helper = GraphHelper(self, core) self.throughput_draw = Throughput(self, core) self.wireless_draw = WirelessConnection(self, core) - self.is_node_context_opened = False # background related self.wallpaper_id = None @@ -66,21 +66,28 @@ class CanvasGraph(tk.Canvas): self.show_grid = tk.BooleanVar(value=True) self.adjust_to_dim = tk.BooleanVar(value=False) - def setup_menus(self): - self.node_context = tk.Menu(self.master) - self.node_context.add_command( - label="Configure", command=self.canvas_action.display_node_configuration - ) - self.node_context.add_command(label="Select adjacent") - self.node_context.add_command(label="Create link to") - self.node_context.add_command(label="Assign to") - self.node_context.add_command(label="Move to") - self.node_context.add_command(label="Cut") - self.node_context.add_command(label="Copy") - self.node_context.add_command(label="Paste") - self.node_context.add_command(label="Delete") - self.node_context.add_command(label="Hide") - self.node_context.add_command(label="Services") + def create_node_context(self, canvas_node): + node = canvas_node.core_node + context = tk.Menu(self.master) + context.add_command(label="Configure", command=canvas_node.show_config) + if node.type == NodeType.WIRELESS_LAN: + context.add_command( + label="WLAN Config", command=canvas_node.show_wlan_config + ) + context.add_command( + label="Mobility Config", command=canvas_node.show_mobility_config + ) + context.add_command(label="Select adjacent", state=tk.DISABLED) + context.add_command(label="Create link to", state=tk.DISABLED) + context.add_command(label="Assign to", state=tk.DISABLED) + context.add_command(label="Move to", state=tk.DISABLED) + context.add_command(label="Cut", state=tk.DISABLED) + context.add_command(label="Copy", state=tk.DISABLED) + context.add_command(label="Paste", state=tk.DISABLED) + context.add_command(label="Delete", state=tk.DISABLED) + context.add_command(label="Hide", state=tk.DISABLED) + context.add_command(label="Services", state=tk.DISABLED) + return context def reset_and_redraw(self, session): """ @@ -97,7 +104,6 @@ class CanvasGraph(tk.Canvas): self.mode = GraphMode.SELECT self.node_draw = None self.selected = None - self.node_context = None self.nodes.clear() self.edges.clear() self.drawing_edge = None @@ -112,7 +118,7 @@ class CanvasGraph(tk.Canvas): self.bind("", self.click_press) self.bind("", self.click_release) self.bind("", self.click_motion) - self.bind("", self.context) + self.bind("", self.click_context) self.bind("", self.press_delete) def draw_grid(self, width=1000, height=800): @@ -254,9 +260,9 @@ class CanvasGraph(tk.Canvas): :param event: mouse event :return: nothing """ - if self.is_node_context_opened: - self.node_context.unpost() - self.is_node_context_opened = False + if self.context: + self.context.unpost() + self.context = None else: self.focus_set() self.selected = self.get_selected(event) @@ -350,18 +356,18 @@ class CanvasGraph(tk.Canvas): x1, y1, _, _ = self.coords(self.drawing_edge.id) self.coords(self.drawing_edge.id, x1, y1, x2, y2) - def context(self, event): - if not self.is_node_context_opened: + def click_context(self, event): + logging.info("context event: %s", self.context) + if not self.context: selected = self.get_selected(event) - nodes = self.find_withtag("node") - if selected in nodes: + canvas_node = self.nodes.get(selected) + if canvas_node: logging.debug(f"node context: {selected}") - self.node_context.post(event.x_root, event.y_root) - self.canvas_action.node_to_show_config = self.nodes[selected] - self.is_node_context_opened = True + self.context = self.create_node_context(canvas_node) + self.context.post(event.x_root, event.y_root) else: - self.node_context.unpost() - self.is_node_context_opened = False + self.context.unpost() + self.context = None # TODO rather than delete, might move the data to somewhere else in order to reuse # TODO when the user undo @@ -443,7 +449,6 @@ class CanvasGraph(tk.Canvas): """ place the image at the center of canvas - :param Image img: image object :return: nothing """ tk_img = ImageTk.PhotoImage(self.wallpaper) @@ -609,7 +614,6 @@ class CanvasNode: self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) self.canvas.tag_bind(self.id, "", self.motion) - self.canvas.tag_bind(self.id, "", self.context) self.canvas.tag_bind(self.id, "", self.double_click) self.canvas.tag_bind(self.id, "", self.select_multiple) self.canvas.tag_bind(self.id, "", self.on_enter) @@ -641,7 +645,7 @@ class CanvasNode: if self.app.core.is_runtime(): self.canvas.core.launch_terminal(self.core_node.id) else: - self.canvas.canvas_action.display_configuration(self) + self.show_config() def update_coords(self): x, y = self.canvas.coords(self.id) @@ -693,5 +697,17 @@ class CanvasNode: def select_multiple(self, event): self.canvas.canvas_management.node_select(self, True) - def context(self, event): - logging.debug(f"context click {self.core_node.name}: {event}") + def show_config(self): + self.canvas.context = None + dialog = NodeConfigDialog(self.app, self.app, self) + dialog.show() + + def show_wlan_config(self): + self.canvas.context = None + dialog = WlanConfigDialog(self.app, self.app, self) + dialog.show() + + def show_mobility_config(self): + self.canvas.context = None + dialog = MobilityConfigDialog(self.app, self.app, self) + dialog.show() diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index e825fdb4..bd6904b2 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -15,6 +15,7 @@ INT_TYPES = { core_pb2.ConfigOptionType.INT32, core_pb2.ConfigOptionType.INT64, } +PAD = 5 class FrameScroll(ttk.LabelFrame): @@ -72,7 +73,7 @@ class ConfigFrame(FrameScroll): for group_name in sorted(group_mapping): group = group_mapping[group_name] - frame = ttk.Frame(self.frame) + frame = ttk.Frame(self.frame, padding=PAD) frame.columnconfigure(1, weight=1) self.frame.add(frame, text=group_name) for index, option in enumerate(sorted(group, key=lambda x: x.name)): diff --git a/coretk/coretk/wlannodeconfig.py b/coretk/coretk/wlannodeconfig.py deleted file mode 100644 index 1ebfb771..00000000 --- a/coretk/coretk/wlannodeconfig.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -wireless node configuration for all the wireless node -""" -from collections import OrderedDict - -from core.api.grpc import core_pb2 - - -class WlanNodeConfig: - def __init__(self): - # maps node id to wlan configuration - self.configurations = {} - - def set_default_config(self, node_type, node_id): - if node_type == core_pb2.NodeType.WIRELESS_LAN: - config = OrderedDict() - config["range"] = "275" - config["bandwidth"] = "54000000" - config["jitter"] = "0" - config["delay"] = "20000" - config["error"] = "0" - self.configurations[node_id] = config - - def set_custom_config(self, node_id, range, bandwidth, jitter, delay, error): - self.configurations[node_id]["range"] = range - self.configurations[node_id]["bandwidth"] = bandwidth - self.configurations[node_id]["jitter"] = jitter - self.configurations[node_id]["delay"] = delay - self.configurations[node_id]["error"] = error - - def delete_node_config(self, node_id): - """ - not implemented - :param node_id: - :return: - """ - return From eced9863ad488928024edecbc839728577a4600d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 09:40:57 -0800 Subject: [PATCH 246/462] removed mobility config class to try and help simplify saving configs --- coretk/coretk/coreclient.py | 32 +-- coretk/coretk/dialogs/mobilityconfig.py | 250 +++--------------------- coretk/coretk/dialogs/wlanconfig.py | 5 - coretk/coretk/mobilitynodeconfig.py | 75 ------- 4 files changed, 48 insertions(+), 314 deletions(-) delete mode 100644 coretk/coretk/mobilitynodeconfig.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index fed8040a..93ebba4c 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -8,7 +8,6 @@ from core.api.grpc import client, core_pb2 from coretk.dialogs.sessions import SessionsDialog from coretk.emaneodelnodeconfig import EmaneModelNodeConfig from coretk.interface import InterfaceManager -from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.nodeutils import NodeDraw, NodeUtils from coretk.servicefileconfig import ServiceFileConfig from coretk.servicenodeconfig import ServiceNodeConfig @@ -70,7 +69,7 @@ class CoreClient: self.preexisting = set() self.interfaces_manager = InterfaceManager() self.wlan_configs = {} - self.mobilityconfig_management = MobilityNodeConfig() + self.mobility_configs = {} self.emaneconfig_management = EmaneModelNodeConfig(app) self.emane_config = None self.serviceconfig_manager = ServiceNodeConfig(app) @@ -136,7 +135,7 @@ class CoreClient: self.links.clear() self.hooks.clear() self.wlan_configs.clear() - self.mobilityconfig_management.configurations.clear() + self.mobility_configs.clear() self.emane_config = None # get session data @@ -164,8 +163,7 @@ class CoreClient: logging.debug("mobility configs: %s", response) for node_id in response.configs: node_config = response.configs[node_id].config - config = {x: node_config[x].value for x in node_config} - self.mobilityconfig_management.configurations[node_id] = config + self.mobility_configs[node_id] = node_config # get emane config response = self.client.get_emane_config(self.session_id) @@ -454,9 +452,6 @@ class CoreClient: image=image, ) - # set default configuration for wireless node - self.mobilityconfig_management.set_default_configuration(node_type, node_id) - # set default emane configuration for emane node if node_type == core_pb2.NodeType.EMANE: self.emaneconfig_management.set_default_config(node_id) @@ -519,8 +514,8 @@ class CoreClient: # delete any mobility configuration, wlan configuration for i in node_ids: - if i in self.mobilityconfig_management.configurations: - self.mobilityconfig_management.configurations.pop(i) + if i in self.mobility_configs: + del self.mobility_configs[i] if i in self.wlan_configs: del self.wlan_configs[i] @@ -615,11 +610,10 @@ class CoreClient: def get_mobility_configs_proto(self): configs = [] - mobility_configs = self.mobilityconfig_management.configurations - for node_id in mobility_configs: - config = mobility_configs[node_id] - config_proto = core_pb2.MobilityConfig(node_id=node_id, config=config) - configs.append(config_proto) + for node_id, config in self.mobility_configs.items(): + config = {x: config[x].value for x in config} + mobility_config = core_pb2.MobilityConfig(node_id=node_id, config=config) + configs.append(mobility_config) return configs def get_emane_model_configs_proto(self): @@ -678,3 +672,11 @@ class CoreClient: config = response.config self.wlan_configs[node_id] = config return config + + def get_mobility_config(self, node_id): + config = self.mobility_configs.get(node_id) + if not config: + response = self.client.get_mobility_config(self.session_id, node_id) + config = response.config + self.mobility_configs[node_id] = config + return config diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/coretk/coretk/dialogs/mobilityconfig.py index 2c20229a..21782ee6 100644 --- a/coretk/coretk/dialogs/mobilityconfig.py +++ b/coretk/coretk/dialogs/mobilityconfig.py @@ -1,236 +1,48 @@ """ mobility configuration """ -import logging -import tkinter as tk -from tkinter import filedialog, ttk +from tkinter import ttk -from coretk import appconfig from coretk.dialogs.dialog import Dialog +from coretk.widgets import ConfigFrame + +PAD = 5 class MobilityConfigDialog(Dialog): def __init__(self, master, app, canvas_node): - """ - create an instance of mobility configuration - - :param app: core application - :param root.master master: - """ - super().__init__(master, app, "ns2script configuration", modal=True) + super().__init__( + master, + app, + f"{canvas_node.core_node.name} Mobility Configuration", + modal=True, + ) self.canvas_node = canvas_node self.node = canvas_node.core_node - logging.info(app.canvas.core.mobilityconfig_management.configurations) - self.node_config = app.canvas.core.mobilityconfig_management.configurations[ - self.node.id - ] + self.config_frame = None + self.config = self.app.core.get_mobility_config(self.node.id) + self.draw() - self.mobility_script_parameters() - self.ns2script_options() - self.loop = "On" + def draw(self): + self.top.columnconfigure(0, weight=1) + self.config_frame = ConfigFrame(self.top, self.app, self.config, borderwidth=0) + self.config_frame.draw_config() + self.config_frame.grid(sticky="nsew", pady=PAD) + self.draw_apply_buttons() - def create_string_var(self, val): - """ - create string variable for entry widget + def draw_apply_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) - :return: nothing - """ - var = tk.StringVar() - var.set(val) - return var + button = ttk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, padx=PAD, sticky="ew") - def open_file(self, entry): - filename = filedialog.askopenfilename( - initialdir=str(appconfig.MOBILITY_PATH), title="Open" - ) - if filename: - entry.delete(0, tk.END) - entry.insert(0, filename) - - def set_loop_value(self, value): - """ - set loop value when user changes the option - :param value: - :return: - """ - self.loop = value - - def create_label_entry_filebrowser( - self, parent_frame, text_label, entry_text, filebrowser=False - ): - f = ttk.Frame(parent_frame) - lbl = ttk.Label(f, text=text_label) - lbl.grid(padx=3, pady=3) - # f.grid() - e = ttk.Entry(f, textvariable=self.create_string_var(entry_text)) - e.grid(row=0, column=1, padx=3, pady=3) - if filebrowser: - b = ttk.Button(f, text="...", command=lambda: self.open_file(e)) - b.grid(row=0, column=2, padx=3, pady=3) - f.grid(sticky=tk.E) - - def mobility_script_parameters(self): - lbl = ttk.Label(self.top, text="node ns2script") - lbl.grid(sticky="ew") - - sb = ttk.Scrollbar(self.top, orient=tk.VERTICAL) - sb.grid(row=1, column=1, sticky="ns") - - f = ttk.Frame(self.top) - lbl = ttk.Label(f, text="ns-2 Mobility Scripts Parameters") - lbl.grid(row=0, column=0, sticky=tk.W) - - f1 = tk.Canvas( - f, - yscrollcommand=sb.set, - bg="#d9d9d9", - relief=tk.RAISED, - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", - highlightthickness=0.5, - bd=0, - ) - self.create_label_entry_filebrowser( - f1, "mobility script file", self.node_config["file"], filebrowser=True - ) - self.create_label_entry_filebrowser( - f1, "Refresh time (ms)", self.node_config["refresh_ms"] - ) - - # f12 = ttk.Frame(f1) - # - # lbl = ttk.Label(f12, text="Refresh time (ms)") - # lbl.grid() - # - # e = ttk.Entry(f12, textvariable=self.create_string_var("50")) - # e.grid(row=0, column=1) - # f12.grid() - - f13 = ttk.Frame(f1) - - lbl = ttk.Label(f13, text="loop") - lbl.grid() - - om = ttk.OptionMenu( - f13, self.create_string_var("On"), "On", "Off", command=self.set_loop_value - ) - om.grid(row=0, column=1) - - f13.grid(sticky=tk.E) - - self.create_label_entry_filebrowser( - f1, "auto-start seconds (0.0 for runtime)", self.node_config["autostart"] - ) - # f14 = ttk.Frame(f1) - # - # lbl = ttk.Label(f14, text="auto-start seconds (0.0 for runtime)") - # lbl.grid() - # - # e = ttk.Entry(f14, textvariable=self.create_string_var("")) - # e.grid(row=0, column=1) - # - # f14.grid() - self.create_label_entry_filebrowser( - f1, "node mapping (optional, e.g. 0:1, 1:2, 2:3)", self.node_config["map"] - ) - # f15 = ttk.Frame(f1) - # - # lbl = ttk.Label(f15, text="node mapping (optional, e.g. 0:1, 1:2, 2:3)") - # lbl.grid() - # - # e = ttk.Entry(f15, textvariable=self.create_string_var("")) - # e.grid(row=0, column=1) - # - # f15.grid() - - self.create_label_entry_filebrowser( - f1, - "script file to run upon start", - self.node_config["script_start"], - filebrowser=True, - ) - self.create_label_entry_filebrowser( - f1, - "script file to run upon pause", - self.node_config["script_pause"], - filebrowser=True, - ) - self.create_label_entry_filebrowser( - f1, - "script file to run upon stop", - self.node_config["script_stop"], - filebrowser=True, - ) - f1.grid() - sb.config(command=f1.yview) - f.grid(row=1, column=0) - - def ns2script_apply(self): - """ - - :return: - """ - config_frame = self.grid_slaves(row=1, column=0)[0] - canvas = config_frame.grid_slaves(row=1, column=0)[0] - file = ( - canvas.grid_slaves(row=0, column=0)[0].grid_slaves(row=0, column=1)[0].get() - ) - - refresh_time = ( - canvas.grid_slaves(row=1, column=0)[0].grid_slaves(row=0, column=1)[0].get() - ) - auto_start_seconds = ( - canvas.grid_slaves(row=3, column=0)[0].grid_slaves(row=0, column=1)[0].get() - ) - - node_mapping = ( - canvas.grid_slaves(row=4, column=0)[0].grid_slaves(row=0, column=1)[0].get() - ) - - file_upon_start = ( - canvas.grid_slaves(row=5, column=0)[0].grid_slaves(row=0, column=1)[0].get() - ) - file_upon_pause = ( - canvas.grid_slaves(row=6, column=0)[0].grid_slaves(row=0, column=1)[0].get() - ) - file_upon_stop = ( - canvas.grid_slaves(row=7, column=0)[0].grid_slaves(row=0, column=1)[0].get() - ) - - # print("mobility script file: ", file) - # print("refresh time: ", refresh_time) - # print("auto start seconds: ", auto_start_seconds) - # print("node mapping: ", node_mapping) - # print("script file to run upon start: ", file_upon_start) - # print("file upon pause: ", file_upon_pause) - # print("file upon stop: ", file_upon_stop) - if self.loop == "On": - loop = "1" - else: - loop = "0" - self.app.canvas.core.mobilityconfig_management.set_custom_configuration( - node_id=self.node.id, - file=file, - refresh_ms=refresh_time, - loop=loop, - autostart=auto_start_seconds, - node_mapping=node_mapping, - script_start=file_upon_start, - script_pause=file_upon_pause, - script_stop=file_upon_stop, - ) + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + def click_apply(self): + self.config_frame.parse_config() + self.app.core.mobility_configs[self.node.id] = self.config self.destroy() - - def ns2script_options(self): - """ - create the options for ns2script configuration - - :return: nothing - """ - f = ttk.Frame(self.top) - b = ttk.Button(f, text="Apply", command=self.ns2script_apply) - b.grid() - b = ttk.Button(f, text="Cancel", command=self.destroy) - b.grid(row=0, column=1) - f.grid() diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index 3973efb5..3c118834 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -5,7 +5,6 @@ wlan configuration from tkinter import ttk from coretk.dialogs.dialog import Dialog -from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.widgets import ConfigFrame PAD = 5 @@ -46,10 +45,6 @@ class WlanConfigDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def click_mobility(self): - dialog = MobilityConfigDialog(self, self.app, self.canvas_node) - dialog.show() - def click_apply(self): """ retrieve user's wlan configuration and store the new configuration values diff --git a/coretk/coretk/mobilitynodeconfig.py b/coretk/coretk/mobilitynodeconfig.py deleted file mode 100644 index 4a94f573..00000000 --- a/coretk/coretk/mobilitynodeconfig.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -mobility configurations for all the nodes -""" - -import logging -from collections import OrderedDict - -from core.api.grpc import core_pb2 - - -class MobilityNodeConfig: - def __init__(self): - """ - create an instance of MobilityConfig object - """ - # dict that maps node id to mobility configuration - self.configurations = {} - - def set_default_configuration(self, node_type, node_id): - """ - set default mobility configuration for a node - - :param core_pb2.NodeType node_type: protobuf node type - :param int node_id: node id - :return: nothing - """ - if node_type == core_pb2.NodeType.WIRELESS_LAN: - config = OrderedDict() - config["autostart"] = "" - config["file"] = "" - config["loop"] = "1" - config["map"] = "" - config["refresh_ms"] = "50" - config["script_pause"] = "" - config["script_start"] = "" - config["script_stop"] = "" - self.configurations[node_id] = config - - def set_custom_configuration( - self, - node_id, - file, - refresh_ms, - loop, - autostart, - node_mapping, - script_start, - script_pause, - script_stop, - ): - """ - set custom mobility configuration for a node - - :param int node_id: node id - :param str file: path to mobility script file - :param str refresh_ms: refresh time - :param str loop: loop option - :param str autostart: auto-start seconds value - :param str node_mapping: node mapping - :param str script_start: path to script to run upon start - :param str script_pause: path to script to run upon pause - :param str script_stop: path to script to run upon stop - :return: nothing - """ - if node_id in self.configurations: - self.configurations[node_id]["autostart"] = autostart - self.configurations[node_id]["file"] = file - self.configurations[node_id]["loop"] = loop - self.configurations[node_id]["map"] = node_mapping - self.configurations[node_id]["refresh_ms"] = refresh_ms - self.configurations[node_id]["script_pause"] = script_pause - self.configurations[node_id]["script_start"] = script_start - self.configurations[node_id]["script_stop"] = script_stop - else: - logging.error("mobilitynodeconfig.py invalid node_id") From 9445b63bd2eb7ea44c2b45d39a49a9f2c576aa54 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 12:29:33 -0800 Subject: [PATCH 247/462] removed saving default configurations for wlan and mobility by default, updated session.add_node to set default configurations for wlan and emane --- coretk/coretk/coreclient.py | 2 -- daemon/core/api/grpc/grpcutils.py | 2 ++ daemon/core/api/grpc/server.py | 4 ---- daemon/core/emulator/emudata.py | 1 + daemon/core/emulator/session.py | 9 ++++++++- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 93ebba4c..beeb1c0a 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -670,7 +670,6 @@ class CoreClient: if not config: response = self.client.get_wlan_config(self.session_id, node_id) config = response.config - self.wlan_configs[node_id] = config return config def get_mobility_config(self, node_id): @@ -678,5 +677,4 @@ class CoreClient: if not config: response = self.client.get_mobility_config(self.session_id, node_id) config = response.config - self.mobility_configs[node_id] = config return config diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index c6b625fb..cf1250c8 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -30,6 +30,8 @@ def add_node_data(node_proto): options.opaque = node_proto.opaque options.image = node_proto.image options.services = node_proto.services + if node_proto.emane: + options.emane = node_proto.emane if node_proto.server: options.server = node_proto.server diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 8c987371..79053eee 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -713,10 +713,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session = self.get_session(request.session_id, context) _type, _id, options = grpcutils.add_node_data(request.node) node = session.add_node(_type=_type, _id=_id, options=options) - # configure emane if provided - emane_model = request.node.emane - if emane_model: - session.emane.set_model_config(id, emane_model) return core_pb2.AddNodeResponse(node_id=node.id) def GetNode(self, request, context): diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index 5e59eaae..2a8f9bf6 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -89,6 +89,7 @@ class NodeOptions: self.emulation_id = None self.server = None self.image = image + self.emane = None def set_position(self, x, y): """ diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index cba99e8e..4852d026 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -30,7 +30,7 @@ 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.mobility import MobilityManager +from core.location.mobility import BasicRangeModel, MobilityManager from core.nodes.base import CoreNetworkBase, CoreNode, CoreNodeBase from core.nodes.docker import DockerNode from core.nodes.ipaddress import MacAddress @@ -703,6 +703,13 @@ class Session: logging.debug("set node type: %s", node.type) self.services.add_services(node, node.type, options.services) + # ensure default emane configuration + if _type == NodeTypes.EMANE: + self.emane.set_model_config(_id, node.emane) + # set default wlan config if needed + if _type == NodeTypes.WIRELESS_LAN: + self.mobility.set_model_config(_id, BasicRangeModel.name) + # boot nodes after runtime, CoreNodes, Physical, and RJ45 are all nodes is_boot_node = isinstance(node, CoreNodeBase) and not isinstance(node, Rj45Node) if self.state == EventTypes.RUNTIME_STATE.value and is_boot_node: From e059f8952098c94ec37e3c93d2b5afa1711d0b32 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 21 Nov 2019 12:33:43 -0800 Subject: [PATCH 248/462] change some icons --- coretk/coretk/icons/docker.png | Bin 0 -> 2084 bytes coretk/coretk/icons/edit-node.png | Bin 0 -> 3050 bytes coretk/coretk/icons/emane.png | Bin 0 -> 3334 bytes coretk/coretk/icons/hub.png | Bin 1108 -> 2242 bytes coretk/coretk/icons/lanswitch.png | Bin 669 -> 1138 bytes coretk/coretk/icons/link.png | Bin 0 -> 1692 bytes coretk/coretk/icons/lxc.png | Bin 0 -> 2167 bytes coretk/coretk/icons/marker.png | Bin 667 -> 1211 bytes coretk/coretk/icons/mdr.png | Bin 0 -> 2786 bytes coretk/coretk/icons/oval.png | Bin 1236 -> 2407 bytes coretk/coretk/icons/pc.png | Bin 517 -> 828 bytes coretk/coretk/icons/prouter.png | Bin 0 -> 2590 bytes coretk/coretk/icons/rectangle.png | Bin 157 -> 259 bytes coretk/coretk/icons/rj45.png | Bin 696 -> 1121 bytes coretk/coretk/icons/router.png | Bin 1462 -> 2082 bytes coretk/coretk/icons/run.png | Bin 0 -> 1805 bytes coretk/coretk/icons/select.png | Bin 348 -> 1038 bytes coretk/coretk/icons/start.png | Bin 588 -> 3010 bytes coretk/coretk/icons/stop.png | Bin 145 -> 2305 bytes coretk/coretk/icons/text.png | Bin 146 -> 314 bytes coretk/coretk/icons/twonode.png | Bin 0 -> 2494 bytes coretk/coretk/icons/wlan.png | Bin 0 -> 3457 bytes coretk/coretk/images.py | 4 ++-- coretk/coretk/{icons => oldicons}/docker.gif | Bin coretk/coretk/{icons => oldicons}/emane.gif | Bin coretk/coretk/{icons => oldicons}/link.gif | Bin coretk/coretk/{icons => oldicons}/lxc.gif | Bin coretk/coretk/{icons => oldicons}/marker.gif | Bin coretk/coretk/{icons => oldicons}/mdr.gif | Bin .../{icons => oldicons}/router_green.gif | Bin coretk/coretk/{icons => oldicons}/run.gif | Bin coretk/coretk/{icons => oldicons}/twonode.gif | Bin coretk/coretk/{icons => oldicons}/wlan.gif | Bin 33 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 coretk/coretk/icons/docker.png create mode 100644 coretk/coretk/icons/edit-node.png create mode 100644 coretk/coretk/icons/emane.png create mode 100644 coretk/coretk/icons/link.png create mode 100644 coretk/coretk/icons/lxc.png create mode 100644 coretk/coretk/icons/mdr.png create mode 100644 coretk/coretk/icons/prouter.png create mode 100644 coretk/coretk/icons/run.png create mode 100644 coretk/coretk/icons/twonode.png create mode 100644 coretk/coretk/icons/wlan.png rename coretk/coretk/{icons => oldicons}/docker.gif (100%) rename coretk/coretk/{icons => oldicons}/emane.gif (100%) rename coretk/coretk/{icons => oldicons}/link.gif (100%) rename coretk/coretk/{icons => oldicons}/lxc.gif (100%) rename coretk/coretk/{icons => oldicons}/marker.gif (100%) rename coretk/coretk/{icons => oldicons}/mdr.gif (100%) rename coretk/coretk/{icons => oldicons}/router_green.gif (100%) rename coretk/coretk/{icons => oldicons}/run.gif (100%) rename coretk/coretk/{icons => oldicons}/twonode.gif (100%) rename coretk/coretk/{icons => oldicons}/wlan.gif (100%) diff --git a/coretk/coretk/icons/docker.png b/coretk/coretk/icons/docker.png new file mode 100644 index 0000000000000000000000000000000000000000..6727a58badf3525beabe79ac32f04f6020c05402 GIT binary patch literal 2084 zcmV+<2;29GP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2f0Z^K~#8N?Va6k z6-5-lXLlB<Q4$3dV#=$;Kxr#qEo5icInz_p_U^~*?wq|_oL_R&nZ5V+&Ym+LXU{o1 z%Pba)#bU8oESBnV@b8?Wx$%sT#Tob~)-)0A^-S`FX7R) z({=^O>EtByf+wV49$}HA*Tc)=M3GeV2n)Q6a@u+okG6%jDM0r2pP4J3W5VmiL+ybF zw^+!}$x-n#9;uYd6`-aWH zAMII+HF7FZKy&ly3$n*Qjn%mU7Z?0v>z>6L8RQC(z5bf)^&~y2YgwfJjt&!@1`o#k zV?AA1BZ187=pE>t-`w;|KqdUeT@m}>P^ex|CP8=&xn>DC7d&DdlCHD$`>ow0NA?b5 z)i|AtLbm_QnL_{g9f#e%L1)EwZ13y-VO#$QR*hj?0rKkgyfo`pVbwSfbcF1{*YQ1m z-&+p5J3)9UL9i|uRX}s|;E)`)Ph!S~l%9gXLb(0a zfeEaV09MZ4&qJ)~@#%XmyFN-}o+6!Z@9Q|e>G&P27NT?uo4wN1j0}4&rx9oI=t6B7 zfrr%}SnFY}5T#ltFS14`g%1`_QQ-sdUHBhlgf3SyC_&*7KJ|O?H{`j2`t6a zx2v^3jE#lFmXWw9&C_SFmW$#F*m#C`rHg0jvScNI@JGll|I+bKtSKbRLfu07cPukd zp56lFR)F&76Zv=MG=bMGWpfH^+TdveH1Ujc$I$C4!r!5_v_CRz$m z=2cZmQ}+W>R=Osso=~}X3QHDUxX_A?Xk9+5;JWEt&4Pd{B8_&rB3f|6z#mvU4{M3& zjQ|6}=bjm_BnUX|WdheT;N}WlEjM+h0#u5)0hTmeTGy~4ppA`2=`e{x%I-HTCXzh@ zAV1#_kj=1Qaeb8l!Jnjzi$b{3qgYBLTLEx$gh4*~BBD#ADgm^qOzp)9B70bJiltmYv23(7Y`uCy-Lu_l+>NI2QpH+rDoF+jE$x4I0Kv zW*qEl)8WNjvCp}``MXE02oOr}K~ z8KKaVX?Jf{+wD>SfmE?LlPtC+sEq=Edy6v>WQ%PHYNdexBehZhq!W_e?IIbw=BVGO z6SZj#EW*^eoB;d={8nAaY>72P^&l}rfg}awCCP04aEHSLW!hj0a3OZ^Nb90(GQ%!Q z0oi8`8($Fc*)Esuev`LN_&^vqZ1;A~4!%#+2H2k>gX~{+!&3S0aok&MYhsR*EzVB1 z>@-FIyNM+ueDZEJi)~&=7H5auz)~XF3JBf5u~ZsZY;RpB8tw)FR|vGx7@>O^ONnUx znu&!N^8HD3^bKOFtvV^|dcUHvm}Fr}_+U+?y-Z-Ft!@}#DHXjDP@`Q=K*)8xL8ApP z81|3$JcPAG^i(bQ5SA>u2!07msc0!c?Xl)W^>QQD87V8>ed+WkJ1(wZ$&v*Z|6nN- zeFX%borZJ?Jgw;m&{lvnsn)9v?kwp@8Ks3FXQMv?fRLZVk|l|pGRma@wY4McE@+8| z2jh9SzsxrRfY3dQCCh9=o;!~=w*u6@nqF`ftClI?!mGgMulYxSL|)<9+m&XabJZJ1 zAq6P7Ft%${9N-X}?dM5zLjdqz+;`xAky1hyrF^f=ErVC3%_~olhOD@)4&?D>FBp-% zzp$UIwDX(#_9eSvQwE(o_b}>P z5X9F7K1&_;gpFMLxAot}YGI6Yksr79|0@@wKe0+eo{Txl>^g;9bWQD|Yix}A$JVWa zO-->%0&L2fh0bS6!Gj@_yXP96B-M_xt~ifX<3Ok|Eoct!k;a|X*2O(WZ(29rT7O{O zYM39fY8)^r%s_{=_IBgG#Oe^C+0-(w7hXVR3CUS zF57mhO5p>Qj)3W?-34KTJK>SKGBTNbkQ+Qd>=isxDQ#206zX1N9z4z>_c2PR-V7!m zWq3tShkxY>pi|Ix1x!bcipht0b{SU&#HdM O0000h{ER5N1j zzfMF7v6+R%UnMhmCZ?`q=}Zqj`6G}yK=)YsbwNHVlO-#-C{_+*5&$g>cE{Gaz}Q&$ zGr&8DOp?rCUbG!WJpft|ya{CY6 zYNF8LR#rjoR-4sZZT1wZXFG<)ujUiZoEYVIbd=<~8ZA5g3Dl_g6_v2^bDOt64RPYM zKU*E!3QtW4m=+)4iu1$lhx{1v^#o{s@F$2a^kbtoYqr_UUlrn{ffbo3wfqYaxmhxU zZ+b57wV)OTyQ6GDHPx$)niQA8-`)?gVYkcVUJo7H3c=d~bWV%UkmvfyL6!2 z?M}n|+7P40jr(_yDTCC=6cZ^m69` z5bI7)6*`ZqvUG!))YE9iuSV6eR*nLo2Pd~c81KDCu=5ZcJm$wjm`h8SVufEpgR=+9GX6Il0hb3KB=g8sdxn{&Ii3 z#Si|glI;gwG$P9y>F6>?#zY($s}I|T{A2zCFm|XFSMiHV;b4)Adr-}Ba#_Rx=;2^` z_>{7Dj-P^aRemhMq2q=Yi(F7se1JT6jp!0B2u9ohjr;`I_7J~E#LFuDi?<`++_Hre zib~<>1>cAdaN?9J6Bf$g$QMPn)6bRLIyigIuLa0I9)6Ny!O;?U>h(&FmR$JvbCqrz zk{waI$N^CG9e_Z8h>Ke+p`+otn5$qmdi0lTNs=Ter1INix%UuGkc!+`!XR zAK{i?Rj2p>=UmStBR5_LPXJxqx~)xGE58=Pl$K#^HeH))@sFLXKg~2xc5ODIc98=h z(#EyhTC{1QH1mJ{0)Qp!Z4S5@4m!Yz_~4}>Mb^$i3R2$z5D~IEm`OvGA(*=~WTayU z=i&pTwQ_q8k+pNu2efwSwNA1-nwvx&C{kv>?SEPONQpWYAD~@pxA$P=2{6M&oy^W+ zH|?`|(Z}AxkrE~NddRW(0Nv8P`DzF!0q}V?EzCvf=9dxHY_plM*nK+kB0fN$47cr7 zBhFkLSQI1EpPrqgvyQWApUsrlD%pS3nV4S14_MH%i|Z6*AOIRUicV+4Gz#hehi`ELsO$JHUW!i>&s-vW?#kic>`3$^l*i zAQ9iBi5vitougICR|q`ca<@@z{9mhyidA*2qSja=19QOUkAgd9l~g zx@8~$Qn?8OkHF@=%7)#s-&FBS`_^!^w*c5^#pXyCMYJp#(~A&mo0l3?vuUp~b4iu! z_qrMb<1P=7>}fe&K9{WN$0J?kEVMScUaA1Fey7chH>;@hxg{A7(pn3n2YXCE;!HD# zoATZQux^LV3-6daiWqC|8X54uiW@Q+0K4=&?FJ6%Xzj8DLl+U#s+c$av;s zrx!c+BoeeiXVvw8s>=M8A^!RAxPE}pH=_qz+;ExCEfSHLA~?!1*3&jSi-{wZX` zi|^Si+t@@C{&~g>wYc{h-{)uYDLX#jO#xQ{kUn!OQ2lfDzbh)?`8TUr9fR{a(IQ~M zJvjmH$q8Wj-5(U0>Gsn=>VE)1K&k87n0j{%}hY{j=(>Vrd!dDp2+mDtijU@1C8m z=jBilNKb`9-7SXpwiwhy{HL1>J-j7dgPz_X@_bxs)Zpan^Nya|dhDe6lNHU9;IW(Q z7at(C6yBWS(@2$5!GhoY$#Y7(&EcRy|_ z;sav6;75Ega4_rEA!Sz$>LC%=y(3n{f=Oco4DH=S#7EXr%wvA7_zpn!6|>8)>8S&I zcA?Cx4H1|)Cg5@7J08fF*xV!ogT7Cw8oA@)jR7!*i^(P zA7PBCujm@P>#^gfZOa)X*SZbD9aje!bxD&EKM$20lV4Rgo^an^`gs4`ZPv!Ug&OfR z{kY2m-0_1ZBmQ=%y1r3=y11H+Ykr@xVw3g7mVI`;uW61LXz{=e7UK6edSZgCrRwZB z>GoW)Y0FV*enUr0E4;LCY1XiQ*7~~i9opOC{xN|jAigRvhZ06dLwtzA4>&AdJmt~c zElfXBsun=sOyS81Nl~1c8sj9Yzm%-0M!$<%o0v4g4Y^MYT)o|1ez;h>O}#ytR?TDK z)erI(R6b-n-`D9W+74<`ydyY1tu3?S_On%y2>-0rgI-oOy%PJMu{t=p1%cH2P#y;D z8f*XckdNprDy)~?d~nYM@%tgS-IG#i(e7?6>k(9Y#i<&55Sd9aubPAW`jb+9?fb`F-6gX&1&68sO9ZII6q zT}4P%Qblg8(@aA&ssPkR3xXX{dkD}Mq(4YMARW~<2x)xt4)bZ05=2fxji0l{+KEPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D44X+rK~#8N?VWva zRMi#0&wcxLNj6`GMm|Ec6^ubpEQ*c@(oh5pUlqtgtBiC;rp|QgDAZPGsw1_rSS@1x zqn#F}A4*%xina*Ul8Ss|t*B)XQc$dzghU_-Az%CP-tD>Xoow>W9#sB zRkNiDjt@Zj9N!2xz)TK&I-cz$G+l#_A=-;(x`4xReBZka_*<&WQ~lM4x zC@(LUT{%US4A{2;_#J3+>8~V=f{$v>*zc9LGn;qq+7;H#&cRUu&>ZhAx{on<1l6l_ z>!5KG7=Jq4cJ6mh3WQ?*CEaSKZcYGT%n;}*RH4fgf)ERX!u0;u#U*fa8M1_YZunBO7uewd zlI2;c01&vRPI^2Hd|mJe60 zk$`hfmfo-9eFUi%Cf6D6!x<6 zU4DNX=}94Gr)L*giszTn&S!YvdF&@a+np~s>%9OURt2=b8O2RqeK2J9YB-(GSRCJ4s7HVoE_~ z{1}Rt=@y-uCqvLGcaWA`3PL$h_C!<=0GhEs7@MsAAZaN`megnP$>99Y9)^@TUvS>t z4z;UD>7h`ST~Cv2J4tB)v*s@ztE%in6e9tzuT-G+#-#RlF)2Be1I3#SEkR+7Ut7I( z^9j;zBPlI_tL#yHGW@NkuZBoUJy-;Fq@x_Lp;bU0(_9e|383cT|96jC+9}IQu{gJ!a~<5)^$B}ZkZPRitex2foy>XZryG%m#5wlH#m%#| zgNe<303S<&26Nlq<$Ph7+c-4h49ZBf1-m^mf}SZ`5dD@!yxm8 z4?cU|!W$G-p2ZoTj0d|GO~dgahVZ6i*FA`j;r1*y4i=*t3t-eQNHHSjDq8T-h{7@a za0j@H?|5^|=EK^n17tt}5QCGJc789O$OCwU31p5-ng_rd4u^5X{K8N#yyW-$jYH{v zkZ=l`F{g4~a|f@%hPgfw=L^&{=mQcyNAv*$z5#u5?2fs6SeNVFSfD99VlpCXjrC+RX_p%2M`u&}x_esA3APT4ynQujLvTiX` zzbFjqS7ZohaR_@@XBJ%VKmz+1@W_+&Uj)Ai0NBXK&-6HH?rS#c@%l5D^-01`06 zLVEoY#gPl?PDLwo@fm}F_lb*0G++wAtXT_(VGA%U!v#wFypzgcL9o<#gUNtTsHpH= zPHF^`XH~Z(=64KzLrYG2jbfcZORmXhOaZu_QD|&%n zb*Tp53QPfzn6ZL6!yuwcCf{1LfOJ?mOj8(`7CbF_SDqPZ*#zgRX=2XY0wk%PM2(W_ zEDT)SNz)8b2%ibx^+sasG>H*R0gxeFLt=zH#dx3$@yQDtDl#?j2FK)EQvmX_WibIK z7M~;JHP~98dQpY5Hekr@Riiu#esXVq5wQdo4tn75B%wJ#ebJ0Kaxqg!Y5k9(NEh!QB z$Kj5%+ek5B833`N5##VvQX`CbH`5u%W5he=^GvE1wFHsN`mC3ao`SvDh@M`spcSwDW$~{3ZW5a0nK^H9 zDF^9A98=#)rVK_7V$NTl$Q~031wbGwz4pC(WH}Bj#J)eHb?c=L|5)5=R&V$JK`6Sd zeUc?E zWx!Z5=a7pRg00`JNTn6sfy zXXReLK~kYcJXY=D)*BM10fTZInry^XFC0&*2Hql;J$JiSQlD0&WeZ>&Mv@po1+aBj z6T3*;6m+qW2|$WT%K)errFvGjbRexx`Um^@7)6DZ9KZ)0hLKr^!RKh(gwN~N-Of&u z)$yc70DPOct7Ifpp@YR%+wscuf*7E+bq(;ie~I``gtKvc09GF0*(?mNp;Kp*slU8Y zpuob_)0v}}=$zdLaL&;A>j&U`!JX$_;P?TWqg%kL-Fzu$@G_|phGqfWIszbvszBO; z0RL(PXtKE0ae%f22WWm$9fxBBAoBjG5%6C;LBsc|CgTun5n5bPXCB6p*sC7{yxl;C z9!?b84r5m{`qejw;n);(Wig07=nT#uKNO(yYP0hNGJXi&lxpI( zfYl#zPs=%2gKwOGxw!iIX-xbSNlaGlxrwvVI{#yhs%?K!aaHZc+OKvj7i)8U=*uK)R3Zd1042i|JneaN7{ksEV4nL#Qj*>D0DbHgzqe8 z9JW69HTG+I$&!cm@Jc-BG8B#57AFWEV?cet>2`o;-y@}mr1t^n{9XBBSc?O}K9iwi zudDCx<5Mx+?Il%1+~M3eKK4nArnlh;z_F;k3h~03f)|b>e!w{z$KHmXst4>!;Uits zIobr^KcV0AM{?BggZn!P_J|4#&0t!OGoy37+9!NR1#y!nX_u7)2Y)*R|nD z7zbd@7GLJX4_Nu!G`5*k$KmJz2v+Uk8!@%qLu#VDv~C)++^NbbI5q`!ldr%EOzwXt zH4(NKDDWMU9gE`wAf}UR8(}`)Q$K}je|9GiV7soWZGZ(%pNbYZJ_YsW;obZym1!$t z-$3AiA{511UB+dum|J9<|O|RA? QVE_OC07*qoM6N<$g150mDgXcg literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/hub.png b/coretk/coretk/icons/hub.png index 26b7eba5034121f98fe6f3c6d2ee395a2eca396a..c9a2523be17ecd00310c38278d8f3a44ac4863eb 100644 GIT binary patch literal 2242 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 MZDEIdh4F~{7sF09RR910 delta 1105 zcmV-X1g`tS5!47FiBL{Q4GJ0x0000DNk~Le0000m0000m2nGNE09OL}hmj!^e*_6h zL_t(&f$f;fPa9VhhrjVd5VtY034s_|lf*HEN=+lgQ*0U_RVvcdE`6zss+&r!)K#mf zk*fX)&7V+pSsceCMO_q>Oa%CB4!KW6Hcbc4_QyIio>omr;ElgUe?HA)DVIi5iq$nmA|&0zorGcJOhFI0RZ})uLCf#a)-deO+7;C zBOoni7zz4t+gt$n=CA7!2GB{sUMZ*TmLx#yzP_7s+HOe?@pgckDl12-Y8Z45;5p?{ zpYblbj_0@CPMI}1N)jN~f7g2)U5ftg_3d2g_zI`hiPP%jO2?P9*PqMtYj~lap_6Z7 zFcJ@>5h3B~!nrCBc9e@v7-ebK|zT`DJMW|3ZKk zyNh$jUgL!I1OQJ{PpPTSofan7C$$qWwKmO#=7F5GNAbthR@c(!f9R#p(MvQLV=J>o zkE2`hoD0nZwAHqd$%+6Z#00-S`i;8}XX$_G{7%_Su1}TRc1sel-PdyijAE;@amL=t zD@WU?t+vx*cNabD=%G4hHfFiIF~c9BKM|$>8o@}=r)pMG{6jMocssvt3gCG0INx@E zgUL|%J4uNtKEM6Ne_oCTV9YZro2zD{#6M5g5TBJsAw-J%>;+9{q2>I zR>19*J4JoQmnXCgsjYye=rZAWMA05egjwBO)iR`Z0?bA;!)`CJRFt`gbIeDAB*i32 zG0A)+$n3@p(PWHwPrpNzu}Z73+6;370&krhqV7-~p+t!9e}DghL?)TzV>X)kr0rvl zS?aia>Jq-7U-kvLS2Xzr`kHz{f30+!<{InHS~<5Cs!B+1I(^yNRPz4D!;N`sYHuwx>q6z;I;h>RN0JGV>PT(x|a+>68m@VpRx;;6m5SF5g2rrEtuefUPCe@OUCi~uKO z>I`5Ne?>b*i-f;O&e<-}8dEUFcZ=2tH-G_aMQaSwAX*?Zl`NO239NK71uNp&jJiU! z!1%emwY~+1n=-I1Tg&*yHvj;VHA!?fAIGYAo$q5)1Lh=R2^`;W49S|Luj5h!{$g7; zj;=q1t!p;>HYPP-9>y5je;Rgsy*QT?RX&2x&TKpF$#GiiTad6<;_in&IIiQ#F&9Et zz#{xuoT&l-p?Yzq1}uTf#aUYe7-MK}5PozLI$9d==ZxCo<94qcj_csYyW4pF<%Q?R z_I($Wh$V2S>7bUoP8pLkleqmpi?7q)lpNpJ1|(~eIIylm+jAFDfBC7r@@=SZ4QQ=z zL3=~H@|&Bp&&~<`eX09~mlGox92bhc#u(bx?y8vio^y_&kHbpl`-LcN zfNj}0u&!g#J+$bZtLPan!f_qk{m_T}R9@K_^C@cpEw)ReqyeKCq{7V{T(frb0tzTPvoca=zVp~_XVvIDVwM# zXC`s&&2?Y5bd^Zy8t~^K)?zneD);r3K>1dMbB@8WA@t`5l;74@BH#9kL0z2v`tiLK zUq=q&;%ramhHxb#U|D9puJyI)-16amlzMFKWLFf1Qt#aRETdxH3^PE<`99 z7XXxu3xH52<034ARAZa2;R8ckGVaLw3^uLaq+@ttp-RTp*|o?t9Ymd7>-W%7C>f^_ zCF3MeGHxD9#x0JLaetv?oJN$4(}&zR20n?_804d9fy}FmheT@(B78~9 zyu7;yfGb*O0M}x?SF}jD0gv|Idkx0BMe9P3@hkT`?!Of+liCHv3)Ni!p2*aO)bQu= z)u&_%r5cdUWDD`xsx*MhFnc<3!T6=m6ZPrb$*Z$6MF%MR-&pN=uxCT@SG05PpBFSD zWUsehzz+bU03KV6_sUE=gb+dqA%qY@2qDBwzX3%$;5Bo4^J4%2002ovPDHLkV1fWo B7GVGY delta 662 zcmV;H0%`s72%QBXiBL{Q4GJ0x0000DNk~Le0000m0000m2nGNE09OL}hmj!^e*#iT zL_t(&f$f)1YZE~j#(y)hZ9Eu=O7$W&r{thCwEp=78tNsYm}pP_`3PPrcoaN|A3y~0 zpf;h>=29y511Ls{AT(Z9(SsFBFg=*IWY&X?pk}r^n`}0R`5p7_yYoD=^Jbp~i^XDz z65&V(*@$O zc%c5s4FIGPDQ+CU%E7${jC=Qlpr<3ZPv7#aCr6T8KX#3BZ4Rg5_`dHze>{OU!*Yn} zOp#NEPx$T$Y(NM>ZtP6Z_MQ>QKs**_I#cAN!A|*MOKtOHb(Ys(-}vq^o`K{@l4AOj z@xa}Ab+_a1?b)br)|su9`Tld==)Ohi4~!Z@;bZtX5)6ZFjX`uQ7T8 zPQ&5ByN7=bvB_Bv1LFf-->mcK{bQrIr58TKoR&bH%~q4At0k&y)xZn4i7d=1S9Lvr zZETnu6#5$Gq>xML>}h4 z#mm5kS7R^3+-?Z?p{scrSPnr;AQUX=YFYzQsu`djiXt=Cm6A7gMXiBKzEX<`cU%a{ wKqC}s-ZWe&wUo>*UVQ!~g7qyHi^T`O0Ns}A&6k_p#{d8T07*qoM6N<$f^>&Ew*UYD diff --git a/coretk/coretk/icons/link.png b/coretk/coretk/icons/link.png new file mode 100644 index 0000000000000000000000000000000000000000..d6b6745b29e839599b85ea86df290f709977529a GIT binary patch literal 1692 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 literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/marker.png b/coretk/coretk/icons/marker.png index 9a41ccbea61bed6d289187b291c6971dd80e0e23..8c60bacbe5bbf2bf76a5347f776d12d774749217 100644 GIT binary patch delta 1209 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 literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/oval.png b/coretk/coretk/icons/oval.png index 06f492d4893b061669037e76487be46de3e9e494..1babf1b7edd79ae20e1e9652a5b419fae7bbbb82 100644 GIT binary patch literal 2407 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 Z{tNmR$e-W{4OjpG002ovPDHLkV1nyAevtqG delta 1234 zcmV;@1TFjL64VJHiBL{Q4GJ0x0000DNk~Le0000m0000m2nGNE09OL}hmj!^e*}q1 zL_t(&f$f-EY|~X7$3MT*ZUcs}fd=FOFUX5Pvfv~H(v5~8iHVo3*eKaFCYqQSd{G~a zWR8KCX!Hd`G{nSkF{bV!8cmRqrW-sU!#HC!Cdfq-$uNv1 zz*$5F@bv^UDv$73?d4nFKrB8RF--`YfptJdf%{(t5{T~S(zPz0-FPYA0{J!2)iWP2 z_$o$TLaEMovq_X(MA?O3_7TxFL%A=@g*G*;NW2WPZQ6yWG6b{|soRB!e@>P>c>@P} z7I3|MA812{3nSU<*PEaDSSNGZ-GFss8s&B{DbA zkyr}!0t=nU1zxtNNbpe;`1B>q@!x_yIb3(e3^QA@lcY@?~JJ(mZoV4@pFP<@Wh zzSE$sNs$K#lzjp`<1R`41infoiy$5`6YtgsdG`oRVyargCVuXY)Z;~x zl{-d$bUc`7k>EkK>u2Y`9+`+a8=b_5TY~rLw}+U@{soF?`)z zZ0;_Cr2HtmvdHmVOg7vG{=kktTm(t^F;6hTs+__hXLERD`XEMGgT z8|myzH|E|Nx(_cwe~HXQn;N{}ZtQ{c7n;^( z`2+w&-gP`7%)-``ykfJww;c#uKGhxWG@SltvLn$0thb%gP9)S;)NvX>Rp{t#N7(8_ zeqdAmMmIfu0|$;QV$Ay~_1}^1BcNMo3=gMWH&wdhRd})!q}hp_L*yYMbtwmH=7TEM zy9%R+fH_X&e_w>Hc!Qr!+msrM1&{?VAZ!Qja-t*PH?S$(pKfXPleROy8a3afDGG1~ z(Y<)OhiLd@LBcx{OA&2Br3JV*O~!dJkFzP{{=DC0@=+m{xQ_r|qO8i0p`gEk^aIBc z`5Dn6RE8P#Zk(EP0@Zj_gEtQor~$3L@sed(c0NYHe}9_B@R@A#a+`JH@gNt>Yrvbp z?3~sV&VMLxbEp3y;qdrm$@vXyvG_tv;C0X!fjbISn1NB0-IQPdV6w}sWD308Jv0|n z9sxE3A)vgVIxq(G0bg*nqJ!quqxlw?`nbI3U?nqWtVLJ})F2br>%y#w>vcsKnYdm* wi|AjdC=6iq1ZA_2PW5`dM2Qk5O5EQ353ikAm0`n5NdN!<07*qoM6N<$f?%{s$N&HU diff --git a/coretk/coretk/icons/pc.png b/coretk/coretk/icons/pc.png index a0577da3f28c33cf1ac9e39f4135b47334787376..3f587e7034e25bd49495cca974ffd31cec6c1242 100644 GIT binary patch literal 828 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoY)RhkE)4%caKYZ?lNlJ8K6<)1 zhE&XXdv|YkaG(U+hvzDcXVp3swgyRUTp;%H-IE7FJr>=I75*`&6_ylbso58ZJXd<8 z^g`k61BKiTEO{GMU6LKH1Sqeabm#Gd@Jn~9i}Ei$TX^!b^yKOyzW2rNYA1V7W>SIz zBR!uxjNG%FN?v=<1-2i zZQeE1F-?e_BK3te@7})M(ywo9e|Kk>;f0F1j450VK8zDI8H`vRmNA?VVVKFJ5X?Yj z!J7x}3(I|}-t^cn9cuo@I{l2LTc&;Ot=tseEy;Rp)|)#kh5eP~<-ZyE9yEAQKHRciXAo^gki{(`&m?W@DAr8hC|*gk9aSuw8D zt2Z2;cSB;%Z0p_-Bg;248@EMY<7}v2{WE*9;h#s$4O>^_n@Jcm=kng%kZ)T3l>f(% zm&VCiJ~Ea)lY_VXcilME=f~!i8=}u14!Zy6*et2|j^fAu&Su^E(=@^ve)`snI)V~M gLkAR`nDB?m&P-g{{?uERLtqzyorgAGWAGyQCzs3=m;xXaVUF(jk$?F?J* z!vP{~_b-d(m^renbk02DSW*xs&Mv<3!MerW^#^qHnkG-48zXacXU7f>D-T(jBVtP@ zELtk!?I?1wD>P@F^Xy4ql((*ZvaMjA?)QR2pYJ_sQDHc2(BrcrGBGh#m#tCKJXNM+ zuH3uxrdk!VwteZdVK`%^UH>DhG%x4<-}PRz-{h>bzBW6)(XNsCL>msY?5TO->inRa zU;c__{D{|>=hMo&NO^ne90kXRFFQ9r-u3lX>*<`ZK)Zc;j4Rg7_&c}#I^+8%H5wsX z^sb$42-3(>2x3&2eEP9M)uU+%&SBHV1?zcw7#gg8u&UW}oNUeg#2G|K)!jR6+J{uqmy|{O+4I=S(A;YDugq zPw@Gzopr0Jad`*#H0l diff --git a/coretk/coretk/icons/prouter.png b/coretk/coretk/icons/prouter.png new file mode 100644 index 0000000000000000000000000000000000000000..b0ccf6641d7dccccc4502cbb2a8c5fcdf08eaa7e GIT binary patch literal 2590 zcmV+(3gPvMP)H!I{Uu%Ie?9gjg5_sjg5_sO}0UqHXs#M z%~kWFin?4;m132+n1Ese<}ok>Kt4wQ0>I}01_}6_fh!F30XU-oz44gqSXUtUpN#zo z4W^U>d|_WMV$Sufs815p8U~iYx{VB`lOR6Kih3w_#B(4R2;LH5uLv^kfcpA+<>=BA zkAZj#1DgTlNiK32Kr4uYIZ=0KFc3^gE-xh+bHM6`I`7Q{dM^ zZ+4Y+jby@rv^&5T_T?g>JOiemFmQjm%TERYpD@#Ia>aii_Jwbzx$N{&-cspfrac5K zN@uwSxCG)C+WoEXq^rQRIKUtBFNy=N0Vo%#yK4RlFcXemIVw^XU}_^*{JF3%d{M0M z-6*mP@P+*TIM4&Zm0y@whz$$YW8=bgD9jT)gb-J7+;P$u@(08UCn-vnaA{|$#}ReE z45l5TwF|s+ux-g!cpY8wBlBL{*i)G4O~;-Ep+~`|)u>5)UVsj#RW( z6pIuU=792W`Gf38ybD11Qv!_6cvjxGQdl~ZEKn1UgJn(C3q=VEaX?jbRRQA80|0~> zb|*%kY$-FNaflxzMeP(dCRlbrX=kY?rl{=z9u{EFU6|?HQ;iiOAmFPmS1erGS?aN> zW!V9asQWiE23)|}>_+CyXpF}kQTNMMMJzd>yruFxV0zZ75y=qo6w90$0RWhGY-+Bm zwjyELGJnXwh?xFhrSMdU>c=I|j6lS1SQ(U^W;4ctJpg8z&96n*h5hKdu>Y?A|Kb}z zPnqwRfB%&%lO|&}De9{L{AMznY5~5G-w(iNChv5FfXecgN>lY=*a5z14~@>PDAo; zIucLnNvO*Ktf;!gY;L0{>KZ-ibnOL=1rURmi zx*USyhYIrwvHg)c%*`nfAt#i4Pd@4vKP~boQ&AJjGR;KVDvHo300}?TGO`{F(ouJs z4p51OIsmxc)W0sKsary3*GM|*PIC|@ph%246Lg8NVc~kr&H1vB!P!*#3)YG1?~}rS zbq4^LpX{GnCsTS^F>e|2y|d8N*MdkQB1BUQufvO)B7e$m(CF_=Iy+-ZT6cG%XB{xp zkR>K4%qzsUC0nJ1k=#jI#~@}-48k`6$nTsndeNe(uLb?X{lc|-K0S`X$Pj9Z{ON23pJ^q~(*iU_L~u27 z4GkadlT!KP^bhyrjgx!v-&r4~9Q4zYt zY;MEE;vA4W;yFOTkTsLC2Lgt2qVB_b(&}wgtY!E}QgJ4J$G`=n`KCJrocnBO*6T*| z8FoO}7rv>8ttft=;L9Y07 z9Cv&hz-+S`sVH5v!Blukvb~}YFZbZxfo>6IC1cQ`s5{MOHXW7{_JuDJ(-sKYs-8&q zz|Y4^8=tY}yc%u2YUF9G00ScQCStT77d3uN z!XI`h8`|z`|5#DfjR2&)as0!}J-XhKt~Z@YE~-pxMZQ&CXmD;@Wm|7d zRUQS?KSk<}qxEFVobfPl+CltyXLU$uoRskB=c05tJgdqMvm0w>Nb41)aCN9XC+1qrzzbmdIGtr{ za0$TkH?GfKoX*MzGWa34(#}$kGwQAb(~Ar|lp!d&`38XN7`@MS1wbE| z&ak5Px>e;^s5&(D;^A#z_PXGV_07*qoM6N<$f*@Dt Ae*gdg literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/rectangle.png b/coretk/coretk/icons/rectangle.png index f8a9581ee1df0f3e5795b74658e731af2f23f45b..ca6c8c06a1b6ae83e78bb593209b5820fd314f01 100644 GIT binary patch literal 259 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoY)RhkE)4%caKYZ?lYt^TJY5_^ zD(1YsxRJ9VK!DY8tKsYoKC?F%^|JDoJ~W;j_i4vmHJ~yOa5LQ>_wDhY!s9=-ef@a- z^nL3c!U}2*J`FP%3>lM{j<9ra3ea2t;u3}f?s{*ZGZ*|h(Xa(1;_2$=vd$@?2>=~K BO+f$v literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCwj^(N7l!{JxM1({$v}}HPZ!6K zjK;U;H*z*OFt8lFek|vZQSPBr_k0XQT6pK2)Y~n~%U-Q9ov({XW@ gD>xtnLwy-z)n8@{hR1x1fTlBey85}Sb4q9e09ofZqyPW_ diff --git a/coretk/coretk/icons/rj45.png b/coretk/coretk/icons/rj45.png index baf4f9e80fb8632f29c5951d6ed6dbc32a1c5a98..c9d87cfdc52472db6adb4f7d234172e13e7a2b68 100644 GIT binary patch literal 1121 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoY)RhkE)4%caKYZ?lNlIT#5`Rb zLn`LHoxMLxG*F;D-TXSMp_*c=n7FA(fE z>z>@)EVw-7$J=zq+x^;W6}P(P**2)1;bFUcXV%Tu%(7nAxdH)!bxqO1N*l9vUbHl5 zzS)>wtRJ$E>6X%a)^CT;WS(N(!pgK^`{WCcuSZ(VRs9sC(LHkVyyocTD_-~X9?hZL8`E~cuZ zS8B@wV^2#bB}`B0;D5sVQ$;kOt)MyDxvAY^9q*r84JYK1c|2JKF1eQZ$|^VPuQGU` zTIkH^$r>W6x1rl0iCgHz>ZXJqK^5T*e{w2T@kJE8HdwG@dCKoq*;>=)y`L{;o;u0L zq3@~lBoVh<$3Ia_4K*bP_UmwXsx3HF5FMmm*|BjeW1vYG=SdcYOPiMHebwb)VBicp z;yY7CIDp+sUrXdpN>cXfD2p&gPnIv{D>bzLJ-24e*|uO6-{rr)N%0aYDuM+$<>hl- z)D0Mp-f3#t>ca5z-1Xg$j`2+5SmCuteeKo=Mg|6#ofT(fCUH0%TKy=g*_eSrAz_DF z{+&50OisPsm!ejS3NUpZKcm>$VDwb=38=DX(W`)cyP z{qjB0ofiH_2(u!ey7m?)W^T8dy)ZYX*u3x$umA56{pKd?USls z(*i!euqn%#l*s@9dS%&_wEmzge`>KLH(=SElC*|c)M80)phLO1%Q@P`pMc3xY|?ON z@eL4wcuX*twOEoH$jHbOAX@whFl1tr#IhDk`Ux2Mm}G(D6-#;p%;GL*M%H3Egd-&H zG9SVbiux-jf2axMMdyj6 zF`^h3(lVl_F}*|?Xb|s-B04!Rpdpc*GeDF9CcY+WkrN=K>oL7V8L*ZHk`^>TvSv}v z8A5Gf^L2IBAr+DyG5}82oa5rgn}c)Oe$)mGHP1Mphjy8ee|uMI9(0Z1lBI}y@o&UX|nuf{o)zlviYva zk^GJ9f3x%k)W5_&ZVpusj-)L81X=?}O|*fX?IG-J50ie5;rLEqQD_YuHLZb`mL=y8 Yt>^W4O{6Fm000002uVdwM6N<$g8j)p@&Et; diff --git a/coretk/coretk/icons/router.png b/coretk/coretk/icons/router.png index a42623a49499cc37ad0986163ddcb67b6f53dd9a..1de5014a578496e2d28f8850d20a3a19e2fbb367 100644 GIT binary patch literal 2082 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 Q*8l(j07*qoM6N<$g0Q%$(EtDd diff --git a/coretk/coretk/icons/run.png b/coretk/coretk/icons/run.png new file mode 100644 index 0000000000000000000000000000000000000000..a39a997fe2f2bccbfd829279511721daef6074ae GIT binary patch literal 1805 zcmV+o2lDudP)#}Y^SQ)msqj7`t-F4-%DMeHpf%T3L?PK9`^m++1GXI z*v%$3xDZ1q2&>oDoM=C?=Rj(nR)~Sz?Hg@BQ(}`30Cz_s`_R^wHs_8DaD0&Ku62*T zmMF2w2Y`6o@I2k~WO-_y;{tf}#Gb+4iGul_LQ+}nn?CpT!H21Njtg*SDD*-k9Cfo?~)F2LAG?EEarTt<#+6r~u4N6XR2xob0ohh&RMPIg%|R92Nu{r2sFQFFf6;c@Id{K2yhcUdg(`^UeU zo9nC(0Jk{VUooKp7c%5K!_gl05%82#{S_IpXrk=Od32y{MEnUg+Xr-OYJ6%5QdfF+r;3krPlKuVO1nO^LF*MEFAhf3A~ zd!f(kfC2j~fc5gkOs6M%xfVd9O??$RptsC+K7xy<+fM=17}TX;qo&+#jc!u00`8IBkPo5|DVJb0f_jV zDN86oUnt`rr05CMil>l4&I^z*$SiNl3kxtbPsSY(V2=-&ynWAqGnqKq z%WDAranS>Plj+OPXRQ@@ey!7CDr-g~jNM=SJCk1JCscSv0`S0rdYTh}OGUZ`F!Nco z%{m>TgmC#uFfbTSw}36nJ+g--h3-3vwDh{g`7J-dJQffxG9SQZ>f|>hYltiyuxa^e zO8NH>43DHu{<5WY1)F>T=)RL~sn!-C-T?tN3(&@_0oWm`6P!%{QBBKdhjgXqMq1gX z#s+zV%MslGu8+@JK!t~}vRb%U7;)Wu$*Nsj1=!se+XA3QRL3m9))WD*ngv*2I01Ub z6PQkUd$N8pK@%_5J*|yodHrXr07T;WPb9_|f0(`mr8;1<1#I51UgGF-(WX>ZMo0_rk9MDUCX9zna9nmeQ(#z>=`3beFtgC7$#%@{o!;AIMNxz zkmO&&jhMwN zi2Wf0Zv8?jV6_7>tRtYQ*mWmt5dn4Q@&i~$Ks8V1m$2ropVw$IK!j#dc`)S8zrWu{ zSXL=sf}(NH#a!ByF2Lzq(+vQYi|V-b4meP&i9f6+>TPYLd%3hJAr?f(f`|OCBvT2^ z+d>dG@zHY&^&MR!>+NTfye)u!Ay0WV~jvp)eR$y1M#gesOnLlcR7E(yo#oBeMssqh`Dq>!T{&C z|4$-S0D|}ma$%F*6{H!t=KwCt!(FvEb1NJ z?+Efrops@d2n(38{p-qvAE1bkVgWJMH^^wml@OoF=W!_lcs$0qjCNc|R3m(xPXkf} z*tfR)I*|TiRtcbk>CL_MKFePf4c_)b4@RL- vC=?2XLZMJ76bgkxp-?Ck3WY+UC~p1(pRFLwce)IN00000NkvXXu0mjf6QxXk literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/select.png b/coretk/coretk/icons/select.png index 9063d308a02c5693c78f9cf680d8a153bbcf7d36..04e18891bc3a3a1c3cea14a66c03019ecae22d20 100644 GIT binary patch delta 1034 zcmV+l1oive0*(kFiBL{Q4GJ0x0000DNk~Le0001B0001B2nGNE0OFW;IFTU~e*?ow zL_t(|ob8-BXjD-YhCfY=iVNZr#GtsKg1DfC25lr33O06Pqc&P9+S$1jiiN0^VB-Q7 zHX@cuAwp0g1SExuh$Mn$bHPN78gnh)Oo)?N?kxA7-22W42c~#)-ha>k=DoasUIr9J zQ4~c{6h%=KMNt&R9d*Dtpa|vdO2z7pqK#$_ylYhQj)`gwFu(&~f2MEYu}}*G z4A2J5^{hP#YGr@{o&gK}$`7Af8eo7fV42qy1Wv6DFu+@2wa-O_j0*z{@B!H9aUH?p z$^b)>r0(#ylrV8=fI*YglDIa&ph;>`d>CNRB()&E3@~Vtniro2WJ{CO2}l@V&?I#n zDFX~}8%0uYD z)Fi3WgDzI7NK&PTT&ki^Ql$rMQ&A_W(!;f_zj*j0NQ6^HV4s!p23Y5E2WmiZMu7$# zYM$<@aU?t^tOLdh>2ten^#Yq6&0w4~XX8JC0g^!vWU4Py-6Vc}6Vl-d`c*_5S z?FITXulq@cJS-(p8rP?eQDBX?)l^B07vVbKgtUmMjE@WGFtEtif~q03PrxtWiN$tv zfa8v}1WTny^><(|u#)6FZ}Vs5EI&&Mi%Nz+1hh=(@zC;pM_n%_M9LlhXP`N!M~mfq zyU>TRCQ!WW*bMC*rEjKY1A~_Rw$^$P zAyaBtFM%e@UKcI;8Q@OsvFZiW8o^3hto!K_gV?N^7PABUx9hm2#I&80f3bF8XTqr}_oEcxFm?vK z2WALqjG6}EBX%Yn64DqwXR$M&Lr7orG?IKBX=B1}8Hw?87drzUNePUdZP*zw25b=0 z7(MOSnQ&7|VC)>k&VUhMsgTB~sV9lDZA>^Xq%nHVU}wM}`n|Ce76U)9Ghx4wMj*V$ zQfwIn?pbcTV9KSCQp9Qiz;(;*`kkssIjs~w{RHy_U8rm#d8Vk40jK4prH5wVHItA0 zDe+TgX?bX=4%i8-CAmO9#=}2fD2k#eilQirq9{|4fA_FisMx}b!vFvP07*qoM6N<$ Ef=v(3_W%F@ delta 339 zcmV-Z0j&Ox2;2f8iBL{Q4GJ0x0000DNk~Le0000O0000O2nGNE0N{5$_>mzLe*qCm zL_t(YiM7_fE(B2&0N`)^6beM45gl5+LdgSYRT_yxBk=}`EforHun)is=rtONj!H!0 zCy}sg?N!XkOk`$uC$slfH|I{ycTVPJ&QQMEXjPmyUIFKrhzSIAFdYLJ&;198HOOd#M3J9UBY8bC-3 z+WVhmssac@3h3ck^0J@O{R$TFI!XttvM!FA0A=@jRrVQq5NQ})$hz2#0c_`h8xejEDO!@b@P}-c lJvi9_=}T_efn=N(u3yq_kEx!A7sCJm002ovPDHLkV1n)GidFys diff --git a/coretk/coretk/icons/start.png b/coretk/coretk/icons/start.png index 191d66367faf2e6433a4dbec7df0deb8f571b007..719f4cd95f744e42b1efd56e43b7e876538a4c09 100644 GIT binary patch literal 3010 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 Ef+`}3D*ylh delta 581 zcmV-L0=oUe7t919iBL{Q4GJ0x0000DNk~Le0000m0000m2nGNE09OL}hmj!^e*ytX zL_t(&f$f(uXj4HL$A9-Osfnne)j_eYB7%y81Y9gQIJoHK)=4KfCkqxz(TW)wXa{Yn z2v(Qk;N}n&Crfn@5k#t+6dg3u7J@Iyd!K_i2)WqE-90V$TmO0g`+o4fdjtZ3Kp;z0 zx3Do(9uzHAfm6VCPc!{uU5To-e>Im&wWar_9~lK2L*>C#%WGi3$#@&59*I7@_37eD zr!X=-C0eRvKRm!dN-}8*%>qBy=HEb>XE6%kf9JbVG6mfP{tJRY)&zm9u`aM>bON=_ z-vwq}Aa~*<#ar`42M^hAQFda(S!W{;pA_$e%q&acYie?>=j_-oFFU{sRhIN5< z?GerC3esrUuI`9+f%wLl?aHo%e`Hw`1hQsR3#@sY;?Jy_0;7Nu_1MiUy`!LJ%8UYu zk`dsmlZkqx-cIz0DKrY~c=BR75`6?=fo#@kO~HdC(dE*!#Sa;r8y^S+0vq55fyRCj T^j?Jl00000NkvXXu0mjfTzv~| diff --git a/coretk/coretk/icons/stop.png b/coretk/coretk/icons/stop.png index 1db886c8eeb185cf1db33c246a3a9e999abd53ff..1e87c929740d1a896decf7f4eca06e55ad65c745 100644 GIT binary patch literal 2305 zcmV+c3I6tpP)^Hgx(Sl}$FqzCbBOD$t>7#Uddk*+i#Pr6@M< zo9ub~klnD^Y&QFH?!8Oy{e9ZKFVFeU`R}>+?mg!`2e2^}GFw18bZ*}5bQaw#XOLS; z%o9X+Fi=8_c>utK0Rs9Y^Z$gv0D(gz!wWjgUp+Kpvh*3rI|HtGc9j&7T*u7o3Ai7? zau8eqvK<~TXDCtbg5?9#Ik-3<7kwzjZ1Jm)C}?FUe#S2;Q}#6&OPCJvOI z_6_S%fG!5~cUy*uDPr#iM$0~nxQkM~I?`W1izm9g`HF(!J> z{h<3<+P`0_3aZS2kE*`?xv(f`0%(h>n0d-c$pt&hg1`NjieO3%=&owGj|9ICV6lpL zw*jGu;7v;c{;RnL61fKG_0+8<;xn*L)AY}W^BN<%7$`H&L=EsU1_`TN+u^|WI$)_6AKH%@*=Et!_9<{ zL-6W*1O6N7o{;uUm;rKx)y~X<6Gbv4RAR7 z_5xU@M3%K!O7r_5!F(wlC==DCF^0F!*t03II62P_8nZt~kG_{F!5G&$jwi z$#x{a2M9a^)&0dH=dK6i&_OFRo=#d+;)Y(&u6v?`rfk$w)4iqP?!=8#3}6x2iToL{ zHk;7MA-|Kjaf$(wvC|km7KErSanmFNLLP5LbV#_3K0Mgv@jej0VR8!~+HR;E3qlEX zoOlWPnt@s@2{AUtf1fbmif2~|fTYi=HVPp;^zYi*iN5p+1BwuN4E8H$BPbXcnYT9f z<<=#Lc)cldtO#L4?Bgv1m{yvh#flK~irDA47J&OqQDa5o>RJHf-HxzFI~Yl6w9dy3 za2DMxhyD7&NVI~Yn`J=M02vW3bM;sdCN~f@fCc88sl|#As04@_Ab=8cZ(Y;t4#5(M;_EyQX_qECU?B}hw9Ps_O{@7`SAP9P2^Ee~WzFZ}xd1lPBf+O*pC=5M zTR3nAz^EC5EJk?F5YEQFOc)T&D0|TqNfv^E3wOWKJP`Yy_%iO4!FsI7Tk&5e8Nfs@ z8>_{7I54@6CmB%M>iY|TzZ$B@TKsuYn{U$7=G6WGrlW?*u^0?ACvKZ!fPi=r_G<|} z7{e&Nl(=z<0gHo;9|34JM3R*t1WJGJ`#5oP(k@9R`Z=I*k}v>BWw}oEN4-`?L>x%A zBdGz=oAK)^6^#77Q)M_^8M&NuI?zcl@XB;)@uy#@Lh!SYq%7n4;Q9A({rQ7%F3<0D z(jy($l+Ka|07CL#|Mg zTEddx1rS!E%o>ami-vR-l1po7Vp*&IT`)b9yDn?Mz#ezokw2z$Y}zSl7@*#54FP7h zGpVHJxT~$HiqiZeoA%&b0$3yYT>znMmCbq>U0R{Tn{MR;8IF#=H{idaapW5Slq}yd zt6*H0(6%gUIsBhYPRn?B`l8^`4+YCg*yBm#=_7&7Wx>XbpBA&4AFw#se36*8!+tUX zfG~l-q#C*M;y48}zaaeCbhZ`)Z zvb5cIK9?X$G9zBA@h;OOy#}CEMZCO+(WN1>X;JX#hq;DQGDmDlz<<>tcnyH#D&plc z476)b#~Kxt59qg);R~33t1IRAQikz_YP^p?k`J>c?LBuexW2Tl`GV@mx=d4b2mI$g z`Mh%_G5-L-?K}Z+h>3nW8ouL8dQ?7Oz=F=9s=88;I0#CfygFYYqPB?8zOVOM^M)-g z9`blAAZRZbJB%zh3r@16ALsgbnJ$J`M9AZ<0MR}GHOQYFMG+8=Nz$Gx3pBoOWWlEW z68~A_{cMD3J2Sih9yLkEWdi%g#qz={CM5Gp(1UaAi17F_XAiCz>QR6n36Mw@xF@=0$-Bg({qM!HhywM zK9F|?B;$kH+QQ;+@lvLUi=-%q;5%7zF%cC505kWKpngdDFALcZ;?RI&_}Yq=7W-|j bO=|uR`17L!0e5Q$00000NkvXXu0mjf09{27 literal 145 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCwj^(N7l!{JxM1({$v_b|PZ!6K zjK;U;H}W#8 b_z*AuI)zzdTjAtdpot8gu6{1-oD!M<$_gx= diff --git a/coretk/coretk/icons/text.png b/coretk/coretk/icons/text.png index 4933dabbc40d0694ad46369a884685bb463f1f04..14a85dc02261ccdfc217bad1a38ff926fffcab17 100644 GIT binary patch literal 314 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoY)RhkE)4%caKYZ?lYt`dJzX3_ zD(1Ys>&VySAi&_L^6&rYs;6m6hs}Ol*YgHT1G9@vg$QUwG vUEqf7{zl94q^+)4-KyO%F9_N*uF6a@fbW^{an^LB{Ts5sLo;l delta 134 zcmdnRG>K8MGr-TCmrII^fq{Y7)59eQNK1e)2OE%lP;UEUqM~SwyQhm|h(~8~f&?oI zgRVcRC&BU=p(_`cdP)<$UHl=AeW1Y(kGap4mxkO}Nq&$lcb=dc(^#havo@ ibHM)v+P>}z3=GRQu_>PP{PYZHG=rzBpUXO@geCw70xO09 diff --git a/coretk/coretk/icons/twonode.png b/coretk/coretk/icons/twonode.png new file mode 100644 index 0000000000000000000000000000000000000000..6828db8e7f9d48f656fe9ce4ad6902aa50c5f51f GIT binary patch literal 2494 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 literal 0 HcmV?d00001 diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 8f13c593..3763f2d2 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -53,9 +53,9 @@ class ImageEnum(Enum): HOST = "host" PC = "pc" MDR = "mdr" - PROUTER = "router_green" + PROUTER = "prouter" OVS = "OVS" - EDITNODE = "document-properties" + EDITNODE = "edit-node" PLOT = "plot" TWONODE = "twonode" STOP = "stop" diff --git a/coretk/coretk/icons/docker.gif b/coretk/coretk/oldicons/docker.gif similarity index 100% rename from coretk/coretk/icons/docker.gif rename to coretk/coretk/oldicons/docker.gif diff --git a/coretk/coretk/icons/emane.gif b/coretk/coretk/oldicons/emane.gif similarity index 100% rename from coretk/coretk/icons/emane.gif rename to coretk/coretk/oldicons/emane.gif diff --git a/coretk/coretk/icons/link.gif b/coretk/coretk/oldicons/link.gif similarity index 100% rename from coretk/coretk/icons/link.gif rename to coretk/coretk/oldicons/link.gif diff --git a/coretk/coretk/icons/lxc.gif b/coretk/coretk/oldicons/lxc.gif similarity index 100% rename from coretk/coretk/icons/lxc.gif rename to coretk/coretk/oldicons/lxc.gif diff --git a/coretk/coretk/icons/marker.gif b/coretk/coretk/oldicons/marker.gif similarity index 100% rename from coretk/coretk/icons/marker.gif rename to coretk/coretk/oldicons/marker.gif diff --git a/coretk/coretk/icons/mdr.gif b/coretk/coretk/oldicons/mdr.gif similarity index 100% rename from coretk/coretk/icons/mdr.gif rename to coretk/coretk/oldicons/mdr.gif diff --git a/coretk/coretk/icons/router_green.gif b/coretk/coretk/oldicons/router_green.gif similarity index 100% rename from coretk/coretk/icons/router_green.gif rename to coretk/coretk/oldicons/router_green.gif diff --git a/coretk/coretk/icons/run.gif b/coretk/coretk/oldicons/run.gif similarity index 100% rename from coretk/coretk/icons/run.gif rename to coretk/coretk/oldicons/run.gif diff --git a/coretk/coretk/icons/twonode.gif b/coretk/coretk/oldicons/twonode.gif similarity index 100% rename from coretk/coretk/icons/twonode.gif rename to coretk/coretk/oldicons/twonode.gif diff --git a/coretk/coretk/icons/wlan.gif b/coretk/coretk/oldicons/wlan.gif similarity index 100% rename from coretk/coretk/icons/wlan.gif rename to coretk/coretk/oldicons/wlan.gif From 059b0cc316b0d1b6a7f63d11814632c1fd82f759 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 12:44:50 -0800 Subject: [PATCH 249/462] changes to fix session adding default emane configuration --- daemon/core/emulator/session.py | 6 +++--- daemon/tests/test_grpc.py | 12 +++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 4852d026..d2d841e4 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -704,10 +704,10 @@ class Session: self.services.add_services(node, node.type, options.services) # ensure default emane configuration - if _type == NodeTypes.EMANE: - self.emane.set_model_config(_id, node.emane) + if isinstance(node, EmaneNet): + self.emane.set_model_config(_id, options.emane) # set default wlan config if needed - if _type == NodeTypes.WIRELESS_LAN: + if isinstance(node, WlanNode): self.mobility.set_model_config(_id, BasicRangeModel.name) # boot nodes after runtime, CoreNodes, Physical, and RJ45 are all nodes diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 5f934b64..28005dbb 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -692,7 +692,9 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() session.set_location(47.57917, -122.13232, 2.00000, 1.0) - emane_network = session.add_node(_type=NodeTypes.EMANE) + options = NodeOptions() + options.emane = EmaneIeee80211abgModel.name + emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) session.emane.set_model(emane_network, EmaneIeee80211abgModel) config_key = "platform_id_start" config_value = "2" @@ -716,7 +718,9 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() session.set_location(47.57917, -122.13232, 2.00000, 1.0) - emane_network = session.add_node(_type=NodeTypes.EMANE) + options = NodeOptions() + options.emane = EmaneIeee80211abgModel.name + emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) session.emane.set_model(emane_network, EmaneIeee80211abgModel) config_key = "bandwidth" config_value = "900000" @@ -742,7 +746,9 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() session.set_location(47.57917, -122.13232, 2.00000, 1.0) - emane_network = session.add_node(_type=NodeTypes.EMANE) + options = NodeOptions() + options.emane = EmaneIeee80211abgModel.name + emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) session.emane.set_model(emane_network, EmaneIeee80211abgModel) # then From b983a09ae7f49fbf82e5135ad5ba479c8277c9c0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 14:41:05 -0800 Subject: [PATCH 250/462] small cleanup to emane config dialog, fixed default service storage to just use names --- coretk/coretk/coreclient.py | 33 +- coretk/coretk/dialogs/emaneconfig.py | 478 +++++++++++---------------- coretk/coretk/emaneodelnodeconfig.py | 2 +- coretk/coretk/graph.py | 10 + 4 files changed, 209 insertions(+), 314 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index beeb1c0a..dbcbcf18 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -50,6 +50,7 @@ class CoreClient: self.master = app.master self.interface_helper = None self.services = {} + self.emane_models = [] self.observer = None # loaded configuration data @@ -70,6 +71,7 @@ class CoreClient: self.interfaces_manager = InterfaceManager() self.wlan_configs = {} self.mobility_configs = {} + self.emane_model_configs = {} self.emaneconfig_management = EmaneModelNodeConfig(app) self.emane_config = None self.serviceconfig_manager = ServiceNodeConfig(app) @@ -145,6 +147,10 @@ class CoreClient: self.state = session.state self.client.events(self.session_id, self.handle_events) + # get emane models + response = self.client.get_emane_models(self.session_id) + self.emane_models = response.models + # get hooks response = self.client.get_hooks(self.session_id) logging.info("joined session hooks: %s", response) @@ -240,8 +246,8 @@ class CoreClient: # get service information response = self.client.get_services() for service in response.services: - group_services = self.services.setdefault(service.group, []) - group_services.append(service) + group_services = self.services.setdefault(service.group, set()) + group_services.add(service.name) # if there are no sessions, create a new session, else join a session response = self.client.get_sessions() @@ -443,6 +449,9 @@ class CoreClient: image = None if NodeUtils.is_image_node(node_type): image = "ubuntu:latest" + emane = None + if node_type == core_pb2.NodeType.EMANE: + emane = self.emane_models[0] node = core_pb2.Node( id=node_id, type=node_type, @@ -450,6 +459,7 @@ class CoreClient: model=model, position=position, image=image, + emane=emane, ) # set default emane configuration for emane node @@ -571,25 +581,6 @@ class CoreClient: if interface_two is not None: self.interface_to_edge[(node_two.id, interface_two.id)] = token - # emane setup - # TODO: determine if this is needed - if ( - node_one.type == core_pb2.NodeType.EMANE - and node_two.type == core_pb2.NodeType.DEFAULT - ): - if node_two.model == "mdr": - self.emaneconfig_management.set_default_for_mdr( - node_one.node_id, node_two.node_id, interface_two.id - ) - elif ( - node_two.type == core_pb2.NodeType.EMANE - and node_one.type == core_pb2.NodeType.DEFAULT - ): - if node_one.model == "mdr": - self.emaneconfig_management.set_default_for_mdr( - node_two.node_id, node_one.node_id, interface_one.id - ) - link = core_pb2.Link( type=core_pb2.LinkType.WIRED, node_one_id=node_one.id, diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index 2c885511..046b0d62 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -1,7 +1,6 @@ """ emane configuration """ - import logging import tkinter as tk import webbrowser @@ -11,271 +10,122 @@ from coretk.dialogs.dialog import Dialog from coretk.images import ImageEnum, Images from coretk.widgets import ConfigFrame -PAD_X = 2 -PAD_Y = 2 +PAD = 5 -class EmaneConfiguration(Dialog): +class GlobalEmaneDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "EMANE Configuration", modal=True) + self.config_frame = None + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + self.config_frame = ConfigFrame( + self.top, self.app, self.app.core.emane_config, borderwidth=0 + ) + self.config_frame.draw_config() + self.config_frame.grid(sticky="nsew", pady=PAD) + self.draw_buttons() + + def draw_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, sticky="ew", padx=PAD) + + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_apply(self): + self.config_frame.parse_config() + self.destroy() + + +class EmaneModelDialog(Dialog): + def __init__(self, master, app, node, model): + super().__init__(master, app, f"{node.name} {model} Configuration", modal=True) + self.node = node + self.model = f"emane_{model}" + self.config_frame = None + session_id = self.app.core.session_id + response = self.app.core.client.get_emane_model_config( + session_id, self.node.id, self.model + ) + self.config = response.config + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + self.config_frame = ConfigFrame(self.top, self.app, self.config, borderwidth=0) + self.config_frame.draw_config() + self.config_frame.grid(sticky="nsew", pady=PAD) + self.draw_buttons() + + def draw_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, sticky="ew", padx=PAD) + + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_apply(self): + self.config_frame.parse_config() + self.app.core.emaneconfig_management.set_custom_emane_cloud_config( + self.node.id, self.model + ) + self.destroy() + + +class EmaneConfigDialog(Dialog): def __init__(self, master, app, canvas_node): - super().__init__(master, app, "emane configuration", modal=False) + super().__init__( + master, app, f"{canvas_node.core_node.name} EMANE Configuration", modal=True + ) self.app = app self.canvas_node = canvas_node self.node = canvas_node.core_node self.radiovar = tk.IntVar() self.radiovar.set(1) - self.columnconfigure(0, weight=1) + self.emane_models = [x.split("_")[1] for x in self.app.core.emane_models] + emane_model = None + if self.emane_models: + emane_model = self.emane_models[0] + self.emane_model = tk.StringVar(value=emane_model) + self.emane_model_button = None + self.draw() - # list(string) of emane models - self.emane_models = None - - self.emane_dialog = Dialog(self, app, "emane configuration", modal=False) - self.emane_model_dialog = None - self.emane_model_combobox = None - - # draw - self.node_name_and_image() - self.emane_configuration() - self.draw_ip_subnets() - self.emane_options() + def draw(self): + self.top.columnconfigure(0, weight=1) + self.draw_emane_configuration() + self.draw_emane_models() + self.draw_emane_buttons() self.draw_apply_and_cancel() - self.emane_config_frame = None - self.options = app.core.emane_config - self.model_options = None - self.model_config_frame = None - - def create_text_variable(self, val): - """ - create a string variable for convenience - - :param str val: entry text - :return: nothing - """ - var = tk.StringVar() - var.set(val) - return var - - def choose_core(self): - logging.info("not implemented") - - def node_name_and_image(self): - f = ttk.Frame(self.top) - - lbl = ttk.Label(f, text="Node name:") - lbl.grid(row=0, column=0, padx=2, pady=2) - e = ttk.Entry(f, textvariable=self.create_text_variable("")) - e.grid(row=0, column=1, padx=2, pady=2) - - cbb = ttk.Combobox(f, values=["(none)", "core1", "core2"], state="readonly") - cbb.current(0) - cbb.grid(row=0, column=2, padx=2, pady=2) - - b = ttk.Button(f, image=self.canvas_node.image) - b.grid(row=0, column=3, padx=2, pady=2) - - f.grid(row=0, column=0, sticky="nsew") - - def save_emane_option(self): - self.emane_config_frame.parse_config() - self.emane_dialog.destroy() - - def draw_emane_options(self): - if not self.emane_dialog.winfo_exists(): - self.emane_dialog = Dialog( - self, self.app, "emane configuration", modal=False - ) - - if self.options is None: - session_id = self.app.core.session_id - response = self.app.core.client.get_emane_config(session_id) - logging.info("emane config: %s", response) - self.options = response.config - - self.emane_dialog.top.columnconfigure(0, weight=1) - self.emane_dialog.top.rowconfigure(0, weight=1) - self.emane_config_frame = ConfigFrame( - self.emane_dialog.top, self.app, config=self.options - ) - self.emane_config_frame.draw_config() - self.emane_config_frame.grid(sticky="nsew") - - frame = ttk.Frame(self.emane_dialog.top) - frame.grid(sticky="ew") - for i in range(2): - frame.columnconfigure(i, weight=1) - b1 = ttk.Button(frame, text="Appy", command=self.save_emane_option) - b1.grid(row=0, column=0, sticky="ew") - b2 = ttk.Button(frame, text="Cancel", command=self.emane_dialog.destroy) - b2.grid(row=0, column=1, sticky="ew") - self.emane_dialog.show() - - def save_emane_model_options(self): - """ - configure the node's emane model on the fly - - :return: nothing - """ - # get model name - model_name = self.emane_models[self.emane_model_combobox.current()] - - # parse configuration - config = self.model_config_frame.parse_config() - - # add string emane_ infront for grpc call - response = self.app.core.client.set_emane_model_config( - self.app.core.session_id, self.node.id, f"emane_{model_name}", config - ) - logging.info( - "emaneconfig.py config emane model (%s), result: %s", self.node.id, response - ) - - # store the change locally - self.app.core.emaneconfig_management.set_custom_emane_cloud_config( - self.node.id, f"emane_{model_name}" - ) - - self.emane_model_dialog.destroy() - - def draw_model_options(self): - """ - draw emane model configuration - - :return: nothing - """ - # get model name - model_name = self.emane_models[self.emane_model_combobox.current()] - - # create the dialog and the necessry widget - if not self.emane_model_dialog or not self.emane_model_dialog.winfo_exists(): - self.emane_model_dialog = Dialog( - self, self.app, f"{model_name} configuration", modal=False - ) - self.emane_model_dialog.top.columnconfigure(0, weight=1) - self.emane_model_dialog.top.rowconfigure(0, weight=1) - - # query for configurations - session_id = self.app.core.session_id - # add string emane_ before model name for grpc call - response = self.app.core.client.get_emane_model_config( - session_id, self.node.id, f"emane_{model_name}" - ) - logging.info("emane model config %s", response) - - self.model_options = response.config - self.model_config_frame = ConfigFrame( - self.emane_model_dialog.top, self.app, config=self.model_options - ) - self.model_config_frame.grid(sticky="nsew") - self.model_config_frame.draw_config() - - frame = ttk.Frame(self.emane_model_dialog.top) - frame.grid(sticky="ew") - for i in range(2): - frame.columnconfigure(i, weight=1) - b1 = ttk.Button(frame, text="Apply", command=self.save_emane_model_options) - b1.grid(row=0, column=0, sticky="ew") - b2 = ttk.Button(frame, text="Cancel", command=self.emane_model_dialog.destroy) - b2.grid(row=0, column=1, sticky="ew") - self.emane_model_dialog.show() - - def draw_option_buttons(self, parent): - f = ttk.Frame(parent) - f.grid(row=4, column=0, sticky="nsew") - f.columnconfigure(0, weight=1) - f.columnconfigure(1, weight=1) - - image = Images.get(ImageEnum.EDITNODE, 16) - b = ttk.Button( - f, - text=self.emane_models[0] + " options", - image=image, - compound=tk.RIGHT, - command=self.draw_model_options, - ) - b.image = image - b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") - - image = Images.get(ImageEnum.EDITNODE, 16) - b = ttk.Button( - f, - text="EMANE options", - image=image, - compound=tk.RIGHT, - command=self.draw_emane_options, - ) - b.image = image - b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") - - def combobox_select(self, event): - """ - update emane model options button - - :param event: - :return: nothing - """ - # get model name - model_name = self.emane_models[self.emane_model_combobox.current()] - - # get the button and configure button text - config_frame = self.grid_slaves(row=2, column=0)[0] - option_button_frame = config_frame.grid_slaves(row=4, column=0)[0] - b = option_button_frame.grid_slaves(row=0, column=0)[0] - b.config(text=model_name + " options") - - def draw_emane_models(self, parent): - """ - create a combobox that has all the known emane models - - :param parent: parent - :return: nothing - """ - # query for all the known model names - session_id = self.app.core.session_id - response = self.app.core.client.get_emane_models(session_id) - self.emane_models = [x.split("_")[1] for x in response.models] - - # create combo box and its binding - f = ttk.Frame(parent) - self.emane_model_combobox = ttk.Combobox( - f, values=self.emane_models, state="readonly" - ) - self.emane_model_combobox.grid() - self.emane_model_combobox.current(0) - self.emane_model_combobox.bind("<>", self.combobox_select) - f.grid(row=3, column=0, sticky="ew") - - def draw_text_label_and_entry(self, parent, label_text, entry_text): - """ - draw a label and an entry on a single row - - :return: nothing - """ - var = tk.StringVar() - var.set(entry_text) - f = ttk.Frame(parent) - lbl = ttk.Label(f, text=label_text) - lbl.grid(row=0, column=0) - e = ttk.Entry(f, textvariable=var) - e.grid(row=0, column=1) - f.grid(stick=tk.W, padx=2, pady=2) - - def emane_configuration(self): + def draw_emane_configuration(self): """ draw the main frame for emane configuration :return: nothing """ - # draw label - lbl = ttk.Label(self.top, text="Emane") - lbl.grid(row=1, column=0) - - # main frame that has emane wiki, a short description, emane models and the configure buttons - f = ttk.Frame(self.top) - f.columnconfigure(0, weight=1) + label = ttk.Label( + self.top, + text="The EMANE emulation system provides more complex wireless radio emulation " + "\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details", + ) + label.grid(sticky="ew", pady=PAD) image = Images.get(ImageEnum.EDITNODE, 16) - b = ttk.Button( - f, + button = ttk.Button( + self.top, image=image, text="EMANE Wiki", compound=tk.RIGHT, @@ -283,55 +133,99 @@ class EmaneConfiguration(Dialog): "https://github.com/adjacentlink/emane/wiki" ), ) - b.image = image - b.grid(row=0, column=0, sticky="w") + button.image = image + button.grid(sticky="ew", pady=PAD) - lbl = ttk.Label( - f, - text="The EMANE emulation system provides more complex wireless radio emulation " - "\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details", + def draw_emane_models(self): + """ + create a combobox that has all the known emane models + + :return: nothing + """ + frame = ttk.Frame(self.top) + frame.grid(sticky="ew", pady=PAD) + frame.columnconfigure(1, weight=1) + + label = ttk.Label(frame, text="Model") + label.grid(row=0, column=0, sticky="w") + + # create combo box and its binding + combobox = ttk.Combobox( + frame, + textvariable=self.emane_model, + values=self.emane_models, + state="readonly", ) - lbl.grid(row=1, column=0, sticky="nsew") + combobox.grid(row=0, column=1, sticky="ew") + combobox.bind("<>", self.emane_model_change) - lbl = ttk.Label(f, text="EMANE Models") - lbl.grid(row=2, column=0, sticky="w") + def draw_emane_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew", pady=PAD) + for i in range(2): + frame.columnconfigure(i, weight=1) - self.draw_emane_models(f) - self.draw_option_buttons(f) + image = Images.get(ImageEnum.EDITNODE, 16) + self.emane_model_button = ttk.Button( + frame, + text=f"{self.emane_model.get()} options", + image=image, + compound=tk.RIGHT, + command=self.click_model_config, + ) + self.emane_model_button.image = image + self.emane_model_button.grid(row=0, column=0, padx=PAD, sticky="ew") - f.grid(row=2, column=0, sticky="nsew") - - def draw_ip_subnets(self): - self.draw_text_label_and_entry(self.top, "IPv4 subnet", "") - self.draw_text_label_and_entry(self.top, "IPv6 subnet", "") - - def emane_options(self): - """ - create wireless node options - - :return: - """ - f = ttk.Frame(self.top) - f.columnconfigure(0, weight=1) - f.columnconfigure(1, weight=1) - b = ttk.Button(f, text="Link to all routers") - b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") - b = ttk.Button(f, text="Choose WLAN members") - b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") - f.grid(row=5, column=0, sticky="nsew") - - def apply(self): - # save emane configuration - self.app.core.emane_config = self.options - self.destroy() + image = Images.get(ImageEnum.EDITNODE, 16) + button = ttk.Button( + frame, + text="EMANE options", + image=image, + compound=tk.RIGHT, + command=self.click_emane_config, + ) + button.image = image + button.grid(row=0, column=1, sticky="ew") def draw_apply_and_cancel(self): - f = ttk.Frame(self.top) - f.columnconfigure(0, weight=1) - f.columnconfigure(1, weight=1) - b = ttk.Button(f, text="Apply", command=self.apply) - b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") - b = ttk.Button(f, text="Cancel", command=self.destroy) - b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) - f.grid(sticky="nsew") + button = ttk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, padx=PAD, sticky="ew") + + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_emane_config(self): + dialog = GlobalEmaneDialog(self, self.app) + dialog.show() + + def click_model_config(self): + """ + draw emane model configuration + + :return: nothing + """ + model_name = self.emane_model.get() + logging.info("configuring emane model: %s", model_name) + dialog = EmaneModelDialog( + self, self.app, self.canvas_node.core_node, model_name + ) + dialog.show() + + def emane_model_change(self, event): + """ + update emane model options button + + :param event: + :return: nothing + """ + model_name = self.emane_model.get() + self.emane_model_button.config(text=f"{model_name} options") + + def click_apply(self): + self.node.emane = f"emane_{self.emane_model.get()}" + self.destroy() diff --git a/coretk/coretk/emaneodelnodeconfig.py b/coretk/coretk/emaneodelnodeconfig.py index e3a9cc33..e1aaadbb 100644 --- a/coretk/coretk/emaneodelnodeconfig.py +++ b/coretk/coretk/emaneodelnodeconfig.py @@ -28,7 +28,7 @@ class EmaneModelNodeConfig: """ session_id = self.app.core.session_id client = self.app.core.client - default_emane_model = client.get_emane_models(session_id).models[0] + default_emane_model = self.app.core.emane_models[0] response = client.get_emane_model_config( session_id, node_id, default_emane_model ) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 86ce617e..e2722c77 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -7,6 +7,7 @@ from PIL import ImageTk from core.api.grpc import core_pb2 from core.api.grpc.core_pb2 import NodeType from coretk.canvastooltip import CanvasTooltip +from coretk.dialogs.emaneconfig import EmaneConfigDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog @@ -77,6 +78,10 @@ class CanvasGraph(tk.Canvas): context.add_command( label="Mobility Config", command=canvas_node.show_mobility_config ) + if node.type == NodeType.EMANE: + context.add_command( + label="EMANE Config", command=canvas_node.show_emane_config + ) context.add_command(label="Select adjacent", state=tk.DISABLED) context.add_command(label="Create link to", state=tk.DISABLED) context.add_command(label="Assign to", state=tk.DISABLED) @@ -711,3 +716,8 @@ class CanvasNode: self.canvas.context = None dialog = MobilityConfigDialog(self.app, self.app, self) dialog.show() + + def show_emane_config(self): + self.canvas.context = None + dialog = EmaneConfigDialog(self.app, self.app, self) + dialog.show() From 4cd42c2a20eb5a66c61c3ad080eb9a14f1870861 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 21 Nov 2019 15:00:17 -0800 Subject: [PATCH 251/462] work on service config --- coretk/coretk/coreclient.py | 20 ++++++++- coretk/coretk/dialogs/nodeservice.py | 12 ++++- coretk/coretk/dialogs/serviceconfiguration.py | 45 +++++++++++++++---- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index beeb1c0a..e3232ef8 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -50,6 +50,7 @@ class CoreClient: self.master = app.master self.interface_helper = None self.services = {} + self.default_services = {} self.observer = None # loaded configuration data @@ -77,6 +78,9 @@ class CoreClient: self.created_nodes = set() self.created_links = set() + self.service_configs = {} + self.file_configs = {} + def set_observer(self, value): self.observer = value @@ -253,9 +257,15 @@ class CoreClient: dialog = SessionsDialog(self.app, self.app) dialog.show() + response = self.client.get_service_defaults(self.session_id) + logging.debug("get service defaults: %s", response) + self.default_services = { + x.node_type: set(x.services) for x in response.defaults + } + def get_session_state(self): response = self.client.get_session(self.session_id) - # logging.info("get session: %s", response) + logging.info("get session: %s", response) return response.session.state def edit_node(self, node_id, x, y): @@ -358,6 +368,9 @@ class CoreClient: self.session_id, node_id, service_name, startups, validations, shutdowns ) logging.debug("set node service %s", response) + response = self.client.get_node_service(self.session_id, node_id, service_name) + logging.debug("get node service : %s", response) + return response.service def get_node_service_file(self, node_id, service_name, file_name): response = self.client.get_node_service_file( @@ -381,7 +394,10 @@ class CoreClient: node_protos = [x.core_node for x in self.canvas_nodes.values()] link_protos = list(self.links.values()) - self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION) + if self.get_session_state() != core_pb2.SessionState.DEFINITION: + self.client.set_session_state( + self.session_id, core_pb2.SessionState.DEFINITION + ) for node_proto in node_protos: if node_proto.id not in self.created_nodes: response = self.client.add_node(self.session_id, node_proto) diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 35c55b75..8a30e051 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -12,6 +12,7 @@ from coretk.widgets import CheckboxList, ListboxScroll class NodeService(Dialog): def __init__(self, master, app, canvas_node, services=None): super().__init__(master, app, "Node Services", modal=True) + self.app = app self.canvas_node = canvas_node self.node_id = canvas_node.core_node.id self.groups = None @@ -108,8 +109,15 @@ class NodeService(Dialog): ) def click_save(self): - print("not implemented") - print(self.current_services) + if ( + self.current_services + != self.app.core.default_services[self.canvas_node.core_node.model] + ): + self.canvas_node.core_node.services[:] = self.current_services + else: + if len(self.canvas_node.core_node.services) > 0: + self.canvas_node.core_node.services[:] = [] + self.destroy() def click_cancel(self): self.current_services = None diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index bb1fd858..20684311 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -14,6 +14,7 @@ class ServiceConfiguration(Dialog): def __init__(self, master, app, service_name, node_id): super().__init__(master, app, f"{service_name} service", modal=True) self.app = app + self.core = app.core self.service_manager = app.core.serviceconfig_manager self.node_id = node_id self.service_name = service_name @@ -50,18 +51,32 @@ class ServiceConfiguration(Dialog): def load(self): # create nodes and links in definition state for getting and setting service file self.app.core.create_nodes_and_links() - # load data from local memory - if self.service_name in self.service_manager.configurations[self.node_id]: - service_config = self.service_manager.configurations[self.node_id][ + + service_configs = self.app.core.service_configs + if ( + self.node_id in service_configs + and self.service_name in service_configs[self.node_id] + ): + service_config = self.app.core.service_configs[self.node_id][ self.service_name ] else: - self.service_manager.node_custom_service_configuration( + service_config = self.app.core.get_node_service( self.node_id, self.service_name ) - service_config = self.service_manager.configurations[self.node_id][ - self.service_name - ] + + # # load data from local memory + # if self.service_name in self.service_manager.configurations[self.node_id]: + # service_config = self.service_manager.configurations[self.node_id][ + # self.service_name + # ] + # else: + # self.service_manager.node_custom_service_configuration( + # self.node_id, self.service_name + # ) + # service_config = self.service_manager.configurations[self.node_id][ + # self.service_name + # ] self.dependencies = [x for x in service_config.dependencies] self.executables = [x for x in service_config.executables] self.metadata = service_config.meta @@ -359,16 +374,30 @@ class ServiceConfiguration(Dialog): entry.delete(0, tk.END) def click_apply(self): + service_configs = self.app.core.service_configs 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") - self.service_manager.node_service_custom_configuration( + config = self.core.set_node_service( self.node_id, self.service_name, startup_commands, validate_commands, shutdown_commands, ) + if self.node_id not in service_configs: + service_configs[self.node_id] = {} + if self.service_name not in service_configs[self.node_id]: + self.app.core.service_configs[self.node_id][self.service_name] = config + print(self.app.core.client.get_session(self.app.core.session_id)) + + # self.service_manager.node_service_custom_configuration( + # self.node_id, + # self.service_name, + # startup_commands, + # validate_commands, + # shutdown_commands, + # ) for file in self.modified_files: self.app.core.servicefileconfig_manager.set_custom_service_file_config( self.node_id, self.service_name, file, self.temp_service_files[file] From 97cb1444f3e60ffb0f410e310e3f04789b3281d5 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 16:41:38 -0800 Subject: [PATCH 252/462] updates to emane model config storage, fixes to reconnecting to a wlan session --- coretk/coretk/coreclient.py | 86 +++++++++++---------------- coretk/coretk/dialogs/emaneconfig.py | 13 ++--- coretk/coretk/emaneodelnodeconfig.py | 78 ------------------------- coretk/coretk/graph.py | 87 +++++++++++++++------------- coretk/coretk/graph_helper.py | 30 +++++----- coretk/coretk/linkinfo.py | 1 - coretk/coretk/nodeutils.py | 7 +++ coretk/coretk/wirelessconnection.py | 10 ++-- daemon/core/nodes/network.py | 2 +- 9 files changed, 111 insertions(+), 203 deletions(-) delete mode 100644 coretk/coretk/emaneodelnodeconfig.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index dbcbcf18..229f59a5 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -6,7 +6,6 @@ import os from core.api.grpc import client, core_pb2 from coretk.dialogs.sessions import SessionsDialog -from coretk.emaneodelnodeconfig import EmaneModelNodeConfig from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils from coretk.servicefileconfig import ServiceFileConfig @@ -72,7 +71,6 @@ class CoreClient: self.wlan_configs = {} self.mobility_configs = {} self.emane_model_configs = {} - self.emaneconfig_management = EmaneModelNodeConfig(app) self.emane_config = None self.serviceconfig_manager = ServiceNodeConfig(app) self.servicefileconfig_manager = ServiceFileConfig() @@ -104,7 +102,7 @@ class CoreClient: def handle_events(self, event): logging.info("event: %s", event) if event.HasField("link_event"): - self.app.canvas.wireless_draw.hangle_link_event(event.link_event) + self.app.canvas.wireless_draw.handle_link_event(event.link_event) elif event.HasField("session_event"): if event.session_event.event <= core_pb2.SessionState.SHUTDOWN: self.state = event.session_event.event @@ -153,7 +151,6 @@ class CoreClient: # get hooks response = self.client.get_hooks(self.session_id) - logging.info("joined session hooks: %s", response) for hook in response.hooks: self.hooks[hook.file] = hook @@ -161,19 +158,16 @@ class CoreClient: for node in session.nodes: if node.type == core_pb2.NodeType.WIRELESS_LAN: response = self.client.get_wlan_config(self.session_id, node.id) - logging.debug("wlan config(%s): %s", node.id, response) self.wlan_configs[node.id] = response.config # get mobility configs response = self.client.get_mobility_configs(self.session_id) - logging.debug("mobility configs: %s", response) for node_id in response.configs: node_config = response.configs[node_id].config self.mobility_configs[node_id] = node_config # get emane config response = self.client.get_emane_config(self.session_id) - logging.debug("emane config: %s", response) self.emane_config = response.config # get emane model config @@ -462,10 +456,6 @@ class CoreClient: emane=emane, ) - # set default emane configuration for emane node - if node_type == core_pb2.NodeType.EMANE: - self.emaneconfig_management.set_default_config(node_id) - # set default service configurations # TODO: need to deal with this and custom node cases if node_type == core_pb2.NodeType.DEFAULT: @@ -492,50 +482,28 @@ class CoreClient: :return: nothing """ # delete the nodes - for node_id in node_ids: + for i in node_ids: try: - del self.canvas_nodes[node_id] - self.reusable.append(node_id) + del self.canvas_nodes[i] + self.reusable.append(i) + if i in self.mobility_configs: + del self.mobility_configs[i] + if i in self.wlan_configs: + del self.wlan_configs[i] + for key in list(self.emane_model_configs): + node_id, _, _ = key + if node_id == i: + del self.emane_model_configs[key] except KeyError: - logging.error("invalid canvas id: %s", node_id) + logging.error("invalid canvas id: %s", i) self.reusable.sort() # delete the edges and interfaces - node_interface_pairs = [] for i in edge_tokens: try: - link = self.links.pop(i) - if link.interface_one is not None: - node_interface_pairs.append( - (link.node_one_id, link.interface_one.id) - ) - if link.interface_two is not None: - node_interface_pairs.append( - (link.node_two_id, link.interface_two.id) - ) + self.links.pop(i) except KeyError: - logging.error("coreclient.py invalid edge token ") - - # delete global emane config if there no longer exist any emane cloud - # TODO: should not need to worry about this - node_types = [x.core_node.type for x in self.canvas_nodes.values()] - if core_pb2.NodeType.EMANE not in node_types: - self.emane_config = None - - # delete any mobility configuration, wlan configuration - for i in node_ids: - if i in self.mobility_configs: - del self.mobility_configs[i] - if i in self.wlan_configs: - del self.wlan_configs[i] - - # delete emane configurations - for i in node_interface_pairs: - if i in self.emaneconfig_management.configurations: - self.emaneconfig_management.configurations.pop(i) - for i in node_ids: - if tuple([i, None]) in self.emaneconfig_management.configurations: - self.emaneconfig_management.configurations.pop(tuple([i, None])) + logging.error("invalid edge token: %s", i) def create_interface(self, canvas_node): interface = None @@ -609,13 +577,11 @@ class CoreClient: def get_emane_model_configs_proto(self): configs = [] - emane_configs = self.emaneconfig_management.configurations - for key, value in emane_configs.items(): - node_id, interface_id = key - model, options = value - config = {x: options[x].value for x in options} + for key, config in self.emane_model_configs.items(): + node_id, model, interface = key + config = {x: config[x].value for x in config} config_proto = core_pb2.EmaneModelConfig( - node_id=node_id, interface_id=interface_id, model=model, config=config + node_id=node_id, interface_id=interface, model=model, config=config ) configs.append(config_proto) return configs @@ -669,3 +635,17 @@ class CoreClient: response = self.client.get_mobility_config(self.session_id, node_id) config = response.config return config + + def get_emane_model_config(self, node_id, model, interface=None): + config = self.emane_model_configs.get((node_id, model, interface)) + if not config: + if interface is None: + interface = -1 + response = self.client.get_emane_model_config( + self.session_id, node_id, model, interface + ) + config = response.config + return config + + def set_emane_model_config(self, node_id, model, config, interface=None): + self.emane_model_configs[(node_id, model, interface)] = config diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index 046b0d62..bb6d8f1b 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -46,16 +46,15 @@ class GlobalEmaneDialog(Dialog): class EmaneModelDialog(Dialog): - def __init__(self, master, app, node, model): + def __init__(self, master, app, node, model, interface=None): super().__init__(master, app, f"{node.name} {model} Configuration", modal=True) self.node = node self.model = f"emane_{model}" + self.interface = interface self.config_frame = None - session_id = self.app.core.session_id - response = self.app.core.client.get_emane_model_config( - session_id, self.node.id, self.model + self.config = self.app.core.get_emane_model_config( + self.node.id, self.model, self.interface ) - self.config = response.config self.draw() def draw(self): @@ -79,8 +78,8 @@ class EmaneModelDialog(Dialog): def click_apply(self): self.config_frame.parse_config() - self.app.core.emaneconfig_management.set_custom_emane_cloud_config( - self.node.id, self.model + self.app.core.set_emane_model_config( + self.node.id, self.model, self.config, self.interface ) self.destroy() diff --git a/coretk/coretk/emaneodelnodeconfig.py b/coretk/coretk/emaneodelnodeconfig.py deleted file mode 100644 index e1aaadbb..00000000 --- a/coretk/coretk/emaneodelnodeconfig.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -emane model configurations -""" -import logging - - -class EmaneModelNodeConfig: - def __init__(self, app): - """ - create an instance for EmaneModelNodeConfig - - :param app: application - """ - # dict(tuple(node_id, interface_id, model) : config) - self.configurations = {} - - # dict(int, list(int)) stores emane node maps to mdr nodes that are linked to that emane node - self.links = {} - - self.app = app - - def set_default_config(self, node_id): - """ - set a default emane configuration for a newly created emane - - :param int node_id: node id - :return: nothing - """ - session_id = self.app.core.session_id - client = self.app.core.client - default_emane_model = self.app.core.emane_models[0] - response = client.get_emane_model_config( - session_id, node_id, default_emane_model - ) - logging.info( - "emanemodelnodeconfig.py get emane model config (%s), result: %s", - node_id, - response, - ) - self.configurations[tuple([node_id, None])] = tuple( - [default_emane_model, response.config] - ) - self.links[node_id] = [] - - def set_default_for_mdr(self, emane_node_id, mdr_node_id, interface_id): - """ - set emane configuration of an mdr node on the correct interface - - :param int emane_node_id: emane node id - :param int mdr_node_id: mdr node id - :param int interface_id: interface id - :return: nothing - """ - self.configurations[tuple([mdr_node_id, interface_id])] = self.configurations[ - tuple([emane_node_id, None]) - ] - self.links[emane_node_id].append(tuple([mdr_node_id, interface_id])) - - def set_custom_emane_cloud_config(self, emane_node_id, model_name): - """ - set custom configuration for an emane node, if model is changed, update the nodes connected to that emane node - - :param int emane_node_id: emane node id - :param str model_name: model name - :return: nothing - """ - prev_model_name = self.configurations[tuple([emane_node_id, None])][0] - session_id = self.app.core.session_id - response = self.app.core.client.get_emane_model_config( - session_id, emane_node_id, model_name - ) - self.configurations[tuple([emane_node_id, None])] = tuple( - [model_name, response.config] - ) - - if prev_model_name != model_name: - for k in self.links[emane_node_id]: - self.configurations[k] = tuple([model_name, response.config]) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index e2722c77..43b8a830 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -176,48 +176,53 @@ class CanvasGraph(tk.Canvas): node_one = canvas_node_one.core_node canvas_node_two = self.core.canvas_nodes[link.node_two_id] node_two = canvas_node_two.core_node - is_wired = link.type == core_pb2.LinkType.WIRED - edge = CanvasEdge( - node_one.position.x, - node_one.position.y, - node_two.position.x, - node_two.position.y, - canvas_node_one.id, - self, - is_wired=is_wired, - ) - edge.token = tuple(sorted((canvas_node_one.id, canvas_node_two.id))) - edge.dst = canvas_node_two.id - canvas_node_one.edges.add(edge) - canvas_node_two.edges.add(edge) - self.edges[edge.token] = edge - self.core.links[edge.token] = link - self.helper.redraw_antenna(link, canvas_node_one, canvas_node_two) + if link.type == core_pb2.LinkType.WIRELESS: + self.wireless_draw.add_connection(link.node_one_id, link.node_two_id) + else: + is_node_one_wireless = NodeUtils.is_wireless_node(node_one.type) + is_node_two_wireless = NodeUtils.is_wireless_node(node_two.type) + has_no_wireless = not (is_node_one_wireless or is_node_two_wireless) + edge = CanvasEdge( + node_one.position.x, + node_one.position.y, + node_two.position.x, + node_two.position.y, + canvas_node_one.id, + self, + is_wired=has_no_wireless, + ) + edge.token = tuple(sorted((canvas_node_one.id, canvas_node_two.id))) + edge.dst = canvas_node_two.id + canvas_node_one.edges.add(edge) + canvas_node_two.edges.add(edge) + self.edges[edge.token] = edge + self.core.links[edge.token] = link + self.helper.redraw_antenna(canvas_node_one, canvas_node_two) - # TODO add back the link info to grpc manager also redraw - # TODO will include throughput and ipv6 in the future - interface_one = link.interface_one - interface_two = link.interface_two - ip4_src = None - ip4_dst = None - ip6_src = None - ip6_dst = None - if interface_one is not None: - ip4_src = interface_one.ip4 - ip6_src = interface_one.ip6 - if interface_two is not None: - ip4_dst = interface_two.ip4 - ip6_dst = interface_two.ip6 - edge.link_info = LinkInfo( - canvas=self, - edge=edge, - ip4_src=ip4_src, - ip6_src=ip6_src, - ip4_dst=ip4_dst, - ip6_dst=ip6_dst, - ) - canvas_node_one.interfaces.append(interface_one) - canvas_node_two.interfaces.append(interface_two) + # TODO add back the link info to grpc manager also redraw + # TODO will include throughput and ipv6 in the future + interface_one = link.interface_one + interface_two = link.interface_two + ip4_src = None + ip4_dst = None + ip6_src = None + ip6_dst = None + if interface_one is not None: + ip4_src = interface_one.ip4 + ip6_src = interface_one.ip6 + if interface_two is not None: + ip4_dst = interface_two.ip4 + ip6_dst = interface_two.ip6 + edge.link_info = LinkInfo( + canvas=self, + edge=edge, + ip4_src=ip4_src, + ip6_src=ip6_src, + ip4_dst=ip4_dst, + ip6_dst=ip6_dst, + ) + canvas_node_one.interfaces.append(interface_one) + canvas_node_two.interfaces.append(interface_two) # raise the nodes so they on top of the links self.tag_raise("node") diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py index 476c0182..e691667b 100644 --- a/coretk/coretk/graph_helper.py +++ b/coretk/coretk/graph_helper.py @@ -4,8 +4,8 @@ Some graph helper functions import logging import tkinter as tk -from core.api.grpc import core_pb2 from coretk.images import ImageEnum, Images +from coretk.nodeutils import NodeUtils CANVAS_COMPONENT_TAGS = ["edge", "node", "nodename", "wallpaper", "linkinfo"] @@ -31,34 +31,30 @@ class GraphHelper: def draw_wireless_case(self, src_id, dst_id, edge): src_node_type = self.canvas.nodes[src_id].core_node.type dst_node_type = self.canvas.nodes[dst_id].core_node.type - is_src_wlan = src_node_type == core_pb2.NodeType.WIRELESS_LAN - is_dst_wlan = dst_node_type == core_pb2.NodeType.WIRELESS_LAN - if is_src_wlan or is_dst_wlan: + is_src_wireless = NodeUtils.is_wireless_node(src_node_type) + is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type) + if is_src_wireless or is_dst_wireless: self.canvas.itemconfig(edge.id, state=tk.HIDDEN) edge.wired = False if edge.token not in self.canvas.edges: - if is_src_wlan and is_dst_wlan: + if is_src_wireless and is_dst_wireless: self.canvas.nodes[src_id].antenna_draw.add_antenna() - elif is_src_wlan: + elif is_src_wireless: self.canvas.nodes[dst_id].antenna_draw.add_antenna() else: self.canvas.nodes[src_id].antenna_draw.add_antenna() edge.wired = True - def redraw_antenna(self, link, node_one, node_two): - is_node_one_wlan = node_one.core_node.type == core_pb2.NodeType.WIRELESS_LAN - is_node_two_wlan = node_two.core_node.type == core_pb2.NodeType.WIRELESS_LAN - if link.type == core_pb2.LinkType.WIRELESS: - if is_node_one_wlan and is_node_two_wlan: - node_one.antenna_draw.add_antenna() - elif is_node_one_wlan and not is_node_two_wlan: + def redraw_antenna(self, node_one, node_two): + is_node_one_wireless = NodeUtils.is_wireless_node(node_one.core_node.type) + is_node_two_wireless = NodeUtils.is_wireless_node(node_two.core_node.type) + if is_node_one_wireless or is_node_two_wireless: + if is_node_one_wireless and not is_node_two_wireless: node_two.antenna_draw.add_antenna() - elif not is_node_one_wlan and is_node_two_wlan: + elif not is_node_one_wireless and is_node_two_wireless: node_one.antenna_draw.add_antenna() else: - logging.error( - "graph_helper.py WIRELESS link but both nodes are non-wireless node" - ) + logging.error("bad link between two wireless nodes") def update_wlan_connection(self, old_x, old_y, new_x, new_y, edge_ids): for eid in edge_ids: diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index 1e0363cf..00df0913 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -18,7 +18,6 @@ class LinkInfo: """ self.canvas = canvas self.edge = edge - # self.edge_id = edge.id self.radius = 37 self.core = self.canvas.core diff --git a/coretk/coretk/nodeutils.py b/coretk/coretk/nodeutils.py index 380c9a0b..fcc29953 100644 --- a/coretk/coretk/nodeutils.py +++ b/coretk/coretk/nodeutils.py @@ -46,6 +46,7 @@ class NodeUtils: NODE_ICONS = {} CONTAINER_NODES = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} IMAGE_NODES = {NodeType.DOCKER, NodeType.LXC} + WIRELESS_NODES = {NodeType.WIRELESS_LAN, NodeType.EMANE} NODE_MODELS = {"router", "host", "PC", "mdr", "prouter"} @classmethod @@ -60,8 +61,14 @@ class NodeUtils: def is_image_node(cls, node_type): return node_type in cls.IMAGE_NODES + @classmethod + def is_wireless_node(cls, node_type): + return node_type in cls.WIRELESS_NODES + @classmethod def node_icon(cls, node_type, model): + if model == "": + model = None return cls.NODE_ICONS[(node_type, model)] @classmethod diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py index e8d03126..2e0e0af9 100644 --- a/coretk/coretk/wirelessconnection.py +++ b/coretk/coretk/wirelessconnection.py @@ -11,7 +11,7 @@ class WirelessConnection: # map a (node_one_id, node_two_id) to a wlan canvas id self.map = {} - def add_wlan_connection(self, node_one_id, node_two_id): + def add_connection(self, node_one_id, node_two_id): canvas_node_one = self.core.canvas_nodes[node_one_id] canvas_node_two = self.core.canvas_nodes[node_two_id] key = tuple(sorted((node_one_id, node_two_id))) @@ -25,7 +25,7 @@ class WirelessConnection: canvas_node_one.wlans.append(wlan_canvas_id) canvas_node_two.wlans.append(wlan_canvas_id) - def delete_wlan_connection(self, node_one_id, node_two_id): + def delete_connection(self, node_one_id, node_two_id): canvas_node_one = self.core.canvas_nodes[node_one_id] canvas_node_two = self.core.canvas_nodes[node_two_id] key = tuple(sorted((node_one_id, node_two_id))) @@ -35,13 +35,13 @@ class WirelessConnection: self.canvas.delete(wlan_canvas_id) self.map.pop(key, None) - def hangle_link_event(self, link_event): + def handle_link_event(self, link_event): if link_event.message_type == core_pb2.MessageType.ADD: - self.add_wlan_connection( + self.add_connection( link_event.link.node_one_id, link_event.link.node_two_id ) if link_event.message_type == core_pb2.MessageType.DELETE: - self.delete_wlan_connection( + self.delete_connection( link_event.link.node_one_id, link_event.link.node_two_id ) diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 80a730e2..6fe291dd 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -1017,7 +1017,7 @@ class WlanNode(CoreNetwork): """ apitype = NodeTypes.WIRELESS_LAN.value - linktype = LinkTypes.WIRELESS.value + linktype = LinkTypes.WIRED.value policy = "DROP" type = "wlan" From c21f06079736f53bce0139cd1ccb77af50deae46 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 21 Nov 2019 16:59:55 -0800 Subject: [PATCH 253/462] service file config --- coretk/coretk/coreclient.py | 44 ++++++++++++------- coretk/coretk/dialogs/nodeservice.py | 8 ++++ coretk/coretk/dialogs/serviceconfiguration.py | 26 ++++++++--- 3 files changed, 55 insertions(+), 23 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 62c33ee7..6b527c8c 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -312,7 +312,6 @@ class CoreClient: emane_model_configs = self.get_emane_model_configs_proto() hooks = list(self.hooks.values()) service_configs = self.get_service_config_proto() - print(service_configs) # service_file_configs = self.get_service_file_config_proto() self.created_links.clear() self.created_nodes.clear() @@ -397,9 +396,9 @@ class CoreClient: :return: nothing """ - node_protos = [x.core_node for x in self.canvas_nodes.values()] link_protos = list(self.links.values()) + print(node_protos) if self.get_session_state() != core_pb2.SessionState.DEFINITION: self.client.set_session_state( self.session_id, core_pb2.SessionState.DEFINITION @@ -426,6 +425,7 @@ class CoreClient: self.created_links.add( tuple([link_proto.node_one_id, link_proto.node_two_id]) ) + print(self.app.core.client.get_session(self.app.core.session_id)) def close(self): """ @@ -638,20 +638,32 @@ class CoreClient: def get_service_config_proto(self): configs = [] - for ( - node_id, - service_configs, - ) in self.serviceconfig_manager.configurations.items(): - for service, config in service_configs.items(): - if service in self.serviceconfig_manager.current_services[node_id]: - config = core_pb2.ServiceConfig( - node_id=node_id, - service=service, - startup=config.startup, - validate=config.validate, - shutdown=config.shutdown, - ) - configs.append(config) + for node_id, services in self.service_configs.items(): + for name, config in services.items(): + config_proto = core_pb2.ServiceConfig( + node_id=node_id, + service=name, + startup=config.startup, + validate=config.validate, + shutdown=config.shutdown, + ) + configs.append(config_proto) + + # configs = [] + # for ( + # node_id, + # service_configs, + # ) in self.serviceconfig_manager.configurations.items(): + # for service, config in service_configs.items(): + # if service in self.serviceconfig_manager.current_services[node_id]: + # config = core_pb2.ServiceConfig( + # node_id=node_id, + # service=service, + # startup=config.startup, + # validate=config.validate, + # shutdown=config.shutdown, + # ) + # configs.append(config) return configs def get_service_file_config_proto(self): diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 6293f9d9..9b3d8978 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -22,6 +22,14 @@ class NodeService(Dialog): services = set( app.core.serviceconfig_manager.current_services[self.node_id] ) + if services is None: + services = canvas_node.core_node.services + model = canvas_node.core_node.model + if len(services) == 0: + services = set(self.app.core.default_services[model]) + else: + services = set(services) + self.current_services = services self.service_manager = self.app.core.serviceconfig_manager self.service_file_manager = self.app.core.servicefileconfig_manager diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 20684311..7505bb40 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -93,9 +93,13 @@ class ServiceConfiguration(Dialog): self.temp_service_files = { x: self.original_service_files[x] for x in self.original_service_files } - configs = self.app.core.servicefileconfig_manager.configurations - if self.node_id in configs and self.service_name in configs[self.node_id]: - for file, data in configs[self.node_id][self.service_name].items(): + # configs = self.app.core.servicefileconfig_manager.configurations + file_configs = self.app.core.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 def draw(self): @@ -389,7 +393,6 @@ class ServiceConfiguration(Dialog): service_configs[self.node_id] = {} if self.service_name not in service_configs[self.node_id]: self.app.core.service_configs[self.node_id][self.service_name] = config - print(self.app.core.client.get_session(self.app.core.session_id)) # self.service_manager.node_service_custom_configuration( # self.node_id, @@ -399,9 +402,18 @@ class ServiceConfiguration(Dialog): # shutdown_commands, # ) for file in self.modified_files: - self.app.core.servicefileconfig_manager.set_custom_service_file_config( - self.node_id, self.service_name, file, self.temp_service_files[file] - ) + # self.app.core.servicefileconfig_manager.set_custom_service_file_config( + # self.node_id, self.service_name, file, self.temp_service_files[file] + # ) + file_configs = self.app.core.file_configs + if self.node_id not in file_configs: + file_configs[self.node_id] = {} + if self.service_name not in file_configs[self.node_id]: + file_configs[self.node_id][self.service_name] = {} + 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] ) From df9c7308dbb6457bd434cb21da86048026cfac71 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 17:03:18 -0800 Subject: [PATCH 254/462] update to avoid issue when old gui creates emane nodes without emane models --- daemon/core/emulator/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index d2d841e4..fee2bde4 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -704,7 +704,7 @@ class Session: self.services.add_services(node, node.type, options.services) # ensure default emane configuration - if isinstance(node, EmaneNet): + if isinstance(node, EmaneNet) and options.emane: self.emane.set_model_config(_id, options.emane) # set default wlan config if needed if isinstance(node, WlanNode): From bb7bad89d32086dae383626cda4891cae2e932b6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 17:14:23 -0800 Subject: [PATCH 255/462] added a default location for now to get emane working, until session location is supported --- coretk/coretk/coreclient.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 229f59a5..49489798 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -60,6 +60,9 @@ class CoreClient: # data for managing the current session self.canvas_nodes = {} + self.location = core_pb2.SessionLocation( + x=0, y=0, z=0, lat=47.5791667, lon=-122.132322, alt=2.0, scale=150.0 + ) self.interface_to_edge = {} self.state = None self.links = {} @@ -308,12 +311,13 @@ class CoreClient: self.session_id, nodes, links, - hooks=hooks, - wlan_configs=wlan_configs, - emane_config=emane_config, - emane_model_configs=emane_model_configs, - mobility_configs=mobility_configs, - service_configs=service_configs, + self.location, + hooks, + emane_config, + emane_model_configs, + wlan_configs, + mobility_configs, + service_configs, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) From fbbf31f4fa09fbee28895919171ffc34554f87d6 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 21:56:28 -0800 Subject: [PATCH 256/462] added saving session location to config, and query location when joining a session --- coretk/coretk/appconfig.py | 9 ++ coretk/coretk/coreclient.py | 23 ++++-- coretk/coretk/dialogs/canvassizeandscale.py | 92 ++++++++++++--------- coretk/coretk/graph.py | 1 - 4 files changed, 79 insertions(+), 46 deletions(-) diff --git a/coretk/coretk/appconfig.py b/coretk/coretk/appconfig.py index e8f5db6e..2af1047c 100644 --- a/coretk/coretk/appconfig.py +++ b/coretk/coretk/appconfig.py @@ -75,6 +75,15 @@ def check_directory(): "terminal": terminal, "gui3d": "/usr/local/bin/std3d.sh", }, + "location": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "lat": 47.5791667, + "lon": -122.132322, + "alt": 2.0, + "scale": 150.0, + }, "servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}], "nodes": [], "observers": [{"name": "hello", "cmd": "echo hello"}], diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 49489798..d801875e 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -60,9 +60,7 @@ class CoreClient: # data for managing the current session self.canvas_nodes = {} - self.location = core_pb2.SessionLocation( - x=0, y=0, z=0, lat=47.5791667, lon=-122.132322, alt=2.0, scale=150.0 - ) + self.location = None self.interface_to_edge = {} self.state = None self.links = {} @@ -123,7 +121,7 @@ class CoreClient: throughputs_belong_to_session ) - def join_session(self, session_id): + def join_session(self, session_id, query_location=True): self.master.config(cursor="watch") self.master.update() @@ -148,6 +146,11 @@ class CoreClient: self.state = session.state self.client.events(self.session_id, self.handle_events) + # get location + if query_location: + response = self.client.get_session_location(self.session_id) + self.location = response.location + # get emane models response = self.client.get_emane_models(self.session_id) self.emane_models = response.models @@ -207,7 +210,17 @@ class CoreClient: """ response = self.client.create_session() logging.info("created session: %s", response) - self.join_session(response.session_id) + location_config = self.app.config["location"] + self.location = core_pb2.SessionLocation( + x=location_config["x"], + y=location_config["y"], + z=location_config["z"], + lat=location_config["lat"], + lon=location_config["lon"], + alt=location_config["alt"], + scale=location_config["scale"], + ) + self.join_session(response.session_id, query_location=False) def delete_session(self, custom_sid=None): if custom_sid is None: diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 3a72389d..0d88b11f 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -6,8 +6,8 @@ from tkinter import font, ttk from coretk.dialogs.dialog import Dialog -FRAME_PAD = 5 -PADX = 5 +PAD = 5 +PIXEL_SCALE = 100 class SizeAndScaleDialog(Dialog): @@ -19,9 +19,7 @@ class SizeAndScaleDialog(Dialog): """ super().__init__(master, app, "Canvas Size and Scale", modal=True) self.canvas = self.app.canvas - self.meter_per_pixel = self.canvas.meters_per_pixel self.section_font = font.Font(weight="bold") - # get current canvas dimensions plot = self.canvas.find_withtag("rectangle") x0, y0, x1, y1 = self.canvas.bbox(plot[0]) @@ -29,14 +27,15 @@ class SizeAndScaleDialog(Dialog): height = abs(y0 - y1) - 2 self.pixel_width = tk.IntVar(value=width) self.pixel_height = tk.IntVar(value=height) - self.meters_width = tk.IntVar(value=width * self.meter_per_pixel) - self.meters_height = tk.IntVar(value=height * self.meter_per_pixel) - self.scale = tk.IntVar(value=self.meter_per_pixel * 100) - self.x = tk.IntVar(value=0) - self.y = tk.IntVar(value=0) - self.lat = tk.DoubleVar(value=47.5791667) - self.lon = tk.DoubleVar(value=-122.132322) - self.alt = tk.DoubleVar(value=2.0) + location = self.app.core.location + self.x = tk.DoubleVar(value=location.x) + self.y = tk.DoubleVar(value=location.y) + self.lat = tk.DoubleVar(value=location.lat) + self.lon = tk.DoubleVar(value=location.lon) + self.alt = tk.DoubleVar(value=location.alt) + self.scale = tk.DoubleVar(value=location.scale) + self.meters_width = tk.IntVar(value=width / PIXEL_SCALE * location.scale) + self.meters_height = tk.IntVar(value=height / PIXEL_SCALE * location.scale) self.save_default = tk.BooleanVar(value=False) self.draw() @@ -49,7 +48,7 @@ class SizeAndScaleDialog(Dialog): self.draw_buttons() def draw_size(self): - label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD) + label_frame = ttk.Labelframe(self.top, text="Size", padding=PAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -59,13 +58,13 @@ class SizeAndScaleDialog(Dialog): frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.pixel_width) - entry.grid(row=0, column=1, sticky="ew", padx=PADX) + entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="x Height") - label.grid(row=0, column=2, sticky="w", padx=PADX) + label.grid(row=0, column=2, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.pixel_height) - entry.grid(row=0, column=3, sticky="ew", padx=PADX) + entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Pixels") label.grid(row=0, column=4, sticky="w") @@ -75,35 +74,33 @@ class SizeAndScaleDialog(Dialog): frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.meters_width) - entry.grid(row=0, column=1, sticky="ew", padx=PADX) + entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="x Height") - label.grid(row=0, column=2, sticky="w", padx=PADX) + label.grid(row=0, column=2, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.meters_height) - entry.grid(row=0, column=3, sticky="ew", padx=PADX) + entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=4, sticky="w") def draw_scale(self): - label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD) + label_frame = ttk.Labelframe(self.top, text="Scale", padding=PAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) frame = ttk.Frame(label_frame) frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) - label = ttk.Label(frame, text="100 Pixels =") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label = ttk.Label(frame, text=f"{PIXEL_SCALE} Pixels =") + label.grid(row=0, column=0, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.scale) - entry.grid(row=0, column=1, sticky="ew", padx=PADX) + entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=2, sticky="w") def draw_reference_point(self): - label_frame = ttk.Labelframe( - self.top, text="Reference Point", padding=FRAME_PAD - ) + label_frame = ttk.Labelframe(self.top, text="Reference Point", padding=PAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -118,14 +115,14 @@ class SizeAndScaleDialog(Dialog): frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="X") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.x) - entry.grid(row=0, column=1, sticky="ew", padx=PADX) + entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Y") - label.grid(row=0, column=2, sticky="w", padx=PADX) + label.grid(row=0, column=2, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.y) - entry.grid(row=0, column=3, sticky="ew", padx=PADX) + entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(label_frame, text="Translates To") label.grid() @@ -137,17 +134,17 @@ class SizeAndScaleDialog(Dialog): frame.columnconfigure(5, weight=1) label = ttk.Label(frame, text="Lat") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.lat) - entry.grid(row=0, column=1, sticky="ew", padx=PADX) + entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Lon") - label.grid(row=0, column=2, sticky="w", padx=PADX) + label.grid(row=0, column=2, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.lon) - entry.grid(row=0, column=3, sticky="ew", padx=PADX) + entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Alt") - label.grid(row=0, column=4, sticky="w", padx=PADX) + label.grid(row=0, column=4, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.alt) entry.grid(row=0, column=5, sticky="ew") @@ -164,16 +161,31 @@ class SizeAndScaleDialog(Dialog): frame.grid(sticky="ew") button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") def click_apply(self): - meter_per_pixel = float(self.scale.get()) / 100 width, height = self.pixel_width.get(), self.pixel_height.get() - self.canvas.meters_per_pixel = meter_per_pixel self.canvas.redraw_grid(width, height) if self.canvas.wallpaper: self.canvas.redraw() + location = self.app.core.location + location.x = self.x.get() + location.y = self.y.get() + location.lat = self.lat.get() + location.lon = self.lon.get() + location.alt = self.alt.get() + location.scale = self.scale.get() + if self.save_default.get(): + location_config = self.app.config["location"] + location_config["x"] = location.x + location_config["y"] = location.y + location_config["z"] = location.z + location_config["lat"] = location.lat + location_config["lon"] = location.lon + location_config["alt"] = location.alt + location_config["scale"] = location.scale + self.app.save_config() self.destroy() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 43b8a830..2c874f34 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -49,7 +49,6 @@ class CanvasGraph(tk.Canvas): self.edges = {} self.drawing_edge = None self.grid = None - self.meters_per_pixel = 1.5 self.canvas_management = CanvasComponentManagement(self, core) self.setup_bindings() self.draw_grid() From a6cdd63570ec65d3d9e3c618c12c7c00c68484f2 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 22:03:07 -0800 Subject: [PATCH 257/462] fixed name issue with app config, renamed to guiconfig --- coretk/coretk/app.py | 6 +++--- coretk/coretk/coreclient.py | 8 ++++---- coretk/coretk/dialogs/canvassizeandscale.py | 2 +- coretk/coretk/dialogs/customnodes.py | 6 +++--- coretk/coretk/dialogs/observers.py | 2 +- coretk/coretk/dialogs/preferences.py | 4 ++-- coretk/coretk/dialogs/servers.py | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index fbce02e1..68060927 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -26,7 +26,7 @@ class Application(tk.Frame): self.statusbar = None # setup - self.config = appconfig.read() + self.guiconfig = appconfig.read() self.style = ttk.Style() self.setup_theme() self.core = CoreClient(self) @@ -36,7 +36,7 @@ class Application(tk.Frame): def setup_theme(self): themes.load(self.style) - self.style.theme_use(self.config["preferences"]["theme"]) + self.style.theme_use(self.guiconfig["preferences"]["theme"]) func = partial(themes.update_menu, self.style) self.master.bind_class("Menu", "<>", func) @@ -88,7 +88,7 @@ class Application(tk.Frame): menu_action.on_quit() def save_config(self): - appconfig.save(self.config) + appconfig.save(self.guiconfig) if __name__ == "__main__": diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index d801875e..b64aab98 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -83,12 +83,12 @@ class CoreClient: def read_config(self): # read distributed server - for config in self.app.config.get("servers", []): + for config in self.app.guiconfig.get("servers", []): server = CoreServer(config["name"], config["address"], config["port"]) self.servers[server.name] = server # read custom nodes - for config in self.app.config.get("nodes", []): + for config in self.app.guiconfig.get("nodes", []): name = config["name"] image_file = config["image"] services = set(config["services"]) @@ -96,7 +96,7 @@ class CoreClient: self.custom_nodes[name] = node_draw # read observers - for config in self.app.config.get("observers", []): + for config in self.app.guiconfig.get("observers", []): observer = Observer(config["name"], config["cmd"]) self.custom_observers[observer.name] = observer @@ -210,7 +210,7 @@ class CoreClient: """ response = self.client.create_session() logging.info("created session: %s", response) - location_config = self.app.config["location"] + location_config = self.app.guiconfig["location"] self.location = core_pb2.SessionLocation( x=location_config["x"], y=location_config["y"], diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 0d88b11f..f25dfdd6 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -179,7 +179,7 @@ class SizeAndScaleDialog(Dialog): location.alt = self.alt.get() location.scale = self.scale.get() if self.save_default.get(): - location_config = self.app.config["location"] + location_config = self.app.guiconfig["location"] location_config["x"] = location.x location_config["y"] = location.y location_config["z"] = location.z diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index da7a1fb0..55199541 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -180,17 +180,17 @@ class CustomNodesDialog(Dialog): self.services.update(dialog.current_services) def click_save(self): - self.app.config["nodes"].clear() + self.app.guiconfig["nodes"].clear() for name in sorted(self.app.core.custom_nodes): node_draw = self.app.core.custom_nodes[name] - self.app.config["nodes"].append( + self.app.guiconfig["nodes"].append( { "name": name, "image": node_draw.image_file, "services": list(node_draw.services), } ) - logging.info("saving custom nodes: %s", self.app.config["nodes"]) + logging.info("saving custom nodes: %s", self.app.guiconfig["nodes"]) self.app.save_config() self.destroy() diff --git a/coretk/coretk/dialogs/observers.py b/coretk/coretk/dialogs/observers.py index 78c5811f..58499bd7 100644 --- a/coretk/coretk/dialogs/observers.py +++ b/coretk/coretk/dialogs/observers.py @@ -95,7 +95,7 @@ class ObserverDialog(Dialog): for name in sorted(self.app.core.custom_observers): observer = self.app.core.custom_observers[name] observers.append({"name": observer.name, "cmd": observer.cmd}) - self.app.config["observers"] = observers + self.app.guiconfig["observers"] = observers self.app.save_config() self.destroy() diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py index 148cb6bb..7298f727 100644 --- a/coretk/coretk/dialogs/preferences.py +++ b/coretk/coretk/dialogs/preferences.py @@ -9,7 +9,7 @@ from coretk.dialogs.dialog import Dialog class PreferencesDialog(Dialog): def __init__(self, master, app): super().__init__(master, app, "Preferences", modal=True) - preferences = self.app.config["preferences"] + preferences = self.app.guiconfig["preferences"] self.editor = tk.StringVar(value=preferences["editor"]) self.theme = tk.StringVar(value=preferences["theme"]) self.terminal = tk.StringVar(value=preferences["terminal"]) @@ -76,7 +76,7 @@ class PreferencesDialog(Dialog): self.app.style.theme_use(theme) def click_save(self): - preferences = self.app.config["preferences"] + preferences = self.app.guiconfig["preferences"] preferences["terminal"] = self.terminal.get() preferences["editor"] = self.editor.get() preferences["gui3d"] = self.gui3d.get() diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index d3db22e7..10a6e79e 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -115,7 +115,7 @@ class ServersDialog(Dialog): servers.append( {"name": server.name, "address": server.address, "port": server.port} ) - self.app.config["servers"] = servers + self.app.guiconfig["servers"] = servers self.app.save_config() self.destroy() From 72e9ae75eb1f0a4579f6324bf72da5347faa2fb3 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 22:39:39 -0800 Subject: [PATCH 258/462] fixed issue with changing themes and abg colors not being present --- coretk/coretk/menubar.py | 6 ++---- coretk/coretk/themes.py | 2 ++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index 621510e2..d9508681 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -444,9 +444,7 @@ class Menubar(tk.Menu): menu.add_command(label="Comments...", state=tk.DISABLED) menu.add_command(label="Hooks...", command=self.menuaction.session_hooks) menu.add_command(label="Reset node positions", state=tk.DISABLED) - menu.add_command( - label="Emulation servers...", command=self.menuaction.session_servers - ) + menu.add_command(label="Servers...", command=self.menuaction.session_servers) menu.add_command(label="Options...", command=self.menuaction.session_options) self.add_cascade(label="Session", menu=menu) @@ -458,7 +456,7 @@ class Menubar(tk.Menu): """ menu = tk.Menu(self) menu.add_command( - label="Core Github (www)", command=self.menuaction.help_core_github + label="Core GitHub (www)", command=self.menuaction.help_core_github ) menu.add_command( label="Core Documentation (www)", diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index 267c3a3c..ad91276a 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -137,6 +137,8 @@ def update_menu(style, event): bg = style.lookup(".", "background") fg = style.lookup(".", "foreground") abg = style.lookup(".", "lightcolor") + if not abg: + abg = bg event.widget.config( background=bg, foreground=fg, activebackground=abg, activeforeground=fg ) From 8ff63219a3f5f13b93865b2180be93a7f7e074bc Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 22:55:37 -0800 Subject: [PATCH 259/462] increased node icon size, added improve way to offset text regardless of icon size --- coretk/coretk/graph.py | 6 +++++- coretk/coretk/images.py | 2 -- coretk/coretk/nodeutils.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 2c874f34..545cbb41 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -18,6 +18,8 @@ from coretk.nodedelete import CanvasComponentManagement from coretk.nodeutils import NodeUtils from coretk.wirelessconnection import WirelessConnection +NODE_TEXT_OFFSET = 5 + class GraphMode(enum.Enum): SELECT = 0 @@ -615,8 +617,10 @@ class CanvasNode: self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags="node" ) + image_box = self.canvas.bbox(self.id) + y = image_box[3] + NODE_TEXT_OFFSET self.text_id = self.canvas.create_text( - x, y + 20, text=self.core_node.name, tags="nodename" + x, y, text=self.core_node.name, tags="nodename" ) self.antenna_draw = WlanAntennaManager(self.canvas, self.id) self.tooltip = CanvasTooltip(self.canvas) diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 3763f2d2..287a7359 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -4,8 +4,6 @@ from PIL import Image, ImageTk from coretk.appconfig import LOCAL_ICONS_PATH -NODE_WIDTH = 32 - class Images: images = {} diff --git a/coretk/coretk/nodeutils.py b/coretk/coretk/nodeutils.py index fcc29953..18b468b5 100644 --- a/coretk/coretk/nodeutils.py +++ b/coretk/coretk/nodeutils.py @@ -1,7 +1,7 @@ from core.api.grpc.core_pb2 import NodeType from coretk.images import ImageEnum, Images -ICON_SIZE = 32 +ICON_SIZE = 48 class NodeDraw: From 6c0d4d3a9372382f87fd86788607b0c08dcbc19d Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 22 Nov 2019 09:09:00 -0800 Subject: [PATCH 260/462] updated node labels --- coretk/coretk/graph.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 545cbb41..b8749e1d 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -1,6 +1,7 @@ import enum import logging import tkinter as tk +from tkinter import font from PIL import ImageTk @@ -619,8 +620,14 @@ class CanvasNode: ) image_box = self.canvas.bbox(self.id) y = image_box[3] + NODE_TEXT_OFFSET + text_font = font.Font(family="TkIconFont", size=12) self.text_id = self.canvas.create_text( - x, y, text=self.core_node.name, tags="nodename" + x, + y, + text=self.core_node.name, + tags="nodename", + font=text_font, + fill="#0000CD", ) self.antenna_draw = WlanAntennaManager(self.canvas, self.id) self.tooltip = CanvasTooltip(self.canvas) @@ -631,7 +638,6 @@ class CanvasNode: self.canvas.tag_bind(self.id, "", self.select_multiple) self.canvas.tag_bind(self.id, "", self.on_enter) self.canvas.tag_bind(self.id, "", self.on_leave) - self.edges = set() self.interfaces = [] self.wlans = [] From 3a73b10902a5c9cf5b49a1980efc2da035712d52 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 22 Nov 2019 10:01:36 -0800 Subject: [PATCH 261/462] service file configs --- coretk/coretk/coreclient.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 6b527c8c..92e2df54 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -312,7 +312,7 @@ class CoreClient: emane_model_configs = self.get_emane_model_configs_proto() hooks = list(self.hooks.values()) service_configs = self.get_service_config_proto() - # service_file_configs = self.get_service_file_config_proto() + file_configs = self.get_service_file_config_proto() self.created_links.clear() self.created_nodes.clear() if self.emane_config: @@ -329,6 +329,7 @@ class CoreClient: emane_model_configs=emane_model_configs, mobility_configs=mobility_configs, service_configs=service_configs, + service_file_configs=file_configs, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) @@ -483,11 +484,10 @@ class CoreClient: self.emaneconfig_management.set_default_config(node_id) # set default service configurations - # TODO: need to deal with this and custom node cases - if node_type == core_pb2.NodeType.DEFAULT: - self.serviceconfig_manager.node_default_services_configuration( - node_id=node_id, node_model=model - ) + # if node_type == core_pb2.NodeType.DEFAULT: + # self.serviceconfig_manager.node_default_services_configuration( + # node_id=node_id, node_model=model + # ) logging.debug( "adding node to core session: %s, coords: (%s, %s), name: %s", @@ -668,16 +668,23 @@ class CoreClient: def get_service_file_config_proto(self): configs = [] - for ( - node_id, - service_file_configs, - ) in self.servicefileconfig_manager.configurations.items(): - for service, file_configs in service_file_configs.items(): - for file, data in file_configs.items(): - config = core_pb2.ServiceFileConfig( + for (node_id, file_configs) in self.file_configs.items(): + for service, file_config in file_configs.items(): + for file, data in file_config.items(): + config_proto = core_pb2.ServiceFileConfig( node_id=node_id, service=service, file=file, data=data ) - configs.append(config) + configs.append(config_proto) + # for ( + # node_id, + # service_file_configs, + # ) in self.servicefileconfig_manager.configurations.items(): + # for service, file_configs in service_file_configs.items(): + # for file, data in file_configs.items(): + # config = core_pb2.ServiceFileConfig( + # node_id=node_id, service=service, file=file, data=data + # ) + # configs.append(config) return configs def run(self, node_id): From e39db4bd639fa563f82282b12e2e598fa8d3053e Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 22 Nov 2019 10:32:25 -0800 Subject: [PATCH 262/462] get rid of 2 unnecessary classes for service config --- coretk/coretk/coreclient.py | 39 ++----------------- coretk/coretk/dialogs/nodeservice.py | 15 +------ coretk/coretk/dialogs/serviceconfiguration.py | 26 ------------- .../{ => todelete}/servicefileconfig.py | 0 .../{ => todelete}/servicenodeconfig.py | 0 5 files changed, 4 insertions(+), 76 deletions(-) rename coretk/coretk/{ => todelete}/servicefileconfig.py (100%) rename coretk/coretk/{ => todelete}/servicenodeconfig.py (100%) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 92e2df54..089a352e 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -9,8 +9,6 @@ from coretk.dialogs.sessions import SessionsDialog from coretk.emaneodelnodeconfig import EmaneModelNodeConfig from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils -from coretk.servicefileconfig import ServiceFileConfig -from coretk.servicenodeconfig import ServiceNodeConfig OBSERVERS = { "processes": "ps", @@ -75,8 +73,6 @@ class CoreClient: self.emane_model_configs = {} self.emaneconfig_management = EmaneModelNodeConfig(app) self.emane_config = None - self.serviceconfig_manager = ServiceNodeConfig(app) - self.servicefileconfig_manager = ServiceFileConfig() self.created_nodes = set() self.created_links = set() @@ -143,6 +139,8 @@ class CoreClient: self.wlan_configs.clear() self.mobility_configs.clear() self.emane_config = None + self.service_configs.clear() + self.file_configs.clear() # get session data response = self.client.get_session(self.session_id) @@ -332,6 +330,7 @@ class CoreClient: service_file_configs=file_configs, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) + print(self.client.get_session(self.session_id)) def stop_session(self): response = self.client.stop_session(session_id=self.session_id) @@ -483,12 +482,6 @@ class CoreClient: if node_type == core_pb2.NodeType.EMANE: self.emaneconfig_management.set_default_config(node_id) - # set default service configurations - # if node_type == core_pb2.NodeType.DEFAULT: - # self.serviceconfig_manager.node_default_services_configuration( - # node_id=node_id, node_model=model - # ) - logging.debug( "adding node to core session: %s, coords: (%s, %s), name: %s", self.session_id, @@ -648,22 +641,6 @@ class CoreClient: shutdown=config.shutdown, ) configs.append(config_proto) - - # configs = [] - # for ( - # node_id, - # service_configs, - # ) in self.serviceconfig_manager.configurations.items(): - # for service, config in service_configs.items(): - # if service in self.serviceconfig_manager.current_services[node_id]: - # config = core_pb2.ServiceConfig( - # node_id=node_id, - # service=service, - # startup=config.startup, - # validate=config.validate, - # shutdown=config.shutdown, - # ) - # configs.append(config) return configs def get_service_file_config_proto(self): @@ -675,16 +652,6 @@ class CoreClient: node_id=node_id, service=service, file=file, data=data ) configs.append(config_proto) - # for ( - # node_id, - # service_file_configs, - # ) in self.servicefileconfig_manager.configurations.items(): - # for service, file_configs in service_file_configs.items(): - # for file, data in file_configs.items(): - # config = core_pb2.ServiceFileConfig( - # node_id=node_id, service=service, file=file, data=data - # ) - # configs.append(config) return configs def run(self, node_id): diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 9b3d8978..35142497 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -18,10 +18,6 @@ class NodeService(Dialog): self.groups = None self.services = None self.current = None - if services is None: - services = set( - app.core.serviceconfig_manager.current_services[self.node_id] - ) if services is None: services = canvas_node.core_node.services model = canvas_node.core_node.model @@ -31,8 +27,6 @@ class NodeService(Dialog): services = set(services) self.current_services = services - self.service_manager = self.app.core.serviceconfig_manager - self.service_file_manager = self.app.core.servicefileconfig_manager self.draw() def draw(self): @@ -87,16 +81,9 @@ class NodeService(Dialog): def service_clicked(self, name, var): if var.get() and name not in self.current_services: - if self.service_manager.node_new_service_configuration(self.node_id, name): - self.current_services.add(name) - else: - for checkbutton in self.services.frame.winfo_children(): - if name == checkbutton.cget("text"): - checkbutton.config(variable=tk.BooleanVar(value=False)) - + self.current_services.add(name) elif not var.get() and name in self.current_services: self.current_services.remove(name) - self.service_manager.current_services[self.node_id].remove(name) self.current.listbox.delete(0, tk.END) for name in sorted(self.current_services): self.current.listbox.insert(tk.END, name) diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 7505bb40..1d8059d8 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -15,7 +15,6 @@ class ServiceConfiguration(Dialog): super().__init__(master, app, f"{service_name} service", modal=True) self.app = app self.core = app.core - self.service_manager = app.core.serviceconfig_manager self.node_id = node_id self.service_name = service_name self.radiovar = tk.IntVar() @@ -64,19 +63,6 @@ class ServiceConfiguration(Dialog): service_config = self.app.core.get_node_service( self.node_id, self.service_name ) - - # # load data from local memory - # if self.service_name in self.service_manager.configurations[self.node_id]: - # service_config = self.service_manager.configurations[self.node_id][ - # self.service_name - # ] - # else: - # self.service_manager.node_custom_service_configuration( - # self.node_id, self.service_name - # ) - # service_config = self.service_manager.configurations[self.node_id][ - # self.service_name - # ] self.dependencies = [x for x in service_config.dependencies] self.executables = [x for x in service_config.executables] self.metadata = service_config.meta @@ -93,7 +79,6 @@ class ServiceConfiguration(Dialog): self.temp_service_files = { x: self.original_service_files[x] for x in self.original_service_files } - # configs = self.app.core.servicefileconfig_manager.configurations file_configs = self.app.core.file_configs if ( self.node_id in file_configs @@ -393,18 +378,7 @@ class ServiceConfiguration(Dialog): service_configs[self.node_id] = {} if self.service_name not in service_configs[self.node_id]: self.app.core.service_configs[self.node_id][self.service_name] = config - - # self.service_manager.node_service_custom_configuration( - # self.node_id, - # self.service_name, - # startup_commands, - # validate_commands, - # shutdown_commands, - # ) for file in self.modified_files: - # self.app.core.servicefileconfig_manager.set_custom_service_file_config( - # self.node_id, self.service_name, file, self.temp_service_files[file] - # ) file_configs = self.app.core.file_configs if self.node_id not in file_configs: file_configs[self.node_id] = {} diff --git a/coretk/coretk/servicefileconfig.py b/coretk/coretk/todelete/servicefileconfig.py similarity index 100% rename from coretk/coretk/servicefileconfig.py rename to coretk/coretk/todelete/servicefileconfig.py diff --git a/coretk/coretk/servicenodeconfig.py b/coretk/coretk/todelete/servicenodeconfig.py similarity index 100% rename from coretk/coretk/servicenodeconfig.py rename to coretk/coretk/todelete/servicenodeconfig.py From f2b37d32c8c011c78f0de85f01a3346530279661 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 22 Nov 2019 12:51:58 -0800 Subject: [PATCH 263/462] added netaddr library, added logic to support dealing with creating new subnet when needed --- coretk/Pipfile | 5 +- coretk/Pipfile.lock | 109 +++++++++++++++++++----------------- coretk/coretk/coreclient.py | 78 +++++++++++++------------- coretk/coretk/graph.py | 12 ++-- coretk/coretk/interface.py | 103 ++++++++++++++++++++++------------ coretk/setup.py | 2 +- 6 files changed, 175 insertions(+), 134 deletions(-) diff --git a/coretk/Pipfile b/coretk/Pipfile index dfeb664c..b30b3d54 100644 --- a/coretk/Pipfile +++ b/coretk/Pipfile @@ -13,6 +13,7 @@ black = "==19.3b0" pre-commit = "*" [packages] -coretk = {editable = true,path = "."} -core = {editable = true,path = "./../daemon"} +coretk = {path = ".",editable = true} +core = {path = "./../daemon",editable = true} pyyaml = "*" +netaddr = "*" diff --git a/coretk/Pipfile.lock b/coretk/Pipfile.lock index 07906a87..7ff33122 100644 --- a/coretk/Pipfile.lock +++ b/coretk/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "eb59f8233d6608de2d67743d8d8afe51c929f837cf6cf4d991ffc79ab5134910" + "sha256": "9024ff4821ee3ccffee21a83f5436953371ad7d64a81a22b6c3723002c92b2cd" }, "pipfile-spec": 6, "requires": {}, @@ -50,6 +50,7 @@ "sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97", "sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43", "sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db", + "sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3", "sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b", "sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579", "sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346", @@ -117,56 +118,56 @@ }, "grpcio": { "hashes": [ - "sha256:01cb705eafba1108e2a947ba0457da4f6a1e8142c729fc61702b5fdd11009eb1", - "sha256:0b5a79e29f167d3cd06faad6b15babbc2661066daaacf79373c3a8e67ca1fca1", - "sha256:1097a61a0e97b3580642e6e1460a3a1f1ba1815e2a70d6057173bcc495417076", - "sha256:13970e665a4ec4cec7d067d7d3504a0398c657d91d26c581144ad9044e429c9a", - "sha256:1557817cea6e0b87fad2a3e20da385170efb03a313db164e8078955add2dfa1b", - "sha256:1b0fb036a2f9dd93d9a35c57c26420eeb4b571fcb14b51cddf5b1e73ea5d882b", - "sha256:24d9e58d08e8cd545d8a3247a18654aff0e5e60414701696a8098fbb0d792b75", - "sha256:2c38b586163d2b91567fe5e6d9e7798f792012365adc838a64b66b22dce3f4d4", - "sha256:2df3ab4348507de60e1cbf75196403df1b9b4c4d4dc5bd11ac4eb63c46f691c7", - "sha256:32f70f7c90454ea568b868af2e96616743718d9233d23f62407e98caed81dfbf", - "sha256:3af2a49d576820045c9c880ff29a5a96d020fe31b35d248519bfc6ccb8be4eac", - "sha256:4ff7d63800a63db031ebac6a6f581ae84877c959401c24c28f2cc51fd36c47ad", - "sha256:502aaa8be56f0ae69cda66bc27e1fb5531ceaa27ca515ec3c34f6178b1297180", - "sha256:55358ce3ec283222e435f7dbc6603521438458f3c65f7c1cb33b8dabf56d70d8", - "sha256:5583b01c67f85fa64a2c3fb085e5517c88b9c1500a2cce12d473cd99d0ed2e49", - "sha256:58d9a5557d3eb7b734a3cea8b16c891099a522b3953a45a30bd4c034f75fc913", - "sha256:5911f042c4ab177757eec5bcb4e2e9a2e823d888835d24577321bf55f02938fa", - "sha256:5e16ea922f4e5017c04fd94e2639b1006e03097e9dd0cbb7a1c852af3ea8bf2e", - "sha256:656e19d3f1b9050ee01b457f92838a9679d7cf84c995f708780f44484048705e", - "sha256:6a1435449a82008c451c7e1a82a834387b9108f9a8d27910f86e7c482f5568e9", - "sha256:6ff02ca6cbed0ddb76e93ba0f8beb6a8c77d83a84eb7cafe2ae3399a8b9d69ea", - "sha256:76de68f60102f333bf4817f38e81ecbee68b850f5a5da9f355235e948ac40981", - "sha256:7c6d7ddd50fc6548ea1dfe09c62509c4f95b8b40082287747be05aa8feb15ee2", - "sha256:836b9d29507de729129e363276fe7c7d6a34c7961e0f155787025552b15d22c0", - "sha256:869242b2baf8a888a4fe0548f86abc47cb4b48bdfd76ae62d6456e939c202e65", - "sha256:8954b24bd08641d906ee50b2d638efc76df893fbd0913149b80484fd0eac40c9", - "sha256:8cdea65d1abb2e698420db8daf20c8d272fbd9d96a51b26a713c1c76f237d181", - "sha256:90161840b4fe9636f91ed0d3ea1e7e615e488cbea4e77594c889e5f3d7a776db", - "sha256:90fb6316b4d7d36700c40db4335902b78dcae13b5466673c21fd3b08a3c1b0c6", - "sha256:91b34f58db2611c9a93ecf751028f97fba1f06e65f49b38f272f6aa5d2977331", - "sha256:9474944a96a33eb8734fa8dc5805403d57973a3526204a5e1c1780d02e0572b6", - "sha256:9a36275db2a4774ac16c6822e7af816ee048071d5030b4c035fd53942b361935", - "sha256:9cbe26e2976b994c5f7c2d35a63354674d6ca0ce62f5b513f078bf63c1745229", - "sha256:9eaeabb3c0eecd6ddd0c16767fd12d130e2cebb8c2618f959a278b1ff336ddc3", - "sha256:a2bc7e10ebcf4be503ae427f9887e75c0cc24e88ce467a8e6eaca6bd2862406e", - "sha256:a5b42e6292ba51b8e67e09fc256963ba4ca9c04026de004d2fe59cc17e3c3776", - "sha256:bd6ec1233c86c0b9bb5d03ec30dbe3ffbfa53335790320d99a7ae9018c5450f2", - "sha256:bef57530816af54d66b1f4c70a8f851f320cb6f84d4b5a0b422b0e9811ea4e59", - "sha256:c146a63eaadc6589b732780061f3c94cd0574388d372baccbb3c1597a9ebdb7a", - "sha256:c2efd3b130dc639d615b6f58980e1bfd1b177ad821f30827afa5001aa30ddd48", - "sha256:c888b18f7392e6cc79a33a803e7ebd7890ac3318f571fca6b356526f35b53b12", - "sha256:ca30721fda297ae22f16bc37aa7ed244970ddfdcb98247570cdd26daaad4665e", - "sha256:cf5f5340dd682ab034baa52f423a0f91326489c262ac9617fa06309ec05880e9", - "sha256:d0726aa0d9b57c56985db5952e90fb1033a317074f2877db5307cdd6eede1564", - "sha256:df442945b2dd6f8ae0e20b403e0fd4548cd5c2aad69200047cc3251257b78f65", - "sha256:e08e758c31919d167c0867539bd3b2441629ef00aa595e3ea2b635273659f40a", - "sha256:e4864339deeeaefaad34dd3a432ee618a039fca28efb292949c855e00878203c", - "sha256:f4cd049cb94d9f517b1cab5668a3b345968beba093bc79a637e671000b3540ec" + "sha256:0419ae5a45f49c7c40d9ae77ae4de9442431b7822851dfbbe56ee0eacb5e5654", + "sha256:1e8631eeee0fb0b4230aeb135e4890035f6ef9159c2a3555fa184468e325691a", + "sha256:24db2fa5438f3815a4edb7a189035051760ca6aa2b0b70a6a948b28bfc63c76b", + "sha256:2adb1cdb7d33e91069517b41249622710a94a1faece1fed31cd36904e4201cde", + "sha256:2cd51f35692b551aeb1fdeb7a256c7c558f6d78fcddff00640942d42f7aeba5f", + "sha256:3247834d24964589f8c2b121b40cd61319b3c2e8d744a6a82008643ef8a378b1", + "sha256:3433cb848b4209717722b62392e575a77a52a34d67c6730138102abc0a441685", + "sha256:39671b7ff77a962bd745746d9d2292c8ed227c5748f16598d16d8631d17dd7e5", + "sha256:40a0b8b2e6f6dd630f8b267eede2f40a848963d0f3c40b1b1f453a4a870f679e", + "sha256:40f9a74c7aa210b3e76eb1c9d56aa8d08722b73426a77626967019df9bbac287", + "sha256:423f76aa504c84cb94594fb88b8a24027c887f1c488cf58f2173f22f4fbd046c", + "sha256:43bd04cec72281a96eb361e1b0232f0f542b46da50bcfe72ef7e5a1b41d00cb3", + "sha256:43e38762635c09e24885d15e3a8e374b72d105d4178ee2cc9491855a8da9c380", + "sha256:4413b11c2385180d7de03add6c8845dd66692b148d36e27ec8c9ef537b2553a1", + "sha256:4450352a87094fd58daf468b04c65a9fa19ad11a0ac8ac7b7ff17d46f873cbc1", + "sha256:49ffda04a6e44de028b3b786278ac9a70043e7905c3eea29eed88b6524d53a29", + "sha256:4a38c4dde4c9120deef43aaabaa44f19186c98659ce554c29788c4071ab2f0a4", + "sha256:50b1febdfd21e2144b56a9aa226829e93a79c354ef22a4e5b013d9965e1ec0ed", + "sha256:559b1a3a8be7395ded2943ea6c2135d096f8cc7039d6d12127110b6496f251fe", + "sha256:5de86c182667ec68cf84019aa0d8ceccf01d352cdca19bf9e373725204bdbf50", + "sha256:5fc069bb481fe3fad0ba24d3baaf69e22dfa6cc1b63290e6dfeaf4ac1e996fb7", + "sha256:6a19d654da49516296515d6f65de4bbcbd734bc57913b21a610cfc45e6df3ff1", + "sha256:7535b3e52f498270e7877dde1c8944d6b7720e93e2e66b89c82a11447b5818f5", + "sha256:7c4e495bcabc308198b8962e60ca12f53b27eb8f03a21ac1d2d711d6dd9ecfca", + "sha256:8a8fc4a0220367cb8370cedac02272d574079ccc32bffbb34d53aaf9e38b5060", + "sha256:8b008515e067232838daca020d1af628bf6520c8cc338bf383284efe6d8bd083", + "sha256:8d1684258e1385e459418f3429e107eec5fb3d75e1f5a8c52e5946b3f329d6ea", + "sha256:8eb5d54b87fb561dc2e00a5c5226c33ffe8dbc13f2e4033a412bafb7b37b194d", + "sha256:94cdef0c61bd014bb7af495e21a1c3a369dd0399c3cd1965b1502043f5c88d94", + "sha256:9d9f3be69c7a5e84c3549a8c4403fa9ac7672da456863d21e390b2bbf45ccad1", + "sha256:9fb6fb5975a448169756da2d124a1beb38c0924ff6c0306d883b6848a9980f38", + "sha256:a5eaae8700b87144d7dfb475aa4675e500ff707292caba3deff41609ddc5b845", + "sha256:aaeac2d552772b76d24eaff67a5d2325bc5205c74c0d4f9fbe71685d4a971db2", + "sha256:bb611e447559b3b5665e12a7da5160c0de6876097f62bf1d23ba66911564868e", + "sha256:bc0d41f4eb07da8b8d3ea85e50b62f6491ab313834db86ae2345be07536a4e5a", + "sha256:bf51051c129b847d1bb63a9b0826346b5f52fb821b15fe5e0d5ef86f268510f5", + "sha256:c948c034d8997526011960db54f512756fb0b4be1b81140a15b4ef094c6594a4", + "sha256:d435a01334157c3b126b4ee5141401d44bdc8440993b18b05e2f267a6647f92d", + "sha256:d46c1f95672b73288e08cdca181e14e84c6229b5879561b7b8cfd48374e09287", + "sha256:d5d58309b42064228b16b0311ff715d6c6e20230e81b35e8d0c8cfa1bbdecad8", + "sha256:dc6e2e91365a1dd6314d615d80291159c7981928b88a4c65654e3fefac83a836", + "sha256:e0dfb5f7a39029a6cbec23affa923b22a2c02207960fd66f109e01d6f632c1eb", + "sha256:eb4bf58d381b1373bd21d50837a53953d625d1693f1b58fed12743c75d3dd321", + "sha256:ebb211a85248dbc396b29320273c1ffde484b898852432613e8df0164c091006", + "sha256:ec759ece4786ae993a5b7dc3b3dead6e9375d89a6c65dfd6860076d2eb2abe7b", + "sha256:f55108397a8fa164268238c3e69cc134e945d1f693572a2f05a028b8d0d2b837", + "sha256:f6c706866d424ff285b85a02de7bbe5ed0ace227766b2c42cbe12f3d9ea5a8aa", + "sha256:f8370ad332b36fbad117440faf0dd4b910e80b9c49db5648afd337abdde9a1b6" ], - "version": "==1.24.3" + "version": "==1.25.0" }, "invoke": { "hashes": [ @@ -207,6 +208,14 @@ ], "version": "==4.4.1" }, + "netaddr": { + "hashes": [ + "sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd", + "sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca" + ], + "index": "pypi", + "version": "==0.7.19" + }, "paramiko": { "hashes": [ "sha256:99f0179bdc176281d21961a003ffdb2ec369daac1a1007241f53374e376576cf", diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index b64aab98..8b8f38be 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -68,7 +68,7 @@ class CoreClient: self.id = 1 self.reusable = [] self.preexisting = set() - self.interfaces_manager = InterfaceManager() + self.interfaces_manager = InterfaceManager(self.app) self.wlan_configs = {} self.mobility_configs = {} self.emane_model_configs = {} @@ -523,57 +523,59 @@ class CoreClient: logging.error("invalid edge token: %s", i) def create_interface(self, canvas_node): - interface = None - core_node = canvas_node.core_node - if NodeUtils.is_container_node(core_node.type): - ifid = len(canvas_node.interfaces) - name = f"eth{ifid}" - interface = core_pb2.Interface( - id=ifid, - name=name, - ip4=str(self.interfaces_manager.get_address()), - ip4mask=24, - ) - canvas_node.interfaces.append(interface) - logging.debug( - "create node(%s) interface IPv4: %s, name: %s", - core_node.name, - interface.ip4, - interface.name, - ) + node = canvas_node.core_node + ip4, ip6, prefix = self.interfaces_manager.get_ips(node.id) + interface_id = len(canvas_node.interfaces) + name = f"eth{interface_id}" + interface = core_pb2.Interface( + id=interface_id, name=name, ip4=ip4, ip4mask=prefix, ip6=ip6, ip6mask=prefix + ) + canvas_node.interfaces.append(interface) + logging.debug( + "create node(%s) interface IPv4: %s, name: %s", + node.name, + interface.ip4, + interface.name, + ) return interface - def create_link(self, token, canvas_node_one, canvas_node_two): + def create_link(self, edge, canvas_src_node, canvas_dst_node): """ Create core link for a pair of canvas nodes, with token referencing the canvas edge. - :param tuple(int, int) token: edge's identification in the canvas - :param canvas_node_one: canvas node one - :param canvas_node_two: canvas node two + :param edge: edge for link + :param canvas_src_node: canvas node one + :param canvas_dst_node: canvas node two :return: nothing """ - node_one = canvas_node_one.core_node - node_two = canvas_node_two.core_node + src_node = canvas_src_node.core_node + dst_node = canvas_dst_node.core_node - # create interfaces - self.interfaces_manager.new_subnet() - interface_one = self.create_interface(canvas_node_one) - if interface_one is not None: - self.interface_to_edge[(node_one.id, interface_one.id)] = token - interface_two = self.create_interface(canvas_node_two) - if interface_two is not None: - self.interface_to_edge[(node_two.id, interface_two.id)] = token + # determine subnet + self.interfaces_manager.determine_subnet(canvas_src_node, canvas_dst_node) + + src_interface = None + if NodeUtils.is_container_node(src_node.type): + src_interface = self.create_interface(canvas_src_node) + edge.src_interface = src_interface + self.interface_to_edge[(src_node.id, src_interface.id)] = edge.token + + dst_interface = None + if NodeUtils.is_container_node(dst_node.type): + dst_interface = self.create_interface(canvas_dst_node) + edge.dst_interface = dst_interface + self.interface_to_edge[(dst_node.id, dst_interface.id)] = edge.token link = core_pb2.Link( type=core_pb2.LinkType.WIRED, - node_one_id=node_one.id, - node_two_id=node_two.id, - interface_one=interface_one, - interface_two=interface_two, + node_one_id=src_node.id, + node_two_id=dst_node.id, + interface_one=src_interface, + interface_two=dst_interface, ) - self.links[token] = link + self.links[edge.token] = link return link def get_wlan_configs_proto(self): diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index b8749e1d..fc14246d 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -297,9 +297,8 @@ class CanvasGraph(tk.Canvas): # edge dst must be a node logging.debug(f"current selected: {self.selected}") - logging.debug(f"current nodes: {self.find_withtag('node')}") - is_node = self.selected in self.find_withtag("node") - if not is_node: + dst_node = self.nodes.get(self.selected) + if not dst_node: edge.delete() return @@ -320,7 +319,7 @@ class CanvasGraph(tk.Canvas): node_src.edges.add(edge) node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) - link = self.core.create_link(edge.token, node_src, node_dst) + link = self.core.create_link(edge, node_src, node_dst) # draw link info on the edge ip4_and_prefix_1 = None @@ -570,8 +569,9 @@ class CanvasEdge: """ self.src = src self.dst = None + self.src_interface = None + self.dst_interface = None self.canvas = canvas - if is_wired is None or is_wired is True: self.id = self.canvas.create_line( x1, y1, x2, y2, tags="edge", width=self.width, fill="#ff0000" @@ -588,8 +588,6 @@ class CanvasEdge: state=tk.HIDDEN, ) self.token = None - - # link info object self.link_info = None self.throughput = None self.wired = is_wired diff --git a/coretk/coretk/interface.py b/coretk/coretk/interface.py index b0fcd346..9b58e7e6 100644 --- a/coretk/coretk/interface.py +++ b/coretk/coretk/interface.py @@ -1,45 +1,76 @@ -import ipaddress +import logging +import random + +from netaddr import IPNetwork + +from coretk.nodeutils import NodeUtils -class SubnetAddresses: - def __init__(self, network, addresses): - self.network = network - self.address = addresses - self.address_index = 0 - - def get_new_ip_address(self): - ipaddr = self.address[self.address_index] - self.address_index = self.address_index + 1 - return ipaddr +def random_mac(): + return ("{:02x}" * 6).format(*[random.randrange(256) for _ in range(6)]) class InterfaceManager: - def __init__(self): - self.core_subnets = list( - ipaddress.ip_network("10.0.0.0/12").subnets(prefixlen_diff=12) - ) - self.subnet_index = 0 - self.address_index = 0 - self.network = None - self.addresses = None + def __init__(self, app, cidr="10.0.0.0/24"): + self.app = app + self.cidr = IPNetwork(cidr) + self.deleted = [] + self.current = None - def start_interface_manager(self): - self.subnet_index = 0 - self.network = self.core_subnets[self.subnet_index] - self.subnet_index = self.subnet_index + 1 - self.addresses = list(self.network.hosts()) - self.address_index = 0 + def get_ips(self, node_id): + ip4 = self.current[node_id] + ip6 = ip4.ipv6() + return str(ip4), str(ip6), self.current.prefixlen - def get_address(self): - """ - Retrieve a new ipv4 address + def next_subnet(self): + if self.current: + self.cidr = self.cidr.next() + return self.cidr - :return: - """ - ipaddr = self.addresses[self.address_index] - self.address_index = self.address_index + 1 - return ipaddr + def determine_subnet(self, canvas_src_node, canvas_dst_node): + src_node = canvas_src_node.core_node + dst_node = canvas_dst_node.core_node + is_src_container = NodeUtils.is_container_node(src_node.type) + is_dst_container = NodeUtils.is_container_node(dst_node.type) + if is_src_container and is_dst_container: + self.current = self.next_subnet() + elif is_src_container and not is_dst_container: + cidr = self.find_subnet(canvas_dst_node, visited={src_node.id}) + if cidr: + self.current = cidr + else: + self.current = self.next_subnet() + # else: + # self.current = self.cidr + elif not is_src_container and is_dst_container: + cidr = self.find_subnet(canvas_src_node, visited={dst_node.id}) + if cidr: + self.current = self.cidr + else: + self.current = self.next_subnet() + else: + logging.info("ignoring subnet change for link between network nodes") - def new_subnet(self): - self.network = self.core_subnets[self.subnet_index] - self.addresses = list(self.network.hosts()) + def find_subnet(self, canvas_node, visited): + logging.info("finding subnet for node: %s", canvas_node.core_node.name) + canvas = self.app.canvas + cidr = None + visited.add(canvas_node.core_node.id) + for edge in canvas_node.edges: + src_node = canvas.nodes[edge.src] + dst_node = canvas.nodes[edge.dst] + interface = edge.src_interface + check_node = src_node + if src_node == canvas_node: + interface = edge.dst_interface + check_node = dst_node + if check_node.core_node.id in visited: + continue + visited.add(check_node.core_node.id) + if interface: + logging.info("found interface: %s", interface) + cidr = IPNetwork(f"{interface.ip4}/{interface.ip4mask}").cidr + break + else: + cidr = self.find_subnet(check_node, visited) + return cidr diff --git a/coretk/setup.py b/coretk/setup.py index 8b8ce2d3..846ab074 100644 --- a/coretk/setup.py +++ b/coretk/setup.py @@ -4,7 +4,7 @@ setup( name="coretk", version="0.1.0", packages=find_packages(), - install_requires=["pillow"], + install_requires=["netaddr", "pillow"], description="CORE GUI", url="https://github.com/coreemu/core", author="Boeing Research & Technology", From 15e05ac580b37fd83378179447406291f07e1320 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 22 Nov 2019 12:59:22 -0800 Subject: [PATCH 264/462] work on status bar --- coretk/coretk/app.py | 3 ++- coretk/coretk/status.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 coretk/coretk/status.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 68060927..f0445257 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -10,6 +10,7 @@ from coretk.images import ImageEnum, Images from coretk.menuaction import MenuAction from coretk.menubar import Menubar from coretk.nodeutils import NodeUtils +from coretk.status import StatusBar from coretk.toolbar import Toolbar @@ -80,7 +81,7 @@ class Application(tk.Frame): self.canvas.configure(yscrollcommand=scroll_y.set) def draw_status(self): - self.statusbar = ttk.Frame(self) + self.statusbar = StatusBar(master=self, app=self) self.statusbar.pack(side=tk.BOTTOM, fill=tk.X) def on_closing(self): diff --git a/coretk/coretk/status.py b/coretk/coretk/status.py new file mode 100644 index 00000000..dde16cd8 --- /dev/null +++ b/coretk/coretk/status.py @@ -0,0 +1,29 @@ +"status bar" +from tkinter import ttk + + +class StatusBar(ttk.Frame): + def __init__(self, master, app, **kwargs): + super().__init__(master, **kwargs) + self.app = app + + self.status = None + self.zoom = None + self.cpu_usage = None + self.memory = None + self.emulation_light = None + self.draw() + + def draw(self): + self.columnconfigure(0, weight=8) + self.columnconfigure(1, weight=1) + self.columnconfigure(2, weight=1) + self.columnconfigure(3, weight=1) + self.status = ttk.Label(self, text="status") + self.status.grid(row=0, column=0) + self.zoom = ttk.Label(self, text="zoom") + self.zoom.grid(row=0, column=1) + self.cpu_usage = ttk.Label(self, text="cpu usage") + self.cpu_usage.grid(row=0, column=2) + self.emulation_light = ttk.Label(self, text="emulation light") + self.emulation_light.grid(row=0, column=3) From 52c6f2f31c7a7766fbeed987e0419af18da559e9 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 22 Nov 2019 14:52:46 -0800 Subject: [PATCH 265/462] fixed issue with services identifying ip4/ip6 addresses --- daemon/core/services/bird.py | 7 ++++--- daemon/core/services/frr.py | 13 ++++++++----- daemon/core/services/nrl.py | 7 ++++--- daemon/core/services/quagga.py | 18 ++++++++++-------- daemon/core/services/sdn.py | 6 ++++-- daemon/core/services/utility.py | 13 +++++++++---- daemon/core/services/xorp.py | 18 ++++++++++-------- 7 files changed, 49 insertions(+), 33 deletions(-) diff --git a/daemon/core/services/bird.py b/daemon/core/services/bird.py index 80774201..a0f4c640 100644 --- a/daemon/core/services/bird.py +++ b/daemon/core/services/bird.py @@ -1,7 +1,7 @@ """ bird.py: defines routing services provided by the BIRD Internet Routing Daemon. """ - +from core.nodes import ipaddress from core.services.coreservices import CoreService @@ -38,8 +38,9 @@ class Bird(CoreService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") >= 0: - return a.split("/")[0] + a = a.split("/")[0] + if ipaddress.is_ipv4_address(a): + return a # raise ValueError, "no IPv4 address found for router ID" return "0.0.0.0" diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py index b4332009..95ffb0b5 100644 --- a/daemon/core/services/frr.py +++ b/daemon/core/services/frr.py @@ -112,9 +112,10 @@ class FRRZebra(CoreService): """ helper for mapping IP addresses to zebra config statements """ - if x.find(".") >= 0: + addr = x.split("/")[0] + if ipaddress.is_ipv4_address(addr): return "ip address %s" % x - elif x.find(":") >= 0: + elif ipaddress.is_ipv6_address(addr): return "ipv6 address %s" % x else: raise ValueError("invalid address: %s", x) @@ -328,8 +329,9 @@ class FrrService(CoreService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") >= 0: - return a.split("/")[0] + a = a.split("/")[0] + if ipaddress.is_ipv4_address(a): + return a # raise ValueError, "no IPv4 address found for router ID" return "0.0.0.0" @@ -411,7 +413,8 @@ class FRROspfv2(FrrService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") < 0: + addr = a.split("/")[0] + if not ipaddress.is_ipv4_address(addr): continue net = ipaddress.Ipv4Prefix(a) cfg += " network %s area 0\n" % net diff --git a/daemon/core/services/nrl.py b/daemon/core/services/nrl.py index a610f1cc..0a6b1f92 100644 --- a/daemon/core/services/nrl.py +++ b/daemon/core/services/nrl.py @@ -4,6 +4,7 @@ nrl.py: defines services provided by NRL protolib tools hosted here: """ from core import utils +from core.nodes import ipaddress from core.nodes.ipaddress import Ipv4Prefix from core.services.coreservices import CoreService @@ -36,9 +37,9 @@ class NrlService(CoreService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") >= 0: - addr = a.split("/")[0] - pre = Ipv4Prefix("%s/%s" % (addr, prefixlen)) + a = a.split("/")[0] + if ipaddress.is_ipv4_address(a): + pre = Ipv4Prefix("%s/%s" % (a, prefixlen)) return str(pre) # raise ValueError, "no IPv4 address found" return "0.0.0.0/%s" % prefixlen diff --git a/daemon/core/services/quagga.py b/daemon/core/services/quagga.py index 5b5cf0ba..267bbcdd 100644 --- a/daemon/core/services/quagga.py +++ b/daemon/core/services/quagga.py @@ -109,9 +109,10 @@ class Zebra(CoreService): """ helper for mapping IP addresses to zebra config statements """ - if x.find(".") >= 0: + addr = x.split("/")[0] + if ipaddress.is_ipv4_address(addr): return "ip address %s" % x - elif x.find(":") >= 0: + elif ipaddress.is_ipv6_address(addr): return "ipv6 address %s" % x else: raise ValueError("invalid address: %s", x) @@ -255,8 +256,9 @@ class QuaggaService(CoreService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") >= 0: - return a.split("/")[0] + a = a.split("/")[0] + if ipaddress.is_ipv4_address(a): + return a # raise ValueError, "no IPv4 address found for router ID" return "0.0.0.0" @@ -338,10 +340,10 @@ class Ospfv2(QuaggaService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") < 0: - continue - net = ipaddress.Ipv4Prefix(a) - cfg += " network %s area 0\n" % net + addr = a.split("/")[0] + if ipaddress.is_ipv4_address(addr): + net = ipaddress.Ipv4Prefix(a) + cfg += " network %s area 0\n" % net cfg += "!\n" return cfg diff --git a/daemon/core/services/sdn.py b/daemon/core/services/sdn.py index c837de53..d924abd7 100644 --- a/daemon/core/services/sdn.py +++ b/daemon/core/services/sdn.py @@ -4,6 +4,7 @@ sdn.py defines services to start Open vSwitch and the Ryu SDN Controller. import re +from core.nodes import ipaddress from core.services.coreservices import CoreService @@ -56,11 +57,12 @@ class OvsService(SdnService): # remove ip address of eths because quagga/zebra will assign same IPs to rtr interfaces # or assign them manually to rtr interfaces if zebra is not running for ifcaddr in ifc.addrlist: - if ifcaddr.find(".") >= 0: + addr = ifcaddr.split("/")[0] + if ipaddress.is_ipv4_address(addr): cfg += "ip addr del %s dev %s\n" % (ifcaddr, ifc.name) if has_zebra == 0: cfg += "ip addr add %s dev rtr%s\n" % (ifcaddr, ifnum) - elif ifcaddr.find(":") >= 0: + elif ipaddress.is_ipv6_address(addr): cfg += "ip -6 addr del %s dev %s\n" % (ifcaddr, ifc.name) if has_zebra == 0: cfg += "ip -6 addr add %s dev rtr%s\n" % (ifcaddr, ifnum) diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index e408b182..16dc6906 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -6,6 +6,7 @@ import os from core import constants, utils from core.errors import CoreCommandError +from core.nodes import ipaddress from core.nodes.ipaddress import Ipv4Prefix, Ipv6Prefix from core.services.coreservices import CoreService @@ -87,7 +88,8 @@ class DefaultRouteService(UtilService): @staticmethod def addrstr(x): - if x.find(":") >= 0: + addr = x.split("/")[0] + if ipaddress.is_ipv6_address(addr): net = Ipv6Prefix(x) else: net = Ipv4Prefix(x) @@ -147,7 +149,8 @@ class StaticRouteService(UtilService): @staticmethod def routestr(x): - if x.find(":") >= 0: + addr = x.split("/")[0] + if ipaddress.is_ipv6_address(addr): net = Ipv6Prefix(x) dst = "3ffe:4::/64" else: @@ -280,7 +283,8 @@ ddns-update-style none; Generate a subnet declaration block given an IPv4 prefix string for inclusion in the dhcpd3 config file. """ - if x.find(":") >= 0: + addr = x.split("/")[0] + if ipaddress.is_ipv6_address(addr): return "" else: addr = x.split("/")[0] @@ -702,7 +706,8 @@ interface %s Generate a subnet declaration block given an IPv6 prefix string for inclusion in the RADVD config file. """ - if x.find(":") >= 0: + addr = x.split("/")[0] + if ipaddress.is_ipv6_address(addr): net = Ipv6Prefix(x) return str(net) else: diff --git a/daemon/core/services/xorp.py b/daemon/core/services/xorp.py index 1cd62620..3c1de852 100644 --- a/daemon/core/services/xorp.py +++ b/daemon/core/services/xorp.py @@ -4,6 +4,7 @@ xorp.py: defines routing services provided by the XORP routing suite. import logging +from core.nodes import ipaddress from core.services.coreservices import CoreService @@ -150,8 +151,9 @@ class XorpService(CoreService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") >= 0: - return a.split("/")[0] + a = a.split("/")[0] + if ipaddress.is_ipv4_address(a): + return a # raise ValueError, "no IPv4 address found for router ID" return "0.0.0.0" @@ -187,9 +189,9 @@ class XorpOspfv2(XorpService): cfg += "\t interface %s {\n" % ifc.name cfg += "\t\tvif %s {\n" % ifc.name for a in ifc.addrlist: - if a.find(".") < 0: - continue addr = a.split("/")[0] + if not ipaddress.is_ipv4_address(addr): + continue cfg += "\t\t address %s {\n" % addr cfg += "\t\t }\n" cfg += "\t\t}\n" @@ -280,9 +282,9 @@ class XorpRip(XorpService): cfg += "\tinterface %s {\n" % ifc.name cfg += "\t vif %s {\n" % ifc.name for a in ifc.addrlist: - if a.find(".") < 0: - continue addr = a.split("/")[0] + if not ipaddress.is_ipv4_address(addr): + continue cfg += "\t\taddress %s {\n" % addr cfg += "\t\t disable: false\n" cfg += "\t\t}\n" @@ -462,9 +464,9 @@ class XorpOlsr(XorpService): cfg += "\tinterface %s {\n" % ifc.name cfg += "\t vif %s {\n" % ifc.name for a in ifc.addrlist: - if a.find(".") < 0: - continue addr = a.split("/")[0] + if not ipaddress.is_ipv4_address(addr): + continue cfg += "\t\taddress %s {\n" % addr cfg += "\t\t}\n" cfg += "\t }\n" From 8ffac10a1e76c241df2cea87b2d5b816773a1ea7 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 22 Nov 2019 14:55:10 -0800 Subject: [PATCH 266/462] status bar --- coretk/coretk/coreclient.py | 3 ++- coretk/coretk/status.py | 15 +++++++++++++++ coretk/coretk/toolbar.py | 10 ++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 46d1cdad..b75b66ed 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -327,6 +327,7 @@ class CoreClient: emane_config = {x: self.emane_config[x].value for x in self.emane_config} else: emane_config = None + print(links) response = self.client.start_session( self.session_id, nodes, @@ -341,7 +342,7 @@ class CoreClient: file_configs, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) - print(self.client.get_session(self.session_id)) + # print(self.client.get_session(self.session_id)) def stop_session(self): response = self.client.stop_session(session_id=self.session_id) diff --git a/coretk/coretk/status.py b/coretk/coretk/status.py index dde16cd8..2a160457 100644 --- a/coretk/coretk/status.py +++ b/coretk/coretk/status.py @@ -1,4 +1,5 @@ "status bar" +import time from tkinter import ttk @@ -12,6 +13,7 @@ class StatusBar(ttk.Frame): self.cpu_usage = None self.memory = None self.emulation_light = None + self.running = False self.draw() def draw(self): @@ -27,3 +29,16 @@ class StatusBar(ttk.Frame): self.cpu_usage.grid(row=0, column=2) self.emulation_light = ttk.Label(self, text="emulation light") self.emulation_light.grid(row=0, column=3) + + def processing(self): + self.running = True + texts = ["Processing.", "Processing..", "Processing...", "Processing...."] + i = 0 + while self.running is True: + self.status.config(text=texts[i % 4]) + self.app.master.update() + i = i + 1 + time.sleep(0.3) + print("running") + print("thread finish") + # self.status.config(text="status") diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 834f7b39..648a2d8c 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -1,4 +1,5 @@ import logging +import threading import tkinter as tk from functools import partial from tkinter import ttk @@ -197,12 +198,21 @@ class Toolbar(ttk.Frame): """ logging.debug("clicked start button") self.master.config(cursor="watch") + status_thread = threading.Thread(target=self.app.statusbar.processing) + status_thread.start() self.master.update() self.app.canvas.mode = GraphMode.SELECT self.app.core.start_session() self.runtime_frame.tkraise() self.master.config(cursor="") + self.app.statusbar.running = False + if status_thread.is_alive(): + print("still running") + status_thread.join() + print("thread terminate") + self.app.statusbar.status.config(text="status") + def click_link(self): logging.debug("Click LINK button") self.design_select(self.link_button) From 981be3b7ffae6a4cbfd448d9ef00ed5a85b77286 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 22 Nov 2019 14:58:41 -0800 Subject: [PATCH 267/462] small cleanup with stopping sessions --- coretk/coretk/coreclient.py | 93 ++++++++++--------------------- coretk/coretk/dialogs/sessions.py | 2 +- coretk/coretk/interface.py | 6 ++ 3 files changed, 36 insertions(+), 65 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 46d1cdad..08003dcf 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -74,10 +74,25 @@ class CoreClient: self.emane_config = None self.created_nodes = set() self.created_links = set() - self.service_configs = {} self.file_configs = {} + def reset(self): + self.id = 1 + self.reusable.clear() + self.preexisting.clear() + self.canvas_nodes.clear() + self.links.clear() + self.hooks.clear() + self.wlan_configs.clear() + self.mobility_configs.clear() + self.emane_config = None + self.location = None + self.service_configs.clear() + self.file_configs.clear() + self.interfaces_manager.reset() + self.interface_to_edge.clear() + def set_observer(self, value): self.observer = value @@ -130,16 +145,7 @@ class CoreClient: self.master.title(f"CORE Session({self.session_id})") # clear session data - self.reusable.clear() - self.preexisting.clear() - self.canvas_nodes.clear() - self.links.clear() - self.hooks.clear() - self.wlan_configs.clear() - self.mobility_configs.clear() - self.emane_config = None - self.service_configs.clear() - self.file_configs.clear() + self.reset() # get session data response = self.client.get_session(self.session_id) @@ -224,29 +230,12 @@ class CoreClient: ) self.join_session(response.session_id, query_location=False) - def delete_session(self, custom_sid=None): - if custom_sid is None: - sid = self.session_id - else: - sid = custom_sid - response = self.client.delete_session(sid) + def delete_session(self, session_id=None): + if session_id is None: + session_id = self.session_id + response = self.client.delete_session(session_id) logging.info("Deleted session result: %s", response) - def shutdown_session(self, custom_sid=None): - if custom_sid is None: - sid = self.session_id - else: - sid = custom_sid - s = self.client.get_session(sid).session - # delete links and nodes from running session - if s.state == core_pb2.SessionState.RUNTIME: - self.client.set_session_state( - self.session_id, core_pb2.SessionState.DATACOLLECT - ) - self.delete_links(sid) - self.delete_nodes(sid) - self.delete_session(sid) - def set_up(self): """ Query sessions, if there exist any, prompt whether to join one @@ -287,31 +276,6 @@ class CoreClient: response = self.client.edit_node(self.session_id, node_id, position) logging.info("updated node id %s: %s", node_id, response) - def delete_nodes(self, delete_session=None): - if delete_session is None: - sid = self.session_id - else: - sid = delete_session - for node in self.client.get_session(sid).session.nodes: - response = self.client.delete_node(self.session_id, node.id) - logging.info("delete nodes %s", response) - - def delete_links(self, delete_session=None): - if delete_session is None: - sid = self.session_id - else: - sid = delete_session - - for link in self.client.get_session(sid).session.links: - response = self.client.delete_link( - self.session_id, - link.node_one_id, - link.node_two_id, - link.interface_one.id, - link.interface_two.id, - ) - logging.info("delete links %s", response) - def start_session(self): nodes = [x.core_node for x in self.canvas_nodes.values()] links = list(self.links.values()) @@ -340,17 +304,18 @@ class CoreClient: service_configs, file_configs, ) - logging.debug("Start session %s, result: %s", self.session_id, response.result) - print(self.client.get_session(self.session_id)) + logging.debug("start session(%s), result: %s", self.session_id, response.result) - def stop_session(self): - response = self.client.stop_session(session_id=self.session_id) - logging.debug("coregrpc.py Stop session, result: %s", response.result) + def stop_session(self, session_id=None): + if not session_id: + session_id = self.session_id + response = self.client.stop_session(session_id) + logging.debug("stopped session(%s), result: %s", session_id, response.result) def launch_terminal(self, node_id): response = self.client.get_node_terminal(self.session_id, node_id) logging.info("get terminal %s", response.terminal) - os.system("xterm -e %s &" % response.terminal) + os.system(f"xterm -e {response.terminal} &") def save_xml(self, file_path): """ @@ -360,7 +325,7 @@ class CoreClient: :return: nothing """ response = self.client.save_xml(self.session_id, file_path) - logging.info("coregrpc.py save xml %s", response) + logging.info("saved xml(%s): %s", file_path, response) self.client.events(self.session_id, self.handle_events) def open_xml(self, file_path): diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index 6ea66973..6f299dfc 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -155,6 +155,6 @@ class SessionsDialog(Dialog): self.join_session(sid) def shutdown_session(self, sid): - self.app.core.shutdown_session(sid) + self.app.core.stop_session(sid) self.click_new() self.destroy() diff --git a/coretk/coretk/interface.py b/coretk/coretk/interface.py index 9b58e7e6..e1203389 100644 --- a/coretk/coretk/interface.py +++ b/coretk/coretk/interface.py @@ -13,10 +13,16 @@ def random_mac(): class InterfaceManager: def __init__(self, app, cidr="10.0.0.0/24"): self.app = app + self.default = cidr self.cidr = IPNetwork(cidr) self.deleted = [] self.current = None + def reset(self): + self.cidr = IPNetwork(self.default) + self.deleted.clear() + self.current = None + def get_ips(self, node_id): ip4 = self.current[node_id] ip6 = ip4.ipv6() From 46627aad11e7befabf8f28ebcc93ae324970a855 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 22 Nov 2019 15:43:43 -0800 Subject: [PATCH 268/462] renamed coreclient delete graph nodes func --- coretk/coretk/coreclient.py | 33 ++++++++++++++++++++------------- coretk/coretk/graph.py | 2 +- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 08003dcf..309fcba2 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -57,41 +57,48 @@ class CoreClient: self.custom_observers = {} self.read_config() - # data for managing the current session - self.canvas_nodes = {} - self.location = None + # helpers self.interface_to_edge = {} - self.state = None - self.links = {} - self.hooks = {} - self.id = 1 self.reusable = [] self.preexisting = set() self.interfaces_manager = InterfaceManager(self.app) + self.created_nodes = set() + self.created_links = set() + + # session data + self.id = 1 + self.state = None + self.canvas_nodes = {} + self.location = None + self.links = {} + self.hooks = {} self.wlan_configs = {} self.mobility_configs = {} self.emane_model_configs = {} self.emane_config = None - self.created_nodes = set() - self.created_links = set() self.service_configs = {} self.file_configs = {} def reset(self): self.id = 1 + # helpers + self.created_nodes.clear() + self.created_links.clear() self.reusable.clear() self.preexisting.clear() + self.interfaces_manager.reset() + self.interface_to_edge.clear() + # session data self.canvas_nodes.clear() + self.location = None self.links.clear() self.hooks.clear() self.wlan_configs.clear() self.mobility_configs.clear() + self.emane_model_configs.clear() self.emane_config = None - self.location = None self.service_configs.clear() self.file_configs.clear() - self.interfaces_manager.reset() - self.interface_to_edge.clear() def set_observer(self, value): self.observer = value @@ -462,7 +469,7 @@ class CoreClient: ) return node - def delete_wanted_graph_nodes(self, node_ids, edge_tokens): + def delete_graph_nodes(self, node_ids, edge_tokens): """ remove the nodes selected by the user and anything related to that node such as link, configurations, interfaces diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index fc14246d..b87bccbf 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -411,7 +411,7 @@ class CanvasGraph(tk.Canvas): self.nodes[nid].edges.remove(edge) # delete the related data from core - self.core.delete_wanted_graph_nodes(node_ids, to_delete_edge_tokens) + self.core.delete_graph_nodes(node_ids, to_delete_edge_tokens) def add_node(self, x, y): plot_id = self.find_all()[0] From e8f8fa3bd5a376bc58646407b8c12d4893feba40 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 22 Nov 2019 16:30:25 -0800 Subject: [PATCH 269/462] cleaned up some of the logic for deleting nodes/edges --- coretk/coretk/coreclient.py | 10 ++++----- coretk/coretk/graph.py | 27 ++++------------------- coretk/coretk/interface.py | 3 +++ coretk/coretk/nodedelete.py | 43 +++++++++++++++++++++---------------- 4 files changed, 37 insertions(+), 46 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 309fcba2..cd10a073 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -469,13 +469,13 @@ class CoreClient: ) return node - def delete_graph_nodes(self, node_ids, edge_tokens): + def delete_graph_nodes(self, node_ids, edges): """ remove the nodes selected by the user and anything related to that node such as link, configurations, interfaces :param list[int] node_ids: list of nodes to delete - :param list edge_tokens: list of edges to delete + :param list edges: list of edges to delete :return: nothing """ # delete the nodes @@ -496,11 +496,11 @@ class CoreClient: self.reusable.sort() # delete the edges and interfaces - for i in edge_tokens: + for edge in edges: try: - self.links.pop(i) + self.links.pop(edge.token) except KeyError: - logging.error("invalid edge token: %s", i) + logging.error("invalid edge token: %s", edge.token) def create_interface(self, canvas_node): node = canvas_node.core_node diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index b87bccbf..3f140261 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -388,30 +388,11 @@ class CanvasGraph(tk.Canvas): :param event: :return: """ - # hide nodes, links, link information that shows on the GUI - to_delete_nodes, to_delete_edge_tokens = ( - self.canvas_management.delete_selected_nodes() - ) + # delete canvas data + node_ids, edges = self.canvas_management.delete_selected_nodes() - # delete nodes and link info stored in CanvasGraph object - node_ids = [] - for nid in to_delete_nodes: - canvas_node = self.nodes.pop(nid) - node_ids.append(canvas_node.core_node.id) - for token in to_delete_edge_tokens: - self.edges.pop(token) - - # delete the edge data inside of canvas node - canvas_node_link_to_delete = [] - for canvas_id, node in self.nodes.items(): - for e in node.edges: - if e.token in to_delete_edge_tokens: - canvas_node_link_to_delete.append(tuple([canvas_id, e])) - for nid, edge in canvas_node_link_to_delete: - self.nodes[nid].edges.remove(edge) - - # delete the related data from core - self.core.delete_graph_nodes(node_ids, to_delete_edge_tokens) + # delete core data + self.core.delete_graph_nodes(node_ids, edges) def add_node(self, x, y): plot_id = self.find_all()[0] diff --git a/coretk/coretk/interface.py b/coretk/coretk/interface.py index e1203389..7d0c374c 100644 --- a/coretk/coretk/interface.py +++ b/coretk/coretk/interface.py @@ -33,6 +33,9 @@ class InterfaceManager: self.cidr = self.cidr.next() return self.cidr + def deleted_interface(self, interface): + logging.info("deleted interface: %s", interface) + def determine_subnet(self, canvas_src_node, canvas_dst_node): src_node = canvas_src_node.core_node dst_node = canvas_dst_node.core_node diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index 6ce3a6f9..496bbc6d 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -39,23 +39,30 @@ class CanvasComponentManagement: self.selected.clear() def delete_selected_nodes(self): - selected_nodes = list(self.selected.keys()) edges = set() - for n in selected_nodes: - edges = edges.union(self.canvas.nodes[n].edges) - edge_canvas_ids = [x.id for x in edges] - edge_tokens = [x.token for x in edges] - link_infos = [x.link_info.id1 for x in edges] + [x.link_info.id2 for x in edges] - - for i in edge_canvas_ids: - self.canvas.itemconfig(i, state="hidden") - - for i in link_infos: - self.canvas.itemconfig(i, state="hidden") - - for cnid, bbid in self.selected.items(): - self.canvas.itemconfig(cnid, state="hidden") - self.canvas.itemconfig(bbid, state="hidden") - self.canvas.itemconfig(self.canvas.nodes[cnid].text_id, state="hidden") + node_ids = [] + for node_id in list(self.selected): + bbox_id = self.selected[node_id] + canvas_node = self.canvas.nodes.pop(node_id) + node_ids.append(canvas_node.core_node.id) + self.canvas.delete(node_id) + self.canvas.delete(bbox_id) + self.canvas.delete(canvas_node.text_id) + for edge in canvas_node.edges: + if edge in edges: + continue + edges.add(edge) + self.canvas.edges.pop(edge.token) + self.canvas.delete(edge.id) + self.canvas.delete(edge.link_info.id1) + self.canvas.delete(edge.link_info.id2) + other_id = edge.src + other_interface = edge.src_interface + if edge.src == node_id: + other_id = edge.dst + other_interface = edge.dst_interface + other_node = self.canvas.nodes[other_id] + other_node.edges.remove(edge) + other_node.interfaces.remove(other_interface) self.selected.clear() - return selected_nodes, edge_tokens + return node_ids, edges From 4e32c9c13cf934d93b90cccf9feaf50103a1c6d5 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 22 Nov 2019 23:48:10 -0800 Subject: [PATCH 270/462] updates to add reuse of deleted subnets --- coretk/coretk/coreclient.py | 76 +++++++++++++++++++++++++------------ coretk/coretk/graph.py | 4 +- coretk/coretk/interface.py | 35 +++++++++++------ coretk/coretk/nodedelete.py | 11 ++++-- 4 files changed, 83 insertions(+), 43 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index cd10a073..d5a2f854 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -469,38 +469,64 @@ class CoreClient: ) return node - def delete_graph_nodes(self, node_ids, edges): + def delete_graph_nodes(self, canvas_nodes): """ remove the nodes selected by the user and anything related to that node such as link, configurations, interfaces - :param list[int] node_ids: list of nodes to delete - :param list edges: list of edges to delete + :param list canvas_nodes: list of nodes to delete :return: nothing """ - # delete the nodes - for i in node_ids: - try: - del self.canvas_nodes[i] - self.reusable.append(i) - if i in self.mobility_configs: - del self.mobility_configs[i] - if i in self.wlan_configs: - del self.wlan_configs[i] - for key in list(self.emane_model_configs): - node_id, _, _ = key - if node_id == i: - del self.emane_model_configs[key] - except KeyError: - logging.error("invalid canvas id: %s", i) - self.reusable.sort() + edges = set() + for canvas_node in canvas_nodes: + node_id = canvas_node.core_node.id + if node_id not in self.canvas_nodes: + logging.error("unknown node: %s", node_id) + continue + del self.canvas_nodes[node_id] + self.reusable.append(node_id) + if node_id in self.mobility_configs: + del self.mobility_configs[node_id] + if node_id in self.wlan_configs: + del self.wlan_configs[node_id] + for key in list(self.emane_model_configs): + node_id, _, _ = key + if node_id == node_id: + del self.emane_model_configs[key] - # delete the edges and interfaces - for edge in edges: - try: - self.links.pop(edge.token) - except KeyError: - logging.error("invalid edge token: %s", edge.token) + deleted_cidrs = set() + keep_cidrs = set() + for edge in canvas_node.edges: + if edge in edges: + continue + edges.add(edge) + if edge.token not in self.links: + logging.error("unknown edge: %s", edge.token) + del self.links[edge.token] + other_id = edge.src + other_interface = edge.src_interface + interface = edge.dst_interface + if canvas_node.id == edge.src: + other_id = edge.dst + other_interface = edge.dst_interface + interface = edge.src_interface + other_node = self.app.canvas.nodes.get(other_id) + if not other_node: + continue + if other_interface: + cidr = self.interfaces_manager.get_cidr(other_interface) + deleted_cidrs.add(cidr) + else: + cidr = self.interfaces_manager.find_subnet(other_node) + if cidr: + keep_cidrs.add(cidr) + else: + cidr = self.interfaces_manager.get_cidr(interface) + deleted_cidrs.add(cidr) + deleted_cidrs = deleted_cidrs - keep_cidrs + for cidr in deleted_cidrs: + self.interfaces_manager.deleted_cidr(cidr) + self.reusable.sort() def create_interface(self, canvas_node): node = canvas_node.core_node diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 3f140261..14e978be 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -389,10 +389,10 @@ class CanvasGraph(tk.Canvas): :return: """ # delete canvas data - node_ids, edges = self.canvas_management.delete_selected_nodes() + nodes = self.canvas_management.delete_selected_nodes() # delete core data - self.core.delete_graph_nodes(node_ids, edges) + self.core.delete_graph_nodes(nodes) def add_node(self, x, y): plot_id = self.find_all()[0] diff --git a/coretk/coretk/interface.py b/coretk/coretk/interface.py index 7d0c374c..4b56f3a8 100644 --- a/coretk/coretk/interface.py +++ b/coretk/coretk/interface.py @@ -29,12 +29,22 @@ class InterfaceManager: return str(ip4), str(ip6), self.current.prefixlen def next_subnet(self): - if self.current: - self.cidr = self.cidr.next() - return self.cidr + if self.deleted: + return self.deleted.pop(0) + else: + if self.current: + self.cidr = self.cidr.next() + return self.cidr - def deleted_interface(self, interface): - logging.info("deleted interface: %s", interface) + def deleted_cidr(self, cidr): + logging.info("deleted cidr: %s", cidr) + if cidr not in self.deleted: + self.deleted.append(cidr) + self.deleted.sort() + + @classmethod + def get_cidr(cls, interface): + return IPNetwork(f"{interface.ip4}/{interface.ip4mask}").cidr def determine_subnet(self, canvas_src_node, canvas_dst_node): src_node = canvas_src_node.core_node @@ -49,21 +59,21 @@ class InterfaceManager: self.current = cidr else: self.current = self.next_subnet() - # else: - # self.current = self.cidr elif not is_src_container and is_dst_container: cidr = self.find_subnet(canvas_src_node, visited={dst_node.id}) if cidr: - self.current = self.cidr + self.current = cidr else: self.current = self.next_subnet() else: logging.info("ignoring subnet change for link between network nodes") - def find_subnet(self, canvas_node, visited): + def find_subnet(self, canvas_node, visited=None): logging.info("finding subnet for node: %s", canvas_node.core_node.name) canvas = self.app.canvas cidr = None + if not visited: + visited = set() visited.add(canvas_node.core_node.id) for edge in canvas_node.edges: src_node = canvas.nodes[edge.src] @@ -77,9 +87,10 @@ class InterfaceManager: continue visited.add(check_node.core_node.id) if interface: - logging.info("found interface: %s", interface) - cidr = IPNetwork(f"{interface.ip4}/{interface.ip4mask}").cidr - break + cidr = self.get_cidr(interface) else: cidr = self.find_subnet(check_node, visited) + if cidr: + logging.info("found subnet: %s", cidr) + break return cidr diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index 496bbc6d..165fde53 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -40,11 +40,11 @@ class CanvasComponentManagement: def delete_selected_nodes(self): edges = set() - node_ids = [] + nodes = [] for node_id in list(self.selected): bbox_id = self.selected[node_id] canvas_node = self.canvas.nodes.pop(node_id) - node_ids.append(canvas_node.core_node.id) + nodes.append(canvas_node) self.canvas.delete(node_id) self.canvas.delete(bbox_id) self.canvas.delete(canvas_node.text_id) @@ -63,6 +63,9 @@ class CanvasComponentManagement: other_interface = edge.dst_interface other_node = self.canvas.nodes[other_id] other_node.edges.remove(edge) - other_node.interfaces.remove(other_interface) + try: + other_node.interfaces.remove(other_interface) + except ValueError: + pass self.selected.clear() - return node_ids, edges + return nodes From 27a37c77fc3e0980e0652336ded514128e849f22 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 Nov 2019 09:52:50 -0800 Subject: [PATCH 271/462] made preexisting nodes a local variable, renamed reuseable to delete_nodes for more clarity --- coretk/coretk/coreclient.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index d5a2f854..df83d9c7 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -59,8 +59,7 @@ class CoreClient: # helpers self.interface_to_edge = {} - self.reusable = [] - self.preexisting = set() + self.deleted_nodes = [] self.interfaces_manager = InterfaceManager(self.app) self.created_nodes = set() self.created_links = set() @@ -84,8 +83,7 @@ class CoreClient: # helpers self.created_nodes.clear() self.created_links.clear() - self.reusable.clear() - self.preexisting.clear() + self.deleted_nodes.clear() self.interfaces_manager.reset() self.interface_to_edge.clear() # session data @@ -195,14 +193,15 @@ class CoreClient: # determine next node id and reusable nodes max_id = 1 + existing_nodes = set() for node in session.nodes: if node.id > max_id: max_id = node.id - self.preexisting.add(node.id) + existing_nodes.add(node.id) self.id = max_id for i in range(1, self.id): - if i not in self.preexisting: - self.reusable.append(i) + if i not in existing_nodes: + self.deleted_nodes.append(i) # draw session self.app.canvas.reset_and_redraw(session) @@ -426,12 +425,12 @@ class CoreClient: :rtype: int :return: the next id to be used """ - if len(self.reusable) == 0: + if self.deleted_nodes: + return self.deleted_nodes.pop(0) + else: new_id = self.id self.id = self.id + 1 return new_id - else: - return self.reusable.pop(0) def create_node(self, x, y, node_type, model): """ @@ -484,7 +483,7 @@ class CoreClient: logging.error("unknown node: %s", node_id) continue del self.canvas_nodes[node_id] - self.reusable.append(node_id) + self.deleted_nodes.append(node_id) if node_id in self.mobility_configs: del self.mobility_configs[node_id] if node_id in self.wlan_configs: @@ -526,7 +525,7 @@ class CoreClient: deleted_cidrs = deleted_cidrs - keep_cidrs for cidr in deleted_cidrs: self.interfaces_manager.deleted_cidr(cidr) - self.reusable.sort() + self.deleted_nodes.sort() def create_interface(self, canvas_node): node = canvas_node.core_node From 89dfeae07c9ce0ab4a5b2723fe55fc47b7e17322 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 25 Nov 2019 11:23:04 -0800 Subject: [PATCH 272/462] basic status bar, node deletion also remove antenna --- coretk/coretk/coreclient.py | 2 +- coretk/coretk/graph.py | 20 ++++++++++++++ coretk/coretk/graph_helper.py | 51 +++++++++++++++++++++-------------- coretk/coretk/status.py | 16 +++++------ coretk/coretk/toolbar.py | 31 +++++++++++++-------- 5 files changed, 80 insertions(+), 40 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index b75b66ed..f847d827 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -327,7 +327,6 @@ class CoreClient: emane_config = {x: self.emane_config[x].value for x in self.emane_config} else: emane_config = None - print(links) response = self.client.start_session( self.session_id, nodes, @@ -342,6 +341,7 @@ class CoreClient: file_configs, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) + print(self.servers) # print(self.client.get_session(self.session_id)) def stop_session(self): diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index fc14246d..b084712d 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -393,6 +393,26 @@ class CanvasGraph(tk.Canvas): self.canvas_management.delete_selected_nodes() ) + # delete antennas + for cnid in to_delete_nodes: + canvas_node = self.nodes[cnid] + if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: + canvas_node.antenna_draw.delete_antennas() + else: + for e in canvas_node.edges: + link_proto = self.core.links[e.token] + node_one_id, node_two_id = ( + link_proto.node_one_id, + link_proto.node_two_id, + ) + if node_one_id == canvas_node.core_node.id: + neighbor_id = node_two_id + else: + neighbor_id = node_one_id + neighbor = self.core.canvas_nodes[neighbor_id] + if neighbor.core_node.type != core_pb2.NodeType.WIRELESS_LAN: + neighbor.antenna_draw.delete_antenna() + # delete nodes and link info stored in CanvasGraph object node_ids = [] for nid in to_delete_nodes: diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py index e691667b..1ad0c8f6 100644 --- a/coretk/coretk/graph_helper.py +++ b/coretk/coretk/graph_helper.py @@ -88,18 +88,41 @@ class WlanAntennaManager: """ if self.quantity < 5: x, y = self.canvas.coords(self.node_id) - self.antennas.append( - self.canvas.create_image( - x - 16 + self.offset, - y - 16, - anchor=tk.CENTER, - image=self.image, - tags="antenna", - ) + aid = self.canvas.create_image( + x - 16 + self.offset, + y - 23, + anchor=tk.CENTER, + image=self.image, + tags="antenna", ) + # self.canvas.tag_raise("antenna") + self.antennas.append(aid) self.quantity = self.quantity + 1 self.offset = self.offset + 8 + def delete_antenna(self): + """ + delete one antenna + + :return: nothing + """ + if len(self.antennas) > 0: + self.canvas.delete(self.antennas.pop()) + self.quantity -= 1 + self.offset -= 8 + + def delete_antennas(self): + """ + delete all antennas + + :return: nothing + """ + for aid in self.antennas: + self.canvas.delete(aid) + self.antennas.clear() + self.quantity = 0 + self.offset = 0 + def update_antennas_position(self, offset_x, offset_y): """ redraw antennas of a node according to the new node position @@ -108,15 +131,3 @@ class WlanAntennaManager: """ for i in self.antennas: self.canvas.move(i, offset_x, offset_y) - - def delete_antenna(self, canvas_id): - return - - def delete_antennas(self): - """ - Delete all the antennas of a node - - :return: nothing - """ - for i in self.antennas: - self.canvas.delete(i) diff --git a/coretk/coretk/status.py b/coretk/coretk/status.py index 2a160457..69869f51 100644 --- a/coretk/coretk/status.py +++ b/coretk/coretk/status.py @@ -1,5 +1,6 @@ "status bar" import time +import tkinter as tk from tkinter import ttk @@ -9,6 +10,7 @@ class StatusBar(ttk.Frame): self.app = app self.status = None + self.statusvar = tk.StringVar() self.zoom = None self.cpu_usage = None self.memory = None @@ -21,7 +23,8 @@ class StatusBar(ttk.Frame): self.columnconfigure(1, weight=1) self.columnconfigure(2, weight=1) self.columnconfigure(3, weight=1) - self.status = ttk.Label(self, text="status") + self.status = ttk.Label(self, textvariable=self.statusvar) + self.statusvar.set("status") self.status.grid(row=0, column=0) self.zoom = ttk.Label(self, text="zoom") self.zoom.grid(row=0, column=1) @@ -31,14 +34,11 @@ class StatusBar(ttk.Frame): self.emulation_light.grid(row=0, column=3) def processing(self): - self.running = True texts = ["Processing.", "Processing..", "Processing...", "Processing...."] i = 0 - while self.running is True: - self.status.config(text=texts[i % 4]) - self.app.master.update() + while self.running: + self.statusvar.set(texts[i % 4]) + self.master.update() i = i + 1 - time.sleep(0.3) - print("running") + time.sleep(0.002) print("thread finish") - # self.status.config(text="status") diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 648a2d8c..1aae22c8 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -1,5 +1,5 @@ import logging -import threading +import time import tkinter as tk from functools import partial from tkinter import ttk @@ -196,22 +196,25 @@ class Toolbar(ttk.Frame): :return: nothing """ - logging.debug("clicked start button") + self.app.statusbar.running = True + # thread = threading.Thread(target=self.app.statusbar.processing) + # thread.start() self.master.config(cursor="watch") - status_thread = threading.Thread(target=self.app.statusbar.processing) - status_thread.start() self.master.update() self.app.canvas.mode = GraphMode.SELECT + start = time.time() self.app.core.start_session() + dur = time.time() - start self.runtime_frame.tkraise() self.master.config(cursor="") - - self.app.statusbar.running = False - if status_thread.is_alive(): - print("still running") - status_thread.join() - print("thread terminate") - self.app.statusbar.status.config(text="status") + nodes_num = len(self.app.core.canvas_nodes) + links_num = len(self.app.core.links) + self.app.statusbar.statusvar.set( + "Network topology instantiated in %s seconds (%s node(s) and %s link(s))" + % ("%.3f" % dur, nodes_num, links_num) + ) + # self.app.statusbar.running = False + # print("done") def click_link(self): logging.debug("Click LINK button") @@ -365,7 +368,13 @@ class Toolbar(ttk.Frame): :return: nothing """ logging.debug("Click on STOP button ") + # self.status_thread.join() + start = time.time() self.app.core.stop_session() + dur = time.time() - start + self.app.statusbar.statusvar.set( + "Cleanup completed in %s seconds" % "%.3f" % dur + ) self.app.canvas.delete("wireless") self.design_frame.tkraise() From 1290dae4f882fd99e021c5306510551985267ffc Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 Nov 2019 11:31:40 -0800 Subject: [PATCH 273/462] simplified logic for picking next subnet, avoiding trying to optimize too early --- coretk/coretk/coreclient.py | 27 +------------ coretk/coretk/interface.py | 75 +++++++++++++++++++------------------ 2 files changed, 39 insertions(+), 63 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index df83d9c7..8c370a91 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -45,7 +45,6 @@ class CoreClient: self.node_ids = [] self.app = app self.master = app.master - self.interface_helper = None self.services = {} self.default_services = {} self.emane_models = [] @@ -493,8 +492,6 @@ class CoreClient: if node_id == node_id: del self.emane_model_configs[key] - deleted_cidrs = set() - keep_cidrs = set() for edge in canvas_node.edges: if edge in edges: continue @@ -502,29 +499,7 @@ class CoreClient: if edge.token not in self.links: logging.error("unknown edge: %s", edge.token) del self.links[edge.token] - other_id = edge.src - other_interface = edge.src_interface - interface = edge.dst_interface - if canvas_node.id == edge.src: - other_id = edge.dst - other_interface = edge.dst_interface - interface = edge.src_interface - other_node = self.app.canvas.nodes.get(other_id) - if not other_node: - continue - if other_interface: - cidr = self.interfaces_manager.get_cidr(other_interface) - deleted_cidrs.add(cidr) - else: - cidr = self.interfaces_manager.find_subnet(other_node) - if cidr: - keep_cidrs.add(cidr) - else: - cidr = self.interfaces_manager.get_cidr(interface) - deleted_cidrs.add(cidr) - deleted_cidrs = deleted_cidrs - keep_cidrs - for cidr in deleted_cidrs: - self.interfaces_manager.deleted_cidr(cidr) + self.deleted_nodes.sort() def create_interface(self, canvas_node): diff --git a/coretk/coretk/interface.py b/coretk/coretk/interface.py index 4b56f3a8..f4a380ae 100644 --- a/coretk/coretk/interface.py +++ b/coretk/coretk/interface.py @@ -11,39 +11,40 @@ def random_mac(): class InterfaceManager: - def __init__(self, app, cidr="10.0.0.0/24"): + def __init__(self, app, address="10.0.0.0", mask=24): self.app = app - self.default = cidr - self.cidr = IPNetwork(cidr) - self.deleted = [] - self.current = None - - def reset(self): - self.cidr = IPNetwork(self.default) - self.deleted.clear() - self.current = None - - def get_ips(self, node_id): - ip4 = self.current[node_id] - ip6 = ip4.ipv6() - return str(ip4), str(ip6), self.current.prefixlen + self.mask = mask + self.base_prefix = max(self.mask - 8, 0) + self.subnets = IPNetwork(f"{address}/{self.base_prefix}") + self.current_subnet = None def next_subnet(self): - if self.deleted: - return self.deleted.pop(0) - else: - if self.current: - self.cidr = self.cidr.next() - return self.cidr + # define currently used subnets + used_subnets = set() + for link in self.app.core.links.values(): + if link.HasField("interface_one"): + subnet = self.get_subnet(link.interface_one) + used_subnets.add(subnet) + if link.HasField("interface_two"): + subnet = self.get_subnet(link.interface_two) + used_subnets.add(subnet) - def deleted_cidr(self, cidr): - logging.info("deleted cidr: %s", cidr) - if cidr not in self.deleted: - self.deleted.append(cidr) - self.deleted.sort() + # find next available subnet + for i in self.subnets.subnet(self.mask): + if i not in used_subnets: + return i + + def reset(self): + self.current_subnet = None + + def get_ips(self, node_id): + ip4 = self.current_subnet[node_id] + ip6 = ip4.ipv6() + prefix = self.current_subnet.prefixlen + return str(ip4), str(ip6), prefix @classmethod - def get_cidr(cls, interface): + def get_subnet(cls, interface): return IPNetwork(f"{interface.ip4}/{interface.ip4mask}").cidr def determine_subnet(self, canvas_src_node, canvas_dst_node): @@ -52,19 +53,19 @@ class InterfaceManager: is_src_container = NodeUtils.is_container_node(src_node.type) is_dst_container = NodeUtils.is_container_node(dst_node.type) if is_src_container and is_dst_container: - self.current = self.next_subnet() + self.current_subnet = self.next_subnet() elif is_src_container and not is_dst_container: - cidr = self.find_subnet(canvas_dst_node, visited={src_node.id}) - if cidr: - self.current = cidr + subnet = self.find_subnet(canvas_dst_node, visited={src_node.id}) + if subnet: + self.current_subnet = subnet else: - self.current = self.next_subnet() + self.current_subnet = self.next_subnet() elif not is_src_container and is_dst_container: - cidr = self.find_subnet(canvas_src_node, visited={dst_node.id}) - if cidr: - self.current = cidr + subnet = self.find_subnet(canvas_src_node, visited={dst_node.id}) + if subnet: + self.current_subnet = subnet else: - self.current = self.next_subnet() + self.current_subnet = self.next_subnet() else: logging.info("ignoring subnet change for link between network nodes") @@ -87,7 +88,7 @@ class InterfaceManager: continue visited.add(check_node.core_node.id) if interface: - cidr = self.get_cidr(interface) + cidr = self.get_subnet(interface) else: cidr = self.find_subnet(check_node, visited) if cidr: From 3d2e372663a0dd799259c28820e8c299fd62eca4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 Nov 2019 12:59:32 -0800 Subject: [PATCH 274/462] simplified next node id logic to avoid trying to over optimize --- coretk/coretk/coreclient.py | 39 ++++++++++--------------------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 8c370a91..c4e58795 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -58,13 +58,11 @@ class CoreClient: # helpers self.interface_to_edge = {} - self.deleted_nodes = [] self.interfaces_manager = InterfaceManager(self.app) self.created_nodes = set() self.created_links = set() # session data - self.id = 1 self.state = None self.canvas_nodes = {} self.location = None @@ -78,11 +76,9 @@ class CoreClient: self.file_configs = {} def reset(self): - self.id = 1 # helpers self.created_nodes.clear() self.created_links.clear() - self.deleted_nodes.clear() self.interfaces_manager.reset() self.interface_to_edge.clear() # session data @@ -190,18 +186,6 @@ class CoreClient: # get emane model config - # determine next node id and reusable nodes - max_id = 1 - existing_nodes = set() - for node in session.nodes: - if node.id > max_id: - max_id = node.id - existing_nodes.add(node.id) - self.id = max_id - for i in range(1, self.id): - if i not in existing_nodes: - self.deleted_nodes.append(i) - # draw session self.app.canvas.reset_and_redraw(session) @@ -417,19 +401,19 @@ class CoreClient: logging.debug("Close grpc") self.client.close() - def get_id(self): + def next_node_id(self): """ - Get the next node id as well as update id status and reusable ids + Get the next usable node id. - :rtype: int :return: the next id to be used + :rtype: int """ - if self.deleted_nodes: - return self.deleted_nodes.pop(0) - else: - new_id = self.id - self.id = self.id + 1 - return new_id + i = 1 + while True: + if i not in self.canvas_nodes: + break + i += 1 + return i def create_node(self, x, y, node_type, model): """ @@ -441,7 +425,7 @@ class CoreClient: :param str model: node model :return: nothing """ - node_id = self.get_id() + node_id = self.next_node_id() position = core_pb2.Position(x=x, y=y) image = None if NodeUtils.is_image_node(node_type): @@ -482,7 +466,6 @@ class CoreClient: logging.error("unknown node: %s", node_id) continue del self.canvas_nodes[node_id] - self.deleted_nodes.append(node_id) if node_id in self.mobility_configs: del self.mobility_configs[node_id] if node_id in self.wlan_configs: @@ -500,8 +483,6 @@ class CoreClient: logging.error("unknown edge: %s", edge.token) del self.links[edge.token] - self.deleted_nodes.sort() - def create_interface(self, canvas_node): node = canvas_node.core_node ip4, ip6, prefix = self.interfaces_manager.get_ips(node.id) From e3fc318a1b6983574cc30f3686cd4a55df38ce5e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 Nov 2019 14:52:00 -0800 Subject: [PATCH 275/462] linkinfo cleanup, changed text distance to be proprotionate of line distance --- coretk/coretk/graph.py | 50 ++-------------- coretk/coretk/linkinfo.py | 119 ++++++++++++++------------------------ 2 files changed, 48 insertions(+), 121 deletions(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 14e978be..ca4b0001 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -200,31 +200,11 @@ class CanvasGraph(tk.Canvas): self.edges[edge.token] = edge self.core.links[edge.token] = link self.helper.redraw_antenna(canvas_node_one, canvas_node_two) - - # TODO add back the link info to grpc manager also redraw - # TODO will include throughput and ipv6 in the future - interface_one = link.interface_one - interface_two = link.interface_two - ip4_src = None - ip4_dst = None - ip6_src = None - ip6_dst = None - if interface_one is not None: - ip4_src = interface_one.ip4 - ip6_src = interface_one.ip6 - if interface_two is not None: - ip4_dst = interface_two.ip4 - ip6_dst = interface_two.ip6 - edge.link_info = LinkInfo( - canvas=self, - edge=edge, - ip4_src=ip4_src, - ip6_src=ip6_src, - ip4_dst=ip4_dst, - ip6_dst=ip6_dst, - ) - canvas_node_one.interfaces.append(interface_one) - canvas_node_two.interfaces.append(interface_two) + edge.link_info = LinkInfo(self, edge, link) + if link.HasField("interface_one"): + canvas_node_one.interfaces.append(link.interface_one) + if link.HasField("interface_two"): + canvas_node_two.interfaces.append(link.interface_two) # raise the nodes so they on top of the links self.tag_raise("node") @@ -320,25 +300,7 @@ class CanvasGraph(tk.Canvas): node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) link = self.core.create_link(edge, node_src, node_dst) - - # draw link info on the edge - ip4_and_prefix_1 = None - ip4_and_prefix_2 = None - if link.HasField("interface_one"): - if1 = link.interface_one - ip4_and_prefix_1 = f"{if1.ip4}/{if1.ip4mask}" - if link.HasField("interface_two"): - if2 = link.interface_two - ip4_and_prefix_2 = f"{if2.ip4}/{if2.ip4mask}" - edge.link_info = LinkInfo( - self, - edge, - ip4_src=ip4_and_prefix_1, - ip6_src=None, - ip4_dst=ip4_and_prefix_2, - ip6_dst=None, - ) - + edge.link_info = LinkInfo(self, edge, link) logging.debug(f"edges: {self.find_withtag('edge')}") def click_press(self, event): diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index 00df0913..2b7ecc7e 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -3,76 +3,60 @@ Link information, such as IPv4, IPv6 and throughput drawn in the canvas """ import logging import math +import tkinter as tk + +TEXT_DISTANCE = 0.25 class LinkInfo: - def __init__(self, canvas, edge, ip4_src, ip6_src, ip4_dst, ip6_dst): + def __init__(self, canvas, edge, link): """ create an instance of LinkInfo object :param coretk.graph.Graph canvas: canvas object :param coretk.graph.CanvasEdge edge: canvas edge onject - :param ip4_src: - :param ip6_src: - :param ip4_dst: - :param ip6_dst: + :param link: core link to draw info for """ self.canvas = canvas self.edge = edge - self.radius = 37 - self.core = self.canvas.core + self.link = link + self.id1 = None + self.id2 = None + self.draw_labels() - self.ip4_address_1 = ip4_src - self.ip6_address_1 = ip6_src - self.ip4_address_2 = ip4_dst - self.ip6_address_2 = ip6_dst - self.id1 = self.create_edge_src_info() - self.id2 = self.create_edge_dst_info() - - def slope_src_dst(self): - """ - calculate slope of the line connecting source node to destination node - :rtype: float - :return: slope of line - """ + def get_coordinates(self): x1, y1, x2, y2 = self.canvas.coords(self.edge.id) - if x2 - x1 == 0: - return 9999.0 - else: - return (y2 - y1) / (x2 - x1) + v1 = x2 - x1 + v2 = y2 - y1 + d = math.sqrt(v1 ** 2 + v2 ** 2) + ux = TEXT_DISTANCE * v1 + uy = TEXT_DISTANCE * v2 + x1 = x1 + ux + y1 = y1 + uy + x2 = x2 - ux + y2 = y2 - uy + logging.info("line distance: %s", d) + return x1, y1, x2, y2 - def create_edge_src_info(self): - """ - draw the ip address for source node - - :return: nothing - """ - x1, y1, x2, _ = self.canvas.coords(self.edge.id) - m = self.slope_src_dst() - distance = math.cos(math.atan(m)) * self.radius - if x1 > x2: - distance = -distance - # id1 = self.canvas.create_text(x1, y1, text=self.ip4_address_1) - id1 = self.canvas.create_text( - x1 + distance, y1 + distance * m, text=self.ip4_address_1, tags="linkinfo" + def draw_labels(self): + x1, y1, x2, y2 = self.get_coordinates() + 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_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" + ) + self.id1 = self.canvas.create_text( + x1, y1, text=label_one, justify=tk.CENTER, tags="linkinfo" ) - return id1 - - def create_edge_dst_info(self): - """ - draw the ip address for destination node - - :return: nothing - """ - x1, _, x2, y2 = self.canvas.coords(self.edge.id) - m = self.slope_src_dst() - distance = math.cos(math.atan(m)) * self.radius - if x1 > x2: - distance = -distance - # id2 = self.canvas.create_text(x2, y2, text=self.ip4_address_2) - id2 = self.canvas.create_text( - x2 - distance, y2 - distance * m, text=self.ip4_address_2, tags="linkinfo" + self.id2 = self.canvas.create_text( + x2, y2, text=label_two, justify=tk.CENTER, tags="linkinfo" ) - return id2 def recalculate_info(self): """ @@ -80,32 +64,13 @@ class LinkInfo: :return: nothing """ - x1, y1, x2, y2 = self.canvas.coords(self.edge.id) - m = self.slope_src_dst() - distance = math.cos(math.atan(m)) * self.radius - if x1 > x2: - distance = -distance - new_x1 = x1 + distance - new_y1 = y1 + distance * m - new_x2 = x2 - distance - new_y2 = y2 - distance * m - self.canvas.coords(self.id1, new_x1, new_y1) - self.canvas.coords(self.id2, new_x2, new_y2) - - # def link_througput(self): - # x1, y1, x2, y2 = self.canvas.coords(self.edge.id) - # x = (x1 + x2) / 2 - # y = (y1 + y2) / 2 - # tid = self.canvas.create_text(x, y, text="place text here") - # return tid + x1, y1, x2, y2 = self.get_coordinates() + self.canvas.coords(self.id1, x1, y1) + self.canvas.coords(self.id2, x2, y2) class Throughput: def __init__(self, canvas, core): - """ - create an instance of Throughput object - :param coretk.app.Application app: application - """ self.canvas = canvas self.core = core # edge canvas id mapped to throughput value From 731b586df041d4e6723ea0d5d9f5950d52e44959 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 Nov 2019 15:05:07 -0800 Subject: [PATCH 276/462] fixed issue with clearing out session location for created sessions --- coretk/coretk/coreclient.py | 1 - coretk/coretk/linkinfo.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index c4e58795..794401b6 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -83,7 +83,6 @@ class CoreClient: self.interface_to_edge.clear() # session data self.canvas_nodes.clear() - self.location = None self.links.clear() self.hooks.clear() self.wlan_configs.clear() diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index 2b7ecc7e..9004afe8 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -5,7 +5,7 @@ import logging import math import tkinter as tk -TEXT_DISTANCE = 0.25 +TEXT_DISTANCE = 0.33 class LinkInfo: From cdc48a765a1e8e9fb171b9090e88e7fcf6ee46c3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 Nov 2019 15:40:09 -0800 Subject: [PATCH 277/462] ignoring control networks on join, fixed emane links on join being wireless, fixed setting emane model being set when configuring emane --- coretk/coretk/coreclient.py | 13 ++++++++++++- coretk/coretk/dialogs/emaneconfig.py | 5 +---- coretk/coretk/graph.py | 2 +- coretk/coretk/nodeutils.py | 5 +++++ 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 794401b6..c88647c3 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -184,6 +184,15 @@ class CoreClient: self.emane_config = response.config # get emane model config + response = self.client.get_emane_model_configs(self.session_id) + for _id in response.configs: + config = response.configs[_id] + interface = None + node_id = _id + if _id >= 1000: + interface = _id % 1000 + node_id = int(_id / 1000) + self.set_emane_model_config(node_id, config.model, config.config, interface) # draw session self.app.canvas.reset_and_redraw(session) @@ -249,7 +258,6 @@ class CoreClient: dialog.show() response = self.client.get_service_defaults(self.session_id) - logging.debug("get service defaults: %s", response) self.default_services = { x.node_type: set(x.services) for x in response.defaults } @@ -609,6 +617,7 @@ class CoreClient: return config def get_emane_model_config(self, node_id, model, interface=None): + logging.info("getting emane model config: %s %s %s", node_id, model, interface) config = self.emane_model_configs.get((node_id, model, interface)) if not config: if interface is None: @@ -620,4 +629,6 @@ class CoreClient: return config def set_emane_model_config(self, node_id, model, config, interface=None): + logging.info("setting emane model config: %s %s %s", node_id, model, interface) + logging.info("model config: %s", config) self.emane_model_configs[(node_id, model, interface)] = config diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index bb6d8f1b..71ffe078 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -95,10 +95,7 @@ class EmaneConfigDialog(Dialog): self.radiovar = tk.IntVar() self.radiovar.set(1) self.emane_models = [x.split("_")[1] for x in self.app.core.emane_models] - emane_model = None - if self.emane_models: - emane_model = self.emane_models[0] - self.emane_model = tk.StringVar(value=emane_model) + self.emane_model = tk.StringVar(value=self.node.emane.split("_")[1]) self.emane_model_button = None self.draw() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index df660c7f..7d46ada8 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -163,7 +163,7 @@ class CanvasGraph(tk.Canvas): # draw existing nodes for core_node in session.nodes: # peer to peer node is not drawn on the GUI - if core_node.type == core_pb2.NodeType.PEER_TO_PEER: + if NodeUtils.is_ignore_node(core_node.type): continue # draw nodes on the canvas diff --git a/coretk/coretk/nodeutils.py b/coretk/coretk/nodeutils.py index 18b468b5..6d880126 100644 --- a/coretk/coretk/nodeutils.py +++ b/coretk/coretk/nodeutils.py @@ -47,8 +47,13 @@ class NodeUtils: CONTAINER_NODES = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} IMAGE_NODES = {NodeType.DOCKER, NodeType.LXC} WIRELESS_NODES = {NodeType.WIRELESS_LAN, NodeType.EMANE} + IGNORE_NODES = {NodeType.CONTROL_NET, NodeType.PEER_TO_PEER} NODE_MODELS = {"router", "host", "PC", "mdr", "prouter"} + @classmethod + def is_ignore_node(cls, node_type): + return node_type in cls.IGNORE_NODES + @classmethod def is_container_node(cls, node_type): return node_type in cls.CONTAINER_NODES From ad4ee58ddd0385023af37ae3ef7d782d73d27aca Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 Nov 2019 15:46:32 -0800 Subject: [PATCH 278/462] properly adding change for emane node links being wireless, fixed issue when sending emane model configs when there is no interface set --- coretk/coretk/coreclient.py | 3 ++- daemon/core/emane/nodes.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index c88647c3..71d6da1b 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -567,6 +567,8 @@ class CoreClient: for key, config in self.emane_model_configs.items(): node_id, model, interface = key config = {x: config[x].value for x in config} + if interface is None: + interface = -1 config_proto = core_pb2.EmaneModelConfig( node_id=node_id, interface_id=interface, model=model, config=config ) @@ -630,5 +632,4 @@ class CoreClient: def set_emane_model_config(self, node_id, model, config, interface=None): logging.info("setting emane model config: %s %s %s", node_id, model, interface) - logging.info("model config: %s", config) self.emane_model_configs[(node_id, model, interface)] = config diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index c7817b41..bd76ed81 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -25,7 +25,7 @@ class EmaneNet(CoreNetworkBase): """ apitype = NodeTypes.EMANE.value - linktype = LinkTypes.WIRELESS.value + linktype = LinkTypes.WIRED.value type = "wlan" is_emane = True From c1a8fada7a45c0734de3fc302ce3ec2c129bd5a6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 Nov 2019 15:48:41 -0800 Subject: [PATCH 279/462] avoid querying state for node movement, check currently known state instead --- coretk/coretk/graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 7d46ada8..ef36e2f9 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -636,7 +636,7 @@ class CanvasNode: new_x, new_y = self.canvas.coords(self.id) - if self.canvas.core.get_session_state() == core_pb2.SessionState.RUNTIME: + if self.canvas.core.is_runtime(): self.canvas.core.edit_node(self.core_node.id, int(new_x), int(new_y)) for edge in self.edges: From 4238c14362a89a5d407a151ad3c4b45115b2c0e2 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 25 Nov 2019 16:50:44 -0800 Subject: [PATCH 280/462] progress bar for start session --- coretk/coretk/coreclient.py | 5 ++++ coretk/coretk/dialogs/nodeconfig.py | 5 ++-- coretk/coretk/status.py | 37 ++++++++++++++++------------- coretk/coretk/toolbar.py | 23 ++++-------------- 4 files changed, 33 insertions(+), 37 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index c4e58795..7b547fd8 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -3,6 +3,7 @@ Incorporate grpc into python tkinter GUI """ import logging import os +import time from core.api.grpc import client, core_pb2 from coretk.dialogs.sessions import SessionsDialog @@ -280,6 +281,8 @@ class CoreClient: emane_config = {x: self.emane_config[x].value for x in self.emane_config} else: emane_config = None + + start = time.time() response = self.client.start_session( self.session_id, nodes, @@ -293,7 +296,9 @@ class CoreClient: service_configs, file_configs, ) + process_time = time.time() - start logging.debug("start session(%s), result: %s", self.session_id, response.result) + self.app.statusbar.start_session_callback(process_time) def stop_session(self, session_id=None): if not session_id: diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 8b9fcfab..f296e4cf 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -197,8 +197,9 @@ class NodeConfigDialog(Dialog): self.node.name = self.name.get() if NodeUtils.is_image_node(self.node.type): self.node.image = self.container_image.get() - if NodeUtils.is_container_node(self.node.type): - self.node.server = self.server.get() + server = self.server.get() + if NodeUtils.is_container_node(self.node.type) and server != "localhost": + self.node.server = server # update canvas node self.canvas_node.image = self.image diff --git a/coretk/coretk/status.py b/coretk/coretk/status.py index 69869f51..eb285760 100644 --- a/coretk/coretk/status.py +++ b/coretk/coretk/status.py @@ -1,5 +1,4 @@ "status bar" -import time import tkinter as tk from tkinter import ttk @@ -11,6 +10,7 @@ class StatusBar(ttk.Frame): self.status = None self.statusvar = tk.StringVar() + self.progress_bar = None self.zoom = None self.cpu_usage = None self.memory = None @@ -19,26 +19,31 @@ class StatusBar(ttk.Frame): self.draw() def draw(self): - self.columnconfigure(0, weight=8) - self.columnconfigure(1, weight=1) + self.columnconfigure(0, weight=1) + self.columnconfigure(1, weight=7) self.columnconfigure(2, weight=1) self.columnconfigure(3, weight=1) + self.columnconfigure(4, weight=1) + + self.progress_bar = ttk.Progressbar( + self, orient="horizontal", mode="indeterminate" + ) + self.progress_bar.grid(row=0, column=0, sticky="nsew") self.status = ttk.Label(self, textvariable=self.statusvar) self.statusvar.set("status") - self.status.grid(row=0, column=0) + self.status.grid(row=0, column=1, sticky="nsew") self.zoom = ttk.Label(self, text="zoom") - self.zoom.grid(row=0, column=1) + self.zoom.grid(row=0, column=2) self.cpu_usage = ttk.Label(self, text="cpu usage") - self.cpu_usage.grid(row=0, column=2) + self.cpu_usage.grid(row=0, column=3) self.emulation_light = ttk.Label(self, text="emulation light") - self.emulation_light.grid(row=0, column=3) + self.emulation_light.grid(row=0, column=4) - def processing(self): - texts = ["Processing.", "Processing..", "Processing...", "Processing...."] - i = 0 - while self.running: - self.statusvar.set(texts[i % 4]) - self.master.update() - i = i + 1 - time.sleep(0.002) - print("thread finish") + def start_session_callback(self, process_time): + num_nodes = len(self.app.core.canvas_nodes) + num_links = len(self.app.core.links) + self.progress_bar.stop() + self.statusvar.set( + "Network topology instantiated in %s seconds (%s node(s) and %s link(s))" + % ("%.3f" % process_time, num_nodes, num_links) + ) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 1aae22c8..9bda6eda 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -1,4 +1,5 @@ import logging +import threading import time import tkinter as tk from functools import partial @@ -196,25 +197,11 @@ class Toolbar(ttk.Frame): :return: nothing """ - self.app.statusbar.running = True - # thread = threading.Thread(target=self.app.statusbar.processing) - # thread.start() - self.master.config(cursor="watch") - self.master.update() + self.app.statusbar.progress_bar.start(5) self.app.canvas.mode = GraphMode.SELECT - start = time.time() - self.app.core.start_session() - dur = time.time() - start + thread = threading.Thread(target=self.app.core.start_session) + thread.start() self.runtime_frame.tkraise() - self.master.config(cursor="") - nodes_num = len(self.app.core.canvas_nodes) - links_num = len(self.app.core.links) - self.app.statusbar.statusvar.set( - "Network topology instantiated in %s seconds (%s node(s) and %s link(s))" - % ("%.3f" % dur, nodes_num, links_num) - ) - # self.app.statusbar.running = False - # print("done") def click_link(self): logging.debug("Click LINK button") @@ -367,8 +354,6 @@ class Toolbar(ttk.Frame): :return: nothing """ - logging.debug("Click on STOP button ") - # self.status_thread.join() start = time.time() self.app.core.stop_session() dur = time.time() - start From fa7e5e321ba04c6ae401434233137df3a91ae460 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 26 Nov 2019 11:30:25 -0800 Subject: [PATCH 281/462] save service config, file config when join session, update progress bar to start, stop, join session, delete antennas, wirelesslink as well as other stuff that we had before when join session --- coretk/coretk/coreclient.py | 35 +++++++++++++++++++++++++++---- coretk/coretk/dialogs/sessions.py | 7 ++++++- coretk/coretk/graph_helper.py | 11 +++++++++- coretk/coretk/menuaction.py | 35 ++++++++++++++++++++++++------- coretk/coretk/nodedelete.py | 5 ++++- coretk/coretk/status.py | 4 ++++ coretk/coretk/toolbar.py | 10 +++------ 7 files changed, 86 insertions(+), 21 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index e6531817..3feb334a 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -137,8 +137,8 @@ class CoreClient: ) def join_session(self, session_id, query_location=True): - self.master.config(cursor="watch") - self.master.update() + # self.master.config(cursor="watch") + # self.master.update() # update session and title self.session_id = session_id @@ -198,12 +198,36 @@ class CoreClient: # draw session self.app.canvas.reset_and_redraw(session) - # draw tool bar appropritate with session state + # get node service config and file config + for node in session.nodes: + self.created_nodes.add(node.id) + for link in session.links: + self.created_links.add(tuple(sorted([link.node_one_id, link.node_two_id]))) + for node in session.nodes: + if node.type == core_pb2.NodeType.DEFAULT: + for service in node.services: + response = self.client.get_node_service( + self.session_id, node.id, service + ) + if node.id not in self.service_configs: + self.service_configs[node.id] = {} + self.service_configs[node.id][service] = response.service + for file in response.service.configs: + response = self.client.get_node_service_file( + self.session_id, node.id, service, file + ) + if node.id not in self.file_configs: + self.file_configs[node.id] = {} + if service not in self.file_configs[node.id]: + self.file_configs[node.id][service] = {} + self.file_configs[node.id][service][file] = response.data + if self.is_runtime(): self.app.toolbar.runtime_frame.tkraise() else: self.app.toolbar.design_frame.tkraise() - self.master.config(cursor="") + # self.master.config(cursor="") + self.app.statusbar.progress_bar.stop() def is_runtime(self): return self.state == core_pb2.SessionState.RUNTIME @@ -310,7 +334,10 @@ class CoreClient: def stop_session(self, session_id=None): if not session_id: session_id = self.session_id + start = time.time() response = self.client.stop_session(session_id) + process_time = time.time() - start + self.app.statusbar.stop_session_callback(process_time) logging.debug("stopped session(%s), result: %s", session_id, response.result) def launch_terminal(self, node_id): diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index 6f299dfc..7a07c059 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -1,4 +1,5 @@ import logging +import threading import tkinter as tk from tkinter import ttk @@ -146,7 +147,11 @@ class SessionsDialog(Dialog): logging.error("querysessiondrawing.py invalid state") def join_session(self, session_id): - self.app.core.join_session(session_id) + self.app.statusbar.progress_bar.start(5) + thread = threading.Thread( + target=self.app.core.join_session, args=([session_id]) + ) + thread.start() self.destroy() def on_selected(self, event): diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py index 1ad0c8f6..eea4ff55 100644 --- a/coretk/coretk/graph_helper.py +++ b/coretk/coretk/graph_helper.py @@ -7,7 +7,16 @@ import tkinter as tk from coretk.images import ImageEnum, Images from coretk.nodeutils import NodeUtils -CANVAS_COMPONENT_TAGS = ["edge", "node", "nodename", "wallpaper", "linkinfo"] +CANVAS_COMPONENT_TAGS = [ + "edge", + "node", + "nodename", + "wallpaper", + "linkinfo", + "antenna", + "wireless", + "selectednodes", +] class GraphHelper: diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index b47c6ef2..066e37e2 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -3,6 +3,8 @@ The actions taken when each menubar option is clicked """ import logging +import threading +import time import webbrowser from tkinter import filedialog, messagebox @@ -27,7 +29,16 @@ class MenuAction: self.master = master self.app = app - def prompt_save_running_session(self): + def cleanup_old_session(self, quitapp=False): + start = time.time() + self.app.core.stop_session() + self.app.core.delete_session() + process_time = time.time() - start + self.app.statusbar.stop_session_callback(process_time) + if quitapp: + self.app.quit() + + def prompt_save_running_session(self, quitapp=False): """ Prompt use to stop running session before application is closed @@ -43,13 +54,20 @@ class MenuAction: or state == core_pb2.SessionState.DEFINITION ): self.app.core.delete_session() + if quitapp: + self.app.quit() else: msgbox = messagebox.askyesnocancel("stop", "Stop the running session?") - if msgbox or msgbox is False: if msgbox: - self.app.core.stop_session() - self.app.core.delete_session() + self.app.statusbar.progress_bar.start(5) + thread = threading.Thread( + target=self.cleanup_old_session, args=([quitapp]) + ) + thread.start() + + # self.app.core.stop_session() + # self.app.core.delete_session() def on_quit(self, event=None): """ @@ -57,8 +75,8 @@ class MenuAction: :return: nothing """ - self.prompt_save_running_session() - self.app.quit() + self.prompt_save_running_session(quitapp=True) + # self.app.quit() def file_save_as_xml(self, event=None): logging.info("menuaction.py file_save_as_xml()") @@ -81,7 +99,10 @@ class MenuAction: if file_path: logging.info("opening xml: %s", file_path) self.prompt_save_running_session() - self.app.core.open_xml(file_path) + self.app.statusbar.progress_bar.start(5) + thread = threading.Thread(target=self.app.core.open_xml, args=([file_path])) + thread.start() + # self.app.core.open_xml(file_path) def gui_preferences(self): dialog = PreferencesDialog(self.app, self.app) diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index 27e330b5..4ec986aa 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -27,7 +27,10 @@ class CanvasComponentManagement: if canvas_node.id not in self.selected: x0, y0, x1, y1 = self.canvas.bbox(canvas_node.id) bbox_id = self.canvas.create_rectangle( - (x0 - 6, y0 - 6, x1 + 6, y1 + 6), activedash=True, dash="-" + (x0 - 6, y0 - 6, x1 + 6, y1 + 6), + activedash=True, + dash="-", + tags="selectednodes", ) self.selected[canvas_node.id] = bbox_id diff --git a/coretk/coretk/status.py b/coretk/coretk/status.py index eb285760..ea31d6fc 100644 --- a/coretk/coretk/status.py +++ b/coretk/coretk/status.py @@ -47,3 +47,7 @@ class StatusBar(ttk.Frame): "Network topology instantiated in %s seconds (%s node(s) and %s link(s))" % ("%.3f" % process_time, num_nodes, num_links) ) + + def stop_session_callback(self, cleanup_time): + self.progress_bar.stop() + self.statusvar.set("Cleanup completed in %s seconds" % "%.3f" % cleanup_time) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 9bda6eda..40fc36c0 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -1,6 +1,5 @@ import logging import threading -import time import tkinter as tk from functools import partial from tkinter import ttk @@ -354,12 +353,9 @@ class Toolbar(ttk.Frame): :return: nothing """ - start = time.time() - self.app.core.stop_session() - dur = time.time() - start - self.app.statusbar.statusvar.set( - "Cleanup completed in %s seconds" % "%.3f" % dur - ) + self.app.statusbar.progress_bar.start(5) + thread = threading.Thread(target=self.app.core.stop_session) + thread.start() self.app.canvas.delete("wireless") self.design_frame.tkraise() From bc026bb69369c2dbb7ae733f44744fffeb99b251 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 26 Nov 2019 11:32:48 -0800 Subject: [PATCH 282/462] initial work for mobility player dialog, added default sample1.xml to created gui directory, fixed issue with non modal dialogs --- coretk/coretk/appconfig.py | 4 + coretk/coretk/coreclient.py | 30 ++- coretk/coretk/dialogs/dialog.py | 2 +- coretk/coretk/dialogs/mobilityplayer.py | 61 +++++ coretk/coretk/linkinfo.py | 11 +- coretk/coretk/menuaction.py | 2 +- coretk/coretk/xmls/sample1.xml | 296 ++++++++++++++++++++++++ 7 files changed, 397 insertions(+), 9 deletions(-) create mode 100644 coretk/coretk/dialogs/mobilityplayer.py create mode 100644 coretk/coretk/xmls/sample1.xml diff --git a/coretk/coretk/appconfig.py b/coretk/coretk/appconfig.py index 2af1047c..74d52553 100644 --- a/coretk/coretk/appconfig.py +++ b/coretk/coretk/appconfig.py @@ -20,6 +20,7 @@ CONFIG_PATH = HOME_PATH.joinpath("gui.yaml") # local paths LOCAL_ICONS_PATH = Path(__file__).parent.joinpath("icons").absolute() LOCAL_BACKGROUND_PATH = Path(__file__).parent.joinpath("backgrounds").absolute() +LOCAL_XMLS_PATH = Path(__file__).parent.joinpath("xmls").absolute() # configuration data TERMINALS = [ @@ -59,6 +60,9 @@ def check_directory(): for background in LOCAL_BACKGROUND_PATH.glob("*"): new_background = BACKGROUNDS_PATH.joinpath(background.name) shutil.copy(background, new_background) + for xml_file in LOCAL_XMLS_PATH.glob("*"): + new_xml = XML_PATH.joinpath(xml_file.name) + shutil.copy(xml_file, new_xml) if "TERM" in os.environ: terminal = TERMINALS[0] diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 71d6da1b..865baedc 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -5,6 +5,7 @@ import logging import os from core.api.grpc import client, core_pb2 +from coretk.dialogs.mobilityplayer import MobilityPlayerDialog from coretk.dialogs.sessions import SessionsDialog from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils @@ -74,6 +75,7 @@ class CoreClient: self.emane_config = None self.service_configs = {} self.file_configs = {} + self.mobility_players = {} def reset(self): # helpers @@ -91,6 +93,7 @@ class CoreClient: self.emane_config = None self.service_configs.clear() self.file_configs.clear() + self.mobility_players.clear() def set_observer(self, value): self.observer = value @@ -119,8 +122,33 @@ class CoreClient: if event.HasField("link_event"): self.app.canvas.wireless_draw.handle_link_event(event.link_event) elif event.HasField("session_event"): - if event.session_event.event <= core_pb2.SessionState.SHUTDOWN: + session_event = event.session_event + if session_event.event <= core_pb2.SessionState.SHUTDOWN: self.state = event.session_event.event + # mobility start + elif session_event.event == 7: + node_id = session_event.node_id + if node_id not in self.mobility_players: + canvas_node = self.canvas_nodes[node_id] + dialog = MobilityPlayerDialog(self.app, self.app, canvas_node) + dialog.show() + self.mobility_players[node_id] = dialog + # mobility stop + elif session_event.event == 8: + node_id = session_event.node_id + if node_id not in self.mobility_players: + canvas_node = self.canvas_nodes[node_id] + dialog = MobilityPlayerDialog(self.app, self.app, canvas_node) + dialog.show() + self.mobility_players[node_id] = dialog + # mobility pause + elif session_event.event == 9: + node_id = session_event.node_id + if node_id not in self.mobility_players: + canvas_node = self.canvas_nodes[node_id] + dialog = MobilityPlayerDialog(self.app, self.app, canvas_node) + dialog.show() + self.mobility_players[node_id] = dialog def handle_throughputs(self, event): interface_throughputs = event.interface_throughputs diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index 769fd176..29362960 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -29,4 +29,4 @@ class Dialog(tk.Toplevel): if self.modal: self.wait_visibility() self.grab_set() - self.wait_window() + self.wait_window() diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py new file mode 100644 index 00000000..cac4caa7 --- /dev/null +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -0,0 +1,61 @@ +from tkinter import ttk + +from coretk.dialogs.dialog import Dialog + +PAD = 5 + + +class MobilityPlayerDialog(Dialog): + def __init__(self, master, app, canvas_node): + super().__init__( + master, app, f"{canvas_node.core_node.name} Mobility Player", modal=False + ) + self.play_button = None + self.pause_button = None + self.stop_button = None + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + + label = ttk.Label(self.top, text="File Name") + label.grid(sticky="ew", pady=PAD) + + frame = ttk.Frame(self.top) + frame.grid(sticky="ew", pady=PAD) + frame.columnconfigure(0, weight=1) + progressbar = ttk.Progressbar(frame, mode="indeterminate") + progressbar.grid(row=0, column=0, sticky="ew", padx=PAD) + progressbar.start() + label = ttk.Label(frame, text="time") + label.grid(row=0, column=1) + + frame = ttk.Frame(self.top) + frame.grid(sticky="ew", pady=PAD) + self.play_button = ttk.Button(frame, text="Play", command=self.click_play) + self.play_button.grid(row=0, column=0, sticky="ew", padx=PAD) + self.pause_button = ttk.Button(frame, text="Pause", command=self.click_pause) + self.pause_button.grid(row=0, column=1, sticky="ew", padx=PAD) + self.stop_button = ttk.Button(frame, text="Stop", command=self.click_stop) + self.stop_button.grid(row=0, column=2, sticky="ew", padx=PAD) + checkbutton = ttk.Checkbutton(frame, text="Loop?") + checkbutton.grid(row=0, column=3, padx=PAD) + label = ttk.Label(frame, text="rate 50 ms") + label.grid(row=0, column=4) + + def clear_buttons(self): + self.play_button.state(["!pressed"]) + self.pause_button.state(["!pressed"]) + self.stop_button.state(["!pressed"]) + + def click_play(self): + self.clear_buttons() + self.play_button.state(["pressed"]) + + def click_pause(self): + self.clear_buttons() + self.pause_button.state(["pressed"]) + + def click_stop(self): + self.clear_buttons() + self.stop_button.state(["pressed"]) diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index 9004afe8..c78fe7e4 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -2,10 +2,10 @@ Link information, such as IPv4, IPv6 and throughput drawn in the canvas """ import logging -import math import tkinter as tk +from tkinter import font -TEXT_DISTANCE = 0.33 +TEXT_DISTANCE = 0.30 class LinkInfo: @@ -21,20 +21,19 @@ class LinkInfo: self.link = link self.id1 = None self.id2 = None + self.font = font.Font(size=8) self.draw_labels() def get_coordinates(self): x1, y1, x2, y2 = self.canvas.coords(self.edge.id) v1 = x2 - x1 v2 = y2 - y1 - d = math.sqrt(v1 ** 2 + v2 ** 2) ux = TEXT_DISTANCE * v1 uy = TEXT_DISTANCE * v2 x1 = x1 + ux y1 = y1 + uy x2 = x2 - ux y2 = y2 - uy - logging.info("line distance: %s", d) return x1, y1, x2, y2 def draw_labels(self): @@ -52,10 +51,10 @@ class LinkInfo: f"{self.link.interface_two.ip6}/{self.link.interface_two.ip6mask}\n" ) self.id1 = self.canvas.create_text( - x1, y1, text=label_one, justify=tk.CENTER, tags="linkinfo" + x1, y1, text=label_one, justify=tk.CENTER, font=self.font, tags="linkinfo" ) self.id2 = self.canvas.create_text( - x2, y2, text=label_two, justify=tk.CENTER, tags="linkinfo" + x2, y2, text=label_two, justify=tk.CENTER, font=self.font, tags="linkinfo" ) def recalculate_info(self): diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index b47c6ef2..89386162 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -76,7 +76,7 @@ class MenuAction: file_path = filedialog.askopenfilename( initialdir=str(XML_PATH), title="Open", - filetypes=(("EmulationScript XML File", "*.xml"), ("All Files", "*")), + filetypes=(("XML Files", "*.xml"), ("All Files", "*")), ) if file_path: logging.info("opening xml: %s", file_path) diff --git a/coretk/coretk/xmls/sample1.xml b/coretk/coretk/xmls/sample1.xml new file mode 100644 index 00000000..cfa5a99a --- /dev/null +++ b/coretk/coretk/xmls/sample1.xml @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.0.5/32 + ipv6 address a::3/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa +! +interface eth1 + ip address 10.0.6.2/24 + !ip ospf hello-interval 2 + !ip ospf dead-interval 6 + !ip ospf retransmit-interval 5 + !ip ospf network point-to-point + ipv6 address a:6::2/64 +! +router ospf + router-id 10.0.0.5 + network 10.0.0.5/32 area 0 + network 10.0.6.0/24 area 0 + redistribute connected metric-type 1 + redistribute ospf6 metric-type 1 +! +router ospf6 + router-id 10.0.0.5 + interface eth0 area 0.0.0.0 + redistribute connected + redistribute ospf +! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 00e0da2990cf4855cb7104d347f4c7930e9f060d Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 26 Nov 2019 15:12:04 -0800 Subject: [PATCH 283/462] improve wireless link logic when deleting nodes, stop session and join session --- coretk/coretk/dialogs/customnodes.py | 6 +++--- coretk/coretk/graph.py | 1 + coretk/coretk/nodedelete.py | 24 ++++++++++++++++++++++++ coretk/coretk/toolbar.py | 5 ++++- coretk/coretk/wirelessconnection.py | 4 ++++ 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 55199541..dbf431d3 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -62,9 +62,9 @@ class ServicesSelectDialog(Dialog): index = selection[0] group = self.groups.listbox.get(index) self.services.clear() - for service in sorted(self.app.core.services[group], key=lambda x: x.name): - checked = service.name in self.current_services - self.services.add(service.name, checked) + for name in sorted(self.app.core.services[group]): + checked = name in self.current_services + self.services.add(name, checked) def service_clicked(self, name, var): if var.get() and name not in self.current_services: diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index ef36e2f9..b64ce950 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -114,6 +114,7 @@ class CanvasGraph(tk.Canvas): self.nodes.clear() self.edges.clear() self.drawing_edge = None + self.wireless_draw.map.clear() self.draw_session(session) def setup_bindings(self): diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index 4ec986aa..92844aac 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -45,6 +45,18 @@ class CanvasComponentManagement: def delete_selected_nodes(self): edges = set() nodes = [] + + node_to_wlink = {} + for link_tuple in self.canvas.wireless_draw.map: + nid_one, nid_two = link_tuple + if nid_one not in node_to_wlink: + node_to_wlink[nid_one] = [] + if nid_two not in node_to_wlink: + node_to_wlink[nid_two] = [] + node_to_wlink[nid_one].append(link_tuple) + node_to_wlink[nid_two].append(link_tuple) + + # delete antennas and wireless links for cnid in self.selected: canvas_node = self.canvas.nodes[cnid] if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: @@ -63,6 +75,18 @@ class CanvasComponentManagement: neighbor = self.app.canvas_nodes[neighbor_id] if neighbor.core_node.type != core_pb2.NodeType.WIRELESS_LAN: neighbor.antenna_draw.delete_antenna() + for link_tuple in node_to_wlink[canvas_node.core_node.id]: + nid_one, nid_two = link_tuple + if link_tuple in self.canvas.wireless_draw.map: + self.canvas.delete(self.canvas.wireless_draw.map[link_tuple]) + link_cid = self.canvas.wireless_draw.map.pop(link_tuple, None) + canvas_node_one = self.app.canvas_nodes[nid_one] + canvas_node_two = self.app.canvas_nodes[nid_two] + if link_cid in canvas_node_one.wlans: + canvas_node_one.wlans.remove(link_cid) + if link_cid in canvas_node_two.wlans: + canvas_node_two.wlans.remove(link_cid) + for node_id in list(self.selected): bbox_id = self.selected[node_id] canvas_node = self.canvas.nodes.pop(node_id) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 40fc36c0..d1830084 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -356,7 +356,10 @@ class Toolbar(ttk.Frame): self.app.statusbar.progress_bar.start(5) thread = threading.Thread(target=self.app.core.stop_session) thread.start() - self.app.canvas.delete("wireless") + for cid in self.app.canvas.find_withtag("wireless"): + self.app.canvas.itemconfig(cid, state="hidden") + # self.app.canvas.delete("wireless") + self.design_frame.tkraise() def update_annotation(self, image): diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py index 2e0e0af9..7b868bea 100644 --- a/coretk/coretk/wirelessconnection.py +++ b/coretk/coretk/wirelessconnection.py @@ -16,6 +16,7 @@ class WirelessConnection: canvas_node_two = self.core.canvas_nodes[node_two_id] key = tuple(sorted((node_one_id, node_two_id))) if key not in self.map: + print("not in map") x1, y1 = self.canvas.coords(canvas_node_one.id) x2, y2 = self.canvas.coords(canvas_node_two.id) wlan_canvas_id = self.canvas.create_line( @@ -24,6 +25,9 @@ class WirelessConnection: self.map[key] = wlan_canvas_id canvas_node_one.wlans.append(wlan_canvas_id) canvas_node_two.wlans.append(wlan_canvas_id) + else: + print("in map") + self.canvas.itemconfig(self.map[key], state="normal") def delete_connection(self, node_one_id, node_two_id): canvas_node_one = self.core.canvas_nodes[node_one_id] From 2a08c770d37392e0e506c2a5f305ce72184e4253 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 26 Nov 2019 16:50:23 -0800 Subject: [PATCH 284/462] updates to handle node events for moving nodes and displaying mobility player dialog on session event with configured data --- coretk/coretk/coreclient.py | 26 ++++++++++++------------- coretk/coretk/dialogs/mobilityplayer.py | 18 ++++++++++++++--- coretk/coretk/graph.py | 20 ++++++++++++++++++- coretk/coretk/xmls/sample1.xml | 2 +- 4 files changed, 48 insertions(+), 18 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 146ed44d..04d9df06 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -119,10 +119,11 @@ class CoreClient: self.custom_observers[observer.name] = observer def handle_events(self, event): - logging.info("event: %s", event) if event.HasField("link_event"): + logging.info("link event: %s", event) self.app.canvas.wireless_draw.handle_link_event(event.link_event) elif event.HasField("session_event"): + logging.info("session event: %s", event) session_event = event.session_event if session_event.event <= core_pb2.SessionState.SHUTDOWN: self.state = event.session_event.event @@ -136,20 +137,19 @@ class CoreClient: self.mobility_players[node_id] = dialog # mobility stop elif session_event.event == 8: - node_id = session_event.node_id - if node_id not in self.mobility_players: - canvas_node = self.canvas_nodes[node_id] - dialog = MobilityPlayerDialog(self.app, self.app, canvas_node) - dialog.show() - self.mobility_players[node_id] = dialog + pass # mobility pause elif session_event.event == 9: - node_id = session_event.node_id - if node_id not in self.mobility_players: - canvas_node = self.canvas_nodes[node_id] - dialog = MobilityPlayerDialog(self.app, self.app, canvas_node) - dialog.show() - self.mobility_players[node_id] = dialog + pass + elif event.HasField("node_event"): + node_event = event.node_event + node_id = node_event.node.id + x = node_event.node.position.x + y = node_event.node.position.y + canvas_node = self.canvas_nodes[node_id] + canvas_node.move(x, y) + else: + logging.info("unhandled event: %s", event) def handle_throughputs(self, event): interface_throughputs = event.interface_throughputs diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index cac4caa7..0eee7166 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -1,3 +1,4 @@ +import tkinter as tk from tkinter import ttk from coretk.dialogs.dialog import Dialog @@ -10,6 +11,7 @@ class MobilityPlayerDialog(Dialog): super().__init__( master, app, f"{canvas_node.core_node.name} Mobility Player", modal=False ) + self.config = self.app.core.mobility_configs[canvas_node.core_node.id] self.play_button = None self.pause_button = None self.stop_button = None @@ -18,7 +20,8 @@ class MobilityPlayerDialog(Dialog): def draw(self): self.top.columnconfigure(0, weight=1) - label = ttk.Label(self.top, text="File Name") + file_name = self.config["file"].value + label = ttk.Label(self.top, text=file_name) label.grid(sticky="ew", pady=PAD) frame = ttk.Frame(self.top) @@ -32,15 +35,24 @@ class MobilityPlayerDialog(Dialog): frame = ttk.Frame(self.top) frame.grid(sticky="ew", pady=PAD) + self.play_button = ttk.Button(frame, text="Play", command=self.click_play) self.play_button.grid(row=0, column=0, sticky="ew", padx=PAD) + self.pause_button = ttk.Button(frame, text="Pause", command=self.click_pause) self.pause_button.grid(row=0, column=1, sticky="ew", padx=PAD) + self.stop_button = ttk.Button(frame, text="Stop", command=self.click_stop) self.stop_button.grid(row=0, column=2, sticky="ew", padx=PAD) - checkbutton = ttk.Checkbutton(frame, text="Loop?") + + loop = tk.IntVar(value=int(self.config["loop"].value == "1")) + checkbutton = ttk.Checkbutton( + frame, text="Loop?", variable=loop, state=tk.DISABLED + ) checkbutton.grid(row=0, column=3, padx=PAD) - label = ttk.Label(frame, text="rate 50 ms") + + rate = self.config["refresh_ms"].value + label = ttk.Label(frame, text=f"rate {rate} ms") label.grid(row=0, column=4) def clear_buttons(self): diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index ef36e2f9..7c8f154e 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -585,6 +585,25 @@ class CanvasNode: self.canvas.itemconfig(self.id, image=self.image) self.canvas.itemconfig(self.text_id, text=self.core_node.name) + def move(self, x, y): + old_x = self.core_node.position.x + old_y = self.core_node.position.y + x_offset = x - old_x + y_offset = y - old_y + self.core_node.position.x = x + self.core_node.position.y = y + for edge in self.edges: + x1, y1, x2, y2 = self.canvas.coords(edge.id) + if edge.src == self.id: + self.canvas.coords(edge.id, x_offset, y_offset, x2, y2) + else: + self.canvas.coords(edge.id, x1, y1, x_offset, y_offset) + edge.link_info.recalculate_info() + self.canvas.helper.update_wlan_connection(old_x, old_y, x, y, self.wlans) + self.canvas.move(self.id, x_offset, y_offset) + self.canvas.move(self.text_id, x_offset, y_offset) + self.antenna_draw.update_antennas_position(x_offset, y_offset) + def on_enter(self, event): if self.app.core.is_runtime() and self.app.core.observer: self.tooltip.text.set("waiting...") @@ -612,7 +631,6 @@ class CanvasNode: def click_press(self, event): logging.debug(f"node click press {self.core_node.name}: {event}") self.moving = self.canvas.canvas_xy(event) - self.canvas.canvas_management.node_select(self) def click_release(self, event): diff --git a/coretk/coretk/xmls/sample1.xml b/coretk/coretk/xmls/sample1.xml index cfa5a99a..8bda5b8c 100644 --- a/coretk/coretk/xmls/sample1.xml +++ b/coretk/coretk/xmls/sample1.xml @@ -188,7 +188,7 @@ - + From 13ca85cf3f12a374996ccbba1d6c6782b576cce2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 26 Nov 2019 17:00:55 -0800 Subject: [PATCH 285/462] changes to support mobility actions for mobility player dialog --- coretk/coretk/dialogs/mobilityplayer.py | 25 ++++++++++++++++++++++--- coretk/coretk/wirelessconnection.py | 1 - 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index 0eee7166..3a4020e6 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -1,6 +1,7 @@ import tkinter as tk from tkinter import ttk +from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog PAD = 5 @@ -11,10 +12,13 @@ class MobilityPlayerDialog(Dialog): super().__init__( master, app, f"{canvas_node.core_node.name} Mobility Player", modal=False ) + self.canvas_node = canvas_node + self.node = canvas_node.core_node self.config = self.app.core.mobility_configs[canvas_node.core_node.id] self.play_button = None self.pause_button = None self.stop_button = None + self.progressbar = None self.draw() def draw(self): @@ -27,9 +31,9 @@ class MobilityPlayerDialog(Dialog): frame = ttk.Frame(self.top) frame.grid(sticky="ew", pady=PAD) frame.columnconfigure(0, weight=1) - progressbar = ttk.Progressbar(frame, mode="indeterminate") - progressbar.grid(row=0, column=0, sticky="ew", padx=PAD) - progressbar.start() + self.progressbar = ttk.Progressbar(frame, mode="indeterminate") + self.progressbar.grid(row=0, column=0, sticky="ew", padx=PAD) + self.progressbar.start() label = ttk.Label(frame, text="time") label.grid(row=0, column=1) @@ -63,11 +67,26 @@ class MobilityPlayerDialog(Dialog): def click_play(self): self.clear_buttons() self.play_button.state(["pressed"]) + session_id = self.app.core.session_id + self.app.core.client.mobility_action( + session_id, self.node.id, core_pb2.MobilityAction.START + ) + self.progressbar.start() def click_pause(self): self.clear_buttons() self.pause_button.state(["pressed"]) + session_id = self.app.core.session_id + self.app.core.client.mobility_action( + session_id, self.node.id, core_pb2.MobilityAction.PAUSE + ) + self.progressbar.stop() def click_stop(self): self.clear_buttons() self.stop_button.state(["pressed"]) + session_id = self.app.core.session_id + self.app.core.client.mobility_action( + session_id, self.node.id, core_pb2.MobilityAction.STOP + ) + self.progressbar.stop() diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py index 7b868bea..729fd84e 100644 --- a/coretk/coretk/wirelessconnection.py +++ b/coretk/coretk/wirelessconnection.py @@ -16,7 +16,6 @@ class WirelessConnection: canvas_node_two = self.core.canvas_nodes[node_two_id] key = tuple(sorted((node_one_id, node_two_id))) if key not in self.map: - print("not in map") x1, y1 = self.canvas.coords(canvas_node_one.id) x2, y2 = self.canvas.coords(canvas_node_two.id) wlan_canvas_id = self.canvas.create_line( From 3cd48ec1aba9cc2436b2156b9c6a1e3138d203a2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 08:48:21 -0800 Subject: [PATCH 286/462] added pause icon and icons to mobility player buttons --- coretk/coretk/dialogs/mobilityplayer.py | 16 +++++++++++++--- coretk/coretk/icons/pause.png | Bin 0 -> 2368 bytes coretk/coretk/images.py | 1 + 3 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 coretk/coretk/icons/pause.png diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index 3a4020e6..30053498 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -3,8 +3,10 @@ from tkinter import ttk from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog +from coretk.images import ImageEnum, Images PAD = 5 +ICON_SIZE = 16 class MobilityPlayerDialog(Dialog): @@ -39,14 +41,22 @@ class MobilityPlayerDialog(Dialog): frame = ttk.Frame(self.top) frame.grid(sticky="ew", pady=PAD) + for i in range(3): + frame.columnconfigure(i, weight=1) - self.play_button = ttk.Button(frame, text="Play", command=self.click_play) + image = Images.get(ImageEnum.START, width=ICON_SIZE) + self.play_button = ttk.Button(frame, image=image, command=self.click_play) + self.play_button.image = image self.play_button.grid(row=0, column=0, sticky="ew", padx=PAD) - self.pause_button = ttk.Button(frame, text="Pause", command=self.click_pause) + image = Images.get(ImageEnum.PAUSE, width=ICON_SIZE) + self.pause_button = ttk.Button(frame, image=image, command=self.click_pause) + self.pause_button.image = image self.pause_button.grid(row=0, column=1, sticky="ew", padx=PAD) - self.stop_button = ttk.Button(frame, text="Stop", command=self.click_stop) + image = Images.get(ImageEnum.STOP, width=ICON_SIZE) + self.stop_button = ttk.Button(frame, image=image, command=self.click_stop) + self.stop_button.image = image self.stop_button.grid(row=0, column=2, sticky="ew", padx=PAD) loop = tk.IntVar(value=int(self.config["loop"].value == "1")) diff --git a/coretk/coretk/icons/pause.png b/coretk/coretk/icons/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..9ac4e6eab0939e105f042d7b97722579b98b5a82 GIT binary patch literal 2368 zcmV-G3BUG`WoBV_kfjAR0kx<|j5ox#R>T;& zjMPv}Q@=D#Y)zY3iKR+vOKj4l?T3D`QRqb(HY%-{8Y$ke6k{)xYKu#8h22?p7K*YL zV4d?m{jfXh-uKMqoHOj1-={g3_j&($|K~RE`<~}OnLv@C1-PN{rTLSqPj@WH*wl+s zDR7|?trn#cK!q+#r9FJydo`M4qZ$-TGk=0(>_hm7IOGK{pC| z7r9?nyuwlB1+yznlM5$Zq$eLl^ig1nSM_+z8C0I=>7KG>-q)JW zdQpKV2DEICT`wYA6+Y)h`~YZF^uBl^y|37Ti)w%>(z>JO0pKSJ6~&BQ0%TBmy!PM6 zH_M}#Vg)Ul0jb~B&LrY&RajB1m?g^YNJiJ!+?#GIMzEq7kZP#COpWt0aB(r>od-0j zac+yRJMn&@fviFUq#9;lqsDoI@H9R^y;%0X=49-;LIV|KKy$KYxf-(vh!!g3JYFolPx3L%8j2B5k#K;;Hmm?dx6I_U}{I_!=RTG zM^~trUr}}DyW{ON<|U|iFvU8E2mSf>)LJgQax-?N`+K@N-E=lS$=T)?N8Elvd?l3& z?nmsYu46h~NBQ``CYoPfTCStQ_tU;YIXCL&9WO*`}a^aPmnG#=_bGhj=uA(iy z7t{06p!L?I>0Gq*mxz_ym9k>`6)13O-~Bmjalr){ho3RV!^^0bpw#x+H+eaXXjj$e zRZgr)*i+-f<4vv0x$(0^^3>vzl`Y$2*N?VwR0C9Wqc?iOhlo5p+O`o5XlaNp2K|zE zlEQ_;9jWB(t4G>4q5*2`M=0&7Bj+K`_eNSf%z&fM%&hI3G*zaEC~|i?8J#tB{V)UU zNMt>wGh=NcVfP^S4qZ9S08w4BE=?z+{bT9<2An$=xvAXc z>^%w<8Mq<4+;4zjZVpCL_)xKyWqn~+uDNOVMofd z9sFcoUz_(DP<^^%0p;!oKd~!McYPN649HkAH&{L4Lu{>wJ_AH`TA*q|h-0XRtO3%y zP{uz*^iu;2TAt3Ed=rh1VN-Bp=7|0&< zTB8G{RT`H z=M8~R0#Ot$daQG@H@nbpKrGSTsqjuPn!<$cpS7W_E4ws!Y9j3MUr^YvULCYFaKT_2 z{1p>E3_7Td2N@7gwEqM6r>}}ah5UU^qHWOA<_O~f@*BV8gbh{w_0V<03~-#xZz*?6 zctK{8m}iEr9A?19UpbLdV~O_TL)VWoB(f*3B}l3M%pl<0lhb4 zdD&Y{q2k5S(&CT)raWe|cX~pG=!i!@jA1}Lk=`e;%Ns@DbT;KxPE8L^ck}Rg-7Toe zuO}swrxus&tXrG@^Jp8#{#J`^eG|vr0=Uor;*yUKY(jxZF6nAA-ErNDW@rRg}^5&x@@d#e->9$6Xzt3zYX$8;nKo^ zQ66&B_>eL&XNS zjSo4_j*hQtIcYFo1d8(cjtNG)V6T{yX#QU=g>s&rJ}1$BSS)KLl;@KUphK{3uS=wI zep<}y{eapvt?z)Y272--Kf&lhSyh{8{cApj^L|02E|Gpk;dUU*6MMS^b64FxZM*Xg zP;g?t-jDd0U4-w04ky*HGQO_uFNFjtN@m37XJ=j_mi<&DZ_y@Cu7$M*3%Ow?Y$l{ zmfSaIP22urNA~13)p#Pkul3aNMFQUj&gT~Z&!F;Hch$5jy=Z*!AwBW&WX(JWeh%cG zt3FtEifw(%>u=5bmLIyMA-Y(M^{~PkKkLnd-D>!Lp^ukt@x38>jgXCs+)e4^C~^dz zGe#eeuS*~Bv*N&x#OCMfK4WDvs}(i?qkiDPW8On$TZK4VN18O{2PJVSIMxum!d7dI zsxB9})~ocH0-q?dAJ}8ihM^N>gPS-jmRk*I@OV; mQ`U_CE=;^z?yW5|Hvb2nH000xvBxR^0000 Date: Wed, 27 Nov 2019 08:49:58 -0800 Subject: [PATCH 287/462] improve small logic in node deletion and wallpaper change --- coretk/coretk/dialogs/canvasbackground.py | 6 ++++-- coretk/coretk/dialogs/preferences.py | 12 ++++++++++++ coretk/coretk/nodedelete.py | 24 ++++++++++++----------- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/coretk/coretk/dialogs/canvasbackground.py b/coretk/coretk/dialogs/canvasbackground.py index 8fb92c8b..84b0bdb6 100644 --- a/coretk/coretk/dialogs/canvasbackground.py +++ b/coretk/coretk/dialogs/canvasbackground.py @@ -12,6 +12,7 @@ from coretk.dialogs.dialog import Dialog from coretk.images import Images PADX = 5 +ABOVE_WALLPAPER = ["edge", "linkinfo", "wireless", "antenna", "nodename", "node"] class CanvasBackgroundDialog(Dialog): @@ -182,17 +183,18 @@ class CanvasBackgroundDialog(Dialog): self.canvas.wallpaper_file = None self.destroy() return - try: img = Image.open(filename) self.canvas.wallpaper = img self.canvas.wallpaper_file = filename self.canvas.redraw() + for component in ABOVE_WALLPAPER: + self.canvas.tag_raise(component) + except FileNotFoundError: logging.error("invalid background: %s", filename) if self.canvas.wallpaper_id: self.canvas.delete(self.canvas.wallpaper_id) self.canvas.wallpaper_id = None self.canvas.wallpaper_file = None - self.destroy() diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py index 7298f727..d65b87c3 100644 --- a/coretk/coretk/dialogs/preferences.py +++ b/coretk/coretk/dialogs/preferences.py @@ -14,6 +14,8 @@ class PreferencesDialog(Dialog): self.theme = tk.StringVar(value=preferences["theme"]) self.terminal = tk.StringVar(value=preferences["terminal"]) self.gui3d = tk.StringVar(value=preferences["gui3d"]) + self.width = tk.StringVar(value="1000") + self.height = tk.StringVar(value="800") self.draw() def draw(self): @@ -58,6 +60,16 @@ class PreferencesDialog(Dialog): entry = ttk.Entry(frame, textvariable=self.gui3d) entry.grid(row=3, column=1, sticky="ew") + label = ttk.Label(frame, text="Canvas width (in pixel)") + label.grid(row=4, column=0, pady=2, padx=2, sticky="w") + entry = ttk.Entry(frame, textvariable=self.width) + entry.grid(row=4, column=1, sticky="ew") + + label = ttk.Label(frame, text="Canvas height (in pixel)") + label.grid(row=5, column=0, pady=2, padx=2, sticky="w") + entry = ttk.Entry(frame, textvariable=self.height) + entry.grid(row=5, column=1, sticky="ew") + def draw_buttons(self): frame = ttk.Frame(self.top) frame.grid(sticky="ew") diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index 92844aac..58e90170 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -75,17 +75,19 @@ class CanvasComponentManagement: neighbor = self.app.canvas_nodes[neighbor_id] if neighbor.core_node.type != core_pb2.NodeType.WIRELESS_LAN: neighbor.antenna_draw.delete_antenna() - for link_tuple in node_to_wlink[canvas_node.core_node.id]: - nid_one, nid_two = link_tuple - if link_tuple in self.canvas.wireless_draw.map: - self.canvas.delete(self.canvas.wireless_draw.map[link_tuple]) - link_cid = self.canvas.wireless_draw.map.pop(link_tuple, None) - canvas_node_one = self.app.canvas_nodes[nid_one] - canvas_node_two = self.app.canvas_nodes[nid_two] - if link_cid in canvas_node_one.wlans: - canvas_node_one.wlans.remove(link_cid) - if link_cid in canvas_node_two.wlans: - canvas_node_two.wlans.remove(link_cid) + + if canvas_node.core_node.id in node_to_wlink: + for link_tuple in node_to_wlink[canvas_node.core_node.id]: + nid_one, nid_two = link_tuple + if link_tuple in self.canvas.wireless_draw.map: + self.canvas.delete(self.canvas.wireless_draw.map[link_tuple]) + link_cid = self.canvas.wireless_draw.map.pop(link_tuple, None) + canvas_node_one = self.app.canvas_nodes[nid_one] + canvas_node_two = self.app.canvas_nodes[nid_two] + if link_cid in canvas_node_one.wlans: + canvas_node_one.wlans.remove(link_cid) + if link_cid in canvas_node_two.wlans: + canvas_node_two.wlans.remove(link_cid) for node_id in list(self.selected): bbox_id = self.selected[node_id] From af9915191df37d9c89351b789fb813b828430af0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 08:52:20 -0800 Subject: [PATCH 288/462] fixed wireless link events displaying over nodes --- coretk/coretk/wirelessconnection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py index 729fd84e..dd51723b 100644 --- a/coretk/coretk/wirelessconnection.py +++ b/coretk/coretk/wirelessconnection.py @@ -43,6 +43,7 @@ class WirelessConnection: self.add_connection( link_event.link.node_one_id, link_event.link.node_two_id ) + self.canvas.tag_raise("node") if link_event.message_type == core_pb2.MessageType.DELETE: self.delete_connection( From 8b7d651d061600d9e6469c5c624177c0816cc0f7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 09:05:37 -0800 Subject: [PATCH 289/462] fixed issue with nod emobility location calculations being forced to use ints --- daemon/core/api/grpc/server.py | 2 +- daemon/core/api/tlv/dataconversion.py | 4 ++-- daemon/core/location/mobility.py | 6 ------ 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 79053eee..54606818 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -488,7 +488,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :return: node event that contains node id, name, model, position, and services :rtype: core.api.grpc.core_pb2.NodeEvent """ - position = core_pb2.Position(x=event.x_position, y=event.y_position) + position = core_pb2.Position(x=int(event.x_position), y=int(event.y_position)) services = event.services or "" services = services.split("|") node_proto = core_pb2.Node( diff --git a/daemon/core/api/tlv/dataconversion.py b/daemon/core/api/tlv/dataconversion.py index 8e1c270d..8228b536 100644 --- a/daemon/core/api/tlv/dataconversion.py +++ b/daemon/core/api/tlv/dataconversion.py @@ -26,8 +26,8 @@ def convert_node(node_data): (NodeTlvs.EMULATION_ID, node_data.emulation_id), (NodeTlvs.EMULATION_SERVER, node_data.server), (NodeTlvs.SESSION, node_data.session), - (NodeTlvs.X_POSITION, node_data.x_position), - (NodeTlvs.Y_POSITION, node_data.y_position), + (NodeTlvs.X_POSITION, int(node_data.x_position)), + (NodeTlvs.Y_POSITION, int(node_data.y_position)), (NodeTlvs.CANVAS, node_data.canvas), (NodeTlvs.NETWORK_ID, node_data.network_id), (NodeTlvs.SERVICES, node_data.services), diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 085962ff..6a796526 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -811,12 +811,6 @@ class WayPointMobility(WirelessModel): :param z: z position :return: nothing """ - if x is not None: - x = int(x) - if y is not None: - y = int(y) - if z is not None: - z = int(z) node.position.set(x, y, z) node_data = node.data(message_type=0) self.session.broadcast_node(node_data) From da203d578e7e9ef7d795ce66929f446ce3a88554 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 09:15:19 -0800 Subject: [PATCH 290/462] fixed issue with node event tests when position is None --- daemon/core/api/grpc/server.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 54606818..c0fe1d94 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -488,7 +488,13 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :return: node event that contains node id, name, model, position, and services :rtype: core.api.grpc.core_pb2.NodeEvent """ - position = core_pb2.Position(x=int(event.x_position), y=int(event.y_position)) + x = None + if event.x_position is not None: + x = int(event.x_position) + y = None + if event.y_position is not None: + y = int(event.y_position) + position = core_pb2.Position(x=x, y=y) services = event.services or "" services = services.split("|") node_proto = core_pb2.Node( From 1ca9aec247c738522619644ec3abebfcf601752d Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 27 Nov 2019 09:54:43 -0800 Subject: [PATCH 291/462] canvas size added to preferences and updated to dialog --- coretk/coretk/app.py | 9 ++++++++- coretk/coretk/appconfig.py | 2 ++ coretk/coretk/dialogs/preferences.py | 6 ++++-- coretk/coretk/graph.py | 4 ++-- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index f0445257..c8232ebd 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -67,8 +67,15 @@ class Application(tk.Frame): self.draw_status() def draw_canvas(self): + width = self.guiconfig["preferences"]["width"] + height = self.guiconfig["preferences"]["height"] self.canvas = CanvasGraph( - self, self.core, background="#cccccc", scrollregion=(0, 0, 1200, 1000) + self, + self.core, + width, + height, + background="#cccccc", + scrollregion=(0, 0, 1200, 1000), ) self.canvas.pack(fill=tk.BOTH, expand=True) scroll_x = ttk.Scrollbar( diff --git a/coretk/coretk/appconfig.py b/coretk/coretk/appconfig.py index 74d52553..fc1bc1a5 100644 --- a/coretk/coretk/appconfig.py +++ b/coretk/coretk/appconfig.py @@ -78,6 +78,8 @@ def check_directory(): "editor": editor, "terminal": terminal, "gui3d": "/usr/local/bin/std3d.sh", + "width": 1000, + "height": 750, }, "location": { "x": 0.0, diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py index d65b87c3..11903d2d 100644 --- a/coretk/coretk/dialogs/preferences.py +++ b/coretk/coretk/dialogs/preferences.py @@ -14,8 +14,8 @@ class PreferencesDialog(Dialog): self.theme = tk.StringVar(value=preferences["theme"]) self.terminal = tk.StringVar(value=preferences["terminal"]) self.gui3d = tk.StringVar(value=preferences["gui3d"]) - self.width = tk.StringVar(value="1000") - self.height = tk.StringVar(value="800") + self.width = tk.StringVar(value=preferences["width"]) + self.height = tk.StringVar(value=preferences["height"]) self.draw() def draw(self): @@ -93,5 +93,7 @@ class PreferencesDialog(Dialog): preferences["editor"] = self.editor.get() preferences["gui3d"] = self.gui3d.get() preferences["theme"] = self.theme.get() + preferences["width"] = self.width.get() + preferences["height"] = self.height.get() self.app.save_config() self.destroy() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index c03d7b48..518245d3 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -39,7 +39,7 @@ class ScaleOption(enum.Enum): class CanvasGraph(tk.Canvas): - def __init__(self, master, core, cnf=None, **kwargs): + def __init__(self, master, core, width, height, cnf=None, **kwargs): if cnf is None: cnf = {} kwargs["highlightthickness"] = 0 @@ -54,7 +54,7 @@ class CanvasGraph(tk.Canvas): self.grid = None self.canvas_management = CanvasComponentManagement(self, core) self.setup_bindings() - self.draw_grid() + self.draw_grid(width, height) self.core = core self.helper = GraphHelper(self, core) self.throughput_draw = Throughput(self, core) From d51dc0e9097727053a3b9318b537dc50170404c4 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 27 Nov 2019 11:21:03 -0800 Subject: [PATCH 292/462] canvas size --- coretk/coretk/dialogs/canvassizeandscale.py | 3 +++ coretk/coretk/dialogs/preferences.py | 14 -------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index f25dfdd6..fe4c3b62 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -187,5 +187,8 @@ class SizeAndScaleDialog(Dialog): location_config["lon"] = location.lon location_config["alt"] = location.alt location_config["scale"] = location.scale + preferences = self.app.guiconfig["preferences"] + preferences["width"] = width + preferences["height"] = height self.app.save_config() self.destroy() diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py index 11903d2d..7298f727 100644 --- a/coretk/coretk/dialogs/preferences.py +++ b/coretk/coretk/dialogs/preferences.py @@ -14,8 +14,6 @@ class PreferencesDialog(Dialog): self.theme = tk.StringVar(value=preferences["theme"]) self.terminal = tk.StringVar(value=preferences["terminal"]) self.gui3d = tk.StringVar(value=preferences["gui3d"]) - self.width = tk.StringVar(value=preferences["width"]) - self.height = tk.StringVar(value=preferences["height"]) self.draw() def draw(self): @@ -60,16 +58,6 @@ class PreferencesDialog(Dialog): entry = ttk.Entry(frame, textvariable=self.gui3d) entry.grid(row=3, column=1, sticky="ew") - label = ttk.Label(frame, text="Canvas width (in pixel)") - label.grid(row=4, column=0, pady=2, padx=2, sticky="w") - entry = ttk.Entry(frame, textvariable=self.width) - entry.grid(row=4, column=1, sticky="ew") - - label = ttk.Label(frame, text="Canvas height (in pixel)") - label.grid(row=5, column=0, pady=2, padx=2, sticky="w") - entry = ttk.Entry(frame, textvariable=self.height) - entry.grid(row=5, column=1, sticky="ew") - def draw_buttons(self): frame = ttk.Frame(self.top) frame.grid(sticky="ew") @@ -93,7 +81,5 @@ class PreferencesDialog(Dialog): preferences["editor"] = self.editor.get() preferences["gui3d"] = self.gui3d.get() preferences["theme"] = self.theme.get() - preferences["width"] = self.width.get() - preferences["height"] = self.height.get() self.app.save_config() self.destroy() From 9a55ff4ca5964bc4cef915da66e9cf85831ec0c2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 11:41:54 -0800 Subject: [PATCH 293/462] display mobility player on session start, change buttons and progress bar on session events --- coretk/coretk/coreclient.py | 30 +++++++++++-------- coretk/coretk/dialogs/mobilityplayer.py | 40 ++++++++++++++----------- coretk/coretk/menubar.py | 2 +- 3 files changed, 41 insertions(+), 31 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 04d9df06..e6dfb77e 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -128,19 +128,18 @@ class CoreClient: if session_event.event <= core_pb2.SessionState.SHUTDOWN: self.state = event.session_event.event # mobility start - elif session_event.event == 7: + elif session_event.event in {7, 8, 9}: node_id = session_event.node_id - if node_id not in self.mobility_players: - canvas_node = self.canvas_nodes[node_id] - dialog = MobilityPlayerDialog(self.app, self.app, canvas_node) - dialog.show() - self.mobility_players[node_id] = dialog - # mobility stop - elif session_event.event == 8: - pass - # mobility pause - elif session_event.event == 9: - pass + dialog = self.mobility_players.get(node_id) + if dialog: + if session_event.event == 7: + dialog.set_play() + elif session_event.event == 8: + dialog.set_stop() + else: + dialog.set_pause() + else: + logging.warning("unknown session event: %s", session_event) elif event.HasField("node_event"): node_event = event.node_event node_id = node_event.node.id @@ -359,6 +358,13 @@ class CoreClient: logging.debug("start session(%s), result: %s", self.session_id, response.result) self.app.statusbar.start_session_callback(process_time) + # display mobility players + for node_id, config in self.mobility_configs.items(): + canvas_node = self.canvas_nodes[node_id] + dialog = MobilityPlayerDialog(self.app, self.app, canvas_node, config) + dialog.show() + self.mobility_players[node_id] = dialog + def stop_session(self, session_id=None): if not session_id: session_id = self.session_id diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index 30053498..65b16058 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -10,13 +10,13 @@ ICON_SIZE = 16 class MobilityPlayerDialog(Dialog): - def __init__(self, master, app, canvas_node): + def __init__(self, master, app, canvas_node, config): super().__init__( master, app, f"{canvas_node.core_node.name} Mobility Player", modal=False ) self.canvas_node = canvas_node self.node = canvas_node.core_node - self.config = self.app.core.mobility_configs[canvas_node.core_node.id] + self.config = config self.play_button = None self.pause_button = None self.stop_button = None @@ -30,14 +30,8 @@ class MobilityPlayerDialog(Dialog): label = ttk.Label(self.top, text=file_name) label.grid(sticky="ew", pady=PAD) - frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PAD) - frame.columnconfigure(0, weight=1) - self.progressbar = ttk.Progressbar(frame, mode="indeterminate") - self.progressbar.grid(row=0, column=0, sticky="ew", padx=PAD) - self.progressbar.start() - label = ttk.Label(frame, text="time") - label.grid(row=0, column=1) + self.progressbar = ttk.Progressbar(self.top, mode="indeterminate") + self.progressbar.grid(sticky="ew", pady=PAD) frame = ttk.Frame(self.top) frame.grid(sticky="ew", pady=PAD) @@ -58,6 +52,7 @@ class MobilityPlayerDialog(Dialog): 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=PAD) + self.stop_button.state(["pressed"]) loop = tk.IntVar(value=int(self.config["loop"].value == "1")) checkbutton = ttk.Checkbutton( @@ -74,29 +69,38 @@ class MobilityPlayerDialog(Dialog): self.pause_button.state(["!pressed"]) self.stop_button.state(["!pressed"]) - def click_play(self): + def set_play(self): self.clear_buttons() self.play_button.state(["pressed"]) + self.progressbar.start() + + def set_pause(self): + self.clear_buttons() + self.pause_button.state(["pressed"]) + self.progressbar.stop() + + def set_stop(self): + self.clear_buttons() + self.stop_button.state(["pressed"]) + self.progressbar.stop() + + def click_play(self): + self.set_play() session_id = self.app.core.session_id self.app.core.client.mobility_action( session_id, self.node.id, core_pb2.MobilityAction.START ) - self.progressbar.start() def click_pause(self): - self.clear_buttons() - self.pause_button.state(["pressed"]) + self.set_pause() session_id = self.app.core.session_id self.app.core.client.mobility_action( session_id, self.node.id, core_pb2.MobilityAction.PAUSE ) - self.progressbar.stop() def click_stop(self): - self.clear_buttons() - self.stop_button.state(["pressed"]) + self.set_stop() session_id = self.app.core.session_id self.app.core.client.mobility_action( session_id, self.node.id, core_pb2.MobilityAction.STOP ) - self.progressbar.stop() diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index d9508681..e56c55ec 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -51,10 +51,10 @@ class Menubar(tk.Menu): label="Open...", command=self.menuaction.file_open_xml, accelerator="Ctrl+O" ) self.app.bind_all("", self.menuaction.file_open_xml) - menu.add_command(label="Reload", underline=0, state=tk.DISABLED) menu.add_command( label="Save", accelerator="Ctrl+S", command=self.menuaction.file_save_as_xml ) + menu.add_command(label="Reload", underline=0, state=tk.DISABLED) self.app.bind_all("", self.menuaction.file_save_as_xml) menu.add_separator() menu.add_command(label="Export Python script...", state=tk.DISABLED) From 554028ad5ce56ab368b6a94b5663430f20dc3993 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 12:11:11 -0800 Subject: [PATCH 294/462] display mobility player context for mobility configured nodes during runtime --- coretk/coretk/graph.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index c03d7b48..d45cce9b 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -77,9 +77,16 @@ class CanvasGraph(tk.Canvas): context.add_command( label="WLAN Config", command=canvas_node.show_wlan_config ) - context.add_command( - label="Mobility Config", command=canvas_node.show_mobility_config - ) + if self.master.core.is_runtime(): + if canvas_node.core_node.id in self.master.core.mobility_players: + context.add_command( + label="Mobility Player", + command=canvas_node.show_mobility_player, + ) + else: + context.add_command( + label="Mobility Config", command=canvas_node.show_mobility_config + ) if node.type == NodeType.EMANE: context.add_command( label="EMANE Config", command=canvas_node.show_emane_config @@ -688,6 +695,9 @@ class CanvasNode: dialog = MobilityConfigDialog(self.app, self.app, self) dialog.show() + def show_mobility_player(self): + self.canvas.context = None + def show_emane_config(self): self.canvas.context = None dialog = EmaneConfigDialog(self.app, self.app, self) From 5708dbd083378409abe5a00578c0f3a207d57bd4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 12:16:56 -0800 Subject: [PATCH 295/462] fixed issue when deleting node without a link tuple --- coretk/coretk/appconfig.py | 7 ++++--- coretk/coretk/{ => data}/backgrounds/sample1-bg.gif | Bin coretk/coretk/{ => data}/backgrounds/sample4-bg.jpg | Bin coretk/coretk/{ => data}/icons/OVS.gif | Bin coretk/coretk/{ => data}/icons/antenna.gif | Bin coretk/coretk/{ => data}/icons/core-icon.png | Bin coretk/coretk/{ => data}/icons/docker.png | Bin coretk/coretk/{ => data}/icons/document-new.gif | Bin .../coretk/{ => data}/icons/document-properties.gif | Bin coretk/coretk/{ => data}/icons/document-save.gif | Bin coretk/coretk/{ => data}/icons/edit-delete.gif | Bin coretk/coretk/{ => data}/icons/edit-node.png | Bin coretk/coretk/{ => data}/icons/emane.png | Bin coretk/coretk/{ => data}/icons/fileopen.gif | Bin coretk/coretk/{ => data}/icons/host.png | Bin coretk/coretk/{ => data}/icons/hub.png | Bin coretk/coretk/{ => data}/icons/lanswitch.png | Bin coretk/coretk/{ => data}/icons/link.png | Bin coretk/coretk/{ => data}/icons/lxc.png | Bin coretk/coretk/{ => data}/icons/marker.png | Bin coretk/coretk/{ => data}/icons/mdr.png | Bin coretk/coretk/{ => data}/icons/observe.gif | Bin coretk/coretk/{ => data}/icons/oval.png | Bin coretk/coretk/{ => data}/icons/pause.png | Bin coretk/coretk/{ => data}/icons/pc.png | Bin coretk/coretk/{ => data}/icons/plot.gif | Bin coretk/coretk/{ => data}/icons/prouter.png | Bin coretk/coretk/{ => data}/icons/rectangle.png | Bin coretk/coretk/{ => data}/icons/rj45.png | Bin coretk/coretk/{ => data}/icons/router.png | Bin coretk/coretk/{ => data}/icons/run.png | Bin coretk/coretk/{ => data}/icons/select.png | Bin coretk/coretk/{ => data}/icons/start.png | Bin coretk/coretk/{ => data}/icons/stop.png | Bin coretk/coretk/{ => data}/icons/text.png | Bin coretk/coretk/{ => data}/icons/tunnel.gif | Bin coretk/coretk/{ => data}/icons/twonode.png | Bin coretk/coretk/{ => data}/icons/wlan.png | Bin coretk/coretk/{ => data}/oldicons/docker.gif | Bin coretk/coretk/{ => data}/oldicons/emane.gif | Bin coretk/coretk/{ => data}/oldicons/host.gif | Bin coretk/coretk/{ => data}/oldicons/hub.gif | Bin coretk/coretk/{ => data}/oldicons/lanswitch.gif | Bin coretk/coretk/{ => data}/oldicons/link.gif | Bin coretk/coretk/{ => data}/oldicons/lxc.gif | Bin coretk/coretk/{ => data}/oldicons/marker.gif | Bin coretk/coretk/{ => data}/oldicons/mdr.gif | Bin coretk/coretk/{ => data}/oldicons/oval.gif | Bin coretk/coretk/{ => data}/oldicons/pc.gif | Bin coretk/coretk/{ => data}/oldicons/rectangle.gif | Bin coretk/coretk/{ => data}/oldicons/rj45.gif | Bin coretk/coretk/{ => data}/oldicons/router.gif | Bin coretk/coretk/{ => data}/oldicons/router_green.gif | Bin coretk/coretk/{ => data}/oldicons/run.gif | Bin coretk/coretk/{ => data}/oldicons/select.gif | Bin coretk/coretk/{ => data}/oldicons/start.gif | Bin coretk/coretk/{ => data}/oldicons/stop.gif | Bin coretk/coretk/{ => data}/oldicons/text.gif | Bin coretk/coretk/{ => data}/oldicons/twonode.gif | Bin coretk/coretk/{ => data}/oldicons/wlan.gif | Bin .../coretk/{ => data}/todelete/servicefileconfig.py | 0 .../coretk/{ => data}/todelete/servicenodeconfig.py | 0 coretk/coretk/{ => data}/xmls/sample1.xml | 0 coretk/coretk/nodedelete.py | 3 ++- 64 files changed, 6 insertions(+), 4 deletions(-) rename coretk/coretk/{ => data}/backgrounds/sample1-bg.gif (100%) rename coretk/coretk/{ => data}/backgrounds/sample4-bg.jpg (100%) rename coretk/coretk/{ => data}/icons/OVS.gif (100%) rename coretk/coretk/{ => data}/icons/antenna.gif (100%) rename coretk/coretk/{ => data}/icons/core-icon.png (100%) rename coretk/coretk/{ => data}/icons/docker.png (100%) rename coretk/coretk/{ => data}/icons/document-new.gif (100%) rename coretk/coretk/{ => data}/icons/document-properties.gif (100%) rename coretk/coretk/{ => data}/icons/document-save.gif (100%) rename coretk/coretk/{ => data}/icons/edit-delete.gif (100%) rename coretk/coretk/{ => data}/icons/edit-node.png (100%) rename coretk/coretk/{ => data}/icons/emane.png (100%) rename coretk/coretk/{ => data}/icons/fileopen.gif (100%) rename coretk/coretk/{ => data}/icons/host.png (100%) rename coretk/coretk/{ => data}/icons/hub.png (100%) rename coretk/coretk/{ => data}/icons/lanswitch.png (100%) rename coretk/coretk/{ => data}/icons/link.png (100%) rename coretk/coretk/{ => data}/icons/lxc.png (100%) rename coretk/coretk/{ => data}/icons/marker.png (100%) rename coretk/coretk/{ => data}/icons/mdr.png (100%) rename coretk/coretk/{ => data}/icons/observe.gif (100%) rename coretk/coretk/{ => data}/icons/oval.png (100%) rename coretk/coretk/{ => data}/icons/pause.png (100%) rename coretk/coretk/{ => data}/icons/pc.png (100%) rename coretk/coretk/{ => data}/icons/plot.gif (100%) rename coretk/coretk/{ => data}/icons/prouter.png (100%) rename coretk/coretk/{ => data}/icons/rectangle.png (100%) rename coretk/coretk/{ => data}/icons/rj45.png (100%) rename coretk/coretk/{ => data}/icons/router.png (100%) rename coretk/coretk/{ => data}/icons/run.png (100%) rename coretk/coretk/{ => data}/icons/select.png (100%) rename coretk/coretk/{ => data}/icons/start.png (100%) rename coretk/coretk/{ => data}/icons/stop.png (100%) rename coretk/coretk/{ => data}/icons/text.png (100%) rename coretk/coretk/{ => data}/icons/tunnel.gif (100%) rename coretk/coretk/{ => data}/icons/twonode.png (100%) rename coretk/coretk/{ => data}/icons/wlan.png (100%) rename coretk/coretk/{ => data}/oldicons/docker.gif (100%) rename coretk/coretk/{ => data}/oldicons/emane.gif (100%) rename coretk/coretk/{ => data}/oldicons/host.gif (100%) rename coretk/coretk/{ => data}/oldicons/hub.gif (100%) rename coretk/coretk/{ => data}/oldicons/lanswitch.gif (100%) rename coretk/coretk/{ => data}/oldicons/link.gif (100%) rename coretk/coretk/{ => data}/oldicons/lxc.gif (100%) rename coretk/coretk/{ => data}/oldicons/marker.gif (100%) rename coretk/coretk/{ => data}/oldicons/mdr.gif (100%) rename coretk/coretk/{ => data}/oldicons/oval.gif (100%) rename coretk/coretk/{ => data}/oldicons/pc.gif (100%) rename coretk/coretk/{ => data}/oldicons/rectangle.gif (100%) rename coretk/coretk/{ => data}/oldicons/rj45.gif (100%) rename coretk/coretk/{ => data}/oldicons/router.gif (100%) rename coretk/coretk/{ => data}/oldicons/router_green.gif (100%) rename coretk/coretk/{ => data}/oldicons/run.gif (100%) rename coretk/coretk/{ => data}/oldicons/select.gif (100%) rename coretk/coretk/{ => data}/oldicons/start.gif (100%) rename coretk/coretk/{ => data}/oldicons/stop.gif (100%) rename coretk/coretk/{ => data}/oldicons/text.gif (100%) rename coretk/coretk/{ => data}/oldicons/twonode.gif (100%) rename coretk/coretk/{ => data}/oldicons/wlan.gif (100%) rename coretk/coretk/{ => data}/todelete/servicefileconfig.py (100%) rename coretk/coretk/{ => data}/todelete/servicenodeconfig.py (100%) rename coretk/coretk/{ => data}/xmls/sample1.xml (100%) diff --git a/coretk/coretk/appconfig.py b/coretk/coretk/appconfig.py index 74d52553..a85e5991 100644 --- a/coretk/coretk/appconfig.py +++ b/coretk/coretk/appconfig.py @@ -18,9 +18,10 @@ XML_PATH = HOME_PATH.joinpath("xml") CONFIG_PATH = HOME_PATH.joinpath("gui.yaml") # local paths -LOCAL_ICONS_PATH = Path(__file__).parent.joinpath("icons").absolute() -LOCAL_BACKGROUND_PATH = Path(__file__).parent.joinpath("backgrounds").absolute() -LOCAL_XMLS_PATH = Path(__file__).parent.joinpath("xmls").absolute() +DATA_PATH = Path(__file__).parent.joinpath("data") +LOCAL_ICONS_PATH = DATA_PATH.joinpath("icons").absolute() +LOCAL_BACKGROUND_PATH = DATA_PATH.joinpath("backgrounds").absolute() +LOCAL_XMLS_PATH = DATA_PATH.joinpath("xmls").absolute() # configuration data TERMINALS = [ diff --git a/coretk/coretk/backgrounds/sample1-bg.gif b/coretk/coretk/data/backgrounds/sample1-bg.gif similarity index 100% rename from coretk/coretk/backgrounds/sample1-bg.gif rename to coretk/coretk/data/backgrounds/sample1-bg.gif diff --git a/coretk/coretk/backgrounds/sample4-bg.jpg b/coretk/coretk/data/backgrounds/sample4-bg.jpg similarity index 100% rename from coretk/coretk/backgrounds/sample4-bg.jpg rename to coretk/coretk/data/backgrounds/sample4-bg.jpg diff --git a/coretk/coretk/icons/OVS.gif b/coretk/coretk/data/icons/OVS.gif similarity index 100% rename from coretk/coretk/icons/OVS.gif rename to coretk/coretk/data/icons/OVS.gif diff --git a/coretk/coretk/icons/antenna.gif b/coretk/coretk/data/icons/antenna.gif similarity index 100% rename from coretk/coretk/icons/antenna.gif rename to coretk/coretk/data/icons/antenna.gif diff --git a/coretk/coretk/icons/core-icon.png b/coretk/coretk/data/icons/core-icon.png similarity index 100% rename from coretk/coretk/icons/core-icon.png rename to coretk/coretk/data/icons/core-icon.png diff --git a/coretk/coretk/icons/docker.png b/coretk/coretk/data/icons/docker.png similarity index 100% rename from coretk/coretk/icons/docker.png rename to coretk/coretk/data/icons/docker.png diff --git a/coretk/coretk/icons/document-new.gif b/coretk/coretk/data/icons/document-new.gif similarity index 100% rename from coretk/coretk/icons/document-new.gif rename to coretk/coretk/data/icons/document-new.gif diff --git a/coretk/coretk/icons/document-properties.gif b/coretk/coretk/data/icons/document-properties.gif similarity index 100% rename from coretk/coretk/icons/document-properties.gif rename to coretk/coretk/data/icons/document-properties.gif diff --git a/coretk/coretk/icons/document-save.gif b/coretk/coretk/data/icons/document-save.gif similarity index 100% rename from coretk/coretk/icons/document-save.gif rename to coretk/coretk/data/icons/document-save.gif diff --git a/coretk/coretk/icons/edit-delete.gif b/coretk/coretk/data/icons/edit-delete.gif similarity index 100% rename from coretk/coretk/icons/edit-delete.gif rename to coretk/coretk/data/icons/edit-delete.gif diff --git a/coretk/coretk/icons/edit-node.png b/coretk/coretk/data/icons/edit-node.png similarity index 100% rename from coretk/coretk/icons/edit-node.png rename to coretk/coretk/data/icons/edit-node.png diff --git a/coretk/coretk/icons/emane.png b/coretk/coretk/data/icons/emane.png similarity index 100% rename from coretk/coretk/icons/emane.png rename to coretk/coretk/data/icons/emane.png diff --git a/coretk/coretk/icons/fileopen.gif b/coretk/coretk/data/icons/fileopen.gif similarity index 100% rename from coretk/coretk/icons/fileopen.gif rename to coretk/coretk/data/icons/fileopen.gif diff --git a/coretk/coretk/icons/host.png b/coretk/coretk/data/icons/host.png similarity index 100% rename from coretk/coretk/icons/host.png rename to coretk/coretk/data/icons/host.png diff --git a/coretk/coretk/icons/hub.png b/coretk/coretk/data/icons/hub.png similarity index 100% rename from coretk/coretk/icons/hub.png rename to coretk/coretk/data/icons/hub.png diff --git a/coretk/coretk/icons/lanswitch.png b/coretk/coretk/data/icons/lanswitch.png similarity index 100% rename from coretk/coretk/icons/lanswitch.png rename to coretk/coretk/data/icons/lanswitch.png diff --git a/coretk/coretk/icons/link.png b/coretk/coretk/data/icons/link.png similarity index 100% rename from coretk/coretk/icons/link.png rename to coretk/coretk/data/icons/link.png diff --git a/coretk/coretk/icons/lxc.png b/coretk/coretk/data/icons/lxc.png similarity index 100% rename from coretk/coretk/icons/lxc.png rename to coretk/coretk/data/icons/lxc.png diff --git a/coretk/coretk/icons/marker.png b/coretk/coretk/data/icons/marker.png similarity index 100% rename from coretk/coretk/icons/marker.png rename to coretk/coretk/data/icons/marker.png diff --git a/coretk/coretk/icons/mdr.png b/coretk/coretk/data/icons/mdr.png similarity index 100% rename from coretk/coretk/icons/mdr.png rename to coretk/coretk/data/icons/mdr.png diff --git a/coretk/coretk/icons/observe.gif b/coretk/coretk/data/icons/observe.gif similarity index 100% rename from coretk/coretk/icons/observe.gif rename to coretk/coretk/data/icons/observe.gif diff --git a/coretk/coretk/icons/oval.png b/coretk/coretk/data/icons/oval.png similarity index 100% rename from coretk/coretk/icons/oval.png rename to coretk/coretk/data/icons/oval.png diff --git a/coretk/coretk/icons/pause.png b/coretk/coretk/data/icons/pause.png similarity index 100% rename from coretk/coretk/icons/pause.png rename to coretk/coretk/data/icons/pause.png diff --git a/coretk/coretk/icons/pc.png b/coretk/coretk/data/icons/pc.png similarity index 100% rename from coretk/coretk/icons/pc.png rename to coretk/coretk/data/icons/pc.png diff --git a/coretk/coretk/icons/plot.gif b/coretk/coretk/data/icons/plot.gif similarity index 100% rename from coretk/coretk/icons/plot.gif rename to coretk/coretk/data/icons/plot.gif diff --git a/coretk/coretk/icons/prouter.png b/coretk/coretk/data/icons/prouter.png similarity index 100% rename from coretk/coretk/icons/prouter.png rename to coretk/coretk/data/icons/prouter.png diff --git a/coretk/coretk/icons/rectangle.png b/coretk/coretk/data/icons/rectangle.png similarity index 100% rename from coretk/coretk/icons/rectangle.png rename to coretk/coretk/data/icons/rectangle.png diff --git a/coretk/coretk/icons/rj45.png b/coretk/coretk/data/icons/rj45.png similarity index 100% rename from coretk/coretk/icons/rj45.png rename to coretk/coretk/data/icons/rj45.png diff --git a/coretk/coretk/icons/router.png b/coretk/coretk/data/icons/router.png similarity index 100% rename from coretk/coretk/icons/router.png rename to coretk/coretk/data/icons/router.png diff --git a/coretk/coretk/icons/run.png b/coretk/coretk/data/icons/run.png similarity index 100% rename from coretk/coretk/icons/run.png rename to coretk/coretk/data/icons/run.png diff --git a/coretk/coretk/icons/select.png b/coretk/coretk/data/icons/select.png similarity index 100% rename from coretk/coretk/icons/select.png rename to coretk/coretk/data/icons/select.png diff --git a/coretk/coretk/icons/start.png b/coretk/coretk/data/icons/start.png similarity index 100% rename from coretk/coretk/icons/start.png rename to coretk/coretk/data/icons/start.png diff --git a/coretk/coretk/icons/stop.png b/coretk/coretk/data/icons/stop.png similarity index 100% rename from coretk/coretk/icons/stop.png rename to coretk/coretk/data/icons/stop.png diff --git a/coretk/coretk/icons/text.png b/coretk/coretk/data/icons/text.png similarity index 100% rename from coretk/coretk/icons/text.png rename to coretk/coretk/data/icons/text.png diff --git a/coretk/coretk/icons/tunnel.gif b/coretk/coretk/data/icons/tunnel.gif similarity index 100% rename from coretk/coretk/icons/tunnel.gif rename to coretk/coretk/data/icons/tunnel.gif diff --git a/coretk/coretk/icons/twonode.png b/coretk/coretk/data/icons/twonode.png similarity index 100% rename from coretk/coretk/icons/twonode.png rename to coretk/coretk/data/icons/twonode.png diff --git a/coretk/coretk/icons/wlan.png b/coretk/coretk/data/icons/wlan.png similarity index 100% rename from coretk/coretk/icons/wlan.png rename to coretk/coretk/data/icons/wlan.png diff --git a/coretk/coretk/oldicons/docker.gif b/coretk/coretk/data/oldicons/docker.gif similarity index 100% rename from coretk/coretk/oldicons/docker.gif rename to coretk/coretk/data/oldicons/docker.gif diff --git a/coretk/coretk/oldicons/emane.gif b/coretk/coretk/data/oldicons/emane.gif similarity index 100% rename from coretk/coretk/oldicons/emane.gif rename to coretk/coretk/data/oldicons/emane.gif diff --git a/coretk/coretk/oldicons/host.gif b/coretk/coretk/data/oldicons/host.gif similarity index 100% rename from coretk/coretk/oldicons/host.gif rename to coretk/coretk/data/oldicons/host.gif diff --git a/coretk/coretk/oldicons/hub.gif b/coretk/coretk/data/oldicons/hub.gif similarity index 100% rename from coretk/coretk/oldicons/hub.gif rename to coretk/coretk/data/oldicons/hub.gif diff --git a/coretk/coretk/oldicons/lanswitch.gif b/coretk/coretk/data/oldicons/lanswitch.gif similarity index 100% rename from coretk/coretk/oldicons/lanswitch.gif rename to coretk/coretk/data/oldicons/lanswitch.gif diff --git a/coretk/coretk/oldicons/link.gif b/coretk/coretk/data/oldicons/link.gif similarity index 100% rename from coretk/coretk/oldicons/link.gif rename to coretk/coretk/data/oldicons/link.gif diff --git a/coretk/coretk/oldicons/lxc.gif b/coretk/coretk/data/oldicons/lxc.gif similarity index 100% rename from coretk/coretk/oldicons/lxc.gif rename to coretk/coretk/data/oldicons/lxc.gif diff --git a/coretk/coretk/oldicons/marker.gif b/coretk/coretk/data/oldicons/marker.gif similarity index 100% rename from coretk/coretk/oldicons/marker.gif rename to coretk/coretk/data/oldicons/marker.gif diff --git a/coretk/coretk/oldicons/mdr.gif b/coretk/coretk/data/oldicons/mdr.gif similarity index 100% rename from coretk/coretk/oldicons/mdr.gif rename to coretk/coretk/data/oldicons/mdr.gif diff --git a/coretk/coretk/oldicons/oval.gif b/coretk/coretk/data/oldicons/oval.gif similarity index 100% rename from coretk/coretk/oldicons/oval.gif rename to coretk/coretk/data/oldicons/oval.gif diff --git a/coretk/coretk/oldicons/pc.gif b/coretk/coretk/data/oldicons/pc.gif similarity index 100% rename from coretk/coretk/oldicons/pc.gif rename to coretk/coretk/data/oldicons/pc.gif diff --git a/coretk/coretk/oldicons/rectangle.gif b/coretk/coretk/data/oldicons/rectangle.gif similarity index 100% rename from coretk/coretk/oldicons/rectangle.gif rename to coretk/coretk/data/oldicons/rectangle.gif diff --git a/coretk/coretk/oldicons/rj45.gif b/coretk/coretk/data/oldicons/rj45.gif similarity index 100% rename from coretk/coretk/oldicons/rj45.gif rename to coretk/coretk/data/oldicons/rj45.gif diff --git a/coretk/coretk/oldicons/router.gif b/coretk/coretk/data/oldicons/router.gif similarity index 100% rename from coretk/coretk/oldicons/router.gif rename to coretk/coretk/data/oldicons/router.gif diff --git a/coretk/coretk/oldicons/router_green.gif b/coretk/coretk/data/oldicons/router_green.gif similarity index 100% rename from coretk/coretk/oldicons/router_green.gif rename to coretk/coretk/data/oldicons/router_green.gif diff --git a/coretk/coretk/oldicons/run.gif b/coretk/coretk/data/oldicons/run.gif similarity index 100% rename from coretk/coretk/oldicons/run.gif rename to coretk/coretk/data/oldicons/run.gif diff --git a/coretk/coretk/oldicons/select.gif b/coretk/coretk/data/oldicons/select.gif similarity index 100% rename from coretk/coretk/oldicons/select.gif rename to coretk/coretk/data/oldicons/select.gif diff --git a/coretk/coretk/oldicons/start.gif b/coretk/coretk/data/oldicons/start.gif similarity index 100% rename from coretk/coretk/oldicons/start.gif rename to coretk/coretk/data/oldicons/start.gif diff --git a/coretk/coretk/oldicons/stop.gif b/coretk/coretk/data/oldicons/stop.gif similarity index 100% rename from coretk/coretk/oldicons/stop.gif rename to coretk/coretk/data/oldicons/stop.gif diff --git a/coretk/coretk/oldicons/text.gif b/coretk/coretk/data/oldicons/text.gif similarity index 100% rename from coretk/coretk/oldicons/text.gif rename to coretk/coretk/data/oldicons/text.gif diff --git a/coretk/coretk/oldicons/twonode.gif b/coretk/coretk/data/oldicons/twonode.gif similarity index 100% rename from coretk/coretk/oldicons/twonode.gif rename to coretk/coretk/data/oldicons/twonode.gif diff --git a/coretk/coretk/oldicons/wlan.gif b/coretk/coretk/data/oldicons/wlan.gif similarity index 100% rename from coretk/coretk/oldicons/wlan.gif rename to coretk/coretk/data/oldicons/wlan.gif diff --git a/coretk/coretk/todelete/servicefileconfig.py b/coretk/coretk/data/todelete/servicefileconfig.py similarity index 100% rename from coretk/coretk/todelete/servicefileconfig.py rename to coretk/coretk/data/todelete/servicefileconfig.py diff --git a/coretk/coretk/todelete/servicenodeconfig.py b/coretk/coretk/data/todelete/servicenodeconfig.py similarity index 100% rename from coretk/coretk/todelete/servicenodeconfig.py rename to coretk/coretk/data/todelete/servicenodeconfig.py diff --git a/coretk/coretk/xmls/sample1.xml b/coretk/coretk/data/xmls/sample1.xml similarity index 100% rename from coretk/coretk/xmls/sample1.xml rename to coretk/coretk/data/xmls/sample1.xml diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index 92844aac..f52731e4 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -75,7 +75,8 @@ class CanvasComponentManagement: neighbor = self.app.canvas_nodes[neighbor_id] if neighbor.core_node.type != core_pb2.NodeType.WIRELESS_LAN: neighbor.antenna_draw.delete_antenna() - for link_tuple in node_to_wlink[canvas_node.core_node.id]: + + for link_tuple in node_to_wlink.get(canvas_node.core_node.id, []): nid_one, nid_two = link_tuple if link_tuple in self.canvas.wireless_draw.map: self.canvas.delete(self.canvas.wireless_draw.map[link_tuple]) From 354d227cb3a6badc24c741327ac8a084c6620e30 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 12:20:00 -0800 Subject: [PATCH 296/462] removed old service config code --- .../coretk/data/todelete/servicefileconfig.py | 37 -------- .../coretk/data/todelete/servicenodeconfig.py | 85 ------------------- 2 files changed, 122 deletions(-) delete mode 100644 coretk/coretk/data/todelete/servicefileconfig.py delete mode 100644 coretk/coretk/data/todelete/servicenodeconfig.py diff --git a/coretk/coretk/data/todelete/servicefileconfig.py b/coretk/coretk/data/todelete/servicefileconfig.py deleted file mode 100644 index 06a134cd..00000000 --- a/coretk/coretk/data/todelete/servicefileconfig.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -service file configuration -""" - - -class ServiceFileConfig: - def __init__(self): - # dict(node_id:dict(service:dict(filename, data))) - self.configurations = {} - - # def set_service_configs(self, node_id, service_name, file_configs): - # """ - # store file configs - # - # :param int node_id: node id - # :param str service_name: service name - # :param dict(str, str) file_configs: map of service file to its data - # :return: nothing - # """ - # for key, value in file_configs.items(): - # self.configurations[node_id][service_name][key] = value - - def set_custom_service_file_config(self, node_id, service_name, file_name, data): - """ - store file config - - :param int node_id: node id - :param str service_name: service name - :param str file_name: file name - :param str data: data - :return: nothing - """ - if node_id not in self.configurations: - self.configurations[node_id] = {} - if service_name not in self.configurations[node_id]: - self.configurations[node_id][service_name] = {} - self.configurations[node_id][service_name][file_name] = data diff --git a/coretk/coretk/data/todelete/servicenodeconfig.py b/coretk/coretk/data/todelete/servicenodeconfig.py deleted file mode 100644 index 3473e1d8..00000000 --- a/coretk/coretk/data/todelete/servicenodeconfig.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -service node configuration -""" -import logging -from tkinter import messagebox - -import grpc - - -class ServiceNodeConfig: - def __init__(self, app): - self.app = app - # dict(node_id:dict(service:node_service_config_proto)) - # maps node to all of its service configuration - self.configurations = {} - # dict(node_id:set(str)) - # maps node to current configurations - self.current_services = {} - self.default_services = {} - - # todo rewrite, no need self.default services - def node_default_services_configuration(self, node_id, node_model): - """ - set the default configurations for the default services of a node - - :param coretk.graph.CanvasNode canvas_node: canvas node object - :return: nothing - """ - session_id = self.app.core.session_id - client = self.app.core.client - - if len(self.default_services) == 0: - response = client.get_service_defaults(session_id) - logging.info("session default services: %s", response) - for default in response.defaults: - self.default_services[default.node_type] = default.services - - self.configurations[node_id] = {} - - self.current_services[node_id] = set() - for default in self.default_services[node_model]: - response = client.get_node_service(session_id, node_id, default) - logging.info( - "servicenodeconfig.py get node service (%s), result: %s", - node_id, - response, - ) - self.configurations[node_id][default] = response.service - self.current_services[node_id].add(default) - - def node_new_service_configuration(self, node_id, service_name): - """ - store node's configuration if a new service is added from the GUI - - :param int node_id: node id - :param str service_name: service name - :return: nothing - """ - try: - config = self.app.core.get_node_service(node_id, service_name) - except grpc.RpcError: - messagebox.showerror("Service problem", "Service not found") - return False - if node_id not in self.configurations: - self.configurations[node_id] = {} - if node_id not in self.current_services: - self.current_services[node_id] = set() - if service_name not in self.configurations[node_id]: - self.configurations[node_id][service_name] = config - self.current_services[node_id].add(service_name) - return True - - def node_custom_service_configuration(self, node_id, service_name): - self.configurations[node_id][service_name] = self.app.core.get_node_service( - node_id, service_name - ) - - def node_service_custom_configuration( - self, node_id, service_name, startups, validates, shutdowns - ): - self.app.core.set_node_service( - node_id, service_name, startups, validates, shutdowns - ) - config = self.app.core.get_node_service(node_id, service_name) - self.configurations[node_id][service_name] = config From d1db5e4b4ec00f96c9a03d2038a4b27834a0b0f1 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 12:55:39 -0800 Subject: [PATCH 297/462] added some data for node events to have a source field to help distinguish what originates from the gui or not --- coretk/coretk/coreclient.py | 23 +++++++++++------------ coretk/coretk/graph.py | 6 +++--- daemon/core/api/grpc/client.py | 9 +++++++-- daemon/core/api/grpc/server.py | 7 +++++-- daemon/core/emulator/data.py | 1 + daemon/core/nodes/base.py | 4 +++- daemon/proto/core/api/grpc/core.proto | 2 ++ 7 files changed, 32 insertions(+), 20 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index e6dfb77e..1671a0fe 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -141,15 +141,19 @@ class CoreClient: else: logging.warning("unknown session event: %s", session_event) elif event.HasField("node_event"): - node_event = event.node_event - node_id = node_event.node.id - x = node_event.node.position.x - y = node_event.node.position.y - canvas_node = self.canvas_nodes[node_id] - canvas_node.move(x, y) + self.handle_node_event(event.node_event) else: logging.info("unhandled event: %s", event) + def handle_node_event(self, event): + if event.source == "gui": + return + node_id = event.node.id + x = event.node.position.x + y = event.node.position.y + canvas_node = self.canvas_nodes[node_id] + canvas_node.move(x, y) + def handle_throughputs(self, event): interface_throughputs = event.interface_throughputs for i in interface_throughputs: @@ -164,9 +168,6 @@ class CoreClient: ) def join_session(self, session_id, query_location=True): - # self.master.config(cursor="watch") - # self.master.update() - # update session and title self.session_id = session_id self.master.title(f"CORE Session({self.session_id})") @@ -253,7 +254,6 @@ class CoreClient: self.app.toolbar.runtime_frame.tkraise() else: self.app.toolbar.design_frame.tkraise() - # self.master.config(cursor="") self.app.statusbar.progress_bar.stop() def is_runtime(self): @@ -321,8 +321,7 @@ class CoreClient: def edit_node(self, node_id, x, y): position = core_pb2.Position(x=x, y=y) - response = self.client.edit_node(self.session_id, node_id, position) - logging.info("updated node id %s: %s", node_id, response) + self.client.edit_node(self.session_id, node_id, position, source="gui") def start_session(self): nodes = [x.core_node for x in self.canvas_nodes.values()] diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index d45cce9b..7bfe7ed3 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -600,6 +600,9 @@ class CanvasNode: y_offset = y - old_y self.core_node.position.x = x self.core_node.position.y = y + self.canvas.move(self.id, x_offset, y_offset) + self.canvas.move(self.text_id, x_offset, y_offset) + self.antenna_draw.update_antennas_position(x_offset, y_offset) for edge in self.edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) if edge.src == self.id: @@ -608,9 +611,6 @@ class CanvasNode: self.canvas.coords(edge.id, x1, y1, x_offset, y_offset) edge.link_info.recalculate_info() self.canvas.helper.update_wlan_connection(old_x, old_y, x, y, self.wlans) - self.canvas.move(self.id, x_offset, y_offset) - self.canvas.move(self.text_id, x_offset, y_offset) - self.antenna_draw.update_antennas_position(x_offset, y_offset) def on_enter(self, event): if self.app.core.is_runtime() and self.app.core.observer: diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index ceec0448..05380b79 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -428,7 +428,7 @@ class CoreGrpcClient: request = core_pb2.GetNodeRequest(session_id=session_id, node_id=node_id) return self.stub.GetNode(request) - def edit_node(self, session_id, node_id, position, icon=None): + def edit_node(self, session_id, node_id, position, icon=None, source=None): """ Edit a node, currently only changes position. @@ -436,12 +436,17 @@ class CoreGrpcClient: :param int node_id: node id :param core_pb2.Position position: position to set node to :param str icon: path to icon for gui to use for node + :param str source: application source editing node :return: response with result of success or failure :rtype: core_pb2.EditNodeResponse :raises grpc.RpcError: when session or node doesn't exist """ request = core_pb2.EditNodeRequest( - session_id=session_id, node_id=node_id, position=position, icon=icon + session_id=session_id, + node_id=node_id, + position=position, + icon=icon, + source=source, ) return self.stub.EditNode(request) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index c0fe1d94..a914d617 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -504,7 +504,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): position=position, services=services, ) - return core_pb2.NodeEvent(node=node_proto) + return core_pb2.NodeEvent(node=node_proto, source=event.source) def _handle_link_event(self, event): """ @@ -800,7 +800,10 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): result = True try: session.edit_node(node.id, options) - node_data = node.data(0) + source = None + if request.source: + source = request.source + node_data = node.data(0, source=source) session.broadcast_node(node_data) except CoreError: result = False diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index ba0dd457..0ed1fa67 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -76,6 +76,7 @@ NodeData = collections.namedtuple( "altitude", "icon", "opaque", + "source", ], ) NodeData.__new__.__defaults__ = (None,) * len(NodeData._fields) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 72fc0fe1..a663741e 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -176,7 +176,7 @@ class NodeBase: self.ifindex += 1 return ifindex - def data(self, message_type, lat=None, lon=None, alt=None): + def data(self, message_type, lat=None, lon=None, alt=None, source=None): """ Build a data object for this node. @@ -184,6 +184,7 @@ class NodeBase: :param str lat: latitude :param str lon: longitude :param str alt: altitude + :param str source: source of node data :return: node data object :rtype: core.emulator.data.NodeData """ @@ -217,6 +218,7 @@ class NodeBase: model=model, server=server, services=services, + source=source, ) return node_data diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index ac7cc2ed..57bbf3f4 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -298,6 +298,7 @@ message Event { message NodeEvent { Node node = 1; + string source = 2; } message LinkEvent { @@ -378,6 +379,7 @@ message EditNodeRequest { int32 node_id = 2; Position position = 3; string icon = 4; + string source = 5; } message EditNodeResponse { From f3ca5682ac9e4700d50f65db57296aec2c326974 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 27 Nov 2019 13:15:04 -0800 Subject: [PATCH 298/462] start on shape drawing --- coretk/coretk/graph.py | 13 ++++++++++++- coretk/coretk/shape.py | 15 +++++++++++++++ coretk/coretk/toolbar.py | 1 + 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 coretk/coretk/shape.py diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 518245d3..71774826 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -17,6 +17,7 @@ from coretk.images import Images from coretk.linkinfo import LinkInfo, Throughput from coretk.nodedelete import CanvasComponentManagement from coretk.nodeutils import NodeUtils +from coretk.shape import Shape from coretk.wirelessconnection import WirelessConnection NODE_TEXT_OFFSET = 5 @@ -27,7 +28,8 @@ class GraphMode(enum.Enum): EDGE = 1 PICKNODE = 2 NODE = 3 - OTHER = 4 + ANNOTATION = 4 + OTHER = 5 class ScaleOption(enum.Enum): @@ -38,12 +40,18 @@ class ScaleOption(enum.Enum): TILED = 4 +class ShapeType(enum.Enum): + OVAL = 0 + RECTANGLE = 1 + + class CanvasGraph(tk.Canvas): def __init__(self, master, core, width, height, cnf=None, **kwargs): if cnf is None: cnf = {} kwargs["highlightthickness"] = 0 super().__init__(master, cnf, **kwargs) + self.app = master self.mode = GraphMode.SELECT self.selected = None self.node_draw = None @@ -317,6 +325,9 @@ class CanvasGraph(tk.Canvas): if self.mode == GraphMode.EDGE and is_node: x, y = self.coords(selected) self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) + if self.mode == GraphMode.ANNOTATION: + shape = Shape(self.app, self, event.x, event.y) + print(shape) def click_motion(self, event): """ diff --git a/coretk/coretk/shape.py b/coretk/coretk/shape.py new file mode 100644 index 00000000..29c4a4c4 --- /dev/null +++ b/coretk/coretk/shape.py @@ -0,0 +1,15 @@ +""" +class for shapes +""" +# from coretk.images import ImageEnum, Images + + +class Shape: + def __init__(self, app, canvas, topleft_x, topleft_y): + self.app = app + self.canvas = canvas + self.x0 = topleft_x + self.y0 = topleft_y + self.id = self.canvas.create_oval( + topleft_x, topleft_y, topleft_x + 30, topleft_y + 30 + ) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index d1830084..c68c8f24 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -367,6 +367,7 @@ class Toolbar(ttk.Frame): self.hide_pickers() self.annotation_button.configure(image=image) self.annotation_button.image = image + self.app.canvas.mode = GraphMode.ANNOTATION def click_run_button(self): logging.debug("Click on RUN button") From 804b95d486de3ac21e218a965da68c34ff826703 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 13:23:45 -0800 Subject: [PATCH 299/462] consolidated node move logic to one function, used by both node events and node drag --- coretk/coretk/graph.py | 36 ++++++------------------------------ coretk/coretk/nodedelete.py | 4 +++- 2 files changed, 9 insertions(+), 31 deletions(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 7bfe7ed3..274073df 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -598,17 +598,18 @@ class CanvasNode: old_y = self.core_node.position.y x_offset = x - old_x y_offset = y - old_y - self.core_node.position.x = x - self.core_node.position.y = y + self.core_node.position.x = int(x) + self.core_node.position.y = int(y) self.canvas.move(self.id, x_offset, y_offset) self.canvas.move(self.text_id, x_offset, y_offset) self.antenna_draw.update_antennas_position(x_offset, y_offset) + self.canvas.canvas_management.node_drag(self, x_offset, y_offset) for edge in self.edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) if edge.src == self.id: - self.canvas.coords(edge.id, x_offset, y_offset, x2, y2) + self.canvas.coords(edge.id, x, y, x2, y2) else: - self.canvas.coords(edge.id, x1, y1, x_offset, y_offset) + self.canvas.coords(edge.id, x1, y1, x, y) edge.link_info.recalculate_info() self.canvas.helper.update_wlan_connection(old_x, old_y, x, y, self.wlans) @@ -650,32 +651,7 @@ class CanvasNode: if self.canvas.mode == GraphMode.EDGE or self.canvas.mode == GraphMode.NODE: return x, y = self.canvas.canvas_xy(event) - moving_x, moving_y = self.moving - offset_x, offset_y = x - moving_x, y - moving_y - self.moving = x, y - - old_x, old_y = self.canvas.coords(self.id) - self.canvas.move(self.id, offset_x, offset_y) - self.canvas.move(self.text_id, offset_x, offset_y) - self.antenna_draw.update_antennas_position(offset_x, offset_y) - self.canvas.canvas_management.node_drag(self, offset_x, offset_y) - - new_x, new_y = self.canvas.coords(self.id) - - if self.canvas.core.is_runtime(): - self.canvas.core.edit_node(self.core_node.id, int(new_x), int(new_y)) - - for edge in self.edges: - x1, y1, x2, y2 = self.canvas.coords(edge.id) - if x1 == old_x and y1 == old_y: - self.canvas.coords(edge.id, new_x, new_y, x2, y2) - else: - self.canvas.coords(edge.id, x1, y1, new_x, new_y) - edge.link_info.recalculate_info() - - self.canvas.helper.update_wlan_connection( - old_x, old_y, new_x, new_y, self.wlans - ) + self.move(x, y) def select_multiple(self, event): self.canvas.canvas_management.node_select(self, True) diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index f52731e4..341f95ef 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -35,7 +35,9 @@ class CanvasComponentManagement: self.selected[canvas_node.id] = bbox_id def node_drag(self, canvas_node, offset_x, offset_y): - self.canvas.move(self.selected[canvas_node.id], offset_x, offset_y) + select_id = self.selected.get(canvas_node.id) + if select_id is not None: + self.canvas.move(select_id, offset_x, offset_y) def delete_current_bbox(self): for bbid in self.selected.values(): From 2fc8782360cbc50b97e3548a169e5fd63991d8e8 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 27 Nov 2019 13:58:34 -0800 Subject: [PATCH 300/462] shape --- coretk/coretk/graph.py | 8 ++++---- coretk/coretk/shape.py | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 71774826..eb18869b 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -366,9 +366,9 @@ class CanvasGraph(tk.Canvas): self.core.delete_graph_nodes(nodes) def add_node(self, x, y): - plot_id = self.find_all()[0] - logging.info("add node event: %s - %s", plot_id, self.selected) - if self.selected == plot_id: + canvas_id = self.find_all()[0] + logging.info("add node event: %s - %s", canvas_id, self.selected) + if self.selected == canvas_id: core_node = self.core.create_node( int(x), int(y), self.node_draw.node_type, self.node_draw.model ) @@ -651,7 +651,7 @@ class CanvasNode: self.moving = None def motion(self, event): - if self.canvas.mode == GraphMode.EDGE or self.canvas.mode == GraphMode.NODE: + if self.canvas.mode == GraphMode.EDGE: return x, y = self.canvas.canvas_xy(event) moving_x, moving_y = self.moving diff --git a/coretk/coretk/shape.py b/coretk/coretk/shape.py index 29c4a4c4..7b9df97b 100644 --- a/coretk/coretk/shape.py +++ b/coretk/coretk/shape.py @@ -10,6 +10,7 @@ class Shape: self.canvas = canvas self.x0 = topleft_x self.y0 = topleft_y + # imageenum = self.app.toolbar self.id = self.canvas.create_oval( topleft_x, topleft_y, topleft_x + 30, topleft_y + 30 ) From 3c7bf57b5cf736c1c45e7ea921a9cd33a0ca0c53 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 14:25:29 -0800 Subject: [PATCH 301/462] simplified select logic to check against known nodes and modified get_selected to avoid returning the canvas id --- coretk/coretk/graph.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 274073df..d9b9e1e3 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -238,19 +238,15 @@ class CanvasGraph(tk.Canvas): :return: the item that the mouse point to """ overlapping = self.find_overlapping(event.x, event.y, event.x, event.y) - nodes = set(self.find_withtag("node")) selected = None for _id in overlapping: if self.drawing_edge and self.drawing_edge.id == _id: continue - if _id in nodes: + if _id in self.nodes: selected = _id break - if selected is None: - selected = _id - return selected def click_release(self, event): @@ -362,9 +358,7 @@ class CanvasGraph(tk.Canvas): self.core.delete_graph_nodes(nodes) def add_node(self, x, y): - plot_id = self.find_all()[0] - logging.info("add node event: %s - %s", plot_id, self.selected) - if self.selected == plot_id: + if self.selected is None: core_node = self.core.create_node( int(x), int(y), self.node_draw.node_type, self.node_draw.model ) @@ -648,7 +642,7 @@ class CanvasNode: self.moving = None def motion(self, event): - if self.canvas.mode == GraphMode.EDGE or self.canvas.mode == GraphMode.NODE: + if self.canvas.mode == GraphMode.EDGE: return x, y = self.canvas.canvas_xy(event) self.move(x, y) From 693d7beeb77bef8c569c350dc497c69daa2434ae Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 15:40:54 -0800 Subject: [PATCH 302/462] updated remove antenna logic to simplify, updated wireless edges to be objects --- coretk/coretk/coreclient.py | 16 +++++++-- coretk/coretk/graph.py | 51 +++++++++++++++++++++++---- coretk/coretk/nodedelete.py | 53 ++++++----------------------- coretk/coretk/wirelessconnection.py | 51 --------------------------- 4 files changed, 69 insertions(+), 102 deletions(-) delete mode 100644 coretk/coretk/wirelessconnection.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 1671a0fe..fec00a73 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -121,13 +121,12 @@ class CoreClient: def handle_events(self, event): if event.HasField("link_event"): logging.info("link event: %s", event) - self.app.canvas.wireless_draw.handle_link_event(event.link_event) + self.handle_link_event(event.link_event) elif event.HasField("session_event"): logging.info("session event: %s", event) session_event = event.session_event if session_event.event <= core_pb2.SessionState.SHUTDOWN: self.state = event.session_event.event - # mobility start elif session_event.event in {7, 8, 9}: node_id = session_event.node_id dialog = self.mobility_players.get(node_id) @@ -145,6 +144,19 @@ class CoreClient: else: logging.info("unhandled event: %s", event) + def handle_link_event(self, event): + node_one_id = event.link.node_one_id + node_two_id = event.link.node_two_id + canvas_node_one = self.canvas_nodes[node_one_id] + canvas_node_two = self.canvas_nodes[node_two_id] + + if event.message_type == core_pb2.MessageType.ADD: + self.app.canvas.add_wireless_edge(canvas_node_one, canvas_node_two) + elif event.message_type == core_pb2.MessageType.DELETE: + self.app.canvas.delete_wireless_edge(canvas_node_one, canvas_node_two) + else: + logging.warning("unknown link event: %s", event.message_type) + def handle_node_event(self, event): if event.source == "gui": return diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index d9b9e1e3..6c38b83c 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -17,7 +17,6 @@ from coretk.images import Images from coretk.linkinfo import LinkInfo, Throughput from coretk.nodedelete import CanvasComponentManagement from coretk.nodeutils import NodeUtils -from coretk.wirelessconnection import WirelessConnection NODE_TEXT_OFFSET = 5 @@ -50,6 +49,7 @@ class CanvasGraph(tk.Canvas): self.context = None self.nodes = {} self.edges = {} + self.wireless_edges = {} self.drawing_edge = None self.grid = None self.canvas_management = CanvasComponentManagement(self, core) @@ -58,7 +58,6 @@ class CanvasGraph(tk.Canvas): self.core = core self.helper = GraphHelper(self, core) self.throughput_draw = Throughput(self, core) - self.wireless_draw = WirelessConnection(self, core) # background related self.wallpaper_id = None @@ -120,8 +119,8 @@ class CanvasGraph(tk.Canvas): self.selected = None self.nodes.clear() self.edges.clear() + self.wireless_edges.clear() self.drawing_edge = None - self.wireless_draw.map.clear() self.draw_session(session) def setup_bindings(self): @@ -162,6 +161,25 @@ class CanvasGraph(tk.Canvas): self.tag_lower("gridline") self.tag_lower(self.grid) + def add_wireless_edge(self, src, dst): + token = tuple(sorted((src.id, dst.id))) + x1, y1 = self.coords(src.id) + x2, y2 = self.coords(dst.id) + position = (x1, y1, x2, y2) + edge = CanvasWirelessEdge(token, position, src.id, dst.id, self) + self.wireless_edges[token] = edge + src.wireless_edges.add(edge) + dst.wireless_edges.add(edge) + self.tag_raise(src.id) + self.tag_raise(dst.id) + + def delete_wireless_edge(self, src, dst): + token = tuple(sorted((src.id, dst.id))) + edge = self.wireless_edges.pop(token) + edge.delete() + src.wireless_edges.remove(edge) + dst.wireless_edges.remove(edge) + def draw_session(self, session): """ Draw existing session. @@ -187,7 +205,7 @@ class CanvasGraph(tk.Canvas): canvas_node_two = self.core.canvas_nodes[link.node_two_id] node_two = canvas_node_two.core_node if link.type == core_pb2.LinkType.WIRELESS: - self.wireless_draw.add_connection(link.node_one_id, link.node_two_id) + self.add_wireless_edge(canvas_node_one, canvas_node_two) else: is_node_one_wireless = NodeUtils.is_wireless_node(node_one.type) is_node_two_wireless = NodeUtils.is_wireless_node(node_two.type) @@ -492,6 +510,20 @@ class CanvasGraph(tk.Canvas): self.itemconfig("gridline", state=tk.HIDDEN) +class CanvasWirelessEdge: + def __init__(self, token, position, src, dst, canvas): + self.token = token + self.src = src + self.dst = dst + self.canvas = canvas + self.id = self.canvas.create_line( + *position, tags="wireless", width=1.5, fill="#009933" + ) + + def delete(self): + self.canvas.delete(self.id) + + class CanvasEdge: """ Canvas edge class @@ -580,7 +612,7 @@ class CanvasNode: self.canvas.tag_bind(self.id, "", self.on_leave) self.edges = set() self.interfaces = [] - self.wlans = [] + self.wireless_edges = set() self.moving = None def redraw(self): @@ -605,7 +637,14 @@ class CanvasNode: else: self.canvas.coords(edge.id, x1, y1, x, y) edge.link_info.recalculate_info() - self.canvas.helper.update_wlan_connection(old_x, old_y, x, y, self.wlans) + for edge in self.wireless_edges: + x1, y1, x2, y2 = self.canvas.coords(edge.id) + if edge.src == self.id: + self.canvas.coords(edge.id, x, y, x2, y2) + else: + self.canvas.coords(edge.id, x1, y1, x, y) + if self.app.core.is_runtime(): + self.app.core.edit_node(self.core_node.id, int(x), int(y)) def on_enter(self, event): if self.app.core.is_runtime() and self.app.core.observer: diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index 341f95ef..a4de5f0d 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -1,7 +1,7 @@ """ manage deletion """ -from core.api.grpc import core_pb2 +from coretk.nodeutils import NodeUtils class CanvasComponentManagement: @@ -48,48 +48,6 @@ class CanvasComponentManagement: edges = set() nodes = [] - node_to_wlink = {} - for link_tuple in self.canvas.wireless_draw.map: - nid_one, nid_two = link_tuple - if nid_one not in node_to_wlink: - node_to_wlink[nid_one] = [] - if nid_two not in node_to_wlink: - node_to_wlink[nid_two] = [] - node_to_wlink[nid_one].append(link_tuple) - node_to_wlink[nid_two].append(link_tuple) - - # delete antennas and wireless links - for cnid in self.selected: - canvas_node = self.canvas.nodes[cnid] - if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: - canvas_node.antenna_draw.delete_antennas() - else: - for e in canvas_node.edges: - link_proto = self.app.links[e.token] - node_one_id, node_two_id = ( - link_proto.node_one_id, - link_proto.node_two_id, - ) - if node_one_id == canvas_node.core_node.id: - neighbor_id = node_two_id - else: - neighbor_id = node_one_id - neighbor = self.app.canvas_nodes[neighbor_id] - if neighbor.core_node.type != core_pb2.NodeType.WIRELESS_LAN: - neighbor.antenna_draw.delete_antenna() - - for link_tuple in node_to_wlink.get(canvas_node.core_node.id, []): - nid_one, nid_two = link_tuple - if link_tuple in self.canvas.wireless_draw.map: - self.canvas.delete(self.canvas.wireless_draw.map[link_tuple]) - link_cid = self.canvas.wireless_draw.map.pop(link_tuple, None) - canvas_node_one = self.app.canvas_nodes[nid_one] - canvas_node_two = self.app.canvas_nodes[nid_two] - if link_cid in canvas_node_one.wlans: - canvas_node_one.wlans.remove(link_cid) - if link_cid in canvas_node_two.wlans: - canvas_node_two.wlans.remove(link_cid) - for node_id in list(self.selected): bbox_id = self.selected[node_id] canvas_node = self.canvas.nodes.pop(node_id) @@ -97,6 +55,13 @@ class CanvasComponentManagement: self.canvas.delete(node_id) self.canvas.delete(bbox_id) self.canvas.delete(canvas_node.text_id) + + # delete antennas + is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type) + if is_wireless: + canvas_node.antenna_draw.delete_antennas() + + # delete related edges for edge in canvas_node.edges: if edge in edges: continue @@ -116,5 +81,7 @@ class CanvasComponentManagement: other_node.interfaces.remove(other_interface) except ValueError: pass + if is_wireless: + other_node.antenna_draw.delete_antenna() self.selected.clear() return nodes diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py deleted file mode 100644 index dd51723b..00000000 --- a/coretk/coretk/wirelessconnection.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Wireless connection handler -""" -from core.api.grpc import core_pb2 - - -class WirelessConnection: - def __init__(self, canvas, core): - self.canvas = canvas - self.core = core - # map a (node_one_id, node_two_id) to a wlan canvas id - self.map = {} - - def add_connection(self, node_one_id, node_two_id): - canvas_node_one = self.core.canvas_nodes[node_one_id] - canvas_node_two = self.core.canvas_nodes[node_two_id] - key = tuple(sorted((node_one_id, node_two_id))) - if key not in self.map: - x1, y1 = self.canvas.coords(canvas_node_one.id) - x2, y2 = self.canvas.coords(canvas_node_two.id) - wlan_canvas_id = self.canvas.create_line( - x1, y1, x2, y2, fill="#009933", tags="wireless", width=1.5 - ) - self.map[key] = wlan_canvas_id - canvas_node_one.wlans.append(wlan_canvas_id) - canvas_node_two.wlans.append(wlan_canvas_id) - else: - print("in map") - self.canvas.itemconfig(self.map[key], state="normal") - - def delete_connection(self, node_one_id, node_two_id): - canvas_node_one = self.core.canvas_nodes[node_one_id] - canvas_node_two = self.core.canvas_nodes[node_two_id] - key = tuple(sorted((node_one_id, node_two_id))) - wlan_canvas_id = self.map[key] - canvas_node_one.wlans.remove(wlan_canvas_id) - canvas_node_two.wlans.remove(wlan_canvas_id) - self.canvas.delete(wlan_canvas_id) - self.map.pop(key, None) - - def handle_link_event(self, link_event): - if link_event.message_type == core_pb2.MessageType.ADD: - self.add_connection( - link_event.link.node_one_id, link_event.link.node_two_id - ) - self.canvas.tag_raise("node") - - if link_event.message_type == core_pb2.MessageType.DELETE: - self.delete_connection( - link_event.link.node_one_id, link_event.link.node_two_id - ) From b30b8ab83de6d4bd494fbcd21e10a0b93592be6a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 16:14:14 -0800 Subject: [PATCH 303/462] updated status bar text to be centered --- coretk/coretk/status.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/coretk/coretk/status.py b/coretk/coretk/status.py index ea31d6fc..c4263e85 100644 --- a/coretk/coretk/status.py +++ b/coretk/coretk/status.py @@ -28,16 +28,20 @@ class StatusBar(ttk.Frame): self.progress_bar = ttk.Progressbar( self, orient="horizontal", mode="indeterminate" ) - self.progress_bar.grid(row=0, column=0, sticky="nsew") - self.status = ttk.Label(self, textvariable=self.statusvar) + self.progress_bar.grid(row=0, column=0, sticky="ew") + + self.status = ttk.Label(self, textvariable=self.statusvar, anchor=tk.CENTER) self.statusvar.set("status") - self.status.grid(row=0, column=1, sticky="nsew") - self.zoom = ttk.Label(self, text="zoom") - self.zoom.grid(row=0, column=2) - self.cpu_usage = ttk.Label(self, text="cpu usage") - self.cpu_usage.grid(row=0, column=3) - self.emulation_light = ttk.Label(self, text="emulation light") - self.emulation_light.grid(row=0, column=4) + self.status.grid(row=0, column=1, sticky="ew") + + self.zoom = ttk.Label(self, text="zoom", anchor=tk.CENTER) + self.zoom.grid(row=0, column=2, sticky="ew") + + self.cpu_usage = ttk.Label(self, text="cpu usage", anchor=tk.CENTER) + self.cpu_usage.grid(row=0, column=3, sticky="ew") + + self.emulation_light = ttk.Label(self, text="emulation light", anchor=tk.CENTER) + self.emulation_light.grid(row=0, column=4, sticky="ew") def start_session_callback(self, process_time): num_nodes = len(self.app.core.canvas_nodes) From 6a8e0c8360a523faa2904eca8ba26deffa5cffef Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 16:27:53 -0800 Subject: [PATCH 304/462] fixed app quit when grpc fails, fixed quitting when not stopping the running session --- coretk/coretk/menuaction.py | 38 +++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 5b1cec9e..d8dc89cb 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -8,6 +8,8 @@ import time import webbrowser from tkinter import filedialog, messagebox +import grpc + from core.api.grpc import core_pb2 from coretk.appconfig import XML_PATH from coretk.dialogs.canvasbackground import CanvasBackgroundDialog @@ -30,6 +32,7 @@ class MenuAction: self.app = app def cleanup_old_session(self, quitapp=False): + logging.info("cleaning up old session") start = time.time() self.app.core.stop_session() self.app.core.delete_session() @@ -47,27 +50,31 @@ class MenuAction: logging.info( "menuaction.py: clean_nodes_links_and_set_configuration() Exiting the program" ) - state = self.app.core.get_session_state() + try: + state = self.app.core.get_session_state() - if ( - state == core_pb2.SessionState.SHUTDOWN - or state == core_pb2.SessionState.DEFINITION - ): - self.app.core.delete_session() - if quitapp: - self.app.quit() - else: - msgbox = messagebox.askyesnocancel("stop", "Stop the running session?") - if msgbox or msgbox is False: - if msgbox: + if ( + state == core_pb2.SessionState.SHUTDOWN + or state == core_pb2.SessionState.DEFINITION + ): + self.app.core.delete_session() + if quitapp: + self.app.quit() + else: + result = messagebox.askyesnocancel("stop", "Stop the running session?") + if result: self.app.statusbar.progress_bar.start(5) thread = threading.Thread( target=self.cleanup_old_session, args=([quitapp]) ) + thread.daemon = True thread.start() - - # self.app.core.stop_session() - # self.app.core.delete_session() + elif quitapp: + self.app.quit() + except grpc.RpcError: + logging.error("error getting session state") + if quitapp: + self.app.quit() def on_quit(self, event=None): """ @@ -76,7 +83,6 @@ class MenuAction: :return: nothing """ self.prompt_save_running_session(quitapp=True) - # self.app.quit() def file_save_as_xml(self, event=None): logging.info("menuaction.py file_save_as_xml()") From 5c04225cc4255b127aa849f7c19547bfbadbbd42 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 27 Nov 2019 16:39:48 -0800 Subject: [PATCH 305/462] working on shape --- coretk/coretk/graph.py | 42 ++++++++++++++++++++---------- coretk/coretk/shape.py | 55 +++++++++++++++++++++++++++++++++++----- coretk/coretk/toolbar.py | 5 ++-- 3 files changed, 80 insertions(+), 22 deletions(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 1faaff76..c80834da 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -13,7 +13,7 @@ from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog from coretk.graph_helper import GraphHelper, WlanAntennaManager -from coretk.images import Images +from coretk.images import ImageEnum, Images from coretk.linkinfo import LinkInfo, Throughput from coretk.nodedelete import CanvasComponentManagement from coretk.nodeutils import NodeUtils @@ -53,11 +53,13 @@ class CanvasGraph(tk.Canvas): super().__init__(master, cnf, **kwargs) self.app = master self.mode = GraphMode.SELECT + self.annotation_type = None self.selected = None self.node_draw = None self.context = None self.nodes = {} self.edges = {} + self.shapes = {} self.drawing_edge = None self.grid = None self.canvas_management = CanvasComponentManagement(self, core) @@ -272,16 +274,23 @@ class CanvasGraph(tk.Canvas): self.context.unpost() self.context = None else: - self.focus_set() - self.selected = self.get_selected(event) - logging.debug(f"click release selected({self.selected}) mode({self.mode})") - if self.mode == GraphMode.EDGE: - self.handle_edge_release(event) - elif self.mode == GraphMode.NODE: - x, y = self.canvas_xy(event) - self.add_node(x, y) - elif self.mode == GraphMode.PICKNODE: - self.mode = GraphMode.NODE + if self.mode == GraphMode.ANNOTATION: + if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: + x, y = self.canvas_xy(event) + self.shapes[self.selected].shape_complete(x, y) + else: + self.focus_set() + self.selected = self.get_selected(event) + logging.debug( + f"click release selected({self.selected}) mode({self.mode})" + ) + if self.mode == GraphMode.EDGE: + self.handle_edge_release(event) + elif self.mode == GraphMode.NODE: + x, y = self.canvas_xy(event) + self.add_node(x, y) + elif self.mode == GraphMode.PICKNODE: + self.mode = GraphMode.NODE def handle_edge_release(self, event): edge = self.drawing_edge @@ -333,8 +342,11 @@ class CanvasGraph(tk.Canvas): x, y = self.coords(selected) self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) if self.mode == GraphMode.ANNOTATION: - shape = Shape(self.app, self, event.x, event.y) - print(shape) + if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: + x, y = self.canvas_xy(event) + shape = Shape(self.app, self, x, y) + self.selected = shape.id + self.shapes[shape.id] = shape def click_motion(self, event): """ @@ -347,6 +359,10 @@ class CanvasGraph(tk.Canvas): x2, y2 = self.canvas_xy(event) x1, y1, _, _ = self.coords(self.drawing_edge.id) self.coords(self.drawing_edge.id, x1, y1, x2, y2) + if self.mode == GraphMode.ANNOTATION: + if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: + x, y = self.canvas_xy(event) + self.shapes[self.selected].shape_motion(x, y) def click_context(self, event): logging.info("context event: %s", self.context) diff --git a/coretk/coretk/shape.py b/coretk/coretk/shape.py index 7b9df97b..76e76972 100644 --- a/coretk/coretk/shape.py +++ b/coretk/coretk/shape.py @@ -1,16 +1,57 @@ """ class for shapes """ -# from coretk.images import ImageEnum, Images +import logging + +from coretk.images import ImageEnum + +ABOVE_COMPONENT = ["gridline", "edge", "linkinfo", "antenna", "node", "nodename"] class Shape: - def __init__(self, app, canvas, topleft_x, topleft_y): + def __init__(self, app, canvas, top_x, top_y): self.app = app self.canvas = canvas - self.x0 = topleft_x - self.y0 = topleft_y - # imageenum = self.app.toolbar - self.id = self.canvas.create_oval( - topleft_x, topleft_y, topleft_x + 30, topleft_y + 30 + self.x0 = top_x + self.y0 = top_y + self.cursor_x = None + self.cursor_y = None + annotation_type = self.canvas.annotation_type + if annotation_type == ImageEnum.OVAL: + self.id = canvas.create_oval( + top_x, top_y, top_x, top_y, tags="shape", dash="-" + ) + elif annotation_type == ImageEnum.RECTANGLE: + self.id = canvas.create_rectangle( + top_x, top_y, top_x, top_y, tags="shape", dash="-" + ) + self.canvas.tag_bind(self.id, "", self.click_press) + self.canvas.tag_bind(self.id, "", self.click_release) + self.canvas.tag_bind(self.id, "", self.motion) + + def shape_motion(self, x1, y1): + self.canvas.coords(self.id, self.x0, self.y0, x1, y1) + + def shape_complete(self, x, y): + self.canvas.itemconfig(self.id, width=0, fill="#ccccff") + for component in ABOVE_COMPONENT: + self.canvas.tag_raise(component) + + def click_press(self, event): + logging.debug("Click on shape %s", self.id) + self.cursor_x = event.x + self.cursor_y = event.y + + def click_release(self, event): + logging.debug("Click release on shape %s", self.id) + + def motion(self, event): + logging.debug("motion on shape %s", self.id) + delta_x = event.x - self.cursor_x + delta_y = event.y - self.cursor_y + x0, y0, x1, y1 = self.canvas.bbox(self.id) + self.canvas.coords( + self.id, x0 + delta_x, y0 + delta_y, x1 + delta_x, y1 + delta_y ) + self.cursor_x = event.x + self.cursor_y = event.y diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index c68c8f24..fb0560dc 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -300,7 +300,7 @@ class Toolbar(ttk.Frame): image = icon(image_enum) self.create_picker_button( image, - partial(self.update_annotation, image), + partial(self.update_annotation, image, image_enum), self.annotation_picker, tooltip, ) @@ -362,12 +362,13 @@ class Toolbar(ttk.Frame): self.design_frame.tkraise() - def update_annotation(self, image): + def update_annotation(self, image, image_enum): logging.info("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 = image_enum def click_run_button(self): logging.debug("Click on RUN button") From ff473b97489c00615a9dc4c3b4c35decd24899fd Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 2 Dec 2019 16:05:10 -0800 Subject: [PATCH 306/462] shape dialog --- coretk/coretk/dialogs/shapemod.py | 126 ++++++++++++++++++++++++++++++ coretk/coretk/graph.py | 49 ++++++++++-- coretk/coretk/nodedelete.py | 76 ++++++++++-------- coretk/coretk/shape.py | 13 ++- 4 files changed, 215 insertions(+), 49 deletions(-) create mode 100644 coretk/coretk/dialogs/shapemod.py diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py new file mode 100644 index 00000000..b76fc0d1 --- /dev/null +++ b/coretk/coretk/dialogs/shapemod.py @@ -0,0 +1,126 @@ +""" +shape input dialog +""" +import tkinter as tk +from tkinter import colorchooser, font, ttk + +from coretk.dialogs.dialog import Dialog + +FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] +BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + +class ShapeDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Add a new shape", modal=True) + self.shape_text = tk.StringVar(value="") + self.font = tk.StringVar(value="Arial") + self.font_size = tk.IntVar(value=12) + self.text_color = "#000000" + self.fill_color = "#CFCFFF" + self.border_color = "black" + self.border_width = tk.IntVar(value=0) + + self.fill = None + self.border = None + + self.top.columnconfigure(0, weight=1) + self.draw() + + def draw(self): + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=2) + label = ttk.Label(frame, text="Text for top of shape: ") + label.grid(row=0, column=0, sticky="nsew") + entry = ttk.Entry(frame, textvariable=self.shape_text) + entry.grid(row=0, column=1, sticky="nsew") + frame.grid(row=0, column=0, sticky="nsew", padx=3, pady=3) + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + combobox = ttk.Combobox( + frame, + textvariable=self.font, + values=sorted(font.families()), + state="readonly", + ) + combobox.grid(row=0, column=0, sticky="nsew") + combobox = ttk.Combobox( + frame, textvariable=self.font_size, values=FONT_SIZES, state="readonly" + ) + combobox.grid(row=0, column=1, padx=3, sticky="nsew") + button = ttk.Button(frame, text="Text color", command=self.choose_text_color) + button.grid(row=0, column=2, sticky="nsew") + frame.grid(row=1, column=0, sticky="nsew", padx=3, pady=3) + + frame = ttk.Frame(self.top) + button = ttk.Checkbutton(frame, text="Bold") + button.grid(row=0, column=0) + button = ttk.Checkbutton(frame, text="Italic") + button.grid(row=0, column=1, padx=3) + button = ttk.Checkbutton(frame, text="Underline") + button.grid(row=0, column=2) + frame.grid(row=2, column=0, sticky="nsew", padx=3, pady=3) + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + label = ttk.Label(frame, text="Fill color") + label.grid(row=0, column=0, sticky="nsew") + self.fill = ttk.Label(frame, text=self.fill_color, background="#CFCFFF") + self.fill.grid(row=0, column=1, sticky="nsew", padx=3) + button = ttk.Button(frame, text="Color", command=self.choose_fill_color) + button.grid(row=0, column=2, sticky="nsew") + frame.grid(row=3, column=0, sticky="nsew", padx=3, pady=3) + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + label = ttk.Label(frame, text="Border color:") + label.grid(row=0, column=0, sticky="nsew") + self.border = ttk.Label( + frame, text=self.border_color, background=self.fill_color + ) + self.border.grid(row=0, column=1, sticky="nsew", padx=3) + button = ttk.Button(frame, text="Color", command=self.choose_border_color) + button.grid(row=0, column=2, sticky="nsew") + frame.grid(row=4, column=0, sticky="nsew", padx=3, pady=3) + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=2) + label = ttk.Label(frame, text="Border width:") + label.grid(row=0, column=0, sticky="nsew") + combobox = ttk.Combobox( + frame, textvariable=self.border_width, values=BORDER_WIDTH, state="readonly" + ) + combobox.grid(row=0, column=1, sticky="nsew") + frame.grid(row=5, column=0, sticky="nsew", padx=3, pady=3) + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + button = ttk.Button(frame, text="Add shape") + button.grid(row=0, column=0, sticky="e", padx=3) + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="w", pady=3) + frame.grid(row=6, column=0, sticky="nsew", padx=3, pady=3) + + def choose_text_color(self): + color = colorchooser.askcolor(color="black") + self.text_color = color[1] + + def choose_fill_color(self): + color = colorchooser.askcolor(color=self.fill_color) + self.fill_color = color[1] + self.fill.config(background=color[1], text=color[1]) + + def choose_border_color(self): + color = colorchooser.askcolor(color="black") + self.border_color = color[1] + self.border.config(background=color[1], text=color[1]) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 942dfce8..c8c5ceba 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -68,6 +68,7 @@ class CanvasGraph(tk.Canvas): self.core = core self.helper = GraphHelper(self, core) self.throughput_draw = Throughput(self, core) + self.shape_drawing = False # background related self.wallpaper_id = None @@ -144,6 +145,7 @@ class CanvasGraph(tk.Canvas): self.bind("", self.click_motion) self.bind("", self.click_context) self.bind("", self.press_delete) + self.bind("", self.ctrl_click) def draw_grid(self, width=1000, height=800): """ @@ -275,6 +277,9 @@ class CanvasGraph(tk.Canvas): selected = _id break + if _id in self.shapes: + selected = _id + return selected def click_release(self, event): @@ -290,8 +295,10 @@ class CanvasGraph(tk.Canvas): else: if self.mode == GraphMode.ANNOTATION: if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: + self.focus_set() x, y = self.canvas_xy(event) self.shapes[self.selected].shape_complete(x, y) + self.shape_drawing = False else: self.focus_set() self.selected = self.get_selected(event) @@ -355,12 +362,28 @@ class CanvasGraph(tk.Canvas): if self.mode == GraphMode.EDGE and is_node: x, y = self.coords(selected) self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) - if self.mode == GraphMode.ANNOTATION: - if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: - x, y = self.canvas_xy(event) - shape = Shape(self.app, self, x, y) - self.selected = shape.id - self.shapes[shape.id] = shape + if ( + self.mode == GraphMode.ANNOTATION + and self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE] + and selected is None + ): + x, y = self.canvas_xy(event) + shape = Shape(self.app, self, x, y) + self.selected = shape.id + self.shapes[shape.id] = shape + self.shape_drawing = True + if self.mode == GraphMode.SELECT and "shape" in self.gettags(selected): + x, y = self.canvas_xy(event) + self.shapes[selected].cursor_x = x + self.shapes[selected].cursor_y = y + self.canvas_management.node_select(self.shapes[selected]) + self.selected = selected + + def ctrl_click(self, event): + logging.debug("Control left click %s", event) + selected = self.get_selected(event) + if self.mode == GraphMode.SELECT and "shape" in self.gettags(selected): + self.canvas_management.node_select(self.shapes[selected], True) def click_motion(self, event): """ @@ -374,9 +397,14 @@ class CanvasGraph(tk.Canvas): x1, y1, _, _ = self.coords(self.drawing_edge.id) self.coords(self.drawing_edge.id, x1, y1, x2, y2) if self.mode == GraphMode.ANNOTATION: - if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: + if ( + self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE] + and self.shape_drawing + ): x, y = self.canvas_xy(event) self.shapes[self.selected].shape_motion(x, y) + if self.mode == GraphMode.SELECT and "shape" in self.gettags(self.selected): + self.shapes[self.selected].motion(event) def click_context(self, event): logging.info("context event: %s", self.context) @@ -399,11 +427,12 @@ class CanvasGraph(tk.Canvas): :param event: :return: """ + logging.debug("press delete key") nodes = self.canvas_management.delete_selected_nodes() self.core.delete_graph_nodes(nodes) def add_node(self, x, y): - if self.selected is None: + if self.selected is None or "shape" in self.gettags(self.selected): core_node = self.core.create_node( int(x), int(y), self.node_draw.node_type, self.node_draw.model ) @@ -536,6 +565,9 @@ class CanvasGraph(tk.Canvas): else: self.itemconfig("gridline", state=tk.HIDDEN) + def is_selection_mode(self): + return self.mode == GraphMode.SELECT + class CanvasWirelessEdge: def __init__(self, token, position, src, dst, canvas): @@ -701,6 +733,7 @@ class CanvasNode: logging.debug(f"node click press {self.core_node.name}: {event}") self.moving = self.canvas.canvas_xy(event) self.canvas.canvas_management.node_select(self) + self.canvas.selected = self.id def click_release(self, event): logging.debug(f"node click release {self.core_node.name}: {event}") diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index a4de5f0d..8ea1500e 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -47,41 +47,49 @@ class CanvasComponentManagement: def delete_selected_nodes(self): edges = set() nodes = [] + for node_id in self.selected: + if "node" in self.canvas.gettags(node_id): + bbox_id = self.selected[node_id] + canvas_node = self.canvas.nodes.pop(node_id) + nodes.append(canvas_node) + self.canvas.delete(node_id) + self.canvas.delete(bbox_id) + self.canvas.delete(canvas_node.text_id) - for node_id in list(self.selected): - bbox_id = self.selected[node_id] - canvas_node = self.canvas.nodes.pop(node_id) - nodes.append(canvas_node) - self.canvas.delete(node_id) - self.canvas.delete(bbox_id) - self.canvas.delete(canvas_node.text_id) - - # delete antennas - is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type) - if is_wireless: - canvas_node.antenna_draw.delete_antennas() - - # delete related edges - for edge in canvas_node.edges: - if edge in edges: - continue - edges.add(edge) - self.canvas.edges.pop(edge.token) - self.canvas.delete(edge.id) - self.canvas.delete(edge.link_info.id1) - self.canvas.delete(edge.link_info.id2) - other_id = edge.src - other_interface = edge.src_interface - if edge.src == node_id: - other_id = edge.dst - other_interface = edge.dst_interface - other_node = self.canvas.nodes[other_id] - other_node.edges.remove(edge) - try: - other_node.interfaces.remove(other_interface) - except ValueError: - pass + # delete antennas + is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type) if is_wireless: - other_node.antenna_draw.delete_antenna() + canvas_node.antenna_draw.delete_antennas() + + # delete related edges + for edge in canvas_node.edges: + if edge in edges: + continue + edges.add(edge) + self.canvas.edges.pop(edge.token) + self.canvas.delete(edge.id) + self.canvas.delete(edge.link_info.id1) + self.canvas.delete(edge.link_info.id2) + other_id = edge.src + other_interface = edge.src_interface + if edge.src == node_id: + other_id = edge.dst + other_interface = edge.dst_interface + other_node = self.canvas.nodes[other_id] + other_node.edges.remove(edge) + try: + other_node.interfaces.remove(other_interface) + except ValueError: + pass + if is_wireless: + other_node.antenna_draw.delete_antenna() + + for shape_id in self.selected: + if "shape" in self.canvas.gettags(shape_id): + bbox_id = self.selected[node_id] + self.canvas.delete(shape_id) + self.canvas.delete(bbox_id) + self.canvas.shapes.pop(shape_id) + self.selected.clear() return nodes diff --git a/coretk/coretk/shape.py b/coretk/coretk/shape.py index 76e76972..696f1e5e 100644 --- a/coretk/coretk/shape.py +++ b/coretk/coretk/shape.py @@ -3,6 +3,7 @@ class for shapes """ import logging +from coretk.dialogs.shapemod import ShapeDialog from coretk.images import ImageEnum ABOVE_COMPONENT = ["gridline", "edge", "linkinfo", "antenna", "node", "nodename"] @@ -16,6 +17,7 @@ class Shape: self.y0 = top_y self.cursor_x = None self.cursor_y = None + canvas.delete(canvas.find_withtag("selectednodes")) annotation_type = self.canvas.annotation_type if annotation_type == ImageEnum.OVAL: self.id = canvas.create_oval( @@ -25,9 +27,8 @@ class Shape: self.id = canvas.create_rectangle( top_x, top_y, top_x, top_y, tags="shape", dash="-" ) - self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) - self.canvas.tag_bind(self.id, "", self.motion) + # self.canvas.tag_bind(self.id, "", self.motion) def shape_motion(self, x1, y1): self.canvas.coords(self.id, self.x0, self.y0, x1, y1) @@ -36,11 +37,8 @@ class Shape: self.canvas.itemconfig(self.id, width=0, fill="#ccccff") for component in ABOVE_COMPONENT: self.canvas.tag_raise(component) - - def click_press(self, event): - logging.debug("Click on shape %s", self.id) - self.cursor_x = event.x - self.cursor_y = event.y + s = ShapeDialog(self.app, self.app) + s.show() def click_release(self, event): logging.debug("Click release on shape %s", self.id) @@ -53,5 +51,6 @@ class Shape: self.canvas.coords( self.id, x0 + delta_x, y0 + delta_y, x1 + delta_x, y1 + delta_y ) + self.canvas.canvas_management.node_drag(self, delta_x, delta_y) self.cursor_x = event.x self.cursor_y = event.y From 0a26a8f8e38a890cca9e720287ad667e4dea52c0 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 3 Dec 2019 16:18:00 -0800 Subject: [PATCH 307/462] shape dialog, fix move shape --- coretk/coretk/dialogs/shapemod.py | 51 +++++++++++++++++++++++++++---- coretk/coretk/graph.py | 24 ++++++++++++--- coretk/coretk/shape.py | 5 +-- 3 files changed, 68 insertions(+), 12 deletions(-) diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index b76fc0d1..577d3821 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -11,8 +11,10 @@ BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] class ShapeDialog(Dialog): - def __init__(self, master, app): + def __init__(self, master, app, shape_id): super().__init__(master, app, "Add a new shape", modal=True) + self.canvas = app.canvas + self.id = shape_id self.shape_text = tk.StringVar(value="") self.font = tk.StringVar(value="Arial") self.font_size = tk.IntVar(value=12) @@ -20,6 +22,9 @@ class ShapeDialog(Dialog): self.fill_color = "#CFCFFF" self.border_color = "black" self.border_width = tk.IntVar(value=0) + self.bold = tk.IntVar(value=0) + self.italic = tk.IntVar(value=0) + self.underline = tk.IntVar(value=0) self.fill = None self.border = None @@ -57,11 +62,11 @@ class ShapeDialog(Dialog): frame.grid(row=1, column=0, sticky="nsew", padx=3, pady=3) frame = ttk.Frame(self.top) - button = ttk.Checkbutton(frame, text="Bold") + button = ttk.Checkbutton(frame, variable=self.bold, text="Bold") button.grid(row=0, column=0) - button = ttk.Checkbutton(frame, text="Italic") + button = ttk.Checkbutton(frame, variable=self.italic, text="Italic") button.grid(row=0, column=1, padx=3) - button = ttk.Checkbutton(frame, text="Underline") + button = ttk.Checkbutton(frame, variable=self.underline, text="Underline") button.grid(row=0, column=2) frame.grid(row=2, column=0, sticky="nsew", padx=3, pady=3) @@ -105,9 +110,9 @@ class ShapeDialog(Dialog): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - button = ttk.Button(frame, text="Add shape") + button = ttk.Button(frame, text="Add shape", command=self.add_shape) button.grid(row=0, column=0, sticky="e", padx=3) - button = ttk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.cancel) button.grid(row=0, column=1, sticky="w", pady=3) frame.grid(row=6, column=0, sticky="nsew", padx=3, pady=3) @@ -124,3 +129,37 @@ class ShapeDialog(Dialog): color = colorchooser.askcolor(color="black") self.border_color = color[1] self.border.config(background=color[1], text=color[1]) + + def cancel(self): + if not self.canvas.shapes[self.id].created: + self.canvas.delete(self.id) + self.canvas.shapes.pop(self.id) + self.destroy() + + def add_shape(self): + self.canvas.itemconfig( + self.id, + fill=self.fill_color, + dash="", + outline=self.border_color, + width=int(self.border_width.get()), + ) + shape = self.canvas.shapes[self.id] + shape_text = self.shape_text.get() + if shape.text is None: + size = int(self.font_size.get()) + x0, y0, x1, y1 = self.canvas.bbox(self.id) + text_y = y0 + 2 * size + text_x = (x0 + x1) / 2 + f = [self.font.get(), size] + if self.bold.get() == 1: + f.append("bold") + if self.italic.get() == 1: + f.append("italic") + if self.underline.get() == 1: + f.append("underline") + shape.text = self.canvas.create_text( + text_x, text_y, text=shape_text, fill=self.text_color, font=f + ) + self.canvas.shapes[self.id].created = True + self.destroy() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index c8c5ceba..c9c349ec 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -146,6 +146,7 @@ class CanvasGraph(tk.Canvas): self.bind("", self.click_context) self.bind("", self.press_delete) self.bind("", self.ctrl_click) + self.bind("", self.double_click) def draw_grid(self, width=1000, height=800): """ @@ -297,8 +298,9 @@ class CanvasGraph(tk.Canvas): if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: self.focus_set() x, y = self.canvas_xy(event) - self.shapes[self.selected].shape_complete(x, y) - self.shape_drawing = False + if self.shape_drawing: + self.shapes[self.selected].shape_complete(x, y) + self.shape_drawing = False else: self.focus_set() self.selected = self.get_selected(event) @@ -312,6 +314,7 @@ class CanvasGraph(tk.Canvas): self.add_node(x, y) elif self.mode == GraphMode.PICKNODE: self.mode = GraphMode.NODE + self.selected = None def handle_edge_release(self, event): edge = self.drawing_edge @@ -372,7 +375,11 @@ class CanvasGraph(tk.Canvas): self.selected = shape.id self.shapes[shape.id] = shape self.shape_drawing = True - if self.mode == GraphMode.SELECT and "shape" in self.gettags(selected): + if ( + self.mode == GraphMode.SELECT + and selected is not None + and "shape" in self.gettags(selected) + ): x, y = self.canvas_xy(event) self.shapes[selected].cursor_x = x self.shapes[selected].cursor_y = y @@ -403,7 +410,11 @@ class CanvasGraph(tk.Canvas): ): x, y = self.canvas_xy(event) self.shapes[self.selected].shape_motion(x, y) - if self.mode == GraphMode.SELECT and "shape" in self.gettags(self.selected): + if ( + self.mode == GraphMode.SELECT + and self.selected is not None + and "shape" in self.gettags(self.selected) + ): self.shapes[self.selected].motion(event) def click_context(self, event): @@ -431,6 +442,11 @@ class CanvasGraph(tk.Canvas): nodes = self.canvas_management.delete_selected_nodes() self.core.delete_graph_nodes(nodes) + def double_click(self, event): + selected = self.get_selected(event) + if selected is not None and "shape" in self.gettags(selected): + print("bring up shape dialog ") + def add_node(self, x, y): if self.selected is None or "shape" in self.gettags(self.selected): core_node = self.core.create_node( diff --git a/coretk/coretk/shape.py b/coretk/coretk/shape.py index 696f1e5e..9c85837e 100644 --- a/coretk/coretk/shape.py +++ b/coretk/coretk/shape.py @@ -17,6 +17,8 @@ class Shape: self.y0 = top_y self.cursor_x = None self.cursor_y = None + self.created = False + self.text = None canvas.delete(canvas.find_withtag("selectednodes")) annotation_type = self.canvas.annotation_type if annotation_type == ImageEnum.OVAL: @@ -34,10 +36,9 @@ class Shape: self.canvas.coords(self.id, self.x0, self.y0, x1, y1) def shape_complete(self, x, y): - self.canvas.itemconfig(self.id, width=0, fill="#ccccff") for component in ABOVE_COMPONENT: self.canvas.tag_raise(component) - s = ShapeDialog(self.app, self.app) + s = ShapeDialog(self.app, self.app, self.id) s.show() def click_release(self, event): From 41a9b88189a2330163e41fcdc076c8fc7a9e404c Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 3 Dec 2019 17:17:45 -0800 Subject: [PATCH 308/462] working on shape config --- coretk/coretk/dialogs/shapemod.py | 69 ++++++++++++++++++++----------- coretk/coretk/graph.py | 4 +- coretk/coretk/shape.py | 20 ++++++++- 3 files changed, 65 insertions(+), 28 deletions(-) diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index 577d3821..c00a9649 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -11,20 +11,35 @@ BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] class ShapeDialog(Dialog): - def __init__(self, master, app, shape_id): + def __init__(self, master, app, shape): super().__init__(master, app, "Add a new shape", modal=True) self.canvas = app.canvas - self.id = shape_id - self.shape_text = tk.StringVar(value="") - self.font = tk.StringVar(value="Arial") - self.font_size = tk.IntVar(value=12) - self.text_color = "#000000" - self.fill_color = "#CFCFFF" - self.border_color = "black" - self.border_width = tk.IntVar(value=0) - self.bold = tk.IntVar(value=0) - self.italic = tk.IntVar(value=0) - self.underline = tk.IntVar(value=0) + self.id = shape.id + data = shape.shape_data + self.shape_text = tk.StringVar(value=data.text) + self.font = tk.StringVar(value=data.font) + self.font_size = tk.IntVar(value=data.font_size) + self.text_color = data.text_color + self.fill_color = data.fill_color + self.border_color = data.border_color + self.border_width = tk.IntVar(value=data.border_width) + self.bold = tk.IntVar(value=data.bold) + self.italic = tk.IntVar(value=data.italic) + self.underline = tk.IntVar(value=data.underline) + + # else: + # self.fill_color = self.canvas.itemcget(self.id, "fill") + # self.shape_text = tk.StringVar(value="") + # self.font = tk.StringVar(value="Arial") + # self.font_size = tk.IntVar(value=12) + # self.text_color = "#000000" + # # self.fill_color = "#CFCFFF" + # self.border_color = "black" + # self.border_width = tk.IntVar(value=0) + # self.bold = tk.IntVar(value=0) + # self.italic = tk.IntVar(value=0) + # self.underline = tk.IntVar(value=0) + # print(self.fill_color) self.fill = None self.border = None @@ -146,20 +161,24 @@ class ShapeDialog(Dialog): ) shape = self.canvas.shapes[self.id] shape_text = self.shape_text.get() - if shape.text is None: - size = int(self.font_size.get()) - x0, y0, x1, y1 = self.canvas.bbox(self.id) - text_y = y0 + 2 * size - text_x = (x0 + x1) / 2 - f = [self.font.get(), size] - if self.bold.get() == 1: - f.append("bold") - if self.italic.get() == 1: - f.append("italic") - if self.underline.get() == 1: - f.append("underline") + size = int(self.font_size.get()) + x0, y0, x1, y1 = self.canvas.bbox(self.id) + text_y = y0 + 2 * size + text_x = (x0 + x1) / 2 + f = [self.font.get(), size] + if self.bold.get() == 1: + f.append("bold") + if self.italic.get() == 1: + f.append("italic") + if self.underline.get() == 1: + f.append("underline") + if shape.text_id is None: shape.text = self.canvas.create_text( text_x, text_y, text=shape_text, fill=self.text_color, font=f ) - self.canvas.shapes[self.id].created = True + self.canvas.shapes[self.id].created = True + else: + self.canvas.itemconfig( + shape.text_id, text=shape_text, fill=self.text_color, font=f + ) self.destroy() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index c9c349ec..7389be52 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -11,6 +11,7 @@ from coretk.canvastooltip import CanvasTooltip from coretk.dialogs.emaneconfig import EmaneConfigDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog +from coretk.dialogs.shapemod import ShapeDialog from coretk.dialogs.wlanconfig import WlanConfigDialog from coretk.graph_helper import GraphHelper, WlanAntennaManager from coretk.images import ImageEnum, Images @@ -445,7 +446,8 @@ class CanvasGraph(tk.Canvas): def double_click(self, event): selected = self.get_selected(event) if selected is not None and "shape" in self.gettags(selected): - print("bring up shape dialog ") + s = ShapeDialog(self.app, self.app, self.shapes[selected]) + print(s) def add_node(self, x, y): if self.selected is None or "shape" in self.gettags(self.selected): diff --git a/coretk/coretk/shape.py b/coretk/coretk/shape.py index 9c85837e..2bb7b516 100644 --- a/coretk/coretk/shape.py +++ b/coretk/coretk/shape.py @@ -9,6 +9,20 @@ from coretk.images import ImageEnum ABOVE_COMPONENT = ["gridline", "edge", "linkinfo", "antenna", "node", "nodename"] +class ShapeData: + def __init__(self): + self.text = "" + self.font = "Arial" + self.font_size = 12 + self.text_color = "#000000" + self.fill_color = "#CFCFFF" + self.border_color = "#000000" + self.border_width = 0 + self.bold = 0 + self.italic = 0 + self.underline = 0 + + class Shape: def __init__(self, app, canvas, top_x, top_y): self.app = app @@ -18,7 +32,9 @@ class Shape: self.cursor_x = None self.cursor_y = None self.created = False - self.text = None + self.text_id = None + + self.shape_data = ShapeData() canvas.delete(canvas.find_withtag("selectednodes")) annotation_type = self.canvas.annotation_type if annotation_type == ImageEnum.OVAL: @@ -38,7 +54,7 @@ class Shape: def shape_complete(self, x, y): for component in ABOVE_COMPONENT: self.canvas.tag_raise(component) - s = ShapeDialog(self.app, self.app, self.id) + s = ShapeDialog(self.app, self.app, self) s.show() def click_release(self, event): From bbb8be6655cd37646be8b964d5941b6ec2f817e5 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 4 Dec 2019 09:28:16 -0800 Subject: [PATCH 309/462] shape configuration, replace tunntel tool image --- coretk/coretk/data/icons/tunnel.png | Bin 0 -> 2256 bytes .../data/{icons => oldicons}/tunnel.gif | Bin coretk/coretk/dialogs/shapemod.py | 31 ++++++++---------- coretk/coretk/graph.py | 8 ++++- coretk/coretk/nodedelete.py | 2 +- coretk/coretk/shape.py | 7 +++- 6 files changed, 27 insertions(+), 21 deletions(-) create mode 100644 coretk/coretk/data/icons/tunnel.png rename coretk/coretk/data/{icons => oldicons}/tunnel.gif (100%) diff --git a/coretk/coretk/data/icons/tunnel.png b/coretk/coretk/data/icons/tunnel.png new file mode 100644 index 0000000000000000000000000000000000000000..2871b74f01e8f4080a3a88e30cabaed82a182b3a GIT binary patch literal 2256 zcmV;>2ru`EP)J3k%D_a<^qW!7*UKY22>d7`&}r zz{%E%)VMd*Ra-YoA9_(=s=g!-ReDJ+KQxL`rHYgkD!Hyz>Nu^esx3|$2u>ty?BY^` zi!m3oP;=YeGkw4qa53zeGv}Peo!=9z_WOUc^FNn4-^>|6AP@)y0)apv5C{YUfk5#8 z0x`M)>k*AcO#{<&)dZ$0BGauPWD6)QgG3#m!~?)__p5h6=xxa44hXqMAos~cx`L#9 zu{hiIL0el}(#YSj9%*MledKT#G-{Wp;!gwgD1dzcB#lxf0rDph`E~o;RQvh!=jSxalQM=H5DrJ3v$pv!1LQjZ z3iQlPK}RJbos1_eKk0g>Ev9Ggl^9|`P0cZjDfi~{gp%(8@EXyE6-)r+g~XWu<*u$@ z#Pv?Dy8(4g&3h%_Bn2MPJNE|k5#acRv%h^u&!oB;5DtekX07g*K=3u5c^}`ofbV|k zKQKwai?2HI*B+}HIZ6pm_{hSyJ32b%d9`spbTOdeKuZaw^i2TuytU8#d;&Qh82~UJ zCph-QZ1fG7RH~A03G$K7v#0-|zJ=?f)o9}$4Nc8YQc5pEgU$zlq`dsM%|h|Qb5pRG z)vD5J8RY%C#zXtmDs6Ku*5pSoMF9_sJw z=Aw8Xn%o2G8e3Wjq3zJn;FS;p>~EMyD3Ci+fs`fIM^I?5SV#7Uph1UE`sB zAkyy%VB@WAFbj~$p?A-ndYh*xR|D$zAFPCAdLJ5I^9?3ul1Rt>&et7JCN8D?fs}nEZYfUSXKy$*h)Ri?accb$faiFKaXAC(nwt0CeZox}N(++rIv)U@iOiyL51)ry zLUDXgr1cX#v{V}q4u{PIIDs@NZ7*^4y7_-bLtj5~hnGB|vV5YZ=9uaswrT@rGF;yR zP^DIfH}M4U{g+*sk1rn=obCev017-}@{Jrfs!k3J^e(V!sJaQy*%rP5z^6t-YdSAl(ek`zQx3ja zTl}j4USbtsJ}JZDaK^07eGNc>RpTMyrsT4gb+)z5vIH~V17>XQ&q8!Q0OTj97Ft<@ znGGPov#hZXA5UK|z0CMPgFcn!Th@8MF>BeC)|XisPQx{~0v4evU0Ep^8f~iVdI?Ta?D=&5m6SOr5{P z-u=zk{usj|4x+qol#hZ4wknR%5tGS`ro-wVQ6f)$9wxK0VkeQ6_YKQ1w1go>H@5Gp zhc_orwT>spk8M@E6~$13Ql)uV3~)2V=!VixTUTpmt1`BBGZ-n+($*I0H5BwJjW}`6u&(EHm2{Sc2v7_{204&9q|SJ z+ed5~0|;3>5_5wg5J}mIyLr=q;6uqh0%WjYWjt%Tj7 zd@d-I?}y*Fe3_yY=hwa*_NtIT@d{2(Sq zhCyX{<1eR3+8H285^{ne_yR$weev@Cqkr(`D2B%-nDxvSE8MNT>N3 zz*0QhEbu7OX=Z?$;u#B)MDT^?Mr36!?;G;&hnH~SqsyA*OvY5W+7Ggiur0`R(* zm*&ed#Rr$TZEUz9YKnBK4d7WkV`a?=ml=|70*~$Yr>_AL&=#HU!c&i+ya3>AP@)y0)apv e5C{Z<^yj~|p)*WPe", self.click_release) - # self.canvas.tag_bind(self.id, "", self.motion) def shape_motion(self, x1, y1): self.canvas.coords(self.id, self.x0, self.y0, x1, y1) @@ -69,5 +68,11 @@ class Shape: self.id, x0 + delta_x, y0 + delta_y, x1 + delta_x, y1 + delta_y ) self.canvas.canvas_management.node_drag(self, delta_x, delta_y) + if self.text_id is not None: + self.canvas.move(self.text_id, delta_x, delta_y) self.cursor_x = event.x self.cursor_y = event.y + + def delete(self): + self.canvas.delete(self.id) + self.canvas.delete(self.text_id) From 5a387537bb9f57d99b211379509694bdf8da4e90 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 4 Dec 2019 13:40:35 -0800 Subject: [PATCH 310/462] updates clean up setting wallpaper for graph canvas, updates to read metadata for canvas to display wallpaper from loaded xml file, update to allow reopening mobility player from node context --- coretk/coretk/coreclient.py | 37 ++++++++++++++----- coretk/coretk/data/xmls/sample1.xml | 2 +- coretk/coretk/dialogs/canvasbackground.py | 23 +++--------- coretk/coretk/dialogs/mobilityplayer.py | 44 ++++++++++++++++++++--- coretk/coretk/graph.py | 24 +++++++++++-- 5 files changed, 94 insertions(+), 36 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index fec00a73..ef617ac0 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -1,12 +1,14 @@ """ Incorporate grpc into python tkinter GUI """ +import json import logging import os import time from core.api.grpc import client, core_pb2 -from coretk.dialogs.mobilityplayer import MobilityPlayerDialog +from coretk import appconfig +from coretk.dialogs.mobilityplayer import MobilityPlayer from coretk.dialogs.sessions import SessionsDialog from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils @@ -141,6 +143,8 @@ class CoreClient: logging.warning("unknown session event: %s", session_event) elif event.HasField("node_event"): self.handle_node_event(event.node_event) + elif event.HasField("config_event"): + logging.info("config event: %s", event) else: logging.info("unhandled event: %s", event) @@ -164,7 +168,7 @@ class CoreClient: x = event.node.position.x y = event.node.position.y canvas_node = self.canvas_nodes[node_id] - canvas_node.move(x, y) + canvas_node.move(x, y, update=False) def handle_throughputs(self, event): interface_throughputs = event.interface_throughputs @@ -189,7 +193,6 @@ class CoreClient: # get session data response = self.client.get_session(self.session_id) - logging.info("joining session(%s): %s", self.session_id, response) session = response.session self.state = session.state self.client.events(self.session_id, self.handle_events) @@ -235,9 +238,6 @@ class CoreClient: node_id = int(_id / 1000) self.set_emane_model_config(node_id, config.model, config.config, interface) - # draw session - self.app.canvas.reset_and_redraw(session) - # get node service config and file config for node in session.nodes: self.created_nodes.add(node.id) @@ -262,6 +262,13 @@ class CoreClient: self.file_configs[node.id][service] = {} self.file_configs[node.id][service][file] = response.data + # draw session + self.app.canvas.reset_and_redraw(session) + + # get metadata + response = self.client.get_session_metadata(self.session_id) + self.parse_metadata(response.config) + if self.is_runtime(): self.app.toolbar.runtime_frame.tkraise() else: @@ -271,6 +278,18 @@ class CoreClient: def is_runtime(self): return self.state == core_pb2.SessionState.RUNTIME + def parse_metadata(self, config): + # canvas settings + canvas_config = config.get("canvas") + if canvas_config: + logging.info("canvas metadata: %s", canvas_config) + canvas_config = json.loads(canvas_config) + wallpaper_style = canvas_config["wallpaper-style"] + self.app.canvas.scale_option.set(wallpaper_style) + wallpaper = canvas_config["wallpaper"] + wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) + self.app.canvas.set_wallpaper(wallpaper) + def create_new_session(self): """ Create a new session @@ -372,9 +391,9 @@ class CoreClient: # display mobility players for node_id, config in self.mobility_configs.items(): canvas_node = self.canvas_nodes[node_id] - dialog = MobilityPlayerDialog(self.app, self.app, canvas_node, config) - dialog.show() - self.mobility_players[node_id] = dialog + mobility_player = MobilityPlayer(self.app, self.app, canvas_node, config) + mobility_player.show() + self.mobility_players[node_id] = mobility_player def stop_session(self, session_id=None): if not session_id: diff --git a/coretk/coretk/data/xmls/sample1.xml b/coretk/coretk/data/xmls/sample1.xml index 8bda5b8c..7c8ccf72 100644 --- a/coretk/coretk/data/xmls/sample1.xml +++ b/coretk/coretk/data/xmls/sample1.xml @@ -269,7 +269,7 @@ router ospf6 - + diff --git a/coretk/coretk/dialogs/canvasbackground.py b/coretk/coretk/dialogs/canvasbackground.py index 84b0bdb6..844b5595 100644 --- a/coretk/coretk/dialogs/canvasbackground.py +++ b/coretk/coretk/dialogs/canvasbackground.py @@ -5,14 +5,11 @@ import logging import tkinter as tk from tkinter import filedialog, ttk -from PIL import Image - from coretk.appconfig import BACKGROUNDS_PATH from coretk.dialogs.dialog import Dialog from coretk.images import Images PADX = 5 -ABOVE_WALLPAPER = ["edge", "linkinfo", "wireless", "antenna", "nodename", "node"] class CanvasBackgroundDialog(Dialog): @@ -178,23 +175,11 @@ class CanvasBackgroundDialog(Dialog): filename = self.filename.get() if not filename: - self.canvas.delete(self.canvas.wallpaper_id) - self.canvas.wallpaper = None - self.canvas.wallpaper_file = None - self.destroy() - return - try: - img = Image.open(filename) - self.canvas.wallpaper = img - self.canvas.wallpaper_file = filename - self.canvas.redraw() - for component in ABOVE_WALLPAPER: - self.canvas.tag_raise(component) + filename = None + try: + self.canvas.set_wallpaper(filename) except FileNotFoundError: logging.error("invalid background: %s", filename) - if self.canvas.wallpaper_id: - self.canvas.delete(self.canvas.wallpaper_id) - self.canvas.wallpaper_id = None - self.canvas.wallpaper_file = None + self.destroy() diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index 65b16058..136e6179 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -1,7 +1,7 @@ import tkinter as tk from tkinter import ttk -from core.api.grpc import core_pb2 +from core.api.grpc.core_pb2 import MobilityAction from coretk.dialogs.dialog import Dialog from coretk.images import ImageEnum, Images @@ -9,6 +9,42 @@ PAD = 5 ICON_SIZE = 16 +class MobilityPlayer: + def __init__(self, master, app, canvas_node, config): + self.master = master + self.app = app + self.canvas_node = canvas_node + self.config = config + self.dialog = None + self.state = None + + def show(self): + if self.dialog: + self.dialog.destroy() + self.dialog = MobilityPlayerDialog( + self.master, self.app, self.canvas_node, self.config + ) + if self.state == MobilityAction.START: + self.set_play() + elif self.state == MobilityAction.PAUSE: + self.set_pause() + else: + self.set_stop() + self.dialog.show() + + def set_play(self): + self.dialog.set_play() + self.state = MobilityAction.START + + def set_pause(self): + self.dialog.set_pause() + self.state = MobilityAction.PAUSE + + def set_stop(self): + self.dialog.set_stop() + self.state = MobilityAction.STOP + + class MobilityPlayerDialog(Dialog): def __init__(self, master, app, canvas_node, config): super().__init__( @@ -88,19 +124,19 @@ class MobilityPlayerDialog(Dialog): self.set_play() session_id = self.app.core.session_id self.app.core.client.mobility_action( - session_id, self.node.id, core_pb2.MobilityAction.START + session_id, self.node.id, MobilityAction.START ) def click_pause(self): self.set_pause() session_id = self.app.core.session_id self.app.core.client.mobility_action( - session_id, self.node.id, core_pb2.MobilityAction.PAUSE + session_id, self.node.id, MobilityAction.PAUSE ) def click_stop(self): self.set_stop() session_id = self.app.core.session_id self.app.core.client.mobility_action( - session_id, self.node.id, core_pb2.MobilityAction.STOP + session_id, self.node.id, MobilityAction.STOP ) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 7389be52..7ef341a3 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -3,7 +3,7 @@ import logging import tkinter as tk from tkinter import font -from PIL import ImageTk +from PIL import Image, ImageTk from core.api.grpc import core_pb2 from core.api.grpc.core_pb2 import NodeType @@ -21,6 +21,7 @@ from coretk.nodeutils import NodeUtils from coretk.shape import Shape NODE_TEXT_OFFSET = 5 +ABOVE_WALLPAPER = ["edge", "linkinfo", "wireless", "antenna", "nodename", "node"] class GraphMode(enum.Enum): @@ -583,6 +584,21 @@ class CanvasGraph(tk.Canvas): else: self.itemconfig("gridline", state=tk.HIDDEN) + def set_wallpaper(self, filename): + logging.info("setting wallpaper: %s", filename) + if filename is not None: + img = Image.open(filename) + self.wallpaper = img + self.wallpaper_file = filename + self.redraw() + for component in ABOVE_WALLPAPER: + self.tag_raise(component) + else: + if self.wallpaper_id is not None: + self.delete(self.wallpaper_id) + self.wallpaper = None + self.wallpaper_file = None + def is_selection_mode(self): return self.mode == GraphMode.SELECT @@ -696,7 +712,7 @@ class CanvasNode: self.canvas.itemconfig(self.id, image=self.image) self.canvas.itemconfig(self.text_id, text=self.core_node.name) - def move(self, x, y): + def move(self, x, y, update=True): old_x = self.core_node.position.x old_y = self.core_node.position.y x_offset = x - old_x @@ -720,7 +736,7 @@ class CanvasNode: self.canvas.coords(edge.id, x, y, x2, y2) else: self.canvas.coords(edge.id, x1, y1, x, y) - if self.app.core.is_runtime(): + if self.app.core.is_runtime() and update: self.app.core.edit_node(self.core_node.id, int(x), int(y)) def on_enter(self, event): @@ -784,6 +800,8 @@ class CanvasNode: def show_mobility_player(self): self.canvas.context = None + mobility_player = self.app.core.mobility_players[self.core_node.id] + mobility_player.show() def show_emane_config(self): self.canvas.context = None From 5aa01d9bb55f735dae3357df86252714b67fe190 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 4 Dec 2019 14:44:43 -0800 Subject: [PATCH 311/462] simplify some join session logic to loop over nodes once --- coretk/coretk/coreclient.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index ef617ac0..0f7eb996 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -211,12 +211,6 @@ class CoreClient: for hook in response.hooks: self.hooks[hook.file] = hook - # get wlan configs - for node in session.nodes: - if node.type == core_pb2.NodeType.WIRELESS_LAN: - response = self.client.get_wlan_config(self.session_id, node.id) - self.wlan_configs[node.id] = response.config - # get mobility configs response = self.client.get_mobility_configs(self.session_id) for node_id in response.configs: @@ -238,13 +232,17 @@ class CoreClient: node_id = int(_id / 1000) self.set_emane_model_config(node_id, config.model, config.config, interface) - # get node service config and file config + # save and retrieve data, needed for session nodes for node in session.nodes: + # get node service config and file config self.created_nodes.add(node.id) - for link in session.links: - self.created_links.add(tuple(sorted([link.node_one_id, link.node_two_id]))) - for node in session.nodes: - if node.type == core_pb2.NodeType.DEFAULT: + + # get wlan configs for wlan nodes + if node.type == core_pb2.NodeType.WIRELESS_LAN: + response = self.client.get_wlan_config(self.session_id, node.id) + self.wlan_configs[node.id] = response.config + # retrieve service configurations data for default nodes + elif node.type == core_pb2.NodeType.DEFAULT: for service in node.services: response = self.client.get_node_service( self.session_id, node.id, service @@ -262,6 +260,10 @@ class CoreClient: self.file_configs[node.id][service] = {} self.file_configs[node.id][service][file] = response.data + # store links as created links + for link in session.links: + self.created_links.add(tuple(sorted([link.node_one_id, link.node_two_id]))) + # draw session self.app.canvas.reset_and_redraw(session) From 96d273815d8583367ba9d9809977ce82d367d9ed Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 4 Dec 2019 15:08:05 -0800 Subject: [PATCH 312/462] support moving multiple nodes and shape --- coretk/coretk/graph.py | 60 +++++++++++++++++++++++++------------ coretk/coretk/nodedelete.py | 3 ++ coretk/coretk/shape.py | 16 +++++----- 3 files changed, 51 insertions(+), 28 deletions(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 6f75e78c..e5a79612 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -361,9 +361,6 @@ class CanvasGraph(tk.Canvas): :return: nothing """ logging.debug(f"click press: {event}") - self.delete(self.find_withtag("selectednodes")) - self.canvas_management.selected.clear() - selected = self.get_selected(event) is_node = selected in self.find_withtag("node") if self.mode == GraphMode.EDGE and is_node: @@ -379,16 +376,19 @@ class CanvasGraph(tk.Canvas): self.selected = shape.id self.shapes[shape.id] = shape self.shape_drawing = True - if ( - self.mode == GraphMode.SELECT - and selected is not None - and "shape" in self.gettags(selected) - ): - x, y = self.canvas_xy(event) - self.shapes[selected].cursor_x = x - self.shapes[selected].cursor_y = y - self.canvas_management.node_select(self.shapes[selected]) - self.selected = selected + if self.mode == GraphMode.SELECT: + if selected is not None: + if "shape" in self.gettags(selected): + x, y = self.canvas_xy(event) + self.shapes[selected].cursor_x = x + self.shapes[selected].cursor_y = y + if selected not in self.canvas_management.selected: + self.canvas_management.node_select(self.shapes[selected]) + self.selected = selected + else: + for i in self.find_withtag("selectednodes"): + self.delete(i) + self.canvas_management.selected.clear() def ctrl_click(self, event): logging.debug("Control left click %s", event) @@ -419,7 +419,19 @@ class CanvasGraph(tk.Canvas): and self.selected is not None and "shape" in self.gettags(self.selected) ): - self.shapes[self.selected].motion(event) + x, y = self.canvas_xy(event) + shape = self.shapes[self.selected] + delta_x = x - shape.cursor_x + delta_y = y - shape.cursor_y + shape.motion(event) + # move other selected components + for nid in self.canvas_management.selected: + if nid != self.selected and nid in self.shapes: + self.shapes[nid].motion(None, delta_x, delta_y) + if nid != self.selected and nid in self.nodes: + node_x = self.nodes[nid].core_node.position.x + node_y = self.nodes[nid].core_node.position.y + self.nodes[nid].move(node_x + delta_x, node_y + delta_y) def click_context(self, event): logging.info("context event: %s", self.context) @@ -753,8 +765,9 @@ class CanvasNode: def click_press(self, event): logging.debug(f"node click press {self.core_node.name}: {event}") self.moving = self.canvas.canvas_xy(event) - self.canvas.canvas_management.node_select(self) - self.canvas.selected = self.id + if self.id not in self.canvas.canvas_management.selected: + self.canvas.canvas_management.node_select(self) + self.canvas.selected = self.id def click_release(self, event): logging.debug(f"node click release {self.core_node.name}: {event}") @@ -765,10 +778,19 @@ class CanvasNode: if self.canvas.mode == GraphMode.EDGE: return x, y = self.canvas.canvas_xy(event) + my_x = self.core_node.position.x + my_y = self.core_node.position.y self.move(x, y) - # for nid, bboxid in self.canvas.canvas_management.selected.items(): - # if nid in self.canvas.nodes: - # self.canvas.nodes[nid].motion(event) + # move other selected components + for nid, bboxid in self.canvas.canvas_management.selected.items(): + if nid != self.id and nid in self.canvas.nodes: + other_old_x = self.canvas.nodes[nid].core_node.position.x + other_old_y = self.canvas.nodes[nid].core_node.position.y + other_new_x = x + other_old_x - my_x + other_new_y = y + other_old_y - my_y + self.canvas.nodes[nid].move(other_new_x, other_new_y) + if nid != self.id and nid in self.canvas.shapes: + self.canvas.shapes[nid].motion(None, x - my_x, y - my_y) def select_multiple(self, event): self.canvas.canvas_management.node_select(self, True) diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index e30415c6..1365dc0f 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -33,6 +33,9 @@ class CanvasComponentManagement: tags="selectednodes", ) self.selected[canvas_node.id] = bbox_id + else: + bbox_id = self.selected.pop(canvas_node.id) + self.canvas.delete(bbox_id) def node_drag(self, canvas_node, offset_x, offset_y): select_id = self.selected.get(canvas_node.id) diff --git a/coretk/coretk/shape.py b/coretk/coretk/shape.py index d60beaef..c39c549b 100644 --- a/coretk/coretk/shape.py +++ b/coretk/coretk/shape.py @@ -59,19 +59,17 @@ class Shape: def click_release(self, event): logging.debug("Click release on shape %s", self.id) - def motion(self, event): + def motion(self, event, delta_x=None, delta_y=None): logging.debug("motion on shape %s", self.id) - delta_x = event.x - self.cursor_x - delta_y = event.y - self.cursor_y - x0, y0, x1, y1 = self.canvas.bbox(self.id) - self.canvas.coords( - self.id, x0 + delta_x, y0 + delta_y, x1 + delta_x, y1 + delta_y - ) + if event is not None: + delta_x = event.x - self.cursor_x + delta_y = event.y - self.cursor_y + self.cursor_x = event.x + self.cursor_y = event.y + self.canvas.move(self.id, delta_x, delta_y) self.canvas.canvas_management.node_drag(self, delta_x, delta_y) if self.text_id is not None: self.canvas.move(self.text_id, delta_x, delta_y) - self.cursor_x = event.x - self.cursor_y = event.y def delete(self): self.canvas.delete(self.id) From d024bdf0b73512deca2d066693769c130600038c Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 4 Dec 2019 15:12:31 -0800 Subject: [PATCH 313/462] fix small logic --- coretk/coretk/graph.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index bb2efcad..60f4e498 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -394,7 +394,11 @@ class CanvasGraph(tk.Canvas): def ctrl_click(self, event): logging.debug("Control left click %s", event) selected = self.get_selected(event) - if self.mode == GraphMode.SELECT and "shape" in self.gettags(selected): + if ( + self.mode == GraphMode.SELECT + and selected is not None + and "shape" in self.gettags(selected) + ): self.canvas_management.node_select(self.shapes[selected], True) def click_motion(self, event): From c7b9c7bfb414fbde8d70abb89bf0b249e621a1b3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 4 Dec 2019 16:40:33 -0800 Subject: [PATCH 314/462] updated sample1 xml to store all metadata in json format --- coretk/coretk/data/xmls/sample1.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/data/xmls/sample1.xml b/coretk/coretk/data/xmls/sample1.xml index 7c8ccf72..c780d21b 100644 --- a/coretk/coretk/data/xmls/sample1.xml +++ b/coretk/coretk/data/xmls/sample1.xml @@ -266,11 +266,11 @@ router ospf6 - - - + + + - + From 59614fa8d3cacfe4d498381e1d90ec01d3912564 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 5 Dec 2019 10:12:31 -0800 Subject: [PATCH 315/462] try to load canvas config --- coretk/coretk/coreclient.py | 32 ++++++++ coretk/coretk/dialogs/shapemod.py | 7 +- coretk/coretk/graph.py | 4 +- coretk/coretk/graph_helper.py | 2 + coretk/coretk/parsedata.py | 28 +++++++ coretk/coretk/shape.py | 123 +++++++++++++++++++++++------- 6 files changed, 165 insertions(+), 31 deletions(-) create mode 100644 coretk/coretk/parsedata.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 0f7eb996..79bc5359 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -13,6 +13,8 @@ from coretk.dialogs.sessions import SessionsDialog from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils +# from coretk.shape import Shape, ShapeData + OBSERVERS = { "processes": "ps", "ifconfig": "ifconfig", @@ -281,6 +283,36 @@ class CoreClient: return self.state == core_pb2.SessionState.RUNTIME def parse_metadata(self, config): + # for key, value in config.items(): + # if "global_options" != key: + # canvas_config = parsedata.parse(value) + # print(canvas_config) + # if canvas_config.get("type"): + # if canvas_config["type"] == "rectangle": + # data = ShapeData(False, canvas_config["label"], + # canvas_config["fontfamily"], + # canvas_config["fontsize"], + # canvas_config["labelcolor"], + # canvas_config["color"], + # canvas_config["border"], + # canvas_config["width"], + # ) + # coords = tuple([float(x) for x in canvas_config["iconcoords"].split()]) + # print(coords) + # shape = Shape(self.app, self.app.canvas, None, None, coords, data, canvas_config["type"]) + # self.app.canvas.shapes[shape.id] = shape + # elif canvas_config["type"] == "oval": + # print("not implemented") + # elif canvas_config["type"] == "text": + # print("not implemented") + # else: + # if "wallpaper" in canvas_config: + # logging.info("canvas metadata: %s", canvas_config) + # wallpaper_style = canvas_config["wallpaper-style"] + # self.app.canvas.scale_option.set(wallpaper_style) + # wallpaper = canvas_config["wallpaper"] + # wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) + # self.app.canvas.set_wallpaper(wallpaper) # canvas settings canvas_config = config.get("canvas") if canvas_config: diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index 005ad000..18ba0eca 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -158,7 +158,12 @@ class ShapeDialog(Dialog): f.append("underline") if shape.text_id is None: shape.text_id = self.canvas.create_text( - text_x, text_y, text=shape_text, fill=self.text_color, font=f + text_x, + text_y, + text=shape_text, + fill=self.text_color, + font=f, + tags="shapetext", ) self.canvas.shapes[self.id].created = True else: diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 60f4e498..24f07727 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -132,6 +132,7 @@ class CanvasGraph(tk.Canvas): self.selected = None self.nodes.clear() self.edges.clear() + self.shapes.clear() self.wireless_edges.clear() self.drawing_edge = None self.draw_session(session) @@ -451,8 +452,6 @@ class CanvasGraph(tk.Canvas): self.context.unpost() self.context = None - # TODO rather than delete, might move the data to somewhere else in order to reuse - # TODO when the user undo def press_delete(self, event): """ delete selected nodes and any data that relates to it @@ -585,6 +584,7 @@ class CanvasGraph(tk.Canvas): if self.adjust_to_dim.get(): self.resize_to_wallpaper() else: + print(self.scale_option.get()) option = ScaleOption(self.scale_option.get()) if option == ScaleOption.UPPER_LEFT: self.wallpaper_upper_left() diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py index eea4ff55..af6d4823 100644 --- a/coretk/coretk/graph_helper.py +++ b/coretk/coretk/graph_helper.py @@ -16,6 +16,8 @@ CANVAS_COMPONENT_TAGS = [ "antenna", "wireless", "selectednodes", + "shape", + "shapetext", ] diff --git a/coretk/coretk/parsedata.py b/coretk/coretk/parsedata.py new file mode 100644 index 00000000..9afff3e2 --- /dev/null +++ b/coretk/coretk/parsedata.py @@ -0,0 +1,28 @@ +""" +parse meta data +""" +# from coretk.graph import ScaleOption + + +def parse(meta_string): + parsed = {} + if meta_string[0] == "{" and meta_string[len(meta_string) - 1] == "}": + meta_string = meta_string[1:-1] + for key_value in meta_string.split("} {"): + if key_value[len(key_value) - 1] == "}": + key, value = key_value[:-1].split(" {") + if key == "wallpaper-style": + if value == "upperleft": + parsed[key] = 1 + elif value == "centered": + parsed[key] = 2 + elif value == "scaled": + parsed[key] = 3 + elif value == "tiled": + parsed[key] = 4 + else: + parsed[key] = value + else: + key, value = tuple(key_value.split()) + parsed[key] = value + return parsed diff --git a/coretk/coretk/shape.py b/coretk/coretk/shape.py index c39c549b..63025b34 100644 --- a/coretk/coretk/shape.py +++ b/coretk/coretk/shape.py @@ -10,41 +10,108 @@ ABOVE_COMPONENT = ["gridline", "edge", "linkinfo", "antenna", "node", "nodename" class ShapeData: - def __init__(self): - self.text = "" - self.font = "Arial" - self.font_size = 12 - self.text_color = "#000000" - self.fill_color = "#CFCFFF" - self.border_color = "#000000" - self.border_width = 0 - self.bold = 0 - self.italic = 0 - self.underline = 0 + def __init__( + self, + is_default=True, + text=None, + font=None, + font_size=None, + text_color=None, + fill_color=None, + border_color=None, + border_width=None, + bold=0, + italic=0, + underline=0, + ): + if is_default: + self.text = "" + self.font = "Arial" + self.font_size = 12 + self.text_color = "#000000" + self.fill_color = "#CFCFFF" + self.border_color = "#000000" + self.border_width = 0 + self.bold = 0 + self.italic = 0 + self.underline = 0 + else: + self.text = text + self.font = font + self.font_size = font_size + self.text_color = text_color + self.fill_color = fill_color + self.border_color = border_color + self.border_width = border_width + self.bold = bold + self.italic = italic + self.underline = underline class Shape: - def __init__(self, app, canvas, top_x, top_y): + def __init__( + self, + app, + canvas, + top_x=None, + top_y=None, + coords=None, + data=None, + shape_type=None, + ): self.app = app self.canvas = canvas - self.x0 = top_x - self.y0 = top_y + if data is None: + self.x0 = top_x + self.y0 = top_y + self.created = False + self.text_id = None + self.shape_data = ShapeData() + canvas.delete(canvas.find_withtag("selectednodes")) + annotation_type = self.canvas.annotation_type + if annotation_type == ImageEnum.OVAL: + self.id = canvas.create_oval( + top_x, top_y, top_x, top_y, tags="shape", dash="-" + ) + elif annotation_type == ImageEnum.RECTANGLE: + self.id = canvas.create_rectangle( + top_x, top_y, top_x, top_y, tags="shape", dash="-" + ) + else: + x0, y0, x1, y1 = coords + self.x0 = x0 + self.y0 = y0 + self.created = True + if shape_type == "oval": + self.id = self.canvas.create_oval( + x0, + y0, + x1, + y1, + tags="shape", + fill=data.fill_color, + outline=data.border_color, + width=data.border_width, + ) + elif shape_type == "rectangle": + self.id = self.canvas.create_rectangle( + x0, + y0, + x1, + y1, + tags="shape", + fill=data.fill_color, + outline=data.border_color, + width=data.border_width, + ) + _x = (x0 + x1) / 2 + _y = (y0 + y1) / 2 + self.text_id = self.canvas.create_text( + _x, _y, text=data.text, fill=data.text_color + ) + self.shape_data = data self.cursor_x = None self.cursor_y = None - self.created = False - self.text_id = None - - self.shape_data = ShapeData() - canvas.delete(canvas.find_withtag("selectednodes")) - annotation_type = self.canvas.annotation_type - if annotation_type == ImageEnum.OVAL: - self.id = canvas.create_oval( - top_x, top_y, top_x, top_y, tags="shape", dash="-" - ) - elif annotation_type == ImageEnum.RECTANGLE: - self.id = canvas.create_rectangle( - top_x, top_y, top_x, top_y, tags="shape", dash="-" - ) self.canvas.tag_bind(self.id, "", self.click_release) def shape_motion(self, x1, y1): From 8c30ad6af57bfde50e8a4c90fdf0bb958ecbd57c Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 5 Dec 2019 10:39:53 -0800 Subject: [PATCH 316/462] commit before splitting files --- coretk/coretk/coreclient.py | 75 +++++++++++++++++++++---------------- coretk/coretk/parsedata.py | 1 - 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 79bc5359..1a7d06e3 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -7,13 +7,12 @@ import os import time from core.api.grpc import client, core_pb2 -from coretk import appconfig +from coretk import appconfig, parsedata from coretk.dialogs.mobilityplayer import MobilityPlayer from coretk.dialogs.sessions import SessionsDialog from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils - -# from coretk.shape import Shape, ShapeData +from coretk.shape import Shape, ShapeData OBSERVERS = { "processes": "ps", @@ -283,36 +282,46 @@ class CoreClient: return self.state == core_pb2.SessionState.RUNTIME def parse_metadata(self, config): - # for key, value in config.items(): - # if "global_options" != key: - # canvas_config = parsedata.parse(value) - # print(canvas_config) - # if canvas_config.get("type"): - # if canvas_config["type"] == "rectangle": - # data = ShapeData(False, canvas_config["label"], - # canvas_config["fontfamily"], - # canvas_config["fontsize"], - # canvas_config["labelcolor"], - # canvas_config["color"], - # canvas_config["border"], - # canvas_config["width"], - # ) - # coords = tuple([float(x) for x in canvas_config["iconcoords"].split()]) - # print(coords) - # shape = Shape(self.app, self.app.canvas, None, None, coords, data, canvas_config["type"]) - # self.app.canvas.shapes[shape.id] = shape - # elif canvas_config["type"] == "oval": - # print("not implemented") - # elif canvas_config["type"] == "text": - # print("not implemented") - # else: - # if "wallpaper" in canvas_config: - # logging.info("canvas metadata: %s", canvas_config) - # wallpaper_style = canvas_config["wallpaper-style"] - # self.app.canvas.scale_option.set(wallpaper_style) - # wallpaper = canvas_config["wallpaper"] - # wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) - # self.app.canvas.set_wallpaper(wallpaper) + for key, value in config.items(): + if "global_options" != key: + canvas_config = parsedata.parse(value) + print(canvas_config) + if canvas_config.get("type"): + config_type = canvas_config["type"] + if config_type == "rectangle" or config_type == "oval": + data = ShapeData( + False, + canvas_config["label"], + canvas_config["fontfamily"], + canvas_config["fontsize"], + canvas_config["labelcolor"], + canvas_config["color"], + canvas_config["border"], + canvas_config["width"], + ) + coords = tuple( + [float(x) for x in canvas_config["iconcoords"].split()] + ) + shape = Shape( + self.app, + self.app.canvas, + None, + None, + coords, + data, + config_type, + ) + self.app.canvas.shapes[shape.id] = shape + elif canvas_config["type"] == "text": + print("not implemented") + else: + if "wallpaper" in canvas_config: + logging.info("canvas metadata: %s", canvas_config) + wallpaper_style = canvas_config["wallpaper-style"] + self.app.canvas.scale_option.set(wallpaper_style) + wallpaper = canvas_config["wallpaper"] + wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) + self.app.canvas.set_wallpaper(wallpaper) # canvas settings canvas_config = config.get("canvas") if canvas_config: diff --git a/coretk/coretk/parsedata.py b/coretk/coretk/parsedata.py index 9afff3e2..e4312846 100644 --- a/coretk/coretk/parsedata.py +++ b/coretk/coretk/parsedata.py @@ -1,7 +1,6 @@ """ parse meta data """ -# from coretk.graph import ScaleOption def parse(meta_string): From d970d5ee85d071ecad9e61df248cf782f8479a5c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 5 Dec 2019 11:12:25 -0800 Subject: [PATCH 317/462] separated out graph code into more file and its own sub package --- coretk/coretk/app.py | 4 +- coretk/coretk/coreclient.py | 83 ++++--- coretk/coretk/graph/__init__.py | 0 coretk/coretk/{ => graph}/canvastooltip.py | 0 coretk/coretk/graph/edges.py | 70 ++++++ coretk/coretk/graph/enums.py | 23 ++ coretk/coretk/{ => graph}/graph.py | 260 +-------------------- coretk/coretk/{ => graph}/graph_helper.py | 0 coretk/coretk/{ => graph}/linkinfo.py | 0 coretk/coretk/graph/node.py | 163 +++++++++++++ coretk/coretk/{ => graph}/nodedelete.py | 0 coretk/coretk/{ => graph}/shape.py | 0 coretk/coretk/{status.py => statusbar.py} | 0 coretk/coretk/toolbar.py | 2 +- 14 files changed, 307 insertions(+), 298 deletions(-) create mode 100644 coretk/coretk/graph/__init__.py rename coretk/coretk/{ => graph}/canvastooltip.py (100%) create mode 100644 coretk/coretk/graph/edges.py create mode 100644 coretk/coretk/graph/enums.py rename coretk/coretk/{ => graph}/graph.py (71%) rename coretk/coretk/{ => graph}/graph_helper.py (100%) rename coretk/coretk/{ => graph}/linkinfo.py (100%) create mode 100644 coretk/coretk/graph/node.py rename coretk/coretk/{ => graph}/nodedelete.py (100%) rename coretk/coretk/{ => graph}/shape.py (100%) rename coretk/coretk/{status.py => statusbar.py} (100%) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index c8232ebd..2dd2c5ca 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -5,12 +5,12 @@ from tkinter import ttk from coretk import appconfig, themes from coretk.coreclient import CoreClient -from coretk.graph import CanvasGraph +from coretk.graph.graph import CanvasGraph from coretk.images import ImageEnum, Images from coretk.menuaction import MenuAction from coretk.menubar import Menubar from coretk.nodeutils import NodeUtils -from coretk.status import StatusBar +from coretk.statusbar import StatusBar from coretk.toolbar import Toolbar diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 1a7d06e3..d0519bb5 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -7,12 +7,11 @@ import os import time from core.api.grpc import client, core_pb2 -from coretk import appconfig, parsedata +from coretk import appconfig from coretk.dialogs.mobilityplayer import MobilityPlayer from coretk.dialogs.sessions import SessionsDialog from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils -from coretk.shape import Shape, ShapeData OBSERVERS = { "processes": "ps", @@ -282,46 +281,46 @@ class CoreClient: return self.state == core_pb2.SessionState.RUNTIME def parse_metadata(self, config): - for key, value in config.items(): - if "global_options" != key: - canvas_config = parsedata.parse(value) - print(canvas_config) - if canvas_config.get("type"): - config_type = canvas_config["type"] - if config_type == "rectangle" or config_type == "oval": - data = ShapeData( - False, - canvas_config["label"], - canvas_config["fontfamily"], - canvas_config["fontsize"], - canvas_config["labelcolor"], - canvas_config["color"], - canvas_config["border"], - canvas_config["width"], - ) - coords = tuple( - [float(x) for x in canvas_config["iconcoords"].split()] - ) - shape = Shape( - self.app, - self.app.canvas, - None, - None, - coords, - data, - config_type, - ) - self.app.canvas.shapes[shape.id] = shape - elif canvas_config["type"] == "text": - print("not implemented") - else: - if "wallpaper" in canvas_config: - logging.info("canvas metadata: %s", canvas_config) - wallpaper_style = canvas_config["wallpaper-style"] - self.app.canvas.scale_option.set(wallpaper_style) - wallpaper = canvas_config["wallpaper"] - wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) - self.app.canvas.set_wallpaper(wallpaper) + # for key, value in config.items(): + # if "global_options" != key: + # canvas_config = parsedata.parse(value) + # print(canvas_config) + # if canvas_config.get("type"): + # config_type = canvas_config["type"] + # if config_type == "rectangle" or config_type == "oval": + # data = ShapeData( + # False, + # canvas_config["label"], + # canvas_config["fontfamily"], + # canvas_config["fontsize"], + # canvas_config["labelcolor"], + # canvas_config["color"], + # canvas_config["border"], + # canvas_config["width"], + # ) + # coords = tuple( + # [float(x) for x in canvas_config["iconcoords"].split()] + # ) + # shape = Shape( + # self.app, + # self.app.canvas, + # None, + # None, + # coords, + # data, + # config_type, + # ) + # self.app.canvas.shapes[shape.id] = shape + # elif canvas_config["type"] == "text": + # print("not implemented") + # else: + # if "wallpaper" in canvas_config: + # logging.info("canvas metadata: %s", canvas_config) + # wallpaper_style = canvas_config["wallpaper-style"] + # self.app.canvas.scale_option.set(wallpaper_style) + # wallpaper = canvas_config["wallpaper"] + # wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) + # self.app.canvas.set_wallpaper(wallpaper) # canvas settings canvas_config = config.get("canvas") if canvas_config: diff --git a/coretk/coretk/graph/__init__.py b/coretk/coretk/graph/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/coretk/coretk/canvastooltip.py b/coretk/coretk/graph/canvastooltip.py similarity index 100% rename from coretk/coretk/canvastooltip.py rename to coretk/coretk/graph/canvastooltip.py diff --git a/coretk/coretk/graph/edges.py b/coretk/coretk/graph/edges.py new file mode 100644 index 00000000..92f99400 --- /dev/null +++ b/coretk/coretk/graph/edges.py @@ -0,0 +1,70 @@ +import tkinter as tk + + +class CanvasWirelessEdge: + def __init__(self, token, position, src, dst, canvas): + self.token = token + self.src = src + self.dst = dst + self.canvas = canvas + self.id = self.canvas.create_line( + *position, tags="wireless", width=1.5, fill="#009933" + ) + + def delete(self): + self.canvas.delete(self.id) + + +class CanvasEdge: + """ + Canvas edge class + """ + + width = 1.4 + + def __init__(self, x1, y1, x2, y2, src, canvas, is_wired=None): + """ + Create an instance of canvas edge object + :param int x1: source x-coord + :param int y1: source y-coord + :param int x2: destination x-coord + :param int y2: destination y-coord + :param int src: source id + :param tkinter.Canvas canvas: canvas object + """ + self.src = src + self.dst = None + self.src_interface = None + self.dst_interface = None + self.canvas = canvas + if is_wired is None or is_wired is True: + self.id = self.canvas.create_line( + x1, y1, x2, y2, tags="edge", width=self.width, fill="#ff0000" + ) + else: + self.id = self.canvas.create_line( + x1, + y1, + x2, + y2, + tags="edge", + width=self.width, + fill="#ff0000", + state=tk.HIDDEN, + ) + self.token = None + self.link_info = None + self.throughput = None + self.wired = is_wired + + def complete(self, dst, x, y): + self.dst = dst + self.token = tuple(sorted((self.src, self.dst))) + x1, y1, _, _ = self.canvas.coords(self.id) + self.canvas.coords(self.id, x1, y1, x, y) + self.canvas.helper.draw_wireless_case(self.src, self.dst, self) + self.canvas.tag_raise(self.src) + self.canvas.tag_raise(self.dst) + + def delete(self): + self.canvas.delete(self.id) diff --git a/coretk/coretk/graph/enums.py b/coretk/coretk/graph/enums.py new file mode 100644 index 00000000..65fb10eb --- /dev/null +++ b/coretk/coretk/graph/enums.py @@ -0,0 +1,23 @@ +import enum + + +class GraphMode(enum.Enum): + SELECT = 0 + EDGE = 1 + PICKNODE = 2 + NODE = 3 + ANNOTATION = 4 + OTHER = 5 + + +class ScaleOption(enum.Enum): + NONE = 0 + UPPER_LEFT = 1 + CENTERED = 2 + SCALED = 3 + TILED = 4 + + +class ShapeType(enum.Enum): + OVAL = 0 + RECTANGLE = 1 diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph/graph.py similarity index 71% rename from coretk/coretk/graph.py rename to coretk/coretk/graph/graph.py index 24f07727..d39ca012 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph/graph.py @@ -1,51 +1,24 @@ -import enum import logging import tkinter as tk -from tkinter import font from PIL import Image, ImageTk from core.api.grpc import core_pb2 from core.api.grpc.core_pb2 import NodeType -from coretk.canvastooltip import CanvasTooltip -from coretk.dialogs.emaneconfig import EmaneConfigDialog -from coretk.dialogs.mobilityconfig import MobilityConfigDialog -from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.shapemod import ShapeDialog -from coretk.dialogs.wlanconfig import WlanConfigDialog -from coretk.graph_helper import GraphHelper, WlanAntennaManager +from coretk.graph.edges import CanvasEdge, CanvasWirelessEdge +from coretk.graph.enums import GraphMode, ScaleOption +from coretk.graph.graph_helper import GraphHelper +from coretk.graph.linkinfo import LinkInfo, Throughput +from coretk.graph.node import CanvasNode +from coretk.graph.nodedelete import CanvasComponentManagement +from coretk.graph.shape import Shape from coretk.images import ImageEnum, Images -from coretk.linkinfo import LinkInfo, Throughput -from coretk.nodedelete import CanvasComponentManagement from coretk.nodeutils import NodeUtils -from coretk.shape import Shape -NODE_TEXT_OFFSET = 5 ABOVE_WALLPAPER = ["edge", "linkinfo", "wireless", "antenna", "nodename", "node"] -class GraphMode(enum.Enum): - SELECT = 0 - EDGE = 1 - PICKNODE = 2 - NODE = 3 - ANNOTATION = 4 - OTHER = 5 - - -class ScaleOption(enum.Enum): - NONE = 0 - UPPER_LEFT = 1 - CENTERED = 2 - SCALED = 3 - TILED = 4 - - -class ShapeType(enum.Enum): - OVAL = 0 - RECTANGLE = 1 - - class CanvasGraph(tk.Canvas): def __init__(self, master, core, width, height, cnf=None, **kwargs): if cnf is None: @@ -620,222 +593,3 @@ class CanvasGraph(tk.Canvas): def is_selection_mode(self): return self.mode == GraphMode.SELECT - - -class CanvasWirelessEdge: - def __init__(self, token, position, src, dst, canvas): - self.token = token - self.src = src - self.dst = dst - self.canvas = canvas - self.id = self.canvas.create_line( - *position, tags="wireless", width=1.5, fill="#009933" - ) - - def delete(self): - self.canvas.delete(self.id) - - -class CanvasEdge: - """ - Canvas edge class - """ - - width = 1.4 - - def __init__(self, x1, y1, x2, y2, src, canvas, is_wired=None): - """ - Create an instance of canvas edge object - :param int x1: source x-coord - :param int y1: source y-coord - :param int x2: destination x-coord - :param int y2: destination y-coord - :param int src: source id - :param tkinter.Canvas canvas: canvas object - """ - self.src = src - self.dst = None - self.src_interface = None - self.dst_interface = None - self.canvas = canvas - if is_wired is None or is_wired is True: - self.id = self.canvas.create_line( - x1, y1, x2, y2, tags="edge", width=self.width, fill="#ff0000" - ) - else: - self.id = self.canvas.create_line( - x1, - y1, - x2, - y2, - tags="edge", - width=self.width, - fill="#ff0000", - state=tk.HIDDEN, - ) - self.token = None - self.link_info = None - self.throughput = None - self.wired = is_wired - - def complete(self, dst, x, y): - self.dst = dst - self.token = tuple(sorted((self.src, self.dst))) - x1, y1, _, _ = self.canvas.coords(self.id) - self.canvas.coords(self.id, x1, y1, x, y) - self.canvas.helper.draw_wireless_case(self.src, self.dst, self) - self.canvas.tag_raise(self.src) - self.canvas.tag_raise(self.dst) - - def delete(self): - self.canvas.delete(self.id) - - -class CanvasNode: - def __init__(self, app, core_node, image): - self.app = app - self.canvas = app.canvas - self.image = image - self.core_node = core_node - x = self.core_node.position.x - y = self.core_node.position.y - self.id = self.canvas.create_image( - x, y, anchor=tk.CENTER, image=self.image, tags="node" - ) - image_box = self.canvas.bbox(self.id) - y = image_box[3] + NODE_TEXT_OFFSET - text_font = font.Font(family="TkIconFont", size=12) - self.text_id = self.canvas.create_text( - x, - y, - text=self.core_node.name, - tags="nodename", - font=text_font, - fill="#0000CD", - ) - self.antenna_draw = WlanAntennaManager(self.canvas, self.id) - self.tooltip = CanvasTooltip(self.canvas) - self.canvas.tag_bind(self.id, "", self.click_press) - self.canvas.tag_bind(self.id, "", self.click_release) - self.canvas.tag_bind(self.id, "", self.motion) - self.canvas.tag_bind(self.id, "", self.double_click) - self.canvas.tag_bind(self.id, "", self.select_multiple) - self.canvas.tag_bind(self.id, "", self.on_enter) - self.canvas.tag_bind(self.id, "", self.on_leave) - self.edges = set() - self.interfaces = [] - self.wireless_edges = set() - self.moving = None - - def redraw(self): - self.canvas.itemconfig(self.id, image=self.image) - self.canvas.itemconfig(self.text_id, text=self.core_node.name) - - def move(self, x, y, update=True): - old_x = self.core_node.position.x - old_y = self.core_node.position.y - x_offset = x - old_x - y_offset = y - old_y - self.core_node.position.x = int(x) - self.core_node.position.y = int(y) - self.canvas.move(self.id, x_offset, y_offset) - self.canvas.move(self.text_id, x_offset, y_offset) - self.antenna_draw.update_antennas_position(x_offset, y_offset) - self.canvas.canvas_management.node_drag(self, x_offset, y_offset) - for edge in self.edges: - x1, y1, x2, y2 = self.canvas.coords(edge.id) - if edge.src == self.id: - self.canvas.coords(edge.id, x, y, x2, y2) - else: - self.canvas.coords(edge.id, x1, y1, x, y) - edge.link_info.recalculate_info() - for edge in self.wireless_edges: - x1, y1, x2, y2 = self.canvas.coords(edge.id) - if edge.src == self.id: - self.canvas.coords(edge.id, x, y, x2, y2) - else: - self.canvas.coords(edge.id, x1, y1, x, y) - if self.app.core.is_runtime() and update: - self.app.core.edit_node(self.core_node.id, int(x), int(y)) - - def on_enter(self, event): - if self.app.core.is_runtime() and self.app.core.observer: - self.tooltip.text.set("waiting...") - self.tooltip.on_enter(event) - output = self.app.core.run(self.core_node.id) - self.tooltip.text.set(output) - - def on_leave(self, event): - self.tooltip.on_leave(event) - - def click(self, event): - print("click") - - def double_click(self, event): - if self.app.core.is_runtime(): - self.canvas.core.launch_terminal(self.core_node.id) - else: - self.show_config() - - def update_coords(self): - x, y = self.canvas.coords(self.id) - self.core_node.position.x = int(x) - self.core_node.position.y = int(y) - - def click_press(self, event): - logging.debug(f"node click press {self.core_node.name}: {event}") - self.moving = self.canvas.canvas_xy(event) - if self.id not in self.canvas.canvas_management.selected: - self.canvas.canvas_management.node_select(self) - self.canvas.selected = self.id - - def click_release(self, event): - logging.debug(f"node click release {self.core_node.name}: {event}") - self.update_coords() - self.moving = None - - def motion(self, event): - if self.canvas.mode == GraphMode.EDGE: - return - x, y = self.canvas.canvas_xy(event) - my_x = self.core_node.position.x - my_y = self.core_node.position.y - self.move(x, y) - # move other selected components - for nid, bboxid in self.canvas.canvas_management.selected.items(): - if nid != self.id and nid in self.canvas.nodes: - other_old_x = self.canvas.nodes[nid].core_node.position.x - other_old_y = self.canvas.nodes[nid].core_node.position.y - other_new_x = x + other_old_x - my_x - other_new_y = y + other_old_y - my_y - self.canvas.nodes[nid].move(other_new_x, other_new_y) - if nid != self.id and nid in self.canvas.shapes: - self.canvas.shapes[nid].motion(None, x - my_x, y - my_y) - - def select_multiple(self, event): - self.canvas.canvas_management.node_select(self, True) - - def show_config(self): - self.canvas.context = None - dialog = NodeConfigDialog(self.app, self.app, self) - dialog.show() - - def show_wlan_config(self): - self.canvas.context = None - dialog = WlanConfigDialog(self.app, self.app, self) - dialog.show() - - def show_mobility_config(self): - self.canvas.context = None - dialog = MobilityConfigDialog(self.app, self.app, self) - dialog.show() - - def show_mobility_player(self): - self.canvas.context = None - mobility_player = self.app.core.mobility_players[self.core_node.id] - mobility_player.show() - - def show_emane_config(self): - self.canvas.context = None - dialog = EmaneConfigDialog(self.app, self.app, self) - dialog.show() diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph/graph_helper.py similarity index 100% rename from coretk/coretk/graph_helper.py rename to coretk/coretk/graph/graph_helper.py diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/graph/linkinfo.py similarity index 100% rename from coretk/coretk/linkinfo.py rename to coretk/coretk/graph/linkinfo.py diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py new file mode 100644 index 00000000..4f80f77d --- /dev/null +++ b/coretk/coretk/graph/node.py @@ -0,0 +1,163 @@ +import logging +import tkinter as tk +from tkinter import font + +from coretk.dialogs.emaneconfig import EmaneConfigDialog +from coretk.dialogs.mobilityconfig import MobilityConfigDialog +from coretk.dialogs.nodeconfig import NodeConfigDialog +from coretk.dialogs.wlanconfig import WlanConfigDialog +from coretk.graph.canvastooltip import CanvasTooltip +from coretk.graph.enums import GraphMode +from coretk.graph.graph_helper import WlanAntennaManager + +NODE_TEXT_OFFSET = 5 + + +class CanvasNode: + def __init__(self, app, core_node, image): + self.app = app + self.canvas = app.canvas + self.image = image + self.core_node = core_node + x = self.core_node.position.x + y = self.core_node.position.y + self.id = self.canvas.create_image( + x, y, anchor=tk.CENTER, image=self.image, tags="node" + ) + image_box = self.canvas.bbox(self.id) + y = image_box[3] + NODE_TEXT_OFFSET + text_font = font.Font(family="TkIconFont", size=12) + self.text_id = self.canvas.create_text( + x, + y, + text=self.core_node.name, + tags="nodename", + font=text_font, + fill="#0000CD", + ) + self.antenna_draw = WlanAntennaManager(self.canvas, self.id) + self.tooltip = CanvasTooltip(self.canvas) + self.canvas.tag_bind(self.id, "", self.click_press) + self.canvas.tag_bind(self.id, "", self.click_release) + self.canvas.tag_bind(self.id, "", self.motion) + self.canvas.tag_bind(self.id, "", self.double_click) + self.canvas.tag_bind(self.id, "", self.select_multiple) + self.canvas.tag_bind(self.id, "", self.on_enter) + self.canvas.tag_bind(self.id, "", self.on_leave) + self.edges = set() + self.interfaces = [] + self.wireless_edges = set() + self.moving = None + + def redraw(self): + self.canvas.itemconfig(self.id, image=self.image) + self.canvas.itemconfig(self.text_id, text=self.core_node.name) + + def move(self, x, y, update=True): + old_x = self.core_node.position.x + old_y = self.core_node.position.y + x_offset = x - old_x + y_offset = y - old_y + self.core_node.position.x = int(x) + self.core_node.position.y = int(y) + self.canvas.move(self.id, x_offset, y_offset) + self.canvas.move(self.text_id, x_offset, y_offset) + self.antenna_draw.update_antennas_position(x_offset, y_offset) + self.canvas.canvas_management.node_drag(self, x_offset, y_offset) + for edge in self.edges: + x1, y1, x2, y2 = self.canvas.coords(edge.id) + if edge.src == self.id: + self.canvas.coords(edge.id, x, y, x2, y2) + else: + self.canvas.coords(edge.id, x1, y1, x, y) + edge.link_info.recalculate_info() + for edge in self.wireless_edges: + x1, y1, x2, y2 = self.canvas.coords(edge.id) + if edge.src == self.id: + self.canvas.coords(edge.id, x, y, x2, y2) + else: + self.canvas.coords(edge.id, x1, y1, x, y) + if self.app.core.is_runtime() and update: + self.app.core.edit_node(self.core_node.id, int(x), int(y)) + + def on_enter(self, event): + if self.app.core.is_runtime() and self.app.core.observer: + self.tooltip.text.set("waiting...") + self.tooltip.on_enter(event) + output = self.app.core.run(self.core_node.id) + self.tooltip.text.set(output) + + def on_leave(self, event): + self.tooltip.on_leave(event) + + def click(self, event): + print("click") + + def double_click(self, event): + if self.app.core.is_runtime(): + self.canvas.core.launch_terminal(self.core_node.id) + else: + self.show_config() + + def update_coords(self): + x, y = self.canvas.coords(self.id) + self.core_node.position.x = int(x) + self.core_node.position.y = int(y) + + def click_press(self, event): + logging.debug(f"node click press {self.core_node.name}: {event}") + self.moving = self.canvas.canvas_xy(event) + if self.id not in self.canvas.canvas_management.selected: + self.canvas.canvas_management.node_select(self) + self.canvas.selected = self.id + + def click_release(self, event): + logging.debug(f"node click release {self.core_node.name}: {event}") + self.update_coords() + self.moving = None + + def motion(self, event): + if self.canvas.mode == GraphMode.EDGE: + return + x, y = self.canvas.canvas_xy(event) + my_x = self.core_node.position.x + my_y = self.core_node.position.y + self.move(x, y) + # move other selected components + for nid, bboxid in self.canvas.canvas_management.selected.items(): + if nid != self.id and nid in self.canvas.nodes: + other_old_x = self.canvas.nodes[nid].core_node.position.x + other_old_y = self.canvas.nodes[nid].core_node.position.y + other_new_x = x + other_old_x - my_x + other_new_y = y + other_old_y - my_y + self.canvas.nodes[nid].move(other_new_x, other_new_y) + if nid != self.id and nid in self.canvas.shapes: + self.canvas.shapes[nid].motion(None, x - my_x, y - my_y) + + def select_multiple(self, event): + self.canvas.canvas_management.node_select(self, True) + + def show_config(self): + self.canvas.context = None + dialog = NodeConfigDialog(self.app, self.app, self) + dialog.show() + + def show_wlan_config(self): + self.canvas.context = None + dialog = WlanConfigDialog(self.app, self.app, self) + dialog.show() + + def show_mobility_config(self): + self.canvas.context = None + dialog = MobilityConfigDialog(self.app, self.app, self) + dialog.show() + + def show_mobility_player(self): + self.canvas.context = None + mobility_player = self.app.core.mobility_players[self.core_node.id] + mobility_player.show() + + def show_emane_config(self): + self.canvas.context = None + dialog = EmaneConfigDialog(self.app, self.app, self) + dialog.show() diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/graph/nodedelete.py similarity index 100% rename from coretk/coretk/nodedelete.py rename to coretk/coretk/graph/nodedelete.py diff --git a/coretk/coretk/shape.py b/coretk/coretk/graph/shape.py similarity index 100% rename from coretk/coretk/shape.py rename to coretk/coretk/graph/shape.py diff --git a/coretk/coretk/status.py b/coretk/coretk/statusbar.py similarity index 100% rename from coretk/coretk/status.py rename to coretk/coretk/statusbar.py diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index fb0560dc..5f528628 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -5,7 +5,7 @@ from functools import partial from tkinter import ttk from coretk.dialogs.customnodes import CustomNodesDialog -from coretk.graph import GraphMode +from coretk.graph.enums import GraphMode from coretk.images import ImageEnum, Images from coretk.nodeutils import NodeUtils from coretk.tooltip import Tooltip From c82453b9814a08bfc6134571a7631614e7a95d40 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 5 Dec 2019 11:15:51 -0800 Subject: [PATCH 318/462] load shapes --- coretk/coretk/coreclient.py | 103 +++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 42 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 1a7d06e3..273e3037 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -7,12 +7,12 @@ import os import time from core.api.grpc import client, core_pb2 -from coretk import appconfig, parsedata +from coretk import appconfig from coretk.dialogs.mobilityplayer import MobilityPlayer from coretk.dialogs.sessions import SessionsDialog from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils -from coretk.shape import Shape, ShapeData +from coretk.shape import ShapeData OBSERVERS = { "processes": "ps", @@ -282,47 +282,48 @@ class CoreClient: return self.state == core_pb2.SessionState.RUNTIME def parse_metadata(self, config): - for key, value in config.items(): - if "global_options" != key: - canvas_config = parsedata.parse(value) - print(canvas_config) - if canvas_config.get("type"): - config_type = canvas_config["type"] - if config_type == "rectangle" or config_type == "oval": - data = ShapeData( - False, - canvas_config["label"], - canvas_config["fontfamily"], - canvas_config["fontsize"], - canvas_config["labelcolor"], - canvas_config["color"], - canvas_config["border"], - canvas_config["width"], - ) - coords = tuple( - [float(x) for x in canvas_config["iconcoords"].split()] - ) - shape = Shape( - self.app, - self.app.canvas, - None, - None, - coords, - data, - config_type, - ) - self.app.canvas.shapes[shape.id] = shape - elif canvas_config["type"] == "text": - print("not implemented") - else: - if "wallpaper" in canvas_config: - logging.info("canvas metadata: %s", canvas_config) - wallpaper_style = canvas_config["wallpaper-style"] - self.app.canvas.scale_option.set(wallpaper_style) - wallpaper = canvas_config["wallpaper"] - wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) - self.app.canvas.set_wallpaper(wallpaper) + # for key, value in config.items(): + # if "global_options" != key: + # canvas_config = parsedata.parse(value) + # print(canvas_config) + # if canvas_config.get("type"): + # config_type = canvas_config["type"] + # if config_type == "rectangle" or config_type == "oval": + # data = ShapeData( + # False, + # canvas_config["label"], + # canvas_config["fontfamily"], + # canvas_config["fontsize"], + # canvas_config["labelcolor"], + # canvas_config["color"], + # canvas_config["border"], + # canvas_config["width"], + # ) + # coords = tuple( + # [float(x) for x in canvas_config["iconcoords"].split()] + # ) + # shape = Shape( + # self.app, + # self.app.canvas, + # None, + # None, + # coords, + # data, + # config_type, + # ) + # self.app.canvas.shapes[shape.id] = shape + # elif canvas_config["type"] == "text": + # print("not implemented") + # else: + # if "wallpaper" in canvas_config: + # logging.info("canvas metadata: %s", canvas_config) + # wallpaper_style = canvas_config["wallpaper-style"] + # self.app.canvas.scale_option.set(wallpaper_style) + # wallpaper = canvas_config["wallpaper"] + # wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) + # self.app.canvas.set_wallpaper(wallpaper) # canvas settings + print(config) canvas_config = config.get("canvas") if canvas_config: logging.info("canvas metadata: %s", canvas_config) @@ -332,6 +333,24 @@ class CoreClient: wallpaper = canvas_config["wallpaper"] wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) self.app.canvas.set_wallpaper(wallpaper) + for key, annotation_config in config.items(): + if "annotation" in key: + annotation_config = json.loads(annotation_config) + print(annotation_config) + config_type = annotation_config["type"] + if config_type in ["rectangle", "oval"]: + coords = tuple(annotation_config["iconcoords"]) + data = ShapeData( + False, + annotation_config["label"], + annotation_config["fontfamily"], + annotation_config["fontsize"], + annotation_config["labelcolor"], + annotation_config["color"], + annotation_config["border"], + annotation_config["width"], + ) + print(data, coords) def create_new_session(self): """ From 7c8f9dac0f8e3c44611fd396857d897b4d09f2c8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 5 Dec 2019 13:13:35 -0800 Subject: [PATCH 319/462] removed nodedelete module and added logic to the graph canvas itself --- coretk/coretk/graph/graph.py | 140 ++++++++++++++---- coretk/coretk/graph/node.py | 27 ++-- coretk/coretk/graph/nodedelete.py | 98 ------------ coretk/coretk/graph/shape.py | 3 +- .../graph/{canvastooltip.py => tooltip.py} | 0 5 files changed, 129 insertions(+), 139 deletions(-) delete mode 100644 coretk/coretk/graph/nodedelete.py rename coretk/coretk/graph/{canvastooltip.py => tooltip.py} (100%) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index d39ca012..8e18c157 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -11,7 +11,6 @@ from coretk.graph.enums import GraphMode, ScaleOption from coretk.graph.graph_helper import GraphHelper from coretk.graph.linkinfo import LinkInfo, Throughput from coretk.graph.node import CanvasNode -from coretk.graph.nodedelete import CanvasComponentManagement from coretk.graph.shape import Shape from coretk.images import ImageEnum, Images from coretk.nodeutils import NodeUtils @@ -28,6 +27,7 @@ class CanvasGraph(tk.Canvas): self.app = master self.mode = GraphMode.SELECT self.annotation_type = None + self.selection = {} self.selected = None self.node_draw = None self.context = None @@ -37,7 +37,6 @@ class CanvasGraph(tk.Canvas): self.wireless_edges = {} self.drawing_edge = None self.grid = None - self.canvas_management = CanvasComponentManagement(self, core) self.setup_bindings() self.draw_grid(width, height) self.core = core @@ -328,6 +327,90 @@ class CanvasGraph(tk.Canvas): edge.link_info = LinkInfo(self, edge, link) logging.debug(f"edges: {self.find_withtag('edge')}") + def select_object(self, object_id, choose_multiple=False): + """ + create a bounding box when a node is selected + """ + if not choose_multiple: + self.clear_selection() + + # draw a bounding box if node hasn't been selected yet + if object_id not in self.selection: + x0, y0, x1, y1 = self.bbox(object_id) + selection_id = self.create_rectangle( + (x0 - 6, y0 - 6, x1 + 6, y1 + 6), + activedash=True, + dash="-", + tags="selectednodes", + ) + self.selection[object_id] = selection_id + else: + selection_id = self.selection.pop(object_id) + self.delete(selection_id) + + def clear_selection(self): + """ + Clear current selection boxes. + + :return: nothing + """ + for _id in self.selection.values(): + self.delete(_id) + self.selection.clear() + + def object_drag(self, object_id, offset_x, offset_y): + select_id = self.selection.get(object_id) + if select_id is not None: + self.move(select_id, offset_x, offset_y) + + def delete_selection_objects(self): + edges = set() + nodes = [] + for object_id in self.selection: + if object_id in self.nodes: + selection_id = self.selection[object_id] + canvas_node = self.nodes.pop(object_id) + nodes.append(canvas_node) + self.delete(object_id) + self.delete(selection_id) + self.delete(canvas_node.text_id) + + # delete antennas + is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type) + if is_wireless: + canvas_node.antenna_draw.delete_antennas() + + # delete related edges + for edge in canvas_node.edges: + if edge in edges: + continue + edges.add(edge) + self.edges.pop(edge.token) + self.delete(edge.id) + self.delete(edge.link_info.id1) + self.delete(edge.link_info.id2) + other_id = edge.src + other_interface = edge.src_interface + if edge.src == object_id: + other_id = edge.dst + other_interface = edge.dst_interface + other_node = self.nodes[other_id] + other_node.edges.remove(edge) + try: + other_node.interfaces.remove(other_interface) + except ValueError: + pass + if is_wireless: + other_node.antenna_draw.delete_antenna() + if object_id in self.shapes: + selection_id = self.selection[object_id] + self.shapes[object_id].delete() + self.delete(selection_id) + self.shapes.pop(object_id) + + self.selection.clear() + return nodes + def click_press(self, event): """ Start drawing an edge if mouse click is on a node @@ -337,7 +420,7 @@ class CanvasGraph(tk.Canvas): """ logging.debug(f"click press: {event}") selected = self.get_selected(event) - is_node = selected in self.find_withtag("node") + is_node = selected in self.nodes if self.mode == GraphMode.EDGE and is_node: x, y = self.coords(selected) self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) @@ -353,27 +436,26 @@ class CanvasGraph(tk.Canvas): self.shape_drawing = True if self.mode == GraphMode.SELECT: if selected is not None: - if "shape" in self.gettags(selected): + if selected in self.shapes: x, y = self.canvas_xy(event) - self.shapes[selected].cursor_x = x - self.shapes[selected].cursor_y = y - if selected not in self.canvas_management.selected: - self.canvas_management.node_select(self.shapes[selected]) + shape = self.shapes[selected] + shape.cursor_x = x + shape.cursor_y = y + if selected not in self.selection: + self.select_object(shape.id) self.selected = selected else: - for i in self.find_withtag("selectednodes"): - self.delete(i) - self.canvas_management.selected.clear() + self.clear_selection() def ctrl_click(self, event): - logging.debug("Control left click %s", event) + logging.debug("control left click: %s", event) selected = self.get_selected(event) if ( self.mode == GraphMode.SELECT and selected is not None - and "shape" in self.gettags(selected) + and selected in self.shapes ): - self.canvas_management.node_select(self.shapes[selected], True) + self.select_object(selected, choose_multiple=True) def click_motion(self, event): """ @@ -396,21 +478,24 @@ class CanvasGraph(tk.Canvas): if ( self.mode == GraphMode.SELECT and self.selected is not None - and "shape" in self.gettags(self.selected) + and self.selected in self.shapes ): x, y = self.canvas_xy(event) shape = self.shapes[self.selected] delta_x = x - shape.cursor_x delta_y = y - shape.cursor_y shape.motion(event) + # move other selected components - for nid in self.canvas_management.selected: - if nid != self.selected and nid in self.shapes: - self.shapes[nid].motion(None, delta_x, delta_y) - if nid != self.selected and nid in self.nodes: - node_x = self.nodes[nid].core_node.position.x - node_y = self.nodes[nid].core_node.position.y - self.nodes[nid].move(node_x + delta_x, node_y + delta_y) + for _id in self.selection: + if _id != self.selected and _id in self.shapes: + shape = self.shapes[_id] + shape.motion(None, delta_x, delta_y) + if _id != self.selected and _id in self.nodes: + node = self.nodes[_id] + node_x = node.core_node.position.x + node_y = node.core_node.position.y + node.move(node_x + delta_x, node_y + delta_y) def click_context(self, event): logging.info("context event: %s", self.context) @@ -432,17 +517,18 @@ class CanvasGraph(tk.Canvas): :return: """ logging.debug("press delete key") - nodes = self.canvas_management.delete_selected_nodes() + nodes = self.delete_selection_objects() self.core.delete_graph_nodes(nodes) def double_click(self, event): selected = self.get_selected(event) - if selected is not None and "shape" in self.gettags(selected): - s = ShapeDialog(self.app, self.app, self.shapes[selected]) - s.show() + if selected is not None and selected in self.shapes: + shape = self.shapes[selected] + dialog = ShapeDialog(self.app, self.app, shape) + dialog.show() def add_node(self, x, y): - if self.selected is None or "shape" in self.gettags(self.selected): + if self.selected is None or self.selected in self.shapes: core_node = self.core.create_node( int(x), int(y), self.node_draw.node_type, self.node_draw.model ) diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 4f80f77d..a4e4a539 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -6,9 +6,9 @@ from coretk.dialogs.emaneconfig import EmaneConfigDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog -from coretk.graph.canvastooltip import CanvasTooltip from coretk.graph.enums import GraphMode from coretk.graph.graph_helper import WlanAntennaManager +from coretk.graph.tooltip import CanvasTooltip NODE_TEXT_OFFSET = 5 @@ -63,7 +63,7 @@ class CanvasNode: self.canvas.move(self.id, x_offset, y_offset) self.canvas.move(self.text_id, x_offset, y_offset) self.antenna_draw.update_antennas_position(x_offset, y_offset) - self.canvas.canvas_management.node_drag(self, x_offset, y_offset) + self.canvas.object_drag(self.id, x_offset, y_offset) for edge in self.edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) if edge.src == self.id: @@ -107,8 +107,8 @@ class CanvasNode: def click_press(self, event): logging.debug(f"node click press {self.core_node.name}: {event}") self.moving = self.canvas.canvas_xy(event) - if self.id not in self.canvas.canvas_management.selected: - self.canvas.canvas_management.node_select(self) + if self.id not in self.canvas.selection: + self.canvas.select_object(self.id) self.canvas.selected = self.id def click_release(self, event): @@ -123,19 +123,22 @@ class CanvasNode: my_x = self.core_node.position.x my_y = self.core_node.position.y self.move(x, y) + # move other selected components - for nid, bboxid in self.canvas.canvas_management.selected.items(): - if nid != self.id and nid in self.canvas.nodes: - other_old_x = self.canvas.nodes[nid].core_node.position.x - other_old_y = self.canvas.nodes[nid].core_node.position.y + for object_id, selection_id in self.canvas.selection.items(): + if object_id != self.id and object_id in self.canvas.nodes: + canvas_node = self.canvas.nodes[object_id] + other_old_x = canvas_node.core_node.position.x + other_old_y = canvas_node.core_node.position.y other_new_x = x + other_old_x - my_x other_new_y = y + other_old_y - my_y - self.canvas.nodes[nid].move(other_new_x, other_new_y) - if nid != self.id and nid in self.canvas.shapes: - self.canvas.shapes[nid].motion(None, x - my_x, y - my_y) + self.canvas.nodes[object_id].move(other_new_x, other_new_y) + elif object_id in self.canvas.shapes: + shape = self.canvas.shapes[object_id] + shape.motion(None, x - my_x, y - my_y) def select_multiple(self, event): - self.canvas.canvas_management.node_select(self, True) + self.canvas.select_object(self.id, choose_multiple=True) def show_config(self): self.canvas.context = None diff --git a/coretk/coretk/graph/nodedelete.py b/coretk/coretk/graph/nodedelete.py deleted file mode 100644 index 1365dc0f..00000000 --- a/coretk/coretk/graph/nodedelete.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -manage deletion -""" -from coretk.nodeutils import NodeUtils - - -class CanvasComponentManagement: - def __init__(self, canvas, core): - self.app = core - self.canvas = canvas - - # dictionary that maps node to box - self.selected = {} - - def node_select(self, canvas_node, choose_multiple=False): - """ - create a bounding box when a node is selected - - :param coretk.graph.CanvasNode canvas_node: canvas node object - :return: nothing - """ - - if not choose_multiple: - self.delete_current_bbox() - - # draw a bounding box if node hasn't been selected yet - if canvas_node.id not in self.selected: - x0, y0, x1, y1 = self.canvas.bbox(canvas_node.id) - bbox_id = self.canvas.create_rectangle( - (x0 - 6, y0 - 6, x1 + 6, y1 + 6), - activedash=True, - dash="-", - tags="selectednodes", - ) - self.selected[canvas_node.id] = bbox_id - else: - bbox_id = self.selected.pop(canvas_node.id) - self.canvas.delete(bbox_id) - - def node_drag(self, canvas_node, offset_x, offset_y): - select_id = self.selected.get(canvas_node.id) - if select_id is not None: - self.canvas.move(select_id, offset_x, offset_y) - - def delete_current_bbox(self): - for bbid in self.selected.values(): - self.canvas.delete(bbid) - self.selected.clear() - - def delete_selected_nodes(self): - edges = set() - nodes = [] - for node_id in self.selected: - if "node" in self.canvas.gettags(node_id): - bbox_id = self.selected[node_id] - canvas_node = self.canvas.nodes.pop(node_id) - nodes.append(canvas_node) - self.canvas.delete(node_id) - self.canvas.delete(bbox_id) - self.canvas.delete(canvas_node.text_id) - - # delete antennas - is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type) - if is_wireless: - canvas_node.antenna_draw.delete_antennas() - - # delete related edges - for edge in canvas_node.edges: - if edge in edges: - continue - edges.add(edge) - self.canvas.edges.pop(edge.token) - self.canvas.delete(edge.id) - self.canvas.delete(edge.link_info.id1) - self.canvas.delete(edge.link_info.id2) - other_id = edge.src - other_interface = edge.src_interface - if edge.src == node_id: - other_id = edge.dst - other_interface = edge.dst_interface - other_node = self.canvas.nodes[other_id] - other_node.edges.remove(edge) - try: - other_node.interfaces.remove(other_interface) - except ValueError: - pass - if is_wireless: - other_node.antenna_draw.delete_antenna() - - for shape_id in self.selected: - if "shape" in self.canvas.gettags(shape_id): - bbox_id = self.selected[node_id] - self.canvas.shapes[shape_id].delete() - self.canvas.delete(bbox_id) - self.canvas.shapes.pop(shape_id) - - self.selected.clear() - return nodes diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 63025b34..4940374e 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -127,14 +127,13 @@ class Shape: logging.debug("Click release on shape %s", self.id) def motion(self, event, delta_x=None, delta_y=None): - logging.debug("motion on shape %s", self.id) if event is not None: delta_x = event.x - self.cursor_x delta_y = event.y - self.cursor_y self.cursor_x = event.x self.cursor_y = event.y self.canvas.move(self.id, delta_x, delta_y) - self.canvas.canvas_management.node_drag(self, delta_x, delta_y) + self.canvas.object_drag(self.id, delta_x, delta_y) if self.text_id is not None: self.canvas.move(self.text_id, delta_x, delta_y) diff --git a/coretk/coretk/graph/canvastooltip.py b/coretk/coretk/graph/tooltip.py similarity index 100% rename from coretk/coretk/graph/canvastooltip.py rename to coretk/coretk/graph/tooltip.py From 6425b08878f28a62fa43eace2b259e077d4b1932 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 5 Dec 2019 13:17:12 -0800 Subject: [PATCH 320/462] updated graph canvas width and height to use the id it already knows of --- coretk/coretk/graph/graph.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 8e18c157..95ad19dc 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -543,8 +543,7 @@ class CanvasGraph(tk.Canvas): :return: nothing """ - grid = self.find_withtag("rectangle")[0] - x0, y0, x1, y1 = self.coords(grid) + x0, y0, x1, y1 = self.coords(self.grid) canvas_w = abs(x0 - x1) canvas_h = abs(y0 - y1) return canvas_w, canvas_h From 7e2ebb4a2c845e6d61d0f9bc1c599391659535b6 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 5 Dec 2019 13:43:12 -0800 Subject: [PATCH 321/462] delete some extra print statement --- coretk/coretk/coreclient.py | 3 --- coretk/coretk/graph/shape.py | 2 -- 2 files changed, 5 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 7a760c1f..05a36c82 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -309,7 +309,6 @@ class CoreClient: for key, annotation_config in config.items(): if "annotation" in key: annotation_config = json.loads(annotation_config) - print(annotation_config) config_type = annotation_config["type"] if config_type in ["rectangle", "oval"]: coords = tuple(annotation_config["iconcoords"]) @@ -508,7 +507,6 @@ class CoreClient: """ node_protos = [x.core_node for x in self.canvas_nodes.values()] link_protos = list(self.links.values()) - print(node_protos) if self.get_session_state() != core_pb2.SessionState.DEFINITION: self.client.set_session_state( self.session_id, core_pb2.SessionState.DEFINITION @@ -535,7 +533,6 @@ class CoreClient: self.created_links.add( tuple([link_proto.node_one_id, link_proto.node_two_id]) ) - print(self.app.core.client.get_session(self.app.core.session_id)) def close(self): """ diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index dd83ee3e..91020ecb 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -106,8 +106,6 @@ class Shape: ) _x = (x0 + x1) / 2 _y = y0 + 1.5 * data.font_size - print("create text with text: ", data.text) - print(data.text_color) self.text_id = self.canvas.create_text( _x, _y, tags="shapetext", text=data.text, fill=data.text_color ) From 81eeac9ec62a6c755be066a24dabf2427416943f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 5 Dec 2019 16:37:48 -0800 Subject: [PATCH 322/462] merged drawing antenna code to graph node class --- coretk/coretk/graph/edges.py | 57 +++++++---- coretk/coretk/graph/graph.py | 61 ++++++------ coretk/coretk/graph/graph_helper.py | 144 ---------------------------- coretk/coretk/graph/node.py | 49 +++++++++- coretk/coretk/nodeutils.py | 3 + 5 files changed, 120 insertions(+), 194 deletions(-) delete mode 100644 coretk/coretk/graph/graph_helper.py diff --git a/coretk/coretk/graph/edges.py b/coretk/coretk/graph/edges.py index 92f99400..d62a309b 100644 --- a/coretk/coretk/graph/edges.py +++ b/coretk/coretk/graph/edges.py @@ -1,5 +1,7 @@ import tkinter as tk +from coretk.nodeutils import NodeUtils + class CanvasWirelessEdge: def __init__(self, token, position, src, dst, canvas): @@ -22,7 +24,7 @@ class CanvasEdge: width = 1.4 - def __init__(self, x1, y1, x2, y2, src, canvas, is_wired=None): + def __init__(self, x1, y1, x2, y2, src, canvas): """ Create an instance of canvas edge object :param int x1: source x-coord @@ -30,41 +32,56 @@ class CanvasEdge: :param int x2: destination x-coord :param int y2: destination y-coord :param int src: source id - :param tkinter.Canvas canvas: canvas object + :param coretk.graph.graph.GraphCanvas canvas: canvas object """ self.src = src self.dst = None self.src_interface = None self.dst_interface = None self.canvas = canvas - if is_wired is None or is_wired is True: - self.id = self.canvas.create_line( - x1, y1, x2, y2, tags="edge", width=self.width, fill="#ff0000" - ) - else: - self.id = self.canvas.create_line( - x1, - y1, - x2, - y2, - tags="edge", - width=self.width, - fill="#ff0000", - state=tk.HIDDEN, - ) + self.id = self.canvas.create_line( + x1, y1, x2, y2, tags="edge", width=self.width, fill="#ff0000" + ) self.token = None self.link_info = None self.throughput = None - self.wired = is_wired - def complete(self, dst, x, y): + def complete(self, dst): self.dst = dst self.token = tuple(sorted((self.src, self.dst))) + x, y = self.canvas.coords(self.dst) x1, y1, _, _ = self.canvas.coords(self.id) self.canvas.coords(self.id, x1, y1, x, y) - self.canvas.helper.draw_wireless_case(self.src, self.dst, self) + self.check_wireless() self.canvas.tag_raise(self.src) self.canvas.tag_raise(self.dst) + def check_wireless(self): + src_node = self.canvas.nodes[self.src] + dst_node = self.canvas.nodes[self.dst] + src_node_type = src_node.core_node.type + dst_node_type = dst_node.core_node.type + is_src_wireless = NodeUtils.is_wireless_node(src_node_type) + is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type) + if is_src_wireless or is_dst_wireless: + self.canvas.itemconfig(self.id, state=tk.HIDDEN) + self._check_antenna() + + def _check_antenna(self): + src_node = self.canvas.nodes[self.src] + dst_node = self.canvas.nodes[self.dst] + src_node_type = src_node.core_node.type + dst_node_type = dst_node.core_node.type + is_src_wireless = NodeUtils.is_wireless_node(src_node_type) + is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type) + if is_src_wireless or is_dst_wireless: + if is_src_wireless and not is_dst_wireless: + dst_node.add_antenna() + elif not is_src_wireless and is_dst_wireless: + src_node.add_antenna() + # TODO: remove this? dont allow linking wireless nodes? + else: + src_node.add_antenna() + def delete(self): self.canvas.delete(self.id) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 95ad19dc..ad6788c5 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -8,7 +8,6 @@ from core.api.grpc.core_pb2 import NodeType from coretk.dialogs.shapemod import ShapeDialog from coretk.graph.edges import CanvasEdge, CanvasWirelessEdge from coretk.graph.enums import GraphMode, ScaleOption -from coretk.graph.graph_helper import GraphHelper from coretk.graph.linkinfo import LinkInfo, Throughput from coretk.graph.node import CanvasNode from coretk.graph.shape import Shape @@ -16,6 +15,18 @@ from coretk.images import ImageEnum, Images from coretk.nodeutils import NodeUtils ABOVE_WALLPAPER = ["edge", "linkinfo", "wireless", "antenna", "nodename", "node"] +CANVAS_COMPONENT_TAGS = [ + "edge", + "node", + "nodename", + "wallpaper", + "linkinfo", + "antenna", + "wireless", + "selectednodes", + "shape", + "shapetext", +] class CanvasGraph(tk.Canvas): @@ -40,7 +51,6 @@ class CanvasGraph(tk.Canvas): self.setup_bindings() self.draw_grid(width, height) self.core = core - self.helper = GraphHelper(self, core) self.throughput_draw = Throughput(self, core) self.shape_drawing = False @@ -96,7 +106,8 @@ class CanvasGraph(tk.Canvas): :return: nothing """ # delete any existing drawn items - self.helper.delete_canvas_components() + for tag in CANVAS_COMPONENT_TAGS: + self.delete(tag) # set the private variables to default value self.mode = GraphMode.SELECT @@ -195,9 +206,6 @@ class CanvasGraph(tk.Canvas): if link.type == core_pb2.LinkType.WIRELESS: self.add_wireless_edge(canvas_node_one, canvas_node_two) else: - is_node_one_wireless = NodeUtils.is_wireless_node(node_one.type) - is_node_two_wireless = NodeUtils.is_wireless_node(node_two.type) - has_no_wireless = not (is_node_one_wireless or is_node_two_wireless) edge = CanvasEdge( node_one.position.x, node_one.position.y, @@ -205,15 +213,14 @@ class CanvasGraph(tk.Canvas): node_two.position.y, canvas_node_one.id, self, - is_wired=has_no_wireless, ) edge.token = tuple(sorted((canvas_node_one.id, canvas_node_two.id))) edge.dst = canvas_node_two.id + edge.check_wireless() canvas_node_one.edges.add(edge) canvas_node_two.edges.add(edge) self.edges[edge.token] = edge self.core.links[edge.token] = link - self.helper.redraw_antenna(canvas_node_one, canvas_node_two) edge.link_info = LinkInfo(self, edge, link) if link.HasField("interface_one"): canvas_node_one.interfaces.append(link.interface_one) @@ -311,21 +318,23 @@ class CanvasGraph(tk.Canvas): edge.delete() return - # set dst node and snap edge to center - x, y = self.coords(self.selected) - edge.complete(self.selected, x, y) - logging.debug(f"drawing edge token: {edge.token}") - if edge.token in self.edges: + # ignore repeated edges + token = tuple(sorted((edge.src, self.selected))) + if token in self.edges: edge.delete() - else: - self.edges[edge.token] = edge - node_src = self.nodes[edge.src] - node_src.edges.add(edge) - node_dst = self.nodes[edge.dst] - node_dst.edges.add(edge) - link = self.core.create_link(edge, node_src, node_dst) - edge.link_info = LinkInfo(self, edge, link) - logging.debug(f"edges: {self.find_withtag('edge')}") + return + + # set dst node and snap edge to center + edge.complete(self.selected) + logging.debug("drawing edge token: %s", edge.token) + + self.edges[edge.token] = edge + node_src = self.nodes[edge.src] + node_src.edges.add(edge) + node_dst = self.nodes[edge.dst] + node_dst.edges.add(edge) + link = self.core.create_link(edge, node_src, node_dst) + edge.link_info = LinkInfo(self, edge, link) def select_object(self, object_id, choose_multiple=False): """ @@ -374,11 +383,8 @@ class CanvasGraph(tk.Canvas): self.delete(object_id) self.delete(selection_id) self.delete(canvas_node.text_id) - - # delete antennas + canvas_node.delete_antennae() is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type) - if is_wireless: - canvas_node.antenna_draw.delete_antennas() # delete related edges for edge in canvas_node.edges: @@ -401,7 +407,7 @@ class CanvasGraph(tk.Canvas): except ValueError: pass if is_wireless: - other_node.antenna_draw.delete_antenna() + other_node.delete_antenna() if object_id in self.shapes: selection_id = self.selection[object_id] self.shapes[object_id].delete() @@ -424,6 +430,7 @@ class CanvasGraph(tk.Canvas): if self.mode == GraphMode.EDGE and is_node: x, y = self.coords(selected) self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) + self.tag_raise(selected) if ( self.mode == GraphMode.ANNOTATION and self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE] diff --git a/coretk/coretk/graph/graph_helper.py b/coretk/coretk/graph/graph_helper.py deleted file mode 100644 index af6d4823..00000000 --- a/coretk/coretk/graph/graph_helper.py +++ /dev/null @@ -1,144 +0,0 @@ -""" -Some graph helper functions -""" -import logging -import tkinter as tk - -from coretk.images import ImageEnum, Images -from coretk.nodeutils import NodeUtils - -CANVAS_COMPONENT_TAGS = [ - "edge", - "node", - "nodename", - "wallpaper", - "linkinfo", - "antenna", - "wireless", - "selectednodes", - "shape", - "shapetext", -] - - -class GraphHelper: - def __init__(self, canvas, core): - """ - create an instance of GraphHelper object - """ - self.canvas = canvas - self.core = core - - def delete_canvas_components(self): - """ - delete the components of the graph leaving only the blank canvas - - :return: nothing - """ - for tag in CANVAS_COMPONENT_TAGS: - for i in self.canvas.find_withtag(tag): - self.canvas.delete(i) - - def draw_wireless_case(self, src_id, dst_id, edge): - src_node_type = self.canvas.nodes[src_id].core_node.type - dst_node_type = self.canvas.nodes[dst_id].core_node.type - is_src_wireless = NodeUtils.is_wireless_node(src_node_type) - is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type) - if is_src_wireless or is_dst_wireless: - self.canvas.itemconfig(edge.id, state=tk.HIDDEN) - edge.wired = False - if edge.token not in self.canvas.edges: - if is_src_wireless and is_dst_wireless: - self.canvas.nodes[src_id].antenna_draw.add_antenna() - elif is_src_wireless: - self.canvas.nodes[dst_id].antenna_draw.add_antenna() - else: - self.canvas.nodes[src_id].antenna_draw.add_antenna() - edge.wired = True - - def redraw_antenna(self, node_one, node_two): - is_node_one_wireless = NodeUtils.is_wireless_node(node_one.core_node.type) - is_node_two_wireless = NodeUtils.is_wireless_node(node_two.core_node.type) - if is_node_one_wireless or is_node_two_wireless: - if is_node_one_wireless and not is_node_two_wireless: - node_two.antenna_draw.add_antenna() - elif not is_node_one_wireless and is_node_two_wireless: - node_one.antenna_draw.add_antenna() - else: - logging.error("bad link between two wireless nodes") - - def update_wlan_connection(self, old_x, old_y, new_x, new_y, edge_ids): - for eid in edge_ids: - x1, y1, x2, y2 = self.canvas.coords(eid) - if x1 == old_x and y1 == old_y: - self.canvas.coords(eid, new_x, new_y, x2, y2) - else: - self.canvas.coords(eid, x1, y1, new_x, new_y) - - -class WlanAntennaManager: - def __init__(self, canvas, node_id): - """ - crate an instance for AntennaManager - """ - self.canvas = canvas - self.node_id = node_id - self.quantity = 0 - self._max = 5 - self.antennas = [] - self.image = Images.get(ImageEnum.ANTENNA, 32) - - # distance between each antenna - self.offset = 0 - - def add_antenna(self): - """ - add an antenna to a node - - :return: nothing - """ - if self.quantity < 5: - x, y = self.canvas.coords(self.node_id) - aid = self.canvas.create_image( - x - 16 + self.offset, - y - 23, - anchor=tk.CENTER, - image=self.image, - tags="antenna", - ) - # self.canvas.tag_raise("antenna") - self.antennas.append(aid) - self.quantity = self.quantity + 1 - self.offset = self.offset + 8 - - def delete_antenna(self): - """ - delete one antenna - - :return: nothing - """ - if len(self.antennas) > 0: - self.canvas.delete(self.antennas.pop()) - self.quantity -= 1 - self.offset -= 8 - - def delete_antennas(self): - """ - delete all antennas - - :return: nothing - """ - for aid in self.antennas: - self.canvas.delete(aid) - self.antennas.clear() - self.quantity = 0 - self.offset = 0 - - def update_antennas_position(self, offset_x, offset_y): - """ - redraw antennas of a node according to the new node position - - :return: nothing - """ - for i in self.antennas: - self.canvas.move(i, offset_x, offset_y) diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index a4e4a539..2d34780e 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -7,8 +7,8 @@ from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog from coretk.graph.enums import GraphMode -from coretk.graph.graph_helper import WlanAntennaManager from coretk.graph.tooltip import CanvasTooltip +from coretk.nodeutils import NodeUtils NODE_TEXT_OFFSET = 5 @@ -35,7 +35,6 @@ class CanvasNode: font=text_font, fill="#0000CD", ) - self.antenna_draw = WlanAntennaManager(self.canvas, self.id) self.tooltip = CanvasTooltip(self.canvas) self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) @@ -48,6 +47,50 @@ class CanvasNode: self.interfaces = [] self.wireless_edges = set() self.moving = None + self.antennae = [] + + def add_antenna(self): + x, y = self.canvas.coords(self.id) + offset = len(self.antennae) * 8 + + antenna_id = self.canvas.create_image( + x - 16 + offset, + y - 23, + anchor=tk.CENTER, + image=NodeUtils.ANTENNA_ICON, + tags="antenna", + ) + self.antennae.append(antenna_id) + + def delete_antenna(self): + """ + delete one antenna + + :return: nothing + """ + if self.antennae: + antenna_id = self.antennae.pop() + self.canvas.delete(antenna_id) + + def delete_antennae(self): + """ + delete all antennas + + :return: nothing + """ + logging.info("deleting antennae: %s", self.antennae) + for antenna_id in self.antennae: + self.canvas.delete(antenna_id) + self.antennae.clear() + + def move_antennae(self, x_offset, y_offset): + """ + redraw antennas of a node according to the new node position + + :return: nothing + """ + for antenna_id in self.antennae: + self.canvas.move(antenna_id, x_offset, y_offset) def redraw(self): self.canvas.itemconfig(self.id, image=self.image) @@ -62,7 +105,7 @@ class CanvasNode: self.core_node.position.y = int(y) self.canvas.move(self.id, x_offset, y_offset) self.canvas.move(self.text_id, x_offset, y_offset) - self.antenna_draw.update_antennas_position(x_offset, y_offset) + self.move_antennae(x_offset, y_offset) self.canvas.object_drag(self.id, x_offset, y_offset) for edge in self.edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) diff --git a/coretk/coretk/nodeutils.py b/coretk/coretk/nodeutils.py index 6d880126..10d926f7 100644 --- a/coretk/coretk/nodeutils.py +++ b/coretk/coretk/nodeutils.py @@ -2,6 +2,7 @@ from core.api.grpc.core_pb2 import NodeType from coretk.images import ImageEnum, Images ICON_SIZE = 48 +ANTENNA_SIZE = 32 class NodeDraw: @@ -49,6 +50,7 @@ class NodeUtils: WIRELESS_NODES = {NodeType.WIRELESS_LAN, NodeType.EMANE} IGNORE_NODES = {NodeType.CONTROL_NET, NodeType.PEER_TO_PEER} NODE_MODELS = {"router", "host", "PC", "mdr", "prouter"} + ANTENNA_ICON = None @classmethod def is_ignore_node(cls, node_type): @@ -104,3 +106,4 @@ class NodeUtils: node_draw = NodeDraw.from_setup(image_enum, node_type, tooltip=tooltip) cls.NETWORK_NODES.append(node_draw) cls.NODE_ICONS[(node_type, None)] = node_draw.image + cls.ANTENNA_ICON = Images.get(ImageEnum.ANTENNA, ANTENNA_SIZE) From 6077e81bf4a1def0205b88edb0ba59917d2a0eb5 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 5 Dec 2019 17:01:48 -0800 Subject: [PATCH 323/462] moved node/edge delete logic into their own classes for helping make them managing their own data a bit easier --- coretk/coretk/graph/edges.py | 3 +++ coretk/coretk/graph/graph.py | 27 ++++++++++++++------------- coretk/coretk/graph/node.py | 6 +++++- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/coretk/coretk/graph/edges.py b/coretk/coretk/graph/edges.py index d62a309b..27d1e3ba 100644 --- a/coretk/coretk/graph/edges.py +++ b/coretk/coretk/graph/edges.py @@ -85,3 +85,6 @@ class CanvasEdge: def delete(self): self.canvas.delete(self.id) + if self.link_info: + self.canvas.delete(self.link_info.id1) + self.canvas.delete(self.link_info.id2) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index ad6788c5..abf3f952 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -376,14 +376,15 @@ class CanvasGraph(tk.Canvas): edges = set() nodes = [] for object_id in self.selection: + # delete selection box + selection_id = self.selection[object_id] + self.delete(selection_id) + + # delete node and related edges if object_id in self.nodes: - selection_id = self.selection[object_id] canvas_node = self.nodes.pop(object_id) + canvas_node.delete() nodes.append(canvas_node) - self.delete(object_id) - self.delete(selection_id) - self.delete(canvas_node.text_id) - canvas_node.delete_antennae() is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type) # delete related edges @@ -391,10 +392,10 @@ class CanvasGraph(tk.Canvas): if edge in edges: continue edges.add(edge) - self.edges.pop(edge.token) - self.delete(edge.id) - self.delete(edge.link_info.id1) - self.delete(edge.link_info.id2) + del self.edges[edge.token] + edge.delete() + + # update node connected to edge being deleted other_id = edge.src other_interface = edge.src_interface if edge.src == object_id: @@ -408,11 +409,11 @@ class CanvasGraph(tk.Canvas): pass if is_wireless: other_node.delete_antenna() + + # delete shape if object_id in self.shapes: - selection_id = self.selection[object_id] - self.shapes[object_id].delete() - self.delete(selection_id) - self.shapes.pop(object_id) + shape = self.shapes.pop(object_id) + shape.delete() self.selection.clear() return nodes diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 2d34780e..0a46fd77 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -49,6 +49,11 @@ class CanvasNode: self.moving = None self.antennae = [] + def delete(self): + self.canvas.delete(self.id) + self.canvas.delete(self.text_id) + self.delete_antennae() + def add_antenna(self): x, y = self.canvas.coords(self.id) offset = len(self.antennae) * 8 @@ -78,7 +83,6 @@ class CanvasNode: :return: nothing """ - logging.info("deleting antennae: %s", self.antennae) for antenna_id in self.antennae: self.canvas.delete(antenna_id) self.antennae.clear() From 583df848683a9096f6f17de4685ac7a81328fde9 Mon Sep 17 00:00:00 2001 From: apwiggins Date: Fri, 6 Dec 2019 08:31:37 -0400 Subject: [PATCH 324/462] Update services.md Added FRR features as of version 7.2. Updated FRR installation notes for Ubuntu 19.10 and later which install from native repositories. Added new Fedora 31 installation note for native repository install. --- docs/services.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/services.md b/docs/services.md index ec38c462..f2a1a38a 100644 --- a/docs/services.md +++ b/docs/services.md @@ -195,24 +195,29 @@ In order to be able to do use the Bird Internet Routing Protocol, you must modif ### FRRouting FRRouting is a routing software package that provides TCP/IP based routing services with routing protocols support such as BGP, RIP, OSPF, IS-IS and more. FRR also supports special BGP Route Reflector and Route Server behavior. In addition to traditional IPv4 routing protocols, FRR also supports IPv6 routing protocols. With an SNMP daemon that supports the AgentX protocol, FRR provides routing protocol MIB read-only access (SNMP Support). -FRR currently supports the following protocols: -* BGP +FRR (as of v7.2) currently supports the following protocols: +* BGPv4 * OSPFv2 * OSPFv3 -* RIPv1 -* RIPv2 -* RIPng +* RIPv1/v2/ng * IS-IS -* PIM-SM/MSDP +* PIM-SM/MSDP/BSM(AutoRP) * LDP * BFD * Babel * PBR * OpenFabric +* VRRPv2/v3 * EIGRP (alpha) * NHRP (alpha) #### FRRouting Package Install +Ubuntu 19.10 and later +```shell +sudo apt update && sudo apt install frr +``` + +Ubuntu 16.04 and Ubuntu 18.04 ```shell sudo apt install curl curl -s https://deb.frrouting.org/frr/keys.asc | sudo apt-key add - @@ -220,6 +225,10 @@ FRRVER="frr-stable" echo deb https://deb.frrouting.org/frr $(lsb_release -s -c) $FRRVER | sudo tee -a /etc/apt/sources.list.d/frr.list sudo apt update && sudo apt install frr frr-pythontools ``` +Fedora 31 +```shell +sudo dnf update && sudo dnf install frr +``` #### FRRouting Source Code Install Building FRR from source is the best way to ensure you have the latest features and bug fixes. Details for each supported platform, including dependency package listings, permissions, and other gotchas, are in the developer’s documentation. From 4a34aaa30d72249d6ba65182e67c876597d077f9 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 6 Dec 2019 09:03:21 -0800 Subject: [PATCH 325/462] working on shapes and texts --- coretk/coretk/coreclient.py | 10 +- coretk/coretk/dialogs/shapemod.py | 225 ++++++++++++++++++++---------- coretk/coretk/dialogs/textmod.py | 26 ++++ coretk/coretk/graph/graph.py | 26 ++-- coretk/coretk/graph/shape.py | 58 +++----- 5 files changed, 222 insertions(+), 123 deletions(-) create mode 100644 coretk/coretk/dialogs/textmod.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 05a36c82..efbc6a39 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -10,7 +10,7 @@ from core.api.grpc import client, core_pb2 from coretk import appconfig from coretk.dialogs.mobilityplayer import MobilityPlayer from coretk.dialogs.sessions import SessionsDialog -from coretk.graph.shape import Shape, ShapeData +from coretk.graph.shape import AnnotationData, Shape from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils @@ -136,6 +136,7 @@ class CoreClient: self.custom_observers[observer.name] = observer def handle_events(self, event): + print(event) if event.HasField("link_event"): logging.info("link event: %s", event) self.handle_link_event(event.link_event) @@ -160,6 +161,8 @@ class CoreClient: self.handle_node_event(event.node_event) elif event.HasField("config_event"): logging.info("config event: %s", event) + elif event.HasField("throughput_event"): + print("throughput") else: logging.info("unhandled event: %s", event) @@ -189,7 +192,7 @@ class CoreClient: interface_throughputs = event.interface_throughputs for i in interface_throughputs: print("") - return + # return throughputs_belong_to_session = [] for if_tp in interface_throughputs: if if_tp.node_id in self.node_ids: @@ -312,8 +315,7 @@ class CoreClient: config_type = annotation_config["type"] if config_type in ["rectangle", "oval"]: coords = tuple(annotation_config["iconcoords"]) - data = ShapeData( - False, + data = AnnotationData( annotation_config["label"], annotation_config["fontfamily"], annotation_config["fontsize"], diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index ebeb7139..4b507dfa 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -5,6 +5,7 @@ import tkinter as tk from tkinter import colorchooser, font, ttk from coretk.dialogs.dialog import Dialog +from coretk.images import ImageEnum FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] @@ -12,9 +13,17 @@ BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] class ShapeDialog(Dialog): def __init__(self, master, app, shape): - super().__init__(master, app, "Add a new shape", modal=True) + self.annotation_type = app.canvas.annotation_type self.canvas = app.canvas - self.id = shape.id + if self.is_shape(): + super().__init__(master, app, "Add a new shape", modal=True) + self.id = shape.id + self.fill = None + self.border = None + else: + super().__init__(master, app, "Add a new text", modal=True) + + self.shape = shape data = shape.shape_data self.shape_text = tk.StringVar(value=data.text) self.font = tk.StringVar(value=data.font) @@ -26,11 +35,18 @@ class ShapeDialog(Dialog): self.bold = tk.IntVar(value=data.bold) self.italic = tk.IntVar(value=data.italic) self.underline = tk.IntVar(value=data.underline) - self.fill = None - self.border = None self.top.columnconfigure(0, weight=1) self.draw() + def is_shape(self): + return ( + self.annotation_type == ImageEnum.OVAL + or self.annotation_type == ImageEnum.RECTANGLE + ) + + def is_text(self): + return self.annotation_type == ImageEnum.TEXT + def draw(self): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) @@ -69,47 +85,53 @@ class ShapeDialog(Dialog): button.grid(row=0, column=2) frame.grid(row=2, column=0, sticky="nsew", padx=3, pady=3) - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=1) - label = ttk.Label(frame, text="Fill color") - label.grid(row=0, column=0, sticky="nsew") - self.fill = ttk.Label(frame, text=self.fill_color, background=self.fill_color) - self.fill.grid(row=0, column=1, sticky="nsew", padx=3) - button = ttk.Button(frame, text="Color", command=self.choose_fill_color) - button.grid(row=0, column=2, sticky="nsew") - frame.grid(row=3, column=0, sticky="nsew", padx=3, pady=3) + if self.is_shape(): + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + label = ttk.Label(frame, text="Fill color") + label.grid(row=0, column=0, sticky="nsew") + self.fill = ttk.Label( + frame, text=self.fill_color, background=self.fill_color + ) + self.fill.grid(row=0, column=1, sticky="nsew", padx=3) + button = ttk.Button(frame, text="Color", command=self.choose_fill_color) + button.grid(row=0, column=2, sticky="nsew") + frame.grid(row=3, column=0, sticky="nsew", padx=3, pady=3) + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + label = ttk.Label(frame, text="Border color:") + label.grid(row=0, column=0, sticky="nsew") + self.border = ttk.Label( + frame, text=self.border_color, background=self.fill_color + ) + self.border.grid(row=0, column=1, sticky="nsew", padx=3) + button = ttk.Button(frame, text="Color", command=self.choose_border_color) + button.grid(row=0, column=2, sticky="nsew") + frame.grid(row=4, column=0, sticky="nsew", padx=3, pady=3) + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=2) + label = ttk.Label(frame, text="Border width:") + label.grid(row=0, column=0, sticky="nsew") + combobox = ttk.Combobox( + frame, + textvariable=self.border_width, + values=BORDER_WIDTH, + state="readonly", + ) + combobox.grid(row=0, column=1, sticky="nsew") + frame.grid(row=5, column=0, sticky="nsew", padx=3, pady=3) frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=1) - label = ttk.Label(frame, text="Border color:") - label.grid(row=0, column=0, sticky="nsew") - self.border = ttk.Label( - frame, text=self.border_color, background=self.fill_color - ) - self.border.grid(row=0, column=1, sticky="nsew", padx=3) - button = ttk.Button(frame, text="Color", command=self.choose_border_color) - button.grid(row=0, column=2, sticky="nsew") - frame.grid(row=4, column=0, sticky="nsew", padx=3, pady=3) - - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=2) - label = ttk.Label(frame, text="Border width:") - label.grid(row=0, column=0, sticky="nsew") - combobox = ttk.Combobox( - frame, textvariable=self.border_width, values=BORDER_WIDTH, state="readonly" - ) - combobox.grid(row=0, column=1, sticky="nsew") - frame.grid(row=5, column=0, sticky="nsew", padx=3, pady=3) - - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - button = ttk.Button(frame, text="Add shape", command=self.add_shape) + button = ttk.Button(frame, text="Add shape", command=self.click_add) button.grid(row=0, column=0, sticky="e", padx=3) button = ttk.Button(frame, text="Cancel", command=self.cancel) button.grid(row=0, column=1, sticky="w", pady=3) @@ -130,11 +152,86 @@ class ShapeDialog(Dialog): self.border.config(background=color[1], text=color[1]) def cancel(self): - if not self.canvas.shapes[self.id].created: + if self.is_shape() and not self.canvas.shapes[self.id].created: self.canvas.delete(self.id) self.canvas.shapes.pop(self.id) self.destroy() + def click_add(self): + if self.is_shape(): + self.add_shape() + elif self.is_text(): + self.add_text() + self.destroy() + + def make_font(self): + """ + create font for text or shape label + :return: list(font specifications) + """ + size = int(self.font_size.get()) + text_font = [self.font.get(), size] + if self.bold.get() == 1: + text_font.append("bold") + if self.italic.get() == 1: + text_font.append("italic") + if self.underline.get() == 1: + text_font.append("underline") + return text_font + + def save_text(self): + """ + save info related to text or shape label + + :return: nothing + """ + data = self.shape.shape_data + data.text = self.shape_text.get() + data.font = self.font.get() + data.font_size = int(self.font_size.get()) + data.text_color = self.text_color + data.bold = self.bold.get() + data.italic = self.italic.get() + data.underline = self.underline.get() + + def save_shape(self): + """ + save info related to shape + + :return: nothing + """ + data = self.shape.shape_data + data.fill_color = self.fill_color + data.border_color = self.border_color + data.border_width = int(self.border_width.get()) + + def add_text(self): + """ + add text to canvas + + :return: nothing + """ + text = self.shape_text.get() + x = self.shape.x0 + y = self.shape.y0 + text_font = self.make_font() + if self.shape.text_id is None: + tid = self.canvas.create_text( + x, y, text=text, fill=self.text_color, font=text_font, tags="text" + ) + self.shape.text_id = tid + self.id = tid + self.shape.id = tid + self.canvas.texts[tid] = self.shape + self.shape.created = True + self.save_text() + print(self.canvas.texts) + # self.canvas.shapes[self.id].created = True + # else: + # self.canvas.itemconfig( + # self.shape.text_id, text=text, fill=self.text_color, font=f + # ) + def add_shape(self): self.canvas.itemconfig( self.id, @@ -143,42 +240,28 @@ class ShapeDialog(Dialog): outline=self.border_color, width=int(self.border_width.get()), ) - shape = self.canvas.shapes[self.id] shape_text = self.shape_text.get() size = int(self.font_size.get()) x0, y0, x1, y1 = self.canvas.bbox(self.id) - text_y = y0 + 1.5 * size - text_x = (x0 + x1) / 2 - f = [self.font.get(), size] - if self.bold.get() == 1: - f.append("bold") - if self.italic.get() == 1: - f.append("italic") - if self.underline.get() == 1: - f.append("underline") - if shape.text_id is None: - shape.text_id = self.canvas.create_text( - text_x, - text_y, + _y = y0 + 1.5 * size + _x = (x0 + x1) / 2 + text_font = self.make_font() + if self.shape.text_id is None: + self.shape.text_id = self.canvas.create_text( + _x, + _y, text=shape_text, fill=self.text_color, - font=f, + font=text_font, tags="shapetext", ) - self.canvas.shapes[self.id].created = True + self.shape.created = True else: self.canvas.itemconfig( - shape.text_id, text=shape_text, fill=self.text_color, font=f + self.shape.text_id, + text=shape_text, + fill=self.text_color, + font=text_font, ) - data = self.canvas.shapes[self.id].shape_data - data.text = shape_text - data.font = self.font.get() - data.font_size = int(self.font_size.get()) - data.text_color = self.text_color - data.fill_color = self.fill_color - data.border_color = self.border_color - data.border_width = int(self.border_width.get()) - data.bold = self.bold.get() - data.italic = self.italic.get() - data.underline = self.underline.get() - self.destroy() + self.save_text() + self.save_shape() diff --git a/coretk/coretk/dialogs/textmod.py b/coretk/coretk/dialogs/textmod.py new file mode 100644 index 00000000..03af38fb --- /dev/null +++ b/coretk/coretk/dialogs/textmod.py @@ -0,0 +1,26 @@ +""" +text dialog +""" +import tkinter as tk +from tkinter import ttk + +from coretk.dialogs.dialog import Dialog + + +class TextDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Add a new text", modal=True) + self.canvas = app.canvas + self.text = tk.StringVar(value="") + + self.draw() + + def draw(self): + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + label = ttk.Label(frame, text="Text for top of text: ") + label.grid(row=0, column=0) + entry = ttk.Entry(frame, textvariable=self.text) + entry.grid(row=0, column=1) + frame.grid(row=0, column=0, sticky="nsew") diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 95ad19dc..688e89da 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -34,6 +34,7 @@ class CanvasGraph(tk.Canvas): self.nodes = {} self.edges = {} self.shapes = {} + self.texts = {} self.wireless_edges = {} self.drawing_edge = None self.grid = None @@ -276,6 +277,8 @@ class CanvasGraph(tk.Canvas): if self.shape_drawing: self.shapes[self.selected].shape_complete(x, y) self.shape_drawing = False + elif self.annotation_type == ImageEnum.TEXT: + self.text.shape_complete(self.text.cursor_x, self.text.cursor_y) else: self.focus_set() self.selected = self.get_selected(event) @@ -424,16 +427,19 @@ class CanvasGraph(tk.Canvas): if self.mode == GraphMode.EDGE and is_node: x, y = self.coords(selected) self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) - if ( - self.mode == GraphMode.ANNOTATION - and self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE] - and selected is None - ): - x, y = self.canvas_xy(event) - shape = Shape(self.app, self, x, y) - self.selected = shape.id - self.shapes[shape.id] = shape - self.shape_drawing = True + + if self.mode == GraphMode.ANNOTATION and selected is None: + if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: + x, y = self.canvas_xy(event) + shape = Shape(self.app, self, x, y) + self.selected = shape.id + self.shapes[shape.id] = shape + self.shape_drawing = True + elif self.annotation_type == ImageEnum.TEXT: + x, y = self.canvas_xy(event) + self.text = Shape(self.app, self, x, y) + # self.shapes[shape.id] = shape + if self.mode == GraphMode.SELECT: if selected is not None: if selected in self.shapes: diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 91020ecb..660ef291 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -1,51 +1,37 @@ """ class for shapes """ -import logging - from coretk.dialogs.shapemod import ShapeDialog from coretk.images import ImageEnum ABOVE_COMPONENT = ["gridline", "edge", "linkinfo", "antenna", "node", "nodename"] -class ShapeData: +class AnnotationData: def __init__( self, - is_default=True, - text=None, - font=None, - font_size=None, - text_color=None, - fill_color=None, - border_color=None, - border_width=None, + text="", + font="Arial", + font_size=12, + text_color="#000000", + fill_color="#CFCFFF", + border_color="#000000", + border_width=0, bold=0, italic=0, underline=0, ): - if is_default: - self.text = "" - self.font = "Arial" - self.font_size = 12 - self.text_color = "#000000" - self.fill_color = "#CFCFFF" - self.border_color = "#000000" - self.border_width = 0 - self.bold = 0 - self.italic = 0 - self.underline = 0 - else: - self.text = text - self.font = font - self.font_size = font_size - self.text_color = text_color - self.fill_color = fill_color - self.border_color = border_color - self.border_width = border_width - self.bold = bold - self.italic = italic - self.underline = underline + + self.text = text + self.font = font + self.font_size = font_size + self.text_color = text_color + self.fill_color = fill_color + self.border_color = border_color + self.border_width = border_width + self.bold = bold + self.italic = italic + self.underline = underline class Shape: @@ -66,7 +52,7 @@ class Shape: self.y0 = top_y self.created = False self.text_id = None - self.shape_data = ShapeData() + self.shape_data = AnnotationData() canvas.delete(canvas.find_withtag("selectednodes")) annotation_type = self.canvas.annotation_type if annotation_type == ImageEnum.OVAL: @@ -112,7 +98,6 @@ class Shape: self.shape_data = data self.cursor_x = None self.cursor_y = None - self.canvas.tag_bind(self.id, "", self.click_release) def shape_motion(self, x1, y1): self.canvas.coords(self.id, self.x0, self.y0, x1, y1) @@ -123,9 +108,6 @@ class Shape: s = ShapeDialog(self.app, self.app, self) s.show() - def click_release(self, event): - logging.debug("Click release on shape %s", self.id) - def motion(self, event, delta_x=None, delta_y=None): if event is not None: delta_x = event.x - self.cursor_x From 49acac026c0cc71332bbfe0052c050b87bdec423 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Dec 2019 09:10:50 -0800 Subject: [PATCH 326/462] updates to move node context logic to node class and added check to display options the same as old core --- coretk/coretk/graph/graph.py | 37 +--------------------- coretk/coretk/graph/node.py | 60 +++++++++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 41 deletions(-) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index abf3f952..4abb6e18 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -4,7 +4,6 @@ import tkinter as tk from PIL import Image, ImageTk from core.api.grpc import core_pb2 -from core.api.grpc.core_pb2 import NodeType from coretk.dialogs.shapemod import ShapeDialog from coretk.graph.edges import CanvasEdge, CanvasWirelessEdge from coretk.graph.enums import GraphMode, ScaleOption @@ -63,40 +62,6 @@ class CanvasGraph(tk.Canvas): self.show_grid = tk.BooleanVar(value=True) self.adjust_to_dim = tk.BooleanVar(value=False) - def create_node_context(self, canvas_node): - node = canvas_node.core_node - context = tk.Menu(self.master) - context.add_command(label="Configure", command=canvas_node.show_config) - if node.type == NodeType.WIRELESS_LAN: - context.add_command( - label="WLAN Config", command=canvas_node.show_wlan_config - ) - if self.master.core.is_runtime(): - if canvas_node.core_node.id in self.master.core.mobility_players: - context.add_command( - label="Mobility Player", - command=canvas_node.show_mobility_player, - ) - else: - context.add_command( - label="Mobility Config", command=canvas_node.show_mobility_config - ) - if node.type == NodeType.EMANE: - context.add_command( - label="EMANE Config", command=canvas_node.show_emane_config - ) - context.add_command(label="Select adjacent", state=tk.DISABLED) - context.add_command(label="Create link to", state=tk.DISABLED) - context.add_command(label="Assign to", state=tk.DISABLED) - context.add_command(label="Move to", state=tk.DISABLED) - context.add_command(label="Cut", state=tk.DISABLED) - context.add_command(label="Copy", state=tk.DISABLED) - context.add_command(label="Paste", state=tk.DISABLED) - context.add_command(label="Delete", state=tk.DISABLED) - context.add_command(label="Hide", state=tk.DISABLED) - context.add_command(label="Services", state=tk.DISABLED) - return context - def reset_and_redraw(self, session): """ Reset the private variables CanvasGraph object, redraw nodes given the new grpc @@ -512,7 +477,7 @@ class CanvasGraph(tk.Canvas): canvas_node = self.nodes.get(selected) if canvas_node: logging.debug(f"node context: {selected}") - self.context = self.create_node_context(canvas_node) + self.context = canvas_node.create_context() self.context.post(event.x_root, event.y_root) else: self.context.unpost() diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 0a46fd77..20e95867 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -2,6 +2,7 @@ import logging import tkinter as tk from tkinter import font +from core.api.grpc.core_pb2 import NodeType from coretk.dialogs.emaneconfig import EmaneConfigDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog @@ -36,6 +37,15 @@ class CanvasNode: fill="#0000CD", ) self.tooltip = CanvasTooltip(self.canvas) + self.edges = set() + self.interfaces = [] + self.wireless_edges = set() + self.moving = None + self.antennae = [] + self.setup_bindings() + + def setup_bindings(self): + # self.canvas.bind("", self.click_context) self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) self.canvas.tag_bind(self.id, "", self.motion) @@ -43,11 +53,6 @@ class CanvasNode: self.canvas.tag_bind(self.id, "", self.select_multiple) self.canvas.tag_bind(self.id, "", self.on_enter) self.canvas.tag_bind(self.id, "", self.on_leave) - self.edges = set() - self.interfaces = [] - self.wireless_edges = set() - self.moving = None - self.antennae = [] def delete(self): self.canvas.delete(self.id) @@ -184,6 +189,51 @@ class CanvasNode: shape = self.canvas.shapes[object_id] shape.motion(None, x - my_x, y - my_y) + def create_context(self): + is_wlan = self.core_node.type == NodeType.WIRELESS_LAN + is_emane = self.core_node.type == NodeType.EMANE + context = tk.Menu(self.canvas) + if self.app.core.is_runtime(): + context.add_command(label="Configure", command=self.show_config) + if is_wlan and self.core_node.id in self.app.core.mobility_players: + context.add_command( + label="Mobility Player", command=self.show_mobility_player + ) + context.add_command(label="Select Adjacent", state=tk.DISABLED) + context.add_command(label="Hide", state=tk.DISABLED) + context.add_command(label="Services", state=tk.DISABLED) + if NodeUtils.is_container_node(self.core_node.type): + context.add_command(label="Shell Window", state=tk.DISABLED) + context.add_command(label="Tcpdump", state=tk.DISABLED) + context.add_command(label="Tshark", state=tk.DISABLED) + context.add_command(label="Wireshark", state=tk.DISABLED) + context.add_command(label="View Log", state=tk.DISABLED) + else: + context.add_command(label="Configure", command=self.show_config) + if is_emane: + context.add_command( + label="EMANE Config", command=self.show_emane_config + ) + if is_wlan: + context.add_command(label="WLAN Config", command=self.show_wlan_config) + context.add_command( + label="Mobility Config", command=self.show_mobility_config + ) + if is_wlan or is_emane: + context.add_command(label="Link To All MDRs", state=tk.DISABLED) + context.add_command(label="Select Members", state=tk.DISABLED) + context.add_command(label="Select Adjacent", state=tk.DISABLED) + context.add_command(label="Create Link To", state=tk.DISABLED) + context.add_command(label="Assign To", state=tk.DISABLED) + context.add_command(label="Move To", state=tk.DISABLED) + context.add_command(label="Cut", state=tk.DISABLED) + context.add_command(label="Copy", state=tk.DISABLED) + context.add_command(label="Paste", state=tk.DISABLED) + context.add_command(label="Delete", state=tk.DISABLED) + context.add_command(label="Hide", state=tk.DISABLED) + context.add_command(label="Services", state=tk.DISABLED) + return context + def select_multiple(self, event): self.canvas.select_object(self.id, choose_multiple=True) From 27ead56a15cc056db39a343cc419fa7428c304f0 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 6 Dec 2019 09:11:21 -0800 Subject: [PATCH 327/462] remove redundant file --- coretk/coretk/dialogs/textmod.py | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 coretk/coretk/dialogs/textmod.py diff --git a/coretk/coretk/dialogs/textmod.py b/coretk/coretk/dialogs/textmod.py deleted file mode 100644 index 03af38fb..00000000 --- a/coretk/coretk/dialogs/textmod.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -text dialog -""" -import tkinter as tk -from tkinter import ttk - -from coretk.dialogs.dialog import Dialog - - -class TextDialog(Dialog): - def __init__(self, master, app): - super().__init__(master, app, "Add a new text", modal=True) - self.canvas = app.canvas - self.text = tk.StringVar(value="") - - self.draw() - - def draw(self): - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - label = ttk.Label(frame, text="Text for top of text: ") - label.grid(row=0, column=0) - entry = ttk.Entry(frame, textvariable=self.text) - entry.grid(row=0, column=1) - frame.grid(row=0, column=0, sticky="nsew") From b9bbf397c96f9eaade45e97a093602b6e0d3ce27 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Dec 2019 09:13:58 -0800 Subject: [PATCH 328/462] updated node context menu option for wireless to use nodeutils --- coretk/coretk/graph/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 20e95867..f726739c 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -219,7 +219,7 @@ class CanvasNode: context.add_command( label="Mobility Config", command=self.show_mobility_config ) - if is_wlan or is_emane: + if NodeUtils.is_wireless_node(self.core_node.type): context.add_command(label="Link To All MDRs", state=tk.DISABLED) context.add_command(label="Select Members", state=tk.DISABLED) context.add_command(label="Select Adjacent", state=tk.DISABLED) From 45a23a6c1417d7dc08af9e824d0f65749251c6be Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Dec 2019 09:42:41 -0800 Subject: [PATCH 329/462] updated usage of time.time to time.monotonic or time.perf_counter due to time.time possibly rolling backwards --- coretk/coretk/coreclient.py | 8 ++++---- coretk/coretk/menuaction.py | 4 ++-- daemon/core/api/grpc/server.py | 2 +- daemon/core/api/tlv/corehandlers.py | 2 +- daemon/core/emulator/session.py | 8 ++++---- daemon/core/location/event.py | 8 ++++---- daemon/core/location/mobility.py | 10 +++++----- daemon/core/nodes/network.py | 8 ++++---- daemon/core/services/coreservices.py | 4 ++-- daemon/tests/test_grpc.py | 2 +- daemon/tests/test_gui.py | 2 +- 11 files changed, 29 insertions(+), 29 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 05a36c82..e2aef5a2 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -411,7 +411,7 @@ class CoreClient: else: emane_config = None - start = time.time() + start = time.perf_counter() response = self.client.start_session( self.session_id, nodes, @@ -425,7 +425,7 @@ class CoreClient: service_configs, file_configs, ) - process_time = time.time() - start + process_time = time.perf_counter() - start logging.debug("start session(%s), result: %s", self.session_id, response.result) self.app.statusbar.start_session_callback(process_time) @@ -439,9 +439,9 @@ class CoreClient: def stop_session(self, session_id=None): if not session_id: session_id = self.session_id - start = time.time() + start = time.perf_counter() response = self.client.stop_session(session_id) - process_time = time.time() - start + process_time = time.perf_counter() - start self.app.statusbar.stop_session_callback(process_time) logging.debug("stopped session(%s), result: %s", session_id, response.result) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index d8dc89cb..1a64800c 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -33,10 +33,10 @@ class MenuAction: def cleanup_old_session(self, quitapp=False): logging.info("cleaning up old session") - start = time.time() + start = time.perf_counter() self.app.core.stop_session() self.app.core.delete_session() - process_time = time.time() - start + process_time = time.perf_counter() - start self.app.statusbar.stop_session_callback(process_time) if quitapp: self.app.quit() diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index a914d617..6a6747f1 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -660,7 +660,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): last_check = None last_stats = None while self._is_running(context): - now = time.time() + now = time.monotonic() stats = get_net_stats() # calculate average diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 321306a2..009cb067 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -1710,7 +1710,7 @@ class CoreHandler(socketserver.BaseRequestHandler): event_type=event_type, name=name, data=fail_data + ";" + unknown_data, - time=str(time.time()), + time=str(time.monotonic()), ) self.session.broadcast_event(event_data) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index fee2bde4..1a01bdaa 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -103,7 +103,7 @@ class Session: # TODO: should the default state be definition? self.state = EventTypes.NONE.value - self._state_time = time.time() + self._state_time = time.monotonic() self._state_file = os.path.join(self.session_dir, "state") # hooks handlers @@ -1030,7 +1030,7 @@ class Session: return self.state = state_value - self._state_time = time.time() + self._state_time = time.monotonic() logging.info("changing session(%s) to state %s", self.id, state_name) self.write_state(state_value) @@ -1038,7 +1038,7 @@ class Session: self.run_state_hooks(state_value) if send_event: - event_data = EventData(event_type=state_value, time=str(time.time())) + event_data = EventData(event_type=state_value, time=str(time.monotonic())) self.broadcast_event(event_data) def write_state(self, state): @@ -1821,7 +1821,7 @@ class Session: if not in runtime. """ if self.state == EventTypes.RUNTIME_STATE.value: - return time.time() - self._state_time + return time.monotonic() - self._state_time else: return 0.0 diff --git a/daemon/core/location/event.py b/daemon/core/location/event.py index 11e535d3..f930d9b7 100644 --- a/daemon/core/location/event.py +++ b/daemon/core/location/event.py @@ -145,7 +145,7 @@ class EventLoop: with self.lock: if not self.running or not self.queue: break - now = time.time() + now = time.monotonic() if self.queue[0].time > now: schedule = True break @@ -170,7 +170,7 @@ class EventLoop: raise ValueError("scheduling event while not running") if not self.queue: return - delay = self.queue[0].time - time.time() + delay = self.queue[0].time - time.monotonic() if self.timer: raise ValueError("timer was already set") self.timer = Timer(delay, self.__run_events) @@ -187,7 +187,7 @@ class EventLoop: if self.running: return self.running = True - self.start = time.time() + self.start = time.monotonic() for event in self.queue: event.time += self.start self.__schedule_event() @@ -225,7 +225,7 @@ class EventLoop: self.eventnum += 1 evtime = float(delaysec) if self.running: - evtime += time.time() + evtime += time.monotonic() event = Event(eventnum, evtime, func, *args, **kwds) if self.queue: diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 6a796526..b3eb4884 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -174,7 +174,7 @@ class MobilityManager(ModelManager): event_type=event_type, name=f"mobility:{model.name}", data=data, - time=str(time.time()), + time=str(time.monotonic()), ) self.session.broadcast_event(event_data) @@ -612,7 +612,7 @@ class WayPointMobility(WirelessModel): if self.state != self.STATE_RUNNING: return t = self.lasttime - self.lasttime = time.time() + self.lasttime = time.monotonic() now = self.lasttime - self.timezero dt = self.lasttime - t @@ -664,7 +664,7 @@ class WayPointMobility(WirelessModel): :return: nothing """ logging.info("running mobility scenario") - self.timezero = time.time() + self.timezero = time.monotonic() self.lasttime = self.timezero - (0.001 * self.refresh_ms) self.movenodesinitial() self.runround() @@ -844,7 +844,7 @@ class WayPointMobility(WirelessModel): self.lasttime = 0 self.run() elif laststate == self.STATE_PAUSED: - now = time.time() + now = time.monotonic() self.timezero += now - self.lasttime self.lasttime = now - (0.001 * self.refresh_ms) self.runround() @@ -871,7 +871,7 @@ class WayPointMobility(WirelessModel): :return: nothing """ self.state = self.STATE_PAUSED - self.lasttime = time.time() + self.lasttime = time.monotonic() class Ns2ScriptedMobility(WayPointMobility): diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 6fe291dd..f0639649 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -57,7 +57,7 @@ class EbtablesQueue: :return: nothing """ with self.updatelock: - self.last_update_time[wlan] = time.time() + self.last_update_time[wlan] = time.monotonic() if self.doupdateloop: return @@ -108,9 +108,9 @@ class EbtablesQueue: :rtype: float """ try: - elapsed = time.time() - self.last_update_time[wlan] + elapsed = time.monotonic() - self.last_update_time[wlan] except KeyError: - self.last_update_time[wlan] = time.time() + self.last_update_time[wlan] = time.monotonic() elapsed = 0.0 return elapsed @@ -122,7 +122,7 @@ class EbtablesQueue: :param wlan: wlan entity :return: nothing """ - self.last_update_time[wlan] = time.time() + self.last_update_time[wlan] = time.monotonic() self.updates.remove(wlan) def updateloop(self): diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index 80168425..361061be 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -538,13 +538,13 @@ class CoreServices: time.sleep(service.validation_timer) # non-blocking, attempt to validate periodically, up to validation_timer time elif service.validation_mode == ServiceMode.NON_BLOCKING: - start = time.time() + start = time.monotonic() while True: status = self.validate_service(node, service) if not status: break - if time.time() - start > service.validation_timer: + if time.monotonic() - start > service.validation_timer: break time.sleep(service.validation_period) diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 28005dbb..56523e81 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -1042,7 +1042,7 @@ class TestGrpc: client.events(session.id, handle_event) time.sleep(0.1) event = EventData( - event_type=EventTypes.RUNTIME_STATE.value, time=str(time.time()) + event_type=EventTypes.RUNTIME_STATE.value, time=str(time.monotonic()) ) session.broadcast_event(event) diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index c2a8c9fc..91324d08 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -523,7 +523,7 @@ class TestGui: MessageFlags.ADD.value, [ (EventTlvs.TYPE, EventTypes.SCHEDULED.value), - (EventTlvs.TIME, str(time.time() + 100)), + (EventTlvs.TIME, str(time.monotonic() + 100)), (EventTlvs.NODE, node.id), (EventTlvs.NAME, "event"), (EventTlvs.DATA, "data"), From 9ef5cdd70be6fa180dc97a8628e6f19761283061 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Dec 2019 11:13:43 -0800 Subject: [PATCH 330/462] added basic about dialog and created codetext widget for displaying text in terminal like colors using scrolledtext widget --- coretk/coretk/dialogs/about.py | 44 +++++++++++++++++++ coretk/coretk/dialogs/hooks.py | 3 +- coretk/coretk/dialogs/serviceconfiguration.py | 5 +-- coretk/coretk/menuaction.py | 5 +++ coretk/coretk/menubar.py | 2 +- coretk/coretk/widgets.py | 26 ++++++++++- 6 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 coretk/coretk/dialogs/about.py diff --git a/coretk/coretk/dialogs/about.py b/coretk/coretk/dialogs/about.py new file mode 100644 index 00000000..28ddea33 --- /dev/null +++ b/coretk/coretk/dialogs/about.py @@ -0,0 +1,44 @@ +import tkinter as tk + +from coretk.dialogs.dialog import Dialog +from coretk.widgets import CodeText + +LICENSE = """\ +Copyright (c) 2005-2020, the Boeing Company. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE.\ +""" + + +class AboutDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "About CORE", modal=True) + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + + text = CodeText(self.top) + text.insert("1.0", LICENSE) + text.config(state=tk.DISABLED) + text.grid(sticky="nsew") diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index dc677e12..d40f1c36 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -3,6 +3,7 @@ from tkinter import ttk from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog +from coretk.widgets import CodeText class HookDialog(Dialog): @@ -43,7 +44,7 @@ class HookDialog(Dialog): frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) frame.grid(row=1, sticky="nsew", pady=2) - self.data = tk.Text(frame) + self.data = CodeText(frame) self.data.insert( 1.0, ( diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 1d8059d8..446f9c12 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -2,12 +2,11 @@ import logging import tkinter as tk from tkinter import ttk -from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog from coretk.images import ImageEnum, Images -from coretk.widgets import ListboxScroll +from coretk.widgets import CodeText, ListboxScroll class ServiceConfiguration(Dialog): @@ -182,7 +181,7 @@ class ServiceConfiguration(Dialog): button.grid(row=0, column=2) frame.grid(row=3, column=0, sticky="nsew") - self.service_file_data = ScrolledText(tab1) + self.service_file_data = CodeText(tab1) self.service_file_data.grid(row=4, column=0, sticky="nsew") if len(self.filenames) > 0: self.filename_combobox.current(0) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 1a64800c..c4cc27bc 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -12,6 +12,7 @@ import grpc from core.api.grpc import core_pb2 from coretk.appconfig import XML_PATH +from coretk.dialogs.about import AboutDialog from coretk.dialogs.canvasbackground import CanvasBackgroundDialog from coretk.dialogs.canvassizeandscale import SizeAndScaleDialog from coretk.dialogs.hooks import HooksDialog @@ -151,3 +152,7 @@ class MenuAction: def edit_observer_widgets(self): dialog = ObserverDialog(self.app, self.app) dialog.show() + + def show_about(self): + dialog = AboutDialog(self.app, self.app) + dialog.show() diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index e56c55ec..288a517f 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -462,5 +462,5 @@ class Menubar(tk.Menu): label="Core Documentation (www)", command=self.menuaction.help_core_documentation, ) - menu.add_command(label="About", state=tk.DISABLED) + menu.add_command(label="About", command=self.menuaction.show_about) self.add_cascade(label="Help", menu=menu) diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index bd6904b2..760846a3 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -1,7 +1,8 @@ import logging import tkinter as tk from functools import partial -from tkinter import ttk +from tkinter import font, ttk +from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 @@ -155,3 +156,26 @@ class CheckboxList(FrameScroll): func = partial(self.clicked, name, var) checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func) checkbox.grid(sticky="w") + + +class CodeFont(font.Font): + def __init__(self): + super().__init__(font="TkFixedFont", color="green") + + +class CodeText(ScrolledText): + def __init__(self, master, **kwargs): + super().__init__( + master, + bd=0, + bg="black", + cursor="xterm lime lime", + fg="lime", + font=CodeFont(), + highlightbackground="black", + insertbackground="lime", + selectbackground="lime", + selectforeground="black", + relief=tk.FLAT, + **kwargs + ) From c238b5dfc8ac2b5d5bd708c9d7b81a749c147b55 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Dec 2019 11:17:05 -0800 Subject: [PATCH 331/462] moved node context services option higher for better convenience, added check that context services only shown for container nodes --- coretk/coretk/graph/node.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index f726739c..70424176 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -195,13 +195,14 @@ class CanvasNode: context = tk.Menu(self.canvas) if self.app.core.is_runtime(): context.add_command(label="Configure", command=self.show_config) + if NodeUtils.is_container_node(self.core_node.type): + context.add_command(label="Services", state=tk.DISABLED) if is_wlan and self.core_node.id in self.app.core.mobility_players: context.add_command( label="Mobility Player", command=self.show_mobility_player ) context.add_command(label="Select Adjacent", state=tk.DISABLED) context.add_command(label="Hide", state=tk.DISABLED) - context.add_command(label="Services", state=tk.DISABLED) if NodeUtils.is_container_node(self.core_node.type): context.add_command(label="Shell Window", state=tk.DISABLED) context.add_command(label="Tcpdump", state=tk.DISABLED) @@ -210,6 +211,8 @@ class CanvasNode: context.add_command(label="View Log", state=tk.DISABLED) else: context.add_command(label="Configure", command=self.show_config) + if NodeUtils.is_container_node(self.core_node.type): + context.add_command(label="Services", state=tk.DISABLED) if is_emane: context.add_command( label="EMANE Config", command=self.show_emane_config @@ -231,7 +234,6 @@ class CanvasNode: context.add_command(label="Paste", state=tk.DISABLED) context.add_command(label="Delete", state=tk.DISABLED) context.add_command(label="Hide", state=tk.DISABLED) - context.add_command(label="Services", state=tk.DISABLED) return context def select_multiple(self, event): From a7d9d588ae97f8ffd7a830cab84207f8bc73677e Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 6 Dec 2019 12:46:00 -0800 Subject: [PATCH 332/462] add display throughput back to the gui --- coretk/coretk/coreclient.py | 29 +++--- coretk/coretk/graph/graph.py | 1 + coretk/coretk/graph/linkinfo.py | 160 +++++++++++++++----------------- coretk/coretk/graph/node.py | 2 + 4 files changed, 94 insertions(+), 98 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index efbc6a39..d4cfa2a6 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -136,7 +136,6 @@ class CoreClient: self.custom_observers[observer.name] = observer def handle_events(self, event): - print(event) if event.HasField("link_event"): logging.info("link event: %s", event) self.handle_link_event(event.link_event) @@ -161,8 +160,6 @@ class CoreClient: self.handle_node_event(event.node_event) elif event.HasField("config_event"): logging.info("config event: %s", event) - elif event.HasField("throughput_event"): - print("throughput") else: logging.info("unhandled event: %s", event) @@ -189,16 +186,21 @@ class CoreClient: canvas_node.move(x, y, update=False) def handle_throughputs(self, event): - interface_throughputs = event.interface_throughputs - for i in interface_throughputs: - print("") - # return - throughputs_belong_to_session = [] - for if_tp in interface_throughputs: - if if_tp.node_id in self.node_ids: - throughputs_belong_to_session.append(if_tp) - self.throughput_draw.process_grpc_throughput_event( - throughputs_belong_to_session + # interface_throughputs = event.interface_throughputs + # # print(interface_throughputs) + # # return + # # for i in interface_throughputs: + # # print("") + # # # return + # print(event) + # throughputs_belong_to_session = [] + # print(self.node_ids) + # for throughput in interface_throughputs: + # if throughput.node_id in self.node_ids: + # throughputs_belong_to_session.append(throughput) + # print(throughputs_belong_to_session) + self.app.canvas.throughput_draw.process_grpc_throughput_event( + event.interface_throughputs ) def join_session(self, session_id, query_location=True): @@ -214,6 +216,7 @@ class CoreClient: session = response.session self.state = session.state self.client.events(self.session_id, self.handle_events) + self.client.throughputs(self.handle_throughputs) # get location if query_location: diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 4d7b9c65..9587cbdb 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -395,6 +395,7 @@ class CanvasGraph(tk.Canvas): if edge in edges: continue edges.add(edge) + self.throughput_draw.delete(edge) del self.edges[edge.token] edge.delete() diff --git a/coretk/coretk/graph/linkinfo.py b/coretk/coretk/graph/linkinfo.py index c78fe7e4..15c08e45 100644 --- a/coretk/coretk/graph/linkinfo.py +++ b/coretk/coretk/graph/linkinfo.py @@ -1,10 +1,11 @@ """ Link information, such as IPv4, IPv6 and throughput drawn in the canvas """ -import logging import tkinter as tk from tkinter import font +from core.api.grpc import core_pb2 + TEXT_DISTANCE = 0.30 @@ -76,6 +77,7 @@ class Throughput: self.tracker = {} # map an edge canvas id to a throughput canvas id self.map = {} + # map edge canvas id to token self.edge_id_to_token = {} def load_throughput_info(self, interface_throughputs): @@ -86,21 +88,23 @@ class Throughput: throughputs :return: nothing """ - for t in interface_throughputs: - nid = t.node_id - iid = t.interface_id - tp = t.throughput - token = self.core.interface_to_edge[(nid, iid)] - print(token) - edge_id = self.canvas.edges[token].id - - self.edge_id_to_token[edge_id] = token - - if edge_id not in self.tracker: - self.tracker[edge_id] = tp - else: - temp = self.tracker[edge_id] - self.tracker[edge_id] = (temp + tp) / 2 + for throughput in interface_throughputs: + nid = throughput.node_id + iid = throughput.interface_id + tp = throughput.throughput + token = self.core.interface_to_edge.get((nid, iid)) + if token: + edge = self.canvas.edges.get(token) + if edge: + edge_id = edge.id + self.edge_id_to_token[edge_id] = token + if edge_id not in self.tracker: + self.tracker[edge_id] = tp + else: + temp = self.tracker[edge_id] + self.tracker[edge_id] = (temp + tp) / 2 + else: + self.core.interface_to_edge.pop((nid, iid), None) def edge_is_wired(self, token): """ @@ -112,41 +116,35 @@ class Throughput: canvas_edge = self.canvas.edges[token] canvas_src_id = canvas_edge.src canvas_dst_id = canvas_edge.dst - src_node = self.canvas.nodes[canvas_src_id] - dst_node = self.canvas.nodes[canvas_dst_id] - - if src_node.node_type == "wlan": - if dst_node.node_type == "mdr": - return False - else: - logging.debug("linkinfo.py is_wired WARNING wlan only connected to mdr") - return True - if dst_node.node_type == "wlan": - if src_node.node_type == "mdr": - return False - else: - logging.debug("linkinfo.py is_wired WARNING wlan only connected to mdr") - return True - return True + src = self.canvas.nodes[canvas_src_id].core_node + dst = self.canvas.nodes[canvas_dst_id].core_node + return not ( + src.type == core_pb2.NodeType.WIRELESS_LAN + and dst.model == "mdr" + or src.model == "mdr" + and dst.type == core_pb2.NodeType.WIRELESS_LAN + ) def draw_wired_throughput(self, edge_id): - x1, y1, x2, y2 = self.canvas.coords(edge_id) - x = (x1 + x2) / 2 - y = (y1 + y2) / 2 - + x0, y0, x1, y1 = self.canvas.coords(edge_id) + x = (x0 + x1) / 2 + y = (y0 + y1) / 2 if edge_id not in self.map: - tp_id = self.canvas.create_text( - x, y, text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]) - ) - self.map[edge_id] = tp_id - - # redraw throughput - else: - self.canvas.itemconfig( - self.map[edge_id], + tpid = self.canvas.create_text( + x, + y, + tags="throughput", + font=("Arial", 8), text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), ) + self.map[edge_id] = tpid + else: + tpid = self.map[edge_id] + self.canvas.coords(tpid, x, y) + self.canvas.itemconfig( + tpid, text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]) + ) def draw_wireless_throughput(self, edge_id): token = self.edge_id_to_token[edge_id] @@ -156,17 +154,19 @@ class Throughput: src_node = self.canvas.nodes[canvas_src_id] dst_node = self.canvas.nodes[canvas_dst_id] - # non_wlan_node = None - if src_node.node_type == "wlan": - non_wlan_node = dst_node - else: - non_wlan_node = src_node + not_wlan = ( + dst_node + if src_node.core_node.type == core_pb2.NodeType.WIRELESS_LAN + else src_node + ) - x, y = self.canvas.coords(non_wlan_node.id) + x, y = self.canvas.coords(not_wlan.id) if edge_id not in self.map: tp_id = self.canvas.create_text( x + 50, y + 25, + font=("Arial", 8), + tags="throughput", text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), ) self.map[edge_id] = tp_id @@ -184,43 +184,33 @@ class Throughput: self.draw_wired_throughput(edge_id) else: self.draw_wireless_throughput(edge_id) - # draw wireless throughput - - # x1, y1, x2, y2 = self.canvas.coords(edge_id) - # x = (x1 + x2) / 2 - # y = (y1 + y2) / 2 - # - # print(self.is_wired(self.edge_id_to_token[edge_id])) - # # new throughput - # if edge_id not in self.map: - # tp_id = self.canvas.create_text( - # x, y, text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]) - # ) - # self.map[edge_id] = tp_id - # - # # redraw throughput - # else: - # self.canvas.itemconfig( - # self.map[edge_id], - # text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), - # ) def process_grpc_throughput_event(self, interface_throughputs): self.load_throughput_info(interface_throughputs) self.draw_throughputs() - def update_throughtput_location(self, edge): - tp_id = self.map[edge.id] - if self.edge_is_wired(self.edge_id_to_token[edge.id]): - x1, y1 = self.canvas.coords(edge.src) - x2, y2 = self.canvas.coords(edge.dst) - x = (x1 + x2) / 2 - y = (y1 + y2) / 2 - self.canvas.coords(tp_id, x, y) - else: - if self.canvas.nodes[edge.src].node_type == "wlan": - x, y = self.canvas.coords(edge.dst) - self.canvas.coords(tp_id, x + 50, y + 20) + def move(self, edge): + tpid = self.map.get(edge.id) + if tpid: + if self.edge_is_wired(edge.token): + x0, y0, x1, y1 = self.canvas.coords(edge.id) + self.canvas.coords(tpid, (x0 + x1) / 2, (y0 + y1) / 2) else: - x, y = self.canvas.coords(edge.src) - self.canvas.coords(tp_id, x + 50, y + 25) + if ( + self.canvas.nodes[edge.src].core_node.type + == core_pb2.NodeType.WIRELESS_LAN + ): + x, y = self.canvas.coords(edge.dst) + self.canvas.coords(tpid, x + 50, y + 20) + else: + x, y = self.canvas.coords(edge.src) + self.canvas.coords(tpid, x + 50, y + 25) + + def delete(self, edge): + tpid = self.map.get(edge.id) + if tpid: + eid = edge.id + self.canvas.delete(tpid) + self.tracker.pop(eid) + self.map.pop(eid) + self.edge_id_to_token.pop(eid) diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 0a46fd77..e80f62d2 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -117,6 +117,8 @@ class CanvasNode: self.canvas.coords(edge.id, x, y, x2, y2) else: self.canvas.coords(edge.id, x1, y1, x, y) + self.canvas.throughput_draw.move(edge) + edge.link_info.recalculate_info() for edge in self.wireless_edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) From 742eb2bed6f0cec0b4d9a1f156e165f8c1b01f8d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Dec 2019 14:01:03 -0800 Subject: [PATCH 333/462] cleanup for shapes and creating shapes and storing and restoring shapes from metadata --- coretk/coretk/coreclient.py | 69 +++++++++----- coretk/coretk/dialogs/shapemod.py | 49 +++++----- coretk/coretk/graph/enums.py | 5 -- coretk/coretk/graph/graph.py | 23 +++-- coretk/coretk/graph/shape.py | 143 +++++++++++++++++------------- coretk/coretk/graph/shapeutils.py | 19 ++++ coretk/coretk/toolbar.py | 19 ++-- 7 files changed, 190 insertions(+), 137 deletions(-) create mode 100644 coretk/coretk/graph/shapeutils.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 4295fc21..f2f400d5 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -5,12 +5,14 @@ import json import logging import os import time +from pathlib import Path from core.api.grpc import client, core_pb2 from coretk import appconfig from coretk.dialogs.mobilityplayer import MobilityPlayer from coretk.dialogs.sessions import SessionsDialog from coretk.graph.shape import AnnotationData, Shape +from coretk.graph.shapeutils import ShapeType from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils @@ -136,7 +138,6 @@ class CoreClient: self.custom_observers[observer.name] = observer def handle_events(self, event): - print(event) if event.HasField("link_event"): logging.info("link event: %s", event) self.handle_link_event(event.link_event) @@ -162,7 +163,7 @@ class CoreClient: elif event.HasField("config_event"): logging.info("config event: %s", event) elif event.HasField("throughput_event"): - print("throughput") + logging.info("throughput event: %s", event) else: logging.info("unhandled event: %s", event) @@ -301,35 +302,42 @@ class CoreClient: def parse_metadata(self, config): # canvas setting canvas_config = config.get("canvas") + logging.info("canvas metadata: %s", canvas_config) if canvas_config: - logging.info("canvas metadata: %s", canvas_config) canvas_config = json.loads(canvas_config) wallpaper_style = canvas_config["wallpaper-style"] self.app.canvas.scale_option.set(wallpaper_style) wallpaper = canvas_config["wallpaper"] - wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) - self.app.canvas.set_wallpaper(wallpaper) - for key, annotation_config in config.items(): - if "annotation" in key: - annotation_config = json.loads(annotation_config) - config_type = annotation_config["type"] - if config_type in ["rectangle", "oval"]: - coords = tuple(annotation_config["iconcoords"]) + if wallpaper: + wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) + self.app.canvas.set_wallpaper(wallpaper) + + # load saved shapes + shapes_config = config.get("shapes") + if shapes_config: + shapes_config = json.loads(shapes_config) + for shape_config in shapes_config: + logging.info("loading shape: %s", shapes_config) + shape_type = shape_config["type"] + try: + shape_type = ShapeType(shape_type) + x1, y1, x2, y2 = shape_config["iconcoords"] data = AnnotationData( - annotation_config["label"], - annotation_config["fontfamily"], - annotation_config["fontsize"], - annotation_config["labelcolor"], - annotation_config["color"], - annotation_config["border"], - annotation_config["width"], + shape_config["label"], + shape_config["fontfamily"], + shape_config["fontsize"], + shape_config["labelcolor"], + shape_config["color"], + shape_config["border"], + shape_config["width"], ) shape = Shape( - self.app, self.app.canvas, None, None, coords, data, config_type + self.app, self.app.canvas, shape_type, x1, y1, x2, y2, data ) self.app.canvas.shapes[shape.id] = shape - else: - logging.debug("not implemented") + except ValueError: + logging.debug("unknown shape: %s", shape_type) + for tag in LIFT_ORDER: self.app.canvas.tag_raise(tag) @@ -427,6 +435,7 @@ class CoreClient: service_configs, file_configs, ) + self.set_metadata() process_time = time.perf_counter() - start logging.debug("start session(%s), result: %s", self.session_id, response.result) self.app.statusbar.start_session_callback(process_time) @@ -447,6 +456,24 @@ class CoreClient: self.app.statusbar.stop_session_callback(process_time) logging.debug("stopped session(%s), result: %s", session_id, response.result) + def set_metadata(self): + # create canvas data + canvas_config = { + "wallpaper": Path(self.app.canvas.wallpaper_file).name, + "wallpaper-style": self.app.canvas.scale_option.get(), + } + canvas_config = json.dumps(canvas_config) + + # create shapes data + shapes = [] + for shape in self.app.canvas.shapes.values(): + shapes.append(shape.metadata()) + shapes = json.dumps(shapes) + + metadata = {"canvas": canvas_config, "shapes": shapes} + response = self.client.set_session_metadata(self.session_id, metadata) + logging.info("set session metadata: %s", response) + def launch_terminal(self, node_id): response = self.client.get_node_terminal(self.session_id, node_id) logging.info("get terminal %s", response.terminal) diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index 4b507dfa..faff5ef8 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -5,7 +5,7 @@ import tkinter as tk from tkinter import colorchooser, font, ttk from coretk.dialogs.dialog import Dialog -from coretk.images import ImageEnum +from coretk.graph.shapeutils import is_draw_shape, is_shape_text FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] @@ -13,40 +13,34 @@ BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] class ShapeDialog(Dialog): def __init__(self, master, app, shape): - self.annotation_type = app.canvas.annotation_type - self.canvas = app.canvas - if self.is_shape(): - super().__init__(master, app, "Add a new shape", modal=True) + if is_draw_shape(shape.shape_type): + title = "Add Shape" self.id = shape.id - self.fill = None - self.border = None else: - super().__init__(master, app, "Add a new text", modal=True) - + title = "Add Text" + self.id = None + self.canvas = app.canvas + super().__init__(master, app, title, modal=True) + self.fill = None + self.border = None self.shape = shape data = shape.shape_data self.shape_text = tk.StringVar(value=data.text) self.font = tk.StringVar(value=data.font) self.font_size = tk.IntVar(value=data.font_size) self.text_color = data.text_color - self.fill_color = data.fill_color + fill_color = data.fill_color + if not fill_color: + fill_color = "#CFCFFF" + self.fill_color = fill_color self.border_color = data.border_color - self.border_width = tk.IntVar(value=data.border_width) + self.border_width = tk.IntVar(value=0) self.bold = tk.IntVar(value=data.bold) self.italic = tk.IntVar(value=data.italic) self.underline = tk.IntVar(value=data.underline) self.top.columnconfigure(0, weight=1) self.draw() - def is_shape(self): - return ( - self.annotation_type == ImageEnum.OVAL - or self.annotation_type == ImageEnum.RECTANGLE - ) - - def is_text(self): - return self.annotation_type == ImageEnum.TEXT - def draw(self): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) @@ -85,7 +79,7 @@ class ShapeDialog(Dialog): button.grid(row=0, column=2) frame.grid(row=2, column=0, sticky="nsew", padx=3, pady=3) - if self.is_shape(): + if is_draw_shape(self.shape.shape_type): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) @@ -152,15 +146,18 @@ class ShapeDialog(Dialog): self.border.config(background=color[1], text=color[1]) def cancel(self): - if self.is_shape() and not self.canvas.shapes[self.id].created: + if ( + is_draw_shape(self.shape.shape_type) + and not self.canvas.shapes[self.id].created + ): self.canvas.delete(self.id) self.canvas.shapes.pop(self.id) self.destroy() def click_add(self): - if self.is_shape(): + if is_draw_shape(self.shape.shape_type): self.add_shape() - elif self.is_text(): + elif is_shape_text(self.shape.shape_type): self.add_text() self.destroy() @@ -212,8 +209,8 @@ class ShapeDialog(Dialog): :return: nothing """ text = self.shape_text.get() - x = self.shape.x0 - y = self.shape.y0 + x = self.shape.x1 + y = self.shape.y1 text_font = self.make_font() if self.shape.text_id is None: tid = self.canvas.create_text( diff --git a/coretk/coretk/graph/enums.py b/coretk/coretk/graph/enums.py index 65fb10eb..b292938f 100644 --- a/coretk/coretk/graph/enums.py +++ b/coretk/coretk/graph/enums.py @@ -16,8 +16,3 @@ class ScaleOption(enum.Enum): CENTERED = 2 SCALED = 3 TILED = 4 - - -class ShapeType(enum.Enum): - OVAL = 0 - RECTANGLE = 1 diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 58ae990a..fdade021 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -10,7 +10,8 @@ from coretk.graph.enums import GraphMode, ScaleOption from coretk.graph.linkinfo import LinkInfo, Throughput from coretk.graph.node import CanvasNode from coretk.graph.shape import Shape -from coretk.images import ImageEnum, Images +from coretk.graph.shapeutils import is_draw_shape, is_shape_text +from coretk.images import Images from coretk.nodeutils import NodeUtils ABOVE_WALLPAPER = ["edge", "linkinfo", "wireless", "antenna", "nodename", "node"] @@ -243,13 +244,13 @@ class CanvasGraph(tk.Canvas): self.context = None else: if self.mode == GraphMode.ANNOTATION: - if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: + if is_draw_shape(self.annotation_type): self.focus_set() x, y = self.canvas_xy(event) if self.shape_drawing: self.shapes[self.selected].shape_complete(x, y) self.shape_drawing = False - elif self.annotation_type == ImageEnum.TEXT: + elif is_shape_text(self.annotation_type): self.text.shape_complete(self.text.cursor_x, self.text.cursor_y) else: self.focus_set() @@ -401,15 +402,14 @@ class CanvasGraph(tk.Canvas): self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) if self.mode == GraphMode.ANNOTATION and selected is None: - if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: - x, y = self.canvas_xy(event) - shape = Shape(self.app, self, x, y) + x, y = self.canvas_xy(event) + if is_draw_shape(self.annotation_type): + shape = Shape(self.app, self, self.annotation_type, x, y) self.selected = shape.id self.shapes[shape.id] = shape self.shape_drawing = True - elif self.annotation_type == ImageEnum.TEXT: - x, y = self.canvas_xy(event) - self.text = Shape(self.app, self, x, y) + elif is_shape_text(self.annotation_type): + self.text = Shape(self.app, self, self.annotation_type, x, y) if self.mode == GraphMode.SELECT: if selected is not None: @@ -446,10 +446,7 @@ class CanvasGraph(tk.Canvas): x1, y1, _, _ = self.coords(self.drawing_edge.id) self.coords(self.drawing_edge.id, x1, y1, x2, y2) if self.mode == GraphMode.ANNOTATION: - if ( - self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE] - and self.shape_drawing - ): + if is_draw_shape(self.annotation_type) and self.shape_drawing: x, y = self.canvas_xy(event) self.shapes[self.selected].shape_motion(x, y) if ( diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 660ef291..7ef9530f 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -1,8 +1,7 @@ -""" -class for shapes -""" +from tkinter.font import Font + from coretk.dialogs.shapemod import ShapeDialog -from coretk.images import ImageEnum +from coretk.graph.shapeutils import ShapeType ABOVE_COMPONENT = ["gridline", "edge", "linkinfo", "antenna", "node", "nodename"] @@ -14,14 +13,13 @@ class AnnotationData: font="Arial", font_size=12, text_color="#000000", - fill_color="#CFCFFF", + fill_color="", border_color="#000000", - border_width=0, + border_width=1, bold=0, italic=0, underline=0, ): - self.text = text self.font = font self.font_size = font_size @@ -35,72 +33,77 @@ class AnnotationData: class Shape: - def __init__( - self, - app, - canvas, - top_x=None, - top_y=None, - coords=None, - data=None, - shape_type=None, - ): + def __init__(self, app, canvas, shape_type, x1, y1, x2=None, y2=None, data=None): self.app = app self.canvas = canvas + self.shape_type = shape_type + self.id = None + self.text_id = None + self.x1 = x1 + self.y1 = y1 + if x2 is None: + x2 = x1 + self.x2 = x2 + if y2 is None: + y2 = y1 + self.y2 = y2 if data is None: - self.x0 = top_x - self.y0 = top_y self.created = False - self.text_id = None self.shape_data = AnnotationData() - canvas.delete(canvas.find_withtag("selectednodes")) - annotation_type = self.canvas.annotation_type - if annotation_type == ImageEnum.OVAL: - self.id = canvas.create_oval( - top_x, top_y, top_x, top_y, tags="shape", dash="-" - ) - elif annotation_type == ImageEnum.RECTANGLE: - self.id = canvas.create_rectangle( - top_x, top_y, top_x, top_y, tags="shape", dash="-" - ) + self.cursor_x = x1 + self.cursor_y = y1 else: - x0, y0, x1, y1 = coords - self.x0 = x0 - self.y0 = y0 self.created = True - if shape_type == "oval": - self.id = self.canvas.create_oval( - x0, - y0, - x1, - y1, - tags="shape", - fill=data.fill_color, - outline=data.border_color, - width=data.border_width, - ) - elif shape_type == "rectangle": - self.id = self.canvas.create_rectangle( - x0, - y0, - x1, - y1, - tags="shape", - fill=data.fill_color, - outline=data.border_color, - width=data.border_width, - ) - _x = (x0 + x1) / 2 - _y = y0 + 1.5 * data.font_size - self.text_id = self.canvas.create_text( - _x, _y, tags="shapetext", text=data.text, fill=data.text_color - ) self.shape_data = data - self.cursor_x = None - self.cursor_y = None + self.cursor_x = None + self.cursor_y = None + self.draw() + + def draw(self): + if self.created: + dash = None + else: + dash = "-" + if self.shape_type == ShapeType.OVAL: + self.id = self.canvas.create_oval( + self.x1, + self.y1, + self.x2, + self.y2, + tags="shape", + dash=dash, + fill=self.shape_data.fill_color, + outline=self.shape_data.border_color, + width=self.shape_data.border_width, + ) + elif self.shape_type == ShapeType.RECTANGLE: + self.id = self.canvas.create_rectangle( + self.x1, + self.y1, + self.x2, + self.y2, + tags="shape", + dash=dash, + fill=self.shape_data.fill_color, + outline=self.shape_data.border_color, + width=self.shape_data.border_width, + ) + + if self.shape_data.text: + x = (self.x1 + self.x2) / 2 + y = self.y1 + 1.5 * self.shape_data.font_size + font = Font(family=self.shape_data.font, size=self.shape_data.font_size) + self.text_id = self.canvas.create_text( + x, + y, + tags="shapetext", + text=self.shape_data.text, + fill=self.shape_data.text_color, + font=font, + ) def shape_motion(self, x1, y1): - self.canvas.coords(self.id, self.x0, self.y0, x1, y1) + self.canvas.coords(self.id, self.x1, self.y1, x1, y1) def shape_complete(self, x, y): for component in ABOVE_COMPONENT: @@ -122,3 +125,17 @@ class Shape: def delete(self): self.canvas.delete(self.id) self.canvas.delete(self.text_id) + + def metadata(self): + coords = self.canvas.coords(self.id) + return { + "type": self.shape_type.value, + "iconcoords": coords, + "label": self.shape_data.text, + "fontfamily": self.shape_data.font, + "fontsize": self.shape_data.font_size, + "labelcolor": self.shape_data.text_color, + "color": self.shape_data.fill_color, + "border": self.shape_data.border_color, + "width": self.shape_data.border_width, + } diff --git a/coretk/coretk/graph/shapeutils.py b/coretk/coretk/graph/shapeutils.py new file mode 100644 index 00000000..a566a713 --- /dev/null +++ b/coretk/coretk/graph/shapeutils.py @@ -0,0 +1,19 @@ +import enum + + +class ShapeType(enum.Enum): + MARKER = "marker" + OVAL = "oval" + RECTANGLE = "rectangle" + TEXT = "text" + + +SHAPES = {ShapeType.OVAL, ShapeType.RECTANGLE} + + +def is_draw_shape(shape_type): + return shape_type in SHAPES + + +def is_shape_text(shape_type): + return shape_type == ShapeType.TEXT diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 5f528628..1f1e734f 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -6,6 +6,7 @@ from tkinter import ttk from coretk.dialogs.customnodes import CustomNodesDialog from coretk.graph.enums import GraphMode +from coretk.graph.shapeutils import ShapeType from coretk.images import ImageEnum, Images from coretk.nodeutils import NodeUtils from coretk.tooltip import Tooltip @@ -291,18 +292,18 @@ class Toolbar(ttk.Frame): self.hide_pickers() self.annotation_picker = ttk.Frame(self.master) nodes = [ - (ImageEnum.MARKER, "marker"), - (ImageEnum.OVAL, "oval"), - (ImageEnum.RECTANGLE, "rectangle"), - (ImageEnum.TEXT, "text"), + (ImageEnum.MARKER, ShapeType.MARKER), + (ImageEnum.OVAL, ShapeType.OVAL), + (ImageEnum.RECTANGLE, ShapeType.RECTANGLE), + (ImageEnum.TEXT, ShapeType.TEXT), ] - for image_enum, tooltip in nodes: + for image_enum, shape_type in nodes: image = icon(image_enum) self.create_picker_button( image, - partial(self.update_annotation, image, image_enum), + partial(self.update_annotation, image, shape_type), self.annotation_picker, - tooltip, + shape_type.value, ) self.design_select(self.annotation_button) self.annotation_button.after( @@ -362,13 +363,13 @@ class Toolbar(ttk.Frame): self.design_frame.tkraise() - def update_annotation(self, image, image_enum): + def update_annotation(self, image, shape_type): logging.info("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 = image_enum + self.app.canvas.annotation_type = shape_type def click_run_button(self): logging.debug("Click on RUN button") From 2824ae09b03515235fdd3d1be3e9537f578d1350 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Dec 2019 14:18:13 -0800 Subject: [PATCH 334/462] updated sample1.xml to draw shapes using new metadata logic --- coretk/coretk/data/xmls/sample1.xml | 1711 +++++++++++++++++++++++++-- 1 file changed, 1642 insertions(+), 69 deletions(-) diff --git a/coretk/coretk/data/xmls/sample1.xml b/coretk/coretk/data/xmls/sample1.xml index 0c1526fb..0d5dd02d 100644 --- a/coretk/coretk/data/xmls/sample1.xml +++ b/coretk/coretk/data/xmls/sample1.xml @@ -1,16 +1,16 @@ - + - - + + - - + + - - + + @@ -18,8 +18,8 @@ - - + + @@ -27,8 +27,8 @@ - - + + @@ -36,8 +36,16 @@ - - + + + + + + + + + + @@ -45,65 +53,57 @@ - - + + - - + + - - + + - - - - - - - - - - + + - - + + - - + + - - + + - - + + @@ -113,71 +113,71 @@ - - + + - - - - - - + + + + + + - - - - - + - + + + + + - + - + - - - - - - - - - - + + + + + + + + + + @@ -199,6 +199,686 @@ + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.3.2/24 + ipv6 address a:3::2/64 +! +interface eth1 + ip address 10.0.5.1/24 + ipv6 address a:5::1/64 +! +router ospf + router-id 10.0.3.2 + network 10.0.3.0/24 area 0 + network 10.0.5.0/24 area 0 +! +router ospf6 + router-id 10.0.3.2 + interface eth0 area 0.0.0.0 + interface eth1 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospfd + + + killall ospfd + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.2.2/24 + ipv6 address a:2::2/64 +! +interface eth1 + ip address 10.0.3.1/24 + ipv6 address a:3::1/64 +! +interface eth2 + ip address 10.0.4.1/24 + ipv6 address a:4::1/64 +! +router ospf + router-id 10.0.2.2 + network 10.0.2.0/24 area 0 + network 10.0.3.0/24 area 0 + network 10.0.4.0/24 area 0 +! +router ospf6 + router-id 10.0.2.2 + interface eth0 area 0.0.0.0 + interface eth1 area 0.0.0.0 + interface eth2 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospfd + + + killall ospfd + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth2.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth2.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth2.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.1.1/24 + ipv6 address a:1::1/64 +! +interface eth1 + ip address 10.0.2.1/24 + ipv6 address a:2::1/64 +! +router ospf + router-id 10.0.1.1 + network 10.0.1.0/24 area 0 + network 10.0.2.0/24 area 0 +! +router ospf6 + router-id 10.0.1.1 + interface eth0 area 0.0.0.0 + interface eth1 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospfd + + + killall ospfd + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.0.6/32 + ipv6 address a::6/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa +! +router ospf6 + router-id 10.0.0.6 + interface eth0 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 + + + /usr/local/etc/quagga @@ -248,11 +928,907 @@ router ospf6 redistribute ospf ! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospfd + + + killall ospfd + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.0.7/32 + ipv6 address a::7/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa +! +router ospf6 + router-id 10.0.0.7 + interface eth0 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.0.8/32 + ipv6 address a::8/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa +! +router ospf6 + router-id 10.0.0.8 + interface eth0 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.0.9/32 + ipv6 address a::9/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa +! +router ospf6 + router-id 10.0.0.9 + interface eth0 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 + + + + + + sh defaultroute.sh + + + #!/bin/sh +# auto-generated by DefaultRoute service (utility.py) +ip route add default via 10.0.1.1 +ip route add default via a:1::1 + + + + + + sh defaultroute.sh + + + #!/bin/sh +# auto-generated by DefaultRoute service (utility.py) +ip route add default via 10.0.1.1 +ip route add default via a:1::1 + + + + + + sh defaultroute.sh + + + #!/bin/sh +# auto-generated by DefaultRoute service (utility.py) +ip route add default via 10.0.1.1 +ip route add default via a:1::1 + + + + + + sh defaultroute.sh + + + #!/bin/sh +# auto-generated by DefaultRoute service (utility.py) +ip route add default via 10.0.1.1 +ip route add default via a:1::1 + + + + + + /etc/ssh + /var/run/sshd + + + sh startsshd.sh + + + killall sshd + + + #!/bin/sh +# auto-generated by SSH service (utility.py) +ssh-keygen -q -t rsa -N "" -f /etc/ssh/ssh_host_rsa_key +chmod 655 /var/run/sshd +# wait until RSA host key has been generated to launch sshd +/usr/sbin/sshd -f /etc/ssh/sshd_config + + # auto-generated by SSH service (utility.py) +Port 22 +Protocol 2 +HostKey /etc/ssh/ssh_host_rsa_key +UsePrivilegeSeparation yes +PidFile /var/run/sshd/sshd.pid + +KeyRegenerationInterval 3600 +ServerKeyBits 768 + +SyslogFacility AUTH +LogLevel INFO + +LoginGraceTime 120 +PermitRootLogin yes +StrictModes yes + +RSAAuthentication yes +PubkeyAuthentication yes + +IgnoreRhosts yes +RhostsRSAAuthentication no +HostbasedAuthentication no + +PermitEmptyPasswords no +ChallengeResponseAuthentication no + +X11Forwarding yes +X11DisplayOffset 10 +PrintMotd no +PrintLastLog yes +TCPKeepAlive yes + +AcceptEnv LANG LC_* +Subsystem sftp /usr/lib/openssh/sftp-server +UsePAM yes +UseDNS no + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.4.2/24 + ipv6 address a:4::2/64 +! +interface eth1 + ip address 10.0.5.2/24 + ipv6 address a:5::2/64 +! +interface eth2 + ip address 10.0.6.1/24 + ipv6 address a:6::1/64 +! +router ospf + router-id 10.0.4.2 + network 10.0.4.0/24 area 0 + network 10.0.5.0/24 area 0 + network 10.0.6.0/24 area 0 +! +router ospf6 + router-id 10.0.4.2 + interface eth0 area 0.0.0.0 + interface eth1 area 0.0.0.0 + interface eth2 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospfd + + + killall ospfd + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth2.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth2.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth2.rp_filter=0 - + @@ -266,11 +1842,8 @@ router ospf6 - - - - - + + From 2add2157043ac5142fc0f4a3880b7e3b48c55d5a Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 6 Dec 2019 15:06:35 -0800 Subject: [PATCH 335/462] fix configuring newly added services --- coretk/coretk/coreclient.py | 14 +++++++++++++- coretk/coretk/dialogs/nodeservice.py | 1 + coretk/coretk/graph/node.py | 3 --- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 91d88487..b7b0a0df 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -432,6 +432,14 @@ class CoreClient: ) process_time = time.perf_counter() - start logging.debug("start session(%s), result: %s", self.session_id, response.result) + # # print(self.client.get_node_service(self.session_id, 1, "DefaultRoute")) + # # print(self.client.set_service_defaults(self.session_id, {"router": ["DefaultRouter"]})) + # print(self.client.set_node_service(self.session_id, 1, "DefaultRoute", ["echo hello"], [], [])) + # + # # print(self.client.get_service_defaults(self.session_id)) + # + # # print(self.client.get_node_service(self.session_id, 1, "DefaultRoute")) + # # print(self.client.get_node_service_file(self.session_id, 1, "DefaultRoute", "defaultroute.sh")) self.app.statusbar.start_session_callback(process_time) # display mobility players @@ -516,8 +524,11 @@ class CoreClient: self.client.set_session_state( self.session_id, core_pb2.SessionState.DEFINITION ) + + # temp + self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION) for node_proto in node_protos: - if node_proto.id not in self.created_nodes: + if node_proto.id not in self.created_nodes or True: response = self.client.add_node(self.session_id, node_proto) logging.debug("create node: %s", response) self.created_nodes.add(node_proto.id) @@ -525,6 +536,7 @@ class CoreClient: if ( tuple([link_proto.node_one_id, link_proto.node_two_id]) not in self.created_links + or True ): response = self.client.add_link( self.session_id, diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 35142497..d950371b 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -87,6 +87,7 @@ class NodeService(Dialog): self.current.listbox.delete(0, tk.END) for name in sorted(self.current_services): self.current.listbox.insert(tk.END, name) + self.canvas_node.core_node.services[:] = self.current_services def click_configure(self): current_selection = self.current.listbox.curselection() diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 83b29da2..27f6a5dc 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -144,9 +144,6 @@ class CanvasNode: def on_leave(self, event): self.tooltip.on_leave(event) - def click(self, event): - print("click") - def double_click(self, event): if self.app.core.is_runtime(): self.canvas.core.launch_terminal(self.core_node.id) From 71df2a3b7f415363821301174bb25e8e14cee3b7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Dec 2019 22:10:27 -0800 Subject: [PATCH 336/462] updated annotation text to be selectable/moveable, save annotation text with other shapes and reload from xml --- coretk/coretk/coreclient.py | 8 +++---- coretk/coretk/dialogs/shapemod.py | 36 +++++++------------------------ coretk/coretk/graph/graph.py | 31 +++++++++++--------------- coretk/coretk/graph/shape.py | 17 +++++++++++++++ daemon/core/xml/corexml.py | 6 +++--- 5 files changed, 45 insertions(+), 53 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 5c9edfde..5b6f0fd9 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -322,11 +322,11 @@ class CoreClient: if shapes_config: shapes_config = json.loads(shapes_config) for shape_config in shapes_config: - logging.info("loading shape: %s", shapes_config) + logging.info("loading shape: %s", shape_config) shape_type = shape_config["type"] try: shape_type = ShapeType(shape_type) - x1, y1, x2, y2 = shape_config["iconcoords"] + coords = shape_config["iconcoords"] data = AnnotationData( shape_config["label"], shape_config["fontfamily"], @@ -337,11 +337,11 @@ class CoreClient: shape_config["width"], ) shape = Shape( - self.app, self.app.canvas, shape_type, x1, y1, x2, y2, data + self.app, self.app.canvas, shape_type, *coords, data=data ) self.app.canvas.shapes[shape.id] = shape except ValueError: - logging.debug("unknown shape: %s", shape_type) + logging.exception("unknown shape: %s", shape_type) for tag in LIFT_ORDER: self.app.canvas.tag_raise(tag) diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index faff5ef8..04a617c8 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -15,12 +15,10 @@ class ShapeDialog(Dialog): def __init__(self, master, app, shape): if is_draw_shape(shape.shape_type): title = "Add Shape" - self.id = shape.id else: title = "Add Text" - self.id = None - self.canvas = app.canvas super().__init__(master, app, title, modal=True) + self.canvas = app.canvas self.fill = None self.border = None self.shape = shape @@ -146,12 +144,8 @@ class ShapeDialog(Dialog): self.border.config(background=color[1], text=color[1]) def cancel(self): - if ( - is_draw_shape(self.shape.shape_type) - and not self.canvas.shapes[self.id].created - ): - self.canvas.delete(self.id) - self.canvas.shapes.pop(self.id) + self.shape.delete() + self.canvas.shapes.pop(self.shape.id) self.destroy() def click_add(self): @@ -209,29 +203,15 @@ class ShapeDialog(Dialog): :return: nothing """ text = self.shape_text.get() - x = self.shape.x1 - y = self.shape.y1 text_font = self.make_font() - if self.shape.text_id is None: - tid = self.canvas.create_text( - x, y, text=text, fill=self.text_color, font=text_font, tags="text" - ) - self.shape.text_id = tid - self.id = tid - self.shape.id = tid - self.canvas.texts[tid] = self.shape - self.shape.created = True + self.canvas.itemconfig( + self.shape.id, text=text, fill=self.text_color, font=text_font + ) self.save_text() - print(self.canvas.texts) - # self.canvas.shapes[self.id].created = True - # else: - # self.canvas.itemconfig( - # self.shape.text_id, text=text, fill=self.text_color, font=f - # ) def add_shape(self): self.canvas.itemconfig( - self.id, + self.shape.id, fill=self.fill_color, dash="", outline=self.border_color, @@ -239,7 +219,7 @@ class ShapeDialog(Dialog): ) shape_text = self.shape_text.get() size = int(self.font_size.get()) - x0, y0, x1, y1 = self.canvas.bbox(self.id) + x0, y0, x1, y1 = self.canvas.bbox(self.shape.id) _y = y0 + 1.5 * size _x = (x0 + x1) / 2 text_font = self.make_font() diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 57a2f6e4..bdcfe98b 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -10,7 +10,7 @@ from coretk.graph.enums import GraphMode, ScaleOption from coretk.graph.linkinfo import LinkInfo, Throughput from coretk.graph.node import CanvasNode from coretk.graph.shape import Shape -from coretk.graph.shapeutils import is_draw_shape, is_shape_text +from coretk.graph.shapeutils import is_draw_shape from coretk.images import Images from coretk.nodeutils import NodeUtils @@ -45,7 +45,6 @@ class CanvasGraph(tk.Canvas): self.nodes = {} self.edges = {} self.shapes = {} - self.texts = {} self.wireless_edges = {} self.drawing_edge = None self.grid = None @@ -244,14 +243,12 @@ class CanvasGraph(tk.Canvas): self.context = None else: if self.mode == GraphMode.ANNOTATION: - if is_draw_shape(self.annotation_type): - self.focus_set() - x, y = self.canvas_xy(event) - if self.shape_drawing: - self.shapes[self.selected].shape_complete(x, y) - self.shape_drawing = False - elif is_shape_text(self.annotation_type): - self.text.shape_complete(self.text.cursor_x, self.text.cursor_y) + self.focus_set() + x, y = self.canvas_xy(event) + if self.shape_drawing: + shape = self.shapes[self.selected] + shape.shape_complete(x, y) + self.shape_drawing = False else: self.focus_set() self.selected = self.get_selected(event) @@ -404,13 +401,10 @@ class CanvasGraph(tk.Canvas): if self.mode == GraphMode.ANNOTATION and selected is None: x, y = self.canvas_xy(event) - if is_draw_shape(self.annotation_type): - shape = Shape(self.app, self, self.annotation_type, x, y) - self.selected = shape.id - self.shapes[shape.id] = shape - self.shape_drawing = True - elif is_shape_text(self.annotation_type): - self.text = Shape(self.app, self, self.annotation_type, x, y) + shape = Shape(self.app, self, self.annotation_type, x, y) + self.selected = shape.id + self.shape_drawing = True + self.shapes[shape.id] = shape if self.mode == GraphMode.SELECT: if selected is not None: @@ -449,7 +443,8 @@ class CanvasGraph(tk.Canvas): if self.mode == GraphMode.ANNOTATION: if is_draw_shape(self.annotation_type) and self.shape_drawing: x, y = self.canvas_xy(event) - self.shapes[self.selected].shape_motion(x, y) + shape = self.shapes[self.selected] + shape.shape_motion(x, y) if ( self.mode == GraphMode.SELECT and self.selected is not None diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 7ef9530f..8188b8c2 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -1,3 +1,4 @@ +import logging from tkinter.font import Font from coretk.dialogs.shapemod import ShapeDialog @@ -76,6 +77,7 @@ class Shape: outline=self.shape_data.border_color, width=self.shape_data.border_width, ) + self.draw_shape_text() elif self.shape_type == ShapeType.RECTANGLE: self.id = self.canvas.create_rectangle( self.x1, @@ -88,7 +90,22 @@ class Shape: outline=self.shape_data.border_color, width=self.shape_data.border_width, ) + self.draw_shape_text() + elif self.shape_type == ShapeType.TEXT: + font = Font(family=self.shape_data.font, size=self.shape_data.font_size) + self.id = self.canvas.create_text( + self.x1, + self.y1, + tags="shapetext", + text=self.shape_data.text, + fill=self.shape_data.text_color, + font=font, + ) + else: + logging.error("unknown shape type: %s", self.shape_type) + self.created = True + def draw_shape_text(self): if self.shape_data.text: x = (self.x1 + self.x2) / 2 y = self.y1 + 1.5 * self.shape_data.font_size diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index dcca6b80..96a5a6a5 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -123,9 +123,9 @@ class NodeElement: if x is not None and y is not None: lat, lon, alt = self.session.location.getgeo(x, y, z) position = etree.SubElement(self.element, "position") - add_attribute(position, "x", x) - add_attribute(position, "y", y) - add_attribute(position, "z", z) + add_attribute(position, "x", int(x)) + add_attribute(position, "y", int(y)) + add_attribute(position, "z", int(z)) add_attribute(position, "lat", lat) add_attribute(position, "lon", lon) add_attribute(position, "alt", alt) From 0308a4c8d76aaea803faa55a0b2b82d01e95a87c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Dec 2019 22:33:21 -0800 Subject: [PATCH 337/462] fixed temp issue for dealing with xml and node positions as floats, updated shape metadata to save bold/italic/underline options and read them back from xml --- coretk/coretk/coreclient.py | 3 + coretk/coretk/data/xmls/sample1.xml | 384 ++++++++++++++-------------- coretk/coretk/dialogs/shapemod.py | 12 +- coretk/coretk/graph/shape.py | 24 +- daemon/core/xml/corexml.py | 12 +- 5 files changed, 228 insertions(+), 207 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 5b6f0fd9..2e009425 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -335,6 +335,9 @@ class CoreClient: shape_config["color"], shape_config["border"], shape_config["width"], + shape_config["bold"], + shape_config["italic"], + shape_config["underline"], ) shape = Shape( self.app, self.app.canvas, shape_type, *coords, data=data diff --git a/coretk/coretk/data/xmls/sample1.xml b/coretk/coretk/data/xmls/sample1.xml index 0d5dd02d..8c61b7de 100644 --- a/coretk/coretk/data/xmls/sample1.xml +++ b/coretk/coretk/data/xmls/sample1.xml @@ -1,5 +1,5 @@ - + @@ -37,7 +37,7 @@ - + @@ -53,24 +53,30 @@ - - + + - - + + + + + + + + - + @@ -83,12 +89,6 @@ - - - - - - @@ -113,26 +113,30 @@ + + + + - - - - - - - - - - + + + + + + + + + + @@ -145,10 +149,6 @@ - - - - @@ -1063,6 +1063,164 @@ bootquagga /sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 /sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 /sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.0.8/32 + ipv6 address a::8/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa +! +router ospf6 + router-id 10.0.0.8 + interface eth0 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 @@ -1224,161 +1382,15 @@ bootquagga - - - /usr/local/etc/quagga - /var/run/quagga - + - sh quaggaboot.sh zebra - - - pidof zebra - - - killall zebra - - - interface eth0 - ip address 10.0.0.8/32 - ipv6 address a::8/128 - ipv6 ospf6 instance-id 65 - ipv6 ospf6 hello-interval 2 - ipv6 ospf6 dead-interval 6 - ipv6 ospf6 retransmit-interval 5 - ipv6 ospf6 network manet-designated-router - ipv6 ospf6 diffhellos - ipv6 ospf6 adjacencyconnectivity uniconnected - ipv6 ospf6 lsafullness mincostlsa -! -router ospf6 - router-id 10.0.0.8 - interface eth0 area 0.0.0.0 -! - - #!/bin/sh -# auto-generated by zebra service (quagga.py) -QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf -QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" -QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" -QUAGGA_STATE_DIR=/var/run/quagga - -searchforprog() -{ - prog=$1 - searchpath=$@ - ret= - for p in $searchpath; do - if [ -x $p/$prog ]; then - ret=$p - break - fi - done - echo $ret -} - -confcheck() -{ - CONF_DIR=`dirname $QUAGGA_CONF` - # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then - ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf - fi - # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then - ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf - fi -} - -bootdaemon() -{ - QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) - if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then - echo "ERROR: Quagga's '$1' daemon not found in search path:" - echo " $QUAGGA_SBIN_SEARCH" - return 1 - fi - - flags="" - - if [ "$1" = "xpimd" ] && \ - grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then - flags="$flags -6" - fi - - $QUAGGA_SBIN_DIR/$1 $flags -d - if [ "$?" != "0" ]; then - echo "ERROR: Quagga's '$1' daemon failed to start!:" - return 1 - fi -} - -bootquagga() -{ - QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) - if [ "z$QUAGGA_BIN_DIR" = "z" ]; then - echo "ERROR: Quagga's 'vtysh' program not found in search path:" - echo " $QUAGGA_BIN_SEARCH" - return 1 - fi - - # fix /var/run/quagga permissions - id -u quagga 2>/dev/null >/dev/null - if [ "$?" = "0" ]; then - chown quagga $QUAGGA_STATE_DIR - fi - - bootdaemon "zebra" - for r in rip ripng ospf6 ospf bgp babel; do - if grep -q "^router \<${r}\>" $QUAGGA_CONF; then - bootdaemon "${r}d" - fi - done - - if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then - bootdaemon "xpimd" - fi - - $QUAGGA_BIN_DIR/vtysh -b -} - -if [ "$1" != "zebra" ]; then - echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" - exit 1 -fi -confcheck -bootquagga - - service integrated-vtysh-config - - - - - - pidof ospf6d - - - killall ospf6d - - - - - sh ipforward.sh + sh defaultroute.sh - #!/bin/sh -# auto-generated by IPForward service (utility.py) -/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 -/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 -/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 -/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 -/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 + #!/bin/sh +# auto-generated by DefaultRoute service (utility.py) +ip route add default via 10.0.1.1 +ip route add default via a:1::1 @@ -1549,18 +1561,6 @@ bootquagga # auto-generated by DefaultRoute service (utility.py) ip route add default via 10.0.1.1 ip route add default via a:1::1 - - - - - - sh defaultroute.sh - - - #!/bin/sh -# auto-generated by DefaultRoute service (utility.py) -ip route add default via 10.0.1.1 -ip route add default via a:1::1 @@ -1842,8 +1842,8 @@ bootquagga - + diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index 04a617c8..0410a8ae 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -33,9 +33,9 @@ class ShapeDialog(Dialog): self.fill_color = fill_color self.border_color = data.border_color self.border_width = tk.IntVar(value=0) - self.bold = tk.IntVar(value=data.bold) - self.italic = tk.IntVar(value=data.italic) - self.underline = tk.IntVar(value=data.underline) + self.bold = tk.BooleanVar(value=data.bold) + self.italic = tk.BooleanVar(value=data.italic) + self.underline = tk.BooleanVar(value=data.underline) self.top.columnconfigure(0, weight=1) self.draw() @@ -162,11 +162,11 @@ class ShapeDialog(Dialog): """ size = int(self.font_size.get()) text_font = [self.font.get(), size] - if self.bold.get() == 1: + if self.bold.get(): text_font.append("bold") - if self.italic.get() == 1: + if self.italic.get(): text_font.append("italic") - if self.underline.get() == 1: + if self.underline.get(): text_font.append("underline") return text_font diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 8188b8c2..22c676db 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -1,5 +1,4 @@ import logging -from tkinter.font import Font from coretk.dialogs.shapemod import ShapeDialog from coretk.graph.shapeutils import ShapeType @@ -17,9 +16,9 @@ class AnnotationData: fill_color="", border_color="#000000", border_width=1, - bold=0, - italic=0, - underline=0, + bold=False, + italic=False, + underline=False, ): self.text = text self.font = font @@ -92,7 +91,7 @@ class Shape: ) self.draw_shape_text() elif self.shape_type == ShapeType.TEXT: - font = Font(family=self.shape_data.font, size=self.shape_data.font_size) + font = self.get_font() self.id = self.canvas.create_text( self.x1, self.y1, @@ -105,11 +104,21 @@ class Shape: logging.error("unknown shape type: %s", self.shape_type) self.created = True + def get_font(self): + font = [self.shape_data.font, self.shape_data.font_size] + if self.shape_data.bold: + font.append("bold") + if self.shape_data.italic: + font.append("italic") + if self.shape_data.underline: + font.append("underline") + return font + def draw_shape_text(self): if self.shape_data.text: x = (self.x1 + self.x2) / 2 y = self.y1 + 1.5 * self.shape_data.font_size - font = Font(family=self.shape_data.font, size=self.shape_data.font_size) + font = self.get_font() self.text_id = self.canvas.create_text( x, y, @@ -155,4 +164,7 @@ class Shape: "color": self.shape_data.fill_color, "border": self.shape_data.border_color, "width": self.shape_data.border_width, + "bold": self.shape_data.bold, + "italic": self.shape_data.italic, + "underline": self.shape_data.underline, } diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 96a5a6a5..9ba63395 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -117,15 +117,21 @@ class NodeElement: def add_position(self): x = self.node.position.x + if x is not None: + x = int(x) y = self.node.position.y + if y is not None: + y = int(y) z = self.node.position.z + if z is not None: + z = int(z) lat, lon, alt = None, None, None if x is not None and y is not None: lat, lon, alt = self.session.location.getgeo(x, y, z) position = etree.SubElement(self.element, "position") - add_attribute(position, "x", int(x)) - add_attribute(position, "y", int(y)) - add_attribute(position, "z", int(z)) + add_attribute(position, "x", x) + add_attribute(position, "y", y) + add_attribute(position, "z", z) add_attribute(position, "lat", lat) add_attribute(position, "lon", lon) add_attribute(position, "alt", alt) From 456e33187041ad2590209f16ebfc63001bf34a97 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 9 Dec 2019 08:53:54 -0800 Subject: [PATCH 338/462] loading xml set canvas state to select mode and display that on toolbar --- coretk/coretk/coreclient.py | 31 +++++++------------------------ coretk/coretk/graph/graph.py | 1 + coretk/coretk/menuaction.py | 7 +++++++ coretk/coretk/menubar.py | 2 +- 4 files changed, 16 insertions(+), 25 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index b7b0a0df..64772b98 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -94,6 +94,7 @@ class CoreClient: self.service_configs = {} self.file_configs = {} self.mobility_players = {} + self.throughput = False def reset(self): # helpers @@ -186,22 +187,11 @@ class CoreClient: canvas_node.move(x, y, update=False) def handle_throughputs(self, event): - # interface_throughputs = event.interface_throughputs - # # print(interface_throughputs) - # # return - # # for i in interface_throughputs: - # # print("") - # # # return - # print(event) - # throughputs_belong_to_session = [] - # print(self.node_ids) - # for throughput in interface_throughputs: - # if throughput.node_id in self.node_ids: - # throughputs_belong_to_session.append(throughput) - # print(throughputs_belong_to_session) - self.app.canvas.throughput_draw.process_grpc_throughput_event( - event.interface_throughputs - ) + # print(event.interface_throughputs) + if self.throughput: + self.app.canvas.throughput_draw.process_grpc_throughput_event( + event.interface_throughputs + ) def join_session(self, session_id, query_location=True): # update session and title @@ -296,6 +286,7 @@ class CoreClient: self.app.toolbar.runtime_frame.tkraise() else: self.app.toolbar.design_frame.tkraise() + self.app.toolbar.select_button.invoke() self.app.statusbar.progress_bar.stop() def is_runtime(self): @@ -432,14 +423,6 @@ class CoreClient: ) process_time = time.perf_counter() - start logging.debug("start session(%s), result: %s", self.session_id, response.result) - # # print(self.client.get_node_service(self.session_id, 1, "DefaultRoute")) - # # print(self.client.set_service_defaults(self.session_id, {"router": ["DefaultRouter"]})) - # print(self.client.set_node_service(self.session_id, 1, "DefaultRoute", ["echo hello"], [], [])) - # - # # print(self.client.get_service_defaults(self.session_id)) - # - # # print(self.client.get_node_service(self.session_id, 1, "DefaultRoute")) - # # print(self.client.get_node_service_file(self.session_id, 1, "DefaultRoute", "defaultroute.sh")) self.app.statusbar.start_session_callback(process_time) # display mobility players diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index f4303713..0b9a20d2 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -77,6 +77,7 @@ class CanvasGraph(tk.Canvas): # set the private variables to default value self.mode = GraphMode.SELECT + self.annotation_type = None self.node_draw = None self.selected = None self.nodes.clear() diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index c4cc27bc..08298727 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -156,3 +156,10 @@ class MenuAction: def show_about(self): dialog = AboutDialog(self.app, self.app) dialog.show() + + def throughput(self): + throughput = self.app.core.throughput + if throughput: + self.app.core.throughput = False + else: + self.app.core.throughput = True diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index 288a517f..ed5cb2ca 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -422,7 +422,7 @@ class Menubar(tk.Menu): menu = tk.Menu(self) self.create_observer_widgets_menu(menu) self.create_adjacency_menu(menu) - menu.add_command(label="Throughput", state=tk.DISABLED) + menu.add_checkbutton(label="Throughput", command=self.menuaction.throughput) menu.add_separator() menu.add_command(label="Configure Adjacency...", state=tk.DISABLED) menu.add_command(label="Configure Throughput...", state=tk.DISABLED) From 27131ef36785c9d792467d320297c5cda28a5981 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 9 Dec 2019 10:07:21 -0800 Subject: [PATCH 339/462] start on input validation --- coretk/coretk/validation.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 coretk/coretk/validation.py diff --git a/coretk/coretk/validation.py b/coretk/coretk/validation.py new file mode 100644 index 00000000..00563d68 --- /dev/null +++ b/coretk/coretk/validation.py @@ -0,0 +1,3 @@ +""" +input validation +""" From 5003e2356c605f9d0890972a5de5bf73517df731 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 12:07:42 -0800 Subject: [PATCH 340/462] small cleanup to canvas resize/redraw logic and updates to support saving/drawing gridlines and canvas dimensions --- coretk/coretk/coreclient.py | 21 +++++- coretk/coretk/dialogs/canvassizeandscale.py | 2 +- coretk/coretk/graph/graph.py | 72 +++++++++++++-------- 3 files changed, 64 insertions(+), 31 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 2e009425..43aa88ed 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -310,12 +310,26 @@ class CoreClient: logging.info("canvas metadata: %s", canvas_config) if canvas_config: canvas_config = json.loads(canvas_config) - wallpaper_style = canvas_config["wallpaper-style"] + + gridlines = canvas_config.get("gridlines", True) + self.app.canvas.show_grid.set(gridlines) + + fit_image = canvas_config.get("fit_image", False) + self.app.canvas.adjust_to_dim.set(fit_image) + + wallpaper_style = canvas_config.get("wallpaper-style", 1) self.app.canvas.scale_option.set(wallpaper_style) - wallpaper = canvas_config["wallpaper"] + + width = self.app.guiconfig["preferences"]["width"] + height = self.app.guiconfig["preferences"]["height"] + width, height = canvas_config.get("dimensions", [width, height]) + self.app.canvas.redraw_canvas(width, height) + + wallpaper = canvas_config.get("wallpaper") if wallpaper: wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) self.app.canvas.set_wallpaper(wallpaper) + self.app.canvas.update_grid() # load saved shapes shapes_config = config.get("shapes") @@ -469,6 +483,9 @@ class CoreClient: canvas_config = { "wallpaper": Path(self.app.canvas.wallpaper_file).name, "wallpaper-style": self.app.canvas.scale_option.get(), + "gridlines": self.app.canvas.show_grid.get(), + "fit_image": self.app.canvas.adjust_to_dim.get(), + "dimensions": self.app.canvas.width_and_height(), } canvas_config = json.dumps(canvas_config) diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index fe4c3b62..8d6db86f 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -168,7 +168,7 @@ class SizeAndScaleDialog(Dialog): def click_apply(self): width, height = self.pixel_width.get(), self.pixel_height.get() - self.canvas.redraw_grid(width, height) + self.canvas.redraw_canvas(width, height) if self.canvas.wallpaper: self.canvas.redraw() location = self.app.core.location diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index bdcfe98b..720ce560 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -14,7 +14,17 @@ from coretk.graph.shapeutils import is_draw_shape from coretk.images import Images from coretk.nodeutils import NodeUtils -ABOVE_WALLPAPER = ["edge", "linkinfo", "wireless", "antenna", "nodename", "node"] +ABOVE_WALLPAPER = [ + "gridline", + "shape", + "shapetext", + "edge", + "linkinfo", + "wireless", + "antenna", + "nodename", + "node", +] CANVAS_COMPONENT_TAGS = [ "edge", "node", @@ -49,10 +59,11 @@ class CanvasGraph(tk.Canvas): self.drawing_edge = None self.grid = None self.setup_bindings() - self.draw_grid(width, height) self.core = core self.throughput_draw = Throughput(self, core) self.shape_drawing = False + self.default_width = width + self.default_height = height # background related self.wallpaper_id = None @@ -63,6 +74,22 @@ class CanvasGraph(tk.Canvas): self.show_grid = tk.BooleanVar(value=True) self.adjust_to_dim = tk.BooleanVar(value=False) + # draw base canvas + self.draw_canvas() + self.draw_grid() + + def draw_canvas(self): + self.grid = self.create_rectangle( + 0, + 0, + self.default_width, + self.default_height, + outline="#000000", + fill="#ffffff", + width=1, + tags="rectangle", + ) + def reset_and_redraw(self, session): """ Reset the private variables CanvasGraph object, redraw nodes given the new grpc @@ -100,7 +127,7 @@ class CanvasGraph(tk.Canvas): self.bind("", self.ctrl_click) self.bind("", self.double_click) - def draw_grid(self, width=1000, height=800): + def draw_grid(self): """ Create grid @@ -109,16 +136,9 @@ class CanvasGraph(tk.Canvas): :return: nothing """ - self.grid = self.create_rectangle( - 0, - 0, - width, - height, - outline="#000000", - fill="#ffffff", - width=1, - tags="rectangle", - ) + width, height = self.width_and_height() + width = int(width) + height = int(height) for i in range(0, width, 27): self.create_line(i, 0, i, height, dash=(2, 4), tags="gridline") for i in range(0, height, 27): @@ -584,36 +604,31 @@ class CanvasGraph(tk.Canvas): img_w = image_tk.width() img_h = image_tk.height() self.delete(self.wallpaper_id) - self.delete("rectangle") - self.delete("gridline") - self.draw_grid(img_w, img_h) + self.redraw_canvas(img_w, img_h) self.wallpaper_id = self.create_image((img_w / 2, img_h / 2), image=image_tk) self.wallpaper_drawn = image_tk - def redraw_grid(self, width, height): + def redraw_canvas(self, width, height): """ redraw grid with new dimension :return: nothing """ + # resize canvas and scrollregion self.config(scrollregion=(0, 0, width + 200, height + 200)) + self.coords(self.grid, 0, 0, width, height) - # delete previous grid - self.delete("rectangle") + # redraw gridlines to new canvas size self.delete("gridline") - - # redraw - self.draw_grid(width=width, height=height) - - # hide/show grid + self.draw_grid() self.update_grid() def redraw(self): if self.adjust_to_dim.get(): self.resize_to_wallpaper() else: - print(self.scale_option.get()) option = ScaleOption(self.scale_option.get()) + logging.info("canvas scale option: %s", option) if option == ScaleOption.UPPER_LEFT: self.wallpaper_upper_left() elif option == ScaleOption.CENTERED: @@ -623,11 +638,14 @@ class CanvasGraph(tk.Canvas): elif option == ScaleOption.TILED: logging.warning("tiled background not implemented yet") + # raise items above wallpaper + for component in ABOVE_WALLPAPER: + self.tag_raise(component) + def update_grid(self): logging.info("updating grid show: %s", self.show_grid.get()) if self.show_grid.get(): self.itemconfig("gridline", state=tk.NORMAL) - self.tag_raise("gridline") else: self.itemconfig("gridline", state=tk.HIDDEN) @@ -638,8 +656,6 @@ class CanvasGraph(tk.Canvas): self.wallpaper = img self.wallpaper_file = filename self.redraw() - for component in ABOVE_WALLPAPER: - self.tag_raise(component) else: if self.wallpaper_id is not None: self.delete(self.wallpaper_id) From 4ca9ab910e40c46a724f654944f3c549e8e8b253 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 13:05:07 -0800 Subject: [PATCH 341/462] added spinbox missing from 3.6, added spinbox theme, updated config gen to display a file picker for labels with file in it --- coretk/coretk/dialogs/dialog.py | 1 + coretk/coretk/themes.py | 8 ++++++++ coretk/coretk/widgets.py | 30 +++++++++++++++++++++++++++--- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index 29362960..d814a47c 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -9,6 +9,7 @@ DIALOG_PAD = 5 class Dialog(tk.Toplevel): def __init__(self, master, app, title, modal=False): super().__init__(master) + self.geometry("800x600") self.withdraw() self.app = app self.modal = modal diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index ad91276a..b80caf87 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -88,6 +88,14 @@ def load(style): }, "map": {"fieldbackground": [("disabled", Colors.frame)]}, }, + "TSpinbox": { + "configure": { + "fieldbackground": Colors.white, + "foreground": Colors.black, + "padding": (2, 0), + }, + "map": {"fieldbackground": [("disabled", Colors.frame)]}, + }, "TCombobox": { "configure": { "fieldbackground": Colors.white, diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 760846a3..486370e0 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -1,7 +1,7 @@ import logging import tkinter as tk from functools import partial -from tkinter import font, ttk +from tkinter import filedialog, font, ttk from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 @@ -19,6 +19,12 @@ INT_TYPES = { PAD = 5 +def file_button_click(value): + file_path = filedialog.askopenfilename(title="Select File") + if file_path: + value.set(file_path) + + class FrameScroll(ttk.LabelFrame): def __init__(self, master, app, _cls=ttk.Frame, **kw): super().__init__(master, **kw) @@ -100,8 +106,18 @@ class ConfigFrame(FrameScroll): combobox.grid(row=index, column=1, sticky="ew", pady=pady) elif option.type == core_pb2.ConfigOptionType.STRING: value.set(option.value) - entry = ttk.Entry(frame, textvariable=value) - entry.grid(row=index, column=1, sticky="ew", pady=pady) + if "file" in option.label: + file_frame = ttk.Frame(frame) + file_frame.grid(row=index, column=1, sticky="ew", pady=pady) + file_frame.columnconfigure(0, weight=1) + entry = ttk.Entry(file_frame, textvariable=value) + entry.grid(row=0, column=0, sticky="ew", padx=padx) + func = partial(file_button_click, value) + button = ttk.Button(file_frame, text="...", command=func) + button.grid(row=0, column=1) + else: + entry = ttk.Entry(frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pady) elif option.type in INT_TYPES: value.set(option.value) entry = ttk.Entry(frame, textvariable=value) @@ -179,3 +195,11 @@ class CodeText(ScrolledText): relief=tk.FLAT, **kwargs ) + + +class Spinbox(ttk.Entry): + def __init__(self, master=None, **kwargs): + super().__init__(master, "ttk::spinbox", **kwargs) + + def set(self, value): + self.tk.call(self._w, "set", value) From 2a29cd1fe59b814db5f3e9c9aa861d9ef8df27b6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 13:09:01 -0800 Subject: [PATCH 342/462] after joining a session default to select mode --- coretk/coretk/coreclient.py | 2 ++ coretk/coretk/menuaction.py | 1 - coretk/coretk/toolbar.py | 2 -- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 43aa88ed..7e86c87c 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -295,11 +295,13 @@ class CoreClient: response = self.client.get_session_metadata(self.session_id) self.parse_metadata(response.config) + # update ui to represent current state if self.is_runtime(): self.app.toolbar.runtime_frame.tkraise() else: self.app.toolbar.design_frame.tkraise() self.app.statusbar.progress_bar.stop() + self.app.toolbar.click_selection() def is_runtime(self): return self.state == core_pb2.SessionState.RUNTIME diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index c4cc27bc..f7f9c579 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -109,7 +109,6 @@ class MenuAction: self.app.statusbar.progress_bar.start(5) thread = threading.Thread(target=self.app.core.open_xml, args=([file_path])) thread.start() - # self.app.core.open_xml(file_path) def gui_preferences(self): dialog = PreferencesDialog(self.app, self.app) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 1f1e734f..c9efa74c 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -40,8 +40,6 @@ class Toolbar(ttk.Frame): self.network_button = None self.annotation_button = None - # runtime buttons - # frames self.design_frame = None self.runtime_frame = None From afdacf0c94688612bea5a4018903a8b3145bb814 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 13:19:45 -0800 Subject: [PATCH 343/462] set to selection mode when starting/stopping a session as well as joining for runtime/design states --- coretk/coretk/coreclient.py | 3 ++- coretk/coretk/toolbar.py | 24 ++++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 7e86c87c..6edc22cf 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -298,10 +298,11 @@ class CoreClient: # update ui to represent current state if self.is_runtime(): self.app.toolbar.runtime_frame.tkraise() + self.app.toolbar.click_runtime_selection() else: self.app.toolbar.design_frame.tkraise() + self.app.toolbar.click_selection() self.app.statusbar.progress_bar.stop() - self.app.toolbar.click_selection() def is_runtime(self): return self.state == core_pb2.SessionState.RUNTIME diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index c9efa74c..55b2356e 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -40,6 +40,9 @@ class Toolbar(ttk.Frame): self.network_button = None self.annotation_button = None + # runtime buttons + self.runtime_select_button = None + # frames self.design_frame = None self.runtime_frame = None @@ -89,6 +92,11 @@ class Toolbar(ttk.Frame): self.annotation_button.state(["!pressed"]) button.state(["pressed"]) + def runtime_select(self, button): + logging.info("selecting runtime button: %s", button) + self.runtime_select_button.state(["!pressed"]) + button.state(["pressed"]) + def draw_runtime_frame(self): self.runtime_frame = ttk.Frame(self) self.runtime_frame.grid(row=0, column=0, sticky="nsew") @@ -100,10 +108,10 @@ class Toolbar(ttk.Frame): self.click_stop, "stop the session", ) - self.create_button( + self.runtime_select_button = self.create_button( self.runtime_frame, icon(ImageEnum.SELECT), - self.click_selection, + self.click_runtime_selection, "selection tool", ) # self.create_observe_button() @@ -188,6 +196,11 @@ class Toolbar(ttk.Frame): self.design_select(self.select_button) self.app.canvas.mode = GraphMode.SELECT + def click_runtime_selection(self): + logging.debug("clicked selection tool") + self.runtime_select(self.runtime_select_button) + self.app.canvas.mode = GraphMode.SELECT + def click_start(self): """ Start session handler redraw buttons, send node and link messages to grpc @@ -200,6 +213,7 @@ class Toolbar(ttk.Frame): thread = threading.Thread(target=self.app.core.start_session) thread.start() self.runtime_frame.tkraise() + self.click_runtime_selection() def click_link(self): logging.debug("Click LINK button") @@ -355,11 +369,9 @@ class Toolbar(ttk.Frame): self.app.statusbar.progress_bar.start(5) thread = threading.Thread(target=self.app.core.stop_session) thread.start() - for cid in self.app.canvas.find_withtag("wireless"): - self.app.canvas.itemconfig(cid, state="hidden") - # self.app.canvas.delete("wireless") - + self.app.canvas.delete("wireless") self.design_frame.tkraise() + self.click_selection() def update_annotation(self, image, shape_type): logging.info("clicked annotation: ") From b04e61cceeb159598e5dbe00414c1d2da5df4802 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 14:13:21 -0800 Subject: [PATCH 344/462] added common tags file for canvas created items, to reduce duplicate strings --- coretk/coretk/coreclient.py | 16 ++-------- coretk/coretk/dialogs/shapemod.py | 3 +- coretk/coretk/graph/edges.py | 5 +-- coretk/coretk/graph/graph.py | 51 +++++++++---------------------- coretk/coretk/graph/linkinfo.py | 15 +++++++-- coretk/coretk/graph/node.py | 8 ++--- coretk/coretk/graph/shape.py | 13 ++++---- coretk/coretk/graph/tags.py | 35 +++++++++++++++++++++ coretk/coretk/toolbar.py | 3 +- 9 files changed, 81 insertions(+), 68 deletions(-) create mode 100644 coretk/coretk/graph/tags.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 6edc22cf..44e4ee99 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -11,24 +11,12 @@ from core.api.grpc import client, core_pb2 from coretk import appconfig from coretk.dialogs.mobilityplayer import MobilityPlayer from coretk.dialogs.sessions import SessionsDialog +from coretk.graph import tags from coretk.graph.shape import AnnotationData, Shape from coretk.graph.shapeutils import ShapeType from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils -LIFT_ORDER = [ - "wallpaper", - "shape", - "gridline", - "shapetext", - "text", - "edge", - "antenna", - "nodename", - "linkinfo", - "node", -] - OBSERVERS = { "processes": "ps", "ifconfig": "ifconfig", @@ -363,7 +351,7 @@ class CoreClient: except ValueError: logging.exception("unknown shape: %s", shape_type) - for tag in LIFT_ORDER: + for tag in tags.ABOVE_WALLPAPER_TAGS: self.app.canvas.tag_raise(tag) def create_new_session(self): diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index 0410a8ae..e0085a8c 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -5,6 +5,7 @@ import tkinter as tk from tkinter import colorchooser, font, ttk from coretk.dialogs.dialog import Dialog +from coretk.graph import tags from coretk.graph.shapeutils import is_draw_shape, is_shape_text FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] @@ -230,7 +231,7 @@ class ShapeDialog(Dialog): text=shape_text, fill=self.text_color, font=text_font, - tags="shapetext", + tags=tags.SHAPE_TEXT, ) self.shape.created = True else: diff --git a/coretk/coretk/graph/edges.py b/coretk/coretk/graph/edges.py index 27d1e3ba..94d847bc 100644 --- a/coretk/coretk/graph/edges.py +++ b/coretk/coretk/graph/edges.py @@ -1,5 +1,6 @@ import tkinter as tk +from coretk.graph import tags from coretk.nodeutils import NodeUtils @@ -10,7 +11,7 @@ class CanvasWirelessEdge: self.dst = dst self.canvas = canvas self.id = self.canvas.create_line( - *position, tags="wireless", width=1.5, fill="#009933" + *position, tags=tags.WIRELESS_EDGE, width=1.5, fill="#009933" ) def delete(self): @@ -40,7 +41,7 @@ class CanvasEdge: self.dst_interface = None self.canvas = canvas self.id = self.canvas.create_line( - x1, y1, x2, y2, tags="edge", width=self.width, fill="#ff0000" + x1, y1, x2, y2, tags=tags.EDGE, width=self.width, fill="#ff0000" ) self.token = None self.link_info = None diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 720ce560..c2c0935d 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -5,6 +5,7 @@ from PIL import Image, ImageTk from core.api.grpc import core_pb2 from coretk.dialogs.shapemod import ShapeDialog +from coretk.graph import tags from coretk.graph.edges import CanvasEdge, CanvasWirelessEdge from coretk.graph.enums import GraphMode, ScaleOption from coretk.graph.linkinfo import LinkInfo, Throughput @@ -14,30 +15,6 @@ from coretk.graph.shapeutils import is_draw_shape from coretk.images import Images from coretk.nodeutils import NodeUtils -ABOVE_WALLPAPER = [ - "gridline", - "shape", - "shapetext", - "edge", - "linkinfo", - "wireless", - "antenna", - "nodename", - "node", -] -CANVAS_COMPONENT_TAGS = [ - "edge", - "node", - "nodename", - "wallpaper", - "linkinfo", - "antenna", - "wireless", - "selectednodes", - "shape", - "shapetext", -] - class CanvasGraph(tk.Canvas): def __init__(self, master, core, width, height, cnf=None, **kwargs): @@ -99,7 +76,7 @@ class CanvasGraph(tk.Canvas): :return: nothing """ # delete any existing drawn items - for tag in CANVAS_COMPONENT_TAGS: + for tag in tags.COMPONENT_TAGS: self.delete(tag) # set the private variables to default value @@ -140,10 +117,10 @@ class CanvasGraph(tk.Canvas): width = int(width) height = int(height) for i in range(0, width, 27): - self.create_line(i, 0, i, height, dash=(2, 4), tags="gridline") + self.create_line(i, 0, i, height, dash=(2, 4), tags=tags.GRIDLINE) for i in range(0, height, 27): - self.create_line(0, i, width, i, dash=(2, 4), tags="gridline") - self.tag_lower("gridline") + self.create_line(0, i, width, i, dash=(2, 4), tags=tags.GRIDLINE) + self.tag_lower(tags.GRIDLINE) self.tag_lower(self.grid) def add_wireless_edge(self, src, dst): @@ -214,7 +191,7 @@ class CanvasGraph(tk.Canvas): canvas_node_two.interfaces.append(link.interface_two) # raise the nodes so they on top of the links - self.tag_raise("node") + self.tag_raise(tags.NODE) def canvas_xy(self, event): """ @@ -336,7 +313,7 @@ class CanvasGraph(tk.Canvas): (x0 - 6, y0 - 6, x1 + 6, y1 + 6), activedash=True, dash="-", - tags="selectednodes", + tags=tags.SELECTION, ) self.selection[object_id] = selection_id else: @@ -553,7 +530,7 @@ class CanvasGraph(tk.Canvas): self.delete(self.wallpaper_id) # place left corner of image to the left corner of the canvas self.wallpaper_id = self.create_image( - (cropx / 2, cropy / 2), image=cropped_tk, tags="wallpaper" + (cropx / 2, cropy / 2), image=cropped_tk, tags=tags.WALLPAPER ) self.wallpaper_drawn = cropped_tk @@ -581,7 +558,7 @@ class CanvasGraph(tk.Canvas): # place the center of the image at the center of the canvas self.delete(self.wallpaper_id) self.wallpaper_id = self.create_image( - (canvas_w / 2, canvas_h / 2), image=cropped_tk, tags="wallpaper" + (canvas_w / 2, canvas_h / 2), image=cropped_tk, tags=tags.WALLPAPER ) self.wallpaper_drawn = cropped_tk @@ -595,7 +572,7 @@ class CanvasGraph(tk.Canvas): image = Images.create(self.wallpaper_file, int(canvas_w), int(canvas_h)) self.delete(self.wallpaper_id) self.wallpaper_id = self.create_image( - (canvas_w / 2, canvas_h / 2), image=image, tags="wallpaper" + (canvas_w / 2, canvas_h / 2), image=image, tags=tags.WALLPAPER ) self.wallpaper_drawn = image @@ -619,7 +596,7 @@ class CanvasGraph(tk.Canvas): self.coords(self.grid, 0, 0, width, height) # redraw gridlines to new canvas size - self.delete("gridline") + self.delete(tags.GRIDLINE) self.draw_grid() self.update_grid() @@ -639,15 +616,15 @@ class CanvasGraph(tk.Canvas): logging.warning("tiled background not implemented yet") # raise items above wallpaper - for component in ABOVE_WALLPAPER: + for component in tags.ABOVE_WALLPAPER_TAGS: self.tag_raise(component) def update_grid(self): logging.info("updating grid show: %s", self.show_grid.get()) if self.show_grid.get(): - self.itemconfig("gridline", state=tk.NORMAL) + self.itemconfig(tags.GRIDLINE, state=tk.NORMAL) else: - self.itemconfig("gridline", state=tk.HIDDEN) + self.itemconfig(tags.GRIDLINE, state=tk.HIDDEN) def set_wallpaper(self, filename): logging.info("setting wallpaper: %s", filename) diff --git a/coretk/coretk/graph/linkinfo.py b/coretk/coretk/graph/linkinfo.py index 15c08e45..decca3bb 100644 --- a/coretk/coretk/graph/linkinfo.py +++ b/coretk/coretk/graph/linkinfo.py @@ -5,6 +5,7 @@ import tkinter as tk from tkinter import font from core.api.grpc import core_pb2 +from coretk.graph import tags TEXT_DISTANCE = 0.30 @@ -52,10 +53,20 @@ class LinkInfo: f"{self.link.interface_two.ip6}/{self.link.interface_two.ip6mask}\n" ) self.id1 = self.canvas.create_text( - x1, y1, text=label_one, justify=tk.CENTER, font=self.font, tags="linkinfo" + x1, + y1, + text=label_one, + justify=tk.CENTER, + font=self.font, + tags=tags.LINK_INFO, ) self.id2 = self.canvas.create_text( - x2, y2, text=label_two, justify=tk.CENTER, font=self.font, tags="linkinfo" + x2, + y2, + text=label_two, + justify=tk.CENTER, + font=self.font, + tags=tags.LINK_INFO, ) def recalculate_info(self): diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 83b29da2..1dcb8959 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -7,6 +7,7 @@ from coretk.dialogs.emaneconfig import EmaneConfigDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog +from coretk.graph import tags from coretk.graph.enums import GraphMode from coretk.graph.tooltip import CanvasTooltip from coretk.nodeutils import NodeUtils @@ -23,7 +24,7 @@ class CanvasNode: x = self.core_node.position.x y = self.core_node.position.y self.id = self.canvas.create_image( - x, y, anchor=tk.CENTER, image=self.image, tags="node" + x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE ) image_box = self.canvas.bbox(self.id) y = image_box[3] + NODE_TEXT_OFFSET @@ -32,7 +33,7 @@ class CanvasNode: x, y, text=self.core_node.name, - tags="nodename", + tags=tags.NODE_NAME, font=text_font, fill="#0000CD", ) @@ -62,13 +63,12 @@ class CanvasNode: def add_antenna(self): x, y = self.canvas.coords(self.id) offset = len(self.antennae) * 8 - antenna_id = self.canvas.create_image( x - 16 + offset, y - 23, anchor=tk.CENTER, image=NodeUtils.ANTENNA_ICON, - tags="antenna", + tags=tags.ANTENNA, ) self.antennae.append(antenna_id) diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 22c676db..2928a695 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -1,10 +1,9 @@ import logging from coretk.dialogs.shapemod import ShapeDialog +from coretk.graph import tags from coretk.graph.shapeutils import ShapeType -ABOVE_COMPONENT = ["gridline", "edge", "linkinfo", "antenna", "node", "nodename"] - class AnnotationData: def __init__( @@ -70,7 +69,7 @@ class Shape: self.y1, self.x2, self.y2, - tags="shape", + tags=tags.SHAPE, dash=dash, fill=self.shape_data.fill_color, outline=self.shape_data.border_color, @@ -83,7 +82,7 @@ class Shape: self.y1, self.x2, self.y2, - tags="shape", + tags=tags.SHAPE, dash=dash, fill=self.shape_data.fill_color, outline=self.shape_data.border_color, @@ -95,7 +94,7 @@ class Shape: self.id = self.canvas.create_text( self.x1, self.y1, - tags="shapetext", + tags=tags.SHAPE_TEXT, text=self.shape_data.text, fill=self.shape_data.text_color, font=font, @@ -122,7 +121,7 @@ class Shape: self.text_id = self.canvas.create_text( x, y, - tags="shapetext", + tags=tags.SHAPE_TEXT, text=self.shape_data.text, fill=self.shape_data.text_color, font=font, @@ -132,7 +131,7 @@ class Shape: self.canvas.coords(self.id, self.x1, self.y1, x1, y1) def shape_complete(self, x, y): - for component in ABOVE_COMPONENT: + for component in tags.ABOVE_SHAPE: self.canvas.tag_raise(component) s = ShapeDialog(self.app, self.app, self) s.show() diff --git a/coretk/coretk/graph/tags.py b/coretk/coretk/graph/tags.py new file mode 100644 index 00000000..42f4ff5f --- /dev/null +++ b/coretk/coretk/graph/tags.py @@ -0,0 +1,35 @@ +GRIDLINE = "gridline" +SHAPE = "shape" +SHAPE_TEXT = "shapetext" +EDGE = "edge" +LINK_INFO = "linkinfo" +WIRELESS_EDGE = "wireless" +ANTENNA = "antenna" +NODE_NAME = "nodename" +NODE = "node" +WALLPAPER = "wallpaper" +SELECTION = "selectednodes" +ABOVE_WALLPAPER_TAGS = [ + GRIDLINE, + SHAPE, + SHAPE_TEXT, + EDGE, + LINK_INFO, + WIRELESS_EDGE, + ANTENNA, + NODE, + NODE_NAME, +] +ABOVE_SHAPE = [GRIDLINE, EDGE, LINK_INFO, WIRELESS_EDGE, ANTENNA, NODE, NODE_NAME] +COMPONENT_TAGS = [ + EDGE, + NODE, + NODE_NAME, + WALLPAPER, + LINK_INFO, + ANTENNA, + WIRELESS_EDGE, + SELECTION, + SHAPE, + SHAPE_TEXT, +] diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 55b2356e..a47cac5e 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -5,6 +5,7 @@ from functools import partial from tkinter import ttk from coretk.dialogs.customnodes import CustomNodesDialog +from coretk.graph import tags from coretk.graph.enums import GraphMode from coretk.graph.shapeutils import ShapeType from coretk.images import ImageEnum, Images @@ -369,7 +370,7 @@ class Toolbar(ttk.Frame): self.app.statusbar.progress_bar.start(5) thread = threading.Thread(target=self.app.core.stop_session) thread.start() - self.app.canvas.delete("wireless") + self.app.canvas.delete(tags.WIRELESS_EDGE) self.design_frame.tkraise() self.click_selection() From c36a72bc168df00246d8b514d63848e9a0701a30 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 15:27:51 -0800 Subject: [PATCH 345/462] small tweak to statusbar to define the different sections --- coretk/coretk/statusbar.py | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/coretk/coretk/statusbar.py b/coretk/coretk/statusbar.py index c4263e85..8910c742 100644 --- a/coretk/coretk/statusbar.py +++ b/coretk/coretk/statusbar.py @@ -25,33 +25,42 @@ class StatusBar(ttk.Frame): self.columnconfigure(3, weight=1) self.columnconfigure(4, weight=1) + frame = ttk.Frame(self, borderwidth=1, relief=tk.RIDGE) + frame.grid(row=0, column=0, sticky="ew") + frame.columnconfigure(0, weight=1) self.progress_bar = ttk.Progressbar( - self, orient="horizontal", mode="indeterminate" + frame, orient="horizontal", mode="indeterminate" ) - self.progress_bar.grid(row=0, column=0, sticky="ew") + self.progress_bar.grid(sticky="ew") - self.status = ttk.Label(self, textvariable=self.statusvar, anchor=tk.CENTER) - self.statusvar.set("status") + self.status = ttk.Label( + self, + textvariable=self.statusvar, + anchor=tk.CENTER, + borderwidth=1, + relief=tk.RIDGE, + ) self.status.grid(row=0, column=1, sticky="ew") - self.zoom = ttk.Label(self, text="zoom", anchor=tk.CENTER) + self.zoom = ttk.Label( + self, text="ZOOM TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE + ) self.zoom.grid(row=0, column=2, sticky="ew") - self.cpu_usage = ttk.Label(self, text="cpu usage", anchor=tk.CENTER) + self.cpu_usage = ttk.Label( + self, text="CPU TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE + ) self.cpu_usage.grid(row=0, column=3, sticky="ew") - self.emulation_light = ttk.Label(self, text="emulation light", anchor=tk.CENTER) + self.emulation_light = ttk.Label( + self, text="CEL TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE + ) self.emulation_light.grid(row=0, column=4, sticky="ew") def start_session_callback(self, process_time): - num_nodes = len(self.app.core.canvas_nodes) - num_links = len(self.app.core.links) self.progress_bar.stop() - self.statusvar.set( - "Network topology instantiated in %s seconds (%s node(s) and %s link(s))" - % ("%.3f" % process_time, num_nodes, num_links) - ) + self.statusvar.set(f"Session started in {process_time:.3f} seconds") def stop_session_callback(self, cleanup_time): self.progress_bar.stop() - self.statusvar.set("Cleanup completed in %s seconds" % "%.3f" % cleanup_time) + self.statusvar.set(f"Stopped session in {cleanup_time:.3f} seconds") From 088a69d9d912caccc1b238b1602e2066f0a50b55 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 16:23:09 -0800 Subject: [PATCH 346/462] catching grpc error on setup, displaying error dialog and exiting app --- coretk/coretk/app.py | 3 +++ coretk/coretk/coreclient.py | 48 ++++++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 2dd2c5ca..4d51cd1e 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -98,6 +98,9 @@ class Application(tk.Frame): def save_config(self): appconfig.save(self.guiconfig) + def close(self): + self.master.destroy() + if __name__ == "__main__": log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s" diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 44e4ee99..5c728477 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -6,6 +6,9 @@ import logging import os import time from pathlib import Path +from tkinter import messagebox + +import grpc from core.api.grpc import client, core_pb2 from coretk import appconfig @@ -386,28 +389,35 @@ class CoreClient: :return: existing sessions """ - self.client.connect() + try: + self.client.connect() - # get service information - response = self.client.get_services() - for service in response.services: - group_services = self.services.setdefault(service.group, set()) - group_services.add(service.name) + # get service information + response = self.client.get_services() + for service in response.services: + group_services = self.services.setdefault(service.group, set()) + group_services.add(service.name) - # if there are no sessions, create a new session, else join a session - response = self.client.get_sessions() - logging.info("current sessions: %s", response) - sessions = response.sessions - if len(sessions) == 0: - self.create_new_session() - else: - dialog = SessionsDialog(self.app, self.app) - dialog.show() + # if there are no sessions, create a new session, else join a session + response = self.client.get_sessions() + logging.info("current sessions: %s", response) + sessions = response.sessions + if len(sessions) == 0: + self.create_new_session() + else: + dialog = SessionsDialog(self.app, self.app) + dialog.show() - response = self.client.get_service_defaults(self.session_id) - self.default_services = { - x.node_type: set(x.services) for x in response.defaults - } + response = self.client.get_service_defaults(self.session_id) + self.default_services = { + x.node_type: set(x.services) for x in response.defaults + } + except grpc.RpcError as e: + if e.code() == grpc.StatusCode.UNAVAILABLE: + messagebox.showerror("Server Error", e.details()) + else: + messagebox.showerror("GRPC Error", e.details()) + self.app.close() def get_session_state(self): response = self.client.get_session(self.session_id) From 33e3a4614690c0ab552e921e89eef8d7ce08a274 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 16:25:29 -0800 Subject: [PATCH 347/462] better gui startup connection failure message --- coretk/coretk/coreclient.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 5c728477..376c739c 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -414,7 +414,8 @@ class CoreClient: } except grpc.RpcError as e: if e.code() == grpc.StatusCode.UNAVAILABLE: - messagebox.showerror("Server Error", e.details()) + + messagebox.showerror("Server Error", "CORE Daemon Unavailable") else: messagebox.showerror("GRPC Error", e.details()) self.app.close() From 0b3f3a5166aefc7da80bdc073b9f0f7a8fa27178 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 9 Dec 2019 16:33:32 -0800 Subject: [PATCH 348/462] start working on input validation --- coretk/coretk/dialogs/canvassizeandscale.py | 34 ++++- coretk/coretk/dialogs/nodeconfig.py | 7 +- coretk/coretk/validation.py | 148 ++++++++++++++++++++ coretk/coretk/widgets.py | 18 ++- 4 files changed, 200 insertions(+), 7 deletions(-) diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index fe4c3b62..770d40df 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -4,6 +4,7 @@ size and scale import tkinter as tk from tkinter import font, ttk +from coretk import validation from coretk.dialogs.dialog import Dialog PAD = 5 @@ -37,6 +38,12 @@ class SizeAndScaleDialog(Dialog): self.meters_width = tk.IntVar(value=width / PIXEL_SCALE * location.scale) self.meters_height = tk.IntVar(value=height / PIXEL_SCALE * location.scale) self.save_default = tk.BooleanVar(value=False) + self.vcmd_canvas_int = validation.validate_command( + app.master, validation.check_canvas_int + ) + self.vcmd_canvas_float = validation.validate_command( + app.master, validation.check_canvas_float + ) self.draw() def draw(self): @@ -47,6 +54,11 @@ class SizeAndScaleDialog(Dialog): self.draw_save_as_default() self.draw_buttons() + def focus_out(self, event): + value = event.widget.get() + if value == "": + event.widget.insert(tk.END, 0) + def draw_size(self): label_frame = ttk.Labelframe(self.top, text="Size", padding=PAD) label_frame.grid(sticky="ew") @@ -59,11 +71,22 @@ class SizeAndScaleDialog(Dialog): frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") label.grid(row=0, column=0, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.pixel_width) + entry = ttk.Entry( + frame, + textvariable=self.pixel_width, + validate="key", + validatecommand=(self.vcmd_canvas_int, "%P"), + ) entry.grid(row=0, column=1, sticky="ew", padx=PAD) + entry.bind("", self.focus_out) label = ttk.Label(frame, text="x Height") label.grid(row=0, column=2, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.pixel_height) + entry = ttk.Entry( + frame, + textvariable=self.pixel_height, + validate="key", + validatecommand=(self.vcmd_canvas_int, "%P"), + ) entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Pixels") label.grid(row=0, column=4, sticky="w") @@ -94,7 +117,12 @@ class SizeAndScaleDialog(Dialog): frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text=f"{PIXEL_SCALE} Pixels =") label.grid(row=0, column=0, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.scale) + entry = ttk.Entry( + frame, + textvariable=self.scale, + validate="key", + validatecommand=(self.vcmd_canvas_float, "%P"), + ) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=2, sticky="w") diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index f296e4cf..185a8f62 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -3,6 +3,7 @@ import tkinter as tk from functools import partial from tkinter import ttk +import coretk.validation as validation from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeService @@ -69,7 +70,10 @@ class NodeConfigDialog(Dialog): # name field label = ttk.Label(frame, text="Name") label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) - entry = ttk.Entry(frame, textvariable=self.name) + vcmd = self.app.master.register(validation.check_node_name) + entry = ttk.Entry( + frame, textvariable=self.name, validate="key", validatecommand=(vcmd, "%P") + ) entry.grid(row=row, column=1, sticky="ew") row += 1 @@ -206,5 +210,4 @@ class NodeConfigDialog(Dialog): # redraw self.canvas_node.redraw() - self.destroy() diff --git a/coretk/coretk/validation.py b/coretk/coretk/validation.py index 00563d68..400cac18 100644 --- a/coretk/coretk/validation.py +++ b/coretk/coretk/validation.py @@ -1,3 +1,151 @@ """ input validation """ +import logging +import tkinter as tk + + +def validate_command(master, func): + return master.register(func) + + +def check_positive_int(s): + logging.debug("int validation...") + try: + int_value = int(s) + if int_value >= 0: + return True + return False + except ValueError: + return False + + +def check_positive_float(s): + logging.debug("float validation...") + try: + float_value = float(s) + if float_value >= 0.0: + return True + return False + except ValueError: + return False + + +def check_node_name(name): + logging.debug("node name validation...") + if len(name) <= 0: + return False + for char in name: + if not char.isalnum() and char != "_": + return False + return True + + +def check_canvas_int(s): + logging.debug("int validation...") + if len(s) == 0: + return True + try: + int_value = int(s) + if int_value >= 0: + return True + return False + except ValueError: + return False + + +def check_canvas_float(s): + logging.debug("canvas float validation") + if not s: + return True + try: + float_value = float(s) + if float_value >= 0.0: + return True + return False + except ValueError: + return False + + +def check_interface(name): + logging.debug("interface name validation...") + if len(name) <= 0: + return False, "Interface name cannot be an empty string" + for char in name: + if not char.isalnum() and char != "_": + return ( + False, + "Interface name can only contain alphanumeric letter (a-z) and (0-9) or underscores (_)", + ) + return True, "" + + +def combine_message(key, current_validation, current_message, res, msg): + if not res: + current_validation = res + current_message = current_message + key + ": " + msg + "\n\n" + return current_validation, current_message + + +def check_wlan_config(config): + result = True + message = "" + checks = ["bandwidth", "delay", "error", "jitter", "range"] + for check in checks: + if check in ["bandwidth", "delay", "jitter"]: + res, msg = check_positive_int(config[check].value) + result, message = combine_message(check, result, message, res, msg) + elif check in ["range", "error"]: + res, msg = check_positive_float(config[check].value) + result, message = combine_message(check, result, message, res, msg) + return result, message + + +def check_size_and_scale(dialog): + result = True + message = "" + try: + pixel_width = dialog.pixel_width.get() + if pixel_width < 0: + result, message = combine_message( + "pixel width", result, message, False, "cannot be negative" + ) + except tk.TclError: + result, message = combine_message( + "pixel width", + result, + message, + False, + "invalid value, input non-negative float", + ) + try: + pixel_height = dialog.pixel_height.get() + if pixel_height < 0: + result, message = combine_message( + "pixel height", result, message, False, "cannot be negative" + ) + except tk.TclError: + result, message = combine_message( + "pixel height", + result, + message, + False, + "invalid value, input non-negative float", + ) + try: + scale = dialog.scale.get() + if scale <= 0: + result, message = combine_message( + "scale", result, message, False, "cannot be negative" + ) + except tk.TclError: + result, message = combine_message( + "scale", result, message, False, "invalid value, input non-negative float" + ) + # pixel_height = dialog.pixel_height.get() + # print(pixel_width, pixel_height) + # res, msg = check_positive_int(pixel_width) + # result, message = combine_message("pixel width", result, message, res, msg) + # res, msg = check_positive_int(pixel_height) + # result, message = combine_message("pixel height", result, message, res, msg) + return result, message diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 760846a3..2fddbf9f 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -5,6 +5,7 @@ from tkinter import font, ttk from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 +from coretk import validation INT_TYPES = { core_pb2.ConfigOptionType.UINT8, @@ -60,6 +61,7 @@ class FrameScroll(ttk.LabelFrame): class ConfigFrame(FrameScroll): def __init__(self, master, app, config, **kw): super().__init__(master, app, ttk.Notebook, **kw) + self.app = app self.config = config self.values = {} @@ -67,6 +69,8 @@ class ConfigFrame(FrameScroll): padx = 2 pady = 2 group_mapping = {} + vcmd_int = self.app.master.register(validation.check_positive_int) + vcmd_float = self.app.master.register(validation.check_positive_float) for key in self.config: option = self.config[key] group = group_mapping.setdefault(option.group, []) @@ -104,11 +108,21 @@ class ConfigFrame(FrameScroll): entry.grid(row=index, column=1, sticky="ew", pady=pady) elif option.type in INT_TYPES: value.set(option.value) - entry = ttk.Entry(frame, textvariable=value) + entry = ttk.Entry( + frame, + textvariable=value, + validate="key", + validatecommand=(vcmd_int, "%P"), + ) entry.grid(row=index, column=1, sticky="ew", pady=pady) elif option.type == core_pb2.ConfigOptionType.FLOAT: value.set(option.value) - entry = ttk.Entry(frame, textvariable=value) + entry = ttk.Entry( + frame, + textvariable=value, + validate="key", + validatecommand=(vcmd_float, "%P"), + ) entry.grid(row=index, column=1, sticky="ew", pady=pady) else: logging.error("unhandled config option type: %s", option.type) From 7039a3682e86cd7f223099aec7e14ae01a3da244 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 9 Dec 2019 16:37:52 -0800 Subject: [PATCH 349/462] fix merge conflict --- coretk/coretk/coreclient.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 72ab7cdc..26b9c9c8 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -282,11 +282,7 @@ class CoreClient: self.app.toolbar.click_runtime_selection() else: self.app.toolbar.design_frame.tkraise() - # <<<<<<< HEAD - # self.app.toolbar.select_button.invoke() - # ======= self.app.toolbar.click_selection() - # >>>>>>> coretk self.app.statusbar.progress_bar.stop() def is_runtime(self): From d78ef86cef6a6e9b5f6a35fe20bf4e0943b54628 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 22:50:26 -0800 Subject: [PATCH 350/462] added grpc error display and updated grpc calls to catch and display grpc exceptions --- coretk/coretk/coreclient.py | 291 ++++++++++-------- coretk/coretk/dialogs/emaneconfig.py | 13 +- coretk/coretk/dialogs/mobilityconfig.py | 9 +- coretk/coretk/dialogs/mobilityplayer.py | 30 +- coretk/coretk/dialogs/serviceconfiguration.py | 132 ++++---- coretk/coretk/dialogs/sessionoptions.py | 28 +- coretk/coretk/dialogs/sessions.py | 17 +- coretk/coretk/dialogs/wlanconfig.py | 9 +- coretk/coretk/errors.py | 8 + coretk/coretk/graph/node.py | 10 +- coretk/coretk/menuaction.py | 10 +- 11 files changed, 329 insertions(+), 228 deletions(-) create mode 100644 coretk/coretk/errors.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 376c739c..1b1a30da 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -6,7 +6,6 @@ import logging import os import time from pathlib import Path -from tkinter import messagebox import grpc @@ -14,6 +13,7 @@ from core.api.grpc import client, core_pb2 from coretk import appconfig from coretk.dialogs.mobilityplayer import MobilityPlayer from coretk.dialogs.sessions import SessionsDialog +from coretk.errors import show_grpc_error from coretk.graph import tags from coretk.graph.shape import AnnotationData, Shape from coretk.graph.shapeutils import ShapeType @@ -206,85 +206,92 @@ class CoreClient: self.reset() # get session data - response = self.client.get_session(self.session_id) - session = response.session - self.state = session.state - self.client.events(self.session_id, self.handle_events) - self.client.throughputs(self.handle_throughputs) + try: + response = self.client.get_session(self.session_id) + session = response.session + self.state = session.state + self.client.events(self.session_id, self.handle_events) + self.client.throughputs(self.handle_throughputs) - # get location - if query_location: - response = self.client.get_session_location(self.session_id) - self.location = response.location + # get location + if query_location: + response = self.client.get_session_location(self.session_id) + self.location = response.location - # get emane models - response = self.client.get_emane_models(self.session_id) - self.emane_models = response.models + # get emane models + response = self.client.get_emane_models(self.session_id) + self.emane_models = response.models - # get hooks - response = self.client.get_hooks(self.session_id) - for hook in response.hooks: - self.hooks[hook.file] = hook + # get hooks + response = self.client.get_hooks(self.session_id) + for hook in response.hooks: + self.hooks[hook.file] = hook - # get mobility configs - response = self.client.get_mobility_configs(self.session_id) - for node_id in response.configs: - node_config = response.configs[node_id].config - self.mobility_configs[node_id] = node_config + # get mobility configs + response = self.client.get_mobility_configs(self.session_id) + for node_id in response.configs: + node_config = response.configs[node_id].config + self.mobility_configs[node_id] = node_config - # get emane config - response = self.client.get_emane_config(self.session_id) - self.emane_config = response.config + # get emane config + response = self.client.get_emane_config(self.session_id) + self.emane_config = response.config - # get emane model config - response = self.client.get_emane_model_configs(self.session_id) - for _id in response.configs: - config = response.configs[_id] - interface = None - node_id = _id - if _id >= 1000: - interface = _id % 1000 - node_id = int(_id / 1000) - self.set_emane_model_config(node_id, config.model, config.config, interface) + # get emane model config + response = self.client.get_emane_model_configs(self.session_id) + for _id in response.configs: + config = response.configs[_id] + interface = None + node_id = _id + if _id >= 1000: + interface = _id % 1000 + node_id = int(_id / 1000) + self.set_emane_model_config( + node_id, config.model, config.config, interface + ) - # save and retrieve data, needed for session nodes - for node in session.nodes: - # get node service config and file config - self.created_nodes.add(node.id) + # save and retrieve data, needed for session nodes + for node in session.nodes: + # get node service config and file config + self.created_nodes.add(node.id) - # get wlan configs for wlan nodes - if node.type == core_pb2.NodeType.WIRELESS_LAN: - response = self.client.get_wlan_config(self.session_id, node.id) - self.wlan_configs[node.id] = response.config - # retrieve service configurations data for default nodes - elif node.type == core_pb2.NodeType.DEFAULT: - for service in node.services: - response = self.client.get_node_service( - self.session_id, node.id, service - ) - if node.id not in self.service_configs: - self.service_configs[node.id] = {} - self.service_configs[node.id][service] = response.service - for file in response.service.configs: - response = self.client.get_node_service_file( - self.session_id, node.id, service, file + # get wlan configs for wlan nodes + if node.type == core_pb2.NodeType.WIRELESS_LAN: + response = self.client.get_wlan_config(self.session_id, node.id) + self.wlan_configs[node.id] = response.config + # retrieve service configurations data for default nodes + elif node.type == core_pb2.NodeType.DEFAULT: + for service in node.services: + response = self.client.get_node_service( + self.session_id, node.id, service ) - if node.id not in self.file_configs: - self.file_configs[node.id] = {} - if service not in self.file_configs[node.id]: - self.file_configs[node.id][service] = {} - self.file_configs[node.id][service][file] = response.data + if node.id not in self.service_configs: + self.service_configs[node.id] = {} + self.service_configs[node.id][service] = response.service + for file in response.service.configs: + response = self.client.get_node_service_file( + self.session_id, node.id, service, file + ) + if node.id not in self.file_configs: + self.file_configs[node.id] = {} + if service not in self.file_configs[node.id]: + self.file_configs[node.id][service] = {} + self.file_configs[node.id][service][file] = response.data - # store links as created links - for link in session.links: - self.created_links.add(tuple(sorted([link.node_one_id, link.node_two_id]))) + # store links as created links + for link in session.links: + self.created_links.add( + tuple(sorted([link.node_one_id, link.node_two_id])) + ) - # draw session - self.app.canvas.reset_and_redraw(session) + # draw session + self.app.canvas.reset_and_redraw(session) - # get metadata - response = self.client.get_session_metadata(self.session_id) - self.parse_metadata(response.config) + # get metadata + response = self.client.get_session_metadata(self.session_id) + self.parse_metadata(response.config) + except grpc.RpcError as e: + show_grpc_error(e) # update ui to represent current state if self.is_runtime(): @@ -363,25 +370,31 @@ class CoreClient: :return: nothing """ - response = self.client.create_session() - logging.info("created session: %s", response) - location_config = self.app.guiconfig["location"] - self.location = core_pb2.SessionLocation( - x=location_config["x"], - y=location_config["y"], - z=location_config["z"], - lat=location_config["lat"], - lon=location_config["lon"], - alt=location_config["alt"], - scale=location_config["scale"], - ) - self.join_session(response.session_id, query_location=False) + try: + response = self.client.create_session() + logging.info("created session: %s", response) + location_config = self.app.guiconfig["location"] + self.location = core_pb2.SessionLocation( + x=location_config["x"], + y=location_config["y"], + z=location_config["z"], + lat=location_config["lat"], + lon=location_config["lon"], + alt=location_config["alt"], + scale=location_config["scale"], + ) + self.join_session(response.session_id, query_location=False) + except grpc.RpcError as e: + show_grpc_error(e) def delete_session(self, session_id=None): if session_id is None: session_id = self.session_id - response = self.client.delete_session(session_id) - logging.info("Deleted session result: %s", response) + try: + response = self.client.delete_session(session_id) + logging.info("deleted session result: %s", response) + except grpc.RpcError as e: + show_grpc_error(e) def set_up(self): """ @@ -413,21 +426,15 @@ class CoreClient: x.node_type: set(x.services) for x in response.defaults } except grpc.RpcError as e: - if e.code() == grpc.StatusCode.UNAVAILABLE: - - messagebox.showerror("Server Error", "CORE Daemon Unavailable") - else: - messagebox.showerror("GRPC Error", e.details()) + show_grpc_error(e) self.app.close() - def get_session_state(self): - response = self.client.get_session(self.session_id) - logging.info("get session: %s", response) - return response.session.state - def edit_node(self, node_id, x, y): position = core_pb2.Position(x=x, y=y) - self.client.edit_node(self.session_id, node_id, position, source="gui") + try: + self.client.edit_node(self.session_id, node_id, position, source="gui") + except grpc.RpcError as e: + show_grpc_error(e) def start_session(self): nodes = [x.core_node for x in self.canvas_nodes.values()] @@ -446,39 +453,51 @@ class CoreClient: emane_config = None start = time.perf_counter() - response = self.client.start_session( - self.session_id, - nodes, - links, - self.location, - hooks, - emane_config, - emane_model_configs, - wlan_configs, - mobility_configs, - service_configs, - file_configs, - ) - self.set_metadata() - process_time = time.perf_counter() - start - logging.debug("start session(%s), result: %s", self.session_id, response.result) - self.app.statusbar.start_session_callback(process_time) + try: + response = self.client.start_session( + self.session_id, + nodes, + links, + self.location, + hooks, + emane_config, + emane_model_configs, + wlan_configs, + mobility_configs, + service_configs, + file_configs, + ) + self.set_metadata() + process_time = time.perf_counter() - start + logging.debug( + "start session(%s), result: %s", self.session_id, response.result + ) + self.app.statusbar.start_session_callback(process_time) - # display mobility players - for node_id, config in self.mobility_configs.items(): - canvas_node = self.canvas_nodes[node_id] - mobility_player = MobilityPlayer(self.app, self.app, canvas_node, config) - mobility_player.show() - self.mobility_players[node_id] = mobility_player + # display mobility players + for node_id, config in self.mobility_configs.items(): + canvas_node = self.canvas_nodes[node_id] + mobility_player = MobilityPlayer( + self.app, self.app, canvas_node, config + ) + mobility_player.show() + self.mobility_players[node_id] = mobility_player + except grpc.RpcError as e: + show_grpc_error(e) def stop_session(self, session_id=None): if not session_id: session_id = self.session_id start = time.perf_counter() - response = self.client.stop_session(session_id) - process_time = time.perf_counter() - start - self.app.statusbar.stop_session_callback(process_time) - logging.debug("stopped session(%s), result: %s", session_id, response.result) + try: + response = self.client.stop_session(session_id) + logging.debug( + "stopped session(%s), result: %s", session_id, response.result + ) + process_time = time.perf_counter() - start + self.app.statusbar.stop_session_callback(process_time) + except grpc.RpcError as e: + show_grpc_error(e) def set_metadata(self): # create canvas data @@ -502,9 +521,12 @@ class CoreClient: logging.info("set session metadata: %s", response) def launch_terminal(self, node_id): - response = self.client.get_node_terminal(self.session_id, node_id) - logging.info("get terminal %s", response.terminal) - os.system(f"xterm -e {response.terminal} &") + try: + response = self.client.get_node_terminal(self.session_id, node_id) + logging.info("get terminal %s", response.terminal) + os.system(f"xterm -e {response.terminal} &") + except grpc.RpcError as e: + show_grpc_error(e) def save_xml(self, file_path): """ @@ -513,9 +535,11 @@ class CoreClient: :param str file_path: file path that user pick :return: nothing """ - response = self.client.save_xml(self.session_id, file_path) - logging.info("saved xml(%s): %s", file_path, response) - self.client.events(self.session_id, self.handle_events) + try: + response = self.client.save_xml(self.session_id, file_path) + logging.info("saved xml(%s): %s", file_path, response) + except grpc.RpcError as e: + show_grpc_error(e) def open_xml(self, file_path): """ @@ -524,9 +548,12 @@ class CoreClient: :param str file_path: file to open :return: session id """ - response = self.client.open_xml(file_path) - logging.debug("open xml: %s", response) - self.join_session(response.session_id) + try: + response = self.client.open_xml(file_path) + logging.debug("open xml: %s", response) + self.join_session(response.session_id) + except grpc.RpcError as e: + show_grpc_error(e) def get_node_service(self, node_id, service_name): response = self.client.get_node_service(self.session_id, node_id, service_name) @@ -563,7 +590,7 @@ class CoreClient: """ node_protos = [x.core_node for x in self.canvas_nodes.values()] link_protos = list(self.links.values()) - if self.get_session_state() != core_pb2.SessionState.DEFINITION: + if self.state != core_pb2.SessionState.DEFINITION: self.client.set_session_state( self.session_id, core_pb2.SessionState.DEFINITION ) @@ -596,7 +623,7 @@ class CoreClient: :return: nothing """ - logging.debug("Close grpc") + logging.debug("close grpc") self.client.close() def next_node_id(self): diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index 71ffe078..a9042ba3 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -6,7 +6,10 @@ import tkinter as tk import webbrowser from tkinter import ttk +import grpc + from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error from coretk.images import ImageEnum, Images from coretk.widgets import ConfigFrame @@ -52,9 +55,13 @@ class EmaneModelDialog(Dialog): self.model = f"emane_{model}" self.interface = interface self.config_frame = None - self.config = self.app.core.get_emane_model_config( - self.node.id, self.model, self.interface - ) + try: + self.config = self.app.core.get_emane_model_config( + self.node.id, self.model, self.interface + ) + except grpc.RpcError as e: + show_grpc_error(e) + self.destroy() self.draw() def draw(self): diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/coretk/coretk/dialogs/mobilityconfig.py index 21782ee6..4e9d9590 100644 --- a/coretk/coretk/dialogs/mobilityconfig.py +++ b/coretk/coretk/dialogs/mobilityconfig.py @@ -3,7 +3,10 @@ mobility configuration """ from tkinter import ttk +import grpc + from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error from coretk.widgets import ConfigFrame PAD = 5 @@ -20,7 +23,11 @@ class MobilityConfigDialog(Dialog): self.canvas_node = canvas_node self.node = canvas_node.core_node self.config_frame = None - self.config = self.app.core.get_mobility_config(self.node.id) + try: + self.config = self.app.core.get_mobility_config(self.node.id) + except grpc.RpcError as e: + show_grpc_error(e) + self.destroy() self.draw() def draw(self): diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index 136e6179..3ccf0b5d 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -1,8 +1,11 @@ import tkinter as tk from tkinter import ttk +import grpc + from core.api.grpc.core_pb2 import MobilityAction from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error from coretk.images import ImageEnum, Images PAD = 5 @@ -123,20 +126,29 @@ class MobilityPlayerDialog(Dialog): def click_play(self): self.set_play() session_id = self.app.core.session_id - self.app.core.client.mobility_action( - session_id, self.node.id, MobilityAction.START - ) + try: + self.app.core.client.mobility_action( + session_id, self.node.id, MobilityAction.START + ) + except grpc.RpcError as e: + show_grpc_error(e) def click_pause(self): self.set_pause() session_id = self.app.core.session_id - self.app.core.client.mobility_action( - session_id, self.node.id, MobilityAction.PAUSE - ) + try: + self.app.core.client.mobility_action( + session_id, self.node.id, MobilityAction.PAUSE + ) + except grpc.RpcError as e: + show_grpc_error(e) def click_stop(self): self.set_stop() session_id = self.app.core.session_id - self.app.core.client.mobility_action( - session_id, self.node.id, MobilityAction.STOP - ) + try: + self.app.core.client.mobility_action( + session_id, self.node.id, MobilityAction.STOP + ) + except grpc.RpcError as e: + show_grpc_error(e) diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 446f9c12..354937e5 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -3,8 +3,11 @@ import logging import tkinter as tk from tkinter import ttk +import grpc + from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error from coretk.images import ImageEnum, Images from coretk.widgets import CodeText, ListboxScroll @@ -47,44 +50,48 @@ class ServiceConfiguration(Dialog): self.draw() def load(self): - # create nodes and links in definition state for getting and setting service file - self.app.core.create_nodes_and_links() - - service_configs = self.app.core.service_configs - if ( - self.node_id in service_configs - and self.service_name in service_configs[self.node_id] - ): - service_config = self.app.core.service_configs[self.node_id][ - self.service_name - ] - else: - service_config = self.app.core.get_node_service( - self.node_id, self.service_name - ) - self.dependencies = [x for x in service_config.dependencies] - self.executables = [x for x in service_config.executables] - self.metadata = service_config.meta - self.filenames = [x for x in service_config.configs] - self.startup_commands = [x for x in service_config.startup] - self.validation_commands = [x for x in service_config.validate] - self.shutdown_commands = [x for x in service_config.shutdown] - self.validation_mode = service_config.validation_mode - self.validation_time = service_config.validation_timer - self.original_service_files = { - x: self.app.core.get_node_service_file(self.node_id, self.service_name, x) - for x in self.filenames - } - self.temp_service_files = { - x: self.original_service_files[x] for x in self.original_service_files - } - file_configs = self.app.core.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 + try: + # create nodes and links in definition state for getting and setting service file + self.app.core.create_nodes_and_links() + service_configs = self.app.core.service_configs + if ( + self.node_id in service_configs + and self.service_name in service_configs[self.node_id] + ): + service_config = self.app.core.service_configs[self.node_id][ + self.service_name + ] + else: + service_config = self.app.core.get_node_service( + self.node_id, self.service_name + ) + self.dependencies = [x for x in service_config.dependencies] + self.executables = [x for x in service_config.executables] + self.metadata = service_config.meta + self.filenames = [x for x in service_config.configs] + self.startup_commands = [x for x in service_config.startup] + self.validation_commands = [x for x in service_config.validate] + self.shutdown_commands = [x for x in service_config.shutdown] + self.validation_mode = service_config.validation_mode + self.validation_time = service_config.validation_timer + self.original_service_files = { + x: self.app.core.get_node_service_file( + self.node_id, self.service_name, x + ) + for x in self.filenames + } + self.temp_service_files = { + x: self.original_service_files[x] for x in self.original_service_files + } + file_configs = self.app.core.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 + except grpc.RpcError as e: + show_grpc_error(e) def draw(self): # self.columnconfigure(1, weight=1) @@ -366,30 +373,33 @@ class ServiceConfiguration(Dialog): 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, - startup_commands, - validate_commands, - shutdown_commands, - ) - if self.node_id not in service_configs: - service_configs[self.node_id] = {} - if self.service_name not in service_configs[self.node_id]: - self.app.core.service_configs[self.node_id][self.service_name] = config - for file in self.modified_files: - file_configs = self.app.core.file_configs - if self.node_id not in file_configs: - file_configs[self.node_id] = {} - if self.service_name not in file_configs[self.node_id]: - file_configs[self.node_id][self.service_name] = {} - 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] + try: + config = self.core.set_node_service( + self.node_id, + self.service_name, + startup_commands, + validate_commands, + shutdown_commands, ) + if self.node_id not in service_configs: + service_configs[self.node_id] = {} + if self.service_name not in service_configs[self.node_id]: + self.app.core.service_configs[self.node_id][self.service_name] = config + for file in self.modified_files: + file_configs = self.app.core.file_configs + if self.node_id not in file_configs: + file_configs[self.node_id] = {} + if self.service_name not in file_configs[self.node_id]: + file_configs[self.node_id][self.service_name] = {} + 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] + ) + except grpc.RpcError as e: + show_grpc_error(e) self.destroy() def display_service_file_data(self, event): diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index b2666015..7c77f9df 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -1,7 +1,10 @@ import logging from tkinter import ttk +import grpc + from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error from coretk.widgets import ConfigFrame PAD_X = 2 @@ -12,17 +15,23 @@ class SessionOptionsDialog(Dialog): def __init__(self, master, app): super().__init__(master, app, "Session Options", modal=True) self.config_frame = None + self.config = self.get_config() self.draw() + def get_config(self): + try: + session_id = self.app.core.session_id + response = self.app.core.client.get_session_options(session_id) + return response.config + except grpc.RpcError as e: + show_grpc_error(e) + self.destroy() + def draw(self): self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) - session_id = self.app.core.session_id - response = self.app.core.client.get_session_options(session_id) - logging.info("session options: %s", response) - - self.config_frame = ConfigFrame(self.top, self.app, config=response.config) + self.config_frame = ConfigFrame(self.top, self.app, config=self.config) self.config_frame.draw_config() self.config_frame.grid(sticky="nsew") @@ -37,7 +46,10 @@ class SessionOptionsDialog(Dialog): def save(self): config = self.config_frame.parse_config() - session_id = self.app.core.session_id - response = self.app.core.client.set_session_options(session_id, config) - logging.info("saved session config: %s", response) + try: + session_id = self.app.core.session_id + response = self.app.core.client.set_session_options(session_id, config) + logging.info("saved session config: %s", response) + except grpc.RpcError as e: + show_grpc_error(e) self.destroy() diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index 7a07c059..e3d9e696 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -3,8 +3,11 @@ import threading import tkinter as tk from tkinter import ttk +import grpc + from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error from coretk.images import ImageEnum, Images @@ -14,8 +17,18 @@ class SessionsDialog(Dialog): self.selected = False self.selected_id = None self.tree = None + self.sessions = self.get_sessions() self.draw() + def get_sessions(self): + try: + response = self.app.core.client.get_sessions() + logging.info("sessions: %s", response) + return response.sessions + except grpc.RpcError as e: + show_grpc_error(e) + self.destroy() + def draw(self): self.top.columnconfigure(0, weight=1) self.draw_description() @@ -48,9 +61,7 @@ class SessionsDialog(Dialog): self.tree.column("nodes", stretch=tk.YES) self.tree.heading("nodes", text="Node Count") - response = self.app.core.client.get_sessions() - logging.info("sessions: %s", response) - for index, session in enumerate(response.sessions): + for index, session in enumerate(self.sessions): state_name = core_pb2.SessionState.Enum.Name(session.state) self.tree.insert( "", diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index 3c118834..42adc49a 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -4,7 +4,10 @@ wlan configuration from tkinter import ttk +import grpc + from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error from coretk.widgets import ConfigFrame PAD = 5 @@ -18,7 +21,11 @@ class WlanConfigDialog(Dialog): self.canvas_node = canvas_node self.node = canvas_node.core_node self.config_frame = None - self.config = self.app.core.get_wlan_config(self.node.id) + try: + self.config = self.app.core.get_wlan_config(self.node.id) + except grpc.RpcError as e: + show_grpc_error(e) + self.destroy() self.draw() def draw(self): diff --git a/coretk/coretk/errors.py b/coretk/coretk/errors.py new file mode 100644 index 00000000..936968ad --- /dev/null +++ b/coretk/coretk/errors.py @@ -0,0 +1,8 @@ +from tkinter import messagebox + + +def show_grpc_error(e): + title = [x.capitalize() for x in e.code().name.lower().split("_")] + title = " ".join(title) + title = f"GRPC {title}" + messagebox.showerror(title, e.details()) diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 1dcb8959..79401c04 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -2,11 +2,14 @@ import logging import tkinter as tk from tkinter import font +import grpc + from core.api.grpc.core_pb2 import NodeType from coretk.dialogs.emaneconfig import EmaneConfigDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog +from coretk.errors import show_grpc_error from coretk.graph import tags from coretk.graph.enums import GraphMode from coretk.graph.tooltip import CanvasTooltip @@ -138,8 +141,11 @@ class CanvasNode: if self.app.core.is_runtime() and self.app.core.observer: self.tooltip.text.set("waiting...") self.tooltip.on_enter(event) - output = self.app.core.run(self.core_node.id) - self.tooltip.text.set(output) + try: + output = self.app.core.run(self.core_node.id) + self.tooltip.text.set(output) + except grpc.RpcError as e: + show_grpc_error(e) def on_leave(self, event): self.tooltip.on_leave(event) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index f7f9c579..246020ba 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -10,7 +10,6 @@ from tkinter import filedialog, messagebox import grpc -from core.api.grpc import core_pb2 from coretk.appconfig import XML_PATH from coretk.dialogs.about import AboutDialog from coretk.dialogs.canvasbackground import CanvasBackgroundDialog @@ -52,12 +51,7 @@ class MenuAction: "menuaction.py: clean_nodes_links_and_set_configuration() Exiting the program" ) try: - state = self.app.core.get_session_state() - - if ( - state == core_pb2.SessionState.SHUTDOWN - or state == core_pb2.SessionState.DEFINITION - ): + if not self.app.core.is_runtime(): self.app.core.delete_session() if quitapp: self.app.quit() @@ -73,7 +67,7 @@ class MenuAction: elif quitapp: self.app.quit() except grpc.RpcError: - logging.error("error getting session state") + logging.exception("error deleting session") if quitapp: self.app.quit() From 21f0857e654170c49f15ec7972492acb2496ad30 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 23:09:39 -0800 Subject: [PATCH 351/462] reset mobility player to default dialog size --- coretk/coretk/dialogs/mobilityplayer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index 3ccf0b5d..6c9799b9 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -53,6 +53,7 @@ class MobilityPlayerDialog(Dialog): super().__init__( master, app, f"{canvas_node.core_node.name} Mobility Player", modal=False ) + self.geometry("") self.canvas_node = canvas_node self.node = canvas_node.core_node self.config = config From ada21997e99d68c62d21f94cfe10cbd057211070 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 10 Dec 2019 09:57:12 -0800 Subject: [PATCH 352/462] add validation to canvassizescale, wlanconfig, nodename --- coretk/coretk/app.py | 3 + coretk/coretk/dialogs/canvassizeandscale.py | 79 +++++--- coretk/coretk/dialogs/nodeconfig.py | 8 +- coretk/coretk/validation.py | 206 +++++++------------- coretk/coretk/widgets.py | 9 +- 5 files changed, 140 insertions(+), 165 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 4d51cd1e..47626d0d 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -12,6 +12,7 @@ from coretk.menubar import Menubar from coretk.nodeutils import NodeUtils from coretk.statusbar import StatusBar from coretk.toolbar import Toolbar +from coretk.validation import InputValidation class Application(tk.Frame): @@ -25,6 +26,7 @@ class Application(tk.Frame): self.toolbar = None self.canvas = None self.statusbar = None + self.validation = None # setup self.guiconfig = appconfig.read() @@ -48,6 +50,7 @@ class Application(tk.Frame): image = Images.get(ImageEnum.CORE, 16) self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) + self.validation = InputValidation(self) def center(self): width = 1000 diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 669c0732..624e4246 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -4,7 +4,6 @@ size and scale import tkinter as tk from tkinter import font, ttk -from coretk import validation from coretk.dialogs.dialog import Dialog PAD = 5 @@ -20,6 +19,7 @@ class SizeAndScaleDialog(Dialog): """ super().__init__(master, app, "Canvas Size and Scale", modal=True) self.canvas = self.app.canvas + self.validation = app.validation self.section_font = font.Font(weight="bold") # get current canvas dimensions plot = self.canvas.find_withtag("rectangle") @@ -38,12 +38,6 @@ class SizeAndScaleDialog(Dialog): self.meters_width = tk.IntVar(value=width / PIXEL_SCALE * location.scale) self.meters_height = tk.IntVar(value=height / PIXEL_SCALE * location.scale) self.save_default = tk.BooleanVar(value=False) - self.vcmd_canvas_int = validation.validate_command( - app.master, validation.check_canvas_int - ) - self.vcmd_canvas_float = validation.validate_command( - app.master, validation.check_canvas_float - ) self.draw() def draw(self): @@ -54,11 +48,6 @@ class SizeAndScaleDialog(Dialog): self.draw_save_as_default() self.draw_buttons() - def focus_out(self, event): - value = event.widget.get() - if value == "": - event.widget.insert(tk.END, 0) - def draw_size(self): label_frame = ttk.Labelframe(self.top, text="Size", padding=PAD) label_frame.grid(sticky="ew") @@ -75,18 +64,19 @@ class SizeAndScaleDialog(Dialog): frame, textvariable=self.pixel_width, validate="key", - validatecommand=(self.vcmd_canvas_int, "%P"), + validatecommand=(self.validation.positive_int, "%P"), ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=1, sticky="ew", padx=PAD) - entry.bind("", self.focus_out) label = ttk.Label(frame, text="x Height") label.grid(row=0, column=2, sticky="w", padx=PAD) entry = ttk.Entry( frame, textvariable=self.pixel_height, validate="key", - validatecommand=(self.vcmd_canvas_int, "%P"), + validatecommand=(self.validation.positive_int, "%P"), ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Pixels") label.grid(row=0, column=4, sticky="w") @@ -98,11 +88,23 @@ class SizeAndScaleDialog(Dialog): frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") label.grid(row=0, column=0, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.meters_width) + entry = ttk.Entry( + frame, + textvariable=self.meters_width, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="x Height") label.grid(row=0, column=2, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.meters_height) + entry = ttk.Entry( + frame, + textvariable=self.meters_height, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=4, sticky="w") @@ -121,8 +123,9 @@ class SizeAndScaleDialog(Dialog): frame, textvariable=self.scale, validate="key", - validatecommand=(self.vcmd_canvas_float, "%P"), + validatecommand=(self.validation.positive_float, "%P"), ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=2, sticky="w") @@ -144,12 +147,24 @@ class SizeAndScaleDialog(Dialog): label = ttk.Label(frame, text="X") label.grid(row=0, column=0, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.x) + entry = ttk.Entry( + frame, + textvariable=self.x, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Y") label.grid(row=0, column=2, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.y) + entry = ttk.Entry( + frame, + textvariable=self.y, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(label_frame, text="Translates To") @@ -163,17 +178,35 @@ class SizeAndScaleDialog(Dialog): label = ttk.Label(frame, text="Lat") label.grid(row=0, column=0, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.lat) + entry = ttk.Entry( + frame, + textvariable=self.lat, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Lon") label.grid(row=0, column=2, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.lon) + entry = ttk.Entry( + frame, + textvariable=self.lon, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Alt") label.grid(row=0, column=4, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.alt) + entry = ttk.Entry( + frame, + textvariable=self.alt, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=5, sticky="ew") def draw_save_as_default(self): diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 185a8f62..9f7c4d6c 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -3,7 +3,6 @@ import tkinter as tk from functools import partial from tkinter import ttk -import coretk.validation as validation from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeService @@ -70,10 +69,13 @@ class NodeConfigDialog(Dialog): # name field label = ttk.Label(frame, text="Name") label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) - vcmd = self.app.master.register(validation.check_node_name) entry = ttk.Entry( - frame, textvariable=self.name, validate="key", validatecommand=(vcmd, "%P") + frame, + textvariable=self.name, + validate="key", + validatecommand=(self.app.validation.name, "%P"), ) + entry.bind("", self.app.validation.name_focus_out) entry.grid(row=row, column=1, sticky="ew") row += 1 diff --git a/coretk/coretk/validation.py b/coretk/coretk/validation.py index 400cac18..a580c712 100644 --- a/coretk/coretk/validation.py +++ b/coretk/coretk/validation.py @@ -5,147 +5,85 @@ import logging import tkinter as tk -def validate_command(master, func): - return master.register(func) +class InputValidation: + def __init__(self, app): + self.master = app.master + self.positive_int = None + self.positive_float = None + self.name = None + self.register() + def register(self): + self.positive_int = self.master.register(self.check_positive_int) + self.positive_float = self.master.register(self.check_positive_float) + self.name = self.master.register(self.check_node_name) -def check_positive_int(s): - logging.debug("int validation...") - try: - int_value = int(s) - if int_value >= 0: + def focus_out(self, event): + value = event.widget.get() + if value == "": + event.widget.insert(tk.END, 0) + + def name_focus_out(self, event): + logging.debug("name focus out") + value = event.widget.get() + if value == "": + event.widget.insert(tk.END, "empty") + + def check_positive_int(self, s): + logging.debug("int validation...") + if len(s) == 0: return True - return False - except ValueError: - return False - - -def check_positive_float(s): - logging.debug("float validation...") - try: - float_value = float(s) - if float_value >= 0.0: - return True - return False - except ValueError: - return False - - -def check_node_name(name): - logging.debug("node name validation...") - if len(name) <= 0: - return False - for char in name: - if not char.isalnum() and char != "_": + try: + int_value = int(s) + if int_value >= 0: + return True + return False + except ValueError: return False - return True - -def check_canvas_int(s): - logging.debug("int validation...") - if len(s) == 0: - return True - try: - int_value = int(s) - if int_value >= 0: + def check_positive_float(self, s): + logging.debug("float validation...") + if len(s) == 0: return True - return False - except ValueError: - return False + try: + float_value = float(s) + if float_value >= 0.0: + return True + return False + except ValueError: + return False - -def check_canvas_float(s): - logging.debug("canvas float validation") - if not s: - return True - try: - float_value = float(s) - if float_value >= 0.0: + def check_node_name(self, s): + logging.debug("node name validation...") + if len(s) < 0: + return False + if len(s) == 0: return True - return False - except ValueError: - return False + for char in s: + if not char.isalnum() and char != "_": + return False + return True + def check_canvas_int(sefl, s): + logging.debug("int validation...") + if len(s) == 0: + return True + try: + int_value = int(s) + if int_value >= 0: + return True + return False + except ValueError: + return False -def check_interface(name): - logging.debug("interface name validation...") - if len(name) <= 0: - return False, "Interface name cannot be an empty string" - for char in name: - if not char.isalnum() and char != "_": - return ( - False, - "Interface name can only contain alphanumeric letter (a-z) and (0-9) or underscores (_)", - ) - return True, "" - - -def combine_message(key, current_validation, current_message, res, msg): - if not res: - current_validation = res - current_message = current_message + key + ": " + msg + "\n\n" - return current_validation, current_message - - -def check_wlan_config(config): - result = True - message = "" - checks = ["bandwidth", "delay", "error", "jitter", "range"] - for check in checks: - if check in ["bandwidth", "delay", "jitter"]: - res, msg = check_positive_int(config[check].value) - result, message = combine_message(check, result, message, res, msg) - elif check in ["range", "error"]: - res, msg = check_positive_float(config[check].value) - result, message = combine_message(check, result, message, res, msg) - return result, message - - -def check_size_and_scale(dialog): - result = True - message = "" - try: - pixel_width = dialog.pixel_width.get() - if pixel_width < 0: - result, message = combine_message( - "pixel width", result, message, False, "cannot be negative" - ) - except tk.TclError: - result, message = combine_message( - "pixel width", - result, - message, - False, - "invalid value, input non-negative float", - ) - try: - pixel_height = dialog.pixel_height.get() - if pixel_height < 0: - result, message = combine_message( - "pixel height", result, message, False, "cannot be negative" - ) - except tk.TclError: - result, message = combine_message( - "pixel height", - result, - message, - False, - "invalid value, input non-negative float", - ) - try: - scale = dialog.scale.get() - if scale <= 0: - result, message = combine_message( - "scale", result, message, False, "cannot be negative" - ) - except tk.TclError: - result, message = combine_message( - "scale", result, message, False, "invalid value, input non-negative float" - ) - # pixel_height = dialog.pixel_height.get() - # print(pixel_width, pixel_height) - # res, msg = check_positive_int(pixel_width) - # result, message = combine_message("pixel width", result, message, res, msg) - # res, msg = check_positive_int(pixel_height) - # result, message = combine_message("pixel height", result, message, res, msg) - return result, message + def check_canvas_float(self, s): + logging.debug("canvas float validation") + if not s: + return True + try: + float_value = float(s) + if float_value >= 0.0: + return True + return False + except ValueError: + return False diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 521edc98..7d908476 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -5,7 +5,6 @@ from tkinter import filedialog, font, ttk from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 -from coretk import validation INT_TYPES = { core_pb2.ConfigOptionType.UINT8, @@ -75,8 +74,6 @@ class ConfigFrame(FrameScroll): padx = 2 pady = 2 group_mapping = {} - vcmd_int = self.app.master.register(validation.check_positive_int) - vcmd_float = self.app.master.register(validation.check_positive_float) for key in self.config: option = self.config[key] group = group_mapping.setdefault(option.group, []) @@ -128,8 +125,9 @@ class ConfigFrame(FrameScroll): frame, textvariable=value, validate="key", - validatecommand=(vcmd_int, "%P"), + validatecommand=(self.app.validation.positive_int, "%P"), ) + entry.bind("", self.app.validation.focus_out) entry.grid(row=index, column=1, sticky="ew", pady=pady) elif option.type == core_pb2.ConfigOptionType.FLOAT: value.set(option.value) @@ -137,8 +135,9 @@ class ConfigFrame(FrameScroll): frame, textvariable=value, validate="key", - validatecommand=(vcmd_float, "%P"), + validatecommand=(self.app.validation.positive_float, "%P"), ) + entry.bind("", self.app.validation.focus_out) entry.grid(row=index, column=1, sticky="ew", pady=pady) else: logging.error("unhandled config option type: %s", option.type) From c4a117a2366b42845ba5f2c8462e80f5668cb2f1 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 10 Dec 2019 13:23:03 -0800 Subject: [PATCH 353/462] insert default value to some entry when entry is empty --- coretk/coretk/coreclient.py | 42 +++++-------------- coretk/coretk/dialogs/canvassizeandscale.py | 20 ++++----- coretk/coretk/dialogs/nodeconfig.py | 6 ++- coretk/coretk/dialogs/servers.py | 10 ++++- coretk/coretk/dialogs/serviceconfiguration.py | 1 - coretk/coretk/graph/node.py | 8 +++- coretk/coretk/validation.py | 17 +++++--- coretk/coretk/widgets.py | 10 ++++- 8 files changed, 61 insertions(+), 53 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 8738c4f7..ad85b689 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -70,8 +70,6 @@ class CoreClient: # helpers self.interface_to_edge = {} self.interfaces_manager = InterfaceManager(self.app) - self.created_nodes = set() - self.created_links = set() # session data self.state = None @@ -90,8 +88,6 @@ class CoreClient: def reset(self): # helpers - self.created_nodes.clear() - self.created_links.clear() self.interfaces_manager.reset() self.interface_to_edge.clear() # session data @@ -181,7 +177,6 @@ class CoreClient: canvas_node.move(x, y, update=False) def handle_throughputs(self, event): - # print(event.interface_throughputs) if self.throughput: self.app.canvas.throughput_draw.process_grpc_throughput_event( event.interface_throughputs @@ -243,8 +238,6 @@ class CoreClient: # save and retrieve data, needed for session nodes for node in session.nodes: # get node service config and file config - self.created_nodes.add(node.id) - # get wlan configs for wlan nodes if node.type == core_pb2.NodeType.WIRELESS_LAN: response = self.client.get_wlan_config(self.session_id, node.id) @@ -435,8 +428,6 @@ class CoreClient: hooks = list(self.hooks.values()) service_configs = self.get_service_config_proto() file_configs = self.get_service_file_config_proto() - self.created_links.clear() - self.created_nodes.clear() if self.emane_config: emane_config = {x: self.emane_config[x].value for x in self.emane_config} else: @@ -585,31 +576,20 @@ class CoreClient: self.session_id, core_pb2.SessionState.DEFINITION ) - # temp self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION) for node_proto in node_protos: - if node_proto.id not in self.created_nodes or True: - response = self.client.add_node(self.session_id, node_proto) - logging.debug("create node: %s", response) - self.created_nodes.add(node_proto.id) + response = self.client.add_node(self.session_id, node_proto) + logging.debug("create node: %s", response) for link_proto in link_protos: - if ( - tuple([link_proto.node_one_id, link_proto.node_two_id]) - not in self.created_links - or True - ): - response = self.client.add_link( - self.session_id, - link_proto.node_one_id, - link_proto.node_two_id, - link_proto.interface_one, - link_proto.interface_two, - link_proto.options, - ) - logging.debug("create link: %s", response) - self.created_links.add( - tuple([link_proto.node_one_id, link_proto.node_two_id]) - ) + response = self.client.add_link( + self.session_id, + link_proto.node_one_id, + link_proto.node_two_id, + link_proto.interface_one, + link_proto.interface_two, + link_proto.options, + ) + logging.debug("create link: %s", response) def close(self): """ diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 624e4246..f2bf4fc3 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -66,7 +66,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_int, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="x Height") label.grid(row=0, column=2, sticky="w", padx=PAD) @@ -76,7 +76,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_int, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Pixels") label.grid(row=0, column=4, sticky="w") @@ -94,7 +94,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_float, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="x Height") label.grid(row=0, column=2, sticky="w", padx=PAD) @@ -104,7 +104,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_float, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=4, sticky="w") @@ -125,7 +125,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_float, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=2, sticky="w") @@ -153,7 +153,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_float, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Y") @@ -164,7 +164,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_float, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(label_frame, text="Translates To") @@ -184,7 +184,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_float, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Lon") @@ -195,7 +195,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_float, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Alt") @@ -206,7 +206,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_float, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=5, sticky="ew") def draw_save_as_default(self): diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 9f7c4d6c..8853e908 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -75,7 +75,9 @@ class NodeConfigDialog(Dialog): validate="key", validatecommand=(self.app.validation.name, "%P"), ) - entry.bind("", self.app.validation.name_focus_out) + entry.bind( + "", lambda event: self.app.validation.focus_out(event, "noname") + ) entry.grid(row=row, column=1, sticky="ew") row += 1 @@ -165,12 +167,14 @@ class NodeConfigDialog(Dialog): label.grid(row=1, column=0, padx=PAD, pady=PAD) ip4 = tk.StringVar(value=f"{interface.ip4}/{interface.ip4mask}") entry = ttk.Entry(frame, textvariable=ip4) + entry.bind("", self.app.validation.ip_focus_out) entry.grid(row=1, column=1, columnspan=2, sticky="ew") label = ttk.Label(frame, text="IPv6") label.grid(row=2, column=0, padx=PAD, pady=PAD) ip6 = tk.StringVar(value=f"{interface.ip6}/{interface.ip6mask}") entry = ttk.Entry(frame, textvariable=ip6) + entry.bind("", self.app.validation.ip_focus_out) entry.grid(row=2, column=1, columnspan=2, sticky="ew") self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6) diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index 10a6e79e..9df7bf55 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -72,7 +72,15 @@ class ServersDialog(Dialog): label = ttk.Label(frame, text="Port") label.grid(row=0, column=4, sticky="w") - entry = ttk.Entry(frame, textvariable=self.port) + entry = ttk.Entry( + frame, + textvariable=self.port, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.bind( + "", lambda event: self.app.validation.focus_out(event, "50051") + ) entry.grid(row=0, column=5, sticky="ew") def draw_servers_buttons(self): diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 354937e5..01610eb1 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -51,7 +51,6 @@ class ServiceConfiguration(Dialog): def load(self): try: - # create nodes and links in definition state for getting and setting service file self.app.core.create_nodes_and_links() service_configs = self.app.core.service_configs if ( diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index ede26440..bfc11cbf 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -8,6 +8,7 @@ from core.api.grpc.core_pb2 import NodeType from coretk.dialogs.emaneconfig import EmaneConfigDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog +from coretk.dialogs.nodeservice import NodeService from coretk.dialogs.wlanconfig import WlanConfigDialog from coretk.errors import show_grpc_error from coretk.graph import tags @@ -217,7 +218,7 @@ class CanvasNode: else: context.add_command(label="Configure", command=self.show_config) if NodeUtils.is_container_node(self.core_node.type): - context.add_command(label="Services", state=tk.DISABLED) + context.add_command(label="Services", command=self.show_services) if is_emane: context.add_command( label="EMANE Config", command=self.show_emane_config @@ -268,3 +269,8 @@ class CanvasNode: self.canvas.context = None dialog = EmaneConfigDialog(self.app, self.app, self) dialog.show() + + def show_services(self): + self.canvas.context = None + dialog = NodeService(self.app.master, self.app, self) + dialog.show() diff --git a/coretk/coretk/validation.py b/coretk/coretk/validation.py index a580c712..403e073d 100644 --- a/coretk/coretk/validation.py +++ b/coretk/coretk/validation.py @@ -4,6 +4,9 @@ input validation import logging import tkinter as tk +import netaddr +from netaddr import IPNetwork + class InputValidation: def __init__(self, app): @@ -18,16 +21,18 @@ class InputValidation: self.positive_float = self.master.register(self.check_positive_float) self.name = self.master.register(self.check_node_name) - def focus_out(self, event): + def ip_focus_out(self, event): value = event.widget.get() - if value == "": - event.widget.insert(tk.END, 0) + try: + IPNetwork(value) + except netaddr.core.AddrFormatError: + event.widget.delete(0, tk.END) + event.widget.insert(tk.END, "invalid") - def name_focus_out(self, event): - logging.debug("name focus out") + def focus_out(self, event, default): value = event.widget.get() if value == "": - event.widget.insert(tk.END, "empty") + event.widget.insert(tk.END, default) def check_positive_int(self, s): logging.debug("int validation...") diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 7d908476..68983210 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -127,7 +127,10 @@ class ConfigFrame(FrameScroll): validate="key", validatecommand=(self.app.validation.positive_int, "%P"), ) - entry.bind("", self.app.validation.focus_out) + entry.bind( + "", + lambda event: self.app.validation.focus_out(event, "0"), + ) entry.grid(row=index, column=1, sticky="ew", pady=pady) elif option.type == core_pb2.ConfigOptionType.FLOAT: value.set(option.value) @@ -137,7 +140,10 @@ class ConfigFrame(FrameScroll): validate="key", validatecommand=(self.app.validation.positive_float, "%P"), ) - entry.bind("", self.app.validation.focus_out) + entry.bind( + "", + lambda event: self.app.validation.focus_out(event, "0"), + ) entry.grid(row=index, column=1, sticky="ew", pady=pady) else: logging.error("unhandled config option type: %s", option.type) From 51163a30d3d9bb31f4fdb34589ffd221424c89b6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 10 Dec 2019 14:33:52 -0800 Subject: [PATCH 354/462] updates to handling movement for nodes/shapes on canvas, added initial canvas zoom/panning, fixed some issues with mobility player event handling when dialog is closed --- coretk/coretk/app.py | 9 +- coretk/coretk/coreclient.py | 9 +- coretk/coretk/dialogs/mobilityplayer.py | 15 ++- coretk/coretk/graph/graph.py | 133 +++++++++++++++--------- coretk/coretk/graph/node.py | 93 +++++------------ coretk/coretk/graph/shape.py | 17 +-- 6 files changed, 128 insertions(+), 148 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 4d51cd1e..2c4826a9 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -69,14 +69,7 @@ class Application(tk.Frame): def draw_canvas(self): width = self.guiconfig["preferences"]["width"] height = self.guiconfig["preferences"]["height"] - self.canvas = CanvasGraph( - self, - self.core, - width, - height, - background="#cccccc", - scrollregion=(0, 0, 1200, 1000), - ) + self.canvas = CanvasGraph(self, self.core, width, height) self.canvas.pack(fill=tk.BOTH, expand=True) scroll_x = ttk.Scrollbar( self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 1b1a30da..5ba2384a 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -177,7 +177,7 @@ class CoreClient: x = event.node.position.x y = event.node.position.y canvas_node = self.canvas_nodes[node_id] - canvas_node.move(x, y, update=False) + canvas_node.move(x, y) def handle_throughputs(self, event): # interface_throughputs = event.interface_throughputs @@ -429,10 +429,11 @@ class CoreClient: show_grpc_error(e) self.app.close() - def edit_node(self, node_id, x, y): - position = core_pb2.Position(x=x, y=y) + def edit_node(self, core_node): try: - self.client.edit_node(self.session_id, node_id, position, source="gui") + self.client.edit_node( + self.session_id, core_node.id, core_node.position, source="gui" + ) except grpc.RpcError as e: show_grpc_error(e) diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index 6c9799b9..63698644 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -27,6 +27,7 @@ class MobilityPlayer: self.dialog = MobilityPlayerDialog( self.master, self.app, self.canvas_node, self.config ) + self.dialog.protocol("WM_DELETE_WINDOW", self.handle_close) if self.state == MobilityAction.START: self.set_play() elif self.state == MobilityAction.PAUSE: @@ -35,17 +36,24 @@ class MobilityPlayer: self.set_stop() self.dialog.show() + def handle_close(self): + self.dialog.destroy() + self.dialog = None + def set_play(self): - self.dialog.set_play() self.state = MobilityAction.START + if self.dialog: + self.dialog.set_play() def set_pause(self): - self.dialog.set_pause() self.state = MobilityAction.PAUSE + if self.dialog: + self.dialog.set_pause() def set_stop(self): - self.dialog.set_stop() self.state = MobilityAction.STOP + if self.dialog: + self.dialog.set_stop() class MobilityPlayerDialog(Dialog): @@ -92,7 +100,6 @@ class MobilityPlayerDialog(Dialog): 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=PAD) - self.stop_button.state(["pressed"]) loop = tk.IntVar(value=int(self.config["loop"].value == "1")) checkbutton = ttk.Checkbutton( diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index c2c0935d..612088a0 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -15,14 +15,21 @@ from coretk.graph.shapeutils import is_draw_shape from coretk.images import Images from coretk.nodeutils import NodeUtils +SCROLL_BUFFER = 25 +ZOOM_IN = 1.1 +ZOOM_OUT = 0.9 + class CanvasGraph(tk.Canvas): - def __init__(self, master, core, width, height, cnf=None, **kwargs): - if cnf is None: - cnf = {} - kwargs["highlightthickness"] = 0 - super().__init__(master, cnf, **kwargs) + def __init__(self, master, core, width, height): + super().__init__( + master, + highlightthickness=0, + background="#cccccc", + scrollregion=(0, 0, width + SCROLL_BUFFER, height + SCROLL_BUFFER), + ) self.app = master + self.core = core self.mode = GraphMode.SELECT self.annotation_type = None self.selection = {} @@ -35,12 +42,13 @@ class CanvasGraph(tk.Canvas): self.wireless_edges = {} self.drawing_edge = None self.grid = None - self.setup_bindings() - self.core = core self.throughput_draw = Throughput(self, core) self.shape_drawing = False self.default_width = width self.default_height = height + self.ratio = 1.0 + self.offset = (0, 0) + self.cursor = (0, 0) # background related self.wallpaper_id = None @@ -51,6 +59,9 @@ class CanvasGraph(tk.Canvas): self.show_grid = tk.BooleanVar(value=True) self.adjust_to_dim = tk.BooleanVar(value=False) + # bindings + self.setup_bindings() + # draw base canvas self.draw_canvas() self.draw_grid() @@ -99,17 +110,19 @@ class CanvasGraph(tk.Canvas): self.bind("", self.click_press) self.bind("", self.click_release) self.bind("", self.click_motion) - self.bind("", self.click_context) + self.bind("", self.click_context) self.bind("", self.press_delete) self.bind("", self.ctrl_click) self.bind("", self.double_click) + self.bind("", self.zoom) + self.bind("", lambda e: self.zoom(e, ZOOM_IN)) + self.bind("", lambda e: self.zoom(e, ZOOM_OUT)) + self.bind("", lambda e: self.scan_mark(e.x, e.y)) + self.bind("", lambda e: self.scan_dragto(e.x, e.y, gain=1)) def draw_grid(self): """ - Create grid - - :param int width: the width - :param int height: the height + Create grid. :return: nothing """ @@ -213,7 +226,8 @@ class CanvasGraph(tk.Canvas): :rtype: int :return: the item that the mouse point to """ - overlapping = self.find_overlapping(event.x, event.y, event.x, event.y) + x, y = self.canvas_xy(event) + overlapping = self.find_overlapping(x, y, x, y) selected = None for _id in overlapping: if self.drawing_edge and self.drawing_edge.id == _id: @@ -330,10 +344,10 @@ class CanvasGraph(tk.Canvas): self.delete(_id) self.selection.clear() - def object_drag(self, object_id, offset_x, offset_y): + def move_selection(self, object_id, x_offset, y_offset): select_id = self.selection.get(object_id) if select_id is not None: - self.move(select_id, offset_x, offset_y) + self.move(select_id, x_offset, y_offset) def delete_selection_objects(self): edges = set() @@ -382,6 +396,20 @@ class CanvasGraph(tk.Canvas): self.selection.clear() return nodes + def zoom(self, event, factor=None): + if not factor: + factor = ZOOM_IN if event.delta > 0 else ZOOM_OUT + event.x, event.y = self.canvasx(event.x), self.canvasy(event.y) + self.scale("all", event.x, event.y, factor, factor) + self.configure(scrollregion=self.bbox("all")) + self.ratio *= float(factor) + self.offset = ( + self.offset[0] * factor + event.x * (1 - factor), + self.offset[1] * factor + event.y * (1 - factor), + ) + logging.info("ratio: %s", self.ratio) + logging.info("offset: %s", self.offset) + def click_press(self, event): """ Start drawing an edge if mouse click is on a node @@ -389,40 +417,46 @@ class CanvasGraph(tk.Canvas): :param event: mouse event :return: nothing """ - logging.debug(f"click press: {event}") + x, y = self.canvas_xy(event) + self.cursor = x, y selected = self.get_selected(event) + logging.debug(f"click press: %s", selected) is_node = selected in self.nodes if self.mode == GraphMode.EDGE and is_node: x, y = self.coords(selected) self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) if self.mode == GraphMode.ANNOTATION and selected is None: - x, y = self.canvas_xy(event) shape = Shape(self.app, self, self.annotation_type, x, y) self.selected = shape.id self.shape_drawing = True self.shapes[shape.id] = shape - if self.mode == GraphMode.SELECT: - if selected is not None: + if selected is not None: + if selected not in self.selection: if selected in self.shapes: - x, y = self.canvas_xy(event) shape = self.shapes[selected] - shape.cursor_x = x - shape.cursor_y = y - if selected not in self.selection: - self.select_object(shape.id) + self.select_object(shape.id) self.selected = selected - else: - self.clear_selection() + elif selected in self.nodes: + node = self.nodes[selected] + self.select_object(node.id) + self.selected = selected + else: + self.clear_selection() def ctrl_click(self, event): + # update cursor location + x, y = self.canvas_xy(event) + self.cursor = x, y + + # handle multiple selections logging.debug("control left click: %s", event) selected = self.get_selected(event) if ( - self.mode == GraphMode.SELECT - and selected is not None + selected not in self.selection and selected in self.shapes + or selected in self.nodes ): self.select_object(selected, choose_multiple=True) @@ -433,36 +467,31 @@ class CanvasGraph(tk.Canvas): :param event: mouse event :return: nothing """ + x, y = self.canvas_xy(event) + x_offset = x - self.cursor[0] + y_offset = y - self.cursor[1] + self.cursor = x, y + if self.mode == GraphMode.EDGE and self.drawing_edge is not None: - x2, y2 = self.canvas_xy(event) x1, y1, _, _ = self.coords(self.drawing_edge.id) - self.coords(self.drawing_edge.id, x1, y1, x2, y2) + self.coords(self.drawing_edge.id, x1, y1, x, y) if self.mode == GraphMode.ANNOTATION: if is_draw_shape(self.annotation_type) and self.shape_drawing: - x, y = self.canvas_xy(event) shape = self.shapes[self.selected] shape.shape_motion(x, y) - if ( - self.mode == GraphMode.SELECT - and self.selected is not None - and self.selected in self.shapes - ): - x, y = self.canvas_xy(event) - shape = self.shapes[self.selected] - delta_x = x - shape.cursor_x - delta_y = y - shape.cursor_y - shape.motion(event) - # move other selected components - for _id in self.selection: - if _id != self.selected and _id in self.shapes: - shape = self.shapes[_id] - shape.motion(None, delta_x, delta_y) - if _id != self.selected and _id in self.nodes: - node = self.nodes[_id] - node_x = node.core_node.position.x - node_y = node.core_node.position.y - node.move(node_x + delta_x, node_y + delta_y) + if self.mode == GraphMode.EDGE: + return + + # move selected objects + for selected_id in self.selection: + if selected_id in self.shapes: + shape = self.shapes[selected_id] + shape.motion(x_offset, y_offset) + + if selected_id in self.nodes: + node = self.nodes[selected_id] + node.motion(x_offset, y_offset, update=self.core.is_runtime()) def click_context(self, event): logging.info("context event: %s", self.context) @@ -592,7 +621,7 @@ class CanvasGraph(tk.Canvas): :return: nothing """ # resize canvas and scrollregion - self.config(scrollregion=(0, 0, width + 200, height + 200)) + self.config(scrollregion=(0, 0, width + SCROLL_BUFFER, height + SCROLL_BUFFER)) self.coords(self.grid, 0, 0, width, height) # redraw gridlines to new canvas size diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 79401c04..acd03de3 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -1,4 +1,3 @@ -import logging import tkinter as tk from tkinter import font @@ -11,7 +10,6 @@ from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog from coretk.errors import show_grpc_error from coretk.graph import tags -from coretk.graph.enums import GraphMode from coretk.graph.tooltip import CanvasTooltip from coretk.nodeutils import NodeUtils @@ -29,12 +27,11 @@ class CanvasNode: self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE ) - image_box = self.canvas.bbox(self.id) - y = image_box[3] + NODE_TEXT_OFFSET text_font = font.Font(family="TkIconFont", size=12) + label_y = self._get_label_y() self.text_id = self.canvas.create_text( x, - y, + label_y, text=self.core_node.name, tags=tags.NODE_NAME, font=text_font, @@ -44,17 +41,11 @@ class CanvasNode: self.edges = set() self.interfaces = [] self.wireless_edges = set() - self.moving = None self.antennae = [] self.setup_bindings() def setup_bindings(self): - # self.canvas.bind("", self.click_context) - self.canvas.tag_bind(self.id, "", self.click_press) - self.canvas.tag_bind(self.id, "", self.click_release) - self.canvas.tag_bind(self.id, "", self.motion) self.canvas.tag_bind(self.id, "", self.double_click) - self.canvas.tag_bind(self.id, "", self.select_multiple) self.canvas.tag_bind(self.id, "", self.on_enter) self.canvas.tag_bind(self.id, "", self.on_leave) @@ -95,30 +86,32 @@ class CanvasNode: self.canvas.delete(antenna_id) self.antennae.clear() - def move_antennae(self, x_offset, y_offset): - """ - redraw antennas of a node according to the new node position - - :return: nothing - """ - for antenna_id in self.antennae: - self.canvas.move(antenna_id, x_offset, y_offset) - def redraw(self): self.canvas.itemconfig(self.id, image=self.image) self.canvas.itemconfig(self.text_id, text=self.core_node.name) - def move(self, x, y, update=True): - old_x = self.core_node.position.x - old_y = self.core_node.position.y - x_offset = x - old_x - y_offset = y - old_y - self.core_node.position.x = int(x) - self.core_node.position.y = int(y) + def _get_label_y(self): + image_box = self.canvas.bbox(self.id) + return image_box[3] + NODE_TEXT_OFFSET + + def move(self, x, y): + x_offset = x - self.core_node.position.x + y_offset = y - self.core_node.position.y + self.motion(x_offset, y_offset, update=False) + + def motion(self, x_offset, y_offset, update=True): self.canvas.move(self.id, x_offset, y_offset) self.canvas.move(self.text_id, x_offset, y_offset) - self.move_antennae(x_offset, y_offset) - self.canvas.object_drag(self.id, x_offset, y_offset) + self.canvas.move_selection(self.id, x_offset, y_offset) + x, y = self.canvas.coords(self.id) + self.core_node.position.x = int(x) + self.core_node.position.y = int(y) + + # move antennae + for antenna_id in self.antennae: + self.canvas.move(antenna_id, x_offset, y_offset) + + # move edges for edge in self.edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) if edge.src == self.id: @@ -126,16 +119,18 @@ class CanvasNode: else: self.canvas.coords(edge.id, x1, y1, x, y) self.canvas.throughput_draw.move(edge) - edge.link_info.recalculate_info() + for edge in self.wireless_edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) if edge.src == self.id: self.canvas.coords(edge.id, x, y, x2, y2) else: self.canvas.coords(edge.id, x1, y1, x, y) + + # update core with new location if self.app.core.is_runtime() and update: - self.app.core.edit_node(self.core_node.id, int(x), int(y)) + self.app.core.edit_node(self.core_node) def on_enter(self, event): if self.app.core.is_runtime() and self.app.core.observer: @@ -164,39 +159,6 @@ class CanvasNode: self.core_node.position.x = int(x) self.core_node.position.y = int(y) - def click_press(self, event): - logging.debug(f"node click press {self.core_node.name}: {event}") - self.moving = self.canvas.canvas_xy(event) - if self.id not in self.canvas.selection: - self.canvas.select_object(self.id) - self.canvas.selected = self.id - - def click_release(self, event): - logging.debug(f"node click release {self.core_node.name}: {event}") - self.update_coords() - self.moving = None - - def motion(self, event): - if self.canvas.mode == GraphMode.EDGE: - return - x, y = self.canvas.canvas_xy(event) - my_x = self.core_node.position.x - my_y = self.core_node.position.y - self.move(x, y) - - # move other selected components - for object_id, selection_id in self.canvas.selection.items(): - if object_id != self.id and object_id in self.canvas.nodes: - canvas_node = self.canvas.nodes[object_id] - other_old_x = canvas_node.core_node.position.x - other_old_y = canvas_node.core_node.position.y - other_new_x = x + other_old_x - my_x - other_new_y = y + other_old_y - my_y - self.canvas.nodes[object_id].move(other_new_x, other_new_y) - elif object_id in self.canvas.shapes: - shape = self.canvas.shapes[object_id] - shape.motion(None, x - my_x, y - my_y) - def create_context(self): is_wlan = self.core_node.type == NodeType.WIRELESS_LAN is_emane = self.core_node.type == NodeType.EMANE @@ -244,9 +206,6 @@ class CanvasNode: context.add_command(label="Hide", state=tk.DISABLED) return context - def select_multiple(self, event): - self.canvas.select_object(self.id, choose_multiple=True) - def show_config(self): self.canvas.context = None dialog = NodeConfigDialog(self.app, self.app, self) diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 2928a695..56e679d2 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -49,13 +49,9 @@ class Shape: if data is None: self.created = False self.shape_data = AnnotationData() - self.cursor_x = x1 - self.cursor_y = y1 else: self.created = True self.shape_data = data - self.cursor_x = None - self.cursor_y = None self.draw() def draw(self): @@ -136,16 +132,11 @@ class Shape: s = ShapeDialog(self.app, self.app, self) s.show() - def motion(self, event, delta_x=None, delta_y=None): - if event is not None: - delta_x = event.x - self.cursor_x - delta_y = event.y - self.cursor_y - self.cursor_x = event.x - self.cursor_y = event.y - self.canvas.move(self.id, delta_x, delta_y) - self.canvas.object_drag(self.id, delta_x, delta_y) + def motion(self, x_offset, y_offset): + self.canvas.move(self.id, x_offset, y_offset) + self.canvas.move_selection(self.id, x_offset, y_offset) if self.text_id is not None: - self.canvas.move(self.text_id, delta_x, delta_y) + self.canvas.move(self.text_id, x_offset, y_offset) def delete(self): self.canvas.delete(self.id) From 0c61c6bffe8025cbb25ca3b2ab95fba7d52ae618 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 10 Dec 2019 16:31:01 -0800 Subject: [PATCH 355/462] updates to wallpaper drawing and redrawing on zoom --- coretk/coretk/dialogs/canvassizeandscale.py | 2 +- coretk/coretk/graph/graph.py | 100 ++++++++++---------- 2 files changed, 52 insertions(+), 50 deletions(-) diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 624e4246..b18de7e8 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -231,7 +231,7 @@ class SizeAndScaleDialog(Dialog): width, height = self.pixel_width.get(), self.pixel_height.get() self.canvas.redraw_canvas(width, height) if self.canvas.wallpaper: - self.canvas.redraw() + self.canvas.redraw_wallpaper() location = self.app.core.location location.x = self.x.get() location.y = self.y.get() diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 1091905c..9f9c4d99 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -410,6 +410,7 @@ class CanvasGraph(tk.Canvas): ) logging.info("ratio: %s", self.ratio) logging.info("offset: %s", self.offset) + self.redraw_wallpaper() def click_press(self, event): """ @@ -545,24 +546,30 @@ class CanvasGraph(tk.Canvas): canvas_h = abs(y0 - y1) return canvas_w, canvas_h - def wallpaper_upper_left(self): - tk_img = ImageTk.PhotoImage(self.wallpaper) - # crop image if it is bigger than canvas - canvas_w, canvas_h = self.width_and_height() - cropx = img_w = tk_img.width() - cropy = img_h = tk_img.height() - if img_w > canvas_w: - cropx -= img_w - canvas_w - if img_h > canvas_h: - cropy -= img_h - canvas_h - cropped = self.wallpaper.crop((0, 0, cropx, cropy)) - cropped_tk = ImageTk.PhotoImage(cropped) - self.delete(self.wallpaper_id) - # place left corner of image to the left corner of the canvas + def draw_wallpaper(self, image): + x1, y1, x2, y2 = self.bbox(self.grid) + x = (x1 + x2) / 2 + y = (y1 + y2) / 2 self.wallpaper_id = self.create_image( - (cropx / 2, cropy / 2), image=cropped_tk, tags=tags.WALLPAPER + (x + 1, y + 1), image=image, tags=tags.WALLPAPER ) - self.wallpaper_drawn = cropped_tk + self.wallpaper_drawn = image + + def wallpaper_upper_left(self): + self.delete(self.wallpaper_id) + + # place left corner of image to the left corner of the canvas + tk_img = ImageTk.PhotoImage(self.wallpaper) + width, height = self.width_and_height() + cropx = image_width = tk_img.width() + cropy = image_height = tk_img.height() + if image_width > width: + cropx = width + if image_height > height: + cropy = height + cropped = self.wallpaper.crop((0, 0, cropx, cropy)) + image = ImageTk.PhotoImage(cropped) + self.draw_wallpaper(image) def wallpaper_center(self): """ @@ -570,27 +577,26 @@ class CanvasGraph(tk.Canvas): :return: nothing """ - tk_img = ImageTk.PhotoImage(self.wallpaper) - canvas_w, canvas_h = self.width_and_height() - cropx = img_w = tk_img.width() - cropy = img_h = tk_img.height() - # dimension of the cropped image - if img_w > canvas_w: - cropx -= img_w - canvas_w - if img_h > canvas_h: - cropy -= img_h - canvas_h - x0 = (img_w - cropx) / 2 - y0 = (img_h - cropy) / 2 - x1 = x0 + cropx - y1 = y0 + cropy - cropped = self.wallpaper.crop((x0, y0, x1, y1)) - cropped_tk = ImageTk.PhotoImage(cropped) - # place the center of the image at the center of the canvas self.delete(self.wallpaper_id) - self.wallpaper_id = self.create_image( - (canvas_w / 2, canvas_h / 2), image=cropped_tk, tags=tags.WALLPAPER - ) - self.wallpaper_drawn = cropped_tk + + # dimension of the cropped image + tk_img = ImageTk.PhotoImage(self.wallpaper) + width, height = self.width_and_height() + image_width = tk_img.width() + image_height = tk_img.height() + cropx = 0 + if image_width > width: + cropx = (image_width - width) / 2 + cropy = 0 + if image_height > height: + cropy = (image_height - height) / 2 + x1 = 0 + cropx + y1 = 0 + cropy + x2 = image_width - cropx + y2 = image_height - cropy + cropped = self.wallpaper.crop((x1, y1, x2, y2)) + image = ImageTk.PhotoImage(cropped) + self.draw_wallpaper(image) def wallpaper_scaled(self): """ @@ -598,22 +604,18 @@ class CanvasGraph(tk.Canvas): :return: nothing """ + self.delete(self.wallpaper_id) canvas_w, canvas_h = self.width_and_height() image = Images.create(self.wallpaper_file, int(canvas_w), int(canvas_h)) - self.delete(self.wallpaper_id) - self.wallpaper_id = self.create_image( - (canvas_w / 2, canvas_h / 2), image=image, tags=tags.WALLPAPER - ) - self.wallpaper_drawn = image + self.draw_wallpaper(image) def resize_to_wallpaper(self): - image_tk = ImageTk.PhotoImage(self.wallpaper) - img_w = image_tk.width() - img_h = image_tk.height() self.delete(self.wallpaper_id) - self.redraw_canvas(img_w, img_h) - self.wallpaper_id = self.create_image((img_w / 2, img_h / 2), image=image_tk) - self.wallpaper_drawn = image_tk + image = ImageTk.PhotoImage(self.wallpaper) + image_width = image.width() + image_height = image.height() + self.redraw_canvas(image_width, image_height) + self.draw_wallpaper(image) def redraw_canvas(self, width, height): """ @@ -630,7 +632,7 @@ class CanvasGraph(tk.Canvas): self.draw_grid() self.update_grid() - def redraw(self): + def redraw_wallpaper(self): if self.adjust_to_dim.get(): self.resize_to_wallpaper() else: @@ -662,7 +664,7 @@ class CanvasGraph(tk.Canvas): img = Image.open(filename) self.wallpaper = img self.wallpaper_file = filename - self.redraw() + self.redraw_wallpaper() else: if self.wallpaper_id is not None: self.delete(self.wallpaper_id) From 6a42748191f07e8bd3611d564641d3b06fa17125 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 10 Dec 2019 16:50:28 -0800 Subject: [PATCH 356/462] controlnet validation --- coretk/coretk/validation.py | 24 ++++++++++++++++++------ coretk/coretk/widgets.py | 14 ++++++++++++-- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/coretk/coretk/validation.py b/coretk/coretk/validation.py index 403e073d..68fc79c3 100644 --- a/coretk/coretk/validation.py +++ b/coretk/coretk/validation.py @@ -1,7 +1,7 @@ """ input validation """ -import logging +import re import tkinter as tk import netaddr @@ -14,12 +14,14 @@ class InputValidation: self.positive_int = None self.positive_float = None self.name = None + self.ip4 = None self.register() def register(self): self.positive_int = self.master.register(self.check_positive_int) self.positive_float = self.master.register(self.check_positive_float) self.name = self.master.register(self.check_node_name) + self.ip4 = self.master.register(self.check_ip4) def ip_focus_out(self, event): value = event.widget.get() @@ -35,7 +37,6 @@ class InputValidation: event.widget.insert(tk.END, default) def check_positive_int(self, s): - logging.debug("int validation...") if len(s) == 0: return True try: @@ -47,7 +48,6 @@ class InputValidation: return False def check_positive_float(self, s): - logging.debug("float validation...") if len(s) == 0: return True try: @@ -59,7 +59,6 @@ class InputValidation: return False def check_node_name(self, s): - logging.debug("node name validation...") if len(s) < 0: return False if len(s) == 0: @@ -70,7 +69,6 @@ class InputValidation: return True def check_canvas_int(sefl, s): - logging.debug("int validation...") if len(s) == 0: return True try: @@ -82,7 +80,6 @@ class InputValidation: return False def check_canvas_float(self, s): - logging.debug("canvas float validation") if not s: return True try: @@ -92,3 +89,18 @@ class InputValidation: return False except ValueError: return False + + def check_ip4(self, s): + if not s: + return True + pat = re.compile("^([0-9]+[.])*[0-9]*$") + if pat.match(s) is not None: + _32bits = s.split(".") + if len(_32bits) > 4: + return False + for _8bits in _32bits: + if (_8bits and int(_8bits) > 255) or len(_8bits) > 3: + return False + return True + else: + return False diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 68983210..fb5582a1 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -117,8 +117,18 @@ class ConfigFrame(FrameScroll): button = ttk.Button(file_frame, text="...", command=func) button.grid(row=0, column=1) else: - entry = ttk.Entry(frame, textvariable=value) - entry.grid(row=index, column=1, sticky="ew", pady=pady) + if "controlnet" in option.name and "script" not in option.name: + entry = ttk.Entry( + frame, + textvariable=value, + validate="key", + validatecommand=(self.app.validation.ip4, "%P"), + ) + entry.grid(row=index, column=1, sticky="ew", pady=pady) + else: + entry = ttk.Entry(frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pady) + elif option.type in INT_TYPES: value.set(option.value) entry = ttk.Entry( From 737c14cc0f269cf29ecf4b126a266a3ee2977a1f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 10 Dec 2019 19:25:00 -0800 Subject: [PATCH 357/462] updated canvas wallpaper to scale with zoom level --- coretk/coretk/graph/graph.py | 76 ++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 9f9c4d99..fc5d6fd7 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -12,7 +12,6 @@ from coretk.graph.linkinfo import LinkInfo, Throughput from coretk.graph.node import CanvasNode from coretk.graph.shape import Shape from coretk.graph.shapeutils import is_draw_shape -from coretk.images import Images from coretk.nodeutils import NodeUtils SCROLL_BUFFER = 25 @@ -410,7 +409,8 @@ class CanvasGraph(tk.Canvas): ) logging.info("ratio: %s", self.ratio) logging.info("offset: %s", self.offset) - self.redraw_wallpaper() + if self.wallpaper: + self.redraw_wallpaper() def click_press(self, event): """ @@ -422,7 +422,7 @@ class CanvasGraph(tk.Canvas): x, y = self.canvas_xy(event) self.cursor = x, y selected = self.get_selected(event) - logging.debug(f"click press: %s", selected) + logging.debug("click press: %s", selected) is_node = selected in self.nodes if self.mode == GraphMode.EDGE and is_node: x, y = self.coords(selected) @@ -546,30 +546,41 @@ class CanvasGraph(tk.Canvas): canvas_h = abs(y0 - y1) return canvas_w, canvas_h - def draw_wallpaper(self, image): - x1, y1, x2, y2 = self.bbox(self.grid) - x = (x1 + x2) / 2 - y = (y1 + y2) / 2 - self.wallpaper_id = self.create_image( - (x + 1, y + 1), image=image, tags=tags.WALLPAPER - ) + def get_wallpaper_image(self): + width = int(self.wallpaper.width * self.ratio) + height = int(self.wallpaper.height * self.ratio) + image = self.wallpaper.resize((width, height), Image.ANTIALIAS) + return image + + def draw_wallpaper(self, image, x=None, y=None): + if x is None and y is None: + x1, y1, x2, y2 = self.bbox(self.grid) + x = (x1 + x2) / 2 + y = (y1 + y2) / 2 + + self.wallpaper_id = self.create_image((x, y), image=image, tags=tags.WALLPAPER) self.wallpaper_drawn = image def wallpaper_upper_left(self): self.delete(self.wallpaper_id) - # place left corner of image to the left corner of the canvas - tk_img = ImageTk.PhotoImage(self.wallpaper) + # create new scaled image, cropped if needed width, height = self.width_and_height() - cropx = image_width = tk_img.width() - cropy = image_height = tk_img.height() - if image_width > width: - cropx = width - if image_height > height: - cropy = height - cropped = self.wallpaper.crop((0, 0, cropx, cropy)) + image = self.get_wallpaper_image() + cropx = image.width + cropy = image.height + if image.width > width: + cropx = image.width + if image.height > height: + cropy = image.height + cropped = image.crop((0, 0, cropx, cropy)) image = ImageTk.PhotoImage(cropped) - self.draw_wallpaper(image) + + # draw on canvas + x1, y1, _, _ = self.bbox(self.grid) + x = (cropx / 2) + x1 + y = (cropy / 2) + y1 + self.draw_wallpaper(image, x, y) def wallpaper_center(self): """ @@ -580,21 +591,19 @@ class CanvasGraph(tk.Canvas): self.delete(self.wallpaper_id) # dimension of the cropped image - tk_img = ImageTk.PhotoImage(self.wallpaper) width, height = self.width_and_height() - image_width = tk_img.width() - image_height = tk_img.height() + image = self.get_wallpaper_image() cropx = 0 - if image_width > width: - cropx = (image_width - width) / 2 + if image.width > width: + cropx = (image.width - width) / 2 cropy = 0 - if image_height > height: - cropy = (image_height - height) / 2 + if image.height > height: + cropy = (image.height - height) / 2 x1 = 0 + cropx y1 = 0 + cropy - x2 = image_width - cropx - y2 = image_height - cropy - cropped = self.wallpaper.crop((x1, y1, x2, y2)) + x2 = image.width - cropx + y2 = image.height - cropy + cropped = image.crop((x1, y1, x2, y2)) image = ImageTk.PhotoImage(cropped) self.draw_wallpaper(image) @@ -606,15 +615,14 @@ class CanvasGraph(tk.Canvas): """ self.delete(self.wallpaper_id) canvas_w, canvas_h = self.width_and_height() - image = Images.create(self.wallpaper_file, int(canvas_w), int(canvas_h)) + image = self.wallpaper.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS) + image = ImageTk.PhotoImage(image) self.draw_wallpaper(image) def resize_to_wallpaper(self): self.delete(self.wallpaper_id) image = ImageTk.PhotoImage(self.wallpaper) - image_width = image.width() - image_height = image.height() - self.redraw_canvas(image_width, image_height) + self.redraw_canvas(image.width(), image.height()) self.draw_wallpaper(image) def redraw_canvas(self, width, height): From 4cc83cf3135127dc394cab9b771f83c7830f7455 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 11 Dec 2019 09:17:39 -0800 Subject: [PATCH 358/462] cel --- coretk/coretk/coreclient.py | 6 ---- coretk/coretk/dialogs/cel.py | 29 +++++++++++++++++++ coretk/coretk/dialogs/serviceconfiguration.py | 3 +- coretk/coretk/statusbar.py | 7 +++++ coretk/coretk/validation.py | 6 +++- 5 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 coretk/coretk/dialogs/cel.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index c02a3295..827f4d01 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -261,12 +261,6 @@ class CoreClient: self.file_configs[node.id][service] = {} self.file_configs[node.id][service][file] = response.data - # store links as created links - for link in session.links: - self.created_links.add( - tuple(sorted([link.node_one_id, link.node_two_id])) - ) - # draw session self.app.canvas.reset_and_redraw(session) diff --git a/coretk/coretk/dialogs/cel.py b/coretk/coretk/dialogs/cel.py new file mode 100644 index 00000000..847d245b --- /dev/null +++ b/coretk/coretk/dialogs/cel.py @@ -0,0 +1,29 @@ +""" +check engine light +""" +from tkinter import ttk + +from coretk.dialogs.dialog import Dialog + + +class CheckLight(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "CEL", modal=True) + self.app = app + + self.columnconfigure(0, weight=1) + self.draw() + + def draw(self): + row = 0 + frame = ttk.Frame(self) + button = ttk.Button(frame, text="Reset CEL") + button.grid(row=0, column=0) + button = ttk.Button(frame, text="View core-daemon log") + button.grid(row=0, column=1) + button = ttk.Button(frame, text="View node log") + button.grid(row=0, column=2) + button = ttk.Button(frame, text="Close", command=self.destroy) + button.grid(row=0, column=3) + frame.grid(row=row, column=0, sticky="nsew") + ++row diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 01610eb1..812d1a6a 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -43,6 +43,7 @@ class ServiceConfiguration(Dialog): self.validation_time_entry = None self.validation_mode_entry = None self.service_file_data = None + self.validation_period_entry = None self.original_service_files = {} self.temp_service_files = {} self.modified_files = set() @@ -100,8 +101,6 @@ class ServiceConfiguration(Dialog): label.grid(row=0, column=0, sticky="ew") frame1.grid(row=0, column=0) frame2 = ttk.Frame(frame) - # frame2.columnconfigure(0, weight=1) - # frame2.columnconfigure(1, weight=4) label = ttk.Label(frame2, text="Meta-data") label.grid(row=0, column=0) diff --git a/coretk/coretk/statusbar.py b/coretk/coretk/statusbar.py index 8910c742..f45dce9f 100644 --- a/coretk/coretk/statusbar.py +++ b/coretk/coretk/statusbar.py @@ -2,6 +2,8 @@ import tkinter as tk from tkinter import ttk +from coretk.dialogs.cel import CheckLight + class StatusBar(ttk.Frame): def __init__(self, master, app, **kwargs): @@ -55,8 +57,13 @@ class StatusBar(ttk.Frame): self.emulation_light = ttk.Label( self, text="CEL TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE ) + self.emulation_light.bind("", self.cel_callback) self.emulation_light.grid(row=0, column=4, sticky="ew") + def cel_callback(self, event): + dialog = CheckLight(self.app, self.app) + dialog.show() + def start_session_callback(self, process_time): self.progress_bar.stop() self.statusvar.set(f"Session started in {process_time:.3f} seconds") diff --git a/coretk/coretk/validation.py b/coretk/coretk/validation.py index 68fc79c3..c7923229 100644 --- a/coretk/coretk/validation.py +++ b/coretk/coretk/validation.py @@ -99,7 +99,11 @@ class InputValidation: if len(_32bits) > 4: return False for _8bits in _32bits: - if (_8bits and int(_8bits) > 255) or len(_8bits) > 3: + if ( + (_8bits and int(_8bits) > 225) + or len(_8bits) > 3 + or (_8bits.startswith("0") and len(_8bits) > 1) + ): return False return True else: From 8585911900ddef76de0e9fa6df0940c200e9eac4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 11 Dec 2019 11:42:05 -0800 Subject: [PATCH 359/462] updated theme and size for picker buttons, also added text for clarity --- coretk/coretk/app.py | 2 ++ coretk/coretk/dialogs/dialog.py | 1 - coretk/coretk/nodeutils.py | 39 ++++++++++++++++----------------- coretk/coretk/themes.py | 5 +++++ coretk/coretk/toolbar.py | 31 ++++++++++++++++---------- 5 files changed, 45 insertions(+), 33 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index c2eeadc3..0a9275f1 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -42,6 +42,8 @@ class Application(tk.Frame): self.style.theme_use(self.guiconfig["preferences"]["theme"]) func = partial(themes.update_menu, self.style) self.master.bind_class("Menu", "<>", func) + func = partial(themes.theme_change, self.style) + self.master.bind("<>", func) def setup_app(self): self.master.title("CORE") diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index d814a47c..29362960 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -9,7 +9,6 @@ DIALOG_PAD = 5 class Dialog(tk.Toplevel): def __init__(self, master, app, title, modal=False): super().__init__(master) - self.geometry("800x600") self.withdraw() self.app = app self.modal = modal diff --git a/coretk/coretk/nodeutils.py b/coretk/coretk/nodeutils.py index 10d926f7..cb0bc7a3 100644 --- a/coretk/coretk/nodeutils.py +++ b/coretk/coretk/nodeutils.py @@ -13,18 +13,16 @@ class NodeDraw: self.image_file = None self.node_type = None self.model = None - self.tooltip = None self.services = set() @classmethod - def from_setup(cls, image_enum, node_type, model=None, tooltip=None): + def from_setup(cls, image_enum, node_type, label, model=None, tooltip=None): node_draw = NodeDraw() node_draw.image_enum = image_enum node_draw.image = Images.get(image_enum, ICON_SIZE) node_draw.node_type = node_type + node_draw.label = label node_draw.model = model - if tooltip is None: - tooltip = model node_draw.tooltip = tooltip return node_draw @@ -36,6 +34,7 @@ class NodeDraw: node_draw.image = Images.get_custom(image_file, ICON_SIZE) node_draw.node_type = NodeType.DEFAULT node_draw.services = services + node_draw.label = name node_draw.model = name node_draw.tooltip = name return node_draw @@ -81,29 +80,29 @@ class NodeUtils: @classmethod def setup(cls): nodes = [ - (ImageEnum.ROUTER, NodeType.DEFAULT, "router"), - (ImageEnum.HOST, NodeType.DEFAULT, "host"), - (ImageEnum.PC, NodeType.DEFAULT, "PC"), - (ImageEnum.MDR, NodeType.DEFAULT, "mdr"), - (ImageEnum.PROUTER, NodeType.DEFAULT, "prouter"), - (ImageEnum.DOCKER, NodeType.DOCKER, "Docker"), - (ImageEnum.LXC, NodeType.LXC, "LXC"), + (ImageEnum.ROUTER, NodeType.DEFAULT, "Router", "router"), + (ImageEnum.HOST, NodeType.DEFAULT, "Host", "host"), + (ImageEnum.PC, NodeType.DEFAULT, "PC", "PC"), + (ImageEnum.MDR, NodeType.DEFAULT, "MDR", "mdr"), + (ImageEnum.PROUTER, NodeType.DEFAULT, "PRouter", "prouter"), + (ImageEnum.DOCKER, NodeType.DOCKER, "Docker", None), + (ImageEnum.LXC, NodeType.LXC, "LXC", None), ] - for image_enum, node_type, model in nodes: - node_draw = NodeDraw.from_setup(image_enum, node_type, model) + for image_enum, node_type, label, model in nodes: + node_draw = NodeDraw.from_setup(image_enum, node_type, label, model) cls.NODES.append(node_draw) cls.NODE_ICONS[(node_type, model)] = node_draw.image network_nodes = [ - (ImageEnum.HUB, NodeType.HUB, "ethernet hub"), - (ImageEnum.SWITCH, NodeType.SWITCH, "ethernet switch"), - (ImageEnum.WLAN, NodeType.WIRELESS_LAN, "wireless LAN"), + (ImageEnum.HUB, NodeType.HUB, "Hub"), + (ImageEnum.SWITCH, NodeType.SWITCH, "Switch"), + (ImageEnum.WLAN, NodeType.WIRELESS_LAN, "WLAN"), (ImageEnum.EMANE, NodeType.EMANE, "EMANE"), - (ImageEnum.RJ45, NodeType.RJ45, "rj45 physical interface tool"), - (ImageEnum.TUNNEL, NodeType.TUNNEL, "tunnel tool"), + (ImageEnum.RJ45, NodeType.RJ45, "RJ45"), + (ImageEnum.TUNNEL, NodeType.TUNNEL, "Tunnel"), ] - for image_enum, node_type, tooltip in network_nodes: - node_draw = NodeDraw.from_setup(image_enum, node_type, tooltip=tooltip) + for image_enum, node_type, label in network_nodes: + node_draw = NodeDraw.from_setup(image_enum, node_type, label) cls.NETWORK_NODES.append(node_draw) cls.NODE_ICONS[(node_type, None)] = node_draw.image cls.ANTENNA_ICON = Images.get(ImageEnum.ANTENNA, ANTENNA_SIZE) diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index b80caf87..a1982b3e 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -8,6 +8,7 @@ class Styles: tooltip = "Tooltip.TLabel" tooltip_frame = "Tooltip.TFrame" service_checkbutton = "Service.TCheckbutton" + picker_button = "Picker.TButton" class Colors: @@ -150,3 +151,7 @@ def update_menu(style, event): event.widget.config( background=bg, foreground=fg, activebackground=abg, activeforeground=fg ) + + +def theme_change(style, event): + style.configure(Styles.picker_button, font=("TkDefaultFont", 8, "normal")) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index a47cac5e..72288b71 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -3,6 +3,7 @@ import threading import tkinter as tk from functools import partial from tkinter import ttk +from tkinter.font import Font from coretk.dialogs.customnodes import CustomNodesDialog from coretk.graph import tags @@ -10,13 +11,15 @@ from coretk.graph.enums import GraphMode from coretk.graph.shapeutils import ShapeType from coretk.images import ImageEnum, Images from coretk.nodeutils import NodeUtils +from coretk.themes import Styles from coretk.tooltip import Tooltip WIDTH = 32 +PICKER_SIZE = 24 -def icon(image_enum): - return Images.get(image_enum, WIDTH) +def icon(image_enum, width=WIDTH): + return Images.get(image_enum, width) class Toolbar(ttk.Frame): @@ -34,6 +37,9 @@ class Toolbar(ttk.Frame): self.app = app self.master = app.master + # picker data + self.picker_font = Font(size=8) + # design buttons self.select_button = None self.link_button = None @@ -140,9 +146,9 @@ class Toolbar(ttk.Frame): self.node_picker = ttk.Frame(self.master) # draw default nodes for node_draw in NodeUtils.NODES: - image = icon(node_draw.image_enum) + image = icon(node_draw.image_enum, PICKER_SIZE) func = partial(self.update_button, self.node_button, image, node_draw) - self.create_picker_button(image, func, self.node_picker, node_draw.tooltip) + 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] @@ -152,7 +158,7 @@ class Toolbar(ttk.Frame): # draw edit node image = icon(ImageEnum.EDITNODE) self.create_picker_button( - image, self.click_edit_node, self.node_picker, "custom nodes" + image, self.click_edit_node, self.node_picker, "Custom" ) self.design_select(self.node_button) self.node_button.after( @@ -169,21 +175,22 @@ class Toolbar(ttk.Frame): self.wait_window(picker) self.app.unbind_all("") - def create_picker_button(self, image, func, frame, tooltip): + def create_picker_button(self, image, func, frame, label): """ Create button and put it on the frame :param PIL.Image image: button image :param func: the command that is executed when button is clicked :param tkinter.Frame frame: frame that contains the button - :param str tooltip: tooltip text + :param str label: button label :return: nothing """ - button = ttk.Button(frame, image=image) + button = ttk.Button( + frame, image=image, text=label, compound=tk.TOP, style=Styles.picker_button + ) button.image = image button.bind("", lambda e: func()) button.grid(pady=1) - Tooltip(button, tooltip) def create_button(self, frame, image, func, tooltip): button = ttk.Button(frame, image=image, command=func) @@ -269,12 +276,12 @@ class Toolbar(ttk.Frame): self.hide_pickers() self.network_picker = ttk.Frame(self.master) for node_draw in NodeUtils.NETWORK_NODES: - image = icon(node_draw.image_enum) + image = icon(node_draw.image_enum, PICKER_SIZE) self.create_picker_button( image, partial(self.update_button, self.network_button, image, node_draw), self.network_picker, - node_draw.tooltip, + node_draw.label, ) self.design_select(self.network_button) self.network_button.after( @@ -311,7 +318,7 @@ class Toolbar(ttk.Frame): (ImageEnum.TEXT, ShapeType.TEXT), ] for image_enum, shape_type in nodes: - image = icon(image_enum) + image = icon(image_enum, PICKER_SIZE) self.create_picker_button( image, partial(self.update_annotation, image, shape_type), From 69296d6ea9a3f1efcbccc1c63b4d75661b17301a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 11 Dec 2019 14:09:50 -0800 Subject: [PATCH 360/462] pass on updating dialogs to have buttons float to bottom and start using common pad configuration --- coretk/coretk/app.py | 13 +- coretk/coretk/dialogs/canvassizeandscale.py | 1 + ...canvasbackground.py => canvaswallpaper.py} | 23 ++-- coretk/coretk/dialogs/customnodes.py | 26 ++-- coretk/coretk/dialogs/dialog.py | 6 + coretk/coretk/dialogs/emaneconfig.py | 12 +- coretk/coretk/dialogs/hooks.py | 34 ++--- coretk/coretk/dialogs/icondialog.py | 17 ++- coretk/coretk/dialogs/mobilityconfig.py | 3 +- coretk/coretk/dialogs/mobilityplayer.py | 1 + coretk/coretk/dialogs/nodeconfig.py | 27 ++-- coretk/coretk/dialogs/nodeservice.py | 12 +- coretk/coretk/dialogs/observers.py | 12 +- coretk/coretk/dialogs/preferences.py | 15 ++- coretk/coretk/dialogs/servers.py | 26 ++-- coretk/coretk/dialogs/sessions.py | 26 ++-- coretk/coretk/dialogs/shapemod.py | 126 +++++++++--------- coretk/coretk/dialogs/wlanconfig.py | 3 +- coretk/coretk/graph/node.py | 2 + coretk/coretk/menuaction.py | 2 +- coretk/coretk/menubar.py | 16 +-- coretk/coretk/themes.py | 8 +- coretk/coretk/widgets.py | 2 +- 23 files changed, 223 insertions(+), 190 deletions(-) rename coretk/coretk/dialogs/{canvasbackground.py => canvaswallpaper.py} (90%) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 0a9275f1..7c2562a9 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -14,6 +14,9 @@ from coretk.statusbar import StatusBar from coretk.toolbar import Toolbar from coretk.validation import InputValidation +WIDTH = 1000 +HEIGHT = 800 + class Application(tk.Frame): def __init__(self, master=None): @@ -40,7 +43,7 @@ class Application(tk.Frame): def setup_theme(self): themes.load(self.style) self.style.theme_use(self.guiconfig["preferences"]["theme"]) - func = partial(themes.update_menu, self.style) + func = partial(themes.theme_change_menu, self.style) self.master.bind_class("Menu", "<>", func) func = partial(themes.theme_change, self.style) self.master.bind("<>", func) @@ -55,13 +58,11 @@ class Application(tk.Frame): self.validation = InputValidation(self) def center(self): - width = 1000 - height = 800 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)) - self.master.geometry(f"{width}x{height}+{x}+{y}") + x = int((screen_width / 2) - (WIDTH / 2)) + y = int((screen_height / 2) - (HEIGHT / 2)) + self.master.geometry(f"{WIDTH}x{HEIGHT}+{x}+{y}") def draw(self): self.master.option_add("*tearOff", tk.FALSE) diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 2798f6f2..1fcfff59 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -46,6 +46,7 @@ class SizeAndScaleDialog(Dialog): self.draw_scale() self.draw_reference_point() self.draw_save_as_default() + self.draw_spacer() self.draw_buttons() def draw_size(self): diff --git a/coretk/coretk/dialogs/canvasbackground.py b/coretk/coretk/dialogs/canvaswallpaper.py similarity index 90% rename from coretk/coretk/dialogs/canvasbackground.py rename to coretk/coretk/dialogs/canvaswallpaper.py index 844b5595..bf6bf922 100644 --- a/coretk/coretk/dialogs/canvasbackground.py +++ b/coretk/coretk/dialogs/canvaswallpaper.py @@ -9,7 +9,7 @@ from coretk.appconfig import BACKGROUNDS_PATH from coretk.dialogs.dialog import Dialog from coretk.images import Images -PADX = 5 +PAD = 5 class CanvasBackgroundDialog(Dialog): @@ -36,17 +36,18 @@ class CanvasBackgroundDialog(Dialog): self.draw_image_selection() self.draw_options() self.draw_additional_options() + self.draw_spacer() self.draw_buttons() def draw_image(self): self.image_label = ttk.Label( self.top, text="(image preview)", width=32, anchor=tk.CENTER ) - self.image_label.grid(row=0, column=0, pady=5) + self.image_label.grid(pady=PAD) def draw_image_label(self): label = ttk.Label(self.top, text="Image filename: ") - label.grid(row=1, column=0, sticky="ew") + label.grid(sticky="ew") if self.filename.get(): self.draw_preview() @@ -55,14 +56,14 @@ class CanvasBackgroundDialog(Dialog): frame.columnconfigure(0, weight=2) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) - frame.grid(row=2, column=0, sticky="ew") + frame.grid(sticky="ew") entry = ttk.Entry(frame, textvariable=self.filename) entry.focus() - entry.grid(row=0, column=0, sticky="ew", padx=PADX) + entry.grid(row=0, column=0, sticky="ew", padx=PAD) button = ttk.Button(frame, text="...", command=self.click_open_image) - button.grid(row=0, column=1, sticky="ew", padx=PADX) + button.grid(row=0, column=1, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Clear", command=self.click_clear) button.grid(row=0, column=2, sticky="ew") @@ -73,7 +74,7 @@ class CanvasBackgroundDialog(Dialog): frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) frame.columnconfigure(3, weight=1) - frame.grid(row=3, column=0, sticky="ew") + frame.grid(sticky="ew") button = ttk.Radiobutton( frame, text="upper-left", value=1, variable=self.scale_option @@ -103,7 +104,7 @@ class CanvasBackgroundDialog(Dialog): checkbutton = ttk.Checkbutton( self.top, text="Show grid", variable=self.show_grid ) - checkbutton.grid(row=4, column=0, sticky="ew", padx=PADX) + checkbutton.grid(sticky="ew", padx=PAD) checkbutton = ttk.Checkbutton( self.top, @@ -111,16 +112,16 @@ class CanvasBackgroundDialog(Dialog): variable=self.adjust_to_dim, command=self.click_adjust_canvas, ) - checkbutton.grid(row=5, column=0, sticky="ew", padx=PADX) + checkbutton.grid(sticky="ew", padx=PAD) def draw_buttons(self): frame = ttk.Frame(self.top) - frame.grid(row=6, column=0, pady=5, sticky="ew") + frame.grid(pady=PAD, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index dbf431d3..75052e6b 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -8,6 +8,8 @@ from coretk.dialogs.icondialog import IconDialog from coretk.nodeutils import NodeDraw from coretk.widgets import CheckboxList, ListboxScroll +PAD = 5 + class ServicesSelectDialog(Dialog): def __init__(self, master, app, current_services): @@ -23,11 +25,11 @@ class ServicesSelectDialog(Dialog): self.top.rowconfigure(0, weight=1) frame = ttk.Frame(self.top) - frame.grid(stick="nsew") + frame.grid(stick="nsew", pady=PAD) frame.rowconfigure(0, weight=1) for i in range(3): frame.columnconfigure(i, weight=1) - self.groups = ListboxScroll(frame, text="Groups") + self.groups = ListboxScroll(frame, text="Groups", padding=PAD) self.groups.grid(row=0, column=0, sticky="nsew") for group in sorted(self.app.core.services): self.groups.listbox.insert(tk.END, group) @@ -35,11 +37,11 @@ class ServicesSelectDialog(Dialog): self.groups.listbox.selection_set(0) self.services = CheckboxList( - frame, self.app, text="Services", clicked=self.service_clicked + frame, self.app, text="Services", clicked=self.service_clicked, padding=PAD ) self.services.grid(row=0, column=1, sticky="nsew") - self.current = ListboxScroll(frame, text="Selected") + self.current = ListboxScroll(frame, text="Selected", padding=PAD) self.current.grid(row=0, column=2, sticky="nsew") for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) @@ -49,7 +51,7 @@ class ServicesSelectDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.destroy) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=1, sticky="ew") @@ -104,12 +106,12 @@ class CustomNodesDialog(Dialog): def draw_node_config(self): frame = ttk.Frame(self.top) - frame.grid(sticky="nsew") + frame.grid(sticky="nsew", pady=PAD) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - self.nodes_list = ListboxScroll(frame, text="Nodes") - self.nodes_list.grid(row=0, column=0, sticky="nsew") + self.nodes_list = ListboxScroll(frame, text="Nodes", padding=PAD) + self.nodes_list.grid(row=0, column=0, sticky="nsew", padx=PAD) self.nodes_list.listbox.bind("<>", self.handle_node_select) for name in sorted(self.app.core.custom_nodes): self.nodes_list.listbox.insert(tk.END, name) @@ -128,17 +130,17 @@ class CustomNodesDialog(Dialog): def draw_node_buttons(self): frame = ttk.Frame(self.top) - frame.grid(pady=2, sticky="ew") + frame.grid(sticky="ew", pady=PAD) for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Create", command=self.click_create) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PAD) self.edit_button = ttk.Button( frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) - self.edit_button.grid(row=0, column=1, sticky="ew") + self.edit_button.grid(row=0, column=1, sticky="ew", padx=PAD) self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete @@ -152,7 +154,7 @@ class CustomNodesDialog(Dialog): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.click_save) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index 29362960..49662cc4 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -30,3 +30,9 @@ class Dialog(tk.Toplevel): self.wait_visibility() self.grab_set() self.wait_window() + + def draw_spacer(self, row=None): + frame = ttk.Frame(self.top) + frame.grid(row=row, sticky="nsew") + frame.rowconfigure(0, weight=1) + self.top.rowconfigure(frame.grid_info()["row"], weight=1) diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index a9042ba3..b16f7cd4 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -25,11 +25,10 @@ class GlobalEmaneDialog(Dialog): def draw(self): self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) - self.config_frame = ConfigFrame( - self.top, self.app, self.app.core.emane_config, borderwidth=0 - ) + self.config_frame = ConfigFrame(self.top, self.app, self.app.core.emane_config) self.config_frame.draw_config() self.config_frame.grid(sticky="nsew", pady=PAD) + self.draw_spacer() self.draw_buttons() def draw_buttons(self): @@ -67,9 +66,10 @@ class EmaneModelDialog(Dialog): def draw(self): self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) - self.config_frame = ConfigFrame(self.top, self.app, self.config, borderwidth=0) + self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame.draw_config() self.config_frame.grid(sticky="nsew", pady=PAD) + self.draw_spacer() self.draw_buttons() def draw_buttons(self): @@ -111,6 +111,7 @@ class EmaneConfigDialog(Dialog): self.draw_emane_configuration() self.draw_emane_models() self.draw_emane_buttons() + self.draw_spacer() self.draw_apply_and_cancel() def draw_emane_configuration(self): @@ -123,8 +124,9 @@ class EmaneConfigDialog(Dialog): self.top, text="The EMANE emulation system provides more complex wireless radio emulation " "\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details", + justify=tk.CENTER, ) - label.grid(sticky="ew", pady=PAD) + label.grid(pady=PAD) image = Images.get(ImageEnum.EDITNODE, 16) button = ttk.Button( diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index d40f1c36..2850d70e 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -5,6 +5,8 @@ from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog from coretk.widgets import CodeText +PAD = 5 + class HookDialog(Dialog): def __init__(self, master, app): @@ -21,14 +23,14 @@ class HookDialog(Dialog): # name and states frame = ttk.Frame(self.top) - frame.grid(row=0, sticky="ew", pady=2) + frame.grid(sticky="ew", pady=PAD) frame.columnconfigure(0, weight=2) frame.columnconfigure(1, weight=7) frame.columnconfigure(2, weight=1) label = ttk.Label(frame, text="Name") - label.grid(row=0, column=0, sticky="ew") + label.grid(row=0, column=0, sticky="ew", padx=PAD) entry = ttk.Entry(frame, textvariable=self.name) - entry.grid(row=0, column=1, sticky="ew") + entry.grid(row=0, column=1, sticky="ew", padx=PAD) values = tuple(x for x in core_pb2.SessionState.Enum.keys() if x != "NONE") initial_state = core_pb2.SessionState.Enum.Name(core_pb2.SessionState.RUNTIME) self.state.set(initial_state) @@ -40,11 +42,7 @@ class HookDialog(Dialog): combobox.bind("<>", self.state_change) # data - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.rowconfigure(0, weight=1) - frame.grid(row=1, sticky="nsew", pady=2) - self.data = CodeText(frame) + self.data = CodeText(self.top) self.data.insert( 1.0, ( @@ -53,19 +51,15 @@ class HookDialog(Dialog): "# specified state\n" ), ) - self.data.grid(row=0, column=0, sticky="nsew") - scrollbar = ttk.Scrollbar(frame) - scrollbar.grid(row=0, column=1, sticky="ns") - self.data.config(yscrollcommand=scrollbar.set) - scrollbar.config(command=self.data.yview) + self.data.grid(sticky="nsew") # button row frame = ttk.Frame(self.top) - frame.grid(row=2, sticky="ew", pady=2) + frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=lambda: self.save()) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=1, sticky="ew") @@ -104,25 +98,25 @@ class HooksDialog(Dialog): self.top.rowconfigure(0, weight=1) self.listbox = tk.Listbox(self.top) - self.listbox.grid(row=0, sticky="nsew") + self.listbox.grid(sticky="nsew", pady=PAD) self.listbox.bind("<>", self.select) for hook_file in self.app.core.hooks: self.listbox.insert(tk.END, hook_file) frame = ttk.Frame(self.top) - frame.grid(row=1, sticky="ew") + frame.grid(sticky="ew") for i in range(4): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Create", command=self.click_create) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PAD) self.edit_button = ttk.Button( frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) - self.edit_button.grid(row=0, column=1, sticky="ew") + self.edit_button.grid(row=0, column=1, sticky="ew", padx=PAD) self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete ) - self.delete_button.grid(row=0, column=2, sticky="ew") + self.delete_button.grid(row=0, column=2, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=3, sticky="ew") diff --git a/coretk/coretk/dialogs/icondialog.py b/coretk/coretk/dialogs/icondialog.py index 98a4ed55..9273a177 100644 --- a/coretk/coretk/dialogs/icondialog.py +++ b/coretk/coretk/dialogs/icondialog.py @@ -6,6 +6,8 @@ from coretk.appconfig import ICONS_PATH from coretk.dialogs.dialog import Dialog from coretk.images import Images +PAD = 5 + class IconDialog(Dialog): def __init__(self, master, app, name, image): @@ -20,27 +22,30 @@ class IconDialog(Dialog): # row one frame = ttk.Frame(self.top) - frame.grid(row=0, column=0, pady=2, sticky="ew") + frame.grid(pady=PAD, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=3) label = ttk.Label(frame, text="Image") - label.grid(row=0, column=0, sticky="ew") + label.grid(row=0, column=0, sticky="ew", padx=PAD) entry = ttk.Entry(frame, textvariable=self.file_path) - entry.grid(row=0, column=1, sticky="ew") + entry.grid(row=0, column=1, sticky="ew", padx=PAD) button = ttk.Button(frame, text="...", command=self.click_file) button.grid(row=0, column=2) # row two self.image_label = ttk.Label(self.top, image=self.image, anchor=tk.CENTER) - self.image_label.grid(row=1, column=0, pady=2, sticky="ew") + self.image_label.grid(pady=PAD, sticky="ew") + + # spacer + self.draw_spacer() # row three frame = ttk.Frame(self.top) - frame.grid(row=2, column=0, sticky="ew") + frame.grid(sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Apply", command=self.destroy) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/coretk/coretk/dialogs/mobilityconfig.py index 4e9d9590..007209dd 100644 --- a/coretk/coretk/dialogs/mobilityconfig.py +++ b/coretk/coretk/dialogs/mobilityconfig.py @@ -32,7 +32,8 @@ class MobilityConfigDialog(Dialog): def draw(self): self.top.columnconfigure(0, weight=1) - self.config_frame = ConfigFrame(self.top, self.app, self.config, borderwidth=0) + self.top.rowconfigure(0, weight=1) + self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame.draw_config() self.config_frame.grid(sticky="nsew", pady=PAD) self.draw_apply_buttons() diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index 63698644..1e7b4dc1 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -61,6 +61,7 @@ class MobilityPlayerDialog(Dialog): super().__init__( master, app, f"{canvas_node.core_node.name} Mobility Player", modal=False ) + self.resizable(False, False) self.geometry("") self.canvas_node = canvas_node self.node = canvas_node.core_node diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 8853e908..619c5ee7 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -66,6 +66,19 @@ class NodeConfigDialog(Dialog): frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) + # icon field + label = ttk.Label(frame, text="Icon") + label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + self.image_button = ttk.Button( + frame, + text="Icon", + image=self.image, + compound=tk.NONE, + command=self.click_icon, + ) + self.image_button.grid(row=row, column=1, sticky="ew") + row += 1 + # name field label = ttk.Label(frame, text="Name") label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) @@ -81,19 +94,6 @@ class NodeConfigDialog(Dialog): entry.grid(row=row, column=1, sticky="ew") row += 1 - # icon field - label = ttk.Label(frame, text="Icon") - label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) - self.image_button = ttk.Button( - frame, - text="Icon", - image=self.image, - compound=tk.NONE, - command=self.click_icon, - ) - self.image_button.grid(row=row, column=1, sticky="ew") - row += 1 - # node type field if NodeUtils.is_model_node(self.node.type): label = ttk.Label(frame, text="Type") @@ -137,6 +137,7 @@ class NodeConfigDialog(Dialog): if self.canvas_node.interfaces: self.draw_interfaces() + self.draw_spacer() self.draw_buttons() def draw_interfaces(self): diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index d950371b..dfcef922 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -8,6 +8,8 @@ from coretk.dialogs.dialog import Dialog from coretk.dialogs.serviceconfiguration import ServiceConfiguration from coretk.widgets import CheckboxList, ListboxScroll +PAD = 5 + class NodeService(Dialog): def __init__(self, master, app, canvas_node, services=None): @@ -38,7 +40,7 @@ class NodeService(Dialog): frame.rowconfigure(0, weight=1) for i in range(3): frame.columnconfigure(i, weight=1) - self.groups = ListboxScroll(frame, text="Groups") + self.groups = ListboxScroll(frame, text="Groups", padding=PAD) self.groups.grid(row=0, column=0, sticky="nsew") for group in sorted(self.app.core.services): self.groups.listbox.insert(tk.END, group) @@ -46,11 +48,11 @@ class NodeService(Dialog): self.groups.listbox.selection_set(0) self.services = CheckboxList( - frame, self.app, text="Services", clicked=self.service_clicked + frame, self.app, text="Services", clicked=self.service_clicked, padding=PAD ) self.services.grid(row=0, column=1, sticky="nsew") - self.current = ListboxScroll(frame, text="Selected") + self.current = ListboxScroll(frame, text="Selected", padding=PAD) self.current.grid(row=0, column=2, sticky="nsew") for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) @@ -60,9 +62,9 @@ class NodeService(Dialog): for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Configure", command=self.click_configure) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Save", command=self.click_save) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=2, sticky="ew") diff --git a/coretk/coretk/dialogs/observers.py b/coretk/coretk/dialogs/observers.py index 58499bd7..525939e0 100644 --- a/coretk/coretk/dialogs/observers.py +++ b/coretk/coretk/dialogs/observers.py @@ -4,6 +4,8 @@ from tkinter import ttk from coretk.coreclient import Observer from coretk.dialogs.dialog import Dialog +PAD = 5 + class ObserverDialog(Dialog): def __init__(self, master, app): @@ -27,7 +29,7 @@ class ObserverDialog(Dialog): def draw_listbox(self): frame = ttk.Frame(self.top) - frame.grid(sticky="nsew") + frame.grid(sticky="nsew", pady=PAD) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) @@ -46,22 +48,22 @@ class ObserverDialog(Dialog): def draw_form_fields(self): frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky="ew", pady=PAD) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Name") - label.grid(row=0, column=0, sticky="w") + label.grid(row=0, column=0, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=1, sticky="ew") label = ttk.Label(frame, text="Command") - label.grid(row=1, column=0, sticky="w") + label.grid(row=1, column=0, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.cmd) entry.grid(row=1, column=1, sticky="ew") def draw_config_buttons(self): frame = ttk.Frame(self.top) - frame.grid(pady=2, sticky="ew") + frame.grid(sticky="ew", pady=PAD) for i in range(3): frame.columnconfigure(i, weight=1) diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py index 7298f727..b094eec6 100644 --- a/coretk/coretk/dialogs/preferences.py +++ b/coretk/coretk/dialogs/preferences.py @@ -5,6 +5,8 @@ from tkinter import ttk from coretk import appconfig from coretk.dialogs.dialog import Dialog +PAD = 5 + class PreferencesDialog(Dialog): def __init__(self, master, app): @@ -18,16 +20,17 @@ class PreferencesDialog(Dialog): def draw(self): self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) self.draw_preferences() self.draw_buttons() def draw_preferences(self): - frame = ttk.LabelFrame(self.top, text="Preferences") - frame.grid(sticky="ew", pady=2) + frame = ttk.LabelFrame(self.top, text="Preferences", padding=PAD) + frame.grid(sticky="nsew", pady=2) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Theme") - label.grid(row=0, column=0, pady=2, padx=2, sticky="w") + label.grid(row=0, column=0, pady=PAD, padx=PAD, sticky="w") themes = self.app.style.theme_names() combobox = ttk.Combobox( frame, textvariable=self.theme, values=themes, state="readonly" @@ -37,14 +40,14 @@ class PreferencesDialog(Dialog): combobox.bind("<>", self.theme_change) label = ttk.Label(frame, text="Editor") - label.grid(row=1, column=0, pady=2, padx=2, sticky="w") + label.grid(row=1, column=0, pady=PAD, padx=PAD, sticky="w") combobox = ttk.Combobox( frame, textvariable=self.editor, values=appconfig.EDITORS, state="readonly" ) combobox.grid(row=1, column=1, sticky="ew") label = ttk.Label(frame, text="Terminal") - label.grid(row=2, column=0, pady=2, padx=2, sticky="w") + label.grid(row=2, column=0, pady=PAD, padx=PAD, sticky="w") combobox = ttk.Combobox( frame, textvariable=self.terminal, @@ -54,7 +57,7 @@ class PreferencesDialog(Dialog): combobox.grid(row=2, column=1, sticky="ew") label = ttk.Label(frame, text="3D GUI") - label.grid(row=3, column=0, pady=2, padx=2, sticky="w") + label.grid(row=3, column=0, pady=PAD, padx=PAD, sticky="w") entry = ttk.Entry(frame, textvariable=self.gui3d) entry.grid(row=3, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index 9df7bf55..18c5d6d1 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -4,6 +4,7 @@ from tkinter import ttk from coretk.coreclient import CoreServer from coretk.dialogs.dialog import Dialog +PAD = 5 DEFAULT_NAME = "example" DEFAULT_ADDRESS = "127.0.0.1" DEFAULT_PORT = 50051 @@ -26,13 +27,13 @@ class ServersDialog(Dialog): self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) self.draw_servers() - self.draw_server_configuration() self.draw_servers_buttons() + self.draw_server_configuration() self.draw_apply_buttons() def draw_servers(self): frame = ttk.Frame(self.top) - frame.grid(pady=2, sticky="nsew") + frame.grid(pady=PAD, sticky="nsew") frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) @@ -51,27 +52,24 @@ class ServersDialog(Dialog): scrollbar.config(command=self.servers.yview) def draw_server_configuration(self): - label = ttk.Label(self.top, text="Server Configuration") - label.grid(pady=2, sticky="ew") - - frame = ttk.Frame(self.top) - frame.grid(pady=2, sticky="ew") + frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=PAD) + frame.grid(pady=PAD, sticky="ew") frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) frame.columnconfigure(5, weight=1) label = ttk.Label(frame, text="Name") - label.grid(row=0, column=0, sticky="w") + label.grid(row=0, column=0, sticky="w", padx=PAD, pady=PAD) entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=1, sticky="ew") label = ttk.Label(frame, text="Address") - label.grid(row=0, column=2, sticky="w") + label.grid(row=0, column=2, sticky="w", padx=PAD, pady=PAD) entry = ttk.Entry(frame, textvariable=self.address) entry.grid(row=0, column=3, sticky="ew") label = ttk.Label(frame, text="Port") - label.grid(row=0, column=4, sticky="w") + label.grid(row=0, column=4, sticky="w", padx=PAD, pady=PAD) entry = ttk.Entry( frame, textvariable=self.port, @@ -85,17 +83,17 @@ class ServersDialog(Dialog): def draw_servers_buttons(self): frame = ttk.Frame(self.top) - frame.grid(pady=2, sticky="ew") + frame.grid(pady=PAD, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Create", command=self.click_create) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PAD) self.save_button = ttk.Button( frame, text="Save", state=tk.DISABLED, command=self.click_save ) - self.save_button.grid(row=0, column=1, sticky="ew") + self.save_button.grid(row=0, column=1, sticky="ew", padx=PAD) self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete @@ -111,7 +109,7 @@ class ServersDialog(Dialog): button = ttk.Button( frame, text="Save Configuration", command=self.click_save_configuration ) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index e3d9e696..f8d26667 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -10,6 +10,8 @@ from coretk.dialogs.dialog import Dialog from coretk.errors import show_grpc_error from coretk.images import ImageEnum, Images +PAD = 5 + class SessionsDialog(Dialog): def __init__(self, master, app): @@ -31,6 +33,7 @@ class SessionsDialog(Dialog): def draw(self): self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(1, weight=1) self.draw_description() self.draw_tree() self.draw_buttons() @@ -46,14 +49,19 @@ class SessionsDialog(Dialog): "connect to an existing session. Usually, only sessions in \n" "the RUNTIME state persist in the daemon, except for the \n" "one you might be concurrently editting.", + justify=tk.CENTER, ) - label.grid(row=0, sticky="ew", pady=5) + label.grid(pady=PAD) def draw_tree(self): + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + frame.grid(sticky="nsew") self.tree = ttk.Treeview( - self.top, columns=("id", "state", "nodes"), show="headings" + frame, columns=("id", "state", "nodes"), show="headings" ) - self.tree.grid(row=1, sticky="nsew") + self.tree.grid(sticky="nsew") self.tree.column("id", stretch=tk.YES) self.tree.heading("id", text="ID") self.tree.column("state", stretch=tk.YES) @@ -72,21 +80,19 @@ class SessionsDialog(Dialog): self.tree.bind("", self.on_selected) self.tree.bind("<>", self.click_select) - yscrollbar = ttk.Scrollbar(self.top, orient="vertical", command=self.tree.yview) - yscrollbar.grid(row=1, column=1, sticky="ns") + yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) + yscrollbar.grid(row=0, column=1, sticky="ns") self.tree.configure(yscrollcommand=yscrollbar.set) - xscrollbar = ttk.Scrollbar( - self.top, orient="horizontal", command=self.tree.xview - ) - xscrollbar.grid(row=2, sticky="ew", pady=5) + xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) + xscrollbar.grid(row=1, sticky="ew", pady=5) self.tree.configure(xscrollcommand=xscrollbar.set) def draw_buttons(self): frame = ttk.Frame(self.top) for i in range(4): frame.columnconfigure(i, weight=1) - frame.grid(row=3, sticky="ew") + frame.grid(sticky="ew") image = Images.get(ImageEnum.DOCUMENTNEW, 16) b = ttk.Button( diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index e0085a8c..63037c9d 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -8,6 +8,8 @@ from coretk.dialogs.dialog import Dialog from coretk.graph import tags from coretk.graph.shapeutils import is_draw_shape, is_shape_text +PADX = (0, 5) +PAD = 5 FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] @@ -37,20 +39,27 @@ class ShapeDialog(Dialog): self.bold = tk.BooleanVar(value=data.bold) self.italic = tk.BooleanVar(value=data.italic) self.underline = tk.BooleanVar(value=data.underline) - self.top.columnconfigure(0, weight=1) self.draw() def draw(self): - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=2) - label = ttk.Label(frame, text="Text for top of shape: ") - label.grid(row=0, column=0, sticky="nsew") - entry = ttk.Entry(frame, textvariable=self.shape_text) - entry.grid(row=0, column=1, sticky="nsew") - frame.grid(row=0, column=0, sticky="nsew", padx=3, pady=3) + self.top.columnconfigure(0, weight=1) + self.draw_label_options() + if is_draw_shape(self.shape.shape_type): + self.draw_shape_options() + self.draw_spacer() + self.draw_buttons() - frame = ttk.Frame(self.top) + def draw_label_options(self): + label_frame = ttk.LabelFrame(self.top, text="Label", padding=PAD) + label_frame.grid(sticky="ew") + label_frame.columnconfigure(0, weight=1) + + entry = ttk.Entry(label_frame, textvariable=self.shape_text) + entry.grid(sticky="ew", pady=PAD) + + # font options + frame = ttk.Frame(label_frame) + frame.grid(sticky="nsew", padx=3, pady=3) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) @@ -65,70 +74,65 @@ class ShapeDialog(Dialog): frame, textvariable=self.font_size, values=FONT_SIZES, state="readonly" ) combobox.grid(row=0, column=1, padx=3, sticky="nsew") - button = ttk.Button(frame, text="Text color", command=self.choose_text_color) + button = ttk.Button(frame, text="Color", command=self.choose_text_color) button.grid(row=0, column=2, sticky="nsew") - frame.grid(row=1, column=0, sticky="nsew", padx=3, pady=3) - frame = ttk.Frame(self.top) + # style options + frame = ttk.Frame(label_frame) + frame.grid(sticky="ew") + for i in range(3): + frame.columnconfigure(i, weight=1) button = ttk.Checkbutton(frame, variable=self.bold, text="Bold") - button.grid(row=0, column=0) + button.grid(row=0, column=0, sticky="ew") button = ttk.Checkbutton(frame, variable=self.italic, text="Italic") - button.grid(row=0, column=1, padx=3) + button.grid(row=0, column=1, padx=3, sticky="ew") button = ttk.Checkbutton(frame, variable=self.underline, text="Underline") - button.grid(row=0, column=2) - frame.grid(row=2, column=0, sticky="nsew", padx=3, pady=3) + button.grid(row=0, column=2, sticky="ew") - if is_draw_shape(self.shape.shape_type): - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=1) - label = ttk.Label(frame, text="Fill color") - label.grid(row=0, column=0, sticky="nsew") - self.fill = ttk.Label( - frame, text=self.fill_color, background=self.fill_color - ) - self.fill.grid(row=0, column=1, sticky="nsew", padx=3) - button = ttk.Button(frame, text="Color", command=self.choose_fill_color) - button.grid(row=0, column=2, sticky="nsew") - frame.grid(row=3, column=0, sticky="nsew", padx=3, pady=3) + def draw_shape_options(self): + label_frame = ttk.LabelFrame(self.top, text="Shape", padding=PAD) + label_frame.grid(sticky="ew", pady=PAD) + label_frame.columnconfigure(0, weight=1) - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=1) - label = ttk.Label(frame, text="Border color:") - label.grid(row=0, column=0, sticky="nsew") - self.border = ttk.Label( - frame, text=self.border_color, background=self.fill_color - ) - self.border.grid(row=0, column=1, sticky="nsew", padx=3) - button = ttk.Button(frame, text="Color", command=self.choose_border_color) - button.grid(row=0, column=2, sticky="nsew") - frame.grid(row=4, column=0, sticky="nsew", padx=3, pady=3) + frame = ttk.Frame(label_frame) + frame.grid(sticky="ew") + for i in range(1, 3): + frame.columnconfigure(i, weight=1) + label = ttk.Label(frame, text="Fill Color") + label.grid(row=0, column=0, padx=PADX, sticky="w") + self.fill = ttk.Label(frame, text=self.fill_color, background=self.fill_color) + self.fill.grid(row=0, column=1, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Color", command=self.choose_fill_color) + button.grid(row=0, column=2, sticky="ew") - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=2) - label = ttk.Label(frame, text="Border width:") - label.grid(row=0, column=0, sticky="nsew") - combobox = ttk.Combobox( - frame, - textvariable=self.border_width, - values=BORDER_WIDTH, - state="readonly", - ) - combobox.grid(row=0, column=1, sticky="nsew") - frame.grid(row=5, column=0, sticky="nsew", padx=3, pady=3) + label = ttk.Label(frame, text="Border Color") + label.grid(row=1, column=0, sticky="w", padx=PADX) + self.border = ttk.Label( + frame, text=self.border_color, background=self.border_color + ) + self.border.grid(row=1, column=1, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Color", command=self.choose_border_color) + button.grid(row=1, column=2, sticky="ew") + frame = ttk.Frame(label_frame) + frame.grid(sticky="ew", pady=PAD) + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="Border Width") + label.grid(row=0, column=0, sticky="w", padx=PADX) + combobox = ttk.Combobox( + frame, textvariable=self.border_width, values=BORDER_WIDTH, state="readonly" + ) + combobox.grid(row=0, column=1, sticky="nsew") + + def draw_buttons(self): frame = ttk.Frame(self.top) + frame.grid(sticky="nsew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Add shape", command=self.click_add) - button.grid(row=0, column=0, sticky="e", padx=3) + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.cancel) - button.grid(row=0, column=1, sticky="w", pady=3) - frame.grid(row=6, column=0, sticky="nsew", padx=3, pady=3) + button.grid(row=0, column=1, sticky="ew") def choose_text_color(self): color = colorchooser.askcolor(color="black") @@ -140,7 +144,7 @@ class ShapeDialog(Dialog): self.fill.config(background=color[1], text=color[1]) def choose_border_color(self): - color = colorchooser.askcolor(color="black") + color = colorchooser.askcolor(color=self.border_color) self.border_color = color[1] self.border.config(background=color[1], text=color[1]) diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index 42adc49a..711f94f0 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -30,7 +30,8 @@ class WlanConfigDialog(Dialog): def draw(self): self.top.columnconfigure(0, weight=1) - self.config_frame = ConfigFrame(self.top, self.app, self.config, borderwidth=0) + self.top.rowconfigure(0, weight=1) + self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame.draw_config() self.config_frame.grid(sticky="nsew", pady=PAD) self.draw_apply_buttons() diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 5757f081..ef4686be 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -4,6 +4,7 @@ from tkinter import font import grpc from core.api.grpc.core_pb2 import NodeType +from coretk import themes from coretk.dialogs.emaneconfig import EmaneConfigDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog @@ -161,6 +162,7 @@ class CanvasNode: is_wlan = self.core_node.type == NodeType.WIRELESS_LAN is_emane = self.core_node.type == NodeType.EMANE context = tk.Menu(self.canvas) + themes.update_menu(self.app.style, context) if self.app.core.is_runtime(): context.add_command(label="Configure", command=self.show_config) if NodeUtils.is_container_node(self.core_node.type): diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index a9f9ca34..3115cd58 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -12,8 +12,8 @@ import grpc from coretk.appconfig import XML_PATH from coretk.dialogs.about import AboutDialog -from coretk.dialogs.canvasbackground import CanvasBackgroundDialog from coretk.dialogs.canvassizeandscale import SizeAndScaleDialog +from coretk.dialogs.canvaswallpaper import CanvasBackgroundDialog from coretk.dialogs.hooks import HooksDialog from coretk.dialogs.observers import ObserverDialog from coretk.dialogs.preferences import PreferencesDialog diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index ed5cb2ca..2521bd97 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -80,6 +80,7 @@ class Menubar(tk.Menu): :return: nothing """ menu = tk.Menu(self) + menu.add_command(label="Preferences", command=self.menuaction.gui_preferences) menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED) menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED) menu.add_separator() @@ -94,9 +95,6 @@ class Menubar(tk.Menu): menu.add_separator() menu.add_command(label="Find...", accelerator="Ctrl+F", state=tk.DISABLED) menu.add_command(label="Clear marker", state=tk.DISABLED) - menu.add_command( - label="Preferences...", command=self.menuaction.gui_preferences - ) self.add_cascade(label="Edit", menu=menu) def draw_canvas_menu(self): @@ -436,16 +434,14 @@ class Menubar(tk.Menu): """ menu = tk.Menu(self) menu.add_command( - label="Change sessions...", - command=self.menuaction.session_change_sessions, - underline=0, + label="Sessions...", command=self.menuaction.session_change_sessions ) menu.add_separator() - menu.add_command(label="Comments...", state=tk.DISABLED) - menu.add_command(label="Hooks...", command=self.menuaction.session_hooks) - menu.add_command(label="Reset node positions", state=tk.DISABLED) - menu.add_command(label="Servers...", command=self.menuaction.session_servers) menu.add_command(label="Options...", command=self.menuaction.session_options) + menu.add_command(label="Servers...", command=self.menuaction.session_servers) + menu.add_command(label="Hooks...", command=self.menuaction.session_hooks) + menu.add_command(label="Reset Nodes", state=tk.DISABLED) + menu.add_command(label="Comments...", state=tk.DISABLED) self.add_cascade(label="Session", menu=menu) def draw_help_menu(self): diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index a1982b3e..a870b278 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -140,15 +140,19 @@ def update_bg(style, event): event.widget.config(background=bg) -def update_menu(style, event): +def theme_change_menu(style, event): if not isinstance(event.widget, tk.Menu): return + update_menu(style, event.widget) + + +def update_menu(style, widget): bg = style.lookup(".", "background") fg = style.lookup(".", "foreground") abg = style.lookup(".", "lightcolor") if not abg: abg = bg - event.widget.config( + widget.config( background=bg, foreground=fg, activebackground=abg, activeforeground=fg ) diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index fb5582a1..e3fd5c35 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -65,7 +65,7 @@ class FrameScroll(ttk.LabelFrame): class ConfigFrame(FrameScroll): def __init__(self, master, app, config, **kw): - super().__init__(master, app, ttk.Notebook, **kw) + super().__init__(master, app, ttk.Notebook, borderwidth=0, **kw) self.app = app self.config = config self.values = {} From 899eb51c55bc2b71f575e183709a1537fafa237b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 11 Dec 2019 14:36:27 -0800 Subject: [PATCH 361/462] added common padding for x, y, and frame paddings, to easily modify and provide consistent look and feel --- coretk/coretk/appconfig.py | 2 +- coretk/coretk/dialogs/canvassizeandscale.py | 60 +++++++++++---------- coretk/coretk/dialogs/canvaswallpaper.py | 17 +++--- coretk/coretk/dialogs/customnodes.py | 31 ++++++----- coretk/coretk/dialogs/dialog.py | 3 +- coretk/coretk/dialogs/emaneconfig.py | 23 ++++---- coretk/coretk/dialogs/hooks.py | 19 ++++--- coretk/coretk/dialogs/icondialog.py | 13 +++-- coretk/coretk/dialogs/mobilityconfig.py | 7 ++- coretk/coretk/dialogs/mobilityplayer.py | 16 +++--- coretk/coretk/dialogs/nodeconfig.py | 29 +++++----- coretk/coretk/dialogs/nodeservice.py | 19 ++++--- coretk/coretk/dialogs/observers.py | 19 ++++--- coretk/coretk/dialogs/preferences.py | 17 +++--- coretk/coretk/dialogs/servers.py | 22 ++++---- coretk/coretk/dialogs/sessionoptions.py | 10 ++-- coretk/coretk/dialogs/sessions.py | 17 +++--- coretk/coretk/dialogs/shapemod.py | 19 ++++--- coretk/coretk/dialogs/wlanconfig.py | 7 ++- coretk/coretk/themes.py | 8 ++- coretk/coretk/widgets.py | 24 ++++----- 21 files changed, 189 insertions(+), 193 deletions(-) diff --git a/coretk/coretk/appconfig.py b/coretk/coretk/appconfig.py index 0617562a..b2606994 100644 --- a/coretk/coretk/appconfig.py +++ b/coretk/coretk/appconfig.py @@ -75,7 +75,7 @@ def check_directory(): editor = EDITORS[1] config = { "preferences": { - "theme": themes.DARK, + "theme": themes.THEME_DARK, "editor": editor, "terminal": terminal, "gui3d": "/usr/local/bin/std3d.sh", diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 1fcfff59..5a472104 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -5,8 +5,8 @@ import tkinter as tk from tkinter import font, ttk from coretk.dialogs.dialog import Dialog +from coretk.themes import FRAME_PAD, PADX, PADY -PAD = 5 PIXEL_SCALE = 100 @@ -50,17 +50,17 @@ class SizeAndScaleDialog(Dialog): self.draw_buttons() def draw_size(self): - label_frame = ttk.Labelframe(self.top, text="Size", padding=PAD) + label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) # draw size row 1 frame = ttk.Frame(label_frame) - frame.grid(sticky="ew", pady=3) + frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") - label.grid(row=0, column=0, sticky="w", padx=PAD) + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.pixel_width, @@ -68,9 +68,9 @@ class SizeAndScaleDialog(Dialog): validatecommand=(self.validation.positive_int, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) - entry.grid(row=0, column=1, sticky="ew", padx=PAD) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="x Height") - label.grid(row=0, column=2, sticky="w", padx=PAD) + label.grid(row=0, column=2, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.pixel_height, @@ -78,17 +78,17 @@ class SizeAndScaleDialog(Dialog): validatecommand=(self.validation.positive_int, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) - entry.grid(row=0, column=3, sticky="ew", padx=PAD) + entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Pixels") label.grid(row=0, column=4, sticky="w") # draw size row 2 frame = ttk.Frame(label_frame) - frame.grid(sticky="ew", pady=3) + frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") - label.grid(row=0, column=0, sticky="w", padx=PAD) + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.meters_width, @@ -96,9 +96,9 @@ class SizeAndScaleDialog(Dialog): validatecommand=(self.validation.positive_float, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) - entry.grid(row=0, column=1, sticky="ew", padx=PAD) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="x Height") - label.grid(row=0, column=2, sticky="w", padx=PAD) + label.grid(row=0, column=2, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.meters_height, @@ -106,12 +106,12 @@ class SizeAndScaleDialog(Dialog): validatecommand=(self.validation.positive_float, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) - entry.grid(row=0, column=3, sticky="ew", padx=PAD) + entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=4, sticky="w") def draw_scale(self): - label_frame = ttk.Labelframe(self.top, text="Scale", padding=PAD) + label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -119,7 +119,7 @@ class SizeAndScaleDialog(Dialog): frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text=f"{PIXEL_SCALE} Pixels =") - label.grid(row=0, column=0, sticky="w", padx=PAD) + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.scale, @@ -127,12 +127,14 @@ class SizeAndScaleDialog(Dialog): validatecommand=(self.validation.positive_float, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) - entry.grid(row=0, column=1, sticky="ew", padx=PAD) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=2, sticky="w") def draw_reference_point(self): - label_frame = ttk.Labelframe(self.top, text="Reference Point", padding=PAD) + label_frame = ttk.Labelframe( + self.top, text="Reference Point", padding=FRAME_PAD + ) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -142,12 +144,12 @@ class SizeAndScaleDialog(Dialog): label.grid() frame = ttk.Frame(label_frame) - frame.grid(sticky="ew", pady=3) + frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="X") - label.grid(row=0, column=0, sticky="w", padx=PAD) + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.x, @@ -155,10 +157,10 @@ class SizeAndScaleDialog(Dialog): validatecommand=(self.validation.positive_float, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) - entry.grid(row=0, column=1, sticky="ew", padx=PAD) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Y") - label.grid(row=0, column=2, sticky="w", padx=PAD) + label.grid(row=0, column=2, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.y, @@ -166,19 +168,19 @@ class SizeAndScaleDialog(Dialog): validatecommand=(self.validation.positive_float, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) - entry.grid(row=0, column=3, sticky="ew", padx=PAD) + entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(label_frame, text="Translates To") label.grid() frame = ttk.Frame(label_frame) - frame.grid(sticky="ew", pady=3) + frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) frame.columnconfigure(5, weight=1) label = ttk.Label(frame, text="Lat") - label.grid(row=0, column=0, sticky="w", padx=PAD) + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.lat, @@ -186,10 +188,10 @@ class SizeAndScaleDialog(Dialog): validatecommand=(self.validation.positive_float, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) - entry.grid(row=0, column=1, sticky="ew", padx=PAD) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Lon") - label.grid(row=0, column=2, sticky="w", padx=PAD) + label.grid(row=0, column=2, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.lon, @@ -197,10 +199,10 @@ class SizeAndScaleDialog(Dialog): validatecommand=(self.validation.positive_float, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) - entry.grid(row=0, column=3, sticky="ew", padx=PAD) + entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Alt") - label.grid(row=0, column=4, sticky="w", padx=PAD) + label.grid(row=0, column=4, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.alt, @@ -214,7 +216,7 @@ class SizeAndScaleDialog(Dialog): button = ttk.Checkbutton( self.top, text="Save as default?", variable=self.save_default ) - button.grid(sticky="w", pady=3) + button.grid(sticky="w", pady=PADY) def draw_buttons(self): frame = ttk.Frame(self.top) @@ -223,7 +225,7 @@ class SizeAndScaleDialog(Dialog): frame.grid(sticky="ew") button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + 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") diff --git a/coretk/coretk/dialogs/canvaswallpaper.py b/coretk/coretk/dialogs/canvaswallpaper.py index bf6bf922..923e4d5f 100644 --- a/coretk/coretk/dialogs/canvaswallpaper.py +++ b/coretk/coretk/dialogs/canvaswallpaper.py @@ -8,8 +8,7 @@ from tkinter import filedialog, ttk from coretk.appconfig import BACKGROUNDS_PATH from coretk.dialogs.dialog import Dialog from coretk.images import Images - -PAD = 5 +from coretk.themes import PADX, PADY class CanvasBackgroundDialog(Dialog): @@ -43,7 +42,7 @@ class CanvasBackgroundDialog(Dialog): self.image_label = ttk.Label( self.top, text="(image preview)", width=32, anchor=tk.CENTER ) - self.image_label.grid(pady=PAD) + self.image_label.grid(pady=PADY) def draw_image_label(self): label = ttk.Label(self.top, text="Image filename: ") @@ -60,10 +59,10 @@ class CanvasBackgroundDialog(Dialog): entry = ttk.Entry(frame, textvariable=self.filename) entry.focus() - entry.grid(row=0, column=0, sticky="ew", padx=PAD) + entry.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="...", command=self.click_open_image) - button.grid(row=0, column=1, sticky="ew", padx=PAD) + button.grid(row=0, column=1, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Clear", command=self.click_clear) button.grid(row=0, column=2, sticky="ew") @@ -104,7 +103,7 @@ class CanvasBackgroundDialog(Dialog): checkbutton = ttk.Checkbutton( self.top, text="Show grid", variable=self.show_grid ) - checkbutton.grid(sticky="ew", padx=PAD) + checkbutton.grid(sticky="ew", padx=PADX) checkbutton = ttk.Checkbutton( self.top, @@ -112,16 +111,16 @@ class CanvasBackgroundDialog(Dialog): variable=self.adjust_to_dim, command=self.click_adjust_canvas, ) - checkbutton.grid(sticky="ew", padx=PAD) + checkbutton.grid(sticky="ew", padx=PADX) def draw_buttons(self): frame = ttk.Frame(self.top) - frame.grid(pady=PAD, sticky="ew") + frame.grid(pady=PADY, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + 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") diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 75052e6b..d17d8d19 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -6,10 +6,9 @@ from tkinter import ttk from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog from coretk.nodeutils import NodeDraw +from coretk.themes import FRAME_PAD, PADX, PADY from coretk.widgets import CheckboxList, ListboxScroll -PAD = 5 - class ServicesSelectDialog(Dialog): def __init__(self, master, app, current_services): @@ -25,11 +24,11 @@ class ServicesSelectDialog(Dialog): self.top.rowconfigure(0, weight=1) frame = ttk.Frame(self.top) - frame.grid(stick="nsew", pady=PAD) + frame.grid(stick="nsew", pady=PADY) frame.rowconfigure(0, weight=1) for i in range(3): frame.columnconfigure(i, weight=1) - self.groups = ListboxScroll(frame, text="Groups", padding=PAD) + self.groups = ListboxScroll(frame, text="Groups", padding=FRAME_PAD) self.groups.grid(row=0, column=0, sticky="nsew") for group in sorted(self.app.core.services): self.groups.listbox.insert(tk.END, group) @@ -37,11 +36,15 @@ class ServicesSelectDialog(Dialog): self.groups.listbox.selection_set(0) self.services = CheckboxList( - frame, self.app, text="Services", clicked=self.service_clicked, padding=PAD + frame, + self.app, + text="Services", + clicked=self.service_clicked, + padding=FRAME_PAD, ) self.services.grid(row=0, column=1, sticky="nsew") - self.current = ListboxScroll(frame, text="Selected", padding=PAD) + self.current = ListboxScroll(frame, text="Selected", padding=FRAME_PAD) self.current.grid(row=0, column=2, sticky="nsew") for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) @@ -51,7 +54,7 @@ class ServicesSelectDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.destroy) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=1, sticky="ew") @@ -106,12 +109,12 @@ class CustomNodesDialog(Dialog): def draw_node_config(self): frame = ttk.Frame(self.top) - frame.grid(sticky="nsew", pady=PAD) + frame.grid(sticky="nsew", pady=PADY) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - self.nodes_list = ListboxScroll(frame, text="Nodes", padding=PAD) - self.nodes_list.grid(row=0, column=0, sticky="nsew", padx=PAD) + self.nodes_list = ListboxScroll(frame, text="Nodes", padding=FRAME_PAD) + self.nodes_list.grid(row=0, column=0, sticky="nsew", padx=PADX) self.nodes_list.listbox.bind("<>", self.handle_node_select) for name in sorted(self.app.core.custom_nodes): self.nodes_list.listbox.insert(tk.END, name) @@ -130,17 +133,17 @@ class CustomNodesDialog(Dialog): def draw_node_buttons(self): frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PAD) + frame.grid(sticky="ew", pady=PADY) for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Create", command=self.click_create) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) self.edit_button = ttk.Button( frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) - self.edit_button.grid(row=0, column=1, sticky="ew", padx=PAD) + self.edit_button.grid(row=0, column=1, sticky="ew", padx=PADX) self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete @@ -154,7 +157,7 @@ class CustomNodesDialog(Dialog): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.click_save) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + 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") diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index 49662cc4..92d9a7db 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -2,8 +2,7 @@ import tkinter as tk from tkinter import ttk from coretk.images import ImageEnum, Images - -DIALOG_PAD = 5 +from coretk.themes import DIALOG_PAD class Dialog(tk.Toplevel): diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index b16f7cd4..a1c30a6f 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -11,10 +11,9 @@ import grpc from coretk.dialogs.dialog import Dialog from coretk.errors import show_grpc_error from coretk.images import ImageEnum, Images +from coretk.themes import PADX, PADY from coretk.widgets import ConfigFrame -PAD = 5 - class GlobalEmaneDialog(Dialog): def __init__(self, master, app): @@ -27,7 +26,7 @@ class GlobalEmaneDialog(Dialog): self.top.rowconfigure(0, weight=1) self.config_frame = ConfigFrame(self.top, self.app, self.app.core.emane_config) self.config_frame.draw_config() - self.config_frame.grid(sticky="nsew", pady=PAD) + self.config_frame.grid(sticky="nsew", pady=PADY) self.draw_spacer() self.draw_buttons() @@ -37,7 +36,7 @@ class GlobalEmaneDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") @@ -68,7 +67,7 @@ class EmaneModelDialog(Dialog): self.top.rowconfigure(0, weight=1) self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame.draw_config() - self.config_frame.grid(sticky="nsew", pady=PAD) + self.config_frame.grid(sticky="nsew", pady=PADY) self.draw_spacer() self.draw_buttons() @@ -78,7 +77,7 @@ class EmaneModelDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + 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") @@ -126,7 +125,7 @@ class EmaneConfigDialog(Dialog): "\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details", justify=tk.CENTER, ) - label.grid(pady=PAD) + label.grid(pady=PADY) image = Images.get(ImageEnum.EDITNODE, 16) button = ttk.Button( @@ -139,7 +138,7 @@ class EmaneConfigDialog(Dialog): ), ) button.image = image - button.grid(sticky="ew", pady=PAD) + button.grid(sticky="ew", pady=PADY) def draw_emane_models(self): """ @@ -148,7 +147,7 @@ class EmaneConfigDialog(Dialog): :return: nothing """ frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PAD) + frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Model") @@ -166,7 +165,7 @@ class EmaneConfigDialog(Dialog): def draw_emane_buttons(self): frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PAD) + frame.grid(sticky="ew", pady=PADY) for i in range(2): frame.columnconfigure(i, weight=1) @@ -179,7 +178,7 @@ class EmaneConfigDialog(Dialog): command=self.click_model_config, ) self.emane_model_button.image = image - self.emane_model_button.grid(row=0, column=0, padx=PAD, sticky="ew") + self.emane_model_button.grid(row=0, column=0, padx=PADX, sticky="ew") image = Images.get(ImageEnum.EDITNODE, 16) button = ttk.Button( @@ -199,7 +198,7 @@ class EmaneConfigDialog(Dialog): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, padx=PAD, sticky="ew") + button.grid(row=0, column=0, padx=PADX, sticky="ew") button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index 2850d70e..40101823 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -3,10 +3,9 @@ from tkinter import ttk from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog +from coretk.themes import PADX, PADY from coretk.widgets import CodeText -PAD = 5 - class HookDialog(Dialog): def __init__(self, master, app): @@ -23,14 +22,14 @@ class HookDialog(Dialog): # name and states frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PAD) + frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(0, weight=2) frame.columnconfigure(1, weight=7) frame.columnconfigure(2, weight=1) label = ttk.Label(frame, text="Name") - label.grid(row=0, column=0, sticky="ew", padx=PAD) + label.grid(row=0, column=0, sticky="ew", padx=PADX) entry = ttk.Entry(frame, textvariable=self.name) - entry.grid(row=0, column=1, sticky="ew", padx=PAD) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) values = tuple(x for x in core_pb2.SessionState.Enum.keys() if x != "NONE") initial_state = core_pb2.SessionState.Enum.Name(core_pb2.SessionState.RUNTIME) self.state.set(initial_state) @@ -59,7 +58,7 @@ class HookDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=lambda: self.save()) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=1, sticky="ew") @@ -98,7 +97,7 @@ class HooksDialog(Dialog): self.top.rowconfigure(0, weight=1) self.listbox = tk.Listbox(self.top) - self.listbox.grid(sticky="nsew", pady=PAD) + self.listbox.grid(sticky="nsew", pady=PADY) self.listbox.bind("<>", self.select) for hook_file in self.app.core.hooks: self.listbox.insert(tk.END, hook_file) @@ -108,15 +107,15 @@ class HooksDialog(Dialog): for i in range(4): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Create", command=self.click_create) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) self.edit_button = ttk.Button( frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) - self.edit_button.grid(row=0, column=1, sticky="ew", padx=PAD) + self.edit_button.grid(row=0, column=1, sticky="ew", padx=PADX) self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete ) - self.delete_button.grid(row=0, column=2, sticky="ew", padx=PAD) + self.delete_button.grid(row=0, column=2, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=3, sticky="ew") diff --git a/coretk/coretk/dialogs/icondialog.py b/coretk/coretk/dialogs/icondialog.py index 9273a177..ecbb5d54 100644 --- a/coretk/coretk/dialogs/icondialog.py +++ b/coretk/coretk/dialogs/icondialog.py @@ -5,8 +5,7 @@ from coretk import nodeutils from coretk.appconfig import ICONS_PATH from coretk.dialogs.dialog import Dialog from coretk.images import Images - -PAD = 5 +from coretk.themes import PADX, PADY class IconDialog(Dialog): @@ -22,19 +21,19 @@ class IconDialog(Dialog): # row one frame = ttk.Frame(self.top) - frame.grid(pady=PAD, sticky="ew") + frame.grid(pady=PADY, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=3) label = ttk.Label(frame, text="Image") - label.grid(row=0, column=0, sticky="ew", padx=PAD) + label.grid(row=0, column=0, sticky="ew", padx=PADX) entry = ttk.Entry(frame, textvariable=self.file_path) - entry.grid(row=0, column=1, sticky="ew", padx=PAD) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) button = ttk.Button(frame, text="...", command=self.click_file) button.grid(row=0, column=2) # row two self.image_label = ttk.Label(self.top, image=self.image, anchor=tk.CENTER) - self.image_label.grid(pady=PAD, sticky="ew") + self.image_label.grid(pady=PADY, sticky="ew") # spacer self.draw_spacer() @@ -45,7 +44,7 @@ class IconDialog(Dialog): frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Apply", command=self.destroy) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/coretk/coretk/dialogs/mobilityconfig.py index 007209dd..19dc46f4 100644 --- a/coretk/coretk/dialogs/mobilityconfig.py +++ b/coretk/coretk/dialogs/mobilityconfig.py @@ -7,10 +7,9 @@ import grpc from coretk.dialogs.dialog import Dialog from coretk.errors import show_grpc_error +from coretk.themes import PADX, PADY from coretk.widgets import ConfigFrame -PAD = 5 - class MobilityConfigDialog(Dialog): def __init__(self, master, app, canvas_node): @@ -35,7 +34,7 @@ class MobilityConfigDialog(Dialog): self.top.rowconfigure(0, weight=1) self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame.draw_config() - self.config_frame.grid(sticky="nsew", pady=PAD) + self.config_frame.grid(sticky="nsew", pady=PADY) self.draw_apply_buttons() def draw_apply_buttons(self): @@ -45,7 +44,7 @@ class MobilityConfigDialog(Dialog): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, padx=PAD, sticky="ew") + button.grid(row=0, column=0, padx=PADX, sticky="ew") button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index 1e7b4dc1..f0b46499 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -7,8 +7,8 @@ from core.api.grpc.core_pb2 import MobilityAction from coretk.dialogs.dialog import Dialog from coretk.errors import show_grpc_error from coretk.images import ImageEnum, Images +from coretk.themes import PADX, PADY -PAD = 5 ICON_SIZE = 16 @@ -77,36 +77,36 @@ class MobilityPlayerDialog(Dialog): file_name = self.config["file"].value label = ttk.Label(self.top, text=file_name) - label.grid(sticky="ew", pady=PAD) + label.grid(sticky="ew", pady=PADY) self.progressbar = ttk.Progressbar(self.top, mode="indeterminate") - self.progressbar.grid(sticky="ew", pady=PAD) + self.progressbar.grid(sticky="ew", pady=PADY) frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PAD) + frame.grid(sticky="ew", pady=PADY) for i in range(3): frame.columnconfigure(i, weight=1) image = Images.get(ImageEnum.START, width=ICON_SIZE) self.play_button = ttk.Button(frame, image=image, command=self.click_play) self.play_button.image = image - self.play_button.grid(row=0, column=0, sticky="ew", padx=PAD) + self.play_button.grid(row=0, column=0, sticky="ew", padx=PADX) image = Images.get(ImageEnum.PAUSE, width=ICON_SIZE) self.pause_button = ttk.Button(frame, image=image, command=self.click_pause) self.pause_button.image = image - self.pause_button.grid(row=0, column=1, sticky="ew", padx=PAD) + self.pause_button.grid(row=0, column=1, sticky="ew", padx=PADX) image = Images.get(ImageEnum.STOP, width=ICON_SIZE) self.stop_button = ttk.Button(frame, image=image, command=self.click_stop) self.stop_button.image = image - self.stop_button.grid(row=0, column=2, sticky="ew", padx=PAD) + self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX) loop = tk.IntVar(value=int(self.config["loop"].value == "1")) checkbutton = ttk.Checkbutton( frame, text="Loop?", variable=loop, state=tk.DISABLED ) - checkbutton.grid(row=0, column=3, padx=PAD) + checkbutton.grid(row=0, column=3, padx=PADX) rate = self.config["refresh_ms"].value label = ttk.Label(frame, text=f"rate {rate} ms") diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 619c5ee7..56c2a08d 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -7,10 +7,9 @@ from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeService from coretk.nodeutils import NodeUtils +from coretk.themes import FRAME_PAD, PADX, PADY from coretk.widgets import FrameScroll -PAD = 5 - def mac_auto(is_auto, entry): logging.info("mac auto clicked") @@ -68,7 +67,7 @@ class NodeConfigDialog(Dialog): # icon field label = ttk.Label(frame, text="Icon") - label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) self.image_button = ttk.Button( frame, text="Icon", @@ -81,7 +80,7 @@ class NodeConfigDialog(Dialog): # name field label = ttk.Label(frame, text="Name") - label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) entry = ttk.Entry( frame, textvariable=self.name, @@ -97,7 +96,7 @@ class NodeConfigDialog(Dialog): # node type field if NodeUtils.is_model_node(self.node.type): label = ttk.Label(frame, text="Type") - label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) combobox = ttk.Combobox( frame, textvariable=self.type, @@ -110,7 +109,7 @@ class NodeConfigDialog(Dialog): # container image field if NodeUtils.is_image_node(self.node.type): label = ttk.Label(frame, text="Image") - label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) entry = ttk.Entry(frame, textvariable=self.container_image) entry.grid(row=row, column=1, sticky="ew") row += 1 @@ -120,7 +119,7 @@ class NodeConfigDialog(Dialog): frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Server") - label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) servers = ["localhost"] servers.extend(list(sorted(self.app.core.servers.keys()))) combobox = ttk.Combobox( @@ -131,7 +130,7 @@ class NodeConfigDialog(Dialog): # services button = ttk.Button(self.top, text="Services", command=self.click_services) - button.grid(sticky="ew", pady=PAD) + button.grid(sticky="ew", pady=PADY) # interfaces if self.canvas_node.interfaces: @@ -147,17 +146,17 @@ class NodeConfigDialog(Dialog): scroll.frame.rowconfigure(0, weight=1) for interface in self.canvas_node.interfaces: logging.info("interface: %s", interface) - frame = ttk.LabelFrame(scroll.frame, text=interface.name, padding=PAD) - frame.grid(sticky="ew", pady=PAD) + frame = ttk.LabelFrame(scroll.frame, text=interface.name, padding=FRAME_PAD) + frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) label = ttk.Label(frame, text="MAC") - label.grid(row=0, column=0, padx=PAD, pady=PAD) + label.grid(row=0, column=0, padx=PADX, pady=PADY) is_auto = tk.BooleanVar(value=True) checkbutton = ttk.Checkbutton(frame, text="Auto?", variable=is_auto) checkbutton.var = is_auto - checkbutton.grid(row=0, column=1, padx=PAD) + checkbutton.grid(row=0, column=1, padx=PADX) mac = tk.StringVar(value=interface.mac) entry = ttk.Entry(frame, textvariable=mac, state=tk.DISABLED) entry.grid(row=0, column=2, sticky="ew") @@ -165,14 +164,14 @@ class NodeConfigDialog(Dialog): checkbutton.config(command=func) label = ttk.Label(frame, text="IPv4") - label.grid(row=1, column=0, padx=PAD, pady=PAD) + label.grid(row=1, column=0, padx=PADX, pady=PADY) ip4 = tk.StringVar(value=f"{interface.ip4}/{interface.ip4mask}") entry = ttk.Entry(frame, textvariable=ip4) entry.bind("", self.app.validation.ip_focus_out) entry.grid(row=1, column=1, columnspan=2, sticky="ew") label = ttk.Label(frame, text="IPv6") - label.grid(row=2, column=0, padx=PAD, pady=PAD) + label.grid(row=2, column=0, padx=PADX, pady=PADY) ip6 = tk.StringVar(value=f"{interface.ip6}/{interface.ip6mask}") entry = ttk.Entry(frame, textvariable=ip6) entry.bind("", self.app.validation.ip_focus_out) @@ -187,7 +186,7 @@ class NodeConfigDialog(Dialog): frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Apply", command=self.config_apply) - button.grid(row=0, column=0, padx=2, sticky="ew") + button.grid(row=0, column=0, padx=PADX, sticky="ew") button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index dfcef922..c49aea50 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -6,10 +6,9 @@ from tkinter import messagebox, ttk from coretk.dialogs.dialog import Dialog from coretk.dialogs.serviceconfiguration import ServiceConfiguration +from coretk.themes import FRAME_PAD, PADX, PADY from coretk.widgets import CheckboxList, ListboxScroll -PAD = 5 - class NodeService(Dialog): def __init__(self, master, app, canvas_node, services=None): @@ -36,11 +35,11 @@ class NodeService(Dialog): self.top.rowconfigure(0, weight=1) frame = ttk.Frame(self.top) - frame.grid(stick="nsew") + frame.grid(stick="nsew", pady=PADY) frame.rowconfigure(0, weight=1) for i in range(3): frame.columnconfigure(i, weight=1) - self.groups = ListboxScroll(frame, text="Groups", padding=PAD) + self.groups = ListboxScroll(frame, text="Groups", padding=FRAME_PAD) self.groups.grid(row=0, column=0, sticky="nsew") for group in sorted(self.app.core.services): self.groups.listbox.insert(tk.END, group) @@ -48,11 +47,15 @@ class NodeService(Dialog): self.groups.listbox.selection_set(0) self.services = CheckboxList( - frame, self.app, text="Services", clicked=self.service_clicked, padding=PAD + frame, + self.app, + text="Services", + clicked=self.service_clicked, + padding=FRAME_PAD, ) self.services.grid(row=0, column=1, sticky="nsew") - self.current = ListboxScroll(frame, text="Selected", padding=PAD) + self.current = ListboxScroll(frame, text="Selected", padding=FRAME_PAD) self.current.grid(row=0, column=2, sticky="nsew") for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) @@ -62,9 +65,9 @@ class NodeService(Dialog): for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Configure", command=self.click_configure) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Save", command=self.click_save) - button.grid(row=0, column=1, sticky="ew", padx=PAD) + button.grid(row=0, column=1, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=2, sticky="ew") diff --git a/coretk/coretk/dialogs/observers.py b/coretk/coretk/dialogs/observers.py index 525939e0..f496f7ef 100644 --- a/coretk/coretk/dialogs/observers.py +++ b/coretk/coretk/dialogs/observers.py @@ -3,8 +3,7 @@ from tkinter import ttk from coretk.coreclient import Observer from coretk.dialogs.dialog import Dialog - -PAD = 5 +from coretk.themes import PADX, PADY class ObserverDialog(Dialog): @@ -29,7 +28,7 @@ class ObserverDialog(Dialog): def draw_listbox(self): frame = ttk.Frame(self.top) - frame.grid(sticky="nsew", pady=PAD) + frame.grid(sticky="nsew", pady=PADY) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) @@ -48,32 +47,32 @@ class ObserverDialog(Dialog): def draw_form_fields(self): frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PAD) + frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Name") - label.grid(row=0, column=0, sticky="w", padx=PAD) + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=1, sticky="ew") label = ttk.Label(frame, text="Command") - label.grid(row=1, column=0, sticky="w", padx=PAD) + label.grid(row=1, column=0, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.cmd) entry.grid(row=1, column=1, sticky="ew") def draw_config_buttons(self): frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PAD) + frame.grid(sticky="ew", pady=PADY) for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Create", command=self.click_create) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PADX) self.save_button = ttk.Button( frame, text="Save", state=tk.DISABLED, command=self.click_save ) - self.save_button.grid(row=0, column=1, sticky="ew") + self.save_button.grid(row=0, column=1, sticky="ew", padx=PADX) self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete @@ -87,7 +86,7 @@ class ObserverDialog(Dialog): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.click_save_config) - button.grid(row=0, column=0, sticky="ew") + 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") diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py index b094eec6..8c369027 100644 --- a/coretk/coretk/dialogs/preferences.py +++ b/coretk/coretk/dialogs/preferences.py @@ -4,8 +4,7 @@ from tkinter import ttk from coretk import appconfig from coretk.dialogs.dialog import Dialog - -PAD = 5 +from coretk.themes import FRAME_PAD, PADX, PADY class PreferencesDialog(Dialog): @@ -25,12 +24,12 @@ class PreferencesDialog(Dialog): self.draw_buttons() def draw_preferences(self): - frame = ttk.LabelFrame(self.top, text="Preferences", padding=PAD) - frame.grid(sticky="nsew", pady=2) + frame = ttk.LabelFrame(self.top, text="Preferences", padding=FRAME_PAD) + frame.grid(sticky="nsew", pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Theme") - label.grid(row=0, column=0, pady=PAD, padx=PAD, sticky="w") + label.grid(row=0, column=0, pady=PADY, padx=PADX, sticky="w") themes = self.app.style.theme_names() combobox = ttk.Combobox( frame, textvariable=self.theme, values=themes, state="readonly" @@ -40,14 +39,14 @@ class PreferencesDialog(Dialog): combobox.bind("<>", self.theme_change) label = ttk.Label(frame, text="Editor") - label.grid(row=1, column=0, pady=PAD, padx=PAD, sticky="w") + label.grid(row=1, column=0, pady=PADY, padx=PADX, sticky="w") combobox = ttk.Combobox( frame, textvariable=self.editor, values=appconfig.EDITORS, state="readonly" ) combobox.grid(row=1, column=1, sticky="ew") label = ttk.Label(frame, text="Terminal") - label.grid(row=2, column=0, pady=PAD, padx=PAD, sticky="w") + label.grid(row=2, column=0, pady=PADY, padx=PADX, sticky="w") combobox = ttk.Combobox( frame, textvariable=self.terminal, @@ -57,7 +56,7 @@ class PreferencesDialog(Dialog): combobox.grid(row=2, column=1, sticky="ew") label = ttk.Label(frame, text="3D GUI") - label.grid(row=3, column=0, pady=PAD, padx=PAD, sticky="w") + label.grid(row=3, column=0, pady=PADY, padx=PADX, sticky="w") entry = ttk.Entry(frame, textvariable=self.gui3d) entry.grid(row=3, column=1, sticky="ew") @@ -68,7 +67,7 @@ class PreferencesDialog(Dialog): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.click_save) - button.grid(row=0, column=0, sticky="ew") + 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") diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index 18c5d6d1..ef406ecb 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -3,8 +3,8 @@ from tkinter import ttk from coretk.coreclient import CoreServer from coretk.dialogs.dialog import Dialog +from coretk.themes import FRAME_PAD, PADX, PADY -PAD = 5 DEFAULT_NAME = "example" DEFAULT_ADDRESS = "127.0.0.1" DEFAULT_PORT = 50051 @@ -33,7 +33,7 @@ class ServersDialog(Dialog): def draw_servers(self): frame = ttk.Frame(self.top) - frame.grid(pady=PAD, sticky="nsew") + frame.grid(pady=PADY, sticky="nsew") frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) @@ -52,24 +52,24 @@ class ServersDialog(Dialog): scrollbar.config(command=self.servers.yview) def draw_server_configuration(self): - frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=PAD) - frame.grid(pady=PAD, sticky="ew") + frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=FRAME_PAD) + frame.grid(pady=PADY, sticky="ew") frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) frame.columnconfigure(5, weight=1) label = ttk.Label(frame, text="Name") - label.grid(row=0, column=0, sticky="w", padx=PAD, pady=PAD) + label.grid(row=0, column=0, sticky="w", padx=PADX, pady=PADY) entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=1, sticky="ew") label = ttk.Label(frame, text="Address") - label.grid(row=0, column=2, sticky="w", padx=PAD, pady=PAD) + label.grid(row=0, column=2, sticky="w", padx=PADX, pady=PADY) entry = ttk.Entry(frame, textvariable=self.address) entry.grid(row=0, column=3, sticky="ew") label = ttk.Label(frame, text="Port") - label.grid(row=0, column=4, sticky="w", padx=PAD, pady=PAD) + label.grid(row=0, column=4, sticky="w", padx=PADX, pady=PADY) entry = ttk.Entry( frame, textvariable=self.port, @@ -83,17 +83,17 @@ class ServersDialog(Dialog): def draw_servers_buttons(self): frame = ttk.Frame(self.top) - frame.grid(pady=PAD, sticky="ew") + frame.grid(pady=PADY, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Create", command=self.click_create) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) self.save_button = ttk.Button( frame, text="Save", state=tk.DISABLED, command=self.click_save ) - self.save_button.grid(row=0, column=1, sticky="ew", padx=PAD) + self.save_button.grid(row=0, column=1, sticky="ew", padx=PADX) self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete @@ -109,7 +109,7 @@ class ServersDialog(Dialog): button = ttk.Button( frame, text="Save Configuration", command=self.click_save_configuration ) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + 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") diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index 7c77f9df..24ff7381 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -5,11 +5,9 @@ import grpc from coretk.dialogs.dialog import Dialog from coretk.errors import show_grpc_error +from coretk.themes import PADX, PADY from coretk.widgets import ConfigFrame -PAD_X = 2 -PAD_Y = 2 - class SessionOptionsDialog(Dialog): def __init__(self, master, app): @@ -33,16 +31,16 @@ class SessionOptionsDialog(Dialog): self.config_frame = ConfigFrame(self.top, self.app, config=self.config) self.config_frame.draw_config() - self.config_frame.grid(sticky="nsew") + self.config_frame.grid(sticky="nsew", pady=PADY) frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.save) - button.grid(row=0, column=0, pady=PAD_Y, padx=PAD_X, sticky="ew") + button.grid(row=0, column=0, padx=PADX, sticky="ew") button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, pady=PAD_Y, padx=PAD_X, sticky="ew") + button.grid(row=0, column=1, padx=PADX, sticky="ew") def save(self): config = self.config_frame.parse_config() diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index f8d26667..b1fb970f 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -9,8 +9,7 @@ from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog from coretk.errors import show_grpc_error from coretk.images import ImageEnum, Images - -PAD = 5 +from coretk.themes import PADX, PADY class SessionsDialog(Dialog): @@ -51,13 +50,13 @@ class SessionsDialog(Dialog): "one you might be concurrently editting.", justify=tk.CENTER, ) - label.grid(pady=PAD) + label.grid(pady=PADY) def draw_tree(self): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - frame.grid(sticky="nsew") + frame.grid(sticky="nsew", pady=PADY) self.tree = ttk.Treeview( frame, columns=("id", "state", "nodes"), show="headings" ) @@ -85,7 +84,7 @@ class SessionsDialog(Dialog): self.tree.configure(yscrollcommand=yscrollbar.set) xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) - xscrollbar.grid(row=1, sticky="ew", pady=5) + xscrollbar.grid(row=1, sticky="ew") self.tree.configure(xscrollcommand=xscrollbar.set) def draw_buttons(self): @@ -99,7 +98,7 @@ class SessionsDialog(Dialog): frame, image=image, text="New", compound=tk.LEFT, command=self.click_new ) b.image = image - b.grid(row=0, padx=2, sticky="ew") + b.grid(row=0, padx=PADX, sticky="ew") image = Images.get(ImageEnum.FILEOPEN, 16) b = ttk.Button( @@ -110,7 +109,7 @@ class SessionsDialog(Dialog): command=self.click_connect, ) b.image = image - b.grid(row=0, column=1, padx=2, sticky="ew") + b.grid(row=0, column=1, padx=PADX, sticky="ew") image = Images.get(ImageEnum.EDITDELETE, 16) b = ttk.Button( @@ -121,10 +120,10 @@ class SessionsDialog(Dialog): command=self.click_shutdown, ) b.image = image - b.grid(row=0, column=2, padx=2, sticky="ew") + b.grid(row=0, column=2, padx=PADX, sticky="ew") b = ttk.Button(frame, text="Cancel", command=self.click_new) - b.grid(row=0, column=3, padx=2, sticky="ew") + b.grid(row=0, column=3, sticky="ew") def click_new(self): self.app.core.create_new_session() diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index 63037c9d..a6da0e3b 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -7,9 +7,8 @@ from tkinter import colorchooser, font, ttk from coretk.dialogs.dialog import Dialog from coretk.graph import tags from coretk.graph.shapeutils import is_draw_shape, is_shape_text +from coretk.themes import FRAME_PAD, PADX, PADY -PADX = (0, 5) -PAD = 5 FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] @@ -50,16 +49,16 @@ class ShapeDialog(Dialog): self.draw_buttons() def draw_label_options(self): - label_frame = ttk.LabelFrame(self.top, text="Label", padding=PAD) + label_frame = ttk.LabelFrame(self.top, text="Label", padding=FRAME_PAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) entry = ttk.Entry(label_frame, textvariable=self.shape_text) - entry.grid(sticky="ew", pady=PAD) + entry.grid(sticky="ew", pady=PADY) # font options frame = ttk.Frame(label_frame) - frame.grid(sticky="nsew", padx=3, pady=3) + frame.grid(sticky="nsew", pady=PADY) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) @@ -73,7 +72,7 @@ class ShapeDialog(Dialog): combobox = ttk.Combobox( frame, textvariable=self.font_size, values=FONT_SIZES, state="readonly" ) - combobox.grid(row=0, column=1, padx=3, sticky="nsew") + combobox.grid(row=0, column=1, padx=PADX, sticky="nsew") button = ttk.Button(frame, text="Color", command=self.choose_text_color) button.grid(row=0, column=2, sticky="nsew") @@ -85,13 +84,13 @@ class ShapeDialog(Dialog): button = ttk.Checkbutton(frame, variable=self.bold, text="Bold") button.grid(row=0, column=0, sticky="ew") button = ttk.Checkbutton(frame, variable=self.italic, text="Italic") - button.grid(row=0, column=1, padx=3, sticky="ew") + button.grid(row=0, column=1, padx=PADX, sticky="ew") button = ttk.Checkbutton(frame, variable=self.underline, text="Underline") button.grid(row=0, column=2, sticky="ew") def draw_shape_options(self): - label_frame = ttk.LabelFrame(self.top, text="Shape", padding=PAD) - label_frame.grid(sticky="ew", pady=PAD) + label_frame = ttk.LabelFrame(self.top, text="Shape", padding=FRAME_PAD) + label_frame.grid(sticky="ew", pady=PADY) label_frame.columnconfigure(0, weight=1) frame = ttk.Frame(label_frame) @@ -115,7 +114,7 @@ class ShapeDialog(Dialog): button.grid(row=1, column=2, sticky="ew") frame = ttk.Frame(label_frame) - frame.grid(sticky="ew", pady=PAD) + frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Border Width") label.grid(row=0, column=0, sticky="w", padx=PADX) diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index 711f94f0..aed411b8 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -8,10 +8,9 @@ import grpc from coretk.dialogs.dialog import Dialog from coretk.errors import show_grpc_error +from coretk.themes import PADX, PADY from coretk.widgets import ConfigFrame -PAD = 5 - class WlanConfigDialog(Dialog): def __init__(self, master, app, canvas_node): @@ -33,7 +32,7 @@ class WlanConfigDialog(Dialog): self.top.rowconfigure(0, weight=1) self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame.draw_config() - self.config_frame.grid(sticky="nsew", pady=PAD) + self.config_frame.grid(sticky="nsew", pady=PADY) self.draw_apply_buttons() def draw_apply_buttons(self): @@ -48,7 +47,7 @@ class WlanConfigDialog(Dialog): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, padx=PAD, sticky="ew") + button.grid(row=0, column=0, padx=PADX, sticky="ew") button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index a870b278..cb8a8b06 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -1,7 +1,11 @@ import logging import tkinter as tk -DARK = "black" +THEME_DARK = "black" +PADX = (0, 5) +PADY = (0, 5) +FRAME_PAD = 5 +DIALOG_PAD = 5 class Styles: @@ -28,7 +32,7 @@ class Colors: def load(style): style.theme_create( - DARK, + THEME_DARK, "clam", { ".": { diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index e3fd5c35..cdbfea28 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -5,6 +5,7 @@ from tkinter import filedialog, font, ttk from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 +from coretk.themes import FRAME_PAD, PADX, PADY INT_TYPES = { core_pb2.ConfigOptionType.UINT8, @@ -16,7 +17,6 @@ INT_TYPES = { core_pb2.ConfigOptionType.INT32, core_pb2.ConfigOptionType.INT64, } -PAD = 5 def file_button_click(value): @@ -71,8 +71,6 @@ class ConfigFrame(FrameScroll): self.values = {} def draw_config(self): - padx = 2 - pady = 2 group_mapping = {} for key in self.config: option = self.config[key] @@ -81,19 +79,19 @@ class ConfigFrame(FrameScroll): for group_name in sorted(group_mapping): group = group_mapping[group_name] - frame = ttk.Frame(self.frame, padding=PAD) + frame = ttk.Frame(self.frame, padding=FRAME_PAD) frame.columnconfigure(1, weight=1) self.frame.add(frame, text=group_name) for index, option in enumerate(sorted(group, key=lambda x: x.name)): label = ttk.Label(frame, text=option.label) - label.grid(row=index, pady=pady, padx=padx, sticky="w") + label.grid(row=index, pady=PADY, padx=PADX, sticky="w") value = tk.StringVar() if option.type == core_pb2.ConfigOptionType.BOOL: select = tuple(option.select) combobox = ttk.Combobox( frame, textvariable=value, values=select, state="readonly" ) - combobox.grid(row=index, column=1, sticky="ew", pady=pady) + combobox.grid(row=index, column=1, sticky="ew") if option.value == "1": value.set("On") else: @@ -104,15 +102,15 @@ class ConfigFrame(FrameScroll): combobox = ttk.Combobox( frame, textvariable=value, values=select, state="readonly" ) - combobox.grid(row=index, column=1, sticky="ew", pady=pady) + combobox.grid(row=index, column=1, sticky="ew") elif option.type == core_pb2.ConfigOptionType.STRING: value.set(option.value) if "file" in option.label: file_frame = ttk.Frame(frame) - file_frame.grid(row=index, column=1, sticky="ew", pady=pady) + file_frame.grid(row=index, column=1, sticky="ew") file_frame.columnconfigure(0, weight=1) entry = ttk.Entry(file_frame, textvariable=value) - entry.grid(row=0, column=0, sticky="ew", padx=padx) + entry.grid(row=0, column=0, sticky="ew", padx=PADX) func = partial(file_button_click, value) button = ttk.Button(file_frame, text="...", command=func) button.grid(row=0, column=1) @@ -124,10 +122,10 @@ class ConfigFrame(FrameScroll): validate="key", validatecommand=(self.app.validation.ip4, "%P"), ) - entry.grid(row=index, column=1, sticky="ew", pady=pady) + entry.grid(row=index, column=1, sticky="ew") else: entry = ttk.Entry(frame, textvariable=value) - entry.grid(row=index, column=1, sticky="ew", pady=pady) + entry.grid(row=index, column=1, sticky="ew") elif option.type in INT_TYPES: value.set(option.value) @@ -141,7 +139,7 @@ class ConfigFrame(FrameScroll): "", lambda event: self.app.validation.focus_out(event, "0"), ) - entry.grid(row=index, column=1, sticky="ew", pady=pady) + entry.grid(row=index, column=1, sticky="ew") elif option.type == core_pb2.ConfigOptionType.FLOAT: value.set(option.value) entry = ttk.Entry( @@ -154,7 +152,7 @@ class ConfigFrame(FrameScroll): "", lambda event: self.app.validation.focus_out(event, "0"), ) - entry.grid(row=index, column=1, sticky="ew", pady=pady) + entry.grid(row=index, column=1, sticky="ew") else: logging.error("unhandled config option type: %s", option.type) self.values[option.name] = value From d6ae39089ed3da8c1d591eaafc2a817f8e467d4a Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 11 Dec 2019 14:42:00 -0800 Subject: [PATCH 362/462] select box in select mode, link to selected for wlan/emane nodes, send session data to damon before save xml --- coretk/coretk/coreclient.py | 59 +++++++++++++++++++++++++++++--- coretk/coretk/graph/graph.py | 66 +++++++++++++++++++++++++++++++----- coretk/coretk/graph/node.py | 15 +++++++- coretk/coretk/graph/shape.py | 3 ++ 4 files changed, 130 insertions(+), 13 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 827f4d01..36562cce 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -421,8 +421,8 @@ class CoreClient: mobility_configs = self.get_mobility_configs_proto() emane_model_configs = self.get_emane_model_configs_proto() hooks = list(self.hooks.values()) - service_configs = self.get_service_config_proto() - file_configs = self.get_service_file_config_proto() + service_configs = self.get_service_configs_proto() + file_configs = self.get_service_file_configs_proto() if self.emane_config: emane_config = {x: self.emane_config[x].value for x in self.emane_config} else: @@ -512,6 +512,11 @@ class CoreClient: :return: nothing """ try: + if self.state != core_pb2.SessionState.RUNTIME: + logging.debug( + "session state not runtime, send session data to the daemon..." + ) + self.send_data() response = self.client.save_xml(self.session_id, file_path) logging.info("saved xml(%s): %s", file_path, response) except grpc.RpcError as e: @@ -586,6 +591,52 @@ class CoreClient: ) logging.debug("create link: %s", response) + def send_data(self): + """ + send to daemon all session info, but don't start the session + + :return: nothing + """ + self.create_nodes_and_links() + for config_proto in self.get_wlan_configs_proto(): + self.client.set_wlan_config( + self.session_id, config_proto.node_id, config_proto.config + ) + for config_proto in self.get_mobility_configs_proto(): + self.client.set_mobility_config( + self.session_id, config_proto.node_id, config_proto.config + ) + for config_proto in self.get_service_configs_proto(): + self.client.set_node_service( + self.session_id, + config_proto.node_id, + config_proto.service, + config_proto.startup, + config_proto.validate, + config_proto.shutdown, + ) + for config_proto in self.get_service_file_configs_proto(): + self.client.set_node_service_file( + self.session_id, + config_proto.node_id, + config_proto.service, + config_proto.file, + config_proto.data, + ) + for hook in self.hooks.values(): + self.client.add_hook(self.session_id, hook.state, hook.file, hook.data) + for config_proto in self.get_emane_model_configs_proto(): + self.client.set_emane_model_config( + self.session_id, + config_proto.node_id, + config_proto.model, + config_proto.config, + config_proto.interface_id, + ) + if self.emane_config: + config = {x: self.emane_config[x].value for x in self.emane_config} + self.client.set_emane_config(self.session_id, config) + def close(self): """ Clean ups when done using grpc @@ -762,7 +813,7 @@ class CoreClient: configs.append(config_proto) return configs - def get_service_config_proto(self): + def get_service_configs_proto(self): configs = [] for node_id, services in self.service_configs.items(): for name, config in services.items(): @@ -776,7 +827,7 @@ class CoreClient: configs.append(config_proto) return configs - def get_service_file_config_proto(self): + def get_service_file_configs_proto(self): configs = [] for (node_id, file_configs) in self.file_configs.items(): for service, file_config in file_configs.items(): diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index fc5d6fd7..e79e0f9c 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -11,7 +11,7 @@ from coretk.graph.enums import GraphMode, ScaleOption from coretk.graph.linkinfo import LinkInfo, Throughput from coretk.graph.node import CanvasNode from coretk.graph.shape import Shape -from coretk.graph.shapeutils import is_draw_shape +from coretk.graph.shapeutils import ShapeType, is_draw_shape from coretk.nodeutils import NodeUtils SCROLL_BUFFER = 25 @@ -32,6 +32,7 @@ class CanvasGraph(tk.Canvas): self.mode = GraphMode.SELECT self.annotation_type = None self.selection = {} + self.select_box = None self.selected = None self.node_draw = None self.context = None @@ -137,6 +138,13 @@ class CanvasGraph(tk.Canvas): self.tag_lower(self.grid) def add_wireless_edge(self, src, dst): + """ + add a wireless edge between 2 canvas nodes + + :param CanvasNode src: source node + :param CanvasNode dst: destination node + :return: nothing + """ token = tuple(sorted((src.id, dst.id))) x1, y1 = self.coords(src.id) x2, y2 = self.coords(dst.id) @@ -249,6 +257,7 @@ class CanvasGraph(tk.Canvas): :param event: mouse event :return: nothing """ + logging.debug("click release") if self.context: self.context.unpost() self.context = None @@ -260,6 +269,19 @@ class CanvasGraph(tk.Canvas): shape = self.shapes[self.selected] shape.shape_complete(x, y) self.shape_drawing = False + elif self.mode == GraphMode.SELECT: + self.focus_set() + if self.select_box: + x0, y0, x1, y1 = self.coords(self.select_box.id) + inside = [ + x + for x in self.find_enclosed(x0, y0, x1, y1) + if "node" in self.gettags(x) or "shape" in self.gettags(x) + ] + for i in inside: + self.select_object(i, True) + self.select_box.disappear() + self.select_box = None else: self.focus_set() self.selected = self.get_selected(event) @@ -445,6 +467,10 @@ class CanvasGraph(tk.Canvas): self.select_object(node.id) self.selected = selected else: + logging.debug("create selection box") + if self.mode == GraphMode.SELECT: + shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y) + self.select_box = shape self.clear_selection() def ctrl_click(self, event): @@ -486,14 +512,18 @@ class CanvasGraph(tk.Canvas): return # move selected objects - for selected_id in self.selection: - if selected_id in self.shapes: - shape = self.shapes[selected_id] - shape.motion(x_offset, y_offset) + if len(self.selection) > 0: + for selected_id in self.selection: + if selected_id in self.shapes: + shape = self.shapes[selected_id] + shape.motion(x_offset, y_offset) - if selected_id in self.nodes: - node = self.nodes[selected_id] - node.motion(x_offset, y_offset, update=self.core.is_runtime()) + if selected_id in self.nodes: + node = self.nodes[selected_id] + node.motion(x_offset, y_offset, update=self.core.is_runtime()) + else: + if self.select_box and self.mode == GraphMode.SELECT: + self.select_box.shape_motion(x, y) def click_context(self, event): logging.info("context event: %s", self.context) @@ -681,3 +711,23 @@ class CanvasGraph(tk.Canvas): def is_selection_mode(self): return self.mode == GraphMode.SELECT + + def create_edge(self, source, dest): + """ + create an edge between source node and destination node + + :param CanvasNode source: source node + :param CanvasNode dest: destination node + :return: nothing + """ + if tuple([source.id, dest.id]) not in self.edges: + pos0 = source.core_node.position + x0 = pos0.x + y0 = pos0.y + edge = CanvasEdge(x0, y0, x0, y0, source.id, self) + edge.complete(dest.id) + self.edges[edge.token] = edge + self.nodes[source.id].edges.add(edge) + self.nodes[dest.id].edges.add(edge) + link = self.core.create_link(edge, source, dest) + edge.link_info = LinkInfo(self, edge, link) diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 5757f081..bdad50e6 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -3,6 +3,7 @@ from tkinter import font import grpc +from core.api.grpc import core_pb2 from core.api.grpc.core_pb2 import NodeType from coretk.dialogs.emaneconfig import EmaneConfigDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog @@ -191,7 +192,9 @@ class CanvasNode: label="Mobility Config", command=self.show_mobility_config ) if NodeUtils.is_wireless_node(self.core_node.type): - context.add_command(label="Link To All MDRs", state=tk.DISABLED) + context.add_command( + label="Link To Selected", command=self.wireless_link_selected + ) context.add_command(label="Select Members", state=tk.DISABLED) context.add_command(label="Select Adjacent", state=tk.DISABLED) context.add_command(label="Create Link To", state=tk.DISABLED) @@ -233,3 +236,13 @@ class CanvasNode: self.canvas.context = None dialog = NodeService(self.app.master, self.app, self) dialog.show() + + def wireless_link_selected(self): + self.canvas.context = None + for canvas_nid in [ + x for x in self.canvas.selection if "node" in self.canvas.gettags(x) + ]: + core_node = self.canvas.nodes[canvas_nid].core_node + if core_node.type == core_pb2.NodeType.DEFAULT and core_node.model == "mdr": + self.canvas.create_edge(self, self.canvas.nodes[canvas_nid]) + self.canvas.clear_selection() diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 56e679d2..80621107 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -132,6 +132,9 @@ class Shape: s = ShapeDialog(self.app, self.app, self) s.show() + def disappear(self): + self.canvas.delete(self.id) + def motion(self, x_offset, y_offset): self.canvas.move(self.id, x_offset, y_offset) self.canvas.move_selection(self.id, x_offset, y_offset) From 80fb0e26b61f1a5d846b920878be014ccbbc8a00 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 11 Dec 2019 16:16:59 -0800 Subject: [PATCH 363/462] attempt to work on check engine light dialog --- coretk/coretk/dialogs/cel.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/dialogs/cel.py b/coretk/coretk/dialogs/cel.py index 847d245b..a5fbea63 100644 --- a/coretk/coretk/dialogs/cel.py +++ b/coretk/coretk/dialogs/cel.py @@ -1,6 +1,7 @@ """ check engine light """ +import tkinter as tk from tkinter import ttk from coretk.dialogs.dialog import Dialog @@ -17,9 +18,14 @@ class CheckLight(Dialog): def draw(self): row = 0 frame = ttk.Frame(self) + label = ttk.Label(frame, text="Check Emulation Light") + label.grid(row=0, column=0) + frame.grid(row=row, column=0) + row = row + 1 + frame = ttk.Frame(self) button = ttk.Button(frame, text="Reset CEL") button.grid(row=0, column=0) - button = ttk.Button(frame, text="View core-daemon log") + button = ttk.Button(frame, text="View core-daemon log", command=self.daemon_log) button.grid(row=0, column=1) button = ttk.Button(frame, text="View node log") button.grid(row=0, column=2) @@ -27,3 +33,23 @@ class CheckLight(Dialog): button.grid(row=0, column=3) frame.grid(row=row, column=0, sticky="nsew") ++row + + def daemon_log(self): + dialog = DaemonLog(self, self.app) + dialog.show() + + +class DaemonLog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "core-daemon log", modal=True) + self.columnconfigure(0, weight=1) + self.path = tk.StringVar(value="/var/log/core-daemon.log") + self.draw() + + def draw(self): + frame = ttk.Frame(self) + label = ttk.Label(frame, text="File: ") + label.grid(row=0, column=0) + entry = ttk.Entry(frame, textvariable=self.path, state="readonly") + entry.grid(row=0, column=1) + frame.grid(row=0, column=0) From 9c9ccdf04fb8626ef7040efbdd3e1d090b8875c5 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 11 Dec 2019 16:21:37 -0800 Subject: [PATCH 364/462] some cleanup to layout for service configuration, updated titles for service config and node config dialogs --- coretk/coretk/dialogs/nodeservice.py | 3 +- coretk/coretk/dialogs/serviceconfiguration.py | 264 ++++++++++-------- coretk/coretk/widgets.py | 1 - 3 files changed, 143 insertions(+), 125 deletions(-) diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index c49aea50..2aa9c6ad 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -12,7 +12,8 @@ from coretk.widgets import CheckboxList, ListboxScroll class NodeService(Dialog): def __init__(self, master, app, canvas_node, services=None): - super().__init__(master, app, "Node Services", modal=True) + title = f"{canvas_node.core_node.name} Services" + super().__init__(master, app, title, modal=True) self.app = app self.canvas_node = canvas_node self.node_id = canvas_node.core_node.id diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 812d1a6a..03c74ab2 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -9,12 +9,14 @@ from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog from coretk.errors import show_grpc_error from coretk.images import ImageEnum, Images +from coretk.themes import FRAME_PAD, PADX, PADY from coretk.widgets import CodeText, ListboxScroll class ServiceConfiguration(Dialog): def __init__(self, master, app, service_name, node_id): - super().__init__(master, app, f"{service_name} service", modal=True) + title = f"{service_name} Service" + super().__init__(master, app, title, modal=True) self.app = app self.core = app.core self.node_id = node_id @@ -34,7 +36,7 @@ class ServiceConfiguration(Dialog): self.documentnew_img = Images.get(ImageEnum.DOCUMENTNEW, 16) self.editdelete_img = Images.get(ImageEnum.EDITDELETE, 16) - self.tab_parent = None + self.notebook = None self.metadata_entry = None self.filename_combobox = None self.startup_commands_listbox = None @@ -94,88 +96,88 @@ class ServiceConfiguration(Dialog): show_grpc_error(e) def draw(self): - # self.columnconfigure(1, weight=1) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(1, weight=1) + + # draw metadata frame = ttk.Frame(self.top) - frame1 = ttk.Frame(frame) - label = ttk.Label(frame1, text=self.service_name) - label.grid(row=0, column=0, sticky="ew") - frame1.grid(row=0, column=0) - frame2 = ttk.Frame(frame) - label = ttk.Label(frame2, text="Meta-data") - label.grid(row=0, column=0) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="Meta-data") + label.grid(row=0, column=0, sticky="w", padx=PADX) + self.metadata_entry = ttk.Entry(frame, textvariable=self.metadata) + self.metadata_entry.grid(row=0, column=1, sticky="ew") - self.metadata_entry = ttk.Entry(frame2, textvariable=self.metadata) - self.metadata_entry.grid(row=0, column=1) - frame2.grid(row=1, column=0) - frame.grid(row=0, column=0) + # draw notebook + self.notebook = ttk.Notebook(self.top) + self.notebook.grid(sticky="nsew") + self.draw_tab_files() + self.draw_tab_directories() + self.draw_tab_startstop() + self.draw_tab_configuration() - frame = ttk.Frame(self.top) - self.tab_parent = ttk.Notebook(frame) - tab1 = ttk.Frame(self.tab_parent) - tab2 = ttk.Frame(self.tab_parent) - tab3 = ttk.Frame(self.tab_parent) - tab4 = ttk.Frame(self.tab_parent) - tab1.columnconfigure(0, weight=1) - tab2.columnconfigure(0, weight=1) - tab3.columnconfigure(0, weight=1) - tab4.columnconfigure(0, weight=1) + button = ttk.Button(self.top, text="Only Save Changes") + button.grid(sticky="ew", pady=PADY) + self.draw_buttons() - self.tab_parent.add(tab1, text="Files", sticky="nsew") - self.tab_parent.add(tab2, text="Directories", sticky="nsew") - self.tab_parent.add(tab3, text="Startup/shutdown", sticky="nsew") - self.tab_parent.add(tab4, text="Configuration", sticky="nsew") - self.tab_parent.grid(row=0, column=0, sticky="nsew") - frame.grid(row=1, column=0, sticky="nsew") + def draw_tab_files(self): + tab = ttk.Frame(self.notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew") + tab.columnconfigure(0, weight=1) + self.notebook.add(tab, text="Files") - # tab 1 label = ttk.Label( - tab1, text="Config files and scripts that are generated for this service." + tab, text="Config files and scripts that are generated for this service." ) - label.grid(row=0, column=0, sticky="nsew") + label.grid() - frame = ttk.Frame(tab1) - label = ttk.Label(frame, text="File name: ") - label.grid(row=0, column=0) + frame = ttk.Frame(tab) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="File Name") + label.grid(row=0, column=0, padx=PADX, sticky="w") self.filename_combobox = ttk.Combobox( frame, values=self.filenames, state="readonly" ) - self.filename_combobox.grid(row=0, column=1) 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.grid(row=0, column=2) + button.grid(row=0, column=2, padx=PADX) button = ttk.Button(frame, image=self.editdelete_img, state="disabled") button.bind("", self.delete_filename) button.grid(row=0, column=3) - frame.grid(row=1, column=0, sticky="nsew") - frame = ttk.Frame(tab1) + frame = ttk.Frame(tab) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) button = ttk.Radiobutton( frame, variable=self.radiovar, - text="Copy this source file:", + text="Copy Source File", value=1, - state="disabled", + state=tk.DISABLED, ) - button.grid(row=0, column=0) + button.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry(frame, state=tk.DISABLED) - entry.grid(row=0, column=1) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) image = Images.get(ImageEnum.FILEOPEN, 16) button = ttk.Button(frame, image=image) button.image = image button.grid(row=0, column=2) - frame.grid(row=2, column=0, sticky="nsew") - frame = ttk.Frame(tab1) + frame = ttk.Frame(tab) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(0, weight=1) button = ttk.Radiobutton( frame, variable=self.radiovar, - text="Use text below for file contents:", + text="Use text below for file contents", value=2, ) - button.grid(row=0, column=0) + button.grid(row=0, column=0, sticky="ew") image = Images.get(ImageEnum.FILEOPEN, 16) button = ttk.Button(frame, image=image) button.image = image @@ -184,10 +186,10 @@ class ServiceConfiguration(Dialog): button = ttk.Button(frame, image=image) button.image = image button.grid(row=0, column=2) - frame.grid(row=3, column=0, sticky="nsew") - self.service_file_data = CodeText(tab1) - self.service_file_data.grid(row=4, column=0, sticky="nsew") + self.service_file_data = CodeText(tab) + self.service_file_data.grid(sticky="nsew") + tab.rowconfigure(self.service_file_data.grid_info()["row"], weight=1) if len(self.filenames) > 0: self.filename_combobox.current(0) self.service_file_data.delete(1.0, "end") @@ -196,38 +198,60 @@ class ServiceConfiguration(Dialog): ) self.service_file_data.bind("", self.update_temp_service_file_data) - # tab 2 + def draw_tab_directories(self): + tab = ttk.Frame(self.notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew") + tab.columnconfigure(0, weight=1) + self.notebook.add(tab, text="Directories") + label = ttk.Label( - tab2, + tab, text="Directories required by this service that are unique for each node.", ) - label.grid(row=0, column=0, sticky="nsew") + label.grid() + + def draw_tab_startstop(self): + tab = ttk.Frame(self.notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew") + tab.columnconfigure(0, weight=1) + for i in range(3): + tab.rowconfigure(i, weight=1) + self.notebook.add(tab, text="Startup/shutdown") # tab 3 for i in range(3): label_frame = None if i == 0: - label_frame = ttk.LabelFrame(tab3, text="Startup commands") + label_frame = ttk.LabelFrame( + tab, text="Startup commands", padding=FRAME_PAD + ) commands = self.startup_commands elif i == 1: - label_frame = ttk.LabelFrame(tab3, text="Shutdown commands") + label_frame = ttk.LabelFrame( + tab, text="Shutdown commands", padding=FRAME_PAD + ) commands = self.shutdown_commands elif i == 2: - label_frame = ttk.LabelFrame(tab3, text="Validation commands") + label_frame = ttk.LabelFrame( + tab, text="Validation commands", padding=FRAME_PAD + ) commands = self.validation_commands label_frame.columnconfigure(0, weight=1) + label_frame.rowconfigure(1, weight=1) + label_frame.grid(row=i, column=0, sticky="nsew") + frame = ttk.Frame(label_frame) + frame.grid(row=0, column=0, sticky="nsew") frame.columnconfigure(0, weight=1) entry = ttk.Entry(frame, textvariable=tk.StringVar()) - entry.grid(row=0, column=0, stick="nsew") + entry.grid(row=0, column=0, stick="ew", padx=PADX) button = ttk.Button(frame, image=self.documentnew_img) button.bind("", self.add_command) - button.grid(row=0, column=1, sticky="nsew") + button.grid(row=0, column=1, sticky="ew", padx=PADX) button = ttk.Button(frame, image=self.editdelete_img) - button.grid(row=0, column=2, sticky="nsew") + button.grid(row=0, column=2, sticky="ew") button.bind("", self.delete_command) - frame.grid(row=0, column=0, sticky="nsew") - listbox_scroll = ListboxScroll(label_frame) + listbox_scroll = ListboxScroll(label_frame, borderwidth=0) listbox_scroll.listbox.bind("<>", self.update_entry) for command in commands: listbox_scroll.listbox.insert("end", command) @@ -239,81 +263,75 @@ class ServiceConfiguration(Dialog): self.shutdown_commands_listbox = listbox_scroll.listbox elif i == 2: self.validate_commands_listbox = listbox_scroll.listbox - label_frame.grid(row=i, column=0, sticky="nsew") - # tab 4 - for i in range(2): - label_frame = None - if i == 0: - label_frame = ttk.LabelFrame(tab4, text="Executables") - elif i == 1: - label_frame = ttk.LabelFrame(tab4, text="Dependencies") - label_frame.columnconfigure(0, weight=1) - listbox_scroll = ListboxScroll(label_frame) - listbox_scroll.listbox.config(height=4) - listbox_scroll.grid(row=0, column=0, sticky="nsew") - label_frame.grid(row=i, column=0, sticky="nsew") - if i == 0: - for executable in self.executables: - print(executable) - listbox_scroll.listbox.insert("end", executable) - if i == 1: - for dependency in self.dependencies: - listbox_scroll.listbox.insert("end", dependency) + def draw_tab_configuration(self): + tab = ttk.Frame(self.notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew") + tab.columnconfigure(0, weight=1) + self.notebook.add(tab, text="Configuration", sticky="nsew") - for i in range(3): - frame = ttk.Frame(tab4) - frame.columnconfigure(0, weight=1) - if i == 0: - label = ttk.Label(frame, text="Validation time:") - self.validation_time_entry = ttk.Entry(frame) - self.validation_time_entry.insert("end", self.validation_time) - self.validation_time_entry.config(state="disabled") - self.validation_time_entry.grid(row=i, column=1) - elif i == 1: - label = ttk.Label(frame, text="Validation mode:") - if self.validation_mode == core_pb2.ServiceValidationMode.BLOCKING: - mode = "BLOCKING" - elif ( - self.validation_mode == core_pb2.ServiceValidationMode.NON_BLOCKING - ): - mode = "NON_BLOCKING" - elif self.validation_mode == core_pb2.ServiceValidationMode.TIMER: - mode = "TIMER" - self.validation_mode_entry = ttk.Entry( - frame, textvariable=tk.StringVar(value=mode) - ) - self.validation_mode_entry.insert("end", mode) - self.validation_mode_entry.config(state="disabled") - self.validation_mode_entry.grid(row=i, column=1) - elif i == 2: - label = ttk.Label(frame, text="Validation period:") - self.validation_period_entry = ttk.Entry( - frame, state="disabled", textvariable=tk.StringVar() - ) - self.validation_period_entry.grid(row=i, column=1) - label.grid(row=i, column=0) - frame.grid(row=2 + i, column=0, sticky="nsew") + frame = ttk.Frame(tab) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) - button = ttk.Button( - self.top, text="onle store values that have changed from their defaults" + label = ttk.Label(frame, text="Validation Time") + label.grid(row=0, column=0, sticky="w") + self.validation_time_entry = ttk.Entry(frame) + self.validation_time_entry.insert("end", self.validation_time) + self.validation_time_entry.config(state=tk.DISABLED) + self.validation_time_entry.grid(row=0, column=1, sticky="ew") + + label = ttk.Label(frame, text="Validation Mode") + label.grid(row=1, column=0, sticky="w") + if self.validation_mode == core_pb2.ServiceValidationMode.BLOCKING: + mode = "BLOCKING" + elif self.validation_mode == core_pb2.ServiceValidationMode.NON_BLOCKING: + mode = "NON_BLOCKING" + else: + mode = "TIMER" + self.validation_mode_entry = ttk.Entry( + frame, textvariable=tk.StringVar(value=mode) ) - button.grid(row=2, column=0) + self.validation_mode_entry.insert("end", mode) + self.validation_mode_entry.config(state=tk.DISABLED) + self.validation_mode_entry.grid(row=1, column=1, sticky="ew") + label = ttk.Label(frame, text="Validation Period") + label.grid(row=2, column=0, sticky="w") + self.validation_period_entry = ttk.Entry( + frame, state=tk.DISABLED, textvariable=tk.StringVar() + ) + self.validation_period_entry.grid(row=2, column=1, sticky="ew") + + listbox_scroll = ListboxScroll(tab, text="Executables", padding=FRAME_PAD) + listbox_scroll.grid(sticky="nsew", pady=PADY) + tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1) + for executable in self.executables: + listbox_scroll.listbox.insert("end", executable) + + listbox_scroll = ListboxScroll(tab, text="Dependencies", padding=FRAME_PAD) + listbox_scroll.grid(sticky="nsew") + tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1) + for dependency in self.dependencies: + listbox_scroll.listbox.insert("end", dependency) + + def draw_buttons(self): frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(4): + frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="nsew") + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button( frame, text="Dafults", command=self.click_defaults, state="disabled" ) - button.grid(row=0, column=1, sticky="nsew") + button.grid(row=0, column=1, sticky="ew", padx=PADX) button = ttk.Button( frame, text="Copy...", command=self.click_copy, state="disabled" ) - button.grid(row=0, column=2, sticky="nsew") + button.grid(row=0, column=2, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=3, sticky="nsew") - frame.grid(row=3, column=0) + button.grid(row=0, column=3, sticky="ew") def add_filename(self, event): # not worry about it for now diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index cdbfea28..0ba16587 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -183,7 +183,6 @@ class ListboxScroll(ttk.LabelFrame): self.listbox = tk.Listbox( self, selectmode=tk.SINGLE, yscrollcommand=self.scrollbar.set ) - logging.info("listbox background: %s", self.listbox.cget("background")) self.listbox.grid(row=0, column=0, sticky="nsew") self.scrollbar.config(command=self.listbox.yview) From 71e6df76ce6a5c47d7f44cd3c158472a8484c4f3 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 12 Dec 2019 10:49:52 -0800 Subject: [PATCH 365/462] create layout for check emulation light dialog --- coretk/coretk/data/icons/alert.png | Bin 0 -> 2019 bytes coretk/coretk/dialogs/cel.py | 78 +++++++++++++++++++++++------ coretk/coretk/images.py | 1 + coretk/coretk/statusbar.py | 7 ++- 4 files changed, 69 insertions(+), 17 deletions(-) create mode 100644 coretk/coretk/data/icons/alert.png diff --git a/coretk/coretk/data/icons/alert.png b/coretk/coretk/data/icons/alert.png new file mode 100644 index 0000000000000000000000000000000000000000..718fa9f1f2794dc3165f2cf71db560247721c52b GIT binary patch literal 2019 zcmV<92ORi`P)!Qp@lUfU%Db%W|ox?Bz|gZ z*$7A@n3z;FF-GY6FJY@OxU>;W{P&Sqq-~ZzOZ;Hshq}xxsKHjFsiN6x)MAUWrGc_2 zyVK)`ohj?ieQ)l&?@V`gdw_kR z|GUf&^2iDv0b1gIgwMZqfsyke2s+3p%KeKj?a)EK7AjAIj3~1WcsxPkm{j;us6m~K z!}(`#A>1kzp3EvUa{zP`S5o9K(3>K4)deBN`3+7XY?KOr%_29`6r=-m)5U($oDZvR zI`VKnz{MyB^ja>+B0JkV;6lEM@*%Ib3qcsoB)D>Er8Jh^_aTw^% zqF^OYM;V>pU>+EdD!=n->@@-ugc{!^tMeB+ugK{r(D?ve!1vVi;r5$?O56q96kyB( zD=Y{(oZp~CnNRpMY#Al}C~J6~qbQf>FsPxe#8wtkFZ}1n2^vfjkNe$9Rz;W)t+WF9N zKT+G{e1NN{aa&r0^Z*E-F*WD^c~=`{XG2>oEc3A=?Pg*CbdamTbJ3L*yi z;WEAeOteY*Cf&sPq?^ejK-o^VK|Tb|>$=5mdY3cmrq z(+1J&xsms|Zny&=e8k)Y;YHrYH_YW)Q)Cx#++1#fo86aPsJn44ls)n@UN@I(j7)MG zWskXFWyqt*0#L-jMP<;uJP&}+GXKDMH41~vj$)NdUF4kJu3!$hm)e@i{L z(8C-24d}}wX9*{%@(zJBi!U~hfD`m%>wJKHxt(8T8@>DyWj}B`&=2&~|N4R3Q4Y|{ zA9Rq9<GC%aCsMQ)4+mz zBLjjfECkL-m7Ty={HDigV2e~4 zS(<&ysQAv{zDX3DeVde${5Nokq3RiCdAA&5HjfY;pqsU9LD-BkfYeix%CY)S%?W0C zI(W`+=sAZe$*!fXWSp9oses{T81&J@r^#9JQFrWk{jYuehaK2DACyVgh)k-AIB2ST zf&26pdfFh@6dht6nkxz9M)HCRA(xbqvB1wr_-Pno>eK7D;*b`ac$3_Jv^2au-Q++$wW^gBC$hluu>-Vj=|AhRPH_0ms)xR;lwF zqy*usGR0Hp+#oIU(AiJ;mfG^UmySeBj4b+e@$zjdDipNL{`1NC4eCMIpYtT9fgv5V zuYayNpa1trBoc{4B9TZW5{X12kw_#Gi9{m){15#$Llp7S@m>G`002ovPDHLkV1gy+ B*;oJo literal 0 HcmV?d00001 diff --git a/coretk/coretk/dialogs/cel.py b/coretk/coretk/dialogs/cel.py index a5fbea63..77b96367 100644 --- a/coretk/coretk/dialogs/cel.py +++ b/coretk/coretk/dialogs/cel.py @@ -5,34 +5,70 @@ import tkinter as tk from tkinter import ttk from coretk.dialogs.dialog import Dialog +from coretk.themes import PADX, PADY +from coretk.widgets import CodeText class CheckLight(Dialog): def __init__(self, master, app): super().__init__(master, app, "CEL", modal=True) self.app = app - - self.columnconfigure(0, weight=1) + self.tree = None self.draw() def draw(self): row = 0 frame = ttk.Frame(self) + frame.columnconfigure(0, weight=1) label = ttk.Label(frame, text="Check Emulation Light") label.grid(row=0, column=0) - frame.grid(row=row, column=0) + frame.grid(row=row, column=0, padx=PADX, pady=PADY, sticky="nsew") row = row + 1 + frame = ttk.Frame(self) - button = ttk.Button(frame, text="Reset CEL") - button.grid(row=0, column=0) - button = ttk.Button(frame, text="View core-daemon log", command=self.daemon_log) - button.grid(row=0, column=1) - button = ttk.Button(frame, text="View node log") - button.grid(row=0, column=2) - button = ttk.Button(frame, text="Close", command=self.destroy) - button.grid(row=0, column=3) + frame.columnconfigure(0, weight=1) frame.grid(row=row, column=0, sticky="nsew") - ++row + self.tree = ttk.Treeview( + frame, columns=("time", "level", "node", "source"), show="headings" + ) + self.tree.grid(row=0, column=0, sticky="nsew") + self.tree.column("time", stretch=tk.YES) + self.tree.heading("time", text="time") + self.tree.column("level", stretch=tk.YES) + self.tree.heading("level", text="level") + self.tree.column("node", stretch=tk.YES) + self.tree.heading("node", text="node") + self.tree.column("source", stretch=tk.YES) + self.tree.heading("source", text="source") + + yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) + yscrollbar.grid(row=0, column=1, sticky="ns") + self.tree.configure(yscrollcommand=yscrollbar.set) + + xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) + xscrollbar.grid(row=1, sticky="ew") + self.tree.configure(xscrollcommand=xscrollbar.set) + row = row + 1 + + text = CodeText(self) + text.grid(row=row, column=0, sticky="nsew") + row = row + 1 + + frame = ttk.Frame(self) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + frame.columnconfigure(3, weight=1) + button = ttk.Button(frame, text="Reset CEL") + button.grid(row=0, column=0, sticky="nsew", padx=PADX) + button = ttk.Button(frame, text="View core-daemon log", command=self.daemon_log) + button.grid(row=0, column=1, sticky="nsew", padx=PADX) + button = ttk.Button(frame, text="View node log") + button.grid(row=0, column=2, sticky="nsew", padx=PADX) + button = ttk.Button(frame, text="Close", command=self.destroy) + button.grid(row=0, column=3, sticky="nsew", padx=PADX) + frame.grid(row=row, column=0, sticky="nsew") + row = row + 1 def daemon_log(self): dialog = DaemonLog(self, self.app) @@ -48,8 +84,20 @@ class DaemonLog(Dialog): def draw(self): frame = ttk.Frame(self) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=9) label = ttk.Label(frame, text="File: ") label.grid(row=0, column=0) - entry = ttk.Entry(frame, textvariable=self.path, state="readonly") - entry.grid(row=0, column=1) - frame.grid(row=0, column=0) + entry = ttk.Entry(frame, textvariable=self.path, state="disabled") + entry.grid(row=0, column=1, sticky="nsew") + frame.grid(row=0, column=0, sticky="nsew") + try: + file = open("/var/log/core-daemon.log", "r") + log = file.readlines() + except FileNotFoundError: + log = "Log file not found" + text = CodeText(self) + text.insert("1.0", log) + text.see("end") + text.config(state=tk.DISABLED) + text.grid(row=1, column=0, sticky="nsew") diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index db7928b9..9d282dbb 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -67,3 +67,4 @@ class ImageEnum(Enum): ANTENNA = "antenna" DOCKER = "docker" LXC = "lxc" + ALERT = "alert" diff --git a/coretk/coretk/statusbar.py b/coretk/coretk/statusbar.py index f45dce9f..797d61a8 100644 --- a/coretk/coretk/statusbar.py +++ b/coretk/coretk/statusbar.py @@ -3,6 +3,7 @@ import tkinter as tk from tkinter import ttk from coretk.dialogs.cel import CheckLight +from coretk.images import ImageEnum, Images class StatusBar(ttk.Frame): @@ -54,9 +55,11 @@ class StatusBar(ttk.Frame): ) self.cpu_usage.grid(row=0, column=3, sticky="ew") - self.emulation_light = ttk.Label( - self, text="CEL TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE + image = Images.get(ImageEnum.ALERT, 18) + self.emulation_light = ttk.Button( + self, image=image, text="Alert", compound="left" ) + self.emulation_light.image = image self.emulation_light.bind("", self.cel_callback) self.emulation_light.grid(row=0, column=4, sticky="ew") From d34a58dff0f50f7194e2f2f26e0140375ce6e07a Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 12 Dec 2019 11:04:55 -0800 Subject: [PATCH 366/462] small edits on cel dialog --- coretk/coretk/dialogs/cel.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/dialogs/cel.py b/coretk/coretk/dialogs/cel.py index 77b96367..7f95fc79 100644 --- a/coretk/coretk/dialogs/cel.py +++ b/coretk/coretk/dialogs/cel.py @@ -5,6 +5,7 @@ import tkinter as tk from tkinter import ttk from coretk.dialogs.dialog import Dialog +from coretk.images import ImageEnum, Images from coretk.themes import PADX, PADY from coretk.widgets import CodeText @@ -14,14 +15,20 @@ class CheckLight(Dialog): super().__init__(master, app, "CEL", modal=True) self.app = app self.tree = None + self.text = None self.draw() def draw(self): row = 0 frame = ttk.Frame(self) frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + image = Images.get(ImageEnum.ALERT, 18) + label = ttk.Label(frame, image=image) + label.image = image + label.grid(row=0, column=0, sticky="e") label = ttk.Label(frame, text="Check Emulation Light") - label.grid(row=0, column=0) + label.grid(row=0, column=1, sticky="w") frame.grid(row=row, column=0, padx=PADX, pady=PADY, sticky="nsew") row = row + 1 @@ -50,8 +57,8 @@ class CheckLight(Dialog): self.tree.configure(xscrollcommand=xscrollbar.set) row = row + 1 - text = CodeText(self) - text.grid(row=row, column=0, sticky="nsew") + self.text = CodeText(self) + self.text.grid(row=row, column=0, sticky="nsew") row = row + 1 frame = ttk.Frame(self) @@ -59,7 +66,7 @@ class CheckLight(Dialog): frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) frame.columnconfigure(3, weight=1) - button = ttk.Button(frame, text="Reset CEL") + button = ttk.Button(frame, text="Reset CEL", command=self.reset_cel) button.grid(row=0, column=0, sticky="nsew", padx=PADX) button = ttk.Button(frame, text="View core-daemon log", command=self.daemon_log) button.grid(row=0, column=1, sticky="nsew", padx=PADX) @@ -70,6 +77,9 @@ class CheckLight(Dialog): frame.grid(row=row, column=0, sticky="nsew") row = row + 1 + def reset_cel(self): + self.text.delete("1.0", tk.END) + def daemon_log(self): dialog = DaemonLog(self, self.app) dialog.show() From 4e9de862a30e169a04f1c002556918577391fc80 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 12 Dec 2019 11:06:52 -0800 Subject: [PATCH 367/462] updated nodes to store actual positions, instead of scaled ones, updated shapes to return actual positions for metadata, added some logic for resetting the canvas and drawn nodes when resizing, convert node update coords into scaled coords for moving nodes --- coretk/coretk/appconfig.py | 25 +- coretk/coretk/coreclient.py | 12 +- coretk/coretk/data/mobility/sample1.scen | 28 + coretk/coretk/data/xmls/sample1.xml | 786 ++++++++++---------- coretk/coretk/dialogs/canvassizeandscale.py | 8 +- coretk/coretk/graph/graph.py | 88 ++- coretk/coretk/graph/node.py | 17 +- coretk/coretk/graph/shape.py | 10 + coretk/coretk/menuaction.py | 6 +- coretk/coretk/menubar.py | 15 +- 10 files changed, 535 insertions(+), 460 deletions(-) create mode 100644 coretk/coretk/data/mobility/sample1.scen diff --git a/coretk/coretk/appconfig.py b/coretk/coretk/appconfig.py index b2606994..33b08793 100644 --- a/coretk/coretk/appconfig.py +++ b/coretk/coretk/appconfig.py @@ -14,7 +14,7 @@ CUSTOM_EMANE_PATH = HOME_PATH.joinpath("custom_emane") CUSTOM_SERVICE_PATH = HOME_PATH.joinpath("custom_services") ICONS_PATH = HOME_PATH.joinpath("icons") MOBILITY_PATH = HOME_PATH.joinpath("mobility") -XML_PATH = HOME_PATH.joinpath("xml") +XMLS_PATH = HOME_PATH.joinpath("xmls") CONFIG_PATH = HOME_PATH.joinpath("gui.yaml") # local paths @@ -22,6 +22,7 @@ DATA_PATH = Path(__file__).parent.joinpath("data") LOCAL_ICONS_PATH = DATA_PATH.joinpath("icons").absolute() LOCAL_BACKGROUND_PATH = DATA_PATH.joinpath("backgrounds").absolute() LOCAL_XMLS_PATH = DATA_PATH.joinpath("xmls").absolute() +LOCAL_MOBILITY_PATH = DATA_PATH.joinpath("mobility").absolute() # configuration data TERMINALS = [ @@ -43,6 +44,12 @@ class IndentDumper(yaml.Dumper): return super().increase_indent(flow, False) +def copy_files(current_path, new_path): + for current_file in current_path.glob("*"): + new_file = new_path.joinpath(current_file.name) + shutil.copy(current_file, new_file) + + def check_directory(): if HOME_PATH.exists(): logging.info("~/.coretk exists") @@ -54,16 +61,12 @@ def check_directory(): CUSTOM_SERVICE_PATH.mkdir() ICONS_PATH.mkdir() MOBILITY_PATH.mkdir() - XML_PATH.mkdir() - for image in LOCAL_ICONS_PATH.glob("*"): - new_image = ICONS_PATH.joinpath(image.name) - shutil.copy(image, new_image) - for background in LOCAL_BACKGROUND_PATH.glob("*"): - new_background = BACKGROUNDS_PATH.joinpath(background.name) - shutil.copy(background, new_background) - for xml_file in LOCAL_XMLS_PATH.glob("*"): - new_xml = XML_PATH.joinpath(xml_file.name) - shutil.copy(xml_file, new_xml) + XMLS_PATH.mkdir() + + copy_files(LOCAL_ICONS_PATH, ICONS_PATH) + copy_files(LOCAL_BACKGROUND_PATH, BACKGROUNDS_PATH) + copy_files(LOCAL_XMLS_PATH, XMLS_PATH) + copy_files(LOCAL_MOBILITY_PATH, MOBILITY_PATH) if "TERM" in os.environ: terminal = TERMINALS[0] diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 36562cce..57224a93 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -300,14 +300,16 @@ class CoreClient: width = self.app.guiconfig["preferences"]["width"] height = self.app.guiconfig["preferences"]["height"] - width, height = canvas_config.get("dimensions", [width, height]) - self.app.canvas.redraw_canvas(width, height) + dimensions = canvas_config.get("dimensions", [width, height]) + self.app.canvas.redraw_canvas(dimensions) wallpaper = canvas_config.get("wallpaper") if wallpaper: wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) - self.app.canvas.set_wallpaper(wallpaper) - self.app.canvas.update_grid() + self.app.canvas.set_wallpaper(wallpaper) + else: + self.app.canvas.redraw_canvas() + self.app.canvas.set_wallpaper(None) # load saved shapes shapes_config = config.get("shapes") @@ -482,7 +484,7 @@ class CoreClient: "wallpaper-style": self.app.canvas.scale_option.get(), "gridlines": self.app.canvas.show_grid.get(), "fit_image": self.app.canvas.adjust_to_dim.get(), - "dimensions": self.app.canvas.width_and_height(), + "dimensions": self.app.canvas.current_dimensions, } canvas_config = json.dumps(canvas_config) diff --git a/coretk/coretk/data/mobility/sample1.scen b/coretk/coretk/data/mobility/sample1.scen new file mode 100644 index 00000000..c2fc5a44 --- /dev/null +++ b/coretk/coretk/data/mobility/sample1.scen @@ -0,0 +1,28 @@ +# +# nodes: 4, max time: 27.000000, max x: 600.00, max y: 600.00 +# nominal range: 300.00 link bw: 54000000.00 +# pause: 30.00, min speed 1.50 max speed: 4.50 + +$node_(6) set X_ 780.0 +$node_(6) set Y_ 228.0 +$node_(6) set Z_ 0.00 +$node_(7) set X_ 816.0 +$node_(7) set Y_ 348.0 +$node_(7) set Z_ 0.00 +$node_(8) set X_ 672.0 +$node_(8) set Y_ 420.0 +$node_(8) set Z_ 0.00 +$node_(9) set X_ 672.0 +$node_(9) set Y_ 96.0 +$node_(9) set Z_ 0.00 +$ns_ at 1.00 "$node_(6) setdest 500.0 178.0 25.0" +$ns_ at 2.00 "$node_(7) setdest 400.0 288.0 15.0" +$ns_ at 1.00 "$node_(8) setdest 590.0 520.0 17.0" +$ns_ at 3.00 "$node_(9) setdest 720.0 300.0 20.0" +$ns_ at 8.00 "$node_(7) setdest 600.0 350.0 10.0" +$ns_ at 9.00 "$node_(8) setdest 730.0 300.0 15.0" +$ns_ at 10.00 "$node_(6) setdest 600.0 108.0 10.0" +$ns_ at 16.00 "$node_(9) setdest 672.0 96.0 20.0" +$ns_ at 17.00 "$node_(7) setdest 816.0 348.0 20.0" +$ns_ at 18.00 "$node_(6) setdest 780.0 228.0 25.0" +$ns_ at 22.00 "$node_(8) setdest 672.0 420.0 20.0" diff --git a/coretk/coretk/data/xmls/sample1.xml b/coretk/coretk/data/xmls/sample1.xml index 8c61b7de..afec8874 100644 --- a/coretk/coretk/data/xmls/sample1.xml +++ b/coretk/coretk/data/xmls/sample1.xml @@ -1,5 +1,5 @@ - + @@ -18,15 +18,6 @@ - - - - - - - - - @@ -36,6 +27,23 @@ + + + + + + + + + + + + + + + + + @@ -44,6 +52,14 @@ + + + + + + + + @@ -61,28 +77,12 @@ - - - - - - - - - - - - - - - - @@ -117,62 +117,62 @@ - - + + - - + + - - + + - - - - - - + + + + + + - - - - - - - - - + + + + + + + + + @@ -188,7 +188,7 @@ - + @@ -367,6 +367,177 @@ bootquagga /sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 /sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 /sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.1.1/24 + ipv6 address a:1::1/64 +! +interface eth1 + ip address 10.0.2.1/24 + ipv6 address a:2::1/64 +! +router ospf + router-id 10.0.1.1 + network 10.0.1.0/24 area 0 + network 10.0.2.0/24 area 0 +! +router ospf6 + router-id 10.0.1.1 + interface eth0 area 0.0.0.0 + interface eth1 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospfd + + + killall ospfd + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 @@ -550,7 +721,7 @@ bootquagga - + /usr/local/etc/quagga /var/run/quagga @@ -566,22 +737,20 @@ bootquagga interface eth0 - ip address 10.0.1.1/24 - ipv6 address a:1::1/64 -! -interface eth1 - ip address 10.0.2.1/24 - ipv6 address a:2::1/64 -! -router ospf - router-id 10.0.1.1 - network 10.0.1.0/24 area 0 - network 10.0.2.0/24 area 0 + ip address 10.0.0.9/32 + ipv6 address a::9/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa ! router ospf6 - router-id 10.0.1.1 + router-id 10.0.0.9 interface eth0 area 0.0.0.0 - interface eth1 area 0.0.0.0 ! #!/bin/sh @@ -681,15 +850,7 @@ bootquagga - - - pidof ospfd - - - killall ospfd - - - + pidof ospf6d @@ -697,7 +858,7 @@ bootquagga killall ospf6d - + sh ipforward.sh @@ -715,9 +876,6 @@ bootquagga /sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 /sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 /sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 -/sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 @@ -876,6 +1034,164 @@ bootquagga /sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 /sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 /sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.0.7/32 + ipv6 address a::7/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa +! +router ospf6 + router-id 10.0.0.7 + interface eth0 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 @@ -1221,164 +1537,6 @@ bootquagga /sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 /sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 /sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 - - - - - - /usr/local/etc/quagga - /var/run/quagga - - - sh quaggaboot.sh zebra - - - pidof zebra - - - killall zebra - - - interface eth0 - ip address 10.0.0.7/32 - ipv6 address a::7/128 - ipv6 ospf6 instance-id 65 - ipv6 ospf6 hello-interval 2 - ipv6 ospf6 dead-interval 6 - ipv6 ospf6 retransmit-interval 5 - ipv6 ospf6 network manet-designated-router - ipv6 ospf6 diffhellos - ipv6 ospf6 adjacencyconnectivity uniconnected - ipv6 ospf6 lsafullness mincostlsa -! -router ospf6 - router-id 10.0.0.7 - interface eth0 area 0.0.0.0 -! - - #!/bin/sh -# auto-generated by zebra service (quagga.py) -QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf -QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" -QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" -QUAGGA_STATE_DIR=/var/run/quagga - -searchforprog() -{ - prog=$1 - searchpath=$@ - ret= - for p in $searchpath; do - if [ -x $p/$prog ]; then - ret=$p - break - fi - done - echo $ret -} - -confcheck() -{ - CONF_DIR=`dirname $QUAGGA_CONF` - # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then - ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf - fi - # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then - ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf - fi -} - -bootdaemon() -{ - QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) - if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then - echo "ERROR: Quagga's '$1' daemon not found in search path:" - echo " $QUAGGA_SBIN_SEARCH" - return 1 - fi - - flags="" - - if [ "$1" = "xpimd" ] && \ - grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then - flags="$flags -6" - fi - - $QUAGGA_SBIN_DIR/$1 $flags -d - if [ "$?" != "0" ]; then - echo "ERROR: Quagga's '$1' daemon failed to start!:" - return 1 - fi -} - -bootquagga() -{ - QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) - if [ "z$QUAGGA_BIN_DIR" = "z" ]; then - echo "ERROR: Quagga's 'vtysh' program not found in search path:" - echo " $QUAGGA_BIN_SEARCH" - return 1 - fi - - # fix /var/run/quagga permissions - id -u quagga 2>/dev/null >/dev/null - if [ "$?" = "0" ]; then - chown quagga $QUAGGA_STATE_DIR - fi - - bootdaemon "zebra" - for r in rip ripng ospf6 ospf bgp babel; do - if grep -q "^router \<${r}\>" $QUAGGA_CONF; then - bootdaemon "${r}d" - fi - done - - if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then - bootdaemon "xpimd" - fi - - $QUAGGA_BIN_DIR/vtysh -b -} - -if [ "$1" != "zebra" ]; then - echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" - exit 1 -fi -confcheck -bootquagga - - service integrated-vtysh-config - - - - - - pidof ospf6d - - - killall ospf6d - - - - - sh ipforward.sh - - - #!/bin/sh -# auto-generated by IPForward service (utility.py) -/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 -/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 -/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 -/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 -/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 @@ -1391,164 +1549,6 @@ bootquagga # auto-generated by DefaultRoute service (utility.py) ip route add default via 10.0.1.1 ip route add default via a:1::1 - - - - - - /usr/local/etc/quagga - /var/run/quagga - - - sh quaggaboot.sh zebra - - - pidof zebra - - - killall zebra - - - interface eth0 - ip address 10.0.0.9/32 - ipv6 address a::9/128 - ipv6 ospf6 instance-id 65 - ipv6 ospf6 hello-interval 2 - ipv6 ospf6 dead-interval 6 - ipv6 ospf6 retransmit-interval 5 - ipv6 ospf6 network manet-designated-router - ipv6 ospf6 diffhellos - ipv6 ospf6 adjacencyconnectivity uniconnected - ipv6 ospf6 lsafullness mincostlsa -! -router ospf6 - router-id 10.0.0.9 - interface eth0 area 0.0.0.0 -! - - #!/bin/sh -# auto-generated by zebra service (quagga.py) -QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf -QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" -QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" -QUAGGA_STATE_DIR=/var/run/quagga - -searchforprog() -{ - prog=$1 - searchpath=$@ - ret= - for p in $searchpath; do - if [ -x $p/$prog ]; then - ret=$p - break - fi - done - echo $ret -} - -confcheck() -{ - CONF_DIR=`dirname $QUAGGA_CONF` - # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then - ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf - fi - # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then - ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf - fi -} - -bootdaemon() -{ - QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) - if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then - echo "ERROR: Quagga's '$1' daemon not found in search path:" - echo " $QUAGGA_SBIN_SEARCH" - return 1 - fi - - flags="" - - if [ "$1" = "xpimd" ] && \ - grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then - flags="$flags -6" - fi - - $QUAGGA_SBIN_DIR/$1 $flags -d - if [ "$?" != "0" ]; then - echo "ERROR: Quagga's '$1' daemon failed to start!:" - return 1 - fi -} - -bootquagga() -{ - QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) - if [ "z$QUAGGA_BIN_DIR" = "z" ]; then - echo "ERROR: Quagga's 'vtysh' program not found in search path:" - echo " $QUAGGA_BIN_SEARCH" - return 1 - fi - - # fix /var/run/quagga permissions - id -u quagga 2>/dev/null >/dev/null - if [ "$?" = "0" ]; then - chown quagga $QUAGGA_STATE_DIR - fi - - bootdaemon "zebra" - for r in rip ripng ospf6 ospf bgp babel; do - if grep -q "^router \<${r}\>" $QUAGGA_CONF; then - bootdaemon "${r}d" - fi - done - - if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then - bootdaemon "xpimd" - fi - - $QUAGGA_BIN_DIR/vtysh -b -} - -if [ "$1" != "zebra" ]; then - echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" - exit 1 -fi -confcheck -bootquagga - - service integrated-vtysh-config - - - - - - pidof ospf6d - - - killall ospf6d - - - - - sh ipforward.sh - - - #!/bin/sh -# auto-generated by IPForward service (utility.py) -/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 -/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 -/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 -/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 -/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 @@ -1842,8 +1842,8 @@ bootquagga - - + + diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 5a472104..0a113936 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -21,11 +21,7 @@ class SizeAndScaleDialog(Dialog): self.canvas = self.app.canvas self.validation = app.validation self.section_font = font.Font(weight="bold") - # get current canvas dimensions - plot = self.canvas.find_withtag("rectangle") - x0, y0, x1, y1 = self.canvas.bbox(plot[0]) - width = abs(x0 - x1) - 2 - height = abs(y0 - y1) - 2 + width, height = self.canvas.current_dimensions self.pixel_width = tk.IntVar(value=width) self.pixel_height = tk.IntVar(value=height) location = self.app.core.location @@ -232,7 +228,7 @@ class SizeAndScaleDialog(Dialog): def click_apply(self): width, height = self.pixel_width.get(), self.pixel_height.get() - self.canvas.redraw_canvas(width, height) + self.canvas.redraw_canvas((width, height)) if self.canvas.wallpaper: self.canvas.redraw_wallpaper() location = self.app.core.location diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index e79e0f9c..42d2c4de 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -14,19 +14,13 @@ from coretk.graph.shape import Shape from coretk.graph.shapeutils import ShapeType, is_draw_shape from coretk.nodeutils import NodeUtils -SCROLL_BUFFER = 25 ZOOM_IN = 1.1 ZOOM_OUT = 0.9 class CanvasGraph(tk.Canvas): def __init__(self, master, core, width, height): - super().__init__( - master, - highlightthickness=0, - background="#cccccc", - scrollregion=(0, 0, width + SCROLL_BUFFER, height + SCROLL_BUFFER), - ) + super().__init__(master, highlightthickness=0, background="#cccccc") self.app = master self.core = core self.mode = GraphMode.SELECT @@ -44,8 +38,8 @@ class CanvasGraph(tk.Canvas): self.grid = None self.throughput_draw = Throughput(self, core) self.shape_drawing = False - self.default_width = width - self.default_height = height + self.default_dimensions = (width, height) + self.current_dimensions = self.default_dimensions self.ratio = 1.0 self.offset = (0, 0) self.cursor = (0, 0) @@ -66,17 +60,22 @@ class CanvasGraph(tk.Canvas): self.draw_canvas() self.draw_grid() - def draw_canvas(self): + def draw_canvas(self, dimensions=None): + if self.grid is not None: + self.delete(self.grid) + if not dimensions: + dimensions = self.default_dimensions + self.current_dimensions = dimensions self.grid = self.create_rectangle( 0, 0, - self.default_width, - self.default_height, + *dimensions, outline="#000000", fill="#ffffff", width=1, tags="rectangle", ) + self.configure(scrollregion=self.bbox(tk.ALL)) def reset_and_redraw(self, session): """ @@ -121,6 +120,16 @@ class CanvasGraph(tk.Canvas): self.bind("", lambda e: self.scan_mark(e.x, e.y)) self.bind("", lambda e: self.scan_dragto(e.x, e.y, gain=1)) + def get_actual_coords(self, x, y): + actual_x = (x - self.offset[0]) / self.ratio + actual_y = (y - self.offset[1]) / self.ratio + return actual_x, actual_y + + def get_scaled_coords(self, x, y): + scaled_x = (x * self.ratio) + self.offset[0] + scaled_y = (y * self.ratio) + self.offset[1] + return scaled_x, scaled_y + def draw_grid(self): """ Create grid. @@ -177,7 +186,9 @@ class CanvasGraph(tk.Canvas): # draw nodes on the canvas image = NodeUtils.node_icon(core_node.type, core_node.model) - node = CanvasNode(self.master, core_node, image) + x = core_node.position.x + y = core_node.position.y + node = CanvasNode(self.master, x, y, core_node, image) self.nodes[node.id] = node self.core.canvas_nodes[core_node.id] = node @@ -422,8 +433,8 @@ class CanvasGraph(tk.Canvas): if not factor: factor = ZOOM_IN if event.delta > 0 else ZOOM_OUT event.x, event.y = self.canvasx(event.x), self.canvasy(event.y) - self.scale("all", event.x, event.y, factor, factor) - self.configure(scrollregion=self.bbox("all")) + self.scale(tk.ALL, event.x, event.y, factor, factor) + self.configure(scrollregion=self.bbox(tk.ALL)) self.ratio *= float(factor) self.offset = ( self.offset[0] * factor + event.x * (1 - factor), @@ -444,7 +455,10 @@ class CanvasGraph(tk.Canvas): x, y = self.canvas_xy(event) self.cursor = x, y selected = self.get_selected(event) - logging.debug("click press: %s", selected) + logging.debug("click press(%s): %s", self.cursor, selected) + x_check = self.cursor[0] - self.offset[0] + y_check = self.cursor[1] - self.offset[1] + logging.debug("clock press ofset(%s, %s)", x_check, y_check) is_node = selected in self.nodes if self.mode == GraphMode.EDGE and is_node: x, y = self.coords(selected) @@ -466,6 +480,11 @@ class CanvasGraph(tk.Canvas): node = self.nodes[selected] self.select_object(node.id) self.selected = selected + logging.info( + "selected coords: (%s, %s)", + node.core_node.position.x, + node.core_node.position.y, + ) else: logging.debug("create selection box") if self.mode == GraphMode.SELECT: @@ -557,10 +576,14 @@ class CanvasGraph(tk.Canvas): def add_node(self, x, y): if self.selected is None or self.selected in self.shapes: + actual_x, actual_y = self.get_actual_coords(x, y) core_node = self.core.create_node( - int(x), int(y), self.node_draw.node_type, self.node_draw.model + int(actual_x), + int(actual_y), + self.node_draw.node_type, + self.node_draw.model, ) - node = CanvasNode(self.master, core_node, self.node_draw.image) + 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 return node @@ -652,18 +675,24 @@ class CanvasGraph(tk.Canvas): def resize_to_wallpaper(self): self.delete(self.wallpaper_id) image = ImageTk.PhotoImage(self.wallpaper) - self.redraw_canvas(image.width(), image.height()) + self.redraw_canvas((image.width(), image.height())) self.draw_wallpaper(image) - def redraw_canvas(self, width, height): - """ - redraw grid with new dimension + def redraw_canvas(self, dimensions=None): + logging.info("redrawing canvas to dimensions: %s", dimensions) - :return: nothing - """ - # resize canvas and scrollregion - self.config(scrollregion=(0, 0, width + SCROLL_BUFFER, height + SCROLL_BUFFER)) - self.coords(self.grid, 0, 0, width, height) + # reset scale and move back to original position + logging.info("resetting scaling: %s %s", self.ratio, self.offset) + factor = 1 / self.ratio + self.scale(tk.ALL, self.offset[0], self.offset[1], factor, factor) + self.move(tk.ALL, -self.offset[0], -self.offset[1]) + + # reset ratio and offset + self.ratio = 1.0 + self.offset = (0, 0) + + # redraw canvas rectangle + self.draw_canvas(dimensions) # redraw gridlines to new canvas size self.delete(tags.GRIDLINE) @@ -672,10 +701,11 @@ class CanvasGraph(tk.Canvas): def redraw_wallpaper(self): if self.adjust_to_dim.get(): + logging.info("drawing wallpaper to canvas dimensions") self.resize_to_wallpaper() else: option = ScaleOption(self.scale_option.get()) - logging.info("canvas scale option: %s", option) + logging.info("drawing canvas using scaling option: %s", option) if option == ScaleOption.UPPER_LEFT: self.wallpaper_upper_left() elif option == ScaleOption.CENTERED: @@ -698,7 +728,7 @@ class CanvasGraph(tk.Canvas): def set_wallpaper(self, filename): logging.info("setting wallpaper: %s", filename) - if filename is not None: + if filename: img = Image.open(filename) self.wallpaper = img self.wallpaper_file = filename diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index eadcd0df..a0f406f7 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -20,13 +20,11 @@ NODE_TEXT_OFFSET = 5 class CanvasNode: - def __init__(self, app, core_node, image): + def __init__(self, app, x, y, core_node, image): self.app = app self.canvas = app.canvas self.image = image self.core_node = core_node - x = self.core_node.position.x - y = self.core_node.position.y self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE ) @@ -98,8 +96,10 @@ class CanvasNode: return image_box[3] + NODE_TEXT_OFFSET def move(self, x, y): - x_offset = x - self.core_node.position.x - y_offset = y - self.core_node.position.y + x, y = self.canvas.get_scaled_coords(x, y) + current_x, current_y = self.canvas.coords(self.id) + x_offset = x - current_x + y_offset = y - current_y self.motion(x_offset, y_offset, update=False) def motion(self, x_offset, y_offset, update=True): @@ -107,8 +107,6 @@ class CanvasNode: self.canvas.move(self.text_id, x_offset, y_offset) self.canvas.move_selection(self.id, x_offset, y_offset) x, y = self.canvas.coords(self.id) - self.core_node.position.x = int(x) - self.core_node.position.y = int(y) # move antennae for antenna_id in self.antennae: @@ -131,7 +129,10 @@ class CanvasNode: else: self.canvas.coords(edge.id, x1, y1, x, y) - # update core with new location + # set actual coords for node and update core is running + real_x, real_y = self.canvas.get_actual_coords(x, y) + self.core_node.position.x = int(real_x) + self.core_node.position.y = int(real_y) if self.app.core.is_runtime() and update: self.app.core.edit_node(self.core_node) diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 80621107..99a18a9f 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -147,6 +147,16 @@ class Shape: def metadata(self): coords = self.canvas.coords(self.id) + # update coords to actual positions + if len(coords) == 4: + x1, y1, x2, y2 = coords + x1, y1 = self.canvas.get_actual_coords(x1, y1) + x2, y2 = self.canvas.get_actual_coords(x2, y2) + coords = (x1, y1, x2, y2) + else: + x1, y1 = coords + x1, y1 = self.canvas.get_actual_coords(x1, y1) + coords = (x1, y1) return { "type": self.shape_type.value, "iconcoords": coords, diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 3115cd58..616e9bc1 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -10,7 +10,7 @@ from tkinter import filedialog, messagebox import grpc -from coretk.appconfig import XML_PATH +from coretk.appconfig import XMLS_PATH from coretk.dialogs.about import AboutDialog from coretk.dialogs.canvassizeandscale import SizeAndScaleDialog from coretk.dialogs.canvaswallpaper import CanvasBackgroundDialog @@ -82,7 +82,7 @@ class MenuAction: def file_save_as_xml(self, event=None): logging.info("menuaction.py file_save_as_xml()") file_path = filedialog.asksaveasfilename( - initialdir=str(XML_PATH), + initialdir=str(XMLS_PATH), title="Save As", filetypes=(("EmulationScript XML files", "*.xml"), ("All files", "*")), defaultextension=".xml", @@ -93,7 +93,7 @@ class MenuAction: def file_open_xml(self, event=None): logging.info("menuaction.py file_open_xml()") file_path = filedialog.askopenfilename( - initialdir=str(XML_PATH), + initialdir=str(XMLS_PATH), title="Open", filetypes=(("XML Files", "*.xml"), ("All Files", "*")), ) diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index 2521bd97..460ce2bf 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -46,7 +46,12 @@ class Menubar(tk.Menu): :return: nothing """ menu = tk.Menu(self) - menu.add_command(label="New Session", accelerator="Ctrl+N", state=tk.DISABLED) + menu.add_command( + label="New Session", + accelerator="Ctrl+N", + command=self.app.core.create_new_session, + ) + self.app.bind_all("", lambda e: self.app.core.create_new_session()) menu.add_command( label="Open...", command=self.menuaction.file_open_xml, accelerator="Ctrl+O" ) @@ -104,10 +109,6 @@ class Menubar(tk.Menu): :return: nothing """ menu = tk.Menu(self) - menu.add_command(label="New", state=tk.DISABLED) - menu.add_command(label="Manage...", state=tk.DISABLED) - menu.add_command(label="Delete", state=tk.DISABLED) - menu.add_separator() menu.add_command( label="Size/scale...", command=self.menuaction.canvas_size_and_scale ) @@ -115,6 +116,10 @@ class Menubar(tk.Menu): label="Wallpaper...", command=self.menuaction.canvas_set_wallpaper ) menu.add_separator() + menu.add_command(label="New", state=tk.DISABLED) + menu.add_command(label="Manage...", state=tk.DISABLED) + menu.add_command(label="Delete", state=tk.DISABLED) + menu.add_separator() menu.add_command(label="Previous", accelerator="PgUp", state=tk.DISABLED) menu.add_command(label="Next", accelerator="PgDown", state=tk.DISABLED) menu.add_command(label="First", accelerator="Home", state=tk.DISABLED) From 136121ca18da21d4016a02900405036aeaf5159f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 12 Dec 2019 12:34:50 -0800 Subject: [PATCH 368/462] fixed issue saving wallpaper metadata when None --- coretk/coretk/coreclient.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 57224a93..1cf99526 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -479,8 +479,11 @@ class CoreClient: def set_metadata(self): # create canvas data + wallpaper = None + if self.app.canvas.wallpaper_file: + wallpaper = Path(self.app.canvas.wallpaper_file).name canvas_config = { - "wallpaper": Path(self.app.canvas.wallpaper_file).name, + "wallpaper": wallpaper, "wallpaper-style": self.app.canvas.scale_option.get(), "gridlines": self.app.canvas.show_grid.get(), "fit_image": self.app.canvas.adjust_to_dim.get(), From f154733e2ec0d9fb7a68e0b2d9617733ce8c732c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 12 Dec 2019 12:49:29 -0800 Subject: [PATCH 369/462] fixed toolbar updating with the smaller icons from picked nodes/annotations --- coretk/coretk/toolbar.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 72288b71..1a0cac0b 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -14,11 +14,11 @@ from coretk.nodeutils import NodeUtils from coretk.themes import Styles from coretk.tooltip import Tooltip -WIDTH = 32 +TOOLBAR_SIZE = 32 PICKER_SIZE = 24 -def icon(image_enum, width=WIDTH): +def icon(image_enum, width=TOOLBAR_SIZE): return Images.get(image_enum, width) @@ -146,17 +146,23 @@ class Toolbar(ttk.Frame): self.node_picker = ttk.Frame(self.master) # draw default nodes for node_draw in NodeUtils.NODES: + toolbar_image = icon(node_draw.image_enum) image = icon(node_draw.image_enum, PICKER_SIZE) - func = partial(self.update_button, self.node_button, image, node_draw) + func = partial( + self.update_button, self.node_button, toolbar_image, node_draw + ) 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] - image = Images.get_custom(node_draw.image_file, WIDTH) - func = partial(self.update_button, self.node_button, image, node_draw) + toolbar_image = Images.get_custom(node_draw.image_file, TOOLBAR_SIZE) + image = Images.get_custom(node_draw.image_file, PICKER_SIZE) + func = partial( + self.update_button, self.node_button, toolbar_image, node_draw + ) self.create_picker_button(image, func, self.node_picker, name) # draw edit node - image = icon(ImageEnum.EDITNODE) + image = icon(ImageEnum.EDITNODE, PICKER_SIZE) self.create_picker_button( image, self.click_edit_node, self.node_picker, "Custom" ) @@ -276,10 +282,13 @@ class Toolbar(ttk.Frame): self.hide_pickers() self.network_picker = ttk.Frame(self.master) for node_draw in NodeUtils.NETWORK_NODES: + toolbar_image = icon(node_draw.image_enum) image = icon(node_draw.image_enum, PICKER_SIZE) self.create_picker_button( image, - partial(self.update_button, self.network_button, image, node_draw), + partial( + self.update_button, self.network_button, toolbar_image, node_draw + ), self.network_picker, node_draw.label, ) @@ -318,10 +327,11 @@ class Toolbar(ttk.Frame): (ImageEnum.TEXT, ShapeType.TEXT), ] for image_enum, shape_type in nodes: + toolbar_image = icon(image_enum) image = icon(image_enum, PICKER_SIZE) self.create_picker_button( image, - partial(self.update_annotation, image, shape_type), + partial(self.update_annotation, toolbar_image, shape_type), self.annotation_picker, shape_type.value, ) From 2344e026ff21e95d6e9cdee2dcbcc545a79d9421 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 12 Dec 2019 16:17:33 -0800 Subject: [PATCH 370/462] check emulation light, fix one line of backend code --- coretk/coretk/coreclient.py | 9 +++- coretk/coretk/dialogs/cel.py | 78 +++++++++++++++++++++++++++++++--- coretk/coretk/statusbar.py | 1 + coretk/coretk/toolbar.py | 1 + daemon/core/api/grpc/server.py | 2 +- 5 files changed, 82 insertions(+), 9 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 57224a93..fbc6fa8e 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -149,8 +149,8 @@ class CoreClient: self.handle_node_event(event.node_event) elif event.HasField("config_event"): logging.info("config event: %s", event) - elif event.HasField("throughput_event"): - logging.info("throughput event: %s", event) + elif event.HasField("exception_event"): + self.handle_exception_event(event.exception_event) else: logging.info("unhandled event: %s", event) @@ -182,6 +182,11 @@ class CoreClient: event.interface_throughputs ) + def handle_exception_event(self, event): + print(event) + print(event.node_id) + self.app.statusbar.core_alarms.append(event) + def join_session(self, session_id, query_location=True): # update session and title self.session_id = session_id diff --git a/coretk/coretk/dialogs/cel.py b/coretk/coretk/dialogs/cel.py index 7f95fc79..fb821e04 100644 --- a/coretk/coretk/dialogs/cel.py +++ b/coretk/coretk/dialogs/cel.py @@ -4,6 +4,9 @@ check engine light import tkinter as tk from tkinter import ttk +from grpc import RpcError + +from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog from coretk.images import ImageEnum, Images from coretk.themes import PADX, PADY @@ -36,17 +39,43 @@ class CheckLight(Dialog): frame.columnconfigure(0, weight=1) frame.grid(row=row, column=0, sticky="nsew") self.tree = ttk.Treeview( - frame, columns=("time", "level", "node", "source"), show="headings" + frame, + columns=("time", "level", "session_id", "node", "source"), + show="headings", ) self.tree.grid(row=0, column=0, sticky="nsew") self.tree.column("time", stretch=tk.YES) - self.tree.heading("time", text="time") + self.tree.heading("time", text="time", anchor="w") self.tree.column("level", stretch=tk.YES) - self.tree.heading("level", text="level") + self.tree.heading("level", text="level", anchor="w") + self.tree.column("session_id", stretch=tk.YES) + self.tree.heading("session_id", text="session id", anchor="w") self.tree.column("node", stretch=tk.YES) - self.tree.heading("node", text="node") + self.tree.heading("node", text="node", anchor="w") self.tree.column("source", stretch=tk.YES) - self.tree.heading("source", text="source") + self.tree.heading("source", text="source", anchor="w") + self.tree.bind("<>", self.click_select) + + for alarm in self.app.statusbar.core_alarms: + level = self.get_level(alarm.level) + self.tree.insert( + "", + tk.END, + text=str(alarm.date), + values=( + alarm.date, + level + " (%s)" % alarm.level, + alarm.session_id, + alarm.node_id, + alarm.source, + ), + tags=(level,), + ) + + self.tree.tag_configure("ERROR", background="#ff6666") + self.tree.tag_configure("FATAL", background="#d9d9d9") + self.tree.tag_configure("WARNING", background="#ffff99") + self.tree.tag_configure("NOTICE", background="#85e085") yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) yscrollbar.grid(row=0, column=1, sticky="ns") @@ -75,15 +104,52 @@ class CheckLight(Dialog): button = ttk.Button(frame, text="Close", command=self.destroy) button.grid(row=0, column=3, sticky="nsew", padx=PADX) frame.grid(row=row, column=0, sticky="nsew") - row = row + 1 def reset_cel(self): self.text.delete("1.0", tk.END) + for item in self.tree.get_children(): + self.tree.delete(item) + self.app.statusbar.core_alarms.clear() def daemon_log(self): dialog = DaemonLog(self, self.app) dialog.show() + def get_level(self, level): + if level == core_pb2.ExceptionLevel.ERROR: + return "ERROR" + if level == core_pb2.ExceptionLevel.FATAL: + return "FATAL" + if level == core_pb2.ExceptionLevel.WARNING: + return "WARNING" + if level == core_pb2.ExceptionLevel.NOTICE: + return "NOTICE" + + def click_select(self, event): + current = self.tree.selection() + values = self.tree.item(current)["values"] + time = values[0] + level = values[1] + session_id = values[2] + node_id = values[3] + source = values[4] + text = "DATE: %s\nLEVEL: %s\nNODE: %s (%s)\nSESSION: %s\nSOURCE: %s\n\n" % ( + time, + level, + node_id, + self.app.core.canvas_nodes[node_id].core_node.name, + session_id, + source, + ) + try: + sid = self.app.core.session_id + self.app.core.client.get_node(sid, node_id) + text = text + "node created" + except RpcError: + text = text + "node not created" + self.text.delete("1.0", "end") + self.text.insert("1.0", text) + class DaemonLog(Dialog): def __init__(self, master, app): diff --git a/coretk/coretk/statusbar.py b/coretk/coretk/statusbar.py index 797d61a8..945b5336 100644 --- a/coretk/coretk/statusbar.py +++ b/coretk/coretk/statusbar.py @@ -19,6 +19,7 @@ class StatusBar(ttk.Frame): self.memory = None self.emulation_light = None self.running = False + self.core_alarms = [] self.draw() def draw(self): diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 72288b71..24c4a74d 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -216,6 +216,7 @@ class Toolbar(ttk.Frame): :return: nothing """ + self.app.statusbar.core_alarms.clear() self.app.statusbar.progress_bar.start(5) self.app.canvas.mode = GraphMode.SELECT thread = threading.Thread(target=self.app.core.start_session) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 6a6747f1..dac09427 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -620,7 +620,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): return core_pb2.ExceptionEvent( node_id=event.node, session_id=int(event.session), - level=event.level.value, + level=event.level, source=event.source, date=event.date, text=event.text, From 89f8b421b8da17802875c8e4fb4291b77db98029 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 12 Dec 2019 16:23:27 -0800 Subject: [PATCH 371/462] check emulation light --- coretk/coretk/dialogs/cel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/coretk/coretk/dialogs/cel.py b/coretk/coretk/dialogs/cel.py index fb821e04..ee836737 100644 --- a/coretk/coretk/dialogs/cel.py +++ b/coretk/coretk/dialogs/cel.py @@ -87,6 +87,7 @@ class CheckLight(Dialog): row = row + 1 self.text = CodeText(self) + self.text.config(state=tk.DISABLED) self.text.grid(row=row, column=0, sticky="nsew") row = row + 1 From eca92af588a821e35075560e0c15b827bf2a6f16 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 12 Dec 2019 20:30:28 -0800 Subject: [PATCH 372/462] removed icon dialog, just use file chooser directly --- coretk/coretk/dialogs/customnodes.py | 15 +++--- coretk/coretk/dialogs/icondialog.py | 68 ---------------------------- coretk/coretk/dialogs/nodeconfig.py | 12 ++--- coretk/coretk/widgets.py | 13 ++++++ 4 files changed, 27 insertions(+), 81 deletions(-) delete mode 100644 coretk/coretk/dialogs/icondialog.py diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index d17d8d19..6aabf417 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -3,11 +3,12 @@ import tkinter as tk from pathlib import Path from tkinter import ttk +from coretk import nodeutils from coretk.dialogs.dialog import Dialog -from coretk.dialogs.icondialog import IconDialog +from coretk.images import Images from coretk.nodeutils import NodeDraw from coretk.themes import FRAME_PAD, PADX, PADY -from coretk.widgets import CheckboxList, ListboxScroll +from coretk.widgets import CheckboxList, ListboxScroll, image_chooser class ServicesSelectDialog(Dialog): @@ -170,11 +171,11 @@ class CustomNodesDialog(Dialog): self.image_button.config(image="") def click_icon(self): - dialog = IconDialog(self, self.app, self.name.get(), self.image) - dialog.show() - if dialog.image: - self.image = dialog.image - self.image_file = dialog.file_path.get() + file_path = image_chooser(self) + if file_path: + image = Images.create(file_path, nodeutils.ICON_SIZE) + self.image = image + self.image_file = file_path self.image_button.config(image=self.image) def click_services(self): diff --git a/coretk/coretk/dialogs/icondialog.py b/coretk/coretk/dialogs/icondialog.py deleted file mode 100644 index ecbb5d54..00000000 --- a/coretk/coretk/dialogs/icondialog.py +++ /dev/null @@ -1,68 +0,0 @@ -import tkinter as tk -from tkinter import filedialog, ttk - -from coretk import nodeutils -from coretk.appconfig import ICONS_PATH -from coretk.dialogs.dialog import Dialog -from coretk.images import Images -from coretk.themes import PADX, PADY - - -class IconDialog(Dialog): - def __init__(self, master, app, name, image): - super().__init__(master, app, f"{name} Icon", modal=True) - self.file_path = tk.StringVar() - self.image_label = None - self.image = image - self.draw() - - def draw(self): - self.top.columnconfigure(0, weight=1) - - # row one - frame = ttk.Frame(self.top) - frame.grid(pady=PADY, sticky="ew") - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=3) - label = ttk.Label(frame, text="Image") - label.grid(row=0, column=0, sticky="ew", padx=PADX) - entry = ttk.Entry(frame, textvariable=self.file_path) - entry.grid(row=0, column=1, sticky="ew", padx=PADX) - button = ttk.Button(frame, text="...", command=self.click_file) - button.grid(row=0, column=2) - - # row two - self.image_label = ttk.Label(self.top, image=self.image, anchor=tk.CENTER) - self.image_label.grid(pady=PADY, sticky="ew") - - # spacer - self.draw_spacer() - - # row three - frame = ttk.Frame(self.top) - frame.grid(sticky="ew") - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - button = ttk.Button(frame, text="Apply", command=self.destroy) - button.grid(row=0, column=0, sticky="ew", padx=PADX) - - button = ttk.Button(frame, text="Cancel", command=self.click_cancel) - button.grid(row=0, column=1, sticky="ew") - - def click_file(self): - file_path = filedialog.askopenfilename( - initialdir=str(ICONS_PATH), - title="Open", - filetypes=( - ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), - ("All Files", "*"), - ), - ) - if file_path: - self.image = Images.create(file_path, nodeutils.ICON_SIZE) - self.image_label.config(image=self.image) - self.file_path.set(file_path) - - def click_cancel(self): - self.image = None - self.destroy() diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 56c2a08d..8f4ad8a9 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -3,12 +3,13 @@ import tkinter as tk from functools import partial from tkinter import ttk +from coretk import nodeutils from coretk.dialogs.dialog import Dialog -from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeService +from coretk.images import Images from coretk.nodeutils import NodeUtils from coretk.themes import FRAME_PAD, PADX, PADY -from coretk.widgets import FrameScroll +from coretk.widgets import FrameScroll, image_chooser def mac_auto(is_auto, entry): @@ -196,10 +197,9 @@ class NodeConfigDialog(Dialog): dialog.show() def click_icon(self): - dialog = IconDialog(self, self.app, self.node.name, self.canvas_node.image) - dialog.show() - if dialog.image: - self.image = dialog.image + file_path = image_chooser(self) + if file_path: + self.image = Images.create(file_path, nodeutils.ICON_SIZE) self.image_button.config(image=self.image) def config_apply(self): diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 0ba16587..349401c5 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -5,6 +5,7 @@ from tkinter import filedialog, font, ttk from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 +from coretk.appconfig import ICONS_PATH from coretk.themes import FRAME_PAD, PADX, PADY INT_TYPES = { @@ -229,3 +230,15 @@ class Spinbox(ttk.Entry): def set(self, value): self.tk.call(self._w, "set", value) + + +def image_chooser(parent): + return filedialog.askopenfilename( + parent=parent, + initialdir=str(ICONS_PATH), + title="Select Icon", + filetypes=( + ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), + ("All Files", "*"), + ), + ) From fd838613995c7568f90dddf91894c5edc4d2f219 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 12 Dec 2019 21:05:45 -0800 Subject: [PATCH 373/462] updated cel to alerts dialog, updated layout to support resizing, added styles for alert button to be different colors --- coretk/coretk/dialogs/{cel.py => alerts.py} | 58 +++++++++------------ coretk/coretk/statusbar.py | 20 +++---- coretk/coretk/themes.py | 24 +++++++++ 3 files changed, 57 insertions(+), 45 deletions(-) rename coretk/coretk/dialogs/{cel.py => alerts.py} (77%) diff --git a/coretk/coretk/dialogs/cel.py b/coretk/coretk/dialogs/alerts.py similarity index 77% rename from coretk/coretk/dialogs/cel.py rename to coretk/coretk/dialogs/alerts.py index ee836737..58f65933 100644 --- a/coretk/coretk/dialogs/cel.py +++ b/coretk/coretk/dialogs/alerts.py @@ -8,36 +8,26 @@ from grpc import RpcError from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog -from coretk.images import ImageEnum, Images from coretk.themes import PADX, PADY from coretk.widgets import CodeText -class CheckLight(Dialog): +class AlertsDialog(Dialog): def __init__(self, master, app): - super().__init__(master, app, "CEL", modal=True) + super().__init__(master, app, "Alerts", modal=True) self.app = app self.tree = None self.text = None self.draw() def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + self.top.rowconfigure(1, weight=1) row = 0 - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - image = Images.get(ImageEnum.ALERT, 18) - label = ttk.Label(frame, image=image) - label.image = image - label.grid(row=0, column=0, sticky="e") - label = ttk.Label(frame, text="Check Emulation Light") - label.grid(row=0, column=1, sticky="w") - frame.grid(row=row, column=0, padx=PADX, pady=PADY, sticky="nsew") - row = row + 1 - - frame = ttk.Frame(self) - frame.columnconfigure(0, weight=1) - frame.grid(row=row, column=0, sticky="nsew") + frame.grid(row=row, column=0, sticky="nsew", pady=PADY) self.tree = ttk.Treeview( frame, columns=("time", "level", "session_id", "node", "source"), @@ -86,27 +76,27 @@ class CheckLight(Dialog): self.tree.configure(xscrollcommand=xscrollbar.set) row = row + 1 - self.text = CodeText(self) + self.text = CodeText(self.top) self.text.config(state=tk.DISABLED) - self.text.grid(row=row, column=0, sticky="nsew") + self.text.grid(row=row, column=0, sticky="nsew", pady=PADY) row = row + 1 - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) + frame.grid(row=row, column=0, sticky="nsew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) frame.columnconfigure(3, weight=1) - button = ttk.Button(frame, text="Reset CEL", command=self.reset_cel) - button.grid(row=0, column=0, sticky="nsew", padx=PADX) - button = ttk.Button(frame, text="View core-daemon log", command=self.daemon_log) - button.grid(row=0, column=1, sticky="nsew", padx=PADX) - button = ttk.Button(frame, text="View node log") - button.grid(row=0, column=2, sticky="nsew", padx=PADX) + button = ttk.Button(frame, text="Reset", command=self.reset_alerts) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Daemon Log", command=self.daemon_log) + button.grid(row=0, column=1, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Node Log") + button.grid(row=0, column=2, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Close", command=self.destroy) - button.grid(row=0, column=3, sticky="nsew", padx=PADX) - frame.grid(row=row, column=0, sticky="nsew") + button.grid(row=0, column=3, sticky="ew") - def reset_cel(self): + def reset_alerts(self): self.text.delete("1.0", tk.END) for item in self.tree.get_children(): self.tree.delete(item) @@ -160,20 +150,22 @@ class DaemonLog(Dialog): self.draw() def draw(self): - frame = ttk.Frame(self) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(1, weight=1) + frame = ttk.Frame(self.top) + frame.grid(row=0, column=0, sticky="ew", pady=PADY) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=9) label = ttk.Label(frame, text="File: ") label.grid(row=0, column=0) entry = ttk.Entry(frame, textvariable=self.path, state="disabled") - entry.grid(row=0, column=1, sticky="nsew") - frame.grid(row=0, column=0, sticky="nsew") + entry.grid(row=0, column=1, sticky="ew") try: file = open("/var/log/core-daemon.log", "r") log = file.readlines() except FileNotFoundError: log = "Log file not found" - text = CodeText(self) + text = CodeText(self.top) text.insert("1.0", log) text.see("end") text.config(state=tk.DISABLED) diff --git a/coretk/coretk/statusbar.py b/coretk/coretk/statusbar.py index 945b5336..0528bbf2 100644 --- a/coretk/coretk/statusbar.py +++ b/coretk/coretk/statusbar.py @@ -2,22 +2,21 @@ import tkinter as tk from tkinter import ttk -from coretk.dialogs.cel import CheckLight -from coretk.images import ImageEnum, Images +from coretk.dialogs.alerts import AlertsDialog +from coretk.themes import Styles class StatusBar(ttk.Frame): def __init__(self, master, app, **kwargs): super().__init__(master, **kwargs) self.app = app - self.status = None self.statusvar = tk.StringVar() self.progress_bar = None self.zoom = None self.cpu_usage = None self.memory = None - self.emulation_light = None + self.alerts_button = None self.running = False self.core_alarms = [] self.draw() @@ -56,16 +55,13 @@ class StatusBar(ttk.Frame): ) self.cpu_usage.grid(row=0, column=3, sticky="ew") - image = Images.get(ImageEnum.ALERT, 18) - self.emulation_light = ttk.Button( - self, image=image, text="Alert", compound="left" + self.alerts_button = ttk.Button( + self, text="Alerts", command=self.click_alerts, style=Styles.green_alert ) - self.emulation_light.image = image - self.emulation_light.bind("", self.cel_callback) - self.emulation_light.grid(row=0, column=4, sticky="ew") + self.alerts_button.grid(row=0, column=4, sticky="ew") - def cel_callback(self, event): - dialog = CheckLight(self.app, self.app) + def click_alerts(self): + dialog = AlertsDialog(self.app, self.app) dialog.show() def start_session_callback(self, process_time): diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index cb8a8b06..43b59ffd 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -13,6 +13,9 @@ class Styles: tooltip_frame = "Tooltip.TFrame" service_checkbutton = "Service.TCheckbutton" picker_button = "Picker.TButton" + green_alert = "GAlert.TButton" + red_alert = "RAlert.TButton" + yellow_alert = "YAlert.TButton" class Colors: @@ -163,3 +166,24 @@ def update_menu(style, widget): def theme_change(style, event): style.configure(Styles.picker_button, font=("TkDefaultFont", 8, "normal")) + style.configure( + Styles.green_alert, + background="green", + padding=0, + relief=tk.NONE, + font=("TkDefaultFont", 8, "normal"), + ) + style.configure( + Styles.yellow_alert, + background="yellow", + padding=0, + relief=tk.NONE, + font=("TkDefaultFont", 8, "normal"), + ) + style.configure( + Styles.red_alert, + background="red", + padding=0, + relief=tk.NONE, + font=("TkDefaultFont", 8, "normal"), + ) From d5b2edb6ab43b0c0225146d7e1f8746c443de2a5 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 12 Dec 2019 21:10:30 -0800 Subject: [PATCH 374/462] fixed unit tests for broadcasted exceptions to use proper values --- daemon/tests/test_grpc.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 56523e81..4446a74e 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -1086,7 +1086,9 @@ class TestGrpc: with client.context_connect(): client.events(session.id, handle_event) time.sleep(0.1) - session.exception(ExceptionLevels.FATAL, "test", None, "exception message") + session.exception( + ExceptionLevels.FATAL.value, "test", None, "exception message" + ) # then queue.get(timeout=5) From fb63d7e8b36ef3e25b5f33506139cbfae489e70f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 12 Dec 2019 21:33:51 -0800 Subject: [PATCH 375/462] added canvas boundary checks for mouse interactions --- coretk/coretk/graph/graph.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 42d2c4de..e439b842 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -130,6 +130,12 @@ class CanvasGraph(tk.Canvas): scaled_y = (y * self.ratio) + self.offset[1] return scaled_x, scaled_y + def inside_canvas(self, x, y): + x1, y1, x2, y2 = self.bbox(self.grid) + valid_x = x1 <= x <= x2 + valid_y = y1 <= y <= y2 + return valid_x and valid_y + def draw_grid(self): """ Create grid. @@ -269,13 +275,16 @@ class CanvasGraph(tk.Canvas): :return: nothing """ logging.debug("click release") + x, y = self.canvas_xy(event) + if not self.inside_canvas(x, y): + return + if self.context: self.context.unpost() self.context = None else: if self.mode == GraphMode.ANNOTATION: self.focus_set() - x, y = self.canvas_xy(event) if self.shape_drawing: shape = self.shapes[self.selected] shape.shape_complete(x, y) @@ -302,7 +311,6 @@ class CanvasGraph(tk.Canvas): if self.mode == GraphMode.EDGE: self.handle_edge_release(event) elif self.mode == GraphMode.NODE: - x, y = self.canvas_xy(event) self.add_node(x, y) elif self.mode == GraphMode.PICKNODE: self.mode = GraphMode.NODE @@ -453,6 +461,9 @@ class CanvasGraph(tk.Canvas): :return: nothing """ x, y = self.canvas_xy(event) + if not self.inside_canvas(x, y): + return + self.cursor = x, y selected = self.get_selected(event) logging.debug("click press(%s): %s", self.cursor, selected) @@ -495,6 +506,9 @@ class CanvasGraph(tk.Canvas): def ctrl_click(self, event): # update cursor location x, y = self.canvas_xy(event) + if not self.inside_canvas(x, y): + return + self.cursor = x, y # handle multiple selections @@ -515,6 +529,9 @@ class CanvasGraph(tk.Canvas): :return: nothing """ x, y = self.canvas_xy(event) + if not self.inside_canvas(x, y): + return + x_offset = x - self.cursor[0] y_offset = y - self.cursor[1] self.cursor = x, y @@ -531,7 +548,7 @@ class CanvasGraph(tk.Canvas): return # move selected objects - if len(self.selection) > 0: + if self.selection: for selected_id in self.selection: if selected_id in self.shapes: shape = self.shapes[selected_id] From d343bd06552aba1b56178dff058586e5fa27da76 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 08:48:40 -0800 Subject: [PATCH 376/462] updated gui to display custom icons if set, updated grpc to send custon icon and image data when present --- coretk/coretk/dialogs/nodeconfig.py | 6 ++++++ coretk/coretk/graph/graph.py | 9 +++++++++ daemon/core/api/grpc/server.py | 5 +++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 8f4ad8a9..c71a1b34 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -46,6 +46,7 @@ class NodeConfigDialog(Dialog): self.canvas_node = canvas_node self.node = canvas_node.core_node self.image = canvas_node.image + self.image_file = None self.image_button = None self.name = tk.StringVar(value=self.node.name) self.type = tk.StringVar(value=self.node.model) @@ -201,6 +202,7 @@ class NodeConfigDialog(Dialog): if file_path: self.image = Images.create(file_path, nodeutils.ICON_SIZE) self.image_button.config(image=self.image) + self.image_file = file_path def config_apply(self): # update core node @@ -211,6 +213,10 @@ class NodeConfigDialog(Dialog): if NodeUtils.is_container_node(self.node.type) and server != "localhost": self.node.server = server + # set custom icon + if self.image_file: + self.node.icon = self.image_file + # update canvas node self.canvas_node.image = self.image diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index e439b842..baa18170 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -4,6 +4,7 @@ import tkinter as tk from PIL import Image, ImageTk from core.api.grpc import core_pb2 +from coretk import nodeutils from coretk.dialogs.shapemod import ShapeDialog from coretk.graph import tags from coretk.graph.edges import CanvasEdge, CanvasWirelessEdge @@ -12,6 +13,7 @@ from coretk.graph.linkinfo import LinkInfo, Throughput from coretk.graph.node import CanvasNode from coretk.graph.shape import Shape from coretk.graph.shapeutils import ShapeType, is_draw_shape +from coretk.images import Images from coretk.nodeutils import NodeUtils ZOOM_IN = 1.1 @@ -186,12 +188,19 @@ class CanvasGraph(tk.Canvas): """ # draw existing nodes for core_node in session.nodes: + logging.info("drawing core node: %s", core_node) # peer to peer node is not drawn on the GUI if NodeUtils.is_ignore_node(core_node.type): continue # draw nodes on the canvas image = NodeUtils.node_icon(core_node.type, core_node.model) + if core_node.icon: + try: + image = Images.create(core_node.icon, nodeutils.ICON_SIZE) + except OSError: + logging.error("invalid icon: %s", core_node.icon) + x = core_node.position.x y = core_node.position.y node = CanvasNode(self.master, x, y, core_node, image) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index dac09427..fd1a73c3 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -393,15 +393,14 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): position = core_pb2.Position( x=node.position.x, y=node.position.y, z=node.position.z ) - services = getattr(node, "services", []) if services is None: services = [] services = [x.name for x in services] - emane_model = None if isinstance(node, EmaneNet): emane_model = node.model.name + image = getattr(node, "image", None) node_proto = core_pb2.Node( id=node.id, @@ -411,6 +410,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): type=node_type.value, position=position, services=services, + icon=node.icon, + image=image, ) if isinstance(node, (DockerNode, LxcNode)): node_proto.image = node.image From 9c302e8bc672c62c752da787bb2f0ddf42d812a8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 09:28:51 -0800 Subject: [PATCH 377/462] added some basic boundary checking for moving nodes and shapes --- coretk/coretk/graph/graph.py | 12 ++++++++++++ coretk/coretk/graph/node.py | 11 ++++++++++- coretk/coretk/graph/shape.py | 6 ++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index baa18170..42dbc934 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -138,6 +138,11 @@ class CanvasGraph(tk.Canvas): valid_y = y1 <= y <= y2 return valid_x and valid_y + def valid_position(self, x1, y1, x2, y2): + valid_topleft = self.inside_canvas(x1, y1) + valid_bottomright = self.inside_canvas(x2, y2) + return valid_topleft and valid_bottomright + def draw_grid(self): """ Create grid. @@ -539,6 +544,13 @@ class CanvasGraph(tk.Canvas): """ x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): + if self.select_box: + self.select_box.delete() + self.select_box = None + if is_draw_shape(self.annotation_type) and self.shape_drawing: + shape = self.shapes.pop(self.selected) + shape.delete() + self.shape_drawing = False return x_offset = x - self.cursor[0] diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index a0f406f7..11c7eaf5 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -103,10 +103,19 @@ class CanvasNode: self.motion(x_offset, y_offset, update=False) def motion(self, x_offset, y_offset, update=True): + original_position = self.canvas.coords(self.id) self.canvas.move(self.id, x_offset, y_offset) + x, y = self.canvas.coords(self.id) + + # check new position + bbox = self.canvas.bbox(self.id) + if not self.canvas.valid_position(*bbox): + self.canvas.coords(self.id, original_position) + return + + # move test and selection box self.canvas.move(self.text_id, x_offset, y_offset) self.canvas.move_selection(self.id, x_offset, y_offset) - x, y = self.canvas.coords(self.id) # move antennae for antenna_id in self.antennae: diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 99a18a9f..e7277b49 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -136,7 +136,13 @@ class Shape: self.canvas.delete(self.id) def motion(self, x_offset, y_offset): + original_position = self.canvas.coords(self.id) self.canvas.move(self.id, x_offset, y_offset) + coords = self.canvas.coords(self.id) + if not self.canvas.valid_position(*coords): + self.canvas.coords(self.id, original_position) + return + self.canvas.move_selection(self.id, x_offset, y_offset) if self.text_id is not None: self.canvas.move(self.text_id, x_offset, y_offset) From 819954a695d1a6e0b2cb721c68ca83b1761543dc Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 10:47:23 -0800 Subject: [PATCH 378/462] updated grpc node positions to use floats, avoids needing to deal with int conversions --- coretk/coretk/coreclient.py | 3 +-- coretk/coretk/graph/graph.py | 5 +---- coretk/coretk/graph/node.py | 9 ++------- daemon/core/api/grpc/server.py | 8 +------- daemon/core/xml/corexml.py | 14 ++++---------- daemon/proto/core/api/grpc/core.proto | 6 +++--- 6 files changed, 12 insertions(+), 33 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index ec6ca772..a5f407b7 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -183,8 +183,7 @@ class CoreClient: ) def handle_exception_event(self, event): - print(event) - print(event.node_id) + logging.info("exception event: %s", event) self.app.statusbar.core_alarms.append(event) def join_session(self, session_id, query_location=True): diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 42dbc934..a2d82dc4 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -616,10 +616,7 @@ class CanvasGraph(tk.Canvas): if self.selected is None or self.selected in self.shapes: actual_x, actual_y = self.get_actual_coords(x, y) core_node = self.core.create_node( - int(actual_x), - int(actual_y), - self.node_draw.node_type, - self.node_draw.model, + actual_x, actual_y, self.node_draw.node_type, self.node_draw.model ) node = CanvasNode(self.master, x, y, core_node, self.node_draw.image) self.core.canvas_nodes[core_node.id] = node diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 11c7eaf5..0d0f8706 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -140,8 +140,8 @@ class CanvasNode: # set actual coords for node and update core is running real_x, real_y = self.canvas.get_actual_coords(x, y) - self.core_node.position.x = int(real_x) - self.core_node.position.y = int(real_y) + self.core_node.position.x = real_x + self.core_node.position.y = real_y if self.app.core.is_runtime() and update: self.app.core.edit_node(self.core_node) @@ -164,11 +164,6 @@ class CanvasNode: else: self.show_config() - def update_coords(self): - x, y = self.canvas.coords(self.id) - self.core_node.position.x = int(x) - self.core_node.position.y = int(y) - def create_context(self): is_wlan = self.core_node.type == NodeType.WIRELESS_LAN is_emane = self.core_node.type == NodeType.EMANE diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index fd1a73c3..4ee32751 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -489,13 +489,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :return: node event that contains node id, name, model, position, and services :rtype: core.api.grpc.core_pb2.NodeEvent """ - x = None - if event.x_position is not None: - x = int(event.x_position) - y = None - if event.y_position is not None: - y = int(event.y_position) - position = core_pb2.Position(x=x, y=y) + position = core_pb2.Position(x=event.x_position, y=event.y_position) services = event.services or "" services = services.split("|") node_proto = core_pb2.Node( diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 9ba63395..285b7a3b 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -117,14 +117,8 @@ class NodeElement: def add_position(self): x = self.node.position.x - if x is not None: - x = int(x) y = self.node.position.y - if y is not None: - y = int(y) z = self.node.position.z - if z is not None: - z = int(z) lat, lon, alt = None, None, None if x is not None and y is not None: lat, lon, alt = self.session.location.getgeo(x, y, z) @@ -751,8 +745,8 @@ class CoreXmlReader: position_element = device_element.find("position") if position_element is not None: - x = get_int(position_element, "x") - y = get_int(position_element, "y") + x = get_float(position_element, "x") + y = get_float(position_element, "y") if all([x, y]): options.set_position(x, y) @@ -773,8 +767,8 @@ class CoreXmlReader: position_element = network_element.find("position") if position_element is not None: - x = get_int(position_element, "x") - y = get_int(position_element, "y") + x = get_float(position_element, "x") + y = get_float(position_element, "y") if all([x, y]): options.set_position(x, y) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 57bbf3f4..253102bf 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -949,9 +949,9 @@ message SessionLocation { } message Position { - int32 x = 1; - int32 y = 2; - int32 z = 3; + float x = 1; + float y = 2; + float z = 3; float lat = 4; float lon = 5; float alt = 6; From 358985d1292f08a016eedbcf439f4402d8a3c1d3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 10:54:42 -0800 Subject: [PATCH 379/462] update to avoid not reusing session ids --- daemon/core/emulator/coreemu.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py index cdba4e44..158dc296 100644 --- a/daemon/core/emulator/coreemu.py +++ b/daemon/core/emulator/coreemu.py @@ -5,7 +5,6 @@ import signal import sys import core.services -from core.emulator.emudata import IdGen from core.emulator.session import Session from core.services.coreservices import ServiceManager @@ -49,7 +48,6 @@ class CoreEmu: self.config = config # session management - self.session_id_gen = IdGen() self.sessions = {} # load services @@ -79,7 +77,6 @@ class CoreEmu: :return: nothing """ logging.info("shutting down all sessions") - self.session_id_gen.id = 0 sessions = self.sessions.copy() self.sessions.clear() for _id in sessions: @@ -96,11 +93,9 @@ class CoreEmu: :rtype: EmuSession """ if not _id: - while True: - _id = self.session_id_gen.next() - if _id not in self.sessions: - break - + _id = 1 + while _id in self.sessions: + _id += 1 session = _cls(_id, config=self.config) logging.info("created session: %s", _id) self.sessions[_id] = session From b993fadedb91f0439decfd889cd881803d52d909 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 11:24:35 -0800 Subject: [PATCH 380/462] removed grpc check for getting a node service file, it will return the default value when not currently set --- daemon/core/api/grpc/server.py | 7 ------- daemon/core/services/coreservices.py | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 4ee32751..27b069f0 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -1158,13 +1158,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("get node service file: %s", request) session = self.get_session(request.session_id, context) node = self.get_node(session, request.node_id, context) - service = None - for current_service in node.services: - if current_service.name == request.service: - service = current_service - break - if not service: - context.abort(grpc.StatusCode.NOT_FOUND, "service not found") file_data = session.services.get_service_file( node, request.service, request.file ) diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index 361061be..b51eb715 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -396,7 +396,7 @@ class CoreServices: """ Add services to a node. - :param core.coreobj.PyCoreNode node: node to add services to + :param core.nodes.base.CoreNode node: node to add services to :param str node_type: node type to add services to :param list[str] services: names of services to add to node :return: nothing From 9b16f272b86f73c39ab20e60c6ea527caeae79cc Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 11:48:36 -0800 Subject: [PATCH 381/462] added get wlan configs, made use of it in coretk, updated node context to allow wlan config during runtime --- coretk/coretk/coreclient.py | 12 +++++++----- coretk/coretk/graph/node.py | 2 ++ daemon/core/api/grpc/client.py | 12 ++++++++++++ daemon/core/api/grpc/server.py | 25 +++++++++++++++++++++++++ daemon/proto/core/api/grpc/core.proto | 10 ++++++++++ 5 files changed, 56 insertions(+), 5 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index a5f407b7..f9ac0384 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -239,15 +239,17 @@ class CoreClient: node_id, config.model, config.config, interface ) + # get wlan configurations + response = self.client.get_wlan_configs(self.session_id) + for _id in response.configs: + mapped_config = response.configs[_id] + self.wlan_configs[_id] = mapped_config.config + # save and retrieve data, needed for session nodes for node in session.nodes: # get node service config and file config - # get wlan configs for wlan nodes - if node.type == core_pb2.NodeType.WIRELESS_LAN: - response = self.client.get_wlan_config(self.session_id, node.id) - self.wlan_configs[node.id] = response.config # retrieve service configurations data for default nodes - elif node.type == core_pb2.NodeType.DEFAULT: + if node.type == core_pb2.NodeType.DEFAULT: for service in node.services: response = self.client.get_node_service( self.session_id, node.id, service diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 0d0f8706..93b6c390 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -173,6 +173,8 @@ class CanvasNode: context.add_command(label="Configure", command=self.show_config) if NodeUtils.is_container_node(self.core_node.type): context.add_command(label="Services", state=tk.DISABLED) + if is_wlan: + context.add_command(label="WLAN Config", command=self.show_wlan_config) if is_wlan and self.core_node.id in self.app.core.mobility_players: context.add_command( label="Mobility Player", command=self.show_mobility_player diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 05380b79..7887d5e8 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -827,6 +827,18 @@ class CoreGrpcClient: ) return self.stub.ServiceAction(request) + def get_wlan_configs(self, session_id): + """ + Get all wlan configurations. + + :param int session_id: session id + :return: response with a dict of node ids to wlan configurations + :rtype: core_pb2.GetWlanConfigsResponse + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.GetWlanConfigsRequest(session_id=session_id) + return self.stub.GetWlanConfigs(request) + def get_wlan_config(self, session_id, node_id): """ Get wlan configuration for a node. diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 27b069f0..9b8c51c7 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -1236,6 +1236,31 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): return core_pb2.ServiceActionResponse(result=result) + def GetWlanConfigs(self, request, context): + """ + Retrieve all wireless-lan configurations. + + :param core.api.grpc.core_pb2.GetWlanConfigsRequest request: request + :param context: core.api.grpc.core_pb2.GetWlanConfigResponse + :return: all wlan configurations + :rtype: core.api.grpc.core_pb2.GetWlanConfigsResponse + """ + logging.debug("get wlan configs: %s", request) + session = self.get_session(request.session_id, context) + response = core_pb2.GetWlanConfigsResponse() + for node_id in session.mobility.node_configurations: + model_config = session.mobility.node_configurations[node_id] + if node_id == -1: + continue + for model_name in model_config: + if model_name != BasicRangeModel.name: + continue + current_config = session.mobility.get_model_config(node_id, model_name) + config = get_config_options(current_config, BasicRangeModel) + mapped_config = core_pb2.MappedConfig(config=config) + response.configs[node_id].CopyFrom(mapped_config) + return response + def GetWlanConfig(self, request, context): """ Retrieve wireless-lan configuration of a node diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 253102bf..609316f8 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -101,6 +101,8 @@ service CoreApi { } // wlan rpc + rpc GetWlanConfigs (GetWlanConfigsRequest) returns (GetWlanConfigsResponse) { + } rpc GetWlanConfig (GetWlanConfigRequest) returns (GetWlanConfigResponse) { } rpc SetWlanConfig (SetWlanConfigRequest) returns (SetWlanConfigResponse) { @@ -585,6 +587,14 @@ message ServiceActionResponse { bool result = 1; } +message GetWlanConfigsRequest { + int32 session_id = 1; +} + +message GetWlanConfigsResponse { + map configs = 1; +} + message GetWlanConfigRequest { int32 session_id = 1; int32 node_id = 2; From 9ada94107e1db5cbe273dec43ba3d3a7c1ff0d7c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 14:03:41 -0800 Subject: [PATCH 382/462] changes to grpc get emane model configs to return the interface value and actual node id, instead of coded value that would need to be parsed --- coretk/coretk/coreclient.py | 10 ++++------ daemon/core/api/grpc/grpcutils.py | 16 ++++++++++++++++ daemon/core/api/grpc/server.py | 11 ++++++----- daemon/proto/core/api/grpc/core.proto | 1 + 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index f9ac0384..86451338 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -228,13 +228,11 @@ class CoreClient: # get emane model config response = self.client.get_emane_model_configs(self.session_id) - for _id in response.configs: - config = response.configs[_id] + for node_id in response.configs: + config = response.configs[node_id] interface = None - node_id = _id - if _id >= 1000: - interface = _id % 1000 - node_id = int(_id / 1000) + if config.interface != -1: + interface = config.interface self.set_emane_model_config( node_id, config.model, config.config, interface ) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index cf1250c8..7df86a18 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -221,6 +221,22 @@ def get_emane_model_id(node_id, interface_id): return node_id +def parse_emane_model_id(_id): + """ + Parses EMANE model id to get true node id and interface id. + + :param _id: id to parse + :return: node id and interface id + :rtype: tuple + """ + interface = -1 + node_id = _id + if _id >= 1000: + interface = _id % 1000 + node_id = int(_id / 1000) + return node_id, interface + + def convert_link(session, link_data): """ Convert link_data into core protobuf Link diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 9b8c51c7..0b566333 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -1395,17 +1395,18 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("get emane model configs: %s", request) session = self.get_session(request.session_id, context) response = core_pb2.GetEmaneModelConfigsResponse() - for node_id in session.emane.node_configurations: - model_config = session.emane.node_configurations[node_id] - if node_id == -1: + for _id in session.emane.node_configurations: + if _id == -1: continue + model_config = session.emane.node_configurations[_id] for model_name in model_config: model = session.emane.models[model_name] - current_config = session.emane.get_model_config(node_id, model_name) + current_config = session.emane.get_model_config(_id, model_name) config = get_config_options(current_config, model) + node_id, interface = grpcutils.parse_emane_model_id(_id) model_config = core_pb2.GetEmaneModelConfigsResponse.ModelConfig( - model=model_name, config=config + model=model_name, config=config, interface=interface ) response.configs[node_id].CopyFrom(model_config) return response diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 609316f8..fb7aaadc 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -666,6 +666,7 @@ message GetEmaneModelConfigsResponse { message ModelConfig { string model = 1; map config = 2; + int32 interface = 3; } map configs = 1; } From 9d988a4b138f740c36f3671496f1c2325ea1af75 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 14:18:13 -0800 Subject: [PATCH 383/462] fixed issue in grpc get emane model configs that would allow key collision --- coretk/coretk/coreclient.py | 5 ++--- daemon/core/api/grpc/server.py | 16 ++++++++++------ daemon/proto/core/api/grpc/core.proto | 7 ++++--- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 86451338..bff47501 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -228,13 +228,12 @@ class CoreClient: # get emane model config response = self.client.get_emane_model_configs(self.session_id) - for node_id in response.configs: - config = response.configs[node_id] + for config in response.configs: interface = None if config.interface != -1: interface = config.interface self.set_emane_model_config( - node_id, config.model, config.config, interface + config.node_id, config.model, config.config, interface ) # get wlan configurations diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 0b566333..e552e1a4 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -1394,22 +1394,26 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get emane model configs: %s", request) session = self.get_session(request.session_id, context) - response = core_pb2.GetEmaneModelConfigsResponse() + + configs = [] for _id in session.emane.node_configurations: if _id == -1: continue - model_config = session.emane.node_configurations[_id] - for model_name in model_config: + model_configs = session.emane.node_configurations[_id] + for model_name in model_configs: model = session.emane.models[model_name] current_config = session.emane.get_model_config(_id, model_name) config = get_config_options(current_config, model) node_id, interface = grpcutils.parse_emane_model_id(_id) model_config = core_pb2.GetEmaneModelConfigsResponse.ModelConfig( - model=model_name, config=config, interface=interface + node_id=node_id, + model=model_name, + interface=interface, + config=config, ) - response.configs[node_id].CopyFrom(model_config) - return response + configs.append(model_config) + return core_pb2.GetEmaneModelConfigsResponse(configs=configs) def SaveXml(self, request, context): """ diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index fb7aaadc..5f6a964b 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -664,11 +664,12 @@ message GetEmaneModelConfigsRequest { message GetEmaneModelConfigsResponse { message ModelConfig { - string model = 1; - map config = 2; + int32 node_id = 1; + string model = 2; int32 interface = 3; + map config = 4; } - map configs = 1; + repeated ModelConfig configs = 1; } message SaveXmlRequest { From 47e087b3651d14a3437f4e468a8bca138428d9ad Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 15:28:22 -0800 Subject: [PATCH 384/462] fixed unit tests for grpc get emane model configs --- daemon/tests/test_grpc.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 4446a74e..d4df63d7 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -708,10 +708,11 @@ class TestGrpc: # then assert len(response.configs) == 1 - assert emane_network.id in response.configs - model_config = response.configs[emane_network.id] + model_config = response.configs[0] + assert emane_network.id == model_config.node_id assert model_config.model == EmaneIeee80211abgModel.name assert len(model_config.config) > 0 + assert model_config.interface == -1 def test_set_emane_model_config(self, grpc_server): # given From 2afbf63e4b76b02c1b538fbc937d8e2ccba1f2ef Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 13 Dec 2019 15:52:48 -0800 Subject: [PATCH 385/462] start working on color picker --- coretk/coretk/dialogs/colorpicker.py | 150 +++++++++++++++++++++++++++ coretk/coretk/validation.py | 32 +++++- 2 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 coretk/coretk/dialogs/colorpicker.py diff --git a/coretk/coretk/dialogs/colorpicker.py b/coretk/coretk/dialogs/colorpicker.py new file mode 100644 index 00000000..ded652a0 --- /dev/null +++ b/coretk/coretk/dialogs/colorpicker.py @@ -0,0 +1,150 @@ +""" +custom color picker +""" +import logging +import tkinter as tk +from tkinter import ttk + +from coretk.dialogs.dialog import Dialog + + +class ColorPicker(Dialog): + def __init__(self, master, app, initcolor="#000000"): + super().__init__(master, app, "color picker", modal=True) + self.red_entry = None + self.blue_entry = None + self.green_entry = None + self.hex_entry = None + self.display = None + + self.red = tk.StringVar(value=0) + self.blue = tk.StringVar(value=0) + self.green = tk.StringVar(value=0) + self.hex = tk.StringVar(value=initcolor) + + self.draw() + self.set_bindings() + + def draw(self): + edit_frame = ttk.Frame(self) + edit_frame.columnconfigure(0, weight=4) + edit_frame.columnconfigure(1, weight=2) + # the rbg frame + frame = ttk.Frame(edit_frame) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.rowconfigure(0, weight=1) + frame.rowconfigure(1, weight=1) + frame.rowconfigure(2, weight=1) + label = ttk.Label(frame, text="R: ") + label.grid(row=0, column=0) + self.red_entry = ttk.Entry( + frame, + textvariable=self.red, + validate="key", + validatecommand=(self.app.validation.rgb, "%P"), + ) + self.red_entry.grid(row=0, column=1, sticky="nsew") + + label = ttk.Label(frame, text="G: ") + label.grid(row=1, column=0) + self.green_entry = ttk.Entry( + frame, + textvariable=self.green, + validate="key", + validatecommand=(self.app.validation.rgb, "%P"), + ) + self.green_entry.grid(row=1, column=1, sticky="nsew") + + label = ttk.Label(frame, text="B: ") + label.grid(row=2, column=0) + self.blue_entry = ttk.Entry( + frame, + textvariable=self.blue, + validate="key", + validatecommand=(self.app.validation.rgb, "%P"), + ) + self.blue_entry.grid(row=2, column=1, sticky="nsew") + + frame.grid(row=0, column=0, sticky="nsew") + + # hex code and color display + frame = ttk.Frame(edit_frame) + frame.columnconfigure(0, weight=1) + label = ttk.Label(frame, text="Selection: ") + label.grid(row=0, column=0, sticky="nsew") + self.hex_entry = ttk.Entry( + frame, + textvariable=self.hex, + validate="key", + validatecommand=(self.app.validation.hex, "%P"), + ) + self.hex_entry.grid(row=1, column=0, sticky="nsew") + self.display = ttk.Label(frame, background="white") + self.display.grid(row=2, column=0, sticky="nsew") + frame.grid(row=0, column=1, sticky="nsew") + + edit_frame.grid(row=0, column=0, sticky="nsew") + + # button frame + frame = ttk.Frame(self) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + button = ttk.Button(frame, text="OK", command=self.button_ok) + button.grid(row=0, column=0, sticky="nsew") + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="nsew") + frame.grid(row=1, column=0, sticky="nsew") + + def set_bindings(self): + self.red_entry.bind("", lambda x: self.current_focus("rgb")) + self.green_entry.bind("", lambda x: self.current_focus("rgb")) + self.blue_entry.bind("", lambda x: self.current_focus("rgb")) + self.hex_entry.bind("", lambda x: self.current_focus("hex")) + self.red.trace_add("write", self.update_color) + self.green.trace_add("write", self.update_color) + self.blue.trace_add("write", self.update_color) + self.hex.trace_add("write", self.update_color) + + def button_ok(self): + logging.debug("not implemented") + + def get_hex(self): + red = self.red_entry.get() + blue = self.blue_entry.get() + green = self.green_entry.get() + return "#%02x%02x%02x" % (int(red), int(green), int(blue)) + + def current_focus(self, focus): + self.focus = focus + + def update_color(self, arg1, arg2, arg3): + if self.focus == "rgb": + red = self.red_entry.get() + blue = self.blue_entry.get() + green = self.green_entry.get() + if red and blue and green: + hex_code = "#%02x%02x%02x" % (int(red), int(green), int(blue)) + self.hex_entry.delete(0, tk.END) + self.hex_entry.insert(0, hex_code) + self.display.config(background=hex_code) + elif self.focus == "hex": + hex_code = self.hex.get() + if len(hex_code) == 4 or len(hex_code) == 7: + if len(hex_code) == 4: + red = hex_code[1] + green = hex_code[2] + blue = hex_code[3] + else: + red = hex_code[1:3] + green = hex_code[3:5] + blue = hex_code[5:] + else: + return + self.red_entry.delete(0, tk.END) + self.green_entry.delete(0, tk.END) + self.blue_entry.delete(0, tk.END) + self.red_entry.insert(0, "%s" % (int(red, 16))) + self.green_entry.insert(0, "%s" % (int(green, 16))) + self.blue_entry.insert(0, "%s" % (int(blue, 16))) + self.display.config(background=hex_code) diff --git a/coretk/coretk/validation.py b/coretk/coretk/validation.py index c7923229..955a7faf 100644 --- a/coretk/coretk/validation.py +++ b/coretk/coretk/validation.py @@ -15,6 +15,8 @@ class InputValidation: self.positive_float = None self.name = None self.ip4 = None + self.rgb = None + self.hex = None self.register() def register(self): @@ -22,6 +24,8 @@ class InputValidation: self.positive_float = self.master.register(self.check_positive_float) self.name = self.master.register(self.check_node_name) self.ip4 = self.master.register(self.check_ip4) + self.rgb = self.master.register(self.check_rbg) + self.hex = self.master.register(self.check_hex) def ip_focus_out(self, event): value = event.widget.get() @@ -100,7 +104,7 @@ class InputValidation: return False for _8bits in _32bits: if ( - (_8bits and int(_8bits) > 225) + (_8bits and int(_8bits) > 255) or len(_8bits) > 3 or (_8bits.startswith("0") and len(_8bits) > 1) ): @@ -108,3 +112,29 @@ class InputValidation: return True else: return False + + def check_rbg(self, s): + if not s: + return True + if s.startswith("0") and len(s) >= 2: + return False + try: + value = int(s) + if 0 <= value <= 255: + return True + else: + return False + except ValueError: + return False + + def check_hex(self, s): + if not s: + return True + pat = re.compile("^([#]([0-9]|[a-f])+)$|^[#]$") + if pat.match(s): + if 0 <= len(s) <= 7: + return True + else: + return False + else: + return False From 47cc20b5671233ce7990244180034f3e8786636b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 16:11:58 -0800 Subject: [PATCH 386/462] updates to grpc event streaming, client can now listen to a subset of desired events --- daemon/core/api/grpc/client.py | 7 +- daemon/core/api/grpc/events.py | 268 ++++++++++++++++++++++++++ daemon/core/api/grpc/server.py | 217 +-------------------- daemon/proto/core/api/grpc/core.proto | 12 ++ 4 files changed, 293 insertions(+), 211 deletions(-) create mode 100644 daemon/core/api/grpc/events.py diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 7887d5e8..ae193909 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -378,16 +378,17 @@ class CoreGrpcClient: ) return self.stub.AddSessionServer(request) - def events(self, session_id, handler): + def events(self, session_id, handler, events=None): """ Listen for session events. :param int session_id: id of session - :param handler: handler for every event + :param handler: handler for received events + :param list events: events to listen to, defaults to all :return: nothing :raises grpc.RpcError: when session doesn't exist """ - request = core_pb2.EventsRequest(session_id=session_id) + request = core_pb2.EventsRequest(session_id=session_id, events=events) stream = self.stub.Events(request) start_streamer(stream, handler) diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py new file mode 100644 index 00000000..d7a3094e --- /dev/null +++ b/daemon/core/api/grpc/events.py @@ -0,0 +1,268 @@ +import logging +from queue import Empty, Queue + +from core.api.grpc import core_pb2 +from core.api.grpc.grpcutils import convert_value +from core.emulator.data import ( + ConfigData, + EventData, + ExceptionData, + FileData, + LinkData, + NodeData, +) + + +def handle_node_event(event): + """ + Handle node event when there is a node event + + :param core.emulator.data.NodeData event: node data + :return: node event that contains node id, name, model, position, and services + :rtype: core.api.grpc.core_pb2.NodeEvent + """ + position = core_pb2.Position(x=event.x_position, y=event.y_position) + services = event.services or "" + services = services.split("|") + node_proto = core_pb2.Node( + id=event.id, + name=event.name, + model=event.model, + position=position, + services=services, + ) + return core_pb2.NodeEvent(node=node_proto, source=event.source) + + +def handle_link_event(event): + """ + Handle link event when there is a link event + + :param core.emulator.data.LinkData event: link data + :return: link event that has message type and link information + :rtype: core.api.grpc.core_pb2.LinkEvent + """ + interface_one = None + if event.interface1_id is not None: + interface_one = core_pb2.Interface( + id=event.interface1_id, + name=event.interface1_name, + mac=convert_value(event.interface1_mac), + ip4=convert_value(event.interface1_ip4), + ip4mask=event.interface1_ip4_mask, + ip6=convert_value(event.interface1_ip6), + ip6mask=event.interface1_ip6_mask, + ) + + interface_two = None + if event.interface2_id is not None: + interface_two = core_pb2.Interface( + id=event.interface2_id, + name=event.interface2_name, + mac=convert_value(event.interface2_mac), + ip4=convert_value(event.interface2_ip4), + ip4mask=event.interface2_ip4_mask, + ip6=convert_value(event.interface2_ip6), + ip6mask=event.interface2_ip6_mask, + ) + + options = core_pb2.LinkOptions( + opaque=event.opaque, + jitter=event.jitter, + key=event.key, + mburst=event.mburst, + mer=event.mer, + per=event.per, + bandwidth=event.bandwidth, + burst=event.burst, + delay=event.delay, + dup=event.dup, + unidirectional=event.unidirectional, + ) + link = core_pb2.Link( + type=event.link_type, + node_one_id=event.node1_id, + node_two_id=event.node2_id, + interface_one=interface_one, + interface_two=interface_two, + options=options, + ) + return core_pb2.LinkEvent(message_type=event.message_type, link=link) + + +def handle_session_event(event): + """ + Handle session event when there is a session event + + :param core.emulator.data.EventData event: event data + :return: session event + :rtype: core.api.grpc.core_pb2.SessionEvent + """ + event_time = event.time + if event_time is not None: + event_time = float(event_time) + return core_pb2.SessionEvent( + node_id=event.node, + event=event.event_type, + name=event.name, + data=event.data, + time=event_time, + session_id=event.session, + ) + + +def handle_config_event(event): + """ + Handle configuration event when there is configuration event + + :param core.emulator.data.ConfigData event: configuration data + :return: configuration event + :rtype: core.api.grpc.core_pb2.ConfigEvent + """ + session_id = None + if event.session is not None: + session_id = int(event.session) + return core_pb2.ConfigEvent( + message_type=event.message_type, + node_id=event.node, + object=event.object, + type=event.type, + captions=event.captions, + bitmap=event.bitmap, + data_values=event.data_values, + possible_values=event.possible_values, + groups=event.groups, + session_id=session_id, + interface=event.interface_number, + network_id=event.network_id, + opaque=event.opaque, + data_types=event.data_types, + ) + + +def handle_exception_event(event): + """ + Handle exception event when there is exception event + + :param core.emulator.data.ExceptionData event: exception data + :return: exception event + :rtype: core.api.grpc.core_pb2.ExceptionEvent + """ + return core_pb2.ExceptionEvent( + node_id=event.node, + session_id=int(event.session), + level=event.level, + source=event.source, + date=event.date, + text=event.text, + opaque=event.opaque, + ) + + +def handle_file_event(event): + """ + Handle file event + + :param core.emulator.data.FileData event: file data + :return: file event + :rtype: core.api.grpc.core_pb2.FileEvent + """ + return core_pb2.FileEvent( + message_type=event.message_type, + node_id=event.node, + name=event.name, + mode=event.mode, + number=event.number, + type=event.type, + source=event.source, + session_id=event.session, + data=event.data, + compressed_data=event.compressed_data, + ) + + +class EventStreamer: + """ + Processes session events to generate grpc events. + """ + + def __init__(self, session, event_types): + """ + Create a EventStreamer instance. + + :param core.emulator.session.Session session: session to process events for + :param set event_types: types of events to process + """ + self.session = session + self.event_types = event_types + self.queue = Queue() + self.add_handlers() + + def add_handlers(self): + """ + Add a session event handler for desired event types. + + :return: nothing + """ + if core_pb2.EventType.NODE in self.event_types: + self.session.node_handlers.append(self.queue.put) + if core_pb2.EventType.LINK in self.event_types: + self.session.link_handlers.append(self.queue.put) + if core_pb2.EventType.CONFIG in self.event_types: + self.session.config_handlers.append(self.queue.put) + if core_pb2.EventType.FILE in self.event_types: + self.session.file_handlers.append(self.queue.put) + if core_pb2.EventType.EXCEPTION in self.event_types: + self.session.exception_handlers.append(self.queue.put) + if core_pb2.EventType.SESSION in self.event_types: + self.session.event_handlers.append(self.queue.put) + + def process(self): + """ + Process the next event in the queue. + + :return: grpc event, or None when invalid event or queue timeout + :rtype: core.api.grpc.core_pb2.Event + """ + event = core_pb2.Event() + try: + data = self.queue.get(timeout=1) + if isinstance(data, NodeData): + event.node_event.CopyFrom(handle_node_event(data)) + elif isinstance(data, LinkData): + event.link_event.CopyFrom(handle_link_event(data)) + elif isinstance(data, EventData): + event.session_event.CopyFrom(handle_session_event(data)) + elif isinstance(data, ConfigData): + event.config_event.CopyFrom(handle_config_event(data)) + # TODO: remove when config events are fixed + event.config_event.session_id = self.session.id + elif isinstance(data, ExceptionData): + event.exception_event.CopyFrom(handle_exception_event(data)) + elif isinstance(data, FileData): + event.file_event.CopyFrom(handle_file_event(data)) + else: + logging.error("unknown event: %s", data) + event = None + except Empty: + event = None + return event + + def remove_handlers(self): + """ + Remove session event handlers for events being watched. + + :return: nothing + """ + if core_pb2.EventType.NODE in self.event_types: + self.session.node_handlers.remove(self.queue.put) + if core_pb2.EventType.LINK in self.event_types: + self.session.link_handlers.remove(self.queue.put) + if core_pb2.EventType.CONFIG in self.event_types: + self.session.config_handlers.remove(self.queue.put) + if core_pb2.EventType.FILE in self.event_types: + self.session.file_handlers.remove(self.queue.put) + if core_pb2.EventType.EXCEPTION in self.event_types: + self.session.exception_handlers.remove(self.queue.put) + if core_pb2.EventType.SESSION in self.event_types: + self.session.event_handlers.remove(self.queue.put) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index e552e1a4..75ce23ba 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -5,27 +5,19 @@ import re import tempfile import time from concurrent import futures -from queue import Empty, Queue import grpc from core.api.grpc import core_pb2, core_pb2_grpc, grpcutils +from core.api.grpc.events import EventStreamer from core.api.grpc.grpcutils import ( - convert_value, get_config_options, get_emane_model_id, get_links, get_net_stats, ) from core.emane.nodes import EmaneNet -from core.emulator.data import ( - ConfigData, - EventData, - ExceptionData, - FileData, - LinkData, - NodeData, -) +from core.emulator.data import LinkData from core.emulator.emudata import LinkOptions, NodeOptions from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags from core.errors import CoreCommandError, CoreError @@ -439,210 +431,19 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): def Events(self, request, context): session = self.get_session(request.session_id, context) - queue = Queue() - session.node_handlers.append(queue.put) - session.link_handlers.append(queue.put) - session.config_handlers.append(queue.put) - session.file_handlers.append(queue.put) - session.exception_handlers.append(queue.put) - session.event_handlers.append(queue.put) + event_types = set(request.events) + if not event_types: + event_types = set(core_pb2.EventType.Enum.values()) + streamer = EventStreamer(session, event_types) while self._is_running(context): - event = core_pb2.Event() - try: - data = queue.get(timeout=1) - if isinstance(data, NodeData): - event.node_event.CopyFrom(self._handle_node_event(data)) - elif isinstance(data, LinkData): - event.link_event.CopyFrom(self._handle_link_event(data)) - elif isinstance(data, EventData): - event.session_event.CopyFrom(self._handle_session_event(data)) - elif isinstance(data, ConfigData): - event.config_event.CopyFrom(self._handle_config_event(data)) - # TODO: remove when config events are fixed - event.config_event.session_id = session.id - elif isinstance(data, ExceptionData): - event.exception_event.CopyFrom(self._handle_exception_event(data)) - elif isinstance(data, FileData): - event.file_event.CopyFrom(self._handle_file_event(data)) - else: - logging.error("unknown event: %s", data) - continue - + event = streamer.process() + if event: yield event - except Empty: - continue - session.node_handlers.remove(queue.put) - session.link_handlers.remove(queue.put) - session.config_handlers.remove(queue.put) - session.file_handlers.remove(queue.put) - session.exception_handlers.remove(queue.put) - session.event_handlers.remove(queue.put) + streamer.remove_handlers() self._cancel_stream(context) - def _handle_node_event(self, event): - """ - Handle node event when there is a node event - - :param core.emulator.data.NodeData event: node data - :return: node event that contains node id, name, model, position, and services - :rtype: core.api.grpc.core_pb2.NodeEvent - """ - position = core_pb2.Position(x=event.x_position, y=event.y_position) - services = event.services or "" - services = services.split("|") - node_proto = core_pb2.Node( - id=event.id, - name=event.name, - model=event.model, - position=position, - services=services, - ) - return core_pb2.NodeEvent(node=node_proto, source=event.source) - - def _handle_link_event(self, event): - """ - Handle link event when there is a link event - - :param core.emulator.data.LinkData event: link data - :return: link event that has message type and link information - :rtype: core.api.grpc.core_pb2.LinkEvent - """ - interface_one = None - if event.interface1_id is not None: - interface_one = core_pb2.Interface( - id=event.interface1_id, - name=event.interface1_name, - mac=convert_value(event.interface1_mac), - ip4=convert_value(event.interface1_ip4), - ip4mask=event.interface1_ip4_mask, - ip6=convert_value(event.interface1_ip6), - ip6mask=event.interface1_ip6_mask, - ) - - interface_two = None - if event.interface2_id is not None: - interface_two = core_pb2.Interface( - id=event.interface2_id, - name=event.interface2_name, - mac=convert_value(event.interface2_mac), - ip4=convert_value(event.interface2_ip4), - ip4mask=event.interface2_ip4_mask, - ip6=convert_value(event.interface2_ip6), - ip6mask=event.interface2_ip6_mask, - ) - - options = core_pb2.LinkOptions( - opaque=event.opaque, - jitter=event.jitter, - key=event.key, - mburst=event.mburst, - mer=event.mer, - per=event.per, - bandwidth=event.bandwidth, - burst=event.burst, - delay=event.delay, - dup=event.dup, - unidirectional=event.unidirectional, - ) - link = core_pb2.Link( - type=event.link_type, - node_one_id=event.node1_id, - node_two_id=event.node2_id, - interface_one=interface_one, - interface_two=interface_two, - options=options, - ) - return core_pb2.LinkEvent(message_type=event.message_type, link=link) - - def _handle_session_event(self, event): - """ - Handle session event when there is a session event - - :param core.emulator.data.EventData event: event data - :return: session event - :rtype: core.api.grpc.core_pb2.SessionEvent - """ - event_time = event.time - if event_time is not None: - event_time = float(event_time) - return core_pb2.SessionEvent( - node_id=event.node, - event=event.event_type, - name=event.name, - data=event.data, - time=event_time, - session_id=event.session, - ) - - def _handle_config_event(self, event): - """ - Handle configuration event when there is configuration event - - :param core.emulator.data.ConfigData event: configuration data - :return: configuration event - :rtype: core.api.grpc.core_pb2.ConfigEvent - """ - session_id = None - if event.session is not None: - session_id = int(event.session) - return core_pb2.ConfigEvent( - message_type=event.message_type, - node_id=event.node, - object=event.object, - type=event.type, - captions=event.captions, - bitmap=event.bitmap, - data_values=event.data_values, - possible_values=event.possible_values, - groups=event.groups, - session_id=session_id, - interface=event.interface_number, - network_id=event.network_id, - opaque=event.opaque, - data_types=event.data_types, - ) - - def _handle_exception_event(self, event): - """ - Handle exception event when there is exception event - - :param core.emulator.data.ExceptionData event: exception data - :return: exception event - :rtype: core.api.grpc.core_pb2.ExceptionEvent - """ - return core_pb2.ExceptionEvent( - node_id=event.node, - session_id=int(event.session), - level=event.level, - source=event.source, - date=event.date, - text=event.text, - opaque=event.opaque, - ) - - def _handle_file_event(self, event): - """ - Handle file event - - :param core.emulator.data.FileData event: file data - :return: file event - :rtype: core.api.grpc.core_pb2.FileEvent - """ - return core_pb2.FileEvent( - message_type=event.message_type, - node_id=event.node, - name=event.name, - mode=event.mode, - number=event.number, - type=event.type, - source=event.source, - session_id=event.session, - data=event.data, - compressed_data=event.compressed_data, - ) - def Throughputs(self, request, context): """ Calculate average throughput after every certain amount of delay time diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 5f6a964b..55ed272e 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -266,6 +266,7 @@ message AddSessionServerResponse { message EventsRequest { int32 session_id = 1; + repeated EventType.Enum events = 2; } message ThroughputsRequest { @@ -742,6 +743,17 @@ message ServiceFileConfig { string data = 4; } +message EventType { + enum Enum { + SESSION = 0; + NODE = 1; + LINK = 2; + CONFIG = 3; + EXCEPTION = 4; + FILE = 5; + } +} + message MessageType { enum Enum { NONE = 0; From 85521e8c8f4e467a0fa73fedf414530539cb8be9 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 18:17:42 -0800 Subject: [PATCH 387/462] added grpc to get current service configurations, fixed bug for core daemon not using custom service configs --- coretk/coretk/coreclient.py | 32 ++++++--------- coretk/coretk/dialogs/serviceconfiguration.py | 2 +- daemon/core/api/grpc/client.py | 12 ++++++ daemon/core/api/grpc/grpcutils.py | 22 +++++++++++ daemon/core/api/grpc/server.py | 39 +++++++++++++------ daemon/core/services/coreservices.py | 7 +++- daemon/proto/core/api/grpc/core.proto | 16 ++++++++ daemon/tests/test_grpc.py | 18 +++++++++ 8 files changed, 112 insertions(+), 36 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index bff47501..776e1750 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -242,27 +242,17 @@ class CoreClient: mapped_config = response.configs[_id] self.wlan_configs[_id] = mapped_config.config - # save and retrieve data, needed for session nodes - for node in session.nodes: - # get node service config and file config - # retrieve service configurations data for default nodes - if node.type == core_pb2.NodeType.DEFAULT: - for service in node.services: - response = self.client.get_node_service( - self.session_id, node.id, service - ) - if node.id not in self.service_configs: - self.service_configs[node.id] = {} - self.service_configs[node.id][service] = response.service - for file in response.service.configs: - response = self.client.get_node_service_file( - self.session_id, node.id, service, file - ) - if node.id not in self.file_configs: - self.file_configs[node.id] = {} - if service not in self.file_configs[node.id]: - self.file_configs[node.id][service] = {} - self.file_configs[node.id][service][file] = response.data + # get service configurations + response = self.client.get_node_service_configs(self.session_id) + for config in response.configs: + service_configs = self.service_configs.setdefault(config.node_id, {}) + service_configs[config.service] = config.data + logging.info("service file configs: %s", config.files) + for file_name in config.files: + file_configs = self.file_configs.setdefault(config.node_id, {}) + files = file_configs.setdefault(config.service, {}) + data = config.files[file_name] + files[file_name] = data # draw session self.app.canvas.reset_and_redraw(session) diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 03c74ab2..a53c2aa1 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -323,7 +323,7 @@ class ServiceConfiguration(Dialog): button = ttk.Button(frame, text="Apply", command=self.click_apply) button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button( - frame, text="Dafults", command=self.click_defaults, state="disabled" + frame, text="Defaults", command=self.click_defaults, state="disabled" ) button.grid(row=0, column=1, sticky="ew", padx=PADX) button = ttk.Button( diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index ae193909..fbafbb44 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -731,6 +731,18 @@ class CoreGrpcClient: ) return self.stub.SetServiceDefaults(request) + def get_node_service_configs(self, session_id): + """ + Get service data for a node. + + :param int session_id: session id + :return: response with all node service configs + :rtype: core_pb2.GetNodeServiceConfigsResponse + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.GetNodeServiceConfigsRequest(session_id=session_id) + return self.stub.GetNodeServiceConfigs(request) + def get_node_service(self, session_id, node_id, service): """ Get service data for a node. diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 7df86a18..4ea752fd 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -352,3 +352,25 @@ def service_configuration(session, config): service.startup = tuple(config.startup) service.validate = tuple(config.validate) service.shutdown = tuple(config.shutdown) + + +def get_service_configuration(service): + """ + Convenience for converting a service to service data proto. + + :param service: service to get proto data for + :return: service proto data + :rtype: core.api.grpc.core_pb2.NodeServiceData + """ + return core_pb2.NodeServiceData( + executables=service.executables, + dependencies=service.dependencies, + dirs=service.dirs, + configs=service.configs, + startup=service.startup, + validate=service.validate, + validation_mode=service.validation_mode.value, + validation_timer=service.validation_timer, + shutdown=service.shutdown, + meta=service.meta, + ) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 75ce23ba..1ada5267 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -917,6 +917,32 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): ] = service_defaults.services return core_pb2.SetServiceDefaultsResponse(result=True) + def GetNodeServiceConfigs(self, request, context): + """ + Retrieve all node service configurations. + + :param core.api.grpc.core_pb2.GetNodeServiceConfigsRequest request: + get-node-service request + :param grpc.ServicerContext context: context object + :return: all node service configs response + :rtype: core.api.grpc.core_pb2.GetNodeServiceConfigsResponse + """ + logging.debug("get node service configs: %s", request) + session = self.get_session(request.session_id, context) + configs = [] + for node_id, service_configs in session.services.custom_services.items(): + for name in service_configs: + service = session.services.get_service(node_id, name) + service_proto = grpcutils.get_service_configuration(service) + config = core_pb2.GetNodeServiceConfigsResponse.ServiceConfig( + node_id=node_id, + service=name, + data=service_proto, + files=service.config_data, + ) + configs.append(config) + return core_pb2.GetNodeServiceConfigsResponse(configs=configs) + def GetNodeService(self, request, context): """ Retrieve a requested service from a node @@ -932,18 +958,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): service = session.services.get_service( request.node_id, request.service, default_service=True ) - service_proto = core_pb2.NodeServiceData( - executables=service.executables, - dependencies=service.dependencies, - dirs=service.dirs, - configs=service.configs, - startup=service.startup, - validate=service.validate, - validation_mode=service.validation_mode.value, - validation_timer=service.validation_timer, - shutdown=service.shutdown, - meta=service.meta, - ) + service_proto = grpcutils.get_service_configuration(service) return core_pb2.GetNodeServiceResponse(service=service_proto) def GetNodeServiceFile(self, request, context): diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index b51eb715..684ccbb9 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -461,8 +461,8 @@ class CoreServices: :param core.netns.vnode.LxcNode node: node to start services on :return: nothing """ - funcs = [] boot_paths = ServiceDependencies(node.services).boot_paths() + funcs = [] for boot_path in boot_paths: args = (node, boot_path) funcs.append((self._start_boot_paths, args, {})) @@ -484,6 +484,7 @@ class CoreServices: " -> ".join([x.name for x in boot_path]), ) for service in boot_path: + service = self.get_service(node.id, service.name, default_service=True) try: self.boot_service(node, service) except Exception: @@ -744,7 +745,9 @@ class CoreServices: config_files = service.get_configs(node) for file_name in config_files: - logging.debug("generating service config: %s", file_name) + logging.debug( + "generating service config custom(%s): %s", service.custom, file_name + ) if service.custom: cfg = service.config_data.get(file_name) if cfg is None: diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 55ed272e..b0a6c0d6 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -89,6 +89,8 @@ service CoreApi { } rpc SetServiceDefaults (SetServiceDefaultsRequest) returns (SetServiceDefaultsResponse) { } + rpc GetNodeServiceConfigs (GetNodeServiceConfigsRequest) returns (GetNodeServiceConfigsResponse) { + } rpc GetNodeService (GetNodeServiceRequest) returns (GetNodeServiceResponse) { } rpc GetNodeServiceFile (GetNodeServiceFileRequest) returns (GetNodeServiceFileResponse) { @@ -538,6 +540,20 @@ message SetServiceDefaultsResponse { bool result = 1; } +message GetNodeServiceConfigsRequest { + int32 session_id = 1; +} + +message GetNodeServiceConfigsResponse { + message ServiceConfig { + int32 node_id = 1; + string service = 2; + NodeServiceData data = 3; + map files = 4; + } + repeated ServiceConfig configs = 1; +} + message GetNodeServiceRequest { int32 session_id = 1; int32 node_id = 2; diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index d4df63d7..796febf7 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -878,6 +878,24 @@ class TestGrpc: assert response.result is True assert session.services.default_services[node_type] == services + def test_get_node_service_configs(self, grpc_server): + # given + client = CoreGrpcClient() + session = grpc_server.coreemu.create_session() + node = session.add_node() + service_name = "DefaultRoute" + session.services.set_service(node.id, service_name) + + # then + with client.context_connect(): + response = client.get_node_service_configs(session.id) + + # then + assert len(response.configs) == 1 + service_config = response.configs[0] + assert service_config.node_id == node.id + assert service_config.service == service_name + def test_get_node_service(self, grpc_server): # given client = CoreGrpcClient() From 424f69bb1519f35852d49780b3f73ed02a0145cd Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 11:14:05 -0800 Subject: [PATCH 388/462] updated grpc throughputs to only check a specific session and verify the data being collected and sent is for that session, fixed data from throughputs being in hex getting converted to int, updated coretk to only run throughputs when enabled, updated grpc streams to return the stream to allow it being canceled --- coretk/coretk/coreclient.py | 22 ++++++++++++++++------ coretk/coretk/menuaction.py | 7 +++---- daemon/core/api/grpc/client.py | 12 ++++++++---- daemon/core/api/grpc/server.py | 19 ++++++++++++++----- daemon/proto/core/api/grpc/core.proto | 6 ++++-- daemon/tests/test_grpc.py | 4 ++-- 6 files changed, 47 insertions(+), 23 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 776e1750..9f6900cc 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -84,7 +84,7 @@ class CoreClient: self.service_configs = {} self.file_configs = {} self.mobility_players = {} - self.throughput = False + self.handling_throughputs = None def reset(self): # helpers @@ -176,11 +176,22 @@ class CoreClient: canvas_node = self.canvas_nodes[node_id] canvas_node.move(x, y) + def enable_throughputs(self): + self.handling_throughputs = self.client.throughputs( + self.session_id, self.handle_throughputs + ) + + def cancel_throughputs(self): + self.handling_throughputs.cancel() + self.handling_throughputs = None + def handle_throughputs(self, event): - if self.throughput: - self.app.canvas.throughput_draw.process_grpc_throughput_event( - event.interface_throughputs - ) + if event.session_id != self.session_id: + return + logging.info("handling throughputs event: %s", event) + self.app.canvas.throughput_draw.process_grpc_throughput_event( + event.interface_throughputs + ) def handle_exception_event(self, event): logging.info("exception event: %s", event) @@ -200,7 +211,6 @@ class CoreClient: session = response.session self.state = session.state self.client.events(self.session_id, self.handle_events) - self.client.throughputs(self.handle_throughputs) # get location if query_location: diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 616e9bc1..ec7e7cb7 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -151,8 +151,7 @@ class MenuAction: dialog.show() def throughput(self): - throughput = self.app.core.throughput - if throughput: - self.app.core.throughput = False + if not self.app.core.handling_throughputs: + self.app.core.enable_throughputs() else: - self.app.core.throughput = True + self.app.core.cancel_throughputs() diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index fbafbb44..bc48c9ab 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -385,23 +385,27 @@ class CoreGrpcClient: :param int session_id: id of session :param handler: handler for received events :param list events: events to listen to, defaults to all - :return: nothing + :return: stream processing events, can be used to cancel stream :raises grpc.RpcError: when session doesn't exist """ request = core_pb2.EventsRequest(session_id=session_id, events=events) stream = self.stub.Events(request) start_streamer(stream, handler) + return stream - def throughputs(self, handler): + def throughputs(self, session_id, handler): """ Listen for throughput events with information for interfaces and bridges. + :param int session_id: session id :param handler: handler for every event - :return: nothing + :return: stream processing events, can be used to cancel stream + :raises grpc.RpcError: when session doesn't exist """ - request = core_pb2.ThroughputsRequest() + request = core_pb2.ThroughputsRequest(session_id=session_id) stream = self.stub.Throughputs(request) start_streamer(stream, handler) + return stream def add_node(self, session_id, node): """ diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 1ada5267..53c1d6d6 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -27,7 +27,7 @@ from core.nodes.lxd import LxcNode from core.services.coreservices import ServiceManager _ONE_DAY_IN_SECONDS = 60 * 60 * 24 -_INTERFACE_REGEX = re.compile(r"\d+") +_INTERFACE_REGEX = re.compile(r"[0-9a-fA-F]+") class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): @@ -452,9 +452,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param grpc.SrevicerContext context: context object :return: nothing """ + session = self.get_session(request.session_id, context) delay = 3 last_check = None last_stats = None + while self._is_running(context): now = time.monotonic() stats = get_net_stats() @@ -462,7 +464,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): # calculate average if last_check is not None: interval = now - last_check - throughputs_event = core_pb2.ThroughputsEvent() + throughputs_event = core_pb2.ThroughputsEvent(session_id=session.id) for key in stats: current_rxtx = stats[key] previous_rxtx = last_stats.get(key) @@ -477,8 +479,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): throughput = rx_kbps + tx_kbps if key.startswith("veth"): key = key.split(".") - node_id = int(_INTERFACE_REGEX.search(key[0]).group()) - interface_id = int(key[1]) + node_id = int(_INTERFACE_REGEX.search(key[0]).group(), base=16) + interface_id = int(key[1], base=16) + session_id = int(key[2], base=16) + if session.id != session_id: + continue interface_throughput = ( throughputs_event.interface_throughputs.add() ) @@ -487,7 +492,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): interface_throughput.throughput = throughput elif key.startswith("b."): try: - node_id = int(key.split(".")[1]) + key = key.split(".") + node_id = int(key[1], base=16) + session_id = int(key[2], base=16) + if session.id != session_id: + continue bridge_throughput = ( throughputs_event.bridge_throughputs.add() ) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index b0a6c0d6..ee099b31 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -272,11 +272,13 @@ message EventsRequest { } message ThroughputsRequest { + int32 session_id = 1; } message ThroughputsEvent { - repeated BridgeThroughput bridge_throughputs = 1; - repeated InterfaceThroughput interface_throughputs = 2; + int32 session_id = 1; + repeated BridgeThroughput bridge_throughputs = 2; + repeated InterfaceThroughput interface_throughputs = 3; } message InterfaceThroughput { diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 796febf7..72e469f3 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -1032,7 +1032,7 @@ class TestGrpc: # given client = CoreGrpcClient() - grpc_server.coreemu.create_session() + session = grpc_server.coreemu.create_session() queue = Queue() def handle_event(event_data): @@ -1040,7 +1040,7 @@ class TestGrpc: # then with client.context_connect(): - client.throughputs(handle_event) + client.throughputs(session.id, handle_event) time.sleep(0.1) # then From d248bc09b5ef6a46c4bcf3529bc7e0540ee18d6e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 11:17:20 -0800 Subject: [PATCH 389/462] added clearing streams to reset for coreclient and set event stream to be cleared --- coretk/coretk/coreclient.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 9f6900cc..b1d4d22f 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -85,6 +85,7 @@ class CoreClient: self.file_configs = {} self.mobility_players = {} self.handling_throughputs = None + self.handling_events = None def reset(self): # helpers @@ -101,6 +102,13 @@ class CoreClient: self.service_configs.clear() self.file_configs.clear() 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 def set_observer(self, value): self.observer = value @@ -210,7 +218,9 @@ class CoreClient: response = self.client.get_session(self.session_id) session = response.session self.state = session.state - self.client.events(self.session_id, self.handle_events) + self.handling_events = self.client.events( + self.session_id, self.handle_events + ) # get location if query_location: From 9eaa1fb36c706a5bd60106848ea807b3c9c11fa7 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 16 Dec 2019 11:42:39 -0800 Subject: [PATCH 390/462] more work on color picker --- coretk/coretk/dialogs/colorpicker.py | 139 ++++++++++++++++++--------- coretk/coretk/dialogs/shapemod.py | 14 ++- 2 files changed, 105 insertions(+), 48 deletions(-) diff --git a/coretk/coretk/dialogs/colorpicker.py b/coretk/coretk/dialogs/colorpicker.py index ded652a0..305be7d4 100644 --- a/coretk/coretk/dialogs/colorpicker.py +++ b/coretk/coretk/dialogs/colorpicker.py @@ -16,60 +16,98 @@ class ColorPicker(Dialog): self.green_entry = None self.hex_entry = None self.display = None - - self.red = tk.StringVar(value=0) - self.blue = tk.StringVar(value=0) - self.green = tk.StringVar(value=0) + self.color = initcolor + red, green, blue = self.get_rgb(initcolor) + self.red = tk.IntVar(value=red) + self.blue = tk.IntVar(value=blue) + self.green = tk.IntVar(value=green) self.hex = tk.StringVar(value=initcolor) + self.red_scale = tk.IntVar(value=red) + self.green_scale = tk.IntVar(value=green) + self.blue_scale = tk.IntVar(value=blue) + def askcolor(self): self.draw() self.set_bindings() + self.show() + return self.color def draw(self): - edit_frame = ttk.Frame(self) - edit_frame.columnconfigure(0, weight=4) - edit_frame.columnconfigure(1, weight=2) - # the rbg frame - frame = ttk.Frame(edit_frame) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - frame.rowconfigure(0, weight=1) - frame.rowconfigure(1, weight=1) - frame.rowconfigure(2, weight=1) + # rgb frames + frame = ttk.Frame(self) label = ttk.Label(frame, text="R: ") label.grid(row=0, column=0) self.red_entry = ttk.Entry( frame, + width=4, textvariable=self.red, validate="key", validatecommand=(self.app.validation.rgb, "%P"), ) self.red_entry.grid(row=0, column=1, sticky="nsew") + scale = ttk.Scale( + frame, + from_=0, + to=255, + value=0, + length=200, + orient=tk.HORIZONTAL, + variable=self.red_scale, + command=lambda x: self.scale_callback(self.red_scale, self.red), + ) + scale.grid(row=0, column=2, sticky="nsew") + frame.grid(row=0, column=0, sticky="nsew") + frame = ttk.Frame(self) label = ttk.Label(frame, text="G: ") - label.grid(row=1, column=0) + label.grid(row=0, column=0) self.green_entry = ttk.Entry( frame, + width=4, textvariable=self.green, validate="key", validatecommand=(self.app.validation.rgb, "%P"), ) - self.green_entry.grid(row=1, column=1, sticky="nsew") + self.green_entry.grid(row=0, column=1, sticky="nsew") + scale = ttk.Scale( + frame, + from_=0, + to=255, + value=0, + length=200, + orient=tk.HORIZONTAL, + variable=self.green_scale, + command=lambda x: self.scale_callback(self.green_scale, self.green), + ) + scale.grid(row=0, column=2, sticky="nsew") + frame.grid(row=1, column=0) + frame = ttk.Frame(self) label = ttk.Label(frame, text="B: ") - label.grid(row=2, column=0) + label.grid(row=0, column=0) self.blue_entry = ttk.Entry( frame, + width=4, textvariable=self.blue, validate="key", validatecommand=(self.app.validation.rgb, "%P"), ) - self.blue_entry.grid(row=2, column=1, sticky="nsew") - - frame.grid(row=0, column=0, sticky="nsew") + self.blue_entry.grid(row=0, column=1, sticky="nsew") + scale = ttk.Scale( + frame, + from_=0, + to=255, + value=0, + length=200, + orient=tk.HORIZONTAL, + variable=self.blue_scale, + command=lambda x: self.scale_callback(self.blue_scale, self.blue), + ) + scale.grid(row=0, column=2, sticky="nsew") + frame.grid(row=2, column=0, sticky="nsew") # hex code and color display - frame = ttk.Frame(edit_frame) + frame = ttk.Frame(self) frame.columnconfigure(0, weight=1) label = ttk.Label(frame, text="Selection: ") label.grid(row=0, column=0, sticky="nsew") @@ -80,11 +118,9 @@ class ColorPicker(Dialog): validatecommand=(self.app.validation.hex, "%P"), ) self.hex_entry.grid(row=1, column=0, sticky="nsew") - self.display = ttk.Label(frame, background="white") + self.display = ttk.Label(frame, background=self.color) self.display.grid(row=2, column=0, sticky="nsew") - frame.grid(row=0, column=1, sticky="nsew") - - edit_frame.grid(row=0, column=0, sticky="nsew") + frame.grid(row=3, column=0, sticky="nsew") # button frame frame = ttk.Frame(self) @@ -94,7 +130,7 @@ class ColorPicker(Dialog): button.grid(row=0, column=0, sticky="nsew") button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="nsew") - frame.grid(row=1, column=0, sticky="nsew") + frame.grid(row=4, column=0, sticky="nsew") def set_bindings(self): self.red_entry.bind("", lambda x: self.current_focus("rgb")) @@ -108,6 +144,8 @@ class ColorPicker(Dialog): def button_ok(self): logging.debug("not implemented") + self.color = self.hex.get() + self.destroy() def get_hex(self): red = self.red_entry.get() @@ -118,33 +156,48 @@ class ColorPicker(Dialog): def current_focus(self, focus): self.focus = focus - def update_color(self, arg1, arg2, arg3): + def update_color(self, arg1=None, arg2=None, arg3=None): if self.focus == "rgb": red = self.red_entry.get() blue = self.blue_entry.get() green = self.green_entry.get() + self.set_scale(red, green, blue) if red and blue and green: hex_code = "#%02x%02x%02x" % (int(red), int(green), int(blue)) - self.hex_entry.delete(0, tk.END) - self.hex_entry.insert(0, hex_code) + self.hex.set(hex_code) self.display.config(background=hex_code) elif self.focus == "hex": hex_code = self.hex.get() if len(hex_code) == 4 or len(hex_code) == 7: - if len(hex_code) == 4: - red = hex_code[1] - green = hex_code[2] - blue = hex_code[3] - else: - red = hex_code[1:3] - green = hex_code[3:5] - blue = hex_code[5:] + red, green, blue = self.get_rgb(hex_code) else: return - self.red_entry.delete(0, tk.END) - self.green_entry.delete(0, tk.END) - self.blue_entry.delete(0, tk.END) - self.red_entry.insert(0, "%s" % (int(red, 16))) - self.green_entry.insert(0, "%s" % (int(green, 16))) - self.blue_entry.insert(0, "%s" % (int(blue, 16))) + self.set_entry(red, green, blue) + self.set_scale(red, green, blue) self.display.config(background=hex_code) + + def scale_callback(self, var, color_var): + color_var.set(var.get()) + self.focus = "rgb" + self.update_color() + + def set_scale(self, r, g, b): + self.red_scale.set(r) + self.green_scale.set(g) + self.blue_scale.set(b) + + def set_entry(self, r, g, b): + self.red.set(r) + self.green.set(g) + self.blue.set(b) + + def get_rgb(self, hex_code): + if len(hex_code) == 4: + red = hex_code[1] + green = hex_code[2] + blue = hex_code[3] + else: + red = hex_code[1:3] + green = hex_code[3:5] + blue = hex_code[5:] + return int(red, 16), int(green, 16), int(blue, 16) diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index a6da0e3b..27bc719f 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -4,6 +4,7 @@ shape input dialog import tkinter as tk from tkinter import colorchooser, font, ttk +from coretk.dialogs.colorpicker import ColorPicker from coretk.dialogs.dialog import Dialog from coretk.graph import tags from coretk.graph.shapeutils import is_draw_shape, is_shape_text @@ -134,13 +135,16 @@ class ShapeDialog(Dialog): button.grid(row=0, column=1, sticky="ew") def choose_text_color(self): - color = colorchooser.askcolor(color="black") - self.text_color = color[1] + color_picker = ColorPicker(self, self.app, "#000000") + color = color_picker.askcolor() + self.text_color = color def choose_fill_color(self): - color = colorchooser.askcolor(color=self.fill_color) - self.fill_color = color[1] - self.fill.config(background=color[1], text=color[1]) + color_picker = ColorPicker(self, self.app, self.fill_color) + color = color_picker.askcolor() + # color = colorchooser.askcolor(color=self.fill_color) + self.fill_color = color + self.fill.config(background=color, text=color) def choose_border_color(self): color = colorchooser.askcolor(color=self.border_color) From 34c0b91228a1ed087556f9691807bcc014e0917c Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 16 Dec 2019 12:04:18 -0800 Subject: [PATCH 391/462] update zoom % on statusbar --- coretk/coretk/graph/graph.py | 2 ++ coretk/coretk/statusbar.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index a2d82dc4..e8fd6cff 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -464,6 +464,8 @@ class CanvasGraph(tk.Canvas): ) logging.info("ratio: %s", self.ratio) logging.info("offset: %s", self.offset) + self.app.statusbar.zoom.config(text="%s" % (int(self.ratio * 100)) + "%") + if self.wallpaper: self.redraw_wallpaper() diff --git a/coretk/coretk/statusbar.py b/coretk/coretk/statusbar.py index 0528bbf2..5ed6f09d 100644 --- a/coretk/coretk/statusbar.py +++ b/coretk/coretk/statusbar.py @@ -46,7 +46,11 @@ class StatusBar(ttk.Frame): self.status.grid(row=0, column=1, sticky="ew") self.zoom = ttk.Label( - self, text="ZOOM TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE + self, + text="%s" % (int(self.app.canvas.ratio * 100)) + "%", + anchor=tk.CENTER, + borderwidth=1, + relief=tk.RIDGE, ) self.zoom.grid(row=0, column=2, sticky="ew") From 44df926fb9d221fb0c4415d52d121144c1e84ae2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 12:21:03 -0800 Subject: [PATCH 392/462] updated events streamed from sessions to include session id for easy identification --- coretk/coretk/coreclient.py | 18 ++++++++++++++++-- daemon/core/api/grpc/events.py | 11 +---------- daemon/proto/core/api/grpc/core.proto | 25 +++++++++++-------------- daemon/tests/test_grpc.py | 7 +++++++ 4 files changed, 35 insertions(+), 26 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index b1d4d22f..8b01a468 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -20,6 +20,7 @@ from coretk.graph.shapeutils import ShapeType from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils +GUI_SOURCE = "gui" OBSERVERS = { "processes": "ps", "ifconfig": "ifconfig", @@ -133,6 +134,14 @@ class CoreClient: self.custom_observers[observer.name] = observer def handle_events(self, event): + if event.session_id != self.session_id: + logging.warn( + "ignoring event session(%s) current(%s)", + event.session_id, + self.session_id, + ) + return + if event.HasField("link_event"): logging.info("link event: %s", event) self.handle_link_event(event.link_event) @@ -176,7 +185,7 @@ class CoreClient: logging.warning("unknown link event: %s", event.message_type) def handle_node_event(self, event): - if event.source == "gui": + if event.source == GUI_SOURCE: return node_id = event.node.id x = event.node.position.x @@ -195,6 +204,11 @@ class CoreClient: def handle_throughputs(self, event): if event.session_id != self.session_id: + logging.warn( + "ignoring throughput event session(%s) current(%s)", + event.session_id, + self.session_id, + ) return logging.info("handling throughputs event: %s", event) self.app.canvas.throughput_draw.process_grpc_throughput_event( @@ -424,7 +438,7 @@ class CoreClient: def edit_node(self, core_node): try: self.client.edit_node( - self.session_id, core_node.id, core_node.position, source="gui" + self.session_id, core_node.id, core_node.position, source=GUI_SOURCE ) except grpc.RpcError as e: show_grpc_error(e) diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index d7a3094e..7b4756b1 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -107,7 +107,6 @@ def handle_session_event(event): name=event.name, data=event.data, time=event_time, - session_id=event.session, ) @@ -119,9 +118,6 @@ def handle_config_event(event): :return: configuration event :rtype: core.api.grpc.core_pb2.ConfigEvent """ - session_id = None - if event.session is not None: - session_id = int(event.session) return core_pb2.ConfigEvent( message_type=event.message_type, node_id=event.node, @@ -132,7 +128,6 @@ def handle_config_event(event): data_values=event.data_values, possible_values=event.possible_values, groups=event.groups, - session_id=session_id, interface=event.interface_number, network_id=event.network_id, opaque=event.opaque, @@ -150,7 +145,6 @@ def handle_exception_event(event): """ return core_pb2.ExceptionEvent( node_id=event.node, - session_id=int(event.session), level=event.level, source=event.source, date=event.date, @@ -175,7 +169,6 @@ def handle_file_event(event): number=event.number, type=event.type, source=event.source, - session_id=event.session, data=event.data, compressed_data=event.compressed_data, ) @@ -224,7 +217,7 @@ class EventStreamer: :return: grpc event, or None when invalid event or queue timeout :rtype: core.api.grpc.core_pb2.Event """ - event = core_pb2.Event() + event = core_pb2.Event(session_id=self.session.id) try: data = self.queue.get(timeout=1) if isinstance(data, NodeData): @@ -235,8 +228,6 @@ class EventStreamer: event.session_event.CopyFrom(handle_session_event(data)) elif isinstance(data, ConfigData): event.config_event.CopyFrom(handle_config_event(data)) - # TODO: remove when config events are fixed - event.config_event.session_id = self.session.id elif isinstance(data, ExceptionData): event.exception_event.CopyFrom(handle_exception_event(data)) elif isinstance(data, FileData): diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index ee099b31..ec7bf49c 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -301,6 +301,7 @@ message Event { ExceptionEvent exception_event = 5; FileEvent file_event = 6; } + int32 session_id = 7; } message NodeEvent { @@ -319,7 +320,6 @@ message SessionEvent { string name = 3; string data = 4; float time = 5; - int32 session_id = 6; } message ConfigEvent { @@ -333,20 +333,18 @@ message ConfigEvent { string bitmap = 8; string possible_values = 9; string groups = 10; - int32 session_id = 11; - int32 interface = 12; - int32 network_id = 13; - string opaque = 14; + int32 interface = 11; + int32 network_id = 12; + string opaque = 13; } message ExceptionEvent { int32 node_id = 1; - int32 session_id = 2; - ExceptionLevel.Enum level = 3; - string source = 4; - string date = 5; - string text = 6; - string opaque = 7; + ExceptionLevel.Enum level = 2; + string source = 3; + string date = 4; + string text = 5; + string opaque = 6; } message FileEvent { @@ -357,9 +355,8 @@ message FileEvent { int32 number = 5; string type = 6; string source = 7; - int32 session_id = 8; - string data = 9; - string compressed_data = 10; + string data = 8; + string compressed_data = 9; } message AddNodeRequest { diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 72e469f3..89e46f9b 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -990,6 +990,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id assert event_data.HasField("node_event") queue.put(event_data) @@ -1014,6 +1015,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id assert event_data.HasField("link_event") queue.put(event_data) @@ -1036,6 +1038,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id queue.put(event_data) # then @@ -1053,6 +1056,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id assert event_data.HasField("session_event") queue.put(event_data) @@ -1075,6 +1079,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id assert event_data.HasField("config_event") queue.put(event_data) @@ -1098,6 +1103,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id assert event_data.HasField("exception_event") queue.put(event_data) @@ -1120,6 +1126,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id assert event_data.HasField("file_event") queue.put(event_data) From 585d10dd28a9fb74d00d6c43f97819e7e2ab04ae Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 12:34:22 -0800 Subject: [PATCH 393/462] small tweaks to alert dialog --- coretk/coretk/dialogs/alerts.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/coretk/coretk/dialogs/alerts.py b/coretk/coretk/dialogs/alerts.py index 58f65933..89e78f02 100644 --- a/coretk/coretk/dialogs/alerts.py +++ b/coretk/coretk/dialogs/alerts.py @@ -24,10 +24,11 @@ class AlertsDialog(Dialog): self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) self.top.rowconfigure(1, weight=1) - row = 0 + frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) - frame.grid(row=row, column=0, sticky="nsew", pady=PADY) + frame.rowconfigure(0, weight=1) + frame.grid(sticky="nsew", pady=PADY) self.tree = ttk.Treeview( frame, columns=("time", "level", "session_id", "node", "source"), @@ -35,15 +36,15 @@ class AlertsDialog(Dialog): ) self.tree.grid(row=0, column=0, sticky="nsew") self.tree.column("time", stretch=tk.YES) - self.tree.heading("time", text="time", anchor="w") + self.tree.heading("time", text="Time") self.tree.column("level", stretch=tk.YES) - self.tree.heading("level", text="level", anchor="w") + self.tree.heading("level", text="Level") self.tree.column("session_id", stretch=tk.YES) - self.tree.heading("session_id", text="session id", anchor="w") + self.tree.heading("session_id", text="Session ID") self.tree.column("node", stretch=tk.YES) - self.tree.heading("node", text="node", anchor="w") + self.tree.heading("node", text="Node") self.tree.column("source", stretch=tk.YES) - self.tree.heading("source", text="source", anchor="w") + self.tree.heading("source", text="Source") self.tree.bind("<>", self.click_select) for alarm in self.app.statusbar.core_alarms: @@ -74,15 +75,13 @@ class AlertsDialog(Dialog): xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) xscrollbar.grid(row=1, sticky="ew") self.tree.configure(xscrollcommand=xscrollbar.set) - row = row + 1 self.text = CodeText(self.top) self.text.config(state=tk.DISABLED) - self.text.grid(row=row, column=0, sticky="nsew", pady=PADY) - row = row + 1 + self.text.grid(sticky="nsew", pady=PADY) frame = ttk.Frame(self.top) - frame.grid(row=row, column=0, sticky="nsew") + frame.grid(sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) From 713c42a64edfcb79c316d24d5e9b5b8621d3c02b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 13:11:23 -0800 Subject: [PATCH 394/462] added styling for listboxes, made use of listbox scroll where all other basic listboxes were being used --- coretk/coretk/dialogs/hooks.py | 7 ++++--- coretk/coretk/dialogs/observers.py | 19 ++++++------------- coretk/coretk/dialogs/servers.py | 18 ++++++------------ coretk/coretk/themes.py | 17 ++++++++++++++++- coretk/coretk/widgets.py | 2 ++ 5 files changed, 34 insertions(+), 29 deletions(-) diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index 40101823..9aeebecc 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -4,7 +4,7 @@ from tkinter import ttk from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog from coretk.themes import PADX, PADY -from coretk.widgets import CodeText +from coretk.widgets import CodeText, ListboxScroll class HookDialog(Dialog): @@ -96,8 +96,9 @@ class HooksDialog(Dialog): self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) - self.listbox = tk.Listbox(self.top) - self.listbox.grid(sticky="nsew", pady=PADY) + listbox_scroll = ListboxScroll(self.top) + listbox_scroll.grid(sticky="nsew", pady=PADY) + self.listbox = listbox_scroll.listbox self.listbox.bind("<>", self.select) for hook_file in self.app.core.hooks: self.listbox.insert(tk.END, hook_file) diff --git a/coretk/coretk/dialogs/observers.py b/coretk/coretk/dialogs/observers.py index f496f7ef..de857b76 100644 --- a/coretk/coretk/dialogs/observers.py +++ b/coretk/coretk/dialogs/observers.py @@ -4,6 +4,7 @@ from tkinter import ttk from coretk.coreclient import Observer from coretk.dialogs.dialog import Dialog from coretk.themes import PADX, PADY +from coretk.widgets import ListboxScroll class ObserverDialog(Dialog): @@ -27,24 +28,16 @@ class ObserverDialog(Dialog): self.draw_apply_buttons() def draw_listbox(self): - frame = ttk.Frame(self.top) - frame.grid(sticky="nsew", pady=PADY) - frame.columnconfigure(0, weight=1) - frame.rowconfigure(0, weight=1) - - scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL) - scrollbar.grid(row=0, column=1, sticky="ns") - - self.observers = tk.Listbox( - frame, selectmode=tk.SINGLE, yscrollcommand=scrollbar.set - ) + listbox_scroll = ListboxScroll(self.top) + listbox_scroll.grid(sticky="nsew", pady=PADY) + listbox_scroll.columnconfigure(0, weight=1) + listbox_scroll.rowconfigure(0, weight=1) + self.observers = listbox_scroll.listbox self.observers.grid(row=0, column=0, sticky="nsew") self.observers.bind("<>", self.handle_observer_change) for name in sorted(self.app.core.custom_observers): self.observers.insert(tk.END, name) - scrollbar.config(command=self.observers.yview) - def draw_form_fields(self): frame = ttk.Frame(self.top) frame.grid(sticky="ew", pady=PADY) diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index ef406ecb..c380c63d 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -4,6 +4,7 @@ from tkinter import ttk from coretk.coreclient import CoreServer from coretk.dialogs.dialog import Dialog from coretk.themes import FRAME_PAD, PADX, PADY +from coretk.widgets import ListboxScroll DEFAULT_NAME = "example" DEFAULT_ADDRESS = "127.0.0.1" @@ -32,25 +33,18 @@ class ServersDialog(Dialog): self.draw_apply_buttons() def draw_servers(self): - frame = ttk.Frame(self.top) - frame.grid(pady=PADY, sticky="nsew") - frame.columnconfigure(0, weight=1) - frame.rowconfigure(0, weight=1) + listbox_scroll = ListboxScroll(self.top) + listbox_scroll.grid(pady=PADY, sticky="nsew") + listbox_scroll.columnconfigure(0, weight=1) + listbox_scroll.rowconfigure(0, weight=1) - scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL) - scrollbar.grid(row=0, column=1, sticky="ns") - - self.servers = tk.Listbox( - frame, selectmode=tk.SINGLE, yscrollcommand=scrollbar.set - ) + self.servers = listbox_scroll.listbox self.servers.grid(row=0, column=0, sticky="nsew") self.servers.bind("<>", self.handle_server_change) for server in self.app.core.servers: self.servers.insert(tk.END, server) - scrollbar.config(command=self.servers.yview) - def draw_server_configuration(self): frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=FRAME_PAD) frame.grid(pady=PADY, sticky="ew") diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index 43b59ffd..685a8350 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -1,5 +1,6 @@ import logging import tkinter as tk +from tkinter import ttk THEME_DARK = "black" PADX = (0, 5) @@ -160,7 +161,21 @@ def update_menu(style, widget): if not abg: abg = bg widget.config( - background=bg, foreground=fg, activebackground=abg, activeforeground=fg + background=bg, foreground=fg, activebackground=abg, activeforeground=fg, bd=0 + ) + + +def update_listbox(widget): + style = ttk.Style() + bg = style.lookup(".", "background") + fg = style.lookup(".", "foreground") + widget.config( + background=bg, + foreground=fg, + highlightthickness=1, + highlightcolor="black", + highlightbackground="black", + bd=0, ) diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 349401c5..a00b590a 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -5,6 +5,7 @@ from tkinter import filedialog, font, ttk from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 +from coretk import themes from coretk.appconfig import ICONS_PATH from coretk.themes import FRAME_PAD, PADX, PADY @@ -184,6 +185,7 @@ class ListboxScroll(ttk.LabelFrame): self.listbox = tk.Listbox( self, selectmode=tk.SINGLE, yscrollcommand=self.scrollbar.set ) + themes.update_listbox(self.listbox) self.listbox.grid(row=0, column=0, sticky="nsew") self.scrollbar.config(command=self.listbox.yview) From 69494b600f31d2d3c411274a9e29904b08760ad0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 13:26:55 -0800 Subject: [PATCH 395/462] fixes for parsing grpc throughputs --- daemon/core/api/grpc/server.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 53c1d6d6..068d3eeb 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -27,7 +27,7 @@ from core.nodes.lxd import LxcNode from core.services.coreservices import ServiceManager _ONE_DAY_IN_SECONDS = 60 * 60 * 24 -_INTERFACE_REGEX = re.compile(r"[0-9a-fA-F]+") +_INTERFACE_REGEX = re.compile(r"veth(?P[0-9a-fA-F]+)") class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): @@ -479,7 +479,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): throughput = rx_kbps + tx_kbps if key.startswith("veth"): key = key.split(".") - node_id = int(_INTERFACE_REGEX.search(key[0]).group(), base=16) + node_id = _INTERFACE_REGEX.search(key[0]).group("node") + node_id = int(node_id, base=16) interface_id = int(key[1], base=16) session_id = int(key[2], base=16) if session.id != session_id: From c4f21e8a2ee12f7797594715f910b3721e16eb8a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 14:17:05 -0800 Subject: [PATCH 396/462] cleaned up listboxscroll widgets to not default to a labeleframe, updated code that really needed a lebelframe --- coretk/coretk/dialogs/customnodes.py | 22 ++++++++++----- coretk/coretk/dialogs/nodeservice.py | 16 ++++++++--- coretk/coretk/dialogs/serviceconfiguration.py | 28 ++++++++++++------- coretk/coretk/widgets.py | 2 +- 4 files changed, 46 insertions(+), 22 deletions(-) diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 6aabf417..7effd11a 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -24,13 +24,17 @@ class ServicesSelectDialog(Dialog): self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) - frame = ttk.Frame(self.top) + frame = ttk.LabelFrame(self.top) frame.grid(stick="nsew", pady=PADY) frame.rowconfigure(0, weight=1) for i in range(3): frame.columnconfigure(i, weight=1) - self.groups = ListboxScroll(frame, text="Groups", padding=FRAME_PAD) - self.groups.grid(row=0, column=0, sticky="nsew") + label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD) + label_frame.grid(row=0, column=0, sticky="nsew") + label_frame.rowconfigure(0, weight=1) + label_frame.columnconfigure(0, weight=1) + self.groups = ListboxScroll(label_frame) + self.groups.grid(sticky="nsew") for group in sorted(self.app.core.services): self.groups.listbox.insert(tk.END, group) self.groups.listbox.bind("<>", self.handle_group_change) @@ -45,8 +49,12 @@ class ServicesSelectDialog(Dialog): ) self.services.grid(row=0, column=1, sticky="nsew") - self.current = ListboxScroll(frame, text="Selected", padding=FRAME_PAD) - self.current.grid(row=0, column=2, sticky="nsew") + label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD) + label_frame.grid(row=0, column=2, sticky="nsew") + label_frame.rowconfigure(0, weight=1) + label_frame.columnconfigure(0, weight=1) + self.current = ListboxScroll(label_frame) + self.current.grid(sticky="nsew") for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) @@ -109,12 +117,12 @@ class CustomNodesDialog(Dialog): self.draw_buttons() def draw_node_config(self): - frame = ttk.Frame(self.top) + frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD) frame.grid(sticky="nsew", pady=PADY) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - self.nodes_list = ListboxScroll(frame, text="Nodes", padding=FRAME_PAD) + self.nodes_list = ListboxScroll(frame) self.nodes_list.grid(row=0, column=0, sticky="nsew", padx=PADX) self.nodes_list.listbox.bind("<>", self.handle_node_select) for name in sorted(self.app.core.custom_nodes): diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 2aa9c6ad..59b8865b 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -40,8 +40,12 @@ class NodeService(Dialog): frame.rowconfigure(0, weight=1) for i in range(3): frame.columnconfigure(i, weight=1) - self.groups = ListboxScroll(frame, text="Groups", padding=FRAME_PAD) - self.groups.grid(row=0, column=0, sticky="nsew") + label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD) + label_frame.grid(row=0, column=0, sticky="nsew") + label_frame.rowconfigure(0, weight=1) + label_frame.columnconfigure(0, weight=1) + self.groups = ListboxScroll(label_frame) + self.groups.grid(sticky="nsew") for group in sorted(self.app.core.services): self.groups.listbox.insert(tk.END, group) self.groups.listbox.bind("<>", self.handle_group_change) @@ -56,8 +60,12 @@ class NodeService(Dialog): ) self.services.grid(row=0, column=1, sticky="nsew") - self.current = ListboxScroll(frame, text="Selected", padding=FRAME_PAD) - self.current.grid(row=0, column=2, sticky="nsew") + label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD) + label_frame.grid(row=0, column=2, sticky="nsew") + label_frame.rowconfigure(0, weight=1) + label_frame.columnconfigure(0, weight=1) + self.current = ListboxScroll(label_frame) + self.current.grid(sticky="nsew") for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index a53c2aa1..0031ee31 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -216,32 +216,32 @@ class ServiceConfiguration(Dialog): tab.columnconfigure(0, weight=1) for i in range(3): tab.rowconfigure(i, weight=1) - self.notebook.add(tab, text="Startup/shutdown") + self.notebook.add(tab, text="Startup/Shutdown") # tab 3 for i in range(3): label_frame = None if i == 0: label_frame = ttk.LabelFrame( - tab, text="Startup commands", padding=FRAME_PAD + tab, text="Startup Commands", padding=FRAME_PAD ) commands = self.startup_commands elif i == 1: label_frame = ttk.LabelFrame( - tab, text="Shutdown commands", padding=FRAME_PAD + tab, text="Shutdown Commands", padding=FRAME_PAD ) commands = self.shutdown_commands elif i == 2: label_frame = ttk.LabelFrame( - tab, text="Validation commands", padding=FRAME_PAD + tab, text="Validation Commands", padding=FRAME_PAD ) commands = self.validation_commands label_frame.columnconfigure(0, weight=1) label_frame.rowconfigure(1, weight=1) - label_frame.grid(row=i, column=0, sticky="nsew") + label_frame.grid(row=i, column=0, sticky="nsew", pady=PADY) frame = ttk.Frame(label_frame) - frame.grid(row=0, column=0, sticky="nsew") + frame.grid(row=0, column=0, sticky="nsew", pady=PADY) frame.columnconfigure(0, weight=1) entry = ttk.Entry(frame, textvariable=tk.StringVar()) entry.grid(row=0, column=0, stick="ew", padx=PADX) @@ -251,7 +251,7 @@ class ServiceConfiguration(Dialog): button = ttk.Button(frame, image=self.editdelete_img) button.grid(row=0, column=2, sticky="ew") button.bind("", self.delete_command) - listbox_scroll = ListboxScroll(label_frame, borderwidth=0) + listbox_scroll = ListboxScroll(label_frame) listbox_scroll.listbox.bind("<>", self.update_entry) for command in commands: listbox_scroll.listbox.insert("end", command) @@ -303,13 +303,21 @@ class ServiceConfiguration(Dialog): ) self.validation_period_entry.grid(row=2, column=1, sticky="ew") - listbox_scroll = ListboxScroll(tab, text="Executables", padding=FRAME_PAD) - listbox_scroll.grid(sticky="nsew", pady=PADY) + label_frame = ttk.LabelFrame(tab, text="Executables", padding=FRAME_PAD) + label_frame.grid(sticky="nsew", pady=PADY) + label_frame.columnconfigure(0, weight=1) + label_frame.rowconfigure(0, weight=1) + listbox_scroll = ListboxScroll(label_frame) + listbox_scroll.grid(sticky="nsew") tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1) for executable in self.executables: listbox_scroll.listbox.insert("end", executable) - listbox_scroll = ListboxScroll(tab, text="Dependencies", padding=FRAME_PAD) + label_frame = ttk.LabelFrame(tab, text="Dependencies", padding=FRAME_PAD) + label_frame.grid(sticky="nsew", pady=PADY) + label_frame.columnconfigure(0, weight=1) + label_frame.rowconfigure(0, weight=1) + listbox_scroll = ListboxScroll(label_frame) listbox_scroll.grid(sticky="nsew") tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1) for dependency in self.dependencies: diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index a00b590a..feed7958 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -175,7 +175,7 @@ class ConfigFrame(FrameScroll): return {x: self.config[x].value for x in self.config} -class ListboxScroll(ttk.LabelFrame): +class ListboxScroll(ttk.Frame): def __init__(self, master=None, **kw): super().__init__(master, **kw) self.columnconfigure(0, weight=1) From dcdcb6a711b308dd882236aad40c53d2e83a4f0e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 14:21:30 -0800 Subject: [PATCH 397/462] updated theme style function names, removed unused function --- coretk/coretk/graph/node.py | 2 +- coretk/coretk/themes.py | 13 +++---------- coretk/coretk/widgets.py | 2 +- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 93b6c390..105e21b7 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -168,7 +168,7 @@ class CanvasNode: is_wlan = self.core_node.type == NodeType.WIRELESS_LAN is_emane = self.core_node.type == NodeType.EMANE context = tk.Menu(self.canvas) - themes.update_menu(self.app.style, context) + themes.style_menu(self.app.style, context) if self.app.core.is_runtime(): context.add_command(label="Configure", command=self.show_config) if NodeUtils.is_container_node(self.core_node.type): diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index 685a8350..55b5732d 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -1,4 +1,3 @@ -import logging import tkinter as tk from tkinter import ttk @@ -142,19 +141,13 @@ def load(style): ) -def update_bg(style, event): - logging.info("updating background: %s", event.widget) - bg = style.lookup(".", "background") - event.widget.config(background=bg) - - def theme_change_menu(style, event): if not isinstance(event.widget, tk.Menu): return - update_menu(style, event.widget) + style_menu(style, event.widget) -def update_menu(style, widget): +def style_menu(style, widget): bg = style.lookup(".", "background") fg = style.lookup(".", "foreground") abg = style.lookup(".", "lightcolor") @@ -165,7 +158,7 @@ def update_menu(style, widget): ) -def update_listbox(widget): +def style_listbox(widget): style = ttk.Style() bg = style.lookup(".", "background") fg = style.lookup(".", "foreground") diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index feed7958..9ca96c0e 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -185,7 +185,7 @@ class ListboxScroll(ttk.Frame): self.listbox = tk.Listbox( self, selectmode=tk.SINGLE, yscrollcommand=self.scrollbar.set ) - themes.update_listbox(self.listbox) + themes.style_listbox(self.listbox) self.listbox.grid(row=0, column=0, sticky="nsew") self.scrollbar.config(command=self.listbox.yview) From 5dbd34f230eb20c93d4b3e6a23cc3cfc7d7f87bb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 14:30:38 -0800 Subject: [PATCH 398/462] cleanup for theming widgets --- coretk/coretk/app.py | 7 ++----- coretk/coretk/graph/node.py | 2 +- coretk/coretk/themes.py | 17 +++++++++++------ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 7c2562a9..c8d4e597 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,6 +1,5 @@ import logging import tkinter as tk -from functools import partial from tkinter import ttk from coretk import appconfig, themes @@ -42,11 +41,9 @@ class Application(tk.Frame): def setup_theme(self): themes.load(self.style) + self.master.bind_class("Menu", "<>", themes.theme_change_menu) + self.master.bind("<>", themes.theme_change) self.style.theme_use(self.guiconfig["preferences"]["theme"]) - func = partial(themes.theme_change_menu, self.style) - self.master.bind_class("Menu", "<>", func) - func = partial(themes.theme_change, self.style) - self.master.bind("<>", func) def setup_app(self): self.master.title("CORE") diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 105e21b7..e9f59882 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -168,7 +168,7 @@ class CanvasNode: is_wlan = self.core_node.type == NodeType.WIRELESS_LAN is_emane = self.core_node.type == NodeType.EMANE context = tk.Menu(self.canvas) - themes.style_menu(self.app.style, context) + themes.style_menu(context) if self.app.core.is_runtime(): context.add_command(label="Configure", command=self.show_config) if NodeUtils.is_container_node(self.core_node.type): diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index 55b5732d..66565f8d 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -141,13 +141,14 @@ def load(style): ) -def theme_change_menu(style, event): +def theme_change_menu(event): if not isinstance(event.widget, tk.Menu): return - style_menu(style, event.widget) + style_menu(event.widget) -def style_menu(style, widget): +def style_menu(widget): + style = ttk.Style() bg = style.lookup(".", "background") fg = style.lookup(".", "foreground") abg = style.lookup(".", "lightcolor") @@ -162,17 +163,21 @@ def style_listbox(widget): style = ttk.Style() bg = style.lookup(".", "background") fg = style.lookup(".", "foreground") + bc = style.lookup(".", "bordercolor") + if not bc: + bc = "black" widget.config( background=bg, foreground=fg, highlightthickness=1, - highlightcolor="black", - highlightbackground="black", + highlightcolor=bc, + highlightbackground=bc, bd=0, ) -def theme_change(style, event): +def theme_change(event): + style = ttk.Style() style.configure(Styles.picker_button, font=("TkDefaultFont", 8, "normal")) style.configure( Styles.green_alert, From 084aedf3d251846cdbc4afe59dd61bf02757e1e2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 14:34:37 -0800 Subject: [PATCH 399/462] removed services button from node config dialog, limited to context menu for now --- coretk/coretk/dialogs/nodeconfig.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index c71a1b34..cfec3b24 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -5,7 +5,6 @@ from tkinter import ttk from coretk import nodeutils from coretk.dialogs.dialog import Dialog -from coretk.dialogs.nodeservice import NodeService from coretk.images import Images from coretk.nodeutils import NodeUtils from coretk.themes import FRAME_PAD, PADX, PADY @@ -130,10 +129,6 @@ class NodeConfigDialog(Dialog): combobox.grid(row=row, column=1, sticky="ew") row += 1 - # services - button = ttk.Button(self.top, text="Services", command=self.click_services) - button.grid(sticky="ew", pady=PADY) - # interfaces if self.canvas_node.interfaces: self.draw_interfaces() @@ -193,10 +188,6 @@ class NodeConfigDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def click_services(self): - dialog = NodeService(self, self.app, self.canvas_node) - dialog.show() - def click_icon(self): file_path = image_chooser(self) if file_path: From b7139996e174c65eebd996cf50adc2355f7766be Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 16 Dec 2019 14:55:05 -0800 Subject: [PATCH 400/462] have a working color picker --- coretk/coretk/dialogs/colorpicker.py | 90 +++++++++++++++++++++------- coretk/coretk/dialogs/shapemod.py | 10 ++-- 2 files changed, 75 insertions(+), 25 deletions(-) diff --git a/coretk/coretk/dialogs/colorpicker.py b/coretk/coretk/dialogs/colorpicker.py index 305be7d4..5b3a4fc4 100644 --- a/coretk/coretk/dialogs/colorpicker.py +++ b/coretk/coretk/dialogs/colorpicker.py @@ -15,6 +15,9 @@ class ColorPicker(Dialog): self.blue_entry = None self.green_entry = None self.hex_entry = None + self.red_label = None + self.green_label = None + self.blue_label = None self.display = None self.color = initcolor red, green, blue = self.get_rgb(initcolor) @@ -25,16 +28,21 @@ class ColorPicker(Dialog): self.red_scale = tk.IntVar(value=red) self.green_scale = tk.IntVar(value=green) self.blue_scale = tk.IntVar(value=blue) - - def askcolor(self): self.draw() self.set_bindings() + + def askcolor(self): self.show() return self.color def draw(self): + self.top.columnconfigure(0, weight=1) # rgb frames - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=4) + frame.columnconfigure(3, weight=2) label = ttk.Label(frame, text="R: ") label.grid(row=0, column=0) self.red_entry = ttk.Entry( @@ -50,15 +58,23 @@ class ColorPicker(Dialog): from_=0, to=255, value=0, - length=200, + # length=200, orient=tk.HORIZONTAL, variable=self.red_scale, command=lambda x: self.scale_callback(self.red_scale, self.red), ) scale.grid(row=0, column=2, sticky="nsew") + self.red_label = ttk.Label( + frame, background="#%02x%02x%02x" % (self.red.get(), 0, 0) + ) + self.red_label.grid(row=0, column=3, sticky="nsew") frame.grid(row=0, column=0, sticky="nsew") - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=4) + frame.columnconfigure(3, weight=2) label = ttk.Label(frame, text="G: ") label.grid(row=0, column=0) self.green_entry = ttk.Entry( @@ -74,15 +90,23 @@ class ColorPicker(Dialog): from_=0, to=255, value=0, - length=200, + # length=200, orient=tk.HORIZONTAL, variable=self.green_scale, command=lambda x: self.scale_callback(self.green_scale, self.green), ) scale.grid(row=0, column=2, sticky="nsew") - frame.grid(row=1, column=0) + self.green_label = ttk.Label( + frame, background="#%02x%02x%02x" % (0, self.green.get(), 0) + ) + self.green_label.grid(row=0, column=3, sticky="nsew") + frame.grid(row=1, column=0, sticky="nsew") - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=4) + frame.columnconfigure(3, weight=2) label = ttk.Label(frame, text="B: ") label.grid(row=0, column=0) self.blue_entry = ttk.Entry( @@ -98,16 +122,20 @@ class ColorPicker(Dialog): from_=0, to=255, value=0, - length=200, + # length=200, orient=tk.HORIZONTAL, variable=self.blue_scale, command=lambda x: self.scale_callback(self.blue_scale, self.blue), ) scale.grid(row=0, column=2, sticky="nsew") + self.blue_label = ttk.Label( + frame, background="#%02x%02x%02x" % (0, 0, self.blue.get()) + ) + self.blue_label.grid(row=0, column=3, sticky="nsew") frame.grid(row=2, column=0, sticky="nsew") # hex code and color display - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) label = ttk.Label(frame, text="Selection: ") label.grid(row=0, column=0, sticky="nsew") @@ -118,12 +146,12 @@ class ColorPicker(Dialog): validatecommand=(self.app.validation.hex, "%P"), ) self.hex_entry.grid(row=1, column=0, sticky="nsew") - self.display = ttk.Label(frame, background=self.color) - self.display.grid(row=2, column=0, sticky="nsew") + self.display = tk.Frame(frame, background=self.color, width=100, height=100) + self.display.grid(row=2, column=0) frame.grid(row=3, column=0, sticky="nsew") # button frame - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="OK", command=self.button_ok) @@ -148,6 +176,12 @@ class ColorPicker(Dialog): self.destroy() def get_hex(self): + """ + convert current RGB values into hex color + + :rtype: str + :return: hex color + """ red = self.red_entry.get() blue = self.blue_entry.get() green = self.green_entry.get() @@ -166,6 +200,7 @@ class ColorPicker(Dialog): hex_code = "#%02x%02x%02x" % (int(red), int(green), int(blue)) self.hex.set(hex_code) self.display.config(background=hex_code) + self.set_label(red, green, blue) elif self.focus == "hex": hex_code = self.hex.get() if len(hex_code) == 4 or len(hex_code) == 7: @@ -175,23 +210,36 @@ class ColorPicker(Dialog): self.set_entry(red, green, blue) self.set_scale(red, green, blue) self.display.config(background=hex_code) + self.set_label(red, green, blue) def scale_callback(self, var, color_var): color_var.set(var.get()) self.focus = "rgb" self.update_color() - def set_scale(self, r, g, b): - self.red_scale.set(r) - self.green_scale.set(g) - self.blue_scale.set(b) + def set_scale(self, red, green, blue): + self.red_scale.set(red) + self.green_scale.set(green) + self.blue_scale.set(blue) - def set_entry(self, r, g, b): - self.red.set(r) - self.green.set(g) - self.blue.set(b) + def set_entry(self, red, green, blue): + self.red.set(red) + self.green.set(green) + self.blue.set(blue) + + def set_label(self, red, green, blue): + self.red_label.configure(background="#%02x%02x%02x" % (int(red), 0, 0)) + self.green_label.configure(background="#%02x%02x%02x" % (0, int(green), 0)) + self.blue_label.configure(background="#%02x%02x%02x" % (0, 0, int(blue))) def get_rgb(self, hex_code): + """ + convert a valid hex code to RGB values + + :param string hex_code: color in hex + :rtype: tuple(int, int, int) + :return: the RGB values + """ if len(hex_code) == 4: red = hex_code[1] green = hex_code[2] diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index 27bc719f..e915e3d9 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -2,7 +2,7 @@ shape input dialog """ import tkinter as tk -from tkinter import colorchooser, font, ttk +from tkinter import font, ttk from coretk.dialogs.colorpicker import ColorPicker from coretk.dialogs.dialog import Dialog @@ -147,9 +147,11 @@ class ShapeDialog(Dialog): self.fill.config(background=color, text=color) def choose_border_color(self): - color = colorchooser.askcolor(color=self.border_color) - self.border_color = color[1] - self.border.config(background=color[1], text=color[1]) + color_picker = ColorPicker(self, self.app, self.border_color) + color = color_picker.askcolor() + # color = colorchooser.askcolor(color=self.border_color) + self.border_color = color + self.border.config(background=color, text=color) def cancel(self): self.shape.delete() From 0082d61517604fa3cbf45a4bb9b28af4616f1e8c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 14:55:54 -0800 Subject: [PATCH 401/462] updates to allow wlan config changes during runtime --- coretk/coretk/dialogs/wlanconfig.py | 5 ++++- coretk/coretk/graph/graph.py | 14 ++++++++++---- coretk/coretk/toolbar.py | 2 ++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index aed411b8..20966d2b 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -58,6 +58,9 @@ class WlanConfigDialog(Dialog): :return: nothing """ - self.config_frame.parse_config() + config = self.config_frame.parse_config() self.app.core.wlan_configs[self.node.id] = self.config + if self.app.core.is_runtime(): + session_id = self.app.core.session_id + self.app.core.client.set_wlan_config(session_id, self.node.id, config) self.destroy() diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index a2d82dc4..acce0e28 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -87,6 +87,9 @@ class CanvasGraph(tk.Canvas): :param core.api.grpc.core_pb2.Session session: session to draw :return: nothing """ + # hide context + self.hide_context() + # delete any existing drawn items for tag in tags.COMPONENT_TAGS: self.delete(tag) @@ -122,6 +125,11 @@ class CanvasGraph(tk.Canvas): self.bind("", lambda e: self.scan_mark(e.x, e.y)) self.bind("", lambda e: self.scan_dragto(e.x, e.y, gain=1)) + def hide_context(self): + if self.context: + self.context.unpost() + self.context = None + def get_actual_coords(self, x, y): actual_x = (x - self.offset[0]) / self.ratio actual_y = (y - self.offset[1]) / self.ratio @@ -294,8 +302,7 @@ class CanvasGraph(tk.Canvas): return if self.context: - self.context.unpost() - self.context = None + self.hide_context() else: if self.mode == GraphMode.ANNOTATION: self.focus_set() @@ -592,8 +599,7 @@ class CanvasGraph(tk.Canvas): self.context = canvas_node.create_context() self.context.post(event.x_root, event.y_root) else: - self.context.unpost() - self.context = None + self.hide_context() def press_delete(self, event): """ diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index df183494..323c5d67 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -222,6 +222,7 @@ class Toolbar(ttk.Frame): :return: nothing """ + self.app.canvas.hide_context() self.app.statusbar.core_alarms.clear() self.app.statusbar.progress_bar.start(5) self.app.canvas.mode = GraphMode.SELECT @@ -385,6 +386,7 @@ class Toolbar(ttk.Frame): :return: nothing """ + self.app.canvas.hide_context() self.app.statusbar.progress_bar.start(5) thread = threading.Thread(target=self.app.core.stop_session) thread.start() From 921e002997d9a7f9ae43251a0598a80966ab9e2e Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 16 Dec 2019 16:05:15 -0800 Subject: [PATCH 402/462] small fix for color picker --- coretk/coretk/dialogs/colorpicker.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/coretk/coretk/dialogs/colorpicker.py b/coretk/coretk/dialogs/colorpicker.py index 5b3a4fc4..9734468d 100644 --- a/coretk/coretk/dialogs/colorpicker.py +++ b/coretk/coretk/dialogs/colorpicker.py @@ -41,7 +41,7 @@ class ColorPicker(Dialog): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=4) + frame.columnconfigure(2, weight=6) frame.columnconfigure(3, weight=2) label = ttk.Label(frame, text="R: ") label.grid(row=0, column=0) @@ -65,7 +65,7 @@ class ColorPicker(Dialog): ) scale.grid(row=0, column=2, sticky="nsew") self.red_label = ttk.Label( - frame, background="#%02x%02x%02x" % (self.red.get(), 0, 0) + frame, background="#%02x%02x%02x" % (self.red.get(), 0, 0), width=5 ) self.red_label.grid(row=0, column=3, sticky="nsew") frame.grid(row=0, column=0, sticky="nsew") @@ -73,7 +73,7 @@ class ColorPicker(Dialog): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=4) + frame.columnconfigure(2, weight=6) frame.columnconfigure(3, weight=2) label = ttk.Label(frame, text="G: ") label.grid(row=0, column=0) @@ -97,7 +97,7 @@ class ColorPicker(Dialog): ) scale.grid(row=0, column=2, sticky="nsew") self.green_label = ttk.Label( - frame, background="#%02x%02x%02x" % (0, self.green.get(), 0) + frame, background="#%02x%02x%02x" % (0, self.green.get(), 0), width=5 ) self.green_label.grid(row=0, column=3, sticky="nsew") frame.grid(row=1, column=0, sticky="nsew") @@ -105,7 +105,7 @@ class ColorPicker(Dialog): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=4) + frame.columnconfigure(2, weight=6) frame.columnconfigure(3, weight=2) label = ttk.Label(frame, text="B: ") label.grid(row=0, column=0) @@ -129,7 +129,7 @@ class ColorPicker(Dialog): ) scale.grid(row=0, column=2, sticky="nsew") self.blue_label = ttk.Label( - frame, background="#%02x%02x%02x" % (0, 0, self.blue.get()) + frame, background="#%02x%02x%02x" % (0, 0, self.blue.get()), width=5 ) self.blue_label.grid(row=0, column=3, sticky="nsew") frame.grid(row=2, column=0, sticky="nsew") From cb1322a28a143072a9b8efaa5a1bf8f128c66c9f Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 16 Dec 2019 16:06:21 -0800 Subject: [PATCH 403/462] small fix for color picker --- coretk/coretk/dialogs/shapemod.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index e915e3d9..62fed9f9 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -142,14 +142,12 @@ class ShapeDialog(Dialog): def choose_fill_color(self): color_picker = ColorPicker(self, self.app, self.fill_color) color = color_picker.askcolor() - # color = colorchooser.askcolor(color=self.fill_color) self.fill_color = color self.fill.config(background=color, text=color) def choose_border_color(self): color_picker = ColorPicker(self, self.app, self.border_color) color = color_picker.askcolor() - # color = colorchooser.askcolor(color=self.border_color) self.border_color = color self.border.config(background=color, text=color) From 042a56ecb3db8b84a527b70ede6c288255b56d56 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 20:57:46 -0800 Subject: [PATCH 404/462] updated node dialog to display interfaces in notebook tabs, added emane interface configuration for nodes connected to an emane network --- coretk/coretk/dialogs/nodeconfig.py | 66 +++++++++++++++++++---------- coretk/coretk/graph/node.py | 17 ++++++++ 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index cfec3b24..1137d6b9 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -5,10 +5,11 @@ from tkinter import ttk from coretk import nodeutils from coretk.dialogs.dialog import Dialog +from coretk.dialogs.emaneconfig import EmaneModelDialog from coretk.images import Images from coretk.nodeutils import NodeUtils from coretk.themes import FRAME_PAD, PADX, PADY -from coretk.widgets import FrameScroll, image_chooser +from coretk.widgets import image_chooser def mac_auto(is_auto, entry): @@ -137,42 +138,57 @@ class NodeConfigDialog(Dialog): self.draw_buttons() def draw_interfaces(self): - scroll = FrameScroll(self.top, self.app, text="Interfaces") - scroll.grid(sticky="nsew") - scroll.frame.columnconfigure(0, weight=1) - scroll.frame.rowconfigure(0, weight=1) + notebook = ttk.Notebook(self.top) + notebook.grid(sticky="nsew", pady=PADY) + self.top.rowconfigure(notebook.grid_info()["row"], weight=1) + for interface in self.canvas_node.interfaces: logging.info("interface: %s", interface) - frame = ttk.LabelFrame(scroll.frame, text=interface.name, padding=FRAME_PAD) - frame.grid(sticky="ew", pady=PADY) - frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=1) + tab = ttk.Frame(notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew", pady=PADY) + tab.columnconfigure(1, weight=1) + tab.columnconfigure(2, weight=1) + notebook.add(tab, text=interface.name) - label = ttk.Label(frame, text="MAC") - label.grid(row=0, column=0, padx=PADX, pady=PADY) + row = 0 + emane_node = self.canvas_node.has_emane_link(interface.id) + if emane_node: + emane_model = emane_node.emane.split("_")[1] + button = ttk.Button( + tab, + text=f"Configure EMANE {emane_model}", + command=lambda: self.click_emane_config(emane_model, interface.id), + ) + button.grid(row=row, sticky="ew", columnspan=3, pady=PADY) + row += 1 + + label = ttk.Label(tab, text="MAC") + label.grid(row=row, column=0, padx=PADX, pady=PADY) is_auto = tk.BooleanVar(value=True) - checkbutton = ttk.Checkbutton(frame, text="Auto?", variable=is_auto) + checkbutton = ttk.Checkbutton(tab, text="Auto?", variable=is_auto) checkbutton.var = is_auto - checkbutton.grid(row=0, column=1, padx=PADX) + checkbutton.grid(row=row, column=1, padx=PADX) mac = tk.StringVar(value=interface.mac) - entry = ttk.Entry(frame, textvariable=mac, state=tk.DISABLED) - entry.grid(row=0, column=2, sticky="ew") + entry = ttk.Entry(tab, textvariable=mac, state=tk.DISABLED) + entry.grid(row=row, column=2, sticky="ew") func = partial(mac_auto, is_auto, entry) checkbutton.config(command=func) + row += 1 - label = ttk.Label(frame, text="IPv4") - label.grid(row=1, column=0, padx=PADX, pady=PADY) + 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}") - entry = ttk.Entry(frame, textvariable=ip4) + entry = ttk.Entry(tab, textvariable=ip4) entry.bind("", self.app.validation.ip_focus_out) - entry.grid(row=1, column=1, columnspan=2, sticky="ew") + entry.grid(row=row, column=1, columnspan=2, sticky="ew") + row += 1 - label = ttk.Label(frame, text="IPv6") - label.grid(row=2, column=0, padx=PADX, pady=PADY) + 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}") - entry = ttk.Entry(frame, textvariable=ip6) + entry = ttk.Entry(tab, textvariable=ip6) entry.bind("", self.app.validation.ip_focus_out) - entry.grid(row=2, column=1, columnspan=2, sticky="ew") + entry.grid(row=row, column=1, columnspan=2, sticky="ew") self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6) @@ -188,6 +204,10 @@ class NodeConfigDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") + def click_emane_config(self, emane_model, interface_id): + dialog = EmaneModelDialog(self, self.app, self.node, emane_model, interface_id) + dialog.show() + def click_icon(self): file_path = image_chooser(self) if file_path: diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index e9f59882..4836d2d2 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -246,6 +246,23 @@ class CanvasNode: dialog = NodeService(self.app.master, self.app, self) dialog.show() + def has_emane_link(self, interface_id): + result = None + for edge in self.edges: + if self.id == edge.src: + other_id = edge.dst + edge_interface_id = edge.src_interface.id + else: + other_id = edge.src + edge_interface_id = edge.dst_interface.id + if edge_interface_id != interface_id: + continue + other_node = self.canvas.nodes[other_id] + if other_node.core_node.type == NodeType.EMANE: + result = other_node.core_node + break + return result + def wireless_link_selected(self): self.canvas.context = None for canvas_nid in [ From 02695f16721707a1267dddeeb0e51855e885e2d0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 17 Dec 2019 10:01:25 -0800 Subject: [PATCH 405/462] revamped codetext widget to use ttk scrollbars for better theme matching, slight adjustments to service config dialog layout --- coretk/coretk/dialogs/about.py | 8 +++--- coretk/coretk/dialogs/alerts.py | 28 +++++++++---------- coretk/coretk/dialogs/hooks.py | 14 +++++----- coretk/coretk/dialogs/serviceconfiguration.py | 26 +++++++++-------- coretk/coretk/widgets.py | 15 ++++++---- 5 files changed, 49 insertions(+), 42 deletions(-) diff --git a/coretk/coretk/dialogs/about.py b/coretk/coretk/dialogs/about.py index 28ddea33..9e3ff7a9 100644 --- a/coretk/coretk/dialogs/about.py +++ b/coretk/coretk/dialogs/about.py @@ -38,7 +38,7 @@ class AboutDialog(Dialog): self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) - text = CodeText(self.top) - text.insert("1.0", LICENSE) - text.config(state=tk.DISABLED) - text.grid(sticky="nsew") + codetext = CodeText(self.top) + codetext.text.insert("1.0", LICENSE) + codetext.text.config(state=tk.DISABLED) + codetext.grid(sticky="nsew") diff --git a/coretk/coretk/dialogs/alerts.py b/coretk/coretk/dialogs/alerts.py index 89e78f02..e782547f 100644 --- a/coretk/coretk/dialogs/alerts.py +++ b/coretk/coretk/dialogs/alerts.py @@ -17,7 +17,7 @@ class AlertsDialog(Dialog): super().__init__(master, app, "Alerts", modal=True) self.app = app self.tree = None - self.text = None + self.codetext = None self.draw() def draw(self): @@ -76,9 +76,9 @@ class AlertsDialog(Dialog): xscrollbar.grid(row=1, sticky="ew") self.tree.configure(xscrollcommand=xscrollbar.set) - self.text = CodeText(self.top) - self.text.config(state=tk.DISABLED) - self.text.grid(sticky="nsew", pady=PADY) + self.codetext = CodeText(self.top) + self.codetext.text.config(state=tk.DISABLED) + self.codetext.grid(sticky="nsew", pady=PADY) frame = ttk.Frame(self.top) frame.grid(sticky="ew") @@ -96,7 +96,7 @@ class AlertsDialog(Dialog): button.grid(row=0, column=3, sticky="ew") def reset_alerts(self): - self.text.delete("1.0", tk.END) + self.codetext.text.delete("1.0", tk.END) for item in self.tree.get_children(): self.tree.delete(item) self.app.statusbar.core_alarms.clear() @@ -137,8 +137,8 @@ class AlertsDialog(Dialog): text = text + "node created" except RpcError: text = text + "node not created" - self.text.delete("1.0", "end") - self.text.insert("1.0", text) + self.codetext.text.delete("1.0", "end") + self.codetext.text.insert("1.0", text) class DaemonLog(Dialog): @@ -155,8 +155,8 @@ class DaemonLog(Dialog): frame.grid(row=0, column=0, sticky="ew", pady=PADY) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=9) - label = ttk.Label(frame, text="File: ") - label.grid(row=0, column=0) + label = ttk.Label(frame, text="File", anchor="w") + label.grid(row=0, column=0, sticky="ew") entry = ttk.Entry(frame, textvariable=self.path, state="disabled") entry.grid(row=0, column=1, sticky="ew") try: @@ -164,8 +164,8 @@ class DaemonLog(Dialog): log = file.readlines() except FileNotFoundError: log = "Log file not found" - text = CodeText(self.top) - text.insert("1.0", log) - text.see("end") - text.config(state=tk.DISABLED) - text.grid(row=1, column=0, sticky="nsew") + codetext = CodeText(self.top) + codetext.text.insert("1.0", log) + codetext.text.see("end") + codetext.text.config(state=tk.DISABLED) + codetext.grid(row=1, column=0, sticky="nsew") diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index 9aeebecc..37503d66 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -11,7 +11,7 @@ class HookDialog(Dialog): def __init__(self, master, app): super().__init__(master, app, "Hook", modal=True) self.name = tk.StringVar() - self.data = None + self.codetext = None self.hook = core_pb2.Hook() self.state = tk.StringVar() self.draw() @@ -41,8 +41,8 @@ class HookDialog(Dialog): combobox.bind("<>", self.state_change) # data - self.data = CodeText(self.top) - self.data.insert( + self.codetext = CodeText(self.top) + self.codetext.text.insert( 1.0, ( "#!/bin/sh\n" @@ -50,7 +50,7 @@ class HookDialog(Dialog): "# specified state\n" ), ) - self.data.grid(sticky="nsew") + self.codetext.grid(sticky="nsew", pady=PADY) # button row frame = ttk.Frame(self.top) @@ -69,13 +69,13 @@ class HookDialog(Dialog): def set(self, hook): self.hook = hook self.name.set(hook.file) - self.data.delete(1.0, tk.END) - self.data.insert(tk.END, hook.data) + self.codetext.text.delete(1.0, tk.END) + self.codetext.text.insert(tk.END, hook.data) state_name = core_pb2.SessionState.Enum.Name(hook.state) self.state.set(state_name) def save(self): - data = self.data.get("1.0", tk.END).strip() + data = self.codetext.text.get("1.0", tk.END).strip() state_value = core_pb2.SessionState.Enum.Value(self.state.get()) self.hook.file = self.name.get() self.hook.data = data diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 0031ee31..53aca1b3 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -110,7 +110,7 @@ class ServiceConfiguration(Dialog): # draw notebook self.notebook = ttk.Notebook(self.top) - self.notebook.grid(sticky="nsew") + self.notebook.grid(sticky="nsew", pady=PADY) self.draw_tab_files() self.draw_tab_directories() self.draw_tab_startstop() @@ -192,11 +192,13 @@ class ServiceConfiguration(Dialog): tab.rowconfigure(self.service_file_data.grid_info()["row"], weight=1) if len(self.filenames) > 0: self.filename_combobox.current(0) - self.service_file_data.delete(1.0, "end") - self.service_file_data.insert( + self.service_file_data.text.delete(1.0, "end") + self.service_file_data.text.insert( "end", self.temp_service_files[self.filenames[0]] ) - self.service_file_data.bind("", self.update_temp_service_file_data) + self.service_file_data.text.bind( + "", self.update_temp_service_file_data + ) def draw_tab_directories(self): tab = ttk.Frame(self.notebook, padding=FRAME_PAD) @@ -275,14 +277,14 @@ class ServiceConfiguration(Dialog): frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Validation Time") - label.grid(row=0, column=0, sticky="w") + label.grid(row=0, column=0, sticky="w", padx=PADX) self.validation_time_entry = ttk.Entry(frame) self.validation_time_entry.insert("end", self.validation_time) self.validation_time_entry.config(state=tk.DISABLED) - self.validation_time_entry.grid(row=0, column=1, sticky="ew") + self.validation_time_entry.grid(row=0, column=1, sticky="ew", pady=PADY) label = ttk.Label(frame, text="Validation Mode") - label.grid(row=1, column=0, sticky="w") + label.grid(row=1, column=0, sticky="w", padx=PADX) if self.validation_mode == core_pb2.ServiceValidationMode.BLOCKING: mode = "BLOCKING" elif self.validation_mode == core_pb2.ServiceValidationMode.NON_BLOCKING: @@ -294,14 +296,14 @@ class ServiceConfiguration(Dialog): ) self.validation_mode_entry.insert("end", mode) self.validation_mode_entry.config(state=tk.DISABLED) - self.validation_mode_entry.grid(row=1, column=1, sticky="ew") + self.validation_mode_entry.grid(row=1, column=1, sticky="ew", pady=PADY) label = ttk.Label(frame, text="Validation Period") - label.grid(row=2, column=0, sticky="w") + label.grid(row=2, column=0, sticky="w", padx=PADX) self.validation_period_entry = ttk.Entry( frame, state=tk.DISABLED, textvariable=tk.StringVar() ) - self.validation_period_entry.grid(row=2, column=1, sticky="ew") + self.validation_period_entry.grid(row=2, column=1, sticky="ew", pady=PADY) label_frame = ttk.LabelFrame(tab, text="Executables", padding=FRAME_PAD) label_frame.grid(sticky="nsew", pady=PADY) @@ -429,8 +431,8 @@ class ServiceConfiguration(Dialog): def display_service_file_data(self, event): combobox = event.widget filename = combobox.get() - self.service_file_data.delete(1.0, "end") - self.service_file_data.insert("end", self.temp_service_files[filename]) + 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): scrolledtext = event.widget diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 9ca96c0e..461cb31a 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -2,7 +2,6 @@ import logging import tkinter as tk from functools import partial from tkinter import filedialog, font, ttk -from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 from coretk import themes @@ -208,10 +207,13 @@ class CodeFont(font.Font): super().__init__(font="TkFixedFont", color="green") -class CodeText(ScrolledText): +class CodeText(ttk.Frame): def __init__(self, master, **kwargs): - super().__init__( - master, + super().__init__(master, **kwargs) + self.rowconfigure(0, weight=1) + self.columnconfigure(0, weight=1) + self.text = tk.Text( + self, bd=0, bg="black", cursor="xterm lime lime", @@ -222,8 +224,11 @@ class CodeText(ScrolledText): selectbackground="lime", selectforeground="black", relief=tk.FLAT, - **kwargs ) + self.text.grid(row=0, column=0, sticky="nsew") + yscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.text.yview) + yscrollbar.grid(row=0, column=1, sticky="ns") + self.text.configure(yscrollcommand=yscrollbar.set) class Spinbox(ttk.Entry): From 80609da90a8369398bc76e516d1c1d379d3dc82f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 17 Dec 2019 10:58:03 -0800 Subject: [PATCH 406/462] updates to docs for nrl changes to github --- docs/install.md | 9 ++++----- docs/services.md | 10 +++++----- docs/usage.md | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/install.md b/docs/install.md index 12242b4b..e10c1450 100644 --- a/docs/install.md +++ b/docs/install.md @@ -68,7 +68,7 @@ Virtual networks generally require some form of routing in order to work (e.g. t tables for routing packets from one subnet to another.) CORE builds OSPF routing protocol configurations by default when the blue router node type is used. -* [OSPF MANET Designated Routers](http://www.nrl.navy.mil/itd/ncs/products/ospf-manet) (MDR) - the Quagga routing +* [OSPF MANET Designated Routers](https://github.com/USNavalResearchLaboratory/ospf-mdr) (MDR) - the Quagga routing suite with a modified version of OSPFv3, optimized for use with mobile wireless networks. The **mdr** node type (and the MDR service) requires this variant of Quagga. @@ -77,7 +77,7 @@ suite with a modified version of OSPFv3, optimized for use with mobile wireless There is a built package which can be used. ```shell -wget https://downloads.pf.itd.nrl.navy.mil/ospf-manet/quagga-0.99.21mr2.2/quagga-mr_0.99.21mr2.2_amd64.deb +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 ``` @@ -89,9 +89,8 @@ Requires building from source, from the latest nightly snapshot. # packages needed beyond what's normally required to build core on ubuntu sudo apt install libtool libreadline-dev autoconf -wget https://downloads.pf.itd.nrl.navy.mil/ospf-manet/nightly_snapshots/quagga-svnsnap.tgz -tar xzf quagga-svnsnap.tgz -cd quagga +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 \ diff --git a/docs/services.md b/docs/services.md index f2a1a38a..692bcedd 100644 --- a/docs/services.md +++ b/docs/services.md @@ -312,17 +312,17 @@ Currently the Naval Research Laboratory uses this library to develop a wide vari * arouted #### NRL Installation -In order to be able to use the different protocols that NRL offers, you must first download the support library itself. You can get the source code from their [official nightly snapshots website](https://downloads.pf.itd.nrl.navy.mil/protolib/nightly_snapshots/). +In order to be able to use the different protocols that NRL offers, you must first download the support library itself. You can get the source code from their [NRL Protolib Repo](https://github.com/USNavalResearchLaboratory/protolib). #### Multi-Generator (MGEN) -Download MGEN from the [NRL MGEN nightly snapshots](https://downloads.pf.itd.nrl.navy.mil/mgen/nightly_snapshots/), unpack it and copy the protolib library into the main folder *mgen*. Execute the following commands to build the protocol. +Download MGEN from the [NRL MGEN Repo](https://github.com/USNavalResearchLaboratory/mgen), unpack it and copy the protolib library into the main folder *mgen*. Execute the following commands to build the protocol. ```shell cd mgen/makefiles make -f Makefile.{os} mgen ``` #### Neighborhood Discovery Protocol (NHDP) -Download NHDP from the [NRL NHDP nightly snapshots](https://downloads.pf.itd.nrl.navy.mil/nhdp/nightly_snapshots/). +Download NHDP from the [NRL NHDP Repo](https://github.com/USNavalResearchLaboratory/NCS-Downloads/tree/master/nhdp). ```shell sudo apt-get install libpcap-dev libboost-all-dev wget https://github.com/protocolbuffers/protobuf/releases/download/v3.8.0/protoc-3.8.0-linux-x86_64.zip @@ -339,14 +339,14 @@ make -f Makefile.{os} ``` #### Simplified Multicast Forwarding (SMF) -Download SMF from the [NRL SMF nightly snapshot](https://downloads.pf.itd.nrl.navy.mil/smf/nightly_snapshots/) , unpack it and place the protolib library inside the *smf* main folder. +Download SMF from the [NRL SMF Repo](https://github.com/USNavalResearchLaboratory/nrlsmf) , unpack it and place the protolib library inside the *smf* main folder. ```shell cd mgen/makefiles make -f Makefile.{os} ``` #### Optimized Link State Routing Protocol (OLSR) -To install the OLSR protocol, download their source code from their [nightly snapshots](https://downloads.pf.itd.nrl.navy.mil/olsr/nightly_snapshots/nrlolsr-svnsnap.tgz). Unpack it and place the previously downloaded protolib library inside the *nrlolsr* main directory. Then execute the following commands: +To install the OLSR protocol, download their source code from their [NRL OLSR Repo](https://github.com/USNavalResearchLaboratory/nrlolsr). Unpack it and place the previously downloaded protolib library inside the *nrlolsr* main directory. Then execute the following commands: ```shell cd ./unix make -f Makefile.{os} diff --git a/docs/usage.md b/docs/usage.md index 7f8cf672..02e1295f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -650,7 +650,7 @@ Any time you edit the topology file, you will need to stop the emulation if it were running and reload the file. -The **.xml** [file schema is specified by NRL](http://www.nrl.navy.mil/itd/ncs/products/mnmtools) and there are two versions to date: +The **.xml** [file schema is specified by NRL](https://github.com/USNavalResearchLaboratory/NCS-Downloads/blob/master/mnmtools/EmulationScriptSchemaDescription.pdf) and there are two versions to date: version 0.0 and version 1.0, with 1.0 as the current default. CORE can open either XML version. However, the xmlfilever line in **/etc/core/core.conf** controls the version of the XML file From e72f44ed85ab4b6112063c6e32b7e6a15949a1df Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 17 Dec 2019 11:35:30 -0800 Subject: [PATCH 407/462] updated canvas wallpaper selection to use common image chooser function --- coretk/coretk/dialogs/canvaswallpaper.py | 12 +++--------- coretk/coretk/dialogs/customnodes.py | 3 ++- coretk/coretk/dialogs/nodeconfig.py | 3 ++- coretk/coretk/widgets.py | 7 +++---- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/coretk/coretk/dialogs/canvaswallpaper.py b/coretk/coretk/dialogs/canvaswallpaper.py index 923e4d5f..570bfa08 100644 --- a/coretk/coretk/dialogs/canvaswallpaper.py +++ b/coretk/coretk/dialogs/canvaswallpaper.py @@ -3,12 +3,13 @@ set wallpaper """ import logging import tkinter as tk -from tkinter import filedialog, ttk +from tkinter import ttk from coretk.appconfig import BACKGROUNDS_PATH from coretk.dialogs.dialog import Dialog from coretk.images import Images from coretk.themes import PADX, PADY +from coretk.widgets import image_chooser class CanvasBackgroundDialog(Dialog): @@ -126,14 +127,7 @@ class CanvasBackgroundDialog(Dialog): button.grid(row=0, column=1, sticky="ew") def click_open_image(self): - filename = filedialog.askopenfilename( - initialdir=str(BACKGROUNDS_PATH), - title="Open", - filetypes=( - ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), - ("All Files", "*"), - ), - ) + filename = image_chooser(self, BACKGROUNDS_PATH) if filename: self.filename.set(filename) self.draw_preview() diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 7effd11a..6416d24a 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -4,6 +4,7 @@ from pathlib import Path from tkinter import ttk from coretk import nodeutils +from coretk.appconfig import ICONS_PATH from coretk.dialogs.dialog import Dialog from coretk.images import Images from coretk.nodeutils import NodeDraw @@ -179,7 +180,7 @@ class CustomNodesDialog(Dialog): self.image_button.config(image="") def click_icon(self): - file_path = image_chooser(self) + file_path = image_chooser(self, ICONS_PATH) if file_path: image = Images.create(file_path, nodeutils.ICON_SIZE) self.image = image diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 1137d6b9..4e5bc864 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -4,6 +4,7 @@ from functools import partial from tkinter import ttk from coretk import nodeutils +from coretk.appconfig import ICONS_PATH from coretk.dialogs.dialog import Dialog from coretk.dialogs.emaneconfig import EmaneModelDialog from coretk.images import Images @@ -209,7 +210,7 @@ class NodeConfigDialog(Dialog): dialog.show() def click_icon(self): - file_path = image_chooser(self) + file_path = image_chooser(self, ICONS_PATH) if file_path: self.image = Images.create(file_path, nodeutils.ICON_SIZE) self.image_button.config(image=self.image) diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 461cb31a..6567ca75 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -5,7 +5,6 @@ from tkinter import filedialog, font, ttk from core.api.grpc import core_pb2 from coretk import themes -from coretk.appconfig import ICONS_PATH from coretk.themes import FRAME_PAD, PADX, PADY INT_TYPES = { @@ -239,11 +238,11 @@ class Spinbox(ttk.Entry): self.tk.call(self._w, "set", value) -def image_chooser(parent): +def image_chooser(parent, path): return filedialog.askopenfilename( parent=parent, - initialdir=str(ICONS_PATH), - title="Select Icon", + initialdir=str(path), + title="Select", filetypes=( ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), ("All Files", "*"), From 50efd2ebc3f2394a53507e042737919e3f86f47f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 17 Dec 2019 11:43:49 -0800 Subject: [PATCH 408/462] changes to framescroll widget to use a standard frame by default instead of a label frame --- coretk/coretk/dialogs/customnodes.py | 12 ++++++------ coretk/coretk/dialogs/nodeservice.py | 12 ++++++------ coretk/coretk/widgets.py | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 6416d24a..94f2a32f 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -41,14 +41,14 @@ class ServicesSelectDialog(Dialog): self.groups.listbox.bind("<>", self.handle_group_change) self.groups.listbox.selection_set(0) + label_frame = ttk.LabelFrame(frame, text="Services") + label_frame.grid(row=0, column=1, sticky="nsew") + label_frame.columnconfigure(0, weight=1) + label_frame.rowconfigure(0, weight=1) self.services = CheckboxList( - frame, - self.app, - text="Services", - clicked=self.service_clicked, - padding=FRAME_PAD, + label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD ) - self.services.grid(row=0, column=1, sticky="nsew") + self.services.grid(sticky="nsew") label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD) label_frame.grid(row=0, column=2, sticky="nsew") diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 59b8865b..8ad87649 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -51,14 +51,14 @@ class NodeService(Dialog): self.groups.listbox.bind("<>", self.handle_group_change) self.groups.listbox.selection_set(0) + label_frame = ttk.LabelFrame(frame, text="Services") + label_frame.grid(row=0, column=1, sticky="nsew") + label_frame.columnconfigure(0, weight=1) + label_frame.rowconfigure(0, weight=1) self.services = CheckboxList( - frame, - self.app, - text="Services", - clicked=self.service_clicked, - padding=FRAME_PAD, + label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD ) - self.services.grid(row=0, column=1, sticky="nsew") + self.services.grid(sticky="nsew") label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD) label_frame.grid(row=0, column=2, sticky="nsew") diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 6567ca75..7deb7282 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -25,7 +25,7 @@ def file_button_click(value): value.set(file_path) -class FrameScroll(ttk.LabelFrame): +class FrameScroll(ttk.Frame): def __init__(self, master, app, _cls=ttk.Frame, **kw): super().__init__(master, **kw) self.app = app From d11ce3ef7f3c7072eec092053406a146d0e56775 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 17 Dec 2019 11:47:05 -0800 Subject: [PATCH 409/462] create layout for link configuration --- coretk/coretk/dialogs/linkconfig.py | 212 ++++++++++++++++++++++++++++ coretk/coretk/graph/edges.py | 28 +++- 2 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 coretk/coretk/dialogs/linkconfig.py diff --git a/coretk/coretk/dialogs/linkconfig.py b/coretk/coretk/dialogs/linkconfig.py new file mode 100644 index 00000000..3ece4ec0 --- /dev/null +++ b/coretk/coretk/dialogs/linkconfig.py @@ -0,0 +1,212 @@ +""" +link configuration +""" +import logging +import tkinter as tk +from tkinter import ttk + +from coretk.dialogs.dialog import Dialog + + +class LinkConfiguration(Dialog): + def __init__(self, master, app, edge): + super().__init__(master, app, "link configuration", modal=True) + self.app = app + self.edge = edge + self.is_symmetric = True + if self.is_symmetric: + self.symmetry_var = tk.StringVar(value=">>") + else: + self.symmetry_var = tk.StringVar(value="<<") + + self.bandwidth = tk.DoubleVar() + self.delay = tk.DoubleVar() + self.jitter = tk.DoubleVar() + self.loss = tk.DoubleVar() + self.duplicate = tk.DoubleVar() + self.color = "#000000" + self.width = tk.DoubleVar() + + self.down_bandwidth = tk.DoubleVar() + self.down_delay = tk.DoubleVar() + self.down_jitter = tk.DoubleVar() + self.down_loss = tk.DoubleVar() + self.down_duplicate = tk.DoubleVar() + + self.load_link_config() + self.symmetric_frame = None + self.asymmetric_frame = None + + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + source_name = self.app.canvas.nodes[self.edge.src].core_node.name + dest_name = self.app.canvas.nodes[self.edge.dst].core_node.name + label = ttk.Label( + self.top, + text="Link from %s to %s" % (source_name, dest_name), + anchor=tk.CENTER, + ) + label.grid(row=0, column=0, sticky="nsew") + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.grid(row=1, column=0, sticky="nsew") + button = ttk.Button(frame, text="unlimited") + button.grid(row=0, column=0, sticky="nsew") + if self.is_symmetric: + button = ttk.Button( + frame, textvariable=self.symmetry_var, command=self.change_symmetry + ) + else: + button = ttk.Button( + frame, textvariable=self.symmetry_var, command=self.change_symmetry + ) + button.grid(row=0, column=1, sticky="nsew") + + if self.is_symmetric: + self.symmetric_frame = self.get_frame() + self.symmetric_frame.grid(row=2, column=0, sticky="nsew") + else: + self.asymmetric_frame = self.get_frame() + self.asymmetric_frame.grid(row=2, column=0, sticky="nsew") + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.grid(row=3, column=0, sticky="nsew") + + button = ttk.Button(frame, text="Apply") + button.grid(row=0, column=0, sticky="nsew") + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="nsew") + + def get_frame(self): + main_frame = ttk.Frame(self.top) + main_frame.columnconfigure(0, weight=1) + if self.is_symmetric: + label_name = "Symmetric link effects: " + else: + label_name = "Asymmetric effects: downstream / upstream " + row = 0 + label = ttk.Label(main_frame, text=label_name, anchor=tk.CENTER) + label.grid(row=row, column=0, sticky="nsew") + row = row + 1 + + frame = ttk.Frame(main_frame) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=row, column=0, sticky="nsew") + label = ttk.Label(frame, text="Bandwidth (bps): ") + label.grid(row=0, column=0, sticky="nsew") + entry = ttk.Entry(frame, textvariable=self.bandwidth) + entry.grid(row=0, column=1, sticky="nsew") + if not self.is_symmetric: + entry = ttk.Entry(frame, textvariable=self.down_bandwidth) + entry.grid(row=0, column=2, sticky="nsew") + + row = row + 1 + + frame = ttk.Frame(main_frame) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=row, column=0, sticky="nsew") + label = ttk.Label(frame, text="Delay (us): ") + label.grid(row=0, column=0, sticky="nsew") + entry = ttk.Entry(frame, textvariable=self.delay) + entry.grid(row=0, column=1, sticky="nsew") + if not self.is_symmetric: + entry = ttk.Entry(frame, textvariable=self.down_delay) + entry.grid(row=0, column=2, sticky="nsew") + row = row + 1 + + frame = ttk.Frame(main_frame) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=row, column=0, sticky="nsew") + label = ttk.Label(frame, text="Jitter (us): ") + label.grid(row=0, column=0, sticky="nsew") + entry = ttk.Entry(frame, textvariable=self.jitter) + entry.grid(row=0, column=1, sticky="nsew") + if not self.is_symmetric: + entry = ttk.Entry(frame, textvariable=self.down_jitter) + entry.grid(row=0, column=2, sticky="nsew") + row = row + 1 + + frame = ttk.Frame(main_frame) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=row, column=0, sticky="nsew") + label = ttk.Label(frame, text="Loss (%): ") + label.grid(row=0, column=0, sticky="nsew") + entry = ttk.Entry(frame, textvariable=self.loss) + entry.grid(row=0, column=1, sticky="nsew") + if not self.is_symmetric: + entry = ttk.Entry(frame, textvariable=self.down_loss) + entry.grid(row=0, column=1, sticky="nsew") + row = row + 1 + + frame = ttk.Frame(main_frame) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=row, column=0, sticky="nsew") + label = ttk.Label(frame, text="Duplicate (%): ") + label.grid(row=0, column=0, sticky="nsew") + entry = ttk.Entry(frame, textvariable=self.duplicate) + entry.grid(row=0, column=1, sticky="nsew") + if not self.is_symmetric: + entry = ttk.Entry(frame, textvariable=self.down_duplicate) + entry.grid(row=0, column=1, sticky="nsew") + row = row + 1 + + frame = ttk.Frame(main_frame) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=row, column=0, sticky="nsew") + label = ttk.Label(frame, text="Color: ") + label.grid(row=0, column=0, sticky="nsew") + button = ttk.Button(frame, text=self.color) + button.grid(row=0, column=1, sticky="nsew") + row = row + 1 + + frame = ttk.Frame(main_frame) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=row, column=0, sticky="nsew") + label = ttk.Label(frame, text="Width: ") + label.grid(row=0, column=0, sticky="nsew") + entry = ttk.Entry(frame, textvariable=self.width) + entry.grid(row=0, column=1, sticky="nsew") + + return main_frame + + def apply(self): + logging.debug("click apply") + + def change_symmetry(self): + logging.debug("change symmetry") + + if self.is_symmetric: + self.is_symmetric = False + self.symmetry_var.set("<<") + if not self.asymmetric_frame: + self.asymmetric_frame = self.get_frame() + self.symmetric_frame.grid_forget() + self.asymmetric_frame.grid(row=2, column=0) + else: + self.is_symmetric = True + self.symmetry_var.set(">>") + if not self.symmetric_frame: + self.symmetric_frame = self.get_frame() + self.asymmetric_frame.grid_forget() + self.symmetric_frame.grid(row=2, column=0) + + def load_link_config(self): + """ + populate link config to the table + + :return: nothing + """ + width = self.app.canvas.itemcget(self.edge.id, "width") + self.width.set(width) diff --git a/coretk/coretk/graph/edges.py b/coretk/coretk/graph/edges.py index 94d847bc..ab9447be 100644 --- a/coretk/coretk/graph/edges.py +++ b/coretk/coretk/graph/edges.py @@ -1,5 +1,8 @@ +import logging import tkinter as tk +from coretk import themes +from coretk.dialogs.linkconfig import LinkConfiguration from coretk.graph import tags from coretk.nodeutils import NodeUtils @@ -23,7 +26,7 @@ class CanvasEdge: Canvas edge class """ - width = 1.4 + width = 3 def __init__(self, x1, y1, x2, y2, src, canvas): """ @@ -46,6 +49,10 @@ class CanvasEdge: self.token = None self.link_info = None self.throughput = None + self.set_binding() + + def set_binding(self): + self.canvas.tag_bind(self.id, "", self.create_context) def complete(self, dst): self.dst = dst @@ -89,3 +96,22 @@ class CanvasEdge: if self.link_info: self.canvas.delete(self.link_info.id1) self.canvas.delete(self.link_info.id2) + + def create_context(self, event): + logging.debug("create link context") + context = tk.Menu(self.canvas) + themes.style_menu(context) + context.add_command(label="Configure", command=self.configure) + context.add_command(label="Delete") + context.add_command(label="Split") + context.add_command(label="Merge") + if self.canvas.app.core.is_runtime(): + context.entryconfigure(1, state="disabled") + context.entryconfigure(2, state="disabled") + context.entryconfigure(3, state="disabled") + context.post(event.x_root, event.y_root) + + def configure(self): + logging.debug("link configuration") + dialog = LinkConfiguration(self.canvas, self.canvas.app, self) + dialog.show() From 582beea1be8e8ba09a744069b6b48edc7be636e6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 17 Dec 2019 11:56:22 -0800 Subject: [PATCH 410/462] improvement to configframe, it is now a notebook itself, with scrollable frames for contents in each tab --- coretk/coretk/widgets.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 7deb7282..649dce95 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -63,9 +63,9 @@ class FrameScroll(ttk.Frame): widget.destroy() -class ConfigFrame(FrameScroll): +class ConfigFrame(ttk.Notebook): def __init__(self, master, app, config, **kw): - super().__init__(master, app, ttk.Notebook, borderwidth=0, **kw) + super().__init__(master, **kw) self.app = app self.config = config self.values = {} @@ -79,17 +79,17 @@ class ConfigFrame(FrameScroll): for group_name in sorted(group_mapping): group = group_mapping[group_name] - frame = ttk.Frame(self.frame, padding=FRAME_PAD) - frame.columnconfigure(1, weight=1) - self.frame.add(frame, text=group_name) + tab = FrameScroll(self, self.app, borderwidth=0, padding=FRAME_PAD) + tab.frame.columnconfigure(1, weight=1) + self.add(tab, text=group_name) for index, option in enumerate(sorted(group, key=lambda x: x.name)): - label = ttk.Label(frame, text=option.label) + label = ttk.Label(tab.frame, text=option.label) label.grid(row=index, pady=PADY, padx=PADX, sticky="w") value = tk.StringVar() if option.type == core_pb2.ConfigOptionType.BOOL: select = tuple(option.select) combobox = ttk.Combobox( - frame, textvariable=value, values=select, state="readonly" + tab.frame, textvariable=value, values=select, state="readonly" ) combobox.grid(row=index, column=1, sticky="ew") if option.value == "1": @@ -100,13 +100,13 @@ class ConfigFrame(FrameScroll): value.set(option.value) select = tuple(option.select) combobox = ttk.Combobox( - frame, textvariable=value, values=select, state="readonly" + tab.frame, textvariable=value, values=select, state="readonly" ) combobox.grid(row=index, column=1, sticky="ew") elif option.type == core_pb2.ConfigOptionType.STRING: value.set(option.value) if "file" in option.label: - file_frame = ttk.Frame(frame) + file_frame = ttk.Frame(tab.frame) file_frame.grid(row=index, column=1, sticky="ew") file_frame.columnconfigure(0, weight=1) entry = ttk.Entry(file_frame, textvariable=value) @@ -117,20 +117,20 @@ class ConfigFrame(FrameScroll): else: if "controlnet" in option.name and "script" not in option.name: entry = ttk.Entry( - frame, + tab.frame, textvariable=value, validate="key", validatecommand=(self.app.validation.ip4, "%P"), ) entry.grid(row=index, column=1, sticky="ew") else: - entry = ttk.Entry(frame, textvariable=value) + entry = ttk.Entry(tab.frame, textvariable=value) entry.grid(row=index, column=1, sticky="ew") elif option.type in INT_TYPES: value.set(option.value) entry = ttk.Entry( - frame, + tab.frame, textvariable=value, validate="key", validatecommand=(self.app.validation.positive_int, "%P"), @@ -143,7 +143,7 @@ class ConfigFrame(FrameScroll): elif option.type == core_pb2.ConfigOptionType.FLOAT: value.set(option.value) entry = ttk.Entry( - frame, + tab.frame, textvariable=value, validate="key", validatecommand=(self.app.validation.positive_float, "%P"), From 752b3956a6a14638740381415ac79e297c8dad8c Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 17 Dec 2019 11:59:34 -0800 Subject: [PATCH 411/462] create layout for link configuration --- coretk/coretk/dialogs/linkconfig.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/coretk/coretk/dialogs/linkconfig.py b/coretk/coretk/dialogs/linkconfig.py index 3ece4ec0..bd0a9207 100644 --- a/coretk/coretk/dialogs/linkconfig.py +++ b/coretk/coretk/dialogs/linkconfig.py @@ -24,7 +24,7 @@ class LinkConfiguration(Dialog): self.jitter = tk.DoubleVar() self.loss = tk.DoubleVar() self.duplicate = tk.DoubleVar() - self.color = "#000000" + self.color = tk.StringVar(value="#000000") self.width = tk.DoubleVar() self.down_bandwidth = tk.DoubleVar() @@ -77,7 +77,7 @@ class LinkConfiguration(Dialog): frame.columnconfigure(1, weight=1) frame.grid(row=3, column=0, sticky="nsew") - button = ttk.Button(frame, text="Apply") + button = ttk.Button(frame, text="Apply", command=self.apply) button.grid(row=0, column=0, sticky="nsew") button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="nsew") @@ -166,7 +166,7 @@ class LinkConfiguration(Dialog): frame.grid(row=row, column=0, sticky="nsew") label = ttk.Label(frame, text="Color: ") label.grid(row=0, column=0, sticky="nsew") - button = ttk.Button(frame, text=self.color) + button = ttk.Button(frame, textvariable=self.color) button.grid(row=0, column=1, sticky="nsew") row = row + 1 @@ -183,6 +183,9 @@ class LinkConfiguration(Dialog): def apply(self): logging.debug("click apply") + width = self.width.get() + self.app.canvas.itemconfigure(self.edge.id, width=width) + self.destroy() def change_symmetry(self): logging.debug("change symmetry") @@ -209,4 +212,6 @@ class LinkConfiguration(Dialog): :return: nothing """ width = self.app.canvas.itemcget(self.edge.id, "width") + # color = self.app.canvas.itemcget(self.edge.id, "fill") self.width.set(width) + # self.color From 7bd8c5a5aa45c70c7ae17db83bb5159130e15c11 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 17 Dec 2019 22:02:56 -0800 Subject: [PATCH 412/462] updates to linkconfig, working configuration for symmetric, updates to change link color and use color picker --- coretk/coretk/dialogs/linkconfig.py | 276 +++++++++++++++++----------- 1 file changed, 171 insertions(+), 105 deletions(-) diff --git a/coretk/coretk/dialogs/linkconfig.py b/coretk/coretk/dialogs/linkconfig.py index bd0a9207..9a2008ab 100644 --- a/coretk/coretk/dialogs/linkconfig.py +++ b/coretk/coretk/dialogs/linkconfig.py @@ -5,12 +5,14 @@ import logging import tkinter as tk from tkinter import ttk +from coretk.dialogs.colorpicker import ColorPicker from coretk.dialogs.dialog import Dialog +from coretk.themes import PADX, PADY class LinkConfiguration(Dialog): def __init__(self, master, app, edge): - super().__init__(master, app, "link configuration", modal=True) + super().__init__(master, app, "Link Configuration", modal=True) self.app = app self.edge = edge self.is_symmetric = True @@ -19,19 +21,20 @@ class LinkConfiguration(Dialog): else: self.symmetry_var = tk.StringVar(value="<<") - self.bandwidth = tk.DoubleVar() - self.delay = tk.DoubleVar() - self.jitter = tk.DoubleVar() - self.loss = tk.DoubleVar() - self.duplicate = tk.DoubleVar() + self.bandwidth = tk.StringVar() + self.delay = tk.StringVar() + self.jitter = tk.StringVar() + self.loss = tk.StringVar() + self.duplicate = tk.StringVar() self.color = tk.StringVar(value="#000000") + self.color_button = None self.width = tk.DoubleVar() - self.down_bandwidth = tk.DoubleVar() - self.down_delay = tk.DoubleVar() - self.down_jitter = tk.DoubleVar() - self.down_loss = tk.DoubleVar() - self.down_duplicate = tk.DoubleVar() + self.down_bandwidth = tk.IntVar(value="") + self.down_delay = tk.IntVar(value="") + self.down_jitter = tk.IntVar(value="") + self.down_loss = tk.DoubleVar(value="") + self.down_duplicate = tk.IntVar(value="") self.load_link_config() self.symmetric_frame = None @@ -44,17 +47,16 @@ class LinkConfiguration(Dialog): source_name = self.app.canvas.nodes[self.edge.src].core_node.name dest_name = self.app.canvas.nodes[self.edge.dst].core_node.name label = ttk.Label( - self.top, - text="Link from %s to %s" % (source_name, dest_name), - anchor=tk.CENTER, + self.top, text=f"Link from {source_name} to {dest_name}", anchor=tk.CENTER ) - label.grid(row=0, column=0, sticky="nsew") + label.grid(row=0, column=0, sticky="ew", pady=PADY) + frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - frame.grid(row=1, column=0, sticky="nsew") - button = ttk.Button(frame, text="unlimited") - button.grid(row=0, column=0, sticky="nsew") + frame.grid(row=1, column=0, sticky="ew", pady=PADY) + button = ttk.Button(frame, text="Unlimited") + button.grid(row=0, column=0, sticky="ew", padx=PADX) if self.is_symmetric: button = ttk.Button( frame, textvariable=self.symmetry_var, command=self.change_symmetry @@ -63,128 +65,185 @@ class LinkConfiguration(Dialog): button = ttk.Button( frame, textvariable=self.symmetry_var, command=self.change_symmetry ) - button.grid(row=0, column=1, sticky="nsew") + button.grid(row=0, column=1, sticky="ew") if self.is_symmetric: self.symmetric_frame = self.get_frame() - self.symmetric_frame.grid(row=2, column=0, sticky="nsew") + self.symmetric_frame.grid(row=2, column=0, sticky="ew", pady=PADY) else: self.asymmetric_frame = self.get_frame() - self.asymmetric_frame.grid(row=2, column=0, sticky="nsew") + self.asymmetric_frame.grid(row=2, column=0, sticky="ew", pady=PADY) + + self.draw_spacer(row=3) frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - frame.grid(row=3, column=0, sticky="nsew") - - button = ttk.Button(frame, text="Apply", command=self.apply) - button.grid(row=0, column=0, sticky="nsew") + frame.grid(row=4, column=0, sticky="ew") + button = ttk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="nsew") + button.grid(row=0, column=1, sticky="ew") def get_frame(self): - main_frame = ttk.Frame(self.top) - main_frame.columnconfigure(0, weight=1) + frame = ttk.Frame(self.top) + frame.columnconfigure(1, weight=1) if self.is_symmetric: - label_name = "Symmetric link effects: " + label_name = "Symmetric Link Effects" else: - label_name = "Asymmetric effects: downstream / upstream " + label_name = "Asymmetric Effects: Downstream / Upstream " row = 0 - label = ttk.Label(main_frame, text=label_name, anchor=tk.CENTER) - label.grid(row=row, column=0, sticky="nsew") + label = ttk.Label(frame, text=label_name, anchor=tk.CENTER) + label.grid(row=row, column=0, columnspan=2, sticky="ew", pady=PADY) row = row + 1 - frame = ttk.Frame(main_frame) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - frame.grid(row=row, column=0, sticky="nsew") - label = ttk.Label(frame, text="Bandwidth (bps): ") - label.grid(row=0, column=0, sticky="nsew") - entry = ttk.Entry(frame, textvariable=self.bandwidth) - entry.grid(row=0, column=1, sticky="nsew") + label = ttk.Label(frame, text="Bandwidth (bps)") + label.grid(row=row, column=0, sticky="ew") + entry = ttk.Entry( + frame, + textvariable=self.bandwidth, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=1, sticky="ew", pady=PADY) if not self.is_symmetric: - entry = ttk.Entry(frame, textvariable=self.down_bandwidth) - entry.grid(row=0, column=2, sticky="nsew") - + entry = ttk.Entry( + frame, + textvariable=self.down_bandwidth, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=2, sticky="nsew") row = row + 1 - frame = ttk.Frame(main_frame) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - frame.grid(row=row, column=0, sticky="nsew") - label = ttk.Label(frame, text="Delay (us): ") - label.grid(row=0, column=0, sticky="nsew") - entry = ttk.Entry(frame, textvariable=self.delay) - entry.grid(row=0, column=1, sticky="nsew") + label = ttk.Label(frame, text="Delay (us)") + label.grid(row=row, column=0, sticky="ew") + entry = ttk.Entry( + frame, + textvariable=self.delay, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=1, sticky="ew", pady=PADY) if not self.is_symmetric: - entry = ttk.Entry(frame, textvariable=self.down_delay) - entry.grid(row=0, column=2, sticky="nsew") + entry = ttk.Entry( + frame, + textvariable=self.down_delay, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=2, sticky="ew") row = row + 1 - frame = ttk.Frame(main_frame) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - frame.grid(row=row, column=0, sticky="nsew") - label = ttk.Label(frame, text="Jitter (us): ") - label.grid(row=0, column=0, sticky="nsew") - entry = ttk.Entry(frame, textvariable=self.jitter) - entry.grid(row=0, column=1, sticky="nsew") + label = ttk.Label(frame, text="Jitter (us)") + label.grid(row=row, column=0, sticky="ew") + entry = ttk.Entry( + frame, + textvariable=self.jitter, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=1, sticky="ew", pady=PADY) if not self.is_symmetric: - entry = ttk.Entry(frame, textvariable=self.down_jitter) - entry.grid(row=0, column=2, sticky="nsew") + entry = ttk.Entry( + frame, + textvariable=self.down_jitter, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=2, sticky="ew") row = row + 1 - frame = ttk.Frame(main_frame) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - frame.grid(row=row, column=0, sticky="nsew") - label = ttk.Label(frame, text="Loss (%): ") - label.grid(row=0, column=0, sticky="nsew") - entry = ttk.Entry(frame, textvariable=self.loss) - entry.grid(row=0, column=1, sticky="nsew") + label = ttk.Label(frame, text="Loss (%)") + label.grid(row=row, column=0, sticky="ew") + entry = ttk.Entry( + frame, + textvariable=self.loss, + validate="key", + validatecommand=(self.app.validation.positive_float, "%P"), + ) + entry.grid(row=row, column=1, sticky="ew", pady=PADY) if not self.is_symmetric: - entry = ttk.Entry(frame, textvariable=self.down_loss) - entry.grid(row=0, column=1, sticky="nsew") + entry = ttk.Entry( + frame, + textvariable=self.down_loss, + validate="key", + validatecommand=(self.app.validation.positive_float, "%P"), + ) + entry.grid(row=row, column=2, sticky="ew") row = row + 1 - frame = ttk.Frame(main_frame) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - frame.grid(row=row, column=0, sticky="nsew") - label = ttk.Label(frame, text="Duplicate (%): ") - label.grid(row=0, column=0, sticky="nsew") - entry = ttk.Entry(frame, textvariable=self.duplicate) - entry.grid(row=0, column=1, sticky="nsew") + label = ttk.Label(frame, text="Duplicate (%)") + label.grid(row=row, column=0, sticky="ew") + entry = ttk.Entry( + frame, + textvariable=self.duplicate, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=1, sticky="ew", pady=PADY) if not self.is_symmetric: - entry = ttk.Entry(frame, textvariable=self.down_duplicate) - entry.grid(row=0, column=1, sticky="nsew") + entry = ttk.Entry( + frame, + textvariable=self.down_duplicate, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=2, sticky="ew") row = row + 1 - frame = ttk.Frame(main_frame) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - frame.grid(row=row, column=0, sticky="nsew") - label = ttk.Label(frame, text="Color: ") - label.grid(row=0, column=0, sticky="nsew") - button = ttk.Button(frame, textvariable=self.color) - button.grid(row=0, column=1, sticky="nsew") + label = ttk.Label(frame, text="Color") + label.grid(row=row, column=0, sticky="ew") + self.color_button = tk.Button( + frame, + textvariable=self.color, + background=self.color.get(), + bd=0, + relief=tk.FLAT, + highlightthickness=0, + command=self.click_color, + ) + self.color_button.grid(row=row, column=1, sticky="ew", pady=PADY) row = row + 1 - frame = ttk.Frame(main_frame) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - frame.grid(row=row, column=0, sticky="nsew") - label = ttk.Label(frame, text="Width: ") - label.grid(row=0, column=0, sticky="nsew") - entry = ttk.Entry(frame, textvariable=self.width) - entry.grid(row=0, column=1, sticky="nsew") + label = ttk.Label(frame, text="Width") + label.grid(row=row, column=0, sticky="ew") + entry = ttk.Entry( + frame, + textvariable=self.width, + validate="key", + validatecommand=(self.app.validation.positive_float, "%P"), + ) + entry.grid(row=row, column=1, sticky="ew", pady=PADY) - return main_frame + return frame - def apply(self): + def click_color(self): + dialog = ColorPicker(self, self.app, self.color.get()) + color = dialog.askcolor() + self.color.set(color) + self.color_button.config(background=color) + + def click_apply(self): logging.debug("click apply") - width = self.width.get() - self.app.canvas.itemconfigure(self.edge.id, width=width) + self.app.canvas.itemconfigure(self.edge.id, width=self.width.get()) + self.app.canvas.itemconfigure(self.edge.id, fill=self.color.get()) + link = self.edge.link_info.link + bandwidth = self.bandwidth.get() + if bandwidth != "": + link.options.bandwidth = int(bandwidth) + jitter = self.jitter.get() + if jitter != "": + link.options.jitter = int(jitter) + delay = self.delay.get() + if delay != "": + link.options.delay = int(delay) + duplicate = self.duplicate.get() + if duplicate != "": + link.options.dup = int(duplicate) + loss = self.loss.get() + if loss != "": + link.options.per = float(loss) self.destroy() def change_symmetry(self): @@ -212,6 +271,13 @@ class LinkConfiguration(Dialog): :return: nothing """ width = self.app.canvas.itemcget(self.edge.id, "width") - # color = self.app.canvas.itemcget(self.edge.id, "fill") self.width.set(width) - # self.color + color = self.app.canvas.itemcget(self.edge.id, "fill") + self.color.set(color) + link = self.edge.link_info.link + if link.HasField("options"): + self.bandwidth.set(str(link.options.bandwidth)) + self.jitter.set(str(link.options.jitter)) + self.duplicate.set(str(link.options.dup)) + self.loss.set(str(link.options.per)) + self.delay.set(str(link.options.delay)) From ccb433a32df81653906b3aaf5f9e8525c96fdaed Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 17 Dec 2019 22:08:14 -0800 Subject: [PATCH 413/462] update to support link config during runtime --- coretk/coretk/dialogs/linkconfig.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/coretk/coretk/dialogs/linkconfig.py b/coretk/coretk/dialogs/linkconfig.py index 9a2008ab..6f0bd51a 100644 --- a/coretk/coretk/dialogs/linkconfig.py +++ b/coretk/coretk/dialogs/linkconfig.py @@ -244,6 +244,24 @@ class LinkConfiguration(Dialog): loss = self.loss.get() if loss != "": link.options.per = float(loss) + + if self.app.core.is_runtime() and link.HasField("options"): + interface_one = None + if link.HasField("interface_one"): + interface_one = link.interface_one.id + interface_two = None + if link.HasField("interface_two"): + interface_two = link.interface_two.id + session_id = self.app.core.session_id + self.app.core.client.edit_link( + session_id, + link.node_one_id, + link.node_two_id, + link.options, + interface_one, + interface_two, + ) + self.destroy() def change_symmetry(self): From da26e347655568ff433d559ff565291cd341ee49 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 18 Dec 2019 09:49:45 -0800 Subject: [PATCH 414/462] work on marker tool --- coretk/coretk/dialogs/marker.py | 53 +++++++++++++++++++++++++++++++ coretk/coretk/graph/graph.py | 35 +++++++++++++++++--- coretk/coretk/graph/shapeutils.py | 4 +++ coretk/coretk/toolbar.py | 11 ++++++- 4 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 coretk/coretk/dialogs/marker.py diff --git a/coretk/coretk/dialogs/marker.py b/coretk/coretk/dialogs/marker.py new file mode 100644 index 00000000..8a476548 --- /dev/null +++ b/coretk/coretk/dialogs/marker.py @@ -0,0 +1,53 @@ +""" +marker dialog +""" + +from tkinter import ttk + +from coretk.dialogs.colorpicker import ColorPicker +from coretk.dialogs.dialog import Dialog + +MARKER_THICKNESS = [3, 5, 8, 10] + + +class Marker(Dialog): + def __init__(self, master, app, initcolor="#000000"): + super().__init__(master, app, "marker tool", modal=False) + self.app = app + self.color = initcolor + self.radius = MARKER_THICKNESS[0] + self.draw() + + def draw(self): + button = ttk.Button(self.top, text="clear", command=self.clear_marker) + button.grid(row=0, column=0) + + frame = ttk.Frame(self.top) + frame.grid(row=1, column=0) + + button = ttk.Button(frame, text="radius 1") + button.grid(row=0, column=0) + button = ttk.Button(frame, text="radius 2") + button.grid(row=0, column=1) + button = ttk.Button(frame, text="radius 3") + button.grid(row=1, column=0) + button = ttk.Button(frame, text="radius 4") + button.grid(row=1, column=1) + + label = ttk.Label(self.top, background=self.color) + label.grid(row=2, column=0, sticky="nsew") + label.bind("", self.change_color) + + # button = ttk.Button(self.top, text="color", command=self.change_color) + # button.grid(row=2, column=0) + + def clear_marker(self): + canvas = self.app.canvas + for i in canvas.find_withtag("marker"): + canvas.delete(i) + + def change_color(self, event): + color_picker = ColorPicker(self, self.app, self.color) + color = color_picker.askcolor() + event.widget.configure(background=color) + self.color = color diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 77cc2264..e0878f20 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -12,7 +12,7 @@ from coretk.graph.enums import GraphMode, ScaleOption from coretk.graph.linkinfo import LinkInfo, Throughput from coretk.graph.node import CanvasNode from coretk.graph.shape import Shape -from coretk.graph.shapeutils import ShapeType, is_draw_shape +from coretk.graph.shapeutils import ShapeType, is_draw_shape, is_marker from coretk.images import Images from coretk.nodeutils import NodeUtils @@ -45,6 +45,7 @@ class CanvasGraph(tk.Canvas): self.ratio = 1.0 self.offset = (0, 0) self.cursor = (0, 0) + self.marker_tool = None # background related self.wallpaper_id = None @@ -499,10 +500,22 @@ class CanvasGraph(tk.Canvas): self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) if self.mode == GraphMode.ANNOTATION and selected is None: - shape = Shape(self.app, self, self.annotation_type, x, y) - self.selected = shape.id - self.shape_drawing = True - self.shapes[shape.id] = shape + if is_marker(self.annotation_type): + r = self.app.toolbar.marker_tool.radius + self.create_oval( + x - r, + y - r, + x + r, + y + r, + fill=self.app.toolbar.marker_tool.color, + outline="", + tags="marker", + ) + else: + shape = Shape(self.app, self, self.annotation_type, x, y) + self.selected = shape.id + self.shape_drawing = True + self.shapes[shape.id] = shape if selected is not None: if selected not in self.selection: @@ -573,6 +586,18 @@ class CanvasGraph(tk.Canvas): if is_draw_shape(self.annotation_type) and self.shape_drawing: shape = self.shapes[self.selected] shape.shape_motion(x, y) + elif is_marker(self.annotation_type): + marker_tool = self.app.toolbar.marker_tool + r = marker_tool.radius + self.create_oval( + x - r, + y - r, + x + r, + y + r, + fill=self.app.toolbar.marker_tool.color, + outline="", + tags="marker", + ) if self.mode == GraphMode.EDGE: return diff --git a/coretk/coretk/graph/shapeutils.py b/coretk/coretk/graph/shapeutils.py index a566a713..0e2cc29c 100644 --- a/coretk/coretk/graph/shapeutils.py +++ b/coretk/coretk/graph/shapeutils.py @@ -17,3 +17,7 @@ def is_draw_shape(shape_type): def is_shape_text(shape_type): return shape_type == ShapeType.TEXT + + +def is_marker(shape_type): + return shape_type == ShapeType.MARKER diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 323c5d67..96b317c2 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -6,9 +6,10 @@ from tkinter import ttk from tkinter.font import Font from coretk.dialogs.customnodes import CustomNodesDialog +from coretk.dialogs.marker import Marker from coretk.graph import tags from coretk.graph.enums import GraphMode -from coretk.graph.shapeutils import ShapeType +from coretk.graph.shapeutils import ShapeType, is_marker from coretk.images import ImageEnum, Images from coretk.nodeutils import NodeUtils from coretk.themes import Styles @@ -57,6 +58,9 @@ class Toolbar(ttk.Frame): self.network_picker = None self.annotation_picker = None + # dialog + self.marker_tool = None + # draw components self.draw() @@ -401,6 +405,9 @@ class Toolbar(ttk.Frame): self.annotation_button.image = image self.app.canvas.mode = GraphMode.ANNOTATION self.app.canvas.annotation_type = shape_type + if is_marker(shape_type): + self.marker_tool = Marker(self.master, self.app) + self.marker_tool.show() def click_run_button(self): logging.debug("Click on RUN button") @@ -410,6 +417,8 @@ class Toolbar(ttk.Frame): def click_marker_button(self): logging.debug("Click on marker button") + dialog = Marker(self.master, self.app) + dialog.show() def click_two_node_button(self): logging.debug("Click TWONODE button") From 5f927f75ba0c3a8cf34e2845193794aaff008654 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 18 Dec 2019 11:57:47 -0800 Subject: [PATCH 415/462] marker tool --- coretk/coretk/dialogs/marker.py | 32 +++++++++++++++++++------------- coretk/coretk/toolbar.py | 5 +++-- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/coretk/coretk/dialogs/marker.py b/coretk/coretk/dialogs/marker.py index 8a476548..feb8bd09 100644 --- a/coretk/coretk/dialogs/marker.py +++ b/coretk/coretk/dialogs/marker.py @@ -2,6 +2,8 @@ marker dialog """ +import logging +import tkinter as tk from tkinter import ttk from coretk.dialogs.colorpicker import ColorPicker @@ -16,7 +18,9 @@ class Marker(Dialog): self.app = app self.color = initcolor self.radius = MARKER_THICKNESS[0] + self.marker_thickness = tk.IntVar(value=MARKER_THICKNESS[0]) self.draw() + self.top.bind("", self.close_marker) def draw(self): button = ttk.Button(self.top, text="clear", command=self.clear_marker) @@ -24,23 +28,18 @@ class Marker(Dialog): frame = ttk.Frame(self.top) frame.grid(row=1, column=0) - - button = ttk.Button(frame, text="radius 1") - button.grid(row=0, column=0) - button = ttk.Button(frame, text="radius 2") - button.grid(row=0, column=1) - button = ttk.Button(frame, text="radius 3") - button.grid(row=1, column=0) - button = ttk.Button(frame, text="radius 4") - button.grid(row=1, column=1) - + combobox = ttk.Combobox( + frame, + textvariable=self.marker_thickness, + values=MARKER_THICKNESS, + state="readonly", + ) + combobox.grid(row=0, column=1, sticky="nsew") + combobox.bind("<>", self.change_thickness) label = ttk.Label(self.top, background=self.color) label.grid(row=2, column=0, sticky="nsew") label.bind("", self.change_color) - # button = ttk.Button(self.top, text="color", command=self.change_color) - # button.grid(row=2, column=0) - def clear_marker(self): canvas = self.app.canvas for i in canvas.find_withtag("marker"): @@ -51,3 +50,10 @@ class Marker(Dialog): color = color_picker.askcolor() event.widget.configure(background=color) self.color = color + + def change_thickness(self, event): + self.radius = self.marker_thickness.get() + + def close_marker(self, event): + logging.debug("destroy marker dialog") + self.app.toolbar.marker_tool = None diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 96b317c2..0769380a 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -406,8 +406,9 @@ class Toolbar(ttk.Frame): self.app.canvas.mode = GraphMode.ANNOTATION self.app.canvas.annotation_type = shape_type if is_marker(shape_type): - self.marker_tool = Marker(self.master, self.app) - self.marker_tool.show() + if not self.marker_tool: + self.marker_tool = Marker(self.master, self.app) + self.marker_tool.show() def click_run_button(self): logging.debug("Click on RUN button") From 1884cda27188d5f05bf741269ec05fb042b174e2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 18 Dec 2019 16:51:05 -0800 Subject: [PATCH 416/462] consolidated logic for drawing edge labels based on link to be contained within the edge class itself --- coretk/coretk/coreclient.py | 8 +-- coretk/coretk/dialogs/linkconfig.py | 4 +- coretk/coretk/graph/edges.py | 71 +++++++++++++++++++++++++-- coretk/coretk/graph/graph.py | 14 +++--- coretk/coretk/graph/linkinfo.py | 75 ----------------------------- coretk/coretk/graph/node.py | 2 +- 6 files changed, 80 insertions(+), 94 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 8b01a468..320c84e0 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -445,7 +445,7 @@ class CoreClient: def start_session(self): nodes = [x.core_node for x in self.canvas_nodes.values()] - links = list(self.links.values()) + links = [x.link for x in self.links.values()] wlan_configs = self.get_wlan_configs_proto() mobility_configs = self.get_mobility_configs_proto() emane_model_configs = self.get_emane_model_configs_proto() @@ -602,7 +602,7 @@ class CoreClient: :return: nothing """ node_protos = [x.core_node for x in self.canvas_nodes.values()] - link_protos = list(self.links.values()) + link_protos = [x.link for x in self.links.values()] if self.state != core_pb2.SessionState.DEFINITION: self.client.set_session_state( self.session_id, core_pb2.SessionState.DEFINITION @@ -813,8 +813,8 @@ class CoreClient: interface_one=src_interface, interface_two=dst_interface, ) - self.links[edge.token] = link - return link + edge.set_link(link) + self.links[edge.token] = edge def get_wlan_configs_proto(self): configs = [] diff --git a/coretk/coretk/dialogs/linkconfig.py b/coretk/coretk/dialogs/linkconfig.py index 6f0bd51a..f96765dc 100644 --- a/coretk/coretk/dialogs/linkconfig.py +++ b/coretk/coretk/dialogs/linkconfig.py @@ -228,7 +228,7 @@ class LinkConfiguration(Dialog): logging.debug("click apply") self.app.canvas.itemconfigure(self.edge.id, width=self.width.get()) self.app.canvas.itemconfigure(self.edge.id, fill=self.color.get()) - link = self.edge.link_info.link + link = self.edge.link bandwidth = self.bandwidth.get() if bandwidth != "": link.options.bandwidth = int(bandwidth) @@ -292,7 +292,7 @@ class LinkConfiguration(Dialog): self.width.set(width) color = self.app.canvas.itemcget(self.edge.id, "fill") self.color.set(color) - link = self.edge.link_info.link + link = self.edge.link if link.HasField("options"): self.bandwidth.set(str(link.options.bandwidth)) self.jitter.set(str(link.options.jitter)) diff --git a/coretk/coretk/graph/edges.py b/coretk/coretk/graph/edges.py index ab9447be..80193cc8 100644 --- a/coretk/coretk/graph/edges.py +++ b/coretk/coretk/graph/edges.py @@ -1,11 +1,14 @@ import logging import tkinter as tk +from tkinter.font import Font from coretk import themes from coretk.dialogs.linkconfig import LinkConfiguration from coretk.graph import tags from coretk.nodeutils import NodeUtils +TEXT_DISTANCE = 0.30 + class CanvasWirelessEdge: def __init__(self, token, position, src, dst, canvas): @@ -46,14 +49,74 @@ class CanvasEdge: self.id = self.canvas.create_line( x1, y1, x2, y2, tags=tags.EDGE, width=self.width, fill="#ff0000" ) + self.text_src = None + self.text_dst = None self.token = None - self.link_info = None + self.font = Font(size=8) + self.link = None self.throughput = None self.set_binding() def set_binding(self): self.canvas.tag_bind(self.id, "", self.create_context) + def set_link(self, link): + self.link = link + self.draw_labels() + + def get_coordinates(self): + x1, y1, x2, y2 = self.canvas.coords(self.id) + v1 = x2 - x1 + v2 = y2 - y1 + ux = TEXT_DISTANCE * v1 + uy = TEXT_DISTANCE * v2 + x1 = x1 + ux + y1 = y1 + uy + x2 = x2 - ux + y2 = y2 - uy + return x1, y1, x2, y2 + + def draw_labels(self): + x1, y1, x2, y2 = self.get_coordinates() + 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_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" + ) + self.text_src = self.canvas.create_text( + x1, + y1, + text=label_one, + justify=tk.CENTER, + font=self.font, + tags=tags.LINK_INFO, + ) + self.text_dst = self.canvas.create_text( + x2, + y2, + text=label_two, + justify=tk.CENTER, + font=self.font, + tags=tags.LINK_INFO, + ) + + def update_labels(self): + """ + Move edge labels based on current position. + + :return: nothing + """ + x1, y1, x2, y2 = self.get_coordinates() + self.canvas.coords(self.text_src, x1, y1) + self.canvas.coords(self.text_dst, x2, y2) + def complete(self, dst): self.dst = dst self.token = tuple(sorted((self.src, self.dst))) @@ -93,9 +156,9 @@ class CanvasEdge: def delete(self): self.canvas.delete(self.id) - if self.link_info: - self.canvas.delete(self.link_info.id1) - self.canvas.delete(self.link_info.id2) + if self.link: + self.canvas.delete(self.text_src) + self.canvas.delete(self.text_dst) def create_context(self, event): logging.debug("create link context") diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 77cc2264..c448ecd1 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -9,7 +9,7 @@ from coretk.dialogs.shapemod import ShapeDialog from coretk.graph import tags from coretk.graph.edges import CanvasEdge, CanvasWirelessEdge from coretk.graph.enums import GraphMode, ScaleOption -from coretk.graph.linkinfo import LinkInfo, Throughput +from coretk.graph.linkinfo import Throughput from coretk.graph.node import CanvasNode from coretk.graph.shape import Shape from coretk.graph.shapeutils import ShapeType, is_draw_shape @@ -237,14 +237,14 @@ class CanvasGraph(tk.Canvas): canvas_node_one.id, self, ) + edge.set_link(link) edge.token = tuple(sorted((canvas_node_one.id, canvas_node_two.id))) edge.dst = canvas_node_two.id edge.check_wireless() canvas_node_one.edges.add(edge) canvas_node_two.edges.add(edge) self.edges[edge.token] = edge - self.core.links[edge.token] = link - edge.link_info = LinkInfo(self, edge, link) + self.core.links[edge.token] = edge if link.HasField("interface_one"): canvas_node_one.interfaces.append(link.interface_one) if link.HasField("interface_two"): @@ -372,8 +372,7 @@ class CanvasGraph(tk.Canvas): node_src.edges.add(edge) node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) - link = self.core.create_link(edge, node_src, node_dst) - edge.link_info = LinkInfo(self, edge, link) + self.core.create_link(edge, node_src, node_dst) def select_object(self, object_id, choose_multiple=False): """ @@ -793,7 +792,7 @@ class CanvasGraph(tk.Canvas): :param CanvasNode dest: destination node :return: nothing """ - if tuple([source.id, dest.id]) not in self.edges: + if (source.id, dest.id) not in self.edges: pos0 = source.core_node.position x0 = pos0.x y0 = pos0.y @@ -802,5 +801,4 @@ class CanvasGraph(tk.Canvas): self.edges[edge.token] = edge self.nodes[source.id].edges.add(edge) self.nodes[dest.id].edges.add(edge) - link = self.core.create_link(edge, source, dest) - edge.link_info = LinkInfo(self, edge, link) + self.core.create_link(edge, source, dest) diff --git a/coretk/coretk/graph/linkinfo.py b/coretk/coretk/graph/linkinfo.py index decca3bb..de4b2c6e 100644 --- a/coretk/coretk/graph/linkinfo.py +++ b/coretk/coretk/graph/linkinfo.py @@ -1,83 +1,8 @@ """ Link information, such as IPv4, IPv6 and throughput drawn in the canvas """ -import tkinter as tk -from tkinter import font from core.api.grpc import core_pb2 -from coretk.graph import tags - -TEXT_DISTANCE = 0.30 - - -class LinkInfo: - def __init__(self, canvas, edge, link): - """ - create an instance of LinkInfo object - :param coretk.graph.Graph canvas: canvas object - :param coretk.graph.CanvasEdge edge: canvas edge onject - :param link: core link to draw info for - """ - self.canvas = canvas - self.edge = edge - self.link = link - self.id1 = None - self.id2 = None - self.font = font.Font(size=8) - self.draw_labels() - - def get_coordinates(self): - x1, y1, x2, y2 = self.canvas.coords(self.edge.id) - v1 = x2 - x1 - v2 = y2 - y1 - ux = TEXT_DISTANCE * v1 - uy = TEXT_DISTANCE * v2 - x1 = x1 + ux - y1 = y1 + uy - x2 = x2 - ux - y2 = y2 - uy - return x1, y1, x2, y2 - - def draw_labels(self): - x1, y1, x2, y2 = self.get_coordinates() - 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_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" - ) - self.id1 = self.canvas.create_text( - x1, - y1, - text=label_one, - justify=tk.CENTER, - font=self.font, - tags=tags.LINK_INFO, - ) - self.id2 = self.canvas.create_text( - x2, - y2, - text=label_two, - justify=tk.CENTER, - font=self.font, - tags=tags.LINK_INFO, - ) - - def recalculate_info(self): - """ - move the node info when the canvas node move - - :return: nothing - """ - x1, y1, x2, y2 = self.get_coordinates() - self.canvas.coords(self.id1, x1, y1) - self.canvas.coords(self.id2, x2, y2) class Throughput: diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 4836d2d2..a5893ff3 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -129,7 +129,7 @@ class CanvasNode: else: self.canvas.coords(edge.id, x1, y1, x, y) self.canvas.throughput_draw.move(edge) - edge.link_info.recalculate_info() + edge.update_labels() for edge in self.wireless_edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) From 8eb4df7b1d0c8af17684141f8aee74b5546978bb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 18 Dec 2019 22:09:00 -0800 Subject: [PATCH 417/462] updated linkconfig to support asymmetric links, updated grpc start session to provide asymmetric links, since they currently depend on being processed as a link edit --- coretk/coretk/coreclient.py | 4 + coretk/coretk/dialogs/linkconfig.py | 127 +++++++++++++++++++------- coretk/coretk/graph/edges.py | 1 + coretk/coretk/graph/graph.py | 51 ++++++----- daemon/core/api/grpc/client.py | 3 + daemon/core/api/grpc/grpcutils.py | 25 ++++- daemon/core/api/grpc/server.py | 3 + daemon/core/nodes/base.py | 1 + daemon/core/nodes/network.py | 1 + daemon/proto/core/api/grpc/core.proto | 1 + 10 files changed, 162 insertions(+), 55 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 320c84e0..8f2d479d 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -452,6 +452,9 @@ class CoreClient: hooks = list(self.hooks.values()) service_configs = self.get_service_configs_proto() file_configs = self.get_service_file_configs_proto() + asymmetric_links = [ + x.asymmetric_link for x in self.links.values() if x.asymmetric_link + ] if self.emane_config: emane_config = {x: self.emane_config[x].value for x in self.emane_config} else: @@ -471,6 +474,7 @@ class CoreClient: mobility_configs, service_configs, file_configs, + asymmetric_links, ) self.set_metadata() process_time = time.perf_counter() - start diff --git a/coretk/coretk/dialogs/linkconfig.py b/coretk/coretk/dialogs/linkconfig.py index f96765dc..cf0daafc 100644 --- a/coretk/coretk/dialogs/linkconfig.py +++ b/coretk/coretk/dialogs/linkconfig.py @@ -5,17 +5,34 @@ import logging import tkinter as tk from tkinter import ttk +from core.api.grpc import core_pb2 from coretk.dialogs.colorpicker import ColorPicker from coretk.dialogs.dialog import Dialog from coretk.themes import PADX, PADY +def get_int(var): + value = var.get() + if value != "": + return int(value) + else: + return None + + +def get_float(var): + value = var.get() + if value != "": + return float(value) + else: + return None + + class LinkConfiguration(Dialog): def __init__(self, master, app, edge): super().__init__(master, app, "Link Configuration", modal=True) self.app = app self.edge = edge - self.is_symmetric = True + self.is_symmetric = edge.link.options.unidirectional is False if self.is_symmetric: self.symmetry_var = tk.StringVar(value=">>") else: @@ -26,16 +43,17 @@ class LinkConfiguration(Dialog): self.jitter = tk.StringVar() self.loss = tk.StringVar() self.duplicate = tk.StringVar() + + self.down_bandwidth = tk.StringVar() + self.down_delay = tk.StringVar() + self.down_jitter = tk.StringVar() + self.down_loss = tk.StringVar() + self.down_duplicate = tk.StringVar() + self.color = tk.StringVar(value="#000000") self.color_button = None self.width = tk.DoubleVar() - self.down_bandwidth = tk.IntVar(value="") - self.down_delay = tk.IntVar(value="") - self.down_jitter = tk.IntVar(value="") - self.down_loss = tk.DoubleVar(value="") - self.down_duplicate = tk.IntVar(value="") - self.load_link_config() self.symmetric_frame = None self.asymmetric_frame = None @@ -113,7 +131,7 @@ class LinkConfiguration(Dialog): validate="key", validatecommand=(self.app.validation.positive_int, "%P"), ) - entry.grid(row=row, column=2, sticky="nsew") + entry.grid(row=row, column=2, sticky="ew", pady=PADY) row = row + 1 label = ttk.Label(frame, text="Delay (us)") @@ -132,7 +150,7 @@ class LinkConfiguration(Dialog): validate="key", validatecommand=(self.app.validation.positive_int, "%P"), ) - entry.grid(row=row, column=2, sticky="ew") + entry.grid(row=row, column=2, sticky="ew", pady=PADY) row = row + 1 label = ttk.Label(frame, text="Jitter (us)") @@ -151,7 +169,7 @@ class LinkConfiguration(Dialog): validate="key", validatecommand=(self.app.validation.positive_int, "%P"), ) - entry.grid(row=row, column=2, sticky="ew") + entry.grid(row=row, column=2, sticky="ew", pady=PADY) row = row + 1 label = ttk.Label(frame, text="Loss (%)") @@ -170,7 +188,7 @@ class LinkConfiguration(Dialog): validate="key", validatecommand=(self.app.validation.positive_float, "%P"), ) - entry.grid(row=row, column=2, sticky="ew") + entry.grid(row=row, column=2, sticky="ew", pady=PADY) row = row + 1 label = ttk.Label(frame, text="Duplicate (%)") @@ -189,7 +207,7 @@ class LinkConfiguration(Dialog): validate="key", validatecommand=(self.app.validation.positive_int, "%P"), ) - entry.grid(row=row, column=2, sticky="ew") + entry.grid(row=row, column=2, sticky="ew", pady=PADY) row = row + 1 label = ttk.Label(frame, text="Color") @@ -229,29 +247,56 @@ class LinkConfiguration(Dialog): self.app.canvas.itemconfigure(self.edge.id, width=self.width.get()) self.app.canvas.itemconfigure(self.edge.id, fill=self.color.get()) link = self.edge.link - bandwidth = self.bandwidth.get() - if bandwidth != "": - link.options.bandwidth = int(bandwidth) - jitter = self.jitter.get() - if jitter != "": - link.options.jitter = int(jitter) - delay = self.delay.get() - if delay != "": - link.options.delay = int(delay) - duplicate = self.duplicate.get() - if duplicate != "": - link.options.dup = int(duplicate) - loss = self.loss.get() - if loss != "": - link.options.per = float(loss) + bandwidth = get_int(self.bandwidth) + jitter = get_int(self.jitter) + delay = get_int(self.delay) + duplicate = get_int(self.duplicate) + loss = get_float(self.loss) + options = core_pb2.LinkOptions( + bandwidth=bandwidth, jitter=jitter, delay=delay, dup=duplicate, per=loss + ) + link.options.CopyFrom(options) + + interface_one = None + if link.HasField("interface_one"): + interface_one = link.interface_one.id + interface_two = None + if link.HasField("interface_two"): + interface_two = link.interface_two.id + + if not self.is_symmetric: + link.options.unidirectional = True + asym_interface_one = None + if interface_one: + asym_interface_one = core_pb2.Interface(id=interface_one) + asym_interface_two = None + if interface_two: + asym_interface_two = core_pb2.Interface(id=interface_two) + down_bandwidth = get_int(self.down_bandwidth) + down_jitter = get_int(self.down_jitter) + down_delay = get_int(self.down_delay) + down_duplicate = get_int(self.down_duplicate) + down_loss = get_float(self.down_loss) + options = core_pb2.LinkOptions( + bandwidth=down_bandwidth, + jitter=down_jitter, + delay=down_delay, + dup=down_duplicate, + per=down_loss, + unidirectional=True, + ) + self.edge.asymmetric_link = core_pb2.Link( + node_one_id=link.node_two_id, + node_two_id=link.node_one_id, + interface_one=asym_interface_one, + interface_two=asym_interface_two, + options=options, + ) + else: + link.options.unidirectional = False + self.edge.asymmetric_link = None if self.app.core.is_runtime() and link.HasField("options"): - interface_one = None - if link.HasField("interface_one"): - interface_one = link.interface_one.id - interface_two = None - if link.HasField("interface_two"): - interface_two = link.interface_two.id session_id = self.app.core.session_id self.app.core.client.edit_link( session_id, @@ -261,6 +306,15 @@ class LinkConfiguration(Dialog): interface_one, interface_two, ) + if self.edge.asymmetric_link: + self.app.core.client.edit_link( + session_id, + link.node_two_id, + link.node_one_id, + self.edge.asymmetric_link.options, + interface_one, + interface_two, + ) self.destroy() @@ -299,3 +353,10 @@ class LinkConfiguration(Dialog): self.duplicate.set(str(link.options.dup)) self.loss.set(str(link.options.per)) self.delay.set(str(link.options.delay)) + if not self.is_symmetric: + asym_link = self.edge.asymmetric_link + self.down_bandwidth.set(str(asym_link.options.bandwidth)) + self.down_jitter.set(str(asym_link.options.jitter)) + self.down_duplicate.set(str(asym_link.options.dup)) + self.down_loss.set(str(asym_link.options.per)) + self.down_delay.set(str(asym_link.options.delay)) diff --git a/coretk/coretk/graph/edges.py b/coretk/coretk/graph/edges.py index 80193cc8..e25a5305 100644 --- a/coretk/coretk/graph/edges.py +++ b/coretk/coretk/graph/edges.py @@ -54,6 +54,7 @@ class CanvasEdge: self.token = None self.font = Font(size=8) self.link = None + self.asymmetric_link = None self.throughput = None self.set_binding() diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index c448ecd1..1d05d7f5 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -201,12 +201,12 @@ class CanvasGraph(tk.Canvas): """ # draw existing nodes for core_node in session.nodes: - logging.info("drawing core node: %s", core_node) # peer to peer node is not drawn on the GUI if NodeUtils.is_ignore_node(core_node.type): continue # draw nodes on the canvas + logging.info("drawing core node: %s", core_node) image = NodeUtils.node_icon(core_node.type, core_node.model) if core_node.icon: try: @@ -222,33 +222,42 @@ class CanvasGraph(tk.Canvas): # draw existing links for link in session.links: + logging.info("drawing link: %s", link) canvas_node_one = self.core.canvas_nodes[link.node_one_id] node_one = canvas_node_one.core_node canvas_node_two = self.core.canvas_nodes[link.node_two_id] node_two = canvas_node_two.core_node + token = tuple(sorted((canvas_node_one.id, canvas_node_two.id))) + if link.type == core_pb2.LinkType.WIRELESS: self.add_wireless_edge(canvas_node_one, canvas_node_two) else: - edge = CanvasEdge( - node_one.position.x, - node_one.position.y, - node_two.position.x, - node_two.position.y, - canvas_node_one.id, - self, - ) - edge.set_link(link) - edge.token = tuple(sorted((canvas_node_one.id, canvas_node_two.id))) - edge.dst = canvas_node_two.id - edge.check_wireless() - canvas_node_one.edges.add(edge) - canvas_node_two.edges.add(edge) - self.edges[edge.token] = edge - self.core.links[edge.token] = edge - if link.HasField("interface_one"): - canvas_node_one.interfaces.append(link.interface_one) - if link.HasField("interface_two"): - canvas_node_two.interfaces.append(link.interface_two) + if token not in self.edges: + edge = CanvasEdge( + node_one.position.x, + node_one.position.y, + node_two.position.x, + node_two.position.y, + canvas_node_one.id, + self, + ) + edge.token = token + edge.dst = canvas_node_two.id + edge.set_link(link) + edge.check_wireless() + canvas_node_one.edges.add(edge) + canvas_node_two.edges.add(edge) + self.edges[edge.token] = edge + self.core.links[edge.token] = edge + if link.HasField("interface_one"): + canvas_node_one.interfaces.append(link.interface_one) + if link.HasField("interface_two"): + canvas_node_two.interfaces.append(link.interface_two) + elif link.options.unidirectional: + edge = self.edges[token] + edge.asymmetric_link = link + else: + logging.error("duplicate link received: %s", link) # raise the nodes so they on top of the links self.tag_raise(tags.NODE) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index bc48c9ab..9aa99349 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -161,6 +161,7 @@ class CoreGrpcClient: mobility_configs=None, service_configs=None, service_file_configs=None, + asymmetric_links=None, ): """ Start a session. @@ -176,6 +177,7 @@ class CoreGrpcClient: :param list mobility_configs: node mobility configurations :param list service_configs: node service configurations :param list service_file_configs: node service file configurations + :param list asymmetric_links: asymmetric links to edit :return: start session response :rtype: core_pb2.StartSessionResponse """ @@ -191,6 +193,7 @@ class CoreGrpcClient: mobility_configs=mobility_configs, service_configs=service_configs, service_file_configs=service_file_configs, + asymmetric_links=asymmetric_links, ) return self.stub.StartSession(request) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 4ea752fd..a3b25541 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -128,7 +128,7 @@ def create_nodes(session, node_protos): def create_links(session, link_protos): """ - Create nodes using a thread pool and wait for completion. + Create links using a thread pool and wait for completion. :param core.emulator.session.Session session: session to create nodes in :param list[core_pb2.Link] link_protos: link proto messages @@ -149,6 +149,29 @@ def create_links(session, link_protos): return results, exceptions +def edit_links(session, link_protos): + """ + Edit links using a thread pool and wait for completion. + + :param core.emulator.session.Session session: session to create nodes in + :param list[core_pb2.Link] link_protos: link proto messages + :return: results and exceptions for created links + :rtype: tuple + """ + funcs = [] + for link_proto in link_protos: + node_one_id = link_proto.node_one_id + node_two_id = link_proto.node_two_id + interface_one, interface_two, options = add_link_data(link_proto) + args = (node_one_id, node_two_id, interface_one.id, interface_two.id, options) + funcs.append((session.update_link, args, {})) + start = time.monotonic() + results, exceptions = utils.threadpool(funcs) + total = time.monotonic() - start + logging.debug("grpc edit links time: %s", total) + return results, exceptions + + def convert_value(value): """ Convert value into string. diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 068d3eeb..bf2a8ac8 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -158,6 +158,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): # create links grpcutils.create_links(session, request.links) + # asymmetric links + grpcutils.edit_links(session, request.asymmetric_links) + # set to instantiation and start session.set_state(EventTypes.INSTANTIATION_STATE) session.instantiate() diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index a663741e..3193d954 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -1053,6 +1053,7 @@ class CoreNetworkBase(NodeBase): message_type=0, node1_id=linked_node.id, node2_id=self.id, + link_type=self.linktype, unidirectional=1, delay=netif.getparam("delay"), bandwidth=netif.getparam("bw"), diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index f0639649..5342215b 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -965,6 +965,7 @@ class PtpNet(CoreNetwork): if unidirectional: link_data = LinkData( message_type=0, + link_type=self.linktype, node1_id=if2.node.id, node2_id=if1.node.id, delay=if2.getparam("delay"), diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index ec7bf49c..050d5e75 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -150,6 +150,7 @@ message StartSessionRequest { repeated MobilityConfig mobility_configs = 9; repeated ServiceConfig service_configs = 10; repeated ServiceFileConfig service_file_configs = 11; + repeated Link asymmetric_links = 12; } message StartSessionResponse { From 43c8b9e285cae182b6ab233a577d366d44a15f04 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 19 Dec 2019 08:46:21 -0800 Subject: [PATCH 418/462] marker tool --- coretk/coretk/data/icons/markerclear.png | Bin 0 -> 1370 bytes coretk/coretk/dialogs/marker.py | 22 ++++++++++++++++++---- coretk/coretk/graph/graph.py | 9 +++++---- coretk/coretk/toolbar.py | 1 + 4 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 coretk/coretk/data/icons/markerclear.png diff --git a/coretk/coretk/data/icons/markerclear.png b/coretk/coretk/data/icons/markerclear.png new file mode 100644 index 0000000000000000000000000000000000000000..6f58c0059fa6447d91b1bbeb9d9153ab22af4e95 GIT binary patch literal 1370 zcmV-g1*Q6lP)Kcxg%+F)2cF(Mz(F z;MuF7f>zU}RC+MPG-*Kui5DSWgj#DNf|tZY^N8;_SrPb>p z+fhS|jipL!x2FN|W|#q34~bcKV0D}>!c!@CRQu)dA0X&V>D#_3)pDg7aDVvZX%ga^ zYNyQz0eN?MWkI1%4l#DDeSOXSRRD)%Ynzgom@BZiVV+P^h*??L9B8FajM4oIaE>C?#_hLby-NMDUe>+GDm_aJj& z8qR@(a1I;<0F*y?52)2*>(lAUAu~P+IE0O<^YCu|7F%C|#-pp_?cw{R`v=kS>e<+4 zg42^jW?~AI&4M~zWX?^($sLKMufSu{(dka%`@|(-d-(Nn7$6~W0lS6o6Sah$!mp1< zvxLM2w7r<$HGH3R{~$VEc`NkQ(Qe`U=#-GSfXdZ*Y~EN7^x5=t&xg7?3KIUq2&I0) zko-Bo^H83<99)9p;YcQYv0lQ))C9bnzf1KI1$wT6VwPY__<+tEv6v*-6h5H)PB>x- zwuKMq763#h!N%|b69fRECD|lBA23M( zpd=xE;#nZw_-mLLK48irXgiXJBb&nqOcVgfOV|aI!v{LnavSKLhw_KB zfoFnu`xYRQeLggC0p<568~!RK(#@2Bhoj1;AEUZ>HGF@PBraep+(-X3D3+b91`>3I z4`@0GKT1NZ+lV_!5CWC?Ic?zs8ZSftFCmdV-0c#CK;@HI=GsjW;`G?4x&MvP=WvX33vmq-$`&v+|!vZwJ0yM({G{XWk zqw>P;{aw<>zjy6?!d}~YH{ct=+aQ%CNAXP^ju27sD_N!!QiPFbu;m c3}X-f0nFy52u5{MYybcN07*qoM6N<$f=T6tMF0Q* literal 0 HcmV?d00001 diff --git a/coretk/coretk/dialogs/marker.py b/coretk/coretk/dialogs/marker.py index feb8bd09..5648a89b 100644 --- a/coretk/coretk/dialogs/marker.py +++ b/coretk/coretk/dialogs/marker.py @@ -24,10 +24,14 @@ class Marker(Dialog): def draw(self): button = ttk.Button(self.top, text="clear", command=self.clear_marker) - button.grid(row=0, column=0) + button.grid(row=0, column=0, sticky="nsew") frame = ttk.Frame(self.top) - frame.grid(row=1, column=0) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=1, column=0, sticky="nsew") + label = ttk.Label(frame, text="Thickness: ") + label.grid(row=0, column=0, sticky="nsew") combobox = ttk.Combobox( frame, textvariable=self.marker_thickness, @@ -36,8 +40,14 @@ class Marker(Dialog): ) combobox.grid(row=0, column=1, sticky="nsew") combobox.bind("<>", self.change_thickness) - label = ttk.Label(self.top, background=self.color) - label.grid(row=2, column=0, sticky="nsew") + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=2, column=0, sticky="nsew") + label = ttk.Label(frame, text="Color: ") + label.grid(row=0, column=0, sticky="nsew") + label = ttk.Label(frame, background=self.color) + label.grid(row=0, column=1, sticky="nsew") label.bind("", self.change_color) def clear_marker(self): @@ -57,3 +67,7 @@ class Marker(Dialog): def close_marker(self, event): logging.debug("destroy marker dialog") self.app.toolbar.marker_tool = None + + def position(self): + print(self.winfo_width(), self.winfo_height()) + self.geometry("+{}+{}".format(self.app.master.winfo_x, self.app.master.winfo_y)) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index e0878f20..996c85eb 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -499,7 +499,7 @@ class CanvasGraph(tk.Canvas): x, y = self.coords(selected) self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) - if self.mode == GraphMode.ANNOTATION and selected is None: + if self.mode == GraphMode.ANNOTATION: if is_marker(self.annotation_type): r = self.app.toolbar.marker_tool.radius self.create_oval( @@ -511,7 +511,8 @@ class CanvasGraph(tk.Canvas): outline="", tags="marker", ) - else: + return + if selected is None: shape = Shape(self.app, self, self.annotation_type, x, y) self.selected = shape.id self.shape_drawing = True @@ -587,8 +588,7 @@ class CanvasGraph(tk.Canvas): shape = self.shapes[self.selected] shape.shape_motion(x, y) elif is_marker(self.annotation_type): - marker_tool = self.app.toolbar.marker_tool - r = marker_tool.radius + r = self.app.toolbar.marker_tool.radius self.create_oval( x - r, y - r, @@ -598,6 +598,7 @@ class CanvasGraph(tk.Canvas): outline="", tags="marker", ) + return if self.mode == GraphMode.EDGE: return diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 0769380a..c32b0d75 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -420,6 +420,7 @@ class Toolbar(ttk.Frame): logging.debug("Click on marker button") dialog = Marker(self.master, self.app) dialog.show() + # dialog.position() def click_two_node_button(self): logging.debug("Click TWONODE button") From b8b4b7dcce025ea3261b5020186a58092845634b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 19 Dec 2019 08:55:03 -0800 Subject: [PATCH 419/462] removed unwanted log file --- output.txt | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 output.txt diff --git a/output.txt b/output.txt deleted file mode 100644 index ee11dc37..00000000 --- a/output.txt +++ /dev/null @@ -1,23 +0,0 @@ -./netns/vcmdmodule.c:static PyObject *VCmd_popen(VCmd *self, PyObject *args, PyObject *kwds) -./netns/vcmdmodule.c: {"popen", (PyCFunction)VCmd_popen, METH_VARARGS | METH_KEYWORDS, -./netns/vcmdmodule.c: "popen(args...) -> (VCmdWait, cmdin, cmdout, cmderr)\n\n" -./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) -./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: def popen(self, args): -./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: Execute a popen command against the node. -./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: :return: popen object, stdin, stdout, and stderr -./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: logging.debug("popen: %s", cmd) -./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) -./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) -./daemon/tests/test_core.py: p, stdin, stdout, stderr = client.popen(command) -./daemon/tests/test_core.py: p, stdin, stdout, stderr = client.popen(command) -./daemon/examples/netns/ospfmanetmdrtest.py: """ Exceute call to node.popen(). """ -./daemon/examples/netns/ospfmanetmdrtest.py: self.id, self.stdin, self.out, self.err = self.node.client.popen(self.args) -./daemon/examples/netns/ospfmanetmdrtest.py: self.id, self.stdin, self.out, self.err = self.node.client.popen(args) -./daemon/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) -./daemon/core/nodes/client.py: def popen(self, args): -./daemon/core/nodes/client.py: Execute a popen command against the node. -./daemon/core/nodes/client.py: :return: popen object, stdin, stdout, and stderr -./daemon/core/nodes/client.py: logging.debug("popen: %s", cmd) -./daemon/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) -./daemon/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) -./corefx/src/main/resources/js/leaflet.js:!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i(t.L={})}(this,function(t){"use strict";function i(t){var i,e,n,o;for(e=1,n=arguments.length;e=0}function B(t,i,e,n){return"touchstart"===i?O(t,e,n):"touchmove"===i?W(t,e,n):"touchend"===i&&H(t,e,n),this}function I(t,i,e){var n=t["_leaflet_"+i+e];return"touchstart"===i?t.removeEventListener(te,n,!1):"touchmove"===i?t.removeEventListener(ie,n,!1):"touchend"===i&&(t.removeEventListener(ee,n,!1),t.removeEventListener(ne,n,!1)),this}function O(t,i,n){var o=e(function(t){if("mouse"!==t.pointerType&&t.MSPOINTER_TYPE_MOUSE&&t.pointerType!==t.MSPOINTER_TYPE_MOUSE){if(!(oe.indexOf(t.target.tagName)<0))return;Pt(t)}j(t,i)});t["_leaflet_touchstart"+n]=o,t.addEventListener(te,o,!1),re||(document.documentElement.addEventListener(te,R,!0),document.documentElement.addEventListener(ie,N,!0),document.documentElement.addEventListener(ee,D,!0),document.documentElement.addEventListener(ne,D,!0),re=!0)}function R(t){se[t.pointerId]=t,ae++}function N(t){se[t.pointerId]&&(se[t.pointerId]=t)}function D(t){delete se[t.pointerId],ae--}function j(t,i){t.touches=[];for(var e in se)t.touches.push(se[e]);t.changedTouches=[t],i(t)}function W(t,i,e){var n=function(t){(t.pointerType!==t.MSPOINTER_TYPE_MOUSE&&"mouse"!==t.pointerType||0!==t.buttons)&&j(t,i)};t["_leaflet_touchmove"+e]=n,t.addEventListener(ie,n,!1)}function H(t,i,e){var n=function(t){j(t,i)};t["_leaflet_touchend"+e]=n,t.addEventListener(ee,n,!1),t.addEventListener(ne,n,!1)}function F(t,i,e){function n(t){var i;if(Vi){if(!bi||"mouse"===t.pointerType)return;i=ae}else i=t.touches.length;if(!(i>1)){var e=Date.now(),n=e-(s||e);r=t.touches?t.touches[0]:t,a=n>0&&n<=h,s=e}}function o(t){if(a&&!r.cancelBubble){if(Vi){if(!bi||"mouse"===t.pointerType)return;var e,n,o={};for(n in r)e=r[n],o[n]=e&&e.bind?e.bind(r):e;r=o}r.type="dblclick",i(r),s=null}}var s,r,a=!1,h=250;return t[le+he+e]=n,t[le+ue+e]=o,t[le+"dblclick"+e]=i,t.addEventListener(he,n,!1),t.addEventListener(ue,o,!1),t.addEventListener("dblclick",i,!1),this}function U(t,i){var e=t[le+he+i],n=t[le+ue+i],o=t[le+"dblclick"+i];return t.removeEventListener(he,e,!1),t.removeEventListener(ue,n,!1),bi||t.removeEventListener("dblclick",o,!1),this}function V(t){return"string"==typeof t?document.getElementById(t):t}function q(t,i){var e=t.style[i]||t.currentStyle&&t.currentStyle[i];if((!e||"auto"===e)&&document.defaultView){var n=document.defaultView.getComputedStyle(t,null);e=n?n[i]:null}return"auto"===e?null:e}function G(t,i,e){var n=document.createElement(t);return n.className=i||"",e&&e.appendChild(n),n}function K(t){var i=t.parentNode;i&&i.removeChild(t)}function Y(t){for(;t.firstChild;)t.removeChild(t.firstChild)}function X(t){var i=t.parentNode;i.lastChild!==t&&i.appendChild(t)}function J(t){var i=t.parentNode;i.firstChild!==t&&i.insertBefore(t,i.firstChild)}function $(t,i){if(void 0!==t.classList)return t.classList.contains(i);var e=et(t);return e.length>0&&new RegExp("(^|\\s)"+i+"(\\s|$)").test(e)}function Q(t,i){if(void 0!==t.classList)for(var e=u(i),n=0,o=e.length;n100&&n<500||t.target._simulatedClick&&!t._simulated?Lt(t):(ge=e,i(t))}function Zt(t,i){if(!i||!t.length)return t.slice();var e=i*i;return t=Bt(t,e),t=kt(t,e)}function Et(t,i,e){return Math.sqrt(Dt(t,i,e,!0))}function kt(t,i){var e=t.length,n=new(typeof Uint8Array!=void 0+""?Uint8Array:Array)(e);n[0]=n[e-1]=1,At(t,n,i,0,e-1);var o,s=[];for(o=0;oh&&(s=r,h=a);h>e&&(i[s]=1,At(t,i,e,n,s),At(t,i,e,s,o))}function Bt(t,i){for(var e=[t[0]],n=1,o=0,s=t.length;ni&&(e.push(t[n]),o=n);return oi.max.x&&(e|=2),t.yi.max.y&&(e|=8),e}function Nt(t,i){var e=i.x-t.x,n=i.y-t.y;return e*e+n*n}function Dt(t,i,e,n){var o,s=i.x,r=i.y,a=e.x-s,h=e.y-r,u=a*a+h*h;return u>0&&((o=((t.x-s)*a+(t.y-r)*h)/u)>1?(s=e.x,r=e.y):o>0&&(s+=a*o,r+=h*o)),a=t.x-s,h=t.y-r,n?a*a+h*h:new x(s,r)}function jt(t){return!oi(t[0])||"object"!=typeof t[0][0]&&void 0!==t[0][0]}function Wt(t){return console.warn("Deprecated use of _flat, please use L.LineUtil.isFlat instead."),jt(t)}function Ht(t,i,e){var n,o,s,r,a,h,u,l,c,_=[1,4,2,8];for(o=0,u=t.length;o0?Math.floor(t):Math.ceil(t)};x.prototype={clone:function(){return new x(this.x,this.y)},add:function(t){return this.clone()._add(w(t))},_add:function(t){return this.x+=t.x,this.y+=t.y,this},subtract:function(t){return this.clone()._subtract(w(t))},_subtract:function(t){return this.x-=t.x,this.y-=t.y,this},divideBy:function(t){return this.clone()._divideBy(t)},_divideBy:function(t){return this.x/=t,this.y/=t,this},multiplyBy:function(t){return this.clone()._multiplyBy(t)},_multiplyBy:function(t){return this.x*=t,this.y*=t,this},scaleBy:function(t){return new x(this.x*t.x,this.y*t.y)},unscaleBy:function(t){return new x(this.x/t.x,this.y/t.y)},round:function(){return this.clone()._round()},_round:function(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this},floor:function(){return this.clone()._floor()},_floor:function(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this},ceil:function(){return this.clone()._ceil()},_ceil:function(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this},trunc:function(){return this.clone()._trunc()},_trunc:function(){return this.x=_i(this.x),this.y=_i(this.y),this},distanceTo:function(t){var i=(t=w(t)).x-this.x,e=t.y-this.y;return Math.sqrt(i*i+e*e)},equals:function(t){return(t=w(t)).x===this.x&&t.y===this.y},contains:function(t){return t=w(t),Math.abs(t.x)<=Math.abs(this.x)&&Math.abs(t.y)<=Math.abs(this.y)},toString:function(){return"Point("+a(this.x)+", "+a(this.y)+")"}},P.prototype={extend:function(t){return t=w(t),this.min||this.max?(this.min.x=Math.min(t.x,this.min.x),this.max.x=Math.max(t.x,this.max.x),this.min.y=Math.min(t.y,this.min.y),this.max.y=Math.max(t.y,this.max.y)):(this.min=t.clone(),this.max=t.clone()),this},getCenter:function(t){return new x((this.min.x+this.max.x)/2,(this.min.y+this.max.y)/2,t)},getBottomLeft:function(){return new x(this.min.x,this.max.y)},getTopRight:function(){return new x(this.max.x,this.min.y)},getTopLeft:function(){return this.min},getBottomRight:function(){return this.max},getSize:function(){return this.max.subtract(this.min)},contains:function(t){var i,e;return(t="number"==typeof t[0]||t instanceof x?w(t):b(t))instanceof P?(i=t.min,e=t.max):i=e=t,i.x>=this.min.x&&e.x<=this.max.x&&i.y>=this.min.y&&e.y<=this.max.y},intersects:function(t){t=b(t);var i=this.min,e=this.max,n=t.min,o=t.max,s=o.x>=i.x&&n.x<=e.x,r=o.y>=i.y&&n.y<=e.y;return s&&r},overlaps:function(t){t=b(t);var i=this.min,e=this.max,n=t.min,o=t.max,s=o.x>i.x&&n.xi.y&&n.y=n.lat&&e.lat<=o.lat&&i.lng>=n.lng&&e.lng<=o.lng},intersects:function(t){t=z(t);var i=this._southWest,e=this._northEast,n=t.getSouthWest(),o=t.getNorthEast(),s=o.lat>=i.lat&&n.lat<=e.lat,r=o.lng>=i.lng&&n.lng<=e.lng;return s&&r},overlaps:function(t){t=z(t);var i=this._southWest,e=this._northEast,n=t.getSouthWest(),o=t.getNorthEast(),s=o.lat>i.lat&&n.lati.lng&&n.lng1,Xi=!!document.createElement("canvas").getContext,Ji=!(!document.createElementNS||!E("svg").createSVGRect),$i=!Ji&&function(){try{var t=document.createElement("div");t.innerHTML='';var i=t.firstChild;return i.style.behavior="url(#default#VML)",i&&"object"==typeof i.adj}catch(t){return!1}}(),Qi=(Object.freeze||Object)({ie:Pi,ielt9:Li,edge:bi,webkit:Ti,android:zi,android23:Mi,androidStock:Si,opera:Zi,chrome:Ei,gecko:ki,safari:Ai,phantom:Bi,opera12:Ii,win:Oi,ie3d:Ri,webkit3d:Ni,gecko3d:Di,any3d:ji,mobile:Wi,mobileWebkit:Hi,mobileWebkit3d:Fi,msPointer:Ui,pointer:Vi,touch:qi,mobileOpera:Gi,mobileGecko:Ki,retina:Yi,canvas:Xi,svg:Ji,vml:$i}),te=Ui?"MSPointerDown":"pointerdown",ie=Ui?"MSPointerMove":"pointermove",ee=Ui?"MSPointerUp":"pointerup",ne=Ui?"MSPointerCancel":"pointercancel",oe=["INPUT","SELECT","OPTION"],se={},re=!1,ae=0,he=Ui?"MSPointerDown":Vi?"pointerdown":"touchstart",ue=Ui?"MSPointerUp":Vi?"pointerup":"touchend",le="_leaflet_",ce=st(["transform","webkitTransform","OTransform","MozTransform","msTransform"]),_e=st(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),de="webkitTransition"===_e||"OTransition"===_e?_e+"End":"transitionend";if("onselectstart"in document)fi=function(){mt(window,"selectstart",Pt)},gi=function(){ft(window,"selectstart",Pt)};else{var pe=st(["userSelect","WebkitUserSelect","OUserSelect","MozUserSelect","msUserSelect"]);fi=function(){if(pe){var t=document.documentElement.style;vi=t[pe],t[pe]="none"}},gi=function(){pe&&(document.documentElement.style[pe]=vi,vi=void 0)}}var me,fe,ge,ve=(Object.freeze||Object)({TRANSFORM:ce,TRANSITION:_e,TRANSITION_END:de,get:V,getStyle:q,create:G,remove:K,empty:Y,toFront:X,toBack:J,hasClass:$,addClass:Q,removeClass:tt,setClass:it,getClass:et,setOpacity:nt,testProp:st,setTransform:rt,setPosition:at,getPosition:ht,disableTextSelection:fi,enableTextSelection:gi,disableImageDrag:ut,enableImageDrag:lt,preventOutline:ct,restoreOutline:_t,getSizedParentNode:dt,getScale:pt}),ye="_leaflet_events",xe=Oi&&Ei?2*window.devicePixelRatio:ki?window.devicePixelRatio:1,we={},Pe=(Object.freeze||Object)({on:mt,off:ft,stopPropagation:yt,disableScrollPropagation:xt,disableClickPropagation:wt,preventDefault:Pt,stop:Lt,getMousePosition:bt,getWheelDelta:Tt,fakeStop:zt,skipped:Mt,isExternalTarget:Ct,addListener:mt,removeListener:ft}),Le=ci.extend({run:function(t,i,e,n){this.stop(),this._el=t,this._inProgress=!0,this._duration=e||.25,this._easeOutPower=1/Math.max(n||.5,.2),this._startPos=ht(t),this._offset=i.subtract(this._startPos),this._startTime=+new Date,this.fire("start"),this._animate()},stop:function(){this._inProgress&&(this._step(!0),this._complete())},_animate:function(){this._animId=f(this._animate,this),this._step()},_step:function(t){var i=+new Date-this._startTime,e=1e3*this._duration;ithis.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,i){this._enforcingBounds=!0;var e=this.getCenter(),n=this._limitCenter(e,this._zoom,z(t));return e.equals(n)||this.panTo(n,i),this._enforcingBounds=!1,this},invalidateSize:function(t){if(!this._loaded)return this;t=i({animate:!1,pan:!0},!0===t?{animate:!0}:t);var n=this.getSize();this._sizeChanged=!0,this._lastCenter=null;var o=this.getSize(),s=n.divideBy(2).round(),r=o.divideBy(2).round(),a=s.subtract(r);return a.x||a.y?(t.animate&&t.pan?this.panBy(a):(t.pan&&this._rawPanBy(a),this.fire("move"),t.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(e(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:n,newSize:o})):this},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(t){if(t=this._locateOptions=i({timeout:1e4,watch:!1},t),!("geolocation"in navigator))return this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this;var n=e(this._handleGeolocationResponse,this),o=e(this._handleGeolocationError,this);return t.watch?this._locationWatchId=navigator.geolocation.watchPosition(n,o,t):navigator.geolocation.getCurrentPosition(n,o,t),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var i=t.code,e=t.message||(1===i?"permission denied":2===i?"position unavailable":"timeout");this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:i,message:"Geolocation error: "+e+"."})},_handleGeolocationResponse:function(t){var i=new M(t.coords.latitude,t.coords.longitude),e=i.toBounds(2*t.coords.accuracy),n=this._locateOptions;if(n.setView){var o=this.getBoundsZoom(e);this.setView(i,n.maxZoom?Math.min(o,n.maxZoom):o)}var s={latlng:i,bounds:e,timestamp:t.timestamp};for(var r in t.coords)"number"==typeof t.coords[r]&&(s[r]=t.coords[r]);this.fire("locationfound",s)},addHandler:function(t,i){if(!i)return this;var e=this[t]=new i(this);return this._handlers.push(e),this.options[t]&&e.enable(),this},remove:function(){if(this._initEvents(!0),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch(t){this._container._leaflet_id=void 0,this._containerId=void 0}void 0!==this._locationWatchId&&this.stopLocate(),this._stop(),K(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._resizeRequest&&(g(this._resizeRequest),this._resizeRequest=null),this._clearHandlers(),this._loaded&&this.fire("unload");var t;for(t in this._layers)this._layers[t].remove();for(t in this._panes)K(this._panes[t]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(t,i){var e=G("div","leaflet-pane"+(t?" leaflet-"+t.replace("Pane","")+"-pane":""),i||this._mapPane);return t&&(this._panes[t]=e),e},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter:this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var t=this.getPixelBounds();return new T(this.unproject(t.getBottomLeft()),this.unproject(t.getTopRight()))},getMinZoom:function(){return void 0===this.options.minZoom?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return void 0===this.options.maxZoom?void 0===this._layersMaxZoom?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(t,i,e){t=z(t),e=w(e||[0,0]);var n=this.getZoom()||0,o=this.getMinZoom(),s=this.getMaxZoom(),r=t.getNorthWest(),a=t.getSouthEast(),h=this.getSize().subtract(e),u=b(this.project(a,n),this.project(r,n)).getSize(),l=ji?this.options.zoomSnap:1,c=h.x/u.x,_=h.y/u.y,d=i?Math.max(c,_):Math.min(c,_);return n=this.getScaleZoom(d,n),l&&(n=Math.round(n/(l/100))*(l/100),n=i?Math.ceil(n/l)*l:Math.floor(n/l)*l),Math.max(o,Math.min(s,n))},getSize:function(){return this._size&&!this._sizeChanged||(this._size=new x(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(t,i){var e=this._getTopLeftPoint(t,i);return new P(e,e.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(t){return this.options.crs.getProjectedBounds(void 0===t?this.getZoom():t)},getPane:function(t){return"string"==typeof t?this._panes[t]:t},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(t,i){var e=this.options.crs;return i=void 0===i?this._zoom:i,e.scale(t)/e.scale(i)},getScaleZoom:function(t,i){var e=this.options.crs;i=void 0===i?this._zoom:i;var n=e.zoom(t*e.scale(i));return isNaN(n)?1/0:n},project:function(t,i){return i=void 0===i?this._zoom:i,this.options.crs.latLngToPoint(C(t),i)},unproject:function(t,i){return i=void 0===i?this._zoom:i,this.options.crs.pointToLatLng(w(t),i)},layerPointToLatLng:function(t){var i=w(t).add(this.getPixelOrigin());return this.unproject(i)},latLngToLayerPoint:function(t){return this.project(C(t))._round()._subtract(this.getPixelOrigin())},wrapLatLng:function(t){return this.options.crs.wrapLatLng(C(t))},wrapLatLngBounds:function(t){return this.options.crs.wrapLatLngBounds(z(t))},distance:function(t,i){return this.options.crs.distance(C(t),C(i))},containerPointToLayerPoint:function(t){return w(t).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(t){return w(t).add(this._getMapPanePos())},containerPointToLatLng:function(t){var i=this.containerPointToLayerPoint(w(t));return this.layerPointToLatLng(i)},latLngToContainerPoint:function(t){return this.layerPointToContainerPoint(this.latLngToLayerPoint(C(t)))},mouseEventToContainerPoint:function(t){return bt(t,this._container)},mouseEventToLayerPoint:function(t){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t))},mouseEventToLatLng:function(t){return this.layerPointToLatLng(this.mouseEventToLayerPoint(t))},_initContainer:function(t){var i=this._container=V(t);if(!i)throw new Error("Map container not found.");if(i._leaflet_id)throw new Error("Map container is already initialized.");mt(i,"scroll",this._onScroll,this),this._containerId=n(i)},_initLayout:function(){var t=this._container;this._fadeAnimated=this.options.fadeAnimation&&ji,Q(t,"leaflet-container"+(qi?" leaflet-touch":"")+(Yi?" leaflet-retina":"")+(Li?" leaflet-oldie":"")+(Ai?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":""));var i=q(t,"position");"absolute"!==i&&"relative"!==i&&"fixed"!==i&&(t.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var t=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),at(this._mapPane,new x(0,0)),this.createPane("tilePane"),this.createPane("shadowPane"),this.createPane("overlayPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(Q(t.markerPane,"leaflet-zoom-hide"),Q(t.shadowPane,"leaflet-zoom-hide"))},_resetView:function(t,i){at(this._mapPane,new x(0,0));var e=!this._loaded;this._loaded=!0,i=this._limitZoom(i),this.fire("viewprereset");var n=this._zoom!==i;this._moveStart(n,!1)._move(t,i)._moveEnd(n),this.fire("viewreset"),e&&this.fire("load")},_moveStart:function(t,i){return t&&this.fire("zoomstart"),i||this.fire("movestart"),this},_move:function(t,i,e){void 0===i&&(i=this._zoom);var n=this._zoom!==i;return this._zoom=i,this._lastCenter=t,this._pixelOrigin=this._getNewPixelOrigin(t),(n||e&&e.pinch)&&this.fire("zoom",e),this.fire("move",e)},_moveEnd:function(t){return t&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return g(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(t){at(this._mapPane,this._getMapPanePos().subtract(t))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(t){this._targets={},this._targets[n(this._container)]=this;var i=t?ft:mt;i(this._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress",this._handleDOMEvent,this),this.options.trackResize&&i(window,"resize",this._onResize,this),ji&&this.options.transform3DLimit&&(t?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){g(this._resizeRequest),this._resizeRequest=f(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var t=this._getMapPanePos();Math.max(Math.abs(t.x),Math.abs(t.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,i){for(var e,o=[],s="mouseout"===i||"mouseover"===i,r=t.target||t.srcElement,a=!1;r;){if((e=this._targets[n(r)])&&("click"===i||"preclick"===i)&&!t._simulated&&this._draggableMoved(e)){a=!0;break}if(e&&e.listens(i,!0)){if(s&&!Ct(r,t))break;if(o.push(e),s)break}if(r===this._container)break;r=r.parentNode}return o.length||a||s||!Ct(r,t)||(o=[this]),o},_handleDOMEvent:function(t){if(this._loaded&&!Mt(t)){var i=t.type;"mousedown"!==i&&"keypress"!==i||ct(t.target||t.srcElement),this._fireDOMEvent(t,i)}},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,e,n){if("click"===t.type){var o=i({},t);o.type="preclick",this._fireDOMEvent(o,o.type,n)}if(!t._stopped&&(n=(n||[]).concat(this._findEventTargets(t,e))).length){var s=n[0];"contextmenu"===e&&s.listens(e,!0)&&Pt(t);var r={originalEvent:t};if("keypress"!==t.type){var a=s.getLatLng&&(!s._radius||s._radius<=10);r.containerPoint=a?this.latLngToContainerPoint(s.getLatLng()):this.mouseEventToContainerPoint(t),r.layerPoint=this.containerPointToLayerPoint(r.containerPoint),r.latlng=a?s.getLatLng():this.layerPointToLatLng(r.layerPoint)}for(var h=0;h0?Math.round(t-i)/2:Math.max(0,Math.ceil(t))-Math.max(0,Math.floor(i))},_limitZoom:function(t){var i=this.getMinZoom(),e=this.getMaxZoom(),n=ji?this.options.zoomSnap:1;return n&&(t=Math.round(t/n)*n),Math.max(i,Math.min(e,t))},_onPanTransitionStep:function(){this.fire("move")},_onPanTransitionEnd:function(){tt(this._mapPane,"leaflet-pan-anim"),this.fire("moveend")},_tryAnimatedPan:function(t,i){var e=this._getCenterOffset(t)._trunc();return!(!0!==(i&&i.animate)&&!this.getSize().contains(e))&&(this.panBy(e,i),!0)},_createAnimProxy:function(){var t=this._proxy=G("div","leaflet-proxy leaflet-zoom-animated");this._panes.mapPane.appendChild(t),this.on("zoomanim",function(t){var i=ce,e=this._proxy.style[i];rt(this._proxy,this.project(t.center,t.zoom),this.getZoomScale(t.zoom,1)),e===this._proxy.style[i]&&this._animatingZoom&&this._onZoomTransitionEnd()},this),this.on("load moveend",function(){var t=this.getCenter(),i=this.getZoom();rt(this._proxy,this.project(t,i),this.getZoomScale(i,1))},this),this._on("unload",this._destroyAnimProxy,this)},_destroyAnimProxy:function(){K(this._proxy),delete this._proxy},_catchTransitionEnd:function(t){this._animatingZoom&&t.propertyName.indexOf("transform")>=0&&this._onZoomTransitionEnd()},_nothingToAnimate:function(){return!this._container.getElementsByClassName("leaflet-zoom-animated").length},_tryAnimatedZoom:function(t,i,e){if(this._animatingZoom)return!0;if(e=e||{},!this._zoomAnimated||!1===e.animate||this._nothingToAnimate()||Math.abs(i-this._zoom)>this.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(i),o=this._getCenterOffset(t)._divideBy(1-1/n);return!(!0!==e.animate&&!this.getSize().contains(o))&&(f(function(){this._moveStart(!0,!1)._animateZoom(t,i,!0)},this),!0)},_animateZoom:function(t,i,n,o){this._mapPane&&(n&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=i,Q(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:i,noUpdate:o}),setTimeout(e(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&tt(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom),f(function(){this._moveEnd(!0)},this))}}),Te=v.extend({options:{position:"topright"},initialize:function(t){l(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var i=this._map;return i&&i.removeControl(this),this.options.position=t,i&&i.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var i=this._container=this.onAdd(t),e=this.getPosition(),n=t._controlCorners[e];return Q(i,"leaflet-control"),-1!==e.indexOf("bottom")?n.insertBefore(i,n.firstChild):n.appendChild(i),this},remove:function(){return this._map?(K(this._container),this.onRemove&&this.onRemove(this._map),this._map=null,this):this},_refocusOnMap:function(t){this._map&&t&&t.screenX>0&&t.screenY>0&&this._map.getContainer().focus()}}),ze=function(t){return new Te(t)};be.include({addControl:function(t){return t.addTo(this),this},removeControl:function(t){return t.remove(),this},_initControlPos:function(){function t(t,o){var s=e+t+" "+e+o;i[t+o]=G("div",s,n)}var i=this._controlCorners={},e="leaflet-",n=this._controlContainer=G("div",e+"control-container",this._container);t("top","left"),t("top","right"),t("bottom","left"),t("bottom","right")},_clearControlPos:function(){for(var t in this._controlCorners)K(this._controlCorners[t]);K(this._controlContainer),delete this._controlCorners,delete this._controlContainer}});var Me=Te.extend({options:{collapsed:!0,position:"topright",autoZIndex:!0,hideSingleBase:!1,sortLayers:!1,sortFunction:function(t,i,e,n){return e1,this._baseLayersList.style.display=t?"":"none"),this._separator.style.display=i&&t?"":"none",this},_onLayerChange:function(t){this._handlingClick||this._update();var i=this._getLayer(n(t.target)),e=i.overlay?"add"===t.type?"overlayadd":"overlayremove":"add"===t.type?"baselayerchange":null;e&&this._map.fire(e,i)},_createRadioElement:function(t,i){var e='",n=document.createElement("div");return n.innerHTML=e,n.firstChild},_addItem:function(t){var i,e=document.createElement("label"),o=this._map.hasLayer(t.layer);t.overlay?((i=document.createElement("input")).type="checkbox",i.className="leaflet-control-layers-selector",i.defaultChecked=o):i=this._createRadioElement("leaflet-base-layers",o),this._layerControlInputs.push(i),i.layerId=n(t.layer),mt(i,"click",this._onInputClick,this);var s=document.createElement("span");s.innerHTML=" "+t.name;var r=document.createElement("div");return e.appendChild(r),r.appendChild(i),r.appendChild(s),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(e),this._checkDisabledLayers(),e},_onInputClick:function(){var t,i,e=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=e.length-1;s>=0;s--)t=e[s],i=this._getLayer(t.layerId).layer,t.checked?n.push(i):t.checked||o.push(i);for(s=0;s=0;o--)t=e[o],i=this._getLayer(t.layerId).layer,t.disabled=void 0!==i.options.minZoom&&ni.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expand:function(){return this.expand()},_collapse:function(){return this.collapse()}}),Ce=Te.extend({options:{position:"topleft",zoomInText:"+",zoomInTitle:"Zoom in",zoomOutText:"−",zoomOutTitle:"Zoom out"},onAdd:function(t){var i="leaflet-control-zoom",e=G("div",i+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,i+"-in",e,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,i+"-out",e,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),e},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,i,e,n,o){var s=G("a",e,n);return s.innerHTML=t,s.href="#",s.title=i,s.setAttribute("role","button"),s.setAttribute("aria-label",i),wt(s),mt(s,"click",Lt),mt(s,"click",o,this),mt(s,"click",this._refocusOnMap,this),s},_updateDisabled:function(){var t=this._map,i="leaflet-disabled";tt(this._zoomInButton,i),tt(this._zoomOutButton,i),(this._disabled||t._zoom===t.getMinZoom())&&Q(this._zoomOutButton,i),(this._disabled||t._zoom===t.getMaxZoom())&&Q(this._zoomInButton,i)}});be.mergeOptions({zoomControl:!0}),be.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new Ce,this.addControl(this.zoomControl))});var Se=Te.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var i=G("div","leaflet-control-scale"),e=this.options;return this._addScales(e,"leaflet-control-scale-line",i),t.on(e.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,i,e){t.metric&&(this._mScale=G("div",i,e)),t.imperial&&(this._iScale=G("div",i,e))},_update:function(){var t=this._map,i=t.getSize().y/2,e=t.distance(t.containerPointToLatLng([0,i]),t.containerPointToLatLng([this.options.maxWidth,i]));this._updateScales(e)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var i=this._getRoundNum(t),e=i<1e3?i+" m":i/1e3+" km";this._updateScale(this._mScale,e,i/t)},_updateImperial:function(t){var i,e,n,o=3.2808399*t;o>5280?(i=o/5280,e=this._getRoundNum(i),this._updateScale(this._iScale,e+" mi",e/i)):(n=this._getRoundNum(o),this._updateScale(this._iScale,n+" ft",n/o))},_updateScale:function(t,i,e){t.style.width=Math.round(this.options.maxWidth*e)+"px",t.innerHTML=i},_getRoundNum:function(t){var i=Math.pow(10,(Math.floor(t)+"").length-1),e=t/i;return e=e>=10?10:e>=5?5:e>=3?3:e>=2?2:1,i*e}}),Ze=Te.extend({options:{position:"bottomright",prefix:'Leaflet'},initialize:function(t){l(this,t),this._attributions={}},onAdd:function(t){t.attributionControl=this,this._container=G("div","leaflet-control-attribution"),wt(this._container);for(var i in t._layers)t._layers[i].getAttribution&&this.addAttribution(t._layers[i].getAttribution());return this._update(),this._container},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t?(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update(),this):this},removeAttribution:function(t){return t?(this._attributions[t]&&(this._attributions[t]--,this._update()),this):this},_update:function(){if(this._map){var t=[];for(var i in this._attributions)this._attributions[i]&&t.push(i);var e=[];this.options.prefix&&e.push(this.options.prefix),t.length&&e.push(t.join(", ")),this._container.innerHTML=e.join(" | ")}}});be.mergeOptions({attributionControl:!0}),be.addInitHook(function(){this.options.attributionControl&&(new Ze).addTo(this)});Te.Layers=Me,Te.Zoom=Ce,Te.Scale=Se,Te.Attribution=Ze,ze.layers=function(t,i,e){return new Me(t,i,e)},ze.zoom=function(t){return new Ce(t)},ze.scale=function(t){return new Se(t)},ze.attribution=function(t){return new Ze(t)};var Ee=v.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled?this:(this._enabled=!0,this.addHooks(),this)},disable:function(){return this._enabled?(this._enabled=!1,this.removeHooks(),this):this},enabled:function(){return!!this._enabled}});Ee.addTo=function(t,i){return t.addHandler(i,this),this};var ke,Ae={Events:li},Be=qi?"touchstart mousedown":"mousedown",Ie={mousedown:"mouseup",touchstart:"touchend",pointerdown:"touchend",MSPointerDown:"touchend"},Oe={mousedown:"mousemove",touchstart:"touchmove",pointerdown:"touchmove",MSPointerDown:"touchmove"},Re=ci.extend({options:{clickTolerance:3},initialize:function(t,i,e,n){l(this,n),this._element=t,this._dragStartTarget=i||t,this._preventOutline=e},enable:function(){this._enabled||(mt(this._dragStartTarget,Be,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(Re._dragging===this&&this.finishDrag(),ft(this._dragStartTarget,Be,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){if(!t._simulated&&this._enabled&&(this._moved=!1,!$(this._element,"leaflet-zoom-anim")&&!(Re._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||(Re._dragging=this,this._preventOutline&&ct(this._element),ut(),fi(),this._moving)))){this.fire("down");var i=t.touches?t.touches[0]:t,e=dt(this._element);this._startPoint=new x(i.clientX,i.clientY),this._parentScale=pt(e),mt(document,Oe[t.type],this._onMove,this),mt(document,Ie[t.type],this._onUp,this)}},_onMove:function(t){if(!t._simulated&&this._enabled)if(t.touches&&t.touches.length>1)this._moved=!0;else{var i=t.touches&&1===t.touches.length?t.touches[0]:t,e=new x(i.clientX,i.clientY)._subtract(this._startPoint);(e.x||e.y)&&(Math.abs(e.x)+Math.abs(e.y)1e-7;h++)i=s*Math.sin(a),i=Math.pow((1-i)/(1+i),s/2),a+=u=Math.PI/2-2*Math.atan(r*i)-a;return new M(a*e,t.x*e/n)}},He=(Object.freeze||Object)({LonLat:je,Mercator:We,SphericalMercator:mi}),Fe=i({},pi,{code:"EPSG:3395",projection:We,transformation:function(){var t=.5/(Math.PI*We.R);return Z(t,.5,-t,.5)}()}),Ue=i({},pi,{code:"EPSG:4326",projection:je,transformation:Z(1/180,1,-1/180,.5)}),Ve=i({},di,{projection:je,transformation:Z(1,0,-1,0),scale:function(t){return Math.pow(2,t)},zoom:function(t){return Math.log(t)/Math.LN2},distance:function(t,i){var e=i.lng-t.lng,n=i.lat-t.lat;return Math.sqrt(e*e+n*n)},infinite:!0});di.Earth=pi,di.EPSG3395=Fe,di.EPSG3857=yi,di.EPSG900913=xi,di.EPSG4326=Ue,di.Simple=Ve;var qe=ci.extend({options:{pane:"overlayPane",attribution:null,bubblingMouseEvents:!0},addTo:function(t){return t.addLayer(this),this},remove:function(){return this.removeFrom(this._map||this._mapToAdd)},removeFrom:function(t){return t&&t.removeLayer(this),this},getPane:function(t){return this._map.getPane(t?this.options[t]||t:this.options.pane)},addInteractiveTarget:function(t){return this._map._targets[n(t)]=this,this},removeInteractiveTarget:function(t){return delete this._map._targets[n(t)],this},getAttribution:function(){return this.options.attribution},_layerAdd:function(t){var i=t.target;if(i.hasLayer(this)){if(this._map=i,this._zoomAnimated=i._zoomAnimated,this.getEvents){var e=this.getEvents();i.on(e,this),this.once("remove",function(){i.off(e,this)},this)}this.onAdd(i),this.getAttribution&&i.attributionControl&&i.attributionControl.addAttribution(this.getAttribution()),this.fire("add"),i.fire("layeradd",{layer:this})}}});be.include({addLayer:function(t){if(!t._layerAdd)throw new Error("The provided object is not a Layer.");var i=n(t);return this._layers[i]?this:(this._layers[i]=t,t._mapToAdd=this,t.beforeAdd&&t.beforeAdd(this),this.whenReady(t._layerAdd,t),this)},removeLayer:function(t){var i=n(t);return this._layers[i]?(this._loaded&&t.onRemove(this),t.getAttribution&&this.attributionControl&&this.attributionControl.removeAttribution(t.getAttribution()),delete this._layers[i],this._loaded&&(this.fire("layerremove",{layer:t}),t.fire("remove")),t._map=t._mapToAdd=null,this):this},hasLayer:function(t){return!!t&&n(t)in this._layers},eachLayer:function(t,i){for(var e in this._layers)t.call(i,this._layers[e]);return this},_addLayers:function(t){for(var i=0,e=(t=t?oi(t)?t:[t]:[]).length;ithis._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()i)return r=(n-i)/e,this._map.layerPointToLatLng([s.x-r*(s.x-o.x),s.y-r*(s.y-o.y)])},getBounds:function(){return this._bounds},addLatLng:function(t,i){return i=i||this._defaultShape(),t=C(t),i.push(t),this._bounds.extend(t),this.redraw()},_setLatLngs:function(t){this._bounds=new T,this._latlngs=this._convertLatLngs(t)},_defaultShape:function(){return jt(this._latlngs)?this._latlngs:this._latlngs[0]},_convertLatLngs:function(t){for(var i=[],e=jt(t),n=0,o=t.length;n=2&&i[0]instanceof M&&i[0].equals(i[e-1])&&i.pop(),i},_setLatLngs:function(t){nn.prototype._setLatLngs.call(this,t),jt(this._latlngs)&&(this._latlngs=[this._latlngs])},_defaultShape:function(){return jt(this._latlngs[0])?this._latlngs[0]:this._latlngs[0][0]},_clipPoints:function(){var t=this._renderer._bounds,i=this.options.weight,e=new x(i,i);if(t=new P(t.min.subtract(e),t.max.add(e)),this._parts=[],this._pxBounds&&this._pxBounds.intersects(t))if(this.options.noClip)this._parts=this._rings;else for(var n,o=0,s=this._rings.length;ot.y!=n.y>t.y&&t.x<(n.x-e.x)*(t.y-e.y)/(n.y-e.y)+e.x&&(u=!u);return u||nn.prototype._containsPoint.call(this,t,!0)}}),sn=Ke.extend({initialize:function(t,i){l(this,i),this._layers={},t&&this.addData(t)},addData:function(t){var i,e,n,o=oi(t)?t:t.features;if(o){for(i=0,e=o.length;i0?o:[i.src]}else{oi(this._url)||(this._url=[this._url]),i.autoplay=!!this.options.autoplay,i.loop=!!this.options.loop;for(var a=0;ao?(i.height=o+"px",Q(t,"leaflet-popup-scrolled")):tt(t,"leaflet-popup-scrolled"),this._containerWidth=this._container.offsetWidth},_animateZoom:function(t){var i=this._map._latLngToNewLayerPoint(this._latlng,t.zoom,t.center),e=this._getAnchor();at(this._container,i.add(e))},_adjustPan:function(){if(!(!this.options.autoPan||this._map._panAnim&&this._map._panAnim._inProgress)){var t=this._map,i=parseInt(q(this._container,"marginBottom"),10)||0,e=this._container.offsetHeight+i,n=this._containerWidth,o=new x(this._containerLeft,-e-this._containerBottom);o._add(ht(this._container));var s=t.layerPointToContainerPoint(o),r=w(this.options.autoPanPadding),a=w(this.options.autoPanPaddingTopLeft||r),h=w(this.options.autoPanPaddingBottomRight||r),u=t.getSize(),l=0,c=0;s.x+n+h.x>u.x&&(l=s.x+n-u.x+h.x),s.x-l-a.x<0&&(l=s.x-a.x),s.y+e+h.y>u.y&&(c=s.y+e-u.y+h.y),s.y-c-a.y<0&&(c=s.y-a.y),(l||c)&&t.fire("autopanstart").panBy([l,c])}},_onCloseButtonClick:function(t){this._close(),Lt(t)},_getAnchor:function(){return w(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}});be.mergeOptions({closePopupOnClick:!0}),be.include({openPopup:function(t,i,e){return t instanceof cn||(t=new cn(e).setContent(t)),i&&t.setLatLng(i),this.hasLayer(t)?this:(this._popup&&this._popup.options.autoClose&&this.closePopup(),this._popup=t,this.addLayer(t))},closePopup:function(t){return t&&t!==this._popup||(t=this._popup,this._popup=null),t&&this.removeLayer(t),this}}),qe.include({bindPopup:function(t,i){return t instanceof cn?(l(t,i),this._popup=t,t._source=this):(this._popup&&!i||(this._popup=new cn(i,this)),this._popup.setContent(t)),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t,i){if(t instanceof qe||(i=t,t=this),t instanceof Ke)for(var e in this._layers){t=this._layers[e];break}return i||(i=t.getCenter?t.getCenter():t.getLatLng()),this._popup&&this._map&&(this._popup._source=t,this._popup.update(),this._map.openPopup(this._popup,i)),this},closePopup:function(){return this._popup&&this._popup._close(),this},togglePopup:function(t){return this._popup&&(this._popup._map?this.closePopup():this.openPopup(t)),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var i=t.layer||t.target;this._popup&&this._map&&(Lt(t),i instanceof Qe?this.openPopup(t.layer||t.target,t.latlng):this._map.hasLayer(this._popup)&&this._popup._source===i?this.closePopup():this.openPopup(i,t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}});var _n=ln.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,interactive:!1,opacity:.9},onAdd:function(t){ln.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&this._source.fire("tooltipopen",{tooltip:this},!0)},onRemove:function(t){ln.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&this._source.fire("tooltipclose",{tooltip:this},!0)},getEvents:function(){var t=ln.prototype.getEvents.call(this);return qi&&!this.options.permanent&&(t.preclick=this._close),t},_close:function(){this._map&&this._map.closeTooltip(this)},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=G("div",t)},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var i=this._map,e=this._container,n=i.latLngToContainerPoint(i.getCenter()),o=i.layerPointToContainerPoint(t),s=this.options.direction,r=e.offsetWidth,a=e.offsetHeight,h=w(this.options.offset),u=this._getAnchor();"top"===s?t=t.add(w(-r/2+h.x,-a+h.y+u.y,!0)):"bottom"===s?t=t.subtract(w(r/2-h.x,-h.y,!0)):"center"===s?t=t.subtract(w(r/2+h.x,a/2-u.y+h.y,!0)):"right"===s||"auto"===s&&o.xthis.options.maxZoom||en&&this._retainParent(o,s,r,n))},_retainChildren:function(t,i,e,n){for(var o=2*t;o<2*t+2;o++)for(var s=2*i;s<2*i+2;s++){var r=new x(o,s);r.z=e+1;var a=this._tileCoordsToKey(r),h=this._tiles[a];h&&h.active?h.retain=!0:(h&&h.loaded&&(h.retain=!0),e+1this.options.maxZoom||void 0!==this.options.minZoom&&o1)this._setView(t,e);else{for(var c=o.min.y;c<=o.max.y;c++)for(var _=o.min.x;_<=o.max.x;_++){var d=new x(_,c);if(d.z=this._tileZoom,this._isValidTile(d)){var p=this._tiles[this._tileCoordsToKey(d)];p?p.current=!0:r.push(d)}}if(r.sort(function(t,i){return t.distanceTo(s)-i.distanceTo(s)}),0!==r.length){this._loading||(this._loading=!0,this.fire("loading"));var m=document.createDocumentFragment();for(_=0;_e.max.x)||!i.wrapLat&&(t.ye.max.y))return!1}if(!this.options.bounds)return!0;var n=this._tileCoordsToBounds(t);return z(this.options.bounds).overlaps(n)},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var i=this._map,e=this.getTileSize(),n=t.scaleBy(e),o=n.add(e);return[i.unproject(n,t.z),i.unproject(o,t.z)]},_tileCoordsToBounds:function(t){var i=this._tileCoordsToNwSe(t),e=new T(i[0],i[1]);return this.options.noWrap||(e=this._map.wrapLatLngBounds(e)),e},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var i=t.split(":"),e=new x(+i[0],+i[1]);return e.z=+i[2],e},_removeTile:function(t){var i=this._tiles[t];i&&(K(i.el),delete this._tiles[t],this.fire("tileunload",{tile:i.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){Q(t,"leaflet-tile");var i=this.getTileSize();t.style.width=i.x+"px",t.style.height=i.y+"px",t.onselectstart=r,t.onmousemove=r,Li&&this.options.opacity<1&&nt(t,this.options.opacity),zi&&!Mi&&(t.style.WebkitBackfaceVisibility="hidden")},_addTile:function(t,i){var n=this._getTilePos(t),o=this._tileCoordsToKey(t),s=this.createTile(this._wrapCoords(t),e(this._tileReady,this,t));this._initTile(s),this.createTile.length<2&&f(e(this._tileReady,this,t,null,s)),at(s,n),this._tiles[o]={el:s,coords:t,current:!0},i.appendChild(s),this.fire("tileloadstart",{tile:s,coords:t})},_tileReady:function(t,i,n){i&&this.fire("tileerror",{error:i,tile:n,coords:t});var o=this._tileCoordsToKey(t);(n=this._tiles[o])&&(n.loaded=+new Date,this._map._fadeAnimated?(nt(n.el,0),g(this._fadeFrame),this._fadeFrame=f(this._updateOpacity,this)):(n.active=!0,this._pruneTiles()),i||(Q(n.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:n.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),Li||!this._map._fadeAnimated?f(this._pruneTiles,this):setTimeout(e(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var i=new x(this._wrapX?s(t.x,this._wrapX):t.x,this._wrapY?s(t.y,this._wrapY):t.y);return i.z=t.z,i},_pxBoundsToTileRange:function(t){var i=this.getTileSize();return new P(t.min.unscaleBy(i).floor(),t.max.unscaleBy(i).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}}),mn=pn.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1},initialize:function(t,i){this._url=t,(i=l(this,i)).detectRetina&&Yi&&i.maxZoom>0&&(i.tileSize=Math.floor(i.tileSize/2),i.zoomReverse?(i.zoomOffset--,i.minZoom++):(i.zoomOffset++,i.maxZoom--),i.minZoom=Math.max(0,i.minZoom)),"string"==typeof i.subdomains&&(i.subdomains=i.subdomains.split("")),zi||this.on("tileunload",this._onTileRemove)},setUrl:function(t,i){return this._url=t,i||this.redraw(),this},createTile:function(t,i){var n=document.createElement("img");return mt(n,"load",e(this._tileOnLoad,this,i,n)),mt(n,"error",e(this._tileOnError,this,i,n)),(this.options.crossOrigin||""===this.options.crossOrigin)&&(n.crossOrigin=!0===this.options.crossOrigin?"":this.options.crossOrigin),n.alt="",n.setAttribute("role","presentation"),n.src=this.getTileUrl(t),n},getTileUrl:function(t){var e={r:Yi?"@2x":"",s:this._getSubdomain(t),x:t.x,y:t.y,z:this._getZoomForUrl()};if(this._map&&!this._map.options.crs.infinite){var n=this._globalTileRange.max.y-t.y;this.options.tms&&(e.y=n),e["-y"]=n}return _(this._url,i(e,this.options))},_tileOnLoad:function(t,i){Li?setTimeout(e(t,this,null,i),0):t(null,i)},_tileOnError:function(t,i,e){var n=this.options.errorTileUrl;n&&i.getAttribute("src")!==n&&(i.src=n),t(e,i)},_onTileRemove:function(t){t.tile.onload=null},_getZoomForUrl:function(){var t=this._tileZoom,i=this.options.maxZoom,e=this.options.zoomReverse,n=this.options.zoomOffset;return e&&(t=i-t),t+n},_getSubdomain:function(t){var i=Math.abs(t.x+t.y)%this.options.subdomains.length;return this.options.subdomains[i]},_abortLoading:function(){var t,i;for(t in this._tiles)this._tiles[t].coords.z!==this._tileZoom&&((i=this._tiles[t].el).onload=r,i.onerror=r,i.complete||(i.src=si,K(i),delete this._tiles[t]))},_removeTile:function(t){var i=this._tiles[t];if(i)return Si||i.el.setAttribute("src",si),pn.prototype._removeTile.call(this,t)},_tileReady:function(t,i,e){if(this._map&&(!e||e.getAttribute("src")!==si))return pn.prototype._tileReady.call(this,t,i,e)}}),fn=mn.extend({defaultWmsParams:{service:"WMS",request:"GetMap",layers:"",styles:"",format:"image/jpeg",transparent:!1,version:"1.1.1"},options:{crs:null,uppercase:!1},initialize:function(t,e){this._url=t;var n=i({},this.defaultWmsParams);for(var o in e)o in this.options||(n[o]=e[o]);var s=(e=l(this,e)).detectRetina&&Yi?2:1,r=this.getTileSize();n.width=r.x*s,n.height=r.y*s,this.wmsParams=n},onAdd:function(t){this._crs=this.options.crs||t.options.crs,this._wmsVersion=parseFloat(this.wmsParams.version);var i=this._wmsVersion>=1.3?"crs":"srs";this.wmsParams[i]=this._crs.code,mn.prototype.onAdd.call(this,t)},getTileUrl:function(t){var i=this._tileCoordsToNwSe(t),e=this._crs,n=b(e.project(i[0]),e.project(i[1])),o=n.min,s=n.max,r=(this._wmsVersion>=1.3&&this._crs===Ue?[o.y,o.x,s.y,s.x]:[o.x,o.y,s.x,s.y]).join(","),a=mn.prototype.getTileUrl.call(this,t);return a+c(this.wmsParams,a,this.options.uppercase)+(this.options.uppercase?"&BBOX=":"&bbox=")+r},setParams:function(t,e){return i(this.wmsParams,t),e||this.redraw(),this}});mn.WMS=fn,Jt.wms=function(t,i){return new fn(t,i)};var gn=qe.extend({options:{padding:.1,tolerance:0},initialize:function(t){l(this,t),n(this),this._layers=this._layers||{}},onAdd:function(){this._container||(this._initContainer(),this._zoomAnimated&&Q(this._container,"leaflet-zoom-animated")),this.getPane().appendChild(this._container),this._update(),this.on("update",this._updatePaths,this)},onRemove:function(){this.off("update",this._updatePaths,this),this._destroyContainer()},getEvents:function(){var t={viewreset:this._reset,zoom:this._onZoom,moveend:this._update,zoomend:this._onZoomEnd};return this._zoomAnimated&&(t.zoomanim=this._onAnimZoom),t},_onAnimZoom:function(t){this._updateTransform(t.center,t.zoom)},_onZoom:function(){this._updateTransform(this._map.getCenter(),this._map.getZoom())},_updateTransform:function(t,i){var e=this._map.getZoomScale(i,this._zoom),n=ht(this._container),o=this._map.getSize().multiplyBy(.5+this.options.padding),s=this._map.project(this._center,i),r=this._map.project(t,i).subtract(s),a=o.multiplyBy(-e).add(n).add(o).subtract(r);ji?rt(this._container,a,e):at(this._container,a)},_reset:function(){this._update(),this._updateTransform(this._center,this._zoom);for(var t in this._layers)this._layers[t]._reset()},_onZoomEnd:function(){for(var t in this._layers)this._layers[t]._project()},_updatePaths:function(){for(var t in this._layers)this._layers[t]._update()},_update:function(){var t=this.options.padding,i=this._map.getSize(),e=this._map.containerPointToLayerPoint(i.multiplyBy(-t)).round();this._bounds=new P(e,e.add(i.multiplyBy(1+2*t)).round()),this._center=this._map.getCenter(),this._zoom=this._map.getZoom()}}),vn=gn.extend({getEvents:function(){var t=gn.prototype.getEvents.call(this);return t.viewprereset=this._onViewPreReset,t},_onViewPreReset:function(){this._postponeUpdatePaths=!0},onAdd:function(){gn.prototype.onAdd.call(this),this._draw()},_initContainer:function(){var t=this._container=document.createElement("canvas");mt(t,"mousemove",o(this._onMouseMove,32,this),this),mt(t,"click dblclick mousedown mouseup contextmenu",this._onClick,this),mt(t,"mouseout",this._handleMouseOut,this),this._ctx=t.getContext("2d")},_destroyContainer:function(){g(this._redrawRequest),delete this._ctx,K(this._container),ft(this._container),delete this._container},_updatePaths:function(){if(!this._postponeUpdatePaths){this._redrawBounds=null;for(var t in this._layers)this._layers[t]._update();this._redraw()}},_update:function(){if(!this._map._animatingZoom||!this._bounds){this._drawnLayers={},gn.prototype._update.call(this);var t=this._bounds,i=this._container,e=t.getSize(),n=Yi?2:1;at(i,t.min),i.width=n*e.x,i.height=n*e.y,i.style.width=e.x+"px",i.style.height=e.y+"px",Yi&&this._ctx.scale(2,2),this._ctx.translate(-t.min.x,-t.min.y),this.fire("update")}},_reset:function(){gn.prototype._reset.call(this),this._postponeUpdatePaths&&(this._postponeUpdatePaths=!1,this._updatePaths())},_initPath:function(t){this._updateDashArray(t),this._layers[n(t)]=t;var i=t._order={layer:t,prev:this._drawLast,next:null};this._drawLast&&(this._drawLast.next=i),this._drawLast=i,this._drawFirst=this._drawFirst||this._drawLast},_addPath:function(t){this._requestRedraw(t)},_removePath:function(t){var i=t._order,e=i.next,o=i.prev;e?e.prev=o:this._drawLast=o,o?o.next=e:this._drawFirst=e,delete this._drawnLayers[t._leaflet_id],delete t._order,delete this._layers[n(t)],this._requestRedraw(t)},_updatePath:function(t){this._extendRedrawBounds(t),t._project(),t._update(),this._requestRedraw(t)},_updateStyle:function(t){this._updateDashArray(t),this._requestRedraw(t)},_updateDashArray:function(t){if("string"==typeof t.options.dashArray){var i,e=t.options.dashArray.split(/[, ]+/),n=[];for(i=0;i')}}catch(t){return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}}(),xn={_initContainer:function(){this._container=G("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(gn.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var i=t._container=yn("shape");Q(i,"leaflet-vml-shape "+(this.options.className||"")),i.coordsize="1 1",t._path=yn("path"),i.appendChild(t._path),this._updateStyle(t),this._layers[n(t)]=t},_addPath:function(t){var i=t._container;this._container.appendChild(i),t.options.interactive&&t.addInteractiveTarget(i)},_removePath:function(t){var i=t._container;K(i),t.removeInteractiveTarget(i),delete this._layers[n(t)]},_updateStyle:function(t){var i=t._stroke,e=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(i||(i=t._stroke=yn("stroke")),o.appendChild(i),i.weight=n.weight+"px",i.color=n.color,i.opacity=n.opacity,n.dashArray?i.dashStyle=oi(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):i.dashStyle="",i.endcap=n.lineCap.replace("butt","flat"),i.joinstyle=n.lineJoin):i&&(o.removeChild(i),t._stroke=null),n.fill?(e||(e=t._fill=yn("fill")),o.appendChild(e),e.color=n.fillColor||n.color,e.opacity=n.fillOpacity):e&&(o.removeChild(e),t._fill=null)},_updateCircle:function(t){var i=t._point.round(),e=Math.round(t._radius),n=Math.round(t._radiusY||e);this._setPath(t,t._empty()?"M0 0":"AL "+i.x+","+i.y+" "+e+","+n+" 0,23592600")},_setPath:function(t,i){t._path.v=i},_bringToFront:function(t){X(t._container)},_bringToBack:function(t){J(t._container)}},wn=$i?yn:E,Pn=gn.extend({getEvents:function(){var t=gn.prototype.getEvents.call(this);return t.zoomstart=this._onZoomStart,t},_initContainer:function(){this._container=wn("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=wn("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){K(this._container),ft(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_onZoomStart:function(){this._update()},_update:function(){if(!this._map._animatingZoom||!this._bounds){gn.prototype._update.call(this);var t=this._bounds,i=t.getSize(),e=this._container;this._svgSize&&this._svgSize.equals(i)||(this._svgSize=i,e.setAttribute("width",i.x),e.setAttribute("height",i.y)),at(e,t.min),e.setAttribute("viewBox",[t.min.x,t.min.y,i.x,i.y].join(" ")),this.fire("update")}},_initPath:function(t){var i=t._path=wn("path");t.options.className&&Q(i,t.options.className),t.options.interactive&&Q(i,"leaflet-interactive"),this._updateStyle(t),this._layers[n(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){K(t._path),t.removeInteractiveTarget(t._path),delete this._layers[n(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var i=t._path,e=t.options;i&&(e.stroke?(i.setAttribute("stroke",e.color),i.setAttribute("stroke-opacity",e.opacity),i.setAttribute("stroke-width",e.weight),i.setAttribute("stroke-linecap",e.lineCap),i.setAttribute("stroke-linejoin",e.lineJoin),e.dashArray?i.setAttribute("stroke-dasharray",e.dashArray):i.removeAttribute("stroke-dasharray"),e.dashOffset?i.setAttribute("stroke-dashoffset",e.dashOffset):i.removeAttribute("stroke-dashoffset")):i.setAttribute("stroke","none"),e.fill?(i.setAttribute("fill",e.fillColor||e.color),i.setAttribute("fill-opacity",e.fillOpacity),i.setAttribute("fill-rule",e.fillRule||"evenodd")):i.setAttribute("fill","none"))},_updatePoly:function(t,i){this._setPath(t,k(t._parts,i))},_updateCircle:function(t){var i=t._point,e=Math.max(Math.round(t._radius),1),n="a"+e+","+(Math.max(Math.round(t._radiusY),1)||e)+" 0 1,0 ",o=t._empty()?"M0 0":"M"+(i.x-e)+","+i.y+n+2*e+",0 "+n+2*-e+",0 ";this._setPath(t,o)},_setPath:function(t,i){t._path.setAttribute("d",i)},_bringToFront:function(t){X(t._path)},_bringToBack:function(t){J(t._path)}});$i&&Pn.include(xn),be.include({getRenderer:function(t){var i=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer;return i||(i=this._renderer=this._createRenderer()),this.hasLayer(i)||this.addLayer(i),i},_getPaneRenderer:function(t){if("overlayPane"===t||void 0===t)return!1;var i=this._paneRenderers[t];return void 0===i&&(i=this._createRenderer({pane:t}),this._paneRenderers[t]=i),i},_createRenderer:function(t){return this.options.preferCanvas&&$t(t)||Qt(t)}});var Ln=on.extend({initialize:function(t,i){on.prototype.initialize.call(this,this._boundsToLatLngs(t),i)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return t=z(t),[t.getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});Pn.create=wn,Pn.pointsToPath=k,sn.geometryToLayer=Ft,sn.coordsToLatLng=Ut,sn.coordsToLatLngs=Vt,sn.latLngToCoords=qt,sn.latLngsToCoords=Gt,sn.getFeature=Kt,sn.asFeature=Yt,be.mergeOptions({boxZoom:!0});var bn=Ee.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){mt(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){ft(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){K(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),fi(),ut(),this._startPoint=this._map.mouseEventToContainerPoint(t),mt(document,{contextmenu:Lt,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=G("div","leaflet-zoom-box",this._container),Q(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var i=new P(this._point,this._startPoint),e=i.getSize();at(this._box,i.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(K(this._box),tt(this._container,"leaflet-crosshair")),gi(),lt(),ft(document,{contextmenu:Lt,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){if((1===t.which||1===t.button)&&(this._finish(),this._moved)){this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(e(this._resetState,this),0);var i=new T(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point));this._map.fitBounds(i).fire("boxzoomend",{boxZoomBounds:i})}},_onKeyDown:function(t){27===t.keyCode&&this._finish()}});be.addInitHook("addHandler","boxZoom",bn),be.mergeOptions({doubleClickZoom:!0});var Tn=Ee.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var i=this._map,e=i.getZoom(),n=i.options.zoomDelta,o=t.originalEvent.shiftKey?e-n:e+n;"center"===i.options.doubleClickZoom?i.setZoom(o):i.setZoomAround(t.containerPoint,o)}});be.addInitHook("addHandler","doubleClickZoom",Tn),be.mergeOptions({dragging:!0,inertia:!Mi,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0});var zn=Ee.extend({addHooks:function(){if(!this._draggable){var t=this._map;this._draggable=new Re(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))}Q(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){tt(this._map._container,"leaflet-grab"),tt(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t=this._map;if(t._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity){var i=z(this._map.options.maxBounds);this._offsetLimit=b(this._map.latLngToContainerPoint(i.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(i.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))}else this._offsetLimit=null;t.fire("movestart").fire("dragstart"),t.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){if(this._map.options.inertia){var i=this._lastTime=+new Date,e=this._lastPos=this._draggable._absPos||this._draggable._newPos;this._positions.push(e),this._times.push(i),this._prunePositions(i)}this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;this._positions.length>1&&t-this._times[0]>50;)this._positions.shift(),this._times.shift()},_onZoomEnd:function(){var t=this._map.getSize().divideBy(2),i=this._map.latLngToLayerPoint([0,0]);this._initialWorldOffset=i.subtract(t).x,this._worldWidth=this._map.getPixelWorldBounds().getSize().x},_viscousLimit:function(t,i){return t-(t-i)*this._viscosity},_onPreDragLimit:function(){if(this._viscosity&&this._offsetLimit){var t=this._draggable._newPos.subtract(this._draggable._startPos),i=this._offsetLimit;t.xi.max.x&&(t.x=this._viscousLimit(t.x,i.max.x)),t.y>i.max.y&&(t.y=this._viscousLimit(t.y,i.max.y)),this._draggable._newPos=this._draggable._startPos.add(t)}},_onPreDragWrap:function(){var t=this._worldWidth,i=Math.round(t/2),e=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-i+e)%t+i-e,s=(n+i+e)%t-i-e,r=Math.abs(o+e)0?s:-s))-i;this._delta=0,this._startTime=null,r&&("center"===t.options.scrollWheelZoom?t.setZoom(i+r):t.setZoomAround(this._lastMousePos,i+r))}});be.addInitHook("addHandler","scrollWheelZoom",Cn),be.mergeOptions({tap:!0,tapTolerance:15});var Sn=Ee.extend({addHooks:function(){mt(this._map._container,"touchstart",this._onDown,this)},removeHooks:function(){ft(this._map._container,"touchstart",this._onDown,this)},_onDown:function(t){if(t.touches){if(Pt(t),this._fireClick=!0,t.touches.length>1)return this._fireClick=!1,void clearTimeout(this._holdTimeout);var i=t.touches[0],n=i.target;this._startPos=this._newPos=new x(i.clientX,i.clientY),n.tagName&&"a"===n.tagName.toLowerCase()&&Q(n,"leaflet-active"),this._holdTimeout=setTimeout(e(function(){this._isTapValid()&&(this._fireClick=!1,this._onUp(),this._simulateEvent("contextmenu",i))},this),1e3),this._simulateEvent("mousedown",i),mt(document,{touchmove:this._onMove,touchend:this._onUp},this)}},_onUp:function(t){if(clearTimeout(this._holdTimeout),ft(document,{touchmove:this._onMove,touchend:this._onUp},this),this._fireClick&&t&&t.changedTouches){var i=t.changedTouches[0],e=i.target;e&&e.tagName&&"a"===e.tagName.toLowerCase()&&tt(e,"leaflet-active"),this._simulateEvent("mouseup",i),this._isTapValid()&&this._simulateEvent("click",i)}},_isTapValid:function(){return this._newPos.distanceTo(this._startPos)<=this._map.options.tapTolerance},_onMove:function(t){var i=t.touches[0];this._newPos=new x(i.clientX,i.clientY),this._simulateEvent("mousemove",i)},_simulateEvent:function(t,i){var e=document.createEvent("MouseEvents");e._simulated=!0,i.target._simulatedClick=!0,e.initMouseEvent(t,!0,!0,window,1,i.screenX,i.screenY,i.clientX,i.clientY,!1,!1,!1,!1,0,null),i.target.dispatchEvent(e)}});qi&&!Vi&&be.addInitHook("addHandler","tap",Sn),be.mergeOptions({touchZoom:qi&&!Mi,bounceAtZoomLimits:!0});var Zn=Ee.extend({addHooks:function(){Q(this._map._container,"leaflet-touch-zoom"),mt(this._map._container,"touchstart",this._onTouchStart,this)},removeHooks:function(){tt(this._map._container,"leaflet-touch-zoom"),ft(this._map._container,"touchstart",this._onTouchStart,this)},_onTouchStart:function(t){var i=this._map;if(t.touches&&2===t.touches.length&&!i._animatingZoom&&!this._zooming){var e=i.mouseEventToContainerPoint(t.touches[0]),n=i.mouseEventToContainerPoint(t.touches[1]);this._centerPoint=i.getSize()._divideBy(2),this._startLatLng=i.containerPointToLatLng(this._centerPoint),"center"!==i.options.touchZoom&&(this._pinchStartLatLng=i.containerPointToLatLng(e.add(n)._divideBy(2))),this._startDist=e.distanceTo(n),this._startZoom=i.getZoom(),this._moved=!1,this._zooming=!0,i._stop(),mt(document,"touchmove",this._onTouchMove,this),mt(document,"touchend",this._onTouchEnd,this),Pt(t)}},_onTouchMove:function(t){if(t.touches&&2===t.touches.length&&this._zooming){var i=this._map,n=i.mouseEventToContainerPoint(t.touches[0]),o=i.mouseEventToContainerPoint(t.touches[1]),s=n.distanceTo(o)/this._startDist;if(this._zoom=i.getScaleZoom(s,this._startZoom),!i.options.bounceAtZoomLimits&&(this._zoomi.getMaxZoom()&&s>1)&&(this._zoom=i._limitZoom(this._zoom)),"center"===i.options.touchZoom){if(this._center=this._startLatLng,1===s)return}else{var r=n._add(o)._divideBy(2)._subtract(this._centerPoint);if(1===s&&0===r.x&&0===r.y)return;this._center=i.unproject(i.project(this._pinchStartLatLng,this._zoom).subtract(r),this._zoom)}this._moved||(i._moveStart(!0,!1),this._moved=!0),g(this._animRequest);var a=e(i._move,i,this._center,this._zoom,{pinch:!0,round:!1});this._animRequest=f(a,this,!0),Pt(t)}},_onTouchEnd:function(){this._moved&&this._zooming?(this._zooming=!1,g(this._animRequest),ft(document,"touchmove",this._onTouchMove),ft(document,"touchend",this._onTouchEnd),this._map.options.zoomAnimation?this._map._animateZoom(this._center,this._map._limitZoom(this._zoom),!0,this._map.options.zoomSnap):this._map._resetView(this._center,this._map._limitZoom(this._zoom))):this._zooming=!1}});be.addInitHook("addHandler","touchZoom",Zn),be.BoxZoom=bn,be.DoubleClickZoom=Tn,be.Drag=zn,be.Keyboard=Mn,be.ScrollWheelZoom=Cn,be.Tap=Sn,be.TouchZoom=Zn,Object.freeze=ti,t.version="1.3.4+HEAD.0e566b2",t.Control=Te,t.control=ze,t.Browser=Qi,t.Evented=ci,t.Mixin=Ae,t.Util=ui,t.Class=v,t.Handler=Ee,t.extend=i,t.bind=e,t.stamp=n,t.setOptions=l,t.DomEvent=Pe,t.DomUtil=ve,t.PosAnimation=Le,t.Draggable=Re,t.LineUtil=Ne,t.PolyUtil=De,t.Point=x,t.point=w,t.Bounds=P,t.bounds=b,t.Transformation=S,t.transformation=Z,t.Projection=He,t.LatLng=M,t.latLng=C,t.LatLngBounds=T,t.latLngBounds=z,t.CRS=di,t.GeoJSON=sn,t.geoJSON=Xt,t.geoJson=an,t.Layer=qe,t.LayerGroup=Ge,t.layerGroup=function(t,i){return new Ge(t,i)},t.FeatureGroup=Ke,t.featureGroup=function(t){return new Ke(t)},t.ImageOverlay=hn,t.imageOverlay=function(t,i,e){return new hn(t,i,e)},t.VideoOverlay=un,t.videoOverlay=function(t,i,e){return new un(t,i,e)},t.DivOverlay=ln,t.Popup=cn,t.popup=function(t,i){return new cn(t,i)},t.Tooltip=_n,t.tooltip=function(t,i){return new _n(t,i)},t.Icon=Ye,t.icon=function(t){return new Ye(t)},t.DivIcon=dn,t.divIcon=function(t){return new dn(t)},t.Marker=$e,t.marker=function(t,i){return new $e(t,i)},t.TileLayer=mn,t.tileLayer=Jt,t.GridLayer=pn,t.gridLayer=function(t){return new pn(t)},t.SVG=Pn,t.svg=Qt,t.Renderer=gn,t.Canvas=vn,t.canvas=$t,t.Path=Qe,t.CircleMarker=tn,t.circleMarker=function(t,i){return new tn(t,i)},t.Circle=en,t.circle=function(t,i,e){return new en(t,i,e)},t.Polyline=nn,t.polyline=function(t,i){return new nn(t,i)},t.Polygon=on,t.polygon=function(t,i){return new on(t,i)},t.Rectangle=Ln,t.rectangle=function(t,i){return new Ln(t,i)},t.Map=be,t.map=function(t,i){return new be(t,i)};var En=window.L;t.noConflict=function(){return window.L=En,this},window.L=t}); From f13c62a1c97fe41beb08284581d735545ef43612 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 19 Dec 2019 08:56:04 -0800 Subject: [PATCH 420/462] removing corefx javafx based gui --- corefx/pom.xml | 147 -- corefx/src/main/java/com/core/Controller.java | 517 ------- corefx/src/main/java/com/core/Main.java | 72 - .../java/com/core/client/ICoreClient.java | 119 -- .../com/core/client/grpc/CoreGrpcClient.java | 1294 ----------------- .../java/com/core/data/BridgeThroughput.java | 9 - .../java/com/core/data/ConfigDataType.java | 40 - .../main/java/com/core/data/ConfigGroup.java | 12 - .../main/java/com/core/data/ConfigOption.java | 15 - .../main/java/com/core/data/CoreEvent.java | 13 - .../java/com/core/data/CoreInterface.java | 15 - .../src/main/java/com/core/data/CoreLink.java | 34 - .../java/com/core/data/CoreLinkOptions.java | 21 - .../src/main/java/com/core/data/CoreNode.java | 54 - .../main/java/com/core/data/CoreService.java | 20 - .../main/java/com/core/data/EventType.java | 45 - corefx/src/main/java/com/core/data/Hook.java | 11 - .../com/core/data/InterfaceThroughput.java | 10 - .../main/java/com/core/data/LinkTypes.java | 31 - .../src/main/java/com/core/data/Location.java | 10 - .../java/com/core/data/LocationConfig.java | 10 - .../main/java/com/core/data/MessageFlags.java | 36 - .../java/com/core/data/MobilityConfig.java | 18 - .../src/main/java/com/core/data/NodeType.java | 95 -- .../src/main/java/com/core/data/Position.java | 12 - .../main/java/com/core/data/ServiceFile.java | 13 - .../src/main/java/com/core/data/Session.java | 16 - .../java/com/core/data/SessionOverview.java | 13 - .../main/java/com/core/data/SessionState.java | 38 - .../main/java/com/core/data/Throughputs.java | 12 - .../main/java/com/core/data/WlanConfig.java | 12 - .../main/java/com/core/datavis/CoreGraph.java | 11 - .../java/com/core/datavis/CoreGraphAxis.java | 15 - .../java/com/core/datavis/CoreGraphData.java | 15 - .../com/core/datavis/CoreGraphWrapper.java | 157 -- .../main/java/com/core/datavis/GraphType.java | 11 - .../core/graph/AbstractNodeContextMenu.java | 22 - .../com/core/graph/BackgroundPaintable.java | 51 - .../java/com/core/graph/CoreAddresses.java | 72 - .../graph/CoreAnnotatingGraphMousePlugin.java | 57 - .../graph/CoreEditingModalGraphMouse.java | 17 - .../com/core/graph/CoreObservableGraph.java | 31 - .../core/graph/CorePopupGraphMousePlugin.java | 83 -- .../core/graph/CoreVertexLabelRenderer.java | 43 - .../java/com/core/graph/EmaneContextMenu.java | 26 - .../java/com/core/graph/GraphContextMenu.java | 22 - .../java/com/core/graph/LinkContextMenu.java | 21 - .../java/com/core/graph/NetworkGraph.java | 578 -------- .../java/com/core/graph/NodeContextMenu.java | 116 -- .../main/java/com/core/graph/RadioIcon.java | 31 - .../java/com/core/graph/Rj45ContextMenu.java | 21 - .../com/core/graph/UndirectedSimpleGraph.java | 24 - .../java/com/core/graph/WlanContextMenu.java | 26 - .../java/com/core/ui/AnnotationToolbar.java | 104 -- .../main/java/com/core/ui/DetailsPanel.java | 171 --- .../main/java/com/core/ui/GraphToolbar.java | 292 ---- .../main/java/com/core/ui/LinkDetails.java | 69 - .../main/java/com/core/ui/NodeDetails.java | 120 -- .../main/java/com/core/ui/ServiceItem.java | 15 - corefx/src/main/java/com/core/ui/Toast.java | 52 - .../com/core/ui/config/BaseConfigItem.java | 19 - .../com/core/ui/config/BooleanConfigItem.java | 32 - .../com/core/ui/config/ConfigItemUtils.java | 29 - .../com/core/ui/config/DefaultConfigItem.java | 24 - .../com/core/ui/config/FileConfigItem.java | 56 - .../java/com/core/ui/config/IConfigItem.java | 13 - .../com/core/ui/config/SelectConfigItem.java | 29 - .../com/core/ui/dialogs/BackgroundDialog.java | 86 -- .../java/com/core/ui/dialogs/ChartDialog.java | 217 --- .../com/core/ui/dialogs/ConfigDialog.java | 91 -- .../com/core/ui/dialogs/ConnectDialog.java | 39 - .../com/core/ui/dialogs/CoreFoenixDialog.java | 58 - .../java/com/core/ui/dialogs/GeoDialog.java | 28 - .../core/ui/dialogs/GuiPreferencesDialog.java | 80 - .../java/com/core/ui/dialogs/HookDialog.java | 77 - .../java/com/core/ui/dialogs/HooksDialog.java | 117 -- .../com/core/ui/dialogs/LocationDialog.java | 73 - .../com/core/ui/dialogs/MobilityDialog.java | 113 -- .../core/ui/dialogs/MobilityPlayerDialog.java | 89 -- .../com/core/ui/dialogs/NodeEmaneDialog.java | 103 -- .../core/ui/dialogs/NodeServicesDialog.java | 148 -- .../core/ui/dialogs/NodeTypeCreateDialog.java | 74 - .../com/core/ui/dialogs/NodeTypesDialog.java | 136 -- .../com/core/ui/dialogs/NodeWlanDialog.java | 76 - .../java/com/core/ui/dialogs/Rj45Dialog.java | 45 - .../com/core/ui/dialogs/ServiceDialog.java | 119 -- .../com/core/ui/dialogs/SessionsDialog.java | 177 --- .../core/ui/dialogs/SessionsFoenixDialog.java | 70 - .../java/com/core/ui/dialogs/StageDialog.java | 142 -- .../com/core/ui/dialogs/TerminalDialog.java | 72 - .../com/core/ui/textfields/DoubleFilter.java | 15 - .../main/java/com/core/utils/ConfigUtils.java | 122 -- .../java/com/core/utils/Configuration.java | 23 - .../main/java/com/core/utils/FxmlUtils.java | 22 - .../main/java/com/core/utils/IconUtils.java | 65 - .../main/java/com/core/utils/JsonUtils.java | 53 - .../java/com/core/utils/NodeTypeConfig.java | 20 - corefx/src/main/proto/core.proto | 1 - corefx/src/main/resources/config.json | 7 - corefx/src/main/resources/core-icon.png | Bin 2931 -> 0 bytes .../main/resources/css/images/layers-2x.png | Bin 1259 -> 0 bytes .../src/main/resources/css/images/layers.png | Bin 696 -> 0 bytes .../resources/css/images/marker-icon-2x.png | Bin 2464 -> 0 bytes .../main/resources/css/images/marker-icon.png | Bin 1466 -> 0 bytes .../resources/css/images/marker-shadow.png | Bin 618 -> 0 bytes corefx/src/main/resources/css/leaflet.css | 635 -------- corefx/src/main/resources/css/main.css | 138 -- .../resources/fxml/annotation_toolbar.fxml | 26 - .../resources/fxml/background_dialog.fxml | 35 - .../src/main/resources/fxml/chart_dialog.fxml | 19 - .../main/resources/fxml/config_dialog.fxml | 22 - .../main/resources/fxml/connect_dialog.fxml | 18 - .../main/resources/fxml/details_panel.fxml | 36 - .../src/main/resources/fxml/geo_dialog.fxml | 23 - .../main/resources/fxml/graph_toolbar.fxml | 19 - .../main/resources/fxml/gui_preferences.fxml | 54 - .../src/main/resources/fxml/hook_dialog.fxml | 35 - .../src/main/resources/fxml/hooks_dialog.fxml | 28 - .../main/resources/fxml/location_dialog.fxml | 56 - corefx/src/main/resources/fxml/main.fxml | 79 - .../main/resources/fxml/mobility_dialog.fxml | 96 -- .../main/resources/fxml/mobility_player.fxml | 26 - .../resources/fxml/node_emane_dialog.fxml | 28 - .../resources/fxml/node_services_dialog.fxml | 75 - .../fxml/node_type_create_dialog.fxml | 18 - .../resources/fxml/node_types_dialog.fxml | 80 - .../src/main/resources/fxml/rj45_dialog.fxml | 30 - .../main/resources/fxml/service_dialog.fxml | 68 - .../main/resources/fxml/sessions_dialog.fxml | 20 - .../main/resources/fxml/terminal_dialog.fxml | 19 - .../src/main/resources/fxml/wlan_dialog.fxml | 37 - corefx/src/main/resources/html/geo.html | 56 - .../main/resources/icons/dockernode-100.png | Bin 362 -> 0 bytes corefx/src/main/resources/icons/emane-100.png | Bin 1624 -> 0 bytes corefx/src/main/resources/icons/host-100.png | Bin 942 -> 0 bytes corefx/src/main/resources/icons/hub-100.png | Bin 2386 -> 0 bytes .../main/resources/icons/icomoon_material.svg | 855 ----------- .../src/main/resources/icons/lxcnode-100.png | Bin 394 -> 0 bytes corefx/src/main/resources/icons/pc-100.png | Bin 741 -> 0 bytes corefx/src/main/resources/icons/rj45-80.png | Bin 162 -> 0 bytes .../src/main/resources/icons/router-100.png | Bin 2103 -> 0 bytes .../src/main/resources/icons/switch-100.png | Bin 1460 -> 0 bytes corefx/src/main/resources/icons/wlan-100.png | Bin 1509 -> 0 bytes corefx/src/main/resources/js/leaflet.js | 5 - corefx/src/main/resources/log4j2.xml | 17 - 145 files changed, 10450 deletions(-) delete mode 100644 corefx/pom.xml delete mode 100644 corefx/src/main/java/com/core/Controller.java delete mode 100644 corefx/src/main/java/com/core/Main.java delete mode 100644 corefx/src/main/java/com/core/client/ICoreClient.java delete mode 100644 corefx/src/main/java/com/core/client/grpc/CoreGrpcClient.java delete mode 100644 corefx/src/main/java/com/core/data/BridgeThroughput.java delete mode 100644 corefx/src/main/java/com/core/data/ConfigDataType.java delete mode 100644 corefx/src/main/java/com/core/data/ConfigGroup.java delete mode 100644 corefx/src/main/java/com/core/data/ConfigOption.java delete mode 100644 corefx/src/main/java/com/core/data/CoreEvent.java delete mode 100644 corefx/src/main/java/com/core/data/CoreInterface.java delete mode 100644 corefx/src/main/java/com/core/data/CoreLink.java delete mode 100644 corefx/src/main/java/com/core/data/CoreLinkOptions.java delete mode 100644 corefx/src/main/java/com/core/data/CoreNode.java delete mode 100644 corefx/src/main/java/com/core/data/CoreService.java delete mode 100644 corefx/src/main/java/com/core/data/EventType.java delete mode 100644 corefx/src/main/java/com/core/data/Hook.java delete mode 100644 corefx/src/main/java/com/core/data/InterfaceThroughput.java delete mode 100644 corefx/src/main/java/com/core/data/LinkTypes.java delete mode 100644 corefx/src/main/java/com/core/data/Location.java delete mode 100644 corefx/src/main/java/com/core/data/LocationConfig.java delete mode 100644 corefx/src/main/java/com/core/data/MessageFlags.java delete mode 100644 corefx/src/main/java/com/core/data/MobilityConfig.java delete mode 100644 corefx/src/main/java/com/core/data/NodeType.java delete mode 100644 corefx/src/main/java/com/core/data/Position.java delete mode 100644 corefx/src/main/java/com/core/data/ServiceFile.java delete mode 100644 corefx/src/main/java/com/core/data/Session.java delete mode 100644 corefx/src/main/java/com/core/data/SessionOverview.java delete mode 100644 corefx/src/main/java/com/core/data/SessionState.java delete mode 100644 corefx/src/main/java/com/core/data/Throughputs.java delete mode 100644 corefx/src/main/java/com/core/data/WlanConfig.java delete mode 100644 corefx/src/main/java/com/core/datavis/CoreGraph.java delete mode 100644 corefx/src/main/java/com/core/datavis/CoreGraphAxis.java delete mode 100644 corefx/src/main/java/com/core/datavis/CoreGraphData.java delete mode 100644 corefx/src/main/java/com/core/datavis/CoreGraphWrapper.java delete mode 100644 corefx/src/main/java/com/core/datavis/GraphType.java delete mode 100644 corefx/src/main/java/com/core/graph/AbstractNodeContextMenu.java delete mode 100644 corefx/src/main/java/com/core/graph/BackgroundPaintable.java delete mode 100644 corefx/src/main/java/com/core/graph/CoreAddresses.java delete mode 100644 corefx/src/main/java/com/core/graph/CoreAnnotatingGraphMousePlugin.java delete mode 100644 corefx/src/main/java/com/core/graph/CoreEditingModalGraphMouse.java delete mode 100644 corefx/src/main/java/com/core/graph/CoreObservableGraph.java delete mode 100644 corefx/src/main/java/com/core/graph/CorePopupGraphMousePlugin.java delete mode 100644 corefx/src/main/java/com/core/graph/CoreVertexLabelRenderer.java delete mode 100644 corefx/src/main/java/com/core/graph/EmaneContextMenu.java delete mode 100644 corefx/src/main/java/com/core/graph/GraphContextMenu.java delete mode 100644 corefx/src/main/java/com/core/graph/LinkContextMenu.java delete mode 100644 corefx/src/main/java/com/core/graph/NetworkGraph.java delete mode 100644 corefx/src/main/java/com/core/graph/NodeContextMenu.java delete mode 100644 corefx/src/main/java/com/core/graph/RadioIcon.java delete mode 100644 corefx/src/main/java/com/core/graph/Rj45ContextMenu.java delete mode 100644 corefx/src/main/java/com/core/graph/UndirectedSimpleGraph.java delete mode 100644 corefx/src/main/java/com/core/graph/WlanContextMenu.java delete mode 100644 corefx/src/main/java/com/core/ui/AnnotationToolbar.java delete mode 100644 corefx/src/main/java/com/core/ui/DetailsPanel.java delete mode 100644 corefx/src/main/java/com/core/ui/GraphToolbar.java delete mode 100644 corefx/src/main/java/com/core/ui/LinkDetails.java delete mode 100644 corefx/src/main/java/com/core/ui/NodeDetails.java delete mode 100644 corefx/src/main/java/com/core/ui/ServiceItem.java delete mode 100644 corefx/src/main/java/com/core/ui/Toast.java delete mode 100644 corefx/src/main/java/com/core/ui/config/BaseConfigItem.java delete mode 100644 corefx/src/main/java/com/core/ui/config/BooleanConfigItem.java delete mode 100644 corefx/src/main/java/com/core/ui/config/ConfigItemUtils.java delete mode 100644 corefx/src/main/java/com/core/ui/config/DefaultConfigItem.java delete mode 100644 corefx/src/main/java/com/core/ui/config/FileConfigItem.java delete mode 100644 corefx/src/main/java/com/core/ui/config/IConfigItem.java delete mode 100644 corefx/src/main/java/com/core/ui/config/SelectConfigItem.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/BackgroundDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/ChartDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/ConfigDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/ConnectDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/CoreFoenixDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/GeoDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/GuiPreferencesDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/HookDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/HooksDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/LocationDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/MobilityDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/MobilityPlayerDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/NodeEmaneDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/NodeServicesDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/NodeTypeCreateDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/NodeTypesDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/NodeWlanDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/Rj45Dialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/ServiceDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/SessionsDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/SessionsFoenixDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/StageDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/dialogs/TerminalDialog.java delete mode 100644 corefx/src/main/java/com/core/ui/textfields/DoubleFilter.java delete mode 100644 corefx/src/main/java/com/core/utils/ConfigUtils.java delete mode 100644 corefx/src/main/java/com/core/utils/Configuration.java delete mode 100644 corefx/src/main/java/com/core/utils/FxmlUtils.java delete mode 100644 corefx/src/main/java/com/core/utils/IconUtils.java delete mode 100644 corefx/src/main/java/com/core/utils/JsonUtils.java delete mode 100644 corefx/src/main/java/com/core/utils/NodeTypeConfig.java delete mode 120000 corefx/src/main/proto/core.proto delete mode 100644 corefx/src/main/resources/config.json delete mode 100644 corefx/src/main/resources/core-icon.png delete mode 100644 corefx/src/main/resources/css/images/layers-2x.png delete mode 100644 corefx/src/main/resources/css/images/layers.png delete mode 100644 corefx/src/main/resources/css/images/marker-icon-2x.png delete mode 100644 corefx/src/main/resources/css/images/marker-icon.png delete mode 100644 corefx/src/main/resources/css/images/marker-shadow.png delete mode 100644 corefx/src/main/resources/css/leaflet.css delete mode 100644 corefx/src/main/resources/css/main.css delete mode 100644 corefx/src/main/resources/fxml/annotation_toolbar.fxml delete mode 100644 corefx/src/main/resources/fxml/background_dialog.fxml delete mode 100644 corefx/src/main/resources/fxml/chart_dialog.fxml delete mode 100644 corefx/src/main/resources/fxml/config_dialog.fxml delete mode 100644 corefx/src/main/resources/fxml/connect_dialog.fxml delete mode 100644 corefx/src/main/resources/fxml/details_panel.fxml delete mode 100644 corefx/src/main/resources/fxml/geo_dialog.fxml delete mode 100644 corefx/src/main/resources/fxml/graph_toolbar.fxml delete mode 100644 corefx/src/main/resources/fxml/gui_preferences.fxml delete mode 100644 corefx/src/main/resources/fxml/hook_dialog.fxml delete mode 100644 corefx/src/main/resources/fxml/hooks_dialog.fxml delete mode 100644 corefx/src/main/resources/fxml/location_dialog.fxml delete mode 100644 corefx/src/main/resources/fxml/main.fxml delete mode 100644 corefx/src/main/resources/fxml/mobility_dialog.fxml delete mode 100644 corefx/src/main/resources/fxml/mobility_player.fxml delete mode 100644 corefx/src/main/resources/fxml/node_emane_dialog.fxml delete mode 100644 corefx/src/main/resources/fxml/node_services_dialog.fxml delete mode 100644 corefx/src/main/resources/fxml/node_type_create_dialog.fxml delete mode 100644 corefx/src/main/resources/fxml/node_types_dialog.fxml delete mode 100644 corefx/src/main/resources/fxml/rj45_dialog.fxml delete mode 100644 corefx/src/main/resources/fxml/service_dialog.fxml delete mode 100644 corefx/src/main/resources/fxml/sessions_dialog.fxml delete mode 100644 corefx/src/main/resources/fxml/terminal_dialog.fxml delete mode 100644 corefx/src/main/resources/fxml/wlan_dialog.fxml delete mode 100644 corefx/src/main/resources/html/geo.html delete mode 100644 corefx/src/main/resources/icons/dockernode-100.png delete mode 100644 corefx/src/main/resources/icons/emane-100.png delete mode 100644 corefx/src/main/resources/icons/host-100.png delete mode 100644 corefx/src/main/resources/icons/hub-100.png delete mode 100644 corefx/src/main/resources/icons/icomoon_material.svg delete mode 100644 corefx/src/main/resources/icons/lxcnode-100.png delete mode 100644 corefx/src/main/resources/icons/pc-100.png delete mode 100644 corefx/src/main/resources/icons/rj45-80.png delete mode 100644 corefx/src/main/resources/icons/router-100.png delete mode 100644 corefx/src/main/resources/icons/switch-100.png delete mode 100644 corefx/src/main/resources/icons/wlan-100.png delete mode 100644 corefx/src/main/resources/js/leaflet.js delete mode 100644 corefx/src/main/resources/log4j2.xml diff --git a/corefx/pom.xml b/corefx/pom.xml deleted file mode 100644 index ed5454e0..00000000 --- a/corefx/pom.xml +++ /dev/null @@ -1,147 +0,0 @@ - - - 4.0.0 - - com.core - corefx - 1.0-SNAPSHOT - - - UTF-8 - 1.8 - 1.8 - 2.1.1 - 2.10.0.pr2 - 1.20.0 - 2.9.0 - - - - - net.sf.jung - jung-api - ${jung.version} - - - net.sf.jung - jung-graph-impl - ${jung.version} - - - net.sf.jung - jung-algorithms - ${jung.version} - - - net.sf.jung - jung-io - ${jung.version} - - - net.sf.jung - jung-visualization - ${jung.version} - - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - - - com.fasterxml.jackson.core - jackson-annotations - ${jackson.version} - - - org.apache.logging.log4j - log4j-api - ${log4j.version} - - - org.apache.logging.log4j - log4j-core - ${log4j.version} - - - org.projectlombok - lombok - 1.18.0 - provided - - - com.jfoenix - jfoenix - 8.0.7 - - - io.grpc - grpc-netty-shaded - ${grpc.version} - - - io.grpc - grpc-protobuf - ${grpc.version} - - - io.grpc - grpc-stub - ${grpc.version} - - - com.google.guava - guava - 20.0 - - - com.github.seancfoley - ipaddress - 5.0.2 - - - - - - - kr.motd.maven - os-maven-plugin - 1.5.0.Final - - - - - com.zenjava - javafx-maven-plugin - 8.8.3 - - com.core.Main - - - - org.xolstice.maven.plugins - protobuf-maven-plugin - 0.5.1 - - com.google.protobuf:protoc:3.7.1:exe:${os.detected.classifier} - grpc-java - io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} - - - - - compile - compile-custom - - - - - - - diff --git a/corefx/src/main/java/com/core/Controller.java b/corefx/src/main/java/com/core/Controller.java deleted file mode 100644 index a9aa5021..00000000 --- a/corefx/src/main/java/com/core/Controller.java +++ /dev/null @@ -1,517 +0,0 @@ -package com.core; - -import com.core.client.ICoreClient; -import com.core.client.grpc.CoreGrpcClient; -import com.core.data.*; -import com.core.graph.NetworkGraph; -import com.core.ui.*; -import com.core.ui.dialogs.*; -import com.core.utils.ConfigUtils; -import com.core.utils.Configuration; -import com.core.utils.NodeTypeConfig; -import com.jfoenix.controls.JFXDecorator; -import com.jfoenix.controls.JFXProgressBar; -import javafx.application.Application; -import javafx.application.Platform; -import javafx.concurrent.Task; -import javafx.embed.swing.SwingNode; -import javafx.event.ActionEvent; -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.control.CheckMenuItem; -import javafx.scene.control.MenuItem; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.StackPane; -import javafx.scene.layout.VBox; -import javafx.stage.FileChooser; -import javafx.stage.Stage; -import lombok.Data; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.awt.event.ItemEvent; -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.util.*; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.stream.Collectors; - -@Data -public class Controller implements Initializable { - private static final Logger logger = LogManager.getLogger(); - @FXML private StackPane stackPane; - @FXML private BorderPane borderPane; - @FXML private VBox top; - @FXML private VBox bottom; - @FXML private SwingNode swingNode; - @FXML private MenuItem saveXmlMenuItem; - @FXML private JFXProgressBar progressBar; - @FXML private CheckMenuItem throughputMenuItem; - - private final ExecutorService executorService = Executors.newSingleThreadExecutor(); - private final Map mobilityScripts = new HashMap<>(); - private final Map mobilityPlayerDialogs = new HashMap<>(); - private Application application; - private JFXDecorator decorator; - private Stage window; - private Configuration configuration; - private Map> defaultServices = new HashMap<>(); - - // core client utilities - private ICoreClient coreClient = new CoreGrpcClient(); - - // ui elements - private NetworkGraph networkGraph = new NetworkGraph(this); - private AnnotationToolbar annotationToolbar = new AnnotationToolbar(networkGraph); - private NodeDetails nodeDetails = new NodeDetails(this); - private LinkDetails linkDetails = new LinkDetails(this); - private GraphToolbar graphToolbar = new GraphToolbar(this); - - // dialogs - private Rj45Dialog rj45Dialog = new Rj45Dialog(this); - private SessionsDialog sessionsDialog = new SessionsDialog(this); - private ServiceDialog serviceDialog = new ServiceDialog(this); - private NodeServicesDialog nodeServicesDialog = new NodeServicesDialog(this); - private NodeEmaneDialog nodeEmaneDialog = new NodeEmaneDialog(this); - private NodeWlanDialog nodeWlanDialog = new NodeWlanDialog(this); - private ConfigDialog configDialog = new ConfigDialog(this); - private HooksDialog hooksDialog = new HooksDialog(this); - private MobilityDialog mobilityDialog = new MobilityDialog(this); - private ChartDialog chartDialog = new ChartDialog(this); - private NodeTypesDialog nodeTypesDialog = new NodeTypesDialog(this); - private BackgroundDialog backgroundDialog = new BackgroundDialog(this); - private LocationDialog locationDialog = new LocationDialog(this); - private GeoDialog geoDialog = new GeoDialog(this); - private ConnectDialog connectDialog = new ConnectDialog(this); - private GuiPreferencesDialog guiPreferencesDialog = new GuiPreferencesDialog(this); - private NodeTypeCreateDialog nodeTypeCreateDialog = new NodeTypeCreateDialog(this); - - public void connectToCore(String address, int port) { - executorService.submit(() -> { - try { - coreClient.setConnection(address, port); - initialJoin(); - } catch (IOException ex) { - Toast.error(String.format("Connection failure: %s", ex.getMessage()), ex); - Platform.runLater(() -> connectDialog.showDialog()); - } - }); - } - - private void initialJoin() throws IOException { - Map> serviceGroups = coreClient.getServices(); - logger.info("core services: {}", serviceGroups); - nodeServicesDialog.setServices(serviceGroups); - nodeTypeCreateDialog.setServices(serviceGroups); - - logger.info("initial core session join"); - List sessions = coreClient.getSessions(); - - logger.info("existing sessions: {}", sessions); - Integer sessionId; - if (sessions.isEmpty()) { - logger.info("creating initial session"); - SessionOverview sessionOverview = coreClient.createSession(); - sessionId = sessionOverview.getId(); - Toast.info(String.format("Created Session %s", sessionId)); - } else { - SessionOverview sessionOverview = sessions.get(0); - sessionId = sessionOverview.getId(); - Toast.info(String.format("Joined Session %s", sessionId)); - } - - joinSession(sessionId); - - // set emane models - List emaneModels = coreClient.getEmaneModels(); - logger.info("emane models: {}", emaneModels); - nodeEmaneDialog.setModels(emaneModels); - } - - public void joinSession(Integer sessionId) throws IOException { - // clear graph - networkGraph.reset(); - - // clear out any previously set information - mobilityPlayerDialogs.clear(); - mobilityScripts.clear(); - mobilityDialog.setNode(null); - Platform.runLater(() -> borderPane.setRight(null)); - - // get session to join - Session session = coreClient.joinSession(sessionId); - - // display all nodes - for (CoreNode node : session.getNodes()) { - networkGraph.addNode(node); - } - - // display all links - for (CoreLink link : session.getLinks()) { - networkGraph.addLink(link); - } - - // refresh graph - networkGraph.getGraphViewer().repaint(); - - // update other components for new session - graphToolbar.setRunButton(coreClient.isRunning()); - hooksDialog.updateHooks(); - - // update session default services - setCoreDefaultServices(); - - // retrieve current mobility script configurations and show dialogs - Map mobilityConfigMap = coreClient.getMobilityConfigs(); - mobilityScripts.putAll(mobilityConfigMap); - showMobilityScriptDialogs(); - - Platform.runLater(() -> decorator.setTitle(String.format("CORE (Session %s)", sessionId))); - } - - public boolean startSession() { - // force nodes to get latest positions - networkGraph.updatePositions(); - - // retrieve items for creation/start - Collection nodes = networkGraph.getGraph().getVertices(); - Collection links = networkGraph.getGraph().getEdges(); - List hooks = hooksDialog.getHooks(); - - // start/create session - boolean result = false; - progressBar.setVisible(true); - try { - result = coreClient.start(nodes, links, hooks); - if (result) { - showMobilityScriptDialogs(); - saveXmlMenuItem.setDisable(false); - } - } catch (IOException ex) { - Toast.error("Failure Starting Session", ex); - } finally { - progressBar.setVisible(false); - } - return result; - } - - public boolean stopSession() throws IOException { - // clear out any drawn wireless links - List wirelessLinks = networkGraph.getGraph().getEdges().stream() - .filter(CoreLink::isWireless) - .collect(Collectors.toList()); - wirelessLinks.forEach(networkGraph::removeWirelessLink); - networkGraph.getGraphViewer().repaint(); - - // stop session - progressBar.setVisible(true); - boolean result = coreClient.stop(); - progressBar.setVisible(false); - if (result) { - saveXmlMenuItem.setDisable(true); - } - return result; - } - - public void handleThroughputs(Throughputs throughputs) { - for (InterfaceThroughput interfaceThroughput : throughputs.getInterfaces()) { - int nodeId = interfaceThroughput.getNode(); - CoreNode node = networkGraph.getVertex(nodeId); - Collection links = networkGraph.getGraph().getIncidentEdges(node); - int interfaceId = interfaceThroughput.getNodeInterface(); - for (CoreLink link : links) { - if (nodeId == link.getNodeOne()) { - if (interfaceId == link.getInterfaceOne().getId()) { - link.setThroughput(interfaceThroughput.getThroughput()); - } - } else { - if (interfaceId == link.getInterfaceTwo().getId()) { - link.setThroughput(interfaceThroughput.getThroughput()); - } - } - } - } - networkGraph.getGraphViewer().repaint(); - } - - private void setCoreDefaultServices() { - try { - coreClient.setDefaultServices(defaultServices); - } catch (IOException ex) { - Toast.error("Error updating core default services", ex); - } - } - - public void updateNodeTypes() { - graphToolbar.setupNodeTypes(); - setCoreDefaultServices(); - try { - ConfigUtils.save(configuration); - } catch (IOException ex) { - Toast.error("Error saving configuration", ex); - } - } - - public void deleteNode(CoreNode node) { - networkGraph.removeNode(node); - CoreNode mobilityNode = mobilityDialog.getNode(); - if (mobilityNode != null && mobilityNode.getId().equals(node.getId())) { - mobilityDialog.setNode(null); - } - } - - void setWindow(Stage window) { - this.window = window; - sessionsDialog.setOwner(window); - hooksDialog.setOwner(window); - nodeServicesDialog.setOwner(window); - serviceDialog.setOwner(window); - nodeWlanDialog.setOwner(window); - nodeEmaneDialog.setOwner(window); - configDialog.setOwner(window); - mobilityDialog.setOwner(window); - nodeTypesDialog.setOwner(window); - backgroundDialog.setOwner(window); - locationDialog.setOwner(window); - connectDialog.setOwner(window); - guiPreferencesDialog.setOwner(window); - nodeTypeCreateDialog.setOwner(window); - rj45Dialog.setOwner(window); - } - - private void showMobilityScriptDialogs() { - for (Map.Entry entry : mobilityScripts.entrySet()) { - Integer nodeId = entry.getKey(); - CoreNode node = networkGraph.getVertex(nodeId); - MobilityConfig mobilityConfig = entry.getValue(); - Platform.runLater(() -> { - MobilityPlayerDialog mobilityPlayerDialog = new MobilityPlayerDialog(this, node); - mobilityPlayerDialog.setOwner(window); - mobilityPlayerDialogs.put(nodeId, mobilityPlayerDialog); - mobilityPlayerDialog.showDialog(mobilityConfig); - }); - } - } - - @FXML - private void onCoreMenuConnect(ActionEvent event) { - logger.info("showing connect!"); - connectDialog.showDialog(); - } - - @FXML - private void onOptionsMenuNodeTypes(ActionEvent event) { - nodeTypesDialog.showDialog(); - } - - @FXML - private void onOptionsMenuBackground(ActionEvent event) { - backgroundDialog.showDialog(); - } - - @FXML - private void onOptionsMenuLocation(ActionEvent event) { - locationDialog.showDialog(); - } - - @FXML - private void onOptionsMenuPreferences(ActionEvent event) { - guiPreferencesDialog.showDialog(); - } - - @FXML - private void onHelpMenuWebsite(ActionEvent event) { - application.getHostServices().showDocument("https://github.com/coreemu/core"); - } - - @FXML - private void onHelpMenuDocumentation(ActionEvent event) { - application.getHostServices().showDocument("http://coreemu.github.io/core/"); - } - - @FXML - private void onOpenXmlAction() { - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle("Open Session"); - fileChooser.setInitialDirectory(new File(configuration.getXmlPath())); - fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("XML", "*.xml")); - try { - File file = fileChooser.showOpenDialog(window); - if (file != null) { - openXml(file); - } - } catch (IllegalArgumentException ex) { - Toast.error(String.format("Invalid XML directory: %s", configuration.getXmlPath())); - } - } - - private void openXml(File file) { - logger.info("opening session xml: {}", file.getPath()); - try { - SessionOverview sessionOverview = coreClient.openSession(file); - Integer sessionId = sessionOverview.getId(); - joinSession(sessionId); - Toast.info(String.format("Joined Session %s", sessionId)); - } catch (IOException ex) { - Toast.error("Error opening session xml", ex); - } - } - - @FXML - private void onSaveXmlAction() { - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle("Save Session"); - fileChooser.setInitialFileName("session.xml"); - fileChooser.setInitialDirectory(new File(configuration.getXmlPath())); - fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("XML", "*.xml")); - File file = fileChooser.showSaveDialog(window); - if (file != null) { - logger.info("saving session xml: {}", file.getPath()); - try { - coreClient.saveSession(file); - } catch (IOException ex) { - Toast.error("Error saving session xml", ex); - } - } - } - - @FXML - private void onSessionMenu(ActionEvent event) { - logger.info("sessions menu clicked"); - try { - sessionsDialog.showDialog(); - } catch (IOException ex) { - Toast.error("Error retrieving sessions", ex); - } - } - - @FXML - private void onSessionHooksMenu(ActionEvent event) { - hooksDialog.showDialog(); - } - - @FXML - private void onSessionOptionsMenu(ActionEvent event) { - try { - List configGroups = coreClient.getSessionConfig(); - configDialog.showDialog("Session Options", configGroups, () -> { - List options = configDialog.getOptions(); - try { - boolean result = coreClient.setSessionConfig(options); - if (result) { - Toast.info("Session options saved"); - } else { - Toast.error("Failure to set session config"); - } - } catch (IOException ex) { - logger.error("error getting session config"); - } - }); - } catch (IOException ex) { - logger.error("error getting session config"); - } - } - - @FXML - private void onTestMenuCharts(ActionEvent event) { - chartDialog.show(); - } - - @FXML - private void onTestMenuGeo(ActionEvent event) { - geoDialog.showDialog(); - } - - @Override - public void initialize(URL location, ResourceBundle resources) { - configuration = ConfigUtils.load(); - String address = configuration.getCoreAddress(); - int port = configuration.getCorePort(); - logger.info("core connection: {}:{}", address, port); - connectDialog.setAddress(address); - connectDialog.setPort(port); - connectToCore(address, port); - - logger.info("controller initialize"); - coreClient.initialize(this); - swingNode.setContent(networkGraph.getGraphViewer()); - - // update graph preferences - networkGraph.updatePreferences(configuration); - - // set node types / default services - graphToolbar.setupNodeTypes(); - defaultServices = configuration.getNodeTypeConfigs().stream() - .collect(Collectors.toMap(NodeTypeConfig::getModel, NodeTypeConfig::getServices)); - - // set graph toolbar - borderPane.setLeft(graphToolbar); - - // setup snackbar - Toast.setSnackbarRoot(stackPane); - - // setup throughput menu item - throughputMenuItem.setOnAction(event -> executorService.submit(new ChangeThroughputTask())); - - // node details - networkGraph.getGraphViewer().getPickedVertexState().addItemListener(event -> { - CoreNode node = (CoreNode) event.getItem(); - logger.info("picked: {}", node.getName()); - if (event.getStateChange() == ItemEvent.SELECTED) { - Platform.runLater(() -> { - nodeDetails.setNode(node); - borderPane.setRight(nodeDetails); - }); - } else { - Platform.runLater(() -> borderPane.setRight(null)); - } - }); - - // edge details - networkGraph.getGraphViewer().getPickedEdgeState().addItemListener(event -> { - CoreLink link = (CoreLink) event.getItem(); - logger.info("picked: {} - {}", link.getNodeOne(), link.getNodeTwo()); - if (event.getStateChange() == ItemEvent.SELECTED) { - Platform.runLater(() -> { - linkDetails.setLink(link); - borderPane.setRight(linkDetails); - }); - } else { - Platform.runLater(() -> borderPane.setRight(null)); - } - }); - } - - private class ChangeThroughputTask extends Task { - @Override - protected Boolean call() throws Exception { - if (throughputMenuItem.isSelected()) { - return coreClient.startThroughput(Controller.this); - } else { - return coreClient.stopThroughput(); - } - } - - @Override - protected void succeeded() { - if (getValue()) { - if (throughputMenuItem.isSelected()) { - networkGraph.setShowThroughput(true); - } else { - networkGraph.setShowThroughput(false); - networkGraph.getGraph().getEdges().forEach(edge -> edge.setThroughput(0)); - networkGraph.getGraphViewer().repaint(); - } - } else { - Toast.error("Failure changing throughput"); - } - } - - @Override - protected void failed() { - Toast.error("Error changing throughput", new RuntimeException(getException())); - } - } -} diff --git a/corefx/src/main/java/com/core/Main.java b/corefx/src/main/java/com/core/Main.java deleted file mode 100644 index 4834278f..00000000 --- a/corefx/src/main/java/com/core/Main.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.core; - -import com.core.utils.ConfigUtils; -import com.jfoenix.controls.JFXDecorator; -import com.jfoenix.svg.SVGGlyphLoader; -import javafx.application.Application; -import javafx.application.Platform; -import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; -import javafx.scene.Scene; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.text.Font; -import javafx.stage.Stage; - -import java.nio.file.Path; -import java.nio.file.Paths; - -public class Main extends Application { - private static final Path LOG_FILE = Paths.get(System.getProperty("user.home"), ".core", "core.log"); - - @Override - public void start(Stage window) throws Exception { - // set core dir property for logging - System.setProperty("core_log", LOG_FILE.toString()); - - // check for and create gui home directory - ConfigUtils.checkHomeDirectory(); - - // load svg icons - SVGGlyphLoader.loadGlyphsFont(getClass().getResourceAsStream("/icons/icomoon_material.svg"), - "icomoon.svg"); - - // load font - Font.loadFont(getClass().getResourceAsStream("/font/roboto/Roboto-Regular.ttf"), 10); - - // load main fxml - FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/main.fxml")); - Parent root = loader.load(); - - // window decorator - JFXDecorator decorator = new JFXDecorator(window, root); - decorator.setCustomMaximize(true); - decorator.setMaximized(true); - decorator.setTitle("CORE"); - Image coreIcon = new Image(getClass().getResourceAsStream("/core-icon.png")); - decorator.setGraphic(new ImageView(coreIcon)); - window.getIcons().add(coreIcon); - - // create scene and set as current scene within window - Scene scene = new Scene(decorator); - scene.getStylesheets().add(getClass().getResource("/css/main.css").toExternalForm()); - window.setScene(scene); - - // update controller - Controller controller = loader.getController(); - controller.setApplication(this); - controller.setWindow(window); - controller.setDecorator(decorator); - - // configure window - window.setOnCloseRequest(event -> { - Platform.exit(); - System.exit(0); - }); - window.show(); - } - - public static void main(String[] args) { - launch(args); - } -} diff --git a/corefx/src/main/java/com/core/client/ICoreClient.java b/corefx/src/main/java/com/core/client/ICoreClient.java deleted file mode 100644 index 4060956f..00000000 --- a/corefx/src/main/java/com/core/client/ICoreClient.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.core.client; - -import com.core.Controller; -import com.core.data.*; - -import java.io.File; -import java.io.IOException; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; - -public interface ICoreClient { - void setConnection(String address, int port); - - boolean isLocalConnection(); - - Integer currentSession(); - - boolean startThroughput(Controller controller) throws IOException; - - boolean stopThroughput() throws IOException; - - SessionOverview createSession() throws IOException; - - boolean deleteSession(Integer sessionId) throws IOException; - - List getSessions() throws IOException; - - Session joinSession(Integer sessionId) throws IOException; - - Session getSession(Integer sessionId) throws IOException; - - boolean start(Collection nodes, Collection links, List hooks) throws IOException; - - boolean stop() throws IOException; - - boolean setState(SessionState state) throws IOException; - - Map> getServices() throws IOException; - - Map> getDefaultServices() throws IOException; - - boolean setDefaultServices(Map> defaults) throws IOException; - - CoreService getService(CoreNode node, String serviceName) throws IOException; - - boolean setService(CoreNode node, String serviceName, CoreService service) throws IOException; - - String getServiceFile(CoreNode node, String serviceName, String fileName) throws IOException; - - boolean startService(CoreNode node, String serviceName) throws IOException; - - boolean stopService(CoreNode node, String serviceName) throws IOException; - - boolean restartService(CoreNode node, String serviceName) throws IOException; - - boolean validateService(CoreNode node, String serviceName) throws IOException; - - boolean setServiceFile(CoreNode node, String serviceName, ServiceFile serviceFile) throws IOException; - - List getEmaneConfig(CoreNode node) throws IOException; - - List getEmaneModels() throws IOException; - - boolean setEmaneConfig(CoreNode node, List options) throws IOException; - - List getEmaneModelConfig(Integer id, String model) throws IOException; - - boolean setEmaneModelConfig(Integer id, String model, List options) throws IOException; - - boolean isRunning(); - - void saveSession(File file) throws IOException; - - SessionOverview openSession(File file) throws IOException; - - List getSessionConfig() throws IOException; - - boolean setSessionConfig(List configOptions) throws IOException; - - boolean createNode(CoreNode node) throws IOException; - - String nodeCommand(CoreNode node, String command) throws IOException; - - boolean editNode(CoreNode node) throws IOException; - - boolean deleteNode(CoreNode node) throws IOException; - - boolean createLink(CoreLink link) throws IOException; - - boolean editLink(CoreLink link) throws IOException; - - boolean createHook(Hook hook) throws IOException; - - List getHooks() throws IOException; - - WlanConfig getWlanConfig(CoreNode node) throws IOException; - - boolean setWlanConfig(CoreNode node, WlanConfig config) throws IOException; - - String getTerminalCommand(CoreNode node) throws IOException; - - Map getMobilityConfigs() throws IOException; - - boolean setMobilityConfig(CoreNode node, MobilityConfig config) throws IOException; - - MobilityConfig getMobilityConfig(CoreNode node) throws IOException; - - boolean mobilityAction(CoreNode node, String action) throws IOException; - - LocationConfig getLocationConfig() throws IOException; - - boolean setLocationConfig(LocationConfig config) throws IOException; - - void initialize(Controller controller); - - List getInterfaces() throws IOException; -} diff --git a/corefx/src/main/java/com/core/client/grpc/CoreGrpcClient.java b/corefx/src/main/java/com/core/client/grpc/CoreGrpcClient.java deleted file mode 100644 index 665dd1a4..00000000 --- a/corefx/src/main/java/com/core/client/grpc/CoreGrpcClient.java +++ /dev/null @@ -1,1294 +0,0 @@ -package com.core.client.grpc; - -import com.core.Controller; -import com.core.client.ICoreClient; -import com.core.data.*; -import com.core.ui.dialogs.MobilityPlayerDialog; -import inet.ipaddr.IPAddress; -import inet.ipaddr.IPAddressString; -import io.grpc.Context; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import io.grpc.StatusRuntimeException; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.io.File; -import java.io.IOException; -import java.io.PrintWriter; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.*; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public class CoreGrpcClient implements ICoreClient { - private static final Logger logger = LogManager.getLogger(); - private String address; - private int port; - private Integer sessionId; - private SessionState sessionState; - private CoreApiGrpc.CoreApiBlockingStub blockingStub; - private ManagedChannel channel; - private final ExecutorService executorService = Executors.newFixedThreadPool(6); - private boolean handlingEvents = false; - private boolean handlingThroughputs = false; - private Controller controller; - - private CoreProto.Node nodeToProto(CoreNode node) { - CoreProto.Position position = CoreProto.Position.newBuilder() - .setX(node.getPosition().getX().intValue()) - .setY(node.getPosition().getY().intValue()) - .build(); - CoreProto.Node.Builder builder = CoreProto.Node.newBuilder() - .addAllServices(node.getServices()) - .setType(CoreProto.NodeType.Enum.forNumber(node.getType())) - .setPosition(position); - if (node.getId() != null) { - builder.setId(node.getId()); - } - if (node.getName() != null) { - builder.setName(node.getName()); - } - if (node.getEmane() != null) { - builder.setEmane(node.getEmane()); - } - if (node.getModel() != null) { - builder.setModel(node.getModel()); - } - if (node.getIcon() != null) { - builder.setIcon(node.getIcon()); - } - if (node.getImage() != null) { - builder.setImage(node.getImage()); - } - - return builder.build(); - } - - private List protoToConfigGroups(List protoConfigs) { - List configs = new ArrayList<>(); - for (CoreProto.ConfigGroup protoConfig : protoConfigs) { - ConfigGroup config = new ConfigGroup(); - config.setName(protoConfig.getName()); - for (CoreProto.ConfigOption protoOption : protoConfig.getOptionsList()) { - ConfigOption option = new ConfigOption(); - option.setType(protoOption.getType()); - option.setLabel(protoOption.getLabel()); - option.setName(protoOption.getName()); - option.setValue(protoOption.getValue()); - option.setSelect(protoOption.getSelectList()); - config.getOptions().add(option); - } - configs.add(config); - } - return configs; - } - - private CoreProto.LinkOptions linkOptionsToProto(CoreLinkOptions options) { - CoreProto.LinkOptions.Builder builder = CoreProto.LinkOptions.newBuilder(); - if (options.getUnidirectional() != null) { - builder.setUnidirectional(options.getUnidirectional()); - } - if (options.getBandwidth() != null) { - builder.setBandwidth(options.getBandwidth()); - } - if (options.getBurst() != null) { - builder.setBurst(options.getBurst()); - } - if (options.getDelay() != null) { - builder.setDelay(options.getDelay()); - } - if (options.getDup() != null) { - builder.setDup(options.getDup()); - } - if (options.getJitter() != null) { - builder.setJitter(options.getJitter()); - } - if (options.getMburst() != null) { - builder.setMburst(options.getMburst()); - } - if (options.getMer() != null) { - builder.setMer(options.getMer()); - } - if (options.getPer() != null) { - builder.setPer(options.getPer().floatValue()); - } - if (options.getKey() != null) { - builder.setKey(options.getKey()); - } - if (options.getOpaque() != null) { - builder.setOpaque(options.getOpaque()); - } - return builder.build(); - } - - private CoreProto.Interface interfaceToProto(CoreInterface coreInterface) { - CoreProto.Interface.Builder builder = CoreProto.Interface.newBuilder(); - if (coreInterface.getId() != null) { - builder.setId(coreInterface.getId()); - } - if (coreInterface.getName() != null) { - builder.setName(coreInterface.getName()); - } - if (coreInterface.getMac() != null) { - builder.setMac(coreInterface.getMac()); - } - if (coreInterface.getIp4() != null) { - builder.setIp4(coreInterface.getIp4().toAddressString().getHostAddress().toString()); - } - if (coreInterface.getIp4() != null) { - builder.setIp4Mask(coreInterface.getIp4().getPrefixLength()); - } - if (coreInterface.getIp6() != null) { - builder.setIp6(coreInterface.getIp6().toAddressString().getHostAddress().toString()); - } - if (coreInterface.getIp6() != null) { - builder.setIp6Mask(coreInterface.getIp6().getPrefixLength()); - } - return builder.build(); - } - - private Map configOptionListToMap(List options) { - Map config = new HashMap<>(); - for (ConfigOption option : options) { - config.put(option.getName(), option.getValue()); - } - return config; - } - - private CoreNode protoToNode(CoreProto.Node protoNode) { - CoreNode node = new CoreNode(protoNode.getId()); - node.setType(protoNode.getTypeValue()); - node.setName(protoNode.getName()); - node.setIcon(protoNode.getIcon()); - node.setModel(protoNode.getModel()); - if (!protoNode.getEmane().isEmpty()) { - node.setEmane(protoNode.getEmane()); - } - if (!protoNode.getImage().isEmpty()) { - node.setImage(protoNode.getImage()); - } - node.setServices(new HashSet<>(protoNode.getServicesList())); - node.getPosition().setX((double) protoNode.getPosition().getX()); - node.getPosition().setY((double) protoNode.getPosition().getY()); - NodeType nodeType = NodeType.find(node.getType(), node.getModel()); - if (nodeType == null) { - logger.error("failed to find node type({}) model({}): {}", - node.getType(), node.getModel(), node.getName()); - } - node.setNodeType(nodeType); - node.setLoaded(true); - return node; - } - - private CoreInterface protoToInterface(CoreProto.Interface protoInterface) { - CoreInterface coreInterface = new CoreInterface(); - coreInterface.setId(protoInterface.getId()); - coreInterface.setName(protoInterface.getName()); - if (!protoInterface.getMac().isEmpty()) { - coreInterface.setMac(protoInterface.getMac()); - } - String ip4String = String.format("%s/%s", protoInterface.getIp4(), protoInterface.getIp4Mask()); - IPAddress ip4 = new IPAddressString(ip4String).getAddress(); - coreInterface.setIp4(ip4); - String ip6String = String.format("%s/%s", protoInterface.getIp6(), protoInterface.getIp6Mask()); - IPAddress ip6 = new IPAddressString(ip6String).getAddress(); - coreInterface.setIp6(ip6); - return coreInterface; - } - - private CoreLink protoToLink(CoreProto.Link linkProto) { - CoreLink link = new CoreLink(); - link.setType(linkProto.getTypeValue()); - link.setNodeOne(linkProto.getNodeOneId()); - link.setNodeTwo(linkProto.getNodeTwoId()); - CoreInterface interfaceOne = protoToInterface(linkProto.getInterfaceOne()); - link.setInterfaceOne(interfaceOne); - CoreInterface interfaceTwo = protoToInterface(linkProto.getInterfaceTwo()); - link.setInterfaceTwo(interfaceTwo); - - CoreLinkOptions options = new CoreLinkOptions(); - CoreProto.LinkOptions protoOptions = linkProto.getOptions(); - options.setBandwidth((int) protoOptions.getBandwidth()); - options.setDelay((int) protoOptions.getDelay()); - options.setDup((int) protoOptions.getDup()); - options.setJitter((int) protoOptions.getJitter()); - options.setPer((double) protoOptions.getPer()); - options.setBurst((int) protoOptions.getBurst()); - if (protoOptions.hasField(CoreProto.LinkOptions.getDescriptor().findFieldByName("key"))) { - options.setKey(protoOptions.getKey()); - } - options.setMburst((int) protoOptions.getMburst()); - options.setMer((int) protoOptions.getMer()); - options.setOpaque(protoOptions.getOpaque()); - options.setUnidirectional(protoOptions.getUnidirectional()); - link.setOptions(options); - - return link; - } - - @Override - public void initialize(Controller controller) { - this.controller = controller; - } - - @Override - public void setConnection(String address, int port) { - this.address = address; - this.port = port; - logger.info("set connection: {}:{}", this.address, this.port); - channel = ManagedChannelBuilder.forAddress(this.address, this.port).usePlaintext().build(); - logger.info("channel: {}", channel); - blockingStub = CoreApiGrpc.newBlockingStub(channel); - logger.info("stub: {}", blockingStub); - } - - @Override - public boolean isLocalConnection() { - return address.equals("127.0.0.1") || address.equals("localhost"); - } - - @Override - public Integer currentSession() { - return sessionId; - } - - @Override - public boolean startThroughput(Controller controller) throws IOException { - CoreProto.ThroughputsRequest request = CoreProto.ThroughputsRequest.newBuilder().build(); - try { - handlingThroughputs = true; - executorService.submit(() -> { - Context.CancellableContext context = Context.current().withCancellation(); - context.run(() -> { - try { - Iterator iterator = blockingStub.throughputs(request); - while (handlingThroughputs) { - CoreProto.ThroughputsEvent event = iterator.next(); - logger.info("handling throughputs: {}", event); - Throughputs throughputs = new Throughputs(); - for (CoreProto.BridgeThroughput protoBridge : event.getBridgeThroughputsList()) { - BridgeThroughput bridge = new BridgeThroughput(); - bridge.setNode(protoBridge.getNodeId()); - bridge.setThroughput(protoBridge.getThroughput()); - throughputs.getBridges().add(bridge); - } - for (CoreProto.InterfaceThroughput protoInterface : event.getInterfaceThroughputsList()) { - InterfaceThroughput interfaceThroughput = new InterfaceThroughput(); - interfaceThroughput.setNode(protoInterface.getNodeId()); - interfaceThroughput.setNodeInterface(protoInterface.getInterfaceId()); - interfaceThroughput.setThroughput(protoInterface.getThroughput()); - throughputs.getInterfaces().add(interfaceThroughput); - } - controller.handleThroughputs(throughputs); - } - logger.info("exiting handling throughputs"); - } catch (StatusRuntimeException ex) { - logger.error("error handling session events", ex); - } finally { - context.cancel(null); - context.close(); - } - }); - }); - return true; - } catch (StatusRuntimeException ex) { - throw new IOException("setup event handlers error", ex); - } - } - - @Override - public boolean stopThroughput() throws IOException { - logger.info("cancelling throughputs"); - handlingThroughputs = false; - return true; - } - - @Override - public SessionOverview createSession() throws IOException { - CoreProto.CreateSessionRequest request = CoreProto.CreateSessionRequest.newBuilder().build(); - try { - CoreProto.CreateSessionResponse response = blockingStub.createSession(request); - SessionOverview overview = new SessionOverview(); - overview.setId(response.getSessionId()); - overview.setState(response.getStateValue()); - overview.setNodes(0); - return overview; - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean deleteSession(Integer sessionId) throws IOException { - CoreProto.DeleteSessionRequest request = CoreProto.DeleteSessionRequest.newBuilder() - .setSessionId(sessionId).build(); - try { - return blockingStub.deleteSession(request).getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public List getSessions() throws IOException { - CoreProto.GetSessionsRequest request = CoreProto.GetSessionsRequest.newBuilder().build(); - try { - CoreProto.GetSessionsResponse response = blockingStub.getSessions(request); - List sessions = new ArrayList<>(); - for (CoreProto.SessionSummary summary : response.getSessionsList()) { - SessionOverview overview = new SessionOverview(); - overview.setId(summary.getId()); - overview.setNodes(summary.getNodes()); - overview.setState(summary.getStateValue()); - sessions.add(overview); - } - return sessions; - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public Session getSession(Integer sessionId) throws IOException { - logger.info("getting session: {}", sessionId); - CoreProto.GetSessionRequest request = CoreProto.GetSessionRequest.newBuilder().setSessionId(sessionId).build(); - try { - CoreProto.GetSessionResponse response = blockingStub.getSession(request); - Session session = new Session(); - session.setId(sessionId); - for (CoreProto.Node protoNode : response.getSession().getNodesList()) { - if (CoreProto.NodeType.Enum.PEER_TO_PEER == protoNode.getType()) { - continue; - } - - logger.info("adding node: {}", protoNode); - CoreNode node = protoToNode(protoNode); - session.getNodes().add(node); - } - for (CoreProto.Link linkProto : response.getSession().getLinksList()) { - logger.info("adding link: {}", linkProto); - CoreLink link = protoToLink(linkProto); - session.getLinks().add(link); - } - SessionState state = SessionState.get(response.getSession().getStateValue()); - session.setState(state); - return session; - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public Session joinSession(Integer sessionId) throws IOException { - // stop handling previous session events if currently running - if (isRunning()) { - handlingEvents = false; - } - - // join desired session - Session session = getSession(sessionId); - this.sessionId = session.getId(); - sessionState = session.getState(); - logger.info("joining session({}) state({})", this.sessionId, sessionState); - - // setup event handlers if joined session is running - if (isRunning()) { - setupEventHandlers(); - } - - return session; - } - - @Override - public boolean start(Collection nodes, Collection links, List hooks) throws IOException { - setupEventHandlers(); - - boolean result = setState(SessionState.DEFINITION); - if (!result) { - return false; - } - - result = setState(SessionState.CONFIGURATION); - if (!result) { - return false; - } - - for (Hook hook : hooks) { - if (!createHook(hook)) { - return false; - } - } - - for (CoreNode node : nodes) { - // must pre-configure wlan nodes, if not already - if (node.getNodeType().getValue() == NodeType.WLAN) { - WlanConfig config = getWlanConfig(node); - setWlanConfig(node, config); - } - - if (!createNode(node)) { - return false; - } - } - - for (CoreLink link : links) { - if (!createLink(link)) { - return false; - } - } - - return setState(SessionState.INSTANTIATION); - } - - @Override - public boolean stop() throws IOException { - handlingEvents = false; - return setState(SessionState.SHUTDOWN); - } - - @Override - public boolean setState(SessionState state) throws IOException { - CoreProto.SetSessionStateRequest request = CoreProto.SetSessionStateRequest.newBuilder() - .setSessionId(sessionId) - .setStateValue(state.getValue()) - .build(); - try { - CoreProto.SetSessionStateResponse response = blockingStub.setSessionState(request); - if (response.getResult()) { - sessionState = state; - } - return response.getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public Map> getServices() throws IOException { - CoreProto.GetServicesRequest request = CoreProto.GetServicesRequest.newBuilder().build(); - try { - CoreProto.GetServicesResponse response = blockingStub.getServices(request); - Map> servicesMap = new HashMap<>(); - for (CoreProto.Service protoService : response.getServicesList()) { - List services = servicesMap.computeIfAbsent(protoService.getGroup(), x -> new ArrayList<>()); - services.add(protoService.getName()); - } - return servicesMap; - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public Map> getDefaultServices() throws IOException { - CoreProto.GetServiceDefaultsRequest request = CoreProto.GetServiceDefaultsRequest.newBuilder().build(); - try { - CoreProto.GetServiceDefaultsResponse response = blockingStub.getServiceDefaults(request); - Map> servicesMap = new HashMap<>(); - for (CoreProto.ServiceDefaults serviceDefaults : response.getDefaultsList()) { - servicesMap.put(serviceDefaults.getNodeType(), serviceDefaults.getServicesList()); - } - return servicesMap; - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean setDefaultServices(Map> defaults) throws IOException { - List allDefaults = new ArrayList<>(); - for (Map.Entry> entry : defaults.entrySet()) { - String nodeType = entry.getKey(); - Set services = entry.getValue(); - CoreProto.ServiceDefaults serviceDefaults = CoreProto.ServiceDefaults.newBuilder() - .setNodeType(nodeType) - .addAllServices(services) - .build(); - allDefaults.add(serviceDefaults); - } - CoreProto.SetServiceDefaultsRequest request = CoreProto.SetServiceDefaultsRequest.newBuilder() - .setSessionId(sessionId) - .addAllDefaults(allDefaults) - .build(); - try { - CoreProto.SetServiceDefaultsResponse response = blockingStub.setServiceDefaults(request); - return response.getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public CoreService getService(CoreNode node, String serviceName) throws IOException { - CoreProto.GetNodeServiceRequest request = CoreProto.GetNodeServiceRequest.newBuilder().build(); - try { - CoreProto.GetNodeServiceResponse response = blockingStub.getNodeService(request); - CoreProto.NodeServiceData nodeServiceData = response.getService(); - CoreService service = new CoreService(); - service.setShutdown(nodeServiceData.getShutdownList()); - service.setStartup(nodeServiceData.getStartupList()); - service.setValidate(nodeServiceData.getValidateList()); - service.setConfigs(nodeServiceData.getConfigsList()); - service.setDependencies(nodeServiceData.getDependenciesList()); - service.setDirs(nodeServiceData.getDirsList()); - service.setExecutables(nodeServiceData.getExecutablesList()); - service.setMeta(nodeServiceData.getMeta()); - service.setValidationMode(nodeServiceData.getValidationMode().name()); - service.setValidationTimer(Integer.toString(nodeServiceData.getValidationTimer())); - return service; - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean setService(CoreNode node, String serviceName, CoreService service) throws IOException { - CoreProto.SetNodeServiceRequest request = CoreProto.SetNodeServiceRequest.newBuilder() - .setNodeId(node.getId()) - .setSessionId(sessionId) - .setService(serviceName) - .build(); - request.getShutdownList().addAll(service.getShutdown()); - request.getValidateList().addAll(service.getValidate()); - request.getStartupList().addAll(service.getStartup()); - try { - return blockingStub.setNodeService(request).getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public String getServiceFile(CoreNode node, String serviceName, String fileName) throws IOException { - CoreProto.GetNodeServiceFileRequest request = CoreProto.GetNodeServiceFileRequest.newBuilder() - .setSessionId(sessionId) - .setNodeId(node.getId()) - .setService(serviceName) - .build(); - try { - CoreProto.GetNodeServiceFileResponse response = blockingStub.getNodeServiceFile(request); - return response.getData(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean startService(CoreNode node, String serviceName) throws IOException { - CoreProto.ServiceActionRequest request = CoreProto.ServiceActionRequest.newBuilder() - .setSessionId(sessionId) - .setNodeId(node.getId()) - .setService(serviceName) - .setAction(CoreProto.ServiceAction.Enum.START) - .build(); - try { - return blockingStub.serviceAction(request).getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean stopService(CoreNode node, String serviceName) throws IOException { - CoreProto.ServiceActionRequest request = CoreProto.ServiceActionRequest.newBuilder() - .setSessionId(sessionId) - .setNodeId(node.getId()) - .setService(serviceName) - .setAction(CoreProto.ServiceAction.Enum.STOP) - .build(); - try { - return blockingStub.serviceAction(request).getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean restartService(CoreNode node, String serviceName) throws IOException { - CoreProto.ServiceActionRequest request = CoreProto.ServiceActionRequest.newBuilder() - .setSessionId(sessionId) - .setNodeId(node.getId()) - .setService(serviceName) - .setAction(CoreProto.ServiceAction.Enum.RESTART) - .build(); - try { - return blockingStub.serviceAction(request).getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean validateService(CoreNode node, String serviceName) throws IOException { - CoreProto.ServiceActionRequest request = CoreProto.ServiceActionRequest.newBuilder() - .setSessionId(sessionId) - .setNodeId(node.getId()) - .setService(serviceName) - .setAction(CoreProto.ServiceAction.Enum.VALIDATE) - .build(); - try { - return blockingStub.serviceAction(request).getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean setServiceFile(CoreNode node, String serviceName, ServiceFile serviceFile) throws IOException { - CoreProto.SetNodeServiceFileRequest request = CoreProto.SetNodeServiceFileRequest.newBuilder() - .setSessionId(sessionId) - .setNodeId(node.getId()) - .setService(serviceName) - .setFile(serviceFile.getName()) - .setData(serviceFile.getData()) - .build(); - try { - CoreProto.SetNodeServiceFileResponse response = blockingStub.setNodeServiceFile(request); - return response.getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public List getEmaneConfig(CoreNode node) throws IOException { - CoreProto.GetEmaneConfigRequest request = CoreProto.GetEmaneConfigRequest.newBuilder() - .setSessionId(sessionId) - .build(); - try { - CoreProto.GetEmaneConfigResponse response = blockingStub.getEmaneConfig(request); - return protoToConfigGroups(response.getGroupsList()); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public List getEmaneModels() throws IOException { - CoreProto.GetEmaneModelsRequest request = CoreProto.GetEmaneModelsRequest.newBuilder() - .setSessionId(sessionId) - .build(); - try { - CoreProto.GetEmaneModelsResponse response = blockingStub.getEmaneModels(request); - return new ArrayList<>(response.getModelsList()); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean setEmaneConfig(CoreNode node, List options) throws IOException { - Map config = configOptionListToMap(options); - CoreProto.SetEmaneConfigRequest request = CoreProto.SetEmaneConfigRequest.newBuilder() - .setSessionId(sessionId) - .putAllConfig(config) - .build(); - try { - CoreProto.SetEmaneConfigResponse response = blockingStub.setEmaneConfig(request); - return response.getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public List getEmaneModelConfig(Integer id, String model) throws IOException { - CoreProto.GetEmaneModelConfigRequest request = CoreProto.GetEmaneModelConfigRequest.newBuilder() - .setSessionId(sessionId) - .setNodeId(id) - .setModel(model) - .build(); - try { - CoreProto.GetEmaneModelConfigResponse response = blockingStub.getEmaneModelConfig(request); - return protoToConfigGroups(response.getGroupsList()); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean setEmaneModelConfig(Integer id, String model, List options) throws IOException { - Map config = configOptionListToMap(options); - CoreProto.SetEmaneModelConfigRequest request = CoreProto.SetEmaneModelConfigRequest.newBuilder() - .setSessionId(sessionId) - .setNodeId(id) - .setModel(model) - .putAllConfig(config) - .build(); - try { - CoreProto.SetEmaneModelConfigResponse response = blockingStub.setEmaneModelConfig(request); - return response.getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean isRunning() { - return sessionState == SessionState.RUNTIME; - } - - @Override - public void saveSession(File file) throws IOException { - CoreProto.SaveXmlRequest request = CoreProto.SaveXmlRequest.newBuilder() - .setSessionId(sessionId) - .build(); - try { - CoreProto.SaveXmlResponse response = blockingStub.saveXml(request); - try (PrintWriter writer = new PrintWriter(file)) { - writer.print(response.getData()); - } - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public SessionOverview openSession(File file) throws IOException { - try { - byte[] encoded = Files.readAllBytes(file.toPath()); - String data = new String(encoded, StandardCharsets.UTF_8); - CoreProto.OpenXmlRequest request = CoreProto.OpenXmlRequest.newBuilder() - .setData(data) - .build(); - - CoreProto.OpenXmlResponse response = blockingStub.openXml(request); - SessionOverview sessionOverview = new SessionOverview(); - sessionOverview.setId(response.getSessionId()); - return sessionOverview; - } catch (IOException | StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public List getSessionConfig() throws IOException { - CoreProto.GetSessionOptionsRequest request = CoreProto.GetSessionOptionsRequest.newBuilder() - .setSessionId(sessionId) - .build(); - try { - CoreProto.GetSessionOptionsResponse response = blockingStub.getSessionOptions(request); - return protoToConfigGroups(response.getGroupsList()); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean setSessionConfig(List configOptions) throws IOException { - Map config = configOptionListToMap(configOptions); - CoreProto.SetSessionOptionsRequest request = CoreProto.SetSessionOptionsRequest.newBuilder() - .setSessionId(sessionId) - .putAllConfig(config) - .build(); - try { - CoreProto.SetSessionOptionsResponse response = blockingStub.setSessionOptions(request); - return response.getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean createNode(CoreNode node) throws IOException { - CoreProto.Node protoNode = nodeToProto(node); - CoreProto.AddNodeRequest request = CoreProto.AddNodeRequest.newBuilder() - .setSessionId(sessionId) - .setNode(protoNode) - .build(); - try { - blockingStub.addNode(request); - return true; - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public String nodeCommand(CoreNode node, String command) throws IOException { - CoreProto.NodeCommandRequest request = CoreProto.NodeCommandRequest.newBuilder() - .setSessionId(sessionId) - .setNodeId(node.getId()) - .setCommand(command) - .build(); - try { - CoreProto.NodeCommandResponse response = blockingStub.nodeCommand(request); - return response.getOutput(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean editNode(CoreNode node) throws IOException { - CoreProto.Position position = CoreProto.Position.newBuilder() - .setX(node.getPosition().getX().intValue()) - .setY(node.getPosition().getY().intValue()) - .build(); - CoreProto.EditNodeRequest request = CoreProto.EditNodeRequest.newBuilder() - .setSessionId(sessionId) - .setNodeId(node.getId()) - .setPosition(position) - .build(); - try { - CoreProto.EditNodeResponse response = blockingStub.editNode(request); - return response.getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean deleteNode(CoreNode node) throws IOException { - CoreProto.DeleteNodeRequest request = CoreProto.DeleteNodeRequest.newBuilder() - .setSessionId(sessionId) - .setNodeId(node.getId()) - .build(); - try { - CoreProto.DeleteNodeResponse response = blockingStub.deleteNode(request); - return response.getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean createLink(CoreLink link) throws IOException { - CoreProto.Link.Builder builder = CoreProto.Link.newBuilder() - .setTypeValue(link.getType()); - if (link.getNodeOne() != null) { - builder.setNodeOneId(link.getNodeOne()); - } - if (link.getNodeTwo() != null) { - builder.setNodeTwoId(link.getNodeTwo()); - } - if (link.getInterfaceOne() != null) { - builder.setInterfaceOne(interfaceToProto(link.getInterfaceOne())); - } - if (link.getInterfaceTwo() != null) { - builder.setInterfaceTwo(interfaceToProto(link.getInterfaceTwo())); - } - if (link.getOptions() != null) { - builder.setOptions(linkOptionsToProto(link.getOptions())); - } - CoreProto.Link protoLink = builder.build(); - CoreProto.AddLinkRequest request = CoreProto.AddLinkRequest.newBuilder() - .setSessionId(sessionId) - .setLink(protoLink) - .build(); - try { - CoreProto.AddLinkResponse response = blockingStub.addLink(request); - return response.getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean editLink(CoreLink link) throws IOException { - CoreProto.EditLinkRequest.Builder builder = CoreProto.EditLinkRequest.newBuilder() - .setSessionId(sessionId); - if (link.getNodeOne() != null) { - builder.setNodeOneId(link.getNodeOne()); - } - if (link.getNodeTwo() != null) { - builder.setNodeTwoId(link.getNodeTwo()); - } - if (link.getInterfaceOne() != null) { - builder.setInterfaceOneId(link.getInterfaceOne().getId()); - } - if (link.getInterfaceTwo() != null) { - builder.setInterfaceTwoId(link.getInterfaceTwo().getId()); - } - if (link.getOptions() != null) { - CoreProto.LinkOptions protoOptions = linkOptionsToProto(link.getOptions()); - builder.setOptions(protoOptions); - } - CoreProto.EditLinkRequest request = builder.build(); - try { - CoreProto.EditLinkResponse response = blockingStub.editLink(request); - return response.getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean createHook(Hook hook) throws IOException { - CoreProto.Hook hookProto = CoreProto.Hook.newBuilder() - .setStateValue(hook.getState()) - .setData(hook.getData()) - .setFile(hook.getFile()) - .build(); - CoreProto.AddHookRequest request = CoreProto.AddHookRequest.newBuilder() - .setHook(hookProto) - .build(); - try { - CoreProto.AddHookResponse response = blockingStub.addHook(request); - return response.getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public List getHooks() throws IOException { - CoreProto.GetHooksRequest request = CoreProto.GetHooksRequest.newBuilder().setSessionId(sessionId).build(); - try { - CoreProto.GetHooksResponse response = blockingStub.getHooks(request); - List hooks = new ArrayList<>(); - for (CoreProto.Hook protoHook : response.getHooksList()) { - Hook hook = new Hook(); - hook.setFile(protoHook.getFile()); - hook.setData(protoHook.getData()); - hook.setState(protoHook.getStateValue()); - hooks.add(hook); - } - return hooks; - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public WlanConfig getWlanConfig(CoreNode node) throws IOException { - CoreProto.GetWlanConfigRequest request = CoreProto.GetWlanConfigRequest.newBuilder() - .setSessionId(sessionId) - .setNodeId(node.getId()) - .build(); - try { - CoreProto.GetWlanConfigResponse response = blockingStub.getWlanConfig(request); - Map protoConfig = new HashMap<>(); - for (CoreProto.ConfigGroup group : response.getGroupsList()) { - for (CoreProto.ConfigOption option : group.getOptionsList()) { - protoConfig.put(option.getName(), option.getValue()); - } - } - WlanConfig config = new WlanConfig(); - config.setBandwidth(protoConfig.get("bandwidth")); - config.setDelay(protoConfig.get("delay")); - config.setError(protoConfig.get("error")); - config.setJitter(protoConfig.get("jitter")); - config.setRange(protoConfig.get("range")); - return config; - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean setWlanConfig(CoreNode node, WlanConfig config) throws IOException { - Map protoConfig = new HashMap<>(); - protoConfig.put("bandwidth", config.getBandwidth()); - protoConfig.put("delay", config.getDelay()); - protoConfig.put("error", config.getError()); - protoConfig.put("jitter", config.getJitter()); - protoConfig.put("range", config.getRange()); - CoreProto.SetWlanConfigRequest request = CoreProto.SetWlanConfigRequest.newBuilder() - .setSessionId(sessionId) - .setNodeId(node.getId()) - .putAllConfig(protoConfig) - .build(); - try { - CoreProto.SetWlanConfigResponse response = blockingStub.setWlanConfig(request); - return response.getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public String getTerminalCommand(CoreNode node) throws IOException { - CoreProto.GetNodeTerminalRequest request = CoreProto.GetNodeTerminalRequest.newBuilder() - .setSessionId(sessionId) - .setNodeId(node.getId()) - .build(); - try { - return blockingStub.getNodeTerminal(request).getTerminal(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public Map getMobilityConfigs() throws IOException { - CoreProto.GetMobilityConfigsRequest request = CoreProto.GetMobilityConfigsRequest.newBuilder() - .setSessionId(sessionId).build(); - try { - CoreProto.GetMobilityConfigsResponse response = blockingStub.getMobilityConfigs(request); - - Map mobilityConfigs = new HashMap<>(); - for (Integer nodeId : response.getConfigsMap().keySet()) { - CoreProto.GetMobilityConfigsResponse.MobilityConfig protoMobilityConfig = response.getConfigsMap() - .get(nodeId); - MobilityConfig mobilityConfig = new MobilityConfig(); - Map protoConfig = new HashMap<>(); - for (CoreProto.ConfigGroup group : protoMobilityConfig.getGroupsList()) { - for (CoreProto.ConfigOption option : group.getOptionsList()) { - protoConfig.put(option.getName(), option.getValue()); - } - } - mobilityConfig.setFile(protoConfig.get("file")); - mobilityConfig.setRefresh(Integer.parseInt(protoConfig.get("refresh_ms"))); - mobilityConfig.setAutostart(protoConfig.get("autostart")); - mobilityConfig.setLoop(protoConfig.get("loop")); - mobilityConfig.setPauseScript(protoConfig.get("script_pause")); - mobilityConfig.setStartScript(protoConfig.get("script_start")); - mobilityConfig.setStopScript(protoConfig.get("script_stop")); - mobilityConfig.setMap(protoConfig.get("map")); - mobilityConfigs.put(nodeId, mobilityConfig); - } - return mobilityConfigs; - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean setMobilityConfig(CoreNode node, MobilityConfig config) throws IOException { - Map protoConfig = new HashMap<>(); - protoConfig.put("file", config.getFile()); - if (config.getRefresh() != null) { - protoConfig.put("refresh_ms", config.getRefresh().toString()); - } - protoConfig.put("autostart", config.getAutostart()); - protoConfig.put("loop", config.getLoop()); - protoConfig.put("map", config.getMap()); - protoConfig.put("script_pause", config.getPauseScript()); - protoConfig.put("script_start", config.getStartScript()); - protoConfig.put("script_stop", config.getStopScript()); - CoreProto.SetMobilityConfigRequest request = CoreProto.SetMobilityConfigRequest.newBuilder() - .setSessionId(sessionId) - .setNodeId(node.getId()) - .putAllConfig(protoConfig) - .build(); - try { - CoreProto.SetMobilityConfigResponse response = blockingStub.setMobilityConfig(request); - return response.getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public MobilityConfig getMobilityConfig(CoreNode node) throws IOException { - CoreProto.GetMobilityConfigRequest request = CoreProto.GetMobilityConfigRequest.newBuilder() - .setSessionId(sessionId) - .setNodeId(node.getId()) - .build(); - try { - CoreProto.GetMobilityConfigResponse response = blockingStub.getMobilityConfig(request); - Map protoConfig = new HashMap<>(); - for (CoreProto.ConfigGroup group : response.getGroupsList()) { - for (CoreProto.ConfigOption option : group.getOptionsList()) { - protoConfig.put(option.getName(), option.getValue()); - } - } - MobilityConfig config = new MobilityConfig(); - config.setFile(protoConfig.get("file")); - config.setRefresh(Integer.parseInt(protoConfig.get("refresh_ms"))); - config.setAutostart(protoConfig.get("autostart")); - config.setLoop(protoConfig.get("loop")); - config.setPauseScript(protoConfig.get("script_pause")); - config.setStartScript(protoConfig.get("script_start")); - config.setStopScript(protoConfig.get("script_stop")); - config.setMap(protoConfig.get("map")); - return config; - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean mobilityAction(CoreNode node, String action) throws IOException { - CoreProto.MobilityActionRequest request = CoreProto.MobilityActionRequest.newBuilder() - .setSessionId(sessionId) - .setNodeId(node.getId()) - .setAction(CoreProto.MobilityAction.Enum.valueOf(action)) - .build(); - try { - CoreProto.MobilityActionResponse response = blockingStub.mobilityAction(request); - return response.getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public LocationConfig getLocationConfig() throws IOException { - CoreProto.GetSessionLocationRequest request = CoreProto.GetSessionLocationRequest.newBuilder() - .setSessionId(sessionId) - .build(); - try { - CoreProto.GetSessionLocationResponse response = blockingStub.getSessionLocation(request); - LocationConfig config = new LocationConfig(); - config.setScale((double) response.getScale()); - config.getPosition().setX((double) response.getPosition().getX()); - config.getPosition().setY((double) response.getPosition().getY()); - config.getPosition().setZ((double) response.getPosition().getZ()); - config.getLocation().setLatitude((double) response.getPosition().getLat()); - config.getLocation().setLongitude((double) response.getPosition().getLon()); - config.getLocation().setAltitude((double) response.getPosition().getAlt()); - return config; - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - @Override - public boolean setLocationConfig(LocationConfig config) throws IOException { - CoreProto.SetSessionLocationRequest.Builder builder = CoreProto.SetSessionLocationRequest.newBuilder() - .setSessionId(sessionId); - if (config.getScale() != null) { - builder.setScale(config.getScale().floatValue()); - } - CoreProto.Position.Builder positionBuilder = CoreProto.Position.newBuilder(); - if (config.getPosition().getX() != null) { - positionBuilder.setX(config.getPosition().getX().intValue()); - } - if (config.getPosition().getY() != null) { - positionBuilder.setY(config.getPosition().getY().intValue()); - } - if (config.getPosition().getZ() != null) { - positionBuilder.setZ(config.getPosition().getZ().intValue()); - } - if (config.getLocation().getLongitude() != null) { - positionBuilder.setLon(config.getLocation().getLongitude().floatValue()); - } - if (config.getLocation().getLatitude() != null) { - positionBuilder.setLat(config.getLocation().getLatitude().floatValue()); - } - if (config.getLocation().getAltitude() != null) { - positionBuilder.setAlt(config.getLocation().getAltitude().floatValue()); - } - try { - CoreProto.SetSessionLocationResponse response = blockingStub.setSessionLocation(builder.build()); - return response.getResult(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } - - private void setupEventHandlers() throws IOException { - logger.info("setting up event handlers"); - handlingEvents = true; - try { - CoreProto.EventsRequest request = CoreProto.EventsRequest.newBuilder() - .setSessionId(sessionId) - .build(); - - Iterator events = blockingStub.events(request); - executorService.submit(() -> { - Context.CancellableContext context = Context.current().withCancellation(); - context.run(() -> { - try { - while (handlingEvents) { - CoreProto.Event event = events.next(); - logger.info("handling event: {}", event); - switch (event.getEventTypeCase()) { - case SESSION_EVENT: - handleSessionEvents(event.getSessionEvent()); - break; - case NODE_EVENT: - handleNodeEvents(event.getNodeEvent()); - break; - case LINK_EVENT: - handleLinkEvents(event.getLinkEvent()); - break; - case CONFIG_EVENT: - handleConfigEvents(event.getConfigEvent()); - break; - case EXCEPTION_EVENT: - handleExceptionEvents(event.getExceptionEvent()); - break; - case FILE_EVENT: - handleFileEvents(event.getFileEvent()); - break; - default: - logger.error("unknown event type: {}", event.getEventTypeCase()); - } - } - } catch (StatusRuntimeException ex) { - logger.error("error handling session events", ex); - } finally { - context.cancel(null); - context.close(); - } - }); - }); - } catch (StatusRuntimeException ex) { - throw new IOException("setup event handlers error", ex); - } - } - - private void handleSessionEvents(CoreProto.SessionEvent event) { - logger.info("session event: {}", event); - SessionState state = SessionState.get(event.getEvent()); - if (state == null) { - logger.warn("unknown session event: {}", event.getEvent()); - return; - } - - // session state event - if (state.getValue() <= 6) { - logger.info("event updating session state: {}", state); - sessionState = state; - // mobility script event - } else if (state.getValue() <= 9) { - Integer nodeId = event.getNodeId(); - String[] values = event.getData().split("\\s+"); - Integer start = Integer.parseInt(values[0].split("=")[1]); - Integer end = Integer.parseInt(values[1].split("=")[1]); - logger.info(String.format("node(%s) mobility event (%s) - start(%s) stop(%s)", - nodeId, state, start, end)); - logger.info("all dialogs: {}", controller.getMobilityPlayerDialogs().keySet()); - MobilityPlayerDialog mobilityPlayerDialog = controller.getMobilityPlayerDialogs().get(nodeId); - mobilityPlayerDialog.event(state, start, end); - } - } - - private void handleNodeEvents(CoreProto.NodeEvent event) { - logger.info("node event: {}", event); - CoreNode node = protoToNode(event.getNode()); - controller.getNetworkGraph().setNodeLocation(node); - } - - private void handleExceptionEvents(CoreProto.ExceptionEvent event) { - logger.info("exception event: {}", event); - } - - private void handleConfigEvents(CoreProto.ConfigEvent event) { - logger.info("config event: {}", event); - } - - private void handleLinkEvents(CoreProto.LinkEvent event) { - logger.info("link event: {}", event); - CoreLink link = protoToLink(event.getLink()); - MessageFlags flag = MessageFlags.get(event.getMessageTypeValue()); - if (MessageFlags.DELETE == flag) { - logger.info("delete"); - controller.getNetworkGraph().removeWirelessLink(link); - } else if (MessageFlags.ADD == flag) { - link.setLoaded(true); - controller.getNetworkGraph().addLink(link); - } - controller.getNetworkGraph().getGraphViewer().repaint(); - } - - private void handleFileEvents(CoreProto.FileEvent event) { - logger.info("file event: {}", event); - } - - @Override - public List getInterfaces() throws IOException { - CoreProto.GetInterfacesRequest request = CoreProto.GetInterfacesRequest.newBuilder().build(); - try { - CoreProto.GetInterfacesResponse response = blockingStub.getInterfaces(request); - return response.getInterfacesList(); - } catch (StatusRuntimeException ex) { - throw new IOException(ex); - } - } -} diff --git a/corefx/src/main/java/com/core/data/BridgeThroughput.java b/corefx/src/main/java/com/core/data/BridgeThroughput.java deleted file mode 100644 index 167fb402..00000000 --- a/corefx/src/main/java/com/core/data/BridgeThroughput.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.core.data; - -import lombok.Data; - -@Data -public class BridgeThroughput { - private int node; - private Double throughput; -} diff --git a/corefx/src/main/java/com/core/data/ConfigDataType.java b/corefx/src/main/java/com/core/data/ConfigDataType.java deleted file mode 100644 index 3dd44864..00000000 --- a/corefx/src/main/java/com/core/data/ConfigDataType.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.core.data; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -public enum ConfigDataType { - UINT8(1), - UINT16(2), - UINT32(3), - UINT64(4), - INT8(5), - INT16(6), - INT32(7), - INT64(8), - FLOAT(9), - STRING(10), - BOOL(11); - - private static final Map LOOKUP = new HashMap<>(); - - static { - Arrays.stream(ConfigDataType.values()).forEach(x -> LOOKUP.put(x.getValue(), x)); - } - - private final int value; - - ConfigDataType(int value) { - this.value = value; - } - - public int getValue() { - return value; - } - - - public static ConfigDataType get(int value) { - return LOOKUP.get(value); - } -} diff --git a/corefx/src/main/java/com/core/data/ConfigGroup.java b/corefx/src/main/java/com/core/data/ConfigGroup.java deleted file mode 100644 index 9b45bdb7..00000000 --- a/corefx/src/main/java/com/core/data/ConfigGroup.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.core.data; - -import lombok.Data; - -import java.util.ArrayList; -import java.util.List; - -@Data -public class ConfigGroup { - private String name; - private List options = new ArrayList<>(); -} diff --git a/corefx/src/main/java/com/core/data/ConfigOption.java b/corefx/src/main/java/com/core/data/ConfigOption.java deleted file mode 100644 index 77af265e..00000000 --- a/corefx/src/main/java/com/core/data/ConfigOption.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.core.data; - -import lombok.Data; - -import java.util.ArrayList; -import java.util.List; - -@Data -public class ConfigOption { - private String label; - private String name; - private String value; - private Integer type; - private List select = new ArrayList<>(); -} diff --git a/corefx/src/main/java/com/core/data/CoreEvent.java b/corefx/src/main/java/com/core/data/CoreEvent.java deleted file mode 100644 index 718097d8..00000000 --- a/corefx/src/main/java/com/core/data/CoreEvent.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.core.data; - -import lombok.Data; - -@Data -public class CoreEvent { - private Integer session; - private Integer node; - private String name; - private Double time; - private EventType eventType; - private String data; -} diff --git a/corefx/src/main/java/com/core/data/CoreInterface.java b/corefx/src/main/java/com/core/data/CoreInterface.java deleted file mode 100644 index 200aeabd..00000000 --- a/corefx/src/main/java/com/core/data/CoreInterface.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.core.data; - -import inet.ipaddr.IPAddress; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -public class CoreInterface { - private Integer id; - private String name; - private String mac; - private IPAddress ip4; - private IPAddress ip6; -} diff --git a/corefx/src/main/java/com/core/data/CoreLink.java b/corefx/src/main/java/com/core/data/CoreLink.java deleted file mode 100644 index 8500c437..00000000 --- a/corefx/src/main/java/com/core/data/CoreLink.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.core.data; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@EqualsAndHashCode(onlyExplicitlyIncluded = true) -public class CoreLink { - @EqualsAndHashCode.Include - private Integer id; - private Float weight = 1.0f; - private boolean loaded = true; - private double throughput; - private boolean visible = true; - private Integer messageType; - private Integer type = LinkTypes.WIRED.getValue(); - private Integer nodeOne; - private Integer nodeTwo; - private CoreInterface interfaceOne; - private CoreInterface interfaceTwo; - private CoreLinkOptions options = new CoreLinkOptions(); - - public CoreLink(Integer id) { - this.id = id; - this.weight = (float) id; - this.loaded = false; - } - - public boolean isWireless() { - return interfaceOne == null && interfaceTwo == null; - } -} diff --git a/corefx/src/main/java/com/core/data/CoreLinkOptions.java b/corefx/src/main/java/com/core/data/CoreLinkOptions.java deleted file mode 100644 index 036b5e20..00000000 --- a/corefx/src/main/java/com/core/data/CoreLinkOptions.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.core.data; - -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -public class CoreLinkOptions { - private String opaque; - private Integer session; - private Integer jitter; - private Integer key; - private Integer mburst; - private Integer mer; - private Double per; - private Integer bandwidth; - private Integer burst; - private Integer delay; - private Integer dup; - private Boolean unidirectional; -} diff --git a/corefx/src/main/java/com/core/data/CoreNode.java b/corefx/src/main/java/com/core/data/CoreNode.java deleted file mode 100644 index 042355d3..00000000 --- a/corefx/src/main/java/com/core/data/CoreNode.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.core.data; - -import com.core.graph.RadioIcon; -import com.core.utils.IconUtils; -import edu.uci.ics.jung.visualization.LayeredIcon; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.util.HashSet; -import java.util.Set; - -@Data -@NoArgsConstructor -@EqualsAndHashCode(onlyExplicitlyIncluded = true) -public class CoreNode { - private static final Logger logger = LogManager.getLogger(); - @EqualsAndHashCode.Include - private Integer id; - private String name; - private Integer type; - private String model; - private Position position = new Position(); - private Set services = new HashSet<>(); - private String emane; - private String url; - private NodeType nodeType; - private String icon; - private String image; - private boolean loaded = true; - private LayeredIcon graphIcon; - private RadioIcon radioIcon = new RadioIcon(); - - public CoreNode(Integer id) { - this.id = id; - this.name = String.format("Node%s", this.id); - this.loaded = false; - } - - public void setNodeType(NodeType nodeType) { - type = nodeType.getValue(); - model = nodeType.getModel(); - icon = nodeType.getIcon(); - if (icon.startsWith("file:")) { - graphIcon = IconUtils.getExternalLayeredIcon(icon); - } else { - graphIcon = IconUtils.getLayeredIcon(icon); - } - graphIcon.add(radioIcon); - this.nodeType = nodeType; - } -} diff --git a/corefx/src/main/java/com/core/data/CoreService.java b/corefx/src/main/java/com/core/data/CoreService.java deleted file mode 100644 index c33aebed..00000000 --- a/corefx/src/main/java/com/core/data/CoreService.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.core.data; - -import lombok.Data; - -import java.util.ArrayList; -import java.util.List; - -@Data -public class CoreService { - private List executables = new ArrayList<>(); - private List dependencies = new ArrayList<>(); - private List dirs = new ArrayList<>(); - private List configs = new ArrayList<>(); - private List startup = new ArrayList<>(); - private List validate = new ArrayList<>(); - private String validationMode; - private String validationTimer; - private List shutdown = new ArrayList<>(); - private String meta; -} diff --git a/corefx/src/main/java/com/core/data/EventType.java b/corefx/src/main/java/com/core/data/EventType.java deleted file mode 100644 index 487de4c1..00000000 --- a/corefx/src/main/java/com/core/data/EventType.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.core.data; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -public enum EventType { - NONE(0), - DEFINITION_STATE(1), - CONFIGURATION_STATE(2), - INSTANTIATION_STATE(3), - RUNTIME_STATE(4), - DATACOLLECT_STATE(5), - SHUTDOWN_STATE(6), - START(7), - STOP(8), - PAUSE(9), - RESTART(10), - FILE_OPEN(11), - FILE_SAVE(12), - SCHEDULED(13), - RECONFIGURE(14), - INSTANTIATION_COMPLETE(15); - - private static final Map LOOKUP = new HashMap<>(); - - static { - Arrays.stream(EventType.values()).forEach(x -> LOOKUP.put(x.getValue(), x)); - } - - private final int value; - - EventType(int value) { - this.value = value; - } - - public int getValue() { - return value; - } - - - public static EventType get(int value) { - return LOOKUP.get(value); - } -} diff --git a/corefx/src/main/java/com/core/data/Hook.java b/corefx/src/main/java/com/core/data/Hook.java deleted file mode 100644 index 652976b0..00000000 --- a/corefx/src/main/java/com/core/data/Hook.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.core.data; - -import lombok.Data; - -@Data -public class Hook { - private String file; - private Integer state; - private String stateDisplay; - private String data; -} diff --git a/corefx/src/main/java/com/core/data/InterfaceThroughput.java b/corefx/src/main/java/com/core/data/InterfaceThroughput.java deleted file mode 100644 index 8df86501..00000000 --- a/corefx/src/main/java/com/core/data/InterfaceThroughput.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.core.data; - -import lombok.Data; - -@Data -public class InterfaceThroughput { - private int node; - private int nodeInterface; - private double throughput; -} diff --git a/corefx/src/main/java/com/core/data/LinkTypes.java b/corefx/src/main/java/com/core/data/LinkTypes.java deleted file mode 100644 index 4bfaf1dc..00000000 --- a/corefx/src/main/java/com/core/data/LinkTypes.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.core.data; - -import java.util.HashMap; -import java.util.Map; - -public enum LinkTypes { - WIRELESS(0), - WIRED(1); - - private static final Map LOOKUP = new HashMap<>(); - - static { - for (LinkTypes state : LinkTypes.values()) { - LOOKUP.put(state.getValue(), state); - } - } - - private final int value; - - LinkTypes(int value) { - this.value = value; - } - - public int getValue() { - return this.value; - } - - public static LinkTypes get(int value) { - return LOOKUP.get(value); - } -} diff --git a/corefx/src/main/java/com/core/data/Location.java b/corefx/src/main/java/com/core/data/Location.java deleted file mode 100644 index f5c399c4..00000000 --- a/corefx/src/main/java/com/core/data/Location.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.core.data; - -import lombok.Data; - -@Data -public class Location { - private Double latitude = 0.0; - private Double longitude = 0.0; - private Double altitude = 0.0; -} diff --git a/corefx/src/main/java/com/core/data/LocationConfig.java b/corefx/src/main/java/com/core/data/LocationConfig.java deleted file mode 100644 index 10ba11ac..00000000 --- a/corefx/src/main/java/com/core/data/LocationConfig.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.core.data; - -import lombok.Data; - -@Data -public class LocationConfig { - private Position position = new Position(); - private Location location = new Location(); - private Double scale; -} diff --git a/corefx/src/main/java/com/core/data/MessageFlags.java b/corefx/src/main/java/com/core/data/MessageFlags.java deleted file mode 100644 index 6272099b..00000000 --- a/corefx/src/main/java/com/core/data/MessageFlags.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.core.data; - -import java.util.HashMap; -import java.util.Map; - -public enum MessageFlags { - ADD(1), - DELETE(2), - CRI(4), - LOCAL(8), - STRING(16), - TEXT(32), - TTY(64); - - private static final Map LOOKUP = new HashMap<>(); - - static { - for (MessageFlags state : MessageFlags.values()) { - LOOKUP.put(state.getValue(), state); - } - } - - private final int value; - - MessageFlags(int value) { - this.value = value; - } - - public int getValue() { - return this.value; - } - - public static MessageFlags get(int value) { - return LOOKUP.get(value); - } -} diff --git a/corefx/src/main/java/com/core/data/MobilityConfig.java b/corefx/src/main/java/com/core/data/MobilityConfig.java deleted file mode 100644 index f16de6ff..00000000 --- a/corefx/src/main/java/com/core/data/MobilityConfig.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.core.data; - -import lombok.Data; - -import java.io.File; - -@Data -public class MobilityConfig { - private String file; - private File scriptFile; - private Integer refresh; - private String loop; - private String autostart; - private String map; - private String startScript; - private String pauseScript; - private String stopScript; -} diff --git a/corefx/src/main/java/com/core/data/NodeType.java b/corefx/src/main/java/com/core/data/NodeType.java deleted file mode 100644 index e9743ea8..00000000 --- a/corefx/src/main/java/com/core/data/NodeType.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.core.data; - -import javafx.scene.control.Label; -import javafx.scene.image.ImageView; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.util.*; -import java.util.concurrent.atomic.AtomicInteger; - -@Data -@EqualsAndHashCode(onlyExplicitlyIncluded = true) -public class NodeType { - private static final Logger logger = LogManager.getLogger(); - private static final AtomicInteger idGenerator = new AtomicInteger(0); - private static final Map ID_LOOKUP = new HashMap<>(); - public static final int DEFAULT = 0; - public static final int SWITCH = 4; - public static final int HUB = 5; - public static final int WLAN = 6; - public static final int RJ45 = 7; - public static final int EMANE = 10; - public static final int DOCKER = 15; - public static final int LXC = 16; - @EqualsAndHashCode.Include - private final int id; - private final int value; - private final Set services = new TreeSet<>(); - private String display; - private String model; - private String icon; - - static { - add(new NodeType(SWITCH, "lanswitch", "Switch", "/icons/switch-100.png")); - add(new NodeType(HUB, "hub", "Hub", "/icons/hub-100.png")); - add(new NodeType(WLAN, "wlan", "WLAN", "/icons/wlan-100.png")); - add(new NodeType(RJ45, "rj45", "RJ45", "/icons/rj45-80.png")); - add(new NodeType(EMANE, "wlan", "EMANE", "/icons/emane-100.png")); - add(new NodeType(DOCKER, null, "DockerNode", "/icons/dockernode-100.png")); - add(new NodeType(LXC, null, "LxcNode", "/icons/lxcnode-100.png")); - } - - - public NodeType(int value, String model, String display, String icon) { - this.id = idGenerator.incrementAndGet(); - this.value = value; - this.model = model; - this.display = display; - this.icon = icon; - } - - public Label createLabel(int size) { - ImageView labelIcon = new ImageView(icon); - labelIcon.setFitWidth(size); - labelIcon.setFitHeight(size); - Label label = new Label(display, labelIcon); - label.setUserData(id); - return label; - } - - public static boolean isDefault(NodeType nodeType) { - return nodeType.value == DEFAULT || nodeType.value == DOCKER || nodeType.value == LXC; - } - - public static void add(NodeType nodeType) { - ID_LOOKUP.put(nodeType.getId(), nodeType); - } - - public static void remove(NodeType nodeType) { - ID_LOOKUP.remove(nodeType.getId()); - } - - public static NodeType get(Integer id) { - return ID_LOOKUP.get(id); - } - - public static Collection getAll() { - return ID_LOOKUP.values(); - } - - public static NodeType find(Integer type, String model) { - return ID_LOOKUP.values().stream() - .filter(nodeType -> { - boolean sameType = nodeType.getValue() == type; - boolean sameModel = true; - if (model != null && !model.isEmpty()) { - sameModel = model.equals(nodeType.getModel()); - } - return sameType && sameModel; - }) - .findFirst().orElse(null); - } -} diff --git a/corefx/src/main/java/com/core/data/Position.java b/corefx/src/main/java/com/core/data/Position.java deleted file mode 100644 index c5bfa728..00000000 --- a/corefx/src/main/java/com/core/data/Position.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.core.data; - -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -public class Position { - private Double x; - private Double y; - private Double z; -} diff --git a/corefx/src/main/java/com/core/data/ServiceFile.java b/corefx/src/main/java/com/core/data/ServiceFile.java deleted file mode 100644 index f30efcd0..00000000 --- a/corefx/src/main/java/com/core/data/ServiceFile.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.core.data; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class ServiceFile { - private String name; - private String data; -} diff --git a/corefx/src/main/java/com/core/data/Session.java b/corefx/src/main/java/com/core/data/Session.java deleted file mode 100644 index e12ddeb9..00000000 --- a/corefx/src/main/java/com/core/data/Session.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.core.data; - -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; - -@Data -@NoArgsConstructor -public class Session { - private Integer id; - private SessionState state; - private List nodes = new ArrayList<>(); - private List links = new ArrayList<>(); -} diff --git a/corefx/src/main/java/com/core/data/SessionOverview.java b/corefx/src/main/java/com/core/data/SessionOverview.java deleted file mode 100644 index 47773338..00000000 --- a/corefx/src/main/java/com/core/data/SessionOverview.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.core.data; - -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -public class SessionOverview { - private Integer id; - private Integer state; - private Integer nodes = 0; - private String url; -} diff --git a/corefx/src/main/java/com/core/data/SessionState.java b/corefx/src/main/java/com/core/data/SessionState.java deleted file mode 100644 index c9e92904..00000000 --- a/corefx/src/main/java/com/core/data/SessionState.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.core.data; - -import java.util.HashMap; -import java.util.Map; - -public enum SessionState { - DEFINITION(1), - CONFIGURATION(2), - INSTANTIATION(3), - RUNTIME(4), - DATA_COLLECT(5), - SHUTDOWN(6), - START(7), - STOP(8), - PAUSE(9); - - private static final Map LOOKUP = new HashMap<>(); - - static { - for (SessionState state : SessionState.values()) { - LOOKUP.put(state.getValue(), state); - } - } - - private final int value; - - SessionState(int value) { - this.value = value; - } - - public int getValue() { - return this.value; - } - - public static SessionState get(int value) { - return LOOKUP.get(value); - } -} diff --git a/corefx/src/main/java/com/core/data/Throughputs.java b/corefx/src/main/java/com/core/data/Throughputs.java deleted file mode 100644 index c02a7f6f..00000000 --- a/corefx/src/main/java/com/core/data/Throughputs.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.core.data; - -import lombok.Data; - -import java.util.ArrayList; -import java.util.List; - -@Data -public class Throughputs { - private List interfaces = new ArrayList<>(); - private List bridges = new ArrayList<>(); -} diff --git a/corefx/src/main/java/com/core/data/WlanConfig.java b/corefx/src/main/java/com/core/data/WlanConfig.java deleted file mode 100644 index 26048d73..00000000 --- a/corefx/src/main/java/com/core/data/WlanConfig.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.core.data; - -import lombok.Data; - -@Data -public class WlanConfig { - private String range; - private String bandwidth; - private String jitter; - private String delay; - private String error; -} diff --git a/corefx/src/main/java/com/core/datavis/CoreGraph.java b/corefx/src/main/java/com/core/datavis/CoreGraph.java deleted file mode 100644 index f5a0c0fd..00000000 --- a/corefx/src/main/java/com/core/datavis/CoreGraph.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.core.datavis; - -import lombok.Data; - -@Data -public class CoreGraph { - private String title; - private CoreGraphAxis xAxis; - private CoreGraphAxis yAxis; - private GraphType graphType; -} diff --git a/corefx/src/main/java/com/core/datavis/CoreGraphAxis.java b/corefx/src/main/java/com/core/datavis/CoreGraphAxis.java deleted file mode 100644 index 8631f3a1..00000000 --- a/corefx/src/main/java/com/core/datavis/CoreGraphAxis.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.core.datavis; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class CoreGraphAxis { - private String label; - private Double lower; - private Double upper; - private Double tick; -} diff --git a/corefx/src/main/java/com/core/datavis/CoreGraphData.java b/corefx/src/main/java/com/core/datavis/CoreGraphData.java deleted file mode 100644 index ff0d4c77..00000000 --- a/corefx/src/main/java/com/core/datavis/CoreGraphData.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.core.datavis; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class CoreGraphData { - private String name; - private Double x; - private Double y; - private Double weight; -} diff --git a/corefx/src/main/java/com/core/datavis/CoreGraphWrapper.java b/corefx/src/main/java/com/core/datavis/CoreGraphWrapper.java deleted file mode 100644 index eae92e8f..00000000 --- a/corefx/src/main/java/com/core/datavis/CoreGraphWrapper.java +++ /dev/null @@ -1,157 +0,0 @@ -package com.core.datavis; - -import javafx.scene.chart.*; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; - -public class CoreGraphWrapper { - private static final Logger logger = LogManager.getLogger(); - private final GraphType graphType; - private PieChart pieChart; - private final Map pieData = new HashMap<>(); - private BarChart barChart; - private final Map> barMap = new HashMap<>(); - private XYChart xyChart; - private final XYChart.Series series = new XYChart.Series<>(); - private final XYChart.Series barSeries = new XYChart.Series<>(); - private AtomicInteger timeValue = new AtomicInteger(0); - - public CoreGraphWrapper(CoreGraph coreGraph) { - graphType = coreGraph.getGraphType(); - createChart(coreGraph); - } - - public Chart getChart() { - switch (graphType) { - case PIE: - return pieChart; - case BAR: - return barChart; - default: - return xyChart; - } - } - - public void add(CoreGraphData coreGraphData) { - switch (graphType) { - case PIE: - case BAR: - add(coreGraphData.getName(), coreGraphData.getY()); - break; - case TIME: - add(coreGraphData.getY()); - break; - case BUBBLE: - add(coreGraphData.getX(), coreGraphData.getY(), coreGraphData.getWeight()); - break; - default: - add(coreGraphData.getX(), coreGraphData.getY()); - } - } - - public void add(String name, double value) { - if (GraphType.PIE == graphType) { - PieChart.Data data = pieData.computeIfAbsent(name, x -> { - PieChart.Data newData = new PieChart.Data(x, value); - pieChart.getData().add(newData); - return newData; - }); - data.setPieValue(value); - } else { - XYChart.Data data = barMap.computeIfAbsent(name, x -> { - XYChart.Data newData = new XYChart.Data<>(name, value); - barSeries.getData().add(newData); - return newData; - }); - data.setYValue(value); - } - } - - public void add(Number y) { - series.getData().add(new XYChart.Data<>(timeValue.getAndIncrement(), y)); - } - - public void add(Number x, Number y) { - series.getData().add(new XYChart.Data<>(x, y)); - } - - public void add(Number x, Number y, Number weight) { - series.getData().add(new XYChart.Data<>(x, y, weight)); - } - - private NumberAxis getAxis(CoreGraphAxis graphAxis) { - return new NumberAxis(graphAxis.getLabel(), graphAxis.getLower(), - graphAxis.getUpper(), graphAxis.getTick()); - } - - private void createChart(CoreGraph coreGraph) { - NumberAxis xAxis; - NumberAxis yAxis; - - switch (coreGraph.getGraphType()) { - case AREA: - xAxis = getAxis(coreGraph.getXAxis()); - yAxis = getAxis(coreGraph.getYAxis()); - xyChart = new AreaChart<>(xAxis, yAxis); - xyChart.setTitle(coreGraph.getTitle()); - xyChart.setLegendVisible(false); - xyChart.getData().add(series); - break; - case TIME: - xAxis = new NumberAxis(); - xAxis.setLabel(coreGraph.getXAxis().getLabel()); - xAxis.setTickUnit(1); - xAxis.setLowerBound(0); - yAxis = getAxis(coreGraph.getYAxis()); - xyChart = new LineChart<>(xAxis, yAxis); - xyChart.setTitle(coreGraph.getTitle()); - xyChart.setLegendVisible(false); - xyChart.getData().add(series); - break; - case LINE: - xAxis = getAxis(coreGraph.getXAxis()); - yAxis = getAxis(coreGraph.getYAxis()); - xyChart = new LineChart<>(xAxis, yAxis); - xyChart.setTitle(coreGraph.getTitle()); - xyChart.setLegendVisible(false); - xyChart.getData().add(series); - break; - case BUBBLE: - xAxis = getAxis(coreGraph.getXAxis()); - yAxis = getAxis(coreGraph.getYAxis()); - xyChart = new BubbleChart<>(xAxis, yAxis); - xyChart.setTitle(coreGraph.getTitle()); - xyChart.setLegendVisible(false); - xyChart.getData().add(series); - break; - case SCATTER: - xAxis = getAxis(coreGraph.getXAxis()); - yAxis = getAxis(coreGraph.getYAxis()); - xyChart = new ScatterChart<>(xAxis, yAxis); - xyChart.setTitle(coreGraph.getTitle()); - xyChart.setLegendVisible(false); - xyChart.getData().add(series); - break; - case PIE: - pieChart = new PieChart(); - pieChart.setTitle(coreGraph.getTitle()); - break; - case BAR: - CategoryAxis categoryAxis = new CategoryAxis(); - categoryAxis.setLabel(coreGraph.getXAxis().getLabel()); - yAxis = getAxis(coreGraph.getYAxis()); - barChart = new BarChart<>(categoryAxis, yAxis); - barChart.setLegendVisible(false); - barChart.setTitle(coreGraph.getTitle()); - barChart.getData().add(barSeries); - break; - default: - throw new IllegalArgumentException(String.format("unknown graph type: %s", - coreGraph.getGraphType())); - } - } -} diff --git a/corefx/src/main/java/com/core/datavis/GraphType.java b/corefx/src/main/java/com/core/datavis/GraphType.java deleted file mode 100644 index 7609eee3..00000000 --- a/corefx/src/main/java/com/core/datavis/GraphType.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.core.datavis; - -public enum GraphType { - PIE, - LINE, - TIME, - AREA, - BAR, - SCATTER, - BUBBLE -} diff --git a/corefx/src/main/java/com/core/graph/AbstractNodeContextMenu.java b/corefx/src/main/java/com/core/graph/AbstractNodeContextMenu.java deleted file mode 100644 index d04097c2..00000000 --- a/corefx/src/main/java/com/core/graph/AbstractNodeContextMenu.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.core.graph; - -import com.core.Controller; -import com.core.data.CoreNode; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; -import javafx.scene.control.MenuItem; - -abstract class AbstractNodeContextMenu extends GraphContextMenu { - final CoreNode coreNode; - - AbstractNodeContextMenu(Controller controller, CoreNode coreNode) { - super(controller); - this.coreNode = coreNode; - } - - void addMenuItem(String text, EventHandler handler) { - MenuItem menuItem = new MenuItem(text); - menuItem.setOnAction(handler); - getItems().add(menuItem); - } -} diff --git a/corefx/src/main/java/com/core/graph/BackgroundPaintable.java b/corefx/src/main/java/com/core/graph/BackgroundPaintable.java deleted file mode 100644 index b0cfe6a3..00000000 --- a/corefx/src/main/java/com/core/graph/BackgroundPaintable.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.core.graph; - -import edu.uci.ics.jung.visualization.Layer; -import edu.uci.ics.jung.visualization.VisualizationViewer; - -import javax.imageio.ImageIO; -import javax.swing.*; -import java.awt.*; -import java.awt.geom.AffineTransform; -import java.io.IOException; -import java.nio.file.Paths; - -public class BackgroundPaintable implements VisualizationViewer.Paintable { - private final ImageIcon imageIcon; - private final VisualizationViewer vv; - private final String imagePath; - - public BackgroundPaintable(String imagePath, VisualizationViewer vv) throws IOException { - this.imagePath = imagePath; - Image image = ImageIO.read(Paths.get(imagePath).toFile()); - imageIcon = new ImageIcon(image); - this.vv = vv; - } - - public String getImage() { - return imagePath; - } - - @Override - public void paint(Graphics g) { - Graphics2D g2d = (Graphics2D) g; - AffineTransform oldXform = g2d.getTransform(); - AffineTransform lat = - vv.getRenderContext().getMultiLayerTransformer().getTransformer(Layer.LAYOUT).getTransform(); - AffineTransform vat = - vv.getRenderContext().getMultiLayerTransformer().getTransformer(Layer.VIEW).getTransform(); - AffineTransform at = new AffineTransform(); - at.concatenate(g2d.getTransform()); - at.concatenate(vat); - at.concatenate(lat); - g2d.setTransform(at); - g.drawImage(imageIcon.getImage(), 0, 0, - imageIcon.getIconWidth(), imageIcon.getIconHeight(), vv); - g2d.setTransform(oldXform); - } - - @Override - public boolean useTransform() { - return false; - } -} diff --git a/corefx/src/main/java/com/core/graph/CoreAddresses.java b/corefx/src/main/java/com/core/graph/CoreAddresses.java deleted file mode 100644 index ee1daa1a..00000000 --- a/corefx/src/main/java/com/core/graph/CoreAddresses.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.core.graph; - -import com.core.data.CoreInterface; -import inet.ipaddr.IPAddress; -import inet.ipaddr.IPAddressString; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.util.Comparator; -import java.util.HashSet; -import java.util.Queue; -import java.util.Set; -import java.util.concurrent.LinkedBlockingQueue; - -public class CoreAddresses { - private static final String ADDRESS = "10.0.0.0/24"; - private static final Logger logger = LogManager.getLogger(); - private IPAddress currentSubnet = new IPAddressString(ADDRESS).getAddress().toPrefixBlock(); - private Queue deleted = new LinkedBlockingQueue<>(); - private Set usedSubnets = new HashSet<>(); - - public void usedAddress(IPAddress address) { - logger.info("adding used address: {} - {}", address, address.toPrefixBlock()); - usedSubnets.add(address.toPrefixBlock()); - logger.info("used subnets: {}", usedSubnets); - } - - public void reuseSubnet(IPAddress subnet) { - deleted.add(subnet); - } - - public IPAddress nextSubnet() { - logger.info("getting next subnet: {}", currentSubnet); - // skip existing subnets, when loaded from file - while (usedSubnets.contains(currentSubnet)) { - currentSubnet = currentSubnet.incrementBoundary(1).toPrefixBlock(); - } - - // re-use any deleted subnets - IPAddress next = deleted.poll(); - if (next == null) { - next = currentSubnet; - currentSubnet = currentSubnet.incrementBoundary(1).toPrefixBlock(); - } - return next; - } - - public IPAddress findSubnet(Set interfaces) { - IPAddress subnet; - logger.info("finding subnet from interfaces: {}", interfaces); - if (interfaces.isEmpty()) { - subnet = nextSubnet(); - } else { - IPAddress maxAddress = getMaxAddress(interfaces); - subnet = maxAddress.toPrefixBlock(); - } - return subnet; - } - - private IPAddress getMaxAddress(Set interfaces) { - return interfaces.stream() - .map(CoreInterface::getIp4) - .max(Comparator.comparingInt(x -> x.toIPv4().intValue())) - .orElseGet(() -> currentSubnet); - } - - public void reset() { - deleted.clear(); - usedSubnets.clear(); - currentSubnet = new IPAddressString(ADDRESS).getAddress().toPrefixBlock(); - } -} diff --git a/corefx/src/main/java/com/core/graph/CoreAnnotatingGraphMousePlugin.java b/corefx/src/main/java/com/core/graph/CoreAnnotatingGraphMousePlugin.java deleted file mode 100644 index 6cbd1bf8..00000000 --- a/corefx/src/main/java/com/core/graph/CoreAnnotatingGraphMousePlugin.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.core.graph; - -import com.core.Controller; -import edu.uci.ics.jung.visualization.RenderContext; -import edu.uci.ics.jung.visualization.VisualizationViewer; -import edu.uci.ics.jung.visualization.annotations.AnnotatingGraphMousePlugin; -import edu.uci.ics.jung.visualization.annotations.Annotation; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.MouseEvent; -import java.awt.geom.Point2D; -import java.awt.geom.RectangularShape; - -public class CoreAnnotatingGraphMousePlugin extends AnnotatingGraphMousePlugin { - private static final Logger logger = LogManager.getLogger(); - private final Controller controller; - private JFrame frame = new JFrame(); - - public CoreAnnotatingGraphMousePlugin(Controller controller, RenderContext renderContext) { - super(renderContext); - this.controller = controller; - frame.setVisible(false); - frame.setAlwaysOnTop(true); - } - - @Override - public void mouseReleased(MouseEvent e) { - VisualizationViewer vv = (VisualizationViewer) e.getSource(); - if (e.isPopupTrigger()) { - frame.setLocationRelativeTo(vv); - String annotationString = JOptionPane.showInputDialog(frame, "Annotation:", - "Annotation Label", JOptionPane.PLAIN_MESSAGE); - if (annotationString != null && annotationString.length() > 0) { - Point2D p = vv.getRenderContext().getMultiLayerTransformer().inverseTransform(this.down); - Annotation annotation = new Annotation(annotationString, this.layer, - this.annotationColor, this.fill, p); - this.annotationManager.add(this.layer, annotation); - } - } else if (e.getModifiers() == this.modifiers && this.down != null) { - Point2D out = e.getPoint(); - RectangularShape arect = (RectangularShape) this.rectangularShape.clone(); - arect.setFrameFromDiagonal(this.down, out); - Shape s = vv.getRenderContext().getMultiLayerTransformer().inverseTransform(arect); - Annotation annotation = new Annotation(s, this.layer, this.annotationColor, this.fill, out); - this.annotationManager.add(this.layer, annotation); - } - - this.down = null; - vv.removePostRenderPaintable(this.lensPaintable); - vv.repaint(); - } - - -} diff --git a/corefx/src/main/java/com/core/graph/CoreEditingModalGraphMouse.java b/corefx/src/main/java/com/core/graph/CoreEditingModalGraphMouse.java deleted file mode 100644 index 9d80183e..00000000 --- a/corefx/src/main/java/com/core/graph/CoreEditingModalGraphMouse.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.core.graph; - -import com.core.Controller; -import com.google.common.base.Supplier; -import edu.uci.ics.jung.visualization.RenderContext; -import edu.uci.ics.jung.visualization.control.EditingModalGraphMouse; - -public class CoreEditingModalGraphMouse extends EditingModalGraphMouse { - public CoreEditingModalGraphMouse(Controller controller, NetworkGraph networkGraph, - RenderContext rc, Supplier vertexFactory, Supplier edgeFactory) { - super(rc, vertexFactory, edgeFactory); - remove(annotatingPlugin); - remove(popupEditingPlugin); - annotatingPlugin = new CoreAnnotatingGraphMousePlugin<>(controller, rc); - popupEditingPlugin = new CorePopupGraphMousePlugin<>(controller, networkGraph, vertexFactory, edgeFactory); - } -} diff --git a/corefx/src/main/java/com/core/graph/CoreObservableGraph.java b/corefx/src/main/java/com/core/graph/CoreObservableGraph.java deleted file mode 100644 index cbf678f0..00000000 --- a/corefx/src/main/java/com/core/graph/CoreObservableGraph.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.core.graph; - -import edu.uci.ics.jung.graph.Graph; -import edu.uci.ics.jung.graph.ObservableGraph; -import edu.uci.ics.jung.graph.util.EdgeType; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -public class CoreObservableGraph extends ObservableGraph { - private static final Logger logger = LogManager.getLogger(); - - public CoreObservableGraph(Graph graph) { - super(graph); - } - - @Override - public boolean addEdge(E e, V v1, V v2, EdgeType edgeType) { - if (v1 == null || v2 == null) { - return false; - } - return super.addEdge(e, v1, v2, edgeType); - } - - @Override - public boolean addEdge(E e, V v1, V v2) { - if (v1 == null || v2 == null) { - return false; - } - return super.addEdge(e, v1, v2); - } -} diff --git a/corefx/src/main/java/com/core/graph/CorePopupGraphMousePlugin.java b/corefx/src/main/java/com/core/graph/CorePopupGraphMousePlugin.java deleted file mode 100644 index bd46f4c3..00000000 --- a/corefx/src/main/java/com/core/graph/CorePopupGraphMousePlugin.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.core.graph; - -import com.core.Controller; -import com.core.data.CoreLink; -import com.core.data.CoreNode; -import com.core.data.NodeType; -import com.google.common.base.Supplier; -import edu.uci.ics.jung.algorithms.layout.GraphElementAccessor; -import edu.uci.ics.jung.algorithms.layout.Layout; -import edu.uci.ics.jung.visualization.control.EditingPopupGraphMousePlugin; -import javafx.application.Platform; -import javafx.scene.control.ContextMenu; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.awt.event.MouseEvent; -import java.awt.geom.Point2D; - -public class CorePopupGraphMousePlugin extends EditingPopupGraphMousePlugin { - private static final Logger logger = LogManager.getLogger(); - private final Controller controller; - private final NetworkGraph networkGraph; - private final Layout graphLayout; - private final GraphElementAccessor pickSupport; - - public CorePopupGraphMousePlugin(Controller controller, NetworkGraph networkGraph, - Supplier vertexFactory, Supplier edgeFactory) { - super(vertexFactory, edgeFactory); - this.controller = controller; - this.networkGraph = networkGraph; - graphLayout = this.networkGraph.getGraphLayout(); - pickSupport = this.networkGraph.getGraphViewer().getPickSupport(); - } - - @Override - protected void handlePopup(MouseEvent e) { - logger.info("showing popup!"); - final Point2D p = e.getPoint(); - - final CoreNode node = pickSupport.getVertex(graphLayout, p.getX(), p.getY()); - final CoreLink link = pickSupport.getEdge(graphLayout, p.getX(), p.getY()); - - final ContextMenu contextMenu; - if (node != null) { - contextMenu = handleNodeContext(node); - } else if (link != null) { - contextMenu = new LinkContextMenu(controller, link); - } else { - contextMenu = new ContextMenu(); - } - - if (!contextMenu.getItems().isEmpty()) { - logger.info("showing context menu"); - Platform.runLater(() -> contextMenu.show(controller.getWindow(), - e.getXOnScreen(), e.getYOnScreen())); - } - } - - private ContextMenu handleNodeContext(final CoreNode node) { - ContextMenu contextMenu = new ContextMenu(); - switch (node.getType()) { - case NodeType.DEFAULT: - case NodeType.DOCKER: - case NodeType.LXC: - contextMenu = new NodeContextMenu(controller, node); - break; - case NodeType.WLAN: - contextMenu = new WlanContextMenu(controller, node); - break; - case NodeType.EMANE: - contextMenu = new EmaneContextMenu(controller, node); - break; - case NodeType.RJ45: - contextMenu = new Rj45ContextMenu(controller, node); - break; - default: - logger.warn("no context menu for node: {}", node.getType()); - break; - } - - return contextMenu; - } -} diff --git a/corefx/src/main/java/com/core/graph/CoreVertexLabelRenderer.java b/corefx/src/main/java/com/core/graph/CoreVertexLabelRenderer.java deleted file mode 100644 index 25c49fa3..00000000 --- a/corefx/src/main/java/com/core/graph/CoreVertexLabelRenderer.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.core.graph; - -import edu.uci.ics.jung.visualization.renderers.DefaultVertexLabelRenderer; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import java.awt.*; - -public class CoreVertexLabelRenderer extends DefaultVertexLabelRenderer { - private Color foregroundColor = Color.WHITE; - private Color backgroundColor = Color.BLACK; - - CoreVertexLabelRenderer() { - super(Color.YELLOW); - } - - public void setColors(Color foregroundColor, Color backgroundColor) { - this.foregroundColor = foregroundColor; - this.backgroundColor = backgroundColor; - } - - @Override - public Component getVertexLabelRendererComponent(JComponent vv, Object value, Font font, boolean isSelected, V vertex) { - super.setForeground(foregroundColor); - if (isSelected) { - this.setForeground(this.pickedVertexLabelColor); - } - - super.setBackground(backgroundColor); - if (font != null) { - this.setFont(font); - } else { - this.setFont(vv.getFont()); - } - - this.setIcon(null); - EmptyBorder padding = new EmptyBorder(5, 5, 5, 5); - this.setBorder(padding); - this.setValue(value); - setFont(getFont().deriveFont(Font.BOLD)); - return this; - } -} diff --git a/corefx/src/main/java/com/core/graph/EmaneContextMenu.java b/corefx/src/main/java/com/core/graph/EmaneContextMenu.java deleted file mode 100644 index 848bbfb3..00000000 --- a/corefx/src/main/java/com/core/graph/EmaneContextMenu.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.core.graph; - -import com.core.Controller; -import com.core.data.CoreNode; -import com.core.ui.dialogs.MobilityPlayerDialog; - -class EmaneContextMenu extends AbstractNodeContextMenu { - EmaneContextMenu(Controller controller, CoreNode coreNode) { - super(controller, coreNode); - setup(); - } - - private void setup() { - addMenuItem("EMANE Settings", event -> controller.getNodeEmaneDialog().showDialog(coreNode)); - if (controller.getCoreClient().isRunning()) { - MobilityPlayerDialog mobilityPlayerDialog = controller.getMobilityPlayerDialogs().get(coreNode.getId()); - if (mobilityPlayerDialog != null && !mobilityPlayerDialog.getStage().isShowing()) { - addMenuItem("Mobility Script", event -> mobilityPlayerDialog.show()); - } - } else { - addMenuItem("Mobility", event -> controller.getMobilityDialog().showDialog(coreNode)); - addMenuItem("Link MDRs", event -> controller.getNetworkGraph().linkMdrs(coreNode)); - addMenuItem("Delete Node", event -> controller.deleteNode(coreNode)); - } - } -} diff --git a/corefx/src/main/java/com/core/graph/GraphContextMenu.java b/corefx/src/main/java/com/core/graph/GraphContextMenu.java deleted file mode 100644 index c93ecba9..00000000 --- a/corefx/src/main/java/com/core/graph/GraphContextMenu.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.core.graph; - -import com.core.Controller; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; -import javafx.scene.control.ContextMenu; -import javafx.scene.control.MenuItem; - -abstract class GraphContextMenu extends ContextMenu { - final Controller controller; - - GraphContextMenu(Controller controller) { - super(); - this.controller = controller; - } - - void addMenuItem(String text, EventHandler handler) { - MenuItem menuItem = new MenuItem(text); - menuItem.setOnAction(handler); - getItems().add(menuItem); - } -} diff --git a/corefx/src/main/java/com/core/graph/LinkContextMenu.java b/corefx/src/main/java/com/core/graph/LinkContextMenu.java deleted file mode 100644 index d9b11115..00000000 --- a/corefx/src/main/java/com/core/graph/LinkContextMenu.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.core.graph; - -import com.core.Controller; -import com.core.data.CoreLink; - -class LinkContextMenu extends GraphContextMenu { - final CoreLink coreLink; - - LinkContextMenu(Controller controller, CoreLink coreLink) { - super(controller); - this.coreLink = coreLink; - setup(); - } - - private void setup() { - if (!controller.getCoreClient().isRunning()) { - addMenuItem("Delete Link", - event -> controller.getNetworkGraph().removeLink(coreLink)); - } - } -} diff --git a/corefx/src/main/java/com/core/graph/NetworkGraph.java b/corefx/src/main/java/com/core/graph/NetworkGraph.java deleted file mode 100644 index 145bdc4c..00000000 --- a/corefx/src/main/java/com/core/graph/NetworkGraph.java +++ /dev/null @@ -1,578 +0,0 @@ -package com.core.graph; - -import com.core.Controller; -import com.core.data.*; -import com.core.ui.Toast; -import com.core.ui.dialogs.TerminalDialog; -import com.core.utils.Configuration; -import com.core.utils.IconUtils; -import com.google.common.base.Supplier; -import edu.uci.ics.jung.algorithms.layout.StaticLayout; -import edu.uci.ics.jung.graph.ObservableGraph; -import edu.uci.ics.jung.graph.event.GraphEvent; -import edu.uci.ics.jung.graph.event.GraphEventListener; -import edu.uci.ics.jung.graph.util.Pair; -import edu.uci.ics.jung.visualization.RenderContext; -import edu.uci.ics.jung.visualization.VisualizationViewer; -import edu.uci.ics.jung.visualization.annotations.AnnotationControls; -import edu.uci.ics.jung.visualization.control.EditingModalGraphMouse; -import edu.uci.ics.jung.visualization.control.GraphMouseListener; -import edu.uci.ics.jung.visualization.control.ModalGraphMouse; -import edu.uci.ics.jung.visualization.decorators.EdgeShape; -import edu.uci.ics.jung.visualization.renderers.Renderer; -import inet.ipaddr.IPAddress; -import javafx.application.Platform; -import lombok.Data; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.MouseEvent; -import java.awt.geom.Ellipse2D; -import java.io.IOException; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - - -@Data -public class NetworkGraph { - private static final Logger logger = LogManager.getLogger(); - private static final int EDGE_LABEL_OFFSET = -5; - private static final int EDGE_WIDTH = 5; - private Controller controller; - private ObservableGraph graph; - private StaticLayout graphLayout; - private VisualizationViewer graphViewer; - private EditingModalGraphMouse graphMouse; - private AnnotationControls annotationControls; - - private CoreAddresses coreAddresses = new CoreAddresses(); - private NodeType nodeType; - private Map nodeMap = new ConcurrentHashMap<>(); - private int vertexId = 1; - private int linkId = 1; - private Supplier vertexFactory = () -> new CoreNode(vertexId++); - private Supplier linkFactory = () -> new CoreLink(linkId++); - private CorePopupGraphMousePlugin customPopupPlugin; - private CoreAnnotatingGraphMousePlugin customAnnotatingPlugin; - private BackgroundPaintable backgroundPaintable; - private CoreVertexLabelRenderer nodeLabelRenderer = new CoreVertexLabelRenderer(); - - // display options - private boolean showThroughput = false; - private Double throughputLimit = null; - private int throughputWidth = 10; - - public NetworkGraph(Controller controller) { - this.controller = controller; - graph = new CoreObservableGraph<>(new UndirectedSimpleGraph<>()); - graph.addGraphEventListener(graphEventListener); - graphLayout = new StaticLayout<>(graph); - graphViewer = new VisualizationViewer<>(graphLayout); - graphViewer.setBackground(Color.WHITE); - graphViewer.getRenderer().getVertexLabelRenderer().setPosition(Renderer.VertexLabel.Position.S); - - // node render properties - RenderContext renderContext = graphViewer.getRenderContext(); - renderContext.setVertexLabelTransformer(CoreNode::getName); - renderContext.setVertexLabelRenderer(nodeLabelRenderer); - renderContext.setVertexShapeTransformer(node -> { - double offset = -(IconUtils.ICON_SIZE / 2.0); - return new Ellipse2D.Double(offset, offset, IconUtils.ICON_SIZE, IconUtils.ICON_SIZE); - }); - renderContext.setVertexIconTransformer(vertex -> { - long wirelessLinks = wirelessLinkCount(vertex); - vertex.getRadioIcon().setWiressLinks(wirelessLinks); - return vertex.getGraphIcon(); - }); - - // link render properties - renderContext.setEdgeLabelTransformer(edge -> { - if (!showThroughput || edge == null) { - return null; - } - double kbps = edge.getThroughput() / 1000.0; - return String.format("%.2f kbps", kbps); - }); - renderContext.setLabelOffset(EDGE_LABEL_OFFSET); - renderContext.setEdgeStrokeTransformer(edge -> { - // determine edge width - int width = EDGE_WIDTH; - if (throughputLimit != null && edge.getThroughput() > throughputLimit) { - width = throughputWidth; - } - - LinkTypes linkType = LinkTypes.get(edge.getType()); - if (LinkTypes.WIRELESS == linkType) { - float[] dash = {15.0f}; - return new BasicStroke(width, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND, - 0, dash, 0); - } else { - return new BasicStroke(width); - } - }); - renderContext.setEdgeShapeTransformer(EdgeShape.line(graph)); - renderContext.setEdgeDrawPaintTransformer(edge -> { - LinkTypes linkType = LinkTypes.get(edge.getType()); - if (LinkTypes.WIRELESS == linkType) { - return Color.BLUE; - } else { - return Color.BLACK; - } - }); - renderContext.setEdgeIncludePredicate(predicate -> predicate.element.isVisible()); - - graphViewer.setVertexToolTipTransformer(renderContext.getVertexLabelTransformer()); - graphMouse = new CoreEditingModalGraphMouse<>(controller, this, renderContext, - vertexFactory, linkFactory); - graphViewer.setGraphMouse(graphMouse); - - // mouse events - graphViewer.addGraphMouseListener(new GraphMouseListener() { - @Override - public void graphClicked(CoreNode node, MouseEvent mouseEvent) { - // double click - logger.info("click count: {}, running?: {}", mouseEvent.getClickCount(), - controller.getCoreClient().isRunning()); - - if (mouseEvent.getClickCount() == 2 && controller.getCoreClient().isRunning()) { - if (controller.getCoreClient().isLocalConnection()) { - try { - String shellCommand = controller.getConfiguration().getShellCommand(); - String terminalCommand = controller.getCoreClient().getTerminalCommand(node); - terminalCommand = String.format("%s %s", shellCommand, terminalCommand); - logger.info("launching node terminal: {}", terminalCommand); - String[] commands = terminalCommand.split("\\s+"); - logger.info("launching node terminal: {}", Arrays.toString(commands)); - Process p = new ProcessBuilder(commands).start(); - try { - if (!p.waitFor(5, TimeUnit.SECONDS)) { - Toast.error("Node terminal command failed"); - } - } catch (InterruptedException ex) { - logger.error("error waiting for terminal to start", ex); - } - } catch (IOException ex) { - logger.error("error launching terminal", ex); - Toast.error("Node terminal failed to start"); - } - } else { - Platform.runLater(() -> { - TerminalDialog terminalDialog = new TerminalDialog(controller); - terminalDialog.setOwner(controller.getWindow()); - terminalDialog.showDialog(node); - }); - } - } - } - - @Override - public void graphPressed(CoreNode node, MouseEvent mouseEvent) { - logger.debug("graph pressed: {} - {}", node, mouseEvent); - } - - @Override - public void graphReleased(CoreNode node, MouseEvent mouseEvent) { - if (SwingUtilities.isLeftMouseButton(mouseEvent)) { - Double newX = graphLayout.getX(node); - Double newY = graphLayout.getY(node); - Double oldX = node.getPosition().getX(); - Double oldY = node.getPosition().getY(); - if (newX.equals(oldX) && newY.equals(oldY)) { - return; - } - logger.debug("graph moved node({}): {},{}", node.getName(), newX, newY); - node.getPosition().setX(newX); - node.getPosition().setY(newY); - - // upate node when session is active - if (controller.getCoreClient().isRunning()) { - try { - controller.getCoreClient().editNode(node); - } catch (IOException ex) { - Toast.error("failed to update node location"); - } - } - } - } - }); - } - - private Color convertJfxColor(String hexValue) { - javafx.scene.paint.Color color = javafx.scene.paint.Color.web(hexValue); - return new Color((float) color.getRed(), (float) color.getGreen(), (float) color.getBlue()); - } - - public void updatePreferences(Configuration configuration) { - Color nodeLabelColor = convertJfxColor(configuration.getNodeLabelColor()); - Color nodeLabelBackgroundColor = convertJfxColor(configuration.getNodeLabelBackgroundColor()); - nodeLabelRenderer.setColors(nodeLabelColor, nodeLabelBackgroundColor); - throughputLimit = configuration.getThroughputLimit(); - if (configuration.getThroughputWidth() != null) { - throughputWidth = configuration.getThroughputWidth(); - } - graphViewer.repaint(); - } - - public void setBackground(String imagePath) { - try { - backgroundPaintable = new BackgroundPaintable<>(imagePath, graphViewer); - graphViewer.addPreRenderPaintable(backgroundPaintable); - graphViewer.repaint(); - } catch (IOException ex) { - logger.error("error setting background", ex); - } - } - - public void removeBackground() { - if (backgroundPaintable != null) { - graphViewer.removePreRenderPaintable(backgroundPaintable); - graphViewer.repaint(); - backgroundPaintable = null; - } - } - - public void setMode(ModalGraphMouse.Mode mode) { - graphMouse.setMode(mode); - } - - public void reset() { - logger.info("network graph reset"); - vertexId = 1; - linkId = 1; - for (CoreNode node : nodeMap.values()) { - graph.removeVertex(node); - } - nodeMap.clear(); - graphViewer.repaint(); - coreAddresses.reset(); - } - - public void updatePositions() { - for (CoreNode node : graph.getVertices()) { - Double x = graphLayout.getX(node); - Double y = graphLayout.getY(node); - node.getPosition().setX(x); - node.getPosition().setY(y); - logger.debug("updating node position node({}): {},{}", node, x, y); - } - } - - public CoreNode getVertex(int id) { - return nodeMap.get(id); - } - - private GraphEventListener graphEventListener = graphEvent -> { - logger.info("graph event: {}", graphEvent.getType()); - switch (graphEvent.getType()) { - case EDGE_ADDED: - handleEdgeAdded((GraphEvent.Edge) graphEvent); - break; - case EDGE_REMOVED: - handleEdgeRemoved((GraphEvent.Edge) graphEvent); - break; - case VERTEX_ADDED: - handleVertexAdded((GraphEvent.Vertex) graphEvent); - break; - case VERTEX_REMOVED: - handleVertexRemoved((GraphEvent.Vertex) graphEvent); - break; - } - }; - - private void handleEdgeAdded(GraphEvent.Edge edgeEvent) { - CoreLink link = edgeEvent.getEdge(); - if (link.isLoaded()) { - // load addresses to avoid duplication - if (link.getInterfaceOne().getIp4() != null) { - coreAddresses.usedAddress(link.getInterfaceOne().getIp4()); - } - if (link.getInterfaceTwo().getIp4() != null) { - coreAddresses.usedAddress(link.getInterfaceTwo().getIp4()); - } - return; - } - Pair endpoints = graph.getEndpoints(link); - CoreNode nodeOne = endpoints.getFirst(); - CoreNode nodeTwo = endpoints.getSecond(); - boolean nodeOneIsDefault = NodeType.isDefault(nodeOne.getNodeType()); - boolean nodeTwoIsDefault = NodeType.isDefault(nodeTwo.getNodeType()); - - // check what we are linking together - IPAddress subnet = null; - Set interfaces; - if (nodeOneIsDefault && nodeTwoIsDefault) { - subnet = coreAddresses.nextSubnet(); - logger.info("linking node to node using subnet: {}", subnet); - } else if (nodeOneIsDefault) { - interfaces = getNetworkInterfaces(nodeTwo, new HashSet<>()); - subnet = coreAddresses.findSubnet(interfaces); - logger.info("linking node one to network using subnet: {}", subnet); - } else if (nodeTwoIsDefault) { - interfaces = getNetworkInterfaces(nodeOne, new HashSet<>()); - subnet = coreAddresses.findSubnet(interfaces); - logger.info("linking node two to network using subnet: {}", subnet); - } else { - logger.info("subnet not needed for linking networks together"); - } - - link.setNodeOne(nodeOne.getId()); - if (nodeOneIsDefault) { - int interfaceOneId = nextInterfaceId(nodeOne); - CoreInterface interfaceOne = createInterface(nodeOne, interfaceOneId, subnet); - link.setInterfaceOne(interfaceOne); - } - - link.setNodeTwo(nodeTwo.getId()); - if (nodeTwoIsDefault) { - int interfaceTwoId = nextInterfaceId(nodeTwo); - CoreInterface interfaceTwo = createInterface(nodeTwo, interfaceTwoId, subnet); - link.setInterfaceTwo(interfaceTwo); - } - - boolean isVisible = !checkForWirelessNode(nodeOne, nodeTwo); - link.setVisible(isVisible); - logger.info("adding user created edge: {}", link); - } - - public Set getNetworkInterfaces(CoreNode node, Set visited) { - Set interfaces = new HashSet<>(); - if (visited.contains(node)) { - return interfaces; - } - visited.add(node); - - logger.info("checking network node links: {}", node); - for (CoreLink link : graph.getIncidentEdges(node)) { - logger.info("checking link: {}", link); - if (link.getNodeOne() == null && link.getNodeTwo() == null) { - continue; - } - - // ignore oneself - CoreNode currentNode = getVertex(link.getNodeOne()); - CoreInterface currentInterface = link.getInterfaceOne(); - if (node.getId().equals(link.getNodeOne())) { - currentNode = getVertex(link.getNodeTwo()); - currentInterface = link.getInterfaceTwo(); - } - - if (NodeType.isDefault(currentNode.getNodeType())) { - interfaces.add(currentInterface); - } else { - Set nextInterfaces = getNetworkInterfaces(currentNode, visited); - interfaces.addAll(nextInterfaces); - } - } - - return interfaces; - } - - public Set getNodeInterfaces(CoreNode node) { - return graph.getIncidentEdges(node).stream() - .map(link -> { - if (node.getId().equals(link.getNodeOne())) { - return link.getInterfaceOne(); - } else { - return link.getInterfaceTwo(); - } - }) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - } - - private int nextInterfaceId(CoreNode node) { - Set interfaceIds = graph.getIncidentEdges(node).stream() - .map(link -> { - if (node.getId().equals(link.getNodeOne())) { - return link.getInterfaceOne(); - } else { - return link.getInterfaceTwo(); - } - }) - .filter(Objects::nonNull) - .map(CoreInterface::getId) - .collect(Collectors.toSet()); - - int i = 0; - while (true) { - if (!interfaceIds.contains(i)) { - return i; - } - - i += 1; - } - } - - private CoreInterface createInterface(CoreNode node, int interfaceId, IPAddress subnet) { - CoreInterface coreInterface = new CoreInterface(); - coreInterface.setId(interfaceId); - coreInterface.setName(String.format("eth%s", interfaceId)); - IPAddress address = subnet.increment(node.getId()); - logger.info("creating interface for node({}): {}", node.getId(), address); - coreInterface.setIp4(address); - coreInterface.setIp6(address.toIPv6()); - return coreInterface; - } - - private void handleEdgeRemoved(GraphEvent.Edge edgeEvent) { - CoreLink link = edgeEvent.getEdge(); - logger.info("removed edge: {}", link); - CoreNode nodeOne = getVertex(link.getNodeOne()); - CoreInterface interfaceOne = link.getInterfaceOne(); - CoreNode nodeTwo = getVertex(link.getNodeTwo()); - CoreInterface interfaceTwo = link.getInterfaceTwo(); - boolean nodeOneIsDefault = NodeType.isDefault(nodeOne.getNodeType()); - boolean nodeTwoIsDefault = NodeType.isDefault(nodeTwo.getNodeType()); - - // check what we are unlinking - Set interfaces; - IPAddress subnet = null; - if (nodeOneIsDefault && nodeTwoIsDefault) { - subnet = interfaceOne.getIp4().toPrefixBlock(); - logger.info("unlinking node to node reuse subnet: {}", subnet); - } else if (nodeOneIsDefault) { - interfaces = getNetworkInterfaces(nodeTwo, new HashSet<>()); - if (interfaces.isEmpty()) { - subnet = interfaceOne.getIp4().toPrefixBlock(); - logger.info("unlinking node one from network reuse subnet: {}", subnet); - } - } else if (nodeTwoIsDefault) { - interfaces = getNetworkInterfaces(nodeOne, new HashSet<>()); - if (interfaces.isEmpty()) { - subnet = interfaceTwo.getIp4().toPrefixBlock(); - logger.info("unlinking node two from network reuse subnet: {}", subnet); - } - } else { - logger.info("nothing to do when unlinking networks"); - } - - if (subnet != null) { - coreAddresses.reuseSubnet(subnet); - } - } - - private void handleVertexAdded(GraphEvent.Vertex vertexEvent) { - CoreNode node = vertexEvent.getVertex(); - if (node.isLoaded()) { - return; - } - - node.setNodeType(nodeType); - if (node.getType() == NodeType.EMANE) { - String emaneModel = controller.getNodeEmaneDialog().getModels().get(0); - node.setEmane(emaneModel); - } else if (node.getType() == NodeType.DOCKER || node.getType() == NodeType.LXC) { - node.setImage("ubuntu"); - } else if (node.getType() == NodeType.RJ45) { - Platform.runLater(() -> controller.getRj45Dialog().showDialog(node)); - } - - logger.info("adding user created node: {}", node); - nodeMap.put(node.getId(), node); - } - - private void handleVertexRemoved(GraphEvent.Vertex vertexEvent) { - CoreNode node = vertexEvent.getVertex(); - logger.info("removed vertex: {}", node); - nodeMap.remove(node.getId()); - } - - public void addNode(CoreNode node) { - vertexId = Math.max(node.getId() + 1, node.getId()); - double x = Math.abs(node.getPosition().getX()); - double y = Math.abs(node.getPosition().getY()); - logger.info("adding session node: {}", node); - graph.addVertex(node); - graphLayout.setLocation(node, x, y); - nodeMap.put(node.getId(), node); - } - - public void setNodeLocation(CoreNode nodeData) { - // update actual graph node - CoreNode node = nodeMap.get(nodeData.getId()); - node.getPosition().setX(nodeData.getPosition().getX()); - node.getPosition().setY(nodeData.getPosition().getY()); - - // set graph node location - double x = Math.abs(node.getPosition().getX()); - double y = Math.abs(node.getPosition().getY()); - graphLayout.setLocation(node, x, y); - graphViewer.repaint(); - } - - public void removeNode(CoreNode node) { - try { - controller.getCoreClient().deleteNode(node); - } catch (IOException ex) { - logger.error("error deleting node", ex); - Toast.error(String.format("Error deleting node: %s", node.getName())); - } - graphViewer.getPickedVertexState().pick(node, false); - graph.removeVertex(node); - graphViewer.repaint(); - } - - private boolean isWirelessNode(CoreNode node) { - return node != null && (node.getType() == NodeType.EMANE || node.getType() == NodeType.WLAN); - } - - private boolean checkForWirelessNode(CoreNode nodeOne, CoreNode nodeTwo) { - boolean result = isWirelessNode(nodeOne); - return result || isWirelessNode(nodeTwo); - } - - private long wirelessLinkCount(CoreNode node) { - return graph.getNeighbors(node).stream() - .filter(this::isWirelessNode) - .count(); - } - - public void addLink(CoreLink link) { - logger.info("adding session link: {}", link); - link.setId(linkId++); - CoreNode nodeOne = nodeMap.get(link.getNodeOne()); - CoreNode nodeTwo = nodeMap.get(link.getNodeTwo()); - - boolean isVisible = !checkForWirelessNode(nodeOne, nodeTwo); - link.setVisible(isVisible); - - graph.addEdge(link, nodeOne, nodeTwo); - } - - public void removeWirelessLink(CoreLink link) { - logger.info("deleting link: {}", link); - CoreNode nodeOne = nodeMap.get(link.getNodeOne()); - CoreNode nodeTwo = nodeMap.get(link.getNodeTwo()); - - CoreLink existingLink = graph.findEdge(nodeOne, nodeTwo); - if (existingLink != null) { - graph.removeEdge(existingLink); - } - } - - public void removeLink(CoreLink link) { - graphViewer.getPickedEdgeState().pick(link, false); - graph.removeEdge(link); - graphViewer.repaint(); - } - - public void linkMdrs(CoreNode node) { - for (CoreNode currentNode : graph.getVertices()) { - if (!"mdr".equals(currentNode.getModel())) { - continue; - } - - // only links mdrs we have not already linked - Collection links = graph.findEdgeSet(node, currentNode); - if (links.isEmpty()) { - CoreLink link = linkFactory.get(); - graph.addEdge(link, currentNode, node); - graphViewer.repaint(); - } - } - } -} diff --git a/corefx/src/main/java/com/core/graph/NodeContextMenu.java b/corefx/src/main/java/com/core/graph/NodeContextMenu.java deleted file mode 100644 index d50cd58a..00000000 --- a/corefx/src/main/java/com/core/graph/NodeContextMenu.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.core.graph; - -import com.core.Controller; -import com.core.data.CoreNode; -import com.core.ui.Toast; -import javafx.scene.control.Menu; -import javafx.scene.control.MenuItem; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.io.IOException; -import java.util.Collections; -import java.util.Set; - -class NodeContextMenu extends AbstractNodeContextMenu { - private static final Logger logger = LogManager.getLogger(); - - NodeContextMenu(Controller controller, CoreNode coreNode) { - super(controller, coreNode); - setup(); - } - - private MenuItem createStartItem(String service) { - MenuItem menuItem = new MenuItem("Start"); - menuItem.setOnAction(event -> { - try { - boolean result = controller.getCoreClient().startService(coreNode, service); - if (result) { - Toast.success("Started " + service); - } else { - Toast.error("Failure to start " + service); - } - } catch (IOException ex) { - Toast.error("Error starting " + service, ex); - } - }); - return menuItem; - } - - private MenuItem createStopItem(String service) { - MenuItem menuItem = new MenuItem("Stop"); - menuItem.setOnAction(event -> { - try { - boolean result = controller.getCoreClient().stopService(coreNode, service); - if (result) { - Toast.success("Stopped " + service); - } else { - Toast.error("Failure to stop " + service); - } - } catch (IOException ex) { - Toast.error("Error stopping " + service, ex); - } - }); - return menuItem; - } - - private MenuItem createRestartItem(String service) { - MenuItem menuItem = new MenuItem("Restart"); - menuItem.setOnAction(event -> { - try { - boolean result = controller.getCoreClient().restartService(coreNode, service); - if (result) { - Toast.success("Restarted " + service); - } else { - Toast.error("Failure to restart " + service); - } - } catch (IOException ex) { - Toast.error("Error restarting " + service, ex); - } - }); - return menuItem; - } - - private MenuItem createValidateItem(String service) { - MenuItem menuItem = new MenuItem("Validate"); - menuItem.setOnAction(event -> { - try { - boolean result = controller.getCoreClient().validateService(coreNode, service); - if (result) { - Toast.success("Validated " + service); - } else { - Toast.error("Validation failed for " + service); - } - } catch (IOException ex) { - Toast.error("Error validating " + service, ex); - } - }); - return menuItem; - } - - private void setup() { - if (controller.getCoreClient().isRunning()) { - Set services = coreNode.getServices(); - if (services.isEmpty()) { - services = controller.getDefaultServices().getOrDefault(coreNode.getModel(), Collections.emptySet()); - } - - if (!services.isEmpty()) { - Menu menu = new Menu("Manage Services"); - for (String service : services) { - Menu serviceMenu = new Menu(service); - MenuItem startItem = createStartItem(service); - MenuItem stopItem = createStopItem(service); - MenuItem restartItem = createRestartItem(service); - MenuItem validateItem = createValidateItem(service); - serviceMenu.getItems().addAll(startItem, stopItem, restartItem, validateItem); - menu.getItems().add(serviceMenu); - } - getItems().add(menu); - } - } else { - addMenuItem("Services", event -> controller.getNodeServicesDialog().showDialog(coreNode)); - addMenuItem("Delete Node", event -> controller.deleteNode(coreNode)); - } - } -} diff --git a/corefx/src/main/java/com/core/graph/RadioIcon.java b/corefx/src/main/java/com/core/graph/RadioIcon.java deleted file mode 100644 index e3139eaf..00000000 --- a/corefx/src/main/java/com/core/graph/RadioIcon.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.core.graph; - -import com.core.utils.IconUtils; -import lombok.Data; - -import javax.swing.*; -import java.awt.*; - -@Data -public class RadioIcon implements Icon { - private long wiressLinks = 0; - - @Override - public int getIconHeight() { - return IconUtils.ICON_SIZE; - } - - @Override - public int getIconWidth() { - return IconUtils.ICON_SIZE; - } - - @Override - public void paintIcon(Component c, Graphics g, int x, int y) { - g.setColor(Color.black); - for (int i = 0; i < wiressLinks; i++) { - g.fillOval(x, y, 10, 10); - x += 15; - } - } -} diff --git a/corefx/src/main/java/com/core/graph/Rj45ContextMenu.java b/corefx/src/main/java/com/core/graph/Rj45ContextMenu.java deleted file mode 100644 index 3d0da724..00000000 --- a/corefx/src/main/java/com/core/graph/Rj45ContextMenu.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.core.graph; - -import com.core.Controller; -import com.core.data.CoreNode; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -class Rj45ContextMenu extends AbstractNodeContextMenu { - private static final Logger logger = LogManager.getLogger(); - - Rj45ContextMenu(Controller controller, CoreNode coreNode) { - super(controller, coreNode); - setup(); - } - - private void setup() { - if (!controller.getCoreClient().isRunning()) { - addMenuItem("Delete Node", event -> controller.deleteNode(coreNode)); - } - } -} diff --git a/corefx/src/main/java/com/core/graph/UndirectedSimpleGraph.java b/corefx/src/main/java/com/core/graph/UndirectedSimpleGraph.java deleted file mode 100644 index 355b44a0..00000000 --- a/corefx/src/main/java/com/core/graph/UndirectedSimpleGraph.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.core.graph; - -import edu.uci.ics.jung.graph.UndirectedSparseGraph; -import edu.uci.ics.jung.graph.util.EdgeType; -import edu.uci.ics.jung.graph.util.Pair; - -public class UndirectedSimpleGraph extends UndirectedSparseGraph { - @Override - public boolean addEdge(E edge, Pair endpoints, EdgeType edgeType) { - Pair newEndpoints = getValidatedEndpoints(edge, endpoints); - if (newEndpoints == null) { - return false; - } - - V first = newEndpoints.getFirst(); - V second = newEndpoints.getSecond(); - - if (first.equals(second)) { - return false; - } else { - return super.addEdge(edge, endpoints, edgeType); - } - } -} diff --git a/corefx/src/main/java/com/core/graph/WlanContextMenu.java b/corefx/src/main/java/com/core/graph/WlanContextMenu.java deleted file mode 100644 index 937eef15..00000000 --- a/corefx/src/main/java/com/core/graph/WlanContextMenu.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.core.graph; - -import com.core.Controller; -import com.core.data.CoreNode; -import com.core.ui.dialogs.MobilityPlayerDialog; - -class WlanContextMenu extends AbstractNodeContextMenu { - WlanContextMenu(Controller controller, CoreNode coreNode) { - super(controller, coreNode); - setup(); - } - - private void setup() { - addMenuItem("WLAN Settings", event -> controller.getNodeWlanDialog().showDialog(coreNode)); - if (controller.getCoreClient().isRunning()) { - MobilityPlayerDialog mobilityPlayerDialog = controller.getMobilityPlayerDialogs().get(coreNode.getId()); - if (mobilityPlayerDialog != null && !mobilityPlayerDialog.getStage().isShowing()) { - addMenuItem("Mobility Script", event -> mobilityPlayerDialog.show()); - } - } else { - addMenuItem("Mobility", event -> controller.getMobilityDialog().showDialog(coreNode)); - addMenuItem("Link MDRs", event -> controller.getNetworkGraph().linkMdrs(coreNode)); - addMenuItem("Delete Node", event -> controller.deleteNode(coreNode)); - } - } -} diff --git a/corefx/src/main/java/com/core/ui/AnnotationToolbar.java b/corefx/src/main/java/com/core/ui/AnnotationToolbar.java deleted file mode 100644 index b32d3c8b..00000000 --- a/corefx/src/main/java/com/core/ui/AnnotationToolbar.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.core.ui; - -import com.core.graph.NetworkGraph; -import com.core.utils.FxmlUtils; -import com.jfoenix.controls.JFXColorPicker; -import com.jfoenix.controls.JFXComboBox; -import com.jfoenix.controls.JFXToggleButton; -import edu.uci.ics.jung.visualization.annotations.Annotation; -import javafx.event.ActionEvent; -import javafx.fxml.FXML; -import javafx.scene.layout.GridPane; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.awt.*; -import java.awt.geom.Ellipse2D; -import java.awt.geom.RectangularShape; -import java.awt.geom.RoundRectangle2D; - -public class AnnotationToolbar extends GridPane { - private static final Logger logger = LogManager.getLogger(); - private static final String RECTANGLE = "Rectangle"; - private static final String ROUND_RECTANGLE = "RoundRectangle"; - private static final String ELLIPSE = "Ellipse"; - private static final String UPPER_LAYER = "Upper"; - private static final String LOWER_LAYER = "Lower"; - private NetworkGraph graph; - @FXML private JFXComboBox shapeCombo; - @FXML private JFXColorPicker colorPicker; - @FXML private JFXComboBox layerCombo; - @FXML private JFXToggleButton fillToggle; - - public AnnotationToolbar(NetworkGraph graph) { - this.graph = graph; - FxmlUtils.loadRootController(this, "/fxml/annotation_toolbar.fxml"); - - // setup annotation shape combo - shapeCombo.getItems().addAll(RECTANGLE, ROUND_RECTANGLE, ELLIPSE); - shapeCombo.setOnAction(this::shapeChange); - shapeCombo.getSelectionModel().selectFirst(); - - // setup annotation layer combo - layerCombo.getItems().addAll(LOWER_LAYER, UPPER_LAYER); - layerCombo.setOnAction(this::layerChange); - layerCombo.getSelectionModel().selectFirst(); - - // setup annotation color picker - colorPicker.setOnAction(this::colorChange); - colorPicker.setValue(javafx.scene.paint.Color.AQUA); - colorPicker.fireEvent(new ActionEvent()); - - // setup annotation toggle fill - fillToggle.setOnAction(this::fillChange); - } - - private void fillChange(ActionEvent event) { - boolean selected = fillToggle.isSelected(); - graph.getGraphMouse().getAnnotatingPlugin().setFill(selected); - } - - private void colorChange(ActionEvent event) { - javafx.scene.paint.Color fxColor = colorPicker.getValue(); - java.awt.Color color = new java.awt.Color( - (float) fxColor.getRed(), - (float) fxColor.getGreen(), - (float) fxColor.getBlue(), - (float) fxColor.getOpacity() - ); - logger.info("color selected: {}", fxColor); - graph.getGraphMouse().getAnnotatingPlugin().setAnnotationColor(color); - } - - private void layerChange(ActionEvent event) { - String selected = layerCombo.getSelectionModel().getSelectedItem(); - logger.info("annotation layer selected: {}", selected); - Annotation.Layer layer; - if (LOWER_LAYER.equals(selected)) { - layer = Annotation.Layer.LOWER; - } else { - layer = Annotation.Layer.UPPER; - } - graph.getGraphMouse().getAnnotatingPlugin().setLayer(layer); - } - - private void shapeChange(ActionEvent event) { - String selected = shapeCombo.getSelectionModel().getSelectedItem(); - logger.info("annotation shape selected: {}", selected); - RectangularShape shape = new Rectangle(); - switch (selected) { - case RECTANGLE: - shape = new Rectangle(); - break; - case ROUND_RECTANGLE: - shape = new RoundRectangle2D.Double(0, 0, 0, 0, 50.0, 50.0); - break; - case ELLIPSE: - shape = new Ellipse2D.Double(); - break; - default: - Toast.error("Unknown annotation shape " + selected); - } - graph.getGraphMouse().getAnnotatingPlugin().setRectangularShape(shape); - } -} diff --git a/corefx/src/main/java/com/core/ui/DetailsPanel.java b/corefx/src/main/java/com/core/ui/DetailsPanel.java deleted file mode 100644 index 2df434fc..00000000 --- a/corefx/src/main/java/com/core/ui/DetailsPanel.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.core.ui; - -import com.core.Controller; -import com.core.data.CoreInterface; -import com.core.data.CoreNode; -import com.core.ui.textfields.DoubleFilter; -import com.core.utils.FxmlUtils; -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXTextField; -import inet.ipaddr.IPAddress; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; -import javafx.fxml.FXML; -import javafx.geometry.Insets; -import javafx.geometry.Orientation; -import javafx.scene.control.Label; -import javafx.scene.control.ScrollPane; -import javafx.scene.control.Separator; -import javafx.scene.control.TextFormatter; -import javafx.scene.layout.GridPane; -import javafx.util.converter.DoubleStringConverter; -import javafx.util.converter.IntegerStringConverter; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.util.function.UnaryOperator; - -abstract class DetailsPanel extends ScrollPane { - private static final Logger logger = LogManager.getLogger(); - private static final int START_INDEX = 1; - final Controller controller; - @FXML Label title; - @FXML ScrollPane scrollPane; - @FXML GridPane gridPane; - int index = START_INDEX; - - DetailsPanel(Controller controller) { - this.controller = controller; - FxmlUtils.loadRootController(this, "/fxml/details_panel.fxml"); - setPrefWidth(400); - } - - void setTitle(String text) { - title.setText(text); - } - - void addButton(String text, EventHandler handler) { - JFXButton emaneButton = new JFXButton(text); - emaneButton.getStyleClass().add("core-button"); - emaneButton.setMaxWidth(Double.MAX_VALUE); - emaneButton.setOnAction(handler); - gridPane.add(emaneButton, 0, index++, 2, 1); - } - - void addLabel(String text) { - Label label = new Label(text); - label.getStyleClass().add("details-label"); - gridPane.add(label, 0, index++, 2, 1); - } - - void addSeparator() { - Separator separator = new Separator(Orientation.HORIZONTAL); - gridPane.add(separator, 0, index++, 2, 1); - GridPane.setMargin(separator, new Insets(10, 0, 0, 0)); - } - - void addInterface(CoreInterface coreInterface, CoreNode linkedNode) { - if (linkedNode != null) { - addRow("Linked To", linkedNode.getName(), true); - } - addRow("Interface", coreInterface.getName(), true); - if (coreInterface.getMac() != null) { - addRow("MAC", coreInterface.getMac(), true); - } - addAddress("IP4", coreInterface.getIp4()); - addAddress("IP6", coreInterface.getIp6()); - } - - void addInterface(CoreInterface coreInterface) { - addInterface(coreInterface, null); - } - - JFXTextField addRow(String labelText, String value, boolean disabled) { - Label label = new Label(labelText); - JFXTextField textField = new JFXTextField(value); - textField.setDisable(disabled); - gridPane.addRow(index++, label, textField); - return textField; - } - - JFXTextField addDoubleRow(String labelText, Double value) { - Label label = new Label(labelText); - String valueString = null; - if (value != null) { - valueString = value.toString(); - } - JFXTextField textField = new JFXTextField(); - TextFormatter formatter = new TextFormatter<>( - new DoubleStringConverter(), null, new DoubleFilter()); - textField.setTextFormatter(formatter); - textField.setText(valueString); - gridPane.addRow(index++, label, textField); - return textField; - } - - Double getDouble(JFXTextField textField) { - if (textField.getText() == null) { - return null; - } - - Double value = null; - try { - logger.info("double field text: {}", textField.getText()); - value = Double.parseDouble(textField.getText()); - } catch (NumberFormatException ex) { - logger.error("error getting double value", ex); - } - return value; - } - - JFXTextField addIntegerRow(String labelText, Integer value) { - Label label = new Label(labelText); - String valueString = null; - if (value != null) { - valueString = value.toString(); - } - JFXTextField textField = new JFXTextField(); - UnaryOperator filter = change -> { - String text = change.getText(); - if (text.matches("[0-9]*")) { - return change; - } - return null; - }; - TextFormatter formatter = new TextFormatter<>( - new IntegerStringConverter(), null, filter); - textField.setTextFormatter(formatter); - textField.setText(valueString); - gridPane.addRow(index++, label, textField); - return textField; - } - - Integer getInteger(JFXTextField textField) { - if (textField.getText() == null) { - return null; - } - - Integer value = null; - try { - logger.info("integer field text: {}", textField.getText()); - value = Integer.parseInt(textField.getText()); - } catch (NumberFormatException ex) { - logger.error("error getting integer value", ex); - } - return value; - } - - private void addAddress(String label, IPAddress ip) { - if (ip == null) { - return; - } - addRow(label, ip.toString(), true); - } - - void clear() { - if (gridPane.getChildren().size() > START_INDEX) { - gridPane.getChildren().remove(START_INDEX, gridPane.getChildren().size()); - } - index = START_INDEX; - } -} diff --git a/corefx/src/main/java/com/core/ui/GraphToolbar.java b/corefx/src/main/java/com/core/ui/GraphToolbar.java deleted file mode 100644 index 07fa2246..00000000 --- a/corefx/src/main/java/com/core/ui/GraphToolbar.java +++ /dev/null @@ -1,292 +0,0 @@ -package com.core.ui; - -import com.core.Controller; -import com.core.data.NodeType; -import com.core.utils.FxmlUtils; -import com.core.utils.IconUtils; -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXListView; -import com.jfoenix.controls.JFXPopup; -import com.jfoenix.svg.SVGGlyph; -import edu.uci.ics.jung.visualization.control.ModalGraphMouse; -import javafx.application.Platform; -import javafx.css.PseudoClass; -import javafx.event.ActionEvent; -import javafx.fxml.FXML; -import javafx.scene.control.ComboBox; -import javafx.scene.control.Label; -import javafx.scene.control.Tooltip; -import javafx.scene.image.ImageView; -import javafx.scene.layout.VBox; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Comparator; -import java.util.HashMap; -import java.util.Map; - -public class GraphToolbar extends VBox { - private static final Logger logger = LogManager.getLogger(); - private static final int ICON_SIZE = 40; - private static final int NODES_ICON_SIZE = 20; - private static final PseudoClass START_CLASS = PseudoClass.getPseudoClass("start"); - private static final PseudoClass STOP_CLASS = PseudoClass.getPseudoClass("stop"); - private static final PseudoClass SELECTED_CLASS = PseudoClass.getPseudoClass("selected"); - private final Controller controller; - private final Map labelMap = new HashMap<>(); - private SVGGlyph startIcon; - private SVGGlyph stopIcon; - private JFXListView

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - -
- - - - - - diff --git a/corefx/src/main/resources/fxml/mobility_dialog.fxml b/corefx/src/main/resources/fxml/mobility_dialog.fxml deleted file mode 100644 index 5e813304..00000000 --- a/corefx/src/main/resources/fxml/mobility_dialog.fxml +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/corefx/src/main/resources/fxml/mobility_player.fxml b/corefx/src/main/resources/fxml/mobility_player.fxml deleted file mode 100644 index 3efea18c..00000000 --- a/corefx/src/main/resources/fxml/mobility_player.fxml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - diff --git a/corefx/src/main/resources/fxml/node_emane_dialog.fxml b/corefx/src/main/resources/fxml/node_emane_dialog.fxml deleted file mode 100644 index ec407519..00000000 --- a/corefx/src/main/resources/fxml/node_emane_dialog.fxml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/corefx/src/main/resources/fxml/node_services_dialog.fxml b/corefx/src/main/resources/fxml/node_services_dialog.fxml deleted file mode 100644 index 0ffe09d6..00000000 --- a/corefx/src/main/resources/fxml/node_services_dialog.fxml +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/corefx/src/main/resources/fxml/node_type_create_dialog.fxml b/corefx/src/main/resources/fxml/node_type_create_dialog.fxml deleted file mode 100644 index dfe8c26d..00000000 --- a/corefx/src/main/resources/fxml/node_type_create_dialog.fxml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - diff --git a/corefx/src/main/resources/fxml/node_types_dialog.fxml b/corefx/src/main/resources/fxml/node_types_dialog.fxml deleted file mode 100644 index 3cbf566b..00000000 --- a/corefx/src/main/resources/fxml/node_types_dialog.fxml +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/corefx/src/main/resources/fxml/rj45_dialog.fxml b/corefx/src/main/resources/fxml/rj45_dialog.fxml deleted file mode 100644 index 3ae18ea3..00000000 --- a/corefx/src/main/resources/fxml/rj45_dialog.fxml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/corefx/src/main/resources/fxml/service_dialog.fxml b/corefx/src/main/resources/fxml/service_dialog.fxml deleted file mode 100644 index 871d45ec..00000000 --- a/corefx/src/main/resources/fxml/service_dialog.fxml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/corefx/src/main/resources/fxml/sessions_dialog.fxml b/corefx/src/main/resources/fxml/sessions_dialog.fxml deleted file mode 100644 index 95881cad..00000000 --- a/corefx/src/main/resources/fxml/sessions_dialog.fxml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/corefx/src/main/resources/fxml/terminal_dialog.fxml b/corefx/src/main/resources/fxml/terminal_dialog.fxml deleted file mode 100644 index d0192dc3..00000000 --- a/corefx/src/main/resources/fxml/terminal_dialog.fxml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/corefx/src/main/resources/fxml/wlan_dialog.fxml b/corefx/src/main/resources/fxml/wlan_dialog.fxml deleted file mode 100644 index 90c4fabd..00000000 --- a/corefx/src/main/resources/fxml/wlan_dialog.fxml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/corefx/src/main/resources/html/geo.html b/corefx/src/main/resources/html/geo.html deleted file mode 100644 index d45cd54a..00000000 --- a/corefx/src/main/resources/html/geo.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - Geo - - - - -
- - - - diff --git a/corefx/src/main/resources/icons/dockernode-100.png b/corefx/src/main/resources/icons/dockernode-100.png deleted file mode 100644 index 2f38d8c6292baffae3dfe7e053f2c1f9006e98fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 362 zcmeAS@N?(olHy`uVBq!ia0vp^DIm<*9B zVntN82pads-WSu$X9Ox$ynRObp0VZg3(@z}gcLn`6bze=)RbSDzdrIt<(>4Kg;&;l zOptI)V(AoAaT|<`8oxi*{|xsift|?0!0=%G+OLc=ma#5sKPSft^dEz#tDnm{r-UW| D+x=~3 diff --git a/corefx/src/main/resources/icons/emane-100.png b/corefx/src/main/resources/icons/emane-100.png deleted file mode 100644 index 09b6c35c2a7da5f26b5cbfae71cf7a133430302e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1624 zcmV-e2B-OnP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01m?d01m?e$8V@)00007bV*G`2jU17 z6(&00(8ovs00r1dL_t(|+U?wZXjOF_$MN^?Z0_P*KFq19Q78+W5u{-b#q#mCq0?P1 zb217F6)8yVk2FInq5h~KB8#L&k%Bs9Y;F0#b<;mqM){BrO(GVVIp^Gb$fxCY&+peC zn*^eGAI{F*Is3l;+;-2o=lj|3cOHJf^8+D-5JCtcgb+dqA%qY@2q8rOrTWsBIcSbr z7r69@O*GrW%F^iTQS2*El6QK78gEM=F4$@IJdsXXDKHCo3JBcg&pLtQzyXck5mp69 zBoGJL##Xf)SY*&|085YvPg9e%1d@a*6UhTM0na$FxkJE;kP80dmq09+ z!*~%m>`)j0Q-B|C4Ijg%T&=-Uj|ZPcI9R;;21E- z#bfFO@^7E$YfSx68~b%&HINy@=WhX@chdDK{RsSOMu{n2#N4v^UMBQJ<5ta%1KHj15Lp;|TO%veU9a$+nd z=|IX&MMgG5!WLEtC;gkAaPPC9`rH+ny$caG4 zNF5!MCV})V=bQ*6L+a?5znlo9RqE&rg|#o~KonMsl+qE$i`CpeooFG)q?V5O*pWN+ zJjOQwm!zIaIXQ})&5i`pGY!91YMQW{z|vU9+hclkqR}7GJv1reV>EhMNCgkY`hl1N z39Et=z!s@*Ty6kM!m40fJRgdAJS2*}9%z#~$K+>VMo0yB#D8fVhf1?|x`nmBpWZBX z0A0Y~d;H!8egHmO$ef*ND$OKzuM($fQ@6yRzv+je9B$|G^lXOAu&^f3J;Og!VVyPT zbDebUcjB$xM5h7G04jkI{V0%4Ar*YNw|A1z+}@B1HYltqz;?j&lR&E7*h$h;Nvlj` z3b2;$NpqP#4dYw~x5k&LqFa4FklXf|?+0Gz=3HPJ-6Lbed+uG@fH`>httyRP9##ck zdZLBI*ota_1s=`gNFz=8>(wh!H+JUMdRhBiE@Fp6+qn?ME_N#nHv_45GmR+rTHr#G zn`zbP`6a6GtefB6l|Vu&cm`;2>6n_kh<-u$YI8>u?WUsGQ{tVu?xhxjMz3>eKW~?* zqFW&qJkvtV&BMWdHB(v)AK3ja>U3Cq>xFRDzW8tBc}icd}?cha3J_mrE8 zj1dfZ-EmK8pQS3#4)2~k3!1G=Y!}UM_N>t%Vjh@}pRXE`q5Dux}cYb&zkg_F|YE1nY z8+$u2BZorqsKQ_IiIlEsmBgF8?Z7+jTrN-bN!V9`+~X>!h`Uc;y#(w5z8y@~ z?m24O4Sc9iuKHGCcI5;JJZ#We8hs-`;QRtLs~I7L5JCtcgb+dqA%qY@2qA>;!D%XjNU%pd)zxrA2gGkx;k3K*A=dfSNi9@kPz=>ny%!sTk12Y3#nb5Bi-!|x( z{yi+|vuKsvVL1sF9yZ3qvP=HlnUV9ZmM8P)%s}O~p#DVucM75Hzn7?8~TQt+%-wZD7I}9K&hs)*H=CHG28S~S^nfJCMvzgY>b-~nnTLsroVDtuCUgo z>}JQpE6F#Stv#*)Ddy&eg9-@-91`yo{+!~HWMgF3^msdO$rk(D8h7t^FMa$q>gKln z7cJ5!H018ua`ngMlLyPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2jHAW2?_%Y zw*llLUbxBtB4(q}uc_42&XBo0(=*LXcaJ^wp7T3rn5lZJ`hDoG>aMrDGn6S)rc9YK zWy+K(Q>ILrGG)q?DN8q7c5T_8mKM8mBp)p)cI`Ib65sD?VuNPBW-T_58t{i{Z z%rnfrdE@rry0%iRtJ#q9Hk8WT?@ z#kG+#v9TtX*0CBnjvV%f%{)s5LH^m2CnldhSP&u{h_uiBDXnWIX(32%-8Nk@0-(&6 zx~=xo?fOm{cs)>u z`LY7X3$nezxFD#*d|83x1zB5>BW9k>(!euNc1?LU^<@QVF{n3BxoPkjO;gR(hgGD7 zARpe?EoPp_-b<#R&4^vHEc0O%X(0$W<^57u6dbBfdQa23R+2V?fZaDg7M-)*5n^=C zc4@z{I#!cbf`CI0;NAeZl4r5z9E@}2+Ex<)ZekXw)XX}=jL%X|Ra1u5`+Mmx>O_Y_bc zMleH=x2iYM{_3&5dIR-kyC6Mty|lxO?uR|phY_+WbA(s=37m)EGrnVG-)2xlA*P?t zP=AckC*vYBO8Swlg5=h1rybVAJKH0wMr*|J?vrA3-MhN8uMci~Bu0K7rGE6d|MQ4e zH`te}WR>{1d7rp?w0q^CK^N94X0zKGp=nJm><+#0JD8i$A28_ zIOE>`M`$ntU>rCt&u6w>-yu3?JE;#N+?D_JZ9WfeD*z5#U-zC9n`*Xb0_sgtymLKT z85V~CwhMy)Km?j?KrlE#yfS=Mw8-&*1tysOM+4NyV(6dmr)^Y>R*8dchaxgmkeZ5X zS9KE&+TH;08Q(=4H-fQU5U{oWJu&q7HVp{CZzzMZ2*5t~e(}TYLec7Lqrn+kRzt~$Y4qXyE8+Ik_%0W0OAqZG0PtLIGx6gEFX`R3$$BVP_JN(A#tS-wE`&$l( zABV3j99lYOx@ce*3sxC#PBw|-T_;6$<$BtWWRfz1B+45V+2TO!K~ZNQOEF&dG<`zb zGy}&765x>M2n}i?9P%8dZJB}N1UcM(lm;~s;FGmw296VCuROf4peDlZChHO)9483w zief=c1o&ianStX30bLIxQ+*Z-&cIn4X5mOdPIyn#;4X&a-KS_9X5mOdGOw-D&X8Gv zjc`kz@mqaUs3QddyKel$P}X}msEdUs=G#mhE6BHf---VD0WtfG2V56f2iLH^>HC)Y zHWNn*a-gkX;rdhWTpwK}j0SS5dgpo<>f_Q3Yr`xYC&;$?9T8WA-M$uaVekjh|0w#n zg5S;$TolbdcSv3M%B|l{+cE=336fQjLl=_vHvfJt3TxTObV05cz~|ug?cGg#McZ`f z8c5yvb+NWQu|$Y?af~2XVlBE`5)w#o%6o>E*8^8}!~Oj4PI1gM&}85kLC)R!E+UA9 z!Z4#{F*(;zjT7EF3Z;L z8AMX{dkybv>*wKvIBxR98aJzN1f&oJQhfrTt*x?6w2eNXFX$7izZhN`LDpAq6y+lo z!uKpR)gONkKOWX{qXI$Olf7KwGR2WwLM4(nF2#QV8;{oOU8tDmVz?j6a>w zQiM)+C;!Gx4Dwi7MjNc{0O%uf&6tmPNqIq5l&;oIfCvWn3od35j|F^olbb~ugY&nw zd^6@J9#UQq+^WnroPk5fVsn4>fY0#5lpZG%*h^DhkfsOCbfcqjwDSwL(sjUJxU9CV_tqoTq*g0XK58 zwxXeThg2+5UJxTsg9AtdVtx|gGtcL=t!SXn%uhU|ydcPlVjIoCv^1F~X#tR}9{eQJ zSmb*SQ9tpJ@`AipvO=DebLVE-&6TwoPOHz8(9}ES1p%0Lc0Kgc00bz! zec7}GXaL`Z!&hkA0g&yI=(PK#5Cq@>0=U{5JnO=`H*P`u#J!)^F$3JIh+E`>*XuE> z`nvC2QXX<(jUY*W)Ht3Sf(w7ie#(Opkdco*#QO}Bc(LYD0XV!vVbh1Za3uOuzXJdzG* zuVBSCTQ!?frc9YKWy+K(Q>ILrGG)q?DO08_Wh^7(H+CQN;;@X$`2YX_07*qoM6N<$ Ef@3~zcK`qY diff --git a/corefx/src/main/resources/icons/icomoon_material.svg b/corefx/src/main/resources/icons/icomoon_material.svg deleted file mode 100644 index db80fabb..00000000 --- a/corefx/src/main/resources/icons/icomoon_material.svg +++ /dev/null @@ -1,855 +0,0 @@ - - - -Generated by IcoMoon - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/corefx/src/main/resources/icons/lxcnode-100.png b/corefx/src/main/resources/icons/lxcnode-100.png deleted file mode 100644 index 49aaa3bc5343a3cc820eec77cf05488b1f1aecec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 394 zcmeAS@N?(olHy`uVBq!ia0vp^DIm)2;$!Ape9e-lkeXq(?O|a<*htTKM_D?qa zl~I58ZTb4bT^nDlozGX)yYcp#{Yn!E?_v5#j-Z_n%;R^H=Q}I&u^v(Eh+fkx%cNc>5T+- zo^sl5zT5rH={Zjya(}bK@Az2E24T(jm{?nmcHh+(7 TpZBwa(k+9htDnm{r-UW|Ve&!F diff --git a/corefx/src/main/resources/icons/rj45-80.png b/corefx/src/main/resources/icons/rj45-80.png deleted file mode 100644 index 8c8cfcbcfc2e98fa2fd283a199a53f5dbb7921b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 162 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r83?x6-OW6S_o&cW^SN8&+w95X6uRr~Nx46vx z%p#yTV@Z%-FoVOh8)-m}i>HfYh{fsTgaxb|M;UgoEDl@PxO8z-iEvC;N0$au)5nGJ zkDVQVymsIadVenPAfr$VxBJ2+yBswvqBsNuRQ7#Y2DGZx?O^mBr8#Xt^B6o`{an^L HB{Ts5zHK(p diff --git a/corefx/src/main/resources/icons/router-100.png b/corefx/src/main/resources/icons/router-100.png deleted file mode 100644 index 290f74b5cbc65b6dd3ccc91efd3016c42fdbc6d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2103 zcmV-72*~$|P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2h2%CK~#8N?Oj=q z6jc-+UR|Gz8kaxdgT{9SWgDiur>f%&=opY~6v2eVXM(|)2)r5zydlo8Ow!fO4h9DW zVE_r?$srg+g3F^Ih+D++JKd*AhdDjdRdsGv-I@C(UuM#^oO8ZY_uf;ts&98WI5;>s zI5;>sI5;>sv_aT4t9YRJY@yaWuUILsD+c~Xa7!`pb`%1Cm`MLOKEt-Ex3<17j^Q&n zBtid3|3f{Ma(AKXZ3Qnt0ZG2)7!-jpFY= zqPQx=xnTpXY9-lZ^A1gvD&<$v3ig42$O4*U!=|^Vr`G!lyE8+9F`*RrMF=t`36%xm z%UN`31@eu(oEe}NfvfVeBuXBLuIv($Gl$)szF1f(KZ!thOCluU;)4TqO50*H@c+X_ z^EvsZJdreUcEZj?mRpD3H zPfbWhleCTYk&e}1BQ|ddhnsKU3VOFup~%4`B~?>LuN@N;(MsA!z4B@yl5#iqgFX7! zH%%s^Sy4#|kZ7v%Y7$BLmtyAd^93Ygfyr))*jiC_O0$($qx+&8U6vsqplS-u_m1SS z7Nv@+Q<|;3h~CZIR9|$TfnrAP>WMlRGrdv zsdO&rdK5N@4R~{b}lh8`qM>;kQA}PCArgYGFo*_Gv2J@bf z`n_1-&t|y{gWmqg_~BCS6Vl+tRGvK2V^_g=s~lCdmkTSB1SD9fk=n#Dv5G5D@+G+Ng}=sD4W;sY#aav;NtBtua-aCn*c zc~rh}Xlc0Z@{UPK`$)$|rIyb~%0iYiIdHPAl98x9cI|lh-k0meZFwa5Nw~6GE`1u7 zYGrZ1EJ;3*Z%scH6LOmsO$M*(JtG;2!`6flcaQH4`wlJ<_sxRj3;ERQqgFzQ-Iemw zENgs}D&_go$>Z?;gb)w^a3oxMWQDkYnj{~{m;3$GOo+?`;{)TzM(N}qoc&M?ATF~S?3;tP zXF$tAu5BqD|;hzl7u8Ld+BCII=QDs4w%lt#F^Sfp`x+1d86K64WkKr_68I-r zMm^A%86{&`(6)pWYrQL2Mm^NOkDkFIl@e`A2wKM-W9w{T9N8loO`BFF#MIXj2(*-{ zWHfEsln`k4GRtQM7~IcDhSQ`i2|?>P&9a#Tt!pb8Pm{JJgzn#1HiHPeW?`VZBsq{4 zZAb`O#|2i*JYZ}Nx-Mf2l}*LmCj{C+qnB8&bbyL$NOUFz=D&q_J6Wdm&@P0UQwD+tQ+C3z((53XlYv7JwsH zBnP!g4azBTkPhdP5G?|o6D=*U(6(xhJOb$EWfTK&ODNBK9aH`77ctsWXe-&!I zeH^YGKvL$SwcHW|QeeBv%XmeF;>Frubfe|B(Nf06z!WG|UM4FliY0f|R8;W7HOA(> zS}QN36;Rj#5%R*o{}$s#y3aH600h1W%={VsJqG@%bRN6r9Jm_ipeq~3 z->iO|c*ywMAvS_mJ}p(f<+xHuA(XKj%lf%tgKQaDF?2eDrm`U%{TAY8@ErcXEs5I+ z=>`?YkMX${sK59;Q$g!Z@eBa{G~b4Xz}}9=eU3MT&$qMav~3N|!m6xM<>27p;Nalk h;NalkFm2J*^*^IuycG?jtc(Bv002ovPDHLkV1mxf@C^U} diff --git a/corefx/src/main/resources/icons/switch-100.png b/corefx/src/main/resources/icons/switch-100.png deleted file mode 100644 index d6d072d031a72457c679f04b91e43d61bdbab280..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1460 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 B(t,i,e,n){return"touchstart"===i?O(t,e,n):"touchmove"===i?W(t,e,n):"touchend"===i&&H(t,e,n),this}function I(t,i,e){var n=t["_leaflet_"+i+e];return"touchstart"===i?t.removeEventListener(te,n,!1):"touchmove"===i?t.removeEventListener(ie,n,!1):"touchend"===i&&(t.removeEventListener(ee,n,!1),t.removeEventListener(ne,n,!1)),this}function O(t,i,n){var o=e(function(t){if("mouse"!==t.pointerType&&t.MSPOINTER_TYPE_MOUSE&&t.pointerType!==t.MSPOINTER_TYPE_MOUSE){if(!(oe.indexOf(t.target.tagName)<0))return;Pt(t)}j(t,i)});t["_leaflet_touchstart"+n]=o,t.addEventListener(te,o,!1),re||(document.documentElement.addEventListener(te,R,!0),document.documentElement.addEventListener(ie,N,!0),document.documentElement.addEventListener(ee,D,!0),document.documentElement.addEventListener(ne,D,!0),re=!0)}function R(t){se[t.pointerId]=t,ae++}function N(t){se[t.pointerId]&&(se[t.pointerId]=t)}function D(t){delete se[t.pointerId],ae--}function j(t,i){t.touches=[];for(var e in se)t.touches.push(se[e]);t.changedTouches=[t],i(t)}function W(t,i,e){var n=function(t){(t.pointerType!==t.MSPOINTER_TYPE_MOUSE&&"mouse"!==t.pointerType||0!==t.buttons)&&j(t,i)};t["_leaflet_touchmove"+e]=n,t.addEventListener(ie,n,!1)}function H(t,i,e){var n=function(t){j(t,i)};t["_leaflet_touchend"+e]=n,t.addEventListener(ee,n,!1),t.addEventListener(ne,n,!1)}function F(t,i,e){function n(t){var i;if(Vi){if(!bi||"mouse"===t.pointerType)return;i=ae}else i=t.touches.length;if(!(i>1)){var e=Date.now(),n=e-(s||e);r=t.touches?t.touches[0]:t,a=n>0&&n<=h,s=e}}function o(t){if(a&&!r.cancelBubble){if(Vi){if(!bi||"mouse"===t.pointerType)return;var e,n,o={};for(n in r)e=r[n],o[n]=e&&e.bind?e.bind(r):e;r=o}r.type="dblclick",i(r),s=null}}var s,r,a=!1,h=250;return t[le+he+e]=n,t[le+ue+e]=o,t[le+"dblclick"+e]=i,t.addEventListener(he,n,!1),t.addEventListener(ue,o,!1),t.addEventListener("dblclick",i,!1),this}function U(t,i){var e=t[le+he+i],n=t[le+ue+i],o=t[le+"dblclick"+i];return t.removeEventListener(he,e,!1),t.removeEventListener(ue,n,!1),bi||t.removeEventListener("dblclick",o,!1),this}function V(t){return"string"==typeof t?document.getElementById(t):t}function q(t,i){var e=t.style[i]||t.currentStyle&&t.currentStyle[i];if((!e||"auto"===e)&&document.defaultView){var n=document.defaultView.getComputedStyle(t,null);e=n?n[i]:null}return"auto"===e?null:e}function G(t,i,e){var n=document.createElement(t);return n.className=i||"",e&&e.appendChild(n),n}function K(t){var i=t.parentNode;i&&i.removeChild(t)}function Y(t){for(;t.firstChild;)t.removeChild(t.firstChild)}function X(t){var i=t.parentNode;i.lastChild!==t&&i.appendChild(t)}function J(t){var i=t.parentNode;i.firstChild!==t&&i.insertBefore(t,i.firstChild)}function $(t,i){if(void 0!==t.classList)return t.classList.contains(i);var e=et(t);return e.length>0&&new RegExp("(^|\\s)"+i+"(\\s|$)").test(e)}function Q(t,i){if(void 0!==t.classList)for(var e=u(i),n=0,o=e.length;n100&&n<500||t.target._simulatedClick&&!t._simulated?Lt(t):(ge=e,i(t))}function Zt(t,i){if(!i||!t.length)return t.slice();var e=i*i;return t=Bt(t,e),t=kt(t,e)}function Et(t,i,e){return Math.sqrt(Dt(t,i,e,!0))}function kt(t,i){var e=t.length,n=new(typeof Uint8Array!=void 0+""?Uint8Array:Array)(e);n[0]=n[e-1]=1,At(t,n,i,0,e-1);var o,s=[];for(o=0;oh&&(s=r,h=a);h>e&&(i[s]=1,At(t,i,e,n,s),At(t,i,e,s,o))}function Bt(t,i){for(var e=[t[0]],n=1,o=0,s=t.length;ni&&(e.push(t[n]),o=n);return oi.max.x&&(e|=2),t.yi.max.y&&(e|=8),e}function Nt(t,i){var e=i.x-t.x,n=i.y-t.y;return e*e+n*n}function Dt(t,i,e,n){var o,s=i.x,r=i.y,a=e.x-s,h=e.y-r,u=a*a+h*h;return u>0&&((o=((t.x-s)*a+(t.y-r)*h)/u)>1?(s=e.x,r=e.y):o>0&&(s+=a*o,r+=h*o)),a=t.x-s,h=t.y-r,n?a*a+h*h:new x(s,r)}function jt(t){return!oi(t[0])||"object"!=typeof t[0][0]&&void 0!==t[0][0]}function Wt(t){return console.warn("Deprecated use of _flat, please use L.LineUtil.isFlat instead."),jt(t)}function Ht(t,i,e){var n,o,s,r,a,h,u,l,c,_=[1,4,2,8];for(o=0,u=t.length;o0?Math.floor(t):Math.ceil(t)};x.prototype={clone:function(){return new x(this.x,this.y)},add:function(t){return this.clone()._add(w(t))},_add:function(t){return this.x+=t.x,this.y+=t.y,this},subtract:function(t){return this.clone()._subtract(w(t))},_subtract:function(t){return this.x-=t.x,this.y-=t.y,this},divideBy:function(t){return this.clone()._divideBy(t)},_divideBy:function(t){return this.x/=t,this.y/=t,this},multiplyBy:function(t){return this.clone()._multiplyBy(t)},_multiplyBy:function(t){return this.x*=t,this.y*=t,this},scaleBy:function(t){return new x(this.x*t.x,this.y*t.y)},unscaleBy:function(t){return new x(this.x/t.x,this.y/t.y)},round:function(){return this.clone()._round()},_round:function(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this},floor:function(){return this.clone()._floor()},_floor:function(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this},ceil:function(){return this.clone()._ceil()},_ceil:function(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this},trunc:function(){return this.clone()._trunc()},_trunc:function(){return this.x=_i(this.x),this.y=_i(this.y),this},distanceTo:function(t){var i=(t=w(t)).x-this.x,e=t.y-this.y;return Math.sqrt(i*i+e*e)},equals:function(t){return(t=w(t)).x===this.x&&t.y===this.y},contains:function(t){return t=w(t),Math.abs(t.x)<=Math.abs(this.x)&&Math.abs(t.y)<=Math.abs(this.y)},toString:function(){return"Point("+a(this.x)+", "+a(this.y)+")"}},P.prototype={extend:function(t){return t=w(t),this.min||this.max?(this.min.x=Math.min(t.x,this.min.x),this.max.x=Math.max(t.x,this.max.x),this.min.y=Math.min(t.y,this.min.y),this.max.y=Math.max(t.y,this.max.y)):(this.min=t.clone(),this.max=t.clone()),this},getCenter:function(t){return new x((this.min.x+this.max.x)/2,(this.min.y+this.max.y)/2,t)},getBottomLeft:function(){return new x(this.min.x,this.max.y)},getTopRight:function(){return new x(this.max.x,this.min.y)},getTopLeft:function(){return this.min},getBottomRight:function(){return this.max},getSize:function(){return this.max.subtract(this.min)},contains:function(t){var i,e;return(t="number"==typeof t[0]||t instanceof x?w(t):b(t))instanceof P?(i=t.min,e=t.max):i=e=t,i.x>=this.min.x&&e.x<=this.max.x&&i.y>=this.min.y&&e.y<=this.max.y},intersects:function(t){t=b(t);var i=this.min,e=this.max,n=t.min,o=t.max,s=o.x>=i.x&&n.x<=e.x,r=o.y>=i.y&&n.y<=e.y;return s&&r},overlaps:function(t){t=b(t);var i=this.min,e=this.max,n=t.min,o=t.max,s=o.x>i.x&&n.xi.y&&n.y=n.lat&&e.lat<=o.lat&&i.lng>=n.lng&&e.lng<=o.lng},intersects:function(t){t=z(t);var i=this._southWest,e=this._northEast,n=t.getSouthWest(),o=t.getNorthEast(),s=o.lat>=i.lat&&n.lat<=e.lat,r=o.lng>=i.lng&&n.lng<=e.lng;return s&&r},overlaps:function(t){t=z(t);var i=this._southWest,e=this._northEast,n=t.getSouthWest(),o=t.getNorthEast(),s=o.lat>i.lat&&n.lati.lng&&n.lng1,Xi=!!document.createElement("canvas").getContext,Ji=!(!document.createElementNS||!E("svg").createSVGRect),$i=!Ji&&function(){try{var t=document.createElement("div");t.innerHTML='';var i=t.firstChild;return i.style.behavior="url(#default#VML)",i&&"object"==typeof i.adj}catch(t){return!1}}(),Qi=(Object.freeze||Object)({ie:Pi,ielt9:Li,edge:bi,webkit:Ti,android:zi,android23:Mi,androidStock:Si,opera:Zi,chrome:Ei,gecko:ki,safari:Ai,phantom:Bi,opera12:Ii,win:Oi,ie3d:Ri,webkit3d:Ni,gecko3d:Di,any3d:ji,mobile:Wi,mobileWebkit:Hi,mobileWebkit3d:Fi,msPointer:Ui,pointer:Vi,touch:qi,mobileOpera:Gi,mobileGecko:Ki,retina:Yi,canvas:Xi,svg:Ji,vml:$i}),te=Ui?"MSPointerDown":"pointerdown",ie=Ui?"MSPointerMove":"pointermove",ee=Ui?"MSPointerUp":"pointerup",ne=Ui?"MSPointerCancel":"pointercancel",oe=["INPUT","SELECT","OPTION"],se={},re=!1,ae=0,he=Ui?"MSPointerDown":Vi?"pointerdown":"touchstart",ue=Ui?"MSPointerUp":Vi?"pointerup":"touchend",le="_leaflet_",ce=st(["transform","webkitTransform","OTransform","MozTransform","msTransform"]),_e=st(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),de="webkitTransition"===_e||"OTransition"===_e?_e+"End":"transitionend";if("onselectstart"in document)fi=function(){mt(window,"selectstart",Pt)},gi=function(){ft(window,"selectstart",Pt)};else{var pe=st(["userSelect","WebkitUserSelect","OUserSelect","MozUserSelect","msUserSelect"]);fi=function(){if(pe){var t=document.documentElement.style;vi=t[pe],t[pe]="none"}},gi=function(){pe&&(document.documentElement.style[pe]=vi,vi=void 0)}}var me,fe,ge,ve=(Object.freeze||Object)({TRANSFORM:ce,TRANSITION:_e,TRANSITION_END:de,get:V,getStyle:q,create:G,remove:K,empty:Y,toFront:X,toBack:J,hasClass:$,addClass:Q,removeClass:tt,setClass:it,getClass:et,setOpacity:nt,testProp:st,setTransform:rt,setPosition:at,getPosition:ht,disableTextSelection:fi,enableTextSelection:gi,disableImageDrag:ut,enableImageDrag:lt,preventOutline:ct,restoreOutline:_t,getSizedParentNode:dt,getScale:pt}),ye="_leaflet_events",xe=Oi&&Ei?2*window.devicePixelRatio:ki?window.devicePixelRatio:1,we={},Pe=(Object.freeze||Object)({on:mt,off:ft,stopPropagation:yt,disableScrollPropagation:xt,disableClickPropagation:wt,preventDefault:Pt,stop:Lt,getMousePosition:bt,getWheelDelta:Tt,fakeStop:zt,skipped:Mt,isExternalTarget:Ct,addListener:mt,removeListener:ft}),Le=ci.extend({run:function(t,i,e,n){this.stop(),this._el=t,this._inProgress=!0,this._duration=e||.25,this._easeOutPower=1/Math.max(n||.5,.2),this._startPos=ht(t),this._offset=i.subtract(this._startPos),this._startTime=+new Date,this.fire("start"),this._animate()},stop:function(){this._inProgress&&(this._step(!0),this._complete())},_animate:function(){this._animId=f(this._animate,this),this._step()},_step:function(t){var i=+new Date-this._startTime,e=1e3*this._duration;ithis.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,i){this._enforcingBounds=!0;var e=this.getCenter(),n=this._limitCenter(e,this._zoom,z(t));return e.equals(n)||this.panTo(n,i),this._enforcingBounds=!1,this},invalidateSize:function(t){if(!this._loaded)return this;t=i({animate:!1,pan:!0},!0===t?{animate:!0}:t);var n=this.getSize();this._sizeChanged=!0,this._lastCenter=null;var o=this.getSize(),s=n.divideBy(2).round(),r=o.divideBy(2).round(),a=s.subtract(r);return a.x||a.y?(t.animate&&t.pan?this.panBy(a):(t.pan&&this._rawPanBy(a),this.fire("move"),t.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(e(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:n,newSize:o})):this},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(t){if(t=this._locateOptions=i({timeout:1e4,watch:!1},t),!("geolocation"in navigator))return this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this;var n=e(this._handleGeolocationResponse,this),o=e(this._handleGeolocationError,this);return t.watch?this._locationWatchId=navigator.geolocation.watchPosition(n,o,t):navigator.geolocation.getCurrentPosition(n,o,t),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var i=t.code,e=t.message||(1===i?"permission denied":2===i?"position unavailable":"timeout");this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:i,message:"Geolocation error: "+e+"."})},_handleGeolocationResponse:function(t){var i=new M(t.coords.latitude,t.coords.longitude),e=i.toBounds(2*t.coords.accuracy),n=this._locateOptions;if(n.setView){var o=this.getBoundsZoom(e);this.setView(i,n.maxZoom?Math.min(o,n.maxZoom):o)}var s={latlng:i,bounds:e,timestamp:t.timestamp};for(var r in t.coords)"number"==typeof t.coords[r]&&(s[r]=t.coords[r]);this.fire("locationfound",s)},addHandler:function(t,i){if(!i)return this;var e=this[t]=new i(this);return this._handlers.push(e),this.options[t]&&e.enable(),this},remove:function(){if(this._initEvents(!0),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch(t){this._container._leaflet_id=void 0,this._containerId=void 0}void 0!==this._locationWatchId&&this.stopLocate(),this._stop(),K(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._resizeRequest&&(g(this._resizeRequest),this._resizeRequest=null),this._clearHandlers(),this._loaded&&this.fire("unload");var t;for(t in this._layers)this._layers[t].remove();for(t in this._panes)K(this._panes[t]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(t,i){var e=G("div","leaflet-pane"+(t?" leaflet-"+t.replace("Pane","")+"-pane":""),i||this._mapPane);return t&&(this._panes[t]=e),e},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter:this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var t=this.getPixelBounds();return new T(this.unproject(t.getBottomLeft()),this.unproject(t.getTopRight()))},getMinZoom:function(){return void 0===this.options.minZoom?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return void 0===this.options.maxZoom?void 0===this._layersMaxZoom?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(t,i,e){t=z(t),e=w(e||[0,0]);var n=this.getZoom()||0,o=this.getMinZoom(),s=this.getMaxZoom(),r=t.getNorthWest(),a=t.getSouthEast(),h=this.getSize().subtract(e),u=b(this.project(a,n),this.project(r,n)).getSize(),l=ji?this.options.zoomSnap:1,c=h.x/u.x,_=h.y/u.y,d=i?Math.max(c,_):Math.min(c,_);return n=this.getScaleZoom(d,n),l&&(n=Math.round(n/(l/100))*(l/100),n=i?Math.ceil(n/l)*l:Math.floor(n/l)*l),Math.max(o,Math.min(s,n))},getSize:function(){return this._size&&!this._sizeChanged||(this._size=new x(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(t,i){var e=this._getTopLeftPoint(t,i);return new P(e,e.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(t){return this.options.crs.getProjectedBounds(void 0===t?this.getZoom():t)},getPane:function(t){return"string"==typeof t?this._panes[t]:t},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(t,i){var e=this.options.crs;return i=void 0===i?this._zoom:i,e.scale(t)/e.scale(i)},getScaleZoom:function(t,i){var e=this.options.crs;i=void 0===i?this._zoom:i;var n=e.zoom(t*e.scale(i));return isNaN(n)?1/0:n},project:function(t,i){return i=void 0===i?this._zoom:i,this.options.crs.latLngToPoint(C(t),i)},unproject:function(t,i){return i=void 0===i?this._zoom:i,this.options.crs.pointToLatLng(w(t),i)},layerPointToLatLng:function(t){var i=w(t).add(this.getPixelOrigin());return this.unproject(i)},latLngToLayerPoint:function(t){return this.project(C(t))._round()._subtract(this.getPixelOrigin())},wrapLatLng:function(t){return this.options.crs.wrapLatLng(C(t))},wrapLatLngBounds:function(t){return this.options.crs.wrapLatLngBounds(z(t))},distance:function(t,i){return this.options.crs.distance(C(t),C(i))},containerPointToLayerPoint:function(t){return w(t).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(t){return w(t).add(this._getMapPanePos())},containerPointToLatLng:function(t){var i=this.containerPointToLayerPoint(w(t));return this.layerPointToLatLng(i)},latLngToContainerPoint:function(t){return this.layerPointToContainerPoint(this.latLngToLayerPoint(C(t)))},mouseEventToContainerPoint:function(t){return bt(t,this._container)},mouseEventToLayerPoint:function(t){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t))},mouseEventToLatLng:function(t){return this.layerPointToLatLng(this.mouseEventToLayerPoint(t))},_initContainer:function(t){var i=this._container=V(t);if(!i)throw new Error("Map container not found.");if(i._leaflet_id)throw new Error("Map container is already initialized.");mt(i,"scroll",this._onScroll,this),this._containerId=n(i)},_initLayout:function(){var t=this._container;this._fadeAnimated=this.options.fadeAnimation&&ji,Q(t,"leaflet-container"+(qi?" leaflet-touch":"")+(Yi?" leaflet-retina":"")+(Li?" leaflet-oldie":"")+(Ai?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":""));var i=q(t,"position");"absolute"!==i&&"relative"!==i&&"fixed"!==i&&(t.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var t=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),at(this._mapPane,new x(0,0)),this.createPane("tilePane"),this.createPane("shadowPane"),this.createPane("overlayPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(Q(t.markerPane,"leaflet-zoom-hide"),Q(t.shadowPane,"leaflet-zoom-hide"))},_resetView:function(t,i){at(this._mapPane,new x(0,0));var e=!this._loaded;this._loaded=!0,i=this._limitZoom(i),this.fire("viewprereset");var n=this._zoom!==i;this._moveStart(n,!1)._move(t,i)._moveEnd(n),this.fire("viewreset"),e&&this.fire("load")},_moveStart:function(t,i){return t&&this.fire("zoomstart"),i||this.fire("movestart"),this},_move:function(t,i,e){void 0===i&&(i=this._zoom);var n=this._zoom!==i;return this._zoom=i,this._lastCenter=t,this._pixelOrigin=this._getNewPixelOrigin(t),(n||e&&e.pinch)&&this.fire("zoom",e),this.fire("move",e)},_moveEnd:function(t){return t&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return g(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(t){at(this._mapPane,this._getMapPanePos().subtract(t))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(t){this._targets={},this._targets[n(this._container)]=this;var i=t?ft:mt;i(this._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress",this._handleDOMEvent,this),this.options.trackResize&&i(window,"resize",this._onResize,this),ji&&this.options.transform3DLimit&&(t?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){g(this._resizeRequest),this._resizeRequest=f(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var t=this._getMapPanePos();Math.max(Math.abs(t.x),Math.abs(t.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,i){for(var e,o=[],s="mouseout"===i||"mouseover"===i,r=t.target||t.srcElement,a=!1;r;){if((e=this._targets[n(r)])&&("click"===i||"preclick"===i)&&!t._simulated&&this._draggableMoved(e)){a=!0;break}if(e&&e.listens(i,!0)){if(s&&!Ct(r,t))break;if(o.push(e),s)break}if(r===this._container)break;r=r.parentNode}return o.length||a||s||!Ct(r,t)||(o=[this]),o},_handleDOMEvent:function(t){if(this._loaded&&!Mt(t)){var i=t.type;"mousedown"!==i&&"keypress"!==i||ct(t.target||t.srcElement),this._fireDOMEvent(t,i)}},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,e,n){if("click"===t.type){var o=i({},t);o.type="preclick",this._fireDOMEvent(o,o.type,n)}if(!t._stopped&&(n=(n||[]).concat(this._findEventTargets(t,e))).length){var s=n[0];"contextmenu"===e&&s.listens(e,!0)&&Pt(t);var r={originalEvent:t};if("keypress"!==t.type){var a=s.getLatLng&&(!s._radius||s._radius<=10);r.containerPoint=a?this.latLngToContainerPoint(s.getLatLng()):this.mouseEventToContainerPoint(t),r.layerPoint=this.containerPointToLayerPoint(r.containerPoint),r.latlng=a?s.getLatLng():this.layerPointToLatLng(r.layerPoint)}for(var h=0;h0?Math.round(t-i)/2:Math.max(0,Math.ceil(t))-Math.max(0,Math.floor(i))},_limitZoom:function(t){var i=this.getMinZoom(),e=this.getMaxZoom(),n=ji?this.options.zoomSnap:1;return n&&(t=Math.round(t/n)*n),Math.max(i,Math.min(e,t))},_onPanTransitionStep:function(){this.fire("move")},_onPanTransitionEnd:function(){tt(this._mapPane,"leaflet-pan-anim"),this.fire("moveend")},_tryAnimatedPan:function(t,i){var e=this._getCenterOffset(t)._trunc();return!(!0!==(i&&i.animate)&&!this.getSize().contains(e))&&(this.panBy(e,i),!0)},_createAnimProxy:function(){var t=this._proxy=G("div","leaflet-proxy leaflet-zoom-animated");this._panes.mapPane.appendChild(t),this.on("zoomanim",function(t){var i=ce,e=this._proxy.style[i];rt(this._proxy,this.project(t.center,t.zoom),this.getZoomScale(t.zoom,1)),e===this._proxy.style[i]&&this._animatingZoom&&this._onZoomTransitionEnd()},this),this.on("load moveend",function(){var t=this.getCenter(),i=this.getZoom();rt(this._proxy,this.project(t,i),this.getZoomScale(i,1))},this),this._on("unload",this._destroyAnimProxy,this)},_destroyAnimProxy:function(){K(this._proxy),delete this._proxy},_catchTransitionEnd:function(t){this._animatingZoom&&t.propertyName.indexOf("transform")>=0&&this._onZoomTransitionEnd()},_nothingToAnimate:function(){return!this._container.getElementsByClassName("leaflet-zoom-animated").length},_tryAnimatedZoom:function(t,i,e){if(this._animatingZoom)return!0;if(e=e||{},!this._zoomAnimated||!1===e.animate||this._nothingToAnimate()||Math.abs(i-this._zoom)>this.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(i),o=this._getCenterOffset(t)._divideBy(1-1/n);return!(!0!==e.animate&&!this.getSize().contains(o))&&(f(function(){this._moveStart(!0,!1)._animateZoom(t,i,!0)},this),!0)},_animateZoom:function(t,i,n,o){this._mapPane&&(n&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=i,Q(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:i,noUpdate:o}),setTimeout(e(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&tt(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom),f(function(){this._moveEnd(!0)},this))}}),Te=v.extend({options:{position:"topright"},initialize:function(t){l(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var i=this._map;return i&&i.removeControl(this),this.options.position=t,i&&i.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var i=this._container=this.onAdd(t),e=this.getPosition(),n=t._controlCorners[e];return Q(i,"leaflet-control"),-1!==e.indexOf("bottom")?n.insertBefore(i,n.firstChild):n.appendChild(i),this},remove:function(){return this._map?(K(this._container),this.onRemove&&this.onRemove(this._map),this._map=null,this):this},_refocusOnMap:function(t){this._map&&t&&t.screenX>0&&t.screenY>0&&this._map.getContainer().focus()}}),ze=function(t){return new Te(t)};be.include({addControl:function(t){return t.addTo(this),this},removeControl:function(t){return t.remove(),this},_initControlPos:function(){function t(t,o){var s=e+t+" "+e+o;i[t+o]=G("div",s,n)}var i=this._controlCorners={},e="leaflet-",n=this._controlContainer=G("div",e+"control-container",this._container);t("top","left"),t("top","right"),t("bottom","left"),t("bottom","right")},_clearControlPos:function(){for(var t in this._controlCorners)K(this._controlCorners[t]);K(this._controlContainer),delete this._controlCorners,delete this._controlContainer}});var Me=Te.extend({options:{collapsed:!0,position:"topright",autoZIndex:!0,hideSingleBase:!1,sortLayers:!1,sortFunction:function(t,i,e,n){return e1,this._baseLayersList.style.display=t?"":"none"),this._separator.style.display=i&&t?"":"none",this},_onLayerChange:function(t){this._handlingClick||this._update();var i=this._getLayer(n(t.target)),e=i.overlay?"add"===t.type?"overlayadd":"overlayremove":"add"===t.type?"baselayerchange":null;e&&this._map.fire(e,i)},_createRadioElement:function(t,i){var e='",n=document.createElement("div");return n.innerHTML=e,n.firstChild},_addItem:function(t){var i,e=document.createElement("label"),o=this._map.hasLayer(t.layer);t.overlay?((i=document.createElement("input")).type="checkbox",i.className="leaflet-control-layers-selector",i.defaultChecked=o):i=this._createRadioElement("leaflet-base-layers",o),this._layerControlInputs.push(i),i.layerId=n(t.layer),mt(i,"click",this._onInputClick,this);var s=document.createElement("span");s.innerHTML=" "+t.name;var r=document.createElement("div");return e.appendChild(r),r.appendChild(i),r.appendChild(s),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(e),this._checkDisabledLayers(),e},_onInputClick:function(){var t,i,e=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=e.length-1;s>=0;s--)t=e[s],i=this._getLayer(t.layerId).layer,t.checked?n.push(i):t.checked||o.push(i);for(s=0;s=0;o--)t=e[o],i=this._getLayer(t.layerId).layer,t.disabled=void 0!==i.options.minZoom&&ni.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expand:function(){return this.expand()},_collapse:function(){return this.collapse()}}),Ce=Te.extend({options:{position:"topleft",zoomInText:"+",zoomInTitle:"Zoom in",zoomOutText:"−",zoomOutTitle:"Zoom out"},onAdd:function(t){var i="leaflet-control-zoom",e=G("div",i+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,i+"-in",e,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,i+"-out",e,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),e},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,i,e,n,o){var s=G("a",e,n);return s.innerHTML=t,s.href="#",s.title=i,s.setAttribute("role","button"),s.setAttribute("aria-label",i),wt(s),mt(s,"click",Lt),mt(s,"click",o,this),mt(s,"click",this._refocusOnMap,this),s},_updateDisabled:function(){var t=this._map,i="leaflet-disabled";tt(this._zoomInButton,i),tt(this._zoomOutButton,i),(this._disabled||t._zoom===t.getMinZoom())&&Q(this._zoomOutButton,i),(this._disabled||t._zoom===t.getMaxZoom())&&Q(this._zoomInButton,i)}});be.mergeOptions({zoomControl:!0}),be.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new Ce,this.addControl(this.zoomControl))});var Se=Te.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var i=G("div","leaflet-control-scale"),e=this.options;return this._addScales(e,"leaflet-control-scale-line",i),t.on(e.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,i,e){t.metric&&(this._mScale=G("div",i,e)),t.imperial&&(this._iScale=G("div",i,e))},_update:function(){var t=this._map,i=t.getSize().y/2,e=t.distance(t.containerPointToLatLng([0,i]),t.containerPointToLatLng([this.options.maxWidth,i]));this._updateScales(e)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var i=this._getRoundNum(t),e=i<1e3?i+" m":i/1e3+" km";this._updateScale(this._mScale,e,i/t)},_updateImperial:function(t){var i,e,n,o=3.2808399*t;o>5280?(i=o/5280,e=this._getRoundNum(i),this._updateScale(this._iScale,e+" mi",e/i)):(n=this._getRoundNum(o),this._updateScale(this._iScale,n+" ft",n/o))},_updateScale:function(t,i,e){t.style.width=Math.round(this.options.maxWidth*e)+"px",t.innerHTML=i},_getRoundNum:function(t){var i=Math.pow(10,(Math.floor(t)+"").length-1),e=t/i;return e=e>=10?10:e>=5?5:e>=3?3:e>=2?2:1,i*e}}),Ze=Te.extend({options:{position:"bottomright",prefix:'Leaflet'},initialize:function(t){l(this,t),this._attributions={}},onAdd:function(t){t.attributionControl=this,this._container=G("div","leaflet-control-attribution"),wt(this._container);for(var i in t._layers)t._layers[i].getAttribution&&this.addAttribution(t._layers[i].getAttribution());return this._update(),this._container},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t?(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update(),this):this},removeAttribution:function(t){return t?(this._attributions[t]&&(this._attributions[t]--,this._update()),this):this},_update:function(){if(this._map){var t=[];for(var i in this._attributions)this._attributions[i]&&t.push(i);var e=[];this.options.prefix&&e.push(this.options.prefix),t.length&&e.push(t.join(", ")),this._container.innerHTML=e.join(" | ")}}});be.mergeOptions({attributionControl:!0}),be.addInitHook(function(){this.options.attributionControl&&(new Ze).addTo(this)});Te.Layers=Me,Te.Zoom=Ce,Te.Scale=Se,Te.Attribution=Ze,ze.layers=function(t,i,e){return new Me(t,i,e)},ze.zoom=function(t){return new Ce(t)},ze.scale=function(t){return new Se(t)},ze.attribution=function(t){return new Ze(t)};var Ee=v.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled?this:(this._enabled=!0,this.addHooks(),this)},disable:function(){return this._enabled?(this._enabled=!1,this.removeHooks(),this):this},enabled:function(){return!!this._enabled}});Ee.addTo=function(t,i){return t.addHandler(i,this),this};var ke,Ae={Events:li},Be=qi?"touchstart mousedown":"mousedown",Ie={mousedown:"mouseup",touchstart:"touchend",pointerdown:"touchend",MSPointerDown:"touchend"},Oe={mousedown:"mousemove",touchstart:"touchmove",pointerdown:"touchmove",MSPointerDown:"touchmove"},Re=ci.extend({options:{clickTolerance:3},initialize:function(t,i,e,n){l(this,n),this._element=t,this._dragStartTarget=i||t,this._preventOutline=e},enable:function(){this._enabled||(mt(this._dragStartTarget,Be,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(Re._dragging===this&&this.finishDrag(),ft(this._dragStartTarget,Be,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){if(!t._simulated&&this._enabled&&(this._moved=!1,!$(this._element,"leaflet-zoom-anim")&&!(Re._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||(Re._dragging=this,this._preventOutline&&ct(this._element),ut(),fi(),this._moving)))){this.fire("down");var i=t.touches?t.touches[0]:t,e=dt(this._element);this._startPoint=new x(i.clientX,i.clientY),this._parentScale=pt(e),mt(document,Oe[t.type],this._onMove,this),mt(document,Ie[t.type],this._onUp,this)}},_onMove:function(t){if(!t._simulated&&this._enabled)if(t.touches&&t.touches.length>1)this._moved=!0;else{var i=t.touches&&1===t.touches.length?t.touches[0]:t,e=new x(i.clientX,i.clientY)._subtract(this._startPoint);(e.x||e.y)&&(Math.abs(e.x)+Math.abs(e.y)1e-7;h++)i=s*Math.sin(a),i=Math.pow((1-i)/(1+i),s/2),a+=u=Math.PI/2-2*Math.atan(r*i)-a;return new M(a*e,t.x*e/n)}},He=(Object.freeze||Object)({LonLat:je,Mercator:We,SphericalMercator:mi}),Fe=i({},pi,{code:"EPSG:3395",projection:We,transformation:function(){var t=.5/(Math.PI*We.R);return Z(t,.5,-t,.5)}()}),Ue=i({},pi,{code:"EPSG:4326",projection:je,transformation:Z(1/180,1,-1/180,.5)}),Ve=i({},di,{projection:je,transformation:Z(1,0,-1,0),scale:function(t){return Math.pow(2,t)},zoom:function(t){return Math.log(t)/Math.LN2},distance:function(t,i){var e=i.lng-t.lng,n=i.lat-t.lat;return Math.sqrt(e*e+n*n)},infinite:!0});di.Earth=pi,di.EPSG3395=Fe,di.EPSG3857=yi,di.EPSG900913=xi,di.EPSG4326=Ue,di.Simple=Ve;var qe=ci.extend({options:{pane:"overlayPane",attribution:null,bubblingMouseEvents:!0},addTo:function(t){return t.addLayer(this),this},remove:function(){return this.removeFrom(this._map||this._mapToAdd)},removeFrom:function(t){return t&&t.removeLayer(this),this},getPane:function(t){return this._map.getPane(t?this.options[t]||t:this.options.pane)},addInteractiveTarget:function(t){return this._map._targets[n(t)]=this,this},removeInteractiveTarget:function(t){return delete this._map._targets[n(t)],this},getAttribution:function(){return this.options.attribution},_layerAdd:function(t){var i=t.target;if(i.hasLayer(this)){if(this._map=i,this._zoomAnimated=i._zoomAnimated,this.getEvents){var e=this.getEvents();i.on(e,this),this.once("remove",function(){i.off(e,this)},this)}this.onAdd(i),this.getAttribution&&i.attributionControl&&i.attributionControl.addAttribution(this.getAttribution()),this.fire("add"),i.fire("layeradd",{layer:this})}}});be.include({addLayer:function(t){if(!t._layerAdd)throw new Error("The provided object is not a Layer.");var i=n(t);return this._layers[i]?this:(this._layers[i]=t,t._mapToAdd=this,t.beforeAdd&&t.beforeAdd(this),this.whenReady(t._layerAdd,t),this)},removeLayer:function(t){var i=n(t);return this._layers[i]?(this._loaded&&t.onRemove(this),t.getAttribution&&this.attributionControl&&this.attributionControl.removeAttribution(t.getAttribution()),delete this._layers[i],this._loaded&&(this.fire("layerremove",{layer:t}),t.fire("remove")),t._map=t._mapToAdd=null,this):this},hasLayer:function(t){return!!t&&n(t)in this._layers},eachLayer:function(t,i){for(var e in this._layers)t.call(i,this._layers[e]);return this},_addLayers:function(t){for(var i=0,e=(t=t?oi(t)?t:[t]:[]).length;ithis._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()i)return r=(n-i)/e,this._map.layerPointToLatLng([s.x-r*(s.x-o.x),s.y-r*(s.y-o.y)])},getBounds:function(){return this._bounds},addLatLng:function(t,i){return i=i||this._defaultShape(),t=C(t),i.push(t),this._bounds.extend(t),this.redraw()},_setLatLngs:function(t){this._bounds=new T,this._latlngs=this._convertLatLngs(t)},_defaultShape:function(){return jt(this._latlngs)?this._latlngs:this._latlngs[0]},_convertLatLngs:function(t){for(var i=[],e=jt(t),n=0,o=t.length;n=2&&i[0]instanceof M&&i[0].equals(i[e-1])&&i.pop(),i},_setLatLngs:function(t){nn.prototype._setLatLngs.call(this,t),jt(this._latlngs)&&(this._latlngs=[this._latlngs])},_defaultShape:function(){return jt(this._latlngs[0])?this._latlngs[0]:this._latlngs[0][0]},_clipPoints:function(){var t=this._renderer._bounds,i=this.options.weight,e=new x(i,i);if(t=new P(t.min.subtract(e),t.max.add(e)),this._parts=[],this._pxBounds&&this._pxBounds.intersects(t))if(this.options.noClip)this._parts=this._rings;else for(var n,o=0,s=this._rings.length;ot.y!=n.y>t.y&&t.x<(n.x-e.x)*(t.y-e.y)/(n.y-e.y)+e.x&&(u=!u);return u||nn.prototype._containsPoint.call(this,t,!0)}}),sn=Ke.extend({initialize:function(t,i){l(this,i),this._layers={},t&&this.addData(t)},addData:function(t){var i,e,n,o=oi(t)?t:t.features;if(o){for(i=0,e=o.length;i0?o:[i.src]}else{oi(this._url)||(this._url=[this._url]),i.autoplay=!!this.options.autoplay,i.loop=!!this.options.loop;for(var a=0;ao?(i.height=o+"px",Q(t,"leaflet-popup-scrolled")):tt(t,"leaflet-popup-scrolled"),this._containerWidth=this._container.offsetWidth},_animateZoom:function(t){var i=this._map._latLngToNewLayerPoint(this._latlng,t.zoom,t.center),e=this._getAnchor();at(this._container,i.add(e))},_adjustPan:function(){if(!(!this.options.autoPan||this._map._panAnim&&this._map._panAnim._inProgress)){var t=this._map,i=parseInt(q(this._container,"marginBottom"),10)||0,e=this._container.offsetHeight+i,n=this._containerWidth,o=new x(this._containerLeft,-e-this._containerBottom);o._add(ht(this._container));var s=t.layerPointToContainerPoint(o),r=w(this.options.autoPanPadding),a=w(this.options.autoPanPaddingTopLeft||r),h=w(this.options.autoPanPaddingBottomRight||r),u=t.getSize(),l=0,c=0;s.x+n+h.x>u.x&&(l=s.x+n-u.x+h.x),s.x-l-a.x<0&&(l=s.x-a.x),s.y+e+h.y>u.y&&(c=s.y+e-u.y+h.y),s.y-c-a.y<0&&(c=s.y-a.y),(l||c)&&t.fire("autopanstart").panBy([l,c])}},_onCloseButtonClick:function(t){this._close(),Lt(t)},_getAnchor:function(){return w(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}});be.mergeOptions({closePopupOnClick:!0}),be.include({openPopup:function(t,i,e){return t instanceof cn||(t=new cn(e).setContent(t)),i&&t.setLatLng(i),this.hasLayer(t)?this:(this._popup&&this._popup.options.autoClose&&this.closePopup(),this._popup=t,this.addLayer(t))},closePopup:function(t){return t&&t!==this._popup||(t=this._popup,this._popup=null),t&&this.removeLayer(t),this}}),qe.include({bindPopup:function(t,i){return t instanceof cn?(l(t,i),this._popup=t,t._source=this):(this._popup&&!i||(this._popup=new cn(i,this)),this._popup.setContent(t)),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t,i){if(t instanceof qe||(i=t,t=this),t instanceof Ke)for(var e in this._layers){t=this._layers[e];break}return i||(i=t.getCenter?t.getCenter():t.getLatLng()),this._popup&&this._map&&(this._popup._source=t,this._popup.update(),this._map.openPopup(this._popup,i)),this},closePopup:function(){return this._popup&&this._popup._close(),this},togglePopup:function(t){return this._popup&&(this._popup._map?this.closePopup():this.openPopup(t)),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var i=t.layer||t.target;this._popup&&this._map&&(Lt(t),i instanceof Qe?this.openPopup(t.layer||t.target,t.latlng):this._map.hasLayer(this._popup)&&this._popup._source===i?this.closePopup():this.openPopup(i,t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}});var _n=ln.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,interactive:!1,opacity:.9},onAdd:function(t){ln.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&this._source.fire("tooltipopen",{tooltip:this},!0)},onRemove:function(t){ln.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&this._source.fire("tooltipclose",{tooltip:this},!0)},getEvents:function(){var t=ln.prototype.getEvents.call(this);return qi&&!this.options.permanent&&(t.preclick=this._close),t},_close:function(){this._map&&this._map.closeTooltip(this)},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=G("div",t)},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var i=this._map,e=this._container,n=i.latLngToContainerPoint(i.getCenter()),o=i.layerPointToContainerPoint(t),s=this.options.direction,r=e.offsetWidth,a=e.offsetHeight,h=w(this.options.offset),u=this._getAnchor();"top"===s?t=t.add(w(-r/2+h.x,-a+h.y+u.y,!0)):"bottom"===s?t=t.subtract(w(r/2-h.x,-h.y,!0)):"center"===s?t=t.subtract(w(r/2+h.x,a/2-u.y+h.y,!0)):"right"===s||"auto"===s&&o.xthis.options.maxZoom||en&&this._retainParent(o,s,r,n))},_retainChildren:function(t,i,e,n){for(var o=2*t;o<2*t+2;o++)for(var s=2*i;s<2*i+2;s++){var r=new x(o,s);r.z=e+1;var a=this._tileCoordsToKey(r),h=this._tiles[a];h&&h.active?h.retain=!0:(h&&h.loaded&&(h.retain=!0),e+1this.options.maxZoom||void 0!==this.options.minZoom&&o1)this._setView(t,e);else{for(var c=o.min.y;c<=o.max.y;c++)for(var _=o.min.x;_<=o.max.x;_++){var d=new x(_,c);if(d.z=this._tileZoom,this._isValidTile(d)){var p=this._tiles[this._tileCoordsToKey(d)];p?p.current=!0:r.push(d)}}if(r.sort(function(t,i){return t.distanceTo(s)-i.distanceTo(s)}),0!==r.length){this._loading||(this._loading=!0,this.fire("loading"));var m=document.createDocumentFragment();for(_=0;_e.max.x)||!i.wrapLat&&(t.ye.max.y))return!1}if(!this.options.bounds)return!0;var n=this._tileCoordsToBounds(t);return z(this.options.bounds).overlaps(n)},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var i=this._map,e=this.getTileSize(),n=t.scaleBy(e),o=n.add(e);return[i.unproject(n,t.z),i.unproject(o,t.z)]},_tileCoordsToBounds:function(t){var i=this._tileCoordsToNwSe(t),e=new T(i[0],i[1]);return this.options.noWrap||(e=this._map.wrapLatLngBounds(e)),e},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var i=t.split(":"),e=new x(+i[0],+i[1]);return e.z=+i[2],e},_removeTile:function(t){var i=this._tiles[t];i&&(K(i.el),delete this._tiles[t],this.fire("tileunload",{tile:i.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){Q(t,"leaflet-tile");var i=this.getTileSize();t.style.width=i.x+"px",t.style.height=i.y+"px",t.onselectstart=r,t.onmousemove=r,Li&&this.options.opacity<1&&nt(t,this.options.opacity),zi&&!Mi&&(t.style.WebkitBackfaceVisibility="hidden")},_addTile:function(t,i){var n=this._getTilePos(t),o=this._tileCoordsToKey(t),s=this.createTile(this._wrapCoords(t),e(this._tileReady,this,t));this._initTile(s),this.createTile.length<2&&f(e(this._tileReady,this,t,null,s)),at(s,n),this._tiles[o]={el:s,coords:t,current:!0},i.appendChild(s),this.fire("tileloadstart",{tile:s,coords:t})},_tileReady:function(t,i,n){i&&this.fire("tileerror",{error:i,tile:n,coords:t});var o=this._tileCoordsToKey(t);(n=this._tiles[o])&&(n.loaded=+new Date,this._map._fadeAnimated?(nt(n.el,0),g(this._fadeFrame),this._fadeFrame=f(this._updateOpacity,this)):(n.active=!0,this._pruneTiles()),i||(Q(n.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:n.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),Li||!this._map._fadeAnimated?f(this._pruneTiles,this):setTimeout(e(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var i=new x(this._wrapX?s(t.x,this._wrapX):t.x,this._wrapY?s(t.y,this._wrapY):t.y);return i.z=t.z,i},_pxBoundsToTileRange:function(t){var i=this.getTileSize();return new P(t.min.unscaleBy(i).floor(),t.max.unscaleBy(i).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}}),mn=pn.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1},initialize:function(t,i){this._url=t,(i=l(this,i)).detectRetina&&Yi&&i.maxZoom>0&&(i.tileSize=Math.floor(i.tileSize/2),i.zoomReverse?(i.zoomOffset--,i.minZoom++):(i.zoomOffset++,i.maxZoom--),i.minZoom=Math.max(0,i.minZoom)),"string"==typeof i.subdomains&&(i.subdomains=i.subdomains.split("")),zi||this.on("tileunload",this._onTileRemove)},setUrl:function(t,i){return this._url=t,i||this.redraw(),this},createTile:function(t,i){var n=document.createElement("img");return mt(n,"load",e(this._tileOnLoad,this,i,n)),mt(n,"error",e(this._tileOnError,this,i,n)),(this.options.crossOrigin||""===this.options.crossOrigin)&&(n.crossOrigin=!0===this.options.crossOrigin?"":this.options.crossOrigin),n.alt="",n.setAttribute("role","presentation"),n.src=this.getTileUrl(t),n},getTileUrl:function(t){var e={r:Yi?"@2x":"",s:this._getSubdomain(t),x:t.x,y:t.y,z:this._getZoomForUrl()};if(this._map&&!this._map.options.crs.infinite){var n=this._globalTileRange.max.y-t.y;this.options.tms&&(e.y=n),e["-y"]=n}return _(this._url,i(e,this.options))},_tileOnLoad:function(t,i){Li?setTimeout(e(t,this,null,i),0):t(null,i)},_tileOnError:function(t,i,e){var n=this.options.errorTileUrl;n&&i.getAttribute("src")!==n&&(i.src=n),t(e,i)},_onTileRemove:function(t){t.tile.onload=null},_getZoomForUrl:function(){var t=this._tileZoom,i=this.options.maxZoom,e=this.options.zoomReverse,n=this.options.zoomOffset;return e&&(t=i-t),t+n},_getSubdomain:function(t){var i=Math.abs(t.x+t.y)%this.options.subdomains.length;return this.options.subdomains[i]},_abortLoading:function(){var t,i;for(t in this._tiles)this._tiles[t].coords.z!==this._tileZoom&&((i=this._tiles[t].el).onload=r,i.onerror=r,i.complete||(i.src=si,K(i),delete this._tiles[t]))},_removeTile:function(t){var i=this._tiles[t];if(i)return Si||i.el.setAttribute("src",si),pn.prototype._removeTile.call(this,t)},_tileReady:function(t,i,e){if(this._map&&(!e||e.getAttribute("src")!==si))return pn.prototype._tileReady.call(this,t,i,e)}}),fn=mn.extend({defaultWmsParams:{service:"WMS",request:"GetMap",layers:"",styles:"",format:"image/jpeg",transparent:!1,version:"1.1.1"},options:{crs:null,uppercase:!1},initialize:function(t,e){this._url=t;var n=i({},this.defaultWmsParams);for(var o in e)o in this.options||(n[o]=e[o]);var s=(e=l(this,e)).detectRetina&&Yi?2:1,r=this.getTileSize();n.width=r.x*s,n.height=r.y*s,this.wmsParams=n},onAdd:function(t){this._crs=this.options.crs||t.options.crs,this._wmsVersion=parseFloat(this.wmsParams.version);var i=this._wmsVersion>=1.3?"crs":"srs";this.wmsParams[i]=this._crs.code,mn.prototype.onAdd.call(this,t)},getTileUrl:function(t){var i=this._tileCoordsToNwSe(t),e=this._crs,n=b(e.project(i[0]),e.project(i[1])),o=n.min,s=n.max,r=(this._wmsVersion>=1.3&&this._crs===Ue?[o.y,o.x,s.y,s.x]:[o.x,o.y,s.x,s.y]).join(","),a=mn.prototype.getTileUrl.call(this,t);return a+c(this.wmsParams,a,this.options.uppercase)+(this.options.uppercase?"&BBOX=":"&bbox=")+r},setParams:function(t,e){return i(this.wmsParams,t),e||this.redraw(),this}});mn.WMS=fn,Jt.wms=function(t,i){return new fn(t,i)};var gn=qe.extend({options:{padding:.1,tolerance:0},initialize:function(t){l(this,t),n(this),this._layers=this._layers||{}},onAdd:function(){this._container||(this._initContainer(),this._zoomAnimated&&Q(this._container,"leaflet-zoom-animated")),this.getPane().appendChild(this._container),this._update(),this.on("update",this._updatePaths,this)},onRemove:function(){this.off("update",this._updatePaths,this),this._destroyContainer()},getEvents:function(){var t={viewreset:this._reset,zoom:this._onZoom,moveend:this._update,zoomend:this._onZoomEnd};return this._zoomAnimated&&(t.zoomanim=this._onAnimZoom),t},_onAnimZoom:function(t){this._updateTransform(t.center,t.zoom)},_onZoom:function(){this._updateTransform(this._map.getCenter(),this._map.getZoom())},_updateTransform:function(t,i){var e=this._map.getZoomScale(i,this._zoom),n=ht(this._container),o=this._map.getSize().multiplyBy(.5+this.options.padding),s=this._map.project(this._center,i),r=this._map.project(t,i).subtract(s),a=o.multiplyBy(-e).add(n).add(o).subtract(r);ji?rt(this._container,a,e):at(this._container,a)},_reset:function(){this._update(),this._updateTransform(this._center,this._zoom);for(var t in this._layers)this._layers[t]._reset()},_onZoomEnd:function(){for(var t in this._layers)this._layers[t]._project()},_updatePaths:function(){for(var t in this._layers)this._layers[t]._update()},_update:function(){var t=this.options.padding,i=this._map.getSize(),e=this._map.containerPointToLayerPoint(i.multiplyBy(-t)).round();this._bounds=new P(e,e.add(i.multiplyBy(1+2*t)).round()),this._center=this._map.getCenter(),this._zoom=this._map.getZoom()}}),vn=gn.extend({getEvents:function(){var t=gn.prototype.getEvents.call(this);return t.viewprereset=this._onViewPreReset,t},_onViewPreReset:function(){this._postponeUpdatePaths=!0},onAdd:function(){gn.prototype.onAdd.call(this),this._draw()},_initContainer:function(){var t=this._container=document.createElement("canvas");mt(t,"mousemove",o(this._onMouseMove,32,this),this),mt(t,"click dblclick mousedown mouseup contextmenu",this._onClick,this),mt(t,"mouseout",this._handleMouseOut,this),this._ctx=t.getContext("2d")},_destroyContainer:function(){g(this._redrawRequest),delete this._ctx,K(this._container),ft(this._container),delete this._container},_updatePaths:function(){if(!this._postponeUpdatePaths){this._redrawBounds=null;for(var t in this._layers)this._layers[t]._update();this._redraw()}},_update:function(){if(!this._map._animatingZoom||!this._bounds){this._drawnLayers={},gn.prototype._update.call(this);var t=this._bounds,i=this._container,e=t.getSize(),n=Yi?2:1;at(i,t.min),i.width=n*e.x,i.height=n*e.y,i.style.width=e.x+"px",i.style.height=e.y+"px",Yi&&this._ctx.scale(2,2),this._ctx.translate(-t.min.x,-t.min.y),this.fire("update")}},_reset:function(){gn.prototype._reset.call(this),this._postponeUpdatePaths&&(this._postponeUpdatePaths=!1,this._updatePaths())},_initPath:function(t){this._updateDashArray(t),this._layers[n(t)]=t;var i=t._order={layer:t,prev:this._drawLast,next:null};this._drawLast&&(this._drawLast.next=i),this._drawLast=i,this._drawFirst=this._drawFirst||this._drawLast},_addPath:function(t){this._requestRedraw(t)},_removePath:function(t){var i=t._order,e=i.next,o=i.prev;e?e.prev=o:this._drawLast=o,o?o.next=e:this._drawFirst=e,delete this._drawnLayers[t._leaflet_id],delete t._order,delete this._layers[n(t)],this._requestRedraw(t)},_updatePath:function(t){this._extendRedrawBounds(t),t._project(),t._update(),this._requestRedraw(t)},_updateStyle:function(t){this._updateDashArray(t),this._requestRedraw(t)},_updateDashArray:function(t){if("string"==typeof t.options.dashArray){var i,e=t.options.dashArray.split(/[, ]+/),n=[];for(i=0;i')}}catch(t){return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}}(),xn={_initContainer:function(){this._container=G("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(gn.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var i=t._container=yn("shape");Q(i,"leaflet-vml-shape "+(this.options.className||"")),i.coordsize="1 1",t._path=yn("path"),i.appendChild(t._path),this._updateStyle(t),this._layers[n(t)]=t},_addPath:function(t){var i=t._container;this._container.appendChild(i),t.options.interactive&&t.addInteractiveTarget(i)},_removePath:function(t){var i=t._container;K(i),t.removeInteractiveTarget(i),delete this._layers[n(t)]},_updateStyle:function(t){var i=t._stroke,e=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(i||(i=t._stroke=yn("stroke")),o.appendChild(i),i.weight=n.weight+"px",i.color=n.color,i.opacity=n.opacity,n.dashArray?i.dashStyle=oi(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):i.dashStyle="",i.endcap=n.lineCap.replace("butt","flat"),i.joinstyle=n.lineJoin):i&&(o.removeChild(i),t._stroke=null),n.fill?(e||(e=t._fill=yn("fill")),o.appendChild(e),e.color=n.fillColor||n.color,e.opacity=n.fillOpacity):e&&(o.removeChild(e),t._fill=null)},_updateCircle:function(t){var i=t._point.round(),e=Math.round(t._radius),n=Math.round(t._radiusY||e);this._setPath(t,t._empty()?"M0 0":"AL "+i.x+","+i.y+" "+e+","+n+" 0,23592600")},_setPath:function(t,i){t._path.v=i},_bringToFront:function(t){X(t._container)},_bringToBack:function(t){J(t._container)}},wn=$i?yn:E,Pn=gn.extend({getEvents:function(){var t=gn.prototype.getEvents.call(this);return t.zoomstart=this._onZoomStart,t},_initContainer:function(){this._container=wn("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=wn("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){K(this._container),ft(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_onZoomStart:function(){this._update()},_update:function(){if(!this._map._animatingZoom||!this._bounds){gn.prototype._update.call(this);var t=this._bounds,i=t.getSize(),e=this._container;this._svgSize&&this._svgSize.equals(i)||(this._svgSize=i,e.setAttribute("width",i.x),e.setAttribute("height",i.y)),at(e,t.min),e.setAttribute("viewBox",[t.min.x,t.min.y,i.x,i.y].join(" ")),this.fire("update")}},_initPath:function(t){var i=t._path=wn("path");t.options.className&&Q(i,t.options.className),t.options.interactive&&Q(i,"leaflet-interactive"),this._updateStyle(t),this._layers[n(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){K(t._path),t.removeInteractiveTarget(t._path),delete this._layers[n(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var i=t._path,e=t.options;i&&(e.stroke?(i.setAttribute("stroke",e.color),i.setAttribute("stroke-opacity",e.opacity),i.setAttribute("stroke-width",e.weight),i.setAttribute("stroke-linecap",e.lineCap),i.setAttribute("stroke-linejoin",e.lineJoin),e.dashArray?i.setAttribute("stroke-dasharray",e.dashArray):i.removeAttribute("stroke-dasharray"),e.dashOffset?i.setAttribute("stroke-dashoffset",e.dashOffset):i.removeAttribute("stroke-dashoffset")):i.setAttribute("stroke","none"),e.fill?(i.setAttribute("fill",e.fillColor||e.color),i.setAttribute("fill-opacity",e.fillOpacity),i.setAttribute("fill-rule",e.fillRule||"evenodd")):i.setAttribute("fill","none"))},_updatePoly:function(t,i){this._setPath(t,k(t._parts,i))},_updateCircle:function(t){var i=t._point,e=Math.max(Math.round(t._radius),1),n="a"+e+","+(Math.max(Math.round(t._radiusY),1)||e)+" 0 1,0 ",o=t._empty()?"M0 0":"M"+(i.x-e)+","+i.y+n+2*e+",0 "+n+2*-e+",0 ";this._setPath(t,o)},_setPath:function(t,i){t._path.setAttribute("d",i)},_bringToFront:function(t){X(t._path)},_bringToBack:function(t){J(t._path)}});$i&&Pn.include(xn),be.include({getRenderer:function(t){var i=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer;return i||(i=this._renderer=this._createRenderer()),this.hasLayer(i)||this.addLayer(i),i},_getPaneRenderer:function(t){if("overlayPane"===t||void 0===t)return!1;var i=this._paneRenderers[t];return void 0===i&&(i=this._createRenderer({pane:t}),this._paneRenderers[t]=i),i},_createRenderer:function(t){return this.options.preferCanvas&&$t(t)||Qt(t)}});var Ln=on.extend({initialize:function(t,i){on.prototype.initialize.call(this,this._boundsToLatLngs(t),i)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return t=z(t),[t.getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});Pn.create=wn,Pn.pointsToPath=k,sn.geometryToLayer=Ft,sn.coordsToLatLng=Ut,sn.coordsToLatLngs=Vt,sn.latLngToCoords=qt,sn.latLngsToCoords=Gt,sn.getFeature=Kt,sn.asFeature=Yt,be.mergeOptions({boxZoom:!0});var bn=Ee.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){mt(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){ft(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){K(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),fi(),ut(),this._startPoint=this._map.mouseEventToContainerPoint(t),mt(document,{contextmenu:Lt,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=G("div","leaflet-zoom-box",this._container),Q(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var i=new P(this._point,this._startPoint),e=i.getSize();at(this._box,i.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(K(this._box),tt(this._container,"leaflet-crosshair")),gi(),lt(),ft(document,{contextmenu:Lt,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){if((1===t.which||1===t.button)&&(this._finish(),this._moved)){this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(e(this._resetState,this),0);var i=new T(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point));this._map.fitBounds(i).fire("boxzoomend",{boxZoomBounds:i})}},_onKeyDown:function(t){27===t.keyCode&&this._finish()}});be.addInitHook("addHandler","boxZoom",bn),be.mergeOptions({doubleClickZoom:!0});var Tn=Ee.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var i=this._map,e=i.getZoom(),n=i.options.zoomDelta,o=t.originalEvent.shiftKey?e-n:e+n;"center"===i.options.doubleClickZoom?i.setZoom(o):i.setZoomAround(t.containerPoint,o)}});be.addInitHook("addHandler","doubleClickZoom",Tn),be.mergeOptions({dragging:!0,inertia:!Mi,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0});var zn=Ee.extend({addHooks:function(){if(!this._draggable){var t=this._map;this._draggable=new Re(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))}Q(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){tt(this._map._container,"leaflet-grab"),tt(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t=this._map;if(t._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity){var i=z(this._map.options.maxBounds);this._offsetLimit=b(this._map.latLngToContainerPoint(i.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(i.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))}else this._offsetLimit=null;t.fire("movestart").fire("dragstart"),t.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){if(this._map.options.inertia){var i=this._lastTime=+new Date,e=this._lastPos=this._draggable._absPos||this._draggable._newPos;this._positions.push(e),this._times.push(i),this._prunePositions(i)}this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;this._positions.length>1&&t-this._times[0]>50;)this._positions.shift(),this._times.shift()},_onZoomEnd:function(){var t=this._map.getSize().divideBy(2),i=this._map.latLngToLayerPoint([0,0]);this._initialWorldOffset=i.subtract(t).x,this._worldWidth=this._map.getPixelWorldBounds().getSize().x},_viscousLimit:function(t,i){return t-(t-i)*this._viscosity},_onPreDragLimit:function(){if(this._viscosity&&this._offsetLimit){var t=this._draggable._newPos.subtract(this._draggable._startPos),i=this._offsetLimit;t.xi.max.x&&(t.x=this._viscousLimit(t.x,i.max.x)),t.y>i.max.y&&(t.y=this._viscousLimit(t.y,i.max.y)),this._draggable._newPos=this._draggable._startPos.add(t)}},_onPreDragWrap:function(){var t=this._worldWidth,i=Math.round(t/2),e=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-i+e)%t+i-e,s=(n+i+e)%t-i-e,r=Math.abs(o+e)0?s:-s))-i;this._delta=0,this._startTime=null,r&&("center"===t.options.scrollWheelZoom?t.setZoom(i+r):t.setZoomAround(this._lastMousePos,i+r))}});be.addInitHook("addHandler","scrollWheelZoom",Cn),be.mergeOptions({tap:!0,tapTolerance:15});var Sn=Ee.extend({addHooks:function(){mt(this._map._container,"touchstart",this._onDown,this)},removeHooks:function(){ft(this._map._container,"touchstart",this._onDown,this)},_onDown:function(t){if(t.touches){if(Pt(t),this._fireClick=!0,t.touches.length>1)return this._fireClick=!1,void clearTimeout(this._holdTimeout);var i=t.touches[0],n=i.target;this._startPos=this._newPos=new x(i.clientX,i.clientY),n.tagName&&"a"===n.tagName.toLowerCase()&&Q(n,"leaflet-active"),this._holdTimeout=setTimeout(e(function(){this._isTapValid()&&(this._fireClick=!1,this._onUp(),this._simulateEvent("contextmenu",i))},this),1e3),this._simulateEvent("mousedown",i),mt(document,{touchmove:this._onMove,touchend:this._onUp},this)}},_onUp:function(t){if(clearTimeout(this._holdTimeout),ft(document,{touchmove:this._onMove,touchend:this._onUp},this),this._fireClick&&t&&t.changedTouches){var i=t.changedTouches[0],e=i.target;e&&e.tagName&&"a"===e.tagName.toLowerCase()&&tt(e,"leaflet-active"),this._simulateEvent("mouseup",i),this._isTapValid()&&this._simulateEvent("click",i)}},_isTapValid:function(){return this._newPos.distanceTo(this._startPos)<=this._map.options.tapTolerance},_onMove:function(t){var i=t.touches[0];this._newPos=new x(i.clientX,i.clientY),this._simulateEvent("mousemove",i)},_simulateEvent:function(t,i){var e=document.createEvent("MouseEvents");e._simulated=!0,i.target._simulatedClick=!0,e.initMouseEvent(t,!0,!0,window,1,i.screenX,i.screenY,i.clientX,i.clientY,!1,!1,!1,!1,0,null),i.target.dispatchEvent(e)}});qi&&!Vi&&be.addInitHook("addHandler","tap",Sn),be.mergeOptions({touchZoom:qi&&!Mi,bounceAtZoomLimits:!0});var Zn=Ee.extend({addHooks:function(){Q(this._map._container,"leaflet-touch-zoom"),mt(this._map._container,"touchstart",this._onTouchStart,this)},removeHooks:function(){tt(this._map._container,"leaflet-touch-zoom"),ft(this._map._container,"touchstart",this._onTouchStart,this)},_onTouchStart:function(t){var i=this._map;if(t.touches&&2===t.touches.length&&!i._animatingZoom&&!this._zooming){var e=i.mouseEventToContainerPoint(t.touches[0]),n=i.mouseEventToContainerPoint(t.touches[1]);this._centerPoint=i.getSize()._divideBy(2),this._startLatLng=i.containerPointToLatLng(this._centerPoint),"center"!==i.options.touchZoom&&(this._pinchStartLatLng=i.containerPointToLatLng(e.add(n)._divideBy(2))),this._startDist=e.distanceTo(n),this._startZoom=i.getZoom(),this._moved=!1,this._zooming=!0,i._stop(),mt(document,"touchmove",this._onTouchMove,this),mt(document,"touchend",this._onTouchEnd,this),Pt(t)}},_onTouchMove:function(t){if(t.touches&&2===t.touches.length&&this._zooming){var i=this._map,n=i.mouseEventToContainerPoint(t.touches[0]),o=i.mouseEventToContainerPoint(t.touches[1]),s=n.distanceTo(o)/this._startDist;if(this._zoom=i.getScaleZoom(s,this._startZoom),!i.options.bounceAtZoomLimits&&(this._zoomi.getMaxZoom()&&s>1)&&(this._zoom=i._limitZoom(this._zoom)),"center"===i.options.touchZoom){if(this._center=this._startLatLng,1===s)return}else{var r=n._add(o)._divideBy(2)._subtract(this._centerPoint);if(1===s&&0===r.x&&0===r.y)return;this._center=i.unproject(i.project(this._pinchStartLatLng,this._zoom).subtract(r),this._zoom)}this._moved||(i._moveStart(!0,!1),this._moved=!0),g(this._animRequest);var a=e(i._move,i,this._center,this._zoom,{pinch:!0,round:!1});this._animRequest=f(a,this,!0),Pt(t)}},_onTouchEnd:function(){this._moved&&this._zooming?(this._zooming=!1,g(this._animRequest),ft(document,"touchmove",this._onTouchMove),ft(document,"touchend",this._onTouchEnd),this._map.options.zoomAnimation?this._map._animateZoom(this._center,this._map._limitZoom(this._zoom),!0,this._map.options.zoomSnap):this._map._resetView(this._center,this._map._limitZoom(this._zoom))):this._zooming=!1}});be.addInitHook("addHandler","touchZoom",Zn),be.BoxZoom=bn,be.DoubleClickZoom=Tn,be.Drag=zn,be.Keyboard=Mn,be.ScrollWheelZoom=Cn,be.Tap=Sn,be.TouchZoom=Zn,Object.freeze=ti,t.version="1.3.4+HEAD.0e566b2",t.Control=Te,t.control=ze,t.Browser=Qi,t.Evented=ci,t.Mixin=Ae,t.Util=ui,t.Class=v,t.Handler=Ee,t.extend=i,t.bind=e,t.stamp=n,t.setOptions=l,t.DomEvent=Pe,t.DomUtil=ve,t.PosAnimation=Le,t.Draggable=Re,t.LineUtil=Ne,t.PolyUtil=De,t.Point=x,t.point=w,t.Bounds=P,t.bounds=b,t.Transformation=S,t.transformation=Z,t.Projection=He,t.LatLng=M,t.latLng=C,t.LatLngBounds=T,t.latLngBounds=z,t.CRS=di,t.GeoJSON=sn,t.geoJSON=Xt,t.geoJson=an,t.Layer=qe,t.LayerGroup=Ge,t.layerGroup=function(t,i){return new Ge(t,i)},t.FeatureGroup=Ke,t.featureGroup=function(t){return new Ke(t)},t.ImageOverlay=hn,t.imageOverlay=function(t,i,e){return new hn(t,i,e)},t.VideoOverlay=un,t.videoOverlay=function(t,i,e){return new un(t,i,e)},t.DivOverlay=ln,t.Popup=cn,t.popup=function(t,i){return new cn(t,i)},t.Tooltip=_n,t.tooltip=function(t,i){return new _n(t,i)},t.Icon=Ye,t.icon=function(t){return new Ye(t)},t.DivIcon=dn,t.divIcon=function(t){return new dn(t)},t.Marker=$e,t.marker=function(t,i){return new $e(t,i)},t.TileLayer=mn,t.tileLayer=Jt,t.GridLayer=pn,t.gridLayer=function(t){return new pn(t)},t.SVG=Pn,t.svg=Qt,t.Renderer=gn,t.Canvas=vn,t.canvas=$t,t.Path=Qe,t.CircleMarker=tn,t.circleMarker=function(t,i){return new tn(t,i)},t.Circle=en,t.circle=function(t,i,e){return new en(t,i,e)},t.Polyline=nn,t.polyline=function(t,i){return new nn(t,i)},t.Polygon=on,t.polygon=function(t,i){return new on(t,i)},t.Rectangle=Ln,t.rectangle=function(t,i){return new Ln(t,i)},t.Map=be,t.map=function(t,i){return new be(t,i)};var En=window.L;t.noConflict=function(){return window.L=En,this},window.L=t}); \ No newline at end of file diff --git a/corefx/src/main/resources/log4j2.xml b/corefx/src/main/resources/log4j2.xml deleted file mode 100644 index 0ff056d0..00000000 --- a/corefx/src/main/resources/log4j2.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - From 0b5c94778c12bb335ce7a801527e795d05f2a0ce Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 19 Dec 2019 09:30:21 -0800 Subject: [PATCH 421/462] moved coretk under daemon/core/gui --- daemon/.pre-commit-config.yaml | 21 - daemon/Pipfile | 4 +- daemon/Pipfile.lock | 577 ++++++++++-------- .../coretk => daemon/core/gui}/__init__.py | 0 {coretk/coretk => daemon/core/gui}/app.py | 20 +- .../coretk => daemon/core/gui}/appconfig.py | 2 +- .../coretk => daemon/core/gui}/coreclient.py | 18 +- .../core/gui}/data/backgrounds/sample1-bg.gif | Bin .../core/gui}/data/backgrounds/sample4-bg.jpg | Bin .../core/gui}/data/icons/OVS.gif | Bin .../core/gui}/data/icons/alert.png | Bin .../core/gui}/data/icons/antenna.gif | Bin .../core/gui}/data/icons/core-icon.png | Bin .../core/gui}/data/icons/docker.png | Bin .../core/gui}/data/icons/document-new.gif | Bin .../gui}/data/icons/document-properties.gif | Bin .../core/gui}/data/icons/document-save.gif | Bin .../core/gui}/data/icons/edit-delete.gif | Bin .../core/gui}/data/icons/edit-node.png | Bin .../core/gui}/data/icons/emane.png | Bin .../core/gui}/data/icons/fileopen.gif | Bin .../core/gui}/data/icons/host.png | Bin .../core/gui}/data/icons/hub.png | Bin .../core/gui}/data/icons/lanswitch.png | Bin .../core/gui}/data/icons/link.png | Bin .../core/gui}/data/icons/lxc.png | Bin .../core/gui}/data/icons/marker.png | Bin .../core/gui}/data/icons/markerclear.png | Bin .../core/gui}/data/icons/mdr.png | Bin .../core/gui}/data/icons/observe.gif | Bin .../core/gui}/data/icons/oval.png | Bin .../core/gui}/data/icons/pause.png | Bin .../core/gui}/data/icons/pc.png | Bin .../core/gui}/data/icons/plot.gif | Bin .../core/gui}/data/icons/prouter.png | Bin .../core/gui}/data/icons/rectangle.png | Bin .../core/gui}/data/icons/rj45.png | Bin .../core/gui}/data/icons/router.png | Bin .../core/gui}/data/icons/run.png | Bin .../core/gui}/data/icons/select.png | Bin .../core/gui}/data/icons/start.png | Bin .../core/gui}/data/icons/stop.png | Bin .../core/gui}/data/icons/text.png | Bin .../core/gui}/data/icons/tunnel.png | Bin .../core/gui}/data/icons/twonode.png | Bin .../core/gui}/data/icons/wlan.png | Bin .../core/gui}/data/mobility/sample1.scen | 0 .../core/gui}/data/oldicons/docker.gif | Bin .../core/gui}/data/oldicons/emane.gif | Bin .../core/gui}/data/oldicons/host.gif | Bin .../core/gui}/data/oldicons/hub.gif | Bin .../core/gui}/data/oldicons/lanswitch.gif | Bin .../core/gui}/data/oldicons/link.gif | Bin .../core/gui}/data/oldicons/lxc.gif | Bin .../core/gui}/data/oldicons/marker.gif | Bin .../core/gui}/data/oldicons/mdr.gif | Bin .../core/gui}/data/oldicons/oval.gif | Bin .../core/gui}/data/oldicons/pc.gif | Bin .../core/gui}/data/oldicons/rectangle.gif | Bin .../core/gui}/data/oldicons/rj45.gif | Bin .../core/gui}/data/oldicons/router.gif | Bin .../core/gui}/data/oldicons/router_green.gif | Bin .../core/gui}/data/oldicons/run.gif | Bin .../core/gui}/data/oldicons/select.gif | Bin .../core/gui}/data/oldicons/start.gif | Bin .../core/gui}/data/oldicons/stop.gif | Bin .../core/gui}/data/oldicons/text.gif | Bin .../core/gui}/data/oldicons/tunnel.gif | Bin .../core/gui}/data/oldicons/twonode.gif | Bin .../core/gui}/data/oldicons/wlan.gif | Bin .../core/gui}/data/xmls/sample1.xml | 0 .../core/gui}/dialogs/__init__.py | 0 .../core/gui}/dialogs/about.py | 4 +- .../core/gui}/dialogs/alerts.py | 6 +- .../core/gui}/dialogs/canvassizeandscale.py | 4 +- .../core/gui}/dialogs/canvaswallpaper.py | 10 +- .../core/gui}/dialogs/colorpicker.py | 2 +- .../core/gui}/dialogs/customnodes.py | 14 +- .../core/gui}/dialogs/dialog.py | 4 +- .../core/gui}/dialogs/emaneconfig.py | 10 +- .../core/gui}/dialogs/hooks.py | 6 +- .../core/gui}/dialogs/linkconfig.py | 6 +- .../core/gui}/dialogs/marker.py | 4 +- .../core/gui}/dialogs/mobilityconfig.py | 8 +- .../core/gui}/dialogs/mobilityplayer.py | 8 +- .../core/gui}/dialogs/nodeconfig.py | 16 +- .../core/gui}/dialogs/nodeservice.py | 8 +- .../core/gui}/dialogs/observers.py | 8 +- .../core/gui}/dialogs/preferences.py | 6 +- .../core/gui}/dialogs/servers.py | 8 +- .../core/gui}/dialogs/serviceconfiguration.py | 10 +- .../core/gui}/dialogs/sessionoptions.py | 8 +- .../core/gui}/dialogs/sessions.py | 8 +- .../core/gui}/dialogs/shapemod.py | 10 +- .../core/gui}/dialogs/wlanconfig.py | 8 +- {coretk/coretk => daemon/core/gui}/errors.py | 0 .../core/gui}/graph/__init__.py | 0 .../coretk => daemon/core/gui}/graph/edges.py | 8 +- .../coretk => daemon/core/gui}/graph/enums.py | 0 .../coretk => daemon/core/gui}/graph/graph.py | 22 +- .../core/gui}/graph/linkinfo.py | 0 .../coretk => daemon/core/gui}/graph/node.py | 20 +- .../coretk => daemon/core/gui}/graph/shape.py | 6 +- .../core/gui}/graph/shapeutils.py | 0 .../coretk => daemon/core/gui}/graph/tags.py | 0 .../core/gui}/graph/tooltip.py | 2 +- {coretk/coretk => daemon/core/gui}/images.py | 2 +- .../coretk => daemon/core/gui}/interface.py | 2 +- .../coretk => daemon/core/gui}/menuaction.py | 20 +- {coretk/coretk => daemon/core/gui}/menubar.py | 4 +- .../coretk => daemon/core/gui}/nodeutils.py | 2 +- .../coretk => daemon/core/gui}/statusbar.py | 4 +- {coretk/coretk => daemon/core/gui}/themes.py | 0 {coretk/coretk => daemon/core/gui}/toolbar.py | 18 +- {coretk/coretk => daemon/core/gui}/tooltip.py | 2 +- .../coretk => daemon/core/gui}/validation.py | 0 {coretk/coretk => daemon/core/gui}/widgets.py | 4 +- daemon/setup.py.in | 3 + 118 files changed, 505 insertions(+), 432 deletions(-) rename {coretk/coretk => daemon/core/gui}/__init__.py (100%) rename {coretk/coretk => daemon/core/gui}/app.py (88%) rename {coretk/coretk => daemon/core/gui}/appconfig.py (99%) rename {coretk/coretk => daemon/core/gui}/coreclient.py (98%) rename {coretk/coretk => daemon/core/gui}/data/backgrounds/sample1-bg.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/backgrounds/sample4-bg.jpg (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/OVS.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/alert.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/antenna.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/core-icon.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/docker.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/document-new.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/document-properties.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/document-save.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/edit-delete.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/edit-node.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/emane.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/fileopen.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/host.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/hub.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/lanswitch.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/link.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/lxc.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/marker.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/markerclear.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/mdr.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/observe.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/oval.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/pause.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/pc.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/plot.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/prouter.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/rectangle.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/rj45.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/router.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/run.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/select.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/start.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/stop.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/text.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/tunnel.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/twonode.png (100%) rename {coretk/coretk => daemon/core/gui}/data/icons/wlan.png (100%) rename {coretk/coretk => daemon/core/gui}/data/mobility/sample1.scen (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/docker.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/emane.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/host.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/hub.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/lanswitch.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/link.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/lxc.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/marker.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/mdr.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/oval.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/pc.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/rectangle.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/rj45.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/router.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/router_green.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/run.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/select.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/start.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/stop.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/text.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/tunnel.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/twonode.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/oldicons/wlan.gif (100%) rename {coretk/coretk => daemon/core/gui}/data/xmls/sample1.xml (100%) rename {coretk/coretk => daemon/core/gui}/dialogs/__init__.py (100%) rename {coretk/coretk => daemon/core/gui}/dialogs/about.py (95%) rename {coretk/coretk => daemon/core/gui}/dialogs/alerts.py (98%) rename {coretk/coretk => daemon/core/gui}/dialogs/canvassizeandscale.py (99%) rename {coretk/coretk => daemon/core/gui}/dialogs/canvaswallpaper.py (96%) rename {coretk/coretk => daemon/core/gui}/dialogs/colorpicker.py (99%) rename {coretk/coretk => daemon/core/gui}/dialogs/customnodes.py (96%) rename {coretk/coretk => daemon/core/gui}/dialogs/dialog.py (92%) rename {coretk/coretk => daemon/core/gui}/dialogs/emaneconfig.py (97%) rename {coretk/coretk => daemon/core/gui}/dialogs/hooks.py (97%) rename {coretk/coretk => daemon/core/gui}/dialogs/linkconfig.py (98%) rename {coretk/coretk => daemon/core/gui}/dialogs/marker.py (96%) rename {coretk/coretk => daemon/core/gui}/dialogs/mobilityconfig.py (90%) rename {coretk/coretk => daemon/core/gui}/dialogs/mobilityplayer.py (96%) rename {coretk/coretk => daemon/core/gui}/dialogs/nodeconfig.py (96%) rename {coretk/coretk => daemon/core/gui}/dialogs/nodeservice.py (95%) rename {coretk/coretk => daemon/core/gui}/dialogs/observers.py (96%) rename {coretk/coretk => daemon/core/gui}/dialogs/preferences.py (96%) rename {coretk/coretk => daemon/core/gui}/dialogs/servers.py (97%) rename {coretk/coretk => daemon/core/gui}/dialogs/serviceconfiguration.py (98%) rename {coretk/coretk => daemon/core/gui}/dialogs/sessionoptions.py (90%) rename {coretk/coretk => daemon/core/gui}/dialogs/sessions.py (97%) rename {coretk/coretk => daemon/core/gui}/dialogs/shapemod.py (97%) rename {coretk/coretk => daemon/core/gui}/dialogs/wlanconfig.py (91%) rename {coretk/coretk => daemon/core/gui}/errors.py (100%) rename {coretk/coretk => daemon/core/gui}/graph/__init__.py (100%) rename {coretk/coretk => daemon/core/gui}/graph/edges.py (97%) rename {coretk/coretk => daemon/core/gui}/graph/enums.py (100%) rename {coretk/coretk => daemon/core/gui}/graph/graph.py (98%) rename {coretk/coretk => daemon/core/gui}/graph/linkinfo.py (100%) rename {coretk/coretk => daemon/core/gui}/graph/node.py (95%) rename {coretk/coretk => daemon/core/gui}/graph/shape.py (97%) rename {coretk/coretk => daemon/core/gui}/graph/shapeutils.py (100%) rename {coretk/coretk => daemon/core/gui}/graph/tags.py (100%) rename {coretk/coretk => daemon/core/gui}/graph/tooltip.py (98%) rename {coretk/coretk => daemon/core/gui}/images.py (97%) rename {coretk/coretk => daemon/core/gui}/interface.py (98%) rename {coretk/coretk => daemon/core/gui}/menuaction.py (89%) rename {coretk/coretk => daemon/core/gui}/menubar.py (99%) rename {coretk/coretk => daemon/core/gui}/nodeutils.py (98%) rename {coretk/coretk => daemon/core/gui}/statusbar.py (96%) rename {coretk/coretk => daemon/core/gui}/themes.py (100%) rename {coretk/coretk => daemon/core/gui}/toolbar.py (97%) rename {coretk/coretk => daemon/core/gui}/tooltip.py (97%) rename {coretk/coretk => daemon/core/gui}/validation.py (100%) rename {coretk/coretk => daemon/core/gui}/widgets.py (99%) diff --git a/daemon/.pre-commit-config.yaml b/daemon/.pre-commit-config.yaml index ac6bc80b..73566c9d 100644 --- a/daemon/.pre-commit-config.yaml +++ b/daemon/.pre-commit-config.yaml @@ -21,24 +21,3 @@ repos: language: system entry: bash -c 'cd daemon && pipenv run flake8' types: [python] - - - id: isort-tk - name: coretk-isort - stages: [commit] - language: system - entry: bash -c 'cd coretk && pipenv run isort --atomic -y' - types: [python] - - - id: black-tk - name: coretk-black - stages: [commit] - language: system - entry: bash -c 'cd coretk && pipenv run black .' - types: [python] - - - id: flake8-tk - name: coretk-flake8 - stages: [commit] - language: system - entry: bash -c 'cd coretk && pipenv run flake8' - types: [python] diff --git a/daemon/Pipfile b/daemon/Pipfile index a689f4dc..8066415c 100644 --- a/daemon/Pipfile +++ b/daemon/Pipfile @@ -5,8 +5,10 @@ verify_ssl = true [scripts] core = "python scripts/core-daemon -f data/core.conf -l data/logging.conf" +coretk = "python core/gui/app.py" test = "pytest -v tests" -test_emane = "pytest -v tests/emane" +test-mock = "pytest -v --mock tests" +test-emane = "pytest -v tests/emane" [dev-packages] grpcio-tools = "*" diff --git a/daemon/Pipfile.lock b/daemon/Pipfile.lock index 5a19aae7..71c11fd2 100644 --- a/daemon/Pipfile.lock +++ b/daemon/Pipfile.lock @@ -39,38 +39,41 @@ }, "cffi": { "hashes": [ - "sha256:00d890313797d9fe4420506613384b43099ad7d2b905c0752dbcc3a6f14d80fa", - "sha256:0cf9e550ac6c5e57b713437e2f4ac2d7fd0cd10336525a27224f5fc1ec2ee59a", - "sha256:0ea23c9c0cdd6778146a50d867d6405693ac3b80a68829966c98dd5e1bbae400", - "sha256:193697c2918ecdb3865acf6557cddf5076bb39f1f654975e087b67efdff83365", - "sha256:1ae14b542bf3b35e5229439c35653d2ef7d8316c1fffb980f9b7647e544baa98", - "sha256:1e389e069450609c6ffa37f21f40cce36f9be7643bbe5051ab1de99d5a779526", - "sha256:263242b6ace7f9cd4ea401428d2d45066b49a700852334fd55311bde36dcda14", - "sha256:33142ae9807665fa6511cfa9857132b2c3ee6ddffb012b3f0933fc11e1e830d5", - "sha256:364f8404034ae1b232335d8c7f7b57deac566f148f7222cef78cf8ae28ef764e", - "sha256:47368f69fe6529f8f49a5d146ddee713fc9057e31d61e8b6dc86a6a5e38cecc1", - "sha256:4895640844f17bec32943995dc8c96989226974dfeb9dd121cc45d36e0d0c434", - "sha256:558b3afef987cf4b17abd849e7bedf64ee12b28175d564d05b628a0f9355599b", - "sha256:5ba86e1d80d458b338bda676fd9f9d68cb4e7a03819632969cf6d46b01a26730", - "sha256:63424daa6955e6b4c70dc2755897f5be1d719eabe71b2625948b222775ed5c43", - "sha256:6381a7d8b1ebd0bc27c3bc85bc1bfadbb6e6f756b4d4db0aa1425c3719ba26b4", - "sha256:6381ab708158c4e1639da1f2a7679a9bbe3e5a776fc6d1fd808076f0e3145331", - "sha256:6fd58366747debfa5e6163ada468a90788411f10c92597d3b0a912d07e580c36", - "sha256:728ec653964655d65408949b07f9b2219df78badd601d6c49e28d604efe40599", - "sha256:7cfcfda59ef1f95b9f729c56fe8a4041899f96b72685d36ef16a3440a0f85da8", - "sha256:819f8d5197c2684524637f940445c06e003c4a541f9983fd30d6deaa2a5487d8", - "sha256:825ecffd9574557590e3225560a8a9d751f6ffe4a49e3c40918c9969b93395fa", - "sha256:9009e917d8f5ef780c2626e29b6bc126f4cb2a4d43ca67aa2b40f2a5d6385e78", - "sha256:9c77564a51d4d914ed5af096cd9843d90c45b784b511723bd46a8a9d09cf16fc", - "sha256:a19089fa74ed19c4fe96502a291cfdb89223a9705b1d73b3005df4256976142e", - "sha256:a40ed527bffa2b7ebe07acc5a3f782da072e262ca994b4f2085100b5a444bbb2", - "sha256:bb75ba21d5716abc41af16eac1145ab2e471deedde1f22c6f99bd9f995504df0", - "sha256:e22a00c0c81ffcecaf07c2bfb3672fa372c50e2bd1024ffee0da191c1b27fc71", - "sha256:e55b5a746fb77f10c83e8af081979351722f6ea48facea79d470b3731c7b2891", - "sha256:ec2fa3ee81707a5232bf2dfbd6623fdb278e070d596effc7e2d788f2ada71a05", - "sha256:fd82eb4694be712fcae03c717ca2e0fc720657ac226b80bbb597e971fc6928c2" + "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.1" + "version": "==1.13.2" }, "core": { "editable": true, @@ -111,40 +114,51 @@ }, "grpcio": { "hashes": [ - "sha256:0302331e014fc4bac028b6ad480b33f7abfe20b9bdcca7be417124dda8f22115", - "sha256:0aa0cce9c5eb1261b32173a20ed42b51308d55ce28ecc2021e868b3cb90d9503", - "sha256:0c83947575300499adbc308e986d754e7f629be0bdd9bea1ffdd5cf76e1f1eff", - "sha256:0ca26ff968d45efd4ef73447c4d4b34322ea8c7d06fbb6907ce9e5db78f1bbcb", - "sha256:0cf80a7955760c2498f8821880242bb657d70998065ff0d2a082de5ffce230a7", - "sha256:0d40706e57d9833fe0e023a08b468f33940e8909affa12547874216d36bba208", - "sha256:11872069156de34c6f3f9a1deb46cc88bc35dfde88262c4c73eb22b39b16fc55", - "sha256:16065227faae0ab0abf1789bfb92a2cd2ab5da87630663f93f8178026da40e0d", - "sha256:1e33778277685f6fabb22539136269c87c029e39b6321ef1a639b756a1c0a408", - "sha256:2b16be15b1ae656bc7a36642b8c7045be2dde2048bb4b67478003e9d9db8022a", - "sha256:3701dfca3ada27ceef0d17f728ce9dfef155ed20c57979c2b05083082258c6c1", - "sha256:41912ecaf482abf2de74c69f509878f99223f5dd6b2de1a09c955afd4de3cf9b", - "sha256:4332cbd20544fe7406910137590f38b5b3a1f6170258e038652cf478c639430f", - "sha256:44068ecbdc6467c2bff4d8198816c8a2701b6dd1ec16078fceb6adc7c1f577d6", - "sha256:53115960e37059420e2d16a4b04b00dd2ab3b6c3c67babd01ffbfdcd7881a69b", - "sha256:6e7027bcd4070414751e2a5e60706facb98a1fc636497c9bac5442fe37b8ae6b", - "sha256:6ff57fb2f07b7226b5bec89e8e921ea9bd220f35f11e094f2ba38f09eecd49c6", - "sha256:73240e244d7644654bbda1f309f4911748b6a1804b7a8897ddbe8a04c90f7407", - "sha256:785234bbc469bc75e26c868789a2080ffb30bd6e93930167797729889ad06b0b", - "sha256:82f9d3c7f91d2d1885631335c003c5d45ae1cd69cc0bc4893f21fef50b8151bc", - "sha256:86bdc2a965510658407a1372eb61f0c92f763fdfb2795e4d038944da4320c950", - "sha256:95e925b56676a55e6282b3de80a1cbad5774072159779c61eac02791dface049", - "sha256:96673bb4f14bd3263613526d1e7e33fdb38a9130e3ce87bf52314965706e1900", - "sha256:970014205e76920484679035b6fb4b16e02fc977e5aac4d22025da849c79dab9", - "sha256:ace5e8bf11a1571f855f5dab38a9bd34109b6c9bc2864abf24a597598c7e3695", - "sha256:ad375f03eb3b9cb75a24d91eab8609e134d34605f199efc41e20dd642bdac855", - "sha256:b819c4c7dcf0de76788ce5f95daad6d4e753d6da2b6a5f84e5bb5b5ce95fddc4", - "sha256:c17943fd340cbd906db49f3f03c7545e5a66b617e8348b2c7a0d2c759d216af1", - "sha256:d21247150dea86dabd3b628d8bc4b563036db3d332b3f4db3c5b1b0b122cb4f6", - "sha256:d4d500a7221116de9767229ff5dd10db91f789448d85befb0adf5a37b0cd83b5", - "sha256:e2a942a3cfccbbca21a90c144867112698ef36486345c285da9e98c466f22b22", - "sha256:e983273dca91cb8a5043bc88322eb48e2b8d4e4998ff441a1ee79ced89db3909" + "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.24.1" + "version": "==1.26.0" }, "invoke": { "hashes": [ @@ -156,58 +170,104 @@ }, "lxml": { "hashes": [ - "sha256:02ca7bf899da57084041bb0f6095333e4d239948ad3169443f454add9f4e9cb4", - "sha256:096b82c5e0ea27ce9138bcbb205313343ee66a6e132f25c5ed67e2c8d960a1bc", - "sha256:0a920ff98cf1aac310470c644bc23b326402d3ef667ddafecb024e1713d485f1", - "sha256:17cae1730a782858a6e2758fd20dd0ef7567916c47757b694a06ffafdec20046", - "sha256:17e3950add54c882e032527795c625929613adbd2ce5162b94667334458b5a36", - "sha256:1f4f214337f6ee5825bf90a65d04d70aab05526c08191ab888cb5149501923c5", - "sha256:2e8f77db25b0a96af679e64ff9bf9dddb27d379c9900c3272f3041c4d1327c9d", - "sha256:4dffd405390a45ecb95ab5ab1c1b847553c18b0ef8ed01e10c1c8b1a76452916", - "sha256:6b899931a5648862c7b88c795eddff7588fb585e81cecce20f8d9da16eff96e0", - "sha256:726c17f3e0d7a7200718c9a890ccfeab391c9133e363a577a44717c85c71db27", - "sha256:760c12276fee05c36f95f8040180abc7fbebb9e5011447a97cdc289b5d6ab6fc", - "sha256:796685d3969815a633827c818863ee199440696b0961e200b011d79b9394bbe7", - "sha256:891fe897b49abb7db470c55664b198b1095e4943b9f82b7dcab317a19116cd38", - "sha256:a471628e20f03dcdfde00770eeaf9c77811f0c331c8805219ca7b87ac17576c5", - "sha256:a63b4fd3e2cabdcc9d918ed280bdde3e8e9641e04f3c59a2a3109644a07b9832", - "sha256:b0b84408d4eabc6de9dd1e1e0bc63e7731e890c0b378a62443e5741cfd0ae90a", - "sha256:be78485e5d5f3684e875dab60f40cddace2f5b2a8f7fede412358ab3214c3a6f", - "sha256:c27eaed872185f047bb7f7da2d21a7d8913457678c9a100a50db6da890bc28b9", - "sha256:c81cb40bff373ab7a7446d6bbca0190bccc5be3448b47b51d729e37799bb5692", - "sha256:d11874b3c33ee441059464711cd365b89fa1a9cf19ae75b0c189b01fbf735b84", - "sha256:e9c028b5897901361d81a4718d1db217b716424a0283afe9d6735fe0caf70f79", - "sha256:fe489d486cd00b739be826e8c1be188ddb74c7a1ca784d93d06fda882a6a1681" + "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.1" + "version": "==4.4.2" + }, + "netaddr": { + "hashes": [ + "sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd", + "sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca" + ], + "version": "==0.7.19" }, "paramiko": { "hashes": [ - "sha256:99f0179bdc176281d21961a003ffdb2ec369daac1a1007241f53374e376576cf", - "sha256:f4b2edfa0d226b70bd4ca31ea7e389325990283da23465d572ed1f70a7583041" + "sha256:920492895db8013f6cc0179293147f830b8c7b21fdfc839b6bad760c27459d9f", + "sha256:9c980875fa4d2cb751604664e9a2d0f69096643f5be4db1b99599fe114a97b2f" ], - "version": "==2.6.0" + "version": "==2.7.1" + }, + "pillow": { + "hashes": [ + "sha256:047d9473cf68af50ac85f8ee5d5f21a60f849bc17d348da7fc85711287a75031", + "sha256:0f66dc6c8a3cc319561a633b6aa82c44107f12594643efa37210d8c924fc1c71", + "sha256:12c9169c4e8fe0a7329e8658c7e488001f6b4c8e88740e76292c2b857af2e94c", + "sha256:248cffc168896982f125f5c13e9317c059f74fffdb4152893339f3be62a01340", + "sha256:27faf0552bf8c260a5cee21a76e031acaea68babb64daf7e8f2e2540745082aa", + "sha256:285edafad9bc60d96978ed24d77cdc0b91dace88e5da8c548ba5937c425bca8b", + "sha256:384b12c9aa8ef95558abdcb50aada56d74bc7cc131dd62d28c2d0e4d3aadd573", + "sha256:38950b3a707f6cef09cd3cbb142474357ad1a985ceb44d921bdf7b4647b3e13e", + "sha256:4aad1b88933fd6dc2846552b89ad0c74ddbba2f0884e2c162aa368374bf5abab", + "sha256:4ac6148008c169603070c092e81f88738f1a0c511e07bd2bb0f9ef542d375da9", + "sha256:4deb1d2a45861ae6f0b12ea0a786a03d19d29edcc7e05775b85ec2877cb54c5e", + "sha256:59aa2c124df72cc75ed72c8d6005c442d4685691a30c55321e00ed915ad1a291", + "sha256:5a47d2123a9ec86660fe0e8d0ebf0aa6bc6a17edc63f338b73ea20ba11713f12", + "sha256:5cc901c2ab9409b4b7ac7b5bcc3e86ac14548627062463da0af3b6b7c555a871", + "sha256:6c1db03e8dff7b9f955a0fb9907eb9ca5da75b5ce056c0c93d33100a35050281", + "sha256:7ce80c0a65a6ea90ef9c1f63c8593fcd2929448613fc8da0adf3e6bfad669d08", + "sha256:809c19241c14433c5d6135e1b6c72da4e3b56d5c865ad5736ab99af8896b8f41", + "sha256:83792cb4e0b5af480588601467c0764242b9a483caea71ef12d22a0d0d6bdce2", + "sha256:846fa202bd7ee0f6215c897a1d33238ef071b50766339186687bd9b7a6d26ac5", + "sha256:9f5529fc02009f96ba95bea48870173426879dc19eec49ca8e08cd63ecd82ddb", + "sha256:a423c2ea001c6265ed28700df056f75e26215fd28c001e93ef4380b0f05f9547", + "sha256:ac4428094b42907aba5879c7c000d01c8278d451a3b7cccd2103e21f6397ea75", + "sha256:b1ae48d87f10d1384e5beecd169c77502fcc04a2c00a4c02b85f0a94b419e5f9", + "sha256:bf4e972a88f8841d8fdc6db1a75e0f8d763e66e3754b03006cbc3854d89f1cb1", + "sha256:c6414f6aad598364aaf81068cabb077894eb88fed99c6a65e6e8217bab62ae7a", + "sha256:c710fcb7ee32f67baf25aa9ffede4795fd5d93b163ce95fdc724383e38c9df96", + "sha256:c7be4b8a09852291c3c48d3c25d1b876d2494a0a674980089ac9d5e0d78bd132", + "sha256:c9e5ffb910b14f090ac9c38599063e354887a5f6d7e6d26795e916b4514f2c1a", + "sha256:e0697b826da6c2472bb6488db4c0a7fa8af0d52fa08833ceb3681358914b14e5", + "sha256:e9a3edd5f714229d41057d56ac0f39ad9bdba6767e8c888c951869f0bdd129b0" + ], + "version": "==6.2.1" }, "protobuf": { "hashes": [ - "sha256:125713564d8cfed7610e52444c9769b8dcb0b55e25cc7841f2290ee7bc86636f", - "sha256:1accdb7a47e51503be64d9a57543964ba674edac103215576399d2d0e34eac77", - "sha256:27003d12d4f68e3cbea9eb67427cab3bfddd47ff90670cb367fcd7a3a89b9657", - "sha256:3264f3c431a631b0b31e9db2ae8c927b79fc1a7b1b06b31e8e5bcf2af91fe896", - "sha256:3c5ab0f5c71ca5af27143e60613729e3488bb45f6d3f143dc918a20af8bab0bf", - "sha256:45dcf8758873e3f69feab075e5f3177270739f146255225474ee0b90429adef6", - "sha256:56a77d61a91186cc5676d8e11b36a5feb513873e4ae88d2ee5cf530d52bbcd3b", - "sha256:5984e4947bbcef5bd849d6244aec507d31786f2dd3344139adc1489fb403b300", - "sha256:6b0441da73796dd00821763bb4119674eaf252776beb50ae3883bed179a60b2a", - "sha256:6f6677c5ade94d4fe75a912926d6796d5c71a2a90c2aeefe0d6f211d75c74789", - "sha256:84a825a9418d7196e2acc48f8746cf1ee75877ed2f30433ab92a133f3eaf8fbe", - "sha256:b842c34fe043ccf78b4a6cf1019d7b80113707d68c88842d061fa2b8fb6ddedc", - "sha256:ca33d2f09dae149a1dcf942d2d825ebb06343b77b437198c9e2ef115cf5d5bc1", - "sha256:db83b5c12c0cd30150bb568e6feb2435c49ce4e68fe2d7b903113f0e221e58fe", - "sha256:f50f3b1c5c1c1334ca7ce9cad5992f098f460ffd6388a3cabad10b66c2006b09", - "sha256:f99f127909731cafb841c52f9216e447d3e4afb99b17bebfad327a75aee206de" + "sha256:0265379852b9e1f76af6d3d3fe4b3c383a595cc937594bda8565cf69a96baabd", + "sha256:29bd1ed46b2536ad8959401a2f02d2d7b5a309f8e97518e4f92ca6c5ba74dbed", + "sha256:3175d45698edb9a07c1a78a1a4850e674ce8988f20596580158b1d0921d0f057", + "sha256:34a7270940f86da7a28be466ac541c89b6dbf144a6348b9cf7ac6f56b71006ce", + "sha256:38cbc830a4a5ba9956763b0f37090bfd14dd74e72762be6225de2ceac55f4d03", + "sha256:665194f5ad386511ac8d8a0bd57b9ab37b8dd2cd71969458777318e774b9cd46", + "sha256:839bad7d115c77cdff29b488fae6a3ab503ce9a4192bd4c42302a6ea8e5d0f33", + "sha256:934a9869a7f3b0d84eca460e386fba1f7ba2a0c1a120a2648bc41fadf50efd1c", + "sha256:aecdf12ef6dc7fd91713a6da93a86c2f2a8fe54840a3b1670853a2b7402e77c9", + "sha256:c4e90bc27c0691c76e09b5dc506133451e52caee1472b8b3c741b7c912ce43ef", + "sha256:c65d135ea2d85d40309e268106dab02d3bea723db2db21c23ecad4163ced210b", + "sha256:c98dea04a1ff41a70aff2489610f280004831798cb36a068013eed04c698903d", + "sha256:d9049aa194378a426f0b2c784e2054565bf6f754d20fcafdee7102a6250556e8", + "sha256:e028fee51c96de4e81924484c77111dfdea14010ecfc906ea5b252209b0c4de6", + "sha256:e84ad26fb50091b1ea676403c0dd2bd47663099454aa6d88000b1dafecab0941", + "sha256:e88a924b591b06d0191620e9c8aa75297b3111066bb09d49a24bae1054a10c13" ], - "version": "==3.10.0" + "version": "==3.11.1" }, "pycparser": { "hashes": [ @@ -241,12 +301,28 @@ ], "version": "==1.3.0" }, + "pyyaml": { + "hashes": [ + "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", + "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", + "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", + "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", + "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", + "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", + "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", + "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", + "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", + "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", + "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4" + ], + "version": "==5.2" + }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" ], - "version": "==1.12.0" + "version": "==1.13.0" } }, "develop": { @@ -264,13 +340,6 @@ ], "version": "==1.3.0" }, - "atomicwrites": { - "hashes": [ - "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", - "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" - ], - "version": "==1.3.0" - }, "attrs": { "hashes": [ "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", @@ -309,101 +378,123 @@ }, "flake8": { "hashes": [ - "sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", - "sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696" + "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", + "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" ], "index": "pypi", - "version": "==3.7.8" + "version": "==3.7.9" }, "grpcio": { "hashes": [ - "sha256:0302331e014fc4bac028b6ad480b33f7abfe20b9bdcca7be417124dda8f22115", - "sha256:0aa0cce9c5eb1261b32173a20ed42b51308d55ce28ecc2021e868b3cb90d9503", - "sha256:0c83947575300499adbc308e986d754e7f629be0bdd9bea1ffdd5cf76e1f1eff", - "sha256:0ca26ff968d45efd4ef73447c4d4b34322ea8c7d06fbb6907ce9e5db78f1bbcb", - "sha256:0cf80a7955760c2498f8821880242bb657d70998065ff0d2a082de5ffce230a7", - "sha256:0d40706e57d9833fe0e023a08b468f33940e8909affa12547874216d36bba208", - "sha256:11872069156de34c6f3f9a1deb46cc88bc35dfde88262c4c73eb22b39b16fc55", - "sha256:16065227faae0ab0abf1789bfb92a2cd2ab5da87630663f93f8178026da40e0d", - "sha256:1e33778277685f6fabb22539136269c87c029e39b6321ef1a639b756a1c0a408", - "sha256:2b16be15b1ae656bc7a36642b8c7045be2dde2048bb4b67478003e9d9db8022a", - "sha256:3701dfca3ada27ceef0d17f728ce9dfef155ed20c57979c2b05083082258c6c1", - "sha256:41912ecaf482abf2de74c69f509878f99223f5dd6b2de1a09c955afd4de3cf9b", - "sha256:4332cbd20544fe7406910137590f38b5b3a1f6170258e038652cf478c639430f", - "sha256:44068ecbdc6467c2bff4d8198816c8a2701b6dd1ec16078fceb6adc7c1f577d6", - "sha256:53115960e37059420e2d16a4b04b00dd2ab3b6c3c67babd01ffbfdcd7881a69b", - "sha256:6e7027bcd4070414751e2a5e60706facb98a1fc636497c9bac5442fe37b8ae6b", - "sha256:6ff57fb2f07b7226b5bec89e8e921ea9bd220f35f11e094f2ba38f09eecd49c6", - "sha256:73240e244d7644654bbda1f309f4911748b6a1804b7a8897ddbe8a04c90f7407", - "sha256:785234bbc469bc75e26c868789a2080ffb30bd6e93930167797729889ad06b0b", - "sha256:82f9d3c7f91d2d1885631335c003c5d45ae1cd69cc0bc4893f21fef50b8151bc", - "sha256:86bdc2a965510658407a1372eb61f0c92f763fdfb2795e4d038944da4320c950", - "sha256:95e925b56676a55e6282b3de80a1cbad5774072159779c61eac02791dface049", - "sha256:96673bb4f14bd3263613526d1e7e33fdb38a9130e3ce87bf52314965706e1900", - "sha256:970014205e76920484679035b6fb4b16e02fc977e5aac4d22025da849c79dab9", - "sha256:ace5e8bf11a1571f855f5dab38a9bd34109b6c9bc2864abf24a597598c7e3695", - "sha256:ad375f03eb3b9cb75a24d91eab8609e134d34605f199efc41e20dd642bdac855", - "sha256:b819c4c7dcf0de76788ce5f95daad6d4e753d6da2b6a5f84e5bb5b5ce95fddc4", - "sha256:c17943fd340cbd906db49f3f03c7545e5a66b617e8348b2c7a0d2c759d216af1", - "sha256:d21247150dea86dabd3b628d8bc4b563036db3d332b3f4db3c5b1b0b122cb4f6", - "sha256:d4d500a7221116de9767229ff5dd10db91f789448d85befb0adf5a37b0cd83b5", - "sha256:e2a942a3cfccbbca21a90c144867112698ef36486345c285da9e98c466f22b22", - "sha256:e983273dca91cb8a5043bc88322eb48e2b8d4e4998ff441a1ee79ced89db3909" + "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.24.1" + "version": "==1.26.0" }, "grpcio-tools": { "hashes": [ - "sha256:0a849994d7d6411ca6147bb1db042b61ba6232eb5c90c69de5380a441bf80a75", - "sha256:0db96ed52816471ceec8807aedf5cb4fd133ca201f614464cb46ca58584edf84", - "sha256:1b98720459204e9afa33928e4fd53aeec6598afb7f704ed497f6926c67f12b9b", - "sha256:200479310cc083c41a5020f6e5e916a99ee0f7c588b6affe317b96a839120bf4", - "sha256:25543b8f2e59ddcc9929d6f6111faa5c474b21580d2996f93347bb55f2ecba84", - "sha256:2d4609996616114c155c1e697a9faf604d81f2508cd9a4168a0bafd53c799e24", - "sha256:2fdb2a1ed2b3e43514d9c29c9de415c953a46caabbc8a9b7de1439a0c1bd3b89", - "sha256:3886a7983d8ae19df0c11a54114d6546fcdf76cf18cdccf25c3b14200fd5478a", - "sha256:408d111b9341f107bdafc523e2345471547ffe8a4104e6f2ce690b7a25c4bae5", - "sha256:60b3dd5e76c1389fc836bf83675985b92d158ff9a8d3d6d3f0a670f0c227ef13", - "sha256:629be7ce8504530b4adbf0425a44dd53007ccb6212344804294888c9662cc38f", - "sha256:6af3dde07b1051e954230e650a6ef74073cf993cf473c2078580f8a73c4fe46a", - "sha256:7a1e77539d28e90517c55561f40f7872f1348d0e23f25a38d68abbfb5b0eff88", - "sha256:87917a18b3b5951b6c9badd7b5ef09f63f61611966b58427b856bdf5c1d68e91", - "sha256:8823d0ebd185a77edb506e286c88d06847f75620a033ad96ef9c0fd7efc1d859", - "sha256:8bd3e12e1969beb813b861a2a65d4f2d4faaa87de0b60bf7f848da2d8ffc4eb2", - "sha256:8f37e9acc46e75ed9786ece89afeacd86182893eacc3f0642d81531b90fbe25f", - "sha256:9b358dd2f4142e89d760a52a7a8f4ec5dbaf955e7ada09f703f3a5d05dddd12e", - "sha256:9cb43007c4a8aa7adaacf896f5109b578028f23d259615e3fa5866e38855b311", - "sha256:9cf594bfbfbf84dcd462b20a4a753362be7ed376d2b5020a083dac24400b7b6c", - "sha256:ab79940e5c5ed949e1f95e7f417dd916b0992d29f45d073dd64501a76d128e2c", - "sha256:ba8aab6c78a82755477bb8c79f3be0824b297422d1edb21b94ae5a45407bf3ba", - "sha256:bcc00b83bf39f6e60a13f0b24ec3951f4d2ae810b01e6e125b7ff238a85da1ac", - "sha256:c1fcf5cbe6a2ecdc587b469156520b9128ccdb7c5908060c7d9712cd97e76db5", - "sha256:c6e640d39b9615388b59036b29970292b15f4519043e43833e28c674f740d1f7", - "sha256:c6ea2c385da620049b17f0135cf9307a4750e9d9c9988e15bfeeaf1f209c4ada", - "sha256:cec4f37120f93fe2ab4ab9a7eab9a877163d74c232c93a275a624971f8557b81", - "sha256:d2dbb42d237bcdecb7284535ec074c85bbf880124c1cbbff362ed3bd81ed7d41", - "sha256:d5c98a41abd4f7de43b256c21bbba2a97c57e25bf6a170927a90638b18f7509c", - "sha256:dcf5965a24179aa7dcfa00b5ff70f4f2f202e663657e0c74a642307beecda053", - "sha256:e11e3aacf0200d6e00a9b74534e0174738768fe1c41e5aa2f4aab881d6b43afd", - "sha256:e550816bdb2e49bba94bcd7f342004a8adbc46e9a25c8c4ed3fd58f2435c655f" + "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" ], "index": "pypi", - "version": "==1.24.1" + "version": "==1.26.0" }, "identify": { "hashes": [ - "sha256:4f1fe9a59df4e80fcb0213086fcf502bc1765a01ea4fe8be48da3b65afd2a017", - "sha256:d8919589bd2a5f99c66302fec0ef9027b12ae150b0b0213999ad3f695fc7296e" + "sha256:7782115794ec28b011702815d9f5e532244560cd2bf0789c4f09381d43befd90", + "sha256:9e7521e9abeaede4d2d1092a106e418c65ddf6b3182b43930bcb3c8cfb974488" ], - "version": "==1.4.7" + "version": "==1.4.8" }, "importlib-metadata": { "hashes": [ - "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", - "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" + "sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45", + "sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f" ], "markers": "python_version < '3.8'", - "version": "==0.23" + "version": "==1.3.0" }, "importlib-resources": { "hashes": [ @@ -438,10 +529,10 @@ }, "more-itertools": { "hashes": [ - "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", - "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" + "sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d", + "sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564" ], - "version": "==7.2.0" + "version": "==8.0.2" }, "nodeenv": { "hashes": [ @@ -458,39 +549,39 @@ }, "pluggy": { "hashes": [ - "sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", - "sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34" + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], - "version": "==0.13.0" + "version": "==0.13.1" }, "pre-commit": { "hashes": [ - "sha256:1d3c0587bda7c4e537a46c27f2c84aa006acc18facf9970bf947df596ce91f3f", - "sha256:fa78ff96e8e9ac94c748388597693f18b041a181c94a4f039ad20f45287ba44a" + "sha256:9f152687127ec90642a2cc3e4d9e1e6240c4eb153615cb02aa1ad41d331cbb6e", + "sha256:c2e4810d2d3102d354947907514a78c5d30424d299dc0fe48f5aa049826e9b50" ], "index": "pypi", - "version": "==1.18.3" + "version": "==1.20.0" }, "protobuf": { "hashes": [ - "sha256:125713564d8cfed7610e52444c9769b8dcb0b55e25cc7841f2290ee7bc86636f", - "sha256:1accdb7a47e51503be64d9a57543964ba674edac103215576399d2d0e34eac77", - "sha256:27003d12d4f68e3cbea9eb67427cab3bfddd47ff90670cb367fcd7a3a89b9657", - "sha256:3264f3c431a631b0b31e9db2ae8c927b79fc1a7b1b06b31e8e5bcf2af91fe896", - "sha256:3c5ab0f5c71ca5af27143e60613729e3488bb45f6d3f143dc918a20af8bab0bf", - "sha256:45dcf8758873e3f69feab075e5f3177270739f146255225474ee0b90429adef6", - "sha256:56a77d61a91186cc5676d8e11b36a5feb513873e4ae88d2ee5cf530d52bbcd3b", - "sha256:5984e4947bbcef5bd849d6244aec507d31786f2dd3344139adc1489fb403b300", - "sha256:6b0441da73796dd00821763bb4119674eaf252776beb50ae3883bed179a60b2a", - "sha256:6f6677c5ade94d4fe75a912926d6796d5c71a2a90c2aeefe0d6f211d75c74789", - "sha256:84a825a9418d7196e2acc48f8746cf1ee75877ed2f30433ab92a133f3eaf8fbe", - "sha256:b842c34fe043ccf78b4a6cf1019d7b80113707d68c88842d061fa2b8fb6ddedc", - "sha256:ca33d2f09dae149a1dcf942d2d825ebb06343b77b437198c9e2ef115cf5d5bc1", - "sha256:db83b5c12c0cd30150bb568e6feb2435c49ce4e68fe2d7b903113f0e221e58fe", - "sha256:f50f3b1c5c1c1334ca7ce9cad5992f098f460ffd6388a3cabad10b66c2006b09", - "sha256:f99f127909731cafb841c52f9216e447d3e4afb99b17bebfad327a75aee206de" + "sha256:0265379852b9e1f76af6d3d3fe4b3c383a595cc937594bda8565cf69a96baabd", + "sha256:29bd1ed46b2536ad8959401a2f02d2d7b5a309f8e97518e4f92ca6c5ba74dbed", + "sha256:3175d45698edb9a07c1a78a1a4850e674ce8988f20596580158b1d0921d0f057", + "sha256:34a7270940f86da7a28be466ac541c89b6dbf144a6348b9cf7ac6f56b71006ce", + "sha256:38cbc830a4a5ba9956763b0f37090bfd14dd74e72762be6225de2ceac55f4d03", + "sha256:665194f5ad386511ac8d8a0bd57b9ab37b8dd2cd71969458777318e774b9cd46", + "sha256:839bad7d115c77cdff29b488fae6a3ab503ce9a4192bd4c42302a6ea8e5d0f33", + "sha256:934a9869a7f3b0d84eca460e386fba1f7ba2a0c1a120a2648bc41fadf50efd1c", + "sha256:aecdf12ef6dc7fd91713a6da93a86c2f2a8fe54840a3b1670853a2b7402e77c9", + "sha256:c4e90bc27c0691c76e09b5dc506133451e52caee1472b8b3c741b7c912ce43ef", + "sha256:c65d135ea2d85d40309e268106dab02d3bea723db2db21c23ecad4163ced210b", + "sha256:c98dea04a1ff41a70aff2489610f280004831798cb36a068013eed04c698903d", + "sha256:d9049aa194378a426f0b2c784e2054565bf6f754d20fcafdee7102a6250556e8", + "sha256:e028fee51c96de4e81924484c77111dfdea14010ecfc906ea5b252209b0c4de6", + "sha256:e84ad26fb50091b1ea676403c0dd2bd47663099454aa6d88000b1dafecab0941", + "sha256:e88a924b591b06d0191620e9c8aa75297b3111066bb09d49a24bae1054a10c13" ], - "version": "==3.10.0" + "version": "==3.11.1" }, "py": { "hashes": [ @@ -515,43 +606,41 @@ }, "pyparsing": { "hashes": [ - "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", - "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4" + "sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f", + "sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a" ], - "version": "==2.4.2" + "version": "==2.4.5" }, "pytest": { "hashes": [ - "sha256:7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8", - "sha256:ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0" + "sha256:6b571215b5a790f9b41f19f3531c53a45cf6bb8ef2988bc1ff9afb38270b25fa", + "sha256:e41d489ff43948babd0fad7ad5e49b8735d5d55e26628a58673c39ff61d95de4" ], "index": "pypi", - "version": "==5.2.1" + "version": "==5.3.2" }, "pyyaml": { "hashes": [ - "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", - "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", - "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", - "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", - "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", - "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", - "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", - "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", - "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", - "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", - "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", - "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", - "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", + "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", + "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", + "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", + "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", + "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", + "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", + "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", + "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", + "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", + "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4" ], - "version": "==5.1.2" + "version": "==5.2" }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" ], - "version": "==1.12.0" + "version": "==1.13.0" }, "toml": { "hashes": [ @@ -562,10 +651,10 @@ }, "virtualenv": { "hashes": [ - "sha256:3e3597e89c73df9313f5566e8fc582bd7037938d15b05329c232ec57a11a7ad5", - "sha256:5d370508bf32e522d79096e8cbea3499d47e624ac7e11e9089f9397a0b3318df" + "sha256:0d62c70883c0342d59c11d0ddac0d954d0431321a41ab20851facf2b222598f3", + "sha256:55059a7a676e4e19498f1aad09b8313a38fcc0cdbe4fdddc0e9b06946d21b4bb" ], - "version": "==16.7.6" + "version": "==16.7.9" }, "wcwidth": { "hashes": [ diff --git a/coretk/coretk/__init__.py b/daemon/core/gui/__init__.py similarity index 100% rename from coretk/coretk/__init__.py rename to daemon/core/gui/__init__.py diff --git a/coretk/coretk/app.py b/daemon/core/gui/app.py similarity index 88% rename from coretk/coretk/app.py rename to daemon/core/gui/app.py index c8d4e597..cbc44930 100644 --- a/coretk/coretk/app.py +++ b/daemon/core/gui/app.py @@ -2,16 +2,16 @@ import logging import tkinter as tk from tkinter import ttk -from coretk import appconfig, themes -from coretk.coreclient import CoreClient -from coretk.graph.graph import CanvasGraph -from coretk.images import ImageEnum, Images -from coretk.menuaction import MenuAction -from coretk.menubar import Menubar -from coretk.nodeutils import NodeUtils -from coretk.statusbar import StatusBar -from coretk.toolbar import Toolbar -from coretk.validation import InputValidation +from core.gui import appconfig, themes +from core.gui.coreclient import CoreClient +from core.gui.graph.graph import CanvasGraph +from core.gui.images import ImageEnum, Images +from core.gui.menuaction import MenuAction +from core.gui.menubar import Menubar +from core.gui.nodeutils import NodeUtils +from core.gui.statusbar import StatusBar +from core.gui.toolbar import Toolbar +from core.gui.validation import InputValidation WIDTH = 1000 HEIGHT = 800 diff --git a/coretk/coretk/appconfig.py b/daemon/core/gui/appconfig.py similarity index 99% rename from coretk/coretk/appconfig.py rename to daemon/core/gui/appconfig.py index 33b08793..61ace7c8 100644 --- a/coretk/coretk/appconfig.py +++ b/daemon/core/gui/appconfig.py @@ -6,7 +6,7 @@ from pathlib import Path import yaml # gui home paths -from coretk import themes +from core.gui import themes HOME_PATH = Path.home().joinpath(".coretk") BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds") diff --git a/coretk/coretk/coreclient.py b/daemon/core/gui/coreclient.py similarity index 98% rename from coretk/coretk/coreclient.py rename to daemon/core/gui/coreclient.py index 8f2d479d..dee94dbf 100644 --- a/coretk/coretk/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -10,15 +10,15 @@ from pathlib import Path import grpc from core.api.grpc import client, core_pb2 -from coretk import appconfig -from coretk.dialogs.mobilityplayer import MobilityPlayer -from coretk.dialogs.sessions import SessionsDialog -from coretk.errors import show_grpc_error -from coretk.graph import tags -from coretk.graph.shape import AnnotationData, Shape -from coretk.graph.shapeutils import ShapeType -from coretk.interface import InterfaceManager -from coretk.nodeutils import NodeDraw, NodeUtils +from core.gui import appconfig +from core.gui.dialogs.mobilityplayer import MobilityPlayer +from core.gui.dialogs.sessions import SessionsDialog +from core.gui.errors import show_grpc_error +from core.gui.graph import tags +from core.gui.graph.shape import AnnotationData, Shape +from core.gui.graph.shapeutils import ShapeType +from core.gui.interface import InterfaceManager +from core.gui.nodeutils import NodeDraw, NodeUtils GUI_SOURCE = "gui" OBSERVERS = { diff --git a/coretk/coretk/data/backgrounds/sample1-bg.gif b/daemon/core/gui/data/backgrounds/sample1-bg.gif similarity index 100% rename from coretk/coretk/data/backgrounds/sample1-bg.gif rename to daemon/core/gui/data/backgrounds/sample1-bg.gif diff --git a/coretk/coretk/data/backgrounds/sample4-bg.jpg b/daemon/core/gui/data/backgrounds/sample4-bg.jpg similarity index 100% rename from coretk/coretk/data/backgrounds/sample4-bg.jpg rename to daemon/core/gui/data/backgrounds/sample4-bg.jpg diff --git a/coretk/coretk/data/icons/OVS.gif b/daemon/core/gui/data/icons/OVS.gif similarity index 100% rename from coretk/coretk/data/icons/OVS.gif rename to daemon/core/gui/data/icons/OVS.gif diff --git a/coretk/coretk/data/icons/alert.png b/daemon/core/gui/data/icons/alert.png similarity index 100% rename from coretk/coretk/data/icons/alert.png rename to daemon/core/gui/data/icons/alert.png diff --git a/coretk/coretk/data/icons/antenna.gif b/daemon/core/gui/data/icons/antenna.gif similarity index 100% rename from coretk/coretk/data/icons/antenna.gif rename to daemon/core/gui/data/icons/antenna.gif diff --git a/coretk/coretk/data/icons/core-icon.png b/daemon/core/gui/data/icons/core-icon.png similarity index 100% rename from coretk/coretk/data/icons/core-icon.png rename to daemon/core/gui/data/icons/core-icon.png diff --git a/coretk/coretk/data/icons/docker.png b/daemon/core/gui/data/icons/docker.png similarity index 100% rename from coretk/coretk/data/icons/docker.png rename to daemon/core/gui/data/icons/docker.png diff --git a/coretk/coretk/data/icons/document-new.gif b/daemon/core/gui/data/icons/document-new.gif similarity index 100% rename from coretk/coretk/data/icons/document-new.gif rename to daemon/core/gui/data/icons/document-new.gif diff --git a/coretk/coretk/data/icons/document-properties.gif b/daemon/core/gui/data/icons/document-properties.gif similarity index 100% rename from coretk/coretk/data/icons/document-properties.gif rename to daemon/core/gui/data/icons/document-properties.gif diff --git a/coretk/coretk/data/icons/document-save.gif b/daemon/core/gui/data/icons/document-save.gif similarity index 100% rename from coretk/coretk/data/icons/document-save.gif rename to daemon/core/gui/data/icons/document-save.gif diff --git a/coretk/coretk/data/icons/edit-delete.gif b/daemon/core/gui/data/icons/edit-delete.gif similarity index 100% rename from coretk/coretk/data/icons/edit-delete.gif rename to daemon/core/gui/data/icons/edit-delete.gif diff --git a/coretk/coretk/data/icons/edit-node.png b/daemon/core/gui/data/icons/edit-node.png similarity index 100% rename from coretk/coretk/data/icons/edit-node.png rename to daemon/core/gui/data/icons/edit-node.png diff --git a/coretk/coretk/data/icons/emane.png b/daemon/core/gui/data/icons/emane.png similarity index 100% rename from coretk/coretk/data/icons/emane.png rename to daemon/core/gui/data/icons/emane.png diff --git a/coretk/coretk/data/icons/fileopen.gif b/daemon/core/gui/data/icons/fileopen.gif similarity index 100% rename from coretk/coretk/data/icons/fileopen.gif rename to daemon/core/gui/data/icons/fileopen.gif diff --git a/coretk/coretk/data/icons/host.png b/daemon/core/gui/data/icons/host.png similarity index 100% rename from coretk/coretk/data/icons/host.png rename to daemon/core/gui/data/icons/host.png diff --git a/coretk/coretk/data/icons/hub.png b/daemon/core/gui/data/icons/hub.png similarity index 100% rename from coretk/coretk/data/icons/hub.png rename to daemon/core/gui/data/icons/hub.png diff --git a/coretk/coretk/data/icons/lanswitch.png b/daemon/core/gui/data/icons/lanswitch.png similarity index 100% rename from coretk/coretk/data/icons/lanswitch.png rename to daemon/core/gui/data/icons/lanswitch.png diff --git a/coretk/coretk/data/icons/link.png b/daemon/core/gui/data/icons/link.png similarity index 100% rename from coretk/coretk/data/icons/link.png rename to daemon/core/gui/data/icons/link.png diff --git a/coretk/coretk/data/icons/lxc.png b/daemon/core/gui/data/icons/lxc.png similarity index 100% rename from coretk/coretk/data/icons/lxc.png rename to daemon/core/gui/data/icons/lxc.png diff --git a/coretk/coretk/data/icons/marker.png b/daemon/core/gui/data/icons/marker.png similarity index 100% rename from coretk/coretk/data/icons/marker.png rename to daemon/core/gui/data/icons/marker.png diff --git a/coretk/coretk/data/icons/markerclear.png b/daemon/core/gui/data/icons/markerclear.png similarity index 100% rename from coretk/coretk/data/icons/markerclear.png rename to daemon/core/gui/data/icons/markerclear.png diff --git a/coretk/coretk/data/icons/mdr.png b/daemon/core/gui/data/icons/mdr.png similarity index 100% rename from coretk/coretk/data/icons/mdr.png rename to daemon/core/gui/data/icons/mdr.png diff --git a/coretk/coretk/data/icons/observe.gif b/daemon/core/gui/data/icons/observe.gif similarity index 100% rename from coretk/coretk/data/icons/observe.gif rename to daemon/core/gui/data/icons/observe.gif diff --git a/coretk/coretk/data/icons/oval.png b/daemon/core/gui/data/icons/oval.png similarity index 100% rename from coretk/coretk/data/icons/oval.png rename to daemon/core/gui/data/icons/oval.png diff --git a/coretk/coretk/data/icons/pause.png b/daemon/core/gui/data/icons/pause.png similarity index 100% rename from coretk/coretk/data/icons/pause.png rename to daemon/core/gui/data/icons/pause.png diff --git a/coretk/coretk/data/icons/pc.png b/daemon/core/gui/data/icons/pc.png similarity index 100% rename from coretk/coretk/data/icons/pc.png rename to daemon/core/gui/data/icons/pc.png diff --git a/coretk/coretk/data/icons/plot.gif b/daemon/core/gui/data/icons/plot.gif similarity index 100% rename from coretk/coretk/data/icons/plot.gif rename to daemon/core/gui/data/icons/plot.gif diff --git a/coretk/coretk/data/icons/prouter.png b/daemon/core/gui/data/icons/prouter.png similarity index 100% rename from coretk/coretk/data/icons/prouter.png rename to daemon/core/gui/data/icons/prouter.png diff --git a/coretk/coretk/data/icons/rectangle.png b/daemon/core/gui/data/icons/rectangle.png similarity index 100% rename from coretk/coretk/data/icons/rectangle.png rename to daemon/core/gui/data/icons/rectangle.png diff --git a/coretk/coretk/data/icons/rj45.png b/daemon/core/gui/data/icons/rj45.png similarity index 100% rename from coretk/coretk/data/icons/rj45.png rename to daemon/core/gui/data/icons/rj45.png diff --git a/coretk/coretk/data/icons/router.png b/daemon/core/gui/data/icons/router.png similarity index 100% rename from coretk/coretk/data/icons/router.png rename to daemon/core/gui/data/icons/router.png diff --git a/coretk/coretk/data/icons/run.png b/daemon/core/gui/data/icons/run.png similarity index 100% rename from coretk/coretk/data/icons/run.png rename to daemon/core/gui/data/icons/run.png diff --git a/coretk/coretk/data/icons/select.png b/daemon/core/gui/data/icons/select.png similarity index 100% rename from coretk/coretk/data/icons/select.png rename to daemon/core/gui/data/icons/select.png diff --git a/coretk/coretk/data/icons/start.png b/daemon/core/gui/data/icons/start.png similarity index 100% rename from coretk/coretk/data/icons/start.png rename to daemon/core/gui/data/icons/start.png diff --git a/coretk/coretk/data/icons/stop.png b/daemon/core/gui/data/icons/stop.png similarity index 100% rename from coretk/coretk/data/icons/stop.png rename to daemon/core/gui/data/icons/stop.png diff --git a/coretk/coretk/data/icons/text.png b/daemon/core/gui/data/icons/text.png similarity index 100% rename from coretk/coretk/data/icons/text.png rename to daemon/core/gui/data/icons/text.png diff --git a/coretk/coretk/data/icons/tunnel.png b/daemon/core/gui/data/icons/tunnel.png similarity index 100% rename from coretk/coretk/data/icons/tunnel.png rename to daemon/core/gui/data/icons/tunnel.png diff --git a/coretk/coretk/data/icons/twonode.png b/daemon/core/gui/data/icons/twonode.png similarity index 100% rename from coretk/coretk/data/icons/twonode.png rename to daemon/core/gui/data/icons/twonode.png diff --git a/coretk/coretk/data/icons/wlan.png b/daemon/core/gui/data/icons/wlan.png similarity index 100% rename from coretk/coretk/data/icons/wlan.png rename to daemon/core/gui/data/icons/wlan.png diff --git a/coretk/coretk/data/mobility/sample1.scen b/daemon/core/gui/data/mobility/sample1.scen similarity index 100% rename from coretk/coretk/data/mobility/sample1.scen rename to daemon/core/gui/data/mobility/sample1.scen diff --git a/coretk/coretk/data/oldicons/docker.gif b/daemon/core/gui/data/oldicons/docker.gif similarity index 100% rename from coretk/coretk/data/oldicons/docker.gif rename to daemon/core/gui/data/oldicons/docker.gif diff --git a/coretk/coretk/data/oldicons/emane.gif b/daemon/core/gui/data/oldicons/emane.gif similarity index 100% rename from coretk/coretk/data/oldicons/emane.gif rename to daemon/core/gui/data/oldicons/emane.gif diff --git a/coretk/coretk/data/oldicons/host.gif b/daemon/core/gui/data/oldicons/host.gif similarity index 100% rename from coretk/coretk/data/oldicons/host.gif rename to daemon/core/gui/data/oldicons/host.gif diff --git a/coretk/coretk/data/oldicons/hub.gif b/daemon/core/gui/data/oldicons/hub.gif similarity index 100% rename from coretk/coretk/data/oldicons/hub.gif rename to daemon/core/gui/data/oldicons/hub.gif diff --git a/coretk/coretk/data/oldicons/lanswitch.gif b/daemon/core/gui/data/oldicons/lanswitch.gif similarity index 100% rename from coretk/coretk/data/oldicons/lanswitch.gif rename to daemon/core/gui/data/oldicons/lanswitch.gif diff --git a/coretk/coretk/data/oldicons/link.gif b/daemon/core/gui/data/oldicons/link.gif similarity index 100% rename from coretk/coretk/data/oldicons/link.gif rename to daemon/core/gui/data/oldicons/link.gif diff --git a/coretk/coretk/data/oldicons/lxc.gif b/daemon/core/gui/data/oldicons/lxc.gif similarity index 100% rename from coretk/coretk/data/oldicons/lxc.gif rename to daemon/core/gui/data/oldicons/lxc.gif diff --git a/coretk/coretk/data/oldicons/marker.gif b/daemon/core/gui/data/oldicons/marker.gif similarity index 100% rename from coretk/coretk/data/oldicons/marker.gif rename to daemon/core/gui/data/oldicons/marker.gif diff --git a/coretk/coretk/data/oldicons/mdr.gif b/daemon/core/gui/data/oldicons/mdr.gif similarity index 100% rename from coretk/coretk/data/oldicons/mdr.gif rename to daemon/core/gui/data/oldicons/mdr.gif diff --git a/coretk/coretk/data/oldicons/oval.gif b/daemon/core/gui/data/oldicons/oval.gif similarity index 100% rename from coretk/coretk/data/oldicons/oval.gif rename to daemon/core/gui/data/oldicons/oval.gif diff --git a/coretk/coretk/data/oldicons/pc.gif b/daemon/core/gui/data/oldicons/pc.gif similarity index 100% rename from coretk/coretk/data/oldicons/pc.gif rename to daemon/core/gui/data/oldicons/pc.gif diff --git a/coretk/coretk/data/oldicons/rectangle.gif b/daemon/core/gui/data/oldicons/rectangle.gif similarity index 100% rename from coretk/coretk/data/oldicons/rectangle.gif rename to daemon/core/gui/data/oldicons/rectangle.gif diff --git a/coretk/coretk/data/oldicons/rj45.gif b/daemon/core/gui/data/oldicons/rj45.gif similarity index 100% rename from coretk/coretk/data/oldicons/rj45.gif rename to daemon/core/gui/data/oldicons/rj45.gif diff --git a/coretk/coretk/data/oldicons/router.gif b/daemon/core/gui/data/oldicons/router.gif similarity index 100% rename from coretk/coretk/data/oldicons/router.gif rename to daemon/core/gui/data/oldicons/router.gif diff --git a/coretk/coretk/data/oldicons/router_green.gif b/daemon/core/gui/data/oldicons/router_green.gif similarity index 100% rename from coretk/coretk/data/oldicons/router_green.gif rename to daemon/core/gui/data/oldicons/router_green.gif diff --git a/coretk/coretk/data/oldicons/run.gif b/daemon/core/gui/data/oldicons/run.gif similarity index 100% rename from coretk/coretk/data/oldicons/run.gif rename to daemon/core/gui/data/oldicons/run.gif diff --git a/coretk/coretk/data/oldicons/select.gif b/daemon/core/gui/data/oldicons/select.gif similarity index 100% rename from coretk/coretk/data/oldicons/select.gif rename to daemon/core/gui/data/oldicons/select.gif diff --git a/coretk/coretk/data/oldicons/start.gif b/daemon/core/gui/data/oldicons/start.gif similarity index 100% rename from coretk/coretk/data/oldicons/start.gif rename to daemon/core/gui/data/oldicons/start.gif diff --git a/coretk/coretk/data/oldicons/stop.gif b/daemon/core/gui/data/oldicons/stop.gif similarity index 100% rename from coretk/coretk/data/oldicons/stop.gif rename to daemon/core/gui/data/oldicons/stop.gif diff --git a/coretk/coretk/data/oldicons/text.gif b/daemon/core/gui/data/oldicons/text.gif similarity index 100% rename from coretk/coretk/data/oldicons/text.gif rename to daemon/core/gui/data/oldicons/text.gif diff --git a/coretk/coretk/data/oldicons/tunnel.gif b/daemon/core/gui/data/oldicons/tunnel.gif similarity index 100% rename from coretk/coretk/data/oldicons/tunnel.gif rename to daemon/core/gui/data/oldicons/tunnel.gif diff --git a/coretk/coretk/data/oldicons/twonode.gif b/daemon/core/gui/data/oldicons/twonode.gif similarity index 100% rename from coretk/coretk/data/oldicons/twonode.gif rename to daemon/core/gui/data/oldicons/twonode.gif diff --git a/coretk/coretk/data/oldicons/wlan.gif b/daemon/core/gui/data/oldicons/wlan.gif similarity index 100% rename from coretk/coretk/data/oldicons/wlan.gif rename to daemon/core/gui/data/oldicons/wlan.gif diff --git a/coretk/coretk/data/xmls/sample1.xml b/daemon/core/gui/data/xmls/sample1.xml similarity index 100% rename from coretk/coretk/data/xmls/sample1.xml rename to daemon/core/gui/data/xmls/sample1.xml diff --git a/coretk/coretk/dialogs/__init__.py b/daemon/core/gui/dialogs/__init__.py similarity index 100% rename from coretk/coretk/dialogs/__init__.py rename to daemon/core/gui/dialogs/__init__.py diff --git a/coretk/coretk/dialogs/about.py b/daemon/core/gui/dialogs/about.py similarity index 95% rename from coretk/coretk/dialogs/about.py rename to daemon/core/gui/dialogs/about.py index 9e3ff7a9..af33ab61 100644 --- a/coretk/coretk/dialogs/about.py +++ b/daemon/core/gui/dialogs/about.py @@ -1,7 +1,7 @@ import tkinter as tk -from coretk.dialogs.dialog import Dialog -from coretk.widgets import CodeText +from core.gui.dialogs.dialog import Dialog +from core.gui.widgets import CodeText LICENSE = """\ Copyright (c) 2005-2020, the Boeing Company. diff --git a/coretk/coretk/dialogs/alerts.py b/daemon/core/gui/dialogs/alerts.py similarity index 98% rename from coretk/coretk/dialogs/alerts.py rename to daemon/core/gui/dialogs/alerts.py index e782547f..2fb94a4f 100644 --- a/coretk/coretk/dialogs/alerts.py +++ b/daemon/core/gui/dialogs/alerts.py @@ -7,9 +7,9 @@ from tkinter import ttk from grpc import RpcError from core.api.grpc import core_pb2 -from coretk.dialogs.dialog import Dialog -from coretk.themes import PADX, PADY -from coretk.widgets import CodeText +from core.gui.dialogs.dialog import Dialog +from core.gui.themes import PADX, PADY +from core.gui.widgets import CodeText class AlertsDialog(Dialog): diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/daemon/core/gui/dialogs/canvassizeandscale.py similarity index 99% rename from coretk/coretk/dialogs/canvassizeandscale.py rename to daemon/core/gui/dialogs/canvassizeandscale.py index 0a113936..11cc97b4 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/daemon/core/gui/dialogs/canvassizeandscale.py @@ -4,8 +4,8 @@ size and scale import tkinter as tk from tkinter import font, ttk -from coretk.dialogs.dialog import Dialog -from coretk.themes import FRAME_PAD, PADX, PADY +from core.gui.dialogs.dialog import Dialog +from core.gui.themes import FRAME_PAD, PADX, PADY PIXEL_SCALE = 100 diff --git a/coretk/coretk/dialogs/canvaswallpaper.py b/daemon/core/gui/dialogs/canvaswallpaper.py similarity index 96% rename from coretk/coretk/dialogs/canvaswallpaper.py rename to daemon/core/gui/dialogs/canvaswallpaper.py index 570bfa08..f3635377 100644 --- a/coretk/coretk/dialogs/canvaswallpaper.py +++ b/daemon/core/gui/dialogs/canvaswallpaper.py @@ -5,11 +5,11 @@ import logging import tkinter as tk from tkinter import ttk -from coretk.appconfig import BACKGROUNDS_PATH -from coretk.dialogs.dialog import Dialog -from coretk.images import Images -from coretk.themes import PADX, PADY -from coretk.widgets import image_chooser +from core.gui.appconfig import BACKGROUNDS_PATH +from core.gui.dialogs.dialog import Dialog +from core.gui.images import Images +from core.gui.themes import PADX, PADY +from core.gui.widgets import image_chooser class CanvasBackgroundDialog(Dialog): diff --git a/coretk/coretk/dialogs/colorpicker.py b/daemon/core/gui/dialogs/colorpicker.py similarity index 99% rename from coretk/coretk/dialogs/colorpicker.py rename to daemon/core/gui/dialogs/colorpicker.py index 9734468d..b94ae635 100644 --- a/coretk/coretk/dialogs/colorpicker.py +++ b/daemon/core/gui/dialogs/colorpicker.py @@ -5,7 +5,7 @@ import logging import tkinter as tk from tkinter import ttk -from coretk.dialogs.dialog import Dialog +from core.gui.dialogs.dialog import Dialog class ColorPicker(Dialog): diff --git a/coretk/coretk/dialogs/customnodes.py b/daemon/core/gui/dialogs/customnodes.py similarity index 96% rename from coretk/coretk/dialogs/customnodes.py rename to daemon/core/gui/dialogs/customnodes.py index 94f2a32f..86303a69 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/daemon/core/gui/dialogs/customnodes.py @@ -3,13 +3,13 @@ import tkinter as tk from pathlib import Path from tkinter import ttk -from coretk import nodeutils -from coretk.appconfig import ICONS_PATH -from coretk.dialogs.dialog import Dialog -from coretk.images import Images -from coretk.nodeutils import NodeDraw -from coretk.themes import FRAME_PAD, PADX, PADY -from coretk.widgets import CheckboxList, ListboxScroll, image_chooser +from core.gui import nodeutils +from core.gui.appconfig import ICONS_PATH +from core.gui.dialogs.dialog import Dialog +from core.gui.images import Images +from core.gui.nodeutils import NodeDraw +from core.gui.themes import FRAME_PAD, PADX, PADY +from core.gui.widgets import CheckboxList, ListboxScroll, image_chooser class ServicesSelectDialog(Dialog): diff --git a/coretk/coretk/dialogs/dialog.py b/daemon/core/gui/dialogs/dialog.py similarity index 92% rename from coretk/coretk/dialogs/dialog.py rename to daemon/core/gui/dialogs/dialog.py index 92d9a7db..3e6d54f6 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/daemon/core/gui/dialogs/dialog.py @@ -1,8 +1,8 @@ import tkinter as tk from tkinter import ttk -from coretk.images import ImageEnum, Images -from coretk.themes import DIALOG_PAD +from core.gui.images import ImageEnum, Images +from core.gui.themes import DIALOG_PAD class Dialog(tk.Toplevel): diff --git a/coretk/coretk/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py similarity index 97% rename from coretk/coretk/dialogs/emaneconfig.py rename to daemon/core/gui/dialogs/emaneconfig.py index a1c30a6f..6f3dedd8 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -8,11 +8,11 @@ from tkinter import ttk import grpc -from coretk.dialogs.dialog import Dialog -from coretk.errors import show_grpc_error -from coretk.images import ImageEnum, Images -from coretk.themes import PADX, PADY -from coretk.widgets import ConfigFrame +from core.gui.dialogs.dialog import Dialog +from core.gui.errors import show_grpc_error +from core.gui.images import ImageEnum, Images +from core.gui.themes import PADX, PADY +from core.gui.widgets import ConfigFrame class GlobalEmaneDialog(Dialog): diff --git a/coretk/coretk/dialogs/hooks.py b/daemon/core/gui/dialogs/hooks.py similarity index 97% rename from coretk/coretk/dialogs/hooks.py rename to daemon/core/gui/dialogs/hooks.py index 37503d66..79741add 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/daemon/core/gui/dialogs/hooks.py @@ -2,9 +2,9 @@ import tkinter as tk from tkinter import ttk from core.api.grpc import core_pb2 -from coretk.dialogs.dialog import Dialog -from coretk.themes import PADX, PADY -from coretk.widgets import CodeText, ListboxScroll +from core.gui.dialogs.dialog import Dialog +from core.gui.themes import PADX, PADY +from core.gui.widgets import CodeText, ListboxScroll class HookDialog(Dialog): diff --git a/coretk/coretk/dialogs/linkconfig.py b/daemon/core/gui/dialogs/linkconfig.py similarity index 98% rename from coretk/coretk/dialogs/linkconfig.py rename to daemon/core/gui/dialogs/linkconfig.py index cf0daafc..b59307d6 100644 --- a/coretk/coretk/dialogs/linkconfig.py +++ b/daemon/core/gui/dialogs/linkconfig.py @@ -6,9 +6,9 @@ import tkinter as tk from tkinter import ttk from core.api.grpc import core_pb2 -from coretk.dialogs.colorpicker import ColorPicker -from coretk.dialogs.dialog import Dialog -from coretk.themes import PADX, PADY +from core.gui.dialogs.colorpicker import ColorPicker +from core.gui.dialogs.dialog import Dialog +from core.gui.themes import PADX, PADY def get_int(var): diff --git a/coretk/coretk/dialogs/marker.py b/daemon/core/gui/dialogs/marker.py similarity index 96% rename from coretk/coretk/dialogs/marker.py rename to daemon/core/gui/dialogs/marker.py index 5648a89b..3a078d86 100644 --- a/coretk/coretk/dialogs/marker.py +++ b/daemon/core/gui/dialogs/marker.py @@ -6,8 +6,8 @@ import logging import tkinter as tk from tkinter import ttk -from coretk.dialogs.colorpicker import ColorPicker -from coretk.dialogs.dialog import Dialog +from core.gui.dialogs.colorpicker import ColorPicker +from core.gui.dialogs.dialog import Dialog MARKER_THICKNESS = [3, 5, 8, 10] diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/daemon/core/gui/dialogs/mobilityconfig.py similarity index 90% rename from coretk/coretk/dialogs/mobilityconfig.py rename to daemon/core/gui/dialogs/mobilityconfig.py index 19dc46f4..3b9c1ca6 100644 --- a/coretk/coretk/dialogs/mobilityconfig.py +++ b/daemon/core/gui/dialogs/mobilityconfig.py @@ -5,10 +5,10 @@ from tkinter import ttk import grpc -from coretk.dialogs.dialog import Dialog -from coretk.errors import show_grpc_error -from coretk.themes import PADX, PADY -from coretk.widgets import ConfigFrame +from core.gui.dialogs.dialog import Dialog +from core.gui.errors import show_grpc_error +from core.gui.themes import PADX, PADY +from core.gui.widgets import ConfigFrame class MobilityConfigDialog(Dialog): diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/daemon/core/gui/dialogs/mobilityplayer.py similarity index 96% rename from coretk/coretk/dialogs/mobilityplayer.py rename to daemon/core/gui/dialogs/mobilityplayer.py index f0b46499..9c2848d8 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/daemon/core/gui/dialogs/mobilityplayer.py @@ -4,10 +4,10 @@ from tkinter import ttk import grpc from core.api.grpc.core_pb2 import MobilityAction -from coretk.dialogs.dialog import Dialog -from coretk.errors import show_grpc_error -from coretk.images import ImageEnum, Images -from coretk.themes import PADX, PADY +from core.gui.dialogs.dialog import Dialog +from core.gui.errors import show_grpc_error +from core.gui.images import ImageEnum, Images +from core.gui.themes import PADX, PADY ICON_SIZE = 16 diff --git a/coretk/coretk/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py similarity index 96% rename from coretk/coretk/dialogs/nodeconfig.py rename to daemon/core/gui/dialogs/nodeconfig.py index 4e5bc864..47ac9389 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -3,14 +3,14 @@ import tkinter as tk from functools import partial from tkinter import ttk -from coretk import nodeutils -from coretk.appconfig import ICONS_PATH -from coretk.dialogs.dialog import Dialog -from coretk.dialogs.emaneconfig import EmaneModelDialog -from coretk.images import Images -from coretk.nodeutils import NodeUtils -from coretk.themes import FRAME_PAD, PADX, PADY -from coretk.widgets import image_chooser +from core.gui import nodeutils +from core.gui.appconfig import ICONS_PATH +from core.gui.dialogs.dialog import Dialog +from core.gui.dialogs.emaneconfig import EmaneModelDialog +from core.gui.images import Images +from core.gui.nodeutils import NodeUtils +from core.gui.themes import FRAME_PAD, PADX, PADY +from core.gui.widgets import image_chooser def mac_auto(is_auto, entry): diff --git a/coretk/coretk/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py similarity index 95% rename from coretk/coretk/dialogs/nodeservice.py rename to daemon/core/gui/dialogs/nodeservice.py index 8ad87649..730a5ff9 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -4,10 +4,10 @@ core node services import tkinter as tk from tkinter import messagebox, ttk -from coretk.dialogs.dialog import Dialog -from coretk.dialogs.serviceconfiguration import ServiceConfiguration -from coretk.themes import FRAME_PAD, PADX, PADY -from coretk.widgets import CheckboxList, ListboxScroll +from core.gui.dialogs.dialog import Dialog +from core.gui.dialogs.serviceconfiguration import ServiceConfiguration +from core.gui.themes import FRAME_PAD, PADX, PADY +from core.gui.widgets import CheckboxList, ListboxScroll class NodeService(Dialog): diff --git a/coretk/coretk/dialogs/observers.py b/daemon/core/gui/dialogs/observers.py similarity index 96% rename from coretk/coretk/dialogs/observers.py rename to daemon/core/gui/dialogs/observers.py index de857b76..5f0f1b1e 100644 --- a/coretk/coretk/dialogs/observers.py +++ b/daemon/core/gui/dialogs/observers.py @@ -1,10 +1,10 @@ import tkinter as tk from tkinter import ttk -from coretk.coreclient import Observer -from coretk.dialogs.dialog import Dialog -from coretk.themes import PADX, PADY -from coretk.widgets import ListboxScroll +from core.gui.coreclient import Observer +from core.gui.dialogs.dialog import Dialog +from core.gui.themes import PADX, PADY +from core.gui.widgets import ListboxScroll class ObserverDialog(Dialog): diff --git a/coretk/coretk/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py similarity index 96% rename from coretk/coretk/dialogs/preferences.py rename to daemon/core/gui/dialogs/preferences.py index 8c369027..6208990a 100644 --- a/coretk/coretk/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -2,9 +2,9 @@ import logging import tkinter as tk from tkinter import ttk -from coretk import appconfig -from coretk.dialogs.dialog import Dialog -from coretk.themes import FRAME_PAD, PADX, PADY +from core.gui import appconfig +from core.gui.dialogs.dialog import Dialog +from core.gui.themes import FRAME_PAD, PADX, PADY class PreferencesDialog(Dialog): diff --git a/coretk/coretk/dialogs/servers.py b/daemon/core/gui/dialogs/servers.py similarity index 97% rename from coretk/coretk/dialogs/servers.py rename to daemon/core/gui/dialogs/servers.py index c380c63d..a0eadec2 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/daemon/core/gui/dialogs/servers.py @@ -1,10 +1,10 @@ import tkinter as tk from tkinter import ttk -from coretk.coreclient import CoreServer -from coretk.dialogs.dialog import Dialog -from coretk.themes import FRAME_PAD, PADX, PADY -from coretk.widgets import ListboxScroll +from core.gui.coreclient import CoreServer +from core.gui.dialogs.dialog import Dialog +from core.gui.themes import FRAME_PAD, PADX, PADY +from core.gui.widgets import ListboxScroll DEFAULT_NAME = "example" DEFAULT_ADDRESS = "127.0.0.1" diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/daemon/core/gui/dialogs/serviceconfiguration.py similarity index 98% rename from coretk/coretk/dialogs/serviceconfiguration.py rename to daemon/core/gui/dialogs/serviceconfiguration.py index 53aca1b3..f0d5b19b 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/daemon/core/gui/dialogs/serviceconfiguration.py @@ -6,11 +6,11 @@ from tkinter import ttk import grpc from core.api.grpc import core_pb2 -from coretk.dialogs.dialog import Dialog -from coretk.errors import show_grpc_error -from coretk.images import ImageEnum, Images -from coretk.themes import FRAME_PAD, PADX, PADY -from coretk.widgets import CodeText, ListboxScroll +from core.gui.dialogs.dialog import Dialog +from core.gui.errors import show_grpc_error +from core.gui.images import ImageEnum, Images +from core.gui.themes import FRAME_PAD, PADX, PADY +from core.gui.widgets import CodeText, ListboxScroll class ServiceConfiguration(Dialog): diff --git a/coretk/coretk/dialogs/sessionoptions.py b/daemon/core/gui/dialogs/sessionoptions.py similarity index 90% rename from coretk/coretk/dialogs/sessionoptions.py rename to daemon/core/gui/dialogs/sessionoptions.py index 24ff7381..040629b5 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/daemon/core/gui/dialogs/sessionoptions.py @@ -3,10 +3,10 @@ from tkinter import ttk import grpc -from coretk.dialogs.dialog import Dialog -from coretk.errors import show_grpc_error -from coretk.themes import PADX, PADY -from coretk.widgets import ConfigFrame +from core.gui.dialogs.dialog import Dialog +from core.gui.errors import show_grpc_error +from core.gui.themes import PADX, PADY +from core.gui.widgets import ConfigFrame class SessionOptionsDialog(Dialog): diff --git a/coretk/coretk/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py similarity index 97% rename from coretk/coretk/dialogs/sessions.py rename to daemon/core/gui/dialogs/sessions.py index b1fb970f..e86f5351 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -6,10 +6,10 @@ from tkinter import ttk import grpc from core.api.grpc import core_pb2 -from coretk.dialogs.dialog import Dialog -from coretk.errors import show_grpc_error -from coretk.images import ImageEnum, Images -from coretk.themes import PADX, PADY +from core.gui.dialogs.dialog import Dialog +from core.gui.errors import show_grpc_error +from core.gui.images import ImageEnum, Images +from core.gui.themes import PADX, PADY class SessionsDialog(Dialog): diff --git a/coretk/coretk/dialogs/shapemod.py b/daemon/core/gui/dialogs/shapemod.py similarity index 97% rename from coretk/coretk/dialogs/shapemod.py rename to daemon/core/gui/dialogs/shapemod.py index 62fed9f9..f77b45a8 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/daemon/core/gui/dialogs/shapemod.py @@ -4,11 +4,11 @@ shape input dialog import tkinter as tk from tkinter import font, ttk -from coretk.dialogs.colorpicker import ColorPicker -from coretk.dialogs.dialog import Dialog -from coretk.graph import tags -from coretk.graph.shapeutils import is_draw_shape, is_shape_text -from coretk.themes import FRAME_PAD, PADX, PADY +from core.gui.dialogs.colorpicker import ColorPicker +from core.gui.dialogs.dialog import Dialog +from core.gui.graph import tags +from core.gui.graph.shapeutils import is_draw_shape, is_shape_text +from core.gui.themes import FRAME_PAD, PADX, PADY FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] diff --git a/coretk/coretk/dialogs/wlanconfig.py b/daemon/core/gui/dialogs/wlanconfig.py similarity index 91% rename from coretk/coretk/dialogs/wlanconfig.py rename to daemon/core/gui/dialogs/wlanconfig.py index 20966d2b..3ceaa7e8 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/daemon/core/gui/dialogs/wlanconfig.py @@ -6,10 +6,10 @@ from tkinter import ttk import grpc -from coretk.dialogs.dialog import Dialog -from coretk.errors import show_grpc_error -from coretk.themes import PADX, PADY -from coretk.widgets import ConfigFrame +from core.gui.dialogs.dialog import Dialog +from core.gui.errors import show_grpc_error +from core.gui.themes import PADX, PADY +from core.gui.widgets import ConfigFrame class WlanConfigDialog(Dialog): diff --git a/coretk/coretk/errors.py b/daemon/core/gui/errors.py similarity index 100% rename from coretk/coretk/errors.py rename to daemon/core/gui/errors.py diff --git a/coretk/coretk/graph/__init__.py b/daemon/core/gui/graph/__init__.py similarity index 100% rename from coretk/coretk/graph/__init__.py rename to daemon/core/gui/graph/__init__.py diff --git a/coretk/coretk/graph/edges.py b/daemon/core/gui/graph/edges.py similarity index 97% rename from coretk/coretk/graph/edges.py rename to daemon/core/gui/graph/edges.py index e25a5305..b80c106c 100644 --- a/coretk/coretk/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -2,10 +2,10 @@ import logging import tkinter as tk from tkinter.font import Font -from coretk import themes -from coretk.dialogs.linkconfig import LinkConfiguration -from coretk.graph import tags -from coretk.nodeutils import NodeUtils +from core.gui import themes +from core.gui.dialogs.linkconfig import LinkConfiguration +from core.gui.graph import tags +from core.gui.nodeutils import NodeUtils TEXT_DISTANCE = 0.30 diff --git a/coretk/coretk/graph/enums.py b/daemon/core/gui/graph/enums.py similarity index 100% rename from coretk/coretk/graph/enums.py rename to daemon/core/gui/graph/enums.py diff --git a/coretk/coretk/graph/graph.py b/daemon/core/gui/graph/graph.py similarity index 98% rename from coretk/coretk/graph/graph.py rename to daemon/core/gui/graph/graph.py index 5d34f566..4b481a68 100644 --- a/coretk/coretk/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -4,17 +4,17 @@ import tkinter as tk from PIL import Image, ImageTk from core.api.grpc import core_pb2 -from coretk import nodeutils -from coretk.dialogs.shapemod import ShapeDialog -from coretk.graph import tags -from coretk.graph.edges import CanvasEdge, CanvasWirelessEdge -from coretk.graph.enums import GraphMode, ScaleOption -from coretk.graph.linkinfo import Throughput -from coretk.graph.node import CanvasNode -from coretk.graph.shape import Shape -from coretk.graph.shapeutils import ShapeType, is_draw_shape, is_marker -from coretk.images import Images -from coretk.nodeutils import NodeUtils +from core.gui import nodeutils +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.enums import GraphMode, ScaleOption +from core.gui.graph.linkinfo import Throughput +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 Images +from core.gui.nodeutils import NodeUtils ZOOM_IN = 1.1 ZOOM_OUT = 0.9 diff --git a/coretk/coretk/graph/linkinfo.py b/daemon/core/gui/graph/linkinfo.py similarity index 100% rename from coretk/coretk/graph/linkinfo.py rename to daemon/core/gui/graph/linkinfo.py diff --git a/coretk/coretk/graph/node.py b/daemon/core/gui/graph/node.py similarity index 95% rename from coretk/coretk/graph/node.py rename to daemon/core/gui/graph/node.py index a5893ff3..68405a37 100644 --- a/coretk/coretk/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -5,16 +5,16 @@ import grpc from core.api.grpc import core_pb2 from core.api.grpc.core_pb2 import NodeType -from coretk import themes -from coretk.dialogs.emaneconfig import EmaneConfigDialog -from coretk.dialogs.mobilityconfig import MobilityConfigDialog -from coretk.dialogs.nodeconfig import NodeConfigDialog -from coretk.dialogs.nodeservice import NodeService -from coretk.dialogs.wlanconfig import WlanConfigDialog -from coretk.errors import show_grpc_error -from coretk.graph import tags -from coretk.graph.tooltip import CanvasTooltip -from coretk.nodeutils import NodeUtils +from core.gui import themes +from core.gui.dialogs.emaneconfig import EmaneConfigDialog +from core.gui.dialogs.mobilityconfig import MobilityConfigDialog +from core.gui.dialogs.nodeconfig import NodeConfigDialog +from core.gui.dialogs.nodeservice import NodeService +from core.gui.dialogs.wlanconfig import WlanConfigDialog +from core.gui.errors import show_grpc_error +from core.gui.graph import tags +from core.gui.graph.tooltip import CanvasTooltip +from core.gui.nodeutils import NodeUtils NODE_TEXT_OFFSET = 5 diff --git a/coretk/coretk/graph/shape.py b/daemon/core/gui/graph/shape.py similarity index 97% rename from coretk/coretk/graph/shape.py rename to daemon/core/gui/graph/shape.py index e7277b49..b23db2e9 100644 --- a/coretk/coretk/graph/shape.py +++ b/daemon/core/gui/graph/shape.py @@ -1,8 +1,8 @@ import logging -from coretk.dialogs.shapemod import ShapeDialog -from coretk.graph import tags -from coretk.graph.shapeutils import ShapeType +from core.gui.dialogs.shapemod import ShapeDialog +from core.gui.graph import tags +from core.gui.graph.shapeutils import ShapeType class AnnotationData: diff --git a/coretk/coretk/graph/shapeutils.py b/daemon/core/gui/graph/shapeutils.py similarity index 100% rename from coretk/coretk/graph/shapeutils.py rename to daemon/core/gui/graph/shapeutils.py diff --git a/coretk/coretk/graph/tags.py b/daemon/core/gui/graph/tags.py similarity index 100% rename from coretk/coretk/graph/tags.py rename to daemon/core/gui/graph/tags.py diff --git a/coretk/coretk/graph/tooltip.py b/daemon/core/gui/graph/tooltip.py similarity index 98% rename from coretk/coretk/graph/tooltip.py rename to daemon/core/gui/graph/tooltip.py index 8ae528d8..3cc5825c 100644 --- a/coretk/coretk/graph/tooltip.py +++ b/daemon/core/gui/graph/tooltip.py @@ -1,7 +1,7 @@ import tkinter as tk from tkinter import ttk -from coretk.themes import Styles +from core.gui.themes import Styles class CanvasTooltip: diff --git a/coretk/coretk/images.py b/daemon/core/gui/images.py similarity index 97% rename from coretk/coretk/images.py rename to daemon/core/gui/images.py index 9d282dbb..0d55f650 100644 --- a/coretk/coretk/images.py +++ b/daemon/core/gui/images.py @@ -2,7 +2,7 @@ from enum import Enum from PIL import Image, ImageTk -from coretk.appconfig import LOCAL_ICONS_PATH +from core.gui.appconfig import LOCAL_ICONS_PATH class Images: diff --git a/coretk/coretk/interface.py b/daemon/core/gui/interface.py similarity index 98% rename from coretk/coretk/interface.py rename to daemon/core/gui/interface.py index f4a380ae..ec0bcec5 100644 --- a/coretk/coretk/interface.py +++ b/daemon/core/gui/interface.py @@ -3,7 +3,7 @@ import random from netaddr import IPNetwork -from coretk.nodeutils import NodeUtils +from core.gui.nodeutils import NodeUtils def random_mac(): diff --git a/coretk/coretk/menuaction.py b/daemon/core/gui/menuaction.py similarity index 89% rename from coretk/coretk/menuaction.py rename to daemon/core/gui/menuaction.py index ec7e7cb7..4c310cd7 100644 --- a/coretk/coretk/menuaction.py +++ b/daemon/core/gui/menuaction.py @@ -10,16 +10,16 @@ from tkinter import filedialog, messagebox import grpc -from coretk.appconfig import XMLS_PATH -from coretk.dialogs.about import AboutDialog -from coretk.dialogs.canvassizeandscale import SizeAndScaleDialog -from coretk.dialogs.canvaswallpaper import CanvasBackgroundDialog -from coretk.dialogs.hooks import HooksDialog -from coretk.dialogs.observers import ObserverDialog -from coretk.dialogs.preferences import PreferencesDialog -from coretk.dialogs.servers import ServersDialog -from coretk.dialogs.sessionoptions import SessionOptionsDialog -from coretk.dialogs.sessions import SessionsDialog +from core.gui.appconfig import XMLS_PATH +from core.gui.dialogs.about import AboutDialog +from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog +from core.gui.dialogs.canvaswallpaper import CanvasBackgroundDialog +from core.gui.dialogs.hooks import HooksDialog +from core.gui.dialogs.observers import ObserverDialog +from core.gui.dialogs.preferences import PreferencesDialog +from core.gui.dialogs.servers import ServersDialog +from core.gui.dialogs.sessionoptions import SessionOptionsDialog +from core.gui.dialogs.sessions import SessionsDialog class MenuAction: diff --git a/coretk/coretk/menubar.py b/daemon/core/gui/menubar.py similarity index 99% rename from coretk/coretk/menubar.py rename to daemon/core/gui/menubar.py index 460ce2bf..c2a4e353 100644 --- a/coretk/coretk/menubar.py +++ b/daemon/core/gui/menubar.py @@ -1,8 +1,8 @@ import tkinter as tk from functools import partial -import coretk.menuaction as action -from coretk.coreclient import OBSERVERS +import core.gui.menuaction as action +from core.gui.coreclient import OBSERVERS class Menubar(tk.Menu): diff --git a/coretk/coretk/nodeutils.py b/daemon/core/gui/nodeutils.py similarity index 98% rename from coretk/coretk/nodeutils.py rename to daemon/core/gui/nodeutils.py index cb0bc7a3..779e52f8 100644 --- a/coretk/coretk/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -1,5 +1,5 @@ from core.api.grpc.core_pb2 import NodeType -from coretk.images import ImageEnum, Images +from core.gui.images import ImageEnum, Images ICON_SIZE = 48 ANTENNA_SIZE = 32 diff --git a/coretk/coretk/statusbar.py b/daemon/core/gui/statusbar.py similarity index 96% rename from coretk/coretk/statusbar.py rename to daemon/core/gui/statusbar.py index 5ed6f09d..ab0ba4e7 100644 --- a/coretk/coretk/statusbar.py +++ b/daemon/core/gui/statusbar.py @@ -2,8 +2,8 @@ import tkinter as tk from tkinter import ttk -from coretk.dialogs.alerts import AlertsDialog -from coretk.themes import Styles +from core.gui.dialogs.alerts import AlertsDialog +from core.gui.themes import Styles class StatusBar(ttk.Frame): diff --git a/coretk/coretk/themes.py b/daemon/core/gui/themes.py similarity index 100% rename from coretk/coretk/themes.py rename to daemon/core/gui/themes.py diff --git a/coretk/coretk/toolbar.py b/daemon/core/gui/toolbar.py similarity index 97% rename from coretk/coretk/toolbar.py rename to daemon/core/gui/toolbar.py index c32b0d75..2d0dad94 100644 --- a/coretk/coretk/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -5,15 +5,15 @@ from functools import partial from tkinter import ttk from tkinter.font import Font -from coretk.dialogs.customnodes import CustomNodesDialog -from coretk.dialogs.marker import Marker -from coretk.graph import tags -from coretk.graph.enums import GraphMode -from coretk.graph.shapeutils import ShapeType, is_marker -from coretk.images import ImageEnum, Images -from coretk.nodeutils import NodeUtils -from coretk.themes import Styles -from coretk.tooltip import Tooltip +from core.gui.dialogs.customnodes import CustomNodesDialog +from core.gui.dialogs.marker import Marker +from core.gui.graph import tags +from core.gui.graph.enums import GraphMode +from core.gui.graph.shapeutils import ShapeType, is_marker +from core.gui.images import ImageEnum, Images +from core.gui.nodeutils import NodeUtils +from core.gui.themes import Styles +from core.gui.tooltip import Tooltip TOOLBAR_SIZE = 32 PICKER_SIZE = 24 diff --git a/coretk/coretk/tooltip.py b/daemon/core/gui/tooltip.py similarity index 97% rename from coretk/coretk/tooltip.py rename to daemon/core/gui/tooltip.py index 9a3f7ade..4fe2b467 100644 --- a/coretk/coretk/tooltip.py +++ b/daemon/core/gui/tooltip.py @@ -1,7 +1,7 @@ import tkinter as tk from tkinter import ttk -from coretk.themes import Styles +from core.gui.themes import Styles class Tooltip(object): diff --git a/coretk/coretk/validation.py b/daemon/core/gui/validation.py similarity index 100% rename from coretk/coretk/validation.py rename to daemon/core/gui/validation.py diff --git a/coretk/coretk/widgets.py b/daemon/core/gui/widgets.py similarity index 99% rename from coretk/coretk/widgets.py rename to daemon/core/gui/widgets.py index 649dce95..6e8964af 100644 --- a/coretk/coretk/widgets.py +++ b/daemon/core/gui/widgets.py @@ -4,8 +4,8 @@ from functools import partial from tkinter import filedialog, font, ttk from core.api.grpc import core_pb2 -from coretk import themes -from coretk.themes import FRAME_PAD, PADX, PADY +from core.gui import themes +from core.gui.themes import FRAME_PAD, PADX, PADY INT_TYPES = { core_pb2.ConfigOptionType.UINT8, diff --git a/daemon/setup.py.in b/daemon/setup.py.in index 378912d3..8539a1fe 100644 --- a/daemon/setup.py.in +++ b/daemon/setup.py.in @@ -36,9 +36,12 @@ setup( install_requires=[ "fabric", "grpcio", + "netaddr", "invoke", "lxml", + "pillow", "protobuf", + "pyyaml", ], tests_require=[ "pytest", From 322ed4728084b59278641ecb89118a391941d6ee Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 19 Dec 2019 09:31:35 -0800 Subject: [PATCH 422/462] removed coretk directory --- coretk/Pipfile | 19 -- coretk/Pipfile.lock | 521 -------------------------------------------- coretk/setup.cfg | 15 -- coretk/setup.py | 12 - 4 files changed, 567 deletions(-) delete mode 100644 coretk/Pipfile delete mode 100644 coretk/Pipfile.lock delete mode 100644 coretk/setup.cfg delete mode 100644 coretk/setup.py diff --git a/coretk/Pipfile b/coretk/Pipfile deleted file mode 100644 index b30b3d54..00000000 --- a/coretk/Pipfile +++ /dev/null @@ -1,19 +0,0 @@ -[[source]] -name = "pypi" -url = "https://pypi.org/simple" -verify_ssl = true - -[scripts] -coretk = "python coretk/app.py" - -[dev-packages] -flake8 = "*" -isort = "*" -black = "==19.3b0" -pre-commit = "*" - -[packages] -coretk = {path = ".",editable = true} -core = {path = "./../daemon",editable = true} -pyyaml = "*" -netaddr = "*" diff --git a/coretk/Pipfile.lock b/coretk/Pipfile.lock deleted file mode 100644 index 7ff33122..00000000 --- a/coretk/Pipfile.lock +++ /dev/null @@ -1,521 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "9024ff4821ee3ccffee21a83f5436953371ad7d64a81a22b6c3723002c92b2cd" - }, - "pipfile-spec": 6, - "requires": {}, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "bcrypt": { - "hashes": [ - "sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89", - "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42", - "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294", - "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161", - "sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752", - "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31", - "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5", - "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c", - "sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0", - "sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de", - "sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e", - "sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052", - "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09", - "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105", - "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133", - "sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1", - "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7", - "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc" - ], - "version": "==3.1.7" - }, - "cffi": { - "hashes": [ - "sha256: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" - }, - "core": { - "editable": true, - "path": "./../daemon" - }, - "coretk": { - "editable": true, - "path": "." - }, - "cryptography": { - "hashes": [ - "sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c", - "sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595", - "sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad", - "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651", - "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2", - "sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff", - "sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d", - "sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42", - "sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d", - "sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e", - "sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912", - "sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793", - "sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13", - "sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7", - "sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0", - "sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879", - "sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f", - "sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9", - "sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2", - "sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf", - "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8" - ], - "version": "==2.8" - }, - "fabric": { - "hashes": [ - "sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389", - "sha256:24842d7d51556adcabd885ac3cf5e1df73fc622a1708bf3667bf5927576cdfa6" - ], - "version": "==2.5.0" - }, - "grpcio": { - "hashes": [ - "sha256:0419ae5a45f49c7c40d9ae77ae4de9442431b7822851dfbbe56ee0eacb5e5654", - "sha256:1e8631eeee0fb0b4230aeb135e4890035f6ef9159c2a3555fa184468e325691a", - "sha256:24db2fa5438f3815a4edb7a189035051760ca6aa2b0b70a6a948b28bfc63c76b", - "sha256:2adb1cdb7d33e91069517b41249622710a94a1faece1fed31cd36904e4201cde", - "sha256:2cd51f35692b551aeb1fdeb7a256c7c558f6d78fcddff00640942d42f7aeba5f", - "sha256:3247834d24964589f8c2b121b40cd61319b3c2e8d744a6a82008643ef8a378b1", - "sha256:3433cb848b4209717722b62392e575a77a52a34d67c6730138102abc0a441685", - "sha256:39671b7ff77a962bd745746d9d2292c8ed227c5748f16598d16d8631d17dd7e5", - "sha256:40a0b8b2e6f6dd630f8b267eede2f40a848963d0f3c40b1b1f453a4a870f679e", - "sha256:40f9a74c7aa210b3e76eb1c9d56aa8d08722b73426a77626967019df9bbac287", - "sha256:423f76aa504c84cb94594fb88b8a24027c887f1c488cf58f2173f22f4fbd046c", - "sha256:43bd04cec72281a96eb361e1b0232f0f542b46da50bcfe72ef7e5a1b41d00cb3", - "sha256:43e38762635c09e24885d15e3a8e374b72d105d4178ee2cc9491855a8da9c380", - "sha256:4413b11c2385180d7de03add6c8845dd66692b148d36e27ec8c9ef537b2553a1", - "sha256:4450352a87094fd58daf468b04c65a9fa19ad11a0ac8ac7b7ff17d46f873cbc1", - "sha256:49ffda04a6e44de028b3b786278ac9a70043e7905c3eea29eed88b6524d53a29", - "sha256:4a38c4dde4c9120deef43aaabaa44f19186c98659ce554c29788c4071ab2f0a4", - "sha256:50b1febdfd21e2144b56a9aa226829e93a79c354ef22a4e5b013d9965e1ec0ed", - "sha256:559b1a3a8be7395ded2943ea6c2135d096f8cc7039d6d12127110b6496f251fe", - "sha256:5de86c182667ec68cf84019aa0d8ceccf01d352cdca19bf9e373725204bdbf50", - "sha256:5fc069bb481fe3fad0ba24d3baaf69e22dfa6cc1b63290e6dfeaf4ac1e996fb7", - "sha256:6a19d654da49516296515d6f65de4bbcbd734bc57913b21a610cfc45e6df3ff1", - "sha256:7535b3e52f498270e7877dde1c8944d6b7720e93e2e66b89c82a11447b5818f5", - "sha256:7c4e495bcabc308198b8962e60ca12f53b27eb8f03a21ac1d2d711d6dd9ecfca", - "sha256:8a8fc4a0220367cb8370cedac02272d574079ccc32bffbb34d53aaf9e38b5060", - "sha256:8b008515e067232838daca020d1af628bf6520c8cc338bf383284efe6d8bd083", - "sha256:8d1684258e1385e459418f3429e107eec5fb3d75e1f5a8c52e5946b3f329d6ea", - "sha256:8eb5d54b87fb561dc2e00a5c5226c33ffe8dbc13f2e4033a412bafb7b37b194d", - "sha256:94cdef0c61bd014bb7af495e21a1c3a369dd0399c3cd1965b1502043f5c88d94", - "sha256:9d9f3be69c7a5e84c3549a8c4403fa9ac7672da456863d21e390b2bbf45ccad1", - "sha256:9fb6fb5975a448169756da2d124a1beb38c0924ff6c0306d883b6848a9980f38", - "sha256:a5eaae8700b87144d7dfb475aa4675e500ff707292caba3deff41609ddc5b845", - "sha256:aaeac2d552772b76d24eaff67a5d2325bc5205c74c0d4f9fbe71685d4a971db2", - "sha256:bb611e447559b3b5665e12a7da5160c0de6876097f62bf1d23ba66911564868e", - "sha256:bc0d41f4eb07da8b8d3ea85e50b62f6491ab313834db86ae2345be07536a4e5a", - "sha256:bf51051c129b847d1bb63a9b0826346b5f52fb821b15fe5e0d5ef86f268510f5", - "sha256:c948c034d8997526011960db54f512756fb0b4be1b81140a15b4ef094c6594a4", - "sha256:d435a01334157c3b126b4ee5141401d44bdc8440993b18b05e2f267a6647f92d", - "sha256:d46c1f95672b73288e08cdca181e14e84c6229b5879561b7b8cfd48374e09287", - "sha256:d5d58309b42064228b16b0311ff715d6c6e20230e81b35e8d0c8cfa1bbdecad8", - "sha256:dc6e2e91365a1dd6314d615d80291159c7981928b88a4c65654e3fefac83a836", - "sha256:e0dfb5f7a39029a6cbec23affa923b22a2c02207960fd66f109e01d6f632c1eb", - "sha256:eb4bf58d381b1373bd21d50837a53953d625d1693f1b58fed12743c75d3dd321", - "sha256:ebb211a85248dbc396b29320273c1ffde484b898852432613e8df0164c091006", - "sha256:ec759ece4786ae993a5b7dc3b3dead6e9375d89a6c65dfd6860076d2eb2abe7b", - "sha256:f55108397a8fa164268238c3e69cc134e945d1f693572a2f05a028b8d0d2b837", - "sha256:f6c706866d424ff285b85a02de7bbe5ed0ace227766b2c42cbe12f3d9ea5a8aa", - "sha256:f8370ad332b36fbad117440faf0dd4b910e80b9c49db5648afd337abdde9a1b6" - ], - "version": "==1.25.0" - }, - "invoke": { - "hashes": [ - "sha256:c52274d2e8a6d64ef0d61093e1983268ea1fc0cd13facb9448c4ef0c9a7ac7da", - "sha256:f4ec8a134c0122ea042c8912529f87652445d9f4de590b353d23f95bfa1f0efd", - "sha256:fc803a5c9052f15e63310aa81a43498d7c55542beb18564db88a9d75a176fa44" - ], - "version": "==1.3.0" - }, - "lxml": { - "hashes": [ - "sha256:02ca7bf899da57084041bb0f6095333e4d239948ad3169443f454add9f4e9cb4", - "sha256:096b82c5e0ea27ce9138bcbb205313343ee66a6e132f25c5ed67e2c8d960a1bc", - "sha256:0a920ff98cf1aac310470c644bc23b326402d3ef667ddafecb024e1713d485f1", - "sha256:1409b14bf83a7d729f92e2a7fbfe7ec929d4883ca071b06e95c539ceedb6497c", - "sha256:17cae1730a782858a6e2758fd20dd0ef7567916c47757b694a06ffafdec20046", - "sha256:17e3950add54c882e032527795c625929613adbd2ce5162b94667334458b5a36", - "sha256:1f4f214337f6ee5825bf90a65d04d70aab05526c08191ab888cb5149501923c5", - "sha256:2e8f77db25b0a96af679e64ff9bf9dddb27d379c9900c3272f3041c4d1327c9d", - "sha256:4dffd405390a45ecb95ab5ab1c1b847553c18b0ef8ed01e10c1c8b1a76452916", - "sha256:6b899931a5648862c7b88c795eddff7588fb585e81cecce20f8d9da16eff96e0", - "sha256:726c17f3e0d7a7200718c9a890ccfeab391c9133e363a577a44717c85c71db27", - "sha256:760c12276fee05c36f95f8040180abc7fbebb9e5011447a97cdc289b5d6ab6fc", - "sha256:796685d3969815a633827c818863ee199440696b0961e200b011d79b9394bbe7", - "sha256:891fe897b49abb7db470c55664b198b1095e4943b9f82b7dcab317a19116cd38", - "sha256:9277562f175d2334744ad297568677056861070399cec56ff06abbe2564d1232", - "sha256:a471628e20f03dcdfde00770eeaf9c77811f0c331c8805219ca7b87ac17576c5", - "sha256:a63b4fd3e2cabdcc9d918ed280bdde3e8e9641e04f3c59a2a3109644a07b9832", - "sha256:ae88588d687bd476be588010cbbe551e9c2872b816f2da8f01f6f1fda74e1ef0", - "sha256:b0b84408d4eabc6de9dd1e1e0bc63e7731e890c0b378a62443e5741cfd0ae90a", - "sha256:be78485e5d5f3684e875dab60f40cddace2f5b2a8f7fede412358ab3214c3a6f", - "sha256:c27eaed872185f047bb7f7da2d21a7d8913457678c9a100a50db6da890bc28b9", - "sha256:c7fccd08b14aa437fe096c71c645c0f9be0655a9b1a4b7cffc77bcb23b3d61d2", - "sha256:c81cb40bff373ab7a7446d6bbca0190bccc5be3448b47b51d729e37799bb5692", - "sha256:d11874b3c33ee441059464711cd365b89fa1a9cf19ae75b0c189b01fbf735b84", - "sha256:e9c028b5897901361d81a4718d1db217b716424a0283afe9d6735fe0caf70f79", - "sha256:fe489d486cd00b739be826e8c1be188ddb74c7a1ca784d93d06fda882a6a1681" - ], - "version": "==4.4.1" - }, - "netaddr": { - "hashes": [ - "sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd", - "sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca" - ], - "index": "pypi", - "version": "==0.7.19" - }, - "paramiko": { - "hashes": [ - "sha256:99f0179bdc176281d21961a003ffdb2ec369daac1a1007241f53374e376576cf", - "sha256:f4b2edfa0d226b70bd4ca31ea7e389325990283da23465d572ed1f70a7583041" - ], - "version": "==2.6.0" - }, - "pillow": { - "hashes": [ - "sha256:047d9473cf68af50ac85f8ee5d5f21a60f849bc17d348da7fc85711287a75031", - "sha256:0f66dc6c8a3cc319561a633b6aa82c44107f12594643efa37210d8c924fc1c71", - "sha256:12c9169c4e8fe0a7329e8658c7e488001f6b4c8e88740e76292c2b857af2e94c", - "sha256:248cffc168896982f125f5c13e9317c059f74fffdb4152893339f3be62a01340", - "sha256:27faf0552bf8c260a5cee21a76e031acaea68babb64daf7e8f2e2540745082aa", - "sha256:285edafad9bc60d96978ed24d77cdc0b91dace88e5da8c548ba5937c425bca8b", - "sha256:384b12c9aa8ef95558abdcb50aada56d74bc7cc131dd62d28c2d0e4d3aadd573", - "sha256:38950b3a707f6cef09cd3cbb142474357ad1a985ceb44d921bdf7b4647b3e13e", - "sha256:4aad1b88933fd6dc2846552b89ad0c74ddbba2f0884e2c162aa368374bf5abab", - "sha256:4ac6148008c169603070c092e81f88738f1a0c511e07bd2bb0f9ef542d375da9", - "sha256:4deb1d2a45861ae6f0b12ea0a786a03d19d29edcc7e05775b85ec2877cb54c5e", - "sha256:59aa2c124df72cc75ed72c8d6005c442d4685691a30c55321e00ed915ad1a291", - "sha256:5a47d2123a9ec86660fe0e8d0ebf0aa6bc6a17edc63f338b73ea20ba11713f12", - "sha256:5cc901c2ab9409b4b7ac7b5bcc3e86ac14548627062463da0af3b6b7c555a871", - "sha256:6c1db03e8dff7b9f955a0fb9907eb9ca5da75b5ce056c0c93d33100a35050281", - "sha256:7ce80c0a65a6ea90ef9c1f63c8593fcd2929448613fc8da0adf3e6bfad669d08", - "sha256:809c19241c14433c5d6135e1b6c72da4e3b56d5c865ad5736ab99af8896b8f41", - "sha256:83792cb4e0b5af480588601467c0764242b9a483caea71ef12d22a0d0d6bdce2", - "sha256:846fa202bd7ee0f6215c897a1d33238ef071b50766339186687bd9b7a6d26ac5", - "sha256:9f5529fc02009f96ba95bea48870173426879dc19eec49ca8e08cd63ecd82ddb", - "sha256:a423c2ea001c6265ed28700df056f75e26215fd28c001e93ef4380b0f05f9547", - "sha256:ac4428094b42907aba5879c7c000d01c8278d451a3b7cccd2103e21f6397ea75", - "sha256:b1ae48d87f10d1384e5beecd169c77502fcc04a2c00a4c02b85f0a94b419e5f9", - "sha256:bf4e972a88f8841d8fdc6db1a75e0f8d763e66e3754b03006cbc3854d89f1cb1", - "sha256:c6414f6aad598364aaf81068cabb077894eb88fed99c6a65e6e8217bab62ae7a", - "sha256:c710fcb7ee32f67baf25aa9ffede4795fd5d93b163ce95fdc724383e38c9df96", - "sha256:c7be4b8a09852291c3c48d3c25d1b876d2494a0a674980089ac9d5e0d78bd132", - "sha256:c9e5ffb910b14f090ac9c38599063e354887a5f6d7e6d26795e916b4514f2c1a", - "sha256:e0697b826da6c2472bb6488db4c0a7fa8af0d52fa08833ceb3681358914b14e5", - "sha256:e9a3edd5f714229d41057d56ac0f39ad9bdba6767e8c888c951869f0bdd129b0" - ], - "version": "==6.2.1" - }, - "protobuf": { - "hashes": [ - "sha256:125713564d8cfed7610e52444c9769b8dcb0b55e25cc7841f2290ee7bc86636f", - "sha256:1accdb7a47e51503be64d9a57543964ba674edac103215576399d2d0e34eac77", - "sha256:27003d12d4f68e3cbea9eb67427cab3bfddd47ff90670cb367fcd7a3a89b9657", - "sha256:3264f3c431a631b0b31e9db2ae8c927b79fc1a7b1b06b31e8e5bcf2af91fe896", - "sha256:3c5ab0f5c71ca5af27143e60613729e3488bb45f6d3f143dc918a20af8bab0bf", - "sha256:45dcf8758873e3f69feab075e5f3177270739f146255225474ee0b90429adef6", - "sha256:56a77d61a91186cc5676d8e11b36a5feb513873e4ae88d2ee5cf530d52bbcd3b", - "sha256:5984e4947bbcef5bd849d6244aec507d31786f2dd3344139adc1489fb403b300", - "sha256:6b0441da73796dd00821763bb4119674eaf252776beb50ae3883bed179a60b2a", - "sha256:6f6677c5ade94d4fe75a912926d6796d5c71a2a90c2aeefe0d6f211d75c74789", - "sha256:84a825a9418d7196e2acc48f8746cf1ee75877ed2f30433ab92a133f3eaf8fbe", - "sha256:b842c34fe043ccf78b4a6cf1019d7b80113707d68c88842d061fa2b8fb6ddedc", - "sha256:ca33d2f09dae149a1dcf942d2d825ebb06343b77b437198c9e2ef115cf5d5bc1", - "sha256:db83b5c12c0cd30150bb568e6feb2435c49ce4e68fe2d7b903113f0e221e58fe", - "sha256:f50f3b1c5c1c1334ca7ce9cad5992f098f460ffd6388a3cabad10b66c2006b09", - "sha256:f99f127909731cafb841c52f9216e447d3e4afb99b17bebfad327a75aee206de" - ], - "version": "==3.10.0" - }, - "pycparser": { - "hashes": [ - "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" - ], - "version": "==2.19" - }, - "pynacl": { - "hashes": [ - "sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255", - "sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c", - "sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e", - "sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae", - "sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621", - "sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56", - "sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39", - "sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310", - "sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1", - "sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5", - "sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a", - "sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786", - "sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b", - "sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b", - "sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f", - "sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20", - "sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415", - "sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715", - "sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92", - "sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1", - "sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0" - ], - "version": "==1.3.0" - }, - "pyyaml": { - "hashes": [ - "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", - "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", - "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", - "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", - "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", - "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", - "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", - "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", - "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", - "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", - "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", - "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", - "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" - ], - "index": "pypi", - "version": "==5.1.2" - }, - "six": { - "hashes": [ - "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", - "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" - ], - "version": "==1.13.0" - } - }, - "develop": { - "appdirs": { - "hashes": [ - "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", - "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" - ], - "version": "==1.4.3" - }, - "aspy.yaml": { - "hashes": [ - "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc", - "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45" - ], - "version": "==1.3.0" - }, - "attrs": { - "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" - ], - "version": "==19.3.0" - }, - "black": { - "hashes": [ - "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", - "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c" - ], - "index": "pypi", - "version": "==19.3b0" - }, - "cfgv": { - "hashes": [ - "sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144", - "sha256:fbd93c9ab0a523bf7daec408f3be2ed99a980e20b2d19b50fc184ca6b820d289" - ], - "version": "==2.0.1" - }, - "click": { - "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" - ], - "version": "==7.0" - }, - "entrypoints": { - "hashes": [ - "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", - "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" - ], - "version": "==0.3" - }, - "flake8": { - "hashes": [ - "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", - "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" - ], - "index": "pypi", - "version": "==3.7.9" - }, - "identify": { - "hashes": [ - "sha256:4f1fe9a59df4e80fcb0213086fcf502bc1765a01ea4fe8be48da3b65afd2a017", - "sha256:d8919589bd2a5f99c66302fec0ef9027b12ae150b0b0213999ad3f695fc7296e" - ], - "version": "==1.4.7" - }, - "importlib-metadata": { - "hashes": [ - "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", - "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" - ], - "markers": "python_version < '3.8'", - "version": "==0.23" - }, - "importlib-resources": { - "hashes": [ - "sha256:6e2783b2538bd5a14678284a3962b0660c715e5a0f10243fd5e00a4b5974f50b", - "sha256:d3279fd0f6f847cced9f7acc19bd3e5df54d34f93a2e7bb5f238f81545787078" - ], - "markers": "python_version < '3.7'", - "version": "==1.0.2" - }, - "isort": { - "hashes": [ - "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", - "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" - ], - "index": "pypi", - "version": "==4.3.21" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "more-itertools": { - "hashes": [ - "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", - "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" - ], - "version": "==7.2.0" - }, - "nodeenv": { - "hashes": [ - "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a" - ], - "version": "==1.3.3" - }, - "pre-commit": { - "hashes": [ - "sha256:9f152687127ec90642a2cc3e4d9e1e6240c4eb153615cb02aa1ad41d331cbb6e", - "sha256:c2e4810d2d3102d354947907514a78c5d30424d299dc0fe48f5aa049826e9b50" - ], - "index": "pypi", - "version": "==1.20.0" - }, - "pycodestyle": { - "hashes": [ - "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", - "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" - ], - "version": "==2.5.0" - }, - "pyflakes": { - "hashes": [ - "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", - "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" - ], - "version": "==2.1.1" - }, - "pyyaml": { - "hashes": [ - "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", - "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", - "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", - "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", - "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", - "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", - "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", - "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", - "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", - "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", - "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", - "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", - "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" - ], - "index": "pypi", - "version": "==5.1.2" - }, - "six": { - "hashes": [ - "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", - "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" - ], - "version": "==1.13.0" - }, - "toml": { - "hashes": [ - "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" - ], - "version": "==0.10.0" - }, - "virtualenv": { - "hashes": [ - "sha256:11cb4608930d5fd3afb545ecf8db83fa50e1f96fc4fca80c94b07d2c83146589", - "sha256:d257bb3773e48cac60e475a19b608996c73f4d333b3ba2e4e57d5ac6134e0136" - ], - "version": "==16.7.7" - }, - "zipp": { - "hashes": [ - "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", - "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" - ], - "version": "==0.6.0" - } - } -} diff --git a/coretk/setup.cfg b/coretk/setup.cfg deleted file mode 100644 index d9228b5f..00000000 --- a/coretk/setup.cfg +++ /dev/null @@ -1,15 +0,0 @@ -[aliases] -test=pytest - -[isort] -multi_line_output=3 -include_trailing_comma=True -force_grid_wrap=0 -use_parentheses=True -line_length=88 - -[flake8] -ignore=E501,W503,E203 -max-line-length=100 -max-complexity=26 -select=B,C,E,F,W,T4 diff --git a/coretk/setup.py b/coretk/setup.py deleted file mode 100644 index 846ab074..00000000 --- a/coretk/setup.py +++ /dev/null @@ -1,12 +0,0 @@ -from setuptools import find_packages, setup - -setup( - name="coretk", - version="0.1.0", - packages=find_packages(), - install_requires=["netaddr", "pillow"], - description="CORE GUI", - url="https://github.com/coreemu/core", - author="Boeing Research & Technology", - license="BSD", -) From f5ce7b1d3171471ba15b5d9b0e89108b965d65e8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 19 Dec 2019 09:37:49 -0800 Subject: [PATCH 423/462] updated requirements.txt with gui packages --- daemon/requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/daemon/requirements.txt b/daemon/requirements.txt index 96fe83ca..b700247f 100644 --- a/daemon/requirements.txt +++ b/daemon/requirements.txt @@ -3,4 +3,7 @@ grpcio==1.23.0 grpcio-tools==1.21.1 invoke==1.3.0 lxml==4.4.1 +netaddr==0.7.19 +Pillow==6.2.1 protobuf==3.9.1 +PyYAML==5.2 From 1114e4b975715cd1a3664f35b905e02548854f9d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 19 Dec 2019 09:39:18 -0800 Subject: [PATCH 424/462] removed coretk specific github action --- .github/workflows/coretk-checks.yml | 35 ----------------------------- 1 file changed, 35 deletions(-) delete mode 100644 .github/workflows/coretk-checks.yml diff --git a/.github/workflows/coretk-checks.yml b/.github/workflows/coretk-checks.yml deleted file mode 100644 index e99ed214..00000000 --- a/.github/workflows/coretk-checks.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: CORE Tk Checks - -on: [push] - -jobs: - build: - runs-on: ubuntu-18.04 - steps: - - uses: actions/checkout@v1 - - name: Set up Python 3.7 - uses: actions/setup-python@v1 - with: - python-version: 3.7 - - name: Install pipenv - run: | - python -m pip install --upgrade pip - pip install pipenv - cd daemon - cp setup.py.in setup.py - cp core/constants.py.in core/constants.py - sed -i 's/True/False/g' core/constants.py - cd ../coretk - pipenv install --dev - - name: isort - run: | - cd coretk - pipenv run isort -c - - name: black - run: | - cd coretk - pipenv run black --check . - - name: flake8 - run: | - cd coretk - pipenv run flake8 From 05d3b58c5f179f8280858f49d204ae390ac56bdb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 19 Dec 2019 09:50:58 -0800 Subject: [PATCH 425/462] updating dialog based classes to have dialog in name --- daemon/core/gui/dialogs/canvaswallpaper.py | 2 +- daemon/core/gui/dialogs/colorpicker.py | 2 +- daemon/core/gui/dialogs/linkconfig.py | 6 +++--- daemon/core/gui/dialogs/marker.py | 6 +++--- daemon/core/gui/dialogs/nodeservice.py | 6 +++--- .../dialogs/{serviceconfiguration.py => serviceconfig.py} | 2 +- daemon/core/gui/dialogs/shapemod.py | 8 ++++---- daemon/core/gui/graph/edges.py | 4 ++-- daemon/core/gui/graph/node.py | 4 ++-- daemon/core/gui/menuaction.py | 4 ++-- daemon/core/gui/toolbar.py | 6 +++--- 11 files changed, 25 insertions(+), 25 deletions(-) rename daemon/core/gui/dialogs/{serviceconfiguration.py => serviceconfig.py} (99%) diff --git a/daemon/core/gui/dialogs/canvaswallpaper.py b/daemon/core/gui/dialogs/canvaswallpaper.py index f3635377..62fa8fe5 100644 --- a/daemon/core/gui/dialogs/canvaswallpaper.py +++ b/daemon/core/gui/dialogs/canvaswallpaper.py @@ -12,7 +12,7 @@ from core.gui.themes import PADX, PADY from core.gui.widgets import image_chooser -class CanvasBackgroundDialog(Dialog): +class CanvasWallpaperDialog(Dialog): def __init__(self, master, app): """ create an instance of CanvasWallpaper object diff --git a/daemon/core/gui/dialogs/colorpicker.py b/daemon/core/gui/dialogs/colorpicker.py index b94ae635..28d21f42 100644 --- a/daemon/core/gui/dialogs/colorpicker.py +++ b/daemon/core/gui/dialogs/colorpicker.py @@ -8,7 +8,7 @@ from tkinter import ttk from core.gui.dialogs.dialog import Dialog -class ColorPicker(Dialog): +class ColorPickerDialog(Dialog): def __init__(self, master, app, initcolor="#000000"): super().__init__(master, app, "color picker", modal=True) self.red_entry = None diff --git a/daemon/core/gui/dialogs/linkconfig.py b/daemon/core/gui/dialogs/linkconfig.py index b59307d6..9fd9130b 100644 --- a/daemon/core/gui/dialogs/linkconfig.py +++ b/daemon/core/gui/dialogs/linkconfig.py @@ -6,7 +6,7 @@ import tkinter as tk from tkinter import ttk from core.api.grpc import core_pb2 -from core.gui.dialogs.colorpicker import ColorPicker +from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.dialog import Dialog from core.gui.themes import PADX, PADY @@ -27,7 +27,7 @@ def get_float(var): return None -class LinkConfiguration(Dialog): +class LinkConfigurationDialog(Dialog): def __init__(self, master, app, edge): super().__init__(master, app, "Link Configuration", modal=True) self.app = app @@ -237,7 +237,7 @@ class LinkConfiguration(Dialog): return frame def click_color(self): - dialog = ColorPicker(self, self.app, self.color.get()) + dialog = ColorPickerDialog(self, self.app, self.color.get()) color = dialog.askcolor() self.color.set(color) self.color_button.config(background=color) diff --git a/daemon/core/gui/dialogs/marker.py b/daemon/core/gui/dialogs/marker.py index 3a078d86..dc0c3bc4 100644 --- a/daemon/core/gui/dialogs/marker.py +++ b/daemon/core/gui/dialogs/marker.py @@ -6,13 +6,13 @@ import logging import tkinter as tk from tkinter import ttk -from core.gui.dialogs.colorpicker import ColorPicker +from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.dialog import Dialog MARKER_THICKNESS = [3, 5, 8, 10] -class Marker(Dialog): +class MarkerDialog(Dialog): def __init__(self, master, app, initcolor="#000000"): super().__init__(master, app, "marker tool", modal=False) self.app = app @@ -56,7 +56,7 @@ class Marker(Dialog): canvas.delete(i) def change_color(self, event): - color_picker = ColorPicker(self, self.app, self.color) + color_picker = ColorPickerDialog(self, self.app, self.color) color = color_picker.askcolor() event.widget.configure(background=color) self.color = color diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index 730a5ff9..2bbe0589 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -5,12 +5,12 @@ import tkinter as tk from tkinter import messagebox, ttk from core.gui.dialogs.dialog import Dialog -from core.gui.dialogs.serviceconfiguration import ServiceConfiguration +from core.gui.dialogs.serviceconfig import ServiceConfigDialog from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CheckboxList, ListboxScroll -class NodeService(Dialog): +class NodeServiceDialog(Dialog): def __init__(self, master, app, canvas_node, services=None): title = f"{canvas_node.core_node.name} Services" super().__init__(master, app, title, modal=True) @@ -106,7 +106,7 @@ class NodeService(Dialog): def click_configure(self): current_selection = self.current.listbox.curselection() if len(current_selection): - dialog = ServiceConfiguration( + dialog = ServiceConfigDialog( master=self, app=self.app, service_name=self.current.listbox.get(current_selection[0]), diff --git a/daemon/core/gui/dialogs/serviceconfiguration.py b/daemon/core/gui/dialogs/serviceconfig.py similarity index 99% rename from daemon/core/gui/dialogs/serviceconfiguration.py rename to daemon/core/gui/dialogs/serviceconfig.py index f0d5b19b..fd4f8c26 100644 --- a/daemon/core/gui/dialogs/serviceconfiguration.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -13,7 +13,7 @@ from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CodeText, ListboxScroll -class ServiceConfiguration(Dialog): +class ServiceConfigDialog(Dialog): def __init__(self, master, app, service_name, node_id): title = f"{service_name} Service" super().__init__(master, app, title, modal=True) diff --git a/daemon/core/gui/dialogs/shapemod.py b/daemon/core/gui/dialogs/shapemod.py index f77b45a8..8dc56f43 100644 --- a/daemon/core/gui/dialogs/shapemod.py +++ b/daemon/core/gui/dialogs/shapemod.py @@ -4,7 +4,7 @@ shape input dialog import tkinter as tk from tkinter import font, ttk -from core.gui.dialogs.colorpicker import ColorPicker +from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.dialog import Dialog from core.gui.graph import tags from core.gui.graph.shapeutils import is_draw_shape, is_shape_text @@ -135,18 +135,18 @@ class ShapeDialog(Dialog): button.grid(row=0, column=1, sticky="ew") def choose_text_color(self): - color_picker = ColorPicker(self, self.app, "#000000") + color_picker = ColorPickerDialog(self, self.app, "#000000") color = color_picker.askcolor() self.text_color = color def choose_fill_color(self): - color_picker = ColorPicker(self, self.app, self.fill_color) + color_picker = ColorPickerDialog(self, self.app, self.fill_color) color = color_picker.askcolor() self.fill_color = color self.fill.config(background=color, text=color) def choose_border_color(self): - color_picker = ColorPicker(self, self.app, self.border_color) + color_picker = ColorPickerDialog(self, self.app, self.border_color) color = color_picker.askcolor() self.border_color = color self.border.config(background=color, text=color) diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index b80c106c..ebcda49e 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -3,7 +3,7 @@ import tkinter as tk from tkinter.font import Font from core.gui import themes -from core.gui.dialogs.linkconfig import LinkConfiguration +from core.gui.dialogs.linkconfig import LinkConfigurationDialog from core.gui.graph import tags from core.gui.nodeutils import NodeUtils @@ -177,5 +177,5 @@ class CanvasEdge: def configure(self): logging.debug("link configuration") - dialog = LinkConfiguration(self.canvas, self.canvas.app, self) + dialog = LinkConfigurationDialog(self.canvas, self.canvas.app, self) dialog.show() diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 68405a37..b348fe15 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -9,7 +9,7 @@ from core.gui import themes from core.gui.dialogs.emaneconfig import EmaneConfigDialog from core.gui.dialogs.mobilityconfig import MobilityConfigDialog from core.gui.dialogs.nodeconfig import NodeConfigDialog -from core.gui.dialogs.nodeservice import NodeService +from core.gui.dialogs.nodeservice import NodeServiceDialog from core.gui.dialogs.wlanconfig import WlanConfigDialog from core.gui.errors import show_grpc_error from core.gui.graph import tags @@ -243,7 +243,7 @@ class CanvasNode: def show_services(self): self.canvas.context = None - dialog = NodeService(self.app.master, self.app, self) + dialog = NodeServiceDialog(self.app.master, self.app, self) dialog.show() def has_emane_link(self, interface_id): diff --git a/daemon/core/gui/menuaction.py b/daemon/core/gui/menuaction.py index 4c310cd7..be532218 100644 --- a/daemon/core/gui/menuaction.py +++ b/daemon/core/gui/menuaction.py @@ -13,7 +13,7 @@ import grpc from core.gui.appconfig import XMLS_PATH from core.gui.dialogs.about import AboutDialog from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog -from core.gui.dialogs.canvaswallpaper import CanvasBackgroundDialog +from core.gui.dialogs.canvaswallpaper import CanvasWallpaperDialog from core.gui.dialogs.hooks import HooksDialog from core.gui.dialogs.observers import ObserverDialog from core.gui.dialogs.preferences import PreferencesDialog @@ -113,7 +113,7 @@ class MenuAction: dialog.show() def canvas_set_wallpaper(self): - dialog = CanvasBackgroundDialog(self.app, self.app) + dialog = CanvasWallpaperDialog(self.app, self.app) dialog.show() def help_core_github(self): diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 2d0dad94..7c1db24c 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -6,7 +6,7 @@ from tkinter import ttk from tkinter.font import Font from core.gui.dialogs.customnodes import CustomNodesDialog -from core.gui.dialogs.marker import Marker +from core.gui.dialogs.marker import MarkerDialog from core.gui.graph import tags from core.gui.graph.enums import GraphMode from core.gui.graph.shapeutils import ShapeType, is_marker @@ -407,7 +407,7 @@ class Toolbar(ttk.Frame): self.app.canvas.annotation_type = shape_type if is_marker(shape_type): if not self.marker_tool: - self.marker_tool = Marker(self.master, self.app) + self.marker_tool = MarkerDialog(self.master, self.app) self.marker_tool.show() def click_run_button(self): @@ -418,7 +418,7 @@ class Toolbar(ttk.Frame): def click_marker_button(self): logging.debug("Click on marker button") - dialog = Marker(self.master, self.app) + dialog = MarkerDialog(self.master, self.app) dialog.show() # dialog.position() From 5a81adc653d44fbaace1ffb0652d128ffe43fab9 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 19 Dec 2019 10:58:22 -0800 Subject: [PATCH 426/462] some fix one paint tool --- daemon/core/gui/dialogs/marker.py | 8 +++++++- daemon/core/gui/graph/graph.py | 1 + daemon/core/gui/toolbar.py | 32 ++++++++++++++++++++++--------- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/daemon/core/gui/dialogs/marker.py b/daemon/core/gui/dialogs/marker.py index 3a078d86..10ada358 100644 --- a/daemon/core/gui/dialogs/marker.py +++ b/daemon/core/gui/dialogs/marker.py @@ -70,4 +70,10 @@ class Marker(Dialog): def position(self): print(self.winfo_width(), self.winfo_height()) - self.geometry("+{}+{}".format(self.app.master.winfo_x, self.app.master.winfo_y)) + # print(self.app.master.winfo_x(), self.app.master.winfo_y()) + print(self.app.canvas.winfo_rootx()) + self.geometry( + "+{}+{}".format( + self.app.canvas.winfo_rootx(), self.app.canvas.master.winfo_rooty() + ) + ) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 4b481a68..6b8c55ef 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -508,6 +508,7 @@ class CanvasGraph(tk.Canvas): self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) if self.mode == GraphMode.ANNOTATION: + if is_marker(self.annotation_type): r = self.app.toolbar.marker_tool.radius self.create_oval( diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 2d0dad94..910e1fab 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -50,6 +50,11 @@ class Toolbar(ttk.Frame): # runtime buttons self.runtime_select_button = None + self.stop_button = None + self.plot_button = None + self.runtime_marker_button = None + self.node_command_button = None + self.run_command_button = None # frames self.design_frame = None @@ -106,6 +111,11 @@ class Toolbar(ttk.Frame): def runtime_select(self, button): logging.info("selecting runtime button: %s", button) self.runtime_select_button.state(["!pressed"]) + self.stop_button.state(["!pressed"]) + self.plot_button.state(["!pressed"]) + self.runtime_marker_button.state(["!pressed"]) + self.node_command_button.state(["!pressed"]) + self.run_command_button.state(["!pressed"]) button.state(["pressed"]) def draw_runtime_frame(self): @@ -113,7 +123,7 @@ class Toolbar(ttk.Frame): self.runtime_frame.grid(row=0, column=0, sticky="nsew") self.runtime_frame.columnconfigure(0, weight=1) - self.create_button( + self.stop_button = self.create_button( self.runtime_frame, icon(ImageEnum.STOP), self.click_stop, @@ -125,23 +135,22 @@ class Toolbar(ttk.Frame): self.click_runtime_selection, "selection tool", ) - # self.create_observe_button() - self.create_button( + self.plot_button = self.create_button( self.runtime_frame, icon(ImageEnum.PLOT), self.click_plot_button, "plot" ) - self.create_button( + self.runtime_marker_button = self.create_button( self.runtime_frame, icon(ImageEnum.MARKER), self.click_marker_button, "marker", ) - self.create_button( + self.node_command_button = self.create_button( self.runtime_frame, icon(ImageEnum.TWONODE), self.click_two_node_button, "run command from one node to another", ) - self.create_button( + self.run_command_button = self.create_button( self.runtime_frame, icon(ImageEnum.RUN), self.click_run_button, "run" ) @@ -409,6 +418,7 @@ class Toolbar(ttk.Frame): if not self.marker_tool: self.marker_tool = Marker(self.master, self.app) self.marker_tool.show() + self.marker_tool.position() def click_run_button(self): logging.debug("Click on RUN button") @@ -418,9 +428,13 @@ class Toolbar(ttk.Frame): def click_marker_button(self): logging.debug("Click on marker button") - dialog = Marker(self.master, self.app) - dialog.show() - # dialog.position() + self.runtime_select(self.runtime_marker_button) + self.app.canvas.mode = GraphMode.ANNOTATION + self.app.canvas.annotation_type = ShapeType.MARKER + if not self.marker_tool: + self.marker_tool = Marker(self.master, self.app) + self.marker_tool.show() + self.marker_tool.position() def click_two_node_button(self): logging.debug("Click TWONODE button") From 105825808d082ae0972a013cccb3899b4c572dd1 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 19 Dec 2019 11:10:08 -0800 Subject: [PATCH 427/462] fixed edge refactoring issue when finding next subnet --- daemon/core/gui/interface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index ec0bcec5..fe08b0dd 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -21,7 +21,8 @@ class InterfaceManager: def next_subnet(self): # define currently used subnets used_subnets = set() - for link in self.app.core.links.values(): + for edge in self.app.core.links.values(): + link = edge.link if link.HasField("interface_one"): subnet = self.get_subnet(link.interface_one) used_subnets.add(subnet) From 395f8134dcf78253044db3e9ba0bbf8dc98776f5 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 19 Dec 2019 11:30:27 -0800 Subject: [PATCH 428/462] adjust alert table size --- daemon/core/gui/dialogs/alerts.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/daemon/core/gui/dialogs/alerts.py b/daemon/core/gui/dialogs/alerts.py index 2fb94a4f..cb91881c 100644 --- a/daemon/core/gui/dialogs/alerts.py +++ b/daemon/core/gui/dialogs/alerts.py @@ -37,13 +37,13 @@ class AlertsDialog(Dialog): self.tree.grid(row=0, column=0, sticky="nsew") self.tree.column("time", stretch=tk.YES) self.tree.heading("time", text="Time") - self.tree.column("level", stretch=tk.YES) + self.tree.column("level", stretch=tk.YES, width=100) self.tree.heading("level", text="Level") - self.tree.column("session_id", stretch=tk.YES) + self.tree.column("session_id", stretch=tk.YES, width=100) self.tree.heading("session_id", text="Session ID") - self.tree.column("node", stretch=tk.YES) + self.tree.column("node", stretch=tk.YES, width=100) self.tree.heading("node", text="Node") - self.tree.column("source", stretch=tk.YES) + self.tree.column("source", stretch=tk.YES, width=100) self.tree.heading("source", text="Source") self.tree.bind("<>", self.click_select) @@ -77,7 +77,7 @@ class AlertsDialog(Dialog): self.tree.configure(xscrollcommand=xscrollbar.set) self.codetext = CodeText(self.top) - self.codetext.text.config(state=tk.DISABLED) + self.codetext.text.config(state=tk.DISABLED, height=11) self.codetext.grid(sticky="nsew", pady=PADY) frame = ttk.Frame(self.top) From 793d3406681d0594d657a32c9a610cf05f105a44 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 19 Dec 2019 11:32:59 -0800 Subject: [PATCH 429/462] removed mock from setup.py.in as its not needed in python3, added formal script to run coretk gui and remove main line from core/gui/app.py --- daemon/Pipfile | 2 +- daemon/core/gui/app.py | 10 ---------- daemon/core/gui/graph/graph.py | 4 ++-- daemon/scripts/coretk-gui | 14 ++++++++++++++ daemon/setup.py.in | 1 - 5 files changed, 17 insertions(+), 14 deletions(-) create mode 100755 daemon/scripts/coretk-gui diff --git a/daemon/Pipfile b/daemon/Pipfile index 8066415c..d55b248f 100644 --- a/daemon/Pipfile +++ b/daemon/Pipfile @@ -5,7 +5,7 @@ verify_ssl = true [scripts] core = "python scripts/core-daemon -f data/core.conf -l data/logging.conf" -coretk = "python core/gui/app.py" +coretk = "python scripts/coretk-gui" test = "pytest -v tests" test-mock = "pytest -v --mock tests" test-emane = "pytest -v tests/emane" diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index cbc44930..969292fe 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -1,4 +1,3 @@ -import logging import tkinter as tk from tkinter import ttk @@ -96,12 +95,3 @@ class Application(tk.Frame): def close(self): self.master.destroy() - - -if __name__ == "__main__": - log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s" - logging.basicConfig(level=logging.DEBUG, format=log_format) - Images.load_all() - appconfig.check_directory() - app = Application() - app.mainloop() diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 4b481a68..b7220319 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -356,7 +356,7 @@ class CanvasGraph(tk.Canvas): return # edge dst must be a node - logging.debug(f"current selected: {self.selected}") + logging.debug("current selected: %s", self.selected) dst_node = self.nodes.get(self.selected) if not dst_node: edge.delete() @@ -631,7 +631,7 @@ class CanvasGraph(tk.Canvas): selected = self.get_selected(event) canvas_node = self.nodes.get(selected) if canvas_node: - logging.debug(f"node context: {selected}") + logging.debug("node context: %s", selected) self.context = canvas_node.create_context() self.context.post(event.x_root, event.y_root) else: diff --git a/daemon/scripts/coretk-gui b/daemon/scripts/coretk-gui new file mode 100755 index 00000000..a87df4fc --- /dev/null +++ b/daemon/scripts/coretk-gui @@ -0,0 +1,14 @@ +#!/usr/bin/env python +import logging + +from core.gui import appconfig +from core.gui.app import Application +from core.gui.images import Images + +if __name__ == "__main__": + log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s" + logging.basicConfig(level=logging.DEBUG, format=log_format) + Images.load_all() + appconfig.check_directory() + app = Application() + app.mainloop() diff --git a/daemon/setup.py.in b/daemon/setup.py.in index 8539a1fe..5f3ca41d 100644 --- a/daemon/setup.py.in +++ b/daemon/setup.py.in @@ -45,7 +45,6 @@ setup( ], tests_require=[ "pytest", - "mock", ], data_files=data_files, scripts=glob.glob("scripts/*"), From 298cd2c9d3e8618094c239f7457c5e4276ef3549 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 19 Dec 2019 12:32:30 -0800 Subject: [PATCH 430/462] small update to devguide, updates to READMEs for LXD and Docker --- daemon/examples/docker/README.md | 24 ++++++++-- daemon/examples/lxd/README.md | 24 ++++++++-- docs/devguide.md | 78 +++++++------------------------- 3 files changed, 57 insertions(+), 69 deletions(-) diff --git a/daemon/examples/docker/README.md b/daemon/examples/docker/README.md index fac8d846..3c2b1372 100644 --- a/daemon/examples/docker/README.md +++ b/daemon/examples/docker/README.md @@ -4,13 +4,13 @@ Information on how Docker can be leveraged and included to create nodes based on Docker containers and images to interface with existing CORE nodes, when needed. -# Installation +## Installation ```shell sudo apt install docker.io ``` -# Configuration +## Configuration Custom configuration required to avoid iptable rules being added and removing the need for the default docker network, since core will be orchestrating @@ -19,12 +19,28 @@ connections between nodes. Place the file below in **/etc/docker/** * daemon.json -# Tools and Versions Tested With +## Group Setup + +To use Docker nodes within the python GUI, you will need to make sure the user running the GUI is a member of the +docker group. + +```shell +# add group if does not exist +sudo groupadd docker + +# add user to group +sudo usermod -aG docker $USER + +# to get this change to take effect, log out and back in or run the following +newgrp docker +``` + +## Tools and Versions Tested With * Docker version 18.09.5, build e8ff056 * nsenter from util-linux 2.31.1 -# Examples +## Examples This directory provides a few small examples creating Docker nodes and linking them to themselves or with standard CORE nodes. diff --git a/daemon/examples/lxd/README.md b/daemon/examples/lxd/README.md index 25d91ecd..4e3952ee 100644 --- a/daemon/examples/lxd/README.md +++ b/daemon/examples/lxd/README.md @@ -4,13 +4,13 @@ Information on how LXD can be leveraged and included to create nodes based on LXC containers and images to interface with existing CORE nodes, when needed. -# Installation +## Installation ```shell sudo snap install lxd ``` -# Configuration +## Configuration Initialize LXD and say no to adding a default bridge. @@ -18,12 +18,28 @@ Initialize LXD and say no to adding a default bridge. sudo lxd init ``` -# Tools and Versions Tested With +## Group Setup + +To use LXC nodes within the python GUI, you will need to make sure the user running the GUI is a member of the +lxd group. + +```shell +# add group if does not exist +sudo groupadd lxd + +# add user to group +sudo usermod -aG lxd $USER + +# to get this change to take effect, log out and back in or run the following +newgrp lxd +``` + +## Tools and Versions Tested With * LXD 3.14 * nsenter from util-linux 2.31.1 -# Examples +## Examples This directory provides a few small examples creating LXC nodes using LXD and linking them to themselves or with standard CORE nodes. diff --git a/docs/devguide.md b/docs/devguide.md index ca207a82..34ea1536 100644 --- a/docs/devguide.md +++ b/docs/devguide.md @@ -10,7 +10,6 @@ Current development focuses on the Python modules and daemon. Here is a brief de | Directory | Description | |---|---| -|corefx|JavaFX based GUI using gRPC API to replace legacy GUI| |daemon|Python CORE daemon code that handles receiving API calls and creating containers| |docs|Markdown Documentation currently hosted on GitHub| |gui|Tcl/Tk GUI| @@ -24,6 +23,13 @@ Current development focuses on the Python modules and daemon. Here is a brief de Overview for setting up the pipenv environment, building core, installing the GUI and netns, then running the core-daemon for development. +### Clone CORE Repo + +```shell +git clone https://github.com/coreemu/core.git +cd core +``` + ### Setup Python Environment To leverage the dev environment you need python 3.6+. @@ -40,14 +46,13 @@ pip3 install pipenv # setup a virtual environment and install all required development dependencies python3 -m pipenv install --dev - -# setup python variable using pipenv created python -export PYTHON=$(python3 -m pipenv --py) ``` ### Setup pre-commit -Install pre-commit hooks to help automate running tool checks against code. +Install pre-commit hooks to help automate running tool checks against code. Once installed every time a commit is made +python utilities will be ran to check validity of code, potentially failing and backing out the commit. This allows +one to review changes being made by tools ro the fix the issue noted. Then add the changes and commit again. ```shell python3 -m pipenv run pre-commit install @@ -56,11 +61,6 @@ python3 -m pipenv run pre-commit install ### Build CORE ```shell -# clone core -git clone https://github.com/coreemu/core.git -cd core - -# build core ./bootstrap.sh ./configure --prefix=/usr make @@ -89,7 +89,7 @@ EMANE bindings are not available through pip, you will need to build and install ```shell # after building emane above # ./autogen.sh && ./configure --prefix=/usr && make -python3 -m pipenv install --skip-lock $EMANEREPO/src/python +python3 -m pipenv install $EMANEREPO/src/python ``` ### Running CORE @@ -97,20 +97,16 @@ python3 -m pipenv install --skip-lock $EMANEREPO/src/python This will run the core-daemon server using the configuration files within the repo. ```shell -python3 -m pipenv run coredev +python3 -m pipenv run core ``` -## The CORE API +### Running CORE Python GUI -The CORE API is used between different components of CORE for communication. The GUI communicates with the CORE daemon -using the API. One emulation server communicates with another using the API. The API also allows other systems to -interact with the CORE emulation. The API allows another system to add, remove, or modify nodes and links, and enables -executing commands on the emulated systems. Wireless link parameters are updated on-the-fly based on node positions. +Must be ran after the daemon above or will fail to connect. -CORE listens on a local TCP port for API messages. The other system could be software running locally or another -machine accessible across the network. - -The CORE API is currently specified in a separate document, available from the CORE website. +```shell +python3 -m pipenv run coretk +``` ## Linux Network Namespace Commands @@ -169,43 +165,3 @@ tc qdisc show # view the rules that make the wireless LAN work ebtables -L ``` - -### Example Command Usage - -Below is a transcript of creating two emulated nodes and connecting them together with a wired link: - -```shell -# create node 1 namespace container -vnoded -c /tmp/n1.ctl -l /tmp/n1.log -p /tmp/n1.pid -# create a virtual Ethernet (veth) pair, installing one end into node 1 -ip link add name n1.0.1 type veth peer name n1.0 -ip link set n1.0 netns `cat /tmp/n1.pid` -vcmd -c /tmp/n1.ctl -- ip link set lo up -vcmd -c /tmp/n1.ctl -- ip link set n1.0 name eth0 up -vcmd -c /tmp/n1.ctl -- ip addr add 10.0.0.1/24 dev eth0 - -# create node 2 namespace container -vnoded -c /tmp/n2.ctl -l /tmp/n2.log -p /tmp/n2.pid -# create a virtual Ethernet (veth) pair, installing one end into node 2 -ip link add name n2.0.1 type veth peer name n2.0 -ip link set n2.0 netns `cat /tmp/n2.pid` -vcmd -c /tmp/n2.ctl -- ip link set lo up -vcmd -c /tmp/n2.ctl -- ip link set n2.0 name eth0 up -vcmd -c /tmp/n2.ctl -- ip addr add 10.0.0.2/24 eth0 - -# bridge together nodes 1 and 2 using the other end of each veth pair -brctl addbr b.1.1 -brctl setfd b.1.1 0 -brctl addif b.1.1 n1.0.1 -brctl addif b.1.1 n2.0.1 -ip link set n1.0.1 up -ip link set n2.0.1 up -ip link set b.1.1 up - -# display connectivity and ping from node 1 to node 2 -brctl show -vcmd -c /tmp/n1.ctl -- ping 10.0.0.2 -``` - -The above example script can be found as *twonodes.sh* in the *examples/netns* directory. Use *core-cleanup* to clean -up after the script. From fc15918a64b304ddd1b21ea80c596aaa6efae144 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 19 Dec 2019 16:07:17 -0800 Subject: [PATCH 431/462] adding install script to help automate setup for others, small touchup to devguide doc --- docs/devguide.md | 2 +- install.sh | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100755 install.sh diff --git a/docs/devguide.md b/docs/devguide.md index 34ea1536..a71043ef 100644 --- a/docs/devguide.md +++ b/docs/devguide.md @@ -97,7 +97,7 @@ python3 -m pipenv install $EMANEREPO/src/python This will run the core-daemon server using the configuration files within the repo. ```shell -python3 -m pipenv run core +sudo python3 -m pipenv run core ``` ### Running CORE Python GUI diff --git a/install.sh b/install.sh new file mode 100755 index 00000000..2c32f739 --- /dev/null +++ b/install.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# exit on error +set -e + +# detect os/ver for install type +os="" +if [[ -f /etc/os-release ]]; then + . /etc/os-release + os=${NAME} +fi + +# check install was found +if [[ ${os} == "Ubuntu" ]]; then + # install system dependencies + sudo apt install -y automake pkg-config gcc libev-dev bridge-utils ebtables \ + python3.6 python3.6-dev python3-pip python3-tk tk libtk-img ethtool libtool libreadline-dev autoconf + + # install python dependencies + sudo python3 -m pip install -r daemon/requirements.txt + + # make and install ospf mdr + git clone https://github.com/USNavalResearchLaboratory/ospf-mdr /tmp/ospf-mdr + cd /tmp/ospf-mdr + ./bootstrap.sh + ./configure --disable-doc --enable-user=root --enable-group=root --with-cflags=-ggdb \ + --sysconfdir=/usr/local/etc/quagga --enable-vtysh \ + --localstatedir=/var/run/quagga + make -j8 + sudo make install + cd - + + # build and install core + ./bootstrap.sh + ./configure + make -j8 + sudo make install +else + echo "unknown os ${os} cannot install" +fi From c1755afb2f6ddddf7ef9748a289ba295e741db72 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 19 Dec 2019 16:15:29 -0800 Subject: [PATCH 432/462] copy node and links --- daemon/core/gui/coreclient.py | 7 ++--- daemon/core/gui/graph/graph.py | 48 +++++++++++++++++++++++++++++++++- daemon/core/gui/menuaction.py | 9 +++++++ daemon/core/gui/menubar.py | 11 ++++++-- 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index dee94dbf..f226d636 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -760,9 +760,10 @@ class CoreClient: if edge in edges: continue edges.add(edge) - if edge.token not in self.links: - logging.error("unknown edge: %s", edge.token) - del self.links[edge.token] + # + # if edge.token not in self.links: + # logging.error("unknown edge: %s", edge.token) + self.links.pop(edge.token, None) def create_interface(self, canvas_node): node = canvas_node.core_node diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 6b8c55ef..453f35c2 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -46,6 +46,7 @@ class CanvasGraph(tk.Canvas): self.offset = (0, 0) self.cursor = (0, 0) self.marker_tool = None + self.to_copy = [] # background related self.wallpaper_id = None @@ -441,7 +442,8 @@ class CanvasGraph(tk.Canvas): continue edges.add(edge) self.throughput_draw.delete(edge) - del self.edges[edge.token] + self.edges.pop(edge.token, None) + # del self.edges[edge.token] edge.delete() # update node connected to edge being deleted @@ -838,3 +840,47 @@ class CanvasGraph(tk.Canvas): self.nodes[source.id].edges.add(edge) self.nodes[dest.id].edges.add(edge) self.core.create_link(edge, source, dest) + + def copy(self): + if self.selection: + logging.debug( + "store current selection to to_copy, number of nodes: %s", + len(self.selection), + ) + self.to_copy = self.selection.keys() + + def paste(self): + logging.debug("copy") + # maps original node canvas id to copy node canvas id + copy_map = {} + # the edges that will be copy over + to_copy_edges = [] + for canvas_nid in self.to_copy: + core_node = self.nodes[canvas_nid].core_node + actual_x = core_node.position.x + 50 + actual_y = core_node.position.y + 50 + scaled_x, scaled_y = self.get_scaled_coords(actual_x, actual_y) + + copy = self.core.create_node( + actual_x, actual_y, core_node.type, core_node.model + ) + node = CanvasNode( + self.master, scaled_x, scaled_y, copy, self.nodes[canvas_nid].image + ) + copy_map[canvas_nid] = node.id + self.core.canvas_nodes[copy.id] = node + self.nodes[node.id] = node + + edges = self.nodes[canvas_nid].edges + for edge in edges: + if edge.src not in self.to_copy or edge.dst not in self.to_copy: + if canvas_nid == edge.src: + self.create_edge(node, self.nodes[edge.dst]) + elif canvas_nid == edge.dst: + self.create_edge(self.nodes[edge.src], node) + else: + to_copy_edges.append(tuple([edge.src, edge.dst])) + for e in to_copy_edges: + source_node_copy = self.nodes[copy_map[e[0]]] + dest_node_copy = self.nodes[copy_map[e[1]]] + self.create_edge(source_node_copy, dest_node_copy) diff --git a/daemon/core/gui/menuaction.py b/daemon/core/gui/menuaction.py index be532218..8d45c33a 100644 --- a/daemon/core/gui/menuaction.py +++ b/daemon/core/gui/menuaction.py @@ -30,6 +30,7 @@ class MenuAction: def __init__(self, app, master): self.master = master self.app = app + self.canvas = app.canvas def cleanup_old_session(self, quitapp=False): logging.info("cleaning up old session") @@ -155,3 +156,11 @@ class MenuAction: self.app.core.enable_throughputs() else: self.app.core.cancel_throughputs() + + def copy(self, event=None): + logging.debug("copy") + self.app.canvas.copy() + + def paste(self, event=None): + logging.debug("paste") + self.app.canvas.paste() diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index c2a4e353..d797097f 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -90,8 +90,12 @@ class Menubar(tk.Menu): menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED) menu.add_separator() menu.add_command(label="Cut", accelerator="Ctrl+X", state=tk.DISABLED) - menu.add_command(label="Copy", accelerator="Ctrl+C", state=tk.DISABLED) - menu.add_command(label="Paste", accelerator="Ctrl+V", state=tk.DISABLED) + menu.add_command( + label="Copy", accelerator="Ctrl+C", command=self.menuaction.copy + ) + menu.add_command( + label="Paste", accelerator="Ctrl+V", command=self.menuaction.paste + ) menu.add_separator() menu.add_command(label="Select all", accelerator="Ctrl+A", state=tk.DISABLED) menu.add_command( @@ -102,6 +106,9 @@ class Menubar(tk.Menu): menu.add_command(label="Clear marker", state=tk.DISABLED) self.add_cascade(label="Edit", menu=menu) + self.app.master.bind_all("", self.menuaction.copy) + self.app.master.bind_all("", self.menuaction.paste) + def draw_canvas_menu(self): """ Create canvas menu From 43d0ee84c22124aff5aebc3d20f4218d0977c31d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 19 Dec 2019 17:15:11 -0800 Subject: [PATCH 433/462] missing gawk dependency from install.sh --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 2c32f739..14cc9d6b 100755 --- a/install.sh +++ b/install.sh @@ -13,7 +13,7 @@ fi # check install was found if [[ ${os} == "Ubuntu" ]]; then # install system dependencies - sudo apt install -y automake pkg-config gcc libev-dev bridge-utils ebtables \ + sudo apt install -y automake pkg-config gcc libev-dev bridge-utils ebtables gawk \ python3.6 python3.6-dev python3-pip python3-tk tk libtk-img ethtool libtool libreadline-dev autoconf # install python dependencies From a674f5bf78506d8d03ef3935a3c03b8d575397e0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 19 Dec 2019 17:27:57 -0800 Subject: [PATCH 434/462] update python install makefiles to avoid forcing site-packages --- daemon/Makefile.am | 1 - ns3/Makefile.am | 1 - 2 files changed, 2 deletions(-) diff --git a/daemon/Makefile.am b/daemon/Makefile.am index 80483387..a5663654 100644 --- a/daemon/Makefile.am +++ b/daemon/Makefile.am @@ -29,7 +29,6 @@ install-exec-hook: $(PYTHON) $(SETUPPY) $(SETUPPYFLAGS) install \ --root=/$(DESTDIR) \ --prefix=$(prefix) \ - --install-lib=$(pythondir) \ --single-version-externally-managed # Python package uninstall diff --git a/ns3/Makefile.am b/ns3/Makefile.am index 62086e82..eb32389e 100644 --- a/ns3/Makefile.am +++ b/ns3/Makefile.am @@ -22,7 +22,6 @@ install-exec-hook: $(PYTHON) $(SETUPPY) $(SETUPPYFLAGS) install \ --root=/$(DESTDIR) \ --prefix=$(prefix) \ - --install-lib=$(pythondir) \ --single-version-externally-managed \ --no-compile From eb7d81614e6a320ce793c19de30423f0daefaab5 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 19 Dec 2019 21:28:17 -0800 Subject: [PATCH 435/462] set pillow logging to ERROR for coretk-gui --- daemon/scripts/coretk-gui | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon/scripts/coretk-gui b/daemon/scripts/coretk-gui index a87df4fc..1ddb7526 100755 --- a/daemon/scripts/coretk-gui +++ b/daemon/scripts/coretk-gui @@ -8,6 +8,7 @@ from core.gui.images import Images if __name__ == "__main__": log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=log_format) + logging.getLogger("PIL").setLevel(logging.ERROR) Images.load_all() appconfig.check_directory() app = Application() From a7e243ae5314621138538d21d90d781b4ae92159 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 19 Dec 2019 21:28:48 -0800 Subject: [PATCH 436/462] update coretk-gui to use configured terminal when double clicking nodes --- 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 f226d636..530e0ccf 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -534,9 +534,10 @@ class CoreClient: def launch_terminal(self, node_id): try: + terminal = self.app.guiconfig["preferences"]["terminal"] response = self.client.get_node_terminal(self.session_id, node_id) logging.info("get terminal %s", response.terminal) - os.system(f"xterm -e {response.terminal} &") + os.system(f"{terminal} {response.terminal} &") except grpc.RpcError as e: show_grpc_error(e) From 9dd42e03594a2a0eb3f17c1799c26390343e92b1 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 19 Dec 2019 21:29:10 -0800 Subject: [PATCH 437/462] changes to daemon setup.py to support including gui data files --- daemon/MANIFEST.in | 1 + daemon/setup.py.in | 1 + 2 files changed, 2 insertions(+) create mode 100644 daemon/MANIFEST.in diff --git a/daemon/MANIFEST.in b/daemon/MANIFEST.in new file mode 100644 index 00000000..40dbefc8 --- /dev/null +++ b/daemon/MANIFEST.in @@ -0,0 +1 @@ +graft core/gui/data diff --git a/daemon/setup.py.in b/daemon/setup.py.in index 5f3ca41d..e0faf01d 100644 --- a/daemon/setup.py.in +++ b/daemon/setup.py.in @@ -48,6 +48,7 @@ setup( ], data_files=data_files, scripts=glob.glob("scripts/*"), + include_package_data=True, description="Python components of CORE", url="https://github.com/coreemu/core", author="Boeing Research & Technology", From e92441d7285ad6d381364efdcdf2105b9e18c164 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 20 Dec 2019 09:42:43 -0800 Subject: [PATCH 438/462] bumped version to 6.0.0 --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 5d04356e..43a5815f 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, 5.5.2) +AC_INIT(core, 6.0.0) # autoconf and automake initialization AC_CONFIG_SRCDIR([netns/version.h.in]) From 086e3316eb2941d0c6707cd45560128d6fc1431c Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 20 Dec 2019 09:55:30 -0800 Subject: [PATCH 439/462] copy over wlan config, emane, mobility config, service config, service file config --- daemon/core/gui/coreclient.py | 30 ++++++++++++++++++++++++++++++ daemon/core/gui/graph/graph.py | 1 + daemon/core/gui/widgets.py | 6 +++--- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 530e0ccf..83062983 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -909,3 +909,33 @@ class CoreClient: def set_emane_model_config(self, node_id, model, config, interface=None): logging.info("setting emane model config: %s %s %s", node_id, model, interface) self.emane_model_configs[(node_id, model, interface)] = config + + def copy_node_service(self, _from, _to): + services = self.canvas_nodes[_from].core_node.services + self.canvas_nodes[_to].core_node.services[:] = services + + def copy_node_config(self, _from, _to): + node_type = self.canvas_nodes[_from].core_node.type + if node_type == core_pb2.NodeType.DEFAULT: + services = self.canvas_nodes[_from].core_node.services + self.canvas_nodes[_to].core_node.services[:] = services + config = self.service_configs.get(_from) + if config: + self.service_configs[_to] = config + file_configs = self.file_configs.get(_from) + if file_configs: + for key, value in file_configs.items(): + if _to not in self.file_configs: + self.file_configs[_to] = {} + self.file_configs[_to][key] = value + elif node_type == core_pb2.NodeType.WIRELESS_LAN: + config = self.wlan_configs.get(_from) + if config: + self.wlan_configs[_to] = config + config = self.mobility_configs.get(_from) + if config: + self.mobility_configs[_to] = config + elif node_type == core_pb2.NodeType.EMANE: + config = self.emane_model_configs.get(_from) + if config: + self.emane_model_configs[_to] = config diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 36314a53..ead26669 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -870,6 +870,7 @@ class CanvasGraph(tk.Canvas): copy_map[canvas_nid] = node.id self.core.canvas_nodes[copy.id] = node self.nodes[node.id] = node + self.core.copy_node_config(core_node.id, copy.id) edges = self.nodes[canvas_nid].edges for edge in edges: diff --git a/daemon/core/gui/widgets.py b/daemon/core/gui/widgets.py index 6e8964af..d5257533 100644 --- a/daemon/core/gui/widgets.py +++ b/daemon/core/gui/widgets.py @@ -19,8 +19,8 @@ INT_TYPES = { } -def file_button_click(value): - file_path = filedialog.askopenfilename(title="Select File") +def file_button_click(value, parent): + file_path = filedialog.askopenfilename(title="Select File", parent=parent) if file_path: value.set(file_path) @@ -111,7 +111,7 @@ class ConfigFrame(ttk.Notebook): file_frame.columnconfigure(0, weight=1) entry = ttk.Entry(file_frame, textvariable=value) entry.grid(row=0, column=0, sticky="ew", padx=PADX) - func = partial(file_button_click, value) + func = partial(file_button_click, value, self) button = ttk.Button(file_frame, text="...", command=func) button.grid(row=0, column=1) else: From 95c57bbad646dea230ccbb849e431f337efd59cf Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 20 Dec 2019 09:57:34 -0800 Subject: [PATCH 440/462] changes to allow node container commands to leverage shell parameter when needed --- daemon/core/nodes/base.py | 8 +++++--- daemon/core/nodes/client.py | 5 +++-- daemon/core/nodes/docker.py | 8 ++++---- daemon/core/nodes/lxd.py | 4 ++-- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 3193d954..4946875e 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -380,12 +380,13 @@ class CoreNodeBase(NodeBase): return common - def cmd(self, args, wait=True): + def cmd(self, args, wait=True, shell=False): """ Runs a command within a node container. :param str args: command to run :param bool wait: True to wait for status, False otherwise + :param bool shell: True to use shell, False otherwise :return: combined stdout and stderr :rtype: str :raises CoreCommandError: when a non-zero exit status occurs @@ -561,19 +562,20 @@ class CoreNode(CoreNodeBase): finally: self.rmnodedir() - def cmd(self, args, wait=True): + def cmd(self, args, wait=True, shell=False): """ Runs a command that is used to configure and setup the network within a node. :param str args: command to run :param bool wait: True to wait for status, False otherwise + :param bool shell: True to use shell, False otherwise :return: combined stdout and stderr :rtype: str :raises CoreCommandError: when a non-zero exit status occurs """ if self.server is None: - return self.client.check_cmd(args, wait=wait) + return self.client.check_cmd(args, wait=wait, shell=shell) else: args = self.client.create_cmd(args) return self.server.remote_cmd(args, wait=wait) diff --git a/daemon/core/nodes/client.py b/daemon/core/nodes/client.py index 1e949c04..66b61c37 100644 --- a/daemon/core/nodes/client.py +++ b/daemon/core/nodes/client.py @@ -53,16 +53,17 @@ class VnodeClient: def create_cmd(self, args): return f"{VCMD_BIN} -c {self.ctrlchnlname} -- {args}" - def check_cmd(self, args, wait=True): + def check_cmd(self, args, wait=True, shell=False): """ Run command and return exit status and combined stdout and stderr. :param str args: command to run :param bool wait: True to wait for command status, False otherwise + :param bool shell: True to use shell, False otherwise :return: combined stdout and stderr :rtype: str :raises core.CoreCommandError: when there is a non-zero exit status """ self._verify_connection() args = self.create_cmd(args) - return utils.cmd(args, wait=wait) + return utils.cmd(args, wait=wait, shell=shell) diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index 20d6ec20..b56fcc5c 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -43,9 +43,9 @@ class DockerClient: def stop_container(self): self.run(f"docker rm -f {self.name}") - def check_cmd(self, cmd): + def check_cmd(self, cmd, wait=True, shell=False): logging.info("docker cmd output: %s", cmd) - return utils.cmd(f"docker exec {self.name} {cmd}") + return utils.cmd(f"docker exec {self.name} {cmd}", wait=wait, shell=shell) def create_ns_cmd(self, cmd): return f"nsenter -t {self.pid} -u -i -p -n {cmd}" @@ -148,10 +148,10 @@ class DockerNode(CoreNode): self.client.stop_container() self.up = False - def nsenter_cmd(self, args, wait=True): + def nsenter_cmd(self, args, wait=True, shell=False): if self.server is None: args = self.client.create_ns_cmd(args) - return utils.cmd(args, wait=wait) + return utils.cmd(args, wait=wait, shell=shell) else: args = self.client.create_ns_cmd(args) return self.server.remote_cmd(args, wait=wait) diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index fc2ee5cc..2d7a6d3d 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -47,9 +47,9 @@ class LxdClient: def create_ns_cmd(self, cmd): return f"nsenter -t {self.pid} -m -u -i -p -n {cmd}" - def check_cmd(self, cmd, wait=True): + def check_cmd(self, cmd, wait=True, shell=False): args = self.create_cmd(cmd) - return utils.cmd(args, wait=wait) + return utils.cmd(args, wait=wait, shell=shell) def copy_file(self, source, destination): if destination[0] != "/": From 513eaf2b76fb01875905b7f64528d8c95b223ff8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 20 Dec 2019 10:51:52 -0800 Subject: [PATCH 441/462] improved coretk gui alerts to display alert text when selected, fixed merged code for adding a check to cleanup interfaces, updated session.exceptions to use enums directly --- daemon/core/api/grpc/events.py | 2 +- daemon/core/api/tlv/corehandlers.py | 2 +- daemon/core/emulator/session.py | 4 +- daemon/core/gui/coreclient.py | 4 +- daemon/core/gui/dialogs/alerts.py | 72 ++++++++++------------------ daemon/core/nodes/netclient.py | 3 +- daemon/core/services/coreservices.py | 10 +++- daemon/tests/test_grpc.py | 13 +++-- 8 files changed, 50 insertions(+), 60 deletions(-) diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index 7b4756b1..5c4ee25e 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -145,7 +145,7 @@ def handle_exception_event(event): """ return core_pb2.ExceptionEvent( node_id=event.node, - level=event.level, + level=event.level.value, source=event.source, date=event.date, text=event.text, diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 009cb067..79c2e1dc 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -299,7 +299,7 @@ class CoreHandler(socketserver.BaseRequestHandler): [ (ExceptionTlvs.NODE, exception_data.node), (ExceptionTlvs.SESSION, exception_data.session), - (ExceptionTlvs.LEVEL, exception_data.level), + (ExceptionTlvs.LEVEL, exception_data.level.value), (ExceptionTlvs.SOURCE, exception_data.source), (ExceptionTlvs.DATE, exception_data.date), (ExceptionTlvs.TEXT, exception_data.text), diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 1a01bdaa..bc2f750e 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -1411,13 +1411,12 @@ class Session: """ Generate and broadcast an exception event. - :param str level: exception level + :param core.emulator.enumerations.ExceptionLevel level: exception level :param str source: source name :param int node_id: node related to exception :param str text: exception message :return: nothing """ - exception_data = ExceptionData( node=node_id, session=str(self.id), @@ -1426,7 +1425,6 @@ class Session: date=time.ctime(), text=text, ) - self.broadcast_exception(exception_data) def instantiate(self): diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 83062983..65789b23 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -167,7 +167,7 @@ class CoreClient: elif event.HasField("config_event"): logging.info("config event: %s", event) elif event.HasField("exception_event"): - self.handle_exception_event(event.exception_event) + self.handle_exception_event(event) else: logging.info("unhandled event: %s", event) @@ -204,7 +204,7 @@ class CoreClient: def handle_throughputs(self, event): if event.session_id != self.session_id: - logging.warn( + logging.warning( "ignoring throughput event session(%s) current(%s)", event.session_id, self.session_id, diff --git a/daemon/core/gui/dialogs/alerts.py b/daemon/core/gui/dialogs/alerts.py index cb91881c..7e82da73 100644 --- a/daemon/core/gui/dialogs/alerts.py +++ b/daemon/core/gui/dialogs/alerts.py @@ -4,9 +4,7 @@ check engine light import tkinter as tk from tkinter import ttk -from grpc import RpcError - -from core.api.grpc import core_pb2 +from core.api.grpc.core_pb2 import ExceptionLevel from core.gui.dialogs.dialog import Dialog from core.gui.themes import PADX, PADY from core.gui.widgets import CodeText @@ -18,6 +16,7 @@ class AlertsDialog(Dialog): self.app = app self.tree = None self.codetext = None + self.alarm_map = {} self.draw() def draw(self): @@ -48,25 +47,31 @@ class AlertsDialog(Dialog): self.tree.bind("<>", self.click_select) for alarm in self.app.statusbar.core_alarms: - level = self.get_level(alarm.level) - self.tree.insert( + exception = alarm.exception_event + level_name = ExceptionLevel.Enum.Name(exception.level) + insert_id = self.tree.insert( "", tk.END, - text=str(alarm.date), + text=exception.date, values=( - alarm.date, - level + " (%s)" % alarm.level, + exception.date, + level_name, alarm.session_id, - alarm.node_id, - alarm.source, + exception.node_id, + exception.source, ), - tags=(level,), + tags=(level_name,), ) + self.alarm_map[insert_id] = alarm - self.tree.tag_configure("ERROR", background="#ff6666") - self.tree.tag_configure("FATAL", background="#d9d9d9") - self.tree.tag_configure("WARNING", background="#ffff99") - self.tree.tag_configure("NOTICE", background="#85e085") + error_name = ExceptionLevel.Enum.Name(ExceptionLevel.ERROR) + self.tree.tag_configure(error_name, background="#ff6666") + fatal_name = ExceptionLevel.Enum.Name(ExceptionLevel.FATAL) + self.tree.tag_configure(fatal_name, background="#d9d9d9") + warning_name = ExceptionLevel.Enum.Name(ExceptionLevel.WARNING) + self.tree.tag_configure(warning_name, background="#ffff99") + notice_name = ExceptionLevel.Enum.Name(ExceptionLevel.NOTICE) + self.tree.tag_configure(notice_name, background="#85e085") yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) yscrollbar.grid(row=0, column=1, sticky="ns") @@ -105,40 +110,13 @@ class AlertsDialog(Dialog): dialog = DaemonLog(self, self.app) dialog.show() - def get_level(self, level): - if level == core_pb2.ExceptionLevel.ERROR: - return "ERROR" - if level == core_pb2.ExceptionLevel.FATAL: - return "FATAL" - if level == core_pb2.ExceptionLevel.WARNING: - return "WARNING" - if level == core_pb2.ExceptionLevel.NOTICE: - return "NOTICE" - def click_select(self, event): - current = self.tree.selection() - values = self.tree.item(current)["values"] - time = values[0] - level = values[1] - session_id = values[2] - node_id = values[3] - source = values[4] - text = "DATE: %s\nLEVEL: %s\nNODE: %s (%s)\nSESSION: %s\nSOURCE: %s\n\n" % ( - time, - level, - node_id, - self.app.core.canvas_nodes[node_id].core_node.name, - session_id, - source, - ) - try: - sid = self.app.core.session_id - self.app.core.client.get_node(sid, node_id) - text = text + "node created" - except RpcError: - text = text + "node not created" + current = self.tree.selection()[0] + alarm = self.alarm_map[current] + self.codetext.text.config(state=tk.NORMAL) self.codetext.text.delete("1.0", "end") - self.codetext.text.insert("1.0", text) + self.codetext.text.insert("1.0", alarm.exception_event.text) + self.codetext.text.config(state=tk.DISABLED) class DaemonLog(Dialog): diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 4d568a64..4a7250f0 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -127,7 +127,8 @@ class LinuxNetClient: :return: nothing """ self.run( - f"[ -e /sys/class/net/{device} ] && {IP_BIN} -6 address flush dev {device} || true" + f"[ -e /sys/class/net/{device} ] && {IP_BIN} -6 address flush dev {device} || true", + shell=True, ) def device_mac(self, device, mac): diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index 684ccbb9..4372dd88 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -14,7 +14,7 @@ import time from core import utils from core.constants import which from core.emulator.data import FileData -from core.emulator.enumerations import MessageFlags, RegisterTlvs +from core.emulator.enumerations import ExceptionLevels, MessageFlags, RegisterTlvs from core.errors import CoreCommandError @@ -628,7 +628,13 @@ class CoreServices: for args in service.shutdown: try: node.cmd(args) - except CoreCommandError: + except CoreCommandError as e: + self.session.exception( + ExceptionLevels.ERROR, + "services", + node.id, + f"error stopping service {service.name}: {e.stderr}", + ) logging.exception("error running stop command %s", args) status = -1 return status diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 89e46f9b..2e2685ac 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -1101,19 +1101,26 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() queue = Queue() + exception_level = ExceptionLevels.FATAL + source = "test" + node_id = None + text = "exception message" def handle_event(event_data): assert event_data.session_id == session.id assert event_data.HasField("exception_event") + exception_event = event_data.exception_event + assert exception_event.level == exception_level.value + assert exception_event.node_id == 0 + assert exception_event.source == source + assert exception_event.text == text queue.put(event_data) # then with client.context_connect(): client.events(session.id, handle_event) time.sleep(0.1) - session.exception( - ExceptionLevels.FATAL.value, "test", None, "exception message" - ) + session.exception(exception_level, source, node_id, text) # then queue.get(timeout=5) From 396a948bb98eb3fe53aafb4ce6027a63ac3dbf3e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 20 Dec 2019 11:50:43 -0800 Subject: [PATCH 442/462] small tweak to make new has_ebtables_chain variable created in __init__ --- 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 a4d404ec..653385ee 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -275,6 +275,7 @@ class CoreNetwork(CoreNetworkBase): sessionid = self.session.short_session_id() self.brname = f"b.{self.id}.{sessionid}" self.up = False + self.has_ebtables_chain = False if start: self.startup() ebq.startupdateloop(self) From 9f3a3cef28f01d77316f02f50543b798abc83aba Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 20 Dec 2019 11:56:48 -0800 Subject: [PATCH 443/462] update coretk gui to allow proper cancel when there is an attempt to exit a running session --- daemon/core/gui/menuaction.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/daemon/core/gui/menuaction.py b/daemon/core/gui/menuaction.py index 8d45c33a..8eaf7781 100644 --- a/daemon/core/gui/menuaction.py +++ b/daemon/core/gui/menuaction.py @@ -48,24 +48,21 @@ class MenuAction: :return: nothing """ - logging.info( - "menuaction.py: clean_nodes_links_and_set_configuration() Exiting the program" - ) try: if not self.app.core.is_runtime(): self.app.core.delete_session() if quitapp: self.app.quit() else: - result = messagebox.askyesnocancel("stop", "Stop the running session?") - if result: + result = messagebox.askyesnocancel("Exit", "Stop the running session?") + if result is True: self.app.statusbar.progress_bar.start(5) thread = threading.Thread( target=self.cleanup_old_session, args=([quitapp]) ) thread.daemon = True thread.start() - elif quitapp: + elif result is False and quitapp: self.app.quit() except grpc.RpcError: logging.exception("error deleting session") From e4b44d08c1fc8996b5b5dd9ca14b2b4bd9226b00 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 20 Dec 2019 11:56:51 -0800 Subject: [PATCH 444/462] create a rough layout for throughput config dialog --- daemon/core/gui/dialogs/throughput.py | 97 +++++++++++++++++++++++++++ daemon/core/gui/menuaction.py | 6 ++ daemon/core/gui/menubar.py | 4 +- 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 daemon/core/gui/dialogs/throughput.py diff --git a/daemon/core/gui/dialogs/throughput.py b/daemon/core/gui/dialogs/throughput.py new file mode 100644 index 00000000..178c10d1 --- /dev/null +++ b/daemon/core/gui/dialogs/throughput.py @@ -0,0 +1,97 @@ +""" +throughput dialog +""" +import logging +import tkinter as tk +from tkinter import ttk + +from core.gui.dialogs.dialog import Dialog + + +class ThroughputDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "throughput config", modal=False) + self.app = app + self.show_throughput = tk.IntVar(value=1) + self.exponential_weight = tk.IntVar(value=1) + self.transmission = tk.IntVar(value=1) + self.reception = tk.IntVar(value=1) + self.threshold = tk.DoubleVar(value=250.0) + self.width = tk.IntVar(value=10) + self.top.columnconfigure(0, weight=1) + self.draw() + + def draw(self): + button = ttk.Checkbutton( + self.top, + variable=self.show_throughput, + text="Show throughput level on every link", + ) + button.grid(row=0, column=0, sticky="nsew") + button = ttk.Checkbutton( + self.top, + variable=self.exponential_weight, + text="Use exponential weighted moving average", + ) + button.grid(row=1, column=0, sticky="nsew") + button = ttk.Checkbutton( + self.top, variable=self.transmission, text="Include transmissions" + ) + button.grid(row=2, column=0, sticky="nsew") + button = ttk.Checkbutton( + self.top, variable=self.reception, text="Include receptions" + ) + button.grid(row=3, column=0, sticky="nsew") + + label_frame = ttk.LabelFrame(self.top, text="Link highlight") + label_frame.columnconfigure(0, weight=1) + label_frame.grid(row=4, column=0, sticky="nsew") + label = ttk.Label(label_frame, text="Highlight link if throughput exceeds this") + label.grid(row=0, column=0, sticky="nsew") + + frame = ttk.Frame(label_frame) + frame.columnconfigure(0, weight=2) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + frame.grid(row=1, column=0, sticky="nsew") + label = ttk.Label(frame, text="Threshold (0 for disabled)") + label.grid(row=0, column=0, sticky="nsew") + entry = ttk.Entry(frame, textvariable=self.threshold) + entry.grid(row=0, column=1, sticky="nsew") + label = ttk.Label(frame, text="kbps") + label.grid(row=0, column=2, sticky="nsew") + + scale = ttk.Scale( + label_frame, + from_=0, + to=1000, + value=0, + orient=tk.HORIZONTAL, + variable=self.threshold, + ) + scale.grid(row=2, column=0, sticky="nsew") + + frame = ttk.Frame(label_frame) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.grid(row=3, column=0, sticky="nsew") + label = ttk.Label(frame, text="Highlight link width: ") + label.grid(row=0, column=0, sticky="nsew") + entry = ttk.Entry(frame, textvariable=self.width) + entry.grid(row=0, column=1, sticky="nsew") + + frame = ttk.Frame(label_frame) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.grid(row=4, column=0, sticky="nsew") + label = ttk.Label(frame, text="Color: ") + label.grid(row=0, column=0, sticky="nsew") + button = ttk.Button(frame, text="not implemented") + button.grid(row=0, column=1, sticky="nsew") + + button = ttk.Button(self.top, text="OK", command=self.ok) + button.grid(row=5, column=0, sticky="nsew") + + def ok(self): + logging.debug("click ok") + self.destroy() diff --git a/daemon/core/gui/menuaction.py b/daemon/core/gui/menuaction.py index 8d45c33a..7a635ba6 100644 --- a/daemon/core/gui/menuaction.py +++ b/daemon/core/gui/menuaction.py @@ -20,6 +20,7 @@ from core.gui.dialogs.preferences import PreferencesDialog from core.gui.dialogs.servers import ServersDialog from core.gui.dialogs.sessionoptions import SessionOptionsDialog from core.gui.dialogs.sessions import SessionsDialog +from core.gui.dialogs.throughput import ThroughputDialog class MenuAction: @@ -164,3 +165,8 @@ class MenuAction: def paste(self, event=None): logging.debug("paste") self.app.canvas.paste() + + def config_throughput(self): + logging.debug("not implemented") + dialog = ThroughputDialog(self.app, self.app) + dialog.show() diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index d797097f..f8020866 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -435,7 +435,9 @@ class Menubar(tk.Menu): menu.add_checkbutton(label="Throughput", command=self.menuaction.throughput) menu.add_separator() menu.add_command(label="Configure Adjacency...", state=tk.DISABLED) - menu.add_command(label="Configure Throughput...", state=tk.DISABLED) + menu.add_command( + label="Configure Throughput...", command=self.menuaction.config_throughput + ) self.add_cascade(label="Widgets", menu=menu) def draw_session_menu(self): From 8fe6bc76ca8be81cabf456e235c6d9cd6546c1b7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 20 Dec 2019 12:36:29 -0800 Subject: [PATCH 445/462] update devguide notes for installing emane --- docs/devguide.md | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/docs/devguide.md b/docs/devguide.md index a71043ef..319c9fbd 100644 --- a/docs/devguide.md +++ b/docs/devguide.md @@ -87,8 +87,18 @@ EMANE bindings are not available through pip, you will need to build and install [Build EMANE](https://github.com/adjacentlink/emane/wiki/Build#general-build-instructions) ```shell -# after building emane above -# ./autogen.sh && ./configure --prefix=/usr && make +# clone emane repo +git clone https://github.com/adjacentlink/emane.git + +# install emane build deps +sudo apt install libxml2-dev libprotobuf-dev uuid-dev libpcap-dev protobuf-compiler + +# build emane +./autogen.sh +./configure --prefix=/usr +make -j8 + +# install emane binding in pipenv python3 -m pipenv install $EMANEREPO/src/python ``` @@ -110,26 +120,26 @@ python3 -m pipenv run coretk ## Linux Network Namespace Commands -Linux network namespace containers are often managed using the *Linux Container Tools* or *lxc-tools* package. -The lxc-tools website is available here http://lxc.sourceforge.net/ for more information. CORE does not use these -management utilities, but includes its own set of tools for instantiating and configuring network namespace containers. +Linux network namespace containers are often managed using the *Linux Container Tools* or *lxc-tools* package. +The lxc-tools website is available here http://lxc.sourceforge.net/ for more information. CORE does not use these +management utilities, but includes its own set of tools for instantiating and configuring network namespace containers. This section describes these tools. ### vnoded -The *vnoded* daemon is the program used to create a new namespace, and listen on a control channel for commands that -may instantiate other processes. This daemon runs as PID 1 in the container. It is launched automatically by the CORE -daemon. The control channel is a UNIX domain socket usually named */tmp/pycore.23098/n3*, for node 3 running on CORE +The *vnoded* daemon is the program used to create a new namespace, and listen on a control channel for commands that +may instantiate other processes. This daemon runs as PID 1 in the container. It is launched automatically by the CORE +daemon. The control channel is a UNIX domain socket usually named */tmp/pycore.23098/n3*, for node 3 running on CORE session 23098, for example. Root privileges are required for creating a new namespace. ### vcmd -The *vcmd* program is used to connect to the *vnoded* daemon in a Linux network namespace, for running commands in the -namespace. The CORE daemon uses the same channel for setting up a node and running processes within it. This program -has two required arguments, the control channel name, and the command line to be run within the namespace. This command +The *vcmd* program is used to connect to the *vnoded* daemon in a Linux network namespace, for running commands in the +namespace. The CORE daemon uses the same channel for setting up a node and running processes within it. This program +has two required arguments, the control channel name, and the command line to be run within the namespace. This command does not need to run with root privileges. -When you double-click on a node in a running emulation, CORE will open a shell window for that node using a command +When you double-click on a node in a running emulation, CORE will open a shell window for that node using a command such as: ```shell @@ -144,13 +154,13 @@ vcmd -c /tmp/pycore.50160/n1 -- /sbin/ip -4 ro ### core-cleanup script -A script named *core-cleanup* is provided to clean up any running CORE emulations. It will attempt to kill any -remaining vnoded processes, kill any EMANE processes, remove the :file:`/tmp/pycore.*` session directories, and remove +A script named *core-cleanup* is provided to clean up any running CORE emulations. It will attempt to kill any +remaining vnoded processes, kill any EMANE processes, remove the :file:`/tmp/pycore.*` session directories, and remove any bridges or *ebtables* rules. With a *-d* option, it will also kill any running CORE daemon. ### netns command -The *netns* command is not used by CORE directly. This utility can be used to run a command in a new network namespace +The *netns* command is not used by CORE directly. This utility can be used to run a command in a new network namespace for testing purposes. It does not open a control channel for receiving further commands. ### Other Useful Commands From 09756eb7ab668dca167e22d579c36874e4134fee Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 20 Dec 2019 12:55:52 -0800 Subject: [PATCH 446/462] updates for devguide doc --- docs/devguide.md | 93 +++++++++++++++++++++++++++++------------------- docs/install.md | 40 ++++++++++----------- 2 files changed, 76 insertions(+), 57 deletions(-) diff --git a/docs/devguide.md b/docs/devguide.md index 319c9fbd..53c1a8b0 100644 --- a/docs/devguide.md +++ b/docs/devguide.md @@ -21,15 +21,59 @@ 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. +the core-daemon for development based on Ubuntu 18.04. + +### Install Dependencies + +```shell +sudo apt install -y automake pkg-config gcc libev-dev bridge-utils 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 ```shell +cd ~/Documents git clone https://github.com/coreemu/core.git cd core ``` +### Build CORE + +```shell +./bootstrap.sh +./configure +make -j8 +``` + +### 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+. @@ -38,14 +82,11 @@ To leverage the dev environment you need python 3.6+. # change to daemon directory cd $REPO/daemon -# copy setup.py for installation -cp setup.py.in setup.py - # install pipenv -pip3 install pipenv +sudo pip3 install pipenv # setup a virtual environment and install all required development dependencies -python3 -m pipenv install --dev +pipenv install --dev ``` ### Setup pre-commit @@ -55,29 +96,7 @@ python utilities will be ran to check validity of code, potentially failing and one to review changes being made by tools ro the fix the issue noted. Then add the changes and commit again. ```shell -python3 -m pipenv run pre-commit install -``` - -### Build CORE - -```shell -./bootstrap.sh -./configure --prefix=/usr -make -``` - -### Install netns and GUI - -Below commands assume your development will be focused on the daemon. - -```shell -# install GUI -cd $REPO/gui -sudo make install - -# install netns scripts -cd $REPO/netns -sudo make install +pipenv run pre-commit install ``` ### Adding EMANE to Pipenv @@ -99,7 +118,8 @@ sudo apt install libxml2-dev libprotobuf-dev uuid-dev libpcap-dev protobuf-compi make -j8 # install emane binding in pipenv -python3 -m pipenv install $EMANEREPO/src/python +# NOTE: this will mody pipenv Pipfiles and we do not want that, use git checkout -- Pipfile*, to remove changes +pipenv install $EMANEREPO/src/python ``` ### Running CORE @@ -107,15 +127,14 @@ python3 -m pipenv install $EMANEREPO/src/python This will run the core-daemon server using the configuration files within the repo. ```shell -sudo python3 -m pipenv run core -``` +# runs for daemon +sudo pipenv run core -### Running CORE Python GUI +# runs coretk gui +pipenv run coretk -Must be ran after the daemon above or will fail to connect. - -```shell -python3 -m pipenv run coretk +# runs mocked unit tests +pipenv run test-mock ``` ## Linux Network Namespace Commands diff --git a/docs/install.md b/docs/install.md index e10c1450..a9ebb092 100644 --- a/docs/install.md +++ b/docs/install.md @@ -9,19 +9,19 @@ This section will describe how to install CORE from source or from a pre-built p # Required Hardware -Any computer capable of running Linux should be able to run CORE. Since the physical machine will be hosting numerous +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 -CORE requires a Linux operating system because it uses virtualization provided by the kernel. It does not run on -Windows or Mac OS X operating systems (unless it is running within a virtual machine guest.) The virtualization +CORE requires a Linux operating system because it uses 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 Fedora/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 +**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 @@ -64,12 +64,12 @@ sudo python3 -m pip install -r requirements.txt # Pre-Req Installing OSPF MDR -Virtual networks generally require some form of routing in order to work (e.g. to automatically populate routing -tables for routing packets from one subnet to another.) CORE builds OSPF routing protocol configurations by -default when the blue router node type is used. +Virtual networks generally require some form of routing in order to work (e.g. to automatically populate routing +tables for routing packets from one subnet to another.) CORE builds OSPF routing protocol configurations by +default when the blue router node type is used. -* [OSPF MANET Designated Routers](https://github.com/USNavalResearchLaboratory/ospf-mdr) (MDR) - the Quagga routing -suite with a modified version of OSPFv3, optimized for use with mobile wireless networks. The **mdr** node type +* [OSPF MANET Designated Routers](https://github.com/USNavalResearchLaboratory/ospf-mdr) (MDR) - the Quagga routing +suite with a modified version of OSPFv3, optimized for use with mobile wireless networks. The **mdr** node type (and the MDR service) requires this variant of Quagga. ## Ubuntu <= 16.04 and Fedora/CentOS @@ -87,7 +87,7 @@ Requires building from source, from the latest nightly snapshot. ```shell # packages needed beyond what's normally required to build core on ubuntu -sudo apt install libtool libreadline-dev autoconf +sudo apt install libtool libreadline-dev autoconf gawk git clone https://github.com/USNavalResearchLaboratory/ospf-mdr cd ospf-mdr @@ -99,8 +99,8 @@ make sudo make install ``` -Note that the configuration directory */usr/local/etc/quagga* shown for Quagga above could be */etc/quagga*, -if you create a symbolic link from */etc/quagga/Quagga.conf -> /usr/local/etc/quagga/Quagga.conf* on the host. +Note that the configuration directory */usr/local/etc/quagga* shown for Quagga above could be */etc/quagga*, +if you create a symbolic link from */etc/quagga/Quagga.conf -> /usr/local/etc/quagga/Quagga.conf* on the host. The *quaggaboot.sh* script in a Linux network namespace will try and do this for you if needed. If you try to run quagga after installing from source and get an error such as: @@ -113,8 +113,8 @@ this is usually a sign that you have to run ```sudo ldconfig```` to refresh the # Installing from Packages -The easiest way to install CORE is using the pre-built packages. The package managers on Ubuntu or Fedora/CentOS -will help in automatically installing most dependencies, except for the python ones described previously. +The easiest way to install CORE is using the pre-built packages. The package managers on Ubuntu or Fedora/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). @@ -134,13 +134,13 @@ Run the CORE GUI as a normal user: core-gui ``` -After running the *core-gui* command, a GUI should appear with a canvas for drawing topologies. +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 **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** +on CentOS <= 6, or build from source otherwise** ```shell yum install ./core_python3_$VERSION_x86_64.rpm @@ -198,7 +198,7 @@ After running the *core-gui* command, a GUI should appear with a canvas for draw # Building and Installing from Source -This option is listed here for developers and advanced users who are comfortable patching and building source code. +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 @@ -210,7 +210,7 @@ You can obtain the CORE source from the [CORE GitHub](https://github.com/coreemu Python module grpcio-tools is currently needed to generate code from the CORE protobuf file during the build. ```shell -python3 -m pip install grpcio-tools +python3 -m pip install grpcio-tools ``` ## Distro Requirements @@ -270,4 +270,4 @@ mkdir /tmp/core-build make fpm DESTDIR=/tmp/core-build ``` -This will produce and RPM and Deb package for the currently configured python version. +This will produce and RPM and Deb package for the currently configured python version. From d4fae0d89e81330db2ebea4e390cf6e14e62b5b7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 20 Dec 2019 13:30:55 -0800 Subject: [PATCH 447/462] changes to fix emane config data leveraging emane prefix to work as intended --- daemon/core/emane/emanemanager.py | 84 +++++++++++++++++-------------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index dc3e2acf..a6237a7a 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -5,6 +5,7 @@ emane.py: definition of an Emane class for implementing configuration control of import logging import os import threading +from collections import OrderedDict from core import utils from core.config import ConfigGroup, Configuration, ModelManager @@ -40,6 +41,7 @@ EMANE_MODELS = [ EmaneTdmaModel, ] DEFAULT_EMANE_PREFIX = "/usr" +DEFAULT_DEV = "ctrl0" class EmaneManager(ModelManager): @@ -817,57 +819,61 @@ class EmaneManager(ModelManager): return result -class EmaneGlobalModel(EmaneModel): +class EmaneGlobalModel: """ Global EMANE configuration options. """ - _DEFAULT_DEV = "ctrl0" - name = "emane" + bitmap = None - emulator_xml = "/usr/share/emane/manifest/nemmanager.xml" - emulator_defaults = { - "eventservicedevice": _DEFAULT_DEV, - "eventservicegroup": "224.1.2.8:45703", - "otamanagerdevice": _DEFAULT_DEV, - "otamanagergroup": "224.1.2.8:45702", - } - emulator_config = emanemanifest.parse(emulator_xml, emulator_defaults) - emulator_config.insert( - 0, - Configuration( - _id="platform_id_start", - _type=ConfigDataTypes.INT32, - default="1", - label="Starting Platform ID (core)", - ), - ) + def __init__(self, session): + self.session = session + self.nem_config = [ + Configuration( + _id="nem_id_start", + _type=ConfigDataTypes.INT32, + default="1", + label="Starting NEM ID (core)", + ) + ] + self.emulator_config = None + self.parse_config() - nem_config = [ - Configuration( - _id="nem_id_start", - _type=ConfigDataTypes.INT32, - default="1", - label="Starting NEM ID (core)", + def parse_config(self): + emane_prefix = self.session.options.get_config( + "emane_prefix", default=DEFAULT_EMANE_PREFIX + ) + emulator_xml = os.path.join(emane_prefix, "share/emane/manifest/nemmanager.xml") + emulator_defaults = { + "eventservicedevice": DEFAULT_DEV, + "eventservicegroup": "224.1.2.8:45703", + "otamanagerdevice": DEFAULT_DEV, + "otamanagergroup": "224.1.2.8:45702", + } + self.emulator_config = emanemanifest.parse(emulator_xml, emulator_defaults) + self.emulator_config.insert( + 0, + Configuration( + _id="platform_id_start", + _type=ConfigDataTypes.INT32, + default="1", + label="Starting Platform ID (core)", + ), ) - ] - @classmethod - def configurations(cls): - return cls.emulator_config + cls.nem_config + def configurations(self): + return self.emulator_config + self.nem_config - @classmethod - def config_groups(cls): - emulator_len = len(cls.emulator_config) - config_len = len(cls.configurations()) + def config_groups(self): + emulator_len = len(self.emulator_config) + config_len = len(self.configurations()) return [ ConfigGroup("Platform Attributes", 1, emulator_len), ConfigGroup("NEM Parameters", emulator_len + 1, config_len), ] - def __init__(self, session, _id=None): - super().__init__(session, _id) - - def build_xml_files(self, config, interface=None): - raise NotImplementedError + def default_values(self): + return OrderedDict( + [(config.id, config.default) for config in self.configurations()] + ) From 6d680341776044f9995b8961a25fb9ca89ff30fe Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 20 Dec 2019 15:11:34 -0800 Subject: [PATCH 448/462] updated start proto to return exception strings, updated grpc start session to exist early when a failure is found, updated coretk ui to not switch ui to running when start fails and display error dialog --- daemon/core/api/grpc/server.py | 25 ++++++++++++++++---- daemon/core/emulator/session.py | 30 +++++++++++++----------- daemon/core/gui/coreclient.py | 33 ++++++++++++++++++--------- daemon/core/gui/statusbar.py | 7 +++--- daemon/core/gui/toolbar.py | 3 ++- daemon/core/services/coreservices.py | 4 ++-- daemon/core/services/utility.py | 3 ++- daemon/proto/core/api/grpc/core.proto | 1 + 8 files changed, 70 insertions(+), 36 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index bf2a8ac8..ea343165 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -124,7 +124,10 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session.add_hook(hook.state, hook.file, None, hook.data) # create nodes - grpcutils.create_nodes(session, request.nodes) + _, exceptions = grpcutils.create_nodes(session, request.nodes) + if exceptions: + exceptions = [str(x) for x in exceptions] + return core_pb2.StartSessionResponse(result=False, exceptions=exceptions) # emane configs config = session.emane.get_configs() @@ -156,14 +159,28 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): ) # create links - grpcutils.create_links(session, request.links) + _, exceptions = grpcutils.create_links(session, request.links) + if exceptions: + exceptions = [str(x) for x in exceptions] + return core_pb2.StartSessionResponse(result=False, exceptions=exceptions) # asymmetric links - grpcutils.edit_links(session, request.asymmetric_links) + _, exceptions = grpcutils.edit_links(session, request.asymmetric_links) + if exceptions: + exceptions = [str(x) for x in exceptions] + return core_pb2.StartSessionResponse(result=False, exceptions=exceptions) # set to instantiation and start session.set_state(EventTypes.INSTANTIATION_STATE) - session.instantiate() + + # boot services + boot_exceptions = session.instantiate() + if boot_exceptions: + exceptions = [] + for boot_exception in boot_exceptions: + for service_exception in boot_exception.args: + exceptions.append(str(service_exception)) + return core_pb2.StartSessionResponse(result=False, exceptions=exceptions) return core_pb2.StartSessionResponse(result=True) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index bc2f750e..6b1db6b8 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -635,7 +635,6 @@ class Session: :return: created node :raises core.CoreError: when an invalid node type is given """ - # validate node type, get class, or throw error if _cls is None: node_class = self.get_node_class(_type) @@ -1450,17 +1449,19 @@ class Session: return # boot node services and then start mobility - self.boot_nodes() - self.mobility.startup() + exceptions = self.boot_nodes() + if not exceptions: + self.mobility.startup() - # notify listeners that instantiation is complete - event = EventData(event_type=EventTypes.INSTANTIATION_COMPLETE.value) - self.broadcast_event(event) + # notify listeners that instantiation is complete + event = EventData(event_type=EventTypes.INSTANTIATION_COMPLETE.value) + self.broadcast_event(event) - # assume either all nodes have booted already, or there are some - # nodes on slave servers that will be booted and those servers will - # send a node status response message - self.check_runtime() + # assume either all nodes have booted already, or there are some + # nodes on slave servers that will be booted and those servers will + # send a node status response message + self.check_runtime() + return exceptions def get_node_count(self): """ @@ -1577,6 +1578,9 @@ class Session: Invoke the boot() procedure for all nodes and send back node messages to the GUI for node messages that had the status request flag. + + :return: service boot exceptions + :rtype: list[core.services.coreservices.ServiceBootError] """ with self._nodes_lock: funcs = [] @@ -1589,9 +1593,9 @@ class Session: results, exceptions = utils.threadpool(funcs) total = time.monotonic() - start logging.debug("boot run time: %s", total) - if exceptions: - raise CoreError(exceptions) - self.update_control_interface_hosts() + if not exceptions: + self.update_control_interface_hosts() + return exceptions def get_control_net_prefixes(self): """ diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 65789b23..0952af66 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -6,6 +6,7 @@ import logging import os import time from pathlib import Path +from tkinter import messagebox import grpc @@ -476,21 +477,31 @@ class CoreClient: file_configs, asymmetric_links, ) - self.set_metadata() - process_time = time.perf_counter() - start logging.debug( "start session(%s), result: %s", self.session_id, response.result ) - self.app.statusbar.start_session_callback(process_time) + process_time = time.perf_counter() - start - # display mobility players - for node_id, config in self.mobility_configs.items(): - canvas_node = self.canvas_nodes[node_id] - mobility_player = MobilityPlayer( - self.app, self.app, canvas_node, config - ) - mobility_player.show() - self.mobility_players[node_id] = mobility_player + # stop progress bar and update status + self.app.statusbar.progress_bar.stop() + message = f"Start ran for {process_time:.3f} seconds" + self.app.statusbar.set_status(message) + + if response.result: + self.set_metadata() + self.app.toolbar.set_runtime() + + # display mobility players + for node_id, config in self.mobility_configs.items(): + canvas_node = self.canvas_nodes[node_id] + mobility_player = MobilityPlayer( + self.app, self.app, canvas_node, config + ) + mobility_player.show() + self.mobility_players[node_id] = mobility_player + else: + message = "\n".join(response.exceptions) + messagebox.showerror("Start Error", message) except grpc.RpcError as e: show_grpc_error(e) diff --git a/daemon/core/gui/statusbar.py b/daemon/core/gui/statusbar.py index ab0ba4e7..1567e799 100644 --- a/daemon/core/gui/statusbar.py +++ b/daemon/core/gui/statusbar.py @@ -68,10 +68,9 @@ class StatusBar(ttk.Frame): dialog = AlertsDialog(self.app, self.app) dialog.show() - def start_session_callback(self, process_time): - self.progress_bar.stop() - self.statusvar.set(f"Session started in {process_time:.3f} seconds") + def set_status(self, message): + self.statusvar.set(message) def stop_session_callback(self, cleanup_time): self.progress_bar.stop() - self.statusvar.set(f"Stopped session in {cleanup_time:.3f} seconds") + self.statusvar.set(f"Stopped in {cleanup_time:.3f} seconds") diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index ced159c5..a4229ddd 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -236,11 +236,12 @@ class Toolbar(ttk.Frame): :return: nothing """ self.app.canvas.hide_context() - self.app.statusbar.core_alarms.clear() self.app.statusbar.progress_bar.start(5) self.app.canvas.mode = GraphMode.SELECT thread = threading.Thread(target=self.app.core.start_session) thread.start() + + def set_runtime(self): self.runtime_frame.tkraise() self.click_runtime_selection() diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index 4372dd88..686023a2 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -458,7 +458,7 @@ class CoreServices: """ Start all services on a node. - :param core.netns.vnode.LxcNode node: node to start services on + :param core.nodes.base.CoreNode node: node to start services on :return: nothing """ boot_paths = ServiceDependencies(node.services).boot_paths() @@ -468,7 +468,7 @@ class CoreServices: funcs.append((self._start_boot_paths, args, {})) result, exceptions = utils.threadpool(funcs) if exceptions: - raise ServiceBootError(exceptions) + raise ServiceBootError(*exceptions) def _start_boot_paths(self, node, boot_path): """ diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index 16dc6906..fa2b2672 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -8,7 +8,7 @@ from core import constants, utils from core.errors import CoreCommandError from core.nodes import ipaddress from core.nodes.ipaddress import Ipv4Prefix, Ipv6Prefix -from core.services.coreservices import CoreService +from core.services.coreservices import CoreService, ServiceMode class UtilService(CoreService): @@ -173,6 +173,7 @@ class SshService(UtilService): startup = ("sh startsshd.sh",) shutdown = ("killall sshd",) validate = () + validation_mode = ServiceMode.BLOCKING @classmethod def generate_config(cls, node, filename): diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 050d5e75..856ed7aa 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -155,6 +155,7 @@ message StartSessionRequest { message StartSessionResponse { bool result = 1; + repeated string exceptions = 2; } message StopSessionRequest { From fb5f1a771c8d4e0873ed9a3775b23084d59ffe79 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 21 Dec 2019 02:45:05 -0500 Subject: [PATCH 449/462] updates to add centos install support --- install.sh | 51 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/install.sh b/install.sh index 14cc9d6b..9c4e71cb 100755 --- a/install.sh +++ b/install.sh @@ -3,23 +3,11 @@ # exit on error set -e -# detect os/ver for install type -os="" -if [[ -f /etc/os-release ]]; then - . /etc/os-release - os=${NAME} -fi - -# check install was found -if [[ ${os} == "Ubuntu" ]]; then - # install system dependencies - sudo apt install -y automake pkg-config gcc libev-dev bridge-utils ebtables gawk \ - python3.6 python3.6-dev python3-pip python3-tk tk libtk-img ethtool libtool libreadline-dev autoconf - - # install python dependencies +function install_python_depencencies() { sudo python3 -m pip install -r daemon/requirements.txt +} - # make and install ospf mdr +function install_ospf_mdr() { git clone https://github.com/USNavalResearchLaboratory/ospf-mdr /tmp/ospf-mdr cd /tmp/ospf-mdr ./bootstrap.sh @@ -29,12 +17,39 @@ if [[ ${os} == "Ubuntu" ]]; then make -j8 sudo make install cd - +} - # build and install core +function install_core() { ./bootstrap.sh ./configure make -j8 sudo make install -else - echo "unknown os ${os} cannot install" +} + +# detect os/ver for install type +os="" +if [[ -f /etc/os-release ]]; then + . /etc/os-release + os=${NAME} fi + +# check install was found +case ${os} in +"Ubuntu") + sudo apt install -y automake pkg-config gcc libev-dev bridge-utils ebtables gawk \ + python3.6 python3.6-dev python3-pip python3-tk tk libtk-img ethtool libtool libreadline-dev autoconf + install_python_depencencies + install_ospf_mdr + install_core + ;; +"CentOS Linux") + sudo yum install -y automake pkgconf-pkg-config gcc libev-devel bridge-utils iptables-ebtables gawk \ + python36 python36-devel python3-pip python3-tk tk ethtool libtool readline-devel autoconf + install_python_depencencies + install_ospf_mdr + install_core + ;; +*) + echo "unknown os ${os} cannot install" + ;; +esac From ace15636ac18bdd4889588320505953f014aea32 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 21 Dec 2019 11:53:14 -0800 Subject: [PATCH 450/462] Update install.sh updates for centos --- install.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/install.sh b/install.sh index 9c4e71cb..df22cba9 100755 --- a/install.sh +++ b/install.sh @@ -21,7 +21,7 @@ function install_ospf_mdr() { function install_core() { ./bootstrap.sh - ./configure + ./configure $1 make -j8 sudo make install } @@ -43,11 +43,11 @@ case ${os} in install_core ;; "CentOS Linux") - sudo yum install -y automake pkgconf-pkg-config gcc libev-devel bridge-utils iptables-ebtables gawk \ - python36 python36-devel python3-pip python3-tk tk ethtool libtool readline-devel autoconf + sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel bridge-utils iptables-ebtables gawk \ + python36 python36-devel python3-pip python3-tkinter tk ethtool libtool readline-devel autoconf install_python_depencencies install_ospf_mdr - install_core + install_core --prefix=/usr ;; *) echo "unknown os ${os} cannot install" From 0d0862d29c4c402441ac9baaf5ca358b536da317 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 23 Dec 2019 09:28:49 -0800 Subject: [PATCH 451/462] added install.sh options for dev install --- install.sh | 101 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 72 insertions(+), 29 deletions(-) diff --git a/install.sh b/install.sh index df22cba9..9658f813 100755 --- a/install.sh +++ b/install.sh @@ -4,52 +4,95 @@ set -e function install_python_depencencies() { - sudo python3 -m pip install -r daemon/requirements.txt + sudo python3 -m pip install -r daemon/requirements.txt +} + +function install_python_dev_dependencies() { + sudp pip install pipenv grpcio-tools } function install_ospf_mdr() { - git clone https://github.com/USNavalResearchLaboratory/ospf-mdr /tmp/ospf-mdr - cd /tmp/ospf-mdr - ./bootstrap.sh - ./configure --disable-doc --enable-user=root --enable-group=root --with-cflags=-ggdb \ - --sysconfdir=/usr/local/etc/quagga --enable-vtysh \ - --localstatedir=/var/run/quagga - make -j8 - sudo make install - cd - + rm -rf /tmp/ospf-mdr + git clone https://github.com/USNavalResearchLaboratory/ospf-mdr /tmp/ospf-mdr + cd /tmp/ospf-mdr + ./bootstrap.sh + ./configure --disable-doc --enable-user=root --enable-group=root --with-cflags=-ggdb \ + --sysconfdir=/usr/local/etc/quagga --enable-vtysh \ + --localstatedir=/var/run/quagga + make -j8 + sudo make install + cd - +} + +function build_core() { + ./bootstrap.sh + ./configure $1 + make -j8 } function install_core() { - ./bootstrap.sh - ./configure $1 - make -j8 - sudo make install + sudo make install +} + +function install_dev_core() { + cd daemon + pipenv install --dev } # detect os/ver for install type os="" if [[ -f /etc/os-release ]]; then - . /etc/os-release - os=${NAME} + . /etc/os-release + os=${ID} fi +# parse arguments +while getopts ":d" opt; do + case ${opt} in + d) + dev=1 + ;; + \?) + echo "Invalid Option: $OPTARG" 1>&2 + ;; + esac +done +shift $((OPTIND - 1)) + # check install was found case ${os} in -"Ubuntu") - sudo apt install -y automake pkg-config gcc libev-dev bridge-utils ebtables gawk \ - python3.6 python3.6-dev python3-pip python3-tk tk libtk-img ethtool libtool libreadline-dev autoconf +"ubuntu") + echo "Installing CORE for Ubuntu" + sudo apt install -y automake pkg-config gcc libev-dev bridge-utils ebtables gawk iproute2 \ + python3.6 python3.6-dev python3-pip python3-tk tk libtk-img ethtool libtool libreadline-dev autoconf + install_ospf_mdr + if [[ -z ${dev} ]]; then + echo "normal install" install_python_depencencies - install_ospf_mdr + build_core install_core - ;; -"CentOS Linux") - sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel bridge-utils iptables-ebtables gawk \ - python36 python36-devel python3-pip python3-tkinter tk ethtool libtool readline-devel autoconf + else + echo "dev install" + install_python_dev_dependencies + build_core + install_dev_core + fi + ;; +"centos") + sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel bridge-utils iptables-ebtables iproute \ + python36 python36-devel python3-pip python3-tkinter tk ethtool libtool readline-devel autoconf gawk + install_ospf_mdr + if [[ -z ${dev} ]]; then install_python_depencencies - install_ospf_mdr - install_core --prefix=/usr - ;; + build_core --prefix=/usr + install_core + else + install_python_dev_dependencies + build_core --prefix=/usr + install_dev_core + fi + ;; *) - echo "unknown os ${os} cannot install" - ;; + echo "unknown OS ID ${os} cannot install" + ;; esac From 8029a27bb4c2b631dde88fe73d5c5ead1d9f2ec8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 23 Dec 2019 13:36:25 -0800 Subject: [PATCH 452/462] added installing netns and classic gui to dev install --- install.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/install.sh b/install.sh index 9658f813..1b633c2c 100755 --- a/install.sh +++ b/install.sh @@ -35,8 +35,15 @@ function install_core() { } function install_dev_core() { + cd gui + sudo make install + cd - + cd netns + sudo make install + cd - cd daemon pipenv install --dev + cd - } # detect os/ver for install type From fe8bc6f10e494851226d6d72f01f4ff3f7b09e3f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 23 Dec 2019 14:48:56 -0800 Subject: [PATCH 453/462] removed usage of brctl and dependency on bridge-utils library as it is deprecated, replaced with using iproute instead --- Makefile.am | 3 --- configure.ac | 5 ----- daemon/core/constants.py.in | 1 - daemon/core/nodes/netclient.py | 33 ++++++++++++++------------------- docs/devguide.md | 4 ++-- docs/install.md | 12 ++++++------ docs/usage.md | 12 ++++++------ install.sh | 4 ++-- 8 files changed, 30 insertions(+), 44 deletions(-) diff --git a/Makefile.am b/Makefile.am index c51c3707..ed52cdbb 100644 --- a/Makefile.am +++ b/Makefile.am @@ -60,7 +60,6 @@ fpm -s dir -t rpm -n core \ -d "tk" \ -d "procps-ng" \ -d "bash >= 3.0" \ - -d "bridge-utils" \ -d "ebtables" \ -d "iproute" \ -d "libev" \ @@ -88,7 +87,6 @@ fpm -s dir -t deb -n core \ -d "procps" \ -d "libc6 >= 2.14" \ -d "bash >= 3.0" \ - -d "bridge-utils" \ -d "ebtables" \ -d "iproute2" \ -d "libev4" \ @@ -128,7 +126,6 @@ $(info creating file $1 from $1.in) -e 's,[@]CORE_DATA_DIR[@],$(CORE_DATA_DIR),g' \ -e 's,[@]CORE_CONF_DIR[@],$(CORE_CONF_DIR),g' \ -e 's,[@]CORE_GUI_CONF_DIR[@],$(CORE_GUI_CONF_DIR),g' \ - -e 's,[@]brctl_path[@],$(brctl_path),g' \ -e 's,[@]sysctl_path[@],$(sysctl_path),g' \ -e 's,[@]ip_path[@],$(ip_path),g' \ -e 's,[@]tc_path[@],$(tc_path),g' \ diff --git a/configure.ac b/configure.ac index 43a5815f..9c7f799a 100644 --- a/configure.ac +++ b/configure.ac @@ -113,11 +113,6 @@ if test "x$enable_daemon" = "xyes"; then AM_PATH_PYTHON(3.6) AS_IF([$PYTHON -m grpc_tools.protoc -h &> /dev/null], [], [AC_MSG_ERROR([please install python grpcio-tools])]) - AC_CHECK_PROG(brctl_path, brctl, $as_dir, no, $SEARCHPATH) - if test "x$brctl_path" = "xno" ; then - AC_MSG_ERROR([Could not locate brctl (from bridge-utils package).]) - fi - AC_CHECK_PROG(sysctl_path, sysctl, $as_dir, no, $SEARCHPATH) if test "x$sysctl_path" = "xno" ; then AC_MSG_ERROR([Could not locate sysctl (from procps package).]) diff --git a/daemon/core/constants.py.in b/daemon/core/constants.py.in index 38e2b67f..54f3a1c3 100644 --- a/daemon/core/constants.py.in +++ b/daemon/core/constants.py.in @@ -8,7 +8,6 @@ FRR_STATE_DIR = "@CORE_STATE_DIR@/run/frr" VNODED_BIN = which("vnoded", required=True) VCMD_BIN = which("vcmd", required=True) -BRCTL_BIN = which("brctl", required=True) SYSCTL_BIN = which("sysctl", required=True) IP_BIN = which("ip", required=True) ETHTOOL_BIN = which("ethtool", required=True) diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 4a7250f0..8053a0e8 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -1,8 +1,9 @@ """ Clients for dealing with bridge/interface commands. """ +import json -from core.constants import BRCTL_BIN, ETHTOOL_BIN, IP_BIN, OVS_BIN, TC_BIN +from core.constants import ETHTOOL_BIN, IP_BIN, OVS_BIN, TC_BIN def get_net_client(use_ovs, run): @@ -231,17 +232,12 @@ class LinuxNetClient: :param str name: bridge name :return: nothing """ - self.run(f"{BRCTL_BIN} addbr {name}") - self.run(f"{BRCTL_BIN} stp {name} off") - self.run(f"{BRCTL_BIN} setfd {name} 0") + self.run(f"{IP_BIN} link add name {name} type bridge") + self.run(f"{IP_BIN} link set {name} type bridge stp_state 0") + self.run(f"{IP_BIN} link set {name} type bridge forward_delay 0") + self.run(f"{IP_BIN} link set {name} type bridge mcast_snooping 0") self.device_up(name) - # turn off multicast snooping so forwarding occurs w/o IGMP joins - snoop_file = "multicast_snooping" - snoop = f"/sys/devices/virtual/net/{name}/bridge/{snoop_file}" - self.run(f"echo 0 > /tmp/{snoop_file}", shell=True) - self.run(f"cp /tmp/{snoop_file} {snoop}") - def delete_bridge(self, name): """ Bring down and delete a Linux bridge. @@ -250,7 +246,7 @@ class LinuxNetClient: :return: nothing """ self.device_down(name) - self.run(f"{BRCTL_BIN} delbr {name}") + self.run(f"{IP_BIN} link delete {name} type bridge") def create_interface(self, bridge_name, interface_name): """ @@ -260,7 +256,7 @@ class LinuxNetClient: :param str interface_name: interface name :return: nothing """ - self.run(f"{BRCTL_BIN} addif {bridge_name} {interface_name}") + self.run(f"{IP_BIN} link set dev {interface_name} master {bridge_name}") self.device_up(interface_name) def delete_interface(self, bridge_name, interface_name): @@ -271,7 +267,7 @@ class LinuxNetClient: :param str interface_name: interface name :return: nothing """ - self.run(f"{BRCTL_BIN} delif {bridge_name} {interface_name}") + self.run(f"{IP_BIN} link set dev {interface_name} nomaster") def existing_bridges(self, _id): """ @@ -279,11 +275,10 @@ class LinuxNetClient: :param _id: node id to check bridges for """ - output = self.run(f"{BRCTL_BIN} show") - lines = output.split("\n") - for line in lines[1:]: - columns = line.split() - name = columns[0] + output = self.run(f"{IP_BIN} -j link show type bridge") + bridges = json.loads(output) + for bridge in bridges: + name = bridge["ifname"] fields = name.split(".") if len(fields) != 3: continue @@ -298,7 +293,7 @@ class LinuxNetClient: :param str name: bridge name :return: nothing """ - self.run(f"{BRCTL_BIN} setageing {name} 0") + self.run(f"{IP_BIN} link set {name} type bridge ageing_time 0") class OvsNetClient(LinuxNetClient): diff --git a/docs/devguide.md b/docs/devguide.md index 53c1a8b0..e0f40497 100644 --- a/docs/devguide.md +++ b/docs/devguide.md @@ -26,7 +26,7 @@ the core-daemon for development based on Ubuntu 18.04. ### Install Dependencies ```shell -sudo apt install -y automake pkg-config gcc libev-dev bridge-utils ebtables gawk \ +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 ``` @@ -188,7 +188,7 @@ Here are some other Linux commands that are useful for managing the Linux networ ```shell # view the Linux bridging setup -brctl show +ip link show type bridge # view the netem rules used for applying link effects tc qdisc show # view the rules that make the wireless LAN work diff --git a/docs/install.md b/docs/install.md index a9ebb092..251c3669 100644 --- a/docs/install.md +++ b/docs/install.md @@ -218,26 +218,26 @@ python3 -m pip install grpcio-tools ### Ubuntu 18.04 Requirements ```shell -sudo apt install automake pkg-config gcc libev-dev bridge-utils ebtables python3-dev python3-setuptools tk libtk-img ethtool +sudo apt install automake pkg-config gcc libev-dev ebtables python3-dev python3-setuptools tk libtk-img ethtool ``` ### Ubuntu 16.04 Requirements ```shell -sudo apt-get install automake bridge-utils ebtables python3-dev libev-dev python3-setuptools libtk-img ethtool +sudo apt-get install automake ebtables python3-dev libev-dev python3-setuptools libtk-img ethtool ``` ### CentOS 7 with Gnome Desktop Requirements ```shell -sudo yum -y install automake gcc python3-devel python3-devel libev-devel tk ethtool +sudo yum -y install automake gcc python36 python36-devel libev-devel tk ethtool ``` ## Build and Install ```shell ./bootstrap.sh -PYTHON=python3 ./configure +./configure make sudo make install ``` @@ -251,7 +251,7 @@ sudo apt install python3-sphinx sudo yum install python3-sphinx ./bootstrap.sh -PYTHON=python3 ./configure +./configure make doc ``` @@ -264,7 +264,7 @@ Build package commands, DESTDIR is used to make install into and then for packag ```shell ./bootstrap.sh -PYTHON=python3 ./configure +./configure make mkdir /tmp/core-build make fpm DESTDIR=/tmp/core-build diff --git a/docs/usage.md b/docs/usage.md index 02e1295f..80b96c05 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -15,7 +15,7 @@ CORE can be used via the GUI or [Python_Scripting](scripting.md). Often the GUI The following image shows the various phases of a CORE session: ![](static/core-workflow.jpg) -After pressing the start button, CORE will proceed through these phases, staying in the **runtime** phase. After the session is stopped, CORE will proceed to the **data collection** phase before tearing down the emulated state. +After pressing the start button, CORE will proceed through these phases, staying in the **runtime** phase. After the session is stopped, CORE will proceed to the **data collection** phase before tearing down the emulated state. CORE can be customized to perform any action at each phase in the workflow above. See the *Hooks...* entry on the **Session Menu** for details about when these session states are reached. @@ -33,7 +33,7 @@ sudo systemctl start core-daemon sudo service core-daemon start ``` -You can also invoke the daemon directly from the command line, which can be useful if you'd like to see the logging output directly. +You can also invoke the daemon directly from the command line, which can be useful if you'd like to see the logging output directly. ``` # direct invocation sudo core-daemon @@ -94,7 +94,7 @@ When CORE is in Edit mode (the default), the vertical Editing Toolbar exists on | ![alt text](../gui/icons/tiny/rj45.gif) *RJ45* | with the RJ45 Physical Interface Tool, emulated nodes can be linked to real physical interfaces; using this tool, real networks and devices can be physically connected to the live-running emulation. | | ![alt text](../gui/icons/tiny/tunnel.gif) *Tunnel* | the Tunnel Tool allows connecting together more than one CORE emulation using GRE tunnels. | -### Anotation Tools +### Anotation Tools | Tool | Functionality | |---|---| @@ -206,7 +206,7 @@ The tools menu lists different utility functions. |---|---| | *Random* | nodes are randomly placed about the canvas, but are not linked together. This can be used in conjunction with a WLAN node to quickly create a wireless network. | | *Grid* | nodes are placed in horizontal rows starting in the upper-left corner, evenly spaced to the right; nodes are not linked to each other. | -| *Connected Grid* | nodes are placed in an N x M (width and height) rectangular grid, and each node is linked to the node above, below, left and right of itself. | +| *Connected Grid* | nodes are placed in an N x M (width and height) rectangular grid, and each node is linked to the node above, below, left and right of itself. | | *Chain* | nodes are linked together one after the other in a chain. | | *Star* | one node is placed in the center with N nodes surrounding it in a circular pattern, with each node linked to the center node. | | *Cycle* | nodes are arranged in a circular pattern with every node connected to its neighbor to form a closed circular path. | @@ -464,7 +464,7 @@ to **dummy0**, and link this to a node in your scenario. After starting the session, configure an address on the host. ```shell -sudo brctl show +sudo ip link show type bridge # determine bridge name from the above command # assign an IP address on the same network as the linked node sudo ip addr add 10.0.1.2/24 dev b.48304.34658 @@ -500,7 +500,7 @@ The wireless LAN (WLAN) is covered in the next section. The wireless LAN node allows you to build wireless networks where moving nodes around affects the connectivity between them. Connection between a pair of nodes is stronger when the nodes are closer while connection is weaker when the nodes are further away. -The wireless LAN, or WLAN, node appears as a small cloud. The WLAN offers +The wireless LAN, or WLAN, node appears as a small cloud. The WLAN offers several levels of wireless emulation fidelity, depending on your modeling needs. The WLAN tool can be extended with plug-ins for different levels of wireless diff --git a/install.sh b/install.sh index 1b633c2c..cb07ea85 100755 --- a/install.sh +++ b/install.sh @@ -70,7 +70,7 @@ shift $((OPTIND - 1)) case ${os} in "ubuntu") echo "Installing CORE for Ubuntu" - sudo apt install -y automake pkg-config gcc libev-dev bridge-utils ebtables gawk iproute2 \ + 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 install_ospf_mdr if [[ -z ${dev} ]]; then @@ -86,7 +86,7 @@ case ${os} in fi ;; "centos") - sudo yum install -y automake pkgconf-pkg-config gcc gcc-c++ libev-devel bridge-utils iptables-ebtables iproute \ + 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 install_ospf_mdr if [[ -z ${dev} ]]; then From 3512eedc607036fc788d6af4413c91c069d63bb0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 26 Dec 2019 14:00:22 -0800 Subject: [PATCH 454/462] small tweaks to throughput config dialog --- daemon/core/gui/dialogs/shapemod.py | 5 +- daemon/core/gui/dialogs/throughput.py | 69 ++++++++++++++++----------- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/daemon/core/gui/dialogs/shapemod.py b/daemon/core/gui/dialogs/shapemod.py index 8dc56f43..ba799220 100644 --- a/daemon/core/gui/dialogs/shapemod.py +++ b/daemon/core/gui/dialogs/shapemod.py @@ -135,9 +135,8 @@ class ShapeDialog(Dialog): button.grid(row=0, column=1, sticky="ew") def choose_text_color(self): - color_picker = ColorPickerDialog(self, self.app, "#000000") - color = color_picker.askcolor() - self.text_color = color + color_picker = ColorPickerDialog(self, self.app, self.text_color) + self.text_color = color_picker.askcolor() def choose_fill_color(self): color_picker = ColorPickerDialog(self, self.app, self.fill_color) diff --git a/daemon/core/gui/dialogs/throughput.py b/daemon/core/gui/dialogs/throughput.py index 178c10d1..e51c32a5 100644 --- a/daemon/core/gui/dialogs/throughput.py +++ b/daemon/core/gui/dialogs/throughput.py @@ -5,12 +5,14 @@ import logging import tkinter as tk from tkinter import ttk +from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.dialog import Dialog +from core.gui.themes import FRAME_PAD, PADY class ThroughputDialog(Dialog): def __init__(self, master, app): - super().__init__(master, app, "throughput config", modal=False) + super().__init__(master, app, "Throughput Config", modal=False) self.app = app self.show_throughput = tk.IntVar(value=1) self.exponential_weight = tk.IntVar(value=1) @@ -18,6 +20,8 @@ class ThroughputDialog(Dialog): self.reception = tk.IntVar(value=1) self.threshold = tk.DoubleVar(value=250.0) self.width = tk.IntVar(value=10) + self.color = "#FF0000" + self.color_button = None self.top.columnconfigure(0, weight=1) self.draw() @@ -25,41 +29,41 @@ class ThroughputDialog(Dialog): button = ttk.Checkbutton( self.top, variable=self.show_throughput, - text="Show throughput level on every link", + text="Show Throughput Level On Every Link", ) - button.grid(row=0, column=0, sticky="nsew") + button.grid(row=0, column=0, sticky="ew") button = ttk.Checkbutton( self.top, variable=self.exponential_weight, - text="Use exponential weighted moving average", + text="Use Exponential Weighted Moving Average", ) - button.grid(row=1, column=0, sticky="nsew") + button.grid(row=1, column=0, sticky="ew") button = ttk.Checkbutton( - self.top, variable=self.transmission, text="Include transmissions" + self.top, variable=self.transmission, text="Include Transmissions" ) - button.grid(row=2, column=0, sticky="nsew") + button.grid(row=2, column=0, sticky="ew") button = ttk.Checkbutton( - self.top, variable=self.reception, text="Include receptions" + self.top, variable=self.reception, text="Include Receptions" ) - button.grid(row=3, column=0, sticky="nsew") + button.grid(row=3, column=0, sticky="ew") - label_frame = ttk.LabelFrame(self.top, text="Link highlight") + label_frame = ttk.LabelFrame(self.top, text="Link Highlight", padding=FRAME_PAD) label_frame.columnconfigure(0, weight=1) - label_frame.grid(row=4, column=0, sticky="nsew") - label = ttk.Label(label_frame, text="Highlight link if throughput exceeds this") - label.grid(row=0, column=0, sticky="nsew") + label_frame.grid(row=4, column=0, sticky="ew") + label = ttk.Label(label_frame, text="Highlight Link Throughput") + label.grid(row=0, column=0, sticky="ew") frame = ttk.Frame(label_frame) frame.columnconfigure(0, weight=2) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) - frame.grid(row=1, column=0, sticky="nsew") + frame.grid(row=1, column=0, sticky="ew") label = ttk.Label(frame, text="Threshold (0 for disabled)") - label.grid(row=0, column=0, sticky="nsew") + label.grid(row=0, column=0, sticky="ew") entry = ttk.Entry(frame, textvariable=self.threshold) - entry.grid(row=0, column=1, sticky="nsew") + entry.grid(row=0, column=1, sticky="ew") label = ttk.Label(frame, text="kbps") - label.grid(row=0, column=2, sticky="nsew") + label.grid(row=0, column=2, sticky="ew") scale = ttk.Scale( label_frame, @@ -69,28 +73,39 @@ class ThroughputDialog(Dialog): orient=tk.HORIZONTAL, variable=self.threshold, ) - scale.grid(row=2, column=0, sticky="nsew") + scale.grid(row=2, column=0, sticky="ew", pady=PADY) frame = ttk.Frame(label_frame) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.grid(row=3, column=0, sticky="nsew") - label = ttk.Label(frame, text="Highlight link width: ") - label.grid(row=0, column=0, sticky="nsew") + label = ttk.Label(frame, text="Highlight Link Width") + label.grid(row=0, column=0, sticky="ew") entry = ttk.Entry(frame, textvariable=self.width) - entry.grid(row=0, column=1, sticky="nsew") + entry.grid(row=0, column=1, sticky="ew") frame = ttk.Frame(label_frame) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - frame.grid(row=4, column=0, sticky="nsew") - label = ttk.Label(frame, text="Color: ") - label.grid(row=0, column=0, sticky="nsew") - button = ttk.Button(frame, text="not implemented") - button.grid(row=0, column=1, sticky="nsew") + frame.grid(row=4, column=0, sticky="ew") + label = ttk.Label(frame, text="Color") + label.grid(row=0, column=0, sticky="ew") + self.color_button = tk.Button( + frame, + text=self.color, + command=self.click_color, + bg=self.color, + highlightthickness=0, + ) + self.color_button.grid(row=0, column=1, sticky="ew") button = ttk.Button(self.top, text="OK", command=self.ok) - button.grid(row=5, column=0, sticky="nsew") + button.grid(row=5, column=0, sticky="ew") + + def click_color(self): + color_picker = ColorPickerDialog(self, self.app, self.color) + self.color = color_picker.askcolor() + self.color_button.config(bg=self.color, text=self.color, bd=0) def ok(self): logging.debug("click ok") From 5dd08c283a1b3150b7798cb74e73264da3bebb5e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 26 Dec 2019 21:32:30 -0800 Subject: [PATCH 455/462] updated throughput dialog to load and set values from graph class --- daemon/core/gui/dialogs/throughput.py | 76 ++++++++++++--------------- daemon/core/gui/graph/graph.py | 5 ++ 2 files changed, 40 insertions(+), 41 deletions(-) diff --git a/daemon/core/gui/dialogs/throughput.py b/daemon/core/gui/dialogs/throughput.py index e51c32a5..150a3c5a 100644 --- a/daemon/core/gui/dialogs/throughput.py +++ b/daemon/core/gui/dialogs/throughput.py @@ -1,26 +1,26 @@ """ throughput dialog """ -import logging import tkinter as tk from tkinter import ttk from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.dialog import Dialog -from core.gui.themes import FRAME_PAD, PADY +from core.gui.themes import FRAME_PAD, PADX, PADY class ThroughputDialog(Dialog): def __init__(self, master, app): super().__init__(master, app, "Throughput Config", modal=False) self.app = app + self.canvas = app.canvas self.show_throughput = tk.IntVar(value=1) self.exponential_weight = tk.IntVar(value=1) self.transmission = tk.IntVar(value=1) self.reception = tk.IntVar(value=1) - self.threshold = tk.DoubleVar(value=250.0) - self.width = tk.IntVar(value=10) - self.color = "#FF0000" + self.threshold = tk.DoubleVar(value=self.canvas.throughput_threshold) + self.width = tk.IntVar(value=self.canvas.throughput_width) + self.color = self.canvas.throughput_color self.color_button = None self.top.columnconfigure(0, weight=1) self.draw() @@ -31,39 +31,25 @@ class ThroughputDialog(Dialog): variable=self.show_throughput, text="Show Throughput Level On Every Link", ) - button.grid(row=0, column=0, sticky="ew") + button.grid(sticky="ew") button = ttk.Checkbutton( self.top, variable=self.exponential_weight, text="Use Exponential Weighted Moving Average", ) - button.grid(row=1, column=0, sticky="ew") + button.grid(sticky="ew") button = ttk.Checkbutton( self.top, variable=self.transmission, text="Include Transmissions" ) - button.grid(row=2, column=0, sticky="ew") + button.grid(sticky="ew") button = ttk.Checkbutton( self.top, variable=self.reception, text="Include Receptions" ) - button.grid(row=3, column=0, sticky="ew") + button.grid(sticky="ew") label_frame = ttk.LabelFrame(self.top, text="Link Highlight", padding=FRAME_PAD) label_frame.columnconfigure(0, weight=1) - label_frame.grid(row=4, column=0, sticky="ew") - label = ttk.Label(label_frame, text="Highlight Link Throughput") - label.grid(row=0, column=0, sticky="ew") - - frame = ttk.Frame(label_frame) - frame.columnconfigure(0, weight=2) - frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=1) - frame.grid(row=1, column=0, sticky="ew") - label = ttk.Label(frame, text="Threshold (0 for disabled)") - label.grid(row=0, column=0, sticky="ew") - entry = ttk.Entry(frame, textvariable=self.threshold) - entry.grid(row=0, column=1, sticky="ew") - label = ttk.Label(frame, text="kbps") - label.grid(row=0, column=2, sticky="ew") + label_frame.grid(sticky="ew") scale = ttk.Scale( label_frame, @@ -73,23 +59,21 @@ class ThroughputDialog(Dialog): orient=tk.HORIZONTAL, variable=self.threshold, ) - scale.grid(row=2, column=0, sticky="ew", pady=PADY) + scale.grid(sticky="ew", pady=PADY) frame = ttk.Frame(label_frame) - frame.columnconfigure(0, weight=1) + frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) - frame.grid(row=3, column=0, sticky="nsew") - label = ttk.Label(frame, text="Highlight Link Width") - label.grid(row=0, column=0, sticky="ew") + label = ttk.Label(frame, text="Threshold Kbps (0 disabled)") + label.grid(row=0, column=0, sticky="ew", padx=PADX) + entry = ttk.Entry(frame, textvariable=self.threshold) + entry.grid(row=0, column=1, sticky="ew", pady=PADY) + label = ttk.Label(frame, text="Width") + label.grid(row=1, column=0, sticky="ew", padx=PADX) entry = ttk.Entry(frame, textvariable=self.width) - entry.grid(row=0, column=1, sticky="ew") - - frame = ttk.Frame(label_frame) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - frame.grid(row=4, column=0, sticky="ew") + entry.grid(row=1, column=1, sticky="ew", pady=PADY) label = ttk.Label(frame, text="Color") - label.grid(row=0, column=0, sticky="ew") + label.grid(row=2, column=0, sticky="ew", padx=PADX) self.color_button = tk.Button( frame, text=self.color, @@ -97,16 +81,26 @@ class ThroughputDialog(Dialog): bg=self.color, highlightthickness=0, ) - self.color_button.grid(row=0, column=1, sticky="ew") + self.color_button.grid(row=2, column=1, sticky="ew") - button = ttk.Button(self.top, text="OK", command=self.ok) - button.grid(row=5, column=0, sticky="ew") + self.draw_spacer() + + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Save", command=self.click_save) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") def click_color(self): color_picker = ColorPickerDialog(self, self.app, self.color) self.color = color_picker.askcolor() self.color_button.config(bg=self.color, text=self.color, bd=0) - def ok(self): - logging.debug("click ok") + def click_save(self): + self.canvas.throughput_threshold = self.threshold.get() + self.canvas.throughput_width = self.width.get() + self.canvas.throughput_color = self.color self.destroy() diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index ead26669..586f99b2 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -57,6 +57,11 @@ class CanvasGraph(tk.Canvas): self.show_grid = tk.BooleanVar(value=True) self.adjust_to_dim = tk.BooleanVar(value=False) + # throughput related + self.throughput_threshold = 250.0 + self.throughput_width = 10 + self.throughput_color = "#FF0000" + # bindings self.setup_bindings() From 2be0713ed107991445a55149f40fa1bcce884bbf Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 27 Dec 2019 00:32:10 -0800 Subject: [PATCH 456/462] updated so that throughputs will update link color/width based on threshold --- daemon/core/gui/coreclient.py | 5 +- daemon/core/gui/graph/edges.py | 48 +++++++++- daemon/core/gui/graph/graph.py | 33 ++++++- daemon/core/gui/graph/linkinfo.py | 152 ------------------------------ daemon/core/gui/graph/node.py | 1 - daemon/core/gui/graph/tags.py | 1 + daemon/core/gui/toolbar.py | 2 - 7 files changed, 75 insertions(+), 167 deletions(-) delete mode 100644 daemon/core/gui/graph/linkinfo.py diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 0952af66..2f1488fd 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -212,9 +212,7 @@ class CoreClient: ) return logging.info("handling throughputs event: %s", event) - self.app.canvas.throughput_draw.process_grpc_throughput_event( - event.interface_throughputs - ) + self.app.canvas.set_throughputs(event) def handle_exception_event(self, event): logging.info("exception event: %s", event) @@ -511,6 +509,7 @@ class CoreClient: start = time.perf_counter() try: response = self.client.stop_session(session_id) + self.app.canvas.stopped_session() logging.debug( "stopped session(%s), result: %s", session_id, response.result ) diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index ebcda49e..323309de 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -8,6 +8,8 @@ from core.gui.graph import tags from core.gui.nodeutils import NodeUtils TEXT_DISTANCE = 0.30 +EDGE_WIDTH = 3 +EDGE_COLOR = "#ff0000" class CanvasWirelessEdge: @@ -29,8 +31,6 @@ class CanvasEdge: Canvas edge class """ - width = 3 - def __init__(self, x1, y1, x2, y2, src, canvas): """ Create an instance of canvas edge object @@ -47,10 +47,11 @@ class CanvasEdge: self.dst_interface = None self.canvas = canvas self.id = self.canvas.create_line( - x1, y1, x2, y2, tags=tags.EDGE, width=self.width, fill="#ff0000" + x1, y1, x2, y2, tags=tags.EDGE, width=EDGE_WIDTH, 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 @@ -77,6 +78,12 @@ class CanvasEdge: y2 = y2 - uy return x1, y1, x2, y2 + def get_midpoint(self): + x1, y1, x2, y2 = self.canvas.coords(self.id) + x = (x1 + x2) / 2 + y = (y1 + y2) / 2 + return x, y + def draw_labels(self): x1, y1, x2, y2 = self.get_coordinates() label_one = None @@ -117,6 +124,28 @@ class CanvasEdge: x1, y1, x2, y2 = self.get_coordinates() self.canvas.coords(self.text_src, x1, y1) self.canvas.coords(self.text_dst, x2, y2) + if self.text_middle is not None: + x, y = self.get_midpoint() + self.canvas.coords(self.text_middle, x, y) + + def set_throughput(self, throughput): + throughput = 0.001 * throughput + value = f"{throughput:.3f} kbps" + if self.text_middle is None: + x, y = self.get_midpoint() + self.text_middle = self.canvas.create_text( + x, y, tags=tags.THROUGHPUT, font=self.font, text=value + ) + else: + self.canvas.itemconfig(self.text_middle, text=value) + + if throughput > self.canvas.throughput_threshold: + color = self.canvas.throughput_color + width = self.canvas.throughput_width + else: + color = EDGE_COLOR + width = EDGE_WIDTH + self.canvas.itemconfig(self.id, fill=color, width=width) def complete(self, dst): self.dst = dst @@ -128,14 +157,17 @@ class CanvasEdge: self.canvas.tag_raise(self.src) self.canvas.tag_raise(self.dst) - def check_wireless(self): + def is_wireless(self): src_node = self.canvas.nodes[self.src] dst_node = self.canvas.nodes[self.dst] src_node_type = src_node.core_node.type dst_node_type = dst_node.core_node.type is_src_wireless = NodeUtils.is_wireless_node(src_node_type) is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type) - if is_src_wireless or is_dst_wireless: + return is_src_wireless or is_dst_wireless + + def check_wireless(self): + if self.is_wireless(): self.canvas.itemconfig(self.id, state=tk.HIDDEN) self._check_antenna() @@ -160,6 +192,12 @@ class CanvasEdge: if self.link: self.canvas.delete(self.text_src) self.canvas.delete(self.text_dst) + self.canvas.delete(self.text_middle) + + def reset(self): + self.canvas.delete(self.text_middle) + self.text_middle = None + self.canvas.itemconfig(self.id, fill=EDGE_COLOR, width=EDGE_WIDTH) def create_context(self, event): logging.debug("create link context") diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 586f99b2..b10fe0e9 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -9,7 +9,6 @@ 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.enums import GraphMode, ScaleOption -from core.gui.graph.linkinfo import Throughput 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 @@ -38,7 +37,6 @@ class CanvasGraph(tk.Canvas): self.wireless_edges = {} self.drawing_edge = None self.grid = None - self.throughput_draw = Throughput(self, core) self.shape_drawing = False self.default_dimensions = (width, height) self.current_dimensions = self.default_dimensions @@ -158,6 +156,21 @@ class CanvasGraph(tk.Canvas): valid_bottomright = self.inside_canvas(x2, y2) return valid_topleft and valid_bottomright + def set_throughputs(self, throughputs_event): + for interface_throughput in throughputs_event.interface_throughputs: + node_id = interface_throughput.node_id + interface_id = interface_throughput.interface_id + throughput = interface_throughput.throughput + interface_to_edge_id = (node_id, interface_id) + token = self.core.interface_to_edge.get(interface_to_edge_id) + if not token: + continue + edge = self.edges.get(token) + if edge: + edge.set_throughput(throughput) + else: + del self.core.interface_to_edge[interface_to_edge_id] + def draw_grid(self): """ Create grid. @@ -269,6 +282,20 @@ class CanvasGraph(tk.Canvas): # raise the nodes so they on top of the links self.tag_raise(tags.NODE) + def stopped_session(self): + # clear wireless edges + for edge in self.wireless_edges.values(): + edge.delete() + src_node = self.nodes[edge.src] + src_node.wireless_edges.remove(edge) + dst_node = self.nodes[edge.dst] + dst_node.wireless_edges.remove(edge) + self.wireless_edges.clear() + + # clear all middle edge labels + for edge in self.edges.values(): + edge.reset() + def canvas_xy(self, event): """ Convert window coordinate to canvas coordinate @@ -446,9 +473,7 @@ class CanvasGraph(tk.Canvas): if edge in edges: continue edges.add(edge) - self.throughput_draw.delete(edge) self.edges.pop(edge.token, None) - # del self.edges[edge.token] edge.delete() # update node connected to edge being deleted diff --git a/daemon/core/gui/graph/linkinfo.py b/daemon/core/gui/graph/linkinfo.py deleted file mode 100644 index de4b2c6e..00000000 --- a/daemon/core/gui/graph/linkinfo.py +++ /dev/null @@ -1,152 +0,0 @@ -""" -Link information, such as IPv4, IPv6 and throughput drawn in the canvas -""" - -from core.api.grpc import core_pb2 - - -class Throughput: - def __init__(self, canvas, core): - self.canvas = canvas - self.core = core - # edge canvas id mapped to throughput value - self.tracker = {} - # map an edge canvas id to a throughput canvas id - self.map = {} - # map edge canvas id to token - self.edge_id_to_token = {} - - def load_throughput_info(self, interface_throughputs): - """ - load all interface throughouts from an event - - :param repeated core_bp2.InterfaceThroughputinterface_throughputs: interface - throughputs - :return: nothing - """ - for throughput in interface_throughputs: - nid = throughput.node_id - iid = throughput.interface_id - tp = throughput.throughput - token = self.core.interface_to_edge.get((nid, iid)) - if token: - edge = self.canvas.edges.get(token) - if edge: - edge_id = edge.id - self.edge_id_to_token[edge_id] = token - if edge_id not in self.tracker: - self.tracker[edge_id] = tp - else: - temp = self.tracker[edge_id] - self.tracker[edge_id] = (temp + tp) / 2 - else: - self.core.interface_to_edge.pop((nid, iid), None) - - def edge_is_wired(self, token): - """ - determine whether link is a WIRED link - - :param token: - :return: - """ - canvas_edge = self.canvas.edges[token] - canvas_src_id = canvas_edge.src - canvas_dst_id = canvas_edge.dst - src = self.canvas.nodes[canvas_src_id].core_node - dst = self.canvas.nodes[canvas_dst_id].core_node - return not ( - src.type == core_pb2.NodeType.WIRELESS_LAN - and dst.model == "mdr" - or src.model == "mdr" - and dst.type == core_pb2.NodeType.WIRELESS_LAN - ) - - def draw_wired_throughput(self, edge_id): - - x0, y0, x1, y1 = self.canvas.coords(edge_id) - x = (x0 + x1) / 2 - y = (y0 + y1) / 2 - if edge_id not in self.map: - tpid = self.canvas.create_text( - x, - y, - tags="throughput", - font=("Arial", 8), - text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), - ) - self.map[edge_id] = tpid - else: - tpid = self.map[edge_id] - self.canvas.coords(tpid, x, y) - self.canvas.itemconfig( - tpid, text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]) - ) - - def draw_wireless_throughput(self, edge_id): - token = self.edge_id_to_token[edge_id] - canvas_edge = self.canvas.edges[token] - canvas_src_id = canvas_edge.src - canvas_dst_id = canvas_edge.dst - src_node = self.canvas.nodes[canvas_src_id] - dst_node = self.canvas.nodes[canvas_dst_id] - - not_wlan = ( - dst_node - if src_node.core_node.type == core_pb2.NodeType.WIRELESS_LAN - else src_node - ) - - x, y = self.canvas.coords(not_wlan.id) - if edge_id not in self.map: - tp_id = self.canvas.create_text( - x + 50, - y + 25, - font=("Arial", 8), - tags="throughput", - text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), - ) - self.map[edge_id] = tp_id - - # redraw throughput - else: - self.canvas.itemconfig( - self.map[edge_id], - text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), - ) - - def draw_throughputs(self): - for edge_id in self.tracker: - if self.edge_is_wired(self.edge_id_to_token[edge_id]): - self.draw_wired_throughput(edge_id) - else: - self.draw_wireless_throughput(edge_id) - - def process_grpc_throughput_event(self, interface_throughputs): - self.load_throughput_info(interface_throughputs) - self.draw_throughputs() - - def move(self, edge): - tpid = self.map.get(edge.id) - if tpid: - if self.edge_is_wired(edge.token): - x0, y0, x1, y1 = self.canvas.coords(edge.id) - self.canvas.coords(tpid, (x0 + x1) / 2, (y0 + y1) / 2) - else: - if ( - self.canvas.nodes[edge.src].core_node.type - == core_pb2.NodeType.WIRELESS_LAN - ): - x, y = self.canvas.coords(edge.dst) - self.canvas.coords(tpid, x + 50, y + 20) - else: - x, y = self.canvas.coords(edge.src) - self.canvas.coords(tpid, x + 50, y + 25) - - def delete(self, edge): - tpid = self.map.get(edge.id) - if tpid: - eid = edge.id - self.canvas.delete(tpid) - self.tracker.pop(eid) - self.map.pop(eid) - self.edge_id_to_token.pop(eid) diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index b348fe15..c43cbe9c 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -128,7 +128,6 @@ class CanvasNode: self.canvas.coords(edge.id, x, y, x2, y2) else: self.canvas.coords(edge.id, x1, y1, x, y) - self.canvas.throughput_draw.move(edge) edge.update_labels() for edge in self.wireless_edges: diff --git a/daemon/core/gui/graph/tags.py b/daemon/core/gui/graph/tags.py index 42f4ff5f..763465b5 100644 --- a/daemon/core/gui/graph/tags.py +++ b/daemon/core/gui/graph/tags.py @@ -9,6 +9,7 @@ NODE_NAME = "nodename" NODE = "node" WALLPAPER = "wallpaper" SELECTION = "selectednodes" +THROUGHPUT = "throughput" ABOVE_WALLPAPER_TAGS = [ GRIDLINE, SHAPE, diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index a4229ddd..9229f661 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -7,7 +7,6 @@ from tkinter.font import Font from core.gui.dialogs.customnodes import CustomNodesDialog from core.gui.dialogs.marker import MarkerDialog -from core.gui.graph import tags from core.gui.graph.enums import GraphMode from core.gui.graph.shapeutils import ShapeType, is_marker from core.gui.images import ImageEnum, Images @@ -404,7 +403,6 @@ class Toolbar(ttk.Frame): self.app.statusbar.progress_bar.start(5) thread = threading.Thread(target=self.app.core.stop_session) thread.start() - self.app.canvas.delete(tags.WIRELESS_EDGE) self.design_frame.tkraise() self.click_selection() From f2d65efad3f9da8da3899024928e8e8cae4f8489 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 29 Dec 2019 22:55:26 -0800 Subject: [PATCH 457/462] updates to building fpm packages and slight adjustments to install doc --- Makefile.am | 4 ++-- docs/install.md | 17 ++++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Makefile.am b/Makefile.am index ed52cdbb..a0b09165 100644 --- a/Makefile.am +++ b/Makefile.am @@ -51,7 +51,7 @@ fpm -s dir -t rpm -n core \ --description "Common Open Research Emulator" \ --url https://github.com/coreemu/core \ --vendor "$(PACKAGE_VENDOR)" \ - -p core_$(PYTHON)_VERSION_ARCH.rpm \ + -p core_VERSION_ARCH.rpm \ -v $(PACKAGE_VERSION) \ --rpm-init scripts/core-daemon \ --config-files "/etc/core" \ @@ -75,7 +75,7 @@ fpm -s dir -t deb -n core \ --description "Common Open Research Emulator" \ --url https://github.com/coreemu/core \ --vendor "$(PACKAGE_VENDOR)" \ - -p core_$(PYTHON)_VERSION_ARCH.deb \ + -p core_VERSION_ARCH.deb \ -v $(PACKAGE_VERSION) \ --deb-systemd scripts/core-daemon.service \ --deb-no-default-config-files \ diff --git a/docs/install.md b/docs/install.md index 251c3669..64b0cdfd 100644 --- a/docs/install.md +++ b/docs/install.md @@ -31,6 +31,7 @@ CORE files are installed to the following directories, when the installation pre Install Path | Description -------------|------------ /usr/bin/core-gui|GUI startup command +/usr/bin/coretk-gui|BETA Python GUI /usr/bin/core-daemon|Daemon startup command /usr/bin/{core-cleanup, coresendmsg, core-manage}|Misc. helper commands/scripts /usr/lib/core|GUI files @@ -48,7 +49,7 @@ 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. ```shell -sudo apt install python3 +sudo apt install python3.6 sudo apt install python3-pip ``` @@ -59,7 +60,7 @@ To account for this it would be recommended to install the python dependencies u the latest [CORE Release](https://github.com/coreemu/core/releases). ```shell -sudo python3 -m pip install -r requirements.txt +sudo pip3 install -r requirements.txt ``` # Pre-Req Installing OSPF MDR @@ -123,9 +124,7 @@ You can obtain the CORE packages from [CORE Releases](https://github.com/coreemu Ubuntu package defaults to using systemd for running as a service. ```shell -# $PYTHON and $VERSION represent the python and CORE -# versions the package was built for -sudo apt install ./core_$PYTHON_$VERSION_amd64.deb +sudo apt install ./core_$VERSION_amd64.deb ``` Run the CORE GUI as a normal user: @@ -143,7 +142,7 @@ Messages will print out on the console about connecting to the CORE daemon. on CentOS <= 6, or build from source otherwise** ```shell -yum install ./core_python3_$VERSION_x86_64.rpm +yum install ./core_$VERSION_x86_64.rpm ``` Disabling SELINUX: @@ -210,7 +209,7 @@ You can obtain the CORE source from the [CORE GitHub](https://github.com/coreemu Python module grpcio-tools is currently needed to generate code from the CORE protobuf file during the build. ```shell -python3 -m pip install grpcio-tools +sudo pip3 install grpcio-tools ``` ## Distro Requirements @@ -218,7 +217,7 @@ python3 -m pip install grpcio-tools ### Ubuntu 18.04 Requirements ```shell -sudo apt install automake pkg-config gcc libev-dev ebtables python3-dev python3-setuptools tk libtk-img ethtool +sudo apt install automake pkg-config gcc iproute2 libev-dev ebtables python3.6 python3.6-dev python3-pip tk libtk-img ethtool ``` ### Ubuntu 16.04 Requirements @@ -230,7 +229,7 @@ sudo apt-get install automake ebtables python3-dev libev-dev python3-setuptools ### CentOS 7 with Gnome Desktop Requirements ```shell -sudo yum -y install automake gcc python36 python36-devel libev-devel tk ethtool +sudo yum -y install automake gcc python36 python36-devel libev-devel tk ethtool iptables-ebtables iproute python3-pip python3-tkinter ``` ## Build and Install From c7c3b1e3bea02fbdc53796b8fa3cb561850e13c5 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 29 Dec 2019 23:01:29 -0800 Subject: [PATCH 458/462] updated requirements.txt --- daemon/requirements.txt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/daemon/requirements.txt b/daemon/requirements.txt index b700247f..eaddd657 100644 --- a/daemon/requirements.txt +++ b/daemon/requirements.txt @@ -1,9 +1,15 @@ +bcrypt==3.1.7 +cffi==1.13.2 +cryptography==2.8 fabric==2.5.0 -grpcio==1.23.0 -grpcio-tools==1.21.1 +grpcio==1.26.0 invoke==1.3.0 -lxml==4.4.1 +lxml==4.4.2 netaddr==0.7.19 -Pillow==6.2.1 -protobuf==3.9.1 -PyYAML==5.2 +paramiko==2.7.1 +pillow==6.2.1 +protobuf==3.11.1 +pycparser==2.19 +pynacl==1.3.0 +pyyaml==5.2 +six==1.13.0 From bed66ffa1f4ba554a0656ca7a30955ff40b72c03 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 29 Dec 2019 23:18:40 -0800 Subject: [PATCH 459/462] updates to main Makefile.am and install docs --- Makefile.am | 12 ++---------- docs/install.md | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/Makefile.am b/Makefile.am index a0b09165..fec08074 100644 --- a/Makefile.am +++ b/Makefile.am @@ -65,6 +65,7 @@ fpm -s dir -t rpm -n core \ -d "libev" \ -d "net-tools" \ -d "python3 >= 3.6" \ + -d "python3-tkinter" \ -C $(DESTDIR) endef @@ -91,6 +92,7 @@ fpm -s dir -t deb -n core \ -d "iproute2" \ -d "libev4" \ -d "python3 >= 3.6" \ + -d "python3-tk" \ -C $(DESTDIR) endef @@ -117,8 +119,6 @@ define change-files = $(info creating file $1 from $1.in) @$(SED) -e 's,[@]sbindir[@],$(sbindir),g' \ -e 's,[@]bindir[@],$(bindir),g' \ - -e 's,[@]pythondir[@],$(pythondir),g' \ - -e 's,[@]PYTHON[@],$(PYTHON),g' \ -e 's,[@]PACKAGE_VERSION[@],$(PACKAGE_VERSION),g' \ -e 's,[@]PACKAGE_DATE[@],$(PACKAGE_DATE),g' \ -e 's,[@]CORE_LIB_DIR[@],$(CORE_LIB_DIR),g' \ @@ -126,14 +126,6 @@ $(info creating file $1 from $1.in) -e 's,[@]CORE_DATA_DIR[@],$(CORE_DATA_DIR),g' \ -e 's,[@]CORE_CONF_DIR[@],$(CORE_CONF_DIR),g' \ -e 's,[@]CORE_GUI_CONF_DIR[@],$(CORE_GUI_CONF_DIR),g' \ - -e 's,[@]sysctl_path[@],$(sysctl_path),g' \ - -e 's,[@]ip_path[@],$(ip_path),g' \ - -e 's,[@]tc_path[@],$(tc_path),g' \ - -e 's,[@]ebtables_path[@],$(ebtables_path),g' \ - -e 's,[@]mount_path[@],$(mount_path),g' \ - -e 's,[@]umount_path[@],$(umount_path),g' \ - -e 's,[@]ovs_vs_path[@],$(ovs_vs_path),g' \ - -e 's,[@]ovs_of_path[@],$(ovs_of_path),g' \ < $1.in > $1 endef diff --git a/docs/install.md b/docs/install.md index 64b0cdfd..5d3ee3b2 100644 --- a/docs/install.md +++ b/docs/install.md @@ -217,7 +217,7 @@ sudo pip3 install grpcio-tools ### Ubuntu 18.04 Requirements ```shell -sudo apt install automake pkg-config gcc iproute2 libev-dev ebtables python3.6 python3.6-dev python3-pip tk libtk-img ethtool +sudo apt install automake pkg-config gcc iproute2 libev-dev ebtables python3.6 python3.6-dev python3-pip tk libtk-img ethtool python3-tk ``` ### Ubuntu 16.04 Requirements From ff7909e97a7b9d696dc017e65c8723b3a54d5dad Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 29 Dec 2019 23:23:35 -0800 Subject: [PATCH 460/462] removed old icons --- daemon/core/gui/data/oldicons/docker.gif | Bin 719 -> 0 bytes daemon/core/gui/data/oldicons/emane.gif | Bin 337 -> 0 bytes daemon/core/gui/data/oldicons/host.gif | Bin 1189 -> 0 bytes daemon/core/gui/data/oldicons/hub.gif | Bin 719 -> 0 bytes daemon/core/gui/data/oldicons/lanswitch.gif | Bin 744 -> 0 bytes daemon/core/gui/data/oldicons/link.gif | Bin 86 -> 0 bytes daemon/core/gui/data/oldicons/lxc.gif | Bin 724 -> 0 bytes daemon/core/gui/data/oldicons/marker.gif | Bin 375 -> 0 bytes daemon/core/gui/data/oldicons/mdr.gif | Bin 1276 -> 0 bytes daemon/core/gui/data/oldicons/oval.gif | Bin 174 -> 0 bytes daemon/core/gui/data/oldicons/pc.gif | Bin 1300 -> 0 bytes daemon/core/gui/data/oldicons/rectangle.gif | Bin 160 -> 0 bytes daemon/core/gui/data/oldicons/rj45.gif | Bin 755 -> 0 bytes daemon/core/gui/data/oldicons/router.gif | Bin 1152 -> 0 bytes daemon/core/gui/data/oldicons/router_green.gif | Bin 753 -> 0 bytes daemon/core/gui/data/oldicons/run.gif | Bin 324 -> 0 bytes daemon/core/gui/data/oldicons/select.gif | Bin 925 -> 0 bytes daemon/core/gui/data/oldicons/start.gif | Bin 1131 -> 0 bytes daemon/core/gui/data/oldicons/stop.gif | Bin 1204 -> 0 bytes daemon/core/gui/data/oldicons/text.gif | Bin 127 -> 0 bytes daemon/core/gui/data/oldicons/tunnel.gif | Bin 799 -> 0 bytes daemon/core/gui/data/oldicons/twonode.gif | Bin 220 -> 0 bytes daemon/core/gui/data/oldicons/wlan.gif | Bin 173 -> 0 bytes 23 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 daemon/core/gui/data/oldicons/docker.gif delete mode 100644 daemon/core/gui/data/oldicons/emane.gif delete mode 100644 daemon/core/gui/data/oldicons/host.gif delete mode 100644 daemon/core/gui/data/oldicons/hub.gif delete mode 100644 daemon/core/gui/data/oldicons/lanswitch.gif delete mode 100644 daemon/core/gui/data/oldicons/link.gif delete mode 100644 daemon/core/gui/data/oldicons/lxc.gif delete mode 100644 daemon/core/gui/data/oldicons/marker.gif delete mode 100644 daemon/core/gui/data/oldicons/mdr.gif delete mode 100644 daemon/core/gui/data/oldicons/oval.gif delete mode 100644 daemon/core/gui/data/oldicons/pc.gif delete mode 100644 daemon/core/gui/data/oldicons/rectangle.gif delete mode 100644 daemon/core/gui/data/oldicons/rj45.gif delete mode 100644 daemon/core/gui/data/oldicons/router.gif delete mode 100644 daemon/core/gui/data/oldicons/router_green.gif delete mode 100644 daemon/core/gui/data/oldicons/run.gif delete mode 100644 daemon/core/gui/data/oldicons/select.gif delete mode 100644 daemon/core/gui/data/oldicons/start.gif delete mode 100644 daemon/core/gui/data/oldicons/stop.gif delete mode 100644 daemon/core/gui/data/oldicons/text.gif delete mode 100644 daemon/core/gui/data/oldicons/tunnel.gif delete mode 100644 daemon/core/gui/data/oldicons/twonode.gif delete mode 100644 daemon/core/gui/data/oldicons/wlan.gif diff --git a/daemon/core/gui/data/oldicons/docker.gif b/daemon/core/gui/data/oldicons/docker.gif deleted file mode 100644 index dde25750d31c0f3ce9329dd5dd26f33ec3ebd0bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 719 zcmZ?wbhEHblxGlSIOfgZtdQWYnC7jV<)f10tCAb2Rve;H5w2Mit=$x>)10K&ovh!R zYA_+qU}C1>v@E0P*+w&SjA!MW%q=jTUu3qZ#C&O)#fox^m6eujsw~&lSZ}Pg-qdKj zz1eP8i{0*4`@QWB`@38YcDo(y@i^4ud8p6l@Wj9)lY)*;4mma@^w`w!+e$?woLI z*Th?Ur{38&@hcMr_DcW~~#!wc>oS@huevWF*EK03YT@!9oHE^K{var^WCgBvLR zWMSlDsAte&00K~)FtGn?sBda+X>DuoXcp@3>Fw)m>6&ZbT_)L+Qh)j+bfo|;6p&-%r>DD zDiID3o8lQ5*(7wL%@~_IM0KS8F@Bifz{t!gX4A2u$BC6!)@_DIW1zA-qa3e{!vu$e zOziwBGP@@vGPm<9xJ`(-c}ek1zj5}xGnqooZ1Wvjh0bP}qy^2kC=Dy%GJO#+EyTRc z#Y>7;s=2E^o{yNRn>sz~>8dcT8{6vp4u#Iznt5wQyrI-4sqOl=)|Dy#-Klk5 SKj~;kf!l(FgPKnq7_0#ge~iHZ diff --git a/daemon/core/gui/data/oldicons/emane.gif b/daemon/core/gui/data/oldicons/emane.gif deleted file mode 100644 index 0531a932532f4573b3f581364a01a059a7a54e26..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 337 zcmZ?wbhEHbRA5kGc+A591jgD7#-R)|uL6w9SoqqGI`S+J)Z}3~{55m3>-wv~T?H#< zW@b2-6z*ZzwOP^2gZrP5;o(z43Q~UM#g#@z4%MQzE@c_^bw;Lk?IH|)m32vxaa}CA z3fvO{o3pwW8foTD(O4XmndY#7V{QjS_&47}7hO2# Z!QgRoI`a`OK~`a{m=hZowsSC80|0bwhHwA? diff --git a/daemon/core/gui/data/oldicons/host.gif b/daemon/core/gui/data/oldicons/host.gif deleted file mode 100644 index 5bd60ae3d34d9bcd8be206ba2a8a701b19aea319..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1189 zcmb``30Kkw008hm262dT`d1VcH7ko8t5<8Y=8+H2yze8AmB-d>TJ62nYg4kad?v4& zM~5QL6hp;kPN#X`g=i`SsGx=l2;OM*vP`yphyA|8&&!8?{OcqFKn51n00031BLEHn zNC*Id0!Ap-3M58&meYM}^Nii1^A`;HUW+aDPOb$JlayBa^A~QAaVrt~&^ysVU zu~#pg$<9c~y!cbrrA)@p=L&u~%ef84YDQ6|53A zyOhN)Z>=og)RebX6*gBFKC7*0t$xCN#;)aZI%*#_*0XpGPkXscKDVf^?p}MtlfL?r z{)STi^M``Q$GuIDyP7#eFUp6TS;FS0!nVrczj^H)ZQ`~nQ9F0AF*Tvx5@_UB?C>0{ufgN&9cE}$zZ!w_;Nz{*UVt6WVly6 z(m6TOEfNXjqx|X7mt*2l#aPdzNccwFDU}Q)4o~IY1S8%i#nxVJG-+|;J&gZw=hEE2=`yIo!;s0j01%;ph13(4-_1_5q zu>%;8WN#j;`#OSvdLYVS@EK^2sT%CutgEd(_JPmM)a$tvWO-8!ZZMjb+;`VMC~55O zZ8Nu)t;HMEVqV7i7$TIA?NOFFUVpL&d_}6G;!tO9Hwt5&FH99hSJIbFigv>uofpWs zil%hc_f_o8f&6TSug$RC!irVvj|0^3kD=;Q-S+#9SWcSL6V#M4uup%lA`x$XVv5UD z|6pDlk~f#;>(=$uM(SdIU&W(eXoPydho|^6LAH- z5a8TqvnWKEOTGL2@5uXX*GJ%NS|CpVgB{s(IKY-cv>&v9ThKhI(04%|NGmAs4*3eV zF&DNkP4DBLKw}yiL%OoOERP)rdMHmXC1Xvn?aG|Pp=5wzTGz6H@;FVQb$gC}yVs diff --git a/daemon/core/gui/data/oldicons/hub.gif b/daemon/core/gui/data/oldicons/hub.gif deleted file mode 100644 index 17f7c4d3ef726f744da685423057366093df33f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 719 zcmZ?wbhEHbRA3NeIF`X6<6R-+T`3<>s}$0x8qum2(W)NPsSyiAUD`>#I?27dDHHY5 zChDb6G|ZS{kU7OLYpPMsG?V;UrunnXi|3jb&$BFDXi>JnvTT7>*?jBr1(p@_Eh`pS zRm`^nk&CQB{tt%H;S1z=vTxeUh(6(loea&Lqx}}bFOPuPKIM)NwQisM> zt_{l^n^$`_t#EE#=hC{)xow?q+bXxNO@WAtOJ`nH{!xc%(pooDClzr6I=^<~Fy zEI)o@`=tlFu0Gm*?eU&#kM~}Ge1=Lu@h1x-7ehUR4g(N?;)H?yUqgLUb4zPmdq<0u zTu*Obf4`D_=L9(`C!;~HitzeCPr?FNJxqa zbCy%KUAIBbJ2m~}sncho)np9qjxL(X>fF`|N{-XJqoblAaq)7Kbe~IXuu89G zY>4^!<#n%FX}%(x=oD7Zx(x^D7Cd=>$48HrD@OVHGfFIMB$#qu{aN1LM(d zN%JxW&7zjh$r?;@OmoaOK04YXZC%G>x#`KtDFRD*voZx2yYdE7cm|vGV*3$4sHUKQA;Xt`D2tb#>OIgNj>peGaF!W-$oH tY>1eD&xfTUiIIg<%3(%;VvDJ9wZvc%wRbpgMV>J9(izdZ9gfp+0+}KYOA*e5OBqqdk75L4Bk` zeWOBtq(FhHMS!M3gR4h^r$~aQNrI?Dg|0+}u0w{dOogdTg{n`7s!xckMvAgji>+0Q zu1SuzRgA7lkGD#Xw@Q$=SB#%r9qZk@Ytp1W|LyjZ2mSf$H#qQ6_I&0DF?cci~~ zrNDcq!F#8|eW=5Ks>Fe;#fPuQh_T6uvdNFN%aggzn!VDVz|)|@)uF=GrpDN($JnUI z*{jOhtjpW3%-prm-?-G_xzypg)Z)9<;=R}7%*@Qp%*@Qp%*@QpA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=dv*jE#jucwYQHYIwy=OOkz4Hj~p>W&4i3Z zU|?EVaBDeOZhEkLdR2@d7~9=~8y-G!bar@qcWq-WUF1-iLJ&x!2p8np`UC026(iCh zJzDS}A-9DYGIZ!5v5|<6A4!l-8|AaEc70|*i*d`R)429F;{oItU%<%@ul z7Q3Zf+45z~nKf_b+}ZP|Np6FNvUCYkCQX|-b@KEHbQ>Y5MXMskx^$~ki(gevHEK0! zR;ET=O%?)6_2^ZlTAP-%V2Cc;v3TMBba2S8*t>7d8bD;&uG_Lr69G_J06~HV5GGUz aLDL40AWED>vBKp`qSL5Tt6ohg5CA)88DCBS diff --git a/daemon/core/gui/data/oldicons/link.gif b/daemon/core/gui/data/oldicons/link.gif deleted file mode 100644 index 55532ecf0d14eecb81f57e71ae8f90cd9c8f2fea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86 zcmZ?wbhEHblwgox_`m=Kia%Kx85kHDbU=KN3M?hZsq{JzSM|!8CHm iM7a#L2~R9E8Z-jp+asb?o-oSm3;Zc@(qDf#E87F?K7 zd~s&Ur8$+C=ha+W(sF%y$E~#!ZmpYmd&87F8>imgGV9*fIrp~Dy}x7r{hbRQ?pgA1 z@3KexS3W+t`pJ>?&yH_-erm^yGrRr|0-*Slg^`P)o2=Jx;6p9&Kkl-LBYXZ8aFmIu}pS- z7G&0Ipkv`46kvTrw~2w7uh-~BgCi5OF~6>s&MAh+Zu=RS_)HRx^)R)Im}FV}VP;Tj zWMUUFXi#X3WaW`_TT>vY{PFMDa3YoW# ztlWHvl~1mYO*Sgzao|j^{AYK5nrH1g zw35rPkmRJB!>gvuyrA2_kkrJV)Xlf=-J;{txy;PW+SI|;*4E(H*5%&Z_wUx>;o<-P z|NsC0A^8LW002J#ECc`q02lxm000J*z@KnPED`}ofN^OAs3wpIl1UYa7>b8-NL-kM<*DutFxg;BP1>~th8?)DLBExWd|U~ Vk^lwFilNX&NxIh8*xA-W06UFctKt9v diff --git a/daemon/core/gui/data/oldicons/mdr.gif b/daemon/core/gui/data/oldicons/mdr.gif deleted file mode 100644 index d6762f6500828ead06fb73c7a9061165ccdef85b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1276 zcmb``jXTo`0KoCzh-N62mQF9`y{bhS8&Di5W%q3E|!j5ZXj{V1dP z7^69zTAohjVrksC>#Y$b%3K=zJhSp5qbmMJTLzOyzDB>y-XS*imde`bawS84 zmw2%6!-V0cL*B2(Jh`PG1(ilOjJz+z~L3DWyE~ zO;xt!+NFMoy%loArqJ-l_Jm(**X)msz`k|c5+7%ztE@5^S_rHsgwwkfx+OT=Z$;za z1?W%IUsZdN*kB!mhEtzXfV>X7ht>Edn=uv}N8`!@xG`X6rD(8m2lfq+q_=}^$LNJe zrch2W6c0TaQtZgz-JcRb=$y)|Wj z-BiLi_^0&q5I9?(v7Hx8apXh0#<03WyT?yj39`SYQ1$*P0GqVK6ELG(0k9Ks?dS+d z3pf%;4@qAVb9%@ro@T;!2e%9u$e;%Wd0%mEb+qw`b4TNcjXCa=mAQUIuzk0S85EZ* zBUtH@1DtHVzSAt$x4=OWCFxq)a-ePq&RPZ!3`gHsVKo}*{{&+f1UUU>gn=xwL|_AW r4z4Sh`#3ytbr^3Q6*$HAfFoFup6oRk3ar@WthBAq6&LC31nl__5JiQ; diff --git a/daemon/core/gui/data/oldicons/oval.gif b/daemon/core/gui/data/oldicons/oval.gif deleted file mode 100644 index 4b3124d4c9f45c45764c282af9fa277fd20ccc6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 174 zcmZ?wbhEHbRA5kGSjfoG(9rPl?Fk4{{3orEtf^pRU|_CLl98(5l%JZJm#*NPpIeZa zSIMCGlZBCsfr&wf0SG|a8JH5L^shYqmVdFyf+%Zojy>fQ6lc4$C74ZnCAn1Vq}@Ai zXX99F^M5aGkwK}khXSw%@jRar$%MOj@_Rb53* zLseZvO+&*#LsMN-TSG%fPg_?@Ti;MmUsumSSJ%+k(AdDh*umJu$k^1_)ZE0}(#*=* z(#pot+Sba}-p06XUk)~1%W z=GOL>_RhA>?vAdW&hFl>u1PZ|Pn|S<=Cnz(7R{J7eaf6Av**s7Id8$7C2Ll!TE1r8 z>UA5|uGz9@%eKuMb{yEe{n+-M+jj2Wv3bvtz5Dml;r#-+uDu;gbhD7>etiD&*~3?# zUc7q#==JB9uU|ZP^Xu`OFR$LbeE9ayleb@AzkBuk{f}4gKfQhb`t^s;?>@YJ`SI7A zk6+$@eE0U#*AJiGfBgL6)0dC$zkdJx_0xxMKfZkb{OSA8&p&)21O&UZ4U+dI^{!gd>D|h#E6iqaFl|zFQsMJyw=OWRyvJa6$Rl!U zR@l1ZMcNNJ3l}mw>9FJ}Z2q#Shs%CVOlO(ttDwnBECCWCiy96tj+>F9s9ci5+v~R4 zt(VKRlY^y$HMgRo`OM^{iN{KBDit4fo9;Yc%kk90LmM147@K$wKlQwB;JwN4(@H0| z)u(1i(xn6iz5D;z&8dl-$AS zrM*MM*<1JUic6;qwoFj+F)Y&%Z06!ucyvOES=E6-%8p~vY3;2Fi@SBUUUBj^*tDgo zN2{iU;jnU_h0_HN#u!6a(QXcBZ~bi#no@LkeR1MeOki2y)~_Pu;>N(uQq(MZ@J~>; z?!kZ+-HH+o4h+K6bT)OUvr9TM2owiQY?jkgTFfED&(h#5pQ+c>=_GGEv0Ye)C&`VO tJw%~hJ~@t1va|g8z`l6e8jtN{jVv^D?$ diff --git a/daemon/core/gui/data/oldicons/rectangle.gif b/daemon/core/gui/data/oldicons/rectangle.gif deleted file mode 100644 index ed271f5737d0e8c2e35e9f03735de27509996893..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 160 zcmZ?wbhEHbRA5kGSjfcC(9rPV&GC|Nli|1^))!lG!&+qxHk36QOake}^#rBeM zi_xlcpRkb%J(0K*PL82|tP diff --git a/daemon/core/gui/data/oldicons/rj45.gif b/daemon/core/gui/data/oldicons/rj45.gif deleted file mode 100644 index 9ab7ac56dba8946e37182cf05ca160683f53e2e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 755 zcmZ?wbhEHbRA5kGI2O;~>+9?1=NA+d6cQ2=8X6iN?hzjD84=+X8R;Dr6%`ZX5gi>J z6B82`=NcCmmynQT6n4RsEljD$^YhO@cS6FCQP*6}( zQc_=US6^S>(9qD>*x1z6)Z1&^+iTL%(b3u2Icbu?gb8&MCe%-yIC1ji#;H>)Cr_R{ zZCcfoDO09TFP}cWa{Bb?bLJGyom)6-)~q>m=FFWtcj3b9MT@dltWaIDBy06*okfcl zEnd8M#fp@5>y(!)S+Ze+`id1RR<2yRYSpUMt5>gGyLR2WbsIKp*tBWW=FOY8Y}vAP z>(=etx9`}oW6z#F`}glZaNxkXbJB+n9lCT$_}H;y$B!RBdGh3`Q>RX!K7HoQnKy4Z z-n`*^`AY@?w*HGWo+|t_C-qG3B79HZ>KVjme2?5b& zEz#jV-gD;8n?E<)TfQaAf5D3RfsslI&C!uzn>KIRx+U6NVO@Rn-hKNI9E=XywfoTV zL#jvjoH)DJ{ph-LXCo9Z`uX0xb^Fd;ql*gOuIA>3CMJ58n)ce(Iwr;j78_ zwFCvl;?#p%KCCcW)XlD^vg!rURSp6*7r%Vup0Kc?jagL6VZj0h<|bY?J(HRl`SC3b zyuqttEQOK`I|U_lW>_{XJ;lP^w&RLwAk%&hA&wh48VA%T&VBgp=M(jZ2bpHG$bGPu zdfRWu>8&jPY;I>~*ysT1_lCK_Z;G0L58oIA}pZ@NkT43mOcrUkRiiszUY&owWZYf(DavUI*h z*+R?m`IhAit;!czR?M@im~U0Fz^Y=QHIS@aU=2bGZ7LU9RV}gxp~W^;3vH_wT30W! zt6peVy~w_Lk!{UV+uCJzwM!jq7dzH2v8!9|ShvKkewjnVN{2=uTIJfX%&}>eN7Hi8 zrWKAYYn)ovcr-8fY+m8gvevt0rAzzzz>YO;-J3mnw*+^u4e4I%(YH0ccfI$-ol*T8 zqbF>PnXt)s%5LAOK(r@r;%5Kpd;MqZOPsPLY1-DHSqD<4Z4I1#D0RlR;JHWAXKoLk zcO+!q(X81!vuE$jnX@x&(ea3-r;8WvjaqRodd1n&Mf*w@?=N4nzjE2Zs^tf(mmjKK zakzff;fB>mTGk!w*m$yU>zT57yZQXRtrza^xcp$xwa0s}KRz=GXchvBKUo;L82&TpFaQB4 zPcU%&WBAW07$KqoEo1|3* z!_CHs4~gB{Wjwl{j8FI;7LI4q)tb`Ld9+cY`_9GB?E#YVRbre*mzCJu8YN_JWf(8# z>@kmBq{2Dr(V_#q;`4JRHfIFNX)2%6h}xoYVahuGKn|4$2Or68>$+sav-qg5yleZT z1F{EQ6&e^t<(@SNFFV=0!e!fzj1|m1QimB?R2B#*X}O8mubAUl{8PT+3>%+Lg#jZo zyP&xDo)w8HoMQUPZqw~_6}C39@hP}WC~!Q?E^1uH;kf7sr-*iB!Ruppejj-M;IMAW zj{{9^{fcMi#yL;-o3$(d!6DU*=kH`|K0m)$Jl(wamesE}w|Cd`^Utq&S^qV5`n{@O zJJsWA7O!;-C}nzd{9fJrUiJBotO^$vRQ=p~{C-7Jz!!ez8;L&_GzNq`RCKykx`Bz$ LhNm#efx#L8|Dlf2 diff --git a/daemon/core/gui/data/oldicons/router_green.gif b/daemon/core/gui/data/oldicons/router_green.gif deleted file mode 100644 index 76e3ecd57c59ec99767f5641e701f908f09f0258..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 753 zcmV$ZwARvGsB7!0$gd`+|BqoR^Cx|C0iYY0J zDJqL9D~&5Hku5KgFE5iXF_tkhmNGJzGBlYqG@CRwn>IF_HaDF&IG;EPKS85GJf%E6r9DBULPDfMJ*GWE zq(eTZK0c^EMy5tVsX<4lNJytiLaIVQt3XMpN=&IvL#;zYu0u|$P(`stMzKamu}4#_ zRY$T%RIOG=vqwj?M@hCxSg=}Juw6^JOJB2LU$bIPyiQ`YWKF$IW3*;ZzE5SgXi&dU zP{2@9!BT3tZc@ThYq)P~xo>Q_Z*979RK-+pyK-^7bymn$R>@X$y?1rKd3C>eSj$*< zzk6EFT7krdfyIV|#)^%~l8(xhlg*iw&YG3ao0iX=n9!e@(Vw8yr=!)Xq}8jY*R816 zucz3rtJ<=#+_<#fy|v!Gw%@Eq<#>~vjA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=my(jE#=El$DGfh?9&+W>zhgGe=rwY)+0Jg^nay zZG3!fWM&|jQh9!Ub|jCOf|fFFeYa;FPI7*AUNAtGy_SqBI$bsnKeZk z1rZZ4K!FI3>S252fd&*X6#oDSAb`LD2Ng7J300IRJ9!Q{YVZ)>j z9!jK$fuo0!Bu}QqoGFqd0h>4*Jb*C40)`MMUew63L&%V$NtcEkS@R|XsZ_0E)ymZ? zSfx)g$2x_2c52nDS+{omiWDwWvQN#Ty}MSg+rDB4%@u4{ZQizg!^$i)AaK;ic-iiK ji-ij!x`Q2SKKvv~q(ODlW;XnF$?4M`P`G$4C=dWUiqcBu diff --git a/daemon/core/gui/data/oldicons/run.gif b/daemon/core/gui/data/oldicons/run.gif deleted file mode 100644 index 71dcc67eddc7b829b3221ba4823a04e93c317ba9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 324 zcmZ?wbhEHbRA5kGXkh>WMn*;!78X`kRv8%?d3kvQ0|P@tLt|rOQ&ZEBkdW~3@W#f* zzP`R`)22WfAZwX)2B~gy?XWL&6~Gx-+uY>JSuFtAW4$w*aj%1_PAOIL8t&n-yIt7K68$->CRAkUx!(gbo61M8v(>U}Ah^DZGE(+-Pd9n~wOxE_RahlU()Kf}#eGDh z*YY|kSclnhF*_f*ZL2LVvRih;gcu3Vl7L(W;oNF5dkLndh`{&>RXH)u)s9RO_M-ic zeWf|^96S<2Tyxsf7B~v9GqLh5uUZ+O;*@5-)iu^LcxTxBeY@PPj~q2OIdSsT=`&}~ Ko!3%ium%9#D0q7S diff --git a/daemon/core/gui/data/oldicons/select.gif b/daemon/core/gui/data/oldicons/select.gif deleted file mode 100644 index bb7e128c878317f6287564514a302e5e3c40c10e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 925 zcmX|AT}TvB6#nkcsH?a+J25!wWJw!YSVoy{g=r6ESt%99vPu|jWwavpvRe--w$f%s zYflbuf*4Z;~=R4<~Ip225ShbBKwQc6#Xwq9Tr2mq=851@csnR&8f+ycq>V-cDai3y~@!gr8wL0nBC zU{r)Hb5*5f7u61mZ3t|yIUb`VBe1-QqVleY#wih00Goly!xkwyVZ{1lB8I%Sj2 z2m-M!HhYQ+$jnGNeiv{GnB~nh`yh{T`At9|X4{_DxaFbKW*UGN8*$vq~F1bXlQwoK`C%h5?z2 z^9{6>N|K@L0aob!9DW(7DB*YfpQyG!K!#mAI~x7?(4Lbn6<4Dx zBsmXiOOfS?u)5v0FmUO{muJG>%YKj3_qk`!#gV43zkL%=4mf&UnMa1E`pa7HX6 zwqJ$`n~{uVTRP)1JIzL7vZz_If|{*~lS3(L>1YMNREp5@O@UgWlR;}sSO0_k0ekZL z_4(yVp4S^+tEp{9fME3$6#a%3d6h+8OxB~4_u{fkYWYPR?;%tdh>aI$;&(>#2d(q5 zq|ZS55Pb#FeTca%FjoY_pJqd!1@)Oxp9Ml1`7i-t4cyYgh!#e*aJztjI|SS%;9h|a zB09LQQ-@ea61aXdeO(}jc{0Q^LuMvumWN6CNC71urC^MNC<)^vJSd=Gf`Um(ai38{ z8O1oOm|(THknXlgcgI9TOhi;5A6OtpL5zkt4O28s(=ellSv}0@;UNQ$7?@{Zp@4-& z77{Ex<{-(z6Aqg*V)HyUZ^9BLSmGdMfWHe6JVmgA;2$Hb8db|?STjO~hb+(iC2}E= zy;n>}t!$*2i`p!Ao&2Q35-AghO2uI5=DfEu#zz=H@$_Mb?{Rq z{9+lOEYmC(YgRUCGIq4&M5(gkhc7$gRgRhMj=5@EvbrQ*Q!-U!TdHnN9cfQ>p3I%` zKE32ym!#DJX>CAC`=yLudghn1gHmqL|199o2K>39h3w)&E|JY=*YoT7d>;PKe_lXL z%Qpa=2Iup}^G|?k9cpo5kGH%3s7hbT?CS`gs@|e@IvxFC@zo~jk+^@))dtz&Y*T;l z?A{-~bETvTcUU_8qRZQ!rUz#@r|W!B>HK04c84jYlF?$DP{j?^i+0ZQ2+ZIX4SO-=6I%AGsiKi6a3?s^GQOGO8fMnKq);bg%%10X@M4zBI6uI7|O(9f`)8t&J&IEhZ*K% z8p0H3vMt-(>;N^g83y9QrosRx$V4G+SB}zh6w0L>BIQtq|HD3c{rvguotmB!tKG*1 zE-VAE0ptL&09yeP015#11C#)q0w@Kz0PqdK&j3}M2!tvku^OOe6M=9I;5xuH0$ktZ z0aXZc!vk)j#99QYB_Y3{$So3DOF@67l5SyG9hK5R#Tq<48*uD4oq7+)?=tY;nACer z`X3y|eIItSz^mDxWAgMc(LBsFqM1&#;6w|9Xu{EdnM5lKv9ggqFQlJ?40xjhKIq^k zU)07$ZGPwwk7V~J+4&?BlVb6uTR9Y~H`eCk^}vVI%Hwnk{46}Cna4KqIA)QbgHIY3 zl1D`3zs1}xsegxz*BL5&94hP%6%9yzEn==&%crFW;+X`=Xnfd;p0S?9 zTz`wXu$@1jB>69icnv2A%1q*_d#8!8+C0 zTZrvD>>8Vox>4Mdm_Es^ZH+W4(mo!~Z1|$qT$^|IwxO%!+@-oJh{nC8;r*=WWA`-;QAzjtz^Ii7u|z=Wv2fXj#VN>!l;b z?mLBTO~!ZdvdioVmi{Zhp5ZE9)4|Gr^t?m#a)!v5$*HJ3Tk$2o=T;c6)t=K>yv+RC(pN0M-Dd-mC&;N5vlB2PJtvQ!1!r1BjF z+s~Ac1T8?_TUcM_wWIJ%I-4ebdPz@^w*X1jEB<6*W6va?-5+;v-TUT4p_f*)Gl%!N9?d`v_mp{QI;sYb*91-N d=esbwJ0>tWSn~6fcWtY;{pRgIt;xz@4FI19FuMQ% diff --git a/daemon/core/gui/data/oldicons/tunnel.gif b/daemon/core/gui/data/oldicons/tunnel.gif deleted file mode 100644 index d574147f535122637516f4887d931779c5903ead..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 799 zcmZ?wbhEHbRA5kGI2Oae!0_MK*VoU_FDNJ|BqSs>G&DTiBRt$QBEl;&(mN_DDkjDw zIyyQgCMGVRu= zo;-Qlw5lmnrc9q+K7D%S^y$;*%qf~Xw{X_1S###hnLBsx!iCw37GTOS zvSsVmt=qS6-?3xIo;`c^@85smz=3n;qz@fBbm@}tv17-MA3uKbEDBDn2z=biC+ZOKAtSKatw%=FXNJe4Me>Sj+A1D9KCrS*RkU3* z;lrne-kjRjO)MM=OCIfUlrZJGkdVO4#LcG^5-~xMv7K2WY}*zN!ma^ zbPjCKZ)C4{X}6U-tSN3!tNM$%udfIkJDRAe;MWte!OBrWPFoND ze%zhu(T2k5n;-J6D%2?Cv=W`H(&hR5YrE{RRjOvEka_>yUw;J#YXB&pnhF2_ diff --git a/daemon/core/gui/data/oldicons/twonode.gif b/daemon/core/gui/data/oldicons/twonode.gif deleted file mode 100644 index 28e75fac3aadc9286cdb65b1269f3f1355f055d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 220 zcmZ?wbhEHbRA5kGIK;vL1pmSK$$5tVNI>zQv_`U~f{}rNg+fV2s)AE~YGz)#f^&Xu zL1JDdgW^vXMlJ>x1|5(AAfp(Vn>=>i`Dbv-A$vka-~r3C5f^&JB{~NwA;X2aQL-=@frKKWwv3( zs%b?M|9mdLmiXy?;63*j?HbL7MqLIbcP^&3j?Qk~UiW^@i7u0sr`k2mm^rJirgNbX HCxbNrD`Hob diff --git a/daemon/core/gui/data/oldicons/wlan.gif b/daemon/core/gui/data/oldicons/wlan.gif deleted file mode 100644 index 566185767ec66e52772a60d779914e862d701b77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 173 zcmZ?wbhEHbRA5kGSjYeZ|Ns97(+rCL_%xC=jSLLT6rA!?GxO3J6o0ZXaxpM5=ztV} zbTBaGOzB^F`Yr$BkOkK`vu}KRbUAksM-9_~-mCzxs9k-z!pF?KCvUe^f3fd#K!f?h z{)jh Date: Mon, 30 Dec 2019 00:00:36 -0800 Subject: [PATCH 461/462] updated icons and added icon attribution to about dialog --- daemon/core/gui/data/icons/docker.png | Bin 2084 -> 1533 bytes daemon/core/gui/data/icons/lxc.png | Bin 2167 -> 1553 bytes daemon/core/gui/dialogs/about.py | 6 ++++++ 3 files changed, 6 insertions(+) diff --git a/daemon/core/gui/data/icons/docker.png b/daemon/core/gui/data/icons/docker.png index 6727a58badf3525beabe79ac32f04f6020c05402..6021c64036078a1680466c72e23da8f07942a98a 100644 GIT binary patch delta 1496 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 B)?)ww diff --git a/daemon/core/gui/data/icons/lxc.png b/daemon/core/gui/data/icons/lxc.png index 7b515142344da399d906dd6279cfdca2abfc7977..b944b231dd74291e35675a35e55d0cdc0071651c 100644 GIT binary patch delta 1516 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 diff --git a/daemon/core/gui/dialogs/about.py b/daemon/core/gui/dialogs/about.py index af33ab61..d54266f4 100644 --- a/daemon/core/gui/dialogs/about.py +++ b/daemon/core/gui/dialogs/about.py @@ -1,4 +1,5 @@ import tkinter as tk +from tkinter import ttk from core.gui.dialogs.dialog import Dialog from core.gui.widgets import CodeText @@ -42,3 +43,8 @@ class AboutDialog(Dialog): codetext.text.insert("1.0", LICENSE) codetext.text.config(state=tk.DISABLED) codetext.grid(sticky="nsew") + + label = ttk.Label( + self.top, text="Icons from https://icons8.com", anchor=tk.CENTER + ) + label.grid(sticky="ew") From 3e87737ee6fd1aabc20fab928c52efbe611d4408 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 30 Dec 2019 16:34:44 -0800 Subject: [PATCH 462/462] updates to use tk after for backgrounded tasks, also added background task convenience class for running something in the background and running a callback using tk.after when done --- daemon/core/gui/app.py | 7 +++ daemon/core/gui/coreclient.py | 69 ++++++++++------------------- daemon/core/gui/dialogs/marker.py | 16 ++----- daemon/core/gui/dialogs/sessions.py | 8 ++-- daemon/core/gui/menuaction.py | 46 +++++++------------ daemon/core/gui/task.py | 29 ++++++++++++ daemon/core/gui/toolbar.py | 62 +++++++++++++++++++------- 7 files changed, 127 insertions(+), 110 deletions(-) create mode 100644 daemon/core/gui/task.py diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index 969292fe..dba22068 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -93,5 +93,12 @@ class Application(tk.Frame): def save_config(self): appconfig.save(self.guiconfig) + def joined_session_update(self): + self.statusbar.progress_bar.stop() + if self.core.is_runtime(): + self.toolbar.set_runtime() + else: + self.toolbar.set_design() + def close(self): self.master.destroy() diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 2f1488fd..f30dcc94 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -4,9 +4,7 @@ Incorporate grpc into python tkinter GUI import json import logging import os -import time from pathlib import Path -from tkinter import messagebox import grpc @@ -294,16 +292,10 @@ class CoreClient: response = self.client.get_session_metadata(self.session_id) self.parse_metadata(response.config) except grpc.RpcError as e: - show_grpc_error(e) + self.app.after(0, show_grpc_error, e) # update ui to represent current state - if self.is_runtime(): - self.app.toolbar.runtime_frame.tkraise() - self.app.toolbar.click_runtime_selection() - else: - self.app.toolbar.design_frame.tkraise() - self.app.toolbar.click_selection() - self.app.statusbar.progress_bar.stop() + self.app.after(0, self.app.joined_session_update) def is_runtime(self): return self.state == core_pb2.SessionState.RUNTIME @@ -390,7 +382,7 @@ class CoreClient: ) self.join_session(response.session_id, query_location=False) except grpc.RpcError as e: - show_grpc_error(e) + self.app.after(0, show_grpc_error, e) def delete_session(self, session_id=None): if session_id is None: @@ -399,7 +391,7 @@ class CoreClient: response = self.client.delete_session(session_id) logging.info("deleted session result: %s", response) except grpc.RpcError as e: - show_grpc_error(e) + self.app.after(0, show_grpc_error, e) def set_up(self): """ @@ -431,7 +423,7 @@ class CoreClient: x.node_type: set(x.services) for x in response.defaults } except grpc.RpcError as e: - show_grpc_error(e) + self.app.after(0, show_grpc_error, e) self.app.close() def edit_node(self, core_node): @@ -440,7 +432,7 @@ class CoreClient: self.session_id, core_node.id, core_node.position, source=GUI_SOURCE ) except grpc.RpcError as e: - show_grpc_error(e) + self.app.after(0, show_grpc_error, e) def start_session(self): nodes = [x.core_node for x in self.canvas_nodes.values()] @@ -459,7 +451,7 @@ class CoreClient: else: emane_config = None - start = time.perf_counter() + response = core_pb2.StartSessionResponse(result=False) try: response = self.client.start_session( self.session_id, @@ -478,45 +470,30 @@ class CoreClient: logging.debug( "start session(%s), result: %s", self.session_id, response.result ) - process_time = time.perf_counter() - start - - # stop progress bar and update status - self.app.statusbar.progress_bar.stop() - message = f"Start ran for {process_time:.3f} seconds" - self.app.statusbar.set_status(message) if response.result: self.set_metadata() - self.app.toolbar.set_runtime() - - # display mobility players - for node_id, config in self.mobility_configs.items(): - canvas_node = self.canvas_nodes[node_id] - mobility_player = MobilityPlayer( - self.app, self.app, canvas_node, config - ) - mobility_player.show() - self.mobility_players[node_id] = mobility_player - else: - message = "\n".join(response.exceptions) - messagebox.showerror("Start Error", message) except grpc.RpcError as e: - show_grpc_error(e) + self.app.after(0, show_grpc_error, e) + return response def stop_session(self, session_id=None): if not session_id: session_id = self.session_id - start = time.perf_counter() + response = core_pb2.StopSessionResponse(result=False) try: response = self.client.stop_session(session_id) - self.app.canvas.stopped_session() - logging.debug( - "stopped session(%s), result: %s", session_id, response.result - ) - process_time = time.perf_counter() - start - self.app.statusbar.stop_session_callback(process_time) + logging.debug("stopped session(%s), result: %s", session_id, response) except grpc.RpcError as e: - show_grpc_error(e) + self.app.after(0, show_grpc_error, e) + return response + + def show_mobility_players(self): + for node_id, config in self.mobility_configs.items(): + canvas_node = self.canvas_nodes[node_id] + mobility_player = MobilityPlayer(self.app, self.app, canvas_node, config) + mobility_player.show() + self.mobility_players[node_id] = mobility_player def set_metadata(self): # create canvas data @@ -549,7 +526,7 @@ class CoreClient: logging.info("get terminal %s", response.terminal) os.system(f"{terminal} {response.terminal} &") except grpc.RpcError as e: - show_grpc_error(e) + self.app.after(0, show_grpc_error, e) def save_xml(self, file_path): """ @@ -567,7 +544,7 @@ class CoreClient: response = self.client.save_xml(self.session_id, file_path) logging.info("saved xml(%s): %s", file_path, response) except grpc.RpcError as e: - show_grpc_error(e) + self.app.after(0, show_grpc_error, e) def open_xml(self, file_path): """ @@ -581,7 +558,7 @@ class CoreClient: logging.debug("open xml: %s", response) self.join_session(response.session_id) except grpc.RpcError as e: - show_grpc_error(e) + self.app.after(0, show_grpc_error, e) def get_node_service(self, node_id, service_name): response = self.client.get_node_service(self.session_id, node_id, service_name) diff --git a/daemon/core/gui/dialogs/marker.py b/daemon/core/gui/dialogs/marker.py index 7e432305..159abd7f 100644 --- a/daemon/core/gui/dialogs/marker.py +++ b/daemon/core/gui/dialogs/marker.py @@ -2,7 +2,6 @@ marker dialog """ -import logging import tkinter as tk from tkinter import ttk @@ -20,7 +19,6 @@ class MarkerDialog(Dialog): self.radius = MARKER_THICKNESS[0] self.marker_thickness = tk.IntVar(value=MARKER_THICKNESS[0]) self.draw() - self.top.bind("", self.close_marker) def draw(self): button = ttk.Button(self.top, text="clear", command=self.clear_marker) @@ -64,16 +62,8 @@ class MarkerDialog(Dialog): def change_thickness(self, event): self.radius = self.marker_thickness.get() - def close_marker(self, event): - logging.debug("destroy marker dialog") - self.app.toolbar.marker_tool = None - - def position(self): - print(self.winfo_width(), self.winfo_height()) - # print(self.app.master.winfo_x(), self.app.master.winfo_y()) - print(self.app.canvas.winfo_rootx()) + def show(self): + super().show() self.geometry( - "+{}+{}".format( - self.app.canvas.winfo_rootx(), self.app.canvas.master.winfo_rooty() - ) + f"+{self.app.canvas.winfo_rootx()}+{self.app.canvas.master.winfo_rooty()}" ) diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index e86f5351..ad348280 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -1,5 +1,4 @@ import logging -import threading import tkinter as tk from tkinter import ttk @@ -9,6 +8,7 @@ from core.api.grpc import core_pb2 from core.gui.dialogs.dialog import Dialog from core.gui.errors import show_grpc_error from core.gui.images import ImageEnum, Images +from core.gui.task import BackgroundTask from core.gui.themes import PADX, PADY @@ -164,10 +164,8 @@ class SessionsDialog(Dialog): def join_session(self, session_id): self.app.statusbar.progress_bar.start(5) - thread = threading.Thread( - target=self.app.core.join_session, args=([session_id]) - ) - thread.start() + task = BackgroundTask(self.app, self.app.core.join_session, args=(session_id,)) + task.start() self.destroy() def on_selected(self, event): diff --git a/daemon/core/gui/menuaction.py b/daemon/core/gui/menuaction.py index 18e511e6..c48f82ff 100644 --- a/daemon/core/gui/menuaction.py +++ b/daemon/core/gui/menuaction.py @@ -3,13 +3,9 @@ The actions taken when each menubar option is clicked """ import logging -import threading -import time import webbrowser from tkinter import filedialog, messagebox -import grpc - from core.gui.appconfig import XMLS_PATH from core.gui.dialogs.about import AboutDialog from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog @@ -21,6 +17,7 @@ from core.gui.dialogs.servers import ServersDialog from core.gui.dialogs.sessionoptions import SessionOptionsDialog from core.gui.dialogs.sessions import SessionsDialog from core.gui.dialogs.throughput import ThroughputDialog +from core.gui.task import BackgroundTask class MenuAction: @@ -35,13 +32,10 @@ class MenuAction: def cleanup_old_session(self, quitapp=False): logging.info("cleaning up old session") - start = time.perf_counter() self.app.core.stop_session() self.app.core.delete_session() - process_time = time.perf_counter() - start - self.app.statusbar.stop_session_callback(process_time) - if quitapp: - self.app.quit() + # if quitapp: + # self.app.quit() def prompt_save_running_session(self, quitapp=False): """ @@ -49,26 +43,18 @@ class MenuAction: :return: nothing """ - try: - if not self.app.core.is_runtime(): - self.app.core.delete_session() - if quitapp: - self.app.quit() - else: - result = messagebox.askyesnocancel("Exit", "Stop the running session?") - if result is True: - self.app.statusbar.progress_bar.start(5) - thread = threading.Thread( - target=self.cleanup_old_session, args=([quitapp]) - ) - thread.daemon = True - thread.start() - elif result is False and quitapp: - self.app.quit() - except grpc.RpcError: - logging.exception("error deleting session") + result = True + if self.app.core.is_runtime(): + result = messagebox.askyesnocancel("Exit", "Stop the running session?") + + if result: + callback = None if quitapp: - self.app.quit() + callback = self.app.quit + task = BackgroundTask(self.app, self.cleanup_old_session, callback) + task.start() + elif quitapp: + self.app.quit() def on_quit(self, event=None): """ @@ -100,8 +86,8 @@ class MenuAction: logging.info("opening xml: %s", file_path) self.prompt_save_running_session() self.app.statusbar.progress_bar.start(5) - thread = threading.Thread(target=self.app.core.open_xml, args=([file_path])) - thread.start() + task = BackgroundTask(self.app, self.app.core.open_xml, args=(file_path,)) + task.start() def gui_preferences(self): dialog = PreferencesDialog(self.app, self.app) diff --git a/daemon/core/gui/task.py b/daemon/core/gui/task.py new file mode 100644 index 00000000..bf731dd4 --- /dev/null +++ b/daemon/core/gui/task.py @@ -0,0 +1,29 @@ +import logging +import threading + + +class BackgroundTask: + def __init__(self, master, task, callback=None, args=()): + self.master = master + self.args = args + self.task = task + self.callback = callback + self.thread = None + + def start(self): + logging.info("starting task") + self.thread = threading.Thread(target=self.run, daemon=True) + self.thread.start() + + def run(self): + result = self.task(*self.args) + logging.info("task completed") + if self.callback: + if result is None: + args = () + elif isinstance(result, (list, tuple)): + args = result + else: + args = (result,) + logging.info("calling callback: %s", args) + self.master.after(0, self.callback, *args) diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 9229f661..5404d9e5 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -1,8 +1,8 @@ import logging -import threading +import time import tkinter as tk from functools import partial -from tkinter import ttk +from tkinter import messagebox, ttk from tkinter.font import Font from core.gui.dialogs.customnodes import CustomNodesDialog @@ -11,6 +11,7 @@ from core.gui.graph.enums import GraphMode from core.gui.graph.shapeutils import ShapeType, is_marker from core.gui.images import ImageEnum, Images from core.gui.nodeutils import NodeUtils +from core.gui.task import BackgroundTask from core.gui.themes import Styles from core.gui.tooltip import Tooltip @@ -36,6 +37,7 @@ class Toolbar(ttk.Frame): super().__init__(master, **kwargs) self.app = app self.master = app.master + self.time = None # picker data self.picker_font = Font(size=8) @@ -237,13 +239,32 @@ class Toolbar(ttk.Frame): self.app.canvas.hide_context() self.app.statusbar.progress_bar.start(5) self.app.canvas.mode = GraphMode.SELECT - thread = threading.Thread(target=self.app.core.start_session) - thread.start() + self.time = time.perf_counter() + task = BackgroundTask(self, self.app.core.start_session, self.start_callback) + task.start() + + def start_callback(self, response): + self.app.statusbar.progress_bar.stop() + total = time.perf_counter() - self.time + message = f"Start ran for {total:.3f} seconds" + self.app.statusbar.set_status(message) + self.time = None + if response.result: + self.set_runtime() + self.app.core.set_metadata() + self.app.core.show_mobility_players() + else: + message = "\n".join(response.exceptions) + messagebox.showerror("Start Error", message) def set_runtime(self): self.runtime_frame.tkraise() self.click_runtime_selection() + def set_design(self): + self.design_frame.tkraise() + self.click_selection() + def click_link(self): logging.debug("Click LINK button") self.design_select(self.link_button) @@ -401,10 +422,19 @@ class Toolbar(ttk.Frame): """ self.app.canvas.hide_context() self.app.statusbar.progress_bar.start(5) - thread = threading.Thread(target=self.app.core.stop_session) - thread.start() - self.design_frame.tkraise() - self.click_selection() + self.time = time.perf_counter() + task = BackgroundTask(self, self.app.core.stop_session, self.stop_callback) + task.start() + + def stop_callback(self, response): + self.app.statusbar.progress_bar.stop() + self.set_design() + total = time.perf_counter() - self.time + message = f"Stopped in {total:.3f} seconds" + self.app.statusbar.set_status(message) + self.app.canvas.stopped_session() + if not response.result: + messagebox.showerror("Stop Error", "Errors stopping session") def update_annotation(self, image, shape_type): logging.info("clicked annotation: ") @@ -414,10 +444,10 @@ class Toolbar(ttk.Frame): self.app.canvas.mode = GraphMode.ANNOTATION self.app.canvas.annotation_type = shape_type if is_marker(shape_type): - if not self.marker_tool: - self.marker_tool = MarkerDialog(self.master, self.app) - self.marker_tool.show() - self.marker_tool.position() + if self.marker_tool: + self.marker_tool.destroy() + self.marker_tool = MarkerDialog(self.master, self.app) + self.marker_tool.show() def click_run_button(self): logging.debug("Click on RUN button") @@ -430,10 +460,10 @@ class Toolbar(ttk.Frame): self.runtime_select(self.runtime_marker_button) self.app.canvas.mode = GraphMode.ANNOTATION self.app.canvas.annotation_type = ShapeType.MARKER - if not self.marker_tool: - self.marker_tool = MarkerDialog(self.master, self.app) - self.marker_tool.show() - self.marker_tool.position() + if self.marker_tool: + self.marker_tool.destroy() + self.marker_tool = MarkerDialog(self.master, self.app) + self.marker_tool.show() def click_two_node_button(self): logging.debug("Click TWONODE button")