Merge pull request #450 from coreemu/develop

Develop
This commit is contained in:
bharnden 2020-05-12 23:33:53 -07:00 committed by GitHub
commit cc7c1348ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
146 changed files with 3897 additions and 3032 deletions

View file

@ -1,3 +1,33 @@
## 2020-05-11 CORE 6.4.0
* Enhancements
* updates to core-route-monitor, allow specific session, configurable settings, and properly
listen on all interfaces
* install.sh now has a "-r" option to help with reinstalling from current branch and installing
current python dependencies
* \#202 - enable OSPFv2 fast convergence
* \#178 - added comments to OVS service
* Python GUI Enhancements
* added initial documentation to help support usage
* supports drawing multiple links for wireless connections
* supports differentiating wireless networks with different colored links
* implemented unlink in node context menu to delete links to other nodes
* implemented node run tool dialog
* implemented find node dialog
* implemented address configuration dialog
* implemented mac configuration dialog
* updated link address creation to more closely mimic prior behavior
* updated configuration to use yaml class based configs
* implemented auto grid layout for nodes
* fixed drawn wlan ranges during configuration
* Bugfixes
* no longer writes link option data for WLAN/EMANE links in XML
* avoid configuring links for WLAN/EMANE link options in XML, due to them being written to XML prior
* updates to allow building python docs again
* \#431 - peer to peer node uplink link data was not using an enum properly due to code changes
* \#432 - loading XML was not setting EMANE nodes model
* \#435 - loading XML was not maintaining existing session options
* \#448 - fixed issue sorting hooks being saved to XML
## 2020-04-13 CORE 6.3.0
* Features
* \#424 - added FRR IS-IS service

View file

@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script.
# this defines the CORE version number, must be static for AC_INIT
AC_INIT(core, 6.3.0)
AC_INIT(core, 6.4.0)
# autoconf and automake initialization
AC_CONFIG_SRCDIR([netns/version.h.in])
@ -43,6 +43,11 @@ AC_ARG_ENABLE([gui],
[build and install the GUI (default is yes)])],
[], [enable_gui=yes])
AC_SUBST(enable_gui)
AC_ARG_ENABLE([docs],
[AS_HELP_STRING([--enable-docs[=ARG]],
[build python documentation (default is no)])],
[], [enable_docs=no])
AC_SUBST(enable_docs)
AC_ARG_ENABLE([python],
[AS_HELP_STRING([--enable-python[=ARG]],
@ -191,8 +196,7 @@ if [ test "x$enable_daemon" = "xyes" || test "x$enable_vnodedonly" = "xyes" ] ;
fi
want_docs=no
if test "x$enable_docs" = "xyes" ; then
if [test "x$want_python" = "xyes" && test "x$enable_docs" = "xyes"] ; then
AC_CHECK_PROG(help2man, help2man, yes, no, $SEARCHPATH)
if test "x$help2man" = "xno" ; then
@ -210,21 +214,12 @@ if test "x$enable_docs" = "xyes" ; then
# check for sphinx required during make
AC_CHECK_PROG(sphinxapi_path, sphinx-apidoc, $as_dir, no, $SEARCHPATH)
if test "x$sphinxapi_path" = "xno" ; then
AC_MSG_ERROR(["Could not location sphinx-apidoc, from the python-sphinx package"])
AC_MSG_ERROR(["Could not locate sphinx-apidoc, install python3 -m pip install sphinx"])
want_docs=no
fi
AS_IF([$PYTHON -c "import sphinx_rtd_theme" &> /dev/null], [], [AC_MSG_ERROR([doc dependency missing, please install python3 -m pip install sphinx-rtd-theme])])
fi
#AC_PATH_PROGS(tcl_path, [tclsh tclsh8.5 tclsh8.4], no)
#if test "x$tcl_path" = "xno" ; then
# AC_MSG_ERROR([Could not locate tclsh. Please install Tcl/Tk.])
#fi
#AC_PATH_PROGS(wish_path, [wish wish8.5 wish8.4], no)
#if test "x$wish_path" = "xno" ; then
# AC_MSG_ERROR([Could not locate wish. Please install Tcl/Tk.])
#fi
AC_ARG_WITH([startup],
[AS_HELP_STRING([--with-startup=option],
[option=systemd,suse,none to install systemd/SUSE init scripts])],

View file

@ -5,7 +5,7 @@ verify_ssl = true
[scripts]
core = "python scripts/core-daemon -f data/core.conf -l data/logging.conf"
coretk = "python scripts/coretk-gui"
core-pygui = "python scripts/core-pygui"
test = "pytest -v tests"
test-mock = "pytest -v --mock tests"
test-emane = "pytest -v tests/emane"

View file

@ -493,7 +493,6 @@ class CoreGrpcClient:
"""
request = core_pb2.EventsRequest(session_id=session_id, events=events)
stream = self.stub.Events(request)
logging.info("STREAM TYPE: %s", type(stream))
start_streamer(stream, handler)
return stream

View file

@ -3,7 +3,7 @@ from queue import Empty, Queue
from typing import Iterable
from core.api.grpc import core_pb2
from core.api.grpc.grpcutils import convert_value
from core.api.grpc.grpcutils import convert_link
from core.emulator.data import (
ConfigData,
EventData,
@ -40,51 +40,7 @@ def handle_link_event(event: LinkData) -> core_pb2.LinkEvent:
:param event: link data
:return: link event that has message type and link information
"""
interface_one = None
if event.interface1_id is not None:
interface_one = core_pb2.Interface(
id=event.interface1_id,
name=event.interface1_name,
mac=convert_value(event.interface1_mac),
ip4=convert_value(event.interface1_ip4),
ip4mask=event.interface1_ip4_mask,
ip6=convert_value(event.interface1_ip6),
ip6mask=event.interface1_ip6_mask,
)
interface_two = None
if event.interface2_id is not None:
interface_two = core_pb2.Interface(
id=event.interface2_id,
name=event.interface2_name,
mac=convert_value(event.interface2_mac),
ip4=convert_value(event.interface2_ip4),
ip4mask=event.interface2_ip4_mask,
ip6=convert_value(event.interface2_ip6),
ip6mask=event.interface2_ip6_mask,
)
options = core_pb2.LinkOptions(
opaque=event.opaque,
jitter=event.jitter,
key=event.key,
mburst=event.mburst,
mer=event.mer,
per=event.per,
bandwidth=event.bandwidth,
burst=event.burst,
delay=event.delay,
dup=event.dup,
unidirectional=event.unidirectional,
)
link = core_pb2.Link(
type=event.link_type.value,
node_one_id=event.node1_id,
node_two_id=event.node2_id,
interface_one=interface_one,
interface_two=interface_two,
options=options,
)
link = convert_link(event)
return core_pb2.LinkEvent(message_type=event.message_type.value, link=link)

View file

@ -13,7 +13,7 @@ from core.emulator.data import LinkData
from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions
from core.emulator.enumerations import LinkTypes, NodeTypes
from core.emulator.session import Session
from core.nodes.base import CoreNetworkBase, NodeBase
from core.nodes.base import NodeBase
from core.nodes.interface import CoreInterface
from core.services.coreservices import CoreService
@ -263,17 +263,16 @@ def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node:
)
def get_links(session: Session, node: NodeBase):
def get_links(node: NodeBase):
"""
Retrieve a list of links for grpc to use
Retrieve a list of links for grpc to use.
:param session: node's section
:param node: node to get links from
:return: [core.api.grpc.core_pb2.Link]
:return: protobuf links
"""
links = []
for link_data in node.all_link_data():
link = convert_link(session, link_data)
link = convert_link(link_data)
links.append(link)
return links
@ -307,48 +306,35 @@ def parse_emane_model_id(_id: int) -> Tuple[int, int]:
return node_id, interface
def convert_link(session: Session, link_data: LinkData) -> core_pb2.Link:
def convert_link(link_data: LinkData) -> core_pb2.Link:
"""
Convert link_data into core protobuf Link
Convert link_data into core protobuf link.
:param session:
:param link_data:
:param link_data: link to convert
:return: core protobuf Link
"""
interface_one = None
if link_data.interface1_id is not None:
node = session.get_node(link_data.node1_id)
interface_name = None
if not isinstance(node, CoreNetworkBase):
interface = node.netif(link_data.interface1_id)
interface_name = interface.name
interface_one = core_pb2.Interface(
id=link_data.interface1_id,
name=interface_name,
name=link_data.interface1_name,
mac=convert_value(link_data.interface1_mac),
ip4=convert_value(link_data.interface1_ip4),
ip4mask=link_data.interface1_ip4_mask,
ip6=convert_value(link_data.interface1_ip6),
ip6mask=link_data.interface1_ip6_mask,
)
interface_two = None
if link_data.interface2_id is not None:
node = session.get_node(link_data.node2_id)
interface_name = None
if not isinstance(node, CoreNetworkBase):
interface = node.netif(link_data.interface2_id)
interface_name = interface.name
interface_two = core_pb2.Interface(
id=link_data.interface2_id,
name=interface_name,
name=link_data.interface2_name,
mac=convert_value(link_data.interface2_mac),
ip4=convert_value(link_data.interface2_ip4),
ip4mask=link_data.interface2_ip4_mask,
ip6=convert_value(link_data.interface2_ip6),
ip6mask=link_data.interface2_ip6_mask,
)
options = core_pb2.LinkOptions(
opaque=link_data.opaque,
jitter=link_data.jitter,
@ -362,7 +348,6 @@ def convert_link(session: Session, link_data: LinkData) -> core_pb2.Link:
dup=link_data.dup,
unidirectional=link_data.unidirectional,
)
return core_pb2.Link(
type=link_data.link_type.value,
node_one_id=link_data.node1_id,
@ -370,6 +355,9 @@ def convert_link(session: Session, link_data: LinkData) -> core_pb2.Link:
interface_one=interface_one,
interface_two=interface_two,
options=options,
network_id=link_data.network_id,
label=link_data.label,
color=link_data.color,
)

View file

@ -297,7 +297,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
for service_exception in boot_exception.args:
exceptions.append(str(service_exception))
return core_pb2.StartSessionResponse(result=False, exceptions=exceptions)
return core_pb2.StartSessionResponse(result=True)
def StopSession(
@ -543,7 +542,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
continue
node_proto = grpcutils.get_node_proto(session, node)
nodes.append(node_proto)
node_links = get_links(session, node)
node_links = get_links(node)
links.extend(node_links)
session_proto = core_pb2.Session(
@ -788,7 +787,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("get node links: %s", request)
session = self.get_session(request.session_id, context)
node = self.get_node(session, request.node_id, context)
links = get_links(session, node)
links = get_links(node)
return core_pb2.GetNodeLinksResponse(links=links)
def AddLink(
@ -1031,8 +1030,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
Retrieve all the default services of all node types in a session
:param request:
get-default-service request
:param request: get-default-service request
:param context: context object
:return: get-service-defaults response about all the available default services
"""
@ -1050,8 +1048,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
) -> SetServiceDefaultsResponse:
"""
Set new default services to the session after whipping out the old ones
:param request: set-service-defaults
request
:param request: set-service-defaults request
:param context: context object
:return: set-service-defaults response
"""
@ -1494,12 +1492,14 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
flag = MessageFlags.ADD
else:
flag = MessageFlags.DELETE
color = session.get_link_color(emane_one.id)
link = LinkData(
message_type=flag,
link_type=LinkTypes.WIRELESS,
node1_id=node_one.id,
node2_id=node_two.id,
network_id=emane_one.id,
color=color,
)
session.broadcast_link(link)
return EmaneLinkResponse(result=True)

View file

@ -350,8 +350,7 @@ class CoreTlvDataMacAddr(CoreTlvDataObj):
"""
# only use 48 bits
value = binascii.hexlify(value[2:]).decode()
mac = netaddr.EUI(value)
mac.dialect = netaddr.mac_unix
mac = netaddr.EUI(value, dialect=netaddr.mac_unix_expanded)
return str(mac)

View file

@ -367,14 +367,12 @@ class CoreHandler(socketserver.BaseRequestHandler):
(LinkTlvs.NETWORK_ID, link_data.network_id),
(LinkTlvs.KEY, link_data.key),
(LinkTlvs.INTERFACE1_NUMBER, link_data.interface1_id),
(LinkTlvs.INTERFACE1_NAME, link_data.interface1_name),
(LinkTlvs.INTERFACE1_IP4, link_data.interface1_ip4),
(LinkTlvs.INTERFACE1_IP4_MASK, link_data.interface1_ip4_mask),
(LinkTlvs.INTERFACE1_MAC, link_data.interface1_mac),
(LinkTlvs.INTERFACE1_IP6, link_data.interface1_ip6),
(LinkTlvs.INTERFACE1_IP6_MASK, link_data.interface1_ip6_mask),
(LinkTlvs.INTERFACE2_NUMBER, link_data.interface2_id),
(LinkTlvs.INTERFACE2_NAME, link_data.interface2_name),
(LinkTlvs.INTERFACE2_IP4, link_data.interface2_ip4),
(LinkTlvs.INTERFACE2_IP4_MASK, link_data.interface2_ip4_mask),
(LinkTlvs.INTERFACE2_MAC, link_data.interface2_mac),
@ -2062,7 +2060,7 @@ class CoreUdpHandler(CoreHandler):
if not isinstance(message, (coreapi.CoreNodeMessage, coreapi.CoreLinkMessage)):
return
clients = self.tcp_handler.session_clients[self.session.id]
clients = self.tcp_handler.session_clients.get(self.session.id, [])
for client in clients:
try:
client.sendall(message.raw_message)

View file

@ -4,7 +4,6 @@ import netaddr
from core import utils
from core.configservice.base import ConfigService, ConfigServiceMode
from core.nodes.base import CoreNode
GROUP_NAME = "Utility"
@ -26,18 +25,14 @@ class DefaultRouteService(ConfigService):
def data(self) -> Dict[str, Any]:
# only add default routes for linked routing nodes
routes = []
for other_node in self.node.session.nodes.values():
if not isinstance(other_node, CoreNode):
continue
if other_node.type not in ["router", "mdr"]:
continue
commonnets = self.node.commonnets(other_node)
if commonnets:
_, _, router_eth = commonnets[0]
for x in router_eth.addrlist:
addr, prefix = x.split("/")
routes.append(addr)
break
netifs = self.node.netifs(sort=True)
if netifs:
netif = netifs[0]
for x in netif.addrlist:
net = netaddr.IPNetwork(x).cidr
if net.size > 1:
router = net[1]
routes.append(str(router))
return dict(routes=routes)

View file

@ -314,6 +314,7 @@ class EmaneLinkMonitor:
node_two: int,
emane_id: int,
) -> None:
color = self.emane_manager.session.get_link_color(emane_id)
link_data = LinkData(
message_type=message_type,
label=label,
@ -321,6 +322,7 @@ class EmaneLinkMonitor:
node2_id=node_two,
network_id=emane_id,
link_type=LinkTypes.WIRELESS,
color=color,
)
self.emane_manager.session.broadcast_link(link_data)

View file

@ -128,5 +128,4 @@ class CoreEmu:
result = True
else:
logging.error("session to delete did not exist: %s", _id)
return result

View file

@ -129,3 +129,4 @@ class LinkData:
interface2_ip6: str = None
interface2_ip6_mask: int = None
opaque: str = None
color: str = None

View file

@ -76,6 +76,7 @@ NODES = {
}
NODES_TYPE = {NODES[x]: x for x in NODES}
CTRL_NET_ID = 9001
LINK_COLORS = ["green", "blue", "orange", "purple", "turquoise"]
class Session:
@ -105,6 +106,7 @@ class Session:
self.thumbnail = None
self.user = None
self.event_loop = EventLoop()
self.link_colors = {}
# dict of nodes: all nodes and nets
self.node_id_gen = IdGen()
@ -355,7 +357,9 @@ class Session:
)
interface = create_interface(node_one, net_one, interface_one)
node_one_interface = interface
link_config(net_one, interface, link_options)
wireless_net = isinstance(net_one, (EmaneNet, WlanNode))
if not wireless_net:
link_config(net_one, interface, link_options)
# network to node
if node_two and net_one:
@ -366,7 +370,8 @@ class Session:
)
interface = create_interface(node_two, net_one, interface_two)
node_two_interface = interface
if not link_options.unidirectional:
wireless_net = isinstance(net_one, (EmaneNet, WlanNode))
if not link_options.unidirectional and not wireless_net:
link_config(net_one, interface, link_options)
# network to network
@ -693,6 +698,7 @@ class Session:
# generate name if not provided
if not options:
options = NodeOptions()
options.set_position(0, 0)
name = options.name
if not name:
name = f"{node_class.__name__}{_id}"
@ -807,9 +813,7 @@ class Session:
node.setposition(x, y, None)
node.position.set_geo(lon, lat, alt)
self.broadcast_node(node)
else:
if has_empty_position:
x, y = 0, 0
elif not has_empty_position:
node.setposition(x, y, None)
def start_mobility(self, node_ids: List[int] = None) -> None:
@ -927,6 +931,7 @@ class Session:
self.location.reset()
self.services.reset()
self.mobility.config_reset()
self.link_colors.clear()
def start_events(self) -> None:
"""
@ -1956,3 +1961,17 @@ class Session:
else:
node = self.get_node(node_id)
node.cmd(data, wait=False)
def get_link_color(self, network_id: int) -> str:
"""
Assign a color for links associated with a network.
:param network_id: network to get a link color for
:return: link color
"""
color = self.link_colors.get(network_id)
if not color:
index = len(self.link_colors) % len(LINK_COLORS)
color = LINK_COLORS[index]
self.link_colors[network_id] = color
return color

View file

@ -22,3 +22,11 @@ class CoreError(Exception):
"""
pass
class CoreXmlError(Exception):
"""
Used when there was an error parsing a CORE xml file.
"""
pass

View file

@ -1,34 +1,38 @@
import logging
import math
import tkinter as tk
from tkinter import font, ttk
from tkinter.ttk import Progressbar
import grpc
from core.gui import appconfig, themes
from core.gui.coreclient import CoreClient
from core.gui.dialogs.error import ErrorDialog
from core.gui.graph.graph import CanvasGraph
from core.gui.images import ImageEnum, Images
from core.gui.menuaction import MenuAction
from core.gui.menubar import Menubar
from core.gui.nodeutils import NodeUtils
from core.gui.statusbar import StatusBar
from core.gui.toolbar import Toolbar
from core.gui.validation import InputValidation
WIDTH = 1000
HEIGHT = 800
class Application(tk.Frame):
def __init__(self, proxy: bool):
super().__init__(master=None)
class Application(ttk.Frame):
def __init__(self, proxy: bool) -> None:
super().__init__()
# load node icons
NodeUtils.setup()
# widgets
self.menubar = None
self.toolbar = None
self.right_frame = None
self.canvas = None
self.statusbar = None
self.validation = None
self.progress = None
# fonts
self.fonts_size = None
@ -37,7 +41,7 @@ class Application(tk.Frame):
# setup
self.guiconfig = appconfig.read()
self.app_scale = self.guiconfig["scale"]
self.app_scale = self.guiconfig.scale
self.setup_scaling()
self.style = ttk.Style()
self.setup_theme()
@ -46,29 +50,46 @@ class Application(tk.Frame):
self.draw()
self.core.setup()
def setup_scaling(self):
def setup_scaling(self) -> None:
self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()}
text_scale = self.app_scale if self.app_scale < 1 else math.sqrt(self.app_scale)
themes.scale_fonts(self.fonts_size, self.app_scale)
self.icon_text_font = font.Font(family="TkIconFont", size=int(12 * text_scale))
self.edge_font = font.Font(family="TkDefaultFont", size=int(8 * text_scale))
self.edge_font = font.Font(
family="TkDefaultFont", size=int(8 * text_scale), weight=font.BOLD
)
def setup_theme(self):
def setup_theme(self) -> None:
themes.load(self.style)
self.master.bind_class("Menu", "<<ThemeChanged>>", themes.theme_change_menu)
self.master.bind("<<ThemeChanged>>", themes.theme_change)
self.style.theme_use(self.guiconfig["preferences"]["theme"])
self.style.theme_use(self.guiconfig.preferences.theme)
def setup_app(self):
def setup_app(self) -> None:
self.master.title("CORE")
self.center()
self.master.protocol("WM_DELETE_WINDOW", self.on_closing)
image = Images.get(ImageEnum.CORE, 16)
self.master.tk.call("wm", "iconphoto", self.master._w, image)
self.pack(fill=tk.BOTH, expand=True)
self.validation = InputValidation(self)
self.master.option_add("*tearOff", tk.FALSE)
self.setup_file_dialogs()
def center(self):
def setup_file_dialogs(self) -> None:
"""
Hack code that needs to initialize a bad dialog so that we can apply,
global settings for dialogs to not show hidden files by default and display
the hidden file toggle.
:return: nothing
"""
try:
self.master.tk.call("tk_getOpenFile", "-foobar")
except tk.TclError:
pass
self.master.tk.call("set", "::tk::dialog::file::showHiddenBtn", "1")
self.master.tk.call("set", "::tk::dialog::file::showHiddenVar", "0")
def center(self) -> None:
screen_width = self.master.winfo_screenwidth()
screen_height = self.master.winfo_screenheight()
x = int((screen_width / 2) - (WIDTH * self.app_scale / 2))
@ -77,45 +98,68 @@ class Application(tk.Frame):
f"{int(WIDTH * self.app_scale)}x{int(HEIGHT * self.app_scale)}+{x}+{y}"
)
def draw(self):
self.master.option_add("*tearOff", tk.FALSE)
self.menubar = Menubar(self.master, self)
def draw(self) -> None:
self.master.rowconfigure(0, weight=1)
self.master.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.columnconfigure(1, weight=1)
self.grid(sticky="nsew")
self.toolbar = Toolbar(self, self)
self.toolbar.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2)
self.toolbar.grid(sticky="ns")
self.right_frame = ttk.Frame(self)
self.right_frame.columnconfigure(0, weight=1)
self.right_frame.rowconfigure(0, weight=1)
self.right_frame.grid(row=0, column=1, sticky="nsew")
self.draw_canvas()
self.draw_status()
self.progress = Progressbar(self.right_frame, mode="indeterminate")
self.menubar = Menubar(self.master, self)
def draw_canvas(self):
width = self.guiconfig["preferences"]["width"]
height = self.guiconfig["preferences"]["height"]
self.canvas = CanvasGraph(self, self.core, width, height)
self.canvas.pack(fill=tk.BOTH, expand=True)
def draw_canvas(self) -> None:
width = self.guiconfig.preferences.width
height = self.guiconfig.preferences.height
canvas_frame = ttk.Frame(self.right_frame)
canvas_frame.rowconfigure(0, weight=1)
canvas_frame.columnconfigure(0, weight=1)
canvas_frame.grid(sticky="nsew", pady=1)
self.canvas = CanvasGraph(canvas_frame, self, self.core, width, height)
self.canvas.grid(sticky="nsew")
scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview)
scroll_y.grid(row=0, column=1, sticky="ns")
scroll_x = ttk.Scrollbar(
self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview
canvas_frame, orient=tk.HORIZONTAL, command=self.canvas.xview
)
scroll_x.pack(side=tk.BOTTOM, fill=tk.X)
scroll_y = ttk.Scrollbar(self.canvas, command=self.canvas.yview)
scroll_y.pack(side=tk.RIGHT, fill=tk.Y)
scroll_x.grid(row=1, column=0, sticky="ew")
self.canvas.configure(xscrollcommand=scroll_x.set)
self.canvas.configure(yscrollcommand=scroll_y.set)
def draw_status(self):
self.statusbar = StatusBar(master=self, app=self)
self.statusbar.pack(side=tk.BOTTOM, fill=tk.X)
def draw_status(self) -> None:
self.statusbar = StatusBar(self.right_frame, self)
self.statusbar.grid(sticky="ew")
def on_closing(self):
menu_action = MenuAction(self, self.master)
menu_action.on_quit()
def show_grpc_exception(self, title: str, e: grpc.RpcError) -> None:
logging.exception("app grpc exception", exc_info=e)
message = e.details()
self.show_error(title, message)
def save_config(self):
def show_exception(self, title: str, e: Exception) -> None:
logging.exception("app exception", exc_info=e)
self.show_error(title, str(e))
def show_error(self, title: str, message: str) -> None:
self.after(0, lambda: ErrorDialog(self, title, message).show())
def on_closing(self) -> None:
self.menubar.prompt_save_running_session(True)
def save_config(self) -> None:
appconfig.save(self.guiconfig)
def joined_session_update(self):
self.statusbar.progress_bar.stop()
def joined_session_update(self) -> None:
if self.core.is_runtime():
self.toolbar.set_runtime()
else:
self.toolbar.set_design()
def close(self):
def close(self) -> None:
self.master.destroy()

View file

@ -1,20 +1,20 @@
import os
import shutil
from pathlib import Path
from typing import List, Optional
import yaml
# gui home paths
from core.gui import themes
HOME_PATH = Path.home().joinpath(".coretk")
HOME_PATH = Path.home().joinpath(".coregui")
BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds")
CUSTOM_EMANE_PATH = HOME_PATH.joinpath("custom_emane")
CUSTOM_SERVICE_PATH = HOME_PATH.joinpath("custom_services")
ICONS_PATH = HOME_PATH.joinpath("icons")
MOBILITY_PATH = HOME_PATH.joinpath("mobility")
XMLS_PATH = HOME_PATH.joinpath("xmls")
CONFIG_PATH = HOME_PATH.joinpath("gui.yaml")
CONFIG_PATH = HOME_PATH.joinpath("config.yaml")
LOG_PATH = HOME_PATH.joinpath("gui.log")
SCRIPT_PATH = HOME_PATH.joinpath("scripts")
@ -44,13 +44,151 @@ class IndentDumper(yaml.Dumper):
return super().increase_indent(flow, False)
def copy_files(current_path, new_path):
class CustomNode(yaml.YAMLObject):
yaml_tag = "!CustomNode"
yaml_loader = yaml.SafeLoader
def __init__(self, name: str, image: str, services: List[str]) -> None:
self.name = name
self.image = image
self.services = services
class CoreServer(yaml.YAMLObject):
yaml_tag = "!CoreServer"
yaml_loader = yaml.SafeLoader
def __init__(self, name: str, address: str) -> None:
self.name = name
self.address = address
class Observer(yaml.YAMLObject):
yaml_tag = "!Observer"
yaml_loader = yaml.SafeLoader
def __init__(self, name: str, cmd: str) -> None:
self.name = name
self.cmd = cmd
class PreferencesConfig(yaml.YAMLObject):
yaml_tag = "!PreferencesConfig"
yaml_loader = yaml.SafeLoader
def __init__(
self,
editor: str = EDITORS[1],
terminal: str = None,
theme: str = themes.THEME_DARK,
gui3d: str = "/usr/local/bin/std3d.sh",
width: int = 1000,
height: int = 750,
) -> None:
self.theme = theme
self.editor = editor
self.terminal = terminal
self.gui3d = gui3d
self.width = width
self.height = height
class LocationConfig(yaml.YAMLObject):
yaml_tag = "!LocationConfig"
yaml_loader = yaml.SafeLoader
def __init__(
self,
x: float = 0.0,
y: float = 0.0,
z: float = 0.0,
lat: float = 47.5791667,
lon: float = -122.132322,
alt: float = 2.0,
scale: float = 150.0,
) -> None:
self.x = x
self.y = y
self.z = z
self.lat = lat
self.lon = lon
self.alt = alt
self.scale = scale
class IpConfigs(yaml.YAMLObject):
yaml_tag = "!IpConfigs"
yaml_loader = yaml.SafeLoader
def __init__(
self,
ip4: str = None,
ip6: str = None,
ip4s: List[str] = None,
ip6s: List[str] = None,
) -> None:
if ip4s is None:
ip4s = ["10.0.0.0", "192.168.0.0", "172.16.0.0"]
self.ip4s = ip4s
if ip6s is None:
ip6s = ["2001::", "2002::", "a::"]
self.ip6s = ip6s
if ip4 is None:
ip4 = self.ip4s[0]
self.ip4 = ip4
if ip6 is None:
ip6 = self.ip6s[0]
self.ip6 = ip6
class GuiConfig(yaml.YAMLObject):
yaml_tag = "!GuiConfig"
yaml_loader = yaml.SafeLoader
def __init__(
self,
preferences: PreferencesConfig = None,
location: LocationConfig = None,
servers: List[CoreServer] = None,
nodes: List[CustomNode] = None,
recentfiles: List[str] = None,
observers: List[Observer] = None,
scale: float = 1.0,
ips: IpConfigs = None,
mac: str = "00:00:00:aa:00:00",
) -> None:
if preferences is None:
preferences = PreferencesConfig()
self.preferences = preferences
if location is None:
location = LocationConfig()
self.location = location
if servers is None:
servers = []
self.servers = servers
if nodes is None:
nodes = []
self.nodes = nodes
if recentfiles is None:
recentfiles = []
self.recentfiles = recentfiles
if observers is None:
observers = []
self.observers = observers
self.scale = scale
if ips is None:
ips = IpConfigs()
self.ips = ips
self.mac = mac
def copy_files(current_path, new_path) -> None:
for current_file in current_path.glob("*"):
new_file = new_path.joinpath(current_file.name)
shutil.copy(current_file, new_file)
def find_terminal():
def find_terminal() -> Optional[str]:
for term in sorted(TERMINALS):
cmd = TERMINALS[term]
if shutil.which(term):
@ -58,7 +196,7 @@ def find_terminal():
return None
def check_directory():
def check_directory() -> None:
if HOME_PATH.exists():
return
HOME_PATH.mkdir()
@ -80,38 +218,16 @@ def check_directory():
editor = EDITORS[0]
else:
editor = EDITORS[1]
config = {
"preferences": {
"theme": themes.THEME_DARK,
"editor": editor,
"terminal": terminal,
"gui3d": "/usr/local/bin/std3d.sh",
"width": 1000,
"height": 750,
},
"location": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"lat": 47.5791667,
"lon": -122.132322,
"alt": 2.0,
"scale": 150.0,
},
"servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}],
"nodes": [],
"recentfiles": [],
"observers": [{"name": "hello", "cmd": "echo hello"}],
"scale": 1.0,
}
preferences = PreferencesConfig(editor, terminal)
config = GuiConfig(preferences=preferences)
save(config)
def read():
def read() -> GuiConfig:
with CONFIG_PATH.open("r") as f:
return yaml.load(f, Loader=yaml.SafeLoader)
def save(config):
def save(config: GuiConfig) -> None:
with CONFIG_PATH.open("w") as f:
yaml.dump(config, f, Dumper=IndentDumper, default_flow_style=False)

View file

@ -6,7 +6,7 @@ import logging
import os
from pathlib import Path
from tkinter import messagebox
from typing import TYPE_CHECKING, Dict, List
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional
import grpc
@ -16,9 +16,10 @@ from core.api.grpc.mobility_pb2 import MobilityConfig
from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig, ServiceFileConfig
from core.api.grpc.wlan_pb2 import WlanConfig
from core.gui import appconfig
from core.gui.dialogs.emaneinstall import EmaneInstallDialog
from core.gui.dialogs.error import ErrorDialog
from core.gui.dialogs.mobilityplayer import MobilityPlayer
from core.gui.dialogs.sessions import SessionsDialog
from core.gui.errors import show_grpc_error
from core.gui.graph import tags
from core.gui.graph.edges import CanvasEdge
from core.gui.graph.node import CanvasNode
@ -31,30 +32,6 @@ if TYPE_CHECKING:
from core.gui.app import Application
GUI_SOURCE = "gui"
OBSERVERS = {
"processes": "ps",
"ifconfig": "ifconfig",
"IPV4 Routes": "ip -4 ro",
"IPV6 Routes": "ip -6 ro",
"Listening sockets": "netstat -tuwnl",
"IPv4 MFC entries": "ip -4 mroute show",
"IPv6 MFC entries": "ip -6 mroute show",
"firewall rules": "iptables -L",
"IPSec policies": "setkey -DP",
}
class CoreServer:
def __init__(self, name: str, address: str, port: int):
self.name = name
self.address = address
self.port = port
class Observer:
def __init__(self, name: str, cmd: str):
self.name = name
self.cmd = cmd
class CoreClient:
@ -90,22 +67,13 @@ class CoreClient:
self.location = None
self.links = {}
self.hooks = {}
self.wlan_configs = {}
self.mobility_configs = {}
self.emane_model_configs = {}
self.emane_config = None
self.service_configs = {}
self.config_service_configs = {}
self.file_configs = {}
self.mobility_players = {}
self.handling_throughputs = None
self.handling_events = None
self.xml_dir = None
self.xml_file = None
self.modified_service_nodes = set()
@property
def client(self):
if self.session_id:
@ -130,40 +98,32 @@ class CoreClient:
self.canvas_nodes.clear()
self.links.clear()
self.hooks.clear()
self.wlan_configs.clear()
self.mobility_configs.clear()
self.emane_model_configs.clear()
self.emane_config = None
self.service_configs.clear()
self.file_configs.clear()
self.modified_service_nodes.clear()
for mobility_player in self.mobility_players.values():
mobility_player.handle_close()
self.close_mobility_players()
self.mobility_players.clear()
# clear streams
self.cancel_throughputs()
self.cancel_events()
def close_mobility_players(self):
for mobility_player in self.mobility_players.values():
mobility_player.close()
def set_observer(self, value: str):
self.observer = value
def read_config(self):
# read distributed server
for config in self.app.guiconfig.get("servers", []):
server = CoreServer(config["name"], config["address"], config["port"])
# read distributed servers
for server in self.app.guiconfig.servers:
self.servers[server.name] = server
# read custom nodes
for config in self.app.guiconfig.get("nodes", []):
name = config["name"]
image_file = config["image"]
services = set(config["services"])
node_draw = NodeDraw.from_custom(name, image_file, services)
self.custom_nodes[name] = node_draw
for custom_node in self.app.guiconfig.nodes:
node_draw = NodeDraw.from_custom(custom_node)
self.custom_nodes[custom_node.name] = node_draw
# read observers
for config in self.app.guiconfig.get("observers", []):
observer = Observer(config["name"], config["cmd"])
for observer in self.app.guiconfig.observers:
self.custom_observers[observer.name] = observer
def handle_events(self, event: core_pb2.Event):
@ -207,15 +167,25 @@ class CoreClient:
logging.debug("Link event: %s", event)
node_one_id = event.link.node_one_id
node_two_id = event.link.node_two_id
if node_one_id == node_two_id:
logging.warning("ignoring links with loops: %s", event)
return
canvas_node_one = self.canvas_nodes[node_one_id]
canvas_node_two = self.canvas_nodes[node_two_id]
if event.message_type == core_pb2.MessageType.ADD:
self.app.canvas.add_wireless_edge(canvas_node_one, canvas_node_two)
self.app.canvas.add_wireless_edge(
canvas_node_one, canvas_node_two, event.link
)
elif event.message_type == core_pb2.MessageType.DELETE:
self.app.canvas.delete_wireless_edge(canvas_node_one, canvas_node_two)
self.app.canvas.delete_wireless_edge(
canvas_node_one, canvas_node_two, event.link
)
elif event.message_type == core_pb2.MessageType.NONE:
self.app.canvas.update_wireless_edge(
canvas_node_one, canvas_node_two, event.link
)
else:
logging.warning("unknown link event: %s", event.message_type)
logging.warning("unknown link event: %s", event)
def handle_node_event(self, event: core_pb2.NodeEvent):
logging.debug("node event: %s", event)
@ -275,6 +245,12 @@ class CoreClient:
self.session_id, self.handle_events
)
# get session service defaults
response = self.client.get_service_defaults(self.session_id)
self.default_services = {
x.node_type: set(x.services) for x in response.defaults
}
# get location
if query_location:
response = self.client.get_session_location(self.session_id)
@ -289,65 +265,71 @@ class CoreClient:
for hook in response.hooks:
self.hooks[hook.file] = hook
# get mobility configs
response = self.client.get_mobility_configs(self.session_id)
for node_id in response.configs:
node_config = response.configs[node_id].config
self.mobility_configs[node_id] = node_config
# get emane config
response = self.client.get_emane_config(self.session_id)
self.emane_config = response.config
# update interface manager
self.interfaces_manager.joined(session.links)
# draw session
self.app.canvas.reset_and_redraw(session)
# get mobility configs
response = self.client.get_mobility_configs(self.session_id)
for node_id in response.configs:
config = response.configs[node_id].config
canvas_node = self.canvas_nodes[node_id]
canvas_node.mobility_config = dict(config)
# get emane model config
response = self.client.get_emane_model_configs(self.session_id)
for config in response.configs:
interface = None
if config.interface != -1:
interface = config.interface
self.set_emane_model_config(
config.node_id, config.model, config.config, interface
canvas_node = self.canvas_nodes[config.node_id]
canvas_node.emane_model_configs[(config.model, interface)] = dict(
config.config
)
# get wlan configurations
response = self.client.get_wlan_configs(self.session_id)
for _id in response.configs:
mapped_config = response.configs[_id]
self.wlan_configs[_id] = mapped_config.config
canvas_node = self.canvas_nodes[_id]
canvas_node.wlan_config = dict(mapped_config.config)
# get service configurations
response = self.client.get_node_service_configs(self.session_id)
for config in response.configs:
service_configs = self.service_configs.setdefault(config.node_id, {})
service_configs[config.service] = config.data
canvas_node = self.canvas_nodes[config.node_id]
canvas_node.service_configs[config.service] = config.data
logging.debug("service file configs: %s", config.files)
for file_name in config.files:
file_configs = self.file_configs.setdefault(config.node_id, {})
files = file_configs.setdefault(config.service, {})
data = config.files[file_name]
files = canvas_node.service_file_configs.setdefault(
config.service, {}
)
files[file_name] = data
# get config service configurations
response = self.client.get_node_config_service_configs(self.session_id)
for config in response.configs:
node_configs = self.config_service_configs.setdefault(
config.node_id, {}
canvas_node = self.canvas_nodes[config.node_id]
service_config = canvas_node.config_service_configs.setdefault(
config.name, {}
)
service_config = node_configs.setdefault(config.name, {})
if config.templates:
service_config["templates"] = config.templates
if config.config:
service_config["config"] = config.config
# draw session
self.app.canvas.reset_and_redraw(session)
# get metadata
response = self.client.get_session_metadata(self.session_id)
self.parse_metadata(response.config)
except grpc.RpcError as e:
self.app.after(0, show_grpc_error, e, self.app, self.app)
self.app.show_grpc_exception("Join Session Error", e)
# update ui to represent current state
self.app.after(0, self.app.joined_session_update)
@ -361,21 +343,16 @@ class CoreClient:
logging.debug("canvas metadata: %s", canvas_config)
if canvas_config:
canvas_config = json.loads(canvas_config)
gridlines = canvas_config.get("gridlines", True)
self.app.canvas.show_grid.set(gridlines)
fit_image = canvas_config.get("fit_image", False)
self.app.canvas.adjust_to_dim.set(fit_image)
wallpaper_style = canvas_config.get("wallpaper-style", 1)
self.app.canvas.scale_option.set(wallpaper_style)
width = self.app.guiconfig["preferences"]["width"]
height = self.app.guiconfig["preferences"]["height"]
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))
@ -423,32 +400,28 @@ class CoreClient:
try:
response = self.client.create_session()
logging.info("created session: %s", response)
location_config = self.app.guiconfig["location"]
location_config = self.app.guiconfig.location
self.location = core_pb2.SessionLocation(
x=location_config["x"],
y=location_config["y"],
z=location_config["z"],
lat=location_config["lat"],
lon=location_config["lon"],
alt=location_config["alt"],
scale=location_config["scale"],
x=location_config.x,
y=location_config.y,
z=location_config.z,
lat=location_config.lat,
lon=location_config.lon,
alt=location_config.alt,
scale=location_config.scale,
)
self.join_session(response.session_id, query_location=False)
except grpc.RpcError as e:
self.app.after(0, show_grpc_error, e, self.app, self.app)
self.app.show_grpc_exception("New Session Error", e)
def delete_session(self, session_id: int = None, parent_frame=None):
def delete_session(self, session_id: int = None):
if session_id is None:
session_id = self.session_id
try:
response = self.client.delete_session(session_id)
logging.info("deleted session(%s), Result: %s", session_id, response)
except grpc.RpcError as e:
# use the right master widget so the error dialog displays right on top of it
master = self.app
if parent_frame:
master = parent_frame
self.app.after(0, show_grpc_error, e, master, self.app)
self.app.show_grpc_exception("Delete Session Error", e)
def setup(self):
"""
@ -477,14 +450,12 @@ class CoreClient:
if len(sessions) == 0:
self.create_new_session()
else:
dialog = SessionsDialog(self.app, self.app, True)
dialog = SessionsDialog(self.app, True)
dialog.show()
response = self.client.get_service_defaults(self.session_id)
self.default_services = {
x.node_type: set(x.services) for x in response.defaults
}
except grpc.RpcError as e:
show_grpc_error(e, self.app, self.app)
logging.exception("core setup error")
dialog = ErrorDialog(self.app, "Setup Error", e.details())
dialog.show()
self.app.close()
def edit_node(self, core_node: core_pb2.Node):
@ -493,11 +464,20 @@ class CoreClient:
self.session_id, core_node.id, core_node.position, source=GUI_SOURCE
)
except grpc.RpcError as e:
self.app.after(0, show_grpc_error, e, self.app, self.app)
self.app.show_grpc_exception("Edit Node Error", e)
def start_session(self) -> core_pb2.StartSessionResponse:
self.interfaces_manager.reset_mac()
nodes = [x.core_node for x in self.canvas_nodes.values()]
links = [x.link for x in self.links.values()]
links = []
for edge in self.links.values():
link = core_pb2.Link()
link.CopyFrom(edge.link)
if link.HasField("interface_one") and not link.interface_one.mac:
link.interface_one.mac = self.interfaces_manager.next_mac()
if link.HasField("interface_two") and not link.interface_two.mac:
link.interface_two.mac = self.interfaces_manager.next_mac()
links.append(link)
wlan_configs = self.get_wlan_configs_proto()
mobility_configs = self.get_mobility_configs_proto()
emane_model_configs = self.get_emane_model_configs_proto()
@ -535,7 +515,7 @@ class CoreClient:
if response.result:
self.set_metadata()
except grpc.RpcError as e:
self.app.after(0, show_grpc_error, e, self.app, self.app)
self.app.show_grpc_exception("Start Session Error", e)
return response
def stop_session(self, session_id: int = None) -> core_pb2.StartSessionResponse:
@ -546,15 +526,20 @@ class CoreClient:
response = self.client.stop_session(session_id)
logging.info("stopped session(%s), result: %s", session_id, response)
except grpc.RpcError as e:
self.app.after(0, show_grpc_error, e, self.app, self.app)
self.app.show_grpc_exception("Stop Session Error", e)
return response
def show_mobility_players(self):
for node_id, config in self.mobility_configs.items():
canvas_node = self.canvas_nodes[node_id]
mobility_player = MobilityPlayer(self.app, self.app, canvas_node, config)
mobility_player.show()
self.mobility_players[node_id] = mobility_player
for canvas_node in self.canvas_nodes.values():
if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN:
continue
if canvas_node.mobility_config:
mobility_player = MobilityPlayer(
self.app, canvas_node, canvas_node.mobility_config
)
node_id = canvas_node.core_node.id
self.mobility_players[node_id] = mobility_player
mobility_player.show()
def set_metadata(self):
# create canvas data
@ -582,7 +567,7 @@ class CoreClient:
def launch_terminal(self, node_id: int):
try:
terminal = self.app.guiconfig["preferences"]["terminal"]
terminal = self.app.guiconfig.preferences.terminal
if not terminal:
messagebox.showerror(
"Terminal Error",
@ -595,7 +580,7 @@ class CoreClient:
logging.info("launching terminal %s", cmd)
os.system(cmd)
except grpc.RpcError as e:
self.app.after(0, show_grpc_error, e, self.app, self.app)
self.app.show_grpc_exception("Node Terminal Error", e)
def save_xml(self, file_path: str):
"""
@ -608,18 +593,18 @@ class CoreClient:
response = self.client.save_xml(self.session_id, file_path)
logging.info("saved xml file %s, result: %s", file_path, response)
except grpc.RpcError as e:
self.app.after(0, show_grpc_error, e, self.app, self.app)
self.app.show_grpc_exception("Save XML Error", e)
def open_xml(self, file_path: str):
"""
Open core xml
"""
try:
response = self.client.open_xml(file_path)
response = self._client.open_xml(file_path)
logging.info("open xml file %s, response: %s", file_path, response)
self.join_session(response.session_id)
except grpc.RpcError as e:
self.app.after(0, show_grpc_error, e, self.app, self.app)
self.app.show_grpc_exception("Open XML Error", e)
def get_node_service(self, node_id: int, service_name: str) -> NodeServiceData:
response = self.client.get_node_service(self.session_id, node_id, service_name)
@ -783,7 +768,7 @@ class CoreClient:
def create_node(
self, x: float, y: float, node_type: core_pb2.NodeType, model: str
) -> core_pb2.Node:
) -> Optional[core_pb2.Node]:
"""
Add node, with information filled in, to grpc manager
"""
@ -794,6 +779,10 @@ class CoreClient:
image = "ubuntu:latest"
emane = None
if node_type == core_pb2.NodeType.EMANE:
if not self.emane_models:
dialog = EmaneInstallDialog(self.app)
dialog.show()
return
emane = self.emane_models[0]
name = f"EMANE{node_id}"
elif node_type == core_pb2.NodeType.WIRELESS_LAN:
@ -814,6 +803,11 @@ class CoreClient:
if NodeUtils.is_custom(node_type, model):
services = NodeUtils.get_custom_node_services(self.app.guiconfig, model)
node.services[:] = services
# assign default services to CORE node
else:
services = self.default_services.get(model)
if services:
node.services[:] = services
logging.info(
"add node(%s) to session(%s), coordinates(%s, %s)",
node.name,
@ -823,39 +817,25 @@ class CoreClient:
)
return node
def delete_graph_nodes(self, canvas_nodes: List[core_pb2.Node]):
def deleted_graph_nodes(self, canvas_nodes: List[core_pb2.Node]):
"""
remove the nodes selected by the user and anything related to that node
such as link, configurations, interfaces
"""
edges = set()
for canvas_node in canvas_nodes:
node_id = canvas_node.core_node.id
if node_id not in self.canvas_nodes:
logging.error("unknown node: %s", node_id)
continue
del self.canvas_nodes[node_id]
self.modified_service_nodes.discard(node_id)
if node_id in self.mobility_configs:
del self.mobility_configs[node_id]
if node_id in self.wlan_configs:
del self.wlan_configs[node_id]
for key in list(self.emane_model_configs):
node_id, _, _ = key
if node_id == node_id:
del self.emane_model_configs[key]
for edge in canvas_node.edges:
if edge in edges:
continue
edges.add(edge)
self.links.pop(edge.token, None)
def deleted_graph_edges(self, edges: Iterable[CanvasEdge]) -> None:
links = []
for edge in edges:
del self.links[edge.token]
links.append(edge.link)
self.interfaces_manager.removed(links)
def create_interface(self, canvas_node: CanvasNode) -> core_pb2.Interface:
node = canvas_node.core_node
ip4, ip6 = self.interfaces_manager.get_ips(node.id)
ip4, ip6 = self.interfaces_manager.get_ips(node)
ip4_mask = self.interfaces_manager.ip4_mask
ip6_mask = self.interfaces_manager.ip6_mask
interface_id = len(canvas_node.interfaces)
@ -868,7 +848,6 @@ class CoreClient:
ip6=ip6,
ip6mask=ip6_mask,
)
canvas_node.interfaces.append(interface)
logging.debug(
"create node(%s) interface(%s) IPv4(%s) IPv6(%s)",
node.name,
@ -894,13 +873,11 @@ class CoreClient:
src_interface = None
if NodeUtils.is_container_node(src_node.type):
src_interface = self.create_interface(canvas_src_node)
edge.src_interface = src_interface
self.interface_to_edge[(src_node.id, src_interface.id)] = edge.token
dst_interface = None
if NodeUtils.is_container_node(dst_node.type):
dst_interface = self.create_interface(canvas_dst_node)
edge.dst_interface = dst_interface
self.interface_to_edge[(dst_node.id, dst_interface.id)] = edge.token
link = core_pb2.Link(
@ -910,43 +887,70 @@ class CoreClient:
interface_one=src_interface,
interface_two=dst_interface,
)
if src_interface:
edge.src_interface = link.interface_one
canvas_src_node.interfaces.append(link.interface_one)
if dst_interface:
edge.dst_interface = link.interface_two
canvas_dst_node.interfaces.append(link.interface_two)
edge.set_link(link)
self.links[edge.token] = edge
logging.info("Add link between %s and %s", src_node.name, dst_node.name)
def get_wlan_configs_proto(self) -> List[WlanConfig]:
configs = []
for node_id, config in self.wlan_configs.items():
for canvas_node in self.canvas_nodes.values():
if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN:
continue
if not canvas_node.wlan_config:
continue
config = canvas_node.wlan_config
config = {x: config[x].value for x in config}
node_id = canvas_node.core_node.id
wlan_config = WlanConfig(node_id=node_id, config=config)
configs.append(wlan_config)
return configs
def get_mobility_configs_proto(self) -> List[MobilityConfig]:
configs = []
for node_id, config in self.mobility_configs.items():
for canvas_node in self.canvas_nodes.values():
if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN:
continue
if not canvas_node.mobility_config:
continue
config = canvas_node.mobility_config
config = {x: config[x].value for x in config}
node_id = canvas_node.core_node.id
mobility_config = MobilityConfig(node_id=node_id, config=config)
configs.append(mobility_config)
return configs
def get_emane_model_configs_proto(self) -> List[EmaneModelConfig]:
configs = []
for key, config in self.emane_model_configs.items():
node_id, model, interface = key
config = {x: config[x].value for x in config}
if interface is None:
interface = -1
config_proto = EmaneModelConfig(
node_id=node_id, interface_id=interface, model=model, config=config
)
configs.append(config_proto)
for canvas_node in self.canvas_nodes.values():
if canvas_node.core_node.type != core_pb2.NodeType.EMANE:
continue
node_id = canvas_node.core_node.id
for key, config in canvas_node.emane_model_configs.items():
model, interface = key
config = {x: config[x].value for x in config}
if interface is None:
interface = -1
config_proto = EmaneModelConfig(
node_id=node_id, interface_id=interface, model=model, config=config
)
configs.append(config_proto)
return configs
def get_service_configs_proto(self) -> List[ServiceConfig]:
configs = []
for node_id, services in self.service_configs.items():
for name, config in services.items():
for canvas_node in self.canvas_nodes.values():
if not NodeUtils.is_container_node(canvas_node.core_node.type):
continue
if not canvas_node.service_configs:
continue
node_id = canvas_node.core_node.id
for name, config in canvas_node.service_configs.items():
config_proto = ServiceConfig(
node_id=node_id,
service=name,
@ -961,9 +965,14 @@ class CoreClient:
def get_service_file_configs_proto(self) -> List[ServiceFileConfig]:
configs = []
for (node_id, file_configs) in self.file_configs.items():
for service, file_config in file_configs.items():
for file, data in file_config.items():
for canvas_node in self.canvas_nodes.values():
if not NodeUtils.is_container_node(canvas_node.core_node.type):
continue
if not canvas_node.service_file_configs:
continue
node_id = canvas_node.core_node.id
for service, file_configs in canvas_node.service_file_configs.items():
for file, data in file_configs.items():
config_proto = ServiceFileConfig(
node_id=node_id, service=service, file=file, data=data
)
@ -974,8 +983,13 @@ class CoreClient:
self
) -> List[configservices_pb2.ConfigServiceConfig]:
config_service_protos = []
for node_id, node_config in self.config_service_configs.items():
for name, service_config in node_config.items():
for canvas_node in self.canvas_nodes.values():
if not NodeUtils.is_container_node(canvas_node.core_node.type):
continue
if not canvas_node.config_service_configs:
continue
node_id = canvas_node.core_node.id
for name, service_config in canvas_node.config_service_configs.items():
config = service_config.get("config", {})
config_proto = configservices_pb2.ConfigServiceConfig(
node_id=node_id,
@ -991,40 +1005,34 @@ class CoreClient:
return self.client.node_command(self.session_id, node_id, self.observer).output
def get_wlan_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]:
config = self.wlan_configs.get(node_id)
if not config:
response = self.client.get_wlan_config(self.session_id, node_id)
config = response.config
response = self.client.get_wlan_config(self.session_id, node_id)
config = response.config
logging.debug(
"get wlan configuration from node %s, result configuration: %s",
node_id,
config,
)
return config
return dict(config)
def get_mobility_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]:
config = self.mobility_configs.get(node_id)
if not config:
response = self.client.get_mobility_config(self.session_id, node_id)
config = response.config
response = self.client.get_mobility_config(self.session_id, node_id)
config = response.config
logging.debug(
"get mobility config from node %s, result configuration: %s",
node_id,
config,
)
return config
return dict(config)
def get_emane_model_config(
self, node_id: int, model: str, interface: int = None
) -> Dict[str, common_pb2.ConfigOption]:
config = self.emane_model_configs.get((node_id, model, interface))
if not config:
if interface is None:
interface = -1
response = self.client.get_emane_model_config(
self.session_id, node_id, model, interface
)
config = response.config
if interface is None:
interface = -1
response = self.client.get_emane_model_config(
self.session_id, node_id, model, interface
)
config = response.config
logging.debug(
"get emane model config: node id: %s, EMANE model: %s, interface: %s, config: %s",
node_id,
@ -1032,57 +1040,7 @@ class CoreClient:
interface,
config,
)
return config
def set_emane_model_config(
self,
node_id: int,
model: str,
config: Dict[str, common_pb2.ConfigOption],
interface: int = None,
):
logging.info(
"set emane model config: node id: %s, EMANE Model: %s, interface: %s, config: %s",
node_id,
model,
interface,
config,
)
self.emane_model_configs[(node_id, model, interface)] = config
def copy_node_service(self, _from: int, _to: int):
services = self.canvas_nodes[_from].core_node.services
self.canvas_nodes[_to].core_node.services[:] = services
logging.debug("copying node %s service to node %s", _from, _to)
def copy_node_config(self, _from: int, _to: int):
node_type = self.canvas_nodes[_from].core_node.type
if node_type == core_pb2.NodeType.DEFAULT:
services = self.canvas_nodes[_from].core_node.services
self.canvas_nodes[_to].core_node.services[:] = services
config = self.service_configs.get(_from)
if config:
self.service_configs[_to] = config
file_configs = self.file_configs.get(_from)
if file_configs:
for key, value in file_configs.items():
if _to not in self.file_configs:
self.file_configs[_to] = {}
self.file_configs[_to][key] = value
elif node_type == core_pb2.NodeType.WIRELESS_LAN:
config = self.wlan_configs.get(_from)
if config:
self.wlan_configs[_to] = config
config = self.mobility_configs.get(_from)
if config:
self.mobility_configs[_to] = config
elif node_type == core_pb2.NodeType.EMANE:
config = self.emane_model_configs.get(_from)
if config:
self.emane_model_configs[_to] = config
def service_been_modified(self, node_id: int) -> bool:
return node_id in self.modified_service_nodes
return dict(config)
def execute_script(self, script):
response = self.client.execute_script(script)

View file

@ -35,8 +35,8 @@ THE POSSIBILITY OF SUCH DAMAGE.\
class AboutDialog(Dialog):
def __init__(self, master: "Application", app: "Application"):
super().__init__(master, app, "About CORE", modal=True)
def __init__(self, app: "Application"):
super().__init__(app, "About CORE")
self.draw()
def draw(self):

View file

@ -15,9 +15,8 @@ if TYPE_CHECKING:
class AlertsDialog(Dialog):
def __init__(self, master: "Application", app: "Application"):
super().__init__(master, app, "Alerts", modal=True)
self.app = app
def __init__(self, app: "Application"):
super().__init__(app, "Alerts")
self.tree = None
self.codetext = None
self.alarm_map = {}
@ -93,16 +92,10 @@ class AlertsDialog(Dialog):
frame.grid(sticky="ew")
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(2, weight=1)
frame.columnconfigure(3, weight=1)
button = ttk.Button(frame, text="Reset", command=self.reset_alerts)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Daemon Log", command=self.daemon_log)
button.grid(row=0, column=1, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Node Log")
button.grid(row=0, column=2, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Close", command=self.destroy)
button.grid(row=0, column=3, sticky="ew")
button.grid(row=0, column=1, sticky="ew")
def reset_alerts(self):
self.codetext.text.delete("1.0", tk.END)
@ -110,10 +103,6 @@ class AlertsDialog(Dialog):
self.tree.delete(item)
self.app.statusbar.core_alarms.clear()
def daemon_log(self):
dialog = DaemonLog(self, self.app)
dialog.show()
def click_select(self, event: tk.Event):
current = self.tree.selection()[0]
alarm = self.alarm_map[current]
@ -121,33 +110,3 @@ class AlertsDialog(Dialog):
self.codetext.text.delete("1.0", "end")
self.codetext.text.insert("1.0", alarm.exception_event.text)
self.codetext.text.config(state=tk.DISABLED)
class DaemonLog(Dialog):
def __init__(self, master: tk.Widget, app: "Application"):
super().__init__(master, app, "core-daemon log", modal=True)
self.columnconfigure(0, weight=1)
self.path = tk.StringVar(value="/var/log/core-daemon.log")
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(1, weight=1)
frame = ttk.Frame(self.top)
frame.grid(row=0, column=0, sticky="ew", pady=PADY)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=9)
label = ttk.Label(frame, text="File", anchor="w")
label.grid(row=0, column=0, sticky="ew")
entry = ttk.Entry(frame, textvariable=self.path, state="disabled")
entry.grid(row=0, column=1, sticky="ew")
try:
file = open("/var/log/core-daemon.log", "r")
log = file.readlines()
except FileNotFoundError:
log = "Log file not found"
codetext = CodeText(self.top)
codetext.text.insert("1.0", log)
codetext.text.see("end")
codetext.text.config(state=tk.DISABLED)
codetext.grid(row=1, column=0, sticky="nsew")

View file

@ -5,6 +5,7 @@ import tkinter as tk
from tkinter import font, ttk
from typing import TYPE_CHECKING
from core.gui import validation
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY
@ -15,13 +16,12 @@ PIXEL_SCALE = 100
class SizeAndScaleDialog(Dialog):
def __init__(self, master: "Application", app: "Application"):
def __init__(self, app: "Application"):
"""
create an instance for size and scale object
"""
super().__init__(master, app, "Canvas Size and Scale", modal=True)
super().__init__(app, "Canvas Size and Scale")
self.canvas = self.app.canvas
self.validation = app.validation
self.section_font = font.Font(weight="bold")
width, height = self.canvas.current_dimensions
self.pixel_width = tk.IntVar(value=width)
@ -59,23 +59,11 @@ class SizeAndScaleDialog(Dialog):
frame.columnconfigure(3, weight=1)
label = ttk.Label(frame, text="Width")
label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.pixel_width,
validate="key",
validatecommand=(self.validation.positive_int, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_width)
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="x Height")
label.grid(row=0, column=2, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.pixel_height,
validate="key",
validatecommand=(self.validation.positive_int, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_height)
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="Pixels")
label.grid(row=0, column=4, sticky="w")
@ -87,23 +75,11 @@ class SizeAndScaleDialog(Dialog):
frame.columnconfigure(3, weight=1)
label = ttk.Label(frame, text="Width")
label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.meters_width,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry = validation.PositiveFloatEntry(frame, textvariable=self.meters_width)
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="x Height")
label.grid(row=0, column=2, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.meters_height,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry = validation.PositiveFloatEntry(frame, textvariable=self.meters_height)
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="Meters")
label.grid(row=0, column=4, sticky="w")
@ -118,13 +94,7 @@ class SizeAndScaleDialog(Dialog):
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text=f"{PIXEL_SCALE} Pixels =")
label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.scale,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry = validation.PositiveFloatEntry(frame, textvariable=self.scale)
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="Meters")
label.grid(row=0, column=2, sticky="w")
@ -148,24 +118,12 @@ class SizeAndScaleDialog(Dialog):
label = ttk.Label(frame, text="X")
label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.x,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry = validation.PositiveFloatEntry(frame, textvariable=self.x)
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="Y")
label.grid(row=0, column=2, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.y,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry = validation.PositiveFloatEntry(frame, textvariable=self.y)
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
label = ttk.Label(label_frame, text="Translates To")
@ -179,35 +137,17 @@ class SizeAndScaleDialog(Dialog):
label = ttk.Label(frame, text="Lat")
label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.lat,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry = validation.FloatEntry(frame, textvariable=self.lat)
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="Lon")
label.grid(row=0, column=2, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.lon,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry = validation.FloatEntry(frame, textvariable=self.lon)
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="Alt")
label.grid(row=0, column=4, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.alt,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry = validation.FloatEntry(frame, textvariable=self.alt)
entry.grid(row=0, column=5, sticky="ew")
def draw_save_as_default(self):
@ -241,16 +181,16 @@ class SizeAndScaleDialog(Dialog):
location.alt = self.alt.get()
location.scale = self.scale.get()
if self.save_default.get():
location_config = self.app.guiconfig["location"]
location_config["x"] = location.x
location_config["y"] = location.y
location_config["z"] = location.z
location_config["lat"] = location.lat
location_config["lon"] = location.lon
location_config["alt"] = location.alt
location_config["scale"] = location.scale
preferences = self.app.guiconfig["preferences"]
preferences["width"] = width
preferences["height"] = height
location_config = self.app.guiconfig.location
location_config.x = location.x
location_config.y = location.y
location_config.z = location.z
location_config.lat = location.lat
location_config.lon = location.lon
location_config.alt = location.alt
location_config.scale = location.scale
preferences = self.app.guiconfig.preferences
preferences.width = width
preferences.height = height
self.app.save_config()
self.destroy()

View file

@ -17,14 +17,13 @@ if TYPE_CHECKING:
class CanvasWallpaperDialog(Dialog):
def __init__(self, master: "Application", app: "Application"):
def __init__(self, app: "Application"):
"""
create an instance of CanvasWallpaper object
"""
super().__init__(master, app, "Canvas Background", modal=True)
super().__init__(app, "Canvas Background")
self.canvas = self.app.canvas
self.scale_option = tk.IntVar(value=self.canvas.scale_option.get())
self.show_grid = tk.BooleanVar(value=self.canvas.show_grid.get())
self.adjust_to_dim = tk.BooleanVar(value=self.canvas.adjust_to_dim.get())
self.filename = tk.StringVar(value=self.canvas.wallpaper_file)
self.image_label = None
@ -103,11 +102,6 @@ class CanvasWallpaperDialog(Dialog):
self.options.append(button)
def draw_additional_options(self):
checkbutton = ttk.Checkbutton(
self.top, text="Show grid", variable=self.show_grid
)
checkbutton.grid(sticky="ew", padx=PADX)
checkbutton = ttk.Checkbutton(
self.top,
text="Adjust canvas size to image dimensions",
@ -163,17 +157,13 @@ class CanvasWallpaperDialog(Dialog):
def click_apply(self):
self.canvas.scale_option.set(self.scale_option.get())
self.canvas.show_grid.set(self.show_grid.get())
self.canvas.adjust_to_dim.set(self.adjust_to_dim.get())
self.canvas.update_grid()
self.canvas.show_grid.click_handler()
filename = self.filename.get()
if not filename:
filename = None
try:
self.canvas.set_wallpaper(filename)
except FileNotFoundError:
logging.error("invalid background: %s", filename)
self.destroy()

View file

@ -3,8 +3,9 @@ custom color picker
"""
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING
from core.gui import validation
from core.gui.dialogs.dialog import Dialog
if TYPE_CHECKING:
@ -12,8 +13,10 @@ if TYPE_CHECKING:
class ColorPickerDialog(Dialog):
def __init__(self, master: Any, app: "Application", initcolor: str = "#000000"):
super().__init__(master, app, "color picker", modal=True)
def __init__(
self, master: tk.BaseWidget, app: "Application", initcolor: str = "#000000"
):
super().__init__(app, "color picker", master=master)
self.red_entry = None
self.blue_entry = None
self.green_entry = None
@ -48,13 +51,7 @@ class ColorPickerDialog(Dialog):
frame.columnconfigure(3, weight=2)
label = ttk.Label(frame, text="R: ")
label.grid(row=0, column=0)
self.red_entry = ttk.Entry(
frame,
width=4,
textvariable=self.red,
validate="key",
validatecommand=(self.app.validation.rgb, "%P"),
)
self.red_entry = validation.RgbEntry(frame, width=4, textvariable=self.red)
self.red_entry.grid(row=0, column=1, sticky="nsew")
scale = ttk.Scale(
frame,
@ -80,20 +77,13 @@ class ColorPickerDialog(Dialog):
frame.columnconfigure(3, weight=2)
label = ttk.Label(frame, text="G: ")
label.grid(row=0, column=0)
self.green_entry = ttk.Entry(
frame,
width=4,
textvariable=self.green,
validate="key",
validatecommand=(self.app.validation.rgb, "%P"),
)
self.green_entry = validation.RgbEntry(frame, width=4, textvariable=self.green)
self.green_entry.grid(row=0, column=1, sticky="nsew")
scale = ttk.Scale(
frame,
from_=0,
to=255,
value=0,
# length=200,
orient=tk.HORIZONTAL,
variable=self.green_scale,
command=lambda x: self.scale_callback(self.green_scale, self.green),
@ -112,13 +102,7 @@ class ColorPickerDialog(Dialog):
frame.columnconfigure(3, weight=2)
label = ttk.Label(frame, text="B: ")
label.grid(row=0, column=0)
self.blue_entry = ttk.Entry(
frame,
width=4,
textvariable=self.blue,
validate="key",
validatecommand=(self.app.validation.rgb, "%P"),
)
self.blue_entry = validation.RgbEntry(frame, width=4, textvariable=self.blue)
self.blue_entry.grid(row=0, column=1, sticky="nsew")
scale = ttk.Scale(
frame,
@ -142,12 +126,7 @@ class ColorPickerDialog(Dialog):
frame.columnconfigure(0, weight=1)
label = ttk.Label(frame, text="Selection: ")
label.grid(row=0, column=0, sticky="nsew")
self.hex_entry = ttk.Entry(
frame,
textvariable=self.hex,
validate="key",
validatecommand=(self.app.validation.hex, "%P"),
)
self.hex_entry = validation.HexEntry(frame, textvariable=self.hex)
self.hex_entry.grid(row=1, column=0, sticky="nsew")
self.display = tk.Frame(frame, background=self.color, width=100, height=100)
self.display.grid(row=2, column=0)

View file

@ -4,33 +4,35 @@ Service configuration dialog
import logging
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING, Any, List
from typing import TYPE_CHECKING, List
import grpc
from core.api.grpc.services_pb2 import ServiceValidationMode
from core.gui.dialogs.dialog import Dialog
from core.gui.errors import show_grpc_error
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.node import CanvasNode
class ConfigServiceConfigDialog(Dialog):
def __init__(
self, master: Any, app: "Application", service_name: str, node_id: int
self,
master: tk.BaseWidget,
app: "Application",
service_name: str,
canvas_node: "CanvasNode",
node_id: int,
):
title = f"{service_name} Config Service"
super().__init__(master, app, title, modal=True)
self.master = master
self.app = app
super().__init__(app, title, master=master)
self.core = app.core
self.canvas_node = canvas_node
self.node_id = node_id
self.service_name = service_name
self.service_configs = app.core.config_service_configs
self.radiovar = tk.IntVar()
self.radiovar.set(2)
self.directories = []
@ -95,9 +97,9 @@ class ConfigServiceConfigDialog(Dialog):
self.modes = sorted(x.name for x in response.modes)
self.mode_configs = {x.name: x.config for x in response.modes}
node_configs = self.service_configs.get(self.node_id, {})
service_config = node_configs.get(self.service_name, {})
service_config = self.canvas_node.config_service_configs.get(
self.service_name, {}
)
self.config = response.config
self.default_config = {x.name: x.value for x in self.config.values()}
custom_config = service_config.get("config")
@ -111,8 +113,8 @@ class ConfigServiceConfigDialog(Dialog):
self.modified_files.add(file)
self.temp_service_files[file] = data
except grpc.RpcError as e:
self.app.show_grpc_exception("Get Config Service Error", e)
self.has_error = True
show_grpc_error(e, self.app, self.app)
def draw(self):
self.top.columnconfigure(0, weight=1)
@ -313,27 +315,22 @@ class ConfigServiceConfigDialog(Dialog):
def click_apply(self):
current_listbox = self.master.current.listbox
if not self.is_custom():
if self.node_id in self.service_configs:
self.service_configs[self.node_id].pop(self.service_name, None)
self.canvas_node.config_service_configs.pop(self.service_name, None)
current_listbox.itemconfig(current_listbox.curselection()[0], bg="")
self.destroy()
return
try:
node_config = self.service_configs.setdefault(self.node_id, {})
service_config = node_config.setdefault(self.service_name, {})
if self.config_frame:
self.config_frame.parse_config()
service_config["config"] = {
x.name: x.value for x in self.config.values()
}
templates_config = service_config.setdefault("templates", {})
for file in self.modified_files:
templates_config[file] = self.temp_service_files[file]
all_current = current_listbox.get(0, tk.END)
current_listbox.itemconfig(all_current.index(self.service_name), bg="green")
except grpc.RpcError as e:
show_grpc_error(e, self.top, self.app)
service_config = self.canvas_node.config_service_configs.setdefault(
self.service_name, {}
)
if self.config_frame:
self.config_frame.parse_config()
service_config["config"] = {x.name: x.value for x in self.config.values()}
templates_config = service_config.setdefault("templates", {})
for file in self.modified_files:
templates_config[file] = self.temp_service_files[file]
all_current = current_listbox.get(0, tk.END)
current_listbox.itemconfig(all_current.index(self.service_name), bg="green")
self.destroy()
def handle_template_changed(self, event: tk.Event):
@ -365,9 +362,10 @@ class ConfigServiceConfigDialog(Dialog):
return has_custom_templates or has_custom_config
def click_defaults(self):
if self.node_id in self.service_configs:
node_config = self.service_configs.get(self.node_id, {})
node_config.pop(self.service_name, None)
self.canvas_node.config_service_configs.pop(self.service_name, None)
logging.info(
"cleared config service config: %s", self.canvas_node.config_service_configs
)
self.temp_service_files = dict(self.original_service_files)
filename = self.templates_combobox.get()
self.template_text.text.delete(1.0, "end")

View file

@ -4,7 +4,7 @@ copy service config dialog
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING, Any, Tuple
from typing import TYPE_CHECKING, Tuple
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX
@ -15,10 +15,9 @@ if TYPE_CHECKING:
class CopyServiceConfigDialog(Dialog):
def __init__(self, master: Any, app: "Application", node_id: int):
super().__init__(master, app, f"Copy services to node {node_id}", modal=True)
def __init__(self, master: tk.BaseWidget, app: "Application", node_id: int):
super().__init__(app, f"Copy services to node {node_id}", master=master)
self.parent = master
self.app = app
self.node_id = node_id
self.service_configs = app.core.service_configs
self.file_configs = app.core.file_configs
@ -171,13 +170,13 @@ class CopyServiceConfigDialog(Dialog):
class ViewConfigDialog(Dialog):
def __init__(
self,
master: Any,
master: tk.BaseWidget,
app: "Application",
node_id: int,
data: str,
filename: str = None,
):
super().__init__(master, app, f"n{node_id} config data", modal=True)
super().__init__(app, f"n{node_id} config data", master=master)
self.data = data
self.service_data = None
self.filepath = tk.StringVar(value=f"/tmp/services.tmp-n{node_id}-{filename}")

View file

@ -2,10 +2,10 @@ import logging
import tkinter as tk
from pathlib import Path
from tkinter import ttk
from typing import TYPE_CHECKING, Any, Set
from typing import TYPE_CHECKING, Set
from core.gui import nodeutils
from core.gui.appconfig import ICONS_PATH
from core.gui.appconfig import ICONS_PATH, CustomNode
from core.gui.dialogs.dialog import Dialog
from core.gui.images import Images
from core.gui.nodeutils import NodeDraw
@ -17,8 +17,10 @@ if TYPE_CHECKING:
class ServicesSelectDialog(Dialog):
def __init__(self, master: Any, app: "Application", current_services: Set[str]):
super().__init__(master, app, "Node Services", modal=True)
def __init__(
self, master: tk.BaseWidget, app: "Application", current_services: Set[str]
):
super().__init__(app, "Node Services", master=master)
self.groups = None
self.services = None
self.current = None
@ -100,8 +102,8 @@ class ServicesSelectDialog(Dialog):
class CustomNodesDialog(Dialog):
def __init__(self, master: "Application", app: "Application"):
super().__init__(master, app, "Custom Nodes", modal=True)
def __init__(self, app: "Application"):
super().__init__(app, "Custom Nodes")
self.edit_button = None
self.delete_button = None
self.nodes_list = None
@ -137,11 +139,11 @@ class CustomNodesDialog(Dialog):
frame.grid(row=0, column=2, sticky="nsew")
frame.columnconfigure(0, weight=1)
entry = ttk.Entry(frame, textvariable=self.name)
entry.grid(sticky="ew")
entry.grid(sticky="ew", pady=PADY)
self.image_button = ttk.Button(
frame, text="Icon", compound=tk.LEFT, command=self.click_icon
)
self.image_button.grid(sticky="ew")
self.image_button.grid(sticky="ew", pady=PADY)
button = ttk.Button(frame, text="Services", command=self.click_services)
button.grid(sticky="ew")
@ -199,17 +201,12 @@ class CustomNodesDialog(Dialog):
self.services.update(dialog.current_services)
def click_save(self):
self.app.guiconfig["nodes"].clear()
for name in sorted(self.app.core.custom_nodes):
self.app.guiconfig.nodes.clear()
for name in self.app.core.custom_nodes:
node_draw = self.app.core.custom_nodes[name]
self.app.guiconfig["nodes"].append(
{
"name": name,
"image": node_draw.image_file,
"services": list(node_draw.services),
}
)
logging.info("saving custom nodes: %s", self.app.guiconfig["nodes"])
custom_node = CustomNode(name, node_draw.image_file, node_draw.services)
self.app.guiconfig.nodes.append(custom_node)
logging.info("saving custom nodes: %s", self.app.guiconfig.nodes)
self.app.save_config()
self.destroy()
@ -217,7 +214,8 @@ class CustomNodesDialog(Dialog):
name = self.name.get()
if name not in self.app.core.custom_nodes:
image_file = Path(self.image_file).stem
node_draw = NodeDraw.from_custom(name, image_file, set(self.services))
custom_node = CustomNode(name, image_file, list(self.services))
node_draw = NodeDraw.from_custom(custom_node)
logging.info(
"created new custom node (%s), image file (%s), services: (%s)",
name,

View file

@ -11,8 +11,14 @@ if TYPE_CHECKING:
class Dialog(tk.Toplevel):
def __init__(
self, master: tk.Widget, app: "Application", title: str, modal: bool = False
self,
app: "Application",
title: str,
modal: bool = True,
master: tk.BaseWidget = None,
):
if master is None:
master = app
super().__init__(master)
self.withdraw()
self.app = app

View file

@ -4,13 +4,11 @@ emane configuration
import tkinter as tk
import webbrowser
from tkinter import ttk
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING
import grpc
from core.api.grpc import core_pb2
from core.gui.dialogs.dialog import Dialog
from core.gui.errors import show_grpc_error
from core.gui.images import ImageEnum, Images
from core.gui.themes import PADX, PADY
from core.gui.widgets import ConfigFrame
@ -21,8 +19,8 @@ if TYPE_CHECKING:
class GlobalEmaneDialog(Dialog):
def __init__(self, master: Any, app: "Application"):
super().__init__(master, app, "EMANE Configuration", modal=True)
def __init__(self, master: tk.BaseWidget, app: "Application"):
super().__init__(app, "EMANE Configuration", master=master)
self.config_frame = None
self.draw()
@ -54,25 +52,32 @@ class GlobalEmaneDialog(Dialog):
class EmaneModelDialog(Dialog):
def __init__(
self,
master: Any,
master: tk.BaseWidget,
app: "Application",
node: core_pb2.Node,
canvas_node: "CanvasNode",
model: str,
interface: int = None,
):
super().__init__(master, app, f"{node.name} {model} Configuration", modal=True)
self.node = node
super().__init__(
app, f"{canvas_node.core_node.name} {model} Configuration", master=master
)
self.canvas_node = canvas_node
self.node = canvas_node.core_node
self.model = f"emane_{model}"
self.interface = interface
self.config_frame = None
self.has_error = False
try:
self.config = self.app.core.get_emane_model_config(
self.node.id, self.model, self.interface
self.config = self.canvas_node.emane_model_configs.get(
(self.model, self.interface)
)
if not self.config:
self.config = self.app.core.get_emane_model_config(
self.node.id, self.model, self.interface
)
self.draw()
except grpc.RpcError as e:
show_grpc_error(e, self.app, self.app)
self.app.show_grpc_exception("Get EMANE Config Error", e)
self.has_error = True
self.destroy()
@ -98,20 +103,14 @@ class EmaneModelDialog(Dialog):
def click_apply(self):
self.config_frame.parse_config()
self.app.core.set_emane_model_config(
self.node.id, self.model, self.config, self.interface
)
key = (self.model, self.interface)
self.canvas_node.emane_model_configs[key] = self.config
self.destroy()
class EmaneConfigDialog(Dialog):
def __init__(
self, master: "Application", app: "Application", canvas_node: "CanvasNode"
):
super().__init__(
master, app, f"{canvas_node.core_node.name} EMANE Configuration", modal=True
)
self.app = app
def __init__(self, app: "Application", canvas_node: "CanvasNode"):
super().__init__(app, f"{canvas_node.core_node.name} EMANE Configuration")
self.canvas_node = canvas_node
self.node = canvas_node.core_node
self.radiovar = tk.IntVar()
@ -224,9 +223,7 @@ class EmaneConfigDialog(Dialog):
draw emane model configuration
"""
model_name = self.emane_model.get()
dialog = EmaneModelDialog(
self, self.app, self.canvas_node.core_node, model_name
)
dialog = EmaneModelDialog(self, self.app, self.canvas_node, model_name)
if not dialog.has_error:
dialog.show()

View file

@ -0,0 +1,25 @@
import webbrowser
from tkinter import ttk
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import PADY
class EmaneInstallDialog(Dialog):
def __init__(self, app) -> None:
super().__init__(app, "EMANE Error")
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
label = ttk.Label(self.top, text="EMANE needs to be installed!")
label.grid(sticky="ew", pady=PADY)
button = ttk.Button(
self.top, text="EMANE Documentation", command=self.click_doc
)
button.grid(sticky="ew", pady=PADY)
button = ttk.Button(self.top, text="Close", command=self.destroy)
button.grid(sticky="ew")
def click_doc(self):
webbrowser.open_new("https://coreemu.github.io/core/emane.html")

View file

@ -1,8 +1,6 @@
from tkinter import ttk
from typing import TYPE_CHECKING
import grpc
from core.gui.dialogs.dialog import Dialog
from core.gui.images import ImageEnum, Images
from core.gui.themes import FRAME_PAD, PADX, PADY
@ -13,8 +11,8 @@ if TYPE_CHECKING:
class ErrorDialog(Dialog):
def __init__(self, master, app: "Application", title: str, details: str) -> None:
super().__init__(master, app, "CORE Exception", modal=True)
def __init__(self, app: "Application", title: str, details: str) -> None:
super().__init__(app, "CORE Exception")
self.title = title
self.details = details
self.error_message = None
@ -41,18 +39,3 @@ class ErrorDialog(Dialog):
button = ttk.Button(self.top, text="Close", command=lambda: self.destroy())
button.grid(sticky="ew")
def show_grpc_error(e: grpc.RpcError, master, app: "Application"):
title = [x.capitalize() for x in e.code().name.lower().split("_")]
title = " ".join(title)
title = f"GRPC {title}"
dialog = ErrorDialog(master, app, title, e.details())
dialog.show()
def show_grpc_response_exceptions(class_name, exceptions, master, app: "Application"):
title = f"Exceptions from {class_name}"
detail = "\n".join([str(x) for x in exceptions])
dialog = ErrorDialog(master, app, title, detail)
dialog.show()

View file

@ -1,16 +1,19 @@
import logging
import tkinter as tk
from tkinter import filedialog, ttk
from typing import TYPE_CHECKING
from core.gui.appconfig import SCRIPT_PATH
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX
if TYPE_CHECKING:
from core.gui.app import Application
class ExecutePythonDialog(Dialog):
def __init__(self, master, app):
super().__init__(master, app, "Execute Python Script", modal=True)
self.app = app
def __init__(self, app: "Application"):
super().__init__(app, "Execute Python Script")
self.with_options = tk.IntVar(value=0)
self.options = tk.StringVar(value="")
self.option_entry = None

View file

@ -0,0 +1,157 @@
import logging
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING:
from core.gui.app import Application
class FindDialog(Dialog):
def __init__(self, app: "Application") -> None:
super().__init__(app, "Find", modal=False)
self.find_text = tk.StringVar(value="")
self.tree = None
self.draw()
self.protocol("WM_DELETE_WINDOW", self.close_dialog)
self.bind("<Return>", self.find_node)
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(1, weight=1)
# Find node frame
frame = ttk.Frame(self.top, padding=FRAME_PAD)
frame.grid(sticky="ew", pady=PADY)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Find:")
label.grid()
entry = ttk.Entry(frame, textvariable=self.find_text)
entry.grid(row=0, column=1, sticky="nsew")
# node list frame
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.rowconfigure(0, weight=1)
frame.grid(sticky="nsew", pady=PADY)
self.tree = ttk.Treeview(
frame,
columns=("nodeid", "name", "location", "detail"),
show="headings",
selectmode=tk.BROWSE,
)
self.tree.grid(sticky="nsew", pady=PADY)
style = ttk.Style()
heading_size = int(self.app.guiconfig.scale * 10)
style.configure("Treeview.Heading", font=(None, heading_size, "bold"))
self.tree.column("nodeid", stretch=tk.YES, anchor="center")
self.tree.heading("nodeid", text="Node ID")
self.tree.column("name", stretch=tk.YES, anchor="center")
self.tree.heading("name", text="Name")
self.tree.column("location", stretch=tk.YES, anchor="center")
self.tree.heading("location", text="Location")
self.tree.column("detail", stretch=tk.YES, anchor="center")
self.tree.heading("detail", text="Detail")
self.tree.bind("<<TreeviewSelect>>", self.click_select)
yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
yscrollbar.grid(row=0, column=1, sticky="ns")
self.tree.configure(yscrollcommand=yscrollbar.set)
xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
xscrollbar.grid(row=1, sticky="ew")
self.tree.configure(xscrollcommand=xscrollbar.set)
# button frame
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
button = ttk.Button(frame, text="Find", command=self.find_node)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.close_dialog)
button.grid(row=0, column=1, sticky="ew")
def clear_treeview_items(self) -> None:
"""
clear all items in the treeview
"""
for i in list(self.tree.get_children("")):
self.tree.delete(i)
def find_node(self, _event: tk.Event = None) -> None:
"""
Query nodes that have the same node name as our search key,
display results to tree view
"""
node_name = self.find_text.get().strip()
self.clear_treeview_items()
for node_id, node in sorted(
self.app.core.canvas_nodes.items(), key=lambda x: x[0]
):
name = node.core_node.name
if not node_name or node_name == name:
pos_x = round(node.core_node.position.x, 1)
pos_y = round(node.core_node.position.y, 1)
# TODO: I am not sure what to insert for Detail column
# leaving it blank for now
self.tree.insert(
"",
tk.END,
text=str(node_id),
values=(node_id, name, f"<{pos_x}, {pos_y}>", ""),
)
results = self.tree.get_children("")
if results:
self.tree.selection_set(results[0])
def close_dialog(self) -> None:
self.app.canvas.delete("find")
self.destroy()
def click_select(self, _event: tk.Event = None) -> None:
"""
find the node that matches search criteria, circle around that node
and scroll the x and y scrollbar to be able to see the node if
it is out of sight
"""
item = self.tree.selection()
if item:
self.app.canvas.delete("find")
node_id = int(self.tree.item(item, "text"))
canvas_node = self.app.core.canvas_nodes[node_id]
x0, y0, x1, y1 = self.app.canvas.bbox(canvas_node.id)
dist = 5 * self.app.guiconfig.scale
self.app.canvas.create_oval(
x0 - dist,
y0 - dist,
x1 + dist,
y1 + dist,
tags="find",
outline="red",
width=3.0 * self.app.guiconfig.scale,
)
_x, _y, _, _ = self.app.canvas.bbox(canvas_node.id)
oid = self.app.canvas.find_withtag("rectangle")
x0, y0, x1, y1 = self.app.canvas.bbox(oid[0])
logging.debug("Dist to most left: %s", abs(x0 - _x))
logging.debug("White canvas width: %s", abs(x0 - x1))
# calculate the node's location
# (as fractions of white canvas's width and height)
# and instantly scroll the x and y scrollbar to that location
xscroll_fraction = abs(x0 - _x) / abs(x0 - x1)
yscroll_fraction = abs(y0 - _y) / abs(y0 - y1)
# scroll a little more to the left or a little bit up so that the node
# doesn't always fall in the most top-left corner
for i in range(2):
if xscroll_fraction > 0.05:
xscroll_fraction = xscroll_fraction - 0.05
if yscroll_fraction > 0.05:
yscroll_fraction = yscroll_fraction - 0.05
self.app.canvas.xview_moveto(xscroll_fraction)
self.app.canvas.yview_moveto(yscroll_fraction)

View file

@ -1,6 +1,6 @@
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING
from core.api.grpc import core_pb2
from core.gui.dialogs.dialog import Dialog
@ -12,8 +12,8 @@ if TYPE_CHECKING:
class HookDialog(Dialog):
def __init__(self, master: Any, app: "Application"):
super().__init__(master, app, "Hook", modal=True)
def __init__(self, master: tk.BaseWidget, app: "Application"):
super().__init__(app, "Hook", master=master)
self.name = tk.StringVar()
self.codetext = None
self.hook = core_pb2.Hook()
@ -88,8 +88,8 @@ class HookDialog(Dialog):
class HooksDialog(Dialog):
def __init__(self, master: "Application", app: "Application"):
super().__init__(master, app, "Hooks", modal=True)
def __init__(self, app: "Application"):
super().__init__(app, "Hooks")
self.listbox = None
self.edit_button = None
self.delete_button = None

View file

@ -0,0 +1,151 @@
import tkinter as tk
from tkinter import messagebox, ttk
from typing import TYPE_CHECKING
import netaddr
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import ListboxScroll
if TYPE_CHECKING:
from core.gui.app import Application
class IpConfigDialog(Dialog):
def __init__(self, app: "Application") -> None:
super().__init__(app, "IP Configuration")
self.ip4 = self.app.guiconfig.ips.ip4
self.ip6 = self.app.guiconfig.ips.ip6
self.ip4s = self.app.guiconfig.ips.ip4s
self.ip6s = self.app.guiconfig.ips.ip6s
self.ip4_entry = None
self.ip4_listbox = None
self.ip6_entry = None
self.ip6_listbox = None
self.draw()
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
# draw ip4 and ip6 lists
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.rowconfigure(0, weight=1)
frame.grid(sticky="nsew", pady=PADY)
ip4_frame = ttk.LabelFrame(frame, text="IPv4", padding=FRAME_PAD)
ip4_frame.columnconfigure(0, weight=1)
ip4_frame.rowconfigure(0, weight=1)
ip4_frame.grid(row=0, column=0, stick="nsew")
self.ip4_listbox = ListboxScroll(ip4_frame)
self.ip4_listbox.listbox.bind("<<ListboxSelect>>", self.select_ip4)
self.ip4_listbox.grid(sticky="nsew", pady=PADY)
for index, ip4 in enumerate(self.ip4s):
self.ip4_listbox.listbox.insert(tk.END, ip4)
if self.ip4 == ip4:
self.ip4_listbox.listbox.select_set(index)
self.ip4_entry = ttk.Entry(ip4_frame)
self.ip4_entry.grid(sticky="ew", pady=PADY)
ip4_button_frame = ttk.Frame(ip4_frame)
ip4_button_frame.columnconfigure(0, weight=1)
ip4_button_frame.columnconfigure(1, weight=1)
ip4_button_frame.grid(sticky="ew")
ip4_add = ttk.Button(ip4_button_frame, text="Add", command=self.click_add_ip4)
ip4_add.grid(row=0, column=0, sticky="ew")
ip4_del = ttk.Button(
ip4_button_frame, text="Delete", command=self.click_del_ip4
)
ip4_del.grid(row=0, column=1, sticky="ew")
ip6_frame = ttk.LabelFrame(frame, text="IPv6", padding=FRAME_PAD)
ip6_frame.columnconfigure(0, weight=1)
ip6_frame.rowconfigure(0, weight=1)
ip6_frame.grid(row=0, column=1, stick="nsew")
self.ip6_listbox = ListboxScroll(ip6_frame)
self.ip6_listbox.listbox.bind("<<ListboxSelect>>", self.select_ip6)
self.ip6_listbox.grid(sticky="nsew", pady=PADY)
for index, ip6 in enumerate(self.ip6s):
self.ip6_listbox.listbox.insert(tk.END, ip6)
if self.ip6 == ip6:
self.ip6_listbox.listbox.select_set(index)
self.ip6_entry = ttk.Entry(ip6_frame)
self.ip6_entry.grid(sticky="ew", pady=PADY)
ip6_button_frame = ttk.Frame(ip6_frame)
ip6_button_frame.columnconfigure(0, weight=1)
ip6_button_frame.columnconfigure(1, weight=1)
ip6_button_frame.grid(sticky="ew")
ip6_add = ttk.Button(ip6_button_frame, text="Add", command=self.click_add_ip6)
ip6_add.grid(row=0, column=0, sticky="ew")
ip6_del = ttk.Button(
ip6_button_frame, text="Delete", command=self.click_del_ip6
)
ip6_del.grid(row=0, column=1, sticky="ew")
# draw buttons
frame = ttk.Frame(self.top)
frame.grid(stick="ew")
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=self.click_save)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def click_add_ip4(self) -> None:
ip4 = self.ip4_entry.get()
if not ip4 or not netaddr.valid_ipv4(ip4):
messagebox.showerror("IPv4 Error", f"Invalid IPv4 {ip4}")
else:
self.ip4_listbox.listbox.insert(tk.END, ip4)
def click_del_ip4(self) -> None:
if self.ip4_listbox.listbox.size() == 1:
messagebox.showerror("IPv4 Error", "Must have at least one address")
else:
selection = self.ip4_listbox.listbox.curselection()
self.ip4_listbox.listbox.delete(selection)
self.ip4_listbox.listbox.select_set(0)
def click_add_ip6(self) -> None:
ip6 = self.ip6_entry.get()
if not ip6 or not netaddr.valid_ipv6(ip6):
messagebox.showerror("IPv6 Error", f"Invalid IPv6 {ip6}")
else:
self.ip6_listbox.listbox.insert(tk.END, ip6)
def click_del_ip6(self) -> None:
if self.ip6_listbox.listbox.size() == 1:
messagebox.showerror("IPv6 Error", "Must have at least one address")
else:
selection = self.ip6_listbox.listbox.curselection()
self.ip6_listbox.listbox.delete(selection)
self.ip6_listbox.listbox.select_set(0)
def select_ip4(self, _event: tk.Event) -> None:
selection = self.ip4_listbox.listbox.curselection()
self.ip4 = self.ip4_listbox.listbox.get(selection)
def select_ip6(self, _event: tk.Event) -> None:
selection = self.ip6_listbox.listbox.curselection()
self.ip6 = self.ip6_listbox.listbox.get(selection)
def click_save(self) -> None:
ip4s = []
for index in range(self.ip4_listbox.listbox.size()):
ip4 = self.ip4_listbox.listbox.get(index)
ip4s.append(ip4)
ip6s = []
for index in range(self.ip6_listbox.listbox.size()):
ip6 = self.ip6_listbox.listbox.get(index)
ip6s.append(ip6)
ip_config = self.app.guiconfig.ips
ip_config.ip4 = self.ip4
ip_config.ip6 = self.ip6
ip_config.ip4s = ip4s
ip_config.ip6s = ip6s
self.app.core.interfaces_manager.update_ips(self.ip4, self.ip6)
self.app.save_config()
self.destroy()

View file

@ -6,13 +6,14 @@ from tkinter import ttk
from typing import TYPE_CHECKING, Union
from core.api.grpc import core_pb2
from core.gui import validation
from core.gui.dialogs.colorpicker import ColorPickerDialog
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import PADX, PADY
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.graph import CanvasGraph, CanvasEdge
from core.gui.graph.graph import CanvasEdge
def get_int(var: tk.StringVar) -> Union[int, None]:
@ -32,9 +33,8 @@ def get_float(var: tk.StringVar) -> Union[float, None]:
class LinkConfigurationDialog(Dialog):
def __init__(self, master: "CanvasGraph", app: "Application", edge: "CanvasEdge"):
super().__init__(master, app, "Link Configuration", modal=True)
self.app = app
def __init__(self, app: "Application", edge: "CanvasEdge"):
super().__init__(app, "Link Configuration")
self.edge = edge
self.is_symmetric = edge.link.options.unidirectional is False
if self.is_symmetric:
@ -121,95 +121,65 @@ class LinkConfigurationDialog(Dialog):
label = ttk.Label(frame, text="Bandwidth (bps)")
label.grid(row=row, column=0, sticky="ew")
entry = ttk.Entry(
frame,
textvariable=self.bandwidth,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
entry = validation.PositiveIntEntry(
frame, empty_enabled=False, textvariable=self.bandwidth
)
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
if not self.is_symmetric:
entry = ttk.Entry(
frame,
textvariable=self.down_bandwidth,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
entry = validation.PositiveIntEntry(
frame, empty_enabled=False, textvariable=self.down_bandwidth
)
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
row = row + 1
label = ttk.Label(frame, text="Delay (us)")
label.grid(row=row, column=0, sticky="ew")
entry = ttk.Entry(
frame,
textvariable=self.delay,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
entry = validation.PositiveIntEntry(
frame, empty_enabled=False, textvariable=self.delay
)
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
if not self.is_symmetric:
entry = ttk.Entry(
frame,
textvariable=self.down_delay,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
entry = validation.PositiveIntEntry(
frame, empty_enabled=False, textvariable=self.down_delay
)
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
row = row + 1
label = ttk.Label(frame, text="Jitter (us)")
label.grid(row=row, column=0, sticky="ew")
entry = ttk.Entry(
frame,
textvariable=self.jitter,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
entry = validation.PositiveIntEntry(
frame, empty_enabled=False, textvariable=self.jitter
)
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
if not self.is_symmetric:
entry = ttk.Entry(
frame,
textvariable=self.down_jitter,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
entry = validation.PositiveIntEntry(
frame, empty_enabled=False, textvariable=self.down_jitter
)
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
row = row + 1
label = ttk.Label(frame, text="Loss (%)")
label.grid(row=row, column=0, sticky="ew")
entry = ttk.Entry(
frame,
textvariable=self.loss,
validate="key",
validatecommand=(self.app.validation.positive_float, "%P"),
entry = validation.PositiveFloatEntry(
frame, empty_enabled=False, textvariable=self.loss
)
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
if not self.is_symmetric:
entry = ttk.Entry(
frame,
textvariable=self.down_loss,
validate="key",
validatecommand=(self.app.validation.positive_float, "%P"),
entry = validation.PositiveFloatEntry(
frame, empty_enabled=False, textvariable=self.down_loss
)
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
row = row + 1
label = ttk.Label(frame, text="Duplicate (%)")
label.grid(row=row, column=0, sticky="ew")
entry = ttk.Entry(
frame,
textvariable=self.duplicate,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
entry = validation.PositiveIntEntry(
frame, empty_enabled=False, textvariable=self.duplicate
)
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
if not self.is_symmetric:
entry = ttk.Entry(
frame,
textvariable=self.down_duplicate,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
entry = validation.PositiveIntEntry(
frame, empty_enabled=False, textvariable=self.down_duplicate
)
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
row = row + 1
@ -230,11 +200,8 @@ class LinkConfigurationDialog(Dialog):
label = ttk.Label(frame, text="Width")
label.grid(row=row, column=0, sticky="ew")
entry = ttk.Entry(
frame,
textvariable=self.width,
validate="key",
validatecommand=(self.app.validation.positive_float, "%P"),
entry = validation.PositiveFloatEntry(
frame, empty_enabled=False, textvariable=self.width
)
entry.grid(row=row, column=1, sticky="ew", pady=PADY)

View file

@ -0,0 +1,61 @@
import tkinter as tk
from tkinter import messagebox, ttk
from typing import TYPE_CHECKING
import netaddr
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import PADX, PADY
if TYPE_CHECKING:
from core.gui.app import Application
class MacConfigDialog(Dialog):
def __init__(self, app: "Application") -> None:
super().__init__(app, "MAC Configuration")
mac = self.app.guiconfig.mac
self.mac_var = tk.StringVar(value=mac)
self.draw()
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
# draw explanation label
text = (
"MAC addresses will be generated for nodes starting with the\n"
"provided value below and increment by value in order."
)
label = ttk.Label(self.top, text=text)
label.grid(sticky="ew", pady=PADY)
# draw input
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=3)
frame.grid(stick="ew", pady=PADY)
label = ttk.Label(frame, text="Starting MAC")
label.grid(row=0, column=0, sticky="ew", padx=PADX)
entry = ttk.Entry(frame, textvariable=self.mac_var)
entry.grid(row=0, column=1, sticky="ew")
# draw buttons
frame = ttk.Frame(self.top)
frame.grid(stick="ew")
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=self.click_save)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def click_save(self) -> None:
mac = self.mac_var.get()
if not netaddr.valid_mac(mac):
messagebox.showerror("MAC Error", f"{mac} is an invalid mac")
else:
self.app.core.interfaces_manager.mac = netaddr.EUI(mac)
self.app.guiconfig.mac = mac
self.app.save_config()
self.destroy()

View file

@ -17,11 +17,8 @@ MARKER_THICKNESS = [3, 5, 8, 10]
class MarkerDialog(Dialog):
def __init__(
self, master: "Application", app: "Application", initcolor: str = "#000000"
):
super().__init__(master, app, "Marker Tool", modal=False)
self.app = app
def __init__(self, app: "Application", initcolor: str = "#000000"):
super().__init__(app, "Marker Tool", modal=False)
self.color = initcolor
self.radius = MARKER_THICKNESS[0]
self.marker_thickness = tk.IntVar(value=MARKER_THICKNESS[0])

View file

@ -7,7 +7,6 @@ from typing import TYPE_CHECKING
import grpc
from core.gui.dialogs.dialog import Dialog
from core.gui.errors import show_grpc_error
from core.gui.themes import PADX, PADY
from core.gui.widgets import ConfigFrame
@ -17,25 +16,20 @@ if TYPE_CHECKING:
class MobilityConfigDialog(Dialog):
def __init__(
self, master: "Application", app: "Application", canvas_node: "CanvasNode"
):
super().__init__(
master,
app,
f"{canvas_node.core_node.name} Mobility Configuration",
modal=True,
)
def __init__(self, app: "Application", canvas_node: "CanvasNode"):
super().__init__(app, f"{canvas_node.core_node.name} Mobility Configuration")
self.canvas_node = canvas_node
self.node = canvas_node.core_node
self.config_frame = None
self.has_error = False
try:
self.config = self.app.core.get_mobility_config(self.node.id)
self.config = self.canvas_node.mobility_config
if not self.config:
self.config = self.app.core.get_mobility_config(self.node.id)
self.draw()
except grpc.RpcError as e:
self.app.show_grpc_exception("Get Mobility Config Error", e)
self.has_error = True
show_grpc_error(e, self.app, self.app)
self.destroy()
def draw(self):
@ -60,5 +54,5 @@ class MobilityConfigDialog(Dialog):
def click_apply(self):
self.config_frame.parse_config()
self.app.core.mobility_configs[self.node.id] = self.config
self.canvas_node.mobility_config = self.config
self.destroy()

View file

@ -1,12 +1,11 @@
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING
import grpc
from core.api.grpc.mobility_pb2 import MobilityAction
from core.gui.dialogs.dialog import Dialog
from core.gui.errors import show_grpc_error
from core.gui.images import ImageEnum, Images
from core.gui.themes import PADX, PADY
@ -18,14 +17,7 @@ ICON_SIZE = 16
class MobilityPlayer:
def __init__(
self,
master: "Application",
app: "Application",
canvas_node: "CanvasNode",
config,
):
self.master = master
def __init__(self, app: "Application", canvas_node: "CanvasNode", config):
self.app = app
self.canvas_node = canvas_node
self.config = config
@ -35,10 +27,8 @@ class MobilityPlayer:
def show(self):
if self.dialog:
self.dialog.destroy()
self.dialog = MobilityPlayerDialog(
self.master, self.app, self.canvas_node, self.config
)
self.dialog.protocol("WM_DELETE_WINDOW", self.handle_close)
self.dialog = MobilityPlayerDialog(self.app, self.canvas_node, self.config)
self.dialog.protocol("WM_DELETE_WINDOW", self.close)
if self.state == MobilityAction.START:
self.set_play()
elif self.state == MobilityAction.PAUSE:
@ -47,9 +37,10 @@ class MobilityPlayer:
self.set_stop()
self.dialog.show()
def handle_close(self):
self.dialog.destroy()
self.dialog = None
def close(self):
if self.dialog:
self.dialog.destroy()
self.dialog = None
def set_play(self):
self.state = MobilityAction.START
@ -68,11 +59,9 @@ class MobilityPlayer:
class MobilityPlayerDialog(Dialog):
def __init__(
self, master: Any, app: "Application", canvas_node: "CanvasNode", config
):
def __init__(self, app: "Application", canvas_node: "CanvasNode", config):
super().__init__(
master, app, f"{canvas_node.core_node.name} Mobility Player", modal=False
app, f"{canvas_node.core_node.name} Mobility Player", modal=False
)
self.resizable(False, False)
self.geometry("")
@ -153,7 +142,7 @@ class MobilityPlayerDialog(Dialog):
session_id, self.node.id, MobilityAction.START
)
except grpc.RpcError as e:
show_grpc_error(e, self.top, self.app)
self.app.show_grpc_exception("Mobility Error", e)
def click_pause(self):
self.set_pause()
@ -163,7 +152,7 @@ class MobilityPlayerDialog(Dialog):
session_id, self.node.id, MobilityAction.PAUSE
)
except grpc.RpcError as e:
show_grpc_error(e, self.top, self.app)
self.app.show_grpc_exception("Mobility Error", e)
def click_stop(self):
self.set_stop()
@ -173,4 +162,4 @@ class MobilityPlayerDialog(Dialog):
session_id, self.node.id, MobilityAction.STOP
)
except grpc.RpcError as e:
show_grpc_error(e, self.top, self.app)
self.app.show_grpc_exception("Mobility Error", e)

View file

@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
import netaddr
from core.gui import nodeutils
from core.gui import nodeutils, validation
from core.gui.appconfig import ICONS_PATH
from core.gui.dialogs.dialog import Dialog
from core.gui.dialogs.emaneconfig import EmaneModelDialog
@ -70,16 +70,12 @@ def check_ip4(parent, name: str, value: str) -> bool:
return True
def mac_auto(is_auto: tk.BooleanVar, entry: ttk.Entry):
logging.info("mac auto clicked")
def mac_auto(is_auto: tk.BooleanVar, entry: ttk.Entry, mac: tk.StringVar) -> None:
if is_auto.get():
logging.info("disabling mac")
entry.delete(0, tk.END)
entry.insert(tk.END, "")
mac.set("")
entry.config(state=tk.DISABLED)
else:
entry.delete(0, tk.END)
entry.insert(tk.END, "00:00:00:00:00:00")
mac.set("00:00:00:00:00:00")
entry.config(state=tk.NORMAL)
@ -98,15 +94,11 @@ class InterfaceData:
class NodeConfigDialog(Dialog):
def __init__(
self, master: "Application", app: "Application", canvas_node: "CanvasNode"
):
def __init__(self, app: "Application", canvas_node: "CanvasNode"):
"""
create an instance of node configuration
"""
super().__init__(
master, app, f"{canvas_node.core_node.name} Configuration", modal=True
)
super().__init__(app, f"{canvas_node.core_node.name} Configuration")
self.canvas_node = canvas_node
self.node = canvas_node.core_node
self.image = canvas_node.image
@ -126,6 +118,10 @@ class NodeConfigDialog(Dialog):
self.top.columnconfigure(0, weight=1)
row = 0
# field states
state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL
combo_state = tk.DISABLED if self.app.core.is_runtime() else "readonly"
# field frame
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
@ -147,15 +143,7 @@ class NodeConfigDialog(Dialog):
# name field
label = ttk.Label(frame, text="Name")
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
entry = ttk.Entry(
frame,
textvariable=self.name,
validate="key",
validatecommand=(self.app.validation.name, "%P"),
)
entry.bind(
"<FocusOut>", lambda event: self.app.validation.focus_out(event, "noname")
)
entry = validation.NodeNameEntry(frame, textvariable=self.name, state=state)
entry.grid(row=row, column=1, sticky="ew")
row += 1
@ -167,7 +155,7 @@ class NodeConfigDialog(Dialog):
frame,
textvariable=self.type,
values=list(NodeUtils.NODE_MODELS),
state="readonly",
state=combo_state,
)
combobox.grid(row=row, column=1, sticky="ew")
row += 1
@ -176,7 +164,7 @@ class NodeConfigDialog(Dialog):
if NodeUtils.is_image_node(self.node.type):
label = ttk.Label(frame, text="Image")
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
entry = ttk.Entry(frame, textvariable=self.container_image)
entry = ttk.Entry(frame, textvariable=self.container_image, state=state)
entry.grid(row=row, column=1, sticky="ew")
row += 1
@ -189,7 +177,7 @@ class NodeConfigDialog(Dialog):
servers = ["localhost"]
servers.extend(list(sorted(self.app.core.servers.keys())))
combobox = ttk.Combobox(
frame, textvariable=self.server, values=servers, state="readonly"
frame, textvariable=self.server, values=servers, state=combo_state
)
combobox.grid(row=row, column=1, sticky="ew")
row += 1
@ -198,6 +186,7 @@ class NodeConfigDialog(Dialog):
response = self.app.core.client.get_interfaces()
logging.debug("host machine available interfaces: %s", response)
interfaces = ListboxScroll(frame)
interfaces.listbox.config(state=state)
interfaces.grid(
row=row, column=0, columnspan=2, sticky="ew", padx=PADX, pady=PADY
)
@ -217,7 +206,7 @@ class NodeConfigDialog(Dialog):
notebook = ttk.Notebook(self.top)
notebook.grid(sticky="nsew", pady=PADY)
self.top.rowconfigure(notebook.grid_info()["row"], weight=1)
state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL
for interface in self.canvas_node.interfaces:
logging.info("interface: %s", interface)
tab = ttk.Frame(notebook, padding=FRAME_PAD)
@ -241,18 +230,17 @@ class NodeConfigDialog(Dialog):
label = ttk.Label(tab, text="MAC")
label.grid(row=row, column=0, padx=PADX, pady=PADY)
auto_set = not interface.mac
if auto_set:
state = tk.DISABLED
else:
state = tk.NORMAL
mac_state = tk.DISABLED if auto_set else tk.NORMAL
is_auto = tk.BooleanVar(value=auto_set)
checkbutton = ttk.Checkbutton(tab, text="Auto?", variable=is_auto)
checkbutton = ttk.Checkbutton(
tab, text="Auto?", variable=is_auto, state=state
)
checkbutton.var = is_auto
checkbutton.grid(row=row, column=1, padx=PADX)
mac = tk.StringVar(value=interface.mac)
entry = ttk.Entry(tab, textvariable=mac, state=state)
entry = ttk.Entry(tab, textvariable=mac, state=mac_state)
entry.grid(row=row, column=2, sticky="ew")
func = partial(mac_auto, is_auto, entry)
func = partial(mac_auto, is_auto, entry, mac)
checkbutton.config(command=func)
row += 1
@ -262,7 +250,7 @@ class NodeConfigDialog(Dialog):
if interface.ip4:
ip4_net = f"{interface.ip4}/{interface.ip4mask}"
ip4 = tk.StringVar(value=ip4_net)
entry = ttk.Entry(tab, textvariable=ip4)
entry = ttk.Entry(tab, textvariable=ip4, state=state)
entry.grid(row=row, column=1, columnspan=2, sticky="ew")
row += 1
@ -272,7 +260,7 @@ class NodeConfigDialog(Dialog):
if interface.ip6:
ip6_net = f"{interface.ip6}/{interface.ip6mask}"
ip6 = tk.StringVar(value=ip6_net)
entry = ttk.Entry(tab, textvariable=ip6)
entry = ttk.Entry(tab, textvariable=ip6, state=state)
entry.grid(row=row, column=1, columnspan=2, sticky="ew")
self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6)
@ -283,14 +271,16 @@ class NodeConfigDialog(Dialog):
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
button = ttk.Button(frame, text="Apply", command=self.config_apply)
button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, padx=PADX, sticky="ew")
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def click_emane_config(self, emane_model: str, interface_id: int):
dialog = EmaneModelDialog(self, self.app, self.node, emane_model, interface_id)
dialog = EmaneModelDialog(
self, self.app, self.canvas_node, emane_model, interface_id
)
dialog.show()
def click_icon(self):
@ -300,7 +290,7 @@ class NodeConfigDialog(Dialog):
self.image_button.config(image=self.image)
self.image_file = file_path
def config_apply(self):
def click_apply(self):
error = False
# update core node
@ -326,7 +316,6 @@ class NodeConfigDialog(Dialog):
ip4_net = data.ip4.get()
if not check_ip4(self, interface.name, ip4_net):
error = True
data.ip4.set(f"{interface.ip4}/{interface.ip4mask}")
break
if ip4_net:
ip4, ip4mask = ip4_net.split("/")
@ -340,7 +329,6 @@ class NodeConfigDialog(Dialog):
ip6_net = data.ip6.get()
if not check_ip6(self, interface.name, ip6_net):
error = True
data.ip6.set(f"{interface.ip6}/{interface.ip6mask}")
break
if ip6_net:
ip6, ip6mask = ip6_net.split("/")
@ -351,15 +339,14 @@ class NodeConfigDialog(Dialog):
interface.ip6mask = ip6mask
mac = data.mac.get()
if mac and not netaddr.valid_mac(mac):
auto_mac = data.is_auto.get()
if not auto_mac and not netaddr.valid_mac(mac):
title = f"MAC Error for {interface.name}"
messagebox.showerror(title, "Invalid MAC Address")
error = True
data.mac.set(interface.mac)
break
else:
mac = netaddr.EUI(mac)
mac.dialect = netaddr.mac_unix_expanded
elif not auto_mac:
mac = netaddr.EUI(mac, dialect=netaddr.mac_unix_expanded)
interface.mac = str(mac)
# redraw

View file

@ -4,7 +4,7 @@ core node services
import logging
import tkinter as tk
from tkinter import messagebox, ttk
from typing import TYPE_CHECKING, Any, Set
from typing import TYPE_CHECKING, Set
from core.gui.dialogs.configserviceconfig import ConfigServiceConfigDialog
from core.gui.dialogs.dialog import Dialog
@ -18,15 +18,10 @@ if TYPE_CHECKING:
class NodeConfigServiceDialog(Dialog):
def __init__(
self,
master: Any,
app: "Application",
canvas_node: "CanvasNode",
services: Set[str] = None,
self, app: "Application", canvas_node: "CanvasNode", services: Set[str] = None
):
title = f"{canvas_node.core_node.name} Config Services"
super().__init__(master, app, title, modal=True)
self.app = app
super().__init__(app, title)
self.canvas_node = canvas_node
self.node_id = canvas_node.core_node.id
self.groups = None
@ -70,12 +65,10 @@ class NodeConfigServiceDialog(Dialog):
label_frame.grid(row=0, column=2, sticky="nsew")
label_frame.rowconfigure(0, weight=1)
label_frame.columnconfigure(0, weight=1)
self.current = ListboxScroll(label_frame)
self.current.grid(sticky="nsew")
for service in sorted(self.current_services):
self.current.listbox.insert(tk.END, service)
if self.is_custom_service(service):
self.current.listbox.itemconfig(tk.END, bg="green")
self.draw_current_services()
frame = ttk.Frame(self.top)
frame.grid(stick="ew")
@ -108,24 +101,22 @@ class NodeConfigServiceDialog(Dialog):
self.current_services.add(name)
elif not var.get() and name in self.current_services:
self.current_services.remove(name)
self.current.listbox.delete(0, tk.END)
for name in sorted(self.current_services):
self.current.listbox.insert(tk.END, name)
if self.is_custom_service(name):
self.current.listbox.itemconfig(tk.END, bg="green")
self.draw_current_services()
self.canvas_node.core_node.config_services[:] = self.current_services
def click_configure(self):
current_selection = self.current.listbox.curselection()
if len(current_selection):
dialog = ConfigServiceConfigDialog(
master=self,
app=self.app,
service_name=self.current.listbox.get(current_selection[0]),
node_id=self.node_id,
self,
self.app,
self.current.listbox.get(current_selection[0]),
self.canvas_node,
self.node_id,
)
if not dialog.has_error:
dialog.show()
self.draw_current_services()
else:
messagebox.showinfo(
"Config Service Configuration",
@ -133,6 +124,13 @@ class NodeConfigServiceDialog(Dialog):
parent=self,
)
def draw_current_services(self):
self.current.listbox.delete(0, tk.END)
for name in sorted(self.current_services):
self.current.listbox.insert(tk.END, name)
if self.is_custom_service(name):
self.current.listbox.itemconfig(tk.END, bg="green")
def click_save(self):
self.canvas_node.core_node.config_services[:] = self.current_services
logging.info(
@ -156,9 +154,4 @@ class NodeConfigServiceDialog(Dialog):
return
def is_custom_service(self, service: str) -> bool:
node_configs = self.app.core.config_service_configs.get(self.node_id, {})
service_config = node_configs.get(service)
if node_configs and service_config:
return True
else:
return False
return service in self.canvas_node.config_service_configs

View file

@ -3,11 +3,10 @@ core node services
"""
import tkinter as tk
from tkinter import messagebox, ttk
from typing import TYPE_CHECKING, Any, Set
from typing import TYPE_CHECKING
from core.gui.dialogs.dialog import Dialog
from core.gui.dialogs.serviceconfig import ServiceConfigDialog
from core.gui.nodeutils import NodeUtils
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CheckboxList, ListboxScroll
@ -17,39 +16,15 @@ if TYPE_CHECKING:
class NodeServiceDialog(Dialog):
def __init__(
self,
master: Any,
app: "Application",
canvas_node: "CanvasNode",
services: Set[str] = None,
):
def __init__(self, app: "Application", canvas_node: "CanvasNode"):
title = f"{canvas_node.core_node.name} Services"
super().__init__(master, app, title, modal=True)
self.app = app
super().__init__(app, title)
self.canvas_node = canvas_node
self.node_id = canvas_node.core_node.id
self.groups = None
self.services = None
self.current = None
if services is None:
services = canvas_node.core_node.services
model = canvas_node.core_node.model
if len(services) == 0:
# not custom node type and node's services haven't been modified before
if not NodeUtils.is_custom(
canvas_node.core_node.type, canvas_node.core_node.model
) and not self.app.core.service_been_modified(self.node_id):
services = set(self.app.core.default_services[model])
# services of default type nodes were modified to be empty
elif canvas_node.core_node.id in self.app.core.modified_service_nodes:
services = set()
else:
services = set(
NodeUtils.get_custom_node_services(self.app.guiconfig, model)
)
else:
services = set(services)
services = set(canvas_node.core_node.services)
self.current_services = services
self.draw()
@ -103,7 +78,7 @@ class NodeServiceDialog(Dialog):
button.grid(row=0, column=1, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Remove", command=self.click_remove)
button.grid(row=0, column=2, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.click_cancel)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=3, sticky="ew")
# trigger group change
@ -135,10 +110,11 @@ class NodeServiceDialog(Dialog):
current_selection = self.current.listbox.curselection()
if len(current_selection):
dialog = ServiceConfigDialog(
master=self,
app=self.app,
service_name=self.current.listbox.get(current_selection[0]),
node_id=self.node_id,
self,
self.app,
self.current.listbox.get(current_selection[0]),
self.canvas_node,
self.node_id,
)
# if error occurred when creating ServiceConfigDialog, don't show the dialog
@ -152,22 +128,8 @@ class NodeServiceDialog(Dialog):
)
def click_save(self):
# if node is custom type or current services are not the default services then
# set core node services and add node to modified services node set
if (
self.canvas_node.core_node.model not in self.app.core.default_services
or self.current_services
!= self.app.core.default_services[self.canvas_node.core_node.model]
):
self.canvas_node.core_node.services[:] = self.current_services
self.app.core.modified_service_nodes.add(self.canvas_node.core_node.id)
else:
if len(self.canvas_node.core_node.services) > 0:
self.canvas_node.core_node.services[:] = []
self.destroy()
def click_cancel(self):
self.current_services = None
core_node = self.canvas_node.core_node
core_node.services[:] = self.current_services
self.destroy()
def click_remove(self):
@ -182,14 +144,6 @@ class NodeServiceDialog(Dialog):
return
def is_custom_service(self, service: str) -> bool:
service_configs = self.app.core.service_configs
file_configs = self.app.core.file_configs
if self.node_id in service_configs and service in service_configs[self.node_id]:
return True
if (
self.node_id in file_configs
and service in file_configs[self.node_id]
and file_configs[self.node_id][service]
):
return True
return False
has_service_config = service in self.canvas_node.service_configs
has_file_config = service in self.canvas_node.service_file_configs
return has_service_config or has_file_config

View file

@ -1,8 +1,8 @@
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox, ttk
from typing import TYPE_CHECKING
from core.gui.coreclient import Observer
from core.gui.appconfig import Observer
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import PADX, PADY
from core.gui.widgets import ListboxScroll
@ -12,8 +12,8 @@ if TYPE_CHECKING:
class ObserverDialog(Dialog):
def __init__(self, master: "Application", app: "Application"):
super().__init__(master, app, "Observer Widgets", modal=True)
def __init__(self, app: "Application"):
super().__init__(app, "Observer Widgets")
self.observers = None
self.save_button = None
self.delete_button = None
@ -89,11 +89,9 @@ class ObserverDialog(Dialog):
button.grid(row=0, column=1, sticky="ew")
def click_save_config(self):
observers = []
for name in sorted(self.app.core.custom_observers):
observer = self.app.core.custom_observers[name]
observers.append({"name": observer.name, "cmd": observer.cmd})
self.app.guiconfig["observers"] = observers
self.app.guiconfig.observers.clear()
for observer in self.app.core.custom_observers.values():
self.app.guiconfig.observers.append(observer)
self.app.save_config()
self.destroy()
@ -104,6 +102,11 @@ class ObserverDialog(Dialog):
observer = Observer(name, cmd)
self.app.core.custom_observers[name] = observer
self.observers.insert(tk.END, name)
self.name.set("")
self.cmd.set("")
self.app.menubar.draw_custom_observers()
else:
messagebox.showerror("Observer Error", f"{name} already exists")
def click_save(self):
name = self.name.get()
@ -129,6 +132,7 @@ class ObserverDialog(Dialog):
self.observers.selection_clear(0, tk.END)
self.save_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED)
self.app.menubar.draw_custom_observers()
def handle_observer_change(self, event: tk.Event):
selection = self.observers.curselection()

View file

@ -4,7 +4,7 @@ import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING
from core.gui import appconfig
from core.gui import appconfig, validation
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY, scale_fonts
from core.gui.validation import LARGEST_SCALE, SMALLEST_SCALE
@ -16,14 +16,14 @@ SCALE_INTERVAL = 0.01
class PreferencesDialog(Dialog):
def __init__(self, master: "Application", app: "Application"):
super().__init__(master, app, "Preferences", modal=True)
def __init__(self, app: "Application"):
super().__init__(app, "Preferences")
self.gui_scale = tk.DoubleVar(value=self.app.app_scale)
preferences = self.app.guiconfig["preferences"]
self.editor = tk.StringVar(value=preferences["editor"])
self.theme = tk.StringVar(value=preferences["theme"])
self.terminal = tk.StringVar(value=preferences["terminal"])
self.gui3d = tk.StringVar(value=preferences["gui3d"])
preferences = self.app.guiconfig.preferences
self.editor = tk.StringVar(value=preferences.editor)
self.theme = tk.StringVar(value=preferences.theme)
self.terminal = tk.StringVar(value=preferences.terminal)
self.gui3d = tk.StringVar(value=preferences.gui3d)
self.draw()
def draw(self):
@ -80,12 +80,8 @@ class PreferencesDialog(Dialog):
variable=self.gui_scale,
)
scale.grid(row=0, column=0, sticky="ew")
entry = ttk.Entry(
scale_frame,
textvariable=self.gui_scale,
width=4,
validate="key",
validatecommand=(self.app.validation.app_scale, "%P"),
entry = validation.AppScaleEntry(
scale_frame, textvariable=self.gui_scale, width=4
)
entry.grid(row=0, column=1)
@ -110,15 +106,14 @@ class PreferencesDialog(Dialog):
self.app.style.theme_use(theme)
def click_save(self):
preferences = self.app.guiconfig["preferences"]
preferences["terminal"] = self.terminal.get()
preferences["editor"] = self.editor.get()
preferences["gui3d"] = self.gui3d.get()
preferences["theme"] = self.theme.get()
preferences = self.app.guiconfig.preferences
preferences.terminal = self.terminal.get()
preferences.editor = self.editor.get()
preferences.gui3d = self.gui3d.get()
preferences.theme = self.theme.get()
self.gui_scale.set(round(self.gui_scale.get(), 2))
app_scale = self.gui_scale.get()
self.app.guiconfig["scale"] = app_scale
self.app.guiconfig.scale = app_scale
self.app.save_config()
self.scale_adjust()
self.destroy()

View file

@ -0,0 +1,115 @@
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING
from core.gui.dialogs.dialog import Dialog
from core.gui.nodeutils import NodeUtils
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CodeText, ListboxScroll
if TYPE_CHECKING:
from core.gui.app import Application
class RunToolDialog(Dialog):
def __init__(self, app: "Application") -> None:
super().__init__(app, "Run Tool")
self.cmd = tk.StringVar(value="ps ax")
self.result = None
self.node_list = None
self.executable_nodes = {}
self.store_nodes()
self.draw()
def store_nodes(self) -> None:
"""
store all CORE nodes (nodes that execute commands) from all existing nodes
"""
for nid, node in self.app.core.canvas_nodes.items():
if NodeUtils.is_container_node(node.core_node.type):
self.executable_nodes[node.core_node.name] = nid
def draw(self) -> None:
self.top.rowconfigure(0, weight=1)
self.top.columnconfigure(0, weight=1)
self.draw_command_frame()
self.draw_nodes_frame()
def draw_command_frame(self) -> None:
# the main frame
frame = ttk.Frame(self.top)
frame.grid(row=0, column=0, sticky="nsew", padx=PADX)
frame.columnconfigure(0, weight=1)
frame.rowconfigure(1, weight=1)
labeled_frame = ttk.LabelFrame(frame, text="Command", padding=FRAME_PAD)
labeled_frame.grid(sticky="ew", pady=PADY)
labeled_frame.rowconfigure(0, weight=1)
labeled_frame.columnconfigure(0, weight=1)
entry = ttk.Entry(labeled_frame, textvariable=self.cmd)
entry.grid(sticky="ew")
# results frame
labeled_frame = ttk.LabelFrame(frame, text="Output", padding=FRAME_PAD)
labeled_frame.grid(sticky="nsew", pady=PADY)
labeled_frame.columnconfigure(0, weight=1)
labeled_frame.rowconfigure(0, weight=1)
self.result = CodeText(labeled_frame)
self.result.text.config(state=tk.DISABLED, height=15)
self.result.grid(sticky="nsew", pady=PADY)
button_frame = ttk.Frame(labeled_frame)
button_frame.grid(sticky="nsew")
button_frame.columnconfigure(0, weight=1)
button_frame.columnconfigure(1, weight=1)
button = ttk.Button(button_frame, text="Run", command=self.click_run)
button.grid(sticky="ew", padx=PADX)
button = ttk.Button(button_frame, text="Close", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def draw_nodes_frame(self) -> None:
labeled_frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD)
labeled_frame.grid(row=0, column=1, sticky="nsew")
labeled_frame.columnconfigure(0, weight=1)
labeled_frame.rowconfigure(0, weight=1)
self.node_list = ListboxScroll(labeled_frame)
self.node_list.listbox.config(selectmode=tk.MULTIPLE)
self.node_list.grid(sticky="nsew", pady=PADY)
for n in sorted(self.executable_nodes.keys()):
self.node_list.listbox.insert(tk.END, n)
button_frame = ttk.Frame(labeled_frame, padding=FRAME_PAD)
button_frame.grid(sticky="nsew")
button_frame.columnconfigure(0, weight=1)
button_frame.columnconfigure(1, weight=1)
button = ttk.Button(button_frame, text="All", command=self.click_all)
button.grid(sticky="nsew", padx=PADX)
button = ttk.Button(button_frame, text="None", command=self.click_none)
button.grid(row=0, column=1, sticky="nsew")
def click_all(self) -> None:
self.node_list.listbox.selection_set(0, self.node_list.listbox.size() - 1)
def click_none(self) -> None:
self.node_list.listbox.selection_clear(0, self.node_list.listbox.size() - 1)
def click_run(self) -> None:
"""
Run the command on each of the selected nodes and display the output to result
text box.
"""
command = self.cmd.get().strip()
self.result.text.config(state=tk.NORMAL)
self.result.text.delete("1.0", tk.END)
for selection in self.node_list.listbox.curselection():
node_name = self.node_list.listbox.get(selection)
node_id = self.executable_nodes[node_name]
response = self.app.core.client.node_command(
self.app.core.session_id, node_id, command
)
self.result.text.insert(
tk.END, f"> {node_name} > {command}:\n{response.output}\n"
)
self.result.text.config(state=tk.DISABLED)

View file

@ -2,7 +2,7 @@ import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING
from core.gui.coreclient import CoreServer
from core.gui.appconfig import CoreServer
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import ListboxScroll
@ -16,11 +16,10 @@ DEFAULT_PORT = 50051
class ServersDialog(Dialog):
def __init__(self, master: "Application", app: "Application"):
super().__init__(master, app, "CORE Servers", modal=True)
def __init__(self, app: "Application"):
super().__init__(app, "CORE Servers")
self.name = tk.StringVar(value=DEFAULT_NAME)
self.address = tk.StringVar(value=DEFAULT_ADDRESS)
self.port = tk.IntVar(value=DEFAULT_PORT)
self.servers = None
self.selected_index = None
self.selected = None
@ -54,31 +53,17 @@ class ServersDialog(Dialog):
frame.grid(pady=PADY, sticky="ew")
frame.columnconfigure(1, weight=1)
frame.columnconfigure(3, weight=1)
frame.columnconfigure(5, weight=1)
label = ttk.Label(frame, text="Name")
label.grid(row=0, column=0, sticky="w", padx=PADX, pady=PADY)
label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = ttk.Entry(frame, textvariable=self.name)
entry.grid(row=0, column=1, sticky="ew")
label = ttk.Label(frame, text="Address")
label.grid(row=0, column=2, sticky="w", padx=PADX, pady=PADY)
label.grid(row=0, column=2, sticky="w", padx=PADX)
entry = ttk.Entry(frame, textvariable=self.address)
entry.grid(row=0, column=3, sticky="ew")
label = ttk.Label(frame, text="Port")
label.grid(row=0, column=4, sticky="w", padx=PADX, pady=PADY)
entry = ttk.Entry(
frame,
textvariable=self.port,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
)
entry.bind(
"<FocusOut>", lambda event: self.app.validation.focus_out(event, "50051")
)
entry.grid(row=0, column=5, sticky="ew")
def draw_servers_buttons(self):
frame = ttk.Frame(self.top)
frame.grid(pady=PADY, sticky="ew")
@ -113,13 +98,9 @@ class ServersDialog(Dialog):
button.grid(row=0, column=1, sticky="ew")
def click_save_configuration(self):
servers = []
for name in sorted(self.app.core.servers):
server = self.app.core.servers[name]
servers.append(
{"name": server.name, "address": server.address, "port": server.port}
)
self.app.guiconfig["servers"] = servers
self.app.guiconfig.servers.clear()
for server in self.app.core.servers.values():
self.app.guiconfig.servers.append(server)
self.app.save_config()
self.destroy()
@ -127,8 +108,7 @@ class ServersDialog(Dialog):
name = self.name.get()
if name not in self.app.core.servers:
address = self.address.get()
port = self.port.get()
server = CoreServer(name, address, port)
server = CoreServer(name, address)
self.app.core.servers[name] = server
self.servers.insert(tk.END, name)
@ -140,7 +120,6 @@ class ServersDialog(Dialog):
server = self.app.core.servers.pop(previous_name)
server.name = name
server.address = self.address.get()
server.port = self.port.get()
self.app.core.servers[name] = server
self.servers.delete(self.selected_index)
self.servers.insert(self.selected_index, name)
@ -154,7 +133,6 @@ class ServersDialog(Dialog):
self.selected_index = None
self.name.set(DEFAULT_NAME)
self.address.set(DEFAULT_ADDRESS)
self.port.set(DEFAULT_PORT)
self.servers.selection_clear(0, tk.END)
self.save_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED)
@ -167,7 +145,6 @@ class ServersDialog(Dialog):
server = self.app.core.servers[self.selected]
self.name.set(server.name)
self.address.set(server.address)
self.port.set(server.port)
self.save_button.config(state=tk.NORMAL)
self.delete_button.config(state=tk.NORMAL)
else:

View file

@ -2,36 +2,37 @@ import logging
import os
import tkinter as tk
from tkinter import filedialog, ttk
from typing import TYPE_CHECKING, Any, List
from typing import TYPE_CHECKING, List
import grpc
from core.api.grpc.services_pb2 import ServiceValidationMode
from core.gui.dialogs.copyserviceconfig import CopyServiceConfigDialog
from core.gui.dialogs.dialog import Dialog
from core.gui.errors import show_grpc_error
from core.gui.images import ImageEnum, Images
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CodeText, ListboxScroll
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.node import CanvasNode
class ServiceConfigDialog(Dialog):
def __init__(
self, master: Any, app: "Application", service_name: str, node_id: int
self,
master: tk.BaseWidget,
app: "Application",
service_name: str,
canvas_node: "CanvasNode",
node_id: int,
):
title = f"{service_name} Service"
super().__init__(master, app, title, modal=True)
self.master = master
self.app = app
super().__init__(app, title, master=master)
self.core = app.core
self.canvas_node = canvas_node
self.node_id = node_id
self.service_name = service_name
self.service_configs = app.core.service_configs
self.file_configs = app.core.file_configs
self.radiovar = tk.IntVar()
self.radiovar.set(2)
self.metadata = ""
@ -54,7 +55,6 @@ class ServiceConfigDialog(Dialog):
ImageEnum.DOCUMENTNEW, int(16 * app.app_scale)
)
self.editdelete_img = Images.get(ImageEnum.EDITDELETE, int(16 * app.app_scale))
self.notebook = None
self.metadata_entry = None
self.filename_combobox = None
@ -70,9 +70,7 @@ class ServiceConfigDialog(Dialog):
self.default_config = None
self.temp_service_files = {}
self.modified_files = set()
self.has_error = False
self.load()
if not self.has_error:
self.draw()
@ -87,8 +85,8 @@ class ServiceConfigDialog(Dialog):
self.default_validate = default_config.validate[:]
self.default_shutdown = default_config.shutdown[:]
self.default_directories = default_config.dirs[:]
custom_service_config = self.service_configs.get(self.node_id, {}).get(
self.service_name, None
custom_service_config = self.canvas_node.service_configs.get(
self.service_name
)
self.default_config = default_config
service_config = (
@ -111,14 +109,15 @@ class ServiceConfigDialog(Dialog):
for x in default_config.configs
}
self.temp_service_files = dict(self.original_service_files)
file_config = self.file_configs.get(self.node_id, {}).get(
file_configs = self.canvas_node.service_file_configs.get(
self.service_name, {}
)
for file, data in file_config.items():
for file, data in file_configs.items():
self.temp_service_files[file] = data
except grpc.RpcError as e:
self.app.show_grpc_exception("Get Node Service Error", e)
self.has_error = True
show_grpc_error(e, self.master, self.app)
def draw(self):
self.top.columnconfigure(0, weight=1)
@ -227,6 +226,7 @@ class ServiceConfigDialog(Dialog):
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew")
tab.columnconfigure(0, weight=1)
tab.rowconfigure(2, weight=1)
self.notebook.add(tab, text="Directories")
label = ttk.Label(
@ -236,15 +236,14 @@ class ServiceConfigDialog(Dialog):
label.grid(row=0, column=0, sticky="ew")
frame = ttk.Frame(tab, padding=FRAME_PAD)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.grid(row=1, column=0, sticky="nsew")
var = tk.StringVar(value="")
self.directory_entry = ttk.Entry(frame, textvariable=var)
self.directory_entry.grid(row=0, column=0, sticky="ew")
self.directory_entry.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="...", command=self.find_directory_button)
button.grid(row=0, column=1, sticky="ew")
self.dir_list = ListboxScroll(tab)
self.dir_list.grid(row=2, column=0, sticky="nsew")
self.dir_list.grid(row=2, column=0, sticky="nsew", pady=PADY)
self.dir_list.listbox.bind("<<ListboxSelect>>", self.directory_select)
for d in self.temp_directories:
self.dir_list.listbox.insert("end", d)
@ -254,7 +253,7 @@ class ServiceConfigDialog(Dialog):
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
button = ttk.Button(frame, text="Add", command=self.add_directory)
button.grid(row=0, column=0, sticky="ew")
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Remove", command=self.remove_directory)
button.grid(row=0, column=1, sticky="ew")
@ -449,7 +448,7 @@ class ServiceConfigDialog(Dialog):
and not self.has_new_files()
and not self.is_custom_directory()
):
self.service_configs.get(self.node_id, {}).pop(self.service_name, None)
self.canvas_node.service_configs.pop(self.service_name, None)
self.current_service_color("")
self.destroy()
return
@ -470,23 +469,19 @@ class ServiceConfigDialog(Dialog):
validations=validate,
shutdowns=shutdown,
)
if self.node_id not in self.service_configs:
self.service_configs[self.node_id] = {}
self.service_configs[self.node_id][self.service_name] = config
self.canvas_node.service_configs[self.service_name] = config
for file in self.modified_files:
if self.node_id not in self.file_configs:
self.file_configs[self.node_id] = {}
if self.service_name not in self.file_configs[self.node_id]:
self.file_configs[self.node_id][self.service_name] = {}
self.file_configs[self.node_id][self.service_name][
file
] = self.temp_service_files[file]
file_configs = self.canvas_node.service_file_configs.setdefault(
self.service_name, {}
)
file_configs[file] = self.temp_service_files[file]
# TODO: check if this is really needed
self.app.core.set_node_service_file(
self.node_id, self.service_name, file, self.temp_service_files[file]
)
self.current_service_color("green")
except grpc.RpcError as e:
show_grpc_error(e, self.top, self.app)
self.app.show_grpc_exception("Save Service Config Error", e)
self.destroy()
def display_service_file_data(self, event: tk.Event):
@ -526,8 +521,9 @@ class ServiceConfigDialog(Dialog):
clears out any custom configuration permanently
"""
# clear coreclient data
self.service_configs.get(self.node_id, {}).pop(self.service_name, None)
self.file_configs.get(self.node_id, {}).pop(self.service_name, None)
self.canvas_node.service_configs.pop(self.service_name, None)
file_configs = self.canvas_node.service_file_configs.pop(self.service_name, {})
file_configs.pop(self.service_name, None)
self.temp_service_files = dict(self.original_service_files)
self.modified_files.clear()

View file

@ -5,7 +5,6 @@ from typing import TYPE_CHECKING
import grpc
from core.gui.dialogs.dialog import Dialog
from core.gui.errors import show_grpc_error
from core.gui.themes import PADX, PADY
from core.gui.widgets import ConfigFrame
@ -14,8 +13,8 @@ if TYPE_CHECKING:
class SessionOptionsDialog(Dialog):
def __init__(self, master: "Application", app: "Application"):
super().__init__(master, app, "Session Options", modal=True)
def __init__(self, app: "Application"):
super().__init__(app, "Session Options")
self.config_frame = None
self.has_error = False
self.config = self.get_config()
@ -28,8 +27,8 @@ class SessionOptionsDialog(Dialog):
response = self.app.core.client.get_session_options(session_id)
return response.config
except grpc.RpcError as e:
self.app.show_grpc_exception("Get Session Options Error", e)
self.has_error = True
show_grpc_error(e, self.app, self.app)
self.destroy()
def draw(self):
@ -47,7 +46,7 @@ class SessionOptionsDialog(Dialog):
button = ttk.Button(frame, text="Save", command=self.save)
button.grid(row=0, column=0, padx=PADX, sticky="ew")
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, padx=PADX, sticky="ew")
button.grid(row=0, column=1, sticky="ew")
def save(self):
config = self.config_frame.parse_config()
@ -56,5 +55,5 @@ class SessionOptionsDialog(Dialog):
response = self.app.core.client.set_session_options(session_id, config)
logging.info("saved session config: %s", response)
except grpc.RpcError as e:
show_grpc_error(e, self.top, self.app)
self.app.show_grpc_exception("Set Session Options Error", e)
self.destroy()

View file

@ -1,15 +1,14 @@
import logging
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING, Iterable
from tkinter import messagebox, ttk
from typing import TYPE_CHECKING, List
import grpc
from core.api.grpc import core_pb2
from core.gui.dialogs.dialog import Dialog
from core.gui.errors import show_grpc_error
from core.gui.images import ImageEnum, Images
from core.gui.task import BackgroundTask
from core.gui.task import ProgressTask
from core.gui.themes import PADX, PADY
if TYPE_CHECKING:
@ -17,37 +16,35 @@ if TYPE_CHECKING:
class SessionsDialog(Dialog):
def __init__(
self, master: "Application", app: "Application", is_start_app: bool = False
):
super().__init__(master, app, "Sessions", modal=True)
def __init__(self, app: "Application", is_start_app: bool = False) -> None:
super().__init__(app, "Sessions")
self.is_start_app = is_start_app
self.selected = False
self.selected_session = None
self.selected_id = None
self.tree = None
self.has_error = False
self.sessions = self.get_sessions()
if not self.has_error:
self.draw()
self.connect_button = None
self.delete_button = None
self.protocol("WM_DELETE_WINDOW", self.on_closing)
self.draw()
def get_sessions(self) -> Iterable[core_pb2.SessionSummary]:
def get_sessions(self) -> List[core_pb2.SessionSummary]:
try:
response = self.app.core.client.get_sessions()
logging.info("sessions: %s", response)
return response.sessions
except grpc.RpcError as e:
show_grpc_error(e, self.app, self.app)
self.has_error = True
self.app.show_grpc_exception("Get Sessions Error", e)
self.destroy()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(1, weight=1)
self.draw_description()
self.draw_tree()
self.draw_buttons()
def draw_description(self):
def draw_description(self) -> None:
"""
write a short description
"""
@ -61,20 +58,26 @@ class SessionsDialog(Dialog):
)
label.grid(pady=PADY)
def draw_tree(self):
def draw_tree(self) -> None:
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.rowconfigure(0, weight=1)
frame.grid(sticky="nsew", pady=PADY)
self.tree = ttk.Treeview(
frame, columns=("id", "state", "nodes"), show="headings"
frame,
columns=("id", "state", "nodes"),
show="headings",
selectmode=tk.BROWSE,
)
style = ttk.Style()
heading_size = int(self.app.guiconfig.scale * 10)
style.configure("Treeview.Heading", font=(None, heading_size, "bold"))
self.tree.grid(sticky="nsew")
self.tree.column("id", stretch=tk.YES)
self.tree.column("id", stretch=tk.YES, anchor="center")
self.tree.heading("id", text="ID")
self.tree.column("state", stretch=tk.YES)
self.tree.column("state", stretch=tk.YES, anchor="center")
self.tree.heading("state", text="State")
self.tree.column("nodes", stretch=tk.YES)
self.tree.column("nodes", stretch=tk.YES, anchor="center")
self.tree.heading("nodes", text="Node Count")
for index, session in enumerate(self.sessions):
@ -85,7 +88,7 @@ class SessionsDialog(Dialog):
text=str(session.id),
values=(session.id, state_name, session.nodes),
)
self.tree.bind("<Double-1>", self.on_selected)
self.tree.bind("<Double-1>", self.double_click_join)
self.tree.bind("<<TreeviewSelect>>", self.click_select)
yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
@ -96,9 +99,9 @@ class SessionsDialog(Dialog):
xscrollbar.grid(row=1, sticky="ew")
self.tree.configure(xscrollcommand=xscrollbar.set)
def draw_buttons(self):
def draw_buttons(self) -> None:
frame = ttk.Frame(self.top)
for i in range(5):
for i in range(4):
frame.columnconfigure(i, weight=1)
frame.grid(sticky="ew")
@ -110,42 +113,37 @@ class SessionsDialog(Dialog):
b.grid(row=0, padx=PADX, sticky="ew")
image = Images.get(ImageEnum.FILEOPEN, 16)
b = ttk.Button(
self.connect_button = ttk.Button(
frame,
image=image,
text="Connect",
compound=tk.LEFT,
command=self.click_connect,
state=tk.DISABLED,
)
b.image = image
b.grid(row=0, column=1, padx=PADX, sticky="ew")
image = Images.get(ImageEnum.SHUTDOWN, 16)
b = ttk.Button(
frame,
image=image,
text="Shutdown",
compound=tk.LEFT,
command=self.click_shutdown,
)
b.image = image
b.grid(row=0, column=2, padx=PADX, sticky="ew")
self.connect_button.image = image
self.connect_button.grid(row=0, column=1, padx=PADX, sticky="ew")
image = Images.get(ImageEnum.DELETE, 16)
b = ttk.Button(
self.delete_button = ttk.Button(
frame,
image=image,
text="Delete",
compound=tk.LEFT,
command=self.click_delete,
state=tk.DISABLED,
)
b.image = image
b.grid(row=0, column=3, padx=PADX, sticky="ew")
self.delete_button.image = image
self.delete_button.grid(row=0, column=2, padx=PADX, sticky="ew")
image = Images.get(ImageEnum.CANCEL, 16)
if self.is_start_app:
b = ttk.Button(
frame, image=image, text="Exit", compound=tk.LEFT, command=self.destroy
frame,
image=image,
text="Exit",
compound=tk.LEFT,
command=self.click_exit,
)
else:
b = ttk.Button(
@ -156,69 +154,64 @@ class SessionsDialog(Dialog):
command=self.destroy,
)
b.image = image
b.grid(row=0, column=4, sticky="ew")
b.grid(row=0, column=3, sticky="ew")
def click_new(self):
def click_new(self) -> None:
self.app.core.create_new_session()
self.destroy()
def click_select(self, event: tk.Event):
item = self.tree.selection()
session_id = int(self.tree.item(item, "text"))
self.selected = True
self.selected_id = session_id
def click_connect(self):
"""
if no session is selected yet, create a new one else join that session
"""
if self.selected and self.selected_id is not None:
self.join_session(self.selected_id)
elif not self.selected and self.selected_id is None:
self.click_new()
else:
logging.error("sessions invalid state")
def click_shutdown(self):
"""
if no session is currently selected create a new session else shut the selected
session down.
"""
if self.selected and self.selected_id is not None:
self.shutdown_session(self.selected_id)
elif not self.selected and self.selected_id is None:
self.click_new()
else:
logging.error("querysessiondrawing.py invalid state")
def join_session(self, session_id: int):
if self.app.core.xml_file:
self.app.core.xml_file = None
self.app.statusbar.progress_bar.start(5)
task = BackgroundTask(self.app, self.app.core.join_session, args=(session_id,))
task.start()
self.destroy()
def on_selected(self, event: tk.Event):
item = self.tree.selection()
sid = int(self.tree.item(item, "text"))
self.join_session(sid)
def shutdown_session(self, sid: int):
self.app.core.stop_session(sid)
self.click_new()
self.destroy()
def click_delete(self):
logging.debug("Click delete")
def click_select(self, _event: tk.Event = None) -> None:
item = self.tree.selection()
if item:
sid = int(self.tree.item(item, "text"))
self.app.core.delete_session(sid, self.top)
self.tree.delete(item[0])
if sid == self.app.core.session_id:
self.click_new()
selections = self.tree.get_children()
if selections:
self.tree.focus(selections[0])
self.tree.selection_set(selections[0])
self.selected_session = int(self.tree.item(item, "text"))
self.selected_id = item
self.delete_button.config(state=tk.NORMAL)
self.connect_button.config(state=tk.NORMAL)
else:
self.selected_session = None
self.selected_id = None
self.delete_button.config(state=tk.DISABLED)
self.connect_button.config(state=tk.DISABLED)
logging.debug("selected session: %s", self.selected_session)
def click_connect(self) -> None:
if not self.selected_session:
return
self.join_session(self.selected_session)
def join_session(self, session_id: int) -> None:
self.destroy()
if self.app.core.xml_file:
self.app.core.xml_file = None
task = ProgressTask(
self.app, "Join", self.app.core.join_session, args=(session_id,)
)
task.start()
def double_click_join(self, _event: tk.Event) -> None:
item = self.tree.selection()
if item is None:
return
session_id = int(self.tree.item(item, "text"))
self.join_session(session_id)
def click_delete(self) -> None:
if not self.selected_session:
return
logging.debug("delete session: %s", self.selected_session)
self.tree.delete(self.selected_id)
self.app.core.delete_session(self.selected_session)
if self.selected_session == self.app.core.session_id:
self.click_new()
self.destroy()
self.click_select()
def click_exit(self) -> None:
self.destroy()
self.app.close()
def on_closing(self) -> None:
if self.is_start_app and messagebox.askokcancel("Exit", "Quit?", parent=self):
self.click_exit()
if not self.is_start_app:
self.destroy()

View file

@ -20,12 +20,12 @@ BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
class ShapeDialog(Dialog):
def __init__(self, master: "Application", app: "Application", shape: "Shape"):
def __init__(self, app: "Application", shape: "Shape"):
if is_draw_shape(shape.shape_type):
title = "Add Shape"
else:
title = "Add Text"
super().__init__(master, app, title, modal=True)
super().__init__(app, title)
self.canvas = app.canvas
self.fill = None
self.border = None
@ -235,7 +235,7 @@ class ShapeDialog(Dialog):
text=shape_text,
fill=self.text_color,
font=text_font,
tags=tags.SHAPE_TEXT,
tags=(tags.SHAPE_TEXT, tags.ANNOTATION),
)
self.shape.created = True
else:

View file

@ -14,9 +14,8 @@ if TYPE_CHECKING:
class ThroughputDialog(Dialog):
def __init__(self, master: "Application", app: "Application"):
super().__init__(master, app, "Throughput Config", modal=False)
self.app = app
def __init__(self, app: "Application"):
super().__init__(app, "Throughput Config")
self.canvas = app.canvas
self.show_throughput = tk.IntVar(value=1)
self.exponential_weight = tk.IntVar(value=1)

View file

@ -4,7 +4,6 @@ from typing import TYPE_CHECKING
import grpc
from core.gui.dialogs.dialog import Dialog
from core.gui.errors import show_grpc_error
from core.gui.themes import PADX, PADY
from core.gui.widgets import ConfigFrame
@ -17,12 +16,8 @@ RANGE_WIDTH = 3
class WlanConfigDialog(Dialog):
def __init__(
self, master: "Application", app: "Application", canvas_node: "CanvasNode"
):
super().__init__(
master, app, f"{canvas_node.core_node.name} Wlan Configuration", modal=True
)
def __init__(self, app: "Application", canvas_node: "CanvasNode"):
super().__init__(app, f"{canvas_node.core_node.name} WLAN Configuration")
self.canvas_node = canvas_node
self.node = canvas_node.core_node
self.config_frame = None
@ -32,11 +27,13 @@ class WlanConfigDialog(Dialog):
self.ranges = {}
self.positive_int = self.app.master.register(self.validate_and_update)
try:
self.config = self.app.core.get_wlan_config(self.node.id)
self.config = self.canvas_node.wlan_config
if not self.config:
self.config = self.app.core.get_wlan_config(self.node.id)
self.init_draw_range()
self.draw()
except grpc.RpcError as e:
show_grpc_error(e, self.app, self.app)
self.app.show_grpc_exception("WLAN Config Error", e)
self.has_error = True
self.destroy()
@ -83,7 +80,7 @@ class WlanConfigDialog(Dialog):
retrieve user's wlan configuration and store the new configuration values
"""
config = self.config_frame.parse_config()
self.app.core.wlan_configs[self.node.id] = self.config
self.canvas_node.wlan_config = self.config
if self.app.core.is_runtime():
session_id = self.app.core.session_id
self.app.core.client.set_wlan_config(session_id, self.node.id, config)
@ -102,7 +99,7 @@ class WlanConfigDialog(Dialog):
if len(s) == 0:
return True
try:
int_value = int(s)
int_value = int(s) / 2
if int_value >= 0:
net_range = int_value * self.canvas.ratio
if self.canvas_node.id in self.canvas.wireless_network:

View file

@ -1,11 +1,13 @@
import logging
import math
import tkinter as tk
from typing import TYPE_CHECKING, Any, Tuple
from core.api.grpc import core_pb2
from core.gui import themes
from core.gui.dialogs.linkconfig import LinkConfigurationDialog
from core.gui.graph import tags
from core.gui.nodeutils import EdgeUtils, NodeUtils
from core.gui.nodeutils import NodeUtils
if TYPE_CHECKING:
from core.gui.graph.graph import CanvasGraph
@ -15,182 +17,325 @@ EDGE_WIDTH = 3
EDGE_COLOR = "#ff0000"
WIRELESS_WIDTH = 1.5
WIRELESS_COLOR = "#009933"
ARC_DISTANCE = 50
class CanvasWirelessEdge:
def __init__(
self,
token: Tuple[Any, ...],
position: Tuple[float, float, float, float],
src: int,
dst: int,
canvas: "CanvasGraph",
):
logging.debug("Draw wireless link from node %s to node %s", src, dst)
self.token = token
def create_edge_token(src: int, dst: int, network: int = None) -> Tuple[int, ...]:
values = [src, dst]
if network is not None:
values.append(network)
return tuple(sorted(values))
def arc_edges(edges) -> None:
if not edges:
return
mid_index = len(edges) // 2
if mid_index == 0:
arc_step = ARC_DISTANCE
else:
arc_step = ARC_DISTANCE / mid_index
# below edges
arc = 0
for edge in edges[:mid_index]:
arc -= arc_step
edge.arc = arc
edge.redraw()
# mid edge
if len(edges) % 2 != 0:
arc = 0
edge = edges[mid_index]
edge.arc = arc
edge.redraw()
mid_index += 1
# above edges
arc = 0
for edge in edges[mid_index:]:
arc += arc_step
edge.arc = arc
edge.redraw()
class Edge:
tag = tags.EDGE
def __init__(self, canvas: "CanvasGraph", src: int, dst: int = None) -> None:
self.canvas = canvas
self.id = None
self.src = src
self.dst = dst
self.canvas = canvas
self.arc = 0
self.token = None
self.src_label = None
self.middle_label = None
self.dst_label = None
self.color = EDGE_COLOR
self.width = EDGE_WIDTH
@classmethod
def create_token(cls, src: int, dst: int) -> Tuple[int, ...]:
return tuple(sorted([src, dst]))
def scaled_width(self) -> float:
return self.width * self.canvas.app.app_scale
def _get_arcpoint(
self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]
) -> Tuple[float, float]:
src_x, src_y = src_pos
dst_x, dst_y = dst_pos
mp_x = (src_x + dst_x) / 2
mp_y = (src_y + dst_y) / 2
slope_denominator = src_x - dst_x
slope_numerator = src_y - dst_y
# vertical line
if slope_denominator == 0:
return mp_x + self.arc, mp_y
# horizontal line
if slope_numerator == 0:
return mp_x, mp_y + self.arc
# everything else
m = slope_numerator / slope_denominator
perp_m = -1 / m
b = mp_y - (perp_m * mp_x)
# get arc x and y
offset = math.sqrt(self.arc ** 2 / (1 + (1 / m ** 2)))
arc_x = mp_x
if self.arc >= 0:
arc_x += offset
else:
arc_x -= offset
arc_y = (perp_m * arc_x) + b
return arc_x, arc_y
def draw(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None:
arc_pos = self._get_arcpoint(src_pos, dst_pos)
self.id = self.canvas.create_line(
*position,
tags=tags.WIRELESS_EDGE,
width=WIRELESS_WIDTH * self.canvas.app.app_scale,
fill=WIRELESS_COLOR,
*src_pos,
*arc_pos,
*dst_pos,
smooth=True,
tags=self.tag,
width=self.scaled_width(),
fill=self.color,
)
def delete(self):
def redraw(self):
self.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color)
src_x, src_y, _, _, _, _ = self.canvas.coords(self.id)
src_pos = src_x, src_y
self.move_src(src_pos)
def middle_label_pos(self) -> Tuple[float, float]:
_, _, x, y, _, _ = self.canvas.coords(self.id)
return x, y
def middle_label_text(self, text: str) -> None:
if self.middle_label is None:
x, y = self.middle_label_pos()
self.middle_label = self.canvas.create_text(
x,
y,
font=self.canvas.app.edge_font,
text=text,
tags=tags.LINK_LABEL,
state=self.canvas.show_link_labels.state(),
)
else:
self.canvas.itemconfig(self.middle_label, text=text)
def node_label_positions(self) -> Tuple[Tuple[float, float], Tuple[float, float]]:
src_x, src_y, _, _, dst_x, dst_y = self.canvas.coords(self.id)
v1 = dst_x - src_x
v2 = dst_y - src_y
ux = TEXT_DISTANCE * v1
uy = TEXT_DISTANCE * v2
src_x = src_x + ux
src_y = src_y + uy
dst_x = dst_x - ux
dst_y = dst_y - uy
return (src_x, src_y), (dst_x, dst_y)
def src_label_text(self, text: str) -> None:
if self.src_label is None:
src_pos, _ = self.node_label_positions()
self.src_label = self.canvas.create_text(
*src_pos,
text=text,
justify=tk.CENTER,
font=self.canvas.app.edge_font,
tags=tags.LINK_LABEL,
state=self.canvas.show_link_labels.state(),
)
else:
self.canvas.itemconfig(self.src_label, text=text)
def dst_label_text(self, text: str) -> None:
if self.dst_label is None:
_, dst_pos = self.node_label_positions()
self.dst_label = self.canvas.create_text(
*dst_pos,
text=text,
justify=tk.CENTER,
font=self.canvas.app.edge_font,
tags=tags.LINK_LABEL,
state=self.canvas.show_link_labels.state(),
)
else:
self.canvas.itemconfig(self.dst_label, text=text)
def move_node(self, node_id: int, pos: Tuple[float, float]) -> None:
if self.src == node_id:
self.move_src(pos)
else:
self.move_dst(pos)
def move_dst(self, dst_pos: Tuple[float, float]) -> None:
src_x, src_y, _, _, _, _ = self.canvas.coords(self.id)
src_pos = src_x, src_y
self.moved(src_pos, dst_pos)
def move_src(self, src_pos: Tuple[float, float]) -> None:
_, _, _, _, dst_x, dst_y = self.canvas.coords(self.id)
dst_pos = dst_x, dst_y
self.moved(src_pos, dst_pos)
def moved(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None:
arc_pos = self._get_arcpoint(src_pos, dst_pos)
self.canvas.coords(self.id, *src_pos, *arc_pos, *dst_pos)
if self.middle_label:
self.canvas.coords(self.middle_label, *arc_pos)
src_pos, dst_pos = self.node_label_positions()
if self.src_label:
self.canvas.coords(self.src_label, *src_pos)
if self.dst_label:
self.canvas.coords(self.dst_label, *dst_pos)
def delete(self) -> None:
logging.debug("deleting canvas edge, id: %s", self.id)
self.canvas.delete(self.id)
self.canvas.delete(self.src_label)
self.canvas.delete(self.middle_label)
self.canvas.delete(self.dst_label)
self.id = None
self.src_label = None
self.middle_label = None
self.dst_label = None
class CanvasEdge:
class CanvasWirelessEdge(Edge):
tag = tags.WIRELESS_EDGE
def __init__(
self,
canvas: "CanvasGraph",
src: int,
dst: int,
src_pos: Tuple[float, float],
dst_pos: Tuple[float, float],
token: Tuple[Any, ...],
) -> None:
logging.debug("drawing wireless link from node %s to node %s", src, dst)
super().__init__(canvas, src, dst)
self.token = token
self.width = WIRELESS_WIDTH
self.color = WIRELESS_COLOR
self.draw(src_pos, dst_pos)
class CanvasEdge(Edge):
"""
Canvas edge class
"""
def __init__(
self,
x1: float,
y1: float,
x2: float,
y2: float,
src: int,
canvas: "CanvasGraph",
):
src: int,
src_pos: Tuple[float, float],
dst_pos: Tuple[float, float],
) -> None:
"""
Create an instance of canvas edge object
"""
self.src = src
self.dst = None
super().__init__(canvas, src)
self.src_interface = None
self.dst_interface = None
self.canvas = canvas
self.id = self.canvas.create_line(
x1,
y1,
x2,
y2,
tags=tags.EDGE,
width=EDGE_WIDTH * self.canvas.app.app_scale,
fill=EDGE_COLOR,
)
self.text_src = None
self.text_dst = None
self.text_middle = None
self.token = None
self.link = None
self.asymmetric_link = None
self.throughput = None
self.draw(src_pos, dst_pos)
self.set_binding()
self.context = tk.Menu(self.canvas)
self.create_context()
def set_binding(self):
self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.create_context)
def create_context(self):
themes.style_menu(self.context)
self.context.add_command(label="Configure", command=self.click_configure)
self.context.add_command(label="Delete", command=self.click_delete)
def set_link(self, link):
def set_binding(self) -> None:
self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.show_context)
def set_link(self, link) -> None:
self.link = link
self.draw_labels()
def get_coordinates(self) -> [float, float, float, float]:
x1, y1, x2, y2 = self.canvas.coords(self.id)
v1 = x2 - x1
v2 = y2 - y1
ux = TEXT_DISTANCE * v1
uy = TEXT_DISTANCE * v2
x1 = x1 + ux
y1 = y1 + uy
x2 = x2 - ux
y2 = y2 - uy
return x1, y1, x2, y2
def get_midpoint(self) -> [float, float]:
x1, y1, x2, y2 = self.canvas.coords(self.id)
x = (x1 + x2) / 2
y = (y1 + y2) / 2
return x, y
def create_labels(self):
label_one = None
if self.link.HasField("interface_one"):
label_one = self.create_label(self.link.interface_one)
label_two = None
if self.link.HasField("interface_two"):
label_two = self.create_label(self.link.interface_two)
return label_one, label_two
def create_label(self, interface):
def interface_label(self, interface: core_pb2.Interface) -> str:
label = ""
if interface.ip4:
label = f"{interface.ip4}/{interface.ip4mask}"
if interface.ip6:
label = f"{label}\n{interface.ip6}/{interface.ip6mask}"
if interface.name and self.canvas.show_interface_names.get():
label = f"{interface.name}"
if interface.ip4 and self.canvas.show_ip4s.get():
label = f"{label}\n" if label else ""
label += f"{interface.ip4}/{interface.ip4mask}"
if interface.ip6 and self.canvas.show_ip6s.get():
label = f"{label}\n" if label else ""
label += f"{interface.ip6}/{interface.ip6mask}"
return label
def draw_labels(self):
x1, y1, x2, y2 = self.get_coordinates()
label_one, label_two = self.create_labels()
self.text_src = self.canvas.create_text(
x1,
y1,
text=label_one,
justify=tk.CENTER,
font=self.canvas.app.edge_font,
tags=tags.LINK_INFO,
)
self.text_dst = self.canvas.create_text(
x2,
y2,
text=label_two,
justify=tk.CENTER,
font=self.canvas.app.edge_font,
tags=tags.LINK_INFO,
)
def create_node_labels(self) -> Tuple[str, str]:
label_one = None
if self.link.HasField("interface_one"):
label_one = self.interface_label(self.link.interface_one)
label_two = None
if self.link.HasField("interface_two"):
label_two = self.interface_label(self.link.interface_two)
return label_one, label_two
def redraw(self):
label_one, label_two = self.create_labels()
self.canvas.itemconfig(self.text_src, text=label_one)
self.canvas.itemconfig(self.text_dst, text=label_two)
def draw_labels(self) -> None:
src_text, dst_text = self.create_node_labels()
self.src_label_text(src_text)
self.dst_label_text(dst_text)
def update_labels(self):
"""
Move edge labels based on current position.
"""
x1, y1, x2, y2 = self.get_coordinates()
self.canvas.coords(self.text_src, x1, y1)
self.canvas.coords(self.text_dst, x2, y2)
if self.text_middle is not None:
x, y = self.get_midpoint()
self.canvas.coords(self.text_middle, x, y)
def redraw(self) -> None:
super().redraw()
self.draw_labels()
def set_throughput(self, throughput: float):
def set_throughput(self, throughput: float) -> None:
throughput = 0.001 * throughput
value = f"{throughput:.3f} kbps"
if self.text_middle is None:
x, y = self.get_midpoint()
self.text_middle = self.canvas.create_text(
x, y, tags=tags.THROUGHPUT, font=self.canvas.app.edge_font, text=value
)
else:
self.canvas.itemconfig(self.text_middle, text=value)
text = f"{throughput:.3f} kbps"
self.middle_label_text(text)
if throughput > self.canvas.throughput_threshold:
color = self.canvas.throughput_color
width = self.canvas.throughput_width
else:
color = EDGE_COLOR
width = EDGE_WIDTH
color = self.color
width = self.scaled_width()
self.canvas.itemconfig(self.id, fill=color, width=width)
def complete(self, dst: int):
def complete(self, dst: int) -> None:
self.dst = dst
self.token = EdgeUtils.get_token(self.src, self.dst)
x, y = self.canvas.coords(self.dst)
x1, y1, _, _ = self.canvas.coords(self.id)
self.canvas.coords(self.id, x1, y1, x, y)
self.token = create_edge_token(self.src, self.dst)
dst_pos = self.canvas.coords(self.dst)
self.move_dst(dst_pos)
self.check_wireless()
self.canvas.tag_raise(self.src)
self.canvas.tag_raise(self.dst)
logging.debug("Draw wired link from node %s to node %s", self.src, dst)
def is_wireless(self) -> [bool, bool]:
def is_wireless(self) -> bool:
src_node = self.canvas.nodes[self.src]
dst_node = self.canvas.nodes[self.dst]
src_node_type = src_node.core_node.type
@ -210,12 +355,12 @@ class CanvasEdge:
wlan_network[self.dst].add(self.src)
return is_src_wireless or is_dst_wireless
def check_wireless(self):
def check_wireless(self) -> None:
if self.is_wireless():
self.canvas.itemconfig(self.id, state=tk.HIDDEN)
self._check_antenna()
def _check_antenna(self):
def _check_antenna(self) -> None:
src_node = self.canvas.nodes[self.src]
dst_node = self.canvas.nodes[self.dst]
src_node_type = src_node.core_node.type
@ -230,32 +375,19 @@ class CanvasEdge:
else:
src_node.add_antenna()
def delete(self):
logging.debug("Delete canvas edge, id: %s", self.id)
self.canvas.delete(self.id)
if self.link:
self.canvas.delete(self.text_src)
self.canvas.delete(self.text_dst)
self.canvas.delete(self.text_middle)
def reset(self) -> None:
self.canvas.delete(self.middle_label)
self.middle_label = None
self.canvas.itemconfig(self.id, fill=self.color, width=self.scaled_width())
def reset(self):
self.canvas.delete(self.text_middle)
self.text_middle = None
self.canvas.itemconfig(self.id, fill=EDGE_COLOR, width=EDGE_WIDTH)
def show_context(self, event: tk.Event) -> None:
state = tk.DISABLED if self.canvas.core.is_runtime() else tk.NORMAL
self.context.entryconfigure(1, state=state)
self.context.tk_popup(event.x_root, event.y_root)
def create_context(self, event: tk.Event):
context = tk.Menu(self.canvas)
themes.style_menu(context)
context.add_command(label="Configure", command=self.configure)
context.add_command(label="Delete")
context.add_command(label="Split")
context.add_command(label="Merge")
if self.canvas.app.core.is_runtime():
context.entryconfigure(1, state="disabled")
context.entryconfigure(2, state="disabled")
context.entryconfigure(3, state="disabled")
context.post(event.x_root, event.y_root)
def click_delete(self):
self.canvas.delete_edge(self)
def configure(self):
dialog = LinkConfigurationDialog(self.canvas, self.canvas.app, self)
def click_configure(self) -> None:
dialog = LinkConfigurationDialog(self.canvas.app, self)
dialog.show()

View file

@ -1,5 +1,7 @@
import logging
import tkinter as tk
from copy import deepcopy
from tkinter import BooleanVar
from typing import TYPE_CHECKING, Tuple
from PIL import Image, ImageTk
@ -7,13 +9,19 @@ from PIL import Image, ImageTk
from core.api.grpc import core_pb2
from core.gui.dialogs.shapemod import ShapeDialog
from core.gui.graph import tags
from core.gui.graph.edges import EDGE_WIDTH, CanvasEdge, CanvasWirelessEdge
from core.gui.graph.edges import (
EDGE_WIDTH,
CanvasEdge,
CanvasWirelessEdge,
arc_edges,
create_edge_token,
)
from core.gui.graph.enums import GraphMode, ScaleOption
from core.gui.graph.node import CanvasNode
from core.gui.graph.shape import Shape
from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker
from core.gui.images import ImageEnum, Images, TypeToImage
from core.gui.nodeutils import EdgeUtils, NodeUtils
from core.gui.nodeutils import NodeUtils
if TYPE_CHECKING:
from core.gui.app import Application
@ -24,12 +32,30 @@ ZOOM_OUT = 0.9
ICON_SIZE = 48
class ShowVar(BooleanVar):
def __init__(self, canvas: "CanvasGraph", tag: str, value: bool) -> None:
super().__init__(value=value)
self.canvas = canvas
self.tag = tag
def state(self) -> str:
return tk.NORMAL if self.get() else tk.HIDDEN
def click_handler(self):
self.canvas.itemconfigure(self.tag, state=self.state())
class CanvasGraph(tk.Canvas):
def __init__(
self, master: "Application", core: "CoreClient", width: int, height: int
self,
master: tk.Widget,
app: "Application",
core: "CoreClient",
width: int,
height: int,
):
super().__init__(master, highlightthickness=0, background="#cccccc")
self.app = master
self.app = app
self.core = core
self.mode = GraphMode.SELECT
self.annotation_type = None
@ -37,7 +63,6 @@ class CanvasGraph(tk.Canvas):
self.select_box = None
self.selected = None
self.node_draw = None
self.context = None
self.nodes = {}
self.edges = {}
self.shapes = {}
@ -47,7 +72,7 @@ class CanvasGraph(tk.Canvas):
self.wireless_network = {}
self.drawing_edge = None
self.grid = None
self.rect = None
self.shape_drawing = False
self.default_dimensions = (width, height)
self.current_dimensions = self.default_dimensions
@ -63,7 +88,6 @@ class CanvasGraph(tk.Canvas):
self.wallpaper_drawn = None
self.wallpaper_file = ""
self.scale_option = tk.IntVar(value=1)
self.show_grid = tk.BooleanVar(value=True)
self.adjust_to_dim = tk.BooleanVar(value=False)
# throughput related
@ -71,6 +95,15 @@ class CanvasGraph(tk.Canvas):
self.throughput_width = 10
self.throughput_color = "#FF0000"
# drawing related
self.show_node_labels = ShowVar(self, tags.NODE_LABEL, value=True)
self.show_link_labels = ShowVar(self, tags.LINK_LABEL, value=True)
self.show_grid = ShowVar(self, tags.GRIDLINE, value=True)
self.show_annotations = ShowVar(self, tags.ANNOTATION, value=True)
self.show_interface_names = BooleanVar(value=False)
self.show_ip4s = BooleanVar(value=True)
self.show_ip6s = BooleanVar(value=True)
# bindings
self.setup_bindings()
@ -79,12 +112,12 @@ class CanvasGraph(tk.Canvas):
self.draw_grid()
def draw_canvas(self, dimensions: Tuple[int, int] = None):
if self.grid is not None:
self.delete(self.grid)
if self.rect is not None:
self.delete(self.rect)
if not dimensions:
dimensions = self.default_dimensions
self.current_dimensions = dimensions
self.grid = self.create_rectangle(
self.rect = self.create_rectangle(
0,
0,
*dimensions,
@ -101,8 +134,14 @@ class CanvasGraph(tk.Canvas):
client.
:param session: session to draw
"""
# hide context
self.hide_context()
# reset view options to default state
self.show_node_labels.set(True)
self.show_link_labels.set(True)
self.show_grid.set(True)
self.show_annotations.set(True)
self.show_interface_names.set(False)
self.show_ip4s.set(True)
self.show_ip6s.set(True)
# delete any existing drawn items
for tag in tags.COMPONENT_TAGS:
@ -128,7 +167,6 @@ class CanvasGraph(tk.Canvas):
self.bind("<ButtonPress-1>", self.click_press)
self.bind("<ButtonRelease-1>", self.click_release)
self.bind("<B1-Motion>", self.click_motion)
self.bind("<ButtonRelease-3>", self.click_context)
self.bind("<Delete>", self.press_delete)
self.bind("<Control-1>", self.ctrl_click)
self.bind("<Double-Button-1>", self.double_click)
@ -138,11 +176,6 @@ class CanvasGraph(tk.Canvas):
self.bind("<ButtonPress-3>", lambda e: self.scan_mark(e.x, e.y))
self.bind("<B3-Motion>", lambda e: self.scan_dragto(e.x, e.y, gain=1))
def hide_context(self, event=None):
if self.context:
self.context.unpost()
self.context = None
def get_actual_coords(self, x: float, y: float) -> [float, float]:
actual_x = (x - self.offset[0]) / self.ratio
actual_y = (y - self.offset[1]) / self.ratio
@ -154,7 +187,7 @@ class CanvasGraph(tk.Canvas):
return scaled_x, scaled_y
def inside_canvas(self, x: float, y: float) -> [bool, bool]:
x1, y1, x2, y2 = self.bbox(self.grid)
x1, y1, x2, y2 = self.bbox(self.rect)
valid_x = x1 <= x <= x2
valid_y = y1 <= y <= y2
return valid_x and valid_y
@ -191,29 +224,59 @@ class CanvasGraph(tk.Canvas):
for i in range(0, height, 27):
self.create_line(0, i, width, i, dash=(2, 4), tags=tags.GRIDLINE)
self.tag_lower(tags.GRIDLINE)
self.tag_lower(self.grid)
self.tag_lower(self.rect)
def add_wireless_edge(self, src: CanvasNode, dst: CanvasNode):
"""
add a wireless edge between 2 canvas nodes
"""
token = EdgeUtils.get_token(src.id, dst.id)
x1, y1 = self.coords(src.id)
x2, y2 = self.coords(dst.id)
position = (x1, y1, x2, y2)
edge = CanvasWirelessEdge(token, position, src.id, dst.id, self)
def add_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link
) -> None:
network_id = link.network_id if link.network_id else None
token = create_edge_token(src.id, dst.id, network_id)
if token in self.wireless_edges:
logging.warning("ignoring link that already exists: %s", link)
return
src_pos = self.coords(src.id)
dst_pos = self.coords(dst.id)
edge = CanvasWirelessEdge(self, src.id, dst.id, src_pos, dst_pos, token)
if link.label:
edge.middle_label_text(link.label)
if link.color:
edge.color = link.color
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)
# update arcs when there are multiple links
common_edges = list(src.wireless_edges & dst.wireless_edges)
arc_edges(common_edges)
def delete_wireless_edge(self, src: CanvasNode, dst: CanvasNode):
token = EdgeUtils.get_token(src.id, dst.id)
def delete_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link
) -> None:
network_id = link.network_id if link.network_id else None
token = create_edge_token(src.id, dst.id, network_id)
if token not in self.wireless_edges:
return
edge = self.wireless_edges.pop(token)
edge.delete()
src.wireless_edges.remove(edge)
dst.wireless_edges.remove(edge)
# update arcs when there are multiple links
common_edges = list(src.wireless_edges & dst.wireless_edges)
arc_edges(common_edges)
def update_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link
) -> None:
if not link.label:
return
network_id = link.network_id if link.network_id else None
token = create_edge_token(src.id, dst.id, network_id)
if token not in self.wireless_edges:
self.add_wireless_edge(src, dst, link)
else:
edge = self.wireless_edges[token]
edge.middle_label_text(link.label)
def draw_session(self, session: core_pb2.Session):
"""
@ -235,7 +298,7 @@ class CanvasGraph(tk.Canvas):
)
x = core_node.position.x
y = core_node.position.y
node = CanvasNode(self.master, x, y, core_node, image)
node = CanvasNode(self.app, x, y, core_node, image)
self.nodes[node.id] = node
self.core.canvas_nodes[core_node.id] = node
@ -246,20 +309,15 @@ class CanvasGraph(tk.Canvas):
node_one = canvas_node_one.core_node
canvas_node_two = self.core.canvas_nodes[link.node_two_id]
node_two = canvas_node_two.core_node
token = EdgeUtils.get_token(canvas_node_one.id, canvas_node_two.id)
token = create_edge_token(canvas_node_one.id, canvas_node_two.id)
if link.type == core_pb2.LinkType.WIRELESS:
self.add_wireless_edge(canvas_node_one, canvas_node_two)
self.add_wireless_edge(canvas_node_one, canvas_node_two, link)
else:
if token not in self.edges:
edge = CanvasEdge(
node_one.position.x,
node_one.position.y,
node_two.position.x,
node_two.position.y,
canvas_node_one.id,
self,
)
src_pos = (node_one.position.x, node_one.position.y)
dst_pos = (node_two.position.x, node_two.position.y)
edge = CanvasEdge(self, canvas_node_one.id, src_pos, dst_pos)
edge.token = token
edge.dst = canvas_node_two.id
edge.set_link(link)
@ -333,44 +391,38 @@ class CanvasGraph(tk.Canvas):
x, y = self.canvas_xy(event)
if not self.inside_canvas(x, y):
return
if self.context:
self.hide_context()
if self.mode == GraphMode.ANNOTATION:
self.focus_set()
if self.shape_drawing:
shape = self.shapes[self.selected]
shape.shape_complete(x, y)
self.shape_drawing = False
elif self.mode == GraphMode.SELECT:
self.focus_set()
if self.select_box:
x0, y0, x1, y1 = self.coords(self.select_box.id)
inside = [
x
for x in self.find_enclosed(x0, y0, x1, y1)
if "node" in self.gettags(x) or "shape" in self.gettags(x)
]
for i in inside:
self.select_object(i, True)
self.select_box.disappear()
self.select_box = None
else:
if self.mode == GraphMode.ANNOTATION:
self.focus_set()
if self.shape_drawing:
shape = self.shapes[self.selected]
shape.shape_complete(x, y)
self.shape_drawing = False
elif self.mode == GraphMode.SELECT:
self.focus_set()
if self.select_box:
x0, y0, x1, y1 = self.coords(self.select_box.id)
inside = [
x
for x in self.find_enclosed(x0, y0, x1, y1)
if "node" in self.gettags(x) or "shape" in self.gettags(x)
]
for i in inside:
self.select_object(i, True)
self.select_box.disappear()
self.select_box = None
else:
self.focus_set()
self.selected = self.get_selected(event)
logging.debug(
f"click release selected({self.selected}) mode({self.mode})"
)
if self.mode == GraphMode.EDGE:
self.handle_edge_release(event)
elif self.mode == GraphMode.NODE:
self.add_node(x, y)
elif self.mode == GraphMode.PICKNODE:
self.mode = GraphMode.NODE
self.focus_set()
self.selected = self.get_selected(event)
logging.debug(f"click release selected({self.selected}) mode({self.mode})")
if self.mode == GraphMode.EDGE:
self.handle_edge_release(event)
elif self.mode == GraphMode.NODE:
self.add_node(x, y)
elif self.mode == GraphMode.PICKNODE:
self.mode = GraphMode.NODE
self.selected = None
def handle_edge_release(self, event: tk.Event):
def handle_edge_release(self, _event: tk.Event):
edge = self.drawing_edge
self.drawing_edge = None
@ -391,7 +443,7 @@ class CanvasGraph(tk.Canvas):
return
# ignore repeated edges
token = EdgeUtils.get_token(edge.src, self.selected)
token = create_edge_token(edge.src, self.selected)
if token in self.edges:
edge.delete()
return
@ -454,15 +506,13 @@ class CanvasGraph(tk.Canvas):
canvas_node.delete()
nodes.append(canvas_node)
is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type)
# delete related edges
for edge in canvas_node.edges:
if edge in edges:
continue
edges.add(edge)
self.edges.pop(edge.token, None)
del self.edges[edge.token]
edge.delete()
# update node connected to edge being deleted
other_id = edge.src
other_interface = edge.src_interface
@ -471,10 +521,8 @@ class CanvasGraph(tk.Canvas):
other_interface = edge.dst_interface
other_node = self.nodes[other_id]
other_node.edges.remove(edge)
try:
if other_interface in other_node.interfaces:
other_node.interfaces.remove(other_interface)
except ValueError:
pass
if is_wireless:
other_node.delete_antenna()
@ -484,7 +532,27 @@ class CanvasGraph(tk.Canvas):
shape.delete()
self.selection.clear()
self.core.delete_graph_nodes(nodes)
self.core.deleted_graph_nodes(nodes)
self.core.deleted_graph_edges(edges)
def delete_edge(self, edge: CanvasEdge):
edge.delete()
del self.edges[edge.token]
src_node = self.nodes[edge.src]
src_node.edges.discard(edge)
if edge.src_interface in src_node.interfaces:
src_node.interfaces.remove(edge.src_interface)
dst_node = self.nodes[edge.dst]
dst_node.edges.discard(edge)
if edge.dst_interface in dst_node.interfaces:
dst_node.interfaces.remove(edge.dst_interface)
src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type)
if src_wireless:
dst_node.delete_antenna()
dst_wireless = NodeUtils.is_wireless_node(dst_node.core_node.type)
if dst_wireless:
src_node.delete_antenna()
self.core.deleted_graph_edges([edge])
def zoom(self, event: tk.Event, factor: float = None):
if not factor:
@ -520,11 +588,10 @@ class CanvasGraph(tk.Canvas):
logging.debug("click press offset(%s, %s)", x_check, y_check)
is_node = selected in self.nodes
if self.mode == GraphMode.EDGE and is_node:
x, y = self.coords(selected)
self.drawing_edge = CanvasEdge(x, y, x, y, selected, self)
pos = self.coords(selected)
self.drawing_edge = CanvasEdge(self, selected, pos, pos)
if self.mode == GraphMode.ANNOTATION:
if is_marker(self.annotation_type):
r = self.app.toolbar.marker_tool.radius
self.create_oval(
@ -534,7 +601,8 @@ class CanvasGraph(tk.Canvas):
y + r,
fill=self.app.toolbar.marker_tool.color,
outline="",
tags=tags.MARKER,
tags=(tags.MARKER, tags.ANNOTATION),
state=self.show_annotations.state(),
)
return
if selected is None:
@ -603,8 +671,7 @@ class CanvasGraph(tk.Canvas):
self.cursor = x, y
if self.mode == GraphMode.EDGE and self.drawing_edge is not None:
x1, y1, _, _ = self.coords(self.drawing_edge.id)
self.coords(self.drawing_edge.id, x1, y1, x, y)
self.drawing_edge.move_dst(self.cursor)
if self.mode == GraphMode.ANNOTATION:
if is_draw_shape(self.annotation_type) and self.shape_drawing:
shape = self.shapes[self.selected]
@ -618,7 +685,7 @@ class CanvasGraph(tk.Canvas):
y + r,
fill=self.app.toolbar.marker_tool.color,
outline="",
tags="marker",
tags=(tags.MARKER, tags.ANNOTATION),
)
return
@ -639,20 +706,7 @@ class CanvasGraph(tk.Canvas):
if self.select_box and self.mode == GraphMode.SELECT:
self.select_box.shape_motion(x, y)
def click_context(self, event: tk.Event):
logging.info("context: %s", self.context)
if not self.context:
selected = self.get_selected(event)
canvas_node = self.nodes.get(selected)
if canvas_node:
logging.debug("node context: %s", selected)
self.context = canvas_node.create_context()
self.context.bind("<Unmap>", self.hide_context)
self.context.post(event.x_root, event.y_root)
else:
self.hide_context()
def press_delete(self, event: tk.Event):
def press_delete(self, _event: tk.Event):
"""
delete selected nodes and any data that relates to it
"""
@ -666,33 +720,35 @@ class CanvasGraph(tk.Canvas):
selected = self.get_selected(event)
if selected is not None and selected in self.shapes:
shape = self.shapes[selected]
dialog = ShapeDialog(self.app, self.app, shape)
dialog = ShapeDialog(self.app, shape)
dialog.show()
def add_node(self, x: float, y: float) -> CanvasNode:
if self.selected is None or self.selected in self.shapes:
actual_x, actual_y = self.get_actual_coords(x, y)
core_node = self.core.create_node(
actual_x, actual_y, self.node_draw.node_type, self.node_draw.model
def add_node(self, x: float, y: float) -> None:
if self.selected is not None and self.selected not in self.shapes:
return
actual_x, actual_y = self.get_actual_coords(x, y)
core_node = self.core.create_node(
actual_x, actual_y, self.node_draw.node_type, self.node_draw.model
)
if not core_node:
return
try:
self.node_draw.image = Images.get(
self.node_draw.image_enum, int(ICON_SIZE * self.app.app_scale)
)
try:
self.node_draw.image = Images.get(
self.node_draw.image_enum, int(ICON_SIZE * self.app.app_scale)
)
except AttributeError:
self.node_draw.image = Images.get_custom(
self.node_draw.image_file, int(ICON_SIZE * self.app.app_scale)
)
node = CanvasNode(self.master, x, y, core_node, self.node_draw.image)
self.core.canvas_nodes[core_node.id] = node
self.nodes[node.id] = node
return node
except AttributeError:
self.node_draw.image = Images.get_custom(
self.node_draw.image_file, int(ICON_SIZE * self.app.app_scale)
)
node = CanvasNode(self.app, x, y, core_node, self.node_draw.image)
self.core.canvas_nodes[core_node.id] = node
self.nodes[node.id] = node
def width_and_height(self):
"""
retrieve canvas width and height in pixels
"""
x0, y0, x1, y1 = self.coords(self.grid)
x0, y0, x1, y1 = self.coords(self.rect)
canvas_w = abs(x0 - x1)
canvas_h = abs(y0 - y1)
return canvas_w, canvas_h
@ -707,7 +763,7 @@ class CanvasGraph(tk.Canvas):
self, image: ImageTk.PhotoImage, x: float = None, y: float = None
):
if x is None and y is None:
x1, y1, x2, y2 = self.bbox(self.grid)
x1, y1, x2, y2 = self.bbox(self.rect)
x = (x1 + x2) / 2
y = (y1 + y2) / 2
self.wallpaper_id = self.create_image((x, y), image=image, tags=tags.WALLPAPER)
@ -729,7 +785,7 @@ class CanvasGraph(tk.Canvas):
image = ImageTk.PhotoImage(cropped)
# draw on canvas
x1, y1, _, _ = self.bbox(self.grid)
x1, y1, _, _ = self.bbox(self.rect)
x = (cropx / 2) + x1
y = (cropy / 2) + y1
self.draw_wallpaper(image, x, y)
@ -792,7 +848,7 @@ class CanvasGraph(tk.Canvas):
# redraw gridlines to new canvas size
self.delete(tags.GRIDLINE)
self.draw_grid()
self.update_grid()
self.app.canvas.show_grid.click_handler()
def redraw_wallpaper(self):
if self.adjust_to_dim.get():
@ -814,13 +870,6 @@ class CanvasGraph(tk.Canvas):
for component in tags.ABOVE_WALLPAPER_TAGS:
self.tag_raise(component)
def update_grid(self):
logging.debug("updating grid show grid: %s", self.show_grid.get())
if self.show_grid.get():
self.itemconfig(tags.GRIDLINE, state=tk.NORMAL)
else:
self.itemconfig(tags.GRIDLINE, state=tk.HIDDEN)
def set_wallpaper(self, filename: str):
logging.debug("setting wallpaper: %s", filename)
if filename:
@ -841,11 +890,10 @@ class CanvasGraph(tk.Canvas):
"""
create an edge between source node and destination node
"""
if (source.id, dest.id) not in self.edges:
pos0 = source.core_node.position
x0 = pos0.x
y0 = pos0.y
edge = CanvasEdge(x0, y0, x0, y0, source.id, self)
token = create_edge_token(source.id, dest.id)
if token not in self.edges:
pos = (source.core_node.position.x, source.core_node.position.y)
edge = CanvasEdge(self, source.id, pos, pos)
edge.complete(dest.id)
self.edges[edge.token] = edge
self.nodes[source.id].edges.add(edge)
@ -853,60 +901,69 @@ class CanvasGraph(tk.Canvas):
self.core.create_link(edge, source, dest)
def copy(self):
if self.app.core.is_runtime():
if self.core.is_runtime():
logging.info("copy is disabled during runtime state")
return
if self.selection:
logging.debug("to copy %s nodes", len(self.selection))
self.to_copy = self.selection.keys()
logging.info("to copy nodes: %s", self.selection)
self.to_copy.clear()
for node_id in self.selection.keys():
canvas_node = self.nodes[node_id]
self.to_copy.append(canvas_node)
def paste(self):
if self.app.core.is_runtime():
if self.core.is_runtime():
logging.info("paste is disabled during runtime state")
return
# maps original node canvas id to copy node canvas id
copy_map = {}
# the edges that will be copy over
to_copy_edges = []
for canvas_nid in self.to_copy:
core_node = self.nodes[canvas_nid].core_node
for canvas_node in self.to_copy:
core_node = canvas_node.core_node
actual_x = core_node.position.x + 50
actual_y = core_node.position.y + 50
scaled_x, scaled_y = self.get_scaled_coords(actual_x, actual_y)
copy = self.core.create_node(
actual_x, actual_y, core_node.type, core_node.model
)
node = CanvasNode(
self.master, scaled_x, scaled_y, copy, self.nodes[canvas_nid].image
)
if not copy:
continue
node = CanvasNode(self.app, scaled_x, scaled_y, copy, canvas_node.image)
# add new node to modified_service_nodes set if that set contains the to_copy node
if self.app.core.service_been_modified(core_node.id):
self.app.core.modified_service_nodes.add(copy.id)
# copy configurations and services
node.core_node.services[:] = canvas_node.core_node.services
node.core_node.config_services[:] = canvas_node.core_node.config_services
node.emane_model_configs = deepcopy(canvas_node.emane_model_configs)
node.wlan_config = deepcopy(canvas_node.wlan_config)
node.mobility_config = deepcopy(canvas_node.mobility_config)
node.service_configs = deepcopy(canvas_node.service_configs)
node.service_file_configs = deepcopy(canvas_node.service_file_configs)
node.config_service_configs = deepcopy(canvas_node.config_service_configs)
copy_map[canvas_nid] = node.id
copy_map[canvas_node.id] = node.id
self.core.canvas_nodes[copy.id] = node
self.nodes[node.id] = node
self.core.copy_node_config(core_node.id, copy.id)
edges = self.nodes[canvas_nid].edges
for edge in edges:
for edge in canvas_node.edges:
if edge.src not in self.to_copy or edge.dst not in self.to_copy:
if canvas_nid == edge.src:
self.create_edge(node, self.nodes[edge.dst])
elif canvas_nid == edge.dst:
self.create_edge(self.nodes[edge.src], node)
if canvas_node.id == edge.src:
dst_node = self.nodes[edge.dst]
self.create_edge(node, dst_node)
elif canvas_node.id == edge.dst:
src_node = self.nodes[edge.src]
self.create_edge(src_node, node)
else:
to_copy_edges.append(edge)
# copy link and link config
for edge in to_copy_edges:
source_node_copy = self.nodes[copy_map[edge.token[0]]]
dest_node_copy = self.nodes[copy_map[edge.token[1]]]
self.create_edge(source_node_copy, dest_node_copy)
copy_edge = self.edges[
EdgeUtils.get_token(source_node_copy.id, dest_node_copy.id)
]
src_node_id = copy_map[edge.token[0]]
dst_node_id = copy_map[edge.token[1]]
src_node_copy = self.nodes[src_node_id]
dst_node_copy = self.nodes[dst_node_id]
self.create_edge(src_node_copy, dst_node_copy)
token = create_edge_token(src_node_copy.id, dst_node_copy.id)
copy_edge = self.edges[token]
copy_link = copy_edge.link
options = edge.link.options
copy_link.options.CopyFrom(options)
@ -937,6 +994,7 @@ class CanvasGraph(tk.Canvas):
width=self.itemcget(edge.id, "width"),
fill=self.itemcget(edge.id, "fill"),
)
self.tag_raise(tags.NODE)
def scale_graph(self):
for nid, canvas_node in self.nodes.items():
@ -944,10 +1002,10 @@ class CanvasGraph(tk.Canvas):
if NodeUtils.is_custom(
canvas_node.core_node.type, canvas_node.core_node.model
):
for custom_node in self.app.guiconfig["nodes"]:
if custom_node["name"] == canvas_node.core_node.model:
for custom_node in self.app.guiconfig.nodes:
if custom_node.name == canvas_node.core_node.model:
img = Images.get_custom(
custom_node["image"], int(ICON_SIZE * self.app.app_scale)
custom_node.image, int(ICON_SIZE * self.app.app_scale)
)
else:
image_enum = TypeToImage.get(

View file

@ -1,3 +1,4 @@
import functools
import logging
import tkinter as tk
from typing import TYPE_CHECKING
@ -13,8 +14,8 @@ from core.gui.dialogs.nodeconfig import NodeConfigDialog
from core.gui.dialogs.nodeconfigservice import NodeConfigServiceDialog
from core.gui.dialogs.nodeservice import NodeServiceDialog
from core.gui.dialogs.wlanconfig import WlanConfigDialog
from core.gui.errors import show_grpc_error
from core.gui.graph import tags
from core.gui.graph.edges import CanvasEdge
from core.gui.graph.tooltip import CanvasTooltip
from core.gui.images import ImageEnum, Images
from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils
@ -47,9 +48,10 @@ class CanvasNode:
x,
label_y,
text=self.core_node.name,
tags=tags.NODE_NAME,
tags=tags.NODE_LABEL,
font=self.app.icon_text_font,
fill="#0000CD",
state=self.canvas.show_node_labels.state(),
)
self.tooltip = CanvasTooltip(self.canvas)
self.edges = set()
@ -57,12 +59,22 @@ class CanvasNode:
self.wireless_edges = set()
self.antennas = []
self.antenna_images = {}
# possible configurations
self.emane_model_configs = {}
self.wlan_config = {}
self.mobility_config = {}
self.service_configs = {}
self.service_file_configs = {}
self.config_service_configs = {}
self.setup_bindings()
self.context = tk.Menu(self.canvas)
themes.style_menu(self.context)
def setup_bindings(self):
self.canvas.tag_bind(self.id, "<Double-Button-1>", self.double_click)
self.canvas.tag_bind(self.id, "<Enter>", self.on_enter)
self.canvas.tag_bind(self.id, "<Leave>", self.on_leave)
self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.show_context)
def delete(self):
logging.debug("Delete canvas node for %s", self.core_node)
@ -130,7 +142,7 @@ class CanvasNode:
def motion(self, x_offset: int, y_offset: int, update: bool = True):
original_position = self.canvas.coords(self.id)
self.canvas.move(self.id, x_offset, y_offset)
x, y = self.canvas.coords(self.id)
pos = self.canvas.coords(self.id)
# check new position
bbox = self.canvas.bbox(self.id)
@ -148,22 +160,12 @@ class CanvasNode:
# move edges
for edge in self.edges:
x1, y1, x2, y2 = self.canvas.coords(edge.id)
if edge.src == self.id:
self.canvas.coords(edge.id, x, y, x2, y2)
else:
self.canvas.coords(edge.id, x1, y1, x, y)
edge.update_labels()
edge.move_node(self.id, pos)
for edge in self.wireless_edges:
x1, y1, x2, y2 = self.canvas.coords(edge.id)
if edge.src == self.id:
self.canvas.coords(edge.id, x, y, x2, y2)
else:
self.canvas.coords(edge.id, x1, y1, x, y)
edge.move_node(self.id, pos)
# set actual coords for node and update core is running
real_x, real_y = self.canvas.get_actual_coords(x, y)
real_x, real_y = self.canvas.get_actual_coords(*pos)
self.core_node.position.x = real_x
self.core_node.position.y = real_y
if self.app.core.is_runtime() and update:
@ -177,7 +179,7 @@ class CanvasNode:
output = self.app.core.run(self.core_node.id)
self.tooltip.text.set(output)
except grpc.RpcError as e:
show_grpc_error(e, self.app, self.app)
self.app.show_grpc_exception("Observer Error", e)
def on_leave(self, event: tk.Event):
self.tooltip.on_leave(event)
@ -188,59 +190,69 @@ class CanvasNode:
else:
self.show_config()
def create_context(self) -> tk.Menu:
def show_context(self, event: tk.Event) -> None:
# clear existing menu
self.context.delete(0, tk.END)
is_wlan = self.core_node.type == NodeType.WIRELESS_LAN
is_emane = self.core_node.type == NodeType.EMANE
context = tk.Menu(self.canvas)
themes.style_menu(context)
if self.app.core.is_runtime():
context.add_command(label="Configure", command=self.show_config)
if NodeUtils.is_container_node(self.core_node.type):
context.add_command(label="Services", state=tk.DISABLED)
context.add_command(label="Config Services", state=tk.DISABLED)
self.context.add_command(label="Configure", command=self.show_config)
if is_wlan:
context.add_command(label="WLAN Config", command=self.show_wlan_config)
self.context.add_command(
label="WLAN Config", command=self.show_wlan_config
)
if is_wlan and self.core_node.id in self.app.core.mobility_players:
context.add_command(
self.context.add_command(
label="Mobility Player", command=self.show_mobility_player
)
context.add_command(label="Select Adjacent", state=tk.DISABLED)
context.add_command(label="Hide", state=tk.DISABLED)
if NodeUtils.is_container_node(self.core_node.type):
context.add_command(label="Shell Window", state=tk.DISABLED)
context.add_command(label="Tcpdump", state=tk.DISABLED)
context.add_command(label="Tshark", state=tk.DISABLED)
context.add_command(label="Wireshark", state=tk.DISABLED)
context.add_command(label="View Log", state=tk.DISABLED)
else:
context.add_command(label="Configure", command=self.show_config)
self.context.add_command(label="Configure", command=self.show_config)
if NodeUtils.is_container_node(self.core_node.type):
context.add_command(label="Services", command=self.show_services)
context.add_command(
self.context.add_command(label="Services", command=self.show_services)
self.context.add_command(
label="Config Services", command=self.show_config_services
)
if is_emane:
context.add_command(
self.context.add_command(
label="EMANE Config", command=self.show_emane_config
)
if is_wlan:
context.add_command(label="WLAN Config", command=self.show_wlan_config)
context.add_command(
self.context.add_command(
label="WLAN Config", command=self.show_wlan_config
)
self.context.add_command(
label="Mobility Config", command=self.show_mobility_config
)
if NodeUtils.is_wireless_node(self.core_node.type):
context.add_command(
self.context.add_command(
label="Link To Selected", command=self.wireless_link_selected
)
context.add_command(label="Select Members", state=tk.DISABLED)
edit_menu = tk.Menu(context)
unlink_menu = tk.Menu(self.context)
for edge in self.edges:
other_id = edge.src
if self.id == other_id:
other_id = edge.dst
other_node = self.canvas.nodes[other_id]
func_unlink = functools.partial(self.click_unlink, edge)
unlink_menu.add_command(
label=other_node.core_node.name, command=func_unlink
)
themes.style_menu(unlink_menu)
self.context.add_cascade(label="Unlink", menu=unlink_menu)
edit_menu = tk.Menu(self.context)
themes.style_menu(edit_menu)
edit_menu.add_command(label="Cut", state=tk.DISABLED)
edit_menu.add_command(label="Cut", command=self.click_cut)
edit_menu.add_command(label="Copy", command=self.canvas_copy)
edit_menu.add_command(label="Delete", command=self.canvas_delete)
edit_menu.add_command(label="Hide", state=tk.DISABLED)
context.add_cascade(label="Edit", menu=edit_menu)
return context
self.context.add_cascade(label="Edit", menu=edit_menu)
self.context.tk_popup(event.x_root, event.y_root)
def click_cut(self) -> None:
self.canvas_copy()
self.canvas_delete()
def click_unlink(self, edge: CanvasEdge) -> None:
self.canvas.delete_edge(edge)
def canvas_delete(self) -> None:
self.canvas.clear_selection()
@ -253,40 +265,33 @@ class CanvasNode:
self.canvas.copy()
def show_config(self):
self.canvas.context = None
dialog = NodeConfigDialog(self.app, self.app, self)
dialog = NodeConfigDialog(self.app, self)
dialog.show()
def show_wlan_config(self):
self.canvas.context = None
dialog = WlanConfigDialog(self.app, self.app, self)
dialog = WlanConfigDialog(self.app, self)
if not dialog.has_error:
dialog.show()
def show_mobility_config(self):
self.canvas.context = None
dialog = MobilityConfigDialog(self.app, self.app, self)
dialog = MobilityConfigDialog(self.app, self)
if not dialog.has_error:
dialog.show()
def show_mobility_player(self):
self.canvas.context = None
mobility_player = self.app.core.mobility_players[self.core_node.id]
mobility_player.show()
def show_emane_config(self):
self.canvas.context = None
dialog = EmaneConfigDialog(self.app, self.app, self)
dialog = EmaneConfigDialog(self.app, self)
dialog.show()
def show_services(self):
self.canvas.context = None
dialog = NodeServiceDialog(self.app.master, self.app, self)
dialog = NodeServiceDialog(self.app, self)
dialog.show()
def show_config_services(self):
self.canvas.context = None
dialog = NodeConfigServiceDialog(self.app.master, self.app, self)
dialog = NodeConfigServiceDialog(self.app, self)
dialog.show()
def has_emane_link(self, interface_id: int) -> core_pb2.Node:
@ -307,13 +312,10 @@ class CanvasNode:
return result
def wireless_link_selected(self):
self.canvas.context = None
for canvas_nid in [
x for x in self.canvas.selection if "node" in self.canvas.gettags(x)
]:
core_node = self.canvas.nodes[canvas_nid].core_node
if core_node.type == core_pb2.NodeType.DEFAULT and core_node.model == "mdr":
self.canvas.create_edge(self, self.canvas.nodes[canvas_nid])
nodes = [x for x in self.canvas.selection if x in self.canvas.nodes]
for node_id in nodes:
canvas_node = self.canvas.nodes[node_id]
self.canvas.create_edge(self, canvas_node)
self.canvas.clear_selection()
def scale_antennas(self):

View file

@ -80,11 +80,12 @@ class Shape:
self.y1,
self.x2,
self.y2,
tags=tags.SHAPE,
tags=(tags.SHAPE, tags.ANNOTATION),
dash=dash,
fill=self.shape_data.fill_color,
outline=self.shape_data.border_color,
width=self.shape_data.border_width,
state=self.canvas.show_annotations.state(),
)
self.draw_shape_text()
elif self.shape_type == ShapeType.RECTANGLE:
@ -93,11 +94,12 @@ class Shape:
self.y1,
self.x2,
self.y2,
tags=tags.SHAPE,
tags=(tags.SHAPE, tags.ANNOTATION),
dash=dash,
fill=self.shape_data.fill_color,
outline=self.shape_data.border_color,
width=self.shape_data.border_width,
state=self.canvas.show_annotations.state(),
)
self.draw_shape_text()
elif self.shape_type == ShapeType.TEXT:
@ -105,10 +107,11 @@ class Shape:
self.id = self.canvas.create_text(
self.x1,
self.y1,
tags=tags.SHAPE_TEXT,
tags=(tags.SHAPE_TEXT, tags.ANNOTATION),
text=self.shape_data.text,
fill=self.shape_data.text_color,
font=font,
state=self.canvas.show_annotations.state(),
)
else:
logging.error("unknown shape type: %s", self.shape_type)
@ -132,10 +135,11 @@ class Shape:
self.text_id = self.canvas.create_text(
x,
y,
tags=tags.SHAPE_TEXT,
tags=(tags.SHAPE_TEXT, tags.ANNOTATION),
text=self.shape_data.text,
fill=self.shape_data.text_color,
font=font,
state=self.canvas.show_annotations.state(),
)
def shape_motion(self, x1: float, y1: float):
@ -144,7 +148,7 @@ class Shape:
def shape_complete(self, x: float, y: float):
for component in tags.ABOVE_SHAPE:
self.canvas.tag_raise(component)
s = ShapeDialog(self.app, self.app, self)
s = ShapeDialog(self.app, self)
s.show()
def disappear(self):

View file

@ -1,34 +1,34 @@
ANNOTATION = "annotation"
GRIDLINE = "gridline"
SHAPE = "shape"
SHAPE_TEXT = "shapetext"
EDGE = "edge"
LINK_INFO = "linkinfo"
LINK_LABEL = "linklabel"
WIRELESS_EDGE = "wireless"
ANTENNA = "antenna"
NODE_NAME = "nodename"
NODE_LABEL = "nodename"
NODE = "node"
WALLPAPER = "wallpaper"
SELECTION = "selectednodes"
THROUGHPUT = "throughput"
MARKER = "marker"
ABOVE_WALLPAPER_TAGS = [
GRIDLINE,
SHAPE,
SHAPE_TEXT,
EDGE,
LINK_INFO,
LINK_LABEL,
WIRELESS_EDGE,
ANTENNA,
NODE,
NODE_NAME,
NODE_LABEL,
]
ABOVE_SHAPE = [GRIDLINE, EDGE, LINK_INFO, WIRELESS_EDGE, ANTENNA, NODE, NODE_NAME]
ABOVE_SHAPE = [GRIDLINE, EDGE, LINK_LABEL, WIRELESS_EDGE, ANTENNA, NODE, NODE_LABEL]
COMPONENT_TAGS = [
EDGE,
NODE,
NODE_NAME,
NODE_LABEL,
WALLPAPER,
LINK_INFO,
LINK_LABEL,
ANTENNA,
WIRELESS_EDGE,
SELECTION,

View file

@ -47,7 +47,8 @@ class Images:
except KeyError:
messagebox.showwarning(
"Missing image file",
f"{name}.png is missing at daemon/core/gui/data/icons, drop image file at daemon/core/gui/data/icons and restart the gui",
f"{name}.png is missing at daemon/core/gui/data/icons, drop image "
f"file at daemon/core/gui/data/icons and restart the gui",
)

View file

@ -1,8 +1,8 @@
import logging
import random
from typing import TYPE_CHECKING, Set, Union
from typing import TYPE_CHECKING, Any, List, Optional, Set, Tuple
from netaddr import IPNetwork
import netaddr
from netaddr import EUI, IPNetwork
from core.gui.nodeutils import NodeUtils
@ -12,77 +12,154 @@ if TYPE_CHECKING:
from core.gui.graph.node import CanvasNode
def random_mac():
return ("{:02x}" * 6).format(*[random.randrange(256) for _ in range(6)])
def get_index(interface: "core_pb2.Interface") -> int:
net = netaddr.IPNetwork(f"{interface.ip4}/{interface.ip4mask}")
ip_value = net.value
cidr_value = net.cidr.value
return ip_value - cidr_value
class Subnets:
def __init__(self, ip4: IPNetwork, ip6: IPNetwork) -> None:
self.ip4 = ip4
self.ip6 = ip6
self.used_indexes = set()
def __eq__(self, other: "Subnets") -> bool:
return (self.ip4, self.ip6) == (other.ip4, other.ip6)
def __eq__(self, other: Any) -> bool:
if not isinstance(other, Subnets):
return False
return self.key() == other.key()
def __hash__(self) -> int:
return hash((self.ip4, self.ip6))
return hash(self.key())
def key(self) -> Tuple[IPNetwork, IPNetwork]:
return self.ip4, self.ip6
def next(self) -> "Subnets":
return Subnets(self.ip4.next(), self.ip6.next())
class InterfaceManager:
def __init__(
self,
app: "Application",
ip4: str = "10.0.0.0",
ip4_mask: int = 24,
ip6: str = "2001::",
ip6_mask=64,
) -> None:
def __init__(self, app: "Application") -> None:
self.app = app
self.ip4_mask = ip4_mask
self.ip6_mask = ip6_mask
self.ip4_subnets = IPNetwork(f"{ip4}/{ip4_mask}")
self.ip6_subnets = IPNetwork(f"{ip6}/{ip6_mask}")
ip4 = self.app.guiconfig.ips.ip4
ip6 = self.app.guiconfig.ips.ip6
self.ip4_mask = 24
self.ip6_mask = 64
self.ip4_subnets = IPNetwork(f"{ip4}/{self.ip4_mask}")
self.ip6_subnets = IPNetwork(f"{ip6}/{self.ip6_mask}")
mac = self.app.guiconfig.mac
self.mac = EUI(mac, dialect=netaddr.mac_unix_expanded)
self.current_mac = None
self.current_subnets = None
self.used_subnets = {}
def update_ips(self, ip4: str, ip6: str) -> None:
self.reset()
self.ip4_subnets = IPNetwork(f"{ip4}/{self.ip4_mask}")
self.ip6_subnets = IPNetwork(f"{ip6}/{self.ip6_mask}")
def reset_mac(self) -> None:
self.current_mac = self.mac
def next_mac(self) -> str:
mac = str(self.current_mac)
value = self.current_mac.value + 1
self.current_mac = EUI(value, dialect=netaddr.mac_unix_expanded)
return mac
def next_subnets(self) -> Subnets:
# define currently used subnets
used_subnets = set()
for edge in self.app.core.links.values():
link = edge.link
subnets = None
if link.HasField("interface_one"):
subnets = self.get_subnets(link.interface_one)
if link.HasField("interface_two"):
subnets = self.get_subnets(link.interface_two)
if subnets:
used_subnets.add(subnets)
# find next available subnets
subnets = Subnets(self.ip4_subnets, self.ip6_subnets)
while subnets in used_subnets:
subnets = self.current_subnets
if subnets is None:
subnets = Subnets(self.ip4_subnets, self.ip6_subnets)
while subnets.key() in self.used_subnets:
subnets = subnets.next()
self.used_subnets[subnets.key()] = subnets
return subnets
def reset(self):
def reset(self) -> None:
self.current_subnets = None
self.used_subnets.clear()
def get_ips(self, node_id: int) -> [str, str]:
ip4 = self.current_subnets.ip4[node_id]
ip6 = self.current_subnets.ip6[node_id]
def removed(self, links: List["core_pb2.Link"]) -> None:
# get remaining subnets
remaining_subnets = set()
for edge in self.app.core.links.values():
link = edge.link
if link.HasField("interface_one"):
subnets = self.get_subnets(link.interface_one)
remaining_subnets.add(subnets)
if link.HasField("interface_two"):
subnets = self.get_subnets(link.interface_two)
remaining_subnets.add(subnets)
# remove all subnets from used subnets when no longer present
# or remove used indexes from subnet
interfaces = []
for link in links:
if link.HasField("interface_one"):
interfaces.append(link.interface_one)
if link.HasField("interface_two"):
interfaces.append(link.interface_two)
for interface in interfaces:
subnets = self.get_subnets(interface)
if subnets not in remaining_subnets:
if self.current_subnets == subnets:
self.current_subnets = None
self.used_subnets.pop(subnets.key(), None)
else:
index = get_index(interface)
subnets.used_indexes.discard(index)
def joined(self, links: List["core_pb2.Link"]) -> None:
interfaces = []
for link in links:
if link.HasField("interface_one"):
interfaces.append(link.interface_one)
if link.HasField("interface_two"):
interfaces.append(link.interface_two)
# add to used subnets and mark used indexes
for interface in interfaces:
subnets = self.get_subnets(interface)
index = get_index(interface)
subnets.used_indexes.add(index)
if subnets.key() not in self.used_subnets:
self.used_subnets[subnets.key()] = subnets
def next_index(self, node: "core_pb2.Node") -> int:
if NodeUtils.is_router_node(node):
index = 1
else:
index = 20
while True:
if index not in self.current_subnets.used_indexes:
self.current_subnets.used_indexes.add(index)
break
index += 1
return index
def get_ips(self, node: "core_pb2.Node") -> [str, str]:
index = self.next_index(node)
ip4 = self.current_subnets.ip4[index]
ip6 = self.current_subnets.ip6[index]
return str(ip4), str(ip6)
@classmethod
def get_subnets(cls, interface: "core_pb2.Interface") -> Subnets:
ip4_subnet = IPNetwork(f"{interface.ip4}/{interface.ip4mask}").cidr
ip6_subnet = IPNetwork(f"{interface.ip6}/{interface.ip6mask}").cidr
return Subnets(ip4_subnet, ip6_subnet)
def get_subnets(self, interface: "core_pb2.Interface") -> Subnets:
logging.info("get subnets for interface: %s", interface)
ip4_subnet = self.ip4_subnets
if interface.ip4:
ip4_subnet = IPNetwork(f"{interface.ip4}/{interface.ip4mask}").cidr
ip6_subnet = self.ip6_subnets
if interface.ip6:
ip6_subnet = IPNetwork(f"{interface.ip6}/{interface.ip6mask}").cidr
subnets = Subnets(ip4_subnet, ip6_subnet)
return self.used_subnets.get(subnets.key(), subnets)
def determine_subnets(
self, canvas_src_node: "CanvasNode", canvas_dst_node: "CanvasNode"
):
) -> None:
src_node = canvas_src_node.core_node
dst_node = canvas_dst_node.core_node
is_src_container = NodeUtils.is_container_node(src_node.type)
@ -106,7 +183,7 @@ class InterfaceManager:
def find_subnets(
self, canvas_node: "CanvasNode", visited: Set[int] = None
) -> Union[IPNetwork, None]:
) -> Optional[IPNetwork]:
logging.info("finding subnet for node: %s", canvas_node.core_node.name)
canvas = self.app.canvas
subnets = None

View file

@ -1,203 +0,0 @@
"""
The actions taken when each menubar option is clicked
"""
import logging
import os
import tkinter as tk
import webbrowser
from tkinter import filedialog, messagebox
from typing import TYPE_CHECKING
import grpc
from core.gui.appconfig import XMLS_PATH
from core.gui.dialogs.about import AboutDialog
from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog
from core.gui.dialogs.canvaswallpaper import CanvasWallpaperDialog
from core.gui.dialogs.hooks import HooksDialog
from core.gui.dialogs.observers import ObserverDialog
from core.gui.dialogs.preferences import PreferencesDialog
from core.gui.dialogs.servers import ServersDialog
from core.gui.dialogs.sessionoptions import SessionOptionsDialog
from core.gui.dialogs.sessions import SessionsDialog
from core.gui.dialogs.throughput import ThroughputDialog
from core.gui.task import BackgroundTask
MAX_FILES = 3
if TYPE_CHECKING:
from core.gui.app import Application
class MenuAction:
def __init__(self, app: "Application", master: tk.Tk):
self.master = master
self.app = app
self.canvas = app.canvas
def cleanup_old_session(self, session_id: int):
try:
res = self.app.core.client.get_session(session_id)
logging.debug("retrieve session(%s), %s", session_id, res)
stop_response = self.app.core.stop_session()
logging.debug("stop session(%s), result: %s", session_id, stop_response)
delete_response = self.app.core.delete_session(session_id)
logging.debug(
"deleted session(%s), result: %s", session_id, delete_response
)
except grpc.RpcError:
logging.debug("session is not alive")
def prompt_save_running_session(self, quitapp: bool = False):
"""
Prompt use to stop running session before application is closed
"""
result = True
if self.app.core.is_runtime():
result = messagebox.askyesnocancel("Exit", "Stop the running session?")
if result:
callback = None
if quitapp:
callback = self.app.quit
task = BackgroundTask(
self.app,
self.cleanup_old_session,
callback,
(self.app.core.session_id,),
)
task.start()
elif quitapp:
self.app.quit()
def on_quit(self, event: tk.Event = None):
"""
Prompt user whether so save running session, and then close the application
"""
self.prompt_save_running_session(quitapp=True)
def file_save_as_xml(self, event: tk.Event = None):
init_dir = self.app.core.xml_dir
if not init_dir:
init_dir = str(XMLS_PATH)
file_path = filedialog.asksaveasfilename(
initialdir=init_dir,
title="Save As",
filetypes=(("EmulationScript XML files", "*.xml"), ("All files", "*")),
defaultextension=".xml",
)
if file_path:
self.add_recent_file_to_gui_config(file_path)
self.app.core.save_xml(file_path)
self.app.core.xml_file = file_path
def file_open_xml(self, event: tk.Event = None):
init_dir = self.app.core.xml_dir
if not init_dir:
init_dir = str(XMLS_PATH)
file_path = filedialog.askopenfilename(
initialdir=init_dir,
title="Open",
filetypes=(("XML Files", "*.xml"), ("All Files", "*")),
)
self.open_xml_task(file_path)
def open_xml_task(self, filename):
if filename:
self.add_recent_file_to_gui_config(filename)
self.app.core.xml_file = filename
self.app.core.xml_dir = str(os.path.dirname(filename))
self.prompt_save_running_session()
self.app.statusbar.progress_bar.start(5)
task = BackgroundTask(self.app, self.app.core.open_xml, args=(filename,))
task.start()
def gui_preferences(self):
dialog = PreferencesDialog(self.app, self.app)
dialog.show()
def canvas_size_and_scale(self):
dialog = SizeAndScaleDialog(self.app, self.app)
dialog.show()
def canvas_set_wallpaper(self):
dialog = CanvasWallpaperDialog(self.app, self.app)
dialog.show()
def help_core_github(self):
webbrowser.open_new("https://github.com/coreemu/core")
def help_core_documentation(self):
webbrowser.open_new("http://coreemu.github.io/core/")
def session_options(self):
logging.debug("Click options")
dialog = SessionOptionsDialog(self.app, self.app)
if not dialog.has_error:
dialog.show()
def session_change_sessions(self):
logging.debug("Click change sessions")
dialog = SessionsDialog(self.app, self.app)
dialog.show()
def session_hooks(self):
logging.debug("Click hooks")
dialog = HooksDialog(self.app, self.app)
dialog.show()
def session_servers(self):
logging.debug("Click emulation servers")
dialog = ServersDialog(self.app, self.app)
dialog.show()
def edit_observer_widgets(self) -> None:
dialog = ObserverDialog(self.app, self.app)
dialog.show()
def show_about(self) -> None:
dialog = AboutDialog(self.app, self.app)
dialog.show()
def throughput(self) -> None:
if not self.app.core.handling_throughputs:
self.app.core.enable_throughputs()
else:
self.app.core.cancel_throughputs()
def copy(self, event: tk.Event = None) -> None:
self.app.canvas.copy()
def paste(self, event: tk.Event = None) -> None:
self.app.canvas.paste()
def delete(self, event: tk.Event = None) -> None:
self.app.canvas.delete_selected_objects()
def config_throughput(self) -> None:
dialog = ThroughputDialog(self.app, self.app)
dialog.show()
def add_recent_file_to_gui_config(self, file_path) -> None:
recent_files = self.app.guiconfig["recentfiles"]
num_files = len(recent_files)
if num_files == 0:
recent_files.insert(0, file_path)
elif 0 < num_files <= MAX_FILES:
if file_path in recent_files:
recent_files.remove(file_path)
recent_files.insert(0, file_path)
else:
if num_files == MAX_FILES:
recent_files.pop()
recent_files.insert(0, file_path)
else:
logging.error("unexpected number of recent files")
self.app.save_config()
self.app.menubar.update_recent_files()
def new_session(self):
self.prompt_save_running_session()
self.app.core.create_new_session()
self.app.core.xml_file = None

View file

@ -1,35 +1,69 @@
import logging
import os
import tkinter as tk
import webbrowser
from functools import partial
from tkinter import filedialog, messagebox
from typing import TYPE_CHECKING
import core.gui.menuaction as action
from core.gui.coreclient import OBSERVERS
from core.gui.appconfig import XMLS_PATH
from core.gui.dialogs.about import AboutDialog
from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog
from core.gui.dialogs.canvaswallpaper import CanvasWallpaperDialog
from core.gui.dialogs.customnodes import CustomNodesDialog
from core.gui.dialogs.executepython import ExecutePythonDialog
from core.gui.dialogs.find import FindDialog
from core.gui.dialogs.hooks import HooksDialog
from core.gui.dialogs.ipdialog import IpConfigDialog
from core.gui.dialogs.macdialog import MacConfigDialog
from core.gui.dialogs.observers import ObserverDialog
from core.gui.dialogs.preferences import PreferencesDialog
from core.gui.dialogs.servers import ServersDialog
from core.gui.dialogs.sessionoptions import SessionOptionsDialog
from core.gui.dialogs.sessions import SessionsDialog
from core.gui.dialogs.throughput import ThroughputDialog
from core.gui.nodeutils import ICON_SIZE
from core.gui.task import ProgressTask
if TYPE_CHECKING:
from core.gui.app import Application
MAX_FILES = 3
OBSERVERS = {
"List Processes": "ps",
"Show Interfaces": "ip address",
"IPV4 Routes": "ip -4 route",
"IPV6 Routes": "ip -6 route",
"Listening Sockets": "ss -tuwnl",
"IPv4 MFC Entries": "ip -4 mroute show",
"IPv6 MFC Entries": "ip -6 mroute show",
"Firewall Rules": "iptables -L",
"IPSec Policies": "setkey -DP",
}
class Menubar(tk.Menu):
"""
Core menubar
"""
def __init__(self, master: tk.Tk, app: "Application", cnf={}, **kwargs):
def __init__(self, master: tk.Tk, app: "Application", **kwargs) -> None:
"""
Create a CoreMenubar instance
"""
super().__init__(master, cnf, **kwargs)
super().__init__(master, **kwargs)
self.master.config(menu=self)
self.app = app
self.menuaction = action.MenuAction(app, master)
self.core = app.core
self.canvas = app.canvas
self.recent_menu = None
self.edit_menu = None
self.observers_menu = None
self.observers_var = tk.StringVar(value=tk.NONE)
self.observers_custom_index = None
self.draw()
def draw(self):
def draw(self) -> None:
"""
Create core menubar and bind the hot keys to their matching command
"""
@ -42,424 +76,429 @@ class Menubar(tk.Menu):
self.draw_session_menu()
self.draw_help_menu()
def draw_file_menu(self):
def draw_file_menu(self) -> None:
"""
Create file menu
"""
menu = tk.Menu(self)
menu.add_command(
label="New Session",
accelerator="Ctrl+N",
command=self.menuaction.new_session,
label="New Session", accelerator="Ctrl+N", command=self.click_new
)
self.app.bind_all("<Control-n>", lambda e: self.app.core.create_new_session())
self.app.bind_all("<Control-n>", lambda e: self.click_new())
menu.add_command(label="Save", accelerator="Ctrl+S", command=self.click_save)
self.app.bind_all("<Control-s>", self.click_save)
menu.add_command(label="Save As...", command=self.click_save_xml)
menu.add_command(
label="Open...", command=self.menuaction.file_open_xml, accelerator="Ctrl+O"
label="Open...", command=self.click_open_xml, accelerator="Ctrl+O"
)
self.app.bind_all("<Control-o>", self.menuaction.file_open_xml)
menu.add_command(label="Save", accelerator="Ctrl+S", command=self.save)
menu.add_command(label="Save As", command=self.menuaction.file_save_as_xml)
menu.add_command(label="Reload", underline=0, state=tk.DISABLED)
self.app.bind_all("<Control-s>", self.save)
self.app.bind_all("<Control-o>", self.click_open_xml)
self.recent_menu = tk.Menu(menu)
for i in self.app.guiconfig["recentfiles"]:
for i in self.app.guiconfig.recentfiles:
self.recent_menu.add_command(
label=i, command=partial(self.open_recent_files, i)
)
menu.add_cascade(label="Recent files", menu=self.recent_menu)
menu.add_cascade(label="Recent Files", menu=self.recent_menu)
menu.add_separator()
menu.add_command(label="Export Python script...", state=tk.DISABLED)
menu.add_command(label="Execute Python script...", command=self.execute_python)
menu.add_command(
label="Execute Python script with options...", state=tk.DISABLED
)
menu.add_separator()
menu.add_command(label="Open current file in editor...", state=tk.DISABLED)
menu.add_command(label="Print...", underline=0, state=tk.DISABLED)
menu.add_command(label="Save screenshot...", state=tk.DISABLED)
menu.add_command(label="Execute Python Script...", command=self.execute_python)
menu.add_separator()
menu.add_command(
label="Quit", accelerator="Ctrl+Q", command=self.menuaction.on_quit
label="Quit",
accelerator="Ctrl+Q",
command=lambda: self.prompt_save_running_session(True),
)
self.app.bind_all(
"<Control-q>", lambda _: self.prompt_save_running_session(True)
)
self.app.bind_all("<Control-q>", self.menuaction.on_quit)
self.add_cascade(label="File", menu=menu)
def draw_edit_menu(self):
def draw_edit_menu(self) -> None:
"""
Create edit menu
"""
menu = tk.Menu(self)
menu.add_command(label="Preferences", command=self.menuaction.gui_preferences)
menu.add_command(label="Preferences", command=self.click_preferences)
menu.add_command(label="Custom Nodes", command=self.click_custom_nodes)
menu.add_separator()
menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED)
menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED)
menu.add_separator()
menu.add_command(label="Cut", accelerator="Ctrl+X", state=tk.DISABLED)
menu.add_command(label="Cut", accelerator="Ctrl+X", command=self.click_cut)
menu.add_command(label="Copy", accelerator="Ctrl+C", command=self.click_copy)
menu.add_command(label="Paste", accelerator="Ctrl+V", command=self.click_paste)
menu.add_command(
label="Copy", accelerator="Ctrl+C", command=self.menuaction.copy
label="Delete", accelerator="Ctrl+D", command=self.click_delete
)
menu.add_command(
label="Paste", accelerator="Ctrl+V", command=self.menuaction.paste
)
menu.add_command(
label="Delete", accelerator="Ctrl+D", command=self.menuaction.delete
)
menu.add_separator()
menu.add_command(label="Select all", accelerator="Ctrl+A", state=tk.DISABLED)
menu.add_command(
label="Select Adjacent", accelerator="Ctrl+J", state=tk.DISABLED
)
menu.add_separator()
menu.add_command(label="Find...", accelerator="Ctrl+F", state=tk.DISABLED)
menu.add_command(label="Clear marker", state=tk.DISABLED)
self.add_cascade(label="Edit", menu=menu)
self.app.master.bind_all("<Control-c>", self.menuaction.copy)
self.app.master.bind_all("<Control-v>", self.menuaction.paste)
self.app.master.bind_all("<Control-d>", self.menuaction.delete)
self.app.master.bind_all("<Control-x>", self.click_cut)
self.app.master.bind_all("<Control-c>", self.click_copy)
self.app.master.bind_all("<Control-v>", self.click_paste)
self.app.master.bind_all("<Control-d>", self.click_delete)
self.edit_menu = menu
def draw_canvas_menu(self):
def draw_canvas_menu(self) -> None:
"""
Create canvas menu
"""
menu = tk.Menu(self)
menu.add_command(
label="Size/scale...", command=self.menuaction.canvas_size_and_scale
)
menu.add_command(
label="Wallpaper...", command=self.menuaction.canvas_set_wallpaper
)
menu.add_separator()
menu.add_command(label="New", state=tk.DISABLED)
menu.add_command(label="Manage...", state=tk.DISABLED)
menu.add_command(label="Delete", state=tk.DISABLED)
menu.add_separator()
menu.add_command(label="Previous", accelerator="PgUp", state=tk.DISABLED)
menu.add_command(label="Next", accelerator="PgDown", state=tk.DISABLED)
menu.add_command(label="First", accelerator="Home", state=tk.DISABLED)
menu.add_command(label="Last", accelerator="End", state=tk.DISABLED)
menu.add_command(label="Size / Scale", command=self.click_canvas_size_and_scale)
menu.add_command(label="Wallpaper", command=self.click_canvas_wallpaper)
self.add_cascade(label="Canvas", menu=menu)
def draw_view_menu(self):
def draw_view_menu(self) -> None:
"""
Create view menu
"""
view_menu = tk.Menu(self)
self.create_show_menu(view_menu)
view_menu.add_command(label="Show hidden nodes", state=tk.DISABLED)
view_menu.add_command(label="Locked", state=tk.DISABLED)
view_menu.add_command(label="3D GUI...", state=tk.DISABLED)
view_menu.add_separator()
view_menu.add_command(label="Zoom in", accelerator="+", state=tk.DISABLED)
view_menu.add_command(label="Zoom out", accelerator="-", state=tk.DISABLED)
self.add_cascade(label="View", menu=view_menu)
menu = tk.Menu(self)
menu.add_checkbutton(
label="Interface Names",
command=self.click_edge_label_change,
variable=self.canvas.show_interface_names,
)
menu.add_checkbutton(
label="IPv4 Addresses",
command=self.click_edge_label_change,
variable=self.canvas.show_ip4s,
)
menu.add_checkbutton(
label="IPv6 Addresses",
command=self.click_edge_label_change,
variable=self.canvas.show_ip6s,
)
menu.add_checkbutton(
label="Node Labels",
command=self.canvas.show_node_labels.click_handler,
variable=self.canvas.show_node_labels,
)
menu.add_checkbutton(
label="Link Labels",
command=self.canvas.show_link_labels.click_handler,
variable=self.canvas.show_link_labels,
)
menu.add_checkbutton(
label="Annotations",
command=self.canvas.show_annotations.click_handler,
variable=self.canvas.show_annotations,
)
menu.add_checkbutton(
label="Canvas Grid",
command=self.canvas.show_grid.click_handler,
variable=self.canvas.show_grid,
)
self.add_cascade(label="View", menu=menu)
def create_show_menu(self, view_menu: tk.Menu):
"""
Create the menu items in View/Show
"""
menu = tk.Menu(view_menu)
menu.add_command(label="All", state=tk.DISABLED)
menu.add_command(label="None", state=tk.DISABLED)
menu.add_separator()
menu.add_command(label="Interface Names", state=tk.DISABLED)
menu.add_command(label="IPv4 Addresses", state=tk.DISABLED)
menu.add_command(label="IPv6 Addresses", state=tk.DISABLED)
menu.add_command(label="Node Labels", state=tk.DISABLED)
menu.add_command(label="Annotations", state=tk.DISABLED)
menu.add_command(label="Grid", state=tk.DISABLED)
menu.add_command(label="API Messages", state=tk.DISABLED)
view_menu.add_cascade(label="Show", menu=menu)
def create_experimental_menu(self, tools_menu: tk.Menu):
"""
Create experimental menu item and the sub menu items inside
"""
menu = tk.Menu(tools_menu)
menu.add_command(label="Plugins...", state=tk.DISABLED)
menu.add_command(label="ns2immunes converter...", state=tk.DISABLED)
menu.add_command(label="Topology partitioning...", state=tk.DISABLED)
tools_menu.add_cascade(label="Experimental", menu=menu)
def create_random_menu(self, topology_generator_menu: tk.Menu):
"""
Create random menu item and the sub menu items inside
"""
menu = tk.Menu(topology_generator_menu)
# list of number of random nodes to create
nums = [1, 5, 10, 15, 20, 30, 40, 50, 75, 100]
for i in nums:
label = f"R({i})"
menu.add_command(label=label, state=tk.DISABLED)
topology_generator_menu.add_cascade(label="Random", menu=menu)
def create_grid_menu(self, topology_generator_menu: tk.Menu):
"""
Create grid menu item and the sub menu items inside
"""
menu = tk.Menu(topology_generator_menu)
# list of number of nodes to create
nums = [1, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 100]
for i in nums:
label = f"G({i})"
menu.add_command(label=label, state=tk.DISABLED)
topology_generator_menu.add_cascade(label="Grid", menu=menu)
def create_connected_grid_menu(self, topology_generator_menu: tk.Menu):
"""
Create connected grid menu items and the sub menu items inside
"""
menu = tk.Menu(topology_generator_menu)
for i in range(1, 11, 1):
submenu = tk.Menu(menu)
for j in range(1, 11, 1):
label = f"{i} X {j}"
submenu.add_command(label=label, state=tk.DISABLED)
label = str(i) + " X N"
menu.add_cascade(label=label, menu=submenu)
topology_generator_menu.add_cascade(label="Connected Grid", menu=menu)
def create_chain_menu(self, topology_generator_menu: tk.Menu):
"""
Create chain menu item and the sub menu items inside
"""
menu = tk.Menu(topology_generator_menu)
# number of nodes to create
nums = list(range(2, 25, 1)) + [32, 64, 128]
for i in nums:
label = f"P({i})"
menu.add_command(label=label, state=tk.DISABLED)
topology_generator_menu.add_cascade(label="Chain", menu=menu)
def create_star_menu(self, topology_generator_menu: tk.Menu):
"""
Create star menu item and the sub menu items inside
"""
menu = tk.Menu(topology_generator_menu)
for i in range(3, 26, 1):
label = f"C({i})"
menu.add_command(label=label, state=tk.DISABLED)
topology_generator_menu.add_cascade(label="Star", menu=menu)
def create_cycle_menu(self, topology_generator_menu: tk.Menu):
"""
Create cycle menu item and the sub items inside
"""
menu = tk.Menu(topology_generator_menu)
for i in range(3, 25, 1):
label = f"C({i})"
menu.add_command(label=label, state=tk.DISABLED)
topology_generator_menu.add_cascade(label="Cycle", menu=menu)
def create_wheel_menu(self, topology_generator_menu: tk.Menu):
"""
Create wheel menu item and the sub menu items inside
"""
menu = tk.Menu(topology_generator_menu)
for i in range(4, 26, 1):
label = f"W({i})"
menu.add_command(label=label, state=tk.DISABLED)
topology_generator_menu.add_cascade(label="Wheel", menu=menu)
def create_cube_menu(self, topology_generator_menu: tk.Menu):
"""
Create cube menu item and the sub menu items inside
"""
menu = tk.Menu(topology_generator_menu)
for i in range(2, 7, 1):
label = f"Q({i})"
menu.add_command(label=label, state=tk.DISABLED)
topology_generator_menu.add_cascade(label="Cube", menu=menu)
def create_clique_menu(self, topology_generator_menu: tk.Menu):
"""
Create clique menu item and the sub menu items inside
"""
menu = tk.Menu(topology_generator_menu)
for i in range(3, 25, 1):
label = f"K({i})"
menu.add_command(label=label, state=tk.DISABLED)
topology_generator_menu.add_cascade(label="Clique", menu=menu)
def create_bipartite_menu(self, topology_generator_menu: tk.Menu):
"""
Create bipartite menu item and the sub menu items inside
"""
menu = tk.Menu(topology_generator_menu)
temp = 24
for i in range(1, 13, 1):
submenu = tk.Menu(menu)
for j in range(i, temp, 1):
label = f"K({i} X {j})"
submenu.add_command(label=label, state=tk.DISABLED)
label = f"K({i})"
menu.add_cascade(label=label, menu=submenu)
temp = temp - 1
topology_generator_menu.add_cascade(label="Bipartite", menu=menu)
def create_topology_generator_menu(self, tools_menu: tk.Menu):
"""
Create topology menu item and its sub menu items
"""
menu = tk.Menu(tools_menu)
self.create_random_menu(menu)
self.create_grid_menu(menu)
self.create_connected_grid_menu(menu)
self.create_chain_menu(menu)
self.create_star_menu(menu)
self.create_cycle_menu(menu)
self.create_wheel_menu(menu)
self.create_cube_menu(menu)
self.create_clique_menu(menu)
self.create_bipartite_menu(menu)
tools_menu.add_cascade(label="Topology generator", menu=menu)
def draw_tools_menu(self):
def draw_tools_menu(self) -> None:
"""
Create tools menu
"""
menu = tk.Menu(self)
menu.add_command(label="Auto rearrange all", state=tk.DISABLED)
menu.add_command(label="Auto rearrange selected", state=tk.DISABLED)
menu.add_separator()
menu.add_command(label="Align to grid", state=tk.DISABLED)
menu.add_separator()
menu.add_command(label="Traffic...", state=tk.DISABLED)
menu.add_command(label="IP addresses...", state=tk.DISABLED)
menu.add_command(label="MAC addresses...", state=tk.DISABLED)
menu.add_command(label="Build hosts file...", state=tk.DISABLED)
menu.add_command(label="Renumber nodes...", state=tk.DISABLED)
self.create_experimental_menu(menu)
self.create_topology_generator_menu(menu)
menu.add_command(label="Debugger...", state=tk.DISABLED)
menu.add_command(label="Find", accelerator="Ctrl+F", command=self.click_find)
self.app.master.bind_all("<Control-f>", self.click_find)
menu.add_command(label="Auto Grid", command=self.click_autogrid)
menu.add_command(label="IP Addresses", command=self.click_ip_config)
menu.add_command(label="MAC Addresses", command=self.click_mac_config)
self.add_cascade(label="Tools", menu=menu)
def create_observer_widgets_menu(self, widget_menu: tk.Menu):
def create_observer_widgets_menu(self, widget_menu: tk.Menu) -> None:
"""
Create observer widget menu item and create the sub menu items inside
"""
var = tk.StringVar(value="none")
menu = tk.Menu(widget_menu)
menu.var = var
menu.add_command(
label="Edit Observers", command=self.menuaction.edit_observer_widgets
self.observers_menu = tk.Menu(widget_menu)
self.observers_menu.add_command(
label="Edit Observers", command=self.click_edit_observer_widgets
)
menu.add_separator()
menu.add_radiobutton(
self.observers_menu.add_separator()
self.observers_menu.add_radiobutton(
label="None",
variable=var,
variable=self.observers_var,
value="none",
command=lambda: self.app.core.set_observer(None),
command=lambda: self.core.set_observer(None),
)
for name in sorted(OBSERVERS):
cmd = OBSERVERS[name]
menu.add_radiobutton(
self.observers_menu.add_radiobutton(
label=name,
variable=var,
variable=self.observers_var,
value=name,
command=partial(self.app.core.set_observer, cmd),
command=partial(self.core.set_observer, cmd),
)
for name in sorted(self.app.core.custom_observers):
observer = self.app.core.custom_observers[name]
menu.add_radiobutton(
label=name,
variable=var,
value=name,
command=partial(self.app.core.set_observer, observer.cmd),
)
widget_menu.add_cascade(label="Observer Widgets", menu=menu)
self.observers_custom_index = self.observers_menu.index(tk.END) + 1
self.draw_custom_observers()
widget_menu.add_cascade(label="Observer Widgets", menu=self.observers_menu)
def create_adjacency_menu(self, widget_menu: tk.Menu):
def draw_custom_observers(self) -> None:
current_observers_index = self.observers_menu.index(tk.END) + 1
if self.observers_custom_index < current_observers_index:
self.observers_menu.delete(self.observers_custom_index, tk.END)
for name in sorted(self.core.custom_observers):
observer = self.core.custom_observers[name]
self.observers_menu.add_radiobutton(
label=name,
variable=self.observers_var,
value=name,
command=partial(self.core.set_observer, observer.cmd),
)
def create_adjacency_menu(self, widget_menu: tk.Menu) -> None:
"""
Create adjacency menu item and the sub menu items inside
"""
menu = tk.Menu(widget_menu)
menu.add_command(label="OSPFv2", state=tk.DISABLED)
menu.add_command(label="OSPFv3", state=tk.DISABLED)
menu.add_command(label="OSLR", state=tk.DISABLED)
menu.add_command(label="OSLRv2", state=tk.DISABLED)
menu.add_command(label="Configure Adjacency", state=tk.DISABLED)
menu.add_command(label="Enable OSPFv2?", state=tk.DISABLED)
menu.add_command(label="Enable OSPFv3?", state=tk.DISABLED)
menu.add_command(label="Enable OSLR?", state=tk.DISABLED)
menu.add_command(label="Enable OSLRv2?", state=tk.DISABLED)
widget_menu.add_cascade(label="Adjacency", menu=menu)
def draw_widgets_menu(self):
def create_throughput_menu(self, widget_menu: tk.Menu) -> None:
menu = tk.Menu(widget_menu)
menu.add_command(
label="Configure Throughput", command=self.click_config_throughput
)
menu.add_checkbutton(label="Enable Throughput?", command=self.click_throughput)
widget_menu.add_cascade(label="Throughput", menu=menu)
def draw_widgets_menu(self) -> None:
"""
Create widget menu
"""
menu = tk.Menu(self)
self.create_observer_widgets_menu(menu)
self.create_adjacency_menu(menu)
menu.add_checkbutton(label="Throughput", command=self.menuaction.throughput)
menu.add_separator()
menu.add_command(label="Configure Adjacency...", state=tk.DISABLED)
menu.add_command(
label="Configure Throughput...", command=self.menuaction.config_throughput
)
self.create_throughput_menu(menu)
self.add_cascade(label="Widgets", menu=menu)
def draw_session_menu(self):
def draw_session_menu(self) -> None:
"""
Create session menu
"""
menu = tk.Menu(self)
menu.add_command(
label="Sessions...", command=self.menuaction.session_change_sessions
)
menu.add_separator()
menu.add_command(label="Options...", command=self.menuaction.session_options)
menu.add_command(label="Servers...", command=self.menuaction.session_servers)
menu.add_command(label="Hooks...", command=self.menuaction.session_hooks)
menu.add_command(label="Reset Nodes", state=tk.DISABLED)
menu.add_command(label="Comments...", state=tk.DISABLED)
menu.add_command(label="Sessions", command=self.click_sessions)
menu.add_command(label="Servers", command=self.click_servers)
menu.add_command(label="Options", command=self.click_session_options)
menu.add_command(label="Hooks", command=self.click_hooks)
self.add_cascade(label="Session", menu=menu)
def draw_help_menu(self):
def draw_help_menu(self) -> None:
"""
Create help menu
"""
menu = tk.Menu(self)
menu.add_command(
label="Core GitHub (www)", command=self.menuaction.help_core_github
)
menu.add_command(
label="Core Documentation (www)",
command=self.menuaction.help_core_documentation,
)
menu.add_command(label="About", command=self.menuaction.show_about)
menu.add_command(label="Core GitHub (www)", command=self.click_core_github)
menu.add_command(label="Core Documentation (www)", command=self.click_core_doc)
menu.add_command(label="About", command=self.click_about)
self.add_cascade(label="Help", menu=menu)
def open_recent_files(self, filename: str):
def open_recent_files(self, filename: str) -> None:
if os.path.isfile(filename):
logging.debug("Open recent file %s", filename)
self.menuaction.open_xml_task(filename)
self.open_xml_task(filename)
else:
logging.warning("File does not exist %s", filename)
def update_recent_files(self):
def update_recent_files(self) -> None:
self.recent_menu.delete(0, tk.END)
for i in self.app.guiconfig["recentfiles"]:
for i in self.app.guiconfig.recentfiles:
self.recent_menu.add_command(
label=i, command=partial(self.open_recent_files, i)
)
def save(self, event=None):
xml_file = self.app.core.xml_file
def click_save(self, _event=None) -> None:
xml_file = self.core.xml_file
if xml_file:
self.app.core.save_xml(xml_file)
self.core.save_xml(xml_file)
else:
self.menuaction.file_save_as_xml()
self.click_save_xml()
def execute_python(self):
dialog = ExecutePythonDialog(self.app, self.app)
def click_save_xml(self, _event: tk.Event = None) -> None:
init_dir = self.core.xml_dir
if not init_dir:
init_dir = str(XMLS_PATH)
file_path = filedialog.asksaveasfilename(
initialdir=init_dir,
title="Save As",
filetypes=(("XML files", "*.xml"), ("All files", "*")),
defaultextension=".xml",
)
if file_path:
self.add_recent_file_to_gui_config(file_path)
self.core.save_xml(file_path)
self.core.xml_file = file_path
def click_open_xml(self, _event: tk.Event = None) -> None:
init_dir = self.core.xml_dir
if not init_dir:
init_dir = str(XMLS_PATH)
file_path = filedialog.askopenfilename(
initialdir=init_dir,
title="Open",
filetypes=(("XML Files", "*.xml"), ("All Files", "*")),
)
if file_path:
self.open_xml_task(file_path)
def open_xml_task(self, filename: str) -> None:
self.add_recent_file_to_gui_config(filename)
self.core.xml_file = filename
self.core.xml_dir = str(os.path.dirname(filename))
self.prompt_save_running_session()
task = ProgressTask(self.app, "Open XML", self.core.open_xml, args=(filename,))
task.start()
def execute_python(self) -> None:
dialog = ExecutePythonDialog(self.app)
dialog.show()
def change_menubar_item_state(self, is_runtime: bool):
for i in range(self.edit_menu.index("end")):
def add_recent_file_to_gui_config(self, file_path) -> None:
recent_files = self.app.guiconfig.recentfiles
num_files = len(recent_files)
if num_files == 0:
recent_files.insert(0, file_path)
elif 0 < num_files <= MAX_FILES:
if file_path in recent_files:
recent_files.remove(file_path)
recent_files.insert(0, file_path)
else:
if num_files == MAX_FILES:
recent_files.pop()
recent_files.insert(0, file_path)
else:
logging.error("unexpected number of recent files")
self.app.save_config()
self.app.menubar.update_recent_files()
def change_menubar_item_state(self, is_runtime: bool) -> None:
labels = {"Copy", "Paste", "Delete", "Cut"}
for i in range(self.edit_menu.index(tk.END) + 1):
try:
label_name = self.edit_menu.entrycget(i, "label")
if label_name in ["Copy", "Paste"]:
if is_runtime:
self.edit_menu.entryconfig(i, state="disabled")
else:
self.edit_menu.entryconfig(i, state="normal")
label = self.edit_menu.entrycget(i, "label")
if label not in labels:
continue
state = tk.DISABLED if is_runtime else tk.NORMAL
self.edit_menu.entryconfig(i, state=state)
except tk.TclError:
logging.debug("Ignore separators")
pass
def prompt_save_running_session(self, quit_app: bool = False) -> None:
"""
Prompt use to stop running session before application is closed
:param quit_app: True to quit app, False otherwise
"""
result = True
if self.core.is_runtime():
result = messagebox.askyesnocancel("Exit", "Stop the running session?")
if result:
self.core.delete_session()
if quit_app:
self.app.quit()
def click_new(self) -> None:
self.prompt_save_running_session()
self.core.create_new_session()
self.core.xml_file = None
def click_find(self, _event: tk.Event = None) -> None:
dialog = FindDialog(self.app)
dialog.show()
def click_preferences(self) -> None:
dialog = PreferencesDialog(self.app)
dialog.show()
def click_canvas_size_and_scale(self) -> None:
dialog = SizeAndScaleDialog(self.app)
dialog.show()
def click_canvas_wallpaper(self) -> None:
dialog = CanvasWallpaperDialog(self.app)
dialog.show()
def click_core_github(self) -> None:
webbrowser.open_new("https://github.com/coreemu/core")
def click_core_doc(self) -> None:
webbrowser.open_new("http://coreemu.github.io/core/")
def click_about(self) -> None:
dialog = AboutDialog(self.app)
dialog.show()
def click_throughput(self) -> None:
if not self.core.handling_throughputs:
self.core.enable_throughputs()
else:
self.core.cancel_throughputs()
def click_config_throughput(self) -> None:
dialog = ThroughputDialog(self.app)
dialog.show()
def click_copy(self, _event: tk.Event = None) -> None:
self.canvas.copy()
def click_paste(self, _event: tk.Event = None) -> None:
self.canvas.paste()
def click_delete(self, _event: tk.Event = None) -> None:
self.canvas.delete_selected_objects()
def click_cut(self, _event: tk.Event = None) -> None:
self.canvas.copy()
self.canvas.delete_selected_objects()
def click_session_options(self) -> None:
logging.debug("Click options")
dialog = SessionOptionsDialog(self.app)
if not dialog.has_error:
dialog.show()
def click_sessions(self) -> None:
logging.debug("Click change sessions")
dialog = SessionsDialog(self.app)
dialog.show()
def click_hooks(self) -> None:
logging.debug("Click hooks")
dialog = HooksDialog(self.app)
dialog.show()
def click_servers(self) -> None:
logging.debug("Click emulation servers")
dialog = ServersDialog(self.app)
dialog.show()
def click_edit_observer_widgets(self) -> None:
dialog = ObserverDialog(self.app)
dialog.show()
def click_autogrid(self) -> None:
width, height = self.canvas.current_dimensions
padding = (ICON_SIZE / 2) + 10
layout_size = padding + ICON_SIZE
col_count = width // layout_size
logging.info(
"auto grid layout: dimension(%s, %s) col(%s)", width, height, col_count
)
for i, node in enumerate(self.canvas.nodes.values()):
col = i % col_count
row = i // col_count
x = (col * layout_size) + padding
y = (row * layout_size) + padding
node.move(x, y)
def click_edge_label_change(self) -> None:
for edge in self.canvas.edges.values():
edge.draw_labels()
def click_mac_config(self) -> None:
dialog = MacConfigDialog(self.app)
dialog.show()
def click_ip_config(self) -> None:
dialog = IpConfigDialog(self.app)
dialog.show()
def click_custom_nodes(self) -> None:
dialog = CustomNodesDialog(self.app)
dialog.show()

View file

@ -1,7 +1,8 @@
import logging
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union
from typing import TYPE_CHECKING, List, Optional, Set
from core.api.grpc.core_pb2 import NodeType
from core.api.grpc.core_pb2 import Node, NodeType
from core.gui.appconfig import CustomNode, GuiConfig
from core.gui.images import ImageEnum, Images, TypeToImage
if TYPE_CHECKING:
@ -41,16 +42,16 @@ class NodeDraw:
return node_draw
@classmethod
def from_custom(cls, name: str, image_file: str, services: Set[str]):
def from_custom(cls, custom_node: CustomNode):
node_draw = NodeDraw()
node_draw.custom = True
node_draw.image_file = image_file
node_draw.image = Images.get_custom(image_file, ICON_SIZE)
node_draw.image_file = custom_node.image
node_draw.image = Images.get_custom(custom_node.image, ICON_SIZE)
node_draw.node_type = NodeType.DEFAULT
node_draw.services = services
node_draw.label = name
node_draw.model = name
node_draw.tooltip = name
node_draw.services = custom_node.services
node_draw.label = custom_node.name
node_draw.model = custom_node.name
node_draw.tooltip = custom_node.name
return node_draw
@ -64,8 +65,13 @@ class NodeUtils:
RJ45_NODES = {NodeType.RJ45}
IGNORE_NODES = {NodeType.CONTROL_NET, NodeType.PEER_TO_PEER}
NODE_MODELS = {"router", "host", "PC", "mdr", "prouter"}
ROUTER_NODES = {"router", "mdr"}
ANTENNA_ICON = None
@classmethod
def is_router_node(cls, node: Node) -> bool:
return cls.is_model_node(node.type) and node.model in cls.ROUTER_NODES
@classmethod
def is_ignore_node(cls, node_type: NodeType) -> bool:
return node_type in cls.IGNORE_NODES
@ -92,11 +98,7 @@ class NodeUtils:
@classmethod
def node_icon(
cls,
node_type: NodeType,
model: str,
gui_config: Dict[str, List[Dict[str, str]]],
scale=1.0,
cls, node_type: NodeType, model: str, gui_config: GuiConfig, scale=1.0
) -> "ImageTk.PhotoImage":
image_enum = TypeToImage.get(node_type, model)
@ -109,10 +111,7 @@ class NodeUtils:
@classmethod
def node_image(
cls,
core_node: "core_pb2.Node",
gui_config: Dict[str, List[Dict[str, str]]],
scale=1.0,
cls, core_node: "core_pb2.Node", gui_config: GuiConfig, scale=1.0
) -> "ImageTk.PhotoImage":
image = cls.node_icon(core_node.type, core_node.model, gui_config, scale)
if core_node.icon:
@ -127,20 +126,17 @@ class NodeUtils:
return node_type == NodeType.DEFAULT and model not in cls.NODE_MODELS
@classmethod
def get_custom_node_services(
cls, gui_config: Dict[str, List[Dict[str, str]]], name: str
) -> List[str]:
for m in gui_config["nodes"]:
if m["name"] == name:
return m["services"]
def get_custom_node_services(cls, gui_config: GuiConfig, name: str) -> List[str]:
for custom_node in gui_config.nodes:
if custom_node.name == name:
return custom_node.services
return []
@classmethod
def get_image_file(cls, gui_config, name: str) -> Union[str, None]:
if "nodes" in gui_config:
for m in gui_config["nodes"]:
if m["name"] == name:
return m["image"]
def get_image_file(cls, gui_config: GuiConfig, name: str) -> Optional[str]:
for custom_node in gui_config.nodes:
if custom_node.name == name:
return custom_node.image
return None
@classmethod
@ -172,9 +168,3 @@ class NodeUtils:
cls.NETWORK_NODES.append(node_draw)
cls.NODE_ICONS[(node_type, None)] = node_draw.image
cls.ANTENNA_ICON = Images.get(ImageEnum.ANTENNA, ANTENNA_SIZE)
class EdgeUtils:
@classmethod
def get_token(cls, src: int, dst: int) -> Tuple[int, ...]:
return tuple(sorted([src, dst]))

View file

@ -13,12 +13,11 @@ if TYPE_CHECKING:
class StatusBar(ttk.Frame):
def __init__(self, master: "Application", app: "Application", **kwargs):
def __init__(self, master: tk.Widget, app: "Application", **kwargs):
super().__init__(master, **kwargs)
self.app = app
self.status = None
self.statusvar = tk.StringVar()
self.progress_bar = None
self.zoom = None
self.cpu_usage = None
self.memory = None
@ -28,19 +27,14 @@ class StatusBar(ttk.Frame):
self.draw()
def draw(self):
self.columnconfigure(0, weight=1)
self.columnconfigure(1, weight=5)
self.columnconfigure(0, weight=7)
self.columnconfigure(1, weight=1)
self.columnconfigure(2, weight=1)
self.columnconfigure(3, weight=1)
self.columnconfigure(4, weight=1)
frame = ttk.Frame(self, borderwidth=1, relief=tk.RIDGE)
frame.grid(row=0, column=0, sticky="ew")
frame.columnconfigure(0, weight=1)
self.progress_bar = ttk.Progressbar(
frame, orient="horizontal", mode="indeterminate"
)
self.progress_bar.grid(sticky="ew")
self.status = ttk.Label(
self,
@ -49,7 +43,7 @@ class StatusBar(ttk.Frame):
borderwidth=1,
relief=tk.RIDGE,
)
self.status.grid(row=0, column=1, sticky="ew")
self.status.grid(row=0, column=0, sticky="ew")
self.zoom = ttk.Label(
self,
@ -58,20 +52,20 @@ class StatusBar(ttk.Frame):
borderwidth=1,
relief=tk.RIDGE,
)
self.zoom.grid(row=0, column=2, sticky="ew")
self.zoom.grid(row=0, column=1, sticky="ew")
self.cpu_usage = ttk.Label(
self, text="CPU TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE
)
self.cpu_usage.grid(row=0, column=3, sticky="ew")
self.cpu_usage.grid(row=0, column=2, sticky="ew")
self.alerts_button = ttk.Button(
self, text="Alerts", command=self.click_alerts, style=Styles.green_alert
)
self.alerts_button.grid(row=0, column=4, sticky="ew")
self.alerts_button.grid(row=0, column=3, sticky="ew")
def click_alerts(self):
dialog = AlertsDialog(self.app, self.app)
dialog = AlertsDialog(self.app)
dialog.show()
def set_status(self, message: str):

View file

@ -1,45 +1,58 @@
import logging
import threading
from typing import Any, Callable
import time
from typing import TYPE_CHECKING, Any, Callable, Tuple
from core.gui.errors import show_grpc_response_exceptions
if TYPE_CHECKING:
from core.gui.app import Application
class BackgroundTask:
def __init__(self, master: Any, task: Callable, callback: Callable = None, args=()):
self.master = master
self.args = args
class ProgressTask:
def __init__(
self,
app: "Application",
title: str,
task: Callable,
callback: Callable = None,
args: Tuple[Any] = None,
):
self.app = app
self.title = title
self.task = task
self.callback = callback
self.thread = None
self.args = args
if self.args is None:
self.args = ()
self.time = None
def start(self):
logging.info("starting task")
self.thread = threading.Thread(target=self.run, daemon=True)
self.thread.start()
def start(self) -> None:
self.app.progress.grid(sticky="ew")
self.app.progress.start()
self.time = time.perf_counter()
thread = threading.Thread(target=self.run, daemon=True)
thread.start()
def run(self):
result = self.task(*self.args)
logging.info("task completed")
# if start session fails, a response with Result: False and a list of exceptions is returned
if not getattr(result, "result", True):
if len(getattr(result, "exceptions", [])) > 0:
self.master.after(
0,
show_grpc_response_exceptions,
*(
result.__class__.__name__,
result.exceptions,
self.master,
self.master,
)
)
if self.callback:
if result is None:
args = ()
elif isinstance(result, (list, tuple)):
args = result
else:
args = (result,)
logging.info("calling callback: %s", args)
self.master.after(0, self.callback, *args)
def run(self) -> None:
logging.info("running task")
try:
values = self.task(*self.args)
if values is None:
values = ()
elif values and not isinstance(values, tuple):
values = (values,)
if self.callback:
logging.info("calling callback")
self.app.after(0, self.callback, *values)
except Exception as e:
logging.exception("progress task exception")
self.app.show_exception("Task Error", e)
finally:
self.app.after(0, self.complete)
def complete(self):
self.app.progress.stop()
self.app.progress.grid_forget()
total = time.perf_counter() - self.time
self.time = None
message = f"{self.title} ran for {total:.3f} seconds"
self.app.statusbar.set_status(message)

View file

@ -181,21 +181,24 @@ def theme_change(event: tk.Event):
Styles.green_alert,
background="green",
padding=0,
relief=tk.NONE,
relief=tk.RIDGE,
borderwidth=1,
font="TkDefaultFont",
)
style.configure(
Styles.yellow_alert,
background="yellow",
padding=0,
relief=tk.NONE,
relief=tk.RIDGE,
borderwidth=1,
font="TkDefaultFont",
)
style.configure(
Styles.red_alert,
background="red",
padding=0,
relief=tk.NONE,
relief=tk.RIDGE,
borderwidth=1,
font="TkDefaultFont",
)

View file

@ -1,5 +1,4 @@
import logging
import time
import tkinter as tk
from enum import Enum
from functools import partial
@ -7,13 +6,13 @@ from tkinter import ttk
from typing import TYPE_CHECKING, Callable
from core.api.grpc import core_pb2
from core.gui.dialogs.customnodes import CustomNodesDialog
from core.gui.dialogs.marker import MarkerDialog
from core.gui.dialogs.runtool import RunToolDialog
from core.gui.graph.enums import GraphMode
from core.gui.graph.shapeutils import ShapeType, is_marker
from core.gui.images import ImageEnum, Images
from core.gui.nodeutils import NodeDraw, NodeUtils
from core.gui.task import BackgroundTask
from core.gui.task import ProgressTask
from core.gui.themes import Styles
from core.gui.tooltip import Tooltip
@ -40,14 +39,12 @@ class Toolbar(ttk.Frame):
Core toolbar class
"""
def __init__(self, master: "Application", app: "Application", **kwargs):
def __init__(self, master: tk.Widget, app: "Application", **kwargs):
"""
Create a CoreToolbar instance
"""
super().__init__(master, **kwargs)
self.app = app
self.master = app.master
self.time = None
# design buttons
self.play_button = None
@ -60,9 +57,7 @@ class Toolbar(ttk.Frame):
# runtime buttons
self.runtime_select_button = None
self.stop_button = None
self.plot_button = None
self.runtime_marker_button = None
self.node_command_button = None
self.run_command_button = None
# frames
@ -75,8 +70,8 @@ class Toolbar(ttk.Frame):
# dialog
self.marker_tool = None
# these variables help keep track of what images being drawn so that scaling is possible
# since ImageTk.PhotoImage does not have resize method
# these variables help keep track of what images being drawn so that scaling
# is possible since ImageTk.PhotoImage does not have resize method
self.node_enum = None
self.network_enum = None
self.annotation_enum = None
@ -133,9 +128,7 @@ class Toolbar(ttk.Frame):
logging.debug("selecting runtime button: %s", button)
self.runtime_select_button.state(["!pressed"])
self.stop_button.state(["!pressed"])
self.plot_button.state(["!pressed"])
self.runtime_marker_button.state(["!pressed"])
self.node_command_button.state(["!pressed"])
self.run_command_button.state(["!pressed"])
button.state(["pressed"])
@ -143,7 +136,6 @@ class Toolbar(ttk.Frame):
self.runtime_frame = ttk.Frame(self)
self.runtime_frame.grid(row=0, column=0, sticky="nsew")
self.runtime_frame.columnconfigure(0, weight=1)
self.stop_button = self.create_button(
self.runtime_frame,
self.get_icon(ImageEnum.STOP),
@ -156,24 +148,12 @@ class Toolbar(ttk.Frame):
self.click_runtime_selection,
"selection tool",
)
self.plot_button = self.create_button(
self.runtime_frame,
self.get_icon(ImageEnum.PLOT),
self.click_plot_button,
"plot",
)
self.runtime_marker_button = self.create_button(
self.runtime_frame,
icon(ImageEnum.MARKER),
self.click_marker_button,
"marker",
)
self.node_command_button = self.create_button(
self.runtime_frame,
icon(ImageEnum.TWONODE),
self.click_two_node_button,
"run command from one node to another",
)
self.run_command_button = self.create_button(
self.runtime_frame, icon(ImageEnum.RUN), self.click_run_button, "run"
)
@ -212,12 +192,6 @@ class Toolbar(ttk.Frame):
node_draw.image_file,
)
self.create_picker_button(image, func, self.node_picker, name)
# draw edit node
# image = icon(ImageEnum.EDITNODE, PICKER_SIZE)
image = self.get_icon(ImageEnum.EDITNODE, PICKER_SIZE)
self.create_picker_button(
image, self.click_edit_node, self.node_picker, "Custom"
)
self.design_select(self.node_button)
self.node_button.after(
0, lambda: self.show_picker(self.node_button, self.node_picker)
@ -279,24 +253,21 @@ class Toolbar(ttk.Frame):
Start session handler redraw buttons, send node and link messages to grpc
server.
"""
self.app.canvas.hide_context()
self.app.menubar.change_menubar_item_state(is_runtime=True)
self.app.statusbar.progress_bar.start(5)
self.app.canvas.mode = GraphMode.SELECT
self.time = time.perf_counter()
task = BackgroundTask(self, self.app.core.start_session, self.start_callback)
task = ProgressTask(
self.app, "Start", self.app.core.start_session, self.start_callback
)
task.start()
def start_callback(self, response: core_pb2.StartSessionResponse):
self.app.statusbar.progress_bar.stop()
total = time.perf_counter() - self.time
message = f"Start ran for {total:.3f} seconds"
self.app.statusbar.set_status(message)
self.time = None
if response.result:
self.set_runtime()
self.app.core.set_metadata()
self.app.core.show_mobility_players()
else:
message = "\n".join(response.exceptions)
self.app.show_error("Start Session Error", message)
def set_runtime(self):
self.runtime_frame.tkraise()
@ -311,11 +282,6 @@ class Toolbar(ttk.Frame):
self.design_select(self.link_button)
self.app.canvas.mode = GraphMode.EDGE
def click_edit_node(self):
self.hide_pickers()
dialog = CustomNodesDialog(self.app, self.app)
dialog.show()
def update_button(
self,
button: ttk.Button,
@ -468,20 +434,16 @@ class Toolbar(ttk.Frame):
"""
redraw buttons on the toolbar, send node and link messages to grpc server
"""
logging.info("Click stop button")
self.app.canvas.hide_context()
logging.info("clicked stop button")
self.app.menubar.change_menubar_item_state(is_runtime=False)
self.app.statusbar.progress_bar.start(5)
self.time = time.perf_counter()
task = BackgroundTask(self, self.app.core.stop_session, self.stop_callback)
self.app.core.close_mobility_players()
task = ProgressTask(
self.app, "Stop", self.app.core.stop_session, self.stop_callback
)
task.start()
def stop_callback(self, response: core_pb2.StopSessionResponse):
self.app.statusbar.progress_bar.stop()
self.set_design()
total = time.perf_counter() - self.time
message = f"Stopped in {total:.3f} seconds"
self.app.statusbar.set_status(message)
self.app.canvas.stopped_session()
def update_annotation(
@ -497,14 +459,13 @@ class Toolbar(ttk.Frame):
if is_marker(shape_type):
if self.marker_tool:
self.marker_tool.destroy()
self.marker_tool = MarkerDialog(self.app, self.app)
self.marker_tool = MarkerDialog(self.app)
self.marker_tool.show()
def click_run_button(self):
logging.debug("Click on RUN button")
def click_plot_button(self):
logging.debug("Click on plot button")
dialog = RunToolDialog(self.app)
dialog.show()
def click_marker_button(self):
logging.debug("Click on marker button")
@ -513,13 +474,9 @@ class Toolbar(ttk.Frame):
self.app.canvas.annotation_type = ShapeType.MARKER
if self.marker_tool:
self.marker_tool.destroy()
self.marker_tool = MarkerDialog(self.app, self.app)
self.marker_tool = MarkerDialog(self.app)
self.marker_tool.show()
def click_two_node_button(self):
logging.debug("Click TWONODE button")
# def scale_button(cls, button, image_enum, scale):
def scale_button(self, button, image_enum):
image = icon(image_enum, int(TOOLBAR_SIZE * self.app.app_scale))
button.config(image=image)
@ -532,10 +489,7 @@ class Toolbar(ttk.Frame):
self.scale_button(self.node_button, self.node_enum)
self.scale_button(self.network_button, self.network_enum)
self.scale_button(self.annotation_button, self.annotation_enum)
self.scale_button(self.runtime_select_button, ImageEnum.SELECT)
self.scale_button(self.stop_button, ImageEnum.STOP)
self.scale_button(self.plot_button, ImageEnum.PLOT)
self.scale_button(self.runtime_marker_button, ImageEnum.MARKER)
self.scale_button(self.node_command_button, ImageEnum.TWONODE)
self.scale_button(self.run_command_button, ImageEnum.RUN)

View file

@ -3,169 +3,114 @@ input validation
"""
import re
import tkinter as tk
from typing import TYPE_CHECKING
import netaddr
from netaddr import IPNetwork
if TYPE_CHECKING:
from core.gui.app import Application
from tkinter import ttk
SMALLEST_SCALE = 0.5
LARGEST_SCALE = 5.0
HEX_REGEX = re.compile("^([#]([0-9]|[a-f])+)$|^[#]$")
class InputValidation:
def __init__(self, app: "Application"):
self.master = app.master
self.positive_int = None
self.positive_float = None
self.app_scale = None
self.name = None
self.ip4 = None
self.rgb = None
self.hex = None
self.register()
class ValidationEntry(ttk.Entry):
empty = None
def register(self):
self.positive_int = self.master.register(self.check_positive_int)
self.positive_float = self.master.register(self.check_positive_float)
self.app_scale = self.master.register(self.check_scale_value)
self.name = self.master.register(self.check_node_name)
self.ip4 = self.master.register(self.check_ip4)
self.rgb = self.master.register(self.check_rbg)
self.hex = self.master.register(self.check_hex)
def __init__(self, master=None, widget=None, empty_enabled=True, **kwargs) -> None:
super().__init__(master, widget, **kwargs)
cmd = self.register(self.is_valid)
self.configure(validate="key", validatecommand=(cmd, "%P"))
if self.empty is not None and empty_enabled:
self.bind("<FocusOut>", self.focus_out)
@classmethod
def ip_focus_out(cls, event: tk.Event):
value = event.widget.get()
try:
IPNetwork(value)
except netaddr.core.AddrFormatError:
event.widget.delete(0, tk.END)
event.widget.insert(tk.END, "invalid")
def is_valid(self, s: str) -> bool:
raise NotImplementedError
@classmethod
def focus_out(cls, event: tk.Event, default: str):
value = event.widget.get()
if value == "":
event.widget.insert(tk.END, default)
def focus_out(self, _event: tk.Event) -> None:
value = self.get()
if not value:
self.insert(tk.END, self.empty)
@classmethod
def check_positive_int(cls, s: str) -> bool:
if len(s) == 0:
return True
try:
int_value = int(s)
if int_value >= 0:
return True
return False
except ValueError:
return False
@classmethod
def check_positive_float(cls, s: str) -> bool:
if len(s) == 0:
return True
try:
float_value = float(s)
if float_value >= 0.0:
return True
return False
except ValueError:
return False
class PositiveIntEntry(ValidationEntry):
empty = "0"
@classmethod
def check_node_name(cls, s: str) -> bool:
if len(s) < 0:
return False
if len(s) == 0:
return True
for char in s:
if not char.isalnum() and char != "_":
return False
return True
@classmethod
def check_canvas_int(cls, s: str) -> bool:
if len(s) == 0:
return True
try:
int_value = int(s)
if int_value >= 0:
return True
return False
except ValueError:
return False
@classmethod
def check_canvas_float(cls, s: str) -> bool:
def is_valid(self, s: str) -> bool:
if not s:
return True
try:
float_value = float(s)
if float_value >= 0.0:
return True
return False
value = int(s)
return value >= 0
except ValueError:
return False
@classmethod
def check_scale_value(cls, s: str) -> bool:
class PositiveFloatEntry(ValidationEntry):
empty = "0.0"
def is_valid(self, s: str) -> bool:
if not s:
return True
try:
float_value = float(s)
if SMALLEST_SCALE <= float_value <= LARGEST_SCALE or float_value == 0:
return True
return False
value = float(s)
return value >= 0.0
except ValueError:
return False
@classmethod
def check_ip4(cls, s: str) -> bool:
class FloatEntry(ValidationEntry):
empty = "0.0"
def is_valid(self, s: str) -> bool:
if not s:
return True
pat = re.compile("^([0-9]+[.])*[0-9]*$")
if pat.match(s) is not None:
_32bits = s.split(".")
if len(_32bits) > 4:
return False
for _8bits in _32bits:
if (
(_8bits and int(_8bits) > 255)
or len(_8bits) > 3
or (_8bits.startswith("0") and len(_8bits) > 1)
):
return False
try:
float(s)
return True
else:
except ValueError:
return False
@classmethod
def check_rbg(cls, s: str) -> bool:
class RgbEntry(ValidationEntry):
def is_valid(self, s: str) -> bool:
if not s:
return True
if s.startswith("0") and len(s) >= 2:
return False
try:
value = int(s)
if 0 <= value <= 255:
return True
else:
return False
return 0 <= value <= 255
except ValueError:
return False
@classmethod
def check_hex(cls, s: str) -> bool:
class HexEntry(ValidationEntry):
def is_valid(self, s: str) -> bool:
if not s:
return True
pat = re.compile("^([#]([0-9]|[a-f])+)$|^[#]$")
if pat.match(s):
if 0 <= len(s) <= 7:
return True
else:
return False
if HEX_REGEX.match(s):
return 0 <= len(s) <= 7
else:
return False
class NodeNameEntry(ValidationEntry):
empty = "noname"
def is_valid(self, s: str) -> bool:
if len(s) < 0:
return False
if len(s) == 0:
return True
for x in s:
if not x.isalnum() and x != "_":
return False
return True
class AppScaleEntry(ValidationEntry):
def is_valid(self, s: str) -> bool:
if not s:
return True
try:
float_value = float(s)
return SMALLEST_SCALE <= float_value <= LARGEST_SCALE or float_value == 0
except ValueError:
return False

View file

@ -6,7 +6,7 @@ from tkinter import filedialog, font, ttk
from typing import TYPE_CHECKING, Dict
from core.api.grpc import common_pb2, core_pb2
from core.gui import themes
from core.gui import themes, validation
from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING:
@ -127,43 +127,16 @@ class ConfigFrame(ttk.Notebook):
button = ttk.Button(file_frame, text="...", command=func)
button.grid(row=0, column=1)
else:
if "controlnet" in option.name and "script" not in option.name:
entry = ttk.Entry(
tab.frame,
textvariable=value,
validate="key",
validatecommand=(self.app.validation.ip4, "%P"),
)
entry.grid(row=index, column=1, sticky="ew")
else:
entry = ttk.Entry(tab.frame, textvariable=value)
entry.grid(row=index, column=1, sticky="ew")
entry = ttk.Entry(tab.frame, textvariable=value)
entry.grid(row=index, column=1, sticky="ew")
elif option.type in INT_TYPES:
value.set(option.value)
entry = ttk.Entry(
tab.frame,
textvariable=value,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
)
entry.bind(
"<FocusOut>",
lambda event: self.app.validation.focus_out(event, "0"),
)
entry = validation.PositiveIntEntry(tab.frame, textvariable=value)
entry.grid(row=index, column=1, sticky="ew")
elif option.type == core_pb2.ConfigOptionType.FLOAT:
value.set(option.value)
entry = ttk.Entry(
tab.frame,
textvariable=value,
validate="key",
validatecommand=(self.app.validation.positive_float, "%P"),
)
entry.bind(
"<FocusOut>",
lambda event: self.app.validation.focus_out(event, "0"),
)
entry = validation.PositiveFloatEntry(tab.frame, textvariable=value)
entry.grid(row=index, column=1, sticky="ew")
else:
logging.error("unhandled config option type: %s", option.type)
@ -181,7 +154,6 @@ class ConfigFrame(ttk.Notebook):
option.value = "0"
else:
option.value = config_value
return {x: self.config[x].value for x in self.config}
def set_values(self, config: Dict[str, str]) -> None:
@ -204,7 +176,10 @@ class ListboxScroll(ttk.Frame):
self.scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
self.scrollbar.grid(row=0, column=1, sticky="ns")
self.listbox = tk.Listbox(
self, selectmode=tk.SINGLE, yscrollcommand=self.scrollbar.set
self,
selectmode=tk.BROWSE,
yscrollcommand=self.scrollbar.set,
exportselection=False,
)
themes.style_listbox(self.listbox)
self.listbox.grid(row=0, column=0, sticky="nsew")

View file

@ -488,12 +488,14 @@ class BasicRangeModel(WirelessModel):
:param message_type: link message type
:return: link data
"""
color = self.session.get_link_color(self.wlan.id)
return LinkData(
message_type=message_type,
node1_id=interface1.node.id,
node2_id=interface2.node.id,
network_id=self.wlan.id,
link_type=LinkTypes.WIRELESS,
color=color,
)
def sendlinkmsg(

View file

@ -1112,6 +1112,7 @@ class CoreNetworkBase(NodeBase):
link_type=self.linktype,
unidirectional=unidirectional,
interface2_id=linked_node.getifindex(netif),
interface2_name=netif.name,
interface2_mac=netif.hwaddr,
interface2_ip4=interface2_ip4,
interface2_ip4_mask=interface2_ip4_mask,

View file

@ -949,12 +949,14 @@ class PtpNet(CoreNetwork):
dup=if1.getparam("duplicate"),
jitter=if1.getparam("jitter"),
interface1_id=if1.node.getifindex(if1),
interface1_name=if1.name,
interface1_mac=if1.hwaddr,
interface1_ip4=interface1_ip4,
interface1_ip4_mask=interface1_ip4_mask,
interface1_ip6=interface1_ip6,
interface1_ip6_mask=interface1_ip6_mask,
interface2_id=if2.node.getifindex(if2),
interface2_name=if2.name,
interface2_mac=if2.hwaddr,
interface2_ip4=interface2_ip4,
interface2_ip4_mask=interface2_ip4_mask,
@ -968,7 +970,7 @@ class PtpNet(CoreNetwork):
# (swap if1 and if2)
if unidirectional:
link_data = LinkData(
message_type=0,
message_type=MessageFlags.NONE,
link_type=self.linktype,
node1_id=if2.node.id,
node2_id=if1.node.id,

View file

@ -33,7 +33,6 @@ NODE_LAYER = "CORE::Nodes"
LINK_LAYER = "CORE::Links"
CORE_LAYERS = [CORE_LAYER, LINK_LAYER, NODE_LAYER]
DEFAULT_LINK_COLOR = "red"
LINK_COLORS = ["green", "blue", "orange", "purple", "white"]
class Sdt:
@ -73,7 +72,6 @@ class Sdt:
self.url = self.DEFAULT_SDT_URL
self.address = None
self.protocol = None
self.colors = {}
self.network_layers = set()
self.session.node_handlers.append(self.handle_node_update)
self.session.link_handlers.append(self.handle_link_update)
@ -180,7 +178,6 @@ class Sdt:
self.cmd(f"delete layer,{layer}")
self.disconnect()
self.network_layers.clear()
self.colors.clear()
def cmd(self, cmdstr: str) -> bool:
"""
@ -353,24 +350,6 @@ class Sdt:
pass
return result
def get_link_line(self, network_id: int) -> str:
"""
Retrieve link line color based on network.
:param network_id: network id of link, None for wired links
:return: link line configuration
"""
network = self.session.nodes.get(network_id)
if network:
color = self.colors.get(network_id)
if not color:
index = len(self.colors) % len(LINK_COLORS)
color = LINK_COLORS[index]
self.colors[network_id] = color
else:
color = DEFAULT_LINK_COLOR
return f"{color},2"
def add_link(
self, node_one: int, node_two: int, network_id: int = None, label: str = None
) -> None:
@ -388,7 +367,10 @@ class Sdt:
return
if self.wireless_net_check(node_one) or self.wireless_net_check(node_two):
return
line = self.get_link_line(network_id)
color = DEFAULT_LINK_COLOR
if network_id:
color = self.session.get_link_color(network_id)
line = f"{color},2"
link_id = get_link_id(node_one, node_two, network_id)
layer = LINK_LAYER
if network_id:

View file

@ -409,7 +409,6 @@ class CoreServices:
"using default services for node(%s) type(%s)", node.name, node_type
)
services = self.default_services.get(node_type, [])
logging.info("setting services for node(%s): %s", node.name, services)
for service_name in services:
service = self.get_service(node.id, service_name, default_service=True)

View file

@ -1,176 +0,0 @@
"""
Docker service allows running docker containers within CORE nodes.
The running of Docker within a CORE node allows for additional extensibility to
the CORE services. This allows network applications and protocols to be easily
packaged and run on any node.
This service that will add a new group to the services list. This
will have a service called Docker which will just start the docker service
within the node but not run anything. It will also scan all docker images on
the host machine. If any are tagged with 'core' then they will be added as a
service to the Docker group. The image will then be auto run if that service is
selected.
This requires a recent version of Docker. This was tested using a PPA on Ubuntu
with version 1.2.0. The version in the standard Ubuntu repo is to old for
this purpose (we need --net host).
It also requires docker-py (https://pypi.python.org/pypi/docker-py) which can be
installed with 'pip install docker-py'. This is used to interface with Docker
from the python service.
An example use case is to pull an image from Docker.com. A test image has been
uploaded for this purpose:
sudo docker pull stuartmarsden/multicastping
This downloads an image which is based on Ubuntu 14.04 with python and twisted.
It runs a simple program that sends a multicast ping and listens and records
any it receives.
In order for this to appear as a docker service it must be tagged with core.
Find out the id by running 'sudo docker images'. You should see all installed
images and the one you want looks like this:
stuartmarsden/multicastping latest 4833487e66d2 20 hours
ago 487 MB
The id will be different on your machine so use it in the following command:
sudo docker tag 4833487e66d2 stuartmarsden/multicastping:core
This image will be listed in the services after we restart the core-daemon:
sudo service core-daemon restart
You can set up a simple network with a number of PCs connected to a switch. Set
the stuartmarsden/multicastping service for all the PCs. When started they will
all begin sending Multicast pings.
In order to see what is happening you can go in to the terminal of a node and
look at the docker log. Easy shorthand is:
docker logs $(docker ps -q)
Which just shows the log of the running docker container (usually just one per
node). I have added this as an observer node to my setup: Name: docker logs
Command: bash -c 'docker logs $(docker ps -q) | tail -20'
So I can just hover over to see the log which looks like this:
Datagram 'Client: Ping' received from ('10.0.0.20', 8005)
Datagram 'Client: Ping' received from ('10.0.5.21', 8005)
Datagram 'Client: Ping' received from ('10.0.3.20', 8005)
Datagram 'Client: Ping' received from ('10.0.4.20', 8005)
Datagram 'Client: Ping' received from ('10.0.4.20', 8005)
Datagram 'Client: Ping' received from ('10.0.1.21', 8005)
Datagram 'Client: Ping' received from ('10.0.4.21', 8005)
Datagram 'Client: Ping' received from ('10.0.4.21', 8005)
Datagram 'Client: Ping' received from ('10.0.5.20', 8005)
Datagram 'Client: Ping' received from ('10.0.0.21', 8005)
Datagram 'Client: Ping' received from ('10.0.3.21', 8005)
Datagram 'Client: Ping' received from ('10.0.0.20', 8005)
Datagram 'Client: Ping' received from ('10.0.5.21', 8005)
Datagram 'Client: Ping' received from ('10.0.3.20', 8005)
Datagram 'Client: Ping' received from ('10.0.4.20', 8005)
Datagram 'Client: Ping' received from ('10.0.4.20', 8005)
Datagram 'Client: Ping' received from ('10.0.1.21', 8005)
Datagram 'Client: Ping' received from ('10.0.4.21', 8005)
Datagram 'Client: Ping' received from ('10.0.4.21', 8005)
Datagram 'Client: Ping' received from ('10.0.5.20', 8005)
Limitations:
1. Docker images must be downloaded on the host as usually a CORE node does not
have access to the internet.
2. Each node isolates running containers (keeps things simple)
3. Recent version of docker needed so that --net host can be used. This does
not further abstract the network within a node and allows multicast which
is not enabled within Docker containers at the moment.
4. The core-daemon must be restarted for new images to show up.
5. A Docker-daemon is run within each node but the images are shared. This
does mean that the daemon attempts to access an SQLlite database within the
host. At startup all the nodes will try to access this and it will be locked
for most due to contention. The service just does a hackish wait for 1 second
and retry. This means all the docker containers can take a while to come up
depending on how many nodes you have.
"""
import logging
from core.services.coreservices import CoreService, ServiceManager
try:
from docker import Client
except ImportError:
logging.debug("missing python docker bindings")
class DockerService(CoreService):
"""
This is a service which will allow running docker containers in a CORE
node.
"""
name = "Docker"
executables = ("docker",)
group = "Docker"
dirs = ('/var/lib/docker/containers/', '/run/shm', '/run/resolvconf',)
configs = ('docker.sh',)
startup = ('sh docker.sh',)
shutdown = ('service docker stop',)
# Container image to start
image = ""
@classmethod
def generate_config(cls, node, filename):
"""
Returns a string having contents of a docker.sh script that
can be modified to start a specific docker image.
"""
cfg = "#!/bin/sh\n"
cfg += "# auto-generated by Docker (docker.py)\n"
# Docker likes to think it has DNS set up or it complains.
# Unless your network was attached to the Internet this is
# non-functional but hides error messages.
cfg += 'echo "nameserver 8.8.8.8" > /run/resolvconf/resolv.conf\n'
# Starts the docker service. In Ubuntu this is docker.io; in other
# distros may just be docker
cfg += 'service docker start\n'
cfg += "# you could add a command to start a image here eg:\n"
if not cls.image:
cfg += "# docker run -d --net host --name coreDock <imagename>\n"
else:
cfg += """\
result=1
until [ $result -eq 0 ]; do
docker run -d --net host --name coreDock %s
result=$?
# this is to alleviate contention to docker's SQLite database
sleep 0.3
done
""" % (cls.image,)
return cfg
@classmethod
def on_load(cls):
logging.debug("loading custom docker services")
if "Client" in globals():
client = Client(version="1.10")
images = client.images()
del client
else:
images = []
for image in images:
if u"<none>" in image["RepoTags"][0]:
continue
for repo in image["RepoTags"]:
if u":core" not in repo:
continue
dockerid = repo.encode("ascii", "ignore").split(":")[0]
sub_class = type("SubClass", (DockerService,), {"_name": dockerid, "_image": dockerid})
ServiceManager.add(sub_class)
del images

View file

@ -260,7 +260,7 @@ class QuaggaService(CoreService):
if netaddr.valid_ipv4(a):
return a
# raise ValueError, "no IPv4 address found for router ID"
return "0.0.0.0"
return "0.0.0.%d" % node.id
@staticmethod
def rj45check(ifc):
@ -348,7 +348,19 @@ class Ospfv2(QuaggaService):
@classmethod
def generatequaggaifcconfig(cls, node, ifc):
return cls.mtucheck(ifc)
cfg = cls.mtucheck(ifc)
# external RJ45 connections will use default OSPF timers
if cls.rj45check(ifc):
return cfg
cfg += cls.ptpcheck(ifc)
return (
cfg
+ """\
ip ospf hello-interval 2
ip ospf dead-interval 6
ip ospf retransmit-interval 5
"""
)
class Ospfv3(QuaggaService):

View file

@ -40,10 +40,15 @@ class OvsService(SdnService):
cfg = "#!/bin/sh\n"
cfg += "# auto-generated by OvsService (OvsService.py)\n"
cfg += "/etc/init.d/openvswitch-switch start < /dev/null\n"
cfg += "## First make sure that the ovs services are up and running\n"
cfg += "/etc/init.d/openvswitch-switch start < /dev/null\n\n"
cfg += "## create the switch itself, set the fail mode to secure, \n"
cfg += "## this stops it from routing traffic without defined flows.\n"
cfg += "## remove the -- and everything after if you want it to act as a regular switch\n"
cfg += "ovs-vsctl add-br ovsbr0 -- set Bridge ovsbr0 fail-mode=secure\n"
cfg += "ip link set dev ovsbr0 up\n"
cfg += "\n## Now add all our interfaces as ports to the switch\n"
portnum = 1
for ifc in node.netifs():
if hasattr(ifc, "control") and ifc.control is True:
continue
@ -51,9 +56,8 @@ class OvsService(SdnService):
ifnum = ifnumstr[0]
# create virtual interfaces
cfg += "## Create a veth pair to send the data to\n"
cfg += "ip link add rtr%s type veth peer name sw%s\n" % (ifnum, ifnum)
cfg += "ip link set dev rtr%s up\n" % ifnum
cfg += "ip link set dev sw%s up\n" % ifnum
# remove ip address of eths because quagga/zebra will assign same IPs to rtr interfaces
# or assign them manually to rtr interfaces if zebra is not running
@ -71,17 +75,37 @@ class OvsService(SdnService):
raise ValueError("invalid address: %s" % ifcaddr)
# add interfaces to bridge
cfg += "ovs-vsctl add-port ovsbr0 eth%s\n" % ifnum
cfg += "ovs-vsctl add-port ovsbr0 sw%s\n" % ifnum
# Make port numbers explicit so they're easier to follow in reading the script
cfg += "## Add the CORE interface to the switch\n"
cfg += (
"ovs-vsctl add-port ovsbr0 eth%s -- set Interface eth%s ofport_request=%d\n"
% (ifnum, ifnum, portnum)
)
cfg += "## And then add its sibling veth interface\n"
cfg += (
"ovs-vsctl add-port ovsbr0 sw%s -- set Interface sw%s ofport_request=%d\n"
% (ifnum, ifnum, portnum + 1)
)
cfg += "## start them up so we can send/receive data\n"
cfg += "ovs-ofctl mod-port ovsbr0 eth%s up\n" % ifnum
cfg += "ovs-ofctl mod-port ovsbr0 sw%s up\n" % ifnum
cfg += "## Bring up the lower part of the veth pair\n"
cfg += "ip link set dev rtr%s up\n" % ifnum
portnum += 2
# Add rule for default controller if there is one local (even if the controller is not local, it finds it)
cfg += "\n## We assume there will be an SDN controller on the other end of this, \n"
cfg += "## but it will still function if there's not\n"
cfg += "ovs-vsctl set-controller ovsbr0 tcp:127.0.0.1:6633\n"
cfg += "\n## Now to create some default flows, \n"
cfg += "## if the above controller will be present then you probably want to delete them\n"
# Setup default flows
portnum = 1
for ifc in node.netifs():
if hasattr(ifc, "control") and ifc.control is True:
continue
cfg += "## Take the data from the CORE interface and put it on the veth and vice versa\n"
cfg += (
"ovs-ofctl add-flow ovsbr0 priority=1000,in_port=%d,action=output:%d\n"
% (portnum, portnum + 1)

View file

@ -7,7 +7,6 @@ import netaddr
from core import constants, utils
from core.errors import CoreCommandError
from core.nodes.base import CoreNode
from core.services.coreservices import CoreService, ServiceMode
@ -77,20 +76,15 @@ class DefaultRouteService(UtilService):
@classmethod
def generate_config(cls, node, filename):
# only add default routes for linked routing nodes
routes = []
for other_node in node.session.nodes.values():
if not isinstance(other_node, CoreNode):
continue
if other_node.type not in ["router", "mdr"]:
continue
commonnets = node.commonnets(other_node)
if commonnets:
_, _, router_eth = commonnets[0]
for x in router_eth.addrlist:
addr, prefix = x.split("/")
routes.append(addr)
break
netifs = node.netifs(sort=True)
if netifs:
netif = netifs[0]
for x in netif.addrlist:
net = netaddr.IPNetwork(x).cidr
if net.size > 1:
router = net[1]
routes.append(str(router))
cfg = "#!/bin/sh\n"
cfg += "# auto-generated by DefaultRoute service (utility.py)\n"
for route in routes:

View file

@ -437,8 +437,7 @@ def random_mac() -> str:
"""
value = random.randint(0, 0xFFFFFF)
value |= 0x00163E << 24
mac = netaddr.EUI(value)
mac.dialect = netaddr.mac_unix_expanded
mac = netaddr.EUI(value, dialect=netaddr.mac_unix_expanded)
return str(mac)
@ -450,8 +449,7 @@ def validate_mac(value: str) -> str:
:return: unix formatted mac
"""
try:
mac = netaddr.EUI(value)
mac.dialect = netaddr.mac_unix_expanded
mac = netaddr.EUI(value, dialect=netaddr.mac_unix_expanded)
return str(mac)
except netaddr.AddrFormatError as e:
raise CoreError(f"invalid mac address {value}: {e}")

View file

@ -9,10 +9,11 @@ from core.emane.nodes import EmaneNet
from core.emulator.data import LinkData
from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions
from core.emulator.enumerations import EventTypes, NodeTypes
from core.errors import CoreXmlError
from core.nodes.base import CoreNetworkBase, CoreNodeBase, NodeBase
from core.nodes.docker import DockerNode
from core.nodes.lxd import LxcNode
from core.nodes.network import CtrlNet
from core.nodes.network import CtrlNet, WlanNode
from core.services.coreservices import CoreService
if TYPE_CHECKING:
@ -162,14 +163,12 @@ class ServiceElement:
self.element.append(directories)
def add_files(self) -> None:
# get custom files
file_elements = etree.Element("files")
for file_name in self.service.config_data:
data = self.service.config_data[file_name]
file_element = etree.SubElement(file_elements, "file")
add_attribute(file_element, "name", file_name)
file_element.text = data
file_element.text = etree.CDATA(data)
if file_elements.getchildren():
self.element.append(file_elements)
@ -313,7 +312,7 @@ class CoreXmlWriter:
def write_session_hooks(self) -> None:
# hook scripts
hooks = etree.Element("session_hooks")
for state in sorted(self.session._hooks.keys()):
for state in sorted(self.session._hooks, key=lambda x: x.value):
for file_name, data in self.session._hooks[state]:
hook = etree.SubElement(hooks, "hook")
add_attribute(hook, "name", file_name)
@ -560,26 +559,31 @@ class CoreXmlWriter:
)
link_element.append(interface_two)
# check for options
options = etree.Element("options")
add_attribute(options, "delay", link_data.delay)
add_attribute(options, "bandwidth", link_data.bandwidth)
add_attribute(options, "per", link_data.per)
add_attribute(options, "dup", link_data.dup)
add_attribute(options, "jitter", link_data.jitter)
add_attribute(options, "mer", link_data.mer)
add_attribute(options, "burst", link_data.burst)
add_attribute(options, "mburst", link_data.mburst)
add_attribute(options, "type", link_data.link_type)
add_attribute(options, "gui_attributes", link_data.gui_attributes)
add_attribute(options, "unidirectional", link_data.unidirectional)
add_attribute(options, "emulation_id", link_data.emulation_id)
add_attribute(options, "network_id", link_data.network_id)
add_attribute(options, "key", link_data.key)
add_attribute(options, "opaque", link_data.opaque)
add_attribute(options, "session", link_data.session)
if options.items():
link_element.append(options)
# check for options, don't write for emane/wlan links
node_one = self.session.get_node(link_data.node1_id)
node_two = self.session.get_node(link_data.node2_id)
is_node_one_wireless = isinstance(node_one, (WlanNode, EmaneNet))
is_node_two_wireless = isinstance(node_two, (WlanNode, EmaneNet))
if not any([is_node_one_wireless, is_node_two_wireless]):
options = etree.Element("options")
add_attribute(options, "delay", link_data.delay)
add_attribute(options, "bandwidth", link_data.bandwidth)
add_attribute(options, "per", link_data.per)
add_attribute(options, "dup", link_data.dup)
add_attribute(options, "jitter", link_data.jitter)
add_attribute(options, "mer", link_data.mer)
add_attribute(options, "burst", link_data.burst)
add_attribute(options, "mburst", link_data.mburst)
add_attribute(options, "type", link_data.link_type)
add_attribute(options, "gui_attributes", link_data.gui_attributes)
add_attribute(options, "unidirectional", link_data.unidirectional)
add_attribute(options, "emulation_id", link_data.emulation_id)
add_attribute(options, "network_id", link_data.network_id)
add_attribute(options, "key", link_data.key)
add_attribute(options, "opaque", link_data.opaque)
add_attribute(options, "session", link_data.session)
if options.items():
link_element.append(options)
return link_element
@ -602,8 +606,8 @@ class CoreXmlReader:
self.read_service_configs()
self.read_mobility_configs()
self.read_emane_global_config()
self.read_emane_configs()
self.read_nodes()
self.read_emane_configs()
self.read_configservice_configs()
self.read_links()
@ -639,14 +643,14 @@ class CoreXmlReader:
session_options = self.scenario.find("session_options")
if session_options is None:
return
configs = {}
for config in session_options.iterchildren():
name = config.get("name")
value = config.get("value")
configs[name] = value
logging.info("reading session options: %s", configs)
self.session.options.set_configs(configs)
xml_config = {}
for configuration in session_options.iterchildren():
name = configuration.get("name")
value = configuration.get("value")
xml_config[name] = value
logging.info("reading session options: %s", xml_config)
config = self.session.options.get_configs()
config.update(xml_config)
def read_session_hooks(self) -> None:
session_hooks = self.scenario.find("session_hooks")
@ -756,6 +760,18 @@ class CoreXmlReader:
model_name = emane_configuration.get("model")
configs = {}
# validate node and model
node = self.session.nodes.get(node_id)
if not node:
raise CoreXmlError(f"node for emane config doesn't exist: {node_id}")
if not isinstance(node, EmaneNet):
raise CoreXmlError(f"invalid node for emane config: {node.name}")
model = self.session.emane.models.get(model_name)
if not model:
raise CoreXmlError(f"invalid emane model: {model_name}")
node.setmodel(model, {})
# read and set emane model configuration
mac_configuration = emane_configuration.find("mac")
for config in mac_configuration.iterchildren():
name = config.get("name")

View file

@ -10,7 +10,7 @@
# extra cruft to remove
DISTCLEANFILES = conf.py Makefile Makefile.in stamp-vti *.rst
all: index.rst
all: html
# auto-generated Python documentation using Sphinx
index.rst:

View file

@ -121,7 +121,7 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
#html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.

View file

@ -1,12 +1,31 @@
"""
Example for scripting a standalone distributed EMANE session that does not interact
with the GUI.
"""
import argparse
import logging
import distributed_parser
from core.emane.ieee80211abg import EmaneIeee80211abgModel
from core.emulator.coreemu import CoreEmu
from core.emulator.emudata import IpPrefixes, NodeOptions
from core.emulator.enumerations import EventTypes, NodeTypes
def parse(name):
parser = argparse.ArgumentParser(description=f"Run {name} example")
parser.add_argument(
"-a",
"--address",
help="local address that distributed servers will use for gre tunneling",
)
parser.add_argument(
"-s", "--server", help="distributed server to use for creating nodes"
)
options = parser.parse_args()
return options
def main(args):
# ip generator for example
prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16")
@ -55,5 +74,5 @@ def main(args):
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
args = distributed_parser.parse(__file__)
args = parse(__file__)
main(args)

View file

@ -1,11 +1,30 @@
"""
Example for scripting a standalone distributed LXD session that does not interact
with the GUI.
"""
import argparse
import logging
import distributed_parser
from core.emulator.coreemu import CoreEmu
from core.emulator.emudata import IpPrefixes, NodeOptions
from core.emulator.enumerations import EventTypes, NodeTypes
def parse(name):
parser = argparse.ArgumentParser(description=f"Run {name} example")
parser.add_argument(
"-a",
"--address",
help="local address that distributed servers will use for gre tunneling",
)
parser.add_argument(
"-s", "--server", help="distributed server to use for creating nodes"
)
options = parser.parse_args()
return options
def main(args):
# ip generator for example
prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16")
@ -44,5 +63,5 @@ def main(args):
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
args = distributed_parser.parse(__file__)
args = parse(__file__)
main(args)

View file

@ -1,15 +0,0 @@
import argparse
def parse(name):
parser = argparse.ArgumentParser(description=f"Run {name} example")
parser.add_argument(
"-a",
"--address",
help="local address that distributed servers will use for gre tunneling",
)
parser.add_argument(
"-s", "--server", help="distributed server to use for creating nodes"
)
options = parser.parse_args()
return options

View file

@ -1,11 +1,30 @@
"""
Example for scripting a standalone distributed peer to peer session that does not
interact with the GUI.
"""
import argparse
import logging
import distributed_parser
from core.emulator.coreemu import CoreEmu
from core.emulator.emudata import IpPrefixes, NodeOptions
from core.emulator.enumerations import EventTypes
def parse(name):
parser = argparse.ArgumentParser(description=f"Run {name} example")
parser.add_argument(
"-a",
"--address",
help="local address that distributed servers will use for gre tunneling",
)
parser.add_argument(
"-s", "--server", help="distributed server to use for creating nodes"
)
options = parser.parse_args()
return options
def main(args):
# ip generator for example
prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16")
@ -44,5 +63,5 @@ def main(args):
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
args = distributed_parser.parse(__file__)
args = parse(__file__)
main(args)

View file

@ -1,11 +1,30 @@
"""
Example for scripting a standalone distributed switch session that does not
interact with the GUI.
"""
import argparse
import logging
import distributed_parser
from core.emulator.coreemu import CoreEmu
from core.emulator.emudata import IpPrefixes, NodeOptions
from core.emulator.enumerations import EventTypes, NodeTypes
def parse(name):
parser = argparse.ArgumentParser(description=f"Run {name} example")
parser.add_argument(
"-a",
"--address",
help="local address that distributed servers will use for gre tunneling",
)
parser.add_argument(
"-s", "--server", help="distributed server to use for creating nodes"
)
options = parser.parse_args()
return options
def main(args):
# ip generator for example
prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16")
@ -48,5 +67,5 @@ def main(args):
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
args = distributed_parser.parse(__file__)
args = parse(__file__)
main(args)

View file

@ -1,14 +1,22 @@
import datetime
"""
This is a standalone script to run a small EMANE scenario and will not interact
with the GUI. You also must have installed OSPF MDR as noted in the documentation
installation page.
"""
import logging
import parser
import time
from core.emane.ieee80211abg import EmaneIeee80211abgModel
from core.emulator.coreemu import CoreEmu
from core.emulator.emudata import IpPrefixes, NodeOptions
from core.emulator.enumerations import EventTypes, NodeTypes
NODES = 2
EMANE_DELAY = 10
def example(args):
def main():
# ip generator for example
prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16")
@ -19,16 +27,17 @@ def example(args):
# must be in configuration state for nodes to start, when using "node_add" below
session.set_state(EventTypes.CONFIGURATION_STATE)
# create emane network node
# create emane network node, emane determines connectivity based on
# location, so the session and nodes must be configured to provide one
session.set_location(47.57917, -122.13232, 2.00000, 1.0)
options = NodeOptions()
options.set_position(80, 50)
emane_network = session.add_node(_type=NodeTypes.EMANE, options=options)
emane_network = session.add_node(_type=NodeTypes.EMANE, options=options, _id=100)
session.emane.set_model(emane_network, EmaneIeee80211abgModel)
# create nodes
options = NodeOptions(model="mdr")
for i in range(args.nodes):
for i in range(NODES):
node = session.add_node(options=options)
node.setposition(x=150 * (i + 1), y=150)
interface = prefixes.create_interface(node)
@ -37,21 +46,22 @@ def example(args):
# instantiate session
session.instantiate()
# OSPF MDR requires some time for routes to be created
logging.info("waiting %s seconds for OSPF MDR to create routes", EMANE_DELAY)
time.sleep(EMANE_DELAY)
# get nodes to run example
first_node = session.get_node(1)
last_node = session.get_node(NODES)
address = prefixes.ip4_address(first_node)
logging.info("node %s pinging %s", last_node.name, address)
output = last_node.cmd(f"ping -c 3 {address}")
logging.info(output)
# shutdown session
input("press enter to exit...")
coreemu.shutdown()
def main():
logging.basicConfig(level=logging.INFO)
args = parser.parse("emane80211")
start = datetime.datetime.now()
logging.info(
"running emane 80211 example: nodes(%s) time(%s)", args.nodes, args.time
)
example(args)
logging.info("elapsed time: %s", datetime.datetime.now() - start)
if __name__ == "__main__" or __name__ == "__builtin__":
logging.basicConfig(level=logging.INFO)
main()

View file

@ -1,32 +0,0 @@
import argparse
DEFAULT_NODES = 2
DEFAULT_TIME = 10
DEFAULT_STEP = 1
def parse(name):
parser = argparse.ArgumentParser(description=f"Run {name} example")
parser.add_argument(
"-n",
"--nodes",
type=int,
default=DEFAULT_NODES,
help="number of nodes to create in this example",
)
parser.add_argument(
"-c",
"--count",
type=int,
default=DEFAULT_TIME,
help="number of time to ping node",
)
args = parser.parse_args()
if args.nodes < 2:
parser.error(f"invalid min number of nodes: {args.nodes}")
if args.count < 1:
parser.error(f"invalid ping count({args.count}), count must be greater than 0")
return args

View file

@ -1,13 +1,18 @@
import logging
import time
"""
This is a standalone script to run a small switch based scenario and will not
interact with the GUI.
"""
import logging
import params
from core.emulator.coreemu import CoreEmu
from core.emulator.emudata import IpPrefixes
from core.emulator.enumerations import EventTypes, NodeTypes
NODES = 2
def example(args):
def main():
# ip generator for example
prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16")
@ -19,10 +24,10 @@ def example(args):
session.set_state(EventTypes.CONFIGURATION_STATE)
# create switch network node
switch = session.add_node(_type=NodeTypes.SWITCH)
switch = session.add_node(_type=NodeTypes.SWITCH, _id=100)
# create nodes
for _ in range(args.nodes):
for _ in range(NODES):
node = session.add_node()
interface = prefixes.create_interface(node)
session.add_link(node.id, switch.id, interface_one=interface)
@ -31,27 +36,17 @@ def example(args):
session.instantiate()
# get nodes to run example
first_node = session.get_node(2)
last_node = session.get_node(args.nodes + 1)
first_node_address = prefixes.ip4_address(first_node)
logging.info("node %s pinging %s", last_node.name, first_node_address)
output = last_node.cmd(f"ping -c {args.count} {first_node_address}")
first_node = session.get_node(1)
last_node = session.get_node(NODES)
address = prefixes.ip4_address(first_node)
logging.info("node %s pinging %s", last_node.name, address)
output = last_node.cmd(f"ping -c 3 {address}")
logging.info(output)
# shutdown session
coreemu.shutdown()
def main():
logging.basicConfig(level=logging.INFO)
args = params.parse("switch")
start = time.perf_counter()
logging.info(
"running switch example: nodes(%s) ping count(%s)", args.nodes, args.count
)
example(args)
logging.info("elapsed time: %s", time.perf_counter() - start)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
main()

View file

@ -1,10 +1,19 @@
"""
This is a script to run a small switch based scenario and depends on
the user running this script through the "Execute Python Script" option
in the GUI. The usage of globals() below allows this script to leverage the
same CoreEmu instance the GUI is using.
"""
import logging
from core.emulator.emudata import IpPrefixes
from core.emulator.enumerations import EventTypes, NodeTypes
NODES = 2
def example(nodes):
def main():
# ip generator for example
prefixes = IpPrefixes("10.83.0.0/16")
@ -19,7 +28,7 @@ def example(nodes):
switch = session.add_node(_type=NodeTypes.SWITCH)
# create nodes
for _ in range(nodes):
for _ in range(NODES):
node = session.add_node()
interface = prefixes.create_interface(node)
session.add_link(node.id, switch.id, interface_one=interface)
@ -30,4 +39,4 @@ def example(nodes):
if __name__ in {"__main__", "__builtin__"}:
logging.basicConfig(level=logging.INFO)
example(2)
main()

View file

@ -1,14 +1,19 @@
import logging
import time
"""
This is a standalone script to run a small WLAN based scenario and will not
interact with the GUI.
"""
import logging
import params
from core.emulator.coreemu import CoreEmu
from core.emulator.emudata import IpPrefixes, NodeOptions
from core.emulator.enumerations import EventTypes, NodeTypes
from core.location.mobility import BasicRangeModel
NODES = 2
def example(args):
def main():
# ip generator for example
prefixes = IpPrefixes("10.83.0.0/16")
@ -20,13 +25,13 @@ def example(args):
session.set_state(EventTypes.CONFIGURATION_STATE)
# create wlan network node
wlan = session.add_node(_type=NodeTypes.WIRELESS_LAN)
wlan = session.add_node(_type=NodeTypes.WIRELESS_LAN, _id=100)
session.mobility.set_model(wlan, BasicRangeModel)
# create nodes, must set a position for wlan basic range model
options = NodeOptions(model="mdr")
options.set_position(0, 0)
for _ in range(args.nodes):
for _ in range(NODES):
node = session.add_node(options=options)
interface = prefixes.create_interface(node)
session.add_link(node.id, wlan.id, interface_one=interface)
@ -35,27 +40,17 @@ def example(args):
session.instantiate()
# get nodes for example run
first_node = session.get_node(2)
last_node = session.get_node(args.nodes + 1)
first_node = session.get_node(1)
last_node = session.get_node(NODES)
address = prefixes.ip4_address(first_node)
logging.info("node %s pinging %s", last_node.name, address)
output = last_node.cmd(f"ping -c {args.count} {address}")
output = last_node.cmd(f"ping -c 3 {address}")
logging.info(output)
# shutdown session
coreemu.shutdown()
def main():
logging.basicConfig(level=logging.INFO)
args = params.parse("wlan")
start = time.perf_counter()
logging.info(
"running wlan example: nodes(%s) ping count(%s)", args.nodes, args.count
)
example(args)
logging.info("elapsed time: %s", time.perf_counter() - start)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
main()

View file

@ -686,6 +686,9 @@ message Link {
Interface interface_one = 4;
Interface interface_two = 5;
LinkOptions options = 6;
int32 network_id = 7;
string label = 8;
string color = 9;
}
message LinkOptions {

View file

@ -9,7 +9,7 @@ from core.gui.images import Images
if __name__ == "__main__":
# parse flags
parser = argparse.ArgumentParser(description=f"CORE Python Tk GUI")
parser = argparse.ArgumentParser(description=f"CORE Python GUI")
parser.add_argument("-l", "--level", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], default="INFO",
help="logging level")
parser.add_argument("-p", "--proxy", action="store_true", help="enable proxy")

View file

@ -4,6 +4,7 @@ import enum
import select
import socket
import subprocess
import sys
import time
from argparse import ArgumentDefaultsHelpFormatter
from functools import cmp_to_key
@ -11,6 +12,8 @@ from queue import Queue
from threading import Thread
from typing import Dict, Tuple
import grpc
from core import utils
from core.api.grpc.client import CoreGrpcClient
from core.api.grpc.core_pb2 import NodeType
@ -56,14 +59,26 @@ class SdtClient:
class RouterMonitor:
def __init__(self, src_id: str, src: str, dst: str, pkt: str,
sdt_host: str, sdt_port: int) -> None:
def __init__(
self,
session: int,
src: str,
dst: str,
pkt: str,
rate: int,
dead: int,
sdt_host: str,
sdt_port: int,
) -> None:
self.queue = Queue()
self.core = CoreGrpcClient()
self.src_id = src_id
self.session = session
self.src_id = None
self.src = src
self.dst = dst
self.pkt = pkt
self.rate = rate
self.dead = dead
self.seen = {}
self.running = False
self.route_time = None
@ -71,23 +86,46 @@ class RouterMonitor:
self.sdt = SdtClient((sdt_host, sdt_port))
self.nodes = self.get_nodes()
def get_nodes(self) -> Dict[str, str]:
nodes = {}
def get_nodes(self) -> Dict[int, str]:
with self.core.context_connect():
response = self.core.get_sessions()
sessions = response.sessions
session = None
if sessions:
session = sessions[0]
if not session:
raise Exception("no current core sessions")
print("session: ", session.dir)
response = self.core.get_session(session.id)
for node in response.session.nodes:
if node.type != NodeType.DEFAULT:
continue
nodes[node.id] = node.channel
return nodes
if self.session is None:
self.session = self.get_session()
print("session: ", self.session)
try:
response = self.core.get_session(self.session)
nodes = response.session.nodes
node_map = {}
for node in nodes:
if node.type != NodeType.DEFAULT:
continue
node_map[node.id] = node.channel
if self.src_id is None:
response = self.core.get_node(self.session, node.id)
for netif in response.interfaces:
if self.src == netif.ip4:
self.src_id = node.id
break
except grpc.RpcError:
print(f"invalid session: {self.session}")
sys.exit(1)
if self.src_id is None:
print(f"could not find node with source address: {self.src}")
sys.exit(1)
print(
f"monitoring src_id ({self.src_id}) src({self.src}) dst({self.dst}) pkt({self.pkt})"
)
return node_map
def get_session(self) -> int:
response = self.core.get_sessions()
sessions = response.sessions
session = None
if sessions:
session = sessions[0]
if not session:
print("no current core sessions")
sys.exit(1)
return session.id
def start(self) -> None:
self.running = True
@ -107,7 +145,7 @@ class RouterMonitor:
elif node in self.seen:
del self.seen[node]
if (time.monotonic() - self.route_time) >= ROUTE_TIME:
if (time.monotonic() - self.route_time) >= self.rate:
self.manage_routes()
self.route_time = time.monotonic()
@ -125,9 +163,9 @@ class RouterMonitor:
self.sdt.delete_links()
if not self.seen:
return
values = sorted(self.seen.items(),
key=cmp_to_key(self.route_sort),
reverse=True)
values = sorted(
self.seen.items(), key=cmp_to_key(self.route_sort), reverse=True
)
print("current route:")
for index, node_data in enumerate(values):
next_index = index + 1
@ -147,12 +185,11 @@ class RouterMonitor:
self.listeners.clear()
def listen(self, node_id, node) -> None:
cmd = (
f"tcpdump -lnv src host {self.src} and dst host {self.dst} and {self.pkt}"
)
cmd = f"tcpdump -lnvi any src host {self.src} and dst host {self.dst} and {self.pkt}"
node_cmd = f"vcmd -c {node} -- {cmd}"
p = subprocess.Popen(node_cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL)
p = subprocess.Popen(
node_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
)
current = time.monotonic()
try:
while not p.poll() and self.running:
@ -166,7 +203,7 @@ class RouterMonitor:
self.queue.put((RouteEnum.ADD, node_id, ttl))
current = time.monotonic()
else:
if (time.monotonic() - current) >= DEAD_TIME:
if (time.monotonic() - current) >= self.dead:
self.queue.put((RouteEnum.DEL, node_id, None))
except Exception as e:
print(f"listener error: {e}")
@ -177,27 +214,40 @@ def main() -> None:
print("core-route-monitor requires tcpdump to be installed")
return
desc = "core route monitor leverages tcpdump to monitor traffic and find route using TTL"
parser = argparse.ArgumentParser(
description="core route monitor",
formatter_class=ArgumentDefaultsHelpFormatter,
description=desc, formatter_class=ArgumentDefaultsHelpFormatter
)
parser.add_argument(
"--src", required=True, help="source address for route monitoring"
)
parser.add_argument(
"--dst", required=True, help="destination address for route monitoring"
)
parser.add_argument("--session", type=int, help="session to monitor route")
parser.add_argument(
"--pkt", default="icmp", choices=PACKET_CHOICES, help="packet type"
)
parser.add_argument(
"--rate", type=int, default=ROUTE_TIME, help="rate to update route, in seconds"
)
parser.add_argument(
"--dead",
type=int,
default=DEAD_TIME,
help="timeout to declare path dead, in seconds",
)
parser.add_argument("--id", required=True,
help="source node id for determining path")
parser.add_argument("--src", default="10.0.0.20",
help="source address for route monitoring")
parser.add_argument("--dst", default="10.0.2.20",
help="destination address for route monitoring")
parser.add_argument("--pkt", default="icmp", choices=PACKET_CHOICES,
help="packet type")
parser.add_argument("--sdt-host", default=SDT_HOST, help="sdt host address")
parser.add_argument("--sdt-port", type=int, default=SDT_PORT, help="sdt port")
args = parser.parse_args()
monitor = RouterMonitor(
args.id,
args.session,
args.src,
args.dst,
args.pkt,
args.rate,
args.dead,
args.sdt_host,
args.sdt_port,
)

View file

@ -17,12 +17,17 @@ class TestXml:
:param session: session for test
:param tmpdir: tmpdir to create data in
"""
# create hook
# create hooks
file_name = "runtime_hook.sh"
data = "#!/bin/sh\necho hello"
state = EventTypes.RUNTIME_STATE
session.add_hook(state, file_name, None, data)
file_name = "instantiation_hook.sh"
data = "#!/bin/sh\necho hello"
state = EventTypes.INSTANTIATION_STATE
session.add_hook(state, file_name, None, data)
# save xml
xml_file = tmpdir.join("session.xml")
file_path = xml_file.strpath

View file

@ -89,7 +89,7 @@ Commands below can be used to run the core-daemon, the new core gui, and tests.
sudo python3 -m pipenv run core
# runs coretk gui
python3 -m pipenv run coretk
python3 -m pipenv run core-pygui
# runs mocked unit tests
python3 -m pipenv run test-mock

View file

@ -50,10 +50,27 @@ can also subscribe to EMANE location events and move the nodes on the canvas
as they are moved in the EMANE emulation. This would occur when an Emulation
Script Generator, for example, is running a mobility script.
## EMANE Installation
EMANE can be installed from deb or RPM packages or from source. See the
[EMANE GitHub](https://github.com/adjacentlink/emane) for full details.
Here are quick instructions for installing all EMANE packages for Ubuntu 18.04:
```shell
# install dependencies
sudo apt-get install libssl-dev libxml-libxml-perl libxml-simple-perl
wget https://adjacentlink.com/downloads/emane/emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz
tar xzf emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz
# install base emane packages
sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/emane*.deb
# install python3 bindings
sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/python3*.deb
```
## EMANE Configuration
The CORE configuration file */etc/core/core.conf* has options specific to
EMANE. An example emane section from the *core.conf* file is shown below:
The CORE configuration file **/etc/core/core.conf** has options specific to
EMANE. An example emane section from the **core.conf** file is shown below:
```shell
# EMANE configuration
@ -64,40 +81,28 @@ emane_event_monitor = False
# EMANE log level range [0,4] default: 2
emane_log_level = 2
emane_realtime = True
```
EMANE can be installed from deb or RPM packages or from source. See the
[EMANE GitHub](https://github.com/adjacentlink/emane) for full details.
Here are quick instructions for installing all EMANE packages:
```shell
# install dependencies
sudo apt-get install libssl-dev libxml-libxml-perl libxml-simple-perl
wget https://adjacentlink.com/downloads/emane/emane-1.2.1-release-1.ubuntu-16_04.amd64.tar.gz
tar xzf emane-1.2.1-release-1.ubuntu-16_04.amd64.tar.gz
sudo dpkg -i emane-1.2.1-release-1/deb/ubuntu-16_04/amd64/*.deb
# prefix used for emane installation
# emane_prefix = /usr
```
If you have an EMANE event generator (e.g. mobility or pathloss scripts) and
want to have CORE subscribe to EMANE location events, set the following line
in the */etc/core/core.conf* configuration file:
in the **core.conf** configuration file.
> **NOTE:** Do not set this option to True if you want to manually drag nodes around
on the canvas to update their location in EMANE.
```shell
emane_event_monitor = True
```
Do not set the above option to True if you want to manually drag nodes around
on the canvas to update their location in EMANE.
Another common issue is if installing EMANE from source, the default configure
prefix will place the DTD files in */usr/local/share/emane/dtd* while CORE
expects them in */usr/share/emane/dtd*.
A symbolic link will fix this:
prefix will place the DTD files in **/usr/local/share/emane/dtd** while CORE
expects them in **/usr/share/emane/dtd**.
Update the EMANE prefix configuration to resolve this problem.
```shell
sudo ln -s /usr/local/share/emane /usr/share/emane
emane_prefix = /usr/local
```
## Custom EMANE Models

View file

@ -23,6 +23,7 @@ networking scenarios, security studies, and increasing the size of physical test
|[Architecture](architecture.md)|Overview of the architecture|
|[Installation](install.md)|How to install CORE and its requirements|
|[GUI](gui.md)|How to use the GUI|
|[(BETA) Python GUI](pygui.md)|How to use the BETA python based GUI|
|[Distributed](distributed.md)|Details for running CORE across multiple servers|
|[Python Scripting](scripting.md)|How to write python scripts for creating a CORE session|
|[gRPC API](grpc.md)|How to enable and use the gRPC API|

View file

@ -6,6 +6,11 @@
## Overview
This section will describe how to install CORE from source or from a pre-built package.
CORE has been vetted on Ubuntu 18 and CentOS 7.6. Other versions and distributions
can work, assuming you can get the required packages and versions similar to those
noted below for the tested distributions.
> **NOTE:** iproute2 4.5+ is a requirement for bridge related commands
## Required Hardware

653
docs/pygui.md Normal file
View file

@ -0,0 +1,653 @@
# (BETA) Python GUI
* Table of Contents
{:toc}
![](static/core-pygui.png)
## Overview
The GUI is used to draw nodes and network devices on a canvas, linking them
together to create an emulated network session.
After pressing the start button, CORE will proceed through these phases,
staying in the **runtime** phase. After the session is stopped, CORE will
proceed to the **data collection** phase before tearing down the emulated
state.
CORE can be customized to perform any action at each state. See the
**Hooks...** entry on the [Session Menu](#session-menu) for details about
when these session states are reached.
## Prerequisites
Beyond installing CORE, you must have the CORE daemon running. This is done
on the command line with either systemd or sysv.
```shell
# systemd service
sudo systemctl daemon-reload
sudo systemctl start core-daemon
# sysv service
sudo service core-daemon start
# direct invocation
sudo core-daemon
```
## GUI Files
> **NOTE:** Previously the BETA GUI placed files under ~/.coretk, this has been
> updated to be ~/.coregui. The prior config file named ~/.coretk/gui.yaml is
> also now known as ~/.coregui/config.yaml and has a slightly different format
The GUI will create a directory in your home directory on first run called
~/.coregui. This directory will help layout various files that the GUI may use.
* .coregui/
* backgrounds/
* place backgrounds used for display in the GUI
* custom_emane/
* place to keep custom emane models to use with the core-daemon
* custom_services/
* place to keep custom services to use with the core-daemon
* icons/
* icons the GUI uses along with customs icons desired
* mobility/
* place to keep custom mobility files
* scripts/
* place to keep core related scripts
* xmls/
* place to keep saved session xml files
* gui.log
* log file when running the gui, look here when issues occur for exceptions etc
* config.yaml
* configuration file used to save/load various gui related settings (custom nodes, layouts, addresses, etc)
## Modes of Operation
The CORE GUI has two primary modes of operation, **Edit** and **Execute**
modes. Running the GUI, by typing **core-pygui** with no options, starts in
Edit mode. Nodes are drawn on a blank canvas using the toolbar on the left
and configured from right-click menus or by double-clicking them. The GUI
does not need to be run as root.
Once editing is complete, pressing the green **Start** button instantiates
the topology and enters Execute mode. In execute mode,
the user can interact with the running emulated machines by double-clicking or
right-clicking on them. The editing toolbar disappears and is replaced by an
execute toolbar, which provides tools while running the emulation. Pressing
the red **Stop** button will destroy the running emulation and return CORE
to Edit mode.
Once the emulation is running, the GUI can be closed, and a prompt will appear
asking if the emulation should be terminated. The emulation may be left
running and the GUI can reconnect to an existing session at a later time.
The GUI can be run as a normal user on Linux.
The python GUI currently provides the following options on startup.
```shell
usage: core-pygui [-h] [-l {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [-p]
CORE Python GUI
optional arguments:
-h, --help show this help message and exit
-l {DEBUG,INFO,WARNING,ERROR,CRITICAL}, --level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
logging level
-p, --proxy enable proxy
```
## Toolbar
The toolbar is a row of buttons that runs vertically along the left side of the
CORE GUI window. The toolbar changes depending on the mode of operation.
### Editing Toolbar
When CORE is in Edit mode (the default), the vertical Editing Toolbar exists on
the left side of the CORE window. Below are brief descriptions for each toolbar
item, starting from the top. Most of the tools are grouped into related
sub-menus, which appear when you click on their group icon.
| Icon | Name | Description |
|---|---|---|
| ![](static/pygui/select.png) | Selection Tool | Tool for selecting, moving, configuring nodes. |
| ![](static/pygui/start.png) | Start Button | Starts Execute mode, instantiates the emulation. |
| ![](static/pygui/link.png) | Link | Allows network links to be drawn between two nodes by clicking and dragging the mouse. |
### CORE Nodes
These nodes will create a new node container and run associated services.
| Icon | Name | Description |
|---|---|---|
| ![](static/pygui/router.png) | Router | Runs Quagga OSPFv2 and OSPFv3 routing to forward packets. |
| ![](static/pygui/host.png) | Host | Emulated server machine having a default route, runs SSH server. |
| ![](static/pygui/pc.png) | PC | Basic emulated machine having a default route, runs no processes by default. |
| ![](static/pygui/mdr.png) | MDR | Runs Quagga OSPFv3 MDR routing for MANET-optimized routing. |
| ![](static/pygui/router.png) | PRouter | Physical router represents a real testbed machine. |
### Network Nodes
These nodes are mostly used to create a Linux bridge that serves the
purpose described below.
| Icon | Name | Description |
|---|---|---|
| ![](static/pygui/hub.png) | Hub | Ethernet hub forwards incoming packets to every connected node. |
| ![](static/pygui/lanswitch.png) | Switch | Ethernet switch intelligently forwards incoming packets to attached hosts using an Ethernet address hash table. |
| ![](static/pygui/wlan.png) | Wireless LAN | When routers are connected to this WLAN node, they join a wireless network and an antenna is drawn instead of a connecting line; the WLAN node typically controls connectivity between attached wireless nodes based on the distance between them. |
| ![](static/pygui/rj45.png) | RJ45 | RJ45 Physical Interface Tool, emulated nodes can be linked to real physical interfaces; using this tool, real networks and devices can be physically connected to the live-running emulation. |
| ![](static/pygui/tunnel.png) | Tunnel | Tool allows connecting together more than one CORE emulation using GRE tunnels. |
### Annotation Tools
| Icon | Name | Description |
|---|---|---|
| ![](static/pygui/marker.png) | Marker | For drawing marks on the canvas. |
| ![](static/pygui/oval.png) | Oval | For drawing circles on the canvas that appear in the background. |
| ![](static/pygui/rectangle.png) | Rectangle | For drawing rectangles on the canvas that appear in the background. |
| ![](static/pygui/text.png) | Text | For placing text captions on the canvas. |
### Execution Toolbar
When the Start button is pressed, CORE switches to Execute mode, and the Edit
toolbar on the left of the CORE window is replaced with the Execution toolbar
Below are the items on this toolbar, starting from the top.
| Icon | Name | Description |
|---|---|---|
| ![](static/pygui/stop.png) | Stop Button | Stops Execute mode, terminates the emulation, returns CORE to edit mode. |
| ![](static/pygui/select.png) | Selection Tool | In Execute mode, the Selection Tool can be used for moving nodes around the canvas, and double-clicking on a node will open a shell window for that node; right-clicking on a node invokes a pop-up menu of run-time options for that node. |
| ![](static/pygui/marker.png) | Marker | For drawing freehand lines on the canvas, useful during demonstrations; markings are not saved. |
| ![](static/pygui/run.png) | Run Tool | This tool allows easily running a command on all or a subset of all nodes. A list box allows selecting any of the nodes. A text entry box allows entering any command. The command should return immediately, otherwise the display will block awaiting response. The *ping* command, for example, with no parameters, is not a good idea. The result of each command is displayed in a results box. The first occurrence of the special text "NODE" will be replaced with the node name. The command will not be attempted to run on nodes that are not routers, PCs, or hosts, even if they are selected. |
## Menu
The menubar runs along the top of the CORE GUI window and provides access to a
variety of features. Some of the menus are detachable, such as the *Widgets*
menu, by clicking the dashed line at the top.
### File Menu
The File menu contains options for manipulating the **.imn** Configuration
Files. Generally, these menu items should not be used in Execute mode.
| Option | Description |
|---|---|
| New Session | This starts a new session with an empty canvas. |
| Save | Saves the current topology. If you have not yet specified a file name, the Save As dialog box is invoked. |
| Save As | Invokes the Save As dialog box for selecting a new **.xml** file for saving the current configuration in the XML file. |
| Open | Invokes the File Open dialog box for selecting a new XML file to open. |
| Recently used files | Above the Quit menu command is a list of recently use files, if any have been opened. You can clear this list in the Preferences dialog box. You can specify the number of files to keep in this list from the Preferences dialog. Click on one of the file names listed to open that configuration file. |
| Execute Python Script | Invokes a File Open dialog box for selecting a Python script to run and automatically connect to. After a selection is made, a Python Script Options dialog box is invoked to allow for command-line options to be added. The Python script must create a new CORE Session and add this session to the daemon's list of sessions in order for this to work. |
| Quit | The Quit command should be used to exit the CORE GUI. CORE may prompt for termination if you are currently in Execute mode. Preferences and the recently-used files list are saved. |
### Edit Menu
| Option | Description |
|---|---|
| Preferences | Invokes the Preferences dialog box. |
| Custom Nodes | Custom node creation dialog box. |
| Undo | (Disabled) Attempts to undo the last edit in edit mode. |
| Redo | (Disabled) Attempts to redo an edit that has been undone. |
| Cut, Copy, Paste, Delete | Used to cut, copy, paste, and delete a selection. When nodes are pasted, their node numbers are automatically incremented, and existing links are preserved with new IP addresses assigned. Services and their customizations are copied to the new node, but care should be taken as node IP addresses have changed with possibly old addresses remaining in any custom service configurations. Annotations may also be copied and pasted.
### Canvas Menu
The canvas menu provides commands related to the editing canvas.
| Option | Description |
|---|---|
| Size/scale | Invokes a Canvas Size and Scale dialog that allows configuring the canvas size, scale, and geographic reference point. The size controls allow changing the width and height of the current canvas, in pixels or meters. The scale allows specifying how many meters are equivalent to 100 pixels. The reference point controls specify the latitude, longitude, and altitude reference point used to convert between geographic and Cartesian coordinate systems. By clicking the *Save as default* option, all new canvases will be created with these properties. The default canvas size can also be changed in the Preferences dialog box.
| Wallpaper | Used for setting the canvas background image. |
### View Menu
The View menu features items for toggling on and off their display on the canvas.
| Option | Description |
|---|---|
| Interface Names | Display interface names on links. |
| IPv4 Addresses | Display IPv4 addresses on links. |
| IPv6 Addresses | Display IPv6 addresses on links. |
| Node Labels | Display node names. |
| Link Labels | Display link labels. |
| Annotations | Display annotations. |
| Canvas Grid | Display the canvas grid. |
### Tools Menu
The tools menu lists different utility functions.
| Option | Description |
|---|---|
| Find | Display find dialog used for highlighting a node on the canvas. |
| Auto Grid | Automatically layout nodes in a grid. |
| IP addresses | Invokes the IP Addresses dialog box for configuring which IPv4/IPv6 prefixes are used when automatically addressing new interfaces. |
| MAC addresses | Invokes the MAC Addresses dialog box for configuring the starting number used as the lowest byte when generating each interface MAC address. This value should be changed when tunneling between CORE emulations to prevent MAC address conflicts. |
### Widgets Menu
Widgets are GUI elements that allow interaction with a running emulation.
Widgets typically automate the running of commands on emulated nodes to report
status information of some type and display this on screen.
#### Periodic Widgets
These Widgets are those available from the main *Widgets* menu. More than one
of these Widgets may be run concurrently. An event loop fires once every second
that the emulation is running. If one of these Widgets is enabled, its periodic
routine will be invoked at this time. Each Widget may have a configuration
dialog box which is also accessible from the *Widgets* menu.
Here are some standard widgets:
* **Adjacency** - displays router adjacency states for Quagga's OSPFv2 and OSPFv3
routing protocols. A line is drawn from each router halfway to the router ID
of an adjacent router. The color of the line is based on the OSPF adjacency
state such as Two-way or Full. To learn about the different colors, see the
*Configure Adjacency...* menu item. The **vtysh** command is used to
dump OSPF neighbor information.
Only half of the line is drawn because each
router may be in a different adjacency state with respect to the other.
* **Throughput** - displays the kilobits-per-second throughput above each link,
using statistics gathered from the ng_pipe Netgraph node that implements each
link. If the throughput exceeds a certain threshold, the link will become
highlighted. For wireless nodes which broadcast data to all nodes in range,
the throughput rate is displayed next to the node and the node will become
circled if the threshold is exceeded.
#### Observer Widgets
These Widgets are available from the **Observer Widgets** submenu of the
**Widgets** menu, and from the Widgets Tool on the toolbar. Only one Observer Widget may
be used at a time. Mouse over a node while the session is running to pop up
an informational display about that node.
Available Observer Widgets include IPv4 and IPv6 routing tables, socket
information, list of running processes, and OSPFv2/v3 neighbor information.
Observer Widgets may be edited by the user and rearranged. Choosing
**Widgets->Observer Widgets->Edit Observers** from the Observer Widget menu will
invoke the Observer Widgets dialog. A list of Observer Widgets is displayed along
with up and down arrows for rearranging the list. Controls are available for
renaming each widget, for changing the command that is run during mouse over, and
for adding and deleting items from the list. Note that specified commands should
return immediately to avoid delays in the GUI display. Changes are saved to a
**config.yaml** file in the CORE configuration directory.
### Session Menu
The Session Menu has entries for starting, stopping, and managing sessions,
in addition to global options such as node types, comments, hooks, servers,
and options.
| Option | Description |
|---|---|
| Sessions | Invokes the CORE Sessions dialog box containing a list of active CORE sessions in the daemon. Basic session information such as name, node count, start time, and a thumbnail are displayed. This dialog allows connecting to different sessions, shutting them down, or starting a new session. |
| Servers | Invokes the CORE emulation servers dialog for configuring. |
| Options | Presents per-session options, such as the IPv4 prefix to be used, if any, for a control network the ability to preserve the session directory; and an on/off switch for SDT3D support. |
| Hooks | Invokes the CORE Session Hooks window where scripts may be configured for a particular session state. The session states are defined in the [table](#session-states) below. The top of the window has a list of configured hooks, and buttons on the bottom left allow adding, editing, and removing hook scripts. The new or edit button will open a hook script editing window. A hook script is a shell script invoked on the host (not within a virtual node). |
#### Session States
| State | Description |
|---|---|
| Definition | Used by the GUI to tell the backend to clear any state. |
| Configuration | When the user presses the *Start* button, node, link, and other configuration data is sent to the backend. This state is also reached when the user customizes a service. |
| Instantiation | After configuration data has been sent, just before the nodes are created. |
| Runtime | All nodes and networks have been built and are running. (This is the same state at which the previously-named *global experiment script* was run.)
| Datacollect | The user has pressed the *Stop* button, but before services have been stopped and nodes have been shut down. This is a good time to collect log files and other data from the nodes. |
| Shutdown | All nodes and networks have been shut down and destroyed. |
### Help Menu
| Option | Description |
|---|---|
| CORE Github (www) | Link to the CORE GitHub page. |
| CORE Documentation (www) | Lnk to the CORE Documentation page. |
| About | Invokes the About dialog box for viewing version information. |
## Connecting with Physical Networks
CORE's emulated networks run in real time, so they can be connected to live
physical networks. The RJ45 tool and the Tunnel tool help with connecting to
the real world. These tools are available from the **Link-layer nodes** menu.
When connecting two or more CORE emulations together, MAC address collisions
should be avoided. CORE automatically assigns MAC addresses to interfaces when
the emulation is started, starting with **00:00:00:aa:00:00** and incrementing
the bottom byte. The starting byte should be changed on the second CORE machine
using the **Tools->MAC Addresses** option the menu.
### RJ45 Tool
The RJ45 node in CORE represents a physical interface on the real CORE machine.
Any real-world network device can be connected to the interface and communicate
with the CORE nodes in real time.
The main drawback is that one physical interface is required for each
connection. When the physical interface is assigned to CORE, it may not be used
for anything else. Another consideration is that the computer or network that
you are connecting to must be co-located with the CORE machine.
To place an RJ45 connection, click on the **Link-layer nodes** toolbar and select
the **RJ45 Tool** from the submenu. Click on the canvas near the node you want to
connect to. This could be a router, hub, switch, or WLAN, for example. Now
click on the *Link Tool* and draw a link between the RJ45 and the other node.
The RJ45 node will display "UNASSIGNED". Double-click the RJ45 node to assign a
physical interface. A list of available interfaces will be shown, and one may
be selected by double-clicking its name in the list, or an interface name may
be entered into the text box.
> **NOTE:** When you press the Start button to instantiate your topology, the
interface assigned to the RJ45 will be connected to the CORE topology. The
interface can no longer be used by the system. For example, if there was an
IP address assigned to the physical interface before execution, the address
will be removed and control given over to CORE. No IP address is needed; the
interface is put into promiscuous mode so it will receive all packets and
send them into the emulated world.
Multiple RJ45 nodes can be used within CORE and assigned to the same physical
interface if 802.1x VLANs are used. This allows for more RJ45 nodes than
physical ports are available, but the (e.g. switching) hardware connected to
the physical port must support the VLAN tagging, and the available bandwidth
will be shared.
You need to create separate VLAN virtual devices on the Linux host,
and then assign these devices to RJ45 nodes inside of CORE. The VLANning is
actually performed outside of CORE, so when the CORE emulated node receives a
packet, the VLAN tag will already be removed.
Here are example commands for creating VLAN devices under Linux:
```shell
ip link add link eth0 name eth0.1 type vlan id 1
ip link add link eth0 name eth0.2 type vlan id 2
ip link add link eth0 name eth0.3 type vlan id 3
```
### Tunnel Tool
The tunnel tool builds GRE tunnels between CORE emulations or other hosts.
Tunneling can be helpful when the number of physical interfaces is limited or
when the peer is located on a different network. Also a physical interface does
not need to be dedicated to CORE as with the RJ45 tool.
The peer GRE tunnel endpoint may be another CORE machine or another
host that supports GRE tunneling. When placing a Tunnel node, initially
the node will display "UNASSIGNED". This text should be replaced with the IP
address of the tunnel peer. This is the IP address of the other CORE machine or
physical machine, not an IP address of another virtual node.
> **NOTE:** Be aware of possible MTU (Maximum Transmission Unit) issues with GRE devices. The *gretap* device
has an interface MTU of 1,458 bytes; when joined to a Linux bridge, the
bridge's MTU
becomes 1,458 bytes. The Linux bridge will not perform fragmentation for
large packets if other bridge ports have a higher MTU such as 1,500 bytes.
The GRE key is used to identify flows with GRE tunneling. This allows multiple
GRE tunnels to exist between that same pair of tunnel peers. A unique number
should be used when multiple tunnels are used with the same peer. When
configuring the peer side of the tunnel, ensure that the matching keys are
used.
Here are example commands for building the other end of a tunnel on a Linux
machine. In this example, a router in CORE has the virtual address
**10.0.0.1/24** and the CORE host machine has the (real) address
**198.51.100.34/24**. The Linux box
that will connect with the CORE machine is reachable over the (real) network
at **198.51.100.76/24**.
The emulated router is linked with the Tunnel Node. In the
Tunnel Node configuration dialog, the address **198.51.100.76** is entered, with
the key set to **1**. The gretap interface on the Linux box will be assigned
an address from the subnet of the virtual router node,
**10.0.0.2/24**.
```shell
# these commands are run on the tunnel peer
sudo ip link add gt0 type gretap remote 198.51.100.34 local 198.51.100.76 key 1
sudo ip addr add 10.0.0.2/24 dev gt0
sudo ip link set dev gt0 up
```
Now the virtual router should be able to ping the Linux machine:
```shell
# from the CORE router node
ping 10.0.0.2
```
And the Linux machine should be able to ping inside the CORE emulation:
```shell
# from the tunnel peer
ping 10.0.0.1
```
To debug this configuration, **tcpdump** can be run on the gretap devices, or
on the physical interfaces on the CORE or Linux machines. Make sure that a
firewall is not blocking the GRE traffic.
### Communicating with the Host Machine
The host machine that runs the CORE GUI and/or daemon is not necessarily
accessible from a node. Running an X11 application on a node, for example,
requires some channel of communication for the application to connect with
the X server for graphical display. There are several different ways to
connect from the node to the host and vice versa.
#### Control Network
The quickest way to connect with the host machine through the primary control
network.
With a control network, the host can launch an X11 application on a node.
To run an X11 application on the node, the **SSH** service can be enabled on
the node, and SSH with X11 forwarding can be used from the host to the node.
```shell
# SSH from host to node n5 to run an X11 app
ssh -X 172.16.0.5 xclock
```
Note that the **coresendmsg** utility can be used for a node to send
messages to the CORE daemon running on the host (if the **listenaddr = 0.0.0.0**
is set in the **/etc/core/core.conf** file) to interact with the running
emulation. For example, a node may move itself or other nodes, or change
its icon based on some node state.
#### Other Methods
There are still other ways to connect a host with a node. The RJ45 Tool
can be used in conjunction with a dummy interface to access a node:
```shell
sudo modprobe dummy numdummies=1
```
A **dummy0** interface should appear on the host. Use the RJ45 tool assigned
to **dummy0**, and link this to a node in your scenario. After starting the
session, configure an address on the host.
```shell
sudo ip link show type bridge
# determine bridge name from the above command
# assign an IP address on the same network as the linked node
sudo ip addr add 10.0.1.2/24 dev b.48304.34658
```
In the example shown above, the host will have the address **10.0.1.2** and
the node linked to the RJ45 may have the address **10.0.1.1**.
## Building Sample Networks
### Wired Networks
Wired networks are created using the **Link Tool** to draw a link between two
nodes. This automatically draws a red line representing an Ethernet link and
creates new interfaces on network-layer nodes.
Double-click on the link to invoke the **link configuration** dialog box. Here
you can change the Bandwidth, Delay, Loss, and Duplicate
rate parameters for that link. You can also modify the color and width of the
link, affecting its display.
Link-layer nodes are provided for modeling wired networks. These do not create
a separate network stack when instantiated, but are implemented using Linux bridging.
These are the hub, switch, and wireless LAN nodes. The hub copies each packet from
the incoming link to every connected link, while the switch behaves more like an
Ethernet switch and keeps track of the Ethernet address of the connected peer,
forwarding unicast traffic only to the appropriate ports.
The wireless LAN (WLAN) is covered in the next section.
### Wireless Networks
The wireless LAN node allows you to build wireless networks where moving nodes
around affects the connectivity between them. Connection between a pair of nodes is stronger
when the nodes are closer while connection is weaker when the nodes are further away.
The wireless LAN, or WLAN, node appears as a small cloud. The WLAN offers
several levels of wireless emulation fidelity, depending on your modeling needs.
The WLAN tool can be extended with plug-ins for different levels of wireless
fidelity. The basic on/off range is the default setting available on all
platforms. Other plug-ins offer higher fidelity at the expense of greater
complexity and CPU usage. The availability of certain plug-ins varies depending
on platform. See the table below for a brief overview of wireless model types.
|Model|Type|Supported Platform(s)|Fidelity|Description|
|-----|----|---------------------|--------|-----------|
|Basic|on/off|Linux|Low|Ethernet bridging with ebtables|
|EMANE|Plug-in|Linux|High|TAP device connected to EMANE emulator with pluggable MAC and PHY radio types|
To quickly build a wireless network, you can first place several router nodes
onto the canvas. If you have the
Quagga MDR software installed, it is
recommended that you use the **mdr** node type for reduced routing overhead. Next
choose the **WLAN** from the **Link-layer nodes** submenu. First set the
desired WLAN parameters by double-clicking the cloud icon. Then you can link
all selected right-clicking on the WLAN and choosing **Link to Selected**.
Linking a router to the WLAN causes a small antenna to appear, but no red link
line is drawn. Routers can have multiple wireless links and both wireless and
wired links (however, you will need to manually configure route
redistribution.) The mdr node type will generate a routing configuration that
enables OSPFv3 with MANET extensions. This is a Boeing-developed extension to
Quagga's OSPFv3 that reduces flooding overhead and optimizes the flooding
procedure for mobile ad-hoc (MANET) networks.
The default configuration of the WLAN is set to use the basic range model. Having this model
selected causes **core-daemon** to calculate the distance between nodes based
on screen pixels. A numeric range in screen pixels is set for the wireless
network using the **Range** slider. When two wireless nodes are within range of
each other, a green line is drawn between them and they are linked. Two
wireless nodes that are farther than the range pixels apart are not linked.
During Execute mode, users may move wireless nodes around by clicking and
dragging them, and wireless links will be dynamically made or broken.
The **EMANE Nodes** leverage available EMANE models to use for wireless networking.
See the [EMANE](emane.md) chapter for details on using EMANE.
### Mobility Scripting
CORE has a few ways to script mobility.
| Option | Description |
|---|---|
| ns-2 script | The script specifies either absolute positions or waypoints with a velocity. Locations are given with Cartesian coordinates. |
| CORE API | An external entity can move nodes by sending CORE API Node messages with updated X,Y coordinates; the **coresendmsg** utility allows a shell script to generate these messages. |
| EMANE events | See [EMANE](emane.md) for details on using EMANE scripts to move nodes around. Location information is typically given as latitude, longitude, and altitude. |
For the first method, you can create a mobility script using a text
editor, or using a tool such as [BonnMotion](http://net.cs.uni-bonn.de/wg/cs/applications/bonnmotion/), and associate the script with one of the wireless
using the WLAN configuration dialog box. Click the *ns-2 mobility script...*
button, and set the *mobility script file* field in the resulting *ns2script*
configuration dialog.
Here is an example for creating a BonnMotion script for 10 nodes:
```shell
bm -f sample RandomWaypoint -n 10 -d 60 -x 1000 -y 750
bm NSFile -f sample
# use the resulting 'sample.ns_movements' file in CORE
```
When the Execute mode is started and one of the WLAN nodes has a mobility
script, a mobility script window will appear. This window contains controls for
starting, stopping, and resetting the running time for the mobility script. The
**loop** checkbox causes the script to play continuously. The **resolution** text
box contains the number of milliseconds between each timer event; lower values
cause the mobility to appear smoother but consumes greater CPU time.
The format of an ns-2 mobility script looks like:
```shell
# nodes: 3, max time: 35.000000, max x: 600.00, max y: 600.00
$node_(2) set X_ 144.0
$node_(2) set Y_ 240.0
$node_(2) set Z_ 0.00
$ns_ at 1.00 "$node_(2) setdest 130.0 280.0 15.0"
```
The first three lines set an initial position for node 2. The last line in the
above example causes node 2 to move towards the destination **(130, 280)** at
speed **15**. All units are screen coordinates, with speed in units per second.
The total script time is learned after all nodes have reached their waypoints.
Initially, the time slider in the mobility script dialog will not be
accurate.
Examples mobility scripts (and their associated topology files) can be found
in the **configs/** directory.
## Alerts
The alerts button is located in the bottom right-hand corner
of the status bar in the CORE GUI. This will change colors to indicate one or
more problems with the running emulation. Clicking on the alerts button will invoke the
alerts dialog.
The alerts dialog contains a list of alerts received from
the CORE daemon. An alert has a time, severity level, optional node number,
and source. When the alerts button is red, this indicates one or more fatal
exceptions. An alert with a fatal severity level indicates that one or more
of the basic pieces of emulation could not be created, such as failure to
create a bridge or namespace, or the failure to launch EMANE processes for an
EMANE-based network.
Clicking on an alert displays details for that
exceptio. The exception source is a text string
to help trace where the exception occurred; "service:UserDefined" for example,
would appear for a failed validation command with the UserDefined service.
A button is available at the bottom of the dialog for clearing the exception
list.
## Customizing your Topology's Look
Several annotation tools are provided for changing the way your topology is
presented. Captions may be added with the Text tool. Ovals and rectangles may
be drawn in the background, helpful for visually grouping nodes together.
During live demonstrations the marker tool may be helpful for drawing temporary
annotations on the canvas that may be quickly erased. A size and color palette
appears at the bottom of the toolbar when the marker tool is selected. Markings
are only temporary and are not saved in the topology file.
The basic node icons can be replaced with a custom image of your choice. Icons
appear best when they use the GIF or PNG format with a transparent background.
To change a node's icon, double-click the node to invoke its configuration
dialog and click on the button to the right of the node name that shows the
node's current icon.
A background image for the canvas may be set using the *Wallpaper...* option
from the *Canvas* menu. The image may be centered, tiled, or scaled to fit the
canvas size. An existing terrain, map, or network diagram could be used as a
background, for example, with CORE nodes drawn on top.

Some files were not shown because too many files have changed in this diff Show more