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 ## 2021-01-11 CORE 7.4.0
* Installation * Installation

View file

@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script. # Process this file with autoconf to produce a configure script.
# this defines the CORE version number, must be static for AC_INIT # 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 # autoconf and automake initialization
AC_CONFIG_SRCDIR([netns/version.h.in]) 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.errors import CoreError
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
from core.nodes.base import CoreNode, CoreNodeBase, NodeBase from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
from core.nodes.docker import DockerNode
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.nodes.lxd import LxcNode
from core.nodes.network import WlanNode from core.nodes.network import WlanNode
from core.services.coreservices import CoreService 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, image=node_proto.image,
services=node_proto.services, services=node_proto.services,
config_services=node_proto.config_services, config_services=node_proto.config_services,
canvas=node_proto.canvas,
) )
if node_proto.emane: if node_proto.emane:
options.emane = 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( geo = core_pb2.Geo(
lat=node.position.lat, lon=node.position.lon, alt=node.position.alt lat=node.position.lat, lon=node.position.lon, alt=node.position.alt
) )
services = getattr(node, "services", []) services = [x.name for x in node.services]
if services is None: model = node.type
services = [] node_dir = None
services = [x.name for x in services] config_services = []
config_services = getattr(node, "config_services", {}) if isinstance(node, CoreNodeBase):
config_services = [x for x in config_services] 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 emane_model = None
if isinstance(node, EmaneNet): if isinstance(node, EmaneNet):
emane_model = node.model.name emane_model = node.model.name
model = getattr(node, "type", None) image = None
node_dir = getattr(node, "nodedir", None) if isinstance(node, (DockerNode, LxcNode)):
channel = getattr(node, "ctrlchnlname", None) image = node.image
image = getattr(node, "image", None)
return core_pb2.Node( return core_pb2.Node(
id=node.id, id=node.id,
name=node.name, name=node.name,
@ -290,6 +296,7 @@ def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node:
config_services=config_services, config_services=config_services,
dir=node_dir, dir=node_dir,
channel=channel, channel=channel,
canvas=node.canvas,
) )

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -185,6 +185,7 @@ class GuiConfig(yaml.YAMLObject):
def copy_files(current_path: Path, new_path: Path) -> None: def copy_files(current_path: Path, new_path: Path) -> None:
for current_file in current_path.glob("*"): for current_file in current_path.glob("*"):
new_file = new_path.joinpath(current_file.name) new_file = new_path.joinpath(current_file.name)
if not new_file.exists():
shutil.copy(current_file, new_file) shutil.copy(current_file, new_file)
@ -197,22 +198,19 @@ def find_terminal() -> Optional[str]:
def check_directory() -> None: def check_directory() -> None:
if HOME_PATH.exists(): HOME_PATH.mkdir(exist_ok=True)
return BACKGROUNDS_PATH.mkdir(exist_ok=True)
HOME_PATH.mkdir() CUSTOM_EMANE_PATH.mkdir(exist_ok=True)
BACKGROUNDS_PATH.mkdir() CUSTOM_SERVICE_PATH.mkdir(exist_ok=True)
CUSTOM_EMANE_PATH.mkdir() ICONS_PATH.mkdir(exist_ok=True)
CUSTOM_SERVICE_PATH.mkdir() MOBILITY_PATH.mkdir(exist_ok=True)
ICONS_PATH.mkdir() XMLS_PATH.mkdir(exist_ok=True)
MOBILITY_PATH.mkdir() SCRIPT_PATH.mkdir(exist_ok=True)
XMLS_PATH.mkdir()
SCRIPT_PATH.mkdir()
copy_files(LOCAL_ICONS_PATH, ICONS_PATH) copy_files(LOCAL_ICONS_PATH, ICONS_PATH)
copy_files(LOCAL_BACKGROUND_PATH, BACKGROUNDS_PATH) copy_files(LOCAL_BACKGROUND_PATH, BACKGROUNDS_PATH)
copy_files(LOCAL_XMLS_PATH, XMLS_PATH) copy_files(LOCAL_XMLS_PATH, XMLS_PATH)
copy_files(LOCAL_MOBILITY_PATH, MOBILITY_PATH) copy_files(LOCAL_MOBILITY_PATH, MOBILITY_PATH)
if not CONFIG_PATH.exists():
terminal = find_terminal() terminal = find_terminal()
if "EDITOR" in os.environ: if "EDITOR" in os.environ:
editor = EDITORS[0] editor = EDITORS[0]

View file

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

View file

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

View file

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

View file

@ -2,7 +2,8 @@ import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING 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 from core.gui.themes import DIALOG_PAD
if TYPE_CHECKING: if TYPE_CHECKING:
@ -25,7 +26,7 @@ class Dialog(tk.Toplevel):
self.modal: bool = modal self.modal: bool = modal
self.title(title) self.title(title)
self.protocol("WM_DELETE_WINDOW", self.destroy) 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.tk.call("wm", "iconphoto", self._w, image)
self.columnconfigure(0, weight=1) self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1) self.rowconfigure(0, weight=1)

View file

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

View file

@ -2,8 +2,9 @@ import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from core.gui import images
from core.gui.dialogs.dialog import Dialog 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.themes import PADY
from core.gui.widgets import CodeText from core.gui.widgets import CodeText
@ -12,9 +13,11 @@ if TYPE_CHECKING:
class ErrorDialog(Dialog): class ErrorDialog(Dialog):
def __init__(self, app: "Application", title: str, details: str) -> None: def __init__(
super().__init__(app, "CORE Exception") self, app: "Application", title: str, message: str, details: str
self.title: str = title ) -> None:
super().__init__(app, title)
self.message: str = message
self.details: str = details self.details: str = details
self.error_message: Optional[CodeText] = None self.error_message: Optional[CodeText] = None
self.draw() self.draw()
@ -22,15 +25,15 @@ class ErrorDialog(Dialog):
def draw(self) -> None: def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(1, 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( 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.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 = CodeText(self.top)
self.error_message.text.insert("1.0", self.details) self.error_message.text.insert("1.0", self.details)
self.error_message.text.config(state=tk.DISABLED) 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 = ttk.Button(self.top, text="Close", command=lambda: self.destroy())
button.grid(sticky=tk.EW) button.grid(sticky=tk.EW)

View file

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

View file

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

View file

@ -84,17 +84,17 @@ class MobilityPlayerDialog(Dialog):
for i in range(3): for i in range(3):
frame.columnconfigure(i, weight=1) 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 = ttk.Button(frame, image=image, command=self.click_play)
self.play_button.image = image self.play_button.image = image
self.play_button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) self.play_button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
image = self.app.get_icon(ImageEnum.PAUSE, ICON_SIZE) 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 = ttk.Button(frame, image=image, command=self.click_pause)
self.pause_button.image = image self.pause_button.image = image
self.pause_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX) self.pause_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
image = self.app.get_icon(ImageEnum.STOP, ICON_SIZE) 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 = ttk.Button(frame, image=image, command=self.click_stop)
self.stop_button.image = image self.stop_button.image = image
self.stop_button.grid(row=0, column=2, sticky=tk.EW, padx=PADX) 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 PIL.ImageTk import PhotoImage
from core.api.grpc.wrappers import Interface, Node 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.appconfig import ICONS_PATH
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.dialogs.emaneconfig import EmaneModelDialog 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.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import ListboxScroll, image_chooser from core.gui.widgets import ListboxScroll, image_chooser
@ -225,27 +225,27 @@ class NodeConfigDialog(Dialog):
row += 1 row += 1
# node type field # node type field
if NodeUtils.is_model_node(self.node.type): if nutils.is_model(self.node):
label = ttk.Label(frame, text="Type") label = ttk.Label(frame, text="Type")
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY) label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
combobox = ttk.Combobox( combobox = ttk.Combobox(
frame, frame,
textvariable=self.type, textvariable=self.type,
values=list(NodeUtils.NODE_MODELS), values=list(nutils.NODE_MODELS),
state=combo_state, state=combo_state,
) )
combobox.grid(row=row, column=1, sticky=tk.EW) combobox.grid(row=row, column=1, sticky=tk.EW)
row += 1 row += 1
# container image field # 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 = ttk.Label(frame, text="Image")
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY) label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
entry = ttk.Entry(frame, textvariable=self.container_image, state=state) entry = ttk.Entry(frame, textvariable=self.container_image, state=state)
entry.grid(row=row, column=1, sticky=tk.EW) entry.grid(row=row, column=1, sticky=tk.EW)
row += 1 row += 1
if NodeUtils.is_container_node(self.node.type): if nutils.is_container(self.node):
# server # server
frame.grid(sticky=tk.EW) frame.grid(sticky=tk.EW)
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
@ -259,7 +259,7 @@ class NodeConfigDialog(Dialog):
combobox.grid(row=row, column=1, sticky=tk.EW) combobox.grid(row=row, column=1, sticky=tk.EW)
row += 1 row += 1
if NodeUtils.is_rj45_node(self.node.type): if nutils.is_rj45(self.node):
response = self.app.core.client.get_ifaces() response = self.app.core.client.get_ifaces()
logging.debug("host machine available interfaces: %s", response) logging.debug("host machine available interfaces: %s", response)
ifaces = ListboxScroll(frame) ifaces = ListboxScroll(frame)
@ -371,7 +371,7 @@ class NodeConfigDialog(Dialog):
def click_icon(self) -> None: def click_icon(self) -> None:
file_path = image_chooser(self, ICONS_PATH) file_path = image_chooser(self, ICONS_PATH)
if file_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_button.config(image=self.image)
self.image_file = file_path self.image_file = file_path
@ -380,10 +380,10 @@ class NodeConfigDialog(Dialog):
# update core node # update core node
self.node.name = self.name.get() 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() self.node.image = self.container_image.get()
server = self.server.get() server = self.server.get()
if NodeUtils.is_container_node(self.node.type): if nutils.is_container(self.node):
if server == DEFAULT_SERVER: if server == DEFAULT_SERVER:
self.node.server = None self.node.server = None
else: else:

View file

@ -134,7 +134,8 @@ class PreferencesDialog(Dialog):
# scale toolbar and canvas items # scale toolbar and canvas items
self.app.toolbar.scale() 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: def adjust_scale(self, arg1: str, arg2: str, arg3: str) -> None:
scale_value = self.gui_scale.get() scale_value = self.gui_scale.get()

View file

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

View file

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

View file

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

View file

@ -27,7 +27,7 @@ class ShapeDialog(Dialog):
else: else:
title = "Add Text" title = "Add Text"
super().__init__(app, title) super().__init__(app, title)
self.canvas: "CanvasGraph" = app.canvas self.canvas: "CanvasGraph" = app.manager.current()
self.fill: Optional[ttk.Label] = None self.fill: Optional[ttk.Label] = None
self.border: Optional[ttk.Label] = None self.border: Optional[ttk.Label] = None
self.shape: "Shape" = shape 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.colorpicker import ColorPickerDialog
from core.gui.dialogs.dialog import Dialog 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 from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING: if TYPE_CHECKING:
@ -17,16 +17,16 @@ if TYPE_CHECKING:
class ThroughputDialog(Dialog): class ThroughputDialog(Dialog):
def __init__(self, app: "Application") -> None: def __init__(self, app: "Application") -> None:
super().__init__(app, "Throughput Config") 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.show_throughput: tk.IntVar = tk.IntVar(value=1)
self.exponential_weight: tk.IntVar = tk.IntVar(value=1) self.exponential_weight: tk.IntVar = tk.IntVar(value=1)
self.transmission: tk.IntVar = tk.IntVar(value=1) self.transmission: tk.IntVar = tk.IntVar(value=1)
self.reception: tk.IntVar = tk.IntVar(value=1) self.reception: tk.IntVar = tk.IntVar(value=1)
self.threshold: tk.DoubleVar = tk.DoubleVar( 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.width: tk.IntVar = tk.IntVar(value=self.manager.throughput_width)
self.color: str = self.canvas.throughput_color self.color: str = self.manager.throughput_color
self.color_button: Optional[tk.Button] = None self.color_button: Optional[tk.Button] = None
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.draw() self.draw()
@ -106,7 +106,7 @@ class ThroughputDialog(Dialog):
self.color_button.config(bg=self.color, text=self.color, bd=0) self.color_button.config(bg=self.color, text=self.color, bd=0)
def click_save(self) -> None: def click_save(self) -> None:
self.canvas.throughput_threshold = self.threshold.get() self.manager.throughput_threshold = self.threshold.get()
self.canvas.throughput_width = self.width.get() self.manager.throughput_width = self.width.get()
self.canvas.throughput_color = self.color self.manager.throughput_color = self.color
self.destroy() self.destroy()

View file

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

View file

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

View file

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

View file

@ -1,18 +1,21 @@
import functools
import logging import logging
import math import math
import tkinter as tk 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.api.grpc.wrappers import Interface, Link
from core.gui import themes from core.gui import themes
from core.gui.dialogs.linkconfig import LinkConfigurationDialog from core.gui.dialogs.linkconfig import LinkConfigurationDialog
from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame
from core.gui.graph import tags from core.gui.graph import tags
from core.gui.nodeutils import NodeUtils
from core.gui.utils import bandwidth_text, delay_jitter_text from core.gui.utils import bandwidth_text, delay_jitter_text
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.graph import CanvasGraph 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 TEXT_DISTANCE: int = 60
EDGE_WIDTH: int = 3 EDGE_WIDTH: int = 3
@ -24,13 +27,40 @@ ARC_DISTANCE: int = 50
def create_wireless_token(src: int, dst: int, network: int) -> str: 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: def create_edge_token(link: Link) -> str:
iface1_id = link.iface1.id if link.iface1 else 0 iface1_id = link.iface1.id if link.iface1 else 0
iface2_id = link.iface2.id if link.iface2 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: def arc_edges(edges) -> None:
@ -65,21 +95,35 @@ def arc_edges(edges) -> None:
class Edge: class Edge:
tag: str = tags.EDGE tag: str = tags.EDGE
def __init__(self, canvas: "CanvasGraph", src: int, dst: int = None) -> None: def __init__(
self.canvas = canvas self, app: "Application", src: "CanvasNode", dst: "CanvasNode" = None
) -> None:
self.app: "Application" = app
self.manager: CanvasManager = app.manager
self.id: Optional[int] = None self.id: Optional[int] = None
self.src: int = src self.id2: Optional[int] = None
self.dst: int = dst 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.arc: int = 0
self.token: Optional[str] = None self.token: Optional[str] = None
self.src_label: Optional[int] = None self.src_label: Optional[int] = None
self.src_label2: Optional[int] = None
self.middle_label: Optional[int] = None self.middle_label: Optional[int] = None
self.middle_label2: Optional[int] = None
self.dst_label: Optional[int] = None self.dst_label: Optional[int] = None
self.dst_label2: Optional[int] = None
self.color: str = EDGE_COLOR self.color: str = EDGE_COLOR
self.width: int = EDGE_WIDTH 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: def scaled_width(self) -> float:
return self.width * self.canvas.app.app_scale return self.width * self.app.app_scale
def _get_arcpoint( def _get_arcpoint(
self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float] self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]
@ -110,11 +154,53 @@ class Edge:
arc_y = (perp_m * arc_x) + b arc_y = (perp_m * arc_x) + b
return arc_x, arc_y return arc_x, arc_y
def draw( def arc_common_edges(self) -> None:
self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float], state: str common_edges = list(self.src.edges & self.dst.edges)
) -> None: 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) arc_pos = self._get_arcpoint(src_pos, dst_pos)
self.id = self.canvas.create_line( return canvas.create_line(
*src_pos, *src_pos,
*arc_pos, *arc_pos,
*dst_pos, *dst_pos,
@ -126,112 +212,268 @@ class Edge:
) )
def redraw(self) -> None: def redraw(self) -> None:
self.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color) self.src.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color)
src_x, src_y, _, _, _, _ = self.canvas.coords(self.id) self.move_src()
src_pos = src_x, src_y if self.id2:
self.move_src(src_pos) self.dst.canvas.itemconfig(
self.id2, width=self.scaled_width(), fill=self.color
def middle_label_pos(self) -> Tuple[float, float]: )
_, _, x, y, _, _ = self.canvas.coords(self.id) self.move_dst()
return x, y
def middle_label_text(self, text: str) -> None: def middle_label_text(self, text: str) -> None:
if self.middle_label is None: if self.middle_label is None:
x, y = self.middle_label_pos() _, _, x, y, _, _ = self.src.canvas.coords(self.id)
self.middle_label = self.canvas.create_text( self.middle_label = self.src.canvas.create_text(
x, x,
y, y,
font=self.canvas.app.edge_font, font=self.app.edge_font,
text=text, text=text,
tags=tags.LINK_LABEL, tags=tags.LINK_LABEL,
justify=tk.CENTER, 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: 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: def clear_middle_label(self) -> None:
self.canvas.delete(self.middle_label) self.src.canvas.delete(self.middle_label)
self.middle_label = None self.middle_label = None
if self.middle_label2:
def node_label_positions(self) -> Tuple[Tuple[float, float], Tuple[float, float]]: self.dst.canvas.delete(self.middle_label2)
src_x, src_y, _, _, dst_x, dst_y = self.canvas.coords(self.id) self.middle_label2 = None
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 src_label_text(self, text: str) -> None: def src_label_text(self, text: str) -> None:
if self.src_label is None: if self.src_label is None and self.src_label2 is None:
src_pos, _ = self.node_label_positions() if self.id:
self.src_label = self.canvas.create_text( 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, *src_pos,
text=text, text=text,
justify=tk.CENTER, justify=tk.CENTER,
font=self.canvas.app.edge_font, font=self.app.edge_font,
tags=tags.LINK_LABEL, tags=tags.LINK_LABEL,
state=self.canvas.show_link_labels.state(), 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: 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: def dst_label_text(self, text: str) -> None:
if self.dst_label is None: if self.dst_label is None and self.dst_label2 is None:
_, dst_pos = self.node_label_positions() if self.id:
self.dst_label = self.canvas.create_text( 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, *dst_pos,
text=text, text=text,
justify=tk.CENTER, justify=tk.CENTER,
font=self.canvas.app.edge_font, font=self.app.edge_font,
tags=tags.LINK_LABEL, tags=tags.LINK_LABEL,
state=self.canvas.show_link_labels.state(), 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: 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: def drawing(self, pos: Tuple[float, float]) -> None:
if self.src == node_id: src_x, src_y, _, _, _, _ = self.src.canvas.coords(self.id)
self.move_src(pos) src_pos = src_x, src_y
self.moved(src_pos, pos)
def move_node(self, node: "CanvasNode") -> None:
if self.src == node:
self.move_src()
else: else:
self.move_dst(pos) self.move_dst()
def move_dst(self, dst_pos: Tuple[float, float]) -> None: def move_shadow(self, node: "ShadowNode") -> None:
src_x, src_y, _, _, _, _ = self.canvas.coords(self.id) 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.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 src_pos = src_x, src_y
self.moved(src_pos, dst_pos) self.moved(src_pos, dst_pos)
def move_src(self, src_pos: Tuple[float, float]) -> None: def move_src(self) -> None:
_, _, _, _, dst_x, dst_y = self.canvas.coords(self.id) if not self.id:
return
_, _, _, _, dst_x, dst_y = self.src.canvas.coords(self.id)
dst_pos = dst_x, dst_y dst_pos = dst_x, dst_y
self.moved(src_pos, dst_pos) self.moved(self.src.position(), dst_pos)
def moved(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None: def moved(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None:
arc_pos = self._get_arcpoint(src_pos, dst_pos) 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: if self.middle_label:
self.canvas.coords(self.middle_label, *arc_pos) self.src.canvas.coords(self.middle_label, *arc_pos)
src_pos, dst_pos = self.node_label_positions() 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: 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: 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: def delete(self) -> None:
logging.debug("deleting canvas edge, id: %s", self.id) logging.debug("deleting canvas edge, id: %s", self.id)
self.canvas.delete(self.id) self.src.canvas.delete(self.id)
self.canvas.delete(self.src_label) self.src.canvas.delete(self.src_label)
self.canvas.delete(self.dst_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.clear_middle_label()
self.id = None self.id = None
self.id2 = None
self.src_label = None self.src_label = None
self.src_label2 = None
self.dst_label = 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): class CanvasWirelessEdge(Edge):
@ -239,35 +481,43 @@ class CanvasWirelessEdge(Edge):
def __init__( def __init__(
self, self,
canvas: "CanvasGraph", app: "Application",
src: int, src: "CanvasNode",
dst: int, dst: "CanvasNode",
network_id: int, network_id: int,
token: str, token: str,
src_pos: Tuple[float, float],
dst_pos: Tuple[float, float],
link: Link, link: Link,
) -> None: ) -> None:
logging.debug("drawing wireless link from node %s to node %s", src, dst) 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.network_id: int = network_id
self.link: Link = link self.link: Link = link
self.token: str = token self.token: str = token
self.width: float = WIRELESS_WIDTH self.width: float = WIRELESS_WIDTH
color = link.color if link.color else WIRELESS_COLOR color = link.color if link.color else WIRELESS_COLOR
self.color: str = 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: if link.label:
self.middle_label_text(link.label) self.middle_label_text(link.label)
if self.src.hidden or self.dst.hidden:
self.hide()
self.set_binding() self.set_binding()
def set_binding(self) -> None: 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: def show_info(self, _event: tk.Event) -> None:
self.canvas.app.display_info( self.app.display_info(WirelessEdgeInfoFrame, app=self.app, edge=self)
WirelessEdgeInfoFrame, app=self.canvas.app, edge=self
) def delete(self) -> None:
self.src.wireless_edges.discard(self)
self.dst.wireless_edges.remove(self)
super().delete()
class CanvasEdge(Edge): class CanvasEdge(Edge):
@ -276,47 +526,39 @@ class CanvasEdge(Edge):
""" """
def __init__( def __init__(
self, self, app: "Application", src: "CanvasNode", dst: "CanvasNode" = None
canvas: "CanvasGraph",
src: int,
src_pos: Tuple[float, float],
dst_pos: Tuple[float, float],
) -> None: ) -> None:
""" """
Create an instance of canvas edge object Create an instance of canvas edge object
""" """
super().__init__(canvas, src) super().__init__(app, src, dst)
self.text_src: Optional[int] = None self.text_src: Optional[int] = None
self.text_dst: 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.asymmetric_link: Optional[Link] = None
self.throughput: Optional[float] = None self.throughput: Optional[float] = None
self.draw(src_pos, dst_pos, tk.NORMAL) self.draw(tk.NORMAL)
self.set_binding()
self.context: tk.Menu = tk.Menu(self.canvas)
self.create_context()
def is_customized(self) -> bool: def is_customized(self) -> bool:
return self.width != EDGE_WIDTH or self.color != EDGE_COLOR return self.width != EDGE_WIDTH or self.color != EDGE_COLOR
def create_context(self) -> None: def set_bindings(self) -> None:
themes.style_menu(self.context) if self.id:
self.context.add_command(label="Configure", command=self.click_configure) show_context = functools.partial(self.show_context, self.src.canvas)
self.context.add_command(label="Delete", command=self.click_delete) self.src.canvas.tag_bind(self.id, "<ButtonRelease-3>", show_context)
self.src.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
def set_binding(self) -> None: if self.id2:
self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.show_context) show_context = functools.partial(self.show_context, self.dst.canvas)
self.canvas.tag_bind(self.id, "<Button-1>", self.show_info) 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: def iface_label(self, iface: Interface) -> str:
label = "" 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}" 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"{label}\n" if label else ""
label += f"{iface.ip4}/{iface.ip4_mask}" 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"{label}\n" if label else ""
label += f"{iface.ip6}/{iface.ip6_mask}" label += f"{iface.ip6}/{iface.ip6_mask}"
return label return label
@ -341,82 +583,126 @@ class CanvasEdge(Edge):
super().redraw() super().redraw()
self.draw_labels() self.draw_labels()
def check_options(self) -> None: def show(self) -> None:
if not self.link.options: super().show()
return self.check_visibility()
if self.link.options.loss == EDGE_LOSS:
state = tk.HIDDEN def check_visibility(self) -> None:
self.canvas.addtag_withtag(tags.LOSS_EDGES, self.id)
else:
state = tk.NORMAL state = tk.NORMAL
self.canvas.dtag(self.id, tags.LOSS_EDGES) hide_links = self.manager.show_links.state() == tk.HIDDEN
if self.canvas.show_loss_links.state() == tk.HIDDEN: if self.linked_wireless or hide_links:
self.canvas.itemconfigure(self.id, state=state) state = tk.HIDDEN
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: def set_throughput(self, throughput: float) -> None:
throughput = 0.001 * throughput throughput = 0.001 * throughput
text = f"{throughput:.3f} kbps" text = f"{throughput:.3f} kbps"
self.middle_label_text(text) self.middle_label_text(text)
if throughput > self.canvas.throughput_threshold: if throughput > self.manager.throughput_threshold:
color = self.canvas.throughput_color color = self.manager.throughput_color
width = self.canvas.throughput_width width = self.manager.throughput_width
else: else:
color = self.color color = self.color
width = self.scaled_width() 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: def clear_throughput(self) -> None:
self.clear_middle_label() self.clear_middle_label()
if not self.linked_wireless: if not self.linked_wireless:
self.draw_link_options() 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.dst = dst
self.linked_wireless = linked_wireless self.linked_wireless = self.src.is_wireless() or self.dst.is_wireless()
dst_pos = self.canvas.coords(self.dst) self.set_bindings()
self.move_dst(dst_pos)
self.check_wireless() 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: def check_wireless(self) -> None:
if self.linked_wireless: if not self.linked_wireless:
self.canvas.itemconfig(self.id, state=tk.HIDDEN) return
self.canvas.dtag(self.id, tags.EDGE) if self.id:
self._check_antenna() self.src.canvas.itemconfig(self.id, state=tk.HIDDEN)
self.src.canvas.dtag(self.id, tags.EDGE)
def _check_antenna(self) -> None: if self.id2:
src_node = self.canvas.nodes[self.src] self.dst.canvas.itemconfig(self.id2, state=tk.HIDDEN)
dst_node = self.canvas.nodes[self.dst] self.dst.canvas.dtag(self.id2, tags.EDGE)
src_node_type = src_node.core_node.type # add antenna to node
dst_node_type = dst_node.core_node.type if self.src.is_wireless() and not self.dst.is_wireless():
is_src_wireless = NodeUtils.is_wireless_node(src_node_type) self.dst.add_antenna()
is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type) elif not self.src.is_wireless() and self.dst.is_wireless():
if is_src_wireless or is_dst_wireless: self.src.add_antenna()
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: else:
src_node.add_antenna() self.src.add_antenna()
def reset(self) -> None: def reset(self) -> None:
self.canvas.delete(self.middle_label) if self.middle_label:
self.src.canvas.delete(self.middle_label)
self.middle_label = None self.middle_label = None
self.canvas.itemconfig(self.id, fill=self.color, width=self.scaled_width()) 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: 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: def show_context(self, canvas: "CanvasGraph", event: tk.Event) -> None:
state = tk.DISABLED if self.canvas.core.is_runtime() else tk.NORMAL context: tk.Menu = tk.Menu(canvas)
self.context.entryconfigure(1, state=state) themes.style_menu(context)
self.context.tk_popup(event.x_root, event.y_root) 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: def click_delete(self) -> None:
self.canvas.delete_edge(self) self.delete()
def click_configure(self) -> None: def click_configure(self) -> None:
dialog = LinkConfigurationDialog(self.canvas.app, self) dialog = LinkConfigurationDialog(self.app, self)
dialog.show() dialog.show()
def draw_link_options(self): def draw_link_options(self):
@ -455,3 +741,21 @@ class CanvasEdge(Edge):
lines.append(dup_line) lines.append(dup_line)
label = "\n".join(lines) label = "\n".join(lines)
self.middle_label_text(label) 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 logging
import tkinter as tk import tkinter as tk
from copy import deepcopy from copy import deepcopy
from tkinter import BooleanVar from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
from PIL import Image from PIL import Image
from PIL.ImageTk import PhotoImage from PIL.ImageTk import PhotoImage
from core.api.grpc.wrappers import ( from core.api.grpc.wrappers import Interface, Link
Interface, from core.gui import appconfig
Link, from core.gui import nodeutils as nutils
LinkType,
Node,
Session,
ThroughputsEvent,
)
from core.gui.dialogs.shapemod import ShapeDialog from core.gui.dialogs.shapemod import ShapeDialog
from core.gui.graph import tags from core.gui.graph import tags
from core.gui.graph.edges import ( from core.gui.graph.edges import EDGE_WIDTH, CanvasEdge
EDGE_WIDTH,
CanvasEdge,
CanvasWirelessEdge,
Edge,
arc_edges,
create_edge_token,
create_wireless_token,
)
from core.gui.graph.enums import GraphMode, ScaleOption 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.shape import Shape
from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker 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: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.graph.manager import CanvasManager
from core.gui.coreclient import CoreClient from core.gui.coreclient import CoreClient
ZOOM_IN = 1.1 ZOOM_IN: float = 1.1
ZOOM_OUT = 0.9 ZOOM_OUT: float = 0.9
ICON_SIZE = 48 MOVE_NODE_MODES: Set[GraphMode] = {GraphMode.NODE, GraphMode.SELECT}
MOVE_NODE_MODES = {GraphMode.NODE, GraphMode.SELECT} MOVE_SHAPE_MODES: Set[GraphMode] = {GraphMode.ANNOTATION, GraphMode.SELECT}
MOVE_SHAPE_MODES = {GraphMode.ANNOTATION, GraphMode.SELECT} BACKGROUND_COLOR: str = "#cccccc"
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())
class CanvasGraph(tk.Canvas): class CanvasGraph(tk.Canvas):
def __init__( 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: ) -> 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.app: "Application" = app
self.manager: "CanvasManager" = manager
self.core: "CoreClient" = core self.core: "CoreClient" = core
self.mode: GraphMode = GraphMode.SELECT
self.annotation_type: Optional[ShapeType] = None
self.selection: Dict[int, int] = {} self.selection: Dict[int, int] = {}
self.select_box: Optional[Shape] = None self.select_box: Optional[Shape] = None
self.selected: Optional[int] = None self.selected: Optional[int] = None
self.node_draw: Optional[NodeDraw] = None
self.nodes: Dict[int, CanvasNode] = {} self.nodes: Dict[int, CanvasNode] = {}
self.edges: Dict[str, CanvasEdge] = {} self.shadow_nodes: Dict[int, ShadowNode] = {}
self.shapes: Dict[int, Shape] = {} 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 # map wireless/EMANE node to the set of MDRs connected to that node
self.wireless_network: Dict[int, Set[int]] = {} self.wireless_network: Dict[int, Set[int]] = {}
@ -81,10 +59,7 @@ class CanvasGraph(tk.Canvas):
self.drawing_edge: Optional[CanvasEdge] = None self.drawing_edge: Optional[CanvasEdge] = None
self.rect: Optional[int] = None self.rect: Optional[int] = None
self.shape_drawing: bool = False self.shape_drawing: bool = False
width = self.app.guiconfig.preferences.width self.current_dimensions: Tuple[int, int] = dimensions
height = self.app.guiconfig.preferences.height
self.default_dimensions: Tuple[int, int] = (width, height)
self.current_dimensions: Tuple[int, int] = self.default_dimensions
self.ratio: float = 1.0 self.ratio: float = 1.0
self.offset: Tuple[int, int] = (0, 0) self.offset: Tuple[int, int] = (0, 0)
self.cursor: 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.scale_option: tk.IntVar = tk.IntVar(value=1)
self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(value=False) 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 # bindings
self.setup_bindings() self.setup_bindings()
@ -126,7 +84,7 @@ class CanvasGraph(tk.Canvas):
if self.rect is not None: if self.rect is not None:
self.delete(self.rect) self.delete(self.rect)
if not dimensions: if not dimensions:
dimensions = self.default_dimensions dimensions = self.manager.default_dimensions
self.current_dimensions = dimensions self.current_dimensions = dimensions
self.rect = self.create_rectangle( self.rect = self.create_rectangle(
0, 0,
@ -139,34 +97,6 @@ class CanvasGraph(tk.Canvas):
) )
self.configure(scrollregion=self.bbox(tk.ALL)) 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: def setup_bindings(self) -> None:
""" """
Bind any mouse events or hot keys to the matching action 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("<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)) 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]: def get_actual_coords(self, x: float, y: float) -> Tuple[float, float]:
actual_x = (x - self.offset[0]) / self.ratio actual_x = (x - self.offset[0]) / self.ratio
actual_y = (y - self.offset[1]) / 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) valid_bottomright = self.inside_canvas(x2, y2)
return valid_topleft and valid_bottomright 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: def draw_grid(self) -> None:
""" """
Create grid. Create grid.
@ -228,123 +154,6 @@ class CanvasGraph(tk.Canvas):
self.tag_lower(tags.GRIDLINE) self.tag_lower(tags.GRIDLINE)
self.tag_lower(self.rect) 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]: def canvas_xy(self, event: tk.Event) -> Tuple[float, float]:
""" """
Convert window coordinate to canvas coordinate Convert window coordinate to canvas coordinate
@ -363,14 +172,12 @@ class CanvasGraph(tk.Canvas):
for _id in overlapping: for _id in overlapping:
if self.drawing_edge and self.drawing_edge.id == _id: if self.drawing_edge and self.drawing_edge.id == _id:
continue continue
elif _id in self.nodes:
if _id in self.nodes:
selected = _id selected = _id
break elif _id in self.shapes:
selected = _id
if _id in self.shapes: elif _id in self.shadow_nodes:
selected = _id selected = _id
return selected return selected
def click_release(self, event: tk.Event) -> None: def click_release(self, event: tk.Event) -> None:
@ -381,13 +188,13 @@ class CanvasGraph(tk.Canvas):
x, y = self.canvas_xy(event) x, y = self.canvas_xy(event)
if not self.inside_canvas(x, y): if not self.inside_canvas(x, y):
return return
if self.mode == GraphMode.ANNOTATION: if self.manager.mode == GraphMode.ANNOTATION:
self.focus_set() self.focus_set()
if self.shape_drawing: if self.shape_drawing:
shape = self.shapes[self.selected] shape = self.shapes[self.selected]
shape.shape_complete(x, y) shape.shape_complete(x, y)
self.shape_drawing = False self.shape_drawing = False
elif self.mode == GraphMode.SELECT: elif self.manager.mode == GraphMode.SELECT:
self.focus_set() self.focus_set()
if self.select_box: if self.select_box:
x0, y0, x1, y1 = self.coords(self.select_box.id) x0, y0, x1, y1 = self.coords(self.select_box.id)
@ -403,61 +210,36 @@ class CanvasGraph(tk.Canvas):
else: else:
self.focus_set() self.focus_set()
self.selected = self.get_selected(event) self.selected = self.get_selected(event)
logging.debug(f"click release selected({self.selected}) mode({self.mode})") logging.debug(
if self.mode == GraphMode.EDGE: "click release selected(%s) mode(%s)", self.selected, self.manager.mode
)
if self.manager.mode == GraphMode.EDGE:
self.handle_edge_release(event) self.handle_edge_release(event)
elif self.mode == GraphMode.NODE: elif self.manager.mode == GraphMode.NODE:
self.add_node(x, y) self.add_node(x, y)
elif self.mode == GraphMode.PICKNODE: elif self.manager.mode == GraphMode.PICKNODE:
self.mode = GraphMode.NODE self.manager.mode = GraphMode.NODE
self.selected = None self.selected = None
def handle_edge_release(self, _event: tk.Event) -> None: def handle_edge_release(self, _event: tk.Event) -> None:
# not drawing edge return
if not self.drawing_edge:
return
edge = self.drawing_edge edge = self.drawing_edge
self.drawing_edge = None self.drawing_edge = None
# not drawing edge return
if edge is None:
return
# edge dst must be a node # edge dst must be a node
logging.debug("current selected: %s", self.selected) logging.debug("current selected: %s", self.selected)
src_node = self.nodes.get(edge.src)
dst_node = self.nodes.get(self.selected) dst_node = self.nodes.get(self.selected)
if not dst_node or not src_node: if not dst_node:
edge.delete() edge.delete()
return return
# check if node can be linked
# edge dst is same as src, delete edge if not edge.src.is_linkable(dst_node):
if edge.src == self.selected:
edge.delete() edge.delete()
return 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 # finalize edge creation
self.complete_edge(src_node, dst_node, edge) edge.drawing(dst_node.position())
edge.complete(dst_node)
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)
def select_object(self, object_id: int, choose_multiple: bool = False) -> None: 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 # delete node and related edges
if object_id in self.nodes: if object_id in self.nodes:
canvas_node = self.nodes.pop(object_id) 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 # delete related edges
for edge in canvas_node.edges: while canvas_node.edges:
edge = canvas_node.edges.pop()
if edge in edges: if edge in edges:
continue continue
edges.add(edge) edges.add(edge)
del self.edges[edge.token]
edge.delete() edge.delete()
# update node connected to edge being deleted # delete node
other_id = edge.src canvas_node.delete()
other_iface = edge.link.iface1 nodes.append(canvas_node)
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 shape # delete shape
if object_id in self.shapes: if object_id in self.shapes:
@ -534,27 +304,21 @@ class CanvasGraph(tk.Canvas):
self.selection.clear() self.selection.clear()
self.core.deleted_canvas_nodes(nodes) self.core.deleted_canvas_nodes(nodes)
self.core.deleted_canvas_edges(edges)
def delete_edge(self, edge: CanvasEdge) -> None: def hide_selected_objects(self) -> None:
edge.delete() for object_id in self.selection:
del self.edges[edge.token] # delete selection box
src_node = self.nodes[edge.src] selection_id = self.selection[object_id]
src_node.edges.discard(edge) self.delete(selection_id)
if edge.link.iface1: # hide node and related edges
del src_node.ifaces[edge.link.iface1.id] if object_id in self.nodes:
dst_node = self.nodes[edge.dst] canvas_node = self.nodes[object_id]
dst_node.edges.discard(edge) canvas_node.hide()
if edge.link.iface2:
del dst_node.ifaces[edge.link.iface2.id] def show_hidden(self) -> None:
src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type) for node in self.nodes.values():
if src_wireless: if node.hidden:
dst_node.delete_antenna() node.show()
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 zoom(self, event: tk.Event, factor: float = None) -> None: def zoom(self, event: tk.Event, factor: float = None) -> None:
if not factor: if not factor:
@ -588,13 +352,13 @@ class CanvasGraph(tk.Canvas):
y_check = self.cursor[1] - self.offset[1] y_check = self.cursor[1] - self.offset[1]
logging.debug("click press offset(%s, %s)", x_check, y_check) logging.debug("click press offset(%s, %s)", x_check, y_check)
is_node = selected in self.nodes is_node = selected in self.nodes
if self.mode == GraphMode.EDGE and is_node: if self.manager.mode == GraphMode.EDGE and is_node:
pos = self.coords(selected) node = self.nodes[selected]
self.drawing_edge = CanvasEdge(self, selected, pos, pos) self.drawing_edge = CanvasEdge(self.app, node)
self.organize() self.organize()
if self.mode == GraphMode.ANNOTATION: if self.manager.mode == GraphMode.ANNOTATION:
if is_marker(self.annotation_type): if is_marker(self.manager.annotation_type):
r = self.app.toolbar.marker_frame.size.get() r = self.app.toolbar.marker_frame.size.get()
self.create_oval( self.create_oval(
x - r, x - r,
@ -604,11 +368,11 @@ class CanvasGraph(tk.Canvas):
fill=self.app.toolbar.marker_frame.color, fill=self.app.toolbar.marker_frame.color,
outline="", outline="",
tags=(tags.MARKER, tags.ANNOTATION), tags=(tags.MARKER, tags.ANNOTATION),
state=self.show_annotations.state(), state=self.manager.show_annotations.state(),
) )
return return
if selected is None: 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.selected = shape.id
self.shape_drawing = True self.shape_drawing = True
self.shapes[shape.id] = shape self.shapes[shape.id] = shape
@ -629,8 +393,18 @@ class CanvasGraph(tk.Canvas):
node.core_node.position.x, node.core_node.position.x,
node.core_node.position.y, 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: else:
if self.mode == GraphMode.SELECT: if self.manager.mode == GraphMode.SELECT:
shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y) shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y)
self.select_box = shape self.select_box = shape
self.clear_selection() self.clear_selection()
@ -659,7 +433,7 @@ class CanvasGraph(tk.Canvas):
if self.select_box: if self.select_box:
self.select_box.delete() self.select_box.delete()
self.select_box = None 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 = self.shapes.pop(self.selected)
shape.delete() shape.delete()
self.shape_drawing = False self.shape_drawing = False
@ -669,14 +443,14 @@ class CanvasGraph(tk.Canvas):
y_offset = y - self.cursor[1] y_offset = y - self.cursor[1]
self.cursor = x, y self.cursor = x, y
if self.mode == GraphMode.EDGE and self.drawing_edge is not None: if self.manager.mode == GraphMode.EDGE and self.drawing_edge is not None:
self.drawing_edge.move_dst(self.cursor) self.drawing_edge.drawing(self.cursor)
if self.mode == GraphMode.ANNOTATION: if self.manager.mode == GraphMode.ANNOTATION:
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[self.selected] shape = self.shapes[self.selected]
shape.shape_motion(x, y) shape.shape_motion(x, y)
return return
elif is_marker(self.annotation_type): elif is_marker(self.manager.annotation_type):
r = self.app.toolbar.marker_frame.size.get() r = self.app.toolbar.marker_frame.size.get()
self.create_oval( self.create_oval(
x - r, x - r,
@ -689,21 +463,26 @@ class CanvasGraph(tk.Canvas):
) )
return return
if self.mode == GraphMode.EDGE: if self.manager.mode == GraphMode.EDGE:
return return
# move selected objects # move selected objects
if self.selection: if self.selection:
for selected_id in 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 = self.shapes[selected_id]
shape.motion(x_offset, y_offset) shape.motion(x_offset, y_offset)
elif self.manager.mode in MOVE_NODE_MODES and selected_id in self.nodes:
if self.mode in MOVE_NODE_MODES and selected_id in self.nodes:
node = self.nodes[selected_id] node = self.nodes[selected_id]
node.motion(x_offset, y_offset, update=self.core.is_runtime()) 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: 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) self.select_box.shape_motion(x, y)
def press_delete(self, _event: tk.Event) -> None: def press_delete(self, _event: tk.Event) -> None:
@ -729,17 +508,15 @@ class CanvasGraph(tk.Canvas):
return return
actual_x, actual_y = self.get_actual_coords(x, y) actual_x, actual_y = self.get_actual_coords(x, y)
core_node = self.core.create_node( 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: if not core_node:
return return
try: core_node.canvas = self.id
image_enum = self.node_draw.image_enum node = CanvasNode(self.app, self, x, y, core_node, self.manager.node_draw.image)
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)
self.nodes[node.id] = node self.nodes[node.id] = node
self.core.set_canvas_node(core_node, node) self.core.set_canvas_node(core_node, node)
@ -847,7 +624,7 @@ class CanvasGraph(tk.Canvas):
# redraw gridlines to new canvas size # redraw gridlines to new canvas size
self.delete(tags.GRIDLINE) self.delete(tags.GRIDLINE)
self.draw_grid() self.draw_grid()
self.app.canvas.show_grid.click_handler() self.app.manager.show_grid.click_handler()
def redraw_wallpaper(self) -> None: def redraw_wallpaper(self) -> None:
if self.adjust_to_dim.get(): if self.adjust_to_dim.get():
@ -871,7 +648,7 @@ class CanvasGraph(tk.Canvas):
self.tag_raise(tag) self.tag_raise(tag)
def set_wallpaper(self, filename: Optional[str]) -> None: 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: if filename:
img = Image.open(filename) img = Image.open(filename)
self.wallpaper = img self.wallpaper = img
@ -884,44 +661,16 @@ class CanvasGraph(tk.Canvas):
self.wallpaper_file = None self.wallpaper_file = None
def is_selection_mode(self) -> bool: 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: def create_edge(self, src: CanvasNode, dst: CanvasNode) -> CanvasEdge:
""" """
create an edge between source node and destination node create an edge between source node and destination node
""" """
pos = (src.core_node.position.x, src.core_node.position.y) edge = CanvasEdge(self.app, src)
edge = CanvasEdge(self, src.id, pos, pos) edge.complete(dst)
self.complete_edge(src, dst, edge)
return edge 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: def copy(self) -> None:
if self.core.is_runtime(): if self.core.is_runtime():
logging.debug("copy is disabled during runtime state") logging.debug("copy is disabled during runtime state")
@ -952,7 +701,9 @@ class CanvasGraph(tk.Canvas):
) )
if not copy: if not copy:
continue 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 # copy configurations and services
node.core_node.services = core_node.services.copy() node.core_node.services = core_node.services.copy()
node.core_node.config_services = core_node.config_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) 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: def scale_graph(self) -> None:
for nid, canvas_node in self.nodes.items(): for node_id, canvas_node in self.nodes.items():
img = None image = nutils.get_icon(canvas_node.core_node, self.app)
if NodeUtils.is_custom( self.itemconfig(node_id, image=image)
canvas_node.core_node.type, canvas_node.core_node.model canvas_node.image = image
):
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
canvas_node.scale_text() canvas_node.scale_text()
canvas_node.scale_antennas() canvas_node.scale_antennas()
for edge_id in self.find_withtag(tags.EDGE): for edge_id in self.find_withtag(tags.EDGE):
self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app.app_scale)) 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 logging
import tkinter as tk import tkinter as tk
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Set from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
import grpc import grpc
from PIL.ImageTk import PhotoImage from PIL.ImageTk import PhotoImage
from core.api.grpc.services_pb2 import ServiceAction
from core.api.grpc.wrappers import Interface, Node, NodeType 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.emaneconfig import EmaneConfigDialog
from core.gui.dialogs.mobilityconfig import MobilityConfigDialog from core.gui.dialogs.mobilityconfig import MobilityConfigDialog
from core.gui.dialogs.nodeconfig import NodeConfigDialog 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 import tags
from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge
from core.gui.graph.tooltip import CanvasTooltip from core.gui.graph.tooltip import CanvasTooltip
from core.gui.images import ImageEnum, Images from core.gui.images import ImageEnum
from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -31,10 +33,16 @@ NODE_TEXT_OFFSET: int = 5
class CanvasNode: class CanvasNode:
def __init__( 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.app: "Application" = app
self.canvas: "CanvasGraph" = app.canvas self.canvas: "CanvasGraph" = canvas
self.image: PhotoImage = image self.image: PhotoImage = image
self.core_node: Node = core_node self.core_node: Node = core_node
self.id: int = self.canvas.create_image( self.id: int = self.canvas.create_image(
@ -49,7 +57,7 @@ class CanvasNode:
tags=tags.NODE_LABEL, tags=tags.NODE_LABEL,
font=self.app.icon_text_font, font=self.app.icon_text_font,
fill="#0000CD", fill="#0000CD",
state=self.canvas.show_node_labels.state(), state=self.app.manager.show_node_labels.state(),
) )
self.tooltip: CanvasTooltip = CanvasTooltip(self.canvas) self.tooltip: CanvasTooltip = CanvasTooltip(self.canvas)
self.edges: Set[CanvasEdge] = set() self.edges: Set[CanvasEdge] = set()
@ -57,10 +65,14 @@ class CanvasNode:
self.wireless_edges: Set[CanvasWirelessEdge] = set() self.wireless_edges: Set[CanvasWirelessEdge] = set()
self.antennas: List[int] = [] self.antennas: List[int] = []
self.antenna_images: Dict[int, PhotoImage] = {} self.antenna_images: Dict[int, PhotoImage] = {}
self.hidden: bool = False
self.setup_bindings() self.setup_bindings()
self.context: tk.Menu = tk.Menu(self.canvas) self.context: tk.Menu = tk.Menu(self.canvas)
themes.style_menu(self.context) themes.style_menu(self.context)
def position(self) -> Tuple[int, int]:
return self.canvas.coords(self.id)
def next_iface_id(self) -> int: def next_iface_id(self) -> int:
i = 0 i = 0
while i in self.ifaces: while i in self.ifaces:
@ -81,9 +93,9 @@ class CanvasNode:
self.delete_antennas() self.delete_antennas()
def add_antenna(self) -> None: 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 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( antenna_id = self.canvas.create_image(
x - 16 + offset, x - 16 + offset,
y - int(23 * self.app.app_scale), y - int(23 * self.app.app_scale),
@ -139,15 +151,14 @@ class CanvasNode:
def move(self, x: float, y: float) -> None: def move(self, x: float, y: float) -> None:
x, y = self.canvas.get_scaled_coords(x, y) 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 x_offset = x - current_x
y_offset = y - current_y y_offset = y - current_y
self.motion(x_offset, y_offset, update=False) self.motion(x_offset, y_offset, update=False)
def motion(self, x_offset: float, y_offset: float, update: bool = True) -> None: 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) self.canvas.move(self.id, x_offset, y_offset)
pos = self.canvas.coords(self.id)
# check new position # check new position
bbox = self.canvas.bbox(self.id) bbox = self.canvas.bbox(self.id)
@ -165,11 +176,12 @@ class CanvasNode:
# move edges # move edges
for edge in self.edges: for edge in self.edges:
edge.move_node(self.id, pos) edge.move_node(self)
for edge in self.wireless_edges: 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 # set actual coords for node and update core is running
pos = self.position()
real_x, real_y = self.canvas.get_actual_coords(*pos) real_x, real_y = self.canvas.get_actual_coords(*pos)
self.core_node.position.x = real_x self.core_node.position.x = real_x
self.core_node.position.y = real_y self.core_node.position.y = real_y
@ -179,7 +191,7 @@ class CanvasNode:
def on_enter(self, event: tk.Event) -> None: def on_enter(self, event: tk.Event) -> None:
is_runtime = self.app.core.is_runtime() is_runtime = self.app.core.is_runtime()
has_observer = self.app.core.observer is not None 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: if is_runtime and has_observer and is_container:
self.tooltip.text.set("waiting...") self.tooltip.text.set("waiting...")
self.tooltip.on_enter(event) self.tooltip.on_enter(event)
@ -194,7 +206,7 @@ class CanvasNode:
def double_click(self, event: tk.Event) -> None: def double_click(self, event: tk.Event) -> None:
if self.app.core.is_runtime(): 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) self.canvas.core.launch_terminal(self.core_node.id)
else: else:
self.show_config() self.show_config()
@ -222,9 +234,25 @@ class CanvasNode:
self.context.add_command( self.context.add_command(
label="Mobility Player", command=self.show_mobility_player 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: else:
self.context.add_command(label="Configure", command=self.show_config) self.context.add_command(label="Configure", command=self.show_config)
if NodeUtils.is_container_node(self.core_node.type): if nutils.is_container(self.core_node):
self.context.add_command(label="Services", command=self.show_services) self.context.add_command(label="Services", command=self.show_services)
self.context.add_command( self.context.add_command(
label="Config Services", command=self.show_config_services label="Config Services", command=self.show_config_services
@ -241,31 +269,44 @@ class CanvasNode:
self.context.add_command( self.context.add_command(
label="Mobility Config", command=self.show_mobility_config 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( self.context.add_command(
label="Link To Selected", command=self.wireless_link_selected 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) unlink_menu = tk.Menu(self.context)
for edge in self.edges: for edge in self.edges:
link = edge.link other_node = edge.other_node(self)
if self.id == edge.src: other_iface = edge.other_iface(self)
other_id = edge.dst label = other_node.core_node.name
other_iface = link.iface2.name if link.iface2 else None if other_iface:
else: label = f"{label}:{other_iface.name}"
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
func_unlink = functools.partial(self.click_unlink, edge) func_unlink = functools.partial(self.click_unlink, edge)
unlink_menu.add_command(label=label, command=func_unlink) unlink_menu.add_command(label=label, command=func_unlink)
themes.style_menu(unlink_menu) themes.style_menu(unlink_menu)
self.context.add_cascade(label="Unlink", menu=unlink_menu) self.context.add_cascade(label="Unlink", menu=unlink_menu)
edit_menu = tk.Menu(self.context) edit_menu = tk.Menu(self.context)
themes.style_menu(edit_menu) themes.style_menu(edit_menu)
edit_menu.add_command(label="Cut", command=self.click_cut) 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="Copy", command=self.canvas_copy)
edit_menu.add_command(label="Delete", command=self.canvas_delete) 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.add_cascade(label="Edit", menu=edit_menu)
self.context.tk_popup(event.x_root, event.y_root) self.context.tk_popup(event.x_root, event.y_root)
@ -273,10 +314,18 @@ class CanvasNode:
self.canvas_copy() self.canvas_copy()
self.canvas_delete() self.canvas_delete()
def click_hide(self) -> None:
self.canvas.clear_selection()
self.hide()
def click_unlink(self, edge: CanvasEdge) -> None: def click_unlink(self, edge: CanvasEdge) -> None:
self.canvas.delete_edge(edge) edge.delete()
self.app.default_info() 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: def canvas_delete(self) -> None:
self.canvas.clear_selection() self.canvas.clear_selection()
self.canvas.select_object(self.id) self.canvas.select_object(self.id)
@ -320,15 +369,11 @@ class CanvasNode:
def has_emane_link(self, iface_id: int) -> Node: def has_emane_link(self, iface_id: int) -> Node:
result = None result = None
for edge in self.edges: for edge in self.edges:
if self.id == edge.src: other_node = edge.other_node(self)
other_id = edge.dst iface = edge.iface(self)
edge_iface_id = edge.link.iface1.id edge_iface_id = iface.id if iface else None
else:
other_id = edge.src
edge_iface_id = edge.link.iface2.id
if edge_iface_id != iface_id: if edge_iface_id != iface_id:
continue continue
other_node = self.canvas.nodes[other_id]
if other_node.core_node.type == NodeType.EMANE: if other_node.core_node.type == NodeType.EMANE:
result = other_node.core_node result = other_node.core_node
break break
@ -344,7 +389,7 @@ class CanvasNode:
def scale_antennas(self) -> None: def scale_antennas(self) -> None:
for i in range(len(self.antennas)): for i in range(len(self.antennas)):
antenna_id = self.antennas[i] 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.canvas.itemconfig(antenna_id, image=image)
self.antenna_images[antenna_id] = image self.antenna_images[antenna_id] = image
node_x, node_y = self.canvas.coords(self.id) 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}") logging.error(f"node icon does not exist: {icon_path}")
return return
self.core_node.icon = icon_path 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) 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 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.dialogs.shapemod import ShapeDialog
from core.gui.graph import tags from core.gui.graph import tags
@ -69,6 +69,31 @@ class Shape:
self.shape_data = data self.shape_data = data
self.draw() 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: def draw(self) -> None:
if self.created: if self.created:
dash = None dash = None
@ -85,7 +110,7 @@ class Shape:
fill=self.shape_data.fill_color, fill=self.shape_data.fill_color,
outline=self.shape_data.border_color, outline=self.shape_data.border_color,
width=self.shape_data.border_width, width=self.shape_data.border_width,
state=self.canvas.show_annotations.state(), state=self.app.manager.show_annotations.state(),
) )
self.draw_shape_text() self.draw_shape_text()
elif self.shape_type == ShapeType.RECTANGLE: elif self.shape_type == ShapeType.RECTANGLE:
@ -99,7 +124,7 @@ class Shape:
fill=self.shape_data.fill_color, fill=self.shape_data.fill_color,
outline=self.shape_data.border_color, outline=self.shape_data.border_color,
width=self.shape_data.border_width, width=self.shape_data.border_width,
state=self.canvas.show_annotations.state(), state=self.app.manager.show_annotations.state(),
) )
self.draw_shape_text() self.draw_shape_text()
elif self.shape_type == ShapeType.TEXT: elif self.shape_type == ShapeType.TEXT:
@ -111,7 +136,7 @@ class Shape:
text=self.shape_data.text, text=self.shape_data.text,
fill=self.shape_data.text_color, fill=self.shape_data.text_color,
font=font, font=font,
state=self.canvas.show_annotations.state(), state=self.app.manager.show_annotations.state(),
) )
else: else:
logging.error("unknown shape type: %s", self.shape_type) logging.error("unknown shape type: %s", self.shape_type)
@ -139,7 +164,7 @@ class Shape:
text=self.shape_data.text, text=self.shape_data.text,
fill=self.shape_data.text_color, fill=self.shape_data.text_color,
font=font, font=font,
state=self.canvas.show_annotations.state(), state=self.app.manager.show_annotations.state(),
) )
def shape_motion(self, x1: float, y1: float) -> None: def shape_motion(self, x1: float, y1: float) -> None:
@ -184,6 +209,7 @@ class Shape:
x1, y1 = self.canvas.get_actual_coords(x1, y1) x1, y1 = self.canvas.get_actual_coords(x1, y1)
coords = (x1, y1) coords = (x1, y1)
return { return {
"canvas": self.canvas.id,
"type": self.shape_type.value, "type": self.shape_type.value,
"iconcoords": coords, "iconcoords": coords,
"label": self.shape_data.text, "label": self.shape_data.text,

View file

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

View file

@ -1,53 +1,46 @@
from enum import Enum from enum import Enum
from tkinter import messagebox
from typing import Dict, Optional, Tuple from typing import Dict, Optional, Tuple
from PIL import Image from PIL import Image
from PIL.ImageTk import PhotoImage 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 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 load_all() -> None:
def create(cls, file_path: str, width: int, height: int = None) -> PhotoImage: for image in LOCAL_ICONS_PATH.glob("*"):
try:
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: if height is None:
height = width height = width
width = int(width * scale)
height = int(height * scale)
image = Image.open(file_path) image = Image.open(file_path)
image = image.resize((width, height), Image.ANTIALIAS) image = image.resize((width, height), Image.ANTIALIAS)
return PhotoImage(image) return PhotoImage(image)
@classmethod
def load_all(cls) -> None:
for image in LOCAL_ICONS_PATH.glob("*"):
cls.images[image.stem] = str(image)
@classmethod def from_enum(
def get(cls, image_enum: Enum, width: int, height: int = None) -> PhotoImage: image_enum: "ImageEnum", *, width: int, height: int = None, scale: float = 1.0
file_path = cls.images[image_enum.value] ) -> PhotoImage:
return cls.create(file_path, width, height) file_path = IMAGES[image_enum.value]
return from_file(file_path, width=width, height=height, scale=scale)
@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:
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",
)
class ImageEnum(Enum): class ImageEnum(Enum):
@ -90,10 +83,10 @@ class ImageEnum(Enum):
SHUTDOWN = "shutdown" SHUTDOWN = "shutdown"
CANCEL = "cancel" CANCEL = "cancel"
ERROR = "error" ERROR = "error"
SHADOW = "shadow"
class TypeToImage: TYPE_MAP: Dict[Tuple[NodeType, str], ImageEnum] = {
type_to_image: Dict[Tuple[NodeType, str], ImageEnum] = {
(NodeType.DEFAULT, "router"): ImageEnum.ROUTER, (NodeType.DEFAULT, "router"): ImageEnum.ROUTER,
(NodeType.DEFAULT, "PC"): ImageEnum.PC, (NodeType.DEFAULT, "PC"): ImageEnum.PC,
(NodeType.DEFAULT, "host"): ImageEnum.HOST, (NodeType.DEFAULT, "host"): ImageEnum.HOST,
@ -107,8 +100,12 @@ class TypeToImage:
(NodeType.TUNNEL, ""): ImageEnum.TUNNEL, (NodeType.TUNNEL, ""): ImageEnum.TUNNEL,
(NodeType.DOCKER, ""): ImageEnum.DOCKER, (NodeType.DOCKER, ""): ImageEnum.DOCKER,
(NodeType.LXC, ""): ImageEnum.LXC, (NodeType.LXC, ""): ImageEnum.LXC,
} }
@classmethod
def get(cls, node_type, model) -> Optional[ImageEnum]: def from_node(node: Node, *, scale: float) -> Optional[PhotoImage]:
return cls.type_to_image.get((node_type, model)) 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 import netaddr
from netaddr import EUI, IPNetwork 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.graph.node import CanvasNode
from core.gui.nodeutils import NodeUtils
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application 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]: def get_index(iface: Interface) -> Optional[int]:
if not iface.ip4: if not iface.ip4:
@ -47,25 +53,24 @@ class InterfaceManager:
self.app: "Application" = app self.app: "Application" = app
ip4 = self.app.guiconfig.ips.ip4 ip4 = self.app.guiconfig.ips.ip4
ip6 = self.app.guiconfig.ips.ip6 ip6 = self.app.guiconfig.ips.ip6
self.ip4_mask: int = 24 self.ip4_subnets: IPNetwork = IPNetwork(f"{ip4}/{IP4_MASK}")
self.ip6_mask: int = 64 self.ip6_subnets: IPNetwork = IPNetwork(f"{ip6}/{IP6_MASK}")
self.ip4_subnets: IPNetwork = IPNetwork(f"{ip4}/{self.ip4_mask}")
self.ip6_subnets: IPNetwork = IPNetwork(f"{ip6}/{self.ip6_mask}")
mac = self.app.guiconfig.mac mac = self.app.guiconfig.mac
self.mac: EUI = EUI(mac, dialect=netaddr.mac_unix_expanded) self.mac: EUI = EUI(mac, dialect=netaddr.mac_unix_expanded)
self.current_mac: Optional[EUI] = None self.current_mac: Optional[EUI] = None
self.current_subnets: Optional[Subnets] = None self.current_subnets: Optional[Subnets] = None
self.used_subnets: Dict[Tuple[IPNetwork, IPNetwork], Subnets] = {} self.used_subnets: Dict[Tuple[IPNetwork, IPNetwork], Subnets] = {}
self.used_macs: Set[str] = set()
def update_ips(self, ip4: str, ip6: str) -> None: def update_ips(self, ip4: str, ip6: str) -> None:
self.reset() self.reset()
self.ip4_subnets = IPNetwork(f"{ip4}/{self.ip4_mask}") self.ip4_subnets = IPNetwork(f"{ip4}/{IP4_MASK}")
self.ip6_subnets = IPNetwork(f"{ip6}/{self.ip6_mask}") self.ip6_subnets = IPNetwork(f"{ip6}/{IP6_MASK}")
def reset_mac(self) -> None:
self.current_mac = self.mac
def next_mac(self) -> str: 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) mac = str(self.current_mac)
value = self.current_mac.value + 1 value = self.current_mac.value + 1
self.current_mac = EUI(value, dialect=netaddr.mac_unix_expanded) self.current_mac = EUI(value, dialect=netaddr.mac_unix_expanded)
@ -114,6 +119,15 @@ class InterfaceManager:
subnets.used_indexes.discard(index) subnets.used_indexes.discard(index)
self.current_subnets = None 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: def joined(self, links: List[Link]) -> None:
ifaces = [] ifaces = []
for link in links: for link in links:
@ -133,7 +147,7 @@ class InterfaceManager:
self.used_subnets[subnets.key()] = subnets self.used_subnets[subnets.key()] = subnets
def next_index(self, node: Node) -> int: def next_index(self, node: Node) -> int:
if NodeUtils.is_router_node(node): if nutils.is_router(node):
index = 1 index = 1
else: else:
index = 20 index = 20
@ -153,10 +167,10 @@ class InterfaceManager:
def get_subnets(self, iface: Interface) -> Subnets: def get_subnets(self, iface: Interface) -> Subnets:
ip4_subnet = self.ip4_subnets ip4_subnet = self.ip4_subnets
if iface.ip4: 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 ip6_subnet = self.ip6_subnets
if iface.ip6: 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) subnets = Subnets(ip4_subnet, ip6_subnet)
return self.used_subnets.get(subnets.key(), subnets) return self.used_subnets.get(subnets.key(), subnets)
@ -165,8 +179,8 @@ class InterfaceManager:
) -> None: ) -> None:
src_node = canvas_src_node.core_node src_node = canvas_src_node.core_node
dst_node = canvas_dst_node.core_node dst_node = canvas_dst_node.core_node
is_src_container = NodeUtils.is_container_node(src_node.type) is_src_container = nutils.is_container(src_node)
is_dst_container = NodeUtils.is_container_node(dst_node.type) is_dst_container = nutils.is_container(dst_node)
if is_src_container and is_dst_container: if is_src_container and is_dst_container:
self.current_subnets = self.next_subnets() self.current_subnets = self.next_subnets()
elif is_src_container and not is_dst_container: elif is_src_container and not is_dst_container:
@ -188,19 +202,16 @@ class InterfaceManager:
self, canvas_node: CanvasNode, visited: Set[int] = None self, canvas_node: CanvasNode, visited: Set[int] = None
) -> Optional[IPNetwork]: ) -> Optional[IPNetwork]:
logging.info("finding subnet for node: %s", canvas_node.core_node.name) logging.info("finding subnet for node: %s", canvas_node.core_node.name)
canvas = self.app.canvas
subnets = None subnets = None
if not visited: if not visited:
visited = set() visited = set()
visited.add(canvas_node.core_node.id) visited.add(canvas_node.core_node.id)
for edge in canvas_node.edges: for edge in canvas_node.edges:
src_node = canvas.nodes[edge.src]
dst_node = canvas.nodes[edge.dst]
iface = edge.link.iface1 iface = edge.link.iface1
check_node = src_node check_node = edge.src
if src_node == canvas_node: if edge.src == canvas_node:
iface = edge.link.iface2 iface = edge.link.iface2
check_node = dst_node check_node = edge.dst
if check_node.core_node.id in visited: if check_node.core_node.id in visited:
continue continue
visited.add(check_node.core_node.id) visited.add(check_node.core_node.id)
@ -212,3 +223,48 @@ class InterfaceManager:
logging.info("found subnets: %s", subnets) logging.info("found subnets: %s", subnets)
break break
return subnets 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 tkinter import filedialog, messagebox
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from core.gui import images
from core.gui.coreclient import CoreClient from core.gui.coreclient import CoreClient
from core.gui.dialogs.about import AboutDialog from core.gui.dialogs.about import AboutDialog
from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog 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.sessionoptions import SessionOptionsDialog
from core.gui.dialogs.sessions import SessionsDialog from core.gui.dialogs.sessions import SessionsDialog
from core.gui.dialogs.throughput import ThroughputDialog from core.gui.dialogs.throughput import ThroughputDialog
from core.gui.graph.graph import CanvasGraph from core.gui.graph.manager import CanvasManager
from core.gui.nodeutils import ICON_SIZE
from core.gui.observers import ObserversMenu from core.gui.observers import ObserversMenu
from core.gui.task import ProgressTask from core.gui.task import ProgressTask
@ -45,9 +45,10 @@ class Menubar(tk.Menu):
super().__init__(app) super().__init__(app)
self.app: "Application" = app self.app: "Application" = app
self.core: CoreClient = app.core self.core: CoreClient = app.core
self.canvas: CanvasGraph = app.canvas self.manager: CanvasManager = app.manager
self.recent_menu: Optional[tk.Menu] = None self.recent_menu: Optional[tk.Menu] = None
self.edit_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.observers_menu: Optional[ObserversMenu] = None
self.draw() self.draw()
@ -106,6 +107,7 @@ class Menubar(tk.Menu):
menu = tk.Menu(self) menu = tk.Menu(self)
menu.add_command(label="Preferences", command=self.click_preferences) menu.add_command(label="Preferences", command=self.click_preferences)
menu.add_command(label="Custom Nodes", command=self.click_custom_nodes) 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_separator()
menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED) menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED)
menu.add_command(label="Redo", accelerator="Ctrl+Y", 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( menu.add_command(
label="Delete", accelerator="Ctrl+D", command=self.click_delete 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.add_cascade(label="Edit", menu=menu)
self.app.master.bind_all("<Control-x>", self.click_cut) 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-c>", self.click_copy)
self.app.master.bind_all("<Control-v>", self.click_paste) 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-d>", self.click_delete)
self.app.master.bind_all("<Control-h>", self.click_hide)
self.edit_menu = menu self.edit_menu = menu
def draw_canvas_menu(self) -> None: def draw_canvas_menu(self) -> None:
@ -128,9 +132,13 @@ class Menubar(tk.Menu):
Create canvas menu Create canvas menu
""" """
menu = tk.Menu(self) 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_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) menu.add_command(label="Wallpaper", command=self.click_canvas_wallpaper)
self.add_cascade(label="Canvas", menu=menu) self.add_cascade(label="Canvas", menu=menu)
self.canvas_menu = menu
def draw_view_menu(self) -> None: def draw_view_menu(self) -> None:
""" """
@ -145,52 +153,52 @@ class Menubar(tk.Menu):
menu.add_checkbutton( menu.add_checkbutton(
label="Interface Names", label="Interface Names",
command=self.click_edge_label_change, command=self.click_edge_label_change,
variable=self.canvas.show_iface_names, variable=self.manager.show_iface_names,
) )
menu.add_checkbutton( menu.add_checkbutton(
label="IPv4 Addresses", label="IPv4 Addresses",
command=self.click_edge_label_change, command=self.click_edge_label_change,
variable=self.canvas.show_ip4s, variable=self.manager.show_ip4s,
) )
menu.add_checkbutton( menu.add_checkbutton(
label="IPv6 Addresses", label="IPv6 Addresses",
command=self.click_edge_label_change, command=self.click_edge_label_change,
variable=self.canvas.show_ip6s, variable=self.manager.show_ip6s,
) )
menu.add_checkbutton( menu.add_checkbutton(
label="Node Labels", label="Node Labels",
command=self.canvas.show_node_labels.click_handler, command=self.manager.show_node_labels.click_handler,
variable=self.canvas.show_node_labels, variable=self.manager.show_node_labels,
) )
menu.add_checkbutton( menu.add_checkbutton(
label="Link Labels", label="Link Labels",
command=self.canvas.show_link_labels.click_handler, command=self.manager.show_link_labels.click_handler,
variable=self.canvas.show_link_labels, variable=self.manager.show_link_labels,
) )
menu.add_checkbutton( menu.add_checkbutton(
label="Links", label="Links",
command=self.canvas.show_links.click_handler, command=self.manager.show_links.click_handler,
variable=self.canvas.show_links, variable=self.manager.show_links,
) )
menu.add_checkbutton( menu.add_checkbutton(
label="Loss Links", label="Loss Links",
command=self.canvas.show_loss_links.click_handler, command=self.manager.show_loss_links.click_handler,
variable=self.canvas.show_loss_links, variable=self.manager.show_loss_links,
) )
menu.add_checkbutton( menu.add_checkbutton(
label="Wireless Links", label="Wireless Links",
command=self.canvas.show_wireless.click_handler, command=self.manager.show_wireless.click_handler,
variable=self.canvas.show_wireless, variable=self.manager.show_wireless,
) )
menu.add_checkbutton( menu.add_checkbutton(
label="Annotations", label="Annotations",
command=self.canvas.show_annotations.click_handler, command=self.manager.show_annotations.click_handler,
variable=self.canvas.show_annotations, variable=self.manager.show_annotations,
) )
menu.add_checkbutton( menu.add_checkbutton(
label="Canvas Grid", label="Canvas Grid",
command=self.canvas.show_grid.click_handler, command=self.manager.show_grid.click_handler,
variable=self.canvas.show_grid, variable=self.manager.show_grid,
) )
self.add_cascade(label="View", menu=menu) self.add_cascade(label="View", menu=menu)
@ -334,17 +342,12 @@ class Menubar(tk.Menu):
self.app.save_config() self.app.save_config()
self.app.menubar.update_recent_files() self.app.menubar.update_recent_files()
def change_menubar_item_state(self, is_runtime: bool) -> None: def set_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 state = tk.DISABLED if is_runtime else tk.NORMAL
self.edit_menu.entryconfig(i, state=state) for entry in {"Copy", "Paste", "Delete", "Cut"}:
except tk.TclError: self.edit_menu.entryconfigure(entry, state=state)
pass for entry in {"Delete"}:
self.canvas_menu.entryconfigure(entry, state=state)
def prompt_save_running_session(self, quit_app: bool = False) -> None: def prompt_save_running_session(self, quit_app: bool = False) -> None:
""" """
@ -372,6 +375,12 @@ class Menubar(tk.Menu):
dialog = PreferencesDialog(self.app) dialog = PreferencesDialog(self.app)
dialog.show() 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: def click_canvas_size_and_scale(self) -> None:
dialog = SizeAndScaleDialog(self.app) dialog = SizeAndScaleDialog(self.app)
dialog.show() dialog.show()
@ -401,17 +410,29 @@ class Menubar(tk.Menu):
dialog.show() dialog.show()
def click_copy(self, _event: tk.Event = None) -> None: 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: 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: 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: def click_cut(self, _event: tk.Event = None) -> None:
self.canvas.copy() canvas = self.manager.current()
self.canvas.delete_selected_objects() 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: def click_session_options(self) -> None:
logging.debug("Click options") logging.debug("Click options")
@ -439,14 +460,15 @@ class Menubar(tk.Menu):
dialog.show() dialog.show()
def click_autogrid(self) -> None: def click_autogrid(self) -> None:
width, height = self.canvas.current_dimensions width, height = self.manager.current_dimensions
padding = (ICON_SIZE / 2) + 10 padding = (images.NODE_SIZE / 2) + 10
layout_size = padding + ICON_SIZE layout_size = padding + images.NODE_SIZE
col_count = width // layout_size col_count = width // layout_size
logging.info( logging.info(
"auto grid layout: dimension(%s, %s) col(%s)", width, height, col_count "auto grid layout: dimension(%s, %s) col(%s)", width, height, col_count
) )
for i, node in enumerate(self.canvas.nodes.values()): canvas = self.manager.current()
for i, node in enumerate(canvas.nodes.values()):
col = i % col_count col = i % col_count
row = i // col_count row = i // col_count
x = (col * layout_size) + padding x = (col * layout_size) + padding
@ -460,7 +482,7 @@ class Menubar(tk.Menu):
self.app.hide_info() self.app.hide_info()
def click_edge_label_change(self) -> None: def click_edge_label_change(self) -> None:
for edge in self.canvas.edges.values(): for edge in self.manager.edges.values():
edge.draw_labels() edge.draw_labels()
def click_mac_config(self) -> None: def click_mac_config(self) -> None:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -581,7 +581,7 @@ if [ "x$1" = "xstart" ]; then
""" """
for iface in node.get_ifaces(): for iface in node.get_ifaces():
if hasattr(iface, "control") and iface.control is True: if iface.control:
cfg += "# " cfg += "# "
redir = "< /dev/null" redir = "< /dev/null"
cfg += "tcpdump ${DUMPOPTS} -w %s.%s.pcap -i %s %s &\n" % ( 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.base import CoreNodeBase, NodeBase
from core.nodes.docker import DockerNode from core.nodes.docker import DockerNode
from core.nodes.lxd import LxcNode 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 from core.services.coreservices import CoreService
if TYPE_CHECKING: if TYPE_CHECKING:
@ -253,15 +253,13 @@ class DeviceElement(NodeElement):
class NetworkElement(NodeElement): class NetworkElement(NodeElement):
def __init__(self, session: "Session", node: NodeBase) -> None: def __init__(self, session: "Session", node: NodeBase) -> None:
super().__init__(session, node, "network") super().__init__(session, node, "network")
model = getattr(self.node, "model", None) if isinstance(self.node, (WlanNode, EmaneNet)):
if model: if self.node.model:
add_attribute(self.element, "model", model.name) add_attribute(self.element, "model", self.node.model.name)
mobility = getattr(self.node, "mobility", None) if self.node.mobility:
if mobility: add_attribute(self.element, "mobility", self.node.mobility.name)
add_attribute(self.element, "mobility", mobility.name) if isinstance(self.node, GreTapBridge):
grekey = getattr(self.node, "grekey", None) add_attribute(self.element, "grekey", self.node.grekey)
if grekey and grekey is not None:
add_attribute(self.element, "grekey", grekey)
self.add_type() self.add_type()
def add_type(self) -> None: 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) core.add_link(session_id, n1_id, emane_id, iface1)
iface1 = iface_helper.create_iface(n2_id, 0) iface1 = iface_helper.create_iface(n2_id, 0)
core.add_link(session_id, n2_id, emane_id, iface1) 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; Geo geo = 12;
string dir = 13; string dir = 13;
string channel = 14; string channel = 14;
int32 canvas = 15;
} }
message Link { message Link {

View file

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

View file

@ -262,6 +262,15 @@ def query_node(core: CoreGrpcClient, args: Namespace) -> None:
print_iface(iface) 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 @coreclient
def add_node(core: CoreGrpcClient, args: Namespace) -> None: def add_node(core: CoreGrpcClient, args: Namespace) -> None:
session_id = get_current_session(core, args.session) 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}") 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: def setup_node_parser(parent: _SubParsersAction) -> None:
parser = parent.add_parser("node", help="node interactions") parser = parent.add_parser("node", help="node interactions")
parser.formatter_class = ArgumentDefaultsHelpFormatter parser.formatter_class = ArgumentDefaultsHelpFormatter
@ -528,6 +550,7 @@ def main() -> None:
subparsers = parser.add_subparsers(help="supported commands") subparsers = parser.add_subparsers(help="supported commands")
subparsers.required = True subparsers.required = True
subparsers.dest = "command" subparsers.dest = "command"
setup_sessions_parser(subparsers)
setup_node_parser(subparsers) setup_node_parser(subparsers)
setup_link_parser(subparsers) setup_link_parser(subparsers)
setup_query_parser(subparsers) setup_query_parser(subparsers)

View file

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

View file

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