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 ## 2020-04-13 CORE 6.3.0
* Features * Features
* \#424 - added FRR IS-IS service * \#424 - added FRR IS-IS service

View file

@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script. # Process this file with autoconf to produce a configure script.
# this defines the CORE version number, must be static for AC_INIT # this defines the CORE version number, must be static for AC_INIT
AC_INIT(core, 6.3.0) AC_INIT(core, 6.4.0)
# autoconf and automake initialization # autoconf and automake initialization
AC_CONFIG_SRCDIR([netns/version.h.in]) AC_CONFIG_SRCDIR([netns/version.h.in])
@ -43,6 +43,11 @@ AC_ARG_ENABLE([gui],
[build and install the GUI (default is yes)])], [build and install the GUI (default is yes)])],
[], [enable_gui=yes]) [], [enable_gui=yes])
AC_SUBST(enable_gui) 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], AC_ARG_ENABLE([python],
[AS_HELP_STRING([--enable-python[=ARG]], [AS_HELP_STRING([--enable-python[=ARG]],
@ -191,8 +196,7 @@ if [ test "x$enable_daemon" = "xyes" || test "x$enable_vnodedonly" = "xyes" ] ;
fi fi
want_docs=no 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) AC_CHECK_PROG(help2man, help2man, yes, no, $SEARCHPATH)
if test "x$help2man" = "xno" ; then if test "x$help2man" = "xno" ; then
@ -210,21 +214,12 @@ if test "x$enable_docs" = "xyes" ; then
# check for sphinx required during make # check for sphinx required during make
AC_CHECK_PROG(sphinxapi_path, sphinx-apidoc, $as_dir, no, $SEARCHPATH) AC_CHECK_PROG(sphinxapi_path, sphinx-apidoc, $as_dir, no, $SEARCHPATH)
if test "x$sphinxapi_path" = "xno" ; then 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 want_docs=no
fi 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 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], AC_ARG_WITH([startup],
[AS_HELP_STRING([--with-startup=option], [AS_HELP_STRING([--with-startup=option],
[option=systemd,suse,none to install systemd/SUSE init scripts])], [option=systemd,suse,none to install systemd/SUSE init scripts])],

View file

@ -5,7 +5,7 @@ verify_ssl = true
[scripts] [scripts]
core = "python scripts/core-daemon -f data/core.conf -l data/logging.conf" 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 = "pytest -v tests"
test-mock = "pytest -v --mock tests" test-mock = "pytest -v --mock tests"
test-emane = "pytest -v tests/emane" test-emane = "pytest -v tests/emane"

View file

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

View file

@ -3,7 +3,7 @@ from queue import Empty, Queue
from typing import Iterable from typing import Iterable
from core.api.grpc import core_pb2 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 ( from core.emulator.data import (
ConfigData, ConfigData,
EventData, EventData,
@ -40,51 +40,7 @@ def handle_link_event(event: LinkData) -> core_pb2.LinkEvent:
:param event: link data :param event: link data
:return: link event that has message type and link information :return: link event that has message type and link information
""" """
interface_one = None link = convert_link(event)
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,
)
return core_pb2.LinkEvent(message_type=event.message_type.value, link=link) 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.emudata import InterfaceData, LinkOptions, NodeOptions
from core.emulator.enumerations import LinkTypes, NodeTypes from core.emulator.enumerations import LinkTypes, NodeTypes
from core.emulator.session import Session 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.nodes.interface import CoreInterface
from core.services.coreservices import CoreService 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 :param node: node to get links from
:return: [core.api.grpc.core_pb2.Link] :return: protobuf links
""" """
links = [] links = []
for link_data in node.all_link_data(): for link_data in node.all_link_data():
link = convert_link(session, link_data) link = convert_link(link_data)
links.append(link) links.append(link)
return links return links
@ -307,48 +306,35 @@ def parse_emane_model_id(_id: int) -> Tuple[int, int]:
return node_id, interface 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: link to convert
:param link_data:
:return: core protobuf Link :return: core protobuf Link
""" """
interface_one = None interface_one = None
if link_data.interface1_id is not 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( interface_one = core_pb2.Interface(
id=link_data.interface1_id, id=link_data.interface1_id,
name=interface_name, name=link_data.interface1_name,
mac=convert_value(link_data.interface1_mac), mac=convert_value(link_data.interface1_mac),
ip4=convert_value(link_data.interface1_ip4), ip4=convert_value(link_data.interface1_ip4),
ip4mask=link_data.interface1_ip4_mask, ip4mask=link_data.interface1_ip4_mask,
ip6=convert_value(link_data.interface1_ip6), ip6=convert_value(link_data.interface1_ip6),
ip6mask=link_data.interface1_ip6_mask, ip6mask=link_data.interface1_ip6_mask,
) )
interface_two = None interface_two = None
if link_data.interface2_id is not 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( interface_two = core_pb2.Interface(
id=link_data.interface2_id, id=link_data.interface2_id,
name=interface_name, name=link_data.interface2_name,
mac=convert_value(link_data.interface2_mac), mac=convert_value(link_data.interface2_mac),
ip4=convert_value(link_data.interface2_ip4), ip4=convert_value(link_data.interface2_ip4),
ip4mask=link_data.interface2_ip4_mask, ip4mask=link_data.interface2_ip4_mask,
ip6=convert_value(link_data.interface2_ip6), ip6=convert_value(link_data.interface2_ip6),
ip6mask=link_data.interface2_ip6_mask, ip6mask=link_data.interface2_ip6_mask,
) )
options = core_pb2.LinkOptions( options = core_pb2.LinkOptions(
opaque=link_data.opaque, opaque=link_data.opaque,
jitter=link_data.jitter, jitter=link_data.jitter,
@ -362,7 +348,6 @@ def convert_link(session: Session, link_data: LinkData) -> core_pb2.Link:
dup=link_data.dup, dup=link_data.dup,
unidirectional=link_data.unidirectional, unidirectional=link_data.unidirectional,
) )
return core_pb2.Link( return core_pb2.Link(
type=link_data.link_type.value, type=link_data.link_type.value,
node_one_id=link_data.node1_id, 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_one=interface_one,
interface_two=interface_two, interface_two=interface_two,
options=options, 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: for service_exception in boot_exception.args:
exceptions.append(str(service_exception)) exceptions.append(str(service_exception))
return core_pb2.StartSessionResponse(result=False, exceptions=exceptions) return core_pb2.StartSessionResponse(result=False, exceptions=exceptions)
return core_pb2.StartSessionResponse(result=True) return core_pb2.StartSessionResponse(result=True)
def StopSession( def StopSession(
@ -543,7 +542,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
continue continue
node_proto = grpcutils.get_node_proto(session, node) node_proto = grpcutils.get_node_proto(session, node)
nodes.append(node_proto) nodes.append(node_proto)
node_links = get_links(session, node) node_links = get_links(node)
links.extend(node_links) links.extend(node_links)
session_proto = core_pb2.Session( session_proto = core_pb2.Session(
@ -788,7 +787,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("get node links: %s", request) logging.debug("get node links: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
node = self.get_node(session, request.node_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) return core_pb2.GetNodeLinksResponse(links=links)
def AddLink( def AddLink(
@ -1031,8 +1030,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
Retrieve all the default services of all node types in a session Retrieve all the default services of all node types in a session
:param request: :param request: get-default-service request
get-default-service request
:param context: context object :param context: context object
:return: get-service-defaults response about all the available default services :return: get-service-defaults response about all the available default services
""" """
@ -1050,8 +1048,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
) -> SetServiceDefaultsResponse: ) -> SetServiceDefaultsResponse:
""" """
Set new default services to the session after whipping out the old ones 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 :param context: context object
:return: set-service-defaults response :return: set-service-defaults response
""" """
@ -1494,12 +1492,14 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
flag = MessageFlags.ADD flag = MessageFlags.ADD
else: else:
flag = MessageFlags.DELETE flag = MessageFlags.DELETE
color = session.get_link_color(emane_one.id)
link = LinkData( link = LinkData(
message_type=flag, message_type=flag,
link_type=LinkTypes.WIRELESS, link_type=LinkTypes.WIRELESS,
node1_id=node_one.id, node1_id=node_one.id,
node2_id=node_two.id, node2_id=node_two.id,
network_id=emane_one.id, network_id=emane_one.id,
color=color,
) )
session.broadcast_link(link) session.broadcast_link(link)
return EmaneLinkResponse(result=True) return EmaneLinkResponse(result=True)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -76,6 +76,7 @@ NODES = {
} }
NODES_TYPE = {NODES[x]: x for x in NODES} NODES_TYPE = {NODES[x]: x for x in NODES}
CTRL_NET_ID = 9001 CTRL_NET_ID = 9001
LINK_COLORS = ["green", "blue", "orange", "purple", "turquoise"]
class Session: class Session:
@ -105,6 +106,7 @@ class Session:
self.thumbnail = None self.thumbnail = None
self.user = None self.user = None
self.event_loop = EventLoop() self.event_loop = EventLoop()
self.link_colors = {}
# dict of nodes: all nodes and nets # dict of nodes: all nodes and nets
self.node_id_gen = IdGen() self.node_id_gen = IdGen()
@ -355,7 +357,9 @@ class Session:
) )
interface = create_interface(node_one, net_one, interface_one) interface = create_interface(node_one, net_one, interface_one)
node_one_interface = interface 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 # network to node
if node_two and net_one: if node_two and net_one:
@ -366,7 +370,8 @@ class Session:
) )
interface = create_interface(node_two, net_one, interface_two) interface = create_interface(node_two, net_one, interface_two)
node_two_interface = interface 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) link_config(net_one, interface, link_options)
# network to network # network to network
@ -693,6 +698,7 @@ class Session:
# generate name if not provided # generate name if not provided
if not options: if not options:
options = NodeOptions() options = NodeOptions()
options.set_position(0, 0)
name = options.name name = options.name
if not name: if not name:
name = f"{node_class.__name__}{_id}" name = f"{node_class.__name__}{_id}"
@ -807,9 +813,7 @@ class Session:
node.setposition(x, y, None) node.setposition(x, y, None)
node.position.set_geo(lon, lat, alt) node.position.set_geo(lon, lat, alt)
self.broadcast_node(node) self.broadcast_node(node)
else: elif not has_empty_position:
if has_empty_position:
x, y = 0, 0
node.setposition(x, y, None) node.setposition(x, y, None)
def start_mobility(self, node_ids: List[int] = None) -> None: def start_mobility(self, node_ids: List[int] = None) -> None:
@ -927,6 +931,7 @@ class Session:
self.location.reset() self.location.reset()
self.services.reset() self.services.reset()
self.mobility.config_reset() self.mobility.config_reset()
self.link_colors.clear()
def start_events(self) -> None: def start_events(self) -> None:
""" """
@ -1956,3 +1961,17 @@ class Session:
else: else:
node = self.get_node(node_id) node = self.get_node(node_id)
node.cmd(data, wait=False) 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 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 math
import tkinter as tk import tkinter as tk
from tkinter import font, ttk from tkinter import font, ttk
from tkinter.ttk import Progressbar
import grpc
from core.gui import appconfig, themes from core.gui import appconfig, themes
from core.gui.coreclient import CoreClient from core.gui.coreclient import CoreClient
from core.gui.dialogs.error import ErrorDialog
from core.gui.graph.graph import CanvasGraph from core.gui.graph.graph import CanvasGraph
from core.gui.images import ImageEnum, Images from core.gui.images import ImageEnum, Images
from core.gui.menuaction import MenuAction
from core.gui.menubar import Menubar from core.gui.menubar import Menubar
from core.gui.nodeutils import NodeUtils from core.gui.nodeutils import NodeUtils
from core.gui.statusbar import StatusBar from core.gui.statusbar import StatusBar
from core.gui.toolbar import Toolbar from core.gui.toolbar import Toolbar
from core.gui.validation import InputValidation
WIDTH = 1000 WIDTH = 1000
HEIGHT = 800 HEIGHT = 800
class Application(tk.Frame): class Application(ttk.Frame):
def __init__(self, proxy: bool): def __init__(self, proxy: bool) -> None:
super().__init__(master=None) super().__init__()
# load node icons # load node icons
NodeUtils.setup() NodeUtils.setup()
# widgets # widgets
self.menubar = None self.menubar = None
self.toolbar = None self.toolbar = None
self.right_frame = None
self.canvas = None self.canvas = None
self.statusbar = None self.statusbar = None
self.validation = None self.progress = None
# fonts # fonts
self.fonts_size = None self.fonts_size = None
@ -37,7 +41,7 @@ class Application(tk.Frame):
# setup # setup
self.guiconfig = appconfig.read() self.guiconfig = appconfig.read()
self.app_scale = self.guiconfig["scale"] self.app_scale = self.guiconfig.scale
self.setup_scaling() self.setup_scaling()
self.style = ttk.Style() self.style = ttk.Style()
self.setup_theme() self.setup_theme()
@ -46,29 +50,46 @@ class Application(tk.Frame):
self.draw() self.draw()
self.core.setup() 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()} 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) 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) themes.scale_fonts(self.fonts_size, self.app_scale)
self.icon_text_font = font.Font(family="TkIconFont", size=int(12 * text_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) themes.load(self.style)
self.master.bind_class("Menu", "<<ThemeChanged>>", themes.theme_change_menu) self.master.bind_class("Menu", "<<ThemeChanged>>", themes.theme_change_menu)
self.master.bind("<<ThemeChanged>>", themes.theme_change) 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.master.title("CORE")
self.center() self.center()
self.master.protocol("WM_DELETE_WINDOW", self.on_closing) self.master.protocol("WM_DELETE_WINDOW", self.on_closing)
image = Images.get(ImageEnum.CORE, 16) image = Images.get(ImageEnum.CORE, 16)
self.master.tk.call("wm", "iconphoto", self.master._w, image) self.master.tk.call("wm", "iconphoto", self.master._w, image)
self.pack(fill=tk.BOTH, expand=True) self.master.option_add("*tearOff", tk.FALSE)
self.validation = InputValidation(self) 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_width = self.master.winfo_screenwidth()
screen_height = self.master.winfo_screenheight() screen_height = self.master.winfo_screenheight()
x = int((screen_width / 2) - (WIDTH * self.app_scale / 2)) 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}" f"{int(WIDTH * self.app_scale)}x{int(HEIGHT * self.app_scale)}+{x}+{y}"
) )
def draw(self): def draw(self) -> None:
self.master.option_add("*tearOff", tk.FALSE) self.master.rowconfigure(0, weight=1)
self.menubar = Menubar(self.master, self) 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 = 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_canvas()
self.draw_status() self.draw_status()
self.progress = Progressbar(self.right_frame, mode="indeterminate")
self.menubar = Menubar(self.master, self)
def draw_canvas(self): def draw_canvas(self) -> None:
width = self.guiconfig["preferences"]["width"] width = self.guiconfig.preferences.width
height = self.guiconfig["preferences"]["height"] height = self.guiconfig.preferences.height
self.canvas = CanvasGraph(self, self.core, width, height) canvas_frame = ttk.Frame(self.right_frame)
self.canvas.pack(fill=tk.BOTH, expand=True) 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( 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_x.grid(row=1, column=0, sticky="ew")
scroll_y = ttk.Scrollbar(self.canvas, command=self.canvas.yview)
scroll_y.pack(side=tk.RIGHT, fill=tk.Y)
self.canvas.configure(xscrollcommand=scroll_x.set) self.canvas.configure(xscrollcommand=scroll_x.set)
self.canvas.configure(yscrollcommand=scroll_y.set) self.canvas.configure(yscrollcommand=scroll_y.set)
def draw_status(self): def draw_status(self) -> None:
self.statusbar = StatusBar(master=self, app=self) self.statusbar = StatusBar(self.right_frame, self)
self.statusbar.pack(side=tk.BOTTOM, fill=tk.X) self.statusbar.grid(sticky="ew")
def on_closing(self): def show_grpc_exception(self, title: str, e: grpc.RpcError) -> None:
menu_action = MenuAction(self, self.master) logging.exception("app grpc exception", exc_info=e)
menu_action.on_quit() 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) appconfig.save(self.guiconfig)
def joined_session_update(self): def joined_session_update(self) -> None:
self.statusbar.progress_bar.stop()
if self.core.is_runtime(): if self.core.is_runtime():
self.toolbar.set_runtime() self.toolbar.set_runtime()
else: else:
self.toolbar.set_design() self.toolbar.set_design()
def close(self): def close(self) -> None:
self.master.destroy() self.master.destroy()

View file

@ -1,20 +1,20 @@
import os import os
import shutil import shutil
from pathlib import Path from pathlib import Path
from typing import List, Optional
import yaml import yaml
# gui home paths
from core.gui import themes from core.gui import themes
HOME_PATH = Path.home().joinpath(".coretk") HOME_PATH = Path.home().joinpath(".coregui")
BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds") BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds")
CUSTOM_EMANE_PATH = HOME_PATH.joinpath("custom_emane") CUSTOM_EMANE_PATH = HOME_PATH.joinpath("custom_emane")
CUSTOM_SERVICE_PATH = HOME_PATH.joinpath("custom_services") CUSTOM_SERVICE_PATH = HOME_PATH.joinpath("custom_services")
ICONS_PATH = HOME_PATH.joinpath("icons") ICONS_PATH = HOME_PATH.joinpath("icons")
MOBILITY_PATH = HOME_PATH.joinpath("mobility") MOBILITY_PATH = HOME_PATH.joinpath("mobility")
XMLS_PATH = HOME_PATH.joinpath("xmls") 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") LOG_PATH = HOME_PATH.joinpath("gui.log")
SCRIPT_PATH = HOME_PATH.joinpath("scripts") SCRIPT_PATH = HOME_PATH.joinpath("scripts")
@ -44,13 +44,151 @@ class IndentDumper(yaml.Dumper):
return super().increase_indent(flow, False) 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("*"): for current_file in current_path.glob("*"):
new_file = new_path.joinpath(current_file.name) new_file = new_path.joinpath(current_file.name)
shutil.copy(current_file, new_file) shutil.copy(current_file, new_file)
def find_terminal(): def find_terminal() -> Optional[str]:
for term in sorted(TERMINALS): for term in sorted(TERMINALS):
cmd = TERMINALS[term] cmd = TERMINALS[term]
if shutil.which(term): if shutil.which(term):
@ -58,7 +196,7 @@ def find_terminal():
return None return None
def check_directory(): def check_directory() -> None:
if HOME_PATH.exists(): if HOME_PATH.exists():
return return
HOME_PATH.mkdir() HOME_PATH.mkdir()
@ -80,38 +218,16 @@ def check_directory():
editor = EDITORS[0] editor = EDITORS[0]
else: else:
editor = EDITORS[1] editor = EDITORS[1]
config = { preferences = PreferencesConfig(editor, terminal)
"preferences": { config = GuiConfig(preferences=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,
}
save(config) save(config)
def read(): def read() -> GuiConfig:
with CONFIG_PATH.open("r") as f: with CONFIG_PATH.open("r") as f:
return yaml.load(f, Loader=yaml.SafeLoader) return yaml.load(f, Loader=yaml.SafeLoader)
def save(config): def save(config: GuiConfig) -> None:
with CONFIG_PATH.open("w") as f: with CONFIG_PATH.open("w") as f:
yaml.dump(config, f, Dumper=IndentDumper, default_flow_style=False) yaml.dump(config, f, Dumper=IndentDumper, default_flow_style=False)

View file

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

View file

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

View file

@ -15,9 +15,8 @@ if TYPE_CHECKING:
class AlertsDialog(Dialog): class AlertsDialog(Dialog):
def __init__(self, master: "Application", app: "Application"): def __init__(self, app: "Application"):
super().__init__(master, app, "Alerts", modal=True) super().__init__(app, "Alerts")
self.app = app
self.tree = None self.tree = None
self.codetext = None self.codetext = None
self.alarm_map = {} self.alarm_map = {}
@ -93,16 +92,10 @@ class AlertsDialog(Dialog):
frame.grid(sticky="ew") frame.grid(sticky="ew")
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, 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 = ttk.Button(frame, text="Reset", command=self.reset_alerts)
button.grid(row=0, column=0, sticky="ew", padx=PADX) 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 = 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): def reset_alerts(self):
self.codetext.text.delete("1.0", tk.END) self.codetext.text.delete("1.0", tk.END)
@ -110,10 +103,6 @@ class AlertsDialog(Dialog):
self.tree.delete(item) self.tree.delete(item)
self.app.statusbar.core_alarms.clear() self.app.statusbar.core_alarms.clear()
def daemon_log(self):
dialog = DaemonLog(self, self.app)
dialog.show()
def click_select(self, event: tk.Event): def click_select(self, event: tk.Event):
current = self.tree.selection()[0] current = self.tree.selection()[0]
alarm = self.alarm_map[current] alarm = self.alarm_map[current]
@ -121,33 +110,3 @@ class AlertsDialog(Dialog):
self.codetext.text.delete("1.0", "end") self.codetext.text.delete("1.0", "end")
self.codetext.text.insert("1.0", alarm.exception_event.text) self.codetext.text.insert("1.0", alarm.exception_event.text)
self.codetext.text.config(state=tk.DISABLED) 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 tkinter import font, ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from core.gui import validation
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
@ -15,13 +16,12 @@ PIXEL_SCALE = 100
class SizeAndScaleDialog(Dialog): class SizeAndScaleDialog(Dialog):
def __init__(self, master: "Application", app: "Application"): def __init__(self, app: "Application"):
""" """
create an instance for size and scale object 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.canvas = self.app.canvas
self.validation = app.validation
self.section_font = font.Font(weight="bold") self.section_font = font.Font(weight="bold")
width, height = self.canvas.current_dimensions width, height = self.canvas.current_dimensions
self.pixel_width = tk.IntVar(value=width) self.pixel_width = tk.IntVar(value=width)
@ -59,23 +59,11 @@ class SizeAndScaleDialog(Dialog):
frame.columnconfigure(3, weight=1) frame.columnconfigure(3, weight=1)
label = ttk.Label(frame, text="Width") label = ttk.Label(frame, text="Width")
label.grid(row=0, column=0, sticky="w", padx=PADX) label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = ttk.Entry( entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_width)
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.grid(row=0, column=1, sticky="ew", padx=PADX) entry.grid(row=0, column=1, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="x Height") label = ttk.Label(frame, text="x Height")
label.grid(row=0, column=2, sticky="w", padx=PADX) label.grid(row=0, column=2, sticky="w", padx=PADX)
entry = ttk.Entry( entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_height)
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.grid(row=0, column=3, sticky="ew", padx=PADX) entry.grid(row=0, column=3, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="Pixels") label = ttk.Label(frame, text="Pixels")
label.grid(row=0, column=4, sticky="w") label.grid(row=0, column=4, sticky="w")
@ -87,23 +75,11 @@ class SizeAndScaleDialog(Dialog):
frame.columnconfigure(3, weight=1) frame.columnconfigure(3, weight=1)
label = ttk.Label(frame, text="Width") label = ttk.Label(frame, text="Width")
label.grid(row=0, column=0, sticky="w", padx=PADX) label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = ttk.Entry( entry = validation.PositiveFloatEntry(frame, textvariable=self.meters_width)
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.grid(row=0, column=1, sticky="ew", padx=PADX) entry.grid(row=0, column=1, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="x Height") label = ttk.Label(frame, text="x Height")
label.grid(row=0, column=2, sticky="w", padx=PADX) label.grid(row=0, column=2, sticky="w", padx=PADX)
entry = ttk.Entry( entry = validation.PositiveFloatEntry(frame, textvariable=self.meters_height)
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.grid(row=0, column=3, sticky="ew", padx=PADX) entry.grid(row=0, column=3, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="Meters") label = ttk.Label(frame, text="Meters")
label.grid(row=0, column=4, sticky="w") label.grid(row=0, column=4, sticky="w")
@ -118,13 +94,7 @@ class SizeAndScaleDialog(Dialog):
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text=f"{PIXEL_SCALE} Pixels =") label = ttk.Label(frame, text=f"{PIXEL_SCALE} Pixels =")
label.grid(row=0, column=0, sticky="w", padx=PADX) label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = ttk.Entry( entry = validation.PositiveFloatEntry(frame, textvariable=self.scale)
frame,
textvariable=self.scale,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry.grid(row=0, column=1, sticky="ew", padx=PADX) entry.grid(row=0, column=1, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="Meters") label = ttk.Label(frame, text="Meters")
label.grid(row=0, column=2, sticky="w") label.grid(row=0, column=2, sticky="w")
@ -148,24 +118,12 @@ class SizeAndScaleDialog(Dialog):
label = ttk.Label(frame, text="X") label = ttk.Label(frame, text="X")
label.grid(row=0, column=0, sticky="w", padx=PADX) label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = ttk.Entry( entry = validation.PositiveFloatEntry(frame, textvariable=self.x)
frame,
textvariable=self.x,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry.grid(row=0, column=1, sticky="ew", padx=PADX) entry.grid(row=0, column=1, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="Y") label = ttk.Label(frame, text="Y")
label.grid(row=0, column=2, sticky="w", padx=PADX) label.grid(row=0, column=2, sticky="w", padx=PADX)
entry = ttk.Entry( entry = validation.PositiveFloatEntry(frame, textvariable=self.y)
frame,
textvariable=self.y,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry.grid(row=0, column=3, sticky="ew", padx=PADX) entry.grid(row=0, column=3, sticky="ew", padx=PADX)
label = ttk.Label(label_frame, text="Translates To") label = ttk.Label(label_frame, text="Translates To")
@ -179,35 +137,17 @@ class SizeAndScaleDialog(Dialog):
label = ttk.Label(frame, text="Lat") label = ttk.Label(frame, text="Lat")
label.grid(row=0, column=0, sticky="w", padx=PADX) label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = ttk.Entry( entry = validation.FloatEntry(frame, textvariable=self.lat)
frame,
textvariable=self.lat,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry.grid(row=0, column=1, sticky="ew", padx=PADX) entry.grid(row=0, column=1, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="Lon") label = ttk.Label(frame, text="Lon")
label.grid(row=0, column=2, sticky="w", padx=PADX) label.grid(row=0, column=2, sticky="w", padx=PADX)
entry = ttk.Entry( entry = validation.FloatEntry(frame, textvariable=self.lon)
frame,
textvariable=self.lon,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry.grid(row=0, column=3, sticky="ew", padx=PADX) entry.grid(row=0, column=3, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="Alt") label = ttk.Label(frame, text="Alt")
label.grid(row=0, column=4, sticky="w", padx=PADX) label.grid(row=0, column=4, sticky="w", padx=PADX)
entry = ttk.Entry( entry = validation.FloatEntry(frame, textvariable=self.alt)
frame,
textvariable=self.alt,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry.grid(row=0, column=5, sticky="ew") entry.grid(row=0, column=5, sticky="ew")
def draw_save_as_default(self): def draw_save_as_default(self):
@ -241,16 +181,16 @@ class SizeAndScaleDialog(Dialog):
location.alt = self.alt.get() location.alt = self.alt.get()
location.scale = self.scale.get() location.scale = self.scale.get()
if self.save_default.get(): if self.save_default.get():
location_config = self.app.guiconfig["location"] location_config = self.app.guiconfig.location
location_config["x"] = location.x location_config.x = location.x
location_config["y"] = location.y location_config.y = location.y
location_config["z"] = location.z location_config.z = location.z
location_config["lat"] = location.lat location_config.lat = location.lat
location_config["lon"] = location.lon location_config.lon = location.lon
location_config["alt"] = location.alt location_config.alt = location.alt
location_config["scale"] = location.scale location_config.scale = location.scale
preferences = self.app.guiconfig["preferences"] preferences = self.app.guiconfig.preferences
preferences["width"] = width preferences.width = width
preferences["height"] = height preferences.height = height
self.app.save_config() self.app.save_config()
self.destroy() self.destroy()

View file

@ -17,14 +17,13 @@ if TYPE_CHECKING:
class CanvasWallpaperDialog(Dialog): class CanvasWallpaperDialog(Dialog):
def __init__(self, master: "Application", app: "Application"): def __init__(self, app: "Application"):
""" """
create an instance of CanvasWallpaper object 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.canvas = self.app.canvas
self.scale_option = tk.IntVar(value=self.canvas.scale_option.get()) 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.adjust_to_dim = tk.BooleanVar(value=self.canvas.adjust_to_dim.get())
self.filename = tk.StringVar(value=self.canvas.wallpaper_file) self.filename = tk.StringVar(value=self.canvas.wallpaper_file)
self.image_label = None self.image_label = None
@ -103,11 +102,6 @@ class CanvasWallpaperDialog(Dialog):
self.options.append(button) self.options.append(button)
def draw_additional_options(self): 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( checkbutton = ttk.Checkbutton(
self.top, self.top,
text="Adjust canvas size to image dimensions", text="Adjust canvas size to image dimensions",
@ -163,17 +157,13 @@ class CanvasWallpaperDialog(Dialog):
def click_apply(self): def click_apply(self):
self.canvas.scale_option.set(self.scale_option.get()) self.canvas.scale_option.set(self.scale_option.get())
self.canvas.show_grid.set(self.show_grid.get())
self.canvas.adjust_to_dim.set(self.adjust_to_dim.get()) 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() filename = self.filename.get()
if not filename: if not filename:
filename = None filename = None
try: try:
self.canvas.set_wallpaper(filename) self.canvas.set_wallpaper(filename)
except FileNotFoundError: except FileNotFoundError:
logging.error("invalid background: %s", filename) logging.error("invalid background: %s", filename)
self.destroy() self.destroy()

View file

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

View file

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

View file

@ -4,7 +4,7 @@ copy service config dialog
import tkinter as tk import tkinter as tk
from tkinter import ttk 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.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX from core.gui.themes import FRAME_PAD, PADX
@ -15,10 +15,9 @@ if TYPE_CHECKING:
class CopyServiceConfigDialog(Dialog): class CopyServiceConfigDialog(Dialog):
def __init__(self, master: Any, app: "Application", node_id: int): def __init__(self, master: tk.BaseWidget, app: "Application", node_id: int):
super().__init__(master, app, f"Copy services to node {node_id}", modal=True) super().__init__(app, f"Copy services to node {node_id}", master=master)
self.parent = master self.parent = master
self.app = app
self.node_id = node_id self.node_id = node_id
self.service_configs = app.core.service_configs self.service_configs = app.core.service_configs
self.file_configs = app.core.file_configs self.file_configs = app.core.file_configs
@ -171,13 +170,13 @@ class CopyServiceConfigDialog(Dialog):
class ViewConfigDialog(Dialog): class ViewConfigDialog(Dialog):
def __init__( def __init__(
self, self,
master: Any, master: tk.BaseWidget,
app: "Application", app: "Application",
node_id: int, node_id: int,
data: str, data: str,
filename: str = None, 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.data = data
self.service_data = None self.service_data = None
self.filepath = tk.StringVar(value=f"/tmp/services.tmp-n{node_id}-{filename}") 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 import tkinter as tk
from pathlib import Path from pathlib import Path
from tkinter import ttk 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 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.dialogs.dialog import Dialog
from core.gui.images import Images from core.gui.images import Images
from core.gui.nodeutils import NodeDraw from core.gui.nodeutils import NodeDraw
@ -17,8 +17,10 @@ if TYPE_CHECKING:
class ServicesSelectDialog(Dialog): class ServicesSelectDialog(Dialog):
def __init__(self, master: Any, app: "Application", current_services: Set[str]): def __init__(
super().__init__(master, app, "Node Services", modal=True) self, master: tk.BaseWidget, app: "Application", current_services: Set[str]
):
super().__init__(app, "Node Services", master=master)
self.groups = None self.groups = None
self.services = None self.services = None
self.current = None self.current = None
@ -100,8 +102,8 @@ class ServicesSelectDialog(Dialog):
class CustomNodesDialog(Dialog): class CustomNodesDialog(Dialog):
def __init__(self, master: "Application", app: "Application"): def __init__(self, app: "Application"):
super().__init__(master, app, "Custom Nodes", modal=True) super().__init__(app, "Custom Nodes")
self.edit_button = None self.edit_button = None
self.delete_button = None self.delete_button = None
self.nodes_list = None self.nodes_list = None
@ -137,11 +139,11 @@ class CustomNodesDialog(Dialog):
frame.grid(row=0, column=2, sticky="nsew") frame.grid(row=0, column=2, sticky="nsew")
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
entry = ttk.Entry(frame, textvariable=self.name) entry = ttk.Entry(frame, textvariable=self.name)
entry.grid(sticky="ew") entry.grid(sticky="ew", pady=PADY)
self.image_button = ttk.Button( self.image_button = ttk.Button(
frame, text="Icon", compound=tk.LEFT, command=self.click_icon 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 = ttk.Button(frame, text="Services", command=self.click_services)
button.grid(sticky="ew") button.grid(sticky="ew")
@ -199,17 +201,12 @@ class CustomNodesDialog(Dialog):
self.services.update(dialog.current_services) self.services.update(dialog.current_services)
def click_save(self): def click_save(self):
self.app.guiconfig["nodes"].clear() self.app.guiconfig.nodes.clear()
for name in sorted(self.app.core.custom_nodes): for name in self.app.core.custom_nodes:
node_draw = self.app.core.custom_nodes[name] node_draw = self.app.core.custom_nodes[name]
self.app.guiconfig["nodes"].append( custom_node = CustomNode(name, node_draw.image_file, node_draw.services)
{ self.app.guiconfig.nodes.append(custom_node)
"name": name, logging.info("saving custom nodes: %s", self.app.guiconfig.nodes)
"image": node_draw.image_file,
"services": list(node_draw.services),
}
)
logging.info("saving custom nodes: %s", self.app.guiconfig["nodes"])
self.app.save_config() self.app.save_config()
self.destroy() self.destroy()
@ -217,7 +214,8 @@ class CustomNodesDialog(Dialog):
name = self.name.get() name = self.name.get()
if name not in self.app.core.custom_nodes: if name not in self.app.core.custom_nodes:
image_file = Path(self.image_file).stem image_file = 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( logging.info(
"created new custom node (%s), image file (%s), services: (%s)", "created new custom node (%s), image file (%s), services: (%s)",
name, name,

View file

@ -11,8 +11,14 @@ if TYPE_CHECKING:
class Dialog(tk.Toplevel): class Dialog(tk.Toplevel):
def __init__( 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) super().__init__(master)
self.withdraw() self.withdraw()
self.app = app self.app = app

View file

@ -4,13 +4,11 @@ emane configuration
import tkinter as tk import tkinter as tk
import webbrowser import webbrowser
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING
import grpc import grpc
from core.api.grpc import core_pb2
from core.gui.dialogs.dialog import Dialog 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.images import ImageEnum, Images
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
from core.gui.widgets import ConfigFrame from core.gui.widgets import ConfigFrame
@ -21,8 +19,8 @@ if TYPE_CHECKING:
class GlobalEmaneDialog(Dialog): class GlobalEmaneDialog(Dialog):
def __init__(self, master: Any, app: "Application"): def __init__(self, master: tk.BaseWidget, app: "Application"):
super().__init__(master, app, "EMANE Configuration", modal=True) super().__init__(app, "EMANE Configuration", master=master)
self.config_frame = None self.config_frame = None
self.draw() self.draw()
@ -54,25 +52,32 @@ class GlobalEmaneDialog(Dialog):
class EmaneModelDialog(Dialog): class EmaneModelDialog(Dialog):
def __init__( def __init__(
self, self,
master: Any, master: tk.BaseWidget,
app: "Application", app: "Application",
node: core_pb2.Node, canvas_node: "CanvasNode",
model: str, model: str,
interface: int = None, interface: int = None,
): ):
super().__init__(master, app, f"{node.name} {model} Configuration", modal=True) super().__init__(
self.node = node 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.model = f"emane_{model}"
self.interface = interface self.interface = interface
self.config_frame = None self.config_frame = None
self.has_error = False self.has_error = False
try: try:
self.config = self.app.core.get_emane_model_config( self.config = self.canvas_node.emane_model_configs.get(
self.node.id, self.model, self.interface (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() self.draw()
except grpc.RpcError as e: 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.has_error = True
self.destroy() self.destroy()
@ -98,20 +103,14 @@ class EmaneModelDialog(Dialog):
def click_apply(self): def click_apply(self):
self.config_frame.parse_config() self.config_frame.parse_config()
self.app.core.set_emane_model_config( key = (self.model, self.interface)
self.node.id, self.model, self.config, self.interface self.canvas_node.emane_model_configs[key] = self.config
)
self.destroy() self.destroy()
class EmaneConfigDialog(Dialog): class EmaneConfigDialog(Dialog):
def __init__( def __init__(self, app: "Application", canvas_node: "CanvasNode"):
self, master: "Application", app: "Application", canvas_node: "CanvasNode" super().__init__(app, f"{canvas_node.core_node.name} EMANE Configuration")
):
super().__init__(
master, app, f"{canvas_node.core_node.name} EMANE Configuration", modal=True
)
self.app = app
self.canvas_node = canvas_node self.canvas_node = canvas_node
self.node = canvas_node.core_node self.node = canvas_node.core_node
self.radiovar = tk.IntVar() self.radiovar = tk.IntVar()
@ -224,9 +223,7 @@ class EmaneConfigDialog(Dialog):
draw emane model configuration draw emane model configuration
""" """
model_name = self.emane_model.get() model_name = self.emane_model.get()
dialog = EmaneModelDialog( dialog = EmaneModelDialog(self, self.app, self.canvas_node, model_name)
self, self.app, self.canvas_node.core_node, model_name
)
if not dialog.has_error: if not dialog.has_error:
dialog.show() 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 tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import grpc
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.images import ImageEnum, Images from core.gui.images import ImageEnum, Images
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
@ -13,8 +11,8 @@ if TYPE_CHECKING:
class ErrorDialog(Dialog): class ErrorDialog(Dialog):
def __init__(self, master, app: "Application", title: str, details: str) -> None: def __init__(self, app: "Application", title: str, details: str) -> None:
super().__init__(master, app, "CORE Exception", modal=True) super().__init__(app, "CORE Exception")
self.title = title self.title = title
self.details = details self.details = details
self.error_message = None self.error_message = None
@ -41,18 +39,3 @@ class ErrorDialog(Dialog):
button = ttk.Button(self.top, text="Close", command=lambda: self.destroy()) button = ttk.Button(self.top, text="Close", command=lambda: self.destroy())
button.grid(sticky="ew") 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 logging
import tkinter as tk import tkinter as tk
from tkinter import filedialog, ttk from tkinter import filedialog, ttk
from typing import TYPE_CHECKING
from core.gui.appconfig import SCRIPT_PATH from core.gui.appconfig import SCRIPT_PATH
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX from core.gui.themes import FRAME_PAD, PADX
if TYPE_CHECKING:
from core.gui.app import Application
class ExecutePythonDialog(Dialog): class ExecutePythonDialog(Dialog):
def __init__(self, master, app): def __init__(self, app: "Application"):
super().__init__(master, app, "Execute Python Script", modal=True) super().__init__(app, "Execute Python Script")
self.app = app
self.with_options = tk.IntVar(value=0) self.with_options = tk.IntVar(value=0)
self.options = tk.StringVar(value="") self.options = tk.StringVar(value="")
self.option_entry = None 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 import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -12,8 +12,8 @@ if TYPE_CHECKING:
class HookDialog(Dialog): class HookDialog(Dialog):
def __init__(self, master: Any, app: "Application"): def __init__(self, master: tk.BaseWidget, app: "Application"):
super().__init__(master, app, "Hook", modal=True) super().__init__(app, "Hook", master=master)
self.name = tk.StringVar() self.name = tk.StringVar()
self.codetext = None self.codetext = None
self.hook = core_pb2.Hook() self.hook = core_pb2.Hook()
@ -88,8 +88,8 @@ class HookDialog(Dialog):
class HooksDialog(Dialog): class HooksDialog(Dialog):
def __init__(self, master: "Application", app: "Application"): def __init__(self, app: "Application"):
super().__init__(master, app, "Hooks", modal=True) super().__init__(app, "Hooks")
self.listbox = None self.listbox = None
self.edit_button = None self.edit_button = None
self.delete_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 typing import TYPE_CHECKING, Union
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
from core.gui import validation
from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.colorpicker import ColorPickerDialog
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application 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]: 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): class LinkConfigurationDialog(Dialog):
def __init__(self, master: "CanvasGraph", app: "Application", edge: "CanvasEdge"): def __init__(self, app: "Application", edge: "CanvasEdge"):
super().__init__(master, app, "Link Configuration", modal=True) super().__init__(app, "Link Configuration")
self.app = app
self.edge = edge self.edge = edge
self.is_symmetric = edge.link.options.unidirectional is False self.is_symmetric = edge.link.options.unidirectional is False
if self.is_symmetric: if self.is_symmetric:
@ -121,95 +121,65 @@ class LinkConfigurationDialog(Dialog):
label = ttk.Label(frame, text="Bandwidth (bps)") label = ttk.Label(frame, text="Bandwidth (bps)")
label.grid(row=row, column=0, sticky="ew") label.grid(row=row, column=0, sticky="ew")
entry = ttk.Entry( entry = validation.PositiveIntEntry(
frame, frame, empty_enabled=False, textvariable=self.bandwidth
textvariable=self.bandwidth,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
) )
entry.grid(row=row, column=1, sticky="ew", pady=PADY) entry.grid(row=row, column=1, sticky="ew", pady=PADY)
if not self.is_symmetric: if not self.is_symmetric:
entry = ttk.Entry( entry = validation.PositiveIntEntry(
frame, frame, empty_enabled=False, textvariable=self.down_bandwidth
textvariable=self.down_bandwidth,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
) )
entry.grid(row=row, column=2, sticky="ew", pady=PADY) entry.grid(row=row, column=2, sticky="ew", pady=PADY)
row = row + 1 row = row + 1
label = ttk.Label(frame, text="Delay (us)") label = ttk.Label(frame, text="Delay (us)")
label.grid(row=row, column=0, sticky="ew") label.grid(row=row, column=0, sticky="ew")
entry = ttk.Entry( entry = validation.PositiveIntEntry(
frame, frame, empty_enabled=False, textvariable=self.delay
textvariable=self.delay,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
) )
entry.grid(row=row, column=1, sticky="ew", pady=PADY) entry.grid(row=row, column=1, sticky="ew", pady=PADY)
if not self.is_symmetric: if not self.is_symmetric:
entry = ttk.Entry( entry = validation.PositiveIntEntry(
frame, frame, empty_enabled=False, textvariable=self.down_delay
textvariable=self.down_delay,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
) )
entry.grid(row=row, column=2, sticky="ew", pady=PADY) entry.grid(row=row, column=2, sticky="ew", pady=PADY)
row = row + 1 row = row + 1
label = ttk.Label(frame, text="Jitter (us)") label = ttk.Label(frame, text="Jitter (us)")
label.grid(row=row, column=0, sticky="ew") label.grid(row=row, column=0, sticky="ew")
entry = ttk.Entry( entry = validation.PositiveIntEntry(
frame, frame, empty_enabled=False, textvariable=self.jitter
textvariable=self.jitter,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
) )
entry.grid(row=row, column=1, sticky="ew", pady=PADY) entry.grid(row=row, column=1, sticky="ew", pady=PADY)
if not self.is_symmetric: if not self.is_symmetric:
entry = ttk.Entry( entry = validation.PositiveIntEntry(
frame, frame, empty_enabled=False, textvariable=self.down_jitter
textvariable=self.down_jitter,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
) )
entry.grid(row=row, column=2, sticky="ew", pady=PADY) entry.grid(row=row, column=2, sticky="ew", pady=PADY)
row = row + 1 row = row + 1
label = ttk.Label(frame, text="Loss (%)") label = ttk.Label(frame, text="Loss (%)")
label.grid(row=row, column=0, sticky="ew") label.grid(row=row, column=0, sticky="ew")
entry = ttk.Entry( entry = validation.PositiveFloatEntry(
frame, frame, empty_enabled=False, textvariable=self.loss
textvariable=self.loss,
validate="key",
validatecommand=(self.app.validation.positive_float, "%P"),
) )
entry.grid(row=row, column=1, sticky="ew", pady=PADY) entry.grid(row=row, column=1, sticky="ew", pady=PADY)
if not self.is_symmetric: if not self.is_symmetric:
entry = ttk.Entry( entry = validation.PositiveFloatEntry(
frame, frame, empty_enabled=False, textvariable=self.down_loss
textvariable=self.down_loss,
validate="key",
validatecommand=(self.app.validation.positive_float, "%P"),
) )
entry.grid(row=row, column=2, sticky="ew", pady=PADY) entry.grid(row=row, column=2, sticky="ew", pady=PADY)
row = row + 1 row = row + 1
label = ttk.Label(frame, text="Duplicate (%)") label = ttk.Label(frame, text="Duplicate (%)")
label.grid(row=row, column=0, sticky="ew") label.grid(row=row, column=0, sticky="ew")
entry = ttk.Entry( entry = validation.PositiveIntEntry(
frame, frame, empty_enabled=False, textvariable=self.duplicate
textvariable=self.duplicate,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
) )
entry.grid(row=row, column=1, sticky="ew", pady=PADY) entry.grid(row=row, column=1, sticky="ew", pady=PADY)
if not self.is_symmetric: if not self.is_symmetric:
entry = ttk.Entry( entry = validation.PositiveIntEntry(
frame, frame, empty_enabled=False, textvariable=self.down_duplicate
textvariable=self.down_duplicate,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
) )
entry.grid(row=row, column=2, sticky="ew", pady=PADY) entry.grid(row=row, column=2, sticky="ew", pady=PADY)
row = row + 1 row = row + 1
@ -230,11 +200,8 @@ class LinkConfigurationDialog(Dialog):
label = ttk.Label(frame, text="Width") label = ttk.Label(frame, text="Width")
label.grid(row=row, column=0, sticky="ew") label.grid(row=row, column=0, sticky="ew")
entry = ttk.Entry( entry = validation.PositiveFloatEntry(
frame, frame, empty_enabled=False, textvariable=self.width
textvariable=self.width,
validate="key",
validatecommand=(self.app.validation.positive_float, "%P"),
) )
entry.grid(row=row, column=1, sticky="ew", pady=PADY) 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): class MarkerDialog(Dialog):
def __init__( def __init__(self, app: "Application", initcolor: str = "#000000"):
self, master: "Application", app: "Application", initcolor: str = "#000000" super().__init__(app, "Marker Tool", modal=False)
):
super().__init__(master, app, "Marker Tool", modal=False)
self.app = app
self.color = initcolor self.color = initcolor
self.radius = MARKER_THICKNESS[0] self.radius = MARKER_THICKNESS[0]
self.marker_thickness = tk.IntVar(value=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 import grpc
from core.gui.dialogs.dialog import Dialog 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.themes import PADX, PADY
from core.gui.widgets import ConfigFrame from core.gui.widgets import ConfigFrame
@ -17,25 +16,20 @@ if TYPE_CHECKING:
class MobilityConfigDialog(Dialog): class MobilityConfigDialog(Dialog):
def __init__( def __init__(self, app: "Application", canvas_node: "CanvasNode"):
self, master: "Application", app: "Application", canvas_node: "CanvasNode" super().__init__(app, f"{canvas_node.core_node.name} Mobility Configuration")
):
super().__init__(
master,
app,
f"{canvas_node.core_node.name} Mobility Configuration",
modal=True,
)
self.canvas_node = canvas_node self.canvas_node = canvas_node
self.node = canvas_node.core_node self.node = canvas_node.core_node
self.config_frame = None self.config_frame = None
self.has_error = False self.has_error = False
try: 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() self.draw()
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Get Mobility Config Error", e)
self.has_error = True self.has_error = True
show_grpc_error(e, self.app, self.app)
self.destroy() self.destroy()
def draw(self): def draw(self):
@ -60,5 +54,5 @@ class MobilityConfigDialog(Dialog):
def click_apply(self): def click_apply(self):
self.config_frame.parse_config() self.config_frame.parse_config()
self.app.core.mobility_configs[self.node.id] = self.config self.canvas_node.mobility_config = self.config
self.destroy() self.destroy()

View file

@ -1,12 +1,11 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING
import grpc import grpc
from core.api.grpc.mobility_pb2 import MobilityAction from core.api.grpc.mobility_pb2 import MobilityAction
from core.gui.dialogs.dialog import Dialog 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.images import ImageEnum, Images
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
@ -18,14 +17,7 @@ ICON_SIZE = 16
class MobilityPlayer: class MobilityPlayer:
def __init__( def __init__(self, app: "Application", canvas_node: "CanvasNode", config):
self,
master: "Application",
app: "Application",
canvas_node: "CanvasNode",
config,
):
self.master = master
self.app = app self.app = app
self.canvas_node = canvas_node self.canvas_node = canvas_node
self.config = config self.config = config
@ -35,10 +27,8 @@ class MobilityPlayer:
def show(self): def show(self):
if self.dialog: if self.dialog:
self.dialog.destroy() self.dialog.destroy()
self.dialog = MobilityPlayerDialog( self.dialog = MobilityPlayerDialog(self.app, self.canvas_node, self.config)
self.master, self.app, self.canvas_node, self.config self.dialog.protocol("WM_DELETE_WINDOW", self.close)
)
self.dialog.protocol("WM_DELETE_WINDOW", self.handle_close)
if self.state == MobilityAction.START: if self.state == MobilityAction.START:
self.set_play() self.set_play()
elif self.state == MobilityAction.PAUSE: elif self.state == MobilityAction.PAUSE:
@ -47,9 +37,10 @@ class MobilityPlayer:
self.set_stop() self.set_stop()
self.dialog.show() self.dialog.show()
def handle_close(self): def close(self):
self.dialog.destroy() if self.dialog:
self.dialog = None self.dialog.destroy()
self.dialog = None
def set_play(self): def set_play(self):
self.state = MobilityAction.START self.state = MobilityAction.START
@ -68,11 +59,9 @@ class MobilityPlayer:
class MobilityPlayerDialog(Dialog): class MobilityPlayerDialog(Dialog):
def __init__( def __init__(self, app: "Application", canvas_node: "CanvasNode", config):
self, master: Any, app: "Application", canvas_node: "CanvasNode", config
):
super().__init__( 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.resizable(False, False)
self.geometry("") self.geometry("")
@ -153,7 +142,7 @@ class MobilityPlayerDialog(Dialog):
session_id, self.node.id, MobilityAction.START session_id, self.node.id, MobilityAction.START
) )
except grpc.RpcError as e: 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): def click_pause(self):
self.set_pause() self.set_pause()
@ -163,7 +152,7 @@ class MobilityPlayerDialog(Dialog):
session_id, self.node.id, MobilityAction.PAUSE session_id, self.node.id, MobilityAction.PAUSE
) )
except grpc.RpcError as e: 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): def click_stop(self):
self.set_stop() self.set_stop()
@ -173,4 +162,4 @@ class MobilityPlayerDialog(Dialog):
session_id, self.node.id, MobilityAction.STOP session_id, self.node.id, MobilityAction.STOP
) )
except grpc.RpcError as e: 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 import netaddr
from core.gui import nodeutils from core.gui import nodeutils, validation
from core.gui.appconfig import ICONS_PATH from core.gui.appconfig import ICONS_PATH
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.dialogs.emaneconfig import EmaneModelDialog from core.gui.dialogs.emaneconfig import EmaneModelDialog
@ -70,16 +70,12 @@ def check_ip4(parent, name: str, value: str) -> bool:
return True return True
def mac_auto(is_auto: tk.BooleanVar, entry: ttk.Entry): def mac_auto(is_auto: tk.BooleanVar, entry: ttk.Entry, mac: tk.StringVar) -> None:
logging.info("mac auto clicked")
if is_auto.get(): if is_auto.get():
logging.info("disabling mac") mac.set("")
entry.delete(0, tk.END)
entry.insert(tk.END, "")
entry.config(state=tk.DISABLED) entry.config(state=tk.DISABLED)
else: else:
entry.delete(0, tk.END) mac.set("00:00:00:00:00:00")
entry.insert(tk.END, "00:00:00:00:00:00")
entry.config(state=tk.NORMAL) entry.config(state=tk.NORMAL)
@ -98,15 +94,11 @@ class InterfaceData:
class NodeConfigDialog(Dialog): class NodeConfigDialog(Dialog):
def __init__( def __init__(self, app: "Application", canvas_node: "CanvasNode"):
self, master: "Application", app: "Application", canvas_node: "CanvasNode"
):
""" """
create an instance of node configuration create an instance of node configuration
""" """
super().__init__( super().__init__(app, f"{canvas_node.core_node.name} Configuration")
master, app, f"{canvas_node.core_node.name} Configuration", modal=True
)
self.canvas_node = canvas_node self.canvas_node = canvas_node
self.node = canvas_node.core_node self.node = canvas_node.core_node
self.image = canvas_node.image self.image = canvas_node.image
@ -126,6 +118,10 @@ class NodeConfigDialog(Dialog):
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
row = 0 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 # field frame
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
@ -147,15 +143,7 @@ class NodeConfigDialog(Dialog):
# name field # name field
label = ttk.Label(frame, text="Name") label = ttk.Label(frame, text="Name")
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
entry = ttk.Entry( entry = validation.NodeNameEntry(frame, textvariable=self.name, state=state)
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.grid(row=row, column=1, sticky="ew") entry.grid(row=row, column=1, sticky="ew")
row += 1 row += 1
@ -167,7 +155,7 @@ class NodeConfigDialog(Dialog):
frame, frame,
textvariable=self.type, textvariable=self.type,
values=list(NodeUtils.NODE_MODELS), values=list(NodeUtils.NODE_MODELS),
state="readonly", state=combo_state,
) )
combobox.grid(row=row, column=1, sticky="ew") combobox.grid(row=row, column=1, sticky="ew")
row += 1 row += 1
@ -176,7 +164,7 @@ class NodeConfigDialog(Dialog):
if NodeUtils.is_image_node(self.node.type): if NodeUtils.is_image_node(self.node.type):
label = ttk.Label(frame, text="Image") label = ttk.Label(frame, text="Image")
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) 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") entry.grid(row=row, column=1, sticky="ew")
row += 1 row += 1
@ -189,7 +177,7 @@ class NodeConfigDialog(Dialog):
servers = ["localhost"] servers = ["localhost"]
servers.extend(list(sorted(self.app.core.servers.keys()))) servers.extend(list(sorted(self.app.core.servers.keys())))
combobox = ttk.Combobox( 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") combobox.grid(row=row, column=1, sticky="ew")
row += 1 row += 1
@ -198,6 +186,7 @@ class NodeConfigDialog(Dialog):
response = self.app.core.client.get_interfaces() response = self.app.core.client.get_interfaces()
logging.debug("host machine available interfaces: %s", response) logging.debug("host machine available interfaces: %s", response)
interfaces = ListboxScroll(frame) interfaces = ListboxScroll(frame)
interfaces.listbox.config(state=state)
interfaces.grid( interfaces.grid(
row=row, column=0, columnspan=2, sticky="ew", padx=PADX, pady=PADY 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 = ttk.Notebook(self.top)
notebook.grid(sticky="nsew", pady=PADY) notebook.grid(sticky="nsew", pady=PADY)
self.top.rowconfigure(notebook.grid_info()["row"], weight=1) 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: for interface in self.canvas_node.interfaces:
logging.info("interface: %s", interface) logging.info("interface: %s", interface)
tab = ttk.Frame(notebook, padding=FRAME_PAD) tab = ttk.Frame(notebook, padding=FRAME_PAD)
@ -241,18 +230,17 @@ class NodeConfigDialog(Dialog):
label = ttk.Label(tab, text="MAC") label = ttk.Label(tab, text="MAC")
label.grid(row=row, column=0, padx=PADX, pady=PADY) label.grid(row=row, column=0, padx=PADX, pady=PADY)
auto_set = not interface.mac auto_set = not interface.mac
if auto_set: mac_state = tk.DISABLED if auto_set else tk.NORMAL
state = tk.DISABLED
else:
state = tk.NORMAL
is_auto = tk.BooleanVar(value=auto_set) 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.var = is_auto
checkbutton.grid(row=row, column=1, padx=PADX) checkbutton.grid(row=row, column=1, padx=PADX)
mac = tk.StringVar(value=interface.mac) 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") 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) checkbutton.config(command=func)
row += 1 row += 1
@ -262,7 +250,7 @@ class NodeConfigDialog(Dialog):
if interface.ip4: if interface.ip4:
ip4_net = f"{interface.ip4}/{interface.ip4mask}" ip4_net = f"{interface.ip4}/{interface.ip4mask}"
ip4 = tk.StringVar(value=ip4_net) 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") entry.grid(row=row, column=1, columnspan=2, sticky="ew")
row += 1 row += 1
@ -272,7 +260,7 @@ class NodeConfigDialog(Dialog):
if interface.ip6: if interface.ip6:
ip6_net = f"{interface.ip6}/{interface.ip6mask}" ip6_net = f"{interface.ip6}/{interface.ip6mask}"
ip6 = tk.StringVar(value=ip6_net) 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") entry.grid(row=row, column=1, columnspan=2, sticky="ew")
self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6) self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6)
@ -283,14 +271,16 @@ class NodeConfigDialog(Dialog):
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, 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.grid(row=0, column=0, padx=PADX, sticky="ew")
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def click_emane_config(self, emane_model: str, interface_id: int): 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() dialog.show()
def click_icon(self): def click_icon(self):
@ -300,7 +290,7 @@ class NodeConfigDialog(Dialog):
self.image_button.config(image=self.image) self.image_button.config(image=self.image)
self.image_file = file_path self.image_file = file_path
def config_apply(self): def click_apply(self):
error = False error = False
# update core node # update core node
@ -326,7 +316,6 @@ class NodeConfigDialog(Dialog):
ip4_net = data.ip4.get() ip4_net = data.ip4.get()
if not check_ip4(self, interface.name, ip4_net): if not check_ip4(self, interface.name, ip4_net):
error = True error = True
data.ip4.set(f"{interface.ip4}/{interface.ip4mask}")
break break
if ip4_net: if ip4_net:
ip4, ip4mask = ip4_net.split("/") ip4, ip4mask = ip4_net.split("/")
@ -340,7 +329,6 @@ class NodeConfigDialog(Dialog):
ip6_net = data.ip6.get() ip6_net = data.ip6.get()
if not check_ip6(self, interface.name, ip6_net): if not check_ip6(self, interface.name, ip6_net):
error = True error = True
data.ip6.set(f"{interface.ip6}/{interface.ip6mask}")
break break
if ip6_net: if ip6_net:
ip6, ip6mask = ip6_net.split("/") ip6, ip6mask = ip6_net.split("/")
@ -351,15 +339,14 @@ class NodeConfigDialog(Dialog):
interface.ip6mask = ip6mask interface.ip6mask = ip6mask
mac = data.mac.get() 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}" title = f"MAC Error for {interface.name}"
messagebox.showerror(title, "Invalid MAC Address") messagebox.showerror(title, "Invalid MAC Address")
error = True error = True
data.mac.set(interface.mac)
break break
else: elif not auto_mac:
mac = netaddr.EUI(mac) mac = netaddr.EUI(mac, dialect=netaddr.mac_unix_expanded)
mac.dialect = netaddr.mac_unix_expanded
interface.mac = str(mac) interface.mac = str(mac)
# redraw # redraw

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from core.gui import appconfig from core.gui import appconfig, validation
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY, scale_fonts from core.gui.themes import FRAME_PAD, PADX, PADY, scale_fonts
from core.gui.validation import LARGEST_SCALE, SMALLEST_SCALE from core.gui.validation import LARGEST_SCALE, SMALLEST_SCALE
@ -16,14 +16,14 @@ SCALE_INTERVAL = 0.01
class PreferencesDialog(Dialog): class PreferencesDialog(Dialog):
def __init__(self, master: "Application", app: "Application"): def __init__(self, app: "Application"):
super().__init__(master, app, "Preferences", modal=True) super().__init__(app, "Preferences")
self.gui_scale = tk.DoubleVar(value=self.app.app_scale) self.gui_scale = tk.DoubleVar(value=self.app.app_scale)
preferences = self.app.guiconfig["preferences"] preferences = self.app.guiconfig.preferences
self.editor = tk.StringVar(value=preferences["editor"]) self.editor = tk.StringVar(value=preferences.editor)
self.theme = tk.StringVar(value=preferences["theme"]) self.theme = tk.StringVar(value=preferences.theme)
self.terminal = tk.StringVar(value=preferences["terminal"]) self.terminal = tk.StringVar(value=preferences.terminal)
self.gui3d = tk.StringVar(value=preferences["gui3d"]) self.gui3d = tk.StringVar(value=preferences.gui3d)
self.draw() self.draw()
def draw(self): def draw(self):
@ -80,12 +80,8 @@ class PreferencesDialog(Dialog):
variable=self.gui_scale, variable=self.gui_scale,
) )
scale.grid(row=0, column=0, sticky="ew") scale.grid(row=0, column=0, sticky="ew")
entry = ttk.Entry( entry = validation.AppScaleEntry(
scale_frame, scale_frame, textvariable=self.gui_scale, width=4
textvariable=self.gui_scale,
width=4,
validate="key",
validatecommand=(self.app.validation.app_scale, "%P"),
) )
entry.grid(row=0, column=1) entry.grid(row=0, column=1)
@ -110,15 +106,14 @@ class PreferencesDialog(Dialog):
self.app.style.theme_use(theme) self.app.style.theme_use(theme)
def click_save(self): def click_save(self):
preferences = self.app.guiconfig["preferences"] preferences = self.app.guiconfig.preferences
preferences["terminal"] = self.terminal.get() preferences.terminal = self.terminal.get()
preferences["editor"] = self.editor.get() preferences.editor = self.editor.get()
preferences["gui3d"] = self.gui3d.get() preferences.gui3d = self.gui3d.get()
preferences["theme"] = self.theme.get() preferences.theme = self.theme.get()
self.gui_scale.set(round(self.gui_scale.get(), 2)) self.gui_scale.set(round(self.gui_scale.get(), 2))
app_scale = self.gui_scale.get() app_scale = self.gui_scale.get()
self.app.guiconfig["scale"] = app_scale self.app.guiconfig.scale = app_scale
self.app.save_config() self.app.save_config()
self.scale_adjust() self.scale_adjust()
self.destroy() 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 tkinter import ttk
from typing import TYPE_CHECKING 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.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import ListboxScroll from core.gui.widgets import ListboxScroll
@ -16,11 +16,10 @@ DEFAULT_PORT = 50051
class ServersDialog(Dialog): class ServersDialog(Dialog):
def __init__(self, master: "Application", app: "Application"): def __init__(self, app: "Application"):
super().__init__(master, app, "CORE Servers", modal=True) super().__init__(app, "CORE Servers")
self.name = tk.StringVar(value=DEFAULT_NAME) self.name = tk.StringVar(value=DEFAULT_NAME)
self.address = tk.StringVar(value=DEFAULT_ADDRESS) self.address = tk.StringVar(value=DEFAULT_ADDRESS)
self.port = tk.IntVar(value=DEFAULT_PORT)
self.servers = None self.servers = None
self.selected_index = None self.selected_index = None
self.selected = None self.selected = None
@ -54,31 +53,17 @@ class ServersDialog(Dialog):
frame.grid(pady=PADY, sticky="ew") frame.grid(pady=PADY, sticky="ew")
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
frame.columnconfigure(3, weight=1) frame.columnconfigure(3, weight=1)
frame.columnconfigure(5, weight=1)
label = ttk.Label(frame, text="Name") 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 = ttk.Entry(frame, textvariable=self.name)
entry.grid(row=0, column=1, sticky="ew") entry.grid(row=0, column=1, sticky="ew")
label = ttk.Label(frame, text="Address") 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 = ttk.Entry(frame, textvariable=self.address)
entry.grid(row=0, column=3, sticky="ew") 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): def draw_servers_buttons(self):
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(pady=PADY, sticky="ew") frame.grid(pady=PADY, sticky="ew")
@ -113,13 +98,9 @@ class ServersDialog(Dialog):
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def click_save_configuration(self): def click_save_configuration(self):
servers = [] self.app.guiconfig.servers.clear()
for name in sorted(self.app.core.servers): for server in self.app.core.servers.values():
server = self.app.core.servers[name] self.app.guiconfig.servers.append(server)
servers.append(
{"name": server.name, "address": server.address, "port": server.port}
)
self.app.guiconfig["servers"] = servers
self.app.save_config() self.app.save_config()
self.destroy() self.destroy()
@ -127,8 +108,7 @@ class ServersDialog(Dialog):
name = self.name.get() name = self.name.get()
if name not in self.app.core.servers: if name not in self.app.core.servers:
address = self.address.get() address = self.address.get()
port = self.port.get() server = CoreServer(name, address)
server = CoreServer(name, address, port)
self.app.core.servers[name] = server self.app.core.servers[name] = server
self.servers.insert(tk.END, name) self.servers.insert(tk.END, name)
@ -140,7 +120,6 @@ class ServersDialog(Dialog):
server = self.app.core.servers.pop(previous_name) server = self.app.core.servers.pop(previous_name)
server.name = name server.name = name
server.address = self.address.get() server.address = self.address.get()
server.port = self.port.get()
self.app.core.servers[name] = server self.app.core.servers[name] = server
self.servers.delete(self.selected_index) self.servers.delete(self.selected_index)
self.servers.insert(self.selected_index, name) self.servers.insert(self.selected_index, name)
@ -154,7 +133,6 @@ class ServersDialog(Dialog):
self.selected_index = None self.selected_index = None
self.name.set(DEFAULT_NAME) self.name.set(DEFAULT_NAME)
self.address.set(DEFAULT_ADDRESS) self.address.set(DEFAULT_ADDRESS)
self.port.set(DEFAULT_PORT)
self.servers.selection_clear(0, tk.END) self.servers.selection_clear(0, tk.END)
self.save_button.config(state=tk.DISABLED) self.save_button.config(state=tk.DISABLED)
self.delete_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] server = self.app.core.servers[self.selected]
self.name.set(server.name) self.name.set(server.name)
self.address.set(server.address) self.address.set(server.address)
self.port.set(server.port)
self.save_button.config(state=tk.NORMAL) self.save_button.config(state=tk.NORMAL)
self.delete_button.config(state=tk.NORMAL) self.delete_button.config(state=tk.NORMAL)
else: else:

View file

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

View file

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

View file

@ -1,15 +1,14 @@
import logging import logging
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import messagebox, ttk
from typing import TYPE_CHECKING, Iterable from typing import TYPE_CHECKING, List
import grpc import grpc
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
from core.gui.dialogs.dialog import Dialog 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.images import ImageEnum, Images
from core.gui.task import BackgroundTask from core.gui.task import ProgressTask
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
if TYPE_CHECKING: if TYPE_CHECKING:
@ -17,37 +16,35 @@ if TYPE_CHECKING:
class SessionsDialog(Dialog): class SessionsDialog(Dialog):
def __init__( def __init__(self, app: "Application", is_start_app: bool = False) -> None:
self, master: "Application", app: "Application", is_start_app: bool = False super().__init__(app, "Sessions")
):
super().__init__(master, app, "Sessions", modal=True)
self.is_start_app = is_start_app self.is_start_app = is_start_app
self.selected = False self.selected_session = None
self.selected_id = None self.selected_id = None
self.tree = None self.tree = None
self.has_error = False
self.sessions = self.get_sessions() self.sessions = self.get_sessions()
if not self.has_error: self.connect_button = None
self.draw() 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: try:
response = self.app.core.client.get_sessions() response = self.app.core.client.get_sessions()
logging.info("sessions: %s", response) logging.info("sessions: %s", response)
return response.sessions return response.sessions
except grpc.RpcError as e: except grpc.RpcError as e:
show_grpc_error(e, self.app, self.app) self.app.show_grpc_exception("Get Sessions Error", e)
self.has_error = True
self.destroy() self.destroy()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(1, weight=1) self.top.rowconfigure(1, weight=1)
self.draw_description() self.draw_description()
self.draw_tree() self.draw_tree()
self.draw_buttons() self.draw_buttons()
def draw_description(self): def draw_description(self) -> None:
""" """
write a short description write a short description
""" """
@ -61,20 +58,26 @@ class SessionsDialog(Dialog):
) )
label.grid(pady=PADY) label.grid(pady=PADY)
def draw_tree(self): def draw_tree(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
frame.rowconfigure(0, weight=1) frame.rowconfigure(0, weight=1)
frame.grid(sticky="nsew", pady=PADY) frame.grid(sticky="nsew", pady=PADY)
self.tree = ttk.Treeview( 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.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.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.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") self.tree.heading("nodes", text="Node Count")
for index, session in enumerate(self.sessions): for index, session in enumerate(self.sessions):
@ -85,7 +88,7 @@ class SessionsDialog(Dialog):
text=str(session.id), text=str(session.id),
values=(session.id, state_name, session.nodes), 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) self.tree.bind("<<TreeviewSelect>>", self.click_select)
yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
@ -96,9 +99,9 @@ class SessionsDialog(Dialog):
xscrollbar.grid(row=1, sticky="ew") xscrollbar.grid(row=1, sticky="ew")
self.tree.configure(xscrollcommand=xscrollbar.set) self.tree.configure(xscrollcommand=xscrollbar.set)
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
for i in range(5): for i in range(4):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
frame.grid(sticky="ew") frame.grid(sticky="ew")
@ -110,42 +113,37 @@ class SessionsDialog(Dialog):
b.grid(row=0, padx=PADX, sticky="ew") b.grid(row=0, padx=PADX, sticky="ew")
image = Images.get(ImageEnum.FILEOPEN, 16) image = Images.get(ImageEnum.FILEOPEN, 16)
b = ttk.Button( self.connect_button = ttk.Button(
frame, frame,
image=image, image=image,
text="Connect", text="Connect",
compound=tk.LEFT, compound=tk.LEFT,
command=self.click_connect, command=self.click_connect,
state=tk.DISABLED,
) )
b.image = image self.connect_button.image = image
b.grid(row=0, column=1, padx=PADX, sticky="ew") self.connect_button.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")
image = Images.get(ImageEnum.DELETE, 16) image = Images.get(ImageEnum.DELETE, 16)
b = ttk.Button( self.delete_button = ttk.Button(
frame, frame,
image=image, image=image,
text="Delete", text="Delete",
compound=tk.LEFT, compound=tk.LEFT,
command=self.click_delete, command=self.click_delete,
state=tk.DISABLED,
) )
b.image = image self.delete_button.image = image
b.grid(row=0, column=3, padx=PADX, sticky="ew") self.delete_button.grid(row=0, column=2, padx=PADX, sticky="ew")
image = Images.get(ImageEnum.CANCEL, 16) image = Images.get(ImageEnum.CANCEL, 16)
if self.is_start_app: if self.is_start_app:
b = ttk.Button( 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: else:
b = ttk.Button( b = ttk.Button(
@ -156,69 +154,64 @@ class SessionsDialog(Dialog):
command=self.destroy, command=self.destroy,
) )
b.image = image 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.app.core.create_new_session()
self.destroy() self.destroy()
def click_select(self, event: tk.Event): def click_select(self, _event: tk.Event = None) -> None:
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")
item = self.tree.selection() item = self.tree.selection()
if item: if item:
sid = int(self.tree.item(item, "text")) self.selected_session = int(self.tree.item(item, "text"))
self.app.core.delete_session(sid, self.top) self.selected_id = item
self.tree.delete(item[0]) self.delete_button.config(state=tk.NORMAL)
if sid == self.app.core.session_id: self.connect_button.config(state=tk.NORMAL)
self.click_new() else:
selections = self.tree.get_children() self.selected_session = None
if selections: self.selected_id = None
self.tree.focus(selections[0]) self.delete_button.config(state=tk.DISABLED)
self.tree.selection_set(selections[0]) 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): 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): if is_draw_shape(shape.shape_type):
title = "Add Shape" title = "Add Shape"
else: else:
title = "Add Text" title = "Add Text"
super().__init__(master, app, title, modal=True) super().__init__(app, title)
self.canvas = app.canvas self.canvas = app.canvas
self.fill = None self.fill = None
self.border = None self.border = None
@ -235,7 +235,7 @@ class ShapeDialog(Dialog):
text=shape_text, text=shape_text,
fill=self.text_color, fill=self.text_color,
font=text_font, font=text_font,
tags=tags.SHAPE_TEXT, tags=(tags.SHAPE_TEXT, tags.ANNOTATION),
) )
self.shape.created = True self.shape.created = True
else: else:

View file

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

View file

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

View file

@ -1,11 +1,13 @@
import logging import logging
import math
import tkinter as tk import tkinter as tk
from typing import TYPE_CHECKING, Any, Tuple from typing import TYPE_CHECKING, Any, Tuple
from core.api.grpc import core_pb2
from core.gui import themes from core.gui import themes
from core.gui.dialogs.linkconfig import LinkConfigurationDialog from core.gui.dialogs.linkconfig import LinkConfigurationDialog
from core.gui.graph import tags from core.gui.graph import tags
from core.gui.nodeutils import EdgeUtils, NodeUtils from core.gui.nodeutils import NodeUtils
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.graph.graph import CanvasGraph from core.gui.graph.graph import CanvasGraph
@ -15,182 +17,325 @@ EDGE_WIDTH = 3
EDGE_COLOR = "#ff0000" EDGE_COLOR = "#ff0000"
WIRELESS_WIDTH = 1.5 WIRELESS_WIDTH = 1.5
WIRELESS_COLOR = "#009933" WIRELESS_COLOR = "#009933"
ARC_DISTANCE = 50
class CanvasWirelessEdge: def create_edge_token(src: int, dst: int, network: int = None) -> Tuple[int, ...]:
def __init__( values = [src, dst]
self, if network is not None:
token: Tuple[Any, ...], values.append(network)
position: Tuple[float, float, float, float], return tuple(sorted(values))
src: int,
dst: int,
canvas: "CanvasGraph", def arc_edges(edges) -> None:
): if not edges:
logging.debug("Draw wireless link from node %s to node %s", src, dst) return
self.token = token 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.src = src
self.dst = dst 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( self.id = self.canvas.create_line(
*position, *src_pos,
tags=tags.WIRELESS_EDGE, *arc_pos,
width=WIRELESS_WIDTH * self.canvas.app.app_scale, *dst_pos,
fill=WIRELESS_COLOR, 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.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 Canvas edge class
""" """
def __init__( def __init__(
self, self,
x1: float,
y1: float,
x2: float,
y2: float,
src: int,
canvas: "CanvasGraph", canvas: "CanvasGraph",
): src: int,
src_pos: Tuple[float, float],
dst_pos: Tuple[float, float],
) -> None:
""" """
Create an instance of canvas edge object Create an instance of canvas edge object
""" """
self.src = src super().__init__(canvas, src)
self.dst = None
self.src_interface = None self.src_interface = None
self.dst_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_src = None
self.text_dst = None self.text_dst = None
self.text_middle = None
self.token = None
self.link = None self.link = None
self.asymmetric_link = None self.asymmetric_link = None
self.throughput = None self.throughput = None
self.draw(src_pos, dst_pos)
self.set_binding() self.set_binding()
self.context = tk.Menu(self.canvas)
self.create_context()
def set_binding(self): def create_context(self):
self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.create_context) 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.link = link
self.draw_labels() self.draw_labels()
def get_coordinates(self) -> [float, float, float, float]: def interface_label(self, interface: core_pb2.Interface) -> str:
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):
label = "" label = ""
if interface.ip4: if interface.name and self.canvas.show_interface_names.get():
label = f"{interface.ip4}/{interface.ip4mask}" label = f"{interface.name}"
if interface.ip6: if interface.ip4 and self.canvas.show_ip4s.get():
label = f"{label}\n{interface.ip6}/{interface.ip6mask}" 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 return label
def draw_labels(self): def create_node_labels(self) -> Tuple[str, str]:
x1, y1, x2, y2 = self.get_coordinates() label_one = None
label_one, label_two = self.create_labels() if self.link.HasField("interface_one"):
self.text_src = self.canvas.create_text( label_one = self.interface_label(self.link.interface_one)
x1, label_two = None
y1, if self.link.HasField("interface_two"):
text=label_one, label_two = self.interface_label(self.link.interface_two)
justify=tk.CENTER, return label_one, label_two
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 redraw(self): def draw_labels(self) -> None:
label_one, label_two = self.create_labels() src_text, dst_text = self.create_node_labels()
self.canvas.itemconfig(self.text_src, text=label_one) self.src_label_text(src_text)
self.canvas.itemconfig(self.text_dst, text=label_two) self.dst_label_text(dst_text)
def update_labels(self): def redraw(self) -> None:
""" super().redraw()
Move edge labels based on current position. self.draw_labels()
"""
x1, y1, x2, y2 = self.get_coordinates()
self.canvas.coords(self.text_src, x1, y1)
self.canvas.coords(self.text_dst, x2, y2)
if self.text_middle is not None:
x, y = self.get_midpoint()
self.canvas.coords(self.text_middle, x, y)
def set_throughput(self, throughput: float): def set_throughput(self, throughput: float) -> None:
throughput = 0.001 * throughput throughput = 0.001 * throughput
value = f"{throughput:.3f} kbps" text = f"{throughput:.3f} kbps"
if self.text_middle is None: self.middle_label_text(text)
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)
if throughput > self.canvas.throughput_threshold: if throughput > self.canvas.throughput_threshold:
color = self.canvas.throughput_color color = self.canvas.throughput_color
width = self.canvas.throughput_width width = self.canvas.throughput_width
else: else:
color = EDGE_COLOR color = self.color
width = EDGE_WIDTH width = self.scaled_width()
self.canvas.itemconfig(self.id, fill=color, width=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.dst = dst
self.token = EdgeUtils.get_token(self.src, self.dst) self.token = create_edge_token(self.src, self.dst)
x, y = self.canvas.coords(self.dst) dst_pos = self.canvas.coords(self.dst)
x1, y1, _, _ = self.canvas.coords(self.id) self.move_dst(dst_pos)
self.canvas.coords(self.id, x1, y1, x, y)
self.check_wireless() self.check_wireless()
self.canvas.tag_raise(self.src) self.canvas.tag_raise(self.src)
self.canvas.tag_raise(self.dst) self.canvas.tag_raise(self.dst)
logging.debug("Draw wired link from node %s to node %s", self.src, 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] src_node = self.canvas.nodes[self.src]
dst_node = self.canvas.nodes[self.dst] dst_node = self.canvas.nodes[self.dst]
src_node_type = src_node.core_node.type src_node_type = src_node.core_node.type
@ -210,12 +355,12 @@ class CanvasEdge:
wlan_network[self.dst].add(self.src) wlan_network[self.dst].add(self.src)
return is_src_wireless or is_dst_wireless return is_src_wireless or is_dst_wireless
def check_wireless(self): def check_wireless(self) -> None:
if self.is_wireless(): if self.is_wireless():
self.canvas.itemconfig(self.id, state=tk.HIDDEN) self.canvas.itemconfig(self.id, state=tk.HIDDEN)
self._check_antenna() self._check_antenna()
def _check_antenna(self): def _check_antenna(self) -> None:
src_node = self.canvas.nodes[self.src] src_node = self.canvas.nodes[self.src]
dst_node = self.canvas.nodes[self.dst] dst_node = self.canvas.nodes[self.dst]
src_node_type = src_node.core_node.type src_node_type = src_node.core_node.type
@ -230,32 +375,19 @@ class CanvasEdge:
else: else:
src_node.add_antenna() src_node.add_antenna()
def delete(self): def reset(self) -> None:
logging.debug("Delete canvas edge, id: %s", self.id) self.canvas.delete(self.middle_label)
self.canvas.delete(self.id) self.middle_label = None
if self.link: self.canvas.itemconfig(self.id, fill=self.color, width=self.scaled_width())
self.canvas.delete(self.text_src)
self.canvas.delete(self.text_dst)
self.canvas.delete(self.text_middle)
def reset(self): def show_context(self, event: tk.Event) -> None:
self.canvas.delete(self.text_middle) state = tk.DISABLED if self.canvas.core.is_runtime() else tk.NORMAL
self.text_middle = None self.context.entryconfigure(1, state=state)
self.canvas.itemconfig(self.id, fill=EDGE_COLOR, width=EDGE_WIDTH) self.context.tk_popup(event.x_root, event.y_root)
def create_context(self, event: tk.Event): def click_delete(self):
context = tk.Menu(self.canvas) self.canvas.delete_edge(self)
themes.style_menu(context)
context.add_command(label="Configure", command=self.configure)
context.add_command(label="Delete")
context.add_command(label="Split")
context.add_command(label="Merge")
if self.canvas.app.core.is_runtime():
context.entryconfigure(1, state="disabled")
context.entryconfigure(2, state="disabled")
context.entryconfigure(3, state="disabled")
context.post(event.x_root, event.y_root)
def configure(self): def click_configure(self) -> None:
dialog = LinkConfigurationDialog(self.canvas, self.canvas.app, self) dialog = LinkConfigurationDialog(self.canvas.app, self)
dialog.show() dialog.show()

View file

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

View file

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

View file

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

View file

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

View file

@ -47,7 +47,8 @@ class Images:
except KeyError: except KeyError:
messagebox.showwarning( messagebox.showwarning(
"Missing image file", "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 logging
import random from typing import TYPE_CHECKING, Any, List, Optional, Set, Tuple
from typing import TYPE_CHECKING, Set, Union
from netaddr import IPNetwork import netaddr
from netaddr import EUI, IPNetwork
from core.gui.nodeutils import NodeUtils from core.gui.nodeutils import NodeUtils
@ -12,77 +12,154 @@ if TYPE_CHECKING:
from core.gui.graph.node import CanvasNode from core.gui.graph.node import CanvasNode
def random_mac(): def get_index(interface: "core_pb2.Interface") -> int:
return ("{:02x}" * 6).format(*[random.randrange(256) for _ in range(6)]) net = netaddr.IPNetwork(f"{interface.ip4}/{interface.ip4mask}")
ip_value = net.value
cidr_value = net.cidr.value
return ip_value - cidr_value
class Subnets: class Subnets:
def __init__(self, ip4: IPNetwork, ip6: IPNetwork) -> None: def __init__(self, ip4: IPNetwork, ip6: IPNetwork) -> None:
self.ip4 = ip4 self.ip4 = ip4
self.ip6 = ip6 self.ip6 = ip6
self.used_indexes = set()
def __eq__(self, other: "Subnets") -> bool: def __eq__(self, other: Any) -> bool:
return (self.ip4, self.ip6) == (other.ip4, other.ip6) if not isinstance(other, Subnets):
return False
return self.key() == other.key()
def __hash__(self) -> int: 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": def next(self) -> "Subnets":
return Subnets(self.ip4.next(), self.ip6.next()) return Subnets(self.ip4.next(), self.ip6.next())
class InterfaceManager: class InterfaceManager:
def __init__( def __init__(self, app: "Application") -> None:
self,
app: "Application",
ip4: str = "10.0.0.0",
ip4_mask: int = 24,
ip6: str = "2001::",
ip6_mask=64,
) -> None:
self.app = app self.app = app
self.ip4_mask = ip4_mask ip4 = self.app.guiconfig.ips.ip4
self.ip6_mask = ip6_mask ip6 = self.app.guiconfig.ips.ip6
self.ip4_subnets = IPNetwork(f"{ip4}/{ip4_mask}") self.ip4_mask = 24
self.ip6_subnets = IPNetwork(f"{ip6}/{ip6_mask}") 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.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: def next_subnets(self) -> Subnets:
# define currently used subnets subnets = self.current_subnets
used_subnets = set() if subnets is None:
for edge in self.app.core.links.values(): subnets = Subnets(self.ip4_subnets, self.ip6_subnets)
link = edge.link while subnets.key() in self.used_subnets:
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 = subnets.next() subnets = subnets.next()
self.used_subnets[subnets.key()] = subnets
return subnets return subnets
def reset(self): def reset(self) -> None:
self.current_subnets = None self.current_subnets = None
self.used_subnets.clear()
def get_ips(self, node_id: int) -> [str, str]: def removed(self, links: List["core_pb2.Link"]) -> None:
ip4 = self.current_subnets.ip4[node_id] # get remaining subnets
ip6 = self.current_subnets.ip6[node_id] 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) return str(ip4), str(ip6)
@classmethod def get_subnets(self, interface: "core_pb2.Interface") -> Subnets:
def get_subnets(cls, interface: "core_pb2.Interface") -> Subnets: logging.info("get subnets for interface: %s", interface)
ip4_subnet = IPNetwork(f"{interface.ip4}/{interface.ip4mask}").cidr ip4_subnet = self.ip4_subnets
ip6_subnet = IPNetwork(f"{interface.ip6}/{interface.ip6mask}").cidr if interface.ip4:
return Subnets(ip4_subnet, ip6_subnet) 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( def determine_subnets(
self, canvas_src_node: "CanvasNode", canvas_dst_node: "CanvasNode" self, canvas_src_node: "CanvasNode", canvas_dst_node: "CanvasNode"
): ) -> None:
src_node = canvas_src_node.core_node src_node = canvas_src_node.core_node
dst_node = canvas_dst_node.core_node dst_node = canvas_dst_node.core_node
is_src_container = NodeUtils.is_container_node(src_node.type) is_src_container = NodeUtils.is_container_node(src_node.type)
@ -106,7 +183,7 @@ class InterfaceManager:
def find_subnets( def find_subnets(
self, canvas_node: "CanvasNode", visited: Set[int] = None 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) logging.info("finding subnet for node: %s", canvas_node.core_node.name)
canvas = self.app.canvas canvas = self.app.canvas
subnets = None 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 logging
import os import os
import tkinter as tk import tkinter as tk
import webbrowser
from functools import partial from functools import partial
from tkinter import filedialog, messagebox
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import core.gui.menuaction as action from core.gui.appconfig import XMLS_PATH
from core.gui.coreclient import OBSERVERS 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.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: if TYPE_CHECKING:
from core.gui.app import Application 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): class Menubar(tk.Menu):
""" """
Core menubar 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 Create a CoreMenubar instance
""" """
super().__init__(master, cnf, **kwargs) super().__init__(master, **kwargs)
self.master.config(menu=self) self.master.config(menu=self)
self.app = app self.app = app
self.menuaction = action.MenuAction(app, master) self.core = app.core
self.canvas = app.canvas
self.recent_menu = None self.recent_menu = None
self.edit_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() self.draw()
def draw(self): def draw(self) -> None:
""" """
Create core menubar and bind the hot keys to their matching command 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_session_menu()
self.draw_help_menu() self.draw_help_menu()
def draw_file_menu(self): def draw_file_menu(self) -> None:
""" """
Create file menu Create file menu
""" """
menu = tk.Menu(self) menu = tk.Menu(self)
menu.add_command( menu.add_command(
label="New Session", label="New Session", accelerator="Ctrl+N", command=self.click_new
accelerator="Ctrl+N",
command=self.menuaction.new_session,
) )
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( 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) self.app.bind_all("<Control-o>", self.click_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.recent_menu = tk.Menu(menu) 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( self.recent_menu.add_command(
label=i, command=partial(self.open_recent_files, i) 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_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...", 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_separator() menu.add_separator()
menu.add_command( 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) self.add_cascade(label="File", menu=menu)
def draw_edit_menu(self): def draw_edit_menu(self) -> None:
""" """
Create edit menu Create edit menu
""" """
menu = tk.Menu(self) 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="Undo", accelerator="Ctrl+Z", state=tk.DISABLED)
menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED) menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED)
menu.add_separator() 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( 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.add_cascade(label="Edit", menu=menu)
self.app.master.bind_all("<Control-x>", self.click_cut)
self.app.master.bind_all("<Control-c>", self.menuaction.copy) self.app.master.bind_all("<Control-c>", self.click_copy)
self.app.master.bind_all("<Control-v>", self.menuaction.paste) self.app.master.bind_all("<Control-v>", self.click_paste)
self.app.master.bind_all("<Control-d>", self.menuaction.delete) self.app.master.bind_all("<Control-d>", self.click_delete)
self.edit_menu = menu self.edit_menu = menu
def draw_canvas_menu(self): def draw_canvas_menu(self) -> None:
""" """
Create canvas menu Create canvas menu
""" """
menu = tk.Menu(self) menu = tk.Menu(self)
menu.add_command( menu.add_command(label="Size / Scale", command=self.click_canvas_size_and_scale)
label="Size/scale...", command=self.menuaction.canvas_size_and_scale menu.add_command(label="Wallpaper", command=self.click_canvas_wallpaper)
)
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)
self.add_cascade(label="Canvas", menu=menu) self.add_cascade(label="Canvas", menu=menu)
def draw_view_menu(self): def draw_view_menu(self) -> None:
""" """
Create view menu Create view menu
""" """
view_menu = tk.Menu(self) menu = tk.Menu(self)
self.create_show_menu(view_menu) menu.add_checkbutton(
view_menu.add_command(label="Show hidden nodes", state=tk.DISABLED) label="Interface Names",
view_menu.add_command(label="Locked", state=tk.DISABLED) command=self.click_edge_label_change,
view_menu.add_command(label="3D GUI...", state=tk.DISABLED) variable=self.canvas.show_interface_names,
view_menu.add_separator() )
view_menu.add_command(label="Zoom in", accelerator="+", state=tk.DISABLED) menu.add_checkbutton(
view_menu.add_command(label="Zoom out", accelerator="-", state=tk.DISABLED) label="IPv4 Addresses",
self.add_cascade(label="View", menu=view_menu) 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): def draw_tools_menu(self) -> None:
"""
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):
""" """
Create tools menu Create tools menu
""" """
menu = tk.Menu(self) menu = tk.Menu(self)
menu.add_command(label="Auto rearrange all", state=tk.DISABLED) menu.add_command(label="Find", accelerator="Ctrl+F", command=self.click_find)
menu.add_command(label="Auto rearrange selected", state=tk.DISABLED) self.app.master.bind_all("<Control-f>", self.click_find)
menu.add_separator() menu.add_command(label="Auto Grid", command=self.click_autogrid)
menu.add_command(label="Align to grid", state=tk.DISABLED) menu.add_command(label="IP Addresses", command=self.click_ip_config)
menu.add_separator() menu.add_command(label="MAC Addresses", command=self.click_mac_config)
menu.add_command(label="Traffic...", state=tk.DISABLED)
menu.add_command(label="IP addresses...", state=tk.DISABLED)
menu.add_command(label="MAC addresses...", state=tk.DISABLED)
menu.add_command(label="Build hosts file...", state=tk.DISABLED)
menu.add_command(label="Renumber nodes...", state=tk.DISABLED)
self.create_experimental_menu(menu)
self.create_topology_generator_menu(menu)
menu.add_command(label="Debugger...", state=tk.DISABLED)
self.add_cascade(label="Tools", menu=menu) 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 Create observer widget menu item and create the sub menu items inside
""" """
var = tk.StringVar(value="none") self.observers_menu = tk.Menu(widget_menu)
menu = tk.Menu(widget_menu) self.observers_menu.add_command(
menu.var = var label="Edit Observers", command=self.click_edit_observer_widgets
menu.add_command(
label="Edit Observers", command=self.menuaction.edit_observer_widgets
) )
menu.add_separator() self.observers_menu.add_separator()
menu.add_radiobutton( self.observers_menu.add_radiobutton(
label="None", label="None",
variable=var, variable=self.observers_var,
value="none", value="none",
command=lambda: self.app.core.set_observer(None), command=lambda: self.core.set_observer(None),
) )
for name in sorted(OBSERVERS): for name in sorted(OBSERVERS):
cmd = OBSERVERS[name] cmd = OBSERVERS[name]
menu.add_radiobutton( self.observers_menu.add_radiobutton(
label=name, label=name,
variable=var, variable=self.observers_var,
value=name, 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): self.observers_custom_index = self.observers_menu.index(tk.END) + 1
observer = self.app.core.custom_observers[name] self.draw_custom_observers()
menu.add_radiobutton( widget_menu.add_cascade(label="Observer Widgets", menu=self.observers_menu)
label=name,
variable=var,
value=name,
command=partial(self.app.core.set_observer, observer.cmd),
)
widget_menu.add_cascade(label="Observer Widgets", menu=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 Create adjacency menu item and the sub menu items inside
""" """
menu = tk.Menu(widget_menu) menu = tk.Menu(widget_menu)
menu.add_command(label="OSPFv2", state=tk.DISABLED) menu.add_command(label="Configure Adjacency", state=tk.DISABLED)
menu.add_command(label="OSPFv3", state=tk.DISABLED) menu.add_command(label="Enable OSPFv2?", state=tk.DISABLED)
menu.add_command(label="OSLR", state=tk.DISABLED) menu.add_command(label="Enable OSPFv3?", state=tk.DISABLED)
menu.add_command(label="OSLRv2", 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) 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 Create widget menu
""" """
menu = tk.Menu(self) menu = tk.Menu(self)
self.create_observer_widgets_menu(menu) self.create_observer_widgets_menu(menu)
self.create_adjacency_menu(menu) self.create_adjacency_menu(menu)
menu.add_checkbutton(label="Throughput", command=self.menuaction.throughput) self.create_throughput_menu(menu)
menu.add_separator()
menu.add_command(label="Configure Adjacency...", state=tk.DISABLED)
menu.add_command(
label="Configure Throughput...", command=self.menuaction.config_throughput
)
self.add_cascade(label="Widgets", menu=menu) self.add_cascade(label="Widgets", menu=menu)
def draw_session_menu(self): def draw_session_menu(self) -> None:
""" """
Create session menu Create session menu
""" """
menu = tk.Menu(self) menu = tk.Menu(self)
menu.add_command( menu.add_command(label="Sessions", command=self.click_sessions)
label="Sessions...", command=self.menuaction.session_change_sessions menu.add_command(label="Servers", command=self.click_servers)
) menu.add_command(label="Options", command=self.click_session_options)
menu.add_separator() menu.add_command(label="Hooks", command=self.click_hooks)
menu.add_command(label="Options...", command=self.menuaction.session_options)
menu.add_command(label="Servers...", command=self.menuaction.session_servers)
menu.add_command(label="Hooks...", command=self.menuaction.session_hooks)
menu.add_command(label="Reset Nodes", state=tk.DISABLED)
menu.add_command(label="Comments...", state=tk.DISABLED)
self.add_cascade(label="Session", menu=menu) self.add_cascade(label="Session", menu=menu)
def draw_help_menu(self): def draw_help_menu(self) -> None:
""" """
Create help menu Create help menu
""" """
menu = tk.Menu(self) menu = tk.Menu(self)
menu.add_command( menu.add_command(label="Core GitHub (www)", command=self.click_core_github)
label="Core GitHub (www)", command=self.menuaction.help_core_github menu.add_command(label="Core Documentation (www)", command=self.click_core_doc)
) menu.add_command(label="About", command=self.click_about)
menu.add_command(
label="Core Documentation (www)",
command=self.menuaction.help_core_documentation,
)
menu.add_command(label="About", command=self.menuaction.show_about)
self.add_cascade(label="Help", menu=menu) 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): if os.path.isfile(filename):
logging.debug("Open recent file %s", filename) logging.debug("Open recent file %s", filename)
self.menuaction.open_xml_task(filename) self.open_xml_task(filename)
else: else:
logging.warning("File does not exist %s", filename) 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) 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( self.recent_menu.add_command(
label=i, command=partial(self.open_recent_files, i) label=i, command=partial(self.open_recent_files, i)
) )
def save(self, event=None): def click_save(self, _event=None) -> None:
xml_file = self.app.core.xml_file xml_file = self.core.xml_file
if xml_file: if xml_file:
self.app.core.save_xml(xml_file) self.core.save_xml(xml_file)
else: else:
self.menuaction.file_save_as_xml() self.click_save_xml()
def execute_python(self): def click_save_xml(self, _event: tk.Event = None) -> None:
dialog = ExecutePythonDialog(self.app, self.app) 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() dialog.show()
def change_menubar_item_state(self, is_runtime: bool): def add_recent_file_to_gui_config(self, file_path) -> None:
for i in range(self.edit_menu.index("end")): 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: try:
label_name = self.edit_menu.entrycget(i, "label") label = self.edit_menu.entrycget(i, "label")
if label_name in ["Copy", "Paste"]: if label not in labels:
if is_runtime: continue
self.edit_menu.entryconfig(i, state="disabled") state = tk.DISABLED if is_runtime else tk.NORMAL
else: self.edit_menu.entryconfig(i, state=state)
self.edit_menu.entryconfig(i, state="normal")
except tk.TclError: 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 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 from core.gui.images import ImageEnum, Images, TypeToImage
if TYPE_CHECKING: if TYPE_CHECKING:
@ -41,16 +42,16 @@ class NodeDraw:
return node_draw return node_draw
@classmethod @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 = NodeDraw()
node_draw.custom = True node_draw.custom = True
node_draw.image_file = image_file node_draw.image_file = custom_node.image
node_draw.image = Images.get_custom(image_file, ICON_SIZE) node_draw.image = Images.get_custom(custom_node.image, ICON_SIZE)
node_draw.node_type = NodeType.DEFAULT node_draw.node_type = NodeType.DEFAULT
node_draw.services = services node_draw.services = custom_node.services
node_draw.label = name node_draw.label = custom_node.name
node_draw.model = name node_draw.model = custom_node.name
node_draw.tooltip = name node_draw.tooltip = custom_node.name
return node_draw return node_draw
@ -64,8 +65,13 @@ class NodeUtils:
RJ45_NODES = {NodeType.RJ45} RJ45_NODES = {NodeType.RJ45}
IGNORE_NODES = {NodeType.CONTROL_NET, NodeType.PEER_TO_PEER} IGNORE_NODES = {NodeType.CONTROL_NET, NodeType.PEER_TO_PEER}
NODE_MODELS = {"router", "host", "PC", "mdr", "prouter"} NODE_MODELS = {"router", "host", "PC", "mdr", "prouter"}
ROUTER_NODES = {"router", "mdr"}
ANTENNA_ICON = None 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 @classmethod
def is_ignore_node(cls, node_type: NodeType) -> bool: def is_ignore_node(cls, node_type: NodeType) -> bool:
return node_type in cls.IGNORE_NODES return node_type in cls.IGNORE_NODES
@ -92,11 +98,7 @@ class NodeUtils:
@classmethod @classmethod
def node_icon( def node_icon(
cls, cls, node_type: NodeType, model: str, gui_config: GuiConfig, scale=1.0
node_type: NodeType,
model: str,
gui_config: Dict[str, List[Dict[str, str]]],
scale=1.0,
) -> "ImageTk.PhotoImage": ) -> "ImageTk.PhotoImage":
image_enum = TypeToImage.get(node_type, model) image_enum = TypeToImage.get(node_type, model)
@ -109,10 +111,7 @@ class NodeUtils:
@classmethod @classmethod
def node_image( def node_image(
cls, cls, core_node: "core_pb2.Node", gui_config: GuiConfig, scale=1.0
core_node: "core_pb2.Node",
gui_config: Dict[str, List[Dict[str, str]]],
scale=1.0,
) -> "ImageTk.PhotoImage": ) -> "ImageTk.PhotoImage":
image = cls.node_icon(core_node.type, core_node.model, gui_config, scale) image = cls.node_icon(core_node.type, core_node.model, gui_config, scale)
if core_node.icon: if core_node.icon:
@ -127,20 +126,17 @@ class NodeUtils:
return node_type == NodeType.DEFAULT and model not in cls.NODE_MODELS return node_type == NodeType.DEFAULT and model not in cls.NODE_MODELS
@classmethod @classmethod
def get_custom_node_services( def get_custom_node_services(cls, gui_config: GuiConfig, name: str) -> List[str]:
cls, gui_config: Dict[str, List[Dict[str, str]]], name: str for custom_node in gui_config.nodes:
) -> List[str]: if custom_node.name == name:
for m in gui_config["nodes"]: return custom_node.services
if m["name"] == name:
return m["services"]
return [] return []
@classmethod @classmethod
def get_image_file(cls, gui_config, name: str) -> Union[str, None]: def get_image_file(cls, gui_config: GuiConfig, name: str) -> Optional[str]:
if "nodes" in gui_config: for custom_node in gui_config.nodes:
for m in gui_config["nodes"]: if custom_node.name == name:
if m["name"] == name: return custom_node.image
return m["image"]
return None return None
@classmethod @classmethod
@ -172,9 +168,3 @@ class NodeUtils:
cls.NETWORK_NODES.append(node_draw) cls.NETWORK_NODES.append(node_draw)
cls.NODE_ICONS[(node_type, None)] = node_draw.image cls.NODE_ICONS[(node_type, None)] = node_draw.image
cls.ANTENNA_ICON = Images.get(ImageEnum.ANTENNA, ANTENNA_SIZE) 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): 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) super().__init__(master, **kwargs)
self.app = app self.app = app
self.status = None self.status = None
self.statusvar = tk.StringVar() self.statusvar = tk.StringVar()
self.progress_bar = None
self.zoom = None self.zoom = None
self.cpu_usage = None self.cpu_usage = None
self.memory = None self.memory = None
@ -28,19 +27,14 @@ class StatusBar(ttk.Frame):
self.draw() self.draw()
def draw(self): def draw(self):
self.columnconfigure(0, weight=1) self.columnconfigure(0, weight=7)
self.columnconfigure(1, weight=5) self.columnconfigure(1, weight=1)
self.columnconfigure(2, weight=1) self.columnconfigure(2, weight=1)
self.columnconfigure(3, weight=1) self.columnconfigure(3, weight=1)
self.columnconfigure(4, weight=1)
frame = ttk.Frame(self, borderwidth=1, relief=tk.RIDGE) frame = ttk.Frame(self, borderwidth=1, relief=tk.RIDGE)
frame.grid(row=0, column=0, sticky="ew") frame.grid(row=0, column=0, sticky="ew")
frame.columnconfigure(0, weight=1) 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.status = ttk.Label(
self, self,
@ -49,7 +43,7 @@ class StatusBar(ttk.Frame):
borderwidth=1, borderwidth=1,
relief=tk.RIDGE, 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.zoom = ttk.Label(
self, self,
@ -58,20 +52,20 @@ class StatusBar(ttk.Frame):
borderwidth=1, borderwidth=1,
relief=tk.RIDGE, 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.cpu_usage = ttk.Label(
self, text="CPU TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE 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.alerts_button = ttk.Button(
self, text="Alerts", command=self.click_alerts, style=Styles.green_alert 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): def click_alerts(self):
dialog = AlertsDialog(self.app, self.app) dialog = AlertsDialog(self.app)
dialog.show() dialog.show()
def set_status(self, message: str): def set_status(self, message: str):

View file

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

View file

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

View file

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

View file

@ -3,169 +3,114 @@ input validation
""" """
import re import re
import tkinter as tk import tkinter as tk
from typing import TYPE_CHECKING from tkinter import ttk
import netaddr
from netaddr import IPNetwork
if TYPE_CHECKING:
from core.gui.app import Application
SMALLEST_SCALE = 0.5 SMALLEST_SCALE = 0.5
LARGEST_SCALE = 5.0 LARGEST_SCALE = 5.0
HEX_REGEX = re.compile("^([#]([0-9]|[a-f])+)$|^[#]$")
class InputValidation: class ValidationEntry(ttk.Entry):
def __init__(self, app: "Application"): empty = None
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()
def register(self): def __init__(self, master=None, widget=None, empty_enabled=True, **kwargs) -> None:
self.positive_int = self.master.register(self.check_positive_int) super().__init__(master, widget, **kwargs)
self.positive_float = self.master.register(self.check_positive_float) cmd = self.register(self.is_valid)
self.app_scale = self.master.register(self.check_scale_value) self.configure(validate="key", validatecommand=(cmd, "%P"))
self.name = self.master.register(self.check_node_name) if self.empty is not None and empty_enabled:
self.ip4 = self.master.register(self.check_ip4) self.bind("<FocusOut>", self.focus_out)
self.rgb = self.master.register(self.check_rbg)
self.hex = self.master.register(self.check_hex)
@classmethod def is_valid(self, s: str) -> bool:
def ip_focus_out(cls, event: tk.Event): raise NotImplementedError
value = event.widget.get()
try:
IPNetwork(value)
except netaddr.core.AddrFormatError:
event.widget.delete(0, tk.END)
event.widget.insert(tk.END, "invalid")
@classmethod def focus_out(self, _event: tk.Event) -> None:
def focus_out(cls, event: tk.Event, default: str): value = self.get()
value = event.widget.get() if not value:
if value == "": self.insert(tk.END, self.empty)
event.widget.insert(tk.END, default)
@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 class PositiveIntEntry(ValidationEntry):
def check_positive_float(cls, s: str) -> bool: empty = "0"
if len(s) == 0:
return True
try:
float_value = float(s)
if float_value >= 0.0:
return True
return False
except ValueError:
return False
@classmethod def is_valid(self, s: str) -> bool:
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:
if not s: if not s:
return True return True
try: try:
float_value = float(s) value = int(s)
if float_value >= 0.0: return value >= 0
return True
return False
except ValueError: except ValueError:
return False 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: if not s:
return True return True
try: try:
float_value = float(s) value = float(s)
if SMALLEST_SCALE <= float_value <= LARGEST_SCALE or float_value == 0: return value >= 0.0
return True
return False
except ValueError: except ValueError:
return False 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: if not s:
return True return True
pat = re.compile("^([0-9]+[.])*[0-9]*$") try:
if pat.match(s) is not None: float(s)
_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
return True return True
else: except ValueError:
return False return False
@classmethod
def check_rbg(cls, s: str) -> bool: class RgbEntry(ValidationEntry):
def is_valid(self, s: str) -> bool:
if not s: if not s:
return True return True
if s.startswith("0") and len(s) >= 2: if s.startswith("0") and len(s) >= 2:
return False return False
try: try:
value = int(s) value = int(s)
if 0 <= value <= 255: return 0 <= value <= 255
return True
else:
return False
except ValueError: except ValueError:
return False return False
@classmethod
def check_hex(cls, s: str) -> bool: class HexEntry(ValidationEntry):
def is_valid(self, s: str) -> bool:
if not s: if not s:
return True return True
pat = re.compile("^([#]([0-9]|[a-f])+)$|^[#]$") if HEX_REGEX.match(s):
if pat.match(s): return 0 <= len(s) <= 7
if 0 <= len(s) <= 7:
return True
else:
return False
else: else:
return False 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 typing import TYPE_CHECKING, Dict
from core.api.grpc import common_pb2, core_pb2 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 from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING: if TYPE_CHECKING:
@ -127,43 +127,16 @@ class ConfigFrame(ttk.Notebook):
button = ttk.Button(file_frame, text="...", command=func) button = ttk.Button(file_frame, text="...", command=func)
button.grid(row=0, column=1) button.grid(row=0, column=1)
else: else:
if "controlnet" in option.name and "script" not in option.name: entry = ttk.Entry(tab.frame, textvariable=value)
entry = ttk.Entry( entry.grid(row=index, column=1, sticky="ew")
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")
elif option.type in INT_TYPES: elif option.type in INT_TYPES:
value.set(option.value) value.set(option.value)
entry = ttk.Entry( entry = validation.PositiveIntEntry(tab.frame, textvariable=value)
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.grid(row=index, column=1, sticky="ew") entry.grid(row=index, column=1, sticky="ew")
elif option.type == core_pb2.ConfigOptionType.FLOAT: elif option.type == core_pb2.ConfigOptionType.FLOAT:
value.set(option.value) value.set(option.value)
entry = ttk.Entry( entry = validation.PositiveFloatEntry(tab.frame, textvariable=value)
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.grid(row=index, column=1, sticky="ew") entry.grid(row=index, column=1, sticky="ew")
else: else:
logging.error("unhandled config option type: %s", option.type) logging.error("unhandled config option type: %s", option.type)
@ -181,7 +154,6 @@ class ConfigFrame(ttk.Notebook):
option.value = "0" option.value = "0"
else: else:
option.value = config_value option.value = config_value
return {x: self.config[x].value for x in self.config} return {x: self.config[x].value for x in self.config}
def set_values(self, config: Dict[str, str]) -> None: 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 = ttk.Scrollbar(self, orient=tk.VERTICAL)
self.scrollbar.grid(row=0, column=1, sticky="ns") self.scrollbar.grid(row=0, column=1, sticky="ns")
self.listbox = tk.Listbox( 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) themes.style_listbox(self.listbox)
self.listbox.grid(row=0, column=0, sticky="nsew") self.listbox.grid(row=0, column=0, sticky="nsew")

View file

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

View file

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

View file

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

View file

@ -33,7 +33,6 @@ NODE_LAYER = "CORE::Nodes"
LINK_LAYER = "CORE::Links" LINK_LAYER = "CORE::Links"
CORE_LAYERS = [CORE_LAYER, LINK_LAYER, NODE_LAYER] CORE_LAYERS = [CORE_LAYER, LINK_LAYER, NODE_LAYER]
DEFAULT_LINK_COLOR = "red" DEFAULT_LINK_COLOR = "red"
LINK_COLORS = ["green", "blue", "orange", "purple", "white"]
class Sdt: class Sdt:
@ -73,7 +72,6 @@ class Sdt:
self.url = self.DEFAULT_SDT_URL self.url = self.DEFAULT_SDT_URL
self.address = None self.address = None
self.protocol = None self.protocol = None
self.colors = {}
self.network_layers = set() self.network_layers = set()
self.session.node_handlers.append(self.handle_node_update) self.session.node_handlers.append(self.handle_node_update)
self.session.link_handlers.append(self.handle_link_update) self.session.link_handlers.append(self.handle_link_update)
@ -180,7 +178,6 @@ class Sdt:
self.cmd(f"delete layer,{layer}") self.cmd(f"delete layer,{layer}")
self.disconnect() self.disconnect()
self.network_layers.clear() self.network_layers.clear()
self.colors.clear()
def cmd(self, cmdstr: str) -> bool: def cmd(self, cmdstr: str) -> bool:
""" """
@ -353,24 +350,6 @@ class Sdt:
pass pass
return result 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( def add_link(
self, node_one: int, node_two: int, network_id: int = None, label: str = None self, node_one: int, node_two: int, network_id: int = None, label: str = None
) -> None: ) -> None:
@ -388,7 +367,10 @@ class Sdt:
return return
if self.wireless_net_check(node_one) or self.wireless_net_check(node_two): if self.wireless_net_check(node_one) or self.wireless_net_check(node_two):
return 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) link_id = get_link_id(node_one, node_two, network_id)
layer = LINK_LAYER layer = LINK_LAYER
if network_id: if network_id:

View file

@ -409,7 +409,6 @@ class CoreServices:
"using default services for node(%s) type(%s)", node.name, node_type "using default services for node(%s) type(%s)", node.name, node_type
) )
services = self.default_services.get(node_type, []) services = self.default_services.get(node_type, [])
logging.info("setting services for node(%s): %s", node.name, services) logging.info("setting services for node(%s): %s", node.name, services)
for service_name in services: for service_name in services:
service = self.get_service(node.id, service_name, default_service=True) 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): if netaddr.valid_ipv4(a):
return a return a
# raise ValueError, "no IPv4 address found for router ID" # raise ValueError, "no IPv4 address found for router ID"
return "0.0.0.0" return "0.0.0.%d" % node.id
@staticmethod @staticmethod
def rj45check(ifc): def rj45check(ifc):
@ -348,7 +348,19 @@ class Ospfv2(QuaggaService):
@classmethod @classmethod
def generatequaggaifcconfig(cls, node, ifc): 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): class Ospfv3(QuaggaService):

View file

@ -40,10 +40,15 @@ class OvsService(SdnService):
cfg = "#!/bin/sh\n" cfg = "#!/bin/sh\n"
cfg += "# auto-generated by OvsService (OvsService.py)\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 += "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(): for ifc in node.netifs():
if hasattr(ifc, "control") and ifc.control is True: if hasattr(ifc, "control") and ifc.control is True:
continue continue
@ -51,9 +56,8 @@ class OvsService(SdnService):
ifnum = ifnumstr[0] ifnum = ifnumstr[0]
# create virtual interfaces # 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 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 # 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 # 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) raise ValueError("invalid address: %s" % ifcaddr)
# add interfaces to bridge # add interfaces to bridge
cfg += "ovs-vsctl add-port ovsbr0 eth%s\n" % ifnum # Make port numbers explicit so they're easier to follow in reading the script
cfg += "ovs-vsctl add-port ovsbr0 sw%s\n" % ifnum 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) # 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 += "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 # Setup default flows
portnum = 1 portnum = 1
for ifc in node.netifs(): for ifc in node.netifs():
if hasattr(ifc, "control") and ifc.control is True: if hasattr(ifc, "control") and ifc.control is True:
continue continue
cfg += "## Take the data from the CORE interface and put it on the veth and vice versa\n"
cfg += ( cfg += (
"ovs-ofctl add-flow ovsbr0 priority=1000,in_port=%d,action=output:%d\n" "ovs-ofctl add-flow ovsbr0 priority=1000,in_port=%d,action=output:%d\n"
% (portnum, portnum + 1) % (portnum, portnum + 1)

View file

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

View file

@ -437,8 +437,7 @@ def random_mac() -> str:
""" """
value = random.randint(0, 0xFFFFFF) value = random.randint(0, 0xFFFFFF)
value |= 0x00163E << 24 value |= 0x00163E << 24
mac = netaddr.EUI(value) mac = netaddr.EUI(value, dialect=netaddr.mac_unix_expanded)
mac.dialect = netaddr.mac_unix_expanded
return str(mac) return str(mac)
@ -450,8 +449,7 @@ def validate_mac(value: str) -> str:
:return: unix formatted mac :return: unix formatted mac
""" """
try: try:
mac = netaddr.EUI(value) mac = netaddr.EUI(value, dialect=netaddr.mac_unix_expanded)
mac.dialect = netaddr.mac_unix_expanded
return str(mac) return str(mac)
except netaddr.AddrFormatError as e: except netaddr.AddrFormatError as e:
raise CoreError(f"invalid mac address {value}: {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.data import LinkData
from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions
from core.emulator.enumerations import EventTypes, NodeTypes from core.emulator.enumerations import EventTypes, NodeTypes
from core.errors import CoreXmlError
from core.nodes.base import CoreNetworkBase, CoreNodeBase, NodeBase from core.nodes.base import CoreNetworkBase, CoreNodeBase, NodeBase
from core.nodes.docker import DockerNode from core.nodes.docker import DockerNode
from core.nodes.lxd import LxcNode from core.nodes.lxd import LxcNode
from core.nodes.network import CtrlNet from core.nodes.network import CtrlNet, WlanNode
from core.services.coreservices import CoreService from core.services.coreservices import CoreService
if TYPE_CHECKING: if TYPE_CHECKING:
@ -162,14 +163,12 @@ class ServiceElement:
self.element.append(directories) self.element.append(directories)
def add_files(self) -> None: def add_files(self) -> None:
# get custom files
file_elements = etree.Element("files") file_elements = etree.Element("files")
for file_name in self.service.config_data: for file_name in self.service.config_data:
data = self.service.config_data[file_name] data = self.service.config_data[file_name]
file_element = etree.SubElement(file_elements, "file") file_element = etree.SubElement(file_elements, "file")
add_attribute(file_element, "name", file_name) add_attribute(file_element, "name", file_name)
file_element.text = data file_element.text = etree.CDATA(data)
if file_elements.getchildren(): if file_elements.getchildren():
self.element.append(file_elements) self.element.append(file_elements)
@ -313,7 +312,7 @@ class CoreXmlWriter:
def write_session_hooks(self) -> None: def write_session_hooks(self) -> None:
# hook scripts # hook scripts
hooks = etree.Element("session_hooks") 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]: for file_name, data in self.session._hooks[state]:
hook = etree.SubElement(hooks, "hook") hook = etree.SubElement(hooks, "hook")
add_attribute(hook, "name", file_name) add_attribute(hook, "name", file_name)
@ -560,26 +559,31 @@ class CoreXmlWriter:
) )
link_element.append(interface_two) link_element.append(interface_two)
# check for options # check for options, don't write for emane/wlan links
options = etree.Element("options") node_one = self.session.get_node(link_data.node1_id)
add_attribute(options, "delay", link_data.delay) node_two = self.session.get_node(link_data.node2_id)
add_attribute(options, "bandwidth", link_data.bandwidth) is_node_one_wireless = isinstance(node_one, (WlanNode, EmaneNet))
add_attribute(options, "per", link_data.per) is_node_two_wireless = isinstance(node_two, (WlanNode, EmaneNet))
add_attribute(options, "dup", link_data.dup) if not any([is_node_one_wireless, is_node_two_wireless]):
add_attribute(options, "jitter", link_data.jitter) options = etree.Element("options")
add_attribute(options, "mer", link_data.mer) add_attribute(options, "delay", link_data.delay)
add_attribute(options, "burst", link_data.burst) add_attribute(options, "bandwidth", link_data.bandwidth)
add_attribute(options, "mburst", link_data.mburst) add_attribute(options, "per", link_data.per)
add_attribute(options, "type", link_data.link_type) add_attribute(options, "dup", link_data.dup)
add_attribute(options, "gui_attributes", link_data.gui_attributes) add_attribute(options, "jitter", link_data.jitter)
add_attribute(options, "unidirectional", link_data.unidirectional) add_attribute(options, "mer", link_data.mer)
add_attribute(options, "emulation_id", link_data.emulation_id) add_attribute(options, "burst", link_data.burst)
add_attribute(options, "network_id", link_data.network_id) add_attribute(options, "mburst", link_data.mburst)
add_attribute(options, "key", link_data.key) add_attribute(options, "type", link_data.link_type)
add_attribute(options, "opaque", link_data.opaque) add_attribute(options, "gui_attributes", link_data.gui_attributes)
add_attribute(options, "session", link_data.session) add_attribute(options, "unidirectional", link_data.unidirectional)
if options.items(): add_attribute(options, "emulation_id", link_data.emulation_id)
link_element.append(options) 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 return link_element
@ -602,8 +606,8 @@ class CoreXmlReader:
self.read_service_configs() self.read_service_configs()
self.read_mobility_configs() self.read_mobility_configs()
self.read_emane_global_config() self.read_emane_global_config()
self.read_emane_configs()
self.read_nodes() self.read_nodes()
self.read_emane_configs()
self.read_configservice_configs() self.read_configservice_configs()
self.read_links() self.read_links()
@ -639,14 +643,14 @@ class CoreXmlReader:
session_options = self.scenario.find("session_options") session_options = self.scenario.find("session_options")
if session_options is None: if session_options is None:
return return
xml_config = {}
configs = {} for configuration in session_options.iterchildren():
for config in session_options.iterchildren(): name = configuration.get("name")
name = config.get("name") value = configuration.get("value")
value = config.get("value") xml_config[name] = value
configs[name] = value logging.info("reading session options: %s", xml_config)
logging.info("reading session options: %s", configs) config = self.session.options.get_configs()
self.session.options.set_configs(configs) config.update(xml_config)
def read_session_hooks(self) -> None: def read_session_hooks(self) -> None:
session_hooks = self.scenario.find("session_hooks") session_hooks = self.scenario.find("session_hooks")
@ -756,6 +760,18 @@ class CoreXmlReader:
model_name = emane_configuration.get("model") model_name = emane_configuration.get("model")
configs = {} 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") mac_configuration = emane_configuration.find("mac")
for config in mac_configuration.iterchildren(): for config in mac_configuration.iterchildren():
name = config.get("name") name = config.get("name")

View file

@ -10,7 +10,7 @@
# extra cruft to remove # extra cruft to remove
DISTCLEANFILES = conf.py Makefile Makefile.in stamp-vti *.rst DISTCLEANFILES = conf.py Makefile Makefile.in stamp-vti *.rst
all: index.rst all: html
# auto-generated Python documentation using Sphinx # auto-generated Python documentation using Sphinx
index.rst: 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, # 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, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # 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, # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format. # 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 logging
import distributed_parser
from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emane.ieee80211abg import EmaneIeee80211abgModel
from core.emulator.coreemu import CoreEmu from core.emulator.coreemu import CoreEmu
from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.emudata import IpPrefixes, NodeOptions
from core.emulator.enumerations import EventTypes, NodeTypes 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): def main(args):
# ip generator for example # ip generator for example
prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16")
@ -55,5 +74,5 @@ def main(args):
if __name__ == "__main__": if __name__ == "__main__":
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
args = distributed_parser.parse(__file__) args = parse(__file__)
main(args) 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 logging
import distributed_parser
from core.emulator.coreemu import CoreEmu from core.emulator.coreemu import CoreEmu
from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.emudata import IpPrefixes, NodeOptions
from core.emulator.enumerations import EventTypes, NodeTypes 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): def main(args):
# ip generator for example # ip generator for example
prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16")
@ -44,5 +63,5 @@ def main(args):
if __name__ == "__main__": if __name__ == "__main__":
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
args = distributed_parser.parse(__file__) args = parse(__file__)
main(args) 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 logging
import distributed_parser
from core.emulator.coreemu import CoreEmu from core.emulator.coreemu import CoreEmu
from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.emudata import IpPrefixes, NodeOptions
from core.emulator.enumerations import EventTypes 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): def main(args):
# ip generator for example # ip generator for example
prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16")
@ -44,5 +63,5 @@ def main(args):
if __name__ == "__main__": if __name__ == "__main__":
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
args = distributed_parser.parse(__file__) args = parse(__file__)
main(args) 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 logging
import distributed_parser
from core.emulator.coreemu import CoreEmu from core.emulator.coreemu import CoreEmu
from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.emudata import IpPrefixes, NodeOptions
from core.emulator.enumerations import EventTypes, NodeTypes 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): def main(args):
# ip generator for example # ip generator for example
prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16")
@ -48,5 +67,5 @@ def main(args):
if __name__ == "__main__": if __name__ == "__main__":
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
args = distributed_parser.parse(__file__) args = parse(__file__)
main(args) 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 logging
import parser import time
from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emane.ieee80211abg import EmaneIeee80211abgModel
from core.emulator.coreemu import CoreEmu from core.emulator.coreemu import CoreEmu
from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.emudata import IpPrefixes, NodeOptions
from core.emulator.enumerations import EventTypes, NodeTypes from core.emulator.enumerations import EventTypes, NodeTypes
NODES = 2
EMANE_DELAY = 10
def example(args):
def main():
# ip generator for example # ip generator for example
prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") 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 # must be in configuration state for nodes to start, when using "node_add" below
session.set_state(EventTypes.CONFIGURATION_STATE) 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) session.set_location(47.57917, -122.13232, 2.00000, 1.0)
options = NodeOptions() options = NodeOptions()
options.set_position(80, 50) 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) session.emane.set_model(emane_network, EmaneIeee80211abgModel)
# create nodes # create nodes
options = NodeOptions(model="mdr") options = NodeOptions(model="mdr")
for i in range(args.nodes): for i in range(NODES):
node = session.add_node(options=options) node = session.add_node(options=options)
node.setposition(x=150 * (i + 1), y=150) node.setposition(x=150 * (i + 1), y=150)
interface = prefixes.create_interface(node) interface = prefixes.create_interface(node)
@ -37,21 +46,22 @@ def example(args):
# instantiate session # instantiate session
session.instantiate() 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 # shutdown session
input("press enter to exit...")
coreemu.shutdown() 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__": if __name__ == "__main__" or __name__ == "__builtin__":
logging.basicConfig(level=logging.INFO)
main() 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.coreemu import CoreEmu
from core.emulator.emudata import IpPrefixes from core.emulator.emudata import IpPrefixes
from core.emulator.enumerations import EventTypes, NodeTypes from core.emulator.enumerations import EventTypes, NodeTypes
NODES = 2
def example(args):
def main():
# ip generator for example # ip generator for example
prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16") prefixes = IpPrefixes(ip4_prefix="10.83.0.0/16")
@ -19,10 +24,10 @@ def example(args):
session.set_state(EventTypes.CONFIGURATION_STATE) session.set_state(EventTypes.CONFIGURATION_STATE)
# create switch network node # create switch network node
switch = session.add_node(_type=NodeTypes.SWITCH) switch = session.add_node(_type=NodeTypes.SWITCH, _id=100)
# create nodes # create nodes
for _ in range(args.nodes): for _ in range(NODES):
node = session.add_node() node = session.add_node()
interface = prefixes.create_interface(node) interface = prefixes.create_interface(node)
session.add_link(node.id, switch.id, interface_one=interface) session.add_link(node.id, switch.id, interface_one=interface)
@ -31,27 +36,17 @@ def example(args):
session.instantiate() session.instantiate()
# get nodes to run example # get nodes to run example
first_node = session.get_node(2) first_node = session.get_node(1)
last_node = session.get_node(args.nodes + 1) last_node = session.get_node(NODES)
first_node_address = prefixes.ip4_address(first_node) address = prefixes.ip4_address(first_node)
logging.info("node %s pinging %s", last_node.name, first_node_address) logging.info("node %s pinging %s", last_node.name, address)
output = last_node.cmd(f"ping -c {args.count} {first_node_address}") output = last_node.cmd(f"ping -c 3 {address}")
logging.info(output) logging.info(output)
# shutdown session # shutdown session
coreemu.shutdown() 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__": if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
main() 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 import logging
from core.emulator.emudata import IpPrefixes from core.emulator.emudata import IpPrefixes
from core.emulator.enumerations import EventTypes, NodeTypes from core.emulator.enumerations import EventTypes, NodeTypes
NODES = 2
def example(nodes):
def main():
# ip generator for example # ip generator for example
prefixes = IpPrefixes("10.83.0.0/16") prefixes = IpPrefixes("10.83.0.0/16")
@ -19,7 +28,7 @@ def example(nodes):
switch = session.add_node(_type=NodeTypes.SWITCH) switch = session.add_node(_type=NodeTypes.SWITCH)
# create nodes # create nodes
for _ in range(nodes): for _ in range(NODES):
node = session.add_node() node = session.add_node()
interface = prefixes.create_interface(node) interface = prefixes.create_interface(node)
session.add_link(node.id, switch.id, interface_one=interface) session.add_link(node.id, switch.id, interface_one=interface)
@ -30,4 +39,4 @@ def example(nodes):
if __name__ in {"__main__", "__builtin__"}: if __name__ in {"__main__", "__builtin__"}:
logging.basicConfig(level=logging.INFO) 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.coreemu import CoreEmu
from core.emulator.emudata import IpPrefixes, NodeOptions from core.emulator.emudata import IpPrefixes, NodeOptions
from core.emulator.enumerations import EventTypes, NodeTypes from core.emulator.enumerations import EventTypes, NodeTypes
from core.location.mobility import BasicRangeModel from core.location.mobility import BasicRangeModel
NODES = 2
def example(args):
def main():
# ip generator for example # ip generator for example
prefixes = IpPrefixes("10.83.0.0/16") prefixes = IpPrefixes("10.83.0.0/16")
@ -20,13 +25,13 @@ def example(args):
session.set_state(EventTypes.CONFIGURATION_STATE) session.set_state(EventTypes.CONFIGURATION_STATE)
# create wlan network node # 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) session.mobility.set_model(wlan, BasicRangeModel)
# create nodes, must set a position for wlan basic range model # create nodes, must set a position for wlan basic range model
options = NodeOptions(model="mdr") options = NodeOptions(model="mdr")
options.set_position(0, 0) options.set_position(0, 0)
for _ in range(args.nodes): for _ in range(NODES):
node = session.add_node(options=options) node = session.add_node(options=options)
interface = prefixes.create_interface(node) interface = prefixes.create_interface(node)
session.add_link(node.id, wlan.id, interface_one=interface) session.add_link(node.id, wlan.id, interface_one=interface)
@ -35,27 +40,17 @@ def example(args):
session.instantiate() session.instantiate()
# get nodes for example run # get nodes for example run
first_node = session.get_node(2) first_node = session.get_node(1)
last_node = session.get_node(args.nodes + 1) last_node = session.get_node(NODES)
address = prefixes.ip4_address(first_node) address = prefixes.ip4_address(first_node)
logging.info("node %s pinging %s", last_node.name, address) 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) logging.info(output)
# shutdown session # shutdown session
coreemu.shutdown() 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__": if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
main() main()

View file

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

View file

@ -9,7 +9,7 @@ from core.gui.images import Images
if __name__ == "__main__": if __name__ == "__main__":
# parse flags # 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", parser.add_argument("-l", "--level", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], default="INFO",
help="logging level") help="logging level")
parser.add_argument("-p", "--proxy", action="store_true", help="enable proxy") parser.add_argument("-p", "--proxy", action="store_true", help="enable proxy")

View file

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

View file

@ -17,12 +17,17 @@ class TestXml:
:param session: session for test :param session: session for test
:param tmpdir: tmpdir to create data in :param tmpdir: tmpdir to create data in
""" """
# create hook # create hooks
file_name = "runtime_hook.sh" file_name = "runtime_hook.sh"
data = "#!/bin/sh\necho hello" data = "#!/bin/sh\necho hello"
state = EventTypes.RUNTIME_STATE state = EventTypes.RUNTIME_STATE
session.add_hook(state, file_name, None, data) 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 # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = xml_file.strpath 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 sudo python3 -m pipenv run core
# runs coretk gui # runs coretk gui
python3 -m pipenv run coretk python3 -m pipenv run core-pygui
# runs mocked unit tests # runs mocked unit tests
python3 -m pipenv run test-mock 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 as they are moved in the EMANE emulation. This would occur when an Emulation
Script Generator, for example, is running a mobility script. 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 ## EMANE Configuration
The CORE configuration file */etc/core/core.conf* has options specific to 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: EMANE. An example emane section from the **core.conf** file is shown below:
```shell ```shell
# EMANE configuration # EMANE configuration
@ -64,40 +81,28 @@ emane_event_monitor = False
# EMANE log level range [0,4] default: 2 # EMANE log level range [0,4] default: 2
emane_log_level = 2 emane_log_level = 2
emane_realtime = True emane_realtime = True
``` # prefix used for emane installation
# emane_prefix = /usr
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
``` ```
If you have an EMANE event generator (e.g. mobility or pathloss scripts) and 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 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 ```shell
emane_event_monitor = True 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 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 prefix will place the DTD files in **/usr/local/share/emane/dtd** while CORE
expects them in */usr/share/emane/dtd*. expects them in **/usr/share/emane/dtd**.
A symbolic link will fix this:
Update the EMANE prefix configuration to resolve this problem.
```shell ```shell
sudo ln -s /usr/local/share/emane /usr/share/emane emane_prefix = /usr/local
``` ```
## Custom EMANE Models ## 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| |[Architecture](architecture.md)|Overview of the architecture|
|[Installation](install.md)|How to install CORE and its requirements| |[Installation](install.md)|How to install CORE and its requirements|
|[GUI](gui.md)|How to use the GUI| |[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| |[Distributed](distributed.md)|Details for running CORE across multiple servers|
|[Python Scripting](scripting.md)|How to write python scripts for creating a CORE session| |[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| |[gRPC API](grpc.md)|How to enable and use the gRPC API|

View file

@ -6,6 +6,11 @@
## Overview ## Overview
This section will describe how to install CORE from source or from a pre-built package. 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 ## 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