Merge pull request #562 from coreemu/develop

7.5.0 merge
This commit is contained in:
bharnden 2021-03-18 21:22:35 -07:00 committed by GitHub
commit b2726b627f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 2564 additions and 1805 deletions

View file

@ -1,3 +1,22 @@
## 2021-03-11 CORE 7.5.0
* core-daemon
* fixed issue setting mobility loop value properly
* fixed issue that some states would not properly remove session directories
* \#560 - fixed issues with sdt integration for mobility movement and layer creation
* core-pygui
* added multiple canvas support
* added support to hide nodes and restore them visually
* update to assign full netmasks to wireless connected nodes by default
* update to display services and action controls for nodes during runtime
* fixed issues with custom nodes
* fixed issue auto assigning macs, avoiding duplication
* fixed issue joining session with different netmasks
* fixed issues when deleting a session from the sessions dialog
* \#550 - fixed issue not sending all service customization data
* core-cli
* added delete session command
## 2021-01-11 CORE 7.4.0
* Installation

View file

@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script.
# this defines the CORE version number, must be static for AC_INIT
AC_INIT(core, 7.4.0)
AC_INIT(core, 7.5.0)
# autoconf and automake initialization
AC_CONFIG_SRCDIR([netns/version.h.in])

View file

@ -25,7 +25,9 @@ from core.emulator.session import Session
from core.errors import CoreError
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
from core.nodes.docker import DockerNode
from core.nodes.interface import CoreInterface
from core.nodes.lxd import LxcNode
from core.nodes.network import WlanNode
from core.services.coreservices import CoreService
@ -67,6 +69,7 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption
image=node_proto.image,
services=node_proto.services,
config_services=node_proto.config_services,
canvas=node_proto.canvas,
)
if node_proto.emane:
options.emane = node_proto.emane
@ -263,19 +266,22 @@ def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node:
geo = core_pb2.Geo(
lat=node.position.lat, lon=node.position.lon, alt=node.position.alt
)
services = getattr(node, "services", [])
if services is None:
services = []
services = [x.name for x in services]
config_services = getattr(node, "config_services", {})
config_services = [x for x in config_services]
services = [x.name for x in node.services]
model = node.type
node_dir = None
config_services = []
if isinstance(node, CoreNodeBase):
node_dir = node.nodedir
config_services = [x for x in node.config_services]
channel = None
if isinstance(node, CoreNode):
channel = node.ctrlchnlname
emane_model = None
if isinstance(node, EmaneNet):
emane_model = node.model.name
model = getattr(node, "type", None)
node_dir = getattr(node, "nodedir", None)
channel = getattr(node, "ctrlchnlname", None)
image = getattr(node, "image", None)
image = None
if isinstance(node, (DockerNode, LxcNode)):
image = node.image
return core_pb2.Node(
id=node.id,
name=node.name,
@ -290,6 +296,7 @@ def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node:
config_services=config_services,
dir=node_dir,
channel=channel,
canvas=node.canvas,
)

View file

@ -649,6 +649,7 @@ class Node:
geo: Geo = None
dir: str = None
channel: str = None
canvas: int = None
# configurations
emane_model_configs: Dict[
@ -683,6 +684,7 @@ class Node:
geo=Geo.from_proto(proto.geo),
dir=proto.dir,
channel=proto.channel,
canvas=proto.canvas,
)
def to_proto(self) -> core_pb2.Node:
@ -700,6 +702,7 @@ class Node:
server=self.server,
dir=self.dir,
channel=self.channel,
canvas=self.canvas,
)

View file

@ -103,8 +103,7 @@ class FRRZebra(ConfigService):
ip4s.append(str(ip4.ip))
for ip6 in iface.ip6s:
ip6s.append(str(ip6.ip))
is_control = getattr(iface, "control", False)
ifaces.append((iface, ip4s, ip6s, is_control))
ifaces.append((iface, ip4s, ip6s, iface.control))
return dict(
frr_conf=frr_conf,

View file

@ -104,8 +104,7 @@ class Zebra(ConfigService):
ip4s.append(str(ip4.ip))
for ip6 in iface.ip6s:
ip6s.append(str(ip6.ip))
is_control = getattr(iface, "control", False)
ifaces.append((iface, ip4s, ip6s, is_control))
ifaces.append((iface, ip4s, ip6s, iface.control))
return dict(
quagga_bin_search=quagga_bin_search,

View file

@ -134,7 +134,6 @@ class Session:
self.link_handlers: List[Callable[[LinkData], None]] = []
self.file_handlers: List[Callable[[FileData], None]] = []
self.config_handlers: List[Callable[[ConfigData], None]] = []
self.shutdown_handlers: List[Callable[[Session], None]] = []
# session options/metadata
self.options: SessionConfig = SessionConfig()
@ -591,7 +590,6 @@ class Session:
:raises core.CoreError: when node to update does not exist
"""
node = self.get_node(node_id, NodeBase)
node.canvas = options.canvas
node.icon = options.icon
self.set_node_position(node, options)
self.sdt.edit_node(node, options.lon, options.lat, options.alt)
@ -772,20 +770,17 @@ class Session:
"""
if self.state == EventTypes.SHUTDOWN_STATE:
logging.info("session(%s) state(%s) already shutdown", self.id, self.state)
return
logging.info("session(%s) state(%s) shutting down", self.id, self.state)
self.set_state(EventTypes.SHUTDOWN_STATE, send_event=True)
# clear out current core session
self.clear()
# shutdown sdt
self.sdt.shutdown()
else:
logging.info("session(%s) state(%s) shutting down", self.id, self.state)
self.set_state(EventTypes.SHUTDOWN_STATE, send_event=True)
# clear out current core session
self.clear()
# shutdown sdt
self.sdt.shutdown()
# remove this sessions working directory
preserve = self.options.get_config("preservedir") == "1"
if not preserve:
shutil.rmtree(self.session_dir, ignore_errors=True)
# call session shutdown handlers
for handler in self.shutdown_handlers:
handler(self)
def broadcast_event(self, event_data: EventData) -> None:
"""

View file

@ -1,22 +1,23 @@
import logging
import math
import tkinter as tk
from tkinter import PhotoImage, font, ttk
from tkinter import PhotoImage, font, messagebox, ttk
from tkinter.ttk import Progressbar
from typing import Any, Dict, Optional, Type
import grpc
from core.gui import appconfig, themes
from core.gui import appconfig, images
from core.gui import nodeutils as nutils
from core.gui import themes
from core.gui.appconfig import GuiConfig
from core.gui.coreclient import CoreClient
from core.gui.dialogs.error import ErrorDialog
from core.gui.frames.base import InfoFrameBase
from core.gui.frames.default import DefaultInfoFrame
from core.gui.graph.graph import CanvasGraph
from core.gui.images import ImageEnum, Images
from core.gui.graph.manager import CanvasManager
from core.gui.images import ImageEnum
from core.gui.menubar import Menubar
from core.gui.nodeutils import NodeUtils
from core.gui.statusbar import StatusBar
from core.gui.themes import PADY
from core.gui.toolbar import Toolbar
@ -29,13 +30,13 @@ class Application(ttk.Frame):
def __init__(self, proxy: bool, session_id: int = None) -> None:
super().__init__()
# load node icons
NodeUtils.setup()
nutils.setup()
# widgets
self.menubar: Optional[Menubar] = None
self.toolbar: Optional[Toolbar] = None
self.right_frame: Optional[ttk.Frame] = None
self.canvas: Optional[CanvasGraph] = None
self.manager: Optional[CanvasManager] = None
self.statusbar: Optional[StatusBar] = None
self.progress: Optional[Progressbar] = None
self.infobar: Optional[ttk.Frame] = None
@ -77,7 +78,7 @@ class Application(ttk.Frame):
self.master.title("CORE")
self.center()
self.master.protocol("WM_DELETE_WINDOW", self.on_closing)
image = Images.get(ImageEnum.CORE, 16)
image = images.from_enum(ImageEnum.CORE, width=images.DIALOG_SIZE)
self.master.tk.call("wm", "iconphoto", self.master._w, image)
self.master.option_add("*tearOff", tk.FALSE)
self.setup_file_dialogs()
@ -136,20 +137,8 @@ class Application(ttk.Frame):
label.grid(sticky=tk.EW, pady=PADY)
def draw_canvas(self) -> None:
canvas_frame = ttk.Frame(self.right_frame)
canvas_frame.rowconfigure(0, weight=1)
canvas_frame.columnconfigure(0, weight=1)
canvas_frame.grid(row=0, column=0, sticky=tk.NSEW, pady=1)
self.canvas = CanvasGraph(canvas_frame, self, self.core)
self.canvas.grid(sticky=tk.NSEW)
scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview)
scroll_y.grid(row=0, column=1, sticky=tk.NS)
scroll_x = ttk.Scrollbar(
canvas_frame, orient=tk.HORIZONTAL, command=self.canvas.xview
)
scroll_x.grid(row=1, column=0, sticky=tk.EW)
self.canvas.configure(xscrollcommand=scroll_x.set)
self.canvas.configure(yscrollcommand=scroll_y.set)
self.manager = CanvasManager(self.right_frame, self, self.core)
self.manager.notebook.grid(sticky=tk.NSEW)
def draw_status(self) -> None:
self.statusbar = StatusBar(self.right_frame, self)
@ -179,17 +168,30 @@ class Application(ttk.Frame):
def hide_info(self) -> None:
self.infobar.grid_forget()
def show_grpc_exception(self, title: str, e: grpc.RpcError) -> None:
def show_grpc_exception(
self, message: str, e: grpc.RpcError, blocking: bool = False
) -> None:
logging.exception("app grpc exception", exc_info=e)
message = e.details()
self.show_error(title, message)
dialog = ErrorDialog(self, "GRPC Exception", message, e.details())
if blocking:
dialog.show()
else:
self.after(0, lambda: dialog.show())
def show_exception(self, title: str, e: Exception) -> None:
def show_exception(self, message: str, e: Exception) -> None:
logging.exception("app exception", exc_info=e)
self.show_error(title, str(e))
self.after(
0, lambda: ErrorDialog(self, "App Exception", message, str(e)).show()
)
def show_error(self, title: str, message: str) -> None:
self.after(0, lambda: ErrorDialog(self, title, message).show())
def show_exception_data(self, title: str, message: str, details: str) -> None:
self.after(0, lambda: ErrorDialog(self, title, message, details).show())
def show_error(self, title: str, message: str, blocking: bool = False) -> None:
if blocking:
messagebox.showerror(title, message, parent=self)
else:
self.after(0, lambda: messagebox.showerror(title, message, parent=self))
def on_closing(self) -> None:
if self.toolbar.picker:
@ -201,15 +203,17 @@ class Application(ttk.Frame):
def joined_session_update(self) -> None:
if self.core.is_runtime():
self.menubar.set_state(is_runtime=True)
self.toolbar.set_runtime()
else:
self.menubar.set_state(is_runtime=False)
self.toolbar.set_design()
def get_icon(self, image_enum: ImageEnum, width: int) -> PhotoImage:
return Images.get(image_enum, int(width * self.app_scale))
def get_enum_icon(self, image_enum: ImageEnum, *, width: int) -> PhotoImage:
return images.from_enum(image_enum, width=width, scale=self.app_scale)
def get_custom_icon(self, image_file: str, width: int) -> PhotoImage:
return Images.get_custom(image_file, int(width * self.app_scale))
def get_file_icon(self, file_path: str, *, width: int) -> PhotoImage:
return images.from_file(file_path, width=width, scale=self.app_scale)
def close(self) -> None:
self.master.destroy()

View file

@ -185,7 +185,8 @@ class GuiConfig(yaml.YAMLObject):
def copy_files(current_path: Path, new_path: Path) -> None:
for current_file in current_path.glob("*"):
new_file = new_path.joinpath(current_file.name)
shutil.copy(current_file, new_file)
if not new_file.exists():
shutil.copy(current_file, new_file)
def find_terminal() -> Optional[str]:
@ -197,30 +198,27 @@ def find_terminal() -> Optional[str]:
def check_directory() -> None:
if HOME_PATH.exists():
return
HOME_PATH.mkdir()
BACKGROUNDS_PATH.mkdir()
CUSTOM_EMANE_PATH.mkdir()
CUSTOM_SERVICE_PATH.mkdir()
ICONS_PATH.mkdir()
MOBILITY_PATH.mkdir()
XMLS_PATH.mkdir()
SCRIPT_PATH.mkdir()
HOME_PATH.mkdir(exist_ok=True)
BACKGROUNDS_PATH.mkdir(exist_ok=True)
CUSTOM_EMANE_PATH.mkdir(exist_ok=True)
CUSTOM_SERVICE_PATH.mkdir(exist_ok=True)
ICONS_PATH.mkdir(exist_ok=True)
MOBILITY_PATH.mkdir(exist_ok=True)
XMLS_PATH.mkdir(exist_ok=True)
SCRIPT_PATH.mkdir(exist_ok=True)
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)
terminal = find_terminal()
if "EDITOR" in os.environ:
editor = EDITORS[0]
else:
editor = EDITORS[1]
preferences = PreferencesConfig(editor, terminal)
config = GuiConfig(preferences=preferences)
save(config)
if not CONFIG_PATH.exists():
terminal = find_terminal()
if "EDITOR" in os.environ:
editor = EDITORS[0]
else:
editor = EDITORS[1]
preferences = PreferencesConfig(editor, terminal)
config = GuiConfig(preferences=preferences)
save(config)
def read() -> GuiConfig:

View file

@ -6,7 +6,6 @@ import json
import logging
import os
import tkinter as tk
from pathlib import Path
from tkinter import messagebox
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple
@ -25,7 +24,6 @@ from core.api.grpc.wrappers import (
ConfigOption,
ConfigService,
ExceptionEvent,
Interface,
Link,
LinkEvent,
LinkType,
@ -40,18 +38,16 @@ from core.api.grpc.wrappers import (
SessionState,
ThroughputsEvent,
)
from core.gui import appconfig
from core.gui.appconfig import BACKGROUNDS_PATH, XMLS_PATH, CoreServer, Observer
from core.gui import nodeutils as nutils
from core.gui.appconfig import XMLS_PATH, CoreServer, Observer
from core.gui.dialogs.emaneinstall import EmaneInstallDialog
from core.gui.dialogs.error import ErrorDialog
from core.gui.dialogs.mobilityplayer import MobilityPlayer
from core.gui.dialogs.sessions import SessionsDialog
from core.gui.graph.edges import CanvasEdge
from core.gui.graph.node import CanvasNode
from core.gui.graph.shape import AnnotationData, Shape
from core.gui.graph.shapeutils import ShapeType
from core.gui.graph.shape import Shape
from core.gui.interface import InterfaceManager
from core.gui.nodeutils import NodeDraw, NodeUtils
from core.gui.nodeutils import NodeDraw
if TYPE_CHECKING:
from core.gui.app import Application
@ -155,7 +151,7 @@ class CoreClient:
self.custom_observers[observer.name] = observer
def handle_events(self, event: core_pb2.Event) -> None:
if event.source == GUI_SOURCE:
if not self.session or event.source == GUI_SOURCE:
return
if event.session_id != self.session.id:
logging.warning(
@ -207,27 +203,26 @@ class CoreClient:
canvas_node2 = self.canvas_nodes[node2_id]
if event.link.type == LinkType.WIRELESS:
if event.message_type == MessageType.ADD:
self.app.canvas.add_wireless_edge(
self.app.manager.add_wireless_edge(
canvas_node1, canvas_node2, event.link
)
elif event.message_type == MessageType.DELETE:
self.app.canvas.delete_wireless_edge(
self.app.manager.delete_wireless_edge(
canvas_node1, canvas_node2, event.link
)
elif event.message_type == MessageType.NONE:
self.app.canvas.update_wireless_edge(
self.app.manager.update_wireless_edge(
canvas_node1, canvas_node2, event.link
)
else:
logging.warning("unknown link event: %s", event)
else:
if event.message_type == MessageType.ADD:
self.app.canvas.add_wired_edge(canvas_node1, canvas_node2, event.link)
self.app.canvas.organize()
self.app.manager.add_wired_edge(canvas_node1, canvas_node2, event.link)
elif event.message_type == MessageType.DELETE:
self.app.canvas.delete_wired_edge(event.link)
self.app.manager.delete_wired_edge(event.link)
elif event.message_type == MessageType.NONE:
self.app.canvas.update_wired_edge(event.link)
self.app.manager.update_wired_edge(event.link)
else:
logging.warning("unknown link event: %s", event)
@ -243,13 +238,13 @@ class CoreClient:
canvas_node.update_icon(node.icon)
elif event.message_type == MessageType.DELETE:
canvas_node = self.canvas_nodes[node.id]
self.app.canvas.clear_selection()
self.app.canvas.select_object(canvas_node.id)
self.app.canvas.delete_selected_objects()
canvas_node.canvas.clear_selection()
canvas_node.canvas.select_object(canvas_node.id)
canvas_node.canvas.delete_selected_objects()
elif event.message_type == MessageType.ADD:
if node.id in self.session.nodes:
logging.error("core node already exists: %s", node)
self.app.canvas.add_core_node(node)
self.app.manager.add_core_node(node)
else:
logging.warning("unknown node event: %s", event)
@ -262,7 +257,7 @@ class CoreClient:
if self.handling_throughputs:
self.handling_throughputs.cancel()
self.handling_throughputs = None
self.app.canvas.clear_throughputs()
self.app.manager.clear_throughputs()
def cancel_events(self) -> None:
if self.handling_events:
@ -293,7 +288,7 @@ class CoreClient:
)
return
logging.debug("handling throughputs event: %s", event)
self.app.after(0, self.app.canvas.set_throughputs, event)
self.app.after(0, self.app.manager.set_throughputs, event)
def handle_cpu_event(self, event: core_pb2.CpuUsageEvent) -> None:
self.app.after(0, self.app.statusbar.set_cpu, event.usage)
@ -315,9 +310,7 @@ class CoreClient:
self.session.id, self.handle_events
)
self.ifaces_manager.joined(self.session.links)
self.app.canvas.reset_and_redraw(self.session)
self.parse_metadata()
self.app.canvas.organize()
self.app.manager.join(self.session)
if self.is_runtime():
self.show_mobility_players()
self.app.after(0, self.app.joined_session_update)
@ -334,23 +327,7 @@ class CoreClient:
logging.debug("canvas metadata: %s", canvas_config)
if canvas_config:
canvas_config = json.loads(canvas_config)
gridlines = canvas_config.get("gridlines", True)
self.app.canvas.show_grid.set(gridlines)
fit_image = canvas_config.get("fit_image", False)
self.app.canvas.adjust_to_dim.set(fit_image)
wallpaper_style = canvas_config.get("wallpaper-style", 1)
self.app.canvas.scale_option.set(wallpaper_style)
width = self.app.guiconfig.preferences.width
height = self.app.guiconfig.preferences.height
dimensions = canvas_config.get("dimensions", [width, height])
self.app.canvas.redraw_canvas(dimensions)
wallpaper = canvas_config.get("wallpaper")
if wallpaper:
wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper))
self.app.canvas.set_wallpaper(wallpaper)
else:
self.app.canvas.redraw_canvas()
self.app.canvas.set_wallpaper(None)
self.app.manager.parse_metadata(canvas_config)
# load saved shapes
shapes_config = config.get("shapes")
@ -358,28 +335,7 @@ class CoreClient:
shapes_config = json.loads(shapes_config)
for shape_config in shapes_config:
logging.debug("loading shape: %s", shape_config)
shape_type = shape_config["type"]
try:
shape_type = ShapeType(shape_type)
coords = shape_config["iconcoords"]
data = AnnotationData(
shape_config["label"],
shape_config["fontfamily"],
shape_config["fontsize"],
shape_config["labelcolor"],
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
)
self.app.canvas.shapes[shape.id] = shape
except ValueError:
logging.exception("unknown shape: %s", shape_type)
Shape.from_metadata(self.app, shape_config)
# load edges config
edges_config = config.get("edges")
@ -392,6 +348,17 @@ class CoreClient:
edge.color = edge_config["color"]
edge.redraw()
# read hidden nodes
hidden = config.get("hidden")
if hidden:
hidden = json.loads(hidden)
for _id in hidden:
canvas_node = self.canvas_nodes.get(_id)
if canvas_node:
canvas_node.hide()
else:
logging.warning("invalid node to hide: %s", _id)
def create_new_session(self) -> None:
"""
Create a new session
@ -450,10 +417,11 @@ class CoreClient:
if session_id:
session_ids = set(x.id for x in sessions)
if session_id not in session_ids:
dialog = ErrorDialog(
self.app, "Join Session Error", f"{session_id} does not exist"
self.app.show_error(
"Join Session Error",
f"{session_id} does not exist",
blocking=True,
)
dialog.show()
self.app.close()
else:
self.join_session(session_id)
@ -465,8 +433,7 @@ class CoreClient:
dialog.show()
except grpc.RpcError as e:
logging.exception("core setup error")
dialog = ErrorDialog(self.app, "Setup Error", e.details())
dialog.show()
self.app.show_grpc_exception("Setup Error", e, blocking=True)
self.app.close()
def edit_node(self, core_node: Node) -> None:
@ -483,7 +450,7 @@ class CoreClient:
self.client.add_session_server(self.session.id, server.name, server.address)
def start_session(self) -> Tuple[bool, List[str]]:
self.ifaces_manager.reset_mac()
self.ifaces_manager.set_macs([x.link for x in self.links.values()])
nodes = [x.to_proto() for x in self.session.nodes.values()]
links = []
asymmetric_links = []
@ -548,7 +515,7 @@ class CoreClient:
def show_mobility_players(self) -> None:
for node in self.session.nodes.values():
if not NodeUtils.is_mobility(node):
if not nutils.is_mobility(node):
continue
if node.mobility_config:
mobility_player = MobilityPlayer(self.app, node)
@ -557,26 +524,14 @@ class CoreClient:
def set_metadata(self) -> None:
# create canvas data
wallpaper_path = None
if self.app.canvas.wallpaper_file:
wallpaper = Path(self.app.canvas.wallpaper_file)
if BACKGROUNDS_PATH == wallpaper.parent:
wallpaper_path = wallpaper.name
else:
wallpaper_path = str(wallpaper)
canvas_config = {
"wallpaper": wallpaper_path,
"wallpaper-style": self.app.canvas.scale_option.get(),
"gridlines": self.app.canvas.show_grid.get(),
"fit_image": self.app.canvas.adjust_to_dim.get(),
"dimensions": self.app.canvas.current_dimensions,
}
canvas_config = self.app.manager.get_metadata()
canvas_config = json.dumps(canvas_config)
# create shapes data
shapes = []
for shape in self.app.canvas.shapes.values():
shapes.append(shape.metadata())
for canvas in self.app.manager.all():
for shape in canvas.shapes.values():
shapes.append(shape.metadata())
shapes = json.dumps(shapes)
# create edges config
@ -588,8 +543,14 @@ class CoreClient:
edges_config.append(edge_config)
edges_config = json.dumps(edges_config)
# create hidden metadata
hidden = [x.core_node.id for x in self.canvas_nodes.values() if x.hidden]
hidden = json.dumps(hidden)
# save metadata
metadata = dict(canvas=canvas_config, shapes=shapes, edges=edges_config)
metadata = dict(
canvas=canvas_config, shapes=shapes, edges=edges_config, hidden=hidden
)
response = self.client.set_session_metadata(self.session.id, metadata)
logging.debug("set session metadata %s, result: %s", metadata, response)
@ -750,9 +711,11 @@ class CoreClient:
self.session.id,
config_proto.node_id,
config_proto.service,
startup=config_proto.startup,
validate=config_proto.validate,
shutdown=config_proto.shutdown,
config_proto.files,
config_proto.directories,
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(
@ -816,7 +779,7 @@ class CoreClient:
node_id = self.next_node_id()
position = Position(x=x, y=y)
image = None
if NodeUtils.is_image_node(node_type):
if nutils.has_image(node_type):
image = "ubuntu:latest"
emane = None
if node_type == NodeType.EMANE:
@ -841,9 +804,9 @@ class CoreClient:
image=image,
emane=emane,
)
if NodeUtils.is_custom(node_type, model):
services = NodeUtils.get_custom_node_services(self.app.guiconfig, model)
node.services[:] = services
if nutils.is_custom(node):
services = nutils.get_custom_services(self.app.guiconfig, model)
node.services = set(services)
# assign default services to CORE node
else:
services = self.session.default_services.get(model)
@ -876,60 +839,14 @@ class CoreClient:
links.append(edge.link)
self.ifaces_manager.removed(links)
def create_iface(self, canvas_node: CanvasNode) -> Interface:
node = canvas_node.core_node
ip4, ip6 = self.ifaces_manager.get_ips(node)
ip4_mask = self.ifaces_manager.ip4_mask
ip6_mask = self.ifaces_manager.ip6_mask
iface_id = canvas_node.next_iface_id()
name = f"eth{iface_id}"
iface = Interface(
id=iface_id,
name=name,
ip4=ip4,
ip4_mask=ip4_mask,
ip6=ip6,
ip6_mask=ip6_mask,
)
logging.info("create node(%s) interface(%s)", node.name, iface)
return iface
def create_link(
self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode
) -> Link:
"""
Create core link for a pair of canvas nodes, with token referencing
the canvas edge.
"""
src_node = canvas_src_node.core_node
dst_node = canvas_dst_node.core_node
self.ifaces_manager.determine_subnets(canvas_src_node, canvas_dst_node)
src_iface = None
if NodeUtils.is_container_node(src_node.type):
src_iface = self.create_iface(canvas_src_node)
dst_iface = None
if NodeUtils.is_container_node(dst_node.type):
dst_iface = self.create_iface(canvas_dst_node)
link = Link(
type=LinkType.WIRED,
node1_id=src_node.id,
node2_id=dst_node.id,
iface1=src_iface,
iface2=dst_iface,
)
logging.info("added link between %s and %s", src_node.name, dst_node.name)
return link
def save_edge(
self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode
) -> None:
def save_edge(self, edge: CanvasEdge) -> None:
self.links[edge.token] = edge
src_node = canvas_src_node.core_node
dst_node = canvas_dst_node.core_node
if NodeUtils.is_container_node(src_node.type):
src_node = edge.src.core_node
dst_node = edge.dst.core_node
if nutils.is_container(src_node):
src_iface_id = edge.link.iface1.id
self.iface_to_edge[(src_node.id, src_iface_id)] = edge
if NodeUtils.is_container_node(dst_node.type):
if nutils.is_container(dst_node):
dst_iface_id = edge.link.iface2.id
self.iface_to_edge[(dst_node.id, dst_iface_id)] = edge
@ -948,7 +865,7 @@ class CoreClient:
def get_mobility_configs_proto(self) -> List[mobility_pb2.MobilityConfig]:
configs = []
for node in self.session.nodes.values():
if not NodeUtils.is_mobility(node):
if not nutils.is_mobility(node):
continue
if not node.mobility_config:
continue
@ -976,7 +893,7 @@ class CoreClient:
def get_service_configs_proto(self) -> List[services_pb2.ServiceConfig]:
configs = []
for node in self.session.nodes.values():
if not NodeUtils.is_container_node(node.type):
if not nutils.is_container(node):
continue
if not node.service_configs:
continue
@ -996,7 +913,7 @@ class CoreClient:
def get_service_file_configs_proto(self) -> List[services_pb2.ServiceFileConfig]:
configs = []
for node in self.session.nodes.values():
if not NodeUtils.is_container_node(node.type):
if not nutils.is_container(node):
continue
if not node.service_file_configs:
continue
@ -1013,7 +930,7 @@ class CoreClient:
) -> List[configservices_pb2.ConfigServiceConfig]:
config_service_protos = []
for node in self.session.nodes.values():
if not NodeUtils.is_container_node(node.type):
if not nutils.is_container(node):
continue
if not node.config_service_configs:
continue

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
from core.gui import validation
from core.gui.dialogs.dialog import Dialog
from core.gui.graph.graph import CanvasGraph
from core.gui.graph.manager import CanvasManager
from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING:
@ -22,9 +22,9 @@ class SizeAndScaleDialog(Dialog):
create an instance for size and scale object
"""
super().__init__(app, "Canvas Size and Scale")
self.canvas: CanvasGraph = self.app.canvas
self.manager: CanvasManager = self.app.manager
self.section_font: font.Font = font.Font(weight=font.BOLD)
width, height = self.canvas.current_dimensions
width, height = self.manager.current_dimensions
self.pixel_width: tk.IntVar = tk.IntVar(value=width)
self.pixel_height: tk.IntVar = tk.IntVar(value=height)
location = self.app.core.session.location
@ -189,9 +189,7 @@ class SizeAndScaleDialog(Dialog):
def click_apply(self) -> None:
width, height = self.pixel_width.get(), self.pixel_height.get()
self.canvas.redraw_canvas((width, height))
if self.canvas.wallpaper:
self.canvas.redraw_wallpaper()
self.manager.redraw_canvases((width, height))
location = self.app.core.session.location
location.x = self.x.get()
location.y = self.y.get()

View file

@ -6,10 +6,10 @@ import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING, List, Optional
from core.gui import images
from core.gui.appconfig import BACKGROUNDS_PATH
from core.gui.dialogs.dialog import Dialog
from core.gui.graph.graph import CanvasGraph
from core.gui.images import Images
from core.gui.themes import PADX, PADY
from core.gui.widgets import image_chooser
@ -23,7 +23,7 @@ class CanvasWallpaperDialog(Dialog):
create an instance of CanvasWallpaper object
"""
super().__init__(app, "Canvas Background")
self.canvas: CanvasGraph = self.app.canvas
self.canvas: CanvasGraph = self.app.manager.current()
self.scale_option: tk.IntVar = tk.IntVar(value=self.canvas.scale_option.get())
self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(
value=self.canvas.adjust_to_dim.get()
@ -132,7 +132,7 @@ class CanvasWallpaperDialog(Dialog):
self.draw_preview()
def draw_preview(self) -> None:
image = Images.create(self.filename.get(), 250, 135)
image = images.from_file(self.filename.get(), width=250, height=135)
self.image_label.config(image=image)
self.image_label.image = image
@ -161,7 +161,6 @@ class CanvasWallpaperDialog(Dialog):
def click_apply(self) -> None:
self.canvas.scale_option.set(self.scale_option.get())
self.canvas.adjust_to_dim.set(self.adjust_to_dim.get())
self.canvas.show_grid.click_handler()
filename = self.filename.get()
if not filename:
filename = None

View file

@ -6,10 +6,9 @@ from typing import TYPE_CHECKING, Optional, Set
from PIL.ImageTk import PhotoImage
from core.gui import nodeutils
from core.gui import images
from core.gui.appconfig import ICONS_PATH, CustomNode
from core.gui.dialogs.dialog import Dialog
from core.gui.images import Images
from core.gui.nodeutils import NodeDraw
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CheckboxList, ListboxScroll, image_chooser
@ -190,7 +189,7 @@ class CustomNodesDialog(Dialog):
def click_icon(self) -> None:
file_path = image_chooser(self, ICONS_PATH)
if file_path:
image = Images.create(file_path, nodeutils.ICON_SIZE)
image = images.from_file(file_path, width=images.NODE_SIZE)
self.image = image
self.image_file = file_path
self.image_button.config(image=self.image)
@ -217,7 +216,7 @@ class CustomNodesDialog(Dialog):
def click_create(self) -> None:
name = self.name.get()
if name not in self.app.core.custom_nodes:
image_file = Path(self.image_file).stem
image_file = str(Path(self.image_file).absolute())
custom_node = CustomNode(name, image_file, list(self.services))
node_draw = NodeDraw.from_custom(custom_node)
logging.info(
@ -237,7 +236,7 @@ class CustomNodesDialog(Dialog):
self.selected = name
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_file = str(Path(self.image_file).absolute())
node_draw.image = self.image
node_draw.services = self.services
logging.debug(

View file

@ -2,7 +2,8 @@ import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING
from core.gui.images import ImageEnum, Images
from core.gui import images
from core.gui.images import ImageEnum
from core.gui.themes import DIALOG_PAD
if TYPE_CHECKING:
@ -25,7 +26,7 @@ class Dialog(tk.Toplevel):
self.modal: bool = modal
self.title(title)
self.protocol("WM_DELETE_WINDOW", self.destroy)
image = Images.get(ImageEnum.CORE, 16)
image = images.from_enum(ImageEnum.CORE, width=images.DIALOG_SIZE)
self.tk.call("wm", "iconphoto", self._w, image)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)

View file

@ -9,8 +9,9 @@ from typing import TYPE_CHECKING, Dict, List, Optional
import grpc
from core.api.grpc.wrappers import ConfigOption, Node
from core.gui import images
from core.gui.dialogs.dialog import Dialog
from core.gui.images import ImageEnum, Images
from core.gui.images import ImageEnum
from core.gui.themes import PADX, PADY
from core.gui.widgets import ConfigFrame
@ -143,7 +144,7 @@ class EmaneConfigDialog(Dialog):
)
label.grid(pady=PADY)
image = Images.get(ImageEnum.EDITNODE, 16)
image = images.from_enum(ImageEnum.EDITNODE, width=images.BUTTON_SIZE)
button = ttk.Button(
self.top,
image=image,
@ -181,7 +182,7 @@ class EmaneConfigDialog(Dialog):
for i in range(2):
frame.columnconfigure(i, weight=1)
image = Images.get(ImageEnum.EDITNODE, 16)
image = images.from_enum(ImageEnum.EDITNODE, width=images.BUTTON_SIZE)
self.emane_model_button = ttk.Button(
frame,
text=f"{self.emane_model.get()} options",
@ -192,7 +193,7 @@ class EmaneConfigDialog(Dialog):
self.emane_model_button.image = image
self.emane_model_button.grid(row=0, column=0, padx=PADX, sticky=tk.EW)
image = Images.get(ImageEnum.EDITNODE, 16)
image = images.from_enum(ImageEnum.EDITNODE, width=images.BUTTON_SIZE)
button = ttk.Button(
frame,
text="EMANE options",

View file

@ -2,8 +2,9 @@ import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING, Optional
from core.gui import images
from core.gui.dialogs.dialog import Dialog
from core.gui.images import ImageEnum, Images
from core.gui.images import ImageEnum
from core.gui.themes import PADY
from core.gui.widgets import CodeText
@ -12,9 +13,11 @@ if TYPE_CHECKING:
class ErrorDialog(Dialog):
def __init__(self, app: "Application", title: str, details: str) -> None:
super().__init__(app, "CORE Exception")
self.title: str = title
def __init__(
self, app: "Application", title: str, message: str, details: str
) -> None:
super().__init__(app, title)
self.message: str = message
self.details: str = details
self.error_message: Optional[CodeText] = None
self.draw()
@ -22,15 +25,15 @@ class ErrorDialog(Dialog):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(1, weight=1)
image = Images.get(ImageEnum.ERROR, 24)
image = images.from_enum(ImageEnum.ERROR, width=images.ERROR_SIZE)
label = ttk.Label(
self.top, text=self.title, image=image, compound=tk.LEFT, anchor=tk.CENTER
self.top, text=self.message, image=image, compound=tk.LEFT, anchor=tk.CENTER
)
label.image = image
label.grid(sticky=tk.EW, pady=PADY)
label.grid(sticky=tk.W, pady=PADY)
self.error_message = CodeText(self.top)
self.error_message.text.insert("1.0", self.details)
self.error_message.text.config(state=tk.DISABLED)
self.error_message.grid(sticky=tk.NSEW, pady=PADY)
self.error_message.grid(sticky=tk.EW, pady=PADY)
button = ttk.Button(self.top, text="Close", command=lambda: self.destroy())
button.grid(sticky=tk.EW)

View file

@ -105,9 +105,13 @@ class FindDialog(Dialog):
self.tree.selection_set(results[0])
def close_dialog(self) -> None:
self.app.canvas.delete("find")
self.clear_find()
self.destroy()
def clear_find(self):
for canvas in self.app.manager.all():
canvas.delete("find")
def click_select(self, _event: tk.Event = None) -> None:
"""
find the node that matches search criteria, circle around that node
@ -116,13 +120,13 @@ class FindDialog(Dialog):
"""
item = self.tree.selection()
if item:
self.app.canvas.delete("find")
self.clear_find()
node_id = int(self.tree.item(item, "text"))
canvas_node = self.app.core.get_canvas_node(node_id)
x0, y0, x1, y1 = self.app.canvas.bbox(canvas_node.id)
self.app.manager.select(canvas_node.canvas.id)
x0, y0, x1, y1 = canvas_node.canvas.bbox(canvas_node.id)
dist = 5 * self.app.guiconfig.scale
self.app.canvas.create_oval(
canvas_node.canvas.create_oval(
x0 - dist,
y0 - dist,
x1 + dist,
@ -132,9 +136,9 @@ class FindDialog(Dialog):
width=3.0 * self.app.guiconfig.scale,
)
_x, _y, _, _ = self.app.canvas.bbox(canvas_node.id)
oid = self.app.canvas.find_withtag("rectangle")
x0, y0, x1, y1 = self.app.canvas.bbox(oid[0])
_x, _y, _, _ = canvas_node.canvas.bbox(canvas_node.id)
oid = canvas_node.canvas.find_withtag("rectangle")
x0, y0, x1, y1 = canvas_node.canvas.bbox(oid[0])
logging.debug("Dist to most left: %s", abs(x0 - _x))
logging.debug("White canvas width: %s", abs(x0 - x1))
@ -150,5 +154,5 @@ class FindDialog(Dialog):
xscroll_fraction = xscroll_fraction - 0.05
if yscroll_fraction > 0.05:
yscroll_fraction = yscroll_fraction - 0.05
self.app.canvas.xview_moveto(xscroll_fraction)
self.app.canvas.yview_moveto(yscroll_fraction)
canvas_node.canvas.xview_moveto(xscroll_fraction)
canvas_node.canvas.yview_moveto(yscroll_fraction)

View file

@ -70,10 +70,10 @@ class LinkConfigurationDialog(Dialog):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
src_label = self.app.canvas.nodes[self.edge.src].core_node.name
src_label = self.edge.src.core_node.name
if self.edge.link.iface1:
src_label += f":{self.edge.link.iface1.name}"
dst_label = self.app.canvas.nodes[self.edge.dst].core_node.name
dst_label = self.edge.dst.core_node.name
if self.edge.link.iface2:
dst_label += f":{self.edge.link.iface2.name}"
label = ttk.Label(
@ -293,7 +293,7 @@ class LinkConfigurationDialog(Dialog):
# update edge label
self.edge.redraw()
self.edge.check_options()
self.edge.check_visibility()
self.destroy()
def change_symmetry(self) -> None:
@ -316,10 +316,8 @@ class LinkConfigurationDialog(Dialog):
"""
populate link config to the table
"""
width = self.app.canvas.itemcget(self.edge.id, "width")
self.width.set(width)
color = self.app.canvas.itemcget(self.edge.id, "fill")
self.color.set(color)
self.width.set(self.edge.width)
self.color.set(self.edge.color)
link = self.edge.link
if link.options:
self.bandwidth.set(str(link.options.bandwidth))

View file

@ -84,17 +84,17 @@ class MobilityPlayerDialog(Dialog):
for i in range(3):
frame.columnconfigure(i, weight=1)
image = self.app.get_icon(ImageEnum.START, ICON_SIZE)
image = self.app.get_enum_icon(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=tk.EW, padx=PADX)
image = self.app.get_icon(ImageEnum.PAUSE, ICON_SIZE)
image = self.app.get_enum_icon(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=tk.EW, padx=PADX)
image = self.app.get_icon(ImageEnum.STOP, ICON_SIZE)
image = self.app.get_enum_icon(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=tk.EW, padx=PADX)

View file

@ -8,12 +8,12 @@ import netaddr
from PIL.ImageTk import PhotoImage
from core.api.grpc.wrappers import Interface, Node
from core.gui import nodeutils, validation
from core.gui import images
from core.gui import nodeutils as nutils
from core.gui import validation
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 ListboxScroll, image_chooser
@ -225,27 +225,27 @@ class NodeConfigDialog(Dialog):
row += 1
# node type field
if NodeUtils.is_model_node(self.node.type):
if nutils.is_model(self.node):
label = ttk.Label(frame, text="Type")
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
combobox = ttk.Combobox(
frame,
textvariable=self.type,
values=list(NodeUtils.NODE_MODELS),
values=list(nutils.NODE_MODELS),
state=combo_state,
)
combobox.grid(row=row, column=1, sticky=tk.EW)
row += 1
# container image field
if NodeUtils.is_image_node(self.node.type):
if nutils.has_image(self.node.type):
label = ttk.Label(frame, text="Image")
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
entry = ttk.Entry(frame, textvariable=self.container_image, state=state)
entry.grid(row=row, column=1, sticky=tk.EW)
row += 1
if NodeUtils.is_container_node(self.node.type):
if nutils.is_container(self.node):
# server
frame.grid(sticky=tk.EW)
frame.columnconfigure(1, weight=1)
@ -259,7 +259,7 @@ class NodeConfigDialog(Dialog):
combobox.grid(row=row, column=1, sticky=tk.EW)
row += 1
if NodeUtils.is_rj45_node(self.node.type):
if nutils.is_rj45(self.node):
response = self.app.core.client.get_ifaces()
logging.debug("host machine available interfaces: %s", response)
ifaces = ListboxScroll(frame)
@ -371,7 +371,7 @@ class NodeConfigDialog(Dialog):
def click_icon(self) -> None:
file_path = image_chooser(self, ICONS_PATH)
if file_path:
self.image = Images.create(file_path, nodeutils.ICON_SIZE)
self.image = images.from_file(file_path, width=images.NODE_SIZE)
self.image_button.config(image=self.image)
self.image_file = file_path
@ -380,10 +380,10 @@ class NodeConfigDialog(Dialog):
# update core node
self.node.name = self.name.get()
if NodeUtils.is_image_node(self.node.type):
if nutils.has_image(self.node.type):
self.node.image = self.container_image.get()
server = self.server.get()
if NodeUtils.is_container_node(self.node.type):
if nutils.is_container(self.node):
if server == DEFAULT_SERVER:
self.node.server = None
else:

View file

@ -134,7 +134,8 @@ class PreferencesDialog(Dialog):
# scale toolbar and canvas items
self.app.toolbar.scale()
self.app.canvas.scale_graph()
for canvas in self.app.manager.all():
canvas.scale_graph()
def adjust_scale(self, arg1: str, arg2: str, arg3: str) -> None:
scale_value = self.gui_scale.get()

View file

@ -2,8 +2,8 @@ import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING, Dict, Optional
from core.gui import nodeutils as nutils
from core.gui.dialogs.dialog import Dialog
from core.gui.nodeutils import NodeUtils
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CodeText, ListboxScroll
@ -26,7 +26,7 @@ class RunToolDialog(Dialog):
store all CORE nodes (nodes that execute commands) from all existing nodes
"""
for node in self.app.core.session.nodes.values():
if NodeUtils.is_container_node(node.type):
if nutils.is_container(node):
self.executable_nodes[node.name] = node.id
def draw(self) -> None:

View file

@ -8,9 +8,10 @@ import grpc
from PIL.ImageTk import PhotoImage
from core.api.grpc.wrappers import Node, NodeServiceData, ServiceValidationMode
from core.gui import images
from core.gui.dialogs.copyserviceconfig import CopyServiceConfigDialog
from core.gui.dialogs.dialog import Dialog
from core.gui.images import ImageEnum, Images
from core.gui.images import ImageEnum
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CodeText, ListboxScroll
@ -47,11 +48,11 @@ class ServiceConfigDialog(Dialog):
self.directory_entry: Optional[ttk.Entry] = None
self.default_directories: List[str] = []
self.temp_directories: List[str] = []
self.documentnew_img: PhotoImage = self.app.get_icon(
ImageEnum.DOCUMENTNEW, ICON_SIZE
self.documentnew_img: PhotoImage = self.app.get_enum_icon(
ImageEnum.DOCUMENTNEW, width=ICON_SIZE
)
self.editdelete_img: PhotoImage = self.app.get_icon(
ImageEnum.EDITDELETE, ICON_SIZE
self.editdelete_img: PhotoImage = self.app.get_enum_icon(
ImageEnum.EDITDELETE, width=ICON_SIZE
)
self.notebook: Optional[ttk.Notebook] = None
self.metadata_entry: Optional[ttk.Entry] = None
@ -179,7 +180,7 @@ class ServiceConfigDialog(Dialog):
button.grid(row=0, column=0, sticky=tk.W, padx=PADX)
entry = ttk.Entry(frame, state=tk.DISABLED)
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
image = Images.get(ImageEnum.FILEOPEN, 16)
image = images.from_enum(ImageEnum.FILEOPEN, width=images.BUTTON_SIZE)
button = ttk.Button(frame, image=image)
button.image = image
button.grid(row=0, column=2)
@ -194,11 +195,11 @@ class ServiceConfigDialog(Dialog):
value=2,
)
button.grid(row=0, column=0, sticky=tk.EW)
image = Images.get(ImageEnum.FILEOPEN, 16)
image = images.from_enum(ImageEnum.FILEOPEN, width=images.BUTTON_SIZE)
button = ttk.Button(frame, image=image)
button.image = image
button.grid(row=0, column=1)
image = Images.get(ImageEnum.DOCUMENTSAVE, 16)
image = images.from_enum(ImageEnum.DOCUMENTSAVE, width=images.BUTTON_SIZE)
button = ttk.Button(frame, image=image)
button.image = image
button.grid(row=0, column=2)

View file

@ -6,8 +6,9 @@ from typing import TYPE_CHECKING, List, Optional
import grpc
from core.api.grpc.wrappers import SessionState, SessionSummary
from core.gui import images
from core.gui.dialogs.dialog import Dialog
from core.gui.images import ImageEnum, Images
from core.gui.images import ImageEnum
from core.gui.task import ProgressTask
from core.gui.themes import PADX, PADY
@ -22,7 +23,6 @@ class SessionsDialog(Dialog):
self.selected_session: Optional[int] = None
self.selected_id: Optional[int] = None
self.tree: Optional[ttk.Treeview] = None
self.sessions: List[SessionSummary] = self.get_sessions()
self.connect_button: Optional[ttk.Button] = None
self.delete_button: Optional[ttk.Button] = None
self.protocol("WM_DELETE_WINDOW", self.on_closing)
@ -32,7 +32,8 @@ class SessionsDialog(Dialog):
try:
response = self.app.core.client.get_sessions()
logging.info("sessions: %s", response)
return [SessionSummary.from_proto(x) for x in response.sessions]
sessions = sorted(response.sessions, key=lambda x: x.id)
return [SessionSummary.from_proto(x) for x in sessions]
except grpc.RpcError as e:
self.app.show_grpc_exception("Get Sessions Error", e)
self.destroy()
@ -79,15 +80,7 @@ class SessionsDialog(Dialog):
self.tree.heading("state", text="State")
self.tree.column("nodes", stretch=tk.YES, anchor="center")
self.tree.heading("nodes", text="Node Count")
for index, session in enumerate(self.sessions):
state_name = SessionState(session.state).name
self.tree.insert(
"",
tk.END,
text=str(session.id),
values=(session.id, state_name, session.nodes),
)
self.draw_sessions()
self.tree.bind("<Double-1>", self.double_click_join)
self.tree.bind("<<TreeviewSelect>>", self.click_select)
@ -99,20 +92,31 @@ class SessionsDialog(Dialog):
xscrollbar.grid(row=1, sticky=tk.EW)
self.tree.configure(xscrollcommand=xscrollbar.set)
def draw_sessions(self) -> None:
self.tree.delete(*self.tree.get_children())
for index, session in enumerate(self.get_sessions()):
state_name = SessionState(session.state).name
self.tree.insert(
"",
tk.END,
text=str(session.id),
values=(session.id, state_name, session.nodes),
)
def draw_buttons(self) -> None:
frame = ttk.Frame(self.top)
for i in range(4):
frame.columnconfigure(i, weight=1)
frame.grid(sticky=tk.EW)
image = Images.get(ImageEnum.DOCUMENTNEW, 16)
image = images.from_enum(ImageEnum.DOCUMENTNEW, width=images.BUTTON_SIZE)
b = ttk.Button(
frame, image=image, text="New", compound=tk.LEFT, command=self.click_new
)
b.image = image
b.grid(row=0, padx=PADX, sticky=tk.EW)
image = Images.get(ImageEnum.FILEOPEN, 16)
image = images.from_enum(ImageEnum.FILEOPEN, width=images.BUTTON_SIZE)
self.connect_button = ttk.Button(
frame,
image=image,
@ -124,7 +128,7 @@ class SessionsDialog(Dialog):
self.connect_button.image = image
self.connect_button.grid(row=0, column=1, padx=PADX, sticky=tk.EW)
image = Images.get(ImageEnum.DELETE, 16)
image = images.from_enum(ImageEnum.DELETE, width=images.BUTTON_SIZE)
self.delete_button = ttk.Button(
frame,
image=image,
@ -136,7 +140,7 @@ class SessionsDialog(Dialog):
self.delete_button.image = image
self.delete_button.grid(row=0, column=2, padx=PADX, sticky=tk.EW)
image = Images.get(ImageEnum.CANCEL, 16)
image = images.from_enum(ImageEnum.CANCEL, width=images.BUTTON_SIZE)
if self.is_start_app:
b = ttk.Button(
frame,
@ -196,12 +200,21 @@ class SessionsDialog(Dialog):
def click_delete(self) -> None:
if not self.selected_session:
return
logging.debug("delete session: %s", self.selected_session)
logging.info("click delete session: %s", self.selected_session)
self.tree.delete(self.selected_id)
self.app.core.delete_session(self.selected_session)
if self.selected_session == self.app.core.session.id:
self.click_new()
self.destroy()
session_id = None
if self.app.core.session:
session_id = self.app.core.session.id
if self.selected_session == session_id:
self.app.core.session = None
sessions = self.get_sessions()
if not sessions:
self.app.core.create_new_session()
self.draw_sessions()
else:
session_id = sessions[0].id
self.app.core.join_session(session_id)
self.click_select()
def click_exit(self) -> None:

View file

@ -27,7 +27,7 @@ class ShapeDialog(Dialog):
else:
title = "Add Text"
super().__init__(app, title)
self.canvas: "CanvasGraph" = app.canvas
self.canvas: "CanvasGraph" = app.manager.current()
self.fill: Optional[ttk.Label] = None
self.border: Optional[ttk.Label] = None
self.shape: "Shape" = shape

View file

@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Optional
from core.gui.dialogs.colorpicker import ColorPickerDialog
from core.gui.dialogs.dialog import Dialog
from core.gui.graph.graph import CanvasGraph
from core.gui.graph.manager import CanvasManager
from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING:
@ -17,16 +17,16 @@ if TYPE_CHECKING:
class ThroughputDialog(Dialog):
def __init__(self, app: "Application") -> None:
super().__init__(app, "Throughput Config")
self.canvas: CanvasGraph = app.canvas
self.manager: CanvasManager = app.manager
self.show_throughput: tk.IntVar = tk.IntVar(value=1)
self.exponential_weight: tk.IntVar = tk.IntVar(value=1)
self.transmission: tk.IntVar = tk.IntVar(value=1)
self.reception: tk.IntVar = tk.IntVar(value=1)
self.threshold: tk.DoubleVar = tk.DoubleVar(
value=self.canvas.throughput_threshold
value=self.manager.throughput_threshold
)
self.width: tk.IntVar = tk.IntVar(value=self.canvas.throughput_width)
self.color: str = self.canvas.throughput_color
self.width: tk.IntVar = tk.IntVar(value=self.manager.throughput_width)
self.color: str = self.manager.throughput_color
self.color_button: Optional[tk.Button] = None
self.top.columnconfigure(0, weight=1)
self.draw()
@ -106,7 +106,7 @@ class ThroughputDialog(Dialog):
self.color_button.config(bg=self.color, text=self.color, bd=0)
def click_save(self) -> None:
self.canvas.throughput_threshold = self.threshold.get()
self.canvas.throughput_width = self.width.get()
self.canvas.throughput_color = self.color
self.manager.throughput_threshold = self.threshold.get()
self.manager.throughput_width = self.width.get()
self.manager.throughput_color = self.color
self.destroy()

View file

@ -21,7 +21,7 @@ RANGE_WIDTH: int = 3
class WlanConfigDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
super().__init__(app, f"{canvas_node.core_node.name} WLAN Configuration")
self.canvas: "CanvasGraph" = app.canvas
self.canvas: "CanvasGraph" = app.manager.current()
self.canvas_node: "CanvasNode" = canvas_node
self.node: Node = canvas_node.core_node
self.config_frame: Optional[ConfigFrame] = None

View file

@ -79,15 +79,13 @@ class WirelessEdgeInfoFrame(InfoFrameBase):
def draw(self) -> None:
link = self.edge.link
src_canvas_node = self.app.canvas.nodes[self.edge.src]
src_node = src_canvas_node.core_node
dst_canvas_node = self.app.canvas.nodes[self.edge.dst]
dst_node = dst_canvas_node.core_node
src_node = self.edge.src.core_node
dst_node = self.edge.dst.core_node
# find interface for each node connected to network
net_id = link.network_id
iface1 = get_iface(src_canvas_node, net_id)
iface2 = get_iface(dst_canvas_node, net_id)
iface1 = get_iface(self.edge.src, net_id)
iface2 = get_iface(self.edge.dst, net_id)
frame = DetailsFrame(self)
frame.grid(sticky=tk.EW)

View file

@ -2,8 +2,8 @@ import tkinter as tk
from typing import TYPE_CHECKING
from core.api.grpc.wrappers import NodeType
from core.gui import nodeutils as nutils
from core.gui.frames.base import DetailsFrame, InfoFrameBase
from core.gui.nodeutils import NodeUtils
if TYPE_CHECKING:
from core.gui.app import Application
@ -20,21 +20,21 @@ class NodeInfoFrame(InfoFrameBase):
node = self.canvas_node.core_node
frame = DetailsFrame(self)
frame.grid(sticky=tk.EW)
frame.add_detail("ID", node.id)
frame.add_detail("ID", str(node.id))
frame.add_detail("Name", node.name)
if NodeUtils.is_model_node(node.type):
if nutils.is_model(node):
frame.add_detail("Type", node.model)
if NodeUtils.is_container_node(node.type):
if nutils.is_container(node):
for index, service in enumerate(sorted(node.services)):
if index == 0:
frame.add_detail("Services", service)
else:
frame.add_detail("", service)
if node.type == NodeType.EMANE:
emane = node.emane.split("_")[1:]
emane = "".join(node.emane.split("_")[1:])
frame.add_detail("EMANE", emane)
if NodeUtils.is_image_node(node.type):
if nutils.has_image(node.type):
frame.add_detail("Image", node.image)
if NodeUtils.is_container_node(node.type):
if nutils.is_container(node):
server = node.server if node.server else "localhost"
frame.add_detail("Server", server)

View file

@ -1,18 +1,21 @@
import functools
import logging
import math
import tkinter as tk
from typing import TYPE_CHECKING, Optional, Tuple
from typing import TYPE_CHECKING, Optional, Tuple, Union
from core.api.grpc.wrappers import Interface, Link
from core.gui import themes
from core.gui.dialogs.linkconfig import LinkConfigurationDialog
from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame
from core.gui.graph import tags
from core.gui.nodeutils import NodeUtils
from core.gui.utils import bandwidth_text, delay_jitter_text
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.graph import CanvasGraph
from core.gui.graph.manager import CanvasManager
from core.gui.graph.node import CanvasNode, ShadowNode
TEXT_DISTANCE: int = 60
EDGE_WIDTH: int = 3
@ -24,13 +27,40 @@ ARC_DISTANCE: int = 50
def create_wireless_token(src: int, dst: int, network: int) -> str:
return f"{src}-{dst}-{network}"
if src < dst:
node1, node2 = src, dst
else:
node1, node2 = dst, src
return f"{node1}-{node2}-{network}"
def create_edge_token(link: Link) -> str:
iface1_id = link.iface1.id if link.iface1 else 0
iface2_id = link.iface2.id if link.iface2 else 0
return f"{link.node1_id}-{iface1_id}-{link.node2_id}-{iface2_id}"
if link.node1_id < link.node2_id:
node1 = link.node1_id
node1_iface = iface1_id
node2 = link.node2_id
node2_iface = iface2_id
else:
node1 = link.node2_id
node1_iface = iface2_id
node2 = link.node1_id
node2_iface = iface1_id
return f"{node1}-{node1_iface}-{node2}-{node2_iface}"
def node_label_positions(
src_x: int, src_y: int, dst_x: int, dst_y: int
) -> Tuple[Tuple[float, float], Tuple[float, float]]:
v_x, v_y = dst_x - src_x, dst_y - src_y
v_len = math.sqrt(v_x ** 2 + v_y ** 2)
if v_len == 0:
u_x, u_y = 0.0, 0.0
else:
u_x, u_y = v_x / v_len, v_y / v_len
offset_x, offset_y = TEXT_DISTANCE * u_x, TEXT_DISTANCE * u_y
return (src_x + offset_x, src_y + offset_y), (dst_x - offset_x, dst_y - offset_y)
def arc_edges(edges) -> None:
@ -65,21 +95,35 @@ def arc_edges(edges) -> None:
class Edge:
tag: str = tags.EDGE
def __init__(self, canvas: "CanvasGraph", src: int, dst: int = None) -> None:
self.canvas = canvas
def __init__(
self, app: "Application", src: "CanvasNode", dst: "CanvasNode" = None
) -> None:
self.app: "Application" = app
self.manager: CanvasManager = app.manager
self.id: Optional[int] = None
self.src: int = src
self.dst: int = dst
self.id2: Optional[int] = None
self.src: "CanvasNode" = src
self.src_shadow: Optional[ShadowNode] = None
self.dst: Optional["CanvasNode"] = dst
self.dst_shadow: Optional[ShadowNode] = None
self.link: Optional[Link] = None
self.arc: int = 0
self.token: Optional[str] = None
self.src_label: Optional[int] = None
self.src_label2: Optional[int] = None
self.middle_label: Optional[int] = None
self.middle_label2: Optional[int] = None
self.dst_label: Optional[int] = None
self.dst_label2: Optional[int] = None
self.color: str = EDGE_COLOR
self.width: int = EDGE_WIDTH
self.linked_wireless: bool = False
self.hidden: bool = False
if self.dst:
self.linked_wireless = self.src.is_wireless() or self.dst.is_wireless()
def scaled_width(self) -> float:
return self.width * self.canvas.app.app_scale
return self.width * self.app.app_scale
def _get_arcpoint(
self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]
@ -110,11 +154,53 @@ class Edge:
arc_y = (perp_m * arc_x) + b
return arc_x, arc_y
def draw(
self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float], state: str
) -> None:
def arc_common_edges(self) -> None:
common_edges = list(self.src.edges & self.dst.edges)
common_edges += list(self.src.wireless_edges & self.dst.wireless_edges)
arc_edges(common_edges)
def has_shadows(self) -> bool:
# still drawing
if not self.dst:
return False
return self.src.canvas != self.dst.canvas
def draw(self, state: str) -> None:
if not self.has_shadows():
dst = self.dst if self.dst else self.src
self.id = self.draw_edge(self.src.canvas, self.src, dst, state)
elif self.linked_wireless:
if self.src.is_wireless():
self.src_shadow = self.dst.canvas.get_shadow(self.src)
self.id2 = self.draw_edge(
self.dst.canvas, self.src_shadow, self.dst, state
)
if self.dst.is_wireless():
self.dst_shadow = self.src.canvas.get_shadow(self.dst)
self.id = self.draw_edge(
self.src.canvas, self.src, self.dst_shadow, state
)
else:
# draw shadow nodes and 2 lines
self.src_shadow = self.dst.canvas.get_shadow(self.src)
self.dst_shadow = self.src.canvas.get_shadow(self.dst)
self.id = self.draw_edge(self.src.canvas, self.src, self.dst_shadow, state)
self.id2 = self.draw_edge(self.dst.canvas, self.src_shadow, self.dst, state)
self.src.canvas.organize()
if self.has_shadows():
self.dst.canvas.organize()
def draw_edge(
self,
canvas: "CanvasGraph",
src: Union["CanvasNode", "ShadowNode"],
dst: Union["CanvasNode", "ShadowNode"],
state: str,
) -> int:
src_pos = src.position()
dst_pos = dst.position()
arc_pos = self._get_arcpoint(src_pos, dst_pos)
self.id = self.canvas.create_line(
return canvas.create_line(
*src_pos,
*arc_pos,
*dst_pos,
@ -126,112 +212,268 @@ class Edge:
)
def redraw(self) -> None:
self.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color)
src_x, src_y, _, _, _, _ = self.canvas.coords(self.id)
src_pos = src_x, src_y
self.move_src(src_pos)
def middle_label_pos(self) -> Tuple[float, float]:
_, _, x, y, _, _ = self.canvas.coords(self.id)
return x, y
self.src.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color)
self.move_src()
if self.id2:
self.dst.canvas.itemconfig(
self.id2, width=self.scaled_width(), fill=self.color
)
self.move_dst()
def middle_label_text(self, text: str) -> None:
if self.middle_label is None:
x, y = self.middle_label_pos()
self.middle_label = self.canvas.create_text(
_, _, x, y, _, _ = self.src.canvas.coords(self.id)
self.middle_label = self.src.canvas.create_text(
x,
y,
font=self.canvas.app.edge_font,
font=self.app.edge_font,
text=text,
tags=tags.LINK_LABEL,
justify=tk.CENTER,
state=self.canvas.show_link_labels.state(),
state=self.manager.show_link_labels.state(),
)
if self.id2:
_, _, x, y, _, _ = self.dst.canvas.coords(self.id2)
self.middle_label2 = self.dst.canvas.create_text(
x,
y,
font=self.app.edge_font,
text=text,
tags=tags.LINK_LABEL,
justify=tk.CENTER,
state=self.manager.show_link_labels.state(),
)
else:
self.canvas.itemconfig(self.middle_label, text=text)
self.src.canvas.itemconfig(self.middle_label, text=text)
if self.middle_label2:
self.dst.canvas.itemconfig(self.middle_label2, text=text)
def clear_middle_label(self) -> None:
self.canvas.delete(self.middle_label)
self.src.canvas.delete(self.middle_label)
self.middle_label = None
def node_label_positions(self) -> Tuple[Tuple[float, float], Tuple[float, float]]:
src_x, src_y, _, _, dst_x, dst_y = self.canvas.coords(self.id)
v_x, v_y = dst_x - src_x, dst_y - src_y
v_len = math.sqrt(v_x ** 2 + v_y ** 2)
if v_len == 0:
u_x, u_y = 0.0, 0.0
else:
u_x, u_y = v_x / v_len, v_y / v_len
offset_x, offset_y = TEXT_DISTANCE * u_x, TEXT_DISTANCE * u_y
return (
(src_x + offset_x, src_y + offset_y),
(dst_x - offset_x, dst_y - offset_y),
)
if self.middle_label2:
self.dst.canvas.delete(self.middle_label2)
self.middle_label2 = None
def src_label_text(self, text: str) -> None:
if self.src_label is None:
src_pos, _ = self.node_label_positions()
self.src_label = self.canvas.create_text(
*src_pos,
text=text,
justify=tk.CENTER,
font=self.canvas.app.edge_font,
tags=tags.LINK_LABEL,
state=self.canvas.show_link_labels.state(),
)
if self.src_label is None and self.src_label2 is None:
if self.id:
src_x, src_y, _, _, dst_x, dst_y = self.src.canvas.coords(self.id)
src_pos, _ = node_label_positions(src_x, src_y, dst_x, dst_y)
self.src_label = self.src.canvas.create_text(
*src_pos,
text=text,
justify=tk.CENTER,
font=self.app.edge_font,
tags=tags.LINK_LABEL,
state=self.manager.show_link_labels.state(),
)
if self.id2:
src_x, src_y, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2)
src_pos, _ = node_label_positions(src_x, src_y, dst_x, dst_y)
self.src_label2 = self.dst.canvas.create_text(
*src_pos,
text=text,
justify=tk.CENTER,
font=self.app.edge_font,
tags=tags.LINK_LABEL,
state=self.manager.show_link_labels.state(),
)
else:
self.canvas.itemconfig(self.src_label, text=text)
if self.src_label:
self.src.canvas.itemconfig(self.src_label, text=text)
if self.src_label2:
self.dst.canvas.itemconfig(self.src_label2, text=text)
def dst_label_text(self, text: str) -> None:
if self.dst_label is None:
_, dst_pos = self.node_label_positions()
self.dst_label = self.canvas.create_text(
*dst_pos,
text=text,
justify=tk.CENTER,
font=self.canvas.app.edge_font,
tags=tags.LINK_LABEL,
state=self.canvas.show_link_labels.state(),
)
if self.dst_label is None and self.dst_label2 is None:
if self.id:
src_x, src_y, _, _, dst_x, dst_y = self.src.canvas.coords(self.id)
_, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y)
self.dst_label = self.src.canvas.create_text(
*dst_pos,
text=text,
justify=tk.CENTER,
font=self.app.edge_font,
tags=tags.LINK_LABEL,
state=self.manager.show_link_labels.state(),
)
if self.id2:
src_x, src_y, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2)
_, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y)
self.dst_label2 = self.dst.canvas.create_text(
*dst_pos,
text=text,
justify=tk.CENTER,
font=self.app.edge_font,
tags=tags.LINK_LABEL,
state=self.manager.show_link_labels.state(),
)
else:
self.canvas.itemconfig(self.dst_label, text=text)
if self.dst_label:
self.src.canvas.itemconfig(self.dst_label, text=text)
if self.dst_label2:
self.dst.canvas.itemconfig(self.dst_label2, text=text)
def move_node(self, node_id: int, pos: Tuple[float, float]) -> None:
if self.src == node_id:
self.move_src(pos)
else:
self.move_dst(pos)
def move_dst(self, dst_pos: Tuple[float, float]) -> None:
src_x, src_y, _, _, _, _ = self.canvas.coords(self.id)
def drawing(self, pos: Tuple[float, float]) -> None:
src_x, src_y, _, _, _, _ = self.src.canvas.coords(self.id)
src_pos = src_x, src_y
self.moved(src_pos, dst_pos)
self.moved(src_pos, pos)
def move_src(self, src_pos: Tuple[float, float]) -> None:
_, _, _, _, dst_x, dst_y = self.canvas.coords(self.id)
def move_node(self, node: "CanvasNode") -> None:
if self.src == node:
self.move_src()
else:
self.move_dst()
def move_shadow(self, node: "ShadowNode") -> None:
if self.src_shadow == node:
self.move_src_shadow()
elif self.dst_shadow == node:
self.move_dst_shadow()
def move_src_shadow(self) -> None:
if not self.id2:
return
_, _, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2)
dst_pos = dst_x, dst_y
self.moved(src_pos, dst_pos)
self.moved2(self.src_shadow.position(), dst_pos)
def move_dst_shadow(self) -> None:
if not self.id:
return
src_x, src_y, _, _, _, _ = self.src.canvas.coords(self.id)
src_pos = src_x, src_y
self.moved(src_pos, self.dst_shadow.position())
def move_dst(self) -> None:
if self.dst.is_wireless() and self.has_shadows():
return
dst_pos = self.dst.position()
if self.id2:
src_x, src_y, _, _, _, _ = self.dst.canvas.coords(self.id2)
src_pos = src_x, src_y
self.moved2(src_pos, dst_pos)
elif self.id:
src_x, src_y, _, _, _, _ = self.dst.canvas.coords(self.id)
src_pos = src_x, src_y
self.moved(src_pos, dst_pos)
def move_src(self) -> None:
if not self.id:
return
_, _, _, _, dst_x, dst_y = self.src.canvas.coords(self.id)
dst_pos = dst_x, dst_y
self.moved(self.src.position(), dst_pos)
def moved(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None:
arc_pos = self._get_arcpoint(src_pos, dst_pos)
self.canvas.coords(self.id, *src_pos, *arc_pos, *dst_pos)
self.src.canvas.coords(self.id, *src_pos, *arc_pos, *dst_pos)
if self.middle_label:
self.canvas.coords(self.middle_label, *arc_pos)
src_pos, dst_pos = self.node_label_positions()
self.src.canvas.coords(self.middle_label, *arc_pos)
src_x, src_y, _, _, dst_x, dst_y = self.src.canvas.coords(self.id)
src_pos, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y)
if self.src_label:
self.canvas.coords(self.src_label, *src_pos)
self.src.canvas.coords(self.src_label, *src_pos)
if self.dst_label:
self.canvas.coords(self.dst_label, *dst_pos)
self.src.canvas.coords(self.dst_label, *dst_pos)
def moved2(
self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]
) -> None:
arc_pos = self._get_arcpoint(src_pos, dst_pos)
self.dst.canvas.coords(self.id2, *src_pos, *arc_pos, *dst_pos)
if self.middle_label2:
self.dst.canvas.coords(self.middle_label2, *arc_pos)
src_x, src_y, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2)
src_pos, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y)
if self.src_label2:
self.dst.canvas.coords(self.src_label2, *src_pos)
if self.dst_label2:
self.dst.canvas.coords(self.dst_label2, *dst_pos)
def delete(self) -> None:
logging.debug("deleting canvas edge, id: %s", self.id)
self.canvas.delete(self.id)
self.canvas.delete(self.src_label)
self.canvas.delete(self.dst_label)
self.src.canvas.delete(self.id)
self.src.canvas.delete(self.src_label)
self.src.canvas.delete(self.dst_label)
if self.dst:
self.dst.canvas.delete(self.id2)
self.dst.canvas.delete(self.src_label2)
self.dst.canvas.delete(self.dst_label2)
if self.src_shadow and self.src_shadow.should_delete():
self.src_shadow.delete()
self.src_shadow = None
if self.dst_shadow and self.dst_shadow.should_delete():
self.dst_shadow.delete()
self.dst_shadow = None
self.clear_middle_label()
self.id = None
self.id2 = None
self.src_label = None
self.src_label2 = None
self.dst_label = None
self.dst_label2 = None
def hide(self) -> None:
self.hidden = True
if self.src_shadow:
self.src_shadow.hide()
if self.dst_shadow:
self.dst_shadow.hide()
self.src.canvas.itemconfigure(self.id, state=tk.HIDDEN)
self.src.canvas.itemconfigure(self.src_label, state=tk.HIDDEN)
self.src.canvas.itemconfigure(self.dst_label, state=tk.HIDDEN)
self.src.canvas.itemconfigure(self.middle_label, state=tk.HIDDEN)
if self.id2:
self.dst.canvas.itemconfigure(self.id2, state=tk.HIDDEN)
self.dst.canvas.itemconfigure(self.src_label2, state=tk.HIDDEN)
self.dst.canvas.itemconfigure(self.dst_label2, state=tk.HIDDEN)
self.dst.canvas.itemconfigure(self.middle_label2, state=tk.HIDDEN)
def show(self) -> None:
self.hidden = False
if self.src_shadow:
self.src_shadow.show()
if self.dst_shadow:
self.dst_shadow.show()
self.src.canvas.itemconfigure(self.id, state=tk.NORMAL)
state = self.manager.show_link_labels.state()
self.set_labels(state)
def set_labels(self, state: str) -> None:
self.src.canvas.itemconfigure(self.src_label, state=state)
self.src.canvas.itemconfigure(self.dst_label, state=state)
self.src.canvas.itemconfigure(self.middle_label, state=state)
if self.id2:
self.dst.canvas.itemconfigure(self.id2, state=state)
self.dst.canvas.itemconfigure(self.src_label2, state=state)
self.dst.canvas.itemconfigure(self.dst_label2, state=state)
self.dst.canvas.itemconfigure(self.middle_label2, state=state)
def other_node(self, node: "CanvasNode") -> "CanvasNode":
if self.src == node:
return self.dst
elif self.dst == node:
return self.src
else:
raise ValueError(f"node({node.core_node.name}) does not belong to edge")
def other_iface(self, node: "CanvasNode") -> Optional[Interface]:
if self.src == node:
return self.link.iface2 if self.link else None
elif self.dst == node:
return self.link.iface1 if self.link else None
else:
raise ValueError(f"node({node.core_node.name}) does not belong to edge")
def iface(self, node: "CanvasNode") -> Optional[Interface]:
if self.src == node:
return self.link.iface1 if self.link else None
elif self.dst == node:
return self.link.iface2 if self.link else None
else:
raise ValueError(f"node({node.core_node.name}) does not belong to edge")
class CanvasWirelessEdge(Edge):
@ -239,35 +481,43 @@ class CanvasWirelessEdge(Edge):
def __init__(
self,
canvas: "CanvasGraph",
src: int,
dst: int,
app: "Application",
src: "CanvasNode",
dst: "CanvasNode",
network_id: int,
token: str,
src_pos: Tuple[float, float],
dst_pos: Tuple[float, float],
link: Link,
) -> None:
logging.debug("drawing wireless link from node %s to node %s", src, dst)
super().__init__(canvas, src, dst)
super().__init__(app, src, dst)
self.src.wireless_edges.add(self)
self.dst.wireless_edges.add(self)
self.network_id: int = network_id
self.link: Link = link
self.token: str = token
self.width: float = WIRELESS_WIDTH
color = link.color if link.color else WIRELESS_COLOR
self.color: str = color
self.draw(src_pos, dst_pos, self.canvas.show_wireless.state())
state = self.manager.show_wireless.state()
self.draw(state)
if link.label:
self.middle_label_text(link.label)
if self.src.hidden or self.dst.hidden:
self.hide()
self.set_binding()
def set_binding(self) -> None:
self.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
self.src.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
if self.id2 is not None:
self.dst.canvas.tag_bind(self.id2, "<Button-1>", self.show_info)
def show_info(self, _event: tk.Event) -> None:
self.canvas.app.display_info(
WirelessEdgeInfoFrame, app=self.canvas.app, edge=self
)
self.app.display_info(WirelessEdgeInfoFrame, app=self.app, edge=self)
def delete(self) -> None:
self.src.wireless_edges.discard(self)
self.dst.wireless_edges.remove(self)
super().delete()
class CanvasEdge(Edge):
@ -276,47 +526,39 @@ class CanvasEdge(Edge):
"""
def __init__(
self,
canvas: "CanvasGraph",
src: int,
src_pos: Tuple[float, float],
dst_pos: Tuple[float, float],
self, app: "Application", src: "CanvasNode", dst: "CanvasNode" = None
) -> None:
"""
Create an instance of canvas edge object
"""
super().__init__(canvas, src)
super().__init__(app, src, dst)
self.text_src: Optional[int] = None
self.text_dst: Optional[int] = None
self.link: Optional[Link] = None
self.linked_wireless: bool = False
self.asymmetric_link: Optional[Link] = None
self.throughput: Optional[float] = None
self.draw(src_pos, dst_pos, tk.NORMAL)
self.set_binding()
self.context: tk.Menu = tk.Menu(self.canvas)
self.create_context()
self.draw(tk.NORMAL)
def is_customized(self) -> bool:
return self.width != EDGE_WIDTH or self.color != EDGE_COLOR
def create_context(self) -> None:
themes.style_menu(self.context)
self.context.add_command(label="Configure", command=self.click_configure)
self.context.add_command(label="Delete", command=self.click_delete)
def set_binding(self) -> None:
self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.show_context)
self.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
def set_bindings(self) -> None:
if self.id:
show_context = functools.partial(self.show_context, self.src.canvas)
self.src.canvas.tag_bind(self.id, "<ButtonRelease-3>", show_context)
self.src.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
if self.id2:
show_context = functools.partial(self.show_context, self.dst.canvas)
self.dst.canvas.tag_bind(self.id2, "<ButtonRelease-3>", show_context)
self.dst.canvas.tag_bind(self.id2, "<Button-1>", self.show_info)
def iface_label(self, iface: Interface) -> str:
label = ""
if iface.name and self.canvas.show_iface_names.get():
if iface.name and self.manager.show_iface_names.get():
label = f"{iface.name}"
if iface.ip4 and self.canvas.show_ip4s.get():
if iface.ip4 and self.manager.show_ip4s.get():
label = f"{label}\n" if label else ""
label += f"{iface.ip4}/{iface.ip4_mask}"
if iface.ip6 and self.canvas.show_ip6s.get():
if iface.ip6 and self.manager.show_ip6s.get():
label = f"{label}\n" if label else ""
label += f"{iface.ip6}/{iface.ip6_mask}"
return label
@ -341,82 +583,126 @@ class CanvasEdge(Edge):
super().redraw()
self.draw_labels()
def check_options(self) -> None:
if not self.link.options:
return
if self.link.options.loss == EDGE_LOSS:
def show(self) -> None:
super().show()
self.check_visibility()
def check_visibility(self) -> None:
state = tk.NORMAL
hide_links = self.manager.show_links.state() == tk.HIDDEN
if self.linked_wireless or hide_links:
state = tk.HIDDEN
self.canvas.addtag_withtag(tags.LOSS_EDGES, self.id)
else:
state = tk.NORMAL
self.canvas.dtag(self.id, tags.LOSS_EDGES)
if self.canvas.show_loss_links.state() == tk.HIDDEN:
self.canvas.itemconfigure(self.id, state=state)
elif self.link.options:
hide_loss = self.manager.show_loss_links.state() == tk.HIDDEN
should_hide = self.link.options.loss >= EDGE_LOSS
if hide_loss and should_hide:
state = tk.HIDDEN
if self.id:
self.src.canvas.itemconfigure(self.id, state=state)
if self.id2:
self.dst.canvas.itemconfigure(self.id2, state=state)
def set_throughput(self, throughput: float) -> None:
throughput = 0.001 * throughput
text = f"{throughput:.3f} kbps"
self.middle_label_text(text)
if throughput > self.canvas.throughput_threshold:
color = self.canvas.throughput_color
width = self.canvas.throughput_width
if throughput > self.manager.throughput_threshold:
color = self.manager.throughput_color
width = self.manager.throughput_width
else:
color = self.color
width = self.scaled_width()
self.canvas.itemconfig(self.id, fill=color, width=width)
self.src.canvas.itemconfig(self.id, fill=color, width=width)
if self.id2:
self.dst.canvas.itemconfig(self.id2, fill=color, width=width)
def clear_throughput(self) -> None:
self.clear_middle_label()
if not self.linked_wireless:
self.draw_link_options()
def complete(self, dst: int, linked_wireless: bool) -> None:
def complete(self, dst: "CanvasNode", link: Link = None) -> None:
logging.debug(
"completing wired link from node(%s) to node(%s)",
self.src.core_node.name,
dst.core_node.name,
)
self.dst = dst
self.linked_wireless = linked_wireless
dst_pos = self.canvas.coords(self.dst)
self.move_dst(dst_pos)
self.linked_wireless = self.src.is_wireless() or self.dst.is_wireless()
self.set_bindings()
self.check_wireless()
logging.debug("draw wired link from node %s to node %s", self.src, dst)
if link is None:
link = self.app.core.ifaces_manager.create_link(self)
if link.iface1:
iface1 = link.iface1
self.src.ifaces[iface1.id] = iface1
if link.iface2:
iface2 = link.iface2
self.dst.ifaces[iface2.id] = iface2
self.token = create_edge_token(link)
self.link = link
self.src.edges.add(self)
self.dst.edges.add(self)
if not self.linked_wireless:
self.arc_common_edges()
self.draw_labels()
self.check_visibility()
self.app.core.save_edge(self)
self.src.canvas.organize()
if self.has_shadows():
self.dst.canvas.organize()
self.manager.edges[self.token] = self
def check_wireless(self) -> None:
if self.linked_wireless:
self.canvas.itemconfig(self.id, state=tk.HIDDEN)
self.canvas.dtag(self.id, tags.EDGE)
self._check_antenna()
def _check_antenna(self) -> None:
src_node = self.canvas.nodes[self.src]
dst_node = self.canvas.nodes[self.dst]
src_node_type = src_node.core_node.type
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()
else:
src_node.add_antenna()
if not self.linked_wireless:
return
if self.id:
self.src.canvas.itemconfig(self.id, state=tk.HIDDEN)
self.src.canvas.dtag(self.id, tags.EDGE)
if self.id2:
self.dst.canvas.itemconfig(self.id2, state=tk.HIDDEN)
self.dst.canvas.dtag(self.id2, tags.EDGE)
# add antenna to node
if self.src.is_wireless() and not self.dst.is_wireless():
self.dst.add_antenna()
elif not self.src.is_wireless() and self.dst.is_wireless():
self.src.add_antenna()
else:
self.src.add_antenna()
def reset(self) -> None:
self.canvas.delete(self.middle_label)
self.middle_label = None
self.canvas.itemconfig(self.id, fill=self.color, width=self.scaled_width())
if self.middle_label:
self.src.canvas.delete(self.middle_label)
self.middle_label = None
if self.middle_label2:
self.dst.canvas.delete(self.middle_label2)
self.middle_label2 = None
if self.id:
self.src.canvas.itemconfig(
self.id, fill=self.color, width=self.scaled_width()
)
if self.id2:
self.dst.canvas.itemconfig(
self.id2, fill=self.color, width=self.scaled_width()
)
def show_info(self, _event: tk.Event) -> None:
self.canvas.app.display_info(EdgeInfoFrame, app=self.canvas.app, edge=self)
self.app.display_info(EdgeInfoFrame, app=self.app, edge=self)
def show_context(self, event: tk.Event) -> None:
state = tk.DISABLED if self.canvas.core.is_runtime() else tk.NORMAL
self.context.entryconfigure(1, state=state)
self.context.tk_popup(event.x_root, event.y_root)
def show_context(self, canvas: "CanvasGraph", event: tk.Event) -> None:
context: tk.Menu = tk.Menu(canvas)
themes.style_menu(context)
context.add_command(label="Configure", command=self.click_configure)
context.add_command(label="Delete", command=self.click_delete)
state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL
context.entryconfigure(1, state=state)
context.tk_popup(event.x_root, event.y_root)
def click_delete(self) -> None:
self.canvas.delete_edge(self)
self.delete()
def click_configure(self) -> None:
dialog = LinkConfigurationDialog(self.canvas.app, self)
dialog = LinkConfigurationDialog(self.app, self)
dialog.show()
def draw_link_options(self):
@ -455,3 +741,21 @@ class CanvasEdge(Edge):
lines.append(dup_line)
label = "\n".join(lines)
self.middle_label_text(label)
def delete(self) -> None:
self.src.edges.discard(self)
if self.dst:
self.dst.edges.discard(self)
if self.link.iface1:
del self.src.ifaces[self.link.iface1.id]
if self.link.iface2:
del self.dst.ifaces[self.link.iface2.id]
if self.src.is_wireless():
self.dst.delete_antenna()
if self.dst.is_wireless():
self.src.delete_antenna()
self.app.core.deleted_canvas_edges([self])
super().delete()
if self.dst:
self.arc_common_edges()
self.manager.edges.pop(self.token, None)

View file

@ -1,79 +1,57 @@
import logging
import tkinter as tk
from copy import deepcopy
from tkinter import BooleanVar
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
from PIL import Image
from PIL.ImageTk import PhotoImage
from core.api.grpc.wrappers import (
Interface,
Link,
LinkType,
Node,
Session,
ThroughputsEvent,
)
from core.api.grpc.wrappers import Interface, Link
from core.gui import appconfig
from core.gui import nodeutils as nutils
from core.gui.dialogs.shapemod import ShapeDialog
from core.gui.graph import tags
from core.gui.graph.edges import (
EDGE_WIDTH,
CanvasEdge,
CanvasWirelessEdge,
Edge,
arc_edges,
create_edge_token,
create_wireless_token,
)
from core.gui.graph.edges import EDGE_WIDTH, CanvasEdge
from core.gui.graph.enums import GraphMode, ScaleOption
from core.gui.graph.node import CanvasNode
from core.gui.graph.node import CanvasNode, ShadowNode
from core.gui.graph.shape import Shape
from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker
from core.gui.images import ImageEnum, TypeToImage
from core.gui.nodeutils import NodeDraw, NodeUtils
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.manager import CanvasManager
from core.gui.coreclient import CoreClient
ZOOM_IN = 1.1
ZOOM_OUT = 0.9
ICON_SIZE = 48
MOVE_NODE_MODES = {GraphMode.NODE, GraphMode.SELECT}
MOVE_SHAPE_MODES = {GraphMode.ANNOTATION, GraphMode.SELECT}
class ShowVar(BooleanVar):
def __init__(self, canvas: "CanvasGraph", tag: str, value: bool) -> None:
super().__init__(value=value)
self.canvas = canvas
self.tag = tag
def state(self) -> str:
return tk.NORMAL if self.get() else tk.HIDDEN
def click_handler(self) -> None:
self.canvas.itemconfigure(self.tag, state=self.state())
ZOOM_IN: float = 1.1
ZOOM_OUT: float = 0.9
MOVE_NODE_MODES: Set[GraphMode] = {GraphMode.NODE, GraphMode.SELECT}
MOVE_SHAPE_MODES: Set[GraphMode] = {GraphMode.ANNOTATION, GraphMode.SELECT}
BACKGROUND_COLOR: str = "#cccccc"
class CanvasGraph(tk.Canvas):
def __init__(
self, master: tk.BaseWidget, app: "Application", core: "CoreClient"
self,
master: tk.BaseWidget,
app: "Application",
manager: "CanvasManager",
core: "CoreClient",
_id: int,
dimensions: Tuple[int, int],
) -> None:
super().__init__(master, highlightthickness=0, background="#cccccc")
super().__init__(master, highlightthickness=0, background=BACKGROUND_COLOR)
self.id: int = _id
self.app: "Application" = app
self.manager: "CanvasManager" = manager
self.core: "CoreClient" = core
self.mode: GraphMode = GraphMode.SELECT
self.annotation_type: Optional[ShapeType] = None
self.selection: Dict[int, int] = {}
self.select_box: Optional[Shape] = None
self.selected: Optional[int] = None
self.node_draw: Optional[NodeDraw] = None
self.nodes: Dict[int, CanvasNode] = {}
self.edges: Dict[str, CanvasEdge] = {}
self.shadow_nodes: Dict[int, ShadowNode] = {}
self.shapes: Dict[int, Shape] = {}
self.wireless_edges: Dict[str, CanvasWirelessEdge] = {}
self.shadow_core_nodes: Dict[int, ShadowNode] = {}
# map wireless/EMANE node to the set of MDRs connected to that node
self.wireless_network: Dict[int, Set[int]] = {}
@ -81,10 +59,7 @@ class CanvasGraph(tk.Canvas):
self.drawing_edge: Optional[CanvasEdge] = None
self.rect: Optional[int] = None
self.shape_drawing: bool = False
width = self.app.guiconfig.preferences.width
height = self.app.guiconfig.preferences.height
self.default_dimensions: Tuple[int, int] = (width, height)
self.current_dimensions: Tuple[int, int] = self.default_dimensions
self.current_dimensions: Tuple[int, int] = dimensions
self.ratio: float = 1.0
self.offset: Tuple[int, int] = (0, 0)
self.cursor: Tuple[int, int] = (0, 0)
@ -98,23 +73,6 @@ class CanvasGraph(tk.Canvas):
self.scale_option: tk.IntVar = tk.IntVar(value=1)
self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(value=False)
# throughput related
self.throughput_threshold: float = 250.0
self.throughput_width: int = 10
self.throughput_color: str = "#FF0000"
# drawing related
self.show_node_labels: ShowVar = ShowVar(self, tags.NODE_LABEL, value=True)
self.show_link_labels: ShowVar = ShowVar(self, tags.LINK_LABEL, value=True)
self.show_links: ShowVar = ShowVar(self, tags.EDGE, value=True)
self.show_wireless: ShowVar = ShowVar(self, tags.WIRELESS_EDGE, value=True)
self.show_grid: ShowVar = ShowVar(self, tags.GRIDLINE, value=True)
self.show_annotations: ShowVar = ShowVar(self, tags.ANNOTATION, value=True)
self.show_loss_links: ShowVar = ShowVar(self, tags.LOSS_EDGES, value=True)
self.show_iface_names: BooleanVar = BooleanVar(value=False)
self.show_ip4s: BooleanVar = BooleanVar(value=True)
self.show_ip6s: BooleanVar = BooleanVar(value=True)
# bindings
self.setup_bindings()
@ -126,7 +84,7 @@ class CanvasGraph(tk.Canvas):
if self.rect is not None:
self.delete(self.rect)
if not dimensions:
dimensions = self.default_dimensions
dimensions = self.manager.default_dimensions
self.current_dimensions = dimensions
self.rect = self.create_rectangle(
0,
@ -139,34 +97,6 @@ class CanvasGraph(tk.Canvas):
)
self.configure(scrollregion=self.bbox(tk.ALL))
def reset_and_redraw(self, session: Session) -> None:
# reset view options to default state
self.show_node_labels.set(True)
self.show_link_labels.set(True)
self.show_grid.set(True)
self.show_annotations.set(True)
self.show_iface_names.set(False)
self.show_ip4s.set(True)
self.show_ip6s.set(True)
self.show_loss_links.set(True)
# delete any existing drawn items
for tag in tags.RESET_TAGS:
self.delete(tag)
# 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()
self.edges.clear()
self.shapes.clear()
self.wireless_edges.clear()
self.wireless_network.clear()
self.drawing_edge = None
self.draw_session(session)
def setup_bindings(self) -> None:
"""
Bind any mouse events or hot keys to the matching action
@ -183,6 +113,12 @@ class CanvasGraph(tk.Canvas):
self.bind("<ButtonPress-3>", lambda e: self.scan_mark(e.x, e.y))
self.bind("<B3-Motion>", lambda e: self.scan_dragto(e.x, e.y, gain=1))
def get_shadow(self, node: CanvasNode) -> ShadowNode:
shadow_node = self.shadow_core_nodes.get(node.core_node.id)
if not shadow_node:
shadow_node = ShadowNode(self.app, self, node)
return shadow_node
def get_actual_coords(self, x: float, y: float) -> Tuple[float, float]:
actual_x = (x - self.offset[0]) / self.ratio
actual_y = (y - self.offset[1]) / self.ratio
@ -204,16 +140,6 @@ class CanvasGraph(tk.Canvas):
valid_bottomright = self.inside_canvas(x2, y2)
return valid_topleft and valid_bottomright
def set_throughputs(self, throughputs_event: ThroughputsEvent) -> None:
for iface_throughput in throughputs_event.iface_throughputs:
node_id = iface_throughput.node_id
iface_id = iface_throughput.iface_id
throughput = iface_throughput.throughput
iface_to_edge_id = (node_id, iface_id)
edge = self.core.iface_to_edge.get(iface_to_edge_id)
if edge:
edge.set_throughput(throughput)
def draw_grid(self) -> None:
"""
Create grid.
@ -228,123 +154,6 @@ class CanvasGraph(tk.Canvas):
self.tag_lower(tags.GRIDLINE)
self.tag_lower(self.rect)
def add_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None:
token = create_edge_token(link)
if token in self.edges and link.options.unidirectional:
edge = self.edges[token]
edge.asymmetric_link = link
elif token not in self.edges:
node1 = src.core_node
node2 = dst.core_node
src_pos = (node1.position.x, node1.position.y)
dst_pos = (node2.position.x, node2.position.y)
edge = CanvasEdge(self, src.id, src_pos, dst_pos)
self.complete_edge(src, dst, edge, link)
def delete_wired_edge(self, link: Link) -> None:
token = create_edge_token(link)
edge = self.edges.get(token)
if edge:
self.delete_edge(edge)
def update_wired_edge(self, link: Link) -> None:
token = create_edge_token(link)
edge = self.edges.get(token)
if edge:
edge.link.options = deepcopy(link.options)
edge.draw_link_options()
edge.check_options()
def add_wireless_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None:
network_id = link.network_id if link.network_id else None
token = create_wireless_token(src.id, dst.id, network_id)
if token in self.wireless_edges:
logging.warning("ignoring link that already exists: %s", link)
return
src_pos = self.coords(src.id)
dst_pos = self.coords(dst.id)
edge = CanvasWirelessEdge(
self, src.id, dst.id, network_id, token, src_pos, dst_pos, link
)
self.wireless_edges[token] = edge
src.wireless_edges.add(edge)
dst.wireless_edges.add(edge)
self.tag_raise(src.id)
self.tag_raise(dst.id)
self.arc_common_edges(edge)
def delete_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: Link
) -> None:
network_id = link.network_id if link.network_id else None
token = create_wireless_token(src.id, dst.id, network_id)
if token not in self.wireless_edges:
return
edge = self.wireless_edges.pop(token)
edge.delete()
src.wireless_edges.remove(edge)
dst.wireless_edges.remove(edge)
self.arc_common_edges(edge)
def update_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: Link
) -> None:
if not link.label:
return
network_id = link.network_id if link.network_id else None
token = create_wireless_token(src.id, dst.id, network_id)
if token not in self.wireless_edges:
self.add_wireless_edge(src, dst, link)
else:
edge = self.wireless_edges[token]
edge.middle_label_text(link.label)
def add_core_node(self, core_node: Node) -> None:
logging.debug("adding node: %s", core_node)
# if the gui can't find node's image, default to the "edit-node" image
image = NodeUtils.node_image(core_node, self.app.guiconfig, self.app.app_scale)
if not image:
image = self.app.get_icon(ImageEnum.EDITNODE, ICON_SIZE)
x = core_node.position.x
y = core_node.position.y
node = CanvasNode(self.app, x, y, core_node, image)
self.nodes[node.id] = node
self.core.set_canvas_node(core_node, node)
def draw_session(self, session: Session) -> None:
"""
Draw existing session.
"""
# draw existing nodes
for core_node in session.nodes.values():
logging.debug("drawing node: %s", core_node)
# peer to peer node is not drawn on the GUI
if NodeUtils.is_ignore_node(core_node.type):
continue
self.add_core_node(core_node)
# draw existing links
for link in session.links:
logging.debug("drawing link: %s", link)
canvas_node1 = self.core.get_canvas_node(link.node1_id)
canvas_node2 = self.core.get_canvas_node(link.node2_id)
if link.type == LinkType.WIRELESS:
self.add_wireless_edge(canvas_node1, canvas_node2, link)
else:
self.add_wired_edge(canvas_node1, canvas_node2, link)
def stopped_session(self) -> None:
# 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 throughputs
self.clear_throughputs()
def canvas_xy(self, event: tk.Event) -> Tuple[float, float]:
"""
Convert window coordinate to canvas coordinate
@ -363,14 +172,12 @@ class CanvasGraph(tk.Canvas):
for _id in overlapping:
if self.drawing_edge and self.drawing_edge.id == _id:
continue
if _id in self.nodes:
elif _id in self.nodes:
selected = _id
break
if _id in self.shapes:
elif _id in self.shapes:
selected = _id
elif _id in self.shadow_nodes:
selected = _id
return selected
def click_release(self, event: tk.Event) -> None:
@ -381,13 +188,13 @@ class CanvasGraph(tk.Canvas):
x, y = self.canvas_xy(event)
if not self.inside_canvas(x, y):
return
if self.mode == GraphMode.ANNOTATION:
if self.manager.mode == GraphMode.ANNOTATION:
self.focus_set()
if self.shape_drawing:
shape = self.shapes[self.selected]
shape.shape_complete(x, y)
self.shape_drawing = False
elif self.mode == GraphMode.SELECT:
elif self.manager.mode == GraphMode.SELECT:
self.focus_set()
if self.select_box:
x0, y0, x1, y1 = self.coords(self.select_box.id)
@ -403,61 +210,36 @@ class CanvasGraph(tk.Canvas):
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:
logging.debug(
"click release selected(%s) mode(%s)", self.selected, self.manager.mode
)
if self.manager.mode == GraphMode.EDGE:
self.handle_edge_release(event)
elif self.mode == GraphMode.NODE:
elif self.manager.mode == GraphMode.NODE:
self.add_node(x, y)
elif self.mode == GraphMode.PICKNODE:
self.mode = GraphMode.NODE
elif self.manager.mode == GraphMode.PICKNODE:
self.manager.mode = GraphMode.NODE
self.selected = None
def handle_edge_release(self, _event: tk.Event) -> None:
# not drawing edge return
if not self.drawing_edge:
return
edge = self.drawing_edge
self.drawing_edge = None
# not drawing edge return
if edge is None:
return
# edge dst must be a node
logging.debug("current selected: %s", self.selected)
src_node = self.nodes.get(edge.src)
dst_node = self.nodes.get(self.selected)
if not dst_node or not src_node:
if not dst_node:
edge.delete()
return
# edge dst is same as src, delete edge
if edge.src == self.selected:
# check if node can be linked
if not edge.src.is_linkable(dst_node):
edge.delete()
return
# rj45 nodes can only support one link
if NodeUtils.is_rj45_node(src_node.core_node.type) and src_node.edges:
edge.delete()
return
if NodeUtils.is_rj45_node(dst_node.core_node.type) and dst_node.edges:
edge.delete()
return
# only 1 link between bridge based nodes
is_src_bridge = NodeUtils.is_bridge_node(src_node.core_node)
is_dst_bridge = NodeUtils.is_bridge_node(dst_node.core_node)
common_links = src_node.edges & dst_node.edges
if all([is_src_bridge, is_dst_bridge, common_links]):
edge.delete()
return
# finalize edge creation
self.complete_edge(src_node, dst_node, edge)
def arc_common_edges(self, edge: Edge) -> None:
src_node = self.nodes[edge.src]
dst_node = self.nodes[edge.dst]
common_edges = list(src_node.edges & dst_node.edges)
common_edges += list(src_node.wireless_edges & dst_node.wireless_edges)
arc_edges(common_edges)
edge.drawing(dst_node.position())
edge.complete(dst_node)
def select_object(self, object_id: int, choose_multiple: bool = False) -> None:
"""
@ -504,28 +286,16 @@ class CanvasGraph(tk.Canvas):
# delete node and related edges
if object_id in self.nodes:
canvas_node = self.nodes.pop(object_id)
canvas_node.delete()
nodes.append(canvas_node)
is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type)
# delete related edges
for edge in canvas_node.edges:
while canvas_node.edges:
edge = canvas_node.edges.pop()
if edge in edges:
continue
edges.add(edge)
del self.edges[edge.token]
edge.delete()
# update node connected to edge being deleted
other_id = edge.src
other_iface = edge.link.iface1
if edge.src == object_id:
other_id = edge.dst
other_iface = edge.link.iface2
other_node = self.nodes[other_id]
other_node.edges.remove(edge)
if other_iface:
del other_node.ifaces[other_iface.id]
if is_wireless:
other_node.delete_antenna()
# delete node
canvas_node.delete()
nodes.append(canvas_node)
# delete shape
if object_id in self.shapes:
@ -534,27 +304,21 @@ class CanvasGraph(tk.Canvas):
self.selection.clear()
self.core.deleted_canvas_nodes(nodes)
self.core.deleted_canvas_edges(edges)
def delete_edge(self, edge: CanvasEdge) -> None:
edge.delete()
del self.edges[edge.token]
src_node = self.nodes[edge.src]
src_node.edges.discard(edge)
if edge.link.iface1:
del src_node.ifaces[edge.link.iface1.id]
dst_node = self.nodes[edge.dst]
dst_node.edges.discard(edge)
if edge.link.iface2:
del dst_node.ifaces[edge.link.iface2.id]
src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type)
if src_wireless:
dst_node.delete_antenna()
dst_wireless = NodeUtils.is_wireless_node(dst_node.core_node.type)
if dst_wireless:
src_node.delete_antenna()
self.core.deleted_canvas_edges([edge])
self.arc_common_edges(edge)
def hide_selected_objects(self) -> None:
for object_id in self.selection:
# delete selection box
selection_id = self.selection[object_id]
self.delete(selection_id)
# hide node and related edges
if object_id in self.nodes:
canvas_node = self.nodes[object_id]
canvas_node.hide()
def show_hidden(self) -> None:
for node in self.nodes.values():
if node.hidden:
node.show()
def zoom(self, event: tk.Event, factor: float = None) -> None:
if not factor:
@ -588,13 +352,13 @@ class CanvasGraph(tk.Canvas):
y_check = self.cursor[1] - self.offset[1]
logging.debug("click press offset(%s, %s)", x_check, y_check)
is_node = selected in self.nodes
if self.mode == GraphMode.EDGE and is_node:
pos = self.coords(selected)
self.drawing_edge = CanvasEdge(self, selected, pos, pos)
if self.manager.mode == GraphMode.EDGE and is_node:
node = self.nodes[selected]
self.drawing_edge = CanvasEdge(self.app, node)
self.organize()
if self.mode == GraphMode.ANNOTATION:
if is_marker(self.annotation_type):
if self.manager.mode == GraphMode.ANNOTATION:
if is_marker(self.manager.annotation_type):
r = self.app.toolbar.marker_frame.size.get()
self.create_oval(
x - r,
@ -604,11 +368,11 @@ class CanvasGraph(tk.Canvas):
fill=self.app.toolbar.marker_frame.color,
outline="",
tags=(tags.MARKER, tags.ANNOTATION),
state=self.show_annotations.state(),
state=self.manager.show_annotations.state(),
)
return
if selected is None:
shape = Shape(self.app, self, self.annotation_type, x, y)
shape = Shape(self.app, self, self.manager.annotation_type, x, y)
self.selected = shape.id
self.shape_drawing = True
self.shapes[shape.id] = shape
@ -629,8 +393,18 @@ class CanvasGraph(tk.Canvas):
node.core_node.position.x,
node.core_node.position.y,
)
elif selected in self.shadow_nodes:
shadow_node = self.shadow_nodes[selected]
self.select_object(shadow_node.id)
self.selected = selected
logging.debug(
"selected shadow node(%s), coords: (%s, %s)",
shadow_node.node.core_node.name,
shadow_node.node.core_node.position.x,
shadow_node.node.core_node.position.y,
)
else:
if self.mode == GraphMode.SELECT:
if self.manager.mode == GraphMode.SELECT:
shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y)
self.select_box = shape
self.clear_selection()
@ -659,7 +433,7 @@ class CanvasGraph(tk.Canvas):
if self.select_box:
self.select_box.delete()
self.select_box = None
if is_draw_shape(self.annotation_type) and self.shape_drawing:
if is_draw_shape(self.manager.annotation_type) and self.shape_drawing:
shape = self.shapes.pop(self.selected)
shape.delete()
self.shape_drawing = False
@ -669,14 +443,14 @@ class CanvasGraph(tk.Canvas):
y_offset = y - self.cursor[1]
self.cursor = x, y
if self.mode == GraphMode.EDGE and self.drawing_edge is not None:
self.drawing_edge.move_dst(self.cursor)
if self.mode == GraphMode.ANNOTATION:
if is_draw_shape(self.annotation_type) and self.shape_drawing:
if self.manager.mode == GraphMode.EDGE and self.drawing_edge is not None:
self.drawing_edge.drawing(self.cursor)
if self.manager.mode == GraphMode.ANNOTATION:
if is_draw_shape(self.manager.annotation_type) and self.shape_drawing:
shape = self.shapes[self.selected]
shape.shape_motion(x, y)
return
elif is_marker(self.annotation_type):
elif is_marker(self.manager.annotation_type):
r = self.app.toolbar.marker_frame.size.get()
self.create_oval(
x - r,
@ -689,21 +463,26 @@ class CanvasGraph(tk.Canvas):
)
return
if self.mode == GraphMode.EDGE:
if self.manager.mode == GraphMode.EDGE:
return
# move selected objects
if self.selection:
for selected_id in self.selection:
if self.mode in MOVE_SHAPE_MODES and selected_id in self.shapes:
if self.manager.mode in MOVE_SHAPE_MODES and selected_id in self.shapes:
shape = self.shapes[selected_id]
shape.motion(x_offset, y_offset)
if self.mode in MOVE_NODE_MODES and selected_id in self.nodes:
elif self.manager.mode in MOVE_NODE_MODES and selected_id in self.nodes:
node = self.nodes[selected_id]
node.motion(x_offset, y_offset, update=self.core.is_runtime())
elif (
self.manager.mode in MOVE_NODE_MODES
and selected_id in self.shadow_nodes
):
shadow_node = self.shadow_nodes[selected_id]
shadow_node.motion(x_offset, y_offset)
else:
if self.select_box and self.mode == GraphMode.SELECT:
if self.select_box and self.manager.mode == GraphMode.SELECT:
self.select_box.shape_motion(x, y)
def press_delete(self, _event: tk.Event) -> None:
@ -729,17 +508,15 @@ class CanvasGraph(tk.Canvas):
return
actual_x, actual_y = self.get_actual_coords(x, y)
core_node = self.core.create_node(
actual_x, actual_y, self.node_draw.node_type, self.node_draw.model
actual_x,
actual_y,
self.manager.node_draw.node_type,
self.manager.node_draw.model,
)
if not core_node:
return
try:
image_enum = self.node_draw.image_enum
self.node_draw.image = self.app.get_icon(image_enum, ICON_SIZE)
except AttributeError:
image_file = self.node_draw.image_file
self.node_draw.image = self.app.get_custom_icon(image_file, ICON_SIZE)
node = CanvasNode(self.app, x, y, core_node, self.node_draw.image)
core_node.canvas = self.id
node = CanvasNode(self.app, self, x, y, core_node, self.manager.node_draw.image)
self.nodes[node.id] = node
self.core.set_canvas_node(core_node, node)
@ -847,7 +624,7 @@ class CanvasGraph(tk.Canvas):
# redraw gridlines to new canvas size
self.delete(tags.GRIDLINE)
self.draw_grid()
self.app.canvas.show_grid.click_handler()
self.app.manager.show_grid.click_handler()
def redraw_wallpaper(self) -> None:
if self.adjust_to_dim.get():
@ -871,7 +648,7 @@ class CanvasGraph(tk.Canvas):
self.tag_raise(tag)
def set_wallpaper(self, filename: Optional[str]) -> None:
logging.debug("setting wallpaper: %s", filename)
logging.info("setting canvas(%s) background: %s", self.id, filename)
if filename:
img = Image.open(filename)
self.wallpaper = img
@ -884,44 +661,16 @@ class CanvasGraph(tk.Canvas):
self.wallpaper_file = None
def is_selection_mode(self) -> bool:
return self.mode == GraphMode.SELECT
return self.manager.mode == GraphMode.SELECT
def create_edge(self, src: CanvasNode, dst: CanvasNode) -> CanvasEdge:
"""
create an edge between source node and destination node
"""
pos = (src.core_node.position.x, src.core_node.position.y)
edge = CanvasEdge(self, src.id, pos, pos)
self.complete_edge(src, dst, edge)
edge = CanvasEdge(self.app, src)
edge.complete(dst)
return edge
def complete_edge(
self,
src: CanvasNode,
dst: CanvasNode,
edge: CanvasEdge,
link: Optional[Link] = None,
) -> None:
linked_wireless = self.is_linked_wireless(src.id, dst.id)
edge.complete(dst.id, linked_wireless)
if link is None:
link = self.core.create_link(edge, src, dst)
edge.link = link
if link.iface1:
iface1 = link.iface1
src.ifaces[iface1.id] = iface1
if link.iface2:
iface2 = link.iface2
dst.ifaces[iface2.id] = iface2
src.edges.add(edge)
dst.edges.add(edge)
edge.token = create_edge_token(edge.link)
self.arc_common_edges(edge)
edge.draw_labels()
edge.check_options()
self.edges[edge.token] = edge
self.core.save_edge(edge, src, dst)
def copy(self) -> None:
if self.core.is_runtime():
logging.debug("copy is disabled during runtime state")
@ -952,7 +701,9 @@ class CanvasGraph(tk.Canvas):
)
if not copy:
continue
node = CanvasNode(self.app, scaled_x, scaled_y, copy, canvas_node.image)
node = CanvasNode(
self.app, self, scaled_x, scaled_y, copy, canvas_node.image
)
# copy configurations and services
node.core_node.services = core_node.services.copy()
node.core_node.config_services = core_node.config_services.copy()
@ -1039,49 +790,45 @@ class CanvasGraph(tk.Canvas):
)
self.tag_raise(tags.NODE)
def is_linked_wireless(self, src: int, dst: int) -> bool:
src_node = self.nodes[src]
dst_node = self.nodes[dst]
src_node_type = src_node.core_node.type
dst_node_type = dst_node.core_node.type
is_src_wireless = NodeUtils.is_wireless_node(src_node_type)
is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type)
# update the wlan/EMANE network
wlan_network = self.wireless_network
if is_src_wireless and not is_dst_wireless:
if src not in wlan_network:
wlan_network[src] = set()
wlan_network[src].add(dst)
elif not is_src_wireless and is_dst_wireless:
if dst not in wlan_network:
wlan_network[dst] = set()
wlan_network[dst].add(src)
return is_src_wireless or is_dst_wireless
def clear_throughputs(self) -> None:
for edge in self.edges.values():
edge.clear_throughput()
def scale_graph(self) -> None:
for nid, canvas_node in self.nodes.items():
img = None
if NodeUtils.is_custom(
canvas_node.core_node.type, canvas_node.core_node.model
):
for custom_node in self.app.guiconfig.nodes:
if custom_node.name == canvas_node.core_node.model:
img = self.app.get_custom_icon(custom_node.image, ICON_SIZE)
else:
image_enum = TypeToImage.get(
canvas_node.core_node.type, canvas_node.core_node.model
)
img = self.app.get_icon(image_enum, ICON_SIZE)
self.itemconfig(nid, image=img)
canvas_node.image = img
for node_id, canvas_node in self.nodes.items():
image = nutils.get_icon(canvas_node.core_node, self.app)
self.itemconfig(node_id, image=image)
canvas_node.image = image
canvas_node.scale_text()
canvas_node.scale_antennas()
for edge_id in self.find_withtag(tags.EDGE):
self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app.app_scale))
for edge_id in self.find_withtag(tags.EDGE):
self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app.app_scale))
def get_metadata(self) -> Dict[str, Any]:
wallpaper_path = None
if self.wallpaper_file:
wallpaper = Path(self.wallpaper_file)
if appconfig.BACKGROUNDS_PATH == wallpaper.parent:
wallpaper_path = wallpaper.name
else:
wallpaper_path = str(wallpaper)
return dict(
id=self.id,
wallpaper=wallpaper_path,
wallpaper_style=self.scale_option.get(),
fit_image=self.adjust_to_dim.get(),
)
def parse_metadata(self, config: Dict[str, Any]) -> None:
fit_image = config.get("fit_image", False)
self.adjust_to_dim.set(fit_image)
wallpaper_style = config.get("wallpaper_style", 1)
self.scale_option.set(wallpaper_style)
wallpaper = config.get("wallpaper")
if wallpaper:
wallpaper = Path(wallpaper)
if not wallpaper.is_file():
wallpaper = appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)
logging.info("canvas(%s), wallpaper: %s", self.id, wallpaper)
if wallpaper.is_file():
self.set_wallpaper(str(wallpaper))
else:
self.app.show_error(
"Background Error", f"background file not found: {wallpaper}"
)

View file

@ -0,0 +1,383 @@
import logging
import tkinter as tk
from copy import deepcopy
from tkinter import BooleanVar, messagebox, ttk
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, ValuesView
from core.api.grpc.wrappers import Link, LinkType, Node, Session, ThroughputsEvent
from core.gui import nodeutils as nutils
from core.gui.graph import tags
from core.gui.graph.edges import (
CanvasEdge,
CanvasWirelessEdge,
create_edge_token,
create_wireless_token,
)
from core.gui.graph.enums import GraphMode
from core.gui.graph.graph import CanvasGraph
from core.gui.graph.node import CanvasNode
from core.gui.graph.shapeutils import ShapeType
from core.gui.nodeutils import NodeDraw
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.coreclient import CoreClient
class ShowVar(BooleanVar):
def __init__(self, manager: "CanvasManager", tag: str, value: bool) -> None:
super().__init__(value=value)
self.manager: "CanvasManager" = manager
self.tag: str = tag
def state(self) -> str:
return tk.NORMAL if self.get() else tk.HIDDEN
def click_handler(self) -> None:
for canvas in self.manager.all():
canvas.itemconfigure(self.tag, state=self.state())
class ShowNodeLabels(ShowVar):
def click_handler(self) -> None:
state = self.state()
for canvas in self.manager.all():
for node in canvas.nodes.values():
if not node.hidden:
node.set_label(state)
class ShowLinks(ShowVar):
def click_handler(self) -> None:
for edge in self.manager.edges.values():
if not edge.hidden:
edge.check_visibility()
class ShowLinkLabels(ShowVar):
def click_handler(self) -> None:
state = self.state()
for edge in self.manager.edges.values():
if not edge.hidden:
edge.set_labels(state)
class CanvasManager:
def __init__(
self, master: tk.BaseWidget, app: "Application", core: "CoreClient"
) -> None:
self.master: tk.BaseWidget = master
self.app: "Application" = app
self.core: "CoreClient" = core
# canvas interactions
self.mode: GraphMode = GraphMode.SELECT
self.annotation_type: Optional[ShapeType] = None
self.node_draw: Optional[NodeDraw] = None
self.canvases: Dict[int, CanvasGraph] = {}
# global edge management
self.edges: Dict[str, CanvasEdge] = {}
self.wireless_edges: Dict[str, CanvasWirelessEdge] = {}
# global canvas settings
self.default_dimensions: Tuple[int, int] = (
self.app.guiconfig.preferences.width,
self.app.guiconfig.preferences.height,
)
self.current_dimensions: Tuple[int, int] = self.default_dimensions
self.show_node_labels: ShowVar = ShowNodeLabels(
self, tags.NODE_LABEL, value=True
)
self.show_link_labels: ShowVar = ShowLinkLabels(
self, tags.LINK_LABEL, value=True
)
self.show_links: ShowVar = ShowLinks(self, tags.EDGE, value=True)
self.show_wireless: ShowVar = ShowVar(self, tags.WIRELESS_EDGE, value=True)
self.show_grid: ShowVar = ShowVar(self, tags.GRIDLINE, value=True)
self.show_annotations: ShowVar = ShowVar(self, tags.ANNOTATION, value=True)
self.show_loss_links: ShowVar = ShowLinks(self, tags.LOSS_EDGES, value=True)
self.show_iface_names: BooleanVar = BooleanVar(value=False)
self.show_ip4s: BooleanVar = BooleanVar(value=True)
self.show_ip6s: BooleanVar = BooleanVar(value=True)
# throughput settings
self.throughput_threshold: float = 250.0
self.throughput_width: int = 10
self.throughput_color: str = "#FF0000"
# widget
self.notebook: Optional[ttk.Notebook] = None
self.canvas_ids: Dict[str, int] = {}
self.unique_ids: Dict[int, str] = {}
self.draw()
self.setup_bindings()
# start with a single tab by default
self.add_canvas()
def setup_bindings(self) -> None:
self.notebook.bind("<<NotebookTabChanged>>", self.tab_change)
def tab_change(self, _event: tk.Event) -> None:
# ignore tab change events before tab data has been setup
unique_id = self.notebook.select()
if not unique_id or unique_id not in self.canvas_ids:
return
canvas = self.current()
self.app.statusbar.set_zoom(canvas.ratio)
def select(self, tab_id: int):
unique_id = self.unique_ids.get(tab_id)
self.notebook.select(unique_id)
def draw(self) -> None:
self.notebook = ttk.Notebook(self.master)
self.notebook.grid(sticky=tk.NSEW, pady=1)
def _next_id(self) -> int:
_id = 1
canvas_ids = set(self.canvas_ids.values())
while _id in canvas_ids:
_id += 1
return _id
def current(self) -> CanvasGraph:
unique_id = self.notebook.select()
canvas_id = self.canvas_ids[unique_id]
return self.get(canvas_id)
def all(self) -> ValuesView[CanvasGraph]:
return self.canvases.values()
def get(self, canvas_id: int) -> CanvasGraph:
canvas = self.canvases.get(canvas_id)
if not canvas:
canvas = self.add_canvas(canvas_id)
return canvas
def add_canvas(self, canvas_id: int = None) -> CanvasGraph:
# create tab frame
tab = ttk.Frame(self.notebook, padding=0)
tab.grid(sticky=tk.NSEW)
tab.columnconfigure(0, weight=1)
tab.rowconfigure(0, weight=1)
if canvas_id is None:
canvas_id = self._next_id()
self.notebook.add(tab, text=f"Canvas {canvas_id}")
unique_id = self.notebook.tabs()[-1]
logging.info("creating canvas(%s)", canvas_id)
self.canvas_ids[unique_id] = canvas_id
self.unique_ids[canvas_id] = unique_id
# create canvas
canvas = CanvasGraph(
tab, self.app, self, self.core, canvas_id, self.default_dimensions
)
canvas.grid(sticky=tk.NSEW)
self.canvases[canvas_id] = canvas
# add scrollbars
scroll_y = ttk.Scrollbar(tab, command=canvas.yview)
scroll_y.grid(row=0, column=1, sticky=tk.NS)
scroll_x = ttk.Scrollbar(tab, orient=tk.HORIZONTAL, command=canvas.xview)
scroll_x.grid(row=1, column=0, sticky=tk.EW)
canvas.configure(xscrollcommand=scroll_x.set)
canvas.configure(yscrollcommand=scroll_y.set)
return canvas
def delete_canvas(self) -> None:
if len(self.notebook.tabs()) == 1:
messagebox.showinfo("Canvas", "Cannot delete last canvas", parent=self.app)
return
unique_id = self.notebook.select()
self.notebook.forget(unique_id)
canvas_id = self.canvas_ids.pop(unique_id)
canvas = self.canvases.pop(canvas_id)
edges = set()
for node in canvas.nodes.values():
node.delete()
while node.edges:
edge = node.edges.pop()
if edge in edges:
continue
edges.add(edge)
edge.delete()
def join(self, session: Session) -> None:
# clear out all canvas
for canvas_id in self.notebook.tabs():
self.notebook.forget(canvas_id)
self.canvases.clear()
self.canvas_ids.clear()
self.unique_ids.clear()
self.edges.clear()
self.wireless_edges.clear()
logging.info("cleared canvases")
# reset settings
self.show_node_labels.set(True)
self.show_link_labels.set(True)
self.show_grid.set(True)
self.show_annotations.set(True)
self.show_iface_names.set(False)
self.show_ip4s.set(True)
self.show_ip6s.set(True)
self.show_loss_links.set(True)
self.mode = GraphMode.SELECT
self.annotation_type = None
self.node_draw = None
# draw session
self.draw_session(session)
def draw_session(self, session: Session) -> None:
# create session nodes
for core_node in session.nodes.values():
# add node, avoiding ignored nodes
if nutils.should_ignore(core_node):
continue
self.add_core_node(core_node)
# organize canvas tabs
canvas_ids = sorted(self.canvases)
for index, canvas_id in enumerate(canvas_ids):
canvas = self.canvases[canvas_id]
self.notebook.insert(index, canvas.master)
# draw existing links
for link in session.links:
node1 = self.core.get_canvas_node(link.node1_id)
node2 = self.core.get_canvas_node(link.node2_id)
if link.type == LinkType.WIRELESS:
self.add_wireless_edge(node1, node2, link)
else:
self.add_wired_edge(node1, node2, link)
# parse metadata and organize canvases
self.core.parse_metadata()
for canvas in self.canvases.values():
canvas.organize()
# create a default canvas if none were created prior
if not self.canvases:
self.add_canvas()
def redraw_canvases(self, dimensions: Tuple[int, int]) -> None:
for canvas in self.canvases.values():
canvas.redraw_canvas(dimensions)
if canvas.wallpaper:
canvas.redraw_wallpaper()
def get_metadata(self) -> Dict[str, Any]:
canvases = [x.get_metadata() for x in self.all()]
return dict(
gridlines=self.app.manager.show_grid.get(),
dimensions=self.app.manager.current_dimensions,
canvases=canvases,
)
def parse_metadata(self, config: Dict[str, Any]) -> None:
# get configured dimensions and gridlines option
dimensions = self.default_dimensions
dimensions = config.get("dimensions", dimensions)
gridlines = config.get("gridlines", True)
self.show_grid.set(gridlines)
self.redraw_canvases(dimensions)
# get background configurations
for canvas_config in config.get("canvases", []):
canvas_id = canvas_config.get("id")
if canvas_id is None:
logging.error("canvas config id not provided")
continue
canvas = self.get(canvas_id)
canvas.parse_metadata(canvas_config)
def add_core_node(self, core_node: Node) -> None:
# get canvas tab for node
canvas_id = core_node.canvas if core_node.canvas > 0 else 1
logging.info("adding core node canvas(%s): %s", core_node.name, canvas_id)
canvas = self.get(canvas_id)
image = nutils.get_icon(core_node, self.app)
x = core_node.position.x
y = core_node.position.y
node = CanvasNode(self.app, canvas, x, y, core_node, image)
canvas.nodes[node.id] = node
self.core.set_canvas_node(core_node, node)
def set_throughputs(self, throughputs_event: ThroughputsEvent):
for iface_throughput in throughputs_event.iface_throughputs:
node_id = iface_throughput.node_id
iface_id = iface_throughput.iface_id
throughput = iface_throughput.throughput
iface_to_edge_id = (node_id, iface_id)
edge = self.core.iface_to_edge.get(iface_to_edge_id)
if edge:
edge.set_throughput(throughput)
def clear_throughputs(self) -> None:
for edge in self.edges.values():
edge.clear_throughput()
def stopped_session(self) -> None:
# clear wireless edges
for edge in self.wireless_edges.values():
edge.delete()
self.wireless_edges.clear()
self.clear_throughputs()
def update_wired_edge(self, link: Link) -> None:
token = create_edge_token(link)
edge = self.edges.get(token)
if edge:
edge.link.options = deepcopy(link.options)
edge.draw_link_options()
edge.check_visibility()
def delete_wired_edge(self, link: Link) -> None:
token = create_edge_token(link)
edge = self.edges.get(token)
if edge:
edge.delete()
def add_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None:
token = create_edge_token(link)
if token in self.edges and link.options.unidirectional:
edge = self.edges[token]
edge.asymmetric_link = link
elif token not in self.edges:
edge = CanvasEdge(self.app, src, dst)
edge.complete(dst, link)
def add_wireless_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None:
network_id = link.network_id if link.network_id else None
token = create_wireless_token(src.id, dst.id, network_id)
if token in self.wireless_edges:
logging.warning("ignoring link that already exists: %s", link)
return
edge = CanvasWirelessEdge(self.app, src, dst, network_id, token, link)
self.wireless_edges[token] = edge
def delete_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: Link
) -> None:
network_id = link.network_id if link.network_id else None
token = create_wireless_token(src.id, dst.id, network_id)
if token not in self.wireless_edges:
return
edge = self.wireless_edges.pop(token)
edge.delete()
def update_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: Link
) -> None:
if not link.label:
return
network_id = link.network_id if link.network_id else None
token = create_wireless_token(src.id, dst.id, network_id)
if token not in self.wireless_edges:
self.add_wireless_edge(src, dst, link)
else:
edge = self.wireless_edges[token]
edge.middle_label_text(link.label)

View file

@ -2,13 +2,16 @@ import functools
import logging
import tkinter as tk
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Set
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
import grpc
from PIL.ImageTk import PhotoImage
from core.api.grpc.services_pb2 import ServiceAction
from core.api.grpc.wrappers import Interface, Node, NodeType
from core.gui import nodeutils, themes
from core.gui import images
from core.gui import nodeutils as nutils
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
@ -19,8 +22,7 @@ from core.gui.frames.node import NodeInfoFrame
from core.gui.graph import tags
from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge
from core.gui.graph.tooltip import CanvasTooltip
from core.gui.images import ImageEnum, Images
from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils
from core.gui.images import ImageEnum
if TYPE_CHECKING:
from core.gui.app import Application
@ -31,10 +33,16 @@ NODE_TEXT_OFFSET: int = 5
class CanvasNode:
def __init__(
self, app: "Application", x: float, y: float, core_node: Node, image: PhotoImage
self,
app: "Application",
canvas: "CanvasGraph",
x: float,
y: float,
core_node: Node,
image: PhotoImage,
):
self.app: "Application" = app
self.canvas: "CanvasGraph" = app.canvas
self.canvas: "CanvasGraph" = canvas
self.image: PhotoImage = image
self.core_node: Node = core_node
self.id: int = self.canvas.create_image(
@ -49,7 +57,7 @@ class CanvasNode:
tags=tags.NODE_LABEL,
font=self.app.icon_text_font,
fill="#0000CD",
state=self.canvas.show_node_labels.state(),
state=self.app.manager.show_node_labels.state(),
)
self.tooltip: CanvasTooltip = CanvasTooltip(self.canvas)
self.edges: Set[CanvasEdge] = set()
@ -57,10 +65,14 @@ class CanvasNode:
self.wireless_edges: Set[CanvasWirelessEdge] = set()
self.antennas: List[int] = []
self.antenna_images: Dict[int, PhotoImage] = {}
self.hidden: bool = False
self.setup_bindings()
self.context: tk.Menu = tk.Menu(self.canvas)
themes.style_menu(self.context)
def position(self) -> Tuple[int, int]:
return self.canvas.coords(self.id)
def next_iface_id(self) -> int:
i = 0
while i in self.ifaces:
@ -81,9 +93,9 @@ class CanvasNode:
self.delete_antennas()
def add_antenna(self) -> None:
x, y = self.canvas.coords(self.id)
x, y = self.position()
offset = len(self.antennas) * 8 * self.app.app_scale
img = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE)
img = self.app.get_enum_icon(ImageEnum.ANTENNA, width=images.ANTENNA_SIZE)
antenna_id = self.canvas.create_image(
x - 16 + offset,
y - int(23 * self.app.app_scale),
@ -139,15 +151,14 @@ class CanvasNode:
def move(self, x: float, y: float) -> None:
x, y = self.canvas.get_scaled_coords(x, y)
current_x, current_y = self.canvas.coords(self.id)
current_x, current_y = self.position()
x_offset = x - current_x
y_offset = y - current_y
self.motion(x_offset, y_offset, update=False)
def motion(self, x_offset: float, y_offset: float, update: bool = True) -> None:
original_position = self.canvas.coords(self.id)
original_position = self.position()
self.canvas.move(self.id, x_offset, y_offset)
pos = self.canvas.coords(self.id)
# check new position
bbox = self.canvas.bbox(self.id)
@ -165,11 +176,12 @@ class CanvasNode:
# move edges
for edge in self.edges:
edge.move_node(self.id, pos)
edge.move_node(self)
for edge in self.wireless_edges:
edge.move_node(self.id, pos)
edge.move_node(self)
# set actual coords for node and update core is running
pos = self.position()
real_x, real_y = self.canvas.get_actual_coords(*pos)
self.core_node.position.x = real_x
self.core_node.position.y = real_y
@ -179,7 +191,7 @@ class CanvasNode:
def on_enter(self, event: tk.Event) -> None:
is_runtime = self.app.core.is_runtime()
has_observer = self.app.core.observer is not None
is_container = NodeUtils.is_container_node(self.core_node.type)
is_container = nutils.is_container(self.core_node)
if is_runtime and has_observer and is_container:
self.tooltip.text.set("waiting...")
self.tooltip.on_enter(event)
@ -194,7 +206,7 @@ class CanvasNode:
def double_click(self, event: tk.Event) -> None:
if self.app.core.is_runtime():
if NodeUtils.is_container_node(self.core_node.type):
if nutils.is_container(self.core_node):
self.canvas.core.launch_terminal(self.core_node.id)
else:
self.show_config()
@ -222,9 +234,25 @@ class CanvasNode:
self.context.add_command(
label="Mobility Player", command=self.show_mobility_player
)
if nutils.is_container(self.core_node):
services_menu = tk.Menu(self.context)
for service in sorted(self.core_node.services):
service_menu = tk.Menu(services_menu)
themes.style_menu(service_menu)
start_func = functools.partial(self.start_service, service)
service_menu.add_command(label="Start", command=start_func)
stop_func = functools.partial(self.stop_service, service)
service_menu.add_command(label="Stop", command=stop_func)
restart_func = functools.partial(self.restart_service, service)
service_menu.add_command(label="Restart", command=restart_func)
validate_func = functools.partial(self.validate_service, service)
service_menu.add_command(label="Validate", command=validate_func)
services_menu.add_cascade(label=service, menu=service_menu)
themes.style_menu(services_menu)
self.context.add_cascade(label="Services", menu=services_menu)
else:
self.context.add_command(label="Configure", command=self.show_config)
if NodeUtils.is_container_node(self.core_node.type):
if nutils.is_container(self.core_node):
self.context.add_command(label="Services", command=self.show_services)
self.context.add_command(
label="Config Services", command=self.show_config_services
@ -241,31 +269,44 @@ class CanvasNode:
self.context.add_command(
label="Mobility Config", command=self.show_mobility_config
)
if NodeUtils.is_wireless_node(self.core_node.type):
if nutils.is_wireless(self.core_node):
self.context.add_command(
label="Link To Selected", command=self.wireless_link_selected
)
link_menu = tk.Menu(self.context)
for canvas in self.app.manager.all():
canvas_menu = tk.Menu(link_menu)
themes.style_menu(canvas_menu)
for node in canvas.nodes.values():
if not self.is_linkable(node):
continue
func_link = functools.partial(self.click_link, node)
canvas_menu.add_command(
label=node.core_node.name, command=func_link
)
link_menu.add_cascade(label=f"Canvas {canvas.id}", menu=canvas_menu)
themes.style_menu(link_menu)
self.context.add_cascade(label="Link", menu=link_menu)
unlink_menu = tk.Menu(self.context)
for edge in self.edges:
link = edge.link
if self.id == edge.src:
other_id = edge.dst
other_iface = link.iface2.name if link.iface2 else None
else:
other_id = edge.src
other_iface = link.iface1.name if link.iface1 else None
other_node = self.canvas.nodes[other_id]
other_name = other_node.core_node.name
label = f"{other_name}:{other_iface}" if other_iface else other_name
other_node = edge.other_node(self)
other_iface = edge.other_iface(self)
label = other_node.core_node.name
if other_iface:
label = f"{label}:{other_iface.name}"
func_unlink = functools.partial(self.click_unlink, edge)
unlink_menu.add_command(label=label, command=func_unlink)
themes.style_menu(unlink_menu)
self.context.add_cascade(label="Unlink", menu=unlink_menu)
edit_menu = tk.Menu(self.context)
themes.style_menu(edit_menu)
edit_menu.add_command(label="Cut", command=self.click_cut)
edit_menu.add_command(label="Copy", command=self.canvas_copy)
edit_menu.add_command(label="Delete", command=self.canvas_delete)
edit_menu.add_command(label="Hide", command=self.click_hide)
self.context.add_cascade(label="Edit", menu=edit_menu)
self.context.tk_popup(event.x_root, event.y_root)
@ -273,10 +314,18 @@ class CanvasNode:
self.canvas_copy()
self.canvas_delete()
def click_hide(self) -> None:
self.canvas.clear_selection()
self.hide()
def click_unlink(self, edge: CanvasEdge) -> None:
self.canvas.delete_edge(edge)
edge.delete()
self.app.default_info()
def click_link(self, node: "CanvasNode") -> None:
edge = CanvasEdge(self.app, self, node)
edge.complete(node)
def canvas_delete(self) -> None:
self.canvas.clear_selection()
self.canvas.select_object(self.id)
@ -320,15 +369,11 @@ class CanvasNode:
def has_emane_link(self, iface_id: int) -> Node:
result = None
for edge in self.edges:
if self.id == edge.src:
other_id = edge.dst
edge_iface_id = edge.link.iface1.id
else:
other_id = edge.src
edge_iface_id = edge.link.iface2.id
other_node = edge.other_node(self)
iface = edge.iface(self)
edge_iface_id = iface.id if iface else None
if edge_iface_id != iface_id:
continue
other_node = self.canvas.nodes[other_id]
if other_node.core_node.type == NodeType.EMANE:
result = other_node.core_node
break
@ -344,7 +389,7 @@ class CanvasNode:
def scale_antennas(self) -> None:
for i in range(len(self.antennas)):
antenna_id = self.antennas[i]
image = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE)
image = self.app.get_enum_icon(ImageEnum.ANTENNA, width=images.ANTENNA_SIZE)
self.canvas.itemconfig(antenna_id, image=image)
self.antenna_images[antenna_id] = image
node_x, node_y = self.canvas.coords(self.id)
@ -358,5 +403,166 @@ class CanvasNode:
logging.error(f"node icon does not exist: {icon_path}")
return
self.core_node.icon = icon_path
self.image = Images.create(icon_path, nodeutils.ICON_SIZE)
self.image = images.from_file(icon_path, width=images.NODE_SIZE)
self.canvas.itemconfig(self.id, image=self.image)
def is_linkable(self, node: "CanvasNode") -> bool:
# cannot link to self
if self == node:
return False
# rj45 nodes can only support one link
if nutils.is_rj45(self.core_node) and self.edges:
return False
if nutils.is_rj45(node.core_node) and node.edges:
return False
# only 1 link between bridge based nodes
is_src_bridge = nutils.is_bridge(self.core_node)
is_dst_bridge = nutils.is_bridge(node.core_node)
common_links = self.edges & node.edges
if all([is_src_bridge, is_dst_bridge, common_links]):
return False
# valid link
return True
def hide(self) -> None:
self.hidden = True
self.canvas.itemconfig(self.id, state=tk.HIDDEN)
self.canvas.itemconfig(self.text_id, state=tk.HIDDEN)
for antenna in self.antennas:
self.canvas.itemconfig(antenna, state=tk.HIDDEN)
for edge in self.edges:
if not edge.hidden:
edge.hide()
for edge in self.wireless_edges:
if not edge.hidden:
edge.hide()
def show(self) -> None:
self.hidden = False
self.canvas.itemconfig(self.id, state=tk.NORMAL)
state = self.app.manager.show_node_labels.state()
self.set_label(state)
for antenna in self.antennas:
self.canvas.itemconfig(antenna, state=tk.NORMAL)
for edge in self.edges:
other_node = edge.other_node(self)
if edge.hidden and not other_node.hidden:
edge.show()
for edge in self.wireless_edges:
other_node = edge.other_node(self)
if edge.hidden and not other_node.hidden:
edge.show()
def set_label(self, state: str) -> None:
self.canvas.itemconfig(self.text_id, state=state)
def _service_action(self, service: str, action: ServiceAction) -> None:
session_id = self.app.core.session.id
try:
response = self.app.core.client.service_action(
session_id, self.core_node.id, service, action
)
if not response.result:
self.app.show_error("Service Action Error", "Action Failed!")
except grpc.RpcError as e:
self.app.show_grpc_exception("Service Error", e)
def start_service(self, service: str) -> None:
self._service_action(service, ServiceAction.START)
def stop_service(self, service: str) -> None:
self._service_action(service, ServiceAction.STOP)
def restart_service(self, service: str) -> None:
self._service_action(service, ServiceAction.RESTART)
def validate_service(self, service: str) -> None:
self._service_action(service, ServiceAction.VALIDATE)
def is_wireless(self) -> bool:
return nutils.is_wireless(self.core_node)
class ShadowNode:
def __init__(
self, app: "Application", canvas: "CanvasGraph", node: "CanvasNode"
) -> None:
self.app: "Application" = app
self.canvas: "CanvasGraph" = canvas
self.node: "CanvasNode" = node
self.id: Optional[int] = None
self.text_id: Optional[int] = None
self.image: PhotoImage = self.app.get_enum_icon(
ImageEnum.SHADOW, width=images.NODE_SIZE
)
self.draw()
self.setup_bindings()
def setup_bindings(self) -> None:
self.canvas.tag_bind(self.id, "<Double-Button-1>", self.node.double_click)
self.canvas.tag_bind(self.id, "<Enter>", self.node.on_enter)
self.canvas.tag_bind(self.id, "<Leave>", self.node.on_leave)
self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.node.show_context)
self.canvas.tag_bind(self.id, "<Button-1>", self.node.show_info)
def draw(self) -> None:
x, y = self.node.position()
self.id: int = self.canvas.create_image(
x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE
)
self.text_id = self.canvas.create_text(
x,
y + 20,
text=f"{self.node.get_label()} [{self.node.canvas.id}]",
tags=tags.NODE_LABEL,
font=self.app.icon_text_font,
fill="#0000CD",
state=self.app.manager.show_node_labels.state(),
justify=tk.CENTER,
)
self.canvas.shadow_nodes[self.id] = self
self.canvas.shadow_core_nodes[self.node.core_node.id] = self
def position(self) -> Tuple[int, int]:
return self.canvas.coords(self.id)
def should_delete(self) -> bool:
for edge in self.node.edges:
other_node = edge.other_node(self.node)
if not other_node.is_wireless() and other_node.canvas == self.canvas:
return False
return True
def motion(self, x_offset, y_offset) -> None:
original_position = self.position()
self.canvas.move(self.id, x_offset, y_offset)
# 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 text and selection box
self.canvas.move(self.text_id, x_offset, y_offset)
self.canvas.move_selection(self.id, x_offset, y_offset)
# move edges
for edge in self.node.edges:
edge.move_shadow(self)
for edge in self.node.wireless_edges:
edge.move_shadow(self)
def delete(self):
self.canvas.shadow_nodes.pop(self.id, None)
self.canvas.shadow_core_nodes.pop(self.node.core_node.id, None)
self.canvas.delete(self.id)
self.canvas.delete(self.text_id)
def hide(self) -> None:
self.canvas.itemconfig(self.id, state=tk.HIDDEN)
self.canvas.itemconfig(self.text_id, state=tk.HIDDEN)
def show(self) -> None:
self.canvas.itemconfig(self.id, state=tk.NORMAL)
self.canvas.itemconfig(self.text_id, state=tk.NORMAL)

View file

@ -1,5 +1,5 @@
import logging
from typing import TYPE_CHECKING, Dict, List, Optional, Union
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
from core.gui.dialogs.shapemod import ShapeDialog
from core.gui.graph import tags
@ -69,6 +69,31 @@ class Shape:
self.shape_data = data
self.draw()
@classmethod
def from_metadata(cls, app: "Application", config: Dict[str, Any]) -> None:
shape_type = config["type"]
try:
shape_type = ShapeType(shape_type)
coords = config["iconcoords"]
data = AnnotationData(
config["label"],
config["fontfamily"],
config["fontsize"],
config["labelcolor"],
config["color"],
config["border"],
config["width"],
config["bold"],
config["italic"],
config["underline"],
)
canvas_id = config.get("canvas", 1)
canvas = app.manager.get(canvas_id)
shape = Shape(app, canvas, shape_type, *coords, data=data)
canvas.shapes[shape.id] = shape
except ValueError:
logging.exception("unknown shape: %s", shape_type)
def draw(self) -> None:
if self.created:
dash = None
@ -85,7 +110,7 @@ class Shape:
fill=self.shape_data.fill_color,
outline=self.shape_data.border_color,
width=self.shape_data.border_width,
state=self.canvas.show_annotations.state(),
state=self.app.manager.show_annotations.state(),
)
self.draw_shape_text()
elif self.shape_type == ShapeType.RECTANGLE:
@ -99,7 +124,7 @@ class Shape:
fill=self.shape_data.fill_color,
outline=self.shape_data.border_color,
width=self.shape_data.border_width,
state=self.canvas.show_annotations.state(),
state=self.app.manager.show_annotations.state(),
)
self.draw_shape_text()
elif self.shape_type == ShapeType.TEXT:
@ -111,7 +136,7 @@ class Shape:
text=self.shape_data.text,
fill=self.shape_data.text_color,
font=font,
state=self.canvas.show_annotations.state(),
state=self.app.manager.show_annotations.state(),
)
else:
logging.error("unknown shape type: %s", self.shape_type)
@ -139,7 +164,7 @@ class Shape:
text=self.shape_data.text,
fill=self.shape_data.text_color,
font=font,
state=self.canvas.show_annotations.state(),
state=self.app.manager.show_annotations.state(),
)
def shape_motion(self, x1: float, y1: float) -> None:
@ -184,6 +209,7 @@ class Shape:
x1, y1 = self.canvas.get_actual_coords(x1, y1)
coords = (x1, y1)
return {
"canvas": self.canvas.id,
"type": self.shape_type.value,
"iconcoords": coords,
"label": self.shape_data.text,

View file

@ -14,6 +14,7 @@ NODE: str = "node"
WALLPAPER: str = "wallpaper"
SELECTION: str = "selectednodes"
MARKER: str = "marker"
HIDDEN: str = "hidden"
ORGANIZE_TAGS: List[str] = [
WALLPAPER,
GRIDLINE,

View file

@ -1,53 +1,46 @@
from enum import Enum
from tkinter import messagebox
from typing import Dict, Optional, Tuple
from PIL import Image
from PIL.ImageTk import PhotoImage
from core.api.grpc.wrappers import NodeType
from core.api.grpc.wrappers import Node, NodeType
from core.gui.appconfig import LOCAL_ICONS_PATH
NODE_SIZE: int = 48
ANTENNA_SIZE: int = 32
BUTTON_SIZE: int = 16
ERROR_SIZE: int = 24
DIALOG_SIZE: int = 16
IMAGES: Dict[str, str] = {}
class Images:
images: Dict[str, str] = {}
@classmethod
def create(cls, file_path: str, width: int, height: int = None) -> PhotoImage:
if height is None:
height = width
image = Image.open(file_path)
image = image.resize((width, height), Image.ANTIALIAS)
return PhotoImage(image)
@classmethod
def load_all(cls) -> None:
for image in LOCAL_ICONS_PATH.glob("*"):
cls.images[image.stem] = str(image)
@classmethod
def get(cls, image_enum: Enum, width: int, height: int = None) -> PhotoImage:
file_path = cls.images[image_enum.value]
return cls.create(file_path, width, height)
@classmethod
def get_with_image_file(
cls, stem: str, width: int, height: int = None
) -> PhotoImage:
file_path = cls.images[stem]
return cls.create(file_path, width, height)
@classmethod
def get_custom(cls, name: str, width: int, height: int = None) -> PhotoImage:
def load_all() -> None:
for image in LOCAL_ICONS_PATH.glob("*"):
try:
file_path = cls.images[name]
return cls.create(file_path, width, height)
except KeyError:
messagebox.showwarning(
"Missing image file",
f"{name}.png is missing at daemon/core/gui/data/icons, drop image "
f"file at daemon/core/gui/data/icons and restart the gui",
)
ImageEnum(image.stem)
IMAGES[image.stem] = str(image)
except ValueError:
pass
def from_file(
file_path: str, *, width: int, height: int = None, scale: float = 1.0
) -> PhotoImage:
if height is None:
height = width
width = int(width * scale)
height = int(height * scale)
image = Image.open(file_path)
image = image.resize((width, height), Image.ANTIALIAS)
return PhotoImage(image)
def from_enum(
image_enum: "ImageEnum", *, width: int, height: int = None, scale: float = 1.0
) -> PhotoImage:
file_path = IMAGES[image_enum.value]
return from_file(file_path, width=width, height=height, scale=scale)
class ImageEnum(Enum):
@ -90,25 +83,29 @@ class ImageEnum(Enum):
SHUTDOWN = "shutdown"
CANCEL = "cancel"
ERROR = "error"
SHADOW = "shadow"
class TypeToImage:
type_to_image: Dict[Tuple[NodeType, str], ImageEnum] = {
(NodeType.DEFAULT, "router"): ImageEnum.ROUTER,
(NodeType.DEFAULT, "PC"): ImageEnum.PC,
(NodeType.DEFAULT, "host"): ImageEnum.HOST,
(NodeType.DEFAULT, "mdr"): ImageEnum.MDR,
(NodeType.DEFAULT, "prouter"): ImageEnum.PROUTER,
(NodeType.HUB, ""): ImageEnum.HUB,
(NodeType.SWITCH, ""): ImageEnum.SWITCH,
(NodeType.WIRELESS_LAN, ""): ImageEnum.WLAN,
(NodeType.EMANE, ""): ImageEnum.EMANE,
(NodeType.RJ45, ""): ImageEnum.RJ45,
(NodeType.TUNNEL, ""): ImageEnum.TUNNEL,
(NodeType.DOCKER, ""): ImageEnum.DOCKER,
(NodeType.LXC, ""): ImageEnum.LXC,
}
TYPE_MAP: Dict[Tuple[NodeType, str], ImageEnum] = {
(NodeType.DEFAULT, "router"): ImageEnum.ROUTER,
(NodeType.DEFAULT, "PC"): ImageEnum.PC,
(NodeType.DEFAULT, "host"): ImageEnum.HOST,
(NodeType.DEFAULT, "mdr"): ImageEnum.MDR,
(NodeType.DEFAULT, "prouter"): ImageEnum.PROUTER,
(NodeType.HUB, ""): ImageEnum.HUB,
(NodeType.SWITCH, ""): ImageEnum.SWITCH,
(NodeType.WIRELESS_LAN, ""): ImageEnum.WLAN,
(NodeType.EMANE, ""): ImageEnum.EMANE,
(NodeType.RJ45, ""): ImageEnum.RJ45,
(NodeType.TUNNEL, ""): ImageEnum.TUNNEL,
(NodeType.DOCKER, ""): ImageEnum.DOCKER,
(NodeType.LXC, ""): ImageEnum.LXC,
}
@classmethod
def get(cls, node_type, model) -> Optional[ImageEnum]:
return cls.type_to_image.get((node_type, model))
def from_node(node: Node, *, scale: float) -> Optional[PhotoImage]:
image = None
image_enum = TYPE_MAP.get((node.type, node.model))
if image_enum:
image = from_enum(image_enum, width=NODE_SIZE, scale=scale)
return image

View file

@ -4,13 +4,19 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
import netaddr
from netaddr import EUI, IPNetwork
from core.api.grpc.wrappers import Interface, Link, Node
from core.api.grpc.wrappers import Interface, Link, LinkType, Node
from core.gui import nodeutils as nutils
from core.gui.graph.edges import CanvasEdge
from core.gui.graph.node import CanvasNode
from core.gui.nodeutils import NodeUtils
if TYPE_CHECKING:
from core.gui.app import Application
IP4_MASK: int = 24
IP6_MASK: int = 64
WIRELESS_IP4_MASK: int = 32
WIRELESS_IP6_MASK: int = 128
def get_index(iface: Interface) -> Optional[int]:
if not iface.ip4:
@ -47,25 +53,24 @@ class InterfaceManager:
self.app: "Application" = app
ip4 = self.app.guiconfig.ips.ip4
ip6 = self.app.guiconfig.ips.ip6
self.ip4_mask: int = 24
self.ip6_mask: int = 64
self.ip4_subnets: IPNetwork = IPNetwork(f"{ip4}/{self.ip4_mask}")
self.ip6_subnets: IPNetwork = IPNetwork(f"{ip6}/{self.ip6_mask}")
self.ip4_subnets: IPNetwork = IPNetwork(f"{ip4}/{IP4_MASK}")
self.ip6_subnets: IPNetwork = IPNetwork(f"{ip6}/{IP6_MASK}")
mac = self.app.guiconfig.mac
self.mac: EUI = EUI(mac, dialect=netaddr.mac_unix_expanded)
self.current_mac: Optional[EUI] = None
self.current_subnets: Optional[Subnets] = None
self.used_subnets: Dict[Tuple[IPNetwork, IPNetwork], Subnets] = {}
self.used_macs: Set[str] = set()
def update_ips(self, ip4: str, ip6: str) -> None:
self.reset()
self.ip4_subnets = IPNetwork(f"{ip4}/{self.ip4_mask}")
self.ip6_subnets = IPNetwork(f"{ip6}/{self.ip6_mask}")
def reset_mac(self) -> None:
self.current_mac = self.mac
self.ip4_subnets = IPNetwork(f"{ip4}/{IP4_MASK}")
self.ip6_subnets = IPNetwork(f"{ip6}/{IP6_MASK}")
def next_mac(self) -> str:
while str(self.current_mac) in self.used_macs:
value = self.current_mac.value + 1
self.current_mac = EUI(value, dialect=netaddr.mac_unix_expanded)
mac = str(self.current_mac)
value = self.current_mac.value + 1
self.current_mac = EUI(value, dialect=netaddr.mac_unix_expanded)
@ -114,6 +119,15 @@ class InterfaceManager:
subnets.used_indexes.discard(index)
self.current_subnets = None
def set_macs(self, links: List[Link]) -> None:
self.current_mac = self.mac
self.used_macs.clear()
for link in links:
if link.iface1:
self.used_macs.add(link.iface1.mac)
if link.iface2:
self.used_macs.add(link.iface2.mac)
def joined(self, links: List[Link]) -> None:
ifaces = []
for link in links:
@ -133,7 +147,7 @@ class InterfaceManager:
self.used_subnets[subnets.key()] = subnets
def next_index(self, node: Node) -> int:
if NodeUtils.is_router_node(node):
if nutils.is_router(node):
index = 1
else:
index = 20
@ -153,10 +167,10 @@ class InterfaceManager:
def get_subnets(self, iface: Interface) -> Subnets:
ip4_subnet = self.ip4_subnets
if iface.ip4:
ip4_subnet = IPNetwork(f"{iface.ip4}/{iface.ip4_mask}").cidr
ip4_subnet = IPNetwork(f"{iface.ip4}/{IP4_MASK}").cidr
ip6_subnet = self.ip6_subnets
if iface.ip6:
ip6_subnet = IPNetwork(f"{iface.ip6}/{iface.ip6_mask}").cidr
ip6_subnet = IPNetwork(f"{iface.ip6}/{IP6_MASK}").cidr
subnets = Subnets(ip4_subnet, ip6_subnet)
return self.used_subnets.get(subnets.key(), subnets)
@ -165,8 +179,8 @@ class InterfaceManager:
) -> None:
src_node = canvas_src_node.core_node
dst_node = canvas_dst_node.core_node
is_src_container = NodeUtils.is_container_node(src_node.type)
is_dst_container = NodeUtils.is_container_node(dst_node.type)
is_src_container = nutils.is_container(src_node)
is_dst_container = nutils.is_container(dst_node)
if is_src_container and is_dst_container:
self.current_subnets = self.next_subnets()
elif is_src_container and not is_dst_container:
@ -188,19 +202,16 @@ class InterfaceManager:
self, canvas_node: CanvasNode, visited: Set[int] = None
) -> Optional[IPNetwork]:
logging.info("finding subnet for node: %s", canvas_node.core_node.name)
canvas = self.app.canvas
subnets = 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]
dst_node = canvas.nodes[edge.dst]
iface = edge.link.iface1
check_node = src_node
if src_node == canvas_node:
check_node = edge.src
if edge.src == canvas_node:
iface = edge.link.iface2
check_node = dst_node
check_node = edge.dst
if check_node.core_node.id in visited:
continue
visited.add(check_node.core_node.id)
@ -212,3 +223,48 @@ class InterfaceManager:
logging.info("found subnets: %s", subnets)
break
return subnets
def create_link(self, edge: CanvasEdge) -> Link:
"""
Create core link for a given edge based on src/dst nodes.
"""
src_node = edge.src.core_node
dst_node = edge.dst.core_node
self.determine_subnets(edge.src, edge.dst)
src_iface = None
if nutils.is_container(src_node):
src_iface = self.create_iface(edge.src, edge.linked_wireless)
dst_iface = None
if nutils.is_container(dst_node):
dst_iface = self.create_iface(edge.dst, edge.linked_wireless)
link = Link(
type=LinkType.WIRED,
node1_id=src_node.id,
node2_id=dst_node.id,
iface1=src_iface,
iface2=dst_iface,
)
logging.info("added link between %s and %s", src_node.name, dst_node.name)
return link
def create_iface(self, canvas_node: CanvasNode, wireless_link: bool) -> Interface:
node = canvas_node.core_node
ip4, ip6 = self.get_ips(node)
if wireless_link:
ip4_mask = WIRELESS_IP4_MASK
ip6_mask = WIRELESS_IP6_MASK
else:
ip4_mask = IP4_MASK
ip6_mask = IP6_MASK
iface_id = canvas_node.next_iface_id()
name = f"eth{iface_id}"
iface = Interface(
id=iface_id,
name=name,
ip4=ip4,
ip4_mask=ip4_mask,
ip6=ip6,
ip6_mask=ip6_mask,
)
logging.info("create node(%s) interface(%s)", node.name, iface)
return iface

View file

@ -6,6 +6,7 @@ from functools import partial
from tkinter import filedialog, messagebox
from typing import TYPE_CHECKING, Optional
from core.gui import images
from core.gui.coreclient import CoreClient
from core.gui.dialogs.about import AboutDialog
from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog
@ -22,8 +23,7 @@ from core.gui.dialogs.servers import ServersDialog
from core.gui.dialogs.sessionoptions import SessionOptionsDialog
from core.gui.dialogs.sessions import SessionsDialog
from core.gui.dialogs.throughput import ThroughputDialog
from core.gui.graph.graph import CanvasGraph
from core.gui.nodeutils import ICON_SIZE
from core.gui.graph.manager import CanvasManager
from core.gui.observers import ObserversMenu
from core.gui.task import ProgressTask
@ -45,9 +45,10 @@ class Menubar(tk.Menu):
super().__init__(app)
self.app: "Application" = app
self.core: CoreClient = app.core
self.canvas: CanvasGraph = app.canvas
self.manager: CanvasManager = app.manager
self.recent_menu: Optional[tk.Menu] = None
self.edit_menu: Optional[tk.Menu] = None
self.canvas_menu: Optional[tk.Menu] = None
self.observers_menu: Optional[ObserversMenu] = None
self.draw()
@ -106,6 +107,7 @@ class Menubar(tk.Menu):
menu = tk.Menu(self)
menu.add_command(label="Preferences", command=self.click_preferences)
menu.add_command(label="Custom Nodes", command=self.click_custom_nodes)
menu.add_command(label="Show Hidden Nodes", command=self.click_show_hidden)
menu.add_separator()
menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED)
menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED)
@ -116,11 +118,13 @@ class Menubar(tk.Menu):
menu.add_command(
label="Delete", accelerator="Ctrl+D", command=self.click_delete
)
menu.add_command(label="Hide", accelerator="Ctrl+H", command=self.click_hide)
self.add_cascade(label="Edit", menu=menu)
self.app.master.bind_all("<Control-x>", self.click_cut)
self.app.master.bind_all("<Control-c>", self.click_copy)
self.app.master.bind_all("<Control-v>", self.click_paste)
self.app.master.bind_all("<Control-d>", self.click_delete)
self.app.master.bind_all("<Control-h>", self.click_hide)
self.edit_menu = menu
def draw_canvas_menu(self) -> None:
@ -128,9 +132,13 @@ class Menubar(tk.Menu):
Create canvas menu
"""
menu = tk.Menu(self)
menu.add_command(label="New", command=self.click_canvas_add)
menu.add_command(label="Size / Scale", command=self.click_canvas_size_and_scale)
menu.add_separator()
menu.add_command(label="Delete", command=self.click_canvas_delete)
menu.add_command(label="Wallpaper", command=self.click_canvas_wallpaper)
self.add_cascade(label="Canvas", menu=menu)
self.canvas_menu = menu
def draw_view_menu(self) -> None:
"""
@ -145,52 +153,52 @@ class Menubar(tk.Menu):
menu.add_checkbutton(
label="Interface Names",
command=self.click_edge_label_change,
variable=self.canvas.show_iface_names,
variable=self.manager.show_iface_names,
)
menu.add_checkbutton(
label="IPv4 Addresses",
command=self.click_edge_label_change,
variable=self.canvas.show_ip4s,
variable=self.manager.show_ip4s,
)
menu.add_checkbutton(
label="IPv6 Addresses",
command=self.click_edge_label_change,
variable=self.canvas.show_ip6s,
variable=self.manager.show_ip6s,
)
menu.add_checkbutton(
label="Node Labels",
command=self.canvas.show_node_labels.click_handler,
variable=self.canvas.show_node_labels,
command=self.manager.show_node_labels.click_handler,
variable=self.manager.show_node_labels,
)
menu.add_checkbutton(
label="Link Labels",
command=self.canvas.show_link_labels.click_handler,
variable=self.canvas.show_link_labels,
command=self.manager.show_link_labels.click_handler,
variable=self.manager.show_link_labels,
)
menu.add_checkbutton(
label="Links",
command=self.canvas.show_links.click_handler,
variable=self.canvas.show_links,
command=self.manager.show_links.click_handler,
variable=self.manager.show_links,
)
menu.add_checkbutton(
label="Loss Links",
command=self.canvas.show_loss_links.click_handler,
variable=self.canvas.show_loss_links,
command=self.manager.show_loss_links.click_handler,
variable=self.manager.show_loss_links,
)
menu.add_checkbutton(
label="Wireless Links",
command=self.canvas.show_wireless.click_handler,
variable=self.canvas.show_wireless,
command=self.manager.show_wireless.click_handler,
variable=self.manager.show_wireless,
)
menu.add_checkbutton(
label="Annotations",
command=self.canvas.show_annotations.click_handler,
variable=self.canvas.show_annotations,
command=self.manager.show_annotations.click_handler,
variable=self.manager.show_annotations,
)
menu.add_checkbutton(
label="Canvas Grid",
command=self.canvas.show_grid.click_handler,
variable=self.canvas.show_grid,
command=self.manager.show_grid.click_handler,
variable=self.manager.show_grid,
)
self.add_cascade(label="View", menu=menu)
@ -334,17 +342,12 @@ class Menubar(tk.Menu):
self.app.save_config()
self.app.menubar.update_recent_files()
def change_menubar_item_state(self, is_runtime: bool) -> None:
labels = {"Copy", "Paste", "Delete", "Cut"}
for i in range(self.edit_menu.index(tk.END) + 1):
try:
label = self.edit_menu.entrycget(i, "label")
if label not in labels:
continue
state = tk.DISABLED if is_runtime else tk.NORMAL
self.edit_menu.entryconfig(i, state=state)
except tk.TclError:
pass
def set_state(self, is_runtime: bool) -> None:
state = tk.DISABLED if is_runtime else tk.NORMAL
for entry in {"Copy", "Paste", "Delete", "Cut"}:
self.edit_menu.entryconfigure(entry, state=state)
for entry in {"Delete"}:
self.canvas_menu.entryconfigure(entry, state=state)
def prompt_save_running_session(self, quit_app: bool = False) -> None:
"""
@ -372,6 +375,12 @@ class Menubar(tk.Menu):
dialog = PreferencesDialog(self.app)
dialog.show()
def click_canvas_add(self) -> None:
self.manager.add_canvas()
def click_canvas_delete(self) -> None:
self.manager.delete_canvas()
def click_canvas_size_and_scale(self) -> None:
dialog = SizeAndScaleDialog(self.app)
dialog.show()
@ -401,17 +410,29 @@ class Menubar(tk.Menu):
dialog.show()
def click_copy(self, _event: tk.Event = None) -> None:
self.canvas.copy()
canvas = self.manager.current()
canvas.copy()
def click_paste(self, _event: tk.Event = None) -> None:
self.canvas.paste()
canvas = self.manager.current()
canvas.paste()
def click_delete(self, _event: tk.Event = None) -> None:
self.canvas.delete_selected_objects()
canvas = self.manager.current()
canvas.delete_selected_objects()
def click_hide(self, _event: tk.Event = None) -> None:
canvas = self.manager.current()
canvas.hide_selected_objects()
def click_cut(self, _event: tk.Event = None) -> None:
self.canvas.copy()
self.canvas.delete_selected_objects()
canvas = self.manager.current()
canvas.copy()
canvas.delete_selected_objects()
def click_show_hidden(self, _event: tk.Event = None) -> None:
for canvas in self.manager.all():
canvas.show_hidden()
def click_session_options(self) -> None:
logging.debug("Click options")
@ -439,14 +460,15 @@ class Menubar(tk.Menu):
dialog.show()
def click_autogrid(self) -> None:
width, height = self.canvas.current_dimensions
padding = (ICON_SIZE / 2) + 10
layout_size = padding + ICON_SIZE
width, height = self.manager.current_dimensions
padding = (images.NODE_SIZE / 2) + 10
layout_size = padding + images.NODE_SIZE
col_count = width // layout_size
logging.info(
"auto grid layout: dimension(%s, %s) col(%s)", width, height, col_count
)
for i, node in enumerate(self.canvas.nodes.values()):
canvas = self.manager.current()
for i, node in enumerate(canvas.nodes.values()):
col = i % col_count
row = i // col_count
x = (col * layout_size) + padding
@ -460,7 +482,7 @@ class Menubar(tk.Menu):
self.app.hide_info()
def click_edge_label_change(self) -> None:
for edge in self.canvas.edges.values():
for edge in self.manager.edges.values():
edge.draw_labels()
def click_mac_config(self) -> None:

View file

@ -1,14 +1,139 @@
import logging
from typing import List, Optional, Set
from typing import TYPE_CHECKING, List, Optional, Set
from PIL.ImageTk import PhotoImage
from core.api.grpc.wrappers import Node, NodeType
from core.gui import images
from core.gui.appconfig import CustomNode, GuiConfig
from core.gui.images import ImageEnum, Images, TypeToImage
from core.gui.images import ImageEnum
ICON_SIZE: int = 48
ANTENNA_SIZE: int = 32
if TYPE_CHECKING:
from core.gui.app import Application
NODES: List["NodeDraw"] = []
NETWORK_NODES: List["NodeDraw"] = []
NODE_ICONS = {}
CONTAINER_NODES: Set[NodeType] = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC}
IMAGE_NODES: Set[NodeType] = {NodeType.DOCKER, NodeType.LXC}
WIRELESS_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE}
RJ45_NODES: Set[NodeType] = {NodeType.RJ45}
BRIDGE_NODES: Set[NodeType] = {NodeType.HUB, NodeType.SWITCH}
IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET}
MOBILITY_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE}
NODE_MODELS: Set[str] = {"router", "host", "PC", "mdr", "prouter"}
ROUTER_NODES: Set[str] = {"router", "mdr"}
ANTENNA_ICON: Optional[PhotoImage] = None
def setup() -> None:
global ANTENNA_ICON
nodes = [
(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, label, model in nodes:
node_draw = NodeDraw.from_setup(image_enum, node_type, label, model)
NODES.append(node_draw)
NODE_ICONS[(node_type, model)] = node_draw.image
network_nodes = [
(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"),
(ImageEnum.TUNNEL, NodeType.TUNNEL, "Tunnel"),
]
for image_enum, node_type, label in network_nodes:
node_draw = NodeDraw.from_setup(image_enum, node_type, label)
NETWORK_NODES.append(node_draw)
NODE_ICONS[(node_type, None)] = node_draw.image
ANTENNA_ICON = images.from_enum(ImageEnum.ANTENNA, width=images.ANTENNA_SIZE)
def is_bridge(node: Node) -> bool:
return node.type in BRIDGE_NODES
def is_mobility(node: Node) -> bool:
return node.type in MOBILITY_NODES
def is_router(node: Node) -> bool:
return is_model(node) and node.model in ROUTER_NODES
def should_ignore(node: Node) -> bool:
return node.type in IGNORE_NODES
def is_container(node: Node) -> bool:
return node.type in CONTAINER_NODES
def is_model(node: Node) -> bool:
return node.type == NodeType.DEFAULT
def has_image(node_type: NodeType) -> bool:
return node_type in IMAGE_NODES
def is_wireless(node: Node) -> bool:
return node.type in WIRELESS_NODES
def is_rj45(node: Node) -> bool:
return node.type in RJ45_NODES
def is_custom(node: Node) -> bool:
return is_model(node) and node.model not in NODE_MODELS
def get_custom_services(gui_config: GuiConfig, name: str) -> List[str]:
for custom_node in gui_config.nodes:
if custom_node.name == name:
return custom_node.services
return []
def _get_custom_file(config: GuiConfig, name: str) -> Optional[str]:
for custom_node in config.nodes:
if custom_node.name == name:
return custom_node.image
return None
def get_icon(node: Node, app: "Application") -> PhotoImage:
scale = app.app_scale
image = None
# node icon was overriden with a specific value
if node.icon:
try:
image = images.from_file(node.icon, width=images.NODE_SIZE, scale=scale)
except OSError:
logging.error("invalid icon: %s", node.icon)
# custom node
elif is_custom(node):
image_file = _get_custom_file(app.guiconfig, node.model)
logging.info("custom node file: %s", image_file)
if image_file:
image = images.from_file(image_file, width=images.NODE_SIZE, scale=scale)
# built in node
else:
image = images.from_node(node, scale=scale)
# default image, if everything above fails
if not image:
image = images.from_enum(
ImageEnum.EDITNODE, width=images.NODE_SIZE, scale=scale
)
return image
class NodeDraw:
@ -33,7 +158,7 @@ class NodeDraw:
) -> "NodeDraw":
node_draw = NodeDraw()
node_draw.image_enum = image_enum
node_draw.image = Images.get(image_enum, ICON_SIZE)
node_draw.image = images.from_enum(image_enum, width=images.NODE_SIZE)
node_draw.node_type = node_type
node_draw.label = label
node_draw.model = model
@ -45,135 +170,10 @@ class NodeDraw:
node_draw = NodeDraw()
node_draw.custom = True
node_draw.image_file = custom_node.image
node_draw.image = Images.get_custom(custom_node.image, ICON_SIZE)
node_draw.image = images.from_file(custom_node.image, width=images.NODE_SIZE)
node_draw.node_type = NodeType.DEFAULT
node_draw.services = custom_node.services
node_draw.label = custom_node.name
node_draw.model = custom_node.name
node_draw.tooltip = custom_node.name
return node_draw
class NodeUtils:
NODES: List[NodeDraw] = []
NETWORK_NODES: List[NodeDraw] = []
NODE_ICONS = {}
CONTAINER_NODES: Set[NodeType] = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC}
IMAGE_NODES: Set[NodeType] = {NodeType.DOCKER, NodeType.LXC}
WIRELESS_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE}
RJ45_NODES: Set[NodeType] = {NodeType.RJ45}
BRIDGE_NODES: Set[NodeType] = {NodeType.HUB, NodeType.SWITCH}
IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET}
MOBILITY_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE}
NODE_MODELS: Set[str] = {"router", "host", "PC", "mdr", "prouter"}
ROUTER_NODES: Set[str] = {"router", "mdr"}
ANTENNA_ICON: PhotoImage = None
@classmethod
def is_bridge_node(cls, node: Node) -> bool:
return node.type in cls.BRIDGE_NODES
@classmethod
def is_mobility(cls, node: Node) -> bool:
return node.type in cls.MOBILITY_NODES
@classmethod
def is_router_node(cls, node: Node) -> bool:
return cls.is_model_node(node.type) and node.model in cls.ROUTER_NODES
@classmethod
def is_ignore_node(cls, node_type: NodeType) -> bool:
return node_type in cls.IGNORE_NODES
@classmethod
def is_container_node(cls, node_type: NodeType) -> bool:
return node_type in cls.CONTAINER_NODES
@classmethod
def is_model_node(cls, node_type: NodeType) -> bool:
return node_type == NodeType.DEFAULT
@classmethod
def is_image_node(cls, node_type: NodeType) -> bool:
return node_type in cls.IMAGE_NODES
@classmethod
def is_wireless_node(cls, node_type: NodeType) -> bool:
return node_type in cls.WIRELESS_NODES
@classmethod
def is_rj45_node(cls, node_type: NodeType) -> bool:
return node_type in cls.RJ45_NODES
@classmethod
def node_icon(
cls, node_type: NodeType, model: str, gui_config: GuiConfig, scale: float = 1.0
) -> PhotoImage:
image_enum = TypeToImage.get(node_type, model)
if image_enum:
return Images.get(image_enum, int(ICON_SIZE * scale))
else:
image_stem = cls.get_image_file(gui_config, model)
if image_stem:
return Images.get_with_image_file(image_stem, int(ICON_SIZE * scale))
@classmethod
def node_image(
cls, core_node: Node, gui_config: GuiConfig, scale: float = 1.0
) -> PhotoImage:
image = cls.node_icon(core_node.type, core_node.model, gui_config, scale)
if core_node.icon:
try:
image = Images.create(core_node.icon, int(ICON_SIZE * scale))
except OSError:
logging.error("invalid icon: %s", core_node.icon)
return image
@classmethod
def is_custom(cls, node_type: NodeType, model: str) -> bool:
return node_type == NodeType.DEFAULT and model not in cls.NODE_MODELS
@classmethod
def get_custom_node_services(cls, gui_config: GuiConfig, name: str) -> List[str]:
for custom_node in gui_config.nodes:
if custom_node.name == name:
return custom_node.services
return []
@classmethod
def get_image_file(cls, gui_config: GuiConfig, name: str) -> Optional[str]:
for custom_node in gui_config.nodes:
if custom_node.name == name:
return custom_node.image
return None
@classmethod
def setup(cls) -> None:
nodes = [
(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, 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, "Hub"),
(ImageEnum.SWITCH, NodeType.SWITCH, "Switch"),
(ImageEnum.WLAN, NodeType.WIRELESS_LAN, "WLAN"),
(ImageEnum.EMANE, NodeType.EMANE, "EMANE"),
(ImageEnum.RJ45, NodeType.RJ45, "RJ45"),
(ImageEnum.TUNNEL, NodeType.TUNNEL, "Tunnel"),
]
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)

View file

@ -48,7 +48,6 @@ class StatusBar(ttk.Frame):
self.zoom = ttk.Label(self, anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE)
self.zoom.grid(row=0, column=1, sticky=tk.EW)
self.set_zoom(self.app.canvas.ratio)
self.cpu_label = ttk.Label(
self, anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE

View file

@ -7,13 +7,14 @@ from typing import TYPE_CHECKING, Callable, List, Optional
from PIL.ImageTk import PhotoImage
from core.gui import nodeutils as nutils
from core.gui.dialogs.colorpicker import ColorPickerDialog
from core.gui.dialogs.runtool import RunToolDialog
from core.gui.graph import tags
from core.gui.graph.enums import GraphMode
from core.gui.graph.shapeutils import ShapeType, is_marker
from core.gui.images import ImageEnum
from core.gui.nodeutils import NodeDraw, NodeUtils
from core.gui.nodeutils import NodeDraw
from core.gui.observers import ObserversMenu
from core.gui.task import ProgressTask
from core.gui.themes import Styles
@ -57,11 +58,11 @@ class PickerFrame(ttk.Frame):
image_file: str = None,
) -> None:
if image_enum:
bar_image = self.app.get_icon(image_enum, TOOLBAR_SIZE)
image = self.app.get_icon(image_enum, PICKER_SIZE)
bar_image = self.app.get_enum_icon(image_enum, width=TOOLBAR_SIZE)
image = self.app.get_enum_icon(image_enum, width=PICKER_SIZE)
else:
bar_image = self.app.get_custom_icon(image_file, TOOLBAR_SIZE)
image = self.app.get_custom_icon(image_file, PICKER_SIZE)
bar_image = self.app.get_file_icon(image_file, width=TOOLBAR_SIZE)
image = self.app.get_file_icon(image_file, width=PICKER_SIZE)
button = ttk.Button(
self, image=image, text=label, compound=tk.TOP, style=Styles.picker_button
)
@ -92,7 +93,7 @@ class ButtonBar(ttk.Frame):
def create_button(
self, image_enum: ImageEnum, func: Callable, tooltip: str, radio: bool = False
) -> ttk.Button:
image = self.app.get_icon(image_enum, TOOLBAR_SIZE)
image = self.app.get_enum_icon(image_enum, width=TOOLBAR_SIZE)
button = ttk.Button(self, image=image, command=func)
button.image = image
button.grid(sticky=tk.EW)
@ -121,7 +122,7 @@ class MarkerFrame(ttk.Frame):
def draw(self) -> None:
self.columnconfigure(0, weight=1)
image = self.app.get_icon(ImageEnum.DELETE, 16)
image = self.app.get_enum_icon(ImageEnum.DELETE, width=16)
button = ttk.Button(self, image=image, width=2, command=self.click_clear)
button.image = image
button.grid(sticky=tk.EW, pady=self.PAD)
@ -144,7 +145,8 @@ class MarkerFrame(ttk.Frame):
Tooltip(self.color_frame, "Marker Color")
def click_clear(self) -> None:
self.app.canvas.delete(tags.MARKER)
canvas = self.app.manager.current()
canvas.delete(tags.MARKER)
def click_color(self, _event: tk.Event) -> None:
dialog = ColorPickerDialog(self.app, self.app, self.color)
@ -189,8 +191,8 @@ class Toolbar(ttk.Frame):
# these variables help keep track of what images being drawn so that scaling
# is possible since PhotoImage does not have resize method
self.current_node: NodeDraw = NodeUtils.NODES[0]
self.current_network: NodeDraw = NodeUtils.NETWORK_NODES[0]
self.current_node: NodeDraw = nutils.NODES[0]
self.current_network: NodeDraw = nutils.NETWORK_NODES[0]
self.current_annotation: ShapeType = ShapeType.MARKER
self.annotation_enum: ImageEnum = ImageEnum.MARKER
@ -257,12 +259,12 @@ class Toolbar(ttk.Frame):
def draw_node_picker(self) -> None:
self.hide_marker()
self.app.canvas.mode = GraphMode.NODE
self.app.canvas.node_draw = self.current_node
self.app.manager.mode = GraphMode.NODE
self.app.manager.node_draw = self.current_node
self.design_frame.select_radio(self.node_button)
self.picker = PickerFrame(self.app, self.node_button)
# draw default nodes
for node_draw in NodeUtils.NODES:
for node_draw in nutils.NODES:
func = partial(
self.update_button, self.node_button, node_draw, NodeTypeEnum.NODE
)
@ -278,12 +280,12 @@ class Toolbar(ttk.Frame):
def click_selection(self) -> None:
self.design_frame.select_radio(self.select_button)
self.app.canvas.mode = GraphMode.SELECT
self.app.manager.mode = GraphMode.SELECT
self.hide_marker()
def click_runtime_selection(self) -> None:
self.runtime_frame.select_radio(self.runtime_select_button)
self.app.canvas.mode = GraphMode.SELECT
self.app.manager.mode = GraphMode.SELECT
self.hide_marker()
def click_start(self) -> None:
@ -291,8 +293,8 @@ class Toolbar(ttk.Frame):
Start session handler redraw buttons, send node and link messages to grpc
server.
"""
self.app.menubar.change_menubar_item_state(is_runtime=True)
self.app.canvas.mode = GraphMode.SELECT
self.app.menubar.set_state(is_runtime=True)
self.app.manager.mode = GraphMode.SELECT
enable_buttons(self.design_frame, enabled=False)
task = ProgressTask(
self.app, "Start", self.app.core.start_session, self.start_callback
@ -308,7 +310,9 @@ class Toolbar(ttk.Frame):
enable_buttons(self.design_frame, enabled=True)
if exceptions:
message = "\n".join(exceptions)
self.app.show_error("Start Session Error", message)
self.app.show_exception_data(
"Start Exception", "Session failed to start", message
)
def set_runtime(self) -> None:
enable_buttons(self.runtime_frame, enabled=True)
@ -324,7 +328,7 @@ class Toolbar(ttk.Frame):
def click_link(self) -> None:
self.design_frame.select_radio(self.link_button)
self.app.canvas.mode = GraphMode.EDGE
self.app.manager.mode = GraphMode.EDGE
self.hide_marker()
def update_button(
@ -337,7 +341,7 @@ class Toolbar(ttk.Frame):
logging.debug("update button(%s): %s", button, node_draw)
button.configure(image=image)
button.image = image
self.app.canvas.node_draw = node_draw
self.app.manager.node_draw = node_draw
if type_enum == NodeTypeEnum.NODE:
self.current_node = node_draw
elif type_enum == NodeTypeEnum.NETWORK:
@ -348,11 +352,11 @@ class Toolbar(ttk.Frame):
Draw the options for link-layer button.
"""
self.hide_marker()
self.app.canvas.mode = GraphMode.NODE
self.app.canvas.node_draw = self.current_network
self.app.manager.mode = GraphMode.NODE
self.app.manager.node_draw = self.current_network
self.design_frame.select_radio(self.network_button)
self.picker = PickerFrame(self.app, self.network_button)
for node_draw in NodeUtils.NETWORK_NODES:
for node_draw in nutils.NETWORK_NODES:
func = partial(
self.update_button, self.network_button, node_draw, NodeTypeEnum.NETWORK
)
@ -364,8 +368,8 @@ class Toolbar(ttk.Frame):
Draw the options for marker button.
"""
self.design_frame.select_radio(self.annotation_button)
self.app.canvas.mode = GraphMode.ANNOTATION
self.app.canvas.annotation_type = self.current_annotation
self.app.manager.mode = GraphMode.ANNOTATION
self.app.manager.annotation_type = self.current_annotation
if is_marker(self.current_annotation):
self.show_marker()
self.picker = PickerFrame(self.app, self.annotation_button)
@ -382,7 +386,7 @@ class Toolbar(ttk.Frame):
self.picker.show()
def create_observe_button(self) -> None:
image = self.app.get_icon(ImageEnum.OBSERVE, TOOLBAR_SIZE)
image = self.app.get_enum_icon(ImageEnum.OBSERVE, width=TOOLBAR_SIZE)
menu_button = ttk.Menubutton(
self.runtime_frame, image=image, direction=tk.RIGHT
)
@ -396,7 +400,7 @@ class Toolbar(ttk.Frame):
redraw buttons on the toolbar, send node and link messages to grpc server
"""
logging.info("clicked stop button")
self.app.menubar.change_menubar_item_state(is_runtime=False)
self.app.menubar.set_state(is_runtime=False)
self.app.core.close_mobility_players()
enable_buttons(self.runtime_frame, enabled=False)
task = ProgressTask(
@ -406,7 +410,7 @@ class Toolbar(ttk.Frame):
def stop_callback(self, result: bool) -> None:
self.set_design()
self.app.canvas.stopped_session()
self.app.manager.stopped_session()
def update_annotation(
self, shape_type: ShapeType, image_enum: ImageEnum, image: PhotoImage
@ -414,7 +418,7 @@ class Toolbar(ttk.Frame):
logging.debug("clicked annotation")
self.annotation_button.configure(image=image)
self.annotation_button.image = image
self.app.canvas.annotation_type = shape_type
self.app.manager.annotation_type = shape_type
self.current_annotation = shape_type
self.annotation_enum = image_enum
if is_marker(shape_type):
@ -435,8 +439,8 @@ class Toolbar(ttk.Frame):
def click_marker_button(self) -> None:
self.runtime_frame.select_radio(self.runtime_marker_button)
self.app.canvas.mode = GraphMode.ANNOTATION
self.app.canvas.annotation_type = ShapeType.MARKER
self.app.manager.mode = GraphMode.ANNOTATION
self.app.manager.annotation_type = ShapeType.MARKER
self.show_marker()
def scale_button(
@ -444,9 +448,9 @@ class Toolbar(ttk.Frame):
) -> None:
image = None
if image_enum:
image = self.app.get_icon(image_enum, TOOLBAR_SIZE)
image = self.app.get_enum_icon(image_enum, width=TOOLBAR_SIZE)
elif image_file:
image = self.app.get_custom_icon(image_file, TOOLBAR_SIZE)
image = self.app.get_file_icon(image_file, width=TOOLBAR_SIZE)
if image:
button.config(image=image)
button.image = image

View file

@ -10,9 +10,9 @@ from pyproj import Transformer
from core.emulator.enumerations import RegisterTlvs
SCALE_FACTOR = 100.0
CRS_WGS84 = 4326
CRS_PROJ = 3857
SCALE_FACTOR: float = 100.0
CRS_WGS84: int = 4326
CRS_PROJ: int = 3857
class GeoLocation:

View file

@ -921,8 +921,6 @@ class Ns2ScriptedMobility(WayPointMobility):
"""
super().__init__(session, _id)
self.file: Optional[str] = None
self.refresh_ms: Optional[int] = None
self.loop: Optional[bool] = None
self.autostart: Optional[str] = None
self.nodemap: Dict[int, int] = {}
self.script_start: Optional[str] = None
@ -937,7 +935,7 @@ class Ns2ScriptedMobility(WayPointMobility):
self.file,
)
self.refresh_ms = int(config["refresh_ms"])
self.loop = config["loop"].lower() == "on"
self.loop = config["loop"] == "1"
self.autostart = config["autostart"]
self.parsemap(config["map"])
self.script_start = config["script_start"]

View file

@ -159,7 +159,7 @@ class NodeBase(abc.ABC):
ifaces = []
for iface_id in sorted(self.ifaces):
iface = self.ifaces[iface_id]
if not control and getattr(iface, "control", False):
if not control and iface.control:
continue
ifaces.append(iface)
return ifaces

View file

@ -73,6 +73,7 @@ class CoreInterface:
self.net_client: LinuxNetClient = get_net_client(
self.session.use_ovs(), self.host_cmd
)
self.control: bool = False
def host_cmd(
self,

View file

@ -4,7 +4,7 @@ sdt.py: Scripted Display Tool (SDT3D) helper
import logging
import socket
from typing import IO, TYPE_CHECKING, Dict, Optional, Set, Tuple
from typing import IO, TYPE_CHECKING, Dict, List, Optional, Set, Tuple
from urllib.parse import urlparse
from core.constants import CORE_CONF_DIR, CORE_DATA_DIR
@ -26,11 +26,12 @@ def get_link_id(node1_id: int, node2_id: int, network_id: int) -> str:
return link_id
CORE_LAYER = "CORE"
NODE_LAYER = "CORE::Nodes"
LINK_LAYER = "CORE::Links"
CORE_LAYERS = [CORE_LAYER, LINK_LAYER, NODE_LAYER]
DEFAULT_LINK_COLOR = "red"
CORE_LAYER: str = "CORE"
NODE_LAYER: str = "CORE::Nodes"
LINK_LAYER: str = "CORE::Links"
WIRED_LINK_LAYER: str = f"{LINK_LAYER}::wired"
CORE_LAYERS: List[str] = [CORE_LAYER, LINK_LAYER, NODE_LAYER, WIRED_LINK_LAYER]
DEFAULT_LINK_COLOR: str = "red"
class Sdt:
@ -323,7 +324,7 @@ class Sdt:
if all([lat is not None, lon is not None, alt is not None]):
pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}"
self.cmd(f"node {node.id} {pos}")
elif node_data.message_type == 0:
elif node_data.message_type == MessageFlags.NONE:
lat, lon, alt = self.session.location.getgeo(x, y, 0)
pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}"
self.cmd(f"node {node.id} {pos}")
@ -365,13 +366,10 @@ class Sdt:
color = self.session.get_link_color(network_id)
line = f"{color},2"
link_id = get_link_id(node1_id, node2_id, network_id)
layer = LINK_LAYER
if network_id:
node = self.session.nodes.get(network_id)
if node:
network_name = node.name
layer = f"{layer}::{network_name}"
self.network_layers.add(layer)
layer = self.get_network_layer(network_id)
else:
layer = WIRED_LINK_LAYER
link_label = ""
if label:
link_label = f'linklabel on,"{label}"'
@ -380,6 +378,15 @@ class Sdt:
f"{link_label}"
)
def get_network_layer(self, network_id: int) -> str:
node = self.session.nodes.get(network_id)
if node:
layer = f"{LINK_LAYER}::{node.name}"
self.network_layers.add(layer)
else:
layer = WIRED_LINK_LAYER
return layer
def delete_link(self, node1_id: int, node2_id: int, network_id: int = None) -> None:
"""
Handle deleting a link in SDT.

View file

@ -66,7 +66,7 @@ class FRRZebra(CoreService):
for iface in node.get_ifaces():
cfg += "interface %s\n" % iface.name
# include control interfaces in addressing but not routing daemons
if hasattr(iface, "control") and iface.control is True:
if iface.control:
cfg += " "
cfg += "\n ".join(map(cls.addrstr, iface.ips()))
cfg += "\n"

View file

@ -63,7 +63,7 @@ class Zebra(CoreService):
for iface in node.get_ifaces():
cfg += "interface %s\n" % iface.name
# include control interfaces in addressing but not routing daemons
if getattr(iface, "control", False):
if iface.control:
cfg += " "
cfg += "\n ".join(map(cls.addrstr, iface.ips()))
cfg += "\n"

View file

@ -581,7 +581,7 @@ if [ "x$1" = "xstart" ]; then
"""
for iface in node.get_ifaces():
if hasattr(iface, "control") and iface.control is True:
if iface.control:
cfg += "# "
redir = "< /dev/null"
cfg += "tcpdump ${DUMPOPTS} -w %s.%s.pcap -i %s %s &\n" % (

View file

@ -13,7 +13,7 @@ from core.errors import CoreXmlError
from core.nodes.base import CoreNodeBase, NodeBase
from core.nodes.docker import DockerNode
from core.nodes.lxd import LxcNode
from core.nodes.network import CtrlNet, WlanNode
from core.nodes.network import CtrlNet, GreTapBridge, WlanNode
from core.services.coreservices import CoreService
if TYPE_CHECKING:
@ -253,15 +253,13 @@ class DeviceElement(NodeElement):
class NetworkElement(NodeElement):
def __init__(self, session: "Session", node: NodeBase) -> None:
super().__init__(session, node, "network")
model = getattr(self.node, "model", None)
if model:
add_attribute(self.element, "model", model.name)
mobility = getattr(self.node, "mobility", None)
if mobility:
add_attribute(self.element, "mobility", mobility.name)
grekey = getattr(self.node, "grekey", None)
if grekey and grekey is not None:
add_attribute(self.element, "grekey", grekey)
if isinstance(self.node, (WlanNode, EmaneNet)):
if self.node.model:
add_attribute(self.element, "model", self.node.model.name)
if self.node.mobility:
add_attribute(self.element, "mobility", self.node.mobility.name)
if isinstance(self.node, GreTapBridge):
add_attribute(self.element, "grekey", self.node.grekey)
self.add_type()
def add_type(self) -> None:

View file

@ -49,3 +49,6 @@ iface1 = iface_helper.create_iface(n1_id, 0)
core.add_link(session_id, n1_id, emane_id, iface1)
iface1 = iface_helper.create_iface(n2_id, 0)
core.add_link(session_id, n2_id, emane_id, iface1)
# change session state
core.set_session_state(session_id, SessionState.INSTANTIATION)

View file

@ -752,6 +752,7 @@ message Node {
Geo geo = 12;
string dir = 13;
string channel = 14;
int32 canvas = 15;
}
message Link {

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "core"
version = "7.4.0"
version = "7.5.0"
description = "CORE Common Open Research Emulator"
authors = ["Boeing Research and Technology"]
license = "BSD-2-Clause"

View file

@ -262,6 +262,15 @@ def query_node(core: CoreGrpcClient, args: Namespace) -> None:
print_iface(iface)
@coreclient
def delete_session(core: CoreGrpcClient, args: Namespace) -> None:
response = core.delete_session(args.id)
if args.json:
print_json(response)
else:
print(f"delete session({args.id}): {response.result}")
@coreclient
def add_node(core: CoreGrpcClient, args: Namespace) -> None:
session_id = get_current_session(core, args.session)
@ -374,6 +383,19 @@ def delete_link(core: CoreGrpcClient, args: Namespace) -> None:
print(f"delete link: {response.result}")
def setup_sessions_parser(parent: _SubParsersAction) -> None:
parser = parent.add_parser("session", help="session interactions")
parser.formatter_class = ArgumentDefaultsHelpFormatter
parser.add_argument("-i", "--id", type=int, help="session id to use", required=True)
subparsers = parser.add_subparsers(help="session commands")
subparsers.required = True
subparsers.dest = "command"
delete_parser = subparsers.add_parser("delete", help="delete a session")
delete_parser.formatter_class = ArgumentDefaultsHelpFormatter
delete_parser.set_defaults(func=delete_session)
def setup_node_parser(parent: _SubParsersAction) -> None:
parser = parent.add_parser("node", help="node interactions")
parser.formatter_class = ArgumentDefaultsHelpFormatter
@ -528,6 +550,7 @@ def main() -> None:
subparsers = parser.add_subparsers(help="supported commands")
subparsers.required = True
subparsers.dest = "command"
setup_sessions_parser(subparsers)
setup_node_parser(subparsers)
setup_link_parser(subparsers)
setup_query_parser(subparsers)

View file

@ -3,9 +3,8 @@ import argparse
import logging
from logging.handlers import TimedRotatingFileHandler
from core.gui import appconfig
from core.gui import appconfig, images
from core.gui.app import Application
from core.gui.images import Images
if __name__ == "__main__":
# parse flags
@ -28,6 +27,6 @@ if __name__ == "__main__":
logging.getLogger("PIL").setLevel(logging.ERROR)
# start app
Images.load_all()
images.load_all()
app = Application(args.proxy, args.session)
app.mainloop()

View file

@ -11,7 +11,7 @@
* Links are created using Linux bridges and virtual ethernet peers
* Packets sent over links are manipulated using traffic control
* Controlled via the CORE GUI
* Provides both a custo TLV API and gRPC API
* Provides both a custom TLV API and gRPC API
* Python program that leverages a small C binary for node creation
* core-gui
* GUI and daemon communicate over the custom TLV API