commit
cc7c1348ce
146 changed files with 3897 additions and 3032 deletions
30
CHANGELOG.md
30
CHANGELOG.md
|
@ -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
|
||||||
|
|
23
configure.ac
23
configure.ac
|
@ -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])],
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -22,3 +22,11 @@ class CoreError(Exception):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CoreXmlError(Exception):
|
||||||
|
"""
|
||||||
|
Used when there was an error parsing a CORE xml file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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")
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
25
daemon/core/gui/dialogs/emaneinstall.py
Normal file
25
daemon/core/gui/dialogs/emaneinstall.py
Normal 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")
|
|
@ -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()
|
|
|
@ -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
|
||||||
|
|
157
daemon/core/gui/dialogs/find.py
Normal file
157
daemon/core/gui/dialogs/find.py
Normal 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)
|
|
@ -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
|
||||||
|
|
151
daemon/core/gui/dialogs/ipdialog.py
Normal file
151
daemon/core/gui/dialogs/ipdialog.py
Normal 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()
|
|
@ -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)
|
||||||
|
|
||||||
|
|
61
daemon/core/gui/dialogs/macdialog.py
Normal file
61
daemon/core/gui/dialogs/macdialog.py
Normal 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()
|
|
@ -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])
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
115
daemon/core/gui/dialogs/runtool.py
Normal file
115
daemon/core/gui/dialogs/runtool.py
Normal 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)
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
|
@ -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()
|
||||||
|
|
|
@ -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]))
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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")
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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|
|
||||||
|
|
|
@ -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
653
docs/pygui.md
Normal 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
Loading…
Reference in a new issue